Skip to main content

machi_types/
id.rs

1//! Typed identifiers for kernel entities.
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7use uuid::Uuid;
8
9use crate::error::{ErrorCode, MachiError};
10
11macro_rules! typed_id {
12    ($(#[$meta:meta])* $name:ident, $prefix:literal) => {
13        $(#[$meta])*
14        #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
15        #[serde(transparent)]
16        pub struct $name(String);
17
18        impl $name {
19            /// Generate a new random id with a stable prefix.
20            #[must_use]
21            pub fn generate() -> Self {
22                Self(format!("{}_{}", $prefix, Uuid::new_v4().simple()))
23            }
24
25            /// Borrow the raw string.
26            #[must_use]
27            pub fn as_str(&self) -> &str {
28                &self.0
29            }
30
31            /// Construct from a non-empty string without prefix validation.
32            ///
33            /// # Errors
34            ///
35            /// Returns [`MachiError`] when `value` is empty or whitespace-only.
36            pub fn new(value: impl Into<String>) -> Result<Self, MachiError> {
37                let value = value.into();
38                if value.trim().is_empty() {
39                    return Err(MachiError::new(
40                        ErrorCode::TypesInvalidId,
41                        format!("{} must be non-empty", stringify!($name)),
42                    ));
43                }
44                Ok(Self(value))
45            }
46        }
47
48        impl fmt::Display for $name {
49            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50                f.write_str(&self.0)
51            }
52        }
53
54        impl FromStr for $name {
55            type Err = MachiError;
56
57            fn from_str(s: &str) -> Result<Self, Self::Err> {
58                Self::new(s)
59            }
60        }
61
62        impl AsRef<str> for $name {
63            fn as_ref(&self) -> &str {
64                self.as_str()
65            }
66        }
67    };
68}
69
70typed_id!(
71    /// Identifies an agent instance or nested run.
72    AgentId,
73    "agent"
74);
75typed_id!(
76    /// Identifies a turn or top-level run.
77    RunId,
78    "run"
79);
80typed_id!(
81    /// Identifies a multi-turn session.
82    SessionId,
83    "session"
84);
85typed_id!(
86    /// Identifies a model tool call within a turn.
87    ToolCallId,
88    "call"
89);
90typed_id!(
91    /// Identifies a workflow orchestration run.
92    WorkflowRunId,
93    "wf"
94);
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn generate_has_prefix() {
102        let id = AgentId::generate();
103        assert!(id.as_str().starts_with("agent_"), "{}", id);
104    }
105
106    #[test]
107    fn rejects_empty() {
108        let err = SessionId::new("  ").expect_err("empty");
109        assert_eq!(err.code(), ErrorCode::TypesInvalidId);
110    }
111}