Skip to content

Provider CLI integration

AgentMux does not call AI provider APIs directly. Instead, it manages a provider CLI subprocess — claude, codex, gemini, or others — inside a PTY, and communicates with the provider’s API through that CLI. This page describes how that subprocess is created, configured, and supervised.

ProviderCLI binarynpm packageController typeOutput format
Claude Codeclaude@anthropic-ai/claude-codepersistentstream-json (NDJSON)
Codexcodex@openai/codexsubprocessJSON
Geminigemini@google/gemini-clisubprocessstream-json (NDJSON)
OpenClawopenclawacpJSON-RPC 2.0 over stdio
Kimikimisubprocessstream-json (NDJSON)
Muxcodemuxcode@agentmuxai/muxcodesubprocessstream-json (NDJSON) — reuses the Claude translator

The controller type determines the spawn strategy: persistent keeps the CLI process alive between turns (Claude Code); subprocess spawns a fresh process per turn; acp uses the Agent Communication Protocol over stdio.

Provider definitions live in frontend/app/view/agent/providers/index.ts.

Before spawning, AgentMux resolves the CLI binary path via the ResolveCliCommand RPC. The resolution order is:

  1. AgentMux-managed install~/.agentmux/cli/<provider>/node_modules/.bin/<provider> (.cmd on Windows). This is always preferred; versions are pinned per release (e.g., Claude Code 2.1.185).
  2. System PATH — used only as a fallback for informational display (detect_installed_clis), never for actually launching agents.

AgentMux installs provider CLIs via npm into its own directory so agents always run a known, tested version regardless of what the user has globally installed.

Source: agentmux-cef/src/commands/providers.rs get_local_cli_bin_path(), detect_installed_clis().

Launch arguments are assembled by frontend/app/view/agent/buildRuntimeArgs.ts. When they are applied depends on the controller type:

  • persistent (Claude Code): Args are built once at the time the CLI subprocess is spawned and do not change between turns — the process stays alive for the session.
  • subprocess (Codex, Gemini, others): Args are rebuilt per turn, since a fresh process is spawned for each turn.

The construction process:

  1. Start from base argsProviderDefinition.launchArgs (e.g., ["-p", "--output-format", "stream-json", "--verbose", "--include-partial-messages"] for Claude).
  2. Strip conflicting flags — any --dangerously-skip-permissions, --permission-mode, --yolo, --model, and --effort flags already in the base are removed so runtime values take precedence.
  3. Append permission mode — provider-dependent:
    • Claude/others: --permission-mode <value> (e.g., default, bypass-permissions)
    • Kimi / Gemini / Qwen: only --yolo is supported
    • Codex: no permission flag (baked into base args)
  4. Append model and effort (Claude only):
    • --model <opus|sonnet|haiku>
    • --effort <low|medium|high> — skipped for Haiku (returns HTTP 400)
  5. Codex model special case — the model value is prepended to the trailing - positional arg rather than passed as --model.

This means the same agent definition can be launched with different models or effort levels without changing the stored definition — the runtime config overrides are applied at spawn time.

Step 3 — PTY creation (the blockcontroller)

Section titled “Step 3 — PTY creation (the blockcontroller)”

The blockcontroller (agentmux-srv/src/backend/blockcontroller/shell.rs) is the component that creates and supervises the PTY. It owns the agent subprocess from birth to death.

let pty_system = native_pty_system();
let pair = pty_system.openpty(pty_size)?;

native_pty_system() from the portable-pty crate selects the platform implementation:

  • Windows: ConPTY (Windows Pseudo Console API)
  • Unix: POSIX posix_openpt()

The PTY size is seeded from the frontend-computed terminal dimensions before the process starts, avoiding a post-spawn resize race. Fallback is 25 rows × 200 columns.

The following environment variables are injected into the agent’s PTY at spawn:

VariableValue
AGENTMUX_LOCAL_URLhttp://127.0.0.1:<port> — sidecar base URL
AGENTMUX_BLOCKIDPane UUID — the agent’s own pane context
AGENTMUX_AGENT_IDAgent’s registered name
TERM, COLORTERM, TERM_PROGRAMTerminal capability hints
PATHAugmented with AgentMux-managed binary paths

AGENTMUX_AUTH_KEY is not placed in the agent PTY environment (stripped at sidecar startup, security PR #801). Only internal tooling that requires it (agentmux-mcp, agentmux-bashwrap) receives it via their own spawn configuration.

See Environment variable contract for the full variable list.

Once the process is running, the blockcontroller runs three concurrent async tasks:

TaskResponsibility
PTY read loopReads process stdout → line-buffer → JSON parse → FileStore + WPS events
Input loopRoutes the input channel → PTY writer (user turns, resize events, signals)
Wait loopMonitors process exit, updates block status, classifies failures

The blockcontroller state machine has three states: INIT → RUNNING → DONE.

Each provider emits a different JSON format. The backend uses a Translator trait (agentmux-srv/src/agents/translator/mod.rs) with per-provider implementations:

pub trait Translator: Send {
fn translate(&mut self, frame: Value) -> Vec<AgentEvent>;
}

The runner is provider-agnostic and holds Box<dyn Translator>. Each implementation maps provider-specific JSON frames to the internal AgentEvent enum.

Raw bytes from the PTY are accumulated in a line buffer before JSON parsing:

  • Lines not starting with { are fast-rejected (terminal UI output, not structured events)
  • The buffer preserves raw bytes (not lossy-decoded strings) so multi-byte UTF-8 sequences that span chunk boundaries are handled correctly
  • Buffer cap is 1 MiB (AGENT_LINE_BUFFER_CAP); reset if no newline is found within that window

Parsed events are published on the WPS scope agent_event:<block_id> and simultaneously written as raw bytes to the xterm.js renderer.

Source: agentmux-srv/src/backend/blockcontroller/shell.rs extract_agent_events().

Frame typeMaps to
stream_event.content_block_delta.text_deltaAssistantText event
stream_event.content_block_stop (tool_use)ToolUse event
result frame with cost_usdCost + Done events
result frame with is_error: trueFailure classification

Source: agentmux-srv/src/agents/translator/claude.rs.

OpenClaw uses JSON-RPC 2.0 over stdio rather than NDJSON. Its controller is in agentmux-srv/src/backend/blockcontroller/acp.rs.

Step 5 — Subprocess lifecycle and signal handling

Section titled “Step 5 — Subprocess lifecycle and signal handling”

When an agent is stopped (via agent.stop RPC or user action):

Unix:

  1. SIGTERM sent to the process group (negative PID) — signals the CLI and all its children
  2. Grace period waits (KILL_GRACE_SECS = 5)
  3. SIGKILL sent if the process is still running
  4. PTY writer (input_tx) is dropped, delivering EOF/SIGHUP as a belt-and-suspenders signal

Windows:

  • child.kill() only — no signal support in Win32

Source: agentmux-srv/src/backend/blockcontroller/shell.rs stop().

When the CLI exits unexpectedly:

  1. Stdout/stderr readers are drained with a 2-second timeout
  2. The result frame’s is_error flag is checked
  3. In-band API errors (401 auth failure, rate limits) are detected from synthetic assistant frames
  4. The last ~40 lines of stderr are retained for failure classification
  5. crate::agents::failure::AgentFailure classifies the exit into a structured failure type surfaced to the UI

Source: agentmux-srv/src/backend/blockcontroller/subprocess.rs.

Session IDs are captured from the CLI’s stdout and persisted in block metadata. On resume, the frontend passes --resume <session_id> to the CLI so the conversation context is restored without re-sending history.

The abstraction is hybrid — not a single unified interface throughout:

LayerApproach
Frontend provider definitionsStatic lookup table (PROVIDERS record), conditional branching on provider.id
Argument constructionConditional logic per provider in buildRuntimeArgs.ts
Backend spawnConditional on ProviderDefinition.controllerType (persistent / subprocess / acp)
Backend stream parsingAbstract Translator trait — each provider implements translate(frame) -> Vec<AgentEvent>