ADR-0030: Agent Client Protocol Groundwork
- Status: Accepted — groundwork only. Protocol vocabulary, framing and the initialize handshake are implemented and tested. No transport is wired into the spawn path, and every client capability is off.
- Date: 2026-07-30
- Supersedes: None
- Related: ADR-0021 (the role in which nubos-pilot is an ACP client), ADR-0002 (why there is no SDK), ADR-0012 (why "groundwork" is stated rather than implied)
Context and Problem Statement
nubos-pilot maintains a per-runtime payload for fourteen host CLIs. Each new host means another entry in the runtimes registry, another install path, another managed-Markdown target, another set of doctor checks, and — as v1.4.0's Codex work showed — another set of host-specific decisions about what a "skill" or a "subagent" even is on that platform.
The Agent Client Protocol is a JSON-RPC 2.0 protocol standardising exactly the boundary those adapters keep re-implementing: capability negotiation, session lifecycle, prompting, permission requests, filesystem and terminal access. If it gains adoption, one adapter eventually replaces N.
The question is what to build now. Two failure modes are available:
- Build nothing and re-litigate the question every time a host is added.
- Build a full ACP client against a protocol that is still moving, and own the maintenance of speculative plumbing — which is what the Economy axis exists to prevent.
There is also a modelling question that is easy to get backwards. In ACP terms the editor is the client and the coding agent is the agent. nubos-pilot installs a methodology payload into a host, so in that role it is neither. But since ADR-0021 it also runs its own agent loop and spawns agents itself — and in that role it is a client. The second role is the one where an adapter replaces per-host spawn integrations.
Decision Drivers
- The per-host adapter cost is real and recurring.
- The protocol is young. Building against a moving target has a genuine cost, and the parts most likely to change are the session and prompt shapes, not the framing.
- [ADR-0002] Zero runtime dependencies. No ACP SDK.
- A capability is a promise. Over-declaring produces a hang, not an error.
- Stated stage over implied readiness. A module that looks finished and is not is worse than an obviously partial one.
Considered Options
- A: Do nothing; revisit when ACP is widely adopted. Zero cost, and every future host keeps paying full adapter price.
- B: Full ACP client wired into the spawn path as a third dispatch route. Highest value if the protocol holds; highest waste if it moves.
- C: Adopt an ACP SDK. Rejected by ADR-0002.
- D: Build the parts that are correct regardless of how the session layer evolves, and stop there. Chosen.
Decision Outcome
Chosen: Option D. What is implemented is the part that is useful before any transport exists and that a future client must get right anyway:
- the method vocabulary as data (
AGENT_METHODS,CLIENT_METHODS), so the wire strings live in one place; - newline-delimited JSON-RPC framing, encode and decode;
- the
initializehandshake — request construction and response validation; - version negotiation;
- an honest capability declaration.
What is not implemented: any transport, any session, any client-side handler. There is deliberately no verb that connects to an agent, and a test pins that absence — while no transport exists, a verb that looked like it connected would be the most misleading thing this could ship.
Four decisions worth recording
Every client capability is false. A declared capability is a promise the peer acts on. Claiming fs.readTextFile without the handler makes the agent send a request that never gets answered — a hang, at the least debuggable moment. Flags are enabled by the commit that implements the handler and not before. CAPABILITY_METHODS pins flag→method parity so a future implementer cannot flip one side without the other.
Version negotiation refuses rather than guesses. protocolVersion is a single integer identifying a MAJOR version. A differing MAJOR means the message shapes differ, so continuing optimistically would send well-formed requests that mean something else — a silent behavioural bug instead of a handshake error. A non-integer protocolVersion is likewise treated as a protocol mismatch rather than something to coerce. Supported versions are a set, not a number, because supporting two adjacent majors during a transition is normal and a single constant would force a hard cutover.
The decoder returns a remainder. A chunk boundary lands mid-message routinely, and a decoder that assumed chunk == message would drop or corrupt traffic under exactly the load that makes it hard to reproduce. A malformed line is collected as an error without discarding the messages that framed correctly around it.
session/cancel is built as a notification and refuses an id. The spec defines it as one, and a caller awaiting that id would wait for the life of the process.
An unstated agent capability is treated as absent. Symmetrical to our own rule: reading an omitted loadSession as available is the same class of bug as over-declaring.
Consequences
Good, because:
- The expensive-to-get-right, unlikely-to-change parts are done and tested (33 cases), so a future transport is a smaller, better-understood change.
- The method strings and capability semantics have a single home, so a future implementer starts from data rather than from the spec page.
- Zero cost if ACP does not gain adoption: no dependency, no daemon, nothing on the default path.
Bad, because:
- It delivers no user-visible capability today. Nothing installs differently, nothing spawns differently. Judged as a feature this is unfinished; judged as groundwork it is complete, which is exactly why the status line says "groundwork only".
- It is a bet. If ACP stalls, this is dead code with tests — small, isolated dead code, but dead.
- The session and prompt layers, which are the largest remaining work and the most likely to change, are untouched. The estimate that "one adapter replaces fourteen" is unproven until they exist.
More Information
- Implementation:
lib/acp.cjs, CLIbin/np-tools/acp.cjs(verbsstatus | initialize-request | negotiate). acp statusreports the stage, the supported versions, the capability declaration andtransport_wired: false— the single honest answer to "does nubos-pilot speak ACP yet".- Protocol reference: https://agentclientprotocol.com. Verified against the specification's initialization page on 2026-07-30 (
protocolVersionis an integer; keys arecamelCase; discriminator string values aresnake_case). - Revisit trigger: a second first-class runtime shipping ACP support, or the next host-adapter request. At that point the session layer becomes worth building and this ADR should be superseded rather than extended.
