1use crate::types::Role;
2use serde::{Deserialize, Serialize};
3use serde_json::{json, Value};
4use std::collections::HashMap;
5use tirea_contract::Suspension;
6use tracing::warn;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
13pub struct BaseEvent {
14 #[serde(skip_serializing_if = "Option::is_none")]
16 pub timestamp: Option<u64>,
17 #[serde(rename = "rawEvent", skip_serializing_if = "Option::is_none")]
19 pub raw_event: Option<Value>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
24#[serde(rename_all = "kebab-case")]
25pub enum ReasoningEncryptedValueSubtype {
26 ToolCall,
27 Message,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
39#[serde(tag = "type")]
40pub enum Event {
41 #[serde(rename = "RUN_STARTED")]
46 RunStarted {
47 #[serde(rename = "threadId")]
48 thread_id: String,
49 #[serde(rename = "runId")]
50 run_id: String,
51 #[serde(rename = "parentRunId", skip_serializing_if = "Option::is_none")]
52 parent_run_id: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 input: Option<Value>,
55 #[serde(flatten)]
56 base: BaseEvent,
57 },
58
59 #[serde(rename = "RUN_FINISHED")]
61 RunFinished {
62 #[serde(rename = "threadId")]
63 thread_id: String,
64 #[serde(rename = "runId")]
65 run_id: String,
66 #[serde(skip_serializing_if = "Option::is_none")]
67 result: Option<Value>,
68 #[serde(flatten)]
69 base: BaseEvent,
70 },
71
72 #[serde(rename = "RUN_ERROR")]
74 RunError {
75 message: String,
76 #[serde(skip_serializing_if = "Option::is_none")]
77 code: Option<String>,
78 #[serde(flatten)]
79 base: BaseEvent,
80 },
81
82 #[serde(rename = "STEP_STARTED")]
84 StepStarted {
85 #[serde(rename = "stepName")]
86 step_name: String,
87 #[serde(flatten)]
88 base: BaseEvent,
89 },
90
91 #[serde(rename = "STEP_FINISHED")]
93 StepFinished {
94 #[serde(rename = "stepName")]
95 step_name: String,
96 #[serde(flatten)]
97 base: BaseEvent,
98 },
99
100 #[serde(rename = "TEXT_MESSAGE_START")]
105 TextMessageStart {
106 #[serde(rename = "messageId")]
107 message_id: String,
108 role: Role,
110 #[serde(flatten)]
111 base: BaseEvent,
112 },
113
114 #[serde(rename = "TEXT_MESSAGE_CONTENT")]
116 TextMessageContent {
117 #[serde(rename = "messageId")]
118 message_id: String,
119 delta: String,
120 #[serde(flatten)]
121 base: BaseEvent,
122 },
123
124 #[serde(rename = "TEXT_MESSAGE_END")]
126 TextMessageEnd {
127 #[serde(rename = "messageId")]
128 message_id: String,
129 #[serde(flatten)]
130 base: BaseEvent,
131 },
132
133 #[serde(rename = "TEXT_MESSAGE_CHUNK")]
135 TextMessageChunk {
136 #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
137 message_id: Option<String>,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 role: Option<Role>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 delta: Option<String>,
142 #[serde(flatten)]
143 base: BaseEvent,
144 },
145
146 #[serde(rename = "REASONING_START")]
151 ReasoningStart {
152 #[serde(rename = "messageId")]
153 message_id: String,
154 #[serde(flatten)]
155 base: BaseEvent,
156 },
157
158 #[serde(rename = "REASONING_MESSAGE_START")]
160 ReasoningMessageStart {
161 #[serde(rename = "messageId")]
162 message_id: String,
163 role: Role,
164 #[serde(flatten)]
165 base: BaseEvent,
166 },
167
168 #[serde(rename = "REASONING_MESSAGE_CONTENT")]
170 ReasoningMessageContent {
171 #[serde(rename = "messageId")]
172 message_id: String,
173 delta: String,
174 #[serde(flatten)]
175 base: BaseEvent,
176 },
177
178 #[serde(rename = "REASONING_MESSAGE_END")]
180 ReasoningMessageEnd {
181 #[serde(rename = "messageId")]
182 message_id: String,
183 #[serde(flatten)]
184 base: BaseEvent,
185 },
186
187 #[serde(rename = "REASONING_MESSAGE_CHUNK")]
189 ReasoningMessageChunk {
190 #[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
191 message_id: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 delta: Option<String>,
194 #[serde(flatten)]
195 base: BaseEvent,
196 },
197
198 #[serde(rename = "REASONING_END")]
200 ReasoningEnd {
201 #[serde(rename = "messageId")]
202 message_id: String,
203 #[serde(flatten)]
204 base: BaseEvent,
205 },
206
207 #[serde(rename = "REASONING_ENCRYPTED_VALUE")]
209 ReasoningEncryptedValue {
210 subtype: ReasoningEncryptedValueSubtype,
211 #[serde(rename = "entityId")]
212 entity_id: String,
213 #[serde(rename = "encryptedValue")]
214 encrypted_value: String,
215 #[serde(flatten)]
216 base: BaseEvent,
217 },
218
219 #[serde(rename = "TOOL_CALL_START")]
224 ToolCallStart {
225 #[serde(rename = "toolCallId")]
226 tool_call_id: String,
227 #[serde(rename = "toolCallName")]
228 tool_call_name: String,
229 #[serde(rename = "parentMessageId", skip_serializing_if = "Option::is_none")]
230 parent_message_id: Option<String>,
231 #[serde(flatten)]
232 base: BaseEvent,
233 },
234
235 #[serde(rename = "TOOL_CALL_ARGS")]
237 ToolCallArgs {
238 #[serde(rename = "toolCallId")]
239 tool_call_id: String,
240 delta: String,
241 #[serde(flatten)]
242 base: BaseEvent,
243 },
244
245 #[serde(rename = "TOOL_CALL_END")]
247 ToolCallEnd {
248 #[serde(rename = "toolCallId")]
249 tool_call_id: String,
250 #[serde(flatten)]
251 base: BaseEvent,
252 },
253
254 #[serde(rename = "TOOL_CALL_RESULT")]
256 ToolCallResult {
257 #[serde(rename = "messageId")]
258 message_id: String,
259 #[serde(rename = "toolCallId")]
260 tool_call_id: String,
261 content: String,
262 #[serde(skip_serializing_if = "Option::is_none")]
263 role: Option<Role>,
264 #[serde(flatten)]
265 base: BaseEvent,
266 },
267
268 #[serde(rename = "TOOL_CALL_CHUNK")]
270 ToolCallChunk {
271 #[serde(rename = "toolCallId", skip_serializing_if = "Option::is_none")]
272 tool_call_id: Option<String>,
273 #[serde(rename = "toolCallName", skip_serializing_if = "Option::is_none")]
274 tool_call_name: Option<String>,
275 #[serde(rename = "parentMessageId", skip_serializing_if = "Option::is_none")]
276 parent_message_id: Option<String>,
277 #[serde(skip_serializing_if = "Option::is_none")]
278 delta: Option<String>,
279 #[serde(flatten)]
280 base: BaseEvent,
281 },
282
283 #[serde(rename = "STATE_SNAPSHOT")]
288 StateSnapshot {
289 snapshot: Value,
290 #[serde(flatten)]
291 base: BaseEvent,
292 },
293
294 #[serde(rename = "STATE_DELTA")]
296 StateDelta {
297 delta: Vec<Value>,
299 #[serde(flatten)]
300 base: BaseEvent,
301 },
302
303 #[serde(rename = "MESSAGES_SNAPSHOT")]
305 MessagesSnapshot {
306 messages: Vec<Value>,
307 #[serde(flatten)]
308 base: BaseEvent,
309 },
310
311 #[serde(rename = "ACTIVITY_SNAPSHOT")]
316 ActivitySnapshot {
317 #[serde(rename = "messageId")]
318 message_id: String,
319 #[serde(rename = "activityType")]
320 activity_type: String,
321 content: HashMap<String, Value>,
322 #[serde(skip_serializing_if = "Option::is_none")]
323 replace: Option<bool>,
324 #[serde(flatten)]
325 base: BaseEvent,
326 },
327
328 #[serde(rename = "ACTIVITY_DELTA")]
330 ActivityDelta {
331 #[serde(rename = "messageId")]
332 message_id: String,
333 #[serde(rename = "activityType")]
334 activity_type: String,
335 patch: Vec<Value>,
337 #[serde(flatten)]
338 base: BaseEvent,
339 },
340
341 #[serde(rename = "RAW")]
346 Raw {
347 event: Value,
348 #[serde(skip_serializing_if = "Option::is_none")]
349 source: Option<String>,
350 #[serde(flatten)]
351 base: BaseEvent,
352 },
353
354 #[serde(rename = "CUSTOM")]
356 Custom {
357 name: String,
358 value: Value,
359 #[serde(flatten)]
360 base: BaseEvent,
361 },
362}
363
364impl Event {
365 pub fn run_started(
371 thread_id: impl Into<String>,
372 run_id: impl Into<String>,
373 parent_run_id: Option<String>,
374 ) -> Self {
375 Self::RunStarted {
376 thread_id: thread_id.into(),
377 run_id: run_id.into(),
378 parent_run_id,
379 input: None,
380 base: BaseEvent::default(),
381 }
382 }
383
384 pub fn run_started_with_input(
386 thread_id: impl Into<String>,
387 run_id: impl Into<String>,
388 parent_run_id: Option<String>,
389 input: Value,
390 ) -> Self {
391 Self::RunStarted {
392 thread_id: thread_id.into(),
393 run_id: run_id.into(),
394 parent_run_id,
395 input: Some(input),
396 base: BaseEvent::default(),
397 }
398 }
399
400 pub fn run_finished(
402 thread_id: impl Into<String>,
403 run_id: impl Into<String>,
404 result: Option<Value>,
405 ) -> Self {
406 Self::RunFinished {
407 thread_id: thread_id.into(),
408 run_id: run_id.into(),
409 result,
410 base: BaseEvent::default(),
411 }
412 }
413
414 pub fn run_error(message: impl Into<String>, code: Option<String>) -> Self {
416 Self::RunError {
417 message: message.into(),
418 code,
419 base: BaseEvent::default(),
420 }
421 }
422
423 pub fn step_started(step_name: impl Into<String>) -> Self {
425 Self::StepStarted {
426 step_name: step_name.into(),
427 base: BaseEvent::default(),
428 }
429 }
430
431 pub fn step_finished(step_name: impl Into<String>) -> Self {
433 Self::StepFinished {
434 step_name: step_name.into(),
435 base: BaseEvent::default(),
436 }
437 }
438
439 pub fn text_message_start(message_id: impl Into<String>) -> Self {
445 Self::TextMessageStart {
446 message_id: message_id.into(),
447 role: Role::Assistant,
448 base: BaseEvent::default(),
449 }
450 }
451
452 pub fn text_message_content(message_id: impl Into<String>, delta: impl Into<String>) -> Self {
454 Self::TextMessageContent {
455 message_id: message_id.into(),
456 delta: delta.into(),
457 base: BaseEvent::default(),
458 }
459 }
460
461 pub fn text_message_end(message_id: impl Into<String>) -> Self {
463 Self::TextMessageEnd {
464 message_id: message_id.into(),
465 base: BaseEvent::default(),
466 }
467 }
468
469 pub fn text_message_chunk(
471 message_id: Option<String>,
472 role: Option<Role>,
473 delta: Option<String>,
474 ) -> Self {
475 Self::TextMessageChunk {
476 message_id,
477 role,
478 delta,
479 base: BaseEvent::default(),
480 }
481 }
482
483 pub fn reasoning_start(message_id: impl Into<String>) -> Self {
489 Self::ReasoningStart {
490 message_id: message_id.into(),
491 base: BaseEvent::default(),
492 }
493 }
494
495 pub fn reasoning_message_start(message_id: impl Into<String>) -> Self {
497 Self::ReasoningMessageStart {
498 message_id: message_id.into(),
499 role: Role::Assistant,
500 base: BaseEvent::default(),
501 }
502 }
503
504 pub fn reasoning_message_content(
506 message_id: impl Into<String>,
507 delta: impl Into<String>,
508 ) -> Self {
509 Self::ReasoningMessageContent {
510 message_id: message_id.into(),
511 delta: delta.into(),
512 base: BaseEvent::default(),
513 }
514 }
515
516 pub fn reasoning_message_end(message_id: impl Into<String>) -> Self {
518 Self::ReasoningMessageEnd {
519 message_id: message_id.into(),
520 base: BaseEvent::default(),
521 }
522 }
523
524 pub fn reasoning_message_chunk(message_id: Option<String>, delta: Option<String>) -> Self {
526 Self::ReasoningMessageChunk {
527 message_id,
528 delta,
529 base: BaseEvent::default(),
530 }
531 }
532
533 pub fn reasoning_end(message_id: impl Into<String>) -> Self {
535 Self::ReasoningEnd {
536 message_id: message_id.into(),
537 base: BaseEvent::default(),
538 }
539 }
540
541 pub fn reasoning_encrypted_value(
543 subtype: ReasoningEncryptedValueSubtype,
544 entity_id: impl Into<String>,
545 encrypted_value: impl Into<String>,
546 ) -> Self {
547 Self::ReasoningEncryptedValue {
548 subtype,
549 entity_id: entity_id.into(),
550 encrypted_value: encrypted_value.into(),
551 base: BaseEvent::default(),
552 }
553 }
554
555 pub fn tool_call_start(
561 tool_call_id: impl Into<String>,
562 tool_call_name: impl Into<String>,
563 parent_message_id: Option<String>,
564 ) -> Self {
565 Self::ToolCallStart {
566 tool_call_id: tool_call_id.into(),
567 tool_call_name: tool_call_name.into(),
568 parent_message_id,
569 base: BaseEvent::default(),
570 }
571 }
572
573 pub fn tool_call_args(tool_call_id: impl Into<String>, delta: impl Into<String>) -> Self {
575 Self::ToolCallArgs {
576 tool_call_id: tool_call_id.into(),
577 delta: delta.into(),
578 base: BaseEvent::default(),
579 }
580 }
581
582 pub fn tool_call_end(tool_call_id: impl Into<String>) -> Self {
584 Self::ToolCallEnd {
585 tool_call_id: tool_call_id.into(),
586 base: BaseEvent::default(),
587 }
588 }
589
590 pub fn tool_call_result(
592 message_id: impl Into<String>,
593 tool_call_id: impl Into<String>,
594 content: impl Into<String>,
595 ) -> Self {
596 Self::ToolCallResult {
597 message_id: message_id.into(),
598 tool_call_id: tool_call_id.into(),
599 content: content.into(),
600 role: Some(Role::Tool),
601 base: BaseEvent::default(),
602 }
603 }
604
605 pub fn tool_call_chunk(
607 tool_call_id: Option<String>,
608 tool_call_name: Option<String>,
609 parent_message_id: Option<String>,
610 delta: Option<String>,
611 ) -> Self {
612 Self::ToolCallChunk {
613 tool_call_id,
614 tool_call_name,
615 parent_message_id,
616 delta,
617 base: BaseEvent::default(),
618 }
619 }
620
621 pub fn state_snapshot(snapshot: Value) -> Self {
627 Self::StateSnapshot {
628 snapshot,
629 base: BaseEvent::default(),
630 }
631 }
632
633 pub fn state_delta(delta: Vec<Value>) -> Self {
635 Self::StateDelta {
636 delta,
637 base: BaseEvent::default(),
638 }
639 }
640
641 pub fn messages_snapshot(messages: Vec<Value>) -> Self {
643 Self::MessagesSnapshot {
644 messages,
645 base: BaseEvent::default(),
646 }
647 }
648
649 pub fn activity_snapshot(
655 message_id: impl Into<String>,
656 activity_type: impl Into<String>,
657 content: HashMap<String, Value>,
658 replace: Option<bool>,
659 ) -> Self {
660 Self::ActivitySnapshot {
661 message_id: message_id.into(),
662 activity_type: activity_type.into(),
663 content,
664 replace,
665 base: BaseEvent::default(),
666 }
667 }
668
669 pub fn activity_delta(
671 message_id: impl Into<String>,
672 activity_type: impl Into<String>,
673 patch: Vec<Value>,
674 ) -> Self {
675 Self::ActivityDelta {
676 message_id: message_id.into(),
677 activity_type: activity_type.into(),
678 patch,
679 base: BaseEvent::default(),
680 }
681 }
682
683 pub fn raw(event: Value, source: Option<String>) -> Self {
689 Self::Raw {
690 event,
691 source,
692 base: BaseEvent::default(),
693 }
694 }
695
696 pub fn custom(name: impl Into<String>, value: Value) -> Self {
698 Self::Custom {
699 name: name.into(),
700 value,
701 base: BaseEvent::default(),
702 }
703 }
704
705 pub fn with_timestamp(mut self, timestamp: u64) -> Self {
711 match &mut self {
712 Self::RunStarted { base, .. }
713 | Self::RunFinished { base, .. }
714 | Self::RunError { base, .. }
715 | Self::StepStarted { base, .. }
716 | Self::StepFinished { base, .. }
717 | Self::TextMessageStart { base, .. }
718 | Self::TextMessageContent { base, .. }
719 | Self::TextMessageEnd { base, .. }
720 | Self::TextMessageChunk { base, .. }
721 | Self::ReasoningStart { base, .. }
722 | Self::ReasoningMessageStart { base, .. }
723 | Self::ReasoningMessageContent { base, .. }
724 | Self::ReasoningMessageEnd { base, .. }
725 | Self::ReasoningMessageChunk { base, .. }
726 | Self::ReasoningEnd { base, .. }
727 | Self::ReasoningEncryptedValue { base, .. }
728 | Self::ToolCallStart { base, .. }
729 | Self::ToolCallArgs { base, .. }
730 | Self::ToolCallEnd { base, .. }
731 | Self::ToolCallResult { base, .. }
732 | Self::ToolCallChunk { base, .. }
733 | Self::StateSnapshot { base, .. }
734 | Self::StateDelta { base, .. }
735 | Self::MessagesSnapshot { base, .. }
736 | Self::ActivitySnapshot { base, .. }
737 | Self::ActivityDelta { base, .. }
738 | Self::Raw { base, .. }
739 | Self::Custom { base, .. } => {
740 base.timestamp = Some(timestamp);
741 }
742 }
743 self
744 }
745
746 pub fn now_millis() -> u64 {
748 use std::time::{SystemTime, UNIX_EPOCH};
749 SystemTime::now()
750 .duration_since(UNIX_EPOCH)
751 .map(|d| d.as_millis() as u64)
752 .unwrap_or(0)
753 }
754}
755
756pub fn interaction_to_ag_ui_events(interaction: &Suspension) -> Vec<Event> {
762 let args = json!({
763 "id": interaction.id,
764 "message": interaction.message,
765 "parameters": interaction.parameters,
766 "response_schema": interaction.response_schema,
767 });
768
769 let tool_name = interaction
773 .action
774 .strip_prefix("tool:")
775 .unwrap_or(&interaction.action);
776
777 vec![
778 Event::tool_call_start(&interaction.id, tool_name, None),
779 Event::tool_call_args(
780 &interaction.id,
781 match serde_json::to_string(&args) {
782 Ok(value) => value,
783 Err(err) => {
784 warn!(
785 error = %err,
786 target_id = %interaction.id,
787 "failed to serialize interaction arguments for AG-UI"
788 );
789 "{}".to_string()
790 }
791 },
792 ),
793 Event::tool_call_end(&interaction.id),
794 ]
795}
796
797