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: Option<u32>,
38
39 pub created_at: DateTime<Utc>,
41
42 pub updated_at: DateTime<Utc>,
44
45 pub version: u64,
47
48 pub task_list_dirty: bool,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize, Default)]
57pub struct TaskLoopItem {
58 pub id: String,
60
61 pub description: String,
63
64 pub status: TaskItemStatus,
66
67 pub depends_on: Vec<String>,
69
70 pub notes: String,
72
73 pub active_form: Option<String>,
75
76 pub parent_id: Option<String>,
78
79 pub phase: TaskPhase,
81
82 pub priority: TaskPriority,
84
85 pub completion_criteria: Vec<String>,
87
88 pub evidence: Vec<TaskEvidence>,
90
91 pub blockers: Vec<TaskBlocker>,
93
94 pub transitions: Vec<TaskTransition>,
96
97 pub tool_calls: Vec<ToolCallRecord>,
99
100 pub started_at_round: Option<u32>,
102
103 pub completed_at_round: Option<u32>,
105}
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ToolCallRecord {
110 pub round: u32,
112
113 pub tool_name: String,
115
116 pub success: bool,
118
119 pub timestamp: DateTime<Utc>,
121}
122
123impl TaskLoopContext {
124 pub fn is_all_completed(&self) -> bool {
126 !self.items.is_empty()
127 && self
128 .items
129 .iter()
130 .all(|item| matches!(item.status, TaskItemStatus::Completed))
131 }
132
133 fn completed_item_ids(&self) -> HashSet<String> {
134 self.items
135 .iter()
136 .filter(|item| matches!(item.status, TaskItemStatus::Completed))
137 .map(|item| item.id.clone())
138 .collect()
139 }
140
141 fn unresolved_dependencies(&self, depends_on: &[String]) -> Vec<String> {
142 let completed = self.completed_item_ids();
143 depends_on
144 .iter()
145 .filter(|dependency| !completed.contains(*dependency))
146 .cloned()
147 .collect()
148 }
149
150 fn append_item_notes(item: &mut TaskLoopItem, note: &str) {
151 let note = note.trim();
152 if note.is_empty() {
153 return;
154 }
155 if !item.notes.is_empty() {
156 item.notes.push('\n');
157 }
158 item.notes.push_str(note);
159 }
160
161 fn transition_item(
162 item: &mut TaskLoopItem,
163 status: TaskItemStatus,
164 reason: Option<&str>,
165 round: Option<u32>,
166 ) -> bool {
167 let reason = reason.map(str::trim).filter(|value| !value.is_empty());
168 if item.status == status {
169 if let Some(reason) = reason {
170 Self::append_item_notes(item, reason);
171 }
172 return false;
173 }
174
175 let transition = TaskTransition {
176 from_status: item.status.clone(),
177 to_status: status.clone(),
178 reason: reason.map(ToOwned::to_owned),
179 round,
180 changed_at: Utc::now(),
181 };
182 item.status = status.clone();
183
184 if matches!(status, TaskItemStatus::InProgress) && item.started_at_round.is_none() {
185 item.started_at_round = round;
186 }
187 if matches!(status, TaskItemStatus::Completed) {
188 item.completed_at_round = round;
189 }
190 if let Some(reason) = transition.reason.as_deref() {
191 Self::append_item_notes(item, reason);
192 }
193
194 item.transitions.push(transition);
195 true
196 }
197
198 fn add_item_blocker(item: &mut TaskLoopItem, blocker: TaskBlocker) {
199 if blocker.summary.trim().is_empty() {
200 return;
201 }
202 if item.blockers.iter().any(|existing| {
203 existing.kind == blocker.kind
204 && existing.summary == blocker.summary
205 && existing.waiting_on == blocker.waiting_on
206 }) {
207 return;
208 }
209 item.blockers.push(blocker);
210 }
211
212 fn push_item_evidence(item: &mut TaskLoopItem, evidence: TaskEvidence) {
213 if evidence.summary.trim().is_empty() {
214 return;
215 }
216 item.evidence.push(evidence);
217 }
218}
219
220#[cfg(test)]
221mod tests;