ADR 0001 — Talking to agents with ACP
- Status: accepted
- Date: 2026-10-29
- Issue: #2 — Spike : maîtriser le protocole ACP et le valider avec mm
Context
Sidekicks must be agent-agnostic: every agent is reached through the Agent Client Protocol (ACP), JSON-RPC 2.0 over the agent's stdin/stdout. The test agents are mm v1.0.3 agents (Alice, Bob, Riker, Garfield). This spike read the specification and drove the real agents with a throwaway client (spikes/acp-trace) to record the actual traffic (docs/spikes/acp/traces).
Decisions
1. Target ACP protocol version 1
The site now documents a v2 (renamed methods such as session/resume, auth/login, a prompt response sent on acceptance with completion reported by a state_update). mm v1.0.3 answers initialize with protocolVersion: 1, and the Go SDK below implements v1. Sidekicks sends protocolVersion: 1 and refuses (with a clear message) an agent that answers with a version it does not support. v2 support is a later, separate change, isolated behind our own internal/acp interface (see ADR 0003).
2. Use github.com/coder/acp-go-sdk (v0.13.5)
| Criterion | coder/acp-go-sdk | Hand-written JSON-RPC |
|---|---|---|
| Licence | Apache-2.0 (compatible with our licence, notice to keep) | — |
| Coverage | All v1 methods and update types, generated from the official schema (types_gen.go) |
Only what we write |
| Maintenance | Listed on the official site, regular releases (v0.13.5, 2026-06) | Ours forever |
| Go version | go 1.21, no transitive dependencies |
— |
| Proven here | Drove Alice, Riker and Garfield without a single protocol error | — |
Other Go libraries listed by the ACP site (ironpark/acp-go, eino-contrib/acp, …) are younger (v0.x with few releases) and not needed. The SDK is wrapped by internal/acp so the rest of the code never imports it directly; replacing it, or adding v2, touches one package.
3. One ACP session per (channel, agent)
ACP is one client ↔ one agent. A Sidekicks channel can contain several agents, so each member of a channel gets its own ACP session (session/new with the agent's workspace as cwd). One agent process serves all its sessions; the routing between agents of a channel is Sidekicks' job (issue #23).
4. Methods and notifications used
| Direction | Method / update | Used for | Milestone |
|---|---|---|---|
| C → A | initialize |
version + capabilities; store agentInfo (name, title) |
M1 |
| C → A | session/new |
one per (channel, agent) | M2 |
| C → A | session/prompt |
a user turn; the response carries stopReason and usage tokens |
M2 |
| C → A | session/cancel (notification) |
the Stop button; the turn ends with stopReason: cancelled |
M2 |
| A → C | session/update · agent_message_chunk |
streamed Markdown text | M2 |
| A → C | session/update · agent_thought_chunk |
"thinking…" indicator (folded) | M2 |
| A → C | session/update · tool_call / tool_call_update |
tool cards with status pending → in_progress → completed / failed |
M2 |
| A → C | session/request_permission |
the permission dialog | M2 |
| A → C | session/update · usage_update |
context usage gauge (used / size) |
M2 |
| A → C | session/update · available_commands_update |
the / popup |
M3 |
| A → C | session/update · plan |
plan display | M3 |
| C → A | session/load |
only if the agent advertises loadSession (mm does not, see below) |
M3 |
Not used for now: authenticate (mm returns authMethods: []), session/set_mode and config options (not advertised by mm), elicitation/* (advertised as false by us; mm's ask_user tool falls back to plain text), client fs/* and terminal/* capabilities (see decision 5).
Unknown session/update kinds must be ignored, not treated as errors: agents add new ones over time.
5. Client capabilities fs and terminal: not advertised at first
mm uses its own tools (bash, read/write/edit file) and never asked for client file or terminal access in the traces. Advertising them would only matter for agents that delegate to the client. Issue #32 stays low priority and reuses the Workspace abstraction when it comes.
Facts observed on mm v1.0.3
These come from the traces and drive later issues.
- Capabilities are minimal:
agentCapabilitiesis{auth:{}, mcpCapabilities:{}, promptCapabilities:{}, sessionCapabilities:{}}. In particular noloadSession: after a restart, Sidekicks cannot reload an mm session and must re-inject the conversation context into a new one (issue #25). - Slash commands = built-ins + skills: every mm agent advertises
compact,new,unload; Garfield adds its skills (email-letter,idioms,styles,technical-text). They arrive in anavailable_commands_updatenotification sent right after thesession/newresponse, before any prompt. A command is run by sending its text in the prompt (/idioms Il pleut des cordes.→ theidiomsskill ran). So/garfield:idioms …in a multi-agent channel is translated by Sidekicks to/idioms …sent to Garfield only (issue #24). - Permission options come with stable ids:
allow(allow_once),allow_always("Allow bash for this session"),reject(reject_once),never(reject_always, "Never allow bash in this session"). The tool call carrieskind(execute,edit,read), a humantitle(bash: ls,write_file: notes.txt (5 bytes)),rawInputand, for edits, absolutelocations. - A small model loops on
reject_once: Riker retried the refused write 40 times with variations (echo >,printf >, heredoc,/tmp…) until mm aborted the turn with a JSON-RPC error (-32603, "exceeded maximum tool call iterations (40)") instead of astopReason. Consequences for the permission UX (issue #17):- the dialog offers "Stop the agent" (sends
session/cancel) next to Allow / Reject; - "Reject" for a citizen user should map to
reject_always("never in this session") when the agent offers it, with "reject once" as a secondary choice; - after N refused calls in the same turn, Sidekicks proposes to stop the turn;
- a
session/promptthat ends with a JSON-RPC error is a normal outcome to display, not a crash.
- the dialog offers "Stop the agent" (sends
- Cancellation works:
session/cancelduring generation ends the turn withstopReason: cancelledwithin a second. - Usage is reported:
usage_update(used,size—sizeis 0 until mm knows the window) andusagetokens in the prompt response. - mm ignores
mcpServersinsession/new(logged as "ignored") and readsAGENTS.md/skills/relative to the sessioncwd. The agent's workspace is therefore its folder, and thecwdsent by Sidekicks must be that folder. - stderr is chatty and useful (
[acp] turn done: 1 command(s) · 0 file op(s) · 1 skill(s)): it is captured per agent and shown in the Agents panel logs (issue #10, #27), never mixed with stdout.
Consequences
internal/acpwraps the SDK, exposes Sidekicks events (not SDK types) to the rest of the app, and is unit-tested with a fake agent built on the SDK's agent side.- The real-agent tests always go through
scripts/agents.sh run <agent>, so the two-agents limit and model unloading apply to them too. - The SDK's Apache-2.0 notice must be shipped with the binary (issue #36).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
|