Skip to main content

bamboo_engine/runtime/task_context/
tracking.rs

1use 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    /// Track tool execution
29    ///
30    /// Records a tool call and associates it with the active task item.
31    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    /// Set active task item
63    ///
64    /// Moves the previous active item back to pending and activates a new item.
65    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    /// Update item status manually
151    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    /// Apply an evaluated task update and report whether it changed the context.
182    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}