pub struct PreparedRun { /* private fields */ }Expand description
A session and the prompt to send it. Nothing has been sent yet.
Implementations§
Source§impl PreparedRun
impl PreparedRun
Sourcepub fn transcript(&self) -> Vec<TranscriptEntry>
pub fn transcript(&self) -> Vec<TranscriptEntry>
The conversation as the model sees it: the active path, oldest first.
This is the list to pick a branch point from. Entries left behind by an
earlier branch are not in it — abandoned has those.
Sourcepub fn abandoned(&self) -> Vec<TranscriptEntry>
pub fn abandoned(&self) -> Vec<TranscriptEntry>
Entries no longer on the active path, in the order they left it.
Kept rather than deleted, so a client can show what was tried and discarded. mentra cannot return to one; they are history, not a destination.
Sourcepub fn leaf(&self) -> Option<String>
pub fn leaf(&self) -> Option<String>
The entry the next turn will continue from. None before anything has
been said.
Sourcepub fn children(&self, entry: &str) -> Vec<TranscriptEntry>
pub fn children(&self, entry: &str) -> Vec<TranscriptEntry>
The entries recorded as continuing from entry, in creation order.
More than one means the conversation branched there: each is the start of a path explored from the same point, and at most one of them is on the active path. Empty for an entry this conversation does not have, which is the same answer as for a leaf — asking about an id is not an operation that can fail.
Sourcepub fn branch_from(&mut self, entry: &str) -> Result<usize, BranchError>
pub fn branch_from(&mut self, entry: &str) -> Result<usize, BranchError>
Returns to entry, so the next turn continues from there along a
different path. Answers how many entries left the active path.
entry may be anywhere in the tree. Naming a point on the active path
shortens it; naming one an earlier branch left behind returns to it.
Either way nothing is deleted — whatever leaves the path stays reachable
through children, so a client can still show it.
Branching emits SessionEvent::Branched, which basis maps to
Event::Branched. Nothing is streaming
between turns, so that event reaches only a subscriber the host holds
itself — the count returned here is what an ordinary caller reads.
Source§impl PreparedRun
impl PreparedRun
Sourcepub async fn compact<S: EventSink>(
&mut self,
instructions: Option<&str>,
sink: &mut S,
) -> Result<Option<Compacted>, RunError>
pub async fn compact<S: EventSink>( &mut self, instructions: Option<&str>, sink: &mut S, ) -> Result<Option<Compacted>, RunError>
Compacts this conversation now, without waiting for a threshold.
instructions says what to keep — “hold on to the migration plan, drop
the log spelunking” — and is added to the standing continuity
requirements rather than replacing them, so asking for one extra thing
cannot cost a caller the file paths and command outcomes every summary
needs. None asks for the standing ones alone.
This is a model call: the summary is written by the same provider the conversation runs on, and it is billed and can fail like any other request. It is not a turn, though — no prompt is committed, the transcript gains no exchange, and nothing is sent afterwards.
Ok(None) means there was nothing to compact, which is the honest
answer for a conversation that has not spoken yet: the last turn is
always preserved whole, exactly as it is for the model’s own compact
intrinsic, so a session with only that has no older prefix to summarize.
Nothing is emitted in that case either — a lone “compacting…” on a
client’s stream, with no second line, is worse than silence.
The sink is borrowed rather than taken. Every other verb on a run hands its sink back inside a report, and there is no report here: two events and a value are the whole of what happened.
Source§impl PreparedRun
impl PreparedRun
Sourcepub async fn output<T: DeserializeOwned, S: EventSink, A: Approver>(
&mut self,
prompt: impl Into<String>,
spec: OutputSpec,
sink: S,
approver: A,
) -> Result<OutputReport<T, S>, RunError>
pub async fn output<T: DeserializeOwned, S: EventSink, A: Approver>( &mut self, prompt: impl Into<String>, spec: OutputSpec, sink: S, approver: A, ) -> Result<OutputReport<T, S>, RunError>
Sends a prompt whose answer must be a value of type T rather than
prose.
ADR-0010’s structured output, and the primitive a workflow is built on:
the model is handed one terminal tool whose input is the answer, and
T is deserialized from what it sent. The caller writes the schema —
see OutputSpec for why basis derives nothing.
By default a typed turn is a shaping turn, not a working one. That
terminal tool is the only tool the turn holds — no files, no shell, no
MCP — and the model is required to call it, so the turn can answer only
from the conversation it already has. Asking it to review code in the
same call returns a structurally valid answer from a model that read
nothing, reported as a success. Two ways past that, and they are
different trades. OutputSpec::with_tools keeps the ordinary toolset
on this turn, so one call reads and answers — and gives up the forcing
that guaranteed an answer. Or do the work on an ordinary turn
(send or execute) and ask for the
shape on the next, which keeps the forcing and keeps each run’s reading
in a context of its own; examples/review_workflow.rs is that written
out.
The stream is unchanged. Header, forwarded events, permissions put to
the approver, RunFinished: a client reading events cannot tell a typed
turn from any other, which is the point — only the return value differs.
The answer travels as the terminal tool’s
ToolQueued input and
ToolCompleted summary, and
RunReport::final_message stays
None, because a typed turn’s
committed final message is that tool result — putting a JSON payload in
a field named for the assistant’s prose would have every client render
it as speech. Prose the model wrote alongside the call, usually none,
arrives as Event::AssistantMessage.
Where a plain turn reports its failure on the stream and still returns
Ok, this returns Err: a typed turn without a value has nothing to
hand back.
RunError::OutputMismatch— an answer arrived thatTdid not accept. mentra commits the exchange before basis reads it, so the transcript keeps the attempt and a follow-up turn can say what was wrong with it.RunError::Runtime— the turn failed, or it finished without ever calling the terminal tool. mentra reports both asMalformedProviderEventand basis will not read error prose to tell them apart. A working turn (OutputSpec::with_tools) reaches the second of those the most ways, since nothing forces its ending: it can answer in prose, or be refused another round by a bound while it is still gathering. Which bound that was is on the stream, asEvent::RunFinished’sstopped_by—Bound::TokenBudgetfor an allowance spent mid-gather — and only there, because the report that would otherwise carry it is not handed back when there is no value to hand back with it.
The stream is complete and closed in every one of those cases, so a sink with somewhere to put events — a file, a channel — has the whole run. Only the sink value is lost, because it comes back inside the report.
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct Review {
verdict: String,
}
let spec = basis::OutputSpec::new(
"submit_review",
"call this once you have weighed everything you read on the last turn",
json!({
"type": "object",
"properties": {
"verdict": { "type": "string", "description": "ship or hold" }
},
"required": ["verdict"]
}),
);
// The reading happened on an ordinary turn; this one only shapes it.
run.execute(basis::NullSink).await?;
let output = run
.output::<Review, _, _>(
"submit your review of what you just read",
spec,
basis::NullSink,
basis::AllowAll,
)
.await?;
// A value, not a paragraph to parse — and what it cost, for a caller
// adding runs up against a budget.
println!("{} ({} tokens)", output.value.verdict, output.report.usage.total_tokens());Sourcepub async fn output_with_options<T: DeserializeOwned, S: EventSink, A: Approver>(
&mut self,
prompt: impl Into<String>,
spec: OutputSpec,
sink: S,
approver: A,
options: TurnOptions,
) -> Result<OutputReport<T, S>, RunError>
pub async fn output_with_options<T: DeserializeOwned, S: EventSink, A: Approver>( &mut self, prompt: impl Into<String>, spec: OutputSpec, sink: S, approver: A, options: TurnOptions, ) -> Result<OutputReport<T, S>, RunError>
A typed turn with explicit run options.
Same relationship to output as
send_with_options has to
send: a typed turn is cancellable and boundable like any
other, and a fan-out that gives each of its runs a deadline should not
have to give up types to get one.
Source§impl PreparedRun
impl PreparedRun
pub fn new(session: Session, run: RunContext) -> Self
Sourcepub fn with_bounds(self, bounds: TurnOptions) -> Self
pub fn with_bounds(self, bounds: TurnOptions) -> Self
Sets what every turn on this run may spend.
Workspace installs RunSpec’s
bounds here at mint; a host that built its own session says so itself. Only the
limits are read — a cancellation token belongs to one call, not to the
run, and arrives through send_with_options.
Sourcepub const fn bounds(&self) -> &TurnOptions
pub const fn bounds(&self) -> &TurnOptions
What every turn on this run may spend.
Sourcepub fn with_workspace(self, workspace: Arc<Workspace>) -> Self
pub fn with_workspace(self, workspace: Arc<Workspace>) -> Self
Makes this run the keeper of the workspace that minted it.
A PreparedRun owns its session but only describes its workspace,
and two things live exactly as long as the workspace does: its hook
registration on the runtime’s dispatcher, and its MCP connections. A
caller that drops the workspace at mint and drives the run afterwards
runs every turn with the workspace’s hooks silently unenforced — the
dispatcher fails open for a directory no live workspace claims, which
is correct for a retired workspace and catastrophic for one that was
merely dropped early — and with its MCP servers torn down while the
minted roster still offers their tools. The free functions in
run attach the workspace here for exactly that
reason; a host that keeps the workspace itself needs nothing from this.
Sourcepub fn workspace(&self) -> Option<&Arc<Workspace>>
pub fn workspace(&self) -> Option<&Arc<Workspace>>
The workspace this run keeps alive, when it is the one keeping it.
None does not mean there is no workspace — only that someone else
holds it, which is the Workspace::prepare shape.
Sourcepub fn header(&self) -> Event
pub fn header(&self) -> Event
The header line this run will open with, before anything is sent.
Sourcepub fn session(&self) -> &Session
pub fn session(&self) -> &Session
The session this run drives, for a host that wants mentra’s own surface — branching, the transcript tree, subagents — alongside basis’s.
pub fn session_mut(&mut self) -> &mut Session
Sourcepub fn into_session(self) -> Session
pub fn into_session(self) -> Session
Gives the session back, ending basis’s involvement.
A workspace this run was keeping alive
(with_workspace) is dropped here with the
rest of the run, and its hooks and MCP connections end with it — the
session that comes back is mentra’s alone.
Sourcepub fn session_id(&self) -> String
pub fn session_id(&self) -> String
The session’s id, which changes every time a session is created — including on resume.
Sourcepub fn agent_id(&self) -> &str
pub fn agent_id(&self) -> &str
The persisted agent id: the handle
resume takes.
Unlike the session id this survives the process, because it names the row in mentra’s store rather than this run of it.
Sourcepub fn answered_turns(&self) -> usize
pub fn answered_turns(&self) -> usize
How many of the assistant’s turns this run’s history has committed.
The count, not the presence: a session resumed with --continue or
--session arrives with answers already on it, and “has this run
answered yet” is only a question a count can settle against a
watermark taken earlier — one taken right after mint, before anything
was asked, tells a caller recovering from a crash mid-turn whether the
last committed message is the crashed turn’s own answer or one it
inherited.
The fact history alone does not expose: reading it
off history() directly means matching on mentra::Role, which pulls
a host into a dependency on mentra’s own type for a question basis can
just answer. This is the narrower of the two fixes — it settles
exactly that one count rather than growing history()’s element type
a role of basis’s own, which a caller wanting the text of a message
still would not need.
Sourcepub fn context_window(&self) -> Option<usize>
pub fn context_window(&self) -> Option<usize>
This run’s model’s context window, when it is known.
Read from the live session, so it is whatever mentra is compacting
against right now. Known when the model was resolved through the
provider’s listing and that listing reports one — mentra looks a
pinned id up there too (bfe952b), so --model, a repository’s
config.json and WorkspaceBuilder::with_model all get a window when the
provider publishes one. Gemini’s listing does, as inputTokenLimit;
Anthropic’s and the OpenAI wires’ do not, and neither does a server
that cannot list. None for a run set_model has
since moved onto a model named by id alone, and for a resumed
conversation that is no longer on the model its workspace resolved —
mentra does not persist a window, and Workspace::resume reapplies
the workspace’s model only while the conversation is still on it.
Sourcepub fn estimated_context_tokens(&self) -> usize
pub fn estimated_context_tokens(&self) -> usize
Estimates how many tokens the next request would spend on this run’s
history and system prompt, using mentra’s own estimator
(mentra::memory::estimated_request_tokens) — the same one mentra’s
auto-compaction threshold is compared against.
A floor, not the real number. mentra’s actual request adds a
task-reminder banner and a skill-description block on top of the
system prompt basis configured, when either applies — both are
computed inside mentra’s own Agent::effective_system_prompt, which is
private, so nothing outside it can include them. The gap is largest
for a run with many skills registered or an overdue task reminder, and
zero for a run with neither. Useful beside
context_window for a host deciding whether to
compact or warn before mentra’s own trigger would.
Sourcepub fn context(&self) -> &RunContext
pub fn context(&self) -> &RunContext
What this run is about, minus the session.
Sourcepub fn set_model(&mut self, model: impl Into<String>) -> Result<(), RunError>
pub fn set_model(&mut self, model: impl Into<String>) -> Result<(), RunError>
Switches the model this conversation’s later turns run on, keeping the provider it was opened with.
Takes effect from the next turn: mentra threads the model into each model request as it builds it, so a turn already in flight finishes on the model it started with. It also persists — mentra rewrites the agent record — so a session resumed in another process comes back on the model it was last set to.
model is not checked against the provider’s catalogue, and
deliberately: mentra does not check either, listing models is a network
round trip, and a caller naming a model basis has never heard of is the
ordinary case for a self-hosted endpoint. An id the provider rejects
fails on the next turn, where the provider can say why.
The provider is not switchable here. mentra resolves a provider from the runtime’s registry, and a run built on one provider’s credential and endpoint (ADR-0018) has no second connection to move to.
Sourcepub fn set_name(&mut self, name: impl Into<String>) -> Result<(), RunError>
pub fn set_name(&mut self, name: impl Into<String>) -> Result<(), RunError>
Renames this conversation, and persists the new name.
The name is what store::list — and so ACP’s
session/list — reports as a conversation’s title, and mentra fixes it
at creation otherwise. That is the wrong moment for it: a session is
opened before anyone knows what it will be about, so a host that mints
one per conversation is stuck offering a list of identical placeholders.
Nothing derives a name here. What a conversation should be called is a convention of whatever is driving it — its first prompt, a ticket id, what the user typed — and basis has no opinion to impose (PROPOSAL.md Bet 4).
Sourcepub fn set_effort(&mut self, effort: Option<Effort>) -> Result<(), RunError>
pub fn set_effort(&mut self, effort: Option<Effort>) -> Result<(), RunError>
Asks the model to think harder, or less hard, from the next turn on.
None clears the request and restores the provider’s own default.
Persisted and deferred exactly as set_model is, and
for the same reason: mentra reads the level live when it builds each
model request.
A provider or model that does not offer the requested level fails the
turn rather than quietly running at a lower one — see Effort.
Sourcepub fn effort(&self) -> Option<Effort>
pub fn effort(&self) -> Option<Effort>
The level this session’s next turn will be sent with.
Read off the session rather than tracked here, which is what makes it
an answer about the conversation rather than about this handle on it: a
run whose RunSpec or whose repository’s
config.json named an effort had it applied at mint, before anything
called set_effort, and a tracked copy reported
None for a session demonstrably running at high.
None means no level is being requested — the provider’s own default —
and not that nobody has asked yet. A level mentra has grown and basis
has no name for also reads as None, because reporting the wrong one
is worse than reporting none; see Effort’s TryFrom.
Sourcepub async fn execute<S: EventSink>(
&mut self,
sink: S,
) -> Result<RunReport<S>, RunError>
pub async fn execute<S: EventSink>( &mut self, sink: S, ) -> Result<RunReport<S>, RunError>
Sends the configured prompt and streams the turn into sink.
Consequential calls are approved by AllowAll, the default for a run
that was given no approver of its own;
execute_with_approver is where anything
stricter goes.
The stream always opens with Event::RunStarted and always closes
with Event::RunFinished, including when the turn fails: by then the
stream has content a client needs to be able to finish reading.
The session survives, so this can be called again — see
send for a turn with a different prompt.
Sourcepub async fn execute_with_approver<S: EventSink, A: Approver>(
&mut self,
sink: S,
approver: A,
) -> Result<RunReport<S>, RunError>
pub async fn execute_with_approver<S: EventSink, A: Approver>( &mut self, sink: S, approver: A, ) -> Result<RunReport<S>, RunError>
Sends the configured prompt, streaming into sink and putting every
consequential call to approver.
The approver runs on the forwarding task while the turn is blocked
waiting on it, which is what makes an interactive answer possible at
all — and what means an approver must answer rather than defer. One that
cannot answer denies; see Approver.
Sourcepub async fn execute_with_options<S: EventSink>(
&mut self,
sink: S,
options: TurnOptions,
) -> Result<RunReport<S>, RunError>
pub async fn execute_with_options<S: EventSink>( &mut self, sink: S, options: TurnOptions, ) -> Result<RunReport<S>, RunError>
Sends the configured prompt with explicit run options — a cancellation token, a deadline, a tool budget.
The one-shot path is bounded by its config but had no way to be
stopped: a token belongs to one call, so it cannot travel in a config
that mints many. This is where it arrives, and it is what a host driving
a one-prompt run behind a UI needs, exactly as
send_with_options serves a conversation.
Sourcepub async fn execute_with_approver_and_options<S: EventSink, A: Approver>(
&mut self,
sink: S,
approver: A,
options: TurnOptions,
) -> Result<RunReport<S>, RunError>
pub async fn execute_with_approver_and_options<S: EventSink, A: Approver>( &mut self, sink: S, approver: A, options: TurnOptions, ) -> Result<RunReport<S>, RunError>
Sends the configured prompt with both an approver and explicit options.
Sourcepub async fn send<S: EventSink, A: Approver>(
&mut self,
prompt: impl Into<String>,
sink: S,
approver: A,
) -> Result<RunReport<S>, RunError>
pub async fn send<S: EventSink, A: Approver>( &mut self, prompt: impl Into<String>, sink: S, approver: A, ) -> Result<RunReport<S>, RunError>
Sends a further prompt on the same conversation.
This is what separates a session from a one-shot: the model sees every earlier turn, because the session was never thrown away.
Sourcepub async fn send_parts<S: EventSink, A: Approver>(
&mut self,
parts: Vec<PromptPart>,
sink: S,
approver: A,
options: TurnOptions,
) -> Result<RunReport<S>, RunError>
pub async fn send_parts<S: EventSink, A: Approver>( &mut self, parts: Vec<PromptPart>, sink: S, approver: A, options: TurnOptions, ) -> Result<RunReport<S>, RunError>
Sends a prompt that is not only text — a screenshot, a diagram, a photo of a whiteboard — on the same conversation.
Additive to send rather than replacing it, because the
overwhelming majority of turns are a line of text and should not have to
build a vector to say so. send is this with one
PromptPart::Text.
The parts reach the model in the order they are given, which is load-bearing: “look at this, and tell me what changed” reads differently depending on which side of the image the question is on.
Every provider mentra serves carries inline image bytes — the Responses
transport as a data: URL, Anthropic as a base64 source, Gemini as
inlineData — so this is portable in a way an image URL is not; see
PromptPart for why basis offers only the bytes. A media type a
particular model does not accept fails the turn, with the provider’s own
reason on the stream.
use basis::PromptPart;
run.send_parts(
vec![
PromptPart::text("this is what the page renders as"),
PromptPart::image("image/png", png),
PromptPart::text("the footer overlaps the last row — why?"),
],
basis::NullSink,
basis::AllowAll,
basis::TurnOptions::default(),
)
.await?;Sourcepub async fn send_with_options<S: EventSink, A: Approver>(
&mut self,
prompt: impl Into<String>,
sink: S,
approver: A,
options: TurnOptions,
) -> Result<RunReport<S>, RunError>
pub async fn send_with_options<S: EventSink, A: Approver>( &mut self, prompt: impl Into<String>, sink: S, approver: A, options: TurnOptions, ) -> Result<RunReport<S>, RunError>
Sends a prompt with explicit run options — a cancellation token, a deadline, a tool budget.
This is what a protocol server’s stop button needs: ACP’s
session/cancel trips the token, and the turn ends rather than running
to completion unheard.
A bound left unset here falls back to the run’s own
(bounds). Attaching a token is a statement about
stopping, not about limits, and reading it as “no deadline after all”
would quietly unbound a run its caller had configured.