Skip to content

draft: keyed subgraphs through send - #8819

Draft
Nuno Campos (nfcampos) wants to merge 4 commits into
langchain-ai:mainfrom
nfcampos:claude/langgraph-work-items-oxgi3k
Draft

draft: keyed subgraphs through send#8819
Nuno Campos (nfcampos) wants to merge 4 commits into
langchain-ai:mainfrom
nfcampos:claude/langgraph-work-items-oxgi3k

Conversation

@nfcampos

@nfcampos Nuno Campos (nfcampos) commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

https://claude.ai/code/artifact/befd728a-68be-470a-9bce-7efa2e25fd7f

langchain/deepagents should then be modified to pass this new send arg in tool calls

…as-is)

Prototype for the "per-thread subgraph limitation" design item. Adds a
`subgraph_key` configurable: when a subgraph is invoked with it, its
checkpoint namespace becomes `<parent frames>|:key` instead of one derived
from the invoking task id or the in-node call-order counter. Parallel
invocations with distinct keys never share state; a later invocation with
the same key continues that history. Works for both `checkpointer=None`
and `checkpointer=True` subgraphs.

Also:
- nested loops only honour the parent's RESUMING flag when their latest
  checkpoint was written under the same parent checkpoint (same invocation)
  or has unfinished work; fixes a stateful subgraph ignoring new input when
  invoked from a resumed task
- in-process guard: two concurrent loops on one addressable namespace
  raise instead of silently forking its history
- `recast_checkpoint_ns(keep_keys=...)`, parent-command namespace and
  replay first-visit logic understand `:key` segments
- exit-durability predicate treats keyed namespaces as addressable

Tests in tests/test_subgraph_key.py document the intended semantics,
including the two pre-existing bugs the design fixes (racy call-order
counter on resume; RESUMING leak into a stateful subgraph's new turn).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUNm3vKV4k9YP4wpvQcyUX
Revise the prototype after review:

- `checkpointer=` stays meaningful with a key. A per-invocation subgraph
  gets `call:<task_id>|:key` (key scoped to the invoking task, fresh each
  turn); a checkpointer=True subgraph gets `call|:key` (key names an
  addressable memory a later invocation continues).
- Drop the in-process concurrent-namespace guard.
- The call-order counter is now per key within a task (unkeyed invocations
  keep one counter per task, unchanged), so two subgraphs invoked under one
  injected key are told apart while an explicit key elsewhere is untouched.
- ToolNode gains `subgraph_key="tool_call_id" | "tool_name" | callable`,
  which sets the key on the config each tool runs with so graphs invoked
  inside tools are keyed without tool code. Default None keeps current
  behaviour.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUNm3vKV4k9YP4wpvQcyUX
Two tool calls in one ToolNode batch that resolve to the same subgraph_key
would run one subgraph memory concurrently and silently fork its history.
The batch always executes in one process, so refuse it before any tool
runs. Note this only covers batch mode: langchain's create_agent fans tool
calls out with Send, one ToolNode call each, so the equivalent check there
belongs in the edge that builds the Sends.

Also test that checkpointer=True subgraphs run unpersisted under a parent
without a checkpointer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUNm3vKV4k9YP4wpvQcyUX
…hey invoke

Revision 4. Move the harness-facing surface from ToolNode to Send, which is
how create_agent and create_react_agent fan tool calls out:

- `Send(node, arg, key=...)`: the task id derives from the key instead of the
  packet's position; two keyed packets for one node with the same key in one
  step raise InvalidUpdateError at task preparation (holds wherever tasks are
  prepared, no in-process assumption); the key is set as `subgraph_key` on
  the task's config so subgraphs invoked inside the task are keyed by it.
- A key now names an addressable namespace (`<parent frames>|:key`) in either
  persistence mode. `checkpointer=` keeps its jobs: False disables, and
  without a key it decides per-task vs per-calling-node naming.
- Send serialization round-trips the key; older readers drop it.
- Back out the ToolNode `subgraph_key` argument and batch duplicate check.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DUNm3vKV4k9YP4wpvQcyUX
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants