fix pre-step lifecycle regressions

This commit is contained in:
_Kerman
2026-07-31 19:40:59 +08:00
parent fcc2b5e282
commit 8e88b17c9f
37 changed files with 331 additions and 265 deletions

View File

@@ -217,6 +217,7 @@ export class ReactLoopAgent implements Agent {
await this.loopCtx.serial(agentCarrier(this), 'agent/turn-stopping', this, turn, signal)
signal.throwIfAborted()
}
if (turnEnds && this.inbox.nextStep.length === 0) break
decision = await this.preStep('next-step', { turn, step: phase.step + 1 })
if (decision.kind === 'reject') {
turnEnds = { kind: 'blocked' }

View File

@@ -292,7 +292,6 @@ describe('abort during tool execution ends the turn', () => {
{ type: 'finish', reason: { kind: 'tool-calls' } },
] satisfies StreamChunk[],
textResponse('later turn'),
textResponse('context accepted'),
])
const ctx = await harness(adapter)
const agent = ctx.agentLoop.create(SessionId('a-historical-tool-pair'), { provider: 'mock', model: 'mock' })
@@ -338,7 +337,7 @@ describe('abort during tool execution ends the turn', () => {
? [event.data.content]
: [])[0])
.toEqual([{ type: 'text', text: 'new turn context' }])
expect(JSON.stringify(adapter.requests[2]?.messages)).toContain('new turn context')
expect(JSON.stringify(adapter.requests[1]?.messages)).toContain('new turn context')
})
})

View File

@@ -313,11 +313,12 @@ describe('agent loop', () => {
const steering = agent.session.events.find(e =>
e.type === 'user/message' && JSON.stringify(e.data.content).includes('change of plans'))
expect(steering).toBeDefined()
// Steering enters history before the second step's request derives it.
// The entered batch is appended after the second step opens and before its
// request derives history.
const steeringSeq = steering!.seq
const secondStepStart = agent.session.events.filter(e => e.type === 'step/start')[1]
expect(secondStepStart).toBeDefined()
expect(steeringSeq).toBeLessThan(secondStepStart!.seq)
expect(steeringSeq).toBeGreaterThan(secondStepStart!.seq)
// the second model request saw the steering content
const secondRequest = adapter.requests[1]

View File

@@ -590,14 +590,18 @@ describe('request stability across the loop', () => {
adapter.requests.forEach((request, index) => {
const stepStart = stepStarts[index]!
// Messages: the derivation over the log prefix strictly before this
// step's step/start — rebuilt here through a completely fresh Session.
const rebuilt = new Session(SessionId(`rebuild-${index}`), structuredClone(events.slice(0, stepStart.seq)))
const firstChunk = events.find(e =>
e.type === 'assistant/chunk'
&& e.data.turn === stepStart.data.turn
&& e.data.step === stepStart.data.step,
)!
// Messages: the entered batch is logged after step/start, so rebuild the
// complete dispatch prefix through a completely fresh Session.
const rebuilt = new Session(SessionId(`rebuild-${index}`), structuredClone(events.slice(0, firstChunk.seq)))
expect(structuredClone(request.messages)).toEqual(rebuilt.deriveMessages())
// Header: the latest request/header snapshot up to this step's dispatch
// (its header event sits between step/start and the first chunk).
const firstChunk = events.find(e => e.type === 'assistant/chunk' && e.seq > stepStart.seq)!
const header = foldRequestHeader(events.slice(0, firstChunk.seq))!
expect(request.model).toBe(header.config.model)
expect(request.reasoningEffort).toBe(header.config.reasoningEffort)