1use ai_agents_core::AgentResponse;
2use serde::{Deserialize, Serialize};
3
4#[non_exhaustive]
6#[derive(Debug, Clone)]
7pub enum AgentStreamEvent {
8 Chunk(StreamChunk),
10 Final(AgentResponse),
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "snake_case")]
17pub enum StreamChunk {
18 Content { text: String },
20 ToolCallStart { id: String, name: String },
22 ToolCallDelta { id: String, arguments: String },
24 ToolCallEnd { id: String },
26 ToolResult {
28 id: String,
29 name: String,
30 output: String,
31 success: bool,
32 },
33 StateTransition { from: Option<String>, to: String },
35 Done {},
37 Error { message: String },
39}
40
41impl StreamChunk {
42 pub fn content(text: impl Into<String>) -> Self {
43 StreamChunk::Content { text: text.into() }
44 }
45
46 pub fn tool_start(id: impl Into<String>, name: impl Into<String>) -> Self {
47 StreamChunk::ToolCallStart {
48 id: id.into(),
49 name: name.into(),
50 }
51 }
52
53 pub fn tool_delta(id: impl Into<String>, arguments: impl Into<String>) -> Self {
54 StreamChunk::ToolCallDelta {
55 id: id.into(),
56 arguments: arguments.into(),
57 }
58 }
59
60 pub fn tool_end(id: impl Into<String>) -> Self {
61 StreamChunk::ToolCallEnd { id: id.into() }
62 }
63
64 pub fn tool_result(
65 id: impl Into<String>,
66 name: impl Into<String>,
67 output: impl Into<String>,
68 success: bool,
69 ) -> Self {
70 StreamChunk::ToolResult {
71 id: id.into(),
72 name: name.into(),
73 output: output.into(),
74 success,
75 }
76 }
77
78 pub fn state_transition(from: Option<String>, to: impl Into<String>) -> Self {
79 StreamChunk::StateTransition {
80 from,
81 to: to.into(),
82 }
83 }
84
85 pub fn error(message: impl Into<String>) -> Self {
86 StreamChunk::Error {
87 message: message.into(),
88 }
89 }
90
91 pub fn is_done(&self) -> bool {
92 matches!(self, StreamChunk::Done {})
93 }
94
95 pub fn is_error(&self) -> bool {
96 matches!(self, StreamChunk::Error { .. })
97 }
98
99 pub fn is_content(&self) -> bool {
100 matches!(self, StreamChunk::Content { .. })
101 }
102}
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
106pub struct StreamingConfig {
107 #[serde(default = "default_true")]
109 pub enabled: bool,
110 #[serde(default = "default_buffer_size")]
112 pub buffer_size: usize,
113 #[serde(default = "default_true")]
115 pub include_tool_events: bool,
116 #[serde(default = "default_true")]
118 pub include_state_events: bool,
119}
120
121fn default_true() -> bool {
122 true
123}
124
125fn default_buffer_size() -> usize {
126 256
127}
128
129impl Default for StreamingConfig {
130 fn default() -> Self {
131 Self {
132 enabled: true,
133 buffer_size: default_buffer_size(),
134 include_tool_events: true,
135 include_state_events: true,
136 }
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_agent_stream_event_carries_chunks_and_final_response() {
146 let chunk = AgentStreamEvent::Chunk(StreamChunk::content("Hello"));
147 assert!(matches!(
148 chunk,
149 AgentStreamEvent::Chunk(StreamChunk::Content { .. })
150 ));
151
152 let final_response = AgentStreamEvent::Final(AgentResponse::new("Hello"));
153 assert!(
154 matches!(final_response, AgentStreamEvent::Final(response) if response.content == "Hello")
155 );
156 }
157
158 #[test]
159 fn test_stream_chunk_constructors() {
160 let content = StreamChunk::content("Hello");
161 assert!(content.is_content());
162
163 let tool_start = StreamChunk::tool_start("id1", "calculator");
164 assert!(matches!(tool_start, StreamChunk::ToolCallStart { .. }));
165
166 let tool_delta = StreamChunk::tool_delta("id1", r#"{"expr":"1+1"}"#);
167 assert!(matches!(tool_delta, StreamChunk::ToolCallDelta { .. }));
168
169 let tool_end = StreamChunk::tool_end("id1");
170 assert!(matches!(tool_end, StreamChunk::ToolCallEnd { .. }));
171
172 let done = StreamChunk::Done {};
173 assert!(done.is_done());
174
175 let error = StreamChunk::error("Something went wrong");
176 assert!(error.is_error());
177 }
178
179 #[test]
180 fn test_stream_chunk_serialization() {
181 let content = StreamChunk::content("Hello");
182 let json = serde_json::to_string(&content).unwrap();
183 assert!(json.contains("content"));
184 assert!(json.contains("Hello"));
185
186 let tool_start = StreamChunk::tool_start("id1", "calculator");
187 let json = serde_json::to_string(&tool_start).unwrap();
188 assert!(json.contains("tool_call_start"));
189 assert!(json.contains("calculator"));
190
191 assert_eq!(
192 serde_json::to_string(&StreamChunk::Done {}).unwrap(),
193 r#"{"type":"done"}"#
194 );
195 }
196
197 #[test]
198 fn test_streaming_config_defaults() {
199 let config = StreamingConfig::default();
200 assert!(config.enabled);
201 assert_eq!(config.buffer_size, 256);
202 assert!(config.include_tool_events);
203 assert!(config.include_state_events);
204 }
205
206 #[test]
207 fn test_streaming_config_deserialization() {
208 let yaml = r#"
209enabled: true
210buffer_size: 64
211include_tool_events: false
212"#;
213 let config: StreamingConfig = serde_yaml::from_str(yaml).unwrap();
214 assert!(config.enabled);
215 assert_eq!(config.buffer_size, 64);
216 assert!(!config.include_tool_events);
217 assert!(config.include_state_events);
218 }
219
220 #[test]
221 fn test_tool_result_chunk() {
222 let result = StreamChunk::tool_result("id1", "calculator", "42", true);
223 match result {
224 StreamChunk::ToolResult {
225 id,
226 name,
227 output,
228 success,
229 } => {
230 assert_eq!(id, "id1");
231 assert_eq!(name, "calculator");
232 assert_eq!(output, "42");
233 assert!(success);
234 }
235 _ => panic!("Expected ToolResult"),
236 }
237 }
238
239 #[test]
240 fn test_state_transition_chunk() {
241 let transition = StreamChunk::state_transition(Some("greeting".to_string()), "support");
242 match transition {
243 StreamChunk::StateTransition { from, to } => {
244 assert_eq!(from, Some("greeting".to_string()));
245 assert_eq!(to, "support");
246 }
247 _ => panic!("Expected StateTransition"),
248 }
249 }
250
251 #[test]
252 fn test_stream_chunk_done_serialization() {
253 let done = StreamChunk::Done {};
254 let json = serde_json::to_string(&done).unwrap();
255 assert!(json.contains("done"));
256 }
257
258 #[test]
259 fn test_stream_chunk_error_serialization() {
260 let error = StreamChunk::error("Test error");
261 let json = serde_json::to_string(&error).unwrap();
262 assert!(json.contains("error"));
263 assert!(json.contains("Test error"));
264 }
265
266 #[test]
267 fn test_stream_chunk_tool_result_serialization() {
268 let result = StreamChunk::tool_result("id1", "calculator", "42", true);
269 let json = serde_json::to_string(&result).unwrap();
270 assert!(json.contains("tool_result"));
271 assert!(json.contains("calculator"));
272 assert!(json.contains("42"));
273 assert!(json.contains("true"));
274 }
275
276 #[test]
277 fn test_streaming_config_full_yaml() {
278 let yaml = r#"
279enabled: false
280buffer_size: 128
281include_tool_events: false
282include_state_events: false
283"#;
284 let config: StreamingConfig = serde_yaml::from_str(yaml).unwrap();
285 assert!(!config.enabled);
286 assert_eq!(config.buffer_size, 128);
287 assert!(!config.include_tool_events);
288 assert!(!config.include_state_events);
289 }
290
291 #[test]
292 fn test_stream_chunk_deserialization() {
293 let json = r#"{"type":"content","text":"Hello"}"#;
294 let chunk: StreamChunk = serde_json::from_str(json).unwrap();
295 assert!(chunk.is_content());
296
297 let json = r#"{"type":"done"}"#;
298 let chunk: StreamChunk = serde_json::from_str(json).unwrap();
299 assert!(chunk.is_done());
300
301 let json = r#"{"type":"error","message":"fail"}"#;
302 let chunk: StreamChunk = serde_json::from_str(json).unwrap();
303 assert!(chunk.is_error());
304 }
305
306 #[test]
307 fn test_stream_chunk_tool_events() {
308 let start = StreamChunk::tool_start("tool-1", "http");
309 let delta = StreamChunk::tool_delta("tool-1", r#"{"url":"test"}"#);
310 let end = StreamChunk::tool_end("tool-1");
311
312 match start {
313 StreamChunk::ToolCallStart { id, name } => {
314 assert_eq!(id, "tool-1");
315 assert_eq!(name, "http");
316 }
317 _ => panic!("Expected ToolCallStart"),
318 }
319
320 match delta {
321 StreamChunk::ToolCallDelta { id, arguments } => {
322 assert_eq!(id, "tool-1");
323 assert!(arguments.contains("url"));
324 }
325 _ => panic!("Expected ToolCallDelta"),
326 }
327
328 match end {
329 StreamChunk::ToolCallEnd { id } => {
330 assert_eq!(id, "tool-1");
331 }
332 _ => panic!("Expected ToolCallEnd"),
333 }
334 }
335}