Skip to content

state

import "github.com/stablekernel/crucible/state"

Package state is the pure, abstract state machine kernel of the Crucible suite — a portable, domain-agnostic engine for forging event-driven services in Go.

Import path: github.com/stablekernel/crucible/state

state is an abstract, domain-agnostic state machine kernel built once and usable everywhere. It is generic over state, event, and context types (conceptually Machine[S, E, C]) and knows nothing about any particular application domain. The same machine definition runs unchanged from a unit test, a synchronous request handler, and an asynchronous event consumer.

The kernel is stdlib-only. It imports only the Go standard library and performs no injected IO. This is the extreme end of the suite’s “thin seams, no-op defaults, no forced dependencies” philosophy: a tiny dependency graph is a tiny attack surface, and the kernel stays a clean, extractable unit forever. The stdlib-only boundary is enforced mechanically by an import-graph test.

Firing an event returns (newState, effects, trace) without performing any IO. The caller dispatches the effects however it likes — publish to a broker, write to a store, call an RPC. Effects are abstract at the kernel (the kernel never inspects the payload) and concrete at your domain layer. This is what makes one machine usable across tests, handlers, and consumers without change.

An effect is discriminated data: every kernel-emitted effect reports a stable, serializable Kind (the KindedEffect interface) and serializes to an EffectEnvelope (kind + payload + meta), so effects can be journaled, deduped, rendered, and routed across a serialization boundary by kind rather than by Go type. An EffectRegistry decodes an envelope back to a concrete effect; built-in kinds are pre-registered and a host registers its own through RegisterEffect. An unknown effect kind is preserved on load and rejected only at dispatch, never silently dropped. Effects stay data the host applies — the kernel never executes them.

The canonical machine is a serializable definition IR: pure data, lossless to and from JSON. Behavior is not embedded as closures in the IR; every guard, action, and effect is a named reference with serializable params, bound to host-provided implementations through a registry at freeze time. Binding fails loudly if any reference does not resolve.

This is the config/implementation split: structure is dual-authored (code or, eventually, a visual UI) while behavior is always code, surfaced to authors as a named palette. The Go DSL and a future UI are two front-ends that emit the same IR; a machine authored in Go and a machine loaded from JSON are the same machine.

The lifecycle API uses a small “foundry” verb vocabulary. The noun stays plain — the type is a Machine — only the verbs are themed:

  • Forge — open the builder DSL.
  • Temper — optional, non-failing dev-time diagnostics pass (lint / static analysis), chainable before Quench.
  • Quench — freeze the definition into an immutable Machine; the always-call finalizer that binds refs and panics on misconfiguration.
  • Cast — pour a running instance from the machine.
  • Fire — send an event to an instance and advance it.
  • Assay — check that an externally-constructed entity is legally in a given state.

Operations that favor discoverability over metaphor stay plain: PlanPath, Requirements, Trace, and the To*/LoadFromJSON serializers.

Context (the C type) is updated only through an assign — a pure reducer, AssignFn[C], that takes the prior context by value, the triggering event, and the ref’s static params and returns the next context. This is the sole context-mutation site (the G1 contract): guards and actions receive context read-only, actions emit effects-as-data and never write context, and the kernel folds the assigns declared on a transition’s exit, transition, and entry phases — in that order, declaration order within each phase, each reducer seeing the prior result — committing the folded value to the instance at the end of the step. Wire an assign with the Assign transition verb or the OnEntryAssign / OnExitAssign state verbs; register the reducer with Builder.Reducer (or Registry.Assign). A service result or actor done-data reaches its onDone transition’s assign through the re-fired done event’s payload (AssignCtx.Event), delivered with the WithEventData fire option — no host side channel.

Use a VALUE context type (Machine[S, E, Order], not Machine[S, E, *Order]). Under a value C the kernel’s structural guarantees hold: a guard or action that writes the context copy it receives mutates a throwaway, so the instance is untouched (read-only falls out for free), and a service or actor observes a point-in-time snapshot value at invocation rather than an alias that could leak later mutations. A pointer C stays compilable as an ergonomics/performance escape hatch, but it forfeits these guarantees: the copy is a copied pointer to the same value, so a guard/action can mutate through the alias and a service can observe later mutations. With a pointer C the consumer owns that discipline; the structural read-only, clean-replay, and deterministic-analysis contracts hold only for a value C.

The pure step is also a deterministic step: given the same machine, the same starting configuration, and the same event, a Fire produces the same effects, the same context, and the same Trace — byte-for-byte, every time. Purity keeps a Fire from reading the clock or doing IO; determinism additionally freezes the ORDER in which the step emits effects, folds assigns, and advances states. This is what makes a Trace journalable and a run replayable: a consumer that records the event stream can re-derive the identical effect/context sequence later.

The emission order is frozen as follows, and is golden-locked by a regression test so a reorder is a visible failure:

  • Cascade phases run exit -> transition -> entry, in that fixed order. The exit cascade runs innermost-first (the source leaf, then its ancestors up to but not including the least common ancestor); the entry cascade runs outermost-first (the least common ancestor’s child down to the target, then the descent into the target’s initial children). A reentering self/ancestor transition exits up to and including its target, then re-enters it.

  • Within a single state’s phase, effects (actions) run before assigns (reducers), each in declaration order. The folded context of a phase becomes the input to the next phase’s assigns; the value committed to the instance at the end of the step is the fold of every phase’s assigns in cascade order. Effects read the context as it stood at phase entry (read-only).

  • Parallel regions are broadcast in REGION DECLARATION ORDER. When several regions handle the same event in one macrostep, the earlier-declared region’s effects and assigns are emitted and folded before the later one’s, so a cross-region assign fold is deterministic and order-stable. Likewise a parallel target’s entry descends its regions in declaration order, and the active configuration lists region leaves in that same order.

  • The run-to-completion (RTC) microstep interleave is fixed: after the triggering transition settles, the macrostep drains raised internal events FIRST (FIFO, in the order they were raised), then fires one enabled eventless (“always”) transition, and repeats until the configuration is stable. Raised events always precede eventless transitions within a microstep. The internal queue is macrostep-local, so the interleave is reproducible and Fire stays pure. A cycle is bounded and fails fast with a typed overflow error rather than spinning.

  • Auto-emitted lifecycle effects keep their cascade slot: a ScheduleAfter / StartService / SpawnActor for an entered state is appended after that state’s entry effects and assigns; a CancelScheduled / StopService / StopActor for an exited state after its exit effects and assigns — all in exit/entry order.

The Trace records each of these in order: EffectsEmitted and AssignsApplied list the per-step effects and folds in emission order, ExitedStates and EnteredStates the cascade in execution order, and Microsteps the RTC interleave (each raised event and eventless step, plus per-region markers) as it happened. FireResult’s Effects slice carries the same effects, in the same order, as data.

The ordering is structural, not incidental. Every emission, fold, and cascade walk iterates declaration-ordered slices — states, transitions, regions, children, refs — never a Go map. The kernel’s maps (node and state indices, the behavior registry) are consulted only for keyed lookup, never iterated to drive order, so no map-iteration nondeterminism can leak into a Fire. This holds under a value context (see above); a pointer context forfeits the clean-replay guarantee because a guard or action can mutate through the shared alias.

The public API follows the suite’s functional-options convention: every public constructor and operation takes a variadic option tail. Required inputs stay positional; everything optional or extensible is an option; a zero-option call reads clean. New capability arrives as a new option — additive-only, never a signature or breaking change. The kernel idiom is fail-fast by default, with resilience and aggregation available opt-in via options.

Observability is Trace-first: the structured Trace is the canonical surface, recording matched transitions, guard and policy evaluations, emitted effects, and the outcome as pure data. An optional WithLogger(*slog.Logger) (no-op by default) is the only logging seam; the kernel never logs unless asked and never imports a third-party logger. Determinism is preserved by injecting time and identifier seams rather than calling time.Now or rand directly.

As a library, the kernel never exits the process — it never calls os.Exit or log.Fatal on an operational error. Panics are reserved strictly for programmer error at construction time (Quench).

The kernel implements the Forge/Temper/Quench build path, Cast/Fire pure step semantics with guards, actions, typed errors and an always-recorded Trace, Assay/Requirements, PlanPath (BFS), FireSeq/FireEach batch helpers, and lossless ToJSON/LoadFromJSON/Provide round-trip.

Hierarchical and orthogonal states extend the same surface: a state may declare nested substates with an initial child (compound states) or parallel regions (orthogonal states). Superstates nest to arbitrary depth — a SuperState block may contain another SuperState block — and parallel regions may contain nested compounds. Events resolve child-first and bubble to ancestors; orthogonal regions each receive the event and resolve independently; transitions run the standard exit/entry cascade across the hierarchy; and final states drive done-event completion, including the all-regions-final join for parallel states. The hierarchy serializes, so a nested machine round-trips through JSON losslessly.

History pseudo-states (shallow and deep) let a transition re-enter a compound state’s last active configuration rather than its initial child; the pseudo-states serialize while the recorded per-instance configuration is runtime state threaded through the pure Fire step.

Delayed (`after`) transitions are drivable: entering a state with an `after` transition emits a ScheduleAfter effect and exiting it a CancelScheduled effect (auto-cancel-on-exit), while Fire stays pure — a host Scheduler driver owns the real timer and re-fires the delayed event, with a deterministic FakeClock for testing.

Invoked services (`invoke`) are drivable: entering a state that declares an invoke emits a StartService effect and exiting it before the service completes emits a StopService effect (auto-stop-on-exit), while Fire stays pure — a host ServiceRunner runs the bound service and re-fires the invocation’s onDone (with the result) or onError (with the error) back through Fire, with a deterministic settle-by-id harness for testing.

Child-machine actors are live: a state may invoke another Machine as a sub-actor (InvokeActor) or spawn one dynamically (Spawn), driven by a host ActorSystem that runs the child, routes its done-data to the parent’s onDone and its failure to the parent’s onError, and carries inter-actor messages (SendTo / SendParent / Respond / Forward) between mailboxes — all as host-dispatched effects, so the pure Fire step still owns no mailbox and performs no IO. When a child fails and the parent declared no onError, the failure does not vanish: the default is escalate-to-parent — a typed *ActorEscalation recorded on the system (LastEscalation), surfaced to the inspector, climbed up the supervision chain, and optionally routed to a host EscalationHandler. Supervision STRATEGIES (restart / resume / backoff) layer additively on that frozen default.

A transition guard is authored at one of three graduated tiers, all bindings of the same frozen Guard data contract (context + params -> bool), so a machine mixes tiers freely and the tier is a property of the guard, not the kernel:

  • Core — a small, dependency-free expression built with the in-package builder (Field(”…”).Eq/Lt/In/…, And/Or/Not, StateIn) over a fixed vocabulary — boolean composition, typed compare, membership, and state-tests. It lowers to a serializable GuardNode tree (GuardKindCore) the kernel evaluates IN-KERNEL, adds no dependency, serializes losslessly, and stays transparent to tooling and analysis.
  • Rich — a mature embedded expression engine (CEL) for cross-stack evaluation and richer logic (arithmetic, map construction) than Core admits. It lives in the opt-in github.com/stablekernel/crucible/state/expr module so the kernel itself stays stdlib-only; a Rich guard is checked against the ContextSchema at freeze time and serializes as a GuardKindRich node.
  • Escape — a plain Go func registered as a named guard (Registry.Guard). It is the always-available, maximally-expressive tier; it is opaque to the analyzer and does not cross a serialization boundary, so reserve it for logic the declarative tiers cannot express.

Core and Rich guards are STRUCTURALLY read-only — an expression cannot mutate context. An Escape Go-func guard is read-only by CONTRACT (documented; under a value context the kernel’s value semantics make a mutation a throwaway anyway).

A machine may declare a ContextSchema — a serializable description of the context type’s fields and their types. It is the type contract the declarative guard tiers check against: a Core or Rich expression that references a field is validated against the schema at freeze time rather than failing at run time, and the schema is the data contract a cross-language evaluator binds the same machine to. It is optional; an Escape Go-func guard needs none.

A definition carries a SchemaVersion (the IR wire form), an optional machine ID and definition version, and serializes losslessly with unknown fields preserved, so a newer document round-trips through an older loader without corruption and a higher MAJOR schema version is refused rather than guessed at. An instance snapshots to a versioned Snapshot and restores under a lenient version posture (accept-and-upgrade within a compatible range, reject across a major boundary; strict machine-version checking is opt-in via RejectMachineVersionMismatch). The Trace records a structured EventPayload alongside the human Event label so a recorded event stream replays the exact event — the journal/durable-execution seam the deterministic step makes sound.

Example (Connection Lifecycle)

Example_connectionLifecycle drives the connection lifecycle exemplar end-to-end through the real host runtime — an ActorSystem, a Scheduler on a FakeClock, and a ServiceRunner wired around one instance. It shows a transient dial failure that backs off and retries on a timer, a guarded admission into a parallel Connected configuration, a worker actor that runs a task to completion, and an eventless run-to-completion shutdown. The connHarness (in exemplar_test.go) wires the three drivers and routes every Fire’s effects through them.

ctx := context.Background()
h := newConnHarness()
fmt.Println("start:", fmtConfig(h.inst.Configuration()))
// Connect arms the dial service; the first attempt fails, falling back to
// Backoff, where a connect-timeout timer is armed.
h.fire(ctx, Connect)
h.settleDial(ctx, false)
fmt.Println("dial failed:", fmtConfig(h.inst.Configuration()))
// Advancing the fake clock past the timeout fires the delayed Retry edge, which
// re-enters Connecting; the second dial succeeds and the guarded Dialed edge
// admits the instance into the parallel Connected configuration.
h.advancePastTimeout(ctx)
h.settleDial(ctx, true)
fmt.Println("connected:", fmtConfig(h.inst.Configuration()))
// Assigning work spawns a worker actor; stepping it to completion routes the
// result back through the parent, draining the Work region.
h.fire(ctx, Assign)
h.runWorkers(ctx)
fmt.Println("work done:", fmtConfig(h.inst.Configuration()))
// Close runs to completion through the eventless edge into the final state.
h.fire(ctx, Close)
fmt.Println("closed:", fmtConfig(h.inst.Configuration()), "final:", h.inst.InFinal())
// Output:
// start: Disconnected
// dial failed: Backoff
// connected: Beating,WorkIdle
// work done: Beating,Drained
// closed: Closed final: true
start: Disconnected
dial failed: Backoff
connected: Beating,WorkIdle
work done: Beating,Drained
closed: Closed final: true

Built-in effect kinds. Each is the stable discriminant the matching kernel effect reports from Kind() and carries on its serialized envelope. They share the reserved crucible. namespace so a host’s own effect kinds never collide with the kernel’s. These are part of the wire contract and are closed-enum extended per the unknown-variant policy: a decoder that meets an unrecognized kind preserves it (see UnknownEffect) and rejects it only at dispatch.

const (
EffectKindSpawnActor = "crucible.spawnActor"
EffectKindStopActor = "crucible.stopActor"
EffectKindStartService = "crucible.startService"
EffectKindStopService = "crucible.stopService"
EffectKindScheduleAfter = "crucible.scheduleAfter"
EffectKindCancelScheduled = "crucible.cancelScheduled"
EffectKindSendTo = "crucible.sendTo"
EffectKindSendParent = "crucible.sendParent"
EffectKindRespondToSender = "crucible.respondToSender"
EffectKindForwardEvent = "crucible.forwardEvent"
)

CurrentSchemaVersion is the IR wire-format version this build emits and accepts. It is a major.minor string: a higher minor (same major) loads with unknown fields preserved for forward-compat, while a higher major is refused by LoadFromJSON as *ErrUnsupportedSchema. Every document ToJSON emits is stamped with this version, so an IR on the wire is self-describing.

const CurrentSchemaVersion = "1.0"

CurrentSnapshotVersion is the snapshot-format schema version stamped by Snapshot and validated by Restore. It is the major.minor schema generation of the Snapshot envelope encoded as major*1000 + minor, so a single int both orders versions and exposes the major for the restore-version posture: a snapshot is restorable within the same major (snapshotMajor), and a major mismatch is rejected. Version 1 is (1*1000 + 0); a future additive field bumps the minor, a breaking change bumps the major.

const CurrentSnapshotVersion = 1 * snapshotMajorScale

TransportInProcess is the v1 default binding transport: the behavior is a Go func held in the host registry and called in-process. It is the only transport the kernel dispatches at v1; every other transport is reserved.

const TransportInProcess = "in-process"

func ActorID[S comparable](machine string, from S, idx int) string

ActorID returns the stable identifier the kernel assigns to the child-machine actor invocation at index idx on owning state `from` of machine `machine` when the invocation declares no explicit ID. A host or test uses it to correlate a SpawnActor with a later StopActor, to Deliver an event to the actor, or to assert which actor a StopActor targets.

func BindingTransportOf(d Descriptor) string

BindingTransportOf returns the binding transport a descriptor declares, defaulting to in-process when the descriptor has no Binding (the common case) or an empty transport. It is the canonical reader of the reserved binding default.

func InvokeID[S comparable](machine string, from S, idx int) string

InvokeID returns the stable identifier the kernel assigns to the invoked service at index idx on owning state `from` of machine `machine` when the invocation declares no explicit ID. A host or test uses it to correlate a StartService with a later StopService, or to assert which service a StopService targets.

func MarshalSnapshot[S comparable, E comparable, C any](snap Snapshot[S, E, C], opts ...SnapshotCodecOption[C]) ([]byte, error)

MarshalSnapshot serializes snap to JSON, encoding its context through codec (or the default JSON codec when codec is nil). It is the explicit serialization entry point when a non-JSON-marshalable context needs a custom codec; for a JSON-marshalable context, json.Marshal(snap) works directly via the snapshot’s own MarshalJSON.

func ScheduleID[S comparable](machine string, from S, idx int) string

ScheduleID returns the stable schedule identifier the kernel assigns to the delayed (`after`) transition at index idx on source state `from` of machine `machine`. A host or test uses it to correlate a ScheduleAfter with a later Cancel, or to assert which timer a CancelScheduled targets.

ActionBinding turns an action request into emitted effects. The in-process binding wraps an ActionFn.

type ActionBinding[C any] interface {
EvalAction(ctx context.Context, req ActionRequest[C]) (ActionResult, error)
}

ActionCtx is passed to a bound action function at run time.

type ActionCtx[C any] struct {
Entity C
Params map[string]any
}

ActionFn produces an effect (or error) for a transition.

type ActionFn[C any] func(ctx ActionCtx[C]) (Effect, error)

ActionRequest is the serializable invocation envelope for an action: the named ref, its params, and the read-only context projection.

type ActionRequest[C any] struct {
Name string
Params map[string]any
Context ContextView
}

ActionResult is the action’s serializable result. Effects carries the emitted effects-as-data (today an action emits exactly one). Actions never write context: under the value-semantics contract a context change is expressed only through an Assign, whose AssignResult.Context carries the new value. The channel an action formerly reserved for a context delta now lives on the assign binding, the sole context writer.

type ActionResult struct {
Effects []Effect
}

ActorBehavior creates a fresh child-machine actor instance bound to the given input. It is the actor-palette analog of a ServiceFn: a host registers one per child-machine src name, and the ActorSystem calls it to spawn an actor when it absorbs a SpawnActor effect for that src. The returned ActorInstance erases the child’s own (S, E, C) generic parameters behind the ActorInstance interface, so a parent of any type can host children of any type. The input is the SpawnActor Input is the actor input; a behavior typically Casts its child machine with a WithInitialState derived from input.

type ActorBehavior func(input map[string]any) (ActorInstance, error)

ActorEscalation is the typed failure an unhandled child-machine actor error raises to its parent. It is produced when an actor fails (an error settlement, a behavior that could not start, a panic recovered while the actor stepped, or an explicit SettleError) and the spawning parent declared no onError event for that actor: rather than swallow the failure, the ActorSystem escalates it.

It is the v1 default escalation signal — the actor-model analog of an unhandled crash propagating up a supervision hierarchy. It wraps the underlying child error (so errors.Unwrap and errors.As reach it) and identifies the failed actor.

type ActorEscalation struct {
// ActorID is the registry id of the actor that failed.
ActorID string
// SystemID is the failed actor's system-scoped name, empty when it had none.
SystemID string
// Src is the actor ref name the failed actor was spawned from.
Src string
// ParentID is the id of the actor the failure escalated TO: the failed actor's
// parent actor, or empty when it escalated to the parent instance (the system
// root), which has no actor id of its own.
ParentID string
// Err is the underlying child failure that triggered the escalation.
Err error
}

func (e *ActorEscalation) Error() string

Error renders the escalation, naming the failed actor and the wrapped cause.

func (e *ActorEscalation) Unwrap() error

Unwrap returns the underlying child failure so errors.Is / errors.As reach the cause an escalation wraps.

ActorInstance is a running child actor as the ActorSystem sees it, with the child’s own (S, E, C) generic parameters erased. A host obtains one by wrapping a Cast child *Instance with NewActor; the deterministic test driver and the production driver both drive actors purely through this interface.

type ActorInstance interface {
// DeliverFire fires one event through the actor, returning whether the actor
// reached its final state and the output it exposes on completion. The event is
// the actor's own event type, passed type-erased; an implementation type-asserts
// it and ignores an event of the wrong type (a no-op, mirroring the kernel's
// effect-type guards). A backing *Instance implementation also surfaces the
// SpawnActor / StopActor effects the child itself emitted, so the system can run
// nested actors — those are returned via ChildEffects.
DeliverFire(ctx context.Context, event any) (done bool, output any)
// ChildEffects returns the actor effects the actor emitted on its most recent
// DeliverFire (and on its initial entry): the SpawnActor / StopActor lifecycle
// effects so the ActorSystem can spawn or stop the actor's own children, and the
// SendTo / SendParent / RespondToSender / ForwardEvent communication effects so
// the system can route the actor's outbound messages. It returns a fresh slice
// each call and drains the buffer.
ChildEffects() []Effect
// Output returns the actor's completion output once it has reached its final
// state, or nil before then. It lets a host expose a snapshot's output.
Output() any
}

func NewActor[S comparable, E comparable, C any](inst *Instance[S, E, C], output func(*Instance[S, E, C]) any) ActorInstance

NewActor adapts a Cast child *Instance into an ActorInstance an ActorSystem can run as a child-machine actor. output, when non-nil, extracts the actor’s v5 `output` from the child instance once it reaches its final state (typically reading the child entity); pass nil for an actor whose completion carries no output. The returned ActorInstance is what an ActorBehavior returns. The child’s initial-entry actor effects (StartEffects) are buffered immediately, so the system spawns any actors the child invokes on entry.

ActorKind tags an Invocation as either a host-run service or a child-machine actor. The default (ActorKindService) preserves the invoked-services contract verbatim; ActorKindMachine marks the invocation as spawning a child MACHINE actor, so entering the owning state emits a SpawnActor effect instead of a StartService effect, and the host’s ActorSystem (not a ServiceRunner) runs it.

