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-RPCnpx @google/genai-cli --stdio
Provider ID: copilotProcess: gh copilot (GitHub CLI extension)Transport: stdio + JSON-RPC
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
Executable command (e.g., "npx", "/usr/bin/claude-code")
Command arguments (e.g., ["opencode", "--stdio"])
Working directory for the agent process
Environment variables (API keys, MCP config, etc.)
Whether to use stdin for communication
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
Create — newSession creates a new ACP session and backing Routa agent
Active — Agent processes prompts and emits events
Idle — No activity for configurable timeout
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
Always Provide workspaceId
Every ACP session requires a workspaceId to scope operations: // WRONG
await sessionManager . createSession ({ specialist: "CRAFTER" });
// CORRECT
await sessionManager . createSession ({
workspaceId: "workspace-123" ,
specialist: "CRAFTER"
});
Configure MCP for Each Provider
Handle Process Failures Gracefully
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