pub struct RuntimeBuilder { /* private fields */ }Expand description
How a runtime is built.
Named a builder because it is one: filled in, then consumed by
build — or embedded in a
WorkspaceBuilder via
with_runtime_builder,
where Workspace::open builds it bound to the
workspace’s own path. Fields are private because one of them is a
credential. with_* returns a new value, so a host can keep a
half-configured builder and finish it differently per runtime.
Implementations§
Source§impl RuntimeBuilder
impl RuntimeBuilder
Sourcepub fn with_command_timeout(self, timeout: Duration) -> Self
pub fn with_command_timeout(self, timeout: Duration) -> Self
How long a command may run before it is killed.
Two minutes by default, which suits the commands a harness usually runs and does not suit the ones that build software. A host whose agent runs container builds, test suites, or archives needs to say so: past the limit the process is killed mid-stream, and what reaches the caller is truncated output with no error in it — a build that looks like it failed silently rather than one that was stopped.
Clamped by mentra’s ceiling for the runtime’s policy; asking for longer than that grants the ceiling rather than failing, because a host that asked for patience should not get less than the default for asking.
Sourcepub fn with_command_environment(
self,
name: impl Into<String>,
value: impl Into<String>,
) -> Self
pub fn with_command_environment( self, name: impl Into<String>, value: impl Into<String>, ) -> Self
Adds one fixed environment value to every process this runtime spawns.
Mentra clears the ambient environment before running a model command, so a host must state execution context explicitly. A later call with the same name replaces the earlier value. Debug output names variables but redacts values.
Every process is meant literally, and it did not used to be: a
command through spawn received these pairs and
a declared tool’s program did not, so a host that had told the runtime
where its service lived watched .basis/tools.json tools fail at the
far end asking for a variable the runtime was holding. Both get them
now. A declared tool’s own env block still wins for a name they share,
because that is the tool’s own statement about itself
(crate::tools::declared).
Runtime-scoped, so on a shared runtime every workspace’s commands see
the same pairs. A host that wants two concurrently driven workspaces to
carry different identities gives each its own runtime through
WorkspaceBuilder::with_runtime_builder,
which is what the local task service does.
Sourcepub fn with_hop(self, hop: u32) -> Self
pub fn with_hop(self, hop: u32) -> Self
Says the work this runtime does arrived at hop in a chain of agents — the
number of agent-to-agent hands it is from a top-level caller — so the
remote agents its sessions reach in turn are told hop + 1
(ADR-0030 §9, docs/a2a/hops-v1.md).
Zero, the default, is a top-level caller. Runtime-scoped for the
reason the command environment is: one runtime per task (ADR-0018),
and a served task’s hop is a fact about the whole task. A server
such as basis-a2a sets it from the hop the task was reached at; a
host driving its own work leaves it alone.
Source§impl RuntimeBuilder
impl RuntimeBuilder
Sourcepub fn with_gateway_ring(
self,
members: impl IntoIterator<Item = GatewayMember>,
) -> Self
pub fn with_gateway_ring( self, members: impl IntoIterator<Item = GatewayMember>, ) -> Self
Reaches the model through several gateways, preferring the first and rotating to the next when the current one keeps failing.
This is with_base_url said more than once.
Each member is built exactly as a lone base URL would be — normalized
the same way, spoken to in the wire with_wire
names, filed under the id with_provider
names — and the ring of them is registered as this runtime’s one
provider. Nothing above the provider knows there is more than one
gateway: the runtime’s retry schedule keeps pacing the attempts, and
once the current member’s streak reaches the policy’s threshold the
ring finishes that attempt on the next member.
Members must front the same upstream serving the same model. A rotation replays the conversation to the new member, and a transcript carrying one vendor’s reasoning items is refused by another vendor’s endpoint. Two relays in front of one model are a ring; two vendors are not, and basis cannot tell them apart for you.
How patient the ring is, and whether it drifts back to the preferred
member, is with_gateway_ring_policy.
What it did is with_gateway_ring_observer.
Beside with_base_url,
with_api_key, or either host-provider seam this
is refused at build: a ring answers the endpoint
question in full, so a second answer next to it has nowhere to point.
A ring with no members is refused there too. A later call replaces the
earlier members.
Sourcepub fn with_gateway_ring_policy(self, policy: GatewayRingPolicy) -> Self
pub fn with_gateway_ring_policy(self, policy: GatewayRingPolicy) -> Self
When the ring leaves a member and whether it comes back — see
GatewayRingPolicy. The default rotates after five consecutive
failures and returns to the preferred member once it has rested a
minute; GatewayRingPolicy::sticky never returns on its own.
Read only beside with_gateway_ring; alone
it says nothing.
Sourcepub fn with_gateway_ring_observer(
self,
observer: impl Fn(GatewayRingEvent) + Send + Sync + 'static,
) -> Self
pub fn with_gateway_ring_observer( self, observer: impl Fn(GatewayRingEvent) + Send + Sync + 'static, ) -> Self
Hears every counted failure, rotation, return, and recovery the ring
goes through, as GatewayRingEvents. basis has no logger of its own
and a rotation is otherwise invisible — a turn that was answered by
the fallback gateway reads exactly like one answered by the preferred
one — so a host that will ever need to know which gateway answered
wires this to its logs.
Read only beside with_gateway_ring.
Source§impl RuntimeBuilder
impl RuntimeBuilder
Sourcepub fn with_store_dir(self, dir: impl Into<PathBuf>) -> Self
pub fn with_store_dir(self, dir: impl Into<PathBuf>) -> Self
Keeps this runtime’s conversations in dir rather than in the
machine-wide default.
Unset, mentra chooses, and what it chooses is keyed by the process’s
current directory rather than by any workspace basis opened — so a host
that opens two workspaces from one place writes both histories to one
place, and a test suite writes into the user’s real data directory
whatever temp directory it opened. Two callers want to say
otherwise: a host that keeps basis’s history inside its own application
data, and a test that wants no persistent side effect at all. Both are
asking the same question — where — so that is what this takes.
with_ephemeral_history answers it with
nowhere, and is the last word between the two: whichever was called
last decides.
§What lands in the directory
dir is the store’s root, and since 0.7 what fills it is plain
files, no database (ADR-0023): agents/<id>/ holding an agent.json,
a state.json, a transcript.jsonl and a leaf, plus a rules.json
and a runs.jsonl beside them — mentra’s own file-store layout,
readable with grep and jq. Compaction snapshots go in a
transcripts/ sibling under the same root, so this one call moves
both or neither. Nothing is created until the first write, and
pointing this at
store::default_directory is
exactly the default. store::list_in is how
the same conversations are read back, and it is pointed at the same
directory.
§A directory from basis 0.6 is refused, not adopted
basis ≤0.6 kept conversations in a runtime.sqlite in this same
directory, and this build neither links SQLite nor migrates
(ADR-0023’s E2 precedent). Naming a directory that still holds one
fails build with
RunError::LegacyStore rather than
starting an empty store beside it, which would read as every
conversation having vanished. The ways forward are in the message:
basis 0.6 to continue an old conversation, or this knob pointed
somewhere fresh — BASIS_DATA_DIR for the CLI — to start new work.
§Not the store itself
Though mentra’s RuntimeBuilder::with_store would take one.
RuntimeStore is a composition of nine traits, and under the
rule written on CancellationToken — every
mentra type basis’s surface makes a caller name, basis re-exports — that
shape would cost the re-export of all nine plus the record types they
pass. What it would buy is reachable without it: between this and
with_ephemeral_history a caller
already picks durable-here or nowhere-at-all without naming a mentra
type. A caller that genuinely wants its own backend still has one, on
Runtime::mentra_runtime’s side of the bargain: build the mentra
runtime and drive it directly.
Deliberately not a per-run knob: a run describes an invocation, and
where a machine keeps its history is not something an invocation
decides. A one-shot caller that needs it opens the
Workspace itself and hands
WorkspaceBuilder::with_runtime_builder
a recipe, which is the documented migration path.
Sourcepub fn with_ephemeral_history(self) -> Self
pub fn with_ephemeral_history(self) -> Self
Keeps this runtime’s conversations in memory, and nowhere else.
The sibling of with_store_dir, for the caller
whose answer to where is nowhere. mentra’s in-memory store backs it:
nothing is written, no tool output is spilled, no directory is
created, and dropping the Runtime takes the history with it.
One file is still written, and only if a conversation gets long enough to be summarized: mentra persists a compaction snapshot before it replaces a prefix of the transcript, and does that without consulting the store. basis files those under the operating system’s temp directory, unique per runtime — never the user’s data directory and never the workspace.
Nothing survives the process. While the runtime lives a conversation
behaves as it always does — Workspace::resume
finds an agent this runtime minted, because the store lives exactly as
long as the runtime does. Past that edge there is nothing to find: a
later process cannot resume one of these by agent id, a second runtime
gets its own empty store, and
store::list_in has no file to read whichever
directory it is pointed at, so session/list over ACP reports nothing.
There is no flush and no export — a host that might want a transcript
later wants with_store_dir now.
Who asks for it. A test suite, which otherwise writes to the real database under the user’s data directory. And a host whose conversations are genuinely disposable — a request-scoped run inside a server, a one-shot classifier — where keeping a transcript is a cost and a disclosure rather than a feature.
Setting this and with_store_dir is not an
error: they write one field, so the last call wins — the same rule as
every single-valued knob on this builder, and what makes the
half-configured builder this type advertises usable.
Source§impl RuntimeBuilder
impl RuntimeBuilder
Sourcepub fn with_provider(self, provider: BuiltinProvider) -> Self
pub fn with_provider(self, provider: BuiltinProvider) -> Self
Names the provider basis resolves the credential and the models
against — one of the three knobs crate::provider’s resolution
reads, beside with_base_url and
with_api_key. Like them it cannot sit beside a
either with_provider_instance or
with_registered_provider: the
supplied provider already answers what this chooses, so
build refuses the pair by name rather than ranking it.
Sourcepub fn with_registered_provider<P>(self, provider: P) -> Selfwhere
P: Provider + 'static,
pub fn with_registered_provider<P>(self, provider: P) -> Selfwhere
P: Provider + 'static,
Runs this runtime on a low-level provider-core implementation the host constructed, instead of one basis resolves.
This is mentra’s registered-provider seam, surfaced without another
adapter: a customized
provider_core::responses::ResponsesProvider
or
provider_core::anthropic::AnthropicProvider
goes straight through mentra’s own bridge. It is distinct from
with_provider_instance, whose
Provider is mentra’s higher-level runtime trait.
A host that needs the concrete Responses session after construction may
clone the provider before passing one clone here. ResponsesProvider’s
clone shares its session state, so opening a WebSocket through the
retained clone prewarms the same connection the registered clone’s real
runs use; basis neither wraps that clone in a second provider nor
reimplements mentra’s bridge.
Like a runtime-level instance, a registered provider is an answer, not a
preference. Provider resolution and environment lookup are skipped, and
with_provider,
with_base_url, and
with_api_key are refused beside it at build time.
Calling either host-provider method again replaces the earlier answer.
Sourcepub fn with_base_url(self, base_url: impl Into<String>) -> Self
pub fn with_base_url(self, base_url: impl Into<String>) -> Self
Points the runtime at an OpenAI-compatible endpoint.
Paste the URL the server publishes. A trailing /v1 is stripped during
resolution, because every gateway advertises itself with one — that is
the form the OpenAI SDKs take — and mentra’s transports append their
own v1/…; without the strip the published URL would produce
/v1/v1/… and a 404 that names nothing.
The endpoint is spoken to in chat/completions, which is what
“OpenAI-compatible” means in the wild: Ollama, LM Studio, vLLM,
llama.cpp, and the gateways in front of them serve that wire and not
OpenAI’s own v1/responses. A proxy that does serve Responses is
reached by saying with_wire(Wire::Responses), and
such an endpoint then uses complete local replay rather than automatic
previous_response_id chaining.
Beside either host-provider seam this is refused at
build: an instance reaches its endpoint itself, so a
base URL next to one has nowhere left to point.
Sourcepub fn with_api_key(self, api_key: impl Into<String>) -> Self
pub fn with_api_key(self, api_key: impl Into<String>) -> Self
Supplies the provider credential directly, instead of having basis read it from the environment.
A host whose key lives in a vault, a keychain, or a token it just
exchanged should not have to export an environment variable for basis to
find it again. Unset by default, which is the behavior every existing
caller has: the key is looked up by the variable names the ecosystem
already uses (see crate::provider).
A key with no with_provider and no
with_base_url is refused rather than guessed
at — with nothing to attribute it to, basis would be picking a service to
send someone’s credential to.
Beside either host-provider seam this is refused at
build for the same reason: an instance authenticates
itself, so a key basis cannot hand it is a credential on its way to
being ignored.
Sourcepub fn with_model(self, model: ModelSelector) -> Self
pub fn with_model(self, model: ModelSelector) -> Self
Sets the model resolution policy: what every workspace on this runtime
resolves unless it overrides with
WorkspaceBuilder::with_model.
A policy rather than a resolved model, because resolution needs the
provider and may need the network, and both are workspace-open facts:
the resolved id stays a Workspace fact (ADR-0018).
Sourcepub fn with_config(self, config: &Config) -> Self
pub fn with_config(self, config: &Config) -> Self
Fills in what a config.json said, wherever this builder has not been
told otherwise.
The provider, the endpoint and the model policy are this builder’s
three answers that a Config can also give, and it
gives them from a file rather than from the process’s arguments — so
they go below every with_* call above and above the environment,
which build consults only for what nothing has
answered. A host calling with_provider and then this keeps its
provider; a host calling them the other way round keeps it too, because
what this reads is emptiness rather than order.
effort is not here because a runtime has no effort: it is a per-turn
request, and Workspace applies the file’s answer
as the default for a RunSpec that asked
for none.
A workspace file cannot reach base_url. Config refuses to
carry one from a repository at all (see crate::config), so what
arrives here is always the user’s own — this method needs no rule of
its own to keep that true.
The same holds for gateways, the ring said in data: at
build it becomes this builder’s
with_gateway_ring if the code stated no
ring and no base URL, keeping any policy and observer the host did
state. A host’s base URL or ring leaves the file’s ring unread, in
whichever order the two were called.
Workspace::open calls this for the private
runtime it builds, with whatever Config the host
handed the workspace — none unless it handed one: the SDK reads no
config.json it was not given (ADR-0046). A host building a shared
Runtime states its own process facts and calls this
itself if it wants a file to speak for them.
A host-supplied provider leaves the file’s provider and base_url
unread. with_provider_instance and
with_registered_provider answer the
question those keys answer, and this method only ever fills emptiness —
so they yield silently where an explicit builder call is refused by
name. The model policy still arrives: which model is asked for is
orthogonal to who answers.
Sourcepub fn with_provider_retry(self, provider_retry: ProviderRetry) -> Self
pub fn with_provider_retry(self, provider_retry: ProviderRetry) -> Self
How patiently every run minted on this runtime waits out a provider that is failing transiently.
mentra retries a transient provider error on a doubling backoff and gives up when the budget runs out. Its default — five retries after the initial call, from 500ms, capped at 5s — permits six calls and waits about 12.5 seconds before the run fails, which is tuned for a provider that hiccups and not for one that is rate-limiting you: a gateway’s 429 routinely names a window longer than that, so the whole schedule elapses inside a limit that was never going to lift, and the caller reads a provider failure where the honest answer was wait.
What a host knows that basis cannot is how long its own caller will hold still. An interactive editor session should fail fast, because somebody is watching a cursor blink; a chat bot whose turn already takes eight minutes can afford to spend one of them waiting, and would far rather do that than hand back an error the user has to re-ask. That is the judgement this knob is for, and it is why the number is the host’s rather than a constant here.
Runtime-scoped (ADR-0018) because it describes the connection to the
provider — the same kind of fact as the credential and the base URL
beside it. Every run Workspace mints on this
runtime carries it as its default; an exceptional turn can override it
with TurnOptions::with_provider_retry.
It also reaches every subagent a run delegates to through
spawn: a child that reset to the default would
be a delegated run quietly less patient than the run that delegated it,
against the same rate limit.
Unset is exactly mentra’s default, so a host that never calls this gets
the behavior it has always had. Takes mentra’s own
ProviderRetry rather than a basis type — see the re-export in
crate::runtime for why there is only one spelling of this policy.
Not a deadline. TurnOptions::with_deadline
still bounds the whole turn, and a generous schedule inside a short
deadline is bounded by the deadline. Set both, and set them knowingly.
Sets the waits, not the count. mentra keeps how many retries follow
the initial call on RunOptions::retry_budget, so widening the
schedule alone still gives up after five retries (six calls) —
with_provider_retry_budget is the
other half, and the rate-limit case above needs both.
Sourcepub fn with_provider_retry_budget(self, budget: usize) -> Self
pub fn with_provider_retry_budget(self, budget: usize) -> Self
How many times a run minted here retries a transient provider error before giving up. Five by default.
The count half of with_provider_retry,
separate because mentra keeps the two apart: the schedule is a value
with a type, the count is a bare number on each run’s options. They are
two knobs here rather than one because they are genuinely two questions
— how long between retries and how many retries — and because the
commonest adjustment is this one alone, which should not require
constructing a ProviderRetry to express.
Worth doing the arithmetic before choosing: with the default schedule the waits double from 500ms to a 5s ceiling, so raising the count from five to eight reaches about 27 seconds in total — still short of the minute a rate-limit window usually wants. Widening the schedule is what makes a larger count worth having.
Runtime-scoped by default and inherited by delegated runs, exactly as
the schedule is; a turn may override the count with
TurnOptions::with_retry_budget.
Sourcepub fn with_responses_transport(self, transport: ResponsesTransport) -> Self
pub fn with_responses_transport(self, transport: ResponsesTransport) -> Self
Which transport mentra streams the Responses wire format over.
Passed straight through to mentra, which owns both transports and the choice between them. Unset, mentra picks, and what it picks is HTTP+SSE — the transport every basis run has ever used.
Who asks for it: a host driving basis against an endpoint where the websocket transport is the point rather than an option — lower per-turn setup on a long conversation, or a gateway that only offers it. Nothing else in basis selects a transport, so before this method a host that wanted one had to build the mentra runtime itself and give up basis’s own surface to get it.
Two ways this can disappoint, and neither is basis’s to soften.
Selecting ResponsesTransport::WebSocket needs basis’s
responses-websocket feature, which forwards to mentra’s, which
forwards to mentra-provider’s and compiles the websocket client back
in. It is off by default — the default build links no websocket stack
— and without it the choice is accepted here and fails at request
time, loudly, which is mentra’s stance rather than a silent fallback
to HTTP+SSE: a host that asked for a transport should learn it did not
get one, not discover later that its traffic went the other way. The
second is the provider: not every one serves websockets — Anthropic and
Gemini report that they do not — and such a provider refuses an
explicit WebSocket at its first request, naming itself, for the same
reason and with the same loudness.
Read back through Runtime::mentra_runtime().responses_transport(),
for a host that reports its own configuration.
Sourcepub fn with_responses_state_mode(self, state_mode: ResponsesStateMode) -> Self
pub fn with_responses_state_mode(self, state_mode: ResponsesStateMode) -> Self
Chooses whether Responses requests chain a previous_response_id.
ResponsesStateMode::ReplayOnly by default, and that default is the
one basis wants: every request carries the projected transcript, and
nothing elides the items a chain would repeat, so a chained id can only
hand the endpoint the same context twice. It is also the safe answer
about whose context — a chain head belongs to a provider scope, and a
host that got that wrong would chain one conversation from another.
So this exists for the host that wants chaining anyway: an endpoint
whose pricing or routing rewards a chained id despite the repetition.
Stating Hybrid or
Stateful here says so once, for the
runtime, instead of restating a complete ProviderRequestOptions on
every run profile to change one field.
Read back through Runtime::mentra_runtime().responses_state_mode(),
beside the transport’s reader and for its reason.
Source§impl RuntimeBuilder
impl RuntimeBuilder
Sourcepub fn with_file_tools(self, file_tools: FileToolProfile) -> Self
pub fn with_file_tools(self, file_tools: FileToolProfile) -> Self
Which builtin file tools this runtime offers the model.
FileToolProfile::Split by default, which is not mentra’s default.
The roster is the model’s API, and this is the one place basis writes
it. mentra’s Batched profile registers a single files tool whose
input is an operations array over nine variants —
read/list/search/create/set/replace/insert/move/delete
— so reading one file means picking a branch out of a nine-way oneOf
and nesting the path inside an array of objects. Split registers the
six names every model in this class was trained on: read, ls,
grep, glob, write, edit. Same workspace engine underneath, same
policy, same hook points — a different surface presented to the one
consumer that cannot be given a migration note.
Two of the differences are capability rather than shape.
mentra’s grep carries glob, ignore_case, literal, context and
multiline; the batched search op hardcodes all five to their
defaults, so a case-insensitive search scoped to *.rs is not
expressible through files at all. And glob — find files whose path
matches a pattern — has no batched equivalent, so under Batched a
model that wants one reaches for a shell command instead, which is a
tool call that goes to the approver in place of a read that would not
have.
Who wants Batched back. A host whose .basis/hooks.json matchers
or whose operators’ remembered rules name files: both key on the
exact tool name, so under Split a "tools": ["files"] entry stops
matching and nothing errors — the same silent-stop ADR-0016’s
shell → spawn note describes. Choosing Batched keeps the roster
those were written against, unchanged, for as long as the host needs to
rewrite them. That is a migration path rather than an opinion; the
opinion is the default. Both exists too, and costs the model both
surfaces in its context for one engine.
Runtime-scoped (ADR-0018) because the roster is a property of the mentra runtime’s registry, which is fixed at build: every workspace on this runtime, and every subagent, is offered the same set.
Sourcepub fn with_tool_result_policy(
self,
tool_result_policy: ToolResultPolicy,
) -> Self
pub fn with_tool_result_policy( self, tool_result_policy: ToolResultPolicy, ) -> Self
Sets the limits applied to completed tool results before the next provider request.
This is intentionally narrower than Mentra’s RuntimePolicy: Basis
continues to derive filesystem, command, timeout, and process posture.
Only the result byte limit, physical-line limit, and spill posture from
tool_result_policy replace that derived policy’s corresponding
values. If this method is never called, existing Mentra defaults remain
untouched.
Sourcepub fn with_interceptor(self, interceptor: impl Interceptor + 'static) -> Self
pub fn with_interceptor(self, interceptor: impl Interceptor + 'static) -> Self
Gives the host’s own code a say over each tool call, on every workspace this runtime carries.
The in-process binding of ADR-0012’s interception contract, and the
sibling of WorkspaceBuilder::with_hooks:
same vocabulary — allow, deny with a reason, modify with a replacement
input — and the same chain. What it buys is the case a subprocess
answers badly, because the judgement needs something the embedding
program is already holding: the vault handle, the token it just
exchanged, the policy it parsed at startup. Redacting a credential out
of a tool’s input is the worked example.
Runtime-scoped because host scope is runtime scope (ADR-0018): the
chain runs host interceptors → supplied hooks → global hooks →
workspace hooks, and this is the registration point that matches the
first slot.
Appends, so a host may register several; they are consulted in the
order registered, and before any subprocess hook. The rule is that
the further a participant is from the workspace’s own data, the earlier
it speaks — an interceptor is compiled into this program, while
.basis/hooks.json came with a repository — and since the first refusal
short-circuits, that is what lets the host’s own guard stop a
repository’s program from being spawned at all. It is not a claim of
precedence: a hook still sees, and can still refuse, whatever an
interceptor rewrote.
Runtime scope is meant literally, and includes sessions basis did not
mint. These are registered globally on mentra’s chain when
build runs, so a session a host creates for itself
through Runtime::mentra_runtime, or
drives through
run::prepare_with_session, is
judged by them exactly as a workspace’s own runs are — such a session
has no tool audience, and only a global registration reaches it. That is
the one thing a workspace’s own hooks cannot say: .basis/hooks.json
belongs to a repository, is registered for that workspace’s audience,
and is not consulted for an agent that belongs to no workspace even when
its base directory is inside one. A host that wants a guard over every
call on the runtime writes it here.
Fail-closed carries over unchanged: an interceptor that returns an error or panics denies the call, and says which one it was.
Sourcepub fn with_tool<T>(self, tool: T) -> Selfwhere
T: ExecutableTool + 'static,
pub fn with_tool<T>(self, tool: T) -> Selfwhere
T: ExecutableTool + 'static,
Registers a tool the host implements, in the embedding program’s own
process — mentra’s ExecutableTool, not a crate::tools::declared
manifest entry.
The gap this closes: .basis/tools.json gives a workspace’s own repo a
tool, wrapping a subprocess that speaks JSON over stdio and sees
nothing beyond that JSON — no session, no caller identity, nothing the
host knows about the call it is answering. A host tool runs in the same
process as the code that is driving the run, so it can close over
whatever context that code already has (a client handle, a connection,
which conversation this is) instead of receiving it, or failing to.
Registered on the runtime (ADR-0018’s host scope), so — like spawn —
it is visible to every workspace and every subagent this runtime opens,
not to one session. A host whose tool belongs to one repository rather
than to the process says so with
WorkspaceBuilder::with_tool,
which registers for that workspace’s tool audience alone.
A name basis or an earlier host tool already answers to is refused,
not replaced (decision D5d). build claims host
tools only after spawn, the builtins, and everything else basis
registers unconditionally already exist on the runtime, with mentra’s
try_register_tool rather than its plain with_tool — a host tool
named spawn fails the build naming the collision
(RunError::HostTool) instead of quietly
taking over the name and every rule an operator ever wrote about
commands and delegation.
Sourcepub fn with_delegation_depth(self, depth: usize) -> Self
pub fn with_delegation_depth(self, depth: usize) -> Self
How many levels of delegation spawn will start before refusing, on
every workspace this runtime carries (decision D9).
crate::tools::DEFAULT_DELEGATION_DEPTH (two) unless a caller says
otherwise here — the smallest bound that leaves delegation
compositional (a subagent may split its own work once) while keeping
runaway recursion structurally impossible rather than merely unlikely.
The root run is depth 0, so the deepest agent that can still delegate
is one less than this value.
The guard’s shape does not move with the number: it is still basis’s own ledger (mentra’s floor is name-specific and does not fire for a registered tool), and it still refuses in the preview, so a remembered allow-rule cannot lift whatever floor is set here.
Sourcepub fn with_task_scope<F>(self, scope: F) -> Self
pub fn with_task_scope<F>(self, scope: F) -> Self
Wraps every future spawn spawns for a background child in scope
(ADR-0042).
A background child runs on a tokio task of its own, and a task-local
does not cross tokio::spawn: a host whose tools read where they are
from one — which conversation, which caller — would find it unset in
a child, and a tool that fails quietly on that is the worst kind. The
scope is called synchronously on the spawning task, before the spawn,
so the host snapshots its locals there and re-scopes them onto the
future it hands back, which is what gets spawned:
.with_task_scope(|future| {
let turn = TURN.try_with(Clone::clone).ok();
Box::pin(async move {
match turn {
Some(turn) => TURN.scope(turn, future).await,
None => future.await,
}
})
})Blocking delegations and mentra’s exclusive-lane tools run inline on the turn’s task and need nothing. mentra’s parallel lane spawns its own tasks and is not covered here; a tool that must see the locals declares an exclusive execution category.
Sourcepub fn with_delegation_width(self, width: usize) -> Self
pub fn with_delegation_width(self, width: usize) -> Self
Caps how many delegations one agent may run in the background at once (ADR-0042).
crate::tools::DEFAULT_DELEGATION_WIDTH (four) unless a caller says
otherwise here. Children share their parent’s token counter and each
is checked against the bound only after a round has spent, so N
running at once can overshoot by N rounds where a blocking child
overshoots by one; this is the N. Zero refuses every background
start, which leaves spawn as it was before ADR-0042. Checked in the
preview like the depth floor, so no approver’s answer and no
remembered rule can lift it.
Sourcepub fn with_child_policy<F>(self, policy: F) -> Self
pub fn with_child_policy<F>(self, policy: F) -> Self
Decides who a delegated child is, per delegation (decision D4).
spawn has always minted a subagent as an exact clone of its parent —
same roster, same model, same system prompt — and unset, it still
does, byte for byte. A policy makes the clone a default instead of
the only shape: consulted with what spawn knows about the delegation
(ChildContext — the child’s prompt, the parent’s agent id, the
workspace directory), it answers which of those three inherited facts
to override (ChildSpec), and ChildSpec::inherit is today’s
behavior exactly. Cheap triage beside a full fixer is the shape this
exists for: a prompt-prefix convention routed to a narrowed roster and
a cheaper model, with everything else inherited —
examples/child_policy.rs runs it.
Runtime-scoped, like the depth floor above and for the same reason:
spawn is registered on the runtime, every workspace and every
subagent on it shares the one instance, so the policy is consulted at
every depth — a child’s own delegations answer to it too, which is how
a host confines a whole chain rather than one generation.
Three facts and no more travel through a spec, deliberately. Bounds
stay on the run options a child already inherits (deadline, budgets,
cancellation, the shared token counter) — a second spelling here would
be a second bounds system. The depth floor is checked before the
policy runs, so no override lifts it. And the approver sees what the
policy decided: a delegation with overrides carries an additive
child key in its preview, so a remembered rule can match on what the
child will be, while an inherit answer leaves the preview byte-
identical to a policy-free runtime’s. ChildSpec’s module docs
carry the rest, including why a system-prompt override is
replace-wholesale with no append.
The policy should be a pure function of its context: it is consulted once for the preview and once at execution, and one that answers differently between the two shows the approver a child it will not spawn.
Sourcepub fn with_reach_policy(self, reach_policy: ReachPolicy) -> Self
pub fn with_reach_policy(self, reach_policy: ReachPolicy) -> Self
Says what spawn "@<name> …" may reach (ADR-0037 §3): a remote agent a
resolver knows, a teammate on this runtime, or — the default,
auto — whichever has the name, remote first.
The default policy allows the model every value and reads an absent
reach as auto. A host that serves only remote agents, or that
wants the behavior before ADR-0037 — an unknown name is a refusal,
never a new teammate — passes ReachPolicy::fixed(Reach::Remote).
A value the policy disallows is refused in the preview, before the
approver. Runtime-scoped, like the child policy and for the same
reason.
Sourcepub fn build(self) -> Result<Runtime, RunError>
pub fn build(self) -> Result<Runtime, RunError>
Builds the workspace-agnostic runtime: the substrate an N-repository
host hands to every WorkspaceBuilder::with_runtime.
Synchronous, and deliberately so: nothing here needs the network. The
provider is resolved (credential lookup, no request), the mentra
runtime is assembled, and that is all — MCP servers are a workspace
concern and are connected by Workspace::open,
never here.
The policy this installs is only what runs on the runtime outside any
workspace: every session a Workspace mints carries
its own complete policy instead
(Runtime::session_policy), which is what lets one shared
runtime hold one workspace’s .git carve-out, memory roots and
ShellAccess::Denied posture without imposing them on the next.
Trait Implementations§
Source§impl Debug for RuntimeBuilder
Hand-written so a supplied credential cannot reach a log through a
{:?}. Everything else is printed as it is; the command environment names
its variables and redacts their values.
impl Debug for RuntimeBuilder
Hand-written so a supplied credential cannot reach a log through a
{:?}. Everything else is printed as it is; the command environment names
its variables and redacts their values.