refactor(agent-loop): simplify observable state machine
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user