flow

package
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 24, 2026 License: Apache-2.0 Imports: 14 Imported by: 0

Documentation

Overview

Package flow is the durable, pregel-style workflow engine. See design docs/plans/2026-06-24-flow-engine-design.md §1.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func AddVertex

func AddVertex[I, O, S any](
	g *Graph[S], id VertexID, task Task[I, O],
	selector Selector[S, I], reducer Reducer[S, O], opts ...VertexOption[S],
) error

AddVertex binds task into g under id with the given selector and reducer (§6.1). It is a package-level generic FUNCTION (not a method) because it introduces the type parameters I and O that the method receiver *Graph[S] cannot.

It fails fast on a zero or duplicate id, and on a nil task, selector, or reducer (Compile re-checks structural invariants later, §8). On success it builds the erased vertex[S] — the three seam closures, each narrowing any back to I/O the instant it re-enters typed code — applies opts, and stores it in g.

The task == nil guard rejects only a LITERAL nil task interface. A TYPED-NIL task — a non-nil interface wrapping a nil pointer, e.g. (*FuncTask[I,O])(nil) — is deliberately NOT caught here: detecting it would require reflect, and the engine is reflection-free by design (see typeName). Such a value instead surfaces at execution as a recovered VertexError when the execute closure calls Execute on the nil receiver (§12.5).

func Interrupt

func Interrupt(ctx context.Context, info any) error

Interrupt pauses the calling vertex with a user-facing reason, returning the error a task returns to request the pause. The coordinator detects it via asInterrupt and writes an Awaiting interrupt to the checkpoint rather than failing the run; info is persisted as the InterruptRecord.Info serialization boundary and read back through ResumePayload on resume.

ctx is part of the documented signature and is intentionally unused today; a future revision may read run identity from it. It is named ctx and accepted so the signature is stable.

func InterruptState

func InterruptState[T any](ctx context.Context) (T, bool)

InterruptState returns the StatefulInterrupt continuation restored for the calling vertex on resume (§10.3). Unlike ResumePayload, the continuation was PERSISTED as json.RawMessage in the checkpoint's InterruptRecord.Continuation and restored into the context as bytes, so it is recovered by json.Unmarshal into T — the typed-read side of the §10.3 serialization boundary. Returns (zero, false) if no continuation was injected, the bytes are nil, or they do not decode into T.

func ResumePayload

func ResumePayload[T any](ctx context.Context) (T, bool)

ResumePayload returns the value passed to Resume(ctx, id, payload) for the run the calling vertex belongs to (§10.3, §9.7). The payload is a LIVE Go value supplied in-process at Resume, so it is recovered by TYPE ASSERTION to T — it is never decoded from JSON here. (Contrast InterruptState, which decodes a persisted continuation.) Returns (zero, false) if no payload was injected or it is not a T.

func Serve

func Serve(ctx context.Context, reg Resolver, cp ControlPlane) error

Serve runs the worker loop (§18.6): it consumes Work for the versions reg serves, resolves each to its RunnerHandle, executes it, and Acks at a QUIESCENT result (Completed/Interrupted/Halted — any returned Result, §18.5) or on a PERMANENT failure (Ack-and-abandon — see settle), Nacking only a transient/infra failure for redelivery. Registration is implicit: it consumes exactly reg.Keys(). A failure to subscribe (Consume error) is returned immediately. Each delivery is handled in its own goroutine — the control plane single-flights per run and distinct runs are independent, so concurrent handling is safe.

Shutdown semantics. On ctx-cancel the control plane closes the Consume channel, so the range ends; Serve then waits (via the WaitGroup) for the in-flight GOROUTINES to RETURN — it does NOT run their work to completion. Each in-flight run executes against the SAME cancelled ctx, so its Run/Resume observes the cancellation, returns an error, and is Nack'd: on a DURABLE control plane that Nack means the work is redelivered (resumed elsewhere later); on the EPHEMERAL in-process plane the Nack's requeue is dropped when the plane shuts down (the run is abandoned, its last durable checkpoint intact). Either way Serve returns promptly with ctx.Err() (nil only if ctx had no error) and leaks no goroutine.

func StatefulInterrupt

func StatefulInterrupt(ctx context.Context, info, continuation any) error

StatefulInterrupt pauses the calling vertex like Interrupt but also stows a LIVE continuation value so the task can pick up where it left off on resume. The continuation is held in the signal untouched; the coordinator marshals it to the InterruptRecord.Continuation serialization boundary when it writes the checkpoint, and the task reads it back through InterruptState on resume. This constructor does NOT marshal — it only carries the live value (§10.3).

ctx is part of the documented signature and is intentionally unused today (see Interrupt).

Types

type AmbiguousRoutingError

type AmbiguousRoutingError struct{ VertexID VertexID }

AmbiguousRoutingError reports that a vertex declares both an unconditional out-edge and a conditional edge, so its routing is ambiguous (§8).

func (*AmbiguousRoutingError) Error

func (e *AmbiguousRoutingError) Error() string

Error names the ambiguously-routed vertex.

type BuildError

type BuildError struct {
	Op     string // the build call: "AddVertex" | "AddEdge" | "AddConditionalEdge"
	Detail string // the offending parameter, e.g. "nil task" or "zero from vertex"
}

BuildError reports a fail-fast violation of an add-time build invariant that has no more specific typed error: a nil required argument (task, selector, reducer, condition Pick) or a malformed identifier/parameter supplied to a build call (a zero VertexID, an empty Condition.Targets, a second conditional edge on a from that already has one). It is distinct from the structural Compile checks (§8); those keep their own typed errors (UnknownVertexError, AmbiguousRoutingError, …). Op names the failing build call (e.g. "AddVertex", "AddEdge", "AddConditionalEdge") and Detail names the offending parameter so an operator can identify the bad call from a log line.

func (*BuildError) Error

func (e *BuildError) Error() string

Error names the failing build call and the offending parameter.

type Checkpoint

type Checkpoint struct {
	Run        GraphRunState     // run-level status + Revision + timestamps
	StepBase   json.RawMessage   `json:",omitempty"` // committed S_N — frozen read snapshot; pending vertices' selectors read THIS
	State      json.RawMessage   `json:",omitempty"` // accumulated S (S_N + reducers of every terminal vertex so far)
	Vertices   []VertexState     // per-vertex records for this step; terminal ones are skipped on resume
	Frontier   []VertexID        // the active vertex set this checkpoint resumes from (meaning depends on Phase)
	Routes     []RouteRecord     // routing decisions that produced Frontier (§9.5)
	Phase      StepPhase         // phase of this checkpoint within the super-step
	Interrupts []InterruptRecord // per-vertex pauses; present in StepRunning AND StepPaused; mutually exclusive with Halt (§9.7)
	Halt       *HaltRecord       // run-level routing/structural halt (StepHalted); mutually exclusive with Interrupts (§9.8)
}

Checkpoint is the engine's durable, append-only record of one unit of execution (§10.1): the run-level state, the frozen read snapshot S_N, the accumulated state S, per-vertex records, the active frontier, the routing decisions, and the phase. Interrupts and Halt are mutually exclusive in a VALID checkpoint (enforced on load in §10.4); the struct can physically hold both, but never does for a well-formed checkpoint.

type CheckpointDecodeError

type CheckpointDecodeError struct {
	Field string // "StepBase" | "State" | "checkpoint"
	Err   error
}

CheckpointDecodeError reports that part of a loaded checkpoint failed to decode. Field names the failing part ("StepBase", "State", or "checkpoint"). It wraps the decode cause (§10.4).

func (*CheckpointDecodeError) Error

func (e *CheckpointDecodeError) Error() string

Error names the failing field and the underlying decode cause.

func (*CheckpointDecodeError) Unwrap

