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