1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::Value;
4use uuid::Uuid;
5
6#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
33#[serde(tag = "stream_type", content = "stream_data")]
34#[serde(rename_all = "snake_case")]
35pub enum StreamId {
36 #[default]
38 Main,
39 Sidechain { agent_id: String },
41 Subagent { name: String },
43}
44
45impl StreamId {
46 pub fn as_str(&self) -> String {
48 match self {
49 StreamId::Main => "main".to_string(),
50 StreamId::Sidechain { agent_id } => format!("sidechain:{}", agent_id),
51 StreamId::Subagent { name } => format!("subagent:{}", name),
52 }
53 }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "snake_case")]
59pub enum ToolKind {
60 Read,
62 Write,
64 Execute,
66 Plan,
68 Search,
70 Ask,
72 Other,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(rename_all = "snake_case")]
91pub enum ToolOrigin {
92 System,
94 Mcp,
96}
97
98#[derive(Debug, Clone, Serialize, Deserialize)]
101pub struct AgentEvent {
102 pub id: Uuid,
104
105 pub session_id: Uuid,
107
108 pub parent_id: Option<Uuid>,
111
112 pub timestamp: DateTime<Utc>,
114
115 #[serde(default)]
118 pub stream_id: StreamId,
119
120 #[serde(flatten)]
122 pub payload: EventPayload,
123
124 #[serde(default, skip_serializing_if = "Option::is_none")]
127 pub metadata: Option<Value>,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(tag = "type", content = "content")]
133#[serde(rename_all = "snake_case")]
134pub enum EventPayload {
135 User(UserPayload),
137
138 Reasoning(ReasoningPayload),
140
141 ToolCall(ToolCallPayload),
145
146 ToolResult(ToolResultPayload),
148
149 Message(MessagePayload),
153
154 TokenUsage(TokenUsagePayload),
158
159 Notification(NotificationPayload),
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct UserPayload {
167 pub text: String,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct ReasoningPayload {
173 pub text: String,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
182#[serde(untagged)]
183pub enum ToolCallPayload {
184 FileRead {
186 name: String,
187 arguments: FileReadArgs,
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 provider_call_id: Option<String>,
190 },
191
192 FileEdit {
194 name: String,
195 arguments: FileEditArgs,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
197 provider_call_id: Option<String>,
198 },
199
200 FileWrite {
202 name: String,
203 arguments: FileWriteArgs,
204 #[serde(default, skip_serializing_if = "Option::is_none")]
205 provider_call_id: Option<String>,
206 },
207
208 Execute {
210 name: String,
211 arguments: ExecuteArgs,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
213 provider_call_id: Option<String>,
214 },
215
216 Search {
218 name: String,
219 arguments: SearchArgs,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
221 provider_call_id: Option<String>,
222 },
223
224 Mcp {
226 name: String,
227 arguments: McpArgs,
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 provider_call_id: Option<String>,
230 },
231
232 Generic {
234 name: String,
235 arguments: Value,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 provider_call_id: Option<String>,
238 },
239}
240
241impl ToolCallPayload {
242 pub fn name(&self) -> &str {
244 match self {
245 ToolCallPayload::FileRead { name, .. } => name,
246 ToolCallPayload::FileEdit { name, .. } => name,
247 ToolCallPayload::FileWrite { name, .. } => name,
248 ToolCallPayload::Execute { name, .. } => name,
249 ToolCallPayload::Search { name, .. } => name,
250 ToolCallPayload::Mcp { name, .. } => name,
251 ToolCallPayload::Generic { name, .. } => name,
252 }
253 }
254
255 pub fn provider_call_id(&self) -> Option<&str> {
257 match self {
258 ToolCallPayload::FileRead {
259 provider_call_id, ..
260 } => provider_call_id.as_deref(),
261 ToolCallPayload::FileEdit {
262 provider_call_id, ..
263 } => provider_call_id.as_deref(),
264 ToolCallPayload::FileWrite {
265 provider_call_id, ..
266 } => provider_call_id.as_deref(),
267 ToolCallPayload::Execute {
268 provider_call_id, ..
269 } => provider_call_id.as_deref(),
270 ToolCallPayload::Search {
271 provider_call_id, ..
272 } => provider_call_id.as_deref(),
273 ToolCallPayload::Mcp {
274 provider_call_id, ..
275 } => provider_call_id.as_deref(),
276 ToolCallPayload::Generic {
277 provider_call_id, ..
278 } => provider_call_id.as_deref(),
279 }
280 }
281
282 pub fn kind(&self) -> ToolKind {
284 match self {
285 ToolCallPayload::FileRead { .. } => ToolKind::Read,
286 ToolCallPayload::FileEdit { .. } => ToolKind::Write,
287 ToolCallPayload::FileWrite { .. } => ToolKind::Write,
288 ToolCallPayload::Execute { .. } => ToolKind::Execute,
289 ToolCallPayload::Search { .. } => ToolKind::Search,
290 ToolCallPayload::Mcp { .. } => ToolKind::Other,
291 ToolCallPayload::Generic { .. } => ToolKind::Other,
292 }
293 }
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
299pub struct FileReadArgs {
300 #[serde(default, skip_serializing_if = "Option::is_none")]
301 pub file_path: Option<String>,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub path: Option<String>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub pattern: Option<String>,
306 #[serde(flatten)]
307 pub extra: Value,
308}
309
310impl FileReadArgs {
311 pub fn path(&self) -> Option<&str> {
313 self.file_path.as_deref().or(self.path.as_deref())
314 }
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct FileEditArgs {
319 pub file_path: String,
320 pub old_string: String,
321 pub new_string: String,
322 #[serde(default)]
323 pub replace_all: bool,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
327pub struct FileWriteArgs {
328 pub file_path: String,
329 pub content: String,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
333pub struct ExecuteArgs {
334 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub command: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub description: Option<String>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub timeout: Option<u64>,
340 #[serde(flatten)]
341 pub extra: Value,
342}
343
344impl ExecuteArgs {
345 pub fn command(&self) -> Option<&str> {
346 self.command.as_deref()
347 }
348}
349
350#[derive(Debug, Clone, Serialize, Deserialize)]
351pub struct SearchArgs {
352 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub pattern: Option<String>,
354 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub query: Option<String>,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
357 pub input: Option<String>,
358 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub path: Option<String>,
360 #[serde(flatten)]
361 pub extra: Value,
362}
363
364impl SearchArgs {
365 pub fn pattern(&self) -> Option<&str> {
367 self.pattern
368 .as_deref()
369 .or(self.query.as_deref())
370 .or(self.input.as_deref())
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct McpArgs {
376 #[serde(flatten)]
377 pub inner: Value,
378}
379
380impl McpArgs {
381 pub fn parse_name(full_name: &str) -> Option<(String, String)> {
383 if !full_name.starts_with("mcp__") {
384 return None;
385 }
386
387 let rest = &full_name[5..]; let parts: Vec<&str> = rest.splitn(2, "__").collect();
389
390 if parts.len() == 2 {
391 Some((parts[0].to_string(), parts[1].to_string()))
392 } else {
393 None
394 }
395 }
396
397 pub fn server_name(full_name: &str) -> Option<String> {
399 Self::parse_name(full_name).map(|(server, _)| server)
400 }
401
402 pub fn tool_name(full_name: &str) -> Option<String> {
404 Self::parse_name(full_name).map(|(_, tool)| tool)
405 }
406}
407
408#[derive(Debug, Clone, Serialize, Deserialize)]
409pub struct ToolResultPayload {
410 pub output: String,
412
413 pub tool_call_id: Uuid,
416
417 #[serde(default)]
419 pub is_error: bool,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct MessagePayload {
424 pub text: String,
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize)]
429pub struct TokenUsagePayload {
430 pub input_tokens: i32,
432 pub output_tokens: i32,
434 pub total_tokens: i32,
436
437 #[serde(default, skip_serializing_if = "Option::is_none")]
439 pub details: Option<TokenUsageDetails>,
440}
441
442#[derive(Debug, Clone, Serialize, Deserialize)]
443pub struct TokenUsageDetails {
444 pub cache_creation_input_tokens: Option<i32>,
446 pub cache_read_input_tokens: Option<i32>,
448 pub reasoning_output_tokens: Option<i32>,
450}
451
452#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct NotificationPayload {
454 pub text: String,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub level: Option<String>,
459}
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464
465 #[test]
466 fn test_serialization() {
467 let event = AgentEvent {
468 id: Uuid::new_v4(),
469 session_id: Uuid::new_v4(),
470 parent_id: None,
471 timestamp: Utc::now(),
472 stream_id: StreamId::Main,
473 payload: EventPayload::User(UserPayload {
474 text: "Hello".to_string(),
475 }),
476 metadata: None,
477 };
478
479 let json = serde_json::to_string(&event).unwrap();
480 let deserialized: AgentEvent = serde_json::from_str(&json).unwrap();
481
482 match deserialized.payload {
483 EventPayload::User(payload) => assert_eq!(payload.text, "Hello"),
484 _ => panic!("Wrong payload type"),
485 }
486 }
487
488 #[test]
489 fn test_stream_id_variants() {
490 let main_stream = StreamId::Main;
492 assert_eq!(main_stream.as_str(), "main");
493
494 let sidechain_stream = StreamId::Sidechain {
496 agent_id: "abc123".to_string(),
497 };
498 assert_eq!(sidechain_stream.as_str(), "sidechain:abc123");
499
500 let subagent_stream = StreamId::Subagent {
502 name: "review".to_string(),
503 };
504 assert_eq!(subagent_stream.as_str(), "subagent:review");
505 }
506
507 #[test]
508 fn test_tool_call_serialization_roundtrip() {
509 let original = ToolCallPayload::FileRead {
510 name: "Read".to_string(),
511 arguments: FileReadArgs {
512 file_path: Some("/path/to/file.rs".to_string()),
513 path: None,
514 pattern: None,
515 extra: serde_json::json!({}),
516 },
517 provider_call_id: Some("call_123".to_string()),
518 };
519
520 let json = serde_json::to_string(&original).unwrap();
521 let deserialized: ToolCallPayload = serde_json::from_str(&json).unwrap();
522
523 match deserialized {
524 ToolCallPayload::FileRead {
525 name,
526 arguments,
527 provider_call_id,
528 } => {
529 assert_eq!(name, "Read");
530 assert_eq!(arguments.file_path, Some("/path/to/file.rs".to_string()));
531 assert_eq!(provider_call_id, Some("call_123".to_string()));
532 }
533 _ => panic!("Expected FileRead variant"),
534 }
535 }
536
537 #[test]
538 fn test_file_read_args_path_helper() {
539 let args1 = FileReadArgs {
540 file_path: Some("/path1".to_string()),
541 path: None,
542 pattern: None,
543 extra: serde_json::json!({}),
544 };
545 assert_eq!(args1.path(), Some("/path1"));
546
547 let args2 = FileReadArgs {
548 file_path: None,
549 path: Some("/path2".to_string()),
550 pattern: None,
551 extra: serde_json::json!({}),
552 };
553 assert_eq!(args2.path(), Some("/path2"));
554
555 let args3 = FileReadArgs {
556 file_path: Some("/path1".to_string()),
557 path: Some("/path2".to_string()),
558 pattern: None,
559 extra: serde_json::json!({}),
560 };
561 assert_eq!(args3.path(), Some("/path1"));
562 }
563
564 #[test]
565 fn test_search_args_pattern_helper() {
566 let args1 = SearchArgs {
567 pattern: Some("pattern1".to_string()),
568 query: None,
569 input: None,
570 path: None,
571 extra: serde_json::json!({}),
572 };
573 assert_eq!(args1.pattern(), Some("pattern1"));
574
575 let args2 = SearchArgs {
576 pattern: None,
577 query: Some("query2".to_string()),
578 input: None,
579 path: None,
580 extra: serde_json::json!({}),
581 };
582 assert_eq!(args2.pattern(), Some("query2"));
583
584 let args3 = SearchArgs {
585 pattern: None,
586 query: None,
587 input: Some("input3".to_string()),
588 path: None,
589 extra: serde_json::json!({}),
590 };
591 assert_eq!(args3.pattern(), Some("input3"));
592 }
593
594 #[test]
595 fn test_mcp_args_parse_name() {
596 assert_eq!(
597 McpArgs::parse_name("mcp__o3__o3-search"),
598 Some(("o3".to_string(), "o3-search".to_string()))
599 );
600
601 assert_eq!(
602 McpArgs::parse_name("mcp__sqlite__query"),
603 Some(("sqlite".to_string(), "query".to_string()))
604 );
605
606 assert_eq!(McpArgs::parse_name("not_mcp_tool"), None);
607 assert_eq!(McpArgs::parse_name("mcp__only_server"), None);
608 }
609
610 #[test]
611 fn test_mcp_args_server_and_tool_name() {
612 assert_eq!(
613 McpArgs::server_name("mcp__o3__o3-search"),
614 Some("o3".to_string())
615 );
616 assert_eq!(
617 McpArgs::tool_name("mcp__o3__o3-search"),
618 Some("o3-search".to_string())
619 );
620 }
621
622 #[test]
623 fn test_tool_call_kind_derivation() {
624 let read_payload = ToolCallPayload::FileRead {
625 name: "Read".to_string(),
626 arguments: FileReadArgs {
627 file_path: Some("/path".to_string()),
628 path: None,
629 pattern: None,
630 extra: serde_json::json!({}),
631 },
632 provider_call_id: None,
633 };
634 assert_eq!(read_payload.kind(), ToolKind::Read);
635
636 let edit_payload = ToolCallPayload::FileEdit {
637 name: "Edit".to_string(),
638 arguments: FileEditArgs {
639 file_path: "/path".to_string(),
640 old_string: "old".to_string(),
641 new_string: "new".to_string(),
642 replace_all: false,
643 },
644 provider_call_id: None,
645 };
646 assert_eq!(edit_payload.kind(), ToolKind::Write);
647
648 let write_payload = ToolCallPayload::FileWrite {
649 name: "Write".to_string(),
650 arguments: FileWriteArgs {
651 file_path: "/path".to_string(),
652 content: "content".to_string(),
653 },
654 provider_call_id: None,
655 };
656 assert_eq!(write_payload.kind(), ToolKind::Write);
657
658 let exec_payload = ToolCallPayload::Execute {
659 name: "Bash".to_string(),
660 arguments: ExecuteArgs {
661 command: Some("ls".to_string()),
662 description: None,
663 timeout: None,
664 extra: serde_json::json!({}),
665 },
666 provider_call_id: None,
667 };
668 assert_eq!(exec_payload.kind(), ToolKind::Execute);
669
670 let search_payload = ToolCallPayload::Search {
671 name: "Grep".to_string(),
672 arguments: SearchArgs {
673 pattern: Some("pattern".to_string()),
674 query: None,
675 input: None,
676 path: None,
677 extra: serde_json::json!({}),
678 },
679 provider_call_id: None,
680 };
681 assert_eq!(search_payload.kind(), ToolKind::Search);
682
683 let mcp_payload = ToolCallPayload::Mcp {
684 name: "mcp__o3__search".to_string(),
685 arguments: McpArgs {
686 inner: serde_json::json!({}),
687 },
688 provider_call_id: None,
689 };
690 assert_eq!(mcp_payload.kind(), ToolKind::Other);
691
692 let generic_payload = ToolCallPayload::Generic {
693 name: "CustomTool".to_string(),
694 arguments: serde_json::json!({}),
695 provider_call_id: None,
696 };
697 assert_eq!(generic_payload.kind(), ToolKind::Other);
698 }
699}