#[non_exhaustive]pub enum RunError {
Show 32 variants
EmptyPrompt,
BudgetExhausted {
limit: u64,
spent: u64,
},
NoSuchSession,
LegacyStore {
dir: PathBuf,
},
SessionRulesNotCleared {
agent_id: String,
error: RuntimeError,
},
WorkspaceMismatch {
agent_id: String,
workspace: PathBuf,
agent_workspace: PathBuf,
},
Config(ConfigError),
Context(ContextError),
ResolvedModelProviderMismatch {
model: String,
model_provider: String,
runtime_provider: String,
},
RunProfileHeadersRequireEphemeralHistory,
UnsupportedResumeProfile {
field: &'static str,
},
NonAtomicResumeProfile,
DiscoveryDisabledSharedRuntime,
FreshOnlySharedRuntime,
FreshOnlyRunAlreadyAttempted,
Provider(ProviderError),
Runtime(RuntimeError),
OutputMismatch(Error),
Sink(Error),
Forwarder(JoinError),
Skills(SkillLoadError),
Mcp(McpError),
Templates(TemplateError),
Memory(MemoryError),
MemoryDiscovery(JoinError),
Hooks(HookConfigError),
WorkspaceGuardConflict {
root: PathBuf,
},
Tools(DeclaredToolError),
CommandTarget {
name: String,
reason: String,
},
WorkspaceHostToolName {
name: String,
reason: String,
},
WorkspaceHostToolNameTaken {
name: String,
reason: String,
},
HostTool(ToolNameCollision),
}Expand description
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 has always been able to receive.
Variants (Non-exhaustive)§
This enum is marked as non-exhaustive
EmptyPrompt
BudgetExhausted
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
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.
NoSuchSession
LegacyStore
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’s module docs for where the check runs.
SessionRulesNotCleared
Resuming a conversation could not clear a pre-0.12 “…for this
session” approval rule the conversation’s rules.json still carries,
so the resume is refused rather than run with a grant or refusal a
person gave to an earlier, unrelated session.
mentra 0.27’s process-scoped rung (mentra#53) means a new
“…for this session” answer never touches the store — but a row an
older basis binary remembered into the durable Session scope before
this floor still sits in rules.json, and mentra still loads and
matches Session-scope rows exactly as it always has. The clear
reads rules.json before it can filter, so one corrupt, truncated, or
unwritable file fails every resume against that store until the
file is repaired or deleted. Fresh conversations, and any store no
pre-0.12 binary ever wrote to, are unaffected.
Fields
error: RuntimeErrorThe store’s own failure, naming the file it could not read or rewrite.
WorkspaceMismatch
Workspace::resume was handed a
conversation that belongs to a different workspace.
mentra’s store is keyed by agent, not by path, so an id alone says
nothing about where its conversation ran — and everything a resume
restates is this workspace’s: the policy carrying its .git
carve-out and shell posture, the tool audience deciding which of the
registry’s tools it can see, the persisted-row tag. Stamping those onto
another repository’s conversation would run it under a posture nobody
chose for it while its agent stayed based in its own directory — which
mentra’s file tools always allow writes under. So the binding is
checked against the persisted agent’s own base directory, and a
mismatch is refused before the session is handed out.
A host that means “resume one of mine” takes the id from
store::list for its own workspace, which is
where a client got it anyway.
Fields
Config(ConfigError)
Context(ContextError)
ResolvedModelProviderMismatch
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, 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.
Fields
RunProfileHeadersRequireEphemeralHistory
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.
UnsupportedResumeProfile
A 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.
NonAtomicResumeProfile
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.23 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.
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
so opening privately constructs the runtime it owns.
Fresh-only ownership was requested with a borrowed runtime.
FreshOnlyRunAlreadyAttempted
The workspace’s one independent mint/resume attempt was already used.
Provider(ProviderError)
Runtime(RuntimeError)
OutputMismatch(Error)
A typed turn answered, but not in the shape that was asked for.
Separate from 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, which delivers this
inside an OutputFailure so the report the turn
earned comes with it.
Sink(Error)
Forwarder(JoinError)
Skills(SkillLoadError)
Mcp(McpError)
Templates(TemplateError)
Memory(MemoryError)
MemoryDiscovery(JoinError)
The blocking thread 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 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.
Hooks(HookConfigError)
WorkspaceGuardConflict
Two live opens of one workspace present different interception chains.
One directory is one tool audience, and a workspace registers its chain for that audience — so a second live open of the same root either joins the registration already there, which needs the two chains to be the same, or would put a second complete chain behind one audience. The second is not a middle ground: mentra would walk both for either open’s calls, spawning every subprocess hook twice per call and feeding a non-idempotent rewrite its own output, and the first open’s sessions would be judged by a chain their caller never configured. So identical chains join and different ones are refused here.
A host that genuinely needs two hook configurations for one directory
needs two Runtimes. A host whose two opens differ
only in their supplied MCP servers — basis-host’s deliberate shape —
never meets this: the hooks come from one discovery configuration and
are equal.
Tools(DeclaredToolError)
CommandTarget
A command target name that cannot be routed on (ADR-0021).
Dormant: with with_command_target withdrawn, nothing can put a name
in front of the validation that raises this, so build currently has
no target to refuse. The variant keeps its slot in this
#[non_exhaustive] enum for the day a registration seam returns.
WorkspaceHostToolName
A host tool a workspace was given
(WorkspaceBuilder::with_tool)
under a name no tool may wear: empty, longer than a provider accepts,
outside the charset, or mcp__-prefixed, which is how mentra names a
bridged server’s tool.
The rules are the declared binding’s, called rather than restated —
what may be a tool name does not depend on which binding put it there
(crate::tools::host).
WorkspaceHostToolNameTaken
The same tool, under a name that is well formed but not available.
Apart from WorkspaceHostToolName
because the two carry different vocabularies and one sentence cannot
host both: a malformed name is something (“has an empty name”), and
an unavailable one is refused because of something else that is true.
The declared binding splits its own refusals the same way
(DeclaredToolError::Invalid beside NameTaken).
Four things can make a name unavailable: something this runtime already
answers to globally (spawn, a mentra builtin, a
RuntimeBuilder::with_tool
global); another repository open on the same runtime; another live open
of this directory; or the tool itself, if its descriptor named one
thing when the name was claimed and another when it was registered.
The third is the one a host meets by accident. One directory is one
tool audience, so two live opens of it share a namespace, and a native
tool is compiled code closing over whatever the host had when it
supplied it — there is no way to tell two of them apart, and joining
would serve the second open the first one’s closure. A host that
genuinely needs its own native tools per open of one directory needs
one Runtime per open; a declaration, which is data,
joins instead.
A sibling open that supplies nothing meets no error at all, and must not: it is refused nothing because it asked for nothing. What keeps it out of the tool it never supplied is not this variant but the pair that covers every shared-audience binding — hidden from its roster at the mint, and refused at the call by the guard in its own chain.
Either variant refuses the open rather than registering part of a set.
HostTool(ToolNameCollision)
A host tool (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.
Implementations§
Trait Implementations§
Source§impl Error for RunError
impl Error for RunError
Source§fn source(&self) -> Option<&(dyn Error + 'static)>
fn source(&self) -> Option<&(dyn Error + 'static)>
1.0.0 · Source§fn description(&self) -> &str
fn description(&self) -> &str
use the Display impl or to_string()
Source§impl From<ConfigError> for RunError
impl From<ConfigError> for RunError
Source§fn from(source: ConfigError) -> Self
fn from(source: ConfigError) -> Self
Source§impl From<ContextError> for RunError
impl From<ContextError> for RunError
Source§fn from(source: ContextError) -> Self
fn from(source: ContextError) -> Self
Source§impl From<DeclaredToolError> for RunError
impl From<DeclaredToolError> for RunError
Source§fn from(source: DeclaredToolError) -> Self
fn from(source: DeclaredToolError) -> Self
Source§impl From<HookConfigError> for RunError
impl From<HookConfigError> for RunError
Source§fn from(source: HookConfigError) -> Self
fn from(source: HookConfigError) -> Self
Source§impl From<MemoryError> for RunError
impl From<MemoryError> for RunError
Source§fn from(source: MemoryError) -> Self
fn from(source: MemoryError) -> Self
Source§impl<S> From<OutputFailure<S>> for RunError
impl<S> From<OutputFailure<S>> for RunError
Source§fn from(failure: OutputFailure<S>) -> Self
fn from(failure: OutputFailure<S>) -> Self
The error alone, worded exactly as the typed turn worded it before it
carried a report. The report — and with it the sink — is dropped here,
which is the whole cost of ? and the reason the richer type is what
the turn returns.