Skip to main content

systemprompt_models/a2a/
message.rs

1//! A2A message and part wire types.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use serde::{Deserialize, Serialize};
7use systemprompt_identifiers::{ContextId, MessageId, TaskId};
8
9#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
10#[serde(rename_all = "camelCase")]
11#[expect(
12    clippy::struct_field_names,
13    reason = "A2A wire schema names these fields message_id/task_id/context_id — public protocol \
14              vocabulary"
15)]
16pub struct Message {
17    pub role: MessageRole,
18    pub parts: Vec<Part>,
19    pub message_id: MessageId,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub task_id: Option<TaskId>,
22    pub context_id: ContextId,
23    // JSON: A2A `Message.metadata` is spec-defined as a free-form object.
24    pub metadata: Option<serde_json::Value>,
25    pub extensions: Option<Vec<String>>,
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub reference_task_ids: Option<Vec<TaskId>>,
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq)]
31pub enum MessageRole {
32    #[serde(rename = "ROLE_USER")]
33    User,
34    #[serde(rename = "ROLE_AGENT")]
35    Agent,
36}
37
38#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
39#[serde(untagged)]
40pub enum Part {
41    Text(TextPart),
42    File(FilePart),
43    Data(DataPart),
44}
45
46#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
47pub struct TextPart {
48    pub text: String,
49}
50
51#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
52pub struct DataPart {
53    // JSON: A2A `DataPart.data` is spec-defined as a free-form object.
54    pub data: serde_json::Map<String, serde_json::Value>,
55}
56
57#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
58pub struct FilePart {
59    pub file: FileContent,
60}
61
62#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
63#[serde(rename_all = "camelCase")]
64pub struct FileContent {
65    pub name: Option<String>,
66    pub mime_type: Option<String>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub bytes: Option<String>,
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub url: Option<String>,
71}
72
73impl Part {
74    pub fn as_text(&self) -> Option<&str> {
75        match self {
76            Self::Text(text_part) => Some(&text_part.text),
77            _ => None,
78        }
79    }
80
81    // JSON: A2A `DataPart.data` is spec-defined as a free-form object.
82    pub fn as_data(&self) -> Option<serde_json::Value> {
83        match self {
84            Self::Data(data_part) => Some(serde_json::Value::Object(data_part.data.clone())),
85            _ => None,
86        }
87    }
88
89    // JSON: A2A `FilePart` wire object.
90    pub fn as_file(&self) -> Option<serde_json::Value> {
91        match self {
92            Self::File(file_part) => serde_json::to_value(&file_part.file).ok(),
93            _ => None,
94        }
95    }
96}