Skip to main content

use_agent/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::{fmt, str::FromStr};
5use std::error::Error;
6
7pub mod prelude {
8    pub use crate::{
9        AgentActionKind, AgentAutonomyLevel, AgentError, AgentHandoffKind, AgentId, AgentKind,
10        AgentLoopKind, AgentMode, AgentName, AgentObservationKind, AgentStatus,
11    };
12}
13
14macro_rules! agent_text_newtype {
15    ($name:ident) => {
16        #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
17        pub struct $name(String);
18
19        impl $name {
20            pub fn new(value: impl AsRef<str>) -> Result<Self, AgentError> {
21                non_empty_text(value).map(Self)
22            }
23
24            pub fn as_str(&self) -> &str {
25                &self.0
26            }
27
28            pub fn value(&self) -> &str {
29                self.as_str()
30            }
31
32            pub fn into_string(self) -> String {
33                self.0
34            }
35        }
36
37        impl AsRef<str> for $name {
38            fn as_ref(&self) -> &str {
39                self.as_str()
40            }
41        }
42
43        impl fmt::Display for $name {
44            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45                formatter.write_str(self.as_str())
46            }
47        }
48
49        impl FromStr for $name {
50            type Err = AgentError;
51
52            fn from_str(value: &str) -> Result<Self, Self::Err> {
53                Self::new(value)
54            }
55        }
56
57        impl TryFrom<&str> for $name {
58            type Error = AgentError;
59
60            fn try_from(value: &str) -> Result<Self, Self::Error> {
61                Self::new(value)
62            }
63        }
64    };
65}
66
67macro_rules! agent_enum {
68    ($name:ident { $($variant:ident => $label:literal),+ $(,)? }) => {
69        #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
70        pub enum $name {
71            $($variant),+
72        }
73
74        impl $name {
75            pub const ALL: &'static [Self] = &[$(Self::$variant),+];
76
77            pub const fn as_str(self) -> &'static str {
78                match self {
79                    $(Self::$variant => $label),+
80                }
81            }
82        }
83
84        impl fmt::Display for $name {
85            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86                formatter.write_str(self.as_str())
87            }
88        }
89
90        impl FromStr for $name {
91            type Err = AgentError;
92
93            fn from_str(value: &str) -> Result<Self, Self::Err> {
94                match normalized_label(value)?.as_str() {
95                    $($label => Ok(Self::$variant),)+
96                    _ => Err(AgentError::UnknownLabel),
97                }
98            }
99        }
100    };
101}
102
103agent_text_newtype!(AgentName);
104agent_text_newtype!(AgentId);
105
106agent_enum!(AgentKind {
107    Assistant => "assistant",
108    Planner => "planner",
109    Executor => "executor",
110    Researcher => "researcher",
111    Coder => "coder",
112    Reviewer => "reviewer",
113    Critic => "critic",
114    Router => "router",
115    Orchestrator => "orchestrator",
116    Worker => "worker",
117    Custom => "custom",
118});
119
120agent_enum!(AgentStatus {
121    Idle => "idle",
122    Planning => "planning",
123    Running => "running",
124    WaitingForTool => "waiting-for-tool",
125    WaitingForUser => "waiting-for-user",
126    Succeeded => "succeeded",
127    Failed => "failed",
128    Cancelled => "cancelled",
129    Paused => "paused",
130});
131
132agent_enum!(AgentMode {
133    Manual => "manual",
134    Assisted => "assisted",
135    SemiAutonomous => "semi-autonomous",
136    Autonomous => "autonomous",
137    Supervised => "supervised",
138});
139
140agent_enum!(AgentAutonomyLevel {
141    None => "none",
142    Low => "low",
143    Medium => "medium",
144    High => "high",
145    Full => "full",
146});
147
148agent_enum!(AgentLoopKind {
149    SingleTurn => "single-turn",
150    MultiTurn => "multi-turn",
151    ReactLike => "react-like",
152    PlanExecute => "plan-execute",
153    ReflectAct => "reflect-act",
154    RouterWorker => "router-worker",
155    Custom => "custom",
156});
157
158agent_enum!(AgentHandoffKind {
159    User => "user",
160    Agent => "agent",
161    Tool => "tool",
162    HumanReviewer => "human-reviewer",
163    System => "system",
164    None => "none",
165});
166
167agent_enum!(AgentObservationKind {
168    UserInput => "user-input",
169    ToolResult => "tool-result",
170    RetrievedContext => "retrieved-context",
171    Memory => "memory",
172    SystemEvent => "system-event",
173    Error => "error",
174    Custom => "custom",
175});
176
177agent_enum!(AgentActionKind {
178    Respond => "respond",
179    AskClarification => "ask-clarification",
180    CallTool => "call-tool",
181    Retrieve => "retrieve",
182    Plan => "plan",
183    Delegate => "delegate",
184    Stop => "stop",
185    Custom => "custom",
186});
187
188#[derive(Clone, Copy, Debug, Eq, PartialEq)]
189pub enum AgentError {
190    Empty,
191    UnknownLabel,
192}
193
194impl fmt::Display for AgentError {
195    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
196        match self {
197            Self::Empty => formatter.write_str("agent metadata text cannot be empty"),
198            Self::UnknownLabel => formatter.write_str("unknown agent metadata label"),
199        }
200    }
201}
202
203impl Error for AgentError {}
204
205fn non_empty_text(value: impl AsRef<str>) -> Result<String, AgentError> {
206    let trimmed = value.as_ref().trim();
207    if trimmed.is_empty() {
208        Err(AgentError::Empty)
209    } else {
210        Ok(trimmed.to_string())
211    }
212}
213
214fn normalized_label(value: &str) -> Result<String, AgentError> {
215    let trimmed = value.trim();
216    if trimmed.is_empty() {
217        Err(AgentError::Empty)
218    } else {
219        Ok(trimmed.to_ascii_lowercase().replace(['_', ' '], "-"))
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::{
226        AgentActionKind, AgentAutonomyLevel, AgentError, AgentHandoffKind, AgentId, AgentKind,
227        AgentLoopKind, AgentMode, AgentName, AgentObservationKind, AgentStatus,
228    };
229    use core::{fmt, str::FromStr};
230
231    macro_rules! assert_text_newtype {
232        ($type:ty, $value:literal) => {{
233            let value = <$type>::new(concat!(" ", $value, " "))?;
234            assert_eq!(value.as_str(), $value);
235            assert_eq!(value.value(), $value);
236            assert_eq!(value.as_ref(), $value);
237            assert_eq!(value.to_string(), $value);
238            assert_eq!(<$type as TryFrom<&str>>::try_from($value)?, value);
239            assert_eq!(value.into_string(), $value.to_string());
240        }};
241    }
242
243    fn assert_enum_family<T>(variants: &[T]) -> Result<(), AgentError>
244    where
245        T: Copy + Eq + fmt::Debug + fmt::Display + FromStr<Err = AgentError>,
246    {
247        for variant in variants {
248            let label = variant.to_string();
249            assert_eq!(label.parse::<T>()?, *variant);
250            assert_eq!(label.replace('-', "_").parse::<T>()?, *variant);
251            assert_eq!(label.replace('-', " ").parse::<T>()?, *variant);
252        }
253        Ok(())
254    }
255
256    #[test]
257    fn validates_agent_text_newtypes() -> Result<(), AgentError> {
258        assert_text_newtype!(AgentName, "triage-agent");
259        assert_text_newtype!(AgentId, "agent-001");
260        assert_eq!(AgentName::new("  "), Err(AgentError::Empty));
261        Ok(())
262    }
263
264    #[test]
265    fn displays_and_parses_agent_enums() -> Result<(), AgentError> {
266        assert_enum_family(AgentKind::ALL)?;
267        assert_enum_family(AgentStatus::ALL)?;
268        assert_enum_family(AgentMode::ALL)?;
269        assert_enum_family(AgentAutonomyLevel::ALL)?;
270        assert_enum_family(AgentLoopKind::ALL)?;
271        assert_enum_family(AgentHandoffKind::ALL)?;
272        assert_enum_family(AgentObservationKind::ALL)?;
273        assert_enum_family(AgentActionKind::ALL)?;
274        assert_eq!(
275            "waiting for tool".parse::<AgentStatus>()?,
276            AgentStatus::WaitingForTool
277        );
278        Ok(())
279    }
280}