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_command_target( self, name: impl Into<String>, executor: impl RuntimeExecutor + 'static, ) -> Self

Registers an executor this runtime’s commands can be routed to by name.

ADR-0021. spawn is still the model’s one door, and where a command runs is a dimension of a call through it rather than a second tool: !@<name> <command> reaches the executor registered here under name, and a command with no @ reaches the local one exactly as before. The case this exists for is basis running inside a Linux container on a macOS build machine, where cargo test belongs in the container and xcodebuild does not exist there at all.

basis ships no executors and claims nothing about what one reaches. The host writes it — SSH to a forced command, docker exec, an agent on a build box — and a target is exactly as trusted as that code. docs/targets.md has the worked pattern, what the executor receives, and the honesty this cannot be written without: routing a command elsewhere is not confinement, and nothing here may be described as a sandbox (ADR-0013).

What the executor is handed is a CommandRequest with this runtime’s fixed command environment already merged, a timeout mentra has already clamped, and the target name still on it, so one executor registered under two names can tell which it was called as. The cwd is advisory: it is a path in this process’s filesystem, and what it means on the far side is the executor’s to decide.

The trait and everything an implementation of it names are re-exported as crate::runtime’s executor types, so a host writes one against basis alone and never adds mentra to its own manifest.

A later call with the same name replaces the earlier one, the same rule with_command_environment follows. Names are [A-Za-z0-9_-]+ and may not be local, which is the wire word for here; a name that breaks either rule is a RunError::CommandTarget from build rather than a panic here, because a host reading its targets out of its own configuration should be able to report a bad one the way it reports every other bad setting.

Runtime-scoped, for ADR-0018’s reason and one of its own: a target that changed per repository would be a different machine per repository, which is not a thing a repository knows.

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_provider_instance<P>(self, provider: P) -> Self
where P: Provider + 'static,

Runs this runtime on a provider the host constructed, instead of one basis resolves.

mentra’s own seam, surfaced: an implementation of Provider — a vendor SDK already living in the host’s process, a gateway spoken to in a shape basis has no preset for, a scripted provider in a test — is registered under the id its own descriptor reports. Every workspace on this runtime resolves models against it and streams turns through it, and Runtime::provider reports its id.

An instance is an answer, not a preference. With one supplied, crate::provider’s resolution never runs: no environment variable is read, no credential is looked up, and build stays as offline as ever. The knobs resolution reads therefore cannot sit beside one — with_provider, with_base_url and with_api_key are each refused at build with ProviderError::AmbiguousProviderSource, whichever order they were called in — a named refusal, the same posture as the unattributed credential, because a silent priority is a knob that silently stopped working. A config.json’s provider and base_url yield instead (with_config fills emptiness, and the question is no longer empty), and with_wire has nothing left to say: it is read only under a base URL, and the instance speaks whatever wire it implements.

A later call replaces the earlier instance — the one-value rule every single-valued knob here follows.

The trait is re-exported at the crate root, and everything an implementation touches as crate::runtime’s provider-authoring re-exports, so a host writes one against basis alone.

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_reusable_registered_provider<P, Make, MakeError, Warm, WarmFuture, WarmError>( self, provider_id: impl Into<ProviderId>, make: Make, warm: Warm, ) -> Self
where P: Provider + Clone + 'static, Make: Fn() -> Result<P, MakeError> + Send + Sync + 'static, MakeError: Error + Send + Sync + 'static, Warm: Fn(P) -> WarmFuture + Send + Sync + 'static, WarmFuture: Future<Output = Result<(), WarmError>> + Send + 'static, WarmError: Error + Send + Sync + 'static,

Supplies the repeatable registered-provider generation used when a private runtime is consumed and rebuilt for reuse.

provider_id fixes the provider identity before any factory or warm activity, so a workspace can reject mismatched resolved model metadata without calling either closure. Every generated provider must report that same id or the generation is dropped before build and warm.

make creates exactly one fresh provider for each runtime. Basis takes one ordinary Clone for warm, moves the other clone through Mentra’s registered-provider seam, completes the runtime build, and only then calls and awaits warm. This ordering matters for connection prewarm: the clone and the installed provider share one newly created session scope, while no warm side effect occurs for a runtime that did not build. A factory or warm failure returns no runtime.

This method is a recipe input, not a synchronous-build variant. Finish with into_reusable_recipe; calling build would have no honest way to await warm and is refused. The builder must also use explicit ephemeral history and may not contain one-shot host tools.

The host-provider setters are one single-valued question. Calling this after with_provider_instance or with_registered_provider replaces it; calling either one-shot method afterward replaces this factory and makes conversion to a reusable recipe fail.

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.

Workspace::open calls this for the private runtime it builds, so the one-repository host gets it without asking. 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_wire(self, wire: Wire) -> Self

Which request format the endpoint behind with_base_url is spoken to in.

Wire::ChatCompletions by default, and that default is the point: an operator who pastes a base URL has pasted Ollama, LM Studio, vLLM, llama.cpp, or a gateway in front of one of them, and every one of those serves chat/completions alone. OpenAI’s own v1/responses is served by OpenAI — reached through the openai preset with no base URL at all — and by a handful of proxies that forward to it.

So this exists for those proxies, and for nothing else. Without it the new default would not be a default but a removal: a Responses-speaking gateway was reachable by base URL before, and one word here keeps it reachable rather than sending its operator off to build a mentra runtime by hand. Choosing wrong is not subtle — the wrong wire is a 404 on the first turn.

Read only when a base URL is set. A provider preset carries the wire its vendor speaks, so calling this without with_base_url says nothing: basis will not talk chat/completions to Anthropic because a builder asked.

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 shellspawn 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 has always run host interceptors → 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.

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 that wants a tool visible to only some workspaces still needs one runtime per audience; there is no per- workspace host-tool registration yet.

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_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 into_reusable_recipe(self) -> Result<RuntimeRecipe, RunError>

Converts this one-use builder into a repeatable private-runtime recipe.

Conversion is deliberately fallible. A concrete host provider and a host tool are values Mentra consumes; Basis cannot synthesize a second instance without changing their public APIs or guessing that a clone is safe. Durable or implicit history also survives a runtime drop, which defeats the fresh-state promise. A reusable recipe therefore requires with_reusable_registered_provider, no with_tool calls, and an explicit with_ephemeral_history posture.

All other builder values are immutable scalars or Arc-backed host policy and can be replayed without cloning request-local state. Provider creation and warming do not happen during conversion; each later build performs them once.

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.

Per-workspace file confinement needs no policy roots: mentra’s builtin file tools always allow paths under the calling agent’s own base_dir, which basis sets per workspace. What this policy grants is command execution — shell and background on, workspace-bounded’s timeouts — and a workspace that says ShellAccess::Denied is enforced per-workspace by the runtime’s hook dispatcher instead of by this shared policy.

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, <T as TryFrom<U>>::Error>

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