Skip to main content

rust_web_server/a2a/
mod.rs

1//! A2A (Agent2Agent) 1.0 server, JSON-RPC binding (18.17.0, #65): other
2//! agents discover this one by its Agent Card at
3//! `/.well-known/agent-card.json` and talk to it with `SendMessage`,
4//! `GetTask`, `ListTasks`, `CancelTask`.
5//!
6//! ```rust
7//! use rust_web_server::a2a::{A2aReply, A2aServer, AgentCard, Artifact, Message, Skill};
8//!
9//! let agent = A2aServer::new(
10//!     AgentCard::new("Echo agent", "Repeats what you say", "1.0.0")
11//!         .skill(Skill::new("echo", "Echo", "Repeats the message").tag("demo")),
12//! )
13//! .on_message(|ctx, message| {
14//!     if message.text().is_empty() {
15//!         return A2aReply::Message(Message::agent_text("Say something."));
16//!     }
17//!     A2aReply::Task(ctx.complete(vec![Artifact::text("echo", message.text())]))
18//! });
19//! ```
20//!
21//! Mount it beside your routes with [`A2aServer::wrap`]; it answers the
22//! card and `POST` at its path (default `/a2a`) and passes everything else
23//! on.
24
25mod model;
26mod push;
27mod tasks;
28
29pub use model::{Artifact, Message, Part, PartContent, Role, Task, TaskState, TaskStatus};
30pub use tasks::TaskHandle;
31
32use std::sync::Arc;
33use std::time::Duration;
34
35use tasks::Shared;
36
37use model::{quote, to_json, Obj};
38
39use crate::application::Application;
40use crate::request::Request;
41use crate::response::{Response, STATUS_CODE_REASON_PHRASE};
42use crate::server::ConnectionInfo;
43use crate::service_discovery::json_lite::{self, JsonValue};
44
45/// The protocol version served (`A2A-Version`).
46pub const PROTOCOL_VERSION: &str = "1.0";
47/// Where the Agent Card is served.
48pub const AGENT_CARD_PATH: &str = "/.well-known/agent-card.json";
49/// Request bodies larger than this are refused.
50pub const MAX_BODY: usize = 4 * 1024 * 1024;
51/// Tasks kept (oldest dropped first).
52pub const MAX_TASKS: usize = 10_000;
53
54/// JSON text of a parsed value (for the A2A gateway route).
55pub(crate) fn json_text(value: &JsonValue) -> String {
56    to_json(value)
57}
58
59/// A JSON string literal.
60pub(crate) fn quote_json(s: &str) -> String {
61    quote(s)
62}
63
64/// A random UUID v4.
65pub(crate) fn new_id() -> String {
66    use std::hash::{BuildHasher, Hasher};
67    let half = || {
68        let mut h = std::collections::hash_map::RandomState::new().build_hasher();
69        h.write_u128(std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default().as_nanos());
70        h.finish()
71    };
72    let (a, b) = (half(), half());
73    let bytes: Vec<u8> = a.to_be_bytes().into_iter().chain(b.to_be_bytes()).collect();
74    let hex: String = bytes
75        .iter()
76        .enumerate()
77        .map(|(i, byte)| match i {
78            6 => format!("{:02x}", (byte & 0x0f) | 0x40),
79            8 => format!("{:02x}", (byte & 0x3f) | 0x80),
80            _ => format!("{byte:02x}"),
81        })
82        .collect();
83    format!("{}-{}-{}-{}-{}", &hex[0..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..32])
84}
85
86// ── Agent Card ───────────────────────────────────────────────────────────────
87
88/// Something the agent can do, as listed on its card.
89#[derive(Debug, Clone, PartialEq)]
90pub struct Skill {
91    pub id: String,
92    pub name: String,
93    pub description: String,
94    pub tags: Vec<String>,
95    pub examples: Vec<String>,
96    pub input_modes: Vec<String>,
97    pub output_modes: Vec<String>,
98}
99
100impl Skill {
101    pub fn new(id: &str, name: &str, description: &str) -> Skill {
102        Skill { id: id.into(), name: name.into(), description: description.into(), tags: Vec::new(), examples: Vec::new(), input_modes: Vec::new(), output_modes: Vec::new() }
103    }
104
105    pub fn tag(mut self, tag: &str) -> Skill {
106        self.tags.push(tag.into());
107        self
108    }
109
110    pub fn example(mut self, example: &str) -> Skill {
111        self.examples.push(example.into());
112        self
113    }
114
115    fn to_json(&self) -> String {
116        let mut o = Obj::new().str("id", &self.id).str("name", &self.name).str("description", &self.description).strs("tags", &self.tags);
117        if !self.examples.is_empty() {
118            o = o.strs("examples", &self.examples);
119        }
120        if !self.input_modes.is_empty() {
121            o = o.strs("inputModes", &self.input_modes);
122        }
123        if !self.output_modes.is_empty() {
124            o = o.strs("outputModes", &self.output_modes);
125        }
126        o.done()
127    }
128}
129
130/// The agent's self-description, served at [`AGENT_CARD_PATH`].
131#[derive(Debug, Clone, PartialEq)]
132pub struct AgentCard {
133    pub name: String,
134    pub description: String,
135    pub version: String,
136    /// The JSON-RPC endpoint's public URL; `None`: from the request's
137    /// `Host` and the server's path.
138    pub url: Option<String>,
139    pub skills: Vec<Skill>,
140    pub default_input_modes: Vec<String>,
141    pub default_output_modes: Vec<String>,
142    pub provider: Option<(String, String)>,
143    pub documentation_url: Option<String>,
144    pub icon_url: Option<String>,
145}
146
147impl AgentCard {
148    pub fn new(name: &str, description: &str, version: &str) -> AgentCard {
149        AgentCard {
150            name: name.into(),
151            description: description.into(),
152            version: version.into(),
153            url: None,
154            skills: Vec::new(),
155            default_input_modes: vec!["text/plain".into()],
156            default_output_modes: vec!["text/plain".into()],
157            provider: None,
158            documentation_url: None,
159            icon_url: None,
160        }
161    }
162
163    pub fn skill(mut self, skill: Skill) -> AgentCard {
164        self.skills.push(skill);
165        self
166    }
167
168    /// The public URL of the JSON-RPC endpoint (behind a proxy, say).
169    pub fn url(mut self, url: &str) -> AgentCard {
170        self.url = Some(url.into());
171        self
172    }
173
174    /// Media types accepted and produced (default `text/plain`).
175    pub fn modes(mut self, input: &[&str], output: &[&str]) -> AgentCard {
176        self.default_input_modes = input.iter().map(|s| s.to_string()).collect();
177        self.default_output_modes = output.iter().map(|s| s.to_string()).collect();
178        self
179    }
180
181    pub fn provider(mut self, organization: &str, url: &str) -> AgentCard {
182        self.provider = Some((organization.into(), url.into()));
183        self
184    }
185
186    pub fn documentation_url(mut self, url: &str) -> AgentCard {
187        self.documentation_url = Some(url.into());
188        self
189    }
190
191    pub(crate) fn to_json(&self, url: &str, (bearer, push, extended): (bool, bool, bool)) -> String {
192        let interface = Obj::new().str("url", url).str("protocolBinding", "JSONRPC").str("protocolVersion", PROTOCOL_VERSION).done();
193        let capabilities = format!(r#"{{"streaming":true,"pushNotifications":{push},"extendedAgentCard":{extended}}}"#);
194        let capabilities = capabilities.as_str();
195        let skills: Vec<String> = self.skills.iter().map(Skill::to_json).collect();
196        let provider = self.provider.as_ref().map(|(org, url)| Obj::new().str("organization", org).str("url", url).done());
197        Obj::new()
198            .str("name", &self.name)
199            .str("description", &self.description)
200            .str("version", &self.version)
201            .list("supportedInterfaces", &[interface])
202            .raw("capabilities", capabilities)
203            .strs("defaultInputModes", &self.default_input_modes)
204            .strs("defaultOutputModes", &self.default_output_modes)
205            .list("skills", &skills)
206            .opt_raw("provider", provider.as_deref())
207            .opt_str("documentationUrl", self.documentation_url.as_deref())
208            .opt_str("iconUrl", self.icon_url.as_deref())
209            .opt_raw("securitySchemes", bearer.then_some(r#"{"bearer":{"httpAuthSecurityScheme":{"scheme":"Bearer"}}}"#))
210            .opt_raw("securityRequirements", bearer.then_some(r#"[{"schemes":{"bearer":{"list":[]}}}]"#))
211            .done()
212    }
213}
214
215// ── Handler API ──────────────────────────────────────────────────────────────
216
217/// What a message handler sees besides the message.
218pub struct A2aContext {
219    task_id: String,
220    context_id: String,
221    user_message: Message,
222    shared: Arc<Shared>,
223    owner: String,
224    /// The task this message continues (one waiting for input), if any.
225    pub task: Option<Task>,
226    /// Media types the client accepts in replies (empty: any).
227    pub accepted_output_modes: Vec<String>,
228}
229
230impl A2aContext {
231    /// The task id this message belongs to (new, or the continued task's).
232    pub fn task_id(&self) -> &str {
233        &self.task_id
234    }
235
236    /// The conversation's context id.
237    pub fn context_id(&self) -> &str {
238        &self.context_id
239    }
240
241    /// A task in `state` with an optional agent message and artifacts; its
242    /// history holds the earlier messages and this one.
243    pub fn task(&self, state: TaskState, message: Option<Message>, artifacts: Vec<Artifact>) -> Task {
244        let mut history = self.task.as_ref().map(|t| t.history.clone()).unwrap_or_default();
245        history.push(self.user_message.clone());
246        let message = message.map(|mut m| {
247            m.task_id = Some(self.task_id.clone());
248            m.context_id = Some(self.context_id.clone());
249            m
250        });
251        if let Some(m) = &message {
252            history.push(m.clone());
253        }
254        let mut all = self.task.as_ref().map(|t| t.artifacts.clone()).unwrap_or_default();
255        all.extend(artifacts);
256        Task {
257            id: self.task_id.clone(),
258            context_id: self.context_id.clone(),
259            status: TaskStatus { state, message, timestamp: model::timestamp() },
260            artifacts: all,
261            history,
262            metadata: None,
263        }
264    }
265
266    /// A completed task with these artifacts.
267    pub fn complete(&self, artifacts: Vec<Artifact>) -> Task {
268        self.task(TaskState::Completed, None, artifacts)
269    }
270
271    /// A failed task, saying why.
272    pub fn fail(&self, reason: &str) -> Task {
273        self.task(TaskState::Failed, Some(Message::agent_text(reason)), Vec::new())
274    }
275
276    /// A refused task, saying why.
277    pub fn reject(&self, reason: &str) -> Task {
278        self.task(TaskState::Rejected, Some(Message::agent_text(reason)), Vec::new())
279    }
280
281    /// A task waiting for the client to answer `question`.
282    pub fn input_required(&self, question: &str) -> Task {
283        self.task(TaskState::InputRequired, Some(Message::agent_text(question)), Vec::new())
284    }
285
286    /// Start working on this message in the background: the task is stored
287    /// (working) at once; return `A2aReply::Running(handle)` and update it
288    /// through the handle from any thread. `SendMessage` waits for it to
289    /// finish (or ask for input), streams follow it.
290    pub fn start(&self) -> TaskHandle {
291        let task = self.task(TaskState::Working, None, Vec::new());
292        let mut store = self.shared.lock();
293        store.put(&self.owner, task);
294        let canceled = store.get_mut(&self.owner, &self.task_id).map(|s| Arc::clone(&s.canceled)).unwrap_or_default();
295        drop(store);
296        TaskHandle { shared: Arc::clone(&self.shared), owner: self.owner.clone(), id: self.task_id.clone(), canceled }
297    }
298}
299
300impl std::fmt::Debug for TaskHandle {
301    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
302        f.debug_struct("TaskHandle").field("id", &self.id).finish()
303    }
304}
305
306/// A handler's answer to a message.
307#[derive(Debug, Clone)]
308pub enum A2aReply {
309    /// A direct reply; no task.
310    Message(Message),
311    /// A task (stored, so `GetTask` and later messages find it).
312    Task(Task),
313    /// A task started with [`A2aContext::start`], still being worked on.
314    Running(TaskHandle),
315}
316
317type Handler = Arc<dyn Fn(&A2aContext, Message) -> A2aReply + Send + Sync>;
318
319/// The A2A server: an [`Application`] serving the Agent Card and the
320/// JSON-RPC endpoint.
321#[derive(Clone)]
322pub struct A2aServer {
323    card: Arc<AgentCard>,
324    path: String,
325    handler: Handler,
326    tasks: Arc<Shared>,
327    fallback: Option<Arc<dyn Application + Send + Sync>>,
328    /// Bearer tokens and the identity each stands for; empty: no auth.
329    tokens: Arc<Vec<(String, String)>>,
330    blocking_timeout: Duration,
331    /// URL prefixes webhooks may point to; empty: push notifications off.
332    push_allow: Arc<Vec<String>>,
333    extended_card: Option<Arc<AgentCard>>,
334    /// When this server was made, as an HTTP date (the card's Last-Modified).
335    started: String,
336}
337
338impl A2aServer {
339    /// An agent described by `card`; until [`on_message`](Self::on_message)
340    /// it rejects every message.
341    pub fn new(card: AgentCard) -> A2aServer {
342        A2aServer {
343            card: Arc::new(card),
344            path: "/a2a".to_string(),
345            handler: Arc::new(|ctx: &A2aContext, _| A2aReply::Task(ctx.reject("this agent has no message handler"))),
346            tasks: Arc::new(Shared::default()),
347            fallback: None,
348            tokens: Arc::new(Vec::new()),
349            blocking_timeout: Duration::from_secs(300),
350            push_allow: Arc::new(Vec::new()),
351            extended_card: None,
352            started: crate::http::date::now(),
353        }
354    }
355
356    /// The JSON-RPC endpoint's path (default `/a2a`).
357    pub fn at(mut self, path: &str) -> A2aServer {
358        self.path = path.to_string();
359        self
360    }
361
362    /// Answer each message.
363    pub fn on_message<F>(mut self, handler: F) -> A2aServer
364    where
365        F: Fn(&A2aContext, Message) -> A2aReply + Send + Sync + 'static,
366    {
367        self.handler = Arc::new(handler);
368        self
369    }
370
371    /// Require `Authorization: Bearer <token>` on the JSON-RPC endpoint (the
372    /// card stays public and lists the scheme).
373    pub fn require_bearer(self, token: &str) -> A2aServer {
374        self.require_bearer_tokens([(token, "default")])
375    }
376
377    /// Several tokens, each standing for an identity; callers only see their
378    /// own tasks.
379    pub fn require_bearer_tokens<'a>(mut self, tokens: impl IntoIterator<Item = (&'a str, &'a str)>) -> A2aServer {
380        self.tokens = Arc::new(tokens.into_iter().filter(|(t, _)| !t.is_empty()).map(|(t, i)| (t.to_string(), i.to_string())).collect());
381        self
382    }
383
384    /// How long `SendMessage` waits for a running task to finish or ask for
385    /// input before answering with its current state (default 300 s).
386    pub fn blocking_timeout(mut self, timeout: Duration) -> A2aServer {
387        self.blocking_timeout = timeout;
388        self
389    }
390
391    /// Turn push notifications on: clients may register webhooks whose URL
392    /// starts with one of `prefixes` (`https://hooks.example.com/`). They
393    /// come from clients, so only listed destinations are called (SSRF).
394    pub fn push_notifications<'a>(mut self, prefixes: impl IntoIterator<Item = &'a str>) -> A2aServer {
395        self.push_allow = Arc::new(prefixes.into_iter().filter(|p| !p.is_empty()).map(str::to_string).collect());
396        if !self.push_allow.is_empty() {
397            let _ = self.tasks.push.set(push::start());
398        }
399        self
400    }
401
402    /// A fuller card for authenticated callers (`GetExtendedAgentCard`):
403    /// skills you don't advertise publicly, say. Needs bearer tokens.
404    pub fn extended_card(mut self, card: AgentCard) -> A2aServer {
405        self.extended_card = Some(Arc::new(card));
406        self
407    }
408
409    /// Requests that aren't A2A go to `app`.
410    pub fn wrap(mut self, app: impl Application + Send + Sync + 'static) -> A2aServer {
411        self.fallback = Some(Arc::new(app));
412        self
413    }
414
415    /// `(bearer auth, push notifications, extended card)` for the card.
416    fn capabilities(&self) -> (bool, bool, bool) {
417        (!self.tokens.is_empty(), !self.push_allow.is_empty(), self.extended_card.is_some() && !self.tokens.is_empty())
418    }
419
420    /// The caller's identity: `""` without auth, the token's identity with
421    /// it, `None` when the token is missing or wrong.
422    fn caller(&self, request: &Request) -> Option<String> {
423        if self.tokens.is_empty() {
424            return Some(String::new());
425        }
426        let sent = request.header(crate::header::Header::_AUTHORIZATION)?.value.strip_prefix("Bearer ")?.trim().to_string();
427        self.tokens.iter().find(|(token, _)| crate::core::constant_time_eq(token.as_bytes(), sent.as_bytes())).map(|(_, identity)| identity.clone())
428    }
429
430    fn endpoint_url(&self, request: &Request, connection: &ConnectionInfo) -> String {
431        if let Some(url) = &self.card.url {
432            return url.clone();
433        }
434        let host = request.header(crate::header::Header::_HOST).map(|h| h.value.to_string()).unwrap_or_else(|| format!("{}:{}", connection.server.ip, connection.server.port));
435        format!("{}://{host}{}", if connection.tls { "https" } else { "http" }, self.path)
436    }
437
438    /// The card with `Cache-Control`, an `ETag` (its content's hash) and
439    /// `Last-Modified` (when the server started); `304` for a matching
440    /// `If-None-Match` (A2A 1.0 §8.6.1).
441    fn card_response(&self, request: &Request, connection: &ConnectionInfo) -> Response {
442        use crate::header::Header;
443        let json = self.card.to_json(&self.endpoint_url(request, connection), self.capabilities());
444        let digest = crate::core::sha256::sha256(json.as_bytes());
445        let etag = format!("\"{}\"", digest[..8].iter().map(|b| format!("{b:02x}")).collect::<String>());
446        let fresh = request.header(Header::_IF_NONE_MATCH).is_some_and(|h| h.value.split(',').any(|t| t.trim().trim_start_matches("W/") == etag || t.trim() == "*"));
447        let mut response = if fresh {
448            Response::get_response(STATUS_CODE_REASON_PHRASE.n304_not_modified, None, None)
449        } else {
450            Response::json(STATUS_CODE_REASON_PHRASE.n200_ok, json.into_bytes())
451        };
452        response.headers.push(Header::new("Cache-Control", "public, max-age=300".to_string()));
453        response.headers.push(Header::new("ETag", etag));
454        response.headers.push(Header::new("Last-Modified", self.started.clone()));
455        response
456    }
457
458    fn rpc(&self, request: &Request, owner: &str) -> Response {
459        let reply = |body: String| Response::json(STATUS_CODE_REASON_PHRASE.n200_ok, body.into_bytes());
460        if request.body.len() > MAX_BODY {
461            return crate::error::problem(STATUS_CODE_REASON_PHRASE.n413_payload_too_large, "A2A request too large");
462        }
463        let Ok(text) = std::str::from_utf8(&request.body) else {
464            return reply(error_json("null", RpcError::Parse("the body isn't UTF-8".into())));
465        };
466        let call = match json_lite::parse(text) {
467            Ok(call) => call,
468            Err(e) => return reply(error_json("null", RpcError::Parse(e))),
469        };
470        let id = call.get("id").map(to_json).unwrap_or_else(|| "null".to_string());
471        let method = match (call.get("jsonrpc").and_then(JsonValue::as_str), call.get("method").and_then(JsonValue::as_str)) {
472            (Some("2.0"), Some(method)) => method,
473            _ => return reply(error_json(&id, RpcError::InvalidRequest)),
474        };
475        if let Err(e) = check_version(request) {
476            return reply(error_json(&id, e));
477        }
478        let params = call.get("params").cloned().unwrap_or(JsonValue::Object(Vec::new()));
479        if method == "SendStreamingMessage" || method == "SubscribeToTask" {
480            return match self.stream(method, &params, owner, &id) {
481                Ok(response) => response,
482                Err(e) => reply(error_json(&id, e)),
483            };
484        }
485        match self.dispatch(method, &params, owner) {
486            Ok(result) => reply(format!(r#"{{"jsonrpc":"2.0","id":{id},"result":{result}}}"#)),
487            Err(e) => reply(error_json(&id, e)),
488        }
489    }
490
491    fn dispatch(&self, method: &str, params: &JsonValue, owner: &str) -> Result<String, RpcError> {
492        match method {
493            "SendMessage" => self.send_message(params, owner),
494            "GetTask" => {
495                let id = params.get("id").and_then(JsonValue::as_str).ok_or_else(|| RpcError::InvalidParams("id is required".into()))?;
496                let tasks = self.tasks.lock();
497                let task = tasks.get(owner, id).ok_or(RpcError::TaskNotFound)?;
498                Ok(task.to_json(history_length(params)?))
499            }
500            "ListTasks" => self.list_tasks(params, owner),
501            "CancelTask" => {
502                let id = params.get("id").and_then(JsonValue::as_str).ok_or_else(|| RpcError::InvalidParams("id is required".into()))?;
503                if !tasks::set_state(&self.tasks, owner, id, TaskState::Canceled, None) {
504                    return Err(match self.tasks.lock().get(owner, id) {
505                        Some(_) => RpcError::TaskNotCancelable,
506                        None => RpcError::TaskNotFound,
507                    });
508                }
509                Ok(self.tasks.lock().get(owner, id).map(|t| t.to_json(None)).unwrap_or_default())
510            }
511            "GetExtendedAgentCard" => match (&self.extended_card, self.tokens.is_empty()) {
512                (Some(card), false) => Ok(card.to_json(&self.card.url.clone().unwrap_or_else(|| self.path.clone()), self.capabilities())),
513                (Some(_), true) => Err(RpcError::UnsupportedOperation("the extended card needs bearer auth configured".into())),
514                // Not declared on the card: unsupported (A2A 1.0 §3.1.11).
515                (None, _) => Err(RpcError::UnsupportedOperation("this agent has no extended card".into())),
516            },
517            "CreateTaskPushNotificationConfig" | "GetTaskPushNotificationConfig" | "ListTaskPushNotificationConfigs" | "DeleteTaskPushNotificationConfig" => {
518                self.push_config(method, params, owner)
519            }
520            _ => Err(RpcError::MethodNotFound(method.to_string())),
521        }
522    }
523
524    /// Run the handler for a `SendMessage` / `SendStreamingMessage`.
525    fn handle_message(&self, params: &JsonValue, owner: &str) -> Result<Outcome, RpcError> {
526        let message = Message::from_json(params.get("message").ok_or_else(|| RpcError::InvalidParams("message is required".into()))?).map_err(RpcError::InvalidParams)?;
527        let configuration = params.get("configuration");
528        let accepted_output_modes = configuration
529            .and_then(|c| model::field(c, "acceptedOutputModes"))
530            .and_then(JsonValue::as_array)
531            .map(|a| a.iter().filter_map(|m| m.as_str().map(str::to_string)).collect())
532            .unwrap_or_default();
533        // A webhook for this message's task, checked before the handler runs.
534        let webhook = match configuration.and_then(|c| model::field(c, "taskPushNotificationConfig")) {
535            Some(_) if self.push_allow.is_empty() => return Err(RpcError::PushNotificationNotSupported),
536            Some(config) => Some(self.checked_config(config, "pending")?),
537            None => None,
538        };
539        let existing = match &message.task_id {
540            Some(id) => {
541                let task = self.tasks.lock().get(owner, id).cloned().ok_or(RpcError::TaskNotFound)?;
542                if task.status.state.is_terminal() {
543                    return Err(RpcError::UnsupportedOperation(format!("task {id} is {} and takes no more messages", task.status.state.as_str())));
544                }
545                Some(task)
546            }
547            None => None,
548        };
549        let task_id = existing.as_ref().map(|t| t.id.clone()).unwrap_or_else(new_id);
550        let context_id = existing.as_ref().map(|t| t.context_id.clone()).or_else(|| message.context_id.clone()).unwrap_or_else(new_id);
551        let mut user_message = message.clone();
552        user_message.context_id = Some(context_id.clone());
553        let ctx = A2aContext {
554            task_id,
555            context_id: context_id.clone(),
556            user_message,
557            shared: Arc::clone(&self.tasks),
558            owner: owner.to_string(),
559            task: existing,
560            accepted_output_modes,
561        };
562        let handler = Arc::clone(&self.handler);
563        let reply = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| handler(&ctx, message))).unwrap_or_else(|_| A2aReply::Task(ctx.fail("the agent failed")));
564        let outcome = match reply {
565            A2aReply::Message(mut m) => {
566                m.context_id.get_or_insert(context_id);
567                Outcome::Message(m)
568            }
569            A2aReply::Task(task) => {
570                let id = task.id.clone();
571                let continued = ctx.task.is_some();
572                let mut store = self.tasks.lock();
573                store.put(owner, task);
574                // A continued task moved on: tell its streams and webhooks.
575                if continued {
576                    if let Some(stored) = store.get_mut(owner, &id) {
577                        let update = Obj::new().str("taskId", &stored.task.id).str("contextId", &stored.task.context_id).raw("status", &stored.task.status.to_json()).done();
578                        tasks::publish(stored, self.tasks.push.get(), "statusUpdate", &update);
579                        let state = stored.task.status.state;
580                        if state.is_terminal() || state.is_interrupted() {
581                            stored.subscribers.clear();
582                        }
583                    }
584                    drop(store);
585                    self.tasks.changed.notify_all();
586                }
587                Outcome::Task(id, false)
588            }
589            A2aReply::Running(handle) => Outcome::Task(handle.id, true),
590        };
591        if let (Some(mut config), Outcome::Task(id, _)) = (webhook, &outcome) {
592            config.task_id = id.clone();
593            let mut store = self.tasks.lock();
594            if let Some(stored) = store.get_mut(owner, id) {
595                // The task as it is now, then every update.
596                if let Some(queue) = self.tasks.push.get() {
597                    push::enqueue(queue, &config, "task", &stored.task.to_json(None));
598                }
599                stored.push.retain(|c| c.id != config.id);
600                stored.push.push(config);
601            }
602        }
603        Ok(outcome)
604    }
605
606    fn send_message(&self, params: &JsonValue, owner: &str) -> Result<String, RpcError> {
607        let configuration = params.get("configuration");
608        let history = match configuration {
609            Some(c) => history_length(c)?,
610            None => None,
611        };
612        let return_immediately = configuration.and_then(|c| model::field(c, "returnImmediately")) == Some(&JsonValue::Bool(true));
613        match self.handle_message(params, owner)? {
614            Outcome::Message(m) => Ok(format!(r#"{{"message":{}}}"#, m.to_json())),
615            Outcome::Task(id, running) => {
616                // Blocking by default: until the task finishes or waits on
617                // the client (A2A 1.0 §3.2.2), within blocking_timeout.
618                let deadline = std::time::Instant::now() + self.blocking_timeout;
619                let mut store = self.tasks.lock();
620                loop {
621                    let Some(task) = store.get(owner, &id) else { return Err(RpcError::TaskNotFound) };
622                    let state = task.status.state;
623                    let now = std::time::Instant::now();
624                    if !running || return_immediately || state.is_terminal() || state.is_interrupted() || now >= deadline {
625                        return Ok(format!(r#"{{"task":{}}}"#, task.to_json(history)));
626                    }
627                    store = self.tasks.changed.wait_timeout(store, deadline - now).map(|(g, _)| g).unwrap_or_else(|e| e.into_inner().0);
628                }
629            }
630        }
631    }
632
633    /// `SendStreamingMessage` and `SubscribeToTask`: an SSE stream whose
634    /// first event is the task (or the one message), then its updates until
635    /// it finishes or waits on the client.
636    fn stream(&self, method: &str, params: &JsonValue, owner: &str, rpc_id: &str) -> Result<Response, RpcError> {
637        let id = if method == "SubscribeToTask" {
638            let id = params.get("id").and_then(JsonValue::as_str).ok_or_else(|| RpcError::InvalidParams("id is required".into()))?.to_string();
639            let store = self.tasks.lock();
640            let task = store.get(owner, &id).ok_or(RpcError::TaskNotFound)?;
641            if task.status.state.is_terminal() {
642                return Err(RpcError::UnsupportedOperation(format!("task {id} is {}", task.status.state.as_str())));
643            }
644            id
645        } else {
646            match self.handle_message(params, owner)? {
647                Outcome::Message(m) => {
648                    let (sender, response) = crate::sse::SseStream::new().start();
649                    let _ = sender.try_send(tasks::event(rpc_id, "message", &m.to_json()));
650                    return Ok(response);
651                }
652                Outcome::Task(id, _) => id,
653            }
654        };
655        let (sender, response) = crate::sse::SseStream::new().buffer(64).start();
656        let mut store = self.tasks.lock();
657        let stored = store.get_mut(owner, &id).ok_or(RpcError::TaskNotFound)?;
658        let _ = sender.try_send(tasks::event(rpc_id, "task", &stored.task.to_json(None)));
659        let state = stored.task.status.state;
660        let follow = !state.is_terminal() && (method == "SubscribeToTask" || !state.is_interrupted());
661        if follow {
662            if stored.subscribers.len() >= tasks::MAX_SUBSCRIBERS {
663                return Err(RpcError::UnsupportedOperation(format!("task {id} has {} streams already", tasks::MAX_SUBSCRIBERS)));
664            }
665            stored.subscribers.push(tasks::Subscriber { sender, rpc_id: rpc_id.to_string() });
666        }
667        Ok(response)
668    }
669
670    fn push_config(&self, method: &str, params: &JsonValue, owner: &str) -> Result<String, RpcError> {
671        if self.push_allow.is_empty() {
672            return Err(RpcError::PushNotificationNotSupported);
673        }
674        let task_id = model::field(params, "taskId").and_then(JsonValue::as_str).ok_or_else(|| RpcError::InvalidParams("taskId is required".into()))?.to_string();
675        let mut store = self.tasks.lock();
676        let stored = store.get_mut(owner, &task_id).ok_or(RpcError::TaskNotFound)?;
677        let id = params.get("id").and_then(JsonValue::as_str);
678        match method {
679            "CreateTaskPushNotificationConfig" => {
680                let config = self.checked_config(params, &task_id)?;
681                stored.push.retain(|c| c.id != config.id);
682                if stored.push.len() >= push::MAX_CONFIGS {
683                    return Err(RpcError::InvalidParams(format!("at most {} push notification configs per task", push::MAX_CONFIGS)));
684                }
685                let json = config.to_json();
686                stored.push.push(config);
687                Ok(json)
688            }
689            "GetTaskPushNotificationConfig" => {
690                let id = id.ok_or_else(|| RpcError::InvalidParams("id is required".into()))?;
691                stored.push.iter().find(|c| c.id == id).map(push::PushConfig::to_json).ok_or_else(|| RpcError::InvalidParams(format!("no push notification config {id}")))
692            }
693            "ListTaskPushNotificationConfigs" => {
694                let configs: Vec<String> = stored.push.iter().map(push::PushConfig::to_json).collect();
695                Ok(Obj::new().list("configs", &configs).str("nextPageToken", "").done())
696            }
697            _ => {
698                let id = id.ok_or_else(|| RpcError::InvalidParams("id is required".into()))?;
699                stored.push.retain(|c| c.id != id);
700                Ok("{}".to_string())
701            }
702        }
703    }
704
705    /// A webhook config whose URL the operator allows.
706    fn checked_config(&self, value: &JsonValue, task_id: &str) -> Result<push::PushConfig, RpcError> {
707        let config = push::PushConfig::from_json(value, Some(task_id)).map_err(RpcError::InvalidParams)?;
708        if !push::allowed(&config.url, &self.push_allow) {
709            return Err(RpcError::InvalidParams(format!("push notification URL {} isn't allowed by this agent", config.url)));
710        }
711        Ok(config)
712    }
713
714    fn list_tasks(&self, params: &JsonValue, owner: &str) -> Result<String, RpcError> {
715        let page_size = match model::field(params, "pageSize").and_then(JsonValue::as_f64) {
716            Some(n) if (1.0..=100.0).contains(&n) && n.fract() == 0.0 => n as usize,
717            Some(_) => return Err(RpcError::InvalidParams("pageSize must be 1 to 100".into())),
718            None => 50,
719        };
720        let state = match params.get("status").and_then(JsonValue::as_str) {
721            Some(s) => Some(TaskState::parse(s).ok_or_else(|| RpcError::InvalidParams(format!("unknown status {s}")))?),
722            None => None,
723        };
724        let context = model::field(params, "contextId").and_then(JsonValue::as_str);
725        let offset: usize = match model::field(params, "pageToken").and_then(JsonValue::as_str).filter(|t| !t.is_empty()) {
726            Some(t) => t.parse().map_err(|_| RpcError::InvalidParams("bad pageToken".into()))?,
727            None => 0,
728        };
729        let history = history_length(params)?.or(Some(0));
730        let include_artifacts = model::field(params, "includeArtifacts") == Some(&JsonValue::Bool(true));
731        let tasks = self.tasks.lock();
732        // Most recently updated first (status timestamps are ISO 8601 UTC,
733        // so they sort as strings), newest-created first among equals.
734        let mut matching: Vec<&Task> = tasks
735            .order
736            .iter()
737            .rev()
738            .filter_map(|id| tasks.get(owner, id))
739            .filter(|t| state.is_none_or(|s| t.status.state == s) && context.is_none_or(|c| t.context_id == c))
740            .collect();
741        matching.sort_by(|a, b| b.status.timestamp.cmp(&a.status.timestamp));
742        let page: Vec<String> = matching
743            .iter()
744            .skip(offset)
745            .take(page_size)
746            .map(|t| {
747                if include_artifacts {
748                    t.to_json(history)
749                } else {
750                    let mut bare = (*t).clone();
751                    bare.artifacts.clear();
752                    bare.to_json(history)
753                }
754            })
755            .collect();
756        let next = if offset + page_size < matching.len() { (offset + page_size).to_string() } else { String::new() };
757        Ok(Obj::new()
758            .list("tasks", &page)
759            .raw("totalSize", &matching.len().to_string())
760            .raw("pageSize", &page_size.to_string())
761            .str("nextPageToken", &next)
762            .done())
763    }
764}
765
766/// What a message led to.
767enum Outcome {
768    Message(Message),
769    /// A stored task's id, and whether a worker is still updating it.
770    Task(String, bool),
771}
772
773fn history_length(params: &JsonValue) -> Result<Option<usize>, RpcError> {
774    match model::field(params, "historyLength") {
775        None | Some(JsonValue::Null) => Ok(None),
776        Some(JsonValue::Number(n)) if *n >= 0.0 && n.fract() == 0.0 => Ok(Some(*n as usize)),
777        Some(_) => Err(RpcError::InvalidParams("historyLength must be a non-negative integer".into())),
778    }
779}
780
781/// `A2A-Version` (header or query parameter): `1.0` is served.
782fn check_version(request: &Request) -> Result<(), RpcError> {
783    let from_query = || {
784        let query = request.request_uri.split_once('?')?.1;
785        query.split('&').find_map(|p| p.strip_prefix("A2A-Version=")).map(str::to_string)
786    };
787    let version = request.header("A2A-Version").map(|h| h.value.trim().to_string()).or_else(from_query).unwrap_or_default();
788    let major_minor: String = version.split('.').take(2).collect::<Vec<_>>().join(".");
789    if major_minor == PROTOCOL_VERSION {
790        Ok(())
791    } else {
792        Err(RpcError::VersionNotSupported(if version.is_empty() { "0.3 (no A2A-Version)".to_string() } else { version }))
793    }
794}
795
796/// A JSON-RPC error.
797#[derive(Debug, Clone, PartialEq)]
798pub(crate) enum RpcError {
799    Parse(String),
800    InvalidRequest,
801    MethodNotFound(String),
802    InvalidParams(String),
803    TaskNotFound,
804    TaskNotCancelable,
805    PushNotificationNotSupported,
806    UnsupportedOperation(String),
807    VersionNotSupported(String),
808}
809
810impl RpcError {
811    fn parts(&self) -> (i32, &'static str, String) {
812        match self {
813            RpcError::Parse(e) => (-32700, "PARSE_ERROR", format!("invalid JSON: {e}")),
814            RpcError::InvalidRequest => (-32600, "INVALID_REQUEST", "not a JSON-RPC 2.0 request".to_string()),
815            RpcError::MethodNotFound(m) => (-32601, "METHOD_NOT_FOUND", format!("unknown method {m}")),
816            RpcError::InvalidParams(e) => (-32602, "INVALID_PARAMS", e.clone()),
817            RpcError::TaskNotFound => (-32001, "TASK_NOT_FOUND", "task not found".to_string()),
818            RpcError::TaskNotCancelable => (-32002, "TASK_NOT_CANCELABLE", "the task is finished and can't be canceled".to_string()),
819            RpcError::PushNotificationNotSupported => (-32003, "PUSH_NOTIFICATION_NOT_SUPPORTED", "push notifications aren't supported by this agent".to_string()),
820            RpcError::UnsupportedOperation(e) => (-32004, "UNSUPPORTED_OPERATION", e.clone()),
821            RpcError::VersionNotSupported(v) => (-32009, "VERSION_NOT_SUPPORTED", format!("A2A version {v} isn't supported; this agent speaks {PROTOCOL_VERSION}")),
822        }
823    }
824}
825
826fn error_json(id: &str, error: RpcError) -> String {
827    let (code, reason, message) = error.parts();
828    let data = format!(r#"[{{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":{},"domain":"a2a-protocol.org"}}]"#, quote(reason));
829    format!(r#"{{"jsonrpc":"2.0","id":{id},"error":{{"code":{code},"message":{},"data":{data}}}}}"#, quote(&message))
830}
831
832impl Application for A2aServer {
833    fn execute(&self, request: &Request, connection: &ConnectionInfo) -> Result<Response, String> {
834        let path = crate::url::URL::request_target_path(&request.request_uri).unwrap_or("");
835        if path == AGENT_CARD_PATH && (request.method == "GET" || request.method == "HEAD") {
836            return Ok(self.card_response(request, connection));
837        }
838        if path == self.path || path.strip_suffix('/') == Some(self.path.as_str()) {
839            if request.method == "POST" {
840                let owner = match self.caller(request) {
841                    Some(owner) => owner,
842                    None => {
843                        let mut response = crate::error::problem(STATUS_CODE_REASON_PHRASE.n401_unauthorized, "A2A requests need a bearer token");
844                        response.headers.push(crate::header::Header::new("WWW-Authenticate", r#"Bearer realm="a2a""#.to_string()));
845                        return Ok(response);
846                    }
847                };
848                return Ok(self.rpc(request, &owner));
849            }
850            let mut response = crate::error::problem(STATUS_CODE_REASON_PHRASE.n405_method_not_allowed, "A2A requests are JSON-RPC POSTs");
851            response.headers.push(crate::header::Header::new("Allow", "POST".to_string()));
852            return Ok(response);
853        }
854        match &self.fallback {
855            Some(app) => app.execute(request, connection),
856            None => Ok(crate::error::problem(STATUS_CODE_REASON_PHRASE.n404_not_found, "")),
857        }
858    }
859}
860
861#[cfg(test)]
862mod tests;