@sema-agent/core
Version:
Stateless, task-oriented AI agent core
187 lines (186 loc) • 12.8 kB
JavaScript
import { isAskOrigin } from "../ask-origin.js";
import { isLiveQuestionFace, markBoundOnlyQuestionFace } from "../ask-question.js";
import { boundInputHashOf } from "../canonical-json.js";
import { CheckpointError } from "../checkpoint-store.js";
import { decidersNote, isHostDecider, settlementOfHostDecision } from "../gate-outcome.js";
import { formatHookFeedback } from "../hooks.js";
import { facetOf, isGateKind, isQuestionGate } from "../pause-registry.js";
import { humanRefusalSentence, policyCannotAnswer, screenApproverAttribution } from "../tool-policy.js";
import { mintGateOutcome } from "./gate-exit.js";
import { delimitUntrusted, REVIEWER_NOTE_MAX_BODY } from "../untrusted-text.js";
import { engineCardOf } from "../tool-roster.js";
export function resumeDecisionWasNegative(resume) {
const o = resume.outcome;
return o.decision === "reject" || o.decision === "deny";
}
export function deepJsonEqual(a, b) {
if (a === b)
return true;
if (typeof a !== typeof b || a === null || b === null || typeof a !== "object") {
return false;
}
const aArr = Array.isArray(a);
const bArr = Array.isArray(b);
if (aArr !== bArr)
return false;
if (aArr && bArr) {
if (a.length !== b.length)
return false;
for (let i = 0; i < a.length; i++) {
if (!deepJsonEqual(a[i], b[i]))
return false;
}
const aExtra = Object.keys(a).filter((k) => !isCanonicalIndexKey(k, a.length));
const bExtra = Object.keys(b).filter((k) => !isCanonicalIndexKey(k, b.length));
if (aExtra.length !== bExtra.length)
return false;
return aExtra.every((k) => Object.prototype.hasOwnProperty.call(b, k) && deepJsonEqual(Reflect.get(a, k), Reflect.get(b, k)));
}
const ao = a;
const bo = b;
const aKeys = Object.keys(ao);
const bKeys = Object.keys(bo);
if (aKeys.length !== bKeys.length)
return false;
return aKeys.every((k) => Object.prototype.hasOwnProperty.call(bo, k) && deepJsonEqual(ao[k], bo[k]));
}
function isCanonicalIndexKey(key, length) {
return /^(0|[1-9]\d*)$/.test(key) && Number(key) < length;
}
export function sameWinner(incoming, persisted) {
return (incoming.boundCallId === persisted.boundCallId &&
incoming.decision === persisted.decision &&
deepJsonEqual(incoming.updatedInput, persisted.updatedInput) &&
deepJsonEqual(incoming.answer, persisted.answer) &&
(persisted.reason === undefined || incoming.reason === persisted.reason));
}
export function pendingContentAskCallId(cp) {
return cp.pendingAction.kind === "tool_approval" && engineCardOf(cp.pendingAction) === "question" ? cp.pendingAction.toolCallId : undefined;
}
export function answerFaceForRedeemedCall(answer, redeemedCallId, base, redeemedQuestionsHash) {
let consumed = false;
const face = async (req, signal) => {
if (!consumed && req.toolCallId === redeemedCallId && boundInputHashOf(req.questions) === redeemedQuestionsHash) {
consumed = true;
return answer;
}
if (base !== undefined)
return base(req, signal);
throw new Error("this resumed leg's answer was bound to the decided question only — a new question has no answer on this leg");
};
return isLiveQuestionFace(base) ? face : markBoundOnlyQuestionFace(face);
}
export function describeSuppliedValue(value) {
return typeof value === "string" ? value : value === null ? "null" : typeof value;
}
export const SUPPLIED_RECORD_KEY = ["gateOutcome", "the settlement record is minted by core from the host's decision facts and the row's origin, never supplied"];
export function refuseSuppliedKeys(decide, keys) {
for (const [key, why] of 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" });
}
}
}
export function captureHostDecision(cp, raw, lane, verdict) {
if (typeof raw !== "object" || raw === null) {
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries no hostDecision — a ${lane} decide names who decided it ({ decidedBy: "person" | "sla_timeout" | "policy", approver? }); refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
}
const decidedBy = raw.decidedBy;
if (!isHostDecider(decidedBy)) {
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 gateIsQuestion = isGateKind(cp.gate.kind) && isQuestionGate(cp.gate);
if (decidedBy === "sla_timeout" && !(isGateKind(cp.gate.kind) && facetOf(cp.gate) === "A")) {
throw new CheckpointError("checkpoint.invalid_outcome", `resume hostDecision.decidedBy "sla_timeout" names the host's SLA sweep, which resolves approval rows only — this ${gateIsQuestion ? "question" : `"${String(cp.gate.kind)}"`} row's deadline is an abandonment TTL (expired, never resolved); refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
}
const cannotAllow = !verdict.allows
? 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 "${verdict.word}" decided by "${decidedBy}"${cannotAllow}; refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
const attribution = screenApproverAttribution(raw.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" });
}
return { decidedBy, ...(attribution.approver !== undefined ? { approver: attribution.approver } : {}) };
}
export function parkedOriginOf(cp) {
if (cp.pendingAction.kind === "tool_approval")
return isAskOrigin(cp.pendingAction.origin) ? cp.pendingAction.origin : undefined;
return cp.pendingAction.kind === "plan_review" ? "plan_review" : undefined;
}
export function mintDecideRecord(cp, facts, verdict) {
const origin = parkedOriginOf(cp);
if (origin === undefined) {
throw new CheckpointError("checkpoint.unsupported_version", "this checkpoint's pending approval carries no origin word (a row minted before the ask's origin was persisted on the park) — its settlement cannot be recorded; the checkpoint stays pending", { reason: "origin_missing" });
}
const settlement = settlementOfHostDecision(facts, verdict.allowed ? "allowed" : "refused", verdict.note);
return mintGateOutcome({ ...(verdict.allowed ? {} : { deniedBy: "ask_resolution" }), settled: { settlement, origin } });
}
export function assertOutcomeText(value, field) {
if (value !== undefined && typeof value !== "string") {
throw new CheckpointError("checkpoint.invalid_outcome", `resume \`${field}\` is not a plain string (got ${typeof value}) — a decide's text payload is an operator's plain data, not a live object; refusing pre-CAS, the checkpoint stays pending`);
}
}
function reorientClause(fullShellReachable) {
const source = fullShellReachable
? "re-orient from the workspace: run `git status` and review your recent changes / last commits"
: "re-orient from the conversation: re-read your most recent messages and tool results";
return `Before continuing, ${source} to confirm what is already done, then continue the remaining work. ` +
"Do NOT restart the task or re-run work that is already committed.";
}
export function resumeContinuation(resume, mark, fullShellReachable, delegatedChild) {
if (resume.outcome.gate === "wake") {
return formatHookFeedback(`You were WOKEN from a parked pause by an operator. ${reorientClause(fullShellReachable)}`, mark);
}
if (resume.outcome.gate === "resource_limit") {
return formatHookFeedback(`You were resumed after a pause. ${reorientClause(fullShellReachable)}`, mark);
}
if (resume.outcome.gate === "dry_run_review") {
const verdict = resume.outcome.decision === "approve"
? "Your predicted change was REVIEWED and APPROVED; it has been applied"
: `Your predicted change was REVIEWED and REJECTED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}; it was NOT applied`;
return formatHookFeedback(`You were resumed after a dry-run review. ${verdict}. ${reorientClause(fullShellReachable)}`, mark);
}
if (resume.outcome.gate === "plan_review") {
const byWhom = resume.outcome.hostDecision.decidedBy === "policy" ? " by this deployment's own policy, not by a person" : "";
if (resume.outcome.decision === "approve") {
return formatHookFeedback(`Your proposed PLAN was REVIEWED and APPROVED${byWhom}. Proceed with that plan now — begin executing it. ` +
"This is a RESUMED task; do NOT re-plan or restart from scratch, just carry out the approved plan.", mark);
}
if (resume.outcome.decision === "edit") {
if (!resume.outcome.editedPlan) {
return formatHookFeedback(`Your proposed PLAN was REVIEWED and EDITED${byWhom}, but no revised plan text was supplied — proceed with your ` +
"ORIGINAL plan as-is, begin executing it now. This is a RESUMED task; do NOT re-plan or restart from scratch.", mark);
}
return formatHookFeedback(`Your proposed PLAN was REVIEWED and EDITED${byWhom || " by a human reviewer"}. Proceed with the REVISED plan below ` +
"(it supersedes your earlier plan); begin executing it now. This is a RESUMED task; do NOT re-plan " +
"or restart from scratch, just carry out the revised plan.\n\nThe REVISED plan to follow is:\n" +
delimitUntrusted("revised plan", resume.outcome.editedPlan), mark);
}
const { reason } = resume.outcome;
const policyNote = decidersNote(reason);
return formatHookFeedback(resume.outcome.hostDecision.decidedBy === "person"
? `${humanRefusalSentence("plan", reason, delegatedChild)} This is a RESUMED task; do NOT execute the rejected plan.`
: `Your proposed PLAN was REVIEWED and REJECTED${byWhom}; it was NOT executed.${policyNote === undefined ? "" : ` Reviewer note: ${delimitUntrusted("reviewer note", policyNote, REVIEWER_NOTE_MAX_BODY)}`} ` +
"This is a RESUMED task — re-plan from the current state; do NOT execute the rejected plan.", mark);
}
const { pendingAction } = resume.cp;
if (pendingAction.kind !== "tool_approval") {
return formatHookFeedback("This is a RESUMED task — continue from where you left off without restarting or re-running prior work.", mark);
}
const { decision, hostDecision } = resume.outcome;
const resumed = "This is a RESUMED task — every step before this point is already done and its results are in the conversation above. Do NOT restart the task or re-run any tool you already ran";
const carryOn = "; continue from this exact point, building on the existing results, and finish the remaining work.";
if (decision === "deny" && hostDecision.decidedBy === "person") {
return formatHookFeedback(`${humanRefusalSentence("tool use", resume.outcome.reason, delegatedChild)} ${resumed}${delegatedChild ? carryOn : "."}`, mark);
}
const decided = decision === "allow"
? "was APPROVED and has now been executed — its result is in the tool results above"
: `was DENIED ${hostDecision.decidedBy === "policy" ? "by this deployment's own policy" : "because its approval window elapsed with no answer"}, not by a person — the refusal and its reason are in the tool results above`;
return formatHookFeedback(`The pending tool call "${pendingAction.toolName}" ${decided}. ${resumed}${carryOn}`, mark);
}