import type { AutopilotBlock } from "@rakazo/contracts"; import { AUTOPILOT_CHECKPOINT_LOG, AUTOPILOT_FAIL_LIMIT, AUTOPILOT_IDLE_LIMIT, } from "@rakazo/contracts"; /** What the autopilot remembers between runs. */ export interface AutopilotState { goal: string; focus: string | null; until: Date | null; lastCheckpoint: string | null; nextStep: string | null; stepCount: number; } /** * The prompt of each continuation run: the order in the user's words and where the work stands, * so a fresh run picks up without the whole history. */ export function autopilotContinuationPrompt(state: AutopilotState, now = new Date()): string { return [ "Autopilot: continue the user's order on your own, one sensible step at a time.", `Order: ${state.goal}`, state.focus ? `Focus: ${state.focus}` : null, state.until ? `Until: ${state.until.toISOString()} (now ${now.toISOString()})` : "Until: done", `Steps so far: ${state.stepCount}`, state.lastCheckpoint ? `Last checkpoint: ${state.lastCheckpoint}` : null, state.nextStep ? `Planned next: ${state.nextStep}` : null, "Do the next step, then call autopilot_checkpoint with what changed. When the order is done, call autopilot_finish with the result. Actions that need the user's approval are parked as suggestions; continue with other work.", ] .filter(Boolean) .join("\n"); } /** Whether the run that just ended moved the autopilot on, and if it should pause now. */ export function advanceAutopilotGuard( counts: { idleCount: number; failCount: number }, run: { checkpointed: boolean; failed: boolean }, ): { idleCount: number; failCount: number; pause: "idle" | "failing" | null } { const idleCount = run.checkpointed ? 0 : counts.idleCount + 1; const failCount = run.failed ? counts.failCount + 1 : 0; const pause = failCount >= AUTOPILOT_FAIL_LIMIT ? "failing" : idleCount >= AUTOPILOT_IDLE_LIMIT ? "idle" : null; return { idleCount, failCount, pause }; } /** The autopilot's time is up. */ export function autopilotExpired(until: Date | null, now = Date.now()): boolean { return Boolean(until && until.getTime() <= now); } /** How a later turn reads an autopilot line. */ export function autopilotHistoryText(block: AutopilotBlock): string { switch (block.event) { case "autopilot.started": { const parts = [block.goal]; if (block.until) parts.push(`until ${block.until}`); if (block.focus) parts.push(`focus: ${block.focus}`); if (block.allowed?.length) parts.push(`allowed without asking: ${block.allowed.join(", ")}`); return `[autopilot ${block.ended ? "was on" : "on"}: ${parts.join(" — ")}]`; } case "autopilot.paused": return `[autopilot paused: ${block.text ?? "no progress"}]`; case "autopilot.stopped": return "[autopilot stopped by the user]"; case "autopilot.finished": return `[autopilot finished after ${block.steps ?? 0} steps: ${block.text ?? ""}]`; } } /** The blocks with the start line of `autopilotId` marked ended, or null when nothing changes. */ export function markAutopilotLinesEnded( blocks: readonly { kind: string }[], autopilotId: string, ): AutopilotBlock[] | null { let changed = false; const next = blocks.map((block) => { const line = block as AutopilotBlock; if (line.kind !== "autopilot" || line.autopilotId !== autopilotId || line.ended) return line; if (line.event !== "autopilot.started" && line.event !== "autopilot.paused") return line; changed = true; return { ...line, ended: true }; }); return changed ? next : null; } /** Count a checkpoint on the autopilot's start line and keep the latest few. */ export function addAutopilotCheckpoint( blocks: readonly { kind: string }[], autopilotId: string, checkpoint: { text: string; at: string }, steps: number, ): AutopilotBlock[] | null { let changed = false; const next = blocks.map((block) => { const line = block as AutopilotBlock; if (line.kind !== "autopilot" || line.autopilotId !== autopilotId) return line; if (line.event !== "autopilot.started") return line; changed = true; return { ...line, steps, checkpoints: [...(line.checkpoints ?? []), checkpoint].slice(-AUTOPILOT_CHECKPOINT_LOG), }; }); return changed ? next : null; } /** When the autopilot should stop: never on its own, at the next 8:00, or at the next given time. */ export type AutopilotUntilChoice = | { kind: "done" } | { kind: "morning" } | { kind: "time"; time: string }; export function autopilotUntil(choice: AutopilotUntilChoice, now: Date): Date | undefined { if (choice.kind === "done") return undefined; const [hours, minutes] = choice.kind === "morning" ? [8, 0] : choice.time.split(":").map((part) => Number(part)); if (!Number.isInteger(hours) || !Number.isInteger(minutes)) return undefined; const until = new Date(now); until.setHours(hours ?? 0, minutes ?? 0, 0, 0); if (until.getTime() <= now.getTime()) until.setDate(until.getDate() + 1); return until; } /** "3 h 42 min", "12 min", or "1 min" for anything shorter. */ export function formatAutopilotDuration(ms: number): string { const minutes = Math.max(1, Math.round(ms / 60_000)); const hours = Math.floor(minutes / 60); const rest = minutes % 60; if (!hours) return `${minutes} min`; return rest ? `${hours} h ${rest} min` : `${hours} h`; }