Managoat.ACP.Testing.ScriptedAgent (managoat_acp v0.1.0)

Copy Markdown View Source

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 → agentreply
initializeagentCapabilities from :capabilities, authMethods from :auth_methods
authenticate{}
session/newsessionId 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/promptthe 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/cancelthe outstanding prompt's response with stopReason: "cancelled"
anything else with an ida -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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

connect(agent, peer)

@spec connect(GenServer.server(), pid()) :: :ok

Tell the agent which peer to answer. Frames written before this call are answered now, in order.

raw(agent, line)

@spec raw(GenServer.server(), binary()) :: :ok

Send an arbitrary line to the peer, newline appended if missing.

start_link(opts \\ [])

@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.

update(agent, update)

@spec update(GenServer.server(), map()) :: :ok

Send one session/update notification to the peer, out of turn or in it.

writer(agent)

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.