fix(subagent): complete output selection contract
This commit is contained in:
@@ -15,6 +15,22 @@ function reasoningDelta(text: string): SessionEvent {
|
||||
return { type: 'assistant/chunk', data: { chunk: { type: 'reasoning-delta', text } } } as SessionEvent
|
||||
}
|
||||
|
||||
function toolResult(text: string): SessionEvent {
|
||||
return {
|
||||
type: 'tool/result',
|
||||
data: {
|
||||
message: {
|
||||
content: [{
|
||||
type: 'tool-result',
|
||||
toolCallId: 'call-1',
|
||||
content: [{ type: 'text', text }],
|
||||
isError: false,
|
||||
}],
|
||||
},
|
||||
},
|
||||
} as SessionEvent
|
||||
}
|
||||
|
||||
describe('finalAssistantOutput', () => {
|
||||
it('selects the last non-empty message past a later empty usage-only message', () => {
|
||||
const events = [
|
||||
@@ -25,19 +41,30 @@ describe('finalAssistantOutput', () => {
|
||||
expect(finalAssistantOutput(events)).toEqual([{ type: 'text', text: 'step two' }])
|
||||
})
|
||||
|
||||
it('prefers a non-empty message over the streamed text', () => {
|
||||
it('prefers a non-empty message over text streamed before and after it', () => {
|
||||
const events = [
|
||||
textDelta('streamed '),
|
||||
textDelta('text'),
|
||||
textDelta('earlier partial'),
|
||||
message([{ type: 'text', text: 'complete answer' }]),
|
||||
textDelta('later partial'),
|
||||
message([]),
|
||||
]
|
||||
expect(finalAssistantOutput(events)).toEqual([{ type: 'text', text: 'complete answer' }])
|
||||
})
|
||||
|
||||
it('falls back to accumulated text deltas when no non-empty message exists', () => {
|
||||
it('treats textless assistant content as a non-empty message', () => {
|
||||
const content: ContentBlock[] = [{ type: 'reasoning', text: 'complete reasoning' }]
|
||||
expect(finalAssistantOutput([
|
||||
textDelta('streamed text'),
|
||||
message(content),
|
||||
textDelta('later partial'),
|
||||
])).toEqual(content)
|
||||
})
|
||||
|
||||
it('falls back to text deltas without including reasoning or tool-result content', () => {
|
||||
const events = [
|
||||
reasoningDelta('thinking'),
|
||||
textDelta('partial '),
|
||||
toolResult('tool output'),
|
||||
textDelta('answer'),
|
||||
message([]),
|
||||
]
|
||||
|
||||
@@ -1201,10 +1201,9 @@ describe('continuable review regressions', () => {
|
||||
})
|
||||
|
||||
it('keeps the epoch\'s earlier text past a final empty usage-only message', async () => {
|
||||
// Step 1 streams text plus a tool call; step 2 hits max-tokens having
|
||||
// assembled only a tool-call block, so the loop appends an EMPTY
|
||||
// assistant/message to host usage. The terminal edge reports the epoch's
|
||||
// real answer text, not the internal usage marker.
|
||||
// A tool-only max-tokens step records an empty assistant/message for
|
||||
// usage. The terminal event retains the previous assistant content,
|
||||
// including its tool call but not the intervening tool result.
|
||||
const { ctx, parent } = await setup([
|
||||
toolCallResponse('t1', 'noop', {}, 'partial one'),
|
||||
[
|
||||
|
||||
@@ -264,8 +264,8 @@ describe('SubagentService', () => {
|
||||
stopReason: 'completed',
|
||||
}))
|
||||
|
||||
// "No output" has ONE encoding on the end edge: the field is absent,
|
||||
// never an empty array, matching the continuable epoch edge.
|
||||
// The lifecycle event omits lastAssistantMessage when output is empty,
|
||||
// matching the continuable epoch event.
|
||||
const silent = new StubProvider('silent', NO_CAPS, { output: [], stopReason: 'completed' })
|
||||
subagents.registerProvider(silent)
|
||||
const silentRun = await subagents.start('silent', baseRequest())
|
||||
|
||||
Reference in New Issue
Block a user