use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use super::message::Message;
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Task {
pub id: Arc<str>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub context_id: Option<Arc<str>>,
pub status: TaskStatus,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artifacts: Vec<Artifact>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub history: Vec<Message>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, serde_json::Value>,
}
use std::sync::Arc;
impl Task {
#[must_use]
pub fn new(id: impl Into<Arc<str>>) -> Self {
Self {
id: id.into(),
context_id: None,
status: TaskStatus {
state: TaskState::Submitted,
message: None,
timestamp: None,
},
artifacts: Vec::new(),
history: Vec::new(),
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn with_status(mut self, status: TaskStatus) -> Self {
self.status = status;
self
}
#[must_use]
pub fn with_artifact(mut self, artifact: impl Into<Artifact>) -> Self {
self.artifacts.push(artifact.into());
self
}
#[must_use]
pub fn with_history_message(mut self, message: impl Into<Message>) -> Self {
self.history.push(message.into());
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct TaskStatus {
pub state: TaskState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<Message>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub timestamp: Option<Arc<str>>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TaskState {
#[default]
Submitted,
Working,
Completed,
Failed,
Cancelled,
InputRequired,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Artifact {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub parts: Vec<super::message::Part>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub metadata: BTreeMap<String, serde_json::Value>,
}
impl Artifact {
#[must_use]
pub fn new(part: impl Into<super::message::Part>) -> Self {
Self {
name: None,
description: None,
parts: vec![part.into()],
metadata: BTreeMap::new(),
}
}
#[must_use]
pub fn with_part(mut self, part: impl Into<super::message::Part>) -> Self {
self.parts.push(part.into());
self
}
#[must_use]
pub fn with_name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use std::sync::Arc;
use super::{Artifact, Task, TaskState, TaskStatus};
use crate::domain::a2a::message::{Message, MessageRole, Part, TextPart};
type TestError = Box<dyn std::error::Error>;
fn text_part(text: &str) -> Part {
Part::Text(TextPart {
text: text.to_owned(),
metadata: BTreeMap::new(),
})
}
#[test]
fn task_serialises_with_required_id_and_status() -> Result<(), TestError> {
let task = Task::new("task-1");
let value = serde_json::to_value(&task)?;
let obj = value
.as_object()
.ok_or("a Task must serialise to a JSON object")?;
assert_eq!(
obj.get("id").and_then(serde_json::Value::as_str),
Some("task-1")
);
let status = obj
.get("status")
.and_then(serde_json::Value::as_object)
.ok_or("`status` must be a JSON object")?;
assert_eq!(
status.get("state").and_then(serde_json::Value::as_str),
Some("submitted")
);
Ok(())
}
#[test]
fn task_with_no_artifacts_or_history_omits_the_fields() -> Result<(), TestError> {
let task = Task::new("task-1");
let value = serde_json::to_value(&task)?;
let obj = value
.as_object()
.ok_or("a Task must serialise to a JSON object")?;
assert!(!obj.contains_key("artifacts"));
assert!(!obj.contains_key("history"));
assert!(!obj.contains_key("metadata"));
assert!(!obj.contains_key("contextId"));
Ok(())
}
#[test]
fn task_round_trips_with_artifacts_and_history() -> Result<(), TestError> {
let task = Task::new("task-1")
.with_status(TaskStatus {
state: TaskState::Working,
message: Some(Message::new(MessageRole::Agent).with_part(text_part("thinking..."))),
timestamp: Some(Arc::from("2025-01-01T00:00:00Z")),
})
.with_artifact(Artifact::new(text_part("answer")))
.with_history_message(Message::new(MessageRole::User).with_part(text_part("hi")));
let json = serde_json::to_string(&task)?;
let parsed: Task = serde_json::from_str(&json)?;
assert_eq!(parsed, task);
Ok(())
}
#[test]
fn task_state_serialises_in_lowercase() -> Result<(), TestError> {
for (state, expected) in [
(TaskState::Submitted, "submitted"),
(TaskState::Working, "working"),
(TaskState::Completed, "completed"),
(TaskState::Failed, "failed"),
(TaskState::Cancelled, "cancelled"),
(TaskState::InputRequired, "input_required"),
] {
let value = serde_json::to_value(state)?;
let got = value
.as_str()
.ok_or("a TaskState must serialise to a JSON string")?;
assert_eq!(got, expected, "TaskState::{state:?}");
}
Ok(())
}
#[test]
fn task_state_round_trips_for_every_variant() -> Result<(), TestError> {
for state in [
TaskState::Submitted,
TaskState::Working,
TaskState::Completed,
TaskState::Failed,
TaskState::Cancelled,
TaskState::InputRequired,
] {
let json = serde_json::to_string(&state)?;
let parsed: TaskState = serde_json::from_str(&json)?;
assert_eq!(parsed, state);
}
Ok(())
}
#[test]
fn task_status_with_no_message_or_timestamp_is_minimal() -> Result<(), TestError> {
let status = TaskStatus {
state: TaskState::Working,
message: None,
timestamp: None,
};
let value = serde_json::to_value(&status)?;
let obj = value
.as_object()
.ok_or("a TaskStatus must serialise to a JSON object")?;
assert_eq!(obj.len(), 1);
assert_eq!(
obj.get("state").and_then(serde_json::Value::as_str),
Some("working")
);
Ok(())
}
#[test]
fn artifact_serialises_with_camel_case_name() -> Result<(), TestError> {
let artifact = Artifact::new(text_part("data"))
.with_name("answer")
.with_description("The agent's final answer.");
let value = serde_json::to_value(&artifact)?;
let obj = value
.as_object()
.ok_or("an Artifact must serialise to a JSON object")?;
assert_eq!(
obj.get("name").and_then(serde_json::Value::as_str),
Some("answer")
);
assert_eq!(
obj.get("description").and_then(serde_json::Value::as_str),
Some("The agent's final answer.")
);
assert!(!obj.contains_key("Name"));
assert!(!obj.contains_key("Description"));
Ok(())
}
#[test]
fn artifact_round_trips_with_multiple_parts() -> Result<(), TestError> {
let artifact = Artifact::new(text_part("first")).with_part(text_part("second"));
let json = serde_json::to_string(&artifact)?;
let parsed: Artifact = serde_json::from_str(&json)?;
assert_eq!(parsed, artifact);
Ok(())
}
}