Skip to main content

WorkspaceBuilder

Struct WorkspaceBuilder 

Source
pub struct WorkspaceBuilder { /* private fields */ }
Expand description

How a workspace is opened.

Named a builder rather than a config because it is one: it exists to be filled in and then consumed by open. The type mentra calls WorkspaceConfig is a different thing entirely — the agent’s base directory — and basis sets that from this one rather than exposing it.

Fields are private because the embedded runtime recipe can hold a credential. with_* returns a new value, so a host can keep a half-configured builder and finish it differently per workspace.

Implementations§

Source§

impl WorkspaceBuilder

Source

pub fn new(path: impl Into<PathBuf>) -> Self

Source

pub fn with_runtime(self, runtime: Arc<Runtime>) -> Self

Borrows the host’s runtime instead of building a private one.

The N-repository shape: one Runtime built once, every workspace opened with a clone of the Arc. Provider, credential, store, and host interceptors are the runtime’s facts and cannot be re-said here; what this workspace still decides is what its repository says, plus the with_model override and its command posture.

Source

pub fn with_runtime_builder(self, runtime: RuntimeBuilder) -> Self

Supplies the recipe for this workspace’s private runtime.

open builds it bound to this workspace’s path — the per-path persist identifier and workspace-bounded policy the bare Workspace::open has always produced — so this is configuring the sugar, not switching shapes. It is also the migration path for every knob ADR-0018 moved: a one-shot caller that needs an interceptor or a store directory puts it on a RuntimeBuilder and hands it here.

Source

pub fn fresh_only(self) -> Self

Allows exactly one independent Workspace::prepare or Workspace::resume attempt from the opened workspace.

Subsequent turns on the returned crate::PreparedRun remain attached and unrestricted. The claim is irreversible even if the first attempt fails: Basis has no scrub contract for a partly minted or resumed runtime, so it cannot prove one is clean enough to retry.

Requires a private runtime recipe. A shared runtime could be minted by another workspace through a different Arc, bypassing this workspace’s gate, so open refuses that ownership shape. Direct session creation through Workspace::mentra_runtime is the raw Mentra escape hatch and is outside this supported Basis lifecycle.

Source

pub fn with_model(self, model: ModelSelector) -> Self

Overrides the runtime’s model policy, for this workspace alone.

Unset, the runtime’s with_model policy decides. Either way the resolved model is this workspace’s fact, fixed at open and reported by every run it mints.

Source

pub fn with_resolved_model(self, model: ModelInfo) -> Self

Supplies the complete model metadata this workspace must use.

Unlike with_model, this is an answer rather than a selection policy: open does not list or resolve models. The metadata, including its context window, reaches every session minted by the workspace unchanged.

The model must name the same provider as the workspace’s runtime. A mismatch is refused by open before provider or tool activity with RunError::ResolvedModelProviderMismatch. Calling this after with_model, or vice versa, replaces the earlier value.

Source

pub fn with_context(self, context: ContextConfig) -> Self

Source

pub fn without_discovery(self) -> Self

Disables every repository- and home-discovered input as one posture.

Opening still validates and resolves the workspace path, and explicit host inputs still apply: a supplied Config, private runtime recipe and provider, model, system prompt, native tools, roster, interceptors, shell posture, and compaction. What stops is file discovery and the work caused by it: context, config, hooks, declared tools, memory, skills, templates, and MCP files/connections are not probed.

Sticky by construction: no source-specific with_* setter changes this private flag, so calling one later cannot accidentally reopen a file input. Build a fresh builder to restore the default discovery posture.

This posture requires a private runtime recipe supplied through with_runtime_builder. A borrowed runtime is mutable through every other Arc holder, while Mentra reads its runtime-global skill descriptions on every round; refusing with_runtime is the only race-free way Gate 1a’s fresh-only lifecycle can guarantee that no later registration widens the prompt or roster through Basis’s builder surface. A caller that subsequently mutates Workspace::mentra_runtime has deliberately left this contract through the raw Mentra escape hatch.

Source

pub fn with_config(self, config: Config) -> Self

Supplies the config.json answers instead of discovering them.

Unset, open reads .basis/config.json and the global config.json itself, because opening a path is what reads a repository’s conventions — the same reason it reads AGENTS.md and .mcp.json without being asked to.

Two callers want to say otherwise. A host that already discovered a Config — to report it, or to apply its process half to a shared Runtime with RuntimeBuilder::with_config — hands the same value here rather than paying for the read twice. And Config::default() says nothing, which is how a host that wants its own configuration to be the only configuration turns the file off.

