fix(agent-loop): append step/start before emitting agent/step-start (P1-6)
Every loop boundary appends the session event before emitting the Cordis
event (ADR 0003's append-before-emit rule) — except step/start, which was
inverted. A listener on agent/step-start that inspected session.events could
not see the step it was just told had started.
- Swap the two lines so session.append('step/start') precedes the emit.
- Fix the two stale pseudo-code copies (the runLoop JSDoc STEP-loop block and
docs/architecture.md) so neither shows step-start emitted before the append.
- Regression test: a step-start listener observes the matching step/start
event already at the tail of session.events. Verified the test fails on the
pre-fix (emit-first) order.
This commit is contained in:
@@ -134,7 +134,7 @@ forever:
|
|||||||
drain queued → session('user/message'…) → 'turn/start' → emit agent/turn-start
|
drain queued → session('user/message'…) → 'turn/start' → emit agent/turn-start
|
||||||
STEP loop:
|
STEP loop:
|
||||||
drain steering (late steering from previous step's listeners)
|
drain steering (late steering from previous step's listeners)
|
||||||
emit agent/step-start
|
session('step/start'); emit agent/step-start
|
||||||
assembly = ctx.systemPrompt.assemble() ⟵ waterfall system-prompt/assemble
|
assembly = ctx.systemPrompt.assemble() ⟵ waterfall system-prompt/assemble
|
||||||
req = {model, system, tools, messages: session.deriveMessages(), signal}
|
req = {model, system, tools, messages: session.deriveMessages(), signal}
|
||||||
req = waterfall agent/request ⟵ hooks, compaction, model switch
|
req = waterfall agent/request ⟵ hooks, compaction, model switch
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export interface LoopHandle {
|
|||||||
* drain queued → session('user/message'…) → 'turn/start' → emit agent/turn-start
|
* drain queued → session('user/message'…) → 'turn/start' → emit agent/turn-start
|
||||||
* STEP loop:
|
* STEP loop:
|
||||||
* drain steering → session('steering/message') ⟵ catches late steering
|
* drain steering → session('steering/message') ⟵ catches late steering
|
||||||
* emit agent/step-start
|
* session('step/start'); emit agent/step-start ⟵ append before emit (ADR 0003)
|
||||||
* assembly = ctx.systemPrompt.assemble() ⟵ waterfall system-prompt/assemble
|
* assembly = ctx.systemPrompt.assemble() ⟵ waterfall system-prompt/assemble
|
||||||
* req = {model, system, tools, messages: session.deriveMessages(), signal}
|
* req = {model, system, tools, messages: session.deriveMessages(), signal}
|
||||||
* req = waterfall agent/request ⟵ hooks/compaction/model-switch
|
* req = waterfall agent/request ⟵ hooks/compaction/model-switch
|
||||||
@@ -174,8 +174,8 @@ async function runTurn(ctx: Context, agent: LoopAgent, handle: LoopHandle, turn:
|
|||||||
// (or turn-start listeners on the first step) joins before the request.
|
// (or turn-start listeners on the first step) joins before the request.
|
||||||
drainSteering(ctx, agent, turn)
|
drainSteering(ctx, agent, turn)
|
||||||
|
|
||||||
ctx.emit('agent/step-start', agent, turn, step)
|
|
||||||
session.append('step/start', { turn, step })
|
session.append('step/start', { turn, step })
|
||||||
|
ctx.emit('agent/step-start', agent, turn, step)
|
||||||
|
|
||||||
const abort = new AbortController()
|
const abort = new AbortController()
|
||||||
handle.setAbort(abort)
|
handle.setAbort(abort)
|
||||||
|
|||||||
@@ -610,3 +610,32 @@ describe('HIGH: a finish-error stream chunk ends the turn as error, not complete
|
|||||||
expect(reasons).toEqual([{ kind: 'error', message: 'codeless failure' }])
|
expect(reasons).toEqual([{ kind: 'error', message: 'codeless failure' }])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('P1-6: step/start is appended before agent/step-start is emitted', () => {
|
||||||
|
it('a step-start listener sees the step/start event already in session.events', async () => {
|
||||||
|
const adapter = new MockAdapter([textResponse('done')])
|
||||||
|
const ctx = await harness(adapter)
|
||||||
|
const agent = ctx.agentLoop.create('a-step-order', { model: 'mock' })
|
||||||
|
|
||||||
|
// Capture, at the moment agent/step-start fires, whether the matching
|
||||||
|
// step/start event is already in the log (append-before-emit, ADR 0003).
|
||||||
|
const observed: { turn: number; step: number; lastEventType: string | undefined; sawStepStart: boolean }[] = []
|
||||||
|
ctx.on('agent/step-start', (subject, turn, step) => {
|
||||||
|
if (subject !== agent) return
|
||||||
|
const events = [...subject.session.events]
|
||||||
|
const last = events.at(-1)
|
||||||
|
observed.push({
|
||||||
|
turn,
|
||||||
|
step,
|
||||||
|
lastEventType: last?.type,
|
||||||
|
sawStepStart: events.some(e => e.type === 'step/start' && e.data.turn === turn && e.data.step === step),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
send(agent, 'go')
|
||||||
|
await waitForIdle(ctx, agent)
|
||||||
|
|
||||||
|
expect(observed).toHaveLength(1)
|
||||||
|
expect(observed[0]).toMatchObject({ turn: 1, step: 1, lastEventType: 'step/start', sawStepStart: true })
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user