use crate::state::now_ms;
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum State {
#[default]
Submitted,
Working,
InputRequired,
Completed,
Failed,
Canceled,
Rejected,
}
impl State {
pub fn wire(self) -> &'static str {
match self {
State::Submitted => "TASK_STATE_SUBMITTED",
State::Working => "TASK_STATE_WORKING",
State::InputRequired => "TASK_STATE_INPUT_REQUIRED",
State::Completed => "TASK_STATE_COMPLETED",
State::Failed => "TASK_STATE_FAILED",
State::Canceled => "TASK_STATE_CANCELED",
State::Rejected => "TASK_STATE_REJECTED",
}
}
pub fn is_terminal(self) -> bool {
matches!(
self,
State::Completed | State::Failed | State::Canceled | State::Rejected
)
}
pub fn from_run(status: &str) -> State {
match status {
"completed" => State::Completed,
"refused" => State::Rejected,
"cancelled" => State::Canceled,
"running" | "suspended" | "paused" | "pending" => State::Working,
_ => State::Failed,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Link {
Run {
id: String,
},
Subagent {
handle: String,
},
Turn {
ctx: String,
},
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PushTarget {
pub id: String,
pub url: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub token: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bearer: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Task {
pub id: String,
pub context_id: String,
#[serde(default)]
pub state: State,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub principal: Option<String>,
pub link: Link,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ask_schema: Option<Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub artifacts: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(default)]
pub created: u64,
#[serde(default)]
pub updated: u64,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub history: Vec<Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub push: Vec<PushTarget>,
#[serde(skip)]
pub dirty: bool,
}
impl Task {
pub fn new(id: &str, context_id: &str, principal: Option<&str>, link: Link) -> Task {
let now = now_ms();
Task {
ask_schema: None,
id: id.to_string(),
context_id: context_id.to_string(),
state: State::Submitted,
principal: principal.map(str::to_string),
link,
message: None,
artifacts: Vec::new(),
result: None,
created: now,
updated: now,
history: vec![json!({"state": State::Submitted.wire(), "ts": now})],
push: Vec::new(),
dirty: true,
}
}
pub fn transition(&mut self, state: State, message: Option<String>) {
if self.state == state && self.message == message {
return;
}
self.state = state;
if message.is_some() {
self.message = message;
}
self.updated = now_ms();
self.history
.push(json!({"state": state.wire(), "ts": self.updated}));
if self.history.len() > 64 {
self.history.remove(0);
}
self.dirty = true;
}
pub fn add_artifact(&mut self, id: &str) {
if !self.artifacts.iter().any(|a| a == id) {
self.artifacts.push(id.to_string());
self.updated = now_ms();
self.dirty = true;
}
}
pub fn set_result(&mut self, v: Value) {
self.result = Some(v);
self.updated = now_ms();
self.dirty = true;
}
#[cfg(feature = "a2a")]
pub fn to_a2a(&self) -> Value {
serde_json::to_value(crate::a2a::wire::task(self)).unwrap_or(Value::Null)
}
#[cfg(feature = "a2a")]
pub fn summary(&self) -> Value {
serde_json::to_value(crate::a2a::wire::task_summary(self)).unwrap_or(Value::Null)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_task_records_its_lifecycle_and_survives_a_round_trip() {
let mut t = Task::new(
"task-1",
"ctx-1",
Some("user:a"),
Link::Run { id: "r1".into() },
);
assert_eq!(t.state, State::Submitted);
t.transition(State::Working, None);
t.transition(State::Working, None); assert_eq!(t.history.len(), 2);
t.add_artifact("art-9");
t.add_artifact("art-9"); assert_eq!(t.artifacts.len(), 1);
t.set_result(json!({"answer": 42}));
t.transition(State::Completed, Some("done".into()));
assert!(t.state.is_terminal());
assert_eq!(t.message.as_deref(), Some("done"));
assert_eq!(State::from_run("refused"), State::Rejected);
assert_eq!(State::from_run("running"), State::Working);
let v = serde_json::to_value(&t).unwrap();
let back: Task = serde_json::from_value(v).unwrap();
assert_eq!(back.state, t.state);
assert_eq!(back.history.len(), t.history.len());
assert!(!back.dirty);
}
}