@sema-agent/core
Version:
Stateless, task-oriented AI agent core
157 lines (156 loc) • 7.68 kB
JavaScript
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 };
}