Skip to content

Commit 14ffe98

Browse files
bloveclaude
andauthored
docs(langgraph): how child streams get matched to tool calls (#879)
The subagent-binding mechanism shipped across three packages (#869, middleware 0.0.2, @threadplane/langgraph 0.0.62) and two demos, but the subgraphs guide still described only the heuristic story. New section documents the three attribution tiers in preference order — server-announced binding, description ladder, single-candidate positional fallback — with the three-line `announce_subagent` tool change and its no-guarding-needed contract. Placed between "Subagent stream details" and "Orchestrator pattern", where the reader has just met SubagentStreamRef and is about to wire dispatch. One renderer correction along the way: `<Tip>` is a cockpit-guide component, not a website-docs one — prerender failed until it became `<Callout>`. Verified: website build green, suite 347/347. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent 0ea5bd9 commit 14ffe98

1 file changed

Lines changed: 34 additions & 0 deletions

File tree

apps/website/content/docs/langgraph/guides/subgraphs.mdx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,40 @@ const researchStatus = computed(() => researchAgent()?.status());
226226
const researchMessages = computed(() => researchAgent()?.messages() ?? []);
227227
```
228228

229+
## How child streams get matched to tool calls
230+
231+
A child graph invoked inside a `@tool` body streams under a `tools:<uuid>` namespace — and that uuid is a **checkpoint id**, assigned independently of the tool-call id. Nothing on the wire links the two. `injectAgent()` bridges the gap in three tiers, in order of preference:
232+
233+
1. **A server-announced binding** (exact, works under any concurrency — see below)
234+
2. **The description ladder**: the child's first human message is compared against each pending tool call's `description` argument — exact match, then substring in either direction
235+
3. **A positional fallback** that fires only when *exactly one* tool child is outstanding — with several in flight, arrival order is not dispatch order, and guessing would cross-wire the cards, so ambiguous streams stay buffered instead
236+
237+
Tier 3 covers the common sequential shape (one dispatch per assistant turn). For parallel fan-out, or a delegation tool whose argument isn't named `description`, announce the binding from the server — the tool body is the one place both halves are known:
238+
239+
```python
240+
from typing import Annotated
241+
242+
from langchain_core.runnables import RunnableConfig
243+
from langchain_core.tools import InjectedToolCallId, tool
244+
from threadplane.middleware.langgraph import announce_subagent
245+
246+
@tool
247+
async def task(
248+
description: str,
249+
tool_call_id: Annotated[str, InjectedToolCallId] = None,
250+
config: RunnableConfig = None,
251+
) -> str:
252+
announce_subagent(config, tool_call_id) # one line — before invoking the child
253+
result = await child_graph.ainvoke({...})
254+
...
255+
```
256+
257+
`announce_subagent` (threadplane-middleware ≥ 0.0.2) emits one custom event pairing the config's `checkpoint_ns` with the injected tool-call id. `injectAgent()` consumes it, attributes the stream exactly — replaying any chunks that arrived before the announcement — and never lets it override an established mapping. It returns `False` instead of raising when anything it needs is unavailable (outside a run, no namespace, no id), so it needs no guarding.
258+
259+
<Callout title="Announce even in sequential graphs">
260+
It costs one line, upgrades attribution from heuristic to exact, and your graph keeps working unchanged if you later parallelize dispatch.
261+
</Callout>
262+
229263
## Orchestrator pattern
230264

231265
The orchestrator pattern delegates specialised work to subagents and merges their results. Each subagent runs its own graph independently while the parent coordinates the whole.

0 commit comments

Comments
 (0)