refactor(agent): clarify turn lifecycle event names

This commit is contained in:
_Kerman
2026-07-27 17:38:42 +08:00
parent 7d5cc498d3
commit 52174e32cb
67 changed files with 180 additions and 176 deletions

View File

@@ -178,7 +178,7 @@ describe('AgentLoop initiator scope', () => {
if (subject === agent) capture(signal)
return next()
})
ctx.on('agent/stopping', (subject, _turn, signal) => {
ctx.on('agent/turn-stopping', (subject, _turn, signal) => {
if (subject === agent) capture(signal)
})
ctx.tools.register(defineContentToolFixture({

View File

@@ -482,7 +482,7 @@ describe('Agent.cancel()', () => {
})
let cancelled = false
ctx.on('agent/stopping', (subject) => {
ctx.on('agent/turn-stopping', (subject) => {
if (subject === agent && !cancelled) {
cancelled = true
agent.cancel({ kind: 'user' })
@@ -719,7 +719,7 @@ describe('Agent.cancel()', () => {
})
break
case 'stopping':
ctx.on('agent/stopping', async (subject, _turn, signal) => {
ctx.on('agent/turn-stopping', async (subject, _turn, signal) => {
if (subject === agent) await blockUntilAbort(signal)
})
break

View File

@@ -270,7 +270,7 @@ describe('abort during tool execution ends the turn', () => {
})
describe('steering from late extension points is never stranded', () => {
it('steer() from an agent/stopping listener continues the same turn', async () => {
it('steer() from an agent/turn-stopping listener continues the same turn', async () => {
const adapter = new MockAdapter([
textResponse('no tools, would stop here'),
textResponse('continued because of steering'),
@@ -279,7 +279,7 @@ describe('steering from late extension points is never stranded', () => {
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
let steeredOnce = false
ctx.on('agent/stopping', () => {
ctx.on('agent/turn-stopping', () => {
if (!steeredOnce) {
steeredOnce = true
agent.steer({ content: [{ type: 'text', text: 'one more thing' }], source: { kind: 'user' } })
@@ -355,13 +355,13 @@ describe('steering from late extension points is never stranded', () => {
})
describe('plugin exceptions are contained', () => {
it('a throwing agent/stopping listener ends the turn with an error, loop survives', async () => {
it('a throwing agent/turn-stopping listener ends the turn with an error, loop survives', async () => {
const adapter = new MockAdapter([textResponse('one'), textResponse('two')])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('a1'), { provider: 'mock', model: 'mock' })
let threwOnce = false
ctx.on('agent/stopping', async () => {
ctx.on('agent/turn-stopping', async () => {
if (!threwOnce) {
threwOnce = true
throw new Error('broken continuation plugin')

View File

@@ -348,17 +348,18 @@ describe('stream failure edges', () => {
})
describe('post-turn continuation edges', () => {
it('an agent/idle listener that enqueues a waking prompt preempts continueOrIdle', async () => {
it('an agent/settled listener that enqueues a waking prompt preempts continueOrIdle', async () => {
const adapter = new MockAdapter([textResponse('one'), textResponse('two')])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('idle-preempt'), { provider: 'mock', model: 'mock' })
const agent = ctx.agentLoop.create(SessionId('settled-preempt'), { provider: 'mock', model: 'mock' })
let injected = false
ctx.on('agent/idle', (subject) => {
ctx.on('agent/settled', (subject) => {
if (subject !== agent || injected) return
expect(subject.status).toBe('running')
injected = true
// kick() installs the next admission synchronously, so the following
// continueOrIdle() sees an abort owner and yields to it.
send(agent, 'follow-up from idle listener')
send(agent, 'follow-up from settled listener')
})
send(agent, 'go')
@@ -531,8 +532,8 @@ describe('driver bookkeeping edges', () => {
// (the run's containment covers only session appends); the waiter's
// catch arm must treat that rejection as quiescence instead of
// propagating it.
ctx.on('agent/idle', (subject) => {
if (subject === agent) throw new Error('idle listener exploded')
ctx.on('agent/settled', (subject) => {
if (subject === agent) throw new Error('settled listener exploded')
})
send(agent, 'one')

View File

@@ -11,7 +11,7 @@ import { MockAdapter, textResponse, toolCallResponse } from './mock-adapter.ts'
/**
* The interception seams introduced by the hooks taxonomy: `agent/prompt-submit`,
* `agent/session-start`, `agent/stopping`, and the
* `agent/session-start`, `agent/turn-stopping`, and the
* `tools/pre-execute` / `tools/post-execute`
* split with `additionalContexts` buffering. These verify the canonical event
* surface a hook bridge (or a native plugin) programs against, WITHOUT any

View File

@@ -463,7 +463,7 @@ describe('agent loop', () => {
expect(agent.session.events.some(event => event.type === 'user/message' && event.data.source.kind === 'plugin')).toBe(false)
})
it('agent/stopping can steer another step (/loop pattern)', async () => {
it('agent/turn-stopping can steer another step (/loop pattern)', async () => {
const adapter = new MockAdapter([
textResponse('step 1'),
textResponse('step 2'),
@@ -474,7 +474,7 @@ describe('agent loop', () => {
let steps = 0
ctx.on('session/event', (_session, event) => { if (event.type === 'step/end') steps++ })
ctx.on('agent/stopping', (subject) => {
ctx.on('agent/turn-stopping', (subject) => {
if (steps < 3) {
subject.steer({ content: [{ type: 'text', text: 'continue' }], source: { kind: 'plugin', plugin: 'loop-test' } })
}
@@ -722,7 +722,7 @@ describe('agent loop', () => {
ctx.on('session/event', (_session, event) => { if (event.type === 'step/end') steps++ })
// Force exactly one continuation (step 1 → step 2), then defer to default
// (step 2 is a plain stop with no tool calls → stops).
ctx.on('agent/stopping', (subject) => {
ctx.on('agent/turn-stopping', (subject) => {
if (steps < 2) {
subject.steer({ content: [{ type: 'text', text: 'continue after truncation' }], source: { kind: 'plugin', plugin: 'max-tokens-test' } })
}

View File

@@ -57,12 +57,12 @@ describe('agent/request-error', () => {
const agent = ctx.agentLoop.create(SessionId('request-error-retry'), { provider: 'mock', model: 'mock' })
const seen: { turn: number; step: number; failure: LlmFailure }[] = []
const statuses: string[] = []
const idleTurns: number[] = []
const settledTurns: number[] = []
ctx.on('agent/status', (subject, status) => {
if (subject === agent) statuses.push(status)
})
ctx.on('agent/idle', (subject, turn) => {
if (subject === agent) idleTurns.push(turn)
ctx.on('agent/settled', (subject, turn) => {
if (subject === agent) settledTurns.push(turn)
})
ctx.on('agent/request-error', async (subject, turn, step, _error, failure) => {
expect(subject).toBe(agent)
@@ -101,7 +101,7 @@ describe('agent/request-error', () => {
{ kind: 'retry' },
])
expect(statuses).toEqual(['running', 'idle'])
expect(idleTurns).toEqual([3])
expect(settledTurns).toEqual([3])
})
it('lets cancellation win over a retry request', async () => {