praxis_observability/types.rs
1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4// Re-export TokenUsage from praxis-llm to avoid duplication
5pub use praxis_llm::TokenUsage;
6
7/// Observation data captured during node execution
8///
9/// Contains all input/output information needed for tracing.
10/// The structure varies based on node type (LLM vs Tool).
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct NodeObservation {
13 /// Unique identifier for this observation/span
14 pub span_id: String,
15
16 /// Run identifier for the overall graph execution
17 pub run_id: String,
18
19 /// Conversation/thread identifier
20 pub conversation_id: String,
21
22 /// Node type: "llm" or "tool"
23 pub node_type: String,
24
25 /// Timestamp when node execution started
26 pub started_at: chrono::DateTime<chrono::Utc>,
27
28 /// Duration of node execution in milliseconds
29 pub duration_ms: u64,
30
31 /// Input/output data specific to node type
32 pub data: NodeObservationData,
33
34 /// Optional metadata
35 pub metadata: HashMap<String, serde_json::Value>,
36}
37
38/// Output from a node execution
39///
40/// Represents structured outputs that can be traced separately
41#[derive(Debug, Clone, Serialize, Deserialize)]
42#[serde(tag = "output_type", rename_all = "snake_case")]
43pub enum NodeOutput {
44 /// Reasoning output from models like GPT-5, o1
45 Reasoning {
46 /// OpenAI reasoning ID (rs_xxx)
47 id: String,
48 /// Reasoning content/summary
49 content: String,
50 },
51 /// Regular message output
52 Message {
53 /// OpenAI message ID (msg_xxx)
54 id: String,
55 /// Message content
56 content: String,
57 },
58 /// Tool calls output
59 ToolCalls {
60 /// Tool call information
61 calls: Vec<ToolCallInfo>,
62 },
63}
64
65/// Node-specific observation data
66#[derive(Debug, Clone, Serialize, Deserialize)]
67#[serde(tag = "type", rename_all = "snake_case")]
68pub enum NodeObservationData {
69 /// LLM node execution data
70 Llm {
71 /// Messages sent to the LLM (input)
72 input_messages: Vec<LangfuseMessage>,
73
74 /// Structured outputs from the LLM (can be multiple: reasoning + message)
75 outputs: Vec<NodeOutput>,
76
77 /// Model identifier
78 model: String,
79
80 /// Token usage information
81 #[serde(skip_serializing_if = "Option::is_none")]
82 usage: Option<TokenUsage>,
83 },
84
85 /// Tool node execution data
86 Tool {
87 /// Tool calls that were executed (input)
88 tool_calls: Vec<ToolCallInfo>,
89
90 /// Results from tool executions (output)
91 tool_results: Vec<ToolResultInfo>,
92 },
93}
94
95/// Message format compatible with Langfuse
96#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct LangfuseMessage {
98 /// Message role: "system", "user", "assistant", "tool"
99 pub role: String,
100
101 /// Message content
102 pub content: String,
103
104 /// Optional message name
105 #[serde(skip_serializing_if = "Option::is_none")]
106 pub name: Option<String>,
107
108 /// Optional tool call ID (for tool messages)
109 #[serde(skip_serializing_if = "Option::is_none")]
110 pub tool_call_id: Option<String>,
111
112 /// Optional tool calls (for assistant messages)
113 #[serde(skip_serializing_if = "Option::is_none")]
114 pub tool_calls: Option<Vec<ToolCallInfo>>,
115}
116
117/// Tool call information
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ToolCallInfo {
120 /// Tool call identifier
121 pub id: String,
122
123 /// Tool name
124 pub name: String,
125
126 /// Tool arguments as JSON
127 pub arguments: serde_json::Value,
128}
129
130/// Tool execution result
131#[derive(Debug, Clone, Serialize, Deserialize)]
132pub struct ToolResultInfo {
133 /// Tool call identifier this result corresponds to
134 pub tool_call_id: String,
135
136 /// Tool name
137 pub tool_name: String,
138
139 /// Result content
140 pub result: String,
141
142 /// Whether the tool execution resulted in an error
143 pub is_error: bool,
144
145 /// Execution duration in milliseconds
146 pub duration_ms: u64,
147}
148
149/// Context for managing trace and span IDs
150///
151/// Maintains state across the lifetime of a graph execution.
152#[derive(Debug, Clone)]
153pub struct TraceContext {
154 /// Unique trace identifier
155 pub trace_id: String,
156
157 /// Run identifier
158 pub run_id: String,
159
160 /// Conversation identifier
161 pub conversation_id: String,
162
163 /// Timestamp when trace started
164 pub started_at: chrono::DateTime<chrono::Utc>,
165
166 /// Counter for generating span IDs
167 pub span_counter: u32,
168}
169
170impl TraceContext {
171 /// Create a new trace context
172 pub fn new(run_id: String, conversation_id: String) -> Self {
173 Self {
174 trace_id: uuid::Uuid::new_v4().to_string(),
175 run_id,
176 conversation_id,
177 started_at: chrono::Utc::now(),
178 span_counter: 0,
179 }
180 }
181
182 /// Generate a new unique span ID
183 pub fn next_span_id(&mut self) -> String {
184 self.span_counter += 1;
185 format!("{}-span-{}", self.trace_id, self.span_counter)
186 }
187}
188