LemonCore. ACPClientBridge
(lemon_core v0.1.0)
View Source
Direct request/reply channel from a run's tool execution to the ACP client handler that owns the client connection for that run.
This replaces the former :acp_client_request Bus broadcast. That event was
RPC wearing an event's clothes: it carried a live pid and a reference() over
a cluster-wide pub/sub topic to fake a synchronous request/reply. Unlike every
other bus event it could not be serialised, persisted or replayed, and under
Phoenix.PubSub it fanned the pid out to every node only for a single process
to answer. This module makes the exchange what it always was: a direct call to
one registered process.
Roles
- The handler is the process serving an ACP
session/promptturn (LemonControlPlane.ACP). It owns the JSON-RPC connection to the ACP client and callsregister/1for the run's id while it waits, then services{:acp_client_request, payload}messages from its own receive loop. - The requester is a coding-agent filesystem tool
(
CodingAgent.Tools.ACPFileBridge) that needs the client to read or write a file on its side. It callsrequest/4and blocks for the reply.
Both apps depend on lemon_core and neither on the other, so this shared
rendezvous lives here rather than in either endpoint.
Failure contract
request/4 answers rather than raises, mirroring LemonCore.RouterBridge.
Three failure modes, deliberately distinguished so the caller can log the
cause even though the consequence is the same (the file operation did not
happen):
{:error, :no_client}— no handler is registered for the run. Under the old broadcast this presented as a full-timeout hang, because a broadcast to an empty topic is silently dropped; now it fails fast.{:error, :client_down}— a handler was registered but its process died before replying. Caught via monitor so the requester is not left waiting.{:error, :timeout}— the handler was reachable but did not reply within the caller's timeout.
Node locality
The registry is node-local. The ACP bridge is a single-connection JSON-RPC preview where the handler owns the client socket and the run it submits is serviced in the same BEAM, so a local registry is sufficient and correct. Making this cross-node would mean a distributed registry behind the same API; it is deliberately out of scope, and the pid/ref-over-broadcast pattern this replaces was never a sound way to do it either.
Summary
Functions
Child spec for the rendezvous registry. Added to LemonCore.Application's
supervision tree so request/4 and register/1 are always available.
Register the calling process as the ACP client handler for run_id.
Make a synchronous client request against the handler registered for run_id.
Unregister the calling process as the handler for run_id. Safe to call even
if nothing is registered; the handler calls it from an after block.
Return the pid of the handler registered for run_id, or nil.
Functions
Child spec for the rendezvous registry. Added to LemonCore.Application's
supervision tree so request/4 and register/1 are always available.
Register the calling process as the ACP client handler for run_id.
Idempotent per process: a second call from the same process is a no-op. A call
from a different process while one is still registered returns
{:error, {:already_registered, pid}} rather than crashing the caller — a run
is submitted and waited exactly once, so this should not happen, but a handler
should not die trying to register.
@spec request(binary(), String.t(), map(), timeout()) :: {:ok, term()} | {:error, :no_client | :client_down | :timeout}
Make a synchronous client request against the handler registered for run_id.
Sends {:acp_client_request, %{method:, params:, reply_to:, ref:}} to the
handler and blocks for {:acp_client_response, ref, response}. The payload
shape matches what the handler's receive loop already expects, so the handler
side is unchanged apart from receiving a direct message instead of a bus event.
@spec unregister(binary()) :: :ok
Unregister the calling process as the handler for run_id. Safe to call even
if nothing is registered; the handler calls it from an after block.
Return the pid of the handler registered for run_id, or nil.