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