I Added ACP to APX Without Adding a Second Brain
The first time I looked at ACP support for APX, I felt the usual temptation: build a special path for the new protocol, give it its own session store, tune it for IDE behavior, and let it grow into a separate product-shaped thing.
That would have been a mistake.
The real lesson for me was simpler: ACP should be a thin adapter, not a second brain. If APX already has a daemon, a super-agent, and a stream of events that describes a turn, then the ACP surface should translate that work, not re-implement it.
That choice sounds small. It is not. It decides whether a new surface stays cheap to maintain or slowly becomes a fork with better branding.
What ACP is doing in APX
APX already knows how to talk to its own daemon. The ACP agent in src/interfaces/acp/index.js does not start a parallel reasoning engine. It opens a JSON-RPC 2.0 stream on stdio and maps ACP session calls onto the daemon's existing chat stream.
That is the important part.
A client calls session/new, session/prompt, or session/cancel. APX does the rest through the same runtime path it already uses elsewhere. The ACP layer is just a translator between one protocol and another.
In practice, that means:
-
initializereports the ACP version and basic capabilities -
authenticateis accepted as a no-op because daemon auth stays on the local bearer token -
session/newresolves the project from the working directory -
session/promptstreams the prompt into/projects/:pid/super-agent/chat/stream -
session/cancelaborts the active turn
That is enough.
The protocol surface can be rich without becoming a second implementation. That distinction matters more than it looks.
Why I did not give ACP its own state machine
I have built enough agent tooling now to know what happens when a surface starts storing its own truth.
At first it feels convenient. The new client gets fast shortcuts. The code looks localized. You can add a field here, a cache there, maybe one special handler for the IDE case.
Then the drift begins.
One session store says the prompt happened.
Another store says it did not.
One adapter thinks cancellation means one thing.
Another adapter thinks it means something slightly different.
By the time you notice, the system is no longer one system.
So I kept ACP session state minimal. The adapter keeps just enough in memory to make the connection work: session id, conversation history, active turn, and a few counters for updates. No daemon-side ACP session database. No duplicate agent brain. No second source of truth.
That is boring. It is also easier to reason about.
The daemon already owns the real work. ACP should not compete with it.
The stream is the contract
The cleanest thing about this design is that ACP listens to the daemon's stream instead of inventing its own event language.
The mapping is plain:
-
assistant_textbecomesagent_message_chunk -
tool_startbecomestool_call -
tool_resultbecomestool_call_update -
confirmation_requiredbecomes an ACP permission round-trip -
finalbecomes the session response
That one-to-one mapping matters because it keeps the adapter honest.
If the daemon says a tool started, ACP does not re-label it into something fancier. If the daemon ends with a final result, ACP does not hide it behind another layer of interpretation. The client sees the same work, only wrapped in ACP wire shapes.
I like that because it preserves debugging power. When something goes wrong, I can trace it through the same stream APX already uses. I do not need to ask whether ACP invented a separate failure mode.
Why history stays in the client connection
One subtle choice in the ACP code is that conversation history stays per connection, in memory.
That might sound like a compromise. For this surface, it is the right one.
The history exists so the ACP session can feed previousMessages on the next turn. That lets a client keep context without forcing the daemon to become a session database for every external protocol that touches it.
It also keeps the adapter narrow. The daemon still owns the real runtime. The ACP client owns its own conversation continuity. APX just bridges the turn.
That split is useful because ACP clients are not all the same. Some are IDEs. Some are editors. Some are automation surfaces. I do not want APX to guess their long-term memory model for them.
The real reason I wanted ACP at all
I did not add ACP because I wanted another badge on the README.
I added it because I want APX to reach more surfaces without changing its core shape.
The CLI is still the CLI.
The daemon is still the daemon.
The web admin is still a local window onto the same runtime.
ACP just lets other clients drive the same super-agent in a language they already speak.
That is the whole point of APC for me: one project context, many compatible tools. If APX is going to be the runtime layer for that idea, then protocol adapters should be cheap, local, and replaceable.
A thin ACP bridge gives me that.
A second brain would not.
What this changed in my thinking
This work changed how I judge new surfaces.
Now I ask a few blunt questions before I add anything:
- Does this surface need its own state, or can it reuse the daemon?
- Is this a translation layer, or am I sneaking in a fork?
- Can I map events one-to-one, or am I inventing new semantics just because I can?
If the honest answer starts drifting toward "new brain," I stop and simplify.
That is not purity for its own sake. It is maintenance math.
Every extra brain creates another place where bugs can hide. Every thin adapter keeps the system closer to the contract I already trust.
The rule I kept
My rule for ACP ended up very close to the rule I keep for APC itself: preserve the contract, do not duplicate the contract.
APC says the project context should stay portable.
APX says the runtime should stay local.
ACP fits only if it stays a bridge between those ideas and another client, not a new center of gravity.
So I kept it thin.
That made the code easier to read.
It made cancellation simpler.
It made debugging less theatrical.
And it kept APX from growing the one thing I did not want to maintain: a second brain pretending to be just another adapter.
Top comments (0)