@sema-agent/core
Version:
Stateless, task-oriented AI agent core
108 lines (107 loc) • 5.2 kB
JavaScript
import { assertSupportedAgentIsolation } from "./workflow.js";
import { childClampSeats, foldClampSources, postureUnderClamps } from "../core/child-clamp-seats.js";
import { WorkflowScriptError } from "./workflow-meta.js";
import { buildGovernedChildSpec, emitIgnoredAgentOptionsNotice, overlayWorktreeBaseline, MAX_ANNOUNCED_KEYS, SPEC_KEY_ALIASES, WHITELIST_KEYS, } from "./workflow-governance.js";
const ACCEPTED_AGENT_OPTION_KEYS = ["label", "phase", "schema", "agentType", "isolation"];
const ACCEPTED_AGENT_OPTION_KEY_SET = new Set(ACCEPTED_AGENT_OPTION_KEYS);
const SPEC_KEYS_MISPLACED_ON_OPTIONS = new Map([
...WHITELIST_KEYS.map((k) => [k, k]),
...SPEC_KEY_ALIASES,
["effort", "thinking"],
]);
function safeAgentOptions(opts, onNotice) {
if (typeof opts !== "object" || opts === null)
return {};
const o = opts;
const names = Object.keys(o);
for (const name of names) {
const spelling = SPEC_KEYS_MISPLACED_ON_OPTIONS.get(name);
if (spelling === undefined || o[name] === undefined)
continue;
throw new WorkflowScriptError(`agent(spec, opts): \`${name}\` is a SPEC field written on the OPTIONS object (the SECOND argument), where nothing ` +
`reads it — move it into the spec (the FIRST argument)${name === spelling ? "" : `, spelled \`${spelling}\``}. ` +
`The options object accepts only: ${ACCEPTED_AGENT_OPTION_KEYS.join(", ")}. The agent was NOT started: silently ` +
`ignoring \`${name}\` would have run a different child than the one asked for.`);
}
const out = {};
const rejected = new Set();
const label = o.label;
if (typeof label === "string")
out.label = label;
else if (label !== undefined)
rejected.add("label");
const phase = o.phase;
if (typeof phase === "string")
out.phase = phase;
else if (phase !== undefined)
rejected.add("phase");
const schema = o.schema;
if (schema !== undefined)
out.schema = schema;
const agentType = o.agentType;
if (typeof agentType === "string")
out.agentType = agentType;
else if (agentType !== undefined)
rejected.add("agentType");
const isolation = o.isolation;
assertSupportedAgentIsolation(isolation);
if (isolation === "worktree")
out.isolation = "worktree";
else if (isolation !== undefined)
rejected.add("isolation");
let total = 0;
const sample = [];
for (const name of names) {
if (ACCEPTED_AGENT_OPTION_KEY_SET.has(name) && !rejected.has(name))
continue;
total++;
if (sample.length < MAX_ANNOUNCED_KEYS)
sample.push(name);
}
if (total > 0)
emitIgnoredAgentOptionsNotice(total, sample, ACCEPTED_AGENT_OPTION_KEYS, onNotice);
return out;
}
function formatResourceClampNote(notes) {
const parts = notes.map((n) => `${n.field}: requested ${n.requested === undefined ? "unset" : n.requested} → applied ${n.applied}`);
return `workflow governance tightened this agent's resource limits (${parts.join("; ")})`;
}
export function buildWorkflowPrimitives(ctx, governance, onAgentSpawn, parentCheckpointStore, parentClamps) {
const agent = (spec, opts) => {
if (typeof spec === "string")
spec = { objective: spec };
const agentOpts = safeAgentOptions(opts, governance?.onNotice);
const effectiveBaseline = (b) => agentOpts.isolation === "worktree" && b.worktreeBase != null ? { ...b, base: overlayWorktreeBaseline(b.base, b.worktreeBase) } : b;
const childSpec = governance
? buildGovernedChildSpec(spec, effectiveBaseline(governance.baseline), governance.models, governance.caps, (notes) => ctx.log(formatResourceClampNote(notes)), governance.onNotice)
: { ...spec };
if (parentCheckpointStore !== undefined && (parentCheckpointStore === "disabled" || childSpec.checkpointStore === undefined)) {
childSpec.checkpointStore = parentCheckpointStore;
}
const hostClamps = parentClamps?.();
const workflowChildClamps = childClampSeats(foldClampSources(childSpec, hostClamps));
Object.assign(childSpec, workflowChildClamps);
const posture = postureUnderClamps(childSpec.interactionPosture, { interactiveTools: workflowChildClamps.interactiveTools, approvalReach: workflowChildClamps.approvalReach });
if (posture !== undefined)
childSpec.interactionPosture = posture;
if (onAgentSpawn) {
return ctx.agentStream(childSpec, agentOpts).then((handle) => {
onAgentSpawn(handle);
return handle.result();
});
}
return ctx.agent(childSpec, agentOpts);
};
return {
agent,
parallel: (thunks) => ctx.parallel(thunks),
pipeline: (items, ...stages) => ctx.pipeline(items, ...stages),
phase: (title, body) => ctx.phase(title, body),
log: (message) => ctx.log(message),
budget: {
total: ctx.budget.total,
spent: () => ctx.budget.spent(),
remaining: () => ctx.budget.remaining(),
},
};
}