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