1use std::{
2 collections::{HashMap, HashSet},
3 sync::{
4 atomic::{AtomicU64, Ordering},
5 Arc,
6 },
7};
8
9use chrono::{DateTime, SecondsFormat, Utc};
10use murmur_artifact::TaskAcceptance;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use tokio::sync::oneshot;
14
15use crate::{
16 cancel::CancelSignal, identity::DOOR_EXTENSION_URI, lanes::TaskLane, origin::TaskProvenance,
17};
18
19pub const A2A_VERSION_HEADER: &str = "A2A-Version";
23
24pub const A2A_PROTOCOL_VERSION: &str = "1.0";
27
28pub(crate) fn accepts_a2a_version(values: &[String]) -> bool {
35 let [value] = values else {
36 return false;
37 };
38 let value = value.trim();
39 value == A2A_PROTOCOL_VERSION
40 || value
41 .strip_prefix(A2A_PROTOCOL_VERSION)
42 .and_then(|rest| rest.strip_prefix('.'))
43 .is_some_and(|patch| !patch.is_empty() && patch.bytes().all(|b| b.is_ascii_digit()))
44}
45
46pub(crate) fn version_not_supported(id: Value, values: &[String]) -> JsonRpcResponse {
50 let requested = values
51 .iter()
52 .map(|value| value.trim())
53 .collect::<Vec<_>>()
54 .join(", ");
55 JsonRpcResponse::a2a_error(
56 id,
57 A2aError::VersionNotSupported,
58 &format!(
59 "A2A version '{requested}' is not supported; this agent speaks {A2A_PROTOCOL_VERSION}"
60 ),
61 &[
62 ("requestedVersion", requested.clone()),
63 ("supportedVersions", A2A_PROTOCOL_VERSION.to_string()),
64 ],
65 )
66}
67
68pub const A2A_EXTENSIONS_HEADER: &str = "A2A-Extensions";
73
74pub(crate) fn activates_extension(values: &[String], uri: &str) -> bool {
80 values
81 .iter()
82 .flat_map(|value| value.split(','))
83 .map(str::trim)
84 .filter(|entry| !entry.is_empty())
85 .any(|entry| entry == uri)
86}
87
88pub(crate) fn extensions_echo(uris: &[&str]) -> String {
91 if uris.is_empty() {
92 return String::new();
93 }
94 format!("{A2A_EXTENSIONS_HEADER}: {}\r\n", uris.join(", "))
95}
96
97pub(crate) const PARSE_ERROR: i32 = -32700;
101pub(crate) const INVALID_REQUEST: i32 = -32600;
103pub(crate) const METHOD_NOT_FOUND: i32 = -32601;
105pub(crate) const INVALID_PARAMS: i32 = -32602;
107pub(crate) const INTERNAL_ERROR: i32 = -32603;
109
110#[derive(Debug, Clone)]
113pub(crate) struct JsonRpcRequest {
114 pub id: Value,
116 pub method: String,
117 pub params: Value,
120}
121
122impl JsonRpcRequest {
123 pub(crate) fn from_body(body: &str) -> Result<JsonRpcRequest, JsonRpcResponse> {
132 let Ok(value) = serde_json::from_str::<Value>(body) else {
133 return Err(JsonRpcResponse::err(
134 Value::Null,
135 PARSE_ERROR,
136 "Invalid JSON payload",
137 ));
138 };
139 let Value::Object(mut object) = value else {
140 return Err(Self::invalid(Value::Null));
141 };
142 let id = object
143 .remove("id")
144 .filter(|id| id.is_string() || id.is_i64() || id.is_u64());
145 let jsonrpc = object.remove("jsonrpc");
146 let (Some(id), Some("2.0"), Some(Value::String(method))) = (
147 id.clone(),
148 jsonrpc.as_ref().and_then(Value::as_str),
149 object.remove("method"),
150 ) else {
151 return Err(Self::invalid(id.unwrap_or(Value::Null)));
152 };
153 let params = object
154 .remove("params")
155 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
156 Ok(JsonRpcRequest { id, method, params })
157 }
158
159 fn invalid(id: Value) -> JsonRpcResponse {
160 JsonRpcResponse::err(id, INVALID_REQUEST, "Request payload validation error")
161 }
162}
163
164#[derive(Debug, Clone, Serialize)]
165pub(crate) struct JsonRpcResponse {
166 pub jsonrpc: &'static str,
167 pub id: Value,
168 #[serde(skip_serializing_if = "Option::is_none")]
169 pub result: Option<Value>,
170 #[serde(skip_serializing_if = "Option::is_none")]
173 pub error: Option<Box<JsonRpcError>>,
174}
175
176impl JsonRpcResponse {
177 pub(crate) fn ok(id: Value, result: impl Serialize) -> Self {
178 Self {
179 jsonrpc: "2.0",
180 id,
181 result: Some(serde_json::to_value(result).unwrap_or(Value::Null)),
182 error: None,
183 }
184 }
185
186 pub(crate) fn err(id: Value, code: i32, message: &str) -> Self {
188 Self::error(id, code, message, None)
189 }
190
191 pub(crate) fn a2a_error(
194 id: Value,
195 error: A2aError,
196 message: &str,
197 metadata: &[(&str, String)],
198 ) -> Self {
199 let data = error_info(error.reason(), A2A_ERROR_DOMAIN, metadata);
200 Self::error(id, error.code(), message, Some(vec![data]))
201 }
202
203 pub(crate) fn murmur_error(
206 id: Value,
207 error: MurmurError,
208 message: &str,
209 metadata: &[(&str, String)],
210 ) -> Self {
211 let data = error_info(error.reason(), MURMUR_ERROR_DOMAIN, metadata);
212 Self::error(id, error.code(), message, Some(vec![data]))
213 }
214
215 fn error(id: Value, code: i32, message: &str, data: Option<Vec<Value>>) -> Self {
216 Self {
217 jsonrpc: "2.0",
218 id,
219 result: None,
220 error: Some(Box::new(JsonRpcError {
221 code,
222 message: message.to_string(),
223 data,
224 })),
225 }
226 }
227
228 pub(crate) fn into_http_response(self) -> String {
229 self.into_http_response_echoing(&[])
230 }
231
232 pub(crate) fn into_http_response_echoing(self, extensions: &[&str]) -> String {
235 let echo = if self.error.is_none() {
236 extensions_echo(extensions)
237 } else {
238 String::new()
239 };
240 let body = serde_json::to_string(&self).unwrap_or_else(|_| "{}".to_string());
241 format!(
242 "HTTP/1.1 200 OK\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n{echo}\r\n{}",
243 body.len(),
244 body,
245 )
246 }
247}
248
249#[derive(Debug, Clone, Serialize)]
250pub(crate) struct JsonRpcError {
251 pub code: i32,
252 pub message: String,
253 #[serde(skip_serializing_if = "Option::is_none")]
255 pub data: Option<Vec<Value>>,
256}
257
258const ERROR_INFO_TYPE: &str = "type.googleapis.com/google.rpc.ErrorInfo";
260
261pub(crate) const A2A_ERROR_DOMAIN: &str = "a2a-protocol.org";
263
264pub(crate) const MURMUR_ERROR_DOMAIN: &str = "murmur.nexus";
266
267fn error_info(reason: &str, domain: &str, metadata: &[(&str, String)]) -> Value {
269 let metadata: serde_json::Map<String, Value> = metadata
270 .iter()
271 .map(|(key, value)| (key.to_string(), Value::String(value.clone())))
272 .collect();
273 serde_json::json!({
274 "@type": ERROR_INFO_TYPE,
275 "reason": reason,
276 "domain": domain,
277 "metadata": metadata,
278 })
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
288pub enum A2aError {
289 TaskNotFound,
290 TaskNotCancelable,
291 PushNotificationNotSupported,
292 UnsupportedOperation,
293 ContentTypeNotSupported,
294 InvalidAgentResponse,
295 ExtendedAgentCardNotConfigured,
296 ExtensionSupportRequired,
297 VersionNotSupported,
298}
299
300impl A2aError {
301 pub const ALL: [A2aError; 9] = [
303 A2aError::TaskNotFound,
304 A2aError::TaskNotCancelable,
305 A2aError::PushNotificationNotSupported,
306 A2aError::UnsupportedOperation,
307 A2aError::ContentTypeNotSupported,
308 A2aError::InvalidAgentResponse,
309 A2aError::ExtendedAgentCardNotConfigured,
310 A2aError::ExtensionSupportRequired,
311 A2aError::VersionNotSupported,
312 ];
313
314 pub fn code(self) -> i32 {
316 match self {
317 A2aError::TaskNotFound => -32001,
318 A2aError::TaskNotCancelable => -32002,
319 A2aError::PushNotificationNotSupported => -32003,
320 A2aError::UnsupportedOperation => -32004,
321 A2aError::ContentTypeNotSupported => -32005,
322 A2aError::InvalidAgentResponse => -32006,
323 A2aError::ExtendedAgentCardNotConfigured => -32007,
324 A2aError::ExtensionSupportRequired => -32008,
325 A2aError::VersionNotSupported => -32009,
326 }
327 }
328
329 pub fn reason(self) -> &'static str {
331 match self {
332 A2aError::TaskNotFound => "TASK_NOT_FOUND",
333 A2aError::TaskNotCancelable => "TASK_NOT_CANCELABLE",
334 A2aError::PushNotificationNotSupported => "PUSH_NOTIFICATION_NOT_SUPPORTED",
335 A2aError::UnsupportedOperation => "UNSUPPORTED_OPERATION",
336 A2aError::ContentTypeNotSupported => "CONTENT_TYPE_NOT_SUPPORTED",
337 A2aError::InvalidAgentResponse => "INVALID_AGENT_RESPONSE",
338 A2aError::ExtendedAgentCardNotConfigured => "EXTENDED_AGENT_CARD_NOT_CONFIGURED",
339 A2aError::ExtensionSupportRequired => "EXTENSION_SUPPORT_REQUIRED",
340 A2aError::VersionNotSupported => "VERSION_NOT_SUPPORTED",
341 }
342 }
343
344 pub fn from_code(code: i32) -> Option<A2aError> {
346 Self::ALL.into_iter().find(|error| error.code() == code)
347 }
348}
349
350#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356pub enum MurmurError {
357 CompletionMisaddressed,
359 CompletionNotAwaited,
362}
363
364impl MurmurError {
365 pub const ALL: [MurmurError; 2] = [
367 MurmurError::CompletionMisaddressed,
368 MurmurError::CompletionNotAwaited,
369 ];
370
371 pub fn code(self) -> i32 {
373 match self {
374 MurmurError::CompletionMisaddressed => -31001,
375 MurmurError::CompletionNotAwaited => -31002,
376 }
377 }
378
379 pub fn reason(self) -> &'static str {
381 match self {
382 MurmurError::CompletionMisaddressed => "COMPLETION_MISADDRESSED",
383 MurmurError::CompletionNotAwaited => "COMPLETION_NOT_AWAITED",
384 }
385 }
386}
387
388pub(crate) const TEXT_MEDIA_TYPE: &str = "text/plain";
392
393pub(crate) const DATA_MEDIA_TYPE: &str = "application/json";
395
396#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
398pub(crate) enum Role {
399 #[serde(rename = "ROLE_USER")]
400 User,
401 #[serde(rename = "ROLE_AGENT")]
402 Agent,
403}
404
405impl Role {
406 pub(crate) fn wire_name(self) -> &'static str {
408 match self {
409 Self::User => "ROLE_USER",
410 Self::Agent => "ROLE_AGENT",
411 }
412 }
413}
414
415#[derive(Debug, Clone, PartialEq)]
417pub(crate) enum PartContent {
418 Text(String),
419 Raw(String),
421 Url(String),
423 Data(Value),
425}
426
427impl PartContent {
428 pub(crate) fn kind(&self) -> &'static str {
430 match self {
431 Self::Text(_) => "text",
432 Self::Raw(_) => "raw",
433 Self::Url(_) => "url",
434 Self::Data(_) => "data",
435 }
436 }
437}
438
439const PART_CONTENT_FIELDS: [&str; 4] = ["text", "raw", "url", "data"];
441
442#[derive(Debug, Clone, PartialEq)]
448pub(crate) struct Part {
449 pub content: PartContent,
450 pub media_type: Option<String>,
451 pub filename: Option<String>,
452 pub metadata: Option<Value>,
453}
454
455impl Part {
456 pub(crate) fn text(text: impl Into<String>) -> Self {
458 Self::of(PartContent::Text(text.into()), TEXT_MEDIA_TYPE)
459 }
460
461 pub(crate) fn data(value: Value) -> Self {
463 Self::of(PartContent::Data(value), DATA_MEDIA_TYPE)
464 }
465
466 fn of(content: PartContent, media_type: &str) -> Self {
467 Self {
468 content,
469 media_type: Some(media_type.to_string()),
470 filename: None,
471 metadata: None,
472 }
473 }
474
475 pub(crate) fn as_text(&self) -> Option<&str> {
477 match &self.content {
478 PartContent::Text(text) => Some(text),
479 _ => None,
480 }
481 }
482
483 #[cfg(test)]
485 pub(crate) fn as_data(&self) -> Option<&Value> {
486 match &self.content {
487 PartContent::Data(value) => Some(value),
488 _ => None,
489 }
490 }
491
492 pub(crate) fn to_json(&self) -> Value {
494 let mut object = serde_json::Map::new();
495 let (field, value) = match &self.content {
496 PartContent::Text(text) => ("text", Value::String(text.clone())),
497 PartContent::Raw(raw) => ("raw", Value::String(raw.clone())),
498 PartContent::Url(url) => ("url", Value::String(url.clone())),
499 PartContent::Data(value) => ("data", value.clone()),
500 };
501 object.insert(field.to_string(), value);
502 if let Some(media_type) = &self.media_type {
503 object.insert("mediaType".to_string(), Value::String(media_type.clone()));
504 }
505 if let Some(filename) = &self.filename {
506 object.insert("filename".to_string(), Value::String(filename.clone()));
507 }
508 if let Some(metadata) = &self.metadata {
509 object.insert("metadata".to_string(), metadata.clone());
510 }
511 Value::Object(object)
512 }
513
514 fn content_count_error(value: &Value) -> Option<String> {
517 let Some(object) = value.as_object() else {
518 return Some("is not an object".to_string());
519 };
520 let set: Vec<&str> = PART_CONTENT_FIELDS
521 .into_iter()
522 .filter(|field| object.contains_key(*field))
523 .collect();
524 match set.as_slice() {
525 [_] => None,
526 [] => Some("sets none of text, raw, url and data".to_string()),
527 more => Some(format!(
528 "sets {}; a part sets exactly one of text, raw, url and data",
529 more.join(" and ")
530 )),
531 }
532 }
533
534 pub(crate) fn from_json(value: &Value) -> Result<Part, String> {
538 if let Some(error) = Self::content_count_error(value) {
539 return Err(error);
540 }
541 let object = value.as_object().expect("checked to be an object");
542 let string = |field: &str| -> Result<Option<String>, String> {
543 match object.get(field) {
544 None => Ok(None),
545 Some(Value::String(text)) => Ok(Some(text.clone())),
546 Some(_) => Err(format!("{field} is not a string")),
547 }
548 };
549 let content = if let Some(text) = string("text")? {
550 PartContent::Text(text)
551 } else if let Some(raw) = string("raw")? {
552 PartContent::Raw(raw)
553 } else if let Some(url) = string("url")? {
554 PartContent::Url(url)
555 } else {
556 PartContent::Data(object.get("data").cloned().unwrap_or(Value::Null))
557 };
558 Ok(Part {
559 content,
560 media_type: string("mediaType")?,
561 filename: string("filename")?,
562 metadata: object.get("metadata").cloned(),
563 })
564 }
565}
566
567impl Serialize for Part {
568 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
569 self.to_json().serialize(serializer)
570 }
571}
572
573impl<'de> Deserialize<'de> for Part {
574 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
575 let value = Value::deserialize(deserializer)?;
576 Part::from_json(&value).map_err(|error| serde::de::Error::custom(format!("part {error}")))
577 }
578}
579
580#[derive(Debug, Clone, Serialize, Deserialize)]
581#[serde(rename_all = "camelCase")]
582pub(crate) struct A2aTask {
583 pub id: String,
584 pub context_id: String,
585 pub status: TaskStatus,
586 #[serde(skip_serializing_if = "Option::is_none")]
587 pub artifacts: Option<Vec<A2aArtifact>>,
588 #[serde(default, skip_serializing_if = "Option::is_none")]
592 pub metadata: Option<serde_json::Value>,
593}
594
595impl A2aTask {
596 pub(crate) fn artifact(&self, artifact_id: &str) -> Option<&A2aArtifact> {
598 self.artifacts
599 .as_deref()?
600 .iter()
601 .find(|artifact| artifact.artifact_id == artifact_id)
602 }
603
604 pub(crate) fn status_text(&self) -> Option<&str> {
606 self.status.message.as_ref()?.text()
607 }
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize)]
613#[serde(rename_all = "camelCase")]
614pub(crate) struct A2aArtifact {
615 pub artifact_id: String,
616 #[serde(default)]
617 pub name: String,
618 pub parts: Vec<Part>,
619}
620
621impl A2aArtifact {
622 pub(crate) fn named(name: &str, parts: Vec<Part>) -> Self {
624 Self {
625 artifact_id: name.to_string(),
626 name: name.to_string(),
627 parts,
628 }
629 }
630
631 pub(crate) fn first_text(&self) -> Option<&str> {
633 self.parts.first()?.as_text()
634 }
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize)]
638pub(crate) struct TaskStatus {
639 pub state: TaskState,
640 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub message: Option<StatusMessage>,
643 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub timestamp: Option<String>,
647}
648
649impl TaskStatus {
650 pub(crate) fn of(state: TaskState) -> Self {
652 Self::at(state, None, status_now())
653 }
654
655 pub(crate) fn at(
657 state: TaskState,
658 message: Option<StatusMessage>,
659 when: DateTime<Utc>,
660 ) -> Self {
661 Self {
662 state,
663 message,
664 timestamp: Some(status_timestamp(when)),
665 }
666 }
667}
668
669pub(crate) fn status_now() -> DateTime<Utc> {
672 let now = Utc::now();
673 DateTime::from_timestamp_millis(now.timestamp_millis()).unwrap_or(now)
674}
675
676#[cfg(test)]
679pub(crate) fn assert_status_timestamp(status: &Value) -> DateTime<Utc> {
680 let timestamp = status["timestamp"]
681 .as_str()
682 .unwrap_or_else(|| panic!("no timestamp: {status}"));
683 let parsed = DateTime::parse_from_rfc3339(timestamp)
684 .unwrap_or_else(|error| panic!("{timestamp} is not RFC 3339: {error}"))
685 .with_timezone(&Utc);
686 assert_eq!(status_timestamp(parsed), timestamp, "{status}");
687 parsed
688}
689
690#[cfg(test)]
693pub(crate) fn assert_bare_status(status: &Value, state: &str) {
694 assert_status_timestamp(status);
695 assert_eq!(
696 *status,
697 serde_json::json!({"state": state, "timestamp": status["timestamp"]})
698 );
699}
700
701pub(crate) fn status_timestamp(at: DateTime<Utc>) -> String {
704 at.to_rfc3339_opts(SecondsFormat::Millis, true)
705}
706
707#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
709#[serde(rename_all = "camelCase")]
710pub(crate) struct StatusMessage {
711 pub message_id: String,
712 pub context_id: String,
713 pub task_id: String,
714 pub role: Role,
715 pub parts: Vec<Part>,
716}
717
718impl StatusMessage {
719 pub(crate) fn agent(task_id: &str, context_id: &str, text: &str) -> Self {
721 Self {
722 message_id: format!("msg_{task_id}_status"),
723 context_id: context_id.to_string(),
724 task_id: task_id.to_string(),
725 role: Role::Agent,
726 parts: vec![Part::text(text)],
727 }
728 }
729
730 pub(crate) fn text(&self) -> Option<&str> {
732 self.parts.first()?.as_text()
733 }
734}
735
736#[derive(Debug, Clone, PartialEq, Eq)]
738struct TaskEnding {
739 message: String,
741 response: Option<String>,
743 no_answer: bool,
745 no_answer_below: Vec<(String, String)>,
747}
748
749impl TaskEnding {
750 fn metadata(&self) -> Option<serde_json::Value> {
752 let mut murmur = serde_json::Map::new();
753 if self.no_answer {
754 murmur.insert("noAnswer".to_string(), serde_json::Value::Bool(true));
755 }
756 if !self.no_answer_below.is_empty() {
757 let below = self
758 .no_answer_below
759 .iter()
760 .map(|(member, status)| serde_json::json!({"member": member, "status": status}))
761 .collect();
762 murmur.insert("noAnswerBelow".to_string(), serde_json::Value::Array(below));
763 }
764 (!murmur.is_empty()).then(|| serde_json::json!({ "murmur": murmur }))
765 }
766}
767
768pub(crate) const RESPONSE_ARTIFACT: &str = "response";
771
772pub(crate) const PROMPT_ARTIFACT: &str = "prompt";
774
775#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
786pub(crate) enum TaskState {
787 #[serde(rename = "TASK_STATE_SUBMITTED")]
788 Submitted,
789 #[serde(rename = "TASK_STATE_WORKING")]
790 Working,
791 #[serde(rename = "TASK_STATE_INPUT_REQUIRED")]
792 InputRequired,
793 #[serde(rename = "TASK_STATE_COMPLETED")]
794 Completed,
795 #[serde(rename = "TASK_STATE_FAILED")]
796 Failed,
797 #[serde(rename = "TASK_STATE_REJECTED")]
798 Rejected,
799 #[serde(rename = "TASK_STATE_CANCELED")]
802 Canceled,
803}
804
805impl TaskState {
806 pub(crate) const ALL: [TaskState; 7] = [
808 TaskState::Submitted,
809 TaskState::Working,
810 TaskState::Completed,
811 TaskState::Failed,
812 TaskState::Canceled,
813 TaskState::InputRequired,
814 TaskState::Rejected,
815 ];
816
817 pub(crate) fn wire_name(&self) -> &'static str {
819 match self {
820 Self::Submitted => "TASK_STATE_SUBMITTED",
821 Self::Working => "TASK_STATE_WORKING",
822 Self::InputRequired => "TASK_STATE_INPUT_REQUIRED",
823 Self::Completed => "TASK_STATE_COMPLETED",
824 Self::Failed => "TASK_STATE_FAILED",
825 Self::Rejected => "TASK_STATE_REJECTED",
826 Self::Canceled => "TASK_STATE_CANCELED",
827 }
828 }
829
830 pub(crate) fn from_wire_name(wire: &str) -> Option<TaskState> {
833 Self::ALL
834 .into_iter()
835 .find(|state| state.wire_name() == wire)
836 }
837
838 pub(crate) fn as_str(&self) -> &'static str {
843 match self {
844 Self::Submitted => "submitted",
845 Self::Working => "working",
846 Self::InputRequired => "input-required",
847 Self::Completed => "completed",
848 Self::Failed => "failed",
849 Self::Rejected => "rejected",
850 Self::Canceled => "canceled",
851 }
852 }
853
854 pub(crate) fn is_terminal(&self) -> bool {
859 match self {
860 Self::Submitted | Self::Working | Self::InputRequired => false,
861 Self::Completed | Self::Failed | Self::Rejected | Self::Canceled => true,
862 }
863 }
864}
865
866pub fn murmur_state_name(wire: &str) -> Option<&'static str> {
873 Some(match wire {
874 "TASK_STATE_SUBMITTED" => "submitted",
875 "TASK_STATE_WORKING" => "working",
876 "TASK_STATE_COMPLETED" => "completed",
877 "TASK_STATE_FAILED" => "failed",
878 "TASK_STATE_CANCELED" => "canceled",
879 "TASK_STATE_INPUT_REQUIRED" => "input-required",
880 "TASK_STATE_REJECTED" => "rejected",
881 "TASK_STATE_AUTH_REQUIRED" => "auth-required",
882 _ => return None,
883 })
884}
885
886pub(crate) const MAX_REFERENCE_TASK_IDS: usize = 16;
890
891#[derive(Debug, Clone, PartialEq)]
894pub(crate) struct IncomingMessage {
895 pub message_id: String,
896 pub context_id: Option<String>,
897 pub task_id: Option<String>,
899 pub reference_task_ids: Vec<String>,
901 pub parts: Vec<Part>,
903 pub door_extension: bool,
907 pub forget_session: bool,
911 pub return_immediately: bool,
914}
915
916#[derive(Debug, Clone, PartialEq, Eq)]
918pub(crate) enum MessageRefusal {
919 InvalidParams(String),
921 ContentTypeNotSupported {
924 part_index: usize,
925 kind: &'static str,
926 media_type: Option<String>,
927 },
928 PushNotificationNotSupported,
931}
932
933impl MessageRefusal {
934 pub(crate) fn into_response(self, id: Value) -> JsonRpcResponse {
936 match self {
937 Self::InvalidParams(message) => {
938 JsonRpcResponse::err(id, INVALID_PARAMS, &format!("Invalid params: {message}"))
939 }
940 Self::ContentTypeNotSupported {
941 part_index,
942 kind,
943 media_type,
944 } => {
945 let mut metadata = vec![("partIndex", part_index.to_string())];
946 if let Some(media_type) = &media_type {
947 metadata.push(("mediaType", media_type.clone()));
948 }
949 JsonRpcResponse::a2a_error(
950 id,
951 A2aError::ContentTypeNotSupported,
952 &format!(
953 "message.parts[{part_index}] is a {kind} part; this agent reads text and \
954 data parts only"
955 ),
956 &metadata,
957 )
958 }
959 Self::PushNotificationNotSupported => push_notification_not_supported(id),
960 }
961 }
962}
963
964pub(crate) fn push_notification_not_supported(id: Value) -> JsonRpcResponse {
967 JsonRpcResponse::a2a_error(
968 id,
969 A2aError::PushNotificationNotSupported,
970 "push notifications are not supported by this agent",
971 &[],
972 )
973}
974
975pub(crate) fn read_send_params(params: &Value) -> Result<IncomingMessage, MessageRefusal> {
992 let invalid = |message: String| Err(MessageRefusal::InvalidParams(message));
993 let Some(message) = params.get("message").and_then(Value::as_object) else {
994 return invalid("params.message must be an A2A Message object".to_string());
995 };
996 let message_id = match message.get("messageId") {
997 Some(Value::String(id)) if !id.is_empty() => id.clone(),
998 _ => return invalid("message.messageId must be a non-empty string".to_string()),
999 };
1000 if message.get("role").and_then(Value::as_str) != Some(Role::User.wire_name()) {
1001 return invalid(format!(
1002 "message.role must be {}: a message to this agent is from the user",
1003 Role::User.wire_name()
1004 ));
1005 }
1006 let raw_parts = match message.get("parts") {
1007 Some(Value::Array(parts)) if !parts.is_empty() => parts,
1008 _ => return invalid("message.parts must be a non-empty array".to_string()),
1009 };
1010 for (index, part) in raw_parts.iter().enumerate() {
1011 if let Some(error) = Part::content_count_error(part) {
1012 return invalid(format!("message.parts[{index}] {error}"));
1013 }
1014 }
1015 let mut parts = Vec::with_capacity(raw_parts.len());
1016 for (index, part) in raw_parts.iter().enumerate() {
1017 match Part::from_json(part) {
1018 Ok(part) => parts.push(part),
1019 Err(error) => return invalid(format!("message.parts[{index}].{error}")),
1020 }
1021 }
1022 let optional_string = |field: &str| -> Result<Option<String>, MessageRefusal> {
1023 match message.get(field) {
1024 None => Ok(None),
1025 Some(Value::String(value)) => Ok(Some(value.clone())),
1026 Some(_) => Err(MessageRefusal::InvalidParams(format!(
1027 "message.{field} must be a string"
1028 ))),
1029 }
1030 };
1031 let task_id = optional_string("taskId")?.filter(|id| !id.is_empty());
1032 let context_id = optional_string("contextId")?;
1033 let reference_task_ids = match message.get("referenceTaskIds") {
1034 None => Vec::new(),
1035 Some(Value::Array(ids)) => {
1036 let mut distinct: Vec<String> = Vec::new();
1037 for id in ids {
1038 match id {
1039 Value::String(id) if !id.is_empty() => {
1040 if !distinct.contains(id) {
1041 distinct.push(id.clone());
1042 }
1043 }
1044 _ => {
1045 return invalid(
1046 "message.referenceTaskIds must hold non-empty strings".to_string(),
1047 )
1048 }
1049 }
1050 }
1051 if distinct.len() > MAX_REFERENCE_TASK_IDS {
1052 return invalid(format!(
1053 "message.referenceTaskIds names {} tasks; at most {MAX_REFERENCE_TASK_IDS} \
1054 may be referenced",
1055 distinct.len()
1056 ));
1057 }
1058 distinct
1059 }
1060 Some(_) => return invalid("message.referenceTaskIds must be an array".to_string()),
1061 };
1062 let door_metadata = message
1063 .get("metadata")
1064 .and_then(Value::as_object)
1065 .and_then(|metadata| metadata.get(DOOR_EXTENSION_URI));
1066 let forget_session = match door_metadata {
1067 None => false,
1068 Some(Value::Object(door)) => match door.get("forgetSession") {
1069 None => false,
1070 Some(Value::Bool(forget)) => *forget,
1071 Some(_) => {
1072 return invalid(format!(
1073 "message.metadata[\"{DOOR_EXTENSION_URI}\"].forgetSession must be a boolean"
1074 ))
1075 }
1076 },
1077 Some(_) => {
1078 return invalid(format!(
1079 "message.metadata[\"{DOOR_EXTENSION_URI}\"] must be an object"
1080 ))
1081 }
1082 };
1083 if let (true, Some(task_id)) = (forget_session, &task_id) {
1084 return invalid(format!(
1085 "forgetSession starts a new conversation, so it goes on a message that starts a task, \
1086 not on one that continues task {task_id}"
1087 ));
1088 }
1089 let configuration = params.get("configuration");
1090 let return_immediately = match configuration.and_then(|c| c.get("returnImmediately")) {
1091 None | Some(Value::Null) => false,
1092 Some(Value::Bool(immediately)) => *immediately,
1093 Some(_) => return invalid("configuration.returnImmediately must be a boolean".to_string()),
1094 };
1095 if let Some((part_index, part)) = parts
1096 .iter()
1097 .enumerate()
1098 .find(|(_, part)| matches!(part.content, PartContent::Raw(_) | PartContent::Url(_)))
1099 {
1100 return Err(MessageRefusal::ContentTypeNotSupported {
1101 part_index,
1102 kind: part.content.kind(),
1103 media_type: part.media_type.clone(),
1104 });
1105 }
1106 if configuration
1107 .and_then(|configuration| configuration.get("taskPushNotificationConfig"))
1108 .is_some_and(|config| !config.is_null())
1109 {
1110 return Err(MessageRefusal::PushNotificationNotSupported);
1111 }
1112 Ok(IncomingMessage {
1113 message_id,
1114 context_id,
1115 task_id,
1116 reference_task_ids,
1117 parts,
1118 door_extension: door_metadata.is_some(),
1119 forget_session,
1120 return_immediately,
1121 })
1122}
1123
1124impl IncomingMessage {
1125 pub(crate) fn agent_text(&self) -> String {
1129 self.parts
1130 .iter()
1131 .map(|part| match &part.content {
1132 PartContent::Text(text) => text.clone(),
1133 PartContent::Data(value) => {
1134 let mut label = "data".to_string();
1135 if let Some(media_type) = &part.media_type {
1136 label.push_str(&format!(" media-type={}", fence_label_value(media_type)));
1137 }
1138 if let Some(filename) = &part.filename {
1139 label.push_str(&format!(" filename={}", fence_label_value(filename)));
1140 }
1141 let body = serde_json::to_string_pretty(value).unwrap_or_default();
1142 code_fence(&label, &body)
1143 }
1144 PartContent::Raw(_) | PartContent::Url(_) => {
1145 unreachable!("read_send_params refuses every file part")
1146 }
1147 })
1148 .collect::<Vec<_>>()
1149 .join("\n")
1150 }
1151
1152 pub(crate) fn part_kinds(&self) -> Vec<&'static str> {
1154 self.parts.iter().map(|part| part.content.kind()).collect()
1155 }
1156}
1157
1158pub(crate) fn code_fence(label: &str, body: &str) -> String {
1162 let mut longest = 0;
1163 let mut run = 0;
1164 for c in body.chars() {
1165 run = if c == '`' { run + 1 } else { 0 };
1166 longest = longest.max(run);
1167 }
1168 let fence = "`".repeat((longest + 1).max(3));
1169 format!("{fence}{label}\n{body}\n{fence}")
1170}
1171
1172fn fence_label_value(value: &str) -> String {
1174 value
1175 .chars()
1176 .map(|c| {
1177 if c.is_whitespace() || c == '`' || c == '=' {
1178 '_'
1179 } else {
1180 c
1181 }
1182 })
1183 .collect()
1184}
1185
1186pub(crate) fn user_message(message_id: &str, context_id: Option<&str>, text: &str) -> Value {
1191 let mut message = serde_json::json!({
1192 "messageId": message_id,
1193 "role": Role::User.wire_name(),
1194 "parts": [Part::text(text).to_json()],
1195 });
1196 if let Some(context_id) = context_id {
1197 message["contextId"] = Value::String(context_id.to_string());
1198 }
1199 message
1200}
1201
1202pub(crate) fn send_message_params(message: Value) -> Value {
1206 serde_json::json!({
1207 "message": message,
1208 "configuration": { "returnImmediately": true },
1209 })
1210}
1211
1212#[derive(Debug, Clone)]
1215pub(crate) enum SendMessageResult {
1216 Task(Box<A2aTask>),
1217 Message(Value),
1219}
1220
1221impl SendMessageResult {
1222 pub(crate) fn from_result(result: &Value) -> Result<SendMessageResult, String> {
1225 match send_message_member(result)? {
1226 ("task", task) => serde_json::from_value(task.clone())
1227 .map(|task| SendMessageResult::Task(Box::new(task)))
1228 .map_err(|error| format!("its task is not an A2A task: {error}")),
1229 (_, message) => Ok(SendMessageResult::Message(message.clone())),
1230 }
1231 }
1232}
1233
1234pub(crate) fn send_message_member(result: &Value) -> Result<(&'static str, &Value), String> {
1237 match (result.get("task"), result.get("message")) {
1238 (Some(task), None) => Ok(("task", task)),
1239 (None, Some(message)) => Ok(("message", message)),
1240 (Some(_), Some(_)) => Err("it answered both a task and a message".to_string()),
1241 (None, None) => Err(format!(
1242 "it answered neither a task nor a message: {result}"
1243 )),
1244 }
1245}
1246
1247#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1253pub(crate) enum CancelOutcome {
1254 Accepted,
1256 AlreadyTerminal,
1258 Unknown,
1260}
1261
1262#[derive(Debug, Clone)]
1265pub(crate) enum TaskSlotState {
1266 Empty,
1267 Running {
1268 task_id: String,
1269 context_id: String,
1270 lane: TaskLane,
1273 },
1274 Done,
1276}
1277
1278pub(crate) struct TaskRegistry {
1283 pub(crate) active_slot: TaskSlotState,
1284 pub(crate) history: HashMap<String, (TaskState, String)>,
1286 pub(crate) pending_count: usize,
1287 pub(crate) queue_depth: usize,
1288 pub(crate) task_acceptance: TaskAcceptance,
1289 input_waiters: HashMap<String, (String, oneshot::Sender<String>)>,
1291 input_timeouts: HashSet<String>,
1293 cancels: HashMap<String, CancelSignal>,
1299 resource_generation: Arc<AtomicU64>,
1303 closed: bool,
1306 endings: HashMap<String, TaskEnding>,
1308 owners: HashMap<String, String>,
1311 revision: u64,
1313 revisions: HashMap<String, u64>,
1315 status_times: HashMap<String, DateTime<Utc>>,
1318}
1319
1320pub(crate) const REJECTED_SESSION_ENDED_MESSAGE: &str =
1322 "task rejected: the session ended before this task started";
1323
1324pub(crate) const REJECTED_SESSION_STOPPED_MESSAGE: &str =
1326 "task rejected: the session was stopped before this task started";
1327
1328pub(crate) const REJECTED_SESSION_CLOSING_MESSAGE: &str = "task rejected: the session is closing";
1330
1331pub(crate) const REJECTED_BUSY_MESSAGE: &str = "task rejected: capsule is busy";
1334
1335impl TaskRegistry {
1336 pub(crate) fn new(queue_depth: usize, task_acceptance: TaskAcceptance) -> Self {
1337 Self {
1338 active_slot: TaskSlotState::Empty,
1339 history: HashMap::new(),
1340 pending_count: 0,
1341 queue_depth,
1342 task_acceptance,
1343 input_waiters: HashMap::new(),
1344 input_timeouts: HashSet::new(),
1345 cancels: HashMap::new(),
1346 resource_generation: Arc::new(AtomicU64::new(0)),
1347 closed: false,
1348 endings: HashMap::new(),
1349 owners: HashMap::new(),
1350 revision: 0,
1351 revisions: HashMap::new(),
1352 status_times: HashMap::new(),
1353 }
1354 }
1355
1356 pub(crate) fn resource_generation(&self) -> Arc<AtomicU64> {
1362 Arc::clone(&self.resource_generation)
1363 }
1364
1365 pub(crate) fn advance_resource_generation(&self) {
1372 self.resource_generation.fetch_add(1, Ordering::SeqCst);
1373 }
1374
1375 pub(crate) fn can_accept(&self) -> bool {
1376 if self.closed {
1377 return false;
1378 }
1379 match self.task_acceptance {
1380 TaskAcceptance::None => false,
1381 TaskAcceptance::Single => {
1382 matches!(self.active_slot, TaskSlotState::Empty) && self.pending_count == 0
1383 }
1384 TaskAcceptance::Queue => self.pending_count < self.queue_depth,
1385 }
1386 }
1387
1388 pub(crate) fn enqueue(&mut self, task_id: &str, context_id: &str) {
1389 self.pending_count += 1;
1390 self.record_state(task_id, TaskState::Submitted, context_id.to_string());
1391 }
1392
1393 pub(crate) fn withdraw(&mut self, task_id: &str) {
1396 self.pending_count = self.pending_count.saturating_sub(1);
1397 self.history.remove(task_id);
1398 self.revisions.remove(task_id);
1399 self.status_times.remove(task_id);
1400 self.owners.remove(task_id);
1401 }
1402
1403 pub(crate) fn record_state(&mut self, task_id: &str, state: TaskState, context_id: String) {
1407 self.history
1408 .insert(task_id.to_string(), (state, context_id));
1409 self.touch(task_id);
1410 }
1411
1412 fn touch(&mut self, task_id: &str) {
1415 self.revision += 1;
1416 self.revisions.insert(task_id.to_string(), self.revision);
1417 self.status_times.insert(task_id.to_string(), status_now());
1418 }
1419
1420 fn status_time(&self, task_id: &str) -> Option<DateTime<Utc>> {
1422 self.status_times.get(task_id).copied()
1423 }
1424
1425 pub(crate) fn record_owner(&mut self, task_id: &str, credential: &str) {
1428 self.owners
1429 .insert(task_id.to_string(), credential.to_string());
1430 }
1431
1432 pub(crate) fn owner(&self, task_id: &str) -> Option<&str> {
1435 self.owners.get(task_id).map(String::as_str)
1436 }
1437
1438 pub(crate) fn record_ending(
1443 &mut self,
1444 task_id: &str,
1445 message: &str,
1446 response: Option<&str>,
1447 no_answer: bool,
1448 no_answer_below: &[(String, String)],
1449 ) {
1450 let completed = matches!(self.history.get(task_id), Some((TaskState::Completed, _)));
1451 self.endings.insert(
1452 task_id.to_string(),
1453 TaskEnding {
1454 message: message.to_string(),
1455 response: response.filter(|_| completed).map(str::to_string),
1456 no_answer,
1457 no_answer_below: no_answer_below.to_vec(),
1458 },
1459 );
1460 self.touch(task_id);
1461 }
1462
1463 pub(crate) fn start_task(&mut self, task_id: String, context_id: String, lane: TaskLane) {
1466 debug_assert!(self.pending_count > 0);
1467 self.pending_count -= 1;
1468 self.record_state(&task_id, TaskState::Working, context_id.clone());
1469 self.active_slot = TaskSlotState::Running {
1470 task_id,
1471 context_id,
1472 lane,
1473 };
1474 }
1475
1476 pub(crate) fn active_lane(&self) -> Option<TaskLane> {
1481 match self.active_slot {
1482 TaskSlotState::Running { lane, .. } => Some(lane),
1483 TaskSlotState::Empty | TaskSlotState::Done => None,
1484 }
1485 }
1486
1487 pub(crate) fn finish_task(&mut self, final_state: TaskState) -> Option<TaskState> {
1491 let TaskSlotState::Running {
1492 ref task_id,
1493 ref context_id,
1494 ..
1495 } = self.active_slot
1496 else {
1497 return None;
1498 };
1499 let (tid, cid) = (task_id.clone(), context_id.clone());
1500 self.input_waiters.remove(&tid);
1501 self.input_timeouts.remove(&tid);
1502 self.cancels.remove(&tid);
1503 let final_state = match self.history.get(&tid) {
1507 Some((TaskState::Canceled, _)) => TaskState::Canceled,
1508 _ => final_state,
1509 };
1510 self.record_state(&tid, final_state.clone(), cid);
1511 self.active_slot = TaskSlotState::Done;
1512 Some(final_state)
1513 }
1514
1515 pub(crate) fn record_input_timeout(&mut self, task_id: &str) {
1520 self.input_waiters.remove(task_id);
1521 if let Some((TaskState::InputRequired, ctx)) = self.history.get(task_id).cloned() {
1522 self.record_state(task_id, TaskState::Working, ctx);
1523 }
1524 self.input_timeouts.insert(task_id.to_string());
1525 }
1526
1527 pub(crate) fn take_input_timeout(&mut self, task_id: &str) -> bool {
1531 self.input_timeouts.remove(task_id)
1532 }
1533
1534 pub(crate) fn set_input_required(
1537 &mut self,
1538 task_id: &str,
1539 prompt: String,
1540 tx: oneshot::Sender<String>,
1541 ) -> Result<(), &'static str> {
1542 match &self.active_slot {
1543 TaskSlotState::Running {
1544 task_id: active_id, ..
1545 } if active_id == task_id => {}
1546 _ => return Err("task is not the active running task"),
1547 }
1548 let state = self.history.get(task_id).map(|(s, _)| s);
1549 if !matches!(state, Some(TaskState::Working)) {
1550 return Err("task is not in working state");
1551 }
1552 if let Some((_, ctx)) = self.history.get(task_id).cloned() {
1553 self.record_state(task_id, TaskState::InputRequired, ctx);
1554 }
1555 self.input_waiters.insert(task_id.to_string(), (prompt, tx));
1556 Ok(())
1557 }
1558
1559 pub(crate) fn deliver_input(
1561 &mut self,
1562 task_id: &str,
1563 text: String,
1564 ) -> Result<(), &'static str> {
1565 let state = self.history.get(task_id).map(|(s, _)| s);
1566 if !matches!(state, Some(TaskState::InputRequired)) {
1567 return Err("task is not in input-required state");
1568 }
1569 let Some((_, tx)) = self.input_waiters.remove(task_id) else {
1570 return Err("no input waiter found for task");
1571 };
1572 if let Some((_, ctx)) = self.history.get(task_id).cloned() {
1573 self.record_state(task_id, TaskState::Working, ctx);
1574 }
1575 let _ = tx.send(text);
1577 Ok(())
1578 }
1579
1580 pub(crate) fn cancel_watch(&mut self, task_id: &str) -> CancelSignal {
1586 self.cancels.entry(task_id.to_string()).or_default().clone()
1587 }
1588
1589 pub(crate) fn is_canceled(&self, task_id: &str) -> bool {
1594 matches!(self.history.get(task_id), Some((TaskState::Canceled, _)))
1595 }
1596
1597 pub(crate) fn request_cancel(&mut self, task_id: &str) -> CancelOutcome {
1607 let Some((state, context_id)) = self.history.get(task_id).cloned() else {
1608 return CancelOutcome::Unknown;
1609 };
1610 if state.is_terminal() {
1611 return CancelOutcome::AlreadyTerminal;
1612 }
1613 if matches!(state, TaskState::Submitted) {
1614 self.pending_count = self.pending_count.saturating_sub(1);
1615 }
1616 self.record_state(task_id, TaskState::Canceled, context_id);
1617 self.cancel_watch(task_id).cancel();
1618 CancelOutcome::Accepted
1619 }
1620
1621 pub(crate) fn cancel_every_live(&mut self) -> Vec<String> {
1627 let live: Vec<String> = self
1628 .history
1629 .iter()
1630 .filter(|(_, (state, _))| !state.is_terminal())
1631 .map(|(task_id, _)| task_id.clone())
1632 .collect();
1633 let mut canceled: Vec<String> = live
1634 .into_iter()
1635 .filter(|task_id| matches!(self.request_cancel(task_id), CancelOutcome::Accepted))
1636 .collect();
1637 canceled.sort();
1640 canceled
1641 }
1642
1643 pub(crate) fn close_to_new_work(&mut self) -> Vec<(String, String)> {
1651 self.closed = true;
1652 let mut refused: Vec<(String, String)> = self
1653 .history
1654 .iter()
1655 .filter(|(_, (state, _))| matches!(state, TaskState::Submitted))
1656 .map(|(task_id, (_, context_id))| (task_id.clone(), context_id.clone()))
1657 .collect();
1658 refused.sort();
1659 for (task_id, context_id) in &refused {
1660 self.record_state(task_id, TaskState::Rejected, context_id.clone());
1661 self.cancels.remove(task_id);
1662 }
1663 self.pending_count = self.pending_count.saturating_sub(refused.len());
1664 refused
1665 }
1666
1667 pub(crate) fn is_closed(&self) -> bool {
1670 self.closed
1671 }
1672
1673 pub(crate) fn has_ending(&self, task_id: &str) -> bool {
1676 self.endings.contains_key(task_id)
1677 }
1678
1679 #[allow(dead_code)] pub(crate) fn get_input_prompt(&self, task_id: &str) -> Option<&str> {
1682 self.input_waiters
1683 .get(task_id)
1684 .map(|(prompt, _)| prompt.as_str())
1685 }
1686
1687 pub(crate) fn get_task(&self, task_id: &str) -> Option<A2aTask> {
1694 self.history.get(task_id).map(|(state, context_id)| {
1695 let artifact =
1696 |name: &str, text: &str| A2aArtifact::named(name, vec![Part::text(text)]);
1697 let ending = self.endings.get(task_id).filter(|_| state.is_terminal());
1698 let artifacts = match state {
1699 TaskState::InputRequired => self
1700 .input_waiters
1701 .get(task_id)
1702 .map(|(prompt, _)| vec![artifact(PROMPT_ARTIFACT, prompt)]),
1703 TaskState::Completed => ending
1704 .and_then(|ending| ending.response.as_deref())
1705 .filter(|response| !response.is_empty())
1706 .map(|response| vec![artifact(RESPONSE_ARTIFACT, response)]),
1707 _ => None,
1708 };
1709 let message = ending
1710 .filter(|ending| !ending.message.is_empty())
1711 .map(|ending| StatusMessage::agent(task_id, context_id, &ending.message));
1712 A2aTask {
1713 id: task_id.to_string(),
1714 context_id: context_id.clone(),
1715 status: TaskStatus::at(
1716 state.clone(),
1717 message,
1718 self.status_time(task_id).unwrap_or_else(status_now),
1719 ),
1720 artifacts,
1721 metadata: ending.and_then(TaskEnding::metadata),
1722 }
1723 })
1724 }
1725
1726 pub(crate) fn reference_block(
1734 &self,
1735 reference_task_ids: &[String],
1736 visible: impl Fn(&str) -> bool,
1737 ) -> Option<String> {
1738 if reference_task_ids.is_empty() {
1739 return None;
1740 }
1741 let lines: Vec<String> = reference_task_ids
1742 .iter()
1743 .map(|task_id| {
1744 let Some((state, _)) = self.history.get(task_id).filter(|_| visible(task_id))
1745 else {
1746 return format!("- {task_id}: not a task this capsule holds");
1747 };
1748 let mut line = format!("- {task_id}: {}", state.as_str());
1749 let ending = self.endings.get(task_id).filter(|_| state.is_terminal());
1750 match (state, ending) {
1751 (TaskState::Completed, Some(ending)) => {
1752 if let Some(response) =
1753 ending.response.as_deref().filter(|text| !text.is_empty())
1754 {
1755 line.push('\n');
1756 line.push_str(&code_fence(RESPONSE_ARTIFACT, response));
1757 }
1758 }
1759 (_, Some(ending)) if !ending.message.is_empty() => {
1760 line.push_str(": ");
1761 line.push_str(&ending.message.replace(['\r', '\n'], " "));
1762 }
1763 _ => {}
1764 }
1765 line
1766 })
1767 .collect();
1768 Some(format!("Referenced tasks:\n{}", lines.join("\n")))
1769 }
1770
1771 pub(crate) fn list_tasks(
1779 &self,
1780 filter: &TaskFilter,
1781 after: Option<&ListCursor>,
1782 page_size: usize,
1783 visible: impl Fn(&str) -> bool,
1784 ) -> TaskPage {
1785 let mut matching: Vec<ListCursor> = self
1786 .history
1787 .iter()
1788 .filter(|(task_id, (state, context_id))| {
1789 filter.matches(state, context_id, self.status_time(task_id)) && visible(task_id)
1790 })
1791 .map(|(task_id, _)| ListCursor {
1792 revision: self.revisions.get(task_id).copied().unwrap_or_default(),
1793 task_id: task_id.clone(),
1794 })
1795 .collect();
1796 matching.sort_unstable_by(|a, b| b.cmp(a));
1797 let total = matching.len();
1798 let rest: Vec<ListCursor> = matching
1799 .into_iter()
1800 .filter(|position| after.is_none_or(|after| position < after))
1801 .collect();
1802 let page: Vec<&ListCursor> = rest.iter().take(page_size).collect();
1803 let next = (rest.len() > page.len())
1804 .then(|| page.last().map(|last| (*last).clone()))
1805 .flatten();
1806 TaskPage {
1807 tasks: page
1808 .iter()
1809 .filter_map(|position| self.get_task(&position.task_id))
1810 .collect(),
1811 next,
1812 total,
1813 }
1814 }
1815}
1816
1817#[derive(Debug, Clone, PartialEq)]
1819pub(crate) struct TaskFilter {
1820 pub context_id: Option<String>,
1822 pub state: StateFilter,
1823 pub status_after: Option<DateTime<Utc>>,
1826}
1827
1828impl TaskFilter {
1829 fn matches(&self, state: &TaskState, context_id: &str, at: Option<DateTime<Utc>>) -> bool {
1830 let in_context = self.context_id.as_deref().is_none_or(|c| c == context_id);
1831 let in_state = match &self.state {
1832 StateFilter::Any => true,
1833 StateFilter::Only(wanted) => wanted == state,
1834 StateFilter::Nothing => false,
1835 };
1836 let in_time = self
1837 .status_after
1838 .is_none_or(|after| at.is_some_and(|at| at >= after));
1839 in_context && in_state && in_time
1840 }
1841}
1842
1843#[derive(Debug, Clone, PartialEq)]
1845pub(crate) enum StateFilter {
1846 Any,
1848 Only(TaskState),
1850 Nothing,
1852}
1853
1854impl StateFilter {
1855 pub(crate) fn from_wire_name(wire: &str) -> Option<StateFilter> {
1858 match wire {
1859 "TASK_STATE_UNSPECIFIED" => Some(StateFilter::Any),
1860 "TASK_STATE_AUTH_REQUIRED" => Some(StateFilter::Nothing),
1861 wire => TaskState::from_wire_name(wire).map(StateFilter::Only),
1862 }
1863 }
1864}
1865
1866#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1869pub(crate) struct ListCursor {
1870 pub revision: u64,
1871 pub task_id: String,
1872}
1873
1874impl ListCursor {
1875 pub(crate) fn encode(&self) -> String {
1878 use base64::Engine as _;
1879 base64::engine::general_purpose::URL_SAFE_NO_PAD
1880 .encode(format!("{}:{}", self.revision, self.task_id))
1881 }
1882
1883 pub(crate) fn decode(token: &str) -> Option<ListCursor> {
1885 use base64::Engine as _;
1886 let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
1887 .decode(token)
1888 .ok()?;
1889 let text = String::from_utf8(bytes).ok()?;
1890 let (revision, task_id) = text.split_once(':')?;
1891 let revision = revision
1892 .parse()
1893 .ok()
1894 .filter(|_| !revision.starts_with('+'))?;
1895 (!task_id.is_empty()).then(|| ListCursor {
1896 revision,
1897 task_id: task_id.to_string(),
1898 })
1899 }
1900}
1901
1902#[derive(Debug, Clone)]
1904pub(crate) struct TaskPage {
1905 pub tasks: Vec<A2aTask>,
1906 pub next: Option<ListCursor>,
1908 pub total: usize,
1910}
1911
1912#[derive(Debug)]
1915pub(crate) struct IncomingTask {
1916 pub task_id: String,
1917 pub context_id: String,
1918 pub message_id: String,
1919 pub message_text: String,
1920 pub traceparent: Option<String>,
1921 pub provenance: TaskProvenance,
1924 pub source: &'static str,
1928 pub forget_session: bool,
1932 pub caller_member: Option<String>,
1935 pub reference_task_ids: Vec<String>,
1938 pub part_kinds: Vec<&'static str>,
1941}
1942
1943pub(crate) const SOURCE_A2A: &str = "a2a";
1945
1946pub(crate) const SOURCE_DETACHED_SHELL: &str = "detached_shell";
1948
1949pub(crate) const SOURCE_DETACHED_LOST: &str = "detached_lost";
1953
1954#[cfg(test)]
1957mod tests {
1958 use super::*;
1959
1960 fn make_registry() -> TaskRegistry {
1961 TaskRegistry::new(1, TaskAcceptance::Single)
1962 }
1963
1964 fn running_registry(task_id: &str) -> TaskRegistry {
1965 let mut r = make_registry();
1966 r.enqueue(task_id, "ctx_001");
1967 r.start_task(task_id.to_string(), "ctx_001".to_string(), TaskLane::Bg);
1968 r
1969 }
1970
1971 #[test]
1972 fn set_input_required_transitions_state() {
1973 let mut r = running_registry("tsk_001");
1974 let (tx, _rx) = oneshot::channel();
1975 assert!(r
1976 .set_input_required("tsk_001", "which branch?".into(), tx)
1977 .is_ok());
1978 let task = r.get_task("tsk_001").unwrap();
1979 assert_eq!(task.status.state, TaskState::InputRequired);
1980 let artifacts = task.artifacts.unwrap();
1981 assert_eq!(artifacts[0].artifact_id, "prompt");
1982 assert_eq!(artifacts[0].first_text(), Some("which branch?"));
1983 }
1984
1985 #[test]
1986 fn set_input_required_fails_for_wrong_task() {
1987 let mut r = running_registry("tsk_001");
1988 let (tx, _rx) = oneshot::channel();
1989 assert!(r
1990 .set_input_required("tsk_other", "prompt".into(), tx)
1991 .is_err());
1992 }
1993
1994 #[test]
1995 fn deliver_input_transitions_back_to_working() {
1996 let mut r = running_registry("tsk_001");
1997 let (tx, mut rx) = oneshot::channel();
1998 r.set_input_required("tsk_001", "which branch?".into(), tx)
1999 .unwrap();
2000 r.deliver_input("tsk_001", "main".to_string()).unwrap();
2001 assert_eq!(rx.try_recv().unwrap(), "main");
2003 let task = r.get_task("tsk_001").unwrap();
2004 assert_eq!(task.status.state, TaskState::Working);
2005 assert!(task.artifacts.is_none());
2006 }
2007
2008 #[test]
2009 fn deliver_input_fails_when_not_input_required() {
2010 let mut r = running_registry("tsk_001");
2011 assert!(r.deliver_input("tsk_001", "text".into()).is_err());
2012 }
2013
2014 #[test]
2015 fn an_input_required_task_carries_its_prompt_and_nothing_else() {
2016 let mut r = running_registry("tsk_1");
2017 let (tx, _rx) = oneshot::channel();
2018 r.set_input_required("tsk_1", "Which file?".to_string(), tx)
2019 .unwrap();
2020 let task = r.get_task("tsk_1").unwrap();
2021 let artifacts = task.artifacts.unwrap();
2022 assert_eq!(artifacts.len(), 1);
2023 assert_eq!(
2024 serde_json::to_value(&artifacts[0]).unwrap(),
2025 serde_json::json!({"artifactId": "prompt", "name": "prompt",
2026 "parts": [{"text": "Which file?", "mediaType": "text/plain"}]})
2027 );
2028 assert!(task.status.message.is_none());
2029 }
2030
2031 #[test]
2033 fn a_working_task_carries_no_artifact_and_no_message() {
2034 let r = running_registry("tsk_1");
2035 let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2036 assert_bare_status(&task["status"], "TASK_STATE_WORKING");
2037 assert_eq!(
2038 task,
2039 serde_json::json!({"id": "tsk_1", "contextId": "ctx_001", "status": {
2040 "state": "TASK_STATE_WORKING",
2041 "timestamp": timestamp_of(&r, "tsk_1"),
2042 }})
2043 );
2044 }
2045
2046 #[test]
2049 fn a_completed_task_carries_its_response_artifact_and_status_message() {
2050 let mut r = running_registry("tsk_1");
2051 r.finish_task(TaskState::Completed);
2052 r.record_ending("tsk_1", "done", Some("the answer is 4"), false, &[]);
2053 let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2054 assert_eq!(
2055 task,
2056 serde_json::json!({
2057 "id": "tsk_1",
2058 "contextId": "ctx_001",
2059 "status": {
2060 "state": "TASK_STATE_COMPLETED",
2061 "message": {
2062 "messageId": "msg_tsk_1_status",
2063 "contextId": "ctx_001",
2064 "taskId": "tsk_1",
2065 "role": "ROLE_AGENT",
2066 "parts": [{"text": "done", "mediaType": "text/plain"}],
2067 },
2068 "timestamp": status_timestamp(r.status_times["tsk_1"]),
2069 },
2070 "artifacts": [{
2071 "artifactId": "response",
2072 "name": "response",
2073 "parts": [{"text": "the answer is 4", "mediaType": "text/plain"}],
2074 }],
2075 })
2076 );
2077 }
2078
2079 #[test]
2084 fn a_no_answer_ending_carries_its_metadata_and_no_other_ending_does() {
2085 let below = [("q".to_string(), "timed_out".to_string())];
2086 let mut r = running_registry("tsk_1");
2087 r.finish_task(TaskState::Failed);
2088 r.record_ending("tsk_1", "q gave no answer", None, true, &below);
2089 let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2090 assert_eq!(task["status"]["state"], "TASK_STATE_FAILED");
2091 assert_eq!(
2092 task["status"]["message"]["parts"][0]["text"],
2093 "q gave no answer"
2094 );
2095 assert_eq!(
2096 task["metadata"],
2097 serde_json::json!({"murmur": {
2098 "noAnswer": true,
2099 "noAnswerBelow": [{"member": "q", "status": "timed_out"}],
2100 }})
2101 );
2102
2103 let mut r = running_registry("tsk_2");
2104 r.finish_task(TaskState::Completed);
2105 r.record_ending("tsk_2", "session ended", Some("none came"), false, &below);
2106 let task = serde_json::to_value(r.get_task("tsk_2").unwrap()).unwrap();
2107 assert_eq!(
2108 task["artifacts"],
2109 serde_json::json!([{"artifactId": "response", "name": "response",
2110 "parts": [{"text": "none came", "mediaType": "text/plain"}]}])
2111 );
2112 assert_eq!(
2113 task["metadata"],
2114 serde_json::json!({"murmur": {
2115 "noAnswerBelow": [{"member": "q", "status": "timed_out"}],
2116 }})
2117 );
2118
2119 for state in [TaskState::Completed, TaskState::Failed] {
2120 let mut r = running_registry("tsk_3");
2121 r.finish_task(state);
2122 r.record_ending("tsk_3", "ended", Some("4"), false, &[]);
2123 let task = serde_json::to_value(r.get_task("tsk_3").unwrap()).unwrap();
2124 assert!(task.get("metadata").is_none(), "{task}");
2125 }
2126 let r = running_registry("tsk_4");
2127 let task = serde_json::to_value(r.get_task("tsk_4").unwrap()).unwrap();
2128 assert!(task.get("metadata").is_none(), "{task}");
2129 }
2130
2131 #[test]
2133 fn a_failed_task_carries_its_status_message_and_no_response() {
2134 let mut r = running_registry("tsk_1");
2135 r.finish_task(TaskState::Failed);
2136 r.record_ending("tsk_1", "the driver failed", Some("partial"), false, &[]);
2137 let task = r.get_task("tsk_1").unwrap();
2138 assert!(task.artifacts.is_none());
2139 let message = task.status.message.unwrap();
2140 assert_eq!(message.role, Role::Agent);
2141 assert_eq!(message.text(), Some("the driver failed"));
2142 }
2143
2144 #[test]
2146 fn a_completed_task_without_a_response_carries_no_artifact() {
2147 let mut r = running_registry("tsk_1");
2148 r.finish_task(TaskState::Completed);
2149 assert!(r.get_task("tsk_1").unwrap().artifacts.is_none());
2150 r.record_ending("tsk_1", "ok", Some(""), false, &[]);
2151 let task = r.get_task("tsk_1").unwrap();
2152 assert!(task.artifacts.is_none());
2153 assert_eq!(task.status.message.unwrap().text(), Some("ok"));
2154 }
2155
2156 #[test]
2157 fn an_owner_is_recorded_per_task() {
2158 let mut r = make_registry();
2159 r.enqueue("tsk_a", "ctx_001");
2160 r.record_owner("tsk_a", "member:lead");
2161 r.enqueue("tsk_b", "ctx_001");
2162 assert_eq!(r.owner("tsk_a"), Some("member:lead"));
2163 assert_eq!(r.owner("tsk_b"), None);
2164 r.withdraw("tsk_a");
2165 assert_eq!(r.owner("tsk_a"), None);
2166 assert!(r.get_task("tsk_a").is_none());
2167 assert_eq!(r.pending_count, 1);
2168 }
2169
2170 fn listed(page: &TaskPage) -> Vec<&str> {
2172 page.tasks.iter().map(|task| task.id.as_str()).collect()
2173 }
2174
2175 fn every_task() -> TaskFilter {
2176 TaskFilter {
2177 context_id: None,
2178 state: StateFilter::Any,
2179 status_after: None,
2180 }
2181 }
2182
2183 #[test]
2185 fn list_tasks_orders_by_the_last_change_newest_first() {
2186 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2187 for id in ["tsk_a", "tsk_b", "tsk_c"] {
2188 r.enqueue(id, "ctx_001");
2189 }
2190 let all = |r: &TaskRegistry| -> Vec<String> {
2191 listed(&r.list_tasks(&every_task(), None, 10, |_| true))
2192 .into_iter()
2193 .map(str::to_string)
2194 .collect()
2195 };
2196 assert_eq!(all(&r), ["tsk_c", "tsk_b", "tsk_a"]);
2197 r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::User);
2198 assert_eq!(all(&r), ["tsk_a", "tsk_c", "tsk_b"]);
2199 r.request_cancel("tsk_b");
2200 assert_eq!(all(&r), ["tsk_b", "tsk_a", "tsk_c"]);
2201 let (tx, _rx) = oneshot::channel();
2202 r.set_input_required("tsk_a", "which?".to_string(), tx)
2203 .unwrap();
2204 assert_eq!(all(&r), ["tsk_a", "tsk_b", "tsk_c"]);
2205 r.deliver_input("tsk_a", "left".to_string()).unwrap();
2206 r.finish_task(TaskState::Completed);
2207 r.record_ending("tsk_c", "", None, false, &[]);
2208 assert_eq!(all(&r), ["tsk_c", "tsk_a", "tsk_b"]);
2209 r.record_ending("tsk_a", "done", Some("four"), false, &[]);
2210 assert_eq!(all(&r), ["tsk_a", "tsk_c", "tsk_b"]);
2211 r.close_to_new_work();
2212 assert_eq!(all(&r), ["tsk_c", "tsk_a", "tsk_b"], "tsk_c was rejected");
2213
2214 let page = r.list_tasks(&every_task(), None, 10, |_| true);
2215 for task in &page.tasks {
2216 assert_eq!(
2217 serde_json::to_value(task).unwrap(),
2218 serde_json::to_value(r.get_task(&task.id).unwrap()).unwrap()
2219 );
2220 }
2221 }
2222
2223 #[test]
2226 fn list_tasks_walks_pages_and_returns_a_task_changed_mid_walk_once() {
2227 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2228 for id in ["tsk_1", "tsk_2", "tsk_3", "tsk_4", "tsk_5"] {
2229 r.enqueue(id, "ctx_001");
2230 }
2231 let first = r.list_tasks(&every_task(), None, 2, |_| true);
2232 assert_eq!(listed(&first), ["tsk_5", "tsk_4"]);
2233 assert_eq!(first.total, 5);
2234 let cursor = first.next.clone().expect("more tasks lie past the page");
2235 assert_eq!(ListCursor::decode(&cursor.encode()), Some(cursor.clone()));
2236
2237 r.request_cancel("tsk_4");
2238 r.request_cancel("tsk_2");
2239 let second = r.list_tasks(&every_task(), Some(&cursor), 2, |_| true);
2240 assert_eq!(listed(&second), ["tsk_3", "tsk_1"], "tsk_2 moved ahead");
2241 assert_eq!(second.next, None);
2242 assert_eq!(second.total, 5);
2243 let fresh = r.list_tasks(&every_task(), None, 2, |_| true);
2244 assert_eq!(listed(&fresh), ["tsk_2", "tsk_4"]);
2245
2246 let exact = r.list_tasks(&every_task(), None, 5, |_| true);
2247 assert_eq!(exact.tasks.len(), 5);
2248 assert_eq!(
2249 exact.next, None,
2250 "no task lies past a page that ends the list"
2251 );
2252 }
2253
2254 fn instant(timestamp: &str) -> DateTime<Utc> {
2256 DateTime::parse_from_rfc3339(timestamp)
2257 .unwrap()
2258 .with_timezone(&Utc)
2259 }
2260
2261 fn timestamp_of(r: &TaskRegistry, task_id: &str) -> String {
2262 r.get_task(task_id).unwrap().status.timestamp.unwrap()
2263 }
2264
2265 fn a_tick() {
2267 std::thread::sleep(std::time::Duration::from_millis(3));
2268 }
2269
2270 #[test]
2273 fn a_status_timestamp_moves_with_the_revision() {
2274 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2275 r.enqueue("tsk_a", "ctx_001");
2276 let submitted = timestamp_of(&r, "tsk_a");
2277 let submitted_revision = r.revisions["tsk_a"];
2278 assert_eq!(r.status_times["tsk_a"], instant(&submitted));
2279
2280 a_tick();
2281 r.record_owner("tsk_a", "alice");
2282 assert_eq!(
2283 timestamp_of(&r, "tsk_a"),
2284 submitted,
2285 "an owner is no change"
2286 );
2287 assert_eq!(r.revisions["tsk_a"], submitted_revision);
2288
2289 r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::User);
2290 let working = timestamp_of(&r, "tsk_a");
2291 assert!(r.revisions["tsk_a"] > submitted_revision);
2292 assert!(
2293 instant(&working) > instant(&submitted),
2294 "{submitted} → {working}"
2295 );
2296
2297 a_tick();
2298 r.finish_task(TaskState::Completed);
2299 let completed_revision = r.revisions["tsk_a"];
2300 let completed = timestamp_of(&r, "tsk_a");
2301 assert!(instant(&completed) > instant(&working));
2302
2303 a_tick();
2304 r.record_ending("tsk_a", "done", Some("four"), false, &[]);
2305 assert!(r.revisions["tsk_a"] > completed_revision);
2306 assert!(instant(×tamp_of(&r, "tsk_a")) > instant(&completed));
2307 }
2308
2309 #[test]
2312 fn a_status_timestamp_round_trips_and_a_cutoff_equal_to_it_includes_the_task() {
2313 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2314 r.enqueue("tsk_a", "ctx_001");
2315 let timestamp = timestamp_of(&r, "tsk_a");
2316 assert!(timestamp.ends_with('Z'), "{timestamp}");
2317 assert_eq!(
2318 timestamp.len(),
2319 "2026-10-13T09:00:00.123Z".len(),
2320 "{timestamp}"
2321 );
2322 assert_eq!(instant(×tamp), r.status_times["tsk_a"]);
2323 let at = TaskFilter {
2324 status_after: Some(instant(×tamp)),
2325 ..every_task()
2326 };
2327 assert_eq!(listed(&r.list_tasks(&at, None, 10, |_| true)), ["tsk_a"]);
2328 let after = TaskFilter {
2329 status_after: Some(instant(×tamp) + chrono::Duration::milliseconds(1)),
2330 ..every_task()
2331 };
2332 assert!(r.list_tasks(&after, None, 10, |_| true).tasks.is_empty());
2333 }
2334
2335 #[test]
2336 fn withdraw_drops_the_status_time() {
2337 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2338 r.enqueue("tsk_a", "ctx_001");
2339 r.withdraw("tsk_a");
2340 assert!(!r.status_times.contains_key("tsk_a"));
2341 assert!(r.get_task("tsk_a").is_none());
2342 }
2343
2344 #[test]
2347 fn list_tasks_filters_by_status_time_before_paging() {
2348 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2349 for id in ["tsk_1", "tsk_2", "tsk_3", "tsk_4"] {
2350 r.enqueue(id, "ctx_001");
2351 }
2352 r.request_cancel("tsk_3");
2353 let base = instant("2026-10-13T09:00:00Z");
2354 for (offset, id) in ["tsk_1", "tsk_2", "tsk_3", "tsk_4"].iter().enumerate() {
2355 r.status_times.insert(
2356 id.to_string(),
2357 base + chrono::Duration::seconds(offset as i64),
2358 );
2359 }
2360 let since = |seconds: i64| TaskFilter {
2361 status_after: Some(base + chrono::Duration::seconds(seconds)),
2362 ..every_task()
2363 };
2364 let first = r.list_tasks(&since(1), None, 2, |_| true);
2365 assert_eq!(listed(&first), ["tsk_3", "tsk_4"]);
2366 assert_eq!(first.total, 3);
2367 let second = r.list_tasks(&since(1), first.next.as_ref(), 2, |_| true);
2368 assert_eq!(listed(&second), ["tsk_2"]);
2369 assert_eq!((second.total, second.next), (3, None));
2370
2371 let canceled_since = TaskFilter {
2372 state: StateFilter::Only(TaskState::Canceled),
2373 ..since(1)
2374 };
2375 let page = r.list_tasks(&canceled_since, None, 10, |_| true);
2376 assert_eq!((listed(&page), page.total), (vec!["tsk_3"], 1));
2377 assert_eq!(r.list_tasks(&since(4), None, 10, |_| true).total, 0);
2378 assert_eq!(r.list_tasks(&every_task(), None, 10, |_| true).total, 4);
2379 }
2380
2381 #[test]
2383 fn list_tasks_filters_by_context_state_and_visibility() {
2384 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2385 r.enqueue("tsk_a", "ctx_one");
2386 r.record_owner("tsk_a", "alice");
2387 r.enqueue("tsk_b", "ctx_two");
2388 r.record_owner("tsk_b", "alice");
2389 r.enqueue("tsk_c", "ctx_one");
2390 r.record_owner("tsk_c", "bob");
2391 r.request_cancel("tsk_c");
2392 let alice = |task_id: &str| r.owner(task_id) == Some("alice");
2393 let page = r.list_tasks(&every_task(), None, 10, alice);
2394 assert_eq!((listed(&page), page.total), (vec!["tsk_b", "tsk_a"], 2));
2395 let in_one = TaskFilter {
2396 context_id: Some("ctx_one".to_string()),
2397 state: StateFilter::Any,
2398 status_after: None,
2399 };
2400 let page = r.list_tasks(&in_one, None, 10, |_| true);
2401 assert_eq!((listed(&page), page.total), (vec!["tsk_c", "tsk_a"], 2));
2402 let canceled = TaskFilter {
2403 context_id: None,
2404 state: StateFilter::Only(TaskState::Canceled),
2405 status_after: None,
2406 };
2407 let page = r.list_tasks(&canceled, None, 10, |_| true);
2408 assert_eq!(listed(&page), ["tsk_c"]);
2409 let page = r.list_tasks(&canceled, None, 10, alice);
2410 assert_eq!((page.tasks.len(), page.total, page.next), (0, 0, None));
2411 let nothing = TaskFilter {
2412 context_id: None,
2413 state: StateFilter::Nothing,
2414 status_after: None,
2415 };
2416 assert_eq!(r.list_tasks(¬hing, None, 10, |_| true).total, 0);
2417 }
2418
2419 #[test]
2420 fn list_cursor_reads_only_what_it_wrote() {
2421 let cursor = ListCursor {
2422 revision: 42,
2423 task_id: "tsk_01a1".to_string(),
2424 };
2425 let token = cursor.encode();
2426 assert!(!token.contains('='), "{token}");
2427 assert_eq!(ListCursor::decode(&token), Some(cursor));
2428 use base64::Engine as _;
2429 let encode = |text: &str| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(text);
2430 for garbage in [
2431 "not-a-token".to_string(),
2432 "".to_string(),
2433 "!!".to_string(),
2434 encode("42"),
2435 encode("42:"),
2436 encode("x:tsk_1"),
2437 encode("-1:tsk_1"),
2438 encode("+1:tsk_1"),
2439 ] {
2440 assert_eq!(ListCursor::decode(&garbage), None, "{garbage}");
2441 }
2442 }
2443
2444 #[test]
2445 fn list_state_filter_reads_every_v1_state_name() {
2446 for state in TaskState::ALL {
2447 assert_eq!(
2448 TaskState::from_wire_name(state.wire_name()),
2449 Some(state.clone())
2450 );
2451 assert_eq!(
2452 StateFilter::from_wire_name(state.wire_name()),
2453 Some(StateFilter::Only(state))
2454 );
2455 }
2456 assert_eq!(
2457 StateFilter::from_wire_name("TASK_STATE_UNSPECIFIED"),
2458 Some(StateFilter::Any)
2459 );
2460 assert_eq!(
2461 StateFilter::from_wire_name("TASK_STATE_AUTH_REQUIRED"),
2462 Some(StateFilter::Nothing)
2463 );
2464 for other in ["done", "completed", "task_state_completed", ""] {
2465 assert_eq!(StateFilter::from_wire_name(other), None, "{other}");
2466 assert_eq!(TaskState::from_wire_name(other), None, "{other}");
2467 }
2468 }
2469
2470 #[test]
2471 fn request_cancel_on_a_running_task_records_canceled() {
2472 let mut r = running_registry("tsk_001");
2473 let signal = r.cancel_watch("tsk_001");
2474 assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::Accepted);
2475 assert!(signal.is_canceled());
2476 assert!(r.is_canceled("tsk_001"));
2477 let task = r.get_task("tsk_001").unwrap();
2478 assert_eq!(task.status.state, TaskState::Canceled);
2479 }
2480
2481 #[test]
2482 fn request_cancel_on_a_completed_task_leaves_it_alone() {
2483 let mut r = running_registry("tsk_001");
2484 r.finish_task(TaskState::Completed);
2485 assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::AlreadyTerminal);
2486 assert_eq!(
2487 r.get_task("tsk_001").unwrap().status.state,
2488 TaskState::Completed
2489 );
2490 let mut r = running_registry("tsk_002");
2492 assert_eq!(r.request_cancel("tsk_002"), CancelOutcome::Accepted);
2493 assert_eq!(r.request_cancel("tsk_002"), CancelOutcome::AlreadyTerminal);
2494 }
2495
2496 #[test]
2497 fn cancel_every_live_cancels_live_tasks_in_order_and_leaves_terminal_ones() {
2498 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2499 r.enqueue("tsk_c", "ctx_001");
2500 r.enqueue("tsk_a", "ctx_001");
2501 r.enqueue("tsk_done", "ctx_001");
2502 r.start_task("tsk_done".to_string(), "ctx_001".to_string(), TaskLane::Bg);
2503 r.finish_task(TaskState::Completed);
2504 r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::Bg);
2505 let running = r.cancel_watch("tsk_a");
2506
2507 assert_eq!(r.cancel_every_live(), vec!["tsk_a", "tsk_c"]);
2508 assert!(running.is_canceled());
2509 assert!(r.is_canceled("tsk_c"));
2510 assert_eq!(
2511 r.get_task("tsk_done").unwrap().status.state,
2512 TaskState::Completed
2513 );
2514 assert!(r.cancel_every_live().is_empty());
2515 }
2516
2517 #[test]
2518 fn request_cancel_on_an_unknown_id_is_unknown() {
2519 let mut r = make_registry();
2520 assert_eq!(r.request_cancel("tsk_doesnotexist"), CancelOutcome::Unknown);
2521 assert!(!r.is_canceled("tsk_doesnotexist"));
2522 }
2523
2524 #[test]
2525 fn finish_task_does_not_overwrite_an_accepted_cancel() {
2526 let mut r = running_registry("tsk_001");
2527 assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::Accepted);
2528 assert_eq!(
2529 r.finish_task(TaskState::Completed),
2530 Some(TaskState::Canceled),
2531 "the state recorded is the accepted cancel, and the caller is told so"
2532 );
2533 assert_eq!(
2534 r.get_task("tsk_001").unwrap().status.state,
2535 TaskState::Canceled
2536 );
2537 assert_eq!(
2538 r.finish_task(TaskState::Failed),
2539 None,
2540 "a slot that is no longer running records nothing"
2541 );
2542 assert_eq!(
2543 r.get_task("tsk_001").unwrap().status.state,
2544 TaskState::Canceled
2545 );
2546 }
2547
2548 #[test]
2549 fn finish_task_returns_the_state_it_recorded() {
2550 let mut r = running_registry("tsk_001");
2551 assert_eq!(
2552 r.finish_task(TaskState::Completed),
2553 Some(TaskState::Completed)
2554 );
2555 assert_eq!(make_registry().finish_task(TaskState::Failed), None);
2556 }
2557
2558 #[test]
2559 fn an_input_timeout_leaves_the_task_working_and_is_read_once() {
2560 let mut r = running_registry("tsk_001");
2561 let (tx, _rx) = oneshot::channel();
2562 r.set_input_required("tsk_001", "prompt".into(), tx)
2563 .unwrap();
2564 r.record_input_timeout("tsk_001");
2565 assert_eq!(
2566 r.get_task("tsk_001").unwrap().status.state,
2567 TaskState::Working,
2568 "the task's terminal state is not the wait's to record"
2569 );
2570 assert!(
2571 r.get_input_prompt("tsk_001").is_none(),
2572 "the waiter is gone"
2573 );
2574 assert!(r.take_input_timeout("tsk_001"));
2575 assert!(
2576 !r.take_input_timeout("tsk_001"),
2577 "a reopened attempt does not end on the same timeout"
2578 );
2579 assert!(!r.take_input_timeout("tsk_other"));
2580 }
2581
2582 #[test]
2583 fn cancelling_a_queued_task_frees_its_queue_slot() {
2584 let mut r = TaskRegistry::new(2, TaskAcceptance::Queue);
2585 r.enqueue("tsk_a", "ctx_001");
2586 r.enqueue("tsk_b", "ctx_001");
2587 assert!(!r.can_accept(), "the queue is full");
2588 assert_eq!(r.request_cancel("tsk_b"), CancelOutcome::Accepted);
2589 assert!(r.can_accept(), "a cancelled task holds no slot");
2590 }
2591
2592 #[test]
2593 fn close_to_new_work_rejects_every_submitted_task_and_nothing_else() {
2594 let mut r = TaskRegistry::new(4, TaskAcceptance::Queue);
2595 for id in ["tsk_a", "tsk_b", "tsk_c", "tsk_d"] {
2596 r.enqueue(id, &format!("ctx_{id}"));
2597 }
2598 r.start_task("tsk_a".to_string(), "ctx_tsk_a".to_string(), TaskLane::Bg);
2599 let (tx, _rx) = oneshot::channel();
2600 r.set_input_required("tsk_a", "prompt".into(), tx).unwrap();
2601 assert_eq!(r.request_cancel("tsk_c"), CancelOutcome::Accepted);
2602 let pending_before = r.pending_count;
2603
2604 let refused = r.close_to_new_work();
2605
2606 assert_eq!(
2607 refused,
2608 vec![
2609 ("tsk_b".to_string(), "ctx_tsk_b".to_string()),
2610 ("tsk_d".to_string(), "ctx_tsk_d".to_string()),
2611 ]
2612 );
2613 for id in ["tsk_b", "tsk_d"] {
2614 assert_eq!(r.get_task(id).unwrap().status.state, TaskState::Rejected);
2615 }
2616 assert_eq!(r.pending_count, pending_before - 2);
2617 assert_eq!(r.pending_count, 0);
2618 assert_eq!(
2619 r.get_task("tsk_a").unwrap().status.state,
2620 TaskState::InputRequired,
2621 "a live task that is not submitted is left alone"
2622 );
2623 assert_eq!(
2624 r.get_task("tsk_c").unwrap().status.state,
2625 TaskState::Canceled
2626 );
2627 assert_eq!(r.request_cancel("tsk_b"), CancelOutcome::AlreadyTerminal);
2628 assert!(!r.is_canceled("tsk_b"));
2629 }
2630
2631 #[test]
2632 fn a_closed_registry_accepts_nothing_and_closes_once() {
2633 for (depth, mode) in [(1, TaskAcceptance::Single), (4, TaskAcceptance::Queue)] {
2634 let mut r = TaskRegistry::new(depth, mode.clone());
2635 assert!(r.can_accept());
2636 assert!(!r.is_closed());
2637 r.enqueue("tsk_a", "ctx_001");
2638 assert_eq!(r.close_to_new_work().len(), 1);
2639 assert!(r.is_closed());
2640 assert!(!r.can_accept(), "{mode:?}: a closed registry refuses work");
2641 assert!(r.close_to_new_work().is_empty(), "{mode:?}: closes once");
2642 assert!(!r.can_accept());
2643 }
2644 }
2645
2646 #[test]
2651 fn tasks_held_at_once_matches_what_the_registry_accepts() {
2652 const SAFETY_CAP: usize = 64;
2653 for (mode, depth, expected) in [
2654 (TaskAcceptance::None, 1, 0),
2655 (TaskAcceptance::Single, 1, 1),
2656 (TaskAcceptance::Queue, 1, 2),
2657 (TaskAcceptance::Queue, 3, 4),
2658 ] {
2659 let mut r = TaskRegistry::new(depth, mode.clone());
2660 let mut accepted = 0;
2661 while r.can_accept() && accepted < SAFETY_CAP {
2662 let task_id = format!("tsk_{accepted}");
2663 r.enqueue(&task_id, "ctx_001");
2664 if accepted == 0 {
2665 r.start_task(task_id, "ctx_001".to_string(), TaskLane::Peer);
2666 }
2667 accepted += 1;
2668 }
2669 let lifecycle = murmur_artifact::LifecycleConfig {
2670 task_acceptance: mode.clone(),
2671 queue_depth: depth,
2672 ..Default::default()
2673 };
2674 assert_eq!(accepted, expected, "{mode:?} at depth {depth}");
2675 assert_eq!(
2676 accepted,
2677 lifecycle.tasks_held_at_once(),
2678 "{mode:?} at depth {depth}: tasks_held_at_once drifted from the registry"
2679 );
2680 }
2681 }
2682
2683 #[test]
2684 fn task_state_serializes_as_its_wire_name_and_reads_only_that() {
2685 for state in TaskState::ALL {
2686 let wire = serde_json::to_value(&state).unwrap();
2687 assert_eq!(wire, serde_json::json!(state.wire_name()));
2688 assert_eq!(
2689 serde_json::from_value::<TaskState>(wire).unwrap(),
2690 state,
2691 "{state:?}"
2692 );
2693 assert!(
2694 serde_json::from_value::<TaskState>(serde_json::json!(state.as_str())).is_err(),
2695 "{state:?}: murmur's word is not a wire state"
2696 );
2697 }
2698 for refused in [
2699 serde_json::json!("TASK_STATE_UNSPECIFIED"),
2700 serde_json::json!("TASK_STATE_AUTH_REQUIRED"),
2701 serde_json::json!("task_state_working"),
2702 serde_json::json!(1),
2703 serde_json::json!(null),
2704 ] {
2705 assert!(
2706 serde_json::from_value::<TaskState>(refused.clone()).is_err(),
2707 "{refused}"
2708 );
2709 }
2710 }
2711
2712 #[test]
2713 fn task_state_as_str_is_murmurs_word() {
2714 let words: Vec<&str> = TaskState::ALL.iter().map(TaskState::as_str).collect();
2715 assert_eq!(
2716 words,
2717 [
2718 "submitted",
2719 "working",
2720 "completed",
2721 "failed",
2722 "canceled",
2723 "input-required",
2724 "rejected"
2725 ]
2726 );
2727 }
2728
2729 #[test]
2730 fn murmur_state_name_maps_the_eight_v1_states_and_nothing_else() {
2731 for state in TaskState::ALL {
2732 assert_eq!(murmur_state_name(state.wire_name()), Some(state.as_str()));
2733 }
2734 assert_eq!(
2735 murmur_state_name("TASK_STATE_AUTH_REQUIRED"),
2736 Some("auth-required")
2737 );
2738 for other in [
2739 "TASK_STATE_UNSPECIFIED",
2740 "working",
2741 "",
2742 "TASK_STATE_working",
2743 ] {
2744 assert_eq!(murmur_state_name(other), None, "{other:?}");
2745 }
2746 }
2747
2748 #[test]
2749 fn roles_serialize_as_their_proto_names() {
2750 assert_eq!(
2751 serde_json::to_value(Role::User).unwrap(),
2752 serde_json::json!("ROLE_USER")
2753 );
2754 assert_eq!(
2755 serde_json::to_value(Role::Agent).unwrap(),
2756 serde_json::json!("ROLE_AGENT")
2757 );
2758 assert!(serde_json::from_value::<Role>(serde_json::json!("agent")).is_err());
2759 }
2760
2761 #[test]
2762 fn finish_task_cleans_up_input_waiters() {
2763 let mut r = running_registry("tsk_001");
2764 let (tx, _rx) = oneshot::channel();
2765 r.set_input_required("tsk_001", "prompt".into(), tx)
2766 .unwrap();
2767 r.finish_task(TaskState::Failed);
2768 assert!(r.get_input_prompt("tsk_001").is_none());
2770 }
2771
2772 #[test]
2773 fn a2a_error_table_is_the_spec_table_in_code_order() {
2774 let table: Vec<(i32, &str)> = A2aError::ALL
2775 .into_iter()
2776 .map(|error| (error.code(), error.reason()))
2777 .collect();
2778 assert_eq!(
2779 table,
2780 [
2781 (-32001, "TASK_NOT_FOUND"),
2782 (-32002, "TASK_NOT_CANCELABLE"),
2783 (-32003, "PUSH_NOTIFICATION_NOT_SUPPORTED"),
2784 (-32004, "UNSUPPORTED_OPERATION"),
2785 (-32005, "CONTENT_TYPE_NOT_SUPPORTED"),
2786 (-32006, "INVALID_AGENT_RESPONSE"),
2787 (-32007, "EXTENDED_AGENT_CARD_NOT_CONFIGURED"),
2788 (-32008, "EXTENSION_SUPPORT_REQUIRED"),
2789 (-32009, "VERSION_NOT_SUPPORTED"),
2790 ]
2791 );
2792 }
2793
2794 #[test]
2795 fn a2a_error_from_code_round_trips_and_knows_no_other_code() {
2796 for error in A2aError::ALL {
2797 assert_eq!(A2aError::from_code(error.code()), Some(error));
2798 }
2799 for code in [-32000, -32010, -32099, -32600, -31001, 0] {
2800 assert_eq!(A2aError::from_code(code), None, "{code}");
2801 }
2802 }
2803
2804 #[test]
2805 fn a2a_murmur_errors_lie_outside_the_reserved_range() {
2806 let table: Vec<(i32, &str)> = MurmurError::ALL
2807 .into_iter()
2808 .map(|error| (error.code(), error.reason()))
2809 .collect();
2810 assert_eq!(
2811 table,
2812 [
2813 (-31001, "COMPLETION_MISADDRESSED"),
2814 (-31002, "COMPLETION_NOT_AWAITED")
2815 ]
2816 );
2817 for error in MurmurError::ALL {
2818 assert!(!(-32768..=-32000).contains(&error.code()), "{error:?}");
2819 }
2820 }
2821
2822 fn wire(response: &JsonRpcResponse) -> Value {
2823 serde_json::to_value(response).unwrap()
2824 }
2825
2826 #[test]
2827 fn a2a_error_carries_one_error_info_with_string_metadata() {
2828 let response = JsonRpcResponse::a2a_error(
2829 Value::from(7),
2830 A2aError::TaskNotCancelable,
2831 "Task cannot be canceled",
2832 &[("taskId", "tsk_1".into()), ("state", "completed".into())],
2833 );
2834 assert_eq!(
2835 wire(&response),
2836 serde_json::json!({
2837 "jsonrpc": "2.0",
2838 "id": 7,
2839 "error": {
2840 "code": -32002,
2841 "message": "Task cannot be canceled",
2842 "data": [{
2843 "@type": "type.googleapis.com/google.rpc.ErrorInfo",
2844 "reason": "TASK_NOT_CANCELABLE",
2845 "domain": "a2a-protocol.org",
2846 "metadata": {"taskId": "tsk_1", "state": "completed"},
2847 }],
2848 },
2849 })
2850 );
2851 }
2852
2853 #[test]
2854 fn a2a_murmur_error_is_in_the_murmur_domain() {
2855 let response = JsonRpcResponse::murmur_error(
2856 Value::from("a"),
2857 MurmurError::CompletionNotAwaited,
2858 "nobody waits",
2859 &[],
2860 );
2861 let error = &wire(&response)["error"];
2862 assert_eq!(error["code"], -31002);
2863 assert_eq!(error["data"][0]["reason"], "COMPLETION_NOT_AWAITED");
2864 assert_eq!(error["data"][0]["domain"], "murmur.nexus");
2865 assert_eq!(error["data"][0]["metadata"], serde_json::json!({}));
2866 }
2867
2868 #[test]
2869 fn a2a_standard_errors_carry_no_data() {
2870 let error = &wire(&JsonRpcResponse::err(
2871 Value::Null,
2872 -32601,
2873 "Method not found",
2874 ))["error"];
2875 assert!(error.get("data").is_none(), "{error}");
2876 }
2877
2878 fn refusal(body: &str) -> Value {
2879 wire(&JsonRpcRequest::from_body(body).expect_err(body))
2880 }
2881
2882 #[test]
2883 fn a2a_from_body_reads_a_request_and_defaults_params() {
2884 let req = JsonRpcRequest::from_body(
2885 r#"{"jsonrpc":"2.0","id":"x","method":"GetExtendedAgentCard","extra":1}"#,
2886 )
2887 .unwrap();
2888 assert_eq!(req.id, Value::from("x"));
2889 assert_eq!(req.method, "GetExtendedAgentCard");
2890 assert_eq!(req.params, serde_json::json!({}));
2891
2892 let req = JsonRpcRequest::from_body(
2893 r#"{"jsonrpc":"2.0","id":3,"method":"GetTask","params":[1]}"#,
2894 )
2895 .unwrap();
2896 assert_eq!(req.id, Value::from(3));
2897 assert_eq!(req.params, serde_json::json!([1]), "a method judges params");
2898 }
2899
2900 #[test]
2901 fn a2a_from_body_refuses_what_is_not_json() {
2902 let answer = refusal("{not json");
2903 assert_eq!(answer["id"], Value::Null);
2904 assert_eq!(answer["error"]["code"], -32700);
2905 assert_eq!(answer["error"]["message"], "Invalid JSON payload");
2906 }
2907
2908 #[test]
2909 fn a2a_from_body_refuses_what_is_not_a_json_rpc_request() {
2910 for (body, id) in [
2911 (
2912 r#"{"jsonrpc":"1.0","id":1,"method":"GetTask"}"#,
2913 Value::from(1),
2914 ),
2915 (r#"{"id":1,"method":"GetTask"}"#, Value::from(1)),
2916 (
2917 r#"{"jsonrpc":2.0,"id":1,"method":"GetTask"}"#,
2918 Value::from(1),
2919 ),
2920 (
2921 r#"[{"jsonrpc":"2.0","id":1,"method":"GetTask"}]"#,
2922 Value::Null,
2923 ),
2924 (r#""GetTask""#, Value::Null),
2925 (r#"{"jsonrpc":"2.0","id":"a","method":7}"#, Value::from("a")),
2926 (r#"{"jsonrpc":"2.0","id":"a"}"#, Value::from("a")),
2927 (
2928 r#"{"jsonrpc":"2.0","id":{"a":1},"method":"GetTask"}"#,
2929 Value::Null,
2930 ),
2931 (
2932 r#"{"jsonrpc":"2.0","id":null,"method":"GetTask"}"#,
2933 Value::Null,
2934 ),
2935 (
2936 r#"{"jsonrpc":"2.0","id":1.5,"method":"GetTask"}"#,
2937 Value::Null,
2938 ),
2939 (r#"{"jsonrpc":"2.0","method":"GetTask"}"#, Value::Null),
2940 ] {
2941 let answer = refusal(body);
2942 assert_eq!(answer["error"]["code"], -32600, "{body}");
2943 assert_eq!(
2944 answer["error"]["message"], "Request payload validation error",
2945 "{body}"
2946 );
2947 assert_eq!(answer["id"], id, "{body}");
2948 }
2949 }
2950
2951 fn versions(values: &[&str]) -> Vec<String> {
2952 values.iter().map(|value| value.to_string()).collect()
2953 }
2954
2955 #[test]
2956 fn a2a_version_accepts_1_0_with_or_without_a_patch() {
2957 for value in ["1.0", " 1.0 ", "1.0.0", "1.0.1", "1.0.17"] {
2958 assert!(accepts_a2a_version(&versions(&[value])), "{value:?}");
2959 }
2960 }
2961
2962 #[test]
2963 fn a2a_version_refuses_every_other_value_and_more_than_one_line() {
2964 for values in [
2965 &[][..],
2966 &[""],
2967 &["0.3"],
2968 &["0.3.0"],
2969 &["2.0"],
2970 &["1"],
2971 &["1.1"],
2972 &["1.0x"],
2973 &["1.0."],
2974 &["1.0.a"],
2975 &["1.00"],
2976 &["v1.0"],
2977 &["1.0", "1.0"],
2978 ] {
2979 assert!(!accepts_a2a_version(&versions(values)), "{values:?}");
2980 }
2981 }
2982
2983 #[test]
2984 fn an_extension_is_activated_by_an_exact_entry_on_any_line() {
2985 const URI: &str =
2986 "https://docs.murmur.nexus/reference/streaming-protocol/#murmur-stream-v1";
2987 let other = "https://example.com/x/v1";
2988 for values in [
2989 vec![URI.to_string()],
2990 vec![format!(" {URI} ")],
2991 vec![format!("{other}, {URI}")],
2992 vec![format!("{other},,{URI},")],
2993 vec![other.to_string(), URI.to_string()],
2994 ] {
2995 assert!(activates_extension(&values, URI), "{values:?}");
2996 }
2997 for values in [
2998 vec![],
2999 vec![String::new()],
3000 vec![other.to_string()],
3001 vec![format!("{URI}/")],
3002 vec![URI.to_uppercase()],
3003 vec![format!("{URI} {other}")],
3004 ] {
3005 assert!(!activates_extension(&values, URI), "{values:?}");
3006 }
3007 }
3008
3009 #[test]
3010 fn a2a_version_refusal_names_what_was_sent() {
3011 let answer = wire(&version_not_supported(Value::from(4), &versions(&[])));
3012 assert_eq!(answer["id"], 4);
3013 assert_eq!(answer["error"]["code"], -32009);
3014 let info = &answer["error"]["data"][0];
3015 assert_eq!(info["reason"], "VERSION_NOT_SUPPORTED");
3016 assert_eq!(info["domain"], "a2a-protocol.org");
3017 assert_eq!(
3018 info["metadata"],
3019 serde_json::json!({"requestedVersion": "", "supportedVersions": "1.0"})
3020 );
3021
3022 let answer = wire(&version_not_supported(Value::Null, &versions(&[" 0.3 "])));
3023 assert_eq!(
3024 answer["error"]["data"][0]["metadata"]["requestedVersion"],
3025 "0.3"
3026 );
3027 let answer = wire(&version_not_supported(
3028 Value::Null,
3029 &versions(&["1.0", "1.0"]),
3030 ));
3031 assert_eq!(
3032 answer["error"]["data"][0]["metadata"]["requestedVersion"],
3033 "1.0, 1.0"
3034 );
3035 }
3036
3037 #[test]
3040 fn a_part_serializes_its_one_content_field_and_reads_back() {
3041 for (part, wire) in [
3042 (
3043 Part::text("hi"),
3044 serde_json::json!({"text": "hi", "mediaType": "text/plain"}),
3045 ),
3046 (
3047 Part::data(serde_json::json!({"a": [1]})),
3048 serde_json::json!({"data": {"a": [1]}, "mediaType": "application/json"}),
3049 ),
3050 (
3051 Part::data(Value::Null),
3052 serde_json::json!({"data": null, "mediaType": "application/json"}),
3053 ),
3054 ] {
3055 assert_eq!(serde_json::to_value(&part).unwrap(), wire);
3056 assert_eq!(serde_json::from_value::<Part>(wire).unwrap(), part);
3057 }
3058 for refused in [
3059 serde_json::json!({}),
3060 serde_json::json!({"text": "a", "data": 1}),
3061 serde_json::json!({"kind": "file", "file": {"uri": "x"}}),
3062 serde_json::json!({"text": 7}),
3063 serde_json::json!("text"),
3064 ] {
3065 assert!(
3066 serde_json::from_value::<Part>(refused.clone()).is_err(),
3067 "{refused}"
3068 );
3069 }
3070 }
3071
3072 fn params(message: Value) -> Value {
3073 serde_json::json!({ "message": message })
3074 }
3075
3076 fn user(parts: Value) -> Value {
3077 serde_json::json!({"messageId": "msg_1", "role": "ROLE_USER", "parts": parts})
3078 }
3079
3080 fn invalid_params(params: &Value) -> String {
3081 match read_send_params(params) {
3082 Err(MessageRefusal::InvalidParams(message)) => message,
3083 other => panic!("expected -32602 for {params}, got {other:?}"),
3084 }
3085 }
3086
3087 #[test]
3088 fn read_send_params_reads_a_v1_message_in_full() {
3089 let message = read_send_params(¶ms(serde_json::json!({
3090 "messageId": "msg_1",
3091 "contextId": "ctx_1",
3092 "taskId": "tsk_1",
3093 "role": "ROLE_USER",
3094 "referenceTaskIds": ["tsk_a", "tsk_b", "tsk_a"],
3095 "parts": [{"text": "a"}, {"data": null, "filename": "f.json"}],
3096 "metadata": {"ignored": true},
3097 "extensions": ["https://example.com/x"],
3098 "unknown": 1,
3099 })))
3100 .unwrap();
3101 assert_eq!(message.message_id, "msg_1");
3102 assert_eq!(message.context_id.as_deref(), Some("ctx_1"));
3103 assert_eq!(message.task_id.as_deref(), Some("tsk_1"));
3104 assert_eq!(message.reference_task_ids, ["tsk_a", "tsk_b"]);
3105 assert_eq!(message.part_kinds(), ["text", "data"]);
3106 assert_eq!(message.parts[1].filename.as_deref(), Some("f.json"));
3107
3108 let message = read_send_params(¶ms(serde_json::json!({
3109 "messageId": "msg_1", "role": "ROLE_USER", "taskId": "", "parts": [{"text": "a"}],
3110 })))
3111 .unwrap();
3112 assert_eq!(message.task_id, None, "an empty taskId names no task");
3113 }
3114
3115 #[test]
3118 fn read_send_params_refuses_structure_in_order() {
3119 let text = serde_json::json!([{"text": "a"}]);
3120 for (params, expected) in [
3121 (serde_json::json!({}), "params.message"),
3122 (user(text.clone()), "params.message"),
3123 (serde_json::json!({"message": "hi"}), "params.message"),
3124 (
3125 params(serde_json::json!({"role": "ROLE_USER", "parts": text})),
3126 "messageId",
3127 ),
3128 (
3129 params(serde_json::json!({"messageId": "", "role": "ROLE_USER", "parts": text})),
3130 "messageId",
3131 ),
3132 (
3133 params(serde_json::json!({"messageId": "m", "role": "user", "parts": []})),
3134 "ROLE_USER",
3135 ),
3136 (
3137 params(serde_json::json!({"messageId": "m", "role": "ROLE_AGENT", "parts": text})),
3138 "ROLE_USER",
3139 ),
3140 (
3141 params(serde_json::json!({"messageId": "m", "role": 1, "parts": text})),
3142 "ROLE_USER",
3143 ),
3144 (
3145 params(serde_json::json!({"messageId": "m", "parts": text})),
3146 "ROLE_USER",
3147 ),
3148 (params(user(serde_json::json!([]))), "non-empty array"),
3149 (
3150 params(user(serde_json::json!({"text": "a"}))),
3151 "non-empty array",
3152 ),
3153 (
3154 params(user(serde_json::json!([{"text": "a"}, {}]))),
3155 "message.parts[1] sets none",
3156 ),
3157 (
3158 params(user(serde_json::json!([{"text": "a", "data": 1}]))),
3159 "message.parts[0] sets text and data",
3160 ),
3161 (
3162 params(user(
3163 serde_json::json!([{"kind": "file", "file": {"uri": "x"}}]),
3164 )),
3165 "message.parts[0] sets none",
3166 ),
3167 (
3168 params(user(serde_json::json!([{"url": 7}, {}]))),
3169 "message.parts[1] sets none",
3170 ),
3171 (
3172 params(user(serde_json::json!([{"text": 7}]))),
3173 "message.parts[0].text is not a string",
3174 ),
3175 (
3176 params(user(serde_json::json!([{"data": 1, "mediaType": 2}]))),
3177 "message.parts[0].mediaType",
3178 ),
3179 (
3180 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3181 "parts": [{"url": "u"}], "taskId": 7})),
3182 "message.taskId",
3183 ),
3184 (
3185 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3186 "parts": text, "contextId": ["c"]})),
3187 "message.contextId",
3188 ),
3189 (
3190 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3191 "parts": [{"raw": "aGk="}], "referenceTaskIds": [""]})),
3192 "referenceTaskIds",
3193 ),
3194 (
3195 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3196 "parts": text, "referenceTaskIds": [7]})),
3197 "referenceTaskIds",
3198 ),
3199 (
3200 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3201 "parts": text, "referenceTaskIds": "tsk_a"})),
3202 "referenceTaskIds",
3203 ),
3204 ] {
3205 let message = invalid_params(¶ms);
3206 assert!(message.contains(expected), "{params}: {message}");
3207 }
3208 }
3209
3210 #[test]
3211 fn read_send_params_caps_distinct_references() {
3212 let ids = |n: usize| -> Vec<String> { (0..n).map(|i| format!("tsk_{i}")).collect() };
3213 let with = |ids: Vec<String>| {
3214 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3215 "parts": [{"text": "a"}], "referenceTaskIds": ids}))
3216 };
3217 let mut sixteen_twice = ids(MAX_REFERENCE_TASK_IDS);
3218 sixteen_twice.extend(ids(MAX_REFERENCE_TASK_IDS));
3219 assert_eq!(
3220 read_send_params(&with(sixteen_twice))
3221 .unwrap()
3222 .reference_task_ids,
3223 ids(MAX_REFERENCE_TASK_IDS)
3224 );
3225 let message = invalid_params(&with(ids(MAX_REFERENCE_TASK_IDS + 1)));
3226 assert!(message.contains("names 17 tasks"), "{message}");
3227 }
3228
3229 #[test]
3232 fn read_send_params_refuses_a_push_notification_config() {
3233 let message = user(serde_json::json!([{"text": "hi"}]));
3234 let asking = serde_json::json!({"message": message,
3235 "configuration": {"taskPushNotificationConfig": {"url": "https://example.com/hook"}}});
3236 assert_eq!(
3237 read_send_params(&asking),
3238 Err(MessageRefusal::PushNotificationNotSupported)
3239 );
3240 let answer = serde_json::to_value(
3241 MessageRefusal::PushNotificationNotSupported.into_response(Value::from(1)),
3242 )
3243 .unwrap();
3244 assert_eq!(answer["error"]["code"], -32003);
3245 assert_eq!(
3246 answer["error"]["message"],
3247 "push notifications are not supported by this agent"
3248 );
3249 assert_eq!(
3250 answer["error"]["data"][0],
3251 serde_json::json!({"@type": "type.googleapis.com/google.rpc.ErrorInfo",
3252 "reason": "PUSH_NOTIFICATION_NOT_SUPPORTED", "domain": "a2a-protocol.org",
3253 "metadata": {}})
3254 );
3255 for configuration in [
3256 serde_json::json!({"taskPushNotificationConfig": null}),
3257 serde_json::json!({"acceptedOutputModes": ["text/plain"], "historyLength": 2}),
3258 serde_json::json!("ignored"),
3259 ] {
3260 let params = serde_json::json!({"message": message, "configuration": configuration});
3261 assert!(read_send_params(¶ms).is_ok(), "{params}");
3262 }
3263 }
3264
3265 fn with_metadata(metadata: Value, task_id: Option<&str>) -> Value {
3267 let mut message = user(serde_json::json!([{"text": "a"}]));
3268 message["metadata"] = metadata;
3269 if let Some(task_id) = task_id {
3270 message["taskId"] = Value::from(task_id);
3271 }
3272 params(message)
3273 }
3274
3275 fn door(door: Value) -> Value {
3277 serde_json::json!({ DOOR_EXTENSION_URI: door })
3278 }
3279
3280 #[test]
3281 fn read_send_params_reads_forget_session_true() {
3282 let message = read_send_params(&with_metadata(
3283 door(serde_json::json!({"forgetSession": true})),
3284 None,
3285 ))
3286 .unwrap();
3287 assert!(message.forget_session);
3288 assert!(message.door_extension);
3289 }
3290
3291 #[test]
3292 fn read_send_params_reads_every_other_metadata_as_no_forget() {
3293 for (metadata, door_extension) in [
3294 (door(serde_json::json!({"forgetSession": false})), true),
3295 (door(serde_json::json!({})), true),
3296 (
3297 door(serde_json::json!({"other": 1, "forgetSession": false})),
3298 true,
3299 ),
3300 (serde_json::json!({}), false),
3301 (
3302 serde_json::json!({"https://example.com/x": {"forgetSession": true}}),
3303 false,
3304 ),
3305 (serde_json::json!({"forgetSession": true}), false),
3306 (serde_json::json!("not an object"), false),
3307 (
3308 serde_json::json!([door(serde_json::json!({"forgetSession": true}))]),
3309 false,
3310 ),
3311 (Value::Null, false),
3312 ] {
3313 let message = read_send_params(&with_metadata(metadata.clone(), None)).unwrap();
3314 assert!(!message.forget_session, "{metadata}");
3315 assert_eq!(message.door_extension, door_extension, "{metadata}");
3316 }
3317 let message = read_send_params(¶ms(user(serde_json::json!([{"text": "a"}])))).unwrap();
3318 assert!(
3319 !message.forget_session && !message.door_extension,
3320 "no metadata at all"
3321 );
3322 }
3323
3324 #[test]
3325 fn read_send_params_ignores_other_keys_beside_forget_session() {
3326 let message = read_send_params(&with_metadata(
3327 door(serde_json::json!({"forgetSession": true, "later": {"x": 1}})),
3328 None,
3329 ))
3330 .unwrap();
3331 assert!(message.forget_session);
3332 }
3333
3334 #[test]
3335 fn read_send_params_refuses_a_door_extension_value_that_is_not_an_object() {
3336 for value in [
3337 serde_json::json!(true),
3338 serde_json::json!("forget"),
3339 serde_json::json!([]),
3340 ] {
3341 assert_eq!(
3342 invalid_params(&with_metadata(door(value), None)),
3343 format!("message.metadata[\"{DOOR_EXTENSION_URI}\"] must be an object")
3344 );
3345 }
3346 }
3347
3348 #[test]
3349 fn read_send_params_refuses_a_forget_session_that_is_not_a_boolean() {
3350 for value in [serde_json::json!("true"), serde_json::json!(1), Value::Null] {
3351 assert_eq!(
3352 invalid_params(&with_metadata(
3353 door(serde_json::json!({"forgetSession": value})),
3354 None
3355 )),
3356 format!(
3357 "message.metadata[\"{DOOR_EXTENSION_URI}\"].forgetSession must be a boolean"
3358 )
3359 );
3360 }
3361 }
3362
3363 #[test]
3364 fn read_send_params_refuses_a_forget_on_a_message_that_continues_a_task() {
3365 assert_eq!(
3366 invalid_params(&with_metadata(
3367 door(serde_json::json!({"forgetSession": true})),
3368 Some("tsk_1")
3369 )),
3370 "forgetSession starts a new conversation, so it goes on a message that starts a task, \
3371 not on one that continues task tsk_1"
3372 );
3373 let message = read_send_params(&with_metadata(
3375 door(serde_json::json!({"forgetSession": false})),
3376 Some("tsk_1"),
3377 ))
3378 .unwrap();
3379 assert_eq!(message.task_id.as_deref(), Some("tsk_1"));
3380 assert!(!message.forget_session);
3381 }
3382
3383 #[test]
3386 fn read_send_params_checks_the_forget_field_in_order() {
3387 let mut message = with_metadata(door(Value::from(1)), None);
3389 message["message"]["referenceTaskIds"] = Value::from("tsk_a");
3390 assert!(invalid_params(&message).contains("referenceTaskIds"));
3391 let message = with_metadata(door(Value::from(1)), Some("tsk_1"));
3393 assert!(invalid_params(&message).ends_with("must be an object"));
3394 let message = with_metadata(
3396 door(serde_json::json!({"forgetSession": "yes"})),
3397 Some("tsk_1"),
3398 );
3399 assert!(invalid_params(&message).ends_with("must be a boolean"));
3400 let mut message = with_metadata(door(serde_json::json!({"forgetSession": 1})), None);
3402 message["message"]["parts"] = serde_json::json!([{"raw": "aGk="}]);
3403 assert!(invalid_params(&message).ends_with("must be a boolean"));
3404 let mut message = with_metadata(door(serde_json::json!({"forgetSession": true})), None);
3406 message["message"]["parts"] = serde_json::json!([{"raw": "aGk="}]);
3407 assert!(matches!(
3408 read_send_params(&message),
3409 Err(MessageRefusal::ContentTypeNotSupported { .. })
3410 ));
3411 let refusal = read_send_params(&with_metadata(door(Value::from(1)), None)).unwrap_err();
3413 let answer = wire(&refusal.into_response(Value::from(3)));
3414 assert_eq!(answer["error"]["code"], -32602);
3415 assert_eq!(
3416 answer["error"]["message"],
3417 format!("Invalid params: message.metadata[\"{DOOR_EXTENSION_URI}\"] must be an object")
3418 );
3419 }
3420
3421 fn configured(configuration: Value) -> Value {
3423 serde_json::json!({
3424 "message": user(serde_json::json!([{"text": "a"}])),
3425 "configuration": configuration,
3426 })
3427 }
3428
3429 #[test]
3430 fn read_send_params_reads_return_immediately_true_only_for_true() {
3431 let immediately = read_send_params(&configured(serde_json::json!({
3432 "returnImmediately": true,
3433 })))
3434 .unwrap();
3435 assert!(immediately.return_immediately);
3436 for params in [
3437 params(user(serde_json::json!([{"text": "a"}]))),
3438 configured(Value::Null),
3439 configured(serde_json::json!({})),
3440 configured(serde_json::json!({"returnImmediately": null})),
3441 configured(serde_json::json!({"returnImmediately": false})),
3442 configured(serde_json::json!({"historyLength": 2})),
3443 configured(serde_json::json!("ignored")),
3444 ] {
3445 assert!(
3446 !read_send_params(¶ms).unwrap().return_immediately,
3447 "{params}"
3448 );
3449 }
3450 }
3451
3452 #[test]
3453 fn read_send_params_refuses_a_return_immediately_that_is_not_a_boolean() {
3454 for value in [
3455 Value::from("yes"),
3456 Value::from(1),
3457 serde_json::json!({}),
3458 serde_json::json!([true]),
3459 ] {
3460 let params = configured(serde_json::json!({ "returnImmediately": value }));
3461 assert_eq!(
3462 invalid_params(¶ms),
3463 "configuration.returnImmediately must be a boolean",
3464 "{params}"
3465 );
3466 let answer = wire(
3467 &read_send_params(¶ms)
3468 .unwrap_err()
3469 .into_response(1.into()),
3470 );
3471 assert_eq!(answer["error"]["code"], -32602);
3472 assert_eq!(
3473 answer["error"]["message"],
3474 "Invalid params: configuration.returnImmediately must be a boolean"
3475 );
3476 }
3477 }
3478
3479 #[test]
3482 fn read_send_params_checks_return_immediately_in_order() {
3483 let mut message = with_metadata(door(serde_json::json!({"forgetSession": 1})), None);
3484 message["configuration"] = serde_json::json!({"returnImmediately": "yes"});
3485 assert!(invalid_params(&message).contains("forgetSession"));
3486
3487 let mut message = configured(serde_json::json!({
3488 "returnImmediately": "yes",
3489 "taskPushNotificationConfig": {"url": "https://example.com/hook"},
3490 }));
3491 message["message"]["parts"] = serde_json::json!([{"raw": "aGk="}]);
3492 assert!(invalid_params(&message).contains("returnImmediately"));
3493
3494 message["configuration"]["returnImmediately"] = Value::Bool(true);
3495 assert!(matches!(
3496 read_send_params(&message),
3497 Err(MessageRefusal::ContentTypeNotSupported { .. })
3498 ));
3499 }
3500
3501 #[test]
3503 fn read_send_params_refuses_the_first_file_part_with_content_type_not_supported() {
3504 assert_eq!(
3505 read_send_params(¶ms(user(serde_json::json!([
3506 {"text": "a"},
3507 {"url": "https://example.com/x.pdf", "mediaType": "application/pdf"},
3508 {"raw": "aGk="},
3509 ])))),
3510 Err(MessageRefusal::ContentTypeNotSupported {
3511 part_index: 1,
3512 kind: "url",
3513 media_type: Some("application/pdf".to_string()),
3514 })
3515 );
3516 let refusal =
3517 read_send_params(¶ms(user(serde_json::json!([{"raw": "aGk="}])))).unwrap_err();
3518 let answer = wire(&refusal.into_response(Value::from(3)));
3519 assert_eq!(answer["error"]["code"], -32005);
3520 let info = &answer["error"]["data"][0];
3521 assert_eq!(info["reason"], "CONTENT_TYPE_NOT_SUPPORTED");
3522 assert_eq!(info["metadata"], serde_json::json!({"partIndex": "0"}));
3523 invalid_params(¶ms(user(
3525 serde_json::json!([{"raw": "aGk="}, {"text": 1}]),
3526 )));
3527 }
3528
3529 fn rendered(parts: Value) -> String {
3530 read_send_params(¶ms(user(parts))).unwrap().agent_text()
3531 }
3532
3533 #[test]
3534 fn agent_text_of_text_parts_is_the_text_joined_by_newlines() {
3535 assert_eq!(
3536 rendered(
3537 serde_json::json!([{"text": "one"}, {"text": "two", "mediaType": "text/markdown"}])
3538 ),
3539 "one\ntwo"
3540 );
3541 }
3542
3543 #[test]
3544 fn agent_text_fences_a_data_part_beyond_its_longest_backtick_run() {
3545 assert_eq!(
3546 rendered(serde_json::json!([{"text": "summarise"}, {"data": {"rows": [1, 2]}}])),
3547 "summarise\n```data\n{\n \"rows\": [\n 1,\n 2\n ]\n}\n```"
3548 );
3549 let three =
3550 rendered(serde_json::json!([{"data": "a ``` b", "mediaType": "application/json"}]));
3551 assert_eq!(
3552 three,
3553 "````data media-type=application/json\n\"a ``` b\"\n````"
3554 );
3555 let five = rendered(serde_json::json!([{"data": {"x": "`````"}}]));
3556 assert!(five.starts_with("``````data\n"), "{five}");
3557 assert!(five.ends_with("\n``````"), "{five}");
3558 assert_eq!(
3559 rendered(serde_json::json!([{"data": null}])),
3560 "```data\nnull\n```"
3561 );
3562 }
3563
3564 #[test]
3565 fn agent_text_sanitises_the_fence_label() {
3566 assert_eq!(
3567 rendered(serde_json::json!([{"data": 1, "mediaType": "a b=`c",
3568 "filename": "my\tfile.json"}])),
3569 "```data media-type=a_b__c filename=my_file.json\n1\n```"
3570 );
3571 }
3572
3573 #[test]
3574 fn a_reference_block_names_each_task_as_it_stands() {
3575 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
3576 for id in ["tsk_a", "tsk_b", "tsk_c", "tsk_e"] {
3577 r.enqueue(id, "ctx_001");
3578 }
3579 r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::Bg);
3580 r.finish_task(TaskState::Completed);
3581 r.record_ending("tsk_a", "done", Some("four ```"), false, &[]);
3582 r.start_task("tsk_b".to_string(), "ctx_001".to_string(), TaskLane::Bg);
3583 r.finish_task(TaskState::Failed);
3584 r.record_ending("tsk_b", "the driver\nfailed", None, false, &[]);
3585 r.start_task("tsk_c".to_string(), "ctx_001".to_string(), TaskLane::Bg);
3586 r.record_owner("tsk_e", "member:reviewer");
3587 let ids: Vec<String> = ["tsk_a", "tsk_b", "tsk_c", "tsk_d", "tsk_e"]
3588 .map(str::to_string)
3589 .to_vec();
3590
3591 assert_eq!(
3592 r.reference_block(&ids, |_| true).unwrap(),
3593 "Referenced tasks:\n\
3594 - tsk_a: completed\n````response\nfour ```\n````\n\
3595 - tsk_b: failed: the driver failed\n\
3596 - tsk_c: working\n\
3597 - tsk_d: not a task this capsule holds\n\
3598 - tsk_e: submitted"
3599 );
3600 let member = |task_id: &str| r.owner(task_id) == Some("member:planner");
3601 let block = r.reference_block(&ids[4..], member).unwrap();
3602 assert_eq!(
3603 block,
3604 "Referenced tasks:\n- tsk_e: not a task this capsule holds"
3605 );
3606 assert_eq!(r.reference_block(&[], |_| true), None);
3607 }
3608
3609 #[test]
3610 fn user_message_is_a_v1_message_from_the_user() {
3611 assert_eq!(
3612 user_message("msg_1", Some("ctx_1"), "hello"),
3613 serde_json::json!({
3614 "messageId": "msg_1",
3615 "contextId": "ctx_1",
3616 "role": "ROLE_USER",
3617 "parts": [{"text": "hello", "mediaType": "text/plain"}],
3618 })
3619 );
3620 assert!(user_message("msg_1", None, "hello")
3621 .get("contextId")
3622 .is_none());
3623 assert!(read_send_params(¶ms(user_message("msg_1", None, "hello"))).is_ok());
3624 }
3625
3626 #[test]
3627 fn send_message_params_answer_at_acceptance() {
3628 let message = user_message("msg_1", None, "hello");
3629 let params = send_message_params(message.clone());
3630 assert_eq!(
3631 params,
3632 serde_json::json!({
3633 "message": message,
3634 "configuration": {"returnImmediately": true},
3635 })
3636 );
3637 assert!(read_send_params(¶ms).unwrap().return_immediately);
3638 }
3639
3640 #[test]
3641 fn send_message_result_is_exactly_one_member_of_the_oneof() {
3642 let task = serde_json::json!({"id": "tsk_1", "contextId": "c",
3643 "status": {"state": "TASK_STATE_SUBMITTED"}});
3644 assert!(matches!(
3645 SendMessageResult::from_result(&serde_json::json!({ "task": task })),
3646 Ok(SendMessageResult::Task(task)) if task.id == "tsk_1"
3647 ));
3648 assert!(matches!(
3649 SendMessageResult::from_result(&serde_json::json!({"message": {"messageId": "m"}})),
3650 Ok(SendMessageResult::Message(_))
3651 ));
3652 for refused in [
3653 serde_json::json!({}),
3654 serde_json::json!({"task": task, "message": {}}),
3655 task.clone(),
3656 serde_json::json!({"task": {"id": "tsk_1", "contextId": "c",
3657 "status": {"state": "submitted"}}}),
3658 ] {
3659 assert!(
3660 SendMessageResult::from_result(&refused).is_err(),
3661 "{refused}"
3662 );
3663 }
3664 }
3665}