Skip to main content

RuntimeBuilder

Struct RuntimeBuilder 

Source
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

Source

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.

Source

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.

Source

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

Source

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.

Source

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.

Source

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

Source

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.

Source

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

Source

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.

Source

pub fn with_registered_provider<P>(self, provider: P) -> Self
where 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.

Source

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.

Source

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.

Source

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).

Source

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.

Source

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.

Source

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.

Source

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.

Source

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

Source

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.

Source

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.

Source

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.

Source

pub fn with_tool<T>(self, tool: T) -> Self
where 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.

Source

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.

Source

pub fn with_task_scope<F>(self, scope: F) -> Self
where F: Fn(TaskFuture) -> TaskFuture + Send + Sync + 'static,

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.

Source

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.

Source

pub fn with_child_policy<F>(self, policy: F) -> Self
where F: Fn(&ChildContext<'_>) -> ChildSpec + Send + Sync + 'static,

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.

Source

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.

Source

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.

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl Default for RuntimeBuilder

Source§

fn default() -> Self

Returns the “default value” for a type. Read more

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<ST, DT> CastableFrom<ST, Initialized, Initialized> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<ST, DT> CastableFrom<ST, Uninit, Uninit> for DT
where ST: ?Sized, DT: ?Sized,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self> ⓘ

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self> ⓘ

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> Pointable for T

Source§

const ALIGN: usize

The alignment of pointer.
Source§

type Init = T

The type for initializers.
Source§

unsafe fn init(init: <T as Pointable>::Init) -> usize

Initializes a with the given initializer. Read more
Source§

unsafe fn deref<'a>(ptr: usize) -> &'a T

Dereferences the given pointer. Read more
Source§

unsafe fn deref_mut<'a>(ptr: usize) -> &'a mut T

Mutably dereferences the given pointer. Read more
Source§

unsafe fn drop(ptr: usize)

Drops the object pointed to by the given pointer. Read more
Source§

impl<T> PolicyExt for T
where T: ?Sized,

Source§

fn and<P, B, E>(self, other: P) -> And<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow only if self and other return Action::Follow. Read more
Source§

fn or<P, B, E>(self, other: P) -> Or<T, P>
where T: Sized + Policy<B, E>, P: Policy<B, E>,

Create a new Policy that returns Action::Follow if either self or other returns Action::Follow. Read more
Source§

impl<T> Read<Exclusive, BecauseExclusive> for T
where T: ?Sized,

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = !

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, !>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<V, T> VZip<V> for T
where V: MultiLane<T>,

Source§

fn vzip(self) -> V

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self> ⓘ
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self> ⓘ

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more