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
Third-party protocol dependency : ACP is an external package with its own schema/maintenance cycle. Any protocol changes require upstream coordination.
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.
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.
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.
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.
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
Define message types in packages/agent-sdk/src/protocol/ — TypeScript types mirroring Claude Code's StdoutMessage, StdinMessage, SDKControlRequest, SDKControlResponse
Implement StructuredIO — bidirectional NDJSON reader/writer over stdin/stdout (same pattern as Claude Code's src/cli/structuredIO.ts)
Implement stream-json output format — replace ACP's AgentSideConnection with StructuredIO-based output
Implement --permission-prompt-tool stdio — forward permission requests as control_request (subtype: can_use_tool) on stdout, read control_response from stdin
Implement control_request handler — parse incoming control_request messages from stdin, handle subtypes: initialize, set_permission_mode, set_model, set_max_thinking_tokens, interrupt
Implement initialize control message — support systemPrompt, hooks, allowedTools, model, mcpServers
Phase 2: CLI Entry Point
Add --output-format stream-json flag to wave-code CLI
Add --permission-prompt-tool flag — stdio enables stdin-based permission flow
Add stdin message loop — read user, control_response, control_request messages from stdin
Phase 3: Remove ACP
Remove @agentclientprotocol/sdk dependency
Remove packages/code/src/acp/ directory
Remove specs/070-acp-bridge/ (superseded by this spec)
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
Problem
The current IDE integration uses the Agent Client Protocol (ACP) (
@agentclientprotocol/sdkv0.17.1), which has several limitations compared to how Claude Code communicates with its VS Code extension:ACP Limitations
stream-jsonmode: ACP uses JSON-RPC method calls (prompt(),requestPermission()) with callback-based updates. There is no equivalent to Claude Code's--output-format stream-jsonNDJSON streaming, which is simpler and more debuggable.requestPermissionis a blocking JSON-RPC call. Claude Code'scontrol_request/control_responsepattern is non-blocking — the CLI sends a request and the extension responds asynchronously, allowing the CLI to keep processing other events.--permission-prompt-tool stdio: Claude Code lets the SDK host handle all permission decisions via stdin/stdout. ACP requires theAgentSideConnectionto stay alive for the permission roundtrip, creating tighter coupling.initialize()is rigid. Claude Code'sinitializecontrol message supportssystemPrompt,appendSystemPrompt,hooks,allowedTools,jsonSchema, and more — all configurable at init time.setSessionMode()andsetSessionConfigOption()RPC methods. Claude Code uses the samecontrol_requestenvelope withsubtypediscriminators, 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-jsonworks.Architecture
Protocol Design
Based on Claude Code's
StructuredIO+controlTypes+controlSchemas:CLI → Extension (stdout, one NDJSON line per message)
assistanttool_resultresultcontrol_requestcan_use_tool)session_state_changedsystemExtension → CLI (stdin, one NDJSON line per message)
usercontrol_requestset_permission_mode,set_model,set_max_thinking_tokens,interrupt)control_responsecontrol_request(allow/deny permission)control_cancel_requestinitializecontrol_request— Bidirectional EnvelopeThe
control_requestmessage type is bidirectional — both CLI and extension can send it. Thesubtypefield determines the semantics:initializecan_use_toolset_permission_modeset_modelset_max_thinking_tokensinterruptEach
control_requestexpects acontrol_responsewith matchingrequest_id.Permission Flow (replaces ACP
requestPermission)Key differences from ACP:
control_requestmessages and continue processing. Extension responds asynchronously.request_id.Mode/Model Switching (replaces ACP
setSessionMode/setSessionConfigOption)This replaces ACP's
connection.setSessionMode()andconnection.setSessionConfigOption()with a uniformcontrol_request/control_responsepattern.Streaming Flow (replaces ACP
sessionUpdate)vs ACP today:
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
systemmessage:{ "type": "system", "subtype": "status", "permissionMode": "acceptEdits", "uuid": "..." }This replaces ACP's
sessionUpdate(current_mode_update)andsessionUpdate(config_option_update).Implementation Plan
Phase 1: Core Protocol
packages/agent-sdk/src/protocol/— TypeScript types mirroring Claude Code'sStdoutMessage,StdinMessage,SDKControlRequest,SDKControlResponseStructuredIO— bidirectional NDJSON reader/writer over stdin/stdout (same pattern as Claude Code'ssrc/cli/structuredIO.ts)stream-jsonoutput format — replace ACP'sAgentSideConnectionwithStructuredIO-based output--permission-prompt-tool stdio— forward permission requests ascontrol_request(subtype:can_use_tool) on stdout, readcontrol_responsefrom stdincontrol_requesthandler — parse incomingcontrol_requestmessages from stdin, handle subtypes:initialize,set_permission_mode,set_model,set_max_thinking_tokens,interruptinitializecontrol message — supportsystemPrompt,hooks,allowedTools,model,mcpServersPhase 2: CLI Entry Point
--output-format stream-jsonflag towave-codeCLI--permission-prompt-toolflag —stdioenables stdin-based permission flowuser,control_response,control_requestmessages from stdinPhase 3: Remove ACP
@agentclientprotocol/sdkdependencypackages/code/src/acp/directoryspecs/070-acp-bridge/(superseded by this spec)wave-code acpsubcommandSuccess Criteria
wave-code --output-format stream-jsonproduces NDJSON on stdout identical in structure toclaude --output-format stream-json--permission-prompt-tool stdiosendscontrol_request(subtype:can_use_tool) and readscontrol_responsefrom stdincontrol_responseto CLI stdininitializemessage supportssystemPrompt,hooks,allowedTools,model,mcpServersset_permission_modecontrol_request switches permission mode and emitssystemstatus messageset_modelcontrol_request switches model and emitssystemstatus messageinterruptcontrol_request aborts the current turn@agentclientprotocol/sdkdependency removedReferences
StructuredIO:src/cli/structuredIO.ts(in claude-code repo)src/entrypoints/sdk/controlTypes.tssrc/entrypoints/sdk/controlSchemas.ts(defines allcontrol_requestsubtypes)control_requesthandling:src/cli/print.ts:2918-2945(handlesset_permission_mode,set_model,set_max_thinking_tokens)set_permission_modefrom bridge:src/bridge/bridgeMessaging.ts:328-355packages/code/src/acp/agent.tsspecs/070-acp-bridge/spec.md