Skip to main content

capsule_runtime/
a2a.rs

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
19// ── A2A protocol version ──────────────────────────────────────────────────────
20
21/// The HTTP header an A2A request names its protocol version in. Matched case-insensitively.
22pub const A2A_VERSION_HEADER: &str = "A2A-Version";
23
24/// The one A2A protocol version the door speaks, and the value every murmur client sends in
25/// [`A2A_VERSION_HEADER`].
26pub const A2A_PROTOCOL_VERSION: &str = "1.0";
27
28/// Whether a request whose [`A2A_VERSION_HEADER`] lines carried `values`, in order, speaks a
29/// version the door serves.
30///
31/// Exactly one line is accepted, holding [`A2A_PROTOCOL_VERSION`] or that version with a patch
32/// number (`1.0.1`), surrounding whitespace aside: a patch number takes no part in negotiation.
33/// No line, an empty value — which A2A reads as 0.3 — and more than one line are refused.
34pub(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
46/// The `VersionNotSupportedError` answering a request whose [`A2A_VERSION_HEADER`] lines carried
47/// `values`. `requestedVersion` is the values as sent, joined with `, `, and `""` when there were
48/// none.
49pub(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
68// ── A2A extensions ────────────────────────────────────────────────────────────
69
70/// The HTTP header a client activates A2A extensions in, and the header a response lists the
71/// extensions it activated in. Matched case-insensitively.
72pub const A2A_EXTENSIONS_HEADER: &str = "A2A-Extensions";
73
74/// Whether the [`A2A_EXTENSIONS_HEADER`] lines a request carried, `values`, activate `uri`.
75///
76/// Each line is a comma-separated list: every entry is trimmed, empty entries are dropped, and
77/// `uri` is activated when one entry equals it exactly. Any other URI is ignored, as an agent
78/// ignores a requested extension it does not support.
79pub(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
88/// The [`A2A_EXTENSIONS_HEADER`] line of a response that used `uris`, in that order, ending in
89/// `\r\n`; empty when it used none.
90pub(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
97// ── JSON-RPC 2.0 envelope types ───────────────────────────────────────────────
98
99/// `-32700`: the body is not JSON.
100pub(crate) const PARSE_ERROR: i32 = -32700;
101/// `-32600`: the body is JSON but not a JSON-RPC 2.0 request.
102pub(crate) const INVALID_REQUEST: i32 = -32600;
103/// `-32601`: the door serves no method of this name.
104pub(crate) const METHOD_NOT_FOUND: i32 = -32601;
105/// `-32602`: the method's parameters are not what it takes.
106pub(crate) const INVALID_PARAMS: i32 = -32602;
107/// `-32603`: the door failed to answer a request it accepted.
108pub(crate) const INTERNAL_ERROR: i32 = -32603;
109
110/// A JSON-RPC 2.0 request, as [`JsonRpcRequest::from_body`] accepts it. Its `jsonrpc` was
111/// `"2.0"`, which is the only version there is to carry.
112#[derive(Debug, Clone)]
113pub(crate) struct JsonRpcRequest {
114    /// A string or an integer.
115    pub id: Value,
116    pub method: String,
117    /// The request's `params` as sent, or `{}` when it sent none. Not necessarily an object: a
118    /// method refuses any other value with `-32602` itself.
119    pub params: Value,
120}
121
122impl JsonRpcRequest {
123    /// The request `body` carries, or the error that answers it.
124    ///
125    /// A body that is not JSON is `-32700` with `id: null`. A body that is JSON but not a JSON-RPC
126    /// 2.0 request is `-32600`: one that is not an object (a batch among them), whose `jsonrpc` is
127    /// not exactly `"2.0"`, whose `method` is not a string, or whose `id` is absent or neither a
128    /// string nor an integer. That answer echoes the request's `id` when the `id` itself is valid,
129    /// and is `null` otherwise. An omitted `params` reads as `{}`; members beyond the four are
130    /// ignored.
131    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    /// Boxed so a response is small enough to travel in a `Result`'s error variant, which is how
171    /// [`JsonRpcRequest::from_body`] answers a request it refuses.
172    #[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    /// A JSON-RPC error with no `data`: the standard codes, `-32700` to `-32603`.
187    pub(crate) fn err(id: Value, code: i32, message: &str) -> Self {
188        Self::error(id, code, message, None)
189    }
190
191    /// The A2A error `error`, carrying a `google.rpc.ErrorInfo` in the `a2a-protocol.org` domain
192    /// whose `metadata` is `metadata`.
193    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    /// The murmur error `error`, carrying a `google.rpc.ErrorInfo` in the [`MURMUR_ERROR_DOMAIN`]
204    /// whose `metadata` is `metadata`.
205    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    /// [`Self::into_http_response`], listing `extensions` in [`A2A_EXTENSIONS_HEADER`] when the
233    /// response is a `result`. An error echoes nothing: a refused request used no extension.
234    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    /// One `google.rpc.ErrorInfo` for an A2A or murmur error, and absent for a standard one.
254    #[serde(skip_serializing_if = "Option::is_none")]
255    pub data: Option<Vec<Value>>,
256}
257
258/// The `@type` of the one detail an A2A or murmur error's `data` carries.
259const ERROR_INFO_TYPE: &str = "type.googleapis.com/google.rpc.ErrorInfo";
260
261/// The `ErrorInfo` `domain` of an A2A error.
262pub(crate) const A2A_ERROR_DOMAIN: &str = "a2a-protocol.org";
263
264/// The `ErrorInfo` `domain` of a [`MurmurError`].
265pub(crate) const MURMUR_ERROR_DOMAIN: &str = "murmur.nexus";
266
267/// A `google.rpc.ErrorInfo` detail. `metadata` holds string values only, as `ErrorInfo` requires.
268fn 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/// The A2A errors, A2A v1.0 §5.4, in code order.
282///
283/// The door answers six of them: [`Self::TaskNotFound`], [`Self::TaskNotCancelable`],
284/// [`Self::PushNotificationNotSupported`], [`Self::UnsupportedOperation`],
285/// [`Self::ContentTypeNotSupported`] and [`Self::VersionNotSupported`]. The others are here so a
286/// client of a door names any error it is answered with through [`Self::from_code`].
287#[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    /// Every A2A error, in code order.
302    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    /// The JSON-RPC `error.code`.
315    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    /// The `ErrorInfo` `reason`: the spec's error name in UPPER_SNAKE_CASE without `Error`.
330    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    /// The A2A error whose code is `code`, or `None` for a code A2A does not assign.
345    pub fn from_code(code: i32) -> Option<A2aError> {
346        Self::ALL.into_iter().find(|error| error.code() == code)
347    }
348}
349
350/// The errors a door answers a completion with: murmur's own protocol riding on `SendMessage`
351/// with `x-murmur-task-origin: completion`, so none of them is an A2A error.
352///
353/// Their codes lie outside the range JSON-RPC 2.0 reserves, which leaves them to applications,
354/// and the door extension documents them.
355#[derive(Debug, Clone, Copy, PartialEq, Eq)]
356pub enum MurmurError {
357    /// The completion's `x-murmur-completion-session` is not the session running here.
358    CompletionMisaddressed,
359    /// The completion names no delegation, names one no task here waits for, or names one this
360    /// session is ending.
361    CompletionNotAwaited,
362}
363
364impl MurmurError {
365    /// Every murmur error, in code order.
366    pub const ALL: [MurmurError; 2] = [
367        MurmurError::CompletionMisaddressed,
368        MurmurError::CompletionNotAwaited,
369    ];
370
371    /// The JSON-RPC `error.code`.
372    pub fn code(self) -> i32 {
373        match self {
374            MurmurError::CompletionMisaddressed => -31001,
375            MurmurError::CompletionNotAwaited => -31002,
376        }
377    }
378
379    /// The `ErrorInfo` `reason`.
380    pub fn reason(self) -> &'static str {
381        match self {
382            MurmurError::CompletionMisaddressed => "COMPLETION_MISADDRESSED",
383            MurmurError::CompletionNotAwaited => "COMPLETION_NOT_AWAITED",
384        }
385    }
386}
387
388// ── A2A protocol types ────────────────────────────────────────────────────────
389
390/// The `mediaType` of every text part murmur builds.
391pub(crate) const TEXT_MEDIA_TYPE: &str = "text/plain";
392
393/// The `mediaType` of every data part murmur builds.
394pub(crate) const DATA_MEDIA_TYPE: &str = "application/json";
395
396/// Who sent a message, A2A v1.0 `Role`, spelled as its ProtoJSON enum name.
397#[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    /// The role's ProtoJSON name, the one it serializes as.
407    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/// What one part carries: A2A v1.0 `Part`'s `content` oneof.
416#[derive(Debug, Clone, PartialEq)]
417pub(crate) enum PartContent {
418    Text(String),
419    /// Base64 file bytes, as ProtoJSON spells `bytes`. The door reads one only as an image, whose
420    /// `mediaType` is one of [`crate::images::ALLOWED_MEDIA_TYPES`], and refuses every other.
421    Raw(String),
422    /// A file by reference. The door refuses a message carrying one.
423    Url(String),
424    /// Any JSON value, `null` included.
425    Data(Value),
426}
427
428impl PartContent {
429    /// The content's field name, which is also the kind a `task_start` record lists.
430    pub(crate) fn kind(&self) -> &'static str {
431        match self {
432            Self::Text(_) => "text",
433            Self::Raw(_) => "raw",
434            Self::Url(_) => "url",
435            Self::Data(_) => "data",
436        }
437    }
438}
439
440/// The four field names of [`PartContent`], in the proto's order.
441const PART_CONTENT_FIELDS: [&str; 4] = ["text", "raw", "url", "data"];
442
443/// One A2A v1.0 `Part`.
444///
445/// Serialized as one object holding exactly one content field beside the optional `mediaType`,
446/// `filename` and `metadata`. Read through [`Part::from_json`], which refuses an object that sets
447/// no content field or more than one, so no part deserializes to nothing.
448#[derive(Debug, Clone, PartialEq)]
449pub(crate) struct Part {
450    pub content: PartContent,
451    pub media_type: Option<String>,
452    pub filename: Option<String>,
453    pub metadata: Option<Value>,
454}
455
456impl Part {
457    /// Whether this is a `raw` part whose `mediaType` is an image type the runtime accepts.
458    pub(crate) fn is_image(&self) -> bool {
459        matches!(self.content, PartContent::Raw(_))
460            && self
461                .media_type
462                .as_deref()
463                .is_some_and(crate::images::is_allowed_media_type)
464    }
465
466    /// A text part with `mediaType` [`TEXT_MEDIA_TYPE`].
467    pub(crate) fn text(text: impl Into<String>) -> Self {
468        Self::of(PartContent::Text(text.into()), TEXT_MEDIA_TYPE)
469    }
470
471    /// A data part with `mediaType` [`DATA_MEDIA_TYPE`].
472    pub(crate) fn data(value: Value) -> Self {
473        Self::of(PartContent::Data(value), DATA_MEDIA_TYPE)
474    }
475
476    fn of(content: PartContent, media_type: &str) -> Self {
477        Self {
478            content,
479            media_type: Some(media_type.to_string()),
480            filename: None,
481            metadata: None,
482        }
483    }
484
485    /// The part's text, when it is a text part.
486    pub(crate) fn as_text(&self) -> Option<&str> {
487        match &self.content {
488            PartContent::Text(text) => Some(text),
489            _ => None,
490        }
491    }
492
493    /// The part's value, when it is a data part.
494    #[cfg(test)]
495    pub(crate) fn as_data(&self) -> Option<&Value> {
496        match &self.content {
497            PartContent::Data(value) => Some(value),
498            _ => None,
499        }
500    }
501
502    /// The part as ProtoJSON.
503    pub(crate) fn to_json(&self) -> Value {
504        let mut object = serde_json::Map::new();
505        let (field, value) = match &self.content {
506            PartContent::Text(text) => ("text", Value::String(text.clone())),
507            PartContent::Raw(raw) => ("raw", Value::String(raw.clone())),
508            PartContent::Url(url) => ("url", Value::String(url.clone())),
509            PartContent::Data(value) => ("data", value.clone()),
510        };
511        object.insert(field.to_string(), value);
512        if let Some(media_type) = &self.media_type {
513            object.insert("mediaType".to_string(), Value::String(media_type.clone()));
514        }
515        if let Some(filename) = &self.filename {
516            object.insert("filename".to_string(), Value::String(filename.clone()));
517        }
518        if let Some(metadata) = &self.metadata {
519            object.insert("metadata".to_string(), metadata.clone());
520        }
521        Value::Object(object)
522    }
523
524    /// Why `value` sets no part content or more than one, or `None` when it sets exactly one.
525    /// A present `null` counts as set, as ProtoJSON reads it.
526    fn content_count_error(value: &Value) -> Option<String> {
527        let Some(object) = value.as_object() else {
528            return Some("is not an object".to_string());
529        };
530        let set: Vec<&str> = PART_CONTENT_FIELDS
531            .into_iter()
532            .filter(|field| object.contains_key(*field))
533            .collect();
534        match set.as_slice() {
535            [_] => None,
536            [] => Some("sets none of text, raw, url and data".to_string()),
537            more => Some(format!(
538                "sets {}; a part sets exactly one of text, raw, url and data",
539                more.join(" and ")
540            )),
541        }
542    }
543
544    /// The part `value` is, or why it is not one: an object setting exactly one of `text`, `raw`,
545    /// `url` and `data`, whose `text`, `raw`, `url`, `mediaType` and `filename` are strings where
546    /// present. Unknown fields are ignored.
547    pub(crate) fn from_json(value: &Value) -> Result<Part, String> {
548        if let Some(error) = Self::content_count_error(value) {
549            return Err(error);
550        }
551        let object = value.as_object().expect("checked to be an object");
552        let string = |field: &str| -> Result<Option<String>, String> {
553            match object.get(field) {
554                None => Ok(None),
555                Some(Value::String(text)) => Ok(Some(text.clone())),
556                Some(_) => Err(format!("{field} is not a string")),
557            }
558        };
559        let content = if let Some(text) = string("text")? {
560            PartContent::Text(text)
561        } else if let Some(raw) = string("raw")? {
562            PartContent::Raw(raw)
563        } else if let Some(url) = string("url")? {
564            PartContent::Url(url)
565        } else {
566            PartContent::Data(object.get("data").cloned().unwrap_or(Value::Null))
567        };
568        Ok(Part {
569            content,
570            media_type: string("mediaType")?,
571            filename: string("filename")?,
572            metadata: object.get("metadata").cloned(),
573        })
574    }
575}
576
577impl Serialize for Part {
578    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
579        self.to_json().serialize(serializer)
580    }
581}
582
583impl<'de> Deserialize<'de> for Part {
584    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
585        let value = Value::deserialize(deserializer)?;
586        Part::from_json(&value).map_err(|error| serde::de::Error::custom(format!("part {error}")))
587    }
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
591#[serde(rename_all = "camelCase")]
592pub(crate) struct A2aTask {
593    pub id: String,
594    pub context_id: String,
595    pub status: TaskStatus,
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub artifacts: Option<Vec<A2aArtifact>>,
598    /// `{"murmur": {...}}` for a terminal task with something to say: `noAnswer: true` for one
599    /// ended through `end-without-answer`, `noAnswerBelow`, the members further down that gave it
600    /// none, and `runningCommands`, the background shell commands still running when it ended.
601    /// Absent when it has none of them.
602    #[serde(default, skip_serializing_if = "Option::is_none")]
603    pub metadata: Option<serde_json::Value>,
604}
605
606impl A2aTask {
607    /// The artifact whose `artifactId` is `artifact_id`.
608    pub(crate) fn artifact(&self, artifact_id: &str) -> Option<&A2aArtifact> {
609        self.artifacts
610            .as_deref()?
611            .iter()
612            .find(|artifact| artifact.artifact_id == artifact_id)
613    }
614
615    /// The text of the status message, when the status carries one with a text part.
616    pub(crate) fn status_text(&self) -> Option<&str> {
617        self.status.message.as_ref()?.text()
618    }
619}
620
621/// An A2A v1.0 `Artifact`. Murmur names each artifact it builds once per task, so the name is
622/// also its `artifactId`: unique within the task, and the same on every read.
623#[derive(Debug, Clone, Serialize, Deserialize)]
624#[serde(rename_all = "camelCase")]
625pub(crate) struct A2aArtifact {
626    pub artifact_id: String,
627    #[serde(default)]
628    pub name: String,
629    pub parts: Vec<Part>,
630}
631
632impl A2aArtifact {
633    /// The artifact `name`, holding `parts`.
634    pub(crate) fn named(name: &str, parts: Vec<Part>) -> Self {
635        Self {
636            artifact_id: name.to_string(),
637            name: name.to_string(),
638            parts,
639        }
640    }
641
642    /// The text of the first part, when it is a text part.
643    pub(crate) fn first_text(&self) -> Option<&str> {
644        self.parts.first()?.as_text()
645    }
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize)]
649pub(crate) struct TaskStatus {
650    pub state: TaskState,
651    /// What the task's final status said, for a terminal task that said something.
652    #[serde(default, skip_serializing_if = "Option::is_none")]
653    pub message: Option<StatusMessage>,
654    /// When this status was recorded, as [`status_timestamp`] writes it. Every status murmur
655    /// writes carries one; a peer's answer may omit it.
656    #[serde(default, skip_serializing_if = "Option::is_none")]
657    pub timestamp: Option<String>,
658}
659
660impl TaskStatus {
661    /// A status in `state` with no message, recorded now.
662    pub(crate) fn of(state: TaskState) -> Self {
663        Self::at(state, None, status_now())
664    }
665
666    /// A status in `state` saying `message`, recorded at `when`.
667    pub(crate) fn at(
668        state: TaskState,
669        message: Option<StatusMessage>,
670        when: DateTime<Utc>,
671    ) -> Self {
672        Self {
673            state,
674            message,
675            timestamp: Some(status_timestamp(when)),
676        }
677    }
678}
679
680/// The current instant, truncated to the millisecond a status timestamp carries, so the string
681/// [`status_timestamp`] writes parses back to exactly the instant stored.
682pub(crate) fn status_now() -> DateTime<Utc> {
683    let now = Utc::now();
684    DateTime::from_timestamp_millis(now.timestamp_millis()).unwrap_or(now)
685}
686
687/// The instant of serialized `status`'s `timestamp`, asserting it is one [`status_timestamp`]
688/// could have written: RFC 3339, in UTC with a `Z`, to the millisecond.
689#[cfg(test)]
690pub(crate) fn assert_status_timestamp(status: &Value) -> DateTime<Utc> {
691    let timestamp = status["timestamp"]
692        .as_str()
693        .unwrap_or_else(|| panic!("no timestamp: {status}"));
694    let parsed = DateTime::parse_from_rfc3339(timestamp)
695        .unwrap_or_else(|error| panic!("{timestamp} is not RFC 3339: {error}"))
696        .with_timezone(&Utc);
697    assert_eq!(status_timestamp(parsed), timestamp, "{status}");
698    parsed
699}
700
701/// Asserts `status` is a serialized status in `state` with no message and a `timestamp`
702/// [`status_timestamp`] could have written.
703#[cfg(test)]
704pub(crate) fn assert_bare_status(status: &Value, state: &str) {
705    assert_status_timestamp(status);
706    assert_eq!(
707        *status,
708        serde_json::json!({"state": state, "timestamp": status["timestamp"]})
709    );
710}
711
712/// `at` as a status's `timestamp`: RFC 3339 in UTC with milliseconds and a `Z`, such as
713/// `2026-10-13T09:00:00.123Z`, which is what A2A v1.0's `google.protobuf.Timestamp` takes.
714pub(crate) fn status_timestamp(at: DateTime<Utc>) -> String {
715    at.to_rfc3339_opts(SecondsFormat::Millis, true)
716}
717
718/// A task status's message: an A2A message from the agent about one task, with one text part.
719#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
720#[serde(rename_all = "camelCase")]
721pub(crate) struct StatusMessage {
722    pub message_id: String,
723    pub context_id: String,
724    pub task_id: String,
725    pub role: Role,
726    pub parts: Vec<Part>,
727}
728
729impl StatusMessage {
730    /// The agent's message `text` about `task_id`, in `context_id`.
731    pub(crate) fn agent(task_id: &str, context_id: &str, text: &str) -> Self {
732        Self {
733            message_id: format!("msg_{task_id}_status"),
734            context_id: context_id.to_string(),
735            task_id: task_id.to_string(),
736            role: Role::Agent,
737            parts: vec![Part::text(text)],
738        }
739    }
740
741    /// The text of the first part, when it is a text part.
742    pub(crate) fn text(&self) -> Option<&str> {
743        self.parts.first()?.as_text()
744    }
745}
746
747/// How a finished task ended, as its final status reported it.
748#[derive(Debug, Clone, PartialEq, Eq)]
749struct TaskEnding {
750    /// The final status's message.
751    message: String,
752    /// The task's response text, kept only for a `completed` task.
753    response: Option<String>,
754    /// Whether the task ended through `end-without-answer`.
755    no_answer: bool,
756    /// `(member, status)` for each member further down that gave the task no answer.
757    no_answer_below: Vec<(String, String)>,
758    /// The background shell commands still running when the task ended, oldest first.
759    running_commands: Vec<RunningCommand>,
760}
761
762/// A background shell command still running when a task ended, as the task's
763/// `metadata.murmur.runningCommands` names it.
764#[derive(Debug, Clone, PartialEq, Eq)]
765pub(crate) struct RunningCommand {
766    pub work_id: String,
767    pub binary: String,
768    /// The command text the model wrote, without the binary name.
769    pub command: String,
770    /// From spawn to the task's end.
771    pub running_ms: u64,
772}
773
774/// The most characters of a running command's text the metadata carries, before the `…`.
775const RUNNING_COMMAND_TEXT_CHARS: usize = 200;
776
777impl TaskEnding {
778    /// The `metadata` `GetTask` carries for this ending, or `None` when it says nothing.
779    fn metadata(&self) -> Option<serde_json::Value> {
780        let mut murmur = serde_json::Map::new();
781        if self.no_answer {
782            murmur.insert("noAnswer".to_string(), serde_json::Value::Bool(true));
783        }
784        if !self.no_answer_below.is_empty() {
785            let below = self
786                .no_answer_below
787                .iter()
788                .map(|(member, status)| serde_json::json!({"member": member, "status": status}))
789                .collect();
790            murmur.insert("noAnswerBelow".to_string(), serde_json::Value::Array(below));
791        }
792        if !self.running_commands.is_empty() {
793            let running = self
794                .running_commands
795                .iter()
796                .map(|running| {
797                    let mut command: String = running
798                        .command
799                        .chars()
800                        .take(RUNNING_COMMAND_TEXT_CHARS)
801                        .collect();
802                    if running
803                        .command
804                        .chars()
805                        .nth(RUNNING_COMMAND_TEXT_CHARS)
806                        .is_some()
807                    {
808                        command.push('…');
809                    }
810                    serde_json::json!({
811                        "workId": running.work_id,
812                        "binary": running.binary,
813                        "command": command,
814                        "runningMs": running.running_ms,
815                    })
816                })
817                .collect();
818            murmur.insert(
819                "runningCommands".to_string(),
820                serde_json::Value::Array(running),
821            );
822        }
823        (!murmur.is_empty()).then(|| serde_json::json!({ "murmur": murmur }))
824    }
825}
826
827/// The name and `artifactId` of the artifact a completed task's response is carried in over
828/// `GetTask`.
829pub(crate) const RESPONSE_ARTIFACT: &str = "response";
830
831/// The name and `artifactId` of the artifact an `input-required` task's prompt is carried in.
832pub(crate) const PROMPT_ARTIFACT: &str = "prompt";
833
834/// A task's state.
835///
836/// It has two spellings. On the A2A wire it is the v1.0 ProtoJSON enum name, which serde and
837/// [`Self::wire_name`] give and which is the only spelling it deserializes from; every stream
838/// event and murmur frame carries that `TASK_STATE_*` name too. Everywhere murmur chooses its own
839/// words — every sentence written for the model, the guest's `task-result.state`, `mur` output
840/// and trace records — it is murmur's word, [`Self::as_str`].
841///
842/// The door never produces `TASK_STATE_AUTH_REQUIRED`: it authenticates a request before any task
843/// exists, so no task can wait for authentication.
844#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
845pub(crate) enum TaskState {
846    #[serde(rename = "TASK_STATE_SUBMITTED")]
847    Submitted,
848    #[serde(rename = "TASK_STATE_WORKING")]
849    Working,
850    #[serde(rename = "TASK_STATE_INPUT_REQUIRED")]
851    InputRequired,
852    #[serde(rename = "TASK_STATE_COMPLETED")]
853    Completed,
854    #[serde(rename = "TASK_STATE_FAILED")]
855    Failed,
856    #[serde(rename = "TASK_STATE_REJECTED")]
857    Rejected,
858    /// A person stopped this task. Terminal, and distinct from `Failed`: nothing went wrong, the
859    /// work was called off.
860    #[serde(rename = "TASK_STATE_CANCELED")]
861    Canceled,
862}
863
864impl TaskState {
865    /// Every state, in the proto's order.
866    pub(crate) const ALL: [TaskState; 7] = [
867        TaskState::Submitted,
868        TaskState::Working,
869        TaskState::Completed,
870        TaskState::Failed,
871        TaskState::Canceled,
872        TaskState::InputRequired,
873        TaskState::Rejected,
874    ];
875
876    /// The state's A2A v1.0 ProtoJSON name, the one it serializes as.
877    pub(crate) fn wire_name(&self) -> &'static str {
878        match self {
879            Self::Submitted => "TASK_STATE_SUBMITTED",
880            Self::Working => "TASK_STATE_WORKING",
881            Self::InputRequired => "TASK_STATE_INPUT_REQUIRED",
882            Self::Completed => "TASK_STATE_COMPLETED",
883            Self::Failed => "TASK_STATE_FAILED",
884            Self::Rejected => "TASK_STATE_REJECTED",
885            Self::Canceled => "TASK_STATE_CANCELED",
886        }
887    }
888
889    /// The state whose A2A v1.0 ProtoJSON name is `wire`, or `None` for `TASK_STATE_UNSPECIFIED`,
890    /// `TASK_STATE_AUTH_REQUIRED`, which no task here is ever in, and anything else.
891    pub(crate) fn from_wire_name(wire: &str) -> Option<TaskState> {
892        Self::ALL
893            .into_iter()
894            .find(|state| state.wire_name() == wire)
895    }
896
897    /// The state in murmur's own word, the one sentences written for the model carry:
898    /// [`TaskRegistry::reference_block`] and `call-member`'s answers. The guest's
899    /// `task-result.state` and `mur` output reach the same word from a wire name through
900    /// [`murmur_state_name`], and a test holds the two equal.
901    pub(crate) fn as_str(&self) -> &'static str {
902        match self {
903            Self::Submitted => "submitted",
904            Self::Working => "working",
905            Self::InputRequired => "input-required",
906            Self::Completed => "completed",
907            Self::Failed => "failed",
908            Self::Rejected => "rejected",
909            Self::Canceled => "canceled",
910        }
911    }
912
913    /// Whether no further work will be done on a task in this state.
914    ///
915    /// The one rule a cancel reads: a terminal task is left exactly as it is, and only a live one
916    /// can be stopped.
917    pub(crate) fn is_terminal(&self) -> bool {
918        match self {
919            Self::Submitted | Self::Working | Self::InputRequired => false,
920            Self::Completed | Self::Failed | Self::Rejected | Self::Canceled => true,
921        }
922    }
923}
924
925/// Murmur's word for the A2A v1.0 task state `wire`, or `None` for `TASK_STATE_UNSPECIFIED` and
926/// anything that is not a v1.0 state name.
927///
928/// Total over the eight meaningful v1.0 states, `TASK_STATE_AUTH_REQUIRED` among them, which is
929/// `auth-required`, A2A 0.3's word for it: a peer that is not murmur may answer it, though this
930/// door never does. This is how a state read off the wire reaches the guest and `mur` output.
931pub fn murmur_state_name(wire: &str) -> Option<&'static str> {
932    Some(match wire {
933        "TASK_STATE_SUBMITTED" => "submitted",
934        "TASK_STATE_WORKING" => "working",
935        "TASK_STATE_COMPLETED" => "completed",
936        "TASK_STATE_FAILED" => "failed",
937        "TASK_STATE_CANCELED" => "canceled",
938        "TASK_STATE_INPUT_REQUIRED" => "input-required",
939        "TASK_STATE_REJECTED" => "rejected",
940        "TASK_STATE_AUTH_REQUIRED" => "auth-required",
941        _ => return None,
942    })
943}
944
945// ── Incoming messages ─────────────────────────────────────────────────────────
946
947/// The most distinct task ids one message may name in `referenceTaskIds`.
948pub(crate) const MAX_REFERENCE_TASK_IDS: usize = 16;
949
950/// A `SendMessage` or `SendStreamingMessage` message the door read in full, as
951/// [`read_send_params`] returns it.
952#[derive(Debug, Clone, PartialEq)]
953pub(crate) struct IncomingMessage {
954    pub message_id: String,
955    pub context_id: Option<String>,
956    /// The task this message continues. An empty `taskId` reads as `None`.
957    pub task_id: Option<String>,
958    /// `referenceTaskIds` with duplicates collapsed to their first occurrence.
959    pub reference_task_ids: Vec<String>,
960    /// Text and data parts, and `raw` parts declaring an image media type: a message carrying any
961    /// other part is refused.
962    pub parts: Vec<Part>,
963    /// Whether `metadata` carries the door extension's key, [`DOOR_EXTENSION_URI`]: the message
964    /// used that extension, so a response that serves it lists the URI in
965    /// [`A2A_EXTENSIONS_HEADER`].
966    pub door_extension: bool,
967    /// `metadata[DOOR_EXTENSION_URI].forgetSession`: the task this message starts drops the
968    /// harness session its context names before its first attempt. Never `true` on a message
969    /// that names a `taskId`.
970    pub forget_session: bool,
971    /// `configuration.returnImmediately`: `SendMessage` answers once the task is accepted rather
972    /// than once it settles. `SendStreamingMessage` reads no meaning into it.
973    pub return_immediately: bool,
974}
975
976/// Why the door refused a message, and so answered no task.
977#[derive(Debug, Clone, PartialEq, Eq)]
978pub(crate) enum MessageRefusal {
979    /// `-32602`: the message is not an A2A v1.0 message. The text says what is wrong.
980    InvalidParams(String),
981    /// `-32005` `ContentTypeNotSupportedError`: the part at `part_index` is a file part, `raw` or
982    /// `url`, which this door does not read.
983    ContentTypeNotSupported {
984        part_index: usize,
985        kind: &'static str,
986        media_type: Option<String>,
987    },
988    /// `-32003` `PushNotificationNotSupportedError`: `configuration.taskPushNotificationConfig`
989    /// asks to be notified, and this agent sends no push notifications.
990    PushNotificationNotSupported,
991}
992
993impl MessageRefusal {
994    /// The JSON-RPC error answering request `id` with this refusal.
995    pub(crate) fn into_response(self, id: Value) -> JsonRpcResponse {
996        match self {
997            Self::InvalidParams(message) => {
998                JsonRpcResponse::err(id, INVALID_PARAMS, &format!("Invalid params: {message}"))
999            }
1000            Self::ContentTypeNotSupported {
1001                part_index,
1002                kind,
1003                media_type,
1004            } => {
1005                let mut metadata = vec![("partIndex", part_index.to_string())];
1006                if let Some(media_type) = &media_type {
1007                    metadata.push(("mediaType", media_type.clone()));
1008                }
1009                JsonRpcResponse::a2a_error(
1010                    id,
1011                    A2aError::ContentTypeNotSupported,
1012                    &format!(
1013                        "message.parts[{part_index}] is a {kind} part; this agent reads text parts, \
1014                         data parts and image raw parts only"
1015                    ),
1016                    &metadata,
1017                )
1018            }
1019            Self::PushNotificationNotSupported => push_notification_not_supported(id),
1020        }
1021    }
1022}
1023
1024/// The `PushNotificationNotSupportedError` answering request `id`: this agent's card says
1025/// `capabilities.pushNotifications: false`, so it neither stores a push config nor notifies.
1026pub(crate) fn push_notification_not_supported(id: Value) -> JsonRpcResponse {
1027    JsonRpcResponse::a2a_error(
1028        id,
1029        A2aError::PushNotificationNotSupported,
1030        "push notifications are not supported by this agent",
1031        &[],
1032    )
1033}
1034
1035/// The message a `SendMessage` or `SendStreamingMessage` request's `params` carries, or why it is
1036/// refused.
1037///
1038/// Read by hand rather than by serde, in this order, each failure `-32602` but the last:
1039/// `params.message` is an object; `messageId` is a non-empty string; `role` is `ROLE_USER`; `parts`
1040/// is a non-empty array; every part sets exactly one of `text`, `raw`, `url` and `data`; `text`,
1041/// `raw`, `url`, `mediaType` and `filename` are strings where present; `taskId` and `contextId`
1042/// are strings where present; `referenceTaskIds` is an array of non-empty strings naming at most
1043/// [`MAX_REFERENCE_TASK_IDS`] distinct tasks; `metadata[DOOR_EXTENSION_URI]` is an object where
1044/// present; its `forgetSession` is a boolean where present; `forgetSession: true` names no
1045/// `taskId`; `configuration.returnImmediately` is a boolean where present and not `null`. Then the
1046/// first `url` part, or `raw` part whose `mediaType` is not an image type the runtime accepts, is
1047/// [`MessageRefusal::ContentTypeNotSupported`], and a
1048/// `configuration.taskPushNotificationConfig` that is present and not `null` is
1049/// [`MessageRefusal::PushNotificationNotSupported`]. The rest of `configuration` and unknown
1050/// fields are ignored, a `configuration` that is not an object, a `metadata` that is not an object
1051/// and every other key of it and of the door extension's object among them.
1052pub(crate) fn read_send_params(params: &Value) -> Result<IncomingMessage, MessageRefusal> {
1053    let invalid = |message: String| Err(MessageRefusal::InvalidParams(message));
1054    let Some(message) = params.get("message").and_then(Value::as_object) else {
1055        return invalid("params.message must be an A2A Message object".to_string());
1056    };
1057    let message_id = match message.get("messageId") {
1058        Some(Value::String(id)) if !id.is_empty() => id.clone(),
1059        _ => return invalid("message.messageId must be a non-empty string".to_string()),
1060    };
1061    if message.get("role").and_then(Value::as_str) != Some(Role::User.wire_name()) {
1062        return invalid(format!(
1063            "message.role must be {}: a message to this agent is from the user",
1064            Role::User.wire_name()
1065        ));
1066    }
1067    let raw_parts = match message.get("parts") {
1068        Some(Value::Array(parts)) if !parts.is_empty() => parts,
1069        _ => return invalid("message.parts must be a non-empty array".to_string()),
1070    };
1071    for (index, part) in raw_parts.iter().enumerate() {
1072        if let Some(error) = Part::content_count_error(part) {
1073            return invalid(format!("message.parts[{index}] {error}"));
1074        }
1075    }
1076    let mut parts = Vec::with_capacity(raw_parts.len());
1077    for (index, part) in raw_parts.iter().enumerate() {
1078        match Part::from_json(part) {
1079            Ok(part) => parts.push(part),
1080            Err(error) => return invalid(format!("message.parts[{index}].{error}")),
1081        }
1082    }
1083    let optional_string = |field: &str| -> Result<Option<String>, MessageRefusal> {
1084        match message.get(field) {
1085            None => Ok(None),
1086            Some(Value::String(value)) => Ok(Some(value.clone())),
1087            Some(_) => Err(MessageRefusal::InvalidParams(format!(
1088                "message.{field} must be a string"
1089            ))),
1090        }
1091    };
1092    let task_id = optional_string("taskId")?.filter(|id| !id.is_empty());
1093    let context_id = optional_string("contextId")?;
1094    let reference_task_ids = match message.get("referenceTaskIds") {
1095        None => Vec::new(),
1096        Some(Value::Array(ids)) => {
1097            let mut distinct: Vec<String> = Vec::new();
1098            for id in ids {
1099                match id {
1100                    Value::String(id) if !id.is_empty() => {
1101                        if !distinct.contains(id) {
1102                            distinct.push(id.clone());
1103                        }
1104                    }
1105                    _ => {
1106                        return invalid(
1107                            "message.referenceTaskIds must hold non-empty strings".to_string(),
1108                        )
1109                    }
1110                }
1111            }
1112            if distinct.len() > MAX_REFERENCE_TASK_IDS {
1113                return invalid(format!(
1114                    "message.referenceTaskIds names {} tasks; at most {MAX_REFERENCE_TASK_IDS} \
1115                     may be referenced",
1116                    distinct.len()
1117                ));
1118            }
1119            distinct
1120        }
1121        Some(_) => return invalid("message.referenceTaskIds must be an array".to_string()),
1122    };
1123    let door_metadata = message
1124        .get("metadata")
1125        .and_then(Value::as_object)
1126        .and_then(|metadata| metadata.get(DOOR_EXTENSION_URI));
1127    let forget_session = match door_metadata {
1128        None => false,
1129        Some(Value::Object(door)) => match door.get("forgetSession") {
1130            None => false,
1131            Some(Value::Bool(forget)) => *forget,
1132            Some(_) => {
1133                return invalid(format!(
1134                    "message.metadata[\"{DOOR_EXTENSION_URI}\"].forgetSession must be a boolean"
1135                ))
1136            }
1137        },
1138        Some(_) => {
1139            return invalid(format!(
1140                "message.metadata[\"{DOOR_EXTENSION_URI}\"] must be an object"
1141            ))
1142        }
1143    };
1144    if let (true, Some(task_id)) = (forget_session, &task_id) {
1145        return invalid(format!(
1146            "forgetSession starts a new conversation, so it goes on a message that starts a task, \
1147             not on one that continues task {task_id}"
1148        ));
1149    }
1150    let configuration = params.get("configuration");
1151    let return_immediately = match configuration.and_then(|c| c.get("returnImmediately")) {
1152        None | Some(Value::Null) => false,
1153        Some(Value::Bool(immediately)) => *immediately,
1154        Some(_) => return invalid("configuration.returnImmediately must be a boolean".to_string()),
1155    };
1156    if let Some((part_index, part)) = parts.iter().enumerate().find(|(_, part)| {
1157        matches!(part.content, PartContent::Url(_))
1158            || (matches!(part.content, PartContent::Raw(_)) && !part.is_image())
1159    }) {
1160        return Err(MessageRefusal::ContentTypeNotSupported {
1161            part_index,
1162            kind: part.content.kind(),
1163            media_type: part.media_type.clone(),
1164        });
1165    }
1166    if configuration
1167        .and_then(|configuration| configuration.get("taskPushNotificationConfig"))
1168        .is_some_and(|config| !config.is_null())
1169    {
1170        return Err(MessageRefusal::PushNotificationNotSupported);
1171    }
1172    Ok(IncomingMessage {
1173        message_id,
1174        context_id,
1175        task_id,
1176        reference_task_ids,
1177        parts,
1178        door_extension: door_metadata.is_some(),
1179        forget_session,
1180        return_immediately,
1181    })
1182}
1183
1184impl IncomingMessage {
1185    /// The message as the agent reads it: each part in order, joined by `"\n"`. A text part is its
1186    /// text; a data part is its value pretty-printed in a code fence labelled `data`, with the
1187    /// part's `mediaType` and `filename` when it has them. No part's `metadata` is rendered.
1188    pub(crate) fn agent_text(&self) -> String {
1189        self.parts
1190            .iter()
1191            .filter_map(|part| {
1192                Some(match &part.content {
1193                    PartContent::Text(text) => text.clone(),
1194                    PartContent::Data(value) => {
1195                        let mut label = "data".to_string();
1196                        if let Some(media_type) = &part.media_type {
1197                            label.push_str(&format!(
1198                                " media-type={}",
1199                                fence_label_value(media_type)
1200                            ));
1201                        }
1202                        if let Some(filename) = &part.filename {
1203                            label.push_str(&format!(" filename={}", fence_label_value(filename)));
1204                        }
1205                        let body = serde_json::to_string_pretty(value).unwrap_or_default();
1206                        code_fence(&label, &body)
1207                    }
1208                    // An image reaches the agent as an image block, not as text: see
1209                    // [`message_images`].
1210                    PartContent::Raw(_) => return None,
1211                    PartContent::Url(_) => unreachable!("read_send_params refuses every url part"),
1212                })
1213            })
1214            .collect::<Vec<_>>()
1215            .join("\n")
1216    }
1217
1218    /// The kind of each part, in order: `text`, `data` or `raw`.
1219    pub(crate) fn part_kinds(&self) -> Vec<&'static str> {
1220        self.parts.iter().map(|part| part.content.kind()).collect()
1221    }
1222}
1223
1224/// Whether, and within which limits, the door takes an image part.
1225#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1226pub(crate) enum ImagePolicy {
1227    /// An image part is checked against these limits and reaches the task's first user message.
1228    Accept(murmur_artifact::ContextImagesConfig),
1229    /// A capsule on `inference.transport: process`, whose harness holds the conversation: every
1230    /// image part is refused.
1231    ProcessTransport,
1232}
1233
1234impl ImagePolicy {
1235    /// The policy for a capsule on `transport` with `limits`.
1236    pub(crate) fn for_transport(
1237        transport: &str,
1238        limits: murmur_artifact::ContextImagesConfig,
1239    ) -> Self {
1240        if transport == "process" {
1241            Self::ProcessTransport
1242        } else {
1243            Self::Accept(limits)
1244        }
1245    }
1246}
1247
1248impl Default for ImagePolicy {
1249    fn default() -> Self {
1250        Self::Accept(murmur_artifact::ContextImagesConfig::default())
1251    }
1252}
1253
1254/// The images `message`'s `raw` parts carry, each with its part index, decoded and checked under
1255/// `policy`, or the `-32602` refusing the whole message.
1256///
1257/// A message is refused, naming `message.parts[<i>]`, when an image is malformed base64, its bytes
1258/// do not match its `mediaType`, it is over `context.images.max_bytes`, it is one more than
1259/// `context.images.max_per_turn`, it continues a task, or the capsule runs on the process
1260/// transport. A message with no image part is never refused here.
1261pub(crate) fn message_images(
1262    message: &IncomingMessage,
1263    policy: ImagePolicy,
1264) -> Result<Vec<(usize, crate::images::CheckedImage)>, MessageRefusal> {
1265    let mut images = Vec::new();
1266    for (index, part) in message.parts.iter().enumerate() {
1267        let PartContent::Raw(data) = &part.content else {
1268            continue;
1269        };
1270        let media_type = part.media_type.as_deref().unwrap_or_default();
1271        let limits = match policy {
1272            ImagePolicy::ProcessTransport => {
1273                return Err(MessageRefusal::InvalidParams(format!(
1274                    "message.parts[{index}] is an image, and this capsule runs on \
1275                     inference.transport: process, whose harness takes no images from the door"
1276                )))
1277            }
1278            ImagePolicy::Accept(limits) => limits,
1279        };
1280        if let Some(task_id) = &message.task_id {
1281            return Err(MessageRefusal::InvalidParams(format!(
1282                "message.parts[{index}] is an image; a message continuing task {task_id} carries \
1283                 text only, so send images on the message that starts a task"
1284            )));
1285        }
1286        if images.len() as u32 >= limits.max_per_turn {
1287            return Err(MessageRefusal::InvalidParams(format!(
1288                "message.parts[{index}] is image {} of this message; context.images.max_per_turn \
1289                 is {}",
1290                images.len() + 1,
1291                limits.max_per_turn
1292            )));
1293        }
1294        match crate::images::decode_raw_part(media_type, data, limits.max_bytes) {
1295            Ok(image) => images.push((index, image)),
1296            Err(refusal) => {
1297                return Err(MessageRefusal::InvalidParams(format!(
1298                    "message.parts[{index}] was not attached: {} ({}); context.images.max_bytes \
1299                     is {} bytes",
1300                    refusal.reason(),
1301                    refusal.code(),
1302                    limits.max_bytes
1303                )))
1304            }
1305        }
1306    }
1307    Ok(images)
1308}
1309
1310/// `body` in a Markdown code fence whose opening line is the fence followed by `label`. The fence
1311/// is a run of backticks one longer than the longest run in `body`, and at least three, so nothing
1312/// in `body` can close it.
1313pub(crate) fn code_fence(label: &str, body: &str) -> String {
1314    let mut longest = 0;
1315    let mut run = 0;
1316    for c in body.chars() {
1317        run = if c == '`' { run + 1 } else { 0 };
1318        longest = longest.max(run);
1319    }
1320    let fence = "`".repeat((longest + 1).max(3));
1321    format!("{fence}{label}\n{body}\n{fence}")
1322}
1323
1324/// `value` as one token of a fence's opening line: whitespace, backticks and `=` become `_`.
1325fn fence_label_value(value: &str) -> String {
1326    value
1327        .chars()
1328        .map(|c| {
1329            if c.is_whitespace() || c == '`' || c == '=' {
1330                '_'
1331            } else {
1332                c
1333            }
1334        })
1335        .collect()
1336}
1337
1338// ── Client helpers ────────────────────────────────────────────────────────────
1339
1340/// The A2A v1.0 message a murmur client sends a door: from `ROLE_USER`, with one text part.
1341/// `contextId` is written only when given.
1342pub(crate) fn user_message(message_id: &str, context_id: Option<&str>, text: &str) -> Value {
1343    let mut message = serde_json::json!({
1344        "messageId": message_id,
1345        "role": Role::User.wire_name(),
1346        "parts": [Part::text(text).to_json()],
1347    });
1348    if let Some(context_id) = context_id {
1349        message["contextId"] = Value::String(context_id.to_string());
1350    }
1351    message
1352}
1353
1354/// The `SendMessage` params a murmur client sends a door: `message` with
1355/// `configuration.returnImmediately: true`, so the door answers once the task is accepted rather
1356/// than once it settles.
1357pub(crate) fn send_message_params(message: Value) -> Value {
1358    serde_json::json!({
1359        "message": message,
1360        "configuration": { "returnImmediately": true },
1361    })
1362}
1363
1364/// A `SendMessageResponse`, as a door's `SendMessage` `result` carries it: the oneof of a task and
1365/// a message.
1366#[derive(Debug, Clone)]
1367pub(crate) enum SendMessageResult {
1368    Task(Box<A2aTask>),
1369    /// The message, as sent. Murmur reads nothing in it.
1370    Message(Value),
1371}
1372
1373impl SendMessageResult {
1374    /// The member `result` sets, or why it is not a `SendMessageResponse`: it sets both members or
1375    /// neither, or its `task` is not a v1.0 task.
1376    pub(crate) fn from_result(result: &Value) -> Result<SendMessageResult, String> {
1377        match send_message_member(result)? {
1378            ("task", task) => serde_json::from_value(task.clone())
1379                .map(|task| SendMessageResult::Task(Box::new(task)))
1380                .map_err(|error| format!("its task is not an A2A task: {error}")),
1381            (_, message) => Ok(SendMessageResult::Message(message.clone())),
1382        }
1383    }
1384}
1385
1386/// Which member of the `SendMessageResponse` oneof `result` sets, `"task"` or `"message"`, with
1387/// its value, or why it sets neither or both.
1388pub(crate) fn send_message_member(result: &Value) -> Result<(&'static str, &Value), String> {
1389    match (result.get("task"), result.get("message")) {
1390        (Some(task), None) => Ok(("task", task)),
1391        (None, Some(message)) => Ok(("message", message)),
1392        (Some(_), Some(_)) => Err("it answered both a task and a message".to_string()),
1393        (None, None) => Err(format!(
1394            "it answered neither a task nor a message: {result}"
1395        )),
1396    }
1397}
1398
1399/// What [`TaskRegistry::request_cancel`] did.
1400///
1401/// Three outcomes and only one of them is an error at the door: cancelling a task that has
1402/// already ended is a clean no-op, because the caller's intent — "do no more work on this" — is
1403/// already true.
1404#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1405pub(crate) enum CancelOutcome {
1406    /// The task was live and is now `Canceled`. Its [`CancelSignal`] has been raised.
1407    Accepted,
1408    /// The task had already reached a terminal state, which is left untouched.
1409    AlreadyTerminal,
1410    /// No task with this id was ever enqueued here.
1411    Unknown,
1412}
1413
1414// ── Task slot state machine ───────────────────────────────────────────────────
1415
1416#[derive(Debug, Clone)]
1417pub(crate) enum TaskSlotState {
1418    Empty,
1419    Running {
1420        task_id: String,
1421        context_id: String,
1422        /// The lane the queue chose this task out of. Set and cleared with the rest of the
1423        /// variant, so it cannot name a lane no task is running in.
1424        lane: TaskLane,
1425    },
1426    /// The last task that ran has ended.
1427    Done,
1428}
1429
1430// ── TaskRegistry — multi-task history tracker ─────────────────────────────────
1431
1432/// Replaces the bare `Arc<Mutex<TaskSlotState>>` at the serve_http boundary.
1433/// Tracks all tasks (active + historical) for queue-mode capsules.
1434pub(crate) struct TaskRegistry {
1435    pub(crate) active_slot: TaskSlotState,
1436    /// All tasks ever enqueued: task_id → (state, context_id)
1437    pub(crate) history: HashMap<String, (TaskState, String)>,
1438    pub(crate) pending_count: usize,
1439    pub(crate) queue_depth: usize,
1440    pub(crate) task_acceptance: TaskAcceptance,
1441    /// Pending input waiters: task_id → (prompt, oneshot sender)
1442    input_waiters: HashMap<String, (String, oneshot::Sender<String>)>,
1443    /// Tasks whose `request-input` wait timed out, until the agent loop takes the mark.
1444    input_timeouts: HashSet<String>,
1445    /// One signal per task anybody has asked about, whether or not it has been cancelled.
1446    ///
1447    /// Minted on demand rather than at enqueue, because both ends need one before the task is
1448    /// running: the task loop watches a task it is about to activate, and the door cancels one
1449    /// that is still queued.
1450    cancels: HashMap<String, CancelSignal>,
1451    /// Which completed turn the capsule's exported files are as of, shared with the resource
1452    /// plane. Lives here because every terminal state passes through this registry, so a third
1453    /// [`Self::finish_task`] call site cannot appear with no matching increment beside it.
1454    resource_generation: Arc<AtomicU64>,
1455    /// Set by [`Self::close_to_new_work`] once the task loop has ended. A closed registry
1456    /// accepts nothing, whatever its acceptance mode and depth.
1457    closed: bool,
1458    /// How each finished task ended, recorded by [`Self::record_ending`].
1459    endings: HashMap<String, TaskEnding>,
1460    /// The door credential that started each task a credential other than the operator's
1461    /// started: a declared credential's name, or `member:<from>` for a formation token.
1462    owners: HashMap<String, String>,
1463    /// The last revision handed out. Every change to a task's state or ending takes the next one.
1464    revision: u64,
1465    /// Each task's revision as of its last change, which orders `ListTasks`, newest first.
1466    revisions: HashMap<String, u64>,
1467    /// The instant of each task's last change, stamped beside its revision: its status's
1468    /// `timestamp`, and what `ListTasks`' `statusTimestampAfter` compares against.
1469    status_times: HashMap<String, DateTime<Utc>>,
1470    /// Tasks kept working under `lifecycle.keep_working` that hold an answer from an earlier turn
1471    /// end, which the session deadline ends `completed` with that answer rather than `canceled`.
1472    standing_answers: HashSet<String>,
1473    /// Tasks holding a standing answer whose signal the session deadline raised without
1474    /// recording `Canceled`; read by [`Self::stopped_at_deadline`].
1475    stopped_at_deadline: HashSet<String>,
1476    /// Tasks the session deadline's cancel was accepted for; read by
1477    /// [`Self::canceled_at_deadline`].
1478    canceled_at_deadline: HashSet<String>,
1479    /// Whether the door takes image parts, and within which limits.
1480    image_policy: ImagePolicy,
1481    /// The images each task's tool calls returned, by sha256: what the door serves at
1482    /// `/attachments/<task-id>/<sha256>`. Filled by the agent loop as it accepts them.
1483    attachments: HashMap<String, HashMap<String, IndexedAttachment>>,
1484}
1485
1486/// One image the door serves for a task: its media type and the blob holding its bytes.
1487#[derive(Debug, Clone, PartialEq, Eq)]
1488pub(crate) struct IndexedAttachment {
1489    pub(crate) media_type: String,
1490    pub(crate) blob: std::path::PathBuf,
1491}
1492
1493/// The `status.message` of a task refused because the session ended before it started.
1494pub(crate) const REJECTED_SESSION_ENDED_MESSAGE: &str =
1495    "task rejected: the session ended before this task started";
1496
1497/// The `status.message` of a task refused because `mur stop` ended the session before it started.
1498pub(crate) const REJECTED_SESSION_STOPPED_MESSAGE: &str =
1499    "task rejected: the session was stopped before this task started";
1500
1501/// The `status.message` the door's `SendStreamingMessage` refusal carries once the registry is closed.
1502pub(crate) const REJECTED_SESSION_CLOSING_MESSAGE: &str = "task rejected: the session is closing";
1503
1504/// The `status.message` of a task refused because the capsule has no room for it. A `call-member`
1505/// call reads exactly this text as "busy" and offers the task again; every other refusal ends it.
1506pub(crate) const REJECTED_BUSY_MESSAGE: &str = "task rejected: capsule is busy";
1507
1508impl TaskRegistry {
1509    pub(crate) fn new(queue_depth: usize, task_acceptance: TaskAcceptance) -> Self {
1510        Self {
1511            active_slot: TaskSlotState::Empty,
1512            history: HashMap::new(),
1513            pending_count: 0,
1514            queue_depth,
1515            task_acceptance,
1516            input_waiters: HashMap::new(),
1517            input_timeouts: HashSet::new(),
1518            cancels: HashMap::new(),
1519            resource_generation: Arc::new(AtomicU64::new(0)),
1520            closed: false,
1521            endings: HashMap::new(),
1522            owners: HashMap::new(),
1523            revision: 0,
1524            revisions: HashMap::new(),
1525            status_times: HashMap::new(),
1526            standing_answers: HashSet::new(),
1527            stopped_at_deadline: HashSet::new(),
1528            canceled_at_deadline: HashSet::new(),
1529            image_policy: ImagePolicy::default(),
1530            attachments: HashMap::new(),
1531        }
1532    }
1533
1534    /// This registry with the door taking image parts under `policy`.
1535    pub(crate) fn with_image_policy(mut self, policy: ImagePolicy) -> Self {
1536        self.image_policy = policy;
1537        self
1538    }
1539
1540    pub(crate) fn image_policy(&self) -> ImagePolicy {
1541        self.image_policy
1542    }
1543
1544    /// Serve the image `sha256`, stored at `blob`, for `task_id` at the door's attachment route.
1545    pub(crate) fn index_attachment(
1546        &mut self,
1547        task_id: &str,
1548        sha256: &str,
1549        media_type: &str,
1550        blob: std::path::PathBuf,
1551    ) {
1552        self.attachments
1553            .entry(task_id.to_string())
1554            .or_default()
1555            .insert(
1556                sha256.to_string(),
1557                IndexedAttachment {
1558                    media_type: media_type.to_string(),
1559                    blob,
1560                },
1561            );
1562    }
1563
1564    /// The image `sha256` a tool call of `task_id` returned, or `None` when it returned none by
1565    /// that hash.
1566    pub(crate) fn attachment(&self, task_id: &str, sha256: &str) -> Option<&IndexedAttachment> {
1567        self.attachments.get(task_id)?.get(sha256)
1568    }
1569
1570    /// A handle on the generation counter, for a reader that must not take this registry's lock.
1571    ///
1572    /// The resource plane serves reads while a turn is running, so it holds its own `Arc` and
1573    /// loads it atomically. Taking the registry mutex to answer a `GET` would make a read wait on
1574    /// the agent loop, which is the one thing this plane promises never to do.
1575    pub(crate) fn resource_generation(&self) -> Arc<AtomicU64> {
1576        Arc::clone(&self.resource_generation)
1577    }
1578
1579    /// Advance the generation by one. Called at each [`Self::finish_task`] call site, immediately
1580    /// after the task reaches a terminal state.
1581    ///
1582    /// Provenance, not a pin: it answers "these bytes are as of turn N" and nothing else. No
1583    /// request selects a generation, no response is refused because it moved, and no superseded
1584    /// bytes are retained.
1585    pub(crate) fn advance_resource_generation(&self) {
1586        self.resource_generation.fetch_add(1, Ordering::SeqCst);
1587    }
1588
1589    pub(crate) fn can_accept(&self) -> bool {
1590        if self.closed {
1591            return false;
1592        }
1593        match self.task_acceptance {
1594            TaskAcceptance::None => false,
1595            TaskAcceptance::Single => {
1596                matches!(self.active_slot, TaskSlotState::Empty) && self.pending_count == 0
1597            }
1598            TaskAcceptance::Queue => self.pending_count < self.queue_depth,
1599        }
1600    }
1601
1602    pub(crate) fn enqueue(&mut self, task_id: &str, context_id: &str) {
1603        self.pending_count += 1;
1604        self.record_state(task_id, TaskState::Submitted, context_id.to_string());
1605    }
1606
1607    /// Take back a task [`Self::enqueue`] just enqueued, which the task loop never received:
1608    /// its queue slot, its state, its revision, its status time and its owner.
1609    pub(crate) fn withdraw(&mut self, task_id: &str) {
1610        self.pending_count = self.pending_count.saturating_sub(1);
1611        self.history.remove(task_id);
1612        self.revisions.remove(task_id);
1613        self.status_times.remove(task_id);
1614        self.owners.remove(task_id);
1615    }
1616
1617    /// Record `state` for `task_id` in `context_id`, under the next revision. Every write to
1618    /// [`Self::history`] goes through here, so no state changes without moving the task to the
1619    /// front of `ListTasks`.
1620    pub(crate) fn record_state(&mut self, task_id: &str, state: TaskState, context_id: String) {
1621        self.history
1622            .insert(task_id.to_string(), (state, context_id));
1623        self.touch(task_id);
1624    }
1625
1626    /// Stamp `task_id` with the next revision and the current instant, together, so its status
1627    /// timestamp moves exactly when its place in `ListTasks` does.
1628    fn touch(&mut self, task_id: &str) {
1629        self.revision += 1;
1630        self.revisions.insert(task_id.to_string(), self.revision);
1631        self.status_times.insert(task_id.to_string(), status_now());
1632    }
1633
1634    /// The instant of `task_id`'s last change, or `None` for a task this registry does not hold.
1635    fn status_time(&self, task_id: &str) -> Option<DateTime<Utc>> {
1636        self.status_times.get(task_id).copied()
1637    }
1638
1639    /// Record that the door credential `credential` started `task_id`. Called under the lock the
1640    /// task was enqueued under, so no `GetTask` can see the task without its owner.
1641    pub(crate) fn record_owner(&mut self, task_id: &str, credential: &str) {
1642        self.owners
1643            .insert(task_id.to_string(), credential.to_string());
1644    }
1645
1646    /// The door credential that started `task_id`, or `None` for a task the operator, a caller of
1647    /// a public door or the runtime itself started.
1648    pub(crate) fn owner(&self, task_id: &str) -> Option<&str> {
1649        self.owners.get(task_id).map(String::as_str)
1650    }
1651
1652    /// Record how `task_id` ended: its final status's `message`, and its `response` when it
1653    /// completed. A response on any other state is not kept. `no_answer` is whether it ended
1654    /// through `end-without-answer`, `no_answer_below` the `(member, status)` of each member
1655    /// further down that gave it no answer, and `running_commands` the background shell commands
1656    /// still running when it ended.
1657    pub(crate) fn record_ending(
1658        &mut self,
1659        task_id: &str,
1660        message: &str,
1661        response: Option<&str>,
1662        no_answer: bool,
1663        no_answer_below: &[(String, String)],
1664        running_commands: &[RunningCommand],
1665    ) {
1666        let completed = matches!(self.history.get(task_id), Some((TaskState::Completed, _)));
1667        self.endings.insert(
1668            task_id.to_string(),
1669            TaskEnding {
1670                message: message.to_string(),
1671                response: response.filter(|_| completed).map(str::to_string),
1672                no_answer,
1673                no_answer_below: no_answer_below.to_vec(),
1674                running_commands: running_commands.to_vec(),
1675            },
1676        );
1677        self.touch(task_id);
1678    }
1679
1680    /// `lane` is the lane the queue selected this task out of, so what the registry reports is
1681    /// what the selection did.
1682    pub(crate) fn start_task(&mut self, task_id: String, context_id: String, lane: TaskLane) {
1683        debug_assert!(self.pending_count > 0);
1684        self.pending_count -= 1;
1685        self.record_state(&task_id, TaskState::Working, context_id.clone());
1686        self.active_slot = TaskSlotState::Running {
1687            task_id,
1688            context_id,
1689            lane,
1690        };
1691    }
1692
1693    /// The lane of the task the capsule is running, or `None` when no task is running.
1694    ///
1695    /// `Some` is what stops the lane queue yielding anything, so a `Done` or `Empty` slot must
1696    /// answer `None`.
1697    pub(crate) fn active_lane(&self) -> Option<TaskLane> {
1698        match self.active_slot {
1699            TaskSlotState::Running { lane, .. } => Some(lane),
1700            TaskSlotState::Empty | TaskSlotState::Done => None,
1701        }
1702    }
1703
1704    /// End the running task in `final_state`, and return the state actually recorded: an
1705    /// accepted cancel already in the history wins over whatever is passed. `None` when no task
1706    /// is running, which records nothing.
1707    pub(crate) fn finish_task(&mut self, final_state: TaskState) -> Option<TaskState> {
1708        let TaskSlotState::Running {
1709            ref task_id,
1710            ref context_id,
1711            ..
1712        } = self.active_slot
1713        else {
1714            return None;
1715        };
1716        let (tid, cid) = (task_id.clone(), context_id.clone());
1717        self.input_waiters.remove(&tid);
1718        self.input_timeouts.remove(&tid);
1719        self.cancels.remove(&tid);
1720        self.standing_answers.remove(&tid);
1721        self.stopped_at_deadline.remove(&tid);
1722        self.canceled_at_deadline.remove(&tid);
1723        // An accepted cancel is final. A turn a person stopped must never later read as one
1724        // that ran to completion, so the outcome the loop reports loses to the one already
1725        // recorded.
1726        let final_state = match self.history.get(&tid) {
1727            Some((TaskState::Canceled, _)) => TaskState::Canceled,
1728            _ => final_state,
1729        };
1730        self.record_state(&tid, final_state.clone(), cid);
1731        self.active_slot = TaskSlotState::Done;
1732        Some(final_state)
1733    }
1734
1735    /// Record that `task_id`'s `request-input` wait passed `lifecycle.input_timeout_secs`
1736    /// without ending the task: the waiter is dropped, an `input-required` task goes back to
1737    /// `working`, and the timeout is marked for [`Self::take_input_timeout`]. The task's
1738    /// terminal state is the reopen loop's to record, once its `on-task-end` hooks have run.
1739    pub(crate) fn record_input_timeout(&mut self, task_id: &str) {
1740        self.input_waiters.remove(task_id);
1741        if let Some((TaskState::InputRequired, ctx)) = self.history.get(task_id).cloned() {
1742            self.record_state(task_id, TaskState::Working, ctx);
1743        }
1744        self.input_timeouts.insert(task_id.to_string());
1745    }
1746
1747    /// Whether `task_id` has an unread input-wait timeout, clearing the mark. Read by the agent
1748    /// loop at each turn boundary, so the attempt that timed out ends and a reopened attempt of
1749    /// the same task does not end on the same mark.
1750    pub(crate) fn take_input_timeout(&mut self, task_id: &str) -> bool {
1751        self.input_timeouts.remove(task_id)
1752    }
1753
1754    /// Transition the active task to InputRequired, storing the prompt and the
1755    /// oneshot sender that will deliver the external response.
1756    pub(crate) fn set_input_required(
1757        &mut self,
1758        task_id: &str,
1759        prompt: String,
1760        tx: oneshot::Sender<String>,
1761    ) -> Result<(), &'static str> {
1762        match &self.active_slot {
1763            TaskSlotState::Running {
1764                task_id: active_id, ..
1765            } if active_id == task_id => {}
1766            _ => return Err("task is not the active running task"),
1767        }
1768        let state = self.history.get(task_id).map(|(s, _)| s);
1769        if !matches!(state, Some(TaskState::Working)) {
1770            return Err("task is not in working state");
1771        }
1772        if let Some((_, ctx)) = self.history.get(task_id).cloned() {
1773            self.record_state(task_id, TaskState::InputRequired, ctx);
1774        }
1775        self.input_waiters.insert(task_id.to_string(), (prompt, tx));
1776        Ok(())
1777    }
1778
1779    /// Deliver external input to an input-required task, transitioning it back to Working.
1780    pub(crate) fn deliver_input(
1781        &mut self,
1782        task_id: &str,
1783        text: String,
1784    ) -> Result<(), &'static str> {
1785        let state = self.history.get(task_id).map(|(s, _)| s);
1786        if !matches!(state, Some(TaskState::InputRequired)) {
1787            return Err("task is not in input-required state");
1788        }
1789        let Some((_, tx)) = self.input_waiters.remove(task_id) else {
1790            return Err("no input waiter found for task");
1791        };
1792        if let Some((_, ctx)) = self.history.get(task_id).cloned() {
1793            self.record_state(task_id, TaskState::Working, ctx);
1794        }
1795        // Sending may fail if the receiver was dropped (timeout path), which is fine.
1796        let _ = tx.send(text);
1797        Ok(())
1798    }
1799
1800    /// The cancel signal for `task_id`, minting one if nobody has asked yet.
1801    ///
1802    /// Handed to whatever has to race against a cancel — the driver call, the input wait, the
1803    /// delegation wait — and to the door, which raises it. A signal for a task that never runs
1804    /// costs one entry and is dropped with the task's terminal state.
1805    pub(crate) fn cancel_watch(&mut self, task_id: &str) -> CancelSignal {
1806        self.cancels.entry(task_id.to_string()).or_default().clone()
1807    }
1808
1809    /// Whether this task's recorded state is `Canceled`.
1810    ///
1811    /// Read at the task loop's activation step, which is what keeps a task cancelled while it was
1812    /// still `submitted` from ever starting.
1813    pub(crate) fn is_canceled(&self, task_id: &str) -> bool {
1814        matches!(self.history.get(task_id), Some((TaskState::Canceled, _)))
1815    }
1816
1817    /// Stop one task: record `Canceled` and raise its signal.
1818    ///
1819    /// Called on the door's connection task and never waits for the agent loop to acknowledge —
1820    /// the state is written here, so a `GetTask` that lands next already reads `canceled`
1821    /// whatever the loop is in the middle of.
1822    ///
1823    /// A cancelled task that was still `submitted` gives its queue slot back immediately: it will
1824    /// be skipped when the loop reaches it, so holding capacity for it would refuse work the
1825    /// capsule can do.
1826    pub(crate) fn request_cancel(&mut self, task_id: &str) -> CancelOutcome {
1827        let Some((state, context_id)) = self.history.get(task_id).cloned() else {
1828            return CancelOutcome::Unknown;
1829        };
1830        if state.is_terminal() {
1831            return CancelOutcome::AlreadyTerminal;
1832        }
1833        if matches!(state, TaskState::Submitted) {
1834            self.pending_count = self.pending_count.saturating_sub(1);
1835        }
1836        self.record_state(task_id, TaskState::Canceled, context_id);
1837        self.cancel_watch(task_id).cancel();
1838        CancelOutcome::Accepted
1839    }
1840
1841    /// Mark `task_id` as holding a standing answer: an earlier turn end's answer, which the task
1842    /// keeps while `lifecycle.keep_working` sends it on or `lifecycle.done_check` checks it.
1843    pub(crate) fn mark_standing_answer(&mut self, task_id: &str) {
1844        self.standing_answers.insert(task_id.to_string());
1845    }
1846
1847    /// Drop `task_id`'s standing-answer mark, as a reopen does: the reopened attempt has no
1848    /// answer of its own yet.
1849    pub(crate) fn clear_standing_answer(&mut self, task_id: &str) {
1850        self.standing_answers.remove(task_id);
1851    }
1852
1853    /// Whether the session deadline stopped `task_id` while it held a standing answer, leaving
1854    /// its state for the task loop to record.
1855    pub(crate) fn stopped_at_deadline(&self, task_id: &str) -> bool {
1856        self.stopped_at_deadline.contains(task_id)
1857    }
1858
1859    /// Whether the session deadline cancelled `task_id`: its cancel was accepted by
1860    /// [`Self::cancel_every_live`] at the deadline, not by a person or another termination first.
1861    pub(crate) fn canceled_at_deadline(&self, task_id: &str) -> bool {
1862        self.canceled_at_deadline.contains(task_id)
1863    }
1864
1865    /// Cancel every task that is not yet terminal, and return the ids that were cancelled, sorted.
1866    ///
1867    /// Each goes through [`Self::request_cancel`], so a running task's signal is raised and a
1868    /// `submitted` task's queue slot is given back. A second call returns nothing: every task the
1869    /// first one touched is `Canceled`, which is terminal.
1870    ///
1871    /// With `at_deadline`, which the session deadline passes, a task holding a standing answer
1872    /// ([`Self::mark_standing_answer`]) only has its signal raised and is marked
1873    /// [`Self::stopped_at_deadline`]: its history is left as it is, it is not in the returned ids,
1874    /// and the task loop ends it with that answer. Every task it does cancel is marked
1875    /// [`Self::canceled_at_deadline`].
1876    pub(crate) fn cancel_every_live(&mut self, at_deadline: bool) -> Vec<String> {
1877        let live: Vec<String> = self
1878            .history
1879            .iter()
1880            .filter(|(_, (state, _))| !state.is_terminal())
1881            .map(|(task_id, _)| task_id.clone())
1882            .collect();
1883        let (spared, live): (Vec<String>, Vec<String>) = live
1884            .into_iter()
1885            .partition(|task_id| at_deadline && self.standing_answers.contains(task_id));
1886        for task_id in spared {
1887            self.cancel_watch(&task_id).cancel();
1888            self.stopped_at_deadline.insert(task_id);
1889        }
1890        let mut canceled: Vec<String> = live
1891            .into_iter()
1892            .filter(|task_id| matches!(self.request_cancel(task_id), CancelOutcome::Accepted))
1893            .collect();
1894        if at_deadline {
1895            self.canceled_at_deadline.extend(canceled.iter().cloned());
1896        }
1897        // `history` is a `HashMap`, so without this the same two tasks come back in a different
1898        // order on every call and nothing can diff two stops.
1899        canceled.sort();
1900        canceled
1901    }
1902
1903    /// Stop taking work, and end every task still `submitted` in `Rejected`. Returns the refused
1904    /// tasks' `(task_id, context_id)`, sorted by task id, which for UUIDv7 ids is acceptance order.
1905    ///
1906    /// Taken under the same lock the door enqueues under, so no task can be enqueued after the
1907    /// call returns: [`Self::can_accept`] answers `false` from here on. Only `Submitted` tasks are
1908    /// touched. A `Canceled` task keeps its state, and a task that ran keeps the state it ended in.
1909    /// No turn ran, so the resource generation does not advance. A second call returns nothing.
1910    pub(crate) fn close_to_new_work(&mut self) -> Vec<(String, String)> {
1911        self.closed = true;
1912        let mut refused: Vec<(String, String)> = self
1913            .history
1914            .iter()
1915            .filter(|(_, (state, _))| matches!(state, TaskState::Submitted))
1916            .map(|(task_id, (_, context_id))| (task_id.clone(), context_id.clone()))
1917            .collect();
1918        refused.sort();
1919        for (task_id, context_id) in &refused {
1920            self.record_state(task_id, TaskState::Rejected, context_id.clone());
1921            self.cancels.remove(task_id);
1922        }
1923        self.pending_count = self.pending_count.saturating_sub(refused.len());
1924        refused
1925    }
1926
1927    /// Whether [`Self::close_to_new_work`] has run. The door reads it to tell a closing session
1928    /// from a busy one when it refuses a task.
1929    pub(crate) fn is_closed(&self) -> bool {
1930        self.closed
1931    }
1932
1933    /// Whether `task_id`'s ending is recorded, which happens under the lock its terminal state is
1934    /// written under on every path but a cancel, whose ending follows once the task stops.
1935    pub(crate) fn has_ending(&self, task_id: &str) -> bool {
1936        self.endings.contains_key(task_id)
1937    }
1938
1939    /// Return the prompt stored for an input-required task.
1940    #[allow(dead_code)] // used in unit tests
1941    pub(crate) fn get_input_prompt(&self, task_id: &str) -> Option<&str> {
1942        self.input_waiters
1943            .get(task_id)
1944            .map(|(prompt, _)| prompt.as_str())
1945    }
1946
1947    /// `task_id` as `GetTask` reports it.
1948    ///
1949    /// Its `status.timestamp` is the instant of the task's last change. An `input-required` task
1950    /// carries its prompt as a `prompt` artifact. A terminal task carries its final status's
1951    /// message as `status.message`, and a `completed` one its response as a `response` artifact,
1952    /// each when it has one.
1953    pub(crate) fn get_task(&self, task_id: &str) -> Option<A2aTask> {
1954        self.history.get(task_id).map(|(state, context_id)| {
1955            let artifact =
1956                |name: &str, text: &str| A2aArtifact::named(name, vec![Part::text(text)]);
1957            let ending = self.endings.get(task_id).filter(|_| state.is_terminal());
1958            let artifacts = match state {
1959                TaskState::InputRequired => self
1960                    .input_waiters
1961                    .get(task_id)
1962                    .map(|(prompt, _)| vec![artifact(PROMPT_ARTIFACT, prompt)]),
1963                TaskState::Completed => ending
1964                    .and_then(|ending| ending.response.as_deref())
1965                    .filter(|response| !response.is_empty())
1966                    .map(|response| vec![artifact(RESPONSE_ARTIFACT, response)]),
1967                _ => None,
1968            };
1969            let message = ending
1970                .filter(|ending| !ending.message.is_empty())
1971                .map(|ending| StatusMessage::agent(task_id, context_id, &ending.message));
1972            A2aTask {
1973                id: task_id.to_string(),
1974                context_id: context_id.clone(),
1975                status: TaskStatus::at(
1976                    state.clone(),
1977                    message,
1978                    self.status_time(task_id).unwrap_or_else(status_now),
1979                ),
1980                artifacts,
1981                metadata: ending.and_then(TaskEnding::metadata),
1982            }
1983        })
1984    }
1985
1986    /// The block naming each task in `reference_task_ids` as it stands now, appended to a message
1987    /// that references them, or `None` when it references none. `visible` says whether the sender
1988    /// may see a task: one it may not reads exactly as one this registry never held.
1989    ///
1990    /// Each task is one line, `- <id>: <state>` in murmur's word. An ended task other than a
1991    /// `completed` one appends what its final status said; a `completed` one with a response is
1992    /// followed by the response in a code fence labelled `response`.
1993    pub(crate) fn reference_block(
1994        &self,
1995        reference_task_ids: &[String],
1996        visible: impl Fn(&str) -> bool,
1997    ) -> Option<String> {
1998        if reference_task_ids.is_empty() {
1999            return None;
2000        }
2001        let lines: Vec<String> = reference_task_ids
2002            .iter()
2003            .map(|task_id| {
2004                let Some((state, _)) = self.history.get(task_id).filter(|_| visible(task_id))
2005                else {
2006                    return format!("- {task_id}: not a task this capsule holds");
2007                };
2008                let mut line = format!("- {task_id}: {}", state.as_str());
2009                let ending = self.endings.get(task_id).filter(|_| state.is_terminal());
2010                match (state, ending) {
2011                    (TaskState::Completed, Some(ending)) => {
2012                        if let Some(response) =
2013                            ending.response.as_deref().filter(|text| !text.is_empty())
2014                        {
2015                            line.push('\n');
2016                            line.push_str(&code_fence(RESPONSE_ARTIFACT, response));
2017                        }
2018                    }
2019                    (_, Some(ending)) if !ending.message.is_empty() => {
2020                        line.push_str(": ");
2021                        line.push_str(&ending.message.replace(['\r', '\n'], " "));
2022                    }
2023                    _ => {}
2024                }
2025                line
2026            })
2027            .collect();
2028        Some(format!("Referenced tasks:\n{}", lines.join("\n")))
2029    }
2030
2031    /// One `ListTasks` page: the tasks `visible` lets the caller see that match `filter`, newest
2032    /// revision first, the first `page_size` of those strictly after `after`.
2033    ///
2034    /// Each task is exactly [`Self::get_task`]'s. `next` is the position of the page's last task
2035    /// when a matching task lies past it, and `total` counts every visible match, before paging.
2036    /// A task whose state changes during a walk takes a newer revision and moves ahead of the
2037    /// cursor, so a walk returns every match exactly once only while no match changes.
2038    pub(crate) fn list_tasks(
2039        &self,
2040        filter: &TaskFilter,
2041        after: Option<&ListCursor>,
2042        page_size: usize,
2043        visible: impl Fn(&str) -> bool,
2044    ) -> TaskPage {
2045        let mut matching: Vec<ListCursor> = self
2046            .history
2047            .iter()
2048            .filter(|(task_id, (state, context_id))| {
2049                filter.matches(state, context_id, self.status_time(task_id)) && visible(task_id)
2050            })
2051            .map(|(task_id, _)| ListCursor {
2052                revision: self.revisions.get(task_id).copied().unwrap_or_default(),
2053                task_id: task_id.clone(),
2054            })
2055            .collect();
2056        matching.sort_unstable_by(|a, b| b.cmp(a));
2057        let total = matching.len();
2058        let rest: Vec<ListCursor> = matching
2059            .into_iter()
2060            .filter(|position| after.is_none_or(|after| position < after))
2061            .collect();
2062        let page: Vec<&ListCursor> = rest.iter().take(page_size).collect();
2063        let next = (rest.len() > page.len())
2064            .then(|| page.last().map(|last| (*last).clone()))
2065            .flatten();
2066        TaskPage {
2067            tasks: page
2068                .iter()
2069                .filter_map(|position| self.get_task(&position.task_id))
2070                .collect(),
2071            next,
2072            total,
2073        }
2074    }
2075}
2076
2077/// Which tasks a `ListTasks` call asks for.
2078#[derive(Debug, Clone, PartialEq)]
2079pub(crate) struct TaskFilter {
2080    /// Only the tasks in this context, or the tasks of every context when `None`.
2081    pub context_id: Option<String>,
2082    pub state: StateFilter,
2083    /// Only the tasks whose status time is at or after this instant (`statusTimestampAfter`), or
2084    /// every task when `None`.
2085    pub status_after: Option<DateTime<Utc>>,
2086}
2087
2088impl TaskFilter {
2089    fn matches(&self, state: &TaskState, context_id: &str, at: Option<DateTime<Utc>>) -> bool {
2090        let in_context = self.context_id.as_deref().is_none_or(|c| c == context_id);
2091        let in_state = match &self.state {
2092            StateFilter::Any => true,
2093            StateFilter::Only(wanted) => wanted == state,
2094            StateFilter::Nothing => false,
2095        };
2096        let in_time = self
2097            .status_after
2098            .is_none_or(|after| at.is_some_and(|at| at >= after));
2099        in_context && in_state && in_time
2100    }
2101}
2102
2103/// The `status` a `ListTasks` call filters by.
2104#[derive(Debug, Clone, PartialEq)]
2105pub(crate) enum StateFilter {
2106    /// No `status`, or `TASK_STATE_UNSPECIFIED`.
2107    Any,
2108    /// Only tasks in this state.
2109    Only(TaskState),
2110    /// A state no task here is ever in: `TASK_STATE_AUTH_REQUIRED`.
2111    Nothing,
2112}
2113
2114impl StateFilter {
2115    /// The filter a `status` naming the v1.0 `TaskState` `wire` asks for, or `None` when `wire`
2116    /// is not a `TaskState` name.
2117    pub(crate) fn from_wire_name(wire: &str) -> Option<StateFilter> {
2118        match wire {
2119            "TASK_STATE_UNSPECIFIED" => Some(StateFilter::Any),
2120            "TASK_STATE_AUTH_REQUIRED" => Some(StateFilter::Nothing),
2121            wire => TaskState::from_wire_name(wire).map(StateFilter::Only),
2122        }
2123    }
2124}
2125
2126/// A place in the `ListTasks` order: a task's revision and its id. Revisions are unique, so the
2127/// order is total; the id only makes the position self-describing.
2128#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
2129pub(crate) struct ListCursor {
2130    pub revision: u64,
2131    pub task_id: String,
2132}
2133
2134impl ListCursor {
2135    /// The `nextPageToken` naming this position: `<revision>:<task_id>` in unpadded base64url.
2136    /// Clients treat it as opaque.
2137    pub(crate) fn encode(&self) -> String {
2138        use base64::Engine as _;
2139        base64::engine::general_purpose::URL_SAFE_NO_PAD
2140            .encode(format!("{}:{}", self.revision, self.task_id))
2141    }
2142
2143    /// The position a `pageToken` names, or `None` for a string [`Self::encode`] did not produce.
2144    pub(crate) fn decode(token: &str) -> Option<ListCursor> {
2145        use base64::Engine as _;
2146        let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
2147            .decode(token)
2148            .ok()?;
2149        let text = String::from_utf8(bytes).ok()?;
2150        let (revision, task_id) = text.split_once(':')?;
2151        let revision = revision
2152            .parse()
2153            .ok()
2154            .filter(|_| !revision.starts_with('+'))?;
2155        (!task_id.is_empty()).then(|| ListCursor {
2156            revision,
2157            task_id: task_id.to_string(),
2158        })
2159    }
2160}
2161
2162/// One `ListTasks` page, as [`TaskRegistry::list_tasks`] returns it.
2163#[derive(Debug, Clone)]
2164pub(crate) struct TaskPage {
2165    pub tasks: Vec<A2aTask>,
2166    /// Where the next page starts, or `None` on the last page.
2167    pub next: Option<ListCursor>,
2168    /// How many tasks the caller sees that match the filter.
2169    pub total: usize,
2170}
2171
2172// ── Incoming task (HTTP server → main thread) ─────────────────────────────────
2173
2174#[derive(Debug)]
2175pub(crate) struct IncomingTask {
2176    pub task_id: String,
2177    pub context_id: String,
2178    pub message_id: String,
2179    pub message_text: String,
2180    pub traceparent: Option<String>,
2181    /// Why this task woke the capsule, classified at the door from the request headers. Every
2182    /// inbound task has one: a caller that claims nothing is an untrusted `event`.
2183    pub provenance: TaskProvenance,
2184    /// Where the task came from, as it appears on its `task_start` record: [`SOURCE_A2A`] for
2185    /// anything that arrived over the peer door, [`SOURCE_TASK_MD`] for the launch's own task,
2186    /// and [`SOURCE_DETACHED_SHELL`] or [`SOURCE_DETACHED_LOST`] for a report the runtime produced
2187    /// locally. The `a2a_task_received` record is written only for the first.
2188    pub source: &'static str,
2189    /// Whether the message that started this task asked the capsule to drop the harness session
2190    /// its context names before the turn: [`IncomingMessage::forget_session`], or for the launch's
2191    /// own task, `mur run --forget-session`. `false` for every task whose message did not ask, and
2192    /// for every report the runtime enqueued for itself.
2193    pub forget_session: bool,
2194    /// The formation member that called, when the door let this task in on a formation token.
2195    /// Recorded as `a2a_task_received.caller_member`; `None` for every other task.
2196    pub caller_member: Option<String>,
2197    /// Who the door let this task in as, recorded as `a2a_task_received.caller`. `Some` exactly
2198    /// for a [`SOURCE_A2A`] task; `None` for the launch's own task and every report the runtime
2199    /// enqueued for itself, none of which crossed the door.
2200    pub caller: Option<crate::identity::TaskCaller>,
2201    /// The message's `referenceTaskIds`, each once, recorded as `task_start.reference_task_ids`.
2202    /// Empty for every task that did not arrive over the door.
2203    pub reference_task_ids: Vec<String>,
2204    /// The kind of each part of the message, recorded as `task_start.part_kinds`. Empty for every
2205    /// task that did not arrive over the door.
2206    pub part_kinds: Vec<&'static str>,
2207    /// The images the message's `raw` parts carried, each with its part index, already checked by
2208    /// [`message_images`]. Empty for every task that did not arrive over the door.
2209    pub images: Vec<(usize, crate::images::CheckedImage)>,
2210}
2211
2212/// The `source` of a task that arrived over the A2A door.
2213pub(crate) const SOURCE_A2A: &str = "a2a";
2214
2215/// The `source` of a launch's own task: `--task`, or a workdir `task.md`/`input.txt` imported at
2216/// session start. It is a task on the door from the moment the door is announced, but it never
2217/// crossed the door, so it gets no `a2a_task_received` record.
2218pub(crate) const SOURCE_TASK_MD: &str = "task_md";
2219
2220/// The `source` of a task the runtime enqueued for itself when a demoted shell command finished.
2221pub(crate) const SOURCE_DETACHED_SHELL: &str = "detached_shell";
2222
2223/// The `source` of a task a resumed launch enqueued for itself to report demoted commands the
2224/// session it resumes never accounted for. Distinct from [`SOURCE_DETACHED_SHELL`] because the
2225/// two say opposite things: one carries a result, the other says no result exists.
2226pub(crate) const SOURCE_DETACHED_LOST: &str = "detached_lost";
2227
2228// ── Unit tests ────────────────────────────────────────────────────────────────
2229
2230#[cfg(test)]
2231mod tests {
2232    use super::*;
2233
2234    fn make_registry() -> TaskRegistry {
2235        TaskRegistry::new(1, TaskAcceptance::Single)
2236    }
2237
2238    fn running_registry(task_id: &str) -> TaskRegistry {
2239        let mut r = make_registry();
2240        r.enqueue(task_id, "ctx_001");
2241        r.start_task(task_id.to_string(), "ctx_001".to_string(), TaskLane::Bg);
2242        r
2243    }
2244
2245    #[test]
2246    fn set_input_required_transitions_state() {
2247        let mut r = running_registry("tsk_001");
2248        let (tx, _rx) = oneshot::channel();
2249        assert!(r
2250            .set_input_required("tsk_001", "which branch?".into(), tx)
2251            .is_ok());
2252        let task = r.get_task("tsk_001").unwrap();
2253        assert_eq!(task.status.state, TaskState::InputRequired);
2254        let artifacts = task.artifacts.unwrap();
2255        assert_eq!(artifacts[0].artifact_id, "prompt");
2256        assert_eq!(artifacts[0].first_text(), Some("which branch?"));
2257    }
2258
2259    #[test]
2260    fn set_input_required_fails_for_wrong_task() {
2261        let mut r = running_registry("tsk_001");
2262        let (tx, _rx) = oneshot::channel();
2263        assert!(r
2264            .set_input_required("tsk_other", "prompt".into(), tx)
2265            .is_err());
2266    }
2267
2268    #[test]
2269    fn deliver_input_transitions_back_to_working() {
2270        let mut r = running_registry("tsk_001");
2271        let (tx, mut rx) = oneshot::channel();
2272        r.set_input_required("tsk_001", "which branch?".into(), tx)
2273            .unwrap();
2274        r.deliver_input("tsk_001", "main".to_string()).unwrap();
2275        // Oneshot should have received the value
2276        assert_eq!(rx.try_recv().unwrap(), "main");
2277        let task = r.get_task("tsk_001").unwrap();
2278        assert_eq!(task.status.state, TaskState::Working);
2279        assert!(task.artifacts.is_none());
2280    }
2281
2282    #[test]
2283    fn deliver_input_fails_when_not_input_required() {
2284        let mut r = running_registry("tsk_001");
2285        assert!(r.deliver_input("tsk_001", "text".into()).is_err());
2286    }
2287
2288    #[test]
2289    fn an_input_required_task_carries_its_prompt_and_nothing_else() {
2290        let mut r = running_registry("tsk_1");
2291        let (tx, _rx) = oneshot::channel();
2292        r.set_input_required("tsk_1", "Which file?".to_string(), tx)
2293            .unwrap();
2294        let task = r.get_task("tsk_1").unwrap();
2295        let artifacts = task.artifacts.unwrap();
2296        assert_eq!(artifacts.len(), 1);
2297        assert_eq!(
2298            serde_json::to_value(&artifacts[0]).unwrap(),
2299            serde_json::json!({"artifactId": "prompt", "name": "prompt",
2300                "parts": [{"text": "Which file?", "mediaType": "text/plain"}]})
2301        );
2302        assert!(task.status.message.is_none());
2303    }
2304
2305    /// A live task carries nothing beyond its state: no artifact and no status message.
2306    #[test]
2307    fn a_working_task_carries_no_artifact_and_no_message() {
2308        let r = running_registry("tsk_1");
2309        let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2310        assert_bare_status(&task["status"], "TASK_STATE_WORKING");
2311        assert_eq!(
2312            task,
2313            serde_json::json!({"id": "tsk_1", "contextId": "ctx_001", "status": {
2314                "state": "TASK_STATE_WORKING",
2315                "timestamp": timestamp_of(&r, "tsk_1"),
2316            }})
2317        );
2318    }
2319
2320    /// A completed task answers with its response as the `response` artifact and its final
2321    /// message as `status.message`, an A2A message from the agent.
2322    #[test]
2323    fn a_completed_task_carries_its_response_artifact_and_status_message() {
2324        let mut r = running_registry("tsk_1");
2325        r.finish_task(TaskState::Completed);
2326        r.record_ending("tsk_1", "done", Some("the answer is 4"), false, &[], &[]);
2327        let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2328        assert_eq!(
2329            task,
2330            serde_json::json!({
2331                "id": "tsk_1",
2332                "contextId": "ctx_001",
2333                "status": {
2334                    "state": "TASK_STATE_COMPLETED",
2335                    "message": {
2336                        "messageId": "msg_tsk_1_status",
2337                        "contextId": "ctx_001",
2338                        "taskId": "tsk_1",
2339                        "role": "ROLE_AGENT",
2340                        "parts": [{"text": "done", "mediaType": "text/plain"}],
2341                    },
2342                    "timestamp": status_timestamp(r.status_times["tsk_1"]),
2343                },
2344                "artifacts": [{
2345                    "artifactId": "response",
2346                    "name": "response",
2347                    "parts": [{"text": "the answer is 4", "mediaType": "text/plain"}],
2348                }],
2349            })
2350        );
2351    }
2352
2353    /// A task that ended with background commands running names each in
2354    /// `metadata.murmur.runningCommands`, its text cut to 200 characters; a task with none
2355    /// carries no such key.
2356    #[test]
2357    fn a_task_ending_with_commands_running_names_them_in_its_metadata() {
2358        let long = "x".repeat(250);
2359        let running = [
2360            RunningCommand {
2361                work_id: "wrk_0a".to_string(),
2362                binary: "/usr/bin/bash".to_string(),
2363                command: "sleep 300".to_string(),
2364                running_ms: 12_000,
2365            },
2366            RunningCommand {
2367                work_id: "wrk_0b".to_string(),
2368                binary: "/usr/bin/bash".to_string(),
2369                command: long.clone(),
2370                running_ms: 3_000,
2371            },
2372        ];
2373        let mut r = running_registry("tsk_1");
2374        r.finish_task(TaskState::Completed);
2375        r.record_ending("tsk_1", "done", Some("4"), false, &[], &running);
2376        let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2377        assert_eq!(
2378            task["metadata"],
2379            serde_json::json!({"murmur": {"runningCommands": [
2380                {"workId": "wrk_0a", "binary": "/usr/bin/bash", "command": "sleep 300",
2381                 "runningMs": 12_000},
2382                {"workId": "wrk_0b", "binary": "/usr/bin/bash",
2383                 "command": format!("{}…", &long[..200]), "runningMs": 3_000},
2384            ]}})
2385        );
2386
2387        let mut r = running_registry("tsk_2");
2388        r.finish_task(TaskState::Completed);
2389        r.record_ending("tsk_2", "done", Some("4"), false, &[], &[]);
2390        let task = serde_json::to_value(r.get_task("tsk_2").unwrap()).unwrap();
2391        assert!(task.get("metadata").is_none(), "{task}");
2392    }
2393
2394    /// A task ended through `end-without-answer` carries `metadata.murmur.noAnswer` and the
2395    /// members further down that gave it none; a completed task with members missing below it
2396    /// carries its response and `noAnswerBelow` but no `noAnswer`; any other ending carries no
2397    /// `metadata` at all.
2398    #[test]
2399    fn a_no_answer_ending_carries_its_metadata_and_no_other_ending_does() {
2400        let below = [("q".to_string(), "timed_out".to_string())];
2401        let mut r = running_registry("tsk_1");
2402        r.finish_task(TaskState::Failed);
2403        r.record_ending("tsk_1", "q gave no answer", None, true, &below, &[]);
2404        let task = serde_json::to_value(r.get_task("tsk_1").unwrap()).unwrap();
2405        assert_eq!(task["status"]["state"], "TASK_STATE_FAILED");
2406        assert_eq!(
2407            task["status"]["message"]["parts"][0]["text"],
2408            "q gave no answer"
2409        );
2410        assert_eq!(
2411            task["metadata"],
2412            serde_json::json!({"murmur": {
2413                "noAnswer": true,
2414                "noAnswerBelow": [{"member": "q", "status": "timed_out"}],
2415            }})
2416        );
2417
2418        let mut r = running_registry("tsk_2");
2419        r.finish_task(TaskState::Completed);
2420        r.record_ending(
2421            "tsk_2",
2422            "task completed",
2423            Some("none came"),
2424            false,
2425            &below,
2426            &[],
2427        );
2428        let task = serde_json::to_value(r.get_task("tsk_2").unwrap()).unwrap();
2429        assert_eq!(
2430            task["artifacts"],
2431            serde_json::json!([{"artifactId": "response", "name": "response",
2432                "parts": [{"text": "none came", "mediaType": "text/plain"}]}])
2433        );
2434        assert_eq!(
2435            task["metadata"],
2436            serde_json::json!({"murmur": {
2437                "noAnswerBelow": [{"member": "q", "status": "timed_out"}],
2438            }})
2439        );
2440
2441        for state in [TaskState::Completed, TaskState::Failed] {
2442            let mut r = running_registry("tsk_3");
2443            r.finish_task(state);
2444            r.record_ending("tsk_3", "ended", Some("4"), false, &[], &[]);
2445            let task = serde_json::to_value(r.get_task("tsk_3").unwrap()).unwrap();
2446            assert!(task.get("metadata").is_none(), "{task}");
2447        }
2448        let r = running_registry("tsk_4");
2449        let task = serde_json::to_value(r.get_task("tsk_4").unwrap()).unwrap();
2450        assert!(task.get("metadata").is_none(), "{task}");
2451    }
2452
2453    /// A task that did not complete carries its message and no response, whatever was passed.
2454    #[test]
2455    fn a_failed_task_carries_its_status_message_and_no_response() {
2456        let mut r = running_registry("tsk_1");
2457        r.finish_task(TaskState::Failed);
2458        r.record_ending(
2459            "tsk_1",
2460            "the driver failed",
2461            Some("partial"),
2462            false,
2463            &[],
2464            &[],
2465        );
2466        let task = r.get_task("tsk_1").unwrap();
2467        assert!(task.artifacts.is_none());
2468        let message = task.status.message.unwrap();
2469        assert_eq!(message.role, Role::Agent);
2470        assert_eq!(message.text(), Some("the driver failed"));
2471    }
2472
2473    /// A completed task with no response text, or no recorded ending, carries no artifact.
2474    #[test]
2475    fn a_completed_task_without_a_response_carries_no_artifact() {
2476        let mut r = running_registry("tsk_1");
2477        r.finish_task(TaskState::Completed);
2478        assert!(r.get_task("tsk_1").unwrap().artifacts.is_none());
2479        r.record_ending("tsk_1", "ok", Some(""), false, &[], &[]);
2480        let task = r.get_task("tsk_1").unwrap();
2481        assert!(task.artifacts.is_none());
2482        assert_eq!(task.status.message.unwrap().text(), Some("ok"));
2483    }
2484
2485    #[test]
2486    fn an_owner_is_recorded_per_task() {
2487        let mut r = make_registry();
2488        r.enqueue("tsk_a", "ctx_001");
2489        r.record_owner("tsk_a", "member:lead");
2490        r.enqueue("tsk_b", "ctx_001");
2491        assert_eq!(r.owner("tsk_a"), Some("member:lead"));
2492        assert_eq!(r.owner("tsk_b"), None);
2493        r.withdraw("tsk_a");
2494        assert_eq!(r.owner("tsk_a"), None);
2495        assert!(r.get_task("tsk_a").is_none());
2496        assert_eq!(r.pending_count, 1);
2497    }
2498
2499    /// The ids of a page, in order.
2500    fn listed(page: &TaskPage) -> Vec<&str> {
2501        page.tasks.iter().map(|task| task.id.as_str()).collect()
2502    }
2503
2504    fn every_task() -> TaskFilter {
2505        TaskFilter {
2506            context_id: None,
2507            state: StateFilter::Any,
2508            status_after: None,
2509        }
2510    }
2511
2512    /// Every change to a task's state or ending moves it to the front of the listing.
2513    #[test]
2514    fn list_tasks_orders_by_the_last_change_newest_first() {
2515        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2516        for id in ["tsk_a", "tsk_b", "tsk_c"] {
2517            r.enqueue(id, "ctx_001");
2518        }
2519        let all = |r: &TaskRegistry| -> Vec<String> {
2520            listed(&r.list_tasks(&every_task(), None, 10, |_| true))
2521                .into_iter()
2522                .map(str::to_string)
2523                .collect()
2524        };
2525        assert_eq!(all(&r), ["tsk_c", "tsk_b", "tsk_a"]);
2526        r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::User);
2527        assert_eq!(all(&r), ["tsk_a", "tsk_c", "tsk_b"]);
2528        r.request_cancel("tsk_b");
2529        assert_eq!(all(&r), ["tsk_b", "tsk_a", "tsk_c"]);
2530        let (tx, _rx) = oneshot::channel();
2531        r.set_input_required("tsk_a", "which?".to_string(), tx)
2532            .unwrap();
2533        assert_eq!(all(&r), ["tsk_a", "tsk_b", "tsk_c"]);
2534        r.deliver_input("tsk_a", "left".to_string()).unwrap();
2535        r.finish_task(TaskState::Completed);
2536        r.record_ending("tsk_c", "", None, false, &[], &[]);
2537        assert_eq!(all(&r), ["tsk_c", "tsk_a", "tsk_b"]);
2538        r.record_ending("tsk_a", "done", Some("four"), false, &[], &[]);
2539        assert_eq!(all(&r), ["tsk_a", "tsk_c", "tsk_b"]);
2540        r.close_to_new_work();
2541        assert_eq!(all(&r), ["tsk_c", "tsk_a", "tsk_b"], "tsk_c was rejected");
2542
2543        let page = r.list_tasks(&every_task(), None, 10, |_| true);
2544        for task in &page.tasks {
2545            assert_eq!(
2546                serde_json::to_value(task).unwrap(),
2547                serde_json::to_value(r.get_task(&task.id).unwrap()).unwrap()
2548            );
2549        }
2550    }
2551
2552    /// Pages follow one another through the cursor; a task that changes during the walk moves
2553    /// ahead of the cursor and is not returned a second time.
2554    #[test]
2555    fn list_tasks_walks_pages_and_returns_a_task_changed_mid_walk_once() {
2556        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2557        for id in ["tsk_1", "tsk_2", "tsk_3", "tsk_4", "tsk_5"] {
2558            r.enqueue(id, "ctx_001");
2559        }
2560        let first = r.list_tasks(&every_task(), None, 2, |_| true);
2561        assert_eq!(listed(&first), ["tsk_5", "tsk_4"]);
2562        assert_eq!(first.total, 5);
2563        let cursor = first.next.clone().expect("more tasks lie past the page");
2564        assert_eq!(ListCursor::decode(&cursor.encode()), Some(cursor.clone()));
2565
2566        r.request_cancel("tsk_4");
2567        r.request_cancel("tsk_2");
2568        let second = r.list_tasks(&every_task(), Some(&cursor), 2, |_| true);
2569        assert_eq!(listed(&second), ["tsk_3", "tsk_1"], "tsk_2 moved ahead");
2570        assert_eq!(second.next, None);
2571        assert_eq!(second.total, 5);
2572        let fresh = r.list_tasks(&every_task(), None, 2, |_| true);
2573        assert_eq!(listed(&fresh), ["tsk_2", "tsk_4"]);
2574
2575        let exact = r.list_tasks(&every_task(), None, 5, |_| true);
2576        assert_eq!(exact.tasks.len(), 5);
2577        assert_eq!(
2578            exact.next, None,
2579            "no task lies past a page that ends the list"
2580        );
2581    }
2582
2583    /// The instant a status timestamp names, parsed back.
2584    fn instant(timestamp: &str) -> DateTime<Utc> {
2585        DateTime::parse_from_rfc3339(timestamp)
2586            .unwrap()
2587            .with_timezone(&Utc)
2588    }
2589
2590    fn timestamp_of(r: &TaskRegistry, task_id: &str) -> String {
2591        r.get_task(task_id).unwrap().status.timestamp.unwrap()
2592    }
2593
2594    /// Long enough that two stamps taken either side of it differ at millisecond precision.
2595    fn a_tick() {
2596        std::thread::sleep(std::time::Duration::from_millis(3));
2597    }
2598
2599    /// The status timestamp and the revision are stamped together: each change moves both, and
2600    /// nothing else moves either.
2601    #[test]
2602    fn a_status_timestamp_moves_with_the_revision() {
2603        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2604        r.enqueue("tsk_a", "ctx_001");
2605        let submitted = timestamp_of(&r, "tsk_a");
2606        let submitted_revision = r.revisions["tsk_a"];
2607        assert_eq!(r.status_times["tsk_a"], instant(&submitted));
2608
2609        a_tick();
2610        r.record_owner("tsk_a", "alice");
2611        assert_eq!(
2612            timestamp_of(&r, "tsk_a"),
2613            submitted,
2614            "an owner is no change"
2615        );
2616        assert_eq!(r.revisions["tsk_a"], submitted_revision);
2617
2618        r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::User);
2619        let working = timestamp_of(&r, "tsk_a");
2620        assert!(r.revisions["tsk_a"] > submitted_revision);
2621        assert!(
2622            instant(&working) > instant(&submitted),
2623            "{submitted} → {working}"
2624        );
2625
2626        a_tick();
2627        r.finish_task(TaskState::Completed);
2628        let completed_revision = r.revisions["tsk_a"];
2629        let completed = timestamp_of(&r, "tsk_a");
2630        assert!(instant(&completed) > instant(&working));
2631
2632        a_tick();
2633        r.record_ending("tsk_a", "done", Some("four"), false, &[], &[]);
2634        assert!(r.revisions["tsk_a"] > completed_revision);
2635        assert!(instant(&timestamp_of(&r, "tsk_a")) > instant(&completed));
2636    }
2637
2638    /// GetTask's string is the stored instant exactly, so a cutoff copied from it includes the
2639    /// task: the comparison is "at or after".
2640    #[test]
2641    fn a_status_timestamp_round_trips_and_a_cutoff_equal_to_it_includes_the_task() {
2642        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2643        r.enqueue("tsk_a", "ctx_001");
2644        let timestamp = timestamp_of(&r, "tsk_a");
2645        assert!(timestamp.ends_with('Z'), "{timestamp}");
2646        assert_eq!(
2647            timestamp.len(),
2648            "2026-10-13T09:00:00.123Z".len(),
2649            "{timestamp}"
2650        );
2651        assert_eq!(instant(&timestamp), r.status_times["tsk_a"]);
2652        let at = TaskFilter {
2653            status_after: Some(instant(&timestamp)),
2654            ..every_task()
2655        };
2656        assert_eq!(listed(&r.list_tasks(&at, None, 10, |_| true)), ["tsk_a"]);
2657        let after = TaskFilter {
2658            status_after: Some(instant(&timestamp) + chrono::Duration::milliseconds(1)),
2659            ..every_task()
2660        };
2661        assert!(r.list_tasks(&after, None, 10, |_| true).tasks.is_empty());
2662    }
2663
2664    #[test]
2665    fn withdraw_drops_the_status_time() {
2666        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2667        r.enqueue("tsk_a", "ctx_001");
2668        r.withdraw("tsk_a");
2669        assert!(!r.status_times.contains_key("tsk_a"));
2670        assert!(r.get_task("tsk_a").is_none());
2671    }
2672
2673    /// `status_after` filters before paging, so `total` counts only the tasks it keeps and the
2674    /// cursor walks only those; it combines with the other filters.
2675    #[test]
2676    fn list_tasks_filters_by_status_time_before_paging() {
2677        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2678        for id in ["tsk_1", "tsk_2", "tsk_3", "tsk_4"] {
2679            r.enqueue(id, "ctx_001");
2680        }
2681        r.request_cancel("tsk_3");
2682        let base = instant("2026-10-13T09:00:00Z");
2683        for (offset, id) in ["tsk_1", "tsk_2", "tsk_3", "tsk_4"].iter().enumerate() {
2684            r.status_times.insert(
2685                id.to_string(),
2686                base + chrono::Duration::seconds(offset as i64),
2687            );
2688        }
2689        let since = |seconds: i64| TaskFilter {
2690            status_after: Some(base + chrono::Duration::seconds(seconds)),
2691            ..every_task()
2692        };
2693        let first = r.list_tasks(&since(1), None, 2, |_| true);
2694        assert_eq!(listed(&first), ["tsk_3", "tsk_4"]);
2695        assert_eq!(first.total, 3);
2696        let second = r.list_tasks(&since(1), first.next.as_ref(), 2, |_| true);
2697        assert_eq!(listed(&second), ["tsk_2"]);
2698        assert_eq!((second.total, second.next), (3, None));
2699
2700        let canceled_since = TaskFilter {
2701            state: StateFilter::Only(TaskState::Canceled),
2702            ..since(1)
2703        };
2704        let page = r.list_tasks(&canceled_since, None, 10, |_| true);
2705        assert_eq!((listed(&page), page.total), (vec!["tsk_3"], 1));
2706        assert_eq!(r.list_tasks(&since(4), None, 10, |_| true).total, 0);
2707        assert_eq!(r.list_tasks(&every_task(), None, 10, |_| true).total, 4);
2708    }
2709
2710    /// The filters and the visibility predicate decide the page and the total alike.
2711    #[test]
2712    fn list_tasks_filters_by_context_state_and_visibility() {
2713        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2714        r.enqueue("tsk_a", "ctx_one");
2715        r.record_owner("tsk_a", "alice");
2716        r.enqueue("tsk_b", "ctx_two");
2717        r.record_owner("tsk_b", "alice");
2718        r.enqueue("tsk_c", "ctx_one");
2719        r.record_owner("tsk_c", "bob");
2720        r.request_cancel("tsk_c");
2721        let alice = |task_id: &str| r.owner(task_id) == Some("alice");
2722        let page = r.list_tasks(&every_task(), None, 10, alice);
2723        assert_eq!((listed(&page), page.total), (vec!["tsk_b", "tsk_a"], 2));
2724        let in_one = TaskFilter {
2725            context_id: Some("ctx_one".to_string()),
2726            state: StateFilter::Any,
2727            status_after: None,
2728        };
2729        let page = r.list_tasks(&in_one, None, 10, |_| true);
2730        assert_eq!((listed(&page), page.total), (vec!["tsk_c", "tsk_a"], 2));
2731        let canceled = TaskFilter {
2732            context_id: None,
2733            state: StateFilter::Only(TaskState::Canceled),
2734            status_after: None,
2735        };
2736        let page = r.list_tasks(&canceled, None, 10, |_| true);
2737        assert_eq!(listed(&page), ["tsk_c"]);
2738        let page = r.list_tasks(&canceled, None, 10, alice);
2739        assert_eq!((page.tasks.len(), page.total, page.next), (0, 0, None));
2740        let nothing = TaskFilter {
2741            context_id: None,
2742            state: StateFilter::Nothing,
2743            status_after: None,
2744        };
2745        assert_eq!(r.list_tasks(&nothing, None, 10, |_| true).total, 0);
2746    }
2747
2748    #[test]
2749    fn list_cursor_reads_only_what_it_wrote() {
2750        let cursor = ListCursor {
2751            revision: 42,
2752            task_id: "tsk_01a1".to_string(),
2753        };
2754        let token = cursor.encode();
2755        assert!(!token.contains('='), "{token}");
2756        assert_eq!(ListCursor::decode(&token), Some(cursor));
2757        use base64::Engine as _;
2758        let encode = |text: &str| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(text);
2759        for garbage in [
2760            "not-a-token".to_string(),
2761            "".to_string(),
2762            "!!".to_string(),
2763            encode("42"),
2764            encode("42:"),
2765            encode("x:tsk_1"),
2766            encode("-1:tsk_1"),
2767            encode("+1:tsk_1"),
2768        ] {
2769            assert_eq!(ListCursor::decode(&garbage), None, "{garbage}");
2770        }
2771    }
2772
2773    #[test]
2774    fn list_state_filter_reads_every_v1_state_name() {
2775        for state in TaskState::ALL {
2776            assert_eq!(
2777                TaskState::from_wire_name(state.wire_name()),
2778                Some(state.clone())
2779            );
2780            assert_eq!(
2781                StateFilter::from_wire_name(state.wire_name()),
2782                Some(StateFilter::Only(state))
2783            );
2784        }
2785        assert_eq!(
2786            StateFilter::from_wire_name("TASK_STATE_UNSPECIFIED"),
2787            Some(StateFilter::Any)
2788        );
2789        assert_eq!(
2790            StateFilter::from_wire_name("TASK_STATE_AUTH_REQUIRED"),
2791            Some(StateFilter::Nothing)
2792        );
2793        for other in ["done", "completed", "task_state_completed", ""] {
2794            assert_eq!(StateFilter::from_wire_name(other), None, "{other}");
2795            assert_eq!(TaskState::from_wire_name(other), None, "{other}");
2796        }
2797    }
2798
2799    #[test]
2800    fn request_cancel_on_a_running_task_records_canceled() {
2801        let mut r = running_registry("tsk_001");
2802        let signal = r.cancel_watch("tsk_001");
2803        assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::Accepted);
2804        assert!(signal.is_canceled());
2805        assert!(r.is_canceled("tsk_001"));
2806        let task = r.get_task("tsk_001").unwrap();
2807        assert_eq!(task.status.state, TaskState::Canceled);
2808    }
2809
2810    #[test]
2811    fn request_cancel_on_a_completed_task_leaves_it_alone() {
2812        let mut r = running_registry("tsk_001");
2813        r.finish_task(TaskState::Completed);
2814        assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::AlreadyTerminal);
2815        assert_eq!(
2816            r.get_task("tsk_001").unwrap().status.state,
2817            TaskState::Completed
2818        );
2819        // And a second cancel of an already-cancelled task says the same thing.
2820        let mut r = running_registry("tsk_002");
2821        assert_eq!(r.request_cancel("tsk_002"), CancelOutcome::Accepted);
2822        assert_eq!(r.request_cancel("tsk_002"), CancelOutcome::AlreadyTerminal);
2823    }
2824
2825    #[test]
2826    fn cancel_every_live_cancels_live_tasks_in_order_and_leaves_terminal_ones() {
2827        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2828        r.enqueue("tsk_c", "ctx_001");
2829        r.enqueue("tsk_a", "ctx_001");
2830        r.enqueue("tsk_done", "ctx_001");
2831        r.start_task("tsk_done".to_string(), "ctx_001".to_string(), TaskLane::Bg);
2832        r.finish_task(TaskState::Completed);
2833        r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::Bg);
2834        let running = r.cancel_watch("tsk_a");
2835
2836        assert_eq!(r.cancel_every_live(false), vec!["tsk_a", "tsk_c"]);
2837        assert!(running.is_canceled());
2838        assert!(r.is_canceled("tsk_c"));
2839        assert_eq!(
2840            r.get_task("tsk_done").unwrap().status.state,
2841            TaskState::Completed
2842        );
2843        assert!(r.cancel_every_live(false).is_empty());
2844    }
2845
2846    /// The deadline spares a running task holding a standing answer: its signal is raised, its
2847    /// history stays non-terminal, and the state the task loop then reports is the one recorded.
2848    #[test]
2849    fn cancel_every_live_at_the_deadline_spares_a_task_holding_a_standing_answer() {
2850        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2851        r.enqueue("tsk_a", "ctx_001");
2852        r.enqueue("tsk_b", "ctx_001");
2853        r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::Bg);
2854        let running = r.cancel_watch("tsk_a");
2855        r.mark_standing_answer("tsk_a");
2856
2857        assert_eq!(r.cancel_every_live(true), vec!["tsk_b"]);
2858        assert!(running.is_canceled());
2859        assert!(!r.is_canceled("tsk_a"));
2860        assert!(!r.get_task("tsk_a").unwrap().status.state.is_terminal());
2861        assert!(r.stopped_at_deadline("tsk_a"));
2862        assert!(
2863            r.is_canceled("tsk_b"),
2864            "an unmarked task is cancelled at once"
2865        );
2866
2867        assert_eq!(
2868            r.finish_task(TaskState::Completed),
2869            Some(TaskState::Completed)
2870        );
2871        assert_eq!(
2872            r.get_task("tsk_a").unwrap().status.state,
2873            TaskState::Completed
2874        );
2875        assert!(
2876            !r.stopped_at_deadline("tsk_a"),
2877            "finish_task clears the mark"
2878        );
2879    }
2880
2881    /// The deadline marks every task it cancels, and only those: a task a person cancelled first
2882    /// keeps its own cancel, and no other termination marks anything.
2883    #[test]
2884    fn cancel_every_live_at_the_deadline_marks_the_tasks_it_cancels() {
2885        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
2886        r.enqueue("tsk_a", "ctx_001");
2887        r.enqueue("tsk_b", "ctx_001");
2888        r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::Bg);
2889        assert_eq!(r.request_cancel("tsk_b"), CancelOutcome::Accepted);
2890
2891        assert_eq!(r.cancel_every_live(true), vec!["tsk_a"]);
2892        assert!(r.canceled_at_deadline("tsk_a"));
2893        assert!(
2894            !r.canceled_at_deadline("tsk_b"),
2895            "a person's cancel came first"
2896        );
2897        assert_eq!(
2898            r.finish_task(TaskState::Completed),
2899            Some(TaskState::Canceled)
2900        );
2901        assert!(
2902            !r.canceled_at_deadline("tsk_a"),
2903            "finish_task clears the mark"
2904        );
2905
2906        let mut r = running_registry("tsk_001");
2907        assert_eq!(r.cancel_every_live(false), vec!["tsk_001"]);
2908        assert!(!r.canceled_at_deadline("tsk_001"));
2909    }
2910
2911    /// Only the deadline spares: every other termination cancels a marked task at once.
2912    #[test]
2913    fn cancel_every_live_without_sparing_cancels_a_task_holding_a_standing_answer() {
2914        let mut r = running_registry("tsk_001");
2915        r.mark_standing_answer("tsk_001");
2916        assert_eq!(r.cancel_every_live(false), vec!["tsk_001"]);
2917        assert!(r.is_canceled("tsk_001"));
2918        assert!(!r.stopped_at_deadline("tsk_001"));
2919        assert_eq!(
2920            r.finish_task(TaskState::Completed),
2921            Some(TaskState::Canceled)
2922        );
2923    }
2924
2925    /// A cleared mark is not spared.
2926    #[test]
2927    fn cancel_every_live_at_the_deadline_cancels_a_task_whose_mark_was_cleared() {
2928        let mut r = running_registry("tsk_001");
2929        r.mark_standing_answer("tsk_001");
2930        r.clear_standing_answer("tsk_001");
2931        assert_eq!(r.cancel_every_live(true), vec!["tsk_001"]);
2932        assert!(r.is_canceled("tsk_001"));
2933        assert!(!r.stopped_at_deadline("tsk_001"));
2934    }
2935
2936    #[test]
2937    fn request_cancel_on_an_unknown_id_is_unknown() {
2938        let mut r = make_registry();
2939        assert_eq!(r.request_cancel("tsk_doesnotexist"), CancelOutcome::Unknown);
2940        assert!(!r.is_canceled("tsk_doesnotexist"));
2941    }
2942
2943    #[test]
2944    fn finish_task_does_not_overwrite_an_accepted_cancel() {
2945        let mut r = running_registry("tsk_001");
2946        assert_eq!(r.request_cancel("tsk_001"), CancelOutcome::Accepted);
2947        assert_eq!(
2948            r.finish_task(TaskState::Completed),
2949            Some(TaskState::Canceled),
2950            "the state recorded is the accepted cancel, and the caller is told so"
2951        );
2952        assert_eq!(
2953            r.get_task("tsk_001").unwrap().status.state,
2954            TaskState::Canceled
2955        );
2956        assert_eq!(
2957            r.finish_task(TaskState::Failed),
2958            None,
2959            "a slot that is no longer running records nothing"
2960        );
2961        assert_eq!(
2962            r.get_task("tsk_001").unwrap().status.state,
2963            TaskState::Canceled
2964        );
2965    }
2966
2967    #[test]
2968    fn finish_task_returns_the_state_it_recorded() {
2969        let mut r = running_registry("tsk_001");
2970        assert_eq!(
2971            r.finish_task(TaskState::Completed),
2972            Some(TaskState::Completed)
2973        );
2974        assert_eq!(make_registry().finish_task(TaskState::Failed), None);
2975    }
2976
2977    #[test]
2978    fn an_input_timeout_leaves_the_task_working_and_is_read_once() {
2979        let mut r = running_registry("tsk_001");
2980        let (tx, _rx) = oneshot::channel();
2981        r.set_input_required("tsk_001", "prompt".into(), tx)
2982            .unwrap();
2983        r.record_input_timeout("tsk_001");
2984        assert_eq!(
2985            r.get_task("tsk_001").unwrap().status.state,
2986            TaskState::Working,
2987            "the task's terminal state is not the wait's to record"
2988        );
2989        assert!(
2990            r.get_input_prompt("tsk_001").is_none(),
2991            "the waiter is gone"
2992        );
2993        assert!(r.take_input_timeout("tsk_001"));
2994        assert!(
2995            !r.take_input_timeout("tsk_001"),
2996            "a reopened attempt does not end on the same timeout"
2997        );
2998        assert!(!r.take_input_timeout("tsk_other"));
2999    }
3000
3001    #[test]
3002    fn cancelling_a_queued_task_frees_its_queue_slot() {
3003        let mut r = TaskRegistry::new(2, TaskAcceptance::Queue);
3004        r.enqueue("tsk_a", "ctx_001");
3005        r.enqueue("tsk_b", "ctx_001");
3006        assert!(!r.can_accept(), "the queue is full");
3007        assert_eq!(r.request_cancel("tsk_b"), CancelOutcome::Accepted);
3008        assert!(r.can_accept(), "a cancelled task holds no slot");
3009    }
3010
3011    #[test]
3012    fn close_to_new_work_rejects_every_submitted_task_and_nothing_else() {
3013        let mut r = TaskRegistry::new(4, TaskAcceptance::Queue);
3014        for id in ["tsk_a", "tsk_b", "tsk_c", "tsk_d"] {
3015            r.enqueue(id, &format!("ctx_{id}"));
3016        }
3017        r.start_task("tsk_a".to_string(), "ctx_tsk_a".to_string(), TaskLane::Bg);
3018        let (tx, _rx) = oneshot::channel();
3019        r.set_input_required("tsk_a", "prompt".into(), tx).unwrap();
3020        assert_eq!(r.request_cancel("tsk_c"), CancelOutcome::Accepted);
3021        let pending_before = r.pending_count;
3022
3023        let refused = r.close_to_new_work();
3024
3025        assert_eq!(
3026            refused,
3027            vec![
3028                ("tsk_b".to_string(), "ctx_tsk_b".to_string()),
3029                ("tsk_d".to_string(), "ctx_tsk_d".to_string()),
3030            ]
3031        );
3032        for id in ["tsk_b", "tsk_d"] {
3033            assert_eq!(r.get_task(id).unwrap().status.state, TaskState::Rejected);
3034        }
3035        assert_eq!(r.pending_count, pending_before - 2);
3036        assert_eq!(r.pending_count, 0);
3037        assert_eq!(
3038            r.get_task("tsk_a").unwrap().status.state,
3039            TaskState::InputRequired,
3040            "a live task that is not submitted is left alone"
3041        );
3042        assert_eq!(
3043            r.get_task("tsk_c").unwrap().status.state,
3044            TaskState::Canceled
3045        );
3046        assert_eq!(r.request_cancel("tsk_b"), CancelOutcome::AlreadyTerminal);
3047        assert!(!r.is_canceled("tsk_b"));
3048    }
3049
3050    #[test]
3051    fn a_closed_registry_accepts_nothing_and_closes_once() {
3052        for (depth, mode) in [(1, TaskAcceptance::Single), (4, TaskAcceptance::Queue)] {
3053            let mut r = TaskRegistry::new(depth, mode.clone());
3054            assert!(r.can_accept());
3055            assert!(!r.is_closed());
3056            r.enqueue("tsk_a", "ctx_001");
3057            assert_eq!(r.close_to_new_work().len(), 1);
3058            assert!(r.is_closed());
3059            assert!(!r.can_accept(), "{mode:?}: a closed registry refuses work");
3060            assert!(r.close_to_new_work().is_empty(), "{mode:?}: closes once");
3061            assert!(!r.can_accept());
3062        }
3063    }
3064
3065    /// `LifecycleConfig::tasks_held_at_once` is what `W-ROS-001` compares a member's callers
3066    /// against; this registry is what actually rejects a call. Filling a registry the way the
3067    /// door and the task loop do must accept exactly that many tasks, so a change to either rule
3068    /// fails here instead of leaving the warning wrong.
3069    #[test]
3070    fn tasks_held_at_once_matches_what_the_registry_accepts() {
3071        const SAFETY_CAP: usize = 64;
3072        for (mode, depth, expected) in [
3073            (TaskAcceptance::None, 1, 0),
3074            (TaskAcceptance::Single, 1, 1),
3075            (TaskAcceptance::Queue, 1, 2),
3076            (TaskAcceptance::Queue, 3, 4),
3077        ] {
3078            let mut r = TaskRegistry::new(depth, mode.clone());
3079            let mut accepted = 0;
3080            while r.can_accept() && accepted < SAFETY_CAP {
3081                let task_id = format!("tsk_{accepted}");
3082                r.enqueue(&task_id, "ctx_001");
3083                if accepted == 0 {
3084                    r.start_task(task_id, "ctx_001".to_string(), TaskLane::Peer);
3085                }
3086                accepted += 1;
3087            }
3088            let lifecycle = murmur_artifact::LifecycleConfig {
3089                task_acceptance: mode.clone(),
3090                queue_depth: depth,
3091                ..Default::default()
3092            };
3093            assert_eq!(accepted, expected, "{mode:?} at depth {depth}");
3094            assert_eq!(
3095                accepted,
3096                lifecycle.tasks_held_at_once(),
3097                "{mode:?} at depth {depth}: tasks_held_at_once drifted from the registry"
3098            );
3099        }
3100    }
3101
3102    #[test]
3103    fn task_state_serializes_as_its_wire_name_and_reads_only_that() {
3104        for state in TaskState::ALL {
3105            let wire = serde_json::to_value(&state).unwrap();
3106            assert_eq!(wire, serde_json::json!(state.wire_name()));
3107            assert_eq!(
3108                serde_json::from_value::<TaskState>(wire).unwrap(),
3109                state,
3110                "{state:?}"
3111            );
3112            assert!(
3113                serde_json::from_value::<TaskState>(serde_json::json!(state.as_str())).is_err(),
3114                "{state:?}: murmur's word is not a wire state"
3115            );
3116        }
3117        for refused in [
3118            serde_json::json!("TASK_STATE_UNSPECIFIED"),
3119            serde_json::json!("TASK_STATE_AUTH_REQUIRED"),
3120            serde_json::json!("task_state_working"),
3121            serde_json::json!(1),
3122            serde_json::json!(null),
3123        ] {
3124            assert!(
3125                serde_json::from_value::<TaskState>(refused.clone()).is_err(),
3126                "{refused}"
3127            );
3128        }
3129    }
3130
3131    #[test]
3132    fn task_state_as_str_is_murmurs_word() {
3133        let words: Vec<&str> = TaskState::ALL.iter().map(TaskState::as_str).collect();
3134        assert_eq!(
3135            words,
3136            [
3137                "submitted",
3138                "working",
3139                "completed",
3140                "failed",
3141                "canceled",
3142                "input-required",
3143                "rejected"
3144            ]
3145        );
3146    }
3147
3148    #[test]
3149    fn murmur_state_name_maps_the_eight_v1_states_and_nothing_else() {
3150        for state in TaskState::ALL {
3151            assert_eq!(murmur_state_name(state.wire_name()), Some(state.as_str()));
3152        }
3153        assert_eq!(
3154            murmur_state_name("TASK_STATE_AUTH_REQUIRED"),
3155            Some("auth-required")
3156        );
3157        for other in [
3158            "TASK_STATE_UNSPECIFIED",
3159            "working",
3160            "",
3161            "TASK_STATE_working",
3162        ] {
3163            assert_eq!(murmur_state_name(other), None, "{other:?}");
3164        }
3165    }
3166
3167    #[test]
3168    fn roles_serialize_as_their_proto_names() {
3169        assert_eq!(
3170            serde_json::to_value(Role::User).unwrap(),
3171            serde_json::json!("ROLE_USER")
3172        );
3173        assert_eq!(
3174            serde_json::to_value(Role::Agent).unwrap(),
3175            serde_json::json!("ROLE_AGENT")
3176        );
3177        assert!(serde_json::from_value::<Role>(serde_json::json!("agent")).is_err());
3178    }
3179
3180    #[test]
3181    fn finish_task_cleans_up_input_waiters() {
3182        let mut r = running_registry("tsk_001");
3183        let (tx, _rx) = oneshot::channel();
3184        r.set_input_required("tsk_001", "prompt".into(), tx)
3185            .unwrap();
3186        r.finish_task(TaskState::Failed);
3187        // input_waiters should be cleaned up
3188        assert!(r.get_input_prompt("tsk_001").is_none());
3189    }
3190
3191    #[test]
3192    fn a2a_error_table_is_the_spec_table_in_code_order() {
3193        let table: Vec<(i32, &str)> = A2aError::ALL
3194            .into_iter()
3195            .map(|error| (error.code(), error.reason()))
3196            .collect();
3197        assert_eq!(
3198            table,
3199            [
3200                (-32001, "TASK_NOT_FOUND"),
3201                (-32002, "TASK_NOT_CANCELABLE"),
3202                (-32003, "PUSH_NOTIFICATION_NOT_SUPPORTED"),
3203                (-32004, "UNSUPPORTED_OPERATION"),
3204                (-32005, "CONTENT_TYPE_NOT_SUPPORTED"),
3205                (-32006, "INVALID_AGENT_RESPONSE"),
3206                (-32007, "EXTENDED_AGENT_CARD_NOT_CONFIGURED"),
3207                (-32008, "EXTENSION_SUPPORT_REQUIRED"),
3208                (-32009, "VERSION_NOT_SUPPORTED"),
3209            ]
3210        );
3211    }
3212
3213    #[test]
3214    fn a2a_error_from_code_round_trips_and_knows_no_other_code() {
3215        for error in A2aError::ALL {
3216            assert_eq!(A2aError::from_code(error.code()), Some(error));
3217        }
3218        for code in [-32000, -32010, -32099, -32600, -31001, 0] {
3219            assert_eq!(A2aError::from_code(code), None, "{code}");
3220        }
3221    }
3222
3223    #[test]
3224    fn a2a_murmur_errors_lie_outside_the_reserved_range() {
3225        let table: Vec<(i32, &str)> = MurmurError::ALL
3226            .into_iter()
3227            .map(|error| (error.code(), error.reason()))
3228            .collect();
3229        assert_eq!(
3230            table,
3231            [
3232                (-31001, "COMPLETION_MISADDRESSED"),
3233                (-31002, "COMPLETION_NOT_AWAITED")
3234            ]
3235        );
3236        for error in MurmurError::ALL {
3237            assert!(!(-32768..=-32000).contains(&error.code()), "{error:?}");
3238        }
3239    }
3240
3241    fn wire(response: &JsonRpcResponse) -> Value {
3242        serde_json::to_value(response).unwrap()
3243    }
3244
3245    #[test]
3246    fn a2a_error_carries_one_error_info_with_string_metadata() {
3247        let response = JsonRpcResponse::a2a_error(
3248            Value::from(7),
3249            A2aError::TaskNotCancelable,
3250            "Task cannot be canceled",
3251            &[("taskId", "tsk_1".into()), ("state", "completed".into())],
3252        );
3253        assert_eq!(
3254            wire(&response),
3255            serde_json::json!({
3256                "jsonrpc": "2.0",
3257                "id": 7,
3258                "error": {
3259                    "code": -32002,
3260                    "message": "Task cannot be canceled",
3261                    "data": [{
3262                        "@type": "type.googleapis.com/google.rpc.ErrorInfo",
3263                        "reason": "TASK_NOT_CANCELABLE",
3264                        "domain": "a2a-protocol.org",
3265                        "metadata": {"taskId": "tsk_1", "state": "completed"},
3266                    }],
3267                },
3268            })
3269        );
3270    }
3271
3272    #[test]
3273    fn a2a_murmur_error_is_in_the_murmur_domain() {
3274        let response = JsonRpcResponse::murmur_error(
3275            Value::from("a"),
3276            MurmurError::CompletionNotAwaited,
3277            "nobody waits",
3278            &[],
3279        );
3280        let error = &wire(&response)["error"];
3281        assert_eq!(error["code"], -31002);
3282        assert_eq!(error["data"][0]["reason"], "COMPLETION_NOT_AWAITED");
3283        assert_eq!(error["data"][0]["domain"], "murmur.nexus");
3284        assert_eq!(error["data"][0]["metadata"], serde_json::json!({}));
3285    }
3286
3287    #[test]
3288    fn a2a_standard_errors_carry_no_data() {
3289        let error = &wire(&JsonRpcResponse::err(
3290            Value::Null,
3291            -32601,
3292            "Method not found",
3293        ))["error"];
3294        assert!(error.get("data").is_none(), "{error}");
3295    }
3296
3297    fn refusal(body: &str) -> Value {
3298        wire(&JsonRpcRequest::from_body(body).expect_err(body))
3299    }
3300
3301    #[test]
3302    fn a2a_from_body_reads_a_request_and_defaults_params() {
3303        let req = JsonRpcRequest::from_body(
3304            r#"{"jsonrpc":"2.0","id":"x","method":"GetExtendedAgentCard","extra":1}"#,
3305        )
3306        .unwrap();
3307        assert_eq!(req.id, Value::from("x"));
3308        assert_eq!(req.method, "GetExtendedAgentCard");
3309        assert_eq!(req.params, serde_json::json!({}));
3310
3311        let req = JsonRpcRequest::from_body(
3312            r#"{"jsonrpc":"2.0","id":3,"method":"GetTask","params":[1]}"#,
3313        )
3314        .unwrap();
3315        assert_eq!(req.id, Value::from(3));
3316        assert_eq!(req.params, serde_json::json!([1]), "a method judges params");
3317    }
3318
3319    #[test]
3320    fn a2a_from_body_refuses_what_is_not_json() {
3321        let answer = refusal("{not json");
3322        assert_eq!(answer["id"], Value::Null);
3323        assert_eq!(answer["error"]["code"], -32700);
3324        assert_eq!(answer["error"]["message"], "Invalid JSON payload");
3325    }
3326
3327    #[test]
3328    fn a2a_from_body_refuses_what_is_not_a_json_rpc_request() {
3329        for (body, id) in [
3330            (
3331                r#"{"jsonrpc":"1.0","id":1,"method":"GetTask"}"#,
3332                Value::from(1),
3333            ),
3334            (r#"{"id":1,"method":"GetTask"}"#, Value::from(1)),
3335            (
3336                r#"{"jsonrpc":2.0,"id":1,"method":"GetTask"}"#,
3337                Value::from(1),
3338            ),
3339            (
3340                r#"[{"jsonrpc":"2.0","id":1,"method":"GetTask"}]"#,
3341                Value::Null,
3342            ),
3343            (r#""GetTask""#, Value::Null),
3344            (r#"{"jsonrpc":"2.0","id":"a","method":7}"#, Value::from("a")),
3345            (r#"{"jsonrpc":"2.0","id":"a"}"#, Value::from("a")),
3346            (
3347                r#"{"jsonrpc":"2.0","id":{"a":1},"method":"GetTask"}"#,
3348                Value::Null,
3349            ),
3350            (
3351                r#"{"jsonrpc":"2.0","id":null,"method":"GetTask"}"#,
3352                Value::Null,
3353            ),
3354            (
3355                r#"{"jsonrpc":"2.0","id":1.5,"method":"GetTask"}"#,
3356                Value::Null,
3357            ),
3358            (r#"{"jsonrpc":"2.0","method":"GetTask"}"#, Value::Null),
3359        ] {
3360            let answer = refusal(body);
3361            assert_eq!(answer["error"]["code"], -32600, "{body}");
3362            assert_eq!(
3363                answer["error"]["message"], "Request payload validation error",
3364                "{body}"
3365            );
3366            assert_eq!(answer["id"], id, "{body}");
3367        }
3368    }
3369
3370    fn versions(values: &[&str]) -> Vec<String> {
3371        values.iter().map(|value| value.to_string()).collect()
3372    }
3373
3374    #[test]
3375    fn a2a_version_accepts_1_0_with_or_without_a_patch() {
3376        for value in ["1.0", " 1.0 ", "1.0.0", "1.0.1", "1.0.17"] {
3377            assert!(accepts_a2a_version(&versions(&[value])), "{value:?}");
3378        }
3379    }
3380
3381    #[test]
3382    fn a2a_version_refuses_every_other_value_and_more_than_one_line() {
3383        for values in [
3384            &[][..],
3385            &[""],
3386            &["0.3"],
3387            &["0.3.0"],
3388            &["2.0"],
3389            &["1"],
3390            &["1.1"],
3391            &["1.0x"],
3392            &["1.0."],
3393            &["1.0.a"],
3394            &["1.00"],
3395            &["v1.0"],
3396            &["1.0", "1.0"],
3397        ] {
3398            assert!(!accepts_a2a_version(&versions(values)), "{values:?}");
3399        }
3400    }
3401
3402    #[test]
3403    fn an_extension_is_activated_by_an_exact_entry_on_any_line() {
3404        const URI: &str =
3405            "https://docs.murmur.nexus/reference/streaming-protocol/#murmur-stream-v1";
3406        let other = "https://example.com/x/v1";
3407        for values in [
3408            vec![URI.to_string()],
3409            vec![format!(" {URI} ")],
3410            vec![format!("{other}, {URI}")],
3411            vec![format!("{other},,{URI},")],
3412            vec![other.to_string(), URI.to_string()],
3413        ] {
3414            assert!(activates_extension(&values, URI), "{values:?}");
3415        }
3416        for values in [
3417            vec![],
3418            vec![String::new()],
3419            vec![other.to_string()],
3420            vec![format!("{URI}/")],
3421            vec![URI.to_uppercase()],
3422            vec![format!("{URI} {other}")],
3423        ] {
3424            assert!(!activates_extension(&values, URI), "{values:?}");
3425        }
3426    }
3427
3428    #[test]
3429    fn a2a_version_refusal_names_what_was_sent() {
3430        let answer = wire(&version_not_supported(Value::from(4), &versions(&[])));
3431        assert_eq!(answer["id"], 4);
3432        assert_eq!(answer["error"]["code"], -32009);
3433        let info = &answer["error"]["data"][0];
3434        assert_eq!(info["reason"], "VERSION_NOT_SUPPORTED");
3435        assert_eq!(info["domain"], "a2a-protocol.org");
3436        assert_eq!(
3437            info["metadata"],
3438            serde_json::json!({"requestedVersion": "", "supportedVersions": "1.0"})
3439        );
3440
3441        let answer = wire(&version_not_supported(Value::Null, &versions(&[" 0.3 "])));
3442        assert_eq!(
3443            answer["error"]["data"][0]["metadata"]["requestedVersion"],
3444            "0.3"
3445        );
3446        let answer = wire(&version_not_supported(
3447            Value::Null,
3448            &versions(&["1.0", "1.0"]),
3449        ));
3450        assert_eq!(
3451            answer["error"]["data"][0]["metadata"]["requestedVersion"],
3452            "1.0, 1.0"
3453        );
3454    }
3455
3456    // ── Parts and incoming messages ──────────────────────────────────────────
3457
3458    #[test]
3459    fn a_part_serializes_its_one_content_field_and_reads_back() {
3460        for (part, wire) in [
3461            (
3462                Part::text("hi"),
3463                serde_json::json!({"text": "hi", "mediaType": "text/plain"}),
3464            ),
3465            (
3466                Part::data(serde_json::json!({"a": [1]})),
3467                serde_json::json!({"data": {"a": [1]}, "mediaType": "application/json"}),
3468            ),
3469            (
3470                Part::data(Value::Null),
3471                serde_json::json!({"data": null, "mediaType": "application/json"}),
3472            ),
3473        ] {
3474            assert_eq!(serde_json::to_value(&part).unwrap(), wire);
3475            assert_eq!(serde_json::from_value::<Part>(wire).unwrap(), part);
3476        }
3477        for refused in [
3478            serde_json::json!({}),
3479            serde_json::json!({"text": "a", "data": 1}),
3480            serde_json::json!({"kind": "file", "file": {"uri": "x"}}),
3481            serde_json::json!({"text": 7}),
3482            serde_json::json!("text"),
3483        ] {
3484            assert!(
3485                serde_json::from_value::<Part>(refused.clone()).is_err(),
3486                "{refused}"
3487            );
3488        }
3489    }
3490
3491    fn params(message: Value) -> Value {
3492        serde_json::json!({ "message": message })
3493    }
3494
3495    fn user(parts: Value) -> Value {
3496        serde_json::json!({"messageId": "msg_1", "role": "ROLE_USER", "parts": parts})
3497    }
3498
3499    fn invalid_params(params: &Value) -> String {
3500        match read_send_params(params) {
3501            Err(MessageRefusal::InvalidParams(message)) => message,
3502            other => panic!("expected -32602 for {params}, got {other:?}"),
3503        }
3504    }
3505
3506    #[test]
3507    fn read_send_params_reads_a_v1_message_in_full() {
3508        let message = read_send_params(&params(serde_json::json!({
3509            "messageId": "msg_1",
3510            "contextId": "ctx_1",
3511            "taskId": "tsk_1",
3512            "role": "ROLE_USER",
3513            "referenceTaskIds": ["tsk_a", "tsk_b", "tsk_a"],
3514            "parts": [{"text": "a"}, {"data": null, "filename": "f.json"}],
3515            "metadata": {"ignored": true},
3516            "extensions": ["https://example.com/x"],
3517            "unknown": 1,
3518        })))
3519        .unwrap();
3520        assert_eq!(message.message_id, "msg_1");
3521        assert_eq!(message.context_id.as_deref(), Some("ctx_1"));
3522        assert_eq!(message.task_id.as_deref(), Some("tsk_1"));
3523        assert_eq!(message.reference_task_ids, ["tsk_a", "tsk_b"]);
3524        assert_eq!(message.part_kinds(), ["text", "data"]);
3525        assert_eq!(message.parts[1].filename.as_deref(), Some("f.json"));
3526
3527        let message = read_send_params(&params(serde_json::json!({
3528            "messageId": "msg_1", "role": "ROLE_USER", "taskId": "", "parts": [{"text": "a"}],
3529        })))
3530        .unwrap();
3531        assert_eq!(message.task_id, None, "an empty taskId names no task");
3532    }
3533
3534    /// Each structural check refuses with `-32602` in the documented order, so a message wrong in
3535    /// two ways learns about the earlier one.
3536    #[test]
3537    fn read_send_params_refuses_structure_in_order() {
3538        let text = serde_json::json!([{"text": "a"}]);
3539        for (params, expected) in [
3540            (serde_json::json!({}), "params.message"),
3541            (user(text.clone()), "params.message"),
3542            (serde_json::json!({"message": "hi"}), "params.message"),
3543            (
3544                params(serde_json::json!({"role": "ROLE_USER", "parts": text})),
3545                "messageId",
3546            ),
3547            (
3548                params(serde_json::json!({"messageId": "", "role": "ROLE_USER", "parts": text})),
3549                "messageId",
3550            ),
3551            (
3552                params(serde_json::json!({"messageId": "m", "role": "user", "parts": []})),
3553                "ROLE_USER",
3554            ),
3555            (
3556                params(serde_json::json!({"messageId": "m", "role": "ROLE_AGENT", "parts": text})),
3557                "ROLE_USER",
3558            ),
3559            (
3560                params(serde_json::json!({"messageId": "m", "role": 1, "parts": text})),
3561                "ROLE_USER",
3562            ),
3563            (
3564                params(serde_json::json!({"messageId": "m", "parts": text})),
3565                "ROLE_USER",
3566            ),
3567            (params(user(serde_json::json!([]))), "non-empty array"),
3568            (
3569                params(user(serde_json::json!({"text": "a"}))),
3570                "non-empty array",
3571            ),
3572            (
3573                params(user(serde_json::json!([{"text": "a"}, {}]))),
3574                "message.parts[1] sets none",
3575            ),
3576            (
3577                params(user(serde_json::json!([{"text": "a", "data": 1}]))),
3578                "message.parts[0] sets text and data",
3579            ),
3580            (
3581                params(user(
3582                    serde_json::json!([{"kind": "file", "file": {"uri": "x"}}]),
3583                )),
3584                "message.parts[0] sets none",
3585            ),
3586            (
3587                params(user(serde_json::json!([{"url": 7}, {}]))),
3588                "message.parts[1] sets none",
3589            ),
3590            (
3591                params(user(serde_json::json!([{"text": 7}]))),
3592                "message.parts[0].text is not a string",
3593            ),
3594            (
3595                params(user(serde_json::json!([{"data": 1, "mediaType": 2}]))),
3596                "message.parts[0].mediaType",
3597            ),
3598            (
3599                params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3600                    "parts": [{"url": "u"}], "taskId": 7})),
3601                "message.taskId",
3602            ),
3603            (
3604                params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3605                    "parts": text, "contextId": ["c"]})),
3606                "message.contextId",
3607            ),
3608            (
3609                params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3610                    "parts": [{"raw": "aGk="}], "referenceTaskIds": [""]})),
3611                "referenceTaskIds",
3612            ),
3613            (
3614                params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3615                    "parts": text, "referenceTaskIds": [7]})),
3616                "referenceTaskIds",
3617            ),
3618            (
3619                params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3620                    "parts": text, "referenceTaskIds": "tsk_a"})),
3621                "referenceTaskIds",
3622            ),
3623        ] {
3624            let message = invalid_params(&params);
3625            assert!(message.contains(expected), "{params}: {message}");
3626        }
3627    }
3628
3629    #[test]
3630    fn read_send_params_caps_distinct_references() {
3631        let ids = |n: usize| -> Vec<String> { (0..n).map(|i| format!("tsk_{i}")).collect() };
3632        let with = |ids: Vec<String>| {
3633            params(serde_json::json!({"messageId": "m", "role": "ROLE_USER",
3634                "parts": [{"text": "a"}], "referenceTaskIds": ids}))
3635        };
3636        let mut sixteen_twice = ids(MAX_REFERENCE_TASK_IDS);
3637        sixteen_twice.extend(ids(MAX_REFERENCE_TASK_IDS));
3638        assert_eq!(
3639            read_send_params(&with(sixteen_twice))
3640                .unwrap()
3641                .reference_task_ids,
3642            ids(MAX_REFERENCE_TASK_IDS)
3643        );
3644        let message = invalid_params(&with(ids(MAX_REFERENCE_TASK_IDS + 1)));
3645        assert!(message.contains("names 17 tasks"), "{message}");
3646    }
3647
3648    /// A message asking for push notifications is refused, whatever else `configuration` holds;
3649    /// a `null` config asks for nothing.
3650    #[test]
3651    fn read_send_params_refuses_a_push_notification_config() {
3652        let message = user(serde_json::json!([{"text": "hi"}]));
3653        let asking = serde_json::json!({"message": message,
3654            "configuration": {"taskPushNotificationConfig": {"url": "https://example.com/hook"}}});
3655        assert_eq!(
3656            read_send_params(&asking),
3657            Err(MessageRefusal::PushNotificationNotSupported)
3658        );
3659        let answer = serde_json::to_value(
3660            MessageRefusal::PushNotificationNotSupported.into_response(Value::from(1)),
3661        )
3662        .unwrap();
3663        assert_eq!(answer["error"]["code"], -32003);
3664        assert_eq!(
3665            answer["error"]["message"],
3666            "push notifications are not supported by this agent"
3667        );
3668        assert_eq!(
3669            answer["error"]["data"][0],
3670            serde_json::json!({"@type": "type.googleapis.com/google.rpc.ErrorInfo",
3671                "reason": "PUSH_NOTIFICATION_NOT_SUPPORTED", "domain": "a2a-protocol.org",
3672                "metadata": {}})
3673        );
3674        for configuration in [
3675            serde_json::json!({"taskPushNotificationConfig": null}),
3676            serde_json::json!({"acceptedOutputModes": ["text/plain"], "historyLength": 2}),
3677            serde_json::json!("ignored"),
3678        ] {
3679            let params = serde_json::json!({"message": message, "configuration": configuration});
3680            assert!(read_send_params(&params).is_ok(), "{params}");
3681        }
3682    }
3683
3684    /// A message whose `metadata` is `metadata`, naming `task_id` when given.
3685    fn with_metadata(metadata: Value, task_id: Option<&str>) -> Value {
3686        let mut message = user(serde_json::json!([{"text": "a"}]));
3687        message["metadata"] = metadata;
3688        if let Some(task_id) = task_id {
3689            message["taskId"] = Value::from(task_id);
3690        }
3691        params(message)
3692    }
3693
3694    /// `metadata` keyed by the door extension's URI, holding `door`.
3695    fn door(door: Value) -> Value {
3696        serde_json::json!({ DOOR_EXTENSION_URI: door })
3697    }
3698
3699    #[test]
3700    fn read_send_params_reads_forget_session_true() {
3701        let message = read_send_params(&with_metadata(
3702            door(serde_json::json!({"forgetSession": true})),
3703            None,
3704        ))
3705        .unwrap();
3706        assert!(message.forget_session);
3707        assert!(message.door_extension);
3708    }
3709
3710    #[test]
3711    fn read_send_params_reads_every_other_metadata_as_no_forget() {
3712        for (metadata, door_extension) in [
3713            (door(serde_json::json!({"forgetSession": false})), true),
3714            (door(serde_json::json!({})), true),
3715            (
3716                door(serde_json::json!({"other": 1, "forgetSession": false})),
3717                true,
3718            ),
3719            (serde_json::json!({}), false),
3720            (
3721                serde_json::json!({"https://example.com/x": {"forgetSession": true}}),
3722                false,
3723            ),
3724            (serde_json::json!({"forgetSession": true}), false),
3725            (serde_json::json!("not an object"), false),
3726            (
3727                serde_json::json!([door(serde_json::json!({"forgetSession": true}))]),
3728                false,
3729            ),
3730            (Value::Null, false),
3731        ] {
3732            let message = read_send_params(&with_metadata(metadata.clone(), None)).unwrap();
3733            assert!(!message.forget_session, "{metadata}");
3734            assert_eq!(message.door_extension, door_extension, "{metadata}");
3735        }
3736        let message = read_send_params(&params(user(serde_json::json!([{"text": "a"}])))).unwrap();
3737        assert!(
3738            !message.forget_session && !message.door_extension,
3739            "no metadata at all"
3740        );
3741    }
3742
3743    #[test]
3744    fn read_send_params_ignores_other_keys_beside_forget_session() {
3745        let message = read_send_params(&with_metadata(
3746            door(serde_json::json!({"forgetSession": true, "later": {"x": 1}})),
3747            None,
3748        ))
3749        .unwrap();
3750        assert!(message.forget_session);
3751    }
3752
3753    #[test]
3754    fn read_send_params_refuses_a_door_extension_value_that_is_not_an_object() {
3755        for value in [
3756            serde_json::json!(true),
3757            serde_json::json!("forget"),
3758            serde_json::json!([]),
3759        ] {
3760            assert_eq!(
3761                invalid_params(&with_metadata(door(value), None)),
3762                format!("message.metadata[\"{DOOR_EXTENSION_URI}\"] must be an object")
3763            );
3764        }
3765    }
3766
3767    #[test]
3768    fn read_send_params_refuses_a_forget_session_that_is_not_a_boolean() {
3769        for value in [serde_json::json!("true"), serde_json::json!(1), Value::Null] {
3770            assert_eq!(
3771                invalid_params(&with_metadata(
3772                    door(serde_json::json!({"forgetSession": value})),
3773                    None
3774                )),
3775                format!(
3776                    "message.metadata[\"{DOOR_EXTENSION_URI}\"].forgetSession must be a boolean"
3777                )
3778            );
3779        }
3780    }
3781
3782    #[test]
3783    fn read_send_params_refuses_a_forget_on_a_message_that_continues_a_task() {
3784        assert_eq!(
3785            invalid_params(&with_metadata(
3786                door(serde_json::json!({"forgetSession": true})),
3787                Some("tsk_1")
3788            )),
3789            "forgetSession starts a new conversation, so it goes on a message that starts a task, \
3790             not on one that continues task tsk_1"
3791        );
3792        // `false` asks for nothing, so it may ride on a continuation.
3793        let message = read_send_params(&with_metadata(
3794            door(serde_json::json!({"forgetSession": false})),
3795            Some("tsk_1"),
3796        ))
3797        .unwrap();
3798        assert_eq!(message.task_id.as_deref(), Some("tsk_1"));
3799        assert!(!message.forget_session);
3800    }
3801
3802    /// The door extension's checks come after every structural check and before the file-part
3803    /// refusal, each refused with `-32602` in order.
3804    #[test]
3805    fn read_send_params_checks_the_forget_field_in_order() {
3806        // A structural fault earlier in the message is reported first.
3807        let mut message = with_metadata(door(Value::from(1)), None);
3808        message["message"]["referenceTaskIds"] = Value::from("tsk_a");
3809        assert!(invalid_params(&message).contains("referenceTaskIds"));
3810        // Not an object beats the boolean and continuation checks (they cannot be reached).
3811        let message = with_metadata(door(Value::from(1)), Some("tsk_1"));
3812        assert!(invalid_params(&message).ends_with("must be an object"));
3813        // Not a boolean beats the continuation check.
3814        let message = with_metadata(
3815            door(serde_json::json!({"forgetSession": "yes"})),
3816            Some("tsk_1"),
3817        );
3818        assert!(invalid_params(&message).ends_with("must be a boolean"));
3819        // A malformed forget beats a file part.
3820        let mut message = with_metadata(door(serde_json::json!({"forgetSession": 1})), None);
3821        message["message"]["parts"] = serde_json::json!([{"raw": "aGk="}]);
3822        assert!(invalid_params(&message).ends_with("must be a boolean"));
3823        // A well-formed forget on a message with a file part is refused for the part.
3824        let mut message = with_metadata(door(serde_json::json!({"forgetSession": true})), None);
3825        message["message"]["parts"] = serde_json::json!([{"raw": "aGk="}]);
3826        assert!(matches!(
3827            read_send_params(&message),
3828            Err(MessageRefusal::ContentTypeNotSupported { .. })
3829        ));
3830        // Each renders as `Invalid params: …`.
3831        let refusal = read_send_params(&with_metadata(door(Value::from(1)), None)).unwrap_err();
3832        let answer = wire(&refusal.into_response(Value::from(3)));
3833        assert_eq!(answer["error"]["code"], -32602);
3834        assert_eq!(
3835            answer["error"]["message"],
3836            format!("Invalid params: message.metadata[\"{DOOR_EXTENSION_URI}\"] must be an object")
3837        );
3838    }
3839
3840    /// `params` for a well-formed message with `configuration` as given.
3841    fn configured(configuration: Value) -> Value {
3842        serde_json::json!({
3843            "message": user(serde_json::json!([{"text": "a"}])),
3844            "configuration": configuration,
3845        })
3846    }
3847
3848    #[test]
3849    fn read_send_params_reads_return_immediately_true_only_for_true() {
3850        let immediately = read_send_params(&configured(serde_json::json!({
3851            "returnImmediately": true,
3852        })))
3853        .unwrap();
3854        assert!(immediately.return_immediately);
3855        for params in [
3856            params(user(serde_json::json!([{"text": "a"}]))),
3857            configured(Value::Null),
3858            configured(serde_json::json!({})),
3859            configured(serde_json::json!({"returnImmediately": null})),
3860            configured(serde_json::json!({"returnImmediately": false})),
3861            configured(serde_json::json!({"historyLength": 2})),
3862            configured(serde_json::json!("ignored")),
3863        ] {
3864            assert!(
3865                !read_send_params(&params).unwrap().return_immediately,
3866                "{params}"
3867            );
3868        }
3869    }
3870
3871    #[test]
3872    fn read_send_params_refuses_a_return_immediately_that_is_not_a_boolean() {
3873        for value in [
3874            Value::from("yes"),
3875            Value::from(1),
3876            serde_json::json!({}),
3877            serde_json::json!([true]),
3878        ] {
3879            let params = configured(serde_json::json!({ "returnImmediately": value }));
3880            assert_eq!(
3881                invalid_params(&params),
3882                "configuration.returnImmediately must be a boolean",
3883                "{params}"
3884            );
3885            let answer = wire(
3886                &read_send_params(&params)
3887                    .unwrap_err()
3888                    .into_response(1.into()),
3889            );
3890            assert_eq!(answer["error"]["code"], -32602);
3891            assert_eq!(
3892                answer["error"]["message"],
3893                "Invalid params: configuration.returnImmediately must be a boolean"
3894            );
3895        }
3896    }
3897
3898    /// The flag is the last `-32602` check: after the door extension's, before the file part and
3899    /// the push config.
3900    #[test]
3901    fn read_send_params_checks_return_immediately_in_order() {
3902        let mut message = with_metadata(door(serde_json::json!({"forgetSession": 1})), None);
3903        message["configuration"] = serde_json::json!({"returnImmediately": "yes"});
3904        assert!(invalid_params(&message).contains("forgetSession"));
3905
3906        let mut message = configured(serde_json::json!({
3907            "returnImmediately": "yes",
3908            "taskPushNotificationConfig": {"url": "https://example.com/hook"},
3909        }));
3910        message["message"]["parts"] = serde_json::json!([{"raw": "aGk="}]);
3911        assert!(invalid_params(&message).contains("returnImmediately"));
3912
3913        message["configuration"]["returnImmediately"] = Value::Bool(true);
3914        assert!(matches!(
3915            read_send_params(&message),
3916            Err(MessageRefusal::ContentTypeNotSupported { .. })
3917        ));
3918    }
3919
3920    /// A file part is refused only once the message is otherwise well formed, naming the first.
3921    #[test]
3922    fn read_send_params_refuses_the_first_file_part_with_content_type_not_supported() {
3923        assert_eq!(
3924            read_send_params(&params(user(serde_json::json!([
3925                {"text": "a"},
3926                {"url": "https://example.com/x.pdf", "mediaType": "application/pdf"},
3927                {"raw": "aGk="},
3928            ])))),
3929            Err(MessageRefusal::ContentTypeNotSupported {
3930                part_index: 1,
3931                kind: "url",
3932                media_type: Some("application/pdf".to_string()),
3933            })
3934        );
3935        let refusal =
3936            read_send_params(&params(user(serde_json::json!([{"raw": "aGk="}])))).unwrap_err();
3937        let answer = wire(&refusal.into_response(Value::from(3)));
3938        assert_eq!(answer["error"]["code"], -32005);
3939        let info = &answer["error"]["data"][0];
3940        assert_eq!(info["reason"], "CONTENT_TYPE_NOT_SUPPORTED");
3941        assert_eq!(info["metadata"], serde_json::json!({"partIndex": "0"}));
3942        // Malformed beats unsupported.
3943        invalid_params(&params(user(
3944            serde_json::json!([{"raw": "aGk="}, {"text": 1}]),
3945        )));
3946    }
3947
3948    fn rendered(parts: Value) -> String {
3949        read_send_params(&params(user(parts))).unwrap().agent_text()
3950    }
3951
3952    #[test]
3953    fn agent_text_of_text_parts_is_the_text_joined_by_newlines() {
3954        assert_eq!(
3955            rendered(
3956                serde_json::json!([{"text": "one"}, {"text": "two", "mediaType": "text/markdown"}])
3957            ),
3958            "one\ntwo"
3959        );
3960    }
3961
3962    #[test]
3963    fn agent_text_fences_a_data_part_beyond_its_longest_backtick_run() {
3964        assert_eq!(
3965            rendered(serde_json::json!([{"text": "summarise"}, {"data": {"rows": [1, 2]}}])),
3966            "summarise\n```data\n{\n  \"rows\": [\n    1,\n    2\n  ]\n}\n```"
3967        );
3968        let three =
3969            rendered(serde_json::json!([{"data": "a ``` b", "mediaType": "application/json"}]));
3970        assert_eq!(
3971            three,
3972            "````data media-type=application/json\n\"a ``` b\"\n````"
3973        );
3974        let five = rendered(serde_json::json!([{"data": {"x": "`````"}}]));
3975        assert!(five.starts_with("``````data\n"), "{five}");
3976        assert!(five.ends_with("\n``````"), "{five}");
3977        assert_eq!(
3978            rendered(serde_json::json!([{"data": null}])),
3979            "```data\nnull\n```"
3980        );
3981    }
3982
3983    #[test]
3984    fn agent_text_sanitises_the_fence_label() {
3985        assert_eq!(
3986            rendered(serde_json::json!([{"data": 1, "mediaType": "a b=`c",
3987                "filename": "my\tfile.json"}])),
3988            "```data media-type=a_b__c filename=my_file.json\n1\n```"
3989        );
3990    }
3991
3992    #[test]
3993    fn a_reference_block_names_each_task_as_it_stands() {
3994        let mut r = TaskRegistry::new(8, TaskAcceptance::Queue);
3995        for id in ["tsk_a", "tsk_b", "tsk_c", "tsk_e"] {
3996            r.enqueue(id, "ctx_001");
3997        }
3998        r.start_task("tsk_a".to_string(), "ctx_001".to_string(), TaskLane::Bg);
3999        r.finish_task(TaskState::Completed);
4000        r.record_ending("tsk_a", "done", Some("four ```"), false, &[], &[]);
4001        r.start_task("tsk_b".to_string(), "ctx_001".to_string(), TaskLane::Bg);
4002        r.finish_task(TaskState::Failed);
4003        r.record_ending("tsk_b", "the driver\nfailed", None, false, &[], &[]);
4004        r.start_task("tsk_c".to_string(), "ctx_001".to_string(), TaskLane::Bg);
4005        r.record_owner("tsk_e", "member:reviewer");
4006        let ids: Vec<String> = ["tsk_a", "tsk_b", "tsk_c", "tsk_d", "tsk_e"]
4007            .map(str::to_string)
4008            .to_vec();
4009
4010        assert_eq!(
4011            r.reference_block(&ids, |_| true).unwrap(),
4012            "Referenced tasks:\n\
4013             - tsk_a: completed\n````response\nfour ```\n````\n\
4014             - tsk_b: failed: the driver failed\n\
4015             - tsk_c: working\n\
4016             - tsk_d: not a task this capsule holds\n\
4017             - tsk_e: submitted"
4018        );
4019        let member = |task_id: &str| r.owner(task_id) == Some("member:planner");
4020        let block = r.reference_block(&ids[4..], member).unwrap();
4021        assert_eq!(
4022            block,
4023            "Referenced tasks:\n- tsk_e: not a task this capsule holds"
4024        );
4025        assert_eq!(r.reference_block(&[], |_| true), None);
4026    }
4027
4028    #[test]
4029    fn user_message_is_a_v1_message_from_the_user() {
4030        assert_eq!(
4031            user_message("msg_1", Some("ctx_1"), "hello"),
4032            serde_json::json!({
4033                "messageId": "msg_1",
4034                "contextId": "ctx_1",
4035                "role": "ROLE_USER",
4036                "parts": [{"text": "hello", "mediaType": "text/plain"}],
4037            })
4038        );
4039        assert!(user_message("msg_1", None, "hello")
4040            .get("contextId")
4041            .is_none());
4042        assert!(read_send_params(&params(user_message("msg_1", None, "hello"))).is_ok());
4043    }
4044
4045    #[test]
4046    fn send_message_params_answer_at_acceptance() {
4047        let message = user_message("msg_1", None, "hello");
4048        let params = send_message_params(message.clone());
4049        assert_eq!(
4050            params,
4051            serde_json::json!({
4052                "message": message,
4053                "configuration": {"returnImmediately": true},
4054            })
4055        );
4056        assert!(read_send_params(&params).unwrap().return_immediately);
4057    }
4058
4059    #[test]
4060    fn send_message_result_is_exactly_one_member_of_the_oneof() {
4061        let task = serde_json::json!({"id": "tsk_1", "contextId": "c",
4062            "status": {"state": "TASK_STATE_SUBMITTED"}});
4063        assert!(matches!(
4064            SendMessageResult::from_result(&serde_json::json!({ "task": task })),
4065            Ok(SendMessageResult::Task(task)) if task.id == "tsk_1"
4066        ));
4067        assert!(matches!(
4068            SendMessageResult::from_result(&serde_json::json!({"message": {"messageId": "m"}})),
4069            Ok(SendMessageResult::Message(_))
4070        ));
4071        for refused in [
4072            serde_json::json!({}),
4073            serde_json::json!({"task": task, "message": {}}),
4074            task.clone(),
4075            serde_json::json!({"task": {"id": "tsk_1", "contextId": "c",
4076                "status": {"state": "submitted"}}}),
4077        ] {
4078            assert!(
4079                SendMessageResult::from_result(&refused).is_err(),
4080                "{refused}"
4081            );
4082        }
4083    }
4084}