devin's process-startup cost, not a protocol-level
speedup. A spike measured the same warm-session path against a fake server with no
startup cost and found ~6x on a ~5ms call — real, and nothing like the table implies.
The feature is still right for the agents people actually run; the justification is
narrower than the draft claimed.devin acp client exists in a consumer's tree today.ACP (Agent Client Protocol) is to agents what MCP is to tools: JSON-RPC 2.0,
one object per line, over a child process's stdin/stdout. devin acp, Gemini CLI and
Zed's agents already speak it, so one client reaches all of them.
The reporter's measurements, on macOS against devin (SWE-1.6 Slow), are the whole
argument:
| call | latency |
|---|---|
devin -p, 17-byte prompt | 15.3s |
devin -p, 12 KB prompt | 14.3s |
ACP session/prompt #1 | 17.2s |
ACP session/prompt #3 | 1.6s |
Two facts fall out, and they are stronger than "ACP is nicer". Prompt size is free (12 KB costs the same as 17 bytes), so the entire per-turn cost is process startup. And a warm session amortises it away — ~15s/turn to ~7.4s/turn over a real 6-turn loop.
toolnexus assembles a complete request every turn: the full message array, every time. An ACP session is stateful: it already has the transcript. Sending the whole thing each turn makes the session accumulate near-duplicate histories, and the reporter observed the agent answering a stale copy. Their mitigation is an explicit "this supersedes everything earlier" line — a prompt-level hack holding a protocol-level mismatch together.
The alternative is to send only the delta, which is faster still and makes the
client responsible for conversation state — i.e. a second, shadow copy of the thing
ConversationStore already owns, which can desynchronise from it.
This is the decision. Everything else in an ACP client is mechanical: initialize
/ session/new / session/set_mode / session/prompt; demultiplexing by JSON-RPC id
because session/update notifications interleave with responses; accumulating only
agent_message_chunk (thoughts and tool narration must be dropped or they wrap prose
around structured output); answering session/request_permission with the first
allow-kind option or the turn hangs to timeout even in bypass mode; process
lifetime independent of any one turn's context; idempotent close; one session per lane
because concurrent prompts interleave into one transcript.
The latency table measures devin, and only devin. Against a fake ACP server over
real pipes (spikes/acp/SPIKE.md) the warm-session gain is ~6x on a ~5ms call — pure
process-spawn overhead. Nobody should cite this ADR as evidence that ACP is faster than
alternatives in general. What the table actually proves is narrower and still worth
shipping: for a CLI whose startup dominates the turn, a warm session removes that cost
from every turn after the first.
The spike also found something no fake could produce. One live call against the
installed devin revealed that real devin acp requires an absolute cwd and an
mcpServers array on session/new, or it rejects with -32602. The hermetic fake
accepted anything. This is the argument for taking the reporter's live-tested
implementation over a from-scratch one, and it is now pinned by a test in every port.
Ship ACP as a Generate source — LoadACP(...) -> acp.Generate, handed to
CreateInProcessClient — so the loop, skills, MCP, adapters and sub-agents are
untouched. This is deliberately the smallest framing: ACP is a model source, not a
new tool source and not a new client.
On the state question, the proposed default is full request every turn plus an
explicit supersedes marker, with delta-mode as an opt-in — stateless-by-default
matches every other toolnexus model source, and a shadow transcript that can drift
from ConversationStore is a worse failure than a wasted token.
That default is exactly what the spike must try to break.
session/request_permission hangs the turn forever, not until
an error; answered with the first allow-kind option it completes in under a millisecond.devin's
startup, and the ADR now says so.SPEC.md §0 surface: ACP fits entirely inside the existing generate
seam. That made it a Go-shaped change, which is not the same as a Go-only one —
see below.The spike concluded "Go-local, no SPEC §0 change", and inferred from that it could ship in
Go alone. That inference was wrong and was overruled. An untouched conformance contract
says nothing about the parity obligation: a capability in one port and not the other six is
drift by definition, which is the single bug this repo exists to prevent. A follow-up audit
(spikes/portability/SPIKE.md) found every port already ships newline-JSON-over-pipes
machinery for MCP local stdio (SPEC §2), so ACP needed no new dependency anywhere.
Shipped in all seven ports, each with hermetic tests over real pipes.
client.py rejects an awaitable). Solved
with a background reader thread and a queue so generate stays synchronous — the seam
was widened nowhere, and nothing async crosses it.ConversationStore, which is a
worse failure than a wasted token.openspec/changes/add-acp-model-source/.Can you improve this documentation?Edit on GitHub
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 |