sigit-code 1.2.0__tar.gz → 1.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. sigit_code-1.2.2/.agents/skills/agent-client-protocol/SKILL.md +576 -0
  2. {sigit_code-1.2.0 → sigit_code-1.2.2}/.agents/skills/ai-assisted-coding/SKILL.md +41 -17
  3. {sigit_code-1.2.0 → sigit_code-1.2.2}/.agents/skills/sigit-code-release/SKILL.md +16 -2
  4. {sigit_code-1.2.0 → sigit_code-1.2.2}/.agents/skills/tool-calling/SKILL.md +95 -39
  5. sigit_code-1.2.2/.claude/skills/run-sigit/SKILL.md +134 -0
  6. sigit_code-1.2.2/.claude/skills/run-sigit/driver.mjs +126 -0
  7. sigit_code-1.2.2/.claude/skills/run-sigit/tui-smoke.sh +41 -0
  8. {sigit_code-1.2.0 → sigit_code-1.2.2}/CHANGELOG.md +22 -0
  9. sigit_code-1.2.2/CLAUDE.md +105 -0
  10. {sigit_code-1.2.0 → sigit_code-1.2.2}/Cargo.lock +23 -1
  11. {sigit_code-1.2.0 → sigit_code-1.2.2}/Cargo.toml +8 -4
  12. {sigit_code-1.2.0 → sigit_code-1.2.2}/PKG-INFO +1 -1
  13. {sigit_code-1.2.0 → sigit_code-1.2.2}/README.md +24 -13
  14. {sigit_code-1.2.0 → sigit_code-1.2.2}/src/account.rs +54 -0
  15. {sigit_code-1.2.0 → sigit_code-1.2.2}/src/backend.rs +311 -6
  16. {sigit_code-1.2.0 → sigit_code-1.2.2}/src/chat.rs +136 -120
  17. {sigit_code-1.2.0 → sigit_code-1.2.2}/src/main.rs +512 -72
  18. {sigit_code-1.2.0 → sigit_code-1.2.2}/src/provider.rs +1 -1
  19. sigit_code-1.2.0/.agents/skills/agent-client-protocol/SKILL.md +0 -319
  20. {sigit_code-1.2.0 → sigit_code-1.2.2}/.agents/AGENTS.md +0 -0
  21. {sigit_code-1.2.0 → sigit_code-1.2.2}/.agents/skills/branding/SKILL.md +0 -0
  22. {sigit_code-1.2.0 → sigit_code-1.2.2}/.github/workflows/ci.yml +0 -0
  23. {sigit_code-1.2.0 → sigit_code-1.2.2}/.github/workflows/release-crates.yml +0 -0
  24. {sigit_code-1.2.0 → sigit_code-1.2.2}/.github/workflows/release-github.yml +0 -0
  25. {sigit_code-1.2.0 → sigit_code-1.2.2}/.github/workflows/release-homebrew.yml +0 -0
  26. {sigit_code-1.2.0 → sigit_code-1.2.2}/.github/workflows/release-npm.yml +0 -0
  27. {sigit_code-1.2.0 → sigit_code-1.2.2}/.github/workflows/release-pypi.yml +0 -0
  28. {sigit_code-1.2.0 → sigit_code-1.2.2}/.gitignore +0 -0
  29. {sigit_code-1.2.0 → sigit_code-1.2.2}/.nvmrc +0 -0
  30. {sigit_code-1.2.0 → sigit_code-1.2.2}/LICENSE +0 -0
  31. {sigit_code-1.2.0 → sigit_code-1.2.2}/npm/README.md.tmpl +0 -0
  32. {sigit_code-1.2.0 → sigit_code-1.2.2}/npm/package-main.json.tmpl +0 -0
  33. {sigit_code-1.2.0 → sigit_code-1.2.2}/npm/package.json.tmpl +0 -0
  34. {sigit_code-1.2.0 → sigit_code-1.2.2}/npm/scripts/render-main-package.cjs +0 -0
  35. {sigit_code-1.2.0 → sigit_code-1.2.2}/npm/scripts/render-platform-package.cjs +0 -0
  36. {sigit_code-1.2.0 → sigit_code-1.2.2}/npm/sigit/.gitignore +0 -0
  37. {sigit_code-1.2.0 → sigit_code-1.2.2}/npm/sigit/README.md +0 -0
  38. {sigit_code-1.2.0 → sigit_code-1.2.2}/npm/sigit/package.json +0 -0
  39. {sigit_code-1.2.0 → sigit_code-1.2.2}/npm/sigit/src/index.ts +0 -0
  40. {sigit_code-1.2.0 → sigit_code-1.2.2}/npm/sigit/tsconfig.json +0 -0
  41. {sigit_code-1.2.0 → sigit_code-1.2.2}/pypi/README.md +0 -0
  42. {sigit_code-1.2.0 → sigit_code-1.2.2}/pypi/pyproject.toml +0 -0
  43. {sigit_code-1.2.0 → sigit_code-1.2.2}/pyproject.toml +0 -0
  44. {sigit_code-1.2.0 → sigit_code-1.2.2}/rust-toolchain.toml +0 -0
  45. {sigit_code-1.2.0 → sigit_code-1.2.2}/src/credentials.rs +0 -0
  46. {sigit_code-1.2.0 → sigit_code-1.2.2}/src/models.rs +0 -0
  47. {sigit_code-1.2.0 → sigit_code-1.2.2}/src/setup.rs +0 -0
  48. {sigit_code-1.2.0 → sigit_code-1.2.2}/src/tools.rs +0 -0
