praxis_graph/types/
events.rs

1use serde::{Deserialize, Serialize};
2
3/// Unified StreamEvent for Graph orchestration
4/// 
5/// Includes both LLM streaming events and Graph-specific orchestration events.
6/// This is the canonical event type used throughout the Graph execution.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(tag = "type", rename_all = "snake_case")]
9pub enum StreamEvent {
10    /// Graph execution started
11    InitStream {
12        run_id: String,
13        conversation_id: String,
14        timestamp: i64,
15    },
16    
17    /// Internal reasoning from LLM (streamed token-by-token)
18    Reasoning {
19        content: String,
20    },
21    
22    /// Response message from LLM (streamed token-by-token)
23    Message {
24        content: String,
25    },
26    
27    /// LLM decided to call a tool (streamed incrementally)
28    ToolCall {
29        index: u32,
30        #[serde(skip_serializing_if = "Option::is_none")]
31        id: Option<String>,
32        #[serde(skip_serializing_if = "Option::is_none")]
33        name: Option<String>,
34        #[serde(skip_serializing_if = "Option::is_none")]
35        arguments: Option<String>,
36    },
37    
38    /// Tool execution completed
39    ToolResult {
40        tool_call_id: String,
41        result: String,
42        is_error: bool,
43        duration_ms: u64,
44    },
45    
46    /// LLM streaming completed
47    Done {
48        #[serde(skip_serializing_if = "Option::is_none")]
49        finish_reason: Option<String>,
50    },
51    
52    /// Fatal error occurred
53    Error {
54        message: String,
55        #[serde(skip_serializing_if = "Option::is_none")]
56        node_id: Option<String>,
57    },
58    
59    /// Graph execution completed
60    EndStream {
61        status: String,
62        total_duration_ms: u64,
63    },
64}
65
66/// Automatic conversion from LLM StreamEvent to Graph StreamEvent
67impl From<praxis_llm::StreamEvent> for StreamEvent {
68    fn from(event: praxis_llm::StreamEvent) -> Self {
69        match event {
70            praxis_llm::StreamEvent::Reasoning { content } => {
71                Self::Reasoning { content }
72            }
73            praxis_llm::StreamEvent::Message { content } => {
74                Self::Message { content }
75            }
76            praxis_llm::StreamEvent::ToolCall {
77                index,
78                id,
79                name,
80                arguments,
81            } => Self::ToolCall {
82                index,
83                id,
84                name,
85                arguments,
86            },
87            praxis_llm::StreamEvent::Done { finish_reason } => {
88                Self::Done { finish_reason }
89            }
90        }
91    }
92}
93
94/// Implementation of StreamEventExtractor for praxis-persist compatibility
95impl praxis_persist::StreamEventExtractor for StreamEvent {
96    fn is_reasoning(&self) -> bool {
97        matches!(self, Self::Reasoning { .. })
98    }
99    
100    fn is_message(&self) -> bool {
101        matches!(self, Self::Message { .. })
102    }
103    
104    fn is_tool_call(&self) -> bool {
105        matches!(self, Self::ToolCall { .. })
106    }
107    
108    fn reasoning_content(&self) -> Option<&str> {
109        match self {
110            Self::Reasoning { content } => Some(content),
111            _ => None,
112        }
113    }
114    
115    fn message_content(&self) -> Option<&str> {
116        match self {
117            Self::Message { content } => Some(content),
118            _ => None,
119        }
120    }
121    
122    fn tool_call_info(&self) -> Option<(u32, Option<&str>, Option<&str>, Option<&str>)> {
123        match self {
124            Self::ToolCall { index, id, name, arguments } => {
125                Some((
126                    *index,
127                    id.as_deref(),
128                    name.as_deref(),
129                    arguments.as_deref(),
130                ))
131            },
132            _ => None,
133        }
134    }
135}
136