1use crate::TokenUsage;
4use serde::{Deserialize, Serialize};
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "type", rename_all = "snake_case")]
10#[non_exhaustive]
11pub enum ThreadEvent {
12 ThreadStarted { thread_id: String },
14
15 TurnStarted { turn_number: usize },
17
18 TurnCompleted {
20 turn_number: usize,
21 usage: TokenUsage,
22 },
23
24 ItemStarted { item: Item },
26
27 ItemCompleted { item: Item },
29
30 ContentDelta { delta: String },
32
33 ThinkingDelta { thinking: String },
35
36 WaitingForInput { prompt: String },
38
39 Error { message: String, recoverable: bool },
41
42 ThreadCompleted { usage: TokenUsage },
44
45 ThreadCancelled,
47
48 GoalVerificationStarted { goals: Vec<String>, method: String },
51 GoalVerificationResult {
53 goal: String,
54 score: f64,
55 target: f64,
56 passed: bool,
57 duration_ms: u64,
58 },
59 GoalVerificationCompleted {
61 all_passed: bool,
62 passed_count: usize,
63 total_count: usize,
64 },
65
66 RalphIterationStarted {
69 iteration: u32,
70 max_iterations: u32,
71 prompt: String,
72 },
73 RalphContinuation {
75 reason: String,
76 confidence: u32,
77 details: String,
78 },
79 RalphCircuitBreak { reason: String, iteration: u32 },
81
82 BackgroundTaskSpawned {
85 task_id: String,
86 description: String,
87 agent: String,
88 },
89 BackgroundTaskProgress {
91 task_id: String,
92 status: String,
93 message: Option<String>,
94 },
95 BackgroundTaskCompleted {
97 task_id: String,
98 success: bool,
99 result_preview: Option<String>,
100 duration_secs: f64,
101 },
102
103 SubagentStarted {
105 task_id: String,
106 agent_name: String,
107 model: String,
108 session_id: String,
109 },
110 SubagentCompleted {
112 task_id: String,
113 session_id: String,
114 success: bool,
115 duration_secs: f64,
116 },
117
118 ModelSwitched { model: String, provider: String },
120
121 PermissionEvaluated {
124 permission: String,
125 path: String,
126 action: String,
127 rule_matched: Option<String>,
128 },
129 ApprovalCached {
131 tool_name: String,
132 pattern: String,
133 decision: String,
134 },
135 CompactionStarted {
137 strategy: String,
138 token_count_before: usize,
139 },
140 CompactionCompleted {
142 token_count_before: usize,
143 token_count_after: usize,
144 messages_removed: usize,
145 },
146
147 TodoUpdated { todos: Vec<crate::TodoItem> },
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
153#[serde(tag = "type", rename_all = "snake_case")]
154pub enum Item {
155 Thinking {
157 #[serde(skip_serializing_if = "Option::is_none")]
158 content: Option<String>,
159 },
160
161 AgentMessage {
163 content: String,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 name: Option<String>,
166 },
167
168 ToolCall {
170 id: String,
171 name: String,
172 input: serde_json::Value,
173 },
174
175 ToolResult {
177 tool_call_id: String,
178 output: String,
179 is_error: bool,
180 },
181
182 CommandExecution {
184 command: String,
185 exit_code: i32,
186 stdout: String,
187 stderr: String,
188 },
189
190 FileChange {
192 path: PathBuf,
193 change_type: FileChangeType,
194 #[serde(skip_serializing_if = "Option::is_none")]
195 patch: Option<String>,
196 },
197
198 ApprovalRequest {
200 id: String,
201 tool_name: String,
202 input: serde_json::Value,
203 reason: String,
204 },
205
206 ApprovalDecision { request_id: String, approved: bool },
208
209 McpToolCall {
211 server: String,
212 tool: String,
213 result: serde_json::Value,
214 },
215}
216
217#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
219#[serde(rename_all = "snake_case")]
220pub enum FileChangeType {
221 Create,
222 Modify,
223 Delete,
224 Rename,
225}
226
227#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
229#[serde(rename_all = "snake_case")]
230pub enum AgentState {
231 Idle,
232 WaitingForUser,
233 Thinking,
234 ExecutingTool,
235 WaitingForApproval,
236 Complete,
237 Failed,
238 Cancelled,
239}
240
241impl std::fmt::Display for AgentState {
242 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
243 match self {
244 Self::Idle => write!(f, "idle"),
245 Self::WaitingForUser => write!(f, "waiting for user"),
246 Self::Thinking => write!(f, "thinking"),
247 Self::ExecutingTool => write!(f, "executing tool"),
248 Self::WaitingForApproval => write!(f, "waiting for approval"),
249 Self::Complete => write!(f, "complete"),
250 Self::Failed => write!(f, "failed"),
251 Self::Cancelled => write!(f, "cancelled"),
252 }
253 }
254}
255
256#[derive(Debug, Clone, thiserror::Error, Serialize, Deserialize)]
258#[serde(tag = "type", rename_all = "snake_case")]
259pub enum AgentError {
260 #[error("tool error: {tool} - {message}")]
261 ToolError { tool: String, message: String },
262
263 #[error("cancelled by user")]
264 Cancelled,
265
266 #[error("max turns exceeded: {turns}")]
267 MaxTurnsExceeded { turns: usize },
268}
269
270impl AgentError {
271 pub fn is_recoverable(&self) -> bool {
272 matches!(self, Self::ToolError { .. })
273 }
274}
275
276#[derive(Debug, Clone, Serialize, Deserialize)]
278pub struct Progress {
279 pub state: AgentState,
280 pub turn: usize,
281 pub message: Option<String>,
282 pub tool_name: Option<String>,
283 pub usage: TokenUsage,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize)]
288pub struct ExecutionResult {
289 pub success: bool,
290 pub output: String,
291 pub turns: usize,
292 pub usage: TokenUsage,
293 #[serde(skip_serializing_if = "Option::is_none")]
294 pub error: Option<AgentError>,
295}
296
297impl ExecutionResult {
298 pub fn success(output: impl Into<String>, turns: usize, usage: TokenUsage) -> Self {
299 Self {
300 success: true,
301 output: output.into(),
302 turns,
303 usage,
304 error: None,
305 }
306 }
307
308 pub fn failure(error: AgentError, turns: usize, usage: TokenUsage) -> Self {
309 Self {
310 success: false,
311 output: String::new(),
312 turns,
313 usage,
314 error: Some(error),
315 }
316 }
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 #[test]
324 fn test_thread_event_serialization() {
325 let event = ThreadEvent::ThreadStarted {
326 thread_id: "thread_123".to_string(),
327 };
328 let json = serde_json::to_string(&event).unwrap();
329 assert!(json.contains("\"type\":\"thread_started\""));
330 }
331
332 #[test]
333 fn test_item_serialization() {
334 let item = Item::AgentMessage {
335 content: "Hello!".to_string(),
336 name: None,
337 };
338 let json = serde_json::to_string(&item).unwrap();
339 assert!(json.contains("\"type\":\"agent_message\""));
340 }
341
342 #[test]
343 fn test_agent_error_recoverable() {
344 assert!(AgentError::ToolError {
345 tool: "bash".to_string(),
346 message: "command failed".to_string(),
347 }
348 .is_recoverable());
349
350 assert!(!AgentError::Cancelled.is_recoverable());
351 }
352
353 #[test]
354 fn test_execution_result() {
355 let result = ExecutionResult::success("Done!", 5, TokenUsage::default());
356 assert!(result.success);
357 assert!(result.error.is_none());
358
359 let failure = ExecutionResult::failure(AgentError::Cancelled, 3, TokenUsage::default());
360 assert!(!failure.success);
361 assert!(failure.error.is_some());
362 }
363
364 #[test]
365 fn test_new_event_serialization() {
366 let event = ThreadEvent::GoalVerificationStarted {
368 goals: vec!["test-coverage".to_string(), "lint".to_string()],
369 method: "auto".to_string(),
370 };
371 let json = serde_json::to_string(&event).unwrap();
372 assert!(json.contains("\"type\":\"goal_verification_started\""));
373 let parsed: ThreadEvent = serde_json::from_str(&json).unwrap();
374 match parsed {
375 ThreadEvent::GoalVerificationStarted { goals, method } => {
376 assert_eq!(goals.len(), 2);
377 assert_eq!(method, "auto");
378 }
379 _ => panic!("Wrong variant"),
380 }
381
382 let event = ThreadEvent::GoalVerificationResult {
383 goal: "test-coverage".to_string(),
384 score: 85.5,
385 target: 80.0,
386 passed: true,
387 duration_ms: 1234,
388 };
389 let json = serde_json::to_string(&event).unwrap();
390 assert!(json.contains("\"type\":\"goal_verification_result\""));
391
392 let event = ThreadEvent::GoalVerificationCompleted {
393 all_passed: true,
394 passed_count: 3,
395 total_count: 3,
396 };
397 let json = serde_json::to_string(&event).unwrap();
398 assert!(json.contains("\"type\":\"goal_verification_completed\""));
399
400 let event = ThreadEvent::RalphIterationStarted {
402 iteration: 1,
403 max_iterations: 10,
404 prompt: "Fix all tests".to_string(),
405 };
406 let json = serde_json::to_string(&event).unwrap();
407 assert!(json.contains("\"type\":\"ralph_iteration_started\""));
408
409 let event = ThreadEvent::RalphContinuation {
410 reason: "verification_failed".to_string(),
411 confidence: 45,
412 details: "2 tests still failing".to_string(),
413 };
414 let json = serde_json::to_string(&event).unwrap();
415 assert!(json.contains("\"type\":\"ralph_continuation\""));
416
417 let event = ThreadEvent::RalphCircuitBreak {
418 reason: "stagnation".to_string(),
419 iteration: 5,
420 };
421 let json = serde_json::to_string(&event).unwrap();
422 assert!(json.contains("\"type\":\"ralph_circuit_break\""));
423
424 let event = ThreadEvent::BackgroundTaskSpawned {
426 task_id: "bg_123".to_string(),
427 description: "Running tests".to_string(),
428 agent: "executor".to_string(),
429 };
430 let json = serde_json::to_string(&event).unwrap();
431 assert!(json.contains("\"type\":\"background_task_spawned\""));
432
433 let event = ThreadEvent::BackgroundTaskProgress {
434 task_id: "bg_123".to_string(),
435 status: "running".to_string(),
436 message: Some("50% complete".to_string()),
437 };
438 let json = serde_json::to_string(&event).unwrap();
439 assert!(json.contains("\"type\":\"background_task_progress\""));
440
441 let event = ThreadEvent::BackgroundTaskCompleted {
442 task_id: "bg_123".to_string(),
443 success: true,
444 result_preview: Some("All tests passed".to_string()),
445 duration_secs: 12.5,
446 };
447 let json = serde_json::to_string(&event).unwrap();
448 assert!(json.contains("\"type\":\"background_task_completed\""));
449 }
450}