func (e *CheckpointDecodeError) Unwrap() error

Unwrap returns the underlying decode cause so errors.Is/As can inspect it.

type CheckpointGranularity

type CheckpointGranularity int

CheckpointGranularity selects WHEN the coordinator writes checkpoints within a super-step (§10.1). PerVertex (the default) appends after each vertex reduces, so a crash mid-step loses at most one vertex's work; PerStep defers all writes to the step boundary for fewer writes at coarser recovery. This sub-task implements PerVertex; the PerStep behavior is a later sub-task.

const (
	PerVertex CheckpointGranularity = iota // append after each vertex (default)
	PerStep                                // defer writes to the step boundary
)

type CheckpointNotFoundError

type CheckpointNotFoundError struct{ GraphRunID GraphRunID }

CheckpointNotFoundError reports that no checkpoint exists for the given run, so there is nothing to resume from (§10.2).

func (*CheckpointNotFoundError) Error

func (e *CheckpointNotFoundError) Error() string

Error names the run with no checkpoint.

type CheckpointStore

type CheckpointStore interface {
	// Append durably records cp iff cp.Run.Revision is the next revision in
	// sequence for cp.Run.GraphRunID (compare-and-append). Otherwise it returns a
	// *RevisionConflictError. A serialization failure is a *StoreError.
	Append(ctx context.Context, cp *Checkpoint) error
	// Latest returns the highest-revision checkpoint for id (the source of truth),
	// or a *CheckpointNotFoundError if the run has no checkpoints. It MUST return
	// the checkpoint with the HIGHEST Run.Revision for the run: the §10.4 resume
	// contract depends on the loaded checkpoint being genuinely the latest, and a
	// backend that returns a stale revision violates the contract (it would fork or
	// overwrite committed history on the next append).
	Latest(ctx context.Context, id GraphRunID) (*Checkpoint, error)
	// History returns every checkpoint for id ordered by revision (0,1,2,…), or a
	// *CheckpointNotFoundError if the run has no checkpoints.
	History(ctx context.Context, id GraphRunID) ([]*Checkpoint, error)
}

CheckpointStore is the engine's durable, append-only checkpoint history for graph runs (§10.2). Every implementation must honor the same contract: compare-and-append on (GraphRunID, Revision), latest-revision-as-source-of- truth, ordered History, structural immutability of stored checkpoints, and a CheckpointNotFoundError for an unknown run. Every method honors ctx.

SECURITY (durable backends): a backend that decodes UNTRUSTED stored bytes on read (§10.4) MUST bound the payload size and nesting before decoding, to guard against oversized or deeply-nested input (CLAUDE.md: guard against unbounded sizes). MemStore is exempt only because its bytes are self-produced in-memory.

CONTRACT (Latest is genuinely the latest, §10.4): Latest MUST return the checkpoint with the HIGHEST Run.Revision for the run. The resume contract (§10.4) depends on the loaded checkpoint being the true latest — Resume continues the append-only sequence from cp.Run.Revision, so a backend that returns a STALE checkpoint would fork or overwrite committed history. A durable backend's conformance test MUST cover this; MemStore guarantees it structurally (it stores the contiguous 0..len-1 revisions and returns the last).

type CompileOption

type CompileOption func(*compileConfig)

CompileOption configures Compile (§8). It is non-generic so the same option value works for any state type S, since the store is fixed at Compile (§9) and not parameterized by S.

func WithStore

func WithStore(s CheckpointStore) CompileOption

WithStore pins the CheckpointStore the Runner uses for ALL operations — Run, Resume, Status, Get (§9). The store is fixed at Compile; there is no per-run override. Passing nil is not an error: Compile falls back to a fresh default MemStore, so a zero/nil store never reaches the Runner (fail safe).

type Condition

type Condition[S any] struct {
	Targets []VertexID                                         // declared possible targets — validated at Compile (§8)
	Pick    func(ctx context.Context, s S) ([]VertexID, error) // choose ≥1 declared target (read-only)
}

Condition is a conditional out-edge from a vertex (§7): a declared set of possible Targets and a Pick that chooses one or more of them from the current state. Pick is read-only and must return at least one declared target (a multi-target return is a fan-out); the empty-set and undeclared-target rules are enforced at runtime (§9.5). Existence of Targets is validated at Compile.

type ConditionError

type ConditionError struct {
	From VertexID
	Err  error
}

ConditionError reports that a condition's Pick returned an error or panicked. It wraps the cause and surfaces as a run-level halt (HaltCondition, §9.5, §9.8).

func (*ConditionError) Error

func (e *ConditionError) Error() string

Error names the source vertex and the underlying cause.

func (*ConditionError) Unwrap

func (e *ConditionError) Unwrap() error

Unwrap returns the underlying cause so errors.Is/As can inspect it.

type ControlPlane

type ControlPlane interface {
	// Submit enqueues w for consumers serving w.Key. It honors ctx and must not
	// block unboundedly.
	Submit(ctx context.Context, w Work) error
	// Consume returns a channel delivering only Work whose Key is in serves. The
	// channel is closed when ctx is done (clean shutdown, no goroutine leak).
	Consume(ctx context.Context, serves []GraphVersionKey) (<-chan Delivery, error)
}

ControlPlane accepts work and distributes it to workers (§18.5). It is SEPARATE from CheckpointStore (transient consume-once dispatch vs durable append-only history). Implementations must honor ctx on every call (no unbounded blocking). Registration is implicit — a worker registers by Consuming the version keys it serves; there is no separate registration RPC.

SINGLE-FLIGHT (best-effort, NOT the correctness boundary). An implementation SHOULD avoid delivering two Works for the same GraphRunID concurrently, to spare duplicate work — but it need not guarantee it. The in-process plane (controlplane.Mem) DOES guarantee it via a central dispatcher; a distributed plane (nats.ControlPlane) provides single-flight at MESSAGE granularity (a work-queue delivers each message once until it is acked), so two DISTINCT messages for the same run can be processed concurrently. That is safe by design: CORRECTNESS — no duplicate COMMITTED effects — is guaranteed by the store's compare-and-append (RevisionConflictError, §10.2) and IdempotencyKey (§4.1), which absorb concurrent duplicate work; the control plane's single-flight is only an efficiency optimization. Providing STRICTER single-flight than required still satisfies this contract (LSP), so controlplane.Mem remains conformant.

type DeadEndError

type DeadEndError struct{ Step StepID }

DeadEndError reports that the frontier drained without the finish vertex ever executing, so the run can make no further progress; a run-level halt (§9.5, §9.8).

func (*DeadEndError) Error

func (e *DeadEndError) Error() string

Error names the step at which the frontier drained.

type Delivery

type Delivery struct {
	Work Work
	Ack  func() error
	Nack func() error
}

Delivery wraps Work with explicit ack semantics so durable backends survive worker crashes (§18.5). A worker calls Ack when the work reaches a QUIESCENT result (completed / interrupted / halted / cancelled, or safely requeued) to drop it from the queue; it calls Nack to requeue the work for redelivery (transient failure / shedding load). Exactly one of Ack/Nack should be called per Delivery; the control plane holds the run's single-flight slot until one of them fires (§18.5).

type DuplicateConditionalEdgeError

type DuplicateConditionalEdgeError struct{ From VertexID }

DuplicateConditionalEdgeError reports that a second conditional out-edge was added for a vertex that already has one; a vertex may have at most one conditional edge, since a second would silently overwrite the first (§7).

func (*DuplicateConditionalEdgeError) Error

Error names the vertex with the duplicate conditional edge.

type DuplicateVertexError

type DuplicateVertexError struct{ VertexID VertexID }

DuplicateVertexError reports that the same VertexID was added to a graph more than once; VertexIDs must be unique within a graph (§8).