type ActorKind int

Actor kinds. ActorKindService is the invoked-services default (a host-run unit of work); ActorKindMachine invokes a child machine as an actor.

const (
ActorKindService ActorKind = iota
ActorKindMachine
)

ActorPhase distinguishes the lifecycle point of an InspectActor event.

type ActorPhase string

const (
// ActorSpawned marks an actor created and started.
ActorSpawned ActorPhase = "spawned"
// ActorStopped marks an actor stopped (completed, errored, or auto-stopped on
// exit).
ActorStopped ActorPhase = "stopped"
// ActorEscalated marks an unhandled child-actor failure escalating to the parent
// because no onError was wired for it (the escalate-to-parent default). The
// event's ActorID/ActorSrc name the failed actor; the typed failure itself is
// retrievable through the ActorSystem's LastEscalation.
ActorEscalated ActorPhase = "escalated"
)

ActorRef is the runtime handle a machine stores in its context to address a spawned actor later (an actor ref). It is created by the ActorSystem when the actor is spawned and surfaced to the spawning machine through the system’s API, never through the IR — refs are runtime, not serializable definition. A ref carries the actor’s ID (and optional system-scoped SystemID) so the holder can Deliver events to it or read its snapshot through the system.

A ref is an OPAQUE, structured handle, not a raw index or positional slot: a holder must treat it as opaque and resolve it only through the ActorSystem API (Ref / RefBySystemID / Deliver / Stop), never by constructing one from a slice position or relying on its ID as an externally-meaningful integer. Construction stays the system’s job. This keeps the ref remote-ready: a future ref that denotes an actor in another system, process, or host carries additional locator data (a system name, a transport address) additively, without breaking any holder that already treats the ref opaquely. {ID, SystemID, Node} is the in-process projection of that fuller locator shape; Node is empty for a local actor and names the owning node for a remote one.

type ActorRef struct {
// ID is the actor's registry key in the ActorSystem.
ID string
// SystemID is the optional system-scoped name the actor registered under
// (its systemId); empty when the actor was spawned without one.
SystemID string
// Src is the actor ref name the actor was spawned from, for diagnostics.
Src string
// Node is the locator of the host that owns the actor: empty for an actor in
// the holder's own in-process ActorSystem, and the owning node's identifier
// for an actor on another host. The in-process ActorSystem leaves it empty;
// a distributed host (crucible/cluster) stamps it when it mints a remote ref
// and routes delivery by it. It is the additive locator the opaque-ref
// contract reserves, so adding it breaks no holder that treats the ref
// opaquely.
Node string
}

ActorSystem is the reusable host-driver that turns the kernel’s SpawnActor / StopActor effects into running child-machine actors, owns each actor’s mailbox, routes delivered events into mailboxes, steps actors via Fire, and re-fires the parent’s onDone / onError when a child completes or fails. It is concurrency-safe. Construct one per parent instance with NewActorSystem, then Register the child-machine behaviors that resolve SpawnActor Src refs; drive it by passing each Fire’s effects (and the parent’s StartEffects) to Absorb, and step actors with Deliver / Step.

In the deterministic form the system records each spawned actor and steps it only when the test calls Deliver / Step, so actor machines are exercised with no real concurrency; a production host instead runs each actor’s Step on its own goroutine fed by the mailbox.

type ActorSystem[S comparable, E comparable, C any] struct {
// contains filtered or unexported fields
}

func NewActorSystem[S comparable, E comparable, C any](parent *Instance[S, E, C]) *ActorSystem[S, E, C]

NewActorSystem returns an ActorSystem driving parent: the instance whose SpawnActor / StopActor effects spawn and stop child actors, and through whose Fire a completed child’s onDone / onError is routed. Register child-machine behaviors with Register before absorbing spawn effects.

func (s *ActorSystem[S, E, C]) Absorb(ctx context.Context, effects []Effect)

Absorb scans effects, spawning an actor for each SpawnActor (resolving its Src against the palette and running the child machine) and stopping the actor for each StopActor (auto-stop-on-exit, recursively stopping the actor’s children). It is how a host wires Fire’s output back into the system; call it with the effects of every Fire (and once with the parent’s StartEffects for the initial state). A SpawnActor whose OnDone/OnError is not the parent’s event type still spawns the actor (a fire-and-forget child) but routes no completion event.

A SpawnActor whose Src does not resolve against the palette is settled immediately as an error: its OnError (when usable) is fired through the parent so the parent routes onError rather than hanging, mirroring the ServiceRunner’s unbound-service handling.

func (s *ActorSystem[S, E, C]) AbsorbFor(ctx context.Context, event any, effects []Effect)

AbsorbFor is Absorb for the effects of a host-driven parent Fire(event): it additionally lets a ForwardEvent the parent emits forward event verbatim to a child. Use it (rather than Absorb) when the parent itself runs forwardTo on a host-injected event; Absorb suffices for sendTo / sendParent / respond and all lifecycle effects.

func (s *ActorSystem[S, E, C]) Deliver(ctx context.Context, ref ActorRef, event any) bool

Deliver routes event into the mailbox of the actor identified by ref, then drains the actor (Step) so the delivered event is processed and any resulting completion is routed to the parent. It returns whether the actor was found running. It is the delivery mechanism the sendTo / sendParent / respond / forwardTo action sugar routes through; a host (or a test) may also call it directly to inject an event into an actor from outside.

func (s *ActorSystem[S, E, C]) DeliverByID(ctx context.Context, id string, event any) bool

DeliverByID is Deliver keyed by raw actor id, for a host that tracks ids rather than refs.

func (s *ActorSystem[S, E, C]) IDs() []string

IDs returns the ids of all live actors, sorted, for deterministic host iteration (e.g. delivering to or stepping every actor in a stable order).

func (s *ActorSystem[S, E, C]) IsRunning(id string) bool

IsRunning reports whether an actor with the given id is live.

func (s *ActorSystem[S, E, C]) LastError() error

LastError returns the error the most recently settled actor produced, or nil when the last settlement was a success or none has occurred.

func (*ActorSystem[S, E, C]) LastEscalation

Section titled “func (*ActorSystem[S, E, C]) LastEscalation”
func (s *ActorSystem[S, E, C]) LastEscalation() *ActorEscalation

LastEscalation returns the most recent escalation the system recorded, or nil when no child failure has escalated. It is the always-on observable record of the escalate-to-parent default: even with no inspector and no handler wired, an unhandled child failure is retrievable here rather than silently lost.

It is LAST-WRITTEN-WINS, including across a single escalation that climbs the supervision chain: a child -> parent -> grandparent climb rewrites this field at each level, so after the climb it holds the topmost level reached, not the originating failure. Wire an inspector (or an EscalationHandler) when you need the FULL record — the inspector stream observes every level of every escalation in order; LastEscalation is the convenience snapshot of the most recent one.

func (s *ActorSystem[S, E, C]) LastOutput() (any, bool)

LastOutput returns the output the most recently settled actor produced, and true when that settlement was a success. The parent action bound to an actor’s onDone transition reads it to consume the child’s output; it is valid only during the synchronous parent Fire the settlement triggers. It returns false after a failure or before any settlement.

func (s *ActorSystem[S, E, C]) Ref(id string) (ActorRef, bool)

Ref returns the ActorRef for the running actor under id, and whether such an actor is running. The spawning machine stores the ref in its context (the host’s spawn action reads it from the system after Absorb) to address the actor later.

func (*ActorSystem[S, E, C]) RefBySystemID

Section titled “func (*ActorSystem[S, E, C]) RefBySystemID”
func (s *ActorSystem[S, E, C]) RefBySystemID(systemID string) (ActorRef, bool)

RefBySystemID returns the ActorRef for the actor registered under the given its systemId, and whether one is running. It lets a sibling address an actor by its well-known system name rather than by spawn id.

func (s *ActorSystem[S, E, C]) Register(src string, behavior ActorBehavior) *ActorSystem[S, E, C]

Register binds a child-machine behavior under src in the system’s actor palette, so a SpawnActor whose Src.Name is src resolves to behavior. It is the actor-model analog of Registry.Service: a host registers each child machine it can spawn. Registering returns the system for chaining.

func (*ActorSystem[S, E, C]) RestoreActors

Section titled “func (*ActorSystem[S, E, C]) RestoreActors”
func (s *ActorSystem[S, E, C]) RestoreActors(ctx context.Context, actors map[string]json.RawMessage) error

RestoreActors re-establishes the system’s child actors from the snapshots SnapshotActors produced, recursively: each actor is re-spawned from the system’s palette under its original id, its captured state reloaded (resuming it in place without re-running entry actions) when it was resumable, and its nested children restored beneath it. A not-yet-done actor whose Src does not resolve against the palette is skipped (the host registered a different palette); a done actor is not re-spawned. Register the same child-machine behaviors before calling it, exactly as for the original Absorb.

An actor recorded as not resumable (its ActorInstance did not implement Snapshotter) is re-spawned fresh rather than resumed — the one deferred actor-tree depth, flagged on the snapshot’s Resumed field.

func (s *ActorSystem[S, E, C]) Running() int

Running reports the number of live (spawned, not-stopped, not-completed) actors. A test asserts on it to confirm an actor spawned or was auto-stopped on exit.

func (s *ActorSystem[S, E, C]) SettleError(ctx context.Context, id string, err error) (FireResult[S], bool)

SettleError fails the running actor under id explicitly (e.g. a host-detected child crash), routing the parent’s onError. It returns the parent FireResult and true, or false when id is not running or routes no onError. When no onError was wired, the failure escalates to the parent as a typed ActorEscalation rather than being swallowed (the G3 default), so the returned false still means “no onError event fired” — not “the failure was lost”.

func (*ActorSystem[S, E, C]) SnapshotActors

Section titled “func (*ActorSystem[S, E, C]) SnapshotActors”
func (s *ActorSystem[S, E, C]) SnapshotActors() (map[string]json.RawMessage, error)

SnapshotActors captures the runtime state of every live child actor the system runs, recursively (each actor’s own spawned children are captured beneath it), as a JSON document keyed by actor id. It is the actor-tree companion to Instance.Snapshot: a host that persists a parent instance also calls SnapshotActors to persist the parent’s spawned children, and stores the result under the parent snapshot’s Actors map. It is a pure read of the system’s actor registry and never fires or mutates an actor.

Call it at a quiescent point (after draining mailboxes with Step), so no in-flight mailbox backlog is lost. An actor whose ActorInstance does not implement Snapshotter is recorded as present but not resumable (Resumed false) and is re-spawned fresh on RestoreActors.

func (s *ActorSystem[S, E, C]) Step(ctx context.Context, id string) []FireResult[S]

Step drains the mailbox of the actor under id, firing each queued event through the actor in order. When the actor reaches its final state it is settled: the parent’s onDone event (carrying the child’s output) is fired through the parent and the resulting effects absorbed; nested-child effects the actor emits are absorbed too. It returns the parent FireResults produced by completion routing, in order (empty when the actor did not complete). Step is safe to call with an empty mailbox (a no-op) and is how the deterministic driver advances an actor; a production driver runs it from the actor’s own goroutine.

func (s *ActorSystem[S, E, C]) Stop(ref ActorRef)

Stop stops the actor identified by ref (and its children), so a machine that holds an ActorRef can explicitly tear an actor down. Stopping an unknown actor is a no-op.

func (*ActorSystem[S, E, C]) WithActorInspector

Section titled “func (*ActorSystem[S, E, C]) WithActorInspector”
func (s *ActorSystem[S, E, C]) WithActorInspector(insp Inspector) *ActorSystem[S, E, C]

WithActorInspector wires a live observer sink fed the ActorSystem’s actor-lifecycle and inter-actor message inspection events — actor spawned / stopped, and message sent / delivered (the actor-to-actor flavor of an event). Pass the same Inspector also wired to the parent instance (WithInspector) to observe the whole system on one sink. It is off by default; an un-inspected system pays nothing.

func (*ActorSystem[S, E, C]) WithEscalationHandler

Section titled “func (*ActorSystem[S, E, C]) WithEscalationHandler”
func (s *ActorSystem[S, E, C]) WithEscalationHandler(handler EscalationHandler) *ActorSystem[S, E, C]

WithEscalationHandler registers handler as the system’s escalation handler, invoked for each child-actor failure that escalates because no onError was wired. It is off by default — the default escalation behavior (record on the system plus an InspectActor event when an inspector is present) needs no handler — so an unwired system still never swallows a failure. Registering returns the system for chaining.

AssayError aggregates one or more failing requirements found by Assay.

type AssayError struct {
Failures []RequirementFailure
}

func (e *AssayError) Error() string

AssayOption configures Assay.

type AssayOption func(*assayConfig)

func Aggregate() AssayOption

Aggregate makes Assay collect all failing requirements in one pass instead of failing fast at the first. It is a pure directive option (it carries no value), so it drops the With prefix that value-carrying options keep — matching Strict and CollectAll.

AssignBinding turns an assign request into the next context value. The in-process binding wraps an AssignFn, reading the prior context off the in-process context projection; a future out-of-process binding marshals the request across its transport. EvalAssign is synchronous so the fold stays callable inside the pure commit step.

type AssignBinding[C any] interface {
EvalAssign(ctx context.Context, req AssignRequest[C]) (AssignResult[C], error)
}

AssignCtx is passed to a bound assign reducer at run time. Entity is the prior context by value (the reducer’s input); the reducer returns the next context. Event is the triggering event payload — the runtime event for an ordinary transition, or the service/actor result for a service/actor onDone transition. Params is the assign ref’s static configuration.

type AssignCtx[C any] struct {
Entity C
Event any
Params map[string]any
}

AssignFn is the sole context writer: a total pure reducer producing the next context from the prior context (by value), the triggering event, and the ref’s static params. It emits no effect and returns no error; it observes context read-only through the copy it receives and yields the new value as its return.

type AssignFn[C any] func(in AssignCtx[C]) C

AssignRequest is the serializable invocation envelope for an assign: the named ref, its params, the triggering event, and the read-only context projection the reducer folds.

type AssignRequest[C any] struct {
Name string
Params map[string]any
Event any
Context ContextView
}

AssignResult is the assign’s serializable result: the new context value. It is the write-side mirror of the read-only ContextView and carries the full folded context (delta encoding is a later additive optimization on this envelope).

type AssignResult[C any] struct {
Context C
}

BatchResult is the result of a batch fire (FireSeq / FireEach).

type BatchResult[S comparable] struct {
Steps []FireResult[S]
Trace Trace
Err error
}

BindingSpec describes how a named behavior is backed. Transport names the invocation transport, defaulting to in-process when empty. Meta is a reserved per-binding extension namespace (e.g. a sandbox fuel budget, an endpoint).

Transport follows the closed-enum extension policy: a transport this build does not recognize is preserved verbatim on round-trip (so a newer producer’s binding survives an older client) and would be rejected only at dispatch — and no non-in-process dispatch path exists at v1. Unknown top-level keys are likewise preserved through extra.

