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