@sema-agent/core
Version:
Stateless, task-oriented AI agent core
89 lines (88 loc) • 5.32 kB
JavaScript
import { CheckpointError } from "../checkpoint-store.js";
import { isGateKind, PAUSE_REGISTRY, resumeGateMatches } from "../pause-registry.js";
import { sanitizeUntrustedText } from "../untrusted-text.js";
import { assertOutcomeText, describeSuppliedValue } from "./decide-continuation.js";
export function resumeReviewOutcome(input) {
const { cp, outcome, outcomeGate, wakeMessage } = input;
const gateMatch = isGateKind(cp.gate.kind) && resumeGateMatches(cp.gate.kind, outcomeGate);
if (!gateMatch) {
throw new CheckpointError("checkpoint.gate_mismatch", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") does not match checkpoint gate "${cp.gate.kind}" — resume serves ${Object.entries(PAUSE_REGISTRY)
.filter(([, row]) => !row.terminal)
.map(([k, row]) => `${k}/${row.resume}`)
.join(", ")}`);
}
let plainReviewOutcome;
if (cp.gate.kind === "plan_review") {
const decide = outcome;
const decision = decide.decision;
const editedPlan = decide.editedPlan;
const reason = decide.reason;
if (decision !== "approve" && decision !== "edit" && decision !== "reject") {
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the plan_review domain — a plan review is exactly "approve", "edit" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
}
assertOutcomeText(editedPlan, "editedPlan");
assertOutcomeText(reason, "reason");
plainReviewOutcome = {
gate: "plan_review",
decision,
...(editedPlan !== undefined ? { editedPlan } : {}),
...(reason !== undefined ? { reason } : {}),
};
}
else if (cp.gate.kind === "needs_review") {
const decide = outcome;
const decision = decide.decision;
const reason = decide.reason;
if (decision !== "approve" && decision !== "reject") {
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the dry_run_review domain — a dry-run review is exactly "approve" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
}
assertOutcomeText(reason, "reason");
plainReviewOutcome = {
gate: "dry_run_review",
decision,
...(reason !== undefined ? { reason } : {}),
};
}
if (plainReviewOutcome !== undefined) {
const reason = plainReviewOutcome.reason;
if (plainReviewOutcome.decision === "reject" && reason && sanitizeUntrustedText(reason) !== reason) {
throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
}
const capturedPlan = plainReviewOutcome.gate === "plan_review" ? plainReviewOutcome.editedPlan : undefined;
if (plainReviewOutcome.decision === "edit" &&
capturedPlan &&
sanitizeUntrustedText(capturedPlan) !== capturedPlan) {
throw new CheckpointError("checkpoint.invalid_outcome", "resume editedPlan must not contain a </system-reminder> tag");
}
if (cp.reopenReason === "env_failed") {
const winner = cp.resolvedOutcome;
if (winner === undefined) {
throw new CheckpointError("checkpoint.reopen_revote", "review checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
}
if (winner.decision !== plainReviewOutcome.decision) {
throw new CheckpointError("checkpoint.reopen_revote", `an env_failed reopen replays the ALREADY-RECORDED review decision ("${winner.decision}") — refusing a re-vote with a different decision ("${plainReviewOutcome.decision}")`);
}
if (winner.updatedInput !== capturedPlan) {
throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose edited plan differs from the recorded one");
}
if (winner.reason !== undefined && winner.reason !== plainReviewOutcome.reason) {
throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose reviewer note differs from the recorded one");
}
}
}
let plainParkOutcome;
if (outcomeGate === "wake") {
plainParkOutcome = {
gate: "wake",
...(wakeMessage !== undefined ? { message: wakeMessage } : {}),
};
}
else if (outcomeGate === "resource_limit") {
const decision = outcome.decision;
if (decision !== "continue") {
throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the resource_limit domain — a slice resume is exactly "continue"; refusing pre-CAS, the checkpoint stays pending`);
}
plainParkOutcome = { gate: "resource_limit", decision };
}
return { plainReviewOutcome, plainParkOutcome };
}