protocol_droid/application/a2a/handler.rs
1//! The A2A request handler trait — the consumer's
2//! extension point for A2A JSON-RPC methods.
3//!
4//! # What this is
5//!
6//! A consumer who runs an A2A server implements
7//! [`A2aRequestHandler`] to define what each A2A
8//! method does. The serving adapter
9//! ([`serve_jsonrpc`](crate::adapters::primary::a2a::jsonrpc_serve::serve_jsonrpc))
10//! decodes the JSON-RPC envelope, looks up the
11//! method name in the [`A2aMethodTable`](crate::adapters::secondary::a2a::A2aMethodTable),
12//! and calls the handler with the matched method,
13//! the raw params, and the per-request context.
14//! The handler returns the raw result (or an
15//! [`A2aError`]); the serving adapter serialises
16//! the result into a JSON-RPC response.
17//!
18//! # Why a single dispatch method
19//!
20//! The A2A v1.0 spec defines 11 methods. A trait
21//! with 11 typed methods is exhaustive (a handler
22//! must implement all 11) and locks the wire
23//! shape of every method at the type level. A
24//! single dispatch method lets a consumer implement
25//! only the methods they care about and return
26//! [`A2aError::MethodNotImplemented`] for the rest,
27//! which is what the 0.3.x → 1.0 migration story
28//! looks like in practice: a server starts with
29//! the two or three methods it needs, and adds
30//! the others as consumers ask.
31//!
32//! The trade-off is no per-method compile-time
33//! type safety on the params. The params arrive
34//! as [`serde_json::Value`]; the consumer is
35//! responsible for decoding them. A future
36//! per-method typed wrapper is a separate
37//! commit, not a replacement for this trait.
38//!
39//! # Why this is object-safe
40//!
41//! The serving adapter is generic over a concrete
42//! handler in the call site, but a consumer who
43//! wants to swap handlers at runtime holds an
44//! `Arc<dyn A2aRequestHandler>`. Object safety
45//! keeps both options open. The trade-off is the
46//! boxed future on the per-call path: the JSON-RPC
47//! dispatch itself is the project's hot path, not
48//! the handler invocation. The
49//! `Pin<Box<dyn Future + Send>>` signature
50//! matches [`RequestHandler`](crate::adapters::primary::transport::http::router::RequestHandler)
51//! in spirit but is a single concrete shape,
52//! not RPITIT, because `async fn` in a trait
53//! does not make the resulting future
54//! object-safe in 1.98.
55
56use std::pin::Pin;
57
58use futures_core::Future;
59
60use crate::adapters::secondary::a2a::A2aMethod;
61use crate::domain::a2a::version::A2aVersion;
62
63/// An A2A-specific error.
64///
65/// The variants are the JSON-RPC error codes the
66/// A2A spec names. The serving adapter maps each
67/// variant to a code and message on the wire; the
68/// consumer never has to know the codes.
69///
70/// # JSON-RPC error codes
71///
72/// - `-32601`: `MethodNotFound`
73/// - `-32602`: `InvalidParams`
74/// - `-32603`: `InternalError`
75/// - `-32000`: `ContentTypeNotSupported`
76/// - `-32001`: `AuthenticationRequired`
77/// - `-32002`: `AuthenticationFailed`
78///
79/// The negative numbers are the JSON-RPC
80/// standard's reserved range for "implementation-
81/// defined server errors"; A2A v1.0 §5.3 fixes
82/// these specific codes. The standard
83/// JSON-RPC error codes (`-32700` `ParseError`,
84/// `-32600` `Invalid Request`, `-32601`
85/// `Method Not Found`, `-32602` `Invalid
86/// Params`, `-32603` `Internal Error`) are
87/// reserved by JSON-RPC itself; the A2A spec
88/// assigns them and extends the
89/// implementation-defined range to `-32000`
90/// through `-32002`.
91#[derive(Debug, Clone, PartialEq, Eq)]
92pub enum A2aError {
93 /// The method name is not in the A2A
94 /// vocabulary. Maps to JSON-RPC `-32601`.
95 MethodNotFound,
96
97 /// The method is known to A2A but the
98 /// consumer's handler does not implement it.
99 /// Maps to JSON-RPC `-32601` (the same code
100 /// as `MethodNotFound`; the distinction is
101 /// for the consumer's logs).
102 MethodNotImplemented {
103 /// The method the consumer did not
104 /// implement.
105 method: A2aMethod,
106 },
107
108 /// The params could not be decoded into the
109 /// shape the handler expects. Maps to JSON-RPC
110 /// `-32602`.
111 InvalidParams {
112 /// Why the params are wrong. The serving
113 /// adapter surfaces this on the wire as
114 /// the JSON-RPC `data` field.
115 reason: String,
116 },
117
118 /// The handler failed in a way that is not
119 /// the caller's fault. Maps to JSON-RPC
120 /// `-32603`.
121 InternalError {
122 /// The error message. The serving adapter
123 /// surfaces this on the wire as the
124 /// JSON-RPC `message` field.
125 message: String,
126 },
127
128 /// The request requires authentication and
129 /// the request did not carry credentials.
130 /// Maps to JSON-RPC `-32001`.
131 AuthenticationRequired,
132
133 /// The request carried credentials and they
134 /// were not accepted. Maps to JSON-RPC
135 /// `-32002`.
136 AuthenticationFailed {
137 /// Why the credentials were rejected.
138 reason: String,
139 },
140
141 /// The request's `Content-Type` is not
142 /// `application/json`. Maps to JSON-RPC
143 /// `-32000`.
144 ContentTypeNotSupported {
145 /// The `Content-Type` the request carried.
146 received: String,
147 },
148}
149
150impl A2aError {
151 /// The JSON-RPC error code for this error.
152 ///
153 /// The codes are the A2A v1.0 spec's
154 /// implementation-defined server-error range.
155 /// The standard JSON-RPC error codes
156 /// (`-32700` `ParseError`, `-32600` `Invalid`
157 /// `Request`, `-32601` `Method` `Not` `Found`,
158 /// `-32602` `Invalid` `Params`, `-32603`
159 /// `Internal` `Error`) are reserved by
160 /// JSON-RPC itself; the A2A spec assigns them
161 /// and extends the implementation-defined
162 /// range to `-32000` through `-32002`.
163 #[must_use]
164 pub const fn code(&self) -> i32 {
165 match self {
166 Self::MethodNotFound | Self::MethodNotImplemented { .. } => -32601,
167 Self::InvalidParams { .. } => -32602,
168 Self::InternalError { .. } => -32603,
169 Self::ContentTypeNotSupported { .. } => -32000,
170 Self::AuthenticationRequired => -32001,
171 Self::AuthenticationFailed { .. } => -32002,
172 }
173 }
174
175 /// The JSON-RPC error message for this error.
176 ///
177 /// A short, fixed string per the A2A spec;
178 /// detailed context goes in the `data` field
179 /// (the serving adapter's responsibility, not
180 /// this one).
181 #[must_use]
182 pub const fn message(&self) -> &'static str {
183 match self {
184 Self::MethodNotFound => "Method not found",
185 Self::MethodNotImplemented { .. } => "Method not implemented",
186 Self::InvalidParams { .. } => "Invalid params",
187 Self::InternalError { .. } => "Internal error",
188 Self::ContentTypeNotSupported { .. } => "Content-Type not supported",
189 Self::AuthenticationRequired => "Authentication required",
190 Self::AuthenticationFailed { .. } => "Authentication failed",
191 }
192 }
193}
194
195/// The per-request context the A2A handler
196/// receives.
197///
198/// Carries the negotiated protocol version (so
199/// the handler can decode per-revision params)
200/// and the auth context (so a handler that needs
201/// to check identity has it without re-parsing
202/// headers). Future fields land here as the
203/// protocol grows: the request id, the request
204/// deadline, the tenant id from the URL.
205#[derive(Debug, Clone)]
206pub struct A2aRequestContext {
207 /// The negotiated A2A protocol version. The
208 /// handler is responsible for decoding params
209 /// against this version's wire shape.
210 pub protocol_version: A2aVersion,
211}
212
213/// The consumer's extension point for A2A methods.
214///
215/// The serving adapter calls [`dispatch`](Self::dispatch)
216/// once per JSON-RPC request. The handler receives
217/// the method (already validated against the
218/// A2A vocabulary) and the raw `serde_json::Value`
219/// params. The handler returns the raw result
220/// value, or an [`A2aError`].
221///
222/// # Object safety
223///
224/// The trait is object-safe. A consumer who wants
225/// to swap handlers at runtime holds an
226/// `Arc<dyn A2aRequestHandler>`; the serving
227/// adapter accepts either an `Arc<dyn>` or a
228/// concrete `H: A2aRequestHandler`.
229///
230/// # Default impls
231///
232/// None of the methods have a default impl. A
233/// consumer who wants to leave a method
234/// unimplemented can return
235/// [`A2aError::MethodNotImplemented`] from
236/// `dispatch`; the serving adapter maps that to
237/// JSON-RPC `-32601` and the caller's client
238/// knows the method is not supported.
239pub trait A2aRequestHandler: Send + Sync + 'static {
240 /// Handles a routed A2A method.
241 ///
242 /// The serving adapter guarantees:
243 /// - `method` was looked up in the A2A
244 /// vocabulary (so it is one of the 11
245 /// methods, not a string);
246 /// - `params` is the raw JSON-RPC params
247 /// object, or `None` if the request had no
248 /// params.
249 ///
250 /// The handler is responsible for:
251 /// - Decoding `params` into the per-method
252 /// type;
253 /// - Running the method's logic;
254 /// - Returning either the raw result value
255 /// (which the serving adapter puts in the
256 /// `result` field) or an [`A2aError`]
257 /// (which becomes the `error` field).
258 ///
259 /// # Errors
260 ///
261 /// Any [`A2aError`]. The serving adapter
262 /// answers with the right JSON-RPC code;
263 /// the handler never has to know the code.
264 ///
265 /// # Example
266 ///
267 /// A minimal handler that implements only
268 /// `SendMessage` and returns
269 /// `A2aError::MethodNotImplemented` for the
270 /// other ten methods. The full reference
271 /// impl is in the `a2a_server` example.
272 ///
273 /// ```no_run
274 /// use std::pin::Pin;
275 /// use std::sync::Arc;
276 ///
277 /// use futures_core::Future;
278 /// use serde_json::{json, Value};
279 ///
280 /// use protocol_droid::adapters::secondary::a2a::A2aMethod;
281 /// use protocol_droid::application::a2a::{
282 /// A2aError, A2aRequestContext, A2aRequestHandler,
283 /// };
284 ///
285 /// struct EchoAgent;
286 ///
287 /// impl A2aRequestHandler for EchoAgent {
288 /// fn dispatch(
289 /// &self,
290 /// method: A2aMethod,
291 /// params: Option<Value>,
292 /// _context: &A2aRequestContext,
293 /// ) -> Pin<
294 /// Box<dyn Future<Output = Result<Value, A2aError>> + Send + '_>,
295 /// > {
296 /// Box::pin(async move {
297 /// match method {
298 /// A2aMethod::SendMessage => {
299 /// // Decode the params, run the
300 /// // agent's logic, return the
301 /// // result as JSON.
302 /// Ok(json!({"id": "t-1", "status":
303 /// {"state": "completed"}}))
304 /// }
305 /// other => Err(A2aError::MethodNotImplemented {
306 /// method: other,
307 /// }),
308 /// }
309 /// })
310 /// }
311 /// }
312 ///
313 /// let _handler: Arc<dyn A2aRequestHandler> = Arc::new(EchoAgent);
314 /// ```
315 fn dispatch(
316 &self,
317 method: A2aMethod,
318 params: Option<serde_json::Value>,
319 context: &A2aRequestContext,
320 ) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, A2aError>> + Send + '_>>;
321}
322
323#[cfg(test)]
324mod tests {
325 use super::A2aError;
326 use crate::adapters::secondary::a2a::A2aMethod;
327
328 #[test]
329 fn method_not_found_is_minus_32601() {
330 assert_eq!(A2aError::MethodNotFound.code(), -32601);
331 }
332
333 #[test]
334 fn method_not_implemented_is_minus_32601() {
335 // Same code as `MethodNotFound`; the
336 // distinction is in the message.
337 let err = A2aError::MethodNotImplemented {
338 method: A2aMethod::SendMessage,
339 };
340 assert_eq!(err.code(), -32601);
341 assert_eq!(err.message(), "Method not implemented");
342 }
343
344 #[test]
345 fn invalid_params_is_minus_32602() {
346 let err = A2aError::InvalidParams {
347 reason: "expected an object".to_owned(),
348 };
349 assert_eq!(err.code(), -32602);
350 }
351
352 #[test]
353 fn internal_error_is_minus_32603() {
354 let err = A2aError::InternalError {
355 message: "database is down".to_owned(),
356 };
357 assert_eq!(err.code(), -32603);
358 }
359
360 #[test]
361 fn content_type_not_supported_is_minus_32000() {
362 let err = A2aError::ContentTypeNotSupported {
363 received: "text/plain".to_owned(),
364 };
365 assert_eq!(err.code(), -32000);
366 }
367
368 #[test]
369 fn authentication_required_is_minus_32001() {
370 assert_eq!(A2aError::AuthenticationRequired.code(), -32001);
371 }
372
373 #[test]
374 fn authentication_failed_is_minus_32002() {
375 let err = A2aError::AuthenticationFailed {
376 reason: "token expired".to_owned(),
377 };
378 assert_eq!(err.code(), -32002);
379 }
380
381 #[test]
382 fn messages_are_stable_strings() {
383 // The spec fixes the message text;
384 // changing any of these is a wire break.
385 assert_eq!(A2aError::MethodNotFound.message(), "Method not found");
386 assert_eq!(
387 A2aError::InternalError {
388 message: "x".to_owned()
389 }
390 .message(),
391 "Internal error"
392 );
393 assert_eq!(
394 A2aError::ContentTypeNotSupported {
395 received: "x".to_owned()
396 }
397 .message(),
398 "Content-Type not supported"
399 );
400 assert_eq!(
401 A2aError::AuthenticationRequired.message(),
402 "Authentication required"
403 );
404 }
405}