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