blob: 4f17c75b01cd72410335e3b34f4a91504de05e08 [file]
/**
* Session-scoped Goal continuation coordinator.
*
* Agent turn outcomes are settled in FIFO order per session. A continuation is
* retained as an intent until the host atomically admits it; waiting verdicts use
* an in-memory backoff instead of immediately spending another turn.
*/
import { evaluateGoal, type GoalEvaluation, type GoalEvaluatorDeps } from './goal-evaluator.js';
import {
goalCheckpoint,
type GoalControlLease,
type GoalCheckpoint,
type GoalManager,
type GoalState,
} from './goal-state.js';
import {
GoalSessionCloseFence,
type GoalSessionCloseKind,
type GoalSessionCloseOperation,
} from './goal-session-close-fence.js';
import { GoalTaskGatePolicy, type GoalTaskGateDeps } from './goal-task-gate-policy.js';
import type { GoalTurnOutcome } from './goal-turn-lifecycle.js';
export type { GoalTurnOutcome } from './goal-turn-lifecycle.js';
export type { GoalSessionCloseOperation } from './goal-session-close-fence.js';
export type {
GoalTaskGateDecision,
GoalTaskGateDeps,
GoalTaskGateTrace,
} from './goal-task-gate-policy.js';
export type GoalTurnAdmission =
| {
kind: 'prepared';
turnId: string;
/** Start the synchronously reserved turn after coordinator ownership is registered. */
start: () => Promise<GoalTurnOutcome>;
}
| { kind: 'busy'; whenIdle: Promise<void> }
| { kind: 'unavailable'; reason: string };
/** Exactly-once settlement capability captured when an external Agent turn starts. */
export type GoalExternalTurnSettler = (outcome: GoalTurnOutcome) => Promise<void>;
export interface GoalContinuationScheduler {
setTimeout: (callback: () => void, delayMs: number) => unknown;
clearTimeout: (handle: unknown) => void;
}
export interface GoalContinuationDeps {
goalManager: GoalManager;
evaluator: GoalEvaluatorDeps;
/** Summarized recent conversation (last ~5 messages) for the evaluator. */
getRecentContext: (sessionId: string) => Promise<string>;
/** Current cumulative token count for the session (for budget tracking). */
getTokenCount?: (sessionId: string) => number;
/** Synchronously prepare, defer, or reject a Goal-owned turn. */
admitTurn: (sessionId: string, text: string) => GoalTurnAdmission;
taskGate?: GoalTaskGateDeps;
scheduler?: GoalContinuationScheduler;
}
export const GOAL_WAIT_BACKOFF_BASE_MS = 5_000;
export const GOAL_WAIT_BACKOFF_MAX_MS = 5 * 60_000;
const CONTINUATION_PREAMBLE =
'[Goal continuation] The goal is not yet met. Keep working toward it. ' +
'Do not redefine success around a smaller task; match your verification to the full requirement.';
interface QueuedTurn {
turnId: string;
outcome: GoalTurnOutcome;
controlLease: GoalControlLease;
checkpoint?: GoalCheckpoint;
resolve: () => void;
}
interface ContinuationIntent {
checkpoint: GoalCheckpoint;
controlLease: GoalControlLease;
triggeringTurnId: string;
evaluation: GoalEvaluation;
}
interface WaitingTimer {
handle: unknown;
}
interface TurnRegistration {
readonly turnId: string;
readonly lane: SessionLane;
observedControlLease?: GoalControlLease;
checkpoint?: GoalCheckpoint;
controlLease?: GoalControlLease;
}
interface SessionLane {
sessionId: string;
queue: QueuedTurn[];
turns: Map<string, TurnRegistration>;
processing?: QueuedTurn;
draining: boolean;
intent?: ContinuationIntent;
busyWake?: Promise<void>;
waitingTimer?: WaitingTimer;
consecutiveWaits: number;
}
export type GoalExternalTurnStart =
| { kind: 'registered'; settle: GoalExternalTurnSettler }
| { kind: 'unavailable'; reason: string };
export class GoalContinuationCoordinator {
private readonly lanes = new Map<string, SessionLane>();
private readonly sessionCloseFence: GoalSessionCloseFence;
private readonly taskGatePolicy: GoalTaskGatePolicy;
private readonly scheduler: GoalContinuationScheduler;
private disposed = false;
constructor(private readonly deps: GoalContinuationDeps) {
this.scheduler = deps.scheduler ?? defaultScheduler;
this.taskGatePolicy = new GoalTaskGatePolicy(deps.taskGate);
this.sessionCloseFence = new GoalSessionCloseFence({
onReopenedAfterRollback: (sessionId, kind) => {
const goal = this.deps.goalManager.get(sessionId);
if (!goal || (goal.status !== 'active' && goal.status !== 'waiting')) return;
const operation = kind === 'archive' ? 'archive' : 'removal';
this.deps.goalManager.pause(sessionId, {
checkpoint: goalCheckpoint(goal),
reason: `Goal continuation paused because session ${operation} did not complete.`,
});
},
});
}
/**
* Register an external Agent turn before its iterator starts. The returned
* closure is the only legal settlement path, so terminal handling never has
* to rediscover Goal ownership from mutable current state.
*/
beginExternalTurn(sessionId: string, turnId: string): GoalExternalTurnStart {
if (this.disposed) {
return { kind: 'unavailable', reason: 'Goal continuation is disposed.' };
}
if (this.sessionCloseFence.isClosed(sessionId)) {
return { kind: 'unavailable', reason: 'Goal continuation session is closed.' };
}
const lane = this.laneFor(sessionId);
if (lane.turns.has(turnId)) {
return { kind: 'unavailable', reason: `Goal turn ${turnId} is already registered.` };
}
const goal = this.deps.goalManager.get(sessionId);
const observedControlLease = this.deps.goalManager.getControlLease(sessionId);
const controlLease =
goal?.status === 'active' || goal?.status === 'waiting' ? observedControlLease : undefined;
const registration: TurnRegistration = {
turnId,
lane,
...(observedControlLease ? { observedControlLease } : {}),
...(controlLease ? { controlLease } : {}),
};
lane.turns.set(turnId, registration);
return {
kind: 'registered',
settle: (outcome) => this.settleRegisteredTurn(registration, outcome),
};
}
/** Execute and bind one Goal activation only while this exact turn owns the right to do so. */
activateGoal(
sessionId: string,
turnId: string,
activate: () => GoalState,
): GoalState | undefined {
const lane = this.lanes.get(sessionId);
const registration = lane?.turns.get(turnId);
if (
!lane ||
!this.isCurrent(lane) ||
!registration ||
registration.controlLease !== undefined ||
this.deps.goalManager.getControlLease(sessionId) !== registration.observedControlLease
) {
return undefined;
}
const goal = activate();
registration.controlLease = this.deps.goalManager.getControlLease(sessionId);
if (registration.checkpoint) registration.checkpoint = goalCheckpoint(goal);
return goal;
}
/** Mutate the exact Goal generation observed by this turn. */
mutateGoal(sessionId: string, turnId: string, mutate: () => GoalState): GoalState | undefined {
const lane = this.lanes.get(sessionId);
const registration = lane?.turns.get(turnId);
const controlLease = registration?.controlLease ?? registration?.observedControlLease;
if (
!lane ||
!this.isCurrent(lane) ||
!registration ||
!controlLease ||
!this.deps.goalManager.matchesControlLease(sessionId, controlLease)
) {
return undefined;
}
const goal = mutate();
this.discardIntentOwnedBy(lane, controlLease);
registration.observedControlLease = this.deps.goalManager.getControlLease(sessionId);
registration.controlLease = undefined;
return goal;
}
private settleRegisteredTurn(
registration: TurnRegistration,
outcome: GoalTurnOutcome,
): Promise<void> {
const { lane, turnId } = registration;
const sessionId = lane.sessionId;
if (!this.isCurrent(lane) || lane.turns.get(turnId) !== registration) {
this.consumeTurnRegistration(registration);
return Promise.resolve();
}
const controlLease = registration.controlLease;
const goal = this.deps.goalManager.get(sessionId);
if (
!controlLease ||
!this.deps.goalManager.matchesControlLease(sessionId, controlLease) ||
!goal ||
(goal.status !== 'active' && goal.status !== 'waiting')
) {
this.discardIntentOwnedBy(lane, controlLease);
this.consumeTurnRegistration(registration);
return Promise.resolve();
}
if (
registration.checkpoint &&
!this.deps.goalManager.matchesActive(sessionId, registration.checkpoint)
) {
this.consumeTurnRegistration(registration);
return Promise.resolve();
}
return this.enqueueTurn(outcome, controlLease, registration);
}
/** Revoke the current lane while allowing later turns in the same session. */
invalidateSession(sessionId: string): void {
const lane = this.lanes.get(sessionId);
if (!lane) return;
this.invalidateLane(lane);
this.lanes.delete(sessionId);
}
/**
* Fence admission synchronously before an archive/removal crosses an async
* boundary. Each operation owns an independent holder, so one rollback can
* never reopen a session still closed by another operation.
*/
beginSessionClose(sessionId: string, kind: GoalSessionCloseKind): GoalSessionCloseOperation {
const operation = this.sessionCloseFence.begin(sessionId, kind);
this.invalidateSession(sessionId);
return operation;
}
/** Clear only a committed archive fence; removal and pending holders remain. */
unarchiveSession(sessionId: string): void {
this.sessionCloseFence.unarchive(sessionId);
}
dispose(): void {
if (this.disposed) return;
this.disposed = true;
for (const lane of this.lanes.values()) this.invalidateLane(lane);
this.lanes.clear();
this.sessionCloseFence.dispose();
}
private enqueueTurn(
outcome: GoalTurnOutcome,
controlLease: GoalControlLease,
registration: TurnRegistration,
): Promise<void> {
const lane = registration.lane;
// New evidence always outranks an older, not-yet-admitted continuation.
lane.intent = undefined;
this.clearWaitingTimer(lane);
return new Promise<void>((resolve) => {
lane.queue.push({
turnId: registration.turnId,
outcome,
controlLease,
...(registration.checkpoint ? { checkpoint: registration.checkpoint } : {}),
resolve,
});
this.consumeTurnRegistration(registration);
this.scheduleDrain(lane);
});
}
private laneFor(sessionId: string): SessionLane {
const existing = this.lanes.get(sessionId);
if (existing) return existing;
const lane: SessionLane = {
sessionId,
queue: [],
turns: new Map(),
draining: false,
consecutiveWaits: 0,
};
this.lanes.set(sessionId, lane);
return lane;
}
private isCurrent(lane: SessionLane): boolean {
return !this.disposed && this.lanes.get(lane.sessionId) === lane;
}
private consumeTurnRegistration(registration: TurnRegistration): void {
const lane = registration.lane;
const turnId = registration.turnId;
if (lane.turns.get(turnId) === registration) {
lane.turns.delete(turnId);
}
this.dropIdleLane(lane);
}
private dropIdleLane(lane: SessionLane): void {
if (
!this.isCurrent(lane) ||
lane.turns.size > 0 ||
lane.queue.length > 0 ||
lane.draining ||
lane.intent ||
lane.busyWake ||
lane.waitingTimer
) {
return;
}
this.lanes.delete(lane.sessionId);
}
private discardIntentOwnedBy(
lane: SessionLane,
controlLease: GoalControlLease | undefined,
): void {
if (!controlLease || lane.intent?.controlLease !== controlLease) return;
lane.intent = undefined;
lane.busyWake = undefined;
this.clearWaitingTimer(lane);
this.resetWaitingBackoff(lane);
}
private scheduleDrain(lane: SessionLane): void {
if (!this.isCurrent(lane) || lane.draining) return;
void this.drainLane(lane).catch((error) => {
if (!this.isCurrent(lane)) return;
this.pauseCurrentGoal(lane, `Goal continuation coordinator failed: ${errorMessage(error)}`);
});
}
private async drainLane(lane: SessionLane): Promise<void> {
if (!this.isCurrent(lane) || lane.draining) return;
lane.draining = true;
try {
while (this.isCurrent(lane) && lane.queue.length > 0) {
const item = lane.queue.shift();
if (!item) break;
lane.processing = item;
try {
await this.processQueuedTurn(lane, item);
} catch (error) {
this.pauseCurrentGoal(
lane,
`Goal continuation coordinator failed: ${errorMessage(error)}`,
);
} finally {
if (lane.processing === item) lane.processing = undefined;
}
item.resolve();
}
if (!this.isCurrent(lane) || lane.queue.length > 0) return;
const goal = this.deps.goalManager.get(lane.sessionId);
if (goal?.status === 'waiting' && lane.intent) {
this.scheduleWaitingRetry(lane, goal);
} else if (goal?.status === 'active' && lane.intent && !lane.busyWake) {
await this.tryAdmitIntent(lane, lane.intent);
}
} finally {
lane.draining = false;
if (!this.isCurrent(lane)) return;
const goal = this.deps.goalManager.get(lane.sessionId);
if (
lane.queue.length > 0 ||
(lane.intent && goal?.status === 'active' && !lane.busyWake && !lane.waitingTimer)
) {
this.scheduleDrain(lane);
} else {
this.dropIdleLane(lane);
}
}
}
private async processQueuedTurn(lane: SessionLane, item: QueuedTurn): Promise<void> {
if (!this.deps.goalManager.matchesControlLease(lane.sessionId, item.controlLease)) {
return;
}
const goal = this.deps.goalManager.get(lane.sessionId)!;
if (item.checkpoint && !this.deps.goalManager.matchesActive(lane.sessionId, item.checkpoint)) {
return;
}
if (item.outcome.kind !== 'completed') {
if (goal.status !== 'active' && goal.status !== 'waiting') return;
this.pauseAtCheckpoint(
lane,
item.checkpoint ?? goalCheckpoint(goal),
turnFailureReason(item.outcome),
);
return;
}
await this.processCompletion(lane, item.controlLease, goal, item.turnId);
}
private async processCompletion(
lane: SessionLane,
controlLease: GoalControlLease,
initialGoal: GoalState,
turnId: string,
): Promise<void> {
let goal = initialGoal;
if (goal.status === 'waiting') {
const woken = this.deps.goalManager.wakeWaiting(lane.sessionId, goalCheckpoint(goal));
if (!woken) return;
goal = woken;
}
if (goal.status !== 'active') return;
const checkpoint = goalCheckpoint(goal);
let context: string;
try {
context = await this.deps.getRecentContext(lane.sessionId);
} catch (error) {
this.pauseAtCheckpoint(
lane,
checkpoint,
`Unable to read Goal evaluation context: ${errorMessage(error)}`,
);
return;
}
if (!this.isCurrent(lane) || !this.deps.goalManager.matchesActive(lane.sessionId, checkpoint)) {
return;
}
const evaluation = await evaluateGoal(
this.deps.evaluator,
goal.condition,
context,
lane.sessionId,
);
if (!this.isCurrent(lane) || !this.deps.goalManager.matchesActive(lane.sessionId, checkpoint)) {
return;
}
const tokensNow = this.deps.getTokenCount?.(lane.sessionId);
const settlementBase = {
checkpoint,
reason: evaluation.reason,
};
let settled: GoalState | undefined;
if (evaluation.met || evaluation.impossible) {
settled = this.deps.goalManager.settleTurn(lane.sessionId, {
...settlementBase,
verdict: evaluation.met ? 'achieved' : 'impossible',
});
} else if (evaluation.waiting) {
settled = this.deps.goalManager.settleTurn(lane.sessionId, {
...settlementBase,
verdict: 'continue',
waiting: true,
...(tokensNow !== undefined ? { tokensNow } : {}),
});
} else {
settled = this.deps.goalManager.settleTurn(lane.sessionId, {
...settlementBase,
verdict: 'continue',
...(!evaluation.evaluatorFailed ? { madeProgress: evaluation.progress } : {}),
...(tokensNow !== undefined ? { tokensNow } : {}),
});
}
if (!settled) return;
if (settled.status === 'achieved' || settled.status === 'impossible') {
lane.intent = undefined;
this.resetWaitingBackoff(lane);
this.taskGatePolicy.record({
sessionId: lane.sessionId,
turnId,
goalId: goal.id,
decision: 'evaluator_terminal',
taskKeys: [],
});
return;
}
if (settled.status !== 'active' && settled.status !== 'waiting') {
lane.intent = undefined;
this.resetWaitingBackoff(lane);
const taskKeys = await this.taskGatePolicy.listActionable(lane.sessionId);
this.taskGatePolicy.record({
sessionId: lane.sessionId,
turnId,
goalId: goal.id,
decision: 'goal_stopped',
taskKeys,
});
return;
}
lane.intent = {
checkpoint: goalCheckpoint(settled),
controlLease,
triggeringTurnId: turnId,
evaluation,
};
if (settled.status === 'waiting') {
lane.consecutiveWaits++;
return;
}
this.resetWaitingBackoff(lane);
}
private async tryAdmitIntent(lane: SessionLane, intent: ContinuationIntent): Promise<void> {
if (!this.isCurrent(lane) || lane.queue.length > 0 || lane.intent !== intent) {
return;
}
if (!this.ownedGoal(lane, intent)) {
lane.intent = undefined;
return;
}
const taskPlan = await this.taskGatePolicy.planAdmission(
lane.sessionId,
intent.checkpoint.goalId,
);
if (!this.isCurrent(lane) || lane.queue.length > 0 || lane.intent !== intent) {
return;
}
const goal = this.ownedGoal(lane, intent);
if (!goal) {
lane.intent = undefined;
return;
}
const prompt = buildContinuationPrompt(goal, intent.evaluation, taskPlan.reminder);
const admission = this.deps.admitTurn(lane.sessionId, prompt);
if (admission.kind === 'busy') {
this.watchBusyLane(lane, admission.whenIdle);
return;
}
if (admission.kind === 'unavailable') {
this.pauseAtCheckpoint(lane, intent.checkpoint, admission.reason);
return;
}
const registration: TurnRegistration = {
turnId: admission.turnId,
lane,
observedControlLease: intent.controlLease,
checkpoint: intent.checkpoint,
controlLease: intent.controlLease,
};
lane.turns.set(admission.turnId, registration);
let completion: Promise<GoalTurnOutcome>;
try {
completion = admission.start();
} catch (error) {
void this.settleRegisteredTurn(registration, {
kind: 'errored',
turnId: admission.turnId,
reason: errorMessage(error),
});
return;
}
void completion.then(
(outcome) => {
void this.settleRegisteredTurn(registration, outcome);
},
(error) => {
void this.settleRegisteredTurn(registration, {
kind: 'errored',
turnId: admission.turnId,
reason: errorMessage(error),
});
},
);
lane.intent = undefined;
this.taskGatePolicy.markStarted(intent.checkpoint.goalId, taskPlan);
this.taskGatePolicy.record({
sessionId: lane.sessionId,
turnId: intent.triggeringTurnId,
goalId: intent.checkpoint.goalId,
decision: taskPlan.decision,
taskKeys: taskPlan.taskKeys,
});
}
private watchBusyLane(lane: SessionLane, whenIdle: Promise<void>): void {
if (!this.isCurrent(lane) || lane.busyWake === whenIdle) return;
lane.busyWake = whenIdle;
const wake = () => {
if (!this.isCurrent(lane) || lane.busyWake !== whenIdle) return;
lane.busyWake = undefined;
this.scheduleDrain(lane);
};
void whenIdle.then(wake);
}
private ownedGoal(lane: SessionLane, intent: ContinuationIntent): GoalState | undefined {
if (
!this.deps.goalManager.matchesControlLease(lane.sessionId, intent.controlLease) ||
!this.deps.goalManager.matchesActive(lane.sessionId, intent.checkpoint)
) {
return undefined;
}
return this.deps.goalManager.get(lane.sessionId);
}
private scheduleWaitingRetry(lane: SessionLane, goal: GoalState): void {
if (!this.isCurrent(lane) || lane.waitingTimer || !lane.intent) return;
if (!this.deps.goalManager.matches(lane.sessionId, lane.intent.checkpoint)) {
lane.intent = undefined;
this.resetWaitingBackoff(lane);
return;
}
const checkpoint = goalCheckpoint(goal);
const delayMs = waitBackoffMs(lane.consecutiveWaits);
const handle = this.scheduler.setTimeout(() => {
lane.waitingTimer = undefined;
if (!this.isCurrent(lane)) return;
if (lane.queue.length > 0) {
this.scheduleDrain(lane);
return;
}
const woken = this.deps.goalManager.wakeWaiting(lane.sessionId, checkpoint);
if (!woken || !lane.intent) return;
lane.intent = { ...lane.intent, checkpoint: goalCheckpoint(woken) };
this.scheduleDrain(lane);
}, delayMs);
lane.waitingTimer = { handle };
}
private pauseCurrentGoal(lane: SessionLane, reason: string): void {
const current = this.deps.goalManager.get(lane.sessionId);
if (!current || (current.status !== 'active' && current.status !== 'waiting')) {
return;
}
this.pauseAtCheckpoint(lane, goalCheckpoint(current), reason);
}
private pauseAtCheckpoint(lane: SessionLane, checkpoint: GoalCheckpoint, reason: string): void {
const paused = this.deps.goalManager.pause(lane.sessionId, { checkpoint, reason });
if (!paused) return;
lane.intent = undefined;
lane.busyWake = undefined;
this.clearWaitingTimer(lane);
this.resetWaitingBackoff(lane);
}
private clearWaitingTimer(lane: SessionLane): void {
const timer = lane.waitingTimer;
if (!timer) return;
lane.waitingTimer = undefined;
this.scheduler.clearTimeout(timer.handle);
}
private resetWaitingBackoff(lane: SessionLane): void {
lane.consecutiveWaits = 0;
}
private invalidateLane(lane: SessionLane): void {
lane.intent = undefined;
lane.busyWake = undefined;
this.clearWaitingTimer(lane);
for (const registration of [...lane.turns.values()]) {
this.consumeTurnRegistration(registration);
}
lane.processing?.resolve();
for (const item of lane.queue.splice(0)) {
item.resolve();
}
}
}
function buildContinuationPrompt(
goal: GoalState,
evaluation: GoalEvaluation,
taskReminder: string | undefined,
): string {
const noProgress =
goal.consecutiveNoProgress > 0
? `, ${goal.consecutiveNoProgress}/${goal.blockCap} no-progress`
: '';
return (
`${CONTINUATION_PREAMBLE}${taskReminder ? `\n\n${taskReminder}` : ''}` +
`\n\nEvaluation: ${evaluation.reason}${evaluation.waiting ? ' (scheduled external-event re-check)' : ''}\n` +
`Goal: "${goal.condition}" (turn ${goal.iterations}/${goal.maxIterations}${noProgress})`
);
}
function waitBackoffMs(consecutiveWaits: number): number {
return Math.min(
GOAL_WAIT_BACKOFF_MAX_MS,
GOAL_WAIT_BACKOFF_BASE_MS * 2 ** Math.max(0, consecutiveWaits - 1),
);
}
function errorMessage(error: unknown): string {
return error instanceof Error ? error.message : String(error);
}
function turnFailureReason(outcome: Exclude<GoalTurnOutcome, { kind: 'completed' }>): string {
if (outcome.kind === 'aborted') return 'Goal-associated turn was aborted.';
if (outcome.kind === 'suspended') return `Goal-associated turn suspended: ${outcome.reason}`;
return `Goal-associated turn failed: ${outcome.reason}`;
}
const defaultScheduler: GoalContinuationScheduler = {
setTimeout(callback, delayMs) {
const handle = setTimeout(callback, delayMs);
if (typeof handle === 'object' && handle !== null && 'unref' in handle) {
(handle as { unref?: () => void }).unref?.();
}
return handle;
},
clearTimeout(handle) {
clearTimeout(handle as ReturnType<typeof setTimeout>);
},
};