sigit-code 1.2.0__tar.gz → 1.2.1__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.
- sigit_code-1.2.1/.agents/skills/agent-client-protocol/SKILL.md +576 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.agents/skills/ai-assisted-coding/SKILL.md +41 -17
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.agents/skills/sigit-code-release/SKILL.md +3 -2
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.agents/skills/tool-calling/SKILL.md +95 -39
- {sigit_code-1.2.0 → sigit_code-1.2.1}/CHANGELOG.md +11 -0
- sigit_code-1.2.1/CLAUDE.md +105 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/Cargo.lock +23 -1
- {sigit_code-1.2.0 → sigit_code-1.2.1}/Cargo.toml +3 -2
- {sigit_code-1.2.0 → sigit_code-1.2.1}/PKG-INFO +1 -1
- {sigit_code-1.2.0 → sigit_code-1.2.1}/src/account.rs +54 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/src/chat.rs +38 -17
- {sigit_code-1.2.0 → sigit_code-1.2.1}/src/main.rs +388 -48
- {sigit_code-1.2.0 → sigit_code-1.2.1}/src/provider.rs +1 -1
- sigit_code-1.2.0/.agents/skills/agent-client-protocol/SKILL.md +0 -319
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.agents/AGENTS.md +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.agents/skills/branding/SKILL.md +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.github/workflows/ci.yml +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.github/workflows/release-crates.yml +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.github/workflows/release-github.yml +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.github/workflows/release-homebrew.yml +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.github/workflows/release-npm.yml +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.github/workflows/release-pypi.yml +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.gitignore +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/.nvmrc +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/LICENSE +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/README.md +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/npm/README.md.tmpl +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/npm/package-main.json.tmpl +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/npm/package.json.tmpl +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/npm/scripts/render-main-package.cjs +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/npm/scripts/render-platform-package.cjs +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/npm/sigit/.gitignore +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/npm/sigit/README.md +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/npm/sigit/package.json +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/npm/sigit/src/index.ts +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/npm/sigit/tsconfig.json +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/pypi/README.md +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/pypi/pyproject.toml +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/pyproject.toml +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/rust-toolchain.toml +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/src/backend.rs +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/src/credentials.rs +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/src/models.rs +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/src/setup.rs +0 -0
- {sigit_code-1.2.0 → sigit_code-1.2.1}/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 =
|
|
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
|
|
176
|
-
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
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
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::
|
|
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
|
-
-
|
|
325
|
-
|
|
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
|
|