agent_memory/
record.rs

1//! Shared record types for the memory subsystem.
2
3use std::time::SystemTime;
4
5use bytes::Bytes;
6use serde::{Deserialize, Serialize};
7use serde_json::{Map, Value};
8use uuid::Uuid;
9
10use crate::embeddings::EmbeddingVector;
11use crate::{MemoryError, MemoryResult};
12
13/// Channel categorising a memory entry.
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16pub enum MemoryChannel {
17    /// Messages originating from outside the agent (e.g. MXP Call payloads).
18    Input,
19    /// Messages produced by the agent (responses to MXP calls).
20    Output,
21    /// Tool invocation results or intermediate tool state.
22    Tool,
23    /// Internal agent/runtime events (checkpoints, policy results, etc.).
24    System,
25    /// Custom channel tagged by implementers for domain-specific routing.
26    Custom(String),
27}
28
29impl MemoryChannel {
30    /// Creates a [`MemoryChannel::Custom`] value after validating the provided name.
31    ///
32    /// # Errors
33    ///
34    /// Returns [`MemoryError::InvalidRecord`] when the supplied label is empty.
35    pub fn custom(label: impl Into<String>) -> MemoryResult<Self> {
36        let value = label.into();
37        if value.trim().is_empty() {
38            return Err(MemoryError::InvalidRecord(
39                "custom memory channel label must not be empty",
40            ));
41        }
42        Ok(Self::Custom(value))
43    }
44}
45
46/// Describes a single captured piece of memory.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct MemoryRecord {
49    id: Uuid,
50    timestamp: SystemTime,
51    channel: MemoryChannel,
52    payload: Bytes,
53    #[serde(default)]
54    tags: Vec<String>,
55    #[serde(default)]
56    metadata: Map<String, Value>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    embedding: Option<EmbeddingVector>,
59}
60
61impl MemoryRecord {
62    /// Creates a builder for a new memory record.
63    #[must_use]
64    pub fn builder(channel: MemoryChannel, payload: Bytes) -> MemoryRecordBuilder {
65        MemoryRecordBuilder {
66            id: Uuid::new_v4(),
67            timestamp: SystemTime::now(),
68            channel,
69            payload,
70            tags: Vec::new(),
71            metadata: Map::new(),
72            embedding: None,
73        }
74    }
75
76    /// Returns the unique identifier for this record.
77    #[must_use]
78    pub fn id(&self) -> Uuid {
79        self.id
80    }
81
82    /// Returns the timestamp associated with the record.
83    #[must_use]
84    pub fn timestamp(&self) -> SystemTime {
85        self.timestamp
86    }
87
88    /// Returns the channel.
89    #[must_use]
90    pub fn channel(&self) -> &MemoryChannel {
91        &self.channel
92    }
93
94    /// Returns the payload bytes.
95    #[must_use]
96    pub fn payload(&self) -> &Bytes {
97        &self.payload
98    }
99
100    /// Returns associated tags.
101    #[must_use]
102    pub fn tags(&self) -> &[String] {
103        &self.tags
104    }
105
106    /// Returns metadata map.
107    #[must_use]
108    pub fn metadata(&self) -> &Map<String, Value> {
109        &self.metadata
110    }
111
112    /// Returns the optional embedding associated with the record.
113    #[must_use]
114    pub fn embedding(&self) -> Option<&EmbeddingVector> {
115        self.embedding.as_ref()
116    }
117}
118
119/// Builder type used to assemble [`MemoryRecord`] instances safely.
120#[derive(Debug)]
121pub struct MemoryRecordBuilder {
122    id: Uuid,
123    timestamp: SystemTime,
124    channel: MemoryChannel,
125    payload: Bytes,
126    tags: Vec<String>,
127    metadata: Map<String, Value>,
128    embedding: Option<EmbeddingVector>,
129}
130
131impl MemoryRecordBuilder {
132    /// Overrides the record identifier.
133    #[must_use]
134    pub fn id(mut self, id: Uuid) -> Self {
135        self.id = id;
136        self
137    }
138
139    /// Sets the timestamp for the record.
140    #[must_use]
141    pub fn timestamp(mut self, timestamp: SystemTime) -> Self {
142        self.timestamp = timestamp;
143        self
144    }
145
146    /// Adds a single tag after validating that it is not empty.
147    ///
148    /// # Errors
149    ///
150    /// Returns [`MemoryError::InvalidRecord`] when the tag is empty or whitespace.
151    pub fn tag(mut self, tag: impl Into<String>) -> MemoryResult<Self> {
152        let value = tag.into();
153        if value.trim().is_empty() {
154            return Err(MemoryError::InvalidRecord("memory tags must not be empty"));
155        }
156        self.tags.push(value);
157        Ok(self)
158    }
159
160    /// Extends the record with multiple tags.
161    ///
162    /// # Errors
163    ///
164    /// Returns [`MemoryError::InvalidRecord`] if any supplied tag is empty.
165    pub fn tags<I, S>(mut self, tags: I) -> MemoryResult<Self>
166    where
167        I: IntoIterator<Item = S>,
168        S: Into<String>,
169    {
170        for tag in tags {
171            self = self.tag(tag)?;
172        }
173        Ok(self)
174    }
175
176    /// Adds metadata entry.
177    #[must_use]
178    pub fn metadata(mut self, key: impl Into<String>, value: Value) -> Self {
179        self.metadata.insert(key.into(), value);
180        self
181    }
182
183    /// Adds a full metadata map, overwriting existing keys when duplicates occur.
184    #[must_use]
185    pub fn merge_metadata(mut self, map: Map<String, Value>) -> Self {
186        self.metadata.extend(map);
187        self
188    }
189
190    /// Attaches an embedding to the record.
191    #[must_use]
192    pub fn embedding(mut self, embedding: EmbeddingVector) -> Self {
193        self.embedding = Some(embedding);
194        self
195    }
196
197    /// Finalises the builder and produces the record.
198    ///
199    /// # Errors
200    ///
201    /// Returns [`MemoryError`] when the builder state fails validation.
202    pub fn build(self) -> MemoryResult<MemoryRecord> {
203        Ok(MemoryRecord {
204            id: self.id,
205            timestamp: self.timestamp,
206            channel: self.channel,
207            payload: self.payload,
208            tags: self.tags,
209            metadata: self.metadata,
210            embedding: self.embedding,
211        })
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218
219    #[test]
220    fn builder_rejects_empty_tags() {
221        let payload = Bytes::from_static(b"payload");
222        let err = MemoryRecord::builder(MemoryChannel::Input, payload.clone())
223            .tag("")
224            .expect_err("empty tag should fail");
225        assert!(matches!(err, MemoryError::InvalidRecord(_)));
226
227        let err = MemoryRecord::builder(MemoryChannel::Input, payload)
228            .tags(vec!["ok", " "])
229            .expect_err("whitespace tag should fail");
230        assert!(matches!(err, MemoryError::InvalidRecord(_)));
231    }
232
233    #[test]
234    fn builder_constructs_record() {
235        let payload = Bytes::from_static(b"payload");
236        let record = MemoryRecord::builder(MemoryChannel::Output, payload.clone())
237            .tag("mxp")
238            .unwrap()
239            .metadata("key", Value::from("value"))
240            .build()
241            .unwrap();
242
243        assert_eq!(record.payload(), &payload);
244        assert_eq!(record.tags(), ["mxp"]);
245        assert_eq!(record.metadata().get("key").unwrap(), "value");
246    }
247}