From 09e46cb079aadc91cf97ff1e8775cd3de002c76f Mon Sep 17 00:00:00 2001 From: James Grugett Date: Wed, 20 May 2026 21:40:34 -0700 Subject: [PATCH] Hide empty subagent spawn errors --- .../__tests__/sdk-event-handlers.test.ts | 98 +++++++++++++++++ cli/src/utils/sdk-event-handlers.ts | 100 ++++++++++++------ 2 files changed, 163 insertions(+), 35 deletions(-) diff --git a/cli/src/utils/__tests__/sdk-event-handlers.test.ts b/cli/src/utils/__tests__/sdk-event-handlers.test.ts index c1e2442656..d5a6ecfbf3 100644 --- a/cli/src/utils/__tests__/sdk-event-handlers.test.ts +++ b/cli/src/utils/__tests__/sdk-event-handlers.test.ts @@ -369,6 +369,104 @@ describe('sdk-event-handlers', () => { expect(getStreamingAgents().size).toBe(0) }) + test('hides spawn_agents error placeholders with no user-facing output', () => { + const { ctx, getMessages, getStreamingAgents } = createTestContext() + ctx.message.updater.addBlock( + createAgentBlock({ + agentId: 'tool-1-0', + agentType: 'basher', + spawnToolCallId: 'tool-1', + spawnIndex: 0, + }), + ) + ctx.streaming.setStreamingAgents(() => new Set(['tool-1-0'])) + + const handleEvent = createEventHandler(ctx) + const toolResultEvent: ToolResultEvent = { + type: 'tool_result', + toolCallId: 'tool-1', + toolName: 'spawn_agents', + output: [ + { + type: 'json', + value: [ + { + agentName: 'basher', + value: { + errorMessage: + 'Error spawning agent: Invalid params for agent basher', + }, + }, + ], + }, + ], + } + handleEvent(toolResultEvent) + + expect(getMessages()[0].blocks).toEqual([]) + expect(getStreamingAgents().size).toBe(0) + }) + + test('renders spawn_agents error content when agent already streamed output', () => { + const { ctx, getMessages, getStreamingAgents } = createTestContext() + ctx.message.updater.updateAiMessageBlocks(() => [ + { + type: 'agent', + agentId: 'tool-1-0', + agentName: 'Basher', + agentType: 'basher', + content: '', + status: 'running', + blocks: [ + { + type: 'text', + content: 'Checking files...', + textType: 'text', + }, + ], + initialPrompt: '', + spawnToolCallId: 'tool-1', + spawnIndex: 0, + } as any, + ]) + ctx.streaming.setStreamingAgents(() => new Set(['tool-1-0'])) + + const handleEvent = createEventHandler(ctx) + const toolResultEvent: ToolResultEvent = { + type: 'tool_result', + toolCallId: 'tool-1', + toolName: 'spawn_agents', + output: [ + { + type: 'json', + value: [ + { + agentName: 'basher', + value: { + errorMessage: + 'Error spawning agent: Invalid params for agent basher', + }, + }, + ], + }, + ], + } + handleEvent(toolResultEvent) + + const agentBlock = (getMessages()[0].blocks ?? [])[0] as AgentContentBlock + expect(agentBlock.status).toBe('complete') + expect(agentBlock.blocks).toHaveLength(2) + expect(agentBlock.blocks?.[0]).toMatchObject({ + type: 'text', + content: 'Checking files...', + }) + expect(agentBlock.blocks?.[1]).toMatchObject({ + type: 'text', + content: 'Error spawning agent: Invalid params for agent basher', + }) + expect(getStreamingAgents().size).toBe(0) + }) + test('handles spawn_agents tool results for agents with tool blocks (lastMessage mode)', () => { const { ctx, getMessages, getStreamingAgents } = createTestContext() diff --git a/cli/src/utils/sdk-event-handlers.ts b/cli/src/utils/sdk-event-handlers.ts index ca9ee14b6a..4cfdf5df0a 100644 --- a/cli/src/utils/sdk-event-handlers.ts +++ b/cli/src/utils/sdk-event-handlers.ts @@ -358,50 +358,79 @@ const handleToolCall = (state: EventHandlerState, event: PrintModeToolCall) => { /** * Recursively finds and updates agent blocks that match a spawn_agents tool call. */ -const updateSpawnAgentBlocks = ( - blocks: ContentBlock[], +const updateSpawnAgentBlock = ( + block: ContentBlock, toolCallId: string, results: any[], -): ContentBlock[] => { - return blocks.map((block) => { - if (block.type !== 'agent') { - return block - } +): ContentBlock | null => { + if (block.type !== 'agent') { + return block + } - if (block.spawnToolCallId === toolCallId && block.spawnIndex !== undefined && block.blocks) { - const result = results[block.spawnIndex] - - if (result?.value) { - const { content, hasError } = extractSpawnAgentResultContent(result.value) - // Check if the agent already streamed text content (e.g., basher). - // Agents like thinker return all output at the end via lastMessage, - // so we should add final content even if they have tool blocks. - const hasStreamedTextContent = block.blocks.some( - (b) => b.type === 'text' && b.textType === 'text' - ) - const finalBlocks = content && !hasStreamedTextContent - ? [...block.blocks, { type: 'text', content } as ContentBlock] - : block.blocks - if (hasError || finalBlocks.length > 0) { - return { - ...block, - blocks: finalBlocks, - status: hasError ? ('failed' as const) : ('complete' as const), - } + const spawnIndex = block.spawnIndex + const childBlocks = block.blocks + const isSpawnResultTarget = + block.spawnToolCallId === toolCallId && + spawnIndex !== undefined && + childBlocks + + if (isSpawnResultTarget) { + const result = results[spawnIndex] + if (result?.value) { + const { content, hasError } = extractSpawnAgentResultContent(result.value) + + if (hasError) { + if (childBlocks.length === 0) { + return null + } + + return { + ...block, + blocks: content + ? [...childBlocks, { type: 'text', content } as ContentBlock] + : childBlocks, + status: 'complete' as const, } } - } - // Recursively process nested agent blocks - if (block.blocks?.length) { - const updatedNestedBlocks = updateSpawnAgentBlocks(block.blocks, toolCallId, results) - if (updatedNestedBlocks !== block.blocks) { - return { ...block, blocks: updatedNestedBlocks } + // Agents like thinker return all output at the end via lastMessage, + // while agents like basher may have already streamed their text. + const hasStreamedTextContent = childBlocks.some( + (b) => b.type === 'text' && b.textType === 'text', + ) + const finalBlocks = + content && !hasStreamedTextContent + ? [...childBlocks, { type: 'text', content } as ContentBlock] + : childBlocks + + if (finalBlocks.length > 0) { + return { + ...block, + blocks: finalBlocks, + status: 'complete' as const, + } } } + } + if (!childBlocks?.length) { return block - }) + } + + return { + ...block, + blocks: updateSpawnAgentBlocks(childBlocks, toolCallId, results), + } +} + +const updateSpawnAgentBlocks = ( + blocks: ContentBlock[], + toolCallId: string, + results: any[], +): ContentBlock[] => { + return blocks + .map((block) => updateSpawnAgentBlock(block, toolCallId, results)) + .filter((block): block is ContentBlock => block !== null) } const handleSpawnAgentsResult = ( @@ -433,7 +462,8 @@ const handleToolResult = ( ) const firstOutput = event.output?.[0] - const firstOutputValue = firstOutput && 'value' in firstOutput ? firstOutput.value : undefined + const firstOutputValue = + firstOutput && 'value' in firstOutput ? firstOutput.value : undefined const isSpawnAgentsResult = Array.isArray(firstOutputValue) && firstOutputValue.some((v: any) => v?.agentName || v?.agentType)