1use crate::auth::RequestContext;
9use crate::error::A2aError;
10use crate::server::TaskEventStream;
11use crate::types::{
12 AgentCard, CancelTaskParams, DeletePushNotificationConfigParams, GetExtendedAgentCardParams,
13 GetPushNotificationConfigParams, GetTaskParams, ListPushNotificationConfigsParams,
14 ListPushNotificationConfigsResult, ListTasksParams, ListTasksResult, PushNotificationConfig,
15 PushNotificationConfigParams, SendMessageParams, SendMessageResult, SubscribeToTaskParams,
16 Task,
17};
18use async_trait::async_trait;
19
20#[cfg(any(test, feature = "test-fixtures"))]
21use crate::types::{AgentCapabilities, Message, Role, TaskStatus};
22#[cfg(any(test, feature = "test-fixtures"))]
23use std::collections::HashMap;
24#[cfg(any(test, feature = "test-fixtures"))]
25use std::sync::Mutex;
26#[cfg(any(test, feature = "test-fixtures"))]
27use uuid::Uuid;
28
29#[async_trait]
35pub trait A2aHandler: Send + Sync {
36 async fn send_message(
40 &self,
41 _ctx: &RequestContext,
42 _params: SendMessageParams,
43 ) -> Result<SendMessageResult, A2aError> {
44 Err(A2aError::MethodNotFound("SendMessage".into()))
45 }
46 async fn send_streaming_message(
51 &self,
52 _ctx: &RequestContext,
53 _params: SendMessageParams,
54 ) -> Result<TaskEventStream, A2aError> {
55 Err(A2aError::MethodNotFound("SendStreamingMessage".into()))
56 }
57 async fn get_task(
59 &self,
60 _ctx: &RequestContext,
61 _params: GetTaskParams,
62 ) -> Result<Task, A2aError> {
63 Err(A2aError::MethodNotFound("GetTask".into()))
64 }
65 async fn list_tasks(
67 &self,
68 _ctx: &RequestContext,
69 _params: ListTasksParams,
70 ) -> Result<ListTasksResult, A2aError> {
71 Err(A2aError::MethodNotFound("ListTasks".into()))
72 }
73 async fn cancel_task(
75 &self,
76 _ctx: &RequestContext,
77 _params: CancelTaskParams,
78 ) -> Result<Task, A2aError> {
79 Err(A2aError::MethodNotFound("CancelTask".into()))
80 }
81 async fn subscribe_to_task(
85 &self,
86 _ctx: &RequestContext,
87 _params: SubscribeToTaskParams,
88 ) -> Result<TaskEventStream, A2aError> {
89 Err(A2aError::MethodNotFound("SubscribeToTask".into()))
90 }
91 async fn create_task_push_notification_config(
93 &self,
94 _ctx: &RequestContext,
95 _params: PushNotificationConfigParams,
96 ) -> Result<PushNotificationConfig, A2aError> {
97 Err(A2aError::MethodNotFound(
98 "CreateTaskPushNotificationConfig".into(),
99 ))
100 }
101 async fn get_task_push_notification_config(
103 &self,
104 _ctx: &RequestContext,
105 _params: GetPushNotificationConfigParams,
106 ) -> Result<PushNotificationConfig, A2aError> {
107 Err(A2aError::MethodNotFound(
108 "GetTaskPushNotificationConfig".into(),
109 ))
110 }
111 async fn list_task_push_notification_configs(
113 &self,
114 _ctx: &RequestContext,
115 _params: ListPushNotificationConfigsParams,
116 ) -> Result<ListPushNotificationConfigsResult, A2aError> {
117 Err(A2aError::MethodNotFound(
118 "ListTaskPushNotificationConfigs".into(),
119 ))
120 }
121 async fn delete_task_push_notification_config(
123 &self,
124 _ctx: &RequestContext,
125 _params: DeletePushNotificationConfigParams,
126 ) -> Result<(), A2aError> {
127 Err(A2aError::MethodNotFound(
128 "DeleteTaskPushNotificationConfig".into(),
129 ))
130 }
131 async fn get_extended_agent_card(
133 &self,
134 _ctx: &RequestContext,
135 _params: GetExtendedAgentCardParams,
136 ) -> Result<AgentCard, A2aError> {
137 Err(A2aError::MethodNotFound("GetExtendedAgentCard".into()))
138 }
139
140 async fn get_agent_card(&self, _ctx: &RequestContext) -> Result<AgentCard, A2aError> {
153 Err(A2aError::MethodNotFound("GetAgentCard".into()))
154 }
155
156 fn is_idempotent(&self) -> bool {
170 false
171 }
172}
173
174#[cfg(any(test, feature = "test-fixtures"))]
190#[derive(Default)]
191pub struct EchoHandler {
192 tasks: Mutex<HashMap<String, Task>>,
193}
194
195#[cfg(any(test, feature = "test-fixtures"))]
196#[async_trait]
197impl A2aHandler for EchoHandler {
198 async fn send_message(
199 &self,
200 _ctx: &RequestContext,
201 params: SendMessageParams,
202 ) -> Result<SendMessageResult, A2aError> {
203 let task_id = format!("task-{}", Uuid::new_v4());
204 let context_id = params
205 .message
206 .contextId
207 .clone()
208 .unwrap_or_else(|| format!("ctx-{}", Uuid::new_v4()));
209 let response = Message {
210 messageId: format!("msg-{}", Uuid::new_v4()),
211 contextId: Some(context_id.clone()),
212 taskId: Some(task_id.clone()),
213 role: Role::Agent,
214 parts: params.message.parts.clone(),
215 metadata: None,
216 extensions: vec![],
217 referenceTaskIds: vec![],
218 };
219 let task = Task {
220 id: task_id.clone(),
221 contextId: context_id,
222 status: TaskStatus::Completed,
223 artifacts: vec![],
224 history: vec![params.message, response],
225 metadata: None,
226 };
227 self.tasks
228 .lock()
229 .map_err(|_| A2aError::Server("mutex poisoned".into()))?
230 .insert(task_id.clone(), task.clone());
231 Ok(SendMessageResult::Task(task))
232 }
233
234 async fn get_task(
235 &self,
236 _ctx: &RequestContext,
237 params: GetTaskParams,
238 ) -> Result<Task, A2aError> {
239 self.tasks
240 .lock()
241 .map_err(|_| A2aError::Server("mutex poisoned".into()))?
242 .get(¶ms.id)
243 .cloned()
244 .ok_or(A2aError::TaskNotFound(params.id))
245 }
246
247 async fn list_tasks(
248 &self,
249 _ctx: &RequestContext,
250 params: ListTasksParams,
251 ) -> Result<ListTasksResult, A2aError> {
252 let guard = self
253 .tasks
254 .lock()
255 .map_err(|_| A2aError::Server("mutex poisoned".into()))?;
256 let tasks: Vec<Task> = guard
257 .values()
258 .filter(|t| match ¶ms.contextId {
259 Some(ctx) => &t.contextId == ctx,
260 None => true,
261 })
262 .cloned()
263 .collect();
264 Ok(ListTasksResult {
265 tasks,
266 nextCursor: None,
267 })
268 }
269
270 async fn cancel_task(
271 &self,
272 _ctx: &RequestContext,
273 params: CancelTaskParams,
274 ) -> Result<Task, A2aError> {
275 let mut guard = self
276 .tasks
277 .lock()
278 .map_err(|_| A2aError::Server("mutex poisoned".into()))?;
279 let task = guard
280 .get_mut(¶ms.id)
281 .ok_or_else(|| A2aError::TaskNotFound(params.id.clone()))?;
282 task.status = TaskStatus::Canceled;
283 Ok(task.clone())
284 }
285
286 async fn get_extended_agent_card(
287 &self,
288 _ctx: &RequestContext,
289 _params: GetExtendedAgentCardParams,
290 ) -> Result<AgentCard, A2aError> {
291 Ok(Self::static_card())
292 }
293
294 async fn get_agent_card(&self, _ctx: &RequestContext) -> Result<AgentCard, A2aError> {
295 Ok(Self::static_card())
296 }
297}
298
299#[cfg(any(test, feature = "test-fixtures"))]
300impl EchoHandler {
301 fn static_card() -> AgentCard {
306 AgentCard {
307 id: "echo".into(),
308 name: "Echo Agent".into(),
309 description: Some("Echoes incoming messages as completed tasks.".into()),
310 provider: None,
311 capabilities: AgentCapabilities {
312 streaming: false,
313 pushNotifications: false,
314 stateTransitionHistory: false,
315 },
316 skills: vec![],
317 interfaces: vec![],
318 securitySchemes: serde_json::Map::new(),
319 security: vec![],
320 extensions: vec![],
321 signature: None,
322 }
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::envelope::A2aHeaders;
330 use crate::types::{
331 CancelTaskParams, GetExtendedAgentCardParams, GetTaskParams, Message, Part, Role,
332 SendMessageParams, TaskStatus,
333 };
334
335 struct AlwaysDefault;
336 #[async_trait::async_trait]
337 impl A2aHandler for AlwaysDefault {}
338
339 fn anon_ctx() -> RequestContext {
340 RequestContext::new(
341 A2aHeaders::decode_from(&klieo_core::Headers::new()),
342 Some(klieo_auth_common::Identity::anonymous()),
343 )
344 }
345
346 #[tokio::test]
347 async fn default_handler_returns_method_not_found() {
348 let h = AlwaysDefault;
349 let err = h
350 .send_message(
351 &anon_ctx(),
352 SendMessageParams {
353 message: Message {
354 messageId: "m".into(),
355 contextId: None,
356 taskId: None,
357 role: Role::User,
358 parts: vec![],
359 metadata: None,
360 extensions: vec![],
361 referenceTaskIds: vec![],
362 },
363 configuration: None,
364 },
365 )
366 .await
367 .unwrap_err();
368 assert!(matches!(err, A2aError::MethodNotFound(_)));
369 }
370
371 #[tokio::test]
372 async fn echo_handler_send_message_returns_completed_task() {
373 let h = EchoHandler::default();
374 let msg = Message {
375 messageId: "m-1".into(),
376 contextId: Some("c-1".into()),
377 taskId: None,
378 role: Role::User,
379 parts: vec![Part::Text {
380 content: "hi".into(),
381 metadata: None,
382 mediaType: None,
383 filename: None,
384 }],
385 metadata: None,
386 extensions: vec![],
387 referenceTaskIds: vec![],
388 };
389 let result = h
390 .send_message(
391 &anon_ctx(),
392 SendMessageParams {
393 message: msg,
394 configuration: None,
395 },
396 )
397 .await
398 .unwrap();
399 match result {
400 crate::types::SendMessageResult::Task(t) => {
401 assert!(matches!(t.status, TaskStatus::Completed));
402 assert_eq!(t.history.len(), 2);
403 }
404 _ => panic!("expected Task"),
405 }
406 }
407
408 #[tokio::test]
409 async fn echo_handler_get_task_recovers_stored_task() {
410 let h = EchoHandler::default();
411 let msg = Message {
412 messageId: "m-2".into(),
413 contextId: Some("c-2".into()),
414 taskId: None,
415 role: Role::User,
416 parts: vec![],
417 metadata: None,
418 extensions: vec![],
419 referenceTaskIds: vec![],
420 };
421 let result = h
422 .send_message(
423 &anon_ctx(),
424 SendMessageParams {
425 message: msg,
426 configuration: None,
427 },
428 )
429 .await
430 .unwrap();
431 let task_id = match result {
432 crate::types::SendMessageResult::Task(t) => t.id,
433 _ => panic!(),
434 };
435 let recovered = h
436 .get_task(
437 &anon_ctx(),
438 GetTaskParams {
439 id: task_id.clone(),
440 historyLength: None,
441 },
442 )
443 .await
444 .unwrap();
445 assert_eq!(recovered.id, task_id);
446 }
447
448 #[tokio::test]
449 async fn echo_handler_cancel_task_marks_canceled() {
450 let h = EchoHandler::default();
451 let msg = Message {
452 messageId: "m-3".into(),
453 contextId: Some("c-3".into()),
454 taskId: None,
455 role: Role::User,
456 parts: vec![],
457 metadata: None,
458 extensions: vec![],
459 referenceTaskIds: vec![],
460 };
461 let task_id = match h
462 .send_message(
463 &anon_ctx(),
464 SendMessageParams {
465 message: msg,
466 configuration: None,
467 },
468 )
469 .await
470 .unwrap()
471 {
472 crate::types::SendMessageResult::Task(t) => t.id,
473 _ => panic!(),
474 };
475 let cancelled = h
476 .cancel_task(&anon_ctx(), CancelTaskParams { id: task_id })
477 .await
478 .unwrap();
479 assert!(matches!(cancelled.status, TaskStatus::Canceled));
480 }
481
482 #[test]
483 fn echo_handler_default_is_not_idempotent() {
484 let h = EchoHandler::default();
485 assert!(!h.is_idempotent());
486 }
487
488 #[test]
489 fn always_default_handler_is_not_idempotent() {
490 let h = AlwaysDefault;
491 assert!(!h.is_idempotent());
492 }
493
494 #[tokio::test]
495 async fn echo_handler_extended_agent_card_returns_static() {
496 let h = EchoHandler::default();
497 let card = h
498 .get_extended_agent_card(&anon_ctx(), GetExtendedAgentCardParams::default())
499 .await
500 .unwrap();
501 assert_eq!(card.id, "echo");
502 }
503
504 #[tokio::test]
505 async fn echo_handler_get_agent_card_returns_static() {
506 let h = EchoHandler::default();
507 let card = h.get_agent_card(&anon_ctx()).await.unwrap();
508 assert_eq!(card.id, "echo");
509 }
510
511 #[tokio::test]
512 async fn default_handler_get_agent_card_returns_method_not_found() {
513 let h = AlwaysDefault;
514 let err = h.get_agent_card(&anon_ctx()).await.unwrap_err();
515 assert!(matches!(err, A2aError::MethodNotFound(_)));
516 }
517}