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 append_structured_feedback(
182 &mut self,
183 item_id: &str,
184 evidence: Option<&str>,
185 blocker: Option<&str>,
186 ) {
187 let mut changed = false;
188 if let Some(item) = self.items.iter_mut().find(|item| item.id == item_id) {
189 if let Some(summary) = evidence
190 .map(str::trim)
191 .filter(|summary| !summary.is_empty())
192 {
193 Self::push_item_evidence(
194 item,
195 TaskEvidence {
196 kind: TaskEvidenceKind::Observation,
197 summary: summary.to_string(),
198 reference: None,
199 tool_name: None,
200 tool_call_id: None,
201 round: Some(self.current_round),
202 success: None,
203 },
204 );
205 changed = true;
206 }
207 if let Some(summary) = blocker.map(str::trim).filter(|summary| !summary.is_empty()) {
208 Self::add_item_blocker(
209 item,
210 TaskBlocker {
211 kind: TaskBlockerKind::Unknown,
212 summary: summary.to_string(),
213 waiting_on: None,
214 },
215 );
216 changed = true;
217 }
218 }
219 if changed {
220 self.updated_at = Utc::now();
221 self.version += 1;
222 }
223 }
224}