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