Models and sessions
Edit on GitHubAn extension never holds a provider key. When it needs a model, it asks the host, and the host signs the request. What it gets back is the answer, the model that gave it, and the usage. The same goes for the session: an extension reads it and writes to it through the host.
Call a model
One completion, no tools and no turn of its own. The model is the one the caller is running, unless the request names another.
// TypeScript
const reply = await ctx.model.complete(
{ prompt: "Summarise this diff in one line.", system: "Be terse.", maxOutputTokens: 200 },
{ onEvent: (e) => {} }, // optional: each event as the model streams it
);
reply.text; // the answer
reply.model; // "provider:model" that answered
reply.usage;// Rust, inside a process extension
use orrery_ext_api::facades::ModelFacade;
use orrery_ext_api::wire::{ModelCallParams, ModelRef};
let host = orrery_ext_sdk::host().expect("served by orrery");
let mut params = ModelCallParams::new(messages); // Vec<orrery_proto::Message>
params.model = ModelRef::Session; // the default is Caller
params.system = Some("Be terse.".into());
let reply = host.complete(None, params).await?; // Result<_, String>ctx.model.list() (Rust: models()) answers the models the extension may name, as provider:model.
Which model
The model field picks it. Every form is resolved by the host:
| Form (TypeScript) | Rust | The model |
|---|---|---|
"caller" (default) | ModelRef::Caller | The model of the agent whose turn or tool call led here. A sub-agent's call uses the sub-agent's model. |
"session" | ModelRef::Session | The main agent's model. |
{ config: "model" } | ModelRef::Config("model") | A setting of the extension's own, [extensions.<id>].model, which the user sets. |
{ role: "summariser" } | ModelRef::Role(…) | Whatever the role is bound to in [roles]. |
{ named: "anthropic:claude-haiku-5-5" } | ModelRef::Named("…") | A model the manifest asked for under [requires] model. |
# config.toml: the user picks the cheap model for this extension
[extensions.digest]
model = "anthropic:claude-haiku-5-5"When that model cannot be called
If the model is not set, not granted, or its provider is down, the call falls back to the caller's model. Pass fallback: "fail" (Rust: Fallback) to refuse instead.
The reply says what happened: model names the model that answered, and fell_back names the one that was asked for and was not used. The audit records both.
Options
system: the system prompt for this call.effort: as agent definitions spell it (low,medium,high). It goes on the request to the provider, as the session's own effort does.maxOutputTokens: a ceiling on the answer.onEvent(the second argument): called with each event as the model produces it, so an extension can show an answer while it is written. The reply still carries the whole answer.
A { named } model must be listed under [requires] model in the manifest; the host checks it, and a call naming any other model is refused, saying which list to add it to.
An agent's effort
ctx.agents.spawn({ task, effort: "high" }) (Rust: SpawnRequest.effort) asks for that effort on every model request the agent sends, in place of the session's. It is not added to the agent's task. Its creator's requests keep the session's effort.
Spend and limits
- The run that caused the call pays for it, as it pays for its own turns.
- With no run going, the call is charged to the extension.
[extensions.<id>] maxUsdcaps that spend; with it unset there is no cap. - A call counts toward
limits.modelCalls(model calls one extension makes in a turn, 8 by default) andlimits.inFlight. Both are in Config keys.
[extensions.digest]
maxUsd = 0.50 # dollars this extension may spend when no run paysThe session
const info = await ctx.session.info(); // session, branch, workspace, profile, model, window, used
const page = await ctx.session.read({ limit: 50 });
await ctx.session.append("The build is green on main.", { shown: "hidden" });session.infosays what session this is and the model's window, and what the last pass used of it.session.readreads the branch the extension runs on, as a model would see it: from the last compaction, in pages. Passnextback asafterto go on. A large body comes as a preview with its blob hash.session.readis read-only; an agent reads only its own branch and the chain above it, never a sibling's.session.appendwrites a message the model sees from its next pass. It is stored as anext.messageentry, behind thesession.writeaspect. Usectx.state(state.set) for data the model must not see.
Graders read sessions through the same call with grade set; an extension does not need it.
Who said it
Every stored message says who produced it, in its by field:
by | Who |
|---|---|
person | A person. |
main | The main loop. The default. |
agent:<id> | A sub-agent. |
ext:<id> with via: input, model or agent | An extension: input it sent, a model it called, or an agent it ran. |
A model call or an agent an extension runs is its own segment, under the call that caused it, labelled with the extension and model or agent. Input an extension sends is drawn as the extension's, never as the person's: see shown in Input, timers and questions.
Messages stored before this have no by; they read as person or main, and replay is unchanged.
A run an extension started with run.send stores its first message with by: ext:<id> and via: input, so a reloaded session never shows it as the person's.