aion_integration_acp/lib.rs
1//! The first-party **Agent Client Protocol** (ACP) adapter for Aion.
2//!
3//! This crate implements the `aion-integrations` [`AgentHarness`] / [`AgentSession`] seam against
4//! any agent that speaks [ACP](https://agentclientprotocol.com) over its stdio. Where
5//! `aion-integration-norn` adapts one specific harness and `aion-integration-cli` adapts the
6//! observability-only tier, this one adapts a **protocol**: a single implementation reaches every
7//! agent that has an ACP mode — Claude Code, Codex, Gemini CLI, `OpenCode`, and whatever ships next.
8//!
9//! # The invariant this crate exists to preserve
10//!
11//! The aion platform crates (`aion-core`, `aion-worker`, `aion-server`, …) stay ACP-blind:
12//! everything ACP-specific — the method namespace, the `session/update` shapes, the permission
13//! handshake — is confined to [`protocol`], [`translate`], and [`messages`] here, behind the neutral
14//! trait. Nothing above this adapter references an ACP type.
15//!
16//! # Shape
17//!
18//! - [`AcpHarness`] implements [`AgentHarness`]: [`AgentHarness::start`] spawns the configured agent
19//! command with piped stdio (stderr inherited), performs the `initialize` handshake gating on ACP
20//! protocol version `1`, creates a session with `session/new`, issues the one long-running
21//! `session/prompt` request carrying the spec's input as a `text` content block, and returns an
22//! [`AcpSession`].
23//! - [`AcpSession`] implements [`AgentSession`]: [`AgentSession::events`] streams translated
24//! [`aion_core::ActivityEvent`]s and **ends the moment the turn is answered**,
25//! [`AgentSession::capabilities`] returns the negotiated set, [`AgentSession::intervene`] maps a
26//! neutral command onto real ACP (capability-gated), and [`AgentSession::wait_result`] interprets
27//! the id-matched `session/prompt` Response as the terminal [`aion_core::Payload`] and then shuts
28//! the agent process down.
29//! - [`protocol`] names the wire contract, [`translate`] is the pure translation between it and the
30//! neutral types, [`messages`] reassembles the streamed message text, and [`permission`] is the
31//! required policy for answering `session/request_permission`.
32//!
33//! # Four rulings worth knowing before you use it
34//!
35//! 1. **`Cancel` is the only intervention primitive advertised.** It is the only one ACP v1 can
36//! actually deliver; the other four have no protocol mechanism, and advertising a control that
37//! silently does nothing would be worse than advertising none. A live session gates on
38//! [`translate::negotiated_capabilities`]; a composition root advertising this adapter at worker
39//! REGISTRATION — before any agent process exists — publishes the same ceiling through
40//! [`registration_capabilities`], which is documented with why the two coincide for ACP and
41//! pinned against what [`translate::intervention_to_notification`] can actually build.
42//! 2. **The permission policy is required, with no default.** An unanswered
43//! `session/request_permission` blocks the agent until the activity times out, and an Aion
44//! activity is unattended, so the answer is decided when the harness is built. See
45//! [`permission`].
46//! 3. **No filesystem or terminal callbacks are offered.** Those exist for editors with unsaved
47//! buffers and embedded panels; a worker has neither, and the agent has direct disk access. An
48//! inbound `fs/*` or `terminal/*` request is still ANSWERED (with `-32601`) and recorded, because
49//! an unanswered peer request wedges the run. See [`protocol`].
50//! 4. **The event stream ending is the end-of-run signal, and it does not wait for the process.**
51//! A real ACP agent is a persistent stdio server: it answers `session/prompt` and keeps running.
52//! The platform's agent driver takes the terminal result only once the event stream yields
53//! `None`, so the adapter closes the transcript at the answer rather than at the child's exit.
54//! See [`pump`].
55//!
56//! # Two limitations to read before you rely on this
57//!
58//! - **The session working directory is per-HARNESS, not per-attempt.** Two agent activities running
59//! concurrently through one [`AcpHarness`] share a workspace root and can overwrite each other's
60//! files. Per-attempt workspace provisioning is a separate design; see [`harness`] for the full
61//! statement and why nothing is invented here.
62//! - **A completed turn may legitimately carry no text.** An agent that ends `end_turn` having
63//! emitted only tool calls yields a SUCCESSFUL result whose `text` is `""`. That is honest, not a
64//! bug: the agent finished and said nothing, and its work is on the transcript as tool events. See
65//! [`outcome`].
66//!
67//! # Wire types
68//!
69//! The ACP request/response/notification types come from the published
70//! [`agent-client-protocol-schema`] crate, pinned
71//! exactly. That is deliberate: a hand-written mirror of a moving external protocol is a fossil
72//! waiting to happen, and a protocol bump should be a compile error rather than a silent wire
73//! mismatch. Only the schema is taken — not the full `agent-client-protocol` SDK, which carries its
74//! own async stack and would drag a second runtime into this tokio codebase.
75//!
76//! [`AgentHarness`]: aion_integrations::AgentHarness
77//! [`AgentSession`]: aion_integrations::AgentSession
78//! [`AgentHarness::start`]: aion_integrations::AgentHarness::start
79//! [`AgentSession::capabilities`]: aion_integrations::AgentSession::capabilities
80//! [`AgentSession::events`]: aion_integrations::AgentSession::events
81//! [`AgentSession::intervene`]: aion_integrations::AgentSession::intervene
82//! [`AgentSession::wait_result`]: aion_integrations::AgentSession::wait_result
83//! [`agent-client-protocol-schema`]: https://docs.rs/agent-client-protocol-schema
84//!
85//! [`AcpHarness`]: crate::harness::AcpHarness
86//! [`AcpSession`]: crate::session::AcpSession
87
88/// The agents this build knows how to launch, as data — id, launch line, install hint.
89pub mod catalogue;
90/// Process-group containment for a spawned agent, so a session takes its whole tree with it.
91pub mod containment;
92/// The ACP adapter implementing [`aion_integrations::AgentHarness`].
93pub mod harness;
94/// The MCP servers an agent is handed when a session opens.
95pub mod mcp;
96/// Reassembly of ACP's per-chunk message stream into durable messages and the run's final text.
97pub mod messages;
98/// Interpretation of the terminal `session/prompt` Response as the activity's result.
99pub mod outcome;
100/// The required policy for answering `session/request_permission`.
101pub mod permission;
102/// ACP's on-wire contract — the ONLY module naming its method strings and protocol version.
103pub mod protocol;
104/// The reader pump that handles every inbound frame for a live session.
105pub mod pump;
106/// The live ACP session implementing [`aion_integrations::AgentSession`].
107pub mod session;
108/// The pure translation between ACP's wire shapes and the neutral `aion-core` types.
109pub mod translate;
110
111// Protocol tests driving a real `JsonRpcConnection` against a scripted fake ACP agent over an
112// in-memory duplex. In-crate rather than under `tests/` because they exercise the adapter's own
113// transport-independent seams (`AcpHarness::connect`, `AcpSession::start`) instead of forcing those
114// internals into the published API just so a test could reach them.
115#[cfg(test)]
116mod wire_tests;
117
118pub use catalogue::{CATALOGUE, CatalogueHarness};
119pub use containment::ProcessGroup;
120pub use harness::{AcpHarness, registration_capabilities};
121pub use mcp::McpServerSpec;
122pub use outcome::PromptOutcome;
123pub use permission::PermissionPolicy;
124pub use session::{AcpConfigValue, AcpSession, TurnHandle};