type BindingSpec struct {
Transport string `json:"transport,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
// contains filtered or unexported fields
}

func (s BindingSpec) MarshalJSON() ([]byte, error)

MarshalJSON encodes a BindingSpec, merging its preserved unknown keys back in with stable key ordering.

func (s *BindingSpec) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a BindingSpec and captures any unknown keys into extra so they survive re-serialization.

Builder is the Forge DSL front-end. It builds the IR and registers implementations by name.

type Builder[S comparable, E comparable, C any] struct {
// contains filtered or unexported fields
}

func Forge[S comparable, E comparable, C any](name string, opts ...ForgeOption) *Builder[S, E, C]

Forge opens a builder.

Example

ExampleForge builds a document-approval machine with the Forge DSL and fires a single event, showing the resulting state and the effect the transition emitted.

m := buildDocMachine()
doc := &Document{Status: Draft}
res := m.Cast(doc).Fire(context.Background(), Submit)
fmt.Println("state:", res.NewState)
fmt.Println("effects:", res.Effects)
// Output:
// state: Submitted
// effects: [{submitted}]
state: Submitted
effects: [{submitted}]

func (b *Builder[S, E, C]) Action(name string, fn ActionFn[C], opts ...DescribeOption) *Builder[S, E, C]

Action registers a named action into the builder’s palette. An optional Describe option attaches palette metadata, mirroring Registry.Action.

func (b *Builder[S, E, C]) Actor(name string, opts ...DescribeOption) *Builder[S, E, C]

Actor declares a named actor behavior in the builder’s palette for discovery. Like Registry.Actor it records palette metadata only — the runnable behavior binds at the host ActorSystem — so it never affects Quench binding or lint.

func (b *Builder[S, E, C]) After(delay time.Duration) *Builder[S, E, C]

After opens a delayed (“after”) transition from the most-recent state: a transition that the host’s runtime fires once `delay` elapses while the source state stays active. Chain On(event).GoTo(target) to name the delayed event the host re-fires and the target it lands in (When/Do as usual). On entering the source state the kernel emits a ScheduleAfter effect; on exiting it before the delay elapses, a CancelScheduled effect (auto-cancel-on-exit). The kernel never sleeps — the host owns the timer and feeds the delayed event back through Fire. This is the DSL form of a delayed (after) transition.

func (b *Builder[S, E, C]) Always() *Builder[S, E, C]

Always opens an eventless (“always”) transition from the most-recent state. It carries no triggering event and is auto-fired by the run-to-completion loop whenever its guards pass and the state is active, within the firing macrostep. Chain GoTo/When/Do as usual. This is the DSL form of an eventless transition.

func (b *Builder[S, E, C]) Assign(assignName string, params ...map[string]any) *Builder[S, E, C]

Assign attaches a named context-reducer ref with params to the most-recent transition. The reducer folds onto the instance’s context when the transition fires — the sole context-mutation site under the value-semantics contract. It is distinct from Do: Do emits an effect, Assign computes the next context. The referenced reducer is registered separately by Builder.Reducer (alias of Registry.Assign); this WIRES a registered reducer by name onto the transition.

Example

ExampleBuilder_Assign demonstrates the assign reducer — the sole context writer. Under value-semantics context, a guard or action receives a copy of the context and cannot change the instance; only an Assign, a pure reducer returning the next context, updates it. The reducer reads the triggering event from AssignCtx.Event and its static configuration from AssignCtx.Params.

package main
import (
"context"
"fmt"
"github.com/stablekernel/crucible/state"
)
// basket is a value-semantics context: an Assign returns a new basket, and guards and
// actions receive a copy they cannot use to mutate the instance.
type basket struct {
Total int
}
// ExampleBuilder_Assign demonstrates the assign reducer — the sole context writer.
// Under value-semantics context, a guard or action receives a copy of the context
// and cannot change the instance; only an Assign, a pure reducer returning the next
// context, updates it. The reducer reads the triggering event from AssignCtx.Event
// and its static configuration from AssignCtx.Params.
func main() {
m := state.Forge[string, string, basket]("checkout").
Reducer("addItem", func(in state.AssignCtx[basket]) basket {
c := in.Entity
if price, ok := in.Params["price"].(int); ok {
c.Total += price
}
return c
}).
State("shopping").
State("paid").
Initial("shopping").
Transition("shopping").On("add").GoTo("shopping").
Assign("addItem", map[string]any{"price": 300}).
Transition("shopping").On("checkout").GoTo("paid").
Quench()
inst := m.Cast(basket{}, state.WithInitialState[string]("shopping"))
inst.Fire(context.Background(), "add")
inst.Fire(context.Background(), "add")
fmt.Println(inst.Entity().Total)
}
600

func (b *Builder[S, E, C]) Cancel(id string) *Builder[S, E, C]

Cancel attaches the kernel Cancel built-in to the most-recent transition: when the transition fires, the kernel emits a CancelScheduled effect for the given schedule id, so a machine can explicitly cancel a pending delayed (`after`) event before its delay elapses. The id is the ScheduleAfter ID the host received; ScheduleID derives it for a known source state and delayed-edge index. Canceling an unknown id is a host-side no-op. The built-in needs no host registration, mirroring the stateIn guard built-in.

func (b *Builder[S, E, C]) CurrentStateFn(fn func(C) S) *Builder[S, E, C]

CurrentStateFn declares how to derive an instance’s current state.

func (b *Builder[S, E, C]) DefaultTo(target S) *Builder[S, E, C]

DefaultTo sets the fallback target of the most-recent history pseudo-state, entered when its owning compound has no recorded history yet. It is a no-op (recorded as a lint at Quench) when the most-recent state is not a history pseudo-state.

func (b *Builder[S, E, C]) Do(actionName string, params ...map[string]any) *Builder[S, E, C]

Do attaches a named action ref with params to the most-recent transition.

func (b *Builder[S, E, C]) EndRegion() *Builder[S, E, C]

EndRegion closes the most-recent Region block.

func (b *Builder[S, E, C]) EndSuperState() *Builder[S, E, C]

EndSuperState closes the most-recent SuperState block.

func (b *Builder[S, E, C]) Final() *Builder[S, E, C]

Final marks the most-recent state as terminal.

func (b *Builder[S, E, C]) Forbid(event E) *Builder[S, E, C]

Forbid declares that the most-recent state blocks the given event: the event is consumed and ignored there and does NOT bubble to ancestors, distinct from having no handler (which bubbles). This is the DSL form of a `on: { E: undefined }`. A forbidden transition takes no target, guards, or effects.

func (b *Builder[S, E, C]) ForbidAny() *Builder[S, E, C]

ForbidAny declares a forbidden wildcard: every event not otherwise handled is consumed and ignored at the most-recent state instead of bubbling. This is the DSL form of a forbidden wildcard transition.

func (b *Builder[S, E, C]) ForwardTo(targetID string, opts ...SendOption) *Builder[S, E, C]

ForwardTo attaches the kernel forwardTo built-in to the most-recent transition: when the transition fires, the kernel emits a ForwardEvent effect so the host’s ActorSystem forwards the event the emitting actor is currently handling, verbatim, to the actor registered under targetID. Address an actor by its system-scoped id instead with WithSendToSystemID. The built-in needs no host registration. This is the DSL form of forwarding the current event to another actor.

func (b *Builder[S, E, C]) GoTo(to S) *Builder[S, E, C]

GoTo sets the target of the most-recent transition.

func (b *Builder[S, E, C]) Guard(name string, fn GuardFn[C], opts ...DescribeOption) *Builder[S, E, C]

Guard registers a named guard into the builder’s palette. An optional Describe option attaches palette metadata, mirroring Registry.Guard.

func (b *Builder[S, E, C]) History(name S, kind HistoryType) *Builder[S, E, C]

History declares a history pseudo-state inside the current SuperState block. The pseudo-state remembers the owning compound’s last active configuration: HistoryShallow restores the compound’s last active direct child, HistoryDeep restores its full nested leaf configuration. Transition to it (by name) to re-enter the remembered configuration instead of the compound’s Initial. Use DefaultTo to declare the target entered when no history has been recorded yet; without it the resolver falls back to the compound’s Initial.

A history pseudo-state is structure, not a leaf: it never appears in the active configuration and is not eligible as a compound’s Initial. Declaring one outside a SuperState block is a Quench lint.

func (b *Builder[S, E, C]) Initial(name S) *Builder[S, E, C]

Initial sets the entry state. At the top level it sets the machine’s initial state; inside a SuperState or Region block it sets that block’s initial child.

func (b *Builder[S, E, C]) Invoke(src string, onDone, onError E, opts ...InvokeOption) *Builder[S, E, C]

Invoke declares an invoked service on the most-recent state (an `invoke`). src names the service in the registry (bind it with Service); onDone and onError name the events the host re-fires through Fire when the service completes or fails, routed by ordinary transitions from this state. Configure the input passed to the service and an explicit id with the variadic InvokeOptions (WithInput, WithInvokeID); omitting WithInvokeID derives a stable id via InvokeID. On entering this state the kernel emits a StartService effect; on exiting it before the service completes, a StopService effect (auto-stop-on-exit). The kernel never runs the service — a host ServiceRunner does, keeping Fire pure.

func (b *Builder[S, E, C]) InvokeActor(src string, onDone, onError E, opts ...InvokeOption) *Builder[S, E, C]

InvokeActor declares a child-MACHINE actor invoked while the most-recent state is active (invoke of a child machine). src names the child-machine factory registered in the host’s ActorSystem actor palette; onDone and onError name the events the host re-fires through the PARENT’s Fire when the child reaches its final state (carrying its output) or fails (carrying the error), routed by ordinary transitions from this state. Configure the input passed to the child, an explicit id, and a system-scoped id with WithInput / WithInvokeID / WithSystemID. On entering this state the kernel emits a SpawnActor effect; on exiting it before the child completes, a StopActor effect (auto-stop-on-exit). The kernel never runs the actor — a host ActorSystem does, keeping Fire pure. Unlike Invoke (a host-run service), the src here is bound at the ActorSystem, not the registry, so it is not subject to the registry’s unbound-ref lint.

func (b *Builder[S, E, C]) On(event E) *Builder[S, E, C]

On sets the triggering event of the most-recent transition. When no transition is currently open — or the open one already has its event set (a completed `.On(…).GoTo(…)` clause) — On opens a fresh transition from the most-recent state. This lets the hierarchical DSL read `.SubState(X).On(e1).GoTo(Y).On(e2).GoTo(Z)` and `.SubState(X).On(e).GoTo(Y)` without an explicit Transition call.

func (b *Builder[S, E, C]) OnAny() *Builder[S, E, C]

OnAny opens a wildcard (catch-all) transition from the most-recent state. It matches any event no specific On-keyed transition of the state handles, and is the lowest-priority candidate — tried only after every specific match fails, before the event bubbles to an ancestor. Chain GoTo/When/Do/Reenter/Raise as usual. This is the DSL form of a wildcard transition.

func (b *Builder[S, E, C]) OnDone(actionName string, params ...map[string]any) *Builder[S, E, C]

OnDone attaches a named done-action ref to the most-recent state. It runs when the state completes — a compound state when its active leaf is final, a parallel state when every region is final.

func (b *Builder[S, E, C]) OnEntry(actionName string, params ...map[string]any) *Builder[S, E, C]

OnEntry attaches a named entry-action ref to the most-recent state.

func (b *Builder[S, E, C]) OnEntryAssign(assignName string, params ...map[string]any) *Builder[S, E, C]

OnEntryAssign attaches a named context-reducer ref to the most-recent state’s entry phase. It folds onto the instance’s context when the state is entered, after the transition’s assigns — the assign sibling of OnEntry.

func (b *Builder[S, E, C]) OnExit(actionName string, params ...map[string]any) *Builder[S, E, C]

OnExit attaches a named exit-action ref to the most-recent state.

func (b *Builder[S, E, C]) OnExitAssign(assignName string, params ...map[string]any) *Builder[S, E, C]

OnExitAssign attaches a named context-reducer ref to the most-recent state’s exit phase. It folds onto the instance’s context when the state is exited, before the transition’s assigns — the assign sibling of OnExit.

func (b *Builder[S, E, C]) OwnedBy(owner string) *Builder[S, E, C]

OwnedBy tags the most-recent state’s ownership.

func (b *Builder[S, E, C]) Palette() []Descriptor

Palette returns the registry’s discoverable descriptor set — every registered guard, action, service, and declared actor behavior — sorted deterministically. It is the Builder-side convenience for Registry.Palette, surfacing the palette of a DSL-authored machine before Quench.

func (b *Builder[S, E, C]) Quench(opts ...QuenchOption) *Machine[S, E, C]

Quench binds refs, lints, and freezes into an immutable Machine. It panics on any misconfiguration (programmer error) with a file:line pointer.

func (b *Builder[S, E, C]) Raise(events ...E) *Builder[S, E, C]

Raise attaches internal events to the most-recent transition. After the transition’s effects run, each raised event is processed within the same Fire macrostep by the run-to-completion loop, before Fire returns. This is the DSL form of raising an internal event.

func (b *Builder[S, E, C]) Reducer(name string, fn AssignFn[C], opts ...DescribeOption) *Builder[S, E, C]

Reducer registers a named assign reducer into the builder’s palette — the sole context writer, wired onto a transition with the Assign DSL verb or onto a state with OnEntryAssign / OnExitAssign. It is the builder-side registration of an assign (the Do verb wires an Action that Action registers; the Assign verb wires a reducer that Reducer registers), forwarding to Registry.Assign. An optional Describe option attaches palette metadata.

func (b *Builder[S, E, C]) Reenter() *Builder[S, E, C]

Reenter marks the most-recent transition external: a self- or ancestor- targeted transition that would otherwise be internal (the v5 default) instead runs the full exit/entry cascade of its target. This is the DSL form of the v5 `reenter: true`.

func (b *Builder[S, E, C]) Region(name string) *Builder[S, E, C]

Region opens an orthogonal region inside the current SuperState block. States declared until the matching EndRegion belong to the region, and Initial names the region’s initial state.

func (b *Builder[S, E, C]) Requires(req Requirement[C]) *Builder[S, E, C]

Requires attaches a requirement to the most-recent state.

func (b *Builder[S, E, C]) Respond(event E) *Builder[S, E, C]

Respond attaches the kernel respond built-in to the most-recent transition: when the transition fires, the kernel emits a RespondToSender effect so the host’s ActorSystem delivers event back to the sender of the event currently being handled (the actor that sent it via SendTo / ForwardTo). When the current event has no identifiable sender it is a host-side no-op. The built-in needs no host registration. This is the DSL form of replying to an event’s origin (the `respond` / `sendBack`).

func (b *Builder[S, E, C]) SendParent(event E) *Builder[S, E, C]

SendParent attaches the kernel sendParent built-in to the most-recent transition: when the transition fires, the kernel emits a SendParent effect so the host’s ActorSystem delivers event to the emitting actor’s parent. Emitted by a top-level machine with no parent it is a host-side no-op. The built-in needs no host registration. This is the DSL form of sending an event to the parent.

func (b *Builder[S, E, C]) SendTo(targetID string, event E, opts ...SendOption) *Builder[S, E, C]

SendTo attaches the kernel sendTo built-in to the most-recent transition: when the transition fires, the kernel emits a SendTo effect so the host’s ActorSystem delivers event to the actor registered under targetID. Address an actor by its system-scoped id instead with WithSendToSystemID. The built-in needs no host registration, mirroring Spawn / Cancel. This is the DSL form of `sendTo(target, event)`.

func (b *Builder[S, E, C]) Service(name string, fn ServiceFn[C], opts ...DescribeOption) *Builder[S, E, C]

Service registers a named invoked-service implementation into the builder’s palette, bound by an invoke’s Src ref. An unbound service ref fails Quench with the typed *ErrUnboundRef, mirroring guards and actions. An optional Describe option attaches palette metadata.

func (b *Builder[S, E, C]) Spawn(src, id string, opts ...SpawnOption) *Builder[S, E, C]

Spawn attaches the kernel spawn built-in to the most-recent transition: when the transition fires, the kernel emits a SpawnActor effect so a machine creates an actor dynamically (spawn). src names the child-machine factory in the host’s ActorSystem actor palette; id is the actor’s registry key (the holder later stores the ActorSystem-returned ActorRef in its context to address it). Configure input and a system-scoped id with the SpawnOptions. The built-in needs no host registration, mirroring Cancel. The ActorSystem creates and runs the actor; routing the spawned actor’s done/error is configured with WithSpawnOnDone / WithSpawnOnError.

func (b *Builder[S, E, C]) State(name S) *Builder[S, E, C]

State declares a state node. Inside a SuperState or Region block it declares a substate of that block (equivalent to SubState); at the top level it declares a top-level state.

func (b *Builder[S, E, C]) StopActor(id string) *Builder[S, E, C]

StopActor attaches the kernel stop-actor built-in to the most-recent transition: when the transition fires, the kernel emits a StopActor effect for the given actor id, so a machine can explicitly stop a spawned actor before its natural completion (stopping an actor). Stopping an unknown id is a host-side no-op. The built-in needs no host registration, mirroring Cancel.

func (b *Builder[S, E, C]) StopChild(id string) *Builder[S, E, C]

StopChild attaches the kernel stopChild built-in to the most-recent transition: when the transition fires, the kernel emits a StopActor effect for the given actor id, so a machine can explicitly stop a spawned child actor (the `stopChild`). It is the action-level twin of StopActor and shares its effect; stopping an unknown id is a host-side no-op. The built-in needs no host registration.

func (b *Builder[S, E, C]) SubState(name S) *Builder[S, E, C]

SubState declares a substate of the current SuperState or Region block.

func (b *Builder[S, E, C]) SuperState(name S) *Builder[S, E, C]

SuperState declares a compound (hierarchical) state and opens its block. The substates declared until the matching EndSuperState become its children, and Initial inside the block names the child entered when the superstate is entered.

func (b *Builder[S, E, C]) Temper(opts ...TemperOption) []Diagnostic

Temper runs a non-failing diagnostics pass over the builder’s current definition, returning the same findings Quench would panic on — as data.

func (b *Builder[S, E, C]) Transition(from S) *Builder[S, E, C]

Transition opens a new edge from the given state.

func (b *Builder[S, E, C]) Use(mw ...Middleware[S, E, C]) *Builder[S, E, C]

Use installs middleware that wraps every Fire.

func (b *Builder[S, E, C]) WaitMode(m WaitMode) *Builder[S, E, C]

WaitMode tags the most-recent transition’s synchronization mode.

func (b *Builder[S, E, C]) When(guardName string, params ...map[string]any) *Builder[S, E, C]

When attaches a named guard ref with params to the most-recent transition.

func (b *Builder[S, E, C]) WhenExpr(expr GuardNode[S]) *Builder[S, E, C]

WhenExpr attaches a composite guard expression to the most-recent transition: a boolean tree over named-ref leaves (Guard), the stateIn built-in (StateIn), and the And/Or/Not combinators, with short-circuit semantics. It is evaluated alongside any When guards — the transition is enabled only when both pass. Use When for the common single-guard case and WhenExpr when a transition needs composition or stateIn.

func (*Builder[S, E, C]) WithContextSchema

Section titled “func (*Builder[S, E, C]) WithContextSchema”
func (b *Builder[S, E, C]) WithContextSchema(schema ContextSchema) *Builder[S, E, C]

WithContextSchema attaches a serializable description of the machine’s context data model to the IR envelope (the IR.Context slot), so a rehydrated machine re-emits it on ToJSON and an expression layer or studio can read the context’s shape. Pair it with SchemaOf to derive the schema from the Go context type:

state.Forge[S, E, *Order]("checkout").
WithContextSchema(state.SchemaOf[*Order]())

It is opt-in and additive: deriving is never automatic at Forge, and a machine with no schema is valid and simply limits later type-checking. The schema is metadata only — the kernel never inspects it and Fire never reads it.

CancelScheduled is the effect the kernel emits when an instance exits a state that had a pending delayed (`after`) timer, or when a Cancel action runs. The host cancels the timer registered under ID; canceling an unknown ID is a no-op. A state’s `after` timers are auto-canceled when the state is exited before the delay elapses.

type CancelScheduled struct {
// ID identifies the timer to cancel. It matches the ID of the ScheduleAfter
// that armed it (auto-cancel-on-exit), or an ID supplied to Cancel.
ID string `json:"id"`
}

func (CancelScheduled) Kind() string

Kind reports the cancel-scheduled effect discriminant.

CastOption configures Cast.

type CastOption[S comparable] func(*castConfig[S])

func WithClock[S comparable](c Clock) CastOption[S]

WithClock injects the time seam an instance’s delayed-transition driver uses. It is consumed only by a Scheduler / host driver wired to the instance — never by the pure Fire step, which neither reads a clock nor sleeps. Supply SystemClock() in production or a fake clock in a test to drive `after` transitions deterministically. When omitted, an instance defaults to SystemClock().

func WithInitialState[S comparable](s S) CastOption[S]

WithInitialState supplies the instance’s starting state explicitly. Use it when the machine declares no CurrentStateFn (i.e. the current state cannot be derived from the entity). When both are present, the explicit initial state takes precedence over CurrentStateFn.

func WithInspector[S comparable](insp Inspector) CastOption[S]

WithInspector registers a live observer sink fed inspection events as the instance advances — event received, transition taken, snapshot update — mirroring the live inspection stream. It is off by default: with no inspector the instance never calls one, so inspection adds zero overhead and the pure Fire step performs no IO. The same inspector can be wired to an ActorSystem (WithActorInspector) so actor lifecycle and inter-actor messages are observed on the same sink. The inspector is notified synchronously and must not block or mutate the instance.

func WithLogger[S comparable](l *slog.Logger) CastOption[S]

WithLogger wires a structured-logging seam an instance writes a terse, fixed-shape record to as each Fire settles — distinct from the event-shaped Inspector. Where an Inspector receives the full, typed InspectionEvent stream for live observation and tooling, the logger is the conventional *slog.Logger a host already threads through its services, so a Fire’s outcome shows up in the host’s ordinary logs (machine, event, from, to, outcome) without the host adapting an Inspector. It is no-op by default: with no WithLogger the instance holds a nil logger and never logs, so the pure Fire step performs no IO and adds zero overhead. The logger is written to synchronously on the Fire path at slog.LevelDebug and must not block; it observes only and never mutates the instance. Wire both seams when you want host logs AND structured inspection — they are independent.

Clock is the deterministic time seam used by host drivers (never by the kernel). A real host wires a wall-clock implementation; a test wires a fake clock so `after` machines are exercised deterministically. The kernel’s Fire step never calls a Clock — only effect-consuming drivers do.

type Clock interface {
// Now reports the current time.
Now() time.Time
// After returns a channel that receives once the duration elapses, mirroring
// time.After. A driver selects on it to learn when a delayed event is due.
After(d time.Duration) <-chan time.Time
}

func SystemClock() Clock

SystemClock returns the wall-clock Clock backed by the standard library, for a production host driver.

ContextCodec encodes and decodes an instance context C to and from bytes for a Snapshot, for a context type that is not directly JSON-marshalable (or needs a custom wire form). Encode is called by Snapshot.MarshalJSON; Decode by Snapshot.UnmarshalJSON. When no codec is supplied, the default codec marshals C with encoding/json, so C must be JSON-marshalable by default.

type ContextCodec[C any] interface {
Encode(C) ([]byte, error)
Decode([]byte) (C, error)
}

ContextSchema is a serializable description of a machine’s context type: an object whose named fields each carry a SchemaField type. It is the root of the data model attached to the IR and reuses the closed-enum extension policy used across the envelope, so an unknown field kind a newer producer emitted survives a load -> save cycle verbatim.

type ContextSchema struct {
// Fields are the context's named top-level fields, in declaration order for
// objects derived by SchemaOf (struct field order) and as authored otherwise.
Fields []SchemaField `json:"fields,omitempty"`
// Meta is the reserved per-schema extension namespace, round-tripped verbatim
// like every other Meta in the IR. The kernel never inspects it.
Meta map[string]any `json:"meta,omitempty"`
// contains filtered or unexported fields
}

func SchemaOf[C any]() ContextSchema

SchemaOf derives a ContextSchema from the Go type C by reflection. It is the opt-in helper a host pairs with WithContextSchema to attach a context’s shape to a machine; deriving is never automatic at Forge, so an absent schema stays valid.

The reflection mapping is:

  • struct -> object; one field per exported field, named by its json tag (falling back to the Go field name), in declaration order. A field tagged `json:”-”` is skipped; an embedded (anonymous) struct is flattened, mirroring encoding/json.
  • string -> string
  • all integer kinds -> int
  • float32/float64 -> float
  • bool -> bool
  • time.Time -> time
  • time.Duration -> duration
  • slice / array -> list, with the element type derived recursively
  • map -> map, with key and value types derived recursively
  • pointer -> the pointee’s type, marked Nullable
  • interface{} / other kinds -> string (the conservative fallback; the kind cannot be reflected to anything narrower)

Enums cannot be reflected reliably: a Go enum is typically a named integer or string type whose allowed values live in package-level constants the reflect package cannot enumerate. SchemaOf therefore maps such a type to its underlying scalar (int or string); declare the allowed values explicitly with a SchemaField of Kind SchemaEnum to override the scalar — for example, by authoring the ContextSchema directly rather than deriving it.

func (s ContextSchema) FieldAt(path string) (SchemaField, bool)

FieldAt resolves the SchemaField at a dotted field path, descending object fields and unwrapping list/map element types when a path segment names a collection’s element. It returns the resolved field and true, or the zero field and false when any segment does not resolve. The lookup is the type-side helper an expression layer uses to type a guard/assign reference like “order.total”.

Path semantics: each segment names a field of the current object; to step into a list or map element, the segment names the list/map field and the next segment continues into its element type (lists and maps both descend through their Elem). An empty path returns the schema’s root object as an unnamed object field.

func (s ContextSchema) MarshalJSON() ([]byte, error)

MarshalJSON encodes a ContextSchema, merging its preserved unknown keys back in with stable key ordering.

func (s *ContextSchema) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a ContextSchema and captures any unknown top-level keys into extra so they survive re-serialization.

ContextView is the read-only projection of a machine’s context C as it crosses the behavior-invocation boundary. Raw returns the live value for the in-process fast path; JSON returns the serialized wire form an out-of-process binding consumes. It carries no mutator — context is read-only at this seam.

type ContextView interface {
// Raw returns the underlying context value. For the in-process projection it is
// the live entity itself (a zero-cost pass-through); a binding that knows it is
// in-process may type-assert it back to its concrete C.
Raw() any
// JSON returns the serialized projection of the context — the wire form an
// out-of-process binding receives. The in-process projection marshals the live
// value with the context codec on demand.
JSON() ([]byte, error)
}

DescribeBuilder fluently accumulates a registration’s descriptor metadata — its description, parameter schema, and read/write hints. Obtain one with Describe, chain Param / OptionalParam / Reads / Writes, and pass it as the trailing option to a registration (Guard / Action / Service / Actor). A DescribeBuilder is itself a DescribeOption, so it drops straight into the options tail.

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

func Describe(description string) *DescribeBuilder

Describe opens a fluent descriptor builder with the given human description. Chain Param / OptionalParam / Reads / Writes to declare the parameter schema and data-flow hints, then pass the builder as the trailing option to a registration:

reg.Guard("minAmount", minAmount,
state.Describe("Passes when the amount is at least min.").
Param("min", state.IntParam).
OptionalParam("currency", state.StringParam).
Reads("Order"))

func (d *DescribeBuilder) EnumParam(name string, allowed ...string) *DescribeBuilder

EnumParam declares a required enum parameter constrained to the given allowed values.

func (d *DescribeBuilder) OptionalParam(name string, typ ParamType) *DescribeBuilder

OptionalParam declares an optional parameter of the given type.

func (d *DescribeBuilder) Param(name string, typ ParamType) *DescribeBuilder

Param declares a required parameter of the given type.

func (d *DescribeBuilder) ParamSpec(p ParamSpec) *DescribeBuilder

ParamSpec appends a fully-specified parameter, for cases needing a description, default, or enum values the shorthand Param/OptionalParam do not express.

func (d *DescribeBuilder) Reads(fields ...string) *DescribeBuilder

Reads records the entity fields the implementation reads, a data-flow hint for a UI. Successive calls accumulate.

func (d *DescribeBuilder) Writes(fields ...string) *DescribeBuilder

Writes records the entity fields the implementation writes, a data-flow hint for a UI. Successive calls accumulate.

DescribeOption configures the optional descriptor attached to a registration. A *DescribeBuilder is the canonical implementation; the option tail keeps registration backward-compatible — calling Guard/Action/Service/Actor with no option still works and yields a minimal descriptor.

type DescribeOption interface {
// contains filtered or unexported methods
}

Descriptor is the serializable palette entry for one registered implementation. It carries the implementation’s kind and name (always present), an optional human description, the parameter schema a UI renders a form from, and optional context read/write hints naming the entity fields the implementation reads or writes. A registration with no Describe yields a minimal Descriptor with only Kind and Name set.

type Descriptor struct {
Kind DescriptorKind `json:"kind"`
Name string `json:"name"`
Description string `json:"description,omitempty"`
Params []ParamSpec `json:"params,omitempty"`
// Reads and Writes are optional type hints naming the entity fields the
// implementation reads from or writes to, for a UI that surfaces data flow.
Reads []string `json:"reads,omitempty"`
Writes []string `json:"writes,omitempty"`
// Binding is the reserved descriptor of how this named behavior is backed. It
// is optional and absent by default; an absent binding means the behavior
// resolves to the in-process Go registry entry (BindingTransportOf reads that
// default). Reserving the slot now keeps a future out-of-process binding
// (a sandboxed component, a remote service) an additive descriptor field rather
// than a breaking change. The kernel never dispatches on it at v1.
Binding *BindingSpec `json:"binding,omitempty"`
}

func BuiltinPalette() []Descriptor

BuiltinPalette returns descriptors for the language-level built-ins the kernel recognizes without host registration — the actor and scheduling actions (spawn, stop-actor/stop-child, send/forward/respond, send-parent, cancel) and the stateIn guard. They are excluded from Palette because they are part of the language, not the host’s registry; a builder lists them from this fixed set so the editor surfaces the full vocabulary. The returned slice is freshly allocated and sorted deterministically.

DescriptorKind names the category of a registered implementation in the palette: a guard predicate, an action/effect, an invoked service, or an actor behavior. It serializes as its lowercase string for a stable wire form.

type DescriptorKind string

The descriptor kinds, one per palette-eligible registration surface.

const (
// KindGuard marks a registered guard predicate.
KindGuard DescriptorKind = "guard"
// KindAction marks a registered action/effect.
KindAction DescriptorKind = "action"
// KindAssign marks a registered assign reducer — the sole context writer.
KindAssign DescriptorKind = "assign"
// KindService marks a registered invoked service.
KindService DescriptorKind = "service"
// KindActor marks a registered actor behavior.
KindActor DescriptorKind = "actor"
)

Diagnostic is a non-failing finding from Temper — a lint/static-analysis result surfaced before Quench. Consumers pattern-match on it, so its field names are stable.

type Diagnostic struct {
// Severity is the finding's level ("warning" | "error"); under Strict, Quench
// rejects any finding, otherwise only "error".
Severity string
// Message is the human-readable description of the finding.
Message string
// SrcFile and SrcLine point at the builder call site that produced the finding,
// captured via runtime.Caller. They are diagnostic-only and may be empty for a
// finding with no single source position.
SrcFile string
SrcLine int
}

Effect is an abstract, domain-defined payload. The kernel never inspects it.

type Effect = any

EffectEnvelope is the serialized form of an effect: a discriminated kind, the effect’s JSON payload, and an optional extension namespace. It is the output half of the data boundary — the shape a host journals, dedupes, renders, or emits across a process boundary — mirroring the IR envelope on the input half.

EffectID is reserved: a later ordering contract assigns each emitted effect a stable, deterministic identity for journal dedup and replay. The field exists in the wire shape now so adding that identity later is non-breaking, but the kernel does not populate or stabilize it yet — an inbound EffectID round-trips verbatim and otherwise carries no meaning.

type EffectEnvelope struct {
// Kind is the effect's stable discriminant (see KindedEffect.Kind).
Kind string `json:"kind"`
// Payload is the effect's marshaled body. It is opaque to the envelope; an
// EffectRegistry decodes it into a concrete effect keyed by Kind.
Payload json.RawMessage `json:"payload,omitempty"`
// Meta is the reserved per-effect extension namespace — a schema hook and the
// attachment point for host annotations. The kernel never inspects it; it
// round-trips verbatim.
Meta map[string]any `json:"meta,omitempty"`
// EffectID is the reserved correlation/identity slot. NOT yet stable: the
// kernel leaves it empty and a later ordering PR will populate it
// deterministically. An inbound value is preserved on round-trip.
EffectID string `json:"effectId,omitempty"`
// contains filtered or unexported fields
}

func MarshalEffect(eff KindedEffect) (EffectEnvelope, error)

MarshalEffect serializes a KindedEffect into an EffectEnvelope. The effect’s Kind becomes the envelope discriminant and the effect marshals to the payload. An UnknownEffect re-emits its preserved kind, payload, and meta verbatim so a foreign effect survives a round-trip without the local build understanding it.

func (e EffectEnvelope) MarshalJSON() ([]byte, error)

MarshalJSON encodes an EffectEnvelope, merging its preserved unknown keys back in with stable key ordering.

func (e *EffectEnvelope) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes an EffectEnvelope and captures any unknown keys into extra so they survive re-serialization.

EffectFactory builds a fresh, zero-valued concrete effect for a kind. The registry unmarshals an envelope’s payload into the value the factory returns, so a factory returns a pointer to a concrete effect type for json.Unmarshal to populate. Built-in factories are pre-registered; a host registers its own effect kinds through RegisterEffect.

type EffectFactory func() Effect

EffectRegistry maps effect kinds to factories for envelope deserialization. It is the output-half counterpart to the host registry on the input half: the built-in effect kinds are pre-registered, and a host adds its own through the RegisterEffect functional option. Deserializing a kind the registry does not know does not fail — the envelope is preserved as an UnknownEffect — but such an effect is not Dispatchable, realizing the preserve-on-load, reject-on-dispatch closed-enum extension policy.

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

func NewEffectRegistry(opts ...RegisterEffectOption) *EffectRegistry

NewEffectRegistry returns an EffectRegistry with every built-in effect kind pre-registered, then applies the supplied options (host effect kinds) in order. Options registering a built-in kind override the pre-registration.

func (r *EffectRegistry) Dispatchable(eff Effect) error

Dispatchable reports whether an effect may be applied by a host. A nil result means the effect carries a kind the registry recognizes (or is not kinded at all — a bare domain effect the kernel never gated). An UnknownEffect, or any KindedEffect whose kind the registry does not know, is rejected with a typed *ErrUnknownEffectKind, completing the preserve-on-load, reject-on-dispatch policy: a foreign effect is never silently applied.

func (r *EffectRegistry) Unmarshal(env EffectEnvelope) (Effect, error)

Unmarshal decodes an EffectEnvelope into a concrete effect. A recognized kind is built by its registered factory and populated from the payload; an unrecognized kind is preserved verbatim as an UnknownEffect rather than dropped or rejected — the reject happens later at Dispatchable. The returned value implements KindedEffect.

ErrActionFailed wraps a bound action that returned an error during emission.

type ErrActionFailed struct {
TransitionName string
ActionName string
Cause error
}

func (e *ErrActionFailed) Error() string

func (e *ErrActionFailed) Unwrap() error

ErrActorPanic is the typed failure raised when a child-machine actor panics while it steps an event. The ActorSystem recovers the panic so it never crashes the host driver, wraps the recovered value here, and settles the actor as a failure — routing its onError, or escalating to the parent when none is wired.

type ErrActorPanic struct {
// ActorID is the registry id of the actor that panicked.
ActorID string
// Value is the recovered panic value, rendered for the error message.
Value any
}

func (e *ErrActorPanic) Error() string

Error renders the recovered actor panic.

ErrAssignPanic is returned when an assign reducer panicked and was recovered, or when an assign ref did not resolve at fire time. An assign is a total reducer, so a panic is a programmer error the kernel surfaces as a typed failure that stops the commit rather than leaving context partly folded.

type ErrAssignPanic struct {
AssignName string
Recovered any
}

func (e *ErrAssignPanic) Error() string

ErrGuardFailed is returned when a named guard returned false.

type ErrGuardFailed struct {
GuardName string
Reason string
}

func (e *ErrGuardFailed) Error() string

ErrGuardPanic is returned when a guard panicked and was recovered.

type ErrGuardPanic struct {
GuardName string
Recovered any
}

func (e *ErrGuardPanic) Error() string

ErrInvalidTransition is returned when no transition matched (current, event), or all matching transitions had failing guards. From names the state the event was fired in, Event the rejected event, and Reason the specific cause (no declared transition, a final-state exit, an undeclared current state, …). To names the intended target when the rejected transition had one (a targeted transition whose guards all failed); it is empty for an unmatched event with no candidate target.

type ErrInvalidTransition struct {
From string
To string
Event string
Reason string
}

func (e *ErrInvalidTransition) Error() string

ErrMicrostepOverflow is returned when a single Fire macrostep does not reach a stable configuration within the run-to-completion step budget. It indicates a cycle of raised internal events or eventless (“always”) transitions that never settles.

type ErrMicrostepOverflow struct {
Limit int
State string
}

func (e *ErrMicrostepOverflow) Error() string

ErrNoInitialState is returned/panicked by Cast when neither a CurrentStateFn is declared on the machine nor an explicit initial state is supplied via WithInitialState — there is no way to derive the instance’s starting state. This is a programmer error, consistent with Quench’s panic-on-misuse posture.

type ErrNoInitialState struct {
Machine string
}

func (e *ErrNoInitialState) Error() string

ErrNoPath is returned by PlanPath when no event sequence connects from->to.

type ErrNoPath struct {
From string
To string
}

func (e *ErrNoPath) Error() string

ErrPolicyDenied is returned when a policy returned Deny.

type ErrPolicyDenied struct {
PolicyName string
Reason string
}

func (e *ErrPolicyDenied) Error() string

ErrUnboundActor is returned by an ActorSystem when a SpawnActor’s Src does not resolve against the system’s actor palette — no child-machine factory was registered under that name. The actor is settled as an error so the parent still routes its onError rather than hanging.

type ErrUnboundActor struct {
Name string
}

func (e *ErrUnboundActor) Error() string

ErrUnboundRef is returned when a guard/action/effect ref in the IR did not resolve against the registry (raised at Quench / Provide).

type ErrUnboundRef struct {
Kind string // "guard" | "action" | "assign" | "service"
Name string
}

func (e *ErrUnboundRef) Error() string

ErrUndeclaredState is returned when a state value was never declared.

type ErrUndeclaredState struct {
State string
}

func (e *ErrUndeclaredState) Error() string

ErrUnknownBuiltin is returned when a ref names a kernel built-in action the kernel does not recognize. It is a defensive programmer-error signal: the DSL and lint only ever produce known built-in names, so this surfaces only a hand-constructed or corrupted ref.

type ErrUnknownBuiltin struct {
Name string
}

func (e *ErrUnknownBuiltin) Error() string

ErrUnknownEffectKind is returned by EffectRegistry.Dispatchable when an effect carries a kind the registry does not recognize. It realizes the reject half of the closed-enum extension policy for effect kinds: an unknown kind is preserved on load (as an UnknownEffect) so a foreign effect round-trips losslessly, but it is refused at dispatch rather than silently applied — the host must register the kind (RegisterEffect) or drop the effect deliberately.

type ErrUnknownEffectKind struct {
// Kind is the unrecognized effect discriminant.
Kind string
}

func (e *ErrUnknownEffectKind) Error() string

ErrUnsupportedSchema is returned by LoadFromJSON when an IR document declares a schema major version newer than the loader supports. The reject-higher-major policy is the reserved compatibility seam: a higher minor (same major) loads, preserving unknown fields for forward-compat, but a higher major signals a wire form this build cannot safely interpret and is refused rather than guessed at.

type ErrUnsupportedSchema struct {
// Got is the schemaVersion declared in the document.
Got string
// Supported is the loader's own schema version.
Supported string
}

func (e *ErrUnsupportedSchema) Error() string

EscalationHandler receives an actor failure that escalated to the parent because no onError was declared for it. It is the host-side opt-in for reacting to an unhandled child failure: a handler may fire a parent event, tear other actors down, propagate further, or record the failure. It is wired with WithEscalationHandler and is invoked once per escalation, outside the system mutex, so it may safely re-enter the ActorSystem.

Returning no error acknowledges the escalation (the default record + inspect still occurred). The handler does not replace the typed record or the inspector event — those always happen — it adds host policy on top of the frozen default.

type EscalationHandler func(ctx context.Context, esc *ActorEscalation)

FakeClock is a deterministic Clock for tests: time advances only when Advance is called. It implements Clock; pair it with a Scheduler (via WithClock at Cast) to drive `after` transitions with no real waiting. It is concurrency-safe.

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

func NewFakeClock(start time.Time) *FakeClock

NewFakeClock returns a FakeClock starting at the given instant. The zero instant is fine; only relative advances matter for delayed transitions.

func (c *FakeClock) Advance(d time.Duration)

Advance moves the fake clock forward by d. After advancing, call Scheduler.Tick to fire any now-due timers.

func (c *FakeClock) After(d time.Duration) <-chan time.Time

After returns a channel that fires once the fake clock has advanced by at least d from the call. It is provided for Clock conformance; the Scheduler drives elapses through Now + Tick rather than this channel, so a test never blocks on it.

func (c *FakeClock) Now() time.Time

Now returns the fake clock’s current instant.

FieldRef is a Core field-ref operand under construction: a dotted context path that becomes either side of a comparison or the subject of a membership test. Obtain one with Field, then close it with a comparison (Eq/Ne/Lt/Le/Gt/Ge) or In to produce a GuardNode. FieldRef is parameterized by the state type so the produced node composes with And/Or/Not and StateIn over the same machine.

type FieldRef[S comparable] struct {
// contains filtered or unexported fields
}

func Field[S comparable](path string) FieldRef[S]

Field opens a Core field-ref operand at the given dotted context path (e.g. “Status” or “order.total”). Close it with a comparison or In:

state.Field[string]("Status").In(state.Str("paid"), state.Str("settled"))
state.Field[string]("Balance").Gte(state.Param("amount"))

func (f FieldRef[S]) Eq(operand Operand[S]) GuardNode[S]

Eq builds a Core equality comparison between the field and the given operand.

func (f FieldRef[S]) Ge(operand Operand[S]) GuardNode[S]

Ge builds a Core greater-than-or-equal comparison: field >= operand.

func (f FieldRef[S]) Gt(operand Operand[S]) GuardNode[S]

Gt builds a Core greater-than comparison: field > operand.

func (f FieldRef[S]) In(values ...Operand[S]) GuardNode[S]

In builds a Core membership test true when the field’s value equals one of the given literal operands. Every operand must be a literal (Str/Int/Float/Bool/ Dur/Param); a field operand in a membership set is rejected at Quench.

func (f FieldRef[S]) Le(operand Operand[S]) GuardNode[S]

Le builds a Core less-than-or-equal comparison: field <= operand.

func (f FieldRef[S]) Lt(operand Operand[S]) GuardNode[S]

Lt builds a Core less-than comparison: field < operand.

func (f FieldRef[S]) Ne(operand Operand[S]) GuardNode[S]

Ne builds a Core inequality comparison between the field and the given operand.

FireFunc is the inner step the middleware chain wraps.

type FireFunc[S comparable, E comparable, C any] func(ctx context.Context, event E) FireResult[S]

FireOption configures Fire / FireSeq / FireEach.

type FireOption func(*fireConfig)

func CollectAll() FireOption

CollectAll makes a batch fire run every step and gather all errors instead of stopping at the first.

func WithEventData(data any) FireOption

WithEventData attaches a payload to a single Fire so the triggering transition’s Assign reads it from AssignCtx.Event. It is the channel by which a host delivers a service result, an actor’s done-data, or an error to the onDone/onError transition’s reducer: the ServiceRunner and ActorSystem re-fire the routing event with the result as the payload, so the reducer consumes it through AssignCtx.Event with no side channel. When omitted, AssignCtx.Event carries the boxed triggering event itself.

FireResult is the result of a single Fire.

type FireResult[S comparable] struct {
NewState S
Effects []Effect
Trace Trace
Err error
}

func FireEach[S comparable, E comparable, C any](ctx context.Context, instances []*Instance[S, E, C], event E, opts ...FireOption) []FireResult[S]

FireEach fans one event across an explicit set of instances, preserving per-instance attribution.

ForgeOption configures Forge.

type ForgeOption func(*forgeConfig)

func WithMachineID(id string) ForgeOption

WithMachineID stamps the machine DEFINITION id (the IR ID) onto a Forge-built machine, carried alongside the version so a migrator can resolve the source definition unambiguously. When omitted, a Forge-built machine has no definition id.

func WithMachineVersion(version string) ForgeOption

WithMachineVersion stamps the machine DEFINITION version (the IR Version, a semver label) onto a Forge-built machine, so a Snapshot taken from it carries the version a restored instance self-identifies by — the precondition for live migration. It mirrors the version a machine rehydrated from a versioned IR already carries. When omitted, a Forge-built machine has no definition version.

ForwardEvent is the effect the kernel emits for the forwardTo built-in: forward the event the emitting actor is currently handling, verbatim, to the actor addressed by TargetID (or SystemID). The kernel does not embed the forwarded event — the host already has it as the event it just delivered — so this effect carries only the target. The host’s ActorSystem routes the current event into the target’s mailbox; addressing an unknown actor is a no-op. This realizes forwards the current event verbatim to another actor.

type ForwardEvent struct {
// TargetID is the registry id of the actor to forward the current event to.
// Empty when the target is addressed by SystemID instead.
TargetID string `json:"targetId,omitempty"`
// SystemID is the system-scoped name of the target actor, used when TargetID is
// empty.
SystemID string `json:"systemId,omitempty"`
}

func (ForwardEvent) Kind() string

Kind reports the forward-event effect discriminant.

GuardBinding turns a guard request into a verdict. The in-process binding wraps a GuardFn; a future out-of-process binding marshals the request across its transport. EvalGuard is synchronous so it remains callable inside the pure Fire step.

type GuardBinding[C any] interface {
EvalGuard(ctx context.Context, req GuardRequest[C]) (GuardResult, error)
}

GuardCtx is passed to a bound guard function at run time.

type GuardCtx[C any] struct {
Entity C
Params map[string]any
}

GuardFn is a pure predicate on the entity.

type GuardFn[C any] func(ctx GuardCtx[C]) bool

GuardKind names the tier of a guard expression node: Core is the structured, dependency-free tree this kernel evaluates in-process; Rich is the reserved source-plus-checked-AST tier an opt-in expression module will evaluate. The boolean spine and the named-ref/stateIn leaves leave Kind empty — they predate the discriminant and are structurally Core. GuardKind follows the closed-enum extension policy: a kind this build does not recognize is preserved verbatim on round-trip (so a newer producer’s node survives an older client) and is rejected only at evaluation.

type GuardKind string

const (
// GuardKindCore tags a node as the structured, in-kernel Core tier. It is set
// on the Core expression leaves built by the Core builder; the legacy boolean
// and named-ref nodes leave it empty and are treated as Core.
GuardKindCore GuardKind = "core"
// GuardKindRich reserves the Rich tier — a guard authored as source text with
// a checked AST, evaluated by an opt-in expression module. No Rich evaluation
// path exists in the kernel; the kind is reserved so adding the tier later is
// additive rather than a breaking change.
GuardKindRich GuardKind = "rich"
)

GuardNode is one node of a serializable guard expression tree. A leaf references a host-provided guard by name (with serializable params) or is the built-in stateIn guard; internal nodes compose children with and/or/not.

The tree is pure, serializable data: like every other behavioral reference in the IR, leaf guards are named — never embedded closures — so a UI- or JSON-authored composite guard binds against the host registry at Provide and round-trips to and from JSON without losing structure. Arbitrary nesting is supported, e.g. And(Or(g1, g2), Not(g3)).

The common case — a single named guard — stays the plain Transition.Guards slice; GuardNode is used only when a transition needs boolean composition or the stateIn built-in.

type GuardNode[S comparable] struct {
Op GuardOp `json:"op"`
// Kind is the node's tier: empty (legacy boolean/named spine, structurally
// Core), GuardKindCore for a Core expression leaf, or GuardKindRich for the
// reserved Rich tier. An unrecognized kind is preserved on round-trip and
// rejected only at evaluation.
Kind GuardKind `json:"kind,omitempty"`
// Ref is the named-ref guard for a GuardLeaf node. Zero for every other op.
Ref *Ref `json:"ref,omitempty"`
// In is the target state for a GuardStateIn node: the guard is true when this
// state is in the instance's active configuration (its leaves and their
// ancestor spine). Zero for every other op.
In *S `json:"in,omitempty"`
// Path is the dotted context path for a GuardField operand node, resolved
// against the context at evaluation and against the ContextSchema at Quench.
// Zero for every other op.
Path string `json:"path,omitempty"`
// Lit is the typed literal value for a GuardLit operand node. Zero for every
// other op.
Lit *Literal `json:"literal,omitempty"`
// Set is the literal membership set for a GuardIn node: the left operand (the
// first child) passes when it equals one of these values. Zero for every other
// op.
Set []Literal `json:"set,omitempty"`
// Children are the operands of an internal node. And/Or take one or more; Not
// takes exactly one; a compare (eq/ne/lt/le/gt/ge) takes exactly two operand
// nodes (each a GuardField or GuardLit); membership (in) takes exactly one
// operand node. Empty for leaf, stateIn, field, and literal nodes.
Children []GuardNode[S] `json:"children,omitempty"`
// contains filtered or unexported fields
}

func And[S comparable](nodes ...GuardNode[S]) GuardNode[S]

And composes guards into a node true only when every operand is true, short-circuiting at the first false — consistent with the AND short-circuit of a plain multi-guard transition. Operands may be named-ref leaves, stateIn, or other combinators, nested arbitrarily.

Example

ExampleAnd composes named-ref guards and the stateIn built-in into a single boolean guard expression on a transition with And/Or/Not, exercising the guard combinators. The transition fires only when the composite passes; And short-circuits at the first false and Or at the first true.

package main
import (
"context"
"fmt"
"github.com/stablekernel/crucible/state"
)
// access is the entity the combinator example guards against.
type access struct {
admin bool
auditor bool
}
// ExampleAnd composes named-ref guards and the stateIn built-in into a single
// boolean guard expression on a transition with And/Or/Not, exercising the
// guard combinators. The transition fires only when the composite passes; And
// short-circuits at the first false and Or at the first true.
func main() {
m := state.Forge[string, string, access]("door").
Guard("admin", func(c state.GuardCtx[access]) bool { return c.Entity.admin }).
Guard("auditor", func(c state.GuardCtx[access]) bool { return c.Entity.auditor }).
State("locked").
Transition("locked").On("open").GoTo("open").
// Enabled while in "locked" AND (admin OR auditor).
WhenExpr(state.And(
state.StateIn("locked"),
state.Or(state.Guard[string]("admin"), state.Guard[string]("auditor")),
)).
State("open").
Initial("locked").
Quench()
denied := m.Cast(access{}, state.WithInitialState("locked"))
denied.Fire(context.Background(), "open")
fmt.Println("no role:", denied.Current())
allowed := m.Cast(access{auditor: true}, state.WithInitialState("locked"))
allowed.Fire(context.Background(), "open")
fmt.Println("auditor:", allowed.Current())
}
no role: locked
auditor: open

func Guard[S comparable](name string, params ...map[string]any) GuardNode[S]

Guard builds a named-ref guard leaf with optional serializable params, the composable form of a single transition guard. It is the leaf used inside And/Or/Not.

func Not[S comparable](node GuardNode[S]) GuardNode[S]

Not inverts a single guard.

func Or[S comparable](nodes ...GuardNode[S]) GuardNode[S]

Or composes guards into a node true when any operand is true, short-circuiting at the first true. Operands may be named-ref leaves, stateIn, or other combinators, nested arbitrarily.

func StateIn[S comparable](state S) GuardNode[S]

StateIn builds the built-in in-state guard leaf: true when the instance’s active configuration includes state. It is config-aware — it reads the live active leaves and their ancestors at evaluation time, so it works for atomic, compound, and parallel configurations (“in” means the state is somewhere in the active set/spine). It is a first-class built-in: the consumer never registers it. The name is stateIn for guard parity; renaming to In would break that documented parity contract.

func (g *GuardNode[S]) LeafRefs() []Ref

LeafRefs returns the named-ref guard leaves of a guard expression tree, in left-to-right order. The stateIn built-in carries no host ref and is omitted. It lets tooling (e.g. evolution diffing) enumerate the host guards a composite expression depends on.

func (g GuardNode[S]) MarshalJSON() ([]byte, error)

MarshalJSON encodes a GuardNode, merging its preserved unknown keys back in with stable key ordering.

func (g *GuardNode[S]) StateInTargets() []S

StateInTargets returns the target states of every stateIn leaf in the tree, in left-to-right order, so tooling can account for in-state dependencies a composite guard introduces.

func (g *GuardNode[S]) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a GuardNode and captures any unknown keys into extra so they survive re-serialization, keeping forward-compat structural for the nested guard tree.

GuardOp tags the kind of a node in a guard expression tree.

type GuardOp string

Guard expression operators. A leaf is either a named-ref guard (resolved against the host registry), the built-in stateIn guard, or one of the Core expression leaves (compare/field/literal/membership) evaluated in-kernel against the context; the internal nodes compose child results with boolean and/or/not. The string form is stable so the tree round-trips losslessly through JSON. The op set follows the closed-enum extension policy: an op this build does not recognize is preserved verbatim on round-trip and rejected only at evaluation.

const (
// GuardLeaf is a named-ref guard leaf: it carries a Ref bound to a host
// GuardFn at Provide/Quench time, exactly like a plain transition guard.
GuardLeaf GuardOp = "leaf"
// GuardStateIn is the built-in in-state guard leaf: it is true when the
// instance's active configuration includes the named state. It needs no
// registration — the kernel evaluates it directly against the live spine.
GuardStateIn GuardOp = "stateIn"
// GuardAnd is true when every child is true; it short-circuits at the first
// false child.
GuardAnd GuardOp = "and"
// GuardOr is true when any child is true; it short-circuits at the first
// true child.
GuardOr GuardOp = "or"
// GuardNot inverts its single child.
GuardNot GuardOp = "not"
// GuardEq is true when its two operands compare equal.
GuardEq GuardOp = "eq"
// GuardNe is true when its two operands compare unequal.
GuardNe GuardOp = "ne"
// GuardLt is true when the left operand is less than the right.
GuardLt GuardOp = "lt"
// GuardLe is true when the left operand is less than or equal to the right.
GuardLe GuardOp = "le"
// GuardGt is true when the left operand is greater than the right.
GuardGt GuardOp = "gt"
// GuardGe is true when the left operand is greater than or equal to the right.
GuardGe GuardOp = "ge"
// GuardIn is true when the left operand is a member of the literal set carried
// on Set.
GuardIn GuardOp = "in"
// GuardField is a field-ref operand: it resolves the dotted Path against the
// context and yields the value there. It is an operand, valid only as a child
// of a compare or membership node, never a standalone boolean.
GuardField GuardOp = "field"
// GuardLit is a typed literal operand carried on Lit. Like GuardField it is an
// operand, valid only inside a compare or membership node.
GuardLit GuardOp = "literal"
)

GuardRequest is the serializable invocation envelope for a guard: the named ref, its params, and the read-only context projection the guard evaluates against.

type GuardRequest[C any] struct {
Name string
Params map[string]any
Context ContextView
}

GuardResult is the guard’s serializable result: a boolean verdict. It is deliberately minimal so a guard stays a pure predicate evaluable inside Fire.

type GuardResult struct {
OK bool
}

HistoryType is the reserved drop-in surface for shallow/deep history states.

type HistoryType int

History kinds. HistoryNone is the v1 default (no history); shallow and deep are reserved for the deferred history-state feature.

const (
HistoryNone HistoryType = iota
HistoryShallow
HistoryDeep
)

IOSpec is the reserved declaration slot for a machine’s input or done-output shape. At v1 it is opaque: Schema is a free-form, namespace-reserved description of the shape and Description is human documentation. A later data-model/typing module can give Schema teeth without changing the wire field. Meta is the per-spec extension namespace, round-tripped verbatim like every other Meta in the IR.

type IOSpec struct {
// Schema is an opaque declaration of the input/output shape. The kernel never
// inspects it; it travels for tooling and a future typing layer.
Schema map[string]any `json:"schema,omitempty"`
// Description is human-readable documentation of the slot.
Description string `json:"description,omitempty"`
// Meta is the reserved extension namespace for this spec.
Meta map[string]any `json:"meta,omitempty"`
}

IR is the serializable definition produced and consumed by the data front-end. It is the canonical machine: pure, lossless data. Behavior lives in a host registry and is referenced by name (via Ref), never embedded, so the IR round-trips to and from JSON without losing structure or bindings’ identity.

Non-serializable concerns — CurrentStateFn, requirement predicates, and middleware — are pure-runtime and are intentionally absent from the IR; a machine rehydrated from JSON is Cast from an explicit state and bound to a registry via Provide. The envelope fields (SchemaVersion, ID, Version, Input, Output, Meta) are an additive, non-breaking superset of the v0 IR: a document without them still loads, and a tolerant loader round-trips a document carrying extension fields it does not model. SchemaVersion is stamped by ToJSON so every emitted document is self-describing; LoadFromJSON rejects a higher schema major and preserves unknown keys within a major line.

type IR[S comparable, E comparable, C any] struct {
// SchemaVersion is the IR wire-format version (major.minor). ToJSON stamps it
// with CurrentSchemaVersion; LoadFromJSON rejects a higher major.
SchemaVersion string `json:"schemaVersion,omitempty"`
// ID is a stable machine identity distinct from the human-facing Name, used to
// pin a durable instance or a migration to the exact definition it derives from.
ID string `json:"id,omitempty"`
Name string `json:"name"`
// Version is the machine definition version (a semver string), the label a
// migration maps from/to and a durable runtime pins an instance against. A
// content digest is reserved for later and is not computed here.
Version string `json:"version,omitempty"`
// Input and Output are the machine's opaque input contract and done-output
// shape — the symmetry actors already have (per-invocation Input) lifted to the
// root machine. At v1 they are reserved declaration slots; the typing layer is
// additive.
Input *IOSpec `json:"input,omitempty"`
Output *IOSpec `json:"output,omitempty"`
// Context is the optional, serializable description of the machine's context
// data model — the L5 data contract an expression layer type-checks guards and
// assigns against and a studio renders context-update forms from. It is opt-in
// (set with Builder.WithContextSchema, helper SchemaOf); an absent schema is
// valid and simply limits later type-checking. The kernel never inspects it; it
// round-trips verbatim.
Context *ContextSchema `json:"context,omitempty"`
States []State[S, E, C] `json:"states,omitempty"`
Initial S `json:"initial"`
HasInitial bool `json:"hasInitial"`
// Meta is the reserved extension namespace at machine granularity: studio
// viewport, property specs, provenance, and codegen hints live here. The kernel
// never inspects it; it round-trips verbatim.
Meta map[string]any `json:"meta,omitempty"`
// contains filtered or unexported fields
}

func LoadFromJSON[S comparable, E comparable, C any](b []byte, opts ...LoadOption) (*IR[S, E, C], error)

LoadFromJSON rehydrates an IR from JSON.

func (ir IR[S, E, C]) MarshalJSON() ([]byte, error)

MarshalJSON encodes an IR, merging its preserved unknown top-level keys back in with stable key ordering so the output is canonical for golden diffing.

func (ir *IR[S, E, C]) Provide(reg *Registry[C], opts ...ProvideOption) *Builder[S, E, C]

Provide binds every Ref in the IR against the host registry and returns a Builder ready to Quench. Refs that do not resolve are surfaced at Quench as the typed *ErrUnboundRef (the same failure the DSL raises for an unregistered ref), so a UI/JSON-authored machine and a DSL-authored machine fail identically.

func (ir *IR[S, E, C]) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes an IR and captures any unknown top-level keys into extra so they survive re-serialization.

InFlightService is the reserved record of an invoked service started but not yet resolved at snapshot time, so a future distributed/async resume can re-establish it. It mirrors the StartService effect’s coordinates: the invocation id, the service src name, the input, and the OnDone/OnError routing event labels.

type InFlightService struct {
// ID is the invocationID of the started service, the stable correlation id a
// resolving JournalEntry reuses.
ID string `json:"id"`
// Src is the service src name (the registry key) the host re-starts.
Src string `json:"src,omitempty"`
// Input is the structured input the service was started with.
Input json.RawMessage `json:"input,omitempty"`
// OnDone and OnError are the routing event labels the host re-fires the result
// through after the service resolves.
OnDone string `json:"onDone,omitempty"`
OnError string `json:"onError,omitempty"`
}

InspectKind names a category of inspection event, covering the inspection event types.

type InspectKind string

const (
// InspectEvent marks an event received by an instance.
InspectEvent InspectKind = "event"
// InspectTransition marks a transition taken — a macrostep that changed (or
// re-entered) the configuration, carrying its from/to and the Trace detail
// (guards, effects, exit/entry cascade). It is the kernel's microstep/transition
// inspection surface.
InspectTransition InspectKind = "transition"
// InspectSnapshot marks a snapshot update: the instance's observable state after
// an event settled.
InspectSnapshot InspectKind = "snapshot"
// InspectActor marks an actor lifecycle change — spawned or stopped
// — an actor lifecycle change.
InspectActor InspectKind = "actor"
// InspectMessage marks a message sent from one actor to another and/or delivered
// to its target (the actor-to-actor flavor of an event).
InspectMessage InspectKind = "message"
)

InspectionEvent is one live observation of an instance’s runtime activity. It is an inspection event: a tagged record whose populated fields depend on Kind. A field that does not apply to a Kind is left zero.

The event is read-only; an Inspector must not retain references to mutable values it does not own. The Trace, when present, is the same structured record Fire records in History — surfaced live rather than after the fact.

type InspectionEvent struct {
// Kind tags which observation this is and which fields are populated.
Kind InspectKind
// Machine names the machine the observed instance was cast from. Always set.
Machine string
// Event is the string rendering of the event that triggered this observation,
// for InspectEvent, InspectTransition, and InspectSnapshot. Empty for actor
// lifecycle events with no triggering instance event.
Event string
// From and To name the configuration's primary leaf before and after a
// transition (InspectTransition) or the settled leaf (InspectSnapshot). For an
// InspectEvent, From is the leaf the event was received in and To is empty.
From string
To string
// Trace is the structured Fire record for an InspectTransition — the live twin
// of the entry History() later reports. Nil for non-transition kinds.
Trace *Trace
// Configuration is every active leaf after the observed step settled, for
// InspectSnapshot and InspectTransition. It is a copy; an Inspector may retain
// it.
Configuration []string
// Status is the instance's lifecycle status for InspectSnapshot
// (running/done/error), so an inspector can observe completion without polling.
Status Status
// ActorID, ActorSrc, and ActorPhase describe an InspectActor lifecycle event:
// the actor's registry id, the ref name it was spawned from, and whether it was
// spawned or stopped.
ActorID string
ActorSrc string
ActorPhase ActorPhase
// SenderID, TargetID, MessagePhase, and Message describe an InspectMessage
// event: the originating actor (empty for a host-injected send), the target
// actor, whether the message was observed on send or on delivery, and the
// string rendering of the message event.
SenderID string
TargetID string
MessagePhase MessagePhase
Message string
}

Inspector is the observer sink an instance (and its ActorSystem) feeds live inspection events to. It is registered at Cast with WithInspector and is off by default — a nil inspector is never called, so an un-inspected instance pays nothing. An Inspector must not mutate the instance or perform blocking IO on the hot path; it is the telemetry-style sink the kernel notifies synchronously, in the same spirit as the existing Trace/observer ethos.

All methods receive a by-value InspectionEvent so an implementation can retain it safely. Implement only the methods that matter and embed BaseInspector to no-op the rest.

type Inspector interface {
// Inspect receives every inspection event. The event's Kind selects the
// populated fields. A single entry point keeps the interface stable as new
// kinds are added — a new InspectKind never changes this signature.
Inspect(ev InspectionEvent)
}

InspectorFunc adapts a plain function to the Inspector interface, for the common case of a single closure sink.

type InspectorFunc func(ev InspectionEvent)

func (f InspectorFunc) Inspect(ev InspectionEvent)

Inspect calls the underlying function.

Instance binds a Machine to one entity and carries trace history.

type Instance[S comparable, E comparable, C any] struct {
// contains filtered or unexported fields
}

func (i *Instance[S, E, C]) Clock() Clock

Clock returns the time seam wired to this instance at Cast (SystemClock() by default). A host driver reads it to schedule delayed (`after`) transitions; the pure Fire step never consults it.

func (i *Instance[S, E, C]) Configuration() []S

Configuration returns all currently-active leaves, in declaration order. len == 1 for a flat or single-spine machine; len == N when N regions are active in parallel.

func (i *Instance[S, E, C]) Current() S

Current returns the primary (first) active leaf — the common “what state am I really in?” answer, back-compatible with flat machines.

func (i *Instance[S, E, C]) Entity() C

Entity returns the entity this instance is bound to.

func (i *Instance[S, E, C]) Fire(ctx context.Context, event E, opts ...FireOption) FireResult[S]

Fire runs the full transition pipeline for a single event.

func (i *Instance[S, E, C]) FireSeq(ctx context.Context, events []E, opts ...FireOption) BatchResult[S]

FireSeq drives a sequence of events into one instance, threading intermediate state and merging the per-step traces into one ordered Trace.

Example

ExampleInstance_FireSeq drives a machine through a sequence of events, walking a document from Draft to Published in one batch.

m := buildDocMachine()
doc := &Document{Status: Draft, ReviewerID: strptr("rev-1")}
batch := m.Cast(doc).FireSeq(context.Background(), []DocEvent{Submit, Approve, Publish})
fmt.Println("steps:", len(batch.Steps))
fmt.Println("final:", batch.Steps[len(batch.Steps)-1].NewState)
// Output:
// steps: 3
// final: Published
steps: 3
final: Published

func (i *Instance[S, E, C]) History() []Trace

History returns the ordered traces recorded on this instance.

func (i *Instance[S, E, C]) InFinal() bool

InFinal reports whether the instance’s current primary leaf is a final state — the signal an ActorSystem reads to learn that a child-machine actor has reached completion and its parent’s onDone should be routed. It is a pure read of the active configuration against the machine definition; it never mutates the instance and consults no clock or IO. For a parallel active configuration it reports whether the whole configuration is complete (every region’s active leaf final), so a child whose root is parallel completes only when all regions do.

func (i *Instance[S, E, C]) ResumeEffects() []Effect

ResumeEffects returns the re-arm effects a host absorbs after Restore to re-establish the instance’s pending timers, invoked services, and spawned actors for its restored configuration: a ScheduleAfter per pending delayed transition, a StartService per invoked service, and a SpawnActor per child-machine actor invocation active in the configuration. It is the restore twin of StartEffects (which arms an initial Cast configuration) extended with the delayed-timer effects, so a restored instance re-establishes its invoked/spawned children. Like StartEffects it is a pure read of the configuration and emits no IO; route the effects through the same Scheduler / ServiceRunner / ActorSystem the host drives for Fire.

Entry actions are NOT re-run: ResumeEffects emits only the lifecycle re-arm effects, never the states’ OnEntry actions, so a restored instance resumes rather than re-enters.

func (i *Instance[S, E, C]) Snapshot() Snapshot[S, E, C]

Snapshot captures the instance’s full runtime state into a serializable Snapshot: the active configuration, recorded history, context, lifecycle status, and the IDs of the pending timers / services / actors armed for the active configuration. It is a pure read — it never fires, mutates the instance, or consults a clock — so Fire stays pure and a snapshot may be taken at any quiescent point between Fires.

The returned Snapshot’s Context holds the live entity value; serialize the whole snapshot with MarshalSnapshot (or json.Marshal once the default codec suffices) to obtain the wire form. Status is derived from the active configuration (StatusDone when the whole configuration is final, else StatusRunning); a host that tracks an explicit failure sets StatusError and Error on the returned snapshot before persisting.

func (i *Instance[S, E, C]) StartEffects() []Effect

StartEffects returns the StartService effects for the invoked services declared on the instance’s initial active configuration, so a host can arm the services of the state(s) entered at Cast — the entry that Fire never observes because no event drove it. Call it once, right after Cast, and route the effects through the same ServiceRunner used for Fire’s effects. It is a pure read of the configuration and emits no IO, consistent with the kernel’s effects-as-data contract. A flat or single-spine instance reports its single starting state’s services; a parallel initial configuration reports every active region’s.

Invocation is a declarative invoked service on a state. On entering the owning state the kernel emits a StartService effect carrying Src and Input; the host runs the bound service and re-fires OnDone with the result or OnError with the error back through Fire. On exiting the state before the service completes, the kernel emits a StopService effect so the host stops the in-flight service (auto-stop-on-exit). The whole struct serializes, so an invoke block round-trips losslessly through JSON.

type Invocation[S comparable, E comparable, C any] struct {
// ID identifies this invocation for the lifetime of the owning state's
// activation. It is stable per (machine, owning state, invoke index), so the
// StartService emitted on entry and the StopService emitted on exit pair up,
// and a host keys its running-service table by ID. When omitted in the DSL it
// defaults to the derived InvokeID.
ID string `json:"id,omitempty"`
// Src is the named reference (plus serializable params) to the host-provided
// service implementation, bound from the service registry at Provide/Quench
// time exactly like a guard or action ref. An unbound Src fails Quench with
// the typed *ErrUnboundRef (Kind "service").
Src Ref `json:"src"`
// Input is the serializable input passed to the service when it starts,
// surfaced on the StartService effect as input. It is data only;
// the kernel never inspects it.
Input map[string]any `json:"input,omitempty"`
// OnDone is the event the host re-fires through Fire when the service
// completes successfully; the service result rides along as the StartService
// host contract's done payload. It routes the result through an ordinary
// transition keyed on this event from the owning state.
OnDone E `json:"onDone"`
// OnError is the event the host re-fires through Fire when the service fails;
// the error rides along as the host contract's error payload. It routes the
// failure through an ordinary transition keyed on this event from the owning
// state.
OnError E `json:"onError"`
// Kind tags this invocation as a host-run service (the default,
// ActorKindService) or a child-MACHINE actor (ActorKindMachine). A service
// invocation emits StartService / StopService and is driven by a ServiceRunner;
// an actor invocation emits SpawnActor / StopActor and is driven by an
// ActorSystem that runs the child machine as an actor and routes its done/error
// back through the parent. The field serializes, so the distinction round-trips
// losslessly through JSON.
Kind ActorKind `json:"kind,omitempty"`
// SystemID is the optional system-scoped name a child-machine actor registers
// under in the ActorSystem (its systemId), so a sibling can address it
// by a well-known name. It is meaningful only for an ActorKindMachine
// invocation and serializes for lossless round-trip.
SystemID string `json:"systemId,omitempty"`
}

InvokeOption configures a Builder.Invoke declaration.

type InvokeOption func(*invokeConfig)

func WithInput(input map[string]any) InvokeOption

WithInput sets the serializable input passed to an invoked service when it starts, surfaced as input on the StartService effect.

func WithInvokeID(id string) InvokeOption

WithInvokeID sets an explicit, stable id for an invoked service instead of the derived InvokeID. Use it when a host or a Cancel-style coordination needs a known id independent of the invocation’s declaration order.

func WithServiceParams(params map[string]any) InvokeOption

WithServiceParams sets the serializable params on an invoked service’s Src ref, available to the bound ServiceFn as ServiceCtx.Params — the per-ref configuration knob, distinct from the per-start Input.

func WithSystemID(id string) InvokeOption

WithSystemID sets the system-scoped name a child-machine actor (InvokeActor) registers under in the ActorSystem (its systemId), so a sibling can address it by a well-known name rather than by ref. It is meaningful only for InvokeActor; on a plain service Invoke it is ignored.

JournalEntry records one external, nondeterministic resolution so a future deterministic replay returns the recorded value rather than re-invoking its source. It is the unit of the reserved Snapshot.Journal.

The recording contract (locked here; the recording/replay runtime is host-side): any result that is NOT a pure function of (current configuration, context, event payload, machine definition) is nondeterministic and MUST be recordable as a JournalEntry so replay returns the recorded value. The nondeterministic sources are the invoked-service OnDone/OnError result payloads, actor message payloads, Clock.Now() reads, and host randomness — each correlated by a stable id reused from the effect that armed it (invocationID / actorInvocationID / scheduleID).

type JournalEntry struct {
// Step is the Fire ordinal the result resolved at, indexing the instance's
// recorded Traces, so replay applies the recorded value at the right step.
Step int `json:"step"`
// Kind classifies which nondeterministic source produced the result.
Kind JournalKind `json:"kind"`
// CorrelationID is the stable id of the source, reused from the arming effect
// (invocationID / actorInvocationID / scheduleID), so replay matches the
// recorded value to the resolution it stands in for.
CorrelationID string `json:"correlationId,omitempty"`
// Payload is the structured, JSON result the source produced (a service's
// done-output, an actor message), returned verbatim on replay.
Payload json.RawMessage `json:"payload,omitempty"`
// ClockUnixNano is the recorded Clock.Now() reading (Unix nanoseconds) for a
// JournalClockRead entry, returned on replay so time-dependent transitions
// resolve identically.
ClockUnixNano int64 `json:"clockUnixNano,omitempty"`
}

JournalKind classifies a JournalEntry’s recorded nondeterministic result, so a replay routes each recorded value back to the source that produced it.

type JournalKind string

JournalKind values, one per nondeterministic source the replay contract covers.

const (
// JournalServiceResult records an invoked service's OnDone/OnError result
// payload, correlated by its invocationID.
JournalServiceResult JournalKind = "serviceResult"
// JournalActorMessage records an actor message payload, correlated by the
// actorInvocationID of the routed actor.
JournalActorMessage JournalKind = "actorMessage"
// JournalClockRead records a Clock.Now() reading consumed during a step.
JournalClockRead JournalKind = "clockRead"
// JournalRandom records a host randomness draw consumed during a step.
JournalRandom JournalKind = "random"
)

KindedEffect is an effect that reports a stable, serializable discriminant without a Go type assertion. Every kernel-emitted built-in effect implements it, and a host effect opts in by adding a Kind() method, so effects can be journaled, deduped, rendered, and routed across a serialization boundary by kind rather than by Go type. The Effect alias stays free-form (Effect = any) so a domain may still emit bare values; only KindedEffect participates in the envelope round-trip and dispatch-time kind checks.

type KindedEffect interface {
// Kind returns the stable string discriminant for this effect. It is part of
// the wire contract: two builds must agree on the kind for an effect to route
// across a serialization boundary, so a kind is never renamed once shipped.
Kind() string
}

Literal is a typed constant operand in a Core expression: a value tagged with the ParamType vocabulary the palette already uses for ref params, so a single type language spans param schemas, the context schema, and Core literals. It serializes cleanly for the IR round-trip.

type Literal struct {
// Type tags the literal's value type, drawn from the ParamType vocabulary
// (string/int/float/bool/duration/enum). It drives type-checking against the
// ContextSchema and the comparison's coercion rules.
Type ParamType `json:"type"`
// Value is the literal's value. It is held as the natural Go value for the
// type (string, int64, float64, bool, or a duration string) and round-trips
// through JSON; a duration is carried as its Go duration string.
Value any `json:"value"`
}

LoadOption configures LoadFromJSON.

type LoadOption func(*loadConfig)

Machine is the immutable, Quenched definition.

type Machine[S comparable, E comparable, C any] struct {
// contains filtered or unexported fields
}

func (m *Machine[S, E, C]) Assay(s S, entity C, opts ...AssayOption) error

Assay checks that an externally-constructed entity legally satisfies a state’s declarative requirements, without firing. The default mode is fail-fast (the returned *AssayError carries the first failure); Aggregate collects every failure in one pass. The error type is uniform across modes.

Example

ExampleMachine_Assay checks an externally-built entity against a state’s declarative requirements without firing a transition.

m := buildDocMachine()
missing := m.Assay(Approved, &Document{Status: Approved})
ok := m.Assay(Approved, &Document{Status: Approved, ReviewerID: strptr("rev-1")})
fmt.Println("missing reviewer:", missing != nil)
fmt.Println("with reviewer:", ok)
// Output:
// missing reviewer: true
// with reviewer: <nil>
missing reviewer: true
with reviewer: <nil>

func (m *Machine[S, E, C]) Cast(entity C, opts ...CastOption[S]) *Instance[S, E, C]

Cast pours a fresh running instance from the machine, binding it to the given entity. The instance’s starting state is derived from the entity via the machine’s CurrentStateFn; if no CurrentStateFn was declared, an explicit initial state must be supplied via WithInitialState. When both are present, WithInitialState wins. With neither, Cast panics with *ErrNoInitialState — a programmer error, consistent with Quench’s panic-on-misuse posture.

The entity value is held on the Instance and supplied to guards and actions at Fire time; it is never threaded through context.

Example (Hierarchical)

ExampleMachine_Cast_hierarchical enters a hierarchical machine: casting into a compound state descends to its initial child, so the job starts in Starting under the Running superstate.

m := buildJobMachine()
job := &Job{Status: Queued}
inst := m.Cast(job)
res := inst.Fire(context.Background(), Enqueue)
fmt.Println("state:", res.NewState)
// Output:
// state: Starting
state: Starting

func (m *Machine[S, E, C]) Name() string

Name returns the machine name.

func (m *Machine[S, E, C]) Palette() []Descriptor

Palette returns the discoverable descriptor set of the machine’s registry — every registered guard, action, service, and declared actor behavior — sorted deterministically. It mirrors Registry.Palette for a Quenched machine so a builder API can enumerate the host behavior a loaded machine binds against.

func (m *Machine[S, E, C]) PlanPath(from, to S, entity C, opts ...PlanOption) ([]E, error)

PlanPath returns the shortest event sequence that drives an instance from the `from` state to the `to` state, found by breadth-first search over the static transition graph. Guards are honored against the supplied entity, so the returned path is one the entity can actually traverse. The entity is never mutated. ErrNoPath is returned when no sequence connects from->to.

Example

ExampleMachine_PlanPath finds the shortest event sequence that drives a document from Draft to Published, honoring guards against the entity.

m := buildDocMachine()
doc := &Document{Status: Draft, ReviewerID: strptr("rev-1")}
path, err := m.PlanPath(Draft, Published, doc)
fmt.Println("err:", err)
fmt.Println("steps:", len(path))
// Output:
// err: <nil>
// steps: 3
err: <nil>
steps: 3

func (m *Machine[S, E, C]) Requirements(s S) []Requirement[C]

Requirements returns the declarative requirements for a state, or nil if the state declares none (or is undeclared).

func (m *Machine[S, E, C]) Restore(snap Snapshot[S, E, C], opts ...RestoreOption[S]) (*Instance[S, E, C], error)

Restore rebuilds a running Instance from snap, resuming at the snapshot’s configuration, context, and recorded history WITHOUT re-running any entry actions (resume, not re-enter). The restored instance picks up at the persisted snapshot. The snapshot’s Machine must match m’s name, every configuration leaf must be a declared state, and the configuration must be non-empty; a violation returns a typed *SnapshotError. The restored instance is wired to the supplied clock (WithRestoreClock) or SystemClock by default, exactly as Cast wires it.

After Restore, a host that drove timers/services/actors re-arms them by absorbing the instance’s ResumeEffects through the same drivers it uses for Fire — Restore itself fires nothing and performs no IO, so Fire stays pure.

func (m *Machine[S, E, C]) Services() map[string]ServiceFn[C]

Services returns the machine’s bound invoked-service palette by name, for a host that constructs a ServiceRunner from the machine’s own registry. The map is a copy; mutating it does not affect the machine.

func (m *Machine[S, E, C]) ToDOT(opts ...VizOption) string

ToDOT renders the machine as GraphViz DOT for richer SVG output — slides, docs sites, and large hierarchical machines where Mermaid grows unreadable.

Compound and parallel states become subgraph clusters, final states draw a double border, owners encode as node fillcolor, and the layout defaults to rankdir=LR (well suited to lifecycles).

func (m *Machine[S, E, C]) ToJSON(opts ...ToJSONOption) ([]byte, error)

ToJSON serializes the machine’s IR losslessly.

Example

ExampleMachine_ToJSON serializes a machine’s IR and reports that the canonical definition round-trips: loading the JSON and reserializing yields identical bytes.

m := buildDocMachine()
data, _ := m.ToJSON()
ir, _ := state.LoadFromJSON[DocState, DocEvent, *Document](data)
m2 := ir.Provide(docRegistry()).Quench()
data2, _ := m2.ToJSON()
fmt.Println("stable:", string(data) == string(data2))
// Output:
// stable: true
stable: true

func (m *Machine[S, E, C]) ToMermaid(opts ...VizOption) string

ToMermaid renders the machine as a GitHub-renderable Mermaid stateDiagram-v2.

Transitions render as labeled edges (Event, with guards as a bracketed suffix); the initial state is reached from the [*] start marker and final states point back to [*]. Compound states render as nested state blocks and parallel states use the — region divider. Owner tags render as classDef color-coding, since stateDiagram-v2 has no native swim lanes.

Example

ExampleMachine_ToMermaid renders a hierarchical machine as a Mermaid stateDiagram-v2: the initial marker, the Running superstate as a nested block with its own initial child, the cross-cutting Cancel transition, and the final-state markers.

fmt.Println(buildJobMachine().ToMermaid())
// Output:
// stateDiagram-v2
// [*] --> Queued
// state Running {
// [*] --> Running__Starting
// Running__Starting
// Running__Executing
// Running__Starting --> Running__Executing: Begin
// }
// JobDone --> [*]
// Canceled --> [*]
// Queued --> Running: Enqueue
// Running --> Canceled: Cancel
// Running__Executing --> JobDone: Finish
// classDef owner_Scheduler fill:#f0d9ff
// classDef owner_Worker fill:#d9f2f2
// class Queued owner_Scheduler
// class Running owner_Worker
stateDiagram-v2
[*] --> Queued
state Running {
[*] --> Running__Starting
Running__Starting
Running__Executing
Running__Starting --> Running__Executing: Begin
}
JobDone --> [*]
Canceled --> [*]
Queued --> Running: Enqueue
Running --> Canceled: Cancel
Running__Executing --> JobDone: Finish
classDef owner_Scheduler fill:#f0d9ff
classDef owner_Worker fill:#d9f2f2
class Queued owner_Scheduler
class Running owner_Worker

MessagePhase distinguishes the lifecycle point of an InspectMessage event: a message is observed when it is sent, and again when the host delivers it.

type MessagePhase string

const (
// MessageSent marks a message emitted toward a target actor (a SendTo /
// SendParent / Respond / Forward effect being routed).
MessageSent MessagePhase = "sent"
// MessageDelivered marks a message handed to its target actor's mailbox.
MessageDelivered MessagePhase = "delivered"
)

Middleware wraps a Fire, outside-in.

type Middleware[S comparable, E comparable, C any] func(next FireFunc[S, E, C]) FireFunc[S, E, C]

MultiRegionErr aggregates the errors raised by more than one orthogonal region firing on a single event. Its Unwrap returns each region’s error so errors.As finds any region’s typed error.

type MultiRegionErr struct {
Errors []error
}

func (e *MultiRegionErr) Error() string

func (e *MultiRegionErr) Unwrap() []error

Unwrap exposes the per-region errors for errors.As / errors.Is traversal.

Operand is a Core comparison operand: either a field-ref or a typed literal. It is produced by Field (via FieldOp), Str/Int/Float/Bool/Dur, or Param, and consumed by the FieldRef comparison methods. The zero Operand is invalid.

type Operand[S comparable] struct {
// contains filtered or unexported fields
}

func Bool[S comparable](v bool) Operand[S]

Bool builds a boolean literal operand.

func Dur[S comparable](v time.Duration) Operand[S]

Dur builds a duration literal operand, carried as its Go duration string.

func FieldOp[S comparable](f FieldRef[S]) Operand[S]

FieldOp wraps a field-ref as a comparison operand, so a comparison can put a field on either side (e.g. Field(“a”).Lt(FieldOp(Field(“b”)))).

func Float[S comparable](v float64) Operand[S]

Float builds a floating-point literal operand.

func Int[S comparable](v int64) Operand[S]

Int builds an integer literal operand.

func Param[S comparable](v string) Operand[S]

Param builds an enum-typed string literal operand — a named, schema-validated constant such as an order status. It is tagged EnumParam so a comparison against an enum-kinded context field type-checks, while still comparing as a string at evaluation.

func Str[S comparable](v string) Operand[S]

Str builds a string literal operand.

Outcome classifies the result recorded in a Trace.

type Outcome int

Outcomes recorded in a Trace, one per Fire: success or the specific failure class that stopped the transition. The values are a stable, ordered enumeration — new outcomes are appended, never reordered — so a recorded Trace stays comparable across versions and a consumer may switch on them safely.

const (
// OutcomeSuccess marks a Fire that matched a transition and settled cleanly.
OutcomeSuccess Outcome = iota
// OutcomeInvalidTransition marks a Fire where no transition matched (current,
// event), or every matching transition had a failing guard.
OutcomeInvalidTransition
// OutcomeGuardFailed marks a Fire stopped because a named guard returned false.
OutcomeGuardFailed
// OutcomeGuardPanic marks a Fire stopped because a guard panicked and was
// recovered.
OutcomeGuardPanic
// OutcomePolicyDenied marks a Fire stopped because a policy returned Deny.
OutcomePolicyDenied
// OutcomeEffectError marks a Fire stopped because a bound action returned an
// error while emitting its effect.
OutcomeEffectError
// OutcomeAssignFailed marks a Fire stopped because an assign reducer panicked or
// its ref did not resolve, so the context fold could not commit.
OutcomeAssignFailed
)

func (o Outcome) String() string

String renders the Outcome as its stable, lower-camel discriminant (“success”, “invalidTransition”, “guardFailed”, …) for logs, the structured- logging seam, and tooling. An unrecognized value renders as “outcome(N)”.

P is a convenience alias for serializable params attached to a named Ref.

type P = map[string]any

ParamSpec describes one parameter a ref accepts: its name, type, whether it is required, an optional human description, an optional default value, and — for EnumParam — the allowed values. It JSON-serializes cleanly for transport to a builder UI that renders a form control from it.

type ParamSpec struct {
Name string `json:"name"`
Type ParamType `json:"type"`
Required bool `json:"required,omitempty"`
Description string `json:"description,omitempty"`
Default any `json:"default,omitempty"`
// Enum lists the allowed values when Type is EnumParam; it is empty for every
// other type.
Enum []string `json:"enum,omitempty"`
}

ParamType is the value type of a single ref parameter, used by a UI to pick the right form control. It is a minimal, stdlib-only set and serializes as its lowercase string so the schema travels cleanly over an API.

type ParamType string

The parameter types. EnumParam additionally carries its allowed values on the owning ParamSpec via the Describe builder’s EnumParamOf helper.

const (
// StringParam is a free-form string.
StringParam ParamType = "string"
// IntParam is an integer.
IntParam ParamType = "int"
// FloatParam is a floating-point number.
FloatParam ParamType = "float"
// BoolParam is a boolean.
BoolParam ParamType = "bool"
// DurationParam is a time.Duration, conventionally carried as a Go duration
// string (e.g. "1500ms").
DurationParam ParamType = "duration"
// EnumParam is a string constrained to an enumerated set; the allowed values
// live on the ParamSpec.Enum field.
EnumParam ParamType = "enum"
)

PendingRefs is the descriptive inventory of an instance’s live timers, invoked services, and spawned actors at snapshot time, by stable ID. It mirrors what ResumeEffects re-arms; a host can assert on it or display it without replaying effects.

type PendingRefs struct {
// Timers are the schedule IDs of the pending delayed (`after`) transitions
// armed for the active configuration.
Timers []string `json:"timers,omitempty"`
// Services are the IDs of the invoked services running for the active
// configuration.
Services []string `json:"services,omitempty"`
// Actors are the IDs of the child-machine actors invoked for the active
// configuration.
Actors []string `json:"actors,omitempty"`
}

PlanOption configures PlanPath.

type PlanOption func(*planConfig)

ProvideOption configures Provide.

type ProvideOption func(*provideConfig)

QuenchOption configures Quench.

type QuenchOption func(*quenchConfig)

func Strict() QuenchOption

Strict makes Quench reject any lint warning, not just hard errors.

Ref is a named reference to a host-provided implementation plus serializable params. The IR carries Refs; the registry binds Name -> func at Provide/Quench time.

Meta is the reserved extension namespace at ref granularity. It is the attachment point for a future polyglot binding descriptor (under the reserved crucible.binding key): absent any descriptor, a ref resolves to an in-process Go registry entry, today’s behavior unchanged. The kernel never inspects Meta; it round-trips verbatim. extra preserves any unknown JSON keys a newer producer emitted so they survive a load -> save cycle (forward-compat).

type Ref struct {
Name string `json:"name"`
Params map[string]any `json:"params,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
// contains filtered or unexported fields
}

