Skip to content

fix(langgraph): don't guess subagent attribution when it's a coin flip - #864

Merged
blove merged 1 commit into
mainfrom
blove/subagent-attribution-ambiguity
Aug 30, 2026
Merged

fix(langgraph): don't guess subagent attribution when it's a coin flip#864
blove merged 1 commit into
mainfrom
blove/subagent-attribution-ambiguity

Conversation

@blove

@blove blove commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Follow-up to the attribution work in #847, which noted the positional fallback "works for sequential dispatch; a graph fanning out parallel look-alike children would lean on it hard." It does worse than lean — it cross-wires them.

Is there a real identity link? No.

I checked before assuming. LangGraph's tools:<uuid> namespace is a checkpoint id, assigned independently of the parent's call_* tool-call id:

langgraph_checkpoint_ns = tools:f61899a8-…|subagent:5f1fba3c-…
checkpoint_ns           = tools:f61899a8-…
langgraph_node          = subagent

No call_* anywhere in the child's metadata, and no payload in a full live run carries both a namespace uuid and a tool-call id. (One grep looked like a hit — call_ appearing inside prose content.) So when a delegation tool carries no matchable description, position is genuinely the only signal left. This can't be fixed client-side by finding a better key; there isn't one.

The actual bug

Position is sound with one outstanding child — and that's what every graph here produces. cockpit/chat/subagents dispatches one tool call per assistant turn, three sequential ToolNode round-trips (verified on the wire).

With several outstanding at once, arrival order is not dispatch order. Reproduced in a test: two children whose streams arrive in reverse dispatch order, and call_ALPHA's card renders beta output. A booking card showing research text is worse than an empty one — it's confidently wrong, and a reader has no way to tell.

The fix

The last-resort fallback now fires only when exactly one candidate is outstanding. Ambiguous streams stay buffered instead of mis-attributed, and can still resolve later: as siblings complete, the candidate set shrinks back to one. The description rungs are untouched and still preferred when a graph provides them.

The trade-off is deliberate — for a parallel fan-out with no descriptions, cards may stay empty. That's the honest failure mode. Silent cross-wiring is not.

Verification

  • 344/344, including the new cross-wiring guard.
  • Mutation-tested: restoring the greedy >= 1 fallback fails exactly that guard and nothing else.
  • Sequential path re-checked against a live model — this is what the narrowing could plausibly have broken. All three cards populate, each attributed to its own distinct call_* id.
  • cockpit-chat-subagents e2e green; lint 0 errors.

🤖 Generated with Claude Code

LangGraph's tools:<uuid> namespace is a checkpoint id assigned
independently of the parent's call_* tool-call id. I checked the wire for a
link between them and there is none — no metadata field, no co-occurrence
in any payload of a live run. When a delegation tool carries no matchable
description, position is genuinely the only signal left.

That is fine with one outstanding child, and it is what every graph here
produces: cockpit/chat/subagents dispatches ONE tool call per assistant
turn, three sequential ToolNode round-trips. With several outstanding at
once, arrival order is not dispatch order, and claiming the first unmapped
call silently cross-wires them. Reproduced: two children whose streams
arrive in reverse dispatch order, and call_ALPHA's card renders beta's
output. A booking card showing research text is worse than an empty one.

The fallback now fires only when exactly one candidate is outstanding.
Ambiguous streams stay buffered rather than mis-attributed, and can still
resolve later — as siblings complete, the candidate set shrinks back to
one. The description rungs are untouched and still preferred.

Verified: 344/344 (new cross-wiring guard included). Mutation-tested —
restoring the greedy  fallback fails exactly that guard. Sequential
attribution re-checked against a live model: all three cards populate,
each on its own call_* id.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
threadplane Ready Ready Preview Aug 30, 2026 1:47am

Request Review

@github-actions github-actions Bot left a comment

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.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove enabled auto-merge (squash) August 30, 2026 01:41
@blove
blove merged commit 8e8892e into main Aug 30, 2026
68 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant