bamboo_engine/runtime/task_context/
auto_status.rs1use bamboo_agent_core::tools::ToolResult;
2use bamboo_domain::task::{TaskBlocker, TaskBlockerKind};
3use bamboo_domain::TaskItemStatus;
4use chrono::Utc;
5
6use super::{TaskLoopContext, TaskLoopItem};
7
8impl TaskLoopContext {
9 pub fn auto_match_tool_to_item(&mut self, tool_name: &str) {
11 if self.active_item_id.is_some() {
12 return;
13 }
14
15 let tool_lower = tool_name.to_lowercase();
16 let matching_item_id = self
17 .items
18 .iter()
19 .find(|item| {
20 if matches!(item.status, TaskItemStatus::Completed) {
21 return false;
22 }
23 if !self.unresolved_dependencies(&item.depends_on).is_empty() {
24 return false;
25 }
26 let desc_lower = item.description.to_lowercase();
27 desc_lower.contains(&tool_lower)
28 || (tool_lower.contains("file") && desc_lower.contains("file"))
29 || (tool_lower.contains("command")
30 && (desc_lower.contains("run") || desc_lower.contains("execute")))
31 })
32 .map(|item| item.id.clone());
33
34 if let Some(item_id) = matching_item_id {
35 self.set_active_item(&item_id);
36 }
37 }
38
39 pub fn auto_update_status(&mut self, tool_name: &str, result: &ToolResult) {
41 if self.active_item_id.is_none() {
42 self.auto_match_tool_to_item(tool_name);
43 }
44
45 if let Some(ref active_id) = self.active_item_id.clone() {
46 let action = self
47 .items
48 .iter()
49 .find(|item| &item.id == active_id)
50 .and_then(|item| {
51 if result.success {
52 if self.should_mark_completed(item) {
53 Some(TaskItemStatus::Completed)
54 } else {
55 None
56 }
57 } else if self.should_mark_blocked(item) {
58 Some(TaskItemStatus::Blocked)
59 } else {
60 None
61 }
62 });
63
64 if let Some(new_status) = action {
65 if let Some(item) = self.items.iter_mut().find(|item| &item.id == active_id) {
66 if matches!(new_status, TaskItemStatus::Blocked) {
67 let mut summary = format!("Tool `{}` failed repeatedly", tool_name);
68 let detail = result.result.trim();
69 if !detail.is_empty() {
70 let clipped: String = detail.chars().take(120).collect();
71 if detail.chars().count() > 120 {
72 summary.push_str(&format!(": {}…", clipped.trim_end()));
73 } else {
74 summary.push_str(&format!(": {clipped}"));
75 }
76 }
77 TaskLoopContext::add_item_blocker(
78 item,
79 TaskBlocker {
80 kind: TaskBlockerKind::ToolFailure,
81 summary,
82 waiting_on: None,
83 },
84 );
85 }
86
87 let reason = if result.success {
88 Some("Auto-updated after sufficient successful tool calls")
89 } else {
90 Some("Auto-updated after repeated tool failures")
91 };
92 let changed = TaskLoopContext::transition_item(
93 item,
94 new_status.clone(),
95 reason,
96 Some(self.current_round),
97 );
98
99 if changed && matches!(new_status, TaskItemStatus::Completed) {
100 item.completed_at_round = Some(self.current_round);
101 self.active_item_id = None;
102 }
103 if changed {
104 self.version += 1;
105 self.updated_at = Utc::now();
106 }
107 }
108 }
109 }
110 }
111
112 fn should_mark_completed(&self, item: &TaskLoopItem) -> bool {
114 if !item.completion_criteria.is_empty() {
115 return false;
116 }
117 let success_count = item
118 .tool_calls
119 .iter()
120 .filter(|record| record.success)
121 .count();
122 success_count >= 3
123 }
124
125 fn should_mark_blocked(&self, item: &TaskLoopItem) -> bool {
127 let recent_failures = item
128 .tool_calls
129 .iter()
130 .rev()
131 .take(2)
132 .filter(|record| !record.success)
133 .count();
134 recent_failures >= 2
135 }
136}