refactor(agent): clarify turn lifecycle event names
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' } })
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user