Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/agent-client-protocol-rmcp/examples/with_mcp_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,12 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mcp_server = McpServer::from_rmcp("example", ExampleMcpServer::new);

// Set up the proxy connection with our MCP server
// ProxyToConductor already has proxy behavior built into its default_message_handler
Proxy
.builder()
.name("mcp-server-proxy")
// Register the MCP server as a handler
.with_mcp_server(mcp_server)
// Start serving
// Connect the proxy to the conductor over stdio
.connect_to(agent_client_protocol::ByteStreams::new(
tokio::io::stdout().compat_write(),
tokio::io::stdin().compat(),
Expand Down
91 changes: 43 additions & 48 deletions src/agent-client-protocol/src/component.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,16 @@
//!
//! To implement a component, implement the `connect_to` method:
//!
//! ```rust,ignore
//! use agent_client_protocol::{Agent, Client, Connect, Result};
//! ```rust
//! use agent_client_protocol::{Agent, Client, ConnectTo, Result};
//!
//! struct MyAgent {
//! // configuration fields
//! }
//! struct MyAgent;
//!
//! // An agent connects to clients
//! impl ConnectTo<Client> for MyAgent {
//! async fn connect_to(self, client: impl ConnectTo<Agent>) -> Result<()> {
//! Agent.builder()
//! .name("my-agent")
//! // configure handlers here
//! .connect_to(client)
//! .await
//! }
Expand All @@ -40,11 +37,11 @@ use crate::{Channel, Result, role::Role};
/// This trait represents anything that can communicate via JSON-RPC messages over channels -
/// agents, proxies, in-process connections, or any ACP-speaking component.
///
/// The type parameter `R` is the role that this component serves (its counterpart).
/// The type parameter `R` is the role that this component connects to (its counterpart).
/// For example:
/// - An agent implements `Serve<Client>` - it serves clients
/// - A proxy implements `Serve<Conductor>` - it serves conductors
/// - Transports like `Channel` implement `Serve<R>` for all `R` since they're role-agnostic
/// - An agent implements `ConnectTo<Client>` to connect to clients
/// - A proxy implements `ConnectTo<Conductor>` to connect to conductors
/// - Transports like `Channel` implement `ConnectTo<R>` for every `R` because they are role-agnostic
///
/// # Component Types
///
Expand All @@ -55,35 +52,29 @@ use crate::{Channel, Result, role::Role};
/// - **[`AcpAgent`]**: An external agent running in a separate process with stdio communication
/// - **Custom components**: Proxies, transformers, or any ACP-aware service
///
/// # Two Ways to Serve
/// # Two Ways to Connect
///
/// Components can be used in two ways:
///
/// 1. **`serve(client)`** - Serve by forwarding to another component (most components implement this)
/// 2. **`into_server()`** - Convert into a channel endpoint and server future (base cases implement this)
/// 1. **`connect_to(client)`** - Connect directly to another component (most components implement this)
/// 2. **`into_channel_and_future()`** - Obtain a channel endpoint and a future that drives the connection
///
/// Most components only need to implement `serve(client)` - the `into_server()` method has a default
/// implementation that creates an intermediate channel and calls `serve`.
/// Most components only need to implement `connect_to(client)`. The
/// `into_channel_and_future()` method has a default implementation that creates an intermediate
/// channel and calls `connect_to`.
///
/// # Implementation Example
///
/// ```rust,ignore
/// use agent_client_protocol::{Agent, Result, Serve, role::Client};
/// ```rust
/// use agent_client_protocol::{Agent, Client, ConnectTo, Result};
///
/// struct MyAgent {
/// config: AgentConfig,
/// }
/// struct MyAgent;
///
/// impl Serve<Client> for MyAgent {
/// async fn serve(self, client: impl Serve<Client::Counterpart>) -> Result<()> {
/// // Set up connection that forwards to client
/// impl ConnectTo<Client> for MyAgent {
/// async fn connect_to(self, client: impl ConnectTo<Agent>) -> Result<()> {
/// Agent.builder()
/// .name("my-agent")
/// .on_receive_request(async |req: MyRequest, cx| {
/// // Handle request
/// cx.respond(MyResponse { status: "ok".into() })
/// })
/// .serve(client)
/// .connect_to(client)
/// .await
/// }
/// }
Expand All @@ -93,32 +84,34 @@ use crate::{Channel, Result, role::Role};
///
/// For storing different component types in the same collection, use [`DynConnectTo`]:
///
/// ```rust,ignore
/// use agent_client_protocol::Client;
/// ```rust
/// use agent_client_protocol::{Channel, Client, DynConnectTo};
///
/// let (first, _first_peer) = Channel::duplex();
/// let (second, _second_peer) = Channel::duplex();
/// let components: Vec<DynConnectTo<Client>> = vec![
/// DynConnectTo::new(proxy1),
/// DynConnectTo::new(proxy2),
/// DynConnectTo::new(agent),
/// DynConnectTo::new(first),
/// DynConnectTo::new(second),
/// ];
/// assert_eq!(components.len(), 2);
/// ```
///
/// [`ByteStreams`]: crate::ByteStreams
/// [`AcpAgent`]: crate::AcpAgent
/// [`Builder`]: crate::Builder
pub trait ConnectTo<R: Role>: Send + 'static {
/// Serve this component by forwarding to a client component.
/// Connect this component to another component.
///
/// Most components implement this method to set up their connection and
/// forward messages to the provided client.
/// exchange messages with the provided component.
///
/// # Arguments
///
/// * `client` - The component to forward messages to (implements `Serve<R::Counterpart>`)
/// * `client` - The component to connect to (implements `ConnectTo<R::Counterpart>`)
///
/// # Returns
///
/// A future that resolves when the component stops serving, either successfully
/// A future that resolves when the connection ends, either successfully
/// or with an error. The future must be `Send`.
///
/// A component that buffers outbound messages should not return `Ok(())`
Expand All @@ -131,25 +124,25 @@ pub trait ConnectTo<R: Role>: Send + 'static {
client: impl ConnectTo<R::Counterpart>,
) -> impl Future<Output = Result<()>> + Send;

/// Convert this component into a channel endpoint and server future.
/// Convert this component into a channel endpoint and connection future.
///
/// The returned [`Channel`] is the canonical frame-aware boundary. It carries
/// complete [`TransportFrame`](crate::TransportFrame) values so default
/// adapters preserve batch grouping.
///
/// This method returns:
/// - A `Channel` that can be used to communicate with this component
/// - A `BoxFuture` that runs the component's server logic
/// - A `BoxFuture` that drives the component's connection logic
///
/// The default implementation creates an intermediate channel pair and calls `serve`
/// The default implementation creates an intermediate channel pair and calls `connect_to`
/// on one endpoint while returning the other endpoint for the caller to use.
///
/// Base cases like `Channel` and `ByteStreams` override this to avoid unnecessary copying.
///
/// # Returns
///
/// A tuple of `(Channel, BoxFuture)` where the channel is for the caller to use
/// and the future must be spawned to run the server.
/// and the future must be polled to drive the connection.
fn into_channel_and_future(self) -> (Channel, BoxFuture<'static, Result<()>>)
where
Self: Sized,
Expand Down Expand Up @@ -177,7 +170,7 @@ trait ErasedConnectTo<R: Role>: Send {
-> (Channel, BoxFuture<'static, Result<()>>);
}

/// Blanket implementation: any `Serve<R>` can be type-erased.
/// Blanket implementation: any `ConnectTo<R>` can be type-erased.
impl<C: ConnectTo<R>, R: Role> ErasedConnectTo<R> for C {
fn type_name(&self) -> &'static str {
std::any::type_name::<C>()
Expand Down Expand Up @@ -210,18 +203,20 @@ impl<C: ConnectTo<R>, R: Role> ErasedConnectTo<R> for C {
/// allowing you to store different component types in the same collection.
///
/// The type parameter `R` is the role that all components in the
/// collection serve (their counterpart).
/// collection connect to (their counterpart).
///
/// # Examples
///
/// ```rust,ignore
/// use agent_client_protocol::{DynConnectTo, Client};
/// ```rust
/// use agent_client_protocol::{Channel, Client, DynConnectTo};
///
/// let (first, _first_peer) = Channel::duplex();
/// let (second, _second_peer) = Channel::duplex();
/// let components: Vec<DynConnectTo<Client>> = vec![
/// DynConnectTo::new(Proxy1),
/// DynConnectTo::new(Proxy2),
/// DynConnectTo::new(Agent),
/// DynConnectTo::new(first),
/// DynConnectTo::new(second),
/// ];
/// assert_eq!(components.len(), 2);
/// ```
pub struct DynConnectTo<R: Role> {
inner: Box<dyn ErasedConnectTo<R>>,
Expand Down
18 changes: 9 additions & 9 deletions src/agent-client-protocol/src/jsonrpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3975,9 +3975,9 @@ impl Dispatch {
///
/// # Returns
///
/// * `Ok(Ok(typed))` if this is a request/notification of the given types
/// * `Ok(Err(self))` if not
/// * `Err` if has the correct method for the given types but parsing fails
/// * `Ok(Ok(typed))` if this dispatch matches the requested type for its variant
/// * `Ok(Err(self))` if it does not match the requested type for its variant
/// * `Err` if its method matches the requested type but parsing fails
#[tracing::instrument(skip(self), fields(Request = ?std::any::type_name::<Req>(), Notif = ?std::any::type_name::<Notif>()), level = "trace", ret)]
pub(crate) fn into_typed_dispatch<Req: JsonRpcRequest, Notif: JsonRpcNotification>(
self,
Expand Down Expand Up @@ -4089,9 +4089,9 @@ impl Dispatch {
///
/// # Returns
///
/// * `Ok(Ok(typed))` if this is a request/notification of the given types
/// * `Ok(Err(self))` if not
/// * `Err` if has the correct method for the given types but parsing fails
/// * `Ok(Ok(typed))` if this is a notification of the requested type
/// * `Ok(Err(self))` if this is not a matching notification
/// * `Err` if its method matches the requested type but parsing fails
pub fn into_notification<N: JsonRpcNotification>(
self,
) -> Result<Result<N, Dispatch>, crate::Error> {
Expand All @@ -4113,9 +4113,9 @@ impl Dispatch {
///
/// # Returns
///
/// * `Ok(Ok(typed))` if this is a request/notification of the given types
/// * `Ok(Err(self))` if not
/// * `Err` if has the correct method for the given types but parsing fails
/// * `Ok(Ok(typed))` if this is a request of the requested type
/// * `Ok(Err(self))` if this is not a matching request
/// * `Err` if its method matches the requested type but parsing fails
pub fn into_request<Req: JsonRpcRequest>(
self,
) -> Result<Result<(Req, Responder<Req::Response>), Dispatch>, crate::Error> {
Expand Down
4 changes: 2 additions & 2 deletions src/agent-client-protocol/src/role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub mod mcp;
///
/// Each role determines:
/// - Who the counterpart is (via [`Role::Counterpart`])
/// - How unhandled messages are processed (via `Role::default_message_handler`)
/// - How unhandled messages are processed (via [`Role::default_handle_dispatch_from`])
pub trait Role: Debug + Clone + Send + Sync + 'static + Eq + Ord + Hash {
/// The role that this endpoint connects to.
///
Expand Down Expand Up @@ -264,7 +264,7 @@ where
}

/// A dummy role you can use to exchange JSON-RPC messages without any knowledge of the underlying protocol.
/// Don't sue this.
/// Prefer a protocol-specific role when one is available.
#[derive(
Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
)]
Expand Down
2 changes: 1 addition & 1 deletion src/agent-client-protocol/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl SessionBlockState for Blocking {}
pub struct NonBlocking;
impl SessionBlockState for NonBlocking {}

/// Trait for marker types that indicate blocking vs blocking API.
/// Trait for marker types that indicate blocking vs non-blocking API.
/// See [`SessionBuilder::block_task`].
pub trait SessionBlockState: Send + 'static + Sync + std::fmt::Debug {}

Expand Down