@sema-agent/core
Version:
Stateless, task-oriented AI agent core
190 lines (189 loc) • 14.7 kB
JavaScript
import { snapshotActorAssertion } from "../../internal/llm.js";
import { isAskOrigin } from "../ask-origin.js";
import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js";
import { CheckpointError, remainingBudgetMicroUsd } from "../checkpoint-store.js";
import { settledByDecider } from "../gate-outcome.js";
import { mintHookInvocationIdentity, resolveHookTimeoutMs, runHookSeat } from "../hooks.js";
import { checkToolPolicyProjection, policyCannotAnswer, refuseOutOfContractDecision } from "../tool-policy.js";
import { callFaceSeat, parkedCallFaceOf } from "../tool-roster.js";
import { inlineUntrusted } from "../untrusted-text.js";
import { deepJsonEqual, describeSuppliedValue } from "./decide-continuation.js";
import { orgVerdictBeforeClaim } from "./permission-rule-lanes.js";
import { mintGateOutcome } from "./gate-exit.js";
import { chainCannotJudge } from "./call-hook-stations.js";
import { asThrownError, thrownText } from "../thrown-value.js";
import { wireOf } from "../tool-name-readings.js";
import { parkedSkillApprovalRefusal } from "../skill-grant.js";
import { SKILL_CONTENT_MAX_CHARS, SKILL_TOOL_NAME, normalizeSkills } from "./synthetic-tools.js";
import { skillLoadReading } from "./skill-base-dir.js";
export async function resumeApply(input) {
const { cp, config, internals, outcomeGate, wakeMessage, resumeSignal, recheckGovernanceWindow, plainReviewOutcome, plainParkOutcome, runner, effectiveDelegationFacts } = input;
let plainPolicyOutcome = input.plainPolicyOutcome;
let wakeHookContext;
if (outcomeGate === "wake" && wakeMessage !== undefined) {
const resumeScreenHooks = config.hooks ?? runner.deps.hooks;
const screen = resumeScreenHooks?.userPromptSubmit;
if (screen !== undefined && resumeSignal?.aborted !== true) {
const screenedMessage = wakeMessage;
const identity = mintHookInvocationIdentity({
sessionId: cp.sessionId,
taskId: config.taskId ?? cp.sessionId,
legKind: "resume",
isDelegatedChild: effectiveDelegationFacts(internals, cp.state.isDelegatedChild).isDelegatedChild,
...(internals?.insideFork === true ? { insideFork: true } : {}),
...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
...(internals?.explicitAgentName !== undefined ? { spawnName: internals.explicitAgentName } : {}),
...(internals?.spawnAgentType !== undefined ? { agentType: internals.spawnAgentType } : {}),
...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
});
const screenTimeoutMs = resolveHookTimeoutMs(resumeScreenHooks?.timeoutMs, (badErr) => {
try {
runner.deps.onError?.(asThrownError(badErr), { phase: "hook", sessionId: cp.sessionId });
}
catch {
}
}, resumeScreenHooks);
let decision;
try {
const seat = await runHookSeat("userPromptSubmit", { timeoutMs: screenTimeoutMs, ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}), abortEnds: true }, (sig) => screen(screenedMessage.text, {
identity,
signal: sig,
source: "resume_message",
...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}),
...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}),
}));
if (seat.expired) {
if (seat.cause === "aborted")
throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while the deployment's userPromptSubmit hook was still screening this wake message — the checkpoint stays pending and is resumable with a live signal");
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook did not answer within its ${screenTimeoutMs}ms bound while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`);
}
decision = seat.value;
}
catch (hookErr) {
if (hookErr instanceof CheckpointError)
throw hookErr;
const err = asThrownError(hookErr);
try {
runner.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
}
catch {
}
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook crashed while screening this wake message (${inlineUntrusted(err.message)}); the resume was refused (fail-closed) and the checkpoint stays pending`);
}
if (decision?.block !== undefined && decision.block !== "") {
throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(decision.block)}`);
}
if (decision?.additionalContext !== undefined && decision.additionalContext !== "") {
wakeHookContext = decision.additionalContext;
}
if (recheckGovernanceWindow !== undefined)
await recheckGovernanceWindow();
}
}
const editSignal = config.signal;
const raceEditAbort = async (p) => {
if (editSignal === undefined)
return p;
if (editSignal.aborted) {
throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while re-adjudicating the call before the claim — the checkpoint stays pending and is resumable with a live signal");
}
return new Promise((resolve, reject) => {
const onAbort = () => reject(new CheckpointError("checkpoint.resume_aborted", "the resume was aborted while re-adjudicating the call before the claim — the checkpoint stays pending and is resumable with a live signal"));
editSignal.addEventListener("abort", onAbort, { once: true });
p.then((v) => { editSignal.removeEventListener("abort", onAbort); resolve(v); }, (e) => { editSignal.removeEventListener("abort", onAbort); reject(e); });
});
};
if (plainPolicyOutcome?.decision === "allow" && cp.pendingAction.kind === "tool_approval") {
const servedSkills = normalizeSkills(config.skills).filter((s) => skillLoadReading(s, SKILL_CONTENT_MAX_CHARS).loads);
const skillRefusal = parkedSkillApprovalRefusal(cp.pendingAction, plainPolicyOutcome.updatedInput ?? cp.pendingAction.args, servedSkills, SKILL_TOOL_NAME);
if (skillRefusal !== undefined)
throw new CheckpointError("checkpoint.invalid_outcome", skillRefusal, { field: "boundGrantDigest" });
}
if (plainPolicyOutcome !== undefined &&
plainPolicyOutcome.decision === "allow" &&
plainPolicyOutcome.updatedInput !== undefined &&
cp.pendingAction.kind === "tool_approval" &&
wireOf("resume.editRecheckQuestionSkip", cp.pendingAction) !== ASK_USER_QUESTION_TOOL_NAME) {
const editedArgs = plainPolicyOutcome.updatedInput;
const editBudget = {
resourceRemainingMicroUsd: remainingBudgetMicroUsd(cp.resourceLedger),
resourceSpentMicroUsd: cp.resourceLedger?.spentMicroUsd ?? 0,
suspendCount: cp.suspendCount ?? 0,
};
const editedReq = {
toolName: cp.pendingAction.toolName,
args: editedArgs,
toolCallId: cp.pendingAction.toolCallId,
budget: editBudget,
...callFaceSeat(parkedCallFaceOf(cp.pendingAction)),
...(cp.state.handsCwd !== undefined ? { cwd: cp.state.handsCwd } : {}),
};
const ig = cp.state.inheritedGate;
if (ig?.requiresParentConstraint === true) {
const chain = ig.constraintChain;
const unprojectable = chainCannotJudge(ig);
if (unprojectable !== undefined) {
throw new CheckpointError("resume.constraint_unprojectable", `the approver EDITED this call's input, but the inherited ancestor chain cannot re-adjudicate the edit statically: ${unprojectable}. ` +
"Refusing the edited resume pre-CAS (the checkpoint stays pending and remains decidable) — approve the shown input as-is, deny it, " +
"or re-issue the edited action as a fresh call so the full live chain adjudicates it");
}
for (const entry of chain) {
const denied = checkToolPolicyProjection(entry, editedReq);
if (denied !== undefined) {
throw new CheckpointError("resume.constraint_rejected", `the approver EDITED this call's input and a FROZEN inherited ancestor constraint denies the edited args${denied.message ? `: ${denied.message}` : ""}. ` +
"Refused pre-CAS (fresh-redecision semantics: the checkpoint stays pending and the same token remains decidable — approve as shown, deny, or edit differently)");
}
}
}
const toolPolicyLocked = runner.deps.lockedConfig?.keys?.includes("toolPolicy") === true;
const editBasePolicy = toolPolicyLocked
? (runner.deps.basePolicyForResumeEdit ?? runner.deps.toolPolicy)
: (config.basePolicyForResumeEdit ?? runner.deps.basePolicyForResumeEdit ?? config.toolPolicy ?? runner.deps.toolPolicy);
if (editBasePolicy !== undefined) {
let rechecked;
try {
rechecked = refuseOutOfContractDecision(await raceEditAbort(Promise.resolve(editBasePolicy.check(editedReq, editSignal))));
}
catch (err) {
if (err instanceof CheckpointError)
throw err;
rechecked = { action: "deny", message: `the resume-edit policy re-check errored: ${thrownText(err)}` };
}
if (rechecked.action === "deny") {
throw new CheckpointError("resume.constraint_rejected", `the approver EDITED this call's input and the deployment's resume-edit policy denies the edited args${rechecked.message ? `: ${rechecked.message}` : ""}. ` +
"Refused pre-CAS (the checkpoint stays pending and remains decidable)");
}
if (policyCannotAnswer(rechecked, plainPolicyOutcome.hostDecision.decidedBy))
throw new CheckpointError("checkpoint.invalid_outcome", `the approver EDITED this call's input, the edited args raise a question that requires real approval, and the decision is the deployment's policy — a policy's answer is not a person's (decide the edit as "person", or approve the shown input); refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
if (rechecked.updatedInput !== undefined && !deepJsonEqual(rechecked.updatedInput, editedArgs)) {
throw new CheckpointError("resume.constraint_rejected", `the deployment's resume-edit policy REWROTE the approver's edited args to a different value than was approved — the durable resume cannot execute either the un-adjudicated edit or an unapproved rewrite; ` +
`refused pre-CAS (the checkpoint stays pending); re-submit the edited action so the rewrite is adjudicated and approved synchronously`);
}
}
}
const orgProvider = runner.deps.permissionRuleStore;
if (plainPolicyOutcome?.decision === "allow" && plainPolicyOutcome.hostDecision.decidedBy !== "person" && orgProvider?.partitions.org === true && cp.pendingAction.kind === "tool_approval") {
const call = { toolName: cp.pendingAction.toolName, args: plainPolicyOutcome.updatedInput !== undefined ? plainPolicyOutcome.updatedInput : cp.pendingAction.args, toolCallId: cp.pendingAction.toolCallId };
const orgNow = await raceEditAbort(orgVerdictBeforeClaim({ provider: orgProvider, principal: config.principal || cp.principal || undefined, localOwnerDeclared: runner.deps.localOwnerRules === true, sessionId: cp.sessionId, signal: editSignal }, call));
if (policyCannotAnswer(orgNow, plainPolicyOutcome.hostDecision.decidedBy)) {
const rule = orgNow.status === "available" ? orgNow.verdict?.rule : undefined;
throw new CheckpointError("checkpoint.invalid_outcome", `resume carries decision "allow" decided by "${plainPolicyOutcome.hostDecision.decidedBy}" on a call an organization policy rule (${rule}) requires a person's approval for — a policy's answer is not a person's (decide it as "person", or refuse it as "policy"); refusing pre-CAS, the checkpoint stays pending`, { field: "hostDecision" });
}
}
if (plainPolicyOutcome !== undefined) {
const parkedOrigin = cp.pendingAction.kind === "tool_approval" ? cp.pendingAction.origin : undefined;
if (!isAskOrigin(parkedOrigin)) {
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 facts = plainPolicyOutcome.hostDecision;
const settlement = facts.decidedBy === "sla_timeout"
? { kind: "park_sla_expired", who: { party: "host", ...(facts.approver !== undefined ? { approver: facts.approver } : {}) }, when: Date.now() }
: settledByDecider(facts.decidedBy, plainPolicyOutcome.decision === "allow" ? "allowed" : "refused", facts.approver, plainPolicyOutcome.decision === "deny" ? plainPolicyOutcome.reason : undefined);
plainPolicyOutcome = { ...plainPolicyOutcome, gateOutcome: mintGateOutcome({ ...(plainPolicyOutcome.decision === "deny" ? { deniedBy: "ask_resolution" } : {}), settled: { settlement, origin: parkedOrigin } }) };
}
const outcomeForStore = plainPolicyOutcome ?? plainReviewOutcome ?? plainParkOutcome;
if (outcomeForStore === undefined) {
throw new CheckpointError("checkpoint.invalid_outcome", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") matched the checkpoint gate but no lane captured it — refusing pre-CAS rather than passing the caller's live object to the store and the resumed run`);
}
return await input.next({ wakeHookContext, plainPolicyOutcome, outcomeForStore });
}