import { randomUUID } from "node:crypto"; import type { SessionEntry } from "@mariozechner/pi-coding-agent"; import { GRIND_STATE_ENTRY_TYPE, type GrindActivation, type GrindRunState, type GrindRunStatus, type GrindStatusPayload, } from "./types.js"; function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object"; } function isGrindRunStatus(value: unknown): value is GrindRunStatus { return ( value === "active" || value === "paused" || value === "done" || value === "blocked" || value === "expired" || value === "stopped" ); } function isGrindActivation(value: unknown): value is GrindActivation { return value === "explicit" || value === "command"; } function isNullableString(value: unknown): value is string | null { return typeof value === "string" || value === null; } export function createRunState(input: { activation: GrindActivation; goal: string; sourcePrompt: string; deadlineAt: string | null; completionCriterion: string | null; }): GrindRunState { const now = new Date().toISOString(); return { version: 1, runId: randomUUID(), activation: input.activation, status: "active", goal: input.goal, sourcePrompt: input.sourcePrompt, deadlineAt: input.deadlineAt, completionCriterion: input.completionCriterion, lastCheckpoint: null, lastNextAction: null, pendingRepair: false, consecutiveParseFailures: 0, updatedAt: now, }; } export function isValidRunState(value: unknown): value is GrindRunState { if (!isRecord(value)) { return false; } return ( value.version === 1 && typeof value.runId === "string" && isGrindActivation(value.activation) && isGrindRunStatus(value.status) && typeof value.goal === "string" && typeof value.sourcePrompt === "string" && isNullableString(value.deadlineAt) && isNullableString(value.completionCriterion) && isNullableString(value.lastCheckpoint) && isNullableString(value.lastNextAction) && typeof value.pendingRepair === "boolean" && typeof value.consecutiveParseFailures === "number" && typeof value.updatedAt === "string" ); } export function getLatestRunState(entries: readonly SessionEntry[]): GrindRunState | null { for (let index = entries.length - 1; index >= 0; index -= 1) { const entry = entries[index]; if (entry.type !== "custom" || entry.customType !== GRIND_STATE_ENTRY_TYPE) { continue; } if (isValidRunState(entry.data)) { return entry.data; } } return null; } export function withStatus( state: GrindRunState, status: GrindRunStatus, overrides: Partial = {}, ): GrindRunState { return { ...state, ...overrides, status, updatedAt: new Date().toISOString(), }; } export function withLoopStatus(state: GrindRunState, payload: GrindStatusPayload): GrindRunState { return { ...state, status: payload.state === "continue" ? "active" : payload.state === "done" ? "done" : "blocked", lastCheckpoint: payload.summary, lastNextAction: payload.nextAction ?? null, pendingRepair: false, consecutiveParseFailures: 0, updatedAt: new Date().toISOString(), }; }