refactor(agent): run maintenance between turns

This commit is contained in:
_Kerman
2026-08-03 10:53:08 +08:00
parent 0efbb7d87a
commit b4258a2c4a
6 changed files with 145 additions and 60 deletions

View File

@@ -35,6 +35,12 @@ import { executeToolCalls } from './tool-calls.ts'
type Phase =
| { kind: 'idle'; lastTurn: number }
| {
kind: 'maintenance'
abort: AbortController
lastTurn: number
wakeRequested: boolean
}
| { kind: 'collecting'; abort: AbortController; lastTurn: number }
| { kind: 'running'; abort: AbortController; turn: number; step: number }
@@ -57,7 +63,7 @@ function requestProposal(header: EpochHeader): LlmCallConfig {
export class ReactLoopAgent implements Agent {
readonly inbox: Inbox
private phase: Phase
private driverDone: Promise<void> = Promise.resolve()
private activityDone: Promise<void> = Promise.resolve()
/** The agent-scoped registration boundary; the lifecycle owner unwinds it after the driver exits. */
readonly scope: Scope
@@ -85,7 +91,7 @@ export class ReactLoopAgent implements Agent {
}
get status(): AgentStatus {
return this.phase.kind === 'idle' ? 'idle' : 'running'
return this.phase.kind === 'idle' || this.phase.kind === 'maintenance' ? 'idle' : 'running'
}
/** Commit a phase and publish its externally visible status transition. */
@@ -99,7 +105,7 @@ export class ReactLoopAgent implements Agent {
}
send(message: UserMessage, target: InboxTarget, wakeup: boolean): void {
// Waking input cannot join an aborted pre-step or turn, so it starts the next turn.
// Waking input cannot join an aborted activity, so it starts the next turn.
const wakingAfterAbort = wakeup && this.phase.kind !== 'idle' && this.phase.abort.signal.aborted
const resolvedTarget = wakingAfterAbort ? 'next-turn' : target
this.inbox.splice(resolvedTarget, Infinity, 0, [message])
@@ -119,15 +125,44 @@ export class ReactLoopAgent implements Agent {
}
cancel(cause: AgentCancelCause, options: CancelOptions = {}): void {
if (!options.keepInbox) this.inbox.clear()
if (!options.keepInbox) {
this.inbox.clear()
if (this.phase.kind === 'maintenance') this.phase.wakeRequested = false
}
if (this.phase.kind !== 'idle') this.phase.abort.abort(cause)
}
/** Reserve a driver before deferring idle pre-step processing. */
runMaintenance<T>(task: (signal: AbortSignal) => Promise<T>): Promise<T> {
if (this.phase.kind !== 'idle') throw new Error(`agent "${this.id}" already has active work`)
const done = Promise.withResolvers<void>()
const maintenance: Phase = {
kind: 'maintenance',
abort: new AbortController(),
lastTurn: this.phase.lastTurn,
wakeRequested: false,
}
this.setPhase(maintenance)
this.activityDone = done.promise
return (async () => {
try {
return await task(maintenance.abort.signal)
} finally {
this.setPhase({ kind: 'idle', lastTurn: maintenance.lastTurn })
if (maintenance.wakeRequested) this.scheduleKick()
done.resolve()
}
})()
}
/** Schedule one driver, or remember its wake behind maintenance. */
private scheduleKick(): void {
if (this.phase.kind === 'maintenance') {
if (!this.phase.abort.signal.aborted) this.phase.wakeRequested = true
return
}
if (this.phase.kind !== 'idle') return
const driver = Promise.withResolvers<void>()
this.driverDone = driver.promise
this.activityDone = driver.promise
this.setPhase({ kind: 'collecting', abort: new AbortController(), lastTurn: this.phase.lastTurn })
queueMicrotask(() => {
this.loopCtx.agents.withInitiator(this, () => this.kick()).then(driver.resolve, driver.reject)
@@ -135,10 +170,10 @@ export class ReactLoopAgent implements Agent {
}
async whenIdle(): Promise<void> {
let driver: Promise<void>
let activity: Promise<void>
do {
await (driver = this.driverDone)
} while (driver !== this.driverDone)
await (activity = this.activityDone)
} while (activity !== this.activityDone)
}
/** Report one failure at its live boundary, then preserve it for driver containment. */
@@ -184,7 +219,7 @@ export class ReactLoopAgent implements Agent {
/** Claimed input stays unowned until `turn/start` commits. */
private async turn(): Promise<boolean> {
if (this.phase.kind === 'idle') {
if (this.phase.kind === 'idle' || this.phase.kind === 'maintenance') {
this.throwError(new Error(`agent "${this.id}": turn without driver reservation`))
}
const abort = this.phase.kind === 'collecting' ? this.phase.abort : new AbortController()