refactor(agent-loop): simplify message machine
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
import type { Context } from 'cordis'
|
||||
import type { Agent, AgentOptions } from '@deepseek-ai/dsh-agent'
|
||||
import { findLastMessageTurnEnd, SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import { SessionId, type SessionEvent, type TurnEndReason } from '@deepseek-ai/dsh-session'
|
||||
import { createUserMessage, type ContentBlock } from '@deepseek-ai/dsh-llm'
|
||||
import { assertSubagentMaxDepth, delegationDepthOf } from '@deepseek-ai/dsh-subagent'
|
||||
import type { SubagentResult, SubagentRun, SubagentStartRequest, SubagentStopReason } from '@deepseek-ai/dsh-subagent'
|
||||
@@ -47,7 +47,6 @@ function toStopReason(reason: TurnEndReason | undefined): SubagentStopReason {
|
||||
case 'aborted':
|
||||
return 'aborted'
|
||||
case 'error':
|
||||
case 'disposed':
|
||||
case 'interrupted':
|
||||
default:
|
||||
return 'error'
|
||||
@@ -160,7 +159,8 @@ export async function startInProcessRun(
|
||||
|
||||
const result: Promise<SubagentResult> = (async () => {
|
||||
try {
|
||||
child.followup(createUserMessage({ content: request.prompt, source: { kind: 'user' } }))
|
||||
const message = createUserMessage({ content: request.prompt, source: { kind: 'user' } })
|
||||
child.followup(message)
|
||||
await child.whenIdle()
|
||||
return readResult(
|
||||
child,
|
||||
@@ -194,12 +194,11 @@ function readResult(
|
||||
): SubagentResult {
|
||||
const own = child.session.events.slice(seedLength)
|
||||
const lastMessage = own.findLast((event): event is SessionEvent<'assistant/message'> => event.type === 'assistant/message')
|
||||
const lastEnd = findLastMessageTurnEnd(own)
|
||||
const lastEnd = own.findLast((event): event is SessionEvent<'turn/end'> => event.type === 'turn/end')
|
||||
const output: ContentBlock[] = lastMessage?.data.message.content ?? []
|
||||
const recorded = toStopReason(lastEnd?.data.reason)
|
||||
// Disposal can tear the owner down before the loop records its ordinary
|
||||
// `aborted` end, yielding `disposed` instead. A requested cancellation owns
|
||||
// every non-completed in-flight outcome; a turn already completed stays so.
|
||||
// A requested cancellation owns every non-completed in-flight outcome; a
|
||||
// turn already completed stays so.
|
||||
const stopReason: SubagentStopReason = cancelled && recorded !== 'completed'
|
||||
? 'aborted'
|
||||
: recorded
|
||||
|
||||
@@ -56,34 +56,27 @@ describe('startInProcessRun', () => {
|
||||
expect(ctx.agents.get(run.id)).toBeUndefined()
|
||||
})
|
||||
|
||||
it('reports the message-turn outcome when a later non-message turn completes during flush', async () => {
|
||||
const { ctx, parent } = await setup([maxTokensResponse('partial answer')])
|
||||
let injected = false
|
||||
ctx.on('session/flush', (session) => {
|
||||
if (injected || session.header.parentSession === undefined) return
|
||||
const lastEnd = session.events.findLast(event => event.type === 'turn/end')
|
||||
if (lastEnd?.type !== 'turn/end' || lastEnd.data.reason.kind !== 'max-tokens') return
|
||||
injected = true
|
||||
const turn = lastEnd.data.turn + 1
|
||||
session.append('turn/start', {
|
||||
turn,
|
||||
trigger: { kind: 'injection', source: { kind: 'plugin', plugin: 'late-metadata' } },
|
||||
})
|
||||
session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'late metadata' }],
|
||||
source: { kind: 'plugin', plugin: 'late-metadata' },
|
||||
}), { surfaceOp: 'append' })
|
||||
session.append('turn/end', { turn, reason: { kind: 'completed' } })
|
||||
it('reports the final whole-agent outcome after idle replacement work', async () => {
|
||||
const { ctx, parent } = await setup([maxTokensResponse('partial answer'), textResponse('replacement answer')])
|
||||
let replaced = false
|
||||
ctx.on('agent/status', (agent, status) => {
|
||||
if (replaced || status !== 'idle' || agent.session.header.parentSession === undefined) return
|
||||
replaced = true
|
||||
agent.followup(createUserMessage({
|
||||
content: [{ type: 'text', text: 'replacement work' }],
|
||||
source: { kind: 'plugin', plugin: 'replacement' },
|
||||
}))
|
||||
})
|
||||
|
||||
const run = await startInProcessRun(request(parent), {})
|
||||
const result = await run.result
|
||||
const child = ctx.agents.get(run.id)!
|
||||
|
||||
expect(injected).toBe(true)
|
||||
expect(replaced).toBe(true)
|
||||
expect(child.session.events.findLast(event => event.type === 'turn/end'))
|
||||
.toMatchObject({ data: { reason: { kind: 'completed' } } })
|
||||
expect(result.stopReason).toBe('max-tokens')
|
||||
expect(result.stopReason).toBe('completed')
|
||||
expect(text(result.output)).toBe('replacement answer')
|
||||
await run.dispose()
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user