UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

108 lines (107 loc) • 5.2 kB
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(), }, }; }