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
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_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.