Whatever arrives still loses to every explicit call on this builder and on the runtime’s: this is the layer below them, never above.

Source

pub fn with_system_prompt(self, system_prompt: SystemPrompt) -> Self

Gives the host a say over the system prompt, for this workspace’s runs.

SystemPrompt::Append puts the host’s text after the discovered context, as the most specific block; SystemPrompt::Replace makes it the whole prompt and leaves discovery out of it. Unset — the default — the prompt is the rendered context and nothing else.

Workspace-level and not runtime-level, deliberately: a host serving several repositories off one shared Runtime (ADR-0018) can give each its own voice, and the prompt is settled at open into the workspace’s own AgentConfig, so runs minted from different workspaces cannot pick up each other’s.

One field, so the last call wins — and the enum makes both at once unspellable rather than undefined.

Source

pub fn with_skills(self, skills: SkillsConfig) -> Self

Source

pub fn with_memory(self, memory: MemoryConfig) -> Self

Sets where memory files are discovered, or turns discovery off.

Memory is files, not a subsystem — see crate::memory for the convention, the two default roots, and what the index costs. Unset, the convention applies: the global config directory’s memory/, plus the sibling memory/ beside the runtime’s store dir when RuntimeBuilder::with_store_dir named one. MemoryConfig::disabled reads nothing at all.

Source

pub fn with_tool_roster(self, roster: ToolRoster) -> Self

Sets which tools the model is offered, for every run this workspace mints (decision D3).

Unset, ToolRoster::default applies: exactly what every workspace has offered — spawn’s replaced doors and basis’s never-surfaced intrinsics hidden, everything else offered. Neither constructor on ToolRoster changes what is registered on the runtime; see its module docs for the two things a roster says nothing about — a sibling workspace’s tools, which its own audience keeps out of reach, and the rendered prompt, which has no opinion about the roster at all.

Source

pub fn with_mcp(self, mcp: McpConfig) -> Self

Sets which MCP servers this workspace connects.

Servers arrive from three places — the caller’s own list, the workspace’s .mcp.json, and the global one — and this is where the first of those goes. See crate::mcp for the precedence.

The connections are opened once, by open, owned by the workspace, and shared by every run minted from it — on a shared runtime they die with this workspace, not with the runtime (ADR-0018).

Source

pub fn with_templates(self, templates: TemplatesConfig) -> Self

Source

pub fn with_hooks(self, hooks: HooksConfig) -> Self

Sets the host-supplied subprocess hooks and where file hooks are discovered.

A hook is an external command that gets a say over each tool call; see crate::hooks for the wire contract and for what happens when one breaks. RuntimeBuilder::with_interceptor is the same say, in the host’s process — host scope is runtime scope. Typed HooksConfig::supplied hooks run before global and workspace file hooks; disabling discovery retains only that typed list.

Source

pub fn with_tools(self, tools: ToolsConfig) -> Self

Sets the host-supplied declared tools and where file declarations are discovered.

A declared tool is a command the workspace offers the model as a tool, with a JSON schema for its input; see crate::tools::declared for the manifest and for what a failing one tells the model. The tools are registered on the runtime this workspace borrows and deregistered — as far as mentra’s registry allows — when the workspace drops, so a repository’s tools never reach another repository’s runs. Typed ToolsConfig::supplied entries outrank workspace and global files and remain active when file discovery is disabled.

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, for this workspace alone.

The per-workspace half of what RuntimeBuilder::with_tool does process-wide, and the same ExecutableTool contract — crate::tools has what a host writes one against, and why a native tool exists at all beside a declared one.

What “for this workspace alone” means. The tool is registered for this workspace’s ToolAudience, so on a runtime serving five repositories the other four’s models are neither offered it nor able to reach it by guessing the name — mentra’s resolution ladder answers a foreign audience’s name with Hidden. A run this workspace mints sees it, and so does a subagent that run delegates to, which inherits the audience with the runtime handle it is spawned from. Nothing is frozen to achieve that: mentra rebuilds a visible set from the live registry each round, so a workspace that opens later adds tools only to its own audience.

A name that is taken refuses the open, naming it (RunError::WorkspaceHostTool) — a global this runtime already answers to, another repository’s, or one another live open of this directory supplied. That last is the one case a declaration handles differently, and the error variant carries the argument: two opens of one directory share one audience, and two dyn ExecutableTool values cannot be compared, so the second is refused rather than silently served the first’s closure. Nothing is registered when any name in the set is refused.

