/** * Pi Loop Extension - State Management * * Manages loop run state via Pi session entries. * State is persisted across session restarts using pi.appendEntry() * and reconstructed from session entries on session_start. */ import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent"; import type { NodeInstanceState, RunState } from "./types.ts"; import { createEvidenceSummary } from "./evidence.ts"; const CUSTOM_TYPE = "pi-loop"; /** * Save the current run state to the Pi session. */ export function saveRunState(pi: ExtensionAPI, run: RunState): void { pi.appendEntry(CUSTOM_TYPE, { version: 1, run, }); } /** * Attempt to restore a run state from the session entries. * Returns the most recent run state if found, or undefined. */ export function restoreRunState(ctx: ExtensionContext): RunState | undefined { const entries = ctx.sessionManager.getEntries(); const loopEntries = entries.filter( (e): e is SessionEntry & { customType: string; data: { version: number; run: RunState } } => e.type === "custom" && (e as unknown as Record).customType === CUSTOM_TYPE, ); if (loopEntries.length === 0) return undefined; // Get the most recent entry const latest = loopEntries[loopEntries.length - 1]; return latest.data?.run; } /** * Find all run state entries in session for a specific run ID. */ export function findRunEntries( ctx: ExtensionContext, runId?: string, ): Array<{ entry: SessionEntry; run: RunState }> { const entries = ctx.sessionManager.getEntries(); return entries .filter( (e): e is SessionEntry & { customType: string; data: { version: number; run: RunState } } => e.type === "custom" && (e as unknown as Record).customType === CUSTOM_TYPE, ) .filter((e) => !runId || e.data?.run?.id === runId) .map((e) => ({ entry: e, run: e.data?.run })); } /** * Create a new RunState for a given loop definition. */ export function createRunState( runId: string, loopId: string, input?: Record, ): RunState { return { id: runId, loopId, status: "running", currentNodeIndex: 0, currentAttemptNumber: 1, nodes: [], startedAt: Date.now(), updatedAt: Date.now(), artifacts: {}, evidence: createEvidenceSummary(), input: input ?? {}, totalTurns: 0, totalToolCalls: 0, steeringEventsTotal: 0, }; } /** * Update the timestamp and status of a run state. */ export function updateRunState( run: RunState, updates: Partial>, ): void { Object.assign(run, updates); run.updatedAt = Date.now(); } /** * Get the current node's state from the run. */ export function getCurrentNode(run: RunState): NodeInstanceState | undefined { return run.nodes[run.currentNodeIndex]; } /** * Check if a run is active (i.e., can be resumed/continued). */ export function isRunActive(run: RunState): boolean { return run.status === "running" || run.status === "paused"; } /** * Get a human-readable status line for the UI. */ export function formatRunStatus(run: RunState): string { const current = getCurrentNode(run); const nodeLabel = current?.nodeDef.label ?? "none"; const attempt = run.currentAttemptNumber; return `Loop: ${run.loopId}#${run.id.slice(0, 8)} | Node: ${nodeLabel} | Attempt: ${attempt} | Status: ${run.status}`; }