use crate::types::Role;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
use std::collections::HashMap;
use tirea_contract::Suspension;
use tracing::warn;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct BaseEvent {
#[serde(skip_serializing_if = "Option::is_none")]
pub timestamp: Option<u64>,
#[serde(rename = "rawEvent", skip_serializing_if = "Option::is_none")]
pub raw_event: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ReasoningEncryptedValueSubtype {
ToolCall,
Message,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum Event {
#[serde(rename = "RUN_STARTED")]
RunStarted {
#[serde(rename = "threadId")]
thread_id: String,
#[serde(rename = "runId")]
run_id: String,
#[serde(rename = "parentRunId", skip_serializing_if = "Option::is_none")]
parent_run_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
input: Option<Value>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "RUN_FINISHED")]
RunFinished {
#[serde(rename = "threadId")]
thread_id: String,
#[serde(rename = "runId")]
run_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
result: Option<Value>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "RUN_ERROR")]
RunError {
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
code: Option<String>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "STEP_STARTED")]
StepStarted {
#[serde(rename = "stepName")]
step_name: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "STEP_FINISHED")]
StepFinished {
#[serde(rename = "stepName")]
step_name: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "TEXT_MESSAGE_START")]
TextMessageStart {
#[serde(rename = "messageId")]
message_id: String,
role: Role,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "TEXT_MESSAGE_CONTENT")]
TextMessageContent {
#[serde(rename = "messageId")]
message_id: String,
delta: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "TEXT_MESSAGE_END")]
TextMessageEnd {
#[serde(rename = "messageId")]
message_id: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "TEXT_MESSAGE_CHUNK")]
TextMessageChunk {
#[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
role: Option<Role>,
#[serde(skip_serializing_if = "Option::is_none")]
delta: Option<String>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "REASONING_START")]
ReasoningStart {
#[serde(rename = "messageId")]
message_id: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "REASONING_MESSAGE_START")]
ReasoningMessageStart {
#[serde(rename = "messageId")]
message_id: String,
role: Role,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "REASONING_MESSAGE_CONTENT")]
ReasoningMessageContent {
#[serde(rename = "messageId")]
message_id: String,
delta: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "REASONING_MESSAGE_END")]
ReasoningMessageEnd {
#[serde(rename = "messageId")]
message_id: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "REASONING_MESSAGE_CHUNK")]
ReasoningMessageChunk {
#[serde(rename = "messageId", skip_serializing_if = "Option::is_none")]
message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
delta: Option<String>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "REASONING_END")]
ReasoningEnd {
#[serde(rename = "messageId")]
message_id: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "REASONING_ENCRYPTED_VALUE")]
ReasoningEncryptedValue {
subtype: ReasoningEncryptedValueSubtype,
#[serde(rename = "entityId")]
entity_id: String,
#[serde(rename = "encryptedValue")]
encrypted_value: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "TOOL_CALL_START")]
ToolCallStart {
#[serde(rename = "toolCallId")]
tool_call_id: String,
#[serde(rename = "toolCallName")]
tool_call_name: String,
#[serde(rename = "parentMessageId", skip_serializing_if = "Option::is_none")]
parent_message_id: Option<String>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "TOOL_CALL_ARGS")]
ToolCallArgs {
#[serde(rename = "toolCallId")]
tool_call_id: String,
delta: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "TOOL_CALL_END")]
ToolCallEnd {
#[serde(rename = "toolCallId")]
tool_call_id: String,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "TOOL_CALL_RESULT")]
ToolCallResult {
#[serde(rename = "messageId")]
message_id: String,
#[serde(rename = "toolCallId")]
tool_call_id: String,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
role: Option<Role>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "TOOL_CALL_CHUNK")]
ToolCallChunk {
#[serde(rename = "toolCallId", skip_serializing_if = "Option::is_none")]
tool_call_id: Option<String>,
#[serde(rename = "toolCallName", skip_serializing_if = "Option::is_none")]
tool_call_name: Option<String>,
#[serde(rename = "parentMessageId", skip_serializing_if = "Option::is_none")]
parent_message_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
delta: Option<String>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "STATE_SNAPSHOT")]
StateSnapshot {
snapshot: Value,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "STATE_DELTA")]
StateDelta {
delta: Vec<Value>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "MESSAGES_SNAPSHOT")]
MessagesSnapshot {
messages: Vec<Value>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "ACTIVITY_SNAPSHOT")]
ActivitySnapshot {
#[serde(rename = "messageId")]
message_id: String,
#[serde(rename = "activityType")]
activity_type: String,
content: HashMap<String, Value>,
#[serde(skip_serializing_if = "Option::is_none")]
replace: Option<bool>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "ACTIVITY_DELTA")]
ActivityDelta {
#[serde(rename = "messageId")]
message_id: String,
#[serde(rename = "activityType")]
activity_type: String,
patch: Vec<Value>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "RAW")]
Raw {
event: Value,
#[serde(skip_serializing_if = "Option::is_none")]
source: Option<String>,
#[serde(flatten)]
base: BaseEvent,
},
#[serde(rename = "CUSTOM")]
Custom {
name: String,
value: Value,
#[serde(flatten)]
base: BaseEvent,
},
}
impl Event {
pub fn run_started(
thread_id: impl Into<String>,
run_id: impl Into<String>,
parent_run_id: Option<String>,
) -> Self {
Self::RunStarted {
thread_id: thread_id.into(),
run_id: run_id.into(),
parent_run_id,
input: None,
base: BaseEvent::default(),
}
}
pub fn run_started_with_input(
thread_id: impl Into<String>,
run_id: impl Into<String>,
parent_run_id: Option<String>,
input: Value,
) -> Self {
Self::RunStarted {
thread_id: thread_id.into(),
run_id: run_id.into(),
parent_run_id,
input: Some(input),
base: BaseEvent::default(),
}
}
pub fn run_finished(
thread_id: impl Into<String>,
run_id: impl Into<String>,
result: Option<Value>,
) -> Self {
Self::RunFinished {
thread_id: thread_id.into(),
run_id: run_id.into(),
result,
base: BaseEvent::default(),
}
}
pub fn run_error(message: impl Into<String>, code: Option<String>) -> Self {
Self::RunError {
message: message.into(),
code,
base: BaseEvent::default(),
}
}
pub fn step_started(step_name: impl Into<String>) -> Self {
Self::StepStarted {
step_name: step_name.into(),
base: BaseEvent::default(),
}
}
pub fn step_finished(step_name: impl Into<String>) -> Self {
Self::StepFinished {
step_name: step_name.into(),
base: BaseEvent::default(),
}
}
pub fn text_message_start(message_id: impl Into<String>) -> Self {
Self::TextMessageStart {
message_id: message_id.into(),
role: Role::Assistant,
base: BaseEvent::default(),
}
}
pub fn text_message_content(message_id: impl Into<String>, delta: impl Into<String>) -> Self {
Self::TextMessageContent {
message_id: message_id.into(),
delta: delta.into(),
base: BaseEvent::default(),
}
}
pub fn text_message_end(message_id: impl Into<String>) -> Self {
Self::TextMessageEnd {
message_id: message_id.into(),
base: BaseEvent::default(),
}
}
pub fn text_message_chunk(
message_id: Option<String>,
role: Option<Role>,
delta: Option<String>,
) -> Self {
Self::TextMessageChunk {
message_id,
role,
delta,
base: BaseEvent::default(),
}
}
pub fn reasoning_start(message_id: impl Into<String>) -> Self {
Self::ReasoningStart {
message_id: message_id.into(),
base: BaseEvent::default(),
}
}
pub fn reasoning_message_start(message_id: impl Into<String>) -> Self {
Self::ReasoningMessageStart {
message_id: message_id.into(),
role: Role::Assistant,
base: BaseEvent::default(),
}
}
pub fn reasoning_message_content(
message_id: impl Into<String>,
delta: impl Into<String>,
) -> Self {
Self::ReasoningMessageContent {
message_id: message_id.into(),
delta: delta.into(),
base: BaseEvent::default(),
}
}
pub fn reasoning_message_end(message_id: impl Into<String>) -> Self {
Self::ReasoningMessageEnd {
message_id: message_id.into(),
base: BaseEvent::default(),
}
}
pub fn reasoning_message_chunk(message_id: Option<String>, delta: Option<String>) -> Self {
Self::ReasoningMessageChunk {
message_id,
delta,
base: BaseEvent::default(),
}
}
pub fn reasoning_end(message_id: impl Into<String>) -> Self {
Self::ReasoningEnd {
message_id: message_id.into(),
base: BaseEvent::default(),
}
}
pub fn reasoning_encrypted_value(
subtype: ReasoningEncryptedValueSubtype,
entity_id: impl Into<String>,
encrypted_value: impl Into<String>,
) -> Self {
Self::ReasoningEncryptedValue {
subtype,
entity_id: entity_id.into(),
encrypted_value: encrypted_value.into(),
base: BaseEvent::default(),
}
}
pub fn tool_call_start(
tool_call_id: impl Into<String>,
tool_call_name: impl Into<String>,
parent_message_id: Option<String>,
) -> Self {
Self::ToolCallStart {
tool_call_id: tool_call_id.into(),
tool_call_name: tool_call_name.into(),
parent_message_id,
base: BaseEvent::default(),
}
}
pub fn tool_call_args(tool_call_id: impl Into<String>, delta: impl Into<String>) -> Self {
Self::ToolCallArgs {
tool_call_id: tool_call_id.into(),
delta: delta.into(),
base: BaseEvent::default(),
}
}
pub fn tool_call_end(tool_call_id: impl Into<String>) -> Self {
Self::ToolCallEnd {
tool_call_id: tool_call_id.into(),
base: BaseEvent::default(),
}
}
pub fn tool_call_result(
message_id: impl Into<String>,
tool_call_id: impl Into<String>,
content: impl Into<String>,
) -> Self {
Self::ToolCallResult {
message_id: message_id.into(),
tool_call_id: tool_call_id.into(),
content: content.into(),
role: Some(Role::Tool),
base: BaseEvent::default(),
}
}
pub fn tool_call_chunk(
tool_call_id: Option<String>,
tool_call_name: Option<String>,
parent_message_id: Option<String>,
delta: Option<String>,
) -> Self {
Self::ToolCallChunk {
tool_call_id,
tool_call_name,
parent_message_id,
delta,
base: BaseEvent::default(),
}
}
pub fn state_snapshot(snapshot: Value) -> Self {
Self::StateSnapshot {
snapshot,
base: BaseEvent::default(),
}
}
pub fn state_delta(delta: Vec<Value>) -> Self {
Self::StateDelta {
delta,
base: BaseEvent::default(),
}
}
pub fn messages_snapshot(messages: Vec<Value>) -> Self {
Self::MessagesSnapshot {
messages,
base: BaseEvent::default(),
}
}
pub fn activity_snapshot(
message_id: impl Into<String>,
activity_type: impl Into<String>,
content: HashMap<String, Value>,
replace: Option<bool>,
) -> Self {
Self::ActivitySnapshot {
message_id: message_id.into(),
activity_type: activity_type.into(),
content,
replace,
base: BaseEvent::default(),
}
}
pub fn activity_delta(
message_id: impl Into<String>,
activity_type: impl Into<String>,
patch: Vec<Value>,
) -> Self {
Self::ActivityDelta {
message_id: message_id.into(),
activity_type: activity_type.into(),
patch,
base: BaseEvent::default(),
}
}
pub fn raw(event: Value, source: Option<String>) -> Self {
Self::Raw {
event,
source,
base: BaseEvent::default(),
}
}
pub fn custom(name: impl Into<String>, value: Value) -> Self {
Self::Custom {
name: name.into(),
value,
base: BaseEvent::default(),
}
}
pub fn with_timestamp(mut self, timestamp: u64) -> Self {
match &mut self {
Self::RunStarted { base, .. }
| Self::RunFinished { base, .. }
| Self::RunError { base, .. }
| Self::StepStarted { base, .. }
| Self::StepFinished { base, .. }
| Self::TextMessageStart { base, .. }
| Self::TextMessageContent { base, .. }
| Self::TextMessageEnd { base, .. }
| Self::TextMessageChunk { base, .. }
| Self::ReasoningStart { base, .. }
| Self::ReasoningMessageStart { base, .. }
| Self::ReasoningMessageContent { base, .. }
| Self::ReasoningMessageEnd { base, .. }
| Self::ReasoningMessageChunk { base, .. }
| Self::ReasoningEnd { base, .. }
| Self::ReasoningEncryptedValue { base, .. }
| Self::ToolCallStart { base, .. }
| Self::ToolCallArgs { base, .. }
| Self::ToolCallEnd { base, .. }
| Self::ToolCallResult { base, .. }
| Self::ToolCallChunk { base, .. }
| Self::StateSnapshot { base, .. }
| Self::StateDelta { base, .. }
| Self::MessagesSnapshot { base, .. }
| Self::ActivitySnapshot { base, .. }
| Self::ActivityDelta { base, .. }
| Self::Raw { base, .. }
| Self::Custom { base, .. } => {
base.timestamp = Some(timestamp);
}
}
self
}
pub fn now_millis() -> u64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
}
pub fn interaction_to_ag_ui_events(interaction: &Suspension) -> Vec<Event> {
let args = json!({
"id": interaction.id,
"message": interaction.message,
"parameters": interaction.parameters,
"response_schema": interaction.response_schema,
});
let tool_name = interaction
.action
.strip_prefix("tool:")
.unwrap_or(&interaction.action);
vec![
Event::tool_call_start(&interaction.id, tool_name, None),
Event::tool_call_args(
&interaction.id,
match serde_json::to_string(&args) {
Ok(value) => value,
Err(err) => {
warn!(
error = %err,
target_id = %interaction.id,
"failed to serialize interaction arguments for AG-UI"
);
"{}".to_string()
}
},
),
Event::tool_call_end(&interaction.id),
]
}