-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
ACP
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.
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.
| 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) |
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 |
You can add your own CLI agents via settings. Custom agents support the same features as built-in agents.
# Example: Install Claude Code CLI
npm install -g @anthropic-ai/claude-code
# Verify installation
claude --versionACP automatically detects installed CLI agents on your system. No configuration needed!
Once detected, ACP can be used as a transport for any supported provider. OmniRoute will automatically use ACP when the CLI is available.
┌─────────────────┐
│ OmniRoute │
│ (HTTP Proxy) │
└────────┬────────┘
│
│ spawn()
▼
┌─────────────────┐
│ Child Process │
│ (CLI Agent) │
│ │
│ stdin ◄──────┤ Send prompt
│ stdout ──────►│ Receive response
│ stderr ──────►│ Receive errors
└─────────────────┘
- 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)
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)
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
}Gets only the agents that are installed and available for ACP.
import { getAvailableAgents } from "@/lib/acp";
const available = getAvailableAgents();
// Returns: CliAgentInfo[] (only installed agents)Gets a specific agent by ID.
import { getAgentById } from "@/lib/acp";
const agent = getAgentById("claude");
// Returns: CliAgentInfo | undefinedSets 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",
},
]);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"]
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>Kills a session and cleans up.
import { acpManager } from "@/lib/acp";
const killed = acpManager.kill("acp-claude-1234567890-abc123");
// Returns: booleanGets all active sessions.
import { acpManager } from "@/lib/acp";
const sessions = acpManager.getActiveSessions();
// Returns: AcpSession[]Kills all sessions.
import { acpManager } from "@/lib/acp";
acpManager.killAll();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
}The AcpManager extends EventEmitter and emits the following events:
Emitted when the CLI agent writes to stdout.
acpManager.on("stdout", ({ sessionId, data }) => {
console.log(`[${sessionId}] stdout: ${data}`);
});Emitted when the CLI agent writes to stderr.
acpManager.on("stderr", ({ sessionId, data }) => {
console.error(`[${sessionId}] stderr: ${data}`);
});Emitted when the CLI agent process exits.
acpManager.on("exit", ({ sessionId, code, signal }) => {
console.log(`[${sessionId}] exited with code ${code}, signal ${signal}`);
});Emitted when the CLI agent process errors.
acpManager.on("error", ({ sessionId, error }) => {
console.error(`[${sessionId}] error: ${error}`);
});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",
});Each agent has default spawn arguments defined in the registry. You can override them:
acpManager.spawn("claude", "claude", ["--print", "--verbose"], {});Default prompt timeout is 120 seconds (2 minutes). You can override:
await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutesAgent detection is cached for 60 seconds to avoid expensive filesystem scans. Force refresh:
import { refreshAgentCache } from "@/lib/acp";
refreshAgentCache();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
ACP validates that the version command binary matches the expected binary name (unless it's a custom agent).
Each ACP session runs in its own child process. The process is killed when the session ends or times out.
-
First call: ~50-200ms (runs
versioncommand for each agent) - Cached calls: <1ms (returns from cache)
- Cache TTL: 60 seconds
- Spawn: ~50-100ms
- Send prompt: ~10-50ms
- Wait for response: Depends on CLI agent (typically 1-30 seconds)
- Kill: ~5 seconds (SIGTERM) + immediate (SIGKILL)
- Memory per session: ~10-50MB (depends on CLI agent)
- CPU: Minimal (I/O bound)
- Disk: None
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.
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", [], {});
}Problem: acpManager.sendPrompt() throws ACP timeout after 120000ms
Solution: Increase the timeout:
await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutesProblem: 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
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
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);
}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);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();- API Reference — REST API endpoints
- Provider Reference — All 226 providers
- MCP Server — Model Context Protocol integration
- A2A Server — Agent-to-Agent protocol
- Cloud Agent — Cloud-based agents
- AionUi Project — Inspiration for ACP auto-detection
-
ACP Source Code — Implementation details
-
manager.ts— Process lifecycle management -
registry.ts— Agent discovery and registration -
index.ts— Public API exports
-
OmniRoute · Website · npm · Docker Hub
- Setup Guide
- User Guide
- Features
- Quick Start (Docker)
- Electron Desktop App
- Termux (Android)
- PWA Guide
- MCP Server
- A2A Server
- Agent Protocols
- OpenCode Plugin
- Webhooks
- Cloud Agents
- Skills
- Memory
- Evals
- Gamification
- Guardrails
- Compliance
- Error Sanitization
- Public Credentials
- Route Guard Tiers
- Stealth Guide
- CLI Token Auth