Skip to main content

State & channels

Normative: MODEL.md §2.

State is a map of named channels. A node returns a partial update; the engine folds each key into its channel via that channel's reducer. A node never mutates state and never sees another node's update within the same superstep — see Supersteps.

Each .channel() also widens the builder's state type, so both state and the update a node returns are checked against exactly the channels declared so far. A typo'd channel is a compile error, not a runtime surprise.

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

type Cart = { items: string[] };

/** Support state — the shape lives in one named place. */
const SupportState = {
messages: channel.append<string>(), // list of strings
cart: channel.lastWrite<Cart>({ items: [] }), // last write wins
} satisfies ChannelMap;

type SupportState = StateOf<typeof SupportState>; // { messages: string[]; cart: Cart }

const g = graph("support", SupportState)
.node("agent", async (state, ctx) => ({ messages: ["hi"] })) // update checked against channels
.edge(START, "agent")
.edge("agent", END)
.compile();

const { status, state } = await run(g, { messages: ["hi"] });
console.log(status, state.messages); // "done" [ "hi", "hi" ]

Reducers​

A reducer has the signature (current, incoming) => next. current is absent on the first write. Every implementation provides these built-ins:

ReducerBehaviour
last_writeincoming wins. The default.
appendList concat: current ++ wrap(incoming).
mergeShallow map merge; incoming wins per key.
customAny (current, incoming) => next function.

The conflict rule​

When two tasks in the same superstep write the same channel, the reducer folds both. For a non-commutative reducer (last_write, append), the fold order is task order — the order nodes appear in the graph's node list, not completion order. This keeps a superstep deterministic regardless of how tasks happen to be scheduled.

Serializability​

Channels must be JSON-serializable — they are checkpointed. A non-serializable value (a PID, socket, or stream handle) belongs in a step result only if the serializer round-trips it; return an id and re-resolve it instead.