Models and sessions
Docs/Extend/Models and sessions

Models and sessions

Edit on GitHub
docs-site/extend/models-and-session.md

An 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)RustThe model
"caller" (default)ModelRef::CallerThe model of the agent whose turn or tool call led here. A sub-agent's call uses the sub-agent's model.
"session"ModelRef::SessionThe 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>] maxUsd caps 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) and limits.inFlight. Both are in Config keys.
[extensions.digest]
maxUsd = 0.50        # dollars this extension may spend when no run pays

The 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.info says what session this is and the model's window, and what the last pass used of it.
  • session.read reads the branch the extension runs on, as a model would see it: from the last compaction, in pages. Pass next back as after to go on. A large body comes as a preview with its blob hash. session.read is read-only; an agent reads only its own branch and the chain above it, never a sibling's.
  • session.append writes a message the model sees from its next pass. It is stored as an ext.message entry, behind the session.write aspect. Use ctx.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:

byWho
personA person.
mainThe main loop. The default.
agent:<id>A sub-agent.
ext:<id> with via: input, model or agentAn 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.