Skip to main content

microagents_events/
lib.rs

1pub mod types;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7use std::{convert::TryFrom, fmt};
8
9use crate::types::{AgentEvent, AgentEventError, JsonRpcNotification, ToolCall, ToolResult};
10
11pub const EVENTS_PROTOCOL_VERSION: &str = "0.1.0";
12
13/// Indicates whether a session is being started fresh or resumed.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[non_exhaustive]
16pub enum SessionInitType {
17    /// Start a new session.
18    Start,
19    /// Resume an existing session.
20    Resume,
21}
22
23impl fmt::Display for SessionInitType {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            Self::Start => write!(f, "start"),
27            Self::Resume => write!(f, "resume"),
28        }
29    }
30}
31
32impl TryFrom<&str> for SessionInitType {
33    type Error = AgentEventError;
34
35    fn try_from(value: &str) -> Result<Self, Self::Error> {
36        match value.to_lowercase().as_str() {
37            "start" => Ok(Self::Start),
38            "resume" => Ok(Self::Resume),
39            _ => Err(AgentEventError::InvalidFieldType(format!(
40                "No init type with message: {}",
41                value
42            ))),
43        }
44    }
45}
46
47/// Type of content delta in a stream.
48#[derive(Debug, Clone, Serialize, Deserialize)]
49#[non_exhaustive]
50pub enum DeltaType {
51    /// Regular text content.
52    Text,
53    /// Model thinking or reasoning content.
54    Thinking,
55}
56
57impl fmt::Display for DeltaType {
58    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59        match self {
60            Self::Text => write!(f, "text"),
61            Self::Thinking => write!(f, "thinking"),
62        }
63    }
64}
65
66impl From<DeltaType> for Value {
67    fn from(value: DeltaType) -> Self {
68        match value {
69            DeltaType::Text => Value::from("text"),
70            DeltaType::Thinking => Value::from("thinking"),
71        }
72    }
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize, Copy, Default)]
76pub struct Usage {
77    pub latency: i64,
78    pub input_chars: usize,
79    pub estimated_input_tokens: usize,
80    pub output_chars: usize,
81    pub estimated_output_tokens: usize,
82}
83
84/// Event emitted when a session is initialized.
85#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct SessionInitEvent {
87    pub session_id: String,
88    pub model: String,
89    pub provider: String,
90    pub system: String,
91    /// Either `'new'` or `'resume'`.
92    pub init_type: SessionInitType,
93    pub timestamp: DateTime<Utc>,
94}
95
96/// Event emitted when a session stops.
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct SessionStopEvent {
99    pub session_id: String,
100    pub success: bool,
101    pub result: Option<String>,
102    pub error: Option<String>,
103    pub timestamp: DateTime<Utc>,
104    pub usage: Usage,
105}
106
107/// Event emitted when the user submits a prompt.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct UserPromptSubmitEvent {
110    pub session_id: String,
111    pub turn_id: String,
112    pub prompt: String,
113    pub timestamp: DateTime<Utc>,
114}
115
116/// Event emitted for each delta in a streaming response.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct StreamDeltaEvent {
119    pub session_id: String,
120    pub turn_id: String,
121    pub delta: String,
122    pub delta_type: DeltaType,
123    pub timestamp: DateTime<Utc>,
124}
125
126/// Event emitted when a tool is called.
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ToolCallEvent {
129    pub session_id: String,
130    pub turn_id: String,
131    pub name: String,
132    pub input: Value,
133    pub timestamp: DateTime<Utc>,
134}
135
136/// Event emitted when a tool returns a result.
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ToolResultEvent {
139    pub session_id: String,
140    pub turn_id: String,
141    /// Tool execution result. [`Value`] implements `From<ToolResult>`.
142    pub result: ToolResult,
143    pub tool_call_id: String,
144    pub timestamp: DateTime<Utc>,
145}
146
147/// Event emitted when a skill is loaded.
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct SkillLoadEvent {
150    pub session_id: String,
151    pub turn_id: String,
152    pub skill_name: String,
153    pub timestamp: DateTime<Utc>,
154}
155
156/// Event emitted when the assistant produces a complete response.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct AssistantResponseEvent {
159    pub session_id: String,
160    pub turn_id: String,
161    pub full_text: String,
162    pub tool_calls: Option<Vec<ToolCall>>,
163    pub timestamp: DateTime<Utc>,
164}
165
166impl AgentEvent for SessionInitEvent {
167    fn to_jsonrpc(&self) -> JsonRpcNotification {
168        JsonRpcNotification::builder()
169            .method("session.init".into())
170            .add_param("session_id".into(), Value::from(self.session_id.clone()))
171            .add_param("system".into(), Value::from(self.system.clone()))
172            .add_param("model".into(), Value::from(self.model.clone()))
173            .add_param("provider".into(), Value::from(self.provider.clone()))
174            .add_param(
175                "init_type".into(),
176                serde_json::to_value(self.init_type.clone()).unwrap(),
177            )
178            .add_param(
179                "timestamp".into(),
180                serde_json::to_value(self.timestamp).unwrap(),
181            )
182    }
183
184    fn session_id(&self) -> String {
185        self.session_id.clone()
186    }
187}
188
189impl AgentEvent for SessionStopEvent {
190    fn to_jsonrpc(&self) -> JsonRpcNotification {
191        JsonRpcNotification::builder()
192            .method("session.stop".into())
193            .add_param("session_id".into(), Value::from(self.session_id.clone()))
194            .add_param("success".into(), Value::from(self.success))
195            .add_param("result".into(), Value::from(self.result.clone()))
196            .add_param("error".into(), Value::from(self.error.clone()))
197            .add_param(
198                "timestamp".into(),
199                serde_json::to_value(self.timestamp).unwrap(),
200            )
201            .add_param("usage".into(), serde_json::to_value(self.usage).unwrap())
202    }
203
204    fn session_id(&self) -> String {
205        self.session_id.clone()
206    }
207}
208
209impl AgentEvent for UserPromptSubmitEvent {
210    fn to_jsonrpc(&self) -> JsonRpcNotification {
211        JsonRpcNotification::builder()
212            .method("user.prompt.submit".into())
213            .add_param("session_id".into(), Value::from(self.session_id.clone()))
214            .add_param("turn_id".into(), Value::from(self.turn_id.clone()))
215            .add_param("prompt".into(), Value::from(self.prompt.clone()))
216            .add_param(
217                "timestamp".into(),
218                serde_json::to_value(self.timestamp).unwrap(),
219            )
220    }
221
222    fn session_id(&self) -> String {
223        self.session_id.clone()
224    }
225}
226
227impl AgentEvent for StreamDeltaEvent {
228    fn to_jsonrpc(&self) -> JsonRpcNotification {
229        JsonRpcNotification::builder()
230            .method("stream.delta".into())
231            .add_param("session_id".into(), Value::from(self.session_id.clone()))
232            .add_param("turn_id".into(), Value::from(self.turn_id.clone()))
233            .add_param("delta".into(), Value::from(self.delta.clone()))
234            .add_param(
235                "delta_type".into(),
236                serde_json::to_value(self.delta_type.clone()).unwrap(),
237            )
238            .add_param(
239                "timestamp".into(),
240                serde_json::to_value(self.timestamp).unwrap(),
241            )
242    }
243
244    fn session_id(&self) -> String {
245        self.session_id.clone()
246    }
247}
248
249impl AgentEvent for ToolCallEvent {
250    fn to_jsonrpc(&self) -> JsonRpcNotification {
251        JsonRpcNotification::builder()
252            .method("tool.call".into())
253            .add_param("session_id".into(), Value::from(self.session_id.clone()))
254            .add_param("turn_id".into(), Value::from(self.turn_id.clone()))
255            .add_param("name".into(), Value::from(self.name.clone()))
256            .add_param("input".into(), self.input.clone())
257            .add_param(
258                "timestamp".into(),
259                serde_json::to_value(self.timestamp).unwrap(),
260            )
261    }
262
263    fn session_id(&self) -> String {
264        self.session_id.clone()
265    }
266}
267
268impl AgentEvent for ToolResultEvent {
269    fn to_jsonrpc(&self) -> JsonRpcNotification {
270        JsonRpcNotification::builder()
271            .method("tool.result".into())
272            .add_param("session_id".into(), Value::from(self.session_id.clone()))
273            .add_param("turn_id".into(), Value::from(self.turn_id.clone()))
274            .add_param("result".into(), serde_json::to_value(&self.result).unwrap())
275            .add_param(
276                "tool_call_id".into(),
277                Value::from(self.tool_call_id.clone()),
278            )
279            .add_param(
280                "timestamp".into(),
281                serde_json::to_value(self.timestamp).unwrap(),
282            )
283    }
284
285    fn session_id(&self) -> String {
286        self.session_id.clone()
287    }
288}
289
290impl AgentEvent for SkillLoadEvent {
291    fn to_jsonrpc(&self) -> JsonRpcNotification {
292        JsonRpcNotification::builder()
293            .method("skill.load".into())
294            .add_param("session_id".into(), Value::from(self.session_id.clone()))
295            .add_param("turn_id".into(), Value::from(self.turn_id.clone()))
296            .add_param("skill_name".into(), Value::from(self.skill_name.clone()))
297            .add_param(
298                "timestamp".into(),
299                serde_json::to_value(self.timestamp).unwrap(),
300            )
301    }
302
303    fn session_id(&self) -> String {
304        self.session_id.clone()
305    }
306}
307
308impl AgentEvent for AssistantResponseEvent {
309    fn to_jsonrpc(&self) -> JsonRpcNotification {
310        JsonRpcNotification::builder()
311            .method("assistant.response".into())
312            .add_param("session_id".into(), Value::from(self.session_id.clone()))
313            .add_param("turn_id".into(), Value::from(self.turn_id.clone()))
314            .add_param("full_text".into(), Value::from(self.full_text.clone()))
315            .add_param(
316                "tool_calls".into(),
317                Value::from(self.tool_calls.clone().map(|tcs| {
318                    tcs.iter()
319                        .map(|tc| serde_json::to_value(tc).unwrap())
320                        .collect::<Vec<Value>>()
321                })),
322            )
323            .add_param(
324                "timestamp".into(),
325                serde_json::to_value(self.timestamp).unwrap(),
326            )
327    }
328
329    fn session_id(&self) -> String {
330        self.session_id.clone()
331    }
332}
333
334/// A sum type wrapping any agent event.
335#[derive(Debug, Clone)]
336#[non_exhaustive]
337pub enum AgentEventAny {
338    SessionInit(SessionInitEvent),
339    SessionStop(SessionStopEvent),
340    StreamDelta(StreamDeltaEvent),
341    ToolCall(ToolCallEvent),
342    ToolResult(ToolResultEvent),
343    AssistantResponse(AssistantResponseEvent),
344    SkillLoad(SkillLoadEvent),
345    UserPromptSubmit(UserPromptSubmitEvent),
346}
347
348impl AgentEventAny {
349    pub fn timestamp(&self) -> DateTime<Utc> {
350        match self {
351            Self::SessionInit(s) => s.timestamp,
352            Self::AssistantResponse(s) => s.timestamp,
353            Self::SessionStop(s) => s.timestamp,
354            Self::SkillLoad(s) => s.timestamp,
355            Self::StreamDelta(s) => s.timestamp,
356            Self::UserPromptSubmit(s) => s.timestamp,
357            Self::ToolCall(s) => s.timestamp,
358            Self::ToolResult(s) => s.timestamp,
359        }
360    }
361}
362
363impl AgentEvent for AgentEventAny {
364    fn to_jsonrpc(&self) -> JsonRpcNotification {
365        match self {
366            Self::SessionInit(s) => s.to_jsonrpc(),
367            Self::AssistantResponse(s) => s.to_jsonrpc(),
368            Self::SessionStop(s) => s.to_jsonrpc(),
369            Self::ToolCall(s) => s.to_jsonrpc(),
370            Self::StreamDelta(s) => s.to_jsonrpc(),
371            Self::UserPromptSubmit(s) => s.to_jsonrpc(),
372            Self::ToolResult(s) => s.to_jsonrpc(),
373            Self::SkillLoad(s) => s.to_jsonrpc(),
374        }
375    }
376
377    fn session_id(&self) -> String {
378        match self {
379            Self::AssistantResponse(s) => s.session_id.clone(),
380            Self::SessionInit(s) => s.session_id.clone(),
381            Self::SessionStop(s) => s.session_id.clone(),
382            Self::StreamDelta(s) => s.session_id.clone(),
383            Self::SkillLoad(s) => s.session_id.clone(),
384            Self::ToolCall(s) => s.session_id.clone(),
385            Self::ToolResult(s) => s.session_id.clone(),
386            Self::UserPromptSubmit(s) => s.session_id.clone(),
387        }
388    }
389}
390
391impl TryFrom<JsonRpcNotification> for AgentEventAny {
392    type Error = AgentEventError;
393
394    fn try_from(value: JsonRpcNotification) -> Result<Self, Self::Error> {
395        let session_id = value
396            .params
397            .get("session_id")
398            .and_then(|v| v.as_str())
399            .ok_or_else(|| AgentEventError::MissingField("session_id".to_string()))?
400            .to_string();
401        let turn_id = value
402            .params
403            .get("turn_id")
404            .and_then(|v| v.as_str())
405            .unwrap_or("")
406            .to_string();
407
408        match value.method.as_str() {
409            "session.init" => Ok(Self::SessionInit(SessionInitEvent {
410                session_id,
411                model: value
412                    .params
413                    .get("model")
414                    .and_then(|v| v.as_str())
415                    .ok_or_else(|| AgentEventError::MissingField("model".to_string()))?
416                    .to_string(),
417                provider: value
418                    .params
419                    .get("provider")
420                    .and_then(|v| v.as_str())
421                    .ok_or_else(|| AgentEventError::MissingField("provider".to_string()))?
422                    .to_string(),
423                system: value
424                    .params
425                    .get("system")
426                    .and_then(|v| v.as_str())
427                    .ok_or_else(|| AgentEventError::MissingField("system".to_string()))?
428                    .to_string(),
429                init_type: {
430                    let raw = value
431                        .params
432                        .get("init_type")
433                        .ok_or_else(|| AgentEventError::MissingField("init_type".to_string()))?;
434                    let init_type: SessionInitType = serde_json::from_value(raw.to_owned())
435                        .map_err(|_| AgentEventError::InvalidFieldType("init_type".to_string()))?;
436                    init_type
437                },
438                timestamp: {
439                    let raw = value
440                        .params
441                        .get("timestamp")
442                        .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?;
443                    let tms: DateTime<Utc> = serde_json::from_value(raw.to_owned())
444                        .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?;
445                    tms
446                },
447            })),
448            "session.stop" => Ok(Self::SessionStop(SessionStopEvent {
449                session_id,
450                success: value
451                    .params
452                    .get("success")
453                    .and_then(|v| v.as_bool())
454                    .ok_or_else(|| AgentEventError::InvalidFieldType("success".to_string()))?,
455                result: value
456                    .params
457                    .get("result")
458                    .and_then(|v| v.as_str())
459                    .map(|s| s.to_string()),
460                error: value
461                    .params
462                    .get("error")
463                    .and_then(|v| v.as_str())
464                    .map(|s| s.to_string()),
465                timestamp: {
466                    let raw = value
467                        .params
468                        .get("timestamp")
469                        .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?;
470                    let tms: DateTime<Utc> = serde_json::from_value(raw.to_owned())
471                        .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?;
472                    tms
473                },
474                usage: {
475                    let raw = value
476                        .params
477                        .get("usage")
478                        .ok_or_else(|| AgentEventError::MissingField("usage".to_string()))?;
479                    let usg: Usage = serde_json::from_value(raw.to_owned())
480                        .map_err(|_| AgentEventError::InvalidFieldType("usage".to_string()))?;
481                    usg
482                },
483            })),
484            "user.prompt.submit" => Ok(Self::UserPromptSubmit(UserPromptSubmitEvent {
485                session_id,
486                turn_id,
487                prompt: value
488                    .params
489                    .get("prompt")
490                    .and_then(|v| v.as_str())
491                    .ok_or_else(|| AgentEventError::MissingField("prompt".to_string()))?
492                    .to_string(),
493                timestamp: {
494                    let raw = value
495                        .params
496                        .get("timestamp")
497                        .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?;
498                    let tms: DateTime<Utc> = serde_json::from_value(raw.to_owned())
499                        .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?;
500                    tms
501                },
502            })),
503            "stream.delta" => Ok(Self::StreamDelta(StreamDeltaEvent {
504                session_id,
505                turn_id,
506                delta: value
507                    .params
508                    .get("delta")
509                    .and_then(|v| v.as_str())
510                    .ok_or_else(|| AgentEventError::MissingField("delta".to_string()))?
511                    .to_string(),
512                delta_type: {
513                    let raw = value
514                        .params
515                        .get("delta_type")
516                        .ok_or_else(|| AgentEventError::MissingField("delta_type".into()))?;
517                    let dt: DeltaType = serde_json::from_value(raw.to_owned())
518                        .map_err(|_| AgentEventError::InvalidFieldType("delta_type".to_string()))?;
519                    dt
520                },
521                timestamp: {
522                    let raw = value
523                        .params
524                        .get("timestamp")
525                        .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?;
526                    let tms: DateTime<Utc> = serde_json::from_value(raw.to_owned())
527                        .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?;
528                    tms
529                },
530            })),
531            "tool.call" => Ok(Self::ToolCall(ToolCallEvent {
532                session_id,
533                turn_id,
534                name: value
535                    .params
536                    .get("name")
537                    .and_then(|v| v.as_str())
538                    .ok_or_else(|| AgentEventError::MissingField("name".to_string()))?
539                    .to_string(),
540                input: value.params.get("input").cloned().unwrap_or(Value::Null),
541                timestamp: {
542                    let raw = value
543                        .params
544                        .get("timestamp")
545                        .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?;
546                    let tms: DateTime<Utc> = serde_json::from_value(raw.to_owned())
547                        .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?;
548                    tms
549                },
550            })),
551            "tool.result" => {
552                let result = value
553                    .params
554                    .get("result")
555                    .ok_or_else(|| AgentEventError::MissingField("result".to_string()))?;
556                let tool_result: ToolResult = serde_json::from_value(result.to_owned())
557                    .map_err(|_| AgentEventError::InvalidFieldType("result".to_string()))?;
558                Ok(Self::ToolResult(ToolResultEvent {
559                    session_id,
560                    turn_id,
561                    result: tool_result,
562                    tool_call_id: value
563                        .params
564                        .get("tool_call_id")
565                        .and_then(|v| v.as_str())
566                        .ok_or_else(|| AgentEventError::MissingField("tool_call_id".to_string()))?
567                        .to_string(),
568                    timestamp: {
569                        let raw = value.params.get("timestamp").ok_or_else(|| {
570                            AgentEventError::MissingField("timestamp".to_string())
571                        })?;
572                        let tms: DateTime<Utc> =
573                            serde_json::from_value(raw.to_owned()).map_err(|_| {
574                                AgentEventError::InvalidFieldType("timestamp".to_string())
575                            })?;
576                        tms
577                    },
578                }))
579            }
580            "skill.load" => Ok(Self::SkillLoad(SkillLoadEvent {
581                session_id,
582                turn_id,
583                skill_name: value
584                    .params
585                    .get("skill_name")
586                    .and_then(|v| v.as_str())
587                    .ok_or_else(|| AgentEventError::MissingField("skill_name".to_string()))?
588                    .to_string(),
589                timestamp: {
590                    let raw = value
591                        .params
592                        .get("timestamp")
593                        .ok_or_else(|| AgentEventError::MissingField("timestamp".to_string()))?;
594                    let tms: DateTime<Utc> = serde_json::from_value(raw.to_owned())
595                        .map_err(|_| AgentEventError::InvalidFieldType("timestamp".to_string()))?;
596                    tms
597                },
598            })),
599            "assistant.response" => {
600                let tool_calls = value.params.get("tool_calls").and_then(|v| match v {
601                    Value::Array(arr) => arr
602                        .iter()
603                        .map(|a| {
604                            let tc: Result<ToolCall, AgentEventError> =
605                                serde_json::from_value(a.to_owned()).map_err(|_| {
606                                    AgentEventError::InvalidFieldType("tool_calls".into())
607                                });
608                            tc
609                        })
610                        .collect::<Result<Vec<_>, _>>()
611                        .ok(),
612                    _ => None,
613                });
614                Ok(Self::AssistantResponse(AssistantResponseEvent {
615                    session_id,
616                    turn_id,
617                    full_text: value
618                        .params
619                        .get("full_text")
620                        .and_then(|v| v.as_str())
621                        .ok_or_else(|| AgentEventError::MissingField("full_text".to_string()))?
622                        .to_string(),
623                    tool_calls,
624                    timestamp: {
625                        let raw = value.params.get("timestamp").ok_or_else(|| {
626                            AgentEventError::MissingField("timestamp".to_string())
627                        })?;
628                        let tms: DateTime<Utc> =
629                            serde_json::from_value(raw.to_owned()).map_err(|_| {
630                                AgentEventError::InvalidFieldType("timestamp".to_string())
631                            })?;
632                        tms
633                    },
634                }))
635            }
636            method => Err(AgentEventError::UnknownMethod(method.to_string())),
637        }
638    }
639}
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644    use serde_json::json;
645
646    #[test]
647    fn session_init_type_from_value() {
648        let start: SessionInitType = serde_json::from_value(Value::from("Start"))
649            .expect("Should be able to convert to SessionInitType");
650        let resume: SessionInitType = serde_json::from_value(Value::from("Resume"))
651            .expect("Should be able to convert to SessionInitType");
652        assert!(matches!(start, SessionInitType::Start));
653        assert!(matches!(resume, SessionInitType::Resume));
654    }
655
656    #[test]
657    fn session_init_type_from_str_ok() {
658        assert!(matches!(
659            SessionInitType::try_from("start"),
660            Ok(SessionInitType::Start)
661        ));
662        assert!(matches!(
663            SessionInitType::try_from("resume"),
664            Ok(SessionInitType::Resume)
665        ));
666    }
667
668    #[test]
669    fn session_init_type_from_str_err() {
670        let err = SessionInitType::try_from("unknown").unwrap_err();
671        assert!(matches!(err, AgentEventError::InvalidFieldType(_)));
672        assert!(
673            err.to_string()
674                .contains("No init type with message: unknown")
675        );
676    }
677
678    #[test]
679    fn delta_type_from_value() {
680        assert_eq!(Value::from(DeltaType::Text), Value::from("text"));
681        assert_eq!(Value::from(DeltaType::Thinking), Value::from("thinking"));
682    }
683
684    #[test]
685    fn session_init_event_to_jsonrpc() {
686        let event = SessionInitEvent {
687            session_id: "s1".into(),
688            model: "gpt-4".into(),
689            provider: "openai".into(),
690            system: "sys".into(),
691            init_type: SessionInitType::Start,
692            timestamp: Utc::now(),
693        };
694        let rpc = event.to_jsonrpc();
695        assert_eq!(rpc.method, "session.init");
696        assert_eq!(rpc.params.get("session_id"), Some(&Value::from("s1")));
697        assert_eq!(rpc.params.get("model"), Some(&Value::from("gpt-4")));
698        assert_eq!(rpc.params.get("provider"), Some(&Value::from("openai")));
699        assert_eq!(rpc.params.get("system"), Some(&Value::from("sys")));
700        assert_eq!(rpc.params.get("init_type"), Some(&Value::from("Start")));
701    }
702
703    #[test]
704    fn session_stop_event_to_jsonrpc() {
705        let event = SessionStopEvent {
706            session_id: "s1".into(),
707            success: true,
708            result: Some("done".into()),
709            error: None,
710            timestamp: Utc::now(),
711            usage: Usage::default(),
712        };
713        let rpc = event.to_jsonrpc();
714        assert_eq!(rpc.method, "session.stop");
715        assert_eq!(rpc.params.get("success"), Some(&Value::from(true)));
716        assert_eq!(rpc.params.get("result"), Some(&Value::from("done")));
717        assert_eq!(rpc.params.get("error"), Some(&Value::Null));
718        assert_eq!(
719            rpc.params.get("usage"),
720            Some(
721                &serde_json::to_value(Usage::default())
722                    .expect("Should be able to convert to value")
723            )
724        );
725    }
726
727    #[test]
728    fn user_prompt_submit_event_to_jsonrpc() {
729        let event = UserPromptSubmitEvent {
730            session_id: "s1".into(),
731            turn_id: "t1".into(),
732            prompt: "hello".into(),
733            timestamp: Utc::now(),
734        };
735        let rpc = event.to_jsonrpc();
736        assert_eq!(rpc.method, "user.prompt.submit");
737        assert_eq!(rpc.params.get("prompt"), Some(&Value::from("hello")));
738    }
739
740    #[test]
741    fn stream_delta_event_to_jsonrpc() {
742        let event = StreamDeltaEvent {
743            session_id: "s1".into(),
744            turn_id: "t1".into(),
745            delta: "world".into(),
746            delta_type: DeltaType::Thinking,
747            timestamp: Utc::now(),
748        };
749        let rpc = event.to_jsonrpc();
750        assert_eq!(rpc.method, "stream.delta");
751        assert_eq!(rpc.params.get("delta"), Some(&Value::from("world")));
752        assert_eq!(rpc.params.get("delta_type"), Some(&Value::from("Thinking")));
753    }
754
755    #[test]
756    fn tool_call_event_to_jsonrpc() {
757        let event = ToolCallEvent {
758            session_id: "s1".into(),
759            turn_id: "t1".into(),
760            name: "read".into(),
761            input: json!({"path": "/tmp"}),
762            timestamp: Utc::now(),
763        };
764        let rpc = event.to_jsonrpc();
765        assert_eq!(rpc.method, "tool.call");
766        assert_eq!(rpc.params.get("name"), Some(&Value::from("read")));
767        assert_eq!(rpc.params.get("input"), Some(&json!({"path": "/tmp"})));
768    }
769
770    #[test]
771    fn tool_result_event_to_jsonrpc() {
772        let event = ToolResultEvent {
773            session_id: "s1".into(),
774            turn_id: "t1".into(),
775            result: ToolResult::Ok("ok".into()),
776            tool_call_id: "tc1".into(),
777            timestamp: Utc::now(),
778        };
779        let rpc = event.to_jsonrpc();
780        assert_eq!(rpc.method, "tool.result");
781        assert_eq!(rpc.params.get("tool_call_id"), Some(&Value::from("tc1")));
782        assert_eq!(rpc.params.get("result"), Some(&json!({"Ok": "ok"})));
783    }
784
785    #[test]
786    fn skill_load_event_to_jsonrpc() {
787        let event = SkillLoadEvent {
788            session_id: "s1".into(),
789            turn_id: "t1".into(),
790            skill_name: "coding".into(),
791            timestamp: Utc::now(),
792        };
793        let rpc = event.to_jsonrpc();
794        assert_eq!(rpc.method, "skill.load");
795        assert_eq!(rpc.params.get("skill_name"), Some(&Value::from("coding")));
796    }
797
798    #[test]
799    fn assistant_response_event_to_jsonrpc() {
800        let event = AssistantResponseEvent {
801            session_id: "s1".into(),
802            turn_id: "t1".into(),
803            full_text: "hi".into(),
804            tool_calls: None,
805            timestamp: Utc::now(),
806        };
807        let rpc = event.to_jsonrpc();
808        assert_eq!(rpc.method, "assistant.response");
809        assert_eq!(rpc.params.get("full_text"), Some(&Value::from("hi")));
810        assert_eq!(rpc.params.get("tool_calls"), Some(&Value::Null));
811    }
812
813    #[test]
814    fn agent_event_any_session_id() {
815        let event = AgentEventAny::SessionInit(SessionInitEvent {
816            session_id: "sid".into(),
817            model: "m".into(),
818            provider: "p".into(),
819            system: "s".into(),
820            init_type: SessionInitType::Start,
821            timestamp: Utc::now(),
822        });
823        assert_eq!(event.session_id(), "sid");
824    }
825
826    #[test]
827    fn agent_event_any_to_jsonrpc_roundtrip() {
828        let event = AgentEventAny::UserPromptSubmit(UserPromptSubmitEvent {
829            session_id: "s1".into(),
830            turn_id: "t1".into(),
831            prompt: "p".into(),
832            timestamp: Utc::now(),
833        });
834        let rpc = event.to_jsonrpc();
835        assert_eq!(rpc.method, "user.prompt.submit");
836        assert_eq!(rpc.params.get("session_id"), Some(&Value::from("s1")));
837    }
838
839    #[test]
840    fn try_from_jsonrpc_session_init_ok() {
841        let rpc = JsonRpcNotification::builder()
842            .method("session.init".into())
843            .add_param("session_id".into(), Value::from("s1"))
844            .add_param("model".into(), Value::from("gpt-4"))
845            .add_param("provider".into(), Value::from("openai"))
846            .add_param("system".into(), Value::from("sys"))
847            .add_param("init_type".into(), Value::from("Resume"))
848            .add_param("timestamp".into(), {
849                let tms = Utc::now();
850                serde_json::to_value(tms).expect("Should convert to value")
851            });
852        let any = AgentEventAny::try_from(rpc).unwrap();
853        assert!(
854            matches!(any, AgentEventAny::SessionInit(ref e) if e.session_id == "s1" && matches!(e.init_type, SessionInitType::Resume))
855        );
856    }
857
858    #[test]
859    fn try_from_jsonrpc_session_init_missing_field() {
860        let rpc = JsonRpcNotification::builder()
861            .method("session.init".into())
862            .add_param("session_id".into(), Value::from("s1"))
863            .add_param("timestamp".into(), {
864                let tms = Utc::now();
865                serde_json::to_value(tms).expect("Should convert to value")
866            });
867        let err = AgentEventAny::try_from(rpc).unwrap_err();
868        assert!(matches!(err, AgentEventError::MissingField(_)));
869    }
870
871    #[test]
872    fn try_from_jsonrpc_session_init_invalid_init_type() {
873        let rpc = JsonRpcNotification::builder()
874            .method("session.init".into())
875            .add_param("session_id".into(), Value::from("s1"))
876            .add_param("model".into(), Value::from("gpt-4"))
877            .add_param("provider".into(), Value::from("openai"))
878            .add_param("system".into(), Value::from("sys"))
879            .add_param("init_type".into(), Value::from("invalid"))
880            .add_param("timestamp".into(), {
881                let tms = Utc::now();
882                serde_json::to_value(tms).expect("Should convert to value")
883            });
884        let err = AgentEventAny::try_from(rpc).unwrap_err();
885        assert!(matches!(err, AgentEventError::InvalidFieldType(_)));
886    }
887
888    #[test]
889    fn try_from_jsonrpc_session_stop_ok() {
890        let rpc = JsonRpcNotification::builder()
891            .method("session.stop".into())
892            .add_param("session_id".into(), Value::from("s1"))
893            .add_param("success".into(), Value::from(true))
894            .add_param("result".into(), Value::from("done"))
895            .add_param("error".into(), Value::Null)
896            .add_param("timestamp".into(), {
897                let tms = Utc::now();
898                serde_json::to_value(tms).expect("Should convert to value")
899            })
900            .add_param("usage".into(), {
901                let usg = Usage::default();
902                serde_json::to_value(usg).expect("Should convert to value")
903            });
904        let any = AgentEventAny::try_from(rpc).unwrap();
905        assert!(
906            matches!(any, AgentEventAny::SessionStop(ref e) if e.success && e.result == Some("done".into()) && e.error.is_none() && e.usage.latency == 0)
907        );
908    }
909
910    #[test]
911    fn try_from_jsonrpc_user_prompt_submit_ok() {
912        let rpc = JsonRpcNotification::builder()
913            .method("user.prompt.submit".into())
914            .add_param("session_id".into(), Value::from("s1"))
915            .add_param("turn_id".into(), Value::from("t1"))
916            .add_param("prompt".into(), Value::from("hello"))
917            .add_param("timestamp".into(), {
918                let tms = Utc::now();
919                serde_json::to_value(tms).expect("Should convert to value")
920            });
921        let any = AgentEventAny::try_from(rpc).unwrap();
922        assert!(matches!(any, AgentEventAny::UserPromptSubmit(ref e) if e.prompt == "hello"));
923    }
924
925    #[test]
926    fn try_from_jsonrpc_stream_delta_no_default() {
927        let rpc = JsonRpcNotification::builder()
928            .method("stream.delta".into())
929            .add_param("session_id".into(), Value::from("s1"))
930            .add_param("turn_id".into(), Value::from("t1"))
931            .add_param("delta".into(), Value::from("d"))
932            .add_param("timestamp".into(), {
933                let tms = Utc::now();
934                serde_json::to_value(tms).expect("Should convert to value")
935            });
936        let any = AgentEventAny::try_from(rpc);
937        assert!(any.is_err_and(
938            |e| matches!(e, AgentEventError::MissingField(ref err) if err == "delta_type")
939        ));
940    }
941
942    #[test]
943    fn try_from_jsonrpc_stream_delta_thinking() {
944        let rpc = JsonRpcNotification::builder()
945            .method("stream.delta".into())
946            .add_param("session_id".into(), Value::from("s1"))
947            .add_param("turn_id".into(), Value::from("t1"))
948            .add_param("delta".into(), Value::from("d"))
949            .add_param(
950                "delta_type".into(),
951                serde_json::to_value(DeltaType::Thinking)
952                    .expect("Should be able to convert to value"),
953            )
954            .add_param("timestamp".into(), {
955                let tms = Utc::now();
956                serde_json::to_value(tms).expect("Should convert to value")
957            });
958        let any = AgentEventAny::try_from(rpc).unwrap();
959        assert!(
960            matches!(any, AgentEventAny::StreamDelta(ref e) if matches!(e.delta_type, DeltaType::Thinking))
961        );
962    }
963
964    #[test]
965    fn try_from_jsonrpc_tool_call_ok() {
966        let rpc = JsonRpcNotification::builder()
967            .method("tool.call".into())
968            .add_param("session_id".into(), Value::from("s1"))
969            .add_param("turn_id".into(), Value::from("t1"))
970            .add_param("name".into(), Value::from("read"))
971            .add_param("input".into(), json!({"path": "/tmp"}))
972            .add_param("timestamp".into(), {
973                let tms = Utc::now();
974                serde_json::to_value(tms).expect("Should convert to value")
975            });
976        let any = AgentEventAny::try_from(rpc).unwrap();
977        assert!(matches!(any, AgentEventAny::ToolCall(ref e) if e.name == "read"));
978    }
979
980    #[test]
981    fn try_from_jsonrpc_tool_result_ok() {
982        let rpc = JsonRpcNotification::builder()
983            .method("tool.result".into())
984            .add_param("session_id".into(), Value::from("s1"))
985            .add_param("turn_id".into(), Value::from("t1"))
986            .add_param("tool_call_id".into(), Value::from("tc1"))
987            .add_param("result".into(), json!({"Ok": "ok"}))
988            .add_param("timestamp".into(), {
989                let tms = Utc::now();
990                serde_json::to_value(tms).expect("Should convert to value")
991            });
992        let any = AgentEventAny::try_from(rpc).unwrap();
993        assert!(
994            matches!(any, AgentEventAny::ToolResult(ref e) if matches!(e.result, ToolResult::Ok(ref s) if s == "ok"))
995        );
996    }
997
998    #[test]
999    fn try_from_jsonrpc_skill_load_ok() {
1000        let rpc = JsonRpcNotification::builder()
1001            .method("skill.load".into())
1002            .add_param("session_id".into(), Value::from("s1"))
1003            .add_param("turn_id".into(), Value::from("t1"))
1004            .add_param("skill_name".into(), Value::from("coding"))
1005            .add_param("timestamp".into(), {
1006                let tms = Utc::now();
1007                serde_json::to_value(tms).expect("Should convert to value")
1008            });
1009        let any = AgentEventAny::try_from(rpc).unwrap();
1010        assert!(matches!(any, AgentEventAny::SkillLoad(ref e) if e.skill_name == "coding"));
1011    }
1012
1013    #[test]
1014    fn try_from_jsonrpc_assistant_response_ok() {
1015        let rpc = JsonRpcNotification::builder()
1016            .method("assistant.response".into())
1017            .add_param("session_id".into(), Value::from("s1"))
1018            .add_param("turn_id".into(), Value::from("t1"))
1019            .add_param("full_text".into(), Value::from("hi"))
1020            .add_param("timestamp".into(), {
1021                let tms = Utc::now();
1022                serde_json::to_value(tms).expect("Should convert to value")
1023            });
1024        let any = AgentEventAny::try_from(rpc).unwrap();
1025        assert!(
1026            matches!(any, AgentEventAny::AssistantResponse(ref e) if e.full_text == "hi" && e.tool_calls.is_none())
1027        );
1028    }
1029
1030    #[test]
1031    fn try_from_jsonrpc_assistant_response_with_tool_calls() {
1032        let rpc = JsonRpcNotification::builder()
1033            .method("assistant.response".into())
1034            .add_param("session_id".into(), Value::from("s1"))
1035            .add_param("turn_id".into(), Value::from("t1"))
1036            .add_param("full_text".into(), Value::from("hi"))
1037            .add_param("tool_calls".into(), json!([{"call_type":"function","id":"1","function":{"name":"tool","arguments":"{}"}}]))
1038            .add_param("timestamp".into(), {
1039                let tms = Utc::now();
1040                serde_json::to_value(tms).expect("Should convert to value")
1041            });
1042        let any = AgentEventAny::try_from(rpc).unwrap();
1043        assert!(matches!(any, AgentEventAny::AssistantResponse(ref e) if e.tool_calls.is_some()));
1044    }
1045
1046    #[test]
1047    fn try_from_jsonrpc_unknown_method() {
1048        let rpc = JsonRpcNotification::builder()
1049            .method("unknown".into())
1050            .add_param("session_id".into(), Value::from("s1"))
1051            .add_param("timestamp".into(), {
1052                let tms = Utc::now();
1053                serde_json::to_value(tms).expect("Should convert to value")
1054            });
1055        let err = AgentEventAny::try_from(rpc).unwrap_err();
1056        assert!(matches!(err, AgentEventError::UnknownMethod(ref m) if m == "unknown"));
1057    }
1058
1059    #[test]
1060    fn try_from_jsonrpc_missing_session_id() {
1061        let rpc = JsonRpcNotification::builder()
1062            .method("session.stop".into())
1063            .add_param("success".into(), Value::from(true))
1064            .add_param("timestamp".into(), {
1065                let tms = Utc::now();
1066                serde_json::to_value(tms).expect("Should convert to value")
1067            });
1068        let err = AgentEventAny::try_from(rpc).unwrap_err();
1069        assert!(matches!(err, AgentEventError::MissingField(ref m) if m == "session_id"));
1070    }
1071}