func (r Ref) MarshalJSON() ([]byte, error)

MarshalJSON encodes a Ref, merging its preserved unknown keys back in with stable key ordering.

func (r *Ref) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a Ref and captures any unknown keys into extra so they survive re-serialization.

Region is one orthogonal region of a parallel state: a self-contained set of substates with its own initial child. When the owning parallel state is active, every region is active simultaneously, each tracking its own leaf.

type Region[S comparable, E comparable, C any] struct {
Name string `json:"name"`
States []State[S, E, C] `json:"states,omitempty"`
InitialChild *S `json:"initialChild,omitempty"`
}

RegisterEffectOption configures a NewEffectRegistry call. New deserialization knobs arrive as new options, never as a signature change.

type RegisterEffectOption func(*EffectRegistry)

func RegisterEffect(kind string, factory EffectFactory) RegisterEffectOption

RegisterEffect registers a factory for an effect kind so the envelope decoder can route that kind back to a concrete effect. A later registration for the same kind overrides an earlier one (and overrides a built-in), letting a host swap a decoder while the kernel’s pre-registration stays the default.

Registry holds the host behavior palette, by name.

type Registry[C any] struct {
// contains filtered or unexported fields
}

func NewRegistry[C any]() *Registry[C]

NewRegistry returns an empty host registry.

