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