func (*DuplicateVertexError) Error

func (e *DuplicateVertexError) Error() string

Error names the duplicated vertex.

type FuncTask

type FuncTask[I, O any] struct {
	// contains filtered or unexported fields
}

FuncTask adapts a TaskFunc into a Task. It holds only the function — no VertexID, no state — so a single value is safe to reuse across graphs and concurrent runs.

func NewFuncTask

func NewFuncTask[I, O any](fn TaskFunc[I, O]) *FuncTask[I, O]

NewFuncTask wraps fn as a *FuncTask, the first concrete Task[I, O] kind (§5).

func (*FuncTask[I, O]) Execute

func (t *FuncTask[I, O]) Execute(ctx context.Context, in I) (O, error)

Execute runs the wrapped TaskFunc, forwarding ctx and in and returning its (O, error) verbatim. It holds no per-call state, so repeated calls are independent (§5 reusability).

type Graph

type Graph[S any] struct {
	// contains filtered or unexported fields
}

Graph is the mutable build-time definition of a workflow over shared state S (§7). It is UNEXPORTED in its fields: callers build it through NewGraph, AddVertex, AddEdge, and AddConditionalEdge, then Compile it (later task) into an immutable Runner. It is not safe for concurrent mutation; build it on one goroutine before compiling.

func NewGraph

func NewGraph[S any](id GraphID, opts ...GraphOption) *Graph[S]

NewGraph creates an empty, mutable Graph[S] with the given stable identity and options (§7). It initializes all maps so AddVertex/AddEdge/AddConditionalEdge never nil-deref, and resolves options into the stored userVersion.

func (*Graph[S]) AddConditionalEdge

func (g *Graph[S]) AddConditionalEdge(from VertexID, c Condition[S]) error

AddConditionalEdge records the conditional out-edge c for from (§7). It rejects a zero from, a nil c.Pick, or empty c.Targets (each a malformed-argument BuildError), and rejects a SECOND conditional edge on the same from (a uniqueness violation — DuplicateConditionalEdgeError — since a second would silently overwrite the first). Target existence — and the rule that a from may not have both a static and a conditional edge — are deferred to Compile (§8).

The stored Condition's Targets are a defensive copy, so a caller mutating their original slice afterward cannot mutate graph state.

func (*Graph[S]) AddEdge

func (g *Graph[S]) AddEdge(from, to VertexID) error

AddEdge records a static out-edge from→to (§7). Multiple edges from one from are a fan-out and are allowed; a self-edge (from == to) is allowed (cycles are legal). It rejects only a zero from or to (a BuildError); endpoint existence and routing-ambiguity are deferred to Compile (§8), since an edge may be declared before its endpoint vertices are added.

func (*Graph[S]) Compile

func (g *Graph[S]) Compile(entry, finish VertexID, opts ...CompileOption) (*Runner[S], error)

Compile validates the whole graph (§8) and, on success, returns an immutable *Runner[S] bound to entry and finish and to a single CheckpointStore (§9). It runs every §8 check in the documented first-error order (see the file comment) and returns the FIRST typed violation; on success the returned Runner is non-nil. The store comes from WithStore if supplied non-nil, else a fresh default MemStore (the default is wired here, the composition point). A malformed graph fails secure with no Runner.

type GraphID

type GraphID uuid.UUID // stable definition id — pinned as a const by callers.

The identifier types of the engine (design §3). The four UUID-backed types are distinct named types — not aliases — so the compiler rejects passing a GraphID where a VertexID is wanted. Each delegates String/MarshalText/ UnmarshalText to the underlying uuid.UUID by conversion, so they serialize as readable canonical strings (not 16-int arrays) in checkpoints.

GraphID and VertexID are stable DEFINITION ids: a checkpoint frontier references vertices by VertexID and a resume rebuilds the graph from code, so they must be stable across restarts and are pinned as consts by callers via uuid.MustParse. They therefore have no generating constructor here — minting a fresh one per build would break resume. GraphRunID and VertexRunID are runtime instances minted fresh each run/execution via uuid.New (see the NewGraphRunID/NewVertexRunID constructors below).

func (GraphID) MarshalText

func (id GraphID) MarshalText() ([]byte, error)

MarshalText encodes the id as its canonical string form so JSON (and any other encoding.TextMarshaler consumer) emits a readable string.

func (GraphID) String

func (id GraphID) String() string

String returns the canonical 8-4-4-4-12 hyphenated encoding of the id.

func (*GraphID) UnmarshalText

func (id *GraphID) UnmarshalText(b []byte) error

UnmarshalText parses the canonical string form back into the id, returning a *uuid.ParseError (surfaced from the underlying uuid.UUID) on malformed input.

type GraphMismatchError

type GraphMismatchError struct {
	Expected GraphID
	Actual   GraphID
}

GraphMismatchError reports that a loaded checkpoint's GraphID does not match the runner's compiled graph, so it belongs to a different graph (§10.4).

func (*GraphMismatchError) Error

func (e *GraphMismatchError) Error() string

Error names the expected and actual graph identities.

type GraphOption

type GraphOption func(*graphConfig)

GraphOption configures a Graph at NewGraph (§7). It is non-generic so the same option value works for any state type S.

func WithVersion

func WithVersion(n uint64) GraphOption

WithVersion sets the graph's userVersion (§8.1), the manual bump callers use to invalidate resume after a BEHAVIOR change (task/selector/reducer/Pick logic) that topology hashing cannot see. Default is 0; last application wins.

type GraphRunExistsError

type GraphRunExistsError struct{ GraphRunID GraphRunID }

GraphRunExistsError reports an attempt to start a run whose GraphRunID already exists in the store (a duplicate start) (§18.2).

func (*GraphRunExistsError) Error

func (e *GraphRunExistsError) Error() string

Error names the already-existing run.

type GraphRunID

type GraphRunID uuid.UUID // runtime instance — minted per run via NewGraphRunID.

The identifier types of the engine (design §3). The four UUID-backed types are distinct named types — not aliases — so the compiler rejects passing a GraphID where a VertexID is wanted. Each delegates String/MarshalText/ UnmarshalText to the underlying uuid.UUID by conversion, so they serialize as readable canonical strings (not 16-int arrays) in checkpoints.

GraphID and VertexID are stable DEFINITION ids: a checkpoint frontier references vertices by VertexID and a resume rebuilds the graph from code, so they must be stable across restarts and are pinned as consts by callers via uuid.MustParse. They therefore have no generating constructor here — minting a fresh one per build would break resume. GraphRunID and VertexRunID are runtime instances minted fresh each run/execution via uuid.New (see the NewGraphRunID/NewVertexRunID constructors below).

func NewGraphRunID

func NewGraphRunID() (GraphRunID, error)

NewGraphRunID mints a fresh runtime GraphRunID for a new run, propagating any *uuid.GenerateError if the randomness source fails.

func (GraphRunID) MarshalText

func (id GraphRunID) MarshalText() ([]byte, error)

MarshalText encodes the id as its canonical string form so JSON (and any other encoding.TextMarshaler consumer) emits a readable string.

func (GraphRunID) String

func (id GraphRunID) String() string

String returns the canonical 8-4-4-4-12 hyphenated encoding of the id.

func (*GraphRunID) UnmarshalText

func (id *GraphRunID) UnmarshalText(b []byte) error

UnmarshalText parses the canonical string form back into the id, returning a *uuid.ParseError (surfaced from the underlying uuid.UUID) on malformed input.

type GraphRunMismatchError

type GraphRunMismatchError struct {
	Requested GraphRunID
	Actual    GraphRunID
}

