Skip to main content

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}