Pure recursive evaluator for Hourglass workflows. Per activation:
Replay-walks the activation's jobs to extend a
Workflow.State:initialize_workflow→state.input; eachresolve_activity{seq}→state.resolved_results[seq] = decoded_value;remove_from_cache→ return:evictshape.Re-executes the workflow module's
run/1inline (no Task spawn). The Workflow API primitives consult the evaluator state via the process dict (CommandAccumulator) and either return a resolved value orthrow(:hourglass_temporal_suspend)once the body hits a command that has no result yet.Catches the suspend sentinel at the
evaluate/3boundary, sorts the accumulated commands by command_id (lexicographic), assigns monotonic global seqs starting atstate.next_global_seq, and encodes aWorkflowActivationCompletionproto.
No GenServer. No :persistent_term. No send + receive suspension.
State is threaded through arguments; the only mutable handle is the
process-dict command accumulator + the evaluator-state pointer, both
cleared on the way out.
Summary
Functions
Two-phase peek for a child workflow command. Like peek_command/2 it appends
the command on first sight and never throws — the caller decides when to
suspend. Unlike an activity, a child resolves twice, so this reports the start
phase and the result phase separately
Non-throwing variant of suspend_or_resolve/2 for racing primitives
(e.g. await_signal/2 with a timeout). First call appends the command;
returns :pending until resolved, then {:resolved, value}. Unlike
suspend_or_resolve/2 it never throws the suspend sentinel — the caller
decides whether to suspend after peeking multiple racers.
Called by Workflow.execute_activity (and friends) when the evaluator is
active on this process. Either returns the resolved value (replay path)
or appends a fresh command and throws the suspend sentinel.
Synchronous primitives (uuid, random) consume a deterministic
command_id but never produce a Temporal command and never suspend. They
short-circuit through this hook.
Functions
@spec evaluate( workflow_module :: module(), activation :: map(), state :: Hourglass.Workflow.State.t() ) :: {:ok, Coresdk.WorkflowCompletion.WorkflowActivationCompletion.t(), Hourglass.Workflow.State.t()}
@spec peek_child( Hourglass.Workflow.State.command_id(), Hourglass.Workflow.State.command_term() ) :: :pending | {:start_failed, term()} | {:start_cancelled, term()} | {:started, String.t()} | {:started, String.t(), {:ok, term()} | {:error, term()}}
Two-phase peek for a child workflow command. Like peek_command/2 it appends
the command on first sight and never throws — the caller decides when to
suspend. Unlike an activity, a child resolves twice, so this reports the start
phase and the result phase separately:
:pending— command issued; the start has not resolved yet.{:start_failed, cause}— the child could not be started. No result resolution will ever arrive, so a caller MUST NOT keep waiting.{:start_cancelled, failure}— cancelled during start.{:started, run_id}— running; the result has not resolved yet.{:started, run_id, {:ok, raw} | {:error, failure}}— result is in.
@spec peek_command( Hourglass.Workflow.State.command_id(), Hourglass.Workflow.State.command_term() ) :: :pending | {:resolved, term()}
Non-throwing variant of suspend_or_resolve/2 for racing primitives
(e.g. await_signal/2 with a timeout). First call appends the command;
returns :pending until resolved, then {:resolved, value}. Unlike
suspend_or_resolve/2 it never throws the suspend sentinel — the caller
decides whether to suspend after peeking multiple racers.
@spec suspend_or_resolve( Hourglass.Workflow.State.command_id(), Hourglass.Workflow.State.command_term() ) :: term() | no_return()
Called by Workflow.execute_activity (and friends) when the evaluator is
active on this process. Either returns the resolved value (replay path)
or appends a fresh command and throws the suspend sentinel.
@spec synchronous( Hourglass.Workflow.State.command_id(), Hourglass.Workflow.State.command_term() ) :: term()
Synchronous primitives (uuid, random) consume a deterministic
command_id but never produce a Temporal command and never suspend. They
short-circuit through this hook.