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)]
13pub enum Event {
14    /// A new turn has started with the given user input.
15    TurnBegin {
16        /// The user's input that triggered this turn.
17        user_input: UserInput,
18    },
19    /// The current turn has ended.
20    TurnEnd,
21    /// A new step within the turn has started.
22    StepBegin {
23        /// Step number, starting from 1.
24        n: u32,
25    },
26    /// The current step was interrupted (e.g. by user input).
27    StepInterrupted,
28    /// Context compaction has started.
29    CompactionBegin,
30    /// Context compaction has finished.
31    CompactionEnd,
32    /// Server status update (token usage, context size, etc.).
33    StatusUpdate(StatusUpdate),
34    /// A content part (text, image, etc.) from the model.
35    ContentPart(ContentPart),
36    /// A tool call from the model.
37    ///
38    /// Wire name is `"function"` because Kimi serializes tool calls as `function` type.
39    ToolCall {
40        /// Tool call id.
41        id: String,
42        /// Function name and arguments.
43        function: ToolCallFunction,
44        /// Extra fields from the wire protocol.
45        extras: Option<serde_json::Value>,
46    },
47    /// A partial tool call (streaming arguments).
48    ToolCallPart {
49        /// Partial JSON arguments.
50        arguments_part: Option<String>,
51    },
52    /// Result of a tool execution.
53    ToolResult {
54        /// Id of the corresponding tool call.
55        tool_call_id: String,
56        /// Return value from the tool.
57        return_value: ToolReturnValue,
58    },
59    /// Response to an approval request (sent by the client).
60    ApprovalResponse {
61        /// Id of the approval request.
62        request_id: String,
63        /// Approval decision.
64        response: ApprovalResponseKind,
65        /// Optional feedback text from the user.
66        feedback: Option<String>,
67    },
68    /// An event from a subagent.
69    SubagentEvent {
70        /// Id of the parent tool call that spawned the subagent.
71        parent_tool_call_id: Option<String>,
72        /// Subagent id.
73        agent_id: Option<String>,
74        /// Subagent type.
75        subagent_type: Option<String>,
76        /// Nested wire message in envelope form.
77        event: SubagentEventPayload,
78    },
79    /// Additional user input steering the current turn.
80    SteerInput {
81        /// The steering input.
82        user_input: UserInput,
83    },
84    /// Plan display content.
85    PlanDisplay {
86        /// Display content.
87        content: String,
88        /// File path associated with the plan.
89        file_path: String,
90    },
91    /// A hook was triggered.
92    HookTriggered {
93        /// Event name.
94        event: String,
95        /// Target of the hook.
96        target: String,
97        /// Number of times this hook has fired.
98        hook_count: u32,
99    },
100    /// A hook was resolved.
101    HookResolved {
102        /// Event name.
103        event: String,
104        /// Target of the hook.
105        target: String,
106        /// Action taken.
107        action: HookAction,
108        /// Reason for the action.
109        reason: String,
110        /// Duration in milliseconds.
111        duration_ms: u64,
112    },
113}
114
115// ---------------------------------------------------------------------------
116// FlatEvent – internal mirror used for (de)serialization
117// ---------------------------------------------------------------------------
118
119#[derive(Serialize, Deserialize)]
120#[serde(tag = "type")]
121pub(crate) enum FlatEvent {
122    TurnBegin { user_input: UserInput },
123    TurnEnd,
124    StepBegin { n: u32 },
125    StepInterrupted,
126    CompactionBegin,
127    CompactionEnd,
128    StatusUpdate(StatusUpdate),
129    ContentPart(ContentPart),
130    #[serde(rename = "function")]
131    ToolCall {
132        id: String,
133        function: ToolCallFunction,
134        #[serde(skip_serializing_if = "Option::is_none")]
135        extras: Option<serde_json::Value>,
136    },
137    ToolCallPart {
138        #[serde(skip_serializing_if = "Option::is_none")]
139        arguments_part: Option<String>,
140    },
141    ToolResult {
142        tool_call_id: String,
143        return_value: ToolReturnValue,
144    },
145    ApprovalResponse {
146        request_id: String,
147        response: ApprovalResponseKind,
148        #[serde(skip_serializing_if = "Option::is_none")]
149        feedback: Option<String>,
150    },
151    SubagentEvent {
152        #[serde(skip_serializing_if = "Option::is_none")]
153        parent_tool_call_id: Option<String>,
154        #[serde(skip_serializing_if = "Option::is_none")]
155        agent_id: Option<String>,
156        #[serde(skip_serializing_if = "Option::is_none")]
157        subagent_type: Option<String>,
158        event: SubagentEventPayload,
159    },
160    SteerInput { user_input: UserInput },
161    PlanDisplay { content: String, file_path: String },
162    HookTriggered { event: String, target: String, hook_count: u32 },
163    HookResolved { event: String, target: String, action: HookAction, reason: String, duration_ms: u64 },
164}
165
166impl From<Event> for FlatEvent {
167    fn from(ev: Event) -> Self {
168        match ev {
169            Event::TurnBegin { user_input } => FlatEvent::TurnBegin { user_input },
170            Event::TurnEnd => FlatEvent::TurnEnd,
171            Event::StepBegin { n } => FlatEvent::StepBegin { n },
172            Event::StepInterrupted => FlatEvent::StepInterrupted,
173            Event::CompactionBegin => FlatEvent::CompactionBegin,
174            Event::CompactionEnd => FlatEvent::CompactionEnd,
175            Event::StatusUpdate(s) => FlatEvent::StatusUpdate(s),
176            Event::ContentPart(c) => FlatEvent::ContentPart(c),
177            Event::ToolCall { id, function, extras } => FlatEvent::ToolCall { id, function, extras },
178            Event::ToolCallPart { arguments_part } => FlatEvent::ToolCallPart { arguments_part },
179            Event::ToolResult { tool_call_id, return_value } => FlatEvent::ToolResult { tool_call_id, return_value },
180            Event::ApprovalResponse { request_id, response, feedback } => FlatEvent::ApprovalResponse { request_id, response, feedback },
181            Event::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event } => FlatEvent::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event },
182            Event::SteerInput { user_input } => FlatEvent::SteerInput { user_input },
183            Event::PlanDisplay { content, file_path } => FlatEvent::PlanDisplay { content, file_path },
184            Event::HookTriggered { event, target, hook_count } => FlatEvent::HookTriggered { event, target, hook_count },
185            Event::HookResolved { event, target, action, reason, duration_ms } => FlatEvent::HookResolved { event, target, action, reason, duration_ms },
186        }
187    }
188}
189
190impl From<FlatEvent> for Event {
191    fn from(ev: FlatEvent) -> Self {
192        match ev {
193            FlatEvent::TurnBegin { user_input } => Event::TurnBegin { user_input },
194            FlatEvent::TurnEnd => Event::TurnEnd,
195            FlatEvent::StepBegin { n } => Event::StepBegin { n },
196            FlatEvent::StepInterrupted => Event::StepInterrupted,
197            FlatEvent::CompactionBegin => Event::CompactionBegin,
198            FlatEvent::CompactionEnd => Event::CompactionEnd,
199            FlatEvent::StatusUpdate(s) => Event::StatusUpdate(s),
200            FlatEvent::ContentPart(c) => Event::ContentPart(c),
201            FlatEvent::ToolCall { id, function, extras } => Event::ToolCall { id, function, extras },
202            FlatEvent::ToolCallPart { arguments_part } => Event::ToolCallPart { arguments_part },
203            FlatEvent::ToolResult { tool_call_id, return_value } => Event::ToolResult { tool_call_id, return_value },
204            FlatEvent::ApprovalResponse { request_id, response, feedback } => Event::ApprovalResponse { request_id, response, feedback },
205            FlatEvent::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event } => Event::SubagentEvent { parent_tool_call_id, agent_id, subagent_type, event },
206            FlatEvent::SteerInput { user_input } => Event::SteerInput { user_input },
207            FlatEvent::PlanDisplay { content, file_path } => Event::PlanDisplay { content, file_path },
208            FlatEvent::HookTriggered { event, target, hook_count } => Event::HookTriggered { event, target, hook_count },
209            FlatEvent::HookResolved { event, target, action, reason, duration_ms } => Event::HookResolved { event, target, action, reason, duration_ms },
210        }
211    }
212}
213
214// ---------------------------------------------------------------------------
215// EventEnvelope – {type, payload} wire format
216// ---------------------------------------------------------------------------
217
218#[derive(Serialize, Deserialize)]
219struct EventEnvelope {
220    #[serde(rename = "type")]
221    type_name: String,
222    payload: serde_json::Value,
223}
224
225impl Serialize for Event {
226    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
227        let flat = FlatEvent::from(self.clone());
228        let mut value = serde_json::to_value(&flat).map_err(serde::ser::Error::custom)?;
229        let obj = value
230            .as_object_mut()
231            .ok_or_else(|| serde::ser::Error::custom("expected object"))?;
232        let type_name = obj
233            .remove("type")
234            .and_then(|v| v.as_str().map(String::from))
235            .ok_or_else(|| serde::ser::Error::custom("missing type"))?;
236        EventEnvelope { type_name, payload: value }.serialize(serializer)
237    }
238}
239
240impl<'de> Deserialize<'de> for Event {
241    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
242        let envelope = EventEnvelope::deserialize(deserializer)?;
243        let mut value = envelope.payload;
244        if let Some(obj) = value.as_object_mut() {
245            obj.insert("type".to_string(), serde_json::Value::String(envelope.type_name));
246        }
247        let flat: FlatEvent = serde_json::from_value(value).map_err(serde::de::Error::custom)?;
248        Ok(Event::from(flat))
249    }
250}
251
252/// Payload of a [`Event::SubagentEvent`].
253///
254/// This is a generic `{type, payload}` envelope rather than a strongly-typed
255/// [`Event`] because subagent events may be any wire message type.
256#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
257pub struct SubagentEventPayload {
258    /// The wire type name of the subagent event.
259    #[serde(rename = "type")]
260    pub type_name: String,
261    /// The raw payload of the subagent event.
262    pub payload: serde_json::Value,
263}
264
265/// Status update from the server.
266#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
267pub struct StatusUpdate {
268    /// Fraction of context window used (0.0–1.0).
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub context_usage: Option<f64>,
271    /// Number of context tokens used.
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub context_tokens: Option<u64>,
274    /// Maximum context tokens allowed.
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub max_context_tokens: Option<u64>,
277    /// Detailed token usage breakdown.
278    #[serde(skip_serializing_if = "Option::is_none")]
279    pub token_usage: Option<TokenUsage>,
280    /// Server-assigned message id.
281    #[serde(skip_serializing_if = "Option::is_none")]
282    pub message_id: Option<String>,
283    /// Whether plan mode is active. `null` means no change.
284    #[serde(skip_serializing_if = "Option::is_none")]
285    pub plan_mode: Option<bool>,
286}
287
288/// Token usage breakdown.
289#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
290pub struct TokenUsage {
291    /// Input tokens excluding `input_cache_read` and `input_cache_creation`.
292    pub input_other: u64,
293    /// Total output tokens.
294    pub output: u64,
295    /// Cached input tokens.
296    pub input_cache_read: u64,
297    /// Input tokens used for cache creation (currently only Anthropic API).
298    pub input_cache_creation: u64,
299}
300
301/// Function name and arguments for a tool call.
302#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
303pub struct ToolCallFunction {
304    /// Function name.
305    pub name: String,
306    /// JSON-encoded arguments.
307    #[serde(skip_serializing_if = "Option::is_none")]
308    pub arguments: Option<String>,
309}
310
311/// Client's response to an approval request.
312#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
313#[serde(rename_all = "snake_case")]
314pub enum ApprovalResponseKind {
315    /// Approve this request once.
316    Approve,
317    /// Approve this request and remember for the session.
318    #[serde(rename = "approve_for_session")]
319    ApproveForSession,
320    /// Reject this request.
321    Reject,
322}
323
324/// Action taken by a hook.
325#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
326#[serde(rename_all = "snake_case")]
327pub enum HookAction {
328    /// Allow the operation to proceed.
329    Allow,
330    /// Block the operation.
331    Block,
332}