Documentation
¶
Overview ¶
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
What this kernel is ¶
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.
Pure-function step semantics ¶
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 definition IR is the spec ¶
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.
Foundry vocabulary ¶
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.
- Verify — plain verb (favoring discoverability over metaphor): check that an externally-constructed entity is legally in a given state.
Operations that favor discoverability over metaphor stay plain: Verify, PlanPath, Requirements, Trace, and the To*/LoadFromJSON serializers.
Context: assigns and value semantics ¶
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.Reducer). 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.
Determinism and ordering ¶
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.
Design ¶
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. Observability is opt-in, in keeping with the suite's no-op-default convention. By default an instance runs in LITE trace mode: each Fire still returns a Trace, but only the always-present fields (Machine, Event, FromState, MatchedAt, Outcome) are populated — enough for structured logging and a settled result, with no per-step diagnostic allocation. FULL trace mode populates the rich per-step fields (GuardsEvaluated, EffectsEmitted, ExitedStates, EnteredStates, AssignsApplied, Microsteps, EventPayload, SelectedTransition) and is enabled by attaching any observer at Cast: WithFullTrace, WithInspector, or one of the history options below. WithLogger(*slog.Logger) (no-op by default) reads only the always-present fields, so a logger-only instance stays lite; the kernel never logs unless asked and never imports a third-party logger.
Trace history is retained only when requested, and is bounded by default: WithHistory(n) keeps the last n settled traces in a ring buffer (n <= 0 disables retention), so a long-lived instance never grows its history without limit. WithUnboundedHistory opts into unbounded retention when a consumer genuinely wants every trace. With no history option, History returns nil. 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).
Status ¶
The kernel implements the Forge/Temper/Quench build path, Cast/Fire pure step semantics with guards, actions, typed errors and an opt-in structured Trace, Verify/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.
Guard expressions ¶
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).
Context schema ¶
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.
Versioning, snapshots, and journal seams ¶
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 (ConnectionLifecycle) ¶
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
Index ¶
- Constants
- func ActorID[S comparable](machine string, from S, idx int) string
- func BindingTransportOf(d Descriptor) string
- func InvokeID[S comparable](machine string, from S, idx int) string
- func MarshalSnapshot[S comparable, E comparable, C any](snap Snapshot[S, E, C], opts ...SnapshotCodecOption[C]) ([]byte, error)
- func ScheduleID[S comparable](machine string, from S, idx int) string
- type ActionBinding
- type ActionCtx
- type ActionFailedError
- type ActionFn
- type ActionRequest
- type ActionResult
- type ActorBehavior
- type ActorEscalation
- type ActorInstance
- type ActorKind
- type ActorPhase
- type ActorRef
- type ActorSystem
- func (s *ActorSystem[S, E, C]) Absorb(ctx context.Context, effects []Effect)
- func (s *ActorSystem[S, E, C]) AbsorbFor(ctx context.Context, event any, effects []Effect)
- func (s *ActorSystem[S, E, C]) Deliver(ctx context.Context, ref ActorRef, event any) bool
- func (s *ActorSystem[S, E, C]) DeliverByID(ctx context.Context, id string, event any) bool
- func (s *ActorSystem[S, E, C]) IDs() []string
- func (s *ActorSystem[S, E, C]) IsRunning(id string) bool
- func (s *ActorSystem[S, E, C]) LastError() error
- func (s *ActorSystem[S, E, C]) LastEscalation() *ActorEscalation
- func (s *ActorSystem[S, E, C]) LastOutput() (any, bool)
- func (s *ActorSystem[S, E, C]) Ref(id string) (ActorRef, bool)
- func (s *ActorSystem[S, E, C]) RefBySystemID(systemID string) (ActorRef, bool)
- func (s *ActorSystem[S, E, C]) Register(src string, behavior ActorBehavior) *ActorSystem[S, E, C]
- func (s *ActorSystem[S, E, C]) RestoreActors(ctx context.Context, actors map[string]json.RawMessage) error
- func (s *ActorSystem[S, E, C]) Running() int
- func (s *ActorSystem[S, E, C]) SettleError(ctx context.Context, id string, err error) (FireResult[S], bool)
- func (s *ActorSystem[S, E, C]) SnapshotActors() (map[string]json.RawMessage, error)
- func (s *ActorSystem[S, E, C]) Stop(ref ActorRef)
- func (s *ActorSystem[S, E, C]) Tick(ctx context.Context, id string) []FireResult[S]
- func (s *ActorSystem[S, E, C]) WithActorInspector(insp Inspector) *ActorSystem[S, E, C]
- func (s *ActorSystem[S, E, C]) WithEscalationHandler(handler EscalationHandler) *ActorSystem[S, E, C]
- type AssignBinding
- type AssignCtx
- type AssignFn
- type AssignPanicError
- type AssignRequest
- type AssignResult
- type BatchResult
- type BindingSpec
- type Builder
- func (b *Builder[S, E, C]) Action(name string, fn ActionFn[C], opts ...DescribeOption) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Actor(name string, opts ...DescribeOption) *Builder[S, E, C]
- func (b *Builder[S, E, C]) After(delay time.Duration) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Always() *Builder[S, E, C]
- func (b *Builder[S, E, C]) Assign(assignName string, params ...map[string]any) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Cancel(id string) *Builder[S, E, C]
- func (b *Builder[S, E, C]) CurrentStateFn(fn func(C) S) *Builder[S, E, C]
- func (b *Builder[S, E, C]) DefaultTo(target S) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Do(actionName string, params ...map[string]any) *Builder[S, E, C]
- func (b *Builder[S, E, C]) EndRegion() *Builder[S, E, C]
- func (b *Builder[S, E, C]) EndSuperState() *Builder[S, E, C]
- func (b *Builder[S, E, C]) Final() *Builder[S, E, C]
- func (b *Builder[S, E, C]) Forbid(event E) *Builder[S, E, C]
- func (b *Builder[S, E, C]) ForbidAny() *Builder[S, E, C]
- func (b *Builder[S, E, C]) ForwardTo(targetID string, opts ...SendOption) *Builder[S, E, C]
- func (b *Builder[S, E, C]) GoTo(to S) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Guard(name string, fn GuardFn[C], opts ...DescribeOption) *Builder[S, E, C]
- func (b *Builder[S, E, C]) History(name S, kind HistoryType) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Initial(name S) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Invoke(src string, opts ...InvokeOption) *Builder[S, E, C]
- func (b *Builder[S, E, C]) InvokeActor(src string, opts ...InvokeOption) *Builder[S, E, C]
- func (b *Builder[S, E, C]) On(event E) *Builder[S, E, C]
- func (b *Builder[S, E, C]) OnAny() *Builder[S, E, C]
- func (b *Builder[S, E, C]) OnDone(actionName string, params ...map[string]any) *Builder[S, E, C]
- func (b *Builder[S, E, C]) OnEntry(actionName string, params ...map[string]any) *Builder[S, E, C]
- func (b *Builder[S, E, C]) OnEntryAssign(assignName string, params ...map[string]any) *Builder[S, E, C]
- func (b *Builder[S, E, C]) OnExit(actionName string, params ...map[string]any) *Builder[S, E, C]
- func (b *Builder[S, E, C]) OnExitAssign(assignName string, params ...map[string]any) *Builder[S, E, C]
- func (b *Builder[S, E, C]) OwnedBy(owner string) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Palette() []Descriptor
- func (b *Builder[S, E, C]) Quench(opts ...QuenchOption) *Machine[S, E, C]
- func (b *Builder[S, E, C]) Raise(events ...E) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Reducer(name string, fn AssignFn[C], opts ...DescribeOption) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Reenter() *Builder[S, E, C]
- func (b *Builder[S, E, C]) Region(name string) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Requires(req Requirement[C]) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Respond(event E) *Builder[S, E, C]
- func (b *Builder[S, E, C]) SendParent(event E) *Builder[S, E, C]
- func (b *Builder[S, E, C]) SendTo(targetID string, event E, opts ...SendOption) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Service(name string, fn ServiceFn[C], opts ...DescribeOption) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Spawn(src, id string, opts ...SpawnOption) *Builder[S, E, C]
- func (b *Builder[S, E, C]) State(name S) *Builder[S, E, C]
- func (b *Builder[S, E, C]) StopActor(id string) *Builder[S, E, C]
- func (b *Builder[S, E, C]) SubState(name S) *Builder[S, E, C]
- func (b *Builder[S, E, C]) SuperState(name S) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Temper(opts ...TemperOption) []Diagnostic
- func (b *Builder[S, E, C]) Transition(from S) *Builder[S, E, C]
- func (b *Builder[S, E, C]) Use(mw ...Middleware[S, E, C]) *Builder[S, E, C]
- func (b *Builder[S, E, C]) WaitMode(m WaitMode) *Builder[S, E, C]
- func (b *Builder[S, E, C]) When(guardName string, params ...map[string]any) *Builder[S, E, C]
- func (b *Builder[S, E, C]) WhenExpr(expr GuardNode[S]) *Builder[S, E, C]
- func (b *Builder[S, E, C]) WithContextSchema(schema ContextSchema) *Builder[S, E, C]
- type CancelScheduled
- type CastOption
- func WithClock[S comparable](c Clock) CastOption[S]
- func WithFullTrace[S comparable]() CastOption[S]
- func WithHistory[S comparable](limit int) CastOption[S]
- func WithInitialState[S comparable](s S) CastOption[S]
- func WithInspector[S comparable](insp Inspector) CastOption[S]
- func WithLogger[S comparable](l *slog.Logger) CastOption[S]
- func WithUnboundedHistory[S comparable]() CastOption[S]
- type Clock
- type ContextCodec
- type ContextSchema
- type ContextView
- type DescribeBuilder
- func (d *DescribeBuilder) EnumParam(name string, allowed ...string) *DescribeBuilder
- func (d *DescribeBuilder) OptionalParam(name string, typ ParamType) *DescribeBuilder
- func (d *DescribeBuilder) Param(name string, typ ParamType) *DescribeBuilder
- func (d *DescribeBuilder) ParamSpec(p ParamSpec) *DescribeBuilder
- func (d *DescribeBuilder) Reads(fields ...string) *DescribeBuilder
- func (d *DescribeBuilder) Writes(fields ...string) *DescribeBuilder
- type DescribeOption
- type Descriptor
- type DescriptorKind
- type Diagnostic
- type Effect
- type EffectEnvelope
- type EffectFactory
- type EffectRegistry
- type ErrActorPanic
- type EscalationHandler
- type FakeClock
- type FieldRef
- func (f FieldRef[S]) Eq(operand Operand[S]) GuardNode[S]
- func (f FieldRef[S]) Ge(operand Operand[S]) GuardNode[S]
- func (f FieldRef[S]) Gt(operand Operand[S]) GuardNode[S]
- func (f FieldRef[S]) In(values ...Operand[S]) GuardNode[S]
- func (f FieldRef[S]) Le(operand Operand[S]) GuardNode[S]
- func (f FieldRef[S]) Lt(operand Operand[S]) GuardNode[S]
- func (f FieldRef[S]) Ne(operand Operand[S]) GuardNode[S]
- type FireFunc
- type FireOption
- type FireResult
- type ForgeOption
- type ForwardEvent
- type GuardBinding
- type GuardCtx
- type GuardFailedError
- type GuardFn
- type GuardKind
- type GuardNode
- type GuardOp
- type GuardPanicError
- type GuardRequest
- type GuardResult
- type HistoryType
- type IOSpec
- type IR
- type InFlightService
- type InspectKind
- type InspectionEvent
- type Inspector
- type InspectorFunc
- type Instance
- func (i *Instance[S, E, C]) Clock() Clock
- func (i *Instance[S, E, C]) Configuration() []S
- func (i *Instance[S, E, C]) Current() S
- func (i *Instance[S, E, C]) Entity() C
- func (i *Instance[S, E, C]) Fire(ctx context.Context, event E, opts ...FireOption) FireResult[S]
- func (i *Instance[S, E, C]) FireSeq(ctx context.Context, events []E, opts ...FireOption) BatchResult[S]
- func (i *Instance[S, E, C]) History() []Trace
- func (i *Instance[S, E, C]) InFinal() bool
- func (i *Instance[S, E, C]) ResumeEffects() []Effect
- func (i *Instance[S, E, C]) Snapshot() Snapshot[S, E, C]
- func (i *Instance[S, E, C]) StartEffects() []Effect
- type InvalidTransitionError
- type Invocation
- type InvokeOption
- func WithInput(input map[string]any) InvokeOption
- func WithInvokeID(id string) InvokeOption
- func WithInvokeOnDone[E comparable](onDone E) InvokeOption
- func WithInvokeOnError[E comparable](onError E) InvokeOption
- func WithServiceParams(params map[string]any) InvokeOption
- func WithSystemID(id string) InvokeOption
- type JournalEntry
- type JournalKind
- type KindedEffect
- type Literal
- type LoadOption
- type Machine
- func (m *Machine[S, E, C]) Cast(entity C, opts ...CastOption[S]) *Instance[S, E, C]
- func (m *Machine[S, E, C]) Name() string
- func (m *Machine[S, E, C]) Palette() []Descriptor
- func (m *Machine[S, E, C]) PlanPath(from, to S, entity C, opts ...PlanOption) ([]E, error)
- func (m *Machine[S, E, C]) Requirements(s S) []Requirement[C]
- func (m *Machine[S, E, C]) Restore(snap Snapshot[S, E, C], opts ...RestoreOption[S]) (*Instance[S, E, C], error)
- func (m *Machine[S, E, C]) Services() map[string]ServiceFn[C]
- func (m *Machine[S, E, C]) ToDOT(opts ...VizOption) string
- func (m *Machine[S, E, C]) ToJSON(opts ...ToJSONOption) ([]byte, error)
- func (m *Machine[S, E, C]) ToMermaid(opts ...VizOption) string
- func (m *Machine[S, E, C]) Verify(s S, entity C, opts ...VerifyOption) error
- type MessagePhase
- type MicrostepOverflowError
- type Middleware
- type MultiRegionError
- type NoInitialStateError
- type NoPathError
- type Operand
- func Bool[S comparable](v bool) Operand[S]
- func Dur[S comparable](v time.Duration) Operand[S]
- func FieldOp[S comparable](f FieldRef[S]) Operand[S]
- func Float[S comparable](v float64) Operand[S]
- func Int[S comparable](v int64) Operand[S]
- func Param[S comparable](v string) Operand[S]
- func Str[S comparable](v string) Operand[S]
- type Outcome
- type P
- type ParamSpec
- type ParamType
- type PendingRefs
- type PlanOption
- type PolicyDeniedError
- type ProvideOption
- type QuenchOption
- type Ref
- type Region
- type RegisterEffectOption
- type Registry
- func (r *Registry[C]) Action(name string, fn ActionFn[C], opts ...DescribeOption) *Registry[C]
- func (r *Registry[C]) Actor(name string, opts ...DescribeOption) *Registry[C]
- func (r *Registry[C]) BindGuard(name string, b GuardBinding[C], opts ...DescribeOption) *Registry[C]
- func (r *Registry[C]) Guard(name string, fn GuardFn[C], opts ...DescribeOption) *Registry[C]
- func (r *Registry[C]) Palette() []Descriptor
- func (r *Registry[C]) Reducer(name string, fn AssignFn[C], opts ...DescribeOption) *Registry[C]
- func (r *Registry[C]) Service(name string, fn ServiceFn[C], opts ...DescribeOption) *Registry[C]
- type Requirement
- type RequirementFailure
- type RespondToSender
- type RestoreOption
- type ScheduleAfter
- type Scheduler
- type SchemaField
- type SchemaKind
- type SendOption
- type SendParent
- type SendTo
- type ServiceBinding
- type ServiceCtx
- type ServiceFn
- type ServiceRequest
- type ServiceRunner
- func (r *ServiceRunner[S, E, C]) Absorb(ctx context.Context, effects []Effect)
- func (r *ServiceRunner[S, E, C]) HasPending(id string) bool
- func (r *ServiceRunner[S, E, C]) LastError() error
- func (r *ServiceRunner[S, E, C]) LastResult() (any, bool)
- func (r *ServiceRunner[S, E, C]) Pending() int
- func (r *ServiceRunner[S, E, C]) PendingIDs() []string
- func (r *ServiceRunner[S, E, C]) SettleDone(ctx context.Context, id string, result any) (FireResult[S], bool)
- func (r *ServiceRunner[S, E, C]) SettleError(ctx context.Context, id string, err error) (FireResult[S], bool)
- func (r *ServiceRunner[S, E, C]) Tick(ctx context.Context, id string) []FireResult[S]
- type Snapshot
- type SnapshotCodecOption
- type SnapshotError
- type SnapshotVersionError
- type Snapshotter
- type SpawnActor
- type SpawnOption
- type StartService
- type State
- type Status
- type StopActor
- type StopService
- type TemperOption
- type ToJSONOption
- type Trace
- type Transition
- type UnboundActorError
- type UnboundRefError
- type UndeclaredStateError
- type UnknownBuiltinError
- type UnknownEffect
- type UnknownEffectKindError
- type UnsupportedSchemaError
- type VerifyError
- type VerifyOption
- type VizOption
- type WaitMode
- type WaitOption
- func WithWaitScheduler[S comparable, E comparable, C any](sch *Scheduler[S, E, C]) WaitOption[S, E, C]
- func WithWaitStep[S comparable, E comparable, C any](d time.Duration) WaitOption[S, E, C]
- func WithWaitStepFunc[S comparable, E comparable, C any](advance func(ctx context.Context, clock Clock, step time.Duration)) WaitOption[S, E, C]
- func WithWaitTimeout[S comparable, E comparable, C any](d time.Duration) WaitOption[S, E, C]
- type WaitPredicate
- type WaitTimeoutError
Examples ¶
Constants ¶
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" )
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 CurrentSchemaVersion = "1.0"
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 *UnsupportedSchemaError. Every document ToJSON emits is stamped with this version, so an IR on the wire is self-describing.
const CurrentSnapshotVersion = 1 * snapshotMajorScale
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 TransportInProcess = "in-process"
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.
Variables ¶
This section is empty.
Functions ¶
func ActorID ¶ added in v0.2.0
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 ¶ added in v0.3.0
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 ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 ¶ added in v0.2.0
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.
Types ¶
type ActionBinding ¶ added in v0.3.0
type ActionBinding[C any] interface { EvalAction(ctx context.Context, req ActionRequest[C]) (ActionResult, error) }
ActionBinding turns an action request into emitted effects. The in-process binding wraps an ActionFn.
type ActionFailedError ¶ added in v0.3.0
ActionFailedError wraps a bound action that returned an error during emission.
func (*ActionFailedError) Error ¶ added in v0.3.0
func (e *ActionFailedError) Error() string
func (*ActionFailedError) Unwrap ¶ added in v0.3.0
func (e *ActionFailedError) Unwrap() error
type ActionRequest ¶ added in v0.3.0
type ActionRequest[C any] struct { Name string Params map[string]any Context ContextView }
ActionRequest is the serializable invocation envelope for an action: the named ref, its params, and the read-only context projection.
type ActionResult ¶ added in v0.3.0
type ActionResult struct {
Effects []Effect
}
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 ActorBehavior ¶ added in v0.2.0
type ActorBehavior func(input map[string]any) (ActorInstance, error)
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 ActorEscalation ¶ added in v0.3.0
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
}
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.
func (*ActorEscalation) Error ¶ added in v0.3.0
func (e *ActorEscalation) Error() string
Error renders the escalation, naming the failed actor and the wrapped cause.
func (*ActorEscalation) Unwrap ¶ added in v0.3.0
func (e *ActorEscalation) Unwrap() error
Unwrap returns the underlying child failure so errors.Is / errors.As reach the cause an escalation wraps.
type ActorInstance ¶ added in v0.2.0
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
}
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.
func NewActor ¶ added in v0.2.0
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.
type ActorKind ¶ added in v0.2.0
type ActorKind int
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 ActorPhase ¶ added in v0.2.0
type ActorPhase string
ActorPhase distinguishes the lifecycle point of an InspectActor event.
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" )
type ActorRef ¶ added in v0.2.0
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
}
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 ActorSystem ¶ added in v0.2.0
type ActorSystem[S comparable, E comparable, C any] struct { // contains filtered or unexported fields }
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.
Example ¶
ExampleActorSystem exercises the actor lifecycle end to end: a parent state dynamically Spawns a child-machine actor, the host delivers events to it by id and observes its progress, and the host Stops it explicitly. The ActorSystem is the host-side driver that turns a parent's SpawnActor/StopActor effects into running child actors and routes their completion back through the parent.
package main
import (
"context"
"fmt"
"github.com/stablekernel/crucible/state"
)
// pingPong is the child-actor entity: it counts the pings it receives before it
// is told to finish.
type pingPong struct {
pings int
}
func main() {
// The child machine counts pings, then completes on "finish".
child := state.Forge[string, string, *pingPong]("counter").
Action("count", func(c state.ActionCtx[*pingPong]) (state.Effect, error) {
c.Entity.pings++
return nil, nil
}).
State("counting").
State("done").Final().
Initial("counting").
Transition("counting").On("ping").GoTo("counting").Do("count").
Transition("counting").On("finish").GoTo("done").
Quench()
// The parent spawns the child on "start" and reacts to its completion.
parent := state.Forge[string, string, *pingPong]("supervisor").
State("idle").
State("active").
Initial("idle").
Transition("idle").On("start").GoTo("active").
Spawn("counter", "worker", state.WithSpawnOnDone("workerDone")).
Transition("active").On("workerDone").GoTo("idle").
Quench()
ctx := context.Background()
root := parent.Cast(&pingPong{}, state.WithInitialState("idle"))
// A spawn behavior Casts a fresh child per spawn and exposes its ping count.
behavior := func(map[string]any) (state.ActorInstance, error) {
inst := child.Cast(&pingPong{}, state.WithInitialState("counting"))
return state.NewActor(inst, func(i *state.Instance[string, string, *pingPong]) any {
return i.Entity().pings
}), nil
}
sys := state.NewActorSystem(root).Register("counter", behavior)
// Firing "start" emits the SpawnActor effect; Absorb spawns the child actor.
res := root.Fire(ctx, "start")
sys.Absorb(ctx, res.Effects)
fmt.Println("running after spawn:", sys.Running())
// Deliver two pings to the child by id; each steps it through its counter.
sys.DeliverByID(ctx, "worker", "ping")
sys.DeliverByID(ctx, "worker", "ping")
// Stop the actor explicitly through the ref the host tracks.
ref, _ := sys.Ref("worker")
sys.Stop(ref)
fmt.Println("running after stop:", sys.Running())
}
Output: running after spawn: 1 running after stop: 0
func NewActorSystem ¶ added in v0.2.0
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 (*ActorSystem[S, E, C]) Absorb ¶ added in v0.2.0
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 (*ActorSystem[S, E, C]) AbsorbFor ¶ added in v0.2.0
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 (*ActorSystem[S, E, C]) Deliver ¶ added in v0.2.0
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 (*ActorSystem[S, E, C]) DeliverByID ¶ added in v0.2.0
DeliverByID is Deliver keyed by raw actor id, for a host that tracks ids rather than refs.
func (*ActorSystem[S, E, C]) IDs ¶ added in v0.2.0
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 (*ActorSystem[S, E, C]) IsRunning ¶ added in v0.2.0
func (s *ActorSystem[S, E, C]) IsRunning(id string) bool
IsRunning reports whether an actor with the given id is live.
func (*ActorSystem[S, E, C]) LastError ¶ added in v0.2.0
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 ¶ added in v0.3.0
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 (*ActorSystem[S, E, C]) LastOutput ¶ added in v0.2.0
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 (*ActorSystem[S, E, C]) Ref ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 (*ActorSystem[S, E, C]) Register ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 (*ActorSystem[S, E, C]) Running ¶ added in v0.2.0
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 (*ActorSystem[S, E, C]) SettleError ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 (*ActorSystem[S, E, C]) Stop ¶ added in v0.2.0
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]) Tick ¶ added in v0.3.0
func (s *ActorSystem[S, E, C]) Tick(ctx context.Context, id string) []FireResult[S]
Tick 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). Tick is the ActorSystem's advance verb — the host-driver counterpart of Scheduler.Tick and ServiceRunner.Tick — 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 (*ActorSystem[S, E, C]) WithActorInspector ¶ added in v0.2.0
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 ¶ added in v0.3.0
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.
type AssignBinding ¶ added in v0.3.0
type AssignBinding[C any] interface { EvalAssign(ctx context.Context, req AssignRequest[C]) (AssignResult[C], error) }
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 AssignCtx ¶ added in v0.3.0
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 AssignFn ¶ added in v0.3.0
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 AssignPanicError ¶ added in v0.3.0
AssignPanicError 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.
func (*AssignPanicError) Error ¶ added in v0.3.0
func (e *AssignPanicError) Error() string
type AssignRequest ¶ added in v0.3.0
type AssignRequest[C any] struct { Name string Params map[string]any Event any Context ContextView }
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 AssignResult ¶ added in v0.3.0
type AssignResult[C any] struct { Context C }
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 BatchResult ¶
type BatchResult[S comparable] struct { Steps []FireResult[S] Trace Trace Err error }
BatchResult is the result of a batch fire (FireSeq / FireEach).
type BindingSpec ¶ added in v0.3.0
type BindingSpec struct {
Transport string `json:"transport,omitempty"`
Meta map[string]any `json:"meta,omitempty"`
// contains filtered or unexported fields
}
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.
func (BindingSpec) MarshalJSON ¶ added in v0.3.0
func (s BindingSpec) MarshalJSON() ([]byte, error)
MarshalJSON encodes a BindingSpec, merging its preserved unknown keys back in with stable key ordering.
func (*BindingSpec) UnmarshalJSON ¶ added in v0.3.0
func (s *BindingSpec) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes a BindingSpec and captures any unknown keys into extra so they survive re-serialization.
type Builder ¶
type Builder[S comparable, E comparable, C any] struct { // contains filtered or unexported fields }
Builder is the Forge DSL front-end. It builds the IR and registers implementations by name.
func Forge ¶
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}]
func (*Builder[S, E, C]) Action ¶
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 (*Builder[S, E, C]) Actor ¶ added in v0.3.0
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 (*Builder[S, E, C]) After ¶ added in v0.2.0
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 (*Builder[S, E, C]) Always ¶ added in v0.2.0
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 (*Builder[S, E, C]) Assign ¶ added in v0.3.0
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.Reducer); this WIRES a registered reducer by name onto the transition, mirroring how When wires a Guard and Do wires an Action.
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)
}
Output: 600
func (*Builder[S, E, C]) Cancel ¶ added in v0.2.0
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 (*Builder[S, E, C]) CurrentStateFn ¶
CurrentStateFn declares how to derive an instance's current state.
func (*Builder[S, E, C]) DefaultTo ¶ added in v0.2.0
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 (*Builder[S, E, C]) Do ¶
Do attaches a named action ref with params to the most-recent transition.
func (*Builder[S, E, C]) EndSuperState ¶
EndSuperState closes the most-recent SuperState block.
func (*Builder[S, E, C]) Forbid ¶ added in v0.2.0
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 (*Builder[S, E, C]) ForbidAny ¶ added in v0.2.0
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 (*Builder[S, E, C]) ForwardTo ¶ added in v0.2.0
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 (*Builder[S, E, C]) Guard ¶
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 (*Builder[S, E, C]) History ¶ added in v0.2.0
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 (*Builder[S, E, C]) Initial ¶
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 (*Builder[S, E, C]) Invoke ¶ added in v0.2.0
func (b *Builder[S, E, C]) Invoke(src string, 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). The completion outcomes are configured with the variadic InvokeOptions, mirroring Spawn: WithInvokeOnDone / WithInvokeOnError name the events the host re-fires through Fire when the service completes or fails, routed by ordinary transitions from this state; WithInput sets the service input and WithInvokeID pins an explicit id (omitting it derives a stable id via InvokeID). Keeping the outcomes as options rather than positional parameters means new routing knobs (fire-and-forget, onCancel) can arrive later as further options without a signature change. 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 (*Builder[S, E, C]) InvokeActor ¶ added in v0.2.0
func (b *Builder[S, E, C]) InvokeActor(src string, 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. The completion outcomes are configured with the variadic InvokeOptions, mirroring Spawn: WithInvokeOnDone / WithInvokeOnError 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 (*Builder[S, E, C]) On ¶
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 (*Builder[S, E, C]) OnAny ¶ added in v0.2.0
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 (*Builder[S, E, C]) OnDone ¶
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 (*Builder[S, E, C]) OnEntry ¶
OnEntry attaches a named entry-action ref to the most-recent state.
func (*Builder[S, E, C]) OnEntryAssign ¶ added in v0.3.0
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 (*Builder[S, E, C]) OnExitAssign ¶ added in v0.3.0
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 (*Builder[S, E, C]) Palette ¶ added in v0.3.0
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 (*Builder[S, E, C]) 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.
Example ¶
ExampleBuilder_Quench walks the full Forge -> Temper -> Quench finalize seam: Forge opens a builder, Temper lints it without freezing (returning diagnostics a tool can surface), and Quench binds refs and freezes the builder into an immutable Machine ready to Cast. A fully specified machine tempers with no findings and quenches without panicking.
package main
import (
"context"
"fmt"
"github.com/stablekernel/crucible/state"
)
// gate is the entity the finalize-seam example guards and assigns against.
type gate struct {
approved bool
stamped bool
}
func main() {
b := state.Forge[string, string, gate]("turnstile").
Guard("approved", func(c state.GuardCtx[gate]) bool { return c.Entity.approved }).
// CurrentStateFn lets the kernel derive the current state from the entity,
// which keeps Temper clean (no "missing CurrentStateFn" warning).
CurrentStateFn(func(g gate) string {
if g.stamped {
return "open"
}
return "locked"
}).
State("locked").
Transition("locked").On("push").GoTo("open").When("approved").
State("open").
Initial("locked")
// Temper lints without freezing: a tool can show findings before committing.
fmt.Println("temper findings:", len(b.Temper()))
// Quench binds and freezes into an immutable Machine ready to Cast.
m := b.Quench()
denied := m.Cast(gate{approved: false})
denied.Fire(context.Background(), "push")
fmt.Println("denied:", denied.Current())
allowed := m.Cast(gate{approved: true})
allowed.Fire(context.Background(), "push")
fmt.Println("allowed:", allowed.Current())
}
Output: temper findings: 0 denied: locked allowed: open
func (*Builder[S, E, C]) Raise ¶ added in v0.2.0
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 (*Builder[S, E, C]) Reducer ¶ added in v0.3.0
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.Reducer. An optional Describe option attaches palette metadata.
func (*Builder[S, E, C]) Reenter ¶ added in v0.2.0
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 (*Builder[S, E, C]) Region ¶
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 (*Builder[S, E, C]) Requires ¶
func (b *Builder[S, E, C]) Requires(req Requirement[C]) *Builder[S, E, C]
Requires attaches a requirement to the most-recent state.
func (*Builder[S, E, C]) Respond ¶ added in v0.2.0
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 (*Builder[S, E, C]) SendParent ¶ added in v0.2.0
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 (*Builder[S, E, C]) SendTo ¶ added in v0.2.0
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 (*Builder[S, E, C]) Service ¶ added in v0.2.0
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 *UnboundRefError, mirroring guards and actions. An optional Describe option attaches palette metadata.
func (*Builder[S, E, C]) Spawn ¶ added in v0.2.0
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 (*Builder[S, E, C]) State ¶
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 (*Builder[S, E, C]) StopActor ¶ added in v0.2.0
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 or invoked-child actor before its natural completion. Stopping an unknown id is a host-side no-op. The built-in needs no host registration, mirroring Cancel.
func (*Builder[S, E, C]) SubState ¶
SubState declares a substate of the current SuperState or Region block.
func (*Builder[S, E, C]) SuperState ¶
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 (*Builder[S, E, C]) Temper ¶
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 (*Builder[S, E, C]) Transition ¶
Transition opens a new edge from the given state.
func (*Builder[S, E, C]) Use ¶
func (b *Builder[S, E, C]) Use(mw ...Middleware[S, E, C]) *Builder[S, E, C]
Use installs middleware that wraps every Fire.
func (*Builder[S, E, C]) WaitMode ¶
WaitMode tags the most-recent transition's synchronization mode.
func (*Builder[S, E, C]) When ¶
When attaches a named guard ref with params to the most-recent transition.
func (*Builder[S, E, C]) WhenExpr ¶ added in v0.2.0
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 ¶ added in v0.3.0
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.
type CancelScheduled ¶ added in v0.2.0
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"`
}
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.
func (CancelScheduled) Kind ¶ added in v0.3.0
func (CancelScheduled) Kind() string
Kind reports the cancel-scheduled effect discriminant.
type CastOption ¶
type CastOption[S comparable] func(*castConfig[S])
CastOption configures Cast.
func WithClock ¶ added in v0.2.0
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 WithFullTrace ¶ added in v0.3.0
func WithFullTrace[S comparable]() CastOption[S]
WithFullTrace opts the instance into full trace mode, populating all rich diagnostic fields on every FireResult.Trace: GuardsEvaluated, EffectsEmitted, ExitedStates, EnteredStates, AssignsApplied, Microsteps, EventPayload, and SelectedTransition. Without this option (or WithInspector / WithHistory / WithUnboundedHistory), those fields are empty to minimize allocations on instances whose traces are not observed. Logger-only instances stay lite.
func WithHistory ¶ added in v0.3.0
func WithHistory[S comparable](limit int) CastOption[S]
WithHistory enables a bounded ring-buffer trace history of capacity limit. The last limit settled traces are retained; older entries are overwritten in declaration order. limit <= 0 is a no-op (no retention). Implies full trace. Use History() to retrieve the stored traces in chronological order.
func WithInitialState ¶
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 ¶ added in v0.2.0
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 ¶ added in v0.3.0
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.
func WithUnboundedHistory ¶ added in v0.3.0
func WithUnboundedHistory[S comparable]() CastOption[S]
WithUnboundedHistory enables append-only trace history: every settled trace is retained. This is the previous default behavior, now opt-in. Suitable for short-lived or test instances; long-lived production instances should prefer WithHistory to bound memory growth. Implies full trace.
type Clock ¶ added in v0.2.0
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
}
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.
func SystemClock ¶ added in v0.2.0
func SystemClock() Clock
SystemClock returns the wall-clock Clock backed by the standard library, for a production host driver.
type ContextCodec ¶ added in v0.2.0
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 ContextSchema ¶ added in v0.3.0
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
}
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.
func SchemaOf ¶ added in v0.3.0
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 (ContextSchema) FieldAt ¶ added in v0.3.0
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 (ContextSchema) MarshalJSON ¶ added in v0.3.0
func (s ContextSchema) MarshalJSON() ([]byte, error)
MarshalJSON encodes a ContextSchema, merging its preserved unknown keys back in with stable key ordering.
func (*ContextSchema) UnmarshalJSON ¶ added in v0.3.0
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.
type ContextView ¶ added in v0.3.0
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)
}
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 DescribeBuilder ¶ added in v0.3.0
type DescribeBuilder struct {
// contains filtered or unexported fields
}
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.
func Describe ¶ added in v0.3.0
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 (*DescribeBuilder) EnumParam ¶ added in v0.3.0
func (d *DescribeBuilder) EnumParam(name string, allowed ...string) *DescribeBuilder
EnumParam declares a required enum parameter constrained to the given allowed values.
func (*DescribeBuilder) OptionalParam ¶ added in v0.3.0
func (d *DescribeBuilder) OptionalParam(name string, typ ParamType) *DescribeBuilder
OptionalParam declares an optional parameter of the given type.
func (*DescribeBuilder) Param ¶ added in v0.3.0
func (d *DescribeBuilder) Param(name string, typ ParamType) *DescribeBuilder
Param declares a required parameter of the given type.
func (*DescribeBuilder) ParamSpec ¶ added in v0.3.0
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 (*DescribeBuilder) Reads ¶ added in v0.3.0
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 (*DescribeBuilder) Writes ¶ added in v0.3.0
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.
type DescribeOption ¶ added in v0.3.0
type DescribeOption interface {
// contains filtered or unexported methods
}
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 Descriptor ¶ added in v0.3.0
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"`
}
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.
func BuiltinPalette ¶ added in v0.3.0
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.
type DescriptorKind ¶ added in v0.3.0
type DescriptorKind string
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.
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" )
The descriptor kinds, one per palette-eligible registration surface.
type Diagnostic ¶
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
}
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 Effect ¶
type Effect = any
Effect is an abstract, domain-defined payload. The kernel never inspects it.
type EffectEnvelope ¶ added in v0.3.0
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
}
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.
func MarshalEffect ¶ added in v0.3.0
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 (EffectEnvelope) MarshalJSON ¶ added in v0.3.0
func (e EffectEnvelope) MarshalJSON() ([]byte, error)
MarshalJSON encodes an EffectEnvelope, merging its preserved unknown keys back in with stable key ordering.
func (*EffectEnvelope) UnmarshalJSON ¶ added in v0.3.0
func (e *EffectEnvelope) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes an EffectEnvelope and captures any unknown keys into extra so they survive re-serialization.
type EffectFactory ¶ added in v0.3.0
type EffectFactory func() Effect
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 EffectRegistry ¶ added in v0.3.0
type EffectRegistry struct {
// contains filtered or unexported fields
}
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.
func NewEffectRegistry ¶ added in v0.3.0
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 (*EffectRegistry) Dispatchable ¶ added in v0.3.0
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 *UnknownEffectKindError, completing the preserve-on-load, reject-on-dispatch policy: a foreign effect is never silently applied.
func (*EffectRegistry) Unmarshal ¶ added in v0.3.0
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.
type ErrActorPanic ¶ added in v0.3.0
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
}
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.
func (*ErrActorPanic) Error ¶ added in v0.3.0
func (e *ErrActorPanic) Error() string
Error renders the recovered actor panic.
type EscalationHandler ¶ added in v0.3.0
type EscalationHandler func(ctx context.Context, esc *ActorEscalation)
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 FakeClock ¶ added in v0.2.0
type FakeClock struct {
// contains filtered or unexported fields
}
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.
func NewFakeClock ¶ added in v0.2.0
NewFakeClock returns a FakeClock starting at the given instant. The zero instant is fine; only relative advances matter for delayed transitions.
func (*FakeClock) Advance ¶ added in v0.2.0
Advance moves the fake clock forward by d. After advancing, call Scheduler.Tick to fire any now-due timers.
type FieldRef ¶ added in v0.3.0
type FieldRef[S comparable] struct { // contains filtered or unexported fields }
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.
func Field ¶ added in v0.3.0
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 (FieldRef[S]) Eq ¶ added in v0.3.0
Eq builds a Core equality comparison between the field and the given operand.
func (FieldRef[S]) Ge ¶ added in v0.3.0
Ge builds a Core greater-than-or-equal comparison: field >= operand.
func (FieldRef[S]) In ¶ added in v0.3.0
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 (FieldRef[S]) Le ¶ added in v0.3.0
Le builds a Core less-than-or-equal comparison: field <= operand.
type FireFunc ¶
type FireFunc[S comparable, E comparable, C any] func(ctx context.Context, event E) FireResult[S]
FireFunc is the inner step the middleware chain wraps.
type FireOption ¶
type FireOption func(*fireConfig)
FireOption configures Fire / FireSeq / FireEach.
func CollectAll ¶
func CollectAll() FireOption
CollectAll makes a batch fire run every step and gather all errors instead of stopping at the first.
func WithEventData ¶ added in v0.3.0
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.
type FireResult ¶
type FireResult[S comparable] struct { NewState S Effects []Effect Trace Trace Err error }
FireResult is the result of a single Fire.
func FireEach ¶
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. It is the many-instances counterpart to Instance.Fire (one event, one instance) and Instance.FireSeq (many events, one instance).
type ForgeOption ¶
type ForgeOption func(*forgeConfig)
ForgeOption configures Forge.
func WithMachineID ¶ added in v0.3.0
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 ¶ added in v0.3.0
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.
type ForwardEvent ¶ added in v0.2.0
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"`
}
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.
func (ForwardEvent) Kind ¶ added in v0.3.0
func (ForwardEvent) Kind() string
Kind reports the forward-event effect discriminant.
type GuardBinding ¶ added in v0.3.0
type GuardBinding[C any] interface { EvalGuard(ctx context.Context, req GuardRequest[C]) (GuardResult, error) }
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 GuardFailedError ¶ added in v0.3.0
GuardFailedError is returned when a named guard returned false.
func (*GuardFailedError) Error ¶ added in v0.3.0
func (e *GuardFailedError) Error() string
type GuardKind ¶ added in v0.3.0
type GuardKind string
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.
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" )
type GuardNode ¶ added in v0.2.0
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 }
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.
func And ¶ added in v0.2.0
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())
}
Output: no role: locked auditor: open
func Guard ¶ added in v0.2.0
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 ¶ added in v0.2.0
func Not[S comparable](node GuardNode[S]) GuardNode[S]
Not inverts a single guard.
func Or ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 (*GuardNode[S]) LeafRefs ¶ added in v0.2.0
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 (GuardNode[S]) MarshalJSON ¶ added in v0.3.0
MarshalJSON encodes a GuardNode, merging its preserved unknown keys back in with stable key ordering.
func (*GuardNode[S]) StateInTargets ¶ added in v0.2.0
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 (*GuardNode[S]) UnmarshalJSON ¶ added in v0.3.0
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.
type GuardOp ¶ added in v0.2.0
type GuardOp string
GuardOp tags the kind of a node in a guard expression tree.
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" )
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.
type GuardPanicError ¶ added in v0.3.0
GuardPanicError is returned when a guard panicked and was recovered.
func (*GuardPanicError) Error ¶ added in v0.3.0
func (e *GuardPanicError) Error() string
type GuardRequest ¶ added in v0.3.0
type GuardRequest[C any] struct { Name string Params map[string]any Context ContextView }
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 GuardResult ¶ added in v0.3.0
type GuardResult struct {
OK bool
}
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 HistoryType ¶
type HistoryType int
HistoryType is the reserved drop-in surface for shallow/deep history states.
const ( HistoryNone HistoryType = iota HistoryShallow HistoryDeep )
History kinds. HistoryNone is the v1 default (no history); shallow and deep are reserved for the deferred history-state feature.
type IOSpec ¶ added in v0.3.0
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"`
}
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 IR ¶
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 }
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.
func LoadFromJSON ¶
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[S, E, C]) MarshalJSON ¶ added in v0.3.0
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[S, E, C]) Provide ¶
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 *UnboundRefError (the same failure the DSL raises for an unregistered ref), so a UI/JSON-authored machine and a DSL-authored machine fail identically.
Example ¶
ExampleIR_Provide shows the JSON-rehydrate-then-run story: a machine authored in code is serialized with ToJSON, reloaded with LoadFromJSON into a behavior- free IR, and only then bound to host behavior with Provide before Quench. The guard func is supplied at Provide time, after the JSON was loaded, proving the structure travels as data while the Go behavior is re-attached by the host.
package main
import (
"context"
"fmt"
"github.com/stablekernel/crucible/state"
)
// gate is the entity the finalize-seam example guards and assigns against.
type gate struct {
approved bool
stamped bool
}
func main() {
// Author and freeze a machine in code, then serialize its structure. The
// guard func itself is not serializable; only its name travels in the JSON.
authored := state.Forge[string, string, gate]("turnstile").
Guard("approved", func(c state.GuardCtx[gate]) bool { return c.Entity.approved }).
State("locked").
Transition("locked").On("push").GoTo("open").When("approved").
State("open").
Initial("locked").
Quench()
jsonBytes, err := authored.ToJSON(state.WithoutSrcPos())
if err != nil {
fmt.Println("ToJSON err:", err)
return
}
// Reload the structure with no behavior attached.
ir, err := state.LoadFromJSON[string, string, gate](jsonBytes)
if err != nil {
fmt.Println("LoadFromJSON err:", err)
return
}
// Bind the guard func AFTER loading, supplying host behavior by name.
reg := state.NewRegistry[gate]().
Guard("approved", func(c state.GuardCtx[gate]) bool { return c.Entity.approved })
m := ir.Provide(reg).Quench()
allowed := m.Cast(gate{approved: true}, state.WithInitialState("locked"))
allowed.Fire(context.Background(), "push")
fmt.Println("rehydrated:", allowed.Current())
}
Output: rehydrated: open
func (*IR[S, E, C]) UnmarshalJSON ¶ added in v0.3.0
UnmarshalJSON decodes an IR and captures any unknown top-level keys into extra so they survive re-serialization.
type InFlightService ¶ added in v0.3.0
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"`
}
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 InspectKind ¶ added in v0.2.0
type InspectKind string
InspectKind names a category of inspection event, covering the inspection event types.
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" )
type InspectionEvent ¶ added in v0.2.0
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
}
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 Inspector ¶ added in v0.2.0
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)
}
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 InspectorFunc ¶ added in v0.2.0
type InspectorFunc func(ev InspectionEvent)
InspectorFunc adapts a plain function to the Inspector interface, for the common case of a single closure sink.
func (InspectorFunc) Inspect ¶ added in v0.2.0
func (f InspectorFunc) Inspect(ev InspectionEvent)
Inspect calls the underlying function.
type Instance ¶
type Instance[S comparable, E comparable, C any] struct { // contains filtered or unexported fields }
Instance binds a Machine to one entity and carries trace history.
func (*Instance[S, E, C]) Clock ¶ added in v0.2.0
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 (*Instance[S, E, C]) Configuration ¶
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 (*Instance[S, E, C]) Current ¶
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 (*Instance[S, E, C]) Entity ¶
func (i *Instance[S, E, C]) Entity() C
Entity returns the entity this instance is bound to.
func (*Instance[S, E, C]) Fire ¶
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. To drive a sequence of events into one instance use FireSeq; to fan one event across many instances use the top-level FireEach.
func (*Instance[S, E, C]) FireSeq ¶
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. It is the many-events form of Fire; to fan a single event across many instances use the top-level FireEach.
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
func (*Instance[S, E, C]) History ¶
History returns a copy of the traces recorded on this instance, in chronological order (oldest first). It returns nil when no history retention mode was selected at Cast (the default).
For a bounded ring buffer (WithHistory), the returned slice contains at most limit entries; if fewer than limit fires have occurred it contains all of them. For unbounded retention (WithUnboundedHistory) it contains every fired trace.
func (*Instance[S, E, C]) InFinal ¶ added in v0.2.0
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 (*Instance[S, E, C]) ResumeEffects ¶ added in v0.2.0
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 (*Instance[S, E, C]) Snapshot ¶ added in v0.2.0
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 (*Instance[S, E, C]) StartEffects ¶ added in v0.2.0
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.
type InvalidTransitionError ¶ added in v0.3.0
InvalidTransitionError 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.
func (*InvalidTransitionError) Error ¶ added in v0.3.0
func (e *InvalidTransitionError) Error() string
type Invocation ¶ added in v0.2.0
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 *UnboundRefError (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"` }
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 InvokeOption ¶ added in v0.2.0
type InvokeOption func(*invokeConfig)
InvokeOption configures a Builder.Invoke declaration.
func WithInput ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 WithInvokeOnDone ¶ added in v0.3.0
func WithInvokeOnDone[E comparable](onDone E) InvokeOption
WithInvokeOnDone sets the event the host re-fires through Fire when an invoked service completes successfully (or, for InvokeActor, when the child machine reaches its final state), routing the result through an ordinary transition from the owning state. Omitting it leaves the invocation's OnDone at the zero event. It mirrors WithSpawnOnDone for the declarative Invoke / InvokeActor surface, so completion routing arrives as an additive option rather than a positional parameter; the same shape lets fire-and-forget or onCancel routing arrive later as further options without a signature change.
func WithInvokeOnError ¶ added in v0.3.0
func WithInvokeOnError[E comparable](onError E) InvokeOption
WithInvokeOnError sets the event the host re-fires through Fire when an invoked service fails (or, for InvokeActor, when the child machine fails), routing the error through an ordinary transition from the owning state. Omitting it leaves the invocation's OnError at the zero event. It mirrors WithSpawnOnError.
func WithServiceParams ¶ added in v0.2.0
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 ¶ added in v0.2.0
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.
type JournalEntry ¶ added in v0.3.0
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"`
}
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 JournalKind ¶ added in v0.3.0
type JournalKind string
JournalKind classifies a JournalEntry's recorded nondeterministic result, so a replay routes each recorded value back to the source that produced it.
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" )
JournalKind values, one per nondeterministic source the replay contract covers.
type KindedEffect ¶ added in v0.3.0
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
}
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 Literal ¶ added in v0.3.0
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"`
}
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 Machine ¶
type Machine[S comparable, E comparable, C any] struct { // contains filtered or unexported fields }
Machine is the immutable, Quenched definition.
func (*Machine[S, E, C]) Cast ¶
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 *NoInitialStateError — 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
func (*Machine[S, E, C]) Palette ¶ added in v0.3.0
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 (*Machine[S, E, C]) PlanPath ¶
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. NoPathError 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
func (*Machine[S, E, C]) Requirements ¶
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 (*Machine[S, E, C]) Restore ¶ added in v0.2.0
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 (*Machine[S, E, C]) Services ¶ added in v0.2.0
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 (*Machine[S, E, C]) ToDOT ¶
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 (*Machine[S, E, C]) ToJSON ¶
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
func (*Machine[S, E, C]) ToMermaid ¶
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
func (*Machine[S, E, C]) Verify ¶ added in v0.3.0
func (m *Machine[S, E, C]) Verify(s S, entity C, opts ...VerifyOption) error
Verify checks that an externally-constructed entity legally satisfies a state's declarative requirements, without firing. The default mode is fail-fast (the returned *VerifyError carries the first failure); Aggregate collects every failure in one pass. The error type is uniform across modes.
Example ¶
ExampleMachine_Verify checks an externally-built entity against a state's declarative requirements without firing a transition.
m := buildDocMachine()
missing := m.Verify(Approved, &Document{Status: Approved})
ok := m.Verify(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>
type MessagePhase ¶ added in v0.2.0
type MessagePhase string
MessagePhase distinguishes the lifecycle point of an InspectMessage event: a message is observed when it is sent, and again when the host delivers it.
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" )
type MicrostepOverflowError ¶ added in v0.3.0
MicrostepOverflowError 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.
func (*MicrostepOverflowError) Error ¶ added in v0.3.0
func (e *MicrostepOverflowError) Error() string
type Middleware ¶
type Middleware[S comparable, E comparable, C any] func(next FireFunc[S, E, C]) FireFunc[S, E, C]
Middleware wraps a Fire, outside-in.
type MultiRegionError ¶ added in v0.3.0
type MultiRegionError struct {
Errors []error
}
MultiRegionError 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.
func (*MultiRegionError) Error ¶ added in v0.3.0
func (e *MultiRegionError) Error() string
func (*MultiRegionError) Unwrap ¶ added in v0.3.0
func (e *MultiRegionError) Unwrap() []error
Unwrap exposes the per-region errors for errors.As / errors.Is traversal.
type NoInitialStateError ¶ added in v0.3.0
type NoInitialStateError struct {
Machine string
}
NoInitialStateError 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.
func (*NoInitialStateError) Error ¶ added in v0.3.0
func (e *NoInitialStateError) Error() string
type NoPathError ¶ added in v0.3.0
NoPathError is returned by PlanPath when no event sequence connects from->to.
func (*NoPathError) Error ¶ added in v0.3.0
func (e *NoPathError) Error() string
type Operand ¶ added in v0.3.0
type Operand[S comparable] struct { // contains filtered or unexported fields }
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.
func Bool ¶ added in v0.3.0
func Bool[S comparable](v bool) Operand[S]
Bool builds a boolean literal operand.
func Dur ¶ added in v0.3.0
func Dur[S comparable](v time.Duration) Operand[S]
Dur builds a duration literal operand, carried as its Go duration string.
func FieldOp ¶ added in v0.3.0
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 ¶ added in v0.3.0
func Float[S comparable](v float64) Operand[S]
Float builds a floating-point literal operand.
func Int ¶ added in v0.3.0
func Int[S comparable](v int64) Operand[S]
Int builds an integer literal operand.
func Param ¶ added in v0.3.0
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 ¶ added in v0.3.0
func Str[S comparable](v string) Operand[S]
Str builds a string literal operand.
type Outcome ¶
type Outcome int
Outcome classifies the result recorded in a Trace.
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 )
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.
type ParamSpec ¶ added in v0.3.0
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"`
}
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 ParamType ¶ added in v0.3.0
type ParamType string
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.
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" )
The parameter types. EnumParam additionally carries its allowed values on the owning ParamSpec via the Describe builder's EnumParamOf helper.
type PendingRefs ¶ added in v0.2.0
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"`
}
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 PolicyDeniedError ¶ added in v0.3.0
PolicyDeniedError is returned when a policy returned Deny.
func (*PolicyDeniedError) Error ¶ added in v0.3.0
func (e *PolicyDeniedError) Error() string
type QuenchOption ¶
type QuenchOption func(*quenchConfig)
QuenchOption configures Quench.
func Strict ¶
func Strict() QuenchOption
Strict makes Quench reject any lint warning, not just hard errors.
type Ref ¶
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
}
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).
func (Ref) MarshalJSON ¶ added in v0.3.0
MarshalJSON encodes a Ref, merging its preserved unknown keys back in with stable key ordering.
func (*Ref) UnmarshalJSON ¶ added in v0.3.0
UnmarshalJSON decodes a Ref and captures any unknown keys into extra so they survive re-serialization.
type Region ¶
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"` }
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 RegisterEffectOption ¶ added in v0.3.0
type RegisterEffectOption func(*EffectRegistry)
RegisterEffectOption configures a NewEffectRegistry call. New deserialization knobs arrive as new options, never as a signature change.
func RegisterEffect ¶ added in v0.3.0
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.
type Registry ¶
type Registry[C any] struct { // contains filtered or unexported fields }
Registry holds the host behavior palette, by name.
func NewRegistry ¶
NewRegistry returns an empty host registry.
func (*Registry[C]) Action ¶
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 (*Registry[C]) Actor ¶ added in v0.3.0
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 (*Registry[C]) BindGuard ¶ added in v0.3.0
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 GuardPanicError. 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 (*Registry[C]) Guard ¶
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 (*Registry[C]) Palette ¶ added in v0.3.0
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))
}
Output: [ { "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 (*Registry[C]) Reducer ¶ added in v0.3.0
func (r *Registry[C]) Reducer(name string, fn AssignFn[C], opts ...DescribeOption) *Registry[C]
Reducer 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: registration and wiring are split cleanly, mirroring Guard/When and Action/Do. Registry.Reducer (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) and wire each use (Assign(name)). The implementation type stays AssignFn — you register it via Reducer and wire it via Assign.
func (*Registry[C]) Service ¶ added in v0.2.0
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 *UnboundRefError (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.
type Requirement ¶
type Requirement[C any] struct { Name string Predicate func(C) bool Setter func(C) // optional: mutate a zero entity to satisfy Predicate }
Requirement is a declarative condition for a state, used by Verify.
type RequirementFailure ¶
RequirementFailure records one unmet requirement.
type RespondToSender ¶ added in v0.2.0
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"`
}
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.
func (RespondToSender) Kind ¶ added in v0.3.0
func (RespondToSender) Kind() string
Kind reports the respond-to-sender effect discriminant.
type RestoreOption ¶ added in v0.2.0
type RestoreOption[S comparable] func(*restoreConfig[S])
RestoreOption configures Machine.Restore.
func RejectMachineVersionMismatch ¶ added in v0.3.0
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 ¶ added in v0.2.0
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().
func WithRestoreFullTrace ¶ added in v0.3.0
func WithRestoreFullTrace[S comparable]() RestoreOption[S]
WithRestoreFullTrace restores an instance in full trace mode, mirroring WithFullTrace at Cast. A restored instance is lite by default like a freshly cast one; a journal/replay consumer that reconstructs from the recorded Trace (the durable runner) restores in full mode so the rich per-step fields and the EventPayload are produced on every replayed Fire.
func WithRestoreUnboundedHistory ¶ added in v0.3.0
func WithRestoreUnboundedHistory[S comparable]() RestoreOption[S]
WithRestoreUnboundedHistory restores an instance in full trace mode with unbounded history retention, mirroring WithUnboundedHistory at Cast. The durable runner uses it on recover: it reconstructs the step ordinal and checkpoint snapshots from the retained Trace history, so post-recovery Fires must keep retaining every trace exactly as a freshly started durable instance does.
type ScheduleAfter ¶ added in v0.2.0
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"`
}
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).
func (ScheduleAfter) Kind ¶ added in v0.3.0
func (ScheduleAfter) Kind() string
Kind reports the schedule-after effect discriminant.
type Scheduler ¶ added in v0.2.0
type Scheduler[S comparable, E comparable, C any] struct { // contains filtered or unexported fields }
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.
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())
}
Output: before: pending pending timers: 1 after: expired pending timers: 0
func NewScheduler ¶ added in v0.2.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 (*Scheduler[S, E, C]) Absorb ¶ added in v0.2.0
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 (*Scheduler[S, E, C]) HasPending ¶ added in v0.2.0
HasPending reports whether a timer with the given schedule id is armed.
func (*Scheduler[S, E, C]) Pending ¶ added in v0.2.0
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 (*Scheduler[S, E, C]) Tick ¶ added in v0.2.0
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.
type SchemaField ¶ added in v0.3.0
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
}
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).
func (SchemaField) MarshalJSON ¶ added in v0.3.0
func (f SchemaField) MarshalJSON() ([]byte, error)
MarshalJSON encodes a SchemaField, merging its preserved unknown keys back in with stable key ordering.
func (*SchemaField) UnmarshalJSON ¶ added in v0.3.0
func (f *SchemaField) UnmarshalJSON(data []byte) error
UnmarshalJSON decodes a SchemaField and captures any unknown keys into extra so they survive re-serialization.
type SchemaKind ¶ added in v0.3.0
type SchemaKind string
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.
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" )
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.
type SendOption ¶ added in v0.2.0
type SendOption func(*sendConfig)
SendOption configures a Builder.SendTo / Builder.ForwardTo declaration (the actor-communication send built-ins).
func WithSendToSystemID ¶ added in v0.2.0
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.
type SendParent ¶ added in v0.2.0
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"`
}
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.
func (SendParent) Kind ¶ added in v0.3.0
func (SendParent) Kind() string
Kind reports the send-parent effect discriminant.
type SendTo ¶ added in v0.2.0
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"`
}
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 ServiceBinding ¶ added in v0.3.0
type ServiceBinding[C any] interface { RunService(ctx context.Context, req ServiceRequest[C]) (any, error) }
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 ServiceCtx ¶ added in v0.2.0
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 ServiceFn ¶ added in v0.2.0
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 ServiceRequest ¶ added in v0.3.0
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 ServiceRunner ¶ added in v0.2.0
type ServiceRunner[S comparable, E comparable, C any] struct { // contains filtered or unexported fields }
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.
func NewServiceRunner ¶ added in v0.2.0
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 (*ServiceRunner[S, E, C]) Absorb ¶ added in v0.2.0
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 (*ServiceRunner[S, E, C]) HasPending ¶ added in v0.2.0
func (r *ServiceRunner[S, E, C]) HasPending(id string) bool
HasPending reports whether a service with the given invoke id is in flight.
func (*ServiceRunner[S, E, C]) LastError ¶ added in v0.2.0
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 (*ServiceRunner[S, E, C]) LastResult ¶ added in v0.2.0
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 (*ServiceRunner[S, E, C]) Pending ¶ added in v0.2.0
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 (*ServiceRunner[S, E, C]) PendingIDs ¶ added in v0.2.0
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 (*ServiceRunner[S, E, C]) SettleDone ¶ added in v0.2.0
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 ¶ added in v0.2.0
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.
func (*ServiceRunner[S, E, C]) Tick ¶ added in v0.3.0
func (r *ServiceRunner[S, E, C]) Tick(ctx context.Context, id string) []FireResult[S]
Tick resolves and runs the in-flight service id against the bound registry, settling it with the ServiceFn's result or error. It is the ServiceRunner's advance verb — the host-driver counterpart of Scheduler.Tick and ActorSystem.Tick — coupling resolve + run + settle: a host that arms services from Absorb and wants the runner to execute them calls Tick(ctx, id) (typically from its own goroutine). It returns a one-element slice holding the routed FireResult when the service settled, sharing the []FireResult[S] shape of Scheduler.Tick and ActorSystem.Tick, or an empty slice when id is not in flight. A missing registry / unresolved ServiceFn still settles the service as an error (so the machine routes onError rather than hanging) and that routed result is returned in the slice.
type Snapshot ¶ added in v0.2.0
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"` }
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.
func UnmarshalSnapshot ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 (Snapshot[S, E, C]) MarshalJSON ¶ added in v0.2.0
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 (*Snapshot[S, E, C]) UnmarshalJSON ¶ added in v0.2.0
UnmarshalJSON deserializes the snapshot, decoding its context with the default JSON codec. The inverse of MarshalJSON.
type SnapshotCodecOption ¶ added in v0.2.0
type SnapshotCodecOption[C any] func(*snapshotCodecConfig[C])
SnapshotCodecOption configures MarshalSnapshot / UnmarshalSnapshot.
func WithContextCodec ¶ added in v0.2.0
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.
type SnapshotError ¶ added in v0.2.0
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.
func (*SnapshotError) Error ¶ added in v0.2.0
func (e *SnapshotError) Error() string
type SnapshotVersionError ¶ added in v0.3.0
type SnapshotVersionError struct {
Kind string
Machine string
Got string
Want string
Reason 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.
func (*SnapshotVersionError) Error ¶ added in v0.3.0
func (e *SnapshotVersionError) Error() string
type Snapshotter ¶ added in v0.2.0
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
}
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 SpawnActor ¶ added in v0.2.0
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"`
}
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).
func (SpawnActor) Kind ¶ added in v0.3.0
func (SpawnActor) Kind() string
Kind reports the spawn-actor effect discriminant.
type SpawnOption ¶ added in v0.2.0
type SpawnOption func(*spawnConfig)
SpawnOption configures a Builder.Spawn declaration (the dynamic spawn built-in).
func WithSpawnInput ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 ¶ added in v0.2.0
func WithSpawnSystemID(id string) SpawnOption
WithSpawnSystemID sets the system-scoped name a dynamically spawned actor registers under in the ActorSystem (its systemId).
type StartService ¶ added in v0.2.0
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"`
}
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).
func (StartService) Kind ¶ added in v0.3.0
func (StartService) Kind() string
Kind reports the start-service effect discriminant.
type State ¶
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 }
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.
func (State[S, E, C]) MarshalJSON ¶ added in v0.3.0
MarshalJSON encodes a State, merging its preserved unknown keys back in with stable key ordering.
func (*State[S, E, C]) UnmarshalJSON ¶ added in v0.3.0
UnmarshalJSON decodes a State and captures any unknown keys into extra so they survive re-serialization.
type Status ¶ added in v0.2.0
type Status int
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.
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 )
Instance lifecycle statuses recorded on a Snapshot.
type StopActor ¶ added in v0.2.0
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"`
}
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 StopService ¶ added in v0.2.0
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"`
}
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.
func (StopService) Kind ¶ added in v0.3.0
func (StopService) Kind() string
Kind reports the stop-service effect discriminant.
type ToJSONOption ¶
type ToJSONOption func(*toJSONConfig)
ToJSONOption configures ToJSON.
func WithoutSrcPos ¶
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.
type Trace ¶
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.
// Populated only in full mode.
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.
// Populated only in full mode.
SelectedTransition *Transition[any, any, any] `json:"-"`
// GuardsEvaluated names each guard the step evaluated, in evaluation order.
// Populated only in full mode.
GuardsEvaluated []string `json:"guardsEvaluated,omitempty"`
// PoliciesEvaluated names each policy the step evaluated, in evaluation order.
// Populated only in full mode.
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).
// Populated only in full mode.
EffectsEmitted []string `json:"effectsEmitted,omitempty"`
// AssignsApplied names each assign reducer the step folded, in fold order.
// Populated only in full mode.
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.
// Populated only in full mode.
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).
// Populated only in full mode.
ExitedStates []string `json:"exitedStates,omitempty"`
// EnteredStates records the entry cascade in execution order (outermost-first).
// Populated only in full mode.
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"`
// contains filtered or unexported fields
}
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.
Rich diagnostic fields (GuardsEvaluated, EffectsEmitted, ExitedStates, EnteredStates, AssignsApplied, Microsteps, EventPayload, SelectedTransition) are populated only when the trace is in full mode — enabled by WithFullTrace, WithInspector, WithHistory, or WithUnboundedHistory at Cast. Lite mode (the default) carries Machine, Event, FromState, MatchedAt, and Outcome, which is sufficient for structured logging and no-overhead default operation.
type Transition ¶
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 }
Transition is a directed edge.
func (Transition[S, E, C]) MarshalJSON ¶ added in v0.3.0
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 (*Transition[S, E, C]) UnmarshalJSON ¶ added in v0.3.0
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.
type UnboundActorError ¶ added in v0.3.0
type UnboundActorError struct {
Name string
}
UnboundActorError 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.
func (*UnboundActorError) Error ¶ added in v0.3.0
func (e *UnboundActorError) Error() string
type UnboundRefError ¶ added in v0.3.0
type UnboundRefError struct {
Kind string // "guard" | "action" | "assign" | "service"
Name string
}
UnboundRefError is returned when a guard/action/effect ref in the IR did not resolve against the registry (raised at Quench / Provide).
func (*UnboundRefError) Error ¶ added in v0.3.0
func (e *UnboundRefError) Error() string
type UndeclaredStateError ¶ added in v0.3.0
type UndeclaredStateError struct {
State string
}
UndeclaredStateError is returned when a state value was never declared.
func (*UndeclaredStateError) Error ¶ added in v0.3.0
func (e *UndeclaredStateError) Error() string
type UnknownBuiltinError ¶ added in v0.3.0
type UnknownBuiltinError struct {
Name string
}
UnknownBuiltinError 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.
func (*UnknownBuiltinError) Error ¶ added in v0.3.0
func (e *UnknownBuiltinError) Error() string
type UnknownEffect ¶ added in v0.3.0
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
}
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 *UnknownEffectKindError. The kernel never produces an UnknownEffect; only deserialization of a foreign envelope yields one.
func (UnknownEffect) Kind ¶ added in v0.3.0
func (u UnknownEffect) Kind() string
Kind reports the preserved, unrecognized discriminant.
type UnknownEffectKindError ¶ added in v0.3.0
type UnknownEffectKindError struct {
// Kind is the unrecognized effect discriminant.
Kind string
}
UnknownEffectKindError 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.
func (*UnknownEffectKindError) Error ¶ added in v0.3.0
func (e *UnknownEffectKindError) Error() string
type UnsupportedSchemaError ¶ added in v0.3.0
type UnsupportedSchemaError struct {
// Got is the schemaVersion declared in the document.
Got string
// Supported is the loader's own schema version.
Supported string
}
UnsupportedSchemaError 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.
func (*UnsupportedSchemaError) Error ¶ added in v0.3.0
func (e *UnsupportedSchemaError) Error() string
type VerifyError ¶ added in v0.3.0
type VerifyError struct {
Failures []RequirementFailure
}
VerifyError aggregates one or more failing requirements found by Verify.
func (*VerifyError) Error ¶ added in v0.3.0
func (e *VerifyError) Error() string
type VerifyOption ¶ added in v0.3.0
type VerifyOption func(*verifyConfig)
VerifyOption configures Verify.
func Aggregate ¶ added in v0.3.0
func Aggregate() VerifyOption
Aggregate makes Verify 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.
type VizOption ¶
type VizOption func(*vizConfig)
VizOption configures the ToMermaid and ToDOT renderers.
func LeftToRight ¶
func LeftToRight() VizOption
LeftToRight lays the diagram out left-to-right (Mermaid direction LR, DOT rankdir=LR).
func TopToBottom ¶
func TopToBottom() VizOption
TopToBottom lays the diagram out top-to-bottom (Mermaid default, DOT rankdir=TB).
func WithoutGuards ¶
func WithoutGuards() VizOption
WithoutGuards omits the bracketed guard annotations from transition labels.
func WithoutOwners ¶
func WithoutOwners() VizOption
WithoutOwners omits owner color-coding (Mermaid classDef / DOT fillcolor).
type WaitMode ¶
type WaitMode int
WaitMode tags a transition's synchronization expectation. The kernel only stores the tag; the consumer acts on it.
type WaitOption ¶ added in v0.2.0
type WaitOption[S comparable, E comparable, C any] func(*waitConfig[S, E, C])
WaitOption configures WaitFor.
func WithWaitScheduler ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 ¶ added in v0.2.0
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 ¶ added in v0.2.0
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.
type WaitPredicate ¶ added in v0.2.0
type WaitPredicate[S comparable, E comparable, C any] func(snap Snapshot[S, E, C]) bool
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.
func WaitDone ¶ added in v0.2.0
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 ¶ added in v0.2.0
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).
type WaitTimeoutError ¶ added in v0.2.0
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.
func (*WaitTimeoutError) Error ¶ added in v0.2.0
func (e *WaitTimeoutError) Error() string
Source Files
¶
- actor.go
- actor_comms.go
- actor_escalation.go
- actor_snapshot.go
- actor_system.go
- assign.go
- binding.go
- builder_hsm.go
- cascade.go
- contextview.go
- coreexpr.go
- doc.go
- driver.go
- effect.go
- envelope.go
- errors.go
- fire.go
- guard.go
- history.go
- hsm.go
- inspect.go
- invoke.go
- ir.go
- kernel.go
- options.go
- palette.go
- parallel.go
- plan.go
- quench.go
- runner.go
- scheduler.go
- schema.go
- snapshot.go
- verify.go
- viz.go
- waitfor.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package analysis performs static model-checking over a Quenched Crucible state machine.
|
Package analysis performs static model-checking over a Quenched Crucible state machine. |
|
Package conformance proves that a state machine behaves correctly.
|
Package conformance proves that a state machine behaves correctly. |
|
Package evolution classifies the difference between two versions of a state machine definition as additive (backward-compatible) or breaking, following the Crucible Evolution Guide.
|
Package evolution classifies the difference between two versions of a state machine definition as additive (backward-compatible) or breaking, following the Crucible Evolution Guide. |
|
Package verify checks behavioral properties of a Quenched Crucible state machine and returns, for every property it decides, a witness: the concrete event sequence that proves or refutes the claim.
|
Package verify checks behavioral properties of a Quenched Crucible state machine and returns, for every property it decides, a witness: the concrete event sequence that proves or refutes the claim. |
|
symbolic
Package symbolic reasons about a machine's guards structurally — without executing them — over the kernel's Core GuardNode tree.
|
Package symbolic reasons about a machine's guards structurally — without executing them — over the kernel's Core GuardNode tree. |