Skip to main content

adk_managed/types/
events.rs

1//! Event types for the managed agent runtime.
2//!
3//! Defines [`UserEvent`] (client → agent) and [`SessionEvent`] (agent → client),
4//! conforming to CANON §3.4 wire shapes. Both enums are `#[non_exhaustive]`
5//! for forward-compatible additive evolution.
6
7use serde::{Deserialize, Serialize};
8
9use super::content::ContentBlock;
10
11// ─── UserEvent ───────────────────────────────────────────────────────────────
12
13/// Client-to-agent event. Discriminated by `type` field for wire serialization.
14///
15/// # Wire Shapes (CANON §3.4)
16///
17/// ```json
18/// {"type": "user.message", "content": [{"type": "text", "text": "Hello"}]}
19/// {"type": "user.interrupt"}
20/// ```
21///
22/// # Example
23///
24/// ```rust
25/// use adk_managed::types::{UserEvent, ContentBlock};
26///
27/// let event = UserEvent::Message {
28///     content: vec![ContentBlock::Text { text: "Hello".to_string() }],
29/// };
30/// let json = serde_json::to_string(&event).unwrap();
31/// assert!(json.contains(r#""type":"user.message""#));
32/// ```
33#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(tag = "type", rename_all = "snake_case")]
35#[non_exhaustive]
36pub enum UserEvent {
37    /// Send a message turn.
38    #[serde(rename = "user.message")]
39    Message {
40        /// The message content blocks.
41        content: Vec<ContentBlock>,
42    },
43
44    /// Interrupt the current turn.
45    #[serde(rename = "user.interrupt")]
46    Interrupt {},
47
48    /// Approve or deny a tool confirmation request.
49    #[serde(rename = "user.tool_confirmation")]
50    ToolConfirmation {
51        /// The tool use ID being confirmed.
52        tool_use_id: String,
53        /// Whether to allow or deny.
54        result: ConfirmationResult,
55        /// Optional message explaining why the tool was denied.
56        #[serde(skip_serializing_if = "Option::is_none")]
57        deny_message: Option<String>,
58    },
59
60    /// Return results for a client-executed custom tool.
61    #[serde(rename = "user.custom_tool_result")]
62    CustomToolResult {
63        /// The custom tool use ID this result corresponds to.
64        custom_tool_use_id: String,
65        /// The result content blocks.
66        content: Vec<ContentBlock>,
67    },
68
69    /// Return results for a built-in tool (self-hosted only).
70    /// In hosted topology, built-in tools execute server-side in the sandbox.
71    #[serde(rename = "user.tool_result")]
72    ToolResult {
73        /// The tool use ID this result corresponds to.
74        tool_use_id: String,
75        /// The result content blocks.
76        content: Vec<ContentBlock>,
77    },
78
79    /// Define success criteria for the session.
80    #[serde(rename = "user.define_outcome")]
81    DefineOutcome {
82        /// The success criteria description.
83        criteria: String,
84    },
85}
86
87/// Result of a tool confirmation request.
88#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
89#[serde(rename_all = "lowercase")]
90pub enum ConfirmationResult {
91    /// Allow the tool to execute.
92    Allow,
93    /// Deny the tool execution.
94    Deny,
95}
96
97// ─── SessionEvent ────────────────────────────────────────────────────────────
98
99/// Agent-to-client event. Each carries a monotonic `seq` per session.
100///
101/// # Wire Shapes (CANON §3.4)
102///
103/// ```json
104/// {"type": "agent.message", "content": [{"type": "text", "text": "Hi"}], "seq": 1}
105/// {"type": "status.running", "seq": 2}
106/// {"type": "status.idle", "seq": 3, "stop_reason": {"reason": "end_turn"}}
107/// ```
108///
109/// # Example
110///
111/// ```rust
112/// use adk_managed::types::{SessionEvent, ContentBlock};
113///
114/// let event = SessionEvent::Message {
115///     content: vec![ContentBlock::Text { text: "Hello".to_string() }],
116///     seq: 1,
117/// };
118/// let json = serde_json::to_string(&event).unwrap();
119/// assert!(json.contains(r#""type":"agent.message""#));
120/// assert!(json.contains(r#""seq":1"#));
121/// ```
122#[derive(Debug, Clone, Serialize, Deserialize)]
123#[serde(tag = "type", rename_all = "snake_case")]
124#[non_exhaustive]
125pub enum SessionEvent {
126    /// Assistant message content.
127    #[serde(rename = "agent.message")]
128    Message {
129        /// The message content blocks.
130        content: Vec<ContentBlock>,
131        /// Monotonically increasing sequence number.
132        seq: u64,
133    },
134
135    /// Built-in tool invocation (executes server-side in sandbox).
136    #[serde(rename = "agent.tool_use")]
137    ToolUse {
138        /// Unique identifier for this tool use.
139        tool_use_id: String,
140        /// Tool name.
141        name: String,
142        /// Tool input parameters.
143        input: serde_json::Value,
144        /// Monotonically increasing sequence number.
145        seq: u64,
146    },
147
148    /// Custom tool invocation (client must execute and return result).
149    /// The loop PARKS until `user.custom_tool_result` with matching ID arrives.
150    #[serde(rename = "agent.custom_tool_use")]
151    CustomToolUse {
152        /// Unique identifier for this custom tool use.
153        custom_tool_use_id: String,
154        /// Tool name.
155        name: String,
156        /// Tool input parameters.
157        input: serde_json::Value,
158        /// Monotonically increasing sequence number.
159        seq: u64,
160    },
161
162    /// MCP tool invocation.
163    #[serde(rename = "agent.mcp_tool_use")]
164    McpToolUse {
165        /// Unique identifier for this MCP tool use.
166        tool_use_id: String,
167        /// Tool name.
168        name: String,
169        /// Tool input parameters.
170        input: serde_json::Value,
171        /// Monotonically increasing sequence number.
172        seq: u64,
173    },
174
175    /// Session became active (processing a turn).
176    #[serde(rename = "status.running")]
177    StatusRunning {
178        /// Monotonically increasing sequence number.
179        seq: u64,
180    },
181
182    /// Turn complete; awaiting next event.
183    /// Includes `stop_reason` to tell the caller WHY the turn ended,
184    /// and `usage` reporting token consumption for billing/metering.
185    #[serde(rename = "status.idle")]
186    StatusIdle {
187        /// Monotonically increasing sequence number.
188        seq: u64,
189        /// Why the turn ended. Enables the client to decide what to do next.
190        stop_reason: Option<StopReason>,
191        /// Token usage for this turn (input/output/total).
192        /// Present when the LLM reports usage metadata; `None` on error turns.
193        #[serde(skip_serializing_if = "Option::is_none")]
194        usage: Option<crate::usage::UsageReport>,
195    },
196
197    /// Error during execution.
198    #[serde(rename = "error")]
199    Error {
200        /// Error code identifier.
201        code: String,
202        /// Human-readable error message.
203        message: String,
204        /// Monotonically increasing sequence number.
205        seq: u64,
206    },
207}
208
209/// Why a turn ended. Included in `status.idle` events.
210///
211/// # Wire Shapes
212///
213/// ```json
214/// {"reason": "end_turn"}
215/// {"reason": "requires_action", "event_ids": ["evt_001", "evt_002"]}
216/// {"reason": "max_tokens"}
217/// ```
218#[derive(Debug, Clone, Serialize, Deserialize)]
219#[serde(tag = "reason", rename_all = "snake_case")]
220#[non_exhaustive]
221pub enum StopReason {
222    /// The LLM naturally ended its turn (end_turn stop reason from provider).
223    EndTurn,
224    /// The agent emitted custom tool calls that require client action.
225    /// The caller must send `user.custom_tool_result` for each listed event_id.
226    RequiresAction {
227        /// IDs of events requiring action.
228        event_ids: Vec<String>,
229    },
230    /// The LLM hit its maximum token limit mid-generation.
231    MaxTokens,
232}
233
234#[cfg(test)]
235mod tests {
236    use super::*;
237    use serde_json::json;
238
239    // ─── UserEvent Tests ─────────────────────────────────────────────────────
240
241    #[test]
242    fn test_user_message_serialization() {
243        let event =
244            UserEvent::Message { content: vec![ContentBlock::Text { text: "Hello".to_string() }] };
245        let value = serde_json::to_value(&event).unwrap();
246        assert_eq!(value["type"], "user.message");
247        assert_eq!(value["content"][0]["type"], "text");
248        assert_eq!(value["content"][0]["text"], "Hello");
249    }
250
251    #[test]
252    fn test_user_interrupt_serialization() {
253        let event = UserEvent::Interrupt {};
254        let value = serde_json::to_value(&event).unwrap();
255        assert_eq!(value["type"], "user.interrupt");
256    }
257
258    #[test]
259    fn test_user_tool_confirmation_serialization() {
260        let event = UserEvent::ToolConfirmation {
261            tool_use_id: "tu_123".to_string(),
262            result: ConfirmationResult::Allow,
263            deny_message: None,
264        };
265        let value = serde_json::to_value(&event).unwrap();
266        assert_eq!(value["type"], "user.tool_confirmation");
267        assert_eq!(value["tool_use_id"], "tu_123");
268        assert_eq!(value["result"], "allow");
269        assert!(value.get("deny_message").is_none());
270    }
271
272    #[test]
273    fn test_user_tool_confirmation_with_deny() {
274        let event = UserEvent::ToolConfirmation {
275            tool_use_id: "tu_456".to_string(),
276            result: ConfirmationResult::Deny,
277            deny_message: Some("Not authorized".to_string()),
278        };
279        let value = serde_json::to_value(&event).unwrap();
280        assert_eq!(value["type"], "user.tool_confirmation");
281        assert_eq!(value["result"], "deny");
282        assert_eq!(value["deny_message"], "Not authorized");
283    }
284
285    #[test]
286    fn test_user_custom_tool_result_serialization() {
287        let event = UserEvent::CustomToolResult {
288            custom_tool_use_id: "ctu_789".to_string(),
289            content: vec![ContentBlock::Text { text: "result data".to_string() }],
290        };
291        let value = serde_json::to_value(&event).unwrap();
292        assert_eq!(value["type"], "user.custom_tool_result");
293        assert_eq!(value["custom_tool_use_id"], "ctu_789");
294    }
295
296    #[test]
297    fn test_user_tool_result_serialization() {
298        let event = UserEvent::ToolResult {
299            tool_use_id: "tu_self_001".to_string(),
300            content: vec![ContentBlock::Text { text: "tool output".to_string() }],
301        };
302        let value = serde_json::to_value(&event).unwrap();
303        assert_eq!(value["type"], "user.tool_result");
304        assert_eq!(value["tool_use_id"], "tu_self_001");
305    }
306
307    #[test]
308    fn test_user_define_outcome_serialization() {
309        let event =
310            UserEvent::DefineOutcome { criteria: "Task is completed successfully".to_string() };
311        let value = serde_json::to_value(&event).unwrap();
312        assert_eq!(value["type"], "user.define_outcome");
313        assert_eq!(value["criteria"], "Task is completed successfully");
314    }
315
316    #[test]
317    fn test_user_event_unknown_type_rejected() {
318        let json_str = r#"{"type": "user.unknown", "data": "something"}"#;
319        let result: Result<UserEvent, _> = serde_json::from_str(json_str);
320        assert!(result.is_err(), "Unknown type should be rejected");
321    }
322
323    #[test]
324    fn test_user_event_round_trip() {
325        let event = UserEvent::Message {
326            content: vec![ContentBlock::Text { text: "Round trip".to_string() }],
327        };
328        let json = serde_json::to_string(&event).unwrap();
329        let deserialized: UserEvent = serde_json::from_str(&json).unwrap();
330        match deserialized {
331            UserEvent::Message { content } => {
332                assert_eq!(content.len(), 1);
333                match &content[0] {
334                    ContentBlock::Text { text } => assert_eq!(text, "Round trip"),
335                    _ => panic!("Expected Text content block"),
336                }
337            }
338            _ => panic!("Expected Message variant"),
339        }
340    }
341
342    // ─── SessionEvent Tests ──────────────────────────────────────────────────
343
344    #[test]
345    fn test_session_message_serialization() {
346        let event = SessionEvent::Message {
347            content: vec![ContentBlock::Text { text: "Hi there".to_string() }],
348            seq: 1,
349        };
350        let value = serde_json::to_value(&event).unwrap();
351        assert_eq!(value["type"], "agent.message");
352        assert_eq!(value["seq"], 1);
353        assert_eq!(value["content"][0]["text"], "Hi there");
354    }
355
356    #[test]
357    fn test_session_tool_use_serialization() {
358        let event = SessionEvent::ToolUse {
359            tool_use_id: "tu_001".to_string(),
360            name: "search".to_string(),
361            input: json!({"query": "rust async"}),
362            seq: 2,
363        };
364        let value = serde_json::to_value(&event).unwrap();
365        assert_eq!(value["type"], "agent.tool_use");
366        assert_eq!(value["tool_use_id"], "tu_001");
367        assert_eq!(value["name"], "search");
368        assert_eq!(value["input"]["query"], "rust async");
369        assert_eq!(value["seq"], 2);
370    }
371
372    #[test]
373    fn test_session_custom_tool_use_serialization() {
374        let event = SessionEvent::CustomToolUse {
375            custom_tool_use_id: "ctu_001".to_string(),
376            name: "deploy".to_string(),
377            input: json!({"target": "production"}),
378            seq: 3,
379        };
380        let value = serde_json::to_value(&event).unwrap();
381        assert_eq!(value["type"], "agent.custom_tool_use");
382        assert_eq!(value["custom_tool_use_id"], "ctu_001");
383        assert_eq!(value["name"], "deploy");
384        assert_eq!(value["input"]["target"], "production");
385        assert_eq!(value["seq"], 3);
386    }
387
388    #[test]
389    fn test_session_mcp_tool_use_serialization() {
390        let event = SessionEvent::McpToolUse {
391            tool_use_id: "mcp_001".to_string(),
392            name: "file_read".to_string(),
393            input: json!({"path": "/tmp/data.txt"}),
394            seq: 4,
395        };
396        let value = serde_json::to_value(&event).unwrap();
397        assert_eq!(value["type"], "agent.mcp_tool_use");
398        assert_eq!(value["tool_use_id"], "mcp_001");
399        assert_eq!(value["name"], "file_read");
400        assert_eq!(value["input"]["path"], "/tmp/data.txt");
401        assert_eq!(value["seq"], 4);
402    }
403
404    #[test]
405    fn test_session_status_running_serialization() {
406        let event = SessionEvent::StatusRunning { seq: 5 };
407        let value = serde_json::to_value(&event).unwrap();
408        assert_eq!(value["type"], "status.running");
409        assert_eq!(value["seq"], 5);
410    }
411
412    #[test]
413    fn test_session_status_idle_serialization_no_stop_reason() {
414        let event = SessionEvent::StatusIdle { seq: 6, stop_reason: None, usage: None };
415        let value = serde_json::to_value(&event).unwrap();
416        assert_eq!(value["type"], "status.idle");
417        assert_eq!(value["seq"], 6);
418        assert_eq!(value["stop_reason"], json!(null));
419    }
420
421    #[test]
422    fn test_session_status_idle_with_end_turn() {
423        let event = SessionEvent::StatusIdle {
424            seq: 7,
425            stop_reason: Some(StopReason::EndTurn),
426            usage: None,
427        };
428        let value = serde_json::to_value(&event).unwrap();
429        assert_eq!(value["type"], "status.idle");
430        assert_eq!(value["seq"], 7);
431        assert_eq!(value["stop_reason"]["reason"], "end_turn");
432    }
433
434    #[test]
435    fn test_session_status_idle_with_requires_action() {
436        let event = SessionEvent::StatusIdle {
437            seq: 8,
438            stop_reason: Some(StopReason::RequiresAction {
439                event_ids: vec!["evt_001".to_string(), "evt_002".to_string()],
440            }),
441            usage: None,
442        };
443        let value = serde_json::to_value(&event).unwrap();
444        assert_eq!(value["type"], "status.idle");
445        assert_eq!(value["seq"], 8);
446        assert_eq!(value["stop_reason"]["reason"], "requires_action");
447        assert_eq!(value["stop_reason"]["event_ids"], json!(["evt_001", "evt_002"]));
448    }
449
450    #[test]
451    fn test_session_status_idle_with_max_tokens() {
452        let event = SessionEvent::StatusIdle {
453            seq: 9,
454            stop_reason: Some(StopReason::MaxTokens),
455            usage: None,
456        };
457        let value = serde_json::to_value(&event).unwrap();
458        assert_eq!(value["type"], "status.idle");
459        assert_eq!(value["seq"], 9);
460        assert_eq!(value["stop_reason"]["reason"], "max_tokens");
461    }
462
463    #[test]
464    fn test_session_error_serialization() {
465        let event = SessionEvent::Error {
466            code: "provider_error".to_string(),
467            message: "Model unavailable".to_string(),
468            seq: 10,
469        };
470        let value = serde_json::to_value(&event).unwrap();
471        assert_eq!(value["type"], "error");
472        assert_eq!(value["code"], "provider_error");
473        assert_eq!(value["message"], "Model unavailable");
474        assert_eq!(value["seq"], 10);
475    }
476
477    #[test]
478    fn test_session_event_seq_strictly_increasing() {
479        // Simulate a sequence of events with strictly increasing seq values
480        let events = vec![
481            SessionEvent::StatusRunning { seq: 0 },
482            SessionEvent::Message {
483                content: vec![ContentBlock::Text { text: "Hello".to_string() }],
484                seq: 1,
485            },
486            SessionEvent::ToolUse {
487                tool_use_id: "tu_1".to_string(),
488                name: "search".to_string(),
489                input: json!({}),
490                seq: 2,
491            },
492            SessionEvent::CustomToolUse {
493                custom_tool_use_id: "ctu_1".to_string(),
494                name: "deploy".to_string(),
495                input: json!({}),
496                seq: 3,
497            },
498            SessionEvent::McpToolUse {
499                tool_use_id: "mcp_1".to_string(),
500                name: "read".to_string(),
501                input: json!({}),
502                seq: 4,
503            },
504            SessionEvent::StatusIdle {
505                seq: 5,
506                stop_reason: Some(StopReason::EndTurn),
507                usage: None,
508            },
509        ];
510
511        // Extract seq values and verify strict monotonic increase
512        let seqs: Vec<u64> = events
513            .iter()
514            .map(|e| match e {
515                SessionEvent::StatusRunning { seq }
516                | SessionEvent::Message { seq, .. }
517                | SessionEvent::ToolUse { seq, .. }
518                | SessionEvent::CustomToolUse { seq, .. }
519                | SessionEvent::McpToolUse { seq, .. }
520                | SessionEvent::StatusIdle { seq, .. }
521                | SessionEvent::Error { seq, .. } => *seq,
522            })
523            .collect();
524
525        for window in seqs.windows(2) {
526            assert!(
527                window[1] > window[0],
528                "seq must be strictly increasing: {} should be > {}",
529                window[1],
530                window[0]
531            );
532        }
533    }
534
535    #[test]
536    fn test_session_event_round_trip() {
537        let event = SessionEvent::CustomToolUse {
538            custom_tool_use_id: "ctu_rt".to_string(),
539            name: "execute".to_string(),
540            input: json!({"command": "ls -la"}),
541            seq: 42,
542        };
543        let json = serde_json::to_string(&event).unwrap();
544        let deserialized: SessionEvent = serde_json::from_str(&json).unwrap();
545        match deserialized {
546            SessionEvent::CustomToolUse { custom_tool_use_id, name, input, seq } => {
547                assert_eq!(custom_tool_use_id, "ctu_rt");
548                assert_eq!(name, "execute");
549                assert_eq!(input["command"], "ls -la");
550                assert_eq!(seq, 42);
551            }
552            _ => panic!("Expected CustomToolUse variant"),
553        }
554    }
555
556    // ─── StopReason Tests ────────────────────────────────────────────────────
557
558    #[test]
559    fn test_stop_reason_end_turn_serialization() {
560        let reason = StopReason::EndTurn;
561        let value = serde_json::to_value(&reason).unwrap();
562        assert_eq!(value, json!({"reason": "end_turn"}));
563    }
564
565    #[test]
566    fn test_stop_reason_requires_action_serialization() {
567        let reason = StopReason::RequiresAction {
568            event_ids: vec!["evt_a".to_string(), "evt_b".to_string()],
569        };
570        let value = serde_json::to_value(&reason).unwrap();
571        assert_eq!(value, json!({"reason": "requires_action", "event_ids": ["evt_a", "evt_b"]}));
572    }
573
574    #[test]
575    fn test_stop_reason_max_tokens_serialization() {
576        let reason = StopReason::MaxTokens;
577        let value = serde_json::to_value(&reason).unwrap();
578        assert_eq!(value, json!({"reason": "max_tokens"}));
579    }
580
581    #[test]
582    fn test_stop_reason_round_trip() {
583        let reasons = vec![
584            StopReason::EndTurn,
585            StopReason::RequiresAction { event_ids: vec!["id1".to_string()] },
586            StopReason::MaxTokens,
587        ];
588        for reason in reasons {
589            let json = serde_json::to_string(&reason).unwrap();
590            let deserialized: StopReason = serde_json::from_str(&json).unwrap();
591            let re_serialized = serde_json::to_string(&deserialized).unwrap();
592            assert_eq!(json, re_serialized);
593        }
594    }
595}