1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct MessageInfo {
12 pub id: String,
14 #[serde(default, skip_serializing_if = "Option::is_none")]
16 pub session_id: Option<String>,
17 pub role: String,
19 pub time: MessageTime,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub agent: Option<String>,
24 #[serde(default, skip_serializing_if = "Option::is_none")]
26 pub variant: Option<String>,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct MessageTime {
32 pub created: i64,
34 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub completed: Option<i64>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct Message {
46 pub info: MessageInfo,
48 pub parts: Vec<Part>,
50}
51
52impl Message {
53 pub fn id(&self) -> &str {
55 &self.info.id
56 }
57
58 pub fn session_id(&self) -> Option<&str> {
60 self.info.session_id.as_deref()
61 }
62
63 pub fn role(&self) -> &str {
65 &self.info.role
66 }
67}
68
69pub type MessageWithParts = Message;
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(tag = "type", rename_all = "kebab-case")]
75pub enum Part {
76 Text {
78 #[serde(default)]
80 id: Option<String>,
81 text: String,
83 #[serde(default, skip_serializing_if = "Option::is_none")]
85 synthetic: Option<bool>,
86 #[serde(default, skip_serializing_if = "Option::is_none")]
88 ignored: Option<bool>,
89 #[serde(default, skip_serializing_if = "Option::is_none")]
91 metadata: Option<serde_json::Value>,
92 },
93 File {
95 #[serde(default)]
97 id: Option<String>,
98 mime: String,
100 url: String,
102 #[serde(skip_serializing_if = "Option::is_none")]
104 filename: Option<String>,
105 #[serde(default, skip_serializing_if = "Option::is_none")]
107 source: Option<FilePartSource>,
108 },
109 Tool {
111 #[serde(default)]
113 id: Option<String>,
114 #[serde(rename = "callID")]
116 call_id: String,
117 tool: String,
119 #[serde(default)]
121 input: serde_json::Value,
122 #[serde(default)]
124 state: Option<ToolState>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
127 metadata: Option<serde_json::Value>,
128 },
129 Reasoning {
131 #[serde(default)]
133 id: Option<String>,
134 text: String,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 metadata: Option<serde_json::Value>,
139 },
140 #[serde(rename = "step-start")]
142 StepStart {
143 #[serde(default)]
145 id: Option<String>,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
148 snapshot: Option<String>,
149 },
150 #[serde(rename = "step-finish")]
152 StepFinish {
153 #[serde(default)]
155 id: Option<String>,
156 reason: String,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
160 snapshot: Option<String>,
161 #[serde(default)]
163 cost: f64,
164 #[serde(default, skip_serializing_if = "Option::is_none")]
166 tokens: Option<TokenUsage>,
167 },
168 Snapshot {
170 #[serde(default)]
172 id: Option<String>,
173 snapshot: String,
175 },
176 Patch {
178 #[serde(default)]
180 id: Option<String>,
181 hash: String,
183 #[serde(default)]
185 files: Vec<String>,
186 },
187 Agent {
189 #[serde(default)]
191 id: Option<String>,
192 name: String,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 source: Option<AgentSource>,
197 },
198 Retry {
200 #[serde(default)]
202 id: Option<String>,
203 attempt: u32,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 error: Option<crate::types::error::APIError>,
208 },
209 Compaction {
211 #[serde(default)]
213 id: Option<String>,
214 #[serde(default)]
216 auto: bool,
217 },
218 Subtask {
220 #[serde(default)]
222 id: Option<String>,
223 prompt: String,
225 description: String,
227 agent: String,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
231 command: Option<String>,
232 },
233 #[serde(other)]
235 Unknown,
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct AgentSource {
241 pub value: String,
243 pub start: i64,
245 pub end: i64,
247}
248
249#[derive(Debug, Clone, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct FilePartSourceText {
255 pub value: String,
257 pub start: i64,
259 pub end: i64,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265#[serde(tag = "type")]
266pub enum FilePartSource {
267 #[serde(rename = "file")]
269 File {
270 text: FilePartSourceText,
272 path: String,
274 #[serde(flatten)]
276 extra: serde_json::Value,
277 },
278 #[serde(rename = "symbol")]
280 Symbol {
281 text: FilePartSourceText,
283 path: String,
285 range: serde_json::Value,
287 name: String,
289 kind: i64,
291 #[serde(flatten)]
293 extra: serde_json::Value,
294 },
295 #[serde(rename = "resource")]
297 Resource {
298 text: FilePartSourceText,
300 #[serde(rename = "clientName")]
302 client_name: String,
303 uri: String,
305 #[serde(flatten)]
307 extra: serde_json::Value,
308 },
309 #[serde(other)]
311 Unknown,
312}
313
314#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "camelCase")]
319pub struct ToolTimeStart {
320 pub start: i64,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326#[serde(rename_all = "camelCase")]
327pub struct ToolTimeRange {
328 pub start: i64,
330 pub end: i64,
332 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub compacted: Option<i64>,
335}
336
337#[derive(Debug, Clone, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct ToolStatePending {
341 pub status: String,
343 pub input: serde_json::Value,
345 pub raw: String,
347 #[serde(flatten)]
349 pub extra: serde_json::Value,
350}
351
352#[derive(Debug, Clone, Serialize, Deserialize)]
354#[serde(rename_all = "camelCase")]
355pub struct ToolStateRunning {
356 pub status: String,
358 pub input: serde_json::Value,
360 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub title: Option<String>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
365 pub metadata: Option<serde_json::Value>,
366 pub time: ToolTimeStart,
368 #[serde(flatten)]
370 pub extra: serde_json::Value,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
375#[serde(rename_all = "camelCase")]
376pub struct ToolStateCompleted {
377 pub status: String,
379 pub input: serde_json::Value,
381 pub output: String,
383 pub title: String,
385 pub metadata: serde_json::Value,
387 pub time: ToolTimeRange,
389 #[serde(default, skip_serializing_if = "Option::is_none")]
391 pub attachments: Option<Vec<serde_json::Value>>,
392 #[serde(flatten)]
394 pub extra: serde_json::Value,
395}
396
397#[derive(Debug, Clone, Serialize, Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct ToolStateError {
401 pub status: String,
403 pub input: serde_json::Value,
405 pub error: String,
407 #[serde(default, skip_serializing_if = "Option::is_none")]
409 pub metadata: Option<serde_json::Value>,
410 pub time: ToolTimeRange,
412 #[serde(flatten)]
414 pub extra: serde_json::Value,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize)]
422#[serde(untagged)]
423pub enum ToolState {
424 Completed(ToolStateCompleted),
426 Error(ToolStateError),
428 Running(ToolStateRunning),
430 Pending(ToolStatePending),
432 Unknown(serde_json::Value),
434}
435
436impl ToolState {
437 pub fn status(&self) -> &str {
439 match self {
440 ToolState::Pending(s) => &s.status,
441 ToolState::Running(s) => &s.status,
442 ToolState::Completed(s) => &s.status,
443 ToolState::Error(s) => &s.status,
444 ToolState::Unknown(_) => "unknown",
445 }
446 }
447
448 pub fn output(&self) -> Option<&str> {
450 match self {
451 ToolState::Completed(s) => Some(&s.output),
452 _ => None,
453 }
454 }
455
456 pub fn error(&self) -> Option<&str> {
458 match self {
459 ToolState::Error(s) => Some(&s.error),
460 _ => None,
461 }
462 }
463
464 pub fn is_pending(&self) -> bool {
466 matches!(self, ToolState::Pending(_))
467 }
468
469 pub fn is_running(&self) -> bool {
471 matches!(self, ToolState::Running(_))
472 }
473
474 pub fn is_completed(&self) -> bool {
476 matches!(self, ToolState::Completed(_))
477 }
478
479 pub fn is_error(&self) -> bool {
481 matches!(self, ToolState::Error(_))
482 }
483}
484
485#[derive(Debug, Clone, Serialize, Deserialize)]
487#[serde(rename_all = "camelCase")]
488pub struct TokenUsage {
489 pub input: u64,
491 pub output: u64,
493 #[serde(default)]
495 pub reasoning: u64,
496 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub cache: Option<CacheUsage>,
499}
500
501#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct CacheUsage {
504 pub read: u64,
506 pub write: u64,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
512#[serde(rename_all = "camelCase")]
513pub struct PromptRequest {
514 pub parts: Vec<PromptPart>,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
518 pub message_id: Option<String>,
519 #[serde(default, skip_serializing_if = "Option::is_none")]
521 pub model: Option<crate::types::project::ModelRef>,
522 #[serde(default, skip_serializing_if = "Option::is_none")]
524 pub agent: Option<String>,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
527 pub no_reply: Option<bool>,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
530 pub system: Option<String>,
531 #[serde(default, skip_serializing_if = "Option::is_none")]
533 pub variant: Option<String>,
534}
535
536impl PromptRequest {
537 pub fn text(text: impl Into<String>) -> Self {
539 Self {
540 parts: vec![PromptPart::Text {
541 text: text.into(),
542 synthetic: None,
543 ignored: None,
544 metadata: None,
545 }],
546 message_id: None,
547 model: None,
548 agent: None,
549 no_reply: None,
550 system: None,
551 variant: None,
552 }
553 }
554
555 pub fn with_model(
557 mut self,
558 provider_id: impl Into<String>,
559 model_id: impl Into<String>,
560 ) -> Self {
561 self.model = Some(crate::types::project::ModelRef {
562 provider_id: provider_id.into(),
563 model_id: model_id.into(),
564 });
565 self
566 }
567
568 pub fn with_system(mut self, system: impl Into<String>) -> Self {
570 self.system = Some(system.into());
571 self
572 }
573
574 pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
576 self.agent = Some(agent.into());
577 self
578 }
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize)]
583#[serde(tag = "type", rename_all = "kebab-case")]
584pub enum PromptPart {
585 Text {
587 text: String,
589 #[serde(default, skip_serializing_if = "Option::is_none")]
591 synthetic: Option<bool>,
592 #[serde(default, skip_serializing_if = "Option::is_none")]
594 ignored: Option<bool>,
595 #[serde(default, skip_serializing_if = "Option::is_none")]
597 metadata: Option<serde_json::Value>,
598 },
599 File {
601 mime: String,
603 url: String,
605 #[serde(default, skip_serializing_if = "Option::is_none")]
607 filename: Option<String>,
608 },
609 Agent {
611 name: String,
613 },
614 Subtask {
616 prompt: String,
618 description: String,
620 agent: String,
622 #[serde(default, skip_serializing_if = "Option::is_none")]
624 command: Option<String>,
625 },
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize)]
630#[serde(rename_all = "camelCase")]
631pub struct CommandRequest {
632 pub command: String,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub args: Option<serde_json::Value>,
637}
638
639#[derive(Debug, Clone, Serialize, Deserialize)]
641#[serde(rename_all = "camelCase")]
642pub struct ShellRequest {
643 pub command: String,
645 #[serde(default, skip_serializing_if = "Option::is_none")]
647 pub model: Option<crate::types::project::ModelRef>,
648}
649
650#[cfg(test)]
651mod tests {
652 use super::*;
653
654 #[test]
655 fn test_part_text_deserialize() {
656 let json = r#"{"type":"text","id":"p1","text":"hello"}"#;
657 let part: Part = serde_json::from_str(json).unwrap();
658 assert!(matches!(part, Part::Text { text, .. } if text == "hello"));
659 }
660
661 #[test]
662 fn test_part_tool_deserialize() {
663 let json = r#"{"type":"tool","callID":"c1","tool":"read_file","input":{}}"#;
664 let part: Part = serde_json::from_str(json).unwrap();
665 assert!(matches!(part, Part::Tool { tool, .. } if tool == "read_file"));
666 }
667
668 #[test]
669 fn test_part_step_start_deserialize() {
670 let json = r#"{"type":"step-start"}"#;
671 let part: Part = serde_json::from_str(json).unwrap();
672 assert!(matches!(part, Part::StepStart { .. }));
673 }
674
675 #[test]
676 fn test_part_step_finish_deserialize() {
677 let json = r#"{"type":"step-finish","reason":"done","cost":0.01}"#;
678 let part: Part = serde_json::from_str(json).unwrap();
679 assert!(matches!(part, Part::StepFinish { reason, .. } if reason == "done"));
680 }
681
682 #[test]
683 fn test_part_unknown_deserialize() {
684 let json = r#"{"type":"future-part-type","data":"whatever"}"#;
685 let part: Part = serde_json::from_str(json).unwrap();
686 assert!(matches!(part, Part::Unknown));
687 }
688
689 #[test]
692 fn test_tool_state_pending() {
693 let json = r#"{
694 "status": "pending",
695 "input": {"file": "test.rs"},
696 "raw": "read test.rs"
697 }"#;
698 let state: ToolState = serde_json::from_str(json).unwrap();
699 assert!(state.is_pending());
700 assert_eq!(state.status(), "pending");
701 assert!(state.output().is_none());
702 }
703
704 #[test]
705 fn test_tool_state_running() {
706 let json = r#"{
707 "status": "running",
708 "input": {"file": "test.rs"},
709 "title": "Reading file",
710 "time": {"start": 1234567890}
711 }"#;
712 let state: ToolState = serde_json::from_str(json).unwrap();
713 assert!(state.is_running());
714 assert_eq!(state.status(), "running");
715 }
716
717 #[test]
718 fn test_tool_state_completed() {
719 let json = r#"{
720 "status": "completed",
721 "input": {"file": "test.rs"},
722 "output": "file contents here",
723 "title": "Read test.rs",
724 "metadata": {},
725 "time": {"start": 1234567890, "end": 1234567900}
726 }"#;
727 let state: ToolState = serde_json::from_str(json).unwrap();
728 assert!(state.is_completed());
729 assert_eq!(state.status(), "completed");
730 assert_eq!(state.output(), Some("file contents here"));
731 }
732
733 #[test]
734 fn test_tool_state_error() {
735 let json = r#"{
736 "status": "error",
737 "input": {"file": "missing.rs"},
738 "error": "File not found",
739 "time": {"start": 1234567890, "end": 1234567900}
740 }"#;
741 let state: ToolState = serde_json::from_str(json).unwrap();
742 assert!(state.is_error());
743 assert_eq!(state.status(), "error");
744 assert_eq!(state.error(), Some("File not found"));
745 }
746
747 #[test]
748 fn test_tool_state_unknown() {
749 let json = r#"{
750 "status": "future-status",
751 "someField": "someValue"
752 }"#;
753 let state: ToolState = serde_json::from_str(json).unwrap();
754 assert!(matches!(state, ToolState::Unknown(_)));
755 assert_eq!(state.status(), "unknown");
756 }
757
758 #[test]
761 fn test_file_part_source_file() {
762 let json = r#"{
763 "type": "file",
764 "text": {"value": "content", "start": 0, "end": 100},
765 "path": "/src/main.rs"
766 }"#;
767 let source: FilePartSource = serde_json::from_str(json).unwrap();
768 assert!(matches!(source, FilePartSource::File { path, .. } if path == "/src/main.rs"));
769 }
770
771 #[test]
772 fn test_file_part_source_symbol() {
773 let json = r#"{
774 "type": "symbol",
775 "text": {"value": "fn main()", "start": 10, "end": 20},
776 "path": "/src/main.rs",
777 "range": {"start": {"line": 0, "character": 0}, "end": {"line": 5, "character": 1}},
778 "name": "main",
779 "kind": 12
780 }"#;
781 let source: FilePartSource = serde_json::from_str(json).unwrap();
782 assert!(
783 matches!(source, FilePartSource::Symbol { name, kind, .. } if name == "main" && kind == 12)
784 );
785 }
786
787 #[test]
788 fn test_file_part_source_resource() {
789 let json = r#"{
790 "type": "resource",
791 "text": {"value": "resource content", "start": 0, "end": 50},
792 "clientName": "my-mcp-server",
793 "uri": "resource://data/file.txt"
794 }"#;
795 let source: FilePartSource = serde_json::from_str(json).unwrap();
796 assert!(
797 matches!(source, FilePartSource::Resource { client_name, uri, .. }
798 if client_name == "my-mcp-server" && uri == "resource://data/file.txt")
799 );
800 }
801
802 #[test]
803 fn test_file_part_source_unknown() {
804 let json = r#"{
805 "type": "future-source",
806 "data": "whatever"
807 }"#;
808 let source: FilePartSource = serde_json::from_str(json).unwrap();
809 assert!(matches!(source, FilePartSource::Unknown));
810 }
811
812 #[test]
813 fn test_file_part_source_with_extra_fields() {
814 let json = r#"{
815 "type": "file",
816 "text": {"value": "content", "start": 0, "end": 100},
817 "path": "/src/main.rs",
818 "newField": "preserved"
819 }"#;
820 let source: FilePartSource = serde_json::from_str(json).unwrap();
821 if let FilePartSource::File { extra, .. } = source {
822 assert_eq!(extra.get("newField").unwrap(), "preserved");
823 } else {
824 panic!("Expected FilePartSource::File");
825 }
826 }
827
828 #[test]
829 fn test_prompt_request_text_builder() {
830 let req = PromptRequest::text("hello");
831 assert!(matches!(req.parts.as_slice(), [PromptPart::Text { text, .. }] if text == "hello"));
832 assert!(req.model.is_none());
833 assert!(req.system.is_none());
834 assert!(req.agent.is_none());
835 }
836
837 #[test]
838 fn test_prompt_request_chain_builders() {
839 let req = PromptRequest::text("hello")
840 .with_model("opencode", "kimi-k2.5-free")
841 .with_system("Be concise")
842 .with_agent("coder");
843
844 assert_eq!(
845 req.model.as_ref().map(|m| m.provider_id.as_str()),
846 Some("opencode")
847 );
848 assert_eq!(
849 req.model.as_ref().map(|m| m.model_id.as_str()),
850 Some("kimi-k2.5-free")
851 );
852 assert_eq!(req.system.as_deref(), Some("Be concise"));
853 assert_eq!(req.agent.as_deref(), Some("coder"));
854 }
855}