GraphRunMismatchError reports that a loaded checkpoint's embedded Run.GraphRunID does not match the GraphRunID requested on Resume, so the store returned a checkpoint belonging to a DIFFERENT run. Resuming it would write into the embedded run's history; this fails secure before any task runs (§10.4). Requested is the id passed to Resume; Actual is the checkpoint's embedded Run.GraphRunID.

func (*GraphRunMismatchError) Error

func (e *GraphRunMismatchError) Error() string

Error names the requested and actual run identities.

type GraphRunState

type GraphRunState struct {
	GraphRunID    GraphRunID
	GraphID       GraphID
	GraphVersion  string // compiled-graph fingerprint (§8.1); a mismatch on resume → GraphVersionMismatchError
	Status        RunStatus
	Step          StepID
	Revision      uint64    // monotonic checkpoint sequence for this run (§10)
	CreatedAt     time.Time // Run() called
	StartedAt     time.Time // first super-step began
	UpdatedAt     time.Time // last checkpoint write
	CompletedAt   time.Time // set when Status == RunCompleted
	InterruptedAt time.Time // set when Status == RunInterrupted (most recent pause)
	CancelledAt   time.Time // set when Status == RunCancelled
	CancelReason  string    // Cancel(id, reason)'s reason, when cancelled
}

GraphRunState is the run-level instrumentation record: identity, status, step, revision, and lifecycle timestamps for one graph run (§4.1).

type GraphVersionKey

type GraphVersionKey struct {
	GraphID      GraphID
	GraphVersion string
}

GraphVersionKey routes work to a worker serving exactly this (GraphID, GraphVersion) — the version IS the route (§18.5). Both fields are comparable value types (GraphID is a [16]byte array, GraphVersion a string), so the struct is usable directly as a map key with no allocation.

type GraphVersionMismatchError

type GraphVersionMismatchError struct {
	Expected string
	Actual   string
}

GraphVersionMismatchError reports that a loaded checkpoint's GraphVersion fingerprint does not match the current compiled graph's, so a changed graph cannot resume an old checkpoint (§8.1, §10.4).

func (*GraphVersionMismatchError) Error

func (e *GraphVersionMismatchError) Error() string

Error names the expected and actual graph-version fingerprints.

type Halt

type Halt struct {
	GraphRunID GraphRunID
	Kind       HaltKind // defined in checkpoint.go
	Step       StepID
	Cause      error
}

Halt is a RUN-LEVEL routing/structural failure surfaced in a run Result — not a vertex pause (§10.3, §9.8). Kind classifies the structural cause; Step names the super-step at which the run halted; Cause wraps the underlying error.

type HaltKind

type HaltKind int

HaltKind classifies a run-level routing/structural halt (§10.1). Persisted in checkpoints, so the iota ordering is pinned.

const (
	HaltCondition        HaltKind = iota // Condition.Pick returned an error
	HaltUndeclaredTarget                 // Pick returned empty / a target outside Targets
	HaltDeadEnd                          // frontier drained without finish executing
	HaltMaxSteps                         // step budget (WithMaxSteps) exceeded
)

type HaltRecord

type HaltRecord struct {
	Kind  HaltKind
	Step  StepID
	Cause string // error message/type name
}

HaltRecord is the durable record of a run-level halt (§10.1, §9.8). It is set only in StepHalted checkpoints and is mutually exclusive with Interrupts.

type Hooks

type Hooks struct {
	OnRunStart     func(ctx context.Context, ev GraphRunState)
	OnRunFinish    func(ctx context.Context, ev GraphRunState)
	OnVertexStart  func(ctx context.Context, ev VertexState)
	OnVertexFinish func(ctx context.Context, ev VertexState)
	OnEdge         func(ctx context.Context, from, to VertexID, run GraphRunState)
	OnStep         func(ctx context.Context, run GraphRunState, activated int)
	OnCheckpoint   func(ctx context.Context, id GraphRunID, rev uint64, step StepID)
	OnInterrupt    func(ctx context.Context, iv Interruption)
	OnHalt         func(ctx context.Context, h Halt)
}

Hooks is a set of optional, purely observational lifecycle callbacks (§11). Every field is nil-able and independent, so a caller wires only the events it cares about (interface segregation). A callback receives the framework-owned instrumentation records of §4.1 and must not mutate engine state; it is fired best-effort and any panic it raises is recovered and discarded (§12.5).

type IdempotencyKey

type IdempotencyKey string

IdempotencyKey identifies one LOGICAL vertex execution, stable across in-process retries AND crash recovery. Side-effecting tasks pass it to external systems that support idempotency, so a re-run after a pre-checkpoint crash does not duplicate effects. It deliberately EXCLUDES VertexRunID and Attempt, which vary per concrete attempt.

type InterruptKind

type InterruptKind int

InterruptKind classifies a vertex pause (§10.1). It is defined HERE because InterruptRecord needs it; the public Interrupt struct in §10.3 (Phase 5) reuses this SAME type, so its iota ordering is a shared persisted contract.

const (
	Awaiting InterruptKind = iota // user-initiated pause (flow.Interrupt) — Info carries the reason
	Errored                       // failure pause (Pause-policy error) — Cause carries the message
)

type InterruptRecord

type InterruptRecord struct {
	Vertex       VertexID
	Kind         InterruptKind   // Awaiting | Errored
	Info         json.RawMessage `json:",omitempty"` // user-facing reason (Awaiting) — a serialization boundary
	Cause        string          // error message/type name (Errored)
	Continuation json.RawMessage `json:",omitempty"` // optional task continuation (StatefulInterrupt) — a serialization boundary
}

InterruptRecord is the durable record of one paused vertex (§10.1). Info and Continuation are serialization-boundary json.RawMessage fields carrying pre-encoded JSON (the user reason and an optional StatefulInterrupt continuation) that must round-trip untouched.

type Interruption

type Interruption struct {
	GraphRunID GraphRunID
	Vertex     VertexID
	Kind       InterruptKind // Awaiting | Errored (defined in checkpoint.go)
	Info       any           // user reason (Awaiting) — serialization boundary (§10.3)
	Cause      error         // underlying failure (Errored)
}

Interruption is the per-vertex pause surfaced in a run Result and to Hooks.OnInterrupt (§10.3). Info carries the user reason for an Awaiting pause and is an `any` because it is a serialization-boundary value (the same reason passed to Interrupt); Cause carries the underlying failure for an Errored pause. Exactly one is meaningful per Kind.

type MaxStepsExceededError

type MaxStepsExceededError struct {
	Max  int
	Step StepID
}

MaxStepsExceededError reports that the super-step budget (WithMaxSteps) was exhausted before the run completed; a run-level halt, not a vertex pause (§9.5, §9.8).

func (*MaxStepsExceededError) Error

func (e *MaxStepsExceededError) Error() string

Error names the budget and the step at which it was exceeded.

type MemStore

type MemStore struct {
	// contains filtered or unexported fields
}

MemStore is an in-memory CheckpointStore for development and tests (§10.2). It holds the ENCODED bytes of each checkpoint per run (so stored history is independent of any caller-held *Checkpoint) under a sync.RWMutex. It is the reference implementation of the CheckpointStore contract; a durable backend (SQLite/Postgres/NATS) honors the same behavior.

func NewMemStore

func NewMemStore() *MemStore

NewMemStore returns an empty in-memory CheckpointStore ready for use.

func (*MemStore) Append

func (s *MemStore) Append(ctx context.Context, cp *Checkpoint) error

Append performs a compare-and-append under the write lock: it serializes cp to an independent JSON copy, then accepts it iff cp.Run.Revision equals the next required revision (the current count of stored checkpoints for the run). Otherwise it returns a *RevisionConflictError with Expected = next required revision and Actual = the supplied revision. The revision check and the append are a single critical section, so concurrent appenders of the same next revision cannot both win.

func (*MemStore) History

