moonstate

The reference

Every public item of the three packages, read from each package's interface file and from the doc comments beside the code. Nothing here is written by hand, and a stale page fails the build. Press / to filter.

moonstate

The graph, its builder, the steps a node returns, the two contracts a store or a saver implements, and the in-memory implementations that ship with them.

suberror GraphError

Why a run could not finish.

pub(all) suberror GraphError {
  StepLimit(limit~ : Int32)
  NodeFailed(node~ : String, message~ : String)
  Interrupted(thread~ : String, value~ : @json.Json)
  NotInterrupted(thread~ : String)
  MissingThread(thread~ : String)
  MissingNode(node~ : String)
  Unreadable(reason~ : String)
  Cancelled(thread~ : String)
  Backend(reason~ : String)
}
StepLimit

The run took limit supersteps without reaching an end.

NodeFailed

A node raised, after its retries if it had any; message is the error text.

Interrupted

A node asked to stop and wait for an answer. resume continues the thread.

NotInterrupted

resume was called on a thread with nothing to continue.

MissingThread

The thread has no checkpoint the saver knows about.

MissingNode

A node routed the run to a name the graph does not define. The compile-time check cannot see the names a Goto, a Send or a branch picks, so the run reports them the first time one is taken.

Unreadable

A checkpoint could not be decoded back into the graph's state type.

Cancelled

A node cancelled the run, or the caller did before the next superstep.

Backend

A saver, a store, an executor or a stream callback raised. The engine keeps going only where it can tell what went wrong, so this is where an outside failure ends up.

suberror Refused

Why a graph definition was refused when it was compiled. Every check here is structural: the nodes and edges are known then, the runs are not.

pub(all) suberror Refused {
  NoNodes
  NoEntry
  UnknownEntry(node~ : String)
  DuplicateNode(node~ : String)
  DanglingEdge(from~ : String, to~ : String)
  Unreachable(node~ : String)
  NoExit
}

struct Builder

Accumulates nodes and edges, then checks them once in compile. Every method returns the builder, so a graph reads as one chain.

pub struct Builder[S, U] {
  merge : (S, U) -> S
  initial : S
  mut entry : String?
  nodes : Map[String, NodeSpec[S, U]]
  edges : Array[(String, String)]
  joins : Array[(Array[String], String)]
  branches : Array[(String, async (S) -> Array[String])]
  routed : Array[String]
  duplicates : Array[String]
}
pub fn[S, U] Builder::branch(Self[S, U], String, async (S) -> Array[String]) -> Self[S, U]

Let route decide where to go after from. It is called with the state as merged at the end of the superstep, so it sees what the nodes wrote.

pub fn[S, U] Builder::compile(Self[S, U], String, saver? : &Saver, store? : &Store, executor? : &Executor, limit? : Int32) -> Graph[S, U] raise Refused

Fold the collected structure into a graph, refusing a definition that cannot run: an edge to nowhere, a node nothing reaches, a graph with no end.

pub fn[S, U] Builder::edge(Self[S, U], String, String) -> Self[S, U]

Run to after from, unconditionally.

pub fn[S, U] Builder::entry(Self[S, U], String) -> Self[S, U]

Make name the node a run starts at. Without it a graph of exactly one node starts there, and anything larger is refused.

pub fn[S, U] Builder::join(Self[S, U], Array[String], String) -> Self[S, U]

Run to once every node in from has run in the same superstep, and not before: a join is a gate, not just an edge.

pub fn[S, U] Builder::new(merge~ : (S, U) -> S, initial~ : S) -> Self[S, U]

A builder whose state starts at initial and whose every merged write goes through merge. initial is what a run starts from before the input is merged in, so it is the empty state: zeroed counters, empty lists.

pub fn[S, U] Builder::node(Self[S, U], String, async (S, Run) -> Array[Step[S, U]], retry? : Retry) -> Self[S, U]

Add node name. run gets the state and a Run handle and returns the steps it wants applied; retry gives it more than one try.

pub fn[S, U] Builder::routed(Self[S, U], String) -> Self[S, U]

