UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

187 lines (186 loc) • 12.8 kB
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); }