UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

184 lines (183 loc) • 11.2 kB
import { markTightenMessage } from "../safety-axis-vocab.js"; import { classifierParseFailureDenyMessage, createAutoModeDenialTracker, denialLimitFallbackMessage, denialLimitSentence, isAutoModeFallbackCause, rungDidNotRule, unarmedWindow } from "../auto-mode.js"; import { sanitizeAutoModeArmingRecipe, tightenDenialLimit } from "../auto-mode-arming.js"; import { classifierNoRulingArm, settleNoRulingAsk } from "../classifier-unavailable-arm.js"; import { attest, attestationOf } from "../tool-policy.js"; import { SETTLEMENT_NOBODY_JUDGED } from "../gate-outcome.js"; import { deliverEngineNotice } from "../types.js"; import { inlineUntrusted } from "../untrusted-text.js"; import { askClassMembersOf, askOriginOf, classifyAsk } from "../ask-origin.js"; export function attachRebuiltDenialTrackers(entries, denialLimit) { if (entries === undefined) return undefined; const needsTracker = (pc) => pc.autoMode !== undefined && pc.autoMode.denialTracking === undefined; const boundsFor = new Map(); const liveTrackers = new Map(); for (const pc of entries) { if (pc.autoMode?.denialTracking !== undefined) liveTrackers.set(pc.autoMode.decider, pc.autoMode.denialTracking); } for (const pc of entries) { if (!needsTracker(pc) || liveTrackers.has(pc.autoMode.decider)) continue; const recorded = sanitizeAutoModeArmingRecipe(pc.autoMode.arming); if (recorded === undefined) continue; const merged = tightenDenialLimit(recorded.denialLimit, denialLimit); const seen = boundsFor.get(pc.autoMode.decider); boundsFor.set(pc.autoMode.decider, seen === undefined ? merged : tightenDenialLimit(seen, merged)); } const rebuiltTrackers = new Map(liveTrackers); const trackerForRebuilt = (decider) => { let t = rebuiltTrackers.get(decider); if (t === undefined) { t = createAutoModeDenialTracker(boundsFor.get(decider) ?? denialLimit); rebuiltTrackers.set(decider, t); } return t; }; return entries.map((pc) => (needsTracker(pc) ? { ...pc, autoMode: { ...pc.autoMode, denialTracking: trackerForRebuilt(pc.autoMode.decider) } } : pc)); } export function createDenialLimitStop(opts) { const gateStopRef = {}; const stopForDenialLimit = (info) => { if (gateStopRef.terminal !== undefined) return; const lane = info.settlement === undefined ? `nobody in this run can be asked to answer it` : `the durable approval it parked was settled with nobody judging it ("${info.settlement}")`; const message = `too many classifier denials in headless mode — ${denialLimitSentence(info.fallback)} The run was stopped: ` + `the auto-mode classifier's denial limit falls back to a person, and ${lane} ` + `(latest blocked action: "${info.toolName}").`; const terminal = Object.assign(new Error(message), { code: "classifier.denial_limit" }); gateStopRef.terminal = terminal; deliverEngineNotice(opts.onNotice, { code: "classifier.denial_limit", message, detail: { sessionId: opts.sessionId, runId: opts.runId, toolName: info.toolName, toolCallId: info.toolCallId, consecutive: info.fallback.consecutive, total: info.fallback.total, limit: info.fallback.limit }, }); opts.abort(); }; return { gateStopRef, stopForDenialLimit }; } export const INHERITED_STATION_FACTS = { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined, mandateMarks: {} }; const noRulingCarry = (why) => (isAutoModeFallbackCause(why) ? { classifierNoRuling: why } : {}); function fallbackCarry(fallback, liveApprover, tracker) { if (fallback === undefined) return {}; return { denialLimitFallback: liveApprover && tracker !== undefined ? tracker.armTimedWindow(fallback) : unarmedWindow(fallback) }; } export function gateAskCarry(decision, liveApprover, tracker) { return { ...(decision.origin !== undefined ? { origin: decision.origin } : {}), ...(decision.askClass !== undefined && decision.classes !== undefined ? { askClass: decision.askClass, classes: decision.classes } : {}), ...fallbackCarry(decision.denialLimitFallback, liveApprover, tracker), ...noRulingCarry(decision.classifierNoRuling), }; } export function inheritedAskCarry(judged, liveApprover, tracker) { return { origin: judged.origin, ...askClassMembersOf(judged.classification), ...fallbackCarry(judged.fallback, liveApprover, tracker), ...noRulingCarry(judged.ask.classifierNoRuling), }; } export const inheritedAskFallbackMessage = (toolName) => `approval required for "${toolName}" (inherited parent policy)`; export async function judgeInheritedClassifier(opts) { const { autoMode, req } = opts; const facts = opts.facts ?? INHERITED_STATION_FACTS, m = facts.mandateMarks; const frames = [...(m.egress === true ? [markTightenMessage(req.toolName, "egress")] : []), ...(m.irreversibility === "always" ? [markTightenMessage(req.toolName, "irreversible")] : [])]; const ask = (frames.length > 0 ? { ...opts.ask, message: [opts.ask.message ?? inheritedAskFallbackMessage(req.toolName), ...frames].join(" — ") } : opts.ask); const origin = askOriginOf(ask, facts); const classification = classifyAsk(ask, facts); const classifierStandsDown = () => { const live = opts.liveMode?.(); return live === "dontAsk" || live === "bypassPermissions" || live === "plan" || (autoMode?.liveMode !== undefined && autoMode.liveMode.current !== "auto"); }; if (autoMode === undefined || classifierStandsDown() || !classification.row.classifierEligible) { return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification }; } const verdict = await autoMode.decider .decide({ req, ...(ask.message !== undefined ? { askMessage: ask.message } : {}) }, opts.signal) .catch(() => ({ kind: "unavailable", cause: "error" })); if (verdict.kind === "allow" && classifierStandsDown()) return { kind: "resolve", ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification }; const attributed = (d) => (verdict.classifier !== undefined ? attest(d, req, { classifierRound: verdict.classifier }) : d); if (verdict.kind === "allow") { autoMode.denialTracking?.recordAllow(); return { kind: "allow", result: attributed({ action: "allow", decisionReason: "classifier", ...(opts.allowRewrite !== undefined ? { updatedInput: opts.allowRewrite } : {}) }) }; } const noRuling = rungDidNotRule(verdict); const arm = noRuling !== undefined ? classifierNoRulingArm({ ask, req, cause: noRuling, seat: opts.unavailableSeat, aborted: opts.signal.aborted }) : undefined; if (arm?.kind === "asked") return { kind: "resolve", ask: arm.ask, fallback: ask.denialLimitFallback, mintedHere: false, origin, classification: classifyAsk(arm.ask, facts), noRulingHere: true }; if (arm?.kind === "denied") return { kind: "deny", result: attributed(arm.deny) }; const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier); const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : ""; const tracked = autoMode.denialTracking?.recordBlock(); if (tracked?.limitReached !== true) { const denied = { action: "deny", message: verdict.kind === "block" ? `auto-mode classifier blocked ${opts.subject} at an inherited ancestor layer${reason ? `: ${reason}` : category ? `: [${category}]` : ""}` : `${reason} (${opts.subject}, at an inherited ancestor layer)`, decisionReason: "classifier", }; return { kind: "deny", result: attributed(verdict.kind === "parse_error" ? attest(denied, req, { classifierCause: "parse_error" }) : denied) }; } return inheritedFallbackJudgment(ask, tracked.fallback, reason || category || req.toolName, facts); } export function inheritedFallbackJudgment(ask, fallback, latestBlockedAction, facts) { const { classifierNoRuling: _why, ...standing } = ask; const minted = { ...standing, message: denialLimitFallbackMessage(fallback, latestBlockedAction), decisionReason: "classifier", requiresRealApproval: true, denialLimitFallback: fallback }; return { kind: "resolve", ask: minted, fallback, mintedHere: true, origin: askOriginOf(minted, facts), classification: classifyAsk(minted, facts) }; } export async function resolveInheritedAsk(judged, resolveWith, tracker, call, facts) { const resolved = await resolveWith(judged); const refallback = settleNoRulingAsk({ ask: judged.ask, mintedHere: judged.noRulingHere === true, resolved: fallbackResolutionOf(resolved, call), tracker }); if (refallback === undefined) return { resolved, judged }; const again = inheritedFallbackJudgment(judged.ask, refallback, call.toolName, facts); return { resolved: await resolveWith(again), judged: again }; } export const fallbackResolutionOf = (r, call) => ({ action: r.action, settlement: attestationOf(r, call)?.settlement, ...(r.approverUnavailable === true ? { approverUnavailable: true } : {}) }); export function headlessDenyAtFold(r) { return r.settlement !== undefined && SETTLEMENT_NOBODY_JUDGED[r.settlement.kind]; } export function headlessDenyAtRecheck(r) { return headlessDenyAtFold(r) || r.settlement?.kind === "approver_unavailable" || r.approverUnavailable === true; } export function settleDenialLimitFallback(opts) { const { fallback } = opts; if (fallback === undefined) return; const resolved = fallbackResolutionOf(opts.resolved, opts); if (resolved.action === "allow") { if (opts.mintedHere) opts.tracker?.recordAllow(); } else if (resolved.action === "deny" && opts.headless(resolved)) { opts.stop({ toolName: opts.toolName, toolCallId: opts.toolCallId, fallback }); } } export function lateAskSettlementObserver(args) { return (late) => { if (late.kind === "approve") { deliverEngineNotice(args.onNotice, { code: "task.late_approval", message: `an approval for "${args.toolName}" was not consumed: a run or turn interrupt released the ask wait, ` + `so the tool did NOT run and the approval was not honored (an unconsumed answer is the approver ` + `releasing its wait, never a verdict).`, detail: { toolName: args.toolName, toolCallId: args.toolCallId, sessionId: args.sessionId, runId: args.runId, ...(args.taskId !== undefined ? { taskId: args.taskId } : {}), }, }); return; } args.onError?.(late.error, { phase: "hook", sessionId: args.sessionId }); }; }