Skip to main content

ri_agent_graph/
error.rs

1use thiserror::Error;
2
3/// The checkpoint-store operation that failed during a configured durable run.
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum CheckpointStoreOperation {
6    CreateRun,
7    RecordAttempt,
8    CompleteAttempt,
9    FailAttempt,
10    SaveStateSnapshot,
11    CompleteRun,
12    FailRun,
13}
14
15impl std::fmt::Display for CheckpointStoreOperation {
16    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
17        let operation = match self {
18            Self::CreateRun => "create run",
19            Self::RecordAttempt => "record attempt",
20            Self::CompleteAttempt => "complete attempt",
21            Self::FailAttempt => "fail attempt",
22            Self::SaveStateSnapshot => "save state snapshot",
23            Self::CompleteRun => "complete run",
24            Self::FailRun => "fail run",
25        };
26        f.write_str(operation)
27    }
28}
29
30#[derive(Error, Debug)]
31pub enum AgentGraphError {
32    #[error("Node not found: {0}")]
33    NodeNotFound(String),
34
35    #[error("Routing error: {0}")]
36    RoutingError(String),
37
38    #[error("State error: {0}")]
39    StateError(String),
40
41    #[error("Max iterations exceeded: {current}/{max}")]
42    MaxIterationsExceeded { current: usize, max: usize },
43
44    #[error("Cycle detected: {path:?}")]
45    CycleDetected { path: Vec<String> },
46
47    #[error("Checkpoint error: {0}")]
48    CheckpointError(String),
49
50    /// A configured granular checkpoint store failed; durable execution cannot continue.
51    #[error("Checkpoint store failed to {operation}: {message}")]
52    CheckpointStore {
53        operation: CheckpointStoreOperation,
54        message: String,
55    },
56
57    #[error("Checkpoint graph mismatch: expected hash '{expected}', got '{actual}'")]
58    CheckpointMismatch { expected: String, actual: String },
59
60    #[error("run not found: {0}")]
61    RunNotFound(String),
62    #[error("attempt not found: {0}")]
63    AttemptNotFound(String),
64    #[error("attempt '{attempt_id}' belongs to run '{actual_run}', not '{expected_run}'")]
65    AttemptRunMismatch {
66        attempt_id: String,
67        expected_run: String,
68        actual_run: String,
69    },
70    #[error("invalid checkpoint transition: {0}")]
71    InvalidTransition(String),
72    #[error("terminal state conflict for run '{0}'")]
73    TerminalStateConflict(String),
74
75    #[error("Execution error: {0}")]
76    ExecutionError(String),
77
78    #[error("Interrupt at node '{node}'")]
79    InterruptError {
80        node: String,
81        value: Option<serde_json::Value>,
82    },
83
84    #[error("Payload error: {0}")]
85    PayloadError(String),
86
87    #[error("Cancelled")]
88    Cancelled,
89
90    #[error("Serialization error: {0}")]
91    SerializationError(#[from] serde_json::Error),
92
93    #[cfg(feature = "checkpointing")]
94    #[error("Database error: {0}")]
95    DatabaseError(#[from] rusqlite::Error),
96
97    #[error("{0}")]
98    Other(String),
99}
100
101impl AgentGraphError {
102    /// Stable string discriminant for structured logging (PRIMITIVES_CONTRACT §2).
103    pub fn kind(&self) -> &'static str {
104        match self {
105            Self::NodeNotFound(_) => "node_not_found",
106            Self::RoutingError(_) => "routing",
107            Self::StateError(_) => "state",
108            Self::MaxIterationsExceeded { .. } => "max_iterations",
109            Self::CycleDetected { .. } => "cycle_detected",
110            Self::CheckpointError(_) => "checkpoint",
111            Self::CheckpointStore { .. } => "checkpoint_store",
112            Self::CheckpointMismatch { .. } => "checkpoint_mismatch",
113            Self::RunNotFound(_) => "run_not_found",
114            Self::AttemptNotFound(_) => "attempt_not_found",
115            Self::AttemptRunMismatch { .. } => "attempt_run_mismatch",
116            Self::InvalidTransition(_) => "invalid_transition",
117            Self::TerminalStateConflict(_) => "terminal_state_conflict",
118            Self::ExecutionError(_) => "execution",
119            Self::InterruptError { .. } => "interrupt",
120            Self::PayloadError(_) => "payload",
121            Self::Cancelled => "cancelled",
122            Self::SerializationError(_) => "serialization",
123            #[cfg(feature = "checkpointing")]
124            Self::DatabaseError(_) => "database",
125            Self::Other(_) => "other",
126        }
127    }
128}
129
130pub type Result<T> = std::result::Result<T, AgentGraphError>;
131
132/// Create an interrupt error that can be returned from within a node.
133/// This causes the graph executor to pause execution at the current node.
134pub fn interrupt(node: impl Into<String>, value: Option<serde_json::Value>) -> AgentGraphError {
135    AgentGraphError::InterruptError {
136        node: node.into(),
137        value,
138    }
139}