Skip to main content

PreparedRun

Struct PreparedRun 

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

A session and the prompt to send it. Nothing has been sent yet.

Implementations§

Source§

impl PreparedRun

Source

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.

Source

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.

Source

pub fn leaf(&self) -> Option<String>

The entry the next turn will continue from. None before anything has been said.

Source

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.

Source

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

Source

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

Source

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 that T did 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 as MalformedProviderEvent and 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, as Event::RunFinished’s stopped_byBound::TokenBudget for 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());
Source

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

Source

pub fn new(session: Session, run: RunContext) -> Self

Source

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.

Source

pub const fn bounds(&self) -> &TurnOptions

What every turn on this run may spend.

Source

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.

Source

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.

Source

pub fn header(&self) -> Event

The header line this run will open with, before anything is sent.

Source

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.

Source

pub fn session_mut(&mut self) -> &mut Session

Source

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.

Source

pub fn session_id(&self) -> String

The session’s id, which changes every time a session is created — including on resume.

Source

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.

Source

pub fn history(&self) -> &[Message]

The committed conversation so far, oldest first.

Source

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.

Source

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.

Source

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.

Source

pub fn context(&self) -> &RunContext

What this run is about, minus the session.

Source

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.

Source

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).

Source

pub fn name(&self) -> &str

The name this conversation is filed under.

Source

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.

Source

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

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.

Source

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.

Source

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.

Source

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.

Source

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.

Source

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?;
Source

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.

Trait Implementations§

Source§

impl Debug for PreparedRun

Hand-written because mentra’s Session is not Debug, and because the context documents hold whole files — a derived impl would dump them.

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, <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