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 } => FlatEvent::TurnBegin { user_input },
247            Event::TurnEnd => FlatEvent::TurnEnd,
248            Event::StepBegin { n } => FlatEvent::StepBegin { n },
249            Event::StepInterrupted => FlatEvent::StepInterrupted,
250            Event::StepRetry {
251                n,
252                next_attempt,
253                max_attempts,
254                wait_s,
255                error_type,
256                status_code,
257            } => FlatEvent::StepRetry {
258                n,
259                next_attempt,
260                max_attempts,
261                wait_s,
262                error_type,
263                status_code,
264            },
265            Event::CompactionBegin => FlatEvent::CompactionBegin,
266            Event::CompactionEnd => FlatEvent::CompactionEnd,
267            Event::StatusUpdate(s) => FlatEvent::StatusUpdate(s),
268            Event::ContentPart(c) => FlatEvent::ContentPart(c),
269            Event::ToolCall {
270                id,
271                function,
272                extras,
273            } => FlatEvent::ToolCall {
274                id,
275                function,
276                extras,
277            },
278            Event::ToolCallPart { arguments_part } => FlatEvent::ToolCallPart { arguments_part },
279            Event::ToolResult {
280                tool_call_id,
281                return_value,
282            } => FlatEvent::ToolResult {
283                tool_call_id,
284                return_value,
285            },
286            Event::ApprovalResponse {
287                request_id,
288                response,
289                feedback,
290            } => FlatEvent::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            } => FlatEvent::SubagentEvent {
301                parent_tool_call_id,
302                agent_id,
303                subagent_type,
304                event,
305            },
306            Event::SteerInput { user_input } => FlatEvent::SteerInput { user_input },
307            Event::BtwBegin { id, question } => FlatEvent::BtwBegin { id, question },
308            Event::BtwEnd {
309                id,
310                response,
311                error,
312            } => FlatEvent::BtwEnd {
313                id,
314                response,
315                error,
316            },
317            Event::PlanDisplay { content, file_path } => {
318                FlatEvent::PlanDisplay { content, file_path }
319            }
320            Event::HookTriggered {
321                event,
322                target,
323                hook_count,
324            } => FlatEvent::HookTriggered {
325                event,
326                target,
327                hook_count,
328            },
329            Event::HookResolved {
330                event,
331                target,
332                action,
333                reason,
334                duration_ms,
335            } => FlatEvent::HookResolved {
336                event,
337                target,
338                action,
339                reason,
340                duration_ms,
341            },
342        }
343    }
344}
345
346impl Event {
347    /// Return the wire type name for this event.
348    ///
349    /// Matches the `type` field in the wire envelope.
350    #[must_use]
351    pub const fn type_name(&self) -> &'static str {
352        match self {
353            Event::TurnBegin { .. } => "TurnBegin",
354            Event::TurnEnd => "TurnEnd",
355            Event::StepBegin { .. } => "StepBegin",
356            Event::StepInterrupted => "StepInterrupted",
357            Event::StepRetry { .. } => "StepRetry",
358            Event::CompactionBegin => "CompactionBegin",
359            Event::CompactionEnd => "CompactionEnd",
360            Event::StatusUpdate(_) => "StatusUpdate",
361            Event::ContentPart(_) => "ContentPart",
362            Event::ToolCall { .. } => "ToolCall",
363            Event::ToolCallPart { .. } => "ToolCallPart",
364            Event::ToolResult { .. } => "ToolResult",
365            Event::ApprovalResponse { .. } => "ApprovalResponse",
366            Event::SubagentEvent { .. } => "SubagentEvent",
367            Event::SteerInput { .. } => "SteerInput",
368            Event::BtwBegin { .. } => "BtwBegin",
369            Event::BtwEnd { .. } => "BtwEnd",
370            Event::PlanDisplay { .. } => "PlanDisplay",
371            Event::HookTriggered { .. } => "HookTriggered",
372            Event::HookResolved { .. } => "HookResolved",
373        }
374    }
375}
376
377impl From<FlatEvent> for Event {
378    fn from(ev: FlatEvent) -> Self {
379        match ev {
380            FlatEvent::TurnBegin { user_input } => Event::TurnBegin { user_input },
381            FlatEvent::TurnEnd => Event::TurnEnd,
382            FlatEvent::StepBegin { n } => Event::StepBegin { n },
383            FlatEvent::StepInterrupted => Event::StepInterrupted,
384            FlatEvent::StepRetry {
385                n,
386                next_attempt,
387                max_attempts,
388                wait_s,
389                error_type,
390                status_code,
391            } => Event::StepRetry {
392                n,
393                next_attempt,
394                max_attempts,
395                wait_s,
396                error_type,
397                status_code,
398            },
399            FlatEvent::CompactionBegin => Event::CompactionBegin,
400            FlatEvent::CompactionEnd => Event::CompactionEnd,
401            FlatEvent::StatusUpdate(s) => Event::StatusUpdate(s),
402            FlatEvent::ContentPart(c) => Event::ContentPart(c),
403            FlatEvent::ToolCall {
404                id,
405                function,
406                extras,
407            } => Event::ToolCall {
408                id,
409                function,
410                extras,
411            },
412            FlatEvent::ToolCallPart { arguments_part } => Event::ToolCallPart { arguments_part },
413            FlatEvent::ToolResult {
414                tool_call_id,
415                return_value,
416            } => Event::ToolResult {
417                tool_call_id,
418                return_value,
419            },
420            FlatEvent::ApprovalResponse {
421                request_id,
422                response,
423                feedback,
424            } => Event::ApprovalResponse {
425                request_id,
426                response,
427                feedback,
428            },
429            FlatEvent::SubagentEvent {
430                parent_tool_call_id,
431                agent_id,
432                subagent_type,
433                event,
434            } => Event::SubagentEvent {
435                parent_tool_call_id,
436                agent_id,
437                subagent_type,
438                event,
439            },
440            FlatEvent::SteerInput { user_input } => Event::SteerInput { user_input },
441            FlatEvent::BtwBegin { id, question } => Event::BtwBegin { id, question },
442            FlatEvent::BtwEnd {
443                id,
444                response,
445                error,
446            } => Event::BtwEnd {
447                id,
448                response,
449                error,
450            },
451            FlatEvent::PlanDisplay { content, file_path } => {
452                Event::PlanDisplay { content, file_path }
453            }
454            FlatEvent::HookTriggered {
455                event,
456                target,
457                hook_count,
458            } => Event::HookTriggered {
459                event,
460                target,
461                hook_count,
462            },
463            FlatEvent::HookResolved {
464                event,
465                target,
466                action,
467                reason,
468                duration_ms,
469            } => Event::HookResolved {
470                event,
471                target,
472                action,
473                reason,
474                duration_ms,
475            },
476        }
477    }
478}
479
480// ---------------------------------------------------------------------------
481// EventEnvelope – {type, payload} wire format
482// ---------------------------------------------------------------------------
483
484#[derive(Serialize, Deserialize)]
485struct EventEnvelope {
486    #[serde(rename = "type")]
487    type_name: String,
488    payload: serde_json::Value,
489}
490
491impl Serialize for Event {
492    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
493        match self {
494            // ContentPart carries its own "type" field (e.g. "text", "image_url").
495            // We must not strip it, otherwise deserialization fails.
496            Event::ContentPart(part) => {
497                let payload = serde_json::to_value(part).map_err(serde::ser::Error::custom)?;
498                EventEnvelope {
499                    type_name: "ContentPart".to_string(),
500                    payload,
501                }
502                .serialize(serializer)
503            }
504            // ToolCall payload carries an inner `type: "function"` discriminator
505            // that must be preserved in the payload, separate from the envelope type.
506            Event::ToolCall {
507                id,
508                function,
509                extras,
510            } => {
511                #[derive(Serialize)]
512                struct ToolCallPayload<'a> {
513                    #[serde(rename = "type")]
514                    type_name: &'a str,
515                    id: &'a str,
516                    function: &'a ToolCallFunction,
517                    #[serde(skip_serializing_if = "Option::is_none")]
518                    extras: &'a Option<serde_json::Value>,
519                }
520                let payload = serde_json::to_value(&ToolCallPayload {
521                    type_name: "function",
522                    id,
523                    function,
524                    extras,
525                })
526                .map_err(serde::ser::Error::custom)?;
527                EventEnvelope {
528                    type_name: "ToolCall".to_string(),
529                    payload,
530                }
531                .serialize(serializer)
532            }
533            _ => {
534                let flat = FlatEvent::from(self.clone());
535                let mut value = serde_json::to_value(&flat).map_err(serde::ser::Error::custom)?;
536                let obj = value
537                    .as_object_mut()
538                    .ok_or_else(|| serde::ser::Error::custom("expected object"))?;
539                let type_name = obj
540                    .remove("type")
541                    .and_then(|v| v.as_str().map(String::from))
542                    .ok_or_else(|| serde::ser::Error::custom("missing type"))?;
543                EventEnvelope {
544                    type_name,
545                    payload: value,
546                }
547                .serialize(serializer)
548            }
549        }
550    }
551}
552
553impl<'de> Deserialize<'de> for Event {
554    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
555        let envelope = EventEnvelope::deserialize(deserializer)?;
556        match envelope.type_name.as_str() {
557            "ContentPart" => {
558                let part: ContentPart =
559                    serde_json::from_value(envelope.payload).map_err(serde::de::Error::custom)?;
560                Ok(Event::ContentPart(part))
561            }
562            _ => {
563                let mut value = envelope.payload;
564                if let Some(obj) = value.as_object_mut() {
565                    obj.insert(
566                        "type".to_string(),
567                        serde_json::Value::String(envelope.type_name),
568                    );
569                }
570                let flat: FlatEvent =
571                    serde_json::from_value(value).map_err(serde::de::Error::custom)?;
572                Ok(Event::from(flat))
573            }
574        }
575    }
576}
577
578/// Payload of a [`Event::SubagentEvent`].
579///
580/// This is a generic `{type, payload}` envelope rather than a strongly-typed
581/// [`Event`] because subagent events may be any wire message type.
582#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
583pub struct SubagentEventPayload {
584    /// The wire type name of the subagent event.
585    #[serde(rename = "type")]
586    pub type_name: String,
587    /// The raw payload of the subagent event.
588    pub payload: serde_json::Value,
589}
590
591/// Status update from the server.
592#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
593pub struct StatusUpdate {
594    /// Fraction of context window used (0.0–1.0).
595    #[serde(skip_serializing_if = "Option::is_none")]
596    pub context_usage: Option<f64>,
597    /// Number of context tokens used.
598    #[serde(skip_serializing_if = "Option::is_none")]
599    pub context_tokens: Option<u64>,
600    /// Maximum context tokens allowed.
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub max_context_tokens: Option<u64>,
603    /// Detailed token usage breakdown.
604    #[serde(skip_serializing_if = "Option::is_none")]
605    pub token_usage: Option<TokenUsage>,
606    /// Server-assigned message id.
607    #[serde(skip_serializing_if = "Option::is_none")]
608    pub message_id: Option<String>,
609    /// Whether plan mode is active. `null` means no change.
610    #[serde(skip_serializing_if = "Option::is_none")]
611    pub plan_mode: Option<bool>,
612}
613
614/// Token usage breakdown.
615#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
616pub struct TokenUsage {
617    /// Input tokens excluding `input_cache_read` and `input_cache_creation`.
618    pub input_other: u64,
619    /// Total output tokens.
620    pub output: u64,
621    /// Cached input tokens.
622    pub input_cache_read: u64,
623    /// Input tokens used for cache creation (currently only Anthropic API).
624    pub input_cache_creation: u64,
625}
626
627/// Function name and arguments for a tool call.
628#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
629pub struct ToolCallFunction {
630    /// Function name.
631    pub name: String,
632    /// JSON-encoded arguments.
633    #[serde(skip_serializing_if = "Option::is_none")]
634    pub arguments: Option<String>,
635}
636
637/// Client's response to an approval request.
638#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
639#[serde(rename_all = "snake_case")]
640#[non_exhaustive]
641pub enum ApprovalResponseKind {
642    /// Approve this request once.
643    Approve,
644    /// Approve this request and remember for the session.
645    #[serde(rename = "approve_for_session")]
646    ApproveForSession,
647    /// Reject this request.
648    Reject,
649}
650
651/// Action taken by a hook.
652#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
653#[serde(rename_all = "snake_case")]
654#[non_exhaustive]
655pub enum HookAction {
656    /// Allow the operation to proceed.
657    Allow,
658    /// Block the operation.
659    Block,
660}