Skip to content

fix(langgraph): subagent cards rendered an empty transcript - #847

Merged
blove merged 1 commit into
mainfrom
blove/subagent-message-attribution-buffer
Aug 29, 2026
Merged

fix(langgraph): subagent cards rendered an empty transcript#847
blove merged 1 commit into
mainfrom
blove/subagent-message-attribution-buffer

Conversation

@blove

@blove blove commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Every subagent card in cockpit/chat/subagents and examples/chat showed 0 message(s) while the child streamed a full response. I found this during #844 via a wire capture, confirmed it was pre-existing, and shipped that PR without it. This is the fix.

Three defects were stacked — each one hid the next, so fixing only the first still left an empty card.

1. Attribution never ran

matchSubgraphToSubagent was only invoked when a child's values.messages[0] was a human message:

if (first['type'] === 'human' || first['type'] === 'user') { matchSubgraphToSubagent(...) }

Wire capture of this graph shows the child's messages start with the AI reply — there is no human message anywhere in the child's state. So the tools:<uuid> namespace was never mapped to its call_* id, and nothing was ever claimed.

The ladder's text rungs couldn't have helped either: the delegation tool takes task_description, not description, so both the exact and substring rungs were structurally unreachable. Only the positional fallback could ever fire for this graph — and it was gated behind a call that never happened.

Now any tools: child claims its tool call on first sight. The precise description match still wins when the shape allows it.

2. Pre-attribution chunks were dropped

addMessageToSubagent returned silently when the namespace resolved to no registered subagent. Those chunks are now buffered per namespace and replayed by establish() on attribution.

3. Delta chunks replaced instead of accumulating

The one that made the other two invisible. Children stream AIMessageChunk deltas — measured on the wire at ~14 chars each, ~500 per message — but the tracker merged by id with a plain overwrite:

if (idx >= 0) { merged[idx] = msg; }   // keeps only the final tiny delta

So a correctly attributed card still rendered an empty message. The tracker now folds chunk content the way the parent transcript has since #751; snapshots still replace.

Verification

Live model, Chrome. All three cards go 0 message(s)1 message(s), each attributed to its own distinct call_* id (so the positional fallback isn't cross-wiring them), and expanding one renders the child's real output: "LAX (Los Angeles International — Los Angeles) Major terminals/airlines: Central Terminal Area with Terminals 1–8…"

343/343 lib tests, 3 new. Worth noting how they were written: my first test encoded my hypothesis (messages arriving before attribution), passed after the buffer fix — and the live UI stayed broken. Every test here is now shaped from wire-captured payloads instead.

e2e: cockpit-chat-subagents 1/1, cockpit-langgraph-subgraphs 3/3, cockpit-deep-agents-subagents 1/1, examples/chat 54/54. Lint 0 errors.

Release

Ships behavior consumers can see, so it wants a patch release (0.0.60) after merge.

🤖 Generated with Claude Code

@vercel

vercel Bot commented Aug 29, 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 29, 2026 8:05pm

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 29, 2026

Copy link
Copy Markdown
Contributor

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


I'll analyze this and get back to you.

@github-actions

github-actions Bot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

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


I'll analyze this and get back to you.

Every subagent card in cockpit/chat/subagents and examples/chat showed
'0 message(s)' despite the child streaming a full response. Three defects
stacked, each hiding the next.

1. Attribution never ran. matchSubgraphToSubagent was only called when a
   child's values.messages[0] was a HUMAN message. Wire capture shows this
   graph's child starts with the AI reply, so the namespace UUID was never
   mapped to its call_* id. Now any tools: child claims its tool call on
   first sight, falling through to the positional rung the ladder already
   relied on (the delegation tool here takes task_description, not
   description, so both text rungs were unreachable anyway).

2. Pre-attribution chunks were dropped. addMessageToSubagent silently
   returned when the namespace had no registered subagent. They are now
   buffered per namespace and replayed by establish().

3. Delta chunks replaced instead of accumulating. Children stream
   AIMessageChunk deltas — ~14 chars each, ~500 per message — and the
   tracker merged by id with a plain overwrite, keeping only the last tiny
   delta. So even a correctly attributed card rendered an empty message.
   Now folds chunk content the way the parent transcript has since #751;
   snapshots still replace.

Verified live against a real model: all three cards go 0 -> 1 message(s),
each attributed to its own distinct call_* id, and expanding one renders
the child's actual research text. 343/343 lib tests (3 new, each written
from wire-captured shapes after an earlier hypothesis-shaped test passed
while the UI stayed broken). e2e: chat-subagents, langgraph-subgraphs,
deep-agents-subagents, examples/chat 54/54.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@blove
blove force-pushed the blove/subagent-message-attribution-buffer branch from 3ff15ba to e8ec118 Compare August 29, 2026 20:02
@github-actions

github-actions Bot commented Aug 29, 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 29, 2026 20:03
@blove
blove merged commit f25fec7 into main Aug 29, 2026
22 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