|
| 1 | +use acp_nats::AcpSessionId; |
| 2 | +use acp_nats::ext_method_name::ExtMethodName; |
| 3 | + |
| 4 | +const AGENT_MARKER: &str = ".agent."; |
| 5 | +const EXT_PREFIX: &str = "agent.ext."; |
| 6 | + |
| 7 | +#[derive(Debug, Clone, PartialEq, Eq)] |
| 8 | +pub enum AgentMethod { |
| 9 | + Initialize, |
| 10 | + Authenticate, |
| 11 | + SessionNew, |
| 12 | + SessionList, |
| 13 | + SessionLoad, |
| 14 | + SessionPrompt, |
| 15 | + SessionCancel, |
| 16 | + SessionSetMode, |
| 17 | + SessionSetConfigOption, |
| 18 | + SessionSetModel, |
| 19 | + SessionFork, |
| 20 | + SessionResume, |
| 21 | + SessionClose, |
| 22 | + Ext(String), |
| 23 | +} |
| 24 | + |
| 25 | +impl AgentMethod { |
| 26 | + pub fn is_session_scoped(&self) -> bool { |
| 27 | + !matches!( |
| 28 | + self, |
| 29 | + Self::Initialize |
| 30 | + | Self::Authenticate |
| 31 | + | Self::SessionNew |
| 32 | + | Self::SessionList |
| 33 | + | Self::Ext(_) |
| 34 | + ) |
| 35 | + } |
| 36 | + |
| 37 | + fn from_suffix(suffix: &str) -> Option<Self> { |
| 38 | + match suffix { |
| 39 | + "agent.initialize" => Some(Self::Initialize), |
| 40 | + "agent.authenticate" => Some(Self::Authenticate), |
| 41 | + "agent.session.new" => Some(Self::SessionNew), |
| 42 | + "agent.session.list" => Some(Self::SessionList), |
| 43 | + "agent.session.load" => Some(Self::SessionLoad), |
| 44 | + "agent.session.prompt" => Some(Self::SessionPrompt), |
| 45 | + "agent.session.cancel" => Some(Self::SessionCancel), |
| 46 | + "agent.session.set_mode" => Some(Self::SessionSetMode), |
| 47 | + "agent.session.set_config_option" => Some(Self::SessionSetConfigOption), |
| 48 | + "agent.session.set_model" => Some(Self::SessionSetModel), |
| 49 | + "agent.session.fork" => Some(Self::SessionFork), |
| 50 | + "agent.session.resume" => Some(Self::SessionResume), |
| 51 | + "agent.session.close" => Some(Self::SessionClose), |
| 52 | + other => { |
| 53 | + let ext_name = other.strip_prefix(EXT_PREFIX)?; |
| 54 | + ExtMethodName::new(ext_name).ok()?; |
| 55 | + Some(Self::Ext(ext_name.to_string())) |
| 56 | + } |
| 57 | + } |
| 58 | + } |
| 59 | +} |
| 60 | + |
| 61 | +#[derive(Debug)] |
| 62 | +pub struct ParsedAgentSubject { |
| 63 | + pub session_id: Option<AcpSessionId>, |
| 64 | + pub method: AgentMethod, |
| 65 | +} |
| 66 | + |
| 67 | +pub fn parse_agent_subject(subject: &str) -> Option<ParsedAgentSubject> { |
| 68 | + let agent_byte_pos = subject.rmatch_indices(AGENT_MARKER).next()?.0; |
| 69 | + |
| 70 | + let suffix = &subject[agent_byte_pos + 1..]; |
| 71 | + let method = AgentMethod::from_suffix(suffix)?; |
| 72 | + |
| 73 | + let session_id = if method.is_session_scoped() { |
| 74 | + let before_agent = &subject[..agent_byte_pos]; |
| 75 | + let session_dot = before_agent.rfind('.')?; |
| 76 | + let raw = &before_agent[session_dot + 1..]; |
| 77 | + Some(AcpSessionId::new(raw).ok()?) |
| 78 | + } else { |
| 79 | + None |
| 80 | + }; |
| 81 | + |
| 82 | + Some(ParsedAgentSubject { session_id, method }) |
| 83 | +} |
| 84 | + |
| 85 | +#[cfg(test)] |
| 86 | +mod tests { |
| 87 | + use super::*; |
| 88 | + |
| 89 | + #[test] |
| 90 | + fn parse_initialize() { |
| 91 | + let parsed = parse_agent_subject("acp.agent.initialize").unwrap(); |
| 92 | + assert!(parsed.session_id.is_none()); |
| 93 | + assert_eq!(parsed.method, AgentMethod::Initialize); |
| 94 | + } |
| 95 | + |
| 96 | + #[test] |
| 97 | + fn parse_authenticate() { |
| 98 | + let parsed = parse_agent_subject("acp.agent.authenticate").unwrap(); |
| 99 | + assert!(parsed.session_id.is_none()); |
| 100 | + assert_eq!(parsed.method, AgentMethod::Authenticate); |
| 101 | + } |
| 102 | + |
| 103 | + #[test] |
| 104 | + fn parse_session_new() { |
| 105 | + let parsed = parse_agent_subject("acp.agent.session.new").unwrap(); |
| 106 | + assert!(parsed.session_id.is_none()); |
| 107 | + assert_eq!(parsed.method, AgentMethod::SessionNew); |
| 108 | + } |
| 109 | + |
| 110 | + #[test] |
| 111 | + fn parse_session_list() { |
| 112 | + let parsed = parse_agent_subject("acp.agent.session.list").unwrap(); |
| 113 | + assert!(parsed.session_id.is_none()); |
| 114 | + assert_eq!(parsed.method, AgentMethod::SessionList); |
| 115 | + } |
| 116 | + |
| 117 | + #[test] |
| 118 | + fn parse_session_load() { |
| 119 | + let parsed = parse_agent_subject("acp.s1.agent.session.load").unwrap(); |
| 120 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 121 | + assert_eq!(parsed.method, AgentMethod::SessionLoad); |
| 122 | + } |
| 123 | + |
| 124 | + #[test] |
| 125 | + fn parse_session_prompt() { |
| 126 | + let parsed = parse_agent_subject("acp.s1.agent.session.prompt").unwrap(); |
| 127 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 128 | + assert_eq!(parsed.method, AgentMethod::SessionPrompt); |
| 129 | + } |
| 130 | + |
| 131 | + #[test] |
| 132 | + fn parse_session_cancel() { |
| 133 | + let parsed = parse_agent_subject("acp.s1.agent.session.cancel").unwrap(); |
| 134 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 135 | + assert_eq!(parsed.method, AgentMethod::SessionCancel); |
| 136 | + } |
| 137 | + |
| 138 | + #[test] |
| 139 | + fn parse_session_set_mode() { |
| 140 | + let parsed = parse_agent_subject("acp.s1.agent.session.set_mode").unwrap(); |
| 141 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 142 | + assert_eq!(parsed.method, AgentMethod::SessionSetMode); |
| 143 | + } |
| 144 | + |
| 145 | + #[test] |
| 146 | + fn parse_session_set_config_option() { |
| 147 | + let parsed = parse_agent_subject("acp.s1.agent.session.set_config_option").unwrap(); |
| 148 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 149 | + assert_eq!(parsed.method, AgentMethod::SessionSetConfigOption); |
| 150 | + } |
| 151 | + |
| 152 | + #[test] |
| 153 | + fn parse_session_set_model() { |
| 154 | + let parsed = parse_agent_subject("acp.s1.agent.session.set_model").unwrap(); |
| 155 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 156 | + assert_eq!(parsed.method, AgentMethod::SessionSetModel); |
| 157 | + } |
| 158 | + |
| 159 | + #[test] |
| 160 | + fn parse_session_fork() { |
| 161 | + let parsed = parse_agent_subject("acp.s1.agent.session.fork").unwrap(); |
| 162 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 163 | + assert_eq!(parsed.method, AgentMethod::SessionFork); |
| 164 | + } |
| 165 | + |
| 166 | + #[test] |
| 167 | + fn parse_session_resume() { |
| 168 | + let parsed = parse_agent_subject("acp.s1.agent.session.resume").unwrap(); |
| 169 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 170 | + assert_eq!(parsed.method, AgentMethod::SessionResume); |
| 171 | + } |
| 172 | + |
| 173 | + #[test] |
| 174 | + fn parse_session_close() { |
| 175 | + let parsed = parse_agent_subject("acp.s1.agent.session.close").unwrap(); |
| 176 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 177 | + assert_eq!(parsed.method, AgentMethod::SessionClose); |
| 178 | + } |
| 179 | + |
| 180 | + #[test] |
| 181 | + fn parse_ext_method() { |
| 182 | + let parsed = parse_agent_subject("acp.agent.ext.my_tool").unwrap(); |
| 183 | + assert!(parsed.session_id.is_none()); |
| 184 | + assert_eq!(parsed.method, AgentMethod::Ext("my_tool".to_string())); |
| 185 | + } |
| 186 | + |
| 187 | + #[test] |
| 188 | + fn parse_ext_dotted_namespace() { |
| 189 | + let parsed = parse_agent_subject("acp.agent.ext.vendor.operation").unwrap(); |
| 190 | + assert!(parsed.session_id.is_none()); |
| 191 | + assert_eq!( |
| 192 | + parsed.method, |
| 193 | + AgentMethod::Ext("vendor.operation".to_string()) |
| 194 | + ); |
| 195 | + } |
| 196 | + |
| 197 | + #[test] |
| 198 | + fn parse_custom_prefix() { |
| 199 | + let parsed = parse_agent_subject("myapp.agent.initialize").unwrap(); |
| 200 | + assert!(parsed.session_id.is_none()); |
| 201 | + assert_eq!(parsed.method, AgentMethod::Initialize); |
| 202 | + } |
| 203 | + |
| 204 | + #[test] |
| 205 | + fn parse_multi_part_prefix() { |
| 206 | + let parsed = parse_agent_subject("my.multi.prefix.s1.agent.session.load").unwrap(); |
| 207 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 208 | + assert_eq!(parsed.method, AgentMethod::SessionLoad); |
| 209 | + } |
| 210 | + |
| 211 | + #[test] |
| 212 | + fn parse_empty_returns_none() { |
| 213 | + assert!(parse_agent_subject("").is_none()); |
| 214 | + } |
| 215 | + |
| 216 | + #[test] |
| 217 | + fn parse_no_agent_marker_returns_none() { |
| 218 | + assert!(parse_agent_subject("acp.client.session.update").is_none()); |
| 219 | + } |
| 220 | + |
| 221 | + #[test] |
| 222 | + fn parse_unknown_method_returns_none() { |
| 223 | + assert!(parse_agent_subject("acp.agent.unknown.method").is_none()); |
| 224 | + } |
| 225 | + |
| 226 | + #[test] |
| 227 | + fn parse_invalid_session_id_returns_none() { |
| 228 | + assert!(parse_agent_subject("acp.sess*ion.agent.session.load").is_none()); |
| 229 | + } |
| 230 | + |
| 231 | + #[test] |
| 232 | + fn parse_ext_empty_name_returns_none() { |
| 233 | + assert!(parse_agent_subject("acp.agent.ext.").is_none()); |
| 234 | + } |
| 235 | + |
| 236 | + #[test] |
| 237 | + fn parse_ext_wildcard_returns_none() { |
| 238 | + assert!(parse_agent_subject("acp.agent.ext.*").is_none()); |
| 239 | + } |
| 240 | + |
| 241 | + #[test] |
| 242 | + fn parse_multi_dot_prefix_global_method_has_no_session() { |
| 243 | + let parsed = parse_agent_subject("my.multi.agent.initialize").unwrap(); |
| 244 | + assert!(parsed.session_id.is_none()); |
| 245 | + assert_eq!(parsed.method, AgentMethod::Initialize); |
| 246 | + } |
| 247 | + |
| 248 | + #[test] |
| 249 | + fn parse_prefix_containing_agent_word() { |
| 250 | + let parsed = parse_agent_subject("org.agent.app.agent.initialize").unwrap(); |
| 251 | + assert!(parsed.session_id.is_none()); |
| 252 | + assert_eq!(parsed.method, AgentMethod::Initialize); |
| 253 | + } |
| 254 | + |
| 255 | + #[test] |
| 256 | + fn parse_multi_dot_prefix_session_scoped() { |
| 257 | + let parsed = parse_agent_subject("my.multi.s1.agent.session.prompt").unwrap(); |
| 258 | + assert_eq!(parsed.session_id.unwrap().as_str(), "s1"); |
| 259 | + assert_eq!(parsed.method, AgentMethod::SessionPrompt); |
| 260 | + } |
| 261 | +} |
0 commit comments