Skip to main content

Overview

The Agent Client Protocol (ACP) is Routa’s process management layer. It spawns, monitors, and communicates with AI agent processes across multiple platforms (Claude Code, OpenCode, Codex, Gemini, etc.). Key Purpose: Abstract away platform-specific agent implementations and provide a unified interface for spawning and managing agent lifecycle. Built on: @agentclientprotocol/sdk

When to Use ACP

Use ACP when you need to:
  • Spawn new agent processes dynamically during workflow execution
  • Route prompts to specific agent providers (Claude, OpenCode, Copilot)
  • Manage agent lifecycle (start, stop, monitor health)
  • Configure MCP servers for each agent instance
  • Track agent usage (tokens, tool calls, file changes)
ACP is ideal for orchestrators that need to dynamically spawn specialist agents.

Architecture

ACP implementation lives in src/core/acp/routa-acp-agent.ts:62:
export function createRoutaAcpAgent(
  system: RoutaSystem,
  skillRegistry?: SkillRegistry
) {
  const sessions = new Map<string, AcpSession>();

  return function agentHandler(connection: AgentSideConnection): Agent {
    return {
      async initialize(params: InitializeRequest): Promise<InitializeResponse> {
        return {
          protocolVersion: params.protocolVersion,
          agentCapabilities: { loadSession: true },
          agentInfo: { name: "routa-acp", version: "0.1.0" }
        };
      },

      async newSession(params: NewSessionRequest): Promise<NewSessionResponse> {
        const sessionId = uuidv4();
        const routaAgentId = await createBackingAgent();
        sessions.set(sessionId, { id: sessionId, routaAgentId, workspaceId, cwd });
        return { sessionId };
      },

      async prompt(params: PromptRequest): Promise<PromptResponse> {
        // Route prompts to Routa tools, spawn agents, stream updates
      }
    };
  };
}

Key Concepts

ACP Sessions

Each ACP session represents a persistent conversation with an agent:
interface AcpSession {
  id: string;              // Unique session identifier
  cwd: string;             // Working directory
  routaAgentId?: string;   // Linked Routa agent record
  workspaceId: string;     // Workspace context
}
Sessions are created via newSession and tracked in src/core/acp/http-session-store.ts:58.

Provider Registry

Supported agent providers are registered in src/core/acp/provider-registry.ts:177:
Provider ID: claudeProcess: claude-code-process (npm package)Transport: SSE + JSON-RPC
// src/core/acp/claude-code-process.ts
const config = buildClaudeCodeConfig({
  apiKey: process.env.ANTHROPIC_API_KEY,
  model: "claude-sonnet-4-20250514",
  mcpServers: {
    routa: {
      transport: { type: "sse", url: "http://localhost:3000/api/mcp" }
    }
  }
});

const process = new ClaudeCodeProcess(config);
await process.start();
Provider ID: opencodeProcess: opencode (npx/binary)Transport: stdio + JSON-RPC
// src/core/acp/opencode-process.ts
const process = new AcpProcess({
  command: "npx",
  args: ["opencode", "--stdio"],
  env: {
    ...process.env,
    OPENCODE_MCP_CONFIG: JSON.stringify({ servers: mcpConfig })
  }
});

await process.start();
Provider ID: codexProcess: codex (binary)Transport: stdio + JSON-RPCSimilar to OpenCode but uses Codex binary.
Provider ID: geminiProcess: gemini-cli (npm package)Transport: stdio + JSON-RPC
npx @google/genai-cli --stdio
Provider ID: copilotProcess: gh copilot (GitHub CLI extension)Transport: stdio + JSON-RPC
gh copilot --stdio

Agent Event Bridge

ACP sessions emit structured events for real-time monitoring (src/core/acp/agent-event-bridge/agent-event-bridge.ts:61):
interface WorkspaceAgentEvent {
  agentId: string;
  workspaceId: string;
  sessionId: string;
  timestamp: string;
}