Declare that name is entered at run time — by a Goto, a Send or a route — rather than by an edge. Without this the compile-time reachability check has no way to tell such a node from dead code, and refuses the graph.

pub fn[T : @json.ToJson + @json.FromJson, V : @json.ToJson + @json.FromJson, S, U] Builder::subgraph(Self[S, U], String, Graph[T, V], enter~ : (S) -> T, leave~ : (T) -> U) -> Self[S, U]

struct Checkpoint

One durable point in a thread's run: the state after step supersteps, the tasks the next superstep will run, and the writes already collected for them. A Saver stores these verbatim; the engine is the only thing that reads the state back into its own type.

pub(all) struct Checkpoint {
  thread : String
  id : String
  parent : String?
  seq : Int32
  step : Int32
  state : @json.Json
  pending : Array[Pending]
  interrupt : @json.Json?
  created_at : Int64
  writes : Array[TaskWrite]?
}
id

Unique within the thread, and ordered by the saver.

parent

The checkpoint this one was written on top of, or None for the first.

seq

How many checkpoints the thread had written before this one. id is derived from it, so an update that forks the thread does not collide with the checkpoints it forked away from.

step

The superstep this checkpoint is about to run.

state

The whole state, encoded.

pending

The tasks of the next superstep, in the order their writes will be merged.

interrupt

Set when the superstep was cut short by Step::Interrupt.

writes

The writes recorded for this checkpoint's tasks. A saver fills this in when it reads the checkpoint back; it is never part of what put stores.

pub fn Checkpoint::from_json(@json.Json, @json.JsonPath) -> Self raise @json.JsonDecodeError
pub fn Checkpoint::to_json(Self) -> @json.Json

struct Context

What an entrypoint hands its body: the run it belongs to, and the machinery a task call needs. One context per attempt; call advances it, which is how a replayed run lines its calls up with the recorded ones.

pub struct Context {
  run : Run
  saver : &Saver?
  executor : &Executor
  graph : String
  mut index : Int32
  recorded : Map[String, @json.Json]
}
pub fn Context::index(Self) -> Int32

How many task calls this attempt has made so far.

pub fn Context::run(Self) -> Run

The run this entrypoint belongs to.

struct Done

What an executor reports back for one work item. Exactly one of values and error is set: a failed node's error travels as text, because the executor may not share a process with the graph that would have to match on it.

pub(all) struct Done {
  values : Array[@json.Json]?
  error : String?
}

struct Entrypoint

A graph built from one function: the function is the only node, and every Task::call inside it is a durable step of that node.

pub struct Entrypoint[I, O] {
  graph : Graph[Frame, Frame]
  encode : (I) -> @json.Json
  decode : (@json.Json) -> O raise GraphError
}

enum Event

What a run reports while it happens. The stream modes of a graph engine are one type here: the caller takes the variants it needs and ignores the rest.

pub(all) enum Event[S, U] {
  Values(S)
  Updates(node~ : String, U)
  Custom(node~ : String, @json.Json)
  Started(node~ : String)
  Finished(node~ : String)
  Interrupted(@json.Json)
}
Values

The whole state after a superstep.

Updates

One node's write, as it is folded in. Several arrive per superstep.

Custom

Whatever a node passed to Run::emit.

Started

A node has been handed to the executor.

Finished

A node finished and its steps are about to be applied.

Interrupted

The run stopped and is waiting for resume with this value.

struct Frame

The state a functional graph runs on: the input the entrypoint was called with, and the value it returned. Keeping both as JSON is what makes a half-finished entrypoint resumable — the input is still there to replay from.

pub(all) struct Frame {
  input : @json.Json
  output : @json.Json?
}
pub fn Frame::from_json(@json.Json, @json.JsonPath) -> Self raise @json.JsonDecodeError
pub fn Frame::to_json(Self) -> @json.Json

struct Graph

A compiled graph: the nodes, the edges between them, and the three services a run leans on — a saver for checkpoints, a store for long-term memory, and an executor for where the nodes run. Everything structural was checked in compile, so a run only fails for reasons the run itself discovers.

