UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

157 lines (156 loc) • 7.68 kB
import { isSystemInjectionPriority, SYSTEM_INJECTION_PRIORITIES } from "../task-notification.js"; export function streamLifecycleVerbs(input) { const { live, ready, orTimeout, steeringError, notifyRef, captureOptOutRef, mcpRedialRef, manualCompactRef, detachHub } = input; const notify = async (input, opts) => { const notifyError = (msg, code) => { const e = new Error(`cannot notify: ${msg}`); e.code = code; return e; }; if (typeof input?.task_id !== "string" || input.task_id.trim() === "") { throw notifyError("input.task_id must be a non-empty string", "notify.invalid_payload"); } const VALID_STATUSES = new Set(["completed", "failed", "killed", "cancelled", "event"]); if (!VALID_STATUSES.has(input.status)) { throw notifyError(`input.status must be one of ${[...VALID_STATUSES].join("/")}`, "notify.invalid_payload"); } if (typeof input.summary !== "string" || input.summary.trim() === "") { throw notifyError("input.summary must be a non-empty string", "notify.invalid_payload"); } if (input.result !== undefined && typeof input.result !== "string") { throw notifyError("input.result must be a string when present", "notify.invalid_payload"); } if (input.seq !== undefined && (typeof input.seq !== "number" || !Number.isFinite(input.seq))) { throw notifyError("input.seq must be a finite number when present", "notify.invalid_payload"); } if (input.source !== undefined && typeof input.source !== "string") { throw notifyError("input.source must be a string when present", "notify.invalid_payload"); } const priorityIn = opts?.priority; if (priorityIn !== undefined && !isSystemInjectionPriority(priorityIn)) { throw notifyError(`opts.priority must be one of ${SYSTEM_INJECTION_PRIORITIES.join("/")} when present`, "notify.invalid_payload"); } const priority = priorityIn; if (priority === "now") { throw notifyError('priority "now" (turn interrupt) is not a notification-lane power — it belongs to the steer face; use "next" for earliest-boundary delivery', "notify.invalid_priority"); } const payload = { task_id: input.task_id, task_type: "external", status: input.status, summary: input.summary, ...(input.result !== undefined ? { result: input.result } : {}), ...(input.seq !== undefined ? { seq: input.seq } : {}), ...(input.source !== undefined ? { source: input.source } : {}), }; const h = live.handle ?? (await orTimeout(ready)); if (!h || notifyRef.inject === undefined) { throw notifyError("the task is not running", "notify.not_running"); } notifyRef.inject(payload, priority !== undefined ? { priority } : undefined); }; const optOutMemoryCapture = async (options) => { const reasonIn = options?.reason; if (reasonIn !== undefined && typeof reasonIn !== "string") { const e = new Error(`cannot opt out of memory capture: options.reason must be a string when present`); e.code = "steering.invalid_content"; throw e; } const reason = reasonIn; if (live.resultValue) throw steeringError("the task has already finished"); const h = live.handle ?? (await orTimeout(ready)); if (!h) throw steeringError("the task is not running"); if (captureOptOutRef.flip === undefined) { const e = new Error(`cannot opt out of memory capture: this run mounted no memory session (no memory spec / no backend / prepare degraded memory-less) — there is nothing to opt out of`); e.code = "memory.capture_optout_unavailable"; throw e; } return captureOptOutRef.flip(reason); }; const reconnectMcpServer = async (server) => { const serverIn = server; if (typeof serverIn !== "string" || serverIn.length === 0) { const e = new Error(`cannot reconnect an MCP server: \`server\` must be a non-empty server name`); e.code = "steering.invalid_content"; throw e; } if (live.resultValue) throw steeringError("the task has already finished"); const h = live.handle ?? (await orTimeout(ready)); if (!h) throw steeringError("the task is not running"); if (live.resultValue !== undefined || h.loop.ended) throw steeringError("the task is no longer running"); if (mcpRedialRef.redial === undefined) { const e = new Error(`cannot reconnect an MCP server: this run declared no MCP servers — there is nothing to re-dial`); e.code = "mcp.reconnect_unavailable"; throw e; } return mcpRedialRef.redial(serverIn); }; const compact = async (opts) => { if (live.resultValue) throw steeringError("the task has already finished"); if (opts?.signal?.aborted) return "mooted"; const h = live.handle ?? (await orTimeout(ready)); if (!h) throw steeringError("the task is not running"); if (opts?.signal?.aborted) return "mooted"; if (manualCompactRef.closed) return "mooted"; return new Promise((resolve) => { const signal = opts?.signal; const entry = { resolve, ...(signal !== undefined ? { signal } : {}), ...(opts?.instructions !== undefined ? { instructions: opts.instructions } : {}), }; if (signal !== undefined) { const onAbort = () => { const i = manualCompactRef.waiters.indexOf(entry); if (i >= 0) { manualCompactRef.waiters.splice(i, 1); if (manualCompactRef.waiters.length === 0) { manualCompactRef.requested = false; } manualCompactRef.emitMooted?.("cancelled"); entry.resolve("mooted"); } }; entry.resolve = (outcome) => { signal.removeEventListener("abort", onAbort); resolve(outcome); }; signal.addEventListener("abort", onAbort, { once: true }); } manualCompactRef.requested = true; manualCompactRef.waiters.push(entry); }); }; const detach = (toolCallId) => { detachHub.request(toolCallId); }; const interrupt = async () => { const interruptRefused = (msg) => { const e = new Error(`cannot interrupt: ${msg}`); e.code = "steering.not_running"; return e; }; if (live.resultValue) throw interruptRefused("the task has already finished"); const h = live.handle ?? (await orTimeout(ready)); if (!h) throw interruptRefused("the task is not running"); if (live.resultValue !== undefined || h.loop.ended) throw interruptRefused("the task is no longer running"); if (!h.abortController.signal.aborted) h.loop.userInterrupted = true; h.abortController.abort(); void h.harness.abort(); }; return { notify, optOutMemoryCapture, reconnectMcpServer, compact, detach, interrupt }; }