- As an agent — editors (Zed, and others) drive Monad as their built-in agent: streamed responses, reviewable inline diffs, integrated-terminal commands, approval prompts.
- As a client — Monad can delegate a subtask to another ACP agent (codex, claude-code, …) via
the
agent_acp_delegatetool (see Multi-agent delegation).
@agentclientprotocol/sdk@^1.3.0.
Architecture — monad acp is a thin bridge
monad acp does not build a daemon in-process. It’s a thin bridge: the editor spawns it over
stdio, and it proxies the ACP connection to an already-running Monad daemon over the local Unix socket
(REST + inline SSE). If no daemon is running, it auto-spawns one (detached) and waits for health,
then bridges. The agent loop runs in that shared daemon — so editor sessions appear in the Web UI/TUI
and reuse one store, model config, and ledger.
apps/monad/src/transports/acp/): agent.ts (the MonadAcpAgent adapter),
connection.ts (AgentSideConnection wiring + the AcpHandlers port), bridge.ts (the
AcpHandlers RPC proxy to the daemon), launch.ts (discover/auto-spawn + bridge),
documents.ts (open-document tracking), translate.ts (pure Event↔ACP), backends.ts
(editor-facing fs/terminal backends). The daemon side:
services/delegation/delegation.ts (reverse fs/terminal) and the per-session runtime
registry behind sessions.configureRuntime (handlers/session/).
The bridge dials the local Unix socket only, so delegation + session-scoped MCP keep the
“local editor = trust boundary” assumption (a remote daemon is never targeted).
Running
--log trace shows per-call transport
activity on stderr.
Register in Zed
What’s supported
- Core:
initialize,session/new,session/prompt(streamingsession/update),session/cancel. - Sessions:
session/list,session/load(replays transcript),session/fork→ Monad’s branch (time-travel),session/resume(re-attach without replay),session/delete,session/close. - Permission: Monad’s oversight gate bridges to
session/request_permission. - Multimodal: image content blocks reach the model (transient on the turn’s user message).
- Document sync:
unstable_did{Open,Change,Close,Focus,Save}Document— open editor docs are tracked per session and folded into the turn as ambient context (utf-16 positions). - Commands: Monad’s unified command set (built-ins + atom pack commands + user-invocable skills) is advertised
via
available_commands_update. - Free-text clarify: via
unstable_createElicitation(form mode) when the client supports it; else the question is surfaced and the turn proceeds.
Per-session runtime config (sessions.configureRuntime)
Because the loop runs in the shared daemon, the editor’s per-session settings reach it out of band
(runOpts can’t cross the wire). On session/new|load|resume|fork the bridge calls
sessions.configureRuntime to push, for that session:
- Sandbox roots — the editor’s
cwd+additionalDirectoriesreplace the daemon’s default roots for this session’s fs/shell. Priority at turn time: explicit per-turn roots > configureRuntime rootsephemeral session root (
sandboxmodeephemeral) > the loop’s global default. - Session-scoped MCP servers — client-provided MCP servers connect daemon-side; their tools join every turn and are released on session close/delete. (Local-daemon only.)
- Delegation flags — which capabilities to delegate back to the editor (below).
Reverse fs/terminal delegation
When the client advertisesfs/terminal capability, Monad delegates those ops back to the editor so
edits appear as reviewable diffs and commands run in the editor’s terminal. Since the loop runs in
the daemon (not in the bridge), this is modelled like oversight/clarify — an out-of-band event + an
inbound RPC, not in-process backends:
- A delegated
file_write/shell tool runs in the daemon against a remote backend (DelegationService), which emits adelegation.fs_request/delegation.terminal_requestevent. - The event rides the turn’s stream to the bridge, which services it against the editor
(
fs/*,terminal/*reverse-RPC) and answers via thedelegation.respondRPC (streaming terminal output viadelegation.output).
process_*, code_execute, file_glob, file_grep) are dropped from a
delegated session so they can’t silently run on the daemon host (isDelegableTool).
Monad as an ACP client — multi-agent delegation
Theagent_acp_delegate tool spawns a configured external ACP agent and drives it to carry out a
self-contained subtask, returning its answer. Register agents in mesh.json:
env values support ${env:NAME} secret-refs (resolved at spawn). Monad serves
the sub-agent’s fs + terminal through its own sandbox (containment) and routes the sub-agent’s
permission prompts through the oversight gate; the sub-agent’s tool calls + plan surface on the parent
turn’s stream. env is operator-vetted infrastructure config → it lives in mesh.json, alongside
the third-party agent and peer definitions.
Monad extensions (_meta / extMethod)
Advertised in initialize under _meta.monad.extMethods:
_monad/session.restore{ sessionId, toMessageId }— rewind to a checkpoint._monad/model.{listProviders,listModels,listProfiles,getDefaultProfile,setDefaultProfile}— pick a model through Monad’s gateway. (Credential mutation is intentionally not exposed over ACP.)
_meta.monad.agentId to session/new to choose a configured agent; the response
echoes _meta.monad.agentIds.
Known limitations
- Per-session runtime config (sandbox roots / MCP / delegation) is in-memory; a daemon restart drops it
and the client re-pushes via
configureRuntimeon re-attach (correct for ACP’s stateless bridge). - Delegated terminals don’t yet pass through the sub-agent’s requested env vars.
- The bridge’s auto-spawn path isn’t covered by an automated test (spawning a second
buntrips the macOS taskgated code-signature kill — see the bun-exec-blocked note).
Debugging
apps/monad/test/e2e/acp-stdio.test.ts (with its acp-stdio.helper.ts) spawns a real
monad --acp --mock-model and drives it with the SDK client — initialize → new → prompt → fork.
Fastest inner loop:
bun scripts/bun-test.ts apps/monad/test/e2e/acp-{transport,bridge,delegate,stdio}.test.ts --only-failures.