@sema-agent/core
Version:
Stateless, task-oriented AI agent core
146 lines (145 loc) • 11.4 kB
TypeScript
/**
* The resumed leg's DECIDE vocabulary — everything the resume engine needs to read a decide outcome and
* carry it into the continuation, none of which is a step the driver calls with an Input:
* · was the decision NEGATIVE (a delivered refusal consumes its gate; no reopen compensation may fire);
* · is a replayed winner the SAME decision as the persisted one (reopen-by-reason guard,
* with the structural JSON equality it needs);
* · which call a CONTENT-ASK checkpoint parks on, and the one-shot answering face a decide's answer is
* bound to for the redeemed call;
* · the capture-time domain checks on a decide's TEXT payloads, and the label a rejected value gets;
* · WHO decided: the host's decision facts, validated once and minted into the settlement record once, for every
* decide that records its decider (a tool approval's and a plan pause's);
* · the continuation prompt itself.
*
* Pure over its arguments (the answering face is the one closure, and it closes over its own arguments
* only). Run-loop machinery (layer 1), named without the `resume-` family prefix: that prefix marks a
* driven rung of the resume decision ladder, which this is not.
*/
import { type AskOrigin } from "../ask-origin.js";
import { type OnQuestion, type QuestionAnswer } from "../ask-question.js";
import { type Checkpoint, type ResolvedOutcome, type ResumeOutcome } from "../checkpoint-store.js";
import { type GateOutcome, type HostDecisionFacts } from "../gate-outcome.js";
import type { ResumeRun } from "./contracts.js";
/**
* Was this resume driven by a NEGATIVE human decision (plan_review/dry_run_review
* `reject`, policy_ask `deny`)? Such a decision consumes its gate by being DELIVERED: there is no owed
* action whose non-start could waste it, so the reopen compensation (both the throw-path and the
* settle-path arm) must never fire for it — reopening re-asks a gate the human already refused, and on
* the plan_review reject leg it minted a zombie gate beside the re-plan's NEW park (the session then
* reported the same decidePath locked no matter how many times the human decided).
*/
export declare function resumeDecisionWasNegative(resume: ResumeRun): boolean;
/**
* Structural deep equality of two JSON-serializable values (order-independent over object keys). Used ONLY
* by the Runner's `resumeStream` reopen-by-reason guard to confirm an `env_failed` re-resume
* replays the EXACT persisted winner (its `updatedInput` payload included). This is a same-runtime
* value-vs-value comparison of two payloads that already round-trip JSON in the durable store — it is NOT
* the cross-runtime `boundInputHash` canonicalization (out of scope for slice 1b), which
* the spec deliberately keeps server-minted-opaque to avoid false mismatches.
*/
export declare function deepJsonEqual(a: unknown, b: unknown): boolean;
/** Two resume winners are the SAME decision iff they bind the same call,
* the same allow/deny verdict, the same (deep-equal) `updatedInput` rewrite AND the same (deep-equal)
* content-ask `answer`. An `env_failed` re-resume must replay an identical winner; any divergence is a
* re-vote (rejected).
*
* ORDER MATTERS — `incoming` is the decide being replayed, `persisted` is the row's recorded winner. The
* `reason` dimension is compared ASYMMETRICALLY (see below), so the two arguments are not interchangeable. */
export declare function sameWinner(incoming: ResolvedOutcome, persisted: ResolvedOutcome): boolean;
/** The tool call a CONTENT-ASK checkpoint parks on — the pending `tool_approval` call of the reserved
* question tool — or `undefined` when this checkpoint parks on anything else (a side-effecting tool's
* approval, a resource slice, a review pause). The single predicate both the answer-binding guard and the
* answering-face injection key on, so "is this decide answering a question?" is asked exactly one way. */
export declare function pendingContentAskCallId(cp: Checkpoint): string | undefined;
/**
* Bind a decide's content-ask answer as the resumed leg's answering face (ruled 2026-08-04) — the same
* shape the documented live arm builds by hand (`{ ...taskConfig, onQuestion: async () => answer }`), so
* the redeemed call executes against the SAME tool and the SAME untrusted-answer fence rather than a
* second, parallel answer path.
*
* ONE-SHOT, bound to the redeemed call id: a decide answers the question the operator was shown, and
* nothing else. Should the resumed leg ask a NEW question, this face must not replay the previous answer
* to it (the questions differ; the fence's `selected ⊆ options` would strip the selections and the
* operator's note would be re-delivered out of context). It falls through to whatever answering face the
* resume already had, and — with none — refuses, which the tool reports to the model as "no answer was
* obtained", its honest headless outcome. In the durable topology this fallthrough is normally
* unreachable: a second question is adjudicated `ask` and parks for its own decide before executing.
*/
export declare function answerFaceForRedeemedCall(answer: QuestionAnswer, redeemedCallId: string, base: OnQuestion | undefined,
/** Digest of the question batch the operator was shown, so redemption cannot be claimed by a
* DIFFERENT question that happens to arrive under the same call id. */
redeemedQuestionsHash: string): OnQuestion;
/**
* A label for a REJECTED deployment-supplied value, safe to put in an error message. Never coerces:
* `String(x)` on an object with no `toPrimitive` path (a null-prototype record, a throwing
* `Symbol.toPrimitive`) throws, which would replace this module's typed refusal with a raw TypeError —
* the malformed input escaping the very classification the refusal exists to give.
*/
export declare function describeSuppliedValue(value: unknown): string;
/** The decide lane a capture serves — the outcome gate word of an arm that carries the facts (the lane only words the refusals). */
export type DecideLane = Extract<ResumeOutcome, {
hostDecision: HostDecisionFacts;
}>["gate"];
/** The member every durable decide refuses when a caller supplies it: the settlement record is core's mint. */
export declare const SUPPLIED_RECORD_KEY: readonly [string, string];
/** Refuse a decide that carries a key it may not (each with the sentence that says why). Presence is the whole test —
* no value read here is ever consumed, so a stateful accessor has no seam to open; `Reflect.get` keeps the read
* dynamic without naming a refused key as a property. */
export declare function refuseSuppliedKeys(decide: object, keys: ReadonlyArray<readonly [string, string]>): void;
/** What a decide says it decided, in the lane's own words: `word` is the verdict as the caller spelled it (for the
* sentence), `allows` whether it is the ALLOW direction (a tool approval's `allow`; a plan pause's `approve` / `edit`). */
export interface DecideVerdict {
readonly word: string;
readonly allows: boolean;
}
/**
* Read and judge the host's decision facts — the ONE validation for every durable decide. `raw` is the caller's
* `hostDecision`, read ONCE by the lane (this function reads `decidedBy` and `approver` once each, off it).
* - the facts are REQUIRED: absence (or a non-object) is refused — absence is not a fact about the wait, and core
* does not guess one;
* - `decidedBy` is a member of the closed set ({@link isHostDecider}); the sweep's word is the host's SLA sweep and
* a sweep resolves only rows whose deadline is an SLA resolve-deny (facet `A`, read through `facetOf`) — on any other row
* the word would file a record of an event that row's contract says cannot happen;
* - WHO MAY ALLOW the row: not the sweep's word (a wait nobody answered), and not the policy on a row that demands a
* person (`policyCannotAnswer`, the synchronous seat's rule);
* - the attribution passes the shared screen (a bounded plain string) — refused rather than dropped.
*/
export declare function captureHostDecision(cp: Checkpoint, raw: unknown, lane: DecideLane, verdict: DecideVerdict): HostDecisionFacts;
/** The origin word of the ask a pending row holds: a tool approval's was stamped on the park by the gate; a plan
* pause's is its kind (nothing persisted — the row's kind is the whole fact). Undefined for a row that holds no ask. */
export declare function parkedOriginOf(cp: Checkpoint): AskOrigin | undefined;
/**
* THE mint for a durable decide's settlement record — the last pre-CAS station of both decide lanes: the host's
* facts joined with the row's origin (invariant I1: a settlement names its origin), one record for the row's
* resolved outcome. `allowed` is the allow direction; the refusal's `note` is the decider's own text (filed on a
* person's refusal only). A row minted before the origin was persisted carries none, and the record cannot be
* minted honestly for it: refused by version floor (the row stays pending), never guessed.
*/
export declare function mintDecideRecord(cp: Checkpoint, facts: HostDecisionFacts, verdict: {
readonly allowed: boolean;
readonly note: string | undefined;
}): GateOutcome;
/**
* Capture-time domain check for a decide outcome's TEXT payloads — the review lanes' `editedPlan` /
* `reason` and the approval lane's deny `reason`. All are persisted (the recorded winner's payload,
* compared when an `env_failed` reopen replays it) and all are fenced into the model-facing
* continuation reminder. The declared wire type is a string; a value outside it reads one way in this
* process and another after a JSON round-trip, and reaches the reminder as whatever its stringification
* happens to be. Worse, the pre-CAS tag-hygiene check and the fence BOTH reach the value through
* `String.prototype.replace`: an object carrying its own `replace` is therefore read twice and may
* answer differently each time — the check passing on text it never inspected. Refused pre-CAS so the
* row stays `pending` for a well-formed decide, exactly like the other capture-time domain refusals,
* and BEFORE the hygiene check so no guard ever consults a value outside the domain. The rejected field
* is named in the message only — `CheckpointError`'s structured `detail.field` is a closed set covering
* the approval lane's payload names, and widening it is a wire-surface change of its own.
*/
export declare function assertOutcomeText(value: string | undefined, field: "editedPlan" | "reason"): void;
/**
* The continuation prompt for a resumed run. The vendored harness only exposes
* `prompt(text)` (it always appends a user message — there is no "continue from tool results" entry), so
* resume re-enters via a `<system-reminder>`-wrapped user message that tells the model the gated decision
* was applied. The "user message wart" is accepted (API-valid, and clearer than a silent continuation).
* `fullShellReachable` is the resumed leg's own roster fact, read by {@link reorientClause}; `delegatedChild` is
* the leg's effective delegation fact, which picks the posture of a person's refusal of a tool use.
*/
export declare function resumeContinuation(resume: ResumeRun, mark: string | undefined, fullShellReachable: boolean, delegatedChild: boolean): string;