Backfill the subagent display name from the second start event

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.)
This commit is contained in:
Siddharth Ganesan
2026-08-13 17:50:46 -07:00
parent ba788296b0
commit 752ddd258f
2 changed files with 85 additions and 8 deletions
@@ -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')
})
})
+19 -8
View File
@@ -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<string, unknown>).name
: undefined
const displayName = typeof rawName === 'string' && rawName ? rawName : undefined
const openParents = (context.openSubagentParents ??= new Set<string>())
if (!openParents.has(toolCallId)) {
openParents.add(toolCallId)
const payloadData = streamEvent.payload.data
const displayName =
payloadData && typeof payloadData === 'object' && !Array.isArray(payloadData)
? (payloadData as Record<string, unknown>).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', {