import { randomUUID } from "node:crypto"; import type { GoalEvent, GoalHistoryEntry, GoalOrchestration, GoalOwner, GoalPhase, GoalStatus, } from "./types.ts"; import { GOAL_STATUSES } from "./types.ts"; const HISTORY_CAP = 64; const BLOCKED_THRESHOLD = 3; export type PauseReason = | "user" | "back_off" | "no_progress" | "infra" | "verification"; export interface CreateGoalOptions { objective: string; verifyMax?: number; skepticN?: number; strategistEvery?: number; tokenBudget?: number; stallThreshold?: number; planPath?: string; planBaselinePath?: string; /** Owner supplied by the extension; tracker never creates session identities. */ owner?: GoalOwner; } export interface VerifyResultInput { achieved: boolean; gaps?: string[]; fingerprint?: string | null; } function shortId(): string { return randomUUID().replace(/-/g, "").slice(0, 12); } function nowIso(): string { return new Date().toISOString(); } /** Unknown wire values restore as user_paused (never self-driving Active). */ export function parseStatus(s: unknown): GoalStatus { if (typeof s === "string" && (GOAL_STATUSES as readonly string[]).includes(s)) { return s as GoalStatus; } return "user_paused"; } function statusForPause(reason: PauseReason): GoalStatus { switch (reason) { case "user": return "user_paused"; case "back_off": return "back_off_paused"; case "no_progress": return "no_progress_paused"; case "infra": return "infra_paused"; case "verification": return "user_paused"; default: return "user_paused"; } } /** * Pure goal lifecycle FSM. No I/O. */ export class GoalTracker { private state: GoalOrchestration | null = null; private readonly defaultStallThreshold: number; constructor(opts?: { stallThreshold?: number }) { this.defaultStallThreshold = opts?.stallThreshold ?? 2; } hasGoal(): boolean { return this.state !== null; } snapshot(): GoalOrchestration | null { if (!this.state) return null; return structuredClone(this.state); } load(state: GoalOrchestration | null): void { if (state === null) { this.state = null; return; } const copy = structuredClone(state); copy.status = parseStatus(copy.status); if (!Array.isArray(copy.history)) copy.history = []; if (copy.history.length > HISTORY_CAP) { copy.history = copy.history.slice(-HISTORY_CAP); } this.state = copy; } isPaused(): boolean { if (!this.state) return false; const s = this.state.status; return ( s === "user_paused" || s === "back_off_paused" || s === "no_progress_paused" || s === "infra_paused" || s === "blocked" || s === "budget_limited" ); } createGoal(opts: CreateGoalOptions): GoalOrchestration { const objective = opts.objective.trim(); if (!objective) { throw new Error("objective must be a non-empty string"); } const goalId = shortId(); const verifierId = shortId(); const verifyMax = Math.max(1, Math.trunc(opts.verifyMax ?? 10)); const skepticN = Math.min(5, Math.max(1, Math.trunc(opts.skepticN ?? 3))); const strategistEvery = Math.max( 1, Math.trunc(opts.strategistEvery ?? Math.max(1, Math.floor(verifyMax / 2))), ); const stallThreshold = Math.max( 1, Math.trunc(opts.stallThreshold ?? this.defaultStallThreshold), ); const planPath = opts.planPath ?? `.pi/goal/${goalId}/plan.md`; const planBaselinePath = opts.planBaselinePath ?? `.pi/goal/${goalId}/plan.baseline.md`; const state: GoalOrchestration = { goalId, objective, status: "active", phase: "executing", createdAt: nowIso(), elapsedMs: 0, history: [], planPath, planBaselinePath, verifierId, verifyAttempts: 0, verifyMax, skepticN, lastGaps: [], lastGapFingerprint: null, stallCount: 0, consecutiveNotAchieved: 0, strategistEvery, subgoals: [], blockedAttempts: 0, stallThreshold, owner: opts.owner ? structuredClone(opts.owner) : undefined, }; if (opts.tokenBudget !== undefined) { state.tokenBudget = opts.tokenBudget; } this.state = state; this.pushHistory("goal_created", objective); return this.requireState(); } startPlanning(): GoalOrchestration { const s = this.requireActiveish(); s.phase = "planning"; this.pushHistory("planning_started"); return this.requireState(); } completePlanning(): GoalOrchestration { const s = this.requireState(); s.phase = "executing"; if (s.status !== "complete" && s.status !== "blocked") { s.status = "active"; } this.pushHistory("planning_completed"); return this.requireState(); } failPlanning(reason: string): GoalOrchestration { const s = this.requireState(); s.status = "user_paused"; s.phase = "idle"; s.pauseMessage = reason; this.pushHistory("planning_failed", reason); return this.requireState(); } pause(reason: PauseReason, message?: string): GoalOrchestration { const s = this.requireState(); s.status = statusForPause(reason); s.pauseMessage = message ?? reason; this.pushHistory( reason === "no_progress" ? "stall_paused" : "goal_paused", message ?? reason, ); return this.requireState(); } resume(): GoalOrchestration { const s = this.requireState(); s.status = "active"; s.phase = "executing"; s.verifyAttempts = 0; s.stallCount = 0; s.consecutiveNotAchieved = 0; s.blockedAttempts = 0; s.pauseMessage = undefined; this.pushHistory("goal_resumed"); return this.requireState(); } complete(): GoalOrchestration { const s = this.requireState(); s.status = "complete"; s.phase = "idle"; this.pushHistory("goal_completed"); return this.requireState(); } clear(): null { if (this.state) { this.pushHistory("goal_cleared"); } this.state = null; return null; } beginVerify(): GoalOrchestration { const s = this.requireState(); if (s.verifyAttempts >= s.verifyMax) { s.status = "back_off_paused"; s.phase = "idle"; s.pauseMessage = `verify cap reached (${s.verifyMax})`; this.pushHistory("goal_paused", s.pauseMessage); return this.requireState(); } s.verifyAttempts += 1; s.phase = "verifying"; this.pushHistory("verify_started", undefined, s.verifyAttempts); return this.requireState(); } recordVerifyResult(input: VerifyResultInput): GoalOrchestration { const s = this.requireState(); const gaps = input.gaps ?? []; const fingerprint = input.fingerprint ?? null; if (input.achieved) { s.status = "complete"; s.phase = "idle"; s.lastGaps = []; s.consecutiveNotAchieved = 0; s.stallCount = 0; this.pushHistory("verify_achieved", undefined, s.verifyAttempts); this.pushHistory("goal_completed"); return this.requireState(); } s.lastGaps = [...gaps]; this.pushHistory("verify_not_achieved", gaps.join("; ") || undefined, s.verifyAttempts, gaps); // Consecutive identical fingerprints. Count appearances of the same fp; // pause when count >= stallThreshold (default 2 = same fingerprint twice). const stallThreshold = s.stallThreshold ?? this.defaultStallThreshold; if (fingerprint !== null) { if (fingerprint === s.lastGapFingerprint) { s.stallCount += 1; } else { s.lastGapFingerprint = fingerprint; s.stallCount = 1; } if (s.stallCount >= stallThreshold) { s.status = "no_progress_paused"; s.phase = "idle"; s.pauseMessage = "same gap fingerprint stalled"; this.pushHistory("stall_paused", s.pauseMessage, s.verifyAttempts, gaps); return this.requireState(); } } else { s.lastGapFingerprint = null; s.stallCount = 0; } s.consecutiveNotAchieved += 1; s.status = "active"; s.phase = "executing"; return this.requireState(); } shouldFireStrategist(): boolean { if (!this.state) return false; const every = this.state.strategistEvery; if (every <= 0) return false; return ( this.state.consecutiveNotAchieved > 0 && this.state.consecutiveNotAchieved % every === 0 ); } noteStrategist(recommendation: string): GoalOrchestration { const s = this.requireState(); s.lastStrategyRecommendation = recommendation; this.pushHistory("strategist_fired", recommendation); return this.requireState(); } /** * Append a progress note to history (`event: "progress"`, raw detail). */ noteProgress(message: string): GoalOrchestration { const s = this.requireState(); const text = message.trim(); if (!text) { throw new Error("progress message must be non-empty"); } this.pushHistory("progress", text); return this.requireState(); } /** * Record a blocked attempt. After 3 consecutive blocked attempts → blocked. */ requestBlocked(reason: string): GoalOrchestration { const s = this.requireState(); s.blockedAttempts = (s.blockedAttempts ?? 0) + 1; s.pauseMessage = reason; if ((s.blockedAttempts ?? 0) >= BLOCKED_THRESHOLD) { s.status = "blocked"; s.phase = "idle"; this.pushHistory("goal_paused", `blocked: ${reason}`); } else { this.pushHistory("unknown", `blocked_attempt ${s.blockedAttempts}: ${reason}`); } return this.requireState(); } private requireState(): GoalOrchestration { if (!this.state) { throw new Error("no active goal"); } return this.state; } private requireActiveish(): GoalOrchestration { return this.requireState(); } private pushHistory( event: GoalEvent, detail?: string, round?: number, unmet?: string[], ): void { if (!this.state) return; const entry: GoalHistoryEntry = { timestamp: nowIso(), event, }; if (detail !== undefined) entry.detail = detail; if (round !== undefined) entry.round = round; if (unmet !== undefined) entry.unmet = unmet; this.state.history.push(entry); if (this.state.history.length > HISTORY_CAP) { this.state.history = this.state.history.slice(-HISTORY_CAP); } } } export type { GoalPhase, GoalStatus, GoalOrchestration };