pub struct Graph[S, U] {
  name : String
  merge : (S, U) -> S
  initial : S
  entry : String
  nodes : Map[String, NodeSpec[S, U]]
  edges : Map[String, Array[String]]
  joins : Array[(Array[String], String)]
  branches : Map[String, Array[async (S) -> Array[String]]]
  saver : &Saver?
  store : &Store?
  executor : &Executor
  limit : Int32
}
pub fn[S, U] Graph::limit(Self[S, U]) -> Int32

The superstep bound this graph was compiled with.

pub fn[S, U] Graph::name(Self[S, U]) -> String

struct Item

One remembered value: a space, a key inside it, and the value itself.

pub(all) struct Item {
  space : Array[String]
  key : String
  value : @json.Json
}

struct MemorySaver

A saver that keeps everything in memory: enough for a run that has to survive a failed node, not the process it runs in.

pub struct MemorySaver {
  threads : Map[String, Array[Checkpoint]]
  writes : Map[String, Array[TaskWrite]]
}
pub fn MemorySaver::new() -> Self

struct MemoryStore

A store that keeps everything in memory, for tests and for short-lived processes.

pub struct MemoryStore {
  items : Map[String, Item]
}
pub fn MemoryStore::new() -> Self

struct Pending

A node queued for a superstep, as a checkpoint records it. input is the state the node is handed: absent means the state as merged at that point, present means a Send gave it its own.

pub(all) struct Pending {
  node : String
  input : @json.Json?
}
pub fn Pending::from_json(@json.Json, @json.JsonPath) -> Self raise @json.JsonDecodeError
pub fn Pending::to_json(Self) -> @json.Json

struct Retry

How often a node is tried before its failure ends the run, and how long to wait between tries. attempts counts the first try: attempts of 1 means no retry at all.

pub(all) struct Retry {
  attempts : Int32
  delay : Int32
}

struct Run

What a node is handed besides the state: which thread it belongs to, which superstep it is running in, where to put long-term memory, how to report progress, and how to stop. A run handle belongs to one task. Calling cancel on it stops the thread at the next superstep boundary — never in the middle of a node, so what a node wrote before asking to stop is kept.

pub struct Run {
  thread : String
  step : Int32
  checkpoint : String
  answer : @json.Json?
  store : &Store?
  saver : &Saver?
  cancelled : @ref.Ref[Bool]
  emit : (@json.Json) -> Unit
  writes : Array[TaskWrite]
}
pub fn Run::answer(Self) -> @json.Json?

The answer resume was called with, for the node that asked for it.

pub fn Run::cancel(Self) -> Unit

Ask the thread to stop at the next superstep boundary.

pub fn Run::checkpoint(Self) -> String

The checkpoint this superstep's writes are filed under. A node that keeps its own progress in Saver::put_writes needs it.

pub fn Run::emit(Self, @json.Json) -> Unit

Report progress to whoever is streaming this run. Does nothing when nobody is.

pub fn Run::is_cancelled(Self) -> Bool
pub fn Run::saver(Self) -> &Saver?

The saver the graph was compiled with, if any. A node that runs another graph hands it on, so the child's checkpoints land next to the parent's.

pub fn Run::step(Self) -> Int32

The superstep this task belongs to, counting from zero.

pub fn Run::store(Self) -> &Store?

The long-term store the graph was compiled with, if any.

pub fn Run::thread(Self) -> String
pub fn Run::writes(Self) -> Array[TaskWrite]

The writes already recorded for this checkpoint's tasks. A node that runs other durable steps itself — the functional form does — reads them back here to skip work a crashed run had already finished.

struct Snapshot

A checkpoint seen through the graph's own state type: what a caller asks for when it wants to look at a thread, not to resume it.

pub(all) struct Snapshot[S] {
  thread : String
  id : String
  parent : String?
  step : Int32
  state : S
  next : Array[String]
  interrupt : @json.Json?
  created_at : Int64
}
next

The nodes the next superstep would run. Empty when the run finished.

enum Step

What one node contributes to the superstep it took part in. A node returns a list of these, applied in the order it wrote them: writes fold into the state, Goto and Send decide who runs next, and Interrupt stops the run there.

