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
impl WorkspaceBuilder
pub fn new(path: impl Into<PathBuf>) -> Self
Sourcepub fn with_runtime(self, runtime: Arc<Runtime>) -> Self
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.
Sourcepub fn with_runtime_builder(self, runtime: RuntimeBuilder) -> Self
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.
Sourcepub fn fresh_only(self) -> Self
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.
Sourcepub fn with_model(self, model: ModelSelector) -> Self
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.
Sourcepub fn with_resolved_model(self, model: ModelInfo) -> Self
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.
pub fn with_context(self, context: ContextConfig) -> Self
Sourcepub fn without_discovery(self) -> Self
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.
Sourcepub fn with_config(self, config: Config) -> Self
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.
Sourcepub fn with_system_prompt(self, system_prompt: SystemPrompt) -> Self
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.
pub fn with_skills(self, skills: SkillsConfig) -> Self
Sourcepub fn with_memory(self, memory: MemoryConfig) -> Self
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.
Sourcepub fn with_tool_roster(self, roster: ToolRoster) -> Self
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.
Sourcepub fn with_mcp(self, mcp: McpConfig) -> Self
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).
pub fn with_templates(self, templates: TemplatesConfig) -> Self
Sourcepub fn with_hooks(self, hooks: HooksConfig) -> Self
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.
Sourcepub fn with_tools(self, tools: ToolsConfig) -> Self
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.
Sourcepub fn with_tool<T>(self, tool: T) -> Selfwhere
T: ExecutableTool + 'static,
pub fn with_tool<T>(self, tool: T) -> Selfwhere
T: ExecutableTool + 'static,
Registers a tool the host implements, in the embedding program’s own process, 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.
Sourcepub fn with_shell(self, shell: ShellAccess) -> Self
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.
Sourcepub fn with_compaction(self, compaction: Compaction) -> Self
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.
Sourcepub async fn open(self) -> Result<Workspace, RunError>
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]).