Skip to main content

agent_works/multi_agent/
path.rs

1//! Tree-style agent path identifier.
2//!
3//! `AgentPath` represents a position in the agent tree, e.g. `root/searcher/worker-1`.
4//! It is used for routing messages between agents and for event attribution.
5//!
6//! With `max_agent_depth=1` (the default), paths are always `root` or `root/<child-name>`.
7
8use std::fmt;
9
10/// A tree-style path identifying an agent in the agent hierarchy.
11///
12/// # Format
13///
14/// Path segments are separated by `/`. The root agent is always `root`.
15/// Child agents have paths like `root/searcher` or `root/analyzer`.
16///
17/// # Examples
18///
19/// ```rust
20/// use agent_works::multi_agent::AgentPath;
21///
22/// let root = AgentPath::root();
23/// assert!(root.is_root());
24/// assert_eq!(root.depth(), 0);
25/// assert_eq!(root.to_string(), "root");
26///
27/// let child = root.join("searcher");
28/// assert!(!child.is_root());
29/// assert_eq!(child.depth(), 1);
30/// assert_eq!(child.to_string(), "root/searcher");
31///
32/// let parsed = "root/searcher".parse::<AgentPath>().unwrap();
33/// assert_eq!(parsed, child);
34/// ```
35#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
36pub struct AgentPath {
37    /// Path segments from root to this agent.
38    /// The first segment is always `"root"`.
39    segments: Vec<String>,
40}
41
42impl AgentPath {
43    /// Create the root agent path.
44    pub fn root() -> Self {
45        Self {
46            segments: vec!["root".to_string()],
47        }
48    }
49
50    /// Create a child path by appending a segment.
51    ///
52    /// # Examples
53    ///
54    /// ```rust
55    /// use agent_works::multi_agent::AgentPath;
56    ///
57    /// let root = AgentPath::root();
58    /// let searcher = root.join("searcher");
59    /// assert_eq!(searcher.to_string(), "root/searcher");
60    /// ```
61    pub fn join(&self, name: &str) -> Self {
62        let mut segments = self.segments.clone();
63        segments.push(name.to_string());
64        Self { segments }
65    }
66
67    /// Parse from a string like `"root/searcher/worker-1"`.
68    ///
69    /// Returns `None` if the string is empty, contains an empty segment, or does not
70    /// start with `"root"`.
71    pub fn parse(path: &str) -> Option<Self> {
72        if path.is_empty() {
73            return None;
74        }
75        let segments: Vec<String> = path.split('/').map(|s| s.to_string()).collect();
76        // Validate: no empty segments
77        if segments.iter().any(|s| s.is_empty()) {
78            return None;
79        }
80        // First segment must be "root"
81        if segments.first().map(|s| s.as_str()) != Some("root") {
82            return None;
83        }
84        Some(Self { segments })
85    }
86
87    /// Returns `true` if this is the root agent.
88    pub fn is_root(&self) -> bool {
89        self.segments.len() == 1
90    }
91
92    /// Returns the depth from root.
93    ///
94    /// Root has depth 0, `root/searcher` has depth 1, `root/searcher/worker-1` has depth 2.
95    pub fn depth(&self) -> usize {
96        self.segments.len() - 1
97    }
98
99    /// Returns the name of this agent (the last segment).
100    ///
101    /// # Examples
102    ///
103    /// ```rust
104    /// use agent_works::multi_agent::AgentPath;
105    ///
106    /// let path = AgentPath::root().join("searcher");
107    /// assert_eq!(path.name(), "searcher");
108    /// assert_eq!(AgentPath::root().name(), "root");
109    /// ```
110    pub fn name(&self) -> &str {
111        self.segments.last().map(|s| s.as_str()).unwrap_or("root")
112    }
113
114    /// Returns the parent path, or `None` if this is the root.
115    ///
116    /// # Examples
117    ///
118    /// ```rust
119    /// use agent_works::multi_agent::AgentPath;
120    ///
121    /// let child = AgentPath::root().join("searcher");
122    /// assert_eq!(child.parent(), Some(AgentPath::root()));
123    /// assert_eq!(AgentPath::root().parent(), None);
124    /// ```
125    pub fn parent(&self) -> Option<Self> {
126        if self.is_root() {
127            return None;
128        }
129        let mut segments = self.segments.clone();
130        segments.pop();
131        Some(Self { segments })
132    }
133
134    /// Returns the segments of this path.
135    pub fn segments(&self) -> &[String] {
136        &self.segments
137    }
138}
139
140impl fmt::Display for AgentPath {
141    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
142        write!(f, "{}", self.segments.join("/"))
143    }
144}
145
146impl std::str::FromStr for AgentPath {
147    type Err = String;
148
149    fn from_str(s: &str) -> Result<Self, Self::Err> {
150        AgentPath::parse(s).ok_or_else(|| format!("invalid agent path: '{}'", s))
151    }
152}
153
154impl serde::Serialize for AgentPath {
155    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
156        serializer.serialize_str(&self.to_string())
157    }
158}
159
160impl<'de> serde::Deserialize<'de> for AgentPath {
161    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
162        let s = String::deserialize(deserializer)?;
163        AgentPath::parse(&s)
164            .ok_or_else(|| serde::de::Error::custom(format!("invalid agent path: '{}'", s)))
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn root_path() {
174        let root = AgentPath::root();
175        assert!(root.is_root());
176        assert_eq!(root.depth(), 0);
177        assert_eq!(root.name(), "root");
178        assert_eq!(root.to_string(), "root");
179        assert_eq!(root.parent(), None);
180    }
181
182    #[test]
183    fn join_child() {
184        let root = AgentPath::root();
185        let child = root.join("searcher");
186        assert!(!child.is_root());
187        assert_eq!(child.depth(), 1);
188        assert_eq!(child.name(), "searcher");
189        assert_eq!(child.to_string(), "root/searcher");
190        assert_eq!(child.parent(), Some(root));
191    }
192
193    #[test]
194    fn join_nested() {
195        let root = AgentPath::root();
196        let grandchild = root.join("searcher").join("worker-1");
197        assert!(!grandchild.is_root());
198        assert_eq!(grandchild.depth(), 2);
199        assert_eq!(grandchild.name(), "worker-1");
200        assert_eq!(grandchild.to_string(), "root/searcher/worker-1");
201        assert_eq!(grandchild.parent(), Some(root.join("searcher")));
202    }
203
204    #[test]
205    fn parse_valid() {
206        assert_eq!("root".parse::<AgentPath>().unwrap(), AgentPath::root());
207        assert_eq!(
208            "root/searcher".parse::<AgentPath>().unwrap(),
209            AgentPath::root().join("searcher")
210        );
211        assert_eq!(
212            "root/a/b".parse::<AgentPath>().unwrap(),
213            AgentPath::root().join("a").join("b")
214        );
215    }
216
217    #[test]
218    fn parse_invalid() {
219        assert!("".parse::<AgentPath>().is_err());
220        assert!("/root".parse::<AgentPath>().is_err()); // empty first segment
221        assert!("not-root/a".parse::<AgentPath>().is_err()); // doesn't start with root
222        assert!("root/".parse::<AgentPath>().is_err()); // trailing empty
223        assert!("root//a".parse::<AgentPath>().is_err()); // empty middle segment
224    }
225
226    #[test]
227    fn display_roundtrip() {
228        let paths = vec!["root", "root/searcher", "root/a/b"];
229        for s in paths {
230            let parsed: AgentPath = s.parse().unwrap();
231            assert_eq!(parsed.to_string(), s);
232        }
233    }
234
235    #[test]
236    fn serde_roundtrip() {
237        let path = AgentPath::root().join("searcher");
238        let json = serde_json::to_string(&path).unwrap();
239        assert_eq!(json, "\"root/searcher\"");
240        let deserialized: AgentPath = serde_json::from_str(&json).unwrap();
241        assert_eq!(deserialized, path);
242    }
243
244    #[test]
245    fn serde_invalid() {
246        assert!(serde_json::from_str::<AgentPath>("\"\"").is_err());
247        assert!(serde_json::from_str::<AgentPath>("\"not-root\"").is_err());
248    }
249
250    #[test]
251    fn segments_accessor() {
252        let path = AgentPath::root().join("a").join("b");
253        assert_eq!(path.segments(), &["root", "a", "b"]);
254    }
255
256    #[test]
257    fn ordering() {
258        let a = AgentPath::root().join("a");
259        let b = AgentPath::root().join("b");
260        let a1 = AgentPath::root().join("a").join("1");
261        assert!(a < b);
262        assert!(a < a1);
263    }
264}