UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

452 lines (451 loc) • 25.1 kB
import { resolveBrainCallGuardrailMs } from "../../brain/timeout.js"; import { assertContainmentFaceValue } from "../../tools/fs/index.js"; import { MICRO_COMPACT_MACHINES } from "../context-edit.js"; import { assertNoRetiredRunnerDeps } from "./retired-runner-deps.js"; import { resolveCheckpointStore } from "../checkpoint-store.js"; import { assertAutoConsolidationWirable } from "./memory-consolidation-driver.js"; import { isModelPriced, malformedPricingField, modelCostToPricing } from "../pricing.js"; import { preflightLockedConfig } from "../locked-config.js"; import { assertRetentionCapability } from "../retention.js"; import { resolveModel, resolveTaskModel, roleModelIfSet } from "../roles.js"; import { resolveSkillListingBudget } from "../skill-listing-budget.js"; import { describeRejectedValue } from "../tool-search-setting.js"; import { applyToolModelGate, assertRestoreGatedToolsValue, modelIdTail } from "../tool-model-gate.js"; import { resolveUsageWindows } from "../usage-window-store.js"; import { deriveAskEffective, resolveAskSeamForm, resolveQuestionSeam } from "../wiring-manifest.js"; import { assertPeerPresenceValue } from "../../agents/peer-directory.js"; import { TOTAL_TOKENS_REMINDER_MODES } from "./turn-attachments.js"; const TASK_LIMIT_KEY_DICT = { maxTokens: true, maxCostUsd: true, maxTurns: true, maxWalltimeMs: true, maxOutputTokens: true, approachNotice: true, budgetStreamCancel: true, degrade: true, brainCallGuardrailMs: true, }; const TASK_LIMIT_KEYS = Object.keys(TASK_LIMIT_KEY_DICT); const NUMERIC_TASK_LIMIT_KEYS = ["maxTokens", "maxCostUsd", "maxTurns", "maxWalltimeMs", "maxOutputTokens"]; const RETIRED_TASK_LIMIT_KEYS = { timeoutSec: "maxWalltimeMs (milliseconds, not seconds)", deadlineNudge: "approachNotice", callCapByDeadline: "(retired — no replacement)", gracefulFinalize: "(retired — the approach notice is the only end-of-budget prompt)", }; export function limitConfigError(code, message) { const e = new Error(message); e.code = code; return e; } export function resolveTaskLimits(limits) { if (limits === undefined) return undefined; if (typeof limits !== "object" || Array.isArray(limits)) { throw limitConfigError("config.limit_invalid", `TaskSpec.limits must be an object (got ${Array.isArray(limits) ? "an array" : typeof limits})`); } const raw = limits; const legal = TASK_LIMIT_KEYS; for (const key of Object.keys(raw)) { if (legal.includes(key)) continue; const replacement = RETIRED_TASK_LIMIT_KEYS[key]; throw limitConfigError("config.limit_unknown_key", `TaskSpec.limits.${key} is not a limit this engine reads` + (replacement !== undefined ? ` — it was replaced by \`${replacement}\`` : "") + `. Legal keys: ${TASK_LIMIT_KEYS.join(", ")}. Refused rather than ignored: a limit that is silently dropped reads to the caller as an armed ceiling.`); } for (const key of NUMERIC_TASK_LIMIT_KEYS) { const value = raw[key]; if (value === undefined) continue; if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { throw limitConfigError("config.limit_invalid", `TaskSpec.limits.${key} must be a finite, non-negative number (got ${String(value)}) — an unevaluable ceiling is not a ceiling, and folding it to a default would run the task under a limit nobody chose.`); } } const notice = raw.approachNotice; if (notice !== undefined && notice !== false) { if (typeof notice !== "object" || notice === null || Array.isArray(notice)) { throw limitConfigError("config.limit_invalid", `TaskSpec.limits.approachNotice must be \`false\` or { at?: [first, second] } (got ${String(notice)})`); } const at = notice.at; if (at !== undefined) { const ok = Array.isArray(at) && at.length === 2 && at.every((n) => typeof n === "number" && Number.isFinite(n) && n > 0 && n <= 1) && at[0] <= at[1]; if (!ok) { throw limitConfigError("config.limit_invalid", `TaskSpec.limits.approachNotice.at must be two fractions in (0, 1] with first <= second (got ${JSON.stringify(at)})`); } } } return limits; } export function resolveAttachmentsConfig(attachments) { if (attachments === null || (typeof attachments !== "object" && typeof attachments !== "function")) return attachments; const mode = attachments.totalTokensReminderMode; if (mode !== undefined) { const legal = TOTAL_TOKENS_REMINDER_MODES; if (typeof mode !== "string" || !legal.includes(mode)) { throw limitConfigError("config.attachment_invalid", `TaskSpec.attachments.totalTokensReminderMode must be one of ${TOTAL_TOKENS_REMINDER_MODES.map((m) => `"${m}"`).join(", ")} ` + `(got ${describeRejectedValue(mode)}) — refused rather than folded to the default, which would publish a different readout than the one the deployment asked for.`); } } return attachments; } export function isFableFamilyModelId(id) { const tail = modelIdTail(id); return /^claude-fable-\d/.test(tail) || /^claude-mythos-5(?!\d)/.test(tail); } export function resolveModelPromptTraits(model, spec, internals) { return { promptProfile: spec.promptProfile ?? internals?.promptProfile ?? "simple", fableMitigations: isFableFamilyModelId(model.id), }; } function microCompactConfigError(field, value, legal) { let shown; try { shown = JSON.stringify(value) ?? String(value); } catch { try { shown = String(value); } catch { shown = `[unrepresentable ${typeof value}]`; } } const seat = field === undefined ? "microCompact" : `microCompact.${field}`; const e = new Error(`${seat} ${shown} is not ${legal} — an unevaluable declaration is refused loudly, ` + `never folded to the default: a silently-ignored declaration would run the DEFAULT machine ` + `while the deployment believes its opt-in or opt-out took effect.`); e.code = "config.microcompact_invalid"; return e; } export function resolveMicroCompactKnob(bag) { if (bag !== undefined && (typeof bag !== "object" || bag === null || Array.isArray(bag))) { throw microCompactConfigError(undefined, bag, "an object carrying optional machine/clearOnRejection keys"); } const declaredMachine = bag?.machine; if (declaredMachine !== undefined && !MICRO_COMPACT_MACHINES.includes(declaredMachine)) { throw microCompactConfigError("machine", declaredMachine, MICRO_COMPACT_MACHINES.map((m) => `"${m}"`).join(" | ")); } return { machine: declaredMachine ?? "cc", clearOnRejection: resolveClearOnRejection(bag) }; } function resolveClearOnRejection(bag) { const declared = bag?.clearOnRejection; if (declared !== undefined && typeof declared !== "boolean") { throw microCompactConfigError("clearOnRejection", declared, "a boolean"); } return declared ?? true; } export function prepareConfigDoors(input) { const { deps, sessions, resume, internals } = input; let spec = input.spec; assertNoRetiredRunnerDeps(deps); assertPeerPresenceValue(spec.peerPresence); assertRestoreGatedToolsValue(spec.restoreGatedTools); const microCompactKnob = resolveMicroCompactKnob(deps.microCompact); const assertToolNameListValue = (value, seat) => { if (value === undefined) return; if (!Array.isArray(value)) { const e = new Error(`TaskSpec.${seat} must be an array of tool names or absent (got ${value === null ? "null" : typeof value}) — a garbage list must refuse loudly, never be read as "no ${seat}".`); e.code = "config.tool_face_control"; throw e; } const entries = value; for (const entry of entries) { if (typeof entry !== "string") { const e = new Error(`TaskSpec.${seat} entries must be strings (got ${entry === null ? "null" : typeof entry}).`); e.code = "config.tool_face_control"; throw e; } } }; assertToolNameListValue(spec.excludeTools, "excludeTools"); assertToolNameListValue(spec.deferTools, "deferTools"); assertToolNameListValue(spec.alwaysLoadTools, "alwaysLoadTools"); const toolFaceSnapshot = { exclude: spec.excludeTools ? Object.freeze([...spec.excludeTools]) : undefined, defer: spec.deferTools ? Object.freeze([...spec.deferTools]) : undefined, alwaysLoad: spec.alwaysLoadTools ? Object.freeze([...spec.alwaysLoadTools]) : undefined, restoreGated: spec.restoreGatedTools === true ? true : spec.restoreGatedTools !== undefined ? Object.freeze([...spec.restoreGatedTools]) : undefined, }; const promptProfile = resolveModelPromptTraits({ id: "" }, spec, internals).promptProfile; if (spec.resumeAt !== undefined) { if (resume) { const e = new Error("resumeAt cannot be combined with a durable resume"); e.code = "resume_at.conflicts_resume"; throw e; } if (!spec.sessionId) { const e = new Error("resumeAt requires a sessionId (the session to branch)"); e.code = "resume_at.no_session"; throw e; } } assertContainmentFaceValue(spec.readFace, "read", "spec"); assertContainmentFaceValue(deps.readFace, "read", "deps"); assertContainmentFaceValue(spec.writeFace, "write", "spec"); assertContainmentFaceValue(deps.writeFace, "write", "deps"); const memoryDelegationEvidenceRaw = deps.memoryDelegationEvidence; if (memoryDelegationEvidenceRaw !== undefined && memoryDelegationEvidenceRaw !== "static-face" && memoryDelegationEvidenceRaw !== "attested-only") { const evidence = memoryDelegationEvidenceRaw; const got = typeof evidence === "string" ? JSON.stringify(evidence.length > 64 ? `${evidence.slice(0, 64)}…` : evidence) : evidence === null ? "null" : Array.isArray(evidence) ? "an array" : typeof evidence; const e = new Error(`RunnerDeps.memoryDelegationEvidence must be "static-face" or "attested-only" when present (got ${got}) — ` + `an unevaluable evidence standard is refused loudly, never folded to either standard.`); e.code = "config.memory_delegation_evidence"; throw e; } const memoryDelegationEvidence = memoryDelegationEvidenceRaw === "attested-only" ? "attested-only" : "static-face"; const memoryProvenanceRaw = deps.memoryProvenance; if (memoryProvenanceRaw !== undefined && memoryProvenanceRaw !== "off" && memoryProvenanceRaw !== "carry") { const got = typeof memoryProvenanceRaw === "string" ? JSON.stringify(memoryProvenanceRaw.length > 64 ? `${memoryProvenanceRaw.slice(0, 64)}…` : memoryProvenanceRaw) : memoryProvenanceRaw === null ? "null" : Array.isArray(memoryProvenanceRaw) ? "an array" : typeof memoryProvenanceRaw; const e = new Error(`RunnerDeps.memoryProvenance must be "off" or "carry" when present (got ${got}) — ` + `an unevaluable provenance mode is refused loudly, never folded to either mode.`); e.code = "config.memory_provenance"; throw e; } const memoryProvenance = memoryProvenanceRaw === "off" ? "off" : "carry"; const memoryCapturePolicyRaw = deps.memoryCapturePolicy; if (memoryCapturePolicyRaw !== undefined && memoryCapturePolicyRaw !== "open" && memoryCapturePolicyRaw !== "governed" && memoryCapturePolicyRaw !== "capture-required") { const got = typeof memoryCapturePolicyRaw === "string" ? JSON.stringify(memoryCapturePolicyRaw.length > 64 ? `${memoryCapturePolicyRaw.slice(0, 64)}…` : memoryCapturePolicyRaw) : memoryCapturePolicyRaw === null ? "null" : Array.isArray(memoryCapturePolicyRaw) ? "an array" : typeof memoryCapturePolicyRaw; const e = new Error(`RunnerDeps.memoryCapturePolicy must be "open", "governed" or "capture-required" when present (got ${got}) — ` + `an unevaluable capture posture is refused loudly, never folded to any posture (the fault arms differ per posture, so a guess would pick a polarity nobody configured).`); e.code = "config.memory_capture_policy"; throw e; } const memoryCapturePolicy = memoryCapturePolicyRaw === "governed" ? "governed" : memoryCapturePolicyRaw === "capture-required" ? "capture-required" : "open"; assertAutoConsolidationWirable(deps); if (spec.resumeAtMode !== undefined) { if (spec.resumeAt === undefined) { const e = new Error(`resumeAtMode "${spec.resumeAtMode}" requires resumeAt (there is no branch target to position against)`); e.code = "resume_at.mode_without_target"; throw e; } if (spec.resumeAtMode !== "at" && spec.resumeAtMode !== "before") { const e = new Error(`resumeAtMode "${String(spec.resumeAtMode)}" is invalid — expected "at" (inclusive branch, the default) or "before" (exclusive branch)`); e.code = "resume_at.invalid_mode"; throw e; } } if (spec.toolMaterializeStrategy !== undefined && spec.toolMaterializeStrategy !== "swap" && spec.toolMaterializeStrategy !== "static") { const e = new Error(`toolMaterializeStrategy must be "swap" or "static" (got ${JSON.stringify(spec.toolMaterializeStrategy)}).`); e.code = "config.tool_materialize_invalid"; throw e; } if (spec.memoryPersistenceCapable !== undefined && typeof spec.memoryPersistenceCapable !== "boolean") { const e = new Error(`memoryPersistenceCapable must be a boolean when present (got ${JSON.stringify(spec.memoryPersistenceCapable)}) — a non-boolean would silently read as capable.`); e.code = "config.memory_persistence_invalid"; throw e; } if (spec.toolMaterializeStrategy === "static" && spec.deferSelfResolve === false) { const e = new Error(`toolMaterializeStrategy "static" cannot be combined with deferSelfResolve: false — with the direct-call ` + `lane disabled a placeholder is never swapped and never self-resolves, so no deferred tool could ever be ` + `called. Use "swap", or leave deferSelfResolve on.`); e.code = "config.tool_materialize_unreachable"; throw e; } if (resume === undefined && spec.objective.trim().length === 0) { const e = new Error("TaskSpec.objective is empty — a task needs an instruction (an empty user message is rejected by strict model endpoints and would fail every later request of the session)."); e.code = "config.empty_objective"; throw e; } const lockedPreflight = preflightLockedConfig(spec, deps); assertRetentionCapability({ policy: deps.retentionPolicy, locked: lockedPreflight.lockedKeys.has("retentionPolicy"), stores: [ { name: "sessionStore", store: sessions }, { name: "checkpointStore", store: resolveCheckpointStore(spec, deps) }, { name: "toolResultStore", store: deps.toolResultStore }, ], }); const resolvedInteractionPosture = spec.interactionPosture ?? internals?.parentInteractionPosture ?? deps.interactionPosture; { const interactionPosture = resolvedInteractionPosture; const discloseInteractionPostureRefusal = (err) => { try { deps.onError?.(err, { phase: "config", sessionId: spec.sessionId ?? "(pre-session)", classification: "interaction-posture-refused" }); } catch { } }; if (interactionPosture !== undefined && interactionPosture !== "interactive" && interactionPosture !== "headless") { const e = new Error(`interactionPosture ${JSON.stringify(interactionPosture)} is not a recognized posture ("interactive" | "headless") — ` + `an unevaluable declaration is refused loudly, never folded to either posture.`); e.code = "config.interaction_posture"; discloseInteractionPostureRefusal(e); throw e; } if (interactionPosture === "interactive") { const askDoor = resolveAskSeamForm(spec, deps); const humanReachable = deriveAskEffective(askDoor.form, "unresolved") === "human_reachable"; const questionDoor = resolveQuestionSeam(spec, deps); const strippedByEngine = internals?.questionFaceStripped === true; if (spec.interactiveTools === false) { const e = new Error(`interaction posture "interactive" declared together with interactiveTools: false — the hard-headless ` + `clamp removes the AskUserQuestion mount, so no content question can ever reach the human this posture ` + `promises. Drop one of the two declarations.`); e.code = "config.interaction_posture"; discloseInteractionPostureRefusal(e); throw e; } if (!humanReachable || !(questionDoor.wired || strippedByEngine)) { const missing = []; if (!humanReachable) { missing.push(askDoor.form === "absent" ? "no onAsk approver is wired (spec.onAsk ?? deps.onAsk is absent — asks would auto-deny or park)" : `the resolved onAsk seat is the blanket policy "${askDoor.form}" (${askDoor.provenance ?? "?"}) — a policy setting is not a reachable human`); } if (!questionDoor.wired && !strippedByEngine) { missing.push("no content-question channel (spec.onQuestion ?? deps.onQuestion is absent, and this leg is not an engine-stripped background lane)"); } const e = new Error(`interaction posture "interactive" declared, but this assembly cannot reach a human: ${missing.join("; ")}. ` + `Wire the missing seam(s), or drop the posture declaration (absent = no check).`); e.code = "config.interaction_posture"; discloseInteractionPostureRefusal(e); throw e; } } } resolveTaskLimits(spec.limits); resolveAttachmentsConfig(spec.attachments); if (spec.resourceSuspend !== undefined) { const rsus = spec.resourceSuspend; if (typeof rsus.scope !== "string" || rsus.scope === "") { throw limitConfigError("config.limit_invalid", `TaskSpec.resourceSuspend.scope must be a non-empty string (got ${String(rsus.scope)}) — it is the multi-tenant isolation key every resource checkpoint is filed under.`); } for (const key of ["totalBudgetUsd", "totalTokens", "maxSlices", "ttlMs"]) { const value = rsus[key]; if (value === undefined) continue; if (typeof value !== "number" || !Number.isFinite(value) || value < 0) { throw limitConfigError("config.limit_invalid", `TaskSpec.resourceSuspend.${key} must be a finite, non-negative number (got ${String(value)}) — an unevaluable allocation is not an allocation, and a NaN here blinds even the validated per-slice window.`); } } } const usageWindows = resolveUsageWindows(deps.usageWindows); const brainCallGuardrailRef = {}; const brainCallGuardrailMs = resolveBrainCallGuardrailMs(spec.limits?.brainCallGuardrailMs ?? deps.brainCallGuardrailMs); if (spec.agents !== undefined && spec.agents.length > 0) { const pool = spec.tools ?? []; if (!pool.some((t) => typeof t.withAgents === "function")) { const e = new Error(`TaskSpec.agents was provided but no delegation tool (createSubagentTool) is mounted in spec.tools — the per-task agents could never be offered. Mount the delegation tool, or drop spec.agents.`); e.code = "config.agents.no_delegation_tool"; throw e; } const perTask = spec.agents; spec = { ...spec, tools: pool.map((t) => { if (typeof t.withAgents !== "function") return t; const rebuilt = t.withAgents(perTask); return t.modelGate !== undefined && rebuilt.modelGate === undefined ? { ...rebuilt, modelGate: t.modelGate } : rebuilt; }), }; } const resolvedRoleRaw = resolveTaskModel(spec, deps); const resolvedRole = internals?.inheritedModelRef !== undefined ? { ...resolvedRoleRaw, modelRef: internals.inheritedModelRef } : resolvedRoleRaw; const model = resolvedRole.model; if (usageWindows !== undefined && deps.usageWindowStore !== undefined) { const costed = usageWindows.find((w) => w.maxCostUsd !== undefined); const why = costed === undefined ? undefined : !isModelPriced(model, deps.pricing) ? `model ${JSON.stringify(model.id)} is unpriced — it has neither a RunnerDeps.pricing entry nor a Model.cost declaration, so this run produces NO cost figure to charge the window with (an unpriced run has no cost, which is not the same as a cost of 0). Price the model, or drop maxCostUsd from the window.` : (() => { const bad = malformedPricingField(deps.pricing?.[model.id] ?? modelCostToPricing(model.cost)); return bad === undefined ? undefined : `the price table for model ${JSON.stringify(model.id)} declares an unevaluable ${bad} — every rate must be a finite, non-negative number of USD per 1M tokens. A table like this prices every turn at zero (or below), so the ceiling would never fill: refused rather than enforced in name only.`; })(); if (costed !== undefined && why !== undefined) { const e = new Error(`RunnerDeps.usageWindows declares a cost ceiling (maxCostUsd ${String(costed.maxCostUsd)} over ${String(costed.windowMs)}ms, ${costed.anchor}) but ${why}`); e.code = "config.usage_window_unpriced"; throw e; } } const dollarCeiling = spec.limits?.maxCostUsd !== undefined ? "limits.maxCostUsd" : spec.resourceSuspend?.totalBudgetUsd !== undefined ? "resourceSuspend.totalBudgetUsd" : undefined; if (dollarCeiling !== undefined && isModelPriced(model, deps.pricing)) { const bad = malformedPricingField(deps.pricing?.[model.id] ?? modelCostToPricing(model.cost)); if (bad !== undefined) { throw limitConfigError("config.limit_invalid", `TaskSpec.${dollarCeiling} is a money ceiling, but the price table for model ${JSON.stringify(model.id)} declares an unevaluable ${bad} — every rate must be a finite, non-negative number of USD per 1M tokens. A table like this prices every turn at zero (or below), so the ceiling would never trip: refused rather than enforced in name only.`); } } const fableMitigations = resolveModelPromptTraits(model, spec, internals).fableMitigations; const gateDecision = applyToolModelGate({ tools: spec.tools, modelId: model.id, depsSeat: deps.toolModelGate, restoreGated: toolFaceSnapshot.restoreGated, envRaw: process.env.SEMA_TOOL_MODEL_GATE, }); if (gateDecision.survivors !== undefined) { spec = { ...spec, tools: gateDecision.survivors }; } const skillListingBudget = resolveSkillListingBudget({ fraction: spec.skillListingBudgetFraction, maxDescChars: spec.skillListingMaxDescChars, model, }); const modelGate = Object.freeze({ removedByClass: gateDecision.removedByClass, unknownClasses: gateDecision.unknownClasses, discardedEnvRaw: gateDecision.discardedEnvRaw, }); const thinking = spec.thinking ?? resolvedRole.thinking ?? model.defaultThinking; const compModel = spec.compactionModel ? resolveModel(spec.compactionModel, deps.models) : roleModelIfSet("summarize", spec, deps); return { spec, toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, microCompactKnob, resolvedRole, model, thinking, compModel, fableMitigations, modelGate, memoryDelegationEvidence, skillListingBudget, memoryProvenance, memoryCapturePolicy, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs, }; }