Skip to main content

salvor_runtime/
error.rs

1//! [`RuntimeError`]: the one error type every `salvor-runtime` operation
2//! returns.
3//!
4//! The variants fall into three groups:
5//!
6//! - **Forwarded layers.** [`Replay`](RuntimeError::Replay),
7//!   [`Store`](RuntimeError::Store), and [`Model`](RuntimeError::Model) wrap
8//!   the typed errors of the crates underneath, unflattened, so a caller can
9//!   still match the inner variant. The one that matters most is
10//!   `Replay(ReplayError::NeedsReconciliation)`: resuming a run whose log
11//!   ends in a write intent with no completion surfaces here, and the runtime
12//!   refuses to continue until a human resolves it.
13//! - **Serialization edges.** [`RequestEncode`](RuntimeError::RequestEncode)
14//!   and [`RecordedResponseDecode`](RuntimeError::RecordedResponseDecode)
15//!   mark the two places JSON conversion can fail around a model call.
16//! - **Runtime protocol.** Starting a run that already has history, resuming
17//!   a run that is not parked, resuming with input the recorded schema
18//!   rejects, or naming a run the store does not know.
19
20use salvor_core::{ReplayError, RunId};
21use salvor_store::StoreError;
22use thiserror::Error;
23
24/// What can go wrong while driving a run.
25#[derive(Debug, Error)]
26pub enum RuntimeError {
27    /// The replay layer refused to continue: divergence, a malformed log, or
28    /// a dangling write intent that needs human reconciliation.
29    #[error("replay: {0}")]
30    Replay(#[from] ReplayError),
31
32    /// The event store failed to persist or read an event.
33    #[error("store: {0}")]
34    Store(#[from] StoreError),
35
36    /// A live model call failed after the client's own retries. The run's
37    /// log is intact (the intent, if any, is recorded), so the run can be
38    /// recovered later; the model intent will be re-issued safely.
39    #[error("model call: {0}")]
40    Model(#[from] salvor_llm::Error),
41
42    /// A model request could not be serialized to JSON for hashing.
43    #[error("model request did not serialize: {0}")]
44    RequestEncode(serde_json::Error),
45
46    /// A recorded model response could not be decoded back into a typed
47    /// response. This means the log holds something this build cannot read,
48    /// which is a storage or versioning fault, not orchestration divergence.
49    #[error("recorded model response did not decode: {0}")]
50    RecordedResponseDecode(serde_json::Error),
51
52    /// `start` was called for a run id that already has recorded history.
53    #[error("run {run_id:?} already has recorded history; use recover or resume")]
54    RunAlreadyStarted {
55        /// The run that already exists.
56        run_id: RunId,
57    },
58
59    /// The named run has no recorded history at all.
60    #[error("run {run_id:?} has no recorded history")]
61    UnknownRun {
62        /// The run that was not found.
63        run_id: RunId,
64    },
65
66    /// `resume` was called on a run whose log does not end at a suspension
67    /// or budget crossing.
68    #[error("run {run_id:?} is not parked (status: {status}); resume needs a parked run")]
69    NotParked {
70        /// The run that was not parked.
71        run_id: RunId,
72        /// A short description of the status the run was actually in.
73        status: String,
74    },
75
76    /// The resume input did not satisfy the recorded suspension schema (or,
77    /// for a budget crossing, the budget-extension shape).
78    #[error("resume input rejected: {0}")]
79    ResumeInputRejected(String),
80
81    /// The labels a run is about to be created with violate the sanity
82    /// bounds (too many, or a key/value over its length cap). See
83    /// [`crate::validate_labels`]. Surfaces only on a genuinely fresh
84    /// `begin`; a replayed run never re-checks the labels it already
85    /// recorded.
86    #[error("invalid labels: {0}")]
87    InvalidLabels(String),
88
89    /// `resolve` was called on a run that is not awaiting reconciliation. The
90    /// hand-recorded completion is only ever appended to a run whose log ends
91    /// at a dangling write intent; every other state is a caller mistake.
92    #[error(
93        "run {run_id:?} does not need reconciliation (status: {status}); resolve records the completion of a dangling write intent, and this run has none"
94    )]
95    NotReconcilable {
96        /// The run that was not awaiting reconciliation.
97        run_id: RunId,
98        /// A short description of the status the run was actually in.
99        status: String,
100    },
101
102    /// `abandon` was called on a run that already reached a terminal event
103    /// (completed, failed, or previously abandoned). A terminal run is already
104    /// at rest; there is nothing left to retire, so the operator action is
105    /// refused rather than appending a second terminal.
106    #[error(
107        "run {run_id:?} is already terminal (status: {status}); there is nothing left to abandon"
108    )]
109    AlreadyTerminal {
110        /// The run that had already finished.
111        run_id: RunId,
112        /// A short description of the terminal status the run was in.
113        status: String,
114    },
115}