func (r *Registry[C]) Action(name string, fn ActionFn[C], opts ...DescribeOption) *Registry[C]

Action registers a named action implementation. An optional Describe option adds palette metadata; registering without one still works and yields a minimal palette descriptor.

func (r *Registry[C]) Actor(name string, opts ...DescribeOption) *Registry[C]

Actor declares a named actor behavior in the registry’s palette. Actor behaviors bind at the host ActorSystem (Register), not at the registry, so this records only the palette metadata a builder needs to enumerate and configure the actor — it does not register a runnable behavior. An optional Describe option adds description, parameter schema, and read/write hints; declaring without one yields a minimal palette descriptor with just Kind and Name.

func (r *Registry[C]) Assign(name string, fn AssignFn[C], opts ...DescribeOption) *Registry[C]

Assign registers a named assign reducer — the sole context writer. The reducer takes the prior context by value, the triggering event, and the ref’s static params, and returns the next context; the kernel folds the assigns declared on a transition’s exit/transition/entry phases to produce the instance’s context. An optional Describe option adds palette metadata; registering without one still works and yields a minimal palette descriptor.

Naming: the assign verb appears three times with distinct roles. Registry.Assign (here) and its builder alias Builder.Reducer both REGISTER a reducer impl under a name; Builder.Assign WIRES a registered reducer (by name) onto a transition. So you register once (Reducer / Registry.Assign) and wire each use (.Assign(name)).

