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 | `PromptRequest` | `handle_prompt` | the turn: parse blocks → slash commands or tool-calling loop |
200 | `SetSessionConfigOptionRequest` | `handle_set_session_config_option` | the Zed model picker — switches/downloads models |
201 | `CancelNotification` | `handle_cancel` | notification, no response |
202
203 Everything else is left to the SDK default (method not found).
204
205 ---
206
207 ## Types and their builders
208
209 All `#[non_exhaustive]` structs require builder methods — struct-literal syntax
210 won't compile.
211
212 ### `InitializeResponse`
213
214 ```rust
215 Ok(InitializeResponse::new(ProtocolVersion::V1) // use V1, not args.protocol_version
216 .agent_info(
217 Implementation::new("sigit", env!("CARGO_PKG_VERSION"))
218 .title("siGit Code - AI Coding Agent"),
219 )
220 .auth_methods(vec![AuthMethod::Agent(
221 AuthMethodAgent::new("sigit", "Sign in to siGit Code")
222 .description("Sign in with `/login <email> <password>` in the message box."),
223 )])
224 .agent_capabilities(
225 AgentCapabilities::default()
226 .load_session(true) // enables LoadSessionRequest
227 .session_capabilities(
228 SessionCapabilities::new()
229 .fork(SessionForkCapabilities::new()), // enables ForkSessionRequest
230 ),
231 )
232 .meta(initialize_meta())) // free-form Meta (see below)
233 ```
234
235 `auth_methods` must include at least one `AuthMethod::Agent` or **Zed hangs on
236 "Loading…" forever.** siGit uses `Agent` (not `Terminal`) because Zed advertises
237 terminal-auth for custom agents but never actually spawns the login terminal, so
238 the button would be a silent no-op. With `Agent`, clicking calls `authenticate`.
239
240 ### `Meta` — free-form server metadata
241
242 `Meta` is a string-keyed JSON map you can attach to `InitializeResponse` (siGit
243 publishes the active model there so the editor can show it):
244
245 ```rust
246 let mut meta = Meta::new();
247 meta.insert("sigit".to_string(), serde_json::json!({
248 "active_model": { "display_name": "...", "model_id": "...", "gguf_file": "..." }
249 }));
250 ```
251
252 ### `AuthenticateResponse`
253
254 ```rust
255 Ok(AuthenticateResponse::default()) // success
256 // failure: return an Error — siGit uses -32000 "not signed in …"
257 ```
258
259 ### `NewSessionResponse` / `LoadSessionResponse` / `ForkSessionResponse`
260
261 ```rust
262 let session_id = SessionId::new(uuid::Uuid::new_v4().to_string());
263
264 Ok(NewSessionResponse::new(session_id).config_options(config_options))
265 Ok(LoadSessionResponse::new().config_options(config_options)) // no id arg — it's in the request
266 Ok(ForkSessionResponse::new(new_id).config_options(config_options))
267 ```
268
269 `SessionId` is a newtype with `Clone`, `PartialEq`, `Display`, `Into<String>`,
270 `AsRef<str>`. Store it as-is so `==` works. `config_options` powers the editor's
271 per-session picker (see Config options below).
272
273 The session requests carry `cwd: PathBuf` and (with the feature)
274 `additional_directories: Vec<PathBuf>` — the other directories of a multi-root
275 project, sent only to an agent whose `initialize` reply carries
276 `SessionCapabilities::new().additional_directories(SessionAdditionalDirectoriesCapabilities::new())`. `SiGitAgent::enter_session_roots` stashes `cwd`, `set_current_dir`s to
277 it, hands the extras to `workspace::set_additional_roots`, and returns the ones
278 that survived (a root that isn't a directory, repeats, or just names `cwd` again
279 is dropped). `session_context_message` then names every root in the system
280 message, and project-local discovery — skills, slash commands, subagent types,
281 instruction files — scans all of them, primary root first.
282
283 ### `PromptRequest` / blocks
284
285 ```rust
286 args.session_id // SessionId
287 args.prompt // Vec<ContentBlock>
288 ```
289
290 Editors send several block kinds — handle the three siGit cares about:
291
292 ```rust
293 for block in &args.prompt {
294 match block {
295 ContentBlock::Text(t) => { /* t.text */ }
296 ContentBlock::Resource(embedded) => match &embedded.resource {
297 // editor already inlined file content
298 EmbeddedResourceResource::TextResourceContents(tr) => { /* tr.uri, tr.text */ }
299 EmbeddedResourceResource::BlobResourceContents(b) => { /* b.uri */ }
300 _ => {}
301 },
302 ContentBlock::ResourceLink(link) => {
303 // a reference (e.g. `@file`); read it yourself.
304 // link.uri is "file:///abs/path#L207:219" (or #L207-219). Strip "file://",
305 // split the "#L<start>:<end>" fragment, read & slice the lines.
306 }
307 _ => {} // non_exhaustive — always a wildcard
308 }
309 }
310 ```
311
312 ### `PromptResponse`
313
314 ```rust
315 Ok(PromptResponse::new(StopReason::EndTurn))
316 // other reasons: MaxTokens, Cancelled, MaxTurnRequests, Refusal
317 ```
318
319 ### Streaming: `ContentChunk` + `SessionUpdate` + `SessionNotification`
320
321 ```rust
322 let chunk = ContentChunk::new(ContentBlock::from(delta_text)); // From<Into<String>>
323 let update = SessionUpdate::AgentMessageChunk(chunk);
324 cx.send_notification(SessionNotification::new(session_id.clone(), update))?;
325 ```
326
327 `SessionUpdate` variants siGit uses:
328
329 - `AgentMessageChunk(ContentChunk)` — assistant text.
330 - `ToolCall(ToolCall)` — start a tool-call card (used for model load/download progress).
331 - `ToolCallUpdate(ToolCallUpdate)` — update that card's title/status/content.
332 - `AvailableCommandsUpdate(AvailableCommandsUpdate)` — advertise slash commands.
333 - `ConfigOptionUpdate(ConfigOptionUpdate)` — refresh the picker mid-session.
334
335 (Other variants exist: `UserMessageChunk`, `AgentThoughtChunk`, `Plan`, …)
336
337 ### `ToolCall` / `ToolCallUpdate` — progress cards
338
339 siGit reuses tool-call cards as a generic progress UI (model loading/download):
340
341 ```rust
342 // open the card
343 SessionUpdate::ToolCall(
344 ToolCall::new(tool_call_id.clone(), "Loading Qwen 2.5 3B")
345 .kind(ToolKind::Think)
346 .status(ToolCallStatus::InProgress)
347 .content(vec!["Loading…".into()]),
348 )
349 // update it (only the fields you set)
350 SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
351 tool_call_id.clone(),
352 ToolCallUpdateFields::new()
353 .title("✓ Qwen 2.5 3B loaded")
354 .status(ToolCallStatus::Completed),
355 ))
356 ```
357
358 `ToolCallStatus`: `InProgress`, `Completed`, `Failed`. `ToolKind::Think` is the
359 "thinking/util" kind.
360
361 ### `Error`
362
363 ```rust
364 agent_client_protocol::Error::new(-32603, "internal error message") // there is NO Error::internal()
365 agent_client_protocol::Error::new(-32602, "invalid params: …") // or Error::invalid_params()
366 agent_client_protocol::Error::new(-32000, "not signed in …") // app-defined
367 ```
368
369 ---
370
371 ## Config options — the editor model picker
372
373 ACP lets the agent expose per-session config controls; Zed renders them in the
374 agent panel. siGit uses one `select` option as a model picker.
375
376 ```rust
377 const MODEL_CONFIG_ID: &str = "sigit-model";
378
379 let options: Vec<SessionConfigSelectOption> = models.iter().map(|m| {
380 SessionConfigSelectOption::new(
381 SessionConfigValueId::new(m.model_id.as_str()),
382 format!("{} {badge}", m.display_name),
383 ).description(desc)
384 }).collect();
385
386 let config_options = vec![
387 SessionConfigOption::select(MODEL_CONFIG_ID, "Model", current_value, options)
388 .category(SessionConfigOptionCategory::Model)
389 .description("Select an on-device model or a siGit Code Cloud tier"),
390 ];
391 ```
392
393 Return these from new/load/fork session via `.config_options(config_options)`.
394 When the user picks one, the client sends `SetSessionConfigOptionRequest`:
395
396 ```rust
397 async fn handle_set_session_config_option(&self, cx: &ConnectionTo<Client>,
398 args: SetSessionConfigOptionRequest) -> Result<SetSessionConfigOptionResponse> {
399 if args.config_id.0.as_ref() != MODEL_CONFIG_ID { return Err(Error::new(-32602, "…")); }
400 let model_id = args.value.0.as_ref();
401 // … switch model, streaming ToolCall progress via cx …
402 Ok(SetSessionConfigOptionResponse::new(rebuilt_config_options))
403 }
404 ```
405
406 To refresh the picker mid-session (e.g. after `/reload`), push
407 `SessionUpdate::ConfigOptionUpdate(ConfigOptionUpdate::new(config_options))`.
408
409 **Gotcha:** Zed re-fires the last selection on (re)connect. Guard against a no-op
410 re-select of the already-active model, and don't try to load a new model while a
411 startup load is still in flight (the old weights still hold GPU memory → the new
412 load fails with "does not fit"). siGit waits for `model_ready` first.
413
414 ---
415
416 ## Slash commands
417
418 Advertise them so the editor forwards `/`-prefixed input (Zed rejects unknown
419 slash commands client-side):
420
421 ```rust
422 let commands = vec![
423 AvailableCommand::new("help", "Show available commands"),
424 AvailableCommand::new("models", "List available models").input(
425 AvailableCommandInput::Unstructured(UnstructuredCommandInput::new(
426 "model number to switch to (optional)"))),
427 // … login/logout/whoami/reload/clear/status …
428 ];
429 cx.send_notification(SessionNotification::new(
430 session_id,
431 SessionUpdate::AvailableCommandsUpdate(AvailableCommandsUpdate::new(commands)),
432 ))?;
433 ```
434
435 siGit parses slash text out of the prompt itself (`parse_slash`) and dispatches in
436 `exec_slash_acp` before falling through to inference. The command turn still ends
437 with `Ok(PromptResponse::new(StopReason::EndTurn))`.
438
439 ---
440
441 ## Concurrency: the `block_in_place` trap (still real)
442
443 `mistralrs` model loading calls `tokio::task::block_in_place` internally, which
444 **panics off a multi-threaded runtime worker** ("can call blocking only when
445 running on the multi-threaded runtime"). The builder's task context and
446 `cx.spawn` tasks are not safe for this.
447
448 siGit's fix: **do the blocking model load on a dedicated `std::thread` with its
449 own fresh `tokio::runtime::Runtime`**, and signal completion back via an
450 `AtomicBool` / `oneshot` channel. Never call `load_gguf_model` directly inside a
451 prompt handler or a `cx.spawn` task.
452
453 ```rust
454 std::thread::spawn(move || {
455 let rt = tokio::runtime::Runtime::new().unwrap();
456 let result = rt.block_on(loader_engine.load_gguf_model(cfg, prompt, sampling));
457 // store result, flip an AtomicBool / send on a oneshot
458 });
459 ```
460
461 The prompt handler then `await`s readiness (siGit polls `model_ready` on a 1s
462 `tokio::time::interval`, streaming a spinner via `cx.send_notification`).
463
464 ---
465
466 ## Logging
467
468 stdout is the ACP JSON-RPC wire — **log only to stderr.** siGit uses
469 `tracing_subscriber` to stderr:
470
471 ```rust
472 tracing_subscriber::fmt::Subscriber::builder()
473 .with_env_filter(EnvFilter::try_from_default_env()
474 .unwrap_or_else(|_| EnvFilter::new("info")))
475 .with_writer(std::io::stderr)
476 .try_init();
477 ```
478
479 In siGit's interactive TTY mode (not ACP), it goes further and redirects the
480 stdout/stderr **fds** to `$TMPDIR/sigit.log` so mistralrs/native noise can't
481 corrupt the ratatui screen. ACP mode keeps stdout pristine for protocol JSON.
482
483 ---
484
485 ## TTY vs ACP split
486
487 `main()` decides mode from `std::io::stdin().is_terminal()`:
488
489 - **TTY** → interactive ratatui chat (`run_interactive`, Unix-only — needs fd
490 redirection).
491 - **non-TTY** → `run_acp_server()` (editor launched it over a pipe).
492
493 Account verbs (`sigit login` / `logout` / `whoami`) are handled before the split,
494 since the editor launches `sigit login` in an embedded terminal.
495
496 ---
497
498 ## Protocol flow
499
500 ```
501 Editor Agent
502 │── initialize ──────────────────────►│ capabilities + auth methods + meta
503 │◄─ InitializeResponse ───────────────│
504 │── authenticate ────────────────────►│ (button → verify stored session)
505 │◄─ AuthenticateResponse ─────────────│
506 │── session/new (or load / fork) ───►│ cwd, reset history
507 │◄─ …Response(config_options) ────────│
508 │◄─ session/update AvailableCommands ─│ advertise slash commands
509 │── session/setConfigOption ─────────►│ (model picker) → ToolCall progress
510 │── session/prompt ──────────────────►│ user message (text + resources)
511 │◄─ session/update (N×) ──────────────│ streaming chunks / tool-call cards
512 │◄─ PromptResponse(EndTurn) ──────────│
513 │── session/cancel (notification) ───►│
514 │── [disconnect] ─────────────────────►│ connect_to future resolves → shutdown
515 ```
516
517 ---
518
519 ## Zed configuration
520
521 ```json
522 {
523 "agent_servers": {
524 "siGit Code": {
525 "type": "custom",
526 "command": "/absolute/path/to/target/release/sigit"
527 }
528 }
529 }
530 ```
531
532 ---
533
534 ## Gotchas
535
536 1. **No `Agent` trait to implement** — it's a builder. Register handler closures
537 with `.on_receive_request(closure, on_receive_request!())` and finish with
538 `.connect_to(transport)`. The `on_receive_request!()` / `on_receive_notification!()`
539 macro is mandatory per handler.
540 2. **`cx: ConnectionTo<Client>` replaces the mpsc forwarder** — send notifications
541 with `cx.send_notification(...)` and background tasks with `cx.spawn(...)`.
542 Don't reintroduce the old channel-based circular-dependency pattern.
543 3. **`Error::internal()` doesn't exist** — use `Error::new(-32603, msg)`.
544 4. **Everything in `agent_client_protocol::schema` is `#[non_exhaustive]`** — use
545 builder methods, never struct literals; add `_ => …` wildcards when matching.
546 5. **`ByteStreams::new(stdout, stdin)`** — writer first, reader second. Adapt
547 tokio stdio with `.compat()` / `.compat_write()` (tokio-util).
548 6. **`block_in_place` panics in handler/`cx.spawn` tasks** — run mistralrs model
549 loads on a dedicated `std::thread` + its own `Runtime`; signal back via
550 `AtomicBool`/`oneshot`. Never load inside a prompt handler directly.
551 7. **Empty `authMethods` hangs Zed** — always include at least one
552 `AuthMethod::Agent(AuthMethodAgent::new("id", "Name"))`. Prefer `Agent` over
553 `Terminal` for custom agents (Zed never spawns the terminal for them).
554 8. **Never write to stdout except JSON-RPC** — log to stderr; in TTY mode siGit
555 redirects fds to `$TMPDIR/sigit.log`. Any stray `println!` or native library
556 stdout write corrupts the wire.
557 9. **Unstable features gate real types** — `unstable_session_fork`,
558 `unstable_session_additional_directories`, `unstable_auth_methods` must be on
559 in `Cargo.toml` or `ForkSessionRequest`, `additional_directories`, and
560 `AuthMethod::Agent` won't exist.
561 10. **Zed re-fires the last config selection on connect** — make
562 `setConfigOption` a no-op when the requested model is already active, and
563 never start a model switch while a startup load is still in flight (GPU OOM).
564 11. **`additional_directories` is not decoration** — dropping it means a
565 multi-root project silently behaves as if only the first root existed, with
566 the other repositories' `AGENTS.md` and skills missing. Route it through
567 `workspace.rs`, not just the log line. Handling it is also only half the
568 job: Zed gates the field on
569 `agentCapabilities.sessionCapabilities.additionalDirectories` being present
570 in the `initialize` reply, so an agent that reads the field but never
571 advertises the capability is never sent one, and the user gets a banner
572 saying the agent has no multi-root support.
573 12. **Store `SessionId` as `SessionId`**, not `String`, so `==` is clean.
574 13. **`SetSessionConfigOptionResponse::new(config_options)`** — the response
575 carries the *rebuilt* options so the picker reflects the new current value.
576
577 ---
578
579 ## Where to look in the code
580
581 Everything ACP lives in `src/main.rs`:
582
583 - `run_acp_server` — builder wiring + transport.
584 - `SiGitAgent` + `handle_*` — the handlers.
585 - `build_model_config_options` / `resolve_model_config` — picker.
586 - `parse_slash` / `exec_slash_acp` — slash commands.
587 - `handle_response` — the `Responder` helper.
588
589 `src/backend.rs` holds the `InferenceBackend` trait (`LocalBackend` /
590 `OpenAiBackend`) used by `handle_prompt`'s tool-calling loop; `src/tools.rs`
591 defines the agent tools and `execute_tool`.