// Event types
type AgentStartedEvent = WorkspaceAgentEvent & {
  type: "agent-started";
  name: string;
  role: AgentRole;
};

type ToolCallBlockEvent = WorkspaceAgentEvent & {
  type: "tool-call-block";
  toolName: string;
  status: BlockStatus;
  kind: ToolKind; // "file" | "terminal" | "mcp" | "other"
};

type FileChangesBlockEvent = WorkspaceAgentEvent & {
  type: "file-changes-block";
  changes: FileChange[];
};

type AgentCompletedEvent = WorkspaceAgentEvent & {
  type: "agent-completed";
  success: boolean;
  summary?: string;
};

Configuration

Process Configuration

command
string
required
Executable command (e.g., "npx", "/usr/bin/claude-code")
args
string[]
required
Command arguments (e.g., ["opencode", "--stdio"])
cwd
string
Working directory for the agent process
env
Record<string, string>
Environment variables (API keys, MCP config, etc.)
stdin
boolean
default:"true"
Whether to use stdin for communication
stdout
boolean
default:"true"
Whether to capture stdout

MCP Configuration

Each ACP agent is automatically configured with Routa’s MCP server:
// src/core/acp/mcp-config-generator.ts:12
export function generateRoutaMcpConfig(params: {
  workspaceId: string;
  sessionId?: string;
  toolMode?: ToolMode;
}): RoutaMcpConfig {
  return {
    mcpServers: {
      routa: {
        transport: {
          type: "sse",
          url: `http://localhost:3000/api/mcp?workspaceId=${params.workspaceId}&sessionId=${params.sessionId}`
        }
      }
    }
  };
}
This configuration is injected into each agent’s environment, enabling them to call MCP coordination tools.

Example Usage

Spawning an Agent

import { AcpSessionManager } from "@/core/acp";

const sessionManager = new AcpSessionManager();

// Create a new session
const session = await sessionManager.createSession({
  workspaceId: "workspace-123",
  specialist: "CRAFTER",
  provider: "opencode",
  cwd: "/workspace/project",
  taskId: "task-uuid"
});

console.log(session.sessionId); // "session-abc123"
console.log(session.agentId);   // "agent-xyz789"

Sending Prompts

// Send a prompt to the agent
const response = await sessionManager.prompt({
  sessionId: session.sessionId,
  prompt: [{
    type: "text",
    text: "Implement user authentication with JWT"
  }]
});

console.log(response.stopReason); // "end_turn"

Monitoring Events

import { AgentEventBridge } from "@/core/acp";

const eventBridge = new AgentEventBridge();

eventBridge.on("agent-started", (event) => {
  console.log(`Agent ${event.name} started`);
});

eventBridge.on("tool-call-block", (event) => {
  console.log(`Tool called: ${event.toolName} (${event.status})`);
});

eventBridge.on("file-changes-block", (event) => {
  console.log(`Files modified: ${event.changes.length}`);
});

eventBridge.on("agent-completed", (event) => {
  console.log(`Agent completed: ${event.success ? "SUCCESS" : "FAILURE"}`);
  if (event.summary) {
    console.log(`Summary: ${event.summary}`);
  }
});

Custom Provider Adapter

To add a new agent provider, implement a provider adapter:
// src/core/acp/provider-adapter/my-custom-adapter.ts
import { BaseProviderAdapter } from "./base-adapter";

export class MyCustomAdapter extends BaseProviderAdapter {
  async initialize(params: InitializeRequest): Promise<InitializeResponse> {
    // Custom initialization logic
    return {
      protocolVersion: params.protocolVersion,
      agentCapabilities: { loadSession: true },
      agentInfo: { name: "my-custom-agent", version: "1.0.0" }
    };
  }

  async prompt(params: PromptRequest): Promise<PromptResponse> {
    // Custom prompt handling
    return { stopReason: "end_turn" };
  }
}
Register it in the provider registry:
// src/core/acp/provider-registry.ts
import { MyCustomAdapter } from "./provider-adapter/my-custom-adapter";

