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