UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

98 lines (97 loc) • 7.53 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; * · 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;