Skip to main content

starweaver_model/message/
history.rs

1//! Canonical model message history items.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use serde_json::Map;
6use starweaver_core::{ConversationId, RunId};
7use starweaver_usage::Usage;
8
9use super::{
10    ContentPart, FinishReason, Metadata, ModelRequestPart, ModelResponsePart, ProviderInfo,
11    ToolCallPart,
12};
13
14/// Provider-neutral model history item.
15#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
16#[serde(tag = "kind", rename_all = "snake_case")]
17pub enum ModelMessage {
18    /// A request sent to a model.
19    Request(ModelRequest),
20    /// A response returned by a model.
21    Response(ModelResponse),
22}
23
24/// Request item in canonical model history.
25#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
26pub struct ModelRequest {
27    /// Request parts sent in one model turn.
28    pub parts: Vec<ModelRequestPart>,
29    /// Creation timestamp.
30    #[serde(default, skip_serializing_if = "Option::is_none")]
31    pub timestamp: Option<DateTime<Utc>>,
32    /// Optional request-level instructions for the current model call.
33    ///
34    /// Provider mappers apply request-level instructions from the current instruction-bearing
35    /// request, using Starweaver's request-level instruction behavior. Use
36    /// `ModelRequestPart::SystemPrompt` for durable static system prompts, and use
37    /// `ModelRequestPart::Instruction` metadata for structured instruction parts that need
38    /// origin or dynamic-cache-boundary information, such as dynamic agent or toolset guidance.
39    #[serde(default, skip_serializing_if = "Option::is_none")]
40    pub instructions: Option<String>,
41    /// Run identifier.
42    #[serde(default, skip_serializing_if = "Option::is_none")]
43    pub run_id: Option<RunId>,
44    /// Conversation identifier.
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub conversation_id: Option<ConversationId>,
47    /// Application metadata.
48    #[serde(default, skip_serializing_if = "Map::is_empty")]
49    pub metadata: Metadata,
50}
51
52impl ModelRequest {
53    /// Build a user request from text.
54    #[must_use]
55    pub fn user_text(text: impl Into<String>) -> Self {
56        Self {
57            parts: vec![ModelRequestPart::UserPrompt {
58                content: vec![ContentPart::Text { text: text.into() }],
59                name: None,
60                metadata: Metadata::default(),
61            }],
62            timestamp: None,
63            instructions: None,
64            run_id: None,
65            conversation_id: None,
66            metadata: Metadata::default(),
67        }
68    }
69}
70
71/// Response item in canonical model history.
72#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
73pub struct ModelResponse {
74    /// Response parts returned by the model.
75    pub parts: Vec<ModelResponsePart>,
76    /// Token and request usage.
77    #[serde(default)]
78    pub usage: Usage,
79    /// Actual provider model name where known.
80    #[serde(default, skip_serializing_if = "Option::is_none")]
81    pub model_name: Option<String>,
82    /// Provider metadata.
83    #[serde(default, skip_serializing_if = "Option::is_none")]
84    pub provider: Option<ProviderInfo>,
85    /// Finish reason.
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub finish_reason: Option<FinishReason>,
88    /// Response timestamp.
89    #[serde(default, skip_serializing_if = "Option::is_none")]
90    pub timestamp: Option<DateTime<Utc>>,
91    /// Run identifier.
92    #[serde(default, skip_serializing_if = "Option::is_none")]
93    pub run_id: Option<RunId>,
94    /// Conversation identifier.
95    #[serde(default, skip_serializing_if = "Option::is_none")]
96    pub conversation_id: Option<ConversationId>,
97    /// Application metadata.
98    #[serde(default, skip_serializing_if = "Map::is_empty")]
99    pub metadata: Metadata,
100}
101
102impl ModelResponse {
103    /// Build a text response.
104    #[must_use]
105    pub fn text(text: impl Into<String>) -> Self {
106        Self {
107            parts: vec![ModelResponsePart::Text { text: text.into() }],
108            usage: Usage::default(),
109            model_name: None,
110            provider: None,
111            finish_reason: None,
112            timestamp: None,
113            run_id: None,
114            conversation_id: None,
115            metadata: Metadata::default(),
116        }
117    }
118
119    /// Concatenate all text response parts.
120    #[must_use]
121    pub fn text_output(&self) -> String {
122        self.parts
123            .iter()
124            .filter_map(ModelResponsePart::text)
125            .collect::<Vec<_>>()
126            .join("")
127    }
128
129    /// Return all provider-neutral tool calls in the response.
130    #[must_use]
131    pub fn tool_calls(&self) -> Vec<ToolCallPart> {
132        self.parts
133            .iter()
134            .filter_map(ModelResponsePart::tool_call)
135            .cloned()
136            .collect()
137    }
138}