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