Skip to main content

starweaver_session/
input.rs

1//! Versioned durable input part records.
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5use starweaver_core::Metadata;
6use starweaver_model::{CachePointTtl, ContentPart};
7use thiserror::Error;
8
9/// Provider-scoped legacy file reference submitted as session input.
10#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
11pub struct FileRef {
12    /// File URI or provider reference.
13    pub uri: String,
14    /// Optional media type.
15    #[serde(default, skip_serializing_if = "Option::is_none")]
16    pub media_type: Option<String>,
17    /// Optional display name.
18    #[serde(default, skip_serializing_if = "Option::is_none")]
19    pub name: Option<String>,
20}
21
22impl FileRef {
23    /// Build a file reference.
24    #[must_use]
25    pub fn new(uri: impl Into<String>) -> Self {
26        Self {
27            uri: uri.into(),
28            media_type: None,
29            name: None,
30        }
31    }
32}
33
34/// Binary resource legacy reference submitted as session input.
35#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
36pub struct BinaryRef {
37    /// Resource URI, object key, or upload token.
38    pub uri: String,
39    /// Optional media type.
40    #[serde(default, skip_serializing_if = "Option::is_none")]
41    pub media_type: Option<String>,
42    /// Optional byte length.
43    #[serde(default, skip_serializing_if = "Option::is_none")]
44    pub bytes: Option<u64>,
45}
46
47impl BinaryRef {
48    /// Build a binary reference.
49    #[must_use]
50    pub fn new(uri: impl Into<String>) -> Self {
51        Self {
52            uri: uri.into(),
53            media_type: None,
54            bytes: None,
55        }
56    }
57}
58
59/// Serializable run input submitted by SDKs and product hosts.
60///
61/// Every canonical model [`ContentPart`] has an explicit durable variant. The
62/// legacy `url`, `file`, `binary`, `mode`, and `command` variants remain readable
63/// for previous-release evidence but are not used as a content escape hatch.
64#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
65#[serde(tag = "kind", rename_all = "snake_case")]
66pub enum InputPart {
67    /// Prompt-cache boundary after the preceding content part.
68    CachePoint {
69        /// Optional provider-compatible cache lifetime.
70        #[serde(default, skip_serializing_if = "Option::is_none")]
71        ttl: Option<CachePointTtl>,
72        /// Application metadata.
73        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
74        metadata: Metadata,
75    },
76    /// Natural language prompt text.
77    Text {
78        /// Text content.
79        text: String,
80        /// Application metadata.
81        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
82        metadata: Metadata,
83    },
84    /// Image URL.
85    ImageUrl {
86        /// Image URL.
87        url: String,
88        /// Application metadata.
89        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
90        metadata: Metadata,
91    },
92    /// Generic file URL with an explicit media type.
93    FileUrl {
94        /// File URL.
95        url: String,
96        /// File media type.
97        media_type: String,
98        /// Application metadata.
99        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
100        metadata: Metadata,
101    },
102    /// Inline binary content.
103    InlineBinary {
104        /// Binary bytes.
105        data: Vec<u8>,
106        /// Declared media type.
107        media_type: String,
108        /// Application metadata.
109        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
110        metadata: Metadata,
111    },
112    /// Resource-backed content reference.
113    ResourceRef {
114        /// Resource URI.
115        uri: String,
116        /// Resource media type.
117        media_type: String,
118        /// Resource type such as `image`, `video`, or `document`.
119        resource_type: String,
120        /// Resource metadata preserved for model preparation.
121        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
122        resource_metadata: Metadata,
123        /// Application metadata that is not model-visible by default.
124        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
125        metadata: Metadata,
126    },
127    /// Inline data URL.
128    DataUrl {
129        /// Data URL payload.
130        data_url: String,
131        /// Media type carried by the data URL.
132        media_type: String,
133        /// Application metadata.
134        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
135        metadata: Metadata,
136    },
137    /// Legacy generic URL reference. New content writers use `image_url` or `file_url`.
138    Url {
139        /// URL string.
140        url: String,
141        /// Application metadata.
142        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
143        metadata: Metadata,
144    },
145    /// Legacy provider-scoped file reference.
146    File {
147        /// File reference.
148        file: FileRef,
149        /// Application metadata.
150        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
151        metadata: Metadata,
152    },
153    /// Legacy binary resource reference. New inline bytes use `inline_binary`.
154    Binary {
155        /// Binary reference.
156        binary: BinaryRef,
157        /// Application metadata.
158        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
159        metadata: Metadata,
160    },
161    /// Legacy product mode or planning hint.
162    Mode {
163        /// Mode name.
164        mode: String,
165        /// Optional structured configuration.
166        #[serde(default, skip_serializing_if = "Value::is_null")]
167        config: Value,
168        /// Application metadata.
169        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
170        metadata: Metadata,
171    },
172    /// Legacy slash-command or product command evidence.
173    Command {
174        /// Command name.
175        command: String,
176        /// Command arguments.
177        #[serde(default, skip_serializing_if = "Vec::is_empty")]
178        args: Vec<String>,
179        /// Optional structured payload.
180        #[serde(default, skip_serializing_if = "Value::is_null")]
181        payload: Value,
182        /// Application metadata.
183        #[serde(default, skip_serializing_if = "Metadata::is_empty")]
184        metadata: Metadata,
185    },
186}
187
188impl InputPart {
189    /// Build text input.
190    #[must_use]
191    pub fn text(text: impl Into<String>) -> Self {
192        Self::Text {
193            text: text.into(),
194            metadata: Metadata::default(),
195        }
196    }
197
198    /// Build an explicit image URL input.
199    #[must_use]
200    pub fn image_url(url: impl Into<String>) -> Self {
201        Self::ImageUrl {
202            url: url.into(),
203            metadata: Metadata::default(),
204        }
205    }
206
207    /// Build a legacy generic URL input.
208    #[must_use]
209    pub fn url(url: impl Into<String>) -> Self {
210        Self::Url {
211            url: url.into(),
212            metadata: Metadata::default(),
213        }
214    }
215
216    /// Build legacy command evidence.
217    #[must_use]
218    pub fn command(command: impl Into<String>, args: Vec<String>) -> Self {
219        Self::Command {
220            command: command.into(),
221            args,
222            payload: Value::Null,
223            metadata: Metadata::default(),
224        }
225    }
226
227    /// Return whether this part is a previous-release product-edge input.
228    #[must_use]
229    pub const fn is_legacy_product_input(&self) -> bool {
230        matches!(self, Self::Mode { .. } | Self::Command { .. })
231    }
232}
233
234impl From<ContentPart> for InputPart {
235    fn from(part: ContentPart) -> Self {
236        let metadata = Metadata::default();
237        match part {
238            ContentPart::CachePoint { ttl } => Self::CachePoint { ttl, metadata },
239            ContentPart::Text { text } => Self::Text { text, metadata },
240            ContentPart::ImageUrl { url } => Self::ImageUrl { url, metadata },
241            ContentPart::FileUrl { url, media_type } => Self::FileUrl {
242                url,
243                media_type,
244                metadata,
245            },
246            ContentPart::Binary { data, media_type } => Self::InlineBinary {
247                data,
248                media_type,
249                metadata,
250            },
251            ContentPart::ResourceRef {
252                uri,
253                media_type,
254                resource_type,
255                metadata: resource_metadata,
256            } => Self::ResourceRef {
257                uri,
258                media_type,
259                resource_type,
260                resource_metadata,
261                metadata,
262            },
263            ContentPart::DataUrl {
264                data_url,
265                media_type,
266            } => Self::DataUrl {
267                data_url,
268                media_type,
269                metadata,
270            },
271        }
272    }
273}
274
275impl TryFrom<InputPart> for ContentPart {
276    type Error = InputConversionError;
277
278    fn try_from(part: InputPart) -> Result<Self, Self::Error> {
279        match part {
280            InputPart::CachePoint { ttl, .. } => Ok(Self::CachePoint { ttl }),
281            InputPart::Text { text, .. } => Ok(Self::Text { text }),
282            InputPart::ImageUrl { url, .. } | InputPart::Url { url, .. } => {
283                Ok(Self::ImageUrl { url })
284            }
285            InputPart::FileUrl {
286                url, media_type, ..
287            } => Ok(Self::FileUrl { url, media_type }),
288            InputPart::InlineBinary {
289                data, media_type, ..
290            } => Ok(Self::Binary { data, media_type }),
291            InputPart::ResourceRef {
292                uri,
293                media_type,
294                resource_type,
295                resource_metadata,
296                ..
297            } => Ok(Self::ResourceRef {
298                uri,
299                media_type,
300                resource_type,
301                metadata: resource_metadata,
302            }),
303            InputPart::DataUrl {
304                data_url,
305                media_type,
306                ..
307            } => Ok(Self::DataUrl {
308                data_url,
309                media_type,
310            }),
311            InputPart::File { file, .. } => {
312                let mut metadata = Metadata::default();
313                if let Some(name) = file.name {
314                    metadata.insert("name".to_string(), Value::String(name));
315                }
316                Ok(Self::ResourceRef {
317                    uri: file.uri,
318                    media_type: file
319                        .media_type
320                        .unwrap_or_else(|| "application/octet-stream".to_string()),
321                    resource_type: "file".to_string(),
322                    metadata,
323                })
324            }
325            InputPart::Binary { binary, .. } => {
326                let mut metadata = Metadata::default();
327                if let Some(bytes) = binary.bytes {
328                    metadata.insert("bytes".to_string(), Value::from(bytes));
329                }
330                Ok(Self::ResourceRef {
331                    uri: binary.uri,
332                    media_type: binary
333                        .media_type
334                        .unwrap_or_else(|| "application/octet-stream".to_string()),
335                    resource_type: "binary".to_string(),
336                    metadata,
337                })
338            }
339            InputPart::Mode { mode, .. } => Err(InputConversionError::ProductMode(mode)),
340            InputPart::Command { command, .. } => {
341                Err(InputConversionError::ProductCommand(command))
342            }
343        }
344    }
345}
346
347/// Failure converting legacy durable input into canonical model content.
348#[derive(Debug, Error)]
349pub enum InputConversionError {
350    /// Previous content-part escape hatch contains invalid content JSON.
351    #[error("legacy content_part input is invalid: {0}")]
352    LegacyContent(serde_json::Error),
353    /// Product mode must be handled before runtime input conversion.
354    #[error("product mode {0} is not model content")]
355    ProductMode(String),
356    /// Product command must be handled before runtime input conversion.
357    #[error("product command {0} is not model content")]
358    ProductCommand(String),
359}