use serde::{Deserialize, Deserializer, Serialize};
use serde_json::{Map, Value};
use crate::error::JsonRpcError;
use crate::protocol::{InputRequests, InputResponses, RequestMeta, ResultType, TaskStatus};
pub const EXTENSION_ID: &str = crate::protocol::TASKS_EXTENSION_ID;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskMetadata {
pub task_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub status_message: Option<String>,
pub created_at: String,
pub last_updated_at: String,
pub ttl_ms: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub poll_interval_ms: Option<u64>,
}
impl TaskMetadata {
pub fn new(
task_id: impl Into<String>,
created_at: impl Into<String>,
last_updated_at: impl Into<String>,
ttl_ms: Option<u64>,
) -> Self {
Self {
task_id: task_id.into(),
status_message: None,
created_at: created_at.into(),
last_updated_at: last_updated_at.into(),
ttl_ms,
poll_interval_ms: None,
}
}
pub fn with_status_message(mut self, message: impl Into<String>) -> Self {
self.status_message = Some(message.into());
self
}
pub fn with_poll_interval_ms(mut self, poll_interval_ms: u64) -> Self {
self.poll_interval_ms = Some(poll_interval_ms);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Task {
#[serde(flatten)]
pub metadata: TaskMetadata,
pub status: TaskStatus,
}
impl Task {
pub fn new(metadata: TaskMetadata, status: TaskStatus) -> Self {
Self { metadata, status }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
#[non_exhaustive]
pub enum DetailedTask {
Working {
#[serde(flatten)]
metadata: TaskMetadata,
},
InputRequired {
#[serde(flatten)]
metadata: TaskMetadata,
#[serde(rename = "inputRequests")]
input_requests: InputRequests,
},
Completed {
#[serde(flatten)]
metadata: TaskMetadata,
result: Map<String, Value>,
},
Failed {
#[serde(flatten)]
metadata: TaskMetadata,
error: JsonRpcError,
},
Cancelled {
#[serde(flatten)]
metadata: TaskMetadata,
},
}
impl DetailedTask {
pub fn working(metadata: TaskMetadata) -> Self {
Self::Working { metadata }
}
pub fn input_required(metadata: TaskMetadata, input_requests: InputRequests) -> Self {
Self::InputRequired {
metadata,
input_requests,
}
}
pub fn completed(metadata: TaskMetadata, result: Map<String, Value>) -> Self {
Self::Completed { metadata, result }
}
pub fn failed(metadata: TaskMetadata, error: JsonRpcError) -> Self {
Self::Failed { metadata, error }
}
pub fn cancelled(metadata: TaskMetadata) -> Self {
Self::Cancelled { metadata }
}
pub fn status(&self) -> TaskStatus {
match self {
Self::Working { .. } => TaskStatus::Working,
Self::InputRequired { .. } => TaskStatus::InputRequired,
Self::Completed { .. } => TaskStatus::Completed,
Self::Failed { .. } => TaskStatus::Failed,
Self::Cancelled { .. } => TaskStatus::Cancelled,
}
}
pub fn metadata(&self) -> &TaskMetadata {
match self {
Self::Working { metadata }
| Self::InputRequired { metadata, .. }
| Self::Completed { metadata, .. }
| Self::Failed { metadata, .. }
| Self::Cancelled { metadata } => metadata,
}
}
pub fn input_requests(&self) -> Option<&InputRequests> {
match self {
Self::InputRequired { input_requests, .. } => Some(input_requests),
_ => None,
}
}
pub fn result(&self) -> Option<&Map<String, Value>> {
match self {
Self::Completed { result, .. } => Some(result),
_ => None,
}
}
pub fn error(&self) -> Option<&JsonRpcError> {
match self {
Self::Failed { error, .. } => Some(error),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateTaskResult {
#[serde(
rename = "resultType",
deserialize_with = "deserialize_task_result_type"
)]
result_type: ResultType,
#[serde(flatten)]
pub task: Task,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Map<String, Value>>,
}
impl CreateTaskResult {
pub fn new(task: Task) -> Self {
Self {
result_type: ResultType::Other(crate::protocol::RESULT_TYPE_TASK.to_string()),
task,
meta: None,
}
}
pub fn result_type(&self) -> &ResultType {
&self.result_type
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetTaskResult {
#[serde(
rename = "resultType",
deserialize_with = "deserialize_complete_result_type"
)]
result_type: ResultType,
#[serde(flatten)]
pub task: DetailedTask,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Map<String, Value>>,
}
impl GetTaskResult {
pub fn new(task: DetailedTask) -> Self {
Self {
result_type: ResultType::Complete,
task,
meta: None,
}
}
pub fn result_type(&self) -> &ResultType {
&self.result_type
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TaskAcknowledgement {
#[serde(
rename = "resultType",
deserialize_with = "deserialize_complete_result_type"
)]
result_type: ResultType,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Map<String, Value>>,
}
impl TaskAcknowledgement {
pub fn new() -> Self {
Self {
result_type: ResultType::Complete,
meta: None,
}
}
pub fn result_type(&self) -> &ResultType {
&self.result_type
}
}
impl Default for TaskAcknowledgement {
fn default() -> Self {
Self::new()
}
}
pub type UpdateTaskResult = TaskAcknowledgement;
pub type CancelTaskResult = TaskAcknowledgement;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GetTaskParams {
pub task_id: String,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct UpdateTaskParams {
pub task_id: String,
pub input_responses: InputResponses,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CancelTaskParams {
pub task_id: String,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<RequestMeta>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaskStatusNotificationParams {
#[serde(flatten)]
pub task: DetailedTask,
#[serde(rename = "_meta", default, skip_serializing_if = "Option::is_none")]
pub meta: Option<Map<String, Value>>,
}
fn deserialize_task_result_type<'de, D>(deserializer: D) -> Result<ResultType, D::Error>
where
D: Deserializer<'de>,
{
let result_type = ResultType::deserialize(deserializer)?;
if result_type.as_str() == crate::protocol::RESULT_TYPE_TASK {
Ok(result_type)
} else {
Err(serde::de::Error::custom(
"resultType must be \"task\" for CreateTaskResult",
))
}
}
fn deserialize_complete_result_type<'de, D>(deserializer: D) -> Result<ResultType, D::Error>
where
D: Deserializer<'de>,
{
let result_type = ResultType::deserialize(deserializer)?;
if result_type == ResultType::Complete {
Ok(result_type)
} else {
Err(serde::de::Error::custom("resultType must be \"complete\""))
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::protocol::{
ElicitAction, ElicitResult, InputRequest, InputResponse, ListRootsParams,
};
fn metadata() -> TaskMetadata {
TaskMetadata::new(
"task-2f68f7c9",
"2026-07-30T00:00:00Z",
"2026-07-30T00:00:01Z",
Some(60_000),
)
.with_poll_interval_ms(2_000)
}
#[test]
fn task_metadata_uses_exact_final_field_names_and_required_null_ttl() {
let value = serde_json::to_value(Task::new(metadata(), TaskStatus::Working)).unwrap();
assert_eq!(value["ttlMs"], 60_000);
assert_eq!(value["pollIntervalMs"], 2_000);
assert!(value.get("ttl").is_none());
assert!(value.get("pollInterval").is_none());
let unlimited = TaskMetadata::new(
"task-unlimited",
"2026-07-30T00:00:00Z",
"2026-07-30T00:00:00Z",
None,
);
let value = serde_json::to_value(Task::new(unlimited, TaskStatus::Working)).unwrap();
assert_eq!(value["ttlMs"], Value::Null);
}
#[test]
fn create_task_result_is_flat_and_rejects_wrong_discriminator() {
let result = CreateTaskResult::new(Task::new(metadata(), TaskStatus::Working));
let value = serde_json::to_value(result).unwrap();
assert_eq!(value["resultType"], "task");
assert_eq!(value["taskId"], "task-2f68f7c9");
assert!(value.get("task").is_none());
let mut invalid = value;
invalid["resultType"] = json!("complete");
assert!(serde_json::from_value::<CreateTaskResult>(invalid).is_err());
}
#[test]
fn detailed_task_variants_have_status_specific_payloads() {
let working =
serde_json::to_value(GetTaskResult::new(DetailedTask::working(metadata()))).unwrap();
assert_eq!(working["resultType"], "complete");
assert_eq!(working["status"], "working");
assert!(working.get("inputRequests").is_none());
assert!(working.get("result").is_none());
assert!(working.get("error").is_none());
let mut requests = InputRequests::new();
requests.insert(
"roots".to_string(),
InputRequest::ListRoots(ListRootsParams { meta: None }),
);
let input_required = serde_json::to_value(GetTaskResult::new(
DetailedTask::input_required(metadata(), requests),
))
.unwrap();
assert_eq!(input_required["status"], "input_required");
assert_eq!(
input_required["inputRequests"]["roots"]["method"],
"roots/list"
);
let result = json!({
"content": [{"type": "text", "text": "domain error"}],
"isError": true
})
.as_object()
.unwrap()
.clone();
let completed = serde_json::to_value(GetTaskResult::new(DetailedTask::completed(
metadata(),
result,
)))
.unwrap();
assert_eq!(completed["status"], "completed");
assert_eq!(completed["result"]["isError"], true);
let failed = serde_json::to_value(GetTaskResult::new(DetailedTask::failed(
metadata(),
JsonRpcError::internal_error("worker crashed"),
)))
.unwrap();
assert_eq!(failed["status"], "failed");
assert_eq!(failed["error"]["code"], -32603);
let cancelled =
serde_json::to_value(GetTaskResult::new(DetailedTask::cancelled(metadata()))).unwrap();
assert_eq!(cancelled["status"], "cancelled");
}
#[test]
fn detailed_task_rejects_missing_required_status_payloads() {
let base = json!({
"taskId": "task-1",
"createdAt": "2026-07-30T00:00:00Z",
"lastUpdatedAt": "2026-07-30T00:00:00Z",
"ttlMs": null
});
for status in ["input_required", "completed", "failed"] {
let mut value = base.clone();
value["status"] = json!(status);
assert!(
serde_json::from_value::<DetailedTask>(value).is_err(),
"{status}"
);
}
}
#[test]
fn update_params_are_typed_and_acknowledgements_are_complete() {
let mut responses = InputResponses::new();
responses.insert(
"approval".to_string(),
InputResponse::Elicit(ElicitResult {
action: ElicitAction::Accept,
content: None,
meta: None,
}),
);
let params = UpdateTaskParams {
task_id: "task-2f68f7c9".to_string(),
input_responses: responses,
meta: None,
};
let value = serde_json::to_value(params).unwrap();
assert_eq!(value["taskId"], "task-2f68f7c9");
assert_eq!(
value["inputResponses"]["approval"]["action"],
json!("accept")
);
let ack = serde_json::to_value(TaskAcknowledgement::new()).unwrap();
assert_eq!(ack, json!({"resultType": "complete"}));
assert!(
serde_json::from_value::<TaskAcknowledgement>(json!({
"resultType": "task"
}))
.is_err()
);
}
#[test]
fn notification_payload_matches_tasks_get_detailed_shape() {
let notification = TaskStatusNotificationParams {
task: DetailedTask::cancelled(metadata()),
meta: None,
};
let value = serde_json::to_value(notification).unwrap();
assert_eq!(value["status"], "cancelled");
assert_eq!(value["taskId"], "task-2f68f7c9");
assert_eq!(value["ttlMs"], 60_000);
}
}