1use std::collections::{HashMap, HashSet};
4use std::sync::Arc;
5
6use async_trait::async_trait;
7use chrono::{DateTime, Utc};
8use parking_lot::RwLock;
9use serde::{Deserialize, Serialize};
10use tracing::{debug, info, warn};
11
12use crate::spec::AgentSpec;
13use crate::{Agent, RuntimeAgent, TurnActorContext};
14use ai_agents_core::{AgentError, AgentResponse, Result};
15use ai_agents_observability::{current_observation_context, with_observation_context};
16
17use super::spawner::SpawnedAgent;
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SpawnedAgentInfo {
22 pub id: String,
23 pub name: String,
24 pub spawned_at: DateTime<Utc>,
25}
26
27pub struct AgentRegistry {
29 agents: RwLock<HashMap<String, Arc<SpawnedAgent>>>,
30 hooks: Option<Arc<dyn RegistryHooks>>,
31 send_with_context: bool,
33}
34
35impl AgentRegistry {
36 pub fn new() -> Self {
37 Self {
38 agents: RwLock::new(HashMap::new()),
39 hooks: None,
40 send_with_context: true,
41 }
42 }
43
44 pub fn with_hooks(mut self, hooks: Arc<dyn RegistryHooks>) -> Self {
46 self.hooks = Some(hooks);
47 self
48 }
49
50 pub fn with_send_context(mut self, enabled: bool) -> Self {
52 self.send_with_context = enabled;
53 self
54 }
55
56 pub async fn register(&self, agent: SpawnedAgent) -> Result<()> {
58 self.register_batch(vec![agent]).await
59 }
60
61 pub async fn register_batch(&self, batch: Vec<SpawnedAgent>) -> Result<()> {
63 let hook_entries = {
64 let mut agents = self.agents.write();
65 let mut batch_ids = HashSet::with_capacity(batch.len());
66
67 for agent in &batch {
68 if !batch_ids.insert(agent.id.clone()) {
69 return Err(AgentError::Config(format!(
70 "Duplicate agent ID in registration batch: {}",
71 agent.id
72 )));
73 }
74 if agents.contains_key(&agent.id) {
75 return Err(AgentError::Config(format!(
76 "Agent already registered: {}",
77 agent.id
78 )));
79 }
80 }
81
82 let hook_entries: Vec<(String, AgentSpec)> = batch
83 .iter()
84 .map(|agent| (agent.id.clone(), agent.spec.clone()))
85 .collect();
86 for agent in batch {
87 agents.insert(agent.id.clone(), Arc::new(agent));
88 }
89 hook_entries
90 };
91
92 for (id, spec) in hook_entries {
93 info!(agent_id = %id, "Agent registered in registry");
94 if let Some(ref hooks) = self.hooks {
95 hooks.on_agent_spawned(&id, &spec).await;
96 }
97 }
98 Ok(())
99 }
100
101 pub(crate) async fn reconcile(
102 &self,
103 target_ids: &HashSet<String>,
104 additions: Vec<SpawnedAgent>,
105 ) -> Result<()> {
106 let (removed, added_hooks) = {
107 let mut agents = self.agents.write();
108 let mut addition_ids = HashSet::with_capacity(additions.len());
109 for addition in &additions {
110 if !target_ids.contains(&addition.id) {
111 return Err(AgentError::Config(format!(
112 "Restored agent is absent from target topology: {}",
113 addition.id
114 )));
115 }
116 if !addition_ids.insert(addition.id.clone()) || agents.contains_key(&addition.id) {
117 return Err(AgentError::Config(format!(
118 "Agent already registered during topology restore: {}",
119 addition.id
120 )));
121 }
122 }
123 for id in target_ids {
124 if !agents.contains_key(id) && !addition_ids.contains(id) {
125 return Err(AgentError::Config(format!(
126 "Target topology has no retained or staged agent: {}",
127 id
128 )));
129 }
130 }
131
132 let mut next = agents.clone();
136 let removed_ids = next
137 .keys()
138 .filter(|id| !target_ids.contains(*id))
139 .cloned()
140 .collect::<Vec<_>>();
141 let removed = removed_ids
142 .into_iter()
143 .filter_map(|id| next.remove(&id))
144 .collect::<Vec<_>>();
145 let added_hooks = additions
146 .iter()
147 .map(|agent| (agent.id.clone(), agent.spec.clone()))
148 .collect::<Vec<_>>();
149 for addition in additions {
150 next.insert(addition.id.clone(), Arc::new(addition));
151 }
152 *agents = next;
153 (removed, added_hooks)
154 };
155
156 for agent in removed {
157 agent.release_capacity();
158 info!(agent_id = %agent.id, "Agent removed during topology restore");
159 if let Some(ref hooks) = self.hooks {
160 hooks.on_agent_removed(&agent.id).await;
161 }
162 }
163 for (id, spec) in added_hooks {
164 info!(agent_id = %id, "Agent registered during topology restore");
165 if let Some(ref hooks) = self.hooks {
166 hooks.on_agent_spawned(&id, &spec).await;
167 }
168 }
169 Ok(())
170 }
171
172 pub fn get(&self, id: &str) -> Option<Arc<RuntimeAgent>> {
174 let agents = self.agents.read();
175 agents.get(id).map(|sa| Arc::clone(&sa.agent))
176 }
177
178 pub fn get_spawned(&self, id: &str) -> Option<Arc<SpawnedAgent>> {
180 let agents = self.agents.read();
181 agents.get(id).cloned()
182 }
183
184 pub fn list(&self) -> Vec<SpawnedAgentInfo> {
186 let agents = self.agents.read();
187 agents
188 .values()
189 .map(|sa| SpawnedAgentInfo {
190 id: sa.id.clone(),
191 name: sa.spec.name.clone(),
192 spawned_at: sa.spawned_at,
193 })
194 .collect()
195 }
196
197 pub fn list_with_specs(&self) -> Vec<ai_agents_core::SpawnedAgentEntry> {
199 let agents = self.agents.read();
200 agents
201 .values()
202 .filter_map(|sa| {
203 let spec_yaml = match serde_yaml::to_string(&sa.spec) {
204 Ok(y) => y,
205 Err(e) => {
206 warn!(agent_id = %sa.id, error = %e, "Failed to serialize agent spec");
207 return None;
208 }
209 };
210 Some(ai_agents_core::SpawnedAgentEntry {
211 id: sa.id.clone(),
212 name: sa.spec.name.clone(),
213 spec_yaml,
214 })
215 })
216 .collect()
217 }
218
219 pub async fn remove(&self, id: &str) -> Option<Arc<SpawnedAgent>> {
221 let removed = {
222 let mut agents = self.agents.write();
223 agents.remove(id)
224 };
225 if let Some(agent) = removed.as_ref() {
226 agent.release_capacity();
228 info!(agent_id = %id, "Agent removed from registry");
229 if let Some(ref hooks) = self.hooks {
230 hooks.on_agent_removed(id).await;
231 }
232 } else {
233 debug!(agent_id = %id, "Attempted to remove non-existent agent");
234 }
235 removed
236 }
237
238 pub async fn send(&self, from: &str, to: &str, message: &str) -> Result<AgentResponse> {
240 self.send_inner(from, to, message, None).await
241 }
242
243 pub async fn send_with_actor_context(
245 &self,
246 from: &str,
247 to: &str,
248 message: &str,
249 actor_context: TurnActorContext,
250 ) -> Result<AgentResponse> {
251 self.send_inner(from, to, message, Some(actor_context))
252 .await
253 }
254
255 async fn send_inner(
256 &self,
257 from: &str,
258 to: &str,
259 message: &str,
260 actor_context: Option<TurnActorContext>,
261 ) -> Result<AgentResponse> {
262 let target = {
263 let agents = self.agents.read();
265 agents.get(to).map(|sa| Arc::clone(&sa.agent))
266 };
267 let target =
268 target.ok_or_else(|| AgentError::Other(format!("Target agent not found: {}", to)))?;
269
270 if let Some(ref hooks) = self.hooks {
271 hooks.on_message_sent(from, to, message).await;
272 }
273
274 let formatted = if self.send_with_context {
275 format!("[From {}]: {}", from, message)
276 } else {
277 message.to_string()
278 };
279
280 debug!(from = %from, to = %to, has_actor_context = actor_context.is_some(), "Sending inter-agent message");
281 if let Some(context) = actor_context {
282 target.chat_with_actor_context(&formatted, context).await
283 } else {
284 target.chat(&formatted).await
285 }
286 }
287
288 pub async fn broadcast(
292 &self,
293 from: &str,
294 message: &str,
295 ) -> Vec<(String, Result<AgentResponse>)> {
296 self.broadcast_inner(from, message, None).await
297 }
298
299 pub async fn broadcast_with_actor_context(
301 &self,
302 from: &str,
303 message: &str,
304 actor_context: TurnActorContext,
305 ) -> Vec<(String, Result<AgentResponse>)> {
306 self.broadcast_inner(from, message, Some(actor_context))
307 .await
308 }
309
310 async fn broadcast_inner(
311 &self,
312 from: &str,
313 message: &str,
314 actor_context: Option<TurnActorContext>,
315 ) -> Vec<(String, Result<AgentResponse>)> {
316 let targets: Vec<(String, Arc<RuntimeAgent>)> = {
317 let agents = self.agents.read();
318 agents
319 .iter()
320 .filter(|(id, _)| id.as_str() != from)
321 .map(|(id, sa)| (id.clone(), Arc::clone(&sa.agent)))
322 .collect()
323 };
324
325 if targets.is_empty() {
326 return Vec::new();
327 }
328
329 let formatted = if self.send_with_context {
330 format!("[From {}]: {}", from, message)
331 } else {
332 message.to_string()
333 };
334
335 debug!(
336 from = %from,
337 target_count = targets.len(),
338 has_actor_context = actor_context.is_some(),
339 "Broadcasting message"
340 );
341
342 let mut handles = Vec::with_capacity(targets.len());
343 let observation_context = current_observation_context();
344 for (id, agent) in targets {
345 let msg = formatted.clone();
346 let context = actor_context.clone();
347 let observation_context = observation_context.clone();
348 handles.push(tokio::spawn(async move {
349 let run = async move {
350 if let Some(context) = context {
351 agent.chat_with_actor_context(&msg, context).await
352 } else {
353 agent.chat(&msg).await
354 }
355 };
356 let result = if let Some(context) = observation_context {
357 with_observation_context(context, run).await
358 } else {
359 run.await
360 };
361 (id, result)
362 }));
363 }
364
365 let mut results = Vec::new();
366 for handle in handles {
367 match handle.await {
368 Ok((id, res)) => results.push((id, res)),
369 Err(e) => {
370 warn!(error = %e, "Broadcast task panicked");
371 }
372 }
373 }
374 results
375 }
376
377 pub fn count(&self) -> usize {
379 self.agents.read().len()
380 }
381
382 pub fn contains(&self, id: &str) -> bool {
384 self.agents.read().contains_key(id)
385 }
386}
387
388impl Default for AgentRegistry {
389 fn default() -> Self {
390 Self::new()
391 }
392}
393
394impl std::fmt::Debug for AgentRegistry {
396 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
397 let count = self.agents.read().len();
398 f.debug_struct("AgentRegistry")
399 .field("agent_count", &count)
400 .field("send_with_context", &self.send_with_context)
401 .field("has_hooks", &self.hooks.is_some())
402 .finish()
403 }
404}
405
406#[async_trait]
408pub trait RegistryHooks: Send + Sync {
409 async fn on_agent_spawned(&self, _id: &str, _spec: &AgentSpec) {}
411
412 async fn on_agent_removed(&self, _id: &str) {}
414
415 async fn on_message_sent(&self, _from: &str, _to: &str, _message: &str) {}
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use crate::AgentBuilder;
423 use ai_agents_core::{
424 ChatMessage, FinishReason, LLMChunk, LLMConfig, LLMError, LLMFeature, LLMProvider,
425 LLMResponse,
426 };
427 use ai_agents_llm::LLMRegistry;
428 use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
429 use std::sync::{Mutex, Weak};
430
431 struct EchoProvider;
432
433 #[async_trait]
434 impl LLMProvider for EchoProvider {
435 async fn complete(
436 &self,
437 messages: &[ChatMessage],
438 _config: Option<&LLMConfig>,
439 ) -> std::result::Result<LLMResponse, LLMError> {
440 let last = messages
441 .last()
442 .map(|m| m.content.clone())
443 .unwrap_or_default();
444 Ok(LLMResponse::new(
445 format!("Echo: {}", last),
446 FinishReason::Stop,
447 ))
448 }
449
450 async fn complete_stream(
451 &self,
452 _messages: &[ChatMessage],
453 _config: Option<&LLMConfig>,
454 ) -> std::result::Result<
455 Box<dyn futures::Stream<Item = std::result::Result<LLMChunk, LLMError>> + Unpin + Send>,
456 LLMError,
457 > {
458 Err(LLMError::Other("not implemented".into()))
459 }
460
461 fn provider_name(&self) -> &str {
462 "echo"
463 }
464
465 fn supports(&self, _feature: LLMFeature) -> bool {
466 false
467 }
468 }
469
470 fn make_test_agent(name: &str) -> RuntimeAgent {
471 let mut registry = LLMRegistry::new();
472 registry.register("default", Arc::new(EchoProvider));
473
474 AgentBuilder::new()
475 .system_prompt(format!("You are {}.", name))
476 .llm_registry(registry)
477 .build()
478 .unwrap()
479 }
480
481 fn make_spawned(id: &str) -> SpawnedAgent {
482 let agent = make_test_agent(id);
483 SpawnedAgent::untracked(
484 id.to_string(),
485 agent,
486 AgentSpec {
487 name: id.to_string(),
488 ..AgentSpec::default()
489 },
490 )
491 }
492
493 #[tokio::test]
494 async fn test_register_and_get() {
495 let registry = AgentRegistry::new();
496 registry.register(make_spawned("agent_a")).await.unwrap();
497
498 assert!(registry.get("agent_a").is_some());
499 assert!(registry.get("agent_b").is_none());
500 assert_eq!(registry.count(), 1);
501 }
502
503 #[tokio::test]
504 async fn test_duplicate_register() {
505 let registry = AgentRegistry::new();
506 registry.register(make_spawned("dup")).await.unwrap();
507 let result = registry.register(make_spawned("dup")).await;
508 assert!(result.is_err());
509 }
510
511 #[tokio::test]
512 async fn test_register_batch_inserts_all_agents() {
513 let registry = AgentRegistry::new();
514 registry
515 .register_batch(vec![make_spawned("a"), make_spawned("b")])
516 .await
517 .unwrap();
518
519 assert!(registry.contains("a"));
520 assert!(registry.contains("b"));
521 assert_eq!(registry.count(), 2);
522 }
523
524 #[tokio::test]
525 async fn test_register_batch_rejects_duplicate_ids_without_inserting() {
526 let registry = AgentRegistry::new();
527 let result = registry
528 .register_batch(vec![make_spawned("dup"), make_spawned("dup")])
529 .await;
530
531 assert!(result.is_err());
532 assert!(!registry.contains("dup"));
533 assert_eq!(registry.count(), 0);
534 }
535
536 #[tokio::test]
537 async fn test_register_batch_rejects_existing_collision_without_inserting() {
538 let registry = AgentRegistry::new();
539 registry.register(make_spawned("existing")).await.unwrap();
540
541 let result = registry
542 .register_batch(vec![make_spawned("new"), make_spawned("existing")])
543 .await;
544
545 assert!(result.is_err());
546 assert!(!registry.contains("new"));
547 assert_eq!(registry.count(), 1);
548 }
549
550 #[tokio::test]
551 async fn reconcile_commits_additions_retained_agents_and_removals_together() {
552 let registry = AgentRegistry::new();
553 registry
554 .register_batch(vec![make_spawned("a"), make_spawned("b")])
555 .await
556 .unwrap();
557 let retained = registry.get("b").unwrap();
558
559 registry
560 .reconcile(
561 &HashSet::from(["b".to_string(), "c".to_string()]),
562 vec![make_spawned("c")],
563 )
564 .await
565 .unwrap();
566
567 assert!(!registry.contains("a"));
568 assert!(Arc::ptr_eq(®istry.get("b").unwrap(), &retained));
569 assert!(registry.contains("c"));
570 assert_eq!(registry.count(), 2);
571
572 registry
573 .reconcile(&HashSet::new(), Vec::new())
574 .await
575 .unwrap();
576 assert_eq!(registry.count(), 0);
577 }
578
579 #[tokio::test]
580 async fn reconcile_validation_failure_preserves_prior_topology() {
581 let registry = AgentRegistry::new();
582 registry
583 .register_batch(vec![make_spawned("a"), make_spawned("b")])
584 .await
585 .unwrap();
586 let before_a = registry.get("a").unwrap();
587 let before_b = registry.get("b").unwrap();
588
589 let result = registry
590 .reconcile(
591 &HashSet::from(["a".to_string(), "missing".to_string()]),
592 Vec::new(),
593 )
594 .await;
595
596 assert!(result.is_err());
597 assert!(Arc::ptr_eq(®istry.get("a").unwrap(), &before_a));
598 assert!(Arc::ptr_eq(®istry.get("b").unwrap(), &before_b));
599 assert_eq!(registry.count(), 2);
600 }
601
602 #[tokio::test]
603 async fn test_list_and_remove() {
604 let registry = AgentRegistry::new();
605 registry.register(make_spawned("a")).await.unwrap();
606 registry.register(make_spawned("b")).await.unwrap();
607
608 assert_eq!(registry.list().len(), 2);
609
610 let removed = registry.remove("a").await;
611 assert!(removed.is_some());
612 assert_eq!(registry.count(), 1);
613 assert!(registry.get("a").is_none());
614 }
615
616 #[tokio::test]
617 async fn test_send_agent_message() {
618 let registry = AgentRegistry::new();
619 registry.register(make_spawned("sender")).await.unwrap();
620 registry.register(make_spawned("receiver")).await.unwrap();
621
622 let response = registry.send("sender", "receiver", "hello").await.unwrap();
623 assert!(response.content.contains("hello"));
624 }
625
626 #[tokio::test]
627 async fn test_send_to_missing() {
628 let registry = AgentRegistry::new();
629 registry.register(make_spawned("sender")).await.unwrap();
630
631 let result = registry.send("sender", "nobody", "hello").await;
632 assert!(result.is_err());
633 }
634
635 #[tokio::test]
636 async fn test_broadcast() {
637 let registry = AgentRegistry::new();
638 registry
639 .register(make_spawned("broadcaster"))
640 .await
641 .unwrap();
642 registry.register(make_spawned("listener_1")).await.unwrap();
643 registry.register(make_spawned("listener_2")).await.unwrap();
644
645 let results = registry.broadcast("broadcaster", "hey everyone").await;
646 assert_eq!(results.len(), 2);
648 for (_, res) in &results {
649 assert!(res.is_ok());
650 }
651 }
652
653 #[tokio::test]
654 async fn test_batch_hooks_run_only_after_successful_commit() {
655 struct CommitObservingHooks {
656 registry: Mutex<Option<Weak<AgentRegistry>>>,
657 spawned: AtomicU32,
658 observed_full_batch: AtomicBool,
659 }
660
661 #[async_trait]
662 impl RegistryHooks for CommitObservingHooks {
663 async fn on_agent_spawned(&self, _id: &str, _spec: &AgentSpec) {
664 self.spawned.fetch_add(1, Ordering::Relaxed);
665 let registry = self
666 .registry
667 .lock()
668 .unwrap()
669 .as_ref()
670 .unwrap()
671 .upgrade()
672 .unwrap();
673 if registry.contains("a") && registry.contains("b") {
674 self.observed_full_batch.store(true, Ordering::Relaxed);
675 }
676 }
677 }
678
679 let hooks = Arc::new(CommitObservingHooks {
680 registry: Mutex::new(None),
681 spawned: AtomicU32::new(0),
682 observed_full_batch: AtomicBool::new(false),
683 });
684 let registry = Arc::new(AgentRegistry::new().with_hooks(hooks.clone()));
685 *hooks.registry.lock().unwrap() = Some(Arc::downgrade(®istry));
686
687 assert!(
688 registry
689 .register_batch(vec![make_spawned("dup"), make_spawned("dup")])
690 .await
691 .is_err()
692 );
693 assert_eq!(hooks.spawned.load(Ordering::Relaxed), 0);
694
695 registry
696 .register_batch(vec![make_spawned("a"), make_spawned("b")])
697 .await
698 .unwrap();
699 assert_eq!(registry.count(), 2);
700 assert_eq!(hooks.spawned.load(Ordering::Relaxed), 2);
701 assert!(hooks.observed_full_batch.load(Ordering::Relaxed));
702 }
703
704 #[tokio::test]
705 async fn test_hooks() {
706 struct CountingHooks {
707 spawned: AtomicU32,
708 removed: AtomicU32,
709 sent: AtomicU32,
710 }
711
712 #[async_trait]
713 impl RegistryHooks for CountingHooks {
714 async fn on_agent_spawned(&self, _id: &str, _spec: &AgentSpec) {
715 self.spawned.fetch_add(1, Ordering::Relaxed);
716 }
717 async fn on_agent_removed(&self, _id: &str) {
718 self.removed.fetch_add(1, Ordering::Relaxed);
719 }
720 async fn on_message_sent(&self, _from: &str, _to: &str, _msg: &str) {
721 self.sent.fetch_add(1, Ordering::Relaxed);
722 }
723 }
724
725 let hooks = Arc::new(CountingHooks {
726 spawned: AtomicU32::new(0),
727 removed: AtomicU32::new(0),
728 sent: AtomicU32::new(0),
729 });
730
731 let registry = AgentRegistry::new().with_hooks(hooks.clone());
732 registry.register(make_spawned("h1")).await.unwrap();
733 registry.register(make_spawned("h2")).await.unwrap();
734 assert_eq!(hooks.spawned.load(Ordering::Relaxed), 2);
735
736 registry.send("h1", "h2", "ping").await.unwrap();
737 assert_eq!(hooks.sent.load(Ordering::Relaxed), 1);
738
739 registry.remove("h1").await;
740 assert_eq!(hooks.removed.load(Ordering::Relaxed), 1);
741 }
742
743 #[tokio::test]
744 async fn test_contains() {
745 let registry = AgentRegistry::new();
746 assert!(!registry.contains("x"));
747 registry.register(make_spawned("x")).await.unwrap();
748 assert!(registry.contains("x"));
749 }
750
751 #[tokio::test]
752 async fn test_send_without_context() {
753 let registry = AgentRegistry::new().with_send_context(false);
754 registry.register(make_spawned("a")).await.unwrap();
755 registry.register(make_spawned("b")).await.unwrap();
756
757 let response = registry.send("a", "b", "raw msg").await.unwrap();
758 assert!(response.content.contains("raw msg"));
760 assert!(!response.content.contains("[From"));
761 }
762}