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