1use std::cmp::Ordering;
20use std::fmt;
21use std::str::FromStr;
22
23use mti::prelude::*;
24
25use crate::agent_prefix::AgentPrefix;
26use crate::constants::{AGENT_SUFFIX_LENGTH, MAX_AGENT_ID_LENGTH};
27use crate::error::AgentIdError;
28
29const BASE32_ALPHABET: &[u8] = b"0123456789abcdefghjkmnpqrstvwxyz";
32
33#[derive(Debug, Clone, PartialEq, Eq, Hash)]
60pub struct AgentId {
61 prefix: AgentPrefix,
62 suffix: String,
63 inner: MagicTypeId,
64}
65
66impl AgentId {
67 #[must_use]
73 pub fn new(prefix: &str) -> Self {
74 Self::try_new(prefix).expect("valid prefix")
75 }
76
77 pub fn try_new(prefix: &str) -> Result<Self, AgentIdError> {
83 let agent_prefix = AgentPrefix::parse(prefix).map_err(AgentIdError::InvalidPrefix)?;
84 let type_id = prefix.create_type_id::<V7>();
85 let suffix = type_id
86 .suffix_str()
87 .map_err(|e| AgentIdError::TypeIdError(e.to_string()))?;
88
89 Ok(Self {
90 prefix: agent_prefix,
91 suffix,
92 inner: type_id,
93 })
94 }
95
96 pub fn parse(input: &str) -> Result<Self, AgentIdError> {
107 if input.is_empty() {
108 return Err(AgentIdError::Empty);
109 }
110
111 if input.len() > MAX_AGENT_ID_LENGTH {
112 return Err(AgentIdError::TooLong {
113 max: MAX_AGENT_ID_LENGTH,
114 actual: input.len(),
115 });
116 }
117
118 let sep_idx = input.rfind('_').ok_or(AgentIdError::MissingSeparator)?;
120
121 if sep_idx == 0 {
122 return Err(AgentIdError::InvalidPrefix(
123 crate::error::AgentPrefixError::Empty,
124 ));
125 }
126
127 let prefix_str = &input[..sep_idx];
128 let suffix_str = &input[sep_idx + 1..];
129
130 let prefix = AgentPrefix::parse(prefix_str).map_err(AgentIdError::InvalidPrefix)?;
132
133 Self::validate_suffix(suffix_str)?;
135
136 let inner =
138 MagicTypeId::from_str(input).map_err(|e| AgentIdError::TypeIdError(e.to_string()))?;
139
140 Ok(Self {
141 prefix,
142 suffix: suffix_str.to_string(),
143 inner,
144 })
145 }
146
147 #[must_use]
149 pub const fn prefix(&self) -> &AgentPrefix {
150 &self.prefix
151 }
152
153 #[must_use]
155 pub fn suffix(&self) -> &str {
156 &self.suffix
157 }
158
159 #[must_use]
161 pub const fn inner(&self) -> &MagicTypeId {
162 &self.inner
163 }
164
165 pub fn uuid(&self) -> Result<uuid::Uuid, AgentIdError> {
171 self.inner
172 .uuid()
173 .map_err(|e| AgentIdError::TypeIdError(e.to_string()))
174 }
175
176 fn validate_suffix(suffix: &str) -> Result<(), AgentIdError> {
177 if suffix.len() != AGENT_SUFFIX_LENGTH {
178 return Err(AgentIdError::InvalidSuffix {
179 value: suffix.to_string(),
180 reason: "suffix must be exactly 26 characters",
181 });
182 }
183
184 let first = suffix
186 .chars()
187 .next()
188 .expect("already checked suffix is not empty");
189 if !('0'..='7').contains(&first) {
190 return Err(AgentIdError::InvalidSuffix {
191 value: suffix.to_string(),
192 reason: "first character must be 0-7",
193 });
194 }
195
196 for c in suffix.chars() {
198 if !BASE32_ALPHABET.contains(&(c as u8)) {
199 return Err(AgentIdError::InvalidSuffix {
200 value: suffix.to_string(),
201 reason: "contains invalid base32 character",
202 });
203 }
204 }
205
206 Ok(())
207 }
208}
209
210impl fmt::Display for AgentId {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 write!(f, "{}", self.inner)
213 }
214}
215
216impl FromStr for AgentId {
217 type Err = AgentIdError;
218
219 fn from_str(s: &str) -> Result<Self, Self::Err> {
220 Self::parse(s)
221 }
222}
223
224impl AsRef<MagicTypeId> for AgentId {
225 fn as_ref(&self) -> &MagicTypeId {
226 &self.inner
227 }
228}
229
230impl TryFrom<&str> for AgentId {
231 type Error = AgentIdError;
232
233 fn try_from(s: &str) -> Result<Self, Self::Error> {
234 Self::parse(s)
235 }
236}
237
238impl PartialOrd for AgentId {
239 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
240 Some(self.cmp(other))
241 }
242}
243
244impl Ord for AgentId {
245 fn cmp(&self, other: &Self) -> Ordering {
246 self.to_string().cmp(&other.to_string())
247 }
248}
249
250#[cfg(feature = "serde")]
251impl serde::Serialize for AgentId {
252 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
253 where
254 S: serde::Serializer,
255 {
256 serializer.serialize_str(&self.to_string())
257 }
258}
259
260#[cfg(feature = "serde")]
261impl<'de> serde::Deserialize<'de> for AgentId {
262 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
263 where
264 D: serde::Deserializer<'de>,
265 {
266 let s = String::deserialize(deserializer)?;
267 Self::parse(&s).map_err(serde::de::Error::custom)
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn create_new_id() {
277 let id = AgentId::new("llm_chat");
278 assert_eq!(id.prefix().as_str(), "llm_chat");
279 assert_eq!(id.suffix().len(), 26);
280 }
281
282 #[test]
283 fn parse_valid_id() {
284 let id = AgentId::parse("llm_chat_01h455vb4pex5vsknk084sn02q").unwrap();
285 assert_eq!(id.prefix().as_str(), "llm_chat");
286 assert_eq!(id.suffix(), "01h455vb4pex5vsknk084sn02q");
287 }
288
289 #[test]
290 fn parse_simple_prefix() {
291 let id = AgentId::parse("llm_01h455vb4pex5vsknk084sn02q").unwrap();
292 assert_eq!(id.prefix().as_str(), "llm");
293 }
294
295 #[test]
296 fn parse_empty_fails() {
297 let result = AgentId::parse("");
298 assert!(matches!(result, Err(AgentIdError::Empty)));
299 }
300
301 #[test]
302 fn parse_too_long_fails() {
303 let long_prefix = "a".repeat(65);
304 let input = format!("{long_prefix}_01h455vb4pex5vsknk084sn02q");
305 let result = AgentId::parse(&input);
306 assert!(matches!(result, Err(AgentIdError::TooLong { .. })));
307 }
308
309 #[test]
310 fn parse_missing_separator_fails() {
311 let result = AgentId::parse("llm01h455vb4pex5vsknk084sn02q");
312 assert!(matches!(result, Err(AgentIdError::MissingSeparator)));
313 }
314
315 #[test]
316 fn parse_invalid_prefix_fails() {
317 let result = AgentId::parse("LLM_01h455vb4pex5vsknk084sn02q");
318 assert!(matches!(result, Err(AgentIdError::InvalidPrefix(_))));
319 }
320
321 #[test]
322 fn parse_suffix_wrong_length_fails() {
323 let result = AgentId::parse("llm_01h455vb4pex");
324 assert!(matches!(result, Err(AgentIdError::InvalidSuffix { .. })));
325 }
326
327 #[test]
328 fn parse_suffix_invalid_first_char_fails() {
329 let result = AgentId::parse("llm_91h455vb4pex5vsknk084sn02q");
331 assert!(matches!(result, Err(AgentIdError::InvalidSuffix { .. })));
332 }
333
334 #[test]
335 fn parse_suffix_invalid_base32_char_fails() {
336 let result = AgentId::parse("llm_01h455vb4pex5vsknk084sn0iq");
338 assert!(matches!(result, Err(AgentIdError::InvalidSuffix { .. })));
339 }
340
341 #[test]
342 fn roundtrip_display_parse() {
343 let id1 = AgentId::new("llm_chat");
344 let display = id1.to_string();
345 let id2 = AgentId::parse(&display).unwrap();
346 assert_eq!(id1.prefix(), id2.prefix());
347 assert_eq!(id1.suffix(), id2.suffix());
348 }
349}