@@ -0,0 +1,576 @@
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>`. siGit stashes `cwd`, `set_current_dir`s
275
+ to it, and pushes a system message telling the model to use absolute paths under it.
276
+
277
+ ### `PromptRequest` / blocks
278
+
279
+ ```rust
280
+ args.session_id // SessionId
281
+ args.prompt // Vec<ContentBlock>
282
+ ```
283
+
284
+ Editors send several block kinds — handle the three siGit cares about:
285
+
286
+ ```rust
287
+ for block in &args.prompt {
288
+ match block {
289
+ ContentBlock::Text(t) => { /* t.text */ }
290
+ ContentBlock::Resource(embedded) => match &embedded.resource {
291
+ // editor already inlined file content
292
+ EmbeddedResourceResource::TextResourceContents(tr) => { /* tr.uri, tr.text */ }
293
+ EmbeddedResourceResource::BlobResourceContents(b) => { /* b.uri */ }
294
+ _ => {}
295
+ },
296
+ ContentBlock::ResourceLink(link) => {
297
+ // a reference (e.g. `@file`); read it yourself.
298
+ // link.uri is "file:///abs/path#L207:219" (or #L207-219). Strip "file://",
299
+ // split the "#L<start>:<end>" fragment, read & slice the lines.
300
+ }
301
+ _ => {} // non_exhaustive — always a wildcard
302
+ }
303
+ }
304
+ ```
305
+
306
+ ### `PromptResponse`
307
+
308
+ ```rust
309
+ Ok(PromptResponse::new(StopReason::EndTurn))
310
+ // other reasons: MaxTokens, Cancelled, MaxTurnRequests, Refusal
311
+ ```
312
+
313
+ ### Streaming: `ContentChunk` + `SessionUpdate` + `SessionNotification`
314
+
315
+ ```rust
316
+ let chunk = ContentChunk::new(ContentBlock::from(delta_text)); // From<Into<String>>
317
+ let update = SessionUpdate::AgentMessageChunk(chunk);
318
+ cx.send_notification(SessionNotification::new(session_id.clone(), update))?;
319
+ ```
320
+
321
+ `SessionUpdate` variants siGit uses:
322
+
323
+ - `AgentMessageChunk(ContentChunk)` — assistant text.
324
+ - `ToolCall(ToolCall)` — start a tool-call card (used for model load/download progress).
325
+ - `ToolCallUpdate(ToolCallUpdate)` — update that card's title/status/content.
326
+ - `AvailableCommandsUpdate(AvailableCommandsUpdate)` — advertise slash commands.
327
+ - `ConfigOptionUpdate(ConfigOptionUpdate)` — refresh the picker mid-session.
328
+
329
+ (Other variants exist: `UserMessageChunk`, `AgentThoughtChunk`, `Plan`, …)
330
+
331
+ ### `ToolCall` / `ToolCallUpdate` — progress cards
332
+
333
+ siGit reuses tool-call cards as a generic progress UI (model loading/download):
334
+
335
+ ```rust
336
+ // open the card
337
+ SessionUpdate::ToolCall(
338
+ ToolCall::new(tool_call_id.clone(), "Loading Qwen 2.5 3B")
339
+ .kind(ToolKind::Think)
340
+ .status(ToolCallStatus::InProgress)
341
+ .content(vec!["Loading…".into()]),
342
+ )
343
+ // update it (only the fields you set)
344
+ SessionUpdate::ToolCallUpdate(ToolCallUpdate::new(
345
+ tool_call_id.clone(),
346
+ ToolCallUpdateFields::new()
347
+ .title("✓ Qwen 2.5 3B loaded")
348
+ .status(ToolCallStatus::Completed),
349
+ ))
350
+ ```
351
+
352
+ `ToolCallStatus`: `InProgress`, `Completed`, `Failed`. `ToolKind::Think` is the
353
+ "thinking/util" kind.
354
+
355
+ ### `Error`
356
+
357
+ ```rust
358
+ agent_client_protocol::Error::new(-32603, "internal error message") // there is NO Error::internal()
359
+ agent_client_protocol::Error::new(-32602, "invalid params: …") // or Error::invalid_params()
360
+ agent_client_protocol::Error::new(-32000, "not signed in …") // app-defined
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Config options — the editor model picker
366
+
367
+ ACP lets the agent expose per-session config controls; Zed renders them in the
368
+ agent panel. siGit uses one `select` option as a model picker.
369
+
370
+ ```rust
371
+ const MODEL_CONFIG_ID: &str = "sigit-model";
372
+
373
+ let options: Vec<SessionConfigSelectOption> = models.iter().map(|m| {
374
+ SessionConfigSelectOption::new(
375
+ SessionConfigValueId::new(m.model_id.as_str()),
376
+ format!("{} {badge}", m.display_name),
377
+ ).description(desc)
378
+ }).collect();
379
+
380
+ let config_options = vec![
381
+ SessionConfigOption::select(MODEL_CONFIG_ID, "Model", current_value, options)
382
+ .category(SessionConfigOptionCategory::Model)
383
+ .description("Select an on-device model or a siGit Code Cloud tier"),
384
+ ];
385
+ ```
386
+
387
+ Return these from new/load/fork session via `.config_options(config_options)`.
388
+ When the user picks one, the client sends `SetSessionConfigOptionRequest`:
389
+
390
+ ```rust
391
+ async fn handle_set_session_config_option(&self, cx: &ConnectionTo<Client>,
392
+ args: SetSessionConfigOptionRequest) -> Result<SetSessionConfigOptionResponse> {
393
+ if args.config_id.0.as_ref() != MODEL_CONFIG_ID { return Err(Error::new(-32602, "…")); }
394
+ let model_id = args.value.0.as_ref();
395
+ // … switch model, streaming ToolCall progress via cx …
396
+ Ok(SetSessionConfigOptionResponse::new(rebuilt_config_options))
397
+ }
398
+ ```
399
+
400
+ To refresh the picker mid-session (e.g. after `/reload`), push
401
+ `SessionUpdate::ConfigOptionUpdate(ConfigOptionUpdate::new(config_options))`.
402
+
403
+ **Gotcha:** Zed re-fires the last selection on (re)connect. Guard against a no-op
404
+ re-select of the already-active model, and don't try to load a new model while a
405
+ startup load is still in flight (the old weights still hold GPU memory → the new
406
+ load fails with "does not fit"). siGit waits for `model_ready` first.
407
+
408
+ ---
409
+
410
+ ## Slash commands
411
+
412
+ Advertise them so the editor forwards `/`-prefixed input (Zed rejects unknown
413
+ slash commands client-side):
414
+
415
+ ```rust
416
+ let commands = vec![
417
+ AvailableCommand::new("help", "Show available commands"),
418
+ AvailableCommand::new("models", "List available models").input(
419
+ AvailableCommandInput::Unstructured(UnstructuredCommandInput::new(
420
+ "model number to switch to (optional)"))),
421
+ // … login/logout/whoami/reload/clear/status …
422
+ ];
423
+ cx.send_notification(SessionNotification::new(
424
+ session_id,
425
+ SessionUpdate::AvailableCommandsUpdate(AvailableCommandsUpdate::new(commands)),
426
+ ))?;
427
+ ```
428
+
429
+ siGit parses slash text out of the prompt itself (`parse_slash`) and dispatches in
430
+ `exec_slash_acp` before falling through to inference. The command turn still ends
431
+ with `Ok(PromptResponse::new(StopReason::EndTurn))`.
432
+
433
+ ---
434
+
435
+ ## Concurrency: the `block_in_place` trap (still real)
436
+
437
+ `mistralrs` model loading calls `tokio::task::block_in_place` internally, which
438
+ **panics off a multi-threaded runtime worker** ("can call blocking only when
439
+ running on the multi-threaded runtime"). The builder's task context and
440
+ `cx.spawn` tasks are not safe for this.
441
+
442
+ siGit's fix: **do the blocking model load on a dedicated `std::thread` with its
443
+ own fresh `tokio::runtime::Runtime`**, and signal completion back via an
444
+ `AtomicBool` / `oneshot` channel. Never call `load_gguf_model` directly inside a
445
+ prompt handler or a `cx.spawn` task.
446
+
447
+ ```rust
448
+ std::thread::spawn(move || {
449
+ let rt = tokio::runtime::Runtime::new().unwrap();
450
+ let result = rt.block_on(loader_engine.load_gguf_model(cfg, prompt, sampling));
451
+ // store result, flip an AtomicBool / send on a oneshot
452
+ });
453
+ ```
454
+
455
+ The prompt handler then `await`s readiness (siGit polls `model_ready` on a 1s
456
+ `tokio::time::interval`, streaming a spinner via `cx.send_notification`).
457
+
458
+ ---
459
+
460
+ ## Logging
461
+
462
+ stdout is the ACP JSON-RPC wire — **log only to stderr.** siGit uses
463
+ `tracing_subscriber` to stderr:
464
+
465
+ ```rust
466
+ tracing_subscriber::fmt::Subscriber::builder()
467
+ .with_env_filter(EnvFilter::try_from_default_env()
468
+ .unwrap_or_else(|_| EnvFilter::new("info")))
469
+ .with_writer(std::io::stderr)
470
+ .try_init();
471
+ ```
472
+
473
+ In siGit's interactive TTY mode (not ACP), it goes further and redirects the
474
+ stdout/stderr **fds** to `$TMPDIR/sigit.log` so mistralrs/native noise can't
475
+ corrupt the ratatui screen. ACP mode keeps stdout pristine for protocol JSON.
476
+
477
+ ---
478
+
479
+ ## TTY vs ACP split
480
+
481
+ `main()` decides mode from `std::io::stdin().is_terminal()`:
482
+
483
+ - **TTY** → interactive ratatui chat (`run_interactive`, Unix-only — needs fd
484
+ redirection).
485
+ - **non-TTY** → `run_acp_server()` (editor launched it over a pipe).
486
+
487
+ Account verbs (`sigit login` / `logout` / `whoami`) are handled before the split,
488
+ since the editor launches `sigit login` in an embedded terminal.
489
+
490
+ ---
491
+
492
+ ## Protocol flow
493
+
494
+ ```
495
+ Editor Agent
496
+ │── initialize ──────────────────────►│ capabilities + auth methods + meta
497
+ │◄─ InitializeResponse ───────────────│
498
+ │── authenticate ────────────────────►│ (button → verify stored session)
499
+ │◄─ AuthenticateResponse ─────────────│
500
+ │── session/new (or load / fork) ───►│ cwd, reset history
501
+ │◄─ …Response(config_options) ────────│
502
+ │◄─ session/update AvailableCommands ─│ advertise slash commands
503
+ │── session/setConfigOption ─────────►│ (model picker) → ToolCall progress
504
+ │── session/prompt ──────────────────►│ user message (text + resources)
505
+ │◄─ session/update (N×) ──────────────│ streaming chunks / tool-call cards
506
+ │◄─ PromptResponse(EndTurn) ──────────│
507
+ │── session/cancel (notification) ───►│
508
+ │── [disconnect] ─────────────────────►│ connect_to future resolves → shutdown
509
+ ```
510
+
511
+ ---
512
+
513
+ ## Zed configuration
514
+
515
+ ```json
516
+ {
517
+ "agent_servers": {
518
+ "siGit Code": {
519
+ "type": "custom",
520
+ "command": "/absolute/path/to/target/release/sigit"
521
+ }
522
+ }
523
+ }
524
+ ```
525
+
526
+ ---
527
+
528
+ ## Gotchas
529
+
530
+ 1. **No `Agent` trait to implement** — it's a builder. Register handler closures
531
+ with `.on_receive_request(closure, on_receive_request!())` and finish with
532
+ `.connect_to(transport)`. The `on_receive_request!()` / `on_receive_notification!()`
533
+ macro is mandatory per handler.
534
+ 2. **`cx: ConnectionTo<Client>` replaces the mpsc forwarder** — send notifications
535
+ with `cx.send_notification(...)` and background tasks with `cx.spawn(...)`.
536
+ Don't reintroduce the old channel-based circular-dependency pattern.
537
+ 3. **`Error::internal()` doesn't exist** — use `Error::new(-32603, msg)`.
538
+ 4. **Everything in `agent_client_protocol::schema` is `#[non_exhaustive]`** — use
539
+ builder methods, never struct literals; add `_ => …` wildcards when matching.
540
+ 5. **`ByteStreams::new(stdout, stdin)`** — writer first, reader second. Adapt
541
+ tokio stdio with `.compat()` / `.compat_write()` (tokio-util).
542
+ 6. **`block_in_place` panics in handler/`cx.spawn` tasks** — run mistralrs model
543
+ loads on a dedicated `std::thread` + its own `Runtime`; signal back via
544
+ `AtomicBool`/`oneshot`. Never load inside a prompt handler directly.
545
+ 7. **Empty `authMethods` hangs Zed** — always include at least one
546
+ `AuthMethod::Agent(AuthMethodAgent::new("id", "Name"))`. Prefer `Agent` over
547
+ `Terminal` for custom agents (Zed never spawns the terminal for them).
548
+ 8. **Never write to stdout except JSON-RPC** — log to stderr; in TTY mode siGit
549
+ redirects fds to `$TMPDIR/sigit.log`. Any stray `println!` or native library
550
+ stdout write corrupts the wire.
551
+ 9. **Unstable features gate real types** — `unstable_session_fork`,
552
+ `unstable_session_additional_directories`, `unstable_auth_methods` must be on
553
+ in `Cargo.toml` or `ForkSessionRequest`, `additional_directories`, and
554
+ `AuthMethod::Agent` won't exist.
555
+ 10. **Zed re-fires the last config selection on connect** — make
556
+ `setConfigOption` a no-op when the requested model is already active, and
557
+ never start a model switch while a startup load is still in flight (GPU OOM).
558
+ 11. **Store `SessionId` as `SessionId`**, not `String`, so `==` is clean.
559
+ 12. **`SetSessionConfigOptionResponse::new(config_options)`** — the response
560
+ carries the *rebuilt* options so the picker reflects the new current value.
561
+
562
+ ---
563
+
564
+ ## Where to look in the code
565
+
566
+ Everything ACP lives in `src/main.rs`:
567
+
568
+ - `run_acp_server` — builder wiring + transport.
569
+ - `SiGitAgent` + `handle_*` — the handlers.
570
+ - `build_model_config_options` / `resolve_model_config` — picker.
571
+ - `parse_slash` / `exec_slash_acp` — slash commands.
572
+ - `handle_response` — the `Responder` helper.
573
+
574
+ `src/backend.rs` holds the `InferenceBackend` trait (`LocalBackend` /
575
+ `OpenAiBackend`) used by `handle_prompt`'s tool-calling loop; `src/tools.rs`
576
+ defines the agent tools and `execute_tool`.
@@ -11,7 +11,7 @@ Building a local AI coding agent in Rust using Onde Inference as the LLM backend
11
11
  Onde wraps mistral.rs with a clean API for model loading, history management, and
12
12
  streaming inference across macOS (Metal), iOS, Android, Linux, and Windows.
13
13
 
14
- Crate: `onde = { path = "../onde" }` or from crates.io when published
14
+ Crate: `onde = "1.1.2"` (published on crates.io; siGit pins it in `Cargo.toml`)
15
15
  Repo: https://github.com/ondeinference/onde
16
16
  Docs: https://ondeinference.com
17
17
 
@@ -165,15 +165,26 @@ GgufModelConfig::qwen25_1_5b() // force 1.5B
165
165
  GgufModelConfig::qwen25_3b() // force 3B
166
166
  GgufModelConfig::qwen25_coder_1_5b() // coder variant 1.5B
167
167
  GgufModelConfig::qwen25_coder_3b() // coder variant 3B
168
+ GgufModelConfig::qwen25_coder_7b() // coder variant 7B (tool calling)
169
+ GgufModelConfig::qwen3_1_7b() // Qwen 3 1.7B (tool calling)
170
+ GgufModelConfig::qwen3_4b() // Qwen 3 4B (tool calling)
171
+ GgufModelConfig::qwen3_8b() // Qwen 3 8B (tool calling)
172
+ GgufModelConfig::qwen3_14b() // Qwen 3 14B (tool calling)
168
173
  ```
169
174
 
175
+ Only the Qwen 3 family and Qwen 2.5 Coder 7B support tool calling — see the
176
+ `tool-calling` skill. The on-device default is the saved selection, falling back
177
+ to `platform_default()` (Qwen 2.5 3B on macOS).
178
+
170
179
  ---
171
180
 
172
181
  ## Adding onde as a Rust library dependency
173
182
 
174
183
  ```toml
175
- # In your crate's Cargo.toml — onde is a path dep since it's not on crates.io yet
176
- onde = { path = "../onde" }
184
+ # In your crate's Cargo.toml — onde is published on crates.io
185
+ onde = "1.1.2"
186
+ # For local SDK development against a checkout, swap to a path dep:
187
+ # onde = { path = "../onde" }
177
188
  ```
178
189
 
179
190
  **Important:** `onde` declares `crate-type = ["lib", "cdylib", "staticlib"]`.
@@ -262,20 +273,19 @@ Key principles:
262
273
  ### Streaming tokens to ACP (connecting onde → ACP)
263
274
 
264
275
  ```rust
265
- // In Agent::prompt():
276
+ // In the prompt handler — cx: &ConnectionTo<Client> is passed in by the builder
277
+ // (agent-client-protocol 0.13). No mpsc forwarder; send through cx directly.
266
278
  let mut rx = self.engine.stream_message(user_text).await
267
279
  .map_err(|e| Error::new(-32603, e.to_string()))?;
268
280
 
269
281
  while let Some(chunk) = rx.recv().await {
270
282
  if !chunk.delta.is_empty() {
271
- self.notification_tx.send(
272
- SessionNotification::new(
273
- session_id.clone(),
274
- SessionUpdate::AgentMessageChunk(
275
- ContentChunk::new(ContentBlock::from(chunk.delta)),
276
- ),
277
- )
278
- ).await.ok(); // .ok() — ignore if forwarder is gone
283
+ cx.send_notification(SessionNotification::new(
284
+ session_id.clone(),
285
+ SessionUpdate::AgentMessageChunk(
286
+ ContentChunk::new(ContentBlock::from(chunk.delta)),
287
+ ),
288
+ )).ok(); // .ok() — ignore if the client is gone
279
289
  }
280
290
  if chunk.done { break; }
281
291
  }
@@ -285,7 +295,13 @@ Ok(PromptResponse::new(StopReason::EndTurn))
285
295
 
286
296
  The `PromptResponse` is returned AFTER the stream finishes. The client receives
287
297
  streaming tokens via `session/update` notifications while blocking on the
288
- `session/prompt` response.
298
+ `session/prompt` response. See the `agent-client-protocol` skill for the `cx`
299
+ (`ConnectionTo<Client>`) model that replaced the old mpsc-channel forwarder.
300
+
301
+ > **Note:** siGit's actual `handle_prompt` does *not* stream token-by-token — it
302
+ > runs a tool-calling loop through an `InferenceBackend` and sends the final text
303
+ > in one `AgentMessageChunk`. The streaming pattern above still applies if you
304
+ > want incremental output. See the `tool-calling` skill for the backend loop.
289
305
 
290
306
  ---
291
307
 
@@ -305,13 +321,18 @@ let user_text: String = args.prompt.iter()
305
321
  .join("\n");
306
322
  ```
307
323
 
308
- For future resource context (e.g. open files provided by Zed):
324
+ For resource context (e.g. open files provided by Zed) — note the variant is
325
+ `TextResourceContents`, not `Text`:
309
326
  ```rust
310
327
  ContentBlock::Resource(r) => match &r.resource {
311
- EmbeddedResourceResource::Text(t) => Some(t.text.as_str()),
328
+ EmbeddedResourceResource::TextResourceContents(t) => Some(t.text.as_str()),
329
+ EmbeddedResourceResource::BlobResourceContents(_) => None,
312
330
  _ => None,
313
331
  },
314
332
  ```
333
+ siGit also handles `ContentBlock::ResourceLink` (a `file://` reference it reads
334
+ from disk, including `#L<start>:<end>` line-range fragments). See the
335
+ `tool-calling` skill.
315
336
 
316
337
  ---
317
338
 
@@ -321,8 +342,11 @@ ContentBlock::Resource(r) => match &r.resource {
321
342
  - Safe to wrap in `Arc<ChatEngine>` and share across tasks.
322
343
  - `stream_message()` spawns a `tokio::spawn` background task internally — the
323
344
  mistralrs model must be `Send`, which it is on all supported platforms.
324
- - Calling `stream_message()` from a `!Send` future (e.g. inside a `LocalSet`) is
325
- fine the future itself doesn't hold a `!Send` value across `.await`.
345
+ - **`block_in_place` trap:** `load_gguf_model` calls `tokio::task::block_in_place`
346
+ internally, which panics unless it's on a multi-threaded runtime worker. Run
347
+ model loads on a dedicated `std::thread` with its own `tokio::runtime::Runtime`
348
+ and signal back via `AtomicBool`/`oneshot`. siGit does exactly this in both ACP
349
+ and TUI modes — see the `agent-client-protocol` and `tool-calling` skills.
326
350
 
327
351
  ---
328
352