Skip to main content

adk_ui/interop/
ag_ui.rs

1use super::surface::UiSurface;
2use crate::notifications::UiNotification;
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5
6/// Event name used for surface payload transport via AG-UI custom events.
7pub const ADK_UI_SURFACE_EVENT_NAME: &str = "adk.ui.surface";
8
9#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
10#[serde(rename_all = "camelCase")]
11pub struct AgUiIdentityCapabilities {
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub name: Option<String>,
14    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
15    pub agent_type: Option<String>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub description: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub version: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub provider: Option<String>,
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub documentation_url: Option<String>,
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub metadata: Option<Value>,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
29#[serde(rename_all = "camelCase")]
30pub struct AgUiTransportCapabilities {
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub streaming: Option<bool>,
33    #[serde(skip_serializing_if = "Option::is_none")]
34    pub websocket: Option<bool>,
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub http_binary: Option<bool>,
37    #[serde(skip_serializing_if = "Option::is_none")]
38    pub push_notifications: Option<bool>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub resumable: Option<bool>,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
44#[serde(rename_all = "camelCase")]
45pub struct AgUiToolsCapabilities {
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub supported: Option<bool>,
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub items: Option<Vec<Value>>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub parallel_calls: Option<bool>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub client_provided: Option<bool>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
57#[serde(rename_all = "camelCase")]
58pub struct AgUiOutputCapabilities {
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub structured_output: Option<bool>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub supported_mime_types: Option<Vec<String>>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
66#[serde(rename_all = "camelCase")]
67pub struct AgUiStateCapabilities {
68    #[serde(skip_serializing_if = "Option::is_none")]
69    pub snapshots: Option<bool>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub deltas: Option<bool>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub memory: Option<bool>,
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub persistent_state: Option<bool>,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
79#[serde(rename_all = "camelCase")]
80pub struct AgUiSubAgentInfo {
81    pub name: String,
82    #[serde(skip_serializing_if = "Option::is_none")]
83    pub description: Option<String>,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
87#[serde(rename_all = "camelCase")]
88pub struct AgUiMultiAgentCapabilities {
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub supported: Option<bool>,
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub delegation: Option<bool>,
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub handoffs: Option<bool>,
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub sub_agents: Option<Vec<AgUiSubAgentInfo>>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
100#[serde(rename_all = "camelCase")]
101pub struct AgUiReasoningCapabilities {
102    #[serde(skip_serializing_if = "Option::is_none")]
103    pub supported: Option<bool>,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub streaming: Option<bool>,
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub encrypted: Option<bool>,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
111#[serde(rename_all = "camelCase")]
112pub struct AgUiMultimodalInputCapabilities {
113    #[serde(skip_serializing_if = "Option::is_none")]
114    pub image: Option<bool>,
115    #[serde(skip_serializing_if = "Option::is_none")]
116    pub audio: Option<bool>,
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub video: Option<bool>,
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub pdf: Option<bool>,
121    #[serde(skip_serializing_if = "Option::is_none")]
122    pub file: Option<bool>,
123}
124
125#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
126#[serde(rename_all = "camelCase")]
127pub struct AgUiMultimodalOutputCapabilities {
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub image: Option<bool>,
130    #[serde(skip_serializing_if = "Option::is_none")]
131    pub audio: Option<bool>,
132}
133
134#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
135#[serde(rename_all = "camelCase")]
136pub struct AgUiMultimodalCapabilities {
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub input: Option<AgUiMultimodalInputCapabilities>,
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub output: Option<AgUiMultimodalOutputCapabilities>,
141}
142
143#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
144#[serde(rename_all = "camelCase")]
145pub struct AgUiExecutionCapabilities {
146    #[serde(skip_serializing_if = "Option::is_none")]
147    pub code_execution: Option<bool>,
148    #[serde(skip_serializing_if = "Option::is_none")]
149    pub sandboxed: Option<bool>,
150    #[serde(skip_serializing_if = "Option::is_none")]
151    pub max_iterations: Option<u64>,
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub max_execution_time: Option<u64>,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
157#[serde(rename_all = "camelCase")]
158pub struct AgUiHumanInTheLoopCapabilities {
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub supported: Option<bool>,
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub approvals: Option<bool>,
163    #[serde(skip_serializing_if = "Option::is_none")]
164    pub interventions: Option<bool>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub feedback: Option<bool>,
167    #[serde(skip_serializing_if = "Option::is_none")]
168    pub interrupts: Option<bool>,
169    #[serde(skip_serializing_if = "Option::is_none")]
170    pub approve_with_edits: Option<bool>,
171}
172
173/// Current AG-UI capability snapshot. All fields are optional by design:
174/// absence means unknown rather than unsupported.
175#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
176#[serde(rename_all = "camelCase")]
177pub struct AgUiAgentCapabilities {
178    #[serde(skip_serializing_if = "Option::is_none")]
179    pub identity: Option<AgUiIdentityCapabilities>,
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub transport: Option<AgUiTransportCapabilities>,
182    #[serde(skip_serializing_if = "Option::is_none")]
183    pub tools: Option<AgUiToolsCapabilities>,
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub output: Option<AgUiOutputCapabilities>,
186    #[serde(skip_serializing_if = "Option::is_none")]
187    pub state: Option<AgUiStateCapabilities>,
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub multi_agent: Option<AgUiMultiAgentCapabilities>,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    pub reasoning: Option<AgUiReasoningCapabilities>,
192    #[serde(skip_serializing_if = "Option::is_none")]
193    pub multimodal: Option<AgUiMultimodalCapabilities>,
194    #[serde(skip_serializing_if = "Option::is_none")]
195    pub execution: Option<AgUiExecutionCapabilities>,
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub human_in_the_loop: Option<AgUiHumanInTheLoopCapabilities>,
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub custom: Option<Value>,
200}
201
202pub fn default_ag_ui_agent_capabilities() -> AgUiAgentCapabilities {
203    AgUiAgentCapabilities {
204        identity: Some(AgUiIdentityCapabilities {
205            name: Some("ADK UI example agent".to_string()),
206            agent_type: Some("adk-rust".to_string()),
207            description: Some("Protocol-aware generative UI agent".to_string()),
208            version: Some(env!("CARGO_PKG_VERSION").to_string()),
209            provider: Some("Zavora AI".to_string()),
210            documentation_url: Some("https://github.com/zavora-ai/adk-ui".to_string()),
211            metadata: None,
212        }),
213        transport: Some(AgUiTransportCapabilities {
214            streaming: Some(true),
215            websocket: Some(false),
216            http_binary: Some(false),
217            push_notifications: Some(true),
218            resumable: Some(false),
219        }),
220        tools: Some(AgUiToolsCapabilities {
221            supported: Some(true),
222            items: None,
223            parallel_calls: None,
224            client_provided: Some(false),
225        }),
226        output: Some(AgUiOutputCapabilities {
227            structured_output: Some(true),
228            supported_mime_types: Some(vec![
229                "text/event-stream".to_string(),
230                "application/json".to_string(),
231            ]),
232        }),
233        state: Some(AgUiStateCapabilities {
234            snapshots: Some(true),
235            deltas: Some(true),
236            memory: None,
237            persistent_state: Some(true),
238        }),
239        reasoning: Some(AgUiReasoningCapabilities {
240            supported: Some(true),
241            streaming: Some(true),
242            encrypted: Some(false),
243        }),
244        human_in_the_loop: Some(AgUiHumanInTheLoopCapabilities {
245            supported: Some(true),
246            approvals: None,
247            interventions: None,
248            feedback: Some(true),
249            interrupts: Some(false),
250            approve_with_edits: Some(false),
251        }),
252        custom: Some(json!({
253            "adkUiSurfaceEvents": ["ACTIVITY_SNAPSHOT", "CUSTOM"],
254            "interruptWireTypes": true,
255            "durableInterruptRuntime": false,
256            "adkUiNotificationChannel": true,
257        })),
258        ..Default::default()
259    }
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
263#[serde(rename_all = "camelCase")]
264pub struct AgUiInterrupt {
265    pub id: String,
266    pub reason: String,
267    #[serde(skip_serializing_if = "Option::is_none")]
268    pub message: Option<String>,
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub tool_call_id: Option<String>,
271    #[serde(skip_serializing_if = "Option::is_none")]
272    pub response_schema: Option<Value>,
273    #[serde(skip_serializing_if = "Option::is_none")]
274    pub expires_at: Option<String>,
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub metadata: Option<Value>,
277}
278
279#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
280#[serde(rename_all = "lowercase")]
281pub enum AgUiResumeStatus {
282    Resolved,
283    Cancelled,
284}
285
286#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
287#[serde(rename_all = "camelCase")]
288pub struct AgUiResumeEntry {
289    pub interrupt_id: String,
290    pub status: AgUiResumeStatus,
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub payload: Option<Value>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
296#[serde(rename_all = "camelCase")]
297pub struct AgUiRunAgentInput {
298    pub thread_id: String,
299    pub run_id: String,
300    #[serde(skip_serializing_if = "Option::is_none")]
301    pub parent_run_id: Option<String>,
302    pub state: Value,
303    pub messages: Vec<Value>,
304    pub tools: Vec<Value>,
305    pub context: Vec<Value>,
306    pub forwarded_props: Value,
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub resume: Option<Vec<AgUiResumeEntry>>,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
312#[serde(tag = "type", rename_all = "lowercase")]
313pub enum AgUiRunFinishedOutcome {
314    Success,
315    Interrupt { interrupts: Vec<AgUiInterrupt> },
316}
317
318/// AG-UI event types from the protocol event model (current stable + reasoning).
319#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
320#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
321pub enum AgUiEventType {
322    RunStarted,
323    RunFinished,
324    RunError,
325    StepStarted,
326    StepFinished,
327    TextMessageStart,
328    TextMessageContent,
329    TextMessageDelta,
330    TextMessageEnd,
331    TextMessageChunk,
332    ToolCallStart,
333    ToolCallArgs,
334    ToolCallEnd,
335    ToolCallResult,
336    ToolCallChunk,
337    StateSnapshot,
338    StateDelta,
339    MessagesSnapshot,
340    ActivitySnapshot,
341    ActivityDelta,
342    ReasoningStart,
343    ReasoningMessageStart,
344    ReasoningMessageContent,
345    ReasoningMessageEnd,
346    ReasoningMessageChunk,
347    ReasoningEnd,
348    ReasoningEncryptedValue,
349    Error,
350    Raw,
351    Custom,
352}
353
354#[derive(Debug, Clone, Serialize, Deserialize)]
355#[serde(rename_all = "camelCase")]
356pub struct AgUiRunStartedEvent {
357    #[serde(rename = "type")]
358    pub event_type: AgUiEventType,
359    pub thread_id: String,
360    pub run_id: String,
361    #[serde(skip_serializing_if = "Option::is_none")]
362    pub parent_run_id: Option<String>,
363    #[serde(skip_serializing_if = "Option::is_none")]
364    pub input: Option<Value>,
365}
366
367#[derive(Debug, Clone, Serialize, Deserialize)]
368#[serde(rename_all = "camelCase")]
369pub struct AgUiRunFinishedEvent {
370    #[serde(rename = "type")]
371    pub event_type: AgUiEventType,
372    pub thread_id: String,
373    pub run_id: String,
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub result: Option<Value>,
376    #[serde(skip_serializing_if = "Option::is_none")]
377    pub outcome: Option<AgUiRunFinishedOutcome>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
381#[serde(rename_all = "camelCase")]
382pub struct AgUiRunErrorEvent {
383    #[serde(rename = "type")]
384    pub event_type: AgUiEventType,
385    pub message: String,
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub code: Option<String>,
388    /// Optional lineage fields retained for multi-run hosts.
389    #[serde(skip_serializing_if = "Option::is_none")]
390    pub thread_id: Option<String>,
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub run_id: Option<String>,
393}
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
396#[serde(rename_all = "camelCase")]
397pub struct AgUiCustomEvent {
398    #[serde(rename = "type")]
399    pub event_type: AgUiEventType,
400    pub name: String,
401    pub value: Value,
402    #[serde(skip_serializing_if = "Option::is_none")]
403    pub timestamp: Option<u64>,
404    #[serde(skip_serializing_if = "Option::is_none")]
405    pub raw_event: Option<Value>,
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
409#[serde(rename_all = "camelCase")]
410pub struct AgUiStepEvent {
411    #[serde(rename = "type")]
412    pub event_type: AgUiEventType,
413    /// Official AG-UI field (`stepName`).
414    pub step_name: String,
415    /// Optional run lineage for multi-thread hosts (not required by core AG-UI).
416    #[serde(skip_serializing_if = "Option::is_none")]
417    pub thread_id: Option<String>,
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub run_id: Option<String>,
420    #[serde(skip_serializing_if = "Option::is_none")]
421    pub step_id: Option<String>,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize)]
425#[serde(rename_all = "camelCase")]
426pub struct AgUiTextMessageStartEvent {
427    #[serde(rename = "type")]
428    pub event_type: AgUiEventType,
429    pub thread_id: String,
430    pub run_id: String,
431    pub message_id: String,
432    pub role: String,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
436#[serde(rename_all = "camelCase")]
437pub struct AgUiTextMessageDeltaEvent {
438    #[serde(rename = "type")]
439    pub event_type: AgUiEventType,
440    pub thread_id: String,
441    pub run_id: String,
442    pub message_id: String,
443    pub delta: String,
444}
445
446#[derive(Debug, Clone, Serialize, Deserialize)]
447#[serde(rename_all = "camelCase")]
448pub struct AgUiTextMessageChunkEvent {
449    #[serde(rename = "type")]
450    pub event_type: AgUiEventType,
451    pub thread_id: String,
452    pub run_id: String,
453    #[serde(skip_serializing_if = "Option::is_none")]
454    pub message_id: Option<String>,
455    #[serde(skip_serializing_if = "Option::is_none")]
456    pub role: Option<String>,
457    #[serde(skip_serializing_if = "Option::is_none")]
458    pub delta: Option<String>,
459}
460
461#[derive(Debug, Clone, Serialize, Deserialize)]
462#[serde(rename_all = "camelCase")]
463pub struct AgUiTextMessageEndEvent {
464    #[serde(rename = "type")]
465    pub event_type: AgUiEventType,
466    pub thread_id: String,
467    pub run_id: String,
468    pub message_id: String,
469}
470
471#[derive(Debug, Clone, Serialize, Deserialize)]
472#[serde(rename_all = "camelCase")]
473pub struct AgUiToolCallStartEvent {
474    #[serde(rename = "type")]
475    pub event_type: AgUiEventType,
476    pub thread_id: String,
477    pub run_id: String,
478    pub tool_call_id: String,
479    pub tool_call_name: String,
480    #[serde(skip_serializing_if = "Option::is_none")]
481    pub parent_message_id: Option<String>,
482}
483
484#[derive(Debug, Clone, Serialize, Deserialize)]
485#[serde(rename_all = "camelCase")]
486pub struct AgUiToolCallArgsEvent {
487    #[serde(rename = "type")]
488    pub event_type: AgUiEventType,
489    pub thread_id: String,
490    pub run_id: String,
491    pub tool_call_id: String,
492    pub delta: String,
493}
494
495#[derive(Debug, Clone, Serialize, Deserialize)]
496#[serde(rename_all = "camelCase")]
497pub struct AgUiToolCallEndEvent {
498    #[serde(rename = "type")]
499    pub event_type: AgUiEventType,
500    pub thread_id: String,
501    pub run_id: String,
502    pub tool_call_id: String,
503}
504
505#[derive(Debug, Clone, Serialize, Deserialize)]
506#[serde(rename_all = "camelCase")]
507pub struct AgUiToolCallResultEvent {
508    #[serde(rename = "type")]
509    pub event_type: AgUiEventType,
510    pub thread_id: String,
511    pub run_id: String,
512    pub tool_call_id: String,
513    pub message_id: String,
514    pub content: String,
515    #[serde(skip_serializing_if = "Option::is_none")]
516    pub role: Option<String>,
517}
518
519#[derive(Debug, Clone, Serialize, Deserialize)]
520#[serde(rename_all = "camelCase")]
521pub struct AgUiToolCallChunkEvent {
522    #[serde(rename = "type")]
523    pub event_type: AgUiEventType,
524    pub thread_id: String,
525    pub run_id: String,
526    #[serde(skip_serializing_if = "Option::is_none")]
527    pub tool_call_id: Option<String>,
528    #[serde(skip_serializing_if = "Option::is_none")]
529    pub tool_call_name: Option<String>,
530    #[serde(skip_serializing_if = "Option::is_none")]
531    pub parent_message_id: Option<String>,
532    #[serde(skip_serializing_if = "Option::is_none")]
533    pub delta: Option<String>,
534}
535
536#[derive(Debug, Clone, Serialize, Deserialize)]
537#[serde(rename_all = "camelCase")]
538pub struct AgUiStateSnapshotEvent {
539    #[serde(rename = "type")]
540    pub event_type: AgUiEventType,
541    /// Official AG-UI field name.
542    pub snapshot: Value,
543    #[serde(skip_serializing_if = "Option::is_none")]
544    pub thread_id: Option<String>,
545    #[serde(skip_serializing_if = "Option::is_none")]
546    pub run_id: Option<String>,
547}
548
549#[derive(Debug, Clone, Serialize, Deserialize)]
550#[serde(rename_all = "camelCase")]
551pub struct AgUiStateDeltaEvent {
552    #[serde(rename = "type")]
553    pub event_type: AgUiEventType,
554    /// JSON Patch operations (RFC 6902).
555    pub delta: Value,
556    #[serde(skip_serializing_if = "Option::is_none")]
557    pub thread_id: Option<String>,
558    #[serde(skip_serializing_if = "Option::is_none")]
559    pub run_id: Option<String>,
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize)]
563#[serde(rename_all = "camelCase")]
564pub struct AgUiMessagesSnapshotEvent {
565    #[serde(rename = "type")]
566    pub event_type: AgUiEventType,
567    pub thread_id: String,
568    pub run_id: String,
569    pub messages: Vec<Value>,
570}
571
572#[derive(Debug, Clone, Serialize, Deserialize)]
573#[serde(rename_all = "camelCase")]
574pub struct AgUiActivitySnapshotEvent {
575    #[serde(rename = "type")]
576    pub event_type: AgUiEventType,
577    pub message_id: String,
578    pub activity_type: String,
579    pub content: Value,
580    #[serde(skip_serializing_if = "Option::is_none")]
581    pub replace: Option<bool>,
582    #[serde(skip_serializing_if = "Option::is_none")]
583    pub thread_id: Option<String>,
584    #[serde(skip_serializing_if = "Option::is_none")]
585    pub run_id: Option<String>,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize)]
589#[serde(rename_all = "camelCase")]
590pub struct AgUiActivityDeltaEvent {
591    #[serde(rename = "type")]
592    pub event_type: AgUiEventType,
593    pub message_id: String,
594    pub activity_type: String,
595    pub patch: Value,
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub thread_id: Option<String>,
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub run_id: Option<String>,
600}
601
602#[derive(Debug, Clone, Serialize, Deserialize)]
603#[serde(rename_all = "camelCase")]
604pub struct AgUiReasoningStartEvent {
605    #[serde(rename = "type")]
606    pub event_type: AgUiEventType,
607    pub message_id: String,
608}
609
610#[derive(Debug, Clone, Serialize, Deserialize)]
611#[serde(rename_all = "camelCase")]
612pub struct AgUiReasoningMessageStartEvent {
613    #[serde(rename = "type")]
614    pub event_type: AgUiEventType,
615    pub message_id: String,
616    pub role: String,
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize)]
620#[serde(rename_all = "camelCase")]
621pub struct AgUiReasoningMessageContentEvent {
622    #[serde(rename = "type")]
623    pub event_type: AgUiEventType,
624    pub message_id: String,
625    pub delta: String,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize)]
629#[serde(rename_all = "camelCase")]
630pub struct AgUiReasoningMessageEndEvent {
631    #[serde(rename = "type")]
632    pub event_type: AgUiEventType,
633    pub message_id: String,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize)]
637#[serde(rename_all = "camelCase")]
638pub struct AgUiReasoningMessageChunkEvent {
639    #[serde(rename = "type")]
640    pub event_type: AgUiEventType,
641    #[serde(skip_serializing_if = "Option::is_none")]
642    pub message_id: Option<String>,
643    #[serde(skip_serializing_if = "Option::is_none")]
644    pub delta: Option<String>,
645}
646
647#[derive(Debug, Clone, Serialize, Deserialize)]
648#[serde(rename_all = "camelCase")]
649pub struct AgUiReasoningEndEvent {
650    #[serde(rename = "type")]
651    pub event_type: AgUiEventType,
652    pub message_id: String,
653}
654
655#[derive(Debug, Clone, Serialize, Deserialize)]
656#[serde(rename_all = "camelCase")]
657pub struct AgUiReasoningEncryptedValueEvent {
658    #[serde(rename = "type")]
659    pub event_type: AgUiEventType,
660    pub subtype: String,
661    pub entity_id: String,
662    pub encrypted_value: String,
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize)]
666#[serde(rename_all = "camelCase")]
667pub struct AgUiErrorEvent {
668    #[serde(rename = "type")]
669    pub event_type: AgUiEventType,
670    pub thread_id: String,
671    pub run_id: String,
672    pub message: String,
673    pub recoverable: bool,
674    #[serde(skip_serializing_if = "Option::is_none")]
675    pub code: Option<String>,
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize)]
679#[serde(rename_all = "camelCase")]
680pub struct AgUiRawEvent {
681    #[serde(rename = "type")]
682    pub event_type: AgUiEventType,
683    pub event: Value,
684    #[serde(skip_serializing_if = "Option::is_none")]
685    pub source: Option<String>,
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize)]
689#[serde(untagged)]
690pub enum AgUiEvent {
691    RunStarted(AgUiRunStartedEvent),
692    RunError(AgUiRunErrorEvent),
693    StepStarted(AgUiStepEvent),
694    StepFinished(AgUiStepEvent),
695    TextMessageStart(AgUiTextMessageStartEvent),
696    TextMessageContent(AgUiTextMessageDeltaEvent),
697    TextMessageDelta(AgUiTextMessageDeltaEvent),
698    TextMessageChunk(AgUiTextMessageChunkEvent),
699    TextMessageEnd(AgUiTextMessageEndEvent),
700    ToolCallStart(AgUiToolCallStartEvent),
701    ToolCallArgs(AgUiToolCallArgsEvent),
702    ToolCallEnd(AgUiToolCallEndEvent),
703    ToolCallResult(AgUiToolCallResultEvent),
704    ToolCallChunk(AgUiToolCallChunkEvent),
705    StateSnapshot(AgUiStateSnapshotEvent),
706    StateDelta(AgUiStateDeltaEvent),
707    MessagesSnapshot(AgUiMessagesSnapshotEvent),
708    ActivitySnapshot(AgUiActivitySnapshotEvent),
709    ActivityDelta(AgUiActivityDeltaEvent),
710    ReasoningStart(AgUiReasoningStartEvent),
711    ReasoningMessageStart(AgUiReasoningMessageStartEvent),
712    ReasoningMessageContent(AgUiReasoningMessageContentEvent),
713    ReasoningMessageEnd(AgUiReasoningMessageEndEvent),
714    ReasoningMessageChunk(AgUiReasoningMessageChunkEvent),
715    ReasoningEnd(AgUiReasoningEndEvent),
716    ReasoningEncryptedValue(AgUiReasoningEncryptedValueEvent),
717    Error(AgUiErrorEvent),
718    Raw(AgUiRawEvent),
719    Custom(AgUiCustomEvent),
720    RunFinished(AgUiRunFinishedEvent),
721}
722
723pub fn run_started_event(thread_id: impl Into<String>, run_id: impl Into<String>) -> AgUiEvent {
724    AgUiEvent::RunStarted(AgUiRunStartedEvent {
725        event_type: AgUiEventType::RunStarted,
726        thread_id: thread_id.into(),
727        run_id: run_id.into(),
728        parent_run_id: None,
729        input: None,
730    })
731}
732
733pub fn run_finished_event(
734    thread_id: impl Into<String>,
735    run_id: impl Into<String>,
736    result: Option<Value>,
737) -> AgUiEvent {
738    AgUiEvent::RunFinished(AgUiRunFinishedEvent {
739        event_type: AgUiEventType::RunFinished,
740        thread_id: thread_id.into(),
741        run_id: run_id.into(),
742        result,
743        outcome: None,
744    })
745}
746
747pub fn run_interrupted_event(
748    thread_id: impl Into<String>,
749    run_id: impl Into<String>,
750    interrupts: Vec<AgUiInterrupt>,
751) -> AgUiEvent {
752    AgUiEvent::RunFinished(AgUiRunFinishedEvent {
753        event_type: AgUiEventType::RunFinished,
754        thread_id: thread_id.into(),
755        run_id: run_id.into(),
756        result: None,
757        outcome: Some(AgUiRunFinishedOutcome::Interrupt { interrupts }),
758    })
759}
760
761pub fn step_started_event(
762    thread_id: impl Into<String>,
763    run_id: impl Into<String>,
764    step_id: impl Into<String>,
765    name: Option<String>,
766) -> AgUiEvent {
767    let step_id = step_id.into();
768    let step_name = name.unwrap_or_else(|| step_id.clone());
769    AgUiEvent::StepStarted(AgUiStepEvent {
770        event_type: AgUiEventType::StepStarted,
771        step_name,
772        thread_id: Some(thread_id.into()),
773        run_id: Some(run_id.into()),
774        step_id: Some(step_id),
775    })
776}
777
778pub fn step_finished_event(
779    thread_id: impl Into<String>,
780    run_id: impl Into<String>,
781    step_id: impl Into<String>,
782    name: Option<String>,
783) -> AgUiEvent {
784    let step_id = step_id.into();
785    let step_name = name.unwrap_or_else(|| step_id.clone());
786    AgUiEvent::StepFinished(AgUiStepEvent {
787        event_type: AgUiEventType::StepFinished,
788        step_name,
789        thread_id: Some(thread_id.into()),
790        run_id: Some(run_id.into()),
791        step_id: Some(step_id),
792    })
793}
794
795pub fn text_message_events(
796    thread_id: impl Into<String>,
797    run_id: impl Into<String>,
798    message_id: impl Into<String>,
799    role: impl Into<String>,
800    delta: impl Into<String>,
801) -> Vec<AgUiEvent> {
802    let thread_id = thread_id.into();
803    let run_id = run_id.into();
804    let message_id = message_id.into();
805    let role = role.into();
806    let delta = delta.into();
807
808    vec![
809        AgUiEvent::TextMessageStart(AgUiTextMessageStartEvent {
810            event_type: AgUiEventType::TextMessageStart,
811            thread_id: thread_id.clone(),
812            run_id: run_id.clone(),
813            message_id: message_id.clone(),
814            role,
815        }),
816        AgUiEvent::TextMessageContent(AgUiTextMessageDeltaEvent {
817            event_type: AgUiEventType::TextMessageContent,
818            thread_id: thread_id.clone(),
819            run_id: run_id.clone(),
820            message_id: message_id.clone(),
821            delta,
822        }),
823        AgUiEvent::TextMessageEnd(AgUiTextMessageEndEvent {
824            event_type: AgUiEventType::TextMessageEnd,
825            thread_id,
826            run_id,
827            message_id,
828        }),
829    ]
830}
831
832pub fn text_message_chunk_event(
833    thread_id: impl Into<String>,
834    run_id: impl Into<String>,
835    message_id: Option<String>,
836    role: Option<String>,
837    delta: Option<String>,
838) -> AgUiEvent {
839    AgUiEvent::TextMessageChunk(AgUiTextMessageChunkEvent {
840        event_type: AgUiEventType::TextMessageChunk,
841        thread_id: thread_id.into(),
842        run_id: run_id.into(),
843        message_id,
844        role,
845        delta,
846    })
847}
848
849pub fn tool_call_events(
850    thread_id: impl Into<String>,
851    run_id: impl Into<String>,
852    tool_call_id: impl Into<String>,
853    name: impl Into<String>,
854    args: Value,
855    result: Value,
856    is_error: bool,
857) -> Vec<AgUiEvent> {
858    let thread_id = thread_id.into();
859    let run_id = run_id.into();
860    let tool_call_id = tool_call_id.into();
861    let name = name.into();
862    let args_delta = serde_json::to_string(&args).unwrap_or_else(|_| args.to_string());
863    let result_content = serde_json::to_string(&if is_error {
864        json!({
865            "is_error": true,
866            "result": result,
867        })
868    } else {
869        result
870    })
871    .unwrap_or_else(|_| "\"\"".to_string());
872    let message_id = format!("msg-{}", tool_call_id);
873
874    vec![
875        AgUiEvent::ToolCallStart(AgUiToolCallStartEvent {
876            event_type: AgUiEventType::ToolCallStart,
877            thread_id: thread_id.clone(),
878            run_id: run_id.clone(),
879            tool_call_id: tool_call_id.clone(),
880            tool_call_name: name,
881            parent_message_id: None,
882        }),
883        AgUiEvent::ToolCallArgs(AgUiToolCallArgsEvent {
884            event_type: AgUiEventType::ToolCallArgs,
885            thread_id: thread_id.clone(),
886            run_id: run_id.clone(),
887            tool_call_id: tool_call_id.clone(),
888            delta: args_delta,
889        }),
890        AgUiEvent::ToolCallEnd(AgUiToolCallEndEvent {
891            event_type: AgUiEventType::ToolCallEnd,
892            thread_id: thread_id.clone(),
893            run_id: run_id.clone(),
894            tool_call_id: tool_call_id.clone(),
895        }),
896        AgUiEvent::ToolCallResult(AgUiToolCallResultEvent {
897            event_type: AgUiEventType::ToolCallResult,
898            thread_id,
899            run_id,
900            tool_call_id,
901            message_id,
902            content: result_content,
903            role: Some("tool".to_string()),
904        }),
905    ]
906}
907
908pub fn tool_call_chunk_event(
909    thread_id: impl Into<String>,
910    run_id: impl Into<String>,
911    tool_call_id: Option<String>,
912    tool_call_name: Option<String>,
913    parent_message_id: Option<String>,
914    delta: Option<String>,
915) -> AgUiEvent {
916    AgUiEvent::ToolCallChunk(AgUiToolCallChunkEvent {
917        event_type: AgUiEventType::ToolCallChunk,
918        thread_id: thread_id.into(),
919        run_id: run_id.into(),
920        tool_call_id,
921        tool_call_name,
922        parent_message_id,
923        delta,
924    })
925}
926
927pub fn state_snapshot_event(
928    thread_id: impl Into<String>,
929    run_id: impl Into<String>,
930    state: Value,
931) -> AgUiEvent {
932    AgUiEvent::StateSnapshot(AgUiStateSnapshotEvent {
933        event_type: AgUiEventType::StateSnapshot,
934        snapshot: state,
935        thread_id: Some(thread_id.into()),
936        run_id: Some(run_id.into()),
937    })
938}
939
940pub fn state_delta_event(
941    thread_id: impl Into<String>,
942    run_id: impl Into<String>,
943    delta: Value,
944) -> AgUiEvent {
945    AgUiEvent::StateDelta(AgUiStateDeltaEvent {
946        event_type: AgUiEventType::StateDelta,
947        delta,
948        thread_id: Some(thread_id.into()),
949        run_id: Some(run_id.into()),
950    })
951}
952
953pub fn error_event(
954    thread_id: impl Into<String>,
955    run_id: impl Into<String>,
956    message: impl Into<String>,
957    code: Option<String>,
958    recoverable: bool,
959) -> AgUiEvent {
960    AgUiEvent::Error(AgUiErrorEvent {
961        event_type: AgUiEventType::Error,
962        thread_id: thread_id.into(),
963        run_id: run_id.into(),
964        message: message.into(),
965        recoverable,
966        code,
967    })
968}
969
970pub fn run_error_event(
971    thread_id: impl Into<String>,
972    run_id: impl Into<String>,
973    message: impl Into<String>,
974    code: Option<String>,
975) -> AgUiEvent {
976    AgUiEvent::RunError(AgUiRunErrorEvent {
977        event_type: AgUiEventType::RunError,
978        message: message.into(),
979        code,
980        thread_id: Some(thread_id.into()),
981        run_id: Some(run_id.into()),
982    })
983}
984
985pub fn messages_snapshot_event(
986    thread_id: impl Into<String>,
987    run_id: impl Into<String>,
988    messages: Vec<Value>,
989) -> AgUiEvent {
990    AgUiEvent::MessagesSnapshot(AgUiMessagesSnapshotEvent {
991        event_type: AgUiEventType::MessagesSnapshot,
992        thread_id: thread_id.into(),
993        run_id: run_id.into(),
994        messages,
995    })
996}
997
998pub fn activity_snapshot_event(
999    thread_id: impl Into<String>,
1000    run_id: impl Into<String>,
1001    message_id: impl Into<String>,
1002    activity_type: impl Into<String>,
1003    content: Value,
1004    replace: Option<bool>,
1005) -> AgUiEvent {
1006    AgUiEvent::ActivitySnapshot(AgUiActivitySnapshotEvent {
1007        event_type: AgUiEventType::ActivitySnapshot,
1008        message_id: message_id.into(),
1009        activity_type: activity_type.into(),
1010        content,
1011        replace,
1012        thread_id: Some(thread_id.into()),
1013        run_id: Some(run_id.into()),
1014    })
1015}
1016
1017pub fn activity_delta_event(
1018    thread_id: impl Into<String>,
1019    run_id: impl Into<String>,
1020    message_id: impl Into<String>,
1021    activity_type: impl Into<String>,
1022    patch: Value,
1023) -> AgUiEvent {
1024    AgUiEvent::ActivityDelta(AgUiActivityDeltaEvent {
1025        event_type: AgUiEventType::ActivityDelta,
1026        message_id: message_id.into(),
1027        activity_type: activity_type.into(),
1028        patch,
1029        thread_id: Some(thread_id.into()),
1030        run_id: Some(run_id.into()),
1031    })
1032}
1033
1034/// Emit a full reasoning message triad (start → content → end).
1035pub fn reasoning_message_events(
1036    message_id: impl Into<String>,
1037    delta: impl Into<String>,
1038) -> Vec<AgUiEvent> {
1039    let message_id = message_id.into();
1040    let delta = delta.into();
1041    vec![
1042        AgUiEvent::ReasoningStart(AgUiReasoningStartEvent {
1043            event_type: AgUiEventType::ReasoningStart,
1044            message_id: message_id.clone(),
1045        }),
1046        AgUiEvent::ReasoningMessageStart(AgUiReasoningMessageStartEvent {
1047            event_type: AgUiEventType::ReasoningMessageStart,
1048            message_id: message_id.clone(),
1049            role: "reasoning".to_string(),
1050        }),
1051        AgUiEvent::ReasoningMessageContent(AgUiReasoningMessageContentEvent {
1052            event_type: AgUiEventType::ReasoningMessageContent,
1053            message_id: message_id.clone(),
1054            delta,
1055        }),
1056        AgUiEvent::ReasoningMessageEnd(AgUiReasoningMessageEndEvent {
1057            event_type: AgUiEventType::ReasoningMessageEnd,
1058            message_id: message_id.clone(),
1059        }),
1060        AgUiEvent::ReasoningEnd(AgUiReasoningEndEvent {
1061            event_type: AgUiEventType::ReasoningEnd,
1062            message_id,
1063        }),
1064    ]
1065}
1066
1067pub fn reasoning_message_chunk_event(
1068    message_id: Option<String>,
1069    delta: Option<String>,
1070) -> AgUiEvent {
1071    AgUiEvent::ReasoningMessageChunk(AgUiReasoningMessageChunkEvent {
1072        event_type: AgUiEventType::ReasoningMessageChunk,
1073        message_id,
1074        delta,
1075    })
1076}
1077
1078pub fn raw_event(event: Value, source: Option<String>) -> AgUiEvent {
1079    AgUiEvent::Raw(AgUiRawEvent {
1080        event_type: AgUiEventType::Raw,
1081        event,
1082        source,
1083    })
1084}
1085
1086/// Convert an out-of-turn UI notification into an AG-UI custom event.
1087pub fn notification_event(notification: &UiNotification) -> AgUiEvent {
1088    AgUiEvent::Custom(AgUiCustomEvent {
1089        event_type: AgUiEventType::Custom,
1090        name: "adk.ui.notification".to_string(),
1091        value: serde_json::to_value(notification).unwrap_or(Value::Null),
1092        timestamp: None,
1093        raw_event: None,
1094    })
1095}
1096
1097pub fn surface_to_custom_event(surface: &UiSurface) -> AgUiCustomEvent {
1098    AgUiCustomEvent {
1099        event_type: AgUiEventType::Custom,
1100        name: ADK_UI_SURFACE_EVENT_NAME.to_string(),
1101        value: json!({
1102            "format": "adk-ui-surface-v1",
1103            "surface": surface
1104        }),
1105        timestamp: None,
1106        raw_event: None,
1107    }
1108}
1109
1110pub fn surface_to_event_stream(
1111    surface: &UiSurface,
1112    thread_id: impl Into<String>,
1113    run_id: impl Into<String>,
1114) -> Vec<AgUiEvent> {
1115    let thread_id = thread_id.into();
1116    let run_id = run_id.into();
1117
1118    vec![
1119        AgUiEvent::RunStarted(AgUiRunStartedEvent {
1120            event_type: AgUiEventType::RunStarted,
1121            thread_id: thread_id.clone(),
1122            run_id: run_id.clone(),
1123            parent_run_id: None,
1124            input: None,
1125        }),
1126        // Prefer native activity for surface lifecycle; keep CUSTOM for legacy clients.
1127        activity_snapshot_event(
1128            thread_id.clone(),
1129            run_id.clone(),
1130            format!("activity-{}", surface.surface_id),
1131            "ADK_UI_SURFACE",
1132            json!({
1133                "format": "adk-ui-surface-v1",
1134                "surface": surface,
1135            }),
1136            Some(true),
1137        ),
1138        AgUiEvent::Custom(surface_to_custom_event(surface)),
1139        AgUiEvent::RunFinished(AgUiRunFinishedEvent {
1140            event_type: AgUiEventType::RunFinished,
1141            thread_id,
1142            run_id,
1143            result: None,
1144            outcome: Some(AgUiRunFinishedOutcome::Success),
1145        }),
1146    ]
1147}
1148
1149#[cfg(test)]
1150mod tests {
1151    use super::*;
1152    use serde_json::json;
1153
1154    #[test]
1155    fn surface_custom_event_is_well_formed() {
1156        let surface = UiSurface::new(
1157            "main",
1158            "catalog",
1159            vec![json!({"id":"root","component":{"Column":{"children":[]}}})],
1160        );
1161        let event = surface_to_custom_event(&surface);
1162        assert_eq!(event.event_type, AgUiEventType::Custom);
1163        assert_eq!(event.name, ADK_UI_SURFACE_EVENT_NAME);
1164        assert!(event.value.get("surface").is_some());
1165    }
1166
1167    #[test]
1168    fn event_stream_wraps_custom_event_with_lifecycle() {
1169        let surface = UiSurface::new(
1170            "main",
1171            "catalog",
1172            vec![json!({"id":"root","component":{"Column":{"children":[]}}})],
1173        );
1174        let stream = surface_to_event_stream(&surface, "thread-1", "run-1");
1175        assert_eq!(stream.len(), 4);
1176
1177        let first = serde_json::to_value(&stream[0]).unwrap();
1178        let second = serde_json::to_value(&stream[1]).unwrap();
1179        let third = serde_json::to_value(&stream[2]).unwrap();
1180        let fourth = serde_json::to_value(&stream[3]).unwrap();
1181
1182        assert_eq!(first["type"], "RUN_STARTED");
1183        assert_eq!(second["type"], "ACTIVITY_SNAPSHOT");
1184        assert_eq!(second["activityType"], "ADK_UI_SURFACE");
1185        assert_eq!(third["type"], "CUSTOM");
1186        assert_eq!(fourth["type"], "RUN_FINISHED");
1187    }
1188
1189    #[test]
1190    fn state_snapshot_uses_official_snapshot_field() {
1191        let event = state_snapshot_event("t", "r", json!({"ui": "ok"}));
1192        let value = serde_json::to_value(event).unwrap();
1193        assert_eq!(value["type"], "STATE_SNAPSHOT");
1194        assert_eq!(value["snapshot"]["ui"], "ok");
1195        assert!(value.get("state").is_none());
1196    }
1197
1198    #[test]
1199    fn reasoning_helpers_emit_official_event_types() {
1200        let events = reasoning_message_events("msg-r1", "thinking...");
1201        let types: Vec<_> = events
1202            .iter()
1203            .map(|e| {
1204                serde_json::to_value(e).unwrap()["type"]
1205                    .as_str()
1206                    .unwrap()
1207                    .to_string()
1208            })
1209            .collect();
1210        assert!(types.contains(&"REASONING_START".to_string()));
1211        assert!(types.contains(&"REASONING_MESSAGE_CONTENT".to_string()));
1212        assert!(types.contains(&"REASONING_END".to_string()));
1213    }
1214
1215    #[test]
1216    fn text_message_helpers_emit_start_content_end() {
1217        let events = text_message_events("thread-1", "run-1", "msg-1", "assistant", "hello");
1218        assert_eq!(events.len(), 3);
1219
1220        let start = serde_json::to_value(&events[0]).unwrap();
1221        let content = serde_json::to_value(&events[1]).unwrap();
1222        let end = serde_json::to_value(&events[2]).unwrap();
1223
1224        assert_eq!(start["type"], "TEXT_MESSAGE_START");
1225        assert_eq!(content["type"], "TEXT_MESSAGE_CONTENT");
1226        assert_eq!(content["delta"], "hello");
1227        assert_eq!(end["type"], "TEXT_MESSAGE_END");
1228    }
1229
1230    #[test]
1231    fn tool_call_helpers_emit_lifecycle_and_result() {
1232        let events = tool_call_events(
1233            "thread-1",
1234            "run-1",
1235            "tool-1",
1236            "lookup_weather",
1237            json!({"city": "Nairobi"}),
1238            json!({"temp": 23}),
1239            false,
1240        );
1241
1242        assert_eq!(events.len(), 4);
1243        let start = serde_json::to_value(&events[0]).unwrap();
1244        let args = serde_json::to_value(&events[1]).unwrap();
1245        let end = serde_json::to_value(&events[2]).unwrap();
1246        let result = serde_json::to_value(&events[3]).unwrap();
1247
1248        assert_eq!(start["type"], "TOOL_CALL_START");
1249        assert_eq!(start["toolCallName"], "lookup_weather");
1250        assert_eq!(args["type"], "TOOL_CALL_ARGS");
1251        assert_eq!(args["delta"], "{\"city\":\"Nairobi\"}");
1252        assert_eq!(end["type"], "TOOL_CALL_END");
1253        assert_eq!(result["type"], "TOOL_CALL_RESULT");
1254        assert_eq!(result["content"], "{\"temp\":23}");
1255        assert_eq!(result["messageId"], "msg-tool-1");
1256        assert_eq!(result["role"], "tool");
1257    }
1258
1259    #[test]
1260    fn state_and_error_helpers_emit_expected_shapes() {
1261        let snapshot = state_snapshot_event("thread-1", "run-1", json!({"phase": "planning"}));
1262        let delta = state_delta_event("thread-1", "run-1", json!({"phase": "acting"}));
1263        let error = error_event(
1264            "thread-1",
1265            "run-1",
1266            "tool timeout",
1267            Some("TIMEOUT".to_string()),
1268            true,
1269        );
1270
1271        let snapshot_json = serde_json::to_value(snapshot).unwrap();
1272        let delta_json = serde_json::to_value(delta).unwrap();
1273        let error_json = serde_json::to_value(error).unwrap();
1274
1275        assert_eq!(snapshot_json["type"], "STATE_SNAPSHOT");
1276        assert_eq!(snapshot_json["snapshot"]["phase"], "planning");
1277        assert_eq!(delta_json["type"], "STATE_DELTA");
1278        assert_eq!(delta_json["delta"]["phase"], "acting");
1279        assert_eq!(error_json["type"], "ERROR");
1280        assert_eq!(error_json["code"], "TIMEOUT");
1281        assert_eq!(error_json["recoverable"], true);
1282    }
1283
1284    #[test]
1285    fn stable_ag_ui_helper_events_emit_expected_shapes() {
1286        let run_error = run_error_event("thread-1", "run-1", "boom", Some("FAIL".to_string()));
1287        let text_chunk = text_message_chunk_event(
1288            "thread-1",
1289            "run-1",
1290            Some("msg-1".to_string()),
1291            Some("assistant".to_string()),
1292            Some("partial".to_string()),
1293        );
1294        let tool_chunk = tool_call_chunk_event(
1295            "thread-1",
1296            "run-1",
1297            Some("tool-1".to_string()),
1298            Some("lookup_weather".to_string()),
1299            Some("msg-1".to_string()),
1300            Some("{\"city\":\"Nairobi\"}".to_string()),
1301        );
1302        let messages_snapshot = messages_snapshot_event(
1303            "thread-1",
1304            "run-1",
1305            vec![json!({"role":"assistant","content":"hello"})],
1306        );
1307        let activity_snapshot = activity_snapshot_event(
1308            "thread-1",
1309            "run-1",
1310            "activity-1",
1311            "PLAN",
1312            json!({"steps":[{"title":"Research"}]}),
1313            Some(true),
1314        );
1315        let activity_delta = activity_delta_event(
1316            "thread-1",
1317            "run-1",
1318            "activity-1",
1319            "PLAN",
1320            json!([{"op":"add","path":"/steps/1","value":{"title":"Implement"}}]),
1321        );
1322        let raw = raw_event(
1323            json!({"source":"legacy"}),
1324            Some("legacy-system".to_string()),
1325        );
1326
1327        let run_error_json = serde_json::to_value(run_error).unwrap();
1328        let text_chunk_json = serde_json::to_value(text_chunk).unwrap();
1329        let tool_chunk_json = serde_json::to_value(tool_chunk).unwrap();
1330        let messages_snapshot_json = serde_json::to_value(messages_snapshot).unwrap();
1331        let activity_snapshot_json = serde_json::to_value(activity_snapshot).unwrap();
1332        let activity_delta_json = serde_json::to_value(activity_delta).unwrap();
1333        let raw_json = serde_json::to_value(raw).unwrap();
1334
1335        assert_eq!(run_error_json["type"], "RUN_ERROR");
1336        assert_eq!(run_error_json["message"], "boom");
1337        assert_eq!(text_chunk_json["type"], "TEXT_MESSAGE_CHUNK");
1338        assert_eq!(tool_chunk_json["type"], "TOOL_CALL_CHUNK");
1339        assert_eq!(messages_snapshot_json["type"], "MESSAGES_SNAPSHOT");
1340        assert_eq!(activity_snapshot_json["type"], "ACTIVITY_SNAPSHOT");
1341        assert_eq!(activity_delta_json["type"], "ACTIVITY_DELTA");
1342        assert_eq!(raw_json["type"], "RAW");
1343    }
1344
1345    #[test]
1346    fn capability_snapshot_uses_current_typed_categories() {
1347        let capabilities = serde_json::to_value(default_ag_ui_agent_capabilities()).unwrap();
1348
1349        assert_eq!(capabilities["transport"]["streaming"], true);
1350        assert_eq!(capabilities["transport"]["resumable"], false);
1351        assert_eq!(capabilities["state"]["snapshots"], true);
1352        assert_eq!(capabilities["reasoning"]["encrypted"], false);
1353        assert_eq!(capabilities["humanInTheLoop"]["interrupts"], false);
1354        assert_eq!(capabilities["custom"]["interruptWireTypes"], true);
1355    }
1356
1357    #[test]
1358    fn interrupt_outcome_and_resume_entry_match_ag_ui_wire_shape() {
1359        let event = run_interrupted_event(
1360            "thread-1",
1361            "run-1",
1362            vec![AgUiInterrupt {
1363                id: "interrupt-1".to_string(),
1364                reason: "confirmation".to_string(),
1365                message: Some("Continue?".to_string()),
1366                tool_call_id: None,
1367                response_schema: Some(json!({"type":"boolean"})),
1368                expires_at: None,
1369                metadata: None,
1370            }],
1371        );
1372        let event_json = serde_json::to_value(event).unwrap();
1373        assert_eq!(event_json["type"], "RUN_FINISHED");
1374        assert_eq!(event_json["outcome"]["type"], "interrupt");
1375        assert_eq!(event_json["outcome"]["interrupts"][0]["id"], "interrupt-1");
1376
1377        let resume: AgUiResumeEntry = serde_json::from_value(json!({
1378            "interruptId": "interrupt-1",
1379            "status": "resolved",
1380            "payload": {"approved": true}
1381        }))
1382        .unwrap();
1383        assert_eq!(resume.status, AgUiResumeStatus::Resolved);
1384    }
1385}