Skip to content

feat: add an Agent Client Protocol adapter - #41

Merged
dviejokfs merged 9 commits into
mainfrom
feat/acp-adapter
Oct 12, 2026
Merged

dviejokfs merged 9 commits into
mainfrom
feat/acp-adapter

Conversation

@dviejokfs

@dviejokfs dviejokfs commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

What

Adds Provider::Acp and providers::acp::Acp (default feature acp): one adapter for any agent that speaks the Agent Client Protocol over stdio, such as Gemini CLI (gemini --acp), GitHub Copilot (copilot --acp) and Cursor (cursor-agent acp).

  • Agents live in a shared, updatable AcpAgents registry. A turn selects one with the agent harness option, advertised as a HarnessControlKind::Agent control.
  • A turn runs initialize, session/new (or session/resume, else session/load without its replay), optional mode and model selection, then session/prompt.
  • session/request_permission reaches the interaction handler unless AcceptEdits/FullAccess decide it. Cancellation sends session/cancel; -32000 fails as an authentication error before the prompt.
  • Acp::probe_agent runs the handshake to report capabilities and sign-in state.
  • New TurnEvent::PlanUpdated (PlanEntry/PlanEntryStatus) and AgentAdapter::terminal_exit_grace.

Docs: docs/how-to/run-acp-agents.md, docs/reference/events.md. End-to-end tests in tests/acp_stdio.rs use a scripted POSIX-shell ACP agent.

Why

Lets Temps Fleet run ACP agents through the same runtime as Claude, Codex and OpenCode. Fleet's ACP code sits behind cfg(fleet_acp_runtime) until this merges and its pin moves.

Verification

cargo fmt --check, cargo clippy --locked --all-targets --all-features -- -D warnings and cargo test --all-features --lib (442 passed) are clean locally. CI runs a newer clippy than my local toolchain, so its first run may report lints I could not reproduce.

🤖 Generated with Claude Code

dviejokfs and others added 2 commits October 12, 2026 20:53
Add Provider::Acp and providers::acp::Acp, one adapter for any agent that
speaks ACP over stdio (Gemini CLI, GitHub Copilot, Cursor and the rest of
the ACP registry), selected per turn through the agent harness option.
Also adds TurnEvent::PlanUpdated and AgentAdapter::terminal_exit_grace.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@dviejokfs
dviejokfs marked this pull request as ready for review October 12, 2026 19:14
@greptile-apps

greptile-apps Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 5/5

[High impact] The PR appears safe to merge; the previous stderr-reader finding is fixed and no new blocking issues remain.

Summary

Adds an ACP adapter that runs registered agents over stdio, handles permissions, and streams tool results and plans.

  • ACP agents run through the runtime one turn at a time.
  • Runtime discovery lists the installed ACP agents.
  • ACP plan updates arrive as complete runtime events.
  • Finished turns stop waiting on lingering agents and stderr pipes.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Start agent and stderr reader] --> B[Run turn]
  B --> C[Early error or timeout]
  C --> D[Drop capture and stop reader]
  B --> E[Provider exits]
  E --> F{Preserve descendants?}
  F -->|Yes| G[Disarm process cleanup and detach reader]
  F -->|No| H[Keep reader attached]
  G --> I[Wait briefly for stderr]
  H --> I
  I --> J[Return result]
Loading

Reviews (8) · Last reviewed commit: "fix(runtime): detach a turn's stderr rea..." · Reviewed by Greptile

Comment thread src/providers/acp.rs Outdated
Comment thread src/providers/acp.rs
Comment thread src/providers/acp.rs
Comment thread src/providers/acp.rs
dviejokfs and others added 2 commits October 12, 2026 21:51
…nd probes

- keep the latest streamed tool output so a bare `completed`/`failed`
  update still reports the result or failure details
- refuse a relative working directory (turns and `probe_agent` sessions):
  the agent starts inside it, so ACP's absolute `cwd` would resolve twice
- add `AgentAdapter::interaction_answered` and drop a permission request
  from the open list when its reply is sent, so cancelling afterwards does
  not answer the same request id twice
- bound the probe's wait for stderr after stopping the agent and abort the
  reader, so a descendant outside the process group cannot hang it

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Resolves the CHANGELOG overlap with the session title PR (#40).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread src/providers/acp.rs
Comment thread src/runtime.rs
…a turn

Discovery and readiness treated Provider::Acp as one executable named
`acp`, so registered agents and their Agent control never appeared in the
inventory and readiness(Provider::Acp) reported the placeholder missing.
AgentAdapter::selectable_executables (default None) lets an adapter that
chooses among several executables per turn expose them; the runtime
resolves each inside the configured transport, reports the provider
installed when one resolves, names each agent in the detail and keeps only
installed agents in the Agent control group. discover_harnesses now
schedules Provider::Acp when the adapter is registered.

A finished ACP turn could still time out: after process.wait() the turn
awaited the stderr reader, which a detached descendant holding the pipe
never ends. StderrCapture waits a bounded 2s for end-of-file, then stops
reading and keeps the tail already read. The turn, the extension command,
the metadata probe and Acp::probe_agent use it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread src/runtime.rs
… grace

Bounding the stderr wait of a finished turn must not close the read end of
the pipe: a tool that ToolProcessPolicy::PreserveOnCompletion leaves running
would then get SIGPIPE on its next stderr write. A turn now returns after
the 2s grace with the tail it has and leaves the reader draining in the
background (bounded tail, ends when the last writer closes). Probes, which
have already stopped the process, still stop reading.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread src/process.rs Outdated
StderrCapture::finish took the reader task out of the capture before
waiting, so cancelling the wait (for example dropping Acp::probe_agent
during cleanup) detached the task and Drop could no longer stop it. Keep
the handle in the capture while waiting; only the deliberate detach path
used by finished turns releases it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread src/process.rs
…ancelled

A turn's stderr capture is detached: neither an elapsed grace nor a
cancelled or dropped wait stops the reader, so a tool that
ToolProcessPolicy::PreserveOnCompletion left running never gets a broken
pipe. Probes keep the stop-on-drop default because they have already
stopped the process. This replaces the finish/finish_detached pair with one
finish and a detached() constructor option that also governs Drop.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Comment thread src/runtime.rs Outdated
Detaching at spawn also kept the reader alive when a turn timed out or
returned early on an emit or write error, leaving a task and pipe per
failure while an escaped descendant held stderr. The capture now stays
stop-on-drop until the completion path has decided to preserve
descendants (ToolProcessPolicy::PreserveOnCompletion); only then does it
keep draining past the grace or a cancelled wait.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@dviejokfs
dviejokfs merged commit c998111 into main Oct 12, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant