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::{SessionTaskStatus, TaskEvent};
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: SessionTaskStatus,
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.values().filter(|t| t.status.is_open()).collect()
50    }
51
52    fn apply(&mut self, ev: &TaskEvent) {
53        match ev {
54            TaskEvent::GoalSet {
55                at,
56                objective,
57                success_criteria,
58                forbidden,
59                ..
60            } => {
61                self.goal = Some(Goal {
62                    objective: objective.clone(),
63                    success_criteria: success_criteria.clone(),
64                    forbidden: forbidden.clone(),
65                    set_at: *at,
66                    last_reaffirmed_at: *at,
67                });
68            }
69            TaskEvent::GoalReaffirmed { at, .. } => {
70                if let Some(g) = self.goal.as_mut() {
71                    g.last_reaffirmed_at = *at;
72                }
73                self.tool_calls_since_reaffirm = 0;
74                self.errors_since_reaffirm = 0;
75            }
76            TaskEvent::GoalCleared { .. } => {
77                self.goal = None;
78            }
79            TaskEvent::TaskAdded {
80                id,
81                content,
82                parent_id,
83                ..
84            } => {
85                self.tasks.insert(
86                    id.clone(),
87                    Task {
88                        id: id.clone(),
89                        content: content.clone(),
90                        parent_id: parent_id.clone(),
91                        status: SessionTaskStatus::Pending,
92                        last_note: None,
93                    },
94                );
95            }
96            TaskEvent::TaskStatus {
97                id, status, note, ..
98            } => {
99                if let Some(t) = self.tasks.get_mut(id) {
100                    t.status = status.clone();
101                    t.last_note = note.clone();
102                }
103            }
104            TaskEvent::DriftDetected { .. } => {}
105        }
106    }
107}