/** * [WHO]: GoalController class - per-thread runtime; serializes goal mutations via mutex; tracks per-turn accounting (on_turn_end); dispatches pull-model continuations at the agent idle point (maybe_dispatch_continuation) and budget-limit steering * [FROM]: Depends on ./goal-store, ./goal-types, ./goal-prompts, ./goal-format, core/extensions-host/types (ExtensionAPI) * [TO]: Consumed by ./index (lifecycle hooks) and ./goal-tools / ./goal-command (mutations) * [HERE]: extensions/builtin/goal/goal-controller.ts - thin per-thread state owner; pure logic, no I/O beyond the store */ import type { ExtensionAPI } from "../../../core/extensions-host/types.js"; import { type GoalControllerState, type GoalRunKind, type GoalTurnAccounting, type ThreadGoal, type ThreadGoalStatus, type UpdateGoalArgs } from "./goal-types.js"; import { GoalStore } from "./goal-store.js"; interface GoalDispatchOutcome { dispatched: boolean; reason: "no_active_goal" | "not_active_status" | "plan_mode" | "already_dispatched" | "no_pending_messages" | "pending_messages" | "continuation_limit_reached" | "total_continuation_limit_reached" | "completed"; goal?: ThreadGoal; consecutiveContinuations?: number; } /** Outcome of per-turn accounting at turn_end. Dispatch decisions live in * maybe_dispatch_continuation (called when the agent run actually ends). */ interface GoalTurnEndOutcome { reason: "no_active_goal" | "not_active_status" | "active"; goal?: ThreadGoal; } export declare class GoalController { private readonly api; private readonly threadId; private readonly store; private readonly state; private mutex; /** Monotonic counter of all continuation dispatches for the current goal. * Never resets on user turns — only resets when the goal itself changes. */ private totalContinuationTurns; /** Set when the goal transitions to a terminal status during the current turn. * Prevents on_turn_end from dispatching a continuation for a just-completed goal. */ private goalJustTransitionedToTerminal; /** stopReason of the most recent agent run (from agent_result). * Consulted at agent_end before dispatching a continuation. */ private lastRunStopReason; constructor(api: ExtensionAPI, threadId: string); get currentThreadId(): string; get goalStore(): GoalStore; get currentState(): GoalControllerState; /** Serialize every mutation through a single in-process mutex. */ private withLock; /** Public API: read current persisted goal. */ get_goal(): Promise; /** * Public API: set / replace / update the goal objective. * Caller is responsible for showing the ConfirmIfExists dialog and re-dispatching * with mode=ReplaceExisting if the user confirms. */ set_objective(objective: string, mode: "ConfirmIfExists" | "ReplaceExisting" | "UpdateExisting", options?: { status?: ThreadGoalStatus; tokenBudget?: number | null; }): Promise<{ kind: "ok" | "confirm_required" | "blocked_existing"; goal: ThreadGoal | null; replaced: boolean; }>; /** Public API: clear the goal entirely. */ clear(): Promise; /** Public API: pause / resume. */ set_status(status: ThreadGoalStatus): Promise; /** Public API: insert a new goal only if the existing one is complete. */ insert_goal(objective: string, tokenBudget: number | null): Promise; /** Public API: tool-driven UpdateGoal. Only complete/blocked transitions. */ apply_update_goal(args: UpdateGoalArgs): Promise; /** Hook: turn_start. Marks the current turn as goal-active if goal is active. * Uses pendingContinuationDispatch to distinguish user-initiated turns from * continuation-triggered turns. User turns reset the consecutive continuation * counter; continuation turns increment it. */ on_turn_start(turnId: string, runKind: GoalRunKind, totalTokensAtStart: number): void; /** Hook: token usage updates from the agent loop (called on every message_end). */ on_token_usage(totalTokens: number): { crossed: boolean; goal?: ThreadGoal; }; /** Hook: a tool finished. Accrue usage from internal turn state. * Skips the UpdateGoal tool itself (it should not be counted toward goal progress). */ on_tool_finish(toolName: string): { crossed: boolean; goal?: ThreadGoal; }; /** * Hook: turn_end. Final accounting and active-turn cleanup only. * * agent-core fires turn_end per assistant cycle — a single prompt loop can * end many turns before the agent is actually idle. Continuation dispatch * therefore lives in maybe_dispatch_continuation(), called at agent_end * (the true idle point, mirroring Codex's continue_if_idle()). */ on_turn_end(): Promise; /** * Pull-model continuation: called when the agent run truly ends (agent_end, * after retry/compaction settle and the agent is idle). Reads goal state and * decides whether to start a new continuation turn — the equivalent of * Codex's continue_if_idle(): non-Active state means no new turn, cleanly. * * `idleContinuationDispatched` guards double-dispatch within one idle window; * it resets at the next on_turn_start. */ maybe_dispatch_continuation(options: { hasPendingMessages: boolean; }): GoalDispatchOutcome; /** Hook: agent_result. Records the run's stopReason for the agent_end decision. */ note_run_stop_reason(stopReason: string | undefined): void; /** stopReason recorded from the most recent agent_result, if any. */ get runStopReason(): string | null; /** Hook: turn aborted (different from error). Final accounting. */ on_turn_abort(): Promise; /** Hook: usage limit hit. Mark current active goal usage_limited. */ on_usage_limit(): ThreadGoal | null; /** Hook: turn error. Mark current active goal blocked. */ on_turn_error(): ThreadGoal | null; /** * Read consecutive-blocked counter; called by tools / commands that need to * decide whether to escalate a single-turn block into a stored "blocked" state. */ record_blocked_signal(): { escalated: boolean; consecutiveBlocked: number; }; /** Reset consecutive-blocked counter (called from successful turn_end). */ reset_blocked_signal(): void; /** Inject budget-limit steering into the active prompt if not already surfaced. */ maybe_build_budget_limit_steering(): string | null; /** Build and inject objective_updated steering after a /goal edit. * Sends the prompt as a follow-up user message so the LLM re-derives * requirements against the updated objective. */ inject_objective_updated_steering(): boolean; /** Kick off agent work immediately after /goal set. * Sends a continuation prompt so the agent starts pursuing the goal * without waiting for the user to send a follow-up message. */ kickOffContinuation(): boolean; /** Build objective_updated prompt string (for external injection or display). */ build_objective_updated_steering(): string | null; private clearActiveTurn; /** Send a persistent goal feedback message to the TUI chat stream. */ sendGoalFeedback(content: string, details?: Record): void; /** Used by session_start to reset the idempotency flag. */ resetIdleContinuationFlag(): void; /** Snapshot the bookkeeping turn for testing / display. */ currentTurnSnapshot(): GoalTurnAccounting | null; } export {};