Skip to content

Add Agent Client Protocol (ACP) support for multi-agent backends #49

@gedeagas

Description

@gedeagas

Motivation

Braid currently uses @anthropic-ai/claude-agent-sdk directly for all AI agent communication. The Agent Client Protocol (ACP) is emerging as an industry standard for editor-to-agent communication, with 17+ agents adopting it (Gemini CLI, Codex CLI, Cursor, Copilot, Cline, Goose, etc.) and IDE support from Zed, JetBrains, Neovim, and others.

Adding ACP as a second backend enables Braid to work with any ACP-compatible agent, starting with Gemini CLI which already has full ACP support. Claude continues to use the SDK directly (more feature-rich), while ACP becomes the gateway for everything else. Both coexist per-session - one session can run Claude SDK, another can run Gemini via ACP.

Architecture

The abstraction boundary is at the worker process layer. Both backends emit the same WorkerEvent types, so the coordinator and renderer need minimal changes.

Renderer -> IPC -> AgentCoordinator -> UtilityProcess -> agentProcess.ts  -> AgentWorker (Claude SDK)
                                    \-> UtilityProcess -> acpProcess.ts   -> AcpWorker   (ACP agents)
                                                            |
                                                            spawns agent subprocess (stdio)
                                                            ClientSideConnection from @agentclientprotocol/sdk

Key design decisions:

  • Claude SDK stays primary - no regression, full feature set (slash commands, plan mode, thinking, resume, elicitation)
  • ACP worker synthesizes WorkerEvents matching the format the renderer already handles - zero changes to eventHandler.ts or message rendering
  • Per-session backend - each session independently chooses Claude SDK or an ACP agent
  • Ephemeral ops (commit message gen, session title gen) always use Claude SDK regardless of session backend

Implementation Plan

Phase 1: Types and Configuration

New types in src/main/services/agentTypes.ts:

export type AgentBackend =
  | { type: 'claude-sdk' }
  | { type: 'acp'; agentId: string }

export interface AcpAgentConfig {
  id: string            // e.g. 'gemini-cli'
  name: string          // e.g. 'Gemini CLI'
  command: string       // e.g. 'gemini'
  args: string[]        // e.g. ['--experimental-acp']
  env?: Record<string, string>
}

New file src/main/services/acpConfig.ts (~60 lines):

  • loadAcpAgents() / saveAcpAgents() / getAcpAgent() - CRUD for agent configs
  • Storage via existing storage.ts patterns

Renderer types in src/renderer/types/session.ts:

  • Add SessionBackend type, backend?: SessionBackend field on AgentSession

Phase 2: ACP Worker

New directory src/main/services/acpWorker/:

File ~Lines Purpose
core.ts 250 AcpWorker class - spawns agent subprocess, manages ClientSideConnection, session lifecycle
eventMapper.ts 150 Maps ACP session/update notifications to Braid's WorkerEvent format
clientHandlers.ts 120 Implements ACP Client callbacks (file ops, terminal, permissions)
index.ts 5 Barrel export

Event mapping (ACP -> WorkerEvent):

ACP Update WorkerEvent
agent_message_chunk (text) sdk_message with synthetic assistant message
agent_thought_chunk sdk_message with synthetic thinking block
tool_call start sdk_message with tool_use content block
tool_call update/complete sdk_message with tool result
plan sdk_message with synthetic system message
cost_update sdk_message with token usage

Client callbacks:

  • readTextFile / writeTextFile - direct fs operations from worker process
  • requestPermission - emit waiting_input with reason: 'tool_permission', wait for answerToolInput
  • createTerminal / terminalOutput / killTerminal - delegate to coordinator via braid_action data_request (same pattern as braidMcp.ts)

New file src/main/services/acpProcess.ts (~50 lines):

  • Parallel to agentProcess.ts - UtilityProcess entry point for ACP sessions

Dependency: yarn add @agentclientprotocol/sdk

Phase 3: Coordinator Changes

