1use 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
13pub type OutputItemId = String;
15
16#[derive(Debug, Clone, PartialEq)]
24pub enum OutputItem {
25 Message(MessageItem),
27
28 Reasoning(ReasoningItem),
30
31 FunctionCall(FunctionCallItem),
33
34 FunctionCallOutput(FunctionCallOutputItem),
36
37 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 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 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 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 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 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 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 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 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 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 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 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 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 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
334pub struct MessageItem {
335 pub id: OutputItemId,
337
338 pub status: ItemStatus,
340
341 pub role: MessageRole,
343
344 pub content: Vec<ContentPart>,
346}
347
348#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
350#[serde(rename_all = "lowercase")]
351pub enum MessageRole {
352 User,
354 #[default]
356 Assistant,
357 System,
359 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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
376pub struct ReasoningItem {
377 pub id: OutputItemId,
379
380 pub status: ItemStatus,
382
383 #[serde(skip_serializing_if = "Option::is_none")]
385 pub summary: Option<String>,
386
387 #[serde(skip_serializing_if = "Option::is_none")]
389 pub content: Option<String>,
390
391 #[serde(skip_serializing_if = "Option::is_none")]
393 pub encrypted_content: Option<String>,
394}
395
396#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
398pub struct FunctionCallItem {
399 pub id: OutputItemId,
401
402 pub status: ItemStatus,
404
405 pub name: String,
407
408 pub arguments: Value,
410
411 #[serde(skip_serializing_if = "Option::is_none")]
413 pub call_id: Option<String>,
414}
415
416#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
418pub struct FunctionCallOutputItem {
419 pub id: OutputItemId,
421
422 pub status: ItemStatus,
424
425 #[serde(skip_serializing_if = "Option::is_none")]
427 pub call_id: Option<String>,
428
429 pub output: String,
431}
432
433#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
437pub struct CustomItem {
438 pub id: OutputItemId,
440
441 pub status: ItemStatus,
443
444 pub custom_type: String,
446
447 pub data: Value,
449}
450
451impl CustomItem {
452 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 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}