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