1use crate::state::now_ms;
10use serde::{Deserialize, Serialize};
11use serde_json::{Value, json};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
16#[serde(rename_all = "snake_case")]
17pub enum State {
18 #[default]
19 Submitted,
20 Working,
21 InputRequired,
22 Completed,
23 Failed,
24 Canceled,
25 Rejected,
26}
27
28impl State {
29 pub fn wire(self) -> &'static str {
30 match self {
31 State::Submitted => "TASK_STATE_SUBMITTED",
32 State::Working => "TASK_STATE_WORKING",
33 State::InputRequired => "TASK_STATE_INPUT_REQUIRED",
34 State::Completed => "TASK_STATE_COMPLETED",
35 State::Failed => "TASK_STATE_FAILED",
36 State::Canceled => "TASK_STATE_CANCELED",
37 State::Rejected => "TASK_STATE_REJECTED",
38 }
39 }
40 pub fn is_terminal(self) -> bool {
41 matches!(
42 self,
43 State::Completed | State::Failed | State::Canceled | State::Rejected
44 )
45 }
46 pub fn from_run(status: &str) -> State {
48 match status {
49 "completed" => State::Completed,
50 "refused" => State::Rejected,
51 "cancelled" => State::Canceled,
52 "running" | "suspended" | "paused" | "pending" => State::Working,
53 _ => State::Failed,
54 }
55 }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum Link {
62 Run {
63 id: String,
64 },
65 Subagent {
66 handle: String,
67 },
68 Turn {
70 ctx: String,
71 },
72}
73
74#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79pub struct PushTarget {
80 pub id: String,
81 pub url: String,
82 #[serde(default, skip_serializing_if = "String::is_empty")]
83 pub token: String,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub bearer: Option<String>,
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
90pub struct Task {
91 pub id: String,
92 pub context_id: String,
93 #[serde(default)]
94 pub state: State,
95 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub principal: Option<String>,
97 pub link: Link,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub message: Option<String>,
101 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub ask_schema: Option<Value>,
109 #[serde(default, skip_serializing_if = "Vec::is_empty")]
111 pub artifacts: Vec<String>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub result: Option<Value>,
115 #[serde(default)]
116 pub created: u64,
117 #[serde(default)]
118 pub updated: u64,
119 #[serde(default, skip_serializing_if = "Vec::is_empty")]
121 pub history: Vec<Value>,
122 #[serde(default, skip_serializing_if = "Vec::is_empty")]
126 pub push: Vec<PushTarget>,
127 #[serde(skip)]
128 pub dirty: bool,
129}
130
131impl Task {
132 pub fn new(id: &str, context_id: &str, principal: Option<&str>, link: Link) -> Task {
133 let now = now_ms();
134 Task {
135 ask_schema: None,
136 id: id.to_string(),
137 context_id: context_id.to_string(),
138 state: State::Submitted,
139 principal: principal.map(str::to_string),
140 link,
141 message: None,
142 artifacts: Vec::new(),
143 result: None,
144 created: now,
145 updated: now,
146 history: vec![json!({"state": State::Submitted.wire(), "ts": now})],
147 push: Vec::new(),
148 dirty: true,
149 }
150 }
151
152 pub fn transition(&mut self, state: State, message: Option<String>) {
153 if self.state == state && self.message == message {
154 return;
155 }
156 self.state = state;
157 if message.is_some() {
158 self.message = message;
159 }
160 self.updated = now_ms();
161 self.history
162 .push(json!({"state": state.wire(), "ts": self.updated}));
163 if self.history.len() > 64 {
164 self.history.remove(0);
165 }
166 self.dirty = true;
167 }
168
169 pub fn add_artifact(&mut self, id: &str) {
170 if !self.artifacts.iter().any(|a| a == id) {
171 self.artifacts.push(id.to_string());
172 self.updated = now_ms();
173 self.dirty = true;
174 }
175 }
176
177 pub fn set_result(&mut self, v: Value) {
178 self.result = Some(v);
179 self.updated = now_ms();
180 self.dirty = true;
181 }
182
183 #[cfg(feature = "a2a")]
187 pub fn to_a2a(&self) -> Value {
188 serde_json::to_value(crate::a2a::wire::task(self)).unwrap_or(Value::Null)
189 }
190
191 #[cfg(feature = "a2a")]
194 pub fn summary(&self) -> Value {
195 serde_json::to_value(crate::a2a::wire::task_summary(self)).unwrap_or(Value::Null)
196 }
197}
198#[cfg(test)]
199mod tests {
200 use super::*;
201
202 #[test]
205 fn a_task_records_its_lifecycle_and_survives_a_round_trip() {
206 let mut t = Task::new(
207 "task-1",
208 "ctx-1",
209 Some("user:a"),
210 Link::Run { id: "r1".into() },
211 );
212 assert_eq!(t.state, State::Submitted);
213 t.transition(State::Working, None);
214 t.transition(State::Working, None); assert_eq!(t.history.len(), 2);
216 t.add_artifact("art-9");
217 t.add_artifact("art-9"); assert_eq!(t.artifacts.len(), 1);
219 t.set_result(json!({"answer": 42}));
220 t.transition(State::Completed, Some("done".into()));
221 assert!(t.state.is_terminal());
222 assert_eq!(t.message.as_deref(), Some("done"));
223 assert_eq!(State::from_run("refused"), State::Rejected);
224 assert_eq!(State::from_run("running"), State::Working);
225
226 let v = serde_json::to_value(&t).unwrap();
227 let back: Task = serde_json::from_value(v).unwrap();
228 assert_eq!(back.state, t.state);
229 assert_eq!(back.history.len(), t.history.len());
230 assert!(!back.dirty);
231 }
232}