Skip to main content

opendev_models/
message.rs

1//! Chat message models.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use strum::{Display, EnumString};
7
8/// Message role enum.
9#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, Display, EnumString)]
10#[serde(rename_all = "lowercase")]
11#[strum(serialize_all = "lowercase")]
12pub enum Role {
13    User,
14    Assistant,
15    System,
16}
17
18/// Provenance kind for tracking message origins.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Display, EnumString)]
20#[serde(rename_all = "snake_case")]
21#[strum(serialize_all = "snake_case")]
22pub enum ProvenanceKind {
23    /// Message from a real user via a channel.
24    ExternalUser,
25    /// Message forwarded from another session.
26    InterSession,
27    /// Message generated by the system itself.
28    InternalSystem,
29}
30
31/// Tracks the origin of a message to prevent loops and maintain context.
32#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct InputProvenance {
34    pub kind: ProvenanceKind,
35    /// Channel the message originated from.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub source_channel: Option<String>,
38    /// Session ID if forwarded from another session.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub source_session_id: Option<String>,
41    #[serde(default = "Utc::now", with = "crate::datetime_compat")]
42    pub timestamp: DateTime<Utc>,
43}
44
45/// Tool call information.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct ToolCall {
48    pub id: String,
49    pub name: String,
50    pub parameters: HashMap<String, serde_json::Value>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub result: Option<serde_json::Value>,
53    /// Concise 1-2 line summary for LLM context.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub result_summary: Option<String>,
56    #[serde(default = "Utc::now", with = "crate::datetime_compat")]
57    pub timestamp: DateTime<Utc>,
58    #[serde(default)]
59    pub approved: bool,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub error: Option<String>,
62    /// Nested tool calls for subagent tools.
63    #[serde(default)]
64    pub nested_tool_calls: Vec<ToolCall>,
65}
66
67/// Represents a single message in the conversation.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ChatMessage {
70    pub role: Role,
71    pub content: String,
72    #[serde(default = "Utc::now", with = "crate::datetime_compat")]
73    pub timestamp: DateTime<Utc>,
74    #[serde(default)]
75    pub metadata: HashMap<String, serde_json::Value>,
76    #[serde(default)]
77    pub tool_calls: Vec<ToolCall>,
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub tokens: Option<u64>,
80
81    // Fields for complete session persistence
82    /// Thinking/reasoning used for this response.
83    #[serde(skip_serializing_if = "Option::is_none")]
84    pub thinking_trace: Option<String>,
85    /// Native model reasoning (o1/o3).
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub reasoning_content: Option<String>,
88    /// Token usage stats (may contain nested dicts).
89    #[serde(skip_serializing_if = "Option::is_none")]
90    pub token_usage: Option<HashMap<String, serde_json::Value>>,
91
92    /// Input provenance tracking (multi-channel architecture).
93    #[serde(skip_serializing_if = "Option::is_none")]
94    pub provenance: Option<InputProvenance>,
95}
96
97impl ChatMessage {
98    /// Estimate token count (rough approximation).
99    pub fn token_estimate(&self) -> u64 {
100        if let Some(tokens) = self.tokens {
101            return tokens;
102        }
103        Self::compute_token_estimate(&self.content, &self.tool_calls)
104    }
105
106    /// Estimate token count and cache the result in `self.tokens`.
107    ///
108    /// On first call (when `tokens` is `None`), computes the estimate and
109    /// stores it so subsequent calls skip recomputation.  Returns the
110    /// (possibly newly cached) value.
111    pub fn cache_token_estimate(&mut self) -> u64 {
112        if let Some(tokens) = self.tokens {
113            return tokens;
114        }
115        let estimate = Self::compute_token_estimate(&self.content, &self.tool_calls);
116        self.tokens = Some(estimate);
117        estimate
118    }
119
120    /// Compute token estimate from content and tool calls (no caching).
121    fn compute_token_estimate(content: &str, tool_calls: &[ToolCall]) -> u64 {
122        // Rough estimate: ~4 chars per token
123        let content_tokens = content.len() as u64 / 4;
124        let tool_tokens: u64 = tool_calls
125            .iter()
126            .map(|tc| {
127                let params_str = serde_json::to_string(&tc.parameters).unwrap_or_default();
128                params_str.len() as u64 / 4
129            })
130            .sum();
131        content_tokens + tool_tokens
132    }
133}
134
135#[cfg(test)]
136#[path = "message_tests.rs"]
137mod tests;