Skip to main content

codetether_agent/session/tasks/
state.rs

1//! Materialized view of a task log: current goal + open tasks.
2
3use chrono::{DateTime, Utc};
4use std::collections::BTreeMap;
5
6use super::event::{TaskEvent, TaskStatus};
7
8/// The session's currently-declared goal.
9#[derive(Clone, Debug)]
10pub struct Goal {
11    pub objective: String,
12    pub success_criteria: Vec<String>,
13    pub forbidden: Vec<String>,
14    pub set_at: DateTime<Utc>,
15    pub last_reaffirmed_at: DateTime<Utc>,
16}
17
18/// A task with its latest status.
19#[derive(Clone, Debug)]
20pub struct Task {
21    pub id: String,
22    pub content: String,
23    pub parent_id: Option<String>,
24    pub status: TaskStatus,
25    pub last_note: Option<String>,
26}
27
28/// Folded state derived from a [`super::TaskLog`].
29#[derive(Clone, Debug, Default)]
30pub struct TaskState {
31    pub goal: Option<Goal>,
32    pub tasks: BTreeMap<String, Task>,
33    pub tool_calls_since_reaffirm: u32,
34    pub errors_since_reaffirm: u32,
35}
36
37impl TaskState {
38    /// Fold a sequence of events into current state.
39    pub fn from_log(events: &[TaskEvent]) -> Self {
40        let mut state = Self::default();
41        for ev in events {
42            state.apply(ev);
43        }
44        state
45    }
46
47    /// Tasks in a non-terminal state, in id order.
48    pub fn open_tasks(&self) -> Vec<&Task> {
49        self.tasks
50            .values()
51            .filter(|t| matches!(t.status, TaskStatus::Pending | TaskStatus::InProgress))
52            .collect()
53    }
54
55    fn apply(&mut self, ev: &TaskEvent) {
56        match ev {
57            TaskEvent::GoalSet {
58                at,
59                objective,
60                success_criteria,
61                forbidden,
62            } => {
63                self.goal = Some(Goal {
64                    objective: objective.clone(),
65                    success_criteria: success_criteria.clone(),
66                    forbidden: forbidden.clone(),
67                    set_at: *at,
68                    last_reaffirmed_at: *at,
69                });
70            }
71            TaskEvent::GoalReaffirmed { at, .. } => {
72                if let Some(g) = self.goal.as_mut() {
73                    g.last_reaffirmed_at = *at;
74                }
75                self.tool_calls_since_reaffirm = 0;
76                self.errors_since_reaffirm = 0;
77            }
78            TaskEvent::GoalCleared { .. } => {
79                self.goal = None;
80            }
81            TaskEvent::TaskAdded {
82                id,
83                content,
84                parent_id,
85                ..
86            } => {
87                self.tasks.insert(
88                    id.clone(),
89                    Task {
90                        id: id.clone(),
91                        content: content.clone(),
92                        parent_id: parent_id.clone(),
93                        status: TaskStatus::Pending,
94                        last_note: None,
95                    },
96                );
97            }
98            TaskEvent::TaskStatus {
99                id, status, note, ..
100            } => {
101                if let Some(t) = self.tasks.get_mut(id) {
102                    t.status = status.clone();
103                    t.last_note = note.clone();
104                }
105            }
106            TaskEvent::DriftDetected { .. } => {}
107        }
108    }
109}