1mod 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
45pub const PROTOCOL_VERSION: &str = "1.0";
47pub const AGENT_CARD_PATH: &str = "/.well-known/agent-card.json";
49pub const MAX_BODY: usize = 4 * 1024 * 1024;
51pub const MAX_TASKS: usize = 10_000;
53
54pub(crate) fn json_text(value: &JsonValue) -> String {
56 to_json(value)
57}
58
59pub(crate) fn quote_json(s: &str) -> String {
61 quote(s)
62}
63
64pub(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#[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#[derive(Debug, Clone, PartialEq)]
132pub struct AgentCard {
133 pub name: String,
134 pub description: String,
135 pub version: String,
136 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 pub fn url(mut self, url: &str) -> AgentCard {
170 self.url = Some(url.into());
171 self
172 }
173
174 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
215pub struct A2aContext {
219 task_id: String,
220 context_id: String,
221 user_message: Message,
222 shared: Arc<Shared>,
223 owner: String,
224 pub task: Option<Task>,
226 pub accepted_output_modes: Vec<String>,
228}
229
230impl A2aContext {
231 pub fn task_id(&self) -> &str {
233 &self.task_id
234 }
235
236 pub fn context_id(&self) -> &str {
238 &self.context_id
239 }
240
241 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 pub fn complete(&self, artifacts: Vec<Artifact>) -> Task {
268 self.task(TaskState::Completed, None, artifacts)
269 }
270
271 pub fn fail(&self, reason: &str) -> Task {
273 self.task(TaskState::Failed, Some(Message::agent_text(reason)), Vec::new())
274 }
275
276 pub fn reject(&self, reason: &str) -> Task {
278 self.task(TaskState::Rejected, Some(Message::agent_text(reason)), Vec::new())
279 }
280
281 pub fn input_required(&self, question: &str) -> Task {
283 self.task(TaskState::InputRequired, Some(Message::agent_text(question)), Vec::new())
284 }
285
286 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#[derive(Debug, Clone)]
308pub enum A2aReply {
309 Message(Message),
311 Task(Task),
313 Running(TaskHandle),
315}
316
317type Handler = Arc<dyn Fn(&A2aContext, Message) -> A2aReply + Send + Sync>;
318
319#[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 tokens: Arc<Vec<(String, String)>>,
330 blocking_timeout: Duration,
331 push_allow: Arc<Vec<String>>,
333 extended_card: Option<Arc<AgentCard>>,
334 started: String,
336}
337
338impl A2aServer {
339 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 pub fn at(mut self, path: &str) -> A2aServer {
358 self.path = path.to_string();
359 self
360 }
361
362 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 pub fn require_bearer(self, token: &str) -> A2aServer {
374 self.require_bearer_tokens([(token, "default")])
375 }
376
377 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 pub fn blocking_timeout(mut self, timeout: Duration) -> A2aServer {
387 self.blocking_timeout = timeout;
388 self
389 }
390
391 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 pub fn extended_card(mut self, card: AgentCard) -> A2aServer {
405 self.extended_card = Some(Arc::new(card));
406 self
407 }
408
409 pub fn wrap(mut self, app: impl Application + Send + Sync + 'static) -> A2aServer {
411 self.fallback = Some(Arc::new(app));
412 self
413 }
414
415 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 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 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, ¶ms, owner, &id) {
481 Ok(response) => response,
482 Err(e) => reply(error_json(&id, e)),
483 };
484 }
485 match self.dispatch(method, ¶ms, 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 (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 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 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 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 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 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 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 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 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
766enum Outcome {
768 Message(Message),
769 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
781fn 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#[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;