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