1use serde::{Deserialize, Serialize};
4use std::time::{SystemTime, UNIX_EPOCH};
5
6use crate::lsp::LspServerInfo;
7
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
10pub struct AgentEvent {
11 pub event_type: EventType,
12 pub timestamp: u64,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub data: Option<EventData>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
19#[serde(rename_all = "snake_case")]
20pub enum EventType {
21 TextStart,
22 TextDelta,
23 TextEnd,
24 ThinkingStart,
25 ThinkingDelta,
26 ThinkingEnd,
27 ToolUseStart,
28 ToolUseInputDelta,
29 ToolUseInputEnd,
30 ToolResult,
31 SessionStarted,
32 SessionEnded,
33 SessionRestored, NewSession,
35 CompressionTriggered,
36 CompressionCompleted,
37 MemoryLoaded,
38 MemoryDetected, KeywordsExtracted, Error,
41 Usage,
42 Progress,
43 ContextSize, AskQuestion, ProxyToolRequest, ProxyToolResponse, DebugLog, SkillsLoaded, WorkflowsLoaded, McpServerAdded, McpServerRemoved, McpServerStatus, LspServerAdded, LspServerRemoved, LspServerStatus, }
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
60#[serde(rename_all = "snake_case")]
61pub enum EventData {
62 Text {
63 delta: String,
64 },
65 Thinking {
66 delta: String,
67 signature: Option<String>,
68 },
69 ToolUse {
70 id: String,
71 name: String,
72 input: Option<serde_json::Value>,
73 },
74 ToolUseInput {
75 id: String,
76 delta: String,
77 },
78 ToolResult {
79 tool_use_id: String,
80 name: String,
81 detail: Option<String>,
82 content: String,
83 is_error: bool,
84 },
85 Error {
86 message: String,
87 code: Option<String>,
88 source: Option<String>,
89 },
90 Usage {
91 input_tokens: u64,
92 output_tokens: u64,
93 cache_creation_input_tokens: Option<u64>,
94 cache_read_input_tokens: Option<u64>,
95 },
96 SessionRestore {
97 input_tokens: u64,
98 total_output_tokens: u64,
99 message_count: usize,
100 },
101 Progress {
102 message: String,
103 percentage: Option<u8>,
104 },
105 ContextSize {
106 context_size: u64,
107 },
108 Compression {
109 original_tokens: u64,
110 compressed_tokens: u64,
111 ratio: f32,
112 },
113 Memory {
114 summary: String,
115 entries_count: usize,
116 },
117 Keywords {
118 keywords: Vec<String>,
119 source: String,
120 }, AskQuestion {
122 question: String,
123 options: Option<serde_json::Value>,
124 },
125 ProxyToolRequest {
127 request_id: String,
128 tool_name: String,
129 tool_input: serde_json::Value,
130 metadata: crate::tools::toolproxy::ProxyMetadata,
131 },
132 ProxyToolResponse {
134 request_id: String,
135 result: String,
136 is_error: bool,
137 },
138 DebugLog {
139 category: String,
140 message: String,
141 }, SkillsLoaded {
143 names: Vec<String>,
144 },
145 WorkflowsLoaded {
146 names: Vec<String>,
147 },
148 McpServerAdded {
149 name: String,
150 tool_count: usize,
151 },
152 McpServerRemoved {
153 name: String,
154 },
155 McpServerStatus {
156 servers: Vec<McpServerInfo>,
157 },
158 LspServerAdded {
159 name: String,
160 language: String,
161 },
162 LspServerRemoved {
163 name: String,
164 },
165 LspServerStatus {
166 servers: Vec<LspServerInfo>,
167 },
168}
169
170impl AgentEvent {
171 pub fn new(event_type: EventType) -> Self {
172 Self {
173 event_type,
174 timestamp: current_timestamp(),
175 data: None,
176 }
177 }
178
179 pub fn with_data(event_type: EventType, data: EventData) -> Self {
180 Self {
181 event_type,
182 timestamp: current_timestamp(),
183 data: Some(data),
184 }
185 }
186
187 pub fn text_delta(delta: impl Into<String>) -> Self {
188 Self::with_data(
189 EventType::TextDelta,
190 EventData::Text {
191 delta: delta.into(),
192 },
193 )
194 }
195
196 pub fn text_start() -> Self {
197 Self::new(EventType::TextStart)
198 }
199 pub fn text_end() -> Self {
200 Self::new(EventType::TextEnd)
201 }
202 pub fn thinking_start() -> Self {
203 Self::new(EventType::ThinkingStart)
204 }
205 pub fn thinking_end() -> Self {
206 Self::new(EventType::ThinkingEnd)
207 }
208 pub fn session_started() -> Self {
209 Self::new(EventType::SessionStarted)
210 }
211 pub fn session_ended() -> Self {
212 Self::new(EventType::SessionEnded)
213 }
214 pub fn session_restored(
215 input_tokens: u64,
216 total_output_tokens: u64,
217 message_count: usize,
218 ) -> Self {
219 Self::with_data(
220 EventType::SessionRestored,
221 EventData::SessionRestore {
222 input_tokens,
223 total_output_tokens,
224 message_count,
225 },
226 )
227 }
228
229 pub fn thinking_delta(delta: impl Into<String>, signature: Option<String>) -> Self {
230 Self::with_data(
231 EventType::ThinkingDelta,
232 EventData::Thinking {
233 delta: delta.into(),
234 signature,
235 },
236 )
237 }
238
239 pub fn tool_use_start(
240 id: impl Into<String>,
241 name: impl Into<String>,
242 input: Option<serde_json::Value>,
243 ) -> Self {
244 Self::with_data(
245 EventType::ToolUseStart,
246 EventData::ToolUse {
247 id: id.into(),
248 name: name.into(),
249 input,
250 },
251 )
252 }
253
254 pub fn tool_result(
255 tool_use_id: impl Into<String>,
256 name: impl Into<String>,
257 detail: Option<String>,
258 content: impl Into<String>,
259 is_error: bool,
260 ) -> Self {
261 Self::with_data(
262 EventType::ToolResult,
263 EventData::ToolResult {
264 tool_use_id: tool_use_id.into(),
265 name: name.into(),
266 detail,
267 content: content.into(),
268 is_error,
269 },
270 )
271 }
272
273 pub fn error(message: impl Into<String>, code: Option<String>, source: Option<String>) -> Self {
274 Self::with_data(
275 EventType::Error,
276 EventData::Error {
277 message: message.into(),
278 code,
279 source,
280 },
281 )
282 }
283
284 pub fn progress(message: impl Into<String>, percentage: Option<u8>) -> Self {
285 Self::with_data(
286 EventType::Progress,
287 EventData::Progress {
288 message: message.into(),
289 percentage,
290 },
291 )
292 }
293
294 pub fn usage(input_tokens: u64, output_tokens: u64) -> Self {
295 Self::with_data(
296 EventType::Usage,
297 EventData::Usage {
298 input_tokens,
299 output_tokens,
300 cache_creation_input_tokens: None,
301 cache_read_input_tokens: None,
302 },
303 )
304 }
305
306 pub fn usage_with_cache(
307 input_tokens: u64,
308 output_tokens: u64,
309 cache_read: u64,
310 cache_created: u64,
311 ) -> Self {
312 Self::with_data(
313 EventType::Usage,
314 EventData::Usage {
315 input_tokens,
316 output_tokens,
317 cache_creation_input_tokens: if cache_created > 0 {
318 Some(cache_created)
319 } else {
320 None
321 },
322 cache_read_input_tokens: if cache_read > 0 {
323 Some(cache_read)
324 } else {
325 None
326 },
327 },
328 )
329 }
330
331 pub fn debug_log(category: impl Into<String>, message: impl Into<String>) -> Self {
332 Self::with_data(
333 EventType::DebugLog,
334 EventData::DebugLog {
335 category: category.into(),
336 message: message.into(),
337 },
338 )
339 }
340
341 pub fn skills_loaded(names: Vec<String>) -> Self {
342 Self::with_data(
343 EventType::SkillsLoaded,
344 EventData::SkillsLoaded { names },
345 )
346 }
347
348 pub fn workflows_loaded(names: Vec<String>) -> Self {
349 Self::with_data(
350 EventType::WorkflowsLoaded,
351 EventData::WorkflowsLoaded { names },
352 )
353 }
354
355 pub fn mcp_server_added(name: impl Into<String>, tool_count: usize) -> Self {
357 Self::with_data(
358 EventType::McpServerAdded,
359 EventData::McpServerAdded {
360 name: name.into(),
361 tool_count,
362 },
363 )
364 }
365
366 pub fn mcp_server_removed(name: impl Into<String>) -> Self {
368 Self::with_data(
369 EventType::McpServerRemoved,
370 EventData::McpServerRemoved {
371 name: name.into(),
372 },
373 )
374 }
375
376 pub fn mcp_server_status(servers: Vec<McpServerInfo>) -> Self {
378 Self::with_data(
379 EventType::McpServerStatus,
380 EventData::McpServerStatus { servers },
381 )
382 }
383
384 pub fn lsp_server_added(name: impl Into<String>, language: impl Into<String>) -> Self {
386 Self::with_data(
387 EventType::LspServerAdded,
388 EventData::LspServerAdded {
389 name: name.into(),
390 language: language.into(),
391 },
392 )
393 }
394
395 pub fn lsp_server_removed(name: impl Into<String>) -> Self {
397 Self::with_data(
398 EventType::LspServerRemoved,
399 EventData::LspServerRemoved {
400 name: name.into(),
401 },
402 )
403 }
404
405 pub fn lsp_server_status(servers: Vec<LspServerInfo>) -> Self {
407 Self::with_data(
408 EventType::LspServerStatus,
409 EventData::LspServerStatus { servers },
410 )
411 }
412
413 pub fn proxy_tool_request(
415 request_id: impl Into<String>,
416 tool_name: impl Into<String>,
417 tool_input: serde_json::Value,
418 metadata: crate::tools::toolproxy::ProxyMetadata,
419 ) -> Self {
420 Self::with_data(
421 EventType::ProxyToolRequest,
422 EventData::ProxyToolRequest {
423 request_id: request_id.into(),
424 tool_name: tool_name.into(),
425 tool_input,
426 metadata,
427 },
428 )
429 }
430
431 pub fn proxy_tool_response(
433 request_id: impl Into<String>,
434 result: impl Into<String>,
435 is_error: bool,
436 ) -> Self {
437 Self::with_data(
438 EventType::ProxyToolResponse,
439 EventData::ProxyToolResponse {
440 request_id: request_id.into(),
441 result: result.into(),
442 is_error,
443 },
444 )
445 }
446
447 pub fn to_json(&self) -> Result<String, serde_json::Error> {
448 serde_json::to_string(self)
449 }
450 pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
451 serde_json::from_str(json)
452 }
453}
454
455fn current_timestamp() -> u64 {
456 SystemTime::now()
457 .duration_since(UNIX_EPOCH)
458 .unwrap_or_default()
459 .as_millis() as u64
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
464pub struct McpServerInfo {
465 pub name: String,
466 pub is_started: bool,
467 pub tool_count: usize,
468}
469
470impl McpServerInfo {
471 pub fn new(name: impl Into<String>, is_started: bool, tool_count: usize) -> Self {
472 Self {
473 name: name.into(),
474 is_started,
475 tool_count,
476 }
477 }
478
479 pub fn from_status(status: &crate::mcp::ServerStatus) -> Self {
480 Self {
481 name: status.name.clone(),
482 is_started: status.is_started,
483 tool_count: status.tool_count,
484 }
485 }
486}
487
488#[derive(Debug, Default)]
489pub struct EventCollector {
490 events: Vec<AgentEvent>,
491}
492
493impl EventCollector {
494 pub fn new() -> Self {
495 Self::default()
496 }
497 pub fn push(&mut self, event: AgentEvent) {
498 self.events.push(event);
499 }
500 pub fn events(&self) -> &[AgentEvent] {
501 &self.events
502 }
503 pub fn len(&self) -> usize {
504 self.events.len()
505 }
506 pub fn is_empty(&self) -> bool {
507 self.events.is_empty()
508 }
509 pub fn clear(&mut self) {
510 self.events.clear();
511 }
512 pub fn to_json_lines(&self) -> Result<Vec<String>, serde_json::Error> {
513 self.events.iter().map(|e| e.to_json()).collect()
514 }
515 pub fn output_json_lines(&self) -> Result<String, serde_json::Error> {
516 Ok(self.to_json_lines()?.join("\n"))
517 }
518}
519
520#[cfg(test)]
521mod tests {
522 use super::*;
523 #[test]
524 fn test_event() {
525 let e = AgentEvent::text_delta("Hello");
526 assert!(e.to_json().unwrap().contains("Hello"));
527 }
528}