func (s *MemStore) History(ctx context.Context, id GraphRunID) ([]*Checkpoint, error)

History returns a fresh slice of freshly decoded checkpoints for id ordered by revision under the read lock, or a *CheckpointNotFoundError if id has no checkpoints. The returned slice and every element are independent of stored state.

func (*MemStore) Latest

func (s *MemStore) Latest(ctx context.Context, id GraphRunID) (*Checkpoint, error)

Latest returns a freshly decoded copy of the highest-revision checkpoint for id under the read lock, or a *CheckpointNotFoundError if id has no checkpoints.

type MissingEntryError

type MissingEntryError struct {
	VertexID VertexID
	Role     string // "entry" or "finish"
}

MissingEntryError reports that a named role vertex is absent from the graph. Role is "entry" or "finish" (§8).

func (*MissingEntryError) Error

func (e *MissingEntryError) Error() string

Error names the absent role and the vertex it should have referenced.

type Reducer

type Reducer[S, O any] func(s *S, out O) error

Reducer folds a vertex's output O into the graph state S, returning an error to reject the fold. The coordinator applies it to a clone and commits only on a nil error, so a reducer that mutates then errors leaves S unchanged (§6.2).

type Resolver

type Resolver interface {
	// Resolve returns the handle registered under the exact (id, version) and true,
	// or (nil, false) if none is registered.
	Resolve(id GraphID, version string) (RunnerHandle, bool)
	// Keys returns one GraphVersionKey per registration — the exact set of versions
	// this worker serves, which Serve hands to Consume.
	Keys() []GraphVersionKey
}

Resolver resolves a (GraphID, GraphVersion) to its RunnerHandle and lists the keys this worker serves, so Serve consumes exactly those (§18.5/§18.6 — registration is implicit via Consume). It is the SUBSET of the registry surface Serve needs (interface segregation: Serve never touches Add/Manifest). registry.Registry satisfies it structurally, so no adapter is required.

type Result

type Result[S any] struct {
	Run        GraphRunState  // ids, status, step, revision, timestamps (§4.1)
	State      S              // final accumulated state
	Interrupts []Interruption // per-vertex pauses (§9.7) — nil on the happy path
	Halt       *Halt          // run-level halt (§9.8) — nil on the happy path
}

Result is the outcome of a Run/Resume (§9). Run is the framework-owned run state (ids, status, step, revision, timestamps); State is the final accumulated graph state S. Interrupts (per-vertex pauses, §9.7) and Halt (a run-level routing/structural halt, §9.8) are mutually exclusive and set only when the run is interrupted; on the happy path both are nil. Result.Run.Status is never RunRunning on return.

type ResumeTerminalError

type ResumeTerminalError struct{ Status RunStatus }

ResumeTerminalError reports an attempt to resume a run that is already in a terminal state (RunCompleted or RunCancelled), which cannot continue (§9.3, §12.4).

func (*ResumeTerminalError) Error

func (e *ResumeTerminalError) Error() string

Error names the terminal status that blocks the resume.

type RetryPolicy

type RetryPolicy struct {
	// MaxAttempts bounds the total number of task executions (the first try plus
	// retries). Values <= 1 mean no retry.
	MaxAttempts int

	// Backoff returns the delay to wait before the given 1-based attempt. A nil
	// Backoff means no delay between attempts. The execution wrapper (later phase)
	// invokes it under panic recovery (§12.5).
	Backoff func(attempt int) time.Duration

	// Retryable reports whether err warrants a retry. A nil Retryable means any
	// non-interrupt error is retryable (§12.2). The execution wrapper (later
	// phase) invokes it under panic recovery (§12.5).
	Retryable func(err error) bool
}

RetryPolicy is the per-vertex retry configuration (§12.2). A zero RetryPolicy is meaningful only as the absence of retry (vertexConfig.retry stays nil when WithRetry is not applied); when set, MaxAttempts bounds the re-runs, Backoff computes the delay before attempt n, and Retryable decides whether a given error is worth retrying.

type RevisionConflictError

type RevisionConflictError struct {
	GraphRunID GraphRunID
	Expected   uint64 // the revision the store required next (latest+1)
	Actual     uint64 // the revision actually supplied by the caller
}

RevisionConflictError reports a failed compare-and-append: the supplied revision was not the store's required next revision, because another writer advanced the run (concurrent-resume detection, §10.2). Expected is the revision the store required next (latest+1); Actual is the revision supplied.

func (*RevisionConflictError) Error

func (e *RevisionConflictError) Error() string

Error names the run and the expected-next versus supplied revisions.

type RouteRecord

type RouteRecord struct {
	From        VertexID
	To          []VertexID // chosen target(s); for a Condition, exactly what Pick returned
	Conditional bool       // true if chosen by Condition.Pick; false for a static AddEdge
}

RouteRecord durably records ONE routing decision — which source vertex activated which target(s) this step, and whether via a Condition (§10.1).

type RunInfo

type RunInfo struct {
	GraphID     GraphID
	GraphRunID  GraphRunID
	VertexID    VertexID
	VertexRunID VertexRunID
	Step        StepID
}

RunInfo is the identity an executing vertex reads from its context (§4.2). It names the graph, run, vertex, vertex execution, and super-step, and computes the vertex's IdempotencyKey.

func Info

func Info(ctx context.Context) (RunInfo, bool)

Info returns the coordinator-injected RunInfo identity for the calling vertex (§4.2). Returns (zero, false) if no RunInfo was injected.

func (RunInfo) IdempotencyKey

func (i RunInfo) IdempotencyKey() IdempotencyKey

IdempotencyKey derives the stable logical-execution key for this RunInfo. It excludes VertexRunID and Attempt so retries and crash-recovery re-runs of the same logical execution share one key (§4.1).

type RunOption

type RunOption func(*runConfig)

RunOption configures a single Run/Resume (§9). Options are applied in order over a defaulted runConfig.

func WithCheckpointEvery

func WithCheckpointEvery(g CheckpointGranularity) RunOption

WithCheckpointEvery selects the checkpoint cadence for this run (§10.1): PerVertex (default) or PerStep.

func WithConcurrency

func WithConcurrency(n int) RunOption

WithConcurrency bounds the number of vertices run in parallel within a super-step (§9.2). The default is GOMAXPROCS. A value <= 0 leaves the default in place. The bound is enforced in a later sub-task; today linear frontiers are size 1, so it is carried but not yet limiting.

func WithGraphRunID

func WithGraphRunID(id GraphRunID) RunOption

WithGraphRunID supplies the GraphRunID for this run instead of minting a fresh one (§9). Run rejects an id that already has checkpoint history with a GraphRunExistsError — continuing an existing run is Resume's job, not Run's.

func WithHooks

func WithHooks(h Hooks) RunOption

WithHooks registers a set of observational lifecycle callbacks for this run (§11). It is repeatable: each call appends another set and all fire, in registration order.

func WithMaxSteps

func WithMaxSteps(n int) RunOption

WithMaxSteps sets the super-step budget for this run (§9.5). The default is a generous safety bound; a value <= 0 leaves the default in place. Exceeding the budget as a run-level HaltMaxSteps is a later sub-task — today this is only a runaway guard.

type RunResult

type RunResult struct {
	Run        GraphRunState   // ids, status, step, revision, timestamps (§4.1)
	State      json.RawMessage // the marshaled final/last-checkpointed S
	Interrupts []Interruption  // per-vertex pauses (§9.7) — nil on the happy path
	Halt       *Halt           // run-level halt (§9.8) — nil on the happy path
}

RunResult is the graph-agnostic, JSON-friendly form of Result[S] (§18.1): State is the marshaled final/last-checkpointed S; Run, Interrupts and Halt are already non-generic and copied through unchanged. As in Result[S], Interrupts and Halt are mutually exclusive and nil on the happy path.

