Skip to main content

ferrin_spec/language_model/
prompt.rs

1//! Specification-level prompt: the message list sent to a language model.
2//!
3//! The core converts application messages (`ferrin-message`) into this form,
4//! downloading files the provider cannot fetch and validating structure.
5//! Adapters convert it into provider-specific request bodies.
6
7use serde::Deserialize;
8use serde::Serialize;
9
10use crate::json::JsonValue;
11use crate::shared::ApprovalId;
12use crate::shared::FileData;
13use crate::shared::MediaType;
14use crate::shared::ProviderOptions;
15use crate::shared::ToolCallId;
16use crate::shared::ToolName;
17
18use super::content::CustomKind;
19
20/// A prompt is an ordered list of messages.
21pub type Prompt = Vec<PromptMessage>;
22
23/// A message in a specification prompt, tagged by `role`.
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(tag = "role", rename_all = "lowercase")]
26#[non_exhaustive]
27pub enum PromptMessage {
28    /// System instructions.
29    System {
30        /// Instruction text.
31        content: String,
32        /// Provider-specific options for this message.
33        #[serde(default, skip_serializing_if = "Option::is_none")]
34        provider_options: Option<ProviderOptions>,
35    },
36    /// User input.
37    User {
38        /// Text and file parts.
39        content: Vec<UserPromptPart>,
40        /// Provider-specific options for this message.
41        #[serde(default, skip_serializing_if = "Option::is_none")]
42        provider_options: Option<ProviderOptions>,
43    },
44    /// Previous assistant output.
45    Assistant {
46        /// Assistant parts, including tool calls and provider-executed results.
47        content: Vec<AssistantPromptPart>,
48        /// Provider-specific options for this message.
49        #[serde(default, skip_serializing_if = "Option::is_none")]
50        provider_options: Option<ProviderOptions>,
51    },
52    /// Tool results and approval responses.
53    Tool {
54        /// Tool parts.
55        content: Vec<ToolPromptPart>,
56        /// Provider-specific options for this message.
57        #[serde(default, skip_serializing_if = "Option::is_none")]
58        provider_options: Option<ProviderOptions>,
59    },
60}
61
62impl PromptMessage {
63    /// Creates a system message.
64    #[must_use]
65    pub fn system(content: impl Into<String>) -> Self {
66        Self::System {
67            content: content.into(),
68            provider_options: None,
69        }
70    }
71
72    /// Creates a user message with a single text part.
73    #[must_use]
74    pub fn user_text(text: impl Into<String>) -> Self {
75        Self::User {
76            content: vec![UserPromptPart::Text(TextPart::new(text))],
77            provider_options: None,
78        }
79    }
80
81    /// Creates a user message from parts.
82    #[must_use]
83    pub fn user(content: Vec<UserPromptPart>) -> Self {
84        Self::User {
85            content,
86            provider_options: None,
87        }
88    }
89
90    /// Creates an assistant message with a single text part.
91    #[must_use]
92    pub fn assistant_text(text: impl Into<String>) -> Self {
93        Self::Assistant {
94            content: vec![AssistantPromptPart::Text(TextPart::new(text))],
95            provider_options: None,
96        }
97    }
98
99    /// Creates an assistant message from parts.
100    #[must_use]
101    pub fn assistant(content: Vec<AssistantPromptPart>) -> Self {
102        Self::Assistant {
103            content,
104            provider_options: None,
105        }
106    }
107
108    /// Creates a tool message from parts.
109    #[must_use]
110    pub fn tool(content: Vec<ToolPromptPart>) -> Self {
111        Self::Tool {
112            content,
113            provider_options: None,
114        }
115    }
116
117    /// Returns the role name (`system`, `user`, `assistant`, `tool`).
118    #[must_use]
119    pub fn role(&self) -> &'static str {
120        match self {
121            Self::System { .. } => "system",
122            Self::User { .. } => "user",
123            Self::Assistant { .. } => "assistant",
124            Self::Tool { .. } => "tool",
125        }
126    }
127
128    /// Returns the provider options attached to the message.
129    #[must_use]
130    pub fn provider_options(&self) -> Option<&ProviderOptions> {
131        match self {
132            Self::System {
133                provider_options, ..
134            }
135            | Self::User {
136                provider_options, ..
137            }
138            | Self::Assistant {
139                provider_options, ..
140            }
141            | Self::Tool {
142                provider_options, ..
143            } => provider_options.as_ref(),
144        }
145    }
146}
147
148/// Parts allowed in a user message.
149#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
150#[serde(tag = "type", rename_all = "kebab-case")]
151#[non_exhaustive]
152pub enum UserPromptPart {
153    /// Text.
154    Text(TextPart),
155    /// A file (image, audio, document, ...).
156    File(FilePart),
157}
158
159/// Parts allowed in an assistant message.
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
161#[serde(tag = "type", rename_all = "kebab-case")]
162#[non_exhaustive]
163pub enum AssistantPromptPart {
164    /// Text.
165    Text(TextPart),
166    /// A generated file.
167    File(FilePart),
168    /// Reasoning text.
169    Reasoning(ReasoningPart),
170    /// A reasoning artifact stored as a file.
171    ReasoningFile(ReasoningFilePart),
172    /// Provider-specific content identified by kind.
173    Custom(CustomPart),
174    /// A tool call issued by the model.
175    ToolCall(ToolCallPart),
176    /// A provider-executed tool result.
177    ToolResult(ToolResultPart),
178}
179
180/// Parts allowed in a tool message.
181#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
182#[serde(tag = "type", rename_all = "kebab-case")]
183#[non_exhaustive]
184pub enum ToolPromptPart {
185    /// Result of a client-executed tool call.
186    ToolResult(ToolResultPart),
187    /// Response to a tool approval request.
188    ToolApprovalResponse(ToolApprovalResponsePart),
189}
190
191/// Text content.
192#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
193pub struct TextPart {
194    /// The text.
195    pub text: String,
196    /// Provider-specific options for this part.
197    #[serde(default, skip_serializing_if = "Option::is_none")]
198    pub provider_options: Option<ProviderOptions>,
199}
200
201impl TextPart {
202    /// Creates a text part without provider options.
203    #[must_use]
204    pub fn new(text: impl Into<String>) -> Self {
205        Self {
206            text: text.into(),
207            provider_options: None,
208        }
209    }
210}
211
212/// Reasoning text produced by the model.
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
214pub struct ReasoningPart {
215    /// The reasoning text.
216    pub text: String,
217    /// Provider-specific options (for example signatures or encrypted state).
218    #[serde(default, skip_serializing_if = "Option::is_none")]
219    pub provider_options: Option<ProviderOptions>,
220}
221
222impl ReasoningPart {
223    /// Creates a reasoning part without provider options.
224    #[must_use]
225    pub fn new(text: impl Into<String>) -> Self {
226        Self {
227            text: text.into(),
228            provider_options: None,
229        }
230    }
231}
232
233/// A reasoning artifact stored as a file.
234#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
235pub struct ReasoningFilePart {
236    /// File payload (inline bytes or URL).
237    pub data: FileData,
238    /// Media type of the payload.
239    pub media_type: MediaType,
240    /// Provider-specific options for this part.
241    #[serde(default, skip_serializing_if = "Option::is_none")]
242    pub provider_options: Option<ProviderOptions>,
243}
244
245/// Provider-specific content identified by a `provider.type` kind.
246#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
247pub struct CustomPart {
248    /// Kind of the content, in `provider.type` form.
249    pub kind: CustomKind,
250    /// Provider-specific options carrying the actual payload.
251    #[serde(default, skip_serializing_if = "Option::is_none")]
252    pub provider_options: Option<ProviderOptions>,
253}
254
255/// A file attached to a message.
256#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
257pub struct FilePart {
258    /// File payload.
259    pub data: FileData,
260    /// Full media type (`image/png`) or top-level type (`image`).
261    pub media_type: MediaType,
262    /// Optional file name.
263    #[serde(default, skip_serializing_if = "Option::is_none")]
264    pub filename: Option<String>,
265    /// Provider-specific options for this part.
266    #[serde(default, skip_serializing_if = "Option::is_none")]
267    pub provider_options: Option<ProviderOptions>,
268}
269
270impl FilePart {
271    /// Creates a file part without name or provider options.
272    #[must_use]
273    pub fn new(data: impl Into<FileData>, media_type: impl Into<MediaType>) -> Self {
274        Self {
275            data: data.into(),
276            media_type: media_type.into(),
277            filename: None,
278            provider_options: None,
279        }
280    }
281
282    /// Sets the file name.
283    #[must_use]
284    pub fn with_filename(mut self, filename: impl Into<String>) -> Self {
285        self.filename = Some(filename.into());
286        self
287    }
288}
289
290/// A tool call issued by the model in a previous step.
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292pub struct ToolCallPart {
293    /// Identifier of the tool call.
294    pub tool_call_id: ToolCallId,
295    /// Name of the tool.
296    pub tool_name: ToolName,
297    /// Parsed tool input.
298    pub input: JsonValue,
299    /// Whether the provider executed the tool itself.
300    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
301    pub provider_executed: bool,
302    /// Provider-specific options for this part.
303    #[serde(default, skip_serializing_if = "Option::is_none")]
304    pub provider_options: Option<ProviderOptions>,
305}
306
307/// The result of a tool call.
308#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
309pub struct ToolResultPart {
310    /// Identifier of the tool call this result answers.
311    pub tool_call_id: ToolCallId,
312    /// Name of the tool.
313    pub tool_name: ToolName,
314    /// The output.
315    pub output: ToolResultOutput,
316    /// Provider-specific options for this part.
317    #[serde(default, skip_serializing_if = "Option::is_none")]
318    pub provider_options: Option<ProviderOptions>,
319}
320
321/// A response to a tool approval request.
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323pub struct ToolApprovalResponsePart {
324    /// Identifier of the approval request.
325    pub approval_id: ApprovalId,
326    /// Whether execution was approved.
327    pub approved: bool,
328    /// Optional reason.
329    #[serde(default, skip_serializing_if = "Option::is_none")]
330    pub reason: Option<String>,
331    /// Provider-specific options for this part.
332    #[serde(default, skip_serializing_if = "Option::is_none")]
333    pub provider_options: Option<ProviderOptions>,
334}
335
336/// The output of a tool call as sent back to the model.
337#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
338#[serde(tag = "type", rename_all = "kebab-case")]
339#[non_exhaustive]
340pub enum ToolResultOutput {
341    /// Plain text.
342    Text {
343        /// The text.
344        value: String,
345        /// Provider-specific options.
346        #[serde(default, skip_serializing_if = "Option::is_none")]
347        provider_options: Option<ProviderOptions>,
348    },
349    /// A JSON value.
350    Json {
351        /// The value.
352        value: JsonValue,
353        /// Provider-specific options.
354        #[serde(default, skip_serializing_if = "Option::is_none")]
355        provider_options: Option<ProviderOptions>,
356    },
357    /// Execution was denied by the application or user.
358    ExecutionDenied {
359        /// Optional reason.
360        #[serde(default, skip_serializing_if = "Option::is_none")]
361        reason: Option<String>,
362        /// Provider-specific options.
363        #[serde(default, skip_serializing_if = "Option::is_none")]
364        provider_options: Option<ProviderOptions>,
365    },
366    /// The tool failed; the error is plain text.
367    ErrorText {
368        /// The error text.
369        value: String,
370        /// Provider-specific options.
371        #[serde(default, skip_serializing_if = "Option::is_none")]
372        provider_options: Option<ProviderOptions>,
373    },
374    /// The tool failed; the error is a JSON value.
375    ErrorJson {
376        /// The error value.
377        value: JsonValue,
378        /// Provider-specific options.
379        #[serde(default, skip_serializing_if = "Option::is_none")]
380        provider_options: Option<ProviderOptions>,
381    },
382    /// Multi-part content (text and files).
383    Content {
384        /// The parts.
385        value: Vec<ToolResultContentPart>,
386    },
387}
388
389impl ToolResultOutput {
390    /// Creates a text output.
391    #[must_use]
392    pub fn text(value: impl Into<String>) -> Self {
393        Self::Text {
394            value: value.into(),
395            provider_options: None,
396        }
397    }
398
399    /// Creates a JSON output.
400    #[must_use]
401    pub fn json(value: JsonValue) -> Self {
402        Self::Json {
403            value,
404            provider_options: None,
405        }
406    }
407
408    /// Creates an error-text output.
409    #[must_use]
410    pub fn error_text(value: impl Into<String>) -> Self {
411        Self::ErrorText {
412            value: value.into(),
413            provider_options: None,
414        }
415    }
416
417    /// Creates an error-JSON output.
418    #[must_use]
419    pub fn error_json(value: JsonValue) -> Self {
420        Self::ErrorJson {
421            value,
422            provider_options: None,
423        }
424    }
425
426    /// Creates an execution-denied output.
427    #[must_use]
428    pub fn execution_denied(reason: Option<String>) -> Self {
429        Self::ExecutionDenied {
430            reason,
431            provider_options: None,
432        }
433    }
434
435    /// Returns `true` for the error variants (`error-text`, `error-json`).
436    #[must_use]
437    pub fn is_error(&self) -> bool {
438        matches!(self, Self::ErrorText { .. } | Self::ErrorJson { .. })
439    }
440}
441
442/// A part of a multi-part tool result.
443#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
444#[serde(tag = "type", rename_all = "kebab-case")]
445#[non_exhaustive]
446pub enum ToolResultContentPart {
447    /// Text.
448    Text {
449        /// The text.
450        text: String,
451        /// Provider-specific options.
452        #[serde(default, skip_serializing_if = "Option::is_none")]
453        provider_options: Option<ProviderOptions>,
454    },
455    /// A file.
456    File {
457        /// File payload.
458        data: FileData,
459        /// Media type of the payload.
460        media_type: MediaType,
461        /// Optional file name.
462        #[serde(default, skip_serializing_if = "Option::is_none")]
463        filename: Option<String>,
464        /// Provider-specific options.
465        #[serde(default, skip_serializing_if = "Option::is_none")]
466        provider_options: Option<ProviderOptions>,
467    },
468    /// Provider-specific content carried entirely in provider options.
469    Custom {
470        /// Provider-specific options.
471        #[serde(default, skip_serializing_if = "Option::is_none")]
472        provider_options: Option<ProviderOptions>,
473    },
474}