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.
- TypeScript
- .NET (C#)
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" ]
using Ilmek;
/// <summary>A shopping cart — a last-write channel value.</summary>
public sealed class Cart
{
public List<string> Items { get; set; } = new();
}
/// <summary>Support state: each property is a channel; its reducer is an attribute.</summary>
public sealed class SupportState
{
[Append] public List<string> Messages { get; set; } = new(); // list of strings
public Cart Cart { get; set; } = new(); // last write wins (default)
}
var g = Graph.Create<SupportState>("support")
.Node("agent", (state, ctx) =>
Update.For<SupportState>().Append(s => s.Messages, "hi"))
.Edge(Graph.Start, "agent")
.Edge("agent", Graph.End)
.Compile();
var input = Update.For<SupportState>().Append(s => s.Messages, "hi");
var result = await IlmekRuntime.RunAsync(g, input);
Console.WriteLine($"{result.Status} [{string.Join(", ", result.State!.Messages)}]");
Reducers
A reducer has the signature (current, incoming) => next. current is absent on
the first write. Every implementation provides these built-ins:
| Reducer | Behaviour |
|---|---|
last_write | incoming wins. The default. |
append | List concat: current ++ wrap(incoming). |
merge | Shallow map merge; incoming wins per key. |
| custom | Any (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.