560 lines
14 KiB
Markdown
560 lines
14 KiB
Markdown
---
|
|
title: ACP (Agent Client Protocol)
|
|
---
|
|
|
|
# ACP (Agent Client Protocol)
|
|
|
|
> **TL;DR**: ACP lets OmniRoute spawn CLI agents (like Claude Code, Codex) 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 |
|
|
| `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
|