1use std::collections::HashMap;
34use std::sync::RwLock;
35
36use chrono::{DateTime, Utc};
37use rust_decimal::Decimal;
38use serde::{Deserialize, Serialize};
39use serde_json::Value;
40use tokio::sync::broadcast;
41use uuid::Uuid;
42
43const DEFAULT_BUFFER_SIZE: usize = 64;
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
62#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
63pub struct WorkflowStepStartedEvent {
64 pub step_name: String,
66 pub step_index: u32,
68 pub timestamp: DateTime<Utc>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
88#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
89pub struct WorkflowStepCompletedEvent {
90 pub step_name: String,
92 pub step_index: u32,
94 pub duration_ms: u64,
96 pub output_summary: Option<String>,
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
116#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
117pub struct WorkflowStepFailedEvent {
118 pub step_name: String,
120 pub step_index: u32,
122 pub error: String,
124 pub duration_ms: u64,
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
144#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
145pub struct WorkflowApprovalRequiredEvent {
146 pub step_name: String,
148 pub step_index: u32,
150 pub approval_id: Uuid,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
174#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
175pub struct WorkflowInputRequiredEvent {
176 pub run_id: Uuid,
178 pub step_id: Uuid,
180 pub step_name: String,
182 pub step_index: u32,
184 pub message: String,
186 #[cfg_attr(feature = "openapi", schema(value_type = Object))]
188 pub schema: Value,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize)]
207#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
208pub struct WorkflowAgentStepTokensUsedEvent {
209 pub step_name: String,
211 pub tokens: u64,
213 pub cost_usd: Decimal,
215}
216
217#[derive(Debug, Clone, Serialize, Deserialize)]
238#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
239pub struct WorkflowAgentStepResumedEvent {
240 pub step_name: String,
242 pub step_index: u32,
244 pub session_id: String,
246 pub timestamp: DateTime<Utc>,
248}
249
250#[derive(Debug, Clone, Serialize, Deserialize)]
278#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
279#[serde(tag = "type", rename_all = "snake_case")]
280pub enum WorkflowEvent {
281 StepStarted(WorkflowStepStartedEvent),
283
284 StepCompleted(WorkflowStepCompletedEvent),
286
287 StepFailed(WorkflowStepFailedEvent),
289
290 ApprovalRequired(WorkflowApprovalRequiredEvent),
292
293 InputRequired(WorkflowInputRequiredEvent),
295
296 AgentStepTokensUsed(WorkflowAgentStepTokensUsedEvent),
298
299 AgentStepResumed(WorkflowAgentStepResumedEvent),
301}
302
303impl WorkflowEvent {
304 pub const STEP_STARTED: &'static str = "step_started";
306 pub const STEP_COMPLETED: &'static str = "step_completed";
308 pub const STEP_FAILED: &'static str = "step_failed";
310 pub const APPROVAL_REQUIRED: &'static str = "approval_required";
312 pub const INPUT_REQUIRED: &'static str = "input_required";
314 pub const AGENT_STEP_TOKENS_USED: &'static str = "agent_step_tokens_used";
316 pub const AGENT_STEP_RESUMED: &'static str = "agent_step_resumed";
318
319 #[deny(unreachable_patterns)]
335 pub fn event_type(&self) -> &'static str {
336 match self {
337 WorkflowEvent::StepStarted(_) => Self::STEP_STARTED,
338 WorkflowEvent::StepCompleted(_) => Self::STEP_COMPLETED,
339 WorkflowEvent::StepFailed(_) => Self::STEP_FAILED,
340 WorkflowEvent::ApprovalRequired(_) => Self::APPROVAL_REQUIRED,
341 WorkflowEvent::InputRequired(_) => Self::INPUT_REQUIRED,
342 WorkflowEvent::AgentStepTokensUsed(_) => Self::AGENT_STEP_TOKENS_USED,
343 WorkflowEvent::AgentStepResumed(_) => Self::AGENT_STEP_RESUMED,
344 }
345 }
346}
347
348#[derive(Clone)]
375pub struct WorkflowEventBus {
376 channels: std::sync::Arc<RwLock<HashMap<Uuid, broadcast::Sender<WorkflowEvent>>>>,
377}
378
379impl WorkflowEventBus {
380 pub fn new() -> Self {
390 Self {
391 channels: std::sync::Arc::new(RwLock::new(HashMap::new())),
392 }
393 }
394
395 pub fn subscribe(&self, run_id: Uuid) -> broadcast::Receiver<WorkflowEvent> {
412 let mut channels = self.channels.write().expect("event bus lock poisoned");
413 let sender = channels
414 .entry(run_id)
415 .or_insert_with(|| broadcast::channel(DEFAULT_BUFFER_SIZE).0);
416 sender.subscribe()
417 }
418
419 pub fn publish(&self, run_id: Uuid, event: WorkflowEvent) {
444 let channels = self.channels.read().expect("event bus lock poisoned");
445 if let Some(sender) = channels.get(&run_id) {
446 let _ = sender.send(event);
447 }
448 }
449
450 pub fn remove(&self, run_id: Uuid) {
467 let mut channels = self.channels.write().expect("event bus lock poisoned");
468 channels.remove(&run_id);
469 }
470}
471
472impl Default for WorkflowEventBus {
473 fn default() -> Self {
474 Self::new()
475 }
476}
477
478impl std::fmt::Debug for WorkflowEventBus {
479 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
480 let count = self.channels.read().map(|c| c.len()).unwrap_or(0);
481 f.debug_struct("WorkflowEventBus")
482 .field("active_channels", &count)
483 .finish()
484 }
485}
486
487#[cfg(test)]
488mod tests {
489 use serde_json::json;
490
491 use super::*;
492
493 fn step_started(step_name: &str) -> WorkflowEvent {
494 WorkflowEvent::StepStarted(WorkflowStepStartedEvent {
495 step_name: step_name.to_string(),
496 step_index: 0,
497 timestamp: Utc::now(),
498 })
499 }
500
501 #[tokio::test]
502 async fn subscribe_receives_published_events() {
503 let bus = WorkflowEventBus::new();
504 let run_id = Uuid::now_v7();
505
506 let mut rx = bus.subscribe(run_id);
507
508 bus.publish(run_id, step_started("build"));
509
510 let received = rx.recv().await.expect("should receive event");
511 assert_eq!(received.event_type(), "step_started");
512 match received {
513 WorkflowEvent::StepStarted(e) => {
514 assert_eq!(e.step_name, "build");
515 assert_eq!(e.step_index, 0);
516 }
517 _ => panic!("expected StepStarted"),
518 }
519 }
520
521 #[test]
522 fn subscribe_creates_channel_on_demand() {
523 let bus = WorkflowEventBus::new();
524 let run_id = Uuid::now_v7();
525
526 let count_before = bus.channels.read().unwrap().len();
527 assert_eq!(count_before, 0);
528
529 let _rx = bus.subscribe(run_id);
530
531 let count_after = bus.channels.read().unwrap().len();
532 assert_eq!(count_after, 1);
533 }
534
535 #[test]
536 fn publish_unknown_run_is_noop() {
537 let bus = WorkflowEventBus::new();
538 let unknown_run = Uuid::now_v7();
539
540 bus.publish(unknown_run, step_started("build"));
541 }
542
543 #[test]
544 fn remove_cleans_up_channel() {
545 let bus = WorkflowEventBus::new();
546 let run_id = Uuid::now_v7();
547
548 let _rx = bus.subscribe(run_id);
549 assert_eq!(bus.channels.read().unwrap().len(), 1);
550
551 bus.remove(run_id);
552 assert_eq!(bus.channels.read().unwrap().len(), 0);
553 }
554
555 #[test]
556 fn remove_unknown_is_noop() {
557 let bus = WorkflowEventBus::new();
558 bus.remove(Uuid::now_v7());
559 }
560
561 #[test]
562 fn workflow_event_serde_roundtrip() {
563 let cases: Vec<WorkflowEvent> = vec![
564 WorkflowEvent::StepStarted(WorkflowStepStartedEvent {
565 step_name: "build".to_string(),
566 step_index: 0,
567 timestamp: Utc::now(),
568 }),
569 WorkflowEvent::StepCompleted(WorkflowStepCompletedEvent {
570 step_name: "deploy".to_string(),
571 step_index: 1,
572 duration_ms: 5000,
573 output_summary: Some("deployed v1.2.3".to_string()),
574 }),
575 WorkflowEvent::StepFailed(WorkflowStepFailedEvent {
576 step_name: "test".to_string(),
577 step_index: 2,
578 error: "exit code 1".to_string(),
579 duration_ms: 3000,
580 }),
581 WorkflowEvent::ApprovalRequired(WorkflowApprovalRequiredEvent {
582 step_name: "prod-gate".to_string(),
583 step_index: 3,
584 approval_id: Uuid::now_v7(),
585 }),
586 WorkflowEvent::InputRequired(WorkflowInputRequiredEvent {
587 run_id: Uuid::now_v7(),
588 step_id: Uuid::now_v7(),
589 step_name: "clarify".to_string(),
590 step_index: 4,
591 message: "Answer the questions".to_string(),
592 schema: json!({"type": "object"}),
593 }),
594 WorkflowEvent::AgentStepTokensUsed(WorkflowAgentStepTokensUsedEvent {
595 step_name: "review".to_string(),
596 tokens: 15000,
597 cost_usd: Decimal::new(42, 4),
598 }),
599 WorkflowEvent::AgentStepResumed(WorkflowAgentStepResumedEvent {
600 step_name: "review".to_string(),
601 step_index: 5,
602 session_id: "0192f0c1-7d2e-7a4b-9c3d-1e2f3a4b5c6d".to_string(),
603 timestamp: Utc::now(),
604 }),
605 ];
606
607 for event in &cases {
608 let json = serde_json::to_string(event).expect("serialize");
609 let back: WorkflowEvent = serde_json::from_str(&json).expect("deserialize");
610
611 assert_eq!(back.event_type(), event.event_type());
612 assert!(json.contains(&format!("\"type\":\"{}\"", event.event_type())));
613 }
614 }
615
616 #[test]
620 fn workflow_event_legacy_flat_json_deserializes() {
621 let approval_id: Uuid = "01890000-0000-7000-8000-000000000002"
622 .parse()
623 .expect("valid uuid");
624
625 let raw = r#"{"type":"step_started","step_name":"build","step_index":0,"timestamp":"2026-01-01T00:00:00Z"}"#;
626 match serde_json::from_str::<WorkflowEvent>(raw).expect("legacy payload") {
627 WorkflowEvent::StepStarted(e) => {
628 assert_eq!(e.step_name, "build");
629 assert_eq!(e.step_index, 0);
630 }
631 other => panic!("expected StepStarted, got {other:?}"),
632 }
633
634 let raw = r#"{"type":"step_completed","step_name":"deploy","step_index":1,"duration_ms":5000,"output_summary":"deployed v1.2.3"}"#;
635 match serde_json::from_str::<WorkflowEvent>(raw).expect("legacy payload") {
636 WorkflowEvent::StepCompleted(e) => {
637 assert_eq!(e.duration_ms, 5000);
638 assert_eq!(e.output_summary.as_deref(), Some("deployed v1.2.3"));
639 }
640 other => panic!("expected StepCompleted, got {other:?}"),
641 }
642
643 let raw = r#"{"type":"step_failed","step_name":"test","step_index":2,"error":"exit code 1","duration_ms":3000}"#;
644 match serde_json::from_str::<WorkflowEvent>(raw).expect("legacy payload") {
645 WorkflowEvent::StepFailed(e) => {
646 assert_eq!(e.error, "exit code 1");
647 assert_eq!(e.duration_ms, 3000);
648 }
649 other => panic!("expected StepFailed, got {other:?}"),
650 }
651
652 let raw = r#"{"type":"approval_required","step_name":"prod-gate","step_index":3,"approval_id":"01890000-0000-7000-8000-000000000002"}"#;
653 match serde_json::from_str::<WorkflowEvent>(raw).expect("legacy payload") {
654 WorkflowEvent::ApprovalRequired(e) => {
655 assert_eq!(e.approval_id, approval_id);
656 }
657 other => panic!("expected ApprovalRequired, got {other:?}"),
658 }
659
660 let raw = r#"{"type":"agent_step_tokens_used","step_name":"review","tokens":15000,"cost_usd":0.5}"#;
661 match serde_json::from_str::<WorkflowEvent>(raw).expect("legacy payload") {
662 WorkflowEvent::AgentStepTokensUsed(e) => {
663 assert_eq!(e.tokens, 15000);
664 assert_eq!(e.cost_usd, Decimal::new(5, 1));
665 }
666 other => panic!("expected AgentStepTokensUsed, got {other:?}"),
667 }
668 }
669
670 #[test]
673 fn serialized_workflow_event_is_flat_with_type_tag() {
674 let event = WorkflowEvent::StepFailed(WorkflowStepFailedEvent {
675 step_name: "test".to_string(),
676 step_index: 2,
677 error: "exit code 1".to_string(),
678 duration_ms: 3000,
679 });
680
681 let value: serde_json::Value = serde_json::to_value(&event).expect("serialize");
682 let object = value.as_object().expect("event serializes to an object");
683
684 assert_eq!(
685 object.get("type").and_then(|v| v.as_str()),
686 Some("step_failed")
687 );
688 assert_eq!(
689 object.get("step_name").and_then(|v| v.as_str()),
690 Some("test")
691 );
692 assert_eq!(object.get("step_index").and_then(|v| v.as_u64()), Some(2));
693 assert_eq!(
694 object.get("error").and_then(|v| v.as_str()),
695 Some("exit code 1")
696 );
697 assert_eq!(
698 object.get("duration_ms").and_then(|v| v.as_u64()),
699 Some(3000)
700 );
701 assert_eq!(object.len(), 5, "no nesting: {object:?}");
702 }
703
704 #[test]
705 fn input_required_event_serializes_flat_with_its_schema() {
706 let step_id = Uuid::now_v7();
707 let event = WorkflowEvent::InputRequired(WorkflowInputRequiredEvent {
708 run_id: Uuid::now_v7(),
709 step_id,
710 step_name: "clarify".to_string(),
711 step_index: 1,
712 message: "Answer the questions".to_string(),
713 schema: json!({"type": "object", "required": ["answers"]}),
714 });
715
716 let value = serde_json::to_value(&event).expect("serialize");
717 assert_eq!(value["type"], "input_required");
718 assert_eq!(value["step_id"], step_id.to_string());
719 assert_eq!(value["message"], "Answer the questions");
720 assert_eq!(value["schema"]["required"][0], "answers");
721 assert_eq!(event.event_type(), WorkflowEvent::INPUT_REQUIRED);
722 }
723
724 #[test]
725 fn event_type_all_variants() {
726 let cases: Vec<(WorkflowEvent, &str)> = vec![
727 (
728 WorkflowEvent::StepStarted(WorkflowStepStartedEvent {
729 step_name: "s".to_string(),
730 step_index: 0,
731 timestamp: Utc::now(),
732 }),
733 "step_started",
734 ),
735 (
736 WorkflowEvent::StepCompleted(WorkflowStepCompletedEvent {
737 step_name: "s".to_string(),
738 step_index: 0,
739 duration_ms: 0,
740 output_summary: None,
741 }),
742 "step_completed",
743 ),
744 (
745 WorkflowEvent::StepFailed(WorkflowStepFailedEvent {
746 step_name: "s".to_string(),
747 step_index: 0,
748 error: "e".to_string(),
749 duration_ms: 0,
750 }),
751 "step_failed",
752 ),
753 (
754 WorkflowEvent::ApprovalRequired(WorkflowApprovalRequiredEvent {
755 step_name: "s".to_string(),
756 step_index: 0,
757 approval_id: Uuid::now_v7(),
758 }),
759 "approval_required",
760 ),
761 (
762 WorkflowEvent::InputRequired(WorkflowInputRequiredEvent {
763 run_id: Uuid::now_v7(),
764 step_id: Uuid::now_v7(),
765 step_name: "s".to_string(),
766 step_index: 0,
767 message: "m".to_string(),
768 schema: Value::Null,
769 }),
770 "input_required",
771 ),
772 (
773 WorkflowEvent::AgentStepTokensUsed(WorkflowAgentStepTokensUsedEvent {
774 step_name: "s".to_string(),
775 tokens: 0,
776 cost_usd: Decimal::ZERO,
777 }),
778 "agent_step_tokens_used",
779 ),
780 (
781 WorkflowEvent::AgentStepResumed(WorkflowAgentStepResumedEvent {
782 step_name: "s".to_string(),
783 step_index: 0,
784 session_id: "sid".to_string(),
785 timestamp: Utc::now(),
786 }),
787 "agent_step_resumed",
788 ),
789 ];
790
791 for (event, expected) in cases {
792 assert_eq!(event.event_type(), expected);
793 }
794 }
795
796 #[tokio::test]
797 async fn multiple_subscribers_receive_same_event() {
798 let bus = WorkflowEventBus::new();
799 let run_id = Uuid::now_v7();
800
801 let mut rx1 = bus.subscribe(run_id);
802 let mut rx2 = bus.subscribe(run_id);
803
804 bus.publish(run_id, step_started("build"));
805
806 let e1 = rx1.recv().await.expect("rx1 should receive");
807 let e2 = rx2.recv().await.expect("rx2 should receive");
808
809 assert_eq!(e1.event_type(), "step_started");
810 assert_eq!(e2.event_type(), "step_started");
811 }
812
813 #[tokio::test]
814 async fn events_isolated_between_runs() {
815 let bus = WorkflowEventBus::new();
816 let run_a = Uuid::now_v7();
817 let run_b = Uuid::now_v7();
818
819 let mut rx_a = bus.subscribe(run_a);
820 let mut rx_b = bus.subscribe(run_b);
821
822 bus.publish(run_a, step_started("only-for-a"));
823
824 let received = rx_a.recv().await.expect("rx_a should receive");
825 match received {
826 WorkflowEvent::StepStarted(e) => {
827 assert_eq!(e.step_name, "only-for-a");
828 }
829 _ => panic!("expected StepStarted"),
830 }
831
832 assert!(rx_b.try_recv().is_err());
834 }
835}