mirror of
https://github.com/stablyai/orca.git
synced 2026-10-03 00:02:19 +00:00
* Restore the exhaustive block handling that adding `subagent-group` to `NativeChatBlock` broke. `formatWorkerTranscriptMessage` and `boundBlock` both fell through to `image-ref` field access, so `tsc -p` failed for the CLI and node projects and `build:cli` could not emit. Both now guard on `image-ref` explicitly and give the roster block its own branch. * Stop the roster's publish from evicting its own append. The sink queue coalesces by `coalescingKey` alone with no op-kind check, so passing the append's key to `tryPublish` spliced the queued append out and the row never reached the journal — permanently, since `lastSerialized` was already set. `tryPublish()` now takes no argument, matching every other call site. The regression test's fake sink honours the key, which the previous fake did not. * Keep `collabAgentToolCall` substantive. Only the MultiAgentV2 path emits `subAgentActivity`, so a V1 turn has no roster row; suppressing its collab tool calls too would have left a V1 fan-out showing nothing at all. * Surface a settled failure while siblings still work. The summary now reports the worst adverse outcome independently of the group verdict, so the row shows `3 working +1 failed` with a failed-coloured dot instead of a neutral pulsing dot. The plain-text twin names it too. * Treat `/morpheus` as a child. Only `/root` is the turn itself; the old segment-count test silently dropped a valid single-segment agent. * Refresh token-usage recency on update so an active thread is not evicted as the oldest entry, and scope the `agentsStates` comment to the V2 path.
153 lines
5.2 KiB
TypeScript
153 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import {
|
|
isTerminalSubagentState,
|
|
normalizeSubagentState,
|
|
subagentGroupFallbackText,
|
|
summarizeSubagentGroup
|
|
} from './native-chat-subagent-summary'
|
|
import type { NativeChatSubagentEntry } from './native-chat-types'
|
|
|
|
function agent(entry: Partial<NativeChatSubagentEntry>): NativeChatSubagentEntry {
|
|
return { id: 'a', label: 'task', state: 'working', ...entry }
|
|
}
|
|
|
|
describe('summarizeSubagentGroup', () => {
|
|
it('collapses in-flight children into one working count', () => {
|
|
const summary = summarizeSubagentGroup([
|
|
agent({ id: 'a', state: 'working' }),
|
|
agent({ id: 'b', state: 'working' }),
|
|
agent({ id: 'c', state: 'completed' })
|
|
])
|
|
|
|
expect(summary).toMatchObject({ total: 3, working: 2, settledState: null, settledCount: 0 })
|
|
})
|
|
|
|
it('ranks the settled verdict worst-first and reports ✓ completed last', () => {
|
|
const cascade: [NativeChatSubagentEntry['state'][], string][] = [
|
|
[['failed', 'stopped', 'idle', 'completed'], 'failed'],
|
|
[['stopped', 'idle', 'completed'], 'stopped'],
|
|
[['unverifiable', 'idle', 'completed'], 'unverifiable'],
|
|
[['idle', 'completed'], 'idle'],
|
|
[['completed', 'completed'], 'completed']
|
|
]
|
|
|
|
for (const [states, expected] of cascade) {
|
|
const summary = summarizeSubagentGroup(
|
|
states.map((state, index) => agent({ id: `a${index}`, state }))
|
|
)
|
|
expect(summary.settledState).toBe(expected)
|
|
}
|
|
})
|
|
|
|
it('counts how many children hold the winning verdict', () => {
|
|
const summary = summarizeSubagentGroup([
|
|
agent({ id: 'a', state: 'failed' }),
|
|
agent({ id: 'b', state: 'failed' }),
|
|
agent({ id: 'c', state: 'completed' })
|
|
])
|
|
|
|
expect(summary).toMatchObject({ settledState: 'failed', settledCount: 2 })
|
|
})
|
|
|
|
it('sums the per-child token snapshots and leaves them null when none reported', () => {
|
|
expect(
|
|
summarizeSubagentGroup([
|
|
agent({ id: 'a', tokens: 40661 }),
|
|
agent({ id: 'b', tokens: 1000 }),
|
|
agent({ id: 'c' })
|
|
]).tokens
|
|
).toBe(41661)
|
|
expect(summarizeSubagentGroup([agent({ id: 'a' })]).tokens).toBeNull()
|
|
})
|
|
|
|
it('reports the earliest start and withholds a settled time while work continues', () => {
|
|
const working = summarizeSubagentGroup([
|
|
agent({ id: 'a', state: 'completed', startedAt: 50, settledAt: 80 }),
|
|
agent({ id: 'b', state: 'working', startedAt: 20 })
|
|
])
|
|
const settled = summarizeSubagentGroup([
|
|
agent({ id: 'a', state: 'completed', startedAt: 50, settledAt: 80 }),
|
|
agent({ id: 'b', state: 'stopped', startedAt: 20, settledAt: 95 })
|
|
])
|
|
|
|
expect(working).toMatchObject({ startedAt: 20, settledAt: null })
|
|
expect(settled).toMatchObject({ startedAt: 20, settledAt: 95 })
|
|
})
|
|
|
|
it('reads a state this build does not know as unverifiable, never as working', () => {
|
|
expect(normalizeSubagentState('paused-for-review')).toBe('unverifiable')
|
|
expect(isTerminalSubagentState('paused-for-review')).toBe(true)
|
|
expect(summarizeSubagentGroup([agent({ state: 'unheard-of' as 'working' })])).toMatchObject({
|
|
working: 0,
|
|
settledState: 'unverifiable'
|
|
})
|
|
})
|
|
|
|
it('reports an adverse outcome before the group settles', () => {
|
|
const summary = summarizeSubagentGroup([
|
|
agent({ id: 'a', state: 'working' }),
|
|
agent({ id: 'b', state: 'working' }),
|
|
agent({ id: 'c', state: 'failed' })
|
|
])
|
|
|
|
// The group verdict is still withheld, but the failure is not.
|
|
expect(summary).toMatchObject({
|
|
working: 2,
|
|
settledState: null,
|
|
adverseState: 'failed',
|
|
adverseCount: 1
|
|
})
|
|
})
|
|
|
|
it('ranks the adverse outcome worst-first and ignores benign settled states', () => {
|
|
expect(
|
|
summarizeSubagentGroup([
|
|
agent({ id: 'a', state: 'working' }),
|
|
agent({ id: 'b', state: 'stopped' }),
|
|
agent({ id: 'c', state: 'failed' })
|
|
]).adverseState
|
|
).toBe('failed')
|
|
expect(
|
|
summarizeSubagentGroup([
|
|
agent({ id: 'a', state: 'working' }),
|
|
agent({ id: 'b', state: 'idle' }),
|
|
agent({ id: 'c', state: 'completed' })
|
|
]).adverseState
|
|
).toBeNull()
|
|
})
|
|
|
|
it('keeps working the only non-terminal state', () => {
|
|
expect(isTerminalSubagentState('working')).toBe(false)
|
|
for (const state of ['idle', 'completed', 'failed', 'stopped', 'unverifiable']) {
|
|
expect(isTerminalSubagentState(state)).toBe(true)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('subagentGroupFallbackText', () => {
|
|
it('names the failure a client without the block type would otherwise never see', () => {
|
|
expect(
|
|
subagentGroupFallbackText([
|
|
agent({ id: 'a', state: 'working' }),
|
|
agent({ id: 'b', state: 'working' }),
|
|
agent({ id: 'c', state: 'failed' })
|
|
])
|
|
).toBe('Kicked off 3 subagents — 2 working (1 failed)')
|
|
expect(
|
|
subagentGroupFallbackText([
|
|
agent({ id: 'a', state: 'completed' }),
|
|
agent({ id: 'b', state: 'stopped' })
|
|
])
|
|
).toBe('Ran 2 subagents (1 stopped)')
|
|
})
|
|
|
|
it('stays quiet when nothing has gone wrong', () => {
|
|
expect(subagentGroupFallbackText([agent({ id: 'a', state: 'working' })])).toBe(
|
|
'Kicked off 1 subagent — 1 working'
|
|
)
|
|
expect(subagentGroupFallbackText([agent({ id: 'a', state: 'completed' })])).toBe(
|
|
'Ran 1 subagent'
|
|
)
|
|
})
|
|
})
|