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