chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,559 @@
|
||||
---
|
||||
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
|
||||
Reference in New Issue
Block a user