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}