Skip to main content

agent_client_protocol/
component.rs

1//! ConnectTo abstraction for agents and proxies.
2//!
3//! This module provides the [`ConnectTo`] trait that defines the interface for things
4//! that can be run as part of a conductor's chain - agents, proxies, or any ACP-speaking component.
5//!
6//! ## Usage
7//!
8//! Components connect to other components, creating a chain of message processors.
9//! The type parameter `R` is the role that this component connects to (its counterpart).
10//!
11//! To implement a component, implement the `connect_to` method:
12//!
13//! ```rust
14//! use agent_client_protocol::{Agent, Client, ConnectTo, Result};
15//!
16//! struct MyAgent;
17//!
18//! // An agent connects to clients
19//! impl ConnectTo<Client> for MyAgent {
20//!     async fn connect_to(self, client: impl ConnectTo<Agent>) -> Result<()> {
21//!         Agent.builder()
22//!             .name("my-agent")
23//!             .connect_to(client)
24//!             .await
25//!     }
26//! }
27//! ```
28
29use futures::future::BoxFuture;
30use std::{fmt::Debug, future::Future, marker::PhantomData};
31
32use crate::{Channel, Result, role::Role};
33
34/// A component that can exchange JSON-RPC messages to an endpoint playing the role `R`
35/// (e.g., an ACP [`Agent`](`crate::role::acp::Agent`) or an MCP [`Server`](`crate::role::mcp::Server`)).
36///
37/// This trait represents anything that can communicate via JSON-RPC messages over channels -
38/// agents, proxies, in-process connections, or any ACP-speaking component.
39///
40/// The type parameter `R` is the role that this component connects to (its counterpart).
41/// For example:
42/// - An agent implements `ConnectTo<Client>` to connect to clients
43/// - A proxy implements `ConnectTo<Conductor>` to connect to conductors
44/// - Transports like `Channel` implement `ConnectTo<R>` for every `R` because they are role-agnostic
45///
46/// # Component Types
47///
48/// The trait is implemented by several built-in types representing different communication patterns:
49///
50/// - **[`Lines`]**: A component communicating over asynchronous line streams
51/// - **[`ByteStreams`]**: A component communicating over byte streams (stdin/stdout, sockets, etc.)
52/// - **[`Channel`]**: A component communicating via in-process message channels (for testing or direct connections)
53/// - **Custom components**: Proxies, transformers, or any ACP-aware service
54#[cfg_attr(
55    not(target_family = "wasm"),
56    doc = "- **[`AcpAgent`]**: An external agent running in a separate process with stdio communication"
57)]
58///
59/// # Two Ways to Connect
60///
61/// Components can be used in two ways:
62///
63/// 1. **`connect_to(client)`** - Connect directly to another component (most components implement this)
64/// 2. **`into_channel_and_future()`** - Obtain a channel endpoint and a future that drives the connection
65///
66/// Most components only need to implement `connect_to(client)`. The
67/// `into_channel_and_future()` method has a default implementation that creates an intermediate
68/// channel and calls `connect_to`.
69///
70/// # Implementation Example
71///
72/// ```rust
73/// use agent_client_protocol::{Agent, Client, ConnectTo, Result};
74///
75/// struct MyAgent;
76///
77/// impl ConnectTo<Client> for MyAgent {
78///     async fn connect_to(self, client: impl ConnectTo<Agent>) -> Result<()> {
79///         Agent.builder()
80///             .name("my-agent")
81///             .connect_to(client)
82///             .await
83///     }
84/// }
85/// ```
86///
87/// # Heterogeneous Collections
88///
89/// For storing different component types in the same collection, use [`DynConnectTo`]:
90///
91/// ```rust
92/// use agent_client_protocol::{Channel, Client, DynConnectTo};
93///
94/// let (first, _first_peer) = Channel::duplex();
95/// let (second, _second_peer) = Channel::duplex();
96/// let components: Vec<DynConnectTo<Client>> = vec![
97///     DynConnectTo::new(first),
98///     DynConnectTo::new(second),
99/// ];
100/// assert_eq!(components.len(), 2);
101/// ```
102///
103/// [`ByteStreams`]: crate::ByteStreams
104/// [`Lines`]: crate::Lines
105/// [`Builder`]: crate::Builder
106#[cfg_attr(not(target_family = "wasm"), doc = "[`AcpAgent`]: crate::AcpAgent")]
107pub trait ConnectTo<R: Role>: Send + 'static {
108    /// Connect this component to another component.
109    ///
110    /// Most components implement this method to set up their connection and
111    /// exchange messages with the provided component.
112    ///
113    /// # Arguments
114    ///
115    /// * `client` - The component to connect to (implements `ConnectTo<R::Counterpart>`)
116    ///
117    /// # Returns
118    ///
119    /// A future that resolves when the connection ends, either successfully
120    /// or with an error. The future must be `Send`.
121    ///
122    /// A component that buffers outbound messages should not return `Ok(())`
123    /// merely because its client completed: it should first finish messages the
124    /// client already transferred to it. This lets wrappers preserve graceful
125    /// drain guarantees through to the physical transport sink. Errors may
126    /// still terminate the connection immediately.
127    fn connect_to(
128        self,
129        client: impl ConnectTo<R::Counterpart>,
130    ) -> impl Future<Output = Result<()>> + Send;
131
132    /// Convert this component into a channel endpoint and connection future.
133    ///
134    /// The returned [`Channel`] is the canonical frame-aware boundary. It carries
135    /// complete [`TransportFrame`](crate::TransportFrame) values so default
136    /// adapters preserve batch grouping.
137    ///
138    /// This method returns:
139    /// - A `Channel` that can be used to communicate with this component
140    /// - A `BoxFuture` that drives the component's connection logic
141    ///
142    /// The default implementation creates an intermediate channel pair and calls `connect_to`
143    /// on one endpoint while returning the other endpoint for the caller to use.
144    ///
145    /// Base cases like `Channel` and `ByteStreams` override this to avoid unnecessary copying.
146    ///
147    /// # Returns
148    ///
149    /// A tuple of `(Channel, BoxFuture)` where the channel is for the caller to use
150    /// and the future must be polled to drive the connection.
151    fn into_channel_and_future(self) -> (Channel, BoxFuture<'static, Result<()>>)
152    where
153        Self: Sized,
154    {
155        let (channel_a, channel_b) = Channel::duplex();
156        let future = Box::pin(self.connect_to(channel_b));
157        (channel_a, future)
158    }
159}
160
161/// Type-erased connect trait for object-safe dynamic dispatch.
162///
163/// This trait is internal and used by [`DynConnectTo`]. Users should implement
164/// [`ConnectTo`] instead, which is automatically converted to `ErasedConnectTo`
165/// via a blanket implementation.
166trait ErasedConnectTo<R: Role>: Send {
167    fn type_name(&self) -> &'static str;
168
169    fn connect_to_erased(
170        self: Box<Self>,
171        client: Box<dyn ErasedConnectTo<R::Counterpart>>,
172    ) -> BoxFuture<'static, Result<()>>;
173
174    fn into_channel_and_future_erased(self: Box<Self>)
175    -> (Channel, BoxFuture<'static, Result<()>>);
176}
177
178/// Blanket implementation: any `ConnectTo<R>` can be type-erased.
179impl<C: ConnectTo<R>, R: Role> ErasedConnectTo<R> for C {
180    fn type_name(&self) -> &'static str {
181        std::any::type_name::<C>()
182    }
183
184    fn connect_to_erased(
185        self: Box<Self>,
186        client: Box<dyn ErasedConnectTo<R::Counterpart>>,
187    ) -> BoxFuture<'static, Result<()>> {
188        Box::pin(async move {
189            (*self)
190                .connect_to(DynConnectTo {
191                    inner: client,
192                    _marker: PhantomData,
193                })
194                .await
195        })
196    }
197
198    fn into_channel_and_future_erased(
199        self: Box<Self>,
200    ) -> (Channel, BoxFuture<'static, Result<()>>) {
201        (*self).into_channel_and_future()
202    }
203}
204
205/// A dynamically-typed component for heterogeneous collections.
206///
207/// This type wraps any [`ConnectTo`] implementation and provides dynamic dispatch,
208/// allowing you to store different component types in the same collection.
209///
210/// The type parameter `R` is the role that all components in the
211/// collection connect to (their counterpart).
212///
213/// # Examples
214///
215/// ```rust
216/// use agent_client_protocol::{Channel, Client, DynConnectTo};
217///
218/// let (first, _first_peer) = Channel::duplex();
219/// let (second, _second_peer) = Channel::duplex();
220/// let components: Vec<DynConnectTo<Client>> = vec![
221///     DynConnectTo::new(first),
222///     DynConnectTo::new(second),
223/// ];
224/// assert_eq!(components.len(), 2);
225/// ```
226pub struct DynConnectTo<R: Role> {
227    inner: Box<dyn ErasedConnectTo<R>>,
228    _marker: PhantomData<R>,
229}
230
231impl<R: Role> DynConnectTo<R> {
232    /// Create a new `DynConnectTo` from any type implementing [`ConnectTo`].
233    pub fn new<C: ConnectTo<R>>(component: C) -> Self {
234        Self {
235            inner: Box::new(component),
236            _marker: PhantomData,
237        }
238    }
239
240    /// Returns the type name of the wrapped component.
241    #[must_use]
242    pub fn type_name(&self) -> &'static str {
243        self.inner.type_name()
244    }
245}
246
247impl<R: Role> ConnectTo<R> for DynConnectTo<R> {
248    async fn connect_to(self, client: impl ConnectTo<R::Counterpart>) -> Result<()> {
249        self.inner
250            .connect_to_erased(Box::new(client) as Box<dyn ErasedConnectTo<R::Counterpart>>)
251            .await
252    }
253
254    fn into_channel_and_future(self) -> (Channel, BoxFuture<'static, Result<()>>) {
255        self.inner.into_channel_and_future_erased()
256    }
257}
258
259impl<R: Role> Debug for DynConnectTo<R> {
260    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
261        f.debug_struct("DynConnectTo")
262            .field("type_name", &self.type_name())
263            .finish()
264    }
265}
266
267#[cfg(test)]
268mod tests {
269    use super::*;
270    use crate::role::UntypedRole;
271
272    #[test]
273    fn dyn_connect_to_reports_static_type_name_and_correct_debug_label() {
274        let (channel, _other) = Channel::duplex();
275        let component = DynConnectTo::<UntypedRole>::new(channel);
276
277        let type_name: &'static str = component.type_name();
278        assert_eq!(type_name, std::any::type_name::<Channel>());
279        assert_eq!(
280            format!("{component:?}"),
281            format!("DynConnectTo {{ type_name: {type_name:?} }}")
282        );
283    }
284}