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}