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")]
593 pub metadata: Option<serde_json::Value>,
594}
595
596impl A2aTask {
597 pub(crate) fn artifact(&self, artifact_id: &str) -> Option<&A2aArtifact> {
599 self.artifacts
600 .as_deref()?
601 .iter()
602 .find(|artifact| artifact.artifact_id == artifact_id)
603 }
604
605 pub(crate) fn status_text(&self) -> Option<&str> {
607 self.status.message.as_ref()?.text()
608 }
609}
610
611#[derive(Debug, Clone, Serialize, Deserialize)]
614#[serde(rename_all = "camelCase")]
615pub(crate) struct A2aArtifact {
616 pub artifact_id: String,
617 #[serde(default)]
618 pub name: String,
619 pub parts: Vec<Part>,
620}
621
622impl A2aArtifact {
623 pub(crate) fn named(name: &str, parts: Vec<Part>) -> Self {
625 Self {
626 artifact_id: name.to_string(),
627 name: name.to_string(),
628 parts,
629 }
630 }
631
632 pub(crate) fn first_text(&self) -> Option<&str> {
634 self.parts.first()?.as_text()
635 }
636}
637
638#[derive(Debug, Clone, Serialize, Deserialize)]
639pub(crate) struct TaskStatus {
640 pub state: TaskState,
641 #[serde(default, skip_serializing_if = "Option::is_none")]
643 pub message: Option<StatusMessage>,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub timestamp: Option<String>,
648}
649
650impl TaskStatus {
651 pub(crate) fn of(state: TaskState) -> Self {
653 Self::at(state, None, status_now())
654 }
655
656 pub(crate) fn at(
658 state: TaskState,
659 message: Option<StatusMessage>,
660 when: DateTime<Utc>,
661 ) -> Self {
662 Self {
663 state,
664 message,
665 timestamp: Some(status_timestamp(when)),
666 }
667 }
668}
669
670pub(crate) fn status_now() -> DateTime<Utc> {
673 let now = Utc::now();
674 DateTime::from_timestamp_millis(now.timestamp_millis()).unwrap_or(now)
675}
676
677#[cfg(test)]
680pub(crate) fn assert_status_timestamp(status: &Value) -> DateTime<Utc> {
681 let timestamp = status["timestamp"]
682 .as_str()
683 .unwrap_or_else(|| panic!("no timestamp: {status}"));
684 let parsed = DateTime::parse_from_rfc3339(timestamp)
685 .unwrap_or_else(|error| panic!("{timestamp} is not RFC 3339: {error}"))
686 .with_timezone(&Utc);
687 assert_eq!(status_timestamp(parsed), timestamp, "{status}");
688 parsed
689}
690
691#[cfg(test)]
694pub(crate) fn assert_bare_status(status: &Value, state: &str) {
695 assert_status_timestamp(status);
696 assert_eq!(
697 *status,
698 serde_json::json!({"state": state, "timestamp": status["timestamp"]})
699 );
700}
701
702pub(crate) fn status_timestamp(at: DateTime<Utc>) -> String {
705 at.to_rfc3339_opts(SecondsFormat::Millis, true)
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
710#[serde(rename_all = "camelCase")]
711pub(crate) struct StatusMessage {
712 pub message_id: String,
713 pub context_id: String,
714 pub task_id: String,
715 pub role: Role,
716 pub parts: Vec<Part>,
717}
718
719impl StatusMessage {
720 pub(crate) fn agent(task_id: &str, context_id: &str, text: &str) -> Self {
722 Self {
723 message_id: format!("msg_{task_id}_status"),
724 context_id: context_id.to_string(),
725 task_id: task_id.to_string(),
726 role: Role::Agent,
727 parts: vec![Part::text(text)],
728 }
729 }
730
731 pub(crate) fn text(&self) -> Option<&str> {
733 self.parts.first()?.as_text()
734 }
735}
736
737#[derive(Debug, Clone, PartialEq, Eq)]
739struct TaskEnding {
740 message: String,
742 response: Option<String>,
744 no_answer: bool,
746 no_answer_below: Vec<(String, String)>,
748 running_commands: Vec<RunningCommand>,
750}
751
752#[derive(Debug, Clone, PartialEq, Eq)]
755pub(crate) struct RunningCommand {
756 pub work_id: String,
757 pub binary: String,
758 pub command: String,
760 pub running_ms: u64,
762}
763
764const RUNNING_COMMAND_TEXT_CHARS: usize = 200;
766
767impl TaskEnding {
768 fn metadata(&self) -> Option<serde_json::Value> {
770 let mut murmur = serde_json::Map::new();
771 if self.no_answer {
772 murmur.insert("noAnswer".to_string(), serde_json::Value::Bool(true));
773 }
774 if !self.no_answer_below.is_empty() {
775 let below = self
776 .no_answer_below
777 .iter()
778 .map(|(member, status)| serde_json::json!({"member": member, "status": status}))
779 .collect();
780 murmur.insert("noAnswerBelow".to_string(), serde_json::Value::Array(below));
781 }
782 if !self.running_commands.is_empty() {
783 let running = self
784 .running_commands
785 .iter()
786 .map(|running| {
787 let mut command: String = running
788 .command
789 .chars()
790 .take(RUNNING_COMMAND_TEXT_CHARS)
791 .collect();
792 if running
793 .command
794 .chars()
795 .nth(RUNNING_COMMAND_TEXT_CHARS)
796 .is_some()
797 {
798 command.push('…');
799 }
800 serde_json::json!({
801 "workId": running.work_id,
802 "binary": running.binary,
803 "command": command,
804 "runningMs": running.running_ms,
805 })
806 })
807 .collect();
808 murmur.insert(
809 "runningCommands".to_string(),
810 serde_json::Value::Array(running),
811 );
812 }
813 (!murmur.is_empty()).then(|| serde_json::json!({ "murmur": murmur }))
814 }
815}
816
817pub(crate) const RESPONSE_ARTIFACT: &str = "response";
820
821pub(crate) const PROMPT_ARTIFACT: &str = "prompt";
823
824#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
835pub(crate) enum TaskState {
836 #[serde(rename = "TASK_STATE_SUBMITTED")]
837 Submitted,
838 #[serde(rename = "TASK_STATE_WORKING")]
839 Working,
840 #[serde(rename = "TASK_STATE_INPUT_REQUIRED")]
841 InputRequired,
842 #[serde(rename = "TASK_STATE_COMPLETED")]
843 Completed,
844 #[serde(rename = "TASK_STATE_FAILED")]
845 Failed,
846 #[serde(rename = "TASK_STATE_REJECTED")]
847 Rejected,
848 #[serde(rename = "TASK_STATE_CANCELED")]
851 Canceled,
852}
853
854impl TaskState {
855 pub(crate) const ALL: [TaskState; 7] = [
857 TaskState::Submitted,
858 TaskState::Working,
859 TaskState::Completed,
860 TaskState::Failed,
861 TaskState::Canceled,
862 TaskState::InputRequired,
863 TaskState::Rejected,
864 ];
865
866 pub(crate) fn wire_name(&self) -> &'static str {
868 match self {
869 Self::Submitted => "TASK_STATE_SUBMITTED",
870 Self::Working => "TASK_STATE_WORKING",
871 Self::InputRequired => "TASK_STATE_INPUT_REQUIRED",
872 Self::Completed => "TASK_STATE_COMPLETED",
873 Self::Failed => "TASK_STATE_FAILED",
874 Self::Rejected => "TASK_STATE_REJECTED",
875 Self::Canceled => "TASK_STATE_CANCELED",
876 }
877 }
878
879 pub(crate) fn from_wire_name(wire: &str) -> Option<TaskState> {
882 Self::ALL
883 .into_iter()
884 .find(|state| state.wire_name() == wire)
885 }
886
887 pub(crate) fn as_str(&self) -> &'static str {
892 match self {
893 Self::Submitted => "submitted",
894 Self::Working => "working",
895 Self::InputRequired => "input-required",
896 Self::Completed => "completed",
897 Self::Failed => "failed",
898 Self::Rejected => "rejected",
899 Self::Canceled => "canceled",
900 }
901 }
902
903 pub(crate) fn is_terminal(&self) -> bool {
908 match self {
909 Self::Submitted | Self::Working | Self::InputRequired => false,
910 Self::Completed | Self::Failed | Self::Rejected | Self::Canceled => true,
911 }
912 }
913}
914
915pub fn murmur_state_name(wire: &str) -> Option<&'static str> {
922 Some(match wire {
923 "TASK_STATE_SUBMITTED" => "submitted",
924 "TASK_STATE_WORKING" => "working",
925 "TASK_STATE_COMPLETED" => "completed",
926 "TASK_STATE_FAILED" => "failed",
927 "TASK_STATE_CANCELED" => "canceled",
928 "TASK_STATE_INPUT_REQUIRED" => "input-required",
929 "TASK_STATE_REJECTED" => "rejected",
930 "TASK_STATE_AUTH_REQUIRED" => "auth-required",
931 _ => return None,
932 })
933}
934
935pub(crate) const MAX_REFERENCE_TASK_IDS: usize = 16;
939
940#[derive(Debug, Clone, PartialEq)]
943pub(crate) struct IncomingMessage {
944 pub message_id: String,
945 pub context_id: Option<String>,
946 pub task_id: Option<String>,
948 pub reference_task_ids: Vec<String>,
950 pub parts: Vec<Part>,
952 pub door_extension: bool,
956 pub forget_session: bool,
960 pub return_immediately: bool,
963}
964
965#[derive(Debug, Clone, PartialEq, Eq)]
967pub(crate) enum MessageRefusal {
968 InvalidParams(String),
970 ContentTypeNotSupported {
973 part_index: usize,
974 kind: &'static str,
975 media_type: Option<String>,
976 },
977 PushNotificationNotSupported,
980}
981
982impl MessageRefusal {
983 pub(crate) fn into_response(self, id: Value) -> JsonRpcResponse {
985 match self {
986 Self::InvalidParams(message) => {
987 JsonRpcResponse::err(id, INVALID_PARAMS, &format!("Invalid params: {message}"))
988 }
989 Self::ContentTypeNotSupported {
990 part_index,
991 kind,
992 media_type,
993 } => {
994 let mut metadata = vec![("partIndex", part_index.to_string())];
995 if let Some(media_type) = &media_type {
996 metadata.push(("mediaType", media_type.clone()));
997 }
998 JsonRpcResponse::a2a_error(
999 id,
1000 A2aError::ContentTypeNotSupported,
1001 &format!(
1002 "message.parts[{part_index}] is a {kind} part; this agent reads text and \
1003 data parts only"
1004 ),
1005 &metadata,
1006 )
1007 }
1008 Self::PushNotificationNotSupported => push_notification_not_supported(id),
1009 }
1010 }
1011}
1012
1013pub(crate) fn push_notification_not_supported(id: Value) -> JsonRpcResponse {
1016 JsonRpcResponse::a2a_error(
1017 id,
1018 A2aError::PushNotificationNotSupported,
1019 "push notifications are not supported by this agent",
1020 &[],
1021 )
1022}
1023
1024pub(crate) fn read_send_params(params: &Value) -> Result<IncomingMessage, MessageRefusal> {
1041 let invalid = |message: String| Err(MessageRefusal::InvalidParams(message));
1042 let Some(message) = params.get("message").and_then(Value::as_object) else {
1043 return invalid("params.message must be an A2A Message object".to_string());
1044 };
1045 let message_id = match message.get("messageId") {
1046 Some(Value::String(id)) if !id.is_empty() => id.clone(),
1047 _ => return invalid("message.messageId must be a non-empty string".to_string()),
1048 };
1049 if message.get("role").and_then(Value::as_str) != Some(Role::User.wire_name()) {
1050 return invalid(format!(
1051 "message.role must be {}: a message to this agent is from the user",
1052 Role::User.wire_name()
1053 ));
1054 }
1055 let raw_parts = match message.get("parts") {
1056 Some(Value::Array(parts)) if !parts.is_empty() => parts,
1057 _ => return invalid("message.parts must be a non-empty array".to_string()),
1058 };
1059 for (index, part) in raw_parts.iter().enumerate() {
1060 if let Some(error) = Part::content_count_error(part) {
1061 return invalid(format!("message.parts[{index}] {error}"));
1062 }
1063 }
1064 let mut parts = Vec::with_capacity(raw_parts.len());
1065 for (index, part) in raw_parts.iter().enumerate() {
1066 match Part::from_json(part) {
1067 Ok(part) => parts.push(part),
1068 Err(error) => return invalid(format!("message.parts[{index}].{error}")),
1069 }
1070 }
1071 let optional_string = |field: &str| -> Result<Option<String>, MessageRefusal> {
1072 match message.get(field) {
1073 None => Ok(None),
1074 Some(Value::String(value)) => Ok(Some(value.clone())),
1075 Some(_) => Err(MessageRefusal::InvalidParams(format!(
1076 "message.{field} must be a string"
1077 ))),
1078 }
1079 };
1080 let task_id = optional_string("taskId")?.filter(|id| !id.is_empty());
1081 let context_id = optional_string("contextId")?;
1082 let reference_task_ids = match message.get("referenceTaskIds") {
1083 None => Vec::new(),
1084 Some(Value::Array(ids)) => {
1085 let mut distinct: Vec<String> = Vec::new();
1086 for id in ids {
1087 match id {
1088 Value::String(id) if !id.is_empty() => {
1089 if !distinct.contains(id) {
1090 distinct.push(id.clone());
1091 }
1092 }
1093 _ => {
1094 return invalid(
1095 "message.referenceTaskIds must hold non-empty strings".to_string(),
1096 )
1097 }
1098 }
1099 }
1100 if distinct.len() > MAX_REFERENCE_TASK_IDS {
1101 return invalid(format!(
1102 "message.referenceTaskIds names {} tasks; at most {MAX_REFERENCE_TASK_IDS} \
1103 may be referenced",
1104 distinct.len()
1105 ));
1106 }
1107 distinct
1108 }
1109 Some(_) => return invalid("message.referenceTaskIds must be an array".to_string()),
1110 };
1111 let door_metadata = message
1112 .get("metadata")
1113 .and_then(Value::as_object)
1114 .and_then(|metadata| metadata.get(DOOR_EXTENSION_URI));
1115 let forget_session = match door_metadata {
1116 None => false,
1117 Some(Value::Object(door)) => match door.get("forgetSession") {
1118 None => false,
1119 Some(Value::Bool(forget)) => *forget,
1120 Some(_) => {
1121 return invalid(format!(
1122 "message.metadata[\"{DOOR_EXTENSION_URI}\"].forgetSession must be a boolean"
1123 ))
1124 }
1125 },
1126 Some(_) => {
1127 return invalid(format!(
1128 "message.metadata[\"{DOOR_EXTENSION_URI}\"] must be an object"
1129 ))
1130 }
1131 };
1132 if let (true, Some(task_id)) = (forget_session, &task_id) {
1133 return invalid(format!(
1134 "forgetSession starts a new conversation, so it goes on a message that starts a task, \
1135 not on one that continues task {task_id}"
1136 ));
1137 }
1138 let configuration = params.get("configuration");
1139 let return_immediately = match configuration.and_then(|c| c.get("returnImmediately")) {
1140 None | Some(Value::Null) => false,
1141 Some(Value::Bool(immediately)) => *immediately,
1142 Some(_) => return invalid("configuration.returnImmediately must be a boolean".to_string()),
1143 };
1144 if let Some((part_index, part)) = parts
1145 .iter()
1146 .enumerate()
1147 .find(|(_, part)| matches!(part.content, PartContent::Raw(_) | PartContent::Url(_)))
1148 {
1149 return Err(MessageRefusal::ContentTypeNotSupported {
1150 part_index,
1151 kind: part.content.kind(),
1152 media_type: part.media_type.clone(),
1153 });
1154 }
1155 if configuration
1156 .and_then(|configuration| configuration.get("taskPushNotificationConfig"))
1157 .is_some_and(|config| !config.is_null())
1158 {
1159 return Err(MessageRefusal::PushNotificationNotSupported);
1160 }
1161 Ok(IncomingMessage {
1162 message_id,
1163 context_id,
1164 task_id,
1165 reference_task_ids,
1166 parts,
1167 door_extension: door_metadata.is_some(),
1168 forget_session,
1169 return_immediately,
1170 })
1171}
1172
1173impl IncomingMessage {
1174 pub(crate) fn agent_text(&self) -> String {
1178 self.parts
1179 .iter()
1180 .map(|part| match &part.content {
1181 PartContent::Text(text) => text.clone(),
1182 PartContent::Data(value) => {
1183 let mut label = "data".to_string();
1184 if let Some(media_type) = &part.media_type {
1185 label.push_str(&format!(" media-type={}", fence_label_value(media_type)));
1186 }
1187 if let Some(filename) = &part.filename {
1188 label.push_str(&format!(" filename={}", fence_label_value(filename)));
1189 }
1190 let body = serde_json::to_string_pretty(value).unwrap_or_default();
1191 code_fence(&label, &body)
1192 }
1193 PartContent::Raw(_) | PartContent::Url(_) => {
1194 unreachable!("read_send_params refuses every file part")
1195 }
1196 })
1197 .collect::<Vec<_>>()
1198 .join("\n")
1199 }
1200
1201 pub(crate) fn part_kinds(&self) -> Vec<&'static str> {
1203 self.parts.iter().map(|part| part.content.kind()).collect()
1204 }
1205}
1206
1207pub(crate) fn code_fence(label: &str, body: &str) -> String {
1211 let mut longest = 0;
1212 let mut run = 0;
1213 for c in body.chars() {
1214 run = if c == '`' { run + 1 } else { 0 };
1215 longest = longest.max(run);
1216 }
1217 let fence = "`".repeat((longest + 1).max(3));
1218 format!("{fence}{label}\n{body}\n{fence}")
1219}
1220
1221fn fence_label_value(value: &str) -> String {
1223 value
1224 .chars()
1225 .map(|c| {
1226 if c.is_whitespace() || c == '`' || c == '=' {
1227 '_'
1228 } else {
1229 c
1230 }
1231 })
1232 .collect()
1233}
1234
1235pub(crate) fn user_message(message_id: &str, context_id: Option<&str>, text: &str) -> Value {
1240 let mut message = serde_json::json!({
1241 "messageId": message_id,
1242 "role": Role::User.wire_name(),
1243 "parts": [Part::text(text).to_json()],
1244 });
1245 if let Some(context_id) = context_id {
1246 message["contextId"] = Value::String(context_id.to_string());
1247 }
1248 message
1249}
1250
1251pub(crate) fn send_message_params(message: Value) -> Value {
1255 serde_json::json!({
1256 "message": message,
1257 "configuration": { "returnImmediately": true },
1258 })
1259}
1260
1261#[derive(Debug, Clone)]
1264pub(crate) enum SendMessageResult {
1265 Task(Box<A2aTask>),
1266 Message(Value),
1268}
1269
1270impl SendMessageResult {
1271 pub(crate) fn from_result(result: &Value) -> Result<SendMessageResult, String> {
1274 match send_message_member(result)? {
1275 ("task", task) => serde_json::from_value(task.clone())
1276 .map(|task| SendMessageResult::Task(Box::new(task)))
1277 .map_err(|error| format!("its task is not an A2A task: {error}")),
1278 (_, message) => Ok(SendMessageResult::Message(message.clone())),
1279 }
1280 }
1281}
1282
1283pub(crate) fn send_message_member(result: &Value) -> Result<(&'static str, &Value), String> {
1286 match (result.get("task"), result.get("message")) {
1287 (Some(task), None) => Ok(("task", task)),
1288 (None, Some(message)) => Ok(("message", message)),
1289 (Some(_), Some(_)) => Err("it answered both a task and a message".to_string()),
1290 (None, None) => Err(format!(
1291 "it answered neither a task nor a message: {result}"
1292 )),
1293 }
1294}
1295
1296#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1302pub(crate) enum CancelOutcome {
1303 Accepted,
1305 AlreadyTerminal,
1307 Unknown,
1309}
1310
1311#[derive(Debug, Clone)]
1314pub(crate) enum TaskSlotState {
1315 Empty,
1316 Running {
1317 task_id: String,
1318 context_id: String,
1319 lane: TaskLane,
1322 },
1323 Done,
1325}
1326
1327pub(crate) struct TaskRegistry {
1332 pub(crate) active_slot: TaskSlotState,
1333 pub(crate) history: HashMap<String, (TaskState, String)>,
1335 pub(crate) pending_count: usize,
1336 pub(crate) queue_depth: usize,
1337 pub(crate) task_acceptance: TaskAcceptance,
1338 input_waiters: HashMap<String, (String, oneshot::Sender<String>)>,
1340 input_timeouts: HashSet<String>,
1342 cancels: HashMap<String, CancelSignal>,
1348 resource_generation: Arc<AtomicU64>,
1352 closed: bool,
1355 endings: HashMap<String, TaskEnding>,
1357 owners: HashMap<String, String>,
1360 revision: u64,
1362 revisions: HashMap<String, u64>,
1364 status_times: HashMap<String, DateTime<Utc>>,
1367 standing_answers: HashSet<String>,
1370 stopped_at_deadline: HashSet<String>,
1373}
1374
1375pub(crate) const REJECTED_SESSION_ENDED_MESSAGE: &str =
1377 "task rejected: the session ended before this task started";
1378
1379pub(crate) const REJECTED_SESSION_STOPPED_MESSAGE: &str =
1381 "task rejected: the session was stopped before this task started";
1382
1383pub(crate) const REJECTED_SESSION_CLOSING_MESSAGE: &str = "task rejected: the session is closing";
1385
1386pub(crate) const REJECTED_BUSY_MESSAGE: &str = "task rejected: capsule is busy";
1389
1390impl TaskRegistry {
1391 pub(crate) fn new(queue_depth: usize, task_acceptance: TaskAcceptance) -> Self {
1392 Self {
1393 active_slot: TaskSlotState::Empty,
1394 history: HashMap::new(),
1395 pending_count: 0,
1396 queue_depth,
1397 task_acceptance,
1398 input_waiters: HashMap::new(),
1399 input_timeouts: HashSet::new(),
1400 cancels: HashMap::new(),
1401 resource_generation: Arc::new(AtomicU64::new(0)),
1402 closed: false,
1403 endings: HashMap::new(),
1404 owners: HashMap::new(),
1405 revision: 0,
1406 revisions: HashMap::new(),
1407 status_times: HashMap::new(),
1408 standing_answers: HashSet::new(),
1409 stopped_at_deadline: HashSet::new(),
1410 }
1411 }
1412
1413 pub(crate) fn resource_generation(&self) -> Arc<AtomicU64> {
1419 Arc::clone(&self.resource_generation)
1420 }
1421
1422 pub(crate) fn advance_resource_generation(&self) {
1429 self.resource_generation.fetch_add(1, Ordering::SeqCst);
1430 }
1431
1432 pub(crate) fn can_accept(&self) -> bool {
1433 if self.closed {
1434 return false;
1435 }
1436 match self.task_acceptance {
1437 TaskAcceptance::None => false,
1438 TaskAcceptance::Single => {
1439 matches!(self.active_slot, TaskSlotState::Empty) && self.pending_count == 0
1440 }
1441 TaskAcceptance::Queue => self.pending_count < self.queue_depth,
1442 }
1443 }
1444
1445 pub(crate) fn enqueue(&mut self, task_id: &str, context_id: &str) {
1446 self.pending_count += 1;
1447 self.record_state(task_id, TaskState::Submitted, context_id.to_string());
1448 }
1449
1450 pub(crate) fn withdraw(&mut self, task_id: &str) {
1453 self.pending_count = self.pending_count.saturating_sub(1);
1454 self.history.remove(task_id);
1455 self.revisions.remove(task_id);
1456 self.status_times.remove(task_id);
1457 self.owners.remove(task_id);
1458 }
1459
1460 pub(crate) fn record_state(&mut self, task_id: &str, state: TaskState, context_id: String) {
1464 self.history
1465 .insert(task_id.to_string(), (state, context_id));
1466 self.touch(task_id);
1467 }
1468
1469 fn touch(&mut self, task_id: &str) {
1472 self.revision += 1;
1473 self.revisions.insert(task_id.to_string(), self.revision);
1474 self.status_times.insert(task_id.to_string(), status_now());
1475 }
1476
1477 fn status_time(&self, task_id: &str) -> Option<DateTime<Utc>> {
1479 self.status_times.get(task_id).copied()
1480 }
1481
1482 pub(crate) fn record_owner(&mut self, task_id: &str, credential: &str) {
1485 self.owners
1486 .insert(task_id.to_string(), credential.to_string());
1487 }
1488
1489 pub(crate) fn owner(&self, task_id: &str) -> Option<&str> {
1492 self.owners.get(task_id).map(String::as_str)
1493 }
1494
1495 pub(crate) fn record_ending(
1501 &mut self,
1502 task_id: &str,
1503 message: &str,
1504 response: Option<&str>,
1505 no_answer: bool,
1506 no_answer_below: &[(String, String)],
1507 running_commands: &[RunningCommand],
1508 ) {
1509 let completed = matches!(self.history.get(task_id), Some((TaskState::Completed, _)));
1510 self.endings.insert(
1511 task_id.to_string(),
1512 TaskEnding {
1513 message: message.to_string(),
1514 response: response.filter(|_| completed).map(str::to_string),
1515 no_answer,
1516 no_answer_below: no_answer_below.to_vec(),
1517 running_commands: running_commands.to_vec(),
1518 },
1519 );
1520 self.touch(task_id);
1521 }
1522
1523 pub(crate) fn start_task(&mut self, task_id: String, context_id: String, lane: TaskLane) {
1526 debug_assert!(self.pending_count > 0);
1527 self.pending_count -= 1;
1528 self.record_state(&task_id, TaskState::Working, context_id.clone());
1529 self.active_slot = TaskSlotState::Running {
1530 task_id,
1531 context_id,
1532 lane,
1533 };
1534 }
1535
1536 pub(crate) fn active_lane(&self) -> Option<TaskLane> {
1541 match self.active_slot {
1542 TaskSlotState::Running { lane, .. } => Some(lane),
1543 TaskSlotState::Empty | TaskSlotState::Done => None,
1544 }
1545 }
1546
1547 pub(crate) fn finish_task(&mut self, final_state: TaskState) -> Option<TaskState> {
1551 let TaskSlotState::Running {
1552 ref task_id,
1553 ref context_id,
1554 ..
1555 } = self.active_slot
1556 else {
1557 return None;
1558 };
1559 let (tid, cid) = (task_id.clone(), context_id.clone());
1560 self.input_waiters.remove(&tid);
1561 self.input_timeouts.remove(&tid);
1562 self.cancels.remove(&tid);
1563 self.standing_answers.remove(&tid);
1564 self.stopped_at_deadline.remove(&tid);
1565 let final_state = match self.history.get(&tid) {
1569 Some((TaskState::Canceled, _)) => TaskState::Canceled,
1570 _ => final_state,
1571 };
1572 self.record_state(&tid, final_state.clone(), cid);
1573 self.active_slot = TaskSlotState::Done;
1574 Some(final_state)
1575 }
1576
1577 pub(crate) fn record_input_timeout(&mut self, task_id: &str) {
1582 self.input_waiters.remove(task_id);
1583 if let Some((TaskState::InputRequired, ctx)) = self.history.get(task_id).cloned() {
1584 self.record_state(task_id, TaskState::Working, ctx);
1585 }
1586 self.input_timeouts.insert(task_id.to_string());
1587 }
1588
1589 pub(crate) fn take_input_timeout(&mut self, task_id: &str) -> bool {
1593 self.input_timeouts.remove(task_id)
1594 }
1595
1596 pub(crate) fn set_input_required(
1599 &mut self,
1600 task_id: &str,
1601 prompt: String,
1602 tx: oneshot::Sender<String>,
1603 ) -> Result<(), &'static str> {
1604 match &self.active_slot {
1605 TaskSlotState::Running {
1606 task_id: active_id, ..
1607 } if active_id == task_id => {}
1608 _ => return Err("task is not the active running task"),
1609 }
1610 let state = self.history.get(task_id).map(|(s, _)| s);
1611 if !matches!(state, Some(TaskState::Working)) {
1612 return Err("task is not in working state");
1613 }
1614 if let Some((_, ctx)) = self.history.get(task_id).cloned() {
1615 self.record_state(task_id, TaskState::InputRequired, ctx);
1616 }
1617 self.input_waiters.insert(task_id.to_string(), (prompt, tx));
1618 Ok(())
1619 }
1620
1621 pub(crate) fn deliver_input(
1623 &mut self,
1624 task_id: &str,
1625 text: String,
1626 ) -> Result<(), &'static str> {
1627 let state = self.history.get(task_id).map(|(s, _)| s);
1628 if !matches!(state, Some(TaskState::InputRequired)) {
1629 return Err("task is not in input-required state");
1630 }
1631 let Some((_, tx)) = self.input_waiters.remove(task_id) else {
1632 return Err("no input waiter found for task");
1633 };
1634 if let Some((_, ctx)) = self.history.get(task_id).cloned() {
1635 self.record_state(task_id, TaskState::Working, ctx);
1636 }
1637 let _ = tx.send(text);
1639 Ok(())
1640 }
1641
1642 pub(crate) fn cancel_watch(&mut self, task_id: &str) -> CancelSignal {
1648 self.cancels.entry(task_id.to_string()).or_default().clone()
1649 }
1650
1651 pub(crate) fn is_canceled(&self, task_id: &str) -> bool {
1656 matches!(self.history.get(task_id), Some((TaskState::Canceled, _)))
1657 }
1658
1659 pub(crate) fn request_cancel(&mut self, task_id: &str) -> CancelOutcome {
1669 let Some((state, context_id)) = self.history.get(task_id).cloned() else {
1670 return CancelOutcome::Unknown;
1671 };
1672 if state.is_terminal() {
1673 return CancelOutcome::AlreadyTerminal;
1674 }
1675 if matches!(state, TaskState::Submitted) {
1676 self.pending_count = self.pending_count.saturating_sub(1);
1677 }
1678 self.record_state(task_id, TaskState::Canceled, context_id);
1679 self.cancel_watch(task_id).cancel();
1680 CancelOutcome::Accepted
1681 }
1682
1683 pub(crate) fn mark_standing_answer(&mut self, task_id: &str) {
1686 self.standing_answers.insert(task_id.to_string());
1687 }
1688
1689 pub(crate) fn clear_standing_answer(&mut self, task_id: &str) {
1692 self.standing_answers.remove(task_id);
1693 }
1694
1695 pub(crate) fn stopped_at_deadline(&self, task_id: &str) -> bool {
1698 self.stopped_at_deadline.contains(task_id)
1699 }
1700
1701 pub(crate) fn cancel_every_live(&mut self, spare_standing_answers: bool) -> Vec<String> {
1712 let live: Vec<String> = self
1713 .history
1714 .iter()
1715 .filter(|(_, (state, _))| !state.is_terminal())
1716 .map(|(task_id, _)| task_id.clone())
1717 .collect();
1718 let (spared, live): (Vec<String>, Vec<String>) = live
1719 .into_iter()
1720 .partition(|task_id| spare_standing_answers && self.standing_answers.contains(task_id));
1721 for task_id in spared {
1722 self.cancel_watch(&task_id).cancel();
1723 self.stopped_at_deadline.insert(task_id);
1724 }
1725 let mut canceled: Vec<String> = live
1726 .into_iter()
1727 .filter(|task_id| matches!(self.request_cancel(task_id), CancelOutcome::Accepted))
1728 .collect();
1729 canceled.sort();
1732 canceled
1733 }
1734
1735 pub(crate) fn close_to_new_work(&mut self) -> Vec<(String, String)> {
1743 self.closed = true;
1744 let mut refused: Vec<(String, String)> = self
1745 .history
1746 .iter()
1747 .filter(|(_, (state, _))| matches!(state, TaskState::Submitted))
1748 .map(|(task_id, (_, context_id))| (task_id.clone(), context_id.clone()))
1749 .collect();
1750 refused.sort();
1751 for (task_id, context_id) in &refused {
1752 self.record_state(task_id, TaskState::Rejected, context_id.clone());
1753 self.cancels.remove(task_id);
1754 }
1755 self.pending_count = self.pending_count.saturating_sub(refused.len());
1756 refused
1757 }
1758
1759 pub(crate) fn is_closed(&self) -> bool {
1762 self.closed
1763 }
1764
1765 pub(crate) fn has_ending(&self, task_id: &str) -> bool {
1768 self.endings.contains_key(task_id)
1769 }
1770
1771 #[allow(dead_code)] pub(crate) fn get_input_prompt(&self, task_id: &str) -> Option<&str> {
1774 self.input_waiters
1775 .get(task_id)
1776 .map(|(prompt, _)| prompt.as_str())
1777 }
1778
1779 pub(crate) fn get_task(&self, task_id: &str) -> Option<A2aTask> {
1786 self.history.get(task_id).map(|(state, context_id)| {
1787 let artifact =
1788 |name: &str, text: &str| A2aArtifact::named(name, vec![Part::text(text)]);
1789 let ending = self.endings.get(task_id).filter(|_| state.is_terminal());
1790 let artifacts = match state {
1791 TaskState::InputRequired => self
1792 .input_waiters
1793 .get(task_id)
1794 .map(|(prompt, _)| vec![artifact(PROMPT_ARTIFACT, prompt)]),
1795 TaskState::Completed => ending
1796 .and_then(|ending| ending.response.as_deref())
1797 .filter(|response| !response.is_empty())
1798 .map(|response| vec![artifact(RESPONSE_ARTIFACT, response)]),
1799 _ => None,
1800 };
1801 let message = ending
1802 .filter(|ending| !ending.message.is_empty())
1803 .map(|ending| StatusMessage::agent(task_id, context_id, &ending.message));
1804 A2aTask {
1805 id: task_id.to_string(),
1806 context_id: context_id.clone(),
1807 status: TaskStatus::at(
1808 state.clone(),
1809 message,
1810 self.status_time(task_id).unwrap_or_else(status_now),
1811 ),
1812 artifacts,
1813 metadata: ending.and_then(TaskEnding::metadata),
1814 }
1815 })
1816 }
1817
1818 pub(crate) fn reference_block(
1826 &self,
1827 reference_task_ids: &[String],
1828 visible: impl Fn(&str) -> bool,
1829 ) -> Option<String> {
1830 if reference_task_ids.is_empty() {
1831 return None;
1832 }
1833 let lines: Vec<String> = reference_task_ids
1834 .iter()
1835 .map(|task_id| {
1836 let Some((state, _)) = self.history.get(task_id).filter(|_| visible(task_id))
1837 else {
1838 return format!("- {task_id}: not a task this capsule holds");
1839 };
1840 let mut line = format!("- {task_id}: {}", state.as_str());
1841 let ending = self.endings.get(task_id).filter(|_| state.is_terminal());
1842 match (state, ending) {
1843 (TaskState::Completed, Some(ending)) => {
1844 if let Some(response) =
1845 ending.response.as_deref().filter(|text| !text.is_empty())
1846 {
1847 line.push('\n');
1848 line.push_str(&code_fence(RESPONSE_ARTIFACT, response));
1849 }
1850 }
1851 (_, Some(ending)) if !ending.message.is_empty() => {
1852 line.push_str(": ");
1853 line.push_str(&ending.message.replace(['\r', '\n'], " "));
1854 }
1855 _ => {}
1856 }
1857 line
1858 })
1859 .collect();
1860 Some(format!("Referenced tasks:\n{}", lines.join("\n")))
1861 }
1862
1863 pub(crate) fn list_tasks(
1871 &self,
1872 filter: &TaskFilter,
1873 after: Option<&ListCursor>,
1874 page_size: usize,
1875 visible: impl Fn(&str) -> bool,
1876 ) -> TaskPage {
1877 let mut matching: Vec<ListCursor> = self
1878 .history
1879 .iter()
1880 .filter(|(task_id, (state, context_id))| {
1881 filter.matches(state, context_id, self.status_time(task_id)) && visible(task_id)
1882 })
1883 .map(|(task_id, _)| ListCursor {
1884 revision: self.revisions.get(task_id).copied().unwrap_or_default(),
1885 task_id: task_id.clone(),
1886 })
1887 .collect();
1888 matching.sort_unstable_by(|a, b| b.cmp(a));
1889 let total = matching.len();
1890 let rest: Vec<ListCursor> = matching
1891 .into_iter()
1892 .filter(|position| after.is_none_or(|after| position < after))
1893 .collect();
1894 let page: Vec<&ListCursor> = rest.iter().take(page_size).collect();
1895 let next = (rest.len() > page.len())
1896 .then(|| page.last().map(|last| (*last).clone()))
1897 .flatten();
1898 TaskPage {
1899 tasks: page
1900 .iter()
1901 .filter_map(|position| self.get_task(&position.task_id))
1902 .collect(),
1903 next,
1904 total,
1905 }
1906 }
1907}
1908
1909#[derive(Debug, Clone, PartialEq)]
1911pub(crate) struct TaskFilter {
1912 pub context_id: Option<String>,
1914 pub state: StateFilter,
1915 pub status_after: Option<DateTime<Utc>>,
1918}
1919
1920impl TaskFilter {
1921 fn matches(&self, state: &TaskState, context_id: &str, at: Option<DateTime<Utc>>) -> bool {
1922 let in_context = self.context_id.as_deref().is_none_or(|c| c == context_id);
1923 let in_state = match &self.state {
1924 StateFilter::Any => true,
1925 StateFilter::Only(wanted) => wanted == state,
1926 StateFilter::Nothing => false,
1927 };
1928 let in_time = self
1929 .status_after
1930 .is_none_or(|after| at.is_some_and(|at| at >= after));
1931 in_context && in_state && in_time
1932 }
1933}
1934
1935#[derive(Debug, Clone, PartialEq)]
1937pub(crate) enum StateFilter {
1938 Any,
1940 Only(TaskState),
1942 Nothing,
1944}
1945
1946impl StateFilter {
1947 pub(crate) fn from_wire_name(wire: &str) -> Option<StateFilter> {
1950 match wire {
1951 "TASK_STATE_UNSPECIFIED" => Some(StateFilter::Any),
1952 "TASK_STATE_AUTH_REQUIRED" => Some(StateFilter::Nothing),
1953 wire => TaskState::from_wire_name(wire).map(StateFilter::Only),
1954 }
1955 }
1956}
1957
1958#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
1961pub(crate) struct ListCursor {
1962 pub revision: u64,
1963 pub task_id: String,
1964}
1965
1966impl ListCursor {
1967 pub(crate) fn encode(&self) -> String {
1970 use base64::Engine as _;
1971 base64::engine::general_purpose::URL_SAFE_NO_PAD
1972 .encode(format!("{}:{}", self.revision, self.task_id))
1973 }
1974
1975 pub(crate) fn decode(token: &str) -> Option<ListCursor> {
1977 use base64::Engine as _;
1978 let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
1979 .decode(token)
1980 .ok()?;
1981 let text = String::from_utf8(bytes).ok()?;
1982 let (revision, task_id) = text.split_once(':')?;
1983 let revision = revision
1984 .parse()
1985 .ok()
1986 .filter(|_| !revision.starts_with('+'))?;
1987 (!task_id.is_empty()).then(|| ListCursor {
1988 revision,
1989 task_id: task_id.to_string(),
1990 })
1991 }
1992}
1993
1994#[derive(Debug, Clone)]
1996pub(crate) struct TaskPage {
1997 pub tasks: Vec<A2aTask>,
1998 pub next: Option<ListCursor>,
2000 pub total: usize,
2002}
2003
2004#[derive(Debug)]
2007pub(crate) struct IncomingTask {
2008 pub task_id: String,
2009 pub context_id: String,
2010 pub message_id: String,
2011 pub message_text: String,
2012 pub traceparent: Option<String>,
2013 pub provenance: TaskProvenance,
2016 pub source: &'static str,
2021 pub forget_session: bool,
2026 pub caller_member: Option<String>,
2029 pub reference_task_ids: Vec<String>,
2032 pub part_kinds: Vec<&'static str>,
2035}
2036
2037pub(crate) const SOURCE_A2A: &str = "a2a";
2039
2040pub(crate) const SOURCE_TASK_MD: &str = "task_md";
2044
2045pub(crate) const SOURCE_DETACHED_SHELL: &str = "detached_shell";
2047
2048pub(crate) const SOURCE_DETACHED_LOST: &str = "detached_lost";
2052
2053#[cfg(test)]
2056mod tests {
2057 use super::*;
2058
2059 fn make_registry() -> TaskRegistry {
2060 TaskRegistry::new(1, TaskAcceptance::Single)
2061 }
2062
2063 fn running_registry(task_id: &str) -> TaskRegistry {
2064 let mut r = make_registry();
2065 r.enqueue(task_id, "ctx_001");
2066 r.start_task(task_id.to_string(), "ctx_001".to_string(), TaskLane::Bg);
2067 r
2068 }
2069
2070 #[test]
2071 fn set_input_required_transitions_state() {
2072 let mut r = running_registry("tsk_001");
2073 let (tx, _rx) = oneshot::channel();
2074 assert!(r
2075 .set_input_required("tsk_001", "which branch?".into(), tx)
2076 .is_ok());
2077 let task = r.get_task("tsk_001").unwrap();
2078 assert_eq!(task.status.state, TaskState::InputRequired);
2079 let artifacts = task.artifacts.unwrap();
2080 assert_eq!(artifacts[0].artifact_id, "prompt");
2081 assert_eq!(artifacts[0].first_text(), Some("which branch?"));
2082 }
2083
2084 #[test]
2085 fn set_input_required_fails_for_wrong_task() {
2086 let mut r = running_registry("tsk_001");
2087 let (tx, _rx) = oneshot::channel();
2088 assert!(r
2089 .set_input_required("tsk_other", "prompt".into(), tx)
2090 .is_err());
2091 }
2092
2093 #[test]
2094 fn deliver_input_transitions_back_to_working() {
2095 let mut r = running_registry("tsk_001");
2096 let (tx, mut rx) = oneshot::channel();
2097 r.set_input_required("tsk_001", "which branch?".into(), tx)
2098 .unwrap();
2099 r.deliver_input("tsk_001", "main".to_string()).unwrap();
2100 assert_eq!(rx.try_recv().unwrap(), "main");
2102 let task = r.get_task("tsk_001").unwrap();
2103 assert_eq!(task.status.state, TaskState::Working);
2104 assert!(task.artifacts.is_none());
2105 }
2106
2107 #[test]
2108 fn deliver_input_fails_when_not_input_required() {
2109 let mut r = running_registry("tsk_001");
2110 assert!(r.deliver_input("tsk_001", "text".into()).is_err());
2111 }
2112
2113 #[test]
2114 fn an_input_required_task_carries_its_prompt_and_nothing_else() {
2115 let mut r = running_registry("tsk_1");
2116 let (tx, _rx) = oneshot::channel();
2117 r.set_input_required("tsk_1", "Which file?".to_string(), tx)
2118 .unwrap();
2119 let task = r.get_task("tsk_1").unwrap();
2120 let artifacts = task.artifacts.unwrap();
2121 assert_eq!(artifacts.len(), 1);
2122 assert_eq!(
2123 serde_json::to_value(&artifacts[0]).unwrap(),
2124 serde_json::json!({"artifactId": "prompt", "name": "prompt",
2125 "parts": [{"text": "Which file?", "mediaType": "text/plain"}]})
2126 );
2127 assert!(task.status.message.is_none());
2128 }
2129
2130 #[test]
2132 fn a_working_task_carries_no_artifact_and_no_message() {
2133 let r = running_registry("tsk_1");
2134 let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2135 assert_bare_status(&task["status"], "TASK_STATE_WORKING");
2136 assert_eq!(
2137 task,
2138 serde_json::json!({"id": "tsk_1", "contextId": "ctx_001", "status": {
2139 "state": "TASK_STATE_WORKING",
2140 "timestamp": timestamp_of(&r, "tsk_1"),
2141 }})
2142 );
2143 }
2144
2145 #[test]
2148 fn a_completed_task_carries_its_response_artifact_and_status_message() {
2149 let mut r = running_registry("tsk_1");
2150 r.finish_task(TaskState::Completed);
2151 r.record_ending("tsk_1", "done", Some("the answer is 4"), false, &[], &[]);
2152 let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2153 assert_eq!(
2154 task,
2155 serde_json::json!({
2156 "id": "tsk_1",
2157 "contextId": "ctx_001",
2158 "status": {
2159 "state": "TASK_STATE_COMPLETED",
2160 "message": {
2161 "messageId": "msg_tsk_1_status",
2162 "contextId": "ctx_001",
2163 "taskId": "tsk_1",
2164 "role": "ROLE_AGENT",
2165 "parts": [{"text": "done", "mediaType": "text/plain"}],
2166 },
2167 "timestamp": status_timestamp(r.status_times["tsk_1"]),
2168 },
2169 "artifacts": [{
2170 "artifactId": "response",
2171 "name": "response",
2172 "parts": [{"text": "the answer is 4", "mediaType": "text/plain"}],
2173 }],
2174 })
2175 );
2176 }
2177
2178 #[test]
2182 fn a_task_ending_with_commands_running_names_them_in_its_metadata() {
2183 let long = "x".repeat(250);
2184 let running = [
2185 RunningCommand {
2186 work_id: "wrk_0a".to_string(),
2187 binary: "/usr/bin/bash".to_string(),
2188 command: "sleep 300".to_string(),
2189 running_ms: 12_000,
2190 },
2191 RunningCommand {
2192 work_id: "wrk_0b".to_string(),
2193 binary: "/usr/bin/bash".to_string(),
2194 command: long.clone(),
2195 running_ms: 3_000,
2196 },
2197 ];
2198 let mut r = running_registry("tsk_1");
2199 r.finish_task(TaskState::Completed);
2200 r.record_ending("tsk_1", "done", Some("4"), false, &[], &running);
2201 let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2202 assert_eq!(
2203 task["metadata"],
2204 serde_json::json!({"murmur": {"runningCommands": [
2205 {"workId": "wrk_0a", "binary": "/usr/bin/bash", "command": "sleep 300",
2206 "runningMs": 12_000},
2207 {"workId": "wrk_0b", "binary": "/usr/bin/bash",
2208 "command": format!("{}…", &long[..200]), "runningMs": 3_000},
2209 ]}})
2210 );
2211
2212 let mut r = running_registry("tsk_2");
2213 r.finish_task(TaskState::Completed);
2214 r.record_ending("tsk_2", "done", Some("4"), false, &[], &[]);
2215 let task = serde_json::to_value(r.get_task("tsk_2").unwrap()).unwrap();
2216 assert!(task.get("metadata").is_none(), "{task}");
2217 }
2218
2219 #[test]
2224 fn a_no_answer_ending_carries_its_metadata_and_no_other_ending_does() {
2225 let below = [("q".to_string(), "timed_out".to_string())];
2226 let mut r = running_registry("tsk_1");
2227 r.finish_task(TaskState::Failed);
2228 r.record_ending("tsk_1", "q gave no answer", None, true, &below, &[]);
2229 let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2230 assert_eq!(task["status"]["state"], "TASK_STATE_FAILED");
2231 assert_eq!(
2232 task["status"]["message"]["parts"][0]["text"],
2233 "q gave no answer"
2234 );
2235 assert_eq!(
2236 task["metadata"],
2237 serde_json::json!({"murmur": {
2238 "noAnswer": true,
2239 "noAnswerBelow": [{"member": "q", "status": "timed_out"}],
2240 }})
2241 );
2242
2243 let mut r = running_registry("tsk_2");
2244 r.finish_task(TaskState::Completed);
2245 r.record_ending(
2246 "tsk_2",
2247 "task completed",
2248 Some("none came"),
2249 false,
2250 &below,
2251 &[],
2252 );
2253 let task = serde_json::to_value(r.get_task("tsk_2").unwrap()).unwrap();
2254 assert_eq!(
2255 task["artifacts"],
2256 serde_json::json!([{"artifactId": "response", "name": "response",
2257 "parts": [{"text": "none came", "mediaType": "text/plain"}]}])
2258 );
2259 assert_eq!(
2260 task["metadata"],
2261 serde_json::json!({"murmur": {
2262 "noAnswerBelow": [{"member": "q", "status": "timed_out"}],
2263 }})
2264 );
2265
2266 for state in [TaskState::Completed, TaskState::Failed] {
2267 let mut r = running_registry("tsk_3");
2268 r.finish_task(state);
2269 r.record_ending("tsk_3", "ended", Some("4"), false, &[], &[]);
2270 let task = serde_json::to_value(r.get_task("tsk_3").unwrap()).unwrap();
2271 assert!(task.get("metadata").is_none(), "{task}");
2272 }
2273 let r = running_registry("tsk_4");
2274 let task = serde_json::to_value(r.get_task("tsk_4").unwrap()).unwrap();
2275 assert!(task.get("metadata").is_none(), "{task}");
2276 }
2277
2278 #[test]
2280 fn a_failed_task_carries_its_status_message_and_no_response() {
2281 let mut r = running_registry("tsk_1");
2282 r.finish_task(TaskState::Failed);
2283 r.record_ending(
2284 "tsk_1",
2285 "the driver failed",
2286 Some("partial"),
2287 false,
2288 &[],
2289 &[],
2290 );
2291 let task = r.get_task("tsk_1").unwrap();
2292 assert!(task.artifacts.is_none());
2293 let message = task.status.message.unwrap();
2294 assert_eq!(message.role, Role::Agent);
2295 assert_eq!(message.text(), Some("the driver failed"));
2296 }
2297
2298 #[test]
2300 fn a_completed_task_without_a_response_carries_no_artifact() {
2301 let mut r = running_registry("tsk_1");
2302 r.finish_task(TaskState::Completed);
2303 assert!(r.get_task("tsk_1").unwrap().artifacts.is_none());
2304 r.record_ending("tsk_1", "ok", Some(""), false, &[], &[]);
2305 let task = r.get_task("tsk_1").unwrap();
2306 assert!(task.artifacts.is_none());
2307 assert_eq!(task.status.message.unwrap().text(), Some("ok"));
2308 }
2309
2310 #[test]
2311 fn an_owner_is_recorded_per_task() {
2312 let mut r = make_registry();
2313 r.enqueue("tsk_a", "ctx_001");
2314 r.record_owner("tsk_a", "member:lead");
2315 r.enqueue("tsk_b", "ctx_001");
2316 assert_eq!(r.owner("tsk_a"), Some("member:lead"));
2317 assert_eq!(r.owner("tsk_b"), None);
2318 r.withdraw("tsk_a");
2319 assert_eq!(r.owner("tsk_a"), None);
2320 assert!(r.get_task("tsk_a").is_none());
2321 assert_eq!(r.pending_count, 1);
2322 }
2323
2324 fn listed(page: &TaskPage) -> Vec<&str> {
2326 page.tasks.iter().map(|task| task.id.as_str()).collect()
2327 }
2328
2329 fn every_task() -> TaskFilter {
2330 TaskFilter {
2331 context_id: None,
2332 state: StateFilter::Any,
2333 status_after: None,
2334 }
2335 }
2336
2337 #[test]
2339 fn list_tasks_orders_by_the_last_change_newest_first() {
2340 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2341 for id in ["tsk_a", "tsk_b", "tsk_c"] {
2342 r.enqueue(id, "ctx_001");
2343 }
2344 let all = |r: &TaskRegistry| -> Vec<String> {
2345 listed(&r.list_tasks(&every_task(), None, 10, |_| true))
2346 .into_iter()
2347 .map(str::to_string)
2348 .collect()
2349 };
2350 assert_eq!(all(&r), ["tsk_c", "tsk_b", "tsk_a"]);
2351 r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::User);
2352 assert_eq!(all(&r), ["tsk_a", "tsk_c", "tsk_b"]);
2353 r.request_cancel("tsk_b");
2354 assert_eq!(all(&r), ["tsk_b", "tsk_a", "tsk_c"]);
2355 let (tx, _rx) = oneshot::channel();
2356 r.set_input_required("tsk_a", "which?".to_string(), tx)
2357 .unwrap();
2358 assert_eq!(all(&r), ["tsk_a", "tsk_b", "tsk_c"]);
2359 r.deliver_input("tsk_a", "left".to_string()).unwrap();
2360 r.finish_task(TaskState::Completed);
2361 r.record_ending("tsk_c", "", None, false, &[], &[]);
2362 assert_eq!(all(&r), ["tsk_c", "tsk_a", "tsk_b"]);
2363 r.record_ending("tsk_a", "done", Some("four"), false, &[], &[]);
2364 assert_eq!(all(&r), ["tsk_a", "tsk_c", "tsk_b"]);
2365 r.close_to_new_work();
2366 assert_eq!(all(&r), ["tsk_c", "tsk_a", "tsk_b"], "tsk_c was rejected");
2367
2368 let page = r.list_tasks(&every_task(), None, 10, |_| true);
2369 for task in &page.tasks {
2370 assert_eq!(
2371 serde_json::to_value(task).unwrap(),
2372 serde_json::to_value(r.get_task(&task.id).unwrap()).unwrap()
2373 );
2374 }
2375 }
2376
2377 #[test]
2380 fn list_tasks_walks_pages_and_returns_a_task_changed_mid_walk_once() {
2381 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2382 for id in ["tsk_1", "tsk_2", "tsk_3", "tsk_4", "tsk_5"] {
2383 r.enqueue(id, "ctx_001");
2384 }
2385 let first = r.list_tasks(&every_task(), None, 2, |_| true);
2386 assert_eq!(listed(&first), ["tsk_5", "tsk_4"]);
2387 assert_eq!(first.total, 5);
2388 let cursor = first.next.clone().expect("more tasks lie past the page");
2389 assert_eq!(ListCursor::decode(&cursor.encode()), Some(cursor.clone()));
2390
2391 r.request_cancel("tsk_4");
2392 r.request_cancel("tsk_2");
2393 let second = r.list_tasks(&every_task(), Some(&cursor), 2, |_| true);
2394 assert_eq!(listed(&second), ["tsk_3", "tsk_1"], "tsk_2 moved ahead");
2395 assert_eq!(second.next, None);
2396 assert_eq!(second.total, 5);
2397 let fresh = r.list_tasks(&every_task(), None, 2, |_| true);
2398 assert_eq!(listed(&fresh), ["tsk_2", "tsk_4"]);
2399
2400 let exact = r.list_tasks(&every_task(), None, 5, |_| true);
2401 assert_eq!(exact.tasks.len(), 5);
2402 assert_eq!(
2403 exact.next, None,
2404 "no task lies past a page that ends the list"
2405 );
2406 }
2407
2408 fn instant(timestamp: &str) -> DateTime<Utc> {
2410 DateTime::parse_from_rfc3339(timestamp)
2411 .unwrap()
2412 .with_timezone(&Utc)
2413 }
2414
2415 fn timestamp_of(r: &TaskRegistry, task_id: &str) -> String {
2416 r.get_task(task_id).unwrap().status.timestamp.unwrap()
2417 }
2418
2419 fn a_tick() {
2421 std::thread::sleep(std::time::Duration::from_millis(3));
2422 }
2423
2424 #[test]
2427 fn a_status_timestamp_moves_with_the_revision() {
2428 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2429 r.enqueue("tsk_a", "ctx_001");
2430 let submitted = timestamp_of(&r, "tsk_a");
2431 let submitted_revision = r.revisions["tsk_a"];
2432 assert_eq!(r.status_times["tsk_a"], instant(&submitted));
2433
2434 a_tick();
2435 r.record_owner("tsk_a", "alice");
2436 assert_eq!(
2437 timestamp_of(&r, "tsk_a"),
2438 submitted,
2439 "an owner is no change"
2440 );
2441 assert_eq!(r.revisions["tsk_a"], submitted_revision);
2442
2443 r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::User);
2444 let working = timestamp_of(&r, "tsk_a");
2445 assert!(r.revisions["tsk_a"] > submitted_revision);
2446 assert!(
2447 instant(&working) > instant(&submitted),
2448 "{submitted} → {working}"
2449 );
2450
2451 a_tick();
2452 r.finish_task(TaskState::Completed);
2453 let completed_revision = r.revisions["tsk_a"];
2454 let completed = timestamp_of(&r, "tsk_a");
2455 assert!(instant(&completed) > instant(&working));
2456
2457 a_tick();
2458 r.record_ending("tsk_a", "done", Some("four"), false, &[], &[]);
2459 assert!(r.revisions["tsk_a"] > completed_revision);
2460 assert!(instant(×tamp_of(&r, "tsk_a")) > instant(&completed));
2461 }
2462
2463 #[test]
2466 fn a_status_timestamp_round_trips_and_a_cutoff_equal_to_it_includes_the_task() {
2467 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2468 r.enqueue("tsk_a", "ctx_001");
2469 let timestamp = timestamp_of(&r, "tsk_a");
2470 assert!(timestamp.ends_with('Z'), "{timestamp}");
2471 assert_eq!(
2472 timestamp.len(),
2473 "2026-10-13T09:00:00.123Z".len(),
2474 "{timestamp}"
2475 );
2476 assert_eq!(instant(×tamp), r.status_times["tsk_a"]);
2477 let at = TaskFilter {
2478 status_after: Some(instant(×tamp)),
2479 ..every_task()
2480 };
2481 assert_eq!(listed(&r.list_tasks(&at, None, 10, |_| true)), ["tsk_a"]);
2482 let after = TaskFilter {
2483 status_after: Some(instant(×tamp) + chrono::Duration::milliseconds(1)),
2484 ..every_task()
2485 };
2486 assert!(r.list_tasks(&after, None, 10, |_| true).tasks.is_empty());
2487 }
2488
2489 #[test]
2490 fn withdraw_drops_the_status_time() {
2491 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2492 r.enqueue("tsk_a", "ctx_001");
2493 r.withdraw("tsk_a");
2494 assert!(!r.status_times.contains_key("tsk_a"));
2495 assert!(r.get_task("tsk_a").is_none());
2496 }
2497
2498 #[test]
2501 fn list_tasks_filters_by_status_time_before_paging() {
2502 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2503 for id in ["tsk_1", "tsk_2", "tsk_3", "tsk_4"] {
2504 r.enqueue(id, "ctx_001");
2505 }
2506 r.request_cancel("tsk_3");
2507 let base = instant("2026-10-13T09:00:00Z");
2508 for (offset, id) in ["tsk_1", "tsk_2", "tsk_3", "tsk_4"].iter().enumerate() {
2509 r.status_times.insert(
2510 id.to_string(),
2511 base + chrono::Duration::seconds(offset as i64),
2512 );
2513 }
2514 let since = |seconds: i64| TaskFilter {
2515 status_after: Some(base + chrono::Duration::seconds(seconds)),
2516 ..every_task()
2517 };
2518 let first = r.list_tasks(&since(1), None, 2, |_| true);
2519 assert_eq!(listed(&first), ["tsk_3", "tsk_4"]);
2520 assert_eq!(first.total, 3);
2521 let second = r.list_tasks(&since(1), first.next.as_ref(), 2, |_| true);
2522 assert_eq!(listed(&second), ["tsk_2"]);
2523 assert_eq!((second.total, second.next), (3, None));
2524
2525 let canceled_since = TaskFilter {
2526 state: StateFilter::Only(TaskState::Canceled),
2527 ..since(1)
2528 };
2529 let page = r.list_tasks(&canceled_since, None, 10, |_| true);
2530 assert_eq!((listed(&page), page.total), (vec!["tsk_3"], 1));
2531 assert_eq!(r.list_tasks(&since(4), None, 10, |_| true).total, 0);
2532 assert_eq!(r.list_tasks(&every_task(), None, 10, |_| true).total, 4);
2533 }
2534
2535 #[test]
2537 fn list_tasks_filters_by_context_state_and_visibility() {
2538 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2539 r.enqueue("tsk_a", "ctx_one");
2540 r.record_owner("tsk_a", "alice");
2541 r.enqueue("tsk_b", "ctx_two");
2542 r.record_owner("tsk_b", "alice");
2543 r.enqueue("tsk_c", "ctx_one");
2544 r.record_owner("tsk_c", "bob");
2545 r.request_cancel("tsk_c");
2546 let alice = |task_id: &str| r.owner(task_id) == Some("alice");
2547 let page = r.list_tasks(&every_task(), None, 10, alice);
2548 assert_eq!((listed(&page), page.total), (vec!["tsk_b", "tsk_a"], 2));
2549 let in_one = TaskFilter {
2550 context_id: Some("ctx_one".to_string()),
2551 state: StateFilter::Any,
2552 status_after: None,
2553 };
2554 let page = r.list_tasks(&in_one, None, 10, |_| true);
2555 assert_eq!((listed(&page), page.total), (vec!["tsk_c", "tsk_a"], 2));
2556 let canceled = TaskFilter {
2557 context_id: None,
2558 state: StateFilter::Only(TaskState::Canceled),
2559 status_after: None,
2560 };
2561 let page = r.list_tasks(&canceled, None, 10, |_| true);
2562 assert_eq!(listed(&page), ["tsk_c"]);
2563 let page = r.list_tasks(&canceled, None, 10, alice);
2564 assert_eq!((page.tasks.len(), page.total, page.next), (0, 0, None));
2565 let nothing = TaskFilter {
2566 context_id: None,
2567 state: StateFilter::Nothing,
2568 status_after: None,
2569 };
2570 assert_eq!(r.list_tasks(¬hing, None, 10, |_| true).total, 0);
2571 }
2572
2573 #[test]
2574 fn list_cursor_reads_only_what_it_wrote() {
2575 let cursor = ListCursor {
2576 revision: 42,
2577 task_id: "tsk_01a1".to_string(),
2578 };
2579 let token = cursor.encode();
2580 assert!(!token.contains('='), "{token}");
2581 assert_eq!(ListCursor::decode(&token), Some(cursor));
2582 use base64::Engine as _;
2583 let encode = |text: &str| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(text);
2584 for garbage in [
2585 "not-a-token".to_string(),
2586 "".to_string(),
2587 "!!".to_string(),
2588 encode("42"),
2589 encode("42:"),
2590 encode("x:tsk_1"),
2591 encode("-1:tsk_1"),
2592 encode("+1:tsk_1"),
2593 ] {
2594 assert_eq!(ListCursor::decode(&garbage), None, "{garbage}");
2595 }
2596 }
2597
2598 #[test]
2599 fn list_state_filter_reads_every_v1_state_name() {
2600 for state in TaskState::ALL {
2601 assert_eq!(
2602 TaskState::from_wire_name(state.wire_name()),
2603 Some(state.clone())
2604 );
2605 assert_eq!(
2606 StateFilter::from_wire_name(state.wire_name()),
2607 Some(StateFilter::Only(state))
2608 );
2609 }
2610 assert_eq!(
2611 StateFilter::from_wire_name("TASK_STATE_UNSPECIFIED"),
2612 Some(StateFilter::Any)
2613 );
2614 assert_eq!(
2615 StateFilter::from_wire_name("TASK_STATE_AUTH_REQUIRED"),
2616 Some(StateFilter::Nothing)
2617 );
2618 for other in ["done", "completed", "task_state_completed", ""] {
2619 assert_eq!(StateFilter::from_wire_name(other), None, "{other}");
2620 assert_eq!(TaskState::from_wire_name(other), None, "{other}");
2621 }
2622 }
2623
2624 #[test]
2625 fn request_cancel_on_a_running_task_records_canceled() {
2626 let mut r = running_registry("tsk_001");
2627 let signal = r.cancel_watch("tsk_001");
2628 assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::Accepted);
2629 assert!(signal.is_canceled());
2630 assert!(r.is_canceled("tsk_001"));
2631 let task = r.get_task("tsk_001").unwrap();
2632 assert_eq!(task.status.state, TaskState::Canceled);
2633 }
2634
2635 #[test]
2636 fn request_cancel_on_a_completed_task_leaves_it_alone() {
2637 let mut r = running_registry("tsk_001");
2638 r.finish_task(TaskState::Completed);
2639 assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::AlreadyTerminal);
2640 assert_eq!(
2641 r.get_task("tsk_001").unwrap().status.state,
2642 TaskState::Completed
2643 );
2644 let mut r = running_registry("tsk_002");
2646 assert_eq!(r.request_cancel("tsk_002"), CancelOutcome::Accepted);
2647 assert_eq!(r.request_cancel("tsk_002"), CancelOutcome::AlreadyTerminal);
2648 }
2649
2650 #[test]
2651 fn cancel_every_live_cancels_live_tasks_in_order_and_leaves_terminal_ones() {
2652 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2653 r.enqueue("tsk_c", "ctx_001");
2654 r.enqueue("tsk_a", "ctx_001");
2655 r.enqueue("tsk_done", "ctx_001");
2656 r.start_task("tsk_done".to_string(), "ctx_001".to_string(), TaskLane::Bg);
2657 r.finish_task(TaskState::Completed);
2658 r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::Bg);
2659 let running = r.cancel_watch("tsk_a");
2660
2661 assert_eq!(r.cancel_every_live(false), vec!["tsk_a", "tsk_c"]);
2662 assert!(running.is_canceled());
2663 assert!(r.is_canceled("tsk_c"));
2664 assert_eq!(
2665 r.get_task("tsk_done").unwrap().status.state,
2666 TaskState::Completed
2667 );
2668 assert!(r.cancel_every_live(false).is_empty());
2669 }
2670
2671 #[test]
2674 fn cancel_every_live_at_the_deadline_spares_a_task_holding_a_standing_answer() {
2675 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2676 r.enqueue("tsk_a", "ctx_001");
2677 r.enqueue("tsk_b", "ctx_001");
2678 r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::Bg);
2679 let running = r.cancel_watch("tsk_a");
2680 r.mark_standing_answer("tsk_a");
2681
2682 assert_eq!(r.cancel_every_live(true), vec!["tsk_b"]);
2683 assert!(running.is_canceled());
2684 assert!(!r.is_canceled("tsk_a"));
2685 assert!(!r.get_task("tsk_a").unwrap().status.state.is_terminal());
2686 assert!(r.stopped_at_deadline("tsk_a"));
2687 assert!(
2688 r.is_canceled("tsk_b"),
2689 "an unmarked task is cancelled at once"
2690 );
2691
2692 assert_eq!(
2693 r.finish_task(TaskState::Completed),
2694 Some(TaskState::Completed)
2695 );
2696 assert_eq!(
2697 r.get_task("tsk_a").unwrap().status.state,
2698 TaskState::Completed
2699 );
2700 assert!(
2701 !r.stopped_at_deadline("tsk_a"),
2702 "finish_task clears the mark"
2703 );
2704 }
2705
2706 #[test]
2708 fn cancel_every_live_without_sparing_cancels_a_task_holding_a_standing_answer() {
2709 let mut r = running_registry("tsk_001");
2710 r.mark_standing_answer("tsk_001");
2711 assert_eq!(r.cancel_every_live(false), vec!["tsk_001"]);
2712 assert!(r.is_canceled("tsk_001"));
2713 assert!(!r.stopped_at_deadline("tsk_001"));
2714 assert_eq!(
2715 r.finish_task(TaskState::Completed),
2716 Some(TaskState::Canceled)
2717 );
2718 }
2719
2720 #[test]
2722 fn cancel_every_live_at_the_deadline_cancels_a_task_whose_mark_was_cleared() {
2723 let mut r = running_registry("tsk_001");
2724 r.mark_standing_answer("tsk_001");
2725 r.clear_standing_answer("tsk_001");
2726 assert_eq!(r.cancel_every_live(true), vec!["tsk_001"]);
2727 assert!(r.is_canceled("tsk_001"));
2728 assert!(!r.stopped_at_deadline("tsk_001"));
2729 }
2730
2731 #[test]
2732 fn request_cancel_on_an_unknown_id_is_unknown() {
2733 let mut r = make_registry();
2734 assert_eq!(r.request_cancel("tsk_doesnotexist"), CancelOutcome::Unknown);
2735 assert!(!r.is_canceled("tsk_doesnotexist"));
2736 }
2737
2738 #[test]
2739 fn finish_task_does_not_overwrite_an_accepted_cancel() {
2740 let mut r = running_registry("tsk_001");
2741 assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::Accepted);
2742 assert_eq!(
2743 r.finish_task(TaskState::Completed),
2744 Some(TaskState::Canceled),
2745 "the state recorded is the accepted cancel, and the caller is told so"
2746 );
2747 assert_eq!(
2748 r.get_task("tsk_001").unwrap().status.state,
2749 TaskState::Canceled
2750 );
2751 assert_eq!(
2752 r.finish_task(TaskState::Failed),
2753 None,
2754 "a slot that is no longer running records nothing"
2755 );
2756 assert_eq!(
2757 r.get_task("tsk_001").unwrap().status.state,
2758 TaskState::Canceled
2759 );
2760 }
2761
2762 #[test]
2763 fn finish_task_returns_the_state_it_recorded() {
2764 let mut r = running_registry("tsk_001");
2765 assert_eq!(
2766 r.finish_task(TaskState::Completed),
2767 Some(TaskState::Completed)
2768 );
2769 assert_eq!(make_registry().finish_task(TaskState::Failed), None);
2770 }
2771
2772 #[test]
2773 fn an_input_timeout_leaves_the_task_working_and_is_read_once() {
2774 let mut r = running_registry("tsk_001");
2775 let (tx, _rx) = oneshot::channel();
2776 r.set_input_required("tsk_001", "prompt".into(), tx)
2777 .unwrap();
2778 r.record_input_timeout("tsk_001");
2779 assert_eq!(
2780 r.get_task("tsk_001").unwrap().status.state,
2781 TaskState::Working,
2782 "the task's terminal state is not the wait's to record"
2783 );
2784 assert!(
2785 r.get_input_prompt("tsk_001").is_none(),
2786 "the waiter is gone"
2787 );
2788 assert!(r.take_input_timeout("tsk_001"));
2789 assert!(
2790 !r.take_input_timeout("tsk_001"),
2791 "a reopened attempt does not end on the same timeout"
2792 );
2793 assert!(!r.take_input_timeout("tsk_other"));
2794 }
2795
2796 #[test]
2797 fn cancelling_a_queued_task_frees_its_queue_slot() {
2798 let mut r = TaskRegistry::new(2, TaskAcceptance::Queue);
2799 r.enqueue("tsk_a", "ctx_001");
2800 r.enqueue("tsk_b", "ctx_001");
2801 assert!(!r.can_accept(), "the queue is full");
2802 assert_eq!(r.request_cancel("tsk_b"), CancelOutcome::Accepted);
2803 assert!(r.can_accept(), "a cancelled task holds no slot");
2804 }
2805
2806 #[test]
2807 fn close_to_new_work_rejects_every_submitted_task_and_nothing_else() {
2808 let mut r = TaskRegistry::new(4, TaskAcceptance::Queue);
2809 for id in ["tsk_a", "tsk_b", "tsk_c", "tsk_d"] {
2810 r.enqueue(id, &format!("ctx_{id}"));
2811 }
2812 r.start_task("tsk_a".to_string(), "ctx_tsk_a".to_string(), TaskLane::Bg);
2813 let (tx, _rx) = oneshot::channel();
2814 r.set_input_required("tsk_a", "prompt".into(), tx).unwrap();
2815 assert_eq!(r.request_cancel("tsk_c"), CancelOutcome::Accepted);
2816 let pending_before = r.pending_count;
2817
2818 let refused = r.close_to_new_work();
2819
2820 assert_eq!(
2821 refused,
2822 vec![
2823 ("tsk_b".to_string(), "ctx_tsk_b".to_string()),
2824 ("tsk_d".to_string(), "ctx_tsk_d".to_string()),
2825 ]
2826 );
2827 for id in ["tsk_b", "tsk_d"] {
2828 assert_eq!(r.get_task(id).unwrap().status.state, TaskState::Rejected);
2829 }
2830 assert_eq!(r.pending_count, pending_before - 2);
2831 assert_eq!(r.pending_count, 0);
2832 assert_eq!(
2833 r.get_task("tsk_a").unwrap().status.state,
2834 TaskState::InputRequired,
2835 "a live task that is not submitted is left alone"
2836 );
2837 assert_eq!(
2838 r.get_task("tsk_c").unwrap().status.state,
2839 TaskState::Canceled
2840 );
2841 assert_eq!(r.request_cancel("tsk_b"), CancelOutcome::AlreadyTerminal);
2842 assert!(!r.is_canceled("tsk_b"));
2843 }
2844
2845 #[test]
2846 fn a_closed_registry_accepts_nothing_and_closes_once() {
2847 for (depth, mode) in [(1, TaskAcceptance::Single), (4, TaskAcceptance::Queue)] {
2848 let mut r = TaskRegistry::new(depth, mode.clone());
2849 assert!(r.can_accept());
2850 assert!(!r.is_closed());
2851 r.enqueue("tsk_a", "ctx_001");
2852 assert_eq!(r.close_to_new_work().len(), 1);
2853 assert!(r.is_closed());
2854 assert!(!r.can_accept(), "{mode:?}: a closed registry refuses work");
2855 assert!(r.close_to_new_work().is_empty(), "{mode:?}: closes once");
2856 assert!(!r.can_accept());
2857 }
2858 }
2859
2860 #[test]
2865 fn tasks_held_at_once_matches_what_the_registry_accepts() {
2866 const SAFETY_CAP: usize = 64;
2867 for (mode, depth, expected) in [
2868 (TaskAcceptance::None, 1, 0),
2869 (TaskAcceptance::Single, 1, 1),
2870 (TaskAcceptance::Queue, 1, 2),
2871 (TaskAcceptance::Queue, 3, 4),
2872 ] {
2873 let mut r = TaskRegistry::new(depth, mode.clone());
2874 let mut accepted = 0;
2875 while r.can_accept() && accepted < SAFETY_CAP {
2876 let task_id = format!("tsk_{accepted}");
2877 r.enqueue(&task_id, "ctx_001");
2878 if accepted == 0 {
2879 r.start_task(task_id, "ctx_001".to_string(), TaskLane::Peer);
2880 }
2881 accepted += 1;
2882 }
2883 let lifecycle = murmur_artifact::LifecycleConfig {
2884 task_acceptance: mode.clone(),
2885 queue_depth: depth,
2886 ..Default::default()
2887 };
2888 assert_eq!(accepted, expected, "{mode:?} at depth {depth}");
2889 assert_eq!(
2890 accepted,
2891 lifecycle.tasks_held_at_once(),
2892 "{mode:?} at depth {depth}: tasks_held_at_once drifted from the registry"
2893 );
2894 }
2895 }
2896
2897 #[test]
2898 fn task_state_serializes_as_its_wire_name_and_reads_only_that() {
2899 for state in TaskState::ALL {
2900 let wire = serde_json::to_value(&state).unwrap();
2901 assert_eq!(wire, serde_json::json!(state.wire_name()));
2902 assert_eq!(
2903 serde_json::from_value::<TaskState>(wire).unwrap(),
2904 state,
2905 "{state:?}"
2906 );
2907 assert!(
2908 serde_json::from_value::<TaskState>(serde_json::json!(state.as_str())).is_err(),
2909 "{state:?}: murmur's word is not a wire state"
2910 );
2911 }
2912 for refused in [
2913 serde_json::json!("TASK_STATE_UNSPECIFIED"),
2914 serde_json::json!("TASK_STATE_AUTH_REQUIRED"),
2915 serde_json::json!("task_state_working"),
2916 serde_json::json!(1),
2917 serde_json::json!(null),
2918 ] {
2919 assert!(
2920 serde_json::from_value::<TaskState>(refused.clone()).is_err(),
2921 "{refused}"
2922 );
2923 }
2924 }
2925
2926 #[test]
2927 fn task_state_as_str_is_murmurs_word() {
2928 let words: Vec<&str> = TaskState::ALL.iter().map(TaskState::as_str).collect();
2929 assert_eq!(
2930 words,
2931 [
2932 "submitted",
2933 "working",
2934 "completed",
2935 "failed",
2936 "canceled",
2937 "input-required",
2938 "rejected"
2939 ]
2940 );
2941 }
2942
2943 #[test]
2944 fn murmur_state_name_maps_the_eight_v1_states_and_nothing_else() {
2945 for state in TaskState::ALL {
2946 assert_eq!(murmur_state_name(state.wire_name()), Some(state.as_str()));
2947 }
2948 assert_eq!(
2949 murmur_state_name("TASK_STATE_AUTH_REQUIRED"),
2950 Some("auth-required")
2951 );
2952 for other in [
2953 "TASK_STATE_UNSPECIFIED",
2954 "working",
2955 "",
2956 "TASK_STATE_working",
2957 ] {
2958 assert_eq!(murmur_state_name(other), None, "{other:?}");
2959 }
2960 }
2961
2962 #[test]
2963 fn roles_serialize_as_their_proto_names() {
2964 assert_eq!(
2965 serde_json::to_value(Role::User).unwrap(),
2966 serde_json::json!("ROLE_USER")
2967 );
2968 assert_eq!(
2969 serde_json::to_value(Role::Agent).unwrap(),
2970 serde_json::json!("ROLE_AGENT")
2971 );
2972 assert!(serde_json::from_value::<Role>(serde_json::json!("agent")).is_err());
2973 }
2974
2975 #[test]
2976 fn finish_task_cleans_up_input_waiters() {
2977 let mut r = running_registry("tsk_001");
2978 let (tx, _rx) = oneshot::channel();
2979 r.set_input_required("tsk_001", "prompt".into(), tx)
2980 .unwrap();
2981 r.finish_task(TaskState::Failed);
2982 assert!(r.get_input_prompt("tsk_001").is_none());
2984 }
2985
2986 #[test]
2987 fn a2a_error_table_is_the_spec_table_in_code_order() {
2988 let table: Vec<(i32, &str)> = A2aError::ALL
2989 .into_iter()
2990 .map(|error| (error.code(), error.reason()))
2991 .collect();
2992 assert_eq!(
2993 table,
2994 [
2995 (-32001, "TASK_NOT_FOUND"),
2996 (-32002, "TASK_NOT_CANCELABLE"),
2997 (-32003, "PUSH_NOTIFICATION_NOT_SUPPORTED"),
2998 (-32004, "UNSUPPORTED_OPERATION"),
2999 (-32005, "CONTENT_TYPE_NOT_SUPPORTED"),
3000 (-32006, "INVALID_AGENT_RESPONSE"),
3001 (-32007, "EXTENDED_AGENT_CARD_NOT_CONFIGURED"),
3002 (-32008, "EXTENSION_SUPPORT_REQUIRED"),
3003 (-32009, "VERSION_NOT_SUPPORTED"),
3004 ]
3005 );
3006 }
3007
3008 #[test]
3009 fn a2a_error_from_code_round_trips_and_knows_no_other_code() {
3010 for error in A2aError::ALL {
3011 assert_eq!(A2aError::from_code(error.code()), Some(error));
3012 }
3013 for code in [-32000, -32010, -32099, -32600, -31001, 0] {
3014 assert_eq!(A2aError::from_code(code), None, "{code}");
3015 }
3016 }
3017
3018 #[test]
3019 fn a2a_murmur_errors_lie_outside_the_reserved_range() {
3020 let table: Vec<(i32, &str)> = MurmurError::ALL
3021 .into_iter()
3022 .map(|error| (error.code(), error.reason()))
3023 .collect();
3024 assert_eq!(
3025 table,
3026 [
3027 (-31001, "COMPLETION_MISADDRESSED"),
3028 (-31002, "COMPLETION_NOT_AWAITED")
3029 ]
3030 );
3031 for error in MurmurError::ALL {
3032 assert!(!(-32768..=-32000).contains(&error.code()), "{error:?}");
3033 }
3034 }
3035
3036 fn wire(response: &JsonRpcResponse) -> Value {
3037 serde_json::to_value(response).unwrap()
3038 }
3039
3040 #[test]
3041 fn a2a_error_carries_one_error_info_with_string_metadata() {
3042 let response = JsonRpcResponse::a2a_error(
3043 Value::from(7),
3044 A2aError::TaskNotCancelable,
3045 "Task cannot be canceled",
3046 &[("taskId", "tsk_1".into()), ("state", "completed".into())],
3047 );
3048 assert_eq!(
3049 wire(&response),
3050 serde_json::json!({
3051 "jsonrpc": "2.0",
3052 "id": 7,
3053 "error": {
3054 "code": -32002,
3055 "message": "Task cannot be canceled",
3056 "data": [{
3057 "@type": "type.googleapis.com/google.rpc.ErrorInfo",
3058 "reason": "TASK_NOT_CANCELABLE",
3059 "domain": "a2a-protocol.org",
3060 "metadata": {"taskId": "tsk_1", "state": "completed"},
3061 }],
3062 },
3063 })
3064 );
3065 }
3066
3067 #[test]
3068 fn a2a_murmur_error_is_in_the_murmur_domain() {
3069 let response = JsonRpcResponse::murmur_error(
3070 Value::from("a"),
3071 MurmurError::CompletionNotAwaited,
3072 "nobody waits",
3073 &[],
3074 );
3075 let error = &wire(&response)["error"];
3076 assert_eq!(error["code"], -31002);
3077 assert_eq!(error["data"][0]["reason"], "COMPLETION_NOT_AWAITED");
3078 assert_eq!(error["data"][0]["domain"], "murmur.nexus");
3079 assert_eq!(error["data"][0]["metadata"], serde_json::json!({}));
3080 }
3081
3082 #[test]
3083 fn a2a_standard_errors_carry_no_data() {
3084 let error = &wire(&JsonRpcResponse::err(
3085 Value::Null,
3086 -32601,
3087 "Method not found",
3088 ))["error"];
3089 assert!(error.get("data").is_none(), "{error}");
3090 }
3091
3092 fn refusal(body: &str) -> Value {
3093 wire(&JsonRpcRequest::from_body(body).expect_err(body))
3094 }
3095
3096 #[test]
3097 fn a2a_from_body_reads_a_request_and_defaults_params() {
3098 let req = JsonRpcRequest::from_body(
3099 r#"{"jsonrpc":"2.0","id":"x","method":"GetExtendedAgentCard","extra":1}"#,
3100 )
3101 .unwrap();
3102 assert_eq!(req.id, Value::from("x"));
3103 assert_eq!(req.method, "GetExtendedAgentCard");
3104 assert_eq!(req.params, serde_json::json!({}));
3105
3106 let req = JsonRpcRequest::from_body(
3107 r#"{"jsonrpc":"2.0","id":3,"method":"GetTask","params":[1]}"#,
3108 )
3109 .unwrap();
3110 assert_eq!(req.id, Value::from(3));
3111 assert_eq!(req.params, serde_json::json!([1]), "a method judges params");
3112 }
3113
3114 #[test]
3115 fn a2a_from_body_refuses_what_is_not_json() {
3116 let answer = refusal("{not json");
3117 assert_eq!(answer["id"], Value::Null);
3118 assert_eq!(answer["error"]["code"], -32700);
3119 assert_eq!(answer["error"]["message"], "Invalid JSON payload");
3120 }
3121
3122 #[test]
3123 fn a2a_from_body_refuses_what_is_not_a_json_rpc_request() {
3124 for (body, id) in [
3125 (
3126 r#"{"jsonrpc":"1.0","id":1,"method":"GetTask"}"#,
3127 Value::from(1),
3128 ),
3129 (r#"{"id":1,"method":"GetTask"}"#, Value::from(1)),
3130 (
3131 r#"{"jsonrpc":2.0,"id":1,"method":"GetTask"}"#,
3132 Value::from(1),
3133 ),
3134 (
3135 r#"[{"jsonrpc":"2.0","id":1,"method":"GetTask"}]"#,
3136 Value::Null,
3137 ),
3138 (r#""GetTask""#, Value::Null),
3139 (r#"{"jsonrpc":"2.0","id":"a","method":7}"#, Value::from("a")),
3140 (r#"{"jsonrpc":"2.0","id":"a"}"#, Value::from("a")),
3141 (
3142 r#"{"jsonrpc":"2.0","id":{"a":1},"method":"GetTask"}"#,
3143 Value::Null,
3144 ),
3145 (
3146 r#"{"jsonrpc":"2.0","id":null,"method":"GetTask"}"#,
3147 Value::Null,
3148 ),
3149 (
3150 r#"{"jsonrpc":"2.0","id":1.5,"method":"GetTask"}"#,
3151 Value::Null,
3152 ),
3153 (r#"{"jsonrpc":"2.0","method":"GetTask"}"#, Value::Null),
3154 ] {
3155 let answer = refusal(body);
3156 assert_eq!(answer["error"]["code"], -32600, "{body}");
3157 assert_eq!(
3158 answer["error"]["message"], "Request payload validation error",
3159 "{body}"
3160 );
3161 assert_eq!(answer["id"], id, "{body}");
3162 }
3163 }
3164
3165 fn versions(values: &[&str]) -> Vec<String> {
3166 values.iter().map(|value| value.to_string()).collect()
3167 }
3168
3169 #[test]
3170 fn a2a_version_accepts_1_0_with_or_without_a_patch() {
3171 for value in ["1.0", " 1.0 ", "1.0.0", "1.0.1", "1.0.17"] {
3172 assert!(accepts_a2a_version(&versions(&[value])), "{value:?}");
3173 }
3174 }
3175
3176 #[test]
3177 fn a2a_version_refuses_every_other_value_and_more_than_one_line() {
3178 for values in [
3179 &[][..],
3180 &[""],
3181 &["0.3"],
3182 &["0.3.0"],
3183 &["2.0"],
3184 &["1"],
3185 &["1.1"],
3186 &["1.0x"],
3187 &["1.0."],
3188 &["1.0.a"],
3189 &["1.00"],
3190 &["v1.0"],
3191 &["1.0", "1.0"],
3192 ] {
3193 assert!(!accepts_a2a_version(&versions(values)), "{values:?}");
3194 }
3195 }
3196
3197 #[test]
3198 fn an_extension_is_activated_by_an_exact_entry_on_any_line() {
3199 const URI: &str =
3200 "https://docs.murmur.nexus/reference/streaming-protocol/#murmur-stream-v1";
3201 let other = "https://example.com/x/v1";
3202 for values in [
3203 vec![URI.to_string()],
3204 vec![format!(" {URI} ")],
3205 vec![format!("{other}, {URI}")],
3206 vec![format!("{other},,{URI},")],
3207 vec![other.to_string(), URI.to_string()],
3208 ] {
3209 assert!(activates_extension(&values, URI), "{values:?}");
3210 }
3211 for values in [
3212 vec![],
3213 vec![String::new()],
3214 vec![other.to_string()],
3215 vec![format!("{URI}/")],
3216 vec![URI.to_uppercase()],
3217 vec![format!("{URI} {other}")],
3218 ] {
3219 assert!(!activates_extension(&values, URI), "{values:?}");
3220 }
3221 }
3222
3223 #[test]
3224 fn a2a_version_refusal_names_what_was_sent() {
3225 let answer = wire(&version_not_supported(Value::from(4), &versions(&[])));
3226 assert_eq!(answer["id"], 4);
3227 assert_eq!(answer["error"]["code"], -32009);
3228 let info = &answer["error"]["data"][0];
3229 assert_eq!(info["reason"], "VERSION_NOT_SUPPORTED");
3230 assert_eq!(info["domain"], "a2a-protocol.org");
3231 assert_eq!(
3232 info["metadata"],
3233 serde_json::json!({"requestedVersion": "", "supportedVersions": "1.0"})
3234 );
3235
3236 let answer = wire(&version_not_supported(Value::Null, &versions(&[" 0.3 "])));
3237 assert_eq!(
3238 answer["error"]["data"][0]["metadata"]["requestedVersion"],
3239 "0.3"
3240 );
3241 let answer = wire(&version_not_supported(
3242 Value::Null,
3243 &versions(&["1.0", "1.0"]),
3244 ));
3245 assert_eq!(
3246 answer["error"]["data"][0]["metadata"]["requestedVersion"],
3247 "1.0, 1.0"
3248 );
3249 }
3250
3251 #[test]
3254 fn a_part_serializes_its_one_content_field_and_reads_back() {
3255 for (part, wire) in [
3256 (
3257 Part::text("hi"),
3258 serde_json::json!({"text": "hi", "mediaType": "text/plain"}),
3259 ),
3260 (
3261 Part::data(serde_json::json!({"a": [1]})),
3262 serde_json::json!({"data": {"a": [1]}, "mediaType": "application/json"}),
3263 ),
3264 (
3265 Part::data(Value::Null),
3266 serde_json::json!({"data": null, "mediaType": "application/json"}),
3267 ),
3268 ] {
3269 assert_eq!(serde_json::to_value(&part).unwrap(), wire);
3270 assert_eq!(serde_json::from_value::<Part>(wire).unwrap(), part);
3271 }
3272 for refused in [
3273 serde_json::json!({}),
3274 serde_json::json!({"text": "a", "data": 1}),
3275 serde_json::json!({"kind": "file", "file": {"uri": "x"}}),
3276 serde_json::json!({"text": 7}),
3277 serde_json::json!("text"),
3278 ] {
3279 assert!(
3280 serde_json::from_value::<Part>(refused.clone()).is_err(),
3281 "{refused}"
3282 );
3283 }
3284 }
3285
3286 fn params(message: Value) -> Value {
3287 serde_json::json!({ "message": message })
3288 }
3289
3290 fn user(parts: Value) -> Value {
3291 serde_json::json!({"messageId": "msg_1", "role": "ROLE_USER", "parts": parts})
3292 }
3293
3294 fn invalid_params(params: &Value) -> String {
3295 match read_send_params(params) {
3296 Err(MessageRefusal::InvalidParams(message)) => message,
3297 other => panic!("expected -32602 for {params}, got {other:?}"),
3298 }
3299 }
3300
3301 #[test]
3302 fn read_send_params_reads_a_v1_message_in_full() {
3303 let message = read_send_params(¶ms(serde_json::json!({
3304 "messageId": "msg_1",
3305 "contextId": "ctx_1",
3306 "taskId": "tsk_1",
3307 "role": "ROLE_USER",
3308 "referenceTaskIds": ["tsk_a", "tsk_b", "tsk_a"],
3309 "parts": [{"text": "a"}, {"data": null, "filename": "f.json"}],
3310 "metadata": {"ignored": true},
3311 "extensions": ["https://example.com/x"],
3312 "unknown": 1,
3313 })))
3314 .unwrap();
3315 assert_eq!(message.message_id, "msg_1");
3316 assert_eq!(message.context_id.as_deref(), Some("ctx_1"));
3317 assert_eq!(message.task_id.as_deref(), Some("tsk_1"));
3318 assert_eq!(message.reference_task_ids, ["tsk_a", "tsk_b"]);
3319 assert_eq!(message.part_kinds(), ["text", "data"]);
3320 assert_eq!(message.parts[1].filename.as_deref(), Some("f.json"));
3321
3322 let message = read_send_params(¶ms(serde_json::json!({
3323 "messageId": "msg_1", "role": "ROLE_USER", "taskId": "", "parts": [{"text": "a"}],
3324 })))
3325 .unwrap();
3326 assert_eq!(message.task_id, None, "an empty taskId names no task");
3327 }
3328
3329 #[test]
3332 fn read_send_params_refuses_structure_in_order() {
3333 let text = serde_json::json!([{"text": "a"}]);
3334 for (params, expected) in [
3335 (serde_json::json!({}), "params.message"),
3336 (user(text.clone()), "params.message"),
3337 (serde_json::json!({"message": "hi"}), "params.message"),
3338 (
3339 params(serde_json::json!({"role": "ROLE_USER", "parts": text})),
3340 "messageId",
3341 ),
3342 (
3343 params(serde_json::json!({"messageId": "", "role": "ROLE_USER", "parts": text})),
3344 "messageId",
3345 ),
3346 (
3347 params(serde_json::json!({"messageId": "m", "role": "user", "parts": []})),
3348 "ROLE_USER",
3349 ),
3350 (
3351 params(serde_json::json!({"messageId": "m", "role": "ROLE_AGENT", "parts": text})),
3352 "ROLE_USER",
3353 ),
3354 (
3355 params(serde_json::json!({"messageId": "m", "role": 1, "parts": text})),
3356 "ROLE_USER",
3357 ),
3358 (
3359 params(serde_json::json!({"messageId": "m", "parts": text})),
3360 "ROLE_USER",
3361 ),
3362 (params(user(serde_json::json!([]))), "non-empty array"),
3363 (
3364 params(user(serde_json::json!({"text": "a"}))),
3365 "non-empty array",
3366 ),
3367 (
3368 params(user(serde_json::json!([{"text": "a"}, {}]))),
3369 "message.parts[1] sets none",
3370 ),
3371 (
3372 params(user(serde_json::json!([{"text": "a", "data": 1}]))),
3373 "message.parts[0] sets text and data",
3374 ),
3375 (
3376 params(user(
3377 serde_json::json!([{"kind": "file", "file": {"uri": "x"}}]),
3378 )),
3379 "message.parts[0] sets none",
3380 ),
3381 (
3382 params(user(serde_json::json!([{"url": 7}, {}]))),
3383 "message.parts[1] sets none",
3384 ),
3385 (
3386 params(user(serde_json::json!([{"text": 7}]))),
3387 "message.parts[0].text is not a string",
3388 ),
3389 (
3390 params(user(serde_json::json!([{"data": 1, "mediaType": 2}]))),
3391 "message.parts[0].mediaType",
3392 ),
3393 (
3394 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3395 "parts": [{"url": "u"}], "taskId": 7})),
3396 "message.taskId",
3397 ),
3398 (
3399 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3400 "parts": text, "contextId": ["c"]})),
3401 "message.contextId",
3402 ),
3403 (
3404 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3405 "parts": [{"raw": "aGk="}], "referenceTaskIds": [""]})),
3406 "referenceTaskIds",
3407 ),
3408 (
3409 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3410 "parts": text, "referenceTaskIds": [7]})),
3411 "referenceTaskIds",
3412 ),
3413 (
3414 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3415 "parts": text, "referenceTaskIds": "tsk_a"})),
3416 "referenceTaskIds",
3417 ),
3418 ] {
3419 let message = invalid_params(¶ms);
3420 assert!(message.contains(expected), "{params}: {message}");
3421 }
3422 }
3423
3424 #[test]
3425 fn read_send_params_caps_distinct_references() {
3426 let ids = |n: usize| -> Vec<String> { (0..n).map(|i| format!("tsk_{i}")).collect() };
3427 let with = |ids: Vec<String>| {
3428 params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3429 "parts": [{"text": "a"}], "referenceTaskIds": ids}))
3430 };
3431 let mut sixteen_twice = ids(MAX_REFERENCE_TASK_IDS);
3432 sixteen_twice.extend(ids(MAX_REFERENCE_TASK_IDS));
3433 assert_eq!(
3434 read_send_params(&with(sixteen_twice))
3435 .unwrap()
3436 .reference_task_ids,
3437 ids(MAX_REFERENCE_TASK_IDS)
3438 );
3439 let message = invalid_params(&with(ids(MAX_REFERENCE_TASK_IDS + 1)));
3440 assert!(message.contains("names 17 tasks"), "{message}");
3441 }
3442
3443 #[test]
3446 fn read_send_params_refuses_a_push_notification_config() {
3447 let message = user(serde_json::json!([{"text": "hi"}]));
3448 let asking = serde_json::json!({"message": message,
3449 "configuration": {"taskPushNotificationConfig": {"url": "https://example.com/hook"}}});
3450 assert_eq!(
3451 read_send_params(&asking),
3452 Err(MessageRefusal::PushNotificationNotSupported)
3453 );
3454 let answer = serde_json::to_value(
3455 MessageRefusal::PushNotificationNotSupported.into_response(Value::from(1)),
3456 )
3457 .unwrap();
3458 assert_eq!(answer["error"]["code"], -32003);
3459 assert_eq!(
3460 answer["error"]["message"],
3461 "push notifications are not supported by this agent"
3462 );
3463 assert_eq!(
3464 answer["error"]["data"][0],
3465 serde_json::json!({"@type": "type.googleapis.com/google.rpc.ErrorInfo",
3466 "reason": "PUSH_NOTIFICATION_NOT_SUPPORTED", "domain": "a2a-protocol.org",
3467 "metadata": {}})
3468 );
3469 for configuration in [
3470 serde_json::json!({"taskPushNotificationConfig": null}),
3471 serde_json::json!({"acceptedOutputModes": ["text/plain"], "historyLength": 2}),
3472 serde_json::json!("ignored"),
3473 ] {
3474 let params = serde_json::json!({"message": message, "configuration": configuration});
3475 assert!(read_send_params(¶ms).is_ok(), "{params}");
3476 }
3477 }
3478
3479 fn with_metadata(metadata: Value, task_id: Option<&str>) -> Value {
3481 let mut message = user(serde_json::json!([{"text": "a"}]));
3482 message["metadata"] = metadata;
3483 if let Some(task_id) = task_id {
3484 message["taskId"] = Value::from(task_id);
3485 }
3486 params(message)
3487 }
3488
3489 fn door(door: Value) -> Value {
3491 serde_json::json!({ DOOR_EXTENSION_URI: door })
3492 }
3493
3494 #[test]
3495 fn read_send_params_reads_forget_session_true() {
3496 let message = read_send_params(&with_metadata(
3497 door(serde_json::json!({"forgetSession": true})),
3498 None,
3499 ))
3500 .unwrap();
3501 assert!(message.forget_session);
3502 assert!(message.door_extension);
3503 }
3504
3505 #[test]
3506 fn read_send_params_reads_every_other_metadata_as_no_forget() {
3507 for (metadata, door_extension) in [
3508 (door(serde_json::json!({"forgetSession": false})), true),
3509 (door(serde_json::json!({})), true),
3510 (
3511 door(serde_json::json!({"other": 1, "forgetSession": false})),
3512 true,
3513 ),
3514 (serde_json::json!({}), false),
3515 (
3516 serde_json::json!({"https://example.com/x": {"forgetSession": true}}),
3517 false,
3518 ),
3519 (serde_json::json!({"forgetSession": true}), false),
3520 (serde_json::json!("not an object"), false),
3521 (
3522 serde_json::json!([door(serde_json::json!({"forgetSession": true}))]),
3523 false,
3524 ),
3525 (Value::Null, false),
3526 ] {
3527 let message = read_send_params(&with_metadata(metadata.clone(), None)).unwrap();
3528 assert!(!message.forget_session, "{metadata}");
3529 assert_eq!(message.door_extension, door_extension, "{metadata}");
3530 }
3531 let message = read_send_params(¶ms(user(serde_json::json!([{"text": "a"}])))).unwrap();
3532 assert!(
3533 !message.forget_session && !message.door_extension,
3534 "no metadata at all"
3535 );
3536 }
3537
3538 #[test]
3539 fn read_send_params_ignores_other_keys_beside_forget_session() {
3540 let message = read_send_params(&with_metadata(
3541 door(serde_json::json!({"forgetSession": true, "later": {"x": 1}})),
3542 None,
3543 ))
3544 .unwrap();
3545 assert!(message.forget_session);
3546 }
3547
3548 #[test]
3549 fn read_send_params_refuses_a_door_extension_value_that_is_not_an_object() {
3550 for value in [
3551 serde_json::json!(true),
3552 serde_json::json!("forget"),
3553 serde_json::json!([]),
3554 ] {
3555 assert_eq!(
3556 invalid_params(&with_metadata(door(value), None)),
3557 format!("message.metadata[\"{DOOR_EXTENSION_URI}\"] must be an object")
3558 );
3559 }
3560 }
3561
3562 #[test]
3563 fn read_send_params_refuses_a_forget_session_that_is_not_a_boolean() {
3564 for value in [serde_json::json!("true"), serde_json::json!(1), Value::Null] {
3565 assert_eq!(
3566 invalid_params(&with_metadata(
3567 door(serde_json::json!({"forgetSession": value})),
3568 None
3569 )),
3570 format!(
3571 "message.metadata[\"{DOOR_EXTENSION_URI}\"].forgetSession must be a boolean"
3572 )
3573 );
3574 }
3575 }
3576
3577 #[test]
3578 fn read_send_params_refuses_a_forget_on_a_message_that_continues_a_task() {
3579 assert_eq!(
3580 invalid_params(&with_metadata(
3581 door(serde_json::json!({"forgetSession": true})),
3582 Some("tsk_1")
3583 )),
3584 "forgetSession starts a new conversation, so it goes on a message that starts a task, \
3585 not on one that continues task tsk_1"
3586 );
3587 let message = read_send_params(&with_metadata(
3589 door(serde_json::json!({"forgetSession": false})),
3590 Some("tsk_1"),
3591 ))
3592 .unwrap();
3593 assert_eq!(message.task_id.as_deref(), Some("tsk_1"));
3594 assert!(!message.forget_session);
3595 }
3596
3597 #[test]
3600 fn read_send_params_checks_the_forget_field_in_order() {
3601 let mut message = with_metadata(door(Value::from(1)), None);
3603 message["message"]["referenceTaskIds"] = Value::from("tsk_a");
3604 assert!(invalid_params(&message).contains("referenceTaskIds"));
3605 let message = with_metadata(door(Value::from(1)), Some("tsk_1"));
3607 assert!(invalid_params(&message).ends_with("must be an object"));
3608 let message = with_metadata(
3610 door(serde_json::json!({"forgetSession": "yes"})),
3611 Some("tsk_1"),
3612 );
3613 assert!(invalid_params(&message).ends_with("must be a boolean"));
3614 let mut message = with_metadata(door(serde_json::json!({"forgetSession": 1})), None);
3616 message["message"]["parts"] = serde_json::json!([{"raw": "aGk="}]);
3617 assert!(invalid_params(&message).ends_with("must be a boolean"));
3618 let mut message = with_metadata(door(serde_json::json!({"forgetSession": true})), None);
3620 message["message"]["parts"] = serde_json::json!([{"raw": "aGk="}]);
3621 assert!(matches!(
3622 read_send_params(&message),
3623 Err(MessageRefusal::ContentTypeNotSupported { .. })
3624 ));
3625 let refusal = read_send_params(&with_metadata(door(Value::from(1)), None)).unwrap_err();
3627 let answer = wire(&refusal.into_response(Value::from(3)));
3628 assert_eq!(answer["error"]["code"], -32602);
3629 assert_eq!(
3630 answer["error"]["message"],
3631 format!("Invalid params: message.metadata[\"{DOOR_EXTENSION_URI}\"] must be an object")
3632 );
3633 }
3634
3635 fn configured(configuration: Value) -> Value {
3637 serde_json::json!({
3638 "message": user(serde_json::json!([{"text": "a"}])),
3639 "configuration": configuration,
3640 })
3641 }
3642
3643 #[test]
3644 fn read_send_params_reads_return_immediately_true_only_for_true() {
3645 let immediately = read_send_params(&configured(serde_json::json!({
3646 "returnImmediately": true,
3647 })))
3648 .unwrap();
3649 assert!(immediately.return_immediately);
3650 for params in [
3651 params(user(serde_json::json!([{"text": "a"}]))),
3652 configured(Value::Null),
3653 configured(serde_json::json!({})),
3654 configured(serde_json::json!({"returnImmediately": null})),
3655 configured(serde_json::json!({"returnImmediately": false})),
3656 configured(serde_json::json!({"historyLength": 2})),
3657 configured(serde_json::json!("ignored")),
3658 ] {
3659 assert!(
3660 !read_send_params(¶ms).unwrap().return_immediately,
3661 "{params}"
3662 );
3663 }
3664 }
3665
3666 #[test]
3667 fn read_send_params_refuses_a_return_immediately_that_is_not_a_boolean() {
3668 for value in [
3669 Value::from("yes"),
3670 Value::from(1),
3671 serde_json::json!({}),
3672 serde_json::json!([true]),
3673 ] {
3674 let params = configured(serde_json::json!({ "returnImmediately": value }));
3675 assert_eq!(
3676 invalid_params(¶ms),
3677 "configuration.returnImmediately must be a boolean",
3678 "{params}"
3679 );
3680 let answer = wire(
3681 &read_send_params(¶ms)
3682 .unwrap_err()
3683 .into_response(1.into()),
3684 );
3685 assert_eq!(answer["error"]["code"], -32602);
3686 assert_eq!(
3687 answer["error"]["message"],
3688 "Invalid params: configuration.returnImmediately must be a boolean"
3689 );
3690 }
3691 }
3692
3693 #[test]
3696 fn read_send_params_checks_return_immediately_in_order() {
3697 let mut message = with_metadata(door(serde_json::json!({"forgetSession": 1})), None);
3698 message["configuration"] = serde_json::json!({"returnImmediately": "yes"});
3699 assert!(invalid_params(&message).contains("forgetSession"));
3700
3701 let mut message = configured(serde_json::json!({
3702 "returnImmediately": "yes",
3703 "taskPushNotificationConfig": {"url": "https://example.com/hook"},
3704 }));
3705 message["message"]["parts"] = serde_json::json!([{"raw": "aGk="}]);
3706 assert!(invalid_params(&message).contains("returnImmediately"));
3707
3708 message["configuration"]["returnImmediately"] = Value::Bool(true);
3709 assert!(matches!(
3710 read_send_params(&message),
3711 Err(MessageRefusal::ContentTypeNotSupported { .. })
3712 ));
3713 }
3714
3715 #[test]
3717 fn read_send_params_refuses_the_first_file_part_with_content_type_not_supported() {
3718 assert_eq!(
3719 read_send_params(¶ms(user(serde_json::json!([
3720 {"text": "a"},
3721 {"url": "https://example.com/x.pdf", "mediaType": "application/pdf"},
3722 {"raw": "aGk="},
3723 ])))),
3724 Err(MessageRefusal::ContentTypeNotSupported {
3725 part_index: 1,
3726 kind: "url",
3727 media_type: Some("application/pdf".to_string()),
3728 })
3729 );
3730 let refusal =
3731 read_send_params(¶ms(user(serde_json::json!([{"raw": "aGk="}])))).unwrap_err();
3732 let answer = wire(&refusal.into_response(Value::from(3)));
3733 assert_eq!(answer["error"]["code"], -32005);
3734 let info = &answer["error"]["data"][0];
3735 assert_eq!(info["reason"], "CONTENT_TYPE_NOT_SUPPORTED");
3736 assert_eq!(info["metadata"], serde_json::json!({"partIndex": "0"}));
3737 invalid_params(¶ms(user(
3739 serde_json::json!([{"raw": "aGk="}, {"text": 1}]),
3740 )));
3741 }
3742
3743 fn rendered(parts: Value) -> String {
3744 read_send_params(¶ms(user(parts))).unwrap().agent_text()
3745 }
3746
3747 #[test]
3748 fn agent_text_of_text_parts_is_the_text_joined_by_newlines() {
3749 assert_eq!(
3750 rendered(
3751 serde_json::json!([{"text": "one"}, {"text": "two", "mediaType": "text/markdown"}])
3752 ),
3753 "one\ntwo"
3754 );
3755 }
3756
3757 #[test]
3758 fn agent_text_fences_a_data_part_beyond_its_longest_backtick_run() {
3759 assert_eq!(
3760 rendered(serde_json::json!([{"text": "summarise"}, {"data": {"rows": [1, 2]}}])),
3761 "summarise\n```data\n{\n \"rows\": [\n 1,\n 2\n ]\n}\n```"
3762 );
3763 let three =
3764 rendered(serde_json::json!([{"data": "a ``` b", "mediaType": "application/json"}]));
3765 assert_eq!(
3766 three,
3767 "````data media-type=application/json\n\"a ``` b\"\n````"
3768 );
3769 let five = rendered(serde_json::json!([{"data": {"x": "`````"}}]));
3770 assert!(five.starts_with("``````data\n"), "{five}");
3771 assert!(five.ends_with("\n``````"), "{five}");
3772 assert_eq!(
3773 rendered(serde_json::json!([{"data": null}])),
3774 "```data\nnull\n```"
3775 );
3776 }
3777
3778 #[test]
3779 fn agent_text_sanitises_the_fence_label() {
3780 assert_eq!(
3781 rendered(serde_json::json!([{"data": 1, "mediaType": "a b=`c",
3782 "filename": "my\tfile.json"}])),
3783 "```data media-type=a_b__c filename=my_file.json\n1\n```"
3784 );
3785 }
3786
3787 #[test]
3788 fn a_reference_block_names_each_task_as_it_stands() {
3789 let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
3790 for id in ["tsk_a", "tsk_b", "tsk_c", "tsk_e"] {
3791 r.enqueue(id, "ctx_001");
3792 }
3793 r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::Bg);
3794 r.finish_task(TaskState::Completed);
3795 r.record_ending("tsk_a", "done", Some("four ```"), false, &[], &[]);
3796 r.start_task("tsk_b".to_string(), "ctx_001".to_string(), TaskLane::Bg);
3797 r.finish_task(TaskState::Failed);
3798 r.record_ending("tsk_b", "the driver\nfailed", None, false, &[], &[]);
3799 r.start_task("tsk_c".to_string(), "ctx_001".to_string(), TaskLane::Bg);
3800 r.record_owner("tsk_e", "member:reviewer");
3801 let ids: Vec<String> = ["tsk_a", "tsk_b", "tsk_c", "tsk_d", "tsk_e"]
3802 .map(str::to_string)
3803 .to_vec();
3804
3805 assert_eq!(
3806 r.reference_block(&ids, |_| true).unwrap(),
3807 "Referenced tasks:\n\
3808 - tsk_a: completed\n````response\nfour ```\n````\n\
3809 - tsk_b: failed: the driver failed\n\
3810 - tsk_c: working\n\
3811 - tsk_d: not a task this capsule holds\n\
3812 - tsk_e: submitted"
3813 );
3814 let member = |task_id: &str| r.owner(task_id) == Some("member:planner");
3815 let block = r.reference_block(&ids[4..], member).unwrap();
3816 assert_eq!(
3817 block,
3818 "Referenced tasks:\n- tsk_e: not a task this capsule holds"
3819 );
3820 assert_eq!(r.reference_block(&[], |_| true), None);
3821 }
3822
3823 #[test]
3824 fn user_message_is_a_v1_message_from_the_user() {
3825 assert_eq!(
3826 user_message("msg_1", Some("ctx_1"), "hello"),
3827 serde_json::json!({
3828 "messageId": "msg_1",
3829 "contextId": "ctx_1",
3830 "role": "ROLE_USER",
3831 "parts": [{"text": "hello", "mediaType": "text/plain"}],
3832 })
3833 );
3834 assert!(user_message("msg_1", None, "hello")
3835 .get("contextId")
3836 .is_none());
3837 assert!(read_send_params(¶ms(user_message("msg_1", None, "hello"))).is_ok());
3838 }
3839
3840 #[test]
3841 fn send_message_params_answer_at_acceptance() {
3842 let message = user_message("msg_1", None, "hello");
3843 let params = send_message_params(message.clone());
3844 assert_eq!(
3845 params,
3846 serde_json::json!({
3847 "message": message,
3848 "configuration": {"returnImmediately": true},
3849 })
3850 );
3851 assert!(read_send_params(¶ms).unwrap().return_immediately);
3852 }
3853
3854 #[test]
3855 fn send_message_result_is_exactly_one_member_of_the_oneof() {
3856 let task = serde_json::json!({"id": "tsk_1", "contextId": "c",
3857 "status": {"state": "TASK_STATE_SUBMITTED"}});
3858 assert!(matches!(
3859 SendMessageResult::from_result(&serde_json::json!({ "task": task })),
3860 Ok(SendMessageResult::Task(task)) if task.id == "tsk_1"
3861 ));
3862 assert!(matches!(
3863 SendMessageResult::from_result(&serde_json::json!({"message": {"messageId": "m"}})),
3864 Ok(SendMessageResult::Message(_))
3865 ));
3866 for refused in [
3867 serde_json::json!({}),
3868 serde_json::json!({"task": task, "message": {}}),
3869 task.clone(),
3870 serde_json::json!({"task": {"id": "tsk_1", "contextId": "c",
3871 "status": {"state": "submitted"}}}),
3872 ] {
3873 assert!(
3874 SendMessageResult::from_result(&refused).is_err(),
3875 "{refused}"
3876 );
3877 }
3878 }
3879}