UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

293 lines (292 loc) • 13 kB
import { readMember, thrownText } from "./thrown-value.js"; export function modelIdTail(id) { return id.toLowerCase().split("/").pop() ?? ""; } export const TOOL_MODEL_GATE_CLASSES = Object.freeze({ "task-scaffold": Object.freeze({ floors: Object.freeze([ Object.freeze(["opus", Object.freeze([4, 8])]), Object.freeze(["sonnet", Object.freeze([5])]), Object.freeze(["fable", Object.freeze([5])]), Object.freeze(["mythos", Object.freeze([5])]), ]), }), }); const CLAUDE_ID_RE = /^claude-([a-z]+)-(\d+(?:-\d+)*)$/; function versionTupleGte(version, floor) { const n = Math.max(version.length, floor.length); for (let i = 0; i < n; i++) { const v = version[i] ?? 0; const f = floor[i] ?? 0; if (v > f) return true; if (v < f) return false; } return true; } export function isModelGatedForClass(modelId, rule) { const tail = modelIdTail(modelId); if (rule.modelIds !== undefined) { for (const row of rule.modelIds) { if (typeof row === "string" && tail === row.toLowerCase()) return true; } } if (rule.floors !== undefined) { const m = CLAUDE_ID_RE.exec(tail); if (m !== null) { const family = m[1] ?? ""; const version = (m[2] ?? "").split("-").map(Number); for (const row of rule.floors) { if (!Array.isArray(row) || row.length !== 2) continue; const [fam, floor] = row; if (typeof fam !== "string" || fam.toLowerCase() !== family) continue; if (Array.isArray(floor) && versionTupleGte(version, floor)) return true; } } } return false; } function gateConfigError(message) { const e = new Error(message); e.code = "config.tool_model_gate_invalid"; return e; } export function assertRestoreGatedToolsValue(value) { if (value === undefined || value === true) return; if (Array.isArray(value)) { const entries = value; for (const entry of entries) { if (typeof entry !== "string") { throw gateConfigError(`TaskSpec.restoreGatedTools entries must be strings (got ${describeValue(entry)}) — an unevaluable selector must not silently restore nothing.`); } } return; } throw gateConfigError(`TaskSpec.restoreGatedTools must be \`true\` or an array of tool names (got ${describeValue(value)}).`); } const isPlainObject = (v) => { if (typeof v !== "object" || v === null || Array.isArray(v)) return false; const proto = Object.getPrototypeOf(v); return proto === Object.prototype || proto === null; }; const describeValue = (v) => { try { const s = JSON.stringify(v); return s === undefined ? String(v) : s; } catch { return typeof v === "bigint" ? `${String(v)}n` : "[unserializable value]"; } }; function mergeToolModelGateClasses(depsSeat) { const merged = new Map(); for (const [cls, rule] of Object.entries(TOOL_MODEL_GATE_CLASSES)) merged.set(cls, rule); if (depsSeat === undefined) return merged; if (!isPlainObject(depsSeat)) { throw gateConfigError(`RunnerDeps.toolModelGate must be \`false\` or a plain object — prototype included (got ${Array.isArray(depsSeat) ? "an array" : describeValue(depsSeat)}).`); } for (const key of Object.keys(depsSeat)) { if (key !== "classes") { throw gateConfigError(`RunnerDeps.toolModelGate has unknown key ${JSON.stringify(key)} — the only legal key is "classes".`); } } const classes = Object.hasOwn(depsSeat, "classes") ? depsSeat.classes : undefined; if (classes === undefined) return merged; if (!isPlainObject(classes)) { throw gateConfigError(`RunnerDeps.toolModelGate.classes must be a plain object (got ${Array.isArray(classes) ? "an array" : describeValue(classes)}).`); } for (const [cls, row] of Object.entries(classes)) { if (cls === "") throw gateConfigError("RunnerDeps.toolModelGate.classes has an empty-string class name."); if (row === undefined) continue; if (!isPlainObject(row)) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}] must be a plain object (got ${Array.isArray(row) ? "an array" : describeValue(row)}).`); } for (const key of Object.keys(row)) { if (key !== "floors" && key !== "modelIds") { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}] has unknown key ${JSON.stringify(key)} — legal keys: "floors", "modelIds".`); } } let floors; if (row.floors !== undefined) { if (!Array.isArray(row.floors)) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors must be an array (got ${describeValue(row.floors)}).`); } const seen = new Set(); const out = []; const rows = row.floors; for (const entry of rows) { if (!Array.isArray(entry) || entry.length !== 2) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors rows must be [family, floor] pairs (got ${describeValue(entry)}).`); } const famRaw = entry[0]; const floorRaw = entry[1]; if (typeof famRaw !== "string" || famRaw.length === 0) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors family must be a non-empty string (got ${describeValue(famRaw)}).`); } const fam = famRaw.toLowerCase(); if (!/^[a-z]+$/.test(fam)) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors family ${JSON.stringify(famRaw)} cannot ever match — the canonical id shape only admits letters. Refused rather than kept as a dead row.`); } if (seen.has(fam)) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors lists family ${JSON.stringify(fam)} twice.`); } seen.add(fam); const floorNums = []; if (Array.isArray(floorRaw)) { const floorEntries = floorRaw; for (const n of floorEntries) { if (typeof n === "number" && Number.isSafeInteger(n) && n >= 0) floorNums.push(n); } } if (!Array.isArray(floorRaw) || floorRaw.length === 0 || floorNums.length !== floorRaw.length) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].floors floor for ${JSON.stringify(fam)} must be a non-empty array of non-negative safe integers (got ${describeValue(floorRaw)}).`); } out.push([fam, floorNums]); } floors = out; } let modelIds; if (row.modelIds !== undefined) { if (!Array.isArray(row.modelIds)) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].modelIds must be an array (got ${describeValue(row.modelIds)}).`); } const seen = new Set(); const out = []; const ids = row.modelIds; for (const entry of ids) { if (typeof entry !== "string" || entry.length === 0) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].modelIds entries must be non-empty strings (got ${describeValue(entry)}).`); } const folded = entry.toLowerCase(); if (folded.includes("/")) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].modelIds entry ${JSON.stringify(entry)} contains "/" — matching is against the id's TAIL segment, so a prefixed row can never fire. Write the bare id.`); } if (seen.has(folded)) { throw gateConfigError(`RunnerDeps.toolModelGate.classes[${JSON.stringify(cls)}].modelIds lists ${JSON.stringify(folded)} twice (case-folded).`); } seen.add(folded); out.push(folded); } modelIds = out; } const base = merged.get(cls); merged.set(cls, { ...(floors !== undefined ? { floors } : base?.floors !== undefined ? { floors: base.floors } : {}), ...(modelIds !== undefined ? { modelIds } : base?.modelIds !== undefined ? { modelIds: base.modelIds } : {}), }); } return merged; } const ENV_OFF = new Set(["off", "0", "false"]); const ENV_ON = new Set(["on", "1", "true"]); export function applyToolModelGate(input) { const noop = (discardedEnvRaw) => ({ survivors: undefined, removedByClass: new Map(), unknownClasses: [], discardedEnvRaw, }); let merged; try { if (input.depsSeat !== false && input.depsSeat !== undefined && !isPlainObject(input.depsSeat)) { throw gateConfigError(`RunnerDeps.toolModelGate must be \`false\` or a plain object — prototype included (got ${describeValue(input.depsSeat)}).`); } merged = input.depsSeat === false ? undefined : mergeToolModelGateClasses(input.depsSeat); } catch (e) { const code = readMember(e, "code"); if (code !== undefined) throw e; const msg = thrownText(e); throw gateConfigError(`RunnerDeps.toolModelGate could not be read (${msg}) — a config seat whose reads throw is a bad value, refused rather than passed.`); } const raw = input.envRaw; let envVerdict; if (raw === undefined) envVerdict = "absent"; else { const tok = raw.toLowerCase(); envVerdict = ENV_OFF.has(tok) ? "off" : ENV_ON.has(tok) ? "on" : "invalid"; } const invalidRaw = envVerdict === "invalid" ? raw : undefined; if (merged === undefined) return noop(invalidRaw); if (envVerdict === "off") return noop(); const tools = input.tools ?? []; const stamped = []; for (const entry of tools) { if (entry.modelGate !== undefined) stamped.push({ entry, cls: entry.modelGate }); } if (stamped.length === 0) return noop(invalidRaw); const restoredClasses = new Set(); if (input.restoreGated === true) { for (const s of stamped) restoredClasses.add(s.cls); } else if (input.restoreGated !== undefined) { const names = new Set(input.restoreGated); for (const s of stamped) if (names.has(s.entry.name)) restoredClasses.add(s.cls); } const unknownClasses = []; const unknownSeen = new Set(); const removed = new Set(); const removedByClass = new Map(); for (const { entry, cls } of stamped) { const rule = merged.get(cls); if (rule === undefined) { if (!unknownSeen.has(cls)) { unknownSeen.add(cls); unknownClasses.push(cls); } continue; } if (rule.floors === undefined && rule.modelIds === undefined) continue; if (restoredClasses.has(cls)) continue; if (!isModelGatedForClass(input.modelId, rule)) continue; removed.add(entry); const list = removedByClass.get(cls) ?? []; if (!list.includes(entry.name)) list.push(entry.name); removedByClass.set(cls, list); } if (invalidRaw !== undefined) { if (removed.size > 0) { const e = new Error(`SEMA_TOOL_MODEL_GATE=${JSON.stringify(invalidRaw)} is not in the closed set (on|1|true|off|0|false, case-insensitive) and IS in force on this task (the model gate would remove default-mounted tool(s)). Refused rather than guessed — "off" mistyped must not silently keep the trim armed.`); e.code = "config.tool_model_gate_env_invalid"; throw e; } return { survivors: undefined, removedByClass: new Map(), unknownClasses, discardedEnvRaw: invalidRaw }; } if (removed.size === 0) { return { survivors: undefined, removedByClass: new Map(), unknownClasses, discardedEnvRaw: undefined }; } for (const list of removedByClass.values()) list.sort(); return { survivors: tools.filter((t) => !removed.has(t)), removedByClass, unknownClasses, discardedEnvRaw: undefined, }; }