acp — the external ACP harness engine
acp is the part of the module that actually talks to other agent
harnesses: it resolves the agent name, spawns the harness in ACP server
mode, drives a full Agent Client Protocol
turn as a client, streams the agent’s output in real time, and accumulates
the result until the turn ends.
pub async fn call(
agent: &str,
input: &str,
cwd: PathBuf,
chunk_tx: tokio::sync::mpsc::UnboundedSender<String>,
) -> Result<(String, String), String>
The chunk_tx channel receives every agent message chunk as it is produced
(for live TUI display); the returned tuple is the accumulated output plus
the ACP stop reason (e.g. "EndTurn", or the client-side terminal marker
"error"). The ACP session runs on a dedicated current-thread
runtime inside tokio::task::spawn_blocking, so the ambient async runtime is
not blocked and the caller’s future stays Send.
The agent registry (ACP_AGENTS)
ACP_AGENTS is the static table of supported ACP harnesses, modeled by the
[Agent] struct:
pub struct Agent {
pub name: &'static str, // public name the model passes as `agent`
pub command: &'static str, // executable that speaks ACP over stdio
pub args: &'static [&'static str],// args that put it into ACP server mode
pub requires: &'static [&'static str], // binaries that must be in PATH
pub install_hint: &'static str, // guidance when the spawn fails
pub model: &'static str, // preferred session model ("" = harness default)
}
There are no per-agent prompt flags: the prompt travels as the ACP
session/prompt payload, never as a command-line argument. Adding an agent
is purely a table entry — anything registered in the official ACP registry
works.
Model selection (model)
Some harnesses pick a different default model for ACP than for their own
CLI — and the ACP default may be a paid-tier model the user’s existing
login cannot use (kilo, for instance, defaults its ACP sessions to
kilo/google/gemini-3-pro-image, which fails the prompt with
"You need to sign in to use this model." even though kilo run works
out of the box with the provider keys in the environment). When an Agent
entry names a model, run_session issues the standard ACP
session/set_config_option request
after session/new to switch the session to it. A model the harness does
not offer aborts the call with a clear error instead of a confusing
auth failure; an empty model leaves the harness default untouched.
| Name | ACP invocation |
|---|---|
gemini |
gemini --experimental-acp |
goose |
goose acp |
opencode |
opencode acp |
kilo |
kilo acp |
cline |
cline --acp |
devin |
devin acp |
claude |
npx -y @agentclientprotocol/claude-agent-acp@latest (official adapter) |
codex |
npx -y @agentclientprotocol/codex-acp@latest (official adapter) |
Two design points behind the table:
- Only ACP-capable harnesses are listed. The protocol contract replaces
the old one-shot CLI scraping (per-agent
input_flagtables that broke on every CLI update). Agents without ACP support (cursor,aider,interpreter) were removed — they cannot satisfy the contract. claudeandcodexare not ACP-native yet: their entries spawn the official Zed ACP adapters vianpx(the same route the SDK’s ownAcpAgent::claude_agent()/AcpAgent::codex()helpers take), and theirrequireslist demands both the engine binary andnpx.
detect_installed — which harnesses exist
pub fn detect_installed() -> &'static Vec<&'static str>
Scans PATH for each registered agent’s required binaries and returns the
names found. The result is cached in a OnceLock, so detection runs once
per process — repeated calls return the cached list. SubAgent::new()
uses this to tailor the tool description’s agent enum to what is actually
installed.
validate_agent — name checking
pub fn validate_agent(agent: &str) -> Result<(), String>
Returns Ok when the name is in ACP_AGENTS, else an error listing every
supported agent:
Unsupported agent 'nope'. Supported agents (ACP): gemini, goose, ….
Use bash_run for shell commands.
This runs first inside call, so an unknown name fails before anything is
spawned.
The ACP turn pipeline
- Validate the agent name (
validate_agent) and build theAcpAgentlauncher for its command + args. - Spawn & initialize. The harness subprocess is spawned speaking
JSON-RPC over stdio; the client sends
initializeadvertising thefscapability (read/write text file) so conformant agents may delegate file operations to cosh. - Authenticate with the first advertised method, when the harness
advertises
auth_methods(e.g.goose acp). Harnesses that manage auth themselves advertise none and are skipped. - Session.
session/newopens a session rooted atcwd(the workspace directory). When the agent entry names amodel, the client switches the session to it viasession/set_config_option(see Model selection above). - Prompt.
session/promptsends the task as a single text content block. While the turn runs:session/updatenotifications carryingAgentMessageChunkfeed the turn’s closure (see below) and stream throughchunk_tx;session/request_permissionrequests are auto-approved (first option, YOLO style) so a headless call never blocks; with no options the request is answeredCancelledper the spec;fs/read_text_file/fs/write_text_filerequests operate on the real workspace files (1-based lines, absolute paths per the protocol).
- Complete. The prompt response’s stop reason is returned with the
turn’s REPORT — the sub-agent’s LAST message (the text written after its
final tool call), not the concatenation of every message it produced.
Earlier narration between tool calls lives only in the TUI’s live
timeline; the market-standard contract (Claude Code’s Task tool, Gemini
CLI’s
generalist,codex exec --output-last-message) returns the final message alone, keeping the caller’s context clean. A defensive fallback applies when the turn ends right after a tool call with no post-call text: the last completed message is returned instead of an empty report (the mechanism issubagent::closure::TurnClosure). There is NO time limit: a sub-agent may work for hours, and the turn ends when the agent ends it — or earlier only when the user stops it, which sendssession/canceland yields the spec-mandated"cancelled"stop reason with the partial output preserved. A harness that ignoressession/canceland never ends the turn is abandoned with the call (its task and process stay alive until the app exits) — the stop signal is the only early-end mechanism.
Errors
| Situation | Result |
|---|---|
| Unknown agent name | Err("Unsupported agent …") — before any spawn |
| ACP handshake/prompt failure, no output | Err naming the failing step |
| ACP turn failed, partial output | Ok((partial_output, "error")) — with a warning log |
| Turn completed | Ok((output, stop_reason)) |