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 { user_input: UserInput },
162    TurnEnd,
163    StepBegin { n: u32 },
164    StepInterrupted,
165    StepRetry {
166        n: u32,
167        next_attempt: u32,
168        max_attempts: u32,
169        wait_s: u32,
170        error_type: String,
171        #[serde(skip_serializing_if = "Option::is_none")]
172        status_code: Option<u32>,
173    },
174    CompactionBegin,
175    CompactionEnd,
176    StatusUpdate(StatusUpdate),
177    ContentPart(ContentPart),
178    ToolCall {
179        id: String,
180        function: ToolCallFunction,
181        #[serde(skip_serializing_if = "Option::is_none")]
182        extras: Option<serde_json::Value>,
183    },
184    ToolCallPart {
185        #[serde(skip_serializing_if = "Option::is_none")]
186        arguments_part: Option<String>,
187    },
188    ToolResult {
189        tool_call_id: String,
190        return_value: ToolReturnValue,
191    },
192    ApprovalResponse {
193        request_id: String,
194        response: ApprovalResponseKind,
195        #[serde(skip_serializing_if = "Option::is_none")]
196        feedback: Option<String>,
197    },
198    SubagentEvent {
199        #[serde(skip_serializing_if = "Option::is_none")]
200        parent_tool_call_id: Option<String>,
201        #[serde(skip_serializing_if = "Option::is_none")]
202        agent_id: Option<String>,
203        #[serde(skip_serializing_if = "Option::is_none")]
204        subagent_type: Option<String>,
205        event: SubagentEventPayload,
206    },
207    SteerInput { user_input: UserInput },
208    BtwBegin { id: String, question: String },
209    BtwEnd {
210        id: String,
211        #[serde(skip_serializing_if = "Option::is_none")]
212        response: Option<String>,
213        #[serde(skip_serializing_if = "Option::is_none")]
214        error: Option<String>,
215    },
216    PlanDisplay { content: String, file_path: String },
217    HookTriggered { event: String, target: String, hook_count: u32 },
218    HookResolved { event: String, target: String, action: HookAction, reason: String, duration_ms: u64 },
219}
220
221impl From<Event> for FlatEvent {
222    fn from(ev: Event) -> Self {
223        match ev {
224            Event::TurnBegin { user_input } => FlatEvent::TurnBegin { user_input },
225            Event::TurnEnd => FlatEvent::TurnEnd,
226            Event::StepBegin { n } => FlatEvent::StepBegin { n },
227            Event::StepInterrupted => FlatEvent::StepInterrupted,
228            Event::StepRetry { n, next_attempt, max_attempts, wait_s, error_type, status_code } => FlatEvent::StepRetry { n, next_attempt, max_attempts, wait_s, error_type, status_code },
229            Event::CompactionBegin => FlatEvent::CompactionBegin,
230            Event::CompactionEnd => FlatEvent::CompactionEnd,
231            Event::StatusUpdate(s) => FlatEvent::StatusUpdate(s),
232            Event::ContentPart(c) => FlatEvent::ContentPart(c),
233            Event::ToolCall { id, function, extras } => FlatEvent::ToolCall { id, function, extras },
234            Event::ToolCallPart { arguments_part } => FlatEvent::ToolCallPart { arguments_part },
235            Event::ToolResult { tool_call_id, return_value } => FlatEvent::ToolResult { tool_call_id, return_value },
236            Event::ApprovalResponse { request_id, response, feedback } => FlatEvent::ApprovalResponse { request_id, response, feedback },
237            Event::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event } => FlatEvent::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event },
238            Event::SteerInput { user_input } => FlatEvent::SteerInput { user_input },
239            Event::BtwBegin { id, question } => FlatEvent::BtwBegin { id, question },
240            Event::BtwEnd { id, response, error } => FlatEvent::BtwEnd { id, response, error },
241            Event::PlanDisplay { content, file_path } => FlatEvent::PlanDisplay { content, file_path },
242            Event::HookTriggered { event, target, hook_count } => FlatEvent::HookTriggered { event, target, hook_count },
243            Event::HookResolved { event, target, action, reason, duration_ms } => FlatEvent::HookResolved { event, target, action, reason, duration_ms },
244        }
245    }
246}
247
248impl Event {
249    /// Return the wire type name for this event.
250    ///
251    /// Matches the `type` field in the wire envelope.
252    pub fn type_name(&self) -> &'static str {
253        match self {
254            Event::TurnBegin { .. } => "TurnBegin",
255            Event::TurnEnd => "TurnEnd",
256            Event::StepBegin { .. } => "StepBegin",
257            Event::StepInterrupted => "StepInterrupted",
258            Event::StepRetry { .. } => "StepRetry",
259            Event::CompactionBegin => "CompactionBegin",
260            Event::CompactionEnd => "CompactionEnd",
261            Event::StatusUpdate(_) => "StatusUpdate",
262            Event::ContentPart(_) => "ContentPart",
263            Event::ToolCall { .. } => "ToolCall",
264            Event::ToolCallPart { .. } => "ToolCallPart",
265            Event::ToolResult { .. } => "ToolResult",
266            Event::ApprovalResponse { .. } => "ApprovalResponse",
267            Event::SubagentEvent { .. } => "SubagentEvent",
268            Event::SteerInput { .. } => "SteerInput",
269            Event::BtwBegin { .. } => "BtwBegin",
270            Event::BtwEnd { .. } => "BtwEnd",
271            Event::PlanDisplay { .. } => "PlanDisplay",
272            Event::HookTriggered { .. } => "HookTriggered",
273            Event::HookResolved { .. } => "HookResolved",
274        }
275    }
276}
277
278impl From<FlatEvent> for Event {
279    fn from(ev: FlatEvent) -> Self {
280        match ev {
281            FlatEvent::TurnBegin { user_input } => Event::TurnBegin { user_input },
282            FlatEvent::TurnEnd => Event::TurnEnd,
283            FlatEvent::StepBegin { n } => Event::StepBegin { n },
284            FlatEvent::StepInterrupted => Event::StepInterrupted,
285            FlatEvent::StepRetry { n, next_attempt, max_attempts, wait_s, error_type, status_code } => Event::StepRetry { n, next_attempt, max_attempts, wait_s, error_type, status_code },
286            FlatEvent::CompactionBegin => Event::CompactionBegin,
287            FlatEvent::CompactionEnd => Event::CompactionEnd,
288            FlatEvent::StatusUpdate(s) => Event::StatusUpdate(s),
289            FlatEvent::ContentPart(c) => Event::ContentPart(c),
290            FlatEvent::ToolCall { id, function, extras } => Event::ToolCall { id, function, extras },
291            FlatEvent::ToolCallPart { arguments_part } => Event::ToolCallPart { arguments_part },
292            FlatEvent::ToolResult { tool_call_id, return_value } => Event::ToolResult { tool_call_id, return_value },
293            FlatEvent::ApprovalResponse { request_id, response, feedback } => Event::ApprovalResponse { request_id, response, feedback },
294            FlatEvent::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event } => Event::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event },
295            FlatEvent::SteerInput { user_input } => Event::SteerInput { user_input },
296            FlatEvent::BtwBegin { id, question } => Event::BtwBegin { id, question },
297            FlatEvent::BtwEnd { id, response, error } => Event::BtwEnd { id, response, error },
298            FlatEvent::PlanDisplay { content, file_path } => Event::PlanDisplay { content, file_path },
299            FlatEvent::HookTriggered { event, target, hook_count } => Event::HookTriggered { event, target, hook_count },
300            FlatEvent::HookResolved { event, target, action, reason, duration_ms } => Event::HookResolved { event, target, action, reason, duration_ms },
301        }
302    }
303}
304
305// ---------------------------------------------------------------------------
306// EventEnvelope – {type, payload} wire format
307// ---------------------------------------------------------------------------
308
309#[derive(Serialize, Deserialize)]
310struct EventEnvelope {
311    #[serde(rename = "type")]
312    type_name: String,
313    payload: serde_json::Value,
314}
315
316impl Serialize for Event {
317    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
318        match self {
319            // ContentPart carries its own "type" field (e.g. "text", "image_url").
320            // We must not strip it, otherwise deserialization fails.
321            Event::ContentPart(part) => {
322                let payload = serde_json::to_value(part).map_err(serde::ser::Error::custom)?;
323                EventEnvelope {
324                    type_name: "ContentPart".to_string(),
325                    payload,
326                }
327                .serialize(serializer)
328            }
329            // ToolCall payload carries an inner `type: "function"` discriminator
330            // that must be preserved in the payload, separate from the envelope type.
331            Event::ToolCall { id, function, extras } => {
332                #[derive(Serialize)]
333                struct ToolCallPayload<'a> {
334                    #[serde(rename = "type")]
335                    type_name: &'a str,
336                    id: &'a str,
337                    function: &'a ToolCallFunction,
338                    #[serde(skip_serializing_if = "Option::is_none")]
339                    extras: &'a Option<serde_json::Value>,
340                }
341                let payload = serde_json::to_value(&ToolCallPayload {
342                    type_name: "function",
343                    id,
344                    function,
345                    extras,
346                })
347                .map_err(serde::ser::Error::custom)?;
348                EventEnvelope {
349                    type_name: "ToolCall".to_string(),
350                    payload,
351                }
352                .serialize(serializer)
353            }
354            _ => {
355                let flat = FlatEvent::from(self.clone());
356                let mut value = serde_json::to_value(&flat).map_err(serde::ser::Error::custom)?;
357                let obj = value
358                    .as_object_mut()
359                    .ok_or_else(|| serde::ser::Error::custom("expected object"))?;
360                let type_name = obj
361                    .remove("type")
362                    .and_then(|v| v.as_str().map(String::from))
363                    .ok_or_else(|| serde::ser::Error::custom("missing type"))?;
364                EventEnvelope { type_name, payload: value }.serialize(serializer)
365            }
366        }
367    }
368}
369
370impl<'de> Deserialize<'de> for Event {
371    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
372        let envelope = EventEnvelope::deserialize(deserializer)?;
373        match envelope.type_name.as_str() {
374            "ContentPart" => {
375                let part: ContentPart =
376                    serde_json::from_value(envelope.payload).map_err(serde::de::Error::custom)?;
377                Ok(Event::ContentPart(part))
378            }
379            _ => {
380                let mut value = envelope.payload;
381                if let Some(obj) = value.as_object_mut() {
382                    obj.insert(
383                        "type".to_string(),
384                        serde_json::Value::String(envelope.type_name),
385                    );
386                }
387                let flat: FlatEvent =
388                    serde_json::from_value(value).map_err(serde::de::Error::custom)?;
389                Ok(Event::from(flat))
390            }
391        }
392    }
393}
394
395/// Payload of a [`Event::SubagentEvent`].
396///
397/// This is a generic `{type, payload}` envelope rather than a strongly-typed
398/// [`Event`] because subagent events may be any wire message type.
399#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
400pub struct SubagentEventPayload {
401    /// The wire type name of the subagent event.
402    #[serde(rename = "type")]
403    pub type_name: String,
404    /// The raw payload of the subagent event.
405    pub payload: serde_json::Value,
406}
407
408/// Status update from the server.
409#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
410pub struct StatusUpdate {
411    /// Fraction of context window used (0.0–1.0).
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub context_usage: Option<f64>,
414    /// Number of context tokens used.
415    #[serde(skip_serializing_if = "Option::is_none")]
416    pub context_tokens: Option<u64>,
417    /// Maximum context tokens allowed.
418    #[serde(skip_serializing_if = "Option::is_none")]
419    pub max_context_tokens: Option<u64>,
420    /// Detailed token usage breakdown.
421    #[serde(skip_serializing_if = "Option::is_none")]
422    pub token_usage: Option<TokenUsage>,
423    /// Server-assigned message id.
424    #[serde(skip_serializing_if = "Option::is_none")]
425    pub message_id: Option<String>,
426    /// Whether plan mode is active. `null` means no change.
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub plan_mode: Option<bool>,
429}
430
431/// Token usage breakdown.
432#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
433pub struct TokenUsage {
434    /// Input tokens excluding `input_cache_read` and `input_cache_creation`.
435    pub input_other: u64,
436    /// Total output tokens.
437    pub output: u64,
438    /// Cached input tokens.
439    pub input_cache_read: u64,
440    /// Input tokens used for cache creation (currently only Anthropic API).
441    pub input_cache_creation: u64,
442}
443
444/// Function name and arguments for a tool call.
445#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
446pub struct ToolCallFunction {
447    /// Function name.
448    pub name: String,
449    /// JSON-encoded arguments.
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub arguments: Option<String>,
452}
453
454/// Client's response to an approval request.
455#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
456#[serde(rename_all = "snake_case")]
457#[non_exhaustive]
458pub enum ApprovalResponseKind {
459    /// Approve this request once.
460    Approve,
461    /// Approve this request and remember for the session.
462    #[serde(rename = "approve_for_session")]
463    ApproveForSession,
464    /// Reject this request.
465    Reject,
466}
467
468/// Action taken by a hook.
469#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
470#[serde(rename_all = "snake_case")]
471#[non_exhaustive]
472pub enum HookAction {
473    /// Allow the operation to proceed.
474    Allow,
475    /// Block the operation.
476    Block,
477}