## agent.* — agent sessions

*Host API · agent.**

Spin up, drive, and inspect **LangGraph-backed agent runs** from your bundle. `session.create` mints an `AnnaAppSession` (kind=`agent`) and caches the underlying `app_session_token` in `_aps_token_cache[uuid]` — the iframe never holds capability credentials; it threads the `app_session_uuid` (`aps_…`) through every subsequent call.

**Path:** Bundle (postMessage) → `POST /api/v1/anna-apps/runtime/rpc` → `_h_agent_session_*` (dispatcher) → `SqlAlchemyWindowStore.agent_session_*` (store) → `app_llm_facade.agent_session_*` → LangGraph runner via `src.services.app_runner._copilot_unified`.

**Two-layer ACL (the dispatcher checks ui.host_api, NOT permissions):**
1. The dispatcher gate `host_api_allows` for the `agent` namespace reads ONLY `manifest.ui.host_api.agent` — `manifest.permissions[]` is display/audit metadata and is NEVER consulted.
2. `manifest.ui.host_api.agent.session.{auto, fixed: {client_ids: []}}` is **submode-aware**: each submode is `false` until explicitly enabled. Fixed mode further validates `fixed_client_id` against the allow-list (empty list = any user-owned Executa).
3. `manifest.ui.host_api.agent.tools` advisory-restricts the tool surface for the session; the facade re-resolves at run time so newly-installed Executas appear without re-creating the session.

**Submodes**
• `auto` — LangGraph runner picks any tool the user's Executa exposes (intersected with `agent.tools`).
• `fixed` — pin the run to one Executa by `client_id`. Use this for deterministic flows or scoped access (e.g. read-only tools).

**Streaming model.** `agent.session.run` returns SYNCHRONOUSLY with `{stream_id, run_id}`; the dispatcher allocates `stream_id`, enqueues the run, and the agent worker publishes each frame directly to a per-stream NATS subject, delivered to the iframe over the `rpc.stream` event topic — `{stream_id, window_uuid, seq, payload, done}`. The SDK's `AgentRunStream` reassembles by `seq` (out-of-order frames are buffered until the missing seq arrives) into an `AsyncIterable`. The pump ALWAYS emits a terminal `{event:'end', run_id}` with `done:true`, even on cancellation or crash.

**Session lifecycle.** `create` → cached uuid for `expires_in` seconds (`_cache_aps_token` stores `ttl - 30s` to refresh slightly early) → many `run` calls → optional `cancel(run_id)` / `history(limit)` → `delete` revokes + pops cache. After `delete` or TTL eviction, subsequent calls fail `session_expired` / `APP_SESSION_NOT_FOUND`.

**Quotas + caps.** Per-call `quotaCaps` requested on `create` are intersected (`min`) with `UserAnnaApp.custom_config.llm_grant.quota_caps`; defaults to `max_tokens_per_call=4096` if neither side declares one. `APP_QUOTA_EXCEEDED` may be raised pre-stream (synchronously) OR mid-stream as an in-band `{event:'error'}` frame.

**SDK ergonomics.** Prefer the high-level helper `await anna.agent.session({submode, ...})` — it returns an `AgentSession` instance with `.run({content})` / `.cancel(runId)` / `.history({limit})` / `.delete()` methods bound to the new uuid, so apps never thread `app_session_uuid` manually. Per-namespace timeout default is **300 000 ms** (ack-only — long streams keep the iterator alive until `done`).

- **agent.session.create** — Mint a server-side `AnnaAppSession` (kind=`agent`). Validates `submode` against the manifest grant and `fixed_client_id` against the allow-list; optional `systemPrompt` overrides the agent's system prompt. Returns `{app_session_uuid, expires_in, submode, fixed_client_id, system_prompt, granted_tools, inherit_host_tools}` (`granted_tools` is runtime-resolved via `effective_granted_tool_names`, NOT a raw manifest echo).  `submode` `fixed_client_id` `systemPrompt` `quotaCaps∩grant`

- **agent.session.catalog** — Pre-create discovery of the platform-tool universe: which names are legal in create-time `quotaCaps.allowed_tools` / per-run `allowed_tools`, and whether they would resolve for THIS app. Returns `{platform_tools: [{name, description, dev_safe, declared_in_manifest, user_granted, eligible, blocked_by?}], inherit_host_tools_granted, manifest_declared, user_granted}`. `blocked_by` pinpoints WHY a tool is unavailable (`manifest` = not declared in `ui.host_api.agent.tools`; `user_grant` = not authorized in the Permissions modal). Read-only — mints no session, consumes no quota. Previously the resolved surface was only visible after-the-fact via the create response's `granted_tools` echo.  `discovery` `read-only` `blocked_by`

- **agent.session.run** — Submit a user turn; returns SYNCHRONOUSLY with `{stream_id, run_id}`. LangGraph response frames arrive via `rpc.stream` events; the SDK's `AgentRunStream` reassembles by `seq` into an `AsyncIterable`. Terminal frame is always `{event:'end', run_id}` + `done:true`. Optional per-run fields: `attachments` (native multimodal image inputs — the session's vision model sees them directly, image/* only, ≤6, SSRF-guarded URLs or base64 `data`; non-vision model fails fast with `APP_MODEL_NOT_VISION_CAPABLE`), `allowed_tools` (narrow this run's tool surface — sandbox sessions only), `modelPreferences`, `systemPrompt`.  `streaming` `rpc.stream` `attachments` `allowed_tools`

- **agent.session.cancel** — Best-effort cancellation. Records a cancel signal keyed by `(app_session_uuid, run_id)` and maps it onto the underlying worker task: a still-queued run is dropped before start; a running run stops at the next poll checkpoint (every ~30 stream events) — an in-flight provider call still completes. Returns `{cancelled: bool}` — NOT a confirmation the run actually stopped (watch for the `task_cancelled` delta and terminal `end` frame).  `run_id required` `best-effort`

- **agent.session.history** — Read the tail of the LangGraph `channel_values.messages` checkpoint. **Best-effort:** returns `{messages: []}` (NOT an error) when the checkpoint store is unavailable or the thread has no writes yet. `limit` clamped to `[1, 200]`, default 50.  `best-effort` `LangGraph checkpoint`

- **agent.session.delete** — Soft-revoke the session, cancel all of its active runs (best-effort fan-out, forum #191), and pop its cached capability token. Idempotent.  `revoke` `idempotent`

- **agent.session.list** — Enumerate the caller's own agent sessions, scoped to this app (`window.app_id`). Returns `{sessions: [{app_session_uuid, kind, submode, fixed_client_id, label, created_at, last_active_at, expires_at, max_lifetime_at}]}` — metadata only, no token. `include_expired?` (default false); `limit` clamped `[1, 100]`, default 50. Use it to re-attach after an iframe reload instead of minting a duplicate.  `read-only` `since v0.8.0`

- **agent.session.refresh** — Re-mint a fresh capability token for an existing `app_session_uuid` and slide its idle window forward — WITHOUT re-creating the session (which would orphan the LangGraph `thread_id`). Can never push past `max_lifetime_at`. Returns `{app_session_uuid, expires_in, submode, fixed_client_id, expires_at, max_lifetime_at, idle_ttl_seconds, session_expires_in}`.  `extend TTL` `since v0.8.0`

---

## Detailed reference

### agent.session.create

*Host API method · iframe surface*

Mint a server-side `AnnaAppSession` (kind=`agent`) bound to the current `(user, app, window)` triple. Returns an `app_session_uuid` the bundle threads through every subsequent `run` / `cancel` / `history` / `delete` call. The underlying capability token is cached in `_aps_token_cache[uuid]` for its TTL — the iframe never sees it.

**Signature**

```ts
anna.agent.session.create(args: {submode: 'auto'|'fixed', fixed_client_id?: string, label?: string, systemPrompt?: string, quotaCaps?: {max_tokens_per_call?: number, max_runs_per_day?: number, max_concurrent_runs?: number}}, opts?: {timeoutMs?: number}) => Promise<{app_session_uuid: string, expires_in: number, submode: 'auto'|'fixed', fixed_client_id: string|null, system_prompt: string|null, granted_tools: string[], inherit_host_tools: boolean}>
```

**Direction:** Bundle (iframe) → Host RPC → app_llm_facade.agent_session_create → AnnaAppSession (kind=agent) + cached app_session_token

**Parameters**

- `submode` ('auto' | 'fixed', required) — Tool-resolution mode. `auto` lets the LangGraph runner pick any tool the user's Executa Agent exposes (constrained by `manifest.ui.host_api.agent.tools`). `fixed` pins the run to a single `client_id` (one specific Executa) and rejects any other tool.
    - `invalid_arg` if absent or not exactly `'auto'` / `'fixed'`.
    - `permission_denied` if the chosen submode is `false` in `manifest.ui.host_api.agent.session.{auto,fixed}` — the manifest must opt into each submode explicitly.
- `fixed_client_id` (string, required) — Executa `client_id` the run is pinned to. Required for `submode='fixed'`; ignored for `submode='auto'`. SDK also accepts the camelCase alias `fixedClientId`.
    - `invalid_arg` if missing when `submode='fixed'`.
    - `permission_denied` if not present in `manifest.ui.host_api.agent.session.fixed.client_ids` (when that list is non-empty — an empty list means "any user-owned Executa").
- `label` (string, optional) — Human-readable label stored on the `AnnaAppSession` row. Visible in admin / debug UIs; not surfaced to the model.
- `systemPrompt` (string, optional) — Optional session-level system prompt persisted on the `AnnaAppSession` row and applied to every `run`. Validated against the platform safety floor (≤ 32000 chars; forbidden role / fence tokens rejected with `APP_INVALID_REQUEST`). A per-run `systemPrompt` on `agent.session.run` overrides it for that turn. Legacy alias: `system_prompt`.
- `quotaCaps` ({max_tokens_per_call?, max_runs_per_day?, max_concurrent_runs?}, optional) — Caller-requested ceiling. Intersected (`min`) with the user's `llm_grant.quota_caps` via `_intersect_quota_caps`; the result is stored on `session.quota_caps` and consulted by every subsequent `run`.
    - Defaults fall back to grant value; if neither side declares a field, `max_tokens_per_call` defaults to **4096**.
    - Requesting *more* than the grant has no effect — silently clamped down.

### agent.session.catalog

*Host API method · iframe surface*

Discover the platform-tool universe for agent sessions BEFORE creating one. Answers two questions that were previously unanswerable from an app: (1) what is the full set of legal `allowed_tools` names (the AppToolRegistry), and (2) which of them would actually resolve for THIS app — with a `blocked_by` diagnostic for each unavailable tool. Read-only: mints no session, consumes no quota, touches no LangGraph state.

**Signature**

```ts
anna.agent.session.catalog() → Promise<CatalogResult>
```

**Direction:** Bundle (iframe) → Host RPC → SqlAlchemyWindowStore.agent_session_catalog → AppToolRegistry + UserAnnaApp.custom_config.llm_grant

### agent.session.run

*Host API method · iframe surface · streaming*

Submit a user turn to an existing agent session and stream the LangGraph response back to the iframe. The handler validates + enqueues and returns *immediately* with `{stream_id, run_id}`; the agent worker publishes each frame directly to a per-stream NATS subject, which the host page forwards as `('rpc.stream', {stream_id, seq, payload, done})` events into the iframe, where the SDK's `AgentRunStream` reassembles them into an `AsyncIterable`.

**Signature**

```ts
anna.agent.session.run(args: {app_session_uuid: string, content: string, run_id?: string, recursion_limit?: number, systemPrompt?: string, allowed_tools?: string[], modelPreferences?: object}, opts?: {timeoutMs?: number}) => Promise<{stream_id: string, run_id: string}>
```

**Direction:** Bundle (iframe) → Host RPC → app_llm_facade.agent_session_dispatch → Redis task queue → agent_worker (LangGraph) → NATS per-stream subject → `rpc.stream` events

**Parameters**

- `app_session_uuid` (string (aps_…), required) — Session id from `agent.session.create`. `invalid_arg` if missing / non-string / does not start with `aps_`.
- `content` (string, required) — User turn text. `invalid_arg` if missing / non-string / whitespace-only.
- `attachments` (Attachment[] — {type, url | data, filename?, detail?}, optional) — Native multimodal image inputs (forum #171): the session's model sees the images DIRECTLY in the same inference as `content` — no upload_local_file → analyze_image round-trip. image/* MIME only, ≤ 6 per run, ≤ 20 MB each; `url` must be public HTTPS (SSRF-guarded), `data` accepts base64 / data-URI (host uploads to storage server-side). Requires a vision-capable model — otherwise the run fails fast with `APP_MODEL_NOT_VISION_CAPABLE` (pair with `modelPreferences` to pick one). Images are visible for THIS run only; re-attach for later turns. Best practice for local files: upload first via `anna.upload.inline`, then pass the returned `url`.
- `run_id` (string (uuid), optional) — Caller-supplied run id (idempotency token). When omitted the store generates a fresh `uuid4`. Pass the same value to `agent.session.cancel` to abort.
- `recursion_limit` (number (int), optional) — LangGraph max recursion depth. Default **8** — increase for long tool-chain workflows; the facade caps server-side via LangGraph config.
- `systemPrompt` (string, optional) — Optional per-run system-prompt override. Takes precedence over the session-level `systemPrompt` from `create` for THIS turn only (not persisted). Validated against the platform safety floor. The server reads `systemPrompt` first, falling back to the legacy `system` field for older callers.
- `allowed_tools` (string[], optional) — Tighten the tool surface for this single run (subset of `granted_tools` returned by `create`). SDK accepts `allowedTools` alias. Useful for "safe mode" / read-only runs.
- `modelPreferences` (object, optional) — Same shape as `llm.complete` — `hints` / `costPriority` / `speedPriority` / `intelligencePriority`. Resolved at each LangGraph node, intersected with the user's grant.

### agent.session.cancel

*Host API method · iframe surface*

Best-effort cancellation of a queued or in-flight `agent.session.run`. Writes a Redis cancel marker keyed by `(app_session_uuid, run_id)` AND — via the active-run registry populated at enqueue time — sets the cancel signal on the underlying worker task. A still-queued run is dropped before it starts (`task_cancelled` + `end` frames); a running run stops at the next poll checkpoint (every ~30 stream events) — an in-flight provider call still completes first.

**Signature**

```ts
anna.agent.session.cancel(args: {app_session_uuid: string, run_id: string}, opts?: {timeoutMs?: number}) => Promise<{cancelled: boolean}>
```

**Direction:** Bundle (iframe) → Host RPC → src.services.app_runner.signal_cancel (Redis cancel marker + worker-task cancel signal)

**Parameters**

- `app_session_uuid` (string (aps_…), required) — Session id from `create`. Resolved through `_claims_for_aps` — `session_expired` if the cached token has been evicted.
- `run_id` (string, required) — Exact `run_id` returned (or supplied) by `agent.session.run`. `invalid_arg` if missing.

### agent.session.history

*Host API method · iframe surface*

Read recent agent messages by replaying the most recent LangGraph checkpoint for this session's `thread_id`. Best-effort: returns `{messages: []}` (NOT an error) if the checkpoint store is unavailable, the row hasn't been written yet, or the read fails — treat absence as "no history yet".

**Signature**

```ts
anna.agent.session.history(args: {app_session_uuid: string, limit?: number}, opts?: {timeoutMs?: number}) => Promise<{messages: Array<{role: 'user'|'assistant'|'system'|string, content: string | unknown[]}>}>
```

**Direction:** Bundle (iframe) → Host RPC → LangGraph AsyncCheckpointSaver.aget

**Parameters**

- `app_session_uuid` (string (aps_…), required) — Session id from `create`.
- `limit` (number (int), optional) — Max messages to return (clamped to `[1, 200]`). Always reads from the *tail* — the most recent `limit` messages.

### agent.session.delete

*Host API method · iframe surface*

Revoke an `AnnaAppSession`, cancel all of its active runs, and drop its cached capability token. Idempotent — repeated calls succeed silently. Use this to free admin-view entries and force the next `create` call to mint a fresh session.

**Signature**

```ts
anna.agent.session.delete(args: {app_session_uuid: string}, opts?: {timeoutMs?: number}) => Promise<{deleted: true}>
```

**Direction:** Bundle (iframe) → Host RPC → app_llm_facade.revoke_session

**Parameters**

- `app_session_uuid` (string (aps_…), required) — Session id from `create`.

### agent.session.list

*Host API method · iframe surface*

Enumerate the caller's own agent sessions, scoped to THIS app (`window.app_id`) — an app can never see another origin's sessions. Use it to re-attach to or clean up sessions after losing in-memory handles (iframe reload, multi-tab, crash). Returns identity + lifecycle metadata only (no token, no `thread_id`, no `quota_caps`).

**Signature**

```ts
anna.agent.session.list(args?: {include_expired?: boolean, limit?: number}, opts?: {timeoutMs?: number}) => Promise<{sessions: Array<{app_session_uuid: string, kind: string, submode: 'auto'|'fixed'|null, fixed_client_id: string|null, label: string|null, created_at: string, last_active_at: string, expires_at: string, max_lifetime_at: string}>}>
```

**Direction:** Bundle (iframe) → Host RPC → app_llm_facade.list_sessions

**Parameters**

- `include_expired` (boolean, optional) — Include expired-but-not-revoked sessions. By default only active (non-revoked, non-expired) sessions are returned.
- `limit` (number (int), optional) — Max sessions to return, newest-first. Clamped to `[1, 100]`.

### agent.session.refresh

*Host API method · iframe surface*

Re-mint a fresh short-lived capability token for an existing session and slide its idle window forward — WITHOUT re-creating the session (which would orphan the LangGraph `thread_id`). The iframe only ever persists `app_session_uuid`; on resume, or proactively near expiry, it calls `refresh` to obtain a new deadline. Returns absolute lifecycle timestamps so the client can schedule its next refresh.

**Signature**

```ts
anna.agent.session.refresh(args: {app_session_uuid: string}, opts?: {timeoutMs?: number}) => Promise<{app_session_uuid: string, expires_in: number, submode: 'auto'|'fixed'|null, fixed_client_id: string|null, expires_at: string, max_lifetime_at: string, idle_ttl_seconds: number, session_expires_in: number}>
```

**Direction:** Bundle (iframe) → Host RPC → app_llm_facade.refresh_session_token (+ session_lifecycle_meta)

**Parameters**

- `app_session_uuid` (string (aps_…), required) — Session id from `create` (or from `agent.session.list`). SDK also accepts the camelCase alias `appSessionUuid`. `invalid_arg` if missing or not `aps_`-prefixed.
