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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
//! What can go wrong, named once at the root.
//!
//! [`RunError`] is the crate's universal error — opening a workspace,
//! preparing a run, driving one — and it used to live in
//! [`run`](mod@crate::run), where history put it. Four of that module's in-edges
//! (the store, the event mapping, the runtime, the budget) imported nothing
//! from `run` *but* this type, which manufactured four of the crate's import
//! cycles out of one name. At the root, an error is something every module
//! may name without owing the run module anything; `run` re-exports it, so
//! `basis::run::RunError` still reads.
use thiserror::Error;
#[cfg(feature = "mcp")]
use crate::mcp::McpError;
use crate::{context::ContextError, provider::ProviderError};
/// Anything that can go wrong opening a workspace, preparing a run, or driving
/// one.
///
/// One error type across all three, rather than a `WorkspaceError` beside it:
/// opening a workspace exists to prepare runs, and every failure listed here is
/// a failure a caller of [`run`](crate::run()) has always been able to receive.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum RunError {
#[error("prompt is empty")]
EmptyPrompt,
/// The shared allowance this turn draws on has nothing left.
///
/// A decision rather than a failure of the work, which is why it is its own
/// variant: a caller fanning out over a [`BudgetPool`](crate::BudgetPool)
/// stops minting on this, where it would retry on a provider error. Raised
/// before the prompt is sent and before the stream opens, so the
/// conversation is left exactly as it was.
#[error("the shared token budget is spent: {spent} of {limit} tokens reported")]
BudgetExhausted { limit: u64, spent: u64 },
#[error("no session to resume")]
NoSuchSession,
/// The directory named for this runtime's conversations holds a basis
/// ≤0.6 store — mentra's SQLite database — which this build neither links
/// nor migrates (ADR-0023).
///
/// basis's own words rather than mentra's: the upstream file store
/// detects the same file and names its `store-sqlite` cargo feature,
/// which is advice for a mentra embedder, not for the person whose
/// conversations are in the file. Raised before any file store is opened
/// in the directory, because an empty store beside the database would
/// read as every conversation being lost. See
/// [`store`](crate::store)'s module docs for where the check runs.
#[error(
"'{}' holds conversations from basis 0.6 or earlier (runtime.sqlite, a SQLite \
database); this build persists conversations as plain files and the database is \
not migrated. To continue an old conversation, use basis 0.6. To start new work \
here, point the store somewhere fresh (`RuntimeBuilder::with_store_dir`; for the \
CLI, `BASIS_DATA_DIR`) or move the old store directory aside",
dir.display()
)]
LegacyStore {
/// The store directory holding the pre-0.7 database.
dir: std::path::PathBuf,
},
#[error(transparent)]
Config(#[from] crate::config::ConfigError),
#[error(transparent)]
Context(#[from] ContextError),
/// Host-resolved model metadata names a provider other than the runtime's.
///
/// Raised while opening a workspace, applying a per-run profile, or
/// switching an attached [`PreparedRun`](crate::PreparedRun), before model
/// catalogue, model request, or tool activity. The mismatch cannot be
/// repaired by looking up the id: provider identity is part of the host's
/// resolved contract.
#[error(
"resolved model `{model}` belongs to provider `{model_provider}`, but the runtime uses \
`{runtime_provider}`"
)]
ResolvedModelProviderMismatch {
/// The host-resolved model id.
model: String,
/// The provider named by the model metadata.
model_provider: String,
/// The provider registered on the runtime.
runtime_provider: String,
},
/// Complete provider request options contain one or more extra headers,
/// but this runtime can persist its Mentra agent configs.
///
/// Header names and values are deliberately absent: either can itself be
/// sensitive. Use an explicitly ephemeral runtime for request-scoped
/// credentials, or configure durable connection credentials on the
/// provider instead.
#[error(
"run profile request headers require a runtime built with \
RuntimeBuilder::with_ephemeral_history"
)]
RunProfileHeadersRequireEphemeralHistory,
/// A [`RunProfile`](crate::RunProfile) field Mentra cannot change on an
/// already persisted agent.
///
/// Refused before the session is looked up or resumed, rather than
/// projecting the supported subset and silently dropping part of the
/// host's contract. Resolved model metadata and the dedicated reasoning
/// override are each supported alone through Mentra's exact session
/// setters; every other field is named here when present.
#[error("run profile field `{field}` cannot be applied while resuming a session")]
UnsupportedResumeProfile {
/// The first unsupported field in deterministic profile order.
field: &'static str,
},
/// A resumed profile model would require separately persisting both model
/// and reasoning changes, because the profile or an effective legacy
/// effort also changes reasoning.
///
/// Mentra 0.22 exposes one setter for each but no atomic combined update.
/// Refused before session lookup so a failed second write can never leave
/// half of the host's profile in force.
#[error(
"a resumed run profile cannot change model and reasoning together; \
apply only one persisted override"
)]
NonAtomicResumeProfile,
/// Discovery was disabled on a builder borrowing a shared runtime.
///
/// Mentra's runtime-global skill loader can be changed after an `Arc` is
/// borrowed, and its model-visible descriptions are read on every round.
/// No one-time inspection can therefore prove that a shared runtime stays
/// discovery-free. Gate 1a's fresh-only lifecycle fails closed before
/// runtime acquisition, model resolution, provider requests, workspace
/// tool registration, or interception; use
/// [`WorkspaceBuilder::with_runtime_builder`](crate::WorkspaceBuilder::with_runtime_builder)
/// so opening privately constructs the runtime it owns.
#[error(
"discovery-disabled workspaces cannot borrow a shared runtime; supply a fresh private \
runtime recipe with WorkspaceBuilder::with_runtime_builder"
)]
DiscoveryDisabledSharedRuntime,
/// Fresh-only ownership was requested with a borrowed runtime.
#[error(
"fresh-only workspaces cannot borrow a shared runtime; supply a fresh private runtime \
recipe with WorkspaceBuilder::with_runtime_builder"
)]
FreshOnlySharedRuntime,
/// The workspace's one independent mint/resume attempt was already used.
#[error(
"this fresh-only workspace has already attempted its one independent prepare or resume; \
open a new workspace with a fresh private runtime to try again"
)]
FreshOnlyRunAlreadyAttempted,
/// A runtime builder contains state that has exactly one owner and cannot
/// honestly be reconstructed for a second runtime.
///
/// Raised by [`RuntimeBuilder::into_reusable_recipe`](crate::RuntimeBuilder::into_reusable_recipe)
/// before a provider factory is called or a runtime is built. The named
/// component is deliberately coarse: provider and tool instances may
/// close over credentials or request state that must not reach an error.
#[error("a reusable runtime recipe cannot replay one-shot {component}")]
NonReusableRuntimeComponent {
/// The one-shot part of the builder (`provider`, `host tools`, or
/// `history`).
component: &'static str,
},
/// A repeatable provider was configured on a builder consumed through the
/// synchronous one-shot build path.
///
/// Warming is asynchronous and part of the reusable contract, so silently
/// skipping it would construct a different runtime than the host asked
/// for. Convert the builder to a [`RuntimeRecipe`](crate::runtime::RuntimeRecipe)
/// and let the workspace lifecycle drive it instead.
#[error("a reusable registered provider requires RuntimeBuilder::into_reusable_recipe")]
ReusableProviderRequiresRuntimeRecipe,
/// The host's repeatable provider factory failed before a runtime existed.
#[error("the reusable runtime provider factory failed: {0}")]
RuntimeRecipeProviderFactory(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
/// A repeatable provider factory returned a provider under a different id
/// from the immutable id declared by its recipe.
#[error(
"the reusable runtime provider factory declared `{declared}` but generated `{generated}`"
)]
RuntimeRecipeProviderMismatch {
/// Provider id fixed when the recipe was created.
declared: String,
/// Provider id returned by this generation's factory.
generated: String,
},
/// The host could not warm the provider clone installed in a newly built
/// runtime; the runtime is dropped before this error is returned.
#[error("the reusable runtime provider warm-up failed: {0}")]
RuntimeRecipeProviderWarm(#[source] Box<dyn std::error::Error + Send + Sync + 'static>),
/// A reusable workspace was opened without the only supported discovery
/// posture for consume/rebuild.
#[error("reusable workspaces require WorkspaceBuilder::without_discovery")]
ReusableWorkspaceRequiresDiscoveryOff,
/// A reusable workspace was opened without the one-independent-mint gate.
#[error("reusable workspaces require WorkspaceBuilder::fresh_only")]
ReusableWorkspaceRequiresFreshOnly,
/// A reusable workspace was opened with a selector or inherited model
/// policy instead of complete host-resolved metadata.
#[error("reusable workspaces require WorkspaceBuilder::with_resolved_model")]
ReusableWorkspaceRequiresResolvedModel,
/// A reusable workspace used a deny-list/default roster whose effective
/// tool set can widen when runtime registrations change.
#[error("reusable workspaces require an exact ToolRoster::only allow-list")]
ReusableWorkspaceRequiresExactRoster,
/// Checkout tools have not yet been explicitly bound to this generation.
#[error("this reusable workspace generation has not bound its host tools")]
ReusableWorkspaceToolsUnbound,
/// Checkout tools were already bound once for this generation.
#[error("this reusable workspace generation already bound its host tools")]
ReusableWorkspaceAlreadyBound,
/// A raw Mentra runtime or session handle escaped this generation.
#[error("this reusable workspace generation exposed raw Mentra state and cannot be reused")]
ReusableWorkspaceRawAccess,
/// Rebuild sealed this generation, so it cannot mint another run.
#[error("this reusable workspace generation is sealed for rebuild")]
ReusableWorkspaceSealed,
/// One or more runs, observer guards, or detached event forwarders still
/// retain this generation.
#[error("this reusable workspace generation still has {leases} outstanding lifecycle lease(s)")]
ReusableWorkspaceOutstanding {
/// Number of live run-derived leases at the rebuild boundary.
leases: usize,
},
/// The operation requires a workspace opened from a reusable recipe.
#[error("this workspace was not opened from a reusable runtime recipe")]
WorkspaceNotReusable,
/// Basis could not recover unique ownership of the old runtime after all
/// tracked workspace registrations were dropped.
#[error("the old reusable runtime still has outstanding Basis owners")]
ReusableRuntimeNotUnique,
/// A reusable checkout supplied a host tool name the provider wire cannot
/// carry safely.
#[error("`{name}` cannot be a reusable host tool name: {reason}")]
ReusableHostToolName {
/// The complete name returned by the tool descriptor.
name: String,
/// Which provider-safe name rule it violated.
reason: &'static str,
},
#[error(transparent)]
Provider(#[from] ProviderError),
#[error("runtime error: {0}")]
Runtime(#[from] mentra::error::RuntimeError),
/// A typed turn answered, but not in the shape that was asked for.
///
/// Separate from [`Runtime`](Self::Runtime) because the two call for
/// different reactions and basis can tell them apart honestly: this one is
/// basis's own verdict. The typed path asks mentra for the raw payload and
/// deserializes it here, so a value that does not fit `T` is a schema or
/// prompt problem — retry with a clearer schema — while a provider failure
/// is not. The exchange stays in the session's transcript either way; see
/// [`PreparedRun::output`](crate::PreparedRun::output).
#[error("the run's output did not match the requested type: {0}")]
OutputMismatch(#[source] serde_json::Error),
#[error("failed to write an event: {0}")]
Sink(#[from] std::io::Error),
#[error("event forwarding task failed: {0}")]
Forwarder(#[from] tokio::task::JoinError),
#[error("failed to load skills: {0}")]
Skills(#[from] mentra::SkillLoadError),
#[error(transparent)]
#[cfg(feature = "mcp")]
Mcp(#[from] McpError),
#[error("failed to load prompt templates: {0}")]
Templates(#[from] crate::templates::TemplateError),
#[error("failed to load memories: {0}")]
Memory(#[from] crate::memory::MemoryError),
/// The blocking thread [`WorkspaceBuilder::open`](crate::WorkspaceBuilder::open)
/// runs memory discovery on (roots, per-file reads, `canonicalize`)
/// panicked or was cancelled before it returned (whole-wave review, G7).
///
/// Not `#[from]`: [`Forwarder`](Self::Forwarder) already claims
/// `tokio::task::JoinError` for the event-forwarding task, and thiserror
/// cannot generate two `From` impls for one source type on one enum — so
/// this is built by hand at the one call site that needs it.
#[error("memory discovery failed: {0}")]
MemoryDiscovery(#[source] tokio::task::JoinError),
#[error("failed to load hooks: {0}")]
Hooks(#[from] crate::hooks::HookConfigError),
#[error("failed to load declared tools: {0}")]
Tools(#[from] crate::tools::declared::DeclaredToolError),
/// A command target name that cannot be routed on
/// ([`RuntimeBuilder::with_command_target`](crate::RuntimeBuilder::with_command_target),
/// ADR-0021).
///
/// Raised by `build` rather than by a panic at the registering call,
/// because that is where this builder answers every other piece of bad
/// input — an unattributed credential is refused by `provider::resolve_with`
/// at exactly the same moment. A host reading its targets out of its own
/// configuration can then report a bad name the way it reports every other
/// bad setting, instead of losing the process to it.
#[error("`{name}` cannot be a command target name: {reason}")]
CommandTarget { name: String, reason: String },
/// A host tool ([`RuntimeBuilder::with_tool`](crate::RuntimeBuilder::with_tool))
/// whose name collides with one basis already registered — `spawn`, a
/// mentra builtin, or an earlier host tool on the same builder (decision
/// D5d).
///
/// mentra's registry is a map and its plain `with_tool` *replaces*, so
/// without this a host tool named `spawn` would silently take over the
/// name and inherit every rule an operator ever wrote about commands and
/// delegation. Raised by `build`, after basis's own registrations exist to
/// collide against, rather than a silent swap.
#[error("a host tool could not be registered: {0}")]
HostTool(#[from] mentra::tool::ToolNameCollision),
}