1use crate::tools::ToolResult;
39use bamboo_domain::{TaskItemStatus, TaskList};
40use chrono::{DateTime, Utc};
41use serde::{Deserialize, Serialize};
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
96#[serde(tag = "type", rename_all = "snake_case")]
97pub enum AgentEvent {
98 Token {
100 content: String,
102 },
103
104 ReasoningToken {
109 content: String,
111 },
112
113 ToolToken {
118 tool_call_id: String,
120 content: String,
122 },
123
124 ToolStart {
126 tool_call_id: String,
128 tool_name: String,
130 arguments: serde_json::Value,
132 },
133
134 ToolComplete {
136 tool_call_id: String,
138 result: ToolResult,
140 },
141
142 ToolError {
144 tool_call_id: String,
146 error: String,
148 },
149
150 ToolLifecycle {
156 tool_call_id: String,
158 tool_name: String,
160 phase: String,
162 #[serde(skip_serializing_if = "Option::is_none")]
164 elapsed_ms: Option<u64>,
165 is_mutating: bool,
167 auto_approved: bool,
169 #[serde(skip_serializing_if = "Option::is_none")]
171 summary: Option<String>,
172 #[serde(skip_serializing_if = "Option::is_none")]
174 error: Option<String>,
175 },
176
177 NeedClarification {
179 question: String,
181 options: Option<Vec<String>>,
183 #[serde(default, skip_serializing_if = "Option::is_none")]
185 tool_call_id: Option<String>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 tool_name: Option<String>,
189 #[serde(default = "default_allow_custom")]
191 allow_custom: bool,
192 },
193
194 TaskListUpdated {
196 task_list: TaskList,
198 },
199
200 TaskListItemProgress {
202 session_id: String,
204 item_id: String,
206 status: TaskItemStatus,
208 tool_calls_count: usize,
210 version: u64,
212 },
213
214 TaskListCompleted {
216 session_id: String,
218 completed_at: DateTime<Utc>,
220 total_rounds: u32,
222 total_tool_calls: usize,
224 },
225
226 TaskEvaluationStarted {
228 session_id: String,
230 items_count: usize,
232 },
233
234 TaskEvaluationCompleted {
236 session_id: String,
238 updates_count: usize,
240 reasoning: String,
242 },
243
244 GoldEvaluationStarted {
246 session_id: String,
248 checkpoint: GoldCheckpoint,
250 iteration: u32,
252 },
253
254 GoldEvaluationCompleted {
256 session_id: String,
258 checkpoint: GoldCheckpoint,
260 iteration: u32,
262 decision: GoldDecision,
264 confidence: GoldConfidence,
266 reasoning: String,
268 },
269
270 TokenBudgetUpdated {
272 usage: TokenBudgetUsage,
274 },
275
276 ContextCompressionStatus {
278 phase: String,
280 status: String,
282 },
283
284 ContextSummarized {
286 summary: String,
288 messages_summarized: usize,
290 tokens_saved: u32,
292 #[serde(default)]
294 usage_before_percent: f64,
295 #[serde(default)]
297 usage_after_percent: f64,
298 #[serde(default)]
300 trigger_type: String,
301 },
302
303 ContextPressureNotification {
306 percent: f64,
308 level: String,
310 message: String,
312 },
313
314 SubAgentStarted {
316 parent_session_id: String,
317 child_session_id: String,
318 #[serde(default, skip_serializing_if = "Option::is_none")]
320 title: Option<String>,
321 },
322
323 SubAgentEvent {
327 parent_session_id: String,
328 child_session_id: String,
329 event: Box<AgentEvent>,
330 },
331
332 SubAgentHeartbeat {
334 parent_session_id: String,
335 child_session_id: String,
336 timestamp: DateTime<Utc>,
337 },
338
339 SubAgentCompleted {
341 parent_session_id: String,
342 child_session_id: String,
343 status: String,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 error: Option<String>,
347 },
348
349 PlanModeEntered {
351 session_id: String,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
355 reason: Option<String>,
356 pre_permission_mode: String,
358 entered_at: chrono::DateTime<chrono::Utc>,
360 status: bamboo_domain::PlanModeStatus,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
364 plan_file_path: Option<String>,
365 },
366
367 PlanModeExited {
369 session_id: String,
371 approved: bool,
373 restored_mode: String,
375 #[serde(default, skip_serializing_if = "Option::is_none")]
377 plan: Option<String>,
378 },
379
380 PlanFileUpdated {
382 session_id: String,
384 file_path: String,
386 content_summary: String,
388 },
389
390 RunnerProgress {
395 session_id: String,
397 round_count: u32,
399 },
400
401 SessionTitleUpdated {
403 session_id: String,
404 title: String,
405 title_version: u64,
406 source: TitleSource,
407 updated_at: chrono::DateTime<chrono::Utc>,
408 },
409
410 SessionPinnedUpdated {
416 session_id: String,
417 pinned: bool,
418 updated_at: chrono::DateTime<chrono::Utc>,
419 },
420
421 ExecutionStarted {
427 run_id: String,
429 session_id: String,
431 started_at: String,
433 },
434
435 ToolApprovalRequested {
442 tool_call_id: String,
444 tool_name: String,
446 parameters: serde_json::Value,
448 },
449
450 Complete {
452 usage: TokenUsage,
454 },
455
456 Cancelled {
458 #[serde(default, skip_serializing_if = "Option::is_none")]
460 message: Option<String>,
461 },
462
463 Error {
465 message: String,
467 },
468}
469
470fn default_allow_custom() -> bool {
471 true
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
476#[serde(rename_all = "snake_case")]
477pub enum GoldCheckpoint {
478 PostRound,
479 Terminal,
480}
481
482impl GoldCheckpoint {
483 pub fn as_str(self) -> &'static str {
484 match self {
485 Self::PostRound => "post_round",
486 Self::Terminal => "terminal",
487 }
488 }
489}
490
491#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
493#[serde(rename_all = "snake_case")]
494pub enum GoldDecision {
495 Continue,
496 Achieved,
497 Blocked,
498 NeedInput,
499 Exhausted,
500}
501
502impl GoldDecision {
503 pub fn as_str(self) -> &'static str {
504 match self {
505 Self::Continue => "continue",
506 Self::Achieved => "achieved",
507 Self::Blocked => "blocked",
508 Self::NeedInput => "need_input",
509 Self::Exhausted => "exhausted",
510 }
511 }
512}
513
514#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
516#[serde(rename_all = "snake_case")]
517pub enum GoldConfidence {
518 Low,
519 Medium,
520 High,
521}
522
523impl GoldConfidence {
524 pub fn as_str(self) -> &'static str {
525 match self {
526 Self::Low => "low",
527 Self::Medium => "medium",
528 Self::High => "high",
529 }
530 }
531
532 pub fn rank(self) -> u8 {
534 match self {
535 Self::Low => 0,
536 Self::Medium => 1,
537 Self::High => 2,
538 }
539 }
540
541 pub fn meets(self, floor: GoldConfidence) -> bool {
543 self.rank() >= floor.rank()
544 }
545}
546
547#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
549#[serde(rename_all = "snake_case")]
550pub enum TitleSource {
551 Auto,
552 Manual,
553 Fallback,
554}
555
556pub use bamboo_domain::TokenUsage;
560
561pub use bamboo_domain::budget_types::TokenBudgetUsage;
562
563#[cfg(test)]
564mod tests {
565 use super::*;
566 use bamboo_domain::{TaskItem, TaskItemStatus, TaskList};
567
568 fn sample_task_list() -> TaskList {
569 TaskList {
570 session_id: "session-1".to_string(),
571 title: "Task List".to_string(),
572 items: vec![TaskItem {
573 id: "task_1".to_string(),
574 description: "Implement event rename".to_string(),
575 status: TaskItemStatus::InProgress,
576 depends_on: Vec::new(),
577 notes: "Implementing".to_string(),
578 ..TaskItem::default()
579 }],
580 created_at: Utc::now(),
581 updated_at: Utc::now(),
582 }
583 }
584
585 #[test]
586 fn task_list_updated_serializes_with_task_names() {
587 let event = AgentEvent::TaskListUpdated {
588 task_list: sample_task_list(),
589 };
590
591 let value = serde_json::to_value(event).expect("event should serialize");
592 assert_eq!(value["type"], "task_list_updated");
593 assert!(value.get("task_list").is_some());
594 assert!(value.get("todo_list").is_none());
595 }
596
597 #[test]
598 fn cancelled_serializes_with_snake_case_type() {
599 let event = AgentEvent::Cancelled {
600 message: Some("Agent execution cancelled by user".to_string()),
601 };
602
603 let value = serde_json::to_value(event).expect("event should serialize");
604 assert_eq!(value["type"], "cancelled");
605 assert_eq!(
606 value["message"],
607 serde_json::Value::String("Agent execution cancelled by user".to_string())
608 );
609 }
610
611 #[test]
612 fn task_evaluation_completed_serializes_with_task_type() {
613 let event = AgentEvent::TaskEvaluationCompleted {
614 session_id: "session-1".to_string(),
615 updates_count: 2,
616 reasoning: "Updated statuses".to_string(),
617 };
618
619 let value = serde_json::to_value(event).expect("event should serialize");
620 assert_eq!(value["type"], "task_evaluation_completed");
621 }
622
623 #[test]
624 fn gold_evaluation_completed_serializes_with_gold_type_and_fields() {
625 let event = AgentEvent::GoldEvaluationCompleted {
626 session_id: "session-1".to_string(),
627 checkpoint: GoldCheckpoint::PostRound,
628 iteration: 3,
629 decision: GoldDecision::Continue,
630 confidence: GoldConfidence::Medium,
631 reasoning: "Need one more iteration".to_string(),
632 };
633
634 let value = serde_json::to_value(event).expect("event should serialize");
635 assert_eq!(value["type"], "gold_evaluation_completed");
636 assert_eq!(value["checkpoint"], "post_round");
637 assert_eq!(value["iteration"], 3);
638 assert_eq!(value["decision"], "continue");
639 assert_eq!(value["confidence"], "medium");
640 assert_eq!(value["reasoning"], "Need one more iteration");
641 }
642
643 #[test]
644 fn gold_evaluation_started_deserializes() {
645 let json = serde_json::json!({
646 "type": "gold_evaluation_started",
647 "session_id": "session-1",
648 "checkpoint": "terminal",
649 "iteration": 7
650 });
651
652 let event: AgentEvent = serde_json::from_value(json).expect("should deserialize");
653 match event {
654 AgentEvent::GoldEvaluationStarted {
655 session_id,
656 checkpoint,
657 iteration,
658 } => {
659 assert_eq!(session_id, "session-1");
660 assert_eq!(checkpoint, GoldCheckpoint::Terminal);
661 assert_eq!(iteration, 7);
662 }
663 other => panic!("unexpected event: {other:?}"),
664 }
665 }
666
667 #[test]
668 fn context_compression_status_serializes_with_phase_and_status() {
669 let event = AgentEvent::ContextCompressionStatus {
670 phase: "mid-turn".to_string(),
671 status: "started".to_string(),
672 };
673
674 let value = serde_json::to_value(event).expect("event should serialize");
675 assert_eq!(value["type"], "context_compression_status");
676 assert_eq!(value["phase"], "mid-turn");
677 assert_eq!(value["status"], "started");
678 }
679
680 #[test]
681 fn need_clarification_serializes_with_new_fields() {
682 let event = AgentEvent::NeedClarification {
683 question: "Continue?".to_string(),
684 options: Some(vec!["Yes".to_string(), "No".to_string()]),
685 tool_call_id: Some("tool-1".to_string()),
686 tool_name: Some("conclusion_with_options".to_string()),
687 allow_custom: false,
688 };
689
690 let value = serde_json::to_value(event).expect("event should serialize");
691 assert_eq!(value["type"], "need_clarification");
692 assert_eq!(value["question"], "Continue?");
693 assert_eq!(value["options"], serde_json::json!(["Yes", "No"]));
694 assert_eq!(value["tool_call_id"], "tool-1");
695 assert_eq!(value["tool_name"], "conclusion_with_options");
696 assert_eq!(value["allow_custom"], false);
697 }
698
699 #[test]
700 fn need_clarification_deserializes_from_old_format_without_new_fields() {
701 let json = serde_json::json!({
702 "type": "need_clarification",
703 "question": "Continue?",
704 "options": ["Yes", "No"]
705 });
706
707 let event: AgentEvent =
708 serde_json::from_value(json).expect("should deserialize old format");
709 match event {
710 AgentEvent::NeedClarification {
711 question,
712 options,
713 tool_call_id,
714 tool_name,
715 allow_custom,
716 } => {
717 assert_eq!(question, "Continue?");
718 assert_eq!(options, Some(vec!["Yes".to_string(), "No".to_string()]));
719 assert_eq!(tool_call_id, None);
720 assert_eq!(tool_name, None);
721 assert!(allow_custom); }
723 other => panic!("unexpected event: {other:?}"),
724 }
725 }
726
727 #[test]
728 fn need_clarification_deserializes_with_allow_custom_false() {
729 let json = serde_json::json!({
730 "type": "need_clarification",
731 "question": "Pick one",
732 "allow_custom": false
733 });
734
735 let event: AgentEvent = serde_json::from_value(json).expect("should deserialize");
736 match event {
737 AgentEvent::NeedClarification {
738 question,
739 options,
740 tool_call_id,
741 tool_name,
742 allow_custom,
743 } => {
744 assert_eq!(question, "Pick one");
745 assert_eq!(options, None);
746 assert_eq!(tool_call_id, None);
747 assert_eq!(tool_name, None);
748 assert!(!allow_custom);
749 }
750 other => panic!("unexpected event: {other:?}"),
751 }
752 }
753
754 #[test]
755 fn plan_mode_entered_serializes_correctly() {
756 let entered_at = Utc::now();
757 let event = AgentEvent::PlanModeEntered {
758 session_id: "sess-1".to_string(),
759 reason: Some("Complex refactor".to_string()),
760 pre_permission_mode: "default".to_string(),
761 entered_at,
762 status: bamboo_domain::PlanModeStatus::Exploring,
763 plan_file_path: None,
764 };
765
766 let value = serde_json::to_value(event).expect("event should serialize");
767 assert_eq!(value["type"], "plan_mode_entered");
768 assert_eq!(value["session_id"], "sess-1");
769 assert_eq!(value["reason"], "Complex refactor");
770 assert_eq!(value["pre_permission_mode"], "default");
771 assert_eq!(value["status"], "exploring");
772 assert_eq!(value["entered_at"], entered_at.to_rfc3339());
773 }
774
775 #[test]
776 fn plan_mode_exited_serializes_correctly() {
777 let event = AgentEvent::PlanModeExited {
778 session_id: "sess-1".to_string(),
779 approved: true,
780 restored_mode: "accept_edits".to_string(),
781 plan: Some("# Plan\n1. Step one".to_string()),
782 };
783
784 let value = serde_json::to_value(event).expect("event should serialize");
785 assert_eq!(value["type"], "plan_mode_exited");
786 assert_eq!(value["session_id"], "sess-1");
787 assert_eq!(value["approved"], true);
788 assert_eq!(value["restored_mode"], "accept_edits");
789 assert_eq!(value["plan"], "# Plan\n1. Step one");
790 }
791
792 #[test]
793 fn plan_file_updated_serializes_correctly() {
794 let event = AgentEvent::PlanFileUpdated {
795 session_id: "sess-1".to_string(),
796 file_path: "/tmp/plans/sess-1.md".to_string(),
797 content_summary: "Implementation plan for feature X".to_string(),
798 };
799
800 let value = serde_json::to_value(event).expect("event should serialize");
801 assert_eq!(value["type"], "plan_file_updated");
802 assert_eq!(value["session_id"], "sess-1");
803 assert_eq!(value["file_path"], "/tmp/plans/sess-1.md");
804 assert_eq!(
805 value["content_summary"],
806 "Implementation plan for feature X"
807 );
808 }
809
810 #[test]
811 fn tool_approval_requested_serializes_correctly() {
812 let event = AgentEvent::ToolApprovalRequested {
813 tool_call_id: "call-abc".to_string(),
814 tool_name: "Write".to_string(),
815 parameters: serde_json::json!({"file_path": "/tmp/test.txt"}),
816 };
817
818 let value = serde_json::to_value(event).expect("event should serialize");
819 assert_eq!(value["type"], "tool_approval_requested");
820 assert_eq!(value["tool_call_id"], "call-abc");
821 assert_eq!(value["tool_name"], "Write");
822 assert_eq!(
823 value["parameters"],
824 serde_json::json!({"file_path": "/tmp/test.txt"})
825 );
826 }
827
828 #[test]
829 fn tool_approval_requested_deserializes_correctly() {
830 let json = serde_json::json!({
831 "type": "tool_approval_requested",
832 "tool_call_id": "call-xyz",
833 "tool_name": "Bash",
834 "parameters": {"command": "ls -la"}
835 });
836
837 let event: AgentEvent = serde_json::from_value(json).expect("should deserialize");
838 match event {
839 AgentEvent::ToolApprovalRequested {
840 tool_call_id,
841 tool_name,
842 parameters,
843 } => {
844 assert_eq!(tool_call_id, "call-xyz");
845 assert_eq!(tool_name, "Bash");
846 assert_eq!(parameters, serde_json::json!({"command": "ls -la"}));
847 }
848 other => panic!("unexpected event: {other:?}"),
849 }
850 }
851
852 #[test]
853 fn session_title_updated_round_trips_with_source_variants() {
854 use chrono::Utc;
855 let event = AgentEvent::SessionTitleUpdated {
856 session_id: "sess-1".to_string(),
857 title: "My title".to_string(),
858 title_version: 3,
859 source: TitleSource::Auto,
860 updated_at: Utc::now(),
861 };
862 let json = serde_json::to_string(&event).unwrap();
863 assert!(
864 json.contains("\"type\":\"session_title_updated\""),
865 "json: {json}"
866 );
867 assert!(json.contains("\"source\":\"auto\""), "json: {json}");
868 let _decoded: AgentEvent = serde_json::from_str(&json).unwrap();
869 }
870
871 #[test]
872 fn plan_mode_events_deserialize_without_optional_fields() {
873 let json = serde_json::json!({
874 "type": "plan_mode_entered",
875 "session_id": "sess-1",
876 "pre_permission_mode": "default",
877 "entered_at": "2025-01-01T00:00:00Z",
878 "status": "exploring"
879 });
880
881 let event: AgentEvent = serde_json::from_value(json).expect("should deserialize");
882 match event {
883 AgentEvent::PlanModeEntered {
884 session_id,
885 reason,
886 pre_permission_mode,
887 entered_at,
888 status,
889 plan_file_path,
890 } => {
891 assert_eq!(session_id, "sess-1");
892 assert_eq!(reason, None);
893 assert_eq!(pre_permission_mode, "default");
894 assert_eq!(entered_at.to_rfc3339(), "2025-01-01T00:00:00+00:00");
895 assert_eq!(status, bamboo_domain::PlanModeStatus::Exploring);
896 assert_eq!(plan_file_path, None);
897 }
898 other => panic!("unexpected event: {other:?}"),
899 }
900 }
901}