import type { WorkflowRun, WorkflowRunStatus } from "../orchestration/workflow.js"; /** * design/97 S1b — the **WorkflowRunStore** persistence seam for the workflow-mode primitive * (`src/orchestration/workflow.ts`, S1a). It is the durable home for a {@link WorkflowRun} so the * `/workflows` observability layer (S1c) can list runs IN PROGRESS *and the history* (completed / failed), * across replicas — the "看之前的" core seam (design/97 §A/§D.1). * * **Distinct from the four load-bearing durable seams** (session / checkpoint / memory / tool-result): those * are the engine's suspend/resume命脉, so core ships PG adapters for them. A WorkflowRunStore is an * **observation layer — opt-in, non-engine-critical** — so per design/97 §D.8 (clay 2026-06-22 拍) core * ships ONLY: this interface + {@link InMemoryWorkflowRunStore} (process-local / default / tests) + * `FileWorkflowRunStore` (zero-dependency, TOC-local persistence — `src/stores/file/workflow-run-store.ts`). * **The PG backend lives in service (TOB), implementing THIS interface** when it needs cross-replica * `/workflows` history. `WorkflowRun` / `WorkflowRunStatus` are imported type-only — this module is purely * the store contract + projection, it does NOT re-home the run model (that stays in `workflow.ts`). * * Mirrors the {@link import("./checkpoint-store.js").CheckpointStore} shape: create-once `put`, `get`, an * **atomic CAS `update`** (scope + `rev` optimistic-concurrency — concurrent agents racing to record progress * on the same run), a `listByScope` returning lightweight {@link WorkflowRunSummary} projections (history * included, NOT pending-only like CheckpointStore.listByScope), and a `reap` retention sweep that only ever * deletes TERMINAL runs (never a running one). The {@link summarizeWorkflowRun} projection is SHARED across * all backends (anti-drift, exactly like `summarizeCheckpoint`). */ /** * A lightweight, read-only projection of one {@link WorkflowRun} returned in bulk by * {@link WorkflowRunStore.listByScope} so the `/workflows` list view (CLI design/92 / client design/90) can * enumerate a scope's runs in ONE call — no N+1 `get`s, no full run payloads. Every field is DERIVED from * the persisted run (nothing here is a new source of truth or a gate input). Built by the shared * {@link summarizeWorkflowRun} so the projection is IDENTICAL across the in-memory, file, and (service) PG * backends. */ export interface WorkflowRunSummary { /** The run id ({@link WorkflowRun.id}). */ id: string; /** The tenant / grouping key this run lives in ({@link WorkflowRun.scope}). */ scope: string; /** design/99 MF-W: the workflow's display name from the script's `export const meta` ({@link WorkflowRun.name}). * Projected here so the `/workflows` list view labels each row WITHOUT an N+1 `get` of the full run. * Absent for a direct `runWorkflow` call that supplied no name. */ name?: string; /** One-line description from the script's meta ({@link WorkflowRun.description}); absent if none. */ description?: string; /** Lifecycle status ({@link WorkflowRun.status}): running / completed / failed. */ status: WorkflowRunStatus; /** Title of the latest phase recorded ({@link WorkflowRun.phases}`.at(-1).title`) — what the run is on RIGHT * NOW for a `running` row (so the `/workflows` list shows the live phase without subscribing to the event * stream). Absent when no phase has started yet. */ currentPhase?: string; /** Number of phases recorded so far ({@link WorkflowRun.phases}`.length`). */ phaseCount: number; /** Number of agent-runs recorded so far ({@link WorkflowRun.agents}`.length`). */ agentCount: number; /** Total tokens spent = own + nested (`stats.tokens + stats.nested.tokens`) — the figure a triage view * sorts/compares by. own/nested stay SEPARATE on the full run (R-5); the summary folds them for display. */ tokens: number; /** When the run started ({@link WorkflowRun.startedAt}, epoch ms). */ startedAt: number; /** When the run finished ({@link WorkflowRun.endedAt}, epoch ms) — absent while still running. */ endedAt?: number; /** When the run record was created ({@link WorkflowRun.createdAt}, epoch ms) — the `listByScope` sort key. */ createdAt: number; } /** * design/97 S1b: project a {@link WorkflowRun} to its lightweight {@link WorkflowRunSummary}. SHARED by every * {@link WorkflowRunStore} impl so the projection is IDENTICAL across the in-memory, file, and (service) PG * backends (the anti-drift guard — exactly like `summarizeCheckpoint`). Pure; reads no clock/random. Total * spend folds own + nested (`stats.tokens + stats.nested.tokens`), the one place the two are summed. */ export declare function summarizeWorkflowRun(run: WorkflowRun): WorkflowRunSummary; /** Whether a run is in a TERMINAL state — the ONLY states {@link WorkflowRunStore.reap} may delete (a * `running` run is never reaped, so an in-flight workflow's record can't vanish out from under it). */ export declare function isTerminalWorkflowStatus(status: WorkflowRunStatus): boolean; /** A typed workflow-run-store error so callers branch on `code` (mirrors `CheckpointError`). */ export declare class WorkflowRunStoreError extends Error { readonly code: "workflow_run.already_exists"; constructor(code: "workflow_run.already_exists", message: string); } /** * The pluggable persistence seam for workflow runs (design/97 S1b). Symmetric with `CheckpointStore`: * create-once `put`, `get`, an atomic CAS `update`, a `listByScope` query (history included), and a `reap` * retention sweep. A durable backend (service's PG) makes `/workflows` history cross-replica; the default * {@link InMemoryWorkflowRunStore} is process-only (single instance / tests). */ export interface WorkflowRunStore { /** Create-once. Throws {@link WorkflowRunStoreError} `already_exists` on a token collision (never a silent * overwrite — a reused id would clobber a live run's record). */ put(id: string, run: WorkflowRun): Promise; get(id: string): Promise; /** * Atomic CAS update of an EXISTING run (`UPDATE … SET … WHERE id=? AND scope=?` [`AND rev=?`]). Returns * `true` for the winner, `false` when the row is missing, the `scope` does not match (multi-tenant * isolation — a wrong-scope update must not win), or `expect.rev` does not match the live row's `rev` * (an intervening update bumped it). On success the store bumps the row's `rev` (monotonic) and persists * the new `run`. The workflow owner is the sole writer (best-effort progress recording — design/97 §F), * so `expect.rev` lets it detect a lost write rather than silently clobber. */ update(id: string, scope: string, run: WorkflowRun, expect?: { rev: number; }): Promise; /** * List lightweight {@link WorkflowRunSummary} projections for runs in `scope` — **history included** (the * completed / failed runs, NOT pending-only like `CheckpointStore.listByScope`), so this is the "看之前的" * core query. Newest first (`createdAt` DESC). `opts.status` filters to one lifecycle state; `opts.limit` * caps the result count (applied AFTER the sort, so it keeps the newest N). An empty scope returns `[]`. */ listByScope(scope: string, opts?: { status?: WorkflowRunStatus; limit?: number; }): Promise; /** * Retention sweep: delete OLD **terminal** runs in `scope` (a `running` run is NEVER deleted — design/97 * §D.1/§F). Returns the count deleted (for metrics). Retention is by either policy (apply BOTH when both * given — a run is reaped if it fails either bound): * - `opts.maxAgeMs`: delete a terminal run whose `endedAt` (else `createdAt`) is older than `now - * maxAgeMs`. * - `opts.keep`: keep only the newest `keep` terminal runs (by `createdAt` DESC); delete the rest. * With NEITHER option set, nothing is deleted (a no-op `0`) — retention is always an explicit policy. */ reap(scope: string, now: number, opts?: { maxAgeMs?: number; keep?: number; }): Promise; } /** * Default in-process {@link WorkflowRunStore}. Single-instance / tests only — it does NOT survive a restart * or span replicas. Single-threaded JS already serializes `update`, so the CAS is trivially atomic here; * `structuredClone` on the boundaries prevents aliasing (a later mutation of the caller's object — the live * `WorkflowRun` the workflow keeps mutating — must not corrupt the stored row, and a returned run must not * be a live reference the caller can mutate back into the store). */ export declare class InMemoryWorkflowRunStore implements WorkflowRunStore { private runs; put(id: string, run: WorkflowRun): Promise; get(id: string): Promise; update(id: string, scope: string, run: WorkflowRun, expect?: { rev: number; }): Promise; listByScope(scope: string, opts?: { status?: WorkflowRunStatus; limit?: number; }): Promise; reap(scope: string, now: number, opts?: { maxAgeMs?: number; keep?: number; }): Promise; /** Test/inspection helper: number of stored runs. */ get size(): number; } //# sourceMappingURL=workflow-run-store.d.ts.map