Skip to content

tracking(serve): ACP Streamable HTTP transport — implementation status, RFD alignment & upgrade plan #4782

@chiga0

Description

@chiga0

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:

  1. Read changelog — identify changed/new/removed methods
  2. Add version branching: `if (negotiated >= 2) { /* V2 path */ }`
  3. Support V1 + V2 simultaneously during transition
  4. 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

Type Links
PRs #4472 (base) · #4563 (workspace service) · #4736 (wave 1) · #4737 (wave 2) · #4773 (WebSocket)
Issues #4542 (workspace extraction) · #4514 (daemon backlog) · #4227 (SDK upgrade) · #987 (IDE integration)
External ACP Protocol · Streamable HTTP RFD · ACP SDK · Gemini CLI ref impl

Metadata

Metadata

Assignees

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions