Skip to content

Replace ACP with Claude Code-like SDK protocol for IDE integration #1178

@lewis617

Description

@lewis617

Problem

The current IDE integration uses the Agent Client Protocol (ACP) (@agentclientprotocol/sdk v0.17.1), which has several limitations compared to how Claude Code communicates with its VS Code extension:

ACP Limitations

  1. Third-party protocol dependency: ACP is an external package with its own schema/maintenance cycle. Any protocol changes require upstream coordination.
  2. No stream-json mode: ACP uses JSON-RPC method calls (prompt(), requestPermission()) with callback-based updates. There is no equivalent to Claude Code's --output-format stream-json NDJSON streaming, which is simpler and more debuggable.
  3. Permission flow is request/response: ACP's requestPermission is a blocking JSON-RPC call. Claude Code's control_request/control_response pattern is non-blocking — the CLI sends a request and the extension responds asynchronously, allowing the CLI to keep processing other events.
  4. No --permission-prompt-tool stdio: Claude Code lets the SDK host handle all permission decisions via stdin/stdout. ACP requires the AgentSideConnection to stay alive for the permission roundtrip, creating tighter coupling.
  5. No init handshake flexibility: ACP's initialize() is rigid. Claude Code's initialize control message supports systemPrompt, appendSystemPrompt, hooks, allowedTools, jsonSchema, and more — all configurable at init time.
  6. Mode/model switching requires dedicated RPC methods: ACP needs setSessionMode() and setSessionConfigOption() RPC methods. Claude Code uses the same control_request envelope with subtype discriminators, keeping the protocol flat and extensible.

Proposed Solution

Replace ACP with a Claude Code-compatible SDK protocol — a bidirectional NDJSON protocol over stdin/stdout that mirrors how claude --output-format stream-json works.

Architecture

VS Code / JetBrains / POPO Extension (SDK Host)
    │
    │  stdin/stdout (NDJSON)
    │
    ▼
wave-code --output-format stream-json [--permission-prompt-tool stdio]

Protocol Design

Based on Claude Code's StructuredIO + controlTypes + controlSchemas:

CLI → Extension (stdout, one NDJSON line per message)

Message Type Purpose
assistant Streaming assistant content (text, tool_use, thinking)
tool_result Tool execution result
result Turn complete (with cost, duration, stop_reason)
control_request Ask extension for permission or elicitation (subtype: can_use_tool)
session_state_changed Session state transition (idle → busy → requires_action → idle)
system System messages (mode changes, warnings, errors)

Extension → CLI (stdin, one NDJSON line per message)

Message Type Purpose
user User text/image input
control_request Extension-initiated control (subtype: set_permission_mode, set_model, set_max_thinking_tokens, interrupt)
control_response Response to a CLI control_request (allow/deny permission)
control_cancel_request Cancel a pending control request
initialize Session configuration (systemPrompt, hooks, allowedTools, model, mcpServers, etc.)

control_request — Bidirectional Envelope

The control_request message type is bidirectional — both CLI and extension can send it. The subtype field determines the semantics:

Subtype Direction Purpose
initialize Extension → CLI Session config (systemPrompt, hooks, allowedTools, model, mcpServers)
can_use_tool CLI → Extension Ask for tool permission
set_permission_mode Extension → CLI Switch permission mode
set_model Extension → CLI Switch model
set_max_thinking_tokens Extension → CLI Set thinking budget
interrupt Extension → CLI Abort current turn

Each control_request expects a control_response with matching request_id.

Permission Flow (replaces ACP requestPermission)

CLI (stdout)                         Extension
   │                                     │
   │── control_request ─────────────────►│  "Bash requires permission"
   │   {request_id, request: {            │
   │     subtype: "can_use_tool",         │
   │     tool_name, input}}              │
   │                                     │  Show dialog in IDE/POPO
   │                                     │
   │◄── control_response ────────────────│  User clicked "Allow"
   │   {request_id,                       │
   │    response: {behavior:"allow"}}     │

Key differences from ACP:

  • Non-blocking: CLI can send multiple control_request messages and continue processing. Extension responds asynchronously.
  • Race-safe: Multiple permission prompts can be in-flight simultaneously, matched by request_id.
  • No roundtrip coupling: CLI does not wait inside a JSON-RPC call. The prompt loop continues reading stdin for other messages.

Mode/Model Switching (replaces ACP setSessionMode/setSessionConfigOption)

Extension (stdin)                    CLI
   │                                     │
   │── control_request ─────────────────►│  Switch to acceptEdits
   │   {request_id, request: {            │
   │     subtype: "set_permission_mode",  │
   │     mode: "acceptEdits"}}           │
   │                                     │
   │◄── control_response ────────────────│  Success
   │   {request_id,                       │
   │    response: {subtype:"success"}}    │
   │                                     │
   │── control_request ─────────────────►│  Switch to sonnet
   │   {request_id, request: {            │
   │     subtype: "set_model",            │
   │     model: "claude-sonnet-4-6"}}    │
   │                                     │
   │◄── control_response ────────────────│  Success
   │   {request_id,                       │
   │    response: {subtype:"success"}}    │

This replaces ACP's connection.setSessionMode() and connection.setSessionConfigOption() with a uniform control_request/control_response pattern.

Streaming Flow (replaces ACP sessionUpdate)

CLI (stdout)                         Extension
   │                                     │
   │── assistant (partial) ─────────────►│  Show streaming text
   │── assistant (partial) ─────────────►│  
   │── assistant (tool_use) ────────────►│  Show tool call starting
   │── tool_result ─────────────────────►│  Show tool result
   │── assistant (partial) ─────────────►│  Continue streaming
   │── result ──────────────────────────►│  Turn complete
   │── system (permissionMode) ─────────►│  Mode changed notification

vs ACP today:

   │── sessionUpdate(agent_message_chunk)►│  Callback-based
   │── sessionUpdate(tool_call) ─────────►│  
   │── sessionUpdate(tool_call_update) ──►│  

The NDJSON approach is simpler — each line is a self-contained message. No JSON-RPC envelope, no method dispatch, no callback registration.

Session State Change Notification

When permission mode changes (from any source — extension request, slash command, Shift+Tab, etc.), the CLI emits a system message:

{
  "type": "system",
  "subtype": "status",
  "permissionMode": "acceptEdits",
  "uuid": "..."
}

This replaces ACP's sessionUpdate(current_mode_update) and sessionUpdate(config_option_update).

Implementation Plan

Phase 1: Core Protocol

  1. Define message types in packages/agent-sdk/src/protocol/ — TypeScript types mirroring Claude Code's StdoutMessage, StdinMessage, SDKControlRequest, SDKControlResponse
  2. Implement StructuredIO — bidirectional NDJSON reader/writer over stdin/stdout (same pattern as Claude Code's src/cli/structuredIO.ts)
  3. Implement stream-json output format — replace ACP's AgentSideConnection with StructuredIO-based output
  4. Implement --permission-prompt-tool stdio — forward permission requests as control_request (subtype: can_use_tool) on stdout, read control_response from stdin
  5. Implement control_request handler — parse incoming control_request messages from stdin, handle subtypes: initialize, set_permission_mode, set_model, set_max_thinking_tokens, interrupt
  6. Implement initialize control message — support systemPrompt, hooks, allowedTools, model, mcpServers

Phase 2: CLI Entry Point

  1. Add --output-format stream-json flag to wave-code CLI
  2. Add --permission-prompt-tool flagstdio enables stdin-based permission flow
  3. Add stdin message loop — read user, control_response, control_request messages from stdin

Phase 3: Remove ACP

  1. Remove @agentclientprotocol/sdk dependency
  2. Remove packages/code/src/acp/ directory
  3. Remove specs/070-acp-bridge/ (superseded by this spec)
  4. Remove wave-code acp subcommand

Success Criteria

  • wave-code --output-format stream-json produces NDJSON on stdout identical in structure to claude --output-format stream-json
  • --permission-prompt-tool stdio sends control_request (subtype: can_use_tool) and reads control_response from stdin
  • Extension can launch CLI as child process, parse NDJSON, render messages in webview/POPO cards
  • Extension can show permission dialogs and write control_response to CLI stdin
  • initialize message supports systemPrompt, hooks, allowedTools, model, mcpServers
  • set_permission_mode control_request switches permission mode and emits system status message
  • set_model control_request switches model and emits system status message
  • interrupt control_request aborts the current turn
  • All existing ACP functionality (streaming, permissions, session management, mode/model switching, MCP) works via the new protocol
  • @agentclientprotocol/sdk dependency removed

References

  • Claude Code StructuredIO: src/cli/structuredIO.ts (in claude-code repo)
  • Claude Code control types: src/entrypoints/sdk/controlTypes.ts
  • Claude Code control schemas: src/entrypoints/sdk/controlSchemas.ts (defines all control_request subtypes)
  • Claude Code control_request handling: src/cli/print.ts:2918-2945 (handles set_permission_mode, set_model, set_max_thinking_tokens)
  • Claude Code set_permission_mode from bridge: src/bridge/bridgeMessaging.ts:328-355
  • Current ACP bridge: packages/code/src/acp/agent.ts
  • Current ACP spec: specs/070-acp-bridge/spec.md

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    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