UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

179 lines (178 loc) • 14 kB
import { snapshotActorAssertion } from "../../internal/llm.js"; import { ASK_USER_QUESTION_TOOL_NAME } from "../ask-question.js"; import { CheckpointError, remainingBudgetMicroUsd } from "../checkpoint-store.js"; import { chainPolicyOnLeg, chainTopAnchors } from "./child-tree.js"; import { mintHookInvocationIdentity } from "../hooks.js"; import { admitLegMods, classicEventBase, failedHandlerClause, screenUserPrompt } from "../mods/stations.js"; import { checkToolPolicyProjection, policyCannotAnswer, refuseOutOfContractDecision } from "../tool-policy.js"; import { callFaceSeat, parkedCallFaceOf } from "../tool-roster.js"; import { readCallTarget } from "../effective-path-target.js"; import { inlineUntrusted } from "../untrusted-text.js"; import { deepJsonEqual, describeSuppliedValue, mintDecideRecord } from "./decide-continuation.js"; import { orgVerdictBeforeClaim } from "./permission-rule-lanes.js"; import { chainCannotJudge } from "./call-hook-stations.js"; import { 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; let legMods; if (outcomeGate === "wake" && wakeMessage !== undefined) { if (resumeSignal?.aborted !== true) { legMods = await admitLegMods({ spec: config, deps: runner.deps, sessionId: cp.sessionId }); } if (legMods !== undefined && legMods.bus.hasHandlers("classic.UserPromptSubmit")) { 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 screen = await screenUserPrompt(legMods.bus, { ...classicEventBase(identity), prompt: screenedMessage.text, source: "resume_message", ...(screenedMessage.inputId !== undefined ? { inputId: screenedMessage.inputId } : {}), ...(screenedMessage.actor !== undefined ? { actor: snapshotActorAssertion(screenedMessage.actor) } : {}), }, resumeSignal); if (screen?.kind === "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"); } if (screen?.kind === "failed") { throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook ${failedHandlerClause(screen)} while screening this wake message; the resume was refused (fail-closed) and the checkpoint stays pending`); } if (screen?.block !== undefined && screen.block !== "") { throw new CheckpointError("steering.blocked_by_hook", `the deployment's userPromptSubmit hook blocked this wake message: ${inlineUntrusted(screen.block)}`); } if (screen?.additionalContext !== undefined && screen.additionalContext !== "") { wakeHookContext = screen.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 editedCall = { toolName: cp.pendingAction.toolName, args: editedArgs, toolCallId: cp.pendingAction.toolCallId, budget: editBudget, ...callFaceSeat(parkedCallFaceOf(cp.pendingAction)), ...(cp.state.handsCwd !== undefined ? { cwd: cp.state.handsCwd } : {}), ...(cp.state.handsRoot !== undefined ? { root: cp.state.handsRoot } : {}), }; const editedReq = { ...editedCall, targetReading: await readCallTarget(undefined, editedCall) }; 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); const rowLeg = { ...(typeof cp.state.handsRoot === "string" ? { root: cp.state.handsRoot } : {}), ...(typeof cp.state.handsHome === "string" ? { home: cp.state.handsHome } : {}) }; const asRecorded = (p) => p; const installedEditBase = chainPolicyOnLeg({ leg: rowLeg, chainTop: chainTopAnchors({ leg: rowLeg, live: internals?.inheritedGate?.pathRuleAnchors, seed: cp.state.inheritedGate?.pathRuleAnchors, placement: undefined, rebase: asRecorded }), placement: undefined, chain: (internals?.inheritedGate?.parentConstraints ?? []).map((c) => c.policy), installations: { live: internals?.inheritedGate?.pathRuleInstallations, seed: cp.state.inheritedGate?.pathRuleInstallations }, rebase: asRecorded, }).own(editBasePolicy); if (installedEditBase !== undefined) { let rechecked; try { rechecked = refuseOutOfContractDecision(await raceEditAbort(Promise.resolve(installedEditBase.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) { plainPolicyOutcome = { ...plainPolicyOutcome, gateOutcome: mintDecideRecord(cp, plainPolicyOutcome.hostDecision, { allowed: plainPolicyOutcome.decision === "allow", note: plainPolicyOutcome.reason }) }; } let outcomeForReview = plainReviewOutcome; if (outcomeForReview?.gate === "plan_review") { outcomeForReview = { ...outcomeForReview, gateOutcome: mintDecideRecord(cp, outcomeForReview.hostDecision, { allowed: outcomeForReview.decision !== "reject", note: outcomeForReview.reason }) }; } const outcomeForStore = plainPolicyOutcome ?? outcomeForReview ?? 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, ...(legMods !== undefined ? { legMods } : {}), plainPolicyOutcome, outcomeForStore }); }