Skip to main content

agent_client_protocol/
role.rs

1//! Role types for ACP connections.
2//!
3//! Roles represent the logical identity of an endpoint in an ACP connection.
4//! Each role has a counterpart (who it connects to) and may have multiple peers
5//! (who it can exchange messages with).
6
7use std::{any::TypeId, fmt::Debug, future::Future, hash::Hash};
8
9use serde::{Deserialize, Serialize};
10
11use crate::schema::{METHOD_SUCCESSOR_MESSAGE, SuccessorMessage};
12use crate::util::json_cast;
13use crate::{Builder, ConnectionTo, Dispatch, Handled, JsonRpcMessage, UntypedMessage};
14
15/// Roles for the ACP protocol.
16pub mod acp;
17
18/// Roles for the MCP protocol.
19pub mod mcp;
20
21/// The role that an endpoint plays in an ACP connection.
22///
23/// Roles are the fundamental building blocks of ACP's type system:
24/// - [`acp::Client`] connects to [`acp::Agent`]
25/// - [`acp::Agent`] connects to [`acp::Client`]
26/// - [`acp::Proxy`] connects to [`acp::Conductor`]
27/// - [`acp::Conductor`] connects to [`acp::Proxy`]
28///
29/// Each role determines:
30/// - Who the counterpart is (via [`Role::Counterpart`])
31/// - How unhandled messages are processed (via `Role::default_message_handler`)
32pub trait Role: Debug + Clone + Send + Sync + 'static + Eq + Ord + Hash {
33    /// The role that this endpoint connects to.
34    ///
35    /// For example:
36    /// - `Client::Counterpart = Agent`
37    /// - `Agent::Counterpart = Client`
38    /// - `Proxy::Counterpart = Conductor`
39    /// - `Conductor::Counterpart = Proxy`
40    type Counterpart: Role<Counterpart = Self>;
41
42    /// Creates a new builder playing this role.
43    fn builder(self) -> Builder<Self>
44    where
45        Self: Sized,
46    {
47        Builder::new(self)
48    }
49
50    /// Returns a unique identifier for this role.
51    fn role_id(&self) -> RoleId;
52
53    /// Method invoked when there is no defined message handler.
54    fn default_handle_dispatch_from(
55        &self,
56        message: Dispatch,
57        connection: ConnectionTo<Self>,
58    ) -> impl Future<Output = Result<Handled<Dispatch>, crate::Error>> + Send;
59
60    /// Returns the counterpart role.
61    fn counterpart(&self) -> Self::Counterpart;
62}
63
64/// Declares that a role can send messages to a specific peer.
65///
66/// Most roles only communicate with their counterpart, but some (like [`acp::Proxy`])
67/// can communicate with multiple peers:
68/// - `Proxy: HasPeer<Client>` - proxy can send/receive from clients
69/// - `Proxy: HasPeer<Agent>` - proxy can send/receive from agents
70/// - `Proxy: HasPeer<Conductor>` - proxy can send/receive from its conductor
71///
72/// The [`RemoteStyle`] determines how messages are transformed:
73/// - [`RemoteStyle::Counterpart`] - pass through unchanged
74/// - [`RemoteStyle::Predecessor`] - pass through, but reject wrapped messages
75/// - [`RemoteStyle::Successor`] - wrap in a [`SuccessorMessage`] envelope
76///
77/// [`SuccessorMessage`]: crate::schema::SuccessorMessage
78pub trait HasPeer<Peer: Role>: Role {
79    /// Returns the remote style for sending to this peer.
80    fn remote_style(&self, peer: Peer) -> RemoteStyle;
81}
82
83/// Describes how messages are transformed when sent to a remote peer.
84#[derive(Clone, Debug, PartialEq, Eq)]
85#[non_exhaustive]
86pub enum RemoteStyle {
87    /// Pass each message through exactly as it is.
88    Counterpart,
89
90    /// Only messages not wrapped in successor.
91    Predecessor,
92
93    /// Wrap messages in a [`SuccessorMessage`] envelope.
94    Successor,
95}
96
97impl RemoteStyle {
98    pub(crate) fn transform_outgoing_message<M: JsonRpcMessage>(
99        &self,
100        msg: M,
101    ) -> Result<UntypedMessage, crate::Error> {
102        match self {
103            RemoteStyle::Counterpart | RemoteStyle::Predecessor => msg.to_untyped_message(),
104            RemoteStyle::Successor => SuccessorMessage {
105                message: msg,
106                meta: None,
107            }
108            .to_untyped_message(),
109        }
110    }
111}
112
113/// Unique identifier for a role instance.
114///
115/// Used to identify the source/destination of messages when multiple
116/// peers are possible on a single connection.
117#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
118#[non_exhaustive]
119pub enum RoleId {
120    /// Singleton role identified by type name and type ID.
121    Singleton(&'static str, TypeId),
122}
123
124impl RoleId {
125    /// Create the role ID for a singleton role type.
126    pub fn from_singleton<R>(_role: &R) -> RoleId
127    where
128        R: Role + Default,
129    {
130        RoleId::Singleton(std::any::type_name::<R>(), TypeId::of::<R>())
131    }
132}
133
134// ============================================================================
135// Role implementations
136// ============================================================================
137
138pub(crate) async fn handle_incoming_dispatch<Counterpart, Peer>(
139    counterpart: Counterpart,
140    peer: Peer,
141    dispatch: Dispatch,
142    connection: ConnectionTo<Counterpart>,
143    handle_dispatch: impl AsyncFnOnce(
144        Dispatch,
145        ConnectionTo<Counterpart>,
146    ) -> Result<Handled<Dispatch>, crate::Error>,
147) -> Result<Handled<Dispatch>, crate::Error>
148where
149    Counterpart: Role + HasPeer<Peer>,
150    Peer: Role,
151{
152    tracing::trace!(
153        method = %dispatch.method(),
154        ?counterpart,
155        ?peer,
156        ?dispatch,
157        "handle_incoming_dispatch: enter"
158    );
159
160    // Responses are different from other messages.
161    //
162    // For normal incoming messages, messages from non-default
163    // peers are tagged with special method names and carry
164    // special payload that have be "unwrapped".
165    //
166    // For responses, the payload is untouched. The response
167    // carries an `id` and we use this `id` to look up information
168    // on the request that was sent to determine which peer it was
169    // directed at (and therefore which peer sent us the response).
170    if let Dispatch::Response(_, router) = &dispatch {
171        tracing::trace!(
172            response_role_id = ?router.role_id(),
173            peer_role_id = ?peer.role_id(),
174            "handle_incoming_dispatch: response"
175        );
176
177        if router.role_id() == peer.role_id() {
178            return handle_dispatch(dispatch, connection).await;
179        }
180        return Ok(Handled::No {
181            message: dispatch,
182            retry: false,
183        });
184    }
185
186    // Handle other messages by looking at the 'remote style'
187    let method = dispatch.method();
188    match counterpart.remote_style(peer) {
189        RemoteStyle::Counterpart => {
190            // "Counterpart" is the default peer, no special checks required.
191            tracing::trace!("handle_incoming_dispatch: Counterpart style, passing through");
192            handle_dispatch(dispatch, connection).await
193        }
194        RemoteStyle::Predecessor => {
195            // "Predecessor" is the default peer, no special checks required.
196            tracing::trace!("handle_incoming_dispatch: Predecessor style, passing through");
197            if method == METHOD_SUCCESSOR_MESSAGE {
198                // Methods coming from the successor are not coming from
199                // our counterpart.
200                Ok(Handled::No {
201                    message: dispatch,
202                    retry: false,
203                })
204            } else {
205                handle_dispatch(dispatch, connection).await
206            }
207        }
208        RemoteStyle::Successor => {
209            // Successor style means we have to look for a special method name.
210            if method != METHOD_SUCCESSOR_MESSAGE {
211                tracing::trace!(
212                    method,
213                    expected = METHOD_SUCCESSOR_MESSAGE,
214                    "handle_incoming_dispatch: Successor style but method doesn't match, returning Handled::No"
215                );
216                return Ok(Handled::No {
217                    message: dispatch,
218                    retry: false,
219                });
220            }
221
222            tracing::trace!(
223                "handle_incoming_dispatch: Successor style, unwrapping SuccessorMessage"
224            );
225
226            // The outer message has method="_proxy/successor" and params containing the inner message.
227            // We need to deserialize the params (not the whole message) to extract the inner UntypedMessage.
228            let untyped_message = dispatch.message().ok_or_else(|| {
229                crate::util::internal_error(
230                    "Response variant cannot be unwrapped as SuccessorMessage",
231                )
232            })?;
233            let SuccessorMessage { message, meta } = json_cast(untyped_message.params())?;
234            let successor_dispatch = dispatch.try_map_message(|_| Ok(message))?;
235            tracing::trace!(
236                unwrapped_method = %successor_dispatch.method(),
237                "handle_incoming_dispatch: unwrapped to inner message"
238            );
239            match handle_dispatch(successor_dispatch, connection).await? {
240                Handled::Yes => {
241                    tracing::trace!(
242                        "handle_incoming_dispatch: inner handler returned Handled::Yes"
243                    );
244                    Ok(Handled::Yes)
245                }
246
247                Handled::No {
248                    message: successor_dispatch,
249                    retry,
250                } => {
251                    tracing::trace!(
252                        "handle_incoming_dispatch: inner handler returned Handled::No, re-wrapping"
253                    );
254                    Ok(Handled::No {
255                        message: successor_dispatch.try_map_message(|message| {
256                            SuccessorMessage { message, meta }.to_untyped_message()
257                        })?,
258                        retry,
259                    })
260                }
261            }
262        }
263    }
264}
265
266/// A dummy role you can use to exchange JSON-RPC messages without any knowledge of the underlying protocol.
267/// Don't sue this.
268#[derive(
269    Copy, Clone, Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
270)]
271pub struct UntypedRole;
272
273impl UntypedRole {
274    /// Creates a new builder for a connection from this role.
275    pub fn builder(self) -> Builder<Self> {
276        Builder::new(self)
277    }
278}
279
280impl Role for UntypedRole {
281    type Counterpart = UntypedRole;
282
283    fn role_id(&self) -> RoleId {
284        RoleId::from_singleton(self)
285    }
286
287    async fn default_handle_dispatch_from(
288        &self,
289        message: Dispatch,
290        _connection: ConnectionTo<Self>,
291    ) -> Result<Handled<Dispatch>, crate::Error> {
292        Ok(Handled::No {
293            message,
294            retry: false,
295        })
296    }
297
298    fn counterpart(&self) -> Self::Counterpart {
299        *self
300    }
301}
302
303impl HasPeer<UntypedRole> for UntypedRole {
304    fn remote_style(&self, _peer: UntypedRole) -> RemoteStyle {
305        RemoteStyle::Counterpart
306    }
307}