From 752ddd258f88d7afaedc58a90050c81633ef71e7 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Thu, 13 Aug 2026 17:50:46 -0700 Subject: [PATCH] Backfill the subagent display name from the second start event MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dispatch-time subagent_start fires before the trigger args (and therefore the name parameter) have streamed; the phase-3 start re-announces the lane with the name. The block builder was dropping that duplicate wholesale, losing the name on streaming providers — now it backfills subagentName onto the existing block instead. (The home turn-model path already reconciled this case.) --- .../sim/lib/copilot/request/go/stream.test.ts | 66 +++++++++++++++++++ apps/sim/lib/copilot/request/go/stream.ts | 27 +++++--- 2 files changed, 85 insertions(+), 8 deletions(-) diff --git a/apps/sim/lib/copilot/request/go/stream.test.ts b/apps/sim/lib/copilot/request/go/stream.test.ts index 25f550a3a0..991570b134 100644 --- a/apps/sim/lib/copilot/request/go/stream.test.ts +++ b/apps/sim/lib/copilot/request/go/stream.test.ts @@ -936,4 +936,70 @@ describe('copilot go stream helpers', () => { expect(subagentBlock?.parentSpanId).toBe('S1') expect(subagentBlock?.parentToolCallId).toBe('tc-deploy-inner') }) + + it('backfills the display name when only the second subagent start carries it', async () => { + const scope = { + lane: 'subagent' as const, + agentId: 'research', + parentToolCallId: 'tc-research', + spanId: 'S3', + parentSpanId: 'S1', + } + vi.mocked(fetch).mockResolvedValueOnce( + createSseResponse([ + // Dispatch-time start: fires before the trigger args stream, so no name. + createEvent({ + streamId: 'stream-1', + cursor: '1', + seq: 1, + requestId: 'req-1', + type: MothershipStreamV1EventType.span, + scope, + payload: { + kind: 'subagent', + event: 'start', + agent: 'research', + data: { tool_call_id: 'tc-research' }, + }, + }), + // Phase-3 start re-announces the lane WITH the orchestrator-chosen name. + createEvent({ + streamId: 'stream-1', + cursor: '2', + seq: 2, + requestId: 'req-1', + type: MothershipStreamV1EventType.span, + scope, + payload: { + kind: 'subagent', + event: 'start', + agent: 'research', + data: { tool_call_id: 'tc-research', name: 'Pricing research' }, + }, + }), + createEvent({ + streamId: 'stream-1', + cursor: '3', + seq: 3, + requestId: 'req-1', + type: MothershipStreamV1EventType.complete, + payload: { status: MothershipStreamV1CompletionStatus.complete }, + }), + ]) + ) + + const context = createStreamingContext() + const execContext: ExecutionContext = { + userId: 'user-1', + workflowId: 'workflow-1', + } + + await runStreamLoop('https://example.com/mothership/stream', {}, context, execContext, { + timeout: 1000, + }) + + const subagentBlocks = context.contentBlocks.filter((block) => block.type === 'subagent') + expect(subagentBlocks).toHaveLength(1) + expect(subagentBlocks[0]?.subagentName).toBe('Pricing research') + }) }) diff --git a/apps/sim/lib/copilot/request/go/stream.ts b/apps/sim/lib/copilot/request/go/stream.ts index e72bb29d0a..b5dc0f2da3 100644 --- a/apps/sim/lib/copilot/request/go/stream.ts +++ b/apps/sim/lib/copilot/request/go/stream.ts @@ -433,25 +433,36 @@ export async function runStreamLoop( context.subAgentToolCalls[toolCallId] ??= [] } if (toolCallId && subagentName) { + const payloadData = streamEvent.payload.data + const rawName = + payloadData && typeof payloadData === 'object' && !Array.isArray(payloadData) + ? (payloadData as Record).name + : undefined + const displayName = typeof rawName === 'string' && rawName ? rawName : undefined const openParents = (context.openSubagentParents ??= new Set()) if (!openParents.has(toolCallId)) { openParents.add(toolCallId) - const payloadData = streamEvent.payload.data - const displayName = - payloadData && typeof payloadData === 'object' && !Array.isArray(payloadData) - ? (payloadData as Record).name - : undefined context.contentBlocks.push({ type: 'subagent', content: subagentName, - ...(typeof displayName === 'string' && displayName - ? { subagentName: displayName } - : {}), + ...(displayName ? { subagentName: displayName } : {}), parentToolCallId: toolCallId, ...(spanId ? { spanId } : {}), ...(parentSpanId ? { parentSpanId } : {}), timestamp: Date.now(), }) + } else if (displayName) { + // The lane was opened by the dispatch-time start, which fires + // before the trigger args (and therefore the name) exist. The + // phase-3 start re-announces the lane WITH the name; backfill + // it instead of dropping the duplicate wholesale. + for (let i = context.contentBlocks.length - 1; i >= 0; i--) { + const b = context.contentBlocks[i] + if (b.type === 'subagent' && b.parentToolCallId === toolCallId) { + if (!b.subagentName) b.subagentName = displayName + break + } + } } } else { logger.warn('subagent start missing toolCallId or agent name', {