Files
OmniRoute/docs/frameworks/ACP.md
Diego Rodrigues de Sa e Souza 8bd17be8aa docs: refresh every stale count to measured values + harden docs-counts gate (#10196)
* docs(reference): regenerate PROVIDER_REFERENCE from live provider modules

The catalog was hand-stale at 291 since 2026-08-05 while the live provider
modules define 338 unique IDs. The generator also omitted the NOAUTH_PROVIDERS
module entirely (10 providers) and hardcoded the executor count in its footer;
both are now sourced from the live modules.

Refs #9985

* docs: refresh stale counts across README/AGENTS/llm.txt and architecture docs

Every count updated to values measured from the live code on 2026-08-12:
providers 291/271/248/236/226/212->338, migrations 110/117/130->144, MCP tools
94/99/104->105 (base 42->43), scopes 13/32->31, strategies 17/18->19,
Auto-Combo factors 12/13->14 (sessionAvailability row added to the table),
executors 67/78/84/89->101, quality gates ~48->~80, locales 29/30/39/40+->43
(41 non-source), A2A skills 5->6 (list-capabilities), free tier 43 pools/516
models/~1.53B/~2.15B->42/495/~1.51B/~2.13B, contributors 500+->320+ (324
unique emails), llm.txt version 3.8.47->3.8.50. llm.txt i18n mirrors resynced
(headers preserved, body mirrored).

Refs #9985

* docs(diagrams): sync SVG hero/pillars/comparison/cli/tier numbers

Text nodes and aria-labels only; layout, coordinates and animation values
untouched. providers 278/290->338 (cli list footer 264->334 more), MCP tools
104->105, strategies 18->19, free tier 43 pools/460+/516 models->42/495,
headline ~1.53B/~2.15B->~1.51B/~2.13B. All six SVGs re-validated as XML.

Refs #9985

* feat(check): harden docs-counts gate - live provider source, llm.txt, migrations, SVGs

The gate trusted PROVIDER_REFERENCE.md as the provider total, so a hand-stale
doc (291 vs 338 live) kept it falsely green. New STRICT checks: doc total vs
the live provider modules (same collections the generator unions), provider
count in llm.txt and package.json description, migration count vs
README/AGENTS/llm.txt, and a canonical-number sweep (providers/MCP
tools/strategies/pools) over the six README SVG diagrams with
coordinate/attribute-safe patterns. TDD: 9 new unit tests (red first on the
missing exports, green after) in tests/unit/check-docs-counts-sync.test.ts.

Refs #9985

* docs(readme): refresh What's New range and add v3.8.50 cycle highlights

---------

Co-authored-by: backryun <bakryun0718@proton.me>
2026-08-12 14:24:56 -03:00

14 KiB

title
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
13 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 13 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 Qwen Code qwen --acp 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 these 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