1use a2a::*;
4use async_trait::async_trait;
5use futures::{StreamExt, stream::BoxStream};
6use std::{collections::HashMap, sync::Arc};
7use tokio::sync::{RwLock, broadcast};
8
9use crate::middleware::ServiceParams;
10
11const EXECUTION_BUFFER_CAPACITY: usize = 32;
12
13#[derive(Clone)]
14struct ExecutionEvent {
15 sequence: u64,
16 result: Result<StreamResponse, A2AError>,
17}
18
19#[derive(Default)]
20struct ActiveExecutionState {
21 sequence: u64,
22 snapshot_task: Option<Task>,
23}
24
25struct ActiveExecution {
26 sender: broadcast::Sender<ExecutionEvent>,
27 state: RwLock<ActiveExecutionState>,
28}
29
30impl ActiveExecution {
31 fn new(task: Task) -> Arc<Self> {
32 let (sender, _) = broadcast::channel(EXECUTION_BUFFER_CAPACITY);
33 Arc::new(Self {
34 sender,
35 state: RwLock::new(ActiveExecutionState {
36 sequence: 0,
37 snapshot_task: Some(task),
38 }),
39 })
40 }
41
42 fn subscribe(&self) -> broadcast::Receiver<ExecutionEvent> {
43 self.sender.subscribe()
44 }
45
46 async fn resubscribe(&self) -> (broadcast::Receiver<ExecutionEvent>, Option<Task>, u64) {
47 let receiver = self.sender.subscribe();
48 let state = self.state.read().await;
49 (receiver, state.snapshot_task.clone(), state.sequence)
50 }
51
52 async fn publish(&self, result: Result<StreamResponse, A2AError>, snapshot_task: Option<Task>) {
53 let event = {
54 let mut state = self.state.write().await;
55 if let Some(task) = snapshot_task {
56 state.snapshot_task = Some(task);
57 }
58 state.sequence += 1;
59 ExecutionEvent {
60 sequence: state.sequence,
61 result,
62 }
63 };
64 let _ = self.sender.send(event);
65 }
66
67 async fn snapshot_is_terminal(&self) -> bool {
68 let state = self.state.read().await;
69 state
70 .snapshot_task
71 .as_ref()
72 .is_some_and(|task| task.status.state.is_terminal())
73 }
74}
75
76#[derive(Default)]
77struct ExecutionManager {
78 executions: RwLock<HashMap<TaskId, Arc<ActiveExecution>>>,
79}
80
81impl ExecutionManager {
82 async fn start(&self, task: Task) -> Result<Arc<ActiveExecution>, A2AError> {
83 let mut executions = self.executions.write().await;
84 if executions.contains_key(&task.id) {
85 return Err(A2AError::invalid_request(format!(
86 "task execution is already in progress: {}",
87 task.id
88 )));
89 }
90
91 let active = ActiveExecution::new(task.clone());
92 executions.insert(task.id.clone(), active.clone());
93 Ok(active)
94 }
95
96 async fn get(&self, task_id: &str) -> Option<Arc<ActiveExecution>> {
97 let executions = self.executions.read().await;
98 executions.get(task_id).cloned()
99 }
100
101 async fn resubscribe(
102 &self,
103 task_id: &str,
104 ) -> Option<(broadcast::Receiver<ExecutionEvent>, Option<Task>, u64)> {
105 let active = self.get(task_id).await?;
106 let (receiver, snapshot_task, sequence) = active.resubscribe().await;
107 Some((receiver, snapshot_task, sequence))
108 }
109
110 async fn finish(&self, task_id: &str, active: &Arc<ActiveExecution>) {
111 let mut executions = self.executions.write().await;
112 let should_remove = executions
113 .get(task_id)
114 .is_some_and(|current| Arc::ptr_eq(current, active));
115 if should_remove {
116 executions.remove(task_id);
117 }
118 }
119}
120
121struct ExecutionRuntime {
122 executor: Arc<dyn crate::AgentExecutor>,
123 task_store: Arc<dyn crate::TaskStore>,
124 push_config_store: Option<Arc<dyn crate::PushConfigStore>>,
125 push_sender: Option<Arc<crate::HttpPushSender>>,
126 execution_manager: Arc<ExecutionManager>,
127}
128
129struct SubscriptionState {
130 receiver: broadcast::Receiver<ExecutionEvent>,
131 snapshot_task: Option<Task>,
132 min_sequence: u64,
133 done: bool,
134}
135
136fn is_terminal_event(event: &StreamResponse) -> bool {
137 match event {
138 StreamResponse::Task(task) => task.status.state.is_terminal(),
139 StreamResponse::StatusUpdate(update) => update.status.state.is_terminal(),
140 _ => false,
141 }
142}
143
144fn task_id_for_event(event: &StreamResponse) -> Option<&str> {
145 match event {
146 StreamResponse::Task(task) => Some(&task.id),
147 StreamResponse::StatusUpdate(update) => Some(&update.task_id),
148 StreamResponse::ArtifactUpdate(update) => Some(&update.task_id),
149 StreamResponse::Message(_) => None,
150 }
151}
152
153fn should_interrupt_non_streaming(
154 req: &SendMessageRequest,
155 event: &StreamResponse,
156) -> Option<TaskId> {
157 if req
158 .configuration
159 .as_ref()
160 .and_then(|config| config.return_immediately)
161 .unwrap_or(false)
162 {
163 return match event {
164 StreamResponse::Message(_) => None,
165 _ => task_id_for_event(event).map(ToOwned::to_owned),
166 };
167 }
168
169 match event {
170 StreamResponse::Task(task) if task.status.state == TaskState::AuthRequired => {
171 Some(task.id.clone())
172 }
173 StreamResponse::StatusUpdate(update) if update.status.state == TaskState::AuthRequired => {
174 Some(update.task_id.clone())
175 }
176 _ => None,
177 }
178}
179
180async fn send_push_notifications(
181 task_id: &str,
182 push_config_store: Option<&dyn crate::PushConfigStore>,
183 push_sender: Option<&crate::HttpPushSender>,
184 event: &StreamResponse,
185) -> Result<(), A2AError> {
186 let Some(push_config_store) = push_config_store else {
187 return Ok(());
188 };
189 let Some(push_sender) = push_sender else {
190 return Ok(());
191 };
192
193 let configs = push_config_store.list(task_id).await?;
194 for config in configs {
195 push_sender.send_push(&config, event.clone()).await?;
196 }
197
198 Ok(())
199}
200
201async fn save_task(task_store: &dyn crate::TaskStore, task: Task) -> Result<Task, A2AError> {
202 match task_store.update(task.clone()).await {
203 Ok(_) => Ok(task),
204 Err(error) if error.code == error_code::TASK_NOT_FOUND => {
205 task_store.create(task.clone()).await?;
206 Ok(task)
207 }
208 Err(error) => Err(error),
209 }
210}
211
212async fn apply_event_to_task(
213 task_store: &dyn crate::TaskStore,
214 current_task: Option<Task>,
215 event: &StreamResponse,
216) -> Result<Option<Task>, A2AError> {
217 match event {
218 StreamResponse::Task(task) => save_task(task_store, task.clone()).await.map(Some),
219 StreamResponse::StatusUpdate(update) => {
220 let mut task = current_task
221 .or(task_store.get(&update.task_id).await?)
222 .ok_or_else(|| A2AError::task_not_found(&update.task_id))?;
223 task.status = update.status.clone();
224 save_task(task_store, task).await.map(Some)
225 }
226 StreamResponse::ArtifactUpdate(update) => {
227 let mut task = current_task
228 .or(task_store.get(&update.task_id).await?)
229 .ok_or_else(|| A2AError::task_not_found(&update.task_id))?;
230 let artifacts = task.artifacts.get_or_insert_with(Vec::new);
231 artifacts.push(update.artifact.clone());
232 save_task(task_store, task).await.map(Some)
233 }
234 StreamResponse::Message(_) => Ok(current_task),
235 }
236}
237
238fn subscription_stream(
239 receiver: broadcast::Receiver<ExecutionEvent>,
240 snapshot_task: Option<Task>,
241 min_sequence: u64,
242) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
243 Box::pin(futures::stream::unfold(
244 SubscriptionState {
245 receiver,
246 snapshot_task,
247 min_sequence,
248 done: false,
249 },
250 |mut state| async move {
251 if state.done {
252 return None;
253 }
254
255 if let Some(task) = state.snapshot_task.take() {
256 if task.status.state.is_terminal() {
257 state.done = true;
258 }
259 return Some((Ok(StreamResponse::Task(task)), state));
260 }
261
262 loop {
263 match state.receiver.recv().await {
264 Ok(event) if event.sequence <= state.min_sequence => continue,
265 Ok(event) => {
266 state.min_sequence = event.sequence;
267 match &event.result {
268 Ok(item) if is_terminal_event(item) => state.done = true,
269 Err(_) => state.done = true,
270 _ => {}
271 }
272 return Some((event.result.clone(), state));
273 }
274 Err(broadcast::error::RecvError::Lagged(_)) => {
275 state.done = true;
276 return Some((
277 Err(A2AError::internal(
278 "subscription fell behind active execution",
279 )),
280 state,
281 ));
282 }
283 Err(broadcast::error::RecvError::Closed) => return None,
284 }
285 }
286 },
287 ))
288}
289
290async fn drive_execution(
291 runtime: ExecutionRuntime,
292 task_id: TaskId,
293 active: Arc<ActiveExecution>,
294 exec_ctx: crate::ExecutorContext,
295) {
296 let mut current_task = {
297 let state = active.state.read().await;
298 state.snapshot_task.clone()
299 };
300 let mut stream = runtime.executor.execute(exec_ctx);
301
302 while let Some(result) = stream.next().await {
303 if active.snapshot_is_terminal().await {
304 break;
305 }
306
307 match result {
308 Ok(event) => {
309 match apply_event_to_task(runtime.task_store.as_ref(), current_task.clone(), &event)
310 .await
311 {
312 Ok(updated_task) => {
313 current_task = updated_task.clone();
314 if let Err(error) = send_push_notifications(
315 &task_id,
316 runtime.push_config_store.as_deref(),
317 runtime.push_sender.as_deref(),
318 &event,
319 )
320 .await
321 {
322 active.publish(Err(error), None).await;
323 break;
324 }
325 active.publish(Ok(event.clone()), updated_task).await;
326 if is_terminal_event(&event) {
327 break;
328 }
329 }
330 Err(error) => {
331 active.publish(Err(error), None).await;
332 break;
333 }
334 }
335 }
336 Err(error) => {
337 active.publish(Err(error), None).await;
338 break;
339 }
340 }
341 }
342
343 runtime.execution_manager.finish(&task_id, &active).await;
344}
345
346#[async_trait]
353pub trait RequestHandler: Send + Sync + 'static {
354 async fn send_message(
355 &self,
356 params: &ServiceParams,
357 req: SendMessageRequest,
358 ) -> Result<SendMessageResponse, A2AError>;
359
360 async fn send_streaming_message(
361 &self,
362 params: &ServiceParams,
363 req: SendMessageRequest,
364 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError>;
365
366 async fn get_task(&self, params: &ServiceParams, req: GetTaskRequest)
367 -> Result<Task, A2AError>;
368
369 async fn list_tasks(
370 &self,
371 params: &ServiceParams,
372 req: ListTasksRequest,
373 ) -> Result<ListTasksResponse, A2AError>;
374
375 async fn cancel_task(
376 &self,
377 params: &ServiceParams,
378 req: CancelTaskRequest,
379 ) -> Result<Task, A2AError>;
380
381 async fn subscribe_to_task(
382 &self,
383 params: &ServiceParams,
384 req: SubscribeToTaskRequest,
385 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError>;
386
387 async fn create_push_config(
388 &self,
389 params: &ServiceParams,
390 req: CreateTaskPushNotificationConfigRequest,
391 ) -> Result<TaskPushNotificationConfig, A2AError>;
392
393 async fn get_push_config(
394 &self,
395 params: &ServiceParams,
396 req: GetTaskPushNotificationConfigRequest,
397 ) -> Result<TaskPushNotificationConfig, A2AError>;
398
399 async fn list_push_configs(
400 &self,
401 params: &ServiceParams,
402 req: ListTaskPushNotificationConfigsRequest,
403 ) -> Result<ListTaskPushNotificationConfigsResponse, A2AError>;
404
405 async fn delete_push_config(
406 &self,
407 params: &ServiceParams,
408 req: DeleteTaskPushNotificationConfigRequest,
409 ) -> Result<(), A2AError>;
410
411 async fn get_extended_agent_card(
412 &self,
413 params: &ServiceParams,
414 req: GetExtendedAgentCardRequest,
415 ) -> Result<AgentCard, A2AError>;
416}
417
418pub struct DefaultRequestHandler {
421 executor: Arc<dyn crate::AgentExecutor>,
422 task_store: Arc<dyn crate::TaskStore>,
423 execution_manager: Arc<ExecutionManager>,
424 push_config_store: Option<Arc<dyn crate::PushConfigStore>>,
425 push_sender: Option<Arc<crate::HttpPushSender>>,
426 capabilities: AgentCapabilities,
427}
428
429impl DefaultRequestHandler {
430 pub fn new(executor: impl crate::AgentExecutor, task_store: impl crate::TaskStore) -> Self {
431 DefaultRequestHandler {
432 executor: Arc::new(executor),
433 task_store: Arc::new(task_store),
434 execution_manager: Arc::new(ExecutionManager::default()),
435 push_config_store: None,
436 push_sender: None,
437 capabilities: AgentCapabilities::default(),
438 }
439 }
440
441 pub fn with_push_config_store(
442 mut self,
443 push_config_store: impl crate::PushConfigStore,
444 ) -> Self {
445 self.push_sender = Some(Arc::new(crate::HttpPushSender::new(None)));
446 self.push_config_store = Some(Arc::new(push_config_store));
447 self.capabilities.push_notifications = Some(true);
448 self
449 }
450
451 pub fn with_push_notifications(
452 mut self,
453 push_config_store: impl crate::PushConfigStore,
454 push_sender: crate::HttpPushSender,
455 ) -> Self {
456 self.push_config_store = Some(Arc::new(push_config_store));
457 self.push_sender = Some(Arc::new(push_sender));
458 self.capabilities.push_notifications = Some(true);
459 self
460 }
461
462 pub fn with_capabilities(mut self, capabilities: AgentCapabilities) -> Self {
463 self.capabilities = capabilities;
464 self
465 }
466
467 fn push_config_store(&self) -> Result<&dyn crate::PushConfigStore, A2AError> {
468 self.push_config_store
469 .as_deref()
470 .ok_or_else(A2AError::push_notification_not_supported)
471 }
472
473 async fn load_task(&self, task_id: &str) -> Result<Task, A2AError> {
474 self.task_store
475 .get(task_id)
476 .await?
477 .ok_or_else(|| A2AError::task_not_found(task_id))
478 }
479
480 async fn prepare_task_for_execution(
481 &self,
482 req: &SendMessageRequest,
483 ) -> Result<(Task, Option<Task>, String), A2AError> {
484 let task_id = req.message.task_id.clone().unwrap_or_else(new_task_id);
485 let stored = self.task_store.get(&task_id).await?;
486 let context_id = stored
487 .as_ref()
488 .map(|task| task.context_id.clone())
489 .or_else(|| req.message.context_id.clone())
490 .unwrap_or_else(new_context_id);
491
492 let task = if let Some(existing) = stored.clone() {
493 existing
494 } else {
495 let task = Task {
496 id: task_id,
497 context_id: context_id.clone(),
498 status: TaskStatus {
499 state: TaskState::Submitted,
500 message: None,
501 timestamp: Some(chrono::Utc::now()),
502 },
503 artifacts: None,
504 history: Some(vec![req.message.clone()]),
505 metadata: None,
506 };
507 self.task_store.create(task.clone()).await?;
508 task
509 };
510
511 Ok((task, stored, context_id))
512 }
513
514 async fn save_request_push_config(
515 &self,
516 task_id: &str,
517 req: &SendMessageRequest,
518 ) -> Result<(), A2AError> {
519 let Some(config) = req
520 .configuration
521 .as_ref()
522 .and_then(|configuration| configuration.push_notification_config.clone())
523 else {
524 return Ok(());
525 };
526
527 self.push_config_store()?.save(task_id, config).await?;
528 Ok(())
529 }
530
531 async fn start_execution(
532 &self,
533 params: &ServiceParams,
534 req: SendMessageRequest,
535 include_task_snapshot_in_context: bool,
536 ) -> Result<(TaskId, BoxStream<'static, Result<StreamResponse, A2AError>>), A2AError> {
537 let (task, stored_task, context_id) = self.prepare_task_for_execution(&req).await?;
538 let task_id = task.id.clone();
539 self.save_request_push_config(&task_id, &req).await?;
540 let active = self.execution_manager.start(task.clone()).await?;
541 let receiver = active.subscribe();
542
543 let exec_ctx = crate::ExecutorContext {
544 message: Some(req.message),
545 task_id: task_id.clone(),
546 stored_task: if include_task_snapshot_in_context {
547 Some(task.clone())
548 } else {
549 stored_task
550 },
551 context_id,
552 metadata: req.metadata,
553 user: None,
554 service_params: params.clone(),
555 tenant: req.tenant,
556 };
557
558 let runtime = ExecutionRuntime {
559 executor: Arc::clone(&self.executor),
560 task_store: Arc::clone(&self.task_store),
561 push_config_store: self.push_config_store.clone(),
562 push_sender: self.push_sender.clone(),
563 execution_manager: Arc::clone(&self.execution_manager),
564 };
565 let active_execution = active.clone();
566 let execution_task_id = task_id.clone();
567
568 tokio::spawn(async move {
569 drive_execution(runtime, execution_task_id, active_execution, exec_ctx).await;
570 });
571
572 Ok((task_id, subscription_stream(receiver, None, 0)))
573 }
574}
575
576#[async_trait]
577impl RequestHandler for DefaultRequestHandler {
578 async fn send_message(
579 &self,
580 params: &ServiceParams,
581 req: SendMessageRequest,
582 ) -> Result<SendMessageResponse, A2AError> {
583 let (task_id, mut stream) = self.start_execution(params, req.clone(), true).await?;
584 let mut last_event = None;
585
586 while let Some(item) = stream.next().await {
587 let event = item?;
588
589 if let Some(interrupt_task_id) = should_interrupt_non_streaming(&req, &event) {
590 return Ok(SendMessageResponse::Task(
591 self.load_task(&interrupt_task_id).await?,
592 ));
593 }
594
595 match event {
596 StreamResponse::Message(message) => {
597 return Ok(SendMessageResponse::Message(message));
598 }
599 other => last_event = Some(other),
600 }
601 }
602
603 match last_event {
604 Some(StreamResponse::Task(task)) => Ok(SendMessageResponse::Task(task)),
605 Some(StreamResponse::StatusUpdate(update)) => Ok(SendMessageResponse::Task(
606 self.load_task(&update.task_id).await?,
607 )),
608 Some(StreamResponse::ArtifactUpdate(update)) => Ok(SendMessageResponse::Task(
609 self.load_task(&update.task_id).await?,
610 )),
611 Some(StreamResponse::Message(message)) => Ok(SendMessageResponse::Message(message)),
612 None => Ok(SendMessageResponse::Task(self.load_task(&task_id).await?)),
613 }
614 }
615
616 async fn send_streaming_message(
617 &self,
618 params: &ServiceParams,
619 req: SendMessageRequest,
620 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
621 let (_, stream) = self.start_execution(params, req, false).await?;
622 Ok(stream)
623 }
624
625 async fn get_task(
626 &self,
627 _params: &ServiceParams,
628 req: GetTaskRequest,
629 ) -> Result<Task, A2AError> {
630 self.task_store
631 .get(&req.id)
632 .await?
633 .ok_or_else(|| A2AError::task_not_found(&req.id))
634 }
635
636 async fn list_tasks(
637 &self,
638 _params: &ServiceParams,
639 req: ListTasksRequest,
640 ) -> Result<ListTasksResponse, A2AError> {
641 self.task_store.list(&req).await
642 }
643
644 async fn cancel_task(
645 &self,
646 params: &ServiceParams,
647 req: CancelTaskRequest,
648 ) -> Result<Task, A2AError> {
649 let task = self.load_task(&req.id).await?;
650
651 if task.status.state.is_terminal() {
652 return Err(A2AError::task_not_cancelable(&req.id));
653 }
654
655 let active_execution = self.execution_manager.get(&req.id).await;
656
657 let exec_ctx = crate::ExecutorContext {
658 message: None,
659 task_id: req.id.clone(),
660 stored_task: Some(task.clone()),
661 context_id: task.context_id.clone(),
662 metadata: req.metadata,
663 user: None,
664 service_params: params.clone(),
665 tenant: req.tenant,
666 };
667
668 let mut stream = self.executor.cancel(exec_ctx);
669 let mut current_task = Some(task);
670
671 while let Some(event) = stream.next().await {
672 match event {
673 Ok(event) => {
674 let updated_task =
675 apply_event_to_task(self.task_store.as_ref(), current_task.clone(), &event)
676 .await?;
677 current_task = updated_task.clone();
678
679 send_push_notifications(
680 &req.id,
681 self.push_config_store.as_deref(),
682 self.push_sender.as_deref(),
683 &event,
684 )
685 .await?;
686
687 if let Some(active) = &active_execution {
688 active.publish(Ok(event.clone()), updated_task).await;
689 }
690
691 if is_terminal_event(&event) {
692 break;
693 }
694 }
695 Err(error) => {
696 if let Some(active) = &active_execution {
697 active.publish(Err(error.clone()), None).await;
698 }
699 return Err(error);
700 }
701 }
702 }
703
704 if let Some(active) = active_execution {
705 self.execution_manager.finish(&req.id, &active).await;
706 }
707
708 self.load_task(&req.id).await
709 }
710
711 async fn subscribe_to_task(
712 &self,
713 _params: &ServiceParams,
714 req: SubscribeToTaskRequest,
715 ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
716 let (receiver, snapshot_task, sequence) = self
717 .execution_manager
718 .resubscribe(&req.id)
719 .await
720 .ok_or_else(|| A2AError::task_not_found(&req.id))?;
721 Ok(subscription_stream(receiver, snapshot_task, sequence))
722 }
723
724 async fn create_push_config(
725 &self,
726 _params: &ServiceParams,
727 req: CreateTaskPushNotificationConfigRequest,
728 ) -> Result<TaskPushNotificationConfig, A2AError> {
729 let saved = self
730 .push_config_store()?
731 .save(&req.task_id, req.config)
732 .await?;
733 Ok(TaskPushNotificationConfig {
734 task_id: req.task_id,
735 config: saved,
736 tenant: req.tenant,
737 })
738 }
739
740 async fn get_push_config(
741 &self,
742 _params: &ServiceParams,
743 req: GetTaskPushNotificationConfigRequest,
744 ) -> Result<TaskPushNotificationConfig, A2AError> {
745 let config = self.push_config_store()?.get(&req.task_id, &req.id).await?;
746 Ok(TaskPushNotificationConfig {
747 task_id: req.task_id,
748 config,
749 tenant: req.tenant,
750 })
751 }
752
753 async fn list_push_configs(
754 &self,
755 _params: &ServiceParams,
756 req: ListTaskPushNotificationConfigsRequest,
757 ) -> Result<ListTaskPushNotificationConfigsResponse, A2AError> {
758 let mut configs = self.push_config_store()?.list(&req.task_id).await?;
759 configs.sort_by(|left, right| left.id.cmp(&right.id));
760
761 let page_size = match req.page_size {
762 Some(size) if size > 0 => size as usize,
763 _ => 50,
764 };
765 let start = req
766 .page_token
767 .as_deref()
768 .and_then(|token| token.parse::<usize>().ok())
769 .unwrap_or(0)
770 .min(configs.len());
771 let end = (start + page_size).min(configs.len());
772 let next_page_token = (end < configs.len()).then(|| end.to_string());
773
774 Ok(ListTaskPushNotificationConfigsResponse {
775 configs: configs[start..end]
776 .iter()
777 .cloned()
778 .map(|config| TaskPushNotificationConfig {
779 task_id: req.task_id.clone(),
780 config,
781 tenant: req.tenant.clone(),
782 })
783 .collect(),
784 next_page_token,
785 })
786 }
787
788 async fn delete_push_config(
789 &self,
790 _params: &ServiceParams,
791 req: DeleteTaskPushNotificationConfigRequest,
792 ) -> Result<(), A2AError> {
793 self.push_config_store()?
794 .delete(&req.task_id, &req.id)
795 .await
796 }
797
798 async fn get_extended_agent_card(
799 &self,
800 _params: &ServiceParams,
801 _req: GetExtendedAgentCardRequest,
802 ) -> Result<AgentCard, A2AError> {
803 Err(A2AError::unsupported_operation(
804 "extended agent card not configured",
805 ))
806 }
807}
808
809#[cfg(test)]
810mod tests {
811 use super::*;
812 use crate::executor::ExecutorContext;
813 use crate::push::InMemoryPushConfigStore;
814 use crate::task_store::InMemoryTaskStore;
815 use axum::{
816 Router,
817 body::Bytes,
818 extract::State,
819 http::{HeaderMap, StatusCode, header},
820 routing::post,
821 };
822 use futures::stream;
823 use std::sync::Arc;
824 use tokio::{
825 net::TcpListener,
826 sync::{Notify, mpsc, oneshot},
827 time::{Duration, timeout},
828 };
829
830 struct EchoExecutor;
831
832 struct PushEventExecutor;
833
834 struct ResumableExecutor {
835 release: Arc<Notify>,
836 }
837
838 #[derive(Debug)]
839 struct CapturedPush {
840 authorization: Option<String>,
841 notification_token: Option<String>,
842 event: StreamResponse,
843 }
844
845 impl crate::AgentExecutor for EchoExecutor {
846 fn execute(
847 &self,
848 ctx: ExecutorContext,
849 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
850 let task = Task {
851 id: ctx.task_id.clone(),
852 context_id: ctx.context_id.clone(),
853 status: TaskStatus {
854 state: TaskState::Completed,
855 message: ctx.message,
856 timestamp: None,
857 },
858 artifacts: None,
859 history: None,
860 metadata: None,
861 };
862 Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
863 }
864
865 fn cancel(
866 &self,
867 ctx: ExecutorContext,
868 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
869 let task = Task {
870 id: ctx.task_id.clone(),
871 context_id: ctx.context_id.clone(),
872 status: TaskStatus {
873 state: TaskState::Canceled,
874 message: None,
875 timestamp: None,
876 },
877 artifacts: None,
878 history: None,
879 metadata: None,
880 };
881 Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
882 }
883 }
884
885 impl crate::AgentExecutor for PushEventExecutor {
886 fn execute(
887 &self,
888 ctx: ExecutorContext,
889 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
890 let working = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
891 task_id: ctx.task_id.clone(),
892 context_id: ctx.context_id.clone(),
893 status: TaskStatus {
894 state: TaskState::Working,
895 message: None,
896 timestamp: None,
897 },
898 metadata: None,
899 });
900 let completed = StreamResponse::Task(Task {
901 id: ctx.task_id.clone(),
902 context_id: ctx.context_id.clone(),
903 status: TaskStatus {
904 state: TaskState::Completed,
905 message: ctx.message,
906 timestamp: None,
907 },
908 artifacts: None,
909 history: ctx.stored_task.and_then(|task| task.history),
910 metadata: None,
911 });
912
913 Box::pin(stream::iter(vec![Ok(working), Ok(completed)]))
914 }
915
916 fn cancel(
917 &self,
918 ctx: ExecutorContext,
919 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
920 let task = Task {
921 id: ctx.task_id.clone(),
922 context_id: ctx.context_id.clone(),
923 status: TaskStatus {
924 state: TaskState::Canceled,
925 message: None,
926 timestamp: None,
927 },
928 artifacts: None,
929 history: None,
930 metadata: None,
931 };
932 Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
933 }
934 }
935
936 impl crate::AgentExecutor for ResumableExecutor {
937 fn execute(
938 &self,
939 ctx: ExecutorContext,
940 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
941 let release = self.release.clone();
942 let working_task = Task {
943 id: ctx.task_id.clone(),
944 context_id: ctx.context_id.clone(),
945 status: TaskStatus {
946 state: TaskState::Working,
947 message: None,
948 timestamp: None,
949 },
950 artifacts: None,
951 history: ctx.stored_task.and_then(|task| task.history),
952 metadata: None,
953 };
954 let completed_task = Task {
955 id: working_task.id.clone(),
956 context_id: working_task.context_id.clone(),
957 status: TaskStatus {
958 state: TaskState::Completed,
959 message: None,
960 timestamp: None,
961 },
962 artifacts: None,
963 history: working_task.history.clone(),
964 metadata: None,
965 };
966
967 Box::pin(stream::unfold(
968 (Some(working_task), Some((release, completed_task))),
969 |(working_task, completion)| async move {
970 if let Some(task) = working_task {
971 return Some((Ok(StreamResponse::Task(task)), (None, completion)));
972 }
973
974 if let Some((release, task)) = completion {
975 release.notified().await;
976 return Some((Ok(StreamResponse::Task(task)), (None, None)));
977 }
978
979 None
980 },
981 ))
982 }
983
984 fn cancel(
985 &self,
986 ctx: ExecutorContext,
987 ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
988 let task = Task {
989 id: ctx.task_id.clone(),
990 context_id: ctx.context_id.clone(),
991 status: TaskStatus {
992 state: TaskState::Canceled,
993 message: None,
994 timestamp: None,
995 },
996 artifacts: None,
997 history: None,
998 metadata: None,
999 };
1000 Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
1001 }
1002 }
1003
1004 fn make_handler() -> DefaultRequestHandler {
1005 DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
1006 }
1007
1008 fn make_handler_with_push_configs() -> DefaultRequestHandler {
1009 DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
1010 .with_push_config_store(InMemoryPushConfigStore::new())
1011 }
1012
1013 fn make_push_delivery_handler() -> DefaultRequestHandler {
1014 DefaultRequestHandler::new(PushEventExecutor, InMemoryTaskStore::new())
1015 .with_push_config_store(InMemoryPushConfigStore::new())
1016 }
1017
1018 fn make_resumable_handler() -> (DefaultRequestHandler, Arc<Notify>) {
1019 let release = Arc::new(Notify::new());
1020 (
1021 DefaultRequestHandler::new(
1022 ResumableExecutor {
1023 release: release.clone(),
1024 },
1025 InMemoryTaskStore::new(),
1026 ),
1027 release,
1028 )
1029 }
1030
1031 fn make_message() -> Message {
1032 Message::new(Role::User, vec![Part::text("hello")])
1033 }
1034
1035 async fn capture_push(
1036 State(sender): State<mpsc::UnboundedSender<CapturedPush>>,
1037 headers: HeaderMap,
1038 body: Bytes,
1039 ) -> StatusCode {
1040 let captured = CapturedPush {
1041 authorization: headers
1042 .get(header::AUTHORIZATION)
1043 .and_then(|value| value.to_str().ok())
1044 .map(ToOwned::to_owned),
1045 notification_token: headers
1046 .get("A2A-Notification-Token")
1047 .and_then(|value| value.to_str().ok())
1048 .map(ToOwned::to_owned),
1049 event: serde_json::from_slice(&body).unwrap(),
1050 };
1051 sender.send(captured).unwrap();
1052 StatusCode::ACCEPTED
1053 }
1054
1055 async fn start_push_webhook() -> (
1056 String,
1057 mpsc::UnboundedReceiver<CapturedPush>,
1058 oneshot::Sender<()>,
1059 tokio::task::JoinHandle<()>,
1060 ) {
1061 let (sender, receiver) = mpsc::unbounded_channel();
1062 let app = Router::new()
1063 .route("/", post(capture_push))
1064 .with_state(sender);
1065 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1066 let address = listener.local_addr().unwrap();
1067 let (shutdown_tx, shutdown_rx) = oneshot::channel();
1068 let server = tokio::spawn(async move {
1069 axum::serve(listener, app)
1070 .with_graceful_shutdown(async {
1071 let _ = shutdown_rx.await;
1072 })
1073 .await
1074 .unwrap();
1075 });
1076
1077 (format!("http://{address}/"), receiver, shutdown_tx, server)
1078 }
1079
1080 async fn next_push(receiver: &mut mpsc::UnboundedReceiver<CapturedPush>) -> CapturedPush {
1081 timeout(Duration::from_secs(5), receiver.recv())
1082 .await
1083 .unwrap()
1084 .unwrap()
1085 }
1086
1087 #[tokio::test]
1088 async fn test_send_message_creates_task() {
1089 let handler = make_handler();
1090 let params = ServiceParams::new();
1091 let req = SendMessageRequest {
1092 message: make_message(),
1093 configuration: None,
1094 metadata: None,
1095 tenant: None,
1096 };
1097 let result = handler.send_message(¶ms, req).await.unwrap();
1098 match result {
1099 SendMessageResponse::Task(task) => {
1100 assert_eq!(task.status.state, TaskState::Completed);
1101 }
1102 _ => panic!("expected Task response"),
1103 }
1104 }
1105
1106 #[tokio::test]
1107 async fn test_send_streaming_message() {
1108 use futures::StreamExt;
1109 let handler = make_handler();
1110 let params = ServiceParams::new();
1111 let req = SendMessageRequest {
1112 message: make_message(),
1113 configuration: None,
1114 metadata: None,
1115 tenant: None,
1116 };
1117 let mut stream = handler.send_streaming_message(¶ms, req).await.unwrap();
1118 let event = stream.next().await.unwrap().unwrap();
1119 assert!(matches!(event, StreamResponse::Task(_)));
1120 }
1121
1122 #[tokio::test]
1123 async fn test_get_task_not_found() {
1124 let handler = make_handler();
1125 let params = ServiceParams::new();
1126 let req = GetTaskRequest {
1127 id: "nonexistent".into(),
1128 history_length: None,
1129 tenant: None,
1130 };
1131 let result = handler.get_task(¶ms, req).await;
1132 assert!(result.is_err());
1133 }
1134
1135 #[tokio::test]
1136 async fn test_get_task_after_send() {
1137 let handler = make_handler();
1138 let params = ServiceParams::new();
1139 let mut msg = make_message();
1140 msg.task_id = Some("t1".into());
1141 msg.context_id = Some("c1".into());
1142 let req = SendMessageRequest {
1143 message: msg,
1144 configuration: None,
1145 metadata: None,
1146 tenant: None,
1147 };
1148 handler.send_message(¶ms, req).await.unwrap();
1149 let task = handler
1150 .get_task(
1151 ¶ms,
1152 GetTaskRequest {
1153 id: "t1".into(),
1154 history_length: None,
1155 tenant: None,
1156 },
1157 )
1158 .await
1159 .unwrap();
1160 assert_eq!(task.id, "t1");
1161 }
1162
1163 #[tokio::test]
1164 async fn test_list_tasks() {
1165 let handler = make_handler();
1166 let params = ServiceParams::new();
1167 let req = ListTasksRequest {
1168 context_id: None,
1169 status: None,
1170 page_size: None,
1171 page_token: None,
1172 history_length: None,
1173 status_timestamp_after: None,
1174 include_artifacts: None,
1175 tenant: None,
1176 };
1177 let resp = handler.list_tasks(¶ms, req).await.unwrap();
1178 assert!(resp.tasks.is_empty());
1179 }
1180
1181 #[tokio::test]
1182 async fn test_cancel_task_not_found() {
1183 let handler = make_handler();
1184 let params = ServiceParams::new();
1185 let req = CancelTaskRequest {
1186 id: "nonexistent".into(),
1187 metadata: None,
1188 tenant: None,
1189 };
1190 let result = handler.cancel_task(¶ms, req).await;
1191 assert!(result.is_err());
1192 }
1193
1194 #[tokio::test]
1195 async fn test_cancel_task_working() {
1196 let handler = make_handler();
1197 let params = ServiceParams::new();
1198 let task = Task {
1200 id: "t2".into(),
1201 context_id: "c2".into(),
1202 status: TaskStatus {
1203 state: TaskState::Working,
1204 message: None,
1205 timestamp: None,
1206 },
1207 artifacts: None,
1208 history: None,
1209 metadata: None,
1210 };
1211 handler.task_store.create(task).await.unwrap();
1212 let req = CancelTaskRequest {
1213 id: "t2".into(),
1214 metadata: None,
1215 tenant: None,
1216 };
1217 let result = handler.cancel_task(¶ms, req).await.unwrap();
1218 assert_eq!(result.status.state, TaskState::Canceled);
1219 }
1220
1221 #[tokio::test]
1222 async fn test_cancel_task_already_completed() {
1223 let handler = make_handler();
1224 let params = ServiceParams::new();
1225 let task = Task {
1226 id: "t3".into(),
1227 context_id: "c3".into(),
1228 status: TaskStatus {
1229 state: TaskState::Completed,
1230 message: None,
1231 timestamp: None,
1232 },
1233 artifacts: None,
1234 history: None,
1235 metadata: None,
1236 };
1237 handler.task_store.create(task).await.unwrap();
1238 let req = CancelTaskRequest {
1239 id: "t3".into(),
1240 metadata: None,
1241 tenant: None,
1242 };
1243 let result = handler.cancel_task(¶ms, req).await;
1244 assert!(result.is_err());
1245 }
1246
1247 #[tokio::test]
1248 async fn test_subscribe_requires_active_execution() {
1249 let handler = make_handler();
1250 let params = ServiceParams::new();
1251 let req = SubscribeToTaskRequest {
1252 id: "t1".into(),
1253 tenant: None,
1254 };
1255 let result = handler.subscribe_to_task(¶ms, req).await;
1256 match result {
1257 Ok(_) => panic!("expected subscribe_to_task to fail"),
1258 Err(error) => assert_eq!(error.code, error_code::TASK_NOT_FOUND),
1259 }
1260 }
1261
1262 #[tokio::test]
1263 async fn test_send_message_return_immediately_allows_resubscribe() {
1264 use futures::StreamExt;
1265
1266 let (handler, release) = make_resumable_handler();
1267 let params = ServiceParams::new();
1268 let mut message = make_message();
1269 message.task_id = Some("t-resume".into());
1270 message.context_id = Some("c-resume".into());
1271
1272 let response = handler
1273 .send_message(
1274 ¶ms,
1275 SendMessageRequest {
1276 message,
1277 configuration: Some(SendMessageConfiguration {
1278 accepted_output_modes: None,
1279 push_notification_config: None,
1280 history_length: None,
1281 return_immediately: Some(true),
1282 }),
1283 metadata: None,
1284 tenant: None,
1285 },
1286 )
1287 .await
1288 .unwrap();
1289
1290 match response {
1291 SendMessageResponse::Task(task) => {
1292 assert_eq!(task.status.state, TaskState::Working);
1293 }
1294 _ => panic!("expected Task response"),
1295 }
1296
1297 let mut subscription = handler
1298 .subscribe_to_task(
1299 ¶ms,
1300 SubscribeToTaskRequest {
1301 id: "t-resume".into(),
1302 tenant: None,
1303 },
1304 )
1305 .await
1306 .unwrap();
1307
1308 let snapshot = subscription.next().await.unwrap().unwrap();
1309 match snapshot {
1310 StreamResponse::Task(task) => assert_eq!(task.status.state, TaskState::Working),
1311 _ => panic!("expected snapshot task"),
1312 }
1313
1314 release.notify_waiters();
1315
1316 let terminal = subscription.next().await.unwrap().unwrap();
1317 match terminal {
1318 StreamResponse::Task(task) => assert_eq!(task.status.state, TaskState::Completed),
1319 _ => panic!("expected terminal task"),
1320 }
1321
1322 assert!(subscription.next().await.is_none());
1323
1324 let result = handler
1325 .subscribe_to_task(
1326 ¶ms,
1327 SubscribeToTaskRequest {
1328 id: "t-resume".into(),
1329 tenant: None,
1330 },
1331 )
1332 .await;
1333 match result {
1334 Ok(_) => panic!("expected subscribe_to_task to fail after completion"),
1335 Err(error) => assert_eq!(error.code, error_code::TASK_NOT_FOUND),
1336 }
1337 }
1338
1339 #[tokio::test]
1340 async fn test_push_config_not_supported_without_store() {
1341 let handler = make_handler();
1342 let params = ServiceParams::new();
1343 let result = handler
1344 .create_push_config(
1345 ¶ms,
1346 CreateTaskPushNotificationConfigRequest {
1347 task_id: "t1".into(),
1348 config: PushNotificationConfig {
1349 url: "http://example.com".into(),
1350 id: None,
1351 token: None,
1352 authentication: None,
1353 },
1354 tenant: None,
1355 },
1356 )
1357 .await;
1358 assert!(result.is_err());
1359 }
1360
1361 #[tokio::test]
1362 async fn test_push_config_crud_with_store() {
1363 let handler = make_handler_with_push_configs();
1364 let params = ServiceParams::new();
1365
1366 let created = handler
1367 .create_push_config(
1368 ¶ms,
1369 CreateTaskPushNotificationConfigRequest {
1370 task_id: "t1".into(),
1371 config: PushNotificationConfig {
1372 url: "https://example.com/first".into(),
1373 id: Some("cfg-1".into()),
1374 token: None,
1375 authentication: None,
1376 },
1377 tenant: Some("tenant-a".into()),
1378 },
1379 )
1380 .await
1381 .unwrap();
1382 assert_eq!(created.config.id.as_deref(), Some("cfg-1"));
1383
1384 handler
1385 .create_push_config(
1386 ¶ms,
1387 CreateTaskPushNotificationConfigRequest {
1388 task_id: "t1".into(),
1389 config: PushNotificationConfig {
1390 url: "https://example.com/second".into(),
1391 id: Some("cfg-2".into()),
1392 token: None,
1393 authentication: None,
1394 },
1395 tenant: Some("tenant-a".into()),
1396 },
1397 )
1398 .await
1399 .unwrap();
1400
1401 let fetched = handler
1402 .get_push_config(
1403 ¶ms,
1404 GetTaskPushNotificationConfigRequest {
1405 task_id: "t1".into(),
1406 id: "cfg-1".into(),
1407 tenant: Some("tenant-a".into()),
1408 },
1409 )
1410 .await
1411 .unwrap();
1412 assert_eq!(fetched.config.url, "https://example.com/first");
1413
1414 let first_page = handler
1415 .list_push_configs(
1416 ¶ms,
1417 ListTaskPushNotificationConfigsRequest {
1418 task_id: "t1".into(),
1419 page_size: Some(1),
1420 page_token: Some("0".into()),
1421 tenant: Some("tenant-a".into()),
1422 },
1423 )
1424 .await
1425 .unwrap();
1426 assert_eq!(first_page.configs.len(), 1);
1427 assert_eq!(first_page.configs[0].config.id.as_deref(), Some("cfg-1"));
1428 assert_eq!(first_page.next_page_token.as_deref(), Some("1"));
1429
1430 let default_page = handler
1431 .list_push_configs(
1432 ¶ms,
1433 ListTaskPushNotificationConfigsRequest {
1434 task_id: "t1".into(),
1435 page_size: Some(0),
1436 page_token: None,
1437 tenant: Some("tenant-a".into()),
1438 },
1439 )
1440 .await
1441 .unwrap();
1442 assert_eq!(default_page.configs.len(), 2);
1443 assert!(default_page.next_page_token.is_none());
1444
1445 handler
1446 .delete_push_config(
1447 ¶ms,
1448 DeleteTaskPushNotificationConfigRequest {
1449 task_id: "t1".into(),
1450 id: "cfg-1".into(),
1451 tenant: Some("tenant-a".into()),
1452 },
1453 )
1454 .await
1455 .unwrap();
1456
1457 let remaining = handler
1458 .list_push_configs(
1459 ¶ms,
1460 ListTaskPushNotificationConfigsRequest {
1461 task_id: "t1".into(),
1462 page_size: None,
1463 page_token: None,
1464 tenant: Some("tenant-a".into()),
1465 },
1466 )
1467 .await
1468 .unwrap();
1469 assert_eq!(remaining.configs.len(), 1);
1470 assert_eq!(remaining.configs[0].config.id.as_deref(), Some("cfg-2"));
1471 }
1472
1473 #[tokio::test]
1474 async fn test_send_message_request_push_config_delivers_webhooks() {
1475 let (url, mut receiver, shutdown_tx, server) = start_push_webhook().await;
1476 let handler = make_push_delivery_handler();
1477 let params = ServiceParams::new();
1478 let mut message = make_message();
1479 message.task_id = Some("t-push-request".into());
1480 message.context_id = Some("c-push-request".into());
1481
1482 let response = handler
1483 .send_message(
1484 ¶ms,
1485 SendMessageRequest {
1486 message,
1487 configuration: Some(SendMessageConfiguration {
1488 accepted_output_modes: None,
1489 push_notification_config: Some(PushNotificationConfig {
1490 url: url.clone(),
1491 id: Some("cfg-request".into()),
1492 token: Some("notify-token".into()),
1493 authentication: Some(AuthenticationInfo {
1494 scheme: "bearer".into(),
1495 credentials: Some("secret-token".into()),
1496 }),
1497 }),
1498 history_length: None,
1499 return_immediately: None,
1500 }),
1501 metadata: None,
1502 tenant: None,
1503 },
1504 )
1505 .await
1506 .unwrap();
1507
1508 match response {
1509 SendMessageResponse::Task(task) => assert_eq!(task.status.state, TaskState::Completed),
1510 _ => panic!("expected Task response"),
1511 }
1512
1513 let saved = handler
1514 .get_push_config(
1515 ¶ms,
1516 GetTaskPushNotificationConfigRequest {
1517 task_id: "t-push-request".into(),
1518 id: "cfg-request".into(),
1519 tenant: None,
1520 },
1521 )
1522 .await
1523 .unwrap();
1524 assert_eq!(saved.config.url, url);
1525
1526 let first = next_push(&mut receiver).await;
1527 assert_eq!(first.authorization.as_deref(), Some("Bearer secret-token"));
1528 assert_eq!(first.notification_token.as_deref(), Some("notify-token"));
1529 match first.event {
1530 StreamResponse::StatusUpdate(update) => {
1531 assert_eq!(update.task_id, "t-push-request");
1532 assert_eq!(update.status.state, TaskState::Working);
1533 }
1534 _ => panic!("expected status update push"),
1535 }
1536
1537 let second = next_push(&mut receiver).await;
1538 assert_eq!(second.authorization.as_deref(), Some("Bearer secret-token"));
1539 assert_eq!(second.notification_token.as_deref(), Some("notify-token"));
1540 match second.event {
1541 StreamResponse::Task(task) => {
1542 assert_eq!(task.id, "t-push-request");
1543 assert_eq!(task.status.state, TaskState::Completed);
1544 }
1545 _ => panic!("expected final task push"),
1546 }
1547
1548 shutdown_tx.send(()).unwrap();
1549 server.await.unwrap();
1550 }
1551
1552 #[tokio::test]
1553 async fn test_send_message_stored_push_config_delivers_webhooks() {
1554 let (url, mut receiver, shutdown_tx, server) = start_push_webhook().await;
1555 let handler = make_push_delivery_handler();
1556 let params = ServiceParams::new();
1557
1558 handler
1559 .create_push_config(
1560 ¶ms,
1561 CreateTaskPushNotificationConfigRequest {
1562 task_id: "t-push-stored".into(),
1563 config: PushNotificationConfig {
1564 url: url.clone(),
1565 id: Some("cfg-stored".into()),
1566 token: Some("stored-token".into()),
1567 authentication: None,
1568 },
1569 tenant: None,
1570 },
1571 )
1572 .await
1573 .unwrap();
1574
1575 let mut message = make_message();
1576 message.task_id = Some("t-push-stored".into());
1577 message.context_id = Some("c-push-stored".into());
1578 let response = handler
1579 .send_message(
1580 ¶ms,
1581 SendMessageRequest {
1582 message,
1583 configuration: None,
1584 metadata: None,
1585 tenant: None,
1586 },
1587 )
1588 .await
1589 .unwrap();
1590
1591 match response {
1592 SendMessageResponse::Task(task) => assert_eq!(task.status.state, TaskState::Completed),
1593 _ => panic!("expected Task response"),
1594 }
1595
1596 let first = next_push(&mut receiver).await;
1597 assert_eq!(first.notification_token.as_deref(), Some("stored-token"));
1598 match first.event {
1599 StreamResponse::StatusUpdate(update) => {
1600 assert_eq!(update.task_id, "t-push-stored");
1601 assert_eq!(update.status.state, TaskState::Working);
1602 }
1603 _ => panic!("expected status update push"),
1604 }
1605
1606 let second = next_push(&mut receiver).await;
1607 assert_eq!(second.notification_token.as_deref(), Some("stored-token"));
1608 match second.event {
1609 StreamResponse::Task(task) => {
1610 assert_eq!(task.id, "t-push-stored");
1611 assert_eq!(task.status.state, TaskState::Completed);
1612 }
1613 _ => panic!("expected final task push"),
1614 }
1615
1616 shutdown_tx.send(()).unwrap();
1617 server.await.unwrap();
1618 }
1619
1620 #[tokio::test]
1621 async fn test_extended_agent_card_not_configured() {
1622 let handler = make_handler();
1623 let params = ServiceParams::new();
1624 let result = handler
1625 .get_extended_agent_card(¶ms, GetExtendedAgentCardRequest { tenant: None })
1626 .await;
1627 assert!(result.is_err());
1628 }
1629
1630 #[test]
1631 fn test_with_capabilities() {
1632 let handler = make_handler().with_capabilities(AgentCapabilities {
1633 streaming: Some(true),
1634 push_notifications: None,
1635 extensions: None,
1636 extended_agent_card: None,
1637 });
1638 assert_eq!(handler.capabilities.streaming, Some(true));
1639 }
1640
1641 #[test]
1642 fn test_with_push_config_store_enables_push_capability() {
1643 let handler = make_handler_with_push_configs();
1644 assert_eq!(handler.capabilities.push_notifications, Some(true));
1645 }
1646}