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 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.
Billed by the provider, and announced on the stream. Since mentra 0.26
a summarizing pass reports its exact provider usage — one
Event::Usage line per provider sample, after the
compaction’s own pair of events — so the sink this verb borrows is
where its cost is read; a provider that reports no usage emits no
line, and basis still estimates nothing. There is deliberately no
figure on the return value: a pass is not a run and produces no
RunReport, so a caller metering it sums the
stream, exactly as a stream-only consumer meters a turn. And because
there is no run there is no run counter: a standalone pass draws
nothing from Bounds::token_budget or a
BudgetPool — unlike a compaction inside a turn,
whose samples land in RunReport::usage and are
charged against both (see RunUsage).
A failure reaches the sink as one Event::Error before
it reaches the caller as an Err, so a client watching the stream is
told why a conversation it expected to shrink did not. The transcript
is untouched in that case and the next turn goes out on the history
this pass did not shorten.
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: a handful of events — the announcement pair, then one usage line per provider sample — and a value are the whole of what happened.
Bounded by whatever the run itself was configured with
(bounds) and by nothing else. For a pass a caller
can take back — a stop button, a deadline of its own — see
compact_with_options, which this is.
Sourcepub async fn compact_with_options<S: EventSink>(
&mut self,
instructions: Option<&str>,
sink: &mut S,
options: TurnOptions,
) -> Result<Option<Compacted>, RunError>
pub async fn compact_with_options<S: EventSink>( &mut self, instructions: Option<&str>, sink: &mut S, options: TurnOptions, ) -> Result<Option<Compacted>, RunError>
Compacts this conversation now, under bounds the caller can trip.
compact with a way to stop it. A summarizing pass is
a full provider round trip over the longest transcript this
conversation has ever had — which is exactly the moment a person
reaches for stop — and until mentra 0.24 it ran to completion whatever
the caller did afterwards. options’ cancellation token and deadline
are the two that apply, so a host driving /compact from a UI gets the
same cancel it already has over a turn.
Only those two, deliberately, and the choice is mentra’s own
(CompactionBounds::from_run_options): a graceful
stop ends a turn at its next round
boundary with everything committed, and abandoning a summary half-way
is not that. The tool and token budgets do not apply either — a
standalone pass is not a run and has no run counter to charge, so
even though its usage is reported on the stream (see
compact) there is no allowance for these bounds to
enforce — and a spent BudgetPool does not
refuse a pass for the same reason. A compaction inside a turn is the
other way around on every count: it inherits the turn’s bounds and its
usage is charged to the turn’s budgets.
A bound left unset falls back to the run’s own, exactly as it does for
send_with_options: attaching a token says
something about stopping, not about limits.
Reaching a bound leaves the transcript exactly as it was — mentra
checks before it issues the request and again while it is in flight,
and applies nothing it did not finish — and reports as the bound rather
than as a refused summarizer: the caller gets
RunError::Runtime carrying
RuntimeError::Cancelled or RuntimeError::DeadlineExceeded, and the
stream gets one Event::Error reading operation cancelled or deadline exceeded with recoverable false. That is the
same announcement a failed pass gets and it is the right one: a client
that expected the conversation to shrink is owed a line saying it did
not, and the two cases are told apart by what the line says — mentra
classifies both bounds as terminal, so recoverable never invites a
retry into a stop somebody asked for.
With one exception, and a caller relying on the bound has to know it:
a pass with nothing to summarize answers Ok(None) before the bounds
are consulted at all. mentra returns on the empty prefix first and
checks the bounds only once there is work worth an artifact (mentra
0.24.0 src/compaction.rs, StandardCompactionEngine::compact), which
is the right order for the check’s own purpose — a run already past its
allowance should leave no snapshot behind — but it means an
already-cancelled pass on a conversation whose whole transcript is the
protected tail succeeds emptily. So Ok(None) says “there was nothing
to compact”, never “your cancel did not arrive”: a host that must not
carry on after the stop it asked for reads its own token, not this
return value. basis does not check the bounds itself ahead of the call
to close the gap — that would be a second opinion about a check the
runtime owns (ADR-0005), and it would turn a truthful Ok(None) into
an error on the one path where nothing was going to happen anyway.
Source§impl PreparedRun
impl PreparedRun
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.
Sourcepub fn register_agent_event_tap(
&self,
tap: impl Fn(&AgentEvent) + Send + Sync + 'static,
) -> AgentEventTapGuard
pub fn register_agent_event_tap( &self, tap: impl Fn(&AgentEvent) + Send + Sync + 'static, ) -> AgentEventTapGuard
Registers a lossless in-process observer for this run’s agent events.
The callback receives Mentra’s complete AgentEvent values unchanged,
synchronously and in occurrence order, before they enter the bounded
broadcast stream. In particular, tool calls and results retain their
complete provider-neutral input, structured content, and error payloads.
A cancellation ends the sequence with AgentEvent::RunFailed.
The callback executes inline on the operation emitting the event. It must return promptly and must not block or panic: blocking stalls that operation, and a panic propagates through it. It must not re-enter an event-emitting operation or drop a tap guard from inside the callback.
Keep the returned AgentEventTapGuard alive for as long as observation
is required. Dropping it waits for any invocation already in flight and
then unregisters; do not drop it while holding a resource that callback
needs. Registration does not replay earlier events.
Sourcepub fn session_mut(&mut self) -> &mut Session
pub fn session_mut(&mut self) -> &mut Session
Mutably exposes Mentra’s session.
Replaces the tool authorizer for this session only, over the one its runtime installed.
The runtime’s authorizer is basis’s own
ApprovalGate, which answers nothing: every
consequential call comes back as a Prompt, and a Prompt is resolved
by a remembered rule before the run’s Approver is
ever consulted. That order is mentra’s and it is deliberate — but it
means a policy the approver enforces can be pre-empted by a durable
rule someone seeded through
session().permission_handle(). An authorizer’s own
Allow and Deny are terminal instead: mentra returns them unchanged,
consulting no rule and emitting no permission request. A host with a
posture that must not be answerable by a remembered rule puts it here
rather than in its approver.
Scoped to this session’s agent and the descendants it spawns after this
call. Sibling sessions keep the runtime’s. Live-only: mentra does
not persist the attachment, so a conversation picked back up with
Workspace::resume arrives without it and
must be given it again — the reason basis-acp installs its mode gate
in AcpSession::new, which both session/new and session/load go
through.
Install before the first turn, which is what taking the run by value enforces at the type level: a run already lent to a turn cannot be moved. A subagent template taken from the session earlier, and any child already spawned, keep the authorizer they inherited.
The replacement is total, not layered — mentra has no way to hand back
the runtime authorizer being displaced, so an implementation here owns
the whole question, ApprovalGate’s
is_consequential filter included.
One that answered Prompt for a read would put every read to whoever
is approving.
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 text_history(
&self,
) -> impl DoubleEndedIterator<Item = (Role, String)> + '_
pub fn text_history( &self, ) -> impl DoubleEndedIterator<Item = (Role, String)> + '_
This run’s committed conversation as runtime roles plus assembled text.
Narrower than history: only the two chat roles a host
commonly needs, and only the assembled text each one said. Unknown roles
stay in history() for callers that want the full transcript.
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 last_assistant_text(&self) -> Option<String>
pub fn last_assistant_text(&self) -> Option<String>
The newest assistant text this run’s history has committed, if any.
The text answered_turns only counts. A host
recovering from a crash mid-turn asks two questions in sequence — did
the recorded prompt already get answered, and what was the answer —
and both are questions about mentra::Role that basis can settle
without pulling the caller into matching on it (ADR-0003: the
in-process Rust consumer is what the API is judged by; basis-tasks
carried a whole mentra dependency for this one match before this
existed).
None when nothing has answered yet, which a fresh run and a resumed
conversation with no assistant turn both are. Owned rather than
borrowed because the text is assembled from the message’s parts, not
stored as one string.
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 whose lossy
set_model wrapper has since moved onto a model
named by id alone; the complete
set_resolved_model preserves a supplied
window. Also None 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 may add a task-reminder banner
and a skill-description block on top of the system prompt Basis
configured — additions that are Mentra-private, so that gap is not
basis’s to close. A resumed run is estimated against the prompt the
persisted agent carries, read back off the resumed session, rather
than the workspace’s current default: the two differ whenever the
original run carried a profile override. A run built through
prepare_with_session knows no prompt at
all — there is no workspace to ask — and its floor excludes one
entirely. 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_resolved_model(&mut self, model: ModelInfo) -> Result<(), RunError>
pub fn set_resolved_model(&mut self, model: ModelInfo) -> Result<(), RunError>
Switches the complete resolved 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.
The ModelInfo is handed to mentra unchanged, including its context
window. It is not checked against a catalogue: listing would be provider
activity at switch time, and host-resolved metadata is already the
caller’s contract. An id the provider rejects fails on the next turn,
where that provider can say why. Mentra currently retains the id,
provider, and context window but neither exposes nor persists the
display name, description, or creation time, so those display fields
cannot be read back through this API.
The provider is not switchable here. Its identity must equal this run’s provider exactly; a mismatch is refused before mentra, catalogue, model-request, or tool activity. A run built on one provider’s credential and endpoint (ADR-0018) has no second connection to move to.
This is a between-turn API. A RoundStrategy
may make its own in-turn model adjustment, but that does not pass
through this method and does not rewrite basis’s header/report context.
Mentra’s setter mutates its live agent before persisting the record. If
that write fails, the returned error can therefore leave mentra’s live
session changed while basis’s RunContext still names the old model.
A host that needs failure-atomic phase switches should use volatile
history; the Nous Gate 1a attached lifecycle does so.
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 later turns to model by id, keeping the current provider.
Compatibility wrapper over set_resolved_model.
It is deliberately lossy: an id carries no context window or display
metadata, so context_window becomes None.
Hosts that already resolved a ModelInfo should use the complete API.
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_reasoning(
&mut self,
reasoning: Option<ReasoningOptions>,
) -> Result<(), RunError>
pub fn set_reasoning( &mut self, reasoning: Option<ReasoningOptions>, ) -> Result<(), RunError>
Sets complete provider-neutral reasoning options from the next turn on.
None clears the request and restores the provider’s own default. The
options are forwarded whole without rewriting any other
ProviderRequestOptions field. They
are provider-neutral: adapters that support summaries map
ReasoningOptions::summary to their wire, while adapters without a
summary control ignore that field.
Persisted and deferred exactly as
set_resolved_model is. Mentra likewise
mutates the live reasoning posture before its persistence write, so an
error can leave that live value changed; the Nous Gate 1a lifecycle
avoids that persistence edge with volatile history.
Sourcepub fn reasoning(&self) -> Option<&ReasoningOptions>
pub fn reasoning(&self) -> Option<&ReasoningOptions>
The complete reasoning options this session’s next turn will receive.
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 for one effort level from the next turn on.
Compatibility wrapper over set_reasoning.
It is deliberately lossy: changing effort through this method clears
any configured reasoning summary. A caller preserving complete options
should use set_reasoning.
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.
Source§impl PreparedRun
impl PreparedRun
Sourcepub async fn output_parts_validated_with_options<T, S, A, V>(
&mut self,
parts: Vec<PromptPart>,
reservation: OutputReservation,
validator: V,
sink: S,
approver: A,
options: TurnOptions,
) -> Result<OutputAttemptReport<T, S>, RunError>where
T: DeserializeOwned,
S: EventSink,
A: Approver,
V: Fn(&Value) -> OutputDecision + Send + Sync + 'static,
pub async fn output_parts_validated_with_options<T, S, A, V>(
&mut self,
parts: Vec<PromptPart>,
reservation: OutputReservation,
validator: V,
sink: S,
approver: A,
options: TurnOptions,
) -> Result<OutputAttemptReport<T, S>, RunError>where
T: DeserializeOwned,
S: EventSink,
A: Approver,
V: Fn(&Value) -> OutputDecision + Send + Sync + 'static,
Drive one multipart working or shaping turn whose candidate output is validated before its generated tool may terminate the run.
Once the turn begins, every in-turn ending returns an
OutputAttemptReport, including a decode mismatch, a missing output,
a tripped bound, or a runtime failure. Only setup and stream-forwarding
failures remain outer RunErrors.
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>, OutputFailure<S>>
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>, OutputFailure<S>>
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_with_options or
execute_with_approver) 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 no
OutputReport to hand back. It hands back an OutputFailure
instead, which is the same error it always returned —
OutputFailure::error — beside the RunReport
the turn earned anyway. A caller that wants only the error writes
.await.map_err(RunError::from)? and is where it was before.
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 report, asRunReport::stopped_by—Bound::TokenBudgetfor an allowance spent mid-gather — and on the stream, asEvent::RunFinished’sstopped_by. The two say the same thing, which is why the report is worth returning: a caller should not have to re-read its own event log to learn that a budget, and not a broken provider, is what it just paid for.
The stream is complete and closed in every one of those cases, and the
sink comes back inside the report either way — so a CollectingSink on
a turn that produced nothing is still readable.
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_with_approver(basis::NullSink, basis::AllowAll)
.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>, OutputFailure<S>>
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>, OutputFailure<S>>
A typed turn with explicit run options.
Same relationship to output as
send_with_options has to
send_parts: 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.
Limits and retry defaults are inherited. Cancellation, graceful stop,
and round strategy belong to one call and arrive through
send_with_options instead.
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
registrations on the runtime, 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 —
dropping a live registration is what deregisters it, 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 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.
Pass AllowAll for a run that has no approver of its
own. 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 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.
The session survives, so this can be called again — see
send_with_options for a turn with a
different prompt.
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 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 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_with_options 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_with_options
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.