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

@@ -12,7 +12,6 @@ import AgentRegistry, {
import type {
AgentCancelCause,
AgentFactory,
ContinuationStop,
CreateAgentOptions,
ResumeAgentOptions,
SendOptions,
@@ -30,6 +29,7 @@ function stubAgent(rawId: string, overrides: Partial<Agent> = {}): Agent {
ctx: new Context(),
send: () => AgentMessageId('stub'),
cancel() {},
retry() {},
whenIdle() { return Promise.resolve() },
...overrides,
})
@@ -58,14 +58,6 @@ describe('Agent delivery aliases', () => {
})
describe('AgentRegistry', () => {
it('allows terminal stop policy to cooperate asynchronously with turn cancellation', () => {
type TurnStopListener = Events['agent/turn-stop']
type AsyncTurnStopListener = () => Promise<ContinuationStop | undefined>
expectTypeOf<AsyncTurnStopListener>().toExtend<TurnStopListener>()
expectTypeOf<Awaited<ReturnType<TurnStopListener>>>().toEqualTypeOf<ContinuationStop | undefined>()
})
it('registers exact entries, emits lifecycle events, and unregisters on owner disposal', async () => {
const ctx = new Context()
await ctx.plugin(AgentRegistry)
@@ -219,7 +211,7 @@ describe('agentEvents()', () => {
describe('explicit cancellation helpers', () => {
it('exposes the closed typed cancellation cause at the Agent seam', () => {
expectTypeOf<Parameters<Agent['cancel']>[0]>().toEqualTypeOf<AgentCancelCause | undefined>()
expectTypeOf<Parameters<Agent['cancel']>[0]>().toEqualTypeOf<AgentCancelCause>()
expectTypeOf<Parameters<Events['agent/cancel-requested']>[1]>().toEqualTypeOf<AgentCancelCause>()
})

View File

@@ -17,19 +17,14 @@ function mockAgent(id: string): Agent {
}
describe('agent status invariants', () => {
it('accepts lifecycle transitions through idle, running, and disposed', async () => {
it('accepts lifecycle transitions between idle and running', async () => {
const ctx = await setup()
const agent = mockAgent('a1')
expect(() => {
ctx.emit(scopeTarget(agent, agent), 'agent/status', agent, 'idle')
ctx.emit(scopeTarget(agent, agent), 'agent/status', agent, 'running')
ctx.emit(scopeTarget(agent, agent), 'agent/status', agent, 'idle')
ctx.emit(scopeTarget(agent, agent), 'agent/status', agent, 'disposed')
}).not.toThrow()
const running = mockAgent('a2')
ctx.emit(scopeTarget(running, running), 'agent/status', running, 'running')
expect(() => { ctx.emit(scopeTarget(running, running), 'agent/status', running, 'disposed') }).not.toThrow()
})
it('rejects a no-op transition', async () => {
@@ -40,14 +35,6 @@ describe('agent status invariants', () => {
.toThrow(/no-op transition/)
})
it('rejects leaving the terminal disposed state', async () => {
const ctx = await setup()
const agent = mockAgent('a4')
ctx.emit(scopeTarget(agent, agent), 'agent/status', agent, 'disposed')
expect(() => { ctx.emit(scopeTarget(agent, agent), 'agent/status', agent, 'idle') })
.toThrow(/left terminal state disposed/)
})
it('tracks agents independently', async () => {
const ctx = await setup()
const a = mockAgent('a5')

View File

@@ -21,25 +21,25 @@ describe('installAgentLlmTarget()', () => {
expect((await ctx.systemPrompt.assemble()).variables).toEqual({})
await expect(agentEvents(ctx, agent).waterfall(
'agent/request', 1, 0, seed, signal, () => Promise.resolve(seed),
'agent/request', 1, 0, signal, () => Promise.resolve(seed),
)).resolves.toBe(seed)
target.current = { provider: 'alpha', model: 'a1' }
expect((await ctx.systemPrompt.assemble()).variables).toMatchObject({ provider: 'alpha', model: 'a1' })
target.current = { provider: 'beta', model: 'b1' }
await expect(agentEvents(ctx, agent).waterfall(
'agent/request', 1, 0, seed, signal, () => Promise.resolve(seed),
'agent/request', 1, 0, signal, () => Promise.resolve(seed),
)).resolves.toEqual({ provider: 'alpha', model: 'a1', temperature: 0.2 })
expect((await ctx.systemPrompt.assemble()).variables).toMatchObject({ provider: 'beta', model: 'b1' })
await expect(agentEvents(ctx, agent).waterfall(
'agent/request', 1, 1, seed, signal, () => Promise.resolve(seed),
'agent/request', 1, 1, signal, () => Promise.resolve(seed),
)).resolves.toEqual({ provider: 'beta', model: 'b1', temperature: 0.2 })
dispose()
expect((await ctx.systemPrompt.assemble()).variables).toEqual({})
await expect(agentEvents(ctx, agent).waterfall(
'agent/request', 2, 0, seed, signal, () => Promise.resolve(seed),
'agent/request', 2, 0, signal, () => Promise.resolve(seed),
)).resolves.toBe(seed)
await ctx.fiber.dispose()
})