Skip to main content

salvor_runtime/
ctx.rs

1//! [`RunCtx`]: the public durability substrate. One recorded run, one
2//! context; every operation is answered from history or executed live and
3//! persisted immediately.
4//!
5//! This is the library-first tier: a Rust team
6//! that wants to own its control flow writes an ordinary async function
7//! against this type and gets the same durability, replay, and budget
8//! guarantees as the built-in loop, which is itself written against exactly
9//! this surface.
10//!
11//! # What it owns, and what it wraps
12//!
13//! The pure replay cursor in `salvor-core` refuses to own three things: a
14//! store, executors, and the ambient clock/RNG. `RunCtx` owns all three and
15//! wraps each cursor request one to one:
16//!
17//! | `RunCtx` method | cursor request | live side effect |
18//! |---|---|---|
19//! | [`begin`](RunCtx::begin) | `begin` | persist `RunStarted` |
20//! | [`now`](RunCtx::now) | `now` | read the injected clock, persist |
21//! | [`random`](RunCtx::random) | `random` | draw from the injected RNG, persist |
22//! | [`model_call`](RunCtx::model_call) | `model_call` | persist intent, call provider, persist completion |
23//! | [`tool_call`](RunCtx::tool_call) | `tool_call` | persist intent **before executing**, execute, persist completion |
24//! | [`suspend`](RunCtx::suspend) | `suspend` | persist `Suspended` |
25//! | [`suspend_for_signal`](RunCtx::suspend_for_signal) | `suspend_for_signal` | persist `Suspended` marked as a signal wait |
26//! | [`await_resume`](RunCtx::await_resume) | `await_resume` | persist `Resumed` when input was provided |
27//! | [`sleep_until`](RunCtx::sleep_until) | `sleep_started` | persist `SleepStarted` |
28//! | [`await_wake`](RunCtx::await_wake) | `sleep_completed` | read the injected clock; persist `SleepCompleted` once the instant has passed |
29//! | [`budget_exceeded`](RunCtx::budget_exceeded) | `budget_exceeded` | persist `BudgetExceeded` |
30//! | [`complete_run`](RunCtx::complete_run) / [`fail_run`](RunCtx::fail_run) | same | persist the terminal event |
31//!
32//! Every live permit redemption persists its event *immediately*, with a
33//! timestamp read from the injected clock at this IO edge. Nothing is
34//! buffered: when a method returns `Ok`, the event is durable.
35//!
36//! # Injected clock and randomness
37//!
38//! The constructor takes the clock and RNG as functions. The defaults read
39//! the real clock and the operating system's randomness; tests inject
40//! deterministic ones, which makes whole event logs (envelopes included)
41//! comparable across runs. Note the injection covers the *envelope
42//! timestamps and observations*, not replay: replayed values always come
43//! from the log, whatever functions are installed.
44//!
45//! # Write-ahead ordering
46//!
47//! [`tool_call`](RunCtx::tool_call) persists the intent event and only then
48//! executes the tool. For a `Write`-effect tool this ordering is the whole
49//! reconciliation story: a crash between intent and completion leaves
50//! evidence, and resume refuses to guess. Model intents persist before the
51//! provider call for the same reason (though a dangling model intent is
52//! safely re-issued rather than reconciled).
53//!
54//! # Nothing happens twice, across runs as well as within one
55//!
56//! Replay is what keeps a resumed run from repeating itself: a recorded
57//! completion is read back, never re-executed. Two *independent* runs share no
58//! log, so replay has nothing to say about them, and something else has to
59//! hold the line. That something is the idempotency key, arbitrated by the
60//! store.
61//!
62//! The whole decision happens in [`tool_call`](RunCtx::tool_call), live,
63//! before the intent is written and before the tool runs; see that method for
64//! the mechanism and its boundaries. What matters here is the boundary it does
65//! not cross: **replay never consults the store about another run.** A
66//! recorded log is a complete description of its run, and folding it back
67//! produces the same result on a machine that has never seen the store the run
68//! was recorded against.
69//!
70//! # Retries inside one tool call
71//!
72//! One `tool_call` is one intent/completion pair, so retries of a failed
73//! live execution happen *inside* the call, between the two events, honoring
74//! `RetryPolicy`: `Read` and `Idempotent` handler failures re-execute up to
75//! [`MAX_TOOL_ATTEMPTS`] total attempts (idempotent retries reuse the same
76//! key, carried on `ToolCtx`), `Write` failures never re-execute, and input
77//! validation, an unreadable tool result, or output serialization never
78//! retries because it would fail identically again. Whatever the final result,
79//! the completion is recorded: an output, a suspension sentinel, or a failure
80//! object (see [`crate::wire`]).
81//!
82//! # Sleeping belongs between calls, never inside one
83//!
84//! [`sleep_until`](RunCtx::sleep_until) and [`sleep_for`](RunCtx::sleep_for)
85//! must not be called between a claimed tool call's intent and its
86//! completion. A claim is held for the whole span between the two, so every
87//! other run presenting that idempotency key gets `CallInFlight` for as long
88//! as the sleep lasts, and a durable timer lasts hours or weeks where a call
89//! lasts seconds. A process death inside such a sleep is worse: it leaves a
90//! dangling `Write` intent, which derives to
91//! [`RunStatus::NeedsReconciliation`](salvor_core::RunStatus::NeedsReconciliation)
92//! and stops the run until a human answers for the write by hand.
93//!
94//! Nothing in `RunCtx` can enforce the ordering, because the caller owns it
95//! and this type sees one request at a time. Sleep between completed calls.
96//!
97//! A tool that asks for the sleep itself is not an exception to that rule, it
98//! is the rule mechanized. A tool returning `ToolOutcome::Sleep` has its
99//! request encoded into its own `ToolCallCompleted` (see [`crate::wire`]), so
100//! the call settles, the claim releases, and only then does the driver call
101//! [`sleep_until`](RunCtx::sleep_until). The recorded order is intent,
102//! completion, `SleepStarted`, and a sleeping run therefore holds no claim.
103
104use std::collections::BTreeMap;
105use std::sync::Arc;
106
107use salvor_core::{
108    Budget, DedupOrigin, Effect, Emitted, Event, EventEnvelope, ModelReply, Outcome, PendingCall,
109    ReplayCursor, RunId, SequenceNumber, SuspensionKind, TokenUsage,
110};
111use salvor_llm::{Client, MessageAccumulator, MessageRequest, MessageResponse, StreamEvent};
112use salvor_store::{CallClaim, CallClaimant, CallCommitment, EventStore};
113use salvor_tools::{DynTool, RetryPolicy, Sleep, Suspension, ToolCtx, ToolError, ToolOutcome};
114use serde_json::Value;
115use time::{Duration, OffsetDateTime, PrimitiveDateTime};
116use uuid::Uuid;
117
118use crate::error::RuntimeError;
119use crate::hash::hash_value;
120use crate::labels::validate_labels;
121use crate::model::{response_value, usage_of};
122use crate::wire::{
123    ToolFailure, decode_failure, decode_sleep, decode_suspension, encode_failure, encode_sleep,
124    encode_suspension,
125};
126
127/// The injected clock: called once per persisted event (for the envelope
128/// timestamp) and once per live [`RunCtx::now`] observation.
129pub type ClockFn = Arc<dyn Fn() -> OffsetDateTime + Send + Sync>;
130
131/// The injected random source: called once per live [`RunCtx::random`]
132/// observation, returning 64 raw bits.
133pub type RandomFn = Arc<dyn Fn() -> u64 + Send + Sync>;
134
135/// The cap on total executions of one live tool call, counting the first
136/// attempt. Applies only where `RetryPolicy` allows retrying at all.
137pub const MAX_TOOL_ATTEMPTS: u32 = 3;
138
139/// A model call's result: the typed response plus the token usage recorded
140/// for it. Identical whether the call was executed live or replayed.
141#[derive(Debug, Clone)]
142pub struct ModelTurn {
143    /// The model's response.
144    pub response: MessageResponse,
145    /// The recorded token usage for this call.
146    pub usage: TokenUsage,
147}
148
149/// A tool call's result, decoded from the recorded completion output (the
150/// decoding is identical live and on replay, which is what keeps a resumed
151/// orchestration on the recorded path).
152#[derive(Debug, Clone)]
153pub enum ToolCallResult {
154    /// The tool produced this output.
155    Output(Value),
156    /// The tool failed after exhausting its retry policy; the full error is
157    /// recorded in the completion. See [`crate::wire`] for the shape.
158    Failed(ToolFailure),
159    /// The tool asked to park the run. Follow with [`RunCtx::suspend`] and
160    /// [`RunCtx::await_resume`].
161    Suspended(Suspension),
162    /// The tool asked to park the run until an instant. Follow with
163    /// [`RunCtx::sleep_until`] and [`RunCtx::await_wake`].
164    ///
165    /// The call itself is finished when this is returned: its completion is
166    /// recorded and any idempotency claim is settled, so the sleep that
167    /// follows holds nothing. See [`crate::wire`] for why the request travels
168    /// in the completion rather than as an event of its own.
169    Sleeping(Sleep),
170}
171
172/// What [`RunCtx::await_resume`] produced.
173#[derive(Debug, Clone)]
174pub enum Resumption {
175    /// The resume input, recorded or just persisted. Continue the run.
176    Resumed(Value),
177    /// No resume input exists yet. The run is parked durably; the log
178    /// already holds everything, so the process may simply stop driving it.
179    Parked,
180}
181
182/// What [`RunCtx::await_wake`] produced: the timer counterpart of
183/// [`Resumption`], and separate from it because the two park for different
184/// reasons and end differently. A suspension ends when someone supplies an
185/// input; a sleep ends when an instant arrives and carries no input at all.
186#[derive(Debug, Clone, Copy)]
187pub enum Waking {
188    /// The wake is recorded, whether it was replayed from the log or just
189    /// persisted. Continue the run.
190    Woken,
191    /// The wake instant has not arrived. The run is parked durably; the
192    /// recorded `SleepStarted` already holds the deadline, so the process may
193    /// simply stop driving it and come back at `wake_at` or later.
194    ///
195    /// Named for the state the run is in rather than for the non-event that
196    /// left it there, exactly as [`Resumption::Parked`] is.
197    Asleep {
198        /// The recorded instant the run may continue at, so a caller deciding
199        /// when to come back does not have to re-derive the log.
200        wake_at: OffsetDateTime,
201    },
202}
203
204/// The public durability substrate for one run. See the module docs.
205pub struct RunCtx {
206    cursor: ReplayCursor,
207    store: Arc<dyn EventStore>,
208    run_id: RunId,
209    clock: ClockFn,
210    random: RandomFn,
211    resume_input: Option<Value>,
212    /// The wake instant of the sleep this drive last recorded or replayed,
213    /// set by [`sleep_until`](Self::sleep_until) and read by
214    /// [`await_wake`](Self::await_wake) to decide whether the deadline has
215    /// arrived. Not state about the run (the log holds that); state about
216    /// where this drive is, which is why it is not persisted and why a fresh
217    /// context starts without it.
218    sleeping_until: Option<OffsetDateTime>,
219    /// Whether to record the full model request body on each
220    /// `ModelCallRequested`. Off unless [`with_record_prompts`](Self::with_record_prompts)
221    /// turns it on. See that method for the PII rationale.
222    record_prompts: bool,
223    /// Correlation tags to stamp on a genuinely fresh `RunStarted`. Unset
224    /// unless [`with_labels`](Self::with_labels) sets them. See that method.
225    labels: Option<BTreeMap<String, String>>,
226}
227
228impl RunCtx {
229    /// Builds a context over a run's recorded log (empty for a fresh run),
230    /// with the default clock (the real UTC clock) and the default random
231    /// source (operating-system randomness).
232    ///
233    /// # Errors
234    ///
235    /// Returns [`RuntimeError::Replay`] when the log is not a well-formed
236    /// run history.
237    pub fn new(
238        store: Arc<dyn EventStore>,
239        run_id: RunId,
240        log: Vec<EventEnvelope>,
241    ) -> Result<Self, RuntimeError> {
242        Self::with_hooks(
243            store,
244            run_id,
245            log,
246            Arc::new(OffsetDateTime::now_utc),
247            Arc::new(os_random),
248        )
249    }
250
251    /// Builds a context with an injected clock and random source.
252    ///
253    /// The clock stamps every persisted envelope and answers live
254    /// [`now`](Self::now) observations; the random source answers live
255    /// [`random`](Self::random) observations. Injecting deterministic
256    /// functions makes complete event logs comparable across runs, which is
257    /// how the kill/resume tests prove byte-identical recovery.
258    ///
259    /// # Errors
260    ///
261    /// Returns [`RuntimeError::Replay`] when the log is not a well-formed
262    /// run history.
263    pub fn with_hooks(
264        store: Arc<dyn EventStore>,
265        run_id: RunId,
266        log: Vec<EventEnvelope>,
267        clock: ClockFn,
268        random: RandomFn,
269    ) -> Result<Self, RuntimeError> {
270        let cursor = ReplayCursor::new(log)?;
271        Ok(Self {
272            cursor,
273            store,
274            run_id,
275            clock,
276            random,
277            resume_input: None,
278            sleeping_until: None,
279            record_prompts: false,
280            labels: None,
281        })
282    }
283
284    /// Turns on recording of the full model request body into the durable log.
285    ///
286    /// Additive and off by default: the existing [`new`](Self::new) and
287    /// [`with_hooks`](Self::with_hooks) constructors leave it off, so no
288    /// caller that predates this method changes behavior. Chained builder
289    /// style keeps those signatures intact, which is why the flag arrives this
290    /// way rather than as a new constructor argument.
291    ///
292    /// When on, each live [`model_call`](Self::model_call) records the exact
293    /// request it sent on the `ModelCallRequested` event, so the v0.3 dashboard
294    /// inspector can show the prompt. This is PII-sensitive: the body can hold
295    /// user data and secrets, which is why the default is off and turning it on
296    /// is a deliberate per-agent or operator choice. The recorded body lands
297    /// only in the event log; it never reaches the progress stream or any
298    /// console output. It does not affect replay: the request hash is computed
299    /// the same either way, and replay ignores the body.
300    #[must_use]
301    pub fn with_record_prompts(mut self, record_prompts: bool) -> Self {
302        self.record_prompts = record_prompts;
303        self
304    }
305
306    /// Sets the correlation tags to stamp on a genuinely fresh `RunStarted`.
307    ///
308    /// Additive and unset by default: the existing [`new`](Self::new) and
309    /// [`with_hooks`](Self::with_hooks) constructors leave it unset, so no
310    /// caller that predates this method changes behavior. Chained builder
311    /// style, mirroring [`with_record_prompts`](Self::with_record_prompts).
312    ///
313    /// Labels are checked against the sanity bounds (see
314    /// [`crate::validate_labels`]) only on [`begin`](Self::begin)'s live path,
315    /// the moment a `RunStarted` is actually about to be created;
316    /// [`RuntimeError::InvalidLabels`] surfaces there, not here, so this
317    /// setter itself is infallible. A replayed `begin` never re-checks them:
318    /// whatever the log already holds is trusted and returned as recorded.
319    /// Labels never enter `agent_def_hash` or any request hash; they are a
320    /// tag on the run, not part of its identity.
321    #[must_use]
322    pub fn with_labels(mut self, labels: BTreeMap<String, String>) -> Self {
323        self.labels = Some(labels);
324        self
325    }
326
327    /// Provides the input a parked run is being resumed with. The next
328    /// [`await_resume`](Self::await_resume) that reaches live mode records
329    /// it as the `Resumed` event and returns it; without one, a live
330    /// `await_resume` reports [`Resumption::Parked`].
331    pub fn set_resume_input(&mut self, input: Value) {
332        self.resume_input = Some(input);
333    }
334
335    /// The resume input staged by [`set_resume_input`](Self::set_resume_input)
336    /// and not yet consumed, without consuming it.
337    ///
338    /// This is the read-only half of the accept edge. A driver that needs to
339    /// vet a resume input against something only it knows (the graph engine
340    /// checks a gate's declared `approval_schema`) has to see the value BEFORE
341    /// [`await_resume`](Self::await_resume) turns it into a `Resumed` event,
342    /// because after that it is history and refusing it would mean an appended
343    /// event the run has to live with. Peeking here and refusing leaves the log
344    /// untouched and the run parked exactly as it was.
345    ///
346    /// `None` once `await_resume` has taken the value, or when none was staged.
347    #[must_use]
348    pub fn staged_resume_input(&self) -> Option<&Value> {
349        self.resume_input.as_ref()
350    }
351
352    /// The run this context drives.
353    #[must_use]
354    pub fn run_id(&self) -> RunId {
355        self.run_id
356    }
357
358    /// Whether recorded history remains to be consumed.
359    #[must_use]
360    pub fn is_replaying(&self) -> bool {
361        self.cursor.is_replaying()
362    }
363
364    /// The log position the next consumed or emitted event occupies.
365    #[must_use]
366    pub fn next_seq(&self) -> SequenceNumber {
367        self.cursor.next_seq()
368    }
369
370    /// Starts (or replays the start of) the run.
371    ///
372    /// Live: records `RunStarted` with `input` and the labels set through
373    /// [`with_labels`](Self::with_labels) (if any), and returns `input`.
374    /// Replayed: verifies `agent_def_hash` against the recorded event and
375    /// returns the *recorded* input, which always wins; the `input` argument
376    /// is only used when the log is empty, exactly like `labels`.
377    ///
378    /// # Errors
379    ///
380    /// [`RuntimeError::Replay`] on a definition-hash mismatch or any other
381    /// divergence; [`RuntimeError::InvalidLabels`] when the labels set
382    /// through [`with_labels`](Self::with_labels) violate the sanity bounds
383    /// (only checked on the live path; see that method); [`RuntimeError::Store`]
384    /// when persistence fails.
385    pub async fn begin(
386        &mut self,
387        agent_def_hash: &str,
388        input: &Value,
389    ) -> Result<Value, RuntimeError> {
390        match self.cursor.begin(agent_def_hash, self.labels.clone())? {
391            Outcome::Replayed(recorded) => Ok(recorded),
392            Outcome::Live(permit) => {
393                if let Some(labels) = &self.labels {
394                    validate_labels(labels).map_err(RuntimeError::InvalidLabels)?;
395                }
396                let emitted = permit.record(input.clone());
397                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await?;
398                Ok(input.clone())
399            }
400        }
401    }
402
403    /// Starts (or replays the start of) a graph run: the graph-document
404    /// counterpart of [`begin`](Self::begin).
405    ///
406    /// Live: records [`salvor_core::Event::GraphRunStarted`] with `input`, the
407    /// labels set through [`with_labels`](Self::with_labels) (if any), and no
408    /// fork origin, then returns `input`. Replayed: verifies `graph_hash`
409    /// against the recorded head (a changed graph document must not silently
410    /// resume an old run) and returns the *recorded* input, which always wins.
411    ///
412    /// A graph run's log opens with this event rather than `RunStarted` because
413    /// a graph coordinates many agent hashes and has none at its head. The graph
414    /// engine calls this once, then frames each node with
415    /// [`node_entered`](Self::node_entered) / [`node_exited`](Self::node_exited)
416    /// and records the single terminal itself after the last node.
417    ///
418    /// # Errors
419    ///
420    /// [`RuntimeError::Replay`] on a graph-hash mismatch or any other
421    /// divergence; [`RuntimeError::InvalidLabels`] when the labels set through
422    /// [`with_labels`](Self::with_labels) violate the sanity bounds (only
423    /// checked on the live path, exactly as [`begin`](Self::begin) does);
424    /// [`RuntimeError::Store`] when persistence fails.
425    pub async fn begin_graph(
426        &mut self,
427        graph_hash: &str,
428        input: &Value,
429    ) -> Result<Value, RuntimeError> {
430        match self
431            .cursor
432            .begin_graph(graph_hash, self.labels.clone(), None)?
433        {
434            Outcome::Replayed(recorded) => Ok(recorded),
435            Outcome::Live(permit) => {
436                if let Some(labels) = &self.labels {
437                    validate_labels(labels).map_err(RuntimeError::InvalidLabels)?;
438                }
439                let emitted = permit.record(input.clone());
440                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await?;
441                Ok(input.clone())
442            }
443        }
444    }
445
446    /// Records (or replays) entry into a graph node. A graph node's own events
447    /// (an agent loop's model calls, a tool call) are recorded between this and
448    /// the matching [`node_exited`](Self::node_exited).
449    ///
450    /// # Errors
451    ///
452    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
453    /// persistence fails.
454    pub async fn node_entered(&mut self, node: &str) -> Result<(), RuntimeError> {
455        match self.cursor.node_entered(node)? {
456            Outcome::Replayed(()) => Ok(()),
457            Outcome::Live(emitted) => {
458                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
459            }
460        }
461    }
462
463    /// Records (or replays) exit from a graph node, having produced its output.
464    /// The counterpart of [`node_entered`](Self::node_entered).
465    ///
466    /// # Errors
467    ///
468    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
469    /// persistence fails.
470    pub async fn node_exited(&mut self, node: &str) -> Result<(), RuntimeError> {
471        match self.cursor.node_exited(node)? {
472            Outcome::Replayed(()) => Ok(()),
473            Outcome::Live(emitted) => {
474                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
475            }
476        }
477    }
478
479    /// Records (or replays) that a graph node was skipped: reached on the walk
480    /// but deliberately not run (a branch routed past it). Unlike an executed
481    /// node there is no [`node_entered`](Self::node_entered)/[`node_exited`](Self::node_exited)
482    /// pair; the skip is the node's sole marker, which is what lets a projection
483    /// tell "skipped" apart from "never reached". `reason` must be a pure
484    /// function of the document and recorded values so it reproduces on replay.
485    ///
486    /// # Errors
487    ///
488    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
489    /// persistence fails.
490    pub async fn node_skipped(&mut self, node: &str, reason: &str) -> Result<(), RuntimeError> {
491        match self.cursor.node_skipped(node, reason)? {
492            Outcome::Replayed(()) => Ok(()),
493            Outcome::Live(emitted) => {
494                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
495            }
496        }
497    }
498
499    /// Records (or replays) that a branch node routed: the named `case` fired.
500    /// Recorded between the branch's [`node_entered`](Self::node_entered) and
501    /// [`node_exited`](Self::node_exited), it is the sole authority for which way
502    /// the branch went. The chosen `case` must be a deterministic function of
503    /// recorded values (a pure expression over the routed value, or a decision
504    /// recomputed from a replayed model reply) so replay reproduces the route.
505    ///
506    /// # Errors
507    ///
508    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
509    /// persistence fails.
510    pub async fn branch_taken(&mut self, node: &str, case: &str) -> Result<(), RuntimeError> {
511        match self.cursor.branch_taken(node, case)? {
512            Outcome::Replayed(()) => Ok(()),
513            Outcome::Live(emitted) => {
514                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
515            }
516        }
517    }
518
519    /// Records (or replays) that a map node fanned out over a resolved item list.
520    ///
521    /// Recorded between the map node's [`node_entered`](Self::node_entered) and its
522    /// per-iteration markers. The `items` must be a deterministic function of
523    /// recorded values (the map's `over` reference resolved against the recorded
524    /// routed value), so replay reproduces the identical fan-out, which is what
525    /// makes the derived per-iteration child ids reproducible.
526    ///
527    /// # Errors
528    ///
529    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
530    /// persistence fails.
531    pub async fn map_fanned_out(&mut self, node: &str, items: &Value) -> Result<(), RuntimeError> {
532        match self.cursor.map_fanned_out(node, items)? {
533            Outcome::Replayed(()) => Ok(()),
534            Outcome::Live(emitted) => {
535                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
536            }
537        }
538    }
539
540    /// Records (or replays) that one iteration of a map fan-out started, as a child
541    /// run with the derived id `child_run`. The `child_run` is derived from the
542    /// parent run id, the node id, and the index. On replay the RECORDED id wins
543    /// and the match is on `node` + `index` alone, so a fork (which replays the
544    /// origin's prefix under a new run id and thus re-derives a different id)
545    /// still replays its inherited map markers cleanly.
546    ///
547    /// # Errors
548    ///
549    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
550    /// persistence fails.
551    pub async fn map_iteration_started(
552        &mut self,
553        node: &str,
554        index: u64,
555        child_run: &str,
556    ) -> Result<(), RuntimeError> {
557        match self.cursor.map_iteration_started(node, index, child_run)? {
558            Outcome::Replayed(()) => Ok(()),
559            Outcome::Live(emitted) => {
560                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
561            }
562        }
563    }
564
565    /// Records (or replays) that one iteration of a map fan-out joined back into
566    /// the map node's output. Joins must be recorded in index order, never
567    /// completion order, so the concurrency of the fan-out never influences the
568    /// parent log's byte sequence.
569    ///
570    /// # Errors
571    ///
572    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
573    /// persistence fails.
574    pub async fn map_iteration_joined(
575        &mut self,
576        node: &str,
577        index: u64,
578    ) -> Result<(), RuntimeError> {
579        match self.cursor.map_iteration_joined(node, index)? {
580            Outcome::Replayed(()) => Ok(()),
581            Outcome::Live(emitted) => {
582                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
583            }
584        }
585    }
586
587    /// Records (or replays) that a fold node began one bounded pass of its
588    /// accumulate-and-refine loop. A fold's passes run inline in this log rather
589    /// than as child runs, so `index` is both the pass position and its recorded
590    /// order, and replay matches it exactly: a replayed pass returns without
591    /// re-recording anything.
592    ///
593    /// # Errors
594    ///
595    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
596    /// persistence fails.
597    pub async fn fold_iteration_started(
598        &mut self,
599        node: &str,
600        index: u64,
601    ) -> Result<(), RuntimeError> {
602        match self.cursor.fold_iteration_started(node, index)? {
603            Outcome::Replayed(()) => Ok(()),
604            Outcome::Live(emitted) => {
605                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
606            }
607        }
608    }
609
610    /// Records (or replays) that one fold pass joined back into the fold node's
611    /// accumulated value. Recorded in index order, which for a fold is already
612    /// completion order because its passes are sequential. A replayed join
613    /// returns without re-recording anything.
614    ///
615    /// # Errors
616    ///
617    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
618    /// persistence fails.
619    pub async fn fold_iteration_joined(
620        &mut self,
621        node: &str,
622        index: u64,
623    ) -> Result<(), RuntimeError> {
624        match self.cursor.fold_iteration_joined(node, index)? {
625            Outcome::Replayed(()) => Ok(()),
626            Outcome::Live(emitted) => {
627                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
628            }
629        }
630    }
631
632    /// Records (or replays) that a fold node settled: its loop stopped and its
633    /// `join` rule selected the pass at `winner_index`, for the recorded
634    /// `reason`. This is the sole authority for which pass the fold's output
635    /// came from, as [`branch_taken`](Self::branch_taken) is for a branch's
636    /// route. Both the winner and the reason must be deterministic functions of
637    /// the recorded pass values, because replay matches all three fields and a
638    /// replayed convergence returns without re-recording anything.
639    ///
640    /// # Errors
641    ///
642    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
643    /// persistence fails.
644    pub async fn fold_converged(
645        &mut self,
646        node: &str,
647        winner_index: u64,
648        reason: &str,
649    ) -> Result<(), RuntimeError> {
650        match self.cursor.fold_converged(node, winner_index, reason)? {
651            Outcome::Replayed(()) => Ok(()),
652            Outcome::Live(emitted) => {
653                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
654            }
655        }
656    }
657
658    /// The recorded clock: reads the injected clock once, live, and replays
659    /// the identical instant forever after.
660    ///
661    /// # Errors
662    ///
663    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
664    /// persistence fails.
665    pub async fn now(&mut self) -> Result<OffsetDateTime, RuntimeError> {
666        match self.cursor.now()? {
667            Outcome::Replayed(instant) => Ok(instant),
668            Outcome::Live(permit) => {
669                let instant = (self.clock)();
670                let emitted = permit.record(instant);
671                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await?;
672                Ok(instant)
673            }
674        }
675    }
676
677    /// The recorded random source: draws 64 bits from the injected source
678    /// once, live, and replays the identical bits forever after. Richer
679    /// random values must be derived from these bits deterministically.
680    ///
681    /// # Errors
682    ///
683    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
684    /// persistence fails.
685    pub async fn random(&mut self) -> Result<u64, RuntimeError> {
686        match self.cursor.random()? {
687            Outcome::Replayed(bits) => Ok(bits),
688            Outcome::Live(permit) => {
689                let bits = (self.random)();
690                let emitted = permit.record(bits);
691                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await?;
692                Ok(bits)
693            }
694        }
695    }
696
697    /// A recorded model call.
698    ///
699    /// The request is identified by its content hash
700    /// (`sha256:` over the canonical serialization; see [`crate::hash`]).
701    /// Replayed: the recorded response is decoded and returned; the provider
702    /// is never contacted. Live: the intent event is persisted, the provider
703    /// is called through `client`, and the completion (response plus usage)
704    /// is persisted. A recorded intent with no completion (a call the
705    /// process died inside) is re-issued safely: the fresh completion
706    /// correlates to the recorded intent.
707    ///
708    /// When [`with_record_prompts`](Self::with_record_prompts) is on, the exact
709    /// request body is recorded alongside the hash on the fresh live intent.
710    /// It is the same value the hash was computed over, it never feeds into the
711    /// hash, and replay ignores it, so recording it changes nothing about how
712    /// the run replays.
713    ///
714    /// # Errors
715    ///
716    /// [`RuntimeError::Replay`] on divergence, [`RuntimeError::Store`] when
717    /// persistence fails, [`RuntimeError::Model`] when the live provider
718    /// call fails (the log stays intact and the run is recoverable),
719    /// [`RuntimeError::RequestEncode`] / [`RuntimeError::RecordedResponseDecode`]
720    /// on the JSON edges.
721    pub async fn model_call(
722        &mut self,
723        client: &Client,
724        request: &MessageRequest,
725    ) -> Result<ModelTurn, RuntimeError> {
726        let request_value = serde_json::to_value(request).map_err(RuntimeError::RequestEncode)?;
727        let request_hash = hash_value(&request_value);
728        // The hash is computed above from `request_value` and is unaffected by
729        // what follows. When prompt recording is on, the body handed to the
730        // cursor is that same `request_value`, so the recorded body is exactly
731        // what was hashed; when off it is `None` and nothing is recorded.
732        let request_body = if self.record_prompts {
733            Some(request_value)
734        } else {
735            None
736        };
737        match self.cursor.model_call(&request_hash, request_body)? {
738            Outcome::Replayed(ModelReply { response, usage }) => {
739                let response = serde_json::from_value(response)
740                    .map_err(RuntimeError::RecordedResponseDecode)?;
741                Ok(ModelTurn { response, usage })
742            }
743            Outcome::Live(permit) => {
744                if let Some(intent) = permit.intent().cloned() {
745                    persist(self.store.as_ref(), self.run_id, &self.clock, &intent).await?;
746                }
747                let response = client.send_message(request).await?;
748                let usage = usage_of(&response);
749                let completion = permit.record(response_value(&response), usage);
750                persist(self.store.as_ref(), self.run_id, &self.clock, &completion).await?;
751                Ok(ModelTurn { response, usage })
752            }
753        }
754    }
755
756    /// A recorded model call that streams live events to `on_event` while it
757    /// runs, recording the identical completion [`model_call`](Self::model_call)
758    /// would record.
759    ///
760    /// This is a live-progress affordance layered on top of the durable record,
761    /// not a different kind of call. The recorded log is byte-for-byte what
762    /// [`model_call`](Self::model_call) writes for the same underlying response:
763    /// the request is hashed the same way (see [`crate::hash`]), the intent is
764    /// the same `ModelCallRequested`, and the completion carries the same
765    /// `response` value and `usage`. A run does not care which path recorded it,
766    /// and replay is deterministic either way.
767    ///
768    /// Replayed: the recorded response is decoded and returned, exactly as
769    /// [`model_call`](Self::model_call) does. The provider is never contacted and
770    /// `on_event` never fires, because there are no live tokens to report; the
771    /// caller gets the final result at once.
772    ///
773    /// Live: the intent event is persisted first (write-ahead, the same ordering
774    /// [`model_call`](Self::model_call) uses), then the provider stream is opened
775    /// through `client`. Each [`StreamEvent`] is handed to `on_event` for a live
776    /// ticker (text deltas ride [`StreamEvent::ContentBlockDelta`], token counts
777    /// ride [`StreamEvent::MessageDelta`]) and, in the same pass, applied to a
778    /// [`MessageAccumulator`]. When the stream ends, the assembled
779    /// [`MessageResponse`] is converted with the same `response_value` and usage
780    /// logic [`model_call`](Self::model_call) uses, the completion is persisted,
781    /// and the [`ModelTurn`] is returned.
782    ///
783    /// All persistence lives inside this method, so a caller cannot record a
784    /// partial or wrong completion: the completion is written only after the
785    /// stream is fully assembled. A caller that drops the returned future before
786    /// the stream completes leaves a dangling model intent (the write-ahead
787    /// intent with no completion), exactly like a live [`model_call`](Self::model_call)
788    /// the process died inside. That intent is re-issued safely on resume: the
789    /// fresh completion correlates to the recorded intent. `on_event` firing is
790    /// not part of the durable record, so a ticker that saw partial tokens before
791    /// the drop has no effect on what replay produces.
792    ///
793    /// When [`with_record_prompts`](Self::with_record_prompts) is on, the exact
794    /// request body is recorded on the fresh live intent, identically to
795    /// [`model_call`](Self::model_call).
796    ///
797    /// # Errors
798    ///
799    /// [`RuntimeError::Replay`] on divergence, [`RuntimeError::Store`] when
800    /// persistence fails, [`RuntimeError::Model`] when the live stream fails
801    /// (opening it, an error event or transport fault mid-stream, or a
802    /// tool-call fragment that does not parse) surfaced as the same error type
803    /// [`model_call`](Self::model_call) returns, with the log left intact and the
804    /// run recoverable, and [`RuntimeError::RequestEncode`] /
805    /// [`RuntimeError::RecordedResponseDecode`] on the JSON edges.
806    pub async fn model_call_streaming(
807        &mut self,
808        client: &Client,
809        request: &MessageRequest,
810        mut on_event: impl FnMut(&StreamEvent),
811    ) -> Result<ModelTurn, RuntimeError> {
812        let request_value = serde_json::to_value(request).map_err(RuntimeError::RequestEncode)?;
813        let request_hash = hash_value(&request_value);
814        // Hashing and body recording are identical to `model_call`: the hash is
815        // computed from `request_value` above, and the body handed to the cursor
816        // is that same value when recording is on, `None` when off. Streaming
817        // changes nothing here, which is half of why the recorded intent matches.
818        let request_body = if self.record_prompts {
819            Some(request_value)
820        } else {
821            None
822        };
823        match self.cursor.model_call(&request_hash, request_body)? {
824            Outcome::Replayed(ModelReply { response, usage }) => {
825                // No live call, so `on_event` never fires: replay has no tokens.
826                let response = serde_json::from_value(response)
827                    .map_err(RuntimeError::RecordedResponseDecode)?;
828                Ok(ModelTurn { response, usage })
829            }
830            Outcome::Live(permit) => {
831                if let Some(intent) = permit.intent().cloned() {
832                    persist(self.store.as_ref(), self.run_id, &self.clock, &intent).await?;
833                }
834                // Pump the stream once: every event feeds the ticker and the
835                // accumulator in the same pass. The accumulator assembles the
836                // exact `MessageResponse` `send_message` would have returned
837                // (salvor-llm guarantees this), so the recorded completion below
838                // is byte-identical to the non-streaming path.
839                let mut stream = client.stream_message(request).await?;
840                let mut accumulator = MessageAccumulator::new();
841                while let Some(event) = stream.next_event().await {
842                    let event = event?;
843                    on_event(&event);
844                    accumulator.apply(&event)?;
845                }
846                let response = accumulator.into_message()?;
847                let usage = usage_of(&response);
848                let completion = permit.record(response_value(&response), usage);
849                persist(self.store.as_ref(), self.run_id, &self.clock, &completion).await?;
850                Ok(ModelTurn { response, usage })
851            }
852        }
853    }
854
855    /// A recorded tool call: one intent/completion pair, whatever happens in
856    /// between.
857    ///
858    /// Replayed: the recorded completion output is decoded (an output, a
859    /// failure object, or a suspension sentinel; see [`crate::wire`]) and
860    /// the tool is never executed. Live: the intent is persisted **before**
861    /// the tool executes (write-ahead), the tool runs with retries per its
862    /// effect's `RetryPolicy` (see [`MAX_TOOL_ATTEMPTS`]), and the
863    /// completion is persisted. A recorded `Read`/`Idempotent` intent with
864    /// no completion re-executes here under its recorded idempotency key; a
865    /// dangling `Write` intent fails with
866    /// `ReplayError::NeedsReconciliation` before anything runs.
867    ///
868    /// `idempotency_key` is the key for a *fresh* call; the built-in loop
869    /// derives it from [`random`](Self::random) for `Idempotent` tools so it
870    /// reproduces on replay. A key the tool declares for itself
871    /// ([`DynTool::idempotency_key`]) takes precedence over the one passed
872    /// here, because only the tool can say what effect a call *is*. For a
873    /// re-executed recorded intent the recorded key wins, and whatever is
874    /// presented must match it (the cursor checks).
875    ///
876    /// # Cross-run deduplication
877    ///
878    /// Within one run, nothing happens twice because a recorded completion is
879    /// replayed rather than re-executed. Across independent runs there is no
880    /// log to replay, so something else has to hold the line, and that
881    /// something is the idempotency key.
882    ///
883    /// ## Which keys count
884    ///
885    /// Only a key the tool **declares** for itself, through
886    /// [`DynTool::idempotency_key`], is an identity to deduplicate on. A key
887    /// the runtime derives on a tool's behalf is not, and the difference is not
888    /// a technicality.
889    ///
890    /// A hand-written tool makes that declaration in Rust. An MCP or wasm tool
891    /// has no code here to make it in, so its operator does, by naming the
892    /// input field that identifies a call in the agent file
893    /// (`idempotency_keys`); the tool derives the key from that field on every
894    /// call and answers through the same trait method. Nothing below this
895    /// distinguishes the two, because there is no distinction to make: both are
896    /// a statement about what the call does in the world, from someone in a
897    /// position to know.
898    ///
899    /// A declared key is a statement about the world: `"pay_claim:wreck-9931"`
900    /// means *this is the payout for claim 9931*, and two calls carrying it are
901    /// the same payment no matter which run asked for them. A derived key says
902    /// something much weaker. The built-in loop draws one from recorded
903    /// randomness so a retry within a run reuses it; the graph engine derives
904    /// one from a node's position so a fork re-executing a node reuses it.
905    /// Both are *attempt* identifiers, scoped to one run or one lineage, and
906    /// two unrelated runs can hold the same derived key over completely
907    /// different arguments. Treating one as an effect identity would let a
908    /// second run collect the first run's output for a call it never made,
909    /// which is a worse failure than the duplicate execution this is meant to
910    /// stop.
911    ///
912    /// So a derived key keeps doing exactly what it always did, at the provider
913    /// and inside its own run, and is recorded exactly as before. Cross-run
914    /// deduplication waits for a tool to say what its calls *are*.
915    ///
916    /// A declared key is also what a call records, in preference to a derived
917    /// one, since only the tool can name its own effect.
918    ///
919    /// ## The mechanism
920    ///
921    /// **The decision is made here, live, before the intent is recorded and
922    /// before the tool runs.** For a [`Effect::Write`] or
923    /// [`Effect::Idempotent`] call carrying a declared key, this method claims
924    /// the identity `(tool name, idempotency key)` in the store
925    /// ([`EventStore::claim_call`](salvor_store::EventStore::claim_call)),
926    /// which is the arbiter:
927    ///
928    /// - **Claimed.** This run is the one execution. The intent is recorded,
929    ///   the tool runs, and the completion is appended *and* settles the
930    ///   commitment as one atomic step, so no crash can leave a committed
931    ///   completion the store still calls unfinished.
932    /// - **Held, and settled.** An equal call is already committed. The origin
933    ///   run's log is read back (through
934    ///   [`read_log`](salvor_store::EventStore::read_log), so its hash chain is
935    ///   verified before a single byte is copied), its recorded input is
936    ///   checked against this call's, and its output becomes this call's
937    ///   output. The intent is still recorded, because an intent that resolves
938    ///   as a duplicate is an honest thing to have recorded, and the completion
939    ///   carries a [`DedupOrigin`] naming what it copied. **The tool does not
940    ///   run.**
941    /// - **Held, and unfinished.** Refused with
942    ///   [`RuntimeError::CallInFlight`], before anything is recorded. See that
943    ///   variant for why refusing beats guessing.
944    ///
945    /// A call with no declared key is untouched by any of this: there is no
946    /// identity to deduplicate on, so a keyless write behaves exactly as it
947    /// always has, and so does a write carrying only a derived key. So does
948    /// every [`Effect::Read`], which has no effect worth naming.
949    ///
950    /// **Replay never participates.** A recorded completion replays from the
951    /// log, whether it was witnessed or copied, with no store lookup of any
952    /// kind; the [`DedupOrigin`] on it is read by humans and audits, never by
953    /// the cursor. That is what keeps a recorded log a self-contained
954    /// description of a run.
955    ///
956    /// The one place resume consults the store is the gap a crash can leave
957    /// between a deduplicated intent and its copied completion. That intent
958    /// executed nothing (this run never held the identity, so it never held the
959    /// right to execute), and the store can prove it, so the call is finished
960    /// as the duplicate it was rather than parked for a human. Every other
961    /// dangling write still parks: see [`recover_deduplicated_intent`](Self::recover_deduplicated_intent).
962    ///
963    /// # Errors
964    ///
965    /// [`RuntimeError::Replay`] on divergence or a dangling write intent;
966    /// [`RuntimeError::Store`] when persistence fails;
967    /// [`RuntimeError::CallInFlight`] when another run holds this call's
968    /// identity and has not finished with it;
969    /// [`RuntimeError::IdempotencyKeyCollision`] when one key names two
970    /// different calls; [`RuntimeError::CommitmentUnreadable`] when the store
971    /// points at a completion its own log does not hold. A failing *tool* is
972    /// not an `Err`: it returns [`ToolCallResult::Failed`], because the
973    /// failure is a recorded outcome the orchestration must handle
974    /// deterministically.
975    pub async fn tool_call(
976        &mut self,
977        tool: &dyn DynTool,
978        input: &Value,
979        idempotency_key: Option<&str>,
980    ) -> Result<ToolCallResult, RuntimeError> {
981        let effect = tool.effect();
982        let declared = tool.idempotency_key(input);
983        // What is recorded on the wire: the tool's own declaration when it
984        // makes one, otherwise the attempt key the caller derived.
985        let recorded = declared
986            .clone()
987            .or_else(|| idempotency_key.map(ToOwned::to_owned));
988        let key = recorded.as_deref();
989        // What deduplication is arbitrated on: a declared key only. See the
990        // method docs for why an attempt key is not an identity.
991        let identity = declared.as_deref().filter(|_| deduplicates(effect));
992
993        // Before the cursor is asked to take a step, because `tool_call` either
994        // advances it or fails, with nothing in between where a store lookup
995        // could go.
996        if let Some(resolved) = self
997            .recover_deduplicated_intent(tool, input, effect, identity)
998            .await?
999        {
1000            return Ok(resolved);
1001        }
1002
1003        match self.cursor.tool_call(tool.name(), input, effect, key)? {
1004            Outcome::Replayed(output) => Ok(decode_tool_output(output)),
1005            Outcome::Live(permit) => {
1006                // THE DECISION POINT. Live, before the write-ahead intent is
1007                // persisted and before the tool is touched. Nothing below this
1008                // block consults the store about other runs, and replay never
1009                // reaches it at all.
1010                let claimant = identity.map(|key| CallClaimant {
1011                    tool: tool.name(),
1012                    idempotency_key: key,
1013                    run_id: self.run_id,
1014                    intent_seq: permit.seq(),
1015                });
1016                let mut copied = None;
1017                if let Some(claimant) = claimant {
1018                    match self.store.claim_call(claimant).await? {
1019                        // This run is the one execution.
1020                        CallClaim::Claimed => {}
1021                        CallClaim::Held(commitment) if commitment.completion_seq.is_some() => {
1022                            copied = Some(
1023                                committed_call(
1024                                    self.store.as_ref(),
1025                                    tool.name(),
1026                                    claimant.idempotency_key,
1027                                    commitment,
1028                                    input,
1029                                )
1030                                .await?,
1031                            );
1032                        }
1033                        // Held by a run that has not finished. Nothing is
1034                        // recorded, so this run can simply be run again once
1035                        // the holder is resolved.
1036                        CallClaim::Held(commitment) => {
1037                            return Err(RuntimeError::CallInFlight {
1038                                tool: tool.name().to_owned(),
1039                                idempotency_key: claimant.idempotency_key.to_owned(),
1040                                holder: commitment.run_id,
1041                                holder_seq: commitment.intent_seq.get(),
1042                            });
1043                        }
1044                    }
1045                }
1046
1047                // Write-ahead, on both paths. An intent that resolves as a
1048                // duplicate is still an honest record of what this run asked
1049                // for.
1050                if let Some(intent) = permit.intent().cloned() {
1051                    persist(self.store.as_ref(), self.run_id, &self.clock, &intent).await?;
1052                }
1053
1054                if let Some((output, origin)) = copied {
1055                    let completion = permit.record_deduplicated(output.clone(), origin);
1056                    persist(self.store.as_ref(), self.run_id, &self.clock, &completion).await?;
1057                    return Ok(decode_tool_output(output));
1058                }
1059
1060                let key = permit.idempotency_key().map(ToOwned::to_owned);
1061                let tool_ctx = ToolCtx::new(key);
1062                let policy = RetryPolicy::for_effect(effect);
1063                let mut attempts: u32 = 0;
1064                let outcome = loop {
1065                    attempts += 1;
1066                    match tool.call_json(&tool_ctx, input.clone()).await {
1067                        Ok(outcome) => break Ok(outcome),
1068                        Err(error) => {
1069                            // Only a handler failure is retryable, and only
1070                            // when the effect's policy allows a re-attempt.
1071                            // Every other variant is a fault in the arguments
1072                            // or in the tool itself, and a second attempt
1073                            // would reach the same verdict against a slower
1074                            // clock.
1075                            let may_retry = matches!(error, ToolError::Handler { .. })
1076                                && policy.allows_retry()
1077                                && attempts < MAX_TOOL_ATTEMPTS;
1078                            if may_retry {
1079                                continue;
1080                            }
1081                            break Err(error);
1082                        }
1083                    }
1084                };
1085                let (output, result) = match outcome {
1086                    Ok(ToolOutcome::Output(value)) => {
1087                        (value.clone(), ToolCallResult::Output(value))
1088                    }
1089                    Ok(ToolOutcome::Suspend(suspension)) => (
1090                        encode_suspension(&suspension),
1091                        ToolCallResult::Suspended(suspension),
1092                    ),
1093                    Ok(ToolOutcome::Sleep(sleep)) => {
1094                        // The instant is normalized on the way into the
1095                        // completion, so what the caller sleeps on is what the
1096                        // log holds and what every later drive decodes.
1097                        let output = encode_sleep(&sleep);
1098                        let recorded = decode_sleep(&output).unwrap_or(sleep);
1099                        (output, ToolCallResult::Sleeping(recorded))
1100                    }
1101                    Err(error) => {
1102                        let failure = ToolFailure::from_error(&error, attempts);
1103                        (encode_failure(&failure), ToolCallResult::Failed(failure))
1104                    }
1105                };
1106                let completion = permit.record(output);
1107                match claimant {
1108                    // The completion and the settlement land together, so the
1109                    // store never believes an identity is still in flight when
1110                    // its result is already recorded.
1111                    Some(claimant) => {
1112                        persist_settling(
1113                            self.store.as_ref(),
1114                            self.run_id,
1115                            &self.clock,
1116                            &completion,
1117                            claimant,
1118                        )
1119                        .await?;
1120                    }
1121                    None => {
1122                        persist(self.store.as_ref(), self.run_id, &self.clock, &completion).await?;
1123                    }
1124                }
1125                Ok(result)
1126            }
1127        }
1128    }
1129
1130    /// Finishes a deduplicated call whose process died between recording the
1131    /// intent and recording the copied completion, or reports that this is not
1132    /// that situation.
1133    ///
1134    /// This is the only place a resume consults the store about another run,
1135    /// and it turns on a fact the store can settle: a call executes only under
1136    /// a claim, so an identity held by a **different** run is proof that this
1137    /// run never executed. The intent it left behind is then not a dangling
1138    /// write at all, it is an unfinished copy, and parking it would ask a human
1139    /// to reconcile an effect that provably never happened.
1140    ///
1141    /// Every other reading falls through to [`tool_call`](Self::tool_call)'s
1142    /// normal path and its normal consequences. In particular an identity held
1143    /// by *this* run is exactly the reconciliation hazard it has always been:
1144    /// this run did hold the right to execute, so nobody can say from the
1145    /// outside whether it did, and the run parks with
1146    /// [`ReplayError::NeedsReconciliation`](salvor_core::ReplayError::NeedsReconciliation).
1147    ///
1148    /// Returns `Ok(None)` when the situation does not apply, which is the
1149    /// overwhelmingly common case.
1150    async fn recover_deduplicated_intent(
1151        &mut self,
1152        tool: &dyn DynTool,
1153        input: &Value,
1154        effect: Effect,
1155        key: Option<&str>,
1156    ) -> Result<Option<ToolCallResult>, RuntimeError> {
1157        let Some(key) = key.filter(|_| deduplicates(effect)) else {
1158            return Ok(None);
1159        };
1160        let Some(PendingCall::Tool {
1161            tool: recorded_tool,
1162            input: recorded_input,
1163            effect: recorded_effect,
1164            idempotency_key: Some(recorded_key),
1165            ..
1166        }) = self.cursor.dangling_intent()
1167        else {
1168            return Ok(None);
1169        };
1170        // Only the call the orchestration is asking for right now. Anything
1171        // else is a divergence for the cursor to report, not ours to smooth
1172        // over.
1173        if recorded_tool != tool.name()
1174            || recorded_input != *input
1175            || recorded_effect != effect
1176            || recorded_key != key
1177        {
1178            return Ok(None);
1179        }
1180
1181        let Some(commitment) = self.store.lookup_call(tool.name(), key).await? else {
1182            return Ok(None);
1183        };
1184        // The proof, and the whole reason this is safe: the identity belongs to
1185        // some other run, and that run finished. This run never held it, so it
1186        // never had the right to execute, so it did not.
1187        if commitment.run_id == self.run_id || commitment.completion_seq.is_none() {
1188            return Ok(None);
1189        }
1190
1191        let (output, origin) =
1192            committed_call(self.store.as_ref(), tool.name(), key, commitment, input).await?;
1193        let permit = self
1194            .cursor
1195            .resume_unexecuted_tool_call(tool.name(), input, effect, key)?;
1196        let completion = permit.record_deduplicated(output.clone(), origin);
1197        persist(self.store.as_ref(), self.run_id, &self.clock, &completion).await?;
1198        Ok(Some(decode_tool_output(output)))
1199    }
1200
1201    /// Parks the run on a human gate: records `Suspended { reason,
1202    /// input_schema }`, with no discriminator, which is what every suspension
1203    /// recorded before signals existed means. Follow with
1204    /// [`await_resume`](Self::await_resume).
1205    ///
1206    /// # Errors
1207    ///
1208    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
1209    /// persistence fails.
1210    pub async fn suspend(
1211        &mut self,
1212        reason: &str,
1213        input_schema: &Value,
1214    ) -> Result<(), RuntimeError> {
1215        self.suspend_with_kind(reason, input_schema, None).await
1216    }
1217
1218    /// Parks the run on an external signal: records `Suspended` with
1219    /// [`SuspensionKind::Signal`](salvor_core::SuspensionKind::Signal), for a
1220    /// wait a webhook or callback answers rather than a person. Follow with
1221    /// [`await_resume`](Self::await_resume), exactly as a gate does.
1222    ///
1223    /// The run parks, validates, and resumes identically either way. The
1224    /// recorded discriminator exists so a surface can route: a signal wait is
1225    /// nobody's task, and listing it in an approval inbox invents work for an
1226    /// operator who cannot do it.
1227    ///
1228    /// # Errors
1229    ///
1230    /// [`RuntimeError::Replay`] on divergence (a replayed suspension whose
1231    /// discriminator differs included); [`RuntimeError::Store`] when
1232    /// persistence fails.
1233    pub async fn suspend_for_signal(
1234        &mut self,
1235        reason: &str,
1236        input_schema: &Value,
1237    ) -> Result<(), RuntimeError> {
1238        self.suspend_with_kind(reason, input_schema, Some(SuspensionKind::Signal))
1239            .await
1240    }
1241
1242    /// Parks the run on a suspension whose discriminator is already a value:
1243    /// records `Suspended { reason, input_schema, kind }`. Follow with
1244    /// [`await_resume`](Self::await_resume).
1245    ///
1246    /// This exists for the drivers, which read the kind off a
1247    /// [`Suspension`](salvor_tools::Suspension) a tool returned and cannot
1248    /// choose between the two named methods without matching on it. Hand-written
1249    /// orchestration should say [`suspend`](Self::suspend) or
1250    /// [`suspend_for_signal`](Self::suspend_for_signal) instead, so the call
1251    /// site reads as what it is.
1252    ///
1253    /// # Errors
1254    ///
1255    /// [`RuntimeError::Replay`] on divergence (a replayed suspension whose
1256    /// discriminator differs included); [`RuntimeError::Store`] when
1257    /// persistence fails.
1258    pub async fn suspend_with_kind(
1259        &mut self,
1260        reason: &str,
1261        input_schema: &Value,
1262        kind: Option<SuspensionKind>,
1263    ) -> Result<(), RuntimeError> {
1264        let requested = match kind {
1265            None => self.cursor.suspend(reason, input_schema)?,
1266            Some(SuspensionKind::Signal) => self.cursor.suspend_for_signal(reason, input_schema)?,
1267        };
1268        match requested {
1269            Outcome::Replayed(()) => Ok(()),
1270            Outcome::Live(emitted) => {
1271                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
1272            }
1273        }
1274    }
1275
1276    /// Obtains the input a parked run was resumed with.
1277    ///
1278    /// Replayed: the recorded `Resumed` input. Live: when a resume input was
1279    /// provided through [`set_resume_input`](Self::set_resume_input), it is
1280    /// recorded and returned; otherwise the run stays parked and
1281    /// [`Resumption::Parked`] tells the caller to stop driving.
1282    ///
1283    /// # Errors
1284    ///
1285    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
1286    /// persistence fails.
1287    pub async fn await_resume(&mut self) -> Result<Resumption, RuntimeError> {
1288        match self.cursor.await_resume()? {
1289            Outcome::Replayed(input) => Ok(Resumption::Resumed(input)),
1290            Outcome::Live(parked) => match self.resume_input.take() {
1291                Some(input) => {
1292                    let emitted = parked.resume(input.clone());
1293                    persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await?;
1294                    Ok(Resumption::Resumed(input))
1295                }
1296                None => Ok(Resumption::Parked),
1297            },
1298        }
1299    }
1300
1301    /// Parks the run on a durable timer: records `SleepStarted { wake_at }`.
1302    /// Follow with [`await_wake`](Self::await_wake).
1303    ///
1304    /// `wake_at` must be derived from recorded data, because replay presents
1305    /// it again and the cursor matches it exactly: derive it from an observed
1306    /// [`now`](Self::now) (which [`sleep_for`](Self::sleep_for) does for you),
1307    /// never from a clock read outside the log. An instant recomputed from an
1308    /// ambient clock differs on every drive and diverges on the first one.
1309    ///
1310    /// # Never inside a claimed tool call
1311    ///
1312    /// A sleep must not be recorded between a claimed call's intent and its
1313    /// completion. The claim is held for the whole span, so every other run
1314    /// under that idempotency key gets `CallInFlight` for as long as the run
1315    /// sleeps, which for a durable timer is hours or weeks rather than the
1316    /// seconds a call takes. Worse, a process death mid-sleep strands a
1317    /// dangling `Write` intent, which derives to
1318    /// [`RunStatus::NeedsReconciliation`](salvor_core::RunStatus::NeedsReconciliation)
1319    /// and needs a human before the run moves again. Sleeping belongs between
1320    /// completed calls. Nothing here can enforce that (the caller owns the
1321    /// ordering, and this context sees one request at a time), so this
1322    /// paragraph is the guardrail.
1323    ///
1324    /// # Errors
1325    ///
1326    /// [`RuntimeError::Replay`] on divergence, including a `wake_at` that
1327    /// differs from the recorded one; [`RuntimeError::Store`] when
1328    /// persistence fails.
1329    pub async fn sleep_until(&mut self, wake_at: OffsetDateTime) -> Result<(), RuntimeError> {
1330        match self.cursor.sleep_started(wake_at)? {
1331            Outcome::Replayed(()) => {}
1332            Outcome::Live(emitted) => {
1333                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await?;
1334            }
1335        }
1336        self.sleeping_until = Some(wake_at);
1337        Ok(())
1338    }
1339
1340    /// Sleeps for `duration` from a recorded reading of the clock, returning
1341    /// the wake instant it recorded.
1342    ///
1343    /// Exactly `now() + duration`, recorded: the reading goes into the log as
1344    /// a `NowObserved` before the sleep is derived from it, so every later
1345    /// drive replays the identical reading and derives the identical instant.
1346    /// A duration alone means nothing to a replay, which has no clock to
1347    /// interpret it against; this is the composition that turns one into an
1348    /// instant without leaving determinism behind.
1349    ///
1350    /// Carries every constraint [`sleep_until`](Self::sleep_until) does,
1351    /// including the never-inside-a-claimed-call rule. Follow it with
1352    /// [`await_wake`](Self::await_wake).
1353    ///
1354    /// # Errors
1355    ///
1356    /// [`RuntimeError::SleepOverflow`] when the wake instant would fall
1357    /// outside the representable range; [`RuntimeError::Replay`] on
1358    /// divergence; [`RuntimeError::Store`] when persistence fails.
1359    pub async fn sleep_for(&mut self, duration: Duration) -> Result<OffsetDateTime, RuntimeError> {
1360        let now = self.now().await?;
1361        let wake_at = now
1362            .checked_add(duration)
1363            .ok_or(RuntimeError::SleepOverflow { now, duration })?;
1364        self.sleep_until(wake_at).await?;
1365        Ok(wake_at)
1366    }
1367
1368    /// Asks whether the sleep is over.
1369    ///
1370    /// Replayed: the log holds the `SleepCompleted`, so the sleep already
1371    /// ended and the run carries on. Live: the injected clock decides. At or
1372    /// past the recorded wake instant the completion is recorded and the run
1373    /// continues; before it, the run stays asleep and [`Waking::Asleep`] tells
1374    /// the caller to stop driving.
1375    ///
1376    /// The clock read belongs here and not in the cursor, which reads none;
1377    /// it is the same category of live-only decision as "was a resume input
1378    /// provided", and like that one it is never recorded as an observation,
1379    /// because what the log needs is the fact that the sleep ended, not the
1380    /// instant something noticed. Enforcing the deadline here also means no
1381    /// caller can wake a run early by driving it early: a driver that comes
1382    /// back too soon simply finds it still asleep.
1383    ///
1384    /// Call it after [`sleep_until`](Self::sleep_until) or
1385    /// [`sleep_for`](Self::sleep_for) in the same drive, so the deadline to
1386    /// compare against is in hand. Without a sleep before it, there is no
1387    /// deadline that could have arrived and the run stays asleep.
1388    ///
1389    /// # Errors
1390    ///
1391    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
1392    /// persistence fails.
1393    pub async fn await_wake(&mut self) -> Result<Waking, RuntimeError> {
1394        match self.cursor.sleep_completed()? {
1395            Outcome::Replayed(()) => {
1396                self.sleeping_until = None;
1397                Ok(Waking::Woken)
1398            }
1399            Outcome::Live(asleep) => {
1400                // The last representable instant stands in for a deadline
1401                // this drive never set: a wake nobody asked for has not
1402                // arrived, and no clock reading will make it so.
1403                let wake_at = self
1404                    .sleeping_until
1405                    .unwrap_or_else(|| PrimitiveDateTime::MAX.assume_utc());
1406                if (self.clock)() < wake_at {
1407                    return Ok(Waking::Asleep { wake_at });
1408                }
1409                let emitted = asleep.wake();
1410                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await?;
1411                self.sleeping_until = None;
1412                Ok(Waking::Woken)
1413            }
1414        }
1415    }
1416
1417    /// Records a budget crossing. The check that led here must be computed
1418    /// from replayed data (recorded usage, recorded `now` observations) so
1419    /// it re-fires identically on replay. Follow with
1420    /// [`await_resume`](Self::await_resume), exactly like a suspension.
1421    ///
1422    /// # Errors
1423    ///
1424    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
1425    /// persistence fails.
1426    pub async fn budget_exceeded(
1427        &mut self,
1428        budget: Budget,
1429        observed: f64,
1430    ) -> Result<(), RuntimeError> {
1431        match self.cursor.budget_exceeded(budget, observed)? {
1432            Outcome::Replayed(()) => Ok(()),
1433            Outcome::Live(emitted) => {
1434                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
1435            }
1436        }
1437    }
1438
1439    /// Completes the run with `output`. Every request after this is a
1440    /// divergence.
1441    ///
1442    /// # Errors
1443    ///
1444    /// [`RuntimeError::Replay`] on divergence (including an output that does
1445    /// not match the recorded one); [`RuntimeError::Store`] when persistence
1446    /// fails.
1447    pub async fn complete_run(&mut self, output: &Value) -> Result<(), RuntimeError> {
1448        match self.cursor.complete_run(output)? {
1449            Outcome::Replayed(()) => Ok(()),
1450            Outcome::Live(emitted) => {
1451                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
1452            }
1453        }
1454    }
1455
1456    /// Fails the run with `error`. Every request after this is a divergence.
1457    ///
1458    /// # Errors
1459    ///
1460    /// [`RuntimeError::Replay`] on divergence; [`RuntimeError::Store`] when
1461    /// persistence fails.
1462    pub async fn fail_run(&mut self, error: &str) -> Result<(), RuntimeError> {
1463        match self.cursor.fail_run(error)? {
1464            Outcome::Replayed(()) => Ok(()),
1465            Outcome::Live(emitted) => {
1466                persist(self.store.as_ref(), self.run_id, &self.clock, &emitted).await
1467            }
1468        }
1469    }
1470}
1471
1472/// Wraps an emitted event in an envelope (timestamp from the injected clock,
1473/// at this IO edge) and appends it durably. When this returns `Ok`, the
1474/// event is in the store.
1475async fn persist(
1476    store: &dyn EventStore,
1477    run_id: RunId,
1478    clock: &ClockFn,
1479    emitted: &Emitted,
1480) -> Result<(), RuntimeError> {
1481    let envelope = EventEnvelope::new(run_id, emitted.seq, (clock)(), emitted.event.clone());
1482    store.append(&envelope).await?;
1483    // The event is durable now, so this is the honest moment to report it.
1484    // Live progress streams from here as the run drives; replayed events take
1485    // the cursor's early return above and never reach this edge, so they never
1486    // re-emit. The detail is truncated (see `crate::progress`), so no full
1487    // payload rides the progress stream.
1488    crate::progress::emit_step(run_id, envelope.seq, &envelope.event);
1489    Ok(())
1490}
1491
1492/// Whether an effect class participates in cross-run deduplication at all.
1493///
1494/// A [`Effect::Read`] performs nothing worth naming, so there is nothing for a
1495/// second run to avoid repeating. The other two classes both do something to
1496/// the world, and a key on them is a claim about which something.
1497fn deduplicates(effect: Effect) -> bool {
1498    matches!(effect, Effect::Write | Effect::Idempotent)
1499}
1500
1501/// Reads back the call a commitment points at, and checks it really is the same
1502/// call.
1503///
1504/// The read goes through [`EventStore::read_log`], never around it, so the
1505/// origin run's hash chain is verified before any of its recorded bytes are
1506/// copied into this run's log. A commitment is a pointer and nothing else,
1507/// which is what makes that unavoidable rather than merely encouraged.
1508///
1509/// The input check is what keeps a key honest. If the origin's recorded input
1510/// differs from this call's, one key is naming two different calls, and both
1511/// available answers are wrong: copying would return an output computed from
1512/// somebody else's arguments, executing would repeat an effect the key says has
1513/// already happened. So neither happens.
1514async fn committed_call(
1515    store: &dyn EventStore,
1516    tool: &str,
1517    idempotency_key: &str,
1518    commitment: CallCommitment,
1519    input: &Value,
1520) -> Result<(Value, DedupOrigin), RuntimeError> {
1521    let log = store.read_log(commitment.run_id).await?;
1522    let correlation = commitment.intent_seq;
1523    let unreadable = || RuntimeError::CommitmentUnreadable {
1524        tool: tool.to_owned(),
1525        idempotency_key: idempotency_key.to_owned(),
1526        origin: commitment.run_id,
1527        origin_seq: correlation.get(),
1528    };
1529
1530    let recorded_input = log
1531        .iter()
1532        .find_map(|envelope| match &envelope.event {
1533            Event::ToolCallRequested {
1534                seq,
1535                tool: recorded_tool,
1536                input,
1537                ..
1538            } if *seq == correlation && recorded_tool == tool => Some(input),
1539            _ => None,
1540        })
1541        .ok_or_else(unreadable)?;
1542    if recorded_input != input {
1543        return Err(RuntimeError::IdempotencyKeyCollision {
1544            tool: tool.to_owned(),
1545            idempotency_key: idempotency_key.to_owned(),
1546            origin: commitment.run_id,
1547            origin_seq: correlation.get(),
1548        });
1549    }
1550
1551    let output = log
1552        .iter()
1553        .find_map(|envelope| match &envelope.event {
1554            Event::ToolCallCompleted { seq, output, .. } if *seq == correlation => Some(output),
1555            _ => None,
1556        })
1557        .ok_or_else(unreadable)?
1558        .clone();
1559
1560    Ok((
1561        output,
1562        DedupOrigin {
1563            run_id: commitment.run_id,
1564            seq: correlation,
1565        },
1566    ))
1567}
1568
1569/// Persists a completion and settles its call commitment in one indivisible
1570/// store operation. The settling counterpart of [`persist`].
1571async fn persist_settling(
1572    store: &dyn EventStore,
1573    run_id: RunId,
1574    clock: &ClockFn,
1575    emitted: &Emitted,
1576    claimant: CallClaimant<'_>,
1577) -> Result<(), RuntimeError> {
1578    let envelope = EventEnvelope::new(run_id, emitted.seq, (clock)(), emitted.event.clone());
1579    store.append_settling_call(&envelope, claimant).await?;
1580    crate::progress::emit_step(run_id, envelope.seq, &envelope.event);
1581    Ok(())
1582}
1583
1584/// Decodes a recorded completion output into the same [`ToolCallResult`] the
1585/// live path produced, so replayed orchestration takes the identical branch.
1586fn decode_tool_output(output: Value) -> ToolCallResult {
1587    if let Some(suspension) = decode_suspension(&output) {
1588        return ToolCallResult::Suspended(suspension);
1589    }
1590    if let Some(sleep) = decode_sleep(&output) {
1591        return ToolCallResult::Sleeping(sleep);
1592    }
1593    if let Some(failure) = decode_failure(&output) {
1594        return ToolCallResult::Failed(failure);
1595    }
1596    ToolCallResult::Output(output)
1597}
1598
1599/// The default random source: 64 bits folded from a freshly drawn version 4
1600/// UUID, which the `uuid` crate fills from operating-system randomness. Non
1601/// cryptographic by design; recorded bits only ever seed idempotency keys
1602/// and user-level derivations.
1603pub(crate) fn os_random() -> u64 {
1604    let bits = Uuid::new_v4().as_u128();
1605    (bits as u64) ^ ((bits >> 64) as u64)
1606}