supercode_harness/error.rs
1use thiserror::Error;
2
3/// Result alias used throughout the crate.
4pub type Result<T> = std::result::Result<T, Error>;
5
6/// Errors that can arise while configuring or running an [`crate::Agent`].
7///
8/// `#[non_exhaustive]` so new variants can be added without a breaking release;
9/// match with a `_` arm.
10#[derive(Debug, Error)]
11#[non_exhaustive]
12pub enum Error {
13 /// A native session interchange operation failed.
14 #[error(transparent)]
15 Interchange(#[from] supercode_interchange::InterchangeError),
16
17 /// No API key was provided and none could be found in the environment.
18 #[error("missing API key: set it on the Config or via the {0} environment variable")]
19 MissingApiKey(String),
20
21 /// The HTTP transport failed. The underlying error is kept as an opaque
22 /// source rather than exposing the `reqwest` type, so a transport-library
23 /// bump is not a breaking change for this crate's public API.
24 #[error("http transport error: {0}")]
25 Http(#[source] Box<dyn std::error::Error + Send + Sync>),
26
27 /// The provider returned a non-success status.
28 #[error("provider returned status {status}: {body}")]
29 Provider {
30 /// HTTP status code.
31 status: u16,
32 /// Raw response body (truncated upstream if large).
33 body: String,
34 },
35
36 /// A response body could not be parsed.
37 #[error("failed to decode provider response: {0}")]
38 Decode(#[from] serde_json::Error),
39
40 /// A persisted session artifact failed its own framing/schema contract.
41 #[error("invalid session artifact: {0}")]
42 InvalidSession(String),
43
44 /// A versioned SDK/runtime operation failed. Runtime adapters retain this
45 /// typed value so outer SDK surfaces preserve its stable error name.
46 #[error(transparent)]
47 Sdk(#[from] crate::sdk::SdkError),
48
49 /// The model asked for a tool that isn't registered.
50 ///
51 /// Constructed by the agent loop's `run_tool` dispatch and fed back to the
52 /// model as this variant's `Display` rendering, so it is load-bearing on
53 /// the real tool-call path, not just a documented-but-unused variant.
54 #[error("model requested unknown tool: {0}")]
55 UnknownTool(String),
56
57 /// A tool's input arguments were not valid for its schema.
58 ///
59 /// Constructed both by built-in tools' argument parsing (`parse_args`) and
60 /// by the agent loop's `run_tool` when the model's raw argument JSON fails
61 /// to parse; either way its `Display` rendering is what the model sees.
62 #[error("invalid arguments for tool `{tool}`: {message}")]
63 InvalidArguments {
64 /// The tool that was called.
65 tool: String,
66 /// What was wrong with the arguments.
67 message: String,
68 },
69
70 /// A tool failed while executing.
71 #[error("tool `{tool}` failed: {message}")]
72 Tool {
73 /// The tool that failed.
74 tool: String,
75 /// Failure detail.
76 message: String,
77 },
78
79 /// The agent loop exceeded its configured iteration budget.
80 #[error("agent exceeded the maximum of {0} reasoning/tool iterations without finishing")]
81 MaxIterations(usize),
82
83 /// An I/O operation failed.
84 #[error("io error: {0}")]
85 Io(#[from] std::io::Error),
86
87 /// PARITY-18 D4 — a live request would exceed the target model's
88 /// context window even after [`crate::tokens::context_guard`]'s safety
89 /// margin and completion reserve are applied. Raised by
90 /// `crate::Agent::run_loop`'s per-send guard, which runs before EVERY
91 /// request this agent issues (not only the first) once
92 /// [`crate::Agent::set_context_limit`] has armed it — so an
93 /// over-context request is refused at any point in a session, not just
94 /// at the CLI's one-shot preflight.
95 ///
96 /// PARITY-18 v3 — `projected_tokens` is [`crate::tokens::context_guard`]'s
97 /// margin-adjusted estimate of messages+tools ONLY; it does NOT include
98 /// the completion reserve, so the refusal condition is actually
99 /// `projected_tokens + reserve_tokens > context_limit`, not
100 /// `projected_tokens > context_limit` — printing the bare comparison
101 /// (v2's wording) was arithmetically false as written (e.g. "projected
102 /// 193,064 > limit 200,000" reads as passing when the refusal is only
103 /// true once the reserve is added). `reserve_tokens` is carried on the
104 /// error so the `Display` impl states the true inequality.
105 #[error(
106 "cannot reduce below context limit: projected {projected_tokens} tokens + {reserve_tokens} reserve exceeds model {model} limit {context_limit}"
107 )]
108 ContextLimitExceeded {
109 /// Margin-adjusted projected token count for the request that was
110 /// about to be sent (messages + tools only; excludes the completion
111 /// reserve — see `reserve_tokens`).
112 projected_tokens: u64,
113 /// The completion-token reserve
114 /// ([`crate::tokens::CONTEXT_RESPONSE_RESERVE_TOKENS`]) added to
115 /// `projected_tokens` to derive the true refusal condition:
116 /// `projected_tokens + reserve_tokens > context_limit`.
117 reserve_tokens: u64,
118 /// The target model's context-window size.
119 context_limit: u64,
120 /// The model slug this limit was resolved for.
121 model: String,
122 },
123
124 /// P5-3 (§2 module 9 `subagents`, §5.3-style resource bound): a
125 /// `spawn_subagent` call was refused because it would exceed
126 /// `capabilities.subagents.max_depth` — the fail-closed depth cap that
127 /// keeps a parent-spawning-children-spawning-children chain from
128 /// growing unbounded. Named so the model (and a test) can tell this
129 /// apart from every other tool-error shape.
130 #[error(
131 "subagent spawn refused: depth {attempted_depth} would exceed \
132 capabilities.subagents.max_depth={max_depth}"
133 )]
134 SubagentDepthExceeded {
135 /// The configured cap.
136 max_depth: usize,
137 /// The depth the new child would have been spawned at.
138 attempted_depth: usize,
139 },
140
141 /// P5-3 (§2 module 9, §5.3-style resource bound): a `spawn_subagent`
142 /// call was refused because `capabilities.subagents.max_concurrent`
143 /// subagents are already in flight ANYWHERE in this spawn tree (the
144 /// concurrency gauge is shared root-to-leaf) — the fail-closed
145 /// fork-bomb guard.
146 #[error(
147 "subagent spawn refused: {max_concurrent} subagent(s) already running \
148 (capabilities.subagents.max_concurrent={max_concurrent})"
149 )]
150 SubagentConcurrencyExceeded {
151 /// The configured cap.
152 max_concurrent: usize,
153 },
154
155 /// P5-3 (§2.2 C6): a `background: true` spawn was refused because no
156 /// `capabilities.subagents.background_prompts` auto-policy
157 /// (`"auto_policy"` or `"parent"`) is configured — a detached child
158 /// cannot prompt interactively, so this is enforced fail-closed at
159 /// spawn time, defensively re-checking what
160 /// `crate::configfile::validate_modules`'s C6 resolver rule already
161 /// requires at config-resolve time (belt-and-suspenders for a `Config`
162 /// hand-built via [`crate::ConfigBuilder`] that bypassed the resolver).
163 #[error(
164 "subagent spawn refused: background=true requires \
165 capabilities.subagents.background_prompts = \"auto_policy\" or \"parent\" (§2.2 C6) \
166 — none is configured"
167 )]
168 SubagentBackgroundPolicyMissing,
169
170 /// P5-3: `spawn_subagent`'s `agent_type` named an agent definition not
171 /// present in `capabilities.subagents.agents`.
172 #[error("unknown subagent agent_type `{0}` — not defined in capabilities.subagents.agents")]
173 SubagentDefinitionNotFound(String),
174
175 /// P5-3: `subagent_status` (or an internal join) named a subagent id
176 /// this agent never spawned (or one already reaped).
177 #[error("unknown subagent id `{0}`")]
178 SubagentNotFound(String),
179
180 /// P5-6 (§2 module 4 `tools.background`, resource bound): a
181 /// `background_exec` call was refused because
182 /// `capabilities.tools_background.max_concurrent` background jobs are
183 /// already running for this agent — fail-closed, mirroring
184 /// [`Error::SubagentConcurrencyExceeded`]'s cap treatment (§2 module
185 /// 9).
186 #[error(
187 "background exec refused: {max_concurrent} background job(s) already running \
188 (capabilities.tools_background.max_concurrent={max_concurrent})"
189 )]
190 BackgroundJobConcurrencyExceeded {
191 /// The configured cap.
192 max_concurrent: usize,
193 },
194
195 /// P5-6: `background_status`/`background_kill` named a job id this
196 /// agent never spawned (or one already reaped after finishing).
197 #[error("unknown background job id `{0}`")]
198 BackgroundJobNotFound(String),
199
200 /// A reversible reduction invariant or sidecar pointer check failed.
201 #[error(transparent)]
202 Reduction(#[from] supercode_reduce::ReductionError),
203
204 /// Catch-all for everything else.
205 #[error("{0}")]
206 Other(String),
207}
208
209impl Error {
210 /// Convenience constructor for a tool failure.
211 pub fn tool(tool: impl Into<String>, message: impl Into<String>) -> Self {
212 Error::Tool {
213 tool: tool.into(),
214 message: message.into(),
215 }
216 }
217}
218
219// Kept as a manual conversion (not `#[from]`) so the `reqwest` type stays out
220// of the public API surface — see `Error::Http`.
221impl From<reqwest::Error> for Error {
222 fn from(e: reqwest::Error) -> Self {
223 Error::Http(Box::new(e))
224 }
225}