func (r *Registry[C]) BindGuard(name string, b GuardBinding[C], opts ...DescribeOption) *Registry[C]

BindGuard registers a guard under name from a GuardBinding directly, instead of from a plain GuardFn. It is the additive seam an opt-in expression module uses to register a guard whose verdict comes from a compiled expression program rather than a hand-written Go predicate: the module compiles its source once and hands the resulting evaluator in as the binding.

The binding is wired into the same name path Guard uses, so a guard registered this way is indistinguishable to the kernel from a Go-func guard — it resolves by name at Provide/Quench, evaluates synchronously inside the pure Fire step, and surfaces a panic as the same typed ErrGuardPanic. The binding’s EvalGuard is adapted to a GuardFn over the in-process context view so the fire-time fast path (which reads r.guards) finds it; the binding is also recorded on the parallel binding seam so a future out-of-process transport can swap it under the same name.

EvalGuard is called with a background context and the in-process context view; an error it returns is treated as a false verdict, matching how a Go guard that cannot decide yields false rather than transitioning. An optional Describe option adds palette metadata exactly as Guard does.

func (r *Registry[C]) Guard(name string, fn GuardFn[C], opts ...DescribeOption) *Registry[C]

Guard registers a named guard implementation. An optional Describe option adds palette metadata (description, parameter schema, read/write hints); registering without one still works and yields a minimal palette descriptor.

func (r *Registry[C]) Palette() []Descriptor

Palette returns a descriptor for every consumer-registered guard, action, service, and actor behavior in the registry, sorted deterministically by kind then name. Entries registered without a Describe descriptor still appear, carrying a minimal descriptor with just Kind and Name. Built-in actions (spawn/cancel/send/raise) and the stateIn guard are language-level, not registered, and are intentionally excluded; BuiltinPalette lists those.

The returned slice is freshly allocated each call and safe for the caller to retain or mutate.

Example

ExampleRegistry_Palette registers a described guard and action, then prints the discoverable palette a visual builder reads to render a form for each ref. The palette is sorted deterministically (by kind, then name) and JSON-serializes cleanly for transport over a builder API.

package main
import (
"encoding/json"
"fmt"
"github.com/stablekernel/crucible/state"
)
// cart is the entity the palette example registers behavior against.
type cart struct {
amount int
}
// ExampleRegistry_Palette registers a described guard and action, then prints the
// discoverable palette a visual builder reads to render a form for each ref. The
// palette is sorted deterministically (by kind, then name) and JSON-serializes
// cleanly for transport over a builder API.
func main() {
reg := state.NewRegistry[cart]()
reg.Guard("minAmount", func(c state.GuardCtx[cart]) bool { return c.Entity.amount >= 1 },
state.Describe("Passes when the amount is at least min.").
Param("min", state.IntParam).
OptionalParam("currency", state.StringParam).
Reads("Cart"))
reg.Action("charge", func(state.ActionCtx[cart]) (state.Effect, error) { return nil, nil },
state.Describe("Charges the cart through the named gateway.").
Param("gateway", state.StringParam).
Writes("Cart"))
out, _ := json.MarshalIndent(reg.Palette(), "", " ")
fmt.Println(string(out))
}
[
{
"kind": "action",
"name": "charge",
"description": "Charges the cart through the named gateway.",
"params": [
{
"name": "gateway",
"type": "string",
"required": true
}
],
"writes": [
"Cart"
]
},
{
"kind": "guard",
"name": "minAmount",
"description": "Passes when the amount is at least min.",
"params": [
{
"name": "min",
"type": "int",
"required": true
},
{
"name": "currency",
"type": "string"
}
],
"reads": [
"Cart"
]
}
]

func (r *Registry[C]) Service(name string, fn ServiceFn[C], opts ...DescribeOption) *Registry[C]

Service registers a named invoked-service implementation. An invoke’s Src ref binds to it at Provide/Quench time exactly like a guard or action ref; an unbound service ref fails Quench with the typed *ErrUnboundRef (Kind “service”). The runner resolves and runs it when the owning state is entered. An optional Describe option adds palette metadata; registering without one still works and yields a minimal palette descriptor.

Requirement is a declarative condition for a state, used by Assay.

type Requirement[C any] struct {
Name string
Predicate func(C) bool
Setter func(C) // optional: mutate a zero entity to satisfy Predicate
}

RequirementFailure records one unmet requirement.

type RequirementFailure struct {
Name string
Reason string
}

RespondToSender is the effect the kernel emits for the respond built-in: reply with Event to the sender of the event the emitting actor is currently handling. The kernel cannot know the sender (it is host routing state), so it emits this effect with only the reply Event; the host’s ActorSystem resolves the target from the routing context it recorded when it delivered the current event. When there is no identifiable sender the host treats it as a no-op. This realizes the reply-to-the-event’s-origin semantic.

type RespondToSender struct {
// Event is the serializable reply delivered to the current event's sender,
// type-erased for the abstract effect surface; an ActorSystem keeps it typed.
Event any `json:"event,omitempty"`
}

func (RespondToSender) Kind() string

Kind reports the respond-to-sender effect discriminant.

RestoreOption configures Machine.Restore.

type RestoreOption[S comparable] func(*restoreConfig[S])

func RejectMachineVersionMismatch[S comparable]() RestoreOption[S]

RejectMachineVersionMismatch makes Restore enforce the machine DEFINITION version strictly: a snapshot whose MachineVersion differs from the target machine’s version is rejected with a typed *SnapshotVersionError instead of the default advisory (accept) posture. Use it when an instance must only resume against the exact machine version it was snapshotted from. The snapshot-format schema version is always validated regardless of this option.

func WithRestoreClock[S comparable](c Clock) RestoreOption[S]

WithRestoreClock wires the time seam a restored instance’s delayed-transition driver reads, mirroring WithClock at Cast. It is consumed only by a Scheduler / host driver, never by the pure Fire step. When omitted, a restored instance defaults to SystemClock().

ScheduleAfter is the effect the kernel emits when an instance enters a state that declares a delayed (`after`) transition. The host’s runtime is expected to start a timer for Delay and, when it elapses, call Fire with Event. ID is stable per (instance, source state, delayed edge), so a later CancelScheduled with the same ID cancels exactly this timer.

The kernel never starts the timer itself: it emits this as data alongside the transition’s other effects, keeping Fire pure (no clock, no goroutine, no IO).

type ScheduleAfter struct {
// ID identifies the pending timer. It is stable across the schedule/cancel
// pair for one source state on one instance, so a host keys its timer table
// by ID.
ID string `json:"id"`
// Delay is the wall-clock duration the host should wait before re-firing.
Delay time.Duration `json:"delay"`
// Event is the delayed event to feed back through Fire when Delay elapses.
// It is the transition's On event, type-erased for the abstract effect
// surface; a host driver built with NewScheduler keeps it typed.
Event any `json:"event,omitempty"`
// State names the source state whose entry scheduled this timer, for
// diagnostics and host bookkeeping.
State string `json:"state,omitempty"`
}

func (ScheduleAfter) Kind() string

