1use crate::ServerConfig;
2use crate::a2a::{
3 AgentCard, Executor, ExecutorConfig, JsonRpcError, JsonRpcRequest, JsonRpcResponse, Message,
4 MessageSendParams, Task, TaskState, TaskStatus, TaskStatusUpdateEvent, TasksCancelParams,
5 TasksGetParams, UpdateEvent, build_agent_card, jsonrpc,
6};
7use crate::auth_bridge::{AuthenticatedCaller, RequestContext};
8use adk_runner::{Runner, RunnerConfig};
9use axum::{
10 extract::State,
11 http::StatusCode,
12 response::{
13 IntoResponse, Json,
14 sse::{Event, Sse},
15 },
16};
17use futures::stream::Stream;
18use serde_json::Value;
19use std::{
20 collections::HashMap,
21 convert::Infallible,
22 sync::Arc,
23 time::{Duration, Instant},
24};
25use tokio::sync::{Mutex, Notify, RwLock, mpsc, oneshot};
26use tokio_util::sync::CancellationToken;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct A2aTaskRetention {
49 pub max_finished: Option<usize>,
51 pub ttl: Option<Duration>,
53}
54
55impl Default for A2aTaskRetention {
56 fn default() -> Self {
57 Self { max_finished: Some(1000), ttl: Some(Duration::from_secs(3600)) }
58 }
59}
60
61impl A2aTaskRetention {
62 pub fn unlimited() -> Self {
64 Self { max_finished: None, ttl: None }
65 }
66
67 pub fn with_max_finished(mut self, count: usize) -> Self {
69 self.max_finished = Some(count);
70 self
71 }
72
73 pub fn with_ttl(mut self, ttl: Duration) -> Self {
75 self.ttl = Some(ttl);
76 self
77 }
78}
79
80struct StoredTask {
82 task: Task,
83 owner: Option<String>,
84 finished_at: Instant,
85}
86
87pub struct TaskStore {
94 tasks: RwLock<HashMap<String, StoredTask>>,
95 retention: A2aTaskRetention,
96}
97
98impl Default for TaskStore {
99 fn default() -> Self {
100 Self::with_retention(A2aTaskRetention::default())
101 }
102}
103
104impl TaskStore {
105 pub fn new() -> Self {
107 Self::default()
108 }
109
110 pub fn with_retention(retention: A2aTaskRetention) -> Self {
112 Self { tasks: RwLock::new(HashMap::new()), retention }
113 }
114
115 pub async fn store(&self, task: Task) {
120 self.store_for(None, task).await;
121 }
122
123 pub async fn store_for(&self, owner: Option<&str>, task: Task) {
125 let mut tasks = self.tasks.write().await;
126 tasks.insert(
127 task.id.clone(),
128 StoredTask { task, owner: owner.map(str::to_string), finished_at: Instant::now() },
129 );
130 self.evict(&mut tasks, Instant::now());
131 }
132
133 pub async fn get(&self, task_id: &str) -> Option<Task> {
138 let tasks = self.tasks.read().await;
139 tasks
140 .get(task_id)
141 .filter(|stored| !self.is_expired(stored))
142 .map(|stored| stored.task.clone())
143 }
144
145 pub async fn get_for(&self, owner: Option<&str>, task_id: &str) -> Option<Task> {
150 let tasks = self.tasks.read().await;
151 tasks
152 .get(task_id)
153 .filter(|stored| !self.is_expired(stored) && stored.owner.as_deref() == owner)
154 .map(|stored| stored.task.clone())
155 }
156
157 pub async fn remove(&self, task_id: &str) -> Option<Task> {
159 self.tasks.write().await.remove(task_id).map(|stored| stored.task)
160 }
161
162 pub async fn len(&self) -> usize {
164 self.tasks.read().await.len()
165 }
166
167 pub async fn is_empty(&self) -> bool {
169 self.tasks.read().await.is_empty()
170 }
171
172 async fn owner_of(&self, task_id: &str) -> Option<Option<String>> {
174 let tasks = self.tasks.read().await;
175 tasks
176 .get(task_id)
177 .filter(|stored| !self.is_expired(stored))
178 .map(|stored| stored.owner.clone())
179 }
180
181 fn is_expired(&self, stored: &StoredTask) -> bool {
182 self.retention.ttl.is_some_and(|ttl| stored.finished_at.elapsed() >= ttl)
183 }
184
185 fn evict(&self, tasks: &mut HashMap<String, StoredTask>, now: Instant) {
186 if let Some(ttl) = self.retention.ttl {
187 tasks.retain(|_, stored| now.saturating_duration_since(stored.finished_at) < ttl);
188 }
189 if let Some(max_finished) = self.retention.max_finished
190 && tasks.len() > max_finished
191 {
192 let mut by_age: Vec<(Instant, String)> =
193 tasks.iter().map(|(id, stored)| (stored.finished_at, id.clone())).collect();
194 by_age.sort();
195 let excess = tasks.len() - max_finished;
196 for (_, task_id) in by_age.into_iter().take(excess) {
197 tasks.remove(&task_id);
198 }
199 }
200 }
201}
202
203#[derive(Clone)]
204struct ActiveTask {
205 token: CancellationToken,
206 abort_handle: tokio::task::AbortHandle,
207 completion: Arc<Notify>,
208 context_id: String,
209 owner: Option<String>,
210}
211
212enum StreamTaskMessage {
213 Update(Box<UpdateEvent>),
214 Error(String),
215}
216
217struct StartedTask {
219 result: oneshot::Receiver<adk_core::Result<Task>>,
220 updates: Option<mpsc::Receiver<StreamTaskMessage>>,
221}
222
223#[derive(Clone)]
230pub struct A2aController {
231 config: ServerConfig,
232 agent_card: AgentCard,
233 task_store: Arc<TaskStore>,
234 active_tasks: Arc<Mutex<HashMap<String, ActiveTask>>>,
235}
236
237impl A2aController {
238 pub fn new(config: ServerConfig, base_url: &str) -> Self {
239 Self::build(config, base_url, None)
240 }
241
242 pub fn with_skill_index(
248 config: ServerConfig,
249 base_url: &str,
250 skill_index: Arc<adk_skill::SkillIndex>,
251 ) -> Self {
252 Self::build(config, base_url, Some(skill_index))
253 }
254
255 pub fn with_task_retention(mut self, retention: A2aTaskRetention) -> Self {
268 self.task_store = Arc::new(TaskStore::with_retention(retention));
269 self
270 }
271
272 fn build(
273 config: ServerConfig,
274 base_url: &str,
275 skill_index: Option<Arc<adk_skill::SkillIndex>>,
276 ) -> Self {
277 let root_agent = config.agent_loader.root_agent();
278 let invoke_url = format!("{}/a2a", base_url.trim_end_matches('/'));
279 let mut agent_card = build_agent_card(root_agent.as_ref(), &invoke_url);
280 if let Some(skill_index) = skill_index {
281 let indexed = crate::a2a::agent_skills_from_index(&skill_index);
282 tracing::debug!(skill.count = indexed.len(), "appending indexed skills to agent card");
283 agent_card.skills.extend(indexed);
284 }
285
286 Self {
287 config,
288 agent_card,
289 task_store: Arc::new(TaskStore::new()),
290 active_tasks: Arc::new(Mutex::new(HashMap::new())),
291 }
292 }
293}
294
295fn build_runner_config(
296 controller: &A2aController,
297 root_agent: Arc<dyn adk_core::Agent>,
298 cancellation_token: Option<CancellationToken>,
299 request_context: Option<RequestContext>,
300) -> Arc<RunnerConfig> {
301 let mut builder = Runner::builder()
302 .app_name(root_agent.name())
303 .agent(root_agent)
304 .session_service(controller.config.session_service.clone());
305 if let Some(ref artifact_service) = controller.config.artifact_service {
306 builder = builder.artifact_service(artifact_service.clone());
307 }
308 if let Some(ref memory_service) = controller.config.memory_service {
309 builder = builder.memory_service(memory_service.clone());
310 }
311 if let Some(ref compaction_config) = controller.config.compaction_config {
312 builder = builder.compaction_config(compaction_config.clone());
313 }
314 if let Some(ref context_cache_config) = controller.config.context_cache_config {
315 builder = builder.context_cache_config(context_cache_config.clone());
316 }
317 if let Some(ref cache_capable) = controller.config.cache_capable {
318 builder = builder.cache_capable(cache_capable.clone());
319 }
320 if let Some(cancellation_token) = cancellation_token {
321 builder = builder.cancellation_token(cancellation_token);
322 }
323 if let Some(request_context) = request_context {
324 builder = builder.request_context(request_context);
325 }
326 Arc::new(builder.build_config())
327}
328
329fn build_task_from_events(task_id: &str, context_id: &str, events: &[UpdateEvent]) -> Task {
330 let mut task = Task {
331 id: task_id.to_string(),
332 context_id: Some(context_id.to_string()),
333 status: TaskStatus { state: TaskState::Completed, message: None },
334 artifacts: Some(vec![]),
335 history: None,
336 };
337
338 for event in events {
339 match event {
340 UpdateEvent::TaskStatusUpdate(status) => {
341 task.status = status.status.clone();
342 }
343 UpdateEvent::TaskArtifactUpdate(artifact) => {
344 if let Some(ref mut artifacts) = task.artifacts {
345 artifacts.push(artifact.artifact.clone());
346 }
347 }
348 }
349 }
350
351 task
352}
353
354fn build_failed_task(task_id: &str, context_id: &str, message: impl Into<String>) -> Task {
355 Task {
356 id: task_id.to_string(),
357 context_id: Some(context_id.to_string()),
358 status: TaskStatus { state: TaskState::Failed, message: Some(message.into()) },
359 artifacts: None,
360 history: None,
361 }
362}
363
364fn build_canceled_task(task_id: &str, context_id: &str) -> Task {
365 Task {
366 id: task_id.to_string(),
367 context_id: Some(context_id.to_string()),
368 status: TaskStatus { state: TaskState::Canceled, message: None },
369 artifacts: None,
370 history: None,
371 }
372}
373
374fn sanitize_internal_error(config: &ServerConfig, error: &adk_core::AdkError) -> String {
375 if config.security.expose_error_details {
376 error.to_string()
377 } else {
378 "Internal server error".to_string()
379 }
380}
381
382fn task_not_found(task_id: &str) -> JsonRpcError {
384 JsonRpcError::internal_error(format!("Task not found: {task_id}"))
385}
386
387async fn start_task(
394 controller: &A2aController,
395 caller: AuthenticatedCaller,
396 context_id: String,
397 task_id: String,
398 message: Message,
399 stream_updates: bool,
400) -> Result<StartedTask, JsonRpcError> {
401 let owner = caller.user_id().map(str::to_string);
402
403 let mut active_tasks = controller.active_tasks.lock().await;
407 let reused_by_other = controller
408 .task_store
409 .owner_of(&task_id)
410 .await
411 .is_some_and(|stored_owner| stored_owner != owner);
412 if active_tasks.contains_key(&task_id) || reused_by_other {
413 tracing::warn!(task.id = %task_id, "rejected a2a message with a task id already in use");
414 return Err(JsonRpcError::invalid_params(format!("task id '{task_id}' is already in use")));
415 }
416
417 let token = CancellationToken::new();
418 let completion = Arc::new(Notify::new());
419 let (task_tx, task_rx) = oneshot::channel();
420 let (stream_tx, stream_rx) = if stream_updates {
421 let (tx, rx) = mpsc::channel(32);
422 (Some(tx), Some(rx))
423 } else {
424 (None, None)
425 };
426
427 let root_agent = controller.config.agent_loader.root_agent();
428 let executor = Executor::new(ExecutorConfig {
429 app_name: root_agent.name().to_string(),
430 runner_config: build_runner_config(controller, root_agent, Some(token.clone()), caller.0),
431 cancellation_token: Some(token.clone()),
432 #[cfg(feature = "a2a-interceptors")]
433 interceptor_chain: controller.config.interceptor_chain.clone(),
434 });
435
436 let controller_clone = controller.clone();
437 let completion_clone = completion.clone();
438 let task_id_for_task = task_id.clone();
439 let context_id_for_task = context_id.clone();
440 let owner_for_task = owner.clone();
441
442 let join_handle = tokio::spawn(async move {
443 let result = executor
444 .execute_for_user(
445 owner_for_task.as_deref(),
446 &context_id_for_task,
447 &task_id_for_task,
448 &message,
449 )
450 .await;
451
452 match result {
453 Ok(events) => {
454 if let Some(sender) = stream_tx {
455 for event in &events {
456 if sender
457 .send(StreamTaskMessage::Update(Box::new(event.clone())))
458 .await
459 .is_err()
460 {
461 break;
462 }
463 }
464 }
465
466 let task = build_task_from_events(&task_id_for_task, &context_id_for_task, &events);
467 controller_clone
468 .task_store
469 .store_for(owner_for_task.as_deref(), task.clone())
470 .await;
471 let _ = task_tx.send(Ok(task));
472 }
473 Err(error) => {
474 let message = sanitize_internal_error(&controller_clone.config, &error);
475 if let Some(sender) = stream_tx {
476 let _ = sender.send(StreamTaskMessage::Error(message.clone())).await;
477 }
478 controller_clone
479 .task_store
480 .store_for(
481 owner_for_task.as_deref(),
482 build_failed_task(&task_id_for_task, &context_id_for_task, message),
483 )
484 .await;
485 let _ = task_tx.send(Err(error));
486 }
487 }
488
489 controller_clone.active_tasks.lock().await.remove(&task_id_for_task);
490 completion_clone.notify_waiters();
491 });
492
493 active_tasks.insert(
494 task_id,
495 ActiveTask {
496 token,
497 abort_handle: join_handle.abort_handle(),
498 completion,
499 context_id,
500 owner,
501 },
502 );
503
504 Ok(StartedTask { result: task_rx, updates: stream_rx })
505}
506
507pub async fn get_agent_card(State(controller): State<A2aController>) -> impl IntoResponse {
509 Json(controller.agent_card.clone())
510}
511
512#[deprecated(
518 note = "ignores the authenticated caller; mount `handle_jsonrpc_for_caller`, which the built-in routers use"
519)]
520pub async fn handle_jsonrpc(
521 State(controller): State<A2aController>,
522 Json(request): Json<JsonRpcRequest>,
523) -> impl IntoResponse {
524 handle_jsonrpc_for_caller(State(controller), AuthenticatedCaller::default(), Json(request))
525 .await
526}
527
528pub async fn handle_jsonrpc_for_caller(
535 State(controller): State<A2aController>,
536 caller: AuthenticatedCaller,
537 Json(request): Json<JsonRpcRequest>,
538) -> Json<JsonRpcResponse> {
539 if request.jsonrpc != "2.0" {
540 return Json(JsonRpcResponse::error(
541 request.id,
542 JsonRpcError::invalid_request("Invalid JSON-RPC version"),
543 ));
544 }
545
546 match request.method.as_str() {
547 jsonrpc::methods::MESSAGE_SEND => {
548 handle_message_send(&controller, caller, request.params, request.id).await
549 }
550 jsonrpc::methods::TASKS_GET => {
551 handle_tasks_get(&controller, &caller, request.params, request.id).await
552 }
553 jsonrpc::methods::TASKS_CANCEL => {
554 handle_tasks_cancel(&controller, &caller, request.params, request.id).await
555 }
556 _ => Json(JsonRpcResponse::error(
557 request.id,
558 JsonRpcError::method_not_found(&request.method),
559 )),
560 }
561}
562
563#[deprecated(
567 note = "ignores the authenticated caller; mount `handle_jsonrpc_stream_for_caller`, which the built-in routers use"
568)]
569pub async fn handle_jsonrpc_stream(
570 State(controller): State<A2aController>,
571 Json(request): Json<JsonRpcRequest>,
572) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, (StatusCode, Json<JsonRpcResponse>)>
573{
574 handle_jsonrpc_stream_for_caller(
575 State(controller),
576 AuthenticatedCaller::default(),
577 Json(request),
578 )
579 .await
580}
581
582pub async fn handle_jsonrpc_stream_for_caller(
587 State(controller): State<A2aController>,
588 caller: AuthenticatedCaller,
589 Json(request): Json<JsonRpcRequest>,
590) -> Result<Sse<impl Stream<Item = Result<Event, Infallible>>>, (StatusCode, Json<JsonRpcResponse>)>
591{
592 let bad_request = |error: JsonRpcError| {
593 (StatusCode::BAD_REQUEST, Json(JsonRpcResponse::error(request.id.clone(), error)))
594 };
595
596 if request.jsonrpc != "2.0" {
597 return Err(bad_request(JsonRpcError::invalid_request("Invalid JSON-RPC version")));
598 }
599
600 if request.method != jsonrpc::methods::MESSAGE_SEND_STREAM
601 && request.method != jsonrpc::methods::MESSAGE_SEND
602 {
603 return Err(bad_request(JsonRpcError::method_not_found(&request.method)));
604 }
605
606 let params: MessageSendParams = match request.params.clone() {
607 Some(p) => serde_json::from_value(p)
608 .map_err(|e| bad_request(JsonRpcError::invalid_params(e.to_string())))?,
609 None => return Err(bad_request(JsonRpcError::invalid_params("Missing params"))),
610 };
611
612 let context_id =
613 params.message.context_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
614 let task_id =
615 params.message.task_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
616
617 let started = start_task(&controller, caller, context_id, task_id, params.message, true)
618 .await
619 .map_err(bad_request)?;
620
621 let stream = create_message_stream(started.updates, request.id.clone());
622
623 Ok(Sse::new(stream).keep_alive(
624 axum::response::sse::KeepAlive::new().interval(Duration::from_secs(15)).text("ping"),
625 ))
626}
627
628fn create_message_stream(
629 updates: Option<mpsc::Receiver<StreamTaskMessage>>,
630 request_id: Option<Value>,
631) -> impl Stream<Item = Result<Event, Infallible>> {
632 async_stream::stream! {
633 let Some(mut stream_rx) = updates else {
634 yield Ok(Event::default().event("done").data(""));
635 return;
636 };
637
638 while let Some(message) = stream_rx.recv().await {
639 match message {
640 StreamTaskMessage::Update(event) => {
641 let event_data = match event.as_ref() {
642 UpdateEvent::TaskStatusUpdate(status) => {
643 serde_json::to_string(&JsonRpcResponse::success(
644 request_id.clone(),
645 serde_json::to_value(status).unwrap_or_default(),
646 ))
647 }
648 UpdateEvent::TaskArtifactUpdate(artifact) => {
649 serde_json::to_string(&JsonRpcResponse::success(
650 request_id.clone(),
651 serde_json::to_value(artifact).unwrap_or_default(),
652 ))
653 }
654 };
655
656 if let Ok(data) = event_data {
657 yield Ok(Event::default().data(data));
658 }
659 }
660 StreamTaskMessage::Error(message) => {
661 let error_response = JsonRpcResponse::error(
662 request_id.clone(),
663 JsonRpcError::internal_error(message),
664 );
665 if let Ok(data) = serde_json::to_string(&error_response) {
666 yield Ok(Event::default().data(data));
667 }
668 }
669 }
670 }
671
672 yield Ok(Event::default().event("done").data(""));
674 }
675}
676
677async fn handle_message_send(
678 controller: &A2aController,
679 caller: AuthenticatedCaller,
680 params: Option<Value>,
681 id: Option<Value>,
682) -> Json<JsonRpcResponse> {
683 let params: MessageSendParams = match params {
684 Some(p) => match serde_json::from_value(p) {
685 Ok(p) => p,
686 Err(e) => {
687 return Json(JsonRpcResponse::error(
688 id,
689 JsonRpcError::invalid_params(e.to_string()),
690 ));
691 }
692 },
693 None => {
694 return Json(JsonRpcResponse::error(
695 id,
696 JsonRpcError::invalid_params("Missing params"),
697 ));
698 }
699 };
700
701 let context_id =
702 params.message.context_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
703 let task_id =
704 params.message.task_id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
705
706 let started =
707 match start_task(controller, caller, context_id, task_id, params.message, false).await {
708 Ok(started) => started,
709 Err(error) => return Json(JsonRpcResponse::error(id, error)),
710 };
711
712 match started.result.await {
713 Ok(Ok(task)) => {
714 Json(JsonRpcResponse::success(id, serde_json::to_value(task).unwrap_or_default()))
715 }
716 Ok(Err(e)) => Json(JsonRpcResponse::error(
717 id,
718 JsonRpcError::internal_error_sanitized(
719 &e,
720 controller.config.security.expose_error_details,
721 ),
722 )),
723 Err(_) => {
724 Json(JsonRpcResponse::error(id, JsonRpcError::internal_error("Task execution aborted")))
725 }
726 }
727}
728
729async fn handle_tasks_get(
730 controller: &A2aController,
731 caller: &AuthenticatedCaller,
732 params: Option<Value>,
733 id: Option<Value>,
734) -> Json<JsonRpcResponse> {
735 let params: TasksGetParams = match params {
736 Some(p) => match serde_json::from_value(p) {
737 Ok(p) => p,
738 Err(e) => {
739 return Json(JsonRpcResponse::error(
740 id,
741 JsonRpcError::invalid_params(e.to_string()),
742 ));
743 }
744 },
745 None => {
746 return Json(JsonRpcResponse::error(
747 id,
748 JsonRpcError::invalid_params("Missing params"),
749 ));
750 }
751 };
752
753 let owner = caller.user_id();
754 let active_task = controller
755 .active_tasks
756 .lock()
757 .await
758 .get(¶ms.task_id)
759 .filter(|task| task.owner.as_deref() == owner)
760 .cloned();
761 if let Some(active_task) = active_task {
762 let task = Task {
763 id: params.task_id.clone(),
764 context_id: Some(active_task.context_id),
765 status: TaskStatus { state: TaskState::Working, message: None },
766 artifacts: None,
767 history: None,
768 };
769
770 return Json(JsonRpcResponse::success(id, serde_json::to_value(task).unwrap_or_default()));
771 }
772
773 match controller.task_store.get_for(owner, ¶ms.task_id).await {
774 Some(task) => {
775 Json(JsonRpcResponse::success(id, serde_json::to_value(task).unwrap_or_default()))
776 }
777 None => Json(JsonRpcResponse::error(id, task_not_found(¶ms.task_id))),
778 }
779}
780
781async fn handle_tasks_cancel(
782 controller: &A2aController,
783 caller: &AuthenticatedCaller,
784 params: Option<Value>,
785 id: Option<Value>,
786) -> Json<JsonRpcResponse> {
787 let params: TasksCancelParams = match params {
788 Some(p) => match serde_json::from_value(p) {
789 Ok(p) => p,
790 Err(e) => {
791 return Json(JsonRpcResponse::error(
792 id,
793 JsonRpcError::invalid_params(e.to_string()),
794 ));
795 }
796 },
797 None => {
798 return Json(JsonRpcResponse::error(
799 id,
800 JsonRpcError::invalid_params("Missing params"),
801 ));
802 }
803 };
804
805 let owner = caller.user_id();
806 let active_task = controller
807 .active_tasks
808 .lock()
809 .await
810 .get(¶ms.task_id)
811 .filter(|task| task.owner.as_deref() == owner)
812 .cloned();
813
814 if let Some(active_task) = active_task {
815 active_task.token.cancel();
816
817 if tokio::time::timeout(Duration::from_secs(5), active_task.completion.notified())
818 .await
819 .is_err()
820 {
821 active_task.abort_handle.abort();
822 controller.active_tasks.lock().await.remove(¶ms.task_id);
823 controller
824 .task_store
825 .store_for(
826 active_task.owner.as_deref(),
827 build_canceled_task(¶ms.task_id, &active_task.context_id),
828 )
829 .await;
830 }
831
832 let status = TaskStatusUpdateEvent {
833 task_id: params.task_id,
834 context_id: Some(active_task.context_id),
835 status: TaskStatus { state: TaskState::Canceled, message: None },
836 final_update: true,
837 };
838
839 return Json(JsonRpcResponse::success(
840 id,
841 serde_json::to_value(status).unwrap_or_default(),
842 ));
843 }
844
845 let Some(stored) = controller.task_store.get_for(owner, ¶ms.task_id).await else {
846 return Json(JsonRpcResponse::error(id, task_not_found(¶ms.task_id)));
847 };
848
849 let status = TaskStatusUpdateEvent {
850 task_id: params.task_id,
851 context_id: stored.context_id,
852 status: TaskStatus { state: TaskState::Canceled, message: None },
853 final_update: true,
854 };
855
856 Json(JsonRpcResponse::success(id, serde_json::to_value(status).unwrap_or_default()))
857}
858#[cfg(test)]
859mod tests {
860 use super::*;
861 use adk_core::{Agent, EventStream, InvocationContext, Result as AdkResult, SingleAgentLoader};
862 use adk_session::InMemorySessionService;
863 use async_trait::async_trait;
864 use futures::stream;
865
866 struct TestAgent;
867
868 #[async_trait]
869 impl Agent for TestAgent {
870 fn name(&self) -> &str {
871 "card_agent"
872 }
873
874 fn description(&self) -> &str {
875 "A card test agent"
876 }
877
878 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
879 &[]
880 }
881
882 async fn run(&self, _ctx: Arc<dyn InvocationContext>) -> AdkResult<EventStream> {
883 Ok(Box::pin(stream::empty()))
884 }
885 }
886
887 fn test_config() -> ServerConfig {
888 let agent_loader = Arc::new(SingleAgentLoader::new(Arc::new(TestAgent)));
889 let session_service = Arc::new(InMemorySessionService::new());
890 ServerConfig::new(agent_loader, session_service)
891 }
892
893 fn skill_doc(name: &str) -> adk_skill::SkillDocument {
894 adk_skill::SkillDocument {
895 id: format!("{name}-0123456789ab"),
896 name: name.to_string(),
897 description: format!("{name} description"),
898 version: None,
899 license: None,
900 compatibility: None,
901 tags: vec!["indexed".to_string()],
902 allowed_tools: vec![],
903 references: vec![],
904 trigger: false,
905 hint: None,
906 metadata: Default::default(),
907 body: String::new(),
908 path: format!("skills/{name}.skill.md").into(),
909 hash: "0123456789ab".to_string(),
910 last_modified: None,
911 triggers: vec![],
912 }
913 }
914
915 #[test]
916 fn with_skill_index_appends_indexed_skills_to_card() {
917 let index = Arc::new(adk_skill::SkillIndex::new(vec![
918 skill_doc("skill-one"),
919 skill_doc("skill-two"),
920 ]));
921
922 let controller =
923 A2aController::with_skill_index(test_config(), "http://localhost:8080", index);
924
925 let expected = serde_json::json!([
926 {
927 "id": "card_agent",
928 "name": "card_agent",
929 "description": "A card test agent",
930 "tags": ["agent"],
931 },
932 {
933 "id": "skill-one",
934 "name": "skill-one",
935 "description": "skill-one description",
936 "tags": ["indexed"],
937 },
938 {
939 "id": "skill-two",
940 "name": "skill-two",
941 "description": "skill-two description",
942 "tags": ["indexed"],
943 },
944 ]);
945 assert_eq!(serde_json::to_value(&controller.agent_card.skills).unwrap(), expected);
946 }
947
948 #[test]
949 fn new_leaves_card_skills_agent_derived() {
950 let controller = A2aController::new(test_config(), "http://localhost:8080");
951
952 let expected = serde_json::json!([
953 {
954 "id": "card_agent",
955 "name": "card_agent",
956 "description": "A card test agent",
957 "tags": ["agent"],
958 },
959 ]);
960 assert_eq!(serde_json::to_value(&controller.agent_card.skills).unwrap(), expected);
961 }
962
963 struct GatedAgent {
967 gate: Arc<Notify>,
968 }
969
970 #[async_trait]
971 impl Agent for GatedAgent {
972 fn name(&self) -> &str {
973 "gated_agent"
974 }
975
976 fn description(&self) -> &str {
977 "replies when released"
978 }
979
980 fn sub_agents(&self) -> &[Arc<dyn Agent>] {
981 &[]
982 }
983
984 async fn run(&self, ctx: Arc<dyn InvocationContext>) -> AdkResult<EventStream> {
985 let gate = self.gate.clone();
986 let invocation_id = ctx.invocation_id().to_string();
987 Ok(Box::pin(async_stream::stream! {
988 gate.notified().await;
989 let mut event = adk_core::Event::new(invocation_id);
990 event.author = "gated_agent".to_string();
991 event.llm_response.content =
992 Some(adk_core::Content::new("model").with_text("released"));
993 yield Ok(event);
994 }))
995 }
996 }
997
998 fn controller_with(agent: Arc<dyn Agent>) -> A2aController {
999 let agent_loader = Arc::new(SingleAgentLoader::new(agent));
1000 let config = ServerConfig::new(agent_loader, Arc::new(InMemorySessionService::new()));
1001 A2aController::new(config, "http://localhost:8080")
1002 }
1003
1004 fn text_message(text: &str) -> Message {
1005 Message::builder()
1006 .role(crate::a2a::Role::User)
1007 .parts(vec![crate::a2a::Part::text(text.to_string())])
1008 .message_id(uuid::Uuid::new_v4().to_string())
1009 .build()
1010 }
1011
1012 fn unconvertible_message() -> Message {
1014 let mut data = serde_json::Map::new();
1015 data.insert("not_a_function_call".to_string(), serde_json::json!("db-password=hunter2"));
1016 Message::builder()
1017 .role(crate::a2a::Role::User)
1018 .parts(vec![crate::a2a::Part::Data { data, metadata: None }])
1019 .message_id(uuid::Uuid::new_v4().to_string())
1020 .build()
1021 }
1022
1023 fn finished_task(task_id: &str) -> Task {
1024 build_task_from_events(task_id, "ctx", &[])
1025 }
1026
1027 #[tokio::test]
1028 async fn store_evicts_the_oldest_records_beyond_max_finished() {
1029 let store = TaskStore::with_retention(A2aTaskRetention::unlimited().with_max_finished(2));
1030 for task_id in ["t1", "t2", "t3"] {
1031 store.store(finished_task(task_id)).await;
1032 }
1033
1034 assert_eq!(store.len().await, 2);
1035 assert!(store.get("t1").await.is_none(), "the oldest record is evicted first");
1036 assert!(store.get("t2").await.is_some());
1037 assert!(store.get("t3").await.is_some());
1038 }
1039
1040 #[tokio::test]
1041 async fn store_expires_records_after_their_ttl() {
1042 let store = TaskStore::with_retention(
1043 A2aTaskRetention::unlimited().with_ttl(Duration::from_secs(60)),
1044 );
1045 let long_ago = Instant::now().checked_sub(Duration::from_secs(120)).unwrap();
1046 store.tasks.write().await.insert(
1047 "old".to_string(),
1048 StoredTask { task: finished_task("old"), owner: None, finished_at: long_ago },
1049 );
1050
1051 assert!(store.get("old").await.is_none(), "an expired record is not readable");
1052
1053 store.store(finished_task("new")).await;
1054 assert_eq!(store.len().await, 1, "storing a record evicts expired ones");
1055 assert!(store.get("new").await.is_some());
1056 }
1057
1058 #[tokio::test]
1059 async fn store_reads_are_scoped_to_the_owner() {
1060 let store = TaskStore::new();
1061 store.store_for(Some("alice"), finished_task("t1")).await;
1062
1063 assert!(store.get_for(Some("bob"), "t1").await.is_none());
1064 assert!(store.get_for(None, "t1").await.is_none());
1065 assert_eq!(
1066 serde_json::to_value(store.get_for(Some("alice"), "t1").await).unwrap(),
1067 serde_json::to_value(finished_task("t1")).unwrap()
1068 );
1069 }
1070
1071 #[tokio::test]
1072 async fn duplicate_task_id_is_rejected_and_the_running_task_keeps_its_handle() {
1073 let gate = Arc::new(Notify::new());
1074 let controller = controller_with(Arc::new(GatedAgent { gate: gate.clone() }));
1075 let caller = AuthenticatedCaller::default();
1076
1077 let first = start_task(
1078 &controller,
1079 caller.clone(),
1080 "ctx".to_string(),
1081 "dup".to_string(),
1082 text_message("one"),
1083 false,
1084 )
1085 .await
1086 .expect("the first task starts");
1087 let original_handle = controller.active_tasks.lock().await["dup"].abort_handle.id();
1088
1089 let duplicate = start_task(
1090 &controller,
1091 caller,
1092 "ctx".to_string(),
1093 "dup".to_string(),
1094 text_message("two"),
1095 false,
1096 )
1097 .await;
1098
1099 let error = duplicate.err().expect("a running task id cannot be reused");
1100 assert_eq!(
1101 serde_json::to_value(error).unwrap(),
1102 serde_json::json!({ "code": -32602, "message": "task id 'dup' is already in use" })
1103 );
1104 assert_eq!(
1105 controller.active_tasks.lock().await["dup"].abort_handle.id(),
1106 original_handle,
1107 "the running task's registration must not be replaced"
1108 );
1109
1110 gate.notify_one();
1111 let task = first.result.await.unwrap().unwrap();
1112 assert_eq!(task.status.state, TaskState::Completed);
1113 }
1114
1115 #[tokio::test]
1116 async fn finished_task_id_of_another_owner_cannot_be_reused() {
1117 let controller = controller_with(Arc::new(TestAgent));
1118 controller.task_store.store_for(Some("bob"), finished_task("bobs-task")).await;
1119
1120 let alice = AuthenticatedCaller(Some(RequestContext {
1121 user_id: "alice".to_string(),
1122 scopes: vec![],
1123 metadata: Default::default(),
1124 }));
1125 let attempt = start_task(
1126 &controller,
1127 alice,
1128 "ctx".to_string(),
1129 "bobs-task".to_string(),
1130 text_message("overwrite"),
1131 false,
1132 )
1133 .await;
1134
1135 assert!(attempt.is_err(), "another owner's record must not be overwritten");
1136 assert!(controller.task_store.get_for(Some("bob"), "bobs-task").await.is_some());
1137 }
1138
1139 #[tokio::test]
1140 async fn failed_task_record_carries_the_sanitized_error() {
1141 let controller = controller_with(Arc::new(TestAgent));
1142
1143 let started = start_task(
1144 &controller,
1145 AuthenticatedCaller::default(),
1146 "ctx".to_string(),
1147 "failing".to_string(),
1148 unconvertible_message(),
1149 false,
1150 )
1151 .await
1152 .unwrap();
1153 assert!(started.result.await.unwrap().is_err());
1154
1155 let stored = controller.task_store.get_for(None, "failing").await.unwrap();
1156 assert_eq!(
1157 serde_json::to_value(stored).unwrap(),
1158 serde_json::to_value(build_failed_task("failing", "ctx", "Internal server error"))
1159 .unwrap()
1160 );
1161 }
1162
1163 #[tokio::test]
1164 async fn fast_failing_tasks_leave_no_working_entry_behind() {
1165 let controller = controller_with(Arc::new(TestAgent));
1166
1167 for attempt in 0..50 {
1168 let task_id = format!("fast-{attempt}");
1169 let started = start_task(
1170 &controller,
1171 AuthenticatedCaller::default(),
1172 "ctx".to_string(),
1173 task_id.clone(),
1174 unconvertible_message(),
1175 false,
1176 )
1177 .await
1178 .unwrap();
1179 let _ = started.result.await;
1180
1181 for _ in 0..100 {
1183 if !controller.active_tasks.lock().await.contains_key(&task_id) {
1184 break;
1185 }
1186 tokio::task::yield_now().await;
1187 }
1188 assert!(
1189 !controller.active_tasks.lock().await.contains_key(&task_id),
1190 "a finished task must not stay registered as running"
1191 );
1192 }
1193 }
1194}