Skip to main content

vtcode_core/open_responses/
items.rs

1//! Output item types for Open Responses.
2//!
3//! Items are the fundamental unit of context in Open Responses. They represent
4//! atomic units of model output, tool invocation, or reasoning state.
5
6use serde::de::{self, MapAccess, Visitor};
7use serde::ser::SerializeMap;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_json::Value;
10
11use super::{ContentPart, ItemStatus};
12
13/// Unique identifier for an output item.
14pub type OutputItemId = String;
15
16/// Output items generated by the model.
17///
18/// Per the Open Responses specification, items are polymorphic (discriminated by `type`),
19/// state machines (with status transitions), and streamable (through delta events).
20///
21/// Custom items serialize with their `custom_type` as the actual `type` field value
22/// (e.g., `"type": "vtcode:file_change"`), per the extension convention.
23#[derive(Debug, Clone, PartialEq)]
24pub enum OutputItem {
25    /// A message from the assistant, user, system, or developer.
26    Message(MessageItem),
27
28    /// Model reasoning/thinking content.
29    Reasoning(ReasoningItem),
30
31    /// A function/tool call request from the model.
32    FunctionCall(FunctionCallItem),
33
34    /// Output from a function/tool call execution.
35    FunctionCallOutput(FunctionCallOutputItem),
36
37    /// Custom/extension item type (prefixed with implementor slug).
38    Custom(CustomItem),
39}
40
41impl Serialize for OutputItem {
42    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
43    where
44        S: Serializer,
45    {
46        match self {
47            Self::Message(item) => {
48                let mut map = serializer.serialize_map(None)?;
49                map.serialize_entry("type", "message")?;
50                map.serialize_entry("id", &item.id)?;
51                map.serialize_entry("status", &item.status)?;
52                map.serialize_entry("role", &item.role)?;
53                map.serialize_entry("content", &item.content)?;
54                map.end()
55            }
56            Self::Reasoning(item) => {
57                let mut map = serializer.serialize_map(None)?;
58                map.serialize_entry("type", "reasoning")?;
59                map.serialize_entry("id", &item.id)?;
60                map.serialize_entry("status", &item.status)?;
61                if let Some(ref summary) = item.summary {
62                    map.serialize_entry("summary", summary)?;
63                }
64                if let Some(ref content) = item.content {
65                    map.serialize_entry("content", content)?;
66                }
67                if let Some(ref encrypted) = item.encrypted_content {
68                    map.serialize_entry("encrypted_content", encrypted)?;
69                }
70                map.end()
71            }
72            Self::FunctionCall(item) => {
73                let mut map = serializer.serialize_map(None)?;
74                map.serialize_entry("type", "function_call")?;
75                map.serialize_entry("id", &item.id)?;
76                map.serialize_entry("status", &item.status)?;
77                map.serialize_entry("name", &item.name)?;
78                map.serialize_entry("arguments", &item.arguments)?;
79                if let Some(ref call_id) = item.call_id {
80                    map.serialize_entry("call_id", call_id)?;
81                }
82                map.end()
83            }
84            Self::FunctionCallOutput(item) => {
85                let mut map = serializer.serialize_map(None)?;
86                map.serialize_entry("type", "function_call_output")?;
87                map.serialize_entry("id", &item.id)?;
88                map.serialize_entry("status", &item.status)?;
89                if let Some(ref call_id) = item.call_id {
90                    map.serialize_entry("call_id", call_id)?;
91                }
92                map.serialize_entry("output", &item.output)?;
93                map.end()
94            }
95            Self::Custom(item) => {
96                // Custom items use their custom_type as the type discriminator
97                let mut map = serializer.serialize_map(None)?;
98                map.serialize_entry("type", &item.custom_type)?;
99                map.serialize_entry("id", &item.id)?;
100                map.serialize_entry("status", &item.status)?;
101                map.serialize_entry("data", &item.data)?;
102                map.end()
103            }
104        }
105    }
106}
107
108impl<'de> Deserialize<'de> for OutputItem {
109    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
110    where
111        D: Deserializer<'de>,
112    {
113        struct OutputItemVisitor;
114
115        impl<'de> Visitor<'de> for OutputItemVisitor {
116            type Value = OutputItem;
117
118            fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119                formatter.write_str("an output item object with a type field")
120            }
121
122            fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
123            where
124                M: MapAccess<'de>,
125            {
126                let mut type_field: Option<String> = None;
127                let mut id: Option<String> = None;
128                let mut status: Option<ItemStatus> = None;
129                let mut role: Option<MessageRole> = None;
130                let mut content: Option<Vec<ContentPart>> = None;
131                let mut summary: Option<String> = None;
132                let mut reasoning_content: Option<String> = None;
133                let mut encrypted_content: Option<String> = None;
134                let mut name: Option<String> = None;
135                let mut arguments: Option<Value> = None;
136                let mut call_id: Option<String> = None;
137                let mut output: Option<String> = None;
138                let mut data: Option<Value> = None;
139
140                while let Some(key) = map.next_key::<String>()? {
141                    match key.as_str() {
142                        "type" => type_field = Some(map.next_value()?),
143                        "id" => id = Some(map.next_value()?),
144                        "status" => status = Some(map.next_value()?),
145                        "role" => role = Some(map.next_value()?),
146                        "content" => {
147                            // content can be Vec<ContentPart> or String
148                            let val: Value = map.next_value()?;
149                            if let Value::Array(_) = &val {
150                                content =
151                                    Some(serde_json::from_value(val).map_err(de::Error::custom)?);
152                            } else if let Value::String(s) = val {
153                                reasoning_content = Some(s);
154                            }
155                        }
156                        "summary" => summary = Some(map.next_value()?),
157                        "encrypted_content" => encrypted_content = Some(map.next_value()?),
158                        "name" => name = Some(map.next_value()?),
159                        "arguments" => arguments = Some(map.next_value()?),
160                        "call_id" => call_id = Some(map.next_value()?),
161                        "output" => output = Some(map.next_value()?),
162                        "data" => data = Some(map.next_value()?),
163                        _ => {
164                            // Skip unknown fields
165                            let _: Value = map.next_value()?;
166                        }
167                    }
168                }
169
170                let type_str = type_field.ok_or_else(|| de::Error::missing_field("type"))?;
171                let id = id.ok_or_else(|| de::Error::missing_field("id"))?;
172                let status = status.unwrap_or(ItemStatus::InProgress);
173
174                match type_str.as_str() {
175                    "message" => Ok(OutputItem::Message(MessageItem {
176                        id,
177                        status,
178                        role: role.unwrap_or_default(),
179                        content: content.unwrap_or_default(),
180                    })),
181                    "reasoning" => Ok(OutputItem::Reasoning(ReasoningItem {
182                        id,
183                        status,
184                        summary,
185                        content: reasoning_content,
186                        encrypted_content,
187                    })),
188                    "function_call" => Ok(OutputItem::FunctionCall(FunctionCallItem {
189                        id,
190                        status,
191                        name: name.ok_or_else(|| de::Error::missing_field("name"))?,
192                        arguments: arguments.unwrap_or(Value::Null),
193                        call_id,
194                    })),
195                    "function_call_output" => {
196                        Ok(OutputItem::FunctionCallOutput(FunctionCallOutputItem {
197                            id,
198                            status,
199                            call_id,
200                            output: output.ok_or_else(|| de::Error::missing_field("output"))?,
201                        }))
202                    }
203                    // Any other type is treated as a custom extension type
204                    custom_type => Ok(OutputItem::Custom(CustomItem {
205                        id,
206                        status,
207                        custom_type: custom_type.to_string(),
208                        data: data.unwrap_or(Value::Null),
209                    })),
210                }
211            }
212        }
213
214        deserializer.deserialize_map(OutputItemVisitor)
215    }
216}
217
218impl OutputItem {
219    /// Returns the unique identifier for this item.
220    pub fn id(&self) -> &str {
221        match self {
222            Self::Message(m) => &m.id,
223            Self::Reasoning(r) => &r.id,
224            Self::FunctionCall(f) => &f.id,
225            Self::FunctionCallOutput(f) => &f.id,
226            Self::Custom(c) => &c.id,
227        }
228    }
229
230    /// Returns the current status of this item.
231    pub fn status(&self) -> ItemStatus {
232        match self {
233            Self::Message(m) => m.status,
234            Self::Reasoning(r) => r.status,
235            Self::FunctionCall(f) => f.status,
236            Self::FunctionCallOutput(f) => f.status,
237            Self::Custom(c) => c.status,
238        }
239    }
240
241    /// Returns the type name for this item.
242    pub fn type_name(&self) -> &str {
243        match self {
244            Self::Message(_) => "message",
245            Self::Reasoning(_) => "reasoning",
246            Self::FunctionCall(_) => "function_call",
247            Self::FunctionCallOutput(_) => "function_call_output",
248            Self::Custom(c) => &c.custom_type,
249        }
250    }
251
252    /// Creates a new message item with the given parameters (status: `InProgress`).
253    pub fn message(id: impl Into<String>, role: MessageRole, content: Vec<ContentPart>) -> Self {
254        Self::Message(MessageItem {
255            id: id.into(),
256            status: ItemStatus::InProgress,
257            role,
258            content,
259        })
260    }
261
262    /// Creates a new completed message item with the given parameters.
263    pub fn completed_message(
264        id: impl Into<String>,
265        role: MessageRole,
266        content: Vec<ContentPart>,
267    ) -> Self {
268        Self::Message(MessageItem {
269            id: id.into(),
270            status: ItemStatus::Completed,
271            role,
272            content,
273        })
274    }
275
276    /// Creates a new reasoning item.
277    pub fn reasoning(id: impl Into<String>) -> Self {
278        Self::Reasoning(ReasoningItem {
279            id: id.into(),
280            status: ItemStatus::InProgress,
281            summary: None,
282            content: None,
283            encrypted_content: None,
284        })
285    }
286
287    /// Creates a new function call item.
288    pub fn function_call(id: impl Into<String>, name: impl Into<String>, arguments: Value) -> Self {
289        Self::FunctionCall(FunctionCallItem {
290            id: id.into(),
291            status: ItemStatus::InProgress,
292            name: name.into(),
293            arguments,
294            call_id: None,
295        })
296    }
297
298    /// Creates a new function call output item (status: `InProgress`).
299    ///
300    /// Use this for streaming scenarios. For completed tool results, use
301    /// [`completed_function_call_output`](Self::completed_function_call_output).
302    pub fn function_call_output(
303        id: impl Into<String>,
304        call_id: Option<String>,
305        output: impl Into<String>,
306    ) -> Self {
307        Self::FunctionCallOutput(FunctionCallOutputItem {
308            id: id.into(),
309            status: ItemStatus::InProgress,
310            call_id,
311            output: output.into(),
312        })
313    }
314
315    /// Creates a new completed function call output item.
316    ///
317    /// Use this when the tool execution has finished and the output is final.
318    pub fn completed_function_call_output(
319        id: impl Into<String>,
320        call_id: Option<String>,
321        output: impl Into<String>,
322    ) -> Self {
323        Self::FunctionCallOutput(FunctionCallOutputItem {
324            id: id.into(),
325            status: ItemStatus::Completed,
326            call_id,
327            output: output.into(),
328        })
329    }
330}
331
332/// A message item representing conversation content.
333#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334pub struct MessageItem {
335    /// Unique identifier for this item.
336    pub id: OutputItemId,
337
338    /// Current lifecycle status.
339    pub status: ItemStatus,
340
341    /// Role of the message author.
342    pub role: MessageRole,
343
344    /// Content parts that make up this message.
345    pub content: Vec<ContentPart>,
346}
347
348/// Role of a message author.
349#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
350#[serde(rename_all = "lowercase")]
351pub enum MessageRole {
352    /// User message.
353    User,
354    /// Assistant/model message.
355    #[default]
356    Assistant,
357    /// System message.
358    System,
359    /// Developer message (for instructions).
360    Developer,
361}
362
363impl std::fmt::Display for MessageRole {
364    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365        match self {
366            Self::User => write!(f, "user"),
367            Self::Assistant => write!(f, "assistant"),
368            Self::System => write!(f, "system"),
369            Self::Developer => write!(f, "developer"),
370        }
371    }
372}
373
374/// A reasoning item containing model's internal thought process.
375#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
376pub struct ReasoningItem {
377    /// Unique identifier for this item.
378    pub id: OutputItemId,
379
380    /// Current lifecycle status.
381    pub status: ItemStatus,
382
383    /// Summary of the reasoning (human-readable).
384    #[serde(skip_serializing_if = "Option::is_none")]
385    pub summary: Option<String>,
386
387    /// Raw reasoning trace content.
388    #[serde(skip_serializing_if = "Option::is_none")]
389    pub content: Option<String>,
390
391    /// Encrypted reasoning content for rehydration.
392    #[serde(skip_serializing_if = "Option::is_none")]
393    pub encrypted_content: Option<String>,
394}
395
396/// A function call item representing a tool invocation request.
397#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398pub struct FunctionCallItem {
399    /// Unique identifier for this item.
400    pub id: OutputItemId,
401
402    /// Current lifecycle status.
403    pub status: ItemStatus,
404
405    /// Name of the function to call.
406    pub name: String,
407
408    /// Arguments to pass to the function (JSON object).
409    pub arguments: Value,
410
411    /// Optional call ID for correlating with output.
412    #[serde(skip_serializing_if = "Option::is_none")]
413    pub call_id: Option<String>,
414}
415
416/// Output from a function call execution.
417#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
418pub struct FunctionCallOutputItem {
419    /// Unique identifier for this item.
420    pub id: OutputItemId,
421
422    /// Current lifecycle status.
423    pub status: ItemStatus,
424
425    /// ID of the function call this output corresponds to.
426    #[serde(skip_serializing_if = "Option::is_none")]
427    pub call_id: Option<String>,
428
429    /// Output content from the function execution.
430    pub output: String,
431}
432
433/// Custom/extension item type.
434///
435/// Custom types must be prefixed with an implementor slug (e.g., `vtcode:file_change`).
436#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
437pub struct CustomItem {
438    /// Unique identifier for this item.
439    pub id: OutputItemId,
440
441    /// Current lifecycle status.
442    pub status: ItemStatus,
443
444    /// Custom type identifier (must be prefixed, e.g., `vtcode:file_change`).
445    pub custom_type: String,
446
447    /// Custom data payload.
448    pub data: Value,
449}
450
451impl CustomItem {
452    /// Creates a new custom item with VT Code prefix.
453    pub fn vtcode(id: impl Into<String>, name: &str, data: Value) -> Self {
454        Self {
455            id: id.into(),
456            status: ItemStatus::InProgress,
457            custom_type: format!("vtcode:{name}"),
458            data,
459        }
460    }
461}
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    #[test]
468    fn test_output_item_id() {
469        let item = OutputItem::message("msg_1", MessageRole::Assistant, vec![]);
470        assert_eq!(item.id(), "msg_1");
471        assert_eq!(item.type_name(), "message");
472    }
473
474    #[test]
475    fn test_function_call_serialization() {
476        let item = OutputItem::function_call(
477            "fc_1",
478            "read_file",
479            serde_json::json!({"path": "/etc/passwd"}),
480        );
481        let json = serde_json::to_string(&item).unwrap();
482        assert!(json.contains("\"type\":\"function_call\""));
483        assert!(json.contains("\"name\":\"read_file\""));
484    }
485
486    #[test]
487    fn test_custom_item_vtcode() {
488        let item = CustomItem::vtcode(
489            "custom_1",
490            "file_change",
491            serde_json::json!({"path": "test.rs", "kind": "update"}),
492        );
493        assert_eq!(item.custom_type, "vtcode:file_change");
494    }
495
496    #[test]
497    fn test_custom_item_serializes_with_custom_type_as_type() {
498        let item = OutputItem::Custom(CustomItem::vtcode(
499            "custom_1",
500            "file_change",
501            serde_json::json!({"path": "test.rs"}),
502        ));
503        let json = serde_json::to_string(&item).unwrap();
504        // Custom type should be the type discriminator, not "custom"
505        assert!(json.contains("\"type\":\"vtcode:file_change\""));
506        assert!(!json.contains("\"type\":\"custom\""));
507        assert!(!json.contains("\"custom_type\""));
508    }
509
510    #[test]
511    fn test_custom_item_roundtrip() {
512        let original = OutputItem::Custom(CustomItem::vtcode(
513            "custom_1",
514            "file_change",
515            serde_json::json!({"path": "test.rs", "kind": "update"}),
516        ));
517        let json = serde_json::to_string(&original).unwrap();
518        let parsed: OutputItem = serde_json::from_str(&json).unwrap();
519        assert_eq!(original, parsed);
520
521        if let OutputItem::Custom(c) = &parsed {
522            assert_eq!(c.custom_type, "vtcode:file_change");
523            assert_eq!(c.data["path"], "test.rs");
524        } else {
525            panic!("Expected Custom variant");
526        }
527    }
528
529    #[test]
530    fn test_deserialize_unknown_type_as_custom() {
531        let json = r#"{"type":"vendor:special_item","id":"item_1","status":"completed","data":{"key":"value"}}"#;
532        let item: OutputItem = serde_json::from_str(json).unwrap();
533        if let OutputItem::Custom(c) = item {
534            assert_eq!(c.custom_type, "vendor:special_item");
535            assert_eq!(c.id, "item_1");
536            assert_eq!(c.status, ItemStatus::Completed);
537            assert_eq!(c.data["key"], "value");
538        } else {
539            panic!("Expected Custom variant for unknown type");
540        }
541    }
542
543    #[test]
544    fn test_completed_message_has_completed_status() {
545        let item = OutputItem::completed_message("msg_1", MessageRole::Assistant, vec![]);
546        assert_eq!(item.status(), ItemStatus::Completed);
547        if let OutputItem::Message(m) = item {
548            assert_eq!(m.status, ItemStatus::Completed);
549        } else {
550            panic!("Expected Message variant");
551        }
552    }
553
554    #[test]
555    fn test_completed_function_call_output_has_completed_status() {
556        let item =
557            OutputItem::completed_function_call_output("fco_1", Some("fc_1".to_string()), "result");
558        assert_eq!(item.status(), ItemStatus::Completed);
559        if let OutputItem::FunctionCallOutput(f) = item {
560            assert_eq!(f.status, ItemStatus::Completed);
561            assert_eq!(f.call_id, Some("fc_1".to_string()));
562            assert_eq!(f.output, "result");
563        } else {
564            panic!("Expected FunctionCallOutput variant");
565        }
566    }
567}