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