Skip to main content

adk_server/rest/controllers/
a2a.rs

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/// How many finished A2A task records a [`TaskStore`] keeps, and for how long.
29///
30/// A record is kept so `tasks/get` can report the outcome after `message/send`
31/// returns. Every record in the store belongs to an execution that has ended —
32/// running tasks are tracked separately and are never evicted.
33///
34/// The default keeps the newest 1,000 records for at most one hour.
35///
36/// # Example
37///
38/// ```rust
39/// use adk_server::A2aTaskRetention;
40/// use std::time::Duration;
41///
42/// let retention =
43///     A2aTaskRetention::default().with_max_finished(500).with_ttl(Duration::from_secs(600));
44/// assert_eq!(retention.max_finished, Some(500));
45/// assert_eq!(retention.ttl, Some(Duration::from_secs(600)));
46/// ```
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub struct A2aTaskRetention {
49    /// How many finished task records to keep, newest first. `None` keeps every record.
50    pub max_finished: Option<usize>,
51    /// How long a finished task record stays readable. `None` disables expiry.
52    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    /// Keeps every finished task record forever. The store then grows without bound.
63    pub fn unlimited() -> Self {
64        Self { max_finished: None, ttl: None }
65    }
66
67    /// Keeps at most `count` finished task records, evicting the oldest first.
68    pub fn with_max_finished(mut self, count: usize) -> Self {
69        self.max_finished = Some(count);
70        self
71    }
72
73    /// Expires a finished task record `ttl` after its execution ended.
74    pub fn with_ttl(mut self, ttl: Duration) -> Self {
75        self.ttl = Some(ttl);
76        self
77    }
78}
79
80/// A finished task, the principal that started it, and when it ended.
81struct StoredTask {
82    task: Task,
83    owner: Option<String>,
84    finished_at: Instant,
85}
86
87/// In-memory storage for finished A2A tasks.
88///
89/// Each record carries the authenticated user that started the task, or `None`
90/// when no authentication was configured. Owner-scoped reads
91/// ([`get_for`](Self::get_for)) only return a record to the same principal.
92/// Records are evicted according to the store's [`A2aTaskRetention`].
93pub 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    /// Creates a store with the default [`A2aTaskRetention`].
106    pub fn new() -> Self {
107        Self::default()
108    }
109
110    /// Creates a store that evicts records according to `retention`.
111    pub fn with_retention(retention: A2aTaskRetention) -> Self {
112        Self { tasks: RwLock::new(HashMap::new()), retention }
113    }
114
115    /// Stores `task` without an owner.
116    ///
117    /// An ownerless record is visible only to unauthenticated callers. Use
118    /// [`store_for`](Self::store_for) to record the principal that started it.
119    pub async fn store(&self, task: Task) {
120        self.store_for(None, task).await;
121    }
122
123    /// Stores `task` as owned by `owner`, then applies the retention policy.
124    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    /// Returns a stored task regardless of its owner.
134    ///
135    /// This is for programmatic access by the server operator; request handlers
136    /// use [`get_for`](Self::get_for).
137    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    /// Returns a stored task only when it belongs to `owner`.
146    ///
147    /// A task owned by someone else is reported exactly like a missing one, so the
148    /// response does not disclose that the ID exists.
149    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    /// Removes a stored task regardless of its owner.
158    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    /// Returns the number of records held, including expired ones not yet evicted.
163    pub async fn len(&self) -> usize {
164        self.tasks.read().await.len()
165    }
166
167    /// Returns `true` when the store holds no records.
168    pub async fn is_empty(&self) -> bool {
169        self.tasks.read().await.is_empty()
170    }
171
172    /// The owner of an unexpired record, or `None` when there is no such record.
173    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
217/// A task that was registered and spawned.
218struct StartedTask {
219    result: oneshot::Receiver<adk_core::Result<Task>>,
220    updates: Option<mpsc::Receiver<StreamTaskMessage>>,
221}
222
223/// Controller for A2A protocol endpoints.
224///
225/// When the request carries an authenticated principal, the principal owns the
226/// session and every task it starts: the session user is the principal's
227/// `user_id`, and `tasks/get` and `tasks/cancel` only reach that principal's
228/// tasks. Without authentication the session user is `A2A_USER_{contextId}`.
229#[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    /// Create a controller whose agent card also lists the skills in `skill_index`.
243    ///
244    /// The indexed skills are appended to the agent-derived `skills[]` entries
245    /// via [`agent_skills_from_index`](crate::a2a::agent_skills_from_index) and
246    /// served at `/.well-known/agent.json`.
247    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    /// Replaces the bounds on retained finished-task records.
256    ///
257    /// Call this before the controller serves requests: it starts a fresh store.
258    ///
259    /// # Example
260    ///
261    /// ```rust,ignore
262    /// use adk_server::{A2aController, A2aTaskRetention};
263    ///
264    /// let controller = A2aController::new(config, "http://localhost:8080")
265    ///     .with_task_retention(A2aTaskRetention::default().with_max_finished(100));
266    /// ```
267    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
382/// The error for a task the caller cannot see, whether missing or owned by another user.
383fn task_not_found(task_id: &str) -> JsonRpcError {
384    JsonRpcError::internal_error(format!("Task not found: {task_id}"))
385}
386
387/// Registers and spawns a task on behalf of `caller`.
388///
389/// # Errors
390///
391/// Returns an `invalid_params` JSON-RPC error when `task_id` names a running task,
392/// or a finished task owned by another principal.
393async 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    // The registry lock is held from the duplicate check until the entry is inserted.
404    // The spawned task removes its entry under the same lock, so it cannot finish
405    // before it is registered and leave a permanent `Working` entry behind.
406    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
507/// GET /.well-known/agent.json - Serve the agent card
508pub async fn get_agent_card(State(controller): State<A2aController>) -> impl IntoResponse {
509    Json(controller.agent_card.clone())
510}
511
512/// POST /a2a - JSON-RPC endpoint for A2A protocol, without caller identity.
513///
514/// Every request is treated as unauthenticated: sessions belong to
515/// `A2A_USER_{contextId}` and only ownerless tasks are visible, even when an
516/// authentication layer ran in front of the route.
517#[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
528/// POST /a2a - JSON-RPC endpoint for the A2A protocol.
529///
530/// The authenticated principal, when present, owns the session and the tasks the
531/// request starts. `tasks/get` and `tasks/cancel` answer "not found" for a task
532/// owned by anyone else. Without authentication the session user is
533/// `A2A_USER_{contextId}`.
534pub 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/// POST /a2a/stream - SSE streaming endpoint for A2A protocol, without caller identity.
564///
565/// Every request is treated as unauthenticated; see [`handle_jsonrpc`].
566#[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
582/// POST /a2a/stream - SSE streaming endpoint for the A2A protocol.
583///
584/// Binds the session and the started task to the authenticated principal, as
585/// [`handle_jsonrpc_for_caller`] does.
586pub 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        // Send done event
673        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(&params.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, &params.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(&params.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(&params.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(&params.task_id);
823            controller
824                .task_store
825                .store_for(
826                    active_task.owner.as_deref(),
827                    build_canceled_task(&params.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, &params.task_id).await else {
846        return Json(JsonRpcResponse::error(id, task_not_found(&params.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    // ── Task store hygiene ─────────────────────────────────────────────────
964
965    /// Replies once `gate` is notified, so a test controls when a task finishes.
966    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    /// A data part the converter does not understand, so execution fails before the agent runs.
1013    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            // The spawned task removes its entry after reporting; wait for that step.
1182            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}