// Pure settle decision for the goal loop — extracted so the abort/error/cap // paths (AC4, AC5) are hermetically testable without a live agent run. import { checkLimitReached, type GoalState } from "./state.ts"; export type SettleDecision = { action: "inject" } | { action: "wait" } | { action: "pause"; reason: string }; /** An untimed wait never spends model tokens on automatic reconciliation. */ export function hasUnresolvedWait(state: GoalState): boolean { return !!state.activeWait && (!state.waitTimeoutSeconds || !state.activeWait.wakeSent); } /** * Decide what the loop does when the agent settles while a goal is active. * stopReason comes from the last assistant message ("stop", "toolUse", * "aborted", "error", ...). */ export function decideSettle(state: GoalState, stopReason: string | undefined): SettleDecision { if (stopReason === "aborted") { return { action: "pause", reason: "paused: run aborted (Esc)" }; } if (stopReason === "error") { return { action: "pause", reason: "paused: run error" }; } if (hasUnresolvedWait(state)) { return { action: "wait" }; } if (checkLimitReached(state)) { return { action: "pause", reason: `paused: cap of ${state.maxChecks} checks reached without a report`, }; } return { action: "inject" }; }