Skip to main content

synwire_core/runnables/
events.rs

1//! Stream events and content categories.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use std::collections::HashMap;
6
7/// An event emitted during runnable execution.
8#[derive(Debug, Clone, Serialize, Deserialize)]
9#[serde(tag = "kind")]
10#[non_exhaustive]
11pub enum StreamEvent {
12    /// Standard lifecycle event.
13    #[serde(rename = "standard")]
14    Standard {
15        /// Event name (e.g. `on_chain_start`).
16        event: String,
17        /// Runnable name.
18        name: String,
19        /// Run identifier.
20        run_id: String,
21        /// Parent run identifiers.
22        #[serde(default)]
23        parent_ids: Vec<String>,
24        /// Tags.
25        #[serde(default)]
26        tags: Vec<String>,
27        /// Metadata.
28        #[serde(default)]
29        metadata: HashMap<String, Value>,
30        /// Event data.
31        data: EventData,
32    },
33    /// Custom user-dispatched event.
34    #[serde(rename = "custom")]
35    Custom {
36        /// Event name.
37        name: String,
38        /// Run identifier.
39        run_id: String,
40        /// Parent run identifiers.
41        #[serde(default)]
42        parent_ids: Vec<String>,
43        /// Tags.
44        #[serde(default)]
45        tags: Vec<String>,
46        /// Metadata.
47        #[serde(default)]
48        metadata: HashMap<String, Value>,
49        /// Custom event data.
50        data: Value,
51    },
52}
53
54/// Data payload for standard stream events.
55#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct EventData {
57    /// Input data.
58    #[serde(default, skip_serializing_if = "Option::is_none")]
59    pub input: Option<Value>,
60    /// Output data.
61    #[serde(default, skip_serializing_if = "Option::is_none")]
62    pub output: Option<Value>,
63    /// Chunk data (for streaming events).
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub chunk: Option<Value>,
66    /// Error description.
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub error: Option<String>,
69    /// Content category distinguishing primary response content from
70    /// secondary metadata such as tool calls and usage metrics.
71    #[serde(default, skip_serializing_if = "Option::is_none")]
72    pub category: Option<ContentCategory>,
73}
74
75/// Dispatch a custom event.
76///
77/// Creates a [`StreamEvent::Custom`] with the given name and data.
78/// The `run_id` is set to an empty string and can be overridden by the
79/// event bus or callback infrastructure.
80pub fn dispatch_custom_event(name: impl Into<String>, data: serde_json::Value) -> StreamEvent {
81    StreamEvent::Custom {
82        name: name.into(),
83        run_id: String::new(),
84        parent_ids: Vec::new(),
85        tags: Vec::new(),
86        metadata: HashMap::new(),
87        data,
88    }
89}
90
91/// Distinguishes primary from secondary content in streaming responses.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
93#[non_exhaustive]
94pub enum ContentCategory {
95    /// Actual response content: text, structured data.
96    Primary,
97    /// Intermediate: tool calls, reasoning, usage metrics.
98    Secondary,
99}
100
101#[cfg(test)]
102#[allow(clippy::unwrap_used, clippy::panic)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_dispatch_custom_event() {
108        let data = serde_json::json!({"key": "value"});
109        let event = dispatch_custom_event("my_event", data.clone());
110        match event {
111            StreamEvent::Custom { name, data: d, .. } => {
112                assert_eq!(name, "my_event");
113                assert_eq!(d, data);
114            }
115            _ => panic!("expected Custom variant"),
116        }
117    }
118
119    #[test]
120    fn test_dispatch_custom_event_empty_defaults() {
121        let event = dispatch_custom_event("test", serde_json::Value::Null);
122        match event {
123            StreamEvent::Custom {
124                run_id,
125                parent_ids,
126                tags,
127                metadata,
128                ..
129            } => {
130                assert!(run_id.is_empty());
131                assert!(parent_ids.is_empty());
132                assert!(tags.is_empty());
133                assert!(metadata.is_empty());
134            }
135            _ => panic!("expected Custom variant"),
136        }
137    }
138}