Skip to main content

ReplayCursor

Struct ReplayCursor 

Source
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

Source

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.

Source

pub fn run_id(&self) -> Option<RunId>

The run id the log carries, or None for a fresh (empty) log.

Source

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.

Source

pub fn is_finished(&self) -> bool

Whether a terminal event (Event::RunCompleted or Event::RunFailed) has been consumed or emitted.

Source

pub fn next_seq(&self) -> SequenceNumber

The position the next consumed or emitted event occupies.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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:

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.

Source

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.

Source

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

Source

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.

Source

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.

Source

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.

Source

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.

Source

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.

Trait Implementations§

Source§

impl Debug for ReplayCursor

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>

Formats the value using the given formatter. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.