bamboo_agent_core/agent/events.rs
1//! Agent event system for real-time streaming.
2//!
3//! This module defines the event types emitted during agent execution,
4//! which are streamed to clients via Server-Sent Events (SSE).
5//!
6//! # Event Types
7//!
8//! - [`AgentEvent`] - All possible agent execution events
9//! - [`TokenUsage`] - Token consumption statistics
10//! - [`TokenBudgetUsage`] - Detailed token budget information
11//!
12//! # Event Flow
13//!
14//! 1. **Token** events stream generated text
15//! 2. **ToolStart/ToolComplete** track tool execution
16//! 3. **TaskListUpdated** tracks progress
17//! 4. **TokenBudgetUpdated** reports context management
18//! 5. **Complete** or **Error** ends the stream
19//!
20//! # Example
21//!
22//! ```javascript
23//! const eventSource = new EventSource('/api/v1/events/session-id');
24//! eventSource.onmessage = (event) => {
25//! const data = JSON.parse(event.data);
26//! switch (data.type) {
27//! case 'token':
28//! console.log('Token:', data.content);
29//! break;
30//! case 'complete':
31//! console.log('Done!');
32//! eventSource.close();
33//! break;
34//! }
35//! };
36//! ```
37
38use crate::tools::ToolResult;
39use bamboo_domain::{TaskItemStatus, TaskList};
40use chrono::{DateTime, Utc};
41use serde::{Deserialize, Serialize};
42
43/// Represents events emitted during agent execution.
44///
45/// These events are streamed to clients via SSE to provide real-time
46/// feedback on agent progress, tool execution, and completion.
47///
48/// # Variants
49///
50/// ## Text Generation
51/// - `Token` - Streaming text token
52/// - `ReasoningToken` - Streaming reasoning/thinking token (separate channel)
53///
54/// ## Tool Execution
55/// - `ToolStart` - Tool execution started
56/// - `ToolComplete` - Tool finished successfully
57/// - `ToolError` - Tool execution failed
58///
59/// ## User Interaction
60/// - `NeedClarification` - Agent needs user input
61///
62/// ## Progress Tracking
63/// - `TaskListUpdated` - Task list created or modified
64/// - `TaskListItemProgress` - Individual item progress
65/// - `TaskListCompleted` - All items completed
66/// - `TaskEvaluationStarted` - Task evaluation began
67/// - `TaskEvaluationCompleted` - Task evaluation finished
68///
69/// ## Context Management
70/// - `TokenBudgetUpdated` - Context budget changed
71/// - `ContextCompressionStatus` - Context compression lifecycle progress
72/// - `ContextSummarized` - Old messages summarized
73///
74/// ## Sub-sessions (Async Spawn)
75/// - `SubSessionStarted` - A child session is created and scheduled to run
76/// - `SubSessionEvent` - Forwarded raw child event (full fidelity)
77/// - `SubSessionHeartbeat` - Periodic heartbeat while the child is running
78/// - `SubSessionCompleted` - Child session finished (completed/cancelled/error)
79///
80/// ## Terminal Events
81/// - `Complete` - Execution finished successfully
82/// - `Error` - Execution failed
83///
84/// # Serialization
85///
86/// Events are serialized as JSON with a `type` field for discrimination:
87/// ```json
88/// {"type": "token", "content": "Hello"}
89/// {"type": "complete", "usage": {"prompt_tokens": 10, "completion_tokens": 5, "total_tokens": 15}}
90/// ```
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(tag = "type", rename_all = "snake_case")]
93pub enum AgentEvent {
94 /// Text token generated by the LLM.
95 Token {
96 /// Generated text content
97 content: String,
98 },
99
100 /// Reasoning/thinking token generated by the LLM.
101 ///
102 /// This is streamed separately from assistant answer tokens so the UI can
103 /// choose whether and how to display model reasoning traces.
104 ReasoningToken {
105 /// Generated reasoning content
106 content: String,
107 },
108
109 /// Streaming output emitted while a specific tool call is running.
110 ///
111 /// This is used to render "live output" inside a tool-call card in the UI
112 /// without mixing tool output into the assistant's main token stream.
113 ToolToken {
114 /// Tool call identifier that this output belongs to.
115 tool_call_id: String,
116 /// Output chunk.
117 content: String,
118 },
119
120 /// Tool execution started.
121 ToolStart {
122 /// Unique tool call identifier
123 tool_call_id: String,
124 /// Name of the tool being executed
125 tool_name: String,
126 /// Tool arguments (JSON)
127 arguments: serde_json::Value,
128 },
129
130 /// Tool execution completed successfully.
131 ToolComplete {
132 /// Tool call identifier
133 tool_call_id: String,
134 /// Tool execution result
135 result: ToolResult,
136 },
137
138 /// Tool execution failed.
139 ToolError {
140 /// Tool call identifier
141 tool_call_id: String,
142 /// Error message
143 error: String,
144 },
145
146 /// Structured lifecycle event for tool execution tracking.
147 ///
148 /// These events complement `ToolStart`/`ToolComplete`/`ToolError` with
149 /// richer metadata (mutability, auto-approval, wall-clock timing) and
150 /// are emitted by `ToolEmitter` (in `bamboo-agent-tools`).
151 ToolLifecycle {
152 /// Tool call identifier
153 tool_call_id: String,
154 /// Canonical tool name
155 tool_name: String,
156 /// Lifecycle phase: "begin", "finished", "error", "cancelled"
157 phase: String,
158 /// Wall-clock milliseconds since the call began (None for begin)
159 #[serde(skip_serializing_if = "Option::is_none")]
160 elapsed_ms: Option<u64>,
161 /// Whether the tool mutates state (writes files, runs commands)
162 is_mutating: bool,
163 /// Whether execution was auto-approved (no user prompt needed)
164 auto_approved: bool,
165 /// Human-readable summary
166 #[serde(skip_serializing_if = "Option::is_none")]
167 summary: Option<String>,
168 /// Error message (if phase == "error")
169 #[serde(skip_serializing_if = "Option::is_none")]
170 error: Option<String>,
171 },
172
173 /// Agent needs clarification from the user.
174 NeedClarification {
175 /// Question to ask the user
176 question: String,
177 /// Optional predefined options
178 options: Option<Vec<String>>,
179 },
180
181 /// Emitted when task list is created or updated.
182 TaskListUpdated {
183 /// Current task list state.
184 task_list: TaskList,
185 },
186
187 /// Emitted when a task item makes progress (delta update).
188 TaskListItemProgress {
189 /// Session identifier
190 session_id: String,
191 /// Item identifier
192 item_id: String,
193 /// New item status
194 status: TaskItemStatus,
195 /// Number of tool calls made
196 tool_calls_count: usize,
197 /// Item version (for optimistic concurrency)
198 version: u64,
199 },
200
201 /// Emitted when all task items are completed.
202 TaskListCompleted {
203 /// Session identifier
204 session_id: String,
205 /// Completion timestamp
206 completed_at: DateTime<Utc>,
207 /// Total agent rounds executed
208 total_rounds: u32,
209 /// Total tool calls made
210 total_tool_calls: usize,
211 },
212
213 /// Emitted when task evaluation starts.
214 TaskEvaluationStarted {
215 /// Session identifier
216 session_id: String,
217 /// Number of items to evaluate
218 items_count: usize,
219 },
220
221 /// Emitted when task evaluation completes.
222 TaskEvaluationCompleted {
223 /// Session identifier
224 session_id: String,
225 /// Number of items updated
226 updates_count: usize,
227 /// Evaluation reasoning
228 reasoning: String,
229 },
230
231 /// Emitted when token budget is prepared (after context truncation)
232 TokenBudgetUpdated {
233 /// Token budget details
234 usage: TokenBudgetUsage,
235 },
236
237 /// Emitted when host-side context compression lifecycle changes.
238 ContextCompressionStatus {
239 /// Compression phase label (for example: pre-turn, mid-turn).
240 phase: String,
241 /// Compression status: started | completed | failed | skipped
242 status: String,
243 },
244
245 /// Emitted when conversation context is summarized
246 ContextSummarized {
247 /// Generated summary text
248 summary: String,
249 /// Number of old messages summarized
250 messages_summarized: usize,
251 /// Tokens saved by summarization
252 tokens_saved: u32,
253 /// Context usage percentage before compression
254 #[serde(default)]
255 usage_before_percent: f64,
256 /// Context usage percentage after compression
257 #[serde(default)]
258 usage_after_percent: f64,
259 /// What triggered the compression: "auto" | "manual" | "critical"
260 #[serde(default)]
261 trigger_type: String,
262 },
263
264 /// Emitted when context pressure reaches warning or critical levels.
265 /// Frontend should display this to the user as a proactive notification.
266 ContextPressureNotification {
267 /// Context usage as a percentage of the context window.
268 percent: f64,
269 /// Severity level: "warning" (70%) or "critical" (90%).
270 level: String,
271 /// Human-readable message describing the pressure state.
272 message: String,
273 },
274
275 /// A child session was spawned from a parent session (async background job).
276 SubSessionStarted {
277 parent_session_id: String,
278 child_session_id: String,
279 /// Optional title (useful for UI lists).
280 #[serde(default, skip_serializing_if = "Option::is_none")]
281 title: Option<String>,
282 },
283
284 /// Forwarded raw child event to the parent session stream.
285 ///
286 /// Child sessions are not allowed to spawn further sessions, so this should not nest.
287 SubSessionEvent {
288 parent_session_id: String,
289 child_session_id: String,
290 event: Box<AgentEvent>,
291 },
292
293 /// Heartbeat emitted while a child session is running.
294 SubSessionHeartbeat {
295 parent_session_id: String,
296 child_session_id: String,
297 timestamp: DateTime<Utc>,
298 },
299
300 /// Child session finished (completed/cancelled/error).
301 SubSessionCompleted {
302 parent_session_id: String,
303 child_session_id: String,
304 /// One of: "completed" | "cancelled" | "error" | "skipped"
305 status: String,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
307 error: Option<String>,
308 },
309
310 /// Agent execution completed successfully.
311 Complete {
312 /// Final token usage statistics
313 usage: TokenUsage,
314 },
315
316 /// Agent execution failed.
317 Error {
318 /// Error message
319 message: String,
320 },
321}
322
323/// Re-exported shared token usage type.
324///
325/// See [`bamboo_domain::TokenUsage`] for the canonical definition.
326pub use bamboo_domain::TokenUsage;
327
328pub use bamboo_domain::budget_types::TokenBudgetUsage;
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use bamboo_domain::{TaskItem, TaskItemStatus, TaskList};
334
335 fn sample_task_list() -> TaskList {
336 TaskList {
337 session_id: "session-1".to_string(),
338 title: "Task List".to_string(),
339 items: vec![TaskItem {
340 id: "task_1".to_string(),
341 description: "Implement event rename".to_string(),
342 status: TaskItemStatus::InProgress,
343 depends_on: Vec::new(),
344 notes: "Implementing".to_string(),
345 ..TaskItem::default()
346 }],
347 created_at: Utc::now(),
348 updated_at: Utc::now(),
349 }
350 }
351
352 #[test]
353 fn task_list_updated_serializes_with_task_names() {
354 let event = AgentEvent::TaskListUpdated {
355 task_list: sample_task_list(),
356 };
357
358 let value = serde_json::to_value(event).expect("event should serialize");
359 assert_eq!(value["type"], "task_list_updated");
360 assert!(value.get("task_list").is_some());
361 assert!(value.get("todo_list").is_none());
362 }
363
364 #[test]
365 fn task_evaluation_completed_serializes_with_task_type() {
366 let event = AgentEvent::TaskEvaluationCompleted {
367 session_id: "session-1".to_string(),
368 updates_count: 2,
369 reasoning: "Updated statuses".to_string(),
370 };
371
372 let value = serde_json::to_value(event).expect("event should serialize");
373 assert_eq!(value["type"], "task_evaluation_completed");
374 }
375
376 #[test]
377 fn context_compression_status_serializes_with_phase_and_status() {
378 let event = AgentEvent::ContextCompressionStatus {
379 phase: "mid-turn".to_string(),
380 status: "started".to_string(),
381 };
382
383 let value = serde_json::to_value(event).expect("event should serialize");
384 assert_eq!(value["type"], "context_compression_status");
385 assert_eq!(value["phase"], "mid-turn");
386 assert_eq!(value["status"], "started");
387 }
388}