Skip to main content

kimi_wire/protocol/
content.rs

1use serde::{Deserialize, Serialize};
2
3// ============================================================================
4// UserInput
5// ============================================================================
6
7/// User input can be plain text or an array of content parts.
8#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
9#[serde(untagged)]
10pub enum UserInput {
11    /// Plain text input.
12    Text(String),
13    /// Structured content parts (text, images, audio, video).
14    Parts(Vec<ContentPart>),
15}
16
17impl From<String> for UserInput {
18    fn from(value: String) -> Self {
19        Self::Text(value)
20    }
21}
22
23impl From<&str> for UserInput {
24    fn from(value: &str) -> Self {
25        Self::Text(value.to_string())
26    }
27}
28
29impl From<Vec<ContentPart>> for UserInput {
30    fn from(value: Vec<ContentPart>) -> Self {
31        Self::Parts(value)
32    }
33}
34
35impl From<String> for ContentPart {
36    fn from(value: String) -> Self {
37        Self::Text(TextPart { text: value })
38    }
39}
40
41impl From<&str> for ContentPart {
42    fn from(value: &str) -> Self {
43        Self::Text(TextPart {
44            text: value.to_string(),
45        })
46    }
47}
48
49// ============================================================================
50// ContentPart
51// ============================================================================
52
53/// A content part in a message.
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
55#[serde(tag = "type", rename_all = "snake_case")]
56#[non_exhaustive]
57pub enum ContentPart {
58    /// Plain text content.
59    Text(TextPart),
60    /// Thinking / reasoning content (may be encrypted).
61    Think(ThinkPart),
62    /// Image referenced by URL or data URI.
63    #[serde(rename = "image_url")]
64    ImageUrl(ImageUrlPart),
65    /// Audio referenced by URL or data URI.
66    #[serde(rename = "audio_url")]
67    AudioUrl(AudioUrlPart),
68    /// Video referenced by URL or data URI.
69    #[serde(rename = "video_url")]
70    VideoUrl(VideoUrlPart),
71}
72
73/// Text content part.
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
75pub struct TextPart {
76    /// The text content.
77    pub text: String,
78}
79
80/// Thinking content part.
81#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
82pub struct ThinkPart {
83    /// The thinking / reasoning text.
84    pub think: String,
85    /// Encrypted thinking content or signature.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub encrypted: Option<String>,
88}
89
90/// Image URL content part wrapper.
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
92pub struct ImageUrlPart {
93    /// Image media URL.
94    pub image_url: MediaUrl,
95}
96
97/// Audio URL content part wrapper.
98#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
99pub struct AudioUrlPart {
100    /// Audio media URL.
101    pub audio_url: MediaUrl,
102}
103
104/// Video URL content part wrapper.
105#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
106pub struct VideoUrlPart {
107    /// Video media URL.
108    pub video_url: MediaUrl,
109}
110
111/// A media URL with an optional ID.
112#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
113pub struct MediaUrl {
114    /// URL or data URI (e.g. `data:image/png;base64,...`).
115    pub url: String,
116    /// Optional ID for distinguishing different media items.
117    #[serde(skip_serializing_if = "Option::is_none")]
118    pub id: Option<String>,
119}
120
121// ============================================================================
122// DisplayBlock
123// ============================================================================
124
125/// A display block shown to the user in tool results or approval requests.
126///
127/// This struct-based design matches the official Go SDK and avoids tag
128/// conflicts when handling unknown block types.
129#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
130pub struct DisplayBlock {
131    /// Block type discriminator.
132    #[serde(rename = "type")]
133    pub block_type: DisplayBlockType,
134    /// Text content (used by `Brief` and `Shell`).
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub text: Option<String>,
137    /// File path (used by `Diff`).
138    #[serde(skip_serializing_if = "Option::is_none")]
139    pub path: Option<String>,
140    /// Old text for a diff.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    pub old_text: Option<String>,
143    /// New text for a diff.
144    #[serde(skip_serializing_if = "Option::is_none")]
145    pub new_text: Option<String>,
146    /// Whether this diff block is a summary (shows line count instead of actual diff).
147    /// Added in Wire protocol v1.8.
148    #[serde(skip_serializing_if = "Option::is_none", default)]
149    pub is_summary: Option<bool>,
150    /// Todo list items.
151    #[serde(skip_serializing_if = "Option::is_none")]
152    pub items: Option<Vec<TodoDisplayItem>>,
153    /// Language identifier for syntax highlighting (e.g. "sh", "powershell").
154    #[serde(skip_serializing_if = "Option::is_none")]
155    pub language: Option<String>,
156    /// Shell command string.
157    #[serde(skip_serializing_if = "Option::is_none")]
158    pub command: Option<String>,
159    /// Raw data for unrecognized block types.
160    #[serde(skip_serializing_if = "Option::is_none")]
161    pub data: Option<serde_json::Value>,
162}
163
164/// Discriminator for [`DisplayBlock`] types.
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
166#[serde(rename_all = "snake_case")]
167#[non_exhaustive]
168pub enum DisplayBlockType {
169    /// Brief textual summary.
170    Brief,
171    /// Diff between old and new text.
172    Diff,
173    /// Todo list.
174    Todo,
175    /// Shell command or output.
176    Shell,
177    /// Unknown block type.
178    #[serde(rename = "unknown")]
179    Unknown,
180}
181
182/// A single item in a todo display block.
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
184pub struct TodoDisplayItem {
185    /// Item title.
186    pub title: String,
187    /// Completion status.
188    pub status: TodoStatus,
189}
190
191/// Status of a todo display item.
192#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
193#[serde(rename_all = "snake_case")]
194#[non_exhaustive]
195pub enum TodoStatus {
196    /// Not started.
197    Pending,
198    /// In progress.
199    InProgress,
200    /// Completed.
201    Done,
202}
203
204// ============================================================================
205// DisplayBlock builders
206// ============================================================================
207
208impl DisplayBlock {
209    /// Create a brief text display block.
210    pub fn brief(text: impl Into<String>) -> Self {
211        Self {
212            block_type: DisplayBlockType::Brief,
213            text: Some(text.into()),
214            path: None,
215            old_text: None,
216            new_text: None,
217            is_summary: None,
218            items: None,
219            language: None,
220            command: None,
221            data: None,
222        }
223    }
224
225    /// Create a diff display block.
226    pub fn diff(
227        path: impl Into<String>,
228        old_text: impl Into<String>,
229        new_text: impl Into<String>,
230    ) -> Self {
231        Self {
232            block_type: DisplayBlockType::Diff,
233            text: None,
234            path: Some(path.into()),
235            old_text: Some(old_text.into()),
236            new_text: Some(new_text.into()),
237            is_summary: None,
238            items: None,
239            language: None,
240            command: None,
241            data: None,
242        }
243    }
244
245    /// Create a todo list display block.
246    #[must_use]
247    pub const fn todo(items: Vec<TodoDisplayItem>) -> Self {
248        Self {
249            block_type: DisplayBlockType::Todo,
250            text: None,
251            path: None,
252            old_text: None,
253            new_text: None,
254            is_summary: None,
255            items: Some(items),
256            language: None,
257            command: None,
258            data: None,
259        }
260    }
261
262    /// Create a shell command display block.
263    pub fn shell(command: impl Into<String>, language: impl Into<String>) -> Self {
264        Self {
265            block_type: DisplayBlockType::Shell,
266            text: None,
267            path: None,
268            old_text: None,
269            new_text: None,
270            is_summary: None,
271            items: None,
272            language: Some(language.into()),
273            command: Some(command.into()),
274            data: None,
275        }
276    }
277}
278
279// ============================================================================
280// ToolReturnValue
281// ============================================================================
282
283/// The result of a tool execution.
284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
285pub struct ToolReturnValue {
286    /// Whether the tool execution failed.
287    pub is_error: bool,
288    /// Output returned to the model. Can be plain text or content parts.
289    pub output: ToolOutput,
290    /// Explanatory message for the model.
291    pub message: String,
292    /// Display blocks shown to the user.
293    pub display: Vec<DisplayBlock>,
294    /// Extra debug info.
295    #[serde(skip_serializing_if = "Option::is_none")]
296    pub extras: Option<serde_json::Value>,
297}
298
299impl ToolReturnValue {
300    /// Create a successful tool return value.
301    pub fn new(message: impl Into<String>) -> Self {
302        Self {
303            is_error: false,
304            output: ToolOutput::Text(String::new()),
305            message: message.into(),
306            display: vec![],
307            extras: None,
308        }
309    }
310
311    /// Mark this return value as an error.
312    #[must_use]
313    pub const fn with_error(mut self) -> Self {
314        self.is_error = true;
315        self
316    }
317
318    /// Set the tool output.
319    #[must_use]
320    pub fn with_output(mut self, output: impl Into<ToolOutput>) -> Self {
321        self.output = output.into();
322        self
323    }
324
325    /// Add a display block.
326    #[must_use]
327    pub fn with_display(mut self, block: DisplayBlock) -> Self {
328        self.display.push(block);
329        self
330    }
331}
332
333/// Tool output can be plain text or an array of content parts.
334#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
335#[serde(untagged)]
336pub enum ToolOutput {
337    /// Plain text output.
338    Text(String),
339    /// Structured content parts.
340    Parts(Vec<ContentPart>),
341}
342
343impl From<String> for ToolOutput {
344    fn from(value: String) -> Self {
345        Self::Text(value)
346    }
347}
348
349impl From<&str> for ToolOutput {
350    fn from(value: &str) -> Self {
351        Self::Text(value.to_string())
352    }
353}
354
355impl From<Vec<ContentPart>> for ToolOutput {
356    fn from(value: Vec<ContentPart>) -> Self {
357        Self::Parts(value)
358    }
359}