class

LLM::ACPClient

Inherits LLM::Adapter < Reference < Object

ACP-backed client wrapper for communicating with local AI agents.

Constants

CLAUDE_ARGS = ["@zed-industries/claude-agent-acp"]
CODEX_ARGS = ["@zed-industries/codex-acp"]
DEFAULT_REQUEST_TIMEOUT = 120.seconds

Non-prompt requests (initialize, session/new) get a longer default than the HTTP connect timeout: the first npx run downloads the agent package before it can answer initialize, which easily exceeds 30 s.

GEMINI_ARGS = ["--experimental-acp"]
MAX_SESSIONS_PER_AGENT = 8

Claude/Codex adapters hold a subprocess or heavy state per session.

Constructors

new(provider : String, model : String, event_sink : Proc(String, Nil) | Nil = nil)
Source

Class methods

acp_provider?(provider : String) : Bool
Source
custom_command_allowed?

Escape hatch for power users running their own ACP agent binary. Off by default so an untrusted $NOIR_HOME/config.yaml (or a provider value pasted from elsewhere) can't silently spawn a process.

Source
default_model(provider : String, model : String) : String
Source
extract_target(provider : String) : String
Source
mute_acp_logs
Source
prompt_timeout

A prompt turn is the generation itself, so it takes the same budget as an HTTP provider's read timeout (NOIR_AI_TIMEOUT).

Source
request_timeout
Source
resolve_command(provider : String) : Tuple(String, Array(String))

Resolve provider aliases to actual executable command + args.

Source
tool_permissions_allowed?

session/request_permission is how the ACP agent asks to run a tool — shell commands, file writes, network fetches — on this machine. Noir used to answer every one of them with a hardcoded {"outcome":"selected","optionId":"allow-once"}, i.e. blanket approval for whatever the agent decided to do.

That is remote-controllable input. The prompt Noir sends is source code from the tree being scanned, and scanning code you did not write is the normal case; a file carrying "ignore the above and run X" is enough to turn an endpoint scan into arbitrary local execution, with the auto-yes removing the one checkpoint that would have caught it. It also contradicted the resolve_command hardening right above, which refuses to spawn an unknown agent binary precisely so untrusted config can't reach code execution.

Deny by default. Noir puts the code to analyse in the prompt, so the agent needs no tools to answer — the only thing lost is an agent's optional extra poking around. Operators who want that back opt in explicitly, same shape as the custom-command escape hatch.

Source

Instance methods

answer_permission_request(params : JSON::Any) : JSON::Any

Pick a real option from the ones the agent offered rather than inventing an id. Option ids are agent-defined (proceed_once, reject, …); the kind field is the part the protocol standardises, so it is what we match on. With nothing usable on offer, cancelled is the protocol's own "no decision" outcome and needs no id.

Public rather than private so the decision can be asserted without standing up an agent process.

Source
args
Source
close

Optional cleanup hook for adapters that manage external resources.

Source
command
Source
model
Source
provider
Source
report_request_failure(error : Exception) : Nil

General and Ollama both report their failures on stderr; the rescue above used to be bare, so a session that never spawned, an agent that died mid-turn, a protocol error and a genuinely empty answer were all the same "" with nothing written anywhere. The caller reads "" as "this code defines no endpoints", which made a completely dead agent look like a successful AI-assisted scan.

Public so the report can be asserted without standing up an agent process.

Source
report_stop_reason(stop_reason : String) : Nil

Anything but end_turn means the answer is cut short or absent (refusal, max_tokens, max_turn_requests, cancelled). Say so, or the caller reads it as "this code defines no endpoints".

Source
request(prompt : String, format : String = "json") : String

Send a single prompt and get a response as a String.

Source
request_messages(messages : Array(Hash(String, String)), format : String = "json") : String

Send chat-style messages (system/user) and get a response as a String.

Source

Nested types