1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4use crate::core::{MessageRole, ToolCall, ToolResponse};
5use crate::execution::ContextBudget;
6use crate::hooks::InlineHookRequest;
7
8#[derive(Debug, Serialize, Deserialize, Clone, Default)]
10pub struct RunUsage {
11 pub total_tokens: u32,
13 pub input_tokens: u32,
14 pub output_tokens: u32,
15 #[serde(default)]
17 pub cached_tokens: u32,
18 pub estimated_tokens: u32,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
22 pub model: Option<String>,
23 #[serde(default, skip_serializing_if = "Option::is_none")]
25 pub cost_usd: Option<f64>,
26}
27
28#[derive(Debug, Serialize, Deserialize, Clone)]
29#[serde(rename_all = "snake_case")]
30pub struct AgentEvent {
31 pub timestamp: chrono::DateTime<chrono::Utc>,
32 pub thread_id: String,
33 pub run_id: String,
34 pub event: AgentEventType,
35 pub task_id: String,
36 pub agent_id: String,
37 #[serde(default)]
39 pub user_id: Option<String>,
40 #[serde(default)]
42 pub identifier_id: Option<String>,
43 #[serde(default)]
45 pub workspace_id: Option<String>,
46 #[serde(default)]
48 pub channel_id: Option<String>,
49}
50
51impl AgentEvent {
52 pub fn from_task_event(task_event: &crate::TaskEvent, thread_id: &str) -> Self {
54 Self {
55 event: task_event.event.clone(),
56 agent_id: String::new(),
57 timestamp: chrono::DateTime::from_timestamp_millis(task_event.created_at)
58 .unwrap_or_default(),
59 thread_id: thread_id.to_string(),
60 run_id: String::new(),
61 task_id: String::new(),
62 user_id: None,
63 identifier_id: None,
64 workspace_id: None,
65 channel_id: None,
66 }
67 }
68}
69
70#[derive(Debug, Serialize, Deserialize, Clone)]
71#[serde(rename_all = "snake_case", tag = "type")]
72#[allow(clippy::large_enum_variant)]
73pub enum AgentEventType {
74 DiagnosticLog {
76 message: String,
77 },
78
79 RunStarted {},
81 RunFinished {
82 success: bool,
83 total_steps: usize,
84 failed_steps: usize,
85 usage: Option<RunUsage>,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 context_budget: Option<ContextBudget>,
89 },
90 RunError {
91 message: String,
92 code: Option<String>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
95 usage: Option<RunUsage>,
96 },
97 PlanStarted {
98 initial_plan: bool,
99 },
100 PlanFinished {
101 total_steps: usize,
102 },
103 PlanPruned {
104 removed_steps: Vec<String>,
105 },
106 StepStarted {
108 step_id: String,
109 step_index: usize,
110 },
111 StepCompleted {
112 step_id: String,
113 success: bool,
114 #[serde(default, skip_serializing_if = "Option::is_none")]
115 context_budget: Option<ContextBudget>,
116 #[serde(default, skip_serializing_if = "Option::is_none")]
118 usage: Option<RunUsage>,
119 },
120
121 ReflectStarted {},
123 ReflectFinished {
124 should_retry: bool,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 reason: Option<String>,
127 },
128
129 ToolExecutionStart {
131 step_id: String,
132 tool_call_id: String,
133 tool_call_name: String,
134 input: Value,
135 },
136 ToolExecutionEnd {
137 step_id: String,
138 tool_call_id: String,
139 tool_call_name: String,
140 success: bool,
141 },
142
143 TextMessageStart {
145 message_id: String,
146 step_id: String,
147 role: MessageRole,
148 is_final: Option<bool>,
149 },
150 TextMessageContent {
151 message_id: String,
152 step_id: String,
153 delta: String,
154 stripped_content: Option<Vec<(usize, String)>>,
155 },
156 TextMessageEnd {
157 message_id: String,
158 step_id: String,
159 },
160
161 ToolCalls {
163 step_id: String,
164 parent_message_id: Option<String>,
165 tool_calls: Vec<ToolCall>,
166 },
167 ToolResults {
168 step_id: String,
169 parent_message_id: Option<String>,
170 results: Vec<ToolResponse>,
171 },
172
173 AgentHandover {
175 from_agent: String,
176 to_agent: String,
177 reason: Option<String>,
178 },
179
180 LiveView {
184 view_id: String,
186 url: String,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
190 title: Option<String>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 display_mode: Option<String>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 width: Option<u32>,
197 #[serde(default, skip_serializing_if = "Option::is_none")]
199 height: Option<u32>,
200 },
201
202 BrowserSessionStarted {
203 session_id: String,
204 viewer_url: Option<String>,
205 stream_url: Option<String>,
206 },
207
208 InlineHookRequested {
209 request: InlineHookRequest,
210 },
211
212 TodosUpdated {
214 formatted_todos: String,
215 action: String,
216 todo_count: usize,
217 },
218
219 ContextCompaction {
221 tier: CompactionTier,
222 tokens_before: usize,
223 tokens_after: usize,
224 entries_affected: usize,
225 context_limit: usize,
226 usage_ratio: f64,
227 summary: Option<String>,
228 #[serde(default, skip_serializing_if = "Vec::is_empty")]
230 reinjected_skills: Vec<String>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
232 context_budget: Option<ContextBudget>,
233 },
234
235 ContextBudgetUpdate {
237 budget: ContextBudget,
238 is_warning: bool,
239 is_critical: bool,
240 },
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize)]
245#[serde(rename_all = "snake_case")]
246pub enum CompactionTier {
247 Trim,
249 Summarize,
251 Reset,
253}
254
255impl AgentEvent {
256 pub fn new(event: AgentEventType) -> Self {
257 Self {
258 timestamp: chrono::Utc::now(),
259 thread_id: uuid::Uuid::new_v4().to_string(),
260 run_id: uuid::Uuid::new_v4().to_string(),
261 event,
262 task_id: uuid::Uuid::new_v4().to_string(),
263 agent_id: "default".to_string(),
264 user_id: None,
265 identifier_id: None,
266 workspace_id: None,
267 channel_id: None,
268 }
269 }
270
271 pub fn with_context(
272 event: AgentEventType,
273 thread_id: String,
274 run_id: String,
275 task_id: String,
276 agent_id: String,
277 ) -> Self {
278 Self {
279 timestamp: chrono::Utc::now(),
280 thread_id,
281 run_id,
282 task_id,
283 event,
284 agent_id,
285 user_id: None,
286 identifier_id: None,
287 workspace_id: None,
288 channel_id: None,
289 }
290 }
291}