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}