type RunStatus

type RunStatus int

RunStatus is the lifecycle status of a graph run. Its values are persisted in checkpoints, so the iota ordering is pinned (§4.1).

const (
	RunRunning     RunStatus = iota // in flight
	RunCompleted                    // finish executed and the frontier drained
	RunInterrupted                  // Awaiting or Errored — there is no RunFailed (§12)
	RunCancelled                    // terminal: Cancel(id) appended a final checkpoint (§18.2); cannot resume
)

func (RunStatus) String

func (s RunStatus) String() string

String renders a RunStatus as a human-readable token for hooks, HTTP, and logging. It satisfies fmt.Stringer; an unrecognized value falls back to its decimal form so the integer is never lost.

type Runner

type Runner[S any] struct {
	// contains filtered or unexported fields
}

Runner is the immutable, validated form of a Graph[S], produced by Compile (§8, §9). It is safe to reuse across concurrent runs. It holds the validated graph, the entry/finish roles, the GraphVersion fingerprint (§8.1), and the single CheckpointStore set at Compile (§9). Per-run behavior (hooks, concurrency, maxSteps, granularity) is supplied per call via RunOptions, not stored on the Runner, so the Runner itself stays immutable and reusable.

The store is the INTERFACE (CheckpointStore), not a concrete type (dependency inversion): the default MemStore is wired at Compile, the composition point, so the Runner never depends on a specific backend. Per §9 it is fixed at Compile — ALL operations (Run, Resume, Status, Get) use this one store; there is no per-run override.

func (*Runner[S]) Cancel

func (r *Runner[S]) Cancel(ctx context.Context, id GraphRunID, reason string, opts ...RunOption) error

Cancel terminates run id by appending a terminal RunCancelled checkpoint (§18.2) and firing OnRunFinish via the resolved hooks (opts → runConfig), so observers see the cancellation (§11). It does NOT execute the graph. A run already in a terminal status (RunCompleted or RunCancelled) cannot be cancelled and returns *ResumeTerminalError{Status} — cancel is terminal-once, and an already-cancelled run is rejected rather than treated as a silent no-op (fail loudly). A run with no checkpoints propagates *CheckpointNotFoundError.

The append is a compare-and-append at the latest revision + 1 (§10.2): if it loses to a concurrent writer the *RevisionConflictError is propagated unchanged. A cancelled run cannot resume — Resume's validateNotTerminal already rejects RunCancelled.

func (*Runner[S]) Get

func (r *Runner[S]) Get(ctx context.Context, id GraphRunID) (*Result[S], error)

Get returns the latest checkpoint's run record AND its decoded graph state S (§18.2), mirroring what Run/Resume returned for the run's current outcome. The decode of cp.State into S is the sanctioned serialization boundary; a failure is a *CheckpointDecodeError{Field:"State"}. It also reconstructs Result.Interrupts (StepPaused) or Result.Halt (StepHalted) best-effort from the latest checkpoint.

BEST-EFFORT caveat: unlike the live typed Result from Run/Resume, the reconstructed Interruption.Info is the RAW json.RawMessage (it cannot be re-typed without the original payload type — a caller that knows the type json.Unmarshals it) and Cause / Halt.Cause are string-wrapped (errors.New of the recorded message, not the original error chain).

func (*Runner[S]) GraphID

func (r *Runner[S]) GraphID() GraphID

GraphID returns the runner's stable definition identity (§3, §8.1): the pinned GraphID of the compiled graph. It is identity, NOT the compatibility key — use GraphVersion for resume compatibility.

func (*Runner[S]) GraphVersion

func (r *Runner[S]) GraphVersion() string

GraphVersion returns the compatibility fingerprint computed at Compile (§8.1): a sha256 of the graph's topology plus a ":userVersion" suffix. Resume compares it to the checkpoint's; any difference is a GraphVersionMismatchError, so a changed graph cannot resume an old checkpoint.

func (*Runner[S]) Resume

func (r *Runner[S]) Resume(ctx context.Context, id GraphRunID, payload any, opts ...RunOption) (*Result[S], error)

Resume continues an interrupted run from its latest checkpoint (§9.3). It loads Latest, validates it against the compiled graph (§10.4) — returning a typed engine error on ANY validation failure BEFORE any task runs — then reconstructs the coordinator and continues based on the checkpoint's Phase. payload is the live value every re-run paused vertex reads via ResumePayload[T] (§9.7); the vertices share the one payload. The error return is for engine/infrastructure and validation failures only; a fresh pause or halt on continuation is surfaced in the Result exactly like Run (§12.3).

func (*Runner[S]) Run

func (r *Runner[S]) Run(ctx context.Context, in S, opts ...RunOption) (*Result[S], error)

Run executes the compiled graph from in, driving the BSP super-step loop to a terminal state and returning the final Result (§9, §9.2). It mints a fresh GraphRunID unless WithGraphRunID supplies one; a supplied id that already has checkpoint history is rejected with *GraphRunExistsError. The error return is for engine/infrastructure failures only (store unavailable, id generation, non-serializable state) — task outcomes are not errors (§12.3). On the happy path Result.Run.Status is RunCompleted with no Interrupts and no Halt.

Example

ExampleRunner_Run is the godoc-facing, verified-by-output rendering of the §16 happy path: build the graph, compile to a Runner with an in-memory store, run it, and read the final state. The // Output line is checked by `go test`, so this doubles as a compile-and-behavior guarantee for the snippet a reader copies from the docs. (The Test* functions above carry the exhaustive assertions; this is the readable one-screen tour.)

package main

import (
	"context"
	"fmt"

	"github.com/looprig/core/uuid"
	"github.com/looprig/flow/pkg/flow"
)

var (
	exGraphID = flow.GraphID(uuid.MustParse("a1b2c3d4-0000-4000-8000-000000000000"))

	intakeID = flow.VertexID(uuid.MustParse("11111111-1111-4111-8111-111111111111"))

	sendToSalesID = flow.VertexID(uuid.MustParse("44444444-4444-4444-8444-444444444444"))
)

// Request is the §16 graph state S — the caller's domain blackboard threaded
// through the flow. The only mutation path is a vertex's reducer (the engine is
// the single writer); tasks are pure and never touch it. It must round-trip
// through JSON so it can be checkpointed and resumed, so every field is exported.
type Request struct {
	Question     string
	Draft        string
	Approved     bool
	NeedsExpert  bool
	Refined      int
	MaxRefines   int
	TicketID     string
	ExpertAnswer string
	Sent         bool
	LastErr      string
}

// query is RAGDraft's input; answer is its output.
type query struct{ Text string }
type answer struct{ Text string }

// ragDraft is the "RAG" drafting task: a deterministic stand-in that returns a
// canned answer for the question. The real engine would run retrieval + an LLM
// here; v1 is a plain function so the example is reproducible.
func ragDraft() *flow.FuncTask[query, answer] {
	return flow.NewFuncTask(func(_ context.Context, in query) (answer, error) {
		return answer{Text: "draft answer for: " + in.Text}, nil
	})
}

