feat(sdk): add artifacts_root to CompositeBackend and middleware#2490
Conversation
…eware Add an optional `artifacts_root` parameter to `CompositeBackend` that propagates through `FilesystemMiddleware` and `SummarizationMiddleware` via `create_deep_agent`. This parameterizes the paths used for large tool result eviction (`/large_tool_results/`) and conversation history offloading (`/conversation_history/`), allowing callers to control where internal artifacts are written. Defaults to `"/"` for backwards compatibility.
| When a tool result is too large, it may be offloaded into the filesystem instead of being returned inline. In those cases, use `read_file` to inspect the saved result in chunks, or use `grep` within `/large_tool_results/` if you need to search across offloaded tool results and do not know the exact file path. Offloaded tool results are stored under `/large_tool_results/<tool_call_id>`.""" | ||
| When a tool result is too large, it may be offloaded into the filesystem instead of being returned inline. In those cases, use `read_file` to inspect the saved result in chunks, or use `grep` within `{large_tool_results_prefix}/` if you need to search across offloaded tool results and do not know the exact file path. Offloaded tool results are stored under `{large_tool_results_prefix}/<tool_call_id>`.""" | ||
|
|
||
| FILESYSTEM_SYSTEM_PROMPT = _FILESYSTEM_SYSTEM_PROMPT_TEMPLATE.format(large_tool_results_prefix="/large_tool_results") |
There was a problem hiding this comment.
is this just here for backwards-compat purposes now?
There was a problem hiding this comment.
yeah exactly. not sure we still need it
There was a problem hiding this comment.
should this also have info about offloading of conversation history
There was a problem hiding this comment.
yes probably
There was a problem hiding this comment.
ok talked with chester and we don't need this unless we can come up with an eval that would fail without it. the message for a large human message comes back with how to handle it
| tool_messages = [m for m in result["messages"] if m.type == "tool"] | ||
| evicted_msg = next(m for m in tool_messages if m.tool_call_id == "call_big") | ||
| assert "/workspace/large_tool_results/" in evicted_msg.content | ||
| assert mem_store.get(("filesystem",), "/workspace/large_tool_results/call_big") is not None |
There was a problem hiding this comment.
does backend.read work?
There was a problem hiding this comment.
i will add a test
There was a problem hiding this comment.
similar to my above comment, i think we should actually not interact w/ the store directly
| def test_create_deep_agent_no_composite_backend(self) -> None: | ||
| """create_deep_agent with a non-composite backend defaults artifacts_root to '/'.""" | ||
| model = FakeChatModelWithHistory(messages=iter([AIMessage(content="done")])) | ||
| agent = create_deep_agent(model=model, backend=StateBackend()) # noqa: F841 | ||
|
|
||
| def test_create_deep_agent_composite_backend_default_artifacts_root(self) -> None: | ||
| """create_deep_agent with CompositeBackend without artifacts_root defaults to '/'.""" | ||
| model = FakeChatModelWithHistory(messages=iter([AIMessage(content="done")])) | ||
| backend = CompositeBackend(default=StateBackend(), routes={}) | ||
| agent = create_deep_agent(model=model, backend=backend) # noqa: F841 |
There was a problem hiding this comment.
what are these tests enforcing?
| default: Backend for paths that don't match any route. | ||
| routes: Map of path prefixes to backends (e.g., {"/memories/": store_backend}). | ||
| sorted_routes: Routes sorted by length (longest first) for correct matching. | ||
| artifacts_root: Root path for artifacts. Defaults to `"/"`. |
There was a problem hiding this comment.
may make sense to flesh this out (e.g., "Root path for artifacts, such as messages offloaded by middleware").
There was a problem hiding this comment.
yeah make sense to me. i'll do a quick pass on all the doc strings
artifacts_root to CompositeBackend and middleware
Sydney Runkle (sydney-runkle)
left a comment
There was a problem hiding this comment.
high level, looks good, congrats on first pr!!
my one high level comment is that we might not need the artifact root on CompositeBackend if we allow folks to customize middlewares given that each middleware that uses offloading has their own artifacts path.
Curious, what was the customer use case motivating this feature?
| routes: dict[str, BackendProtocol], | ||
| *, | ||
| artifacts_root: str = "/", |
There was a problem hiding this comment.
hah just a side note but routes vs root is tough
| # Deep Agents specific attributes | ||
| self._backend = backend | ||
| self._history_path_prefix = history_path_prefix | ||
| self.artifacts_root = artifacts_root |
There was a problem hiding this comment.
why not private?
There was a problem hiding this comment.
changed this
| def _make_store_backend(): | ||
| mem_store = InMemoryStore() | ||
| backend = StoreBackend(store=mem_store, namespace=lambda _ctx: ("filesystem",)) | ||
| return backend, mem_store |
There was a problem hiding this comment.
i don't think we need the mem_store returned? we could just return the backend and then do backend.download_files?
There was a problem hiding this comment.
also changed this
| tool_messages = [m for m in result["messages"] if m.type == "tool"] | ||
| evicted_msg = next(m for m in tool_messages if m.tool_call_id == "call_big") | ||
| assert "/workspace/large_tool_results/" in evicted_msg.content | ||
| assert mem_store.get(("filesystem",), "/workspace/large_tool_results/call_big") is not None |
There was a problem hiding this comment.
similar to my above comment, i think we should actually not interact w/ the store directly
…te tests to not interact with store directly
| # Deep Agents specific attributes | ||
| self._backend = backend | ||
|
|
||
| self._artifacts_root = resolve_artifacts_root(backend) |
There was a problem hiding this comment.
nit, this is simple enough that let's just inline
self._artifacts_root = backend.artifacts_root if isinstance(backend, CompositeBackend) else "/"that way we don't have too much indirection
generally good idea to consolidate shared logic though
| assert isinstance(result, ToolMessage) | ||
| assert "/workspace/large_tool_results/evict_123" in result.content | ||
| [resp] = backend.download_files(["/workspace/large_tool_results/evict_123"]) | ||
| assert resp.content is not None |
There was a problem hiding this comment.
should we check what it has? similar for other tests
There was a problem hiding this comment.
done
| evicted_msg = next(m for m in tool_messages if m.tool_call_id == "call_big") | ||
| assert "/workspace/large_tool_results/" in evicted_msg.content | ||
| [resp] = backend.download_files(["/workspace/large_tool_results/call_big"]) | ||
| assert resp.content is not None |
There was a problem hiding this comment.
same let's confirm, this is important
There was a problem hiding this comment.
done
| # Truncate when 50% of context window reached, ignoring messages in last 10% of window | ||
| {"trigger": ("fraction", 0.5), "keep": ("fraction", 0.1), "max_length": 2000, "truncation_text": "...(truncated)"} | ||
| history_path_prefix: Path prefix for storing conversation history. | ||
| Derived from the backend's `artifacts_root` when not provided. |
There was a problem hiding this comment.
it still uses the backend root when provided right?
There was a problem hiding this comment.
or it should?
…ssertions, and remove shared method in utils since logic is simple enough
| self._artifacts_root = self.backend.artifacts_root if isinstance(self.backend, CompositeBackend) else "/" | ||
| _root = self._artifacts_root.rstrip("/") |
There was a problem hiding this comment.
last nit, any reason to actually set self._artifacts_root?
There was a problem hiding this comment.
not currently
| [resp] = backend.download_files(["/workspace/large_tool_results/evict_123"]) | ||
| assert resp.error is None | ||
| assert resp.content is not None | ||
| assert b"x" * 100 in resp.content |
There was a problem hiding this comment.
any reason not checking just for equivalence to large_content?
There was a problem hiding this comment.
no will do this
Sydney Runkle (sydney-runkle)
left a comment
There was a problem hiding this comment.
awesome!!
…m prompt, update tests to assert equality for downloaded files, and don't set _artifacts_root on summarization and filesystem middleware as a class variable
…ready in the instructions for too large human message
> [!CAUTION] > Merging this PR will automatically publish to **PyPI** and create a **GitHub release**. For the full release process, see [`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md). --- _Everything below this line will be the GitHub release body._ --- ## [0.5.1](deepagents==0.5.0...deepagents==0.5.1) (2026-04-07) ### Features * **sdk:** `BASE_AGENT_PROMPT` tweaks ([#2541](#2541)) ([812eef1](812eef1)) * **sdk:** add `artifacts_root` to `CompositeBackend` and middleware ([#2490](#2490)) ([753ee56](753ee56)) ### Bug Fixes * **sdk:** updates for multimodal ([#2514](#2514)) ([a2edf3e](a2edf3e)) --- _Everything above this line will be the GitHub release body._ --- > [!NOTE] > A **New Contributors** section is appended to the GitHub release notes automatically at publish time (see [Release Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline), step 2). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Port of langchain-ai/deepagents#2490. Adds optional artifactsRoot parameter to CompositeBackend that propagates through FilesystemMiddleware and SummarizationMiddleware, parameterizing paths for large tool result eviction and conversation history offloading. Defaults to "/" for backwards compat. Co-authored-by: Sydney Runkle <[email protected]>
Add an optional
artifacts_rootparameter toCompositeBackendthat propagates throughFilesystemMiddlewareandSummarizationMiddlewareviacreate_deep_agent. This parameterizes the paths used for large tool result eviction (/large_tool_results/) and conversation history offloading (/conversation_history/), allowing callers to control where internal artifacts are written. Defaults to"/"for backwards compatibility.