Skip to main content

Tokens & cancellation

Tokens​

Normative: MODEL.md §10.2.

ilmek is LLM-agnostic — the core never calls a model. But "stream the answer as it is generated" is universal, so a token has a fixed shape:

import { graph, channel, START, END, streamModes } from "@ilmek/core";
import type { ChannelMap, StateOf } from "@ilmek/core";

/** A token has a fixed shape: { type: "token", text, meta? }. */
type TokenChunk = { type: "token"; text: string; meta?: unknown };

/** Answer state — the reply, streamed token by token. */
const AnswerState = {
answer: channel.append<string>(),
} satisfies ChannelMap;

type AnswerState = StateOf<typeof AnswerState>;

const g = graph("answer", AnswerState)
.node("responder", async (state, ctx) => {
// ctx.emitToken(text, meta?) — sugar for ctx.emit(token(...)); not journaled.
for (const tok of ["Hel", "lo"]) ctx.emitToken(tok, { model: "demo" });
return { answer: ["Hello"] };
})
.edge(START, "responder")
.edge("responder", END)
.compile();

// the `messages` projection is exactly "the custom payloads that are tokens"
for await (const part of streamModes(g, { answer: [] }, ["messages"])) {
process.stdout.write((part.data as TokenChunk).text);
}

A node streams one with ctx.emitToken(text, meta?) — sugar for ctx.emit(token(...)). Tokens ride the same transient channel as emit and are therefore not journaled: on replay a node re-streams its tokens, while only the values it commits through ctx.step are memoized.

So the default is exactly "show your work again on resume, but never redo the side effects." The messages projection mode is precisely "the custom payloads that are tokens".

Cancellation​

Normative: MODEL.md §10.3.

A run may be given an AbortSignal. The engine checks it at every superstep boundary: an aborted run stops there and ends with run_end { status: "aborted", reason }.

  • The last committed checkpoint stands — abort stops the stream, it does not roll back — so the thread resumes cleanly later.
  • The same signal reaches node code as ctx.signal. A node must forward it to its own long awaits (an LLM call, a fetch) for cancellation to interrupt work already in flight.
  • The engine never force-kills a running task; cancellation is only as responsive as the node's own signal handling.
import { graph, channel, START, END, stream } from "@ilmek/core";
import type { ChannelMap, StateOf } from "@ilmek/core";

/** Work state — a log of what ran before the abort landed. */
const WorkState = {
log: channel.append<string>(),
} satisfies ChannelMap;

type WorkState = StateOf<typeof WorkState>;

const g = graph("work", WorkState)
.node("slow", async (state, ctx) => {
// Forward the signal to your own long awaits (an LLM call, a fetch).
await fetch("https://example.com/slow", { signal: ctx.signal });
return { log: ["done"] };
})
.edge(START, "slow")
.edge("slow", END)
.compile();

const controller = new AbortController();
const events = stream(g, { log: [] }, { signal: controller.signal });
// ... later:
controller.abort("user navigated away");

for await (const ev of events) console.log(ev.type); // ... run_end { status: "aborted" }

Run pnpm demo:stream to see tokens stream and a mid-stream cancel land cleanly.