mail4agent_messenger_shell/
resolve.rs1use std::path::{Path, PathBuf};
7
8use m4a_agent::{AgentError, ResolvedSession, SessionResolver};
9
10fn found(session_id: &str, local: &Option<String>, source: &'static str) -> Option<ResolvedSession> {
11 Some(ResolvedSession { session_id: session_id.to_string(), local_session: local.clone(), source })
12}
13
14macro_rules! with_local {
15 ($t:ident) => {
16 impl $t {
17 pub fn with_local(mut self, local_session: impl Into<String>) -> Self {
19 self.local = Some(local_session.into());
20 self
21 }
22 }
23 };
24}
25
26pub struct ConfiguredResolver {
28 pub session_id: Option<String>,
29 local: Option<String>,
30}
31impl ConfiguredResolver {
32 pub fn new(session_id: Option<String>) -> Self {
33 Self { session_id: session_id.filter(|s| !s.is_empty()), local: None }
34 }
35}
36with_local!(ConfiguredResolver);
37impl SessionResolver for ConfiguredResolver {
38 fn name(&self) -> &'static str {
39 "configured"
40 }
41 fn resolve(&self) -> Result<Option<ResolvedSession>, AgentError> {
42 Ok(self.session_id.as_deref().and_then(|s| found(s, &self.local, "configured")))
43 }
44}
45
46pub struct AgentsDirResolver {
49 agents_dir: PathBuf,
50 agent_id: String,
51 aliases: Vec<(String, String)>,
52 local: Option<String>,
53}
54impl AgentsDirResolver {
55 pub fn new(agents_dir: impl Into<PathBuf>, agent_id: impl Into<String>, session_ids_env: &str) -> Self {
56 Self { agents_dir: agents_dir.into(), agent_id: agent_id.into(), aliases: crate::machine::parse_session_ids(session_ids_env), local: None }
57 }
58}
59with_local!(AgentsDirResolver);
60impl SessionResolver for AgentsDirResolver {
61 fn name(&self) -> &'static str {
62 "agents-dir"
63 }
64 fn resolve(&self) -> Result<Option<ResolvedSession>, AgentError> {
65 let mut all = match crate::machine::load_agents_dir(&self.agents_dir) {
66 Ok(a) => a,
67 Err(_) => return Ok(None),
68 };
69 all.retain(|s| s.agent_id.as_deref() == Some(self.agent_id.as_str()));
70 crate::machine::apply_session_ids(&mut all, &self.aliases);
71 Ok(all.first().and_then(|s| found(&s.session_id, &self.local, "agents-dir")))
72 }
73}
74
75#[cfg(feature = "wake-grok")]
77pub struct GrokActiveResolver {
78 index_text: String,
79 sessions_root: PathBuf,
80 cwd: String,
81 local: Option<String>,
82}
83#[cfg(feature = "wake-grok")]
84impl GrokActiveResolver {
85 pub fn new(index_text: impl Into<String>, sessions_root: impl Into<PathBuf>, cwd: impl Into<String>) -> Self {
86 Self { index_text: index_text.into(), sessions_root: sessions_root.into(), cwd: cwd.into(), local: None }
87 }
88}
89#[cfg(feature = "wake-grok")]
90with_local!(GrokActiveResolver);
91#[cfg(feature = "wake-grok")]
92impl SessionResolver for GrokActiveResolver {
93 fn name(&self) -> &'static str {
94 "grok-active"
95 }
96 fn resolve(&self) -> Result<Option<ResolvedSession>, AgentError> {
97 let heard = crate::grok_listen::hear(&self.index_text, &self.sessions_root).map_err(|e| AgentError::Identity(e.to_string()))?;
98 let mut rows = heard.iter().filter(|h| h.cwd == self.cwd);
99 let first = rows.next();
100 if rows.next().is_some() {
101 return Err(AgentError::Identity("two live Grok sessions share this working directory".into()));
102 }
103 Ok(first.and_then(|h| found(&h.session_id, &self.local, "grok-active")))
104 }
105}
106
107pub struct ProviderRecordResolver {
109 store_root: PathBuf,
110 provider: String,
111 vendor_session: String,
112 local: Option<String>,
113}
114impl ProviderRecordResolver {
115 pub fn new(store_root: &Path, provider: &str, vendor_session: &str) -> Self {
116 Self { store_root: store_root.to_path_buf(), provider: provider.into(), vendor_session: vendor_session.into(), local: None }
117 }
118}
119with_local!(ProviderRecordResolver);
120impl SessionResolver for ProviderRecordResolver {
121 fn name(&self) -> &'static str {
122 "provider-record"
123 }
124 fn resolve(&self) -> Result<Option<ResolvedSession>, AgentError> {
125 let live = crate::provider::registry::live_sessions(&self.store_root);
126 Ok(live.iter().find(|s| s.kind.provider.id() == self.provider && s.session_id == self.vendor_session).and_then(|s| found(&s.session_id, &self.local, "provider-record")))
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use m4a_agent::{BackendKind, IdentityStore, MemoryVault, ResolverChain};
134 use std::sync::Arc;
135
136 #[test]
137 fn the_sources_agree_through_one_chain_and_link_the_local_session() {
138 let root = tempfile::tempdir().unwrap();
139 let agents = root.path().join("agents");
140 std::fs::create_dir_all(agents.join("bot-7")).unwrap();
141 std::fs::write(agents.join("bot-7/profile.json"), r#"{"name":"Alice"}"#).unwrap();
142 let chain = ResolverChain::new()
144 .with(ConfiguredResolver::new(None))
145 .with(AgentsDirResolver::new(&agents, "bot-7", "bot-7=mail-7").with_local("local-7"))
146 .with(ConfiguredResolver::new(Some("mail-7".into())));
147 let r = chain.resolve().unwrap();
148 assert_eq!((r.session_id.as_str(), r.local_session.as_deref(), r.source), ("mail-7", Some("local-7"), "agents-dir"));
149 let ids = IdentityStore::new(Arc::new(MemoryVault::new()));
150 let id = chain.identity(&ids, BackendKind::Server, "https://p.example").unwrap();
151 assert_eq!((id.session_id.as_str(), id.local_session.as_deref()), ("mail-7", Some("local-7")));
152 assert!(ResolverChain::new().with(AgentsDirResolver::new(&agents, "nobody", "")).resolve().is_err());
154 let clash = ResolverChain::new().with(AgentsDirResolver::new(&agents, "bot-7", "")).with(ConfiguredResolver::new(Some("mail-7".into())));
156 assert!(clash.resolve().is_err(), "agent id bot-7 vs configured mail-7");
157 }
158}