bamboo_engine/runtime/task_context/
prompt.rs1use bamboo_domain::task::{
2 task_evidence_kind_label, task_phase_label, task_priority_label, TaskPhase, TaskPriority,
3};
4use bamboo_domain::TaskItemStatus;
5
6use super::TaskLoopContext;
7
8impl TaskLoopContext {
9 fn truncate_prompt_value(value: &str, max_chars: usize) -> String {
10 let trimmed = value.trim().replace('\n', " ");
11 let char_count = trimmed.chars().count();
12 if char_count <= max_chars {
13 return trimmed;
14 }
15
16 let truncated: String = trimmed.chars().take(max_chars).collect();
17 format!("{}…", truncated.trim_end())
18 }
19
20 pub fn format_for_prompt(&self) -> String {
22 if self.items.is_empty() {
23 return String::new();
24 }
25
26 let mut output = format!(
27 "\n\n## Current Task List (Round {}/{})\n",
28 self.current_round + 1,
29 self.max_rounds
30 );
31
32 for item in &self.items {
33 let status_icon = match item.status {
34 TaskItemStatus::Pending => "[ ]",
35 TaskItemStatus::InProgress => "[/]",
36 TaskItemStatus::Completed => "[x]",
37 TaskItemStatus::Blocked => "[!]",
38 };
39
40 output.push_str(&format!(
41 "\n{} {}: {}",
42 status_icon, item.id, item.description
43 ));
44
45 if !item.tool_calls.is_empty() {
46 output.push_str(&format!(" ({} tool calls)", item.tool_calls.len()));
47 }
48
49 let mut tags = Vec::new();
50 if item.phase != TaskPhase::Execution {
51 tags.push(format!("phase={}", task_phase_label(&item.phase)));
52 }
53 if item.priority != TaskPriority::Medium {
54 tags.push(format!("priority={}", task_priority_label(&item.priority)));
55 }
56 if let Some(parent_id) = item.parent_id.as_deref().filter(|value| !value.is_empty()) {
57 tags.push(format!("parent={parent_id}"));
58 }
59 if !item.depends_on.is_empty() {
60 tags.push(format!("depends_on={}", item.depends_on.join(", ")));
61 }
62 if !tags.is_empty() {
63 output.push_str(&format!(" [{}]", tags.join(" | ")));
64 }
65
66 if matches!(item.status, TaskItemStatus::InProgress) {
67 if let Some(active_form) = item
68 .active_form
69 .as_deref()
70 .filter(|value| !value.is_empty())
71 {
72 output.push_str(&format!(
73 "\n Active: {}",
74 Self::truncate_prompt_value(active_form, 140)
75 ));
76 }
77 }
78
79 if !item.completion_criteria.is_empty() {
80 let criteria = item
81 .completion_criteria
82 .iter()
83 .enumerate()
84 .map(|(index, criterion)| format!("c{}: {}", index + 1, criterion.trim()))
85 .collect::<Vec<_>>()
86 .join(" | ");
87 output.push_str(&format!("\n Completion Criteria: {criteria}"));
88 }
89
90 if let Some(blocker) = item.blockers.last() {
91 let mut blocker_line = Self::truncate_prompt_value(&blocker.summary, 140);
92 if let Some(waiting_on) = blocker
93 .waiting_on
94 .as_deref()
95 .filter(|value| !value.is_empty())
96 {
97 blocker_line.push_str(&format!(
98 " (waiting_on: {})",
99 Self::truncate_prompt_value(waiting_on, 60)
100 ));
101 }
102 output.push_str(&format!("\n Blocked by: {blocker_line}"));
103 }
104
105 if let Some(evidence) = item.evidence.last() {
106 let mut evidence_line = Self::truncate_prompt_value(&evidence.summary, 140);
107 if let Some(reference) = evidence
108 .reference
109 .as_deref()
110 .filter(|value| !value.is_empty())
111 {
112 evidence_line.push_str(&format!(
113 " [ref: {}]",
114 Self::truncate_prompt_value(reference, 60)
115 ));
116 }
117 output.push_str(&format!(
118 "\n Latest evidence [{}]: {}",
119 task_evidence_kind_label(&evidence.kind),
120 evidence_line
121 ));
122 }
123
124 let notes = item.notes.trim();
125 let active_form = item.active_form.as_deref().map(str::trim);
126 if !notes.is_empty() && Some(notes) != active_form {
127 output.push_str(&format!(
128 "\n Notes: {}",
129 Self::truncate_prompt_value(notes, 160)
130 ));
131 }
132 }
133
134 let completed = self
135 .items
136 .iter()
137 .filter(|item| matches!(item.status, TaskItemStatus::Completed))
138 .count();
139
140 output.push_str(&format!(
141 "\n\nProgress: {}/{} tasks completed",
142 completed,
143 self.items.len()
144 ));
145
146 output
147 }
148}