Skip to main content

everruns_core/
message_retriever.rs

1// MessageRetriever - Retrieval-only trait for conversation messages
2//
3// Design decision: MessageRetriever is retrieval-only.
4// Messages are stored via EventEmitter (input.message, output.message.completed events).
5// This trait provides read access for building LLM context.
6
7use async_trait::async_trait;
8
9use crate::error::Result;
10use crate::message::{ContentPart, Controls, Message, MessageRole};
11use crate::message_filter::MessageQuery;
12use crate::typed_id::{MessageId, SessionId};
13
14#[derive(Debug, Clone)]
15pub struct MessageHistory {
16    pub messages: Vec<Message>,
17    /// Highest persisted message-event sequence visible when the load ran.
18    pub source_sequence: Option<i64>,
19}
20
21// ============================================================================
22// InputMessage - Input structure for message creation
23// ============================================================================
24
25/// Input message for creating a new message
26///
27/// This is the input structure for adding messages, without the ID and timestamp
28/// which are generated by the storage layer.
29///
30/// Note: Message creation happens via EventService/EventEmitter, not MessageRetriever.
31/// This struct is used by the API layer and in-memory stores for tests.
32#[derive(Debug, Clone)]
33pub struct InputMessage {
34    /// Message role (user, assistant, tool_result, system)
35    pub role: MessageRole,
36    /// Message content as array of content parts
37    pub content: Vec<ContentPart>,
38    /// Runtime controls (model, reasoning, etc.)
39    pub controls: Option<Controls>,
40    /// Message-level metadata
41    pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
42    /// Tags for filtering/categorization
43    pub tags: Vec<String>,
44}
45
46impl InputMessage {
47    /// Create a new user input message with text content
48    pub fn user(content: impl Into<String>) -> Self {
49        Self {
50            role: MessageRole::User,
51            content: vec![ContentPart::text(content)],
52            controls: None,
53            metadata: None,
54            tags: vec![],
55        }
56    }
57
58    /// Create from a Message (useful for storing existing messages)
59    pub fn from_message(msg: &Message) -> Self {
60        Self {
61            role: msg.role.clone(),
62            content: msg.content.clone(),
63            controls: msg.controls.clone(),
64            metadata: msg.metadata.clone(),
65            tags: vec![],
66        }
67    }
68}
69
70impl From<&str> for InputMessage {
71    fn from(text: &str) -> Self {
72        InputMessage::user(text)
73    }
74}
75
76impl From<String> for InputMessage {
77    fn from(text: String) -> Self {
78        InputMessage::user(text)
79    }
80}
81
82// ============================================================================
83// MessageRetriever trait
84// ============================================================================
85
86/// Trait for retrieving conversation messages
87///
88/// This trait provides read-only access to conversation history for building
89/// LLM context. Message storage is handled separately via EventEmitter
90/// (messages are stored as events: input.message, output.message.completed).
91///
92/// Implementations can:
93/// - Load messages from a database (reconstructing from events)
94/// - Keep messages in memory for testing
95/// - Load messages via gRPC from control-plane
96#[async_trait]
97pub trait MessageRetriever: Send + Sync {
98    /// Get a specific message by ID
99    async fn get(&self, session_id: SessionId, message_id: MessageId) -> Result<Option<Message>>;
100
101    /// Load all messages for a session
102    async fn load(&self, session_id: SessionId) -> Result<Vec<Message>>;
103
104    /// Load messages with filters and injections applied.
105    ///
106    /// This method supports the composable filter system where capabilities
107    /// can contribute filters that modify how messages are loaded.
108    ///
109    /// Default implementation calls `load()` and ignores the query filters,
110    /// maintaining backward compatibility for implementations that don't
111    /// support filtering.
112    async fn load_filtered(&self, query: MessageQuery) -> Result<Vec<Message>> {
113        // Default: load all messages for the session, ignoring filters
114        // Implementations should override this to support filtering
115        self.load(query.session_id).await
116    }
117
118    async fn load_filtered_history(&self, query: MessageQuery) -> Result<MessageHistory> {
119        Ok(MessageHistory {
120            messages: self.load_filtered(query).await?,
121            source_sequence: None,
122        })
123    }
124
125    /// Load messages with pagination
126    async fn load_page(
127        &self,
128        session_id: SessionId,
129        offset: usize,
130        limit: usize,
131    ) -> Result<Vec<Message>> {
132        let all = self.load(session_id).await?;
133        Ok(all.into_iter().skip(offset).take(limit).collect())
134    }
135
136    /// Count messages in a session
137    async fn count(&self, session_id: SessionId) -> Result<usize> {
138        Ok(self.load(session_id).await?.len())
139    }
140}
141
142#[async_trait]
143impl<T: MessageRetriever + ?Sized> MessageRetriever for std::sync::Arc<T> {
144    async fn get(&self, session_id: SessionId, message_id: MessageId) -> Result<Option<Message>> {
145        (**self).get(session_id, message_id).await
146    }
147
148    async fn load(&self, session_id: SessionId) -> Result<Vec<Message>> {
149        (**self).load(session_id).await
150    }
151
152    async fn load_filtered(&self, query: MessageQuery) -> Result<Vec<Message>> {
153        (**self).load_filtered(query).await
154    }
155
156    async fn load_filtered_history(&self, query: MessageQuery) -> Result<MessageHistory> {
157        (**self).load_filtered_history(query).await
158    }
159
160    async fn load_page(
161        &self,
162        session_id: SessionId,
163        offset: usize,
164        limit: usize,
165    ) -> Result<Vec<Message>> {
166        (**self).load_page(session_id, offset, limit).await
167    }
168
169    async fn count(&self, session_id: SessionId) -> Result<usize> {
170        (**self).count(session_id).await
171    }
172}