fix(agent-loop): park empty turn batches, keep max-tokens sticky, and settle headless at idle
- turn boundary now returns false for an empty admitted batch (claimed input removed before the wake) instead of opening a turn and spending a model call on nothing; the step boundary already had the symmetric guard. - a max-token step stays sticky when steering or injected work continues the turn: a later completed step no longer downgrades the outcome, matching the TurnEndReasonMap contract. - session/queue wire schema accepts the context placement (previously the zod union rejected injected-context snapshots wholesale and the client silently dropped the whole frame); schema tests cover all placements. - headless runs settle at whole-agent idle instead of the first turn/end, honoring the one-shot idle-to-idle contract. - flush JSDoc names the real callers (checkpoint policy, goal-session, teardown, self-flushing consumers); apiproxy zh README loses its stale duplicate history section; ACP note/README record the delivered error rejection and turnless-cancelled behaviors.
This commit is contained in:
@@ -234,6 +234,10 @@ export class ReactLoopAgent implements Agent {
|
||||
try {
|
||||
decision = await this.preStep('next-turn', { turn: phase.turn + 1, step: 1 })
|
||||
if (decision.kind === 'reject') return false
|
||||
// An empty admitted batch (claimed input removed before the wake, or no
|
||||
// runtime-context change) parks the driver instead of opening a turn and
|
||||
// spending a model call on nothing.
|
||||
if (decision.messages.length === 0) return false
|
||||
signal.throwIfAborted()
|
||||
} catch (error: unknown) {
|
||||
if (signal.aborted) throw error
|
||||
@@ -256,7 +260,12 @@ export class ReactLoopAgent implements Agent {
|
||||
for (const message of decision.messages) {
|
||||
this.session.append('user/message', message, { surfaceOp: 'append' })
|
||||
}
|
||||
turnEnds = await this.step(decision.assembly)
|
||||
// max-tokens is sticky: once any step hits the ceiling, later steps
|
||||
// that complete normally must not downgrade the turn outcome.
|
||||
const stepEnd = await this.step(decision.assembly)
|
||||
// max-tokens stays sticky: a later completed step must not
|
||||
// downgrade the turn outcome.
|
||||
if (turnEnds === null || turnEnds.kind !== 'max-tokens') turnEnds = stepEnd
|
||||
} finally {
|
||||
this.session.append('step/end', { turn, step })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user