fix(daemon): extract subagent reply text from tool_response envelope#55
Draft
rgao-coreweave wants to merge 2 commits into
Draft
fix(daemon): extract subagent reply text from tool_response envelope#55rgao-coreweave wants to merge 2 commits into
rgao-coreweave wants to merge 2 commits into
Conversation
closeSubagentInvokeAgentSpan was storing the full Claude Code
tool_response envelope JSON-stringified into gen_ai.output.messages:
{"status":"completed","prompt":"...","agentId":"...",
"content":[{"type":"text","text":"ok let me push..."}],
...}
The chat view then rendered that whole blob as the subagent's
assistant_message, instead of just the subagent's reply text.
Extract `content[*].text` from the Anthropic-shape envelope. Plain
strings (orphan-path lastAssistantText, error-path messages) and
unrecognized shapes pass through unchanged so behavior is preserved
for the non-envelope call sites.
Verified locally against a realistic tool_response with sibling
fields (status, prompt, agentId, agentType, description,
totalDurationMs, totalTokens). After fix, output_messages.content is
exactly the subagent's text — none of the envelope keys leak through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cc34932 to
669dccb
Compare
HiveMind Sessions1 session · 24m · $5.33
View all sessions in HiveMind → Run |
- Restructure extractSubagentReplyText so a recognized envelope shape (object with a `content` array) always returns the joined text — even when no text blocks were present — instead of falling through to JSON.stringify and re-emitting the wrapper. The empty-text fall-through was a latent bug-class that would silently reintroduce the original rendering issue if Anthropic returned a content list of non-text blocks (image / tool_use / thinking). JSDoc now states this contract explicitly. Drive-by: collapse the redundant null-and-non-object guard since the null check is load-bearing only for the object-cast that follows. - Add a JSDoc note to closeSubagentInvokeAgentSpan explaining why we deliberately do not set `gen_ai.tool.call.result` on the subagent invoke_agent span. The envelope sibling fields (status, prompt, agentId, agentType, description, totalDurationMs, totalTokens) are fully reconstructible from other span attributes — dispatch metadata is set at PreToolUse from tool_input, duration from span timestamps, and per-turn token counts on child chat spans — so duplicating the envelope would only create a second source of truth. - Trim the now-redundant call-site comment that duplicated the helper's JSDoc. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
closeSubagentInvokeAgentSpanwas storing the full Claude Codetool_responseenvelope JSON-stringified intogen_ai.output.messages. The chat view rendered the subagent'sassistant_messageas{"status":"completed","prompt":"...","agentId":"...","content":[...]}instead of just the reply text.extractSubagentReplyText(...)recognizes the Anthropic envelope shape (object with acontentarray) and returns the joinedtextblocks. Plain strings (orphan-pathlastAssistantText, error-path messages) pass through verbatim. Unrecognized shapes fall back toJSON.stringifyso no data is silently dropped.content: []or only non-text blocks liketool_use/image/thinking), the helper returns the joined text unconditionally — i.e. the empty string — rather than falling through toJSON.stringifyand re-emitting the wrapper. The JSDoc documents this contract so the design intent is locked in.Why this lives in the instrumentation, not the UI
gen_ai.output.messagesin the OTel GenAI semconv is meant to carry the model's actual output messages —[{ role, content }]wherecontentis the assistant's reply. Claude Code'stool_responseis a transport envelope around the reply (status,agentId,agentType,prompt,description,totalDurationMs,totalTokens, pluscontent[]blocks). Stringifying the entire envelope intooutput.messages.contentfeeds consumers the wrapper rather than the message — an instrumentation-contract violation. Any OTel-compliant viewer reading the attribute would misrender it the same way; a UI-side unwrap would be a Claude-Code-envelope-specific special case that doesn't generalize.Why we don't also set
gen_ai.tool.call.resulton the subagent pathThe regular tool path in
handlePostToolUsewrites the fulltool_responsetogen_ai.tool.call.result. The subagent path deliberately does not — the subagent is modeled as aninvoke_agent(chat-flavored) span, not atool_callspan, so the attribute split mirrors the span-type split. The envelope sibling fields are already reconstructible from other attributes on the span tree:status→ mapped to OTel span status (ERROR on failure)agentId/agentType/prompt/description→ set on the span at PreToolUse fromtool_inputtotalDurationMs→ recoverable from the span's own start/end timestampstotalTokens→ covered (more granularly, with cache breakdowns) by per-turn token counts on child chat spansSo
OUTPUT_MESSAGESnow carries just the assistant message; nothing else is dropped. A JSDoc note oncloseSubagentInvokeAgentSpandocuments this so the asymmetry vs the regular tool path doesn't look like an oversight.Discovery
Caught while inspecting the exported
/agents/traces/chatresponse for the talk-to-tim trace — the subagent'sassistant_message.textrendered as the full JSON envelope, drowning out the reply text "ok let me push on this…". Cosmetic but user-visible.Test plan
tool_responsecarrying sibling fields (status,prompt,agentId,agentType,description,totalDurationMs,totalTokens) —output_messages.contentis exactly the subagent's text with no envelope keys leaking.npm run buildclean.🤖 Generated with Claude Code