Skip to content
diegosouzapw edited this page Jun 15, 2026 · 2 revisions

🌍 View in other languages

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 --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.

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 | undefined

setCustomAgents(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: AcpSession

Allowed 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: boolean

acpManager.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 minutes

Detection 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 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:

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 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

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?


Reference

  • 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

Clone this wiki locally