@sema-agent/core
Version:
Stateless, task-oriented AI agent core
98 lines (97 loc) • 7.53 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;
* · 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 OnQuestion, type QuestionAnswer } from "../ask-question.js";
import { type Checkpoint, type ResolvedOutcome } from "../checkpoint-store.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;
/**
* 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}.
*/
export declare function resumeContinuation(resume: ResumeRun, mark: string | undefined, fullShellReachable: boolean): string;