Spaces:
Runtime error
Runtime error
| title: ACP (Agent Client Protocol) | |
| # 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 | |
| ```bash | |
| # Example: Install Claude Code CLI | |
| npm install -g @anthropic-ai/claude-code | |
| # Verify installation | |
| claude --version | |
| ``` | |
| ### Step 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 | |
| 1. **Spawn** β ACP creates a child process for the CLI agent | |
| 2. **Send** β ACP writes prompts to the process's stdin | |
| 3. **Receive** β ACP reads responses from stdout/stderr | |
| 4. **Idle Detection** β ACP waits 2 seconds of inactivity before considering the response complete | |
| 5. **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: | |
| 1. **Send prompt** β Write to stdin with a newline | |
| 2. **Wait for response** β Read from stdout until idle (2s of no output) | |
| 3. **Timeout** β Default 120 seconds (configurable) | |
| --- | |
| ## API Reference | |
| ### Registry Functions | |
| #### `detectInstalledAgents()` | |
| Detects all installed CLI agents on the system. Results are cached for 60 seconds. | |
| ```typescript | |
| 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. | |
| ```typescript | |
| import { getAvailableAgents } from "@/lib/acp"; | |
| const available = getAvailableAgents(); | |
| // Returns: CliAgentInfo[] (only installed agents) | |
| ``` | |
| #### `getAgentById(id)` | |
| Gets a specific agent by ID. | |
| ```typescript | |
| import { getAgentById } from "@/lib/acp"; | |
| const agent = getAgentById("claude"); | |
| // Returns: CliAgentInfo | undefined | |
| ``` | |
| #### `setCustomAgents(agents)` | |
| Sets custom agent definitions from settings. | |
| ```typescript | |
| 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. | |
| ```typescript | |
| import { acpManager } from "@/lib/acp"; | |
| const session = acpManager.spawn("claude", "claude", ["--print", "--output-format", "json"], { | |
| /* custom env vars */ | |
| }); | |
| // Returns: AcpSession | |
| ``` | |
| **Allowed agent IDs**: `["claude", "codex", "gemini", "qwen"]` | |
| #### `acpManager.sendPrompt(sessionId, prompt, timeoutMs)` | |
| Sends a prompt to a CLI agent and collects the response. | |
| ```typescript | |
| 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. | |
| ```typescript | |
| import { acpManager } from "@/lib/acp"; | |
| const killed = acpManager.kill("acp-claude-1234567890-abc123"); | |
| // Returns: boolean | |
| ``` | |
| #### `acpManager.getActiveSessions()` | |
| Gets all active sessions. | |
| ```typescript | |
| import { acpManager } from "@/lib/acp"; | |
| const sessions = acpManager.getActiveSessions(); | |
| // Returns: AcpSession[] | |
| ``` | |
| #### `acpManager.killAll()` | |
| Kills all sessions. | |
| ```typescript | |
| import { acpManager } from "@/lib/acp"; | |
| acpManager.killAll(); | |
| ``` | |
| ### Session Interface | |
| ```typescript | |
| 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. | |
| ```typescript | |
| acpManager.on("stdout", ({ sessionId, data }) => { | |
| console.log(`[${sessionId}] stdout: ${data}`); | |
| }); | |
| ``` | |
| #### `stderr` | |
| Emitted when the CLI agent writes to stderr. | |
| ```typescript | |
| acpManager.on("stderr", ({ sessionId, data }) => { | |
| console.error(`[${sessionId}] stderr: ${data}`); | |
| }); | |
| ``` | |
| #### `exit` | |
| Emitted when the CLI agent process exits. | |
| ```typescript | |
| acpManager.on("exit", ({ sessionId, code, signal }) => { | |
| console.log(`[${sessionId}] exited with code ${code}, signal ${signal}`); | |
| }); | |
| ``` | |
| #### `error` | |
| Emitted when the CLI agent process errors. | |
| ```typescript | |
| 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: | |
| ```typescript | |
| 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: | |
| ```typescript | |
| acpManager.spawn("claude", "claude", ["--print", "--verbose"], {}); | |
| ``` | |
| ### Timeouts | |
| Default prompt timeout is **120 seconds** (2 minutes). You can override: | |
| ```typescript | |
| await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes | |
| ``` | |
| ### Detection Cache | |
| Agent detection is cached for **60 seconds** to avoid expensive filesystem scans. Force refresh: | |
| ```typescript | |
| import { refreshAgentCache } from "@/lib/acp"; | |
| refreshAgentCache(); | |
| ``` | |
| --- | |
| ## Security | |
| ### Command Injection Prevention | |
| ACP validates version commands to prevent command injection attacks: | |
| ```typescript | |
| 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 `version` command 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()`: | |
| - `claude` | |
| - `codex` | |
| - `gemini` | |
| - `qwen` | |
| 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: | |
| ```typescript | |
| 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: | |
| ```typescript | |
| await acpManager.sendPrompt(sessionId, prompt, 300000); // 5 minutes | |
| ``` | |
| ### CLI Not Detected | |
| **Problem**: `detectInstalledAgents()` doesn't find your CLI | |
| **Solutions**: | |
| 1. **Check PATH**: Ensure the CLI is in your system PATH | |
| 2. **Check version command**: Run `claude --version` manually | |
| 3. **Check permissions**: Ensure the CLI is executable | |
| 4. **Custom agent**: Add a custom agent definition for non-standard CLIs | |
| ### Permission Denied | |
| **Problem**: ACP can't execute the CLI | |
| **Solutions**: | |
| 1. **Check file permissions**: `chmod +x /usr/local/bin/claude` | |
| 2. **Check ownership**: Ensure OmniRoute has read/execute permissions | |
| 3. **Check SELinux/AppArmor**: May block process spawning | |
| --- | |
| ## Examples | |
| ### Example 1: Spawn and Use Claude Code | |
| ```typescript | |
| 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 | |
| ```typescript | |
| 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 | |
| ```typescript | |
| 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](../reference/API_REFERENCE.md)** β REST API endpoints | |
| - **[Provider Reference](../reference/PROVIDER_REFERENCE.md)** β All 226 providers | |
| - **[MCP Server](./MCP-SERVER.md)** β Model Context Protocol integration | |
| - **[A2A Server](./A2A-SERVER.md)** β Agent-to-Agent protocol | |
| - **[Cloud Agent](./CLOUD_AGENT.md)** β Cloud-based agents | |
| --- | |
| ## Reference | |
| - [AionUi Project](https://github.com/iOfficeAI/AionUi) β Inspiration for ACP auto-detection | |
| - [ACP Source Code](../../src/lib/acp/) β Implementation details | |
| - `manager.ts` β Process lifecycle management | |
| - `registry.ts` β Agent discovery and registration | |
| - `index.ts` β Public API exports | |