diff --git a/.changeset/tidy-claude-subagents.md b/.changeset/tidy-claude-subagents.md new file mode 100644 index 000000000000..586a8949595b --- /dev/null +++ b/.changeset/tidy-claude-subagents.md @@ -0,0 +1,5 @@ +--- +'@ai-sdk/harness-claude-code': patch +--- + +Exclude Claude Code subagent messages from parent step usage and tool-call tracking. diff --git a/packages/harness-claude-code/src/bridge/__fixtures__/subagent-step-stream.json b/packages/harness-claude-code/src/bridge/__fixtures__/subagent-step-stream.json new file mode 100644 index 000000000000..593814c5f6e7 --- /dev/null +++ b/packages/harness-claude-code/src/bridge/__fixtures__/subagent-step-stream.json @@ -0,0 +1,129 @@ +[ + { + "type": "assistant", + "parent_tool_use_id": null, + "message": { + "content": [{ "type": "thinking", "thinking": "Delegating." }], + "usage": { + "input_tokens": 2, + "cache_creation_input_tokens": 5605, + "cache_read_input_tokens": 2298, + "output_tokens": 5 + } + } + }, + { + "type": "assistant", + "parent_tool_use_id": null, + "message": { + "content": [ + { + "type": "tool_use", + "id": "toolu_main_agent", + "name": "Agent", + "input": { + "description": "Run pwd and echo", + "prompt": "Use Bash to run pwd, then echo subagent-done.", + "subagent_type": "general-purpose", + "run_in_background": false + } + } + ], + "usage": { + "input_tokens": 2, + "cache_creation_input_tokens": 5605, + "cache_read_input_tokens": 2298, + "output_tokens": 5 + } + } + }, + { + "type": "user", + "parent_tool_use_id": "toolu_main_agent", + "message": { + "content": [{ "type": "text", "text": "The subagent started." }] + } + }, + { + "type": "assistant", + "parent_tool_use_id": "toolu_main_agent", + "message": { + "content": [ + { + "type": "tool_use", + "id": "toolu_subagent_pwd", + "name": "Bash", + "input": { "command": "pwd" } + } + ], + "usage": { + "input_tokens": 2, + "cache_creation_input_tokens": 5024, + "cache_read_input_tokens": 3519, + "output_tokens": 4 + } + } + }, + { + "type": "assistant", + "parent_tool_use_id": "toolu_main_agent", + "message": { + "content": [ + { + "type": "tool_use", + "id": "toolu_subagent_echo", + "name": "Bash", + "input": { "command": "echo subagent-done" } + } + ], + "usage": { + "input_tokens": 2, + "cache_creation_input_tokens": 5024, + "cache_read_input_tokens": 3519, + "output_tokens": 4 + } + } + }, + { + "type": "user", + "parent_tool_use_id": "toolu_main_agent", + "message": { + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_subagent_pwd", + "content": "/work/packages/harness-claude-code", + "is_error": false + } + ] + } + }, + { + "type": "user", + "parent_tool_use_id": "toolu_main_agent", + "message": { + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_subagent_echo", + "content": "subagent-done", + "is_error": false + } + ] + } + }, + { + "type": "user", + "parent_tool_use_id": null, + "message": { + "content": [ + { + "type": "tool_result", + "tool_use_id": "toolu_main_agent", + "content": "Both commands ran successfully.", + "is_error": false + } + ] + } + } +] diff --git a/packages/harness-claude-code/src/bridge/create-emit-stream-event.test.ts b/packages/harness-claude-code/src/bridge/create-emit-stream-event.test.ts index 4ca78fc7eaa6..036ae5823cc3 100644 --- a/packages/harness-claude-code/src/bridge/create-emit-stream-event.test.ts +++ b/packages/harness-claude-code/src/bridge/create-emit-stream-event.test.ts @@ -1,5 +1,7 @@ +import { readFileSync } from 'node:fs'; import { describe, expect, it } from 'vitest'; import { + type ClaudeMessage, createClaudeStreamEventState, createEmitStreamEvent, } from './create-emit-stream-event'; @@ -92,6 +94,51 @@ describe('createEmitStreamEvent', () => { `); }); + it('keeps subagent messages out of the main Agent-tool step', () => { + const messages = JSON.parse( + readFileSync( + new URL('./__fixtures__/subagent-step-stream.json', import.meta.url), + 'utf8', + ), + ) as ClaudeMessage[]; + const state = createClaudeStreamEventState(); + const emitted: Record[] = []; + const emitStreamEvent = createEmitStreamEvent({ + state, + emit: event => emitted.push(event), + emitWarning: () => {}, + emitTerminalError: () => {}, + onCompactionBoundary: () => {}, + toCommonName: name => (name === 'Bash' ? 'bash' : name), + }); + + for (const message of messages) { + emitStreamEvent(message); + } + + const finishStep = emitted.find(event => event.type === 'finish-step'); + const leakedSubagentEvents = emitted.filter( + event => + (event.type === 'tool-call' || event.type === 'tool-result') && + typeof event.toolCallId === 'string' && + event.toolCallId.startsWith('toolu_subagent_'), + ); + + expect(finishStep?.usage).toEqual({ + inputTokens: { + total: 7905, + noCache: 2, + cacheRead: 2298, + cacheWrite: 5605, + }, + outputTokens: { + total: 5, + text: 5, + }, + }); + expect(leakedSubagentEvents).toEqual([]); + }); + it('preserves retry and compaction handling', () => { const state = createClaudeStreamEventState(); const warnings: unknown[] = []; diff --git a/packages/harness-claude-code/src/bridge/create-emit-stream-event.ts b/packages/harness-claude-code/src/bridge/create-emit-stream-event.ts index c17e6759aaa8..681cdea139a3 100644 --- a/packages/harness-claude-code/src/bridge/create-emit-stream-event.ts +++ b/packages/harness-claude-code/src/bridge/create-emit-stream-event.ts @@ -5,6 +5,7 @@ type Emit = (message: Record) => void; export type ClaudeMessage = { type?: string; subtype?: string; + parent_tool_use_id?: string | null; model?: string; error?: string; error_status?: number | null; @@ -186,6 +187,10 @@ export function createEmitStreamEvent({ return; } + if (type === 'assistant' && msg.parent_tool_use_id != null) { + return; + } + if (type === 'assistant' && msg.message?.content) { const usage = mapUsage(msg.message.usage); const toolUseIds: string[] = []; @@ -227,6 +232,10 @@ export function createEmitStreamEvent({ return; } + if (type === 'user' && msg.parent_tool_use_id != null) { + return; + } + if (type === 'user' && msg.message?.content) { for (const block of msg.message.content) { if (