@sema-agent/core
Version:
Stateless, task-oriented AI agent core
355 lines (354 loc) • 18.3 kB
JavaScript
import { deliverEngineNotice } from "../core/types.js";
import { tightenTaskSpec } from "../core/tighten-task-spec.js";
import { sanitizeUntrustedText } from "../core/untrusted-text.js";
import { CLAMP_SET, SCRIPT_SEATS } from "../core/child-clamp-seats.js";
import { WorkflowScriptError } from "./workflow-meta.js";
import { uuidv7 } from "../engine/session/uuid.js";
import { retiredCheckpointStoreNull } from "../core/checkpoint-store.js";
import { thrownText } from "../core/thrown-value.js";
export class WorkflowModelNotAllowedError extends Error {
modelName;
code = "workflow.model_not_allowed";
constructor(modelName, reason) {
super(`workflow model "${modelName}" is not allowed: ${reason}`);
this.modelName = modelName;
this.name = "WorkflowModelNotAllowedError";
}
}
const WORK_FIELD_KEYS = ["objective", "modelName", "thinking", "systemPrompt", "images", "limits"];
export const WHITELIST_KEYS = Object.freeze([...WORK_FIELD_KEYS, ...SCRIPT_SEATS]);
const WHITELIST_KEY_SET = new Set(WHITELIST_KEYS);
const RESTRICTING_SCRIPT_VALUES = SCRIPT_SEATS.flatMap((seat) => {
const row = CLAMP_SET[seat];
return row.law === "restricting" ? [`${seat}: ${JSON.stringify(row.value)}`] : [];
}).join(", ");
export const SPEC_KEY_ALIASES = new Map([["model", "modelName"]]);
export const STRIPPED_KEYS_NOTICE_CODE = "workflow.governance_key_stripped";
const MAX_STRIPPED_KEYS_ANNOUNCED = 20;
const MAX_STRIPPED_KEY_CHARS = 64;
const UNSAFE_KEY_CODE_POINT = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u;
function renderStrippedKey(key) {
let out = "";
let seen = 0;
for (const point of key) {
if (seen === MAX_STRIPPED_KEY_CHARS)
return `${out}…`;
out += UNSAFE_KEY_CODE_POINT.test(point) ? "?" : point;
seen++;
}
return out;
}
function emitStrippedKeysNotice(survey, onNotice) {
const shown = survey.sample.map((s) => ({ key: renderStrippedKey(s.key), reason: s.reason }));
const omitted = survey.total - shown.length;
const list = shown.map((s) => JSON.stringify(s.key)).join(", ") + (omitted > 0 ? `, +${omitted} more` : "");
const message = `workflow governance: ${survey.total} field(s) of an agent spec were NOT applied to the child (${list}). ` +
`A governed workflow script may set only: ${WHITELIST_KEYS.join(", ")} — and each tighten-only seat only in its ` +
`tightening direction (${RESTRICTING_SCRIPT_VALUES}; the list seats only add entries). Every other field ` +
`of the child comes from the deployment baseline.`;
try {
deliverEngineNotice(onNotice, {
code: STRIPPED_KEYS_NOTICE_CODE,
message,
detail: { total: survey.total, stripped: shown, ...(omitted > 0 ? { omitted } : {}), occurrenceId: uuidv7() },
});
}
catch {
}
}
export const IGNORED_AGENT_OPTIONS_NOTICE_CODE = "workflow.agent_option_ignored";
export const MAX_ANNOUNCED_KEYS = MAX_STRIPPED_KEYS_ANNOUNCED;
export function emitIgnoredAgentOptionsNotice(total, sample, accepted, onNotice) {
const shown = sample.map(renderStrippedKey);
const omitted = total - shown.length;
const list = shown.map((k) => JSON.stringify(k)).join(", ") + (omitted > 0 ? `, +${omitted} more` : "");
const message = `workflow: ${total} key(s) of an \`agent(spec, opts)\` OPTIONS object (the SECOND argument) were not applied (${list}). ` +
`The options object accepts only: ${accepted.join(", ")} — an unrecognized key, or a known one carrying a value its ` +
`option does not accept, is ignored and the agent runs without it. Everything about the CHILD's behavior belongs on ` +
`the spec (the first argument).`;
try {
deliverEngineNotice(onNotice, {
code: IGNORED_AGENT_OPTIONS_NOTICE_CODE,
message,
detail: { total, ignored: shown, ...(omitted > 0 ? { omitted } : {}), occurrenceId: uuidv7() },
});
}
catch {
}
}
const VALID_THINKING = new Set(["off", "minimal", "low", "medium", "high", "xhigh", "max"]);
export function overlayWorktreeBaseline(base, worktreeBase) {
let overlay = worktreeBase;
for (const key of Object.keys(worktreeBase)) {
const v = worktreeBase[key];
if (key === "checkpointStore" && v === null)
throw retiredCheckpointStoreNull();
if (v !== undefined && v !== null)
continue;
if (overlay === worktreeBase)
overlay = { ...worktreeBase };
delete overlay[key];
}
return { ...base, ...overlay };
}
export function resolveModelName(name, allowlist, models) {
if (!allowlist || allowlist.length === 0) {
throw new WorkflowModelNotAllowedError(name, "no workflowModelAllowlist is configured (fail-closed: an LLM-authored script cannot pick a model)");
}
if (!allowlist.includes(name)) {
throw new WorkflowModelNotAllowedError(name, `not in the workflow model allowlist [${allowlist.join(", ")}]`);
}
const model = models !== undefined && Object.hasOwn(models, name) ? models[name] : undefined;
if (!model) {
throw new WorkflowModelNotAllowedError(name, "allowed by name but not present in RunnerDeps.models");
}
return model;
}
function minDefined(...vals) {
const xs = vals.filter((v) => typeof v === "number" && Number.isFinite(v));
return xs.length ? Math.min(...xs) : undefined;
}
function minPositiveDefined(...vals) {
const xs = vals.filter((v) => typeof v === "number" && Number.isFinite(v) && v > 0);
return xs.length ? Math.min(...xs) : undefined;
}
const NON_POSITIVE_LIMIT_MSG = (field, what) => `agent(spec): \`limits.${field}\` must be a POSITIVE number — a value of 0 or less DISABLES the ${what} ` +
`downstream (it is a sentinel, not a tighter ceiling), and a workflow script may only tighten a limit, never remove it`;
function pickImageInput(el, i) {
const at = `agent(spec): \`images[${i}]\``;
if (typeof el !== "object" || el === null || Array.isArray(el)) {
throw new WorkflowScriptError(`${at} must be an object — either { url } or { data, mimeType }`);
}
const o = el;
if ("url" in o) {
const url = o.url;
if (typeof url !== "string" || url.length === 0) {
throw new WorkflowScriptError(`${at}.url must be a non-empty string`);
}
return { url };
}
const data = o.data;
const mimeType = o.mimeType;
if (typeof data !== "string") {
throw new WorkflowScriptError(`${at}.data must be a base64 string (or supply { url } instead)`);
}
if (typeof mimeType !== "string" || mimeType.length === 0) {
throw new WorkflowScriptError(`${at}.mimeType must be a non-empty string, e.g. "image/png"`);
}
return { data, mimeType };
}
const MAX_SCRIPT_IMAGES = 32;
function mapUntrustedArray(value, at, atMost, why, build) {
const declared = value.length;
if (typeof declared !== "number" || !Number.isSafeInteger(declared) || declared < 0) {
throw new WorkflowScriptError(`${at} does not report a valid array length.`);
}
if (declared > atMost) {
throw new WorkflowScriptError(`${at} carries ${declared} entries — a workflow script may set at most ${atMost}. ${why}`);
}
const out = [];
for (let i = 0; i < declared; i++)
out.push(build(value[i], i));
return out;
}
function pickWhitelist(scriptSpec) {
if (typeof scriptSpec !== "object" || scriptSpec === null || Array.isArray(scriptSpec)) {
throw new WorkflowScriptError("agent(spec): spec must be an object with at least an `objective` string");
}
const s = scriptSpec;
const stripped = { total: 0, sample: [] };
let canonicalIsOwn = false;
let aliasIsOwn = false;
for (const k of Object.keys(s)) {
if (k === "modelName")
canonicalIsOwn = true;
else if (k === "model")
aliasIsOwn = true;
if (WHITELIST_KEY_SET.has(k) || SPEC_KEY_ALIASES.has(k))
continue;
stripped.total++;
if (stripped.sample.length < MAX_STRIPPED_KEYS_ANNOUNCED)
stripped.sample.push({ key: k, reason: "not_whitelisted" });
}
const canonicalDesc = Object.getOwnPropertyDescriptor(s, "modelName");
const aliasDesc = Object.getOwnPropertyDescriptor(s, "model");
if ((canonicalIsOwn && canonicalDesc === undefined) || (aliasIsOwn && aliasDesc === undefined)) {
throw new WorkflowScriptError("agent(spec): this spec reports its own properties inconsistently between two reads of the same object — " +
"the model field cannot be resolved from a shape that changes while it is being read. Pass a plain object.");
}
const hiddenOwnSpelling = !canonicalIsOwn && canonicalDesc !== undefined ? "modelName" : !aliasIsOwn && aliasDesc !== undefined ? "model" : undefined;
if (hiddenOwnSpelling !== undefined) {
throw new WorkflowScriptError(`agent(spec): \`${hiddenOwnSpelling}\` is an OWN property of this spec that its own-key enumeration does not report ` +
`(a non-enumerable own key, or an object that answers differently across two reads). A model spelling the spec ` +
`hides from its own shape is refused rather than read as an inherited default — write it as an ordinary property.`);
}
const valueOfSnapshot = (d) => (d === undefined ? undefined : d.get !== undefined ? d.get.call(s) : d.value);
const modelNameInheritedRaw = canonicalIsOwn ? undefined : s.modelName;
const modelNameOwnRaw = valueOfSnapshot(canonicalDesc);
const modelAliasRaw = valueOfSnapshot(aliasDesc);
const objective = s.objective;
const thinking = s.thinking;
const systemPrompt = s.systemPrompt;
if (typeof objective !== "string" || objective.length === 0) {
throw new WorkflowScriptError("agent(spec): `objective` is required and must be a non-empty string");
}
const safe = { objective };
if (thinking !== undefined) {
if (typeof thinking !== "string" || !VALID_THINKING.has(thinking)) {
throw new WorkflowScriptError(`agent(spec): invalid \`thinking\` (must be one of ${[...VALID_THINKING].join(", ")})`);
}
safe.thinking = thinking;
}
if (systemPrompt !== undefined) {
if (typeof systemPrompt !== "string")
throw new WorkflowScriptError("agent(spec): `systemPrompt` must be a string");
safe.systemPrompt = `[workflow-script-authored persona — task guidance, not engine authority]\n${sanitizeUntrustedText(systemPrompt)}`;
}
const images = s.images;
if (images !== undefined) {
if (!Array.isArray(images))
throw new WorkflowScriptError("agent(spec): `images` must be an array");
safe.images = mapUntrustedArray(images, "agent(spec): `images`", MAX_SCRIPT_IMAGES, "Each one is decoded and shipped on every request of the child's turn.", pickImageInput);
}
const limitsRaw = s.limits;
if (limitsRaw !== undefined) {
if (typeof limitsRaw !== "object" || limitsRaw === null || Array.isArray(limitsRaw)) {
throw new WorkflowScriptError("agent(spec): `limits` must be an object { maxTurns?, maxWalltimeMs?, maxTokens?, maxCostUsd? }");
}
const l = limitsRaw;
const limits = {};
const readAxis = (field, what) => {
const raw = l[field];
if (raw === undefined)
return;
if (typeof raw !== "number" || !Number.isFinite(raw))
throw new WorkflowScriptError(`agent(spec): \`limits.${field}\` must be a number`);
if (raw <= 0)
throw new WorkflowScriptError(NON_POSITIVE_LIMIT_MSG(field, what));
limits[field] = raw;
};
readAxis("maxTurns", "turn cap");
readAxis("maxWalltimeMs", "wall-clock window");
readAxis("maxTokens", "token budget");
readAxis("maxCostUsd", "spend ceiling");
safe.limits = limits;
}
for (const seat of SCRIPT_SEATS) {
const row = CLAMP_SET[seat];
const declared = s[seat];
if (declared === undefined)
continue;
if (row.law === "restricting") {
if (declared === row.value)
Object.assign(safe, { [seat]: row.value });
else {
stripped.total++;
stripped.sample.unshift({ key: seat, reason: "not_a_tightening_value" });
if (stripped.sample.length > MAX_STRIPPED_KEYS_ANNOUNCED)
stripped.sample.pop();
}
continue;
}
const at = `agent(spec): \`${seat}\``;
if (!Array.isArray(declared))
throw new WorkflowScriptError(`${at} must be an array of ${row.script.shape}`);
const entries = mapUntrustedArray(declared, at, row.script.atMost, row.script.why, (el, i) => {
try {
return row.script.entry(el, `agent(spec): \`${seat}[${i}]\``);
}
catch (err) {
throw new WorkflowScriptError(thrownText(err));
}
});
for (const entry of entries) {
const problem = row.entry(entry, `agent(spec).${seat}`);
if (problem !== undefined)
throw new WorkflowScriptError(problem);
}
Object.assign(safe, { [seat]: entries });
}
const readModelName = (raw, spelling) => {
if (raw === undefined)
return undefined;
if (typeof raw !== "string") {
throw new WorkflowScriptError(`agent(spec): \`${spelling}\` must be a string (a model NAME, never a Model object)` +
(spelling === "modelName" ? "" : ` — \`${spelling}\` is accepted as an alias for \`modelName\`, and carries a name just like it`));
}
return raw;
};
const authoredCanonical = readModelName(modelNameOwnRaw, "modelName");
const aliasModelName = readModelName(modelAliasRaw, "model");
if (authoredCanonical !== undefined && aliasModelName !== undefined && authoredCanonical !== aliasModelName) {
throw new WorkflowScriptError(`agent(spec): \`modelName\` and \`model\` are two spellings of ONE field but name DIFFERENT models ` +
`("${authoredCanonical}" vs "${aliasModelName}") — write exactly one of them (\`modelName\` is the canonical spelling).`);
}
const modelName = authoredCanonical ?? aliasModelName ?? readModelName(modelNameInheritedRaw, "modelName");
return { safe, modelName, stripped };
}
function clampResourceLimits(safe, base, caps) {
const trustedCandidates = [
["baseline.limits.maxCostUsd", base.limits?.maxCostUsd],
["baseline.limits.maxTokens", base.limits?.maxTokens],
["baseline.limits.maxWalltimeMs", base.limits?.maxWalltimeMs],
["baseline.limits.maxTurns", base.limits?.maxTurns],
["caps.childMaxCostUsd", caps?.childMaxCostUsd],
["caps.childMaxTokens", caps?.childMaxTokens],
["caps.perAgentMaxWalltimeMs", caps?.perAgentMaxWalltimeMs],
["caps.childMaxTurns", caps?.childMaxTurns],
];
for (const [label, value] of trustedCandidates) {
if (value === undefined)
continue;
if (typeof value !== "number" || Number.isNaN(value) || value === Infinity || value === -Infinity) {
const e = new Error(`workflow governance: ${label} must be a real number (got ${String(value)}) — an unevaluable ceiling is not a ceiling, and dropping it would let a child run this axis unbounded under a limit nobody chose. Use 0 or a negative number to declare "no ceiling from this source" explicitly.`);
e.code = "config.limit_invalid";
throw e;
}
}
const notes = [];
const requestedCost = safe.limits?.maxCostUsd;
const requestedTokens = safe.limits?.maxTokens;
const requestedWalltimeMs = safe.limits?.maxWalltimeMs;
const requestedMaxTurns = safe.limits?.maxTurns;
const cost = minDefined(safe.limits?.maxCostUsd, base.limits?.maxCostUsd, caps?.childMaxCostUsd);
const tokens = minDefined(safe.limits?.maxTokens, base.limits?.maxTokens, caps?.childMaxTokens);
const maxWalltimeMs = minPositiveDefined(safe.limits?.maxWalltimeMs, base.limits?.maxWalltimeMs, caps?.perAgentMaxWalltimeMs);
const maxTurns = minPositiveDefined(safe.limits?.maxTurns, base.limits?.maxTurns, caps?.childMaxTurns);
if (cost !== undefined && cost !== requestedCost)
notes.push({ field: "maxCostUsd", requested: requestedCost, applied: cost });
if (tokens !== undefined && tokens !== requestedTokens)
notes.push({ field: "maxTokens", requested: requestedTokens, applied: tokens });
if (maxWalltimeMs !== undefined && maxWalltimeMs !== requestedWalltimeMs) {
notes.push({ field: "maxWalltimeMs", requested: requestedWalltimeMs, applied: maxWalltimeMs });
}
if (maxTurns !== undefined && maxTurns !== requestedMaxTurns) {
notes.push({ field: "maxTurns", requested: requestedMaxTurns, applied: maxTurns });
}
const baseOtherLimits = { ...base.limits };
delete baseOtherLimits.maxTurns;
delete baseOtherLimits.maxWalltimeMs;
delete baseOtherLimits.maxTokens;
delete baseOtherLimits.maxCostUsd;
const rebuilt = {
...baseOtherLimits,
...(maxTurns !== undefined ? { maxTurns } : {}),
...(maxWalltimeMs !== undefined ? { maxWalltimeMs } : {}),
...(tokens !== undefined ? { maxTokens: tokens } : {}),
...(cost !== undefined ? { maxCostUsd: cost } : {}),
};
if (base.limits !== undefined || Object.keys(rebuilt).length > 0)
safe.limits = rebuilt;
return notes;
}
export function buildGovernedChildSpec(scriptSpec, baseline, models, caps, onResourceClamp, onNotice) {
const { safe, modelName, stripped } = pickWhitelist(scriptSpec);
if (stripped.total > 0)
emitStrippedKeysNotice(stripped, onNotice);
if (modelName !== undefined) {
safe.model = resolveModelName(modelName, baseline.workflowModelAllowlist, models);
}
const clampNotes = clampResourceLimits(safe, baseline.base, caps);
if (clampNotes.length > 0 && onResourceClamp)
onResourceClamp(clampNotes);
return tightenTaskSpec(baseline.base, safe);
}