ACP (Agent Client Protocol)
TL;DR: ACP lets OmniRoute spawn CLI agents (like Claude Code, Codex, Gemini CLI) as child processes instead of using HTTP APIs. This gives you "CLI-as-backend" transport.
What Is ACP?
ACP (Agent Client Protocol) is a "CLI-as-backend" transport for OmniRoute. Instead of intercepting HTTP API calls to AI providers, ACP spawns CLI agents as child processes and feeds prompts through their native interface.
Why Use ACP?
| Benefit | Description |
|---|---|
| No API keys needed | Uses your existing CLI authentication |
| Native protocol | Uses each CLI's native input/output format |
| Auto-discovery | Detects installed CLIs on your system |
| 14 built-in agents | Pre-configured for popular CLI tools |
| Custom agents | Add your own CLI tools via settings |
| Process management | Handles lifecycle (spawn, send, kill) |
Supported CLI Agents
ACP supports 14 built-in CLI agents out of the box:
| Agent ID | Display Name | Binary | Protocol |
|---|---|---|---|
codex | OpenAI Codex CLI | codex | stdio |
claude | Claude Code CLI | claude | stdio |
goose | Goose CLI | goose | stdio |
gemini-cli | Gemini CLI | gemini | stdio |
openclaw | OpenClaw | openclaw | stdio |
aider | Aider | aider | stdio |
opencode | OpenCode | opencode | stdio |
cline | Cline | cline | stdio |
qwen-code | Qwen Code | qwen | stdio |
forge | ForgeCode | forge | stdio |
amazon-q | Amazon Q Developer | q | stdio |
interpreter | Open Interpreter | interpreter | stdio |
cursor-cli | Cursor CLI | cursor | stdio |
warp | Warp AI | warp | stdio |
Custom Agents
You can add your own CLI agents via settings. Custom agents support the same features as built-in agents.
Quick Start
Step 1: Install a CLI Agent
# Example: Install Claude Code CLI
npm install -g @anthropic-ai/claude-code
# Verify installation
claude --versionStep 2: ACP Auto-Detection
ACP automatically detects installed CLI agents on your system. No configuration needed!
Step 3: Use ACP Transport
Once detected, ACP can be used as a transport for any supported provider. OmniRoute will automatically use ACP when the CLI is available.
How ACP Works
Architecture
┌─────────────────┐
│ OmniRoute │
│ (HTTP Proxy) │
└────────┬────────┘
│
│ spawn()
▼
┌─────────────────┐
│ Child Process │
│ (CLI Agent) │
│ │
│ stdin ◄──────┤ Send prompt
│ stdout ──────►│ Receive response
│ stderr ──────►│ Receive errors
└─────────────────┘Process Lifecycle
- Spawn — ACP creates a child process for the CLI agent
- Send — ACP writes prompts to the process's stdin
- Receive — ACP reads responses from stdout/stderr
- Idle Detection — ACP waits 2 seconds of inactivity before considering the response complete
- Kill — ACP terminates the process (SIGTERM, then SIGKILL after 5s)
Communication Protocol
ACP uses stdio (standard input/output) for communication with CLI agents. The protocol is:
- Send prompt — Write to stdin with a newline
- Wait for response — Read from stdout until idle (2s of no output)
- Timeout — Default 120 seconds (configurable)
API Reference
Registry Functions
detectInstalledAgents()
Detects all installed CLI agents on the system. Results are cached for 60 seconds.
import { detectInstalledAgents } from "@/lib/acp";
const agents = detectInstalledAgents();
// Returns: CliAgentInfo[]
interface CliAgentInfo {
id: string; // e.g., "codex", "claude"
name: string; // Display name
binary: string; // Binary name to spawn
versionCommand: string; // Version detection command
version: string | null; // Detected version (null if not installed)
installed: boolean; // Whether the agent is installed
providerAlias: string; // Provider ID in OmniRoute
spawnArgs: string[]; // Arguments to pass when spawning
protocol: "stdio" | "http"; // Communication protocol
isCustom?: boolean; // Whether this is a user-defined custom agent
}getAvailableAgents()
Gets only the agents that are installed and available for ACP.
import { getAvailableAgents } from "@/lib/acp";
const available = getAvailableAgents();
// Returns: CliAgentInfo[] (only installed agents)getAgentById(id)
Gets a specific agent by ID.
import { getAgentById } from "@/lib/acp";
const agent = getAgentById("claude");
// Returns: CliAgentInfo | undefinedsetCustomAgents(agents)
Sets custom agent definitions from settings.
import { setCustomAgents } from "@/lib/acp";
setCustomAgents([
{
id: "my-custom-cli",
name: "My Custom CLI",
binary: "mycli",
versionCommand: "mycli --version",
providerAlias: "my-provider",
spawnArgs: [],
protocol: "stdio",
},
]);Manager Functions
acpManager.spawn(agentId, binary, args, env)
Spawns a new CLI agent process.
import { acpManager } from "@/lib/acp";
const session = acpManager.spawn(
"claude",
"claude",
["--print", "--output-format", "json"],
{ /* custom env vars */ }
);
// Returns: AcpSessionAllowed agent IDs: ["claude", "codex", "gemini", "qwen"]
acpManager.sendPrompt(sessionId, prompt, timeoutMs)
Sends a prompt to a CLI agent and collects the response.
import { acpManager } from "@/lib/acp";
const response = await acpManager.sendPrompt(
"acp-claude-1234567890-abc123",
"What is 2+2?",
120000 // 2 minutes timeout
);
// Returns: Promise<string>acpManager.kill(sessionId)
Kills a session and cleans up.
import { acpManager } from "@/lib/acp";
const killed = acpManager.kill("acp-claude-1234567890-abc123");
// Returns: booleanacpManager.getActiveSessions()
Gets all active sessions.
import { acpManager } from "@/lib/acp";
const sessions = acpManager.getActiveSessions();
// Returns: AcpSession[]acpManager.killAll()
Kills all sessions.
import { acpManager } from "@/lib/acp";
acpManager.killAll();Session Interface
interface AcpSession {
id: string; // Unique session ID
agentId: string; // Agent ID (e.g., "claude")
process: ChildProcess; // Child process handle
alive: boolean; // Whether the process is alive
stdoutBuffer: string; // Accumulated stdout buffer
stderrBuffer: string; // Accumulated stderr buffer
createdAt: Date; // Created timestamp
}Events
The AcpManager extends EventEmitter and emits the following events:
stdout
Emitted when the CLI agent writes to stdout.
acpManager.on("stdout", ({ sessionId, data }) => {
console.log(`[${sessionId}] stdout: ${data}`);
});stderr
Emitted when the CLI agent writes to stderr.
acpManager.on("stderr", ({ sessionId, data }) => {
console.error(`[${sessionId}] stderr: ${data}`);
});exit
Emitted when the CLI agent process exits.
acpManager.on("exit", ({ sessionId, code, signal }) => {
console.log(`[${sessionId}] exited with code ${code}, signal ${signal}`);
});error
Emitted when the CLI agent process errors.
acpManager.on("error", ({ sessionId, error }) => {
console.error(`[${sessionId}] error: ${error}`);
});Configuration
Environment Variables
ACP inherits all environment variables from the parent process and can be extended with custom env vars:
acpManager.spawn("claude", "claude", [], {
ANTHROPIC_API_KEY: "sk-...",
DEBUG: "true",
});Spawn Arguments
Each agent has default spawn arguments defined in the registry. You can override them:
acpManager.spawn("claude", "claude", ["--print", "--verbose"], {});Timeouts
Default prompt timeout is 120 seconds (2 minutes). You can override:
await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutesDetection Cache
Agent detection is cached for 60 seconds to avoid expensive filesystem scans. Force refresh:
import { refreshAgentCache } from "@/lib/acp";
refreshAgentCache();Security
Command Injection Prevention
ACP validates version commands to prevent command injection attacks:
const DISALLOWED_VERSION_COMMAND_CHARS = /[;&|<>`$\r\n]/;Version commands containing these characters are rejected:
;— Command separator&— Background process|— Pipe<,>— Redirection`— Command substitution$— Variable expansion\r,\n— Line breaks
Binary Name Validation
ACP validates that the version command binary matches the expected binary name (unless it's a custom agent).
Process Isolation
Each ACP session runs in its own child process. The process is killed when the session ends or times out.
Performance
Detection Performance
- First call: ~50-200ms (runs
versioncommand for each agent) - Cached calls: <1ms (returns from cache)
- Cache TTL: 60 seconds
Prompt Performance
- Spawn: ~50-100ms
- Send prompt: ~10-50ms
- Wait for response: Depends on CLI agent (typically 1-30 seconds)
- Kill: ~5 seconds (SIGTERM) + immediate (SIGKILL)
Resource Usage
- Memory per session: ~10-50MB (depends on CLI agent)
- CPU: Minimal (I/O bound)
- Disk: None
Troubleshooting
"Unknown agent" Error
Problem: acpManager.spawn() throws Unknown agent: <id>
Solution: Only 4 agents are allowed in spawn():
claudecodexgeminiqwen
Other agents must be spawned manually or via custom agent definitions.
"Session not alive" Error
Problem: acpManager.sendPrompt() throws Session ${sessionId} is not alive
Solution: The session may have exited or been killed. Check session status:
const session = acpManager.getSession(sessionId);
if (!session?.alive) {
// Re-spawn the session
acpManager.spawn("claude", "claude", [], {});
}"ACP timeout" Error
Problem: acpManager.sendPrompt() throws ACP timeout after 120000ms
Solution: Increase the timeout:
await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutesCLI Not Detected
Problem: detectInstalledAgents() doesn't find your CLI
Solutions:
- Check PATH: Ensure the CLI is in your system PATH
- Check version command: Run
claude --versionmanually - Check permissions: Ensure the CLI is executable
- Custom agent: Add a custom agent definition for non-standard CLIs
Permission Denied
Problem: ACP can't execute the CLI
Solutions:
- Check file permissions:
chmod +x /usr/local/bin/claude - Check ownership: Ensure OmniRoute has read/execute permissions
- Check SELinux/AppArmor: May block process spawning
Examples
Example 1: Spawn and Use Claude Code
import { acpManager, detectInstalledAgents } from "@/lib/acp";
// Detect installed agents
const agents = detectInstalledAgents();
const claude = agents.find((a) => a.id === "claude");
if (claude?.installed) {
// Spawn a new session
const session = acpManager.spawn(
"claude",
claude.binary,
["--print", "--output-format", "json"]
);
// Send a prompt
const response = await acpManager.sendPrompt(
session.id,
"Explain quantum computing in 100 words"
);
console.log("Claude's response:", response);
// Clean up
acpManager.kill(session.id);
}Example 2: Auto-Discovery with Fallback
import { acpManager, getAvailableAgents } from "@/lib/acp";
const available = getAvailableAgents();
// Try Claude first, fallback to Codex
let agentId = "claude";
if (!available.find((a) => a.id === "claude")) {
if (available.find((a) => a.id === "codex")) {
agentId = "codex";
} else {
throw new Error("No ACP-compatible CLI agent found");
}
}
const agent = available.find((a) => a.id === agentId)!;
const session = acpManager.spawn(agentId, agent.binary, agent.spawnArgs);
const response = await acpManager.sendPrompt(session.id, "Hello!");
acpManager.kill(session.id);Example 3: Custom Agent
import { setCustomAgents, detectInstalledAgents } from "@/lib/acp";
// Register a custom CLI agent
setCustomAgents([
{
id: "my-llm-cli",
name: "My LLM CLI",
binary: "myllm",
versionCommand: "myllm --version",
providerAlias: "my-llm-provider",
spawnArgs: ["--format", "json"],
protocol: "stdio",
},
]);
// Now detectInstalledAgents() will include "my-llm-cli"
const agents = detectInstalledAgents();What's Next?
- API Reference — REST API endpoints
- Provider Reference — All 223 providers
- MCP Server — Model Context Protocol integration
- A2A Server — Agent-to-Agent protocol
- Cloud Agent — Cloud-based agents
Reference
- AionUi Project — Inspiration for ACP auto-detection
- ACP Source Code — Implementation details
manager.ts— Process lifecycle managementregistry.ts— Agent discovery and registrationindex.ts— Public API exports