bamboo_engine/runtime/
task_context.rs1use bamboo_domain::task::{TaskBlocker, TaskEvidence, TaskPhase, TaskPriority, TaskTransition};
7use bamboo_domain::TaskItemStatus;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashSet;
11
12mod auto_status;
13mod conversion;
14mod prompt;
15mod tracking;
16
17#[derive(Debug, Clone)]
22pub struct TaskLoopContext {
23 pub session_id: String,
25
26 pub items: Vec<TaskLoopItem>,
28
29 pub active_item_id: Option<String>,
31
32 pub current_round: u32,
34
35 pub max_rounds: u32,
37
38 pub created_at: DateTime<Utc>,
40
41 pub updated_at: DateTime<Utc>,
43
44 pub version: u64,
46
47 pub task_list_dirty: bool,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, Default)]
56pub struct TaskLoopItem {
57 pub id: String,
59
60 pub description: String,
62
63 pub status: TaskItemStatus,
65
66 pub depends_on: Vec<String>,
68
69 pub notes: String,
71
72 pub active_form: Option<String>,
74
75 pub parent_id: Option<String>,
77
78 pub phase: TaskPhase,
80
81 pub priority: TaskPriority,
83
84 pub completion_criteria: Vec<String>,
86
87 pub evidence: Vec<TaskEvidence>,
89
90 pub blockers: Vec<TaskBlocker>,
92
93 pub transitions: Vec<TaskTransition>,
95
96 pub tool_calls: Vec<ToolCallRecord>,
98
99 pub started_at_round: Option<u32>,
101
102 pub completed_at_round: Option<u32>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ToolCallRecord {
109 pub round: u32,
111
112 pub tool_name: String,
114
115 pub success: bool,
117
118 pub timestamp: DateTime<Utc>,
120}
121
122impl TaskLoopContext {
123 pub fn is_all_completed(&self) -> bool {
125 !self.items.is_empty()
126 && self
127 .items
128 .iter()
129 .all(|item| matches!(item.status, TaskItemStatus::Completed))
130 }
131
132 fn completed_item_ids(&self) -> HashSet<String> {
133 self.items
134 .iter()
135 .filter(|item| matches!(item.status, TaskItemStatus::Completed))
136 .map(|item| item.id.clone())
137 .collect()
138 }
139
140 fn unresolved_dependencies(&self, depends_on: &[String]) -> Vec<String> {
141 let completed = self.completed_item_ids();
142 depends_on
143 .iter()
144 .filter(|dependency| !completed.contains(*dependency))
145 .cloned()
146 .collect()
147 }
148
149 fn append_item_notes(item: &mut TaskLoopItem, note: &str) {
150 let note = note.trim();
151 if note.is_empty() {
152 return;
153 }
154 if !item.notes.is_empty() {
155 item.notes.push('\n');
156 }
157 item.notes.push_str(note);
158 }
159
160 fn transition_item(
161 item: &mut TaskLoopItem,
162 status: TaskItemStatus,
163 reason: Option<&str>,
164 round: Option<u32>,
165 ) -> bool {
166 let reason = reason.map(str::trim).filter(|value| !value.is_empty());
167 if item.status == status {
168 if let Some(reason) = reason {
169 Self::append_item_notes(item, reason);
170 }
171 return false;
172 }
173
174 let transition = TaskTransition {
175 from_status: item.status.clone(),
176 to_status: status.clone(),
177 reason: reason.map(ToOwned::to_owned),
178 round,
179 changed_at: Utc::now(),
180 };
181 item.status = status.clone();
182
183 if matches!(status, TaskItemStatus::InProgress) && item.started_at_round.is_none() {
184 item.started_at_round = round;
185 }
186 if matches!(status, TaskItemStatus::Completed) {
187 item.completed_at_round = round;
188 }
189 if let Some(reason) = transition.reason.as_deref() {
190 Self::append_item_notes(item, reason);
191 }
192
193 item.transitions.push(transition);
194 true
195 }
196
197 fn add_item_blocker(item: &mut TaskLoopItem, blocker: TaskBlocker) {
198 if blocker.summary.trim().is_empty() {
199 return;
200 }
201 if item.blockers.iter().any(|existing| {
202 existing.kind == blocker.kind
203 && existing.summary == blocker.summary
204 && existing.waiting_on == blocker.waiting_on
205 }) {
206 return;
207 }
208 item.blockers.push(blocker);
209 }
210
211 fn push_item_evidence(item: &mut TaskLoopItem, evidence: TaskEvidence) {
212 if evidence.summary.trim().is_empty() {
213 return;
214 }
215 item.evidence.push(evidence);
216 }
217}
218
219#[cfg(test)]
220mod tests;