Kind reports the schedule-after effect discriminant.

Scheduler is the reusable host-driver that turns the kernel’s ScheduleAfter / CancelScheduled effects into real timers and re-fires delayed events through its instance. It is concurrency-safe. Construct one per instance with NewScheduler; drive it by passing each Fire’s effects to Absorb. With a FakeClock it is fully deterministic — timers fire only when the test advances the clock via FakeClock.Advance.

type Scheduler[S comparable, E comparable, C any] struct {
// contains filtered or unexported fields
}
Example

ExampleScheduler drives a delayed (`after`) transition deterministically with a FakeClock and the reusable Scheduler host-driver. The kernel stays pure: entering “pending” emits a ScheduleAfter effect, the Scheduler arms a timer, and advancing the fake clock past the delay fires the delayed event back through Fire — driving a delayed (after) transition with no real waiting.

package main
import (
"context"
"fmt"
"time"
"github.com/stablekernel/crucible/state"
)
func main() {
type cart struct{}
m := state.Forge[string, string, cart]("checkout").
State("active").
State("pending").
State("expired").
Initial("active").
Transition("active").On("submit").GoTo("pending").
// After 15 minutes in "pending" with no action, the cart expires.
Transition("pending").After(15 * time.Minute).On("timeout").GoTo("expired").
State("expired").Final().
Quench()
clk := state.NewFakeClock(time.Unix(0, 0))
inst := m.Cast(cart{}, state.WithInitialState("active"), state.WithClock[string](clk))
sch := state.NewScheduler(inst)
ctx := context.Background()
// Entering "pending" emits the ScheduleAfter effect; the host absorbs every
// Fire's effects into the Scheduler, which arms the timer.
res := inst.Fire(ctx, "submit")
sch.Absorb(ctx, res.Effects)
fmt.Println("before:", inst.Current(), "pending timers:", sch.Pending())
// Nothing happens until the delay elapses; advancing the fake clock and
// ticking the Scheduler fires the delayed "timeout" event.
clk.Advance(15 * time.Minute)
sch.Tick(ctx)
fmt.Println("after: ", inst.Current(), "pending timers:", sch.Pending())
}
before: pending pending timers: 1
after: expired pending timers: 0

func NewScheduler[S comparable, E comparable, C any](inst *Instance[S, E, C]) *Scheduler[S, E, C]

NewScheduler returns a Scheduler driving inst, reading the time seam wired to inst at Cast (WithClock). With a FakeClock the Scheduler is deterministic.

func (s *Scheduler[S, E, C]) Absorb(ctx context.Context, effects []Effect)

Absorb scans effects, arming a timer for each ScheduleAfter and dropping the timer for each CancelScheduled. It is how a host wires Fire’s output back into the scheduler; call it with the effects of every Fire (including those the Scheduler itself triggers — Fire-on-elapse re-enters Absorb automatically). A ScheduleAfter whose Event is not the instance’s event type is ignored, since the kernel cannot have produced it.

func (s *Scheduler[S, E, C]) HasPending(id string) bool

HasPending reports whether a timer with the given schedule id is armed.

func (s *Scheduler[S, E, C]) Pending() int

Pending reports the number of armed (not-yet-fired, not-canceled) timers. A test asserts on it to confirm a timer was scheduled or auto-canceled on exit.

func (s *Scheduler[S, E, C]) Tick(ctx context.Context) []FireResult[S]

Tick fires every timer whose due time is at or before the Scheduler clock’s current time, in due-time order (ties broken by id for determinism). Each due timer is removed, then its delayed event is fired through the instance and the resulting effects are absorbed (so a chained `after` arms its successor). It returns the FireResults of the events it fired, in order. With a FakeClock a test calls FakeClock.Advance then Tick (or uses the Advance helper) to drive elapses deterministically; with SystemClock a host calls Tick from its own timer loop.

SchemaField is one named field of a context object: its name, its type kind, whether it is nullable (a Go pointer or other nilable type), and the kind-specific shape carried on Fields (object), Elem (list element, map value), Key (map key), and Enum (enum values).

type SchemaField struct {
// Name is the field's wire name — the JSON-tag name for a SchemaOf-derived
// struct field, the Go field name when no JSON tag is present.
Name string `json:"name"`
// Kind is the field's type category.
Kind SchemaKind `json:"kind"`
// Nullable reports whether the field may be absent/nil (a Go pointer, or a
// natively nilable map/slice). It is informational metadata; the kernel never
// enforces it.
Nullable bool `json:"nullable,omitempty"`
// Fields carries the nested named fields when Kind is SchemaObject.
Fields []SchemaField `json:"fields,omitempty"`
// Elem carries the element type when Kind is SchemaList, or the value type when
// Kind is SchemaMap.
Elem *SchemaField `json:"elem,omitempty"`
// Key carries the key type when Kind is SchemaMap.
Key *SchemaField `json:"key,omitempty"`
// Enum lists the allowed values when Kind is SchemaEnum; it is empty otherwise.
Enum []string `json:"enum,omitempty"`
// contains filtered or unexported fields
}

func (f SchemaField) MarshalJSON() ([]byte, error)

MarshalJSON encodes a SchemaField, merging its preserved unknown keys back in with stable key ordering.

func (f *SchemaField) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a SchemaField and captures any unknown keys into extra so they survive re-serialization.

SchemaKind names the type category of a context field. The scalar kinds reuse the ParamType vocabulary verbatim (string/int/float/bool/duration, plus the time scalar and enum); the composite kinds — object, list, map — describe structured shapes that ParamType does not cover. It serializes as its lowercase string for a stable, language-neutral wire form.

type SchemaKind string

The schema kinds. Scalars share their wire string with the matching ParamType so a single vocabulary spans both the param schema and the context schema.

const (
// SchemaString is a free-form string.
SchemaString SchemaKind = "string"
// SchemaInt is an integer.
SchemaInt SchemaKind = "int"
// SchemaFloat is a floating-point number.
SchemaFloat SchemaKind = "float"
// SchemaBool is a boolean.
SchemaBool SchemaKind = "bool"
// SchemaDuration is a time.Duration, conventionally carried as a Go duration
// string (e.g. "1500ms").
SchemaDuration SchemaKind = "duration"
// SchemaTime is a time.Time, conventionally carried as an RFC 3339 string.
SchemaTime SchemaKind = "time"
// SchemaObject is a nested object with named fields, carried on Fields.
SchemaObject SchemaKind = "object"
// SchemaList is an ordered list whose element type is carried on Elem.
SchemaList SchemaKind = "list"
// SchemaMap is a keyed map whose key and value types are carried on Key and
// Elem.
SchemaMap SchemaKind = "map"
// SchemaEnum is a string constrained to an enumerated set carried on Enum.
SchemaEnum SchemaKind = "enum"
)

SendOption configures a Builder.SendTo / Builder.ForwardTo declaration (the actor-communication send built-ins).

type SendOption func(*sendConfig)

func WithSendToSystemID(id string) SendOption

WithSendToSystemID addresses the send target by its system-scoped id (the `systemId`) instead of its registry id, so a sibling actor is addressed by a well-known name. When set it takes precedence over the positional target id.

SendParent is the effect the kernel emits for the sendParent built-in: a child actor sends Event to its parent. The host’s ActorSystem routes it to the parent instance (the one driving the system). Emitted by a top-level machine with no parent it is a host-side no-op. It routes an event to the actor’s parent.

type SendParent struct {
// Event is the serializable event delivered to the parent, type-erased for the
// abstract effect surface; an ActorSystem keeps it typed.
Event any `json:"event,omitempty"`
}

func (SendParent) Kind() string

Kind reports the send-parent effect discriminant.

SendTo is the effect the kernel emits for the sendTo built-in: deliver Event to the actor addressed by TargetID (or SystemID when TargetID is empty). The host’s ActorSystem routes it into that actor’s mailbox; addressing an unknown actor is a no-op. It delivers an event to a named actor.

type SendTo struct {
// TargetID is the registry id of the actor to deliver Event to. Empty when the
// target is addressed by SystemID instead.
TargetID string `json:"targetId,omitempty"`
// SystemID is the system-scoped name of the target actor (its systemId),
// used when TargetID is empty so a sibling can be addressed by a well-known name.
SystemID string `json:"systemId,omitempty"`
// Event is the serializable event delivered to the target actor's mailbox,
// type-erased for the abstract effect surface; an ActorSystem keeps it typed.
Event any `json:"event,omitempty"`
}

func (SendTo) Kind() string

Kind reports the send-to effect discriminant.

ServiceBinding runs an invoked service. The in-process binding wraps a ServiceFn; the result is shuttled by the runner through the invocation’s onDone/onError event.

type ServiceBinding[C any] interface {
RunService(ctx context.Context, req ServiceRequest[C]) (any, error)
}

ServiceCtx is passed to a bound service at run time. It carries the entity the instance is bound to and the start contract the kernel emitted.

Under a value context type, Entity is a point-in-time snapshot taken when the service is invoked: it does not observe context updates that assigns apply on Fires running while the service is in flight. To act on newer context a service returns data, which the runner routes back through the onDone/onError event so a transition assign folds it — Fire, not the service, owns every context change. (Under a pointer context type the snapshot is a copied pointer to the same value, so a long-running service can observe later mutations through the alias; that is the documented escape-hatch tradeoff.)

type ServiceCtx[C any] struct {
Entity C
Params map[string]any
Input map[string]any
}

ServiceFn is a host-provided invoked-service implementation, bound by name into a Registry exactly like a guard or action. It receives the entity it is bound to and the StartService effect (Src params, Input) the kernel emitted, and returns its result on success or an error on failure. A one-shot (promise-style) service returns directly; a streaming service is a host-side wrapper that ultimately resolves to a single done/error through this contract. A ServiceFn never mutates the instance; it returns data, and the runner routes that data through the invocation’s onDone / onError event via Fire — so Fire, not the service, owns every state change.

type ServiceFn[C any] func(ctx context.Context, in ServiceCtx[C]) (any, error)

ServiceRequest is the serializable invocation envelope for an invoked service. The service result is routed back through the kernel’s existing onDone/onError event machinery (the StartService effect), so it needs no result envelope here.

type ServiceRequest[C any] struct {
Name string
Params map[string]any
Input map[string]any
}

ServiceRunner is the reusable host-driver that turns the kernel’s StartService / StopService effects into real service executions and re-fires each result through its instance via the invocation’s onDone / onError event. It is concurrency-safe. Construct one per instance with NewServiceRunner, binding the service registry that resolves Src refs; drive it by passing each Fire’s effects (and the instance’s StartEffects) to Absorb.

In the deterministic form the runner records each started service as pending and settles it only when the test calls SettleDone / SettleError, so invoke machines are exercised with no real IO; a production host instead resolves and runs the bound ServiceFn on its own goroutine and calls SettleDone / SettleError (or the convenience Run) when it finishes.

type ServiceRunner[S comparable, E comparable, C any] struct {
// contains filtered or unexported fields
}

func NewServiceRunner[S comparable, E comparable, C any](inst *Instance[S, E, C], reg *Registry[C]) *ServiceRunner[S, E, C]

NewServiceRunner returns a ServiceRunner driving inst, resolving Src refs against reg’s service palette. reg may be nil for a pure deterministic driver that never resolves a ServiceFn (the test settles services directly by ID).

func (r *ServiceRunner[S, E, C]) Absorb(ctx context.Context, effects []Effect)

Absorb scans effects, recording a running service for each StartService and dropping the running service for each StopService (auto-stop-on-exit). It is how a host wires Fire’s output back into the runner; call it with the effects of every Fire (and once with the instance’s StartEffects for the initial state). A StartService whose OnDone/OnError is not the instance’s event type is ignored, since the kernel cannot have produced it.

func (r *ServiceRunner[S, E, C]) HasPending(id string) bool

HasPending reports whether a service with the given invoke id is in flight.

func (r *ServiceRunner[S, E, C]) LastError() error

LastError returns the error the most recently settled service produced, or nil when the last settlement was a success or none has occurred. The host action bound to an onError transition reads it to consume the failure.

func (r *ServiceRunner[S, E, C]) LastResult() (any, bool)

LastResult returns the result the most recently settled service produced, and true when that settlement was a success (SettleDone). The host action bound to an onDone transition reads it to consume the service output; it is valid only during the synchronous Fire the settlement triggers. It returns false after a SettleError or before any settlement.

func (r *ServiceRunner[S, E, C]) Pending() int

Pending reports the number of in-flight (started, not-yet-settled, not-stopped) services. A test asserts on it to confirm a service was started or auto-stopped on exit.

func (r *ServiceRunner[S, E, C]) PendingIDs() []string

PendingIDs returns the ids of all in-flight services, sorted, for deterministic host iteration (e.g. running every armed service in a stable order).

func (r *ServiceRunner[S, E, C]) Run(ctx context.Context, id string) (FireResult[S], bool)

Run resolves and runs the in-flight service id against the bound registry, settling it with the ServiceFn’s result or error. It is the production convenience that couples resolve + run + settle: a host that arms services from Absorb and wants the runner to execute them calls Run(ctx, id) (typically from its own goroutine). It returns the routed FireResult and true, or false when id is not in flight or no registry / ServiceFn resolves it (in which case the service is settled as an error so the machine still routes onError rather than hanging).

func (r *ServiceRunner[S, E, C]) SettleDone(ctx context.Context, id string, result any) (FireResult[S], bool)

SettleDone completes the in-flight service id successfully: it drops the service and fires its OnDone event (carrying result) through the instance, then absorbs the resulting effects so a chained invoke arms its successor. It returns the FireResult and true, or the zero result and false when id names no in-flight service (already stopped or settled). result is delivered to the onDone transition’s effects through the instance entity by the host’s actions — the kernel routes the event; the action reads the result.

func (*ServiceRunner[S, E, C]) SettleError

Section titled “func (*ServiceRunner[S, E, C]) SettleError”
func (r *ServiceRunner[S, E, C]) SettleError(ctx context.Context, id string, err error) (FireResult[S], bool)

SettleError fails the in-flight service id: it drops the service and fires its OnError event (carrying err) through the instance, then absorbs the resulting effects. It returns the FireResult and true, or the zero result and false when id names no in-flight service.

Snapshot is the serializable, deep runtime state of one Instance at a point in time. It captures the active configuration (all active leaves, in declaration order, plus the primary leaf), the recorded per-compound history (shallow and deep), the instance context, the lifecycle status and optional output/error, and the metadata of the pending timers, invoked services, and spawned actors so a host can re-arm them on restore. Child-actor snapshots are carried under Actors when an ActorSystem snapshots the instance’s spawned children recursively.

A Snapshot round-trips losslessly through JSON when the context type C is JSON-marshalable (the default requirement) or a context codec is supplied via WithContextCodec. The machine definition is NOT carried here — restore binds the snapshot back to a live Machine, exactly as Cast binds an entity — so a snapshot stays small and a definition change is detected at restore rather than silently absorbed.

type Snapshot[S comparable, E comparable, C any] struct {
// Machine names the machine the snapshot was taken from. Restore rejects a
// snapshot whose Machine does not match the target machine with a typed
// *SnapshotError, so a snapshot is never restored against the wrong definition.
Machine string `json:"machine"`
// Current is the primary (first) active leaf — the back-compatible
// "what state am I in?" answer, equal to Configuration[0].
Current S `json:"current"`
// Configuration is every currently-active leaf, in declaration order: length 1
// for a flat or single-spine instance, length N when N parallel regions are
// active. Restore activates exactly this configuration without re-entering it.
Configuration []S `json:"configuration"`
// Context is the instance's bound entity C at snapshot time. With the default
// codec it must be JSON-marshalable; with WithContextCodec the supplied codec
// owns its encoding. In JSON it is held as a raw message so the snapshot
// envelope marshals once and the context decodes through the chosen codec.
Context C `json:"-"`
// ContextRaw is the JSON (or codec-encoded) form of Context, populated when the
// snapshot is marshaled and consumed when it is unmarshaled. It is the wire
// form of Context; callers read Context, not ContextRaw.
ContextRaw json.RawMessage `json:"context,omitempty"`
// HistoryShallow records each compound's last-active direct child, and
// HistoryDeep each compound's last-active leaf configuration, for history
// pseudo-state restoration. Both are restored verbatim so a history-targeted
// transition after restore behaves identically to before the snapshot.
HistoryShallow map[S]S `json:"historyShallow,omitempty"`
HistoryDeep map[S][]S `json:"historyDeep,omitempty"`
// Traces is the instance's recorded Fire history, preserved so History()
// reports the same ordered traces after restore.
Traces []Trace `json:"traces,omitempty"`
// Status is the instance's lifecycle status at snapshot time. Output carries an
// instance's completion output (when StatusDone) and Error a settled instance's
// failure message (when StatusError); both are optional and host-supplied.
Status Status `json:"status"`
Output json.RawMessage `json:"output,omitempty"`
Error string `json:"error,omitempty"`
// Pending records the IDs/metadata of the timers, invoked services, and spawned
// actors that were live for the active configuration, so a host can confirm
// what ResumeEffects re-arms. It is descriptive: the authoritative re-arm is the
// effect slice ResumeEffects returns, derived from the same configuration.
Pending PendingRefs `json:"pending,omitempty"`
// Actors carries the recursively-captured snapshots of the instance's spawned
// child actors, keyed by actor id, when an ActorSystem snapshots the instance.
// Each entry is an opaque per-child snapshot envelope a matching ActorSystem
// restores. It is empty for an instance with no spawned children, or when only
// the instance core (not the actor tree) is snapshotted.
Actors map[string]json.RawMessage `json:"actors,omitempty"`
// SnapshotVersion is the snapshot-format schema version of this envelope, so the
// serialization contract can evolve with explicit, detectable versions. Snapshot
// stamps it with CurrentSnapshotVersion; Restore validates it under the lenient
// restore-version posture (accept within the current major, reject across a major
// mismatch). A zero value is a pre-versioning snapshot and is treated as the
// current version on restore.
SnapshotVersion int `json:"snapshotVersion,omitempty"`
// MachineVersion is the machine DEFINITION version (the IR Version) the snapshot
// was taken from, stamped alongside the Machine name so a restored instance
// self-identifies which version of the machine it belongs to — the precondition
// for live migration. It is advisory by default at restore (recorded, surfaced,
// not enforced) so version stamping is non-breaking; RejectMachineVersionMismatch
// opts into strict rejection.
MachineVersion string `json:"machineVersion,omitempty"`
// MachineID is the machine definition id (the IR ID), carried alongside
// MachineVersion so a migrator can resolve the source definition unambiguously.
MachineID string `json:"machineId,omitempty"`
// Journal is the reserved replay journal: the per-step record of external,
// nondeterministic results (invoked-service done-output, actor messages, clock
// reads, randomness) so a future deterministic replay returns the recorded value
// rather than re-invoking the source. It is empty at this version under the
// recording contract documented on JournalEntry; the runtime that populates and
// consumes it is host-side. Reserved and optional: it round-trips empty and
// populated.
Journal []JournalEntry `json:"journal,omitempty"`
// InFlightServices is the reserved slot for invoked services that were started
// but not yet resolved at snapshot time (id + input + the OnDone/OnError routing
// events), so a future distributed/async resume can re-establish them. Empty at
// this version under the quiescence assumption; present so resume never needs a
// new field.
InFlightServices []InFlightService `json:"inFlightServices,omitempty"`
// Mailboxes is the reserved slot for per-actor mailbox backlog (queued but
// unprocessed envelopes), keyed by actor id, for a future distributed/async
// resume where a node can crash mid-delivery. Empty at this version under the
// quiescence assumption (mailboxes are drained at a snapshot point); present so a
// backlog never needs a new field. This closes the documented mailbox-loss gap in
// the actor-tree snapshot.
Mailboxes map[string][]json.RawMessage `json:"mailboxes,omitempty"`
}

func UnmarshalSnapshot[S comparable, E comparable, C any](b []byte, opts ...SnapshotCodecOption[C]) (Snapshot[S, E, C], error)

UnmarshalSnapshot deserializes a snapshot from JSON, decoding its context through codec (or the default JSON codec when codec is nil). It is the inverse of MarshalSnapshot; for a JSON-marshalable context, json.Unmarshal into a Snapshot works directly via the snapshot’s own UnmarshalJSON.

func WaitFor[S comparable, E comparable, C any](ctx context.Context, inst *Instance[S, E, C], predicate WaitPredicate[S, E, C], opts ...WaitOption[S, E, C]) (Snapshot[S, E, C], error)

WaitFor drives inst until predicate holds over its Snapshot, or until the supplied context is canceled or the wait budget elapses. It returns the matching snapshot on success, or the zero snapshot and a typed *WaitTimeoutError when the budget elapses (or the wrapped context error when ctx is canceled) without the predicate ever holding.

The predicate is checked once immediately, before any advance, so an instance already in the desired state returns at once without driving. When it does not yet hold, WaitFor advances its driver one step at a time and rechecks: by default it ticks a Scheduler over a FakeClock (WithWaitScheduler), advancing the fake clock by a fixed step each iteration so `after` machines progress deterministically; a caller with a different driver supplies WithWaitStep.

With no driver option WaitFor cannot make progress (an undriven instance never changes on its own), so it checks the predicate once and, if unmet, waits out the budget and returns the typed timeout — the correct result for “the instance will never reach this state without being driven”.

WaitFor never reads the wall clock: time is measured by the driver’s clock (the Scheduler’s, a FakeClock in tests), so the whole helper is deterministic under a fake clock.

func (snap Snapshot[S, E, C]) MarshalJSON() ([]byte, error)

MarshalJSON serializes the snapshot, encoding its context with the default JSON codec. It is the convenient path for a JSON-marshalable context; a context that needs a custom codec is serialized with MarshalSnapshot(snap, WithContextCodec).

func (snap *Snapshot[S, E, C]) UnmarshalJSON(b []byte) error

UnmarshalJSON deserializes the snapshot, decoding its context with the default JSON codec. The inverse of MarshalJSON.

SnapshotCodecOption configures MarshalSnapshot / UnmarshalSnapshot.

type SnapshotCodecOption[C any] func(*snapshotCodecConfig[C])

func WithContextCodec[C any](codec ContextCodec[C]) SnapshotCodecOption[C]

WithContextCodec supplies a custom ContextCodec for a snapshot context that is not directly JSON-marshalable (or needs a bespoke wire form). When omitted, the default codec marshals the context with encoding/json, so the context type must be JSON-marshalable by default. Pass it to MarshalSnapshot / UnmarshalSnapshot to override the default.

SnapshotError is returned by Restore / MarshalSnapshot / UnmarshalSnapshot when an instance snapshot cannot be captured, serialized, or restored: a snapshot whose Machine does not match the target, a configuration leaf that is not a declared state, an empty configuration with an unknown current state, or a context encode/decode failure. Op names the failing operation (“restore” | “marshal” | “unmarshal”), State (when set) names the offending configuration leaf, and Reason carries the detail.

type SnapshotError struct {
Op string
State string
Reason string
}

func (e *SnapshotError) Error() string

SnapshotVersionError is returned by Restore when a snapshot’s version identity is incompatible with the target: a snapshot-format schema version across a major boundary (always rejected, under the lenient restore-version posture), or — only when RejectMachineVersionMismatch is set — a machine definition version that does not match the target machine. Kind discriminates the two (“snapshotFormat” | “machineVersion”); Machine names the target; Got and Want carry the offending and expected versions; Reason carries the detail. It is the typed signal a migrator or host keys version-mismatch handling on.

type SnapshotVersionError struct {
Kind string
Machine string
Got string
Want string
Reason string
}

func (e *SnapshotVersionError) Error() string

Snapshotter is implemented by an ActorInstance that can capture and reload its own runtime state as JSON, so an ActorSystem can persist it recursively. The actorAdapter (the standard wrapper for a child *Instance) satisfies it; a host’s bespoke ActorInstance may implement it to participate in deep persistence, and an ActorInstance that does not is re-spawned fresh on restore rather than resumed.

type Snapshotter interface {
// SnapshotJSON captures the actor's runtime state as JSON.
SnapshotJSON() ([]byte, error)
// RestoreJSON reloads the actor's runtime state from JSON produced by
// SnapshotJSON, resuming the actor in place without re-running entry actions.
RestoreJSON([]byte) error
}

SpawnActor is the effect the kernel emits when an instance enters a state that invokes a child MACHINE actor, or when the built-in spawn action runs. The host’s ActorSystem is expected to create the actor named by Src (resolved to a child machine factory against the system’s actor palette), run it with Input, register it under ID, and — when the child reaches its final state — re-fire OnDone (carrying the child’s output) through the PARENT’s Fire, or on the child’s failure re-fire OnError. ID is stable per (instance, owning state, invoke index) for a static invoke, or carried explicitly for a dynamic spawn, so a later StopActor with the same ID stops exactly this actor.

The kernel never runs the actor itself: it emits this as data alongside the transition’s other effects, keeping Fire pure (no goroutine, no mailbox, no IO).

type SpawnActor struct {
// ID identifies the spawned actor. It is stable across the spawn/stop pair for
// one owning state on one instance (static invoke) or supplied explicitly (a
// dynamic spawn), so a host keys its actor registry by ID.
ID string `json:"id"`
// Src is the actor ref (name + params) the host resolves against its actor
// palette to obtain the child machine to run.
Src Ref `json:"src"`
// Input is the serializable input passed to the child actor at spawn. It
// is data only; the kernel never inspects it.
Input map[string]any `json:"input,omitempty"`
// OnDone is the event the host re-fires through the PARENT's Fire (carrying the
// child's output) when the child actor reaches its final state, type-erased for
// the abstract effect surface; an ActorSystem keeps it typed.
OnDone any `json:"onDone,omitempty"`
// OnError is the event the host re-fires through the PARENT's Fire (carrying the
// error) when the child actor fails, type-erased for the abstract effect
// surface.
OnError any `json:"onError,omitempty"`
// State names the owning state whose entry spawned this actor, for diagnostics
// and host bookkeeping. Empty for a dynamic spawn emitted from a transition.
State string `json:"state,omitempty"`
// SystemID is the optional, stable system-scoped identifier the actor registers
// under in the ActorSystem (its systemId), so a sibling can address it
// by a well-known name rather than by ref. Empty when unset.
SystemID string `json:"systemId,omitempty"`
}

func (SpawnActor) Kind() string

Kind reports the spawn-actor effect discriminant.

SpawnOption configures a Builder.Spawn declaration (the dynamic spawn built-in).

type SpawnOption func(*spawnConfig)

func WithSpawnInput(input map[string]any) SpawnOption

WithSpawnInput sets the serializable input passed to a dynamically spawned actor when it is created, surfaced as input on the SpawnActor effect.

func WithSpawnOnDone[E comparable](onDone E) SpawnOption

WithSpawnOnDone sets the event the host re-fires through the parent’s Fire when a dynamically spawned actor reaches its final state, routing the child’s output through an ordinary transition from the spawning state. Omit it for a fire-and-forget spawn whose completion the parent does not observe.

func WithSpawnOnError[E comparable](onError E) SpawnOption

WithSpawnOnError sets the event the host re-fires through the parent’s Fire when a dynamically spawned actor fails, routing the error through an ordinary transition from the spawning state.

func WithSpawnSystemID(id string) SpawnOption

WithSpawnSystemID sets the system-scoped name a dynamically spawned actor registers under in the ActorSystem (its systemId).

StartService is the effect the kernel emits when an instance enters a state that declares an invoked service. The host is expected to run the service named by Src with Input and, on completion, re-fire OnDone with the result through Fire, or on failure re-fire OnError with the error. ID is stable per (instance, owning state, invoke index), so a later StopService with the same ID stops exactly this service.

The kernel never runs the service itself: it emits this as data alongside the transition’s other effects, keeping Fire pure (no goroutine, no IO).

type StartService struct {
// ID identifies the running service. It is stable across the start/stop pair
// for one owning state on one instance, so a host keys its service table by ID.
ID string `json:"id"`
// Src is the service ref (name + params) the host resolves against its service
// registry to obtain the implementation to run.
Src Ref `json:"src"`
// Input is the serializable input passed to the service at start.
Input map[string]any `json:"input,omitempty"`
// OnDone is the event the host re-fires (with the service result) when the
// service completes successfully, type-erased for the abstract effect surface;
// a host driver built with NewServiceRunner keeps it typed.
OnDone any `json:"onDone,omitempty"`
// OnError is the event the host re-fires (with the error) when the service
// fails, type-erased for the abstract effect surface.
OnError any `json:"onError,omitempty"`
// State names the owning state whose entry started this service, for
// diagnostics and host bookkeeping.
State string `json:"state,omitempty"`
}

func (StartService) Kind() string

Kind reports the start-service effect discriminant.

State is a node in the machine graph.

A state is one of three shapes: a leaf (no Children, no Regions), a compound (hierarchical) state declaring Children plus an InitialChild, or a parallel state declaring Regions. A state is never both compound and parallel.

type State[S comparable, E comparable, C any] struct {
Name S `json:"name"`
OwnedBy string `json:"ownedBy,omitempty"`
Transitions []Transition[S, E, C] `json:"transitions,omitempty"`
OnEntry []Ref `json:"onEntry,omitempty"`
OnExit []Ref `json:"onExit,omitempty"`
IsFinal bool `json:"isFinal,omitempty"`
OnDone []Ref `json:"onDone,omitempty"`
// OnEntryAssign and OnExitAssign list the context-reducer refs folded on this
// state's entry and exit respectively — the assign siblings of OnEntry/OnExit.
// Exit assigns fold before transition assigns; entry assigns fold after, each
// seeing the prior result. Both serialize and round-trip losslessly through JSON.
OnEntryAssign []Ref `json:"onEntryAssign,omitempty"`
OnExitAssign []Ref `json:"onExitAssign,omitempty"`
// Hierarchy. Children holds the nested substates of a compound state, and
// InitialChild names the substate entered transitively when the compound
// state is entered. Both serialize, so the hierarchy round-trips through
// JSON. Parent is a runtime-only back-pointer rebuilt after Quench/Provide.
Children []State[S, E, C] `json:"children,omitempty"`
InitialChild *S `json:"initialChild,omitempty"`
// Regions holds the orthogonal regions of a parallel state. Mutually
// exclusive with Children/InitialChild.
Regions []Region[S, E, C] `json:"regions,omitempty"`
// History. HistoryType marks this node as a history pseudo-state (shallow or
// deep) belonging to its parent compound; HistoryNone (the default) is an
// ordinary state. HistoryDefault names the target entered when the owning
// compound has no recorded history yet; nil falls back to the compound's
// InitialChild. Both serialize, so history pseudo-states round-trip through
// JSON; the per-instance recorded configuration is runtime state, not IR.
HistoryType HistoryType `json:"historyType,omitempty"`
HistoryDefault *S `json:"historyDefault,omitempty"`
// Invoke declares the services invoked while this state is active (the
// `invoke`). Entering the state emits a StartService effect per invocation;
// exiting it before a service completes emits a StopService effect
// (auto-stop-on-exit). Each invocation routes its result through OnDone and its
// error through OnError. The whole block serializes, so it round-trips
// losslessly through JSON. A host's ServiceRunner runs the services and re-fires
// onDone/onError through Fire, keeping Fire pure.
Invoke []Invocation[S, E, C] `json:"invoke,omitempty"`
// Parent is a runtime-only back-pointer to the compound state owning this node,
// rebuilt after Quench/Provide; it never serializes. An ActorKindMachine entry
// in Invoke marks a child-machine actor whose lifecycle the host ActorSystem
// drives (the actor model); the per-instance actor mailboxes live on the host
// ActorSystem, not on this definition.
Parent *State[S, E, C] `json:"-"`
// Meta is the reserved extension namespace at state (node) granularity: studio
// layout, documentation strings, tags, and codegen hints live here. The kernel
// never inspects it; it round-trips verbatim.
Meta map[string]any `json:"meta,omitempty"`
// contains filtered or unexported fields
}

func (s State[S, E, C]) MarshalJSON() ([]byte, error)

MarshalJSON encodes a State, merging its preserved unknown keys back in with stable key ordering.

func (s *State[S, E, C]) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a State and captures any unknown keys into extra so they survive re-serialization.

Status classifies a snapshotted instance’s lifecycle. It mirrors the runtime status. StatusRunning is an instance still advancing; StatusDone is an instance whose active configuration is entirely final (every active leaf is a final state); StatusError is an instance the host settled as failed, carrying the error message on the snapshot.

type Status int

Instance lifecycle statuses recorded on a Snapshot.

const (
// StatusRunning is the default: the instance has not reached completion.
StatusRunning Status = iota
// StatusDone marks an instance whose whole active configuration is final.
StatusDone
// StatusError marks an instance the host explicitly failed; Snapshot.Error
// carries the message.
StatusError
)

func (s Status) String() string

String renders a Status for diagnostics and stable JSON.

StopActor is the effect the kernel emits when an instance exits a state that had a running child-machine actor (auto-stop-on-exit), or when the built-in stop action runs. The host’s ActorSystem stops the actor registered under ID (and, transitively, that actor’s own children); stopping an unknown ID is a no-op. A state’s invoked actors are auto-stopped when the state is exited before they complete.

type StopActor struct {
// ID identifies the actor to stop. It matches the ID of the SpawnActor that
// began it (auto-stop-on-exit), or an ID supplied to the stop built-in.
ID string `json:"id"`
}

func (StopActor) Kind() string

Kind reports the stop-actor effect discriminant.

StopService is the effect the kernel emits when an instance exits a state that had an in-flight invoked service. The host stops the service registered under ID; stopping an unknown ID is a no-op. A state’s invoked services are auto-stopped when the state is exited before they complete.

type StopService struct {
// ID identifies the service to stop. It matches the ID of the StartService
// that began it (auto-stop-on-exit).
ID string `json:"id"`
}

func (StopService) Kind() string

Kind reports the stop-service effect discriminant.

TemperOption configures Temper.

type TemperOption func(*temperConfig)

ToJSONOption configures ToJSON.

type ToJSONOption func(*toJSONConfig)

func WithoutSrcPos() ToJSONOption

WithoutSrcPos omits the diagnostic source-position fields (srcFile/srcLine) from the serialized IR. Source positions are captured from the builder via runtime.Caller, so they carry the absolute filesystem path of the worktree that authored the machine — which makes them non-portable across checkouts. They are diagnostic-only metadata (“defined at machine.go:84” tooltips) and have no effect on loading or behavior, so stripping them yields a stable, position-independent serialization. Use it for committed goldens and any interchange that must be byte-identical regardless of where it was generated.

Trace is the kernel’s canonical observability surface — pure data recorded on every Fire and surfaced live on an InspectTransition event. Consumers pattern- match and serialize it, so its field NAMES and JSON tags are stable: fields are added, never renamed or repurposed, and the per-step slices are always in emission order (the order frozen by the determinism contract; see the package overview). A field that does not apply to a given Fire is left zero/empty.

type Trace struct {
// Machine names the machine the traced instance was cast from.
Machine string `json:"machine,omitempty"`
// Event is the human-readable label of the event that drove this Fire — the
// event's string rendering — kept for diagnostics, visualization, and the
// pinned emission-ordering goldens.
Event string `json:"event,omitempty"`
// EventPayload is the structured, JSON-serializable form of the event value
// that drove this Fire, recorded so a future deterministic replay can
// reconstruct the exact event rather than re-parse its label. It is the
// load-bearing journal companion to Event: Event stays the human label,
// EventPayload carries the machine-readable value. It is omitted when the event
// has no JSON form (e.g. an internal "always"/raise microstep marker), so the
// field is additive and the trace stays deterministic across a JSON round-trip.
EventPayload json.RawMessage `json:"eventPayload,omitempty"`
// FromState is the primary active leaf the event was fired in, before the step.
FromState string `json:"fromState,omitempty"`
// SelectedTransition is the transition that fired, for in-process tooling. It is
// not serialized (json:"-") because behavior is bound, not embedded in the IR;
// the serializable record of what happened is the other fields.
SelectedTransition *Transition[any, any, any] `json:"-"`
// GuardsEvaluated names each guard the step evaluated, in evaluation order.
GuardsEvaluated []string `json:"guardsEvaluated,omitempty"`
// PoliciesEvaluated names each policy the step evaluated, in evaluation order.
PoliciesEvaluated []string `json:"policiesEvaluated,omitempty"`
// EffectsEmitted names each effect the step emitted, in emission order — the
// human-readable companion to FireResult.Effects (the effect data itself).
EffectsEmitted []string `json:"effectsEmitted,omitempty"`
// AssignsApplied names each assign reducer the step folded, in fold order.
AssignsApplied []string `json:"assignsApplied,omitempty"`
// Microsteps records the run-to-completion interleave — each raised internal
// event and eventless ("always") step, plus per-region markers — in the order it
// occurred within the macrostep.
Microsteps []string `json:"microsteps,omitempty"`
// MatchedAt names the state whose transition actually fired. For a flat
// machine it equals FromState; for an HSM it may be an ancestor reached by
// the child-first bubble.
MatchedAt string `json:"matchedAt,omitempty"`
// ExitedStates and EnteredStates record the transition's exit/entry cascade
// in execution order (exit innermost-first, entry outermost-first).
ExitedStates []string `json:"exitedStates,omitempty"`
EnteredStates []string `json:"enteredStates,omitempty"`
// Outcome classifies how the Fire settled — success or the specific failure
// class that stopped it. It is always set (OutcomeSuccess on a clean step).
Outcome Outcome `json:"outcome"`
}

Transition is a directed edge.

type Transition[S comparable, E comparable, C any] struct {
From S `json:"from"`
To S `json:"to"`
On E `json:"on"`
Guards []Ref `json:"guards,omitempty"`
Effects []Ref `json:"effects,omitempty"`
WaitMode WaitMode `json:"waitMode,omitempty"`
// Assigns lists the context-reducer refs run when this transition fires, folded
// after the transition's effects in declaration order. Each assign sees the
// context as folded by the assigns preceding it; the result becomes the
// instance's context. Assigns are structurally distinct from Effects (the
// assigner-vs-effector discriminator) so the cascade runs them in distinct
// phases. The slice serializes and round-trips losslessly through JSON.
Assigns []Ref `json:"assigns,omitempty"`
// GuardExpr is an optional composite guard: a serializable boolean
// expression tree over named-ref leaves, the stateIn built-in, and the
// and/or/not combinators. When set it is evaluated in
// addition to every Ref in Guards — the transition is enabled only when both
// the plain guards and the expression pass — so the common single-guard case
// stays the plain Guards slice and composition is purely additive. The tree
// serializes and round-trips losslessly through JSON.
GuardExpr *GuardNode[S] `json:"guardExpr,omitempty"`
Internal bool `json:"internal,omitempty"`
EventLess bool `json:"eventLess,omitempty"`
After *time.Duration `json:"after,omitempty"`
// Wildcard marks a catch-all transition: it matches any event that no
// specific-event transition of the same state handles. Wildcard transitions
// are the lowest-priority candidates in a state, tried only after every
// On-keyed match fails, and resolution still bubbles to ancestors when no
// wildcard fires. On is ignored when Wildcard is set. This is the
// `on: { '*': ... }`.
Wildcard bool `json:"wildcard,omitempty"`
// Forbidden marks an event as explicitly blocked at this state: the event is
// consumed and ignored, and — unlike "no handler declared" — it does NOT
// bubble to ancestor states. To has no meaning for a forbidden transition.
// This is a forbidden transition: the event is consumed and ignored.
Forbidden bool `json:"forbidden,omitempty"`
// Reenter makes a transition external. By default (v5 semantics) a transition
// whose target is the source itself or an ancestor of the source is internal:
// its effects run but the source is not exited and re-entered. Setting Reenter
// forces the external form, running the full exit/entry cascade of the target.
// For an unrelated target (an ordinary state change) the cascade always runs;
// Reenter only changes the self/ancestor case. This is the
// `reenter: true`.
Reenter bool `json:"reenter,omitempty"`
// Raise lists internal events this transition enqueues. They are appended to
// the macrostep's internal queue after the transition's own effects run, and
// drained by Fire's run-to-completion loop within the SAME macrostep — before
// Fire returns and before any externally-sent event. This is the
// `raise(...)`. The queue is local to the macrostep, so Fire stays pure.
Raise []E `json:"raise,omitempty"`
SrcFile string `json:"srcFile,omitempty"`
SrcLine int `json:"srcLine,omitempty"`
// Meta is the reserved extension namespace at transition (edge) granularity:
// edge layout, documentation, and codegen hints live here. The kernel never
// inspects it; it round-trips verbatim.
Meta map[string]any `json:"meta,omitempty"`
// contains filtered or unexported fields
}

func (t Transition[S, E, C]) MarshalJSON() ([]byte, error)

MarshalJSON encodes a Transition, merging its preserved unknown keys back in with stable key ordering.

func (t *Transition[S, E, C]) UnmarshalJSON(data []byte) error

UnmarshalJSON decodes a Transition and captures any unknown keys into extra so they survive re-serialization.

UnknownEffect is the preserved form of an effect whose kind the local registry does not recognize. It carries the original kind and payload verbatim so an unknown effect survives a load -> save cycle byte-for-byte (forward-compat, per the closed-enum extension policy). It implements KindedEffect, so it can be re-marshaled, but it is never dispatchable — EffectRegistry.Dispatchable rejects it with a typed *ErrUnknownEffectKind. The kernel never produces an UnknownEffect; only deserialization of a foreign envelope yields one.

type UnknownEffect struct {
// EffectKind is the unrecognized discriminant, preserved verbatim.
EffectKind string
// Payload is the original effect body, preserved verbatim for re-emission.
Payload json.RawMessage
// Meta is the preserved extension namespace from the source envelope.
Meta map[string]any
}

func (u UnknownEffect) Kind() string

Kind reports the preserved, unrecognized discriminant.

VizOption configures the ToMermaid and ToDOT renderers.

type VizOption func(*vizConfig)

func LeftToRight() VizOption

LeftToRight lays the diagram out left-to-right (Mermaid direction LR, DOT rankdir=LR).

func TopToBottom() VizOption

TopToBottom lays the diagram out top-to-bottom (Mermaid default, DOT rankdir=TB).

func WithoutGuards() VizOption

WithoutGuards omits the bracketed guard annotations from transition labels.

func WithoutOwners() VizOption

WithoutOwners omits owner color-coding (Mermaid classDef / DOT fillcolor).

WaitMode tags a transition’s synchronization expectation. The kernel only stores the tag; the consumer acts on it.

type WaitMode int

Wait modes. SyncReply awaits a reply, FireAndForget emits and moves on, and ValidatePoll signals the consumer to poll the entity (re-running Assay) until it validates.

const (
SyncReply WaitMode = iota
FireAndForget
ValidatePoll
)

WaitOption configures WaitFor.

type WaitOption[S comparable, E comparable, C any] func(*waitConfig[S, E, C])

func WithWaitScheduler[S comparable, E comparable, C any](sch *Scheduler[S, E, C]) WaitOption[S, E, C]

WithWaitScheduler drives the wait by advancing the FakeClock the Scheduler reads and ticking it each iteration, so `after`-driven transitions fire and the instance progresses toward the predicate deterministically. It is the common driver for delayed-transition machines: cast with WithClock(fakeClock), build a Scheduler, then WaitFor(ctx, inst, pred, WithWaitScheduler(sch)). The Scheduler’s clock must be the instance’s clock (it is, by construction of NewScheduler).

func WithWaitStep[S comparable, E comparable, C any](d time.Duration) WaitOption[S, E, C]

WithWaitStep sets the per-iteration advance increment WaitFor applies to the driver’s clock between predicate checks. A smaller step lands closer to the exact instant a delayed transition becomes due; a larger step polls less often.

func WithWaitStepFunc[S comparable, E comparable, C any](advance func(ctx context.Context, clock Clock, step time.Duration)) WaitOption[S, E, C]

WithWaitStepFunc supplies a custom driver advance: a function WaitFor calls each iteration to move time forward and fire any due work (e.g. a ServiceRunner a test settles, or a bespoke host loop). The function should advance the supplied clock by step when it is a FakeClock so the wait budget is consumed deterministically.

func WithWaitTimeout[S comparable, E comparable, C any](d time.Duration) WaitOption[S, E, C]

WithWaitTimeout sets the wait budget, measured on the instance’s clock. When the budget elapses before the predicate holds, WaitFor returns a *WaitTimeoutError.

WaitPredicate is the condition WaitFor waits to become true. It is evaluated against the instance’s live Snapshot after each advance (and once before any advance). It must be a pure read of the snapshot — WaitFor never mutates the instance on the predicate’s behalf.

type WaitPredicate[S comparable, E comparable, C any] func(snap Snapshot[S, E, C]) bool

func WaitDone[S comparable, E comparable, C any]() WaitPredicate[S, E, C]

WaitDone returns a WaitPredicate that holds when the instance has reached completion (its whole active configuration is final), mirroring `waitFor(actor, (s) => s.status === ‘done’)`.

func WaitInState[S comparable, E comparable, C any](target S) WaitPredicate[S, E, C]

WaitInState returns a WaitPredicate that holds when the instance’s primary active leaf equals target — the common “wait until it reaches state X” case (waiting until the instance’s snapshot satisfies a predicate).

WaitTimeoutError is returned by WaitFor when its wait budget elapses (measured on the instance’s clock) before the predicate ever held — the typed timeout returned when a WaitFor budget elapses. Machine names the instance’s machine, Timeout the budget that elapsed, and Last the primary active leaf the instance was in when the wait gave up, for diagnostics.

type WaitTimeoutError struct {
Machine string
Timeout time.Duration
Last string
}

func (e *WaitTimeoutError) Error() string

Generated by gomarkdoc