acp
v0.3.5Agent Client Protocol surface — stdio JSON-RPC, exposes iii agents as ACP sessions
Install
iii trigger compose::add worker=acp@0.3.5Run 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 the brain worker's trigger 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; queue backs durable cancel topics. Agent
# events come from the brain's own worker.
iii trigger compose::add worker=state 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. |
--events-trigger-type (IIIACP_EVENTS_TRIGGER_TYPE) |
Trigger type(s) carrying the brain's AgentEvent frames, comma-separated or repeated. Default (claude::run -> claude::agent-event, codex::run -> codex::agent-event). |
--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's worker publishes AgentEvent frames on its own
trigger type (claude-code claude::agent-event,
pi pi::agent-event, opencode, cursor, codex, grok, devin and hermes the
same way). iii-acp binds it once per session it owns, with config
{ "session_id": ", so the producer only delivers that connection's
sessions; the binding is removed on session/close and when the process
exits. Each delivery is { session_id, event_id, seq, epoch, source, event };
iii-acp restores order by (epoch, seq) (a 250 ms reorder window per stream,
bounded at 256 buffered frames) and drops duplicates. Before running an
external brain it checks that every events trigger type is registered
(engine::triggers::info) and fails the prompt with the missing type and
worker otherwise. Frames are not replayed: if none reached the editor during
a prompt (for example across a reconnect), the final text of the brain's
response is rendered once instead. Each event is translated:
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 |
Any brain that registers a trigger type with this
contract works; point --events-trigger-type at it when its namespace differs
from the brain function's.
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: the brain's trigger (bound per session), 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 test50 lib (including the agent event sequencer, feed contract and readiness) + 10 protocol envelope tests. Integration smoke against a live engine lives in the iii test harness.