#![allow(non_snake_case)]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Role {
User,
Agent,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum TaskStatus {
Submitted,
Working,
InputRequired,
Completed,
Failed,
Canceled,
Rejected,
Authenticated,
#[serde(other)]
Unknown,
}
impl TaskStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
TaskStatus::Completed
| TaskStatus::Failed
| TaskStatus::Canceled
| TaskStatus::Rejected
)
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum Part {
Text {
content: String,
metadata: Option<serde_json::Value>,
mediaType: Option<String>,
filename: Option<String>,
},
Raw {
bytes: String,
mediaType: String,
metadata: Option<serde_json::Value>,
filename: Option<String>,
},
Url {
url: String,
mediaType: Option<String>,
metadata: Option<serde_json::Value>,
filename: Option<String>,
},
Data {
data: serde_json::Value,
metadata: Option<serde_json::Value>,
mediaType: Option<String>,
filename: Option<String>,
},
Unknown,
}
fn take_string(obj: &mut serde_json::Map<String, serde_json::Value>, key: &str) -> Option<String> {
match obj.remove(key) {
Some(serde_json::Value::String(s)) => Some(s),
_ => None,
}
}
struct PartCommonFields {
metadata: Option<serde_json::Value>,
media_type: Option<String>,
filename: Option<String>,
}
impl Part {
fn from_wire_object(mut obj: serde_json::Map<String, serde_json::Value>) -> Part {
let common = PartCommonFields {
metadata: obj.remove("metadata"),
media_type: take_string(&mut obj, "mediaType"),
filename: take_string(&mut obj, "filename"),
};
if let Some(tag) = take_string(&mut obj, "type") {
return Part::from_klieo_pre_interop_shape(&tag, obj, common);
}
if let Some(kind) = take_string(&mut obj, "kind") {
return Part::from_legacy_v03x_shape(&kind, obj, common);
}
Part::from_current_bare_shape(obj, common)
}
fn from_klieo_pre_interop_shape(
tag: &str,
mut obj: serde_json::Map<String, serde_json::Value>,
common: PartCommonFields,
) -> Part {
let PartCommonFields {
metadata,
media_type,
filename,
} = common;
match tag {
"text" => Part::Text {
content: take_string(&mut obj, "content").unwrap_or_default(),
metadata,
mediaType: media_type,
filename,
},
"raw" => Part::Raw {
bytes: take_string(&mut obj, "bytes").unwrap_or_default(),
mediaType: media_type.unwrap_or_default(),
metadata,
filename,
},
"url" => Part::Url {
url: take_string(&mut obj, "url").unwrap_or_default(),
mediaType: media_type,
metadata,
filename,
},
"data" => Part::Data {
data: obj.remove("data").unwrap_or(serde_json::Value::Null),
metadata,
mediaType: media_type,
filename,
},
_ => Part::Unknown,
}
}
fn from_legacy_v03x_shape(
kind: &str,
mut obj: serde_json::Map<String, serde_json::Value>,
common: PartCommonFields,
) -> Part {
let PartCommonFields {
metadata,
media_type,
filename,
} = common;
match kind {
"text" => Part::Text {
content: take_string(&mut obj, "text").unwrap_or_default(),
metadata,
mediaType: media_type,
filename,
},
"data" => Part::Data {
data: obj.remove("data").unwrap_or(serde_json::Value::Null),
metadata,
mediaType: media_type,
filename,
},
_ => Part::Unknown,
}
}
fn from_current_bare_shape(
mut obj: serde_json::Map<String, serde_json::Value>,
common: PartCommonFields,
) -> Part {
let PartCommonFields {
metadata,
media_type,
filename,
} = common;
if let Some(text) = take_string(&mut obj, "text") {
return Part::Text {
content: text,
metadata,
mediaType: media_type,
filename,
};
}
if let Some(raw) = take_string(&mut obj, "raw") {
return Part::Raw {
bytes: raw,
mediaType: media_type.unwrap_or_default(),
metadata,
filename,
};
}
if let Some(url) = take_string(&mut obj, "url") {
return Part::Url {
url,
mediaType: media_type,
metadata,
filename,
};
}
if let Some(data) = obj.remove("data") {
return Part::Data {
data,
metadata,
mediaType: media_type,
filename,
};
}
Part::Unknown
}
}
impl Serialize for Part {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(None)?;
match self {
Part::Text {
content,
metadata,
mediaType,
filename,
} => {
map.serialize_entry("text", content)?;
serialize_common(
&mut map,
metadata,
mediaType.as_deref(),
filename.as_deref(),
)?;
}
Part::Raw {
bytes,
mediaType,
metadata,
filename,
} => {
map.serialize_entry("raw", bytes)?;
map.serialize_entry("mediaType", mediaType)?;
serialize_common(&mut map, metadata, None, filename.as_deref())?;
}
Part::Url {
url,
mediaType,
metadata,
filename,
} => {
map.serialize_entry("url", url)?;
serialize_common(
&mut map,
metadata,
mediaType.as_deref(),
filename.as_deref(),
)?;
}
Part::Data {
data,
metadata,
mediaType,
filename,
} => {
map.serialize_entry("data", data)?;
serialize_common(
&mut map,
metadata,
mediaType.as_deref(),
filename.as_deref(),
)?;
}
Part::Unknown => {}
}
map.end()
}
}
fn serialize_common<M: serde::ser::SerializeMap>(
map: &mut M,
metadata: &Option<serde_json::Value>,
media_type: Option<&str>,
filename: Option<&str>,
) -> Result<(), M::Error> {
if let Some(v) = metadata {
map.serialize_entry("metadata", v)?;
}
if let Some(v) = media_type {
map.serialize_entry("mediaType", v)?;
}
if let Some(v) = filename {
map.serialize_entry("filename", v)?;
}
Ok(())
}
impl<'de> Deserialize<'de> for Part {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = serde_json::Value::deserialize(deserializer)?;
match value {
serde_json::Value::Object(obj) => Ok(Part::from_wire_object(obj)),
other => Err(serde::de::Error::custom(format!(
"Part must be a JSON object, got {other}"
))),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub messageId: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contextId: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub taskId: Option<String>,
pub role: Role,
pub parts: Vec<Part>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub extensions: Vec<String>,
#[serde(default)]
pub referenceTaskIds: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Artifact {
pub artifactId: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub parts: Vec<Part>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
#[serde(default)]
pub extensions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Task {
pub id: String,
pub contextId: String,
pub status: TaskStatus,
#[serde(default)]
pub artifacts: Vec<Artifact>,
#[serde(default)]
pub history: Vec<Message>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentProvider {
pub organization: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub url: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCapabilities {
#[serde(default)]
pub streaming: bool,
#[serde(default)]
pub pushNotifications: bool,
#[serde(default)]
pub stateTransitionHistory: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentSkill {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub tags: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentInterface {
pub transport: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCardSignature {
pub alg: String,
pub kid: String,
pub signature: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentCard {
pub id: String,
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider: Option<AgentProvider>,
pub capabilities: AgentCapabilities,
#[serde(default)]
pub skills: Vec<AgentSkill>,
#[serde(default)]
pub interfaces: Vec<AgentInterface>,
#[serde(default)]
pub securitySchemes: serde_json::Map<String, serde_json::Value>,
#[serde(default)]
pub security: Vec<serde_json::Value>,
#[serde(default)]
pub extensions: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub signature: Option<AgentCardSignature>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SendMessageParams {
pub message: Message,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub configuration: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SendMessageResult {
Message(Message),
Task(Task),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetTaskParams {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub historyLength: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListTasksParams {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub contextId: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cursor: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListTasksResult {
pub tasks: Vec<Task>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub nextCursor: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CancelTaskParams {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscribeToTaskParams {
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PushNotificationConfigParams {
pub taskId: String,
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PushNotificationConfig {
pub id: String,
pub taskId: String,
pub url: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GetPushNotificationConfigParams {
pub taskId: String,
pub id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ListPushNotificationConfigsParams {
pub taskId: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ListPushNotificationConfigsResult {
pub configs: Vec<PushNotificationConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeletePushNotificationConfigParams {
pub taskId: String,
pub id: String,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GetExtendedAgentCardParams {}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn unknown_task_status_deserialises_to_unknown_not_error() {
let s: TaskStatus = serde_json::from_value(json!("some_future_state")).unwrap();
assert_eq!(s, TaskStatus::Unknown);
assert!(!s.is_terminal(), "Unknown status is never terminal");
}
#[test]
fn unknown_part_type_deserialises_to_unknown_not_error() {
let p: Part = serde_json::from_value(json!({"type": "future_part", "x": 1})).unwrap();
assert!(matches!(p, Part::Unknown));
}
#[test]
fn unknown_part_kind_deserialises_to_unknown_not_error() {
let p: Part = serde_json::from_value(json!({"kind": "future_kind", "x": 1})).unwrap();
assert!(matches!(p, Part::Unknown));
}
#[test]
fn unknown_part_shape_with_no_recognised_key_deserialises_to_unknown() {
let p: Part = serde_json::from_value(json!({"nonsense": 1})).unwrap();
assert!(matches!(p, Part::Unknown));
}
#[test]
fn part_text_serialises_to_bare_text_member_no_discriminator() {
let p = Part::Text {
content: "hello".into(),
metadata: None,
mediaType: None,
filename: None,
};
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v["text"], "hello");
assert!(v.get("type").is_none(), "must not emit legacy `type` key");
assert!(v.get("kind").is_none(), "must not emit legacy `kind` key");
assert!(
v.get("content").is_none(),
"must not emit klieo's old `content` key"
);
}
#[test]
fn part_raw_serialises_to_bare_raw_member_no_discriminator() {
let p = Part::Raw {
bytes: "aGVsbG8=".into(),
mediaType: "text/plain".into(),
metadata: None,
filename: None,
};
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v["raw"], "aGVsbG8=");
assert_eq!(v["mediaType"], "text/plain");
assert!(v.get("type").is_none());
assert!(
v.get("bytes").is_none(),
"spec member name is `raw`, not `bytes`"
);
}
#[test]
fn part_url_serialises_to_bare_url_member() {
let p = Part::Url {
url: "https://example.test/doc.pdf".into(),
mediaType: Some("application/pdf".into()),
metadata: None,
filename: None,
};
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v["url"], "https://example.test/doc.pdf");
assert_eq!(v["mediaType"], "application/pdf");
assert!(v.get("type").is_none());
}
#[test]
fn part_data_serialises_to_bare_data_member() {
let p = Part::Data {
data: json!({"a": 1}),
metadata: None,
mediaType: None,
filename: None,
};
let v = serde_json::to_value(&p).unwrap();
assert_eq!(v["data"], json!({"a": 1}));
assert!(v.get("type").is_none());
}
#[test]
fn part_text_current_spec_bare_form_deserialises() {
let p: Part = serde_json::from_value(json!({"text": "hi"})).unwrap();
match p {
Part::Text { content, .. } => assert_eq!(content, "hi"),
other => panic!("expected Part::Text, got {other:?}"),
}
}
#[test]
fn part_raw_current_spec_bare_form_deserialises() {
let p: Part =
serde_json::from_value(json!({"raw": "aGVsbG8=", "mediaType": "text/plain"})).unwrap();
match p {
Part::Raw {
bytes, mediaType, ..
} => {
assert_eq!(bytes, "aGVsbG8=");
assert_eq!(mediaType, "text/plain");
}
other => panic!("expected Part::Raw, got {other:?}"),
}
}
#[test]
fn part_url_current_spec_bare_form_deserialises() {
let p: Part = serde_json::from_value(json!({"url": "https://example.test/x"})).unwrap();
match p {
Part::Url { url, .. } => assert_eq!(url, "https://example.test/x"),
other => panic!("expected Part::Url, got {other:?}"),
}
}
#[test]
fn part_data_current_spec_bare_form_deserialises() {
let p: Part = serde_json::from_value(json!({"data": {"a": 1}})).unwrap();
match p {
Part::Data { data, .. } => assert_eq!(data, json!({"a": 1})),
other => panic!("expected Part::Data, got {other:?}"),
}
}
#[test]
fn part_text_legacy_v03x_kind_form_deserialises() {
let p: Part = serde_json::from_value(json!({"kind": "text", "text": "hi"})).unwrap();
match p {
Part::Text { content, .. } => assert_eq!(content, "hi"),
other => panic!("expected Part::Text, got {other:?}"),
}
}
#[test]
fn part_data_legacy_v03x_kind_form_deserialises() {
let p: Part = serde_json::from_value(json!({"kind": "data", "data": {"a": 1}})).unwrap();
match p {
Part::Data { data, .. } => assert_eq!(data, json!({"a": 1})),
other => panic!("expected Part::Data, got {other:?}"),
}
}
#[test]
fn part_legacy_kind_file_is_unrecognised_not_an_error() {
let p: Part = serde_json::from_value(json!({
"kind": "file",
"file": {"name": "x.png", "mimeType": "image/png", "fileWithBytes": "AA=="}
}))
.unwrap();
assert!(matches!(p, Part::Unknown));
}
#[test]
fn part_text_klieo_pre_interop_type_content_form_deserialises() {
let p: Part = serde_json::from_value(json!({"type": "text", "content": "hi"})).unwrap();
match p {
Part::Text { content, .. } => assert_eq!(content, "hi"),
other => panic!("expected Part::Text, got {other:?}"),
}
}
#[test]
fn part_raw_klieo_pre_interop_type_bytes_form_deserialises() {
let p: Part = serde_json::from_value(
json!({"type": "raw", "bytes": "aGVsbG8=", "mediaType": "text/plain"}),
)
.unwrap();
match p {
Part::Raw {
bytes, mediaType, ..
} => {
assert_eq!(bytes, "aGVsbG8=");
assert_eq!(mediaType, "text/plain");
}
other => panic!("expected Part::Raw, got {other:?}"),
}
}
#[test]
fn part_url_klieo_pre_interop_type_form_deserialises() {
let p: Part =
serde_json::from_value(json!({"type": "url", "url": "https://example.test"})).unwrap();
match p {
Part::Url { url, .. } => assert_eq!(url, "https://example.test"),
other => panic!("expected Part::Url, got {other:?}"),
}
}
#[test]
fn part_data_klieo_pre_interop_type_form_deserialises() {
let p: Part = serde_json::from_value(json!({"type": "data", "data": {"a": 1}})).unwrap();
match p {
Part::Data { data, .. } => assert_eq!(data, json!({"a": 1})),
other => panic!("expected Part::Data, got {other:?}"),
}
}
#[test]
fn part_text_round_trips_through_canonical_emit_form() {
let original = Part::Text {
content: "round trip".into(),
metadata: Some(json!({"k": "v"})),
mediaType: Some("text/markdown".into()),
filename: Some("note.md".into()),
};
let v = serde_json::to_value(&original).unwrap();
let back: Part = serde_json::from_value(v).unwrap();
match back {
Part::Text {
content,
metadata,
mediaType,
filename,
} => {
assert_eq!(content, "round trip");
assert_eq!(metadata, Some(json!({"k": "v"})));
assert_eq!(mediaType, Some("text/markdown".into()));
assert_eq!(filename, Some("note.md".into()));
}
other => panic!("expected Part::Text, got {other:?}"),
}
}
#[test]
fn part_unknown_serialises_to_empty_object() {
let v = serde_json::to_value(&Part::Unknown).unwrap();
assert_eq!(v, json!({}));
}
#[test]
fn message_round_trips() {
let m = Message {
messageId: "m-1".into(),
contextId: Some("c-1".into()),
taskId: Some("t-1".into()),
role: Role::User,
parts: vec![Part::Text {
content: "hi".into(),
metadata: None,
mediaType: None,
filename: None,
}],
metadata: None,
extensions: vec![],
referenceTaskIds: vec![],
};
let v = serde_json::to_value(&m).unwrap();
let back: Message = serde_json::from_value(v).unwrap();
assert_eq!(back.messageId, "m-1");
assert_eq!(back.role, Role::User);
assert_eq!(back.parts.len(), 1);
}
#[test]
fn task_with_empty_history_serialises_minimally() {
let t = Task {
id: "t-1".into(),
contextId: "c-1".into(),
status: TaskStatus::Submitted,
artifacts: vec![],
history: vec![],
metadata: None,
};
let v = serde_json::to_value(&t).unwrap();
assert_eq!(v["id"], "t-1");
assert_eq!(v["status"], "submitted");
}
#[test]
fn agent_card_round_trips() {
let card = AgentCard {
id: "agent-1".into(),
name: "Echo".into(),
description: Some("Echoes input".into()),
provider: None,
capabilities: AgentCapabilities {
streaming: false,
pushNotifications: false,
stateTransitionHistory: false,
},
skills: vec![],
interfaces: vec![],
securitySchemes: serde_json::Map::new(),
security: vec![],
extensions: vec![],
signature: None,
};
let v = serde_json::to_value(&card).unwrap();
let back: AgentCard = serde_json::from_value(v).unwrap();
assert_eq!(back.id, "agent-1");
}
#[test]
fn role_agent_serialises_as_lowercase() {
let v = serde_json::to_value(Role::Agent).unwrap();
assert_eq!(v, json!("agent"));
}
#[test]
fn task_status_is_terminal_covers_completed_failed_canceled_rejected() {
assert!(TaskStatus::Completed.is_terminal());
assert!(TaskStatus::Failed.is_terminal());
assert!(TaskStatus::Canceled.is_terminal());
assert!(TaskStatus::Rejected.is_terminal());
}
#[test]
fn task_status_is_terminal_excludes_in_progress() {
assert!(!TaskStatus::Submitted.is_terminal());
assert!(!TaskStatus::Working.is_terminal());
assert!(!TaskStatus::Authenticated.is_terminal());
assert!(!TaskStatus::InputRequired.is_terminal());
}
}