adk_core/event.rs
1use crate::context::{ToolConfirmationDecision, ToolConfirmationRequest};
2use crate::model::LlmResponse;
3use crate::types::Content;
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use uuid::Uuid;
8
9// State scope prefixes
10/// Key prefix for application-scoped state (persists across sessions).
11pub const KEY_PREFIX_APP: &str = "app:";
12/// Key prefix for temporary state (cleared each turn).
13pub const KEY_PREFIX_TEMP: &str = "temp:";
14/// Key prefix for user-scoped state (persists across sessions).
15pub const KEY_PREFIX_USER: &str = "user:";
16
17/// Event-level `provider_metadata` key marking a tool-progress event and naming
18/// its output stream (e.g. `"stdout"`, `"stderr"`). Present only on events
19/// produced by [`ToolContext::emit_progress`](crate::ToolContext::emit_progress).
20pub const TOOL_PROGRESS_STREAM_KEY: &str = "adk.tool_progress.stream";
21
22/// Event-level `provider_metadata` key carrying the originating tool's
23/// function-call id on a tool-progress event.
24pub const TOOL_PROGRESS_CALL_ID_KEY: &str = "adk.tool_progress.call_id";
25
26/// Event represents a single interaction in a conversation.
27/// This struct embeds LlmResponse to match ADK-Go's design pattern.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Event {
30 /// Unique identifier for this event.
31 pub id: String,
32 /// When this event was created.
33 pub timestamp: DateTime<Utc>,
34 /// The invocation that produced this event.
35 pub invocation_id: String,
36 /// The conversation branch this event belongs to.
37 pub branch: String,
38 /// The agent or role that authored this event.
39 pub author: String,
40 /// The LLM response containing content and metadata.
41 /// Access content via `event.llm_response.content`.
42 #[serde(flatten)]
43 pub llm_response: LlmResponse,
44 /// Actions to apply (state changes, transfers, confirmations).
45 pub actions: EventActions,
46 /// IDs of long-running tools associated with this event.
47 #[serde(default)]
48 pub long_running_tool_ids: Vec<String>,
49 /// LLM request data for UI display (JSON string)
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub llm_request: Option<String>,
52 /// Provider-specific metadata (e.g., GCP Vertex, Azure OpenAI).
53 /// Keeps the core Event struct provider-agnostic.
54 /// Serialized as `"event_metadata"` to avoid collision with
55 /// [`LlmResponse::provider_metadata`](crate::LlmResponse) when flattened.
56 #[serde(default, skip_serializing_if = "HashMap::is_empty", rename = "event_metadata")]
57 pub provider_metadata: HashMap<String, String>,
58}
59
60/// Metadata for a compacted (summarized) event.
61/// When context compaction is enabled, older events are summarized into a single
62/// compacted event containing this metadata.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct EventCompaction {
65 /// Timestamp of the earliest event that was compacted.
66 pub start_timestamp: DateTime<Utc>,
67 /// Timestamp of the latest event that was compacted.
68 pub end_timestamp: DateTime<Utc>,
69 /// The summarized content replacing the original events.
70 pub compacted_content: Content,
71}
72
73/// Actions to apply as side effects of an event.
74#[derive(Debug, Clone, Default, Serialize, Deserialize)]
75pub struct EventActions {
76 /// State key-value changes to apply.
77 pub state_delta: HashMap<String, serde_json::Value>,
78 /// Artifact version changes.
79 pub artifact_delta: HashMap<String, i64>,
80 /// Whether to skip summarization for this event.
81 pub skip_summarization: bool,
82 /// Agent name to transfer control to.
83 pub transfer_to_agent: Option<String>,
84 /// Whether to escalate to a human operator.
85 pub escalate: bool,
86 /// Tool confirmation request awaiting human approval.
87 #[serde(skip_serializing_if = "Option::is_none")]
88 pub tool_confirmation: Option<ToolConfirmationRequest>,
89 /// Decision for a pending tool confirmation.
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub tool_confirmation_decision: Option<ToolConfirmationDecision>,
92 /// Present when this event is a compaction summary replacing older events.
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub compaction: Option<EventCompaction>,
95 /// Target node names for dynamic route dispatch in graph workflows.
96 /// When non-empty, the graph executor routes to these nodes instead of
97 /// following static edges.
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub route: Option<Vec<String>>,
100}
101
102/// A typed, borrowed view of a single tool call carried by an [`Event`].
103///
104/// Produced by [`Event::tool_calls`]. Lets UI/event consumers render the tool a
105/// model requested without matching on [`Part::FunctionCall`](crate::Part::FunctionCall)
106/// internals.
107#[derive(Debug, Clone, Copy, PartialEq, Eq)]
108pub struct ToolCallView<'a> {
109 /// Provider-assigned call id (OpenAI-style). `None` for providers that omit
110 /// it (e.g. Gemini); fall back to [`name`](Self::name) for correlation.
111 pub call_id: Option<&'a str>,
112 /// The tool/function name the model requested.
113 pub name: &'a str,
114 /// The call arguments as raw JSON.
115 pub args: &'a serde_json::Value,
116}
117
118/// A typed, borrowed view of a single tool result carried by an [`Event`].
119///
120/// Produced by [`Event::tool_results`]. Surfaces a completed tool's output
121/// generically so any tool — streaming or not — can be rendered from the event
122/// stream without walking [`Part::FunctionResponse`](crate::Part::FunctionResponse)
123/// internals.
124#[derive(Debug, Clone, Copy, PartialEq, Eq)]
125pub struct ToolResultView<'a> {
126 /// Provider-assigned call id (OpenAI-style), correlating this result with
127 /// its originating [`ToolCallView`] and progress chunks. `None` for
128 /// providers that omit it (e.g. Gemini); fall back to [`name`](Self::name).
129 pub call_id: Option<&'a str>,
130 /// The tool/function name that produced this result.
131 pub name: &'a str,
132 /// The tool's JSON response payload.
133 pub response: &'a serde_json::Value,
134}
135
136impl Event {
137 /// Creates a new event with a generated UUID and current timestamp.
138 pub fn new(invocation_id: impl Into<String>) -> Self {
139 Self {
140 id: Uuid::new_v4().to_string(),
141 timestamp: Utc::now(),
142 invocation_id: invocation_id.into(),
143 branch: String::new(),
144 author: String::new(),
145 llm_response: LlmResponse::default(),
146 actions: EventActions::default(),
147 long_running_tool_ids: Vec::new(),
148 llm_request: None,
149 provider_metadata: HashMap::new(),
150 }
151 }
152
153 /// Create an event with a specific ID.
154 /// Use this for streaming events where all chunks should share the same event ID.
155 pub fn with_id(id: impl Into<String>, invocation_id: impl Into<String>) -> Self {
156 Self {
157 id: id.into(),
158 timestamp: Utc::now(),
159 invocation_id: invocation_id.into(),
160 branch: String::new(),
161 author: String::new(),
162 llm_response: LlmResponse::default(),
163 actions: EventActions::default(),
164 long_running_tool_ids: Vec::new(),
165 llm_request: None,
166 provider_metadata: HashMap::new(),
167 }
168 }
169
170 /// Creates a streaming tool-progress event.
171 ///
172 /// Tools emit these via [`ToolContext::emit_progress`](crate::ToolContext::emit_progress)
173 /// to push intermediate stdout/stderr to the client *while the tool is still
174 /// running*. The event carries the chunk as partial text content (role
175 /// `"tool"`) and is tagged with [`TOOL_PROGRESS_STREAM_KEY`] /
176 /// [`TOOL_PROGRESS_CALL_ID_KEY`] so consumers can distinguish it from a
177 /// final tool result and route it to the right terminal widget.
178 ///
179 /// # Example
180 ///
181 /// ```
182 /// use adk_core::Event;
183 ///
184 /// let event = Event::tool_progress("inv-1", "agent", "call-7", "stdout", "compiling...\n");
185 /// assert_eq!(event.tool_progress_stream(), Some("stdout"));
186 /// assert!(event.llm_response.partial);
187 /// ```
188 pub fn tool_progress(
189 invocation_id: impl Into<String>,
190 author: impl Into<String>,
191 function_call_id: impl Into<String>,
192 stream: impl Into<String>,
193 chunk: impl Into<String>,
194 ) -> Self {
195 let mut event = Event::new(invocation_id);
196 event.author = author.into();
197 event.llm_response.content = Some(Content {
198 role: "tool".to_string(),
199 parts: vec![crate::types::Part::Text { text: chunk.into() }],
200 });
201 // Partial so downstream aggregation/persistence treats it as a streaming
202 // chunk, never as the agent's final response.
203 event.llm_response.partial = true;
204 event.provider_metadata.insert(TOOL_PROGRESS_STREAM_KEY.to_string(), stream.into());
205 event
206 .provider_metadata
207 .insert(TOOL_PROGRESS_CALL_ID_KEY.to_string(), function_call_id.into());
208 event
209 }
210
211 /// Returns the progress stream name (`"stdout"`, `"stderr"`, …) if this is a
212 /// tool-progress event produced by [`ToolContext::emit_progress`](crate::ToolContext::emit_progress),
213 /// otherwise `None`.
214 pub fn tool_progress_stream(&self) -> Option<&str> {
215 self.provider_metadata.get(TOOL_PROGRESS_STREAM_KEY).map(String::as_str)
216 }
217
218 /// Returns the tool calls carried by this event, as a typed, render-ready view.
219 ///
220 /// A UI consuming the agent's `EventStream` can call this on every event to
221 /// detect when the model requested one or more tools, without matching on
222 /// [`Part::FunctionCall`](crate::Part::FunctionCall) internals. Pair it with
223 /// [`tool_results`](Self::tool_results) and [`tool_progress_stream`](Self::tool_progress_stream)
224 /// to render a complete tool lifecycle (call → live progress → result).
225 ///
226 /// Returns an empty vector for events that contain no tool calls.
227 ///
228 /// # Correlation
229 ///
230 /// Use [`ToolCallView::call_id`] to correlate a call with its progress chunks
231 /// and final result. For providers that omit call ids (e.g. Gemini), fall
232 /// back to [`ToolCallView::name`].
233 ///
234 /// # Example
235 ///
236 /// ```
237 /// use adk_core::{Content, Event, Part};
238 ///
239 /// let mut event = Event::new("inv-1");
240 /// event.llm_response.content = Some(Content {
241 /// role: "model".to_string(),
242 /// parts: vec![Part::FunctionCall {
243 /// name: "bash".to_string(),
244 /// args: serde_json::json!({ "command": "ls" }),
245 /// id: Some("call_1".to_string()),
246 /// thought_signature: None,
247 /// }],
248 /// });
249 ///
250 /// let calls = event.tool_calls();
251 /// assert_eq!(calls.len(), 1);
252 /// assert_eq!(calls[0].name, "bash");
253 /// assert_eq!(calls[0].call_id, Some("call_1"));
254 /// ```
255 pub fn tool_calls(&self) -> Vec<ToolCallView<'_>> {
256 let Some(content) = &self.llm_response.content else {
257 return Vec::new();
258 };
259 content
260 .parts
261 .iter()
262 .filter_map(|part| match part {
263 crate::types::Part::FunctionCall { name, args, id, .. } => {
264 Some(ToolCallView { call_id: id.as_deref(), name, args })
265 }
266 _ => None,
267 })
268 .collect()
269 }
270
271 /// Returns the tool results carried by this event, as a typed, render-ready view.
272 ///
273 /// After a tool executes, the agent yields its result on the same
274 /// `EventStream` as everything else, as a `function`-role event holding a
275 /// [`Part::FunctionResponse`](crate::Part::FunctionResponse). This accessor
276 /// surfaces those results generically so a UI can render the output of *any*
277 /// tool — streaming or not — without walking part internals.
278 ///
279 /// Returns an empty vector for events that contain no tool results.
280 ///
281 /// # Correlation
282 ///
283 /// Use [`ToolResultView::call_id`] to attach a result to the originating
284 /// [`tool_calls`](Self::tool_calls) entry and its progress chunks. For
285 /// providers that omit call ids, fall back to [`ToolResultView::name`].
286 ///
287 /// # Example
288 ///
289 /// ```
290 /// use adk_core::{Content, Event, FunctionResponseData, Part};
291 ///
292 /// let mut event = Event::new("inv-1");
293 /// event.llm_response.content = Some(Content {
294 /// role: "function".to_string(),
295 /// parts: vec![Part::FunctionResponse {
296 /// function_response: FunctionResponseData::new(
297 /// "bash",
298 /// serde_json::json!({ "stdout": "ok\n", "exit_code": 0 }),
299 /// ),
300 /// id: Some("call_1".to_string()),
301 /// annotations: None,
302 /// }],
303 /// });
304 ///
305 /// let results = event.tool_results();
306 /// assert_eq!(results.len(), 1);
307 /// assert_eq!(results[0].name, "bash");
308 /// assert_eq!(results[0].call_id, Some("call_1"));
309 /// assert_eq!(results[0].response["exit_code"], 0);
310 /// ```
311 pub fn tool_results(&self) -> Vec<ToolResultView<'_>> {
312 let Some(content) = &self.llm_response.content else {
313 return Vec::new();
314 };
315 content
316 .parts
317 .iter()
318 .filter_map(|part| match part {
319 crate::types::Part::FunctionResponse { function_response, id, .. } => {
320 Some(ToolResultView {
321 call_id: id.as_deref(),
322 name: &function_response.name,
323 response: &function_response.response,
324 })
325 }
326 _ => None,
327 })
328 .collect()
329 }
330
331 /// Convenience method to access content directly.
332 pub fn content(&self) -> Option<&Content> {
333 self.llm_response.content.as_ref()
334 }
335
336 /// Convenience method to set content directly.
337 pub fn set_content(&mut self, content: Content) {
338 self.llm_response.content = Some(content);
339 }
340
341 /// Returns the Interactions API interaction id for this event, if present.
342 ///
343 /// Reads the id from the flattened [`LlmResponse`], mirroring ADK-Python's
344 /// `event.interaction_id`. Returns `None` for events produced by the
345 /// generateContent transport and non-Gemini providers.
346 ///
347 /// # Example
348 ///
349 /// ```
350 /// use adk_core::Event;
351 ///
352 /// let mut event = Event::new("inv-123");
353 /// assert_eq!(event.interaction_id(), None);
354 ///
355 /// event.llm_response.interaction_id = Some("v1_abc".to_string());
356 /// assert_eq!(event.interaction_id(), Some("v1_abc"));
357 /// ```
358 pub fn interaction_id(&self) -> Option<&str> {
359 self.llm_response.interaction_id.as_deref()
360 }
361
362 /// Returns whether the event is the final response of an agent.
363 ///
364 /// Tool-start notifications and provider-native continuation pauses are not final.
365 ///
366 /// An event is considered final if:
367 /// - It has skip_summarization set, OR
368 /// - It has long_running_tool_ids (indicating async operations), OR
369 /// - It has no function calls, no function responses, is not partial,
370 /// and has no trailing code execution results.
371 ///
372 /// Note: When multiple agents participate in one invocation, there could be
373 /// multiple events with is_final_response() as true, for each participating agent.
374 pub fn is_final_response(&self) -> bool {
375 if self.provider_metadata.contains_key("adk_tool_started")
376 || self
377 .llm_response
378 .provider_metadata
379 .as_ref()
380 .and_then(|metadata| metadata.get("continue_turn"))
381 .and_then(serde_json::Value::as_bool)
382 == Some(true)
383 {
384 return false;
385 }
386
387 // If skip_summarization is set or we have long-running tools, it's final
388 if self.actions.skip_summarization || !self.long_running_tool_ids.is_empty() {
389 return true;
390 }
391
392 // Check content for function calls/responses
393 let has_function_calls = self.has_function_calls();
394 let has_function_responses = self.has_function_responses();
395 let is_partial = self.llm_response.partial;
396 let has_trailing_code_result = self.has_trailing_code_execution_result();
397
398 !has_function_calls && !has_function_responses && !is_partial && !has_trailing_code_result
399 }
400
401 /// Returns true if the event content contains function calls.
402 fn has_function_calls(&self) -> bool {
403 if let Some(content) = &self.llm_response.content {
404 for part in &content.parts {
405 if matches!(part, crate::Part::FunctionCall { .. }) {
406 return true;
407 }
408 }
409 }
410 false
411 }
412
413 /// Returns true if the event content contains function responses.
414 fn has_function_responses(&self) -> bool {
415 if let Some(content) = &self.llm_response.content {
416 for part in &content.parts {
417 if matches!(part, crate::Part::FunctionResponse { .. }) {
418 return true;
419 }
420 }
421 }
422 false
423 }
424
425 /// Returns true if the event has a trailing code execution result.
426 #[allow(clippy::match_like_matches_macro)]
427 fn has_trailing_code_execution_result(&self) -> bool {
428 if let Some(content) = &self.llm_response.content
429 && let Some(last_part) = content.parts.last()
430 {
431 // FunctionResponse as the last part indicates a code execution result
432 // that the model still needs to process.
433 return matches!(last_part, crate::Part::FunctionResponse { .. });
434 }
435 false
436 }
437
438 /// Extracts function call IDs from this event's content.
439 /// Used to identify which function calls are associated with long-running tools.
440 pub fn function_call_ids(&self) -> Vec<String> {
441 let mut ids = Vec::new();
442 if let Some(content) = &self.llm_response.content {
443 for part in &content.parts {
444 if let crate::Part::FunctionCall { name, id, .. } = part {
445 // Use the actual call ID when available (OpenAI-style),
446 // fall back to name for providers that don't emit IDs (Gemini).
447 ids.push(id.as_deref().unwrap_or(name).to_string());
448 }
449 }
450 }
451 ids
452 }
453}
454
455/// Whether an event is visible from a given conversation branch.
456///
457/// An event is visible when its branch equals `invocation_branch` or is an
458/// *ancestor* of it. Siblings and descendants are excluded, which is what keeps
459/// concurrent `ParallelAgent` branches from seeing each other's output while
460/// still letting each one see the conversation that led to the fan-out.
461///
462/// An empty branch on either side means "unscoped" and matches everything, so
463/// events written without a branch stay globally visible and callers that never
464/// set a branch are unaffected.
465///
466/// Branch segments are delimited by `.`, and the prefix test requires that
467/// delimiter explicitly: without it `agent_0` would match `agent_00`. This
468/// mirrors ADK Python's `_is_event_belongs_to_branch` and ADK Go's
469/// `eventBelongsToBranch`.
470///
471/// # Example
472///
473/// ```
474/// use adk_core::event_belongs_to_branch;
475///
476/// // Own branch and ancestors are visible.
477/// assert!(event_belongs_to_branch("parent.parallel.a", "parent.parallel.a"));
478/// assert!(event_belongs_to_branch("parent.parallel.a", "parent"));
479///
480/// // A sibling branch is not.
481/// assert!(!event_belongs_to_branch("parent.parallel.a", "parent.parallel.b"));
482///
483/// // Unscoped events remain visible.
484/// assert!(event_belongs_to_branch("parent.parallel.a", ""));
485/// ```
486pub fn event_belongs_to_branch(invocation_branch: &str, event_branch: &str) -> bool {
487 if invocation_branch.is_empty() || event_branch.is_empty() {
488 return true;
489 }
490 if event_branch == invocation_branch {
491 return true;
492 }
493 // Require the delimiter so `agent_0` does not match `agent_00`.
494 invocation_branch.starts_with(event_branch)
495 && invocation_branch.as_bytes().get(event_branch.len()) == Some(&b'.')
496}
497
498#[cfg(test)]
499mod branch_visibility_tests {
500 use super::event_belongs_to_branch;
501
502 #[test]
503 fn own_branch_is_visible() {
504 assert!(event_belongs_to_branch("root.parallel.a", "root.parallel.a"));
505 }
506
507 #[test]
508 fn ancestor_branches_are_visible() {
509 assert!(event_belongs_to_branch("root.parallel.a", "root"));
510 assert!(event_belongs_to_branch("root.parallel.a", "root.parallel"));
511 }
512
513 #[test]
514 fn sibling_branches_are_hidden() {
515 assert!(!event_belongs_to_branch("root.parallel.a", "root.parallel.b"));
516 assert!(!event_belongs_to_branch("root.parallel.b", "root.parallel.a"));
517 }
518
519 #[test]
520 fn descendant_branches_are_hidden() {
521 // A parent must not see what a nested child produced in its own branch.
522 assert!(!event_belongs_to_branch("root", "root.parallel.a"));
523 }
524
525 #[test]
526 fn empty_branch_on_either_side_matches() {
527 assert!(event_belongs_to_branch("", "root.parallel.a"));
528 assert!(event_belongs_to_branch("root.parallel.a", ""));
529 assert!(event_belongs_to_branch("", ""));
530 }
531
532 #[test]
533 fn prefix_match_requires_the_delimiter() {
534 // The bug the delimiter guards against: `agent_0` vs `agent_00`.
535 assert!(!event_belongs_to_branch("root.agent_00", "root.agent_0"));
536 assert!(event_belongs_to_branch("root.agent_0.child", "root.agent_0"));
537 }
538}
539
540#[cfg(test)]
541mod tests {
542 use super::*;
543 use crate::Part;
544
545 #[test]
546 fn test_event_creation() {
547 let event = Event::new("inv-123");
548 assert_eq!(event.invocation_id, "inv-123");
549 assert!(!event.id.is_empty());
550 }
551
552 #[test]
553 fn test_event_actions_default() {
554 let actions = EventActions::default();
555 assert!(actions.state_delta.is_empty());
556 assert!(!actions.skip_summarization);
557 assert!(actions.tool_confirmation.is_none());
558 assert!(actions.tool_confirmation_decision.is_none());
559 }
560
561 #[test]
562 fn test_state_prefixes() {
563 assert_eq!(KEY_PREFIX_APP, "app:");
564 assert_eq!(KEY_PREFIX_TEMP, "temp:");
565 assert_eq!(KEY_PREFIX_USER, "user:");
566 }
567
568 #[test]
569 fn test_is_final_response_no_content() {
570 let event = Event::new("inv-123");
571 // No content, no function calls -> final
572 assert!(event.is_final_response());
573 }
574
575 #[test]
576 fn tool_start_is_not_final() {
577 let mut event = Event::new("inv-123");
578 event.provider_metadata.insert(
579 "adk_tool_started".into(),
580 serde_json::json!({"id": "call-1", "name": "search"}).to_string(),
581 );
582 assert!(!event.llm_response.partial);
583 assert!(!event.is_final_response());
584 }
585
586 #[test]
587 fn provider_continuation_is_not_final() {
588 for content in [None, Some(Content::new("model").with_text("Searching"))] {
589 let mut event = Event::new("inv-123");
590 event.llm_response.content = content;
591 event.llm_response.provider_metadata = Some(serde_json::json!({"continue_turn": true}));
592 assert!(!event.is_final_response());
593 event.llm_response.provider_metadata =
594 Some(serde_json::json!({"continue_turn": false}));
595 assert!(event.is_final_response());
596 }
597 }
598
599 #[test]
600 fn test_is_final_response_text_only() {
601 let mut event = Event::new("inv-123");
602 event.llm_response.content = Some(Content {
603 role: "model".to_string(),
604 parts: vec![Part::Text { text: "Hello!".to_string() }],
605 });
606 // Text only, no function calls -> final
607 assert!(event.is_final_response());
608 }
609
610 #[test]
611 fn test_is_final_response_with_function_call() {
612 let mut event = Event::new("inv-123");
613 event.llm_response.content = Some(Content {
614 role: "model".to_string(),
615 parts: vec![Part::FunctionCall {
616 name: "get_weather".to_string(),
617 args: serde_json::json!({"city": "NYC"}),
618 id: Some("call_123".to_string()),
619 thought_signature: None,
620 }],
621 });
622 // Has function call -> NOT final (need to execute it)
623 assert!(!event.is_final_response());
624 }
625
626 #[test]
627 fn test_is_final_response_with_function_response() {
628 let mut event = Event::new("inv-123");
629 event.llm_response.content = Some(Content {
630 role: "function".to_string(),
631 parts: vec![Part::FunctionResponse {
632 function_response: crate::FunctionResponseData::new(
633 "get_weather",
634 serde_json::json!({"temp": 72}),
635 ),
636 id: Some("call_123".to_string()),
637 annotations: None,
638 }],
639 });
640 // Has function response -> NOT final (model needs to respond)
641 assert!(!event.is_final_response());
642 }
643
644 #[test]
645 fn test_is_final_response_partial() {
646 let mut event = Event::new("inv-123");
647 event.llm_response.partial = true;
648 event.llm_response.content = Some(Content {
649 role: "model".to_string(),
650 parts: vec![Part::Text { text: "Hello...".to_string() }],
651 });
652 // Partial response -> NOT final
653 assert!(!event.is_final_response());
654 }
655
656 #[test]
657 fn test_is_final_response_skip_summarization() {
658 let mut event = Event::new("inv-123");
659 event.actions.skip_summarization = true;
660 event.llm_response.content = Some(Content {
661 role: "function".to_string(),
662 parts: vec![Part::FunctionResponse {
663 function_response: crate::FunctionResponseData::new(
664 "tool",
665 serde_json::json!({"result": "done"}),
666 ),
667 id: Some("call_tool".to_string()),
668 annotations: None,
669 }],
670 });
671 // Even with function response, skip_summarization makes it final
672 assert!(event.is_final_response());
673 }
674
675 #[test]
676 fn test_is_final_response_long_running_tool_ids() {
677 let mut event = Event::new("inv-123");
678 event.long_running_tool_ids = vec!["process_video".to_string()];
679 event.llm_response.content = Some(Content {
680 role: "model".to_string(),
681 parts: vec![Part::FunctionCall {
682 name: "process_video".to_string(),
683 args: serde_json::json!({"file": "video.mp4"}),
684 id: Some("call_process".to_string()),
685 thought_signature: None,
686 }],
687 });
688 // Has long_running_tool_ids -> final (async operation started)
689 assert!(event.is_final_response());
690 }
691
692 #[test]
693 fn test_function_call_ids() {
694 let mut event = Event::new("inv-123");
695 event.llm_response.content = Some(Content {
696 role: "model".to_string(),
697 parts: vec![
698 Part::FunctionCall {
699 name: "get_weather".to_string(),
700 args: serde_json::json!({}),
701 id: Some("call_1".to_string()),
702 thought_signature: None,
703 },
704 Part::Text { text: "I'll check the weather".to_string() },
705 Part::FunctionCall {
706 name: "get_time".to_string(),
707 args: serde_json::json!({}),
708 id: Some("call_2".to_string()),
709 thought_signature: None,
710 },
711 ],
712 });
713
714 let ids = event.function_call_ids();
715 assert_eq!(ids.len(), 2);
716 // Should use actual call IDs, not function names
717 assert!(ids.contains(&"call_1".to_string()));
718 assert!(ids.contains(&"call_2".to_string()));
719 }
720
721 #[test]
722 fn test_function_call_ids_falls_back_to_name() {
723 let mut event = Event::new("inv-123");
724 event.llm_response.content = Some(Content {
725 role: "model".to_string(),
726 parts: vec![Part::FunctionCall {
727 name: "get_weather".to_string(),
728 args: serde_json::json!({}),
729 id: None, // Gemini-style: no explicit ID
730 thought_signature: None,
731 }],
732 });
733
734 let ids = event.function_call_ids();
735 assert_eq!(ids, vec!["get_weather".to_string()]);
736 }
737
738 #[test]
739 fn test_function_call_ids_empty() {
740 let event = Event::new("inv-123");
741 let ids = event.function_call_ids();
742 assert!(ids.is_empty());
743 }
744
745 #[test]
746 fn test_is_final_response_trailing_function_response() {
747 // Text followed by a function response as the last part —
748 // has_trailing_code_execution_result should catch this even though
749 // has_function_responses also catches it.
750 let mut event = Event::new("inv-123");
751 event.llm_response.content = Some(Content {
752 role: "model".to_string(),
753 parts: vec![
754 Part::Text { text: "Running code...".to_string() },
755 Part::FunctionResponse {
756 function_response: crate::FunctionResponseData::new(
757 "code_exec",
758 serde_json::json!({"output": "42"}),
759 ),
760 id: Some("call_exec".to_string()),
761 annotations: None,
762 },
763 ],
764 });
765 // Trailing function response -> NOT final
766 assert!(!event.is_final_response());
767 }
768
769 #[test]
770 fn test_event_roundtrip_with_both_provider_metadata() {
771 let mut event = Event::new("inv-1");
772 event.provider_metadata.insert("adk.tool_progress.stream".into(), "stdout".into());
773 event.provider_metadata.insert("adk.tool_progress.call_id".into(), "call-7".into());
774 event.llm_response.provider_metadata = Some(serde_json::json!({"response_id": "resp-xyz"}));
775
776 let json = serde_json::to_string(&event).expect("serialize");
777 // Round-trip must succeed — regression test for the duplicate
778 // `provider_metadata` flatten collision with LlmResponse.
779 let back: Event = serde_json::from_str(&json)
780 .expect("round-trip must succeed without duplicate field error");
781
782 assert_eq!(
783 back.provider_metadata.get("adk.tool_progress.stream").map(String::as_str),
784 Some("stdout"),
785 );
786 assert_eq!(
787 back.provider_metadata.get("adk.tool_progress.call_id").map(String::as_str),
788 Some("call-7"),
789 );
790 assert_eq!(
791 back.llm_response.provider_metadata,
792 Some(serde_json::json!({"response_id": "resp-xyz"})),
793 );
794 }
795
796 #[test]
797 fn test_is_final_response_text_after_function_response() {
798 // Function response followed by text — the trailing part is text,
799 // so has_trailing_code_execution_result is false, but
800 // has_function_responses is still true.
801 let mut event = Event::new("inv-123");
802 event.llm_response.content = Some(Content {
803 role: "model".to_string(),
804 parts: vec![
805 Part::FunctionResponse {
806 function_response: crate::FunctionResponseData::new(
807 "tool",
808 serde_json::json!({}),
809 ),
810 id: Some("call_1".to_string()),
811 annotations: None,
812 },
813 Part::Text { text: "Done".to_string() },
814 ],
815 });
816 // Still has function responses -> NOT final
817 assert!(!event.is_final_response());
818 }
819}