Files
orca/src/shared/native-chat-subagent-items.test.ts
T
Brennan Benson fe1f0223af Native chat: read one saved row per subagent (part 1 of 2) (#27914)
* feat(chat): read one saved row per subagent

Adds the singular `subagent` journal block (one row per child, revised in
place, with a plain-text twin for older clients) and teaches every reader
to draw it: desktop and phone fold a turn's sibling rows into the existing
"Ran N subagents" row, the client and off-page rosters name children from
their own rows, the reopen pass settles a row a dead host left working,
and the phone and worker-transcript wires bound it. No producer writes the
block yet; that switch lands in the follow-up.

* fix(chat): open subagent background flag, separate nested group ids, page window skips subagent rows

- background is an open boolean so a later build's value never fails a chat load
- a helper's own helpers fold under a groupId of their own, so the phone opens them apart
- each subagent's own row rides along with the page window instead of counting as conversation

* refactor(chat): a subagent's group entry as one object
2026-10-12 14:21:25 -07:00

191 lines
6.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import {
foldSubagentItems,
namedSubagents,
subagentFallbackText
} from './native-chat-subagent-items'
import { projectNativeChatTranscriptMessages } from './native-chat-transcript-projection'
import { formatWorkerTranscriptMessage } from './worker-transcript-text'
import type {
NativeChatMessage,
NativeChatSubagentBlock,
NativeChatSubagentGroupBlock
} from './native-chat-types'
function subagent(
id: string,
overrides: Partial<NativeChatSubagentBlock> = {}
): NativeChatSubagentBlock {
return { type: 'subagent', id, label: id, state: 'working', groupId: 'turn-1', ...overrides }
}
function item(sequence: number, block: NativeChatSubagentBlock): NativeChatMessage {
return {
id: `subagent:${block.id}`,
role: 'system',
blocks: [{ type: 'text', text: subagentFallbackText(block) }, block],
timestamp: sequence,
source: 'transcript',
journalPosition: { sequence, index: 0 }
}
}
function text(sequence: number, role: NativeChatMessage['role'], body: string): NativeChatMessage {
return {
id: `row-${sequence}`,
role,
blocks: [{ type: 'text', text: body }],
timestamp: sequence,
source: 'transcript',
journalPosition: { sequence, index: 0 }
}
}
function groupOf(message: NativeChatMessage | undefined): NativeChatSubagentGroupBlock {
const [block] = message?.blocks ?? []
if (block?.type !== 'subagent-group') {
throw new Error('expected a roster row')
}
return block
}
describe('a turn’s subagents drawn as one row', () => {
it('folds siblings into one roster row at the first one’s place, whatever lies between', () => {
const rows = foldSubagentItems([
text(1, 'user', 'audit it'),
item(2, subagent('a', { label: 'Audit' })),
text(3, 'assistant', 'and another'),
item(4, subagent('b', { label: 'Review', state: 'completed', settledAt: 9 }))
])
expect(rows.map((row) => row.id)).toEqual(['row-1', 'subagent:a', 'row-3'])
expect(groupOf(rows[1])).toEqual({
type: 'subagent-group',
groupId: 'turn-1',
agents: [
{ id: 'a', label: 'Audit', state: 'working' },
{ id: 'b', label: 'Review', state: 'completed', settledAt: 9 }
]
})
})
it('keeps another turn’s, or another spawner’s, subagents in rows of their own', () => {
const rows = foldSubagentItems([
item(1, subagent('a')),
item(2, subagent('b', { groupId: 'turn-2' })),
item(3, subagent('c', { parentId: 'a' }))
])
expect(rows.map((row) => groupOf(row).agents.map((agent) => agent.id))).toEqual([
['a'],
['b'],
['c']
])
// The phone keys a group's open state by groupId, so a helper's helpers get their own.
expect(new Set(rows.map((row) => groupOf(row).groupId)).size).toBe(3)
})
it('tells same-named siblings apart without taking a name a sibling already has', () => {
const rows = foldSubagentItems([
item(1, subagent('a', { label: 'Audit' })),
item(2, subagent('b', { label: 'Audit' })),
item(3, subagent('c', { label: 'Audit 2' }))
])
expect(groupOf(rows[0]).agents.map((agent) => agent.label)).toEqual([
'Audit',
'Audit 3',
'Audit 2'
])
})
it('hands back the same row while no sibling changed, and a new one when one did', () => {
const a = item(1, subagent('a'))
const b = item(2, subagent('b'))
const first = foldSubagentItems([a, b])[0]
expect(foldSubagentItems([a, b])[0]).toBe(first)
const settled = item(2, subagent('b', { state: 'completed' }))
const next = foldSubagentItems([a, settled])[0]
expect(next).not.toBe(first)
// The sibling that did not change keeps its line, so its memoized row stays put.
expect(groupOf(next).agents[0]).toBe(groupOf(first).agents[0])
})
it('leaves an older host’s roster row, and rows that are not one subagent, as they came', () => {
const roster: NativeChatMessage = {
...text(1, 'system', 'Ran 1 subagent'),
blocks: [
{ type: 'text', text: 'Ran 1 subagent' },
{
type: 'subagent-group',
groupId: 'turn-0',
agents: [{ id: 'old', label: 'old', state: 'completed' }]
}
]
}
const mixed: NativeChatMessage = {
...text(2, 'assistant', 'x'),
blocks: [subagent('z'), { type: 'tool-call', name: 'Agent', input: {} }]
}
const messages = [roster, mixed]
expect(foldSubagentItems(messages)).toBe(messages)
})
it('is what the transcript draws, so the row folds no tool run apart', () => {
const rows = projectNativeChatTranscriptMessages([
text(1, 'user', 'go'),
{
...text(2, 'assistant', ''),
blocks: [{ type: 'tool-call', name: 'Agent', input: {}, callId: 'c1' }]
},
item(3, subagent('a')),
{ ...text(4, 'tool', ''), blocks: [{ type: 'tool-result', output: 'ok', callId: 'c1' }] }
])
expect(rows.map((row) => row.blocks.map((block) => block.type))).toEqual([
['text'],
['tool-call', 'tool-result'],
['subagent-group']
])
})
})
describe('the sentence an older client draws', () => {
it('claims no liveness it cannot keep, and names an adverse outcome', () => {
expect(subagentFallbackText(subagent('a', { label: 'Audit' }))).toBe(
'Kicked off subagent Audit'
)
expect(subagentFallbackText(subagent('a', { label: 'Audit', state: 'completed' }))).toBe(
'Ran subagent Audit'
)
expect(subagentFallbackText(subagent('a', { label: 'Audit', state: 'failed' }))).toBe(
'Ran subagent Audit (failed)'
)
})
it('is what a text surface prints, once, and the block alone still prints it', () => {
const own = item(1, subagent('a', { label: 'Audit' }))
expect(formatWorkerTranscriptMessage(own)).toBe('[system] Kicked off subagent Audit')
expect(formatWorkerTranscriptMessage({ ...own, blocks: own.blocks.slice(1) })).toBe(
'[system] [subagents] Kicked off subagent Audit'
)
})
})
describe('the subagents a row names', () => {
it('reads an older host’s roster and a subagent’s own row alike', () => {
expect(
namedSubagents(subagent('a', { run: 'r', outcomeBasis: 'stop-acknowledged', tokens: 3 }))
).toEqual([{ id: 'a', label: 'a', state: 'working', tokens: 3 }])
expect(
namedSubagents({
type: 'subagent-group',
groupId: 'g',
agents: [{ id: 'b', label: 'b', state: 'idle' }]
})
).toEqual([{ id: 'b', label: 'b', state: 'idle' }])
expect(namedSubagents({ type: 'text', text: 'x' })).toEqual([])
})
})