ProviderRegistry.register("my-custom", {
  name: "My Custom Agent",
  createAdapter: (system) => new MyCustomAdapter(system),
  defaultModel: "my-model-v1"
});

Session Management

Session Store

ACP sessions are persisted in memory and optionally synced to the database:
// src/core/acp/http-session-store.ts
interface AcpSessionRecord {
  sessionId: string;
  workspaceId: string;
  cwd: string;
  routaAgentId?: string;
  createdAt: string;
  lastActiveAt?: string;
}

const sessionStore = getHttpSessionStore();

// List all sessions
const sessions = sessionStore.listSessions();

// Get specific session
const session = sessionStore.getSession("session-id");

// Update last active time
sessionStore.touchSession("session-id");

Session Lifecycle

  1. CreatenewSession creates a new ACP session and backing Routa agent
  2. Active — Agent processes prompts and emits events
  3. Idle — No activity for configurable timeout
  4. Cleanup — Session terminated, process killed, resources released

ACP Registry

Discover and install pre-configured agents from the community registry:
import { fetchRegistry, installFromRegistry } from "@/core/acp";

// Fetch available agents
const registry = await fetchRegistry();
console.log(registry.agents); // [{ id: "opencode", name: "OpenCode", ... }]

// Install an agent
const result = await installFromRegistry({
  agentId: "opencode",
  platform: "linux" // or "darwin", "win32"
});

if (result.success) {
  console.log(`Installed: ${result.command}`);
}
Registry JSON structure (src/core/acp/acp-registry.ts:18):
interface RegistryAgent {
  id: string;
  name: string;
  description: string;
  distribution: AgentDistribution;
}

type AgentDistribution =
  | { type: "npx"; package: string; command?: string }
  | { type: "uvx"; package: string; command?: string }
  | { type: "binary"; platforms: Record<PlatformTarget, BinaryPlatformConfig> };

Best Practices

Every ACP session requires a workspaceId to scope operations:
// WRONG
await sessionManager.createSession({ specialist: "CRAFTER" });

// CORRECT
await sessionManager.createSession({
  workspaceId: "workspace-123",
  specialist: "CRAFTER"
});
Use src/core/acp/mcp-setup.ts:37 to ensure MCP is properly configured:
import { ensureMcpForProvider } from "@/core/acp";

await ensureMcpForProvider({
  provider: "opencode",
  workspaceId: "workspace-123",
  sessionId: "session-456",
  toolMode: "essential"
});
ACP processes can fail (missing binaries, API key errors, etc.). Always check installation:
import { isAgentAvailable } from "@/core/acp";

const available = await isAgentAvailable("opencode");
if (!available) {
  console.error("OpenCode not installed. Run: npx opencode --version");
  return;
}
Subscribe to agent events to track progress and detect failures:
eventBridge.on("agent-failed", (event) => {
  console.error(`Agent ${event.agentId} failed:`, event.error);
  // Retry or alert user
});

Process Manager

The ACP process manager handles spawning and monitoring agent processes:
// src/core/acp/acp-process-manager.ts
import { getAcpProcessManager } from "@/core/acp";

const processManager = getAcpProcessManager();

// Spawn a new agent process
const process = await processManager.spawn({
  provider: "opencode",
  workspaceId: "workspace-123",
  cwd: "/workspace/project",
  env: {
    OPENCODE_MCP_CONFIG: JSON.stringify(mcpConfig)
  }
});

// Send JSON-RPC message
await process.send({
  jsonrpc: "2.0",
  method: "session/prompt",
  params: { sessionId: "session-id", prompt: [{ type: "text", text: "Hello" }] },
  id: 1
});

// Listen for responses
process.on("message", (msg) => {
  console.log("Received:", msg);
});

// Terminate process
await process.stop();

API Reference

ACP Agent

Create and manage ACP agent handlers

Session Manager

Manage agent sessions and lifecycle

Provider Registry

Register and resolve agent providers

Event Bridge

Monitor agent events in real-time

Build docs developers (and LLMs) love