Skip to main content

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