Skip to content

feat(sdk): add artifacts_root to CompositeBackend and middleware#2490

Merged
Nick Hollon (nick-hollon-lc) merged 11 commits into
mainfrom
nh/add-artifacts-root
Apr 7, 2026
Merged

feat(sdk): add artifacts_root to CompositeBackend and middleware#2490
Nick Hollon (nick-hollon-lc) merged 11 commits into
mainfrom
nh/add-artifacts-root

Conversation

@nick-hollon-lc

Copy link
Copy Markdown
Contributor

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.

…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.
@github-actions github-actions Bot added deepagents Related to the `deepagents` SDK / agent harness feature New feature/enhancement or request for one internal User is a member of the `langchain-ai` GitHub organization size: M 200-499 LOC labels Apr 6, 2026
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")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this just here for backwards-compat purposes now?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah exactly. not sure we still need it

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this also have info about offloading of conversation history

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes probably

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does backend.read work?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i will add a test

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to my above comment, i think we should actually not interact w/ the store directly

Comment on lines +2003 to +2012
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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 `"/"`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may make sense to flesh this out (e.g., "Root path for artifacts, such as messages offloaded by middleware").

@nick-hollon-lc Nick Hollon (nick-hollon-lc) Apr 6, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah make sense to me. i'll do a quick pass on all the doc strings

@mdrxy Mason Daugherty (mdrxy) changed the title feat(sdk): add artifacts_root parameter to CompositeBackend and middl… feat(sdk): add artifacts_root to CompositeBackend and middleware Apr 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Comment on lines 144 to +146
routes: dict[str, BackendProtocol],
*,
artifacts_root: str = "/",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not private?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed this

Comment on lines +15 to +18
def _make_store_backend():
mem_store = InMemoryStore()
backend = StoreBackend(store=mem_store, namespace=lambda _ctx: ("filesystem",))
return backend, mem_store

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think we need the mem_store returned? we could just return the backend and then do backend.download_files?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

similar to my above comment, i think we should actually not interact w/ the store directly

# Deep Agents specific attributes
self._backend = backend

self._artifacts_root = resolve_artifacts_root(backend)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we check what it has? similar for other tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same let's confirm, this is important

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it still uses the backend root when provided right?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

or it should?

…ssertions, and remove shared method in utils since logic is simple enough
@github-actions github-actions Bot added size: L 500-999 LOC and removed size: M 200-499 LOC labels Apr 7, 2026
Comment on lines +608 to +609
self._artifacts_root = self.backend.artifacts_root if isinstance(self.backend, CompositeBackend) else "/"
_root = self._artifacts_root.rstrip("/")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

last nit, any reason to actually set self._artifacts_root?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason not checking just for equivalence to large_content?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no will do this

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
@nick-hollon-lc
Nick Hollon (nick-hollon-lc) merged commit 753ee56 into main Apr 7, 2026
33 checks passed
@nick-hollon-lc
Nick Hollon (nick-hollon-lc) deleted the nh/add-artifacts-root branch April 7, 2026 20:44
Mason Daugherty (mdrxy) pushed a commit that referenced this pull request Apr 7, 2026
> [!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>
open-swe Bot pushed a commit to langchain-ai/deepagentsjs that referenced this pull request Apr 8, 2026
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]>
james8814 pushed a commit to james8814/deepagents that referenced this pull request May 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deepagents Related to the `deepagents` SDK / agent harness feature New feature/enhancement or request for one internal User is a member of the `langchain-ai` GitHub organization size: L 500-999 LOC

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants