tirea_agentos/composition/registry/
agent.rs1use super::sorted_registry_ids;
2use super::traits::{AgentRegistry, AgentRegistryError};
3use crate::composition::AgentDefinition;
4use std::collections::HashMap;
5use std::sync::{Arc, RwLock};
6
7#[derive(Debug, Clone, Default)]
8pub struct InMemoryAgentRegistry {
9 agents: HashMap<String, AgentDefinition>,
10}
11
12impl InMemoryAgentRegistry {
13 pub fn new() -> Self {
14 Self::default()
15 }
16
17 pub fn len(&self) -> usize {
18 self.agents.len()
19 }
20
21 pub fn is_empty(&self) -> bool {
22 self.agents.is_empty()
23 }
24
25 pub fn get(&self, id: &str) -> Option<AgentDefinition> {
26 self.agents.get(id).cloned()
27 }
28
29 pub fn ids(&self) -> impl Iterator<Item = &String> {
30 self.agents.keys()
31 }
32
33 pub fn register(
34 &mut self,
35 agent_id: impl Into<String>,
36 mut def: AgentDefinition,
37 ) -> Result<(), AgentRegistryError> {
38 let agent_id = agent_id.into();
39 if self.agents.contains_key(&agent_id) {
40 return Err(AgentRegistryError::AgentIdConflict(agent_id));
41 }
42 def.id = agent_id.clone();
44 self.agents.insert(agent_id, def);
45 Ok(())
46 }
47
48 pub fn upsert(&mut self, agent_id: impl Into<String>, mut def: AgentDefinition) {
49 let agent_id = agent_id.into();
50 def.id = agent_id.clone();
51 self.agents.insert(agent_id, def);
52 }
53
54 pub fn extend_upsert(&mut self, defs: HashMap<String, AgentDefinition>) {
55 for (id, def) in defs {
56 self.upsert(id, def);
57 }
58 }
59
60 pub fn extend_registry(&mut self, other: &dyn AgentRegistry) -> Result<(), AgentRegistryError> {
61 for (id, def) in other.snapshot() {
62 self.register(id, def)?;
63 }
64 Ok(())
65 }
66}
67
68impl AgentRegistry for InMemoryAgentRegistry {
69 fn len(&self) -> usize {
70 self.len()
71 }
72
73 fn get(&self, id: &str) -> Option<AgentDefinition> {
74 self.get(id)
75 }
76
77 fn ids(&self) -> Vec<String> {
78 sorted_registry_ids(&self.agents)
79 }
80
81 fn snapshot(&self) -> HashMap<String, AgentDefinition> {
82 self.agents.clone()
83 }
84}
85
86#[derive(Clone, Default)]
87pub struct CompositeAgentRegistry {
88 registries: Vec<Arc<dyn AgentRegistry>>,
89 cached_snapshot: Arc<RwLock<HashMap<String, AgentDefinition>>>,
90}
91
92impl std::fmt::Debug for CompositeAgentRegistry {
93 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
94 let snapshot = match self.cached_snapshot.read() {
95 Ok(guard) => guard,
96 Err(poisoned) => poisoned.into_inner(),
97 };
98 f.debug_struct("CompositeAgentRegistry")
99 .field("registries", &self.registries.len())
100 .field("len", &snapshot.len())
101 .finish()
102 }
103}
104
105impl CompositeAgentRegistry {
106 pub fn try_new(
107 regs: impl IntoIterator<Item = Arc<dyn AgentRegistry>>,
108 ) -> Result<Self, AgentRegistryError> {
109 let registries: Vec<Arc<dyn AgentRegistry>> = regs.into_iter().collect();
110 let merged = Self::merge_snapshots(®istries)?;
111 Ok(Self {
112 registries,
113 cached_snapshot: Arc::new(RwLock::new(merged)),
114 })
115 }
116
117 fn merge_snapshots(
118 registries: &[Arc<dyn AgentRegistry>],
119 ) -> Result<HashMap<String, AgentDefinition>, AgentRegistryError> {
120 let mut merged = InMemoryAgentRegistry::new();
121 for reg in registries {
122 merged.extend_registry(reg.as_ref())?;
123 }
124 Ok(merged.snapshot())
125 }
126
127 fn refresh_snapshot(&self) -> Result<HashMap<String, AgentDefinition>, AgentRegistryError> {
128 Self::merge_snapshots(&self.registries)
129 }
130
131 fn read_cached_snapshot(&self) -> HashMap<String, AgentDefinition> {
132 match self.cached_snapshot.read() {
133 Ok(guard) => guard.clone(),
134 Err(poisoned) => poisoned.into_inner().clone(),
135 }
136 }
137
138 fn write_cached_snapshot(&self, snapshot: HashMap<String, AgentDefinition>) {
139 match self.cached_snapshot.write() {
140 Ok(mut guard) => *guard = snapshot,
141 Err(poisoned) => *poisoned.into_inner() = snapshot,
142 };
143 }
144}
145
146impl AgentRegistry for CompositeAgentRegistry {
147 fn len(&self) -> usize {
148 self.snapshot().len()
149 }
150
151 fn get(&self, id: &str) -> Option<AgentDefinition> {
152 self.snapshot().get(id).cloned()
153 }
154
155 fn ids(&self) -> Vec<String> {
156 let snapshot = self.snapshot();
157 sorted_registry_ids(&snapshot)
158 }
159
160 fn snapshot(&self) -> HashMap<String, AgentDefinition> {
161 match self.refresh_snapshot() {
162 Ok(snapshot) => {
163 self.write_cached_snapshot(snapshot.clone());
164 snapshot
165 }
166 Err(_) => self.read_cached_snapshot(),
167 }
168 }
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 #[derive(Default)]
176 struct MutableAgentRegistry {
177 agents: RwLock<HashMap<String, AgentDefinition>>,
178 }
179
180 impl MutableAgentRegistry {
181 fn replace_ids(&self, ids: &[&str]) {
182 let mut map = HashMap::new();
183 for id in ids {
184 map.insert((*id).to_string(), AgentDefinition::new("gpt-4o-mini"));
185 }
186 match self.agents.write() {
187 Ok(mut guard) => *guard = map,
188 Err(poisoned) => *poisoned.into_inner() = map,
189 }
190 }
191 }
192
193 impl AgentRegistry for MutableAgentRegistry {
194 fn len(&self) -> usize {
195 self.snapshot().len()
196 }
197
198 fn get(&self, id: &str) -> Option<AgentDefinition> {
199 self.snapshot().get(id).cloned()
200 }
201
202 fn ids(&self) -> Vec<String> {
203 let mut ids: Vec<String> = self.snapshot().keys().cloned().collect();
204 ids.sort();
205 ids
206 }
207
208 fn snapshot(&self) -> HashMap<String, AgentDefinition> {
209 match self.agents.read() {
210 Ok(guard) => guard.clone(),
211 Err(poisoned) => poisoned.into_inner().clone(),
212 }
213 }
214 }
215
216 #[test]
217 fn composite_agent_registry_reads_live_updates_from_source_registries() {
218 let dynamic = Arc::new(MutableAgentRegistry::default());
219 dynamic.replace_ids(&["agent_a"]);
220
221 let mut static_registry = InMemoryAgentRegistry::new();
222 static_registry.upsert("agent_static", AgentDefinition::new("gpt-4o-mini"));
223
224 let composite = CompositeAgentRegistry::try_new(vec![
225 dynamic.clone() as Arc<dyn AgentRegistry>,
226 Arc::new(static_registry) as Arc<dyn AgentRegistry>,
227 ])
228 .expect("compose registries");
229
230 assert!(composite.ids().contains(&"agent_a".to_string()));
231 assert!(composite.ids().contains(&"agent_static".to_string()));
232
233 dynamic.replace_ids(&["agent_a", "agent_b"]);
234 let ids = composite.ids();
235 assert!(ids.contains(&"agent_a".to_string()));
236 assert!(ids.contains(&"agent_b".to_string()));
237 assert!(ids.contains(&"agent_static".to_string()));
238 }
239
240 #[test]
241 fn composite_agent_registry_keeps_last_good_snapshot_on_runtime_conflict() {
242 let reg_a = Arc::new(MutableAgentRegistry::default());
243 reg_a.replace_ids(&["agent_a"]);
244
245 let reg_b = Arc::new(MutableAgentRegistry::default());
246 reg_b.replace_ids(&["agent_b"]);
247
248 let composite = CompositeAgentRegistry::try_new(vec![
249 reg_a.clone() as Arc<dyn AgentRegistry>,
250 reg_b.clone() as Arc<dyn AgentRegistry>,
251 ])
252 .expect("compose registries");
253
254 let initial_ids = composite.ids();
255 assert_eq!(
256 initial_ids,
257 vec!["agent_a".to_string(), "agent_b".to_string()]
258 );
259
260 reg_b.replace_ids(&["agent_a"]);
261 assert_eq!(composite.ids(), initial_ids);
262 assert!(composite.get("agent_b").is_some());
263 }
264}