use indexmap::IndexMap;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::genai_types::{Content, FunctionCall, FunctionResponse, Part};
use crate::core::llm_response::LlmResponse;
use crate::core::state::StateDelta;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct EventActions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub skip_summarization: Option<bool>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub state_delta: StateDelta,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub artifact_delta: IndexMap<String, u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub transfer_to_agent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub escalate: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub end_of_agent: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub compaction: Option<EventCompaction>,
#[serde(default, skip_serializing_if = "IndexMap::is_empty")]
pub requested_tool_confirmations:
IndexMap<String, crate::core::tool_confirmation::ToolConfirmation>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agent_state: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rewind_before_invocation_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EventCompaction {
pub start_timestamp: f64,
pub end_timestamp: f64,
pub compacted_content: Content,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Event {
#[serde(default)]
pub id: String,
#[serde(default)]
pub invocation_id: String,
pub author: String,
#[serde(default)]
pub timestamp: f64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub branch: Option<String>,
#[serde(flatten)]
pub response: LlmResponse,
#[serde(default, skip_serializing_if = "is_default_event_actions")]
pub actions: EventActions,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub long_running_tool_ids: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub partial: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub turn_complete: Option<bool>,
}
fn is_default_event_actions(a: &EventActions) -> bool {
*a == EventActions::default()
}
impl Event {
#[must_use]
pub fn new_id() -> String {
Uuid::new_v4().to_string()
}
pub fn new(author: impl Into<String>, response: LlmResponse) -> Self {
Self {
id: Self::new_id(),
invocation_id: String::new(),
author: author.into(),
timestamp: crate::core::session::now_secs(),
branch: None,
response,
actions: EventActions::default(),
long_running_tool_ids: None,
partial: None,
turn_complete: None,
}
}
pub fn user_text(text: impl Into<String>) -> Self {
Self::new(
"user",
LlmResponse {
content: Some(Content::user_text(text)),
..LlmResponse::default()
},
)
}
pub fn model_text(author: impl Into<String>, text: impl Into<String>) -> Self {
Self::new(
author,
LlmResponse {
content: Some(Content::model_text(text)),
..LlmResponse::default()
},
)
}
#[must_use]
pub fn function_calls(&self) -> Vec<FunctionCall> {
self.response
.content
.as_ref()
.map(|c| {
c.parts
.iter()
.filter_map(|p| p.as_function_call().cloned())
.collect()
})
.unwrap_or_default()
}
#[must_use]
pub fn function_responses(&self) -> Vec<FunctionResponse> {
self.response
.content
.as_ref()
.map(|c| {
c.parts
.iter()
.filter_map(|p| p.as_function_response().cloned())
.collect()
})
.unwrap_or_default()
}
#[must_use]
pub fn is_final_response(&self) -> bool {
if self.actions.skip_summarization == Some(true)
|| self
.long_running_tool_ids
.as_ref()
.is_some_and(|ids| !ids.is_empty())
{
return true;
}
let has_calls = !self.function_calls().is_empty();
let has_responses = !self.function_responses().is_empty();
!has_calls
&& !has_responses
&& self.partial != Some(true)
&& !self.has_trailing_code_result()
}
#[must_use]
pub fn has_trailing_code_result(&self) -> bool {
self.response
.content
.as_ref()
.and_then(|c| c.parts.last())
.is_some_and(|p| matches!(p, Part::CodeExecutionResult(_)))
}
}
#[must_use]
pub fn history_with_compaction(events: &[Event]) -> Vec<Content> {
let compactions: Vec<(usize, &EventCompaction)> = events
.iter()
.enumerate()
.filter_map(|(i, e)| e.actions.compaction.as_ref().map(|c| (i, c)))
.collect();
if compactions.is_empty() {
return events
.iter()
.filter_map(|e| e.response.content.clone())
.collect();
}
let mut emitted = vec![false; compactions.len()];
let mut out = Vec::new();
for (idx, e) in events.iter().enumerate() {
if e.actions.compaction.is_some() {
continue;
}
#[allow(clippy::suspicious_operation_groupings)]
let cover = compactions.iter().enumerate().rev().find(|(_, (ci, c))| {
idx < *ci && e.timestamp >= c.start_timestamp && e.timestamp <= c.end_timestamp
});
match cover {
Some((slot, (_, c))) => {
if !emitted[slot] {
emitted[slot] = true;
out.push(c.compacted_content.clone());
}
}
None => {
if let Some(c) = e.response.content.clone() {
out.push(c);
}
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::genai_types::FunctionCall;
use serde_json::json;
#[test]
fn user_text_is_final() {
let e = Event::user_text("hi");
assert!(e.is_final_response());
}
#[test]
fn event_with_function_call_is_not_final() {
let resp = LlmResponse {
content: Some(Content {
role: crate::genai_types::Role::Model,
parts: vec![Part::FunctionCall(FunctionCall::new("f", json!({})))],
}),
..LlmResponse::default()
};
let e = Event::new("agent", resp);
assert!(!e.is_final_response());
assert_eq!(e.function_calls().len(), 1);
}
#[test]
fn history_with_compaction_replaces_covered_events() {
let mut events = Vec::new();
for (i, text) in ["one", "two", "three"].iter().enumerate() {
let mut e = Event::model_text("a", *text);
e.timestamp = i as f64 + 1.0;
events.push(e);
}
let mut comp = Event::new("a", LlmResponse::default());
comp.timestamp = 3.5;
comp.actions.compaction = Some(EventCompaction {
start_timestamp: 1.0,
end_timestamp: 2.0,
compacted_content: Content::user_text("[summary of one+two]"),
});
events.push(comp);
let mut after = Event::model_text("a", "four");
after.timestamp = 4.0;
events.push(after);
let history = history_with_compaction(&events);
let texts: Vec<String> = history.iter().map(|c| c.text_concat()).collect();
assert_eq!(texts, vec!["[summary of one+two]", "three", "four"]);
}
#[test]
fn overlapping_compactions_prefer_newest() {
let mut events = Vec::new();
for i in 0..4 {
let mut e = Event::model_text("a", format!("m{i}"));
e.timestamp = i as f64 + 1.0;
events.push(e);
}
let mut c1 = Event::new("a", LlmResponse::default());
c1.timestamp = 4.2;
c1.actions.compaction = Some(EventCompaction {
start_timestamp: 1.0,
end_timestamp: 2.0,
compacted_content: Content::user_text("[old summary]"),
});
events.push(c1);
let mut c2 = Event::new("a", LlmResponse::default());
c2.timestamp = 4.5;
c2.actions.compaction = Some(EventCompaction {
start_timestamp: 2.0,
end_timestamp: 4.0,
compacted_content: Content::user_text("[new summary]"),
});
events.push(c2);
let texts: Vec<String> = history_with_compaction(&events)
.iter()
.map(|c| c.text_concat())
.collect();
assert_eq!(texts, vec!["[old summary]", "[new summary]"]);
}
#[test]
fn event_round_trips() {
let e = Event::model_text("agent", "hello");
let j = serde_json::to_value(&e).unwrap();
let back: Event = serde_json::from_value(j).unwrap();
assert_eq!(e.id, back.id);
assert_eq!(e.author, back.author);
}
}