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    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}