A host tool’s Drop must not block. Its registration is released while basis holds the lock over the runtime’s tool-name ledger, so a handler that waits on a lock, a channel or a network round trip on its way out stalls every other workspace opening or closing on that runtime. mentra drops its own handlers outside its registry lock and basis cannot: the claim and the registration have to go together or a name is briefly free with a tool still answering to it. Detached work owned only by the tool is outside what a workspace’s lifetime covers.

Call it once per tool; order is the order they are claimed in.

Source

pub fn with_shell(self, shell: ShellAccess) -> Self

Grants or denies command execution, for every run this workspace mints.

Granted by default (ADR-0013). Denying is the read-only posture: it shuts the command tools and nothing else, so it is a narrowing of what these runs do, never a claim about what the process could do.

Workspace-level because it is a statement about this repository’s runs, and carried as such: it goes into the policy every session this workspace mints (crate::Runtime), so a shared runtime holds this posture for these runs and a sibling repository’s for its own. A private runtime bakes it as well, for anything reached through Runtime::mentra_runtime.

Source

pub fn with_compaction(self, compaction: Compaction) -> Self

Sets how much of a conversation reaches the model, for every run this workspace mints.

Unset, Compaction::default applies: every tool result the model was shown stays in front of it, and mentra’s summarizing trigger is untouched. See crate::compaction for the two mechanisms and for why the default is what it is.

Workspace-level, not runtime-level, and the reason is mechanical rather than aesthetic. These numbers live on mentra’s AgentConfig, one is built per workspace by open’s agent_config, and every session this workspace mints — and every subagent that clones its config — carries that one. A runtime-level knob would have to be read back out at the same moment anyway, and could not then be varied per repository, which ADR-0018’s split is precisely about: the runtime owns what changes when the host changes, and how much history a repository’s runs keep is not that.

Source

pub async fn open(self) -> Result<Workspace, RunError>

Does all of it: discovery, runtime acquisition, model, skills, templates, hooks, MCP connections.

This is the expensive call, and the only one. Everything it settles is fixed for the life of the returned Workspace; a run minted from that workspace does no I/O of its own.

§Where the workspace is

The path is made absolute and canonical here, once, and that resolved directory is Workspace::root — a workspace opened as ., through a symlink, or with a .. in it reports the directory those spellings name, not the spelling. Everything downstream takes that one value: the agent’s base directory, the runtime’s policy roots, the hook runner’s directory, the store identifier — which also names this workspace’s tool audience — and the run header’s workspace. Nothing resolves it again, so a process that changes its working directory afterwards changes nothing about a workspace already open. A path that does not exist, or is not a directory, fails the open here rather than at the first tool call.

§What this workspace’s conversations are tagged with

Every agent minted from here carries store::runtime_identifier for this workspace, which is what makes store::list — and therefore ACP’s session/list — able to answer which conversations belong to this repository. Runtime::mint states it per session, so a shared runtime tags each workspace’s conversations with that workspace rather than with the process.

A resumed conversation used to be an exception, and it was upstream’s: mentra’s resume options carried no identifier, so a resumed session re-filed under the runtime’s own tag when it next persisted — which on a shared runtime took it out of this list. mentra 0.27 closed that (mentra#54) by retaining a resumed agent’s own stored tag instead. crate::store has the whole of it, including the older-record gap that fix opened in its place.

§What sharing a runtime shares

Skills are registered on the runtime’s single registry, so a skill one workspace registers is loadable by another’s runs for as long as both are open — an accepted consequence of sharing, and what Workspace::skills therefore reports. It ends with the workspace: the roots this open registered come off the runtime when the Workspace drops, so a sibling that outlives it stops being able to reach its skills, and a root two workspaces both registered — the user’s global ones, on any host that opens more than one repository — stays until the last of them goes. MCP tools live on the same single registry but do not travel even while both are open: every roster minted here hides the mcp__* tools of servers this workspace does not own, and every call of one is refused by this workspace’s own interception chain whether or not the roster it was offered had caught it ([crate::runtime::agents]).

Trait Implementations§

Source§

impl Debug for WorkspaceBuilder

Hand-written for the reason RuntimeBuilder’s is: the private recipe can hold a credential, and its own Debug redacts it.

Source§

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

Formats the value using the given formatter. 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