File src/main/services/agent.ts - minimal changes:

  • Add ACP_ENTRY_PATH alongside existing ENTRY_PATH
  • spawnSessionProcess() picks entry point based on backend.type
  • startSession() accepts optional backend?: AgentBackend parameter
  • handleWorkerEvent() needs zero changes - both backends emit the same types

Phase 4: IPC Threading

Thread backend through the 3-layer IPC:

  • src/main/ipc.ts - add backend param, add agent:getAcpAgents / agent:saveAcpAgents handlers
  • src/preload/index.ts - expose new channels
  • src/renderer/lib/ipc.ts - typed wrappers

Phase 5: Renderer Integration

  • communicationActions.ts - pass backend to startSession, skip Claude-only features for ACP
  • ModelSelector.tsx - extend dropdown with "ACP Agents" section alongside Claude models
  • Conditional feature hiding for ACP sessions: hide slash commands, thinking toggle, plan mode toggle
  • New SettingsAgents.tsx (~200 lines) - add/edit/remove ACP agents, test connectivity

Phase 6: Session Persistence

  • Add backend field to PersistedSession in sessionStorage.ts
  • ACP sessions reconnect via session/load if the agent supports it

Feature Degradation Matrix

Feature Claude SDK ACP Behavior
Slash commands Full None Hidden for ACP sessions
Plan mode Full Agent-dependent Hidden
Thinking toggle Full Agent-dependent Hidden
Resume SDK resume ACP session/load Maps to ACP equivalent
Token display Anthropic format cost_update Mapped in eventMapper
Commit/title gen Ephemeral N/A Always uses Claude SDK
Images SDK blocks ACP ImageContent Direct mapping
Braid MCP tools In-process Via ACP fs/terminal Handled by clientHandlers
Elicitation (OAuth) Full Not in ACP N/A for ACP sessions

Files Summary

New files (7)

File Lines Purpose
src/main/services/acpConfig.ts ~60 Agent registry/storage
src/main/services/acpWorker/core.ts ~250 AcpWorker class
src/main/services/acpWorker/eventMapper.ts ~150 ACP -> WorkerEvent translation
src/main/services/acpWorker/clientHandlers.ts ~120 File, terminal, permission callbacks
src/main/services/acpWorker/index.ts ~5 Barrel export
src/main/services/acpProcess.ts ~50 UtilityProcess entry point
src/renderer/components/Settings/SettingsAgents.tsx ~200 ACP agent management UI

Modified files (14)

File Change
src/main/services/agentTypes.ts Add AgentBackend, AcpAgentConfig types
src/main/services/agentProcessTypes.ts Add backend to startSession WorkerCommand
src/main/services/agent.ts Dual entry path, backend param on startSession
src/main/ipc.ts Thread backend, add ACP config IPC handlers
src/preload/index.ts Expose new IPC channels
src/renderer/lib/ipc.ts Typed wrappers for new IPC
src/renderer/types/session.ts Add SessionBackend, backend field on AgentSession
src/renderer/store/sessions/handlers/communicationActions.ts Pass backend, skip Claude-only features
src/renderer/components/Center/ModelSelector.tsx Add ACP agent section
src/renderer/components/Center/ChatInput.tsx Hide slash commands for ACP
src/renderer/components/Settings/SettingsOverlay.tsx Register agents page
src/renderer/components/Settings/SettingsNav.tsx Add nav entry
src/renderer/locales/{en,ja,id}/settings.json Agent settings translations
src/main/services/sessionStorage.ts Persist backend field

Verification

  • yarn typecheck passes
  • Unit tests for event mapper (ACP update -> WorkerEvent) and ACP config CRUD
  • Claude sessions work identically (no regression)
  • Gemini CLI: add via Settings > Agents, start session, verify streaming, tool permissions, file read/write
  • Session persistence: restart app, ACP session restores correctly
  • Model selector: Claude models and ACP agents both appear, switching works

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions