UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

115 lines (114 loc) • 5.58 kB
import { assertSupportedAgentIsolation } from "./workflow.js"; import { childClampSeats } 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, parentReadFace, parentWriteFace, parentReadDenyPatterns, parentHandsReadOnly, parentInteractiveToolsOff) { 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 pd = parentReadDenyPatterns?.(); const workflowChildClamps = childClampSeats({ handsReadOnly: parentHandsReadOnly, interactiveTools: parentInteractiveToolsOff === true ? false : undefined, readFace: parentReadFace?.() === "roots" ? "roots" : childSpec.readFace, writeFace: parentWriteFace?.() === "roots" ? "roots" : childSpec.writeFace, readDenyPatterns: pd !== undefined && pd.length > 0 ? [...(childSpec.readDenyPatterns ?? []), ...pd] : childSpec.readDenyPatterns, }); Object.assign(childSpec, workflowChildClamps); if (parentInteractiveToolsOff === true) { if (childSpec.interactionPosture === "interactive") delete childSpec.interactionPosture; } 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(), }, }; }