@sema-agent/core
Version:
Stateless, task-oriented AI agent core
108 lines (107 loc) • 7.63 kB
TypeScript
/**
* 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;