pub(all) enum Step[S, U] {
  Write(U)
  Goto(String)
  Send(String, S)
  Interrupt(@json.Json)
}
Write

Fold U into the state with the graph's merge function.

Goto

Leave the node's declared successors behind and run this node next.

Send

Queue this node for the next superstep with its own input state, which is how a fan-out hands each branch a different slice of the work.

Interrupt

Stop before the next superstep and wait for resume. The node that returned this runs again from the same input when the thread resumes, and reads the answer through Run::answer.

pub fn[S : @json.FromJson, U : @json.FromJson] Step::decode(@json.Json) -> Self[S, U] raise GraphError

Read back a step written by encode. A step that cannot be decoded cannot be applied, and guessing at it would corrupt the state silently.

pub fn[S : @json.ToJson, U : @json.ToJson] Step::encode(Self[S, U]) -> @json.Json

The wire form of a step: what a Work carries back from wherever the node ran. Executors only ever see this, which is what lets a node run in another process without the graph itself crossing the boundary.

struct Task

A step that is recorded when it finishes. Calling one through Task::call writes its result to the checkpoint before the entrypoint moves on, so a run that replays after a crash reuses the result instead of doing the work twice.

pub struct Task[I, O] {
  name : String
  run : async (I) -> O
  retry : Retry?
}

struct TaskWrite

The steps one task produced, kept apart from the checkpoint record so that a finished task is never run twice after a crash.

pub(all) struct TaskWrite {
  task : String
  values : Array[@json.Json]
}
pub fn TaskWrite::from_json(@json.Json, @json.JsonPath) -> Self raise @json.JsonDecodeError
pub fn TaskWrite::to_json(Self) -> @json.Json

struct Work

One node's turn to run, as the executor sees it. input is a function so the state is encoded only when it actually crosses a process boundary; run is what an in-process executor calls, and what an executor that ships the task elsewhere ignores — it finds the node by graph and node on the far side.

pub(all) struct Work {
  graph : String
  node : String
  thread : String
  step : Int32
  attempt : Int32
  input : () -> @json.Json
  run : async () -> Array[@json.Json]
}

trait Executor

Where a superstep's tasks run. The engine hands over the whole superstep at once, which is what makes a fan-out parallel; the results come back in the order the work was given, so merging stays deterministic.

pub(open) trait Executor {
  async fn each(Self, Array[Work]) -> Array[Done]
}

trait Saver

Where a thread's checkpoints live. The engine writes one per superstep and reads the last one back to resume, so an implementation only has to store records and hand them back in order.

pub(open) trait Saver {
  async fn get(Self, String, id? : String) -> Checkpoint?
  async fn put(Self, Checkpoint) -> Unit
  async fn put_writes(Self, String, String, String, Array[@json.Json]) -> Unit
  async fn list(Self, String, limit? : Int32, before? : String) -> Array[Checkpoint]
  async fn delete(Self, String) -> Unit
}
async

Drop a thread entirely: checkpoints and writes.

trait Store

Long-term memory that outlives a thread: spaced key-value pairs with a search over them. A node reaches the store through Run::store.

pub(open) trait Store {
  async fn get(Self, Array[String], String) -> Item?
  async fn put(Self, Array[String], String, @json.Json) -> Unit
  async fn delete(Self, Array[String], String) -> Unit
  async fn search(Self, Array[String], query? : String, filter? : Map[String, @json.Json], limit? : Int32) -> Array[Item]
}
async

Items under space (or below it), newest first. query is matched against the item's text, filter against its top-level object fields.

Functions

pub fn[I : @json.ToJson + @json.FromJson, O : @json.ToJson + @json.FromJson] entrypoint(String, async (I, Context) -> O, saver? : &Saver, store? : &Store, executor? : &Executor, limit? : Int32) -> Entrypoint[I, O] raise Refused

Wrap body as a graph with one node. The body may call tasks; each call is recorded, and a resumed run replays the body from the start and reuses the results. The body itself has to be deterministic apart from those calls — replaying it is how the run gets back to where it stopped.