func main() {
	// buildGraph takes *testing.T for its fail-fast helpers, which a godoc example
	// (no T) cannot supply. So this example is a thin, self-contained mirror of the
	// happy path, built directly with the same exported API and checking errors
	// inline instead.
	g := flow.NewGraph[Request](exGraphID)
	_ = flow.AddVertex(g, intakeID, ragDraft(),
		func(s Request) query { return query{Text: s.Question} },
		func(s *Request, a answer) error { s.Draft = a.Text; s.Approved = true; return nil },
	)
	_ = flow.AddVertex(g, sendToSalesID,
		flow.NewFuncTask(func(_ context.Context, draft string) (bool, error) { return draft != "", nil }),
		func(s Request) string { return s.Draft },
		func(s *Request, ok bool) error { s.Sent = ok; return nil },
	)
	_ = g.AddEdge(intakeID, sendToSalesID)

	runner, err := g.Compile(intakeID, sendToSalesID, flow.WithStore(flow.NewMemStore()))
	if err != nil {
		panic(err)
	}
	res, err := runner.Run(context.Background(), Request{Question: "what is the SLA?"})
	if err != nil {
		panic(err)
	}

	fmt.Println(res.Run.Status)
	fmt.Println(res.State.Draft)
	fmt.Println(res.State.Sent)
}
Output:
Completed
draft answer for: what is the SLA?
true

func (*Runner[S]) Status

func (r *Runner[S]) Status(ctx context.Context, id GraphRunID) (GraphRunState, error)

Status returns the latest GraphRunState for id — status, step, revision, and timestamps — WITHOUT executing the graph or decoding the graph state S (§18.2). It is the cheapest control query: a single store.Latest read. A run with no checkpoints propagates the store's *CheckpointNotFoundError; any other store failure is propagated unchanged.

type RunnerHandle

type RunnerHandle interface {
	// GraphID returns the wrapped Runner's stable definition identity (§8.1).
	GraphID() GraphID
	// GraphVersion returns the wrapped Runner's compatibility fingerprint (§8.1).
	GraphVersion() string
	// Run decodes stateJSON into the graph state S and starts a run. A malformed
	// stateJSON is rejected at the decode boundary; an empty/nil stateJSON decodes
	// to the zero S.
	Run(ctx context.Context, stateJSON json.RawMessage, opts ...RunOption) (*RunResult, error)
	// Resume continues run id, passing payloadJSON to the run as the live Resume
	// payload (see the runnerHandle.Resume doc for the payload-typing nuance).
	Resume(ctx context.Context, id GraphRunID, payloadJSON json.RawMessage, opts ...RunOption) (*RunResult, error)
	// Status returns the latest GraphRunState for id without decoding S (§18.2).
	Status(ctx context.Context, id GraphRunID) (GraphRunState, error)
	// Get returns the latest run record with the marshaled current State (§18.2).
	Get(ctx context.Context, id GraphRunID) (*RunResult, error)
	// Cancel appends a terminal RunCancelled checkpoint for id (§18.2).
	Cancel(ctx context.Context, id GraphRunID, reason string, opts ...RunOption) error
}

RunnerHandle is the non-generic, JSON-in/out facade over a typed Runner[S] (§18.1). It lets a graph-agnostic caller (the registry/ingress) start, resume, query, and cancel any compiled graph by JSON alone. It mirrors the in-process control surface (Run/Resume/Status/Get/Cancel + GraphID/GraphVersion, §18.2) with S erased to json.RawMessage at the boundaries. Construct one with NewRunnerHandle.

func NewRunnerHandle

func NewRunnerHandle[S any](r *Runner[S]) RunnerHandle

NewRunnerHandle wraps a typed Runner[S] in the non-generic RunnerHandle facade (§18.1). The single type parameter S is captured at construction; thereafter the returned handle is graph-agnostic and JSON-in/out, so a registry keyed by (GraphID, GraphVersion) can hold handles for many different S uniformly.

type Selector

type Selector[S, I any] func(s S) I

Selector derives a vertex's input I from the graph state S. It is read-only: it must not mutate S (the coordinator passes the immutable step-base snapshot, §6.2).

type StepID

type StepID int // super-step index within a run: 0, 1, 2, …

The identifier types of the engine (design §3). The four UUID-backed types are distinct named types — not aliases — so the compiler rejects passing a GraphID where a VertexID is wanted. Each delegates String/MarshalText/ UnmarshalText to the underlying uuid.UUID by conversion, so they serialize as readable canonical strings (not 16-int arrays) in checkpoints.

GraphID and VertexID are stable DEFINITION ids: a checkpoint frontier references vertices by VertexID and a resume rebuilds the graph from code, so they must be stable across restarts and are pinned as consts by callers via uuid.MustParse. They therefore have no generating constructor here — minting a fresh one per build would break resume. GraphRunID and VertexRunID are runtime instances minted fresh each run/execution via uuid.New (see the NewGraphRunID/NewVertexRunID constructors below).

func (StepID) String

func (s StepID) String() string

String returns the decimal form of the super-step index. StepID is a plain int and serializes as a JSON number natively, so it has no TextMarshaler.

type StepPhase

type StepPhase int

StepPhase is the phase of a checkpoint within a super-step (§10.1). Its values are persisted in checkpoints, so the iota ordering is pinned.

const (
	StepRunning StepPhase = iota // step partly reduced — some vertices terminal, some not
	StepPaused                   // step boundary: ≥1 vertex paused (Interrupts set)
	StepRouted                   // step boundary: routing decisions produced the next Frontier
	StepHalted                   // step boundary: run-level routing/structural halt (Halt set)
)

type StoreError

type StoreError struct {
	Op  string // "Append" | "Latest" | "History"
	Err error
}

StoreError reports a failure of a CheckpointStore operation. Op names the operation ("Append", "Latest", or "History"); it wraps the store cause (§10.2, §12.3).

func (*StoreError) Error

func (e *StoreError) Error() string

Error names the failing store operation and the underlying cause.

func (*StoreError) Unwrap

func (e *StoreError) Unwrap() error

Unwrap returns the underlying store cause so errors.Is/As can inspect it.

type Task

type Task[I, O any] interface {
	Execute(ctx context.Context, in I) (O, error)
}

Task is the reusable, graph-agnostic unit of work (§5). Implementations honor the contract that they are pure with respect to graph state S (they touch no shared state — input is supplied, output is returned) and treat in as read-only (§4.2). The interface is intentionally minimal so new kinds plug in without engine changes.

type TaskFunc

type TaskFunc[I, O any] func(ctx context.Context, in I) (O, error)

TaskFunc is a plain typed In → Out computation. It is the function form a FuncTask wraps; the engine never calls it directly, only via Task.Execute.

type UndeclaredTargetError

type UndeclaredTargetError struct {
	From   VertexID
	Target VertexID // zero value denotes an empty return (no target picked)
}

UndeclaredTargetError reports that a condition's Pick returned a target that is not in its declared Targets set; a zero Target denotes an empty return (Pick returned no target at all), which is equally illegal (§9.5, §9.8).

func (*UndeclaredTargetError) Error

func (e *UndeclaredTargetError) Error() string

Error names the source vertex and the offending target, distinguishing an undeclared target from an empty return.

type UnknownVertexError

type UnknownVertexError struct{ VertexID VertexID }

UnknownVertexError reports that an edge, condition, error-route, or checkpoint frontier endpoint references a VertexID that is not in the compiled graph. Used both at build (§8) and at checkpoint validation on load (§10.4).

func (*UnknownVertexError) Error

func (e *UnknownVertexError) Error() string

Error names the unknown vertex.

type UnknownWorkOpError

type UnknownWorkOpError struct{ Op WorkOp }

UnknownWorkOpError reports a Work carrying a WorkOp outside the closed {OpRun, OpResume} domain (§18.5) — only constructible by a corrupt/forged Work crossing the transport. Per CLAUDE.md it is a concrete typed error so a caller can errors.As it; settle treats it as a PERMANENT failure (Ack-and-abandon) — a corrupt op can never dispatch, so Nacking it would be an infinite redelivery loop (the H1 poison-message DoS). The op is named for an operator log line.

func (*UnknownWorkOpError) Error

func (e *UnknownWorkOpError) Error() string

Error names the offending op for an operator log line.

type UnreachableVertexError

type UnreachableVertexError struct{ VertexID VertexID }

