use adk_core::{Content, Event, Part};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum SessionNotification {
AgentMessageChunk {
text: String,
},
ToolCall {
name: String,
args: serde_json::Value,
},
AgentThoughtChunk {
text: String,
},
Complete,
Error {
error_code: String,
message: String,
},
}
pub struct ResponseStreamer;
impl ResponseStreamer {
pub fn map_event(event: &Event) -> Vec<SessionNotification> {
let mut notifications = Vec::new();
if let Some(content) = event.content() {
Self::map_content(content, &mut notifications);
}
notifications
}
fn map_content(content: &Content, notifications: &mut Vec<SessionNotification>) {
for part in &content.parts {
match part {
Part::Text { text } => {
if !text.is_empty() {
notifications
.push(SessionNotification::AgentMessageChunk { text: text.clone() });
}
}
Part::FunctionCall { name, args, .. } => {
notifications.push(SessionNotification::ToolCall {
name: name.clone(),
args: args.clone(),
});
}
Part::Thinking { thinking, .. } => {
if !thinking.is_empty() {
notifications.push(SessionNotification::AgentThoughtChunk {
text: thinking.clone(),
});
}
}
_ => {}
}
}
}
pub fn make_completion() -> SessionNotification {
SessionNotification::Complete
}
pub fn make_error(error_code: &str, message: &str) -> SessionNotification {
SessionNotification::Error {
error_code: error_code.to_string(),
message: message.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use adk_core::{Content, Event};
#[test]
fn test_map_text_part() {
let mut event = Event::new("inv-1");
event.set_content(Content::new("model").with_text("Hello world"));
let notifications = ResponseStreamer::map_event(&event);
assert_eq!(notifications.len(), 1);
match ¬ifications[0] {
SessionNotification::AgentMessageChunk { text } => {
assert_eq!(text, "Hello world");
}
other => panic!("expected AgentMessageChunk, got {other:?}"),
}
}
#[test]
fn test_map_function_call_part() {
let mut event = Event::new("inv-1");
let mut content = Content::new("model");
content.parts.push(Part::FunctionCall {
name: "read_file".to_string(),
args: serde_json::json!({"path": "/tmp/test.rs"}),
id: None,
thought_signature: None,
});
event.set_content(content);
let notifications = ResponseStreamer::map_event(&event);
assert_eq!(notifications.len(), 1);
match ¬ifications[0] {
SessionNotification::ToolCall { name, args } => {
assert_eq!(name, "read_file");
assert_eq!(args, &serde_json::json!({"path": "/tmp/test.rs"}));
}
other => panic!("expected ToolCall, got {other:?}"),
}
}
#[test]
fn test_map_thinking_part() {
let mut event = Event::new("inv-1");
let mut content = Content::new("model");
content.parts.push(Part::Thinking {
thinking: "Let me think about this...".to_string(),
signature: None,
});
event.set_content(content);
let notifications = ResponseStreamer::map_event(&event);
assert_eq!(notifications.len(), 1);
match ¬ifications[0] {
SessionNotification::AgentThoughtChunk { text } => {
assert_eq!(text, "Let me think about this...");
}
other => panic!("expected AgentThoughtChunk, got {other:?}"),
}
}
#[test]
fn test_map_empty_event() {
let event = Event::new("inv-1");
let notifications = ResponseStreamer::map_event(&event);
assert!(notifications.is_empty());
}
#[test]
fn test_map_multiple_parts_preserves_order() {
let mut event = Event::new("inv-1");
let mut content = Content::new("model");
content
.parts
.push(Part::Thinking { thinking: "thinking first".to_string(), signature: None });
content.parts.push(Part::Text { text: "then text".to_string() });
content.parts.push(Part::FunctionCall {
name: "tool".to_string(),
args: serde_json::json!({}),
id: None,
thought_signature: None,
});
event.set_content(content);
let notifications = ResponseStreamer::map_event(&event);
assert_eq!(notifications.len(), 3);
assert!(matches!(¬ifications[0], SessionNotification::AgentThoughtChunk { .. }));
assert!(matches!(¬ifications[1], SessionNotification::AgentMessageChunk { .. }));
assert!(matches!(¬ifications[2], SessionNotification::ToolCall { .. }));
}
#[test]
fn test_make_completion() {
let notif = ResponseStreamer::make_completion();
assert!(matches!(notif, SessionNotification::Complete));
}
#[test]
fn test_make_error() {
let notif = ResponseStreamer::make_error("execution_error", "something failed");
match notif {
SessionNotification::Error { error_code, message } => {
assert_eq!(error_code, "execution_error");
assert_eq!(message, "something failed");
}
other => panic!("expected Error, got {other:?}"),
}
}
}