Skip to main content

kimi_wire/protocol/
event.rs

1use serde::{Deserialize, Serialize, Serializer};
2
3use super::content::{ContentPart, ToolReturnValue, UserInput};
4
5/// An event emitted by the agent during a turn.
6///
7/// Events are sent as JSON-RPC notifications (`method: "event"`) and do not
8/// require a response.
9///
10/// Serialization follows the official wire envelope format:
11/// `{"type": "TurnBegin", "payload": {"user_input": ...}}`.
12#[derive(Debug, Clone, PartialEq)]
13#[non_exhaustive]
14pub enum Event {
15    /// A new turn has started with the given user input.
16    TurnBegin {
17        /// The user's input that triggered this turn.
18        user_input: UserInput,
19    },
20    /// The current turn has ended.
21    TurnEnd,
22    /// A new step within the turn has started.
23    StepBegin {
24        /// Step number, starting from 1.
25        n: u32,
26    },
27    /// The current step was interrupted (e.g. by user input).
28    StepInterrupted,
29    /// The current step attempt failed and will be retried.
30    ///
31    /// Added in Wire protocol v1.10.
32    StepRetry {
33        /// Step number.
34        n: u32,
35        /// Next attempt number, 1-based.
36        next_attempt: u32,
37        /// Maximum number of attempts for this step.
38        max_attempts: u32,
39        /// Seconds to wait before retrying.
40        wait_s: u32,
41        /// Exception class name that triggered the retry.
42        error_type: String,
43        /// HTTP status code (if available).
44        status_code: Option<u32>,
45    },
46    /// Context compaction has started.
47    CompactionBegin,
48    /// Context compaction has finished.
49    CompactionEnd,
50    /// Server status update (token usage, context size, etc.).
51    StatusUpdate(StatusUpdate),
52    /// A content part (text, image, etc.) from the model.
53    ContentPart(ContentPart),
54    /// A tool call from the model.
55    ///
56    /// Wire envelope type is `"ToolCall"`. The payload carries an inner
57    /// `type: "function"` discriminator, matching the official v1.10 spec.
58    ToolCall {
59        /// Tool call id.
60        id: String,
61        /// Function name and arguments.
62        function: ToolCallFunction,
63        /// Extra fields from the wire protocol.
64        extras: Option<serde_json::Value>,
65    },
66    /// A partial tool call (streaming arguments).
67    ToolCallPart {
68        /// Partial JSON arguments.
69        arguments_part: Option<String>,
70    },
71    /// Result of a tool execution.
72    ToolResult {
73        /// Id of the corresponding tool call.
74        tool_call_id: String,
75        /// Return value from the tool.
76        return_value: ToolReturnValue,
77    },
78    /// Response to an approval request (sent by the client).
79    ApprovalResponse {
80        /// Id of the approval request.
81        request_id: String,
82        /// Approval decision.
83        response: ApprovalResponseKind,
84        /// Optional feedback text from the user.
85        feedback: Option<String>,
86    },
87    /// An event from a subagent.
88    SubagentEvent {
89        /// Id of the parent tool call that spawned the subagent.
90        parent_tool_call_id: Option<String>,
91        /// Subagent id.
92        agent_id: Option<String>,
93        /// Subagent type.
94        subagent_type: Option<String>,
95        /// Nested wire message in envelope form.
96        event: SubagentEventPayload,
97    },
98    /// Additional user input steering the current turn.
99    SteerInput {
100        /// The steering input.
101        user_input: UserInput,
102    },
103    /// A side question (`/btw`) has started processing.
104    ///
105    /// Added in Wire protocol v1.9.
106    BtwBegin {
107        /// Unique ID to pair with the corresponding `BtwEnd`.
108        id: String,
109        /// The user's original side question text.
110        question: String,
111    },
112    /// A side question (`/btw`) has finished processing.
113    ///
114    /// Added in Wire protocol v1.9.
115    BtwEnd {
116        /// Unique ID matching the corresponding `BtwBegin`.
117        id: String,
118        /// The LLM's response text, or null if it failed.
119        response: Option<String>,
120        /// Error message if the side question failed.
121        error: Option<String>,
122    },
123    /// Plan display content.
124    PlanDisplay {
125        /// Display content.
126        content: String,
127        /// File path associated with the plan.
128        file_path: String,
129    },
130    /// A hook was triggered.
131    HookTriggered {
132        /// Event name.
133        event: String,
134        /// Target of the hook.
135        target: String,
136        /// Number of times this hook has fired.
137        hook_count: u32,
138    },
139    /// A hook was resolved.
140    HookResolved {
141        /// Event name.
142        event: String,
143        /// Target of the hook.
144        target: String,
145        /// Action taken.
146        action: HookAction,
147        /// Reason for the action.
148        reason: String,
149        /// Duration in milliseconds.
150        duration_ms: u64,
151    },
152}
153
154// ---------------------------------------------------------------------------
155// FlatEvent – internal mirror used for (de)serialization
156// ---------------------------------------------------------------------------
157
158#[derive(Serialize, Deserialize)]
159#[serde(tag = "type")]
160pub(crate) enum FlatEvent {
161    TurnBegin {
162        user_input: UserInput,
163    },
164    TurnEnd,
165    StepBegin {
166        n: u32,
167    },
168    StepInterrupted,
169    StepRetry {
170        n: u32,
171        next_attempt: u32,
172        max_attempts: u32,
173        wait_s: u32,
174        error_type: String,
175        #[serde(skip_serializing_if = "Option::is_none")]
176        status_code: Option<u32>,
177    },
178    CompactionBegin,
179    CompactionEnd,
180    StatusUpdate(StatusUpdate),
181    ContentPart(ContentPart),
182    ToolCall {
183        id: String,
184        function: ToolCallFunction,
185        #[serde(skip_serializing_if = "Option::is_none")]
186        extras: Option<serde_json::Value>,
187    },
188    ToolCallPart {
189        #[serde(skip_serializing_if = "Option::is_none")]
190        arguments_part: Option<String>,
191    },
192    ToolResult {
193        tool_call_id: String,
194        return_value: ToolReturnValue,
195    },
196    ApprovalResponse {
197        request_id: String,
198        response: ApprovalResponseKind,
199        #[serde(skip_serializing_if = "Option::is_none")]
200        feedback: Option<String>,
201    },
202    SubagentEvent {
203        #[serde(skip_serializing_if = "Option::is_none")]
204        parent_tool_call_id: Option<String>,
205        #[serde(skip_serializing_if = "Option::is_none")]
206        agent_id: Option<String>,
207        #[serde(skip_serializing_if = "Option::is_none")]
208        subagent_type: Option<String>,
209        event: SubagentEventPayload,
210    },
211    SteerInput {
212        user_input: UserInput,
213    },
214    BtwBegin {
215        id: String,
216        question: String,
217    },
218    BtwEnd {
219        id: String,
220        #[serde(skip_serializing_if = "Option::is_none")]
221        response: Option<String>,
222        #[serde(skip_serializing_if = "Option::is_none")]
223        error: Option<String>,
224    },
225    PlanDisplay {
226        content: String,
227        file_path: String,
228    },
229    HookTriggered {
230        event: String,
231        target: String,
232        hook_count: u32,
233    },
234    HookResolved {
235        event: String,
236        target: String,
237        action: HookAction,
238        reason: String,
239        duration_ms: u64,
240    },
241}
242
243impl From<Event> for FlatEvent {
244    fn from(ev: Event) -> Self {
245        match ev {
246            Event::TurnBegin { user_input } => Self::TurnBegin { user_input },
247            Event::TurnEnd => Self::TurnEnd,
248            Event::StepBegin { n } => Self::StepBegin { n },
249            Event::StepInterrupted => Self::StepInterrupted,
250            Event::StepRetry {
251                n,
252                next_attempt,
253                max_attempts,
254                wait_s,
255                error_type,
256                status_code,
257            } => Self::StepRetry {
258                n,
259                next_attempt,
260                max_attempts,
261                wait_s,
262                error_type,
263                status_code,
264            },
265            Event::CompactionBegin => Self::CompactionBegin,
266            Event::CompactionEnd => Self::CompactionEnd,
267            Event::StatusUpdate(s) => Self::StatusUpdate(s),
268            Event::ContentPart(c) => Self::ContentPart(c),
269            Event::ToolCall {
270                id,
271                function,
272                extras,
273            } => Self::ToolCall {
274                id,
275                function,
276                extras,
277            },
278            Event::ToolCallPart { arguments_part } => Self::ToolCallPart { arguments_part },
279            Event::ToolResult {
280                tool_call_id,
281                return_value,
282            } => Self::ToolResult {
283                tool_call_id,
284                return_value,
285            },
286            Event::ApprovalResponse {
287                request_id,
288                response,
289                feedback,
290            } => Self::ApprovalResponse {
291                request_id,
292                response,
293                feedback,
294            },
295            Event::SubagentEvent {
296                parent_tool_call_id,
297                agent_id,
298                subagent_type,
299                event,
300            } => Self::SubagentEvent {
301                parent_tool_call_id,
302                agent_id,
303                subagent_type,
304                event,
305            },
306            Event::SteerInput { user_input } => Self::SteerInput { user_input },
307            Event::BtwBegin { id, question } => Self::BtwBegin { id, question },
308            Event::BtwEnd {
309                id,
310                response,
311                error,
312            } => Self::BtwEnd {
313                id,
314                response,
315                error,
316            },
317            Event::PlanDisplay { content, file_path } => Self::PlanDisplay { content, file_path },
318            Event::HookTriggered {
319                event,
320                target,
321                hook_count,
322            } => Self::HookTriggered {
323                event,
324                target,
325                hook_count,
326            },
327            Event::HookResolved {
328                event,
329                target,
330                action,
331                reason,
332                duration_ms,
333            } => Self::HookResolved {
334                event,
335                target,
336                action,
337                reason,
338                duration_ms,
339            },
340        }
341    }
342}
343
344impl Event {
345    /// Return the wire type name for this event.
346    ///
347    /// Matches the `type` field in the wire envelope.
348    #[must_use]
349    pub const fn type_name(&self) -> &'static str {
350        match self {
351            Self::TurnBegin { .. } => "TurnBegin",
352            Self::TurnEnd => "TurnEnd",
353            Self::StepBegin { .. } => "StepBegin",
354            Self::StepInterrupted => "StepInterrupted",
355            Self::StepRetry { .. } => "StepRetry",
356            Self::CompactionBegin => "CompactionBegin",
357            Self::CompactionEnd => "CompactionEnd",
358            Self::StatusUpdate(_) => "StatusUpdate",
359            Self::ContentPart(_) => "ContentPart",
360            Self::ToolCall { .. } => "ToolCall",
361            Self::ToolCallPart { .. } => "ToolCallPart",
362            Self::ToolResult { .. } => "ToolResult",
363            Self::ApprovalResponse { .. } => "ApprovalResponse",
364            Self::SubagentEvent { .. } => "SubagentEvent",
365            Self::SteerInput { .. } => "SteerInput",
366            Self::BtwBegin { .. } => "BtwBegin",
367            Self::BtwEnd { .. } => "BtwEnd",
368            Self::PlanDisplay { .. } => "PlanDisplay",
369            Self::HookTriggered { .. } => "HookTriggered",
370            Self::HookResolved { .. } => "HookResolved",
371        }
372    }
373}
374
375impl From<FlatEvent> for Event {
376    fn from(ev: FlatEvent) -> Self {
377        match ev {
378            FlatEvent::TurnBegin { user_input } => Self::TurnBegin { user_input },
379            FlatEvent::TurnEnd => Self::TurnEnd,
380            FlatEvent::StepBegin { n } => Self::StepBegin { n },
381            FlatEvent::StepInterrupted => Self::StepInterrupted,
382            FlatEvent::StepRetry {
383                n,
384                next_attempt,
385                max_attempts,
386                wait_s,
387                error_type,
388                status_code,
389            } => Self::StepRetry {
390                n,
391                next_attempt,
392                max_attempts,
393                wait_s,
394                error_type,
395                status_code,
396            },
397            FlatEvent::CompactionBegin => Self::CompactionBegin,
398            FlatEvent::CompactionEnd => Self::CompactionEnd,
399            FlatEvent::StatusUpdate(s) => Self::StatusUpdate(s),
400            FlatEvent::ContentPart(c) => Self::ContentPart(c),
401            FlatEvent::ToolCall {
402                id,
403                function,
404                extras,
405            } => Self::ToolCall {
406                id,
407                function,
408                extras,
409            },
410            FlatEvent::ToolCallPart { arguments_part } => Self::ToolCallPart { arguments_part },
411            FlatEvent::ToolResult {
412                tool_call_id,
413                return_value,
414            } => Self::ToolResult {
415                tool_call_id,
416                return_value,
417            },
418            FlatEvent::ApprovalResponse {
419                request_id,
420                response,
421                feedback,
422            } => Self::ApprovalResponse {
423                request_id,
424                response,
425                feedback,
426            },
427            FlatEvent::SubagentEvent {
428                parent_tool_call_id,
429                agent_id,
430                subagent_type,
431                event,
432            } => Self::SubagentEvent {
433                parent_tool_call_id,
434                agent_id,
435                subagent_type,
436                event,
437            },
438            FlatEvent::SteerInput { user_input } => Self::SteerInput { user_input },
439            FlatEvent::BtwBegin { id, question } => Self::BtwBegin { id, question },
440            FlatEvent::BtwEnd {
441                id,
442                response,
443                error,
444            } => Self::BtwEnd {
445                id,
446                response,
447                error,
448            },
449            FlatEvent::PlanDisplay { content, file_path } => {
450                Self::PlanDisplay { content, file_path }
451            }
452            FlatEvent::HookTriggered {
453                event,
454                target,
455                hook_count,
456            } => Self::HookTriggered {
457                event,
458                target,
459                hook_count,
460            },
461            FlatEvent::HookResolved {
462                event,
463                target,
464                action,
465                reason,
466                duration_ms,
467            } => Self::HookResolved {
468                event,
469                target,
470                action,
471                reason,
472                duration_ms,
473            },
474        }
475    }
476}
477
478// ---------------------------------------------------------------------------
479// EventEnvelope – {type, payload} wire format
480// ---------------------------------------------------------------------------
481
482#[derive(Serialize, Deserialize)]
483struct EventEnvelope {
484    #[serde(rename = "type")]
485    type_name: String,
486    payload: serde_json::Value,
487}
488
489impl Serialize for Event {
490    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
491        match self {
492            // ContentPart carries its own "type" field (e.g. "text", "image_url").
493            // We must not strip it, otherwise deserialization fails.
494            Self::ContentPart(part) => {
495                let payload = serde_json::to_value(part).map_err(serde::ser::Error::custom)?;
496                EventEnvelope {
497                    type_name: "ContentPart".to_string(),
498                    payload,
499                }
500                .serialize(serializer)
501            }
502            // ToolCall payload carries an inner `type: "function"` discriminator
503            // that must be preserved in the payload, separate from the envelope type.
504            Self::ToolCall {
505                id,
506                function,
507                extras,
508            } => {
509                #[derive(Serialize)]
510                struct ToolCallPayload<'a> {
511                    #[serde(rename = "type")]
512                    type_name: &'a str,
513                    id: &'a str,
514                    function: &'a ToolCallFunction,
515                    #[serde(skip_serializing_if = "Option::is_none")]
516                    extras: &'a Option<serde_json::Value>,
517                }
518                let payload = serde_json::to_value(&ToolCallPayload {
519                    type_name: "function",
520                    id,
521                    function,
522                    extras,
523                })
524                .map_err(serde::ser::Error::custom)?;
525                EventEnvelope {
526                    type_name: "ToolCall".to_string(),
527                    payload,
528                }
529                .serialize(serializer)
530            }
531            _ => {
532                let flat = FlatEvent::from(self.clone());
533                let mut value = serde_json::to_value(&flat).map_err(serde::ser::Error::custom)?;
534                let obj = value
535                    .as_object_mut()
536                    .ok_or_else(|| serde::ser::Error::custom("expected object"))?;
537                let type_name = obj
538                    .remove("type")
539                    .and_then(|v| v.as_str().map(String::from))
540                    .ok_or_else(|| serde::ser::Error::custom("missing type"))?;
541                EventEnvelope {
542                    type_name,
543                    payload: value,
544                }
545                .serialize(serializer)
546            }
547        }
548    }
549}
550
551impl<'de> Deserialize<'de> for Event {
552    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
553        let envelope = EventEnvelope::deserialize(deserializer)?;
554        if envelope.type_name.as_str() == "ContentPart" {
555            let part: ContentPart =
556                serde_json::from_value(envelope.payload).map_err(serde::de::Error::custom)?;
557            Ok(Self::ContentPart(part))
558        } else {
559            let mut value = envelope.payload;
560            if let Some(obj) = value.as_object_mut() {
561                obj.insert(
562                    "type".to_string(),
563                    serde_json::Value::String(envelope.type_name),
564                );
565            }
566            let flat: FlatEvent =
567                serde_json::from_value(value).map_err(serde::de::Error::custom)?;
568            Ok(Self::from(flat))
569        }
570    }
571}
572
573/// Payload of a [`Event::SubagentEvent`].
574///
575/// This is a generic `{type, payload}` envelope rather than a strongly-typed
576/// [`Event`] because subagent events may be any wire message type.
577#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
578pub struct SubagentEventPayload {
579    /// The wire type name of the subagent event.
580    #[serde(rename = "type")]
581    pub type_name: String,
582    /// The raw payload of the subagent event.
583    pub payload: serde_json::Value,
584}
585
586/// Status update from the server.
587#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
588pub struct StatusUpdate {
589    /// Fraction of context window used (0.0–1.0).
590    #[serde(skip_serializing_if = "Option::is_none")]
591    pub context_usage: Option<f64>,
592    /// Number of context tokens used.
593    #[serde(skip_serializing_if = "Option::is_none")]
594    pub context_tokens: Option<u64>,
595    /// Maximum context tokens allowed.
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub max_context_tokens: Option<u64>,
598    /// Detailed token usage breakdown.
599    #[serde(skip_serializing_if = "Option::is_none")]
600    pub token_usage: Option<TokenUsage>,
601    /// Server-assigned message id.
602    #[serde(skip_serializing_if = "Option::is_none")]
603    pub message_id: Option<String>,
604    /// Whether plan mode is active. `null` means no change.
605    #[serde(skip_serializing_if = "Option::is_none")]
606    pub plan_mode: Option<bool>,
607}
608
609/// Token usage breakdown.
610#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
611pub struct TokenUsage {
612    /// Input tokens excluding `input_cache_read` and `input_cache_creation`.
613    pub input_other: u64,
614    /// Total output tokens.
615    pub output: u64,
616    /// Cached input tokens.
617    pub input_cache_read: u64,
618    /// Input tokens used for cache creation (currently only Anthropic API).
619    pub input_cache_creation: u64,
620}
621
622/// Function name and arguments for a tool call.
623#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
624pub struct ToolCallFunction {
625    /// Function name.
626    pub name: String,
627    /// JSON-encoded arguments.
628    #[serde(skip_serializing_if = "Option::is_none")]
629    pub arguments: Option<String>,
630}
631
632/// Client's response to an approval request.
633#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
634#[serde(rename_all = "snake_case")]
635#[non_exhaustive]
636pub enum ApprovalResponseKind {
637    /// Approve this request once.
638    Approve,
639    /// Approve this request and remember for the session.
640    #[serde(rename = "approve_for_session")]
641    ApproveForSession,
642    /// Reject this request.
643    Reject,
644}
645
646/// Action taken by a hook.
647#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
648#[serde(rename_all = "snake_case")]
649#[non_exhaustive]
650pub enum HookAction {
651    /// Allow the operation to proceed.
652    Allow,
653    /// Block the operation.
654    Block,
655}