1 ---
2 name: agent-client-protocol
3 description: Implement or debug Agent Client Protocol (ACP) support in Rust for siGit Code. Use when working on ACP JSON-RPC over stdio, the agent-client-protocol crate, session/prompt/fork handlers, config options (model picker), slash commands, streaming notifications, or editor integration.
4 ---
5
6 # Skill: Agent Client Protocol (ACP) — Rust Implementation
7
8 ## Overview
9
10 ACP is a JSON-RPC 2.0 protocol over **stdio** for integrating AI coding agents
11 with editors (Zed, JetBrains, Neovim, etc.). The agent runs as a subprocess;
12 the editor is the client. Communication is newline-delimited JSON on stdin/stdout.
13
14 Crate: `agent-client-protocol = "0.13"` (siGit pins 0.13.0 in `Cargo.lock`)
15 Docs: https://docs.rs/agent-client-protocol
16 Spec: https://agentclientprotocol.com
17
18 > **Big change since 0.10:** the crate moved from a `#[async_trait(?Send)] impl Agent`
19 > model to a **builder** model. You no longer implement a trait. You build an
20 > `Agent` with per-message handler closures and `.connect_to(transport)`. Each
21 > handler receives a `ConnectionTo<Client>` (`cx`) you use to send notifications
22 > and spawn tasks — so the old mpsc "circular dependency" pattern is gone.
23
24 siGit's entire ACP server lives in `src/main.rs` (`run_acp_server`, the
25 `SiGitAgent` struct, and its `handle_*` methods). Read it alongside this skill.
26
27 ---
28
29 ## Dependency setup
30
31 ```toml
32 [dependencies]
33 agent-client-protocol = { version = "0.13", features = [
34 "unstable_session_fork", # session/fork support
35 "unstable_session_additional_directories", # additional_directories on session requests
36 "unstable_auth_methods", # AuthMethod::Agent etc.
37 ] }
38 async-trait = "0.1"
39 tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros", "io-std", "io-util", "sync", "time"] }
40 tokio-util = { version = "0.7", features = ["compat"] }
41 futures = "0.3"
42 uuid = { version = "1", features = ["v4"] }
43 ```
44
45 The `unstable_*` features gate real types/methods (`ForkSessionRequest`,
46 `additional_directories`, `AuthMethod::Agent`). Without them the corresponding
47 APIs don't exist and you'll get "no variant/method" errors.
48
49 ---
50
51 ## Imports
52
53 Protocol message/data types live under `agent_client_protocol::schema::*`.
54 Connection/runtime types live at the crate root.
55
56 ```rust
57 use agent_client_protocol::schema::{
58 AgentCapabilities, AuthMethod, AuthMethodAgent, AuthenticateRequest, AuthenticateResponse,
59 AvailableCommand, AvailableCommandInput, AvailableCommandsUpdate, CancelNotification,
60 ConfigOptionUpdate, ContentBlock, ContentChunk, EmbeddedResourceResource, ForkSessionRequest,
61 ForkSessionResponse, Implementation, InitializeRequest, InitializeResponse, LoadSessionRequest,
62 LoadSessionResponse, Meta, NewSessionRequest, NewSessionResponse, PromptRequest,
63 PromptResponse, ProtocolVersion, SessionCapabilities, SessionConfigOption,
64 SessionConfigOptionCategory, SessionConfigSelectOption, SessionConfigValueId,
65 SessionForkCapabilities, SessionId, SessionNotification, SessionUpdate,
66 SetSessionConfigOptionRequest, SetSessionConfigOptionResponse, StopReason, ToolCall,
67 ToolCallStatus, ToolCallUpdate, ToolCallUpdateFields, ToolKind, UnstructuredCommandInput,
68 };
69 use agent_client_protocol::{Agent, ByteStreams, Client, ConnectionTo, Responder};
70 ```
71
72 ---
73
74 ## Wiring up the server — the builder
75
76 You do **not** implement a trait. You hold your state in an `Arc<MyState>`, then
77 register one closure per incoming message type on `Agent.builder()`, and finish
78 with `.connect_to(transport).await`. The builder owns the JSON-RPC loop and runs
79 until the client disconnects.
80
81 ```rust
82 use tokio_util::compat::{TokioAsyncReadCompatExt, TokioAsyncWriteCompatExt};
83
84 async fn run_acp_server() -> anyhow::Result<()> {
85 let state = Arc::new(SiGitAgent::new(/* … */));
86
87 // Adapt tokio stdio to the futures AsyncRead/AsyncWrite the SDK expects.
88 let stdin = tokio::io::stdin().compat();
89 let stdout = tokio::io::stdout().compat_write();
90 let transport = ByteStreams::new(stdout, stdin); // note: (writer, reader)
91
92 Agent
93 .builder()
94 .on_receive_request(
95 {
96 let state = Arc::clone(&state);
97 async move |req: InitializeRequest, responder, _cx: ConnectionTo<Client>| {
98 handle_response(responder, state.handle_initialize(req).await)
99 }
100 },
101 agent_client_protocol::on_receive_request!(),
102 )
103 .on_receive_request(
104 {
105 let state = Arc::clone(&state);
106 async move |req: PromptRequest, responder, cx: ConnectionTo<Client>| {
107 handle_response(responder, state.handle_prompt(&cx, req).await)
108 }
109 },
110 agent_client_protocol::on_receive_request!(),
111 )
112 // … one .on_receive_request(…) per request type you support …
113 .on_receive_notification(
114 {
115 let state = Arc::clone(&state);
116 async move |notif: CancelNotification, _cx: ConnectionTo<Client>| {
117 state.handle_cancel(notif).await
118 }
119 },
120 agent_client_protocol::on_receive_notification!(),
121 )
122 .connect_to(transport)
123 .await
124 .map_err(|e| anyhow::anyhow!("ACP connection error: {e}"))?;
125
126 Ok(())
127 }
128 ```
129
130 Key points:
131
132 - **`Arc::clone(&state)` per closure.** Each handler closure is `move` and owns
133 its own `Arc` clone of shared state.
134 - **The macro is required.** Each handler is paired with
135 `agent_client_protocol::on_receive_request!()` (or `on_receive_notification!()`).
136 It wires the closure's concrete message type into the dispatcher. Don't omit it.
137 - **Closure signature for requests:** `async move |req: T, responder, cx: ConnectionTo<Client>|`.
138 Use `_cx` when a handler doesn't send notifications (e.g. `initialize`, `authenticate`).
139 - **Closure signature for notifications:** `async move |notif: T, cx: ConnectionTo<Client>|`
140 returning `agent_client_protocol::Result<()>` — no responder (notifications get no reply).
141 - **Unmatched messages fall to the SDK default** — you only register what you support.
142
143 ### The `Responder` + `handle_response` helper
144
145 Requests reply through a `Responder<T>`. siGit funnels every handler's
146 `Result` through one helper:
147
148 ```rust
149 fn handle_response<T: agent_client_protocol::JsonRpcResponse>(
150 responder: Responder<T>,
151 result: agent_client_protocol::Result<T>,
152 ) -> agent_client_protocol::Result<()> {
153 match result {
154 Ok(resp) => responder.respond(resp),
155 Err(err) => responder.respond_with_error(err),
156 }
157 }
158 ```
159
160 So each `handle_*` method just returns `Result<SomeResponse>` and stays free of
161 protocol plumbing.
162
163 ---
164
165 ## `ConnectionTo<Client>` — the `cx`
166
167 The per-handler `cx: ConnectionTo<Client>` replaces the old mpsc-channel forwarder.
168 It is `Clone`. Two things you do with it:
169
170 ```rust
171 // 1. Send a server→client notification (streaming chunks, tool-call updates, …)
172 cx.send_notification(SessionNotification::new(session_id.clone(), update))?;
173
174 // 2. Spawn a background task that keeps using cx (e.g. a progress spinner poller).
175 let cx_for_poller = cx.clone();
176 cx.spawn(async move {
177 loop {
178 // … cx_for_poller.send_notification(progress_update) …
179 # break;
180 }
181 Ok(())
182 }).ok();
183 ```
184
185 Because `cx` is handed to you directly, there is **no circular dependency** between
186 the connection and the agent anymore. Don't reintroduce the mpsc forwarder pattern.
187
188 ---
189
190 ## The handlers siGit implements
191
192 | Message | Method | Notes |
193 |---------|--------|-------|
194 | `InitializeRequest` | `handle_initialize` | capabilities, auth methods, agent info, `meta` |
195 | `AuthenticateRequest` | `handle_authenticate` | verifies stored siGit Code Cloud session |
196 | `NewSessionRequest` | `handle_new_session` | sets cwd, resets history, advertises commands + config options |
197 | `LoadSessionRequest` | `handle_load_session` | like new_session; gated by `load_session(true)` capability |
198 | `ForkSessionRequest` | `handle_fork_session` | gated by `unstable_session_fork` + `SessionForkCapabilities` |
199 | `ListSessionsRequest` | `handle_list_sessions` | the editor's "Import Threads" picker; gated by `SessionListCapabilities` |
200 | `PromptRequest` | `handle_prompt` | the turn: parse blocks → slash commands or tool-calling loop |
201 | `SetSessionConfigOptionRequest` | `handle_set_session_config_option` | the Zed model picker — switches/downloads models |
202 | `CancelNotification` | `handle_cancel` | notification, no response |
203
204 Everything else is left to the SDK default (method not found).
205
206 ---
207
208 ## Types and their builders
209
210 All `#[non_exhaustive]` structs require builder methods — struct-literal syntax
211 won't compile.
212
213 ### `InitializeResponse`
214
215 ```rust
216 Ok(InitializeResponse::new(ProtocolVersion::V1) // use V1, not args.protocol_version
217 .agent_info(
218 Implementation::new("sigit", env!("CARGO_PKG_VERSION"))
219 .title("siGit Code - AI Coding Agent"),
220 )
221 .auth_methods(vec![AuthMethod::Agent(
222 AuthMethodAgent::new("sigit", "Sign in to siGit Code")
223 .description("Sign in with `/login <email> <password>` in the message box."),
224 )])
225 .agent_capabilities(
226 AgentCapabilities::default()
227 .load_session(true) // enables LoadSessionRequest
228 .session_capabilities(
229 SessionCapabilities::new()
230 .fork(SessionForkCapabilities::new()) // enables ForkSessionRequest
231 .list(SessionListCapabilities::new()), // enables ListSessionsRequest
232 ),
233 )
234 .meta(initialize_meta())) // free-form Meta (see below)
235 ```
236
237 `auth_methods` must include at least one `AuthMethod::Agent` or **Zed hangs on
238 "Loading…" forever.** siGit uses `Agent` (not `Terminal`) because Zed advertises
239 terminal-auth for custom agents but never actually spawns the login terminal, so
240 the button would be a silent no-op. With `Agent`, clicking calls `authenticate`.
241
242 ### `Meta` — free-form server metadata
243
244 `Meta` is a string-keyed JSON map you can attach to `InitializeResponse` (siGit
245 publishes the active model there so the editor can show it):
246
247 ```rust
248 let mut meta = Meta::new();
249 meta.insert("sigit".to_string(), serde_json::json!({
250 "active_model": { "display_name": "...", "model_id": "...", "gguf_file": "..." }
251 }));
252 ```
253
254 ### `AuthenticateResponse`
255
256 ```rust
257 Ok(AuthenticateResponse::default()) // success
258 // failure: return an Error — siGit uses -32000 "not signed in …"
259 ```
260
261 ### `NewSessionResponse` / `LoadSessionResponse` / `ForkSessionResponse`
262
263 ```rust
264 let session_id = SessionId::new(uuid::Uuid::new_v4().to_string());
265
266 Ok(NewSessionResponse::new(session_id).config_options(config_options))
267 Ok(LoadSessionResponse::new().config_options(config_options)) // no id arg — it's in the request
268 Ok(ForkSessionResponse::new(new_id).config_options(config_options))
269 ```
270
271 `SessionId` is a newtype with `Clone`, `PartialEq`, `Display`, `Into<String>`,
272 `AsRef<str>`. Store it as-is so `==` works. `config_options` powers the editor's
273 per-session picker (see Config options below).
274
275 The session requests carry `cwd: PathBuf` and (with the feature)
276 `additional_directories: Vec<PathBuf>` — the other directories of a multi-root
277 project, sent only to an agent whose `initialize` reply carries
278 `SessionCapabilities::new().additional_directories(SessionAdditionalDirectoriesCapabilities::new())`. `SiGitAgent::enter_session_roots` stashes `cwd`, `set_current_dir`s to
279 it, hands the extras to `workspace::set_additional_roots`, and returns the ones
280 that survived (a root that isn't a directory, repeats, or just names `cwd` again
281 is dropped). `session_context_message` then names every root in the system
282 message, and project-local discovery — skills, slash commands, subagent types,
283 instruction files — scans all of them, primary root first.
284
285 ### `PromptRequest` / blocks
286
287 ```rust
288 args.session_id // SessionId
289 args.prompt // Vec<ContentBlock>
290 ```
291
292 Editors send several block kinds — handle the three siGit cares about:
293
294 ```rust
295 for block in &args.prompt {
296 match block {
297 ContentBlock::Text(t) => { /* t.text */ }
298 ContentBlock::Resource(embedded) => match &embedded.resource {
299 // editor already inlined file content
300 EmbeddedResourceResource::TextResourceContents(tr) => { /* tr.uri, tr.text */ }
301 EmbeddedResourceResource::BlobResourceContents(b) => { /* b.uri */ }
302 _ => {}
303 },
304 ContentBlock::ResourceLink(link) => {
305 // a reference (e.g. `@file`); read it yourself.
306 // link.uri is "file:///abs/path#L207:219" (or #L207-219). Strip "file://",
307 // split the "#L<start>:<end>" fragment, read & slice the lines.
308 }
309 _ => {} // non_exhaustive — always a wildcard
310 }
311 }
312 ```
313
314 ### `PromptResponse`
315
316 ```rust
317 Ok(PromptResponse::new(StopReason::EndTurn))
318 // other reasons: MaxTokens, Cancelled, MaxTurnRequests, Refusal
319 ```
320
321 ### Streaming: `ContentChunk` + `SessionUpdate` + `SessionNotification`
322
323 ```rust
324 let chunk = ContentChunk::new(ContentBlock::from(delta_text)); // From<Into<String>>
325 let update = SessionUpdate::AgentMessageChunk(chunk);
326 cx.send_notification(SessionNotification::new(session_id.clone(), update))?;
327 ```
328
329 `SessionUpdate` variants siGit uses:
330
331 - `AgentMessageChunk(ContentChunk)` — assistant text.
332 - `ToolCall(ToolCall)` — start a tool-call card (used for model load/download progress).
333 - `ToolCallUpdate(ToolCallUpdate)` — update that card's title/status/content.
334 - `AvailableCommandsUpdate(AvailableCommandsUpdate)` — advertise slash commands.
335 - `ConfigOptionUpdate(ConfigOptionUpdate)` — refresh the picker mid-session.
336
337 (Other variants exist: `UserMessageChunk`, `AgentThoughtChunk`, `Plan`, …)
338
339 ### `ToolCall` / `ToolCallUpdate` — progress cards
340
341 siGit reuses tool-call cards as a generic progress UI (model loading/download):
342
343 ```rust
344 // open the card
345 SessionUpdate::ToolCall(
346 ToolCall::new(tool_call_id.clone(), "Loading Qwen 2.5 3B")
347 .kind(ToolKind::Think)
348 .status(ToolCallStatus::InProgress)
349 .content(vec!["Loading…".into()]),
350 )
351 // update it (only the fields you set)
352 SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
353 tool_call_id.clone(),
354 ToolCallUpdateFields::new()
355 .title("✓ Qwen 2.5 3B loaded")
356 .status(ToolCallStatus::Completed),
357 ))
358 ```
359
360 `ToolCallStatus`: `InProgress`, `Completed`, `Failed`. `ToolKind::Think` is the
361 "thinking/util" kind.
362
363 ### `Error`
364
365 ```rust
366 agent_client_protocol::Error::new(-32603, "internal error message") // there is NO Error::internal()
367 agent_client_protocol::Error::new(-32602, "invalid params: …") // or Error::invalid_params()
368 agent_client_protocol::Error::new(-32000, "not signed in …") // app-defined
369 ```
370
371 ---
372
373 ## Config options — the editor model picker
374
375 ACP lets the agent expose per-session config controls; Zed renders them in the
376 agent panel. siGit uses one `select` option as a model picker.
377
378 ```rust
379 const MODEL_CONFIG_ID: &str = "sigit-model";
380
381 let options: Vec<SessionConfigSelectOption> = models.iter().map(|m| {
382 SessionConfigSelectOption::new(
383 SessionConfigValueId::new(m.model_id.as_str()),
384 format!("{} {badge}", m.display_name),
385 ).description(desc)
386 }).collect();
387
388 let config_options = vec![
389 SessionConfigOption::select(MODEL_CONFIG_ID, "Model", current_value, options)
390 .category(SessionConfigOptionCategory::Model)
391 .description("Select an on-device model or a siGit Code Cloud tier"),
392 ];
393 ```
394
395 Return these from new/load/fork session via `.config_options(config_options)`.
396 When the user picks one, the client sends `SetSessionConfigOptionRequest`:
397
398 ```rust
399 async fn handle_set_session_config_option(&self, cx: &ConnectionTo<Client>,
400 args: SetSessionConfigOptionRequest) -> Result<SetSessionConfigOptionResponse> {
401 if args.config_id.0.as_ref() != MODEL_CONFIG_ID { return Err(Error::new(-32602, "…")); }
402 let model_id = args.value.0.as_ref();
403 // … switch model, streaming ToolCall progress via cx …
404 Ok(SetSessionConfigOptionResponse::new(rebuilt_config_options))
405 }
406 ```
407
408 To refresh the picker mid-session (e.g. after `/reload`), push
409 `SessionUpdate::ConfigOptionUpdate(ConfigOptionUpdate::new(config_options))`.
410
411 **Gotcha:** Zed re-fires the last selection on (re)connect. Guard against a no-op
412 re-select of the already-active model, and don't try to load a new model while a
413 startup load is still in flight (the old weights still hold GPU memory → the new
414 load fails with "does not fit"). siGit waits for `model_ready` first.
415
416 ---
417
418 ## Slash commands
419
420 Advertise them so the editor forwards `/`-prefixed input (Zed rejects unknown
421 slash commands client-side):
422
423 ```rust
424 let commands = vec![
425 AvailableCommand::new("help", "Show available commands"),
426 AvailableCommand::new("models", "List available models").input(
427 AvailableCommandInput::Unstructured(UnstructuredCommandInput::new(
428 "model number to switch to (optional)"))),
429 // … login/logout/whoami/reload/clear/status …
430 ];
431 cx.send_notification(SessionNotification::new(
432 session_id,
433 SessionUpdate::AvailableCommandsUpdate(AvailableCommandsUpdate::new(commands)),
434 ))?;
435 ```
436
437 siGit parses slash text out of the prompt itself (`parse_slash`) and dispatches in
438 `exec_slash_acp` before falling through to inference. The command turn still ends
439 with `Ok(PromptResponse::new(StopReason::EndTurn))`.
440
441 ---
442
443 ## Concurrency: the `block_in_place` trap (still real)
444
445 `mistralrs` model loading calls `tokio::task::block_in_place` internally, which
446 **panics off a multi-threaded runtime worker** ("can call blocking only when
447 running on the multi-threaded runtime"). The builder's task context and
448 `cx.spawn` tasks are not safe for this.
449
450 siGit's fix: **do the blocking model load on a dedicated `std::thread` with its
451 own fresh `tokio::runtime::Runtime`**, and signal completion back via an
452 `AtomicBool` / `oneshot` channel. Never call `load_gguf_model` directly inside a
453 prompt handler or a `cx.spawn` task.
454
455 ```rust
456 std::thread::spawn(move || {
457 let rt = tokio::runtime::Runtime::new().unwrap();
458 let result = rt.block_on(loader_engine.load_gguf_model(cfg, prompt, sampling));
459 // store result, flip an AtomicBool / send on a oneshot
460 });
461 ```
462
463 The prompt handler then `await`s readiness (siGit polls `model_ready` on a 1s
464 `tokio::time::interval`, streaming a spinner via `cx.send_notification`).
465
466 ---
467
468 ## Logging
469
470 stdout is the ACP JSON-RPC wire — **log only to stderr.** siGit uses
471 `tracing_subscriber` to stderr:
472
473 ```rust
474 tracing_subscriber::fmt::Subscriber::builder()
475 .with_env_filter(EnvFilter::try_from_default_env()
476 .unwrap_or_else(|_| EnvFilter::new("info")))
477 .with_writer(std::io::stderr)
478 .try_init();
479 ```
480
481 In siGit's interactive TTY mode (not ACP), it goes further and redirects the
482 stdout/stderr **fds** to `$TMPDIR/sigit.log` so mistralrs/native noise can't
483 corrupt the ratatui screen. ACP mode keeps stdout pristine for protocol JSON.
484
485 ---
486
487 ## TTY vs ACP split
488
489 `main()` decides mode from `std::io::stdin().is_terminal()`:
490
491 - **TTY** → interactive ratatui chat (`run_interactive`, Unix-only — needs fd
492 redirection).
493 - **non-TTY** → `run_acp_server()` (editor launched it over a pipe).
494
495 Account verbs (`sigit login` / `logout` / `whoami`) are handled before the split,
496 since the editor launches `sigit login` in an embedded terminal.
497
498 ---
499
500 ## Protocol flow
501
502 ```
503 Editor Agent
504 │── initialize ──────────────────────►│ capabilities + auth methods + meta
505 │◄─ InitializeResponse ───────────────│
506 │── authenticate ────────────────────►│ (button → verify stored session)
507 │◄─ AuthenticateResponse ─────────────│
508 │── session/new (or load / fork) ───►│ cwd, reset history
509 │◄─ …Response(config_options) ────────│
510 │◄─ session/update AvailableCommands ─│ advertise slash commands
511 │── session/setConfigOption ─────────►│ (model picker) → ToolCall progress
512 │── session/prompt ──────────────────►│ user message (text + resources)
513 │◄─ session/update (N×) ──────────────│ streaming chunks / tool-call cards
514 │◄─ PromptResponse(EndTurn) ──────────│
515 │── session/cancel (notification) ───►│
516 │── [disconnect] ─────────────────────►│ connect_to future resolves → shutdown
517 ```
518
519 ---
520
521 ## Zed configuration
522
523 ```json
524 {
525 "agent_servers": {
526 "siGit Code": {
527 "type": "custom",
528 "command": "/absolute/path/to/target/release/sigit"
529 }
530 }
531 }
532 ```
533
534 ---
535
536 ## Gotchas
537
538 1. **No `Agent` trait to implement** — it's a builder. Register handler closures
539 with `.on_receive_request(closure, on_receive_request!())` and finish with
540 `.connect_to(transport)`. The `on_receive_request!()` / `on_receive_notification!()`
541 macro is mandatory per handler.
542 2. **`cx: ConnectionTo<Client>` replaces the mpsc forwarder** — send notifications
543 with `cx.send_notification(...)` and background tasks with `cx.spawn(...)`.
544 Don't reintroduce the old channel-based circular-dependency pattern.
545 3. **`Error::internal()` doesn't exist** — use `Error::new(-32603, msg)`.
546 4. **Everything in `agent_client_protocol::schema` is `#[non_exhaustive]`** — use
547 builder methods, never struct literals; add `_ => …` wildcards when matching.
548 5. **`ByteStreams::new(stdout, stdin)`** — writer first, reader second. Adapt
549 tokio stdio with `.compat()` / `.compat_write()` (tokio-util).
550 6. **`block_in_place` panics in handler/`cx.spawn` tasks** — run mistralrs model
551 loads on a dedicated `std::thread` + its own `Runtime`; signal back via
552 `AtomicBool`/`oneshot`. Never load inside a prompt handler directly.
553 7. **Empty `authMethods` hangs Zed** — always include at least one
554 `AuthMethod::Agent(AuthMethodAgent::new("id", "Name"))`. Prefer `Agent` over
555 `Terminal` for custom agents (Zed never spawns the terminal for them).
556 8. **Never write to stdout except JSON-RPC** — log to stderr; in TTY mode siGit
557 redirects fds to `$TMPDIR/sigit.log`. Any stray `println!` or native library
558 stdout write corrupts the wire.
559 9. **Unstable features gate real types** — `unstable_session_fork`,
560 `unstable_session_additional_directories`, `unstable_auth_methods` must be on
561 in `Cargo.toml` or `ForkSessionRequest`, `additional_directories`, and
562 `AuthMethod::Agent` won't exist.
563 10. **Zed re-fires the last config selection on connect** — make
564 `setConfigOption` a no-op when the requested model is already active, and
565 never start a model switch while a startup load is still in flight (GPU OOM).
566 11. **`additional_directories` is not decoration** — dropping it means a
567 multi-root project silently behaves as if only the first root existed, with
568 the other repositories' `AGENTS.md` and skills missing. Route it through
569 `workspace.rs`, not just the log line. Handling it is also only half the
570 job: Zed gates the field on
571 `agentCapabilities.sessionCapabilities.additionalDirectories` being present
572 in the `initialize` reply, so an agent that reads the field but never
573 advertises the capability is never sent one, and the user gets a banner
574 saying the agent has no multi-root support.
575 12. **Store `SessionId` as `SessionId`**, not `String`, so `==` is clean.
576 13. **`SetSessionConfigOptionResponse::new(config_options)`** — the response
577 carries the *rebuilt* options so the picker reflects the new current value.
578 14. **`session/list` needs a `cwd` per session, which history alone can't
579 supply.** `SessionInfo` requires an absolute `cwd` and the request may
580 filter on it, so `session_store` writes a `<id>.meta.json` sidecar next to
581 each saved thread; sessions without one are skipped by
582 `handle_list_sessions` rather than guessed at. Advertising
583 `SessionCapabilities::new().list(SessionListCapabilities::new())` is what
584 turns the editor's "Import Threads" picker from an error banner into a
585 list.
586
587 ---
588
589 ## Where to look in the code
590
591 Everything ACP lives in `src/main.rs`:
592
593 - `run_acp_server` — builder wiring + transport.
594 - `SiGitAgent` + `handle_*` — the handlers.
595 - `build_model_config_options` / `resolve_model_config` — picker.
596 - `parse_slash` / `exec_slash_acp` — slash commands.
597 - `handle_response` — the `Responder` helper.
598
599 `src/backend.rs` holds the `InferenceBackend` trait (`LocalBackend` /
600 `OpenAiBackend`) used by `handle_prompt`'s tool-calling loop; `src/tools.rs`
601 defines the agent tools and `execute_tool`.