refactor(agent-loop): simplify observable state machine

This commit is contained in:
_Kerman
2026-07-24 21:18:48 +08:00
parent fb0ef82aa6
commit b73eb7663c
131 changed files with 2011 additions and 4292 deletions

View File

@@ -4,6 +4,8 @@ import { join } from 'node:path'
import { tmpdir } from 'node:os'
import { Context } from 'cordis'
import { CallId, type Message } from '@deepseek-ai/dsh-llm'
import { AgentMessageId } from '@deepseek-ai/dsh-agent'
import { Session, SessionId } from '@deepseek-ai/dsh-session'
import { createScope, type Scope } from '@deepseek-ai/dsh-scope'
import SystemPrompt, { renderPrompt } from '@deepseek-ai/dsh-system-prompt'
import ToolRegistry, { defineContentToolFixture } from '@deepseek-ai/dsh-tools'
@@ -35,7 +37,28 @@ async function setup(home: string, config: toolSkill.Config = {}): Promise<Conte
}
function agentForCwd(cwd: string): Agent {
return { session: { header: { cwd } } } as unknown as Agent
const id = SessionId(`tool-skill-${cwd}`)
const session = new Session(id, [], { version: 0, id, createdAt: 0, cwd })
return {
ctx: new Context(),
id,
options: {},
session,
status: 'idle',
send: () => AgentMessageId('stub'),
followup: () => AgentMessageId('stub'),
steer: () => AgentMessageId('stub'),
inject(content, options) {
session.append('user/message', {
content,
source: options?.source ?? { kind: 'user' },
}, { surfaceOp: 'append' })
return AgentMessageId('stub')
},
cancel() {},
retry() {},
whenIdle: () => Promise.resolve(),
}
}
async function composePrefix(ctx: Context, cwd: string, signal = new AbortController().signal): Promise<Message[]> {
@@ -43,11 +66,8 @@ async function composePrefix(ctx: Context, cwd: string, signal = new AbortContro
}
async function composePrefixForAgent(ctx: Context, agent: Agent, signal = new AbortController().signal): Promise<Message[]> {
const empty: Message[] = []
return await agentEvents(ctx, agent).waterfall(
'agent/session-prefix', empty, signal,
() => Promise.resolve(empty),
)
await agentEvents(ctx, agent).serial('agent/step', 1, 1, signal)
return agent.session.deriveMessages()
}
async function mintAgentScope(ctx: Context, cwd: string): Promise<{ agent: Agent; scope: Scope }> {
@@ -86,7 +106,7 @@ describe('dsh-tool-skill', () => {
expect(ctx.tools.schemas().map(tool => tool.name)).toEqual(['skill'])
})
it('forwards the session-prefix abort signal to skill discovery', async () => {
it('forwards the step abort signal to skill discovery', async () => {
const home = await tempDir('tool-prefix-signal')
const ctx = await setup(home)
let seenSignal: AbortSignal | undefined
@@ -107,7 +127,7 @@ describe('dsh-tool-skill', () => {
expect(seenSignal).toBe(controller.signal)
})
it('contributes a stable name-and-description catalog through the session prefix', async () => {
it('injects a stable durable name-and-description catalog at the first step', async () => {
const home = await tempDir('tool-catalog')
const ctx = await setup(home, { catalogDescriptionMaxLength: 50 })
ctx.skills.register({
@@ -126,10 +146,11 @@ describe('dsh-tool-skill', () => {
provider: 'runtime',
content: 'A body.',
})
ctx.on('agent/session-prefix', async (_agent, _prefix, _signal, next): Promise<Message[]> => [
{ role: 'user', content: [{ type: 'text', text: 'later contribution' }] },
...await next(),
])
ctx.on('agent/step', (agent) => {
agent.inject([{ type: 'text', text: 'later contribution' }], {
source: { kind: 'plugin', plugin: 'later-contribution' },
})
})
const prefix = await composePrefix(ctx, '/workspace')
@@ -162,7 +183,7 @@ describe('dsh-tool-skill', () => {
expect(renderPrompt(await ctx.systemPrompt.assemble({ agent: agentForCwd('/workspace') }))).not.toContain('<available_skills>')
})
it('does not contribute a session-prefix message when no skills are available', async () => {
it('does not inject a catalog when no skills are available', async () => {
const home = await tempDir('tool-empty-catalog')
const ctx = await setup(home)