TL;DR
Qwen-Code Daemon now implements the ACP (Agent Client Protocol) Streamable HTTP transport at `/acp`. This means ACP-native editors like Zed, Goose, and JetBrains can connect to `qwen serve` without any adapter code — just point them at `http://host:4170/acp\`.
This issue tracks the implementation status, documents how we align with the official ACP RFD, and defines how we keep up with protocol evolution.
1. Why We Built `/acp`
The Problem
Qwen-Code Daemon originally exposed a bespoke REST + SSE API (`POST /session/:id/prompt`, `GET /session/:id/events`, etc.). This works, but:
- Every new client (IDE plugin, Web UI, IM bot) must learn and implement our specific HTTP contract
- No existing editor can talk to us out-of-the-box
- We maintain our own wire format, error conventions, and capability negotiation — duplicating work the industry is standardizing
The Solution
The Agent Client Protocol (ACP) is an open standard for communication between code editors and AI coding agents. It is backed by Zed (creator), Google (Gemini CLI), JetBrains, and a growing community. ACP defines:
- A set of JSON-RPC 2.0 methods for session management, prompting, permissions, and configuration
- A transport layer (stdio for local, HTTP Streamable + WebSocket for remote)
- An extensibility model (`_meta` fields + underscore-prefixed vendor methods)
By implementing ACP's HTTP Streamable transport at `/acp`, we get:
| Benefit |
Details |
| Zero-glue integration |
Zed, Goose, JetBrains can connect directly — no adapter, no plugin, no SDK |
| Standard protocol |
Third-party tools speak ACP; they speak to us automatically |
| Dual transport |
Existing REST API stays untouched; `/acp` is additive |
| Ecosystem leverage |
ACP's SDKs (TypeScript, Python, Java, Kotlin, Rust) work with our endpoint |
What We Did NOT Do
- We did not remove the existing REST API — it stays for backward compatibility
- We did not fork the protocol — we implement the standard + vendor extensions under `_qwen/*`
- We did not block on the RFD being "stable" — the transport RFD is in Phase 1, but the protocol (methods, types) is stable and in production use by Gemini CLI and Zed
2. Architecture
```
┌─ REST surface ──── /session/, /workspace/, /file/*
qwen serve ────────┤
(Express) └─ ACP surface ──── /acp (POST + GET SSE + GET WS + DELETE)
│
▼
AcpDispatcher (transport-agnostic)
│
▼
AcpSessionBridge (session multiplexer)
│
▼
qwen --acp child (agent engine)
```
Key design decision: The `AcpDispatcher` is fully transport-agnostic. It receives parsed JSON-RPC messages and writes responses via `conn.sendConn()` / `conn.sendSession()`. Both SSE and WebSocket use the same dispatcher — adding a new transport requires zero changes to the 2000+ line dispatch file.
3. Implementation Status
PRs (merge in order)
| # |
PR |
Status |
What It Does |
| 1 |
#4472 |
Merged |
Base `/acp` transport: POST/GET/DELETE, SSE streams, JSON-RPC dispatch, core session lifecycle (initialize, session/new, load, resume, prompt, cancel, close, set_config_option) |
| 2 |
#4563 |
Open |
Refactor: extract workspace methods from bridge → `DaemonWorkspaceService` |
| 3 |
#4736 |
Open |
ACP/REST parity wave 1: 24 `_qwen/*` extension methods + ACP RFD protocol compliance (415/406/501/404) + production-grade error handling (FsError, MemoryError, AuthError mapping) |
| 4 |
#4737 |
Open |
ACP/REST parity wave 2: 5 agents CRUD methods |
| 5 |
#4773 |
Open |
WebSocket transport: WS upgrade alongside SSE, bearer auth, TransportStream abstraction |
After all PRs merge: 53 ACP methods
Full method list (click to expand)
Standard ACP methods (13):
`initialize`, `authenticate`, `session/new`, `session/load`, `session/resume`, `session/list`, `session/close`, `session/cancel`, `session/prompt`, `session/set_config_option`, `session/set_mode`, `session/update` (event), `session/request_permission` (event)
`_qwen/session/*` extensions (10):
`heartbeat`, `context`, `context_usage`, `supported_commands`, `update_metadata`, `recap`, `btw`, `shell`, `detach`, `tasks`
`_qwen/workspace/*` extensions (11):
`mcp`, `mcp/tools`, `mcp/servers/add`, `mcp/servers/remove`, `skills`, `tools`, `providers`, `env`, `preflight`, `init`, `set_tool_enabled`, `restart_mcp_server`
`_qwen/workspace/memory` (2):* read, write
`_qwen/file/*` (7): read, read_bytes, stat, list, glob, write, edit
`_qwen/workspace/auth/*` (4): status, device_flow/start, device_flow/get, device_flow/cancel
`_qwen/workspace/agents/*` (5): list, get, create, update, delete
`_qwen/sessions/*` (1): delete (batch)
4. RFD Alignment Report
Reference: ACP Streamable HTTP & WebSocket Transport RFD (last updated 2026-05-04)
What We Implement Correctly ✅
| Requirement |
Evidence |
| Single `/acp` endpoint |
`acpHttp/index.ts`: POST + GET + DELETE on one path |
| POST `initialize` → 200 + JSON + `Acp-Connection-Id` header |
`index.ts` line 132-167 |
| POST other methods → 202 Accepted |
`index.ts` line 202-203 |
| Connection-scoped SSE stream (GET with `Acp-Connection-Id` only) |
`index.ts` line 215-249 |
| Session-scoped SSE stream (GET with both headers) |
`index.ts` line 251-306 |
| DELETE → connection teardown |
`index.ts` line 308-334 |
| Missing `Acp-Connection-Id` → 400 |
`index.ts` line 176-186 |
| Unknown `Acp-Connection-Id` → 404 |
`index.ts` line 188-200 |
| Non-JSON Content-Type → 415 |
`index.ts` line 107-111 |
| Missing `text/event-stream` Accept → 406 |
`index.ts` line 218-221 |
| Batch JSON-RPC arrays → 501 |
`index.ts` line 114-119 |
| `agentCapabilities` with `loadSession`, `promptCapabilities`, `sessionCapabilities` |
`dispatch.ts` buildInitializeResult |
| `agentInfo` for client display |
`dispatch.ts` `{ name: "qwen-code", version: "..." }` |
| WebSocket upgrade on GET `/acp` with `Upgrade: websocket` |
`index.ts` WS handler (PR #4773) |
| Bearer token auth on WS upgrade |
`index.ts` manual token check before `handleUpgrade` |
| Permission flow as JSON-RPC request→response |
`dispatch.ts` translateEvent + resolveClientResponse |
| `_meta` + underscore-prefixed vendor extensions |
`_qwen/*` namespace |
Known Deviations from RFD
| RFD Requirement |
Our Status |
Impact |
Rationale |
| HTTP/2 required |
HTTP/1.1 (Express default) |
Low |
Gemini CLI reference impl also uses HTTP/1.1; H2 multiplexing benefit is marginal with SSE long-connections |
| Cookie support |
Not implemented |
None |
Our trust model is bearer-token, not session-cookie; daemon is single-user |
| SSE resumability (`Last-Event-ID`) |
Not implemented on `/acp` |
Low |
RFD marks as Phase 4 (future). Our REST SSE already has resumability via EventBus ring buffer; `/acp` SSE can add it later |
| `unstable_forkSession` |
Not implemented |
Low |
Marked UNSTABLE in SDK; no current consumer |
| `logout` method |
Not implemented |
Low |
Bearer token revocation is out-of-scope for local daemon |
SDK Version Gap
|
Current |
Latest |
| `@agentclientprotocol/sdk` |
0.14.1 |
0.21.0 |
See #4227 for the upgrade tracking. Key unlocks:
- `session/close` stabilized as standard method
- `session/resume` stabilized
- `session/list` stabilized
- New types for conformance checking
5. Can a Zed / Goose / Third-Party Agent Client Connect Today?
Yes, once PRs #4563 → #4736 → #4737 merge. The connection flow:
```
Client Daemon (/acp)
│ POST { initialize } │
│────────────────────────────────→│
│ 200 + capabilities + Acp-Connection-Id
│←────────────────────────────────│
│ │
│ GET (connection SSE stream) │
│════════════════════════════════▶│
│ │
│ POST { session/new } │
│────────────────────────────────→│ 202
│←═══ result on conn SSE ════════│
│ │
│ GET (session SSE stream) │
│════════════════════════════════▶│
│ │
│ POST { session/prompt } │
│────────────────────────────────→│ 202
│←═══ session/update events ═════│ (streaming)
│ │
│ Agent needs permission: │
│←═══ session/request_permission ═│ (JSON-RPC request)
│ POST { response: allow } │
│────────────────────────────────→│
│ │
│ POST { session/set_mode } │
│────────────────────────────────→│ 202
│←═══ result on session SSE ═════│
```
Vendor extensions (`_qwen/*`) are discoverable via `agentCapabilities._meta.qwen.methods` in the `initialize` response. Standard ACP clients ignore unknown extensions gracefully.
6. How We Stay in Sync with ACP Evolution
Version Pinning
```jsonc
// package.json — pin patch, upgrade minor via PR review
"@agentclientprotocol/sdk": "~0.14.1"
```
Breaking Change Strategy
ACP signals breaking changes through `protocolVersion` increment:
```typescript
// dispatch.ts — version negotiation
const negotiated = Math.max(1, Math.min(requested, ACP_PROTOCOL_VERSION));
```
When ACP ships V2:
- Read changelog — identify changed/new/removed methods
- Add version branching: `if (negotiated >= 2) { /* V2 path */ }`
- Support V1 + V2 simultaneously during transition
- Deprecate V1 after ecosystem adoption
Type Safety
Import SDK protocol types in dispatch (planned follow-up):
```typescript
import type { InitializeResponse, AgentCapabilities } from '@agentclientprotocol/sdk';
// TS compiler catches protocol drift on SDK upgrade
```
Monitoring Checklist
7. Next Steps
Related
TL;DR
Qwen-Code Daemon now implements the ACP (Agent Client Protocol) Streamable HTTP transport at `/acp`. This means ACP-native editors like Zed, Goose, and JetBrains can connect to `qwen serve` without any adapter code — just point them at `http://host:4170/acp\`.
This issue tracks the implementation status, documents how we align with the official ACP RFD, and defines how we keep up with protocol evolution.
1. Why We Built `/acp`
The Problem
Qwen-Code Daemon originally exposed a bespoke REST + SSE API (`POST /session/:id/prompt`, `GET /session/:id/events`, etc.). This works, but:
The Solution
The Agent Client Protocol (ACP) is an open standard for communication between code editors and AI coding agents. It is backed by Zed (creator), Google (Gemini CLI), JetBrains, and a growing community. ACP defines:
By implementing ACP's HTTP Streamable transport at `/acp`, we get:
What We Did NOT Do
2. Architecture
```
┌─ REST surface ──── /session/, /workspace/, /file/*
qwen serve ────────┤
(Express) └─ ACP surface ──── /acp (POST + GET SSE + GET WS + DELETE)
│
▼
AcpDispatcher (transport-agnostic)
│
▼
AcpSessionBridge (session multiplexer)
│
▼
qwen --acp child (agent engine)
```
Key design decision: The `AcpDispatcher` is fully transport-agnostic. It receives parsed JSON-RPC messages and writes responses via `conn.sendConn()` / `conn.sendSession()`. Both SSE and WebSocket use the same dispatcher — adding a new transport requires zero changes to the 2000+ line dispatch file.
3. Implementation Status
PRs (merge in order)
After all PRs merge: 53 ACP methods
Full method list (click to expand)
Standard ACP methods (13):
`initialize`, `authenticate`, `session/new`, `session/load`, `session/resume`, `session/list`, `session/close`, `session/cancel`, `session/prompt`, `session/set_config_option`, `session/set_mode`, `session/update` (event), `session/request_permission` (event)
`_qwen/session/*` extensions (10):
`heartbeat`, `context`, `context_usage`, `supported_commands`, `update_metadata`, `recap`, `btw`, `shell`, `detach`, `tasks`
`_qwen/workspace/*` extensions (11):
`mcp`, `mcp/tools`, `mcp/servers/add`, `mcp/servers/remove`, `skills`, `tools`, `providers`, `env`, `preflight`, `init`, `set_tool_enabled`, `restart_mcp_server`
`_qwen/workspace/memory` (2):* read, write
`_qwen/file/*` (7): read, read_bytes, stat, list, glob, write, edit
`_qwen/workspace/auth/*` (4): status, device_flow/start, device_flow/get, device_flow/cancel
`_qwen/workspace/agents/*` (5): list, get, create, update, delete
`_qwen/sessions/*` (1): delete (batch)
4. RFD Alignment Report
Reference: ACP Streamable HTTP & WebSocket Transport RFD (last updated 2026-05-04)
What We Implement Correctly ✅
Known Deviations from RFD
SDK Version Gap
See #4227 for the upgrade tracking. Key unlocks:
5. Can a Zed / Goose / Third-Party Agent Client Connect Today?
Yes, once PRs #4563 → #4736 → #4737 merge. The connection flow:
```
Client Daemon (/acp)
│ POST { initialize } │
│────────────────────────────────→│
│ 200 + capabilities + Acp-Connection-Id
│←────────────────────────────────│
│ │
│ GET (connection SSE stream) │
│════════════════════════════════▶│
│ │
│ POST { session/new } │
│────────────────────────────────→│ 202
│←═══ result on conn SSE ════════│
│ │
│ GET (session SSE stream) │
│════════════════════════════════▶│
│ │
│ POST { session/prompt } │
│────────────────────────────────→│ 202
│←═══ session/update events ═════│ (streaming)
│ │
│ Agent needs permission: │
│←═══ session/request_permission ═│ (JSON-RPC request)
│ POST { response: allow } │
│────────────────────────────────→│
│ │
│ POST { session/set_mode } │
│────────────────────────────────→│ 202
│←═══ result on session SSE ═════│
```
Vendor extensions (`_qwen/*`) are discoverable via `agentCapabilities._meta.qwen.methods` in the `initialize` response. Standard ACP clients ignore unknown extensions gracefully.
6. How We Stay in Sync with ACP Evolution
Version Pinning
```jsonc
// package.json — pin patch, upgrade minor via PR review
"@agentclientprotocol/sdk": "~0.14.1"
```
Breaking Change Strategy
ACP signals breaking changes through `protocolVersion` increment:
```typescript
// dispatch.ts — version negotiation
const negotiated = Math.max(1, Math.min(requested, ACP_PROTOCOL_VERSION));
```
When ACP ships V2:
Type Safety
Import SDK protocol types in dispatch (planned follow-up):
```typescript
import type { InitializeResponse, AgentCapabilities } from '@agentclientprotocol/sdk';
// TS compiler catches protocol drift on SDK upgrade
```
Monitoring Checklist
7. Next Steps
Related