ACP — the Agent Client Protocol (Zed's editor↔agent protocol, also spoken by Neovim/JetBrains plug-ins) served BY vis.
MCP points OUT of the agent (agent → tools). ACP points IN (editor → agent),
so here vis is the SERVER: an editor spawns vis acp, speaks newline-framed
JSON-RPC 2.0 over stdio, and drives real vis sessions.
The protocol layer is deliberately TRANSPORT-FREE and BACKEND-FREE:
handle-line! takes one framed line and returns the response map. Every
adversarial case (bad JSON, batch array, non-object message, wrong
jsonrpc, missing/duplicate id, unknown method, wrong param types, calls
before initialize) is answered with a well-formed JSON-RPC error instead
of a thrown stack trace or a desynchronized stream.
connection takes a :backend map of functions
(:new-session/:load-session/:prompt/:cancel). gateway-backend
is the live one — every ACP session IS an ordinary gateway session, so the
SAME turn can be watched from the TUI, the web surface, and the phone
Companion while the editor drives it. Nobody else's ACP agent does that,
because everyone else's is a private per-editor subprocess.
Two transports ship:
serve! — stdio, the transport the ACP spec requires. Full duplex, so
session/request_permission and fs/* work.routes-contribution — POST /v1/acp on the regular gateway daemon,
one JSON-RPC message per request, every message the agent produced coming
back in messages. HALF-DUPLEX by construction (the agent cannot block on
an answer that would need a second request), so client-bound calls fail
fast instead of deadlocking, and the permission hook stays out of the way.Encoding is total: json-safe renders keywords, non-finite doubles and
unknown objects rather than letting write-json-str throw halfway through a
frame and poison the stream — the same discipline gateway/wire.clj applies.
ACP — the Agent Client Protocol (Zed's editor↔agent protocol, also spoken by
Neovim/JetBrains plug-ins) served BY vis.
MCP points OUT of the agent (agent → tools). ACP points IN (editor → agent),
so here vis is the SERVER: an editor spawns `vis acp`, speaks newline-framed
JSON-RPC 2.0 over stdio, and drives real vis sessions.
The protocol layer is deliberately TRANSPORT-FREE and BACKEND-FREE:
* [[handle-line!]] takes one framed line and returns the response map. Every
adversarial case (bad JSON, batch array, non-object message, wrong
`jsonrpc`, missing/duplicate id, unknown method, wrong param types, calls
before `initialize`) is answered with a well-formed JSON-RPC error instead
of a thrown stack trace or a desynchronized stream.
* [[connection]] takes a `:backend` map of functions
(`:new-session`/`:load-session`/`:prompt`/`:cancel`). [[gateway-backend]]
is the live one — every ACP session IS an ordinary gateway session, so the
SAME turn can be watched from the TUI, the web surface, and the phone
Companion while the editor drives it. Nobody else's ACP agent does that,
because everyone else's is a private per-editor subprocess.
Two transports ship:
* [[serve!]] — stdio, the transport the ACP spec requires. Full duplex, so
`session/request_permission` and `fs/*` work.
* [[routes-contribution]] — `POST /v1/acp` on the regular gateway daemon,
one JSON-RPC message per request, every message the agent produced coming
back in `messages`. HALF-DUPLEX by construction (the agent cannot block on
an answer that would need a second request), so client-bound calls fail
fast instead of deadlocking, and the permission hook stays out of the way.
Encoding is total: [[json-safe]] renders keywords, non-finite doubles and
unknown objects rather than letting `write-json-str` throw halfway through a
frame and poison the stream — the same discipline `gateway/wire.clj` applies.How long the agent waits for an editor to answer fs/* or
session/request_permission before giving up. A hung editor must never hang
the agent forever.
How long the agent waits for an editor to answer `fs/*` or `session/request_permission` before giving up. A hung editor must never hang the agent forever.
Files larger than this are not pushed into an editor buffer.
Files larger than this are not pushed into an editor buffer.
Tool ops whose result is mirrored into the editor's buffer with
fs/write_text_file, so the editor shows the change without a disk reload.
Tool ops whose result is mirrored into the editor's buffer with `fs/write_text_file`, so the editor shows the change without a disk reload.
Tool ops routed through session/request_permission while an ACP editor owns
the session.
Tool ops routed through `session/request_permission` while an ACP editor owns the session.
ACP Implementation for the initialize response — what a client shows the
user and puts in bug reports. Version is the vis/VERSION build resource,
"dev" when running from source.
ACP `Implementation` for the initialize response — what a client shows the user and puts in bug reports. Version is the `vis/VERSION` build resource, "dev" when running from source.
(arg-paths x)Every :path/"path" value anywhere inside a tool's arguments.
The walk is ITERATIVE on purpose. Arguments are untrusted JSON that an editor,
a model, or an extension can nest arbitrarily deep, and this runs inside the
permission/mirror hook: a recursive walk answers a deep value with a
StackOverflowError, which is an Error no handler catches — it kills the
turn instead of degrading the way json-safe does.
Every `:path`/`"path"` value anywhere inside a tool's arguments. The walk is ITERATIVE on purpose. Arguments are untrusted JSON that an editor, a model, or an extension can nest arbitrarily deep, and this runs inside the permission/mirror hook: a recursive walk answers a deep value with a `StackOverflowError`, which is an `Error` no handler catches — it kills the turn instead of degrading the way [[json-safe]] does.
(around-hook env op-kw args next-fn):around op-hook. Outside an ACP session this is a pass-through. Inside one,
a mutating op is approved by the editor first (allow always remembered per
session+op) and its result is mirrored into the editor's buffers.
`:around` op-hook. Outside an ACP session this is a pass-through. Inside one, a mutating op is approved by the editor first (`allow always` remembered per session+op) and its result is mirrored into the editor's buffers.
(call! conn method params & [{:keys [timeout-ms]}])Send a client-bound REQUEST and block for the answer. Returns {:result r} or
{:error e} — never throws, so a hostile/absent editor cannot break a tool.
Send a client-bound REQUEST and block for the answer. Returns `{:result r}` or
`{:error e}` — never throws, so a hostile/absent editor cannot break a tool.(client-supports? conn & ks)True when the client advertised the nested capability, e.g.
(client-supports? conn "fs" "writeTextFile").
True when the client advertised the nested capability, e.g. `(client-supports? conn "fs" "writeTextFile")`.
(connection {:keys [out-fn backend half-duplex?]})A live ACP connection. :out-fn is called with ONE encoded line per outgoing
message and must be safe to call from several threads. :half-duplex? marks a
transport that cannot carry agent→client REQUESTS (the HTTP route).
A live ACP connection. `:out-fn` is called with ONE encoded line per outgoing message and must be safe to call from several threads. `:half-duplex?` marks a transport that cannot carry agent→client REQUESTS (the HTTP route).
(content-block->text b)Flatten ONE ACP content block to the text vis puts in a turn request. Unknown or malformed blocks flatten to nil rather than exploding.
Flatten ONE ACP content block to the text vis puts in a turn request. Unknown or malformed blocks flatten to nil rather than exploding.
(decode line)Parse ONE framed line. Returns {:msg m}, or {:error e} with the JSON-RPC
error object to answer with (id null).
Parse ONE framed line. Returns `{:msg m}`, or `{:error e}` with the JSON-RPC
error object to answer with (id `null`).(echo-backend & [f])A dependency-free backend used by tests and by --self-test: it never starts a
daemon and answers every prompt from f (default: echo the prompt text).
:load-session answers nil for a session it never created, which is the
backend contract for "no such session".
A dependency-free backend used by tests and by `--self-test`: it never starts a daemon and answers every prompt from `f` (default: echo the prompt text). `:load-session` answers `nil` for a session it never created, which is the backend contract for "no such session".
(encode msg)One newline-free JSON line for msg.
One newline-free JSON line for `msg`.
JSON-RPC 2.0 codes plus ACP's own reserves.
ACP hands out meanings inside JSON-RPC's implementation-defined range: -32000
is Authentication required, -32002 is Resource not found, -32800 is
Request cancelled. :not-initialized is ours alone, so it sits on -32001,
which the spec leaves free — it used to squat on -32002 and told any client
that special-cases the standard codes that a FILE was missing.
JSON-RPC 2.0 codes plus ACP's own reserves. ACP hands out meanings inside JSON-RPC's implementation-defined range: -32000 is `Authentication required`, -32002 is `Resource not found`, -32800 is `Request cancelled`. `:not-initialized` is ours alone, so it sits on -32001, which the spec leaves free — it used to squat on -32002 and told any client that special-cases the standard codes that a FILE was missing.
(event->update event)Translate ONE gateway SSE event into the update payload of an ACP
session/update, or nil when the event has no ACP meaning.
Translate ONE gateway SSE event into the `update` payload of an ACP `session/update`, or nil when the event has no ACP meaning.
(gateway-backend)The live backend: every ACP session is an ORDINARY vis gateway session, so the editor, the TUI, the web surface, and the phone all watch the same turns.
The live backend: every ACP session is an ORDINARY vis gateway session, so the editor, the TUI, the web surface, and the phone all watch the same turns.
(handle-line! conn line)Process ONE framed message. Emits the response through the connection's
out-fn and returns it, or nil for a notification / a client answer.
This never throws: malformed input becomes a JSON-RPC error message.
Process ONE framed message. Emits the response through the connection's `out-fn` and returns it, or nil for a notification / a client answer. This never throws: malformed input becomes a JSON-RPC error message.
ACP method → handler (fn [conn params]).
ACP method → handler `(fn [conn params])`.
(json-safe x)(json-safe x depth)Total JSON projection of x: keywords/symbols become strings, map keys become
strings, NaN/±Infinity become nil, anything unknown becomes its str, and
anything nested past [[max-json-depth]] or longer than [[max-json-items]]
becomes a marker string. ACP is one
message per LINE, so a value that cannot be encoded must not be able to throw
halfway through a write and desynchronize the framing.
Total JSON projection of `x`: keywords/symbols become strings, map keys become strings, NaN/±Infinity become nil, anything unknown becomes its `str`, and anything nested past [[max-json-depth]] or longer than [[max-json-items]] becomes a marker string. ACP is one message per LINE, so a value that cannot be encoded must not be able to throw halfway through a write and desynchronize the framing.
(notify! conn method params)Send a client-bound NOTIFICATION (no id, no reply).
Send a client-bound NOTIFICATION (no id, no reply).
(prompt->text blocks)The prompt array of an ACP session/prompt as one request string.
The prompt array of an ACP `session/prompt` as one request string.
Newest ACP protocol version this agent implements.
Newest ACP protocol version this agent implements.
(read-text-file! conn session-id path & [{:keys [line limit]}])The editor's CURRENT buffer for path — unsaved edits included — when the
client advertises fs.readTextFile; the bytes on disk otherwise.
The editor's CURRENT buffer for `path` — unsaved edits included — when the client advertises `fs.readTextFile`; the bytes on disk otherwise.
(request-permission! conn session-id tool-call & [options])Ask the editor to approve tool-call. Returns :allow-once, :allow-always,
:reject-once, :reject-always, or :cancelled.
FAILS CLOSED: a timeout, a transport error, or an option id we never offered is a rejection, never an approval.
Ask the editor to approve `tool-call`. Returns `:allow-once`, `:allow-always`, `:reject-once`, `:reject-always`, or `:cancelled`. FAILS CLOSED: a timeout, a transport error, or an option id we never offered is a rejection, never an approval.
(serve! & [{:keys [in out backend]}])Run the ACP agent loop over :in/:out (default stdin/stdout) until EOF.
Lines are handled IN ORDER, except session/prompt, which runs on its own
virtual thread so session/cancel lands while the turn is still streaming.
Returns the closed connection.
Run the ACP agent loop over `:in`/`:out` (default stdin/stdout) until EOF. Lines are handled IN ORDER, except `session/prompt`, which runs on its own virtual thread so `session/cancel` lands while the turn is still streaming. Returns the closed connection.
Versions handle-line! will echo back as negotiated.
ONLY v1, deliberately. v0 was listed here too, but nothing in this namespace
ever branched on it: we answered protocolVersion 0 and then spoke v1 shapes
at the client anyway. Claiming a version we do not implement is worse than
refusing it — a v0-only client now reads 1 out of the initialize response
and disconnects on its own terms, per the spec's instruction to the client.
Versions [[handle-line!]] will echo back as negotiated. ONLY v1, deliberately. v0 was listed here too, but nothing in this namespace ever branched on it: we answered `protocolVersion 0` and then spoke v1 shapes at the client anyway. Claiming a version we do not implement is worse than refusing it — a v0-only client now reads `1` out of the initialize response and disconnects on its own terms, per the spec's instruction to the client.
(tool-kind tool)Map a vis tool name onto an ACP tool-call kind.
Map a vis tool name onto an ACP tool-call kind.
(write-text-file! conn session-id path content)Push content into the editor's buffer for path. Returns true when the
editor took it, false when the client has no fs.writeTextFile.
Push `content` into the editor's buffer for `path`. Returns true when the editor took it, false when the client has no `fs.writeTextFile`.
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |