use serde::{Deserialize, Serialize};
use crate::types::{FinishReason, ToolCallId};
use crate::usage::Usage;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ToolResultValue {
Json { value: serde_json::Value },
Text { value: String },
Error { value: String },
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StreamEvent {
StepStart {
index: u32,
},
StepFinish {
index: u32,
finish_reason: FinishReason,
#[serde(skip_serializing_if = "Option::is_none")]
usage: Option<Usage>,
},
TextStart {
content_index: usize,
},
TextDelta {
content_index: usize,
delta: String,
},
TextEnd {
content_index: usize,
},
ReasoningStart {
content_index: usize,
},
ReasoningDelta {
content_index: usize,
delta: String,
},
ReasoningEnd {
content_index: usize,
},
ToolCallStart {
content_index: usize,
id: ToolCallId,
name: String,
},
ToolCallDelta {
content_index: usize,
delta: String,
},
ToolCallEnd {
content_index: usize,
},
ToolResult {
id: ToolCallId,
name: String,
result: ToolResultValue,
},
ToolError {
id: ToolCallId,
name: String,
message: String,
},
Finish {
finish_reason: FinishReason,
#[serde(skip_serializing_if = "Option::is_none")]
usage: Option<Usage>,
},
ProviderError {
message: String,
retryable: bool,
},
}
impl StreamEvent {
pub fn is_terminal(&self) -> bool {
matches!(self, Self::Finish { .. } | Self::ProviderError { .. })
}
pub fn is_text_delta(&self) -> bool {
matches!(self, Self::TextDelta { .. })
}
pub fn is_reasoning_delta(&self) -> bool {
matches!(self, Self::ReasoningDelta { .. })
}
pub fn as_text_delta(&self) -> Option<&str> {
match self {
Self::TextDelta { delta, .. } => Some(delta.as_str()),
_ => None,
}
}
pub fn as_reasoning_delta(&self) -> Option<&str> {
match self {
Self::ReasoningDelta { delta, .. } => Some(delta.as_str()),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_stream_event_step_lifecycle_serde() {
let start = StreamEvent::StepStart { index: 0 };
let json = serde_json::to_string(&start).unwrap();
assert!(json.contains(r#""type":"step_start""#));
let restored: StreamEvent = serde_json::from_str(&json).unwrap();
assert_eq!(start, restored);
let finish = StreamEvent::StepFinish {
index: 0,
finish_reason: FinishReason::Stop,
usage: None,
};
let json = serde_json::to_string(&finish).unwrap();
assert!(json.contains(r#""type":"step_finish""#));
assert!(!json.contains("usage"));
let restored: StreamEvent = serde_json::from_str(&json).unwrap();
assert_eq!(finish, restored);
}
#[test]
fn test_stream_event_text_serde() {
let delta = StreamEvent::TextDelta {
content_index: 0,
delta: "Hello".into(),
};
let json = serde_json::to_string(&delta).unwrap();
assert!(json.contains(r#""type":"text_delta""#));
assert!(json.contains(r#""delta":"Hello""#));
let restored: StreamEvent = serde_json::from_str(&json).unwrap();
assert_eq!(delta, restored);
}
#[test]
fn test_stream_event_reasoning_serde() {
let delta = StreamEvent::ReasoningDelta {
content_index: 1,
delta: "thinking...".into(),
};
let json = serde_json::to_string(&delta).unwrap();
assert!(json.contains(r#""type":"reasoning_delta""#));
let restored: StreamEvent = serde_json::from_str(&json).unwrap();
assert_eq!(delta, restored);
}
#[test]
fn test_stream_event_tool_call_serde() {
let start = StreamEvent::ToolCallStart {
content_index: 2,
id: ToolCallId::new("call_abc"),
name: "read_file".into(),
};
let json = serde_json::to_string(&start).unwrap();
assert!(json.contains(r#""type":"tool_call_start""#));
let restored: StreamEvent = serde_json::from_str(&json).unwrap();
assert_eq!(start, restored);
}
#[test]
fn test_stream_event_tool_result_serde() {
let event = StreamEvent::ToolResult {
id: ToolCallId::new("call_1"),
name: "bash".into(),
result: ToolResultValue::Text {
value: "exit 0".into(),
},
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"tool_result""#));
let restored: StreamEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_stream_event_tool_error_serde() {
let event = StreamEvent::ToolError {
id: ToolCallId::new("call_2"),
name: "write_file".into(),
message: "permission denied".into(),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"tool_error""#));
let restored: StreamEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_stream_event_finish_with_usage() {
let event = StreamEvent::Finish {
finish_reason: FinishReason::ToolCalls,
usage: Some(Usage {
input_tokens: 100,
output_tokens: 50,
total_tokens: 150,
..Default::default()
}),
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"finish""#));
assert!(json.contains("usage"));
let restored: StreamEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_stream_event_provider_error_serde() {
let event = StreamEvent::ProviderError {
message: "rate limit exceeded".into(),
retryable: true,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains(r#""type":"provider_error""#));
let restored: StreamEvent = serde_json::from_str(&json).unwrap();
assert_eq!(event, restored);
}
#[test]
fn test_tool_result_value_json_serde() {
let v = ToolResultValue::Json {
value: serde_json::json!({"count": 42}),
};
let json = serde_json::to_string(&v).unwrap();
assert!(json.contains(r#""type":"json""#));
let restored: ToolResultValue = serde_json::from_str(&json).unwrap();
assert_eq!(v, restored);
}
#[test]
fn test_tool_result_value_text_serde() {
let v = ToolResultValue::Text {
value: "hello".into(),
};
let json = serde_json::to_string(&v).unwrap();
assert!(json.contains(r#""type":"text""#));
let restored: ToolResultValue = serde_json::from_str(&json).unwrap();
assert_eq!(v, restored);
}
#[test]
fn test_tool_result_value_error_serde() {
let v = ToolResultValue::Error {
value: "not found".into(),
};
let json = serde_json::to_string(&v).unwrap();
assert!(json.contains(r#""type":"error""#));
let restored: ToolResultValue = serde_json::from_str(&json).unwrap();
assert_eq!(v, restored);
}
#[test]
fn test_is_terminal() {
assert!(StreamEvent::Finish {
finish_reason: FinishReason::Stop,
usage: None,
}
.is_terminal());
assert!(StreamEvent::ProviderError {
message: "err".into(),
retryable: false,
}
.is_terminal());
assert!(!StreamEvent::TextDelta {
content_index: 0,
delta: "hi".into(),
}
.is_terminal());
}
#[test]
fn test_as_text_delta() {
let event = StreamEvent::TextDelta {
content_index: 0,
delta: "hello".into(),
};
assert_eq!(event.as_text_delta(), Some("hello"));
let other = StreamEvent::ReasoningDelta {
content_index: 0,
delta: "think".into(),
};
assert_eq!(other.as_text_delta(), None);
}
#[test]
fn test_as_reasoning_delta() {
let event = StreamEvent::ReasoningDelta {
content_index: 1,
delta: "hmm".into(),
};
assert_eq!(event.as_reasoning_delta(), Some("hmm"));
let other = StreamEvent::TextDelta {
content_index: 0,
delta: "hi".into(),
};
assert_eq!(other.as_reasoning_delta(), None);
}
}