UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

108 lines (107 loc) • 7.63 kB
/** * The execution-outcome record's vocabulary (layer 0, beside the checkpoint store it serves): the closed * answer set of `CheckpointStore.recordExecutionOutcome`, its one disposition table, the conflict error, * the shared write disposition and reopen predicate every backend routes through, the structural * equality the idempotency arm uses, and the one read face of the column. The column itself * (`CheckpointRow.executionOutcome` / `executionAtMs`) and the verb's contract are declared on the store * (`checkpoint-store.ts`); this module names no store type — the row facts it reads are spelled * structurally — so the store reaches DOWN for it and nothing here reaches back. */ import { type AssertAllKeysHandled } from "./ask-origin.js"; import type { GateOutcome } from "./gate-outcome.js"; /** * The closed answer set of `CheckpointStore.recordExecutionOutcome` — what the store did with the * execution record it was handed. One word per way the write can end; a caller keys on the word, never * on an absence: * - `recorded` — the CAS won: the row was `resolved` with no record yet, and now carries this one. * - `already_recorded` — the row already carries a record EQUAL to the one supplied (a retry of the * same write; idempotent, nothing changed). A record that DIFFERS is not a word — it is the * {@link ExecutionOutcomeConflictError} throw. * - `not_resolved` — the row exists in this scope but is not `resolved` (still pending, or expired): * there is no decision an execution could be the outcome of, so nothing was written. * - `absent` — no row under this token in this scope (a missing row and a wrong-scope row alike — the * isolation rule every verb of the store keeps). */ export declare const EXECUTION_OUTCOME_RECORD_WORDS: readonly ["recorded", "already_recorded", "not_resolved", "absent"]; export type ExecutionOutcomeRecordWord = (typeof EXECUTION_OUTCOME_RECORD_WORDS)[number]; /** * The one disposition table over {@link ExecutionOutcomeRecordWord}: does this answer mean the row now * carries NO execution record — i.e. the leg's disposition of the decided action is not on file? The * engine's belt reads it to decide whether to announce `checkpoint.execution_outcome_unrecorded` (the * execution result itself is never changed by the answer: the record is an account of what ran, not a * gate on it). A new word must take a row here (the fence below reds `tsc` otherwise). */ export declare const EXECUTION_RECORD_LEAVES_ROW_UNRECORDED: { readonly recorded: false; readonly already_recorded: false; readonly not_resolved: true; readonly absent: true; }; export type ExecutionRecordTableCoversEveryWord = AssertAllKeysHandled<Exclude<ExecutionOutcomeRecordWord, keyof typeof EXECUTION_RECORD_LEAVES_ROW_UNRECORDED>>; /** * The read face of `CheckpointRow.executionOutcome`: what a reader of a resolved row can say about how * the decided action was disposed. `executed` carries the record the resumed leg filed; `unknown` is the * ONE meaning of the column's absence (the leg has not reached its resolution yet, the row was consumed * by a worker that could not record, or the store answered `not_resolved`/`absent` and the engine * announced it). Never `allowed` by default — the pre-execution decision lives on `resolvedOutcome`. */ export type ExecutionVerdict = { kind: "executed"; gate: GateOutcome; } | { kind: "unknown"; }; /** * `CheckpointStore.recordExecutionOutcome` was handed a record for a row that already carries a DIFFERENT * one. The record is physically immutable (see `CheckpointRow.executionOutcome`), so this is never * resolved by overwriting: two different accounts of one execution mean the engine ran, or believes it * ran, the same decided action twice — an engine defect the belt must surface, not a store condition it * may absorb. Carries both records so the operator can read what was on file and what tried to replace * it. Code `checkpoint.execution_outcome_conflict`; a deployment store may throw any error carrying the * same `code` — the code is the contract, this class is its in-tree form. */ export declare class ExecutionOutcomeConflictError extends Error { readonly code: "checkpoint.execution_outcome_conflict"; /** The record on file — a COPY: a backend hands this constructor its live row value, and a caller that * catches the error must not be able to rewrite the row through it (the record is immutable by CAS, * not by convention). */ readonly recorded: GateOutcome; /** The record that tried to replace it — copied for the same reason. */ readonly attempted: GateOutcome; constructor(recorded: GateOutcome, attempted: GateOutcome); } /** The row facts the record's disposition reads — spelled structurally so a backend can hand a partial * read-back (a Pg row) as easily as a full `Checkpoint`. */ export interface ExecutionRecordRowFacts { scope: string; status: "pending" | "resolved" | "expired"; executionOutcome?: GateOutcome; } /** `true` iff the row carries an execution record — the reopen verbs' extra predicate (a settled action * is not re-openable) and the record verb's own idempotency read. Pure; shared by every backend. */ export declare function checkpointExecutionRecorded(cp: Pick<ExecutionRecordRowFacts, "executionOutcome">): boolean; /** Structural equality of two execution records as the JSON they round-trip as — key-order insensitive * (a jsonb column reorders keys) and indifferent to freezing (the engine's record is minted frozen; a * store's copy is not). A record is a plain data graph by construction, so nothing here needs the * accessor/topology defences a caller-supplied value would. */ export declare function sameExecutionRecord(a: GateOutcome, b: GateOutcome): boolean; /** * THE one disposition of an execution-record write, shared by every backend so the CAS predicate * `status = 'resolved' AND execution_outcome IS NULL` and its two idempotency/conflict arms cannot drift * between stores (the same posture as the shared row predicate `checkpointRowMatches`). Pure: it decides * what the write means for THIS row and this record; the backend keeps its own commit half (an in-place * assignment, an appended ledger event, an `UPDATE … WHERE` that folds the same predicate into SQL). * - `commit` — the row is `resolved` in this scope with no record: write it (the answer is `recorded`). * - `already_recorded` / `not_resolved` / `absent` — the {@link ExecutionOutcomeRecordWord} to answer. * - `conflict` — the row carries a different record: throw {@link ExecutionOutcomeConflictError}. * Order is load-bearing: a record on file is judged BEFORE the status arm, so a retry against a row whose * status a concurrent verb has since moved still answers by the record it carries. */ export declare function executionRecordDisposition(cp: ExecutionRecordRowFacts | undefined, scope: string, gate: GateOutcome): "commit" | "conflict" | Exclude<ExecutionOutcomeRecordWord, "recorded">; /** * The ONE read of `CheckpointRow.executionOutcome`: `executed` with the record when the row carries one, * `unknown` otherwise. A consumer rebuilding a run from its rows (an audit face, a supervisor inbox after * a stream break) reads this rather than the column, so "no column" and "column not yet written" have * exactly one spelling between them and no reader invents a default disposition. */ export declare function executionVerdict(cp: Pick<ExecutionRecordRowFacts, "executionOutcome">): ExecutionVerdict;