Skip to main content

a2a_rust/types/
agent_id.rs

1use std::fmt;
2use std::str::FromStr;
3
4use serde::{Deserialize, Serialize};
5
6use crate::A2AError;
7
8/// Validated helper type for agent identifiers used in application-level naming.
9///
10/// This is not a tagged proto field. It exists to codify the repository's
11/// naming convention for agent IDs:
12///
13/// - only lowercase ASCII letters, digits, and `-`
14/// - length between 3 and 64 characters
15#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
16#[serde(transparent)]
17pub struct AgentId(String);
18
19impl AgentId {
20    /// Validate and construct an `AgentId`.
21    pub fn new(value: impl Into<String>) -> Result<Self, A2AError> {
22        let value = value.into();
23        validate_agent_id(&value)?;
24        Ok(Self(value))
25    }
26
27    /// Return the validated identifier as a string slice.
28    pub fn as_str(&self) -> &str {
29        &self.0
30    }
31}
32
33impl AsRef<str> for AgentId {
34    fn as_ref(&self) -> &str {
35        self.as_str()
36    }
37}
38
39impl fmt::Display for AgentId {
40    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41        self.0.fmt(f)
42    }
43}
44
45impl From<AgentId> for String {
46    fn from(value: AgentId) -> Self {
47        value.0
48    }
49}
50
51impl TryFrom<String> for AgentId {
52    type Error = A2AError;
53
54    fn try_from(value: String) -> Result<Self, Self::Error> {
55        Self::new(value)
56    }
57}
58
59impl TryFrom<&str> for AgentId {
60    type Error = A2AError;
61
62    fn try_from(value: &str) -> Result<Self, Self::Error> {
63        Self::new(value)
64    }
65}
66
67impl FromStr for AgentId {
68    type Err = A2AError;
69
70    fn from_str(s: &str) -> Result<Self, Self::Err> {
71        Self::new(s)
72    }
73}
74
75impl<'de> Deserialize<'de> for AgentId {
76    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
77    where
78        D: serde::Deserializer<'de>,
79    {
80        let value = String::deserialize(deserializer)?;
81        Self::new(value).map_err(serde::de::Error::custom)
82    }
83}
84
85fn validate_agent_id(value: &str) -> Result<(), A2AError> {
86    if !(3..=64).contains(&value.len()) {
87        return Err(A2AError::InvalidRequest(
88            "agent_id must be between 3 and 64 characters".to_owned(),
89        ));
90    }
91
92    if !value
93        .bytes()
94        .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-')
95    {
96        return Err(A2AError::InvalidRequest(
97            "agent_id may only contain lowercase ASCII letters, digits, and '-'".to_owned(),
98        ));
99    }
100
101    Ok(())
102}
103
104#[cfg(test)]
105mod tests {
106    use super::AgentId;
107
108    #[test]
109    fn agent_id_accepts_valid_values() {
110        let agent_id = AgentId::new("echo-agent-01").expect("agent id should validate");
111        assert_eq!(agent_id.as_str(), "echo-agent-01");
112    }
113
114    #[test]
115    fn agent_id_rejects_invalid_characters() {
116        let error = AgentId::new("Echo_Agent").expect_err("agent id should be invalid");
117        assert!(
118            error
119                .to_string()
120                .contains("lowercase ASCII letters, digits, and '-'")
121        );
122    }
123
124    #[test]
125    fn agent_id_rejects_invalid_length() {
126        let error = AgentId::new("ab").expect_err("agent id should be invalid");
127        assert!(error.to_string().contains("between 3 and 64 characters"));
128    }
129}