An ACP agent inside the BEAM, for driving a Managoat.ACP.Peer in tests.
It is the other end of the transport seam: writer/1 is the function a
test hands to Peer.start/1, and every frame the peer writes arrives here,
is decoded, and is answered the way a well-behaved adapter would answer
it. Replies go back into the peer through Managoat.ACP.Peer.stdout/2
once connect/2 has told the agent which peer it is talking to; frames
written before that (the initialize the peer sends on start) are held
and answered on connect.
Ships in lib/, like the other Managoat libraries' fakes, so a host's own
tests can run a real peer against it without a sandbox, a port or a stub.
{:ok, agent} = ScriptedAgent.start_link(updates: [text_chunk("hello")])
{:ok, peer} =
Peer.start(
owner: self(),
writer: ScriptedAgent.writer(agent),
ref: make_ref(),
prompt: "say hello",
mode: :run,
session_id: nil
)
:ok = ScriptedAgent.connect(agent, peer)
assert_receive {:acp, _, {:session, "scripted-session"}}
assert_receive {:acp, _, {:lines, "acp", line}}
assert_receive {:acp, _, {:done, "end_turn", nil}}What it answers
| client → agent | reply |
|---|---|
initialize | agentCapabilities from :capabilities, authMethods from :auth_methods |
authenticate | {} |
session/new | sessionId from :session_id, merged with :session_result (put configOptions or models there to make the peer pin a model) |
session/resume, session/load | :session_result |
session/set_config_option, session/set_model | {} |
session/prompt | the turn: session/request_permission first when :permission is set, then every map in :updates as a session/update notification, then the response with :stop_reason and :usage |
session/cancel | the outstanding prompt's response with stopReason: "cancelled" |
| anything else with an id | a -32601 error |
A :permission is %{"toolCall" => …, "options" => […]} in the protocol's
own shape; the agent sends it as a request and waits for the peer's answer
before streaming the updates, exactly as a blocked adapter would. The
answer's outcome is reported to the observer.
What the observer sees
Every frame the peer writes is reported to :observer (the process that
started the agent, by default) as {:scripted_agent, :wrote, decoded},
so a test can assert on the wire as well as on what the owner received;
and a permission answer as {:scripted_agent, :permission_answered, outcome}. Requests the agent sends carry ids from its own counter, which
starts at 0 per turn — claude-agent-acp's measured habit, and the reason
the peer mints public request ids.
Summary
Functions
Returns a specification to start this module under a supervisor.
Tell the agent which peer to answer. Frames written before this call are answered now, in order.
Send an arbitrary line to the peer, newline appended if missing.
Start an agent. Options: :session_id, :capabilities, :auth_methods,
:session_result, :updates, :stop_reason, :usage, :permission,
:observer (default: the caller), plus :name.
Send one session/update notification to the peer, out of turn or in it.
The writer to pass to Managoat.ACP.Peer.start/1 as writer:.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec connect(GenServer.server(), pid()) :: :ok
Tell the agent which peer to answer. Frames written before this call are answered now, in order.
@spec raw(GenServer.server(), binary()) :: :ok
Send an arbitrary line to the peer, newline appended if missing.
@spec start_link(keyword()) :: GenServer.on_start()
Start an agent. Options: :session_id, :capabilities, :auth_methods,
:session_result, :updates, :stop_reason, :usage, :permission,
:observer (default: the caller), plus :name.
@spec update(GenServer.server(), map()) :: :ok
Send one session/update notification to the peer, out of turn or in it.
@spec writer(GenServer.server()) :: Managoat.ACP.Transport.writer()
The writer to pass to Managoat.ACP.Peer.start/1 as writer:.
Total, as the transport contract requires: an agent that has stopped
answers {:error, :agent_exited}, which the peer reports as
{:failed, {:acp_write_failed, :agent_exited}} — the same shape a sandbox
whose runtime exited produces.