1use axum::http::StatusCode;
2use axum::response::{IntoResponse, Response};
3use thiserror::Error;
4
5#[derive(Error, Debug)]
6pub enum EnvoyError {
7 #[error("graph error: {0}")]
8 Graph(#[from] sqlitegraph::SqliteGraphError),
9
10 #[error("atheneum error: {0}")]
11 Atheneum(#[from] anyhow::Error),
12
13 #[error("serialization error: {0}")]
14 Serialization(#[from] serde_json::Error),
15
16 #[error("channel not found: {0}")]
17 ChannelNotFound(String),
18
19 #[error("channel already exists: {0}")]
20 ChannelAlreadyExists(String),
21
22 #[error("agent not subscribed to channel {channel}")]
23 NotSubscribed { agent: String, channel: String },
24
25 #[error("invalid entity: {0}")]
26 InvalidEntity(String),
27
28 #[error("agent not found: {0}")]
29 AgentNotFound(String),
30
31 #[error("agent offline: {0}")]
32 AgentOffline(String),
33
34 #[error("agent retired: {0}")]
35 AgentRetired(String),
36
37 #[error("agent already exists: {0}")]
38 AgentAlreadyExists(String),
39
40 #[error("message not found: {0}")]
41 MessageNotFound(String),
42
43 #[error("invalid message: {0}")]
44 InvalidMessage(String),
45
46 #[error("websocket error: {0}")]
47 WsError(String),
48
49 #[error("message too large: {0} bytes exceeds 1MB limit")]
50 MessageTooLarge(usize),
51
52 #[error("too many parts: {0} exceeds 20 limit")]
53 TooManyParts(usize),
54
55 #[error("database error: {0}")]
56 Database(#[from] rusqlite::Error),
57
58 #[error("agent status stale: {agent_id} (last heartbeat: {last_heartbeat:?}, threshold: {threshold_minutes}m)")]
59 StaleAgent {
60 agent_id: String,
61 last_heartbeat: Option<String>,
62 threshold_minutes: i64,
63 },
64
65 #[error("dependency already exists: {dependent} is already waiting on {blocker}")]
66 DuplicateDependency { dependent: String, blocker: String },
67
68 #[error("dependency not found: {0}")]
69 DependencyNotFound(String),
70
71 #[error("task not found: {0}")]
72 TaskNotFound(String),
73
74 #[error("task already claimed: {0} by {1}")]
75 TaskAlreadyClaimed(String, String),
76
77 #[error("invalid task state transition: {task_id} from {from} to {to}")]
78 InvalidTaskState {
79 task_id: String,
80 from: String,
81 to: String,
82 },
83
84 #[error("not task claimant: agent {agent} does not own task {task_id}")]
85 NotTaskClaimant { agent: String, task_id: String },
86
87 #[error("project config not found: {0}")]
88 ProjectConfigNotFound(String),
89
90 #[error("subscription not found: {0} for project {1}")]
91 SubscriptionNotFound(String, String),
92}
93
94impl IntoResponse for EnvoyError {
95 fn into_response(self) -> Response {
96 let (status, code) = match &self {
97 Self::AgentNotFound(_) => (StatusCode::NOT_FOUND, "AGENT_NOT_FOUND"),
98 Self::AgentOffline(_) => (StatusCode::CONFLICT, "AGENT_OFFLINE"),
99 Self::AgentRetired(_) => (StatusCode::GONE, "AGENT_RETIRED"),
100 Self::AgentAlreadyExists(_) => (StatusCode::CONFLICT, "AGENT_ALREADY_EXISTS"),
101 Self::MessageNotFound(_) => (StatusCode::NOT_FOUND, "MESSAGE_NOT_FOUND"),
102 Self::ChannelNotFound(_) => (StatusCode::NOT_FOUND, "CHANNEL_NOT_FOUND"),
103 Self::InvalidMessage(_) => (StatusCode::BAD_REQUEST, "INVALID_MESSAGE"),
104 Self::MessageTooLarge(_) => (StatusCode::BAD_REQUEST, "MESSAGE_TOO_LARGE"),
105 Self::TooManyParts(_) => (StatusCode::BAD_REQUEST, "TOO_MANY_PARTS"),
106 Self::Serialization(_) => (StatusCode::BAD_REQUEST, "SERIALIZATION_ERROR"),
107 Self::StaleAgent { .. } => (StatusCode::OK, "STALE_AGENT"),
108 Self::DuplicateDependency { .. } => (StatusCode::CONFLICT, "DUPLICATE_DEPENDENCY"),
109 Self::DependencyNotFound(_) => (StatusCode::NOT_FOUND, "DEPENDENCY_NOT_FOUND"),
110 Self::TaskNotFound(_) => (StatusCode::NOT_FOUND, "TASK_NOT_FOUND"),
111 Self::TaskAlreadyClaimed(_, _) => (StatusCode::CONFLICT, "TASK_ALREADY_CLAIMED"),
112 Self::InvalidTaskState { .. } => (StatusCode::CONFLICT, "INVALID_TASK_STATE"),
113 Self::NotTaskClaimant { .. } => (StatusCode::FORBIDDEN, "NOT_TASK_CLAIMANT"),
114 Self::ProjectConfigNotFound(_) => (StatusCode::NOT_FOUND, "PROJECT_CONFIG_NOT_FOUND"),
115 Self::SubscriptionNotFound(_, _) => (StatusCode::NOT_FOUND, "SUBSCRIPTION_NOT_FOUND"),
116 _ => (StatusCode::INTERNAL_SERVER_ERROR, "INTERNAL_ERROR"),
117 };
118
119 let body = serde_json::json!({
120 "error": {
121 "code": code,
122 "message": self.to_string()
123 }
124 });
125
126 (status, axum::Json(body)).into_response()
127 }
128}
129
130pub type Result<T> = std::result::Result<T, EnvoyError>;