Skip to main content

symbi_runtime/reasoning/
agent_registry.rs

1//! Agent registry for multi-agent composition
2//!
3//! Maps agent names to their configurations and IDs, enabling
4//! runtime agent spawning and lifecycle management.
5
6use crate::reasoning::inference::InferenceProvider;
7use crate::types::AgentId;
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::Arc;
11use tokio::sync::RwLock;
12
13/// Configuration for a registered agent.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct RegisteredAgent {
16    /// Unique identifier.
17    pub agent_id: AgentId,
18    /// Human-readable name.
19    pub name: String,
20    /// System prompt for this agent.
21    pub system_prompt: String,
22    /// Tool names this agent has access to.
23    pub tools: Vec<String>,
24    /// Optional response format (e.g., "json", "text").
25    pub response_format: Option<String>,
26    /// When this agent was registered.
27    pub created_at: chrono::DateTime<chrono::Utc>,
28}
29
30/// Thread-safe registry of named agents.
31#[derive(Clone)]
32pub struct AgentRegistry {
33    agents: Arc<RwLock<HashMap<String, RegisteredAgent>>>,
34}
35
36impl Default for AgentRegistry {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl AgentRegistry {
43    /// Create a new empty registry.
44    pub fn new() -> Self {
45        Self {
46            agents: Arc::new(RwLock::new(HashMap::new())),
47        }
48    }
49
50    /// Spawn (register) a new agent.
51    pub async fn spawn_agent(
52        &self,
53        name: impl Into<String>,
54        system_prompt: impl Into<String>,
55        tools: Vec<String>,
56        response_format: Option<String>,
57    ) -> AgentId {
58        let name = name.into();
59        let agent_id = AgentId::new();
60
61        let agent = RegisteredAgent {
62            agent_id,
63            name: name.clone(),
64            system_prompt: system_prompt.into(),
65            tools,
66            response_format,
67            created_at: chrono::Utc::now(),
68        };
69
70        self.agents.write().await.insert(name, agent);
71        agent_id
72    }
73
74    /// Get a registered agent by name.
75    pub async fn get_agent(&self, name: &str) -> Option<RegisteredAgent> {
76        self.agents.read().await.get(name).cloned()
77    }
78
79    /// List all registered agents.
80    pub async fn list_agents(&self) -> Vec<RegisteredAgent> {
81        self.agents.read().await.values().cloned().collect()
82    }
83
84    /// Remove an agent by name.
85    pub async fn remove_agent(&self, name: &str) -> bool {
86        self.agents.write().await.remove(name).is_some()
87    }
88
89    /// Check if an agent exists.
90    pub async fn has_agent(&self, name: &str) -> bool {
91        self.agents.read().await.contains_key(name)
92    }
93
94    /// Send a message to an agent and get a response.
95    ///
96    /// Uses the agent's system prompt and the provided inference provider
97    /// to run a single-turn conversation.
98    pub async fn ask_agent(
99        &self,
100        name: &str,
101        message: &str,
102        provider: &dyn InferenceProvider,
103    ) -> Result<String, AgentRegistryError> {
104        let agent = self
105            .get_agent(name)
106            .await
107            .ok_or_else(|| AgentRegistryError::NotFound {
108                name: name.to_string(),
109            })?;
110
111        use crate::reasoning::conversation::{Conversation, ConversationMessage};
112        use crate::reasoning::inference::InferenceOptions;
113
114        let mut conv = Conversation::with_system(&agent.system_prompt);
115        conv.push(ConversationMessage::user(message));
116
117        let options = InferenceOptions::default();
118        let response = provider.complete(&conv, &options).await.map_err(|e| {
119            AgentRegistryError::InferenceError {
120                agent_name: name.to_string(),
121                message: e.to_string(),
122            }
123        })?;
124
125        Ok(response.content)
126    }
127}
128
129/// Errors from the agent registry.
130#[derive(Debug, thiserror::Error)]
131pub enum AgentRegistryError {
132    #[error("Agent '{name}' not found")]
133    NotFound { name: String },
134
135    #[error("Inference error for agent '{agent_name}': {message}")]
136    InferenceError { agent_name: String, message: String },
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[tokio::test]
144    async fn test_spawn_and_get_agent() {
145        let registry = AgentRegistry::new();
146
147        let id = registry
148            .spawn_agent(
149                "researcher",
150                "You are a researcher.",
151                vec!["search".into()],
152                None,
153            )
154            .await;
155
156        let agent = registry.get_agent("researcher").await.unwrap();
157        assert_eq!(agent.agent_id, id);
158        assert_eq!(agent.name, "researcher");
159        assert_eq!(agent.tools, vec!["search"]);
160    }
161
162    #[tokio::test]
163    async fn test_list_agents() {
164        let registry = AgentRegistry::new();
165
166        registry.spawn_agent("a", "Agent A", vec![], None).await;
167        registry.spawn_agent("b", "Agent B", vec![], None).await;
168
169        let agents = registry.list_agents().await;
170        assert_eq!(agents.len(), 2);
171    }
172
173    #[tokio::test]
174    async fn test_remove_agent() {
175        let registry = AgentRegistry::new();
176
177        registry
178            .spawn_agent("temp", "Temporary", vec![], None)
179            .await;
180        assert!(registry.has_agent("temp").await);
181
182        assert!(registry.remove_agent("temp").await);
183        assert!(!registry.has_agent("temp").await);
184    }
185
186    #[tokio::test]
187    async fn test_get_nonexistent() {
188        let registry = AgentRegistry::new();
189        assert!(registry.get_agent("nope").await.is_none());
190    }
191
192    #[tokio::test]
193    async fn test_spawn_replaces_existing() {
194        let registry = AgentRegistry::new();
195
196        let id1 = registry.spawn_agent("agent", "v1", vec![], None).await;
197        let id2 = registry.spawn_agent("agent", "v2", vec![], None).await;
198
199        assert_ne!(id1, id2);
200        let agent = registry.get_agent("agent").await.unwrap();
201        assert_eq!(agent.system_prompt, "v2");
202    }
203}