bamboo_engine/runtime/task_context/
tracking.rs1use bamboo_agent_core::tools::ToolResult;
2use bamboo_domain::task::{TaskBlocker, TaskBlockerKind, TaskEvidence, TaskEvidenceKind};
3use bamboo_domain::TaskItemStatus;
4use chrono::Utc;
5
6use super::{TaskLoopContext, ToolCallRecord};
7
8fn summarize_tool_result(tool_name: &str, result: &ToolResult, max_chars: usize) -> String {
9 let status_text = if result.success {
10 "succeeded"
11 } else {
12 "failed"
13 };
14 let mut summary = format!("Tool `{tool_name}` {status_text}");
15 let detail = result.result.trim();
16 if !detail.is_empty() {
17 let clipped: String = detail.chars().take(max_chars).collect();
18 if detail.chars().count() > max_chars {
19 summary.push_str(&format!(": {}…", clipped.trim_end()));
20 } else {
21 summary.push_str(&format!(": {clipped}"));
22 }
23 }
24 summary
25}
26
27impl TaskLoopContext {
28 pub fn track_tool_execution(&mut self, tool_name: &str, result: &ToolResult, round: u32) {
32 self.current_round = round;
33
34 let record = ToolCallRecord {
35 round,
36 tool_name: tool_name.to_string(),
37 success: result.success,
38 timestamp: Utc::now(),
39 };
40
41 if let Some(ref active_id) = self.active_item_id {
42 if let Some(item) = self.items.iter_mut().find(|item| &item.id == active_id) {
43 item.tool_calls.push(record);
44 Self::push_item_evidence(
45 item,
46 TaskEvidence {
47 kind: TaskEvidenceKind::ToolCall,
48 summary: summarize_tool_result(tool_name, result, 160),
49 reference: None,
50 tool_name: Some(tool_name.to_string()),
51 tool_call_id: None,
52 round: Some(round),
53 success: Some(result.success),
54 },
55 );
56 self.updated_at = Utc::now();
57 self.version += 1;
58 }
59 }
60 }
61
62 pub fn set_active_item(&mut self, item_id: &str) {
66 if self.active_item_id.as_deref() == Some(item_id) {
67 return;
68 }
69
70 let Some(target_index) = self.items.iter().position(|item| item.id == item_id) else {
71 return;
72 };
73
74 let unmet_dependencies = {
75 let target = &self.items[target_index];
76 self.unresolved_dependencies(&target.depends_on)
77 };
78
79 if !unmet_dependencies.is_empty() {
80 let mut changed = false;
81 if let Some(item) = self.items.get_mut(target_index) {
82 let summary = if unmet_dependencies.len() == 1 {
83 format!(
84 "Waiting for dependency `{}` to complete",
85 unmet_dependencies[0]
86 )
87 } else {
88 format!(
89 "Waiting for dependencies to complete: {}",
90 unmet_dependencies.join(", ")
91 )
92 };
93 let waiting_on = if unmet_dependencies.len() == 1 {
94 Some(unmet_dependencies[0].clone())
95 } else {
96 None
97 };
98 let blocker_count = item.blockers.len();
99 Self::add_item_blocker(
100 item,
101 TaskBlocker {
102 kind: TaskBlockerKind::Dependency,
103 summary,
104 waiting_on,
105 },
106 );
107 if item.blockers.len() > blocker_count {
108 changed = true;
109 }
110 changed |= Self::transition_item(
111 item,
112 TaskItemStatus::Blocked,
113 Some("Cannot start task before dependencies are completed"),
114 Some(self.current_round),
115 );
116 }
117 if changed {
118 self.updated_at = Utc::now();
119 self.version += 1;
120 }
121 return;
122 }
123
124 if let Some(ref previous_id) = self.active_item_id.clone() {
125 if let Some(item) = self.items.iter_mut().find(|item| &item.id == previous_id) {
126 Self::transition_item(
127 item,
128 TaskItemStatus::Pending,
129 Some("Task focus switched to another item"),
130 Some(self.current_round),
131 );
132 }
133 }
134
135 self.active_item_id = Some(item_id.to_string());
136 if let Some(item) = self.items.get_mut(target_index) {
137 Self::transition_item(
138 item,
139 TaskItemStatus::InProgress,
140 Some("Task marked as active"),
141 Some(self.current_round),
142 );
143 item.started_at_round = Some(self.current_round);
144 }
145
146 self.updated_at = Utc::now();
147 self.version += 1;
148 }
149
150 pub fn update_item_status(&mut self, item_id: &str, status: TaskItemStatus) {
152 if let Some(item) = self.items.iter_mut().find(|item| item.id == item_id) {
153 let changed =
154 Self::transition_item(item, status.clone(), None, Some(self.current_round));
155
156 match status {
157 TaskItemStatus::InProgress => {
158 item.started_at_round = Some(self.current_round);
159 self.active_item_id = Some(item_id.to_string());
160 }
161 TaskItemStatus::Completed => {
162 item.completed_at_round = Some(self.current_round);
163 if self.active_item_id.as_deref() == Some(item_id) {
164 self.active_item_id = None;
165 }
166 }
167 TaskItemStatus::Pending | TaskItemStatus::Blocked => {
168 if self.active_item_id.as_deref() == Some(item_id) {
169 self.active_item_id = None;
170 }
171 }
172 }
173
174 if changed {
175 self.updated_at = Utc::now();
176 self.version += 1;
177 }
178 }
179 }
180
181 pub fn apply_evaluated_update(
183 &mut self,
184 item_id: &str,
185 status: TaskItemStatus,
186 notes: Option<&str>,
187 evidence: Option<&str>,
188 blocker: Option<&str>,
189 ) -> bool {
190 let previous_version = self.version;
191 self.update_item_status(item_id, status);
192 if let Some(note) = notes.map(str::trim).filter(|note| !note.is_empty()) {
193 if let Some(item) = self.items.iter_mut().find(|item| item.id == item_id) {
194 let previous_notes = item.notes.clone();
195 Self::append_item_notes(item, note);
196 if item.notes != previous_notes {
197 self.updated_at = Utc::now();
198 self.version += 1;
199 }
200 }
201 }
202 self.append_structured_feedback(item_id, evidence, blocker);
203 self.version != previous_version
204 }
205
206 pub fn append_structured_feedback(
207 &mut self,
208 item_id: &str,
209 evidence: Option<&str>,
210 blocker: Option<&str>,
211 ) {
212 let mut changed = false;
213 if let Some(item) = self.items.iter_mut().find(|item| item.id == item_id) {
214 if let Some(summary) = evidence
215 .map(str::trim)
216 .filter(|summary| !summary.is_empty())
217 {
218 Self::push_item_evidence(
219 item,
220 TaskEvidence {
221 kind: TaskEvidenceKind::Observation,
222 summary: summary.to_string(),
223 reference: None,
224 tool_name: None,
225 tool_call_id: None,
226 round: Some(self.current_round),
227 success: None,
228 },
229 );
230 changed = true;
231 }
232 if let Some(summary) = blocker.map(str::trim).filter(|summary| !summary.is_empty()) {
233 Self::add_item_blocker(
234 item,
235 TaskBlocker {
236 kind: TaskBlockerKind::Unknown,
237 summary: summary.to_string(),
238 waiting_on: None,
239 },
240 );
241 changed = true;
242 }
243 }
244 if changed {
245 self.updated_at = Utc::now();
246 self.version += 1;
247 }
248 }
249}