diff --git a/docs/agent-sessions/agents.mdx b/docs/agent-sessions/agents.mdx index c652af14..952eb5be 100644 --- a/docs/agent-sessions/agents.mdx +++ b/docs/agent-sessions/agents.mdx @@ -59,7 +59,7 @@ At every rung the agent is just a small directory — at most `agent.toml` + `pr ## Session-pinned config -When a session is created it **freezes** the agent's **active [revision](/agent-sessions/revisions)** — `prompt`, `model`, `runtime`, and `skills` — into the session, and pins the resolved credential too. Editing the named agent afterwards never changes a running or resumed session — its behavior is pinned for its whole life. Update an agent freely: existing sessions keep what they started with, new sessions pick up the change. (Rotating a credential's **key value** is the exception — that flows to running sessions, so a compromised key can be replaced; see [credentials](/agent-sessions/credentials).) +When a session is created it **freezes** the agent's **active [revision](/agent-sessions/revisions)** — `prompt`, `model`, `runtime`, and `skills` — into the session, and pins the resolved credential too. (A session can start with a one-off [`model`](/agent-sessions/sessions#model) override in place of the agent's, pinned the same way for that session's life.) Editing the named agent afterwards never changes a running or resumed session — its behavior is pinned for its whole life. Update an agent freely: existing sessions keep what they started with, new sessions pick up the change. (Rotating a credential's **key value** is the exception — that flows to running sessions, so a compromised key can be replaced; see [credentials](/agent-sessions/credentials).) ## Model diff --git a/docs/agent-sessions/api-reference.mdx b/docs/agent-sessions/api-reference.mdx index 0ea10585..abaa5fd9 100644 --- a/docs/agent-sessions/api-reference.mdx +++ b/docs/agent-sessions/api-reference.mdx @@ -79,6 +79,7 @@ oc agent deploy # deploy the agent directory in the cwd - `input` — the task (string or [envelope](/agent-sessions/messaging#message-input)). Truncate large inputs; let the agent fetch the rest via [sandbox tools](/agent-sessions/runtime-tools). - `revision?` — pin a specific revision (number / `rev_…`); default = the agent's active one. Use to test a [staged](/agent-sessions/revisions#staging-vs-activating) revision. +- `model?` — run this session on a different model than the agent's (same `provider/model` form; its provider must match the agent's [runtime](/agent-sessions/runtimes), e.g. `anthropic/…` for `claude`). Default = the agent's model. Pinned for the session's life like the rest of the snapshot. Rejected (`400 invalid`) for [flue](/agent-sessions/flue) agents — their model is fixed in the deployed artifact. - `key?` — get-or-create (one session per key). Keyless: an `Idempotency-Key` header makes create retry-safe. - `metadata?` — opaque routing state (≤ 16 KB); on get/list + **verbatim in webhooks**; never sent to the model, not indexed. - `limits?` `{ tokens, turn_seconds, turns }` — non-monetary; tripping one ends the turn (matching `yield_reason`). diff --git a/docs/agent-sessions/flue.mdx b/docs/agent-sessions/flue.mdx index afcf20d0..b5b3a821 100644 --- a/docs/agent-sessions/flue.mdx +++ b/docs/agent-sessions/flue.mdx @@ -91,7 +91,7 @@ isn't in the artifact. | Sandbox | You pick one (`local()`, containers, …) | **Leave `sandbox:` unset** — the session's workspace sandbox is supplied (build error) | | Persistence | `db.ts` (or volatile in-memory default) | **No `db.ts`** — the conversation is stored durably in the session's state; survives restarts and hibernation (build error) | | Model credentials | Env keys or `registerProvider` in code | **No keys anywhere in the app** — the deploy scans the artifact for key-shaped strings and fails on a hit. Managed billing or an [OC credential](/agent-sessions/credentials); routing is injected at run time | -| Model choice | In code | Still in code — but declared three times total (`defineAgent`, `agent.toml`, the OC agent) and all three **must be equal**; the deploy rejects divergence | +| Model choice | In code | Still in code — but declared three times total (`defineAgent`, `agent.toml`, the OC agent) and all three **must be equal**; the deploy rejects divergence. Fixed for good at deploy: a [session](/agent-sessions/sessions#model) **can't override** a flue agent's model — passing `model` on a flue session is rejected | | Skills | Packaged imports (`with {type:'skill'}`) or workspace files | **`src/skills//SKILL.md`** — ships with every deploy. Packaged imports are a build error (for now) | | Asking the user | No human-in-the-loop primitive | An **`ask` tool** is added: the session yields `needs_input` and hibernates until the user replies | | Channels / workflows | Slack/Discord ingress, `defineWorkflow` | Not supported — inbound is session messages + [GitHub watches](/agent-sessions/watches) | diff --git a/docs/agent-sessions/runtimes.mdx b/docs/agent-sessions/runtimes.mdx index 215b99ac..0aa36e97 100644 --- a/docs/agent-sessions/runtimes.mdx +++ b/docs/agent-sessions/runtimes.mdx @@ -13,7 +13,7 @@ A **runtime** executes the agent loop: provider SDK, model calls, and tool use. | `pi` | `anthropic/…` today — the runtime is provider-agnostic by construction; more providers land next | Managed, or an Anthropic key | | `flue` experimental | `anthropic/…` today — bring your own [Flue](/agent-sessions/flue) app | Managed, or an Anthropic key | -`model` is passed straight through to the provider, so any model that provider serves works — only the **`provider/` prefix** is validated against the runtime (a bad prefix is rejected at agent create, a model the provider doesn't recognize fails on the first turn). Common choices: +`model` is passed straight through to the provider, so any model that provider serves works — only the **`provider/` prefix** is validated against the runtime (a bad prefix is rejected at agent create, a model the provider doesn't recognize fails on the first turn). A [session](/agent-sessions/sessions#model) can override this per run — pass `model` at create to run one session on a different model than its agent, validated the same way (brain-box runtimes `claude`/`codex`/`pi`; not `flue`, which pins its model in the artifact). Common choices: - **`claude`:** `anthropic/claude-sonnet-5` (default, balanced), `anthropic/claude-opus-4-8` (most capable), `anthropic/claude-fable-5`, `anthropic/claude-haiku-4-5` (fastest/cheapest). - **`codex`:** `openai/gpt-5-codex`. diff --git a/docs/agent-sessions/sessions.mdx b/docs/agent-sessions/sessions.mdx index 587e1337..58ae1137 100644 --- a/docs/agent-sessions/sessions.mdx +++ b/docs/agent-sessions/sessions.mdx @@ -10,7 +10,7 @@ A **session** is an [agent](/agent-sessions/agents) run with an append-only [eve ## Session flow -1. You create a session from an [agent](/agent-sessions/agents). It **pins the agent's active [revision](/agent-sessions/revisions)** (prompt / model / skills) for its whole life — editing the agent later never affects a running session. Pass `revision` to pin a specific one instead (e.g. to test a [staged](/agent-sessions/revisions#staging-vs-activating) revision before promoting). The model runs **Managed** (billed to your credits, no key — the default) or on a [model credential](/agent-sessions/credentials) you supply. Pass [`sources`](/agent-sessions/repos) to check repos out into `/workspace/sources/` before turn 1 — the GitHub token never enters the sandbox. The session starts immediately. +1. You create a session from an [agent](/agent-sessions/agents). It **pins the agent's active [revision](/agent-sessions/revisions)** (prompt / model / skills) for its whole life — editing the agent later never affects a running session. Pass `revision` to pin a specific one instead (e.g. to test a [staged](/agent-sessions/revisions#staging-vs-activating) revision before promoting). Pass `model` to run this one session on a different model than the agent's — same `provider/model` form, and it must resolve to the agent's runtime's provider ([flue](/agent-sessions/flue) agents don't take an override; their model is fixed in the artifact). The model runs **Managed** (billed to your credits, no key — the default) or on a [model credential](/agent-sessions/credentials) you supply. Pass [`sources`](/agent-sessions/repos) to check repos out into `/workspace/sources/` before turn 1 — the GitHub token never enters the sandbox. The session starts immediately. 2. The [runtime](/agent-sessions/runtimes) executes the agent in a sandbox, appending [events](/agent-sessions/events) to the log as it works. 3. With nothing left to do, the session goes **idle** and the sandbox **hibernates**. 4. A [steer](/agent-sessions/messaging) message wakes it; it resumes with its prior context. Repeat. @@ -83,6 +83,36 @@ Three independent knobs — don't overload one for another: Cap a session at create (or default it on the [agent](/agent-sessions/agents)) with `limits: { tokens, turn_seconds, turns }` — a token budget, per-turn wall-clock, and auto-run count. These are runtime limits, not spend controls; model usage is billed to your provider key (BYO) or to your OpenComputer credits (Managed). Hitting one ends the turn with the matching `yield_reason`. +## Model + +A session runs the model pinned in its agent snapshot. Pass `model` at create to run **this one session** on a different model — omit it and the session inherits the agent's model (the default). The override is the same `provider/model` form as the agent's model, and its provider must match the agent's [runtime](/agent-sessions/runtimes) (e.g. `anthropic/…` for a `claude` agent — a mismatched prefix is rejected at create, a model the provider doesn't recognize fails on the [first turn](/agent-sessions/runtimes)). Like the rest of the snapshot it's **pinned for the session's life** — there's no mid-session model switch; start another session to run another model. + +Not available for [flue](/agent-sessions/flue) agents: their model is fixed in the deployed artifact (the [model triangle](/agent-sessions/flue)), so a `model` on a flue session is rejected. + + + +```ts TypeScript SDK +const session = await oc.sessions.create({ + agent: "agt_...", + input: "Summarize the incident channel and draft a postmortem.", + model: "anthropic/claude-sonnet-5", // this session only; omit to use the agent's model +}); +``` + +```http REST API +POST https://api.opencomputer.dev/v3/sessions +Authorization: Bearer $OPENCOMPUTER_API_KEY +Content-Type: application/json + +{ + "agent": "agt_...", + "input": "Summarize the incident channel and draft a postmortem.", + "model": "anthropic/claude-sonnet-5" +} +``` + + + ## Stop a session `POST /sessions/:id/cancel` **requests** a cooperative stop: the turn ends at its next checkpoint with a `turn.completed` (`yield_reason: "canceled"`), so an in-flight model call may run to its timeout — billing for that turn stops when it actually exits, not the instant you call. `POST /sessions/:id/archive` cancels any active turn, then closes the session read-only. diff --git a/sdks/typescript/package-lock.json b/sdks/typescript/package-lock.json index df80ae95..fc041002 100644 --- a/sdks/typescript/package-lock.json +++ b/sdks/typescript/package-lock.json @@ -1,12 +1,12 @@ { "name": "@opencomputer/sdk", - "version": "0.11.2", + "version": "0.12.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@opencomputer/sdk", - "version": "0.11.2", + "version": "0.12.0", "license": "MIT", "devDependencies": { "@types/node": "^25.3.0", diff --git a/sdks/typescript/package.json b/sdks/typescript/package.json index ebf58bab..1a6294e5 100644 --- a/sdks/typescript/package.json +++ b/sdks/typescript/package.json @@ -1,6 +1,6 @@ { "name": "@opencomputer/sdk", - "version": "0.11.2", + "version": "0.12.0", "description": "TypeScript SDK for OpenComputer - cloud sandbox platform", "type": "module", "main": "dist/index.js", diff --git a/sdks/typescript/src/agents/sessions.ts b/sdks/typescript/src/agents/sessions.ts index 989b8a5b..68d1c713 100644 --- a/sdks/typescript/src/agents/sessions.ts +++ b/sdks/typescript/src/agents/sessions.ts @@ -14,6 +14,13 @@ export interface CreateSessionParams { * test a staged revision before promoting it. Defaults to the active revision. */ revision?: number | string; + /** + * Override the agent's model for this session — the session runs this model instead of the + * one pinned by the agent's revision. Same `provider/model` form as the agent's model, and + * must resolve to the agent's runtime's provider. Defaults to the agent's model. Not + * supported for `flue` agents (their model is fixed in the deployed artifact). + */ + model?: string; /** get-or-create idempotency/routing key — one session per key. */ key?: string; webhook?: string; diff --git a/web/src/api/client.ts b/web/src/api/client.ts index c298e172..14b626af 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -703,6 +703,9 @@ export const createSession = ( body: { agent: string input: string + // Optional per-session model override — runs this model instead of the agent's. + // Omit to inherit the agent's model. Not supported for flue agents. + model?: string sources?: { repo: string; ref: string; name?: string }[] }, idempotencyKey?: string, diff --git a/web/src/pages/Sessions.tsx b/web/src/pages/Sessions.tsx index 6b08e506..c414adb3 100644 --- a/web/src/pages/Sessions.tsx +++ b/web/src/pages/Sessions.tsx @@ -18,6 +18,7 @@ import { DialogTitle, } from '@/components/ui/dialog' import { Field, Label, Select } from '@/components/form' +import { Input } from '@/components/ui/input' import { WorkingRepoField, type WorkingRepo, @@ -45,12 +46,20 @@ export default function Sessions() { const [showStart, setShowStart] = useState(false) const [agentId, setAgentId] = useState('') const [message, setMessage] = useState('') + // Optional per-session model override — empty inherits the agent's model + // (cleared when the agent changes, since a model is runtime-specific). + const [model, setModel] = useState('') // Optional working repo — explicitly chosen, per-agent (cleared when the agent changes). const [workingRepo, setWorkingRepo] = useState(null) + const selectedAgent = agents?.find((a) => a.id === agentId) + // flue agents pin their model in the deployed artifact — no per-session override. + const isFlueAgent = selectedAgent?.runtime === 'flue' + const openStart = () => { setAgentId(agents?.[0]?.id ?? '') setMessage('') + setModel('') setWorkingRepo(null) setShowStart(true) } @@ -61,6 +70,8 @@ export default function Sessions() { { agent: agentId, input: message.trim(), + // Only send a model when overridden — omitting it inherits the agent's. + ...(model.trim() && !isFlueAgent ? { model: model.trim() } : {}), ...(workingRepo ? { sources: [{ repo: workingRepo.repo, ref: workingRepo.ref }] } : {}), @@ -217,6 +228,7 @@ export default function Sessions() { value={agentId} onValueChange={(v) => { setAgentId(v) + setModel('') // a model override is per-agent (runtime-specific) setWorkingRepo(null) // a working repo is per-agent }} options={(agents ?? []).map((a) => ({ @@ -243,6 +255,26 @@ export default function Sessions() { className="min-h-24" /> + {agentId && !isFlueAgent ? ( + + setModel(e.target.value)} + placeholder={selectedAgent?.model ?? 'provider/model'} + autoComplete="off" + spellCheck={false} + /> + + ) : null} {agentId ? (