1mod 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
46pub const PROTOCOL_VERSION: &str = "1.0";
48pub const AGENT_CARD_PATH: &str = "/.well-known/agent-card.json";
50pub const MAX_BODY: usize = 4 * 1024 * 1024;
52pub const MAX_TASKS: usize = 10_000;
54
55#[doc(hidden)]
57pub fn json_text(value: &JsonValue) -> String {
58 json_lite::to_text(value)
59}
60
61#[doc(hidden)]
63pub fn quote_json(s: &str) -> String {
64 quote(s)
65}
66
67#[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#[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#[derive(Debug, Clone, PartialEq)]
136pub struct AgentCard {
137 pub name: String,
138 pub description: String,
139 pub version: String,
140 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 pub fn url(mut self, url: &str) -> AgentCard {
174 self.url = Some(url.into());
175 self
176 }
177
178 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
220pub struct A2aContext {
224 task_id: String,
225 context_id: String,
226 user_message: Message,
227 shared: Arc<Shared>,
228 owner: String,
229 pub task: Option<Task>,
231 pub accepted_output_modes: Vec<String>,
233}
234
235impl A2aContext {
236 pub fn task_id(&self) -> &str {
238 &self.task_id
239 }
240
241 pub fn context_id(&self) -> &str {
243 &self.context_id
244 }
245
246 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 pub fn complete(&self, artifacts: Vec<Artifact>) -> Task {
273 self.task(TaskState::Completed, None, artifacts)
274 }
275
276 pub fn fail(&self, reason: &str) -> Task {
278 self.task(TaskState::Failed, Some(Message::agent_text(reason)), Vec::new())
279 }
280
281 pub fn reject(&self, reason: &str) -> Task {
283 self.task(TaskState::Rejected, Some(Message::agent_text(reason)), Vec::new())
284 }
285
286 pub fn input_required(&self, question: &str) -> Task {
288 self.task(TaskState::InputRequired, Some(Message::agent_text(question)), Vec::new())
289 }
290
291 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#[derive(Debug, Clone)]
313pub enum A2aReply {
314 Message(Message),
316 Task(Task),
318 Running(TaskHandle),
320}
321
322type Handler = Arc<dyn Fn(&A2aContext, Message) -> A2aReply + Send + Sync>;
323
324#[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 tokens: Arc<Vec<(String, String)>>,
335 blocking_timeout: Duration,
336 push_allow: Arc<Vec<String>>,
338 extended_card: Option<Arc<AgentCard>>,
339 started: String,
341}
342
343impl A2aServer {
344 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 pub fn at(mut self, path: &str) -> A2aServer {
363 self.path = path.to_string();
364 self
365 }
366
367 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 pub fn require_bearer(self, token: &str) -> A2aServer {
379 self.require_bearer_tokens([(token, "default")])
380 }
381
382 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 pub fn blocking_timeout(mut self, timeout: Duration) -> A2aServer {
392 self.blocking_timeout = timeout;
393 self
394 }
395
396 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 pub fn extended_card(mut self, card: AgentCard) -> A2aServer {
410 self.extended_card = Some(Arc::new(card));
411 self
412 }
413
414 pub fn wrap(mut self, app: impl Application + Send + Sync + 'static) -> A2aServer {
416 self.fallback = Some(Arc::new(app));
417 self
418 }
419
420 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 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}{}", if connection.tls { "https" } else { "http" }, self.path)
441 }
442
443 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, ¶ms, owner, &id) {
486 Ok(response) => response,
487 Err(e) => reply(error_json(&id, e)),
488 };
489 }
490 match self.dispatch(method, ¶ms, 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 (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 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 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 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 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 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 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 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 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
771enum Outcome {
773 Message(Message),
774 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
786fn 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#[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;