Skip to main content

opencode_ctl/
error.rs

1//! Stable error codes and the tool error envelope.
2
3use serde_json::{json, Value};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
6pub enum ErrorCode {
7    OpencodeNotFound,
8    ServiceDown,
9    AuthFailed,
10    SessionNotFound,
11    NoSession,
12    PairNotFound,
13    InvalidArgument,
14    InvalidRequest,
15    RefusedOnHost,
16    ConfirmRequired,
17    PermissionsLocked,
18    MutationBlocked,
19    BlockedOperation,
20    NonJsonResponse,
21    UnknownOperation,
22    BodyTooLarge,
23    Conflict,
24    Deadline,
25    IoError,
26    LauncherConflict,
27    ControllerNotFound,
28    CliError,
29}
30
31impl ErrorCode {
32    pub const ALL: [ErrorCode; 22] = [
33        ErrorCode::OpencodeNotFound,
34        ErrorCode::ServiceDown,
35        ErrorCode::AuthFailed,
36        ErrorCode::SessionNotFound,
37        ErrorCode::NoSession,
38        ErrorCode::PairNotFound,
39        ErrorCode::InvalidArgument,
40        ErrorCode::InvalidRequest,
41        ErrorCode::RefusedOnHost,
42        ErrorCode::ConfirmRequired,
43        ErrorCode::PermissionsLocked,
44        ErrorCode::MutationBlocked,
45        ErrorCode::BlockedOperation,
46        ErrorCode::NonJsonResponse,
47        ErrorCode::UnknownOperation,
48        ErrorCode::BodyTooLarge,
49        ErrorCode::Conflict,
50        ErrorCode::Deadline,
51        ErrorCode::IoError,
52        ErrorCode::LauncherConflict,
53        ErrorCode::ControllerNotFound,
54        ErrorCode::CliError,
55    ];
56
57    pub fn as_str(self) -> &'static str {
58        match self {
59            ErrorCode::OpencodeNotFound => "OPENCODE_NOT_FOUND",
60            ErrorCode::ServiceDown => "SERVICE_DOWN",
61            ErrorCode::AuthFailed => "AUTH_FAILED",
62            ErrorCode::SessionNotFound => "SESSION_NOT_FOUND",
63            ErrorCode::NoSession => "NO_SESSION",
64            ErrorCode::PairNotFound => "PAIR_NOT_FOUND",
65            ErrorCode::InvalidArgument => "INVALID_ARGUMENT",
66            ErrorCode::InvalidRequest => "INVALID_REQUEST",
67            ErrorCode::RefusedOnHost => "REFUSED_ON_HOST",
68            ErrorCode::ConfirmRequired => "CONFIRM_REQUIRED",
69            ErrorCode::PermissionsLocked => "PERMISSIONS_LOCKED",
70            ErrorCode::MutationBlocked => "MUTATION_BLOCKED",
71            ErrorCode::BlockedOperation => "BLOCKED_OPERATION",
72            ErrorCode::NonJsonResponse => "NON_JSON_RESPONSE",
73            ErrorCode::UnknownOperation => "UNKNOWN_OPERATION",
74            ErrorCode::BodyTooLarge => "BODY_TOO_LARGE",
75            ErrorCode::Conflict => "CONFLICT",
76            ErrorCode::Deadline => "DEADLINE",
77            ErrorCode::IoError => "IO_ERROR",
78            ErrorCode::LauncherConflict => "LAUNCHER_CONFLICT",
79            ErrorCode::ControllerNotFound => "CONTROLLER_NOT_FOUND",
80            ErrorCode::CliError => "CLI_ERROR",
81        }
82    }
83}
84
85#[derive(Debug, Clone, PartialEq)]
86pub struct ToolError {
87    pub code: ErrorCode,
88    pub message: String,
89    pub hint: Option<String>,
90    pub details: Option<Value>,
91}
92
93impl ToolError {
94    pub fn new(code: ErrorCode, msg: impl Into<String>) -> Self {
95        Self {
96            code,
97            message: msg.into(),
98            hint: None,
99            details: None,
100        }
101    }
102
103    pub fn hint(mut self, hint: &str) -> Self {
104        self.hint = Some(hint.to_string());
105        self
106    }
107
108    pub fn details(mut self, details: Value) -> Self {
109        self.details = Some(details);
110        self
111    }
112
113    pub fn to_json(&self) -> Value {
114        let mut error = json!({ "code": self.code.as_str(), "message": self.message });
115        if let Some(h) = &self.hint {
116            error["hint"] = json!(h);
117        }
118        if let Some(d) = &self.details {
119            error["details"] = d.clone();
120        }
121        json!({ "error": error })
122    }
123}
124
125impl std::fmt::Display for ToolError {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        write!(f, "{}: {}", self.code.as_str(), self.message)
128    }
129}
130
131impl std::error::Error for ToolError {}
132
133impl From<std::io::Error> for ToolError {
134    fn from(e: std::io::Error) -> Self {
135        ToolError::new(ErrorCode::IoError, e.to_string())
136    }
137}
138
139pub type ToolResult = Result<Value, ToolError>;