UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

190 lines (189 loc) • 14.7 kB
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 }); }