Skip to main content

a2a_server/
handler.rs

1// Copyright AGNTCY Contributors (https://github.com/agntcy)
2// SPDX-License-Identifier: Apache-2.0
3use 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/// Transport-agnostic request handler interface.
347///
348/// This is the core interface consumed by all protocol bindings
349/// (JSON-RPC, REST, gRPC). The [`DefaultRequestHandler`] provides a
350/// standard implementation that orchestrates task lifecycle, storage,
351/// and executor dispatch.
352#[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: TaskPushNotificationConfig,
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
418/// Default implementation of [`RequestHandler`] that orchestrates
419/// task lifecycle management, storage, and executor dispatch.
420pub 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.task_push_notification_config.clone())
523        else {
524            return Ok(());
525        };
526
527        let mut config = config;
528        config.task_id = task_id.to_string();
529        if config.tenant.is_none() {
530            config.tenant = req.tenant.clone();
531        }
532        self.push_config_store()?.save(config).await?;
533        Ok(())
534    }
535
536    async fn start_execution(
537        &self,
538        params: &ServiceParams,
539        req: SendMessageRequest,
540        include_task_snapshot_in_context: bool,
541    ) -> Result<(TaskId, BoxStream<'static, Result<StreamResponse, A2AError>>), A2AError> {
542        let (task, stored_task, context_id) = self.prepare_task_for_execution(&req).await?;
543        let task_id = task.id.clone();
544        self.save_request_push_config(&task_id, &req).await?;
545        let active = self.execution_manager.start(task.clone()).await?;
546        let receiver = active.subscribe();
547
548        let exec_ctx = crate::ExecutorContext {
549            message: Some(req.message),
550            task_id: task_id.clone(),
551            stored_task: if include_task_snapshot_in_context {
552                Some(task.clone())
553            } else {
554                stored_task
555            },
556            context_id,
557            metadata: req.metadata,
558            user: None,
559            service_params: params.clone(),
560            tenant: req.tenant,
561        };
562
563        let runtime = ExecutionRuntime {
564            executor: Arc::clone(&self.executor),
565            task_store: Arc::clone(&self.task_store),
566            push_config_store: self.push_config_store.clone(),
567            push_sender: self.push_sender.clone(),
568            execution_manager: Arc::clone(&self.execution_manager),
569        };
570        let active_execution = active.clone();
571        let execution_task_id = task_id.clone();
572
573        tokio::spawn(async move {
574            drive_execution(runtime, execution_task_id, active_execution, exec_ctx).await;
575        });
576
577        Ok((task_id, subscription_stream(receiver, None, 0)))
578    }
579}
580
581#[async_trait]
582impl RequestHandler for DefaultRequestHandler {
583    async fn send_message(
584        &self,
585        params: &ServiceParams,
586        req: SendMessageRequest,
587    ) -> Result<SendMessageResponse, A2AError> {
588        let (task_id, mut stream) = self.start_execution(params, req.clone(), true).await?;
589        let mut last_event = None;
590
591        while let Some(item) = stream.next().await {
592            let event = item?;
593
594            if let Some(interrupt_task_id) = should_interrupt_non_streaming(&req, &event) {
595                return Ok(SendMessageResponse::Task(
596                    self.load_task(&interrupt_task_id).await?,
597                ));
598            }
599
600            match event {
601                StreamResponse::Message(message) => {
602                    return Ok(SendMessageResponse::Message(message));
603                }
604                other => last_event = Some(other),
605            }
606        }
607
608        match last_event {
609            Some(StreamResponse::Task(task)) => Ok(SendMessageResponse::Task(task)),
610            Some(StreamResponse::StatusUpdate(update)) => Ok(SendMessageResponse::Task(
611                self.load_task(&update.task_id).await?,
612            )),
613            Some(StreamResponse::ArtifactUpdate(update)) => Ok(SendMessageResponse::Task(
614                self.load_task(&update.task_id).await?,
615            )),
616            Some(StreamResponse::Message(message)) => Ok(SendMessageResponse::Message(message)),
617            None => Ok(SendMessageResponse::Task(self.load_task(&task_id).await?)),
618        }
619    }
620
621    async fn send_streaming_message(
622        &self,
623        params: &ServiceParams,
624        req: SendMessageRequest,
625    ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
626        let (_, stream) = self.start_execution(params, req, false).await?;
627        Ok(stream)
628    }
629
630    async fn get_task(
631        &self,
632        _params: &ServiceParams,
633        req: GetTaskRequest,
634    ) -> Result<Task, A2AError> {
635        self.task_store
636            .get(&req.id)
637            .await?
638            .ok_or_else(|| A2AError::task_not_found(&req.id))
639    }
640
641    async fn list_tasks(
642        &self,
643        _params: &ServiceParams,
644        req: ListTasksRequest,
645    ) -> Result<ListTasksResponse, A2AError> {
646        self.task_store.list(&req).await
647    }
648
649    async fn cancel_task(
650        &self,
651        params: &ServiceParams,
652        req: CancelTaskRequest,
653    ) -> Result<Task, A2AError> {
654        let task = self.load_task(&req.id).await?;
655
656        if task.status.state.is_terminal() {
657            return Err(A2AError::task_not_cancelable(&req.id));
658        }
659
660        let active_execution = self.execution_manager.get(&req.id).await;
661
662        let exec_ctx = crate::ExecutorContext {
663            message: None,
664            task_id: req.id.clone(),
665            stored_task: Some(task.clone()),
666            context_id: task.context_id.clone(),
667            metadata: req.metadata,
668            user: None,
669            service_params: params.clone(),
670            tenant: req.tenant,
671        };
672
673        let mut stream = self.executor.cancel(exec_ctx);
674        let mut current_task = Some(task);
675
676        while let Some(event) = stream.next().await {
677            match event {
678                Ok(event) => {
679                    let updated_task =
680                        apply_event_to_task(self.task_store.as_ref(), current_task.clone(), &event)
681                            .await?;
682                    current_task = updated_task.clone();
683
684                    send_push_notifications(
685                        &req.id,
686                        self.push_config_store.as_deref(),
687                        self.push_sender.as_deref(),
688                        &event,
689                    )
690                    .await?;
691
692                    if let Some(active) = &active_execution {
693                        active.publish(Ok(event.clone()), updated_task).await;
694                    }
695
696                    if is_terminal_event(&event) {
697                        break;
698                    }
699                }
700                Err(error) => {
701                    if let Some(active) = &active_execution {
702                        active.publish(Err(error.clone()), None).await;
703                    }
704                    return Err(error);
705                }
706            }
707        }
708
709        if let Some(active) = active_execution {
710            self.execution_manager.finish(&req.id, &active).await;
711        }
712
713        self.load_task(&req.id).await
714    }
715
716    async fn subscribe_to_task(
717        &self,
718        _params: &ServiceParams,
719        req: SubscribeToTaskRequest,
720    ) -> Result<BoxStream<'static, Result<StreamResponse, A2AError>>, A2AError> {
721        let (receiver, snapshot_task, sequence) = self
722            .execution_manager
723            .resubscribe(&req.id)
724            .await
725            .ok_or_else(|| A2AError::task_not_found(&req.id))?;
726        Ok(subscription_stream(receiver, snapshot_task, sequence))
727    }
728
729    async fn create_push_config(
730        &self,
731        _params: &ServiceParams,
732        req: TaskPushNotificationConfig,
733    ) -> Result<TaskPushNotificationConfig, A2AError> {
734        self.push_config_store()?.save(req).await
735    }
736
737    async fn get_push_config(
738        &self,
739        _params: &ServiceParams,
740        req: GetTaskPushNotificationConfigRequest,
741    ) -> Result<TaskPushNotificationConfig, A2AError> {
742        self.push_config_store()?.get(&req.task_id, &req.id).await
743    }
744
745    async fn list_push_configs(
746        &self,
747        _params: &ServiceParams,
748        req: ListTaskPushNotificationConfigsRequest,
749    ) -> Result<ListTaskPushNotificationConfigsResponse, A2AError> {
750        let mut configs = self.push_config_store()?.list(&req.task_id).await?;
751        configs.sort_by(|left, right| left.id.cmp(&right.id));
752
753        let page_size = match req.page_size {
754            Some(size) if size > 0 => size as usize,
755            _ => 50,
756        };
757        let start = req
758            .page_token
759            .as_deref()
760            .and_then(|token| token.parse::<usize>().ok())
761            .unwrap_or(0)
762            .min(configs.len());
763        let end = (start + page_size).min(configs.len());
764        let next_page_token = (end < configs.len()).then(|| end.to_string());
765
766        Ok(ListTaskPushNotificationConfigsResponse {
767            configs: configs[start..end].to_vec(),
768            next_page_token,
769        })
770    }
771
772    async fn delete_push_config(
773        &self,
774        _params: &ServiceParams,
775        req: DeleteTaskPushNotificationConfigRequest,
776    ) -> Result<(), A2AError> {
777        self.push_config_store()?
778            .delete(&req.task_id, &req.id)
779            .await
780    }
781
782    async fn get_extended_agent_card(
783        &self,
784        _params: &ServiceParams,
785        _req: GetExtendedAgentCardRequest,
786    ) -> Result<AgentCard, A2AError> {
787        Err(A2AError::unsupported_operation(
788            "extended agent card not configured",
789        ))
790    }
791}
792
793#[cfg(test)]
794mod tests {
795    use super::*;
796    use crate::executor::ExecutorContext;
797    use crate::push::InMemoryPushConfigStore;
798    use crate::task_store::InMemoryTaskStore;
799
800    use crate::test_util::install_crypto_provider;
801    use axum::{
802        Router,
803        body::Bytes,
804        extract::State,
805        http::{HeaderMap, StatusCode, header},
806        routing::post,
807    };
808    use futures::stream;
809    use std::sync::Arc;
810    use tokio::{
811        net::TcpListener,
812        sync::{Notify, mpsc, oneshot},
813        time::{Duration, timeout},
814    };
815
816    struct EchoExecutor;
817
818    struct PushEventExecutor;
819
820    struct ResumableExecutor {
821        release: Arc<Notify>,
822    }
823
824    #[derive(Debug)]
825    struct CapturedPush {
826        authorization: Option<String>,
827        notification_token: Option<String>,
828        event: StreamResponse,
829    }
830
831    impl crate::AgentExecutor for EchoExecutor {
832        fn execute(
833            &self,
834            ctx: ExecutorContext,
835        ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
836            let task = Task {
837                id: ctx.task_id.clone(),
838                context_id: ctx.context_id.clone(),
839                status: TaskStatus {
840                    state: TaskState::Completed,
841                    message: ctx.message,
842                    timestamp: None,
843                },
844                artifacts: None,
845                history: None,
846                metadata: None,
847            };
848            Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
849        }
850
851        fn cancel(
852            &self,
853            ctx: ExecutorContext,
854        ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
855            let task = Task {
856                id: ctx.task_id.clone(),
857                context_id: ctx.context_id.clone(),
858                status: TaskStatus {
859                    state: TaskState::Canceled,
860                    message: None,
861                    timestamp: None,
862                },
863                artifacts: None,
864                history: None,
865                metadata: None,
866            };
867            Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
868        }
869    }
870
871    impl crate::AgentExecutor for PushEventExecutor {
872        fn execute(
873            &self,
874            ctx: ExecutorContext,
875        ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
876            let working = StreamResponse::StatusUpdate(TaskStatusUpdateEvent {
877                task_id: ctx.task_id.clone(),
878                context_id: ctx.context_id.clone(),
879                status: TaskStatus {
880                    state: TaskState::Working,
881                    message: None,
882                    timestamp: None,
883                },
884                metadata: None,
885            });
886            let completed = StreamResponse::Task(Task {
887                id: ctx.task_id.clone(),
888                context_id: ctx.context_id.clone(),
889                status: TaskStatus {
890                    state: TaskState::Completed,
891                    message: ctx.message,
892                    timestamp: None,
893                },
894                artifacts: None,
895                history: ctx.stored_task.and_then(|task| task.history),
896                metadata: None,
897            });
898
899            Box::pin(stream::iter(vec![Ok(working), Ok(completed)]))
900        }
901
902        fn cancel(
903            &self,
904            ctx: ExecutorContext,
905        ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
906            let task = Task {
907                id: ctx.task_id.clone(),
908                context_id: ctx.context_id.clone(),
909                status: TaskStatus {
910                    state: TaskState::Canceled,
911                    message: None,
912                    timestamp: None,
913                },
914                artifacts: None,
915                history: None,
916                metadata: None,
917            };
918            Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
919        }
920    }
921
922    impl crate::AgentExecutor for ResumableExecutor {
923        fn execute(
924            &self,
925            ctx: ExecutorContext,
926        ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
927            let release = self.release.clone();
928            let working_task = Task {
929                id: ctx.task_id.clone(),
930                context_id: ctx.context_id.clone(),
931                status: TaskStatus {
932                    state: TaskState::Working,
933                    message: None,
934                    timestamp: None,
935                },
936                artifacts: None,
937                history: ctx.stored_task.and_then(|task| task.history),
938                metadata: None,
939            };
940            let completed_task = Task {
941                id: working_task.id.clone(),
942                context_id: working_task.context_id.clone(),
943                status: TaskStatus {
944                    state: TaskState::Completed,
945                    message: None,
946                    timestamp: None,
947                },
948                artifacts: None,
949                history: working_task.history.clone(),
950                metadata: None,
951            };
952
953            Box::pin(stream::unfold(
954                (Some(working_task), Some((release, completed_task))),
955                |(working_task, completion)| async move {
956                    if let Some(task) = working_task {
957                        return Some((Ok(StreamResponse::Task(task)), (None, completion)));
958                    }
959
960                    if let Some((release, task)) = completion {
961                        release.notified().await;
962                        return Some((Ok(StreamResponse::Task(task)), (None, None)));
963                    }
964
965                    None
966                },
967            ))
968        }
969
970        fn cancel(
971            &self,
972            ctx: ExecutorContext,
973        ) -> BoxStream<'static, Result<StreamResponse, A2AError>> {
974            let task = Task {
975                id: ctx.task_id.clone(),
976                context_id: ctx.context_id.clone(),
977                status: TaskStatus {
978                    state: TaskState::Canceled,
979                    message: None,
980                    timestamp: None,
981                },
982                artifacts: None,
983                history: None,
984                metadata: None,
985            };
986            Box::pin(stream::once(async move { Ok(StreamResponse::Task(task)) }))
987        }
988    }
989
990    fn make_handler() -> DefaultRequestHandler {
991        DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
992    }
993
994    fn make_handler_with_push_configs() -> DefaultRequestHandler {
995        DefaultRequestHandler::new(EchoExecutor, InMemoryTaskStore::new())
996            .with_push_config_store(InMemoryPushConfigStore::new())
997    }
998
999    fn make_push_delivery_handler() -> DefaultRequestHandler {
1000        DefaultRequestHandler::new(PushEventExecutor, InMemoryTaskStore::new())
1001            .with_push_config_store(InMemoryPushConfigStore::new())
1002    }
1003
1004    fn make_resumable_handler() -> (DefaultRequestHandler, Arc<Notify>) {
1005        let release = Arc::new(Notify::new());
1006        (
1007            DefaultRequestHandler::new(
1008                ResumableExecutor {
1009                    release: release.clone(),
1010                },
1011                InMemoryTaskStore::new(),
1012            ),
1013            release,
1014        )
1015    }
1016
1017    fn make_message() -> Message {
1018        Message::new(Role::User, vec![Part::text("hello")])
1019    }
1020
1021    async fn capture_push(
1022        State(sender): State<mpsc::UnboundedSender<CapturedPush>>,
1023        headers: HeaderMap,
1024        body: Bytes,
1025    ) -> StatusCode {
1026        let captured = CapturedPush {
1027            authorization: headers
1028                .get(header::AUTHORIZATION)
1029                .and_then(|value| value.to_str().ok())
1030                .map(ToOwned::to_owned),
1031            notification_token: headers
1032                .get("A2A-Notification-Token")
1033                .and_then(|value| value.to_str().ok())
1034                .map(ToOwned::to_owned),
1035            event: serde_json::from_slice(&body).unwrap(),
1036        };
1037        sender.send(captured).unwrap();
1038        StatusCode::ACCEPTED
1039    }
1040
1041    async fn start_push_webhook() -> (
1042        String,
1043        mpsc::UnboundedReceiver<CapturedPush>,
1044        oneshot::Sender<()>,
1045        tokio::task::JoinHandle<()>,
1046    ) {
1047        let (sender, receiver) = mpsc::unbounded_channel();
1048        let app = Router::new()
1049            .route("/", post(capture_push))
1050            .with_state(sender);
1051        let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1052        let address = listener.local_addr().unwrap();
1053        let (shutdown_tx, shutdown_rx) = oneshot::channel();
1054        let server = tokio::spawn(async move {
1055            axum::serve(listener, app)
1056                .with_graceful_shutdown(async {
1057                    let _ = shutdown_rx.await;
1058                })
1059                .await
1060                .unwrap();
1061        });
1062
1063        (format!("http://{address}/"), receiver, shutdown_tx, server)
1064    }
1065
1066    async fn next_push(receiver: &mut mpsc::UnboundedReceiver<CapturedPush>) -> CapturedPush {
1067        timeout(Duration::from_secs(5), receiver.recv())
1068            .await
1069            .unwrap()
1070            .unwrap()
1071    }
1072
1073    #[tokio::test]
1074    async fn test_send_message_creates_task() {
1075        let handler = make_handler();
1076        let params = ServiceParams::new();
1077        let req = SendMessageRequest {
1078            message: make_message(),
1079            configuration: None,
1080            metadata: None,
1081            tenant: None,
1082        };
1083        let result = handler.send_message(&params, req).await.unwrap();
1084        match result {
1085            SendMessageResponse::Task(task) => {
1086                assert_eq!(task.status.state, TaskState::Completed);
1087            }
1088            _ => panic!("expected Task response"),
1089        }
1090    }
1091
1092    #[tokio::test]
1093    async fn test_send_streaming_message() {
1094        use futures::StreamExt;
1095        let handler = make_handler();
1096        let params = ServiceParams::new();
1097        let req = SendMessageRequest {
1098            message: make_message(),
1099            configuration: None,
1100            metadata: None,
1101            tenant: None,
1102        };
1103        let mut stream = handler.send_streaming_message(&params, req).await.unwrap();
1104        let event = stream.next().await.unwrap().unwrap();
1105        assert!(matches!(event, StreamResponse::Task(_)));
1106    }
1107
1108    #[tokio::test]
1109    async fn test_get_task_not_found() {
1110        let handler = make_handler();
1111        let params = ServiceParams::new();
1112        let req = GetTaskRequest {
1113            id: "nonexistent".into(),
1114            history_length: None,
1115            tenant: None,
1116        };
1117        let result = handler.get_task(&params, req).await;
1118        assert!(result.is_err());
1119    }
1120
1121    #[tokio::test]
1122    async fn test_get_task_after_send() {
1123        let handler = make_handler();
1124        let params = ServiceParams::new();
1125        let mut msg = make_message();
1126        msg.task_id = Some("t1".into());
1127        msg.context_id = Some("c1".into());
1128        let req = SendMessageRequest {
1129            message: msg,
1130            configuration: None,
1131            metadata: None,
1132            tenant: None,
1133        };
1134        handler.send_message(&params, req).await.unwrap();
1135        let task = handler
1136            .get_task(
1137                &params,
1138                GetTaskRequest {
1139                    id: "t1".into(),
1140                    history_length: None,
1141                    tenant: None,
1142                },
1143            )
1144            .await
1145            .unwrap();
1146        assert_eq!(task.id, "t1");
1147    }
1148
1149    #[tokio::test]
1150    async fn test_list_tasks() {
1151        let handler = make_handler();
1152        let params = ServiceParams::new();
1153        let req = ListTasksRequest {
1154            context_id: None,
1155            status: None,
1156            page_size: None,
1157            page_token: None,
1158            history_length: None,
1159            status_timestamp_after: None,
1160            include_artifacts: None,
1161            tenant: None,
1162        };
1163        let resp = handler.list_tasks(&params, req).await.unwrap();
1164        assert!(resp.tasks.is_empty());
1165    }
1166
1167    #[tokio::test]
1168    async fn test_cancel_task_not_found() {
1169        let handler = make_handler();
1170        let params = ServiceParams::new();
1171        let req = CancelTaskRequest {
1172            id: "nonexistent".into(),
1173            metadata: None,
1174            tenant: None,
1175        };
1176        let result = handler.cancel_task(&params, req).await;
1177        assert!(result.is_err());
1178    }
1179
1180    #[tokio::test]
1181    async fn test_cancel_task_working() {
1182        let handler = make_handler();
1183        let params = ServiceParams::new();
1184        // First create a task via the store directly
1185        let task = Task {
1186            id: "t2".into(),
1187            context_id: "c2".into(),
1188            status: TaskStatus {
1189                state: TaskState::Working,
1190                message: None,
1191                timestamp: None,
1192            },
1193            artifacts: None,
1194            history: None,
1195            metadata: None,
1196        };
1197        handler.task_store.create(task).await.unwrap();
1198        let req = CancelTaskRequest {
1199            id: "t2".into(),
1200            metadata: None,
1201            tenant: None,
1202        };
1203        let result = handler.cancel_task(&params, req).await.unwrap();
1204        assert_eq!(result.status.state, TaskState::Canceled);
1205    }
1206
1207    #[tokio::test]
1208    async fn test_cancel_task_already_completed() {
1209        let handler = make_handler();
1210        let params = ServiceParams::new();
1211        let task = Task {
1212            id: "t3".into(),
1213            context_id: "c3".into(),
1214            status: TaskStatus {
1215                state: TaskState::Completed,
1216                message: None,
1217                timestamp: None,
1218            },
1219            artifacts: None,
1220            history: None,
1221            metadata: None,
1222        };
1223        handler.task_store.create(task).await.unwrap();
1224        let req = CancelTaskRequest {
1225            id: "t3".into(),
1226            metadata: None,
1227            tenant: None,
1228        };
1229        let result = handler.cancel_task(&params, req).await;
1230        assert!(result.is_err());
1231    }
1232
1233    #[tokio::test]
1234    async fn test_subscribe_requires_active_execution() {
1235        let handler = make_handler();
1236        let params = ServiceParams::new();
1237        let req = SubscribeToTaskRequest {
1238            id: "t1".into(),
1239            tenant: None,
1240        };
1241        let result = handler.subscribe_to_task(&params, req).await;
1242        match result {
1243            Ok(_) => panic!("expected subscribe_to_task to fail"),
1244            Err(error) => assert_eq!(error.code, error_code::TASK_NOT_FOUND),
1245        }
1246    }
1247
1248    #[tokio::test]
1249    async fn test_send_message_return_immediately_allows_resubscribe() {
1250        use futures::StreamExt;
1251
1252        let (handler, release) = make_resumable_handler();
1253        let params = ServiceParams::new();
1254        let mut message = make_message();
1255        message.task_id = Some("t-resume".into());
1256        message.context_id = Some("c-resume".into());
1257
1258        let response = handler
1259            .send_message(
1260                &params,
1261                SendMessageRequest {
1262                    message,
1263                    configuration: Some(SendMessageConfiguration {
1264                        accepted_output_modes: None,
1265                        task_push_notification_config: None,
1266                        history_length: None,
1267                        return_immediately: Some(true),
1268                    }),
1269                    metadata: None,
1270                    tenant: None,
1271                },
1272            )
1273            .await
1274            .unwrap();
1275
1276        match response {
1277            SendMessageResponse::Task(task) => {
1278                assert_eq!(task.status.state, TaskState::Working);
1279            }
1280            _ => panic!("expected Task response"),
1281        }
1282
1283        let mut subscription = handler
1284            .subscribe_to_task(
1285                &params,
1286                SubscribeToTaskRequest {
1287                    id: "t-resume".into(),
1288                    tenant: None,
1289                },
1290            )
1291            .await
1292            .unwrap();
1293
1294        let snapshot = subscription.next().await.unwrap().unwrap();
1295        match snapshot {
1296            StreamResponse::Task(task) => assert_eq!(task.status.state, TaskState::Working),
1297            _ => panic!("expected snapshot task"),
1298        }
1299
1300        release.notify_waiters();
1301
1302        let terminal = subscription.next().await.unwrap().unwrap();
1303        match terminal {
1304            StreamResponse::Task(task) => assert_eq!(task.status.state, TaskState::Completed),
1305            _ => panic!("expected terminal task"),
1306        }
1307
1308        assert!(subscription.next().await.is_none());
1309
1310        let result = handler
1311            .subscribe_to_task(
1312                &params,
1313                SubscribeToTaskRequest {
1314                    id: "t-resume".into(),
1315                    tenant: None,
1316                },
1317            )
1318            .await;
1319        match result {
1320            Ok(_) => panic!("expected subscribe_to_task to fail after completion"),
1321            Err(error) => assert_eq!(error.code, error_code::TASK_NOT_FOUND),
1322        }
1323    }
1324
1325    #[tokio::test]
1326    async fn test_push_config_not_supported_without_store() {
1327        let handler = make_handler();
1328        let params = ServiceParams::new();
1329        let result = handler
1330            .create_push_config(
1331                &params,
1332                TaskPushNotificationConfig {
1333                    task_id: "t1".into(),
1334                    url: "http://example.com".into(),
1335                    id: None,
1336                    token: None,
1337                    authentication: None,
1338                    tenant: None,
1339                },
1340            )
1341            .await;
1342        assert!(result.is_err());
1343    }
1344
1345    #[tokio::test]
1346    async fn test_push_config_crud_with_store() {
1347        install_crypto_provider();
1348        let handler = make_handler_with_push_configs();
1349        let params = ServiceParams::new();
1350
1351        let created = handler
1352            .create_push_config(
1353                &params,
1354                TaskPushNotificationConfig {
1355                    task_id: "t1".into(),
1356                    url: "https://example.com/first".into(),
1357                    id: Some("cfg-1".into()),
1358                    token: None,
1359                    authentication: None,
1360                    tenant: Some("tenant-a".into()),
1361                },
1362            )
1363            .await
1364            .unwrap();
1365        assert_eq!(created.id.as_deref(), Some("cfg-1"));
1366
1367        handler
1368            .create_push_config(
1369                &params,
1370                TaskPushNotificationConfig {
1371                    task_id: "t1".into(),
1372                    url: "https://example.com/second".into(),
1373                    id: Some("cfg-2".into()),
1374                    token: None,
1375                    authentication: None,
1376                    tenant: Some("tenant-a".into()),
1377                },
1378            )
1379            .await
1380            .unwrap();
1381
1382        let fetched = handler
1383            .get_push_config(
1384                &params,
1385                GetTaskPushNotificationConfigRequest {
1386                    task_id: "t1".into(),
1387                    id: "cfg-1".into(),
1388                    tenant: Some("tenant-a".into()),
1389                },
1390            )
1391            .await
1392            .unwrap();
1393        assert_eq!(fetched.url, "https://example.com/first");
1394
1395        let first_page = handler
1396            .list_push_configs(
1397                &params,
1398                ListTaskPushNotificationConfigsRequest {
1399                    task_id: "t1".into(),
1400                    page_size: Some(1),
1401                    page_token: Some("0".into()),
1402                    tenant: Some("tenant-a".into()),
1403                },
1404            )
1405            .await
1406            .unwrap();
1407        assert_eq!(first_page.configs.len(), 1);
1408        assert_eq!(first_page.configs[0].id.as_deref(), Some("cfg-1"));
1409        assert_eq!(first_page.next_page_token.as_deref(), Some("1"));
1410
1411        let default_page = handler
1412            .list_push_configs(
1413                &params,
1414                ListTaskPushNotificationConfigsRequest {
1415                    task_id: "t1".into(),
1416                    page_size: Some(0),
1417                    page_token: None,
1418                    tenant: Some("tenant-a".into()),
1419                },
1420            )
1421            .await
1422            .unwrap();
1423        assert_eq!(default_page.configs.len(), 2);
1424        assert!(default_page.next_page_token.is_none());
1425
1426        handler
1427            .delete_push_config(
1428                &params,
1429                DeleteTaskPushNotificationConfigRequest {
1430                    task_id: "t1".into(),
1431                    id: "cfg-1".into(),
1432                    tenant: Some("tenant-a".into()),
1433                },
1434            )
1435            .await
1436            .unwrap();
1437
1438        let remaining = handler
1439            .list_push_configs(
1440                &params,
1441                ListTaskPushNotificationConfigsRequest {
1442                    task_id: "t1".into(),
1443                    page_size: None,
1444                    page_token: None,
1445                    tenant: Some("tenant-a".into()),
1446                },
1447            )
1448            .await
1449            .unwrap();
1450        assert_eq!(remaining.configs.len(), 1);
1451        assert_eq!(remaining.configs[0].id.as_deref(), Some("cfg-2"));
1452    }
1453
1454    #[tokio::test]
1455    async fn test_send_message_request_push_config_delivers_webhooks() {
1456        install_crypto_provider();
1457        let (url, mut receiver, shutdown_tx, server) = start_push_webhook().await;
1458        let handler = make_push_delivery_handler();
1459        let params = ServiceParams::new();
1460        let mut message = make_message();
1461        message.task_id = Some("t-push-request".into());
1462        message.context_id = Some("c-push-request".into());
1463
1464        let response = handler
1465            .send_message(
1466                &params,
1467                SendMessageRequest {
1468                    message,
1469                    configuration: Some(SendMessageConfiguration {
1470                        accepted_output_modes: None,
1471                        task_push_notification_config: Some(TaskPushNotificationConfig {
1472                            task_id: String::new(),
1473                            url: url.clone(),
1474                            id: Some("cfg-request".into()),
1475                            token: Some("notify-token".into()),
1476                            authentication: Some(AuthenticationInfo {
1477                                scheme: "bearer".into(),
1478                                credentials: Some("secret-token".into()),
1479                            }),
1480                            tenant: None,
1481                        }),
1482                        history_length: None,
1483                        return_immediately: None,
1484                    }),
1485                    metadata: None,
1486                    tenant: None,
1487                },
1488            )
1489            .await
1490            .unwrap();
1491
1492        match response {
1493            SendMessageResponse::Task(task) => assert_eq!(task.status.state, TaskState::Completed),
1494            _ => panic!("expected Task response"),
1495        }
1496
1497        let saved = handler
1498            .get_push_config(
1499                &params,
1500                GetTaskPushNotificationConfigRequest {
1501                    task_id: "t-push-request".into(),
1502                    id: "cfg-request".into(),
1503                    tenant: None,
1504                },
1505            )
1506            .await
1507            .unwrap();
1508        assert_eq!(saved.url, url);
1509
1510        let first = next_push(&mut receiver).await;
1511        assert_eq!(first.authorization.as_deref(), Some("Bearer secret-token"));
1512        assert_eq!(first.notification_token.as_deref(), Some("notify-token"));
1513        match first.event {
1514            StreamResponse::StatusUpdate(update) => {
1515                assert_eq!(update.task_id, "t-push-request");
1516                assert_eq!(update.status.state, TaskState::Working);
1517            }
1518            _ => panic!("expected status update push"),
1519        }
1520
1521        let second = next_push(&mut receiver).await;
1522        assert_eq!(second.authorization.as_deref(), Some("Bearer secret-token"));
1523        assert_eq!(second.notification_token.as_deref(), Some("notify-token"));
1524        match second.event {
1525            StreamResponse::Task(task) => {
1526                assert_eq!(task.id, "t-push-request");
1527                assert_eq!(task.status.state, TaskState::Completed);
1528            }
1529            _ => panic!("expected final task push"),
1530        }
1531
1532        shutdown_tx.send(()).unwrap();
1533        server.await.unwrap();
1534    }
1535
1536    #[tokio::test]
1537    async fn test_send_message_stored_push_config_delivers_webhooks() {
1538        install_crypto_provider();
1539        let (url, mut receiver, shutdown_tx, server) = start_push_webhook().await;
1540        let handler = make_push_delivery_handler();
1541        let params = ServiceParams::new();
1542
1543        handler
1544            .create_push_config(
1545                &params,
1546                TaskPushNotificationConfig {
1547                    task_id: "t-push-stored".into(),
1548                    url: url.clone(),
1549                    id: Some("cfg-stored".into()),
1550                    token: Some("stored-token".into()),
1551                    authentication: None,
1552                    tenant: None,
1553                },
1554            )
1555            .await
1556            .unwrap();
1557
1558        let mut message = make_message();
1559        message.task_id = Some("t-push-stored".into());
1560        message.context_id = Some("c-push-stored".into());
1561        let response = handler
1562            .send_message(
1563                &params,
1564                SendMessageRequest {
1565                    message,
1566                    configuration: None,
1567                    metadata: None,
1568                    tenant: None,
1569                },
1570            )
1571            .await
1572            .unwrap();
1573
1574        match response {
1575            SendMessageResponse::Task(task) => assert_eq!(task.status.state, TaskState::Completed),
1576            _ => panic!("expected Task response"),
1577        }
1578
1579        let first = next_push(&mut receiver).await;
1580        assert_eq!(first.notification_token.as_deref(), Some("stored-token"));
1581        match first.event {
1582            StreamResponse::StatusUpdate(update) => {
1583                assert_eq!(update.task_id, "t-push-stored");
1584                assert_eq!(update.status.state, TaskState::Working);
1585            }
1586            _ => panic!("expected status update push"),
1587        }
1588
1589        let second = next_push(&mut receiver).await;
1590        assert_eq!(second.notification_token.as_deref(), Some("stored-token"));
1591        match second.event {
1592            StreamResponse::Task(task) => {
1593                assert_eq!(task.id, "t-push-stored");
1594                assert_eq!(task.status.state, TaskState::Completed);
1595            }
1596            _ => panic!("expected final task push"),
1597        }
1598
1599        shutdown_tx.send(()).unwrap();
1600        server.await.unwrap();
1601    }
1602
1603    #[tokio::test]
1604    async fn test_extended_agent_card_not_configured() {
1605        let handler = make_handler();
1606        let params = ServiceParams::new();
1607        let result = handler
1608            .get_extended_agent_card(&params, GetExtendedAgentCardRequest { tenant: None })
1609            .await;
1610        assert!(result.is_err());
1611    }
1612
1613    #[test]
1614    fn test_with_capabilities() {
1615        let handler = make_handler().with_capabilities(AgentCapabilities {
1616            streaming: Some(true),
1617            push_notifications: None,
1618            extensions: None,
1619            extended_agent_card: None,
1620        });
1621        assert_eq!(handler.capabilities.streaming, Some(true));
1622    }
1623
1624    #[test]
1625    fn test_with_push_config_store_enables_push_capability() {
1626        install_crypto_provider();
1627        let handler = make_handler_with_push_configs();
1628        assert_eq!(handler.capabilities.push_notifications, Some(true));
1629    }
1630}