gemini_adk_rs/events/mod.rs
1//! Event system — structured events for agent invocations.
2//!
3//! Mirrors ADK-JS's event types. Each event captures a discrete action
4//! within an agent invocation (user message, model response, tool call, etc.).
5
6pub mod structured;
7pub use structured::*;
8
9use std::collections::HashMap;
10
11use serde::{Deserialize, Serialize};
12
13/// A structured event within an agent invocation.
14///
15/// Events form the audit trail of an agent session. They capture user messages,
16/// model responses, tool calls, state changes, and control flow actions.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct Event {
19 /// Unique event ID.
20 pub id: String,
21 /// Invocation ID grouping related events.
22 pub invocation_id: String,
23 /// Who authored this event (e.g., "user", agent name, tool name).
24 pub author: String,
25 /// Optional text content of the event.
26 pub content: Option<String>,
27 /// Actions triggered by this event.
28 pub actions: EventActions,
29 /// Unix timestamp (seconds).
30 pub timestamp: u64,
31}
32
33impl Event {
34 /// Create a new event with the given author and optional content.
35 pub fn new(author: impl Into<String>, content: Option<String>) -> Self {
36 let dur = std::time::SystemTime::now()
37 .duration_since(std::time::UNIX_EPOCH)
38 .unwrap_or_default();
39 Self {
40 id: uuid::Uuid::new_v4().to_string(),
41 invocation_id: String::new(),
42 author: author.into(),
43 content,
44 actions: EventActions::default(),
45 timestamp: dur.as_secs(),
46 }
47 }
48
49 /// Set the invocation ID.
50 pub fn with_invocation(mut self, invocation_id: impl Into<String>) -> Self {
51 self.invocation_id = invocation_id.into();
52 self
53 }
54
55 /// Set actions on this event.
56 pub fn with_actions(mut self, actions: EventActions) -> Self {
57 self.actions = actions;
58 self
59 }
60}
61
62/// Actions triggered by an event — control flow and state mutations.
63#[derive(Debug, Clone, Default, Serialize, Deserialize)]
64pub struct EventActions {
65 /// If true, escalate to a human or parent agent.
66 #[serde(default)]
67 pub escalate: bool,
68 /// If true, skip summarization of this event's content.
69 #[serde(default)]
70 pub skip_summarization: bool,
71 /// Transfer control to another agent by name.
72 #[serde(default)]
73 pub transfer_to_agent: Option<String>,
74 /// State mutations (delta key → new value).
75 ///
76 /// Deletions travel in here too, under the reserved [`Self::REMOVED_KEYS`]
77 /// entry — see that constant for why they are not spelled as `null`. State
78 /// keys are written through [`Self::encode_key`] and read back through
79 /// [`Self::decode_key`], so a state key that collides with a reserved name
80 /// is carried rather than lost, and [`Self::FORMAT`] marks a delta that
81 /// went through that encoding so a pre-1.0.1 one is read literally.
82 #[serde(default)]
83 pub state_delta: HashMap<String, serde_json::Value>,
84}
85
86impl EventActions {
87 /// Reserved `state_delta` entry carrying the keys an event deleted, as a
88 /// JSON array of strings.
89 ///
90 /// Deletion needs its own channel because `null` is a perfectly ordinary
91 /// value an agent may store deliberately: using it as the tombstone made
92 /// replay drop a real `null`, so persistence was not round-trip lossless
93 /// for valid `State`. It rides inside `state_delta` rather than as a
94 /// sibling field so that `EventActions` stays constructible by downstream
95 /// code that already names every field.
96 ///
97 /// `State` accepts arbitrary keys, so this name is not reserved *from*
98 /// applications — it is escaped around them by [`Self::encode_key`]. It is
99 /// only read as a removal list on an event that carries [`Self::FORMAT`].
100 pub const REMOVED_KEYS: &'static str = "adk:removed";
101
102 /// Reserved `state_delta` entry marking an event whose keys went through
103 /// [`Self::encode_key`] and whose removals live at [`Self::REMOVED_KEYS`].
104 ///
105 /// Events written before 1.0.1 have no such marker and no escaping: every
106 /// entry in them is a literal state key, including one that happens to be
107 /// named `adk:removed` or to end in `:literal`. Without this discriminator
108 /// there is nothing in the delta itself to tell the two eras apart, and
109 /// decoding a legacy event would shift those keys or read a stored array
110 /// as a deletion list. Replay therefore decodes only marked events.
111 ///
112 /// A residue is structural: the marker shares the arbitrary key/value space
113 /// of the deltas it discriminates, so a legacy event that stored exactly
114 /// [`Self::FORMAT_VERSION`] at exactly this key would still be misread.
115 /// Closing that completely takes a field outside `state_delta`, which
116 /// `EventActions` cannot grow without breaking source compatibility — so
117 /// the marker value is chosen to make the collision unreachable in
118 /// practice rather than merely unlikely.
119 pub const FORMAT: &'static str = "adk:format";
120
121 /// Value written at [`Self::FORMAT`] by this version.
122 ///
123 /// A sentinel string rather than a version number: a plain `1` is a value
124 /// an application could conceivably have stored under its own key, whereas
125 /// this one only appears if someone wrote this crate's marker by hand.
126 pub const FORMAT_VERSION: &'static str = "gemini-adk/state-delta/1";
127
128 /// Suffix appended by [`Self::encode_key`] to step a colliding state key
129 /// out of a reserved entry's way.
130 const LITERAL: &'static str = ":literal";
131
132 /// Create actions that transfer to another agent.
133 pub fn transfer(agent_name: impl Into<String>) -> Self {
134 Self {
135 transfer_to_agent: Some(agent_name.into()),
136 ..Default::default()
137 }
138 }
139
140 /// Create actions that escalate.
141 pub fn escalate() -> Self {
142 Self {
143 escalate: true,
144 ..Default::default()
145 }
146 }
147
148 /// Create actions with a state delta.
149 pub fn state_delta(delta: HashMap<String, serde_json::Value>) -> Self {
150 Self {
151 state_delta: delta,
152 ..Default::default()
153 }
154 }
155
156 /// Create actions that remove keys from state.
157 pub fn state_removed(keys: impl IntoIterator<Item = String>) -> Self {
158 let keys: Vec<serde_json::Value> =
159 keys.into_iter().map(serde_json::Value::String).collect();
160 let mut delta = HashMap::new();
161 delta.insert(
162 Self::REMOVED_KEYS.to_string(),
163 serde_json::Value::Array(keys),
164 );
165 let mut actions = Self {
166 state_delta: delta,
167 ..Default::default()
168 };
169 actions.mark_format();
170 actions
171 }
172
173 /// Stamp [`Self::FORMAT`] on this delta, declaring its keys encoded and
174 /// its [`Self::REMOVED_KEYS`] entry a removal list.
175 pub fn mark_format(&mut self) {
176 self.state_delta.insert(
177 Self::FORMAT.to_string(),
178 serde_json::Value::from(Self::FORMAT_VERSION),
179 );
180 }
181
182 /// True when this delta carries a [`Self::FORMAT`] marker this build
183 /// understands — i.e. it was written by 1.0.1 or later.
184 ///
185 /// An unmarked delta is a legacy one: every entry in it is a literal state
186 /// key and it has no removal channel. A marker from a *newer* format than
187 /// this build knows also reads as unmarked, so a forward-dated event is
188 /// replayed literally rather than decoded under the wrong rules.
189 pub fn is_format_marked(&self) -> bool {
190 self.state_delta
191 .get(Self::FORMAT)
192 .and_then(serde_json::Value::as_str)
193 .is_some_and(|v| v == Self::FORMAT_VERSION)
194 }
195
196 /// The state keys this event deletes, drawn from [`Self::REMOVED_KEYS`].
197 ///
198 /// Always empty on a delta without [`Self::FORMAT`]: a legacy event that
199 /// happens to hold an array of strings at that key stored it as a value,
200 /// and replaying it as a deletion list would delete every key it names.
201 pub fn removed_keys(&self) -> impl Iterator<Item = &str> {
202 self.is_format_marked()
203 .then(|| self.state_delta.get(Self::REMOVED_KEYS))
204 .flatten()
205 .and_then(serde_json::Value::as_array)
206 .map(Vec::as_slice)
207 .unwrap_or_default()
208 .iter()
209 .filter_map(serde_json::Value::as_str)
210 }
211
212 /// Map a state key to the `state_delta` key that carries it.
213 ///
214 /// Ordinary keys pass through. A key that would land on a reserved entry —
215 /// `adk:removed` or `adk:format`, or an already-escaped form of either
216 /// followed by any number of `:literal` suffixes — gains one more
217 /// `:literal`. That ladder is injective and never produces a bare reserved
218 /// name, so both channels stay free without any state key being dropped or
219 /// overwritten.
220 pub fn encode_key(key: &str) -> std::borrow::Cow<'_, str> {
221 if Self::is_escape_ladder(key) {
222 std::borrow::Cow::Owned(format!("{key}{}", Self::LITERAL))
223 } else {
224 std::borrow::Cow::Borrowed(key)
225 }
226 }
227
228 /// Inverse of [`Self::encode_key`]: recover the state key a `state_delta`
229 /// entry carries.
230 ///
231 /// Apply this only to a delta where [`Self::is_format_marked`] holds. On a
232 /// legacy delta every key is already literal, and stripping a `:literal`
233 /// suffix there would rename a state key the application chose.
234 pub fn decode_key(key: &str) -> std::borrow::Cow<'_, str> {
235 match key.strip_suffix(Self::LITERAL) {
236 Some(stripped) if Self::is_escape_ladder(stripped) => {
237 std::borrow::Cow::Borrowed(stripped)
238 }
239 _ => std::borrow::Cow::Borrowed(key),
240 }
241 }
242
243 /// A reserved name followed by zero or more `:literal` suffixes.
244 fn is_escape_ladder(key: &str) -> bool {
245 let Some(mut rest) = [Self::REMOVED_KEYS, Self::FORMAT]
246 .iter()
247 .find_map(|reserved| key.strip_prefix(reserved))
248 else {
249 return false;
250 };
251 while let Some(next) = rest.strip_prefix(Self::LITERAL) {
252 rest = next;
253 }
254 rest.is_empty()
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[test]
263 fn event_new() {
264 let event = Event::new("user", Some("Hello!".to_string()));
265 assert_eq!(event.author, "user");
266 assert_eq!(event.content, Some("Hello!".to_string()));
267 assert!(!event.id.is_empty());
268 assert!(event.timestamp > 0);
269 }
270
271 #[test]
272 fn event_with_invocation() {
273 let event = Event::new("agent", None).with_invocation("inv-123");
274 assert_eq!(event.invocation_id, "inv-123");
275 }
276
277 #[test]
278 fn event_actions_transfer() {
279 let actions = EventActions::transfer("helper-agent");
280 assert_eq!(actions.transfer_to_agent, Some("helper-agent".to_string()));
281 assert!(!actions.escalate);
282 }
283
284 #[test]
285 fn event_actions_escalate() {
286 let actions = EventActions::escalate();
287 assert!(actions.escalate);
288 assert!(actions.transfer_to_agent.is_none());
289 }
290
291 #[test]
292 fn event_actions_state_delta() {
293 let mut delta = HashMap::new();
294 delta.insert("topic".to_string(), serde_json::json!("Rust"));
295 let actions = EventActions::state_delta(delta);
296 assert_eq!(
297 actions.state_delta.get("topic"),
298 Some(&serde_json::json!("Rust"))
299 );
300 }
301
302 #[test]
303 fn event_actions_state_removed_round_trips_through_the_reserved_entry() {
304 let actions = EventActions::state_removed(["a".to_string(), "b".to_string()]);
305 assert_eq!(actions.removed_keys().collect::<Vec<_>>(), ["a", "b"]);
306
307 // Removals must survive the wire, not just the in-process struct.
308 let json = serde_json::to_string(&actions).unwrap();
309 let parsed: EventActions = serde_json::from_str(&json).unwrap();
310 assert_eq!(parsed.removed_keys().collect::<Vec<_>>(), ["a", "b"]);
311 }
312
313 #[test]
314 fn actions_without_the_reserved_entry_remove_nothing() {
315 let mut delta = HashMap::new();
316 // A deliberately stored `null` is a value, not a tombstone.
317 delta.insert("maybe".to_string(), serde_json::Value::Null);
318 let actions = EventActions::state_delta(delta);
319 assert_eq!(actions.removed_keys().count(), 0);
320 }
321
322 #[test]
323 fn key_escaping_is_injective_and_frees_the_reserved_name() {
324 // Ordinary keys are untouched, including near-misses.
325 for key in [
326 "turn_count",
327 "adk:removedish",
328 "adk:removed:literalish",
329 "adk:formatting",
330 ] {
331 assert_eq!(EventActions::encode_key(key), key);
332 assert_eq!(EventActions::decode_key(key), key);
333 }
334
335 // Both reserved names, and every already-escaped form of either, step
336 // one rung up — so nothing an application stores lands on a channel.
337 for reserved in [EventActions::REMOVED_KEYS, EventActions::FORMAT] {
338 let mut key = reserved.to_string();
339 for _ in 0..4 {
340 let encoded = EventActions::encode_key(&key).into_owned();
341 assert_ne!(encoded, EventActions::REMOVED_KEYS);
342 assert_ne!(encoded, EventActions::FORMAT);
343 assert_ne!(encoded, key);
344 assert_eq!(EventActions::decode_key(&encoded), key);
345 key = encoded;
346 }
347 }
348 }
349
350 /// A pre-1.0.1 event can hold a real state value at the reserved key —
351 /// including a string array, which is exactly the shape a removal list
352 /// has. Without the format marker it is a value, never a deletion list.
353 #[test]
354 fn a_legacy_string_array_at_the_reserved_key_is_not_read_as_removals() {
355 for payload in [
356 serde_json::json!(["a", "b"]),
357 serde_json::json!([]),
358 serde_json::json!({"legacy": true}),
359 ] {
360 let mut delta = HashMap::new();
361 delta.insert(EventActions::REMOVED_KEYS.to_string(), payload.clone());
362 let actions = EventActions::state_delta(delta);
363 assert!(!actions.is_format_marked());
364 assert_eq!(
365 actions.removed_keys().count(),
366 0,
367 "unmarked delta must have no removal channel, got {payload}"
368 );
369 }
370
371 // The same array on a marked delta *is* the removal list.
372 let actions = EventActions::state_removed(["a".to_string(), "b".to_string()]);
373 assert!(actions.is_format_marked());
374 assert_eq!(actions.removed_keys().collect::<Vec<_>>(), ["a", "b"]);
375 }
376
377 /// The marker value is a sentinel string, not a bare number: a legacy
378 /// delta holding `1` — or any other ordinary value — at the marker key
379 /// must not be mistaken for a marked one.
380 #[test]
381 fn an_ordinary_value_at_the_marker_key_does_not_mark_a_delta() {
382 for value in [
383 serde_json::json!(1),
384 serde_json::json!("1"),
385 serde_json::json!(true),
386 serde_json::json!(null),
387 ] {
388 let mut delta = HashMap::new();
389 delta.insert(EventActions::FORMAT.to_string(), value.clone());
390 delta.insert(
391 EventActions::REMOVED_KEYS.to_string(),
392 serde_json::json!(["victim"]),
393 );
394 let actions = EventActions::state_delta(delta);
395 assert!(
396 !actions.is_format_marked(),
397 "{value} at the marker key must not mark the delta"
398 );
399 assert_eq!(actions.removed_keys().count(), 0);
400 }
401 }
402
403 /// A marker this build does not recognise must not be decoded under this
404 /// build's rules — a forward-dated event replays literally instead.
405 #[test]
406 fn an_unrecognised_format_version_reads_as_unmarked() {
407 let mut actions = EventActions::state_removed(["a".to_string()]);
408 actions.state_delta.insert(
409 EventActions::FORMAT.to_string(),
410 serde_json::json!("gemini-adk/state-delta/2"),
411 );
412 assert!(!actions.is_format_marked());
413 assert_eq!(actions.removed_keys().count(), 0);
414 }
415
416 /// `EventActions` is exhaustively constructible by downstream code, so its
417 /// field set is part of the public API and cannot grow in a patch release.
418 /// This literal names every field: if one is added, this test stops
419 /// compiling here rather than in someone else's crate after publish.
420 #[test]
421 fn event_actions_stays_exhaustively_constructible() {
422 let actions = EventActions {
423 escalate: false,
424 skip_summarization: false,
425 transfer_to_agent: None,
426 state_delta: HashMap::new(),
427 };
428 assert!(!actions.escalate);
429 }
430
431 #[test]
432 fn event_serialization() {
433 let event = Event::new("model", Some("Response text".to_string()));
434 let json = serde_json::to_string(&event).unwrap();
435 let parsed: Event = serde_json::from_str(&json).unwrap();
436 assert_eq!(parsed.author, "model");
437 assert_eq!(parsed.content, Some("Response text".to_string()));
438 }
439}