@sema-agent/core
Version:
Stateless, task-oriented AI agent core
134 lines (133 loc) • 10.9 kB
JavaScript
import { canonicalizeCapturedPlainData, classifyQuestionOutcome, isLiveQuestionFace } from "../ask-question.js";
import { CHECKPOINT_FEATURES, CheckpointError, checkpointVersionOf, winnerFromOutcome } from "../checkpoint-store.js";
import { policyCannotAnswer, screenApproverAttribution } from "../tool-policy.js";
import { sanitizeUntrustedText } from "../untrusted-text.js";
import { assertOutcomeText, describeSuppliedValue, pendingContentAskCallId, sameWinner } from "./decide-continuation.js";
const REFUSED_DECIDE_KEYS = [
["gateOutcome", "the settlement record is minted by core from the host's decision facts and the row's origin, never supplied"],
["settledBy", 'a retired key — the host supplies decision FACTS as hostDecision{decidedBy: "person" | "sla_timeout" | "policy", approver?} and core mints the settlement word from them'],
["approver", "a retired key — the attribution rides the facts it belongs to, as hostDecision.approver"],
];
export function resumePolicyOutcome(input) {
const { cp, outcome, config, runner } = input;
let suppliedAnswer;
let redeemedAnswer;
let plainPolicyOutcome;
if (cp.gate.kind === "human" || cp.gate.kind === "irreversible_ask") {
const decide = outcome;
suppliedAnswer = decide.answer;
if (suppliedAnswer !== undefined) {
const reading = classifyQuestionOutcome(suppliedAnswer);
if (reading.shape !== "answered") {
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried a content-ask `answer` that does not read as an answer (malformed, sparse, or not capturable as plain data) — " +
"refusing pre-CAS rather than fabricating an empty human response; the checkpoint stays pending", { field: "answer" });
}
redeemedAnswer = reading.answer;
}
const updatedInputRaw = decide.updatedInput;
let updatedInput;
try {
updatedInput = updatedInputRaw === undefined ? undefined : structuredClone(updatedInputRaw);
}
catch {
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` that could not be captured as plain data — refusing pre-CAS; the checkpoint stays pending");
}
if (Object.is(updatedInput, -0))
updatedInput = 0;
if (updatedInput !== undefined && !canonicalizeCapturedPlainData(updatedInput)) {
throw new CheckpointError("checkpoint.invalid_outcome", "resume carried an `updatedInput` outside the plain-JSON value domain (a Map/Set/Date/buffer, a sparse or expando-carrying array, a non-finite number, or a cycle) — " +
"such a value reads differently on the two sides of the durable store; refusing pre-CAS, the checkpoint stays pending");
}
const decision = decide.decision;
if (decision !== "allow" && decision !== "deny") {
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the policy_ask domain — a decide is exactly "allow" or "deny"; refusing pre-CAS, the checkpoint stays pending`);
}
for (const [key, why] of REFUSED_DECIDE_KEYS) {
if (Reflect.get(decide, key) !== undefined) {
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries \`${key}\` — ${why}; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
}
}
const hostDecisionRaw = decide.hostDecision;
if (typeof hostDecisionRaw !== "object" || hostDecisionRaw === null) {
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries no hostDecision — a policy_ask decide names who decided it ({ decidedBy: "person" | "sla_timeout" | "policy", approver? }); refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
}
const decidedBy = hostDecisionRaw.decidedBy;
if (decidedBy !== "person" && decidedBy !== "sla_timeout" && decidedBy !== "policy") {
throw new CheckpointError("checkpoint.invalid_outcome", `resume hostDecision.decidedBy "${describeSuppliedValue(decidedBy)}" is outside the closed set — it is exactly "person", "sla_timeout" or "policy"; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
}
const cannotAllow = decision !== "allow"
? undefined
: decidedBy === "sla_timeout"
? ` — that word names a wait that ended with nobody answering, and it cannot approve an action that EXECUTES (an allow is decided by "person" or "policy")`
: policyCannotAnswer(cp, decidedBy)
? ` on a row that requires real approval — a policy's answer is not a person's, and this row demands a person (decide it as "person", or refuse it as "policy")`
: undefined;
if (cannotAllow !== undefined)
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" decided by "${decidedBy}"${cannotAllow}; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
const attribution = screenApproverAttribution(hostDecisionRaw.approver);
if (attribution.defect !== undefined) {
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries an approver attribution this engine refuses: ${attribution.defect}; refusing pre-CAS, the checkpoint stays pending`, { field: "approver" });
}
const reason = decide.reason;
assertOutcomeText(reason, "reason");
const approverCell = attribution.approver !== undefined ? { approver: attribution.approver } : {};
plainPolicyOutcome = {
gate: "policy_ask",
boundCallId: decide.boundCallId,
boundInputHash: decide.boundInputHash,
decision,
...(updatedInput !== undefined ? { updatedInput } : {}),
...(reason !== undefined ? { reason } : {}),
...(redeemedAnswer !== undefined ? { answer: redeemedAnswer } : {}),
hostDecision: { decidedBy, ...approverCell },
};
if (plainPolicyOutcome.decision === "deny" && plainPolicyOutcome.reason && sanitizeUntrustedText(plainPolicyOutcome.reason) !== plainPolicyOutcome.reason) {
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
}
}
if (plainPolicyOutcome !== undefined) {
const boundTo = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.toolCallId : undefined;
if (plainPolicyOutcome.boundCallId !== boundTo) {
throw new CheckpointError("checkpoint.invalid_outcome", `resume boundCallId "${plainPolicyOutcome.boundCallId}" does not match the checkpoint's pending tool call "${boundTo ?? "(none)"}" — the decision-action binding (design/80 D-1) failed; refusing to apply a decision bound to a different action`, { field: "boundCallId" });
}
const boundHash = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.boundInputHash : undefined;
if (boundHash !== undefined) {
if (plainPolicyOutcome.boundInputHash !== boundHash) {
throw new CheckpointError("checkpoint.invalid_outcome", "resume boundInputHash does not match the checkpoint's pending tool call input — the decision-action input binding (design/80 D-1 §2) failed; refusing to apply an approval bound to a different input (TOCTOU re-mint guard)", { field: "boundInputHash" });
}
}
else if (checkpointVersionOf(cp) >= CHECKPOINT_FEATURES.binding.version) {
throw new CheckpointError("checkpoint.invalid_outcome", `resume of a binding-version (v${checkpointVersionOf(cp)}) checkpoint whose pending tool call has no persisted boundInputHash — refusing to resume an unbound binding-version checkpoint (corruption / downgrade guard)`);
}
if (cp.reopenReason === "env_failed") {
if (cp.resolvedOutcome === undefined) {
throw new CheckpointError("checkpoint.reopen_revote", "checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
}
const incoming = winnerFromOutcome(plainPolicyOutcome);
const persistedWinner = (() => {
const w = cp.resolvedOutcome;
if (w?.answer === undefined)
return w;
const reading = classifyQuestionOutcome(w.answer);
return reading.shape === "answered" ? { ...w, answer: reading.answer } : w;
})();
if (incoming === undefined || !sameWinner(incoming, persistedWinner)) {
throw new CheckpointError("checkpoint.reopen_revote", `this checkpoint was reopened after an env-restore failure (env_failed) — a re-resume is a system retry of the already-approved action, not a re-vote; the supplied decision must replay the persisted winner (boundCallId "${cp.resolvedOutcome.boundCallId}", decision "${cp.resolvedOutcome.decision}"), refusing a different decision`);
}
}
const contentAskCallId = pendingContentAskCallId(cp);
if (suppliedAnswer !== undefined && (contentAskCallId === undefined || plainPolicyOutcome.decision !== "allow")) {
throw new CheckpointError("checkpoint.invalid_outcome", contentAskCallId === undefined
? "resume carried a content-ask `answer` but the checkpoint's pending call is not the reserved question tool — an answer has no consumer on a side-effecting tool's approval; refusing rather than dropping it silently"
: "resume carried a content-ask `answer` on a `deny` — a denial injects a refusal, never an answer; refusing rather than dropping it silently", { field: "answer" });
}
if (contentAskCallId !== undefined && plainPolicyOutcome.decision === "allow" && suppliedAnswer === undefined) {
if (!isLiveQuestionFace(config.onQuestion ?? runner.deps.onQuestion)) {
throw new CheckpointError("checkpoint.invalid_outcome", "resume approved a content-ask (the reserved question tool) without an `answer`, and this resume has no live answering face — " +
'executing the question against nothing would hand the model a fabricated "no human is available" default while consuming the approval; ' +
"re-resume with the operator's answer on the outcome (or deny it), the checkpoint stays pending", { field: "answer" });
}
}
}
return { plainPolicyOutcome, redeemedAnswer };
}