Skip to content

Commit 68d0748

Browse files
brettchienclaude
andcommitted
docs(acp): as-built Phase 1 ADR + official method coverage
Absorbs the pending ADR proposal from #1258 and adds two companion docs: - `docs/adr/acp-server-websocket.md` — the original proposal (@pahud), verbatim. - `docs/adr/acp-server-websocket-phase1.md` — the as-built Phase 1 ADR: the wire-conformant primitive surface, the `session/resume` (not `session/load`) decision with its rationale, and divergences from the proposal. - `docs/acp-official-methods.md` — the official ACP method surface pinned to Schema v1.19.0, with OpenAB's Phase 1 coverage and intentional non-support. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 8ab31b6 commit 68d0748

3 files changed

Lines changed: 461 additions & 0 deletions

File tree

docs/acp-official-methods.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# ACP — Official Method Surface & OpenAB Coverage
2+
3+
Reference list of the **official** Agent Client Protocol methods/notifications, and
4+
how OpenAB's Phase 1 ACP server (`docs/adr/acp-server-websocket-phase1.md`) maps onto
5+
them. Phase 1 targets **wire conformance** for the chat subset, so standard ACP
6+
clients (Zed, JetBrains, …) interoperate.
7+
8+
### Provenance / version pin
9+
10+
This table is built against a specific ACP revision — pin it so future diffs are
11+
traceable:
12+
13+
| Field | Value |
14+
|---|---|
15+
| Spec docs | <https://agentclientprotocol.com/protocol/overview>, <https://agentclientprotocol.com/protocol/schema> |
16+
| Governance repo | <https://github.com/agentclientprotocol/agent-client-protocol> |
17+
| **Schema release** | **v1.19.0** (latest on GitHub releases as of fetch date) |
18+
| **Rust crate release** | **v1.4.0** |
19+
| Fetched | 2026-07-17 |
20+
| Wire `protocolVersion` | integer **`1`** (single MAJOR version, negotiated at `initialize`) |
21+
22+
> When re-checking conformance later, bump this block to the new Schema/crate release
23+
> and re-diff the tables below.
24+
25+
Directions use the ACP roles: the **Agent** answers prompts (here, OpenAB); the
26+
**Client** is the app/UI (browser, Zed, CLI).
27+
28+
## Agent methods (Client → Agent, request/response)
29+
30+
| Method | Purpose | OpenAB Phase 1 |
31+
|---|---|---|
32+
| `initialize` | Negotiate protocol + capabilities | ✅ conformant (`protocolVersion:1`, `agentCapabilities`, `authMethods:[]`) |
33+
| `authenticate` | Authenticate via a declared auth method | ⛔ (we use a pre-connect token on the WS upgrade; `authMethods:[]`) |
34+
| `logout` | Drop authenticated state ||
35+
| `session/new` | Create a new session | ✅ (`{cwd, mcpServers}` accepted; returns `{sessionId}`) |
36+
| `session/load` | Load a session **with** history replay |**by design**`loadSession:false` (no upstream transcript to replay; see ADR §3) |
37+
| `session/resume` | Resume a session **without** replay | ✅ (`{sessionId, cwd, mcpServers?}``{}`) |
38+
| `session/prompt` | Process a user prompt | ✅ (streams `session/update`, returns `{stopReason}`) |
39+
| `session/close` | Close a session | ⛔ (cleanup on WS disconnect) |
40+
| `session/list` | List known sessions ||
41+
| `session/delete` | Delete a session ||
42+
| `session/set_config_option` | Set a session config option ||
43+
| `session/set_mode` | Set the session mode ||
44+
45+
## Notifications
46+
47+
| Method | Direction | Purpose | OpenAB Phase 1 |
48+
|---|---|---|---|
49+
| `session/cancel` | Client → Agent | Cancel in-flight work (one-way, no response) | ✅ conformant (notification; prompt ends `stopReason:"cancelled"`) |
50+
| `session/update` | Agent → Client | Stream session events |`agent_message_chunk` (text). Other variants (`tool_call`, `tool_call_update`, `plan`, …) are Phase 2 |
51+
| `$/cancel_request` | Bidirectional | Cancel an in-flight JSON-RPC request ||
52+
53+
## Client methods (Agent → Client, request/response)
54+
55+
The agent runs server-side with its own fs/terminal, so OpenAB does not call any of
56+
these in Phase 1.
57+
58+
| Method | Purpose | OpenAB Phase 1 |
59+
|---|---|---|
60+
| `session/request_permission` | Ask the client to approve a tool call | ⛔ (Phase 2) |
61+
| `fs/read_text_file` / `fs/write_text_file` | Read/write a text file on the client ||
62+
| `terminal/create` / `output` / `wait_for_exit` / `kill` / `release` | Drive a client terminal ||
63+
64+
## Conformance status (Phase 1)
65+
66+
The chat subset is **wire-conformant** with ACP Schema v1.19.0:
67+
68+
- `initialize` → integer `protocolVersion:1`, official `agentCapabilities` shape
69+
(`sessionCapabilities.resume`, `loadSession:false`, `promptCapabilities`), `authMethods:[]`.
70+
- Streaming → `session/update` + `sessionUpdate:"agent_message_chunk"` + `content` ContentBlock.
71+
- `stopReason` → official snake_case (`end_turn` / `cancelled`).
72+
- `session/cancel` → one-way notification.
73+
- Resume → `session/resume` (no replay), gated by `sessionCapabilities.resume`.
74+
75+
### Intentional non-support (documented, not a gap to close in Phase 1)
76+
77+
- **`session/load`** — needs an upstream conversation transcript OpenAB does not keep
78+
(history lives in the downstream agent CLI). Advertised as `loadSession:false`.
79+
- **`authenticate`/`logout`, ContentBlock non-text, tool-call updates,
80+
`request_permission`, fs/terminal, session admin (`list`/`delete`/config/mode)**
81+
deferred to later phases per the roadmap.
82+
83+
### To verify against a live client
84+
85+
- Field-level exactness of `agentCapabilities` / `clientCapabilities` sub-objects and
86+
the `ContentBlock` variants beyond `text`, by connecting a real ACP client (e.g.
87+
Zed) and diffing the `initialize` + `session/prompt` exchange against the schema.
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# ADR: ACP Server over WebSocket — Phase 1 (as-built)
2+
3+
- **Status:** Accepted
4+
- **Date:** 2026-07-17
5+
- **Author:** @brettchien
6+
- **Related:** [ADR: ACP Server with WebSocket Transport](./acp-server-websocket.md) (original proposal, @pahud)
7+
- **Conformance:** official ACP Schema **v1.19.0** — see [acp-official-methods.md](../acp-official-methods.md)
8+
- **Implementation:** this PR (revives and completes #1260)
9+
10+
---
11+
12+
## 1. Context
13+
14+
The original proposal ([acp-server-websocket.md](./acp-server-websocket.md)) defines
15+
the full ACP-server vision across five phases. This ADR is the **as-built record of
16+
Phase 1** — the concrete, **wire-conformant** primitive surface the implementation
17+
ships and that future work should follow.
18+
19+
Scope: a standard-ACP **1:1 streaming chat** endpoint for real ACP clients (browser,
20+
desktop, IDE, CLI) over WebSocket. Not in Phase 1: tool calls / permissions, client
21+
fs/terminal methods, multi-agent fan-out, Streamable HTTP.
22+
23+
Design goal (per decision on 2026-07-17): **follow the official ACP guide** so
24+
third-party ACP clients (Zed, JetBrains, …) interoperate — no custom method names.
25+
26+
## 2. Decision — Phase 1 primitive surface (ACP-conformant)
27+
28+
Transport: `GET /acp`, feature-gated `acp` + runtime `OPENAB_ACP_ENABLED`. Token auth
29+
on the WS upgrade via timing-safe compare (`subtle::ConstantTimeEq`,
30+
`OPENAB_ACP_AUTH_KEY`). JSON-RPC 2.0; non-`"2.0"` rejected with `-32600`.
31+
32+
### Client → Agent (requests)
33+
34+
| Method | Params | Result |
35+
|---|---|---|
36+
| `initialize` | `{ protocolVersion: 1, clientCapabilities?, clientInfo? }` | `{ protocolVersion: 1, agentCapabilities, agentInfo, authMethods: [] }` |
37+
| `session/new` | `{ cwd, mcpServers }` | `{ sessionId }` |
38+
| `session/resume` | `{ sessionId, cwd, mcpServers? }` | `{}` (no history replay) |
39+
| `session/prompt` | `{ sessionId, prompt: [ContentBlock] }` | `{ stopReason }` |
40+
41+
`agentCapabilities` advertises `sessionCapabilities.resume` (we support resume) and
42+
`loadSession: false` (we cannot replay history — see §3). `promptCapabilities` are
43+
all `false` in Phase 1 (text only). `protocolVersion` is the integer `1`.
44+
45+
### Client → Agent (notification)
46+
47+
| Method | Params | Effect |
48+
|---|---|---|
49+
| `session/cancel` | `{ sessionId }` | one-way; in-flight prompt ends with `stopReason:"cancelled"`. No response. |
50+
51+
### Agent → Client (notification)
52+
53+
- `session/update` with `update.sessionUpdate = "agent_message_chunk"` and
54+
`update.content = { type:"text", text: <delta> }` — streamed reply text. Delta is
55+
sliced char-boundary-safe (`str::get`, never byte-index) so CJK / 顏文字 / emoji
56+
cannot panic the stream.
57+
- Turn completion is the `session/prompt` **response** (`{ stopReason }`, correlated
58+
to the request id), not a separate notification. `stopReason``end_turn` /
59+
`cancelled`. A backend timeout has no ACP stopReason, so it returns a JSON-RPC
60+
error (`-32603`) instead.
61+
62+
### Session ↔ core mapping
63+
64+
- `sessionId = sess_<uuid>` and `channel_id = acp_<uuid>` share one uuid, so
65+
`channel_id` is always re-derivable from a persisted `sessionId`.
66+
- Prompts become a `GatewayEvent` (`platform:"acp"`, `channel:acp_<uuid>`); core
67+
keys continuity by `session_key = acp:<channel_id>`.
68+
69+
## 3. Resume — why `session/resume`, not `session/load`
70+
71+
ACP distinguishes `session/load` (agent **replays** history via `session/update`,
72+
then responds) from `session/resume` (restores context, **MUST NOT** replay). We
73+
implement **`session/resume`**, decided against `crates/openab-core/src/acp/pool.rs`:
74+
75+
- The conversation history lives inside the **downstream** coding-agent CLI's session
76+
(claude / codex / kiro). The core only persists a `thread_key → agent sessionId`
77+
mapping — it does **not** hold a replayable upstream transcript. So the gateway
78+
cannot satisfy `session/load`'s replay contract; `loadSession: false`.
79+
- Continuation still works: on the next prompt, core recovers the underlying agent
80+
session via its persisted mapping + downstream `session/load` (this survives a
81+
process restart, within the agent's retention / `session_ttl_hours`, default 4h).
82+
- `resume` therefore restores context without replay; the **client** keeps its own
83+
transcript for display. `session/resume` returns `{}` immediately.
84+
85+
Whether the core session is still alive is **not observable** at the gateway — an
86+
expired session silently starts fresh, and the core prefixes its first reply with a
87+
"Session expired" notice the client can surface.
88+
89+
Security: `sessionId` is a server-minted, high-entropy capability; `session/resume`
90+
requires a well-formed `sess_<uuid>`, keeping the channel inside the `acp_` namespace
91+
and rejecting forged ids.
92+
93+
## 4. Divergences from the original proposal
94+
95+
| Proposal (Phase 1) | As-built | Why |
96+
|---|---|---|
97+
| Add `agent-client-protocol` crate dep | removed — hand-rolled JSON-RPC | fewer deps; small surface |
98+
| "Bearer token auth" | `subtle::ConstantTimeEq` + `OPENAB_ACP_AUTH_KEY` | timing-safe, no new dep |
99+
| Resume in **Phase 3** | `session/resume` in **Phase 1** | core continuity is already channel-keyed + persisted, so a gateway-only change buys reconnect resume cheaply |
100+
101+
## 5. Consequences & limits
102+
103+
- **1:1 only** — reply registry is `channel_id → single reply_tx`; the delta stream
104+
assumes one monotonic text. Multi-agent fan-out is Phase 4 (would corrupt this).
105+
- **cwd / mcpServers** — accepted on `session/new` / `session/resume` for wire
106+
conformance but not yet propagated into the agent (follow-up).
107+
- **Emoji** — inline 顏文字 flow through as text; reaction emoji stay no-op in Phase 1.
108+
- **Reconnect** — on WS disconnect the per-connection session map is dropped; the
109+
client reconnects with `session/resume` + its persisted `sessionId`.
110+
111+
## 6. Roadmap (from proposal; resume pulled into Phase 1)
112+
113+
- **Phase 2** — tool calls: `session/update` variants `tool_call` / `tool_call_update`,
114+
and `session/request_permission`; reaction emoji → updates
115+
- **Phase 3**`session/load` with history replay (needs an upstream transcript store)
116+
- **Phase 4** — multi-agent fan-out
117+
- **Phase 5** — Streamable HTTP (POST + SSE) on the same `/acp`
118+
119+
## 7. References
120+
121+
- Original proposal: [acp-server-websocket.md](./acp-server-websocket.md)
122+
- Official method surface + coverage: [acp-official-methods.md](../acp-official-methods.md)

0 commit comments

Comments
 (0)