bots-garden/sidekickspublic Fork
⑂ v0.1.0
Commits
⬇ Clone ▾
git clone https://git.rickub.com/bots-garden/sidekicks.git
git clone ssh://git@rickub.com/bots-garden/sidekicks.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP
0001-acp.md · 79 lines · 7.4 KBmarkdown
Blame HistoryOpen raw

ADR 0001 — Talking to agents with ACP

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.

  1. Capabilities are minimal: agentCapabilities is {auth:{}, mcpCapabilities:{}, promptCapabilities:{}, sessionCapabilities:{}}. In particular no loadSession: after a restart, Sidekicks cannot reload an mm session and must re-inject the conversation context into a new one (issue #25).
  2. 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 an available_commands_update notification sent right after the session/new response, before any prompt. A command is run by sending its text in the prompt (/idioms Il pleut des cordes. → the idioms skill ran). So /garfield:idioms … in a multi-agent channel is translated by Sidekicks to /idioms … sent to Garfield only (issue #24).
  3. 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 carries kind (execute, edit, read), a human title (bash: ls, write_file: notes.txt (5 bytes)), rawInput and, for edits, absolute locations.
  4. 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 a stopReason. 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/prompt that ends with a JSON-RPC error is a normal outcome to display, not a crash.
  5. Cancellation works: session/cancel during generation ends the turn with stopReason: cancelled within a second.
  6. Usage is reported: usage_update (used, size — size is 0 until mm knows the window) and usage tokens in the prompt response.
  7. mm ignores mcpServers in session/new (logged as "ignored") and reads AGENTS.md / skills/ relative to the session cwd. The agent's workspace is therefore its folder, and the cwd sent by Sidekicks must be that folder.
  8. 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/acp wraps 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
# 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](../../.tickets/issues/0002-spike-maitriser-le-protocole-acp-et-le-valider-a.yaml)

## Context

Sidekicks must be agent-agnostic: every agent is reached through the [Agent Client Protocol](https://agentclientprotocol.com) (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](../../spikes/acp-trace)) to record the actual traffic ([docs/spikes/acp/traces](../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.

1. **Capabilities are minimal**: `agentCapabilities` is `{auth:{}, mcpCapabilities:{}, promptCapabilities:{}, sessionCapabilities:{}}`. In particular **no `loadSession`**: after a restart, Sidekicks cannot reload an mm session and must re-inject the conversation context into a new one (issue #25).
2. **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 an `available_commands_update` notification sent right after the `session/new` response, before any prompt. A command is run by sending its text in the prompt (`/idioms Il pleut des cordes.` → the `idioms` skill ran). So `/garfield:idioms …` in a multi-agent channel is translated by Sidekicks to `/idioms …` sent to Garfield only (issue #24).
3. **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 carries `kind` (`execute`, `edit`, `read`), a human `title` (`bash: ls`, `write_file: notes.txt (5 bytes)`), `rawInput` and, for edits, absolute `locations`.
4. **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 a `stopReason`. 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/prompt` that ends with a JSON-RPC error is a normal outcome to display, not a crash.
5. **Cancellation works**: `session/cancel` during generation ends the turn with `stopReason: cancelled` within a second.
6. **Usage is reported**: `usage_update` (`used`, `size` — `size` is 0 until mm knows the window) and `usage` tokens in the prompt response.
7. **mm ignores `mcpServers`** in `session/new` (logged as "ignored") and reads `AGENTS.md` / `skills/` relative to the session `cwd`. The agent's workspace is therefore its folder, and the `cwd` sent by Sidekicks must be that folder.
8. **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/acp` wraps 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).