@sema-agent/core
Version:
Stateless, task-oriented AI agent core
293 lines (292 loc) • 13 kB
JavaScript
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,
};
}