pub struct ReplayCursor { /* private fields */ }Expand description
The pure replay/live cursor over one run’s event log.
Construct it with the recorded log (possibly empty, for a fresh run) and
drive it with one request per orchestration step. Each request returns an
Outcome: Outcome::Replayed while history covers the step,
Outcome::Live from the first uncovered step onward. See the module
docs for the full protocol and how RunCtx wraps it.
Implementations§
Source§impl ReplayCursor
impl ReplayCursor
Sourcepub fn new(log: Vec<EventEnvelope>) -> Result<ReplayCursor, ReplayError>
pub fn new(log: Vec<EventEnvelope>) -> Result<ReplayCursor, ReplayError>
Builds a cursor over a run’s recorded log.
An empty log is a fresh run: every request will be live. A non-empty
log must be well formed: it starts with a run head (Event::RunStarted
for an agent run or Event::GraphRunStarted for a graph run) at
position 0, all envelopes share one run id, positions are contiguous,
and nothing follows a terminal event.
§Errors
Returns ReplayError::MalformedLog when the log violates any of
those shape rules.
Sourcepub fn run_id(&self) -> Option<RunId>
pub fn run_id(&self) -> Option<RunId>
The run id the log carries, or None for a fresh (empty) log.
Sourcepub fn is_replaying(&self) -> bool
pub fn is_replaying(&self) -> bool
Whether recorded history remains to be consumed.
true means the next request will be answered from the log (or fail
as a divergence); false means the cursor has handed off to live
mode.
Sourcepub fn is_finished(&self) -> bool
pub fn is_finished(&self) -> bool
Whether a terminal event (Event::RunCompleted or
Event::RunFailed) has been consumed or emitted.
Sourcepub fn next_seq(&self) -> SequenceNumber
pub fn next_seq(&self) -> SequenceNumber
The position the next consumed or emitted event occupies.
Sourcepub fn begin(
&mut self,
agent_def_hash: &str,
labels: Option<BTreeMap<String, String>>,
) -> Result<Outcome<Value, BeginPermit<'_>>, ReplayError>
pub fn begin( &mut self, agent_def_hash: &str, labels: Option<BTreeMap<String, String>>, ) -> Result<Outcome<Value, BeginPermit<'_>>, ReplayError>
Requests the start of the run.
Replayed: verifies agent_def_hash against the recorded
Event::RunStarted (a changed agent definition must not silently
resume an old run) and returns the recorded input. Live: returns a
BeginPermit the caller redeems with the run’s input.
labels is the optional correlation tags to stamp on a genuinely
fresh Event::RunStarted; see the field’s docs. It plays no part in
replay: a recorded RunStarted is matched on agent_def_hash alone,
and whatever labels the caller passes here is ignored on that path,
exactly as ReplayCursor::model_call’s request_body is ignored
when replaying. Bounds on labels are not checked here (see the
field’s docs for why); a caller enforces them before calling.
§Errors
ReplayError::Divergence on a hash mismatch, a different recorded
event, or a request after the run already ended.
Sourcepub fn begin_graph(
&mut self,
graph_hash: &str,
labels: Option<BTreeMap<String, String>>,
forked_from: Option<ForkOrigin>,
) -> Result<Outcome<Value, GraphBeginPermit<'_>>, ReplayError>
pub fn begin_graph( &mut self, graph_hash: &str, labels: Option<BTreeMap<String, String>>, forked_from: Option<ForkOrigin>, ) -> Result<Outcome<Value, GraphBeginPermit<'_>>, ReplayError>
Requests the start of a graph run.
The graph-run counterpart of begin: the log
opens with Event::GraphRunStarted rather than Event::RunStarted,
because a graph run coordinates many agent hashes and has none at its
head. Replayed: verifies graph_hash against the recorded head (a
changed graph document must not silently resume an old run) and returns
the recorded input. Live: returns a GraphBeginPermit the caller
redeems with the run’s input.
labels and forked_from land on a genuinely fresh
Event::GraphRunStarted; they play no part in replay, exactly as
begin’s labels do. Bounds on labels are not
checked here; a caller enforces them before calling.
§Errors
ReplayError::Divergence on a hash mismatch, a different recorded
event, or a request after the run already ended.
Sourcepub fn node_entered(
&mut self,
node: &str,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn node_entered( &mut self, node: &str, ) -> Result<Outcome<(), Emitted>, ReplayError>
Requests entry into a graph node.
Replayed: matches the recorded Event::NodeEntered for node. Live:
returns the event to persist. A graph node’s own events (an agent loop’s
model calls, a tool call) are recorded between this and the matching
node_exited.
§Errors
ReplayError::Divergence on a node-id mismatch, a different recorded
event, or a request after the run already ended.
Sourcepub fn node_exited(
&mut self,
node: &str,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn node_exited( &mut self, node: &str, ) -> Result<Outcome<(), Emitted>, ReplayError>
Requests exit from a graph node, having produced its output.
Replayed: matches the recorded Event::NodeExited for node. Live:
returns the event to persist. The counterpart of
node_entered.
§Errors
ReplayError::Divergence on a node-id mismatch, a different recorded
event, or a request after the run already ended.
Sourcepub fn node_skipped(
&mut self,
node: &str,
reason: &str,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn node_skipped( &mut self, node: &str, reason: &str, ) -> Result<Outcome<(), Emitted>, ReplayError>
Records (or replays) that a graph node was skipped: reached on the walk
but deliberately not run (a branch routed past it). No
node_entered precedes a skip; the skip is
the sole marker for the node.
Replayed: matches the recorded Event::NodeSkipped for node (reason
included, so a skip that recorded one reason live cannot replay under
another). Live: returns the event to persist. The reason a caller
passes must be a pure function of the document and recorded values, like
every other emitted payload, so it reproduces on replay.
§Errors
ReplayError::Divergence on a node-id or reason mismatch, a different
recorded event, or a request after the run already ended.
Sourcepub fn branch_taken(
&mut self,
node: &str,
case: &str,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn branch_taken( &mut self, node: &str, case: &str, ) -> Result<Outcome<(), Emitted>, ReplayError>
Records (or replays) that a branch node routed: the named case fired.
This is the sole recorded authority for which way a branch went; the
executed path is read from these events, never inferred from skips.
Recorded between the branch’s node_entered
and node_exited. Replayed: matches the
recorded Event::BranchTaken for node and case (a branch that fired
one case live cannot replay having fired another). Live: returns the event
to persist. The chosen case must be a deterministic function of recorded
values (a pure expression over the routed value, or a decision recomputed
from a replayed model reply), so replay reproduces the same route.
§Errors
ReplayError::Divergence on a node-id or case mismatch, a different
recorded event, or a request after the run already ended.
Sourcepub fn map_fanned_out(
&mut self,
node: &str,
items: &Value,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn map_fanned_out( &mut self, node: &str, items: &Value, ) -> Result<Outcome<(), Emitted>, ReplayError>
Records (or replays) that a map node fanned out over a resolved item list.
Recorded between the map node’s node_entered
and its per-iteration markers. Replayed: matches the recorded
Event::MapFannedOut for node and items (the resolved list is part of
the recorded fact, so a fan-out that recorded one list live cannot replay
under another). Live: returns the event to persist. The items a caller
passes must be a deterministic function of recorded values (the over
reference resolved against the recorded routed value) so replay reproduces
the identical fan-out, which is what makes the derived per-iteration child
ids reproducible.
§Errors
ReplayError::Divergence on a node-id or item-list mismatch, a different
recorded event, or a request after the run already ended.
Sourcepub fn map_iteration_started(
&mut self,
node: &str,
index: u64,
child_run: &str,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn map_iteration_started( &mut self, node: &str, index: u64, child_run: &str, ) -> Result<Outcome<(), Emitted>, ReplayError>
Records (or replays) that one iteration of a map fan-out started, as a child
run with the derived id child_run.
Replayed: matches the recorded Event::MapIterationStarted on its
STRUCTURAL position (node and index) and the RECORDED child_run
wins, exactly as the recorded input wins in begin.
The passed child_run is compared only loosely (it is derived data), for a
deliberate reason: the id is sha256: over the parent run id, the node, and
the index, and a FORK replays the origin’s prefix under a NEW run id, so a
caller re-deriving the id there produces a different value than the origin
recorded. Trusting the recorded id keeps a fork’s inherited map markers
replayable while node/index still pin the structural position. Live:
returns the event to persist with the caller’s derived child_run.
§Errors
ReplayError::Divergence on a node-id or index mismatch, a different
recorded event, or a request after the run already ended.
Sourcepub fn map_iteration_joined(
&mut self,
node: &str,
index: u64,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn map_iteration_joined( &mut self, node: &str, index: u64, ) -> Result<Outcome<(), Emitted>, ReplayError>
Records (or replays) that one iteration of a map fan-out joined back into the map node’s output.
Joins are recorded in index order, never completion order, so the
concurrency of the fan-out never influences the parent log’s byte sequence.
Replayed: matches the recorded Event::MapIterationJoined for node and
index. Live: returns the event to persist.
§Errors
ReplayError::Divergence on a node-id or index mismatch, a different
recorded event, or a request after the run already ended.
Sourcepub fn now(
&mut self,
) -> Result<Outcome<OffsetDateTime, NowPermit<'_>>, ReplayError>
pub fn now( &mut self, ) -> Result<Outcome<OffsetDateTime, NowPermit<'_>>, ReplayError>
Requests the current time.
Replayed: returns the recorded instant, bit for bit. Live: returns a
NowPermit; the caller reads a real clock and redeems the permit
with the reading.
§Errors
ReplayError::Divergence when the log records a different kind of
event here, or the run already ended.
Sourcepub fn random(&mut self) -> Result<Outcome<u64, RandomPermit<'_>>, ReplayError>
pub fn random(&mut self) -> Result<Outcome<u64, RandomPermit<'_>>, ReplayError>
Requests random bits.
Replayed: returns the recorded bits, exactly. Live: returns a
RandomPermit; the caller draws from a real random source and
redeems the permit with the draw.
§Errors
ReplayError::Divergence when the log records a different kind of
event here, or the run already ended.
Sourcepub fn model_call(
&mut self,
request_hash: &str,
request_body: Option<Value>,
) -> Result<Outcome<ModelReply, ModelCallPermit<'_>>, ReplayError>
pub fn model_call( &mut self, request_hash: &str, request_body: Option<Value>, ) -> Result<Outcome<ModelReply, ModelCallPermit<'_>>, ReplayError>
Requests a model call identified by its request hash.
Replayed: matches the recorded intent/completion pair and returns the
recorded ModelReply. A recorded intent with no completion (the
process died mid-call) hands back a live ModelCallPermit with no
intent event: re-issuing an unanswered model request is safe, and the
fresh completion correlates to the recorded intent. Live: the permit
carries the intent event to persist before calling the provider.
request_body is the optional full request to record on the intent,
off by default (see the field on Event::ModelCallRequested). It is
stored only on a genuinely fresh live intent and is ignored everywhere
else: correlation keys on request_hash alone, so passing a body never
changes which recorded call matches or what replays.
§Errors
ReplayError::Divergence on a hash mismatch or a different recorded
event; ReplayError::MalformedLog when the event after the intent
is not its completion.
Sourcepub fn tool_call(
&mut self,
tool: &str,
input: &Value,
effect: Effect,
idempotency_key: Option<&str>,
) -> Result<Outcome<Value, ToolCallPermit<'_>>, ReplayError>
pub fn tool_call( &mut self, tool: &str, input: &Value, effect: Effect, idempotency_key: Option<&str>, ) -> Result<Outcome<Value, ToolCallPermit<'_>>, ReplayError>
Requests a tool call.
Replayed: matches the recorded intent/completion pair (tool, input, effect, and idempotency key must all be equal) and returns the recorded output. A recorded intent with no completion is the effect-table case:
Effect::ReadandEffect::Idempotenthand back a liveToolCallPermitto re-execute; the permit surfaces the recorded idempotency key so the retry reuses it.Effect::Writefails withReplayError::NeedsReconciliation. The write may have landed; only a human may decide.
Live: the permit carries the intent event, which the caller must persist before executing the tool. That write-ahead ordering is what makes a crash between intent and completion detectable.
§Errors
ReplayError::Divergence on any payload mismatch or a different
recorded event; ReplayError::NeedsReconciliation for a dangling
write intent; ReplayError::MalformedLog when the event after the
intent is not its completion.
Sourcepub fn dangling_intent(&self) -> Option<PendingCall>
pub fn dangling_intent(&self) -> Option<PendingCall>
The recorded call intent sitting at the cursor’s position with nothing after it: the gap a crash left, seen from where replay currently stands.
Read-only, and the reason it exists is the borrow discipline of the
layer above rather than replay itself. A caller that must consult the
outside world about a dangling intent needs to know it is looking at
one before it commits to a step, because
tool_call either advances the cursor or fails, with
nothing in between. This is the look before the leap.
None unless the cursor is at the last recorded event and that event is
a call intent. Answered from this run’s log alone, like everything else
here.
Sourcepub fn resume_unexecuted_tool_call(
&mut self,
tool: &str,
input: &Value,
effect: Effect,
idempotency_key: &str,
) -> Result<ToolCallPermit<'_>, ReplayError>
pub fn resume_unexecuted_tool_call( &mut self, tool: &str, input: &Value, effect: Effect, idempotency_key: &str, ) -> Result<ToolCallPermit<'_>, ReplayError>
Takes the live permit for a recorded tool intent that the caller has proven, outside this cursor, did not execute.
This is the narrow escape from ReplayError::NeedsReconciliation, and
it exists for exactly one situation: a call that resolved as a duplicate
of work another run had already committed, and whose process died
between recording the intent and recording the copied completion. Such a
call executed nothing, so parking it for a human would be asking a human
to reconcile an effect that provably never happened. Every other
dangling Effect::Write intent still parks; see
tool_call.
The cursor does not check the claim and cannot. Whether an intent
executed is a fact about the store and the world, not about this run’s
log, and this crate reads neither. The caller carries the burden of
proof; salvor-runtime discharges it by showing that the store’s
commitment for this call’s (tool, idempotency_key) is held by a
different run, which means this run never held the right to execute.
On success the recorded intent is consumed and the returned permit
carries no intent to persist (it is already in the log), so the caller
redeems it with
record_deduplicated.
§Errors
ReplayError::Divergence when the log position does not hold a
matching intent for this call, or when that intent is not the last event
in the log (an intent with a completion after it is not dangling and
must be replayed through tool_call instead).
Sourcepub fn suspend(
&mut self,
reason: &str,
input_schema: &Value,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn suspend( &mut self, reason: &str, input_schema: &Value, ) -> Result<Outcome<(), Emitted>, ReplayError>
Requests suspension of the run.
Replayed: matches the recorded Event::Suspended. Live: returns the
event to persist. Either way, follow with ReplayCursor::await_resume
to obtain the resume input (or learn the run is still parked).
§Errors
ReplayError::Divergence on a payload mismatch, a different
recorded event, or a request after the run already ended.
Sourcepub fn await_resume(
&mut self,
) -> Result<Outcome<Value, Parked<'_>>, ReplayError>
pub fn await_resume( &mut self, ) -> Result<Outcome<Value, Parked<'_>>, ReplayError>
Requests the input a parked run was resumed with.
Replayed: returns the recorded Event::Resumed input. Live: the
resume has not happened yet; the Parked value can be redeemed with
the input once a human provides it, or simply dropped, in which case
the run stays parked and the process may exit.
§Errors
ReplayError::Divergence when the log records a different kind of
event here, or the run already ended.
Sourcepub fn budget_exceeded(
&mut self,
budget: Budget,
observed: f64,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn budget_exceeded( &mut self, budget: Budget, observed: f64, ) -> Result<Outcome<(), Emitted>, ReplayError>
Reports a budget crossing, recorded so replay parks at the same point.
This is a runtime request, not an orchestration one: the budget gate
between events calls it when a declared limit is crossed. Checks must
be computed from replayed data (recorded token usage, recorded
timestamps), so a crossing that fired live is recomputed identically
on replay and matched here. Follow with
ReplayCursor::await_resume, exactly like a suspension.
§Errors
ReplayError::Divergence on a payload mismatch, a different
recorded event, or a request after the run already ended.
Sourcepub fn complete_run(
&mut self,
output: &Value,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn complete_run( &mut self, output: &Value, ) -> Result<Outcome<(), Emitted>, ReplayError>
Requests successful completion of the run.
Replayed: verifies the produced output equals the recorded one (a deterministic orchestration over replayed values must reproduce its own final answer). Live: returns the terminal event to persist. After either, every further request is a divergence.
§Errors
ReplayError::Divergence on an output mismatch, a different
recorded event, or a request after the run already ended.
Sourcepub fn fail_run(
&mut self,
error: &str,
) -> Result<Outcome<(), Emitted>, ReplayError>
pub fn fail_run( &mut self, error: &str, ) -> Result<Outcome<(), Emitted>, ReplayError>
Requests failure of the run.
The failing counterpart of ReplayCursor::complete_run, with the
same replay/live/terminal behavior.
§Errors
ReplayError::Divergence on an error-message mismatch, a different
recorded event, or a request after the run already ended.