agent_status/agents/
mod.rs1pub mod claude_code;
2pub mod oh_my_pi;
3pub mod opencode;
4pub mod pi;
5
6#[derive(Clone, Copy, Debug, Eq, PartialEq, clap::ValueEnum)]
14pub enum AgentName {
15 #[value(name = "claude-code")]
16 ClaudeCode,
17 #[value(name = "oh-my-pi")]
18 OhMyPi,
19 #[value(name = "pi")]
20 Pi,
21 #[value(name = "opencode")]
22 Opencode,
23}
24
25impl AgentName {
26 #[must_use]
28 pub fn name(self) -> &'static str {
29 match self {
30 Self::ClaudeCode => "claude-code",
31 Self::OhMyPi => "oh-my-pi",
32 Self::Pi => "pi",
33 Self::Opencode => "opencode",
34 }
35 }
36
37 #[must_use]
41 pub fn agent(self) -> Box<dyn Agent> {
42 match self {
43 Self::ClaudeCode => Box::new(claude_code::ClaudeCodeAgent),
44 Self::OhMyPi => Box::new(oh_my_pi::OhMyPiAgent),
45 Self::Pi => Box::new(pi::PiAgent),
46 Self::Opencode => Box::new(opencode::OpencodeAgent),
47 }
48 }
49}
50
51pub trait Agent {
54 fn name(&self) -> &'static str;
57
58 fn extract_session_id(&self, stdin_json: &str) -> Option<String>;
61
62 fn extract_message(&self, _stdin_json: &str) -> Option<String> {
67 None
68 }
69}
70
71#[must_use]
73pub fn by_name(name: &str) -> Option<Box<dyn Agent>> {
74 match name {
75 "claude-code" => Some(Box::new(claude_code::ClaudeCodeAgent)),
76 "oh-my-pi" => Some(Box::new(oh_my_pi::OhMyPiAgent)),
77 "opencode" => Some(Box::new(opencode::OpencodeAgent)),
78 "pi" => Some(Box::new(pi::PiAgent)),
79 _ => None,
80 }
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86
87 #[test]
88 fn by_name_resolves_claude_code() {
89 let agent = by_name("claude-code").expect("claude-code is a registered agent");
90 assert_eq!(agent.name(), "claude-code");
91 }
92
93 #[test]
94 fn by_name_resolves_oh_my_pi() {
95 let agent = by_name("oh-my-pi").expect("oh-my-pi is a registered agent");
96 assert_eq!(agent.name(), "oh-my-pi");
97 }
98
99 #[test]
100 fn by_name_returns_none_for_unknown() {
101 assert!(by_name("frobnicator").is_none());
102 }
103
104 #[test]
105 fn by_name_is_case_sensitive() {
106 assert!(by_name("Claude-Code").is_none());
107 }
108
109 #[test]
110 fn by_name_resolves_pi() {
111 let agent = by_name("pi").expect("pi is a registered agent");
112 assert_eq!(agent.name(), "pi");
113 }
114
115 #[test]
116 fn by_name_resolves_opencode() {
117 let agent = by_name("opencode").expect("opencode is a registered agent");
118 assert_eq!(agent.name(), "opencode");
119 }
120
121 #[test]
122 fn agent_name_returns_wire_string_for_each_variant() {
123 assert_eq!(AgentName::ClaudeCode.name(), "claude-code");
124 assert_eq!(AgentName::OhMyPi.name(), "oh-my-pi");
125 assert_eq!(AgentName::Pi.name(), "pi");
126 assert_eq!(AgentName::Opencode.name(), "opencode");
127 }
128
129 #[test]
130 fn agent_name_dispatch_returns_matching_trait_object() {
131 assert_eq!(AgentName::ClaudeCode.agent().name(), "claude-code");
132 assert_eq!(AgentName::OhMyPi.agent().name(), "oh-my-pi");
133 assert_eq!(AgentName::Pi.agent().name(), "pi");
134 assert_eq!(AgentName::Opencode.agent().name(), "opencode");
135 }
136
137 #[test]
138 fn agent_name_parses_kebab_case_via_value_enum() {
139 use clap::ValueEnum;
140 assert_eq!(
141 AgentName::from_str("claude-code", true).unwrap(),
142 AgentName::ClaudeCode,
143 );
144 assert_eq!(
145 AgentName::from_str("oh-my-pi", true).unwrap(),
146 AgentName::OhMyPi,
147 );
148 assert_eq!(AgentName::from_str("pi", true).unwrap(), AgentName::Pi);
149 assert_eq!(
150 AgentName::from_str("opencode", true).unwrap(),
151 AgentName::Opencode,
152 );
153 assert!(AgentName::from_str("frobnicator", true).is_err());
154 }
155
156 #[test]
157 fn extract_message_default_returns_none() {
158 struct NoopAgent;
160 impl Agent for NoopAgent {
161 fn name(&self) -> &'static str { "noop" }
162 fn extract_session_id(&self, _: &str) -> Option<String> { None }
163 }
164 assert!(NoopAgent.extract_message(r#"{"message":"hi"}"#).is_none());
165 }
166}