UNPKG

@sema-agent/core

Version:

Stateless, task-oriented AI agent core

83 lines (82 loc) • 3.62 kB
import { deliverEngineNotice } from "../types.js"; export function streamHaltVerbs(input) { const { spec, live, ready, orTimeout, readyTimeoutMs: READY_TIMEOUT_MS, run, reapSuspended, runner } = input; let destroyOnce; const halt = async () => { const haltRefused = (msg) => { const e = new Error(`cannot halt: ${msg}`); e.code = "steering.not_running"; return e; }; if (live.resultValue) throw haltRefused("the task has already finished"); const h = live.handle ?? (await orTimeout(ready)); if (!h) throw haltRefused("the task is not running"); const apply = () => { const abortOwnedBeforeHalt = h.abortController.signal.aborted; const receipt = h.harness.halt(); if (receipt.accepted && !abortOwnedBeforeHalt) { h.loop.userHalted = true; } else if (!h.loop.userHalted) { deliverEngineNotice(runner.deps.onNotice, { code: "task.halt_unconsumed", message: "a user halt arrived while the run was already ending for its own reason: nothing was cut or stopped " + "by it — the run's own ending stands, and the result will not carry haltedByUser for this halt.", detail: { sessionId: h.sessionId, runId: h.runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), }, }); } if (receipt.turnCut) { deliverEngineNotice(runner.deps.onNotice, { code: "task.turn_interrupted", message: "a bare user halt cut the running turn: in-flight work was cut at a manufactured boundary " + "(finished tool calls keep their real results; never-started ones settle as interrupted) and " + "the run is collecting to a clean, resumable stop — no further model turn will start.", detail: { cause: "user_halt", sessionId: h.sessionId, runId: h.runId, ...(spec.taskId !== undefined ? { taskId: spec.taskId } : {}), }, }); } return { turnCut: receipt.turnCut }; }; try { if (live.resultValue !== undefined || h.loop.ended) throw haltRefused("the task is no longer running"); return apply(); } catch (e) { if (!(e instanceof Error && e.code === "invalid_state")) throw e; } const birthDeadline = Date.now() + READY_TIMEOUT_MS; while (live.resultValue === undefined && !h.loop.ended && Date.now() < birthDeadline) { try { return apply(); } catch (e2) { if (!(e2 instanceof Error && e2.code === "invalid_state")) throw e2; } await new Promise((r) => setTimeout(r, 10)); } throw haltRefused("the task is no longer running"); }; const destroy = () => (destroyOnce ??= (async () => { const h = live.handle ?? (await orTimeout(ready)); if (h) { h.abortController.abort(); void h.harness.abort(); } await orTimeout(run.catch(() => { })); await reapSuspended(); })()); return { halt, destroy }; }