1use thiserror::Error;
2
3#[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 #[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 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
132pub fn interrupt(node: impl Into<String>, value: Option<serde_json::Value>) -> AgentGraphError {
135 AgentGraphError::InterruptError {
136 node: node.into(),
137 value,
138 }
139}