1use super::surface::UiSurface;
2use crate::notifications::UiNotification;
3use serde::{Deserialize, Serialize};
4use serde_json::{Value, json};
5
6pub 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#[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#[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 #[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 pub step_name: String,
415 #[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 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 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
1034pub 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
1086pub 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 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}