acp
v0.3.3Agent Client Protocol surface — stdio JSON-RPC, exposes iii agents as ACP sessions
Install
iii trigger compose::add worker=acp@0.3.3Run in your project with the iii engine running. Exact versions are immutable and digest-pinned.
acp
iii as a first-class agent in any ACP-speaking editor.
iii-acp is a stdio JSON-RPC adapter that exposes the iii engine — and every
brain worker on it — through the Agent Client Protocol.
Editors and clients that already speak ACP launch iii-acp as a subprocess
and drive it through their native agent UI. No editor plugin, no fork, no
bespoke per-client integration.
Status: 0.3.0. Server-side only (
acp-clientfor consuming external ACP agents ships separately). All eleven client→agent methods are implemented (initialize,authenticate,session/{new,load,resume,list,prompt,cancel,close,set_mode,set_config_option}) plus thesession/updateagent→client notification. Reverse-RPC paths (session/request_permission,fs/{read,write}_text_file,terminal/{create,kill,output,release,wait_for_exit}) remain deferred — internal iii brains use iii primitives directly for filesystem and terminal access. Full per-method status in the Methods table.
Upgrading from 0.2
Version 0.3 stores records, history, ownership, and prompt claims in the isolated acp-v0.3 state scope. Existing 0.2 sessions remain untouched but are not listed or loadable through 0.3. This deliberate compatibility fence prevents mixed 0.2 and 0.3 processes from mutating the same history or dispatching overlapping prompts; finish or close important 0.2 sessions before upgrading.
Why this exists (vs MCP, skills, agent workers)
These are stack-able layers, not alternatives. ACP fills the slot that the others don't.
| What it does | When you reach for it | |
|---|---|---|
MCP server (iii-mcp) |
Exposes iii functions as tools to an external agent | You're already running Claude Code / Cursor / etc. and want to give it iii tools |
| Skill bundles | Curated prompts + tools loaded into an agent host | You're inside a skill-aware host (Claude Code, Cursor) and want a preset toolset |
Agent workers (turn-orchestrator, agent, coding, …) |
The brain itself — registers run::start_and_wait, runs LLM turns, executes tools |
You're calling iii from your own code (iii.trigger("run::start_and_wait", …)) or backend automation |
iii-acp (this worker) |
Editor → iii. Translates ACP session/* JSON-RPC into the canonical iii brain contract; turns iii's agent::events stream into ACP session/update notifications |
You want iii to be the agent in an editor users already opened today |
ACP is the north edge of the stack. MCP is the south edge. They coexist:
Editor (Zed, VS Code, Neovim, …)
↓ ACP ─ session/prompt, session/update ← iii-acp
iii engine + brain workers (turn-orchestrator,
provider-router, guardrails, llm-budget,
audit-log, dlp-scrubber, policy-denylist, …)
↓ MCP ─ tools/list, tools/call ← iii-mcp / mcp-client
External tool servers (filesystem, browser, …)What iii brings to an editor session that a vanilla agent host doesn't:
- Provider routing — switch Claude/GPT/local model per session via
provider-router, no editor restart - Budgets — hard token/dollar caps per session via
llm-budget - Guardrails — input + output PII/secret scrub via
guardrails,dlp-scrubber - Audit trail — every turn durably logged via
audit-log - RBAC — per-session tool gating via
iii-worker-manager - Observability — full distributed trace from editor click to provider API call via
engine::traces::* - Durable sessions —
session/loadreplays history straight from iii state
Supported clients
ACP is an open spec. Any client that speaks it works with iii-acp. As of
this writing the public client list (agentclientprotocol.com/get-started/clients)
includes:
Editors / IDEs
| Client | How to wire iii-acp |
|---|---|
| Zed | agent_servers block in ~/.config/zed/settings.json (snippet below) |
| Visual Studio Code | ACP Client extension |
| JetBrains | ACP plugin |
| Neovim | CodeCompanion, carlos-algms/agentic.nvim, or yetone/avante.nvim |
| Emacs | agent-shell.el |
| Obsidian | Agent Client plugin |
| Unity | Unity ACP Client / Unity Agent Client |
| Chrome | Chrome ACP |
CLIs / Apps / Notebooks / Mobile
acpx (CLI), Agent Studio, AionUi, aizen, DeepChat, gemini-cli-desktop,
Harnss, iflow-cli, Jockey, Lody, Minion Mind, Mitto, Nori CLI,
Ngent, pool, RayClaw, RLM Code, Sidequery, Tidewave, Toad,
Web Browser with AI SDK, agent-client-kernel (Jupyter), DuckDB
(via sidequery/duckdb-acp), marimo, Agmente (iOS), Ferngeist (Android),
Happy, Mobvibe (mobile), OpenACP (Telegram/Discord/Slack), and others.
Setup pattern is the same everywhere: point the client at the iii-acp
binary, set the IIIACP_* env vars below.
Prerequisites
iii-acp needs an iii engine plus a brain. Minimum stack:
# 1. Workers acp uses directly. state holds session
# records + history; iii-stream carries the agent::events tape;
# queue backs durable cancel topics.
iii trigger compose::add worker=state worker=iii-stream worker=queue
# 2. acp itself.
iii trigger compose::add worker=acp
# 3. The brain stack. turn-orchestrator drives the loop;
# provider-router routes assistant turns to provider-anthropic
# (or any other provider worker); auth-credentials stores the
# Anthropic API key. session-inbox / llm-budget / hook-fanout
# are pulled in transitively.
iii trigger compose::add worker=turn-orchestrator worker=provider-router \
worker=provider-anthropic worker=auth-credentials worker=session-inbox \
worker=llm-budget worker=hook-fanout
# 4. (Optional but recommended) — iii's distinctive primitives.
iii trigger compose::add worker=guardrails worker=dlp-scrubber worker=audit-log \
worker=policy-denylist worker=context-compactionStore the Anthropic API key once:
iii trigger \
--function-id auth::set_token \
--payload '{"provider":"anthropic","credential":{"type":"api_key","key":"sk-ant-..."}}'Verify the brain runs end-to-end before plugging in an editor:
iii trigger --function-id run::start_and_wait --payload '{
"session_id": "smoke",
"messages": [{"role":"user","content":[{"type":"text","text":"reply with hi"}],"timestamp":0}],
"model": "claude-sonnet-4-5-20250929",
"provider": "anthropic"
}' --timeout-ms 30000A messages array ending in an assistant message with "text":"hi" means
the stack is healthy.
Spawn
iii-acp is a stdio agent. The client launches it as a subprocess and
exchanges JSON-RPC frames over stdin/stdout. stderr is reserved for
logs; stdout is reserved for ACP frames.
iii-acp --use-canonical-brain --model claude-sonnet-4-5-20250929 --provider anthropicConfiguration
| Flag / env | Effect |
|---|---|
--engine-url (-e, IIIACP_ENGINE_URL) |
iii engine WebSocket URL. Default ws://localhost:49134. |
--debug (-d) |
Verbose tracing on stderr. |
--brain-fn (IIIACP_BRAIN_FN) |
iii function id that runs the prompt turn. Falls back to a built-in echo brain when unset. Canonical value is run::start_and_wait (turn-orchestrator). |
--brain-stop-fn (IIIACP_BRAIN_STOP_FN) |
Optional iii function id called with session_id when ACP cancels an external brain turn. |
--use-canonical-brain (IIIACP_USE_CANONICAL_BRAIN) |
Shortcut for --brain-fn run::start_and_wait. |
--model (IIIACP_MODEL) |
Model id forwarded to the brain (e.g. claude-sonnet-4-5-20250929). |
--provider (IIIACP_PROVIDER) |
Provider id forwarded to the brain (e.g. anthropic). Routes to provider::. |
--system-prompt (IIIACP_SYSTEM_PROMPT) |
System prompt prepended to every turn. |
--rbac-tag |
Forwards x-iii-rbac-tag on the worker WebSocket so iii-worker-manager's auth_function_id can apply policy. |
Editor wiring
Zed
~/.config/zed/settings.json:
{
"agent_servers": {
"iii-acp": {
"type": "custom",
"command": "/path/to/iii-acp",
"args": [],
"env": {
"IIIACP_ENGINE_URL": "ws://localhost:49134",
"IIIACP_USE_CANONICAL_BRAIN": "true",
"IIIACP_MODEL": "claude-sonnet-4-5-20250929",
"IIIACP_PROVIDER": "anthropic",
"IIIACP_SYSTEM_PROMPT": "You are an iii expert. Answer in iii primitives only."
}
}
}
}Restart Zed → Agent panel → + → pick iii-acp → type a prompt.
VS Code
Install the ACP Client extension. Add iii-acp as a custom agent in the
extension's settings, pointing command at the binary and replicating the
env block above.
Neovim
Pick one of the ACP plugins listed under "Supported clients" and follow its
docs. Each one exposes a command + env config the same way Zed does;
the same env vars work.
JetBrains / Emacs / Obsidian / Unity / Chrome
Same pattern: client config takes a command path and env map. Point at
iii-acp with the env vars above. The protocol is the same on all sides.
CLIs (acpx, Nori CLI, …)
Most CLI ACP clients accept --agent or a config file. Point them
at iii-acp directly.
Methods
| Method | Direction | Status |
|---|---|---|
initialize |
client → agent | implemented |
authenticate |
client → agent | no-op success |
session/new |
client → agent | implemented |
session/load |
client → agent | implemented; replays history as session/update notifications |
session/resume |
client → agent | implemented; refreshes cwd + mcpServers, no history replay |
session/list |
client → agent | implemented; dedupes index on read |
session/prompt |
client → agent | implemented; routes to brain fn or echo |
session/cancel |
client → agent | implemented; flips in-process abort + publishes cancel topic |
session/close |
client → agent | implemented; full cleanup of record + history + index |
session/set_mode |
client → agent | implemented; persists modeId on session record for the brain |
session/set_config_option |
client → agent | implemented; persists configId/value pairs on session record |
session/update |
agent → client | streamed during prompt turn |
session/request_permission |
agent → client | deferred — needs reverse-RPC framer |
fs/read_text_file, fs/write_text_file |
agent → client | deferred — internal iii brains use iii fs primitives directly |
terminal/create, terminal/kill, terminal/output, terminal/release, terminal/wait_for_exit |
agent → client | deferred — internal iii brains use iii terminal primitives directly |
agentCapabilities.sessionCapabilities advertises list, close, and
resume on initialize. The loadSession capability (top-level for now,
unifying with sessionCapabilities is on the ACP roadmap) is also advertised.
session/set_mode and session/set_config_option ship without an explicit
capability flag because the version of the ACP schema in this repo doesn't
include those slots in SessionCapabilities yet — clients that try them
get a successful response; clients that don't try are unaffected. Both
methods persist their values on the session record (mode: Option
and config_options: Map). Brains opt in to honoring them
on the next session/prompt turn; iii-acp just stores.
Brain contract
iii-acp talks to the canonical iii brain shape used by turn-orchestrator
and every provider worker. Any function with this input contract drops in
as a brain — no adapter required.
Input (forwarded as iii.trigger payload):
{
"session_id": "sess_...",
"messages": [
{
"role": "user",
"content": [{"type": "text", "text": "..."}],
"timestamp": 1234567890
}
],
"model": "claude-sonnet-4-5-20250929",
"provider": "anthropic",
"system_prompt": "You are ...",
"timeout_ms": 600000
}Output:
{
"session_id": "sess_...",
"messages": [...],
"turn_count": 1
}iii-acp picks the ACP stopReason from the final assistant message's
stop_reason field (end → end_turn, length → max_tokens,
aborted → cancelled, error → refusal).
Streaming. The brain emits AgentEvent frames into agent::events
(group_id = session_id). iii-acp registers one stream subscriber per
connection at startup and translates each event:
AgentEvent |
ACP sessionUpdate |
|---|---|
message_update { llm_event: text_delta } |
agent_message_chunk |
message_update { llm_event: thinking_delta } |
agent_thought_chunk |
message_complete (assistant role, full text) |
agent_message_chunk (one shot) |
tool_execution_start |
tool_call (status: in_progress) |
tool_execution_end |
tool_call_update (status: completed/failed) |
| other | dropped |
This is the same stream context-compaction and every provider worker
already subscribe to. No bespoke iii-acp publish protocol.
State layout
All keys live in scope acp-v0.3. Session IDs are global UUIDs, while
conn_id is stored as ownership metadata and regenerated per subprocess.
sessions:_index = ["sess_a", "sess_b", ...]
sessions:<session_id> = { session_id, conn_id, cwd, mcp_servers, created_at_ms, last_activity_ms, mode, config_options }
sessions:<session_id>:history = { entries, cursor_item_ids, owner_conn_id, active_prompt, closed, closed_by_conn_id }Streaming wire: agent::events (per-session events), per-connection topic
acp: (best-effort cancel signal).
Wire example (raw stdio)
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1,"clientCapabilities":{},"clientInfo":{"name":"demo","version":"0"}}}' \
| iii-acp --use-canonical-brain --model claude-sonnet-4-5-20250929 --provider anthropicReplies stream on stdout, one JSON frame per line.
Tests
cargo test37 lib + 10 protocol envelope tests. Integration smoke against a live engine lives in the iii test harness.