use crate::context::{ToolConfirmationDecision, ToolConfirmationRequest};
use crate::model::LlmResponse;
use crate::types::Content;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use uuid::Uuid;
pub const KEY_PREFIX_APP: &str = "app:";
pub const KEY_PREFIX_TEMP: &str = "temp:";
pub const KEY_PREFIX_USER: &str = "user:";
pub const TOOL_PROGRESS_STREAM_KEY: &str = "adk.tool_progress.stream";
pub const TOOL_PROGRESS_CALL_ID_KEY: &str = "adk.tool_progress.call_id";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Event {
pub id: String,
pub timestamp: DateTime<Utc>,
pub invocation_id: String,
pub branch: String,
pub author: String,
#[serde(flatten)]
pub llm_response: LlmResponse,
pub actions: EventActions,
#[serde(default)]
pub long_running_tool_ids: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub llm_request: Option<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty", rename = "event_metadata")]
pub provider_metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EventCompaction {
pub start_timestamp: DateTime<Utc>,
pub end_timestamp: DateTime<Utc>,
pub compacted_content: Content,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct EventActions {
pub state_delta: HashMap<String, serde_json::Value>,
pub artifact_delta: HashMap<String, i64>,
pub skip_summarization: bool,
pub transfer_to_agent: Option<String>,
pub escalate: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_confirmation: Option<ToolConfirmationRequest>,
#[serde(skip_serializing_if = "Option::is_none")]
pub tool_confirmation_decision: Option<ToolConfirmationDecision>,
#[serde(skip_serializing_if = "Option::is_none")]
pub compaction: Option<EventCompaction>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub route: Option<Vec<String>>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ToolCallView<'a> {
pub call_id: Option<&'a str>,
pub name: &'a str,
pub args: &'a serde_json::Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ToolResultView<'a> {
pub call_id: Option<&'a str>,
pub name: &'a str,
pub response: &'a serde_json::Value,
}
impl Event {
pub fn new(invocation_id: impl Into<String>) -> Self {
Self {
id: Uuid::new_v4().to_string(),
timestamp: Utc::now(),
invocation_id: invocation_id.into(),
branch: String::new(),
author: String::new(),
llm_response: LlmResponse::default(),
actions: EventActions::default(),
long_running_tool_ids: Vec::new(),
llm_request: None,
provider_metadata: HashMap::new(),
}
}
pub fn with_id(id: impl Into<String>, invocation_id: impl Into<String>) -> Self {
Self {
id: id.into(),
timestamp: Utc::now(),
invocation_id: invocation_id.into(),
branch: String::new(),
author: String::new(),
llm_response: LlmResponse::default(),
actions: EventActions::default(),
long_running_tool_ids: Vec::new(),
llm_request: None,
provider_metadata: HashMap::new(),
}
}
pub fn tool_progress(
invocation_id: impl Into<String>,
author: impl Into<String>,
function_call_id: impl Into<String>,
stream: impl Into<String>,
chunk: impl Into<String>,
) -> Self {
let mut event = Event::new(invocation_id);
event.author = author.into();
event.llm_response.content = Some(Content {
role: "tool".to_string(),
parts: vec![crate::types::Part::Text { text: chunk.into() }],
});
event.llm_response.partial = true;
event.provider_metadata.insert(TOOL_PROGRESS_STREAM_KEY.to_string(), stream.into());
event
.provider_metadata
.insert(TOOL_PROGRESS_CALL_ID_KEY.to_string(), function_call_id.into());
event
}
pub fn tool_progress_stream(&self) -> Option<&str> {
self.provider_metadata.get(TOOL_PROGRESS_STREAM_KEY).map(String::as_str)
}
pub fn tool_calls(&self) -> Vec<ToolCallView<'_>> {
let Some(content) = &self.llm_response.content else {
return Vec::new();
};
content
.parts
.iter()
.filter_map(|part| match part {
crate::types::Part::FunctionCall { name, args, id, .. } => {
Some(ToolCallView { call_id: id.as_deref(), name, args })
}
_ => None,
})
.collect()
}
pub fn tool_results(&self) -> Vec<ToolResultView<'_>> {
let Some(content) = &self.llm_response.content else {
return Vec::new();
};
content
.parts
.iter()
.filter_map(|part| match part {
crate::types::Part::FunctionResponse { function_response, id, .. } => {
Some(ToolResultView {
call_id: id.as_deref(),
name: &function_response.name,
response: &function_response.response,
})
}
_ => None,
})
.collect()
}
pub fn content(&self) -> Option<&Content> {
self.llm_response.content.as_ref()
}
pub fn set_content(&mut self, content: Content) {
self.llm_response.content = Some(content);
}
pub fn interaction_id(&self) -> Option<&str> {
self.llm_response.interaction_id.as_deref()
}
pub fn is_final_response(&self) -> bool {
if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
return true;
}
let has_function_calls = self.has_function_calls();
let has_function_responses = self.has_function_responses();
let is_partial = self.llm_response.partial;
let has_trailing_code_result = self.has_trailing_code_execution_result();
!has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
}
fn has_function_calls(&self) -> bool {
if let Some(content) = &self.llm_response.content {
for part in &content.parts {
if matches!(part, crate::Part::FunctionCall { .. }) {
return true;
}
}
}
false
}
fn has_function_responses(&self) -> bool {
if let Some(content) = &self.llm_response.content {
for part in &content.parts {
if matches!(part, crate::Part::FunctionResponse { .. }) {
return true;
}
}
}
false
}
#[allow(clippy::match_like_matches_macro)]
fn has_trailing_code_execution_result(&self) -> bool {
if let Some(content) = &self.llm_response.content
&& let Some(last_part) = content.parts.last()
{
return matches!(last_part, crate::Part::FunctionResponse { .. });
}
false
}
pub fn function_call_ids(&self) -> Vec<String> {
let mut ids = Vec::new();
if let Some(content) = &self.llm_response.content {
for part in &content.parts {
if let crate::Part::FunctionCall { name, id, .. } = part {
ids.push(id.as_deref().unwrap_or(name).to_string());
}
}
}
ids
}
}
pub fn event_belongs_to_branch(invocation_branch: &str, event_branch: &str) -> bool {
if invocation_branch.is_empty() || event_branch.is_empty() {
return true;
}
if event_branch == invocation_branch {
return true;
}
invocation_branch.starts_with(event_branch)
&& invocation_branch.as_bytes().get(event_branch.len()) == Some(&b'.')
}
#[cfg(test)]
mod branch_visibility_tests {
use super::event_belongs_to_branch;
#[test]
fn own_branch_is_visible() {
assert!(event_belongs_to_branch("root.parallel.a", "root.parallel.a"));
}
#[test]
fn ancestor_branches_are_visible() {
assert!(event_belongs_to_branch("root.parallel.a", "root"));
assert!(event_belongs_to_branch("root.parallel.a", "root.parallel"));
}
#[test]
fn sibling_branches_are_hidden() {
assert!(!event_belongs_to_branch("root.parallel.a", "root.parallel.b"));
assert!(!event_belongs_to_branch("root.parallel.b", "root.parallel.a"));
}
#[test]
fn descendant_branches_are_hidden() {
assert!(!event_belongs_to_branch("root", "root.parallel.a"));
}
#[test]
fn empty_branch_on_either_side_matches() {
assert!(event_belongs_to_branch("", "root.parallel.a"));
assert!(event_belongs_to_branch("root.parallel.a", ""));
assert!(event_belongs_to_branch("", ""));
}
#[test]
fn prefix_match_requires_the_delimiter() {
assert!(!event_belongs_to_branch("root.agent_00", "root.agent_0"));
assert!(event_belongs_to_branch("root.agent_0.child", "root.agent_0"));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Part;
#[test]
fn test_event_creation() {
let event = Event::new("inv-123");
assert_eq!(event.invocation_id, "inv-123");
assert!(!event.id.is_empty());
}
#[test]
fn test_event_actions_default() {
let actions = EventActions::default();
assert!(actions.state_delta.is_empty());
assert!(!actions.skip_summarization);
assert!(actions.tool_confirmation.is_none());
assert!(actions.tool_confirmation_decision.is_none());
}
#[test]
fn test_state_prefixes() {
assert_eq!(KEY_PREFIX_APP, "app:");
assert_eq!(KEY_PREFIX_TEMP, "temp:");
assert_eq!(KEY_PREFIX_USER, "user:");
}
#[test]
fn test_is_final_response_no_content() {
let event = Event::new("inv-123");
assert!(event.is_final_response());
}
#[test]
fn test_is_final_response_text_only() {
let mut event = Event::new("inv-123");
event.llm_response.content = Some(Content {
role: "model".to_string(),
parts: vec![Part::Text { text: "Hello!".to_string() }],
});
assert!(event.is_final_response());
}
#[test]
fn test_is_final_response_with_function_call() {
let mut event = Event::new("inv-123");
event.llm_response.content = Some(Content {
role: "model".to_string(),
parts: vec![Part::FunctionCall {
name: "get_weather".to_string(),
args: serde_json::json!({"city": "NYC"}),
id: Some("call_123".to_string()),
thought_signature: None,
}],
});
assert!(!event.is_final_response());
}
#[test]
fn test_is_final_response_with_function_response() {
let mut event = Event::new("inv-123");
event.llm_response.content = Some(Content {
role: "function".to_string(),
parts: vec![Part::FunctionResponse {
function_response: crate::FunctionResponseData::new(
"get_weather",
serde_json::json!({"temp": 72}),
),
id: Some("call_123".to_string()),
annotations: None,
}],
});
assert!(!event.is_final_response());
}
#[test]
fn test_is_final_response_partial() {
let mut event = Event::new("inv-123");
event.llm_response.partial = true;
event.llm_response.content = Some(Content {
role: "model".to_string(),
parts: vec![Part::Text { text: "Hello...".to_string() }],
});
assert!(!event.is_final_response());
}
#[test]
fn test_is_final_response_skip_summarization() {
let mut event = Event::new("inv-123");
event.actions.skip_summarization = true;
event.llm_response.content = Some(Content {
role: "function".to_string(),
parts: vec![Part::FunctionResponse {
function_response: crate::FunctionResponseData::new(
"tool",
serde_json::json!({"result": "done"}),
),
id: Some("call_tool".to_string()),
annotations: None,
}],
});
assert!(event.is_final_response());
}
#[test]
fn test_is_final_response_long_running_tool_ids() {
let mut event = Event::new("inv-123");
event.long_running_tool_ids = vec!["process_video".to_string()];
event.llm_response.content = Some(Content {
role: "model".to_string(),
parts: vec![Part::FunctionCall {
name: "process_video".to_string(),
args: serde_json::json!({"file": "video.mp4"}),
id: Some("call_process".to_string()),
thought_signature: None,
}],
});
assert!(event.is_final_response());
}
#[test]
fn test_function_call_ids() {
let mut event = Event::new("inv-123");
event.llm_response.content = Some(Content {
role: "model".to_string(),
parts: vec![
Part::FunctionCall {
name: "get_weather".to_string(),
args: serde_json::json!({}),
id: Some("call_1".to_string()),
thought_signature: None,
},
Part::Text { text: "I'll check the weather".to_string() },
Part::FunctionCall {
name: "get_time".to_string(),
args: serde_json::json!({}),
id: Some("call_2".to_string()),
thought_signature: None,
},
],
});
let ids = event.function_call_ids();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&"call_1".to_string()));
assert!(ids.contains(&"call_2".to_string()));
}
#[test]
fn test_function_call_ids_falls_back_to_name() {
let mut event = Event::new("inv-123");
event.llm_response.content = Some(Content {
role: "model".to_string(),
parts: vec![Part::FunctionCall {
name: "get_weather".to_string(),
args: serde_json::json!({}),
id: None, thought_signature: None,
}],
});
let ids = event.function_call_ids();
assert_eq!(ids, vec!["get_weather".to_string()]);
}
#[test]
fn test_function_call_ids_empty() {
let event = Event::new("inv-123");
let ids = event.function_call_ids();
assert!(ids.is_empty());
}
#[test]
fn test_is_final_response_trailing_function_response() {
let mut event = Event::new("inv-123");
event.llm_response.content = Some(Content {
role: "model".to_string(),
parts: vec![
Part::Text { text: "Running code...".to_string() },
Part::FunctionResponse {
function_response: crate::FunctionResponseData::new(
"code_exec",
serde_json::json!({"output": "42"}),
),
id: Some("call_exec".to_string()),
annotations: None,
},
],
});
assert!(!event.is_final_response());
}
#[test]
fn test_event_roundtrip_with_both_provider_metadata() {
let mut event = Event::new("inv-1");
event.provider_metadata.insert("adk.tool_progress.stream".into(), "stdout".into());
event.provider_metadata.insert("adk.tool_progress.call_id".into(), "call-7".into());
event.llm_response.provider_metadata = Some(serde_json::json!({"response_id": "resp-xyz"}));
let json = serde_json::to_string(&event).expect("serialize");
let back: Event = serde_json::from_str(&json)
.expect("round-trip must succeed without duplicate field error");
assert_eq!(
back.provider_metadata.get("adk.tool_progress.stream").map(String::as_str),
Some("stdout"),
);
assert_eq!(
back.provider_metadata.get("adk.tool_progress.call_id").map(String::as_str),
Some("call-7"),
);
assert_eq!(
back.llm_response.provider_metadata,
Some(serde_json::json!({"response_id": "resp-xyz"})),
);
}
#[test]
fn test_is_final_response_text_after_function_response() {
let mut event = Event::new("inv-123");
event.llm_response.content = Some(Content {
role: "model".to_string(),
parts: vec![
Part::FunctionResponse {
function_response: crate::FunctionResponseData::new(
"tool",
serde_json::json!({}),
),
id: Some("call_1".to_string()),
annotations: None,
},
Part::Text { text: "Done".to_string() },
],
});
assert!(!event.is_final_response());
}
}