Skip to main content

bamboo_engine/runtime/
task_context.rs

1//! TaskList context for Agent Loop integration
2//!
3//! This module provides TaskLoopContext which integrates TaskList
4//! as a first-class citizen in the Agent Loop, similar to Token Budget.
5
6use bamboo_domain::task::{TaskBlocker, TaskEvidence, TaskPhase, TaskPriority, TaskTransition};
7use bamboo_domain::TaskItemStatus;
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use std::collections::HashSet;
11
12mod auto_status;
13mod conversion;
14mod prompt;
15mod tracking;
16
17/// TaskList context for Agent Loop
18///
19/// Acts as a first-class citizen in the agent loop, tracking
20/// task progress throughout the entire conversation lifecycle.
21#[derive(Debug, Clone)]
22pub struct TaskLoopContext {
23    /// Session ID
24    pub session_id: String,
25
26    /// Task items with execution tracking
27    pub items: Vec<TaskLoopItem>,
28
29    /// Currently active task item ID
30    pub active_item_id: Option<String>,
31
32    /// Current round number
33    pub current_round: u32,
34
35    /// Maximum rounds allowed
36    pub max_rounds: u32,
37
38    /// Creation timestamp
39    pub created_at: DateTime<Utc>,
40
41    /// Last update timestamp
42    pub updated_at: DateTime<Utc>,
43
44    /// Version number for conflict detection
45    pub version: u64,
46}
47
48/// Task item with execution tracking
49#[derive(Debug, Clone, Serialize, Deserialize, Default)]
50pub struct TaskLoopItem {
51    /// Item ID
52    pub id: String,
53
54    /// Item description
55    pub description: String,
56
57    /// Item status
58    pub status: TaskItemStatus,
59
60    /// IDs of other items this item depends on.
61    pub depends_on: Vec<String>,
62
63    /// Additional notes or context.
64    pub notes: String,
65
66    /// Present-progress phrasing for the active task.
67    pub active_form: Option<String>,
68
69    /// Optional parent task ID when this item is part of a larger task tree.
70    pub parent_id: Option<String>,
71
72    /// Phase of work for the task.
73    pub phase: TaskPhase,
74
75    /// Relative priority of the task.
76    pub priority: TaskPriority,
77
78    /// Explicit completion criteria for the task.
79    pub completion_criteria: Vec<String>,
80
81    /// Structured evidence gathered while working on the task.
82    pub evidence: Vec<TaskEvidence>,
83
84    /// Structured blocker information.
85    pub blockers: Vec<TaskBlocker>,
86
87    /// Transition history for this task item.
88    pub transitions: Vec<TaskTransition>,
89
90    /// Tool call history (tracks execution process)
91    pub tool_calls: Vec<ToolCallRecord>,
92
93    /// Round when item was started
94    pub started_at_round: Option<u32>,
95
96    /// Round when item was completed
97    pub completed_at_round: Option<u32>,
98}
99
100/// Record of a tool call execution
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ToolCallRecord {
103    /// Round number
104    pub round: u32,
105
106    /// Tool name
107    pub tool_name: String,
108
109    /// Whether the call succeeded
110    pub success: bool,
111
112    /// Timestamp
113    pub timestamp: DateTime<Utc>,
114}
115
116impl TaskLoopContext {
117    /// Check if all items are completed
118    pub fn is_all_completed(&self) -> bool {
119        !self.items.is_empty()
120            && self
121                .items
122                .iter()
123                .all(|item| matches!(item.status, TaskItemStatus::Completed))
124    }
125
126    fn completed_item_ids(&self) -> HashSet<String> {
127        self.items
128            .iter()
129            .filter(|item| matches!(item.status, TaskItemStatus::Completed))
130            .map(|item| item.id.clone())
131            .collect()
132    }
133
134    fn unresolved_dependencies(&self, depends_on: &[String]) -> Vec<String> {
135        let completed = self.completed_item_ids();
136        depends_on
137            .iter()
138            .filter(|dependency| !completed.contains(*dependency))
139            .cloned()
140            .collect()
141    }
142
143    fn append_item_notes(item: &mut TaskLoopItem, note: &str) {
144        let note = note.trim();
145        if note.is_empty() {
146            return;
147        }
148        if !item.notes.is_empty() {
149            item.notes.push('\n');
150        }
151        item.notes.push_str(note);
152    }
153
154    fn transition_item(
155        item: &mut TaskLoopItem,
156        status: TaskItemStatus,
157        reason: Option<&str>,
158        round: Option<u32>,
159    ) -> bool {
160        let reason = reason.map(str::trim).filter(|value| !value.is_empty());
161        if item.status == status {
162            if let Some(reason) = reason {
163                Self::append_item_notes(item, reason);
164            }
165            return false;
166        }
167
168        let transition = TaskTransition {
169            from_status: item.status.clone(),
170            to_status: status.clone(),
171            reason: reason.map(ToOwned::to_owned),
172            round,
173            changed_at: Utc::now(),
174        };
175        item.status = status.clone();
176
177        if matches!(status, TaskItemStatus::InProgress) && item.started_at_round.is_none() {
178            item.started_at_round = round;
179        }
180        if matches!(status, TaskItemStatus::Completed) {
181            item.completed_at_round = round;
182        }
183        if let Some(reason) = transition.reason.as_deref() {
184            Self::append_item_notes(item, reason);
185        }
186
187        item.transitions.push(transition);
188        true
189    }
190
191    fn add_item_blocker(item: &mut TaskLoopItem, blocker: TaskBlocker) {
192        if blocker.summary.trim().is_empty() {
193            return;
194        }
195        if item.blockers.iter().any(|existing| {
196            existing.kind == blocker.kind
197                && existing.summary == blocker.summary
198                && existing.waiting_on == blocker.waiting_on
199        }) {
200            return;
201        }
202        item.blockers.push(blocker);
203    }
204
205    fn push_item_evidence(item: &mut TaskLoopItem, evidence: TaskEvidence) {
206        if evidence.summary.trim().is_empty() {
207            return;
208        }
209        item.evidence.push(evidence);
210    }
211}
212
213#[cfg(test)]
214mod tests;