UnreachableVertexError reports that a vertex cannot be reached from the entry vertex (or that finish is unreachable); every vertex must be reachable (§8).

func (*UnreachableVertexError) Error

func (e *UnreachableVertexError) Error() string

Error names the unreachable vertex.

type VertexError

type VertexError struct {
	VertexID    VertexID
	VertexRunID VertexRunID
	Attempt     int
	Err         error
}

VertexError reports that a vertex's task failed, panicked, or timed out. It carries the logical vertex, the concrete execution, the attempt number, and the wrapped cause, and drives the vertex's error policy (§12.2, §12.4).

func (*VertexError) Error

func (e *VertexError) Error() string

Error names the vertex, its execution, the attempt, and the underlying cause.

func (*VertexError) Unwrap

func (e *VertexError) Unwrap() error

Unwrap returns the underlying cause so errors.Is/As can inspect it.

type VertexID

type VertexID uuid.UUID // stable definition id — pinned as a const by callers.

The identifier types of the engine (design §3). The four UUID-backed types are distinct named types — not aliases — so the compiler rejects passing a GraphID where a VertexID is wanted. Each delegates String/MarshalText/ UnmarshalText to the underlying uuid.UUID by conversion, so they serialize as readable canonical strings (not 16-int arrays) in checkpoints.

GraphID and VertexID are stable DEFINITION ids: a checkpoint frontier references vertices by VertexID and a resume rebuilds the graph from code, so they must be stable across restarts and are pinned as consts by callers via uuid.MustParse. They therefore have no generating constructor here — minting a fresh one per build would break resume. GraphRunID and VertexRunID are runtime instances minted fresh each run/execution via uuid.New (see the NewGraphRunID/NewVertexRunID constructors below).

func (VertexID) MarshalText

func (id VertexID) MarshalText() ([]byte, error)

MarshalText encodes the id as its canonical string form so JSON (and any other encoding.TextMarshaler consumer) emits a readable string.

func (VertexID) String

func (id VertexID) String() string

String returns the canonical 8-4-4-4-12 hyphenated encoding of the id.

func (*VertexID) UnmarshalText

func (id *VertexID) UnmarshalText(b []byte) error

UnmarshalText parses the canonical string form back into the id, returning a *uuid.ParseError (surfaced from the underlying uuid.UUID) on malformed input.

type VertexOption

type VertexOption[S any] func(*vertexConfig[S])

VertexOption configures a vertex binding's policy at AddVertex (§6.3). Options are applied in order, so a later option overrides an earlier one (last-wins).

func WithErrorPause

func WithErrorPause[S any]() VertexOption[S]

WithErrorPause selects the default Pause-on-error policy explicitly (§12.2). It clears any error route set by an earlier WithErrorRoute on the same binding (last-wins ordering), so the vertex pauses as Errored rather than routing.

func WithErrorRoute

func WithErrorRoute[S any](handler VertexID, record Reducer[S, error]) VertexOption[S]

WithErrorRoute routes an exhausted/unrecoverable vertex error to handler, first folding the error into S via record (§12.2). Applying it overrides any prior error policy on the binding (last-wins).

func WithRetry

func WithRetry[S any](p RetryPolicy) VertexOption[S]

WithRetry attaches a retry policy to the vertex binding (§12.2). The policy is pure config here; the bounded re-run loop is a later phase.

func WithTimeout

func WithTimeout[S any](d time.Duration) VertexOption[S]

WithTimeout sets a per-vertex deadline on the task's ctx (§12.2). Cancellation is cooperative; 0 means no deadline.

type VertexRunID

type VertexRunID uuid.UUID // runtime instance — minted per vertex execution.

The identifier types of the engine (design §3). The four UUID-backed types are distinct named types — not aliases — so the compiler rejects passing a GraphID where a VertexID is wanted. Each delegates String/MarshalText/ UnmarshalText to the underlying uuid.UUID by conversion, so they serialize as readable canonical strings (not 16-int arrays) in checkpoints.

GraphID and VertexID are stable DEFINITION ids: a checkpoint frontier references vertices by VertexID and a resume rebuilds the graph from code, so they must be stable across restarts and are pinned as consts by callers via uuid.MustParse. They therefore have no generating constructor here — minting a fresh one per build would break resume. GraphRunID and VertexRunID are runtime instances minted fresh each run/execution via uuid.New (see the NewGraphRunID/NewVertexRunID constructors below).

func NewVertexRunID

func NewVertexRunID() (VertexRunID, error)

NewVertexRunID mints a fresh runtime VertexRunID for a vertex execution, propagating any *uuid.GenerateError if the randomness source fails.

func (VertexRunID) MarshalText

func (id VertexRunID) MarshalText() ([]byte, error)

MarshalText encodes the id as its canonical string form so JSON (and any other encoding.TextMarshaler consumer) emits a readable string.

func (VertexRunID) String

func (id VertexRunID) String() string

String returns the canonical 8-4-4-4-12 hyphenated encoding of the id.

func (*VertexRunID) UnmarshalText

func (id *VertexRunID) UnmarshalText(b []byte) error

UnmarshalText parses the canonical string form back into the id, returning a *uuid.ParseError (surfaced from the underlying uuid.UUID) on malformed input.

type VertexState

type VertexState struct {
	VertexID      VertexID
	VertexRunID   VertexRunID
	Step          StepID
	Status        VertexStatus
	Attempt       int
	CreatedAt     time.Time
	StartedAt     time.Time
	CompletedAt   time.Time // Status == VertexDone
	InterruptedAt time.Time // Status == VertexInterrupted
	FailedAt      time.Time // Status == VertexFailed
	Err           string
}

VertexState is the per-vertex-execution instrumentation record: identity, status, attempt, and lifecycle timestamps (§4.1).

func Self

func Self(ctx context.Context) (VertexState, bool)

Self returns the coordinator-injected VertexState record for the calling vertex (§4.2). Returns (zero, false) if no VertexState was injected.

type VertexStatus

type VertexStatus int

VertexStatus is the lifecycle status of a single vertex execution. Its values are persisted in checkpoints, so the iota ordering is pinned (§4.1).

const (
	VertexPending VertexStatus = iota
	VertexRunning
	VertexDone
	VertexInterrupted
	VertexFailed
)

func (VertexStatus) String

func (s VertexStatus) String() string

String renders a VertexStatus as a human-readable token for hooks, HTTP, and logging. It satisfies fmt.Stringer; an unrecognized value falls back to its decimal form so the integer is never lost.

type Work

type Work struct {
	Key        GraphVersionKey
	GraphRunID GraphRunID
	Op         WorkOp
	Input      json.RawMessage
}

Work is one unit of run/resume work submitted to the control plane (§18.5). The GraphRunID is pre-minted by the submitter (ingress, §18.3) so the submit is async-first: the GraphRunID can be returned to the caller immediately while a worker picks the work up later. Input is the initial state JSON for OpRun, or the resume payload JSON for OpResume — a json.RawMessage so the seam stays a pure transport that neither parses nor trusts the payload (the worker's Runner validates it on the way into typed business logic).

type WorkOp

type WorkOp uint8

WorkOp distinguishes the two kinds of work the control plane carries: starting a fresh run (OpRun) versus resuming an interrupted one (OpResume). It is a uint8 so it is a cheap comparable value with a small, closed domain.

const (
	// OpRun starts a fresh run: Work.Input is the initial state JSON.
	OpRun WorkOp = iota
	// OpResume resumes an interrupted run: Work.Input is the resume payload JSON.
	OpResume
)

func (WorkOp) String

func (o WorkOp) String() string

String renders the operation for logs and test failures. An out-of-range value (only constructible by a deserialization fault) renders explicitly rather than silently, so a corrupt Work is visible in a log line.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL