1use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8use crate::graph::TaskGraph;
9
10#[derive(Debug, Clone, Serialize, Deserialize)]
12#[serde(tag = "state", rename_all = "snake_case")]
13pub enum StepState {
14 Pending,
16 Ready,
18 Running { started_at: DateTime<Utc> },
20 AwaitingConfirmation { nonce: String, since: DateTime<Utc> },
22 Completed {
24 outcome: StepOutcome,
25 completed_at: DateTime<Utc>,
26 },
27 Failed {
29 error: String,
30 retryable: bool,
31 failed_at: DateTime<Utc>,
32 },
33 Skipped { reason: String },
35 Cancelled,
37}
38
39impl StepState {
40 pub fn is_terminal(&self) -> bool {
41 matches!(
42 self,
43 StepState::Completed { .. }
44 | StepState::Failed { .. }
45 | StepState::Skipped { .. }
46 | StepState::Cancelled
47 )
48 }
49
50 pub fn is_success(&self) -> bool {
51 matches!(self, StepState::Completed { .. })
52 }
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct StepOutcome {
58 pub stdout: String,
59 pub stderr: String,
60 pub exit_code: Option<i32>,
61 pub artifacts: Vec<String>,
62 pub summary: String,
63}
64
65#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
73#[serde(rename_all = "snake_case")]
74pub enum TaskPhase {
75 Planning,
77 AwaitingApproval,
79 Executing,
81 Reconciling,
86 Completed,
88 Failed,
91 Cancelled,
93}
94
95impl TaskPhase {
96 pub fn as_str(self) -> &'static str {
100 match self {
101 TaskPhase::Planning => "planning",
102 TaskPhase::AwaitingApproval => "awaiting_approval",
103 TaskPhase::Executing => "executing",
104 TaskPhase::Reconciling => "reconciling",
105 TaskPhase::Completed => "completed",
106 TaskPhase::Failed => "failed",
107 TaskPhase::Cancelled => "cancelled",
108 }
109 }
110
111 pub fn is_terminal(self) -> bool {
113 matches!(
114 self,
115 TaskPhase::Completed | TaskPhase::Failed | TaskPhase::Cancelled
116 )
117 }
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct TaskState {
123 pub id: String,
125 pub request: String,
127 pub graph: TaskGraph,
129 pub step_states: HashMap<String, StepState>,
131 pub created_at: DateTime<Utc>,
133 pub completed_at: Option<DateTime<Utc>>,
135 pub phase: TaskPhase,
137 #[serde(default)]
140 pub replan_attempts: u32,
141}
142
143impl TaskState {
144 pub fn new(id: String, request: String, graph: TaskGraph) -> Self {
146 let step_states: HashMap<String, StepState> = graph
147 .steps
148 .keys()
149 .map(|id| (id.clone(), StepState::Pending))
150 .collect();
151
152 Self {
153 id,
154 request,
155 graph,
156 step_states,
157 created_at: Utc::now(),
158 completed_at: None,
159 phase: TaskPhase::Planning,
160 replan_attempts: 0,
161 }
162 }
163
164 pub fn set_step_state(&mut self, step_id: &str, state: StepState) {
166 self.step_states.insert(step_id.to_string(), state);
167 }
168
169 pub fn is_complete(&self) -> bool {
171 self.step_states.values().all(|s| s.is_terminal())
172 }
173
174 pub fn all_succeeded(&self) -> bool {
176 self.step_states.values().all(|s| s.is_success())
177 }
178
179 pub fn counts(&self) -> TaskCounts {
181 let mut c = TaskCounts::default();
182 for state in self.step_states.values() {
183 match state {
184 StepState::Pending => c.pending += 1,
185 StepState::Ready => c.ready += 1,
186 StepState::Running { .. } => c.running += 1,
187 StepState::AwaitingConfirmation { .. } => c.awaiting += 1,
188 StepState::Completed { .. } => c.completed += 1,
189 StepState::Failed { .. } => c.failed += 1,
190 StepState::Skipped { .. } => c.skipped += 1,
191 StepState::Cancelled => c.cancelled += 1,
192 }
193 }
194 c
195 }
196}
197
198#[derive(Debug, Default, Clone, Serialize, Deserialize)]
200pub struct TaskCounts {
201 pub pending: usize,
202 pub ready: usize,
203 pub running: usize,
204 pub awaiting: usize,
205 pub completed: usize,
206 pub failed: usize,
207 pub skipped: usize,
208 pub cancelled: usize,
209}
210
211impl TaskCounts {
212 pub fn total(&self) -> usize {
213 self.pending
214 + self.ready
215 + self.running
216 + self.awaiting
217 + self.completed
218 + self.failed
219 + self.skipped
220 + self.cancelled
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::graph::TaskGraph;
228 use crate::step::{StepAction, TaskStep};
229
230 fn simple_graph() -> TaskGraph {
231 let steps = vec![
232 TaskStep {
233 id: "s1".to_string(),
234 description: "Step 1".to_string(),
235 action: StepAction::Plan {
236 output: "plan".to_string(),
237 },
238 depends_on: vec![],
239 tier: audit::ActionTier::Execute,
240 estimated_tokens: 0,
241 },
242 TaskStep {
243 id: "s2".to_string(),
244 description: "Step 2".to_string(),
245 action: StepAction::Test {
246 command: "cargo test".to_string(),
247 workdir: "/tmp".into(),
248 },
249 depends_on: vec!["s1".to_string()],
250 tier: audit::ActionTier::Execute,
251 estimated_tokens: 0,
252 },
253 ];
254 TaskGraph::from_steps(steps).unwrap()
255 }
256
257 #[test]
258 fn test_new_task_state() {
259 let graph = simple_graph();
260 let state = TaskState::new("t1".to_string(), "build it".to_string(), graph);
261 assert_eq!(state.phase, TaskPhase::Planning);
262 assert!(!state.is_complete());
263 let counts = state.counts();
264 assert_eq!(counts.pending, 2);
265 assert_eq!(counts.total(), 2);
266 }
267
268 #[test]
269 fn test_step_transitions() {
270 let graph = simple_graph();
271 let mut state = TaskState::new("t1".to_string(), "build it".to_string(), graph);
272
273 state.set_step_state(
274 "s1",
275 StepState::Completed {
276 outcome: StepOutcome {
277 stdout: String::new(),
278 stderr: String::new(),
279 exit_code: Some(0),
280 artifacts: vec![],
281 summary: "done".to_string(),
282 },
283 completed_at: Utc::now(),
284 },
285 );
286 assert!(!state.is_complete());
287
288 state.set_step_state(
289 "s2",
290 StepState::Completed {
291 outcome: StepOutcome {
292 stdout: String::new(),
293 stderr: String::new(),
294 exit_code: Some(0),
295 artifacts: vec![],
296 summary: "done".to_string(),
297 },
298 completed_at: Utc::now(),
299 },
300 );
301 assert!(state.is_complete());
302 assert!(state.all_succeeded());
303 }
304}