refactor(agent-loop): simplify message machine
This commit is contained in:
@@ -6,7 +6,6 @@ import AgentRegistry, {
|
||||
type AgentCancelCause,
|
||||
type AgentOptions,
|
||||
type AgentStatus,
|
||||
type SendOptions,
|
||||
} from '@deepseek-ai/dsh-agent'
|
||||
import type {
|
||||
ContentBlock,
|
||||
@@ -27,7 +26,6 @@ interface FakeAgent extends Agent {
|
||||
status: AgentStatus
|
||||
sent: ContentBlock[][]
|
||||
sentMessages: UserMessage[]
|
||||
sentOptions: (SendOptions | undefined)[]
|
||||
steered: ContentBlock[][]
|
||||
steeredIds: MessageId[]
|
||||
steeredOptions: UserMessage[]
|
||||
@@ -178,7 +176,6 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
if (options.omitInitialLifecycle !== true) {
|
||||
session.append('turn/start', {
|
||||
turn: 1,
|
||||
trigger: { kind: 'message', source: { kind: 'user' } },
|
||||
})
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
}
|
||||
@@ -187,7 +184,6 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
const sentMessages: UserMessage[] = []
|
||||
const steered: ContentBlock[][] = []
|
||||
const steeredIds: MessageId[] = []
|
||||
const sentOptions: (SendOptions | undefined)[] = []
|
||||
const steeredOptions: UserMessage[] = []
|
||||
const injected: ContentBlock[][] = []
|
||||
const injectedOptions: UserMessage[] = []
|
||||
@@ -203,23 +199,15 @@ export async function createTuiTestHarness<TerminalType extends Terminal, Exit e
|
||||
ctx,
|
||||
sent,
|
||||
sentMessages,
|
||||
sentOptions,
|
||||
steered,
|
||||
steeredIds,
|
||||
steeredOptions,
|
||||
injected,
|
||||
injectedOptions,
|
||||
cancelled,
|
||||
send(input, options) {
|
||||
sent.push(input.content)
|
||||
sentMessages.push(input)
|
||||
sentOptions.push(options)
|
||||
return input.id
|
||||
},
|
||||
followup(input) {
|
||||
sent.push(input.content)
|
||||
sentMessages.push(input)
|
||||
sentOptions.push(undefined)
|
||||
return input.id
|
||||
},
|
||||
steer(input) {
|
||||
|
||||
@@ -322,7 +322,6 @@ describe('TUI terminal-state snapshots', () => {
|
||||
session.append('turn/end', { turn: 1, reason: { kind: 'completed' } })
|
||||
session.append('turn/start', {
|
||||
turn: 2,
|
||||
trigger: { kind: 'message', source: { kind: 'user' } },
|
||||
})
|
||||
appendUser(session, 'Next question.')
|
||||
},
|
||||
@@ -774,14 +773,14 @@ describe('TUI terminal-state snapshots', () => {
|
||||
turn: 1,
|
||||
reason: { kind: 'error', step: 1, message: 'provider stream failed after partial output' },
|
||||
})
|
||||
harness.session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
harness.session.append('turn/start', { turn: 2 })
|
||||
harness.session.append('turn/end', {
|
||||
turn: 2,
|
||||
reason: { kind: 'interrupted' },
|
||||
})
|
||||
harness.session.append('turn/start', { turn: 3, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
harness.session.append('turn/start', { turn: 3 })
|
||||
harness.session.append('turn/end', { turn: 3, reason: { kind: 'disposed' } })
|
||||
harness.session.append('turn/start', { turn: 4, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
harness.session.append('turn/start', { turn: 4 })
|
||||
// A merge-extensible turn-end kind unknown to the TUI still surfaces its
|
||||
// name so the agent never stops without a visible reason.
|
||||
harness.session.append('turn/end', { turn: 4, reason: { kind: 'plugin-policy' } as never })
|
||||
@@ -822,7 +821,7 @@ describe('TUI terminal-state snapshots', () => {
|
||||
const log = (meta: typeof earlier, title: string, day: string): { meta: typeof earlier; events: SessionEvent[] } => ({
|
||||
meta,
|
||||
events: [
|
||||
{ type: 'turn/start', seq: 0, time: Date.parse(`${day}T00:00:01Z`), data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
|
||||
{ type: 'turn/start', seq: 0, time: Date.parse(`${day}T00:00:01Z`), data: { turn: 1 } },
|
||||
{ type: 'user/message', seq: 1, time: Date.parse(`${day}T00:00:02Z`), data: createUserMessage({ content: [{ type: 'text', text: 'restore the selector' }], source: { kind: 'user' } }), surfaceOp: 'append' },
|
||||
{ type: 'step/start', seq: 2, time: Date.parse(`${day}T00:00:03Z`), data: { turn: 1, step: 1 } },
|
||||
{ type: 'request/header', seq: 3, time: Date.parse(`${day}T00:00:04Z`), data: { header: { config: { provider: 'deepseek', model: 'deepseek-v4-pro' } }, reason: 'initial' } },
|
||||
|
||||
@@ -239,7 +239,7 @@ describe('goodbye message and /resume', () => {
|
||||
time = 100,
|
||||
reason: TurnEndReason = { kind: 'completed' },
|
||||
): SessionEvent[] => [
|
||||
{ type: 'turn/start', seq: 0, time, data: { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } } },
|
||||
{ type: 'turn/start', seq: 0, time, data: { turn: 1 } },
|
||||
{ type: 'user/message', seq: 1, time: time + 1, data: createUserMessage({
|
||||
content: [{ type: 'text', text: 'resume me' }], source: { kind: 'user' },
|
||||
}), surfaceOp: 'append' },
|
||||
@@ -1333,9 +1333,9 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
appendAssistant(result.session, [])
|
||||
result.session.append('step/end', { turn: 1, step: 1 })
|
||||
result.session.append('turn/end', { turn: 1, reason: { kind: 'aborted' } })
|
||||
result.session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
result.session.append('turn/start', { turn: 2 })
|
||||
result.session.append('turn/end', { turn: 2, reason: { kind: 'completed' } })
|
||||
result.session.append('turn/start', { turn: 3, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
result.session.append('turn/start', { turn: 3 })
|
||||
result.session.append('step/start', { turn: 3, step: 1 })
|
||||
result.session.append('assistant/chunk', {
|
||||
turn: 3,
|
||||
@@ -1872,7 +1872,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
expect(result.terminal.output).not.toContain('│')
|
||||
expect(result.terminal.output).not.toContain('Response 1s')
|
||||
|
||||
result.session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
result.session.append('turn/start', { turn: 2 })
|
||||
result.session.append('step/start', { turn: 2, step: 1 })
|
||||
clock += 1_000
|
||||
result.terminal.output = ''
|
||||
@@ -3760,7 +3760,7 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
const events = await setup()
|
||||
const unrelatedSession = events.ctx.sessions.create(SessionId('unrelated-session'))
|
||||
const unrelatedAgent = { ...events.agent, id: unrelatedSession.id, session: unrelatedSession }
|
||||
unrelatedSession.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
unrelatedSession.append('turn/start', { turn: 1 })
|
||||
unrelatedSession.append('todo/write', { todos: [{ content: 'hidden', status: 'pending' }] })
|
||||
agentEvents(events.ctx, unrelatedAgent).emit('agent/status', 'running')
|
||||
agentEvents(events.ctx, unrelatedAgent).emit('agent/error', 1, 1, new Error('hidden error'))
|
||||
@@ -3768,22 +3768,22 @@ describe('pi-tui chat lifecycle and transcript', () => {
|
||||
agentEvents(events.ctx, events.agent).emit('agent/error', 1, 1, new Error('live failure'))
|
||||
events.session.append('step/end', { turn: 1, step: 1 })
|
||||
events.session.append('turn/end', { turn: 1, reason: { kind: 'error', step: 1, message: 'live failure' } })
|
||||
events.session.append('turn/start', { turn: 2, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
events.session.append('turn/start', { turn: 2 })
|
||||
events.session.append('turn/end', { turn: 2, reason: { kind: 'error', step: 1, message: 'durable failure' } })
|
||||
events.session.append('turn/start', { turn: 3, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
events.session.append('turn/start', { turn: 3 })
|
||||
events.session.append('turn/end', { turn: 3, reason: { kind: 'aborted' } })
|
||||
events.session.append('turn/start', { turn: 4, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
events.session.append('turn/start', { turn: 4 })
|
||||
events.session.append('turn/end', { turn: 4, reason: { kind: 'max-tokens' } })
|
||||
events.session.append('turn/start', { turn: 5, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
events.session.append('turn/start', { turn: 5 })
|
||||
events.session.append('turn/end', { turn: 5, reason: { kind: 'interrupted' } })
|
||||
events.session.append('turn/start', { turn: 6, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
events.session.append('turn/start', { turn: 6 })
|
||||
events.session.append('turn/end', {
|
||||
turn: 6,
|
||||
reason: { kind: 'error', step: 1, failure: { message: 'structured provider failure', code: 'SERVER' } },
|
||||
})
|
||||
events.session.append('turn/start', { turn: 8, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
events.session.append('turn/start', { turn: 8 })
|
||||
events.session.append('turn/end', { turn: 8, reason: { kind: 'disposed' } })
|
||||
events.session.append('turn/start', { turn: 9, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
events.session.append('turn/start', { turn: 9 })
|
||||
// Merge-extensible reason kind unknown to the TUI still names the stop.
|
||||
events.session.append('turn/end', { turn: 9, reason: { kind: 'plugin-policy' } as never })
|
||||
agentEvents(events.ctx, events.agent).emit('agent/disposed')
|
||||
@@ -4715,7 +4715,7 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
mountTui(ctx, { theme: { color: false } }, { terminal, exit: vi.fn() })
|
||||
@@ -4740,7 +4740,7 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
// Mirror dsh-tui's own inject (minus loader, the absence under test).
|
||||
@@ -4775,14 +4775,14 @@ describe('terminal mounting', () => {
|
||||
const otherSession = ctx.sessions.create(SessionId('other-session'))
|
||||
ctx.agents.register({
|
||||
id: otherSession.id, options: {}, session: otherSession, status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
expect(terminal.started).toBe(0)
|
||||
|
||||
const session = ctx.sessions.create(SessionId('late-session'))
|
||||
const agent = {
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
} as Agent
|
||||
ctx.agents.register(agent)
|
||||
await tick()
|
||||
@@ -4813,7 +4813,7 @@ describe('terminal mounting', () => {
|
||||
const session = ctx.sessions.create(SessionId('main-session'))
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'idle', acceptsNextStep: false, ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
await tick()
|
||||
expect(terminal.started).toBe(0)
|
||||
@@ -4853,11 +4853,11 @@ describe('terminal mounting', () => {
|
||||
await ctx.plugin(TuiPromptService)
|
||||
ctx.provide('tools', { get: () => undefined } as never)
|
||||
const session = ctx.sessions.create(SessionId('failed-start-session'))
|
||||
session.append('turn/start', { turn: 1, trigger: { kind: 'message', source: { kind: 'user' } } })
|
||||
session.append('turn/start', { turn: 1 })
|
||||
session.append('step/start', { turn: 1, step: 1 })
|
||||
ctx.agents.register({
|
||||
id: session.id, options: {}, session, status: 'running', acceptsNextStep: true, ctx,
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, send: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
followup: () => {}, steer: () => {}, inject: () => {}, cancel() {}, whenIdle: () => Promise.resolve(),
|
||||
})
|
||||
const terminal = new FakeTerminal()
|
||||
terminal.start = () => { throw new Error('terminal startup failed') }
|
||||
|
||||
Reference in New Issue
Block a user