UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

146 lines (145 loc) • 11.4 kB
/** * 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;