Skip to main content

opencode_sdk/types/
message.rs

1//! Message and content part types for opencode_rs.
2//!
3// TODO(3): Add unit tests for Message, Part variants, and PromptPart serialization/deserialization
4// TODO(3): Consider using enum for `role` field (User/Assistant/System) with #[serde(other)] for forward-compat
5
6use serde::{Deserialize, Serialize};
7
8/// Message info (metadata).
9#[derive(Debug, Clone, Serialize, Deserialize)]
10#[serde(rename_all = "camelCase")]
11pub struct MessageInfo {
12    /// Unique message identifier.
13    pub id: String,
14    /// Session ID (may be omitted when context implies it).
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub session_id: Option<String>,
17    /// Message role (user, assistant, system).
18    pub role: String,
19    /// Message timestamps.
20    pub time: MessageTime,
21    /// Agent name if applicable.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub agent: Option<String>,
24    /// Message variant.
25    #[serde(default, skip_serializing_if = "Option::is_none")]
26    pub variant: Option<String>,
27}
28
29/// Message timestamps.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct MessageTime {
32    /// Creation timestamp.
33    pub created: i64,
34    /// Completion timestamp.
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub completed: Option<i64>,
37}
38
39/// A message with its parts (API response format).
40///
41/// This is the format returned by the message list endpoint.
42/// It contains a nested `info` object and a `parts` array.
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(rename_all = "camelCase")]
45pub struct Message {
46    /// Message info/metadata.
47    pub info: MessageInfo,
48    /// Content parts.
49    pub parts: Vec<Part>,
50}
51
52impl Message {
53    /// Get the message ID.
54    pub fn id(&self) -> &str {
55        &self.info.id
56    }
57
58    /// Get the session ID if present.
59    pub fn session_id(&self) -> Option<&str> {
60        self.info.session_id.as_deref()
61    }
62
63    /// Get the message role.
64    pub fn role(&self) -> &str {
65        &self.info.role
66    }
67}
68
69/// Alias for backward compatibility.
70pub type MessageWithParts = Message;
71
72/// A content part within a message (12 variants).
73#[derive(Debug, Clone, Serialize, Deserialize)]
74#[serde(tag = "type", rename_all = "kebab-case")]
75pub enum Part {
76    /// Text content.
77    Text {
78        /// Part identifier.
79        #[serde(default)]
80        id: Option<String>,
81        /// Text content.
82        text: String,
83        /// Whether this is synthetic (generated).
84        #[serde(default, skip_serializing_if = "Option::is_none")]
85        synthetic: Option<bool>,
86        /// Whether this part is ignored.
87        #[serde(default, skip_serializing_if = "Option::is_none")]
88        ignored: Option<bool>,
89        /// Additional metadata.
90        #[serde(default, skip_serializing_if = "Option::is_none")]
91        metadata: Option<serde_json::Value>,
92    },
93    /// File attachment.
94    File {
95        /// Part identifier.
96        #[serde(default)]
97        id: Option<String>,
98        /// MIME type.
99        mime: String,
100        /// File URL.
101        url: String,
102        /// Original filename.
103        #[serde(skip_serializing_if = "Option::is_none")]
104        filename: Option<String>,
105        /// File source info.
106        #[serde(default, skip_serializing_if = "Option::is_none")]
107        source: Option<FilePartSource>,
108    },
109    /// Tool invocation.
110    Tool {
111        /// Part identifier.
112        #[serde(default)]
113        id: Option<String>,
114        /// Tool call ID.
115        #[serde(rename = "callID")]
116        call_id: String,
117        /// Tool name.
118        tool: String,
119        /// Tool input arguments.
120        #[serde(default)]
121        input: serde_json::Value,
122        /// Tool execution state.
123        #[serde(default)]
124        state: Option<ToolState>,
125        /// Additional metadata.
126        #[serde(default, skip_serializing_if = "Option::is_none")]
127        metadata: Option<serde_json::Value>,
128    },
129    /// Reasoning/thinking content.
130    Reasoning {
131        /// Part identifier.
132        #[serde(default)]
133        id: Option<String>,
134        /// Reasoning text.
135        text: String,
136        /// Additional metadata.
137        #[serde(default, skip_serializing_if = "Option::is_none")]
138        metadata: Option<serde_json::Value>,
139    },
140    /// Step start marker.
141    #[serde(rename = "step-start")]
142    StepStart {
143        /// Part identifier.
144        #[serde(default)]
145        id: Option<String>,
146        /// Snapshot ID.
147        #[serde(default, skip_serializing_if = "Option::is_none")]
148        snapshot: Option<String>,
149    },
150    /// Step finish marker.
151    #[serde(rename = "step-finish")]
152    StepFinish {
153        /// Part identifier.
154        #[serde(default)]
155        id: Option<String>,
156        /// Finish reason.
157        reason: String,
158        /// Snapshot ID.
159        #[serde(default, skip_serializing_if = "Option::is_none")]
160        snapshot: Option<String>,
161        /// Cost incurred.
162        #[serde(default)]
163        cost: f64,
164        /// Token usage.
165        #[serde(default, skip_serializing_if = "Option::is_none")]
166        tokens: Option<TokenUsage>,
167    },
168    /// Snapshot marker.
169    Snapshot {
170        /// Part identifier.
171        #[serde(default)]
172        id: Option<String>,
173        /// Snapshot ID.
174        snapshot: String,
175    },
176    /// Patch information.
177    Patch {
178        /// Part identifier.
179        #[serde(default)]
180        id: Option<String>,
181        /// Patch hash.
182        hash: String,
183        /// Affected files.
184        #[serde(default)]
185        files: Vec<String>,
186    },
187    /// Agent delegation.
188    Agent {
189        /// Part identifier.
190        #[serde(default)]
191        id: Option<String>,
192        /// Agent name.
193        name: String,
194        /// Agent source info.
195        #[serde(default, skip_serializing_if = "Option::is_none")]
196        source: Option<AgentSource>,
197    },
198    /// Retry marker.
199    Retry {
200        /// Part identifier.
201        #[serde(default)]
202        id: Option<String>,
203        /// Attempt number.
204        attempt: u32,
205        /// Error that caused retry.
206        #[serde(default, skip_serializing_if = "Option::is_none")]
207        error: Option<crate::types::error::APIError>,
208    },
209    /// Compaction marker.
210    Compaction {
211        /// Part identifier.
212        #[serde(default)]
213        id: Option<String>,
214        /// Whether this was automatic.
215        #[serde(default)]
216        auto: bool,
217    },
218    /// Subtask delegation.
219    Subtask {
220        /// Part identifier.
221        #[serde(default)]
222        id: Option<String>,
223        /// Subtask prompt.
224        prompt: String,
225        /// Subtask description.
226        description: String,
227        /// Agent to handle subtask.
228        agent: String,
229        /// Optional command.
230        #[serde(default, skip_serializing_if = "Option::is_none")]
231        command: Option<String>,
232    },
233    /// Unknown part type (forward compatibility).
234    #[serde(other)]
235    Unknown,
236}
237
238/// Agent source information.
239#[derive(Debug, Clone, Serialize, Deserialize)]
240pub struct AgentSource {
241    /// Source value.
242    pub value: String,
243    /// Start offset.
244    pub start: i64,
245    /// End offset.
246    pub end: i64,
247}
248
249// ==================== FilePartSource ====================
250
251/// Text range within a file part source.
252#[derive(Debug, Clone, Serialize, Deserialize)]
253#[serde(rename_all = "camelCase")]
254pub struct FilePartSourceText {
255    /// The text content.
256    pub value: String,
257    /// Start offset in file.
258    pub start: i64,
259    /// End offset in file.
260    pub end: i64,
261}
262
263/// Source information for a file part (internally tagged by "type").
264#[derive(Debug, Clone, Serialize, Deserialize)]
265#[serde(tag = "type")]
266pub enum FilePartSource {
267    /// File source.
268    #[serde(rename = "file")]
269    File {
270        /// Text range.
271        text: FilePartSourceText,
272        /// File path.
273        path: String,
274        /// Additional fields.
275        #[serde(flatten)]
276        extra: serde_json::Value,
277    },
278    /// Symbol source (from LSP).
279    #[serde(rename = "symbol")]
280    Symbol {
281        /// Text range.
282        text: FilePartSourceText,
283        /// File path.
284        path: String,
285        /// LSP range (kept as Value for now).
286        range: serde_json::Value,
287        /// Symbol name.
288        name: String,
289        /// Symbol kind (LSP SymbolKind).
290        kind: i64,
291        /// Additional fields.
292        #[serde(flatten)]
293        extra: serde_json::Value,
294    },
295    /// MCP resource source.
296    #[serde(rename = "resource")]
297    Resource {
298        /// Text range.
299        text: FilePartSourceText,
300        /// MCP client name.
301        #[serde(rename = "clientName")]
302        client_name: String,
303        /// Resource URI.
304        uri: String,
305        /// Additional fields.
306        #[serde(flatten)]
307        extra: serde_json::Value,
308    },
309    /// Unknown source type (forward compatibility).
310    #[serde(other)]
311    Unknown,
312}
313
314// ==================== ToolState ====================
315
316/// Tool execution time (start only).
317#[derive(Debug, Clone, Serialize, Deserialize)]
318#[serde(rename_all = "camelCase")]
319pub struct ToolTimeStart {
320    /// Start timestamp (ms).
321    pub start: i64,
322}
323
324/// Tool execution time range.
325#[derive(Debug, Clone, Serialize, Deserialize)]
326#[serde(rename_all = "camelCase")]
327pub struct ToolTimeRange {
328    /// Start timestamp (ms).
329    pub start: i64,
330    /// End timestamp (ms).
331    pub end: i64,
332    /// Compacted timestamp if applicable.
333    #[serde(default, skip_serializing_if = "Option::is_none")]
334    pub compacted: Option<i64>,
335}
336
337/// Tool state when pending execution.
338#[derive(Debug, Clone, Serialize, Deserialize)]
339#[serde(rename_all = "camelCase")]
340pub struct ToolStatePending {
341    /// Status field (always "pending").
342    pub status: String,
343    /// Tool input arguments.
344    pub input: serde_json::Value,
345    /// Raw input string.
346    pub raw: String,
347    /// Additional fields.
348    #[serde(flatten)]
349    pub extra: serde_json::Value,
350}
351
352/// Tool state when running.
353#[derive(Debug, Clone, Serialize, Deserialize)]
354#[serde(rename_all = "camelCase")]
355pub struct ToolStateRunning {
356    /// Status field (always "running").
357    pub status: String,
358    /// Tool input arguments.
359    pub input: serde_json::Value,
360    /// Display title.
361    #[serde(default, skip_serializing_if = "Option::is_none")]
362    pub title: Option<String>,
363    /// Additional metadata.
364    #[serde(default, skip_serializing_if = "Option::is_none")]
365    pub metadata: Option<serde_json::Value>,
366    /// Execution time.
367    pub time: ToolTimeStart,
368    /// Additional fields.
369    #[serde(flatten)]
370    pub extra: serde_json::Value,
371}
372
373/// Tool state when completed successfully.
374#[derive(Debug, Clone, Serialize, Deserialize)]
375#[serde(rename_all = "camelCase")]
376pub struct ToolStateCompleted {
377    /// Status field (always "completed").
378    pub status: String,
379    /// Tool input arguments.
380    pub input: serde_json::Value,
381    /// Tool output.
382    pub output: String,
383    /// Display title.
384    pub title: String,
385    /// Additional metadata.
386    pub metadata: serde_json::Value,
387    /// Execution time range.
388    pub time: ToolTimeRange,
389    /// File attachments.
390    #[serde(default, skip_serializing_if = "Option::is_none")]
391    pub attachments: Option<Vec<serde_json::Value>>,
392    /// Additional fields.
393    #[serde(flatten)]
394    pub extra: serde_json::Value,
395}
396
397/// Tool state when errored.
398#[derive(Debug, Clone, Serialize, Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct ToolStateError {
401    /// Status field (always "error").
402    pub status: String,
403    /// Tool input arguments.
404    pub input: serde_json::Value,
405    /// Error message.
406    pub error: String,
407    /// Additional metadata.
408    #[serde(default, skip_serializing_if = "Option::is_none")]
409    pub metadata: Option<serde_json::Value>,
410    /// Execution time range.
411    pub time: ToolTimeRange,
412    /// Additional fields.
413    #[serde(flatten)]
414    pub extra: serde_json::Value,
415}
416
417/// State of a tool execution (untagged enum with Unknown fallback).
418///
419/// Variant order matters for untagged enums - most specific variants with more
420/// required fields must come first to avoid less specific variants matching early.
421#[derive(Debug, Clone, Serialize, Deserialize)]
422#[serde(untagged)]
423pub enum ToolState {
424    /// Tool completed successfully.
425    Completed(ToolStateCompleted),
426    /// Tool encountered an error.
427    Error(ToolStateError),
428    /// Tool is currently running.
429    Running(ToolStateRunning),
430    /// Tool is pending execution.
431    Pending(ToolStatePending),
432    /// Unknown state (forward compatibility).
433    Unknown(serde_json::Value),
434}
435
436impl ToolState {
437    /// Get the status string for this tool state.
438    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    /// Get the output if the tool completed successfully.
449    pub fn output(&self) -> Option<&str> {
450        match self {
451            ToolState::Completed(s) => Some(&s.output),
452            _ => None,
453        }
454    }
455
456    /// Get the error message if the tool errored.
457    pub fn error(&self) -> Option<&str> {
458        match self {
459            ToolState::Error(s) => Some(&s.error),
460            _ => None,
461        }
462    }
463
464    /// Check if the tool is pending.
465    pub fn is_pending(&self) -> bool {
466        matches!(self, ToolState::Pending(_))
467    }
468
469    /// Check if the tool is running.
470    pub fn is_running(&self) -> bool {
471        matches!(self, ToolState::Running(_))
472    }
473
474    /// Check if the tool completed successfully.
475    pub fn is_completed(&self) -> bool {
476        matches!(self, ToolState::Completed(_))
477    }
478
479    /// Check if the tool errored.
480    pub fn is_error(&self) -> bool {
481        matches!(self, ToolState::Error(_))
482    }
483}
484
485/// Token usage information.
486#[derive(Debug, Clone, Serialize, Deserialize)]
487#[serde(rename_all = "camelCase")]
488pub struct TokenUsage {
489    /// Input tokens.
490    pub input: u64,
491    /// Output tokens.
492    pub output: u64,
493    /// Reasoning tokens.
494    #[serde(default)]
495    pub reasoning: u64,
496    /// Cache usage.
497    #[serde(default, skip_serializing_if = "Option::is_none")]
498    pub cache: Option<CacheUsage>,
499}
500
501/// Cache usage information.
502#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct CacheUsage {
504    /// Cache read tokens.
505    pub read: u64,
506    /// Cache write tokens.
507    pub write: u64,
508}
509
510/// Request to send a prompt to a session.
511#[derive(Debug, Clone, Serialize, Deserialize)]
512#[serde(rename_all = "camelCase")]
513pub struct PromptRequest {
514    /// Content parts to send.
515    pub parts: Vec<PromptPart>,
516    /// Message ID to reply to.
517    #[serde(default, skip_serializing_if = "Option::is_none")]
518    pub message_id: Option<String>,
519    /// Model to use.
520    #[serde(default, skip_serializing_if = "Option::is_none")]
521    pub model: Option<crate::types::project::ModelRef>,
522    /// Agent to use.
523    #[serde(default, skip_serializing_if = "Option::is_none")]
524    pub agent: Option<String>,
525    /// Whether to skip reply.
526    #[serde(default, skip_serializing_if = "Option::is_none")]
527    pub no_reply: Option<bool>,
528    /// System prompt override.
529    #[serde(default, skip_serializing_if = "Option::is_none")]
530    pub system: Option<String>,
531    /// Message variant.
532    #[serde(default, skip_serializing_if = "Option::is_none")]
533    pub variant: Option<String>,
534}
535
536impl PromptRequest {
537    /// Build a prompt request with a single text part.
538    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    /// Set model provider and model IDs for this prompt.
556    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    /// Set a system prompt override.
569    pub fn with_system(mut self, system: impl Into<String>) -> Self {
570        self.system = Some(system.into());
571        self
572    }
573
574    /// Set the agent for this prompt.
575    pub fn with_agent(mut self, agent: impl Into<String>) -> Self {
576        self.agent = Some(agent.into());
577        self
578    }
579}
580
581/// A content part in a prompt request.
582#[derive(Debug, Clone, Serialize, Deserialize)]
583#[serde(tag = "type", rename_all = "kebab-case")]
584pub enum PromptPart {
585    /// Text content.
586    Text {
587        /// Text content.
588        text: String,
589        /// Whether this is synthetic.
590        #[serde(default, skip_serializing_if = "Option::is_none")]
591        synthetic: Option<bool>,
592        /// Whether this part is ignored.
593        #[serde(default, skip_serializing_if = "Option::is_none")]
594        ignored: Option<bool>,
595        /// Additional metadata.
596        #[serde(default, skip_serializing_if = "Option::is_none")]
597        metadata: Option<serde_json::Value>,
598    },
599    /// File attachment.
600    File {
601        /// MIME type.
602        mime: String,
603        /// File URL.
604        url: String,
605        /// Original filename.
606        #[serde(default, skip_serializing_if = "Option::is_none")]
607        filename: Option<String>,
608    },
609    /// Agent delegation.
610    Agent {
611        /// Agent name.
612        name: String,
613    },
614    /// Subtask delegation.
615    Subtask {
616        /// Subtask prompt.
617        prompt: String,
618        /// Subtask description.
619        description: String,
620        /// Agent to handle subtask.
621        agent: String,
622        /// Optional command.
623        #[serde(default, skip_serializing_if = "Option::is_none")]
624        command: Option<String>,
625    },
626}
627
628/// Request to execute a command in a session.
629#[derive(Debug, Clone, Serialize, Deserialize)]
630#[serde(rename_all = "camelCase")]
631pub struct CommandRequest {
632    /// Command to execute.
633    pub command: String,
634    /// Command arguments.
635    #[serde(default, skip_serializing_if = "Option::is_none")]
636    pub args: Option<serde_json::Value>,
637}
638
639/// Request to execute a shell command in a session.
640#[derive(Debug, Clone, Serialize, Deserialize)]
641#[serde(rename_all = "camelCase")]
642pub struct ShellRequest {
643    /// Shell command to execute.
644    pub command: String,
645    /// Model to use.
646    #[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    // ==================== ToolState Tests ====================
690
691    #[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    // ==================== FilePartSource Tests ====================
759
760    #[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}