Skip to main content

honcho_ai/types/
message.rs

1//! Message-related types for the Honcho API.
2
3use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7
8pub use super::common::ReasoningConfiguration;
9
10/// Raw API response for a message.
11///
12/// Represents a single message created by a peer within a session.
13/// Use the top-level [`crate::Message`] wrapper for the enriched type.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
15#[non_exhaustive]
16pub struct MessageResponse {
17    /// Unique message identifier.
18    pub id: String,
19    /// Message content text.
20    pub content: String,
21    /// ID of the peer that authored this message.
22    pub peer_id: String,
23    /// ID of the session this message belongs to.
24    pub session_id: String,
25    /// Arbitrary key-value metadata attached to the message.
26    #[serde(default)]
27    pub metadata: HashMap<String, serde_json::Value>,
28    /// Timestamp when the message was created.
29    pub created_at: DateTime<Utc>,
30    /// ID of the workspace this message belongs to.
31    pub workspace_id: String,
32    /// Token count for the message content.
33    #[serde(default)]
34    pub token_count: u64,
35}
36
37/// Parameters for creating a single message.
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, bon::Builder)]
39#[builder(on(String, into))]
40#[builder(finish_fn = build)]
41#[non_exhaustive]
42pub struct MessageCreate {
43    /// Message content text (max 25 000 characters, server-validated).
44    pub content: String,
45    /// ID of the peer authoring the message.
46    pub peer_id: String,
47    /// Optional arbitrary metadata.
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub metadata: Option<HashMap<String, serde_json::Value>>,
50    /// Optional message-level configuration.
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub configuration: Option<MessageConfiguration>,
53    /// Optional override for the creation timestamp.
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub created_at: Option<DateTime<Utc>>,
56}
57
58/// Parameters for batch-creating messages (1–100).
59#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, bon::Builder)]
60#[builder(finish_fn = build)]
61#[non_exhaustive]
62pub struct MessageBatchCreate {
63    /// List of messages to create.
64    pub messages: Vec<MessageCreate>,
65}
66
67/// Parameters for updating a message.
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default, bon::Builder)]
69#[builder(on(String, into))]
70#[builder(finish_fn = build)]
71#[non_exhaustive]
72pub struct MessageUpdate {
73    /// Updated metadata (replaces existing).
74    #[serde(skip_serializing_if = "Option::is_none")]
75    pub metadata: Option<HashMap<String, serde_json::Value>>,
76}
77
78/// Request body for setting message metadata.
79///
80/// Crate-internal: only ever constructed in-crate (see `Session::update_message`)
81/// and not re-exported at the crate root. It carries no `Deserialize` impl, so it
82/// cannot be used as a deserialization target out-of-crate either — hence
83/// `pub(crate)` with no `#[non_exhaustive]` (that attribute only governs a
84/// cross-crate contract).
85#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
86pub(crate) struct MessageMetadataSet {
87    /// Metadata to set.
88    pub metadata: HashMap<String, serde_json::Value>,
89}
90
91/// Configuration that can be attached to a message.
92///
93/// All fields optional; message-level config overrides session and workspace config.
94#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
95#[non_exhaustive]
96pub struct MessageConfiguration {
97    /// Reasoning configuration for this message.
98    #[serde(default, skip_serializing_if = "Option::is_none")]
99    pub reasoning: Option<ReasoningConfiguration>,
100}
101
102/// Parameters for searching messages.
103#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, bon::Builder)]
104#[builder(on(String, into))]
105#[builder(finish_fn = build)]
106#[non_exhaustive]
107pub struct MessageSearchOptions {
108    /// Search query string.
109    pub query: String,
110    /// Optional filters to scope the search.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub filters: Option<HashMap<String, serde_json::Value>>,
113    /// Maximum number of results (1–100, default 10, server-validated).
114    #[serde(default = "default_limit")]
115    #[builder(default = default_limit())]
116    pub limit: u32,
117}
118
119const fn default_limit() -> u32 {
120    MessageSearchOptions::DEFAULT_LIMIT
121}
122
123impl MessageSearchOptions {
124    const DEFAULT_LIMIT: u32 = 10;
125}
126
127/// Paginated response of [`MessageResponse`] items.
128pub type MessagePage = super::pagination::Page<MessageResponse>;
129
130#[cfg(test)]
131#[allow(
132    clippy::unwrap_used,
133    clippy::expect_used,
134    clippy::panic,
135    clippy::unnecessary_wraps,
136    clippy::needless_pass_by_value,
137    clippy::unused_async
138)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn message_search_options_default_limit() {
144        let opts: MessageSearchOptions = serde_json::from_str(r#"{"query":"hello"}"#).unwrap();
145        assert_eq!(opts.limit, 10);
146    }
147
148    #[test]
149    fn message_batch_create_builder_constructs_from_messages() {
150        // The builder makes out-of-crate construction possible, so `#[non_exhaustive]`
151        // no longer "lies": evolution stays non-breaking while construction stays viable.
152        let msg = MessageCreate::builder()
153            .content("hello")
154            .peer_id("peer_01")
155            .build();
156        let batch = MessageBatchCreate::builder().messages(vec![msg]).build();
157        assert_eq!(batch.messages.len(), 1);
158    }
159
160    #[test]
161    fn message_response_token_count_defaults_to_zero_when_missing() {
162        // Older servers may omit token_count; serde(default) must yield 0.
163        let json = r#"{
164            "id": "msg_01",
165            "content": "hello",
166            "peer_id": "peer_01",
167            "session_id": "sess_01",
168            "workspace_id": "ws_01",
169            "created_at": "2024-01-01T00:00:00Z"
170        }"#;
171        let msg: MessageResponse = serde_json::from_str(json).unwrap();
172        assert_eq!(msg.token_count, 0);
173    }
174}