1#[derive(Debug, thiserror::Error)]
2pub enum Error {
3 #[error("{0}")]
4 Filesystem(#[from] crate::filesystem::Error),
5 #[error("{}", format_http_error(.0))]
6 Http(#[from] objectiveai_sdk::HttpError),
7 #[error("{0}")]
8 ResponseError(objectiveai_sdk::error::ResponseError),
9 #[error("{0} source is not supported for function-profile pairs")]
10 PairsSourceNotSupported(&'static str),
11 #[error("authorization is global only")]
12 AuthorizationGlobalOnly,
13 #[error("{0}")]
14 MissingArgs(&'static str),
15 #[error("invalid path: {0}")]
16 PathParse(String),
17 #[error("python wasm runtime error: {0}")]
18 PythonWasm(String),
19 #[error("podman: {0}")]
20 Podman(String),
21 #[error("failed to read python file {0}: {1}")]
22 PythonFileRead(std::path::PathBuf, std::io::Error),
23 #[error("failed to read prompt file {0}: {1}")]
24 PromptFileRead(std::path::PathBuf, std::io::Error),
25 #[error("failed to read JSON file {0}: {1}")]
26 JsonFileRead(std::path::PathBuf, std::io::Error),
27 #[error("python exception:\n{0}")]
28 PythonException(String),
29 #[error("python script produced no output")]
30 PythonNoOutput,
31 #[error("python output deserialization failed: {0}")]
32 PythonDeserialize(serde_path_to_error::Error<serde_json::Error>),
33 #[error("internal error: python harness output is malformed: {0}")]
34 PythonHarnessBroken(String),
35 #[error("inline JSON deserialization failed: {0}")]
36 InlineDeserialize(serde_path_to_error::Error<serde_json::Error>),
37 #[error("inline JSON conversion failed: {0}")]
38 InlineJson(#[from] serde_json::Error),
39 #[error("stream ended without producing any chunks")]
40 EmptyStream,
41 #[error("config set forbidden by server configuration")]
42 ConfigSetForbidden,
43 #[error("log writer task panicked or was cancelled")]
44 WriterPanic,
45 #[error("subscribe timed out")]
46 LogSubscribeTimedOut,
47 #[error("plugin not found: {0}")]
48 PluginNotFound(String),
49 #[error("failed to spawn plugin: {0}")]
50 PluginSpawn(std::io::Error),
51 #[error("failed to read plugin output: {0}")]
52 PluginRead(std::io::Error),
53 #[error("plugin exited with non-zero status: {0}")]
54 PluginExit(i32),
55 #[error("plugins may not invoke `{0}` commands")]
56 PluginCommandForbidden(&'static str),
57 #[error("tool not found: {0}")]
58 ToolNotFound(String),
59 #[error("failed to spawn tool: {0}")]
60 ToolSpawn(std::io::Error),
61 #[error("failed to read tool output: {0}")]
62 ToolRead(std::io::Error),
63 #[error("tool exited with non-zero status: {0}")]
64 ToolExit(i32),
65 #[error(
66 "{kind} {owner}/{repository} (commit {commit_sha}, version {version}) is not in the install whitelist; pass --allow-untrusted to install anyway"
67 )]
68 NotWhitelisted {
69 kind: &'static str,
70 owner: String,
71 repository: String,
72 commit_sha: String,
73 version: String,
74 },
75 #[error("whitelist regex error: {0}")]
76 WhitelistRegex(regex::Error),
77 #[error(
78 "daemon address unavailable: the daemon could not be spawned or its lock could not be read"
79 )]
80 DaemonAddressUnavailable,
81 #[error("updater: {0}")]
82 Updater(String),
83 #[error("instance runner: {0}")]
84 Instance(String),
85 #[error("not found: {0}")]
86 NotFound(String),
87 #[error("{0} remote is not supported for this command")]
88 RemoteNotSupported(&'static str),
89 #[error("{0}")]
90 ClapParse(#[from] clap::Error),
91 #[error("argument parse error at `{}`: {}", .0.field, .0.source)]
92 FromArgs(#[from] objectiveai_sdk::cli::command::FromArgsError),
93 #[error("{name} exited before publishing its lock ({status}); stdout: {stdout}; stderr: {stderr}")]
94 SpawnExitedBeforePublishing {
95 name: String,
96 status: std::process::ExitStatus,
97 stdout: String,
98 stderr: String,
99 },
100 #[error("lockfile {key}: {source}")]
101 Lockfile { key: String, source: std::io::Error },
102 #[error("spawn {0}: {1}")]
103 Spawn(String, std::io::Error),
104 #[error(
105 "no prior agent_completion_request for agent {agent_instance_hierarchy:?}; spawn the agent first with `agents spawn`"
106 )]
107 AgentNoPriorRequest { agent_instance_hierarchy: String },
108 #[error(
109 "agent {agent_instance_hierarchy:?} has no continuations available across {request_count} prior request(s); the most recent turn may still be streaming, or none have finished. Cannot fall back without a continuation."
110 )]
111 AgentNoContinuation {
112 agent_instance_hierarchy: String,
113 request_count: usize,
114 },
115 #[error(
116 "tag {tag:?} exists but the agent has not been spawned yet (tag_group_id={tag_group_id}, parent_agent_instance_hierarchy={parent_agent_instance_hierarchy:?})"
117 )]
118 TagGrouped {
119 tag: String,
120 tag_group_id: i64,
121 parent_agent_instance_hierarchy: String,
122 },
123 #[error("tag {0:?} is not registered")]
124 TagNotFound(String),
125 #[error(
126 "agent instance {agent_instance_hierarchy:?} is already active (its lock is held by a live process)"
127 )]
128 AgentInstanceActive { agent_instance_hierarchy: String },
129 #[error("agent tag {tag:?} is already being spawned (its lock is held by a live process)")]
130 AgentTagActive { tag: String },
131 #[error(
132 "cannot apply tag {tag:?} while an agent holding it is active (its tag lock is held by a live process)"
133 )]
134 TagApplyAgentActive { tag: String },
135 #[error("cannot enqueue against an agent ref; enqueue targets an instance or a tag")]
136 EnqueueRefTarget,
137 #[error("cannot wait on an agent ref; wait targets an instance or a tag")]
138 WaitRefTarget,
139 #[error("cannot attach/detach a laboratory to an agent ref; target an instance or a tag")]
140 LaboratoryRefTarget,
141 #[error("laboratory {laboratory_id:?} is already attached to this agent")]
142 LaboratoryAlreadyAttached { laboratory_id: String },
143 #[error("laboratory {laboratory_id:?} is not attached to this agent")]
144 LaboratoryNotAttached { laboratory_id: String },
145 #[error(
146 "FATAL: tag {tag:?} lock was released without its GROUPED->BOUND upgrade; the spawn flow's upgrade-before-release invariant is broken"
147 )]
148 TagLockDroppedWithoutUpgrade { tag: String },
149 #[error(
150 "queued message {id} was sent by {sender_agent_instance_hierarchy:?}; it can only be deleted by the sender or a parent of the sender (caller is {caller_agent_instance_hierarchy:?})"
151 )]
152 QueueDeleteUnauthorized {
153 id: i64,
154 sender_agent_instance_hierarchy: String,
155 caller_agent_instance_hierarchy: String,
156 },
157 #[error(
158 "a schedule named {name:?} already exists for {agent_instance_hierarchy:?}; pass --overwrite to replace it"
159 )]
160 ScheduleAlreadyExists {
161 name: String,
162 agent_instance_hierarchy: String,
163 },
164 #[error("db: {0}")]
165 Db(#[from] crate::db::Error),
166 #[error("not implemented: {0}")]
170 NotImplemented(&'static str),
171 #[error("invalid query: {0}")]
172 InvalidQuery(String),
173 #[error("query exceeded timeout")]
174 QueryTimeout,
175 #[error("query attempted a write in a read-only context")]
176 QueryReadOnlyViolation,
177 #[error("timed out after {timeout_seconds}s")]
180 Timeout { timeout_seconds: u64 },
181 #[error("response exceeded token budget — actual {actual} tokens, limit {limit}")]
185 TokenBudgetExceeded { limit: u64, actual: u64 },
186}
187
188impl Error {
189 pub fn output_message(&self) -> serde_json::Value {
194 match self {
195 Error::ResponseError(re) => {
196 serde_json::to_value(re).unwrap_or_else(|_| self.to_string().into())
197 }
198 _ => self.to_string().into(),
199 }
200 }
201}
202
203fn http_is_connect_failure(err: &objectiveai_sdk::HttpError) -> bool {
204 use objectiveai_sdk::HttpError as H;
205 let reqwest_err = match err {
206 H::StreamError(reqwest_eventsource::Error::Transport(e)) => e,
207 H::RequestError(e) | H::HttpError(e) => e,
208 _ => return false,
209 };
210 reqwest_err.is_connect() || reqwest_err.is_timeout()
211}
212
213fn format_http_error(err: &objectiveai_sdk::HttpError) -> String {
214 if http_is_connect_failure(err) {
215 format!(
216 "{err}\n\nhint: this looks like a connection failure to the resolved API \
217address. the cli auto-spawns a local objectiveai-api when no address is \
218configured; if you set `api.address`, verify it \
219(`objectiveai api config address get --final`) or unset it to use the \
220local server. to (re)start the local server: `objectiveai api spawn`"
221 )
222 } else {
223 err.to_string()
224 }
225}