UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

151 lines (150 loc) • 7.11 kB
import { NAMESPACED_NAME_SHAPES, carriesNamespaceSeparator } from "../protocol-table.js"; import { engineCardTypes, handBandEffects } from "../tool-registry.js"; import { callerCardIdOf, offendingResultCard, rosterMemberBoundProblem, schemaKeyBoundProblem } from "../tool-roster.js"; import { toolFaceProblem } from "./tool-face-overlay.js"; import { OFFLOAD_TOOL_NAME } from "../tool-result-store.js"; import { PRESENT_PLAN_TOOL_NAME, ENTER_PLAN_MODE_TOOL_NAME } from "../present-plan-tool.js"; const REPEAT_RISK = { read: 0, idempotent: 1, write: 2 }; const leastRepeatSafe = (a, b) => (REPEAT_RISK[a] >= REPEAT_RISK[b] ? a : b); export function prepareSafetyScan(input) { const { spec, deps } = input; const toolEffects = new Map(); const ownToolNames = new Set((spec.tools ?? []).map((t) => t.name)); const claimedAliases = new Set(); const egressTools = new Set(); const irreversibleTools = new Set(); const irreversibilityTier = new Map(); const axisExplicitNegatives = new Map(); const reversibilityProbes = new Map(); const toolMarkSources = new Map(); const markedBy = (name, source) => { const set = toolMarkSources.get(name); if (set !== undefined) set.add(source); else toolMarkSources.set(name, new Set([source])); }; for (const t of spec.tools ?? []) { { const bound = rosterMemberBoundProblem({ name: t.name, aliases: t.aliases, ...(t.contract !== undefined ? { contractId: t.contract.contractId, implementationRevision: t.contract.implementationRevision } : {}), ...(typeof t.modelGate === "string" ? { modelGate: t.modelGate } : {}), }); if (bound !== undefined) { const e = new Error(`A caller tool cannot ride a roster row: ${bound.message}.`); e.code = bound.code; throw e; } } { const keyProblem = schemaKeyBoundProblem(t.parameters); if (keyProblem !== undefined) { const e = new Error(`Tool "${t.name}" cannot ride a roster row: ${keyProblem}.`); e.code = "config.tool_schema_bound"; throw e; } const declared = { ...(t.family !== undefined ? { family: t.family } : {}), ...(t.pathTarget !== undefined ? { pathTarget: t.pathTarget } : {}), ...(t.ruleFace !== undefined ? { ruleFace: t.ruleFace } : {}), ...(t.renderHints !== undefined ? { renderHints: t.renderHints } : {}), }; const faceProblem = toolFaceProblem(declared, t.parameters); if (faceProblem !== undefined) { const e = new Error(`Tool "${t.name}" declares a face its schema cannot carry: ${faceProblem}.`); e.code = "config.tool_face_invalid"; throw e; } const foreignCard = offendingResultCard(t.renderHints, callerCardIdOf(t), engineCardTypes()); if (foreignCard !== undefined) { const e = new Error(`Tool "${t.name}" declares a result card outside its own namespace: ${JSON.stringify(foreignCard)} (a caller card is "${callerCardIdOf(t)}/<card>"; engine cards and other contracts' namespaces are reserved).`); e.code = "config.tool_face_invalid"; throw e; } } if (carriesNamespaceSeparator(t.name)) { const e = new Error(`Tool name "${t.name}" is invalid: "__" is reserved for the engine's protocol tool namespaces (${NAMESPACED_NAME_SHAPES}) and must not appear in a caller tool name.`); e.code = "config.tool_name_invalid"; throw e; } for (const alias of t.aliases ?? []) { if (carriesNamespaceSeparator(alias)) { const e = new Error(`Tool alias "${alias}" (of "${t.name}") is invalid: "__" is reserved for the engine's protocol tool namespaces (${NAMESPACED_NAME_SHAPES}) and must not appear in a caller tool alias.`); e.code = "config.tool_name_invalid"; throw e; } } if (t.effect) { toolEffects.set(t.name, t.effect); } const tier = t.irreversibility ?? (t.reversibilityProbe ? "maybe" : undefined); if (tier !== undefined) { irreversibilityTier.set(t.name, tier); if (tier === "always" || tier === "maybe") { irreversibleTools.add(t.name); markedBy(t.name, "deployment"); } } if (t.reversibilityProbe) reversibilityProbes.set(t.name, t.reversibilityProbe); if (t.egress) { if (t.effect !== undefined && t.effect !== "write") { const e = new Error(`Tool "${t.name}" declares egress:true with effect:"${t.effect}" — an egress tool (external write) must have effect:"write" (or omit effect; write is the default).`); e.code = "config.egress_requires_write_effect"; throw e; } egressTools.add(t.name); markedBy(t.name, "deployment"); } for (const alias of t.aliases ?? []) { if (alias === t.name || ownToolNames.has(alias)) continue; if (claimedAliases.has(alias)) continue; claimedAliases.add(alias); if (t.effect) toolEffects.set(alias, t.effect); if (tier !== undefined) { irreversibilityTier.set(alias, tier); if (tier === "always" || tier === "maybe") { irreversibleTools.add(alias); markedBy(alias, "deployment"); } } if (t.reversibilityProbe) reversibilityProbes.set(alias, t.reversibilityProbe); if (t.egress) { egressTools.add(alias); markedBy(alias, "deployment"); } } } toolEffects.set(OFFLOAD_TOOL_NAME, "read"); const claimerEffects = new Map([...claimedAliases].map((a) => [a, toolEffects.get(a) ?? "write"])); const recordHandEffect = (name, effect) => { const claimed = claimerEffects.get(name); toolEffects.set(name, claimed === undefined ? effect : leastRepeatSafe(claimed, effect)); }; if (deps.executionEnvFactory || deps.executionEnv) { for (const [name, effect] of handBandEffects()) recordHandEffect(name, effect); if (spec.handsReadOnly === true) recordHandEffect("Bash", "read"); } if (spec.enablePlanMode === true) { toolEffects.set(PRESENT_PLAN_TOOL_NAME, "read"); toolEffects.set(ENTER_PLAN_MODE_TOOL_NAME, "read"); } return { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, toolMarkSources, ownToolNames, }; }