Skip to main content

llm_agent_runtime/
types.rs

1//! Core identifier types, available without feature gates.
2//!
3//! [`AgentId`] and [`MemoryId`] are defined here so they can be used by the
4//! agent runtime regardless of whether the `memory` feature is enabled.
5
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9/// Stable identifier for an agent instance.
10#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
11pub struct AgentId(pub String);
12
13impl AgentId {
14    /// Create a new `AgentId` from any string-like value.
15    ///
16    /// # Panics (debug only)
17    ///
18    /// Triggers a `debug_assert!` if `id` is empty.  In release builds a
19    /// `tracing::warn!` is emitted instead so that the misconfiguration is
20    /// surfaced in production logs without aborting the process.
21    pub fn new(id: impl Into<String>) -> Self {
22        let id = id.into();
23        if id.is_empty() {
24            debug_assert!(false, "AgentId must not be empty");
25            tracing::warn!("AgentId::new called with an empty string — agent IDs should be non-empty to avoid lookup ambiguity");
26        }
27        Self(id)
28    }
29
30    /// Create a validated `AgentId`, returning an error if `id` is empty.
31    ///
32    /// Prefer this constructor in user-facing code where empty IDs must be
33    /// rejected explicitly rather than silently warned about.
34    pub fn try_new(id: impl Into<String>) -> Result<Self, crate::error::AgentRuntimeError> {
35        let id = id.into();
36        if id.is_empty() {
37            return Err(crate::error::AgentRuntimeError::Memory(
38                "AgentId must not be empty".into(),
39            ));
40        }
41        Ok(Self(id))
42    }
43
44    /// Generate a random `AgentId` backed by a UUID v4.
45    pub fn random() -> Self {
46        Self(Uuid::new_v4().to_string())
47    }
48
49    /// Return the inner ID string as a `&str`.
50    pub fn as_str(&self) -> &str {
51        &self.0
52    }
53
54    /// Return the byte length of the inner ID string.
55    pub fn len(&self) -> usize {
56        self.0.len()
57    }
58
59    /// Return `true` if the inner ID string is empty.
60    ///
61    /// Note: `AgentId::new` warns (debug: asserts) against empty IDs.
62    /// This predicate is provided for defensive checks.
63    pub fn is_empty(&self) -> bool {
64        self.0.is_empty()
65    }
66
67    /// Return `true` if the inner ID string starts with `prefix`.
68    pub fn starts_with(&self, prefix: &str) -> bool {
69        self.0.starts_with(prefix)
70    }
71}
72
73impl AsRef<str> for AgentId {
74    fn as_ref(&self) -> &str {
75        &self.0
76    }
77}
78
79impl std::fmt::Display for AgentId {
80    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81        write!(f, "{}", self.0)
82    }
83}
84
85impl From<String> for AgentId {
86    /// Create an `AgentId` from an owned `String`.
87    ///
88    /// Equivalent to [`AgentId::new`]; emits a `tracing::warn!` for empty strings.
89    fn from(s: String) -> Self {
90        Self::new(s)
91    }
92}
93
94impl From<&str> for AgentId {
95    /// Create an `AgentId` from a string slice.
96    ///
97    /// Equivalent to [`AgentId::new`]; emits a `tracing::warn!` for empty strings.
98    fn from(s: &str) -> Self {
99        Self::new(s)
100    }
101}
102
103impl std::str::FromStr for AgentId {
104    type Err = crate::error::AgentRuntimeError;
105
106    /// Parse an `AgentId` from a string, returning an error if the string is empty.
107    ///
108    /// Unlike [`AgentId::new`] this is a validated constructor: empty strings are
109    /// rejected with `AgentRuntimeError::Memory` rather than silently warned about.
110    fn from_str(s: &str) -> Result<Self, Self::Err> {
111        Self::try_new(s)
112    }
113}
114
115impl std::ops::Deref for AgentId {
116    type Target = str;
117
118    /// Dereference to the inner ID string slice.
119    ///
120    /// Allows `&agent_id` to coerce to `&str` transparently.
121    fn deref(&self) -> &Self::Target {
122        &self.0
123    }
124}
125
126/// Stable identifier for a memory item.
127#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
128pub struct MemoryId(pub String);
129
130impl MemoryId {
131    /// Create a new `MemoryId` from any string-like value.
132    ///
133    /// # Panics (debug only)
134    ///
135    /// Triggers a `debug_assert!` if `id` is empty.  In release builds a
136    /// `tracing::warn!` is emitted instead so that the misconfiguration is
137    /// surfaced in production logs without aborting the process.
138    pub fn new(id: impl Into<String>) -> Self {
139        let id = id.into();
140        if id.is_empty() {
141            debug_assert!(false, "MemoryId must not be empty");
142            tracing::warn!("MemoryId::new called with an empty string — memory IDs should be non-empty to avoid lookup ambiguity");
143        }
144        Self(id)
145    }
146
147    /// Create a validated `MemoryId`, returning an error if `id` is empty.
148    pub fn try_new(id: impl Into<String>) -> Result<Self, crate::error::AgentRuntimeError> {
149        let id = id.into();
150        if id.is_empty() {
151            return Err(crate::error::AgentRuntimeError::Memory(
152                "MemoryId must not be empty".into(),
153            ));
154        }
155        Ok(Self(id))
156    }
157
158    /// Generate a random `MemoryId` backed by a UUID v4.
159    pub fn random() -> Self {
160        Self(Uuid::new_v4().to_string())
161    }
162
163    /// Return the inner ID string as a `&str`.
164    pub fn as_str(&self) -> &str {
165        &self.0
166    }
167
168    /// Return the byte length of the inner ID string.
169    pub fn len(&self) -> usize {
170        self.0.len()
171    }
172
173    /// Return `true` if the inner ID string is empty.
174    pub fn is_empty(&self) -> bool {
175        self.0.is_empty()
176    }
177
178    /// Return `true` if the inner ID string starts with `prefix`.
179    pub fn starts_with(&self, prefix: &str) -> bool {
180        self.0.starts_with(prefix)
181    }
182}
183
184impl AsRef<str> for MemoryId {
185    fn as_ref(&self) -> &str {
186        &self.0
187    }
188}
189
190impl std::fmt::Display for MemoryId {
191    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192        write!(f, "{}", self.0)
193    }
194}
195
196impl From<String> for MemoryId {
197    /// Create a `MemoryId` from an owned `String`.
198    ///
199    /// Equivalent to [`MemoryId::new`]; emits a `tracing::warn!` for empty strings.
200    fn from(s: String) -> Self {
201        Self::new(s)
202    }
203}
204
205impl From<&str> for MemoryId {
206    /// Create a `MemoryId` from a string slice.
207    ///
208    /// Equivalent to [`MemoryId::new`]; emits a `tracing::warn!` for empty strings.
209    fn from(s: &str) -> Self {
210        Self::new(s)
211    }
212}
213
214impl std::str::FromStr for MemoryId {
215    type Err = crate::error::AgentRuntimeError;
216
217    /// Parse a `MemoryId` from a string, returning an error if the string is empty.
218    ///
219    /// Unlike [`MemoryId::new`] this is a validated constructor: empty strings are
220    /// rejected with `AgentRuntimeError::Memory` rather than silently warned about.
221    fn from_str(s: &str) -> Result<Self, Self::Err> {
222        Self::try_new(s)
223    }
224}
225
226impl std::ops::Deref for MemoryId {
227    type Target = str;
228
229    /// Dereference to the inner ID string slice.
230    ///
231    /// Allows `&memory_id` to coerce to `&str` transparently.
232    fn deref(&self) -> &Self::Target {
233        &self.0
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240
241    #[test]
242    fn test_agent_id_new_stores_value() {
243        let id = AgentId::new("agent-1");
244        assert_eq!(id.as_str(), "agent-1");
245    }
246
247    #[test]
248    fn test_agent_id_try_new_rejects_empty() {
249        assert!(AgentId::try_new("").is_err());
250    }
251
252    #[test]
253    fn test_agent_id_try_new_accepts_nonempty() {
254        let id = AgentId::try_new("ok").unwrap();
255        assert_eq!(id.as_str(), "ok");
256    }
257
258    #[test]
259    fn test_agent_id_random_generates_unique_ids() {
260        let a = AgentId::random();
261        let b = AgentId::random();
262        assert_ne!(a, b);
263    }
264
265    #[test]
266    fn test_agent_id_len_and_is_empty() {
267        let id = AgentId::new("abc");
268        assert_eq!(id.len(), 3);
269        assert!(!id.is_empty());
270    }
271
272    #[test]
273    fn test_agent_id_display() {
274        let id = AgentId::new("my-agent");
275        assert_eq!(id.to_string(), "my-agent");
276    }
277
278    #[test]
279    fn test_memory_id_new_stores_value() {
280        let id = MemoryId::new("mem-42");
281        assert_eq!(id.as_str(), "mem-42");
282    }
283
284    #[test]
285    fn test_memory_id_try_new_rejects_empty() {
286        assert!(MemoryId::try_new("").is_err());
287    }
288
289    #[test]
290    fn test_memory_id_len_and_is_empty() {
291        let id = MemoryId::new("hello");
292        assert_eq!(id.len(), 5);
293        assert!(!id.is_empty());
294    }
295
296    #[test]
297    fn test_memory_id_display() {
298        let id = MemoryId::new("mem-id");
299        assert_eq!(id.to_string(), "mem-id");
300    }
301
302    #[test]
303    fn test_memory_id_random_generates_unique_ids() {
304        let a = MemoryId::random();
305        let b = MemoryId::random();
306        assert_ne!(a, b);
307    }
308
309    // ── Round 11: starts_with ─────────────────────────────────────────────────
310
311    #[test]
312    fn test_agent_id_starts_with_matching_prefix() {
313        let id = AgentId::new("agent-001");
314        assert!(id.starts_with("agent-"));
315        assert!(!id.starts_with("user-"));
316    }
317
318    #[test]
319    fn test_agent_id_starts_with_empty_prefix_always_true() {
320        let id = AgentId::new("anything");
321        assert!(id.starts_with(""));
322    }
323
324    #[test]
325    fn test_memory_id_starts_with_matching_prefix() {
326        let id = MemoryId::new("mem-42");
327        assert!(id.starts_with("mem-"));
328        assert!(!id.starts_with("ep-"));
329    }
330
331    // ── Round 40: PartialOrd/Ord, From, FromStr, Deref ────────────────────────
332
333    #[test]
334    fn test_agent_id_ord_allows_sorting() {
335        let mut ids = vec![AgentId::new("c"), AgentId::new("a"), AgentId::new("b")];
336        ids.sort();
337        assert_eq!(ids[0].as_str(), "a");
338        assert_eq!(ids[2].as_str(), "c");
339    }
340
341    #[test]
342    fn test_agent_id_ord_allows_btreemap_key() {
343        use std::collections::BTreeMap;
344        let mut map: BTreeMap<AgentId, u32> = BTreeMap::new();
345        map.insert(AgentId::new("agent-2"), 2);
346        map.insert(AgentId::new("agent-1"), 1);
347        let keys: Vec<_> = map.keys().map(|k| k.as_str()).collect();
348        assert_eq!(keys, vec!["agent-1", "agent-2"]);
349    }
350
351    #[test]
352    fn test_agent_id_from_string() {
353        let id = AgentId::from("my-agent".to_owned());
354        assert_eq!(id.as_str(), "my-agent");
355    }
356
357    #[test]
358    fn test_agent_id_from_str_ref() {
359        let id = AgentId::from("my-agent");
360        assert_eq!(id.as_str(), "my-agent");
361    }
362
363    #[test]
364    fn test_agent_id_from_str_parse_rejects_empty() {
365        let result: Result<AgentId, _> = "".parse();
366        assert!(result.is_err());
367    }
368
369    #[test]
370    fn test_agent_id_from_str_parse_accepts_nonempty() {
371        let id: AgentId = "worker-1".parse().unwrap();
372        assert_eq!(id.as_str(), "worker-1");
373    }
374
375    #[test]
376    fn test_agent_id_deref_to_str() {
377        let id = AgentId::new("deref-test");
378        let s: &str = &id;
379        assert_eq!(s, "deref-test");
380    }
381
382    #[test]
383    fn test_agent_id_deref_enables_str_methods() {
384        let id = AgentId::new("hello-world");
385        // Deref lets us call &str methods directly on &AgentId
386        assert!(id.contains('-'));
387        assert_eq!(id.len(), 11);
388    }
389
390    #[test]
391    fn test_memory_id_ord_allows_sorting() {
392        let mut ids = vec![MemoryId::new("z"), MemoryId::new("a"), MemoryId::new("m")];
393        ids.sort();
394        assert_eq!(ids[0].as_str(), "a");
395        assert_eq!(ids[2].as_str(), "z");
396    }
397
398    #[test]
399    fn test_memory_id_from_string() {
400        let id = MemoryId::from("mem-x".to_owned());
401        assert_eq!(id.as_str(), "mem-x");
402    }
403
404    #[test]
405    fn test_memory_id_from_str_ref() {
406        let id = MemoryId::from("mem-y");
407        assert_eq!(id.as_str(), "mem-y");
408    }
409
410    #[test]
411    fn test_memory_id_from_str_parse_rejects_empty() {
412        let result: Result<MemoryId, _> = "".parse();
413        assert!(result.is_err());
414    }
415
416    #[test]
417    fn test_memory_id_from_str_parse_accepts_nonempty() {
418        let id: MemoryId = "ep-001".parse().unwrap();
419        assert_eq!(id.as_str(), "ep-001");
420    }
421
422    #[test]
423    fn test_memory_id_deref_to_str() {
424        let id = MemoryId::new("deref-mem");
425        let s: &str = &id;
426        assert_eq!(s, "deref-mem");
427    }
428}