pub fn[I, O] task(String, async (I) -> O, retry? : Retry) -> Task[I, O]

Values

pub let default_limit : Int32

How many supersteps a run may take before it is stopped. This is the bound a graph made of cycles relies on; it is LangGraph 1.2's default recursion limit of 25 (libs/langgraph/langgraph/constants.py), kept the same so a graph ported from there needs no new number.

pub let inline : &Executor

The default executor: every task in this process, at the same time.

pub let retry : Retry

Three tries with no pause, the default a durable workflow wants for a step that talks to something outside the process.

moonstate/sql

Checkpoints and long-term memory on a SQL backend: one implementation, written against the dialect the driver reports.

suberror Failed

A row that could not be read back into the record it was written from: a checkpoint's state, a task's steps, a store item, or a column that is null when the record needs it.

pub(all) suberror Failed {
  Unreadable(column~ : String, reason~ : String)
}
pub fn Failed::report(Self) -> String

The text an error carries, for the Backend a caller ends up matching on.

struct Names

The three tables this package keeps, so that two savers can share one database without sharing one set of names.

pub(all) struct Names {
  checkpoint : String
  write : String
  item : String
}
checkpoint

One row per checkpoint.

write

One row per task that finished inside a superstep.

item

One row per key of the long-term store.

struct SqlSaver

Checkpoints in a SQL table. One connection, one mutex: the driver's connection carries one exchange at a time, and two runs sharing a saver would otherwise interleave their statements on it. Compile a graph with one saver per connection.

pub struct SqlSaver[T] {
  driver : T
  names : Names
  statements : Statements
  lock : @async.Mutex
  dialect : @moondb.Dialect
}
pub fn[T] SqlSaver::dialect(Self[T]) -> @moondb.Dialect

The dialect the driver reported, which is the one the statements were written for.

struct SqlStore

Long-term memory in a SQL table: spaced keys, each holding one JSON value. The store is meant to outlive a thread, so it shares the connection discipline with the saver: one connection, one mutex, one saver and one store per connection.

pub struct SqlStore[T] {
  driver : T
  names : Names
  statements : Statements
  lock : @async.Mutex
  dialect : @moondb.Dialect
}

Functions

pub fn[T : @moondb.AsyncDriver] saver(T, names? : Names, dialect? : @moondb.Dialect) -> SqlSaver[T]

A saver over driver, using the tables in names. Nothing is written until setup has run: a saver does not create tables behind the caller's back, because a service account is often not allowed to. ``moonbit nocheck let saver = @sql.saver(connection) saver.setup() let graph = builder.compile("job", saver=saver) ` dialect` overrides what the driver reports. A driver that numbers its parameters has to report its own dialect for this to be unnecessary; until it does, naming it here is the difference between one statement working and the first one being rejected by the server.

pub fn[T : @moondb.AsyncDriver] store(T, names? : Names, dialect? : @moondb.Dialect) -> SqlStore[T]

A store over driver, using the tables in names. setup on a saver of the same tables creates what this needs. dialect overrides what the driver reports, the way saver's does.

Values

pub let names : Names

The names a saver and a store take when nothing else is asked for.

moonstate/otel

An executor that reports each superstep and each node inside it as a span.

struct Traced

An executor that reports what it runs: one span per superstep, and one child span per node inside that superstep. A trace follows a thread from superstep to superstep — each superstep span is a child of the previous one — so a run is one trace however many supersteps it takes, and a node's span sits under the superstep that ran it. Two threads sharing the executor get two traces, because the chain is kept per thread.

pub struct Traced {
  inner : &@moonstate.Executor
  tracer : @trace.Tracer
  now : () -> Int64
  last : Map[String, @trace.TraceContext]
}
last

The span the next superstep of a thread hangs off. Kept per thread, because the engine runs supersteps one at a time but several threads at once.

Functions

pub fn traced(&@moonstate.Executor, tracer~ : @trace.Tracer, now~ : () -> Int64) -> Traced

Wrap inner so every task it runs is traced. now supplies the nanosecond timestamps; this library does not read a clock of its own, so a replay can run on a timeline the caller chose.