UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

89 lines (88 loc) • 5.32 kB
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 }; }