# Sessions

AgentOS sessions are durable records backed by the VM's SQLite database.

- The public session ID is stable across VM sleep and adapter restarts.
- The adapter's private ACP session ID stays internal.
- The [inspector](/agentos/docs/inspector)'s Transcript tab renders the same session stream live, with a composer for prompting sessions by hand.

## Open a session

`openSession` creates or restores a session, completes ACP negotiation, and
resolves without a value.

- Choose and retain the `sessionId` before calling; omitted → `main`.
- `getSession` — call separately for durable metadata.
- Idempotent to repeat; changing immutable creation options for an existing ID
  returns `session_conflict`.

examples/sessions/create-session.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const agent = client.vm.getOrCreate("my-agent");

await agent.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
```

examples/sessions/server.ts:

```ts
import { agentOS, setup } from "@rivet-dev/agentos";
import pi from "@agentos-software/pi";

const vm = agentOS({ software: [pi] });

export const registry = setup({ use: { vm } });
registry.start();
```

Input fields: `agent`, `cwd`, `additionalDirectories`, `env`, `mcpServers`,
`permissionPolicy`, `skipOsInstructions`, `additionalInstructions`.

- Omitted `cwd` → `/home/agentos`.
- Actor deployments inject their SQLite database automatically; standalone core
  clients must configure a VM SQLite file or descriptor.

## MCP servers

- MCP config belongs to the session — its tools are part of the agent's runtime
  context.
- Configure local child-process or remote servers **before** opening the
  session.
- Config path and transports come from the selected agent adapter (e.g. Pi
  reads `.mcp.json` from its AgentOS home).
- Install local MCP server packages before opening the session so first-run
  package-manager output can't corrupt a stdio handshake.
- See the [agent guide](/agentos/docs/agents/pi) for adapter specifics.

examples/pi/client.ts:

```ts
const mcpConfig = JSON.stringify({
	mcpServers: {
		filesystem: {
			command: "npx",
			args: [
				"-y",
				"@modelcontextprotocol/server-filesystem",
				"/home/agentos",
			],
		},
		remote: {
			url: "https://mcp.example.com/sse",
			headers: { Authorization: "Bearer my-token" },
		},
	},
});

await agent.filesystem.mkdir("/home/agentos/.pi/agent");
await agent.filesystem.writeFile("/home/agentos/.pi/agent/.mcp.json", mcpConfig);

await agent.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
```

## Prompt

- `prompt` accepts native ACP `ContentBlock[]` — not a special text format.
- Never creates a missing session.
- The complete user message is committed before dispatch; a prompt whose
  delivery may have reached the adapter is never auto-replayed.
- Bounded by `limits.acp.maxPromptBytes` and `limits.acp.maxPromptBlocks`; limit
  errors name the field to raise.
- An oversized durable update batch is rejected before it changes history.

examples/sessions/send-prompt.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const agent = client.vm.getOrCreate("my-agent");

await agent.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
const response = await agent.sessions.prompt({
	content: [
		{
			type: "text",
			text: "Create a TypeScript function that checks if a number is prime",
		},
	],
});
console.log(response.message?.content ?? []);
```

examples/sessions/server.ts:

```ts
import { agentOS, setup } from "@rivet-dev/agentos";
import pi from "@agentos-software/pi";

const vm = agentOS({ software: [pi] });

export const registry = setup({ use: { vm } });
registry.start();
```

- `idempotencyKey` — use when the caller may retry. Reusing a key with different
  content fails; retrying while the first call is active waits and returns its
  committed result.

## Events and history

`sessionEvent` is a flat discriminated union.

- Top-level `type` = native ACP `SessionUpdate.sessionUpdate` value; ACP payload
  fields (`content`, `toolCallId`, `entries`) sit beside the durability
  envelope. No nested `update` wrapper.
- `durability: "ephemeral"` — live agent-message/thought delta; not sequenced or
  stored.
- `durability: "durable"` — has a session sequence, emitted only after its
  SQLite commit. Completed/coalesced message chunks are durable.
- Permission request/response variants use the same flat shape with top-level
  `options`, `toolCall`, or `outcome`.

examples/sessions/stream-responses.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const agent = client.vm.getOrCreate("my-agent");
const conn = agent.connect();

// Subscribe to session events before sending the prompt
conn.on("sessionEvent", (event) => {
	console.log(`[${event.sessionId}]`, event.durability, event);
});

await agent.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});
await agent.sessions.prompt({
	content: [{ type: "text", text: "Explain how async/await works" }],
});
```

examples/sessions/server.ts:

```ts
import { agentOS, setup } from "@rivet-dev/agentos";
import pi from "@agentos-software/pi";

const vm = agentOS({ software: [pi] });

export const registry = setup({ use: { vm } });
registry.start();
```

- `readHistory({ sessionId, before, after, limit })` — SQLite-only, never starts
  an adapter. `before`/`after` are exclusive and mutually exclusive. Dedup live
  durable delivery by `(sessionId, sequence)`.
- Also SQLite-only reads: `getSession`, `listSessions`, `getSessionConfig`,
  `getSessionCapabilities`, `getSessionAgentInfo`. Listing uses an opaque keyset
  cursor — not a frozen snapshot.

## Restoration

After VM sleep, the next `prompt` transparently starts the adapter. AgentOS
tries, in order:

1. Native ACP `session/resume`.
2. Stable `session/load`.
3. A fresh private ACP session with bounded continuation context from AgentOS
   history.

- Adapter replay during load is suppressed — SQLite is the sole history source
  of truth.
- Fallback transcript bounded by `limits.acp.maxFallbackContinuationBytes`.
- AgentOS stores its own exact ACP updates because ACP has no portable
  history-reading API and adapters restore inconsistently.

## Permissions

`permissionPolicy` is `reject_all`, `ask`, or `allow_all` (default
`allow_all`).

- Controls how AgentOS answers native ACP permission requests — not VM
  permissions or adapter tool access.
- Set `ask` when opening the session; subscribing alone doesn't change the
  immutable policy.
- `ask` — durably records the request as a `permission_request` event.
- `allow_all` — resolves automatically, emits no permission event.
- Reply with the exact adapter-supplied `optionId` and explicit session ID:

```ts
await agent.sessions.respondPermission({
  sessionId: request.sessionId,
  requestId: request.requestId,
  optionId: request.options[0].optionId,
});
```

- First valid response wins atomically.
- Requests don't expire; cancellation, adapter exit, deletion, or VM shutdown
  records a terminal reason.
- Accepted responses are sequenced in durable history.

## Cancel, unload, and delete

- `cancelPrompt` — cooperative ACP cancellation; returns `cancelled` or
  `no_active_prompt`.
- `unloadSession` — releases the live adapter, keeps SQLite metadata/history; a
  later prompt restores it.
- `deleteSession` — permanently removes the session and history. Omitted ID →
  `main`; repeated deletion is idempotent.

examples/sessions/cancel-prompt.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const agent = client.vm.getOrCreate("my-agent");

await agent.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});

// Start a long-running prompt
const promptPromise = agent.sessions.prompt({
	content: [
		{
			type: "text",
			text: "Refactor the entire codebase to use TypeScript strict mode",
		},
	],
});

// Cancellation is cooperative and does not delete durable history.
setTimeout(async () => {
	await agent.sessions.cancelPrompt();
}, 10_000);

const response = await promptPromise;
console.log(response.message?.content ?? []);
```

examples/sessions/close-destroy.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const agent = client.vm.getOrCreate("my-agent");

await agent.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});

// Release only the adapter. SQLite history remains and the next prompt restores it.
await agent.sessions.unload();

// Permanent deletion requires an explicit public session ID.
await agent.sessions.delete();
```

## Runtime configuration

- `getSessionConfig` — returns the negotiated native ACP config collection + a
  revision.
- `setSessionConfigOption` — may restore the adapter, lets ACP validate the
  value, then replaces the cached collection.

examples/sessions/runtime-config.ts:

```ts
import { createClient } from "@rivet-dev/agentos/client";
import type { registry } from "./server";

const client = createClient<typeof registry>({
	endpoint: "http://localhost:6420",
});
const agent = client.vm.getOrCreate("my-agent");

await agent.sessions.open({
	agent: "pi",
	env: { ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY! },
});

const config = await agent.sessions.getConfig();
console.log(config.options);

await agent.sessions.setConfigOption({
	configId: "model",
	value: "anthropic/claude-sonnet-4",
});
```
