Skip to content

Agent Client Protocol (ACP)

The Agent Client Protocol is an open protocol for AI agent hosting. It defines how a host application spawns, manages, and communicates with external AI agents over a stdio JSON-RPC connection.

Key facts:

  • Full name: Agent Client Protocol (not “Agent Context Protocol”)
  • Specification: agentclientprotocol.com
  • Source: github.com/agentclientprotocol/agent-client-protocol
  • Crucible uses the agent-client-protocol crate, version 2.0.0 (wire schema 1.5.0)
  • Transport: stdio JSON-RPC over newline-delimited messages (same pattern as LSP)
  • Crucible is the host. It spawns external agents (Claude Code, OpenCode, Gemini CLI) as subprocesses.
  • The agent binary receives a stdio connection; Crucible drives the session lifecycle.

Three-Layer Architecture

Crucible’s agent integration stacks three protocols, each with a distinct role:

Crucible (ACP Host)
├── ACP Layer: Manages agent subprocess lifecycle, sessions, streaming
├── Skills Layer: Context injection from knowledge graph
└── MCP Layer: Exposes kiln tools to the agent
External Agent (e.g. Claude Code)
├── Receives ACP connection from Crucible
├── Loads skills context injected by Crucible
└── Calls MCP tools served by Crucible

ACP controls the agent. MCP provides tools to the agent. Skills provides knowledge. These layers compose cleanly: ACP manages the session, skills inject relevant context before each turn, and MCP handles tool calls the agent makes during its response.

Sessions

ACP is session-oriented. Every agent interaction happens within a session that tracks state across multiple turns. State persists between prompts within a session, and a host can load a prior session to continue its conversation.

Don’t confuse ACP sessions with Crucible’s internal daemon sessions. The daemon exposes its own JSON-RPC surface (session.create, session.send_message, session.subscribe, …) over the Unix socket — that is Crucible’s private client protocol, not ACP. When Crucible hosts or serves ACP, it bridges between the two.

Wire Methods

The ACP wire protocol is JSON-RPC 2.0 over stdio. The methods that matter in practice:

MethodDirectionDescription
initializeclient → agentVersion handshake and capability exchange
session/newclient → agentCreate a new session (with working directory)
session/promptclient → agentSend a user prompt; the response ends the turn with a stop reason
session/loadclient → agentResume a previously created session, with a replay of its history
session/resumeclient → agentContinue an earlier session without a replay of its history
session/set_config_optionclient → agentSet one session config option; a model switch uses the model_config category
session/listclient → agentList the sessions that the agent stores (Crucible does not send it yet)
session/deleteclient → agentDelete one stored session (Crucible does not send it yet)
session/cancelclient → agentCancel the in-progress turn
session/closeclient → agentClose a session and release its resources
session/updateagent → clientStreaming notification: message chunks, thought chunks, tool_call / tool_call_update entries
session/request_permissionagent → clientAsk the client to approve a tool call (allow/reject, once/always)
elicitation/createagent → clientAsk the user a free-form question; Crucible refuses it with -32601 today

Streaming

A prompt turn streams through session/update notifications:

  1. Client sends session/prompt with the user’s input
  2. The agent emits session/update notifications as it works: incremental message text, thought chunks (if the model exposes reasoning), and tool_call / tool_call_update entries as tools start and finish
  3. If a tool needs approval, the agent sends session/request_permission and waits for the client’s answer
  4. The session/prompt response returns with a stop reason (end_turn, cancelled, …) when the turn completes

The client renders updates in real time (TUI streaming, web SSE, etc.) and can cancel mid-turn with session/cancel.

Tool calls are an upsert

The agent reports tool calls as an upsert keyed by toolCallId. The spec sets no order between tool_call and tool_call_update: an update can arrive before its call, and some agents send only updates. To model this, Crucible keeps one tool-call table per turn. Crucible announces each call once, at the first update that carries a name. At the end of the turn, Crucible announces each nameless call under the label “Unnamed tool”, and closes each call that has no completion with an error that names the stop reason.

Permissions

Crucible does not enforce a per-capability ACP permission model. Tool calls from a hosted agent go through the same permission gate as every other session — permission patterns, agent-card tool policy, Lua hooks, and the [permissions] config (see Permission Precedence) — and interactive approvals surface as ACP session/request_permission requests.

The capabilities field on an [acp.agents.*] profile is parsed and stored but never read for enforcement. Setting capabilities = ["read_kiln"] on a profile has no effect today; do not rely on it to restrict an agent. Use the permission system instead.

Protocol Details

Handshake

When Crucible spawns an agent subprocess, it performs a version handshake via initialize. The current protocol wire version is 1. Versions are compatible if they share the same major version number.

Transport Configuration

Timeouts and limits under [acp] in config.toml:

  • streaming_timeout_minutes (default 15) — how long a streaming turn may go without completing before it is cut off.
  • The removed fields session_timeout_minutes and max_message_size_mb still load without an error; the values are ignored.

Error Handling

Errors propagate as JSON-RPC error responses with standard error codes. The error event type notifies the host of asynchronous failures during streaming. Crucible surfaces these in the TUI as inline error messages.

Built-in Agent Profiles

Crucible ships with profiles for common ACP-compatible agents:

ProfileCommandInstall
opencodeopencode acpnpm install -g opencode-ai@latest (or curl -fsSL https://opencode.ai/install | bash)
claudenpx @zed-industries/claude-agent-acpnpm install -g @zed-industries/claude-agent-acp (bridges to the Claude Code CLI)
geminigemininpm install -g @google/gemini-cli
codexnpx @zed-industries/codex-acpnpm install -g @zed-industries/codex-acp (bridges to the OpenAI Codex CLI)
cursorcursor-acpnpm install -g cursor-acp (bridges to the Cursor CLI)
hermeshermes acpcurl -fsSL https://hermes-agent.nousresearch.com/install.sh | bash

opencode, gemini and hermes speak ACP directly; the other three are bridges that also need the underlying vendor CLI installed. Hermes notes: a polished tool’s result arrives in content text blocks with no rawOutput; permission requests carry a fresh perm-check-N id that matches no announced tool call; session/close and ping answer -32601, which is not an error. cru prints the same install lines when no agent is found, so if this table ever disagrees with the binary, trust the binary.

Agent discovery uses parallel probing: Crucible checks all known agents concurrently via which + --version, caches the result, and falls back through the priority list if the preferred agent isn’t available.

Custom Agent Profiles

Define custom profiles in config.toml using extends to inherit from a built-in:

[acp.agents.my-claude]
extends = "claude"
env = { ANTHROPIC_BASE_URL = "http://localhost:4000" }
[acp.agents.my-agent]
command = "/usr/local/bin/my-agent"
args = ["--mode", "acp"]

Then use with: cru chat -a my-claude

Crucible as ACP Host

When you run cru chat -a claude, Crucible:

  1. Discovers the agent binary (parallel probe of known agents)
  2. Spawns the agent as a subprocess with stdio pipes
  3. Handshakes over JSON-RPC to negotiate protocol version
  4. Creates an ACP session and configures the agent
  5. Injects skill context and Precognition results (semantic search hits from your kiln)
  6. Streams the conversation through the TUI or web UI
  7. Routes all tool calls through Crucible’s MCP server, enforcing permissions

After a daemon restart, Crucible sends session/resume to continue the agent session; when the agent refuses, Crucible falls back to session/new. To switch the model, Crucible sends session/set_config_option with the agent’s model_config option. At shutdown, Crucible sends session/close when the agent advertises the capability. A -32601 reply to session/resume or session/close is not an error.

The agent never touches your kiln directly. Every file read, search, and write goes through Crucible’s tool layer, giving you full control over what the agent can access.

Precognition Integration

Before each turn, Crucible runs semantic search against your kiln using the user’s message as a query. Relevant note fragments are injected into the agent’s context alongside any loaded skills. This means the agent has access to your knowledge without you manually searching for context.

Crucible as ACP Agent

Crucible also implements the other side of the protocol: the agent role. Run

Terminal window
cru acp

and Crucible speaks ACP on stdin/stdout, so any ACP host (Zed, JetBrains, Neovim, marimo — or another Crucible instance) can drive it as a knowledge-grounded agent. Point your editor’s ACP agent configuration at the cru acp command.

What the host gets is the ordinary internal Crucible agent, exposed through a different front door:

  1. initialize — Crucible advertises protocol v1, text prompts, and loadSession support.
  2. session/new — creates a normal daemon session (type = chat, agent = internal) with the host-supplied cwd as the workspace. It shows up in cru session list and persists like any other session.
  3. session/prompt — the user’s message is forwarded to the daemon; the daemon’s event stream is translated into ACP session/update notifications: text deltas become agent message chunks, thinking becomes thought chunks, and tool calls/results become tool_call / tool_call_update entries (with a coarse tool-kind for host icons).
  4. session/request_permission — when the daemon needs approval to run a tool, Crucible surfaces it to the host as a permission request with Allow/Reject (once/always) options and maps the choice back to Crucible’s permission model.
  5. session/cancel — forwarded to the daemon to stop the turn; the prompt returns stop_reason = cancelled.
  6. session/load — resumes an existing daemon session so the host can continue a prior conversation.

Because sessions are real daemon sessions, Precognition and kiln tools apply automatically — the host does not need to know anything about Crucible’s knowledge graph.

Not yet wired (v1): session modes, model listing/switching and forking over ACP, host-side filesystem/terminal capabilities (tools run daemon-side exactly as for internal sessions), and authentication (none advertised). A free-form question maps to ACP elicitation/create, which Crucible does not serve yet; panels have no ACP analogue. Crucible auto-declines both.

Dogfood: Crucible hosting Crucible

Because Crucible is both host and agent, you can point one instance at another. Add a profile that runs cru acp:

[acp.agents.crucible]
command = "cru"
args = ["acp"]

Then cru chat -a crucible runs a full round trip: the host Crucible spawns cru acp, which serves the internal agent back over the protocol. This is the end-to-end test of both roles at once. (See the “Manual verification” note in the ACP agent-mode module for a scripted stdio recipe.)

Comparison with MCP

AspectACPMCP
PurposeAgent lifecycle and sessionsTool discovery and execution
DirectionHost controls agentAgent calls tools
Transportstdio JSON-RPC (subprocess)stdio or SSE
StateSession-oriented (multi-turn)Stateless (per-call)
StreamingBuilt-in event subscriptionNot specified

ACP and MCP are complementary. ACP manages the agent process and conversation. MCP provides the tools the agent uses during that conversation. In Crucible, both protocols work together: ACP on the outside (host ↔ agent), MCP on the inside (agent ↔ tools).

See Also