/** * runCheckpoint — fault-tolerant resume primitives. * * Today's pause/resume only handles INTENTIONAL pauses (`askHuman`). * Errors mid-run (LLM 500s, vendor outages, tool throws, container * restarts) propagate all the way up and the consumer must restart * from scratch — losing the prior iterations' work. * * This module adds the third piece of the Reliability subsystem: * * 1. **`AgentRunCheckpoint`** — JSON-serializable snapshot of an * agent run's progress. Captured automatically at each * iteration boundary (the natural commit points). Survives * process restart — persist to Redis / Postgres / S3 / queue. * * 2. **`RunCheckpointError`** — wraps the underlying error with * the last-known-good checkpoint. Throwing this instead of the * raw error lets consumers catch + persist + resume later * without losing context. * * 3. **`agent.resumeOnError(checkpoint)`** — replays the agent run * with the checkpointed conversation history restored. The * next iteration retries the call that originally failed (with * the latest provider state — circuit breaker may have closed, * vendor may have recovered, etc.). * * Design tradeoff: we use a CONVERSATION-HISTORY checkpoint shape * rather than a full executor-state checkpoint (which would require * footprintjs API surface changes for mid-run snapshotting). The * tradeoff: * * ✅ Survives process restart (JSON-serializable, tiny payload) * ✅ Works with any LLM provider — replay starts from history * ✅ No footprintjs core changes * ⚠️ Loses mid-iteration partial state (acceptable — iterations * are atomic; we resume from the last completed boundary) * ⚠️ TOOL RE-EXECUTION (idempotency requirement): anything the * failed iteration did after the last completed boundary — * including tool side effects — is NOT in the checkpoint. On * resume the model re-decides from the restored history and may * re-issue those tool calls; they WILL execute again. There is * NO built-in toolCallId-based dedup. Mutating tools (payments, * emails, DB writes) must be idempotent — derive an idempotency * key from stable call content, not from `ctx.toolCallId` (fresh * per issued call, so a re-issued call gets a NEW id). Note the * same requirement exists WITHOUT resume: a tool that performs * its side effect and then throws reports the error message back * to the model as the tool result, and the model typically * retries the call on the next iteration. * * Pattern: Memento (GoF) — snapshot of an object's internal state * for later restoration. Same shape as `FlowchartCheckpoint` * but at the agent layer (one logical iteration vs. one * DFS stage). */ import type { LLMMessage } from '../adapters/types.js'; /** * JSON-serializable checkpoint of an in-progress agent run. Persist * to ANY durable store (Redis / Postgres / S3 / disk / queue) and * resume hours / days / deploys later via `agent.resumeOnError(...)`. * * **Stable shape** — the `version` field guards forward compat. v1 * → v2 transitions will be supported via a migration helper. */ export interface AgentRunCheckpoint { /** Schema version. v1 = conversation-history-based. */ readonly version: 1; /** `runId` of the FAILING run — lets the consumer correlate a * persisted checkpoint back to the original run's observability. * NOT reused on resume: `resumeOnError` starts a fresh run with a * fresh `runId` (only the conversation history is restored). */ readonly runId: string; /** Conversation history at the LAST completed iteration boundary * (LLM messages). The next iteration retries from here. */ readonly history: readonly LLMMessage[]; /** Index of the last completed iteration in the FAILING run * (diagnostic — not consumed on resume). The resumed run restores * this history but re-seeds its own iteration counter at 1 with a * full `maxIterations` budget. */ readonly lastCompletedIteration: number; /** Original input message. Surfaces in observability + lets the * consumer correlate checkpoint to the user's request. */ readonly originalInput: { readonly message: string; }; /** Wall-clock when the checkpoint was captured. Diagnostic only. */ readonly checkpointedAt: number; /** Where the failure happened. Diagnostic — surfaces in oncall * triage so you can tell "LLM 500 mid-iteration" from "tool * threw" from "validation kept failing". */ readonly failurePoint?: { readonly iteration: number; readonly phase: 'iteration' | 'tool' | 'llm' | 'unknown'; }; } /** * Thrown by `agent.run()` when a fault occurs mid-run. Carries the * underlying error AND the last-known-good checkpoint. Catch this * specifically to engage the resume-on-error path; let other errors * propagate normally. * * @example * ```ts * import { Agent, RunCheckpointError } from 'agentfootprint'; * * try { * const result = await agent.run({ message: 'long task' }); * } catch (err) { * if (err instanceof RunCheckpointError) { * await checkpointStore.put(sessionId, err.checkpoint); * // hours / restart later: * const checkpoint = await checkpointStore.get(sessionId); * const result = await agent.resumeOnError(checkpoint); * } else { * throw err; // not a recoverable error — propagate * } * } * ``` */ export declare class RunCheckpointError extends Error { readonly code: "ERR_RUN_CHECKPOINT"; /** The error that triggered the checkpoint. Inspect for retry * decisions ("if cause is CircuitOpenError, wait for cooldown * before resuming"). */ readonly cause: Error; /** The last-known-good checkpoint. Persist + pass back to * `agent.resumeOnError(checkpoint)` to continue from here. */ readonly checkpoint: AgentRunCheckpoint; constructor(cause: Error, checkpoint: AgentRunCheckpoint); } /** * Mutable state the Agent maintains during a run for checkpoint * capture. Keyed by `runId` so multiple in-flight runs don't * collide. Cleared on `turn_end` (success path). * * @internal */ export interface RunCheckpointTracker { readonly runId: string; readonly originalInput: { readonly message: string; }; /** Updated on every `agentfootprint.agent.iteration_end`. */ history: readonly LLMMessage[]; /** Updated on every `agentfootprint.agent.iteration_end`. */ lastCompletedIteration: number; /** Set when an iteration begins (used to attribute the failure * phase if we throw before the next iteration_end). */ inFlightIteration?: number; } /** * Build a JSON-serializable checkpoint from a tracker + failure * info. Pure function — no side effects. * * @internal */ export declare function buildCheckpoint(tracker: RunCheckpointTracker, failurePoint?: { iteration: number; phase: AgentRunCheckpoint['failurePoint'] extends infer F ? F extends { phase: infer P; } ? P : never : never; }): AgentRunCheckpoint; /** * Validate a checkpoint at deserialization time. Catches forward- * incompatible payloads (someone tries to resume a v3 checkpoint on * a v1 runtime, or a corrupted JSON blob). * * Returns the checkpoint typed-narrowed; throws TypeError on * unknown shape. */ export declare function validateCheckpoint(value: unknown): AgentRunCheckpoint; /** * Classify a thrown error into one of the failure-point phase * buckets. Heuristic — uses error name / code / message inspection. * Fast path returns 'unknown' so unrecognized errors still produce * a checkpoint (the cause itself is preserved in * `RunCheckpointError.cause`). */ export declare function classifyFailurePhase(err: Error): 'iteration' | 'tool' | 'llm' | 'unknown';