zeph-acp 0.18.6

ACP (Agent Client Protocol) server for IDE embedding
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# zeph-acp

[![Crates.io](https://img.shields.io/crates/v/zeph-acp)](https://crates.io/crates/zeph-acp)
[![docs.rs](https://img.shields.io/docsrs/zeph-acp)](https://docs.rs/zeph-acp)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](../../LICENSE)
[![MSRV](https://img.shields.io/badge/MSRV-1.88-blue)](https://www.rust-lang.org)

ACP (Agent Client Protocol) server adapter for embedding Zeph in IDE environments.

## Overview

Implements the [Agent Client Protocol](https://agentclientprotocol.org) server side, allowing IDEs and editors to drive the Zeph agent loop over stdio, HTTP+SSE, or WebSocket transports. The crate wires IDE-proxied capabilities — file system access, terminal execution, and permission gates — into the agent loop via `AcpContext`, exposes `AgentSpawner` as the integration point for the host application, and supports runtime model switching via `ProviderFactory` and MCP server management via `ext_method`. Built on the `agent-client-protocol` SDK v0.10.

## Installation

```toml
[dependencies]
zeph-acp = "0.18.3"

# With HTTP+SSE transport
zeph-acp = { version = "0.18.3", features = ["acp-http"] }
```

**Important:**
> Requires Rust 1.88 or later.

## Features

| Feature | Description | Default |
|---------|-------------|---------|
| `acp-http` | HTTP+SSE transport via axum (`AcpHttpState`, `acp_router`, `post_handler`, `get_handler`) | No |

**Tip:**
> Enable `acp-http` only when deploying Zeph as a network-accessible ACP endpoint. The default stdio transport is sufficient for local IDE integrations.

## Key modules

| Module | Description |
|--------|-------------|
| `agent` | `AcpContext` — IDE-proxied capabilities (file executor, shell executor, permission gate, cancel signal) wired into the agent loop per session; `SessionContext` — per-session identity carrying `session_id`, `conversation_id`, and `working_dir` so each ACP session maps to exactly one Zeph conversation in SQLite; `AgentSpawner` factory type; `ZephAcpAgent` ACP protocol handler with multi-session isolation (each session gets its own `ConversationId`), LRU eviction, idle reaper, SQLite persistence, rich content support (images, embedded resources, tool locations), runtime model switching via `ProviderFactory`, and MCP server management via `ext_method` |
| `transport` | `serve_stdio` / `serve_connection` (stdio), HTTP+SSE handlers (`post_handler`, `get_handler`), WebSocket handler (`ws_upgrade_handler`), duplex bridge, axum router; `AcpServerConfig` |
| `fs` | `AcpFileExecutor` — file system executor backed by IDE-proxied ACP file operations |
| `terminal` | `AcpShellExecutor` — shell executor backed by IDE-proxied ACP terminal; configurable command timeout with `kill_terminal` on expiry; deferred `terminal/release` ensures terminal remains alive until IDE receives `ToolCallContent::Terminal` |
| `permission` | `AcpPermissionGate` — forwards tool permission requests to the IDE for user approval; persists "always allow/deny" decisions to TOML file |
| `mcp_bridge` | `acp_mcp_servers_to_entries` — converts ACP-advertised MCP servers (Stdio, Http, Sse) into `McpServerEntry` configs |
| `error` | `AcpError` typed error enum (includes `ResourceLink` variant for resolution failures) |

**Re-exports:** `AcpContext`, `AgentSpawner`, `ProviderFactory`, `SessionContext`, `AcpError`, `AcpFileExecutor`, `AcpPermissionGate`, `AcpShellExecutor`, `AcpServerConfig`, `serve_connection`, `serve_stdio`, `acp_mcp_servers_to_entries`

**Re-exports (feature `acp-http`):** `SendAgentSpawner`, `AcpHttpState`, `acp_router`

## AcpContext

`AcpContext` carries per-session IDE capabilities into the agent loop. Each field is `None` when the IDE did not advertise the corresponding capability:

```rust
pub struct AcpContext {
    pub file_executor: Option<AcpFileExecutor>,
    pub shell_executor: Option<AcpShellExecutor>,
    pub permission_gate: Option<AcpPermissionGate>,
    /// Notify to interrupt the running agent operation.
    pub cancel_signal: Arc<Notify>,
}
```

The `cancel_signal` is shared with the agent's `LoopbackHandle` so that an IDE cancel request immediately interrupts the running inference loop.

## SessionContext

`SessionContext` carries per-session identity into the agent spawner, ensuring each ACP session maps to exactly one Zeph conversation:

```rust
pub struct SessionContext {
    pub session_id: acp::SessionId,
    pub conversation_id: ConversationId,
    pub working_dir: PathBuf,
}
```

The `conversation_id` is pre-created in SQLite before the agent loop starts. This guarantees that the session-to-conversation mapping is persisted atomically, enabling:

- **Session isolation** — concurrent sessions maintain independent conversation histories.
- **Session fork** — `fork_session` copies the source conversation's messages into a new conversation via `copy_conversation`.
- **Session resume** — `load_session` and `resume_session` look up the existing `conversation_id` from the `acp_sessions` table; legacy sessions (pre-migration 026) get a new conversation created on demand.

**Note:**
> When the SQLite store is unavailable, `ConversationId(0)` is used as a sentinel. The agent's `SemanticMemory` will create its own conversation if needed.

## ResourceLink resolution

`ZephAcpAgent` resolves `ResourceLink` content blocks in prompts, as required by the ACP spec. Two URI schemes are supported:

| Scheme | Behavior | Security |
|--------|----------|----------|
| `file://` | Read local file via `tokio::fs` | Canonicalized path must be inside session `cwd`; blocked path components (`/proc`, `.ssh`, `.gnupg`, etc.); size cap (1 MiB); binary rejection; 10 s timeout |
| `http://` / `https://` | Fetch via `reqwest` with `Accept: text/*` | No-redirect policy; post-fetch SSRF check (private IP + CGNAT rejection, fail-closed on missing `remote_addr`); response size cap (1 MiB); 10 s timeout |

Unsupported schemes produce a warning and the block is skipped. Resolution failures are non-fatal.

## StopReason mapping

The agent loop emits `StopHint` values that `ZephAcpAgent` maps to protocol-level `StopReason` variants:

| Agent condition | StopHint | ACP StopReason |
|----------------|----------|----------------|
| Normal completion | _(none)_ | `EndTurn` |
| LLM response truncated by token limit | `MaxTokens` | `MaxTokens` |
| Turn count >= `max_turns` | `MaxTurnRequests` | `MaxTurnRequests` |
| IDE cancel request | _(cancel signal)_ | `Cancelled` |

**Note:**
> `StopHint` is defined in `zeph-core::channel` and carried via `LoopbackEvent::Stop`. The ACP layer consumes it in `prompt()` to produce the final `StopReason`.

## Tool call lifecycle

`ZephAcpAgent` emits ACP session notifications following the protocol-specified two-step lifecycle:

1. **Before execution** — `SessionUpdate::ToolCall` with `status: InProgress` is sent as soon as tool invocation begins, enabling the IDE to display a running indicator.
2. **After execution** — `SessionUpdate::ToolCallUpdate` with `status: Completed` (or `Failed` on error) carries the output content and optional file locations.

Each tool call is identified by a UUID generated per invocation. The UUID is threaded through `LoopbackEvent::ToolStart` / `LoopbackEvent::ToolOutput` so the update correctly references the original announcement. Both the fenced-block execution path (`handle_tool_result`) and the structured parallel tool-call path emit this full two-step sequence unconditionally — output content always appears inside a tool call block in the IDE regardless of which path handled the tool.

**Terminal release ordering** — when a shell tool call embeds a terminal via `ToolCallContent::Terminal`, the ACP spec requires the terminal to remain alive until the IDE has processed the `tool_call_update` notification. `ZephAcpAgent` defers `terminal/release` until after all notifications for that event are dispatched. The deferred release is triggered from the `prompt()` event loop via `AcpShellExecutor::release_terminal()`, which is retained in `SessionEntry` for exactly this purpose.

**Note:**
> Prior to #1003 the fenced-block path did not generate a UUID or emit `ToolStart`. Prior to #1013 the terminal was released inside `execute_in_terminal` before `tool_call_update` was sent, preventing IDEs from displaying terminal output. Both issues are now resolved.

## Subagent IDE visibility

Three `_meta` extensions give IDEs (Zed, VS Code ACP) structured visibility into sub-agent execution:

### Parent tool use ID propagation (#1008)

Every `session_update` emitted by a sub-agent carries `_meta.claudeCode.parentToolUseId` set to the tool call ID that spawned it. IDEs use this to nest sub-agent output under the originating tool card, so multi-agent runs render as collapsible trees rather than interleaved flat messages.

### Terminal streaming (#1009)

`AcpShellExecutor` streams shell output in real time instead of buffering until completion:

- Each chunk of stdout/stderr is emitted as a `session_update` with `_meta.terminal_output`.
- When the command exits, a final `session_update` with `_meta.terminal_exit` carries the exit code.
- IDEs display the output inside the tool card as it arrives.

### Tool call location (#1010)

`ToolCall.location` carries a `filePath` for file read/write tool calls. IDEs move the editor cursor to the referenced file as the agent works, so the user always sees which file is being modified without manually switching tabs.

**Tip:**
> All three extensions are additive `_meta` fields — they are ignored by clients that do not recognize them and require no feature flag.

### Terminal command timeout

`AcpShellExecutor` enforces a configurable wall-clock timeout on every IDE-proxied shell command (default: 120 seconds, controlled via `acp.terminal_timeout_secs`). When the timeout expires:

1. `kill_terminal` is called to terminate the running process.
2. Partial output collected so far is returned as an error result.
3. The terminal is released and `AcpError::TerminalTimeout` is propagated to the agent loop.

```toml
[acp]
terminal_timeout_secs = 120   # set to 0 to disable (wait indefinitely)
```

## Protocol methods

### AgentCapabilities (G3)

The `initialize` response advertises enriched capabilities:

```rust
acp::AgentCapabilities::new()
    .load_session(true)
    .mcp_capabilities(acp::McpCapabilities::new().http(true).sse(true)) // when McpManager is available
    .meta({
        cap_meta.insert("config_options", json!(true));
        cap_meta.insert("ext_methods", json!(true));
        cap_meta
    })
```

This signals to the IDE that the agent supports session config options (`session/configure`), custom `ext_method` extensions, and MCP server management. `McpCapabilities` is only advertised when a `McpManager` is configured.

### set_session_mode (G2)

`ZephAcpAgent` implements `set_session_mode` to handle IDE-driven mode switches per session:

- Validates that the target session exists; returns `invalid_request` error if not found.
- Logs the `session_id` and `mode_id` at debug level.
- Currently a no-op acknowledgement — mode semantics are handled by the IDE.

### ext_notification (G4)

`ZephAcpAgent` implements `ext_notification` to accept IDE-originated fire-and-forget notifications:

- Logs the notification method name at debug level.
- Returns `Ok(())` for all known and unknown methods — unrecognized notifications are silently accepted.

## MCP transport support (G8)

`acp_mcp_servers_to_entries` converts ACP-advertised MCP servers into `zeph-mcp` `ServerEntry` configs. Three transport types are supported:

| ACP variant | Mapped transport | Notes |
|-------------|-----------------|-------|
| `McpServer::Stdio` | `McpTransport::Stdio` | Env vars forwarded as-is to child process |
| `McpServer::Http` | `McpTransport::Http` | Streamable HTTP via rmcp |
| `McpServer::Sse` | `McpTransport::Http` | Legacy SSE mapped to streamable HTTP (backward-compatible) |

**Note:**
> SSE is a legacy MCP transport. rmcp's `StreamableHttpClientTransport` handles both SSE and streamable HTTP endpoints, so both variants map to `McpTransport::Http`.

```rust
use zeph_acp::acp_mcp_servers_to_entries;

let entries = acp_mcp_servers_to_entries(&initialize_request.mcp_servers);
// entries: Vec<ServerEntry> ready for McpManager::start_all
```

## HTTP+SSE transport (feature `acp-http`)

Enable the `acp-http` feature to expose Zeph over HTTP with Server-Sent Events:

```rust
use zeph_acp::{AcpHttpState, AcpServerConfig, acp_router};

let state = AcpHttpState::new(spawner, AcpServerConfig::default());
state.start_reaper(); // prune idle connections every 60 s

let app = acp_router(state);
// mount app into your axum Router
```

Endpoints:

| Method | Path | Description |
|--------|------|-------------|
| `POST` | `/acp` | Send a JSON-RPC request; stream responses as SSE. Creates a new connection when `Acp-Session-Id` header is absent. |
| `GET` | `/acp` | Reconnect to an existing connection's SSE stream. Requires `Acp-Session-Id` header. |
| `GET` | `/acp/ws` | WebSocket upgrade for bidirectional streaming. |

Session IDs are UUIDs returned in the `Acp-Session-Id` response header. Idle connections (beyond `session_idle_timeout_secs`) are reaped by a background task.

**Tip:**
> Use `SendAgentSpawner` (the `Send`-safe variant of `AgentSpawner`) when constructing `AcpHttpState`. This satisfies axum's `State` requirement for `Send + Sync`.
## Rich content

ACP prompts can carry multi-modal content blocks beyond plain text:

- **Images** — base64-encoded image blocks (`image/jpeg`, `image/png`, `image/gif`, `image/webp`) are decoded and forwarded to the LLM provider as inline attachments. Oversized payloads and unsupported MIME types are skipped with a warning.
- **Embedded resources** — `TextResourceContents` blocks are injected into the prompt text wrapped in `<resource>` markers.
- **Tool locations** — tool call results can include file path locations (`ToolCallLocation`) that the IDE uses for source navigation.
- **Thinking chunks** — intermediate reasoning status events are streamed back to the IDE as `session/update` events.

## Model switching

The IDE can switch the active LLM model at runtime via `session/configure` with `config_id = "model"`. `ZephAcpAgent` uses a `ProviderFactory` closure that resolves a `"provider:model"` key to an `AnyProvider`, and an `available_models` allowlist that populates the IDE dropdown. The resolved provider is stored in a shared `Arc<RwLock<Option<AnyProvider>>>` (`provider_override`) that the agent loop checks on each turn.

## ConfigOptionUpdate notifications

When a config option changes via `set_session_config_option`, `ZephAcpAgent` emits a `SessionUpdate::ConfigOptionUpdate` notification containing only the changed option. This keeps the IDE config UI in sync without requiring a full config poll.

## Config option categories

Config options include `SessionConfigOptionCategory` for IDE grouping:

| Config option | Category |
|---------------|----------|
| `model` | `Model` |
| `thinking` | `Model` |
| `auto_approve` | `Other` |

## MCP server management

`ext_method` handles custom JSON-RPC extensions for managing MCP servers at runtime:

| Method | Description |
|--------|-------------|
| `_agent/mcp/list` | List active MCP servers |
| `_agent/mcp/add` | Register a new MCP server |
| `_agent/mcp/remove` | Remove a running MCP server |

Requires a shared `McpManager` reference set via `AcpServerConfig::mcp_manager`.

## Session lifecycle

`ZephAcpAgent` manages multiple concurrent sessions with the following capabilities:

- **Per-session conversation isolation** — each session is assigned its own `ConversationId` at creation time (migration 026). Concurrent sessions maintain fully independent conversation histories in SQLite, preventing cross-session message leakage.
- **LRU eviction** — when the number of active sessions reaches `max_sessions`, the least-recently-used session is evicted to free resources.
- **SQLite persistence** — session events are persisted to `acp_sessions` and `acp_session_events` tables (migrations 013, 026) via `zeph-memory`. The `acp_sessions` table carries a `conversation_id` foreign key linking each session to its conversation.
- **Session resume** — `load_session` replays persisted history as `session/update` events, restoring the conversation state. The existing `conversation_id` is looked up from the store; legacy sessions (pre-026) get a new conversation created on demand.
- **Session fork** — `fork_session` creates a new conversation and copies the source session's messages via `copy_conversation`, preserving history while isolating future turns.
- **Idle reaper** — a background task periodically removes sessions that have been idle longer than `session_idle_timeout_secs`.

### Configuration

| Config field | Type | Default | Env override |
|-------------|------|---------|--------------|
| `acp.max_sessions` | usize | `16` | `ZEPH_ACP_MAX_SESSIONS` |
| `acp.session_idle_timeout_secs` | u64 | `1800` | `ZEPH_ACP_SESSION_IDLE_TIMEOUT_SECS` |
| `acp.permission_file` | PathBuf | `~/.config/zeph/acp-permissions.toml` | `ZEPH_ACP_PERMISSION_FILE` |
| `acp.terminal_timeout_secs` | u64 | `120` | `ZEPH_ACP_TERMINAL_TIMEOUT_SECS` |
| `acp.available_models` | `Vec<String>` | `[]` | — |

## Permission persistence

When the IDE user selects "always allow" or "always deny" for a tool, `AcpPermissionGate` persists the decision to a TOML file (`~/.config/zeph/acp-permissions.toml` by default). On next session startup the gate pre-populates its cache from this file, skipping redundant IDE prompts.

- Atomic write via temp file + rename to prevent corruption.
- File permissions set to `0o600` (owner-only).
- Graceful fallback: if the file is missing or malformed, the gate starts with an empty cache.

## AgentSpawner

`AgentSpawner` is the integration contract between `zeph-acp` and the host application:

```rust
pub type AgentSpawner = Arc<
    dyn Fn(LoopbackChannel, Option<AcpContext>, SessionContext) -> Pin<Box<dyn Future<Output = ()> + 'static>>
        + 'static,
>;
```

The host constructs an `AgentSpawner` closure that wires `AcpContext` capabilities and `SessionContext` (session identity + pre-created `ConversationId`) into `Agent` via the builder, then passes the closure to `serve_stdio` or `serve_connection`.

For HTTP transport, use `SendAgentSpawner` which requires `Send + Sync`:

```rust
pub type SendAgentSpawner = Arc<
    dyn Fn(LoopbackChannel, Option<AcpContext>, SessionContext) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>>
        + Send + Sync + 'static,
>;
```

## Custom methods

`ZephAcpAgent` exposes vendor-specific extensions via `ExtRequest` dispatch. The `custom` module matches on `req.method` and routes to the appropriate handler. Unrecognized methods return `None`, allowing the ACP runtime to respond with "method not found".

| Method | Description |
|--------|-------------|
| `_session/list` | List all sessions (in-memory + persisted via `SqliteStore::list_acp_sessions`) |
| `_session/get` | Get session details and event history |
| `_session/delete` | Remove session from memory and SQLite |
| `_session/export` | Export session events as a portable JSON payload |
| `_session/import` | Import events into a new session (UUID assigned server-side) |
| `_agent/tools` | Return the list of tools available to the agent |
| `_agent/working_dir/update` | Change the working directory for a session |

### Security guards

- **Session ID validation** — IDs must be at most 128 characters, restricted to `[a-zA-Z0-9_-]`. Rejects control characters, slashes, and whitespace.
- **Path traversal protection** — `_agent/working_dir/update` rejects any path containing `..` (`Component::ParentDir`).
- **Import size cap** — `_session/import` rejects payloads exceeding 10,000 events.

### Auth hints in `initialize`

The `initialize` response includes an `auth_hint` key in its metadata map. For stdio transport (trusted local client) this is a generic `"authentication required"` string. IDEs can use this hint to prompt the user for credentials before issuing further requests.

## Feature flags

| Feature | Status | Description |
|---------|--------|-------------|
| `acp-http` | stable | Enables the HTTP+SSE and WebSocket transports (axum-based). Required for `post_handler`, `get_handler`, `ws_upgrade_handler`, and `router`. |
| `unstable-session-fork` | unstable | Enables the `fork_session` ACP method. See below. |
| `unstable-session-resume` | unstable | Enables the `resume_session` ACP method. See below. |
| `unstable-session-usage` | unstable | Enables `UsageUpdate` events — token counts (input, output, cache) sent to the IDE after each turn. See below. |
| `unstable-session-model` | unstable | Enables `SetSessionModel` — IDE-driven model switching via a native picker without `session/configure`. See below. |
| `unstable-session-info-update` | unstable | Enables `SessionInfoUpdate` — agent-generated session title emitted to the IDE after the first turn. See below. |
| `unstable-elicitation` | unstable | Exposes elicitation schema types (`ElicitationRequest`, etc.) for future agent-loop integration. SDK methods not yet available in 0.10.3. |
| `unstable-logout` | unstable | Enables the `logout` ACP method and advertises `auth.logout` capability. Zeph logout is a no-op (vault-based auth). |

**Warning:**
> All `unstable-*` features have wire protocol that is not yet finalized. Expect breaking changes before these features graduate to stable.

To opt in, add the desired features in your `Cargo.toml`:

```toml
[dependencies]
zeph-acp = { version = "*", features = [
    "unstable-session-fork",
    "unstable-session-resume",
    "unstable-session-usage",
    "unstable-session-model",
    "unstable-session-info-update",
    "unstable-elicitation",
    "unstable-logout",
] }
```

All flags are independent and can be combined freely.

### `session/list` (stable)

The `list_sessions` method on `ZephAcpAgent` is stable as of `agent-client-protocol` 0.10.3 — no feature flag required. Returns a snapshot of all active in-memory sessions as `SessionInfo` records (session ID, working directory, last-updated timestamp). Supports an optional `cwd` filter — when provided, only sessions whose working directory matches the given path are returned.

The `initialize` response always advertises `SessionListCapabilities` in the `session` capabilities block, signalling to the IDE that the server supports session enumeration.

### `unstable-session-fork`

Enables the `fork_session` method. Branches an existing conversation into a new session by:

1. Verifying the source session exists in memory or SQLite.
2. Assigning a fresh UUID as the new session ID.
3. Asynchronously copying all persisted events from the source into the new session via `import_acp_events`.
4. Spawning a new agent loop for the forked session with the supplied `cwd`.

The forked session is immediately available for new turns. The event copy is fire-and-forget — if the store write fails, a warning is logged but the session is still created. Model config options are forwarded to the fork response when `available_models` is non-empty.

### `unstable-session-resume`

Enables the `resume_session` method. Restores a persisted session to an active in-memory state without replaying history as `session/update` events:

- If the session is already active in memory, returns success immediately (no-op).
- Otherwise, verifies existence in SQLite and hydrates a new `SessionEntry`, making the session available for new turns with lower latency than the default `load_session` replay path.
- Requires a configured `SqliteStore`; returns an error if no store is present.

### `unstable-session-usage`

Enables `UsageUpdate` session events. After each agent turn `ZephAcpAgent` emits a `SessionUpdate::UsageUpdate` carrying token counts for the turn:

- `input_tokens` — tokens consumed from the prompt.
- `output_tokens` — tokens produced by the model.
- `cache_read_tokens` / `cache_write_tokens` — cache activity when the provider supports prompt caching.

The IDE can use this data to display running cost estimates or token budgets without polling a separate endpoint.

### `unstable-session-model`

Enables `SetSessionModel` handling. When the IDE sends a `set_session_model` request (e.g., from a native model-picker dropdown), `ZephAcpAgent`:

1. Resolves the requested `"provider:model"` key via `ProviderFactory`.
2. Stores the resolved provider in the session-scoped `provider_override`.
3. Returns a confirmation to the IDE so the picker reflects the active selection.

This avoids the need to wrap model selection in a `session/configure` call and maps directly to the Zed AI model picker interaction.

### `unstable-session-info-update`

Enables `SessionInfoUpdate` events. After the first completed turn in a new session, `ZephAcpAgent` emits a `SessionUpdate::SessionInfoUpdate` containing a short, LLM-generated title derived from the opening message. The IDE can use this title to label the session in its sidebar or tab bar.

## Plan updates

During orchestrator runs `ZephAcpAgent` emits `SessionUpdate::Plan` events as the agent formulates its execution plan. The IDE receives these events in real time and can render a collapsible plan view alongside the conversation, giving users visibility into multi-step reasoning before tool calls begin.

## Slash command dispatch

`ZephAcpAgent` sends an `AvailableCommandsUpdate` to the IDE during session initialization listing the built-in slash commands:

| Command | Description |
|---------|-------------|
| `/help` | Show available slash commands |
| `/model` | Switch the active model |
| `/mode` | Change the session mode (`ask` / `code` / `architect`) |
| `/clear` | Clear the current conversation history |
| `/compact` | Trigger a manual context compaction |

User input that begins with `/` is matched against this list and dispatched to the corresponding handler before the message reaches the agent loop.

## Documentation

Full documentation: <https://bug-ops.github.io/zeph/>

## License

MIT