use std::time::Duration;
use chrono::{DateTime, Utc};
use serde_json::{Value, json};
use super::{Mcp, McpError, McpResult};
use crate::error::{Error, Result};
use crate::message::ToolCall;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TaskStatus {
Working,
InputRequired,
Completed,
Failed,
Cancelled,
Other(String),
}
impl TaskStatus {
fn parse(status: &str) -> TaskStatus {
match status {
"working" => TaskStatus::Working,
"input_required" => TaskStatus::InputRequired,
"completed" => TaskStatus::Completed,
"failed" => TaskStatus::Failed,
"cancelled" => TaskStatus::Cancelled,
other => TaskStatus::Other(other.to_string()),
}
}
fn as_str(&self) -> &str {
match self {
TaskStatus::Working => "working",
TaskStatus::InputRequired => "input_required",
TaskStatus::Completed => "completed",
TaskStatus::Failed => "failed",
TaskStatus::Cancelled => "cancelled",
TaskStatus::Other(other) => other,
}
}
}
#[derive(Clone)]
pub struct Task {
mcp: Option<Mcp>,
data: Value,
pub id: String,
answered: Vec<String>,
pub tool_call: Option<ToolCall>,
}
impl std::fmt::Debug for Task {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Task")
.field("id", &self.id)
.field("status", &self.status())
.field("status_message", &self.status_message())
.finish()
}
}
impl Task {
pub(crate) fn new(mcp: Option<Mcp>, data: Value) -> Task {
let id = data
.get("taskId")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string();
Task {
mcp,
data,
id,
answered: Vec::new(),
tool_call: None,
}
}
pub(crate) fn load(mcp: Option<Mcp>, state: &Value, tool_call: Option<ToolCall>) -> Task {
let mut task = Task::new(mcp, state.get("task").cloned().unwrap_or(Value::Null));
task.answered = state
.get("answered")
.and_then(Value::as_array)
.into_iter()
.flatten()
.filter_map(|k| k.as_str().map(str::to_string))
.collect();
task.tool_call = tool_call;
task
}
pub fn server_name(&self) -> String {
self.mcp.as_ref().map_or_else(|| "MCP".into(), Mcp::name)
}
pub fn data(&self) -> &Value {
&self.data
}
pub(crate) fn answered(&self) -> &[String] {
&self.answered
}
pub fn status(&self) -> TaskStatus {
TaskStatus::parse(
self.data
.get("status")
.and_then(Value::as_str)
.unwrap_or(""),
)
}
pub fn status_message(&self) -> Option<&str> {
self.data.get("statusMessage").and_then(Value::as_str)
}
pub fn poll_interval(&self) -> Option<Duration> {
let ms = self.data.get("pollIntervalMs").and_then(Value::as_f64)?;
Duration::try_from_secs_f64(ms / 1000.0).ok()
}
pub fn expires_at(&self) -> Option<DateTime<Utc>> {
let ttl = self.data.get("ttlMs").and_then(Value::as_f64)?;
let created = self.data.get("createdAt").and_then(Value::as_str)?;
let created = DateTime::parse_from_rfc3339(created).ok()?;
if !ttl.is_finite() || ttl < 0.0 {
return None;
}
let ttl = chrono::Duration::try_milliseconds(ttl as i64)?;
created.with_timezone(&Utc).checked_add_signed(ttl)
}
pub fn is_done(&self) -> bool {
matches!(
self.status(),
TaskStatus::Completed | TaskStatus::Failed | TaskStatus::Cancelled
)
}
pub fn is_completed(&self) -> bool {
self.status() == TaskStatus::Completed
}
pub fn is_failed(&self) -> bool {
self.status() == TaskStatus::Failed
}
pub fn is_cancelled(&self) -> bool {
self.status() == TaskStatus::Cancelled
}
pub fn result(&self) -> Result<Option<McpResult>> {
if self.is_failed() || self.is_cancelled() {
return Err(self.error().into());
}
Ok(self.is_completed().then(|| {
McpResult::new(
self.data
.get("result")
.cloned()
.unwrap_or_else(|| json!({})),
None,
)
}))
}
pub async fn refresh(&mut self) -> Result<&mut Self> {
if !self.is_done() {
self.data = self.connected()?.poll_task(&self.id).await?;
}
Ok(self)
}
pub async fn wait(
&mut self,
timeout: Option<Duration>,
interval: Option<Duration>,
) -> Result<&mut Self> {
let mcp = self.connected()?;
mcp.await_task(self, timeout, interval).await?;
Ok(self)
}
pub async fn cancel(&mut self) -> Result<&mut Self> {
if !self.is_done() {
self.connected()?.cancel_task(&self.id).await?;
}
Ok(self)
}
pub fn error(&self) -> McpError {
let details = self.data.get("error");
let message = details
.and_then(|d| d.get("message"))
.and_then(Value::as_str)
.or(self.status_message())
.map(str::to_string)
.unwrap_or_else(|| format!("Task {} was {}", self.id, self.status().as_str()));
McpError {
message,
code: details.and_then(|d| d.get("code")).and_then(Value::as_i64),
data: details.and_then(|d| d.get("data")).cloned(),
..Default::default()
}
}
pub(crate) fn record_answers(&mut self, keys: impl IntoIterator<Item = String>) {
for key in keys {
if !self.answered.contains(&key) {
self.answered.push(key);
}
}
}
pub fn to_h(&self) -> Value {
let mut h = json!({ "task": self.data });
if !self.answered.is_empty() {
h["answered"] = json!(self.answered);
}
h
}
fn connected(&self) -> Result<Mcp> {
self.mcp.clone().ok_or_else(|| {
Error::Configuration(format!(
"Connect the MCP that runs task {} with with_mcp to reach it",
self.id
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn ignores_a_poll_interval_that_is_negative_or_too_large() {
for ms in [json!(-1), json!(1e300)] {
let task = Task::new(None, json!({ "taskId": "t", "pollIntervalMs": ms }));
assert_eq!(task.poll_interval(), None, "{ms}");
}
let task = Task::new(None, json!({ "taskId": "t", "pollIntervalMs": 250 }));
assert_eq!(task.poll_interval(), Some(Duration::from_millis(250)));
}
#[test]
fn has_no_expiry_for_a_ttl_out_of_range() {
for ttl in [json!(1e300), json!(-5)] {
let task = Task::new(
None,
json!({ "taskId": "t", "ttlMs": ttl, "createdAt": "2026-10-13T12:00:00Z" }),
);
assert_eq!(task.expires_at(), None, "{ttl}");
}
let task = Task::new(
None,
json!({ "taskId": "t", "ttlMs": 60000, "createdAt": "2026-10-13T12:00:00Z" }),
);
assert_eq!(
task.expires_at().map(|t| t.to_rfc3339()),
Some("2026-10-13T12:01:00+00:00".to_string())
);
}
}