Skip to main content

bamboo_engine/runtime/task_context/
prompt.rs

1use 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    /// Generate context for prompt injection
21    pub fn format_for_prompt(&self) -> String {
22        if self.items.is_empty() {
23            return String::new();
24        }
25
26        // With no configured round cap the run is unlimited: show the current
27        // round alone rather than a misleading denominator.
28        let header = match self.max_rounds {
29            Some(max_rounds) => {
30                format!(" (Round {}/{})", self.current_round + 1, max_rounds)
31            }
32            None => format!(" (Round {})", self.current_round + 1),
33        };
34        let mut output = format!("\n\n## Current Task List{header}\n");
35
36        for item in &self.items {
37            let status_icon = match item.status {
38                TaskItemStatus::Pending => "[ ]",
39                TaskItemStatus::InProgress => "[/]",
40                TaskItemStatus::Completed => "[x]",
41                TaskItemStatus::Blocked => "[!]",
42            };
43
44            output.push_str(&format!(
45                "\n{} {}: {}",
46                status_icon, item.id, item.description
47            ));
48
49            if !item.tool_calls.is_empty() {
50                output.push_str(&format!(" ({} tool calls)", item.tool_calls.len()));
51            }
52
53            let mut tags = Vec::new();
54            if item.phase != TaskPhase::Execution {
55                tags.push(format!("phase={}", task_phase_label(&item.phase)));
56            }
57            if item.priority != TaskPriority::Medium {
58                tags.push(format!("priority={}", task_priority_label(&item.priority)));
59            }
60            if let Some(parent_id) = item.parent_id.as_deref().filter(|value| !value.is_empty()) {
61                tags.push(format!("parent={parent_id}"));
62            }
63            if !item.depends_on.is_empty() {
64                tags.push(format!("depends_on={}", item.depends_on.join(", ")));
65            }
66            if !tags.is_empty() {
67                output.push_str(&format!(" [{}]", tags.join(" | ")));
68            }
69
70            if matches!(item.status, TaskItemStatus::InProgress) {
71                if let Some(active_form) = item
72                    .active_form
73                    .as_deref()
74                    .filter(|value| !value.is_empty())
75                {
76                    output.push_str(&format!(
77                        "\n    Active: {}",
78                        Self::truncate_prompt_value(active_form, 140)
79                    ));
80                }
81            }
82
83            if !item.completion_criteria.is_empty() {
84                let criteria = item
85                    .completion_criteria
86                    .iter()
87                    .enumerate()
88                    .map(|(index, criterion)| format!("c{}: {}", index + 1, criterion.trim()))
89                    .collect::<Vec<_>>()
90                    .join(" | ");
91                output.push_str(&format!("\n    Completion Criteria: {criteria}"));
92            }
93
94            if let Some(blocker) = item.blockers.last() {
95                let mut blocker_line = Self::truncate_prompt_value(&blocker.summary, 140);
96                if let Some(waiting_on) = blocker
97                    .waiting_on
98                    .as_deref()
99                    .filter(|value| !value.is_empty())
100                {
101                    blocker_line.push_str(&format!(
102                        " (waiting_on: {})",
103                        Self::truncate_prompt_value(waiting_on, 60)
104                    ));
105                }
106                output.push_str(&format!("\n    Blocked by: {blocker_line}"));
107            }
108
109            if let Some(evidence) = item.evidence.last() {
110                let mut evidence_line = Self::truncate_prompt_value(&evidence.summary, 140);
111                if let Some(reference) = evidence
112                    .reference
113                    .as_deref()
114                    .filter(|value| !value.is_empty())
115                {
116                    evidence_line.push_str(&format!(
117                        " [ref: {}]",
118                        Self::truncate_prompt_value(reference, 60)
119                    ));
120                }
121                output.push_str(&format!(
122                    "\n    Latest evidence [{}]: {}",
123                    task_evidence_kind_label(&evidence.kind),
124                    evidence_line
125                ));
126            }
127
128            let notes = item.notes.trim();
129            let active_form = item.active_form.as_deref().map(str::trim);
130            if !notes.is_empty() && Some(notes) != active_form {
131                output.push_str(&format!(
132                    "\n    Notes: {}",
133                    Self::truncate_prompt_value(notes, 160)
134                ));
135            }
136        }
137
138        let completed = self
139            .items
140            .iter()
141            .filter(|item| matches!(item.status, TaskItemStatus::Completed))
142            .count();
143
144        output.push_str(&format!(
145            "\n\nProgress: {}/{} tasks completed",
146            completed,
147            self.items.len()
148        ));
149
150        output
151    }
152}