Skip to main content

a2a_agents/core/
builder.rs

1//! Agent builder for declarative agent construction
2//!
3//! Provides a fluent API for building agents from configuration files
4//! or programmatically with minimal boilerplate.
5
6#[cfg(feature = "mcp-client")]
7use crate::core::McpClientManager;
8use crate::core::config::{AgentConfig, ConfigError, StorageConfig};
9use crate::core::runtime::AgentRuntime;
10use a2a_rs::domain::{
11    A2AError, Task, TaskArtifactUpdateEvent, TaskPushNotificationConfig, TaskState,
12    TaskStatusUpdateEvent,
13};
14use a2a_rs::port::{
15    AsyncMessageHandler, AsyncNotificationManager, AsyncStreamingHandler, AsyncTaskManager,
16    StreamingSubscriber, UpdateEvent,
17};
18use a2a_rs::{HttpPushNotificationSender, InMemoryTaskStorage};
19use async_trait::async_trait;
20use futures::Stream;
21use std::path::Path;
22use std::pin::Pin;
23use std::sync::Arc;
24#[cfg(feature = "mcp-client")]
25use tracing::info;
26
27#[cfg(feature = "sqlx")]
28use a2a_rs::adapter::storage::SqlxTaskStorage;
29
30/// Storage wrapper that can hold either in-memory or SQLx storage
31/// This allows us to return different storage types from the builder
32#[derive(Clone)]
33pub enum AutoStorage {
34    InMemory(InMemoryTaskStorage),
35    #[cfg(feature = "sqlx")]
36    Sqlx(SqlxTaskStorage),
37}
38
39#[async_trait]
40impl AsyncTaskManager for AutoStorage {
41    async fn create_task(&self, task_id: &str, context_id: &str) -> Result<Task, A2AError> {
42        match self {
43            AutoStorage::InMemory(s) => s.create_task(task_id, context_id).await,
44            #[cfg(feature = "sqlx")]
45            AutoStorage::Sqlx(s) => s.create_task(task_id, context_id).await,
46        }
47    }
48
49    async fn get_task(&self, task_id: &str, history_length: Option<u32>) -> Result<Task, A2AError> {
50        match self {
51            AutoStorage::InMemory(s) => s.get_task(task_id, history_length).await,
52            #[cfg(feature = "sqlx")]
53            AutoStorage::Sqlx(s) => s.get_task(task_id, history_length).await,
54        }
55    }
56
57    async fn update_task_status(
58        &self,
59        task_id: &str,
60        state: TaskState,
61        message: Option<a2a_rs::domain::Message>,
62    ) -> Result<Task, A2AError> {
63        match self {
64            AutoStorage::InMemory(s) => s.update_task_status(task_id, state, message).await,
65            #[cfg(feature = "sqlx")]
66            AutoStorage::Sqlx(s) => s.update_task_status(task_id, state, message).await,
67        }
68    }
69
70    async fn cancel_task(&self, task_id: &str) -> Result<Task, A2AError> {
71        match self {
72            AutoStorage::InMemory(s) => s.cancel_task(task_id).await,
73            #[cfg(feature = "sqlx")]
74            AutoStorage::Sqlx(s) => s.cancel_task(task_id).await,
75        }
76    }
77
78    async fn task_exists(&self, task_id: &str) -> Result<bool, A2AError> {
79        match self {
80            AutoStorage::InMemory(s) => s.task_exists(task_id).await,
81            #[cfg(feature = "sqlx")]
82            AutoStorage::Sqlx(s) => s.task_exists(task_id).await,
83        }
84    }
85}
86
87#[async_trait]
88impl AsyncNotificationManager for AutoStorage {
89    async fn set_task_notification(
90        &self,
91        config: &TaskPushNotificationConfig,
92    ) -> Result<TaskPushNotificationConfig, A2AError> {
93        match self {
94            AutoStorage::InMemory(s) => s.set_task_notification(config).await,
95            #[cfg(feature = "sqlx")]
96            AutoStorage::Sqlx(s) => s.set_task_notification(config).await,
97        }
98    }
99
100    async fn get_task_notification(
101        &self,
102        task_id: &str,
103    ) -> Result<TaskPushNotificationConfig, A2AError> {
104        match self {
105            AutoStorage::InMemory(s) => s.get_task_notification(task_id).await,
106            #[cfg(feature = "sqlx")]
107            AutoStorage::Sqlx(s) => s.get_task_notification(task_id).await,
108        }
109    }
110
111    async fn remove_task_notification(&self, task_id: &str) -> Result<(), A2AError> {
112        match self {
113            AutoStorage::InMemory(s) => s.remove_task_notification(task_id).await,
114            #[cfg(feature = "sqlx")]
115            AutoStorage::Sqlx(s) => s.remove_task_notification(task_id).await,
116        }
117    }
118}
119
120impl AutoStorage {
121    /// Create auto storage from server configuration
122    pub async fn from_config(config: &StorageConfig) -> Result<Self, BuildError> {
123        match config {
124            StorageConfig::InMemory => {
125                let push_sender = a2a_rs::adapter::HttpPushNotificationSender::new()
126                    .with_timeout(30)
127                    .with_max_retries(3);
128                let storage = a2a_rs::InMemoryTaskStorage::with_push_sender(push_sender);
129                Ok(AutoStorage::InMemory(storage))
130            }
131            #[cfg(feature = "sqlx")]
132            StorageConfig::Sqlx {
133                url,
134                enable_logging,
135                ..
136            } => {
137                if *enable_logging {
138                    tracing::info!("SQL query logging enabled");
139                }
140
141                let storage = a2a_rs::adapter::storage::SqlxTaskStorage::new(url)
142                    .await
143                    .map_err(|e| {
144                        BuildError::StorageError(format!("Failed to create SQLx storage: {}", e))
145                    })?;
146
147                Ok(AutoStorage::Sqlx(storage))
148            }
149            #[cfg(not(feature = "sqlx"))]
150            StorageConfig::Sqlx { .. } => Err(BuildError::StorageError(
151                "SQLx storage requested but 'sqlx' feature is not enabled".to_string(),
152            )),
153        }
154    }
155}
156
157#[async_trait]
158impl AsyncStreamingHandler for AutoStorage {
159    async fn add_status_subscriber(
160        &self,
161        task_id: &str,
162        subscriber: Box<dyn StreamingSubscriber<TaskStatusUpdateEvent> + Send + Sync>,
163    ) -> Result<String, A2AError> {
164        match self {
165            AutoStorage::InMemory(s) => s.add_status_subscriber(task_id, subscriber).await,
166            #[cfg(feature = "sqlx")]
167            AutoStorage::Sqlx(s) => s.add_status_subscriber(task_id, subscriber).await,
168        }
169    }
170
171    async fn add_artifact_subscriber(
172        &self,
173        task_id: &str,
174        subscriber: Box<dyn StreamingSubscriber<TaskArtifactUpdateEvent> + Send + Sync>,
175    ) -> Result<String, A2AError> {
176        match self {
177            AutoStorage::InMemory(s) => s.add_artifact_subscriber(task_id, subscriber).await,
178            #[cfg(feature = "sqlx")]
179            AutoStorage::Sqlx(s) => s.add_artifact_subscriber(task_id, subscriber).await,
180        }
181    }
182
183    async fn remove_subscription(&self, subscription_id: &str) -> Result<(), A2AError> {
184        match self {
185            AutoStorage::InMemory(s) => s.remove_subscription(subscription_id).await,
186            #[cfg(feature = "sqlx")]
187            AutoStorage::Sqlx(s) => s.remove_subscription(subscription_id).await,
188        }
189    }
190
191    async fn remove_task_subscribers(&self, task_id: &str) -> Result<(), A2AError> {
192        match self {
193            AutoStorage::InMemory(s) => s.remove_task_subscribers(task_id).await,
194            #[cfg(feature = "sqlx")]
195            AutoStorage::Sqlx(s) => s.remove_task_subscribers(task_id).await,
196        }
197    }
198
199    async fn get_subscriber_count(&self, task_id: &str) -> Result<usize, A2AError> {
200        match self {
201            AutoStorage::InMemory(s) => s.get_subscriber_count(task_id).await,
202            #[cfg(feature = "sqlx")]
203            AutoStorage::Sqlx(s) => s.get_subscriber_count(task_id).await,
204        }
205    }
206
207    async fn broadcast_status_update(
208        &self,
209        task_id: &str,
210        update: TaskStatusUpdateEvent,
211    ) -> Result<(), A2AError> {
212        match self {
213            AutoStorage::InMemory(s) => s.broadcast_status_update(task_id, update).await,
214            #[cfg(feature = "sqlx")]
215            AutoStorage::Sqlx(s) => s.broadcast_status_update(task_id, update).await,
216        }
217    }
218
219    async fn broadcast_artifact_update(
220        &self,
221        task_id: &str,
222        update: TaskArtifactUpdateEvent,
223    ) -> Result<(), A2AError> {
224        match self {
225            AutoStorage::InMemory(s) => s.broadcast_artifact_update(task_id, update).await,
226            #[cfg(feature = "sqlx")]
227            AutoStorage::Sqlx(s) => s.broadcast_artifact_update(task_id, update).await,
228        }
229    }
230
231    async fn status_update_stream(
232        &self,
233        task_id: &str,
234    ) -> Result<Pin<Box<dyn Stream<Item = Result<TaskStatusUpdateEvent, A2AError>> + Send>>, A2AError>
235    {
236        match self {
237            AutoStorage::InMemory(s) => s.status_update_stream(task_id).await,
238            #[cfg(feature = "sqlx")]
239            AutoStorage::Sqlx(s) => s.status_update_stream(task_id).await,
240        }
241    }
242
243    async fn artifact_update_stream(
244        &self,
245        task_id: &str,
246    ) -> Result<
247        Pin<Box<dyn Stream<Item = Result<TaskArtifactUpdateEvent, A2AError>> + Send>>,
248        A2AError,
249    > {
250        match self {
251            AutoStorage::InMemory(s) => s.artifact_update_stream(task_id).await,
252            #[cfg(feature = "sqlx")]
253            AutoStorage::Sqlx(s) => s.artifact_update_stream(task_id).await,
254        }
255    }
256
257    async fn combined_update_stream(
258        &self,
259        task_id: &str,
260    ) -> Result<Pin<Box<dyn Stream<Item = Result<UpdateEvent, A2AError>> + Send>>, A2AError> {
261        match self {
262            AutoStorage::InMemory(s) => s.combined_update_stream(task_id).await,
263            #[cfg(feature = "sqlx")]
264            AutoStorage::Sqlx(s) => s.combined_update_stream(task_id).await,
265        }
266    }
267}
268
269/// Builder for creating A2A agents with declarative configuration
270pub struct AgentBuilder<H = (), S = ()> {
271    config: AgentConfig,
272    handler: Option<H>,
273    storage: Option<S>,
274}
275
276impl AgentBuilder<(), ()> {
277    /// Create a new builder from a TOML configuration file
278    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
279        let config = AgentConfig::from_file(path)?;
280        Ok(Self {
281            config,
282            handler: None,
283            storage: None,
284        })
285    }
286
287    /// Create a new builder from a TOML string
288    pub fn from_toml(toml: &str) -> Result<Self, ConfigError> {
289        let config = AgentConfig::from_toml(toml)?;
290        Ok(Self {
291            config,
292            handler: None,
293            storage: None,
294        })
295    }
296
297    /// Create a new builder with programmatic configuration
298    pub fn new(config: AgentConfig) -> Self {
299        Self {
300            config,
301            handler: None,
302            storage: None,
303        }
304    }
305}
306
307impl<H, S> AgentBuilder<H, S> {
308    /// Set the message handler for this agent
309    pub fn with_handler<NewH>(self, handler: NewH) -> AgentBuilder<NewH, S>
310    where
311        NewH: AsyncMessageHandler + Clone + Send + Sync + 'static,
312    {
313        AgentBuilder {
314            config: self.config,
315            handler: Some(handler),
316            storage: self.storage,
317        }
318    }
319
320    /// Set custom storage for this agent
321    pub fn with_storage<NewS>(self, storage: NewS) -> AgentBuilder<H, NewS>
322    where
323        NewS: AsyncTaskManager + AsyncNotificationManager + Clone + Send + Sync + 'static,
324    {
325        AgentBuilder {
326            config: self.config,
327            handler: self.handler,
328            storage: Some(storage),
329        }
330    }
331
332    /// Access the configuration
333    pub fn config(&self) -> &AgentConfig {
334        &self.config
335    }
336
337    /// Modify the configuration
338    pub fn with_config<F>(mut self, f: F) -> Self
339    where
340        F: FnOnce(&mut AgentConfig),
341    {
342        f(&mut self.config);
343        self
344    }
345}
346
347impl<H, S> AgentBuilder<H, S>
348where
349    H: AsyncMessageHandler + Clone + Send + Sync + 'static,
350    S: AsyncTaskManager + AsyncNotificationManager + Clone + Send + Sync + 'static,
351{
352    /// Build the agent runtime
353    pub fn build(self) -> Result<AgentRuntime<H, S>, BuildError> {
354        let handler = self.handler.ok_or(BuildError::MissingHandler)?;
355        let storage = self.storage.ok_or(BuildError::MissingStorage)?;
356
357        Ok(AgentRuntime::new(
358            self.config,
359            Arc::new(handler),
360            Arc::new(storage),
361        ))
362    }
363}
364
365impl<H> AgentBuilder<H, ()>
366where
367    H: AsyncMessageHandler + Clone + Send + Sync + 'static,
368{
369    /// Create storage from the configuration
370    /// This is a convenience method that automatically creates the appropriate storage
371    /// based on what's configured in the TOML file
372    pub async fn build_with_auto_storage(self) -> Result<AgentRuntime<H, AutoStorage>, BuildError> {
373        let handler = self.handler.ok_or(BuildError::MissingHandler)?;
374
375        let storage = match &self.config.server.storage {
376            StorageConfig::InMemory => {
377                let push_sender = HttpPushNotificationSender::new()
378                    .with_timeout(30)
379                    .with_max_retries(3);
380                let storage = InMemoryTaskStorage::with_push_sender(push_sender);
381                AutoStorage::InMemory(storage)
382            }
383            #[cfg(feature = "sqlx")]
384            StorageConfig::Sqlx {
385                url,
386                enable_logging,
387                ..
388            } => {
389                if *enable_logging {
390                    tracing::info!("SQL query logging enabled");
391                }
392
393                let storage = SqlxTaskStorage::new(url).await.map_err(|e| {
394                    BuildError::StorageError(format!("Failed to create SQLx storage: {}", e))
395                })?;
396
397                AutoStorage::Sqlx(storage)
398            }
399            #[cfg(not(feature = "sqlx"))]
400            StorageConfig::Sqlx { .. } => {
401                return Err(BuildError::StorageError(
402                    "SQLx storage requested but 'sqlx' feature is not enabled".to_string(),
403                ));
404            }
405        };
406
407        // Initialize MCP client if configured
408        #[cfg(feature = "mcp-client")]
409        if self.config.features.mcp_client.enabled {
410            info!("Initializing MCP client...");
411            let mcp_client = McpClientManager::new();
412
413            // Initialize connections to configured servers
414            if let Err(e) = mcp_client
415                .initialize(&self.config.features.mcp_client)
416                .await
417            {
418                return Err(BuildError::RuntimeError(format!(
419                    "Failed to initialize MCP client: {}",
420                    e
421                )));
422            }
423
424            return Ok(AgentRuntime::with_mcp_client(
425                self.config,
426                Arc::new(handler),
427                Arc::new(storage),
428                mcp_client,
429            ));
430        }
431
432        Ok(AgentRuntime::new(
433            self.config,
434            Arc::new(handler),
435            Arc::new(storage),
436        ))
437    }
438
439    /// Create storage from configuration with custom migrations
440    /// This is useful when you need to run agent-specific database migrations
441    #[cfg(feature = "sqlx")]
442    pub async fn build_with_auto_storage_and_migrations(
443        self,
444        migrations: &'static [&'static str],
445    ) -> Result<AgentRuntime<H, AutoStorage>, BuildError> {
446        let handler = self.handler.ok_or(BuildError::MissingHandler)?;
447
448        let storage = match &self.config.server.storage {
449            StorageConfig::InMemory => {
450                tracing::warn!(
451                    "Migrations provided but using in-memory storage - migrations ignored"
452                );
453                let push_sender = HttpPushNotificationSender::new()
454                    .with_timeout(30)
455                    .with_max_retries(3);
456                let storage = InMemoryTaskStorage::with_push_sender(push_sender);
457                AutoStorage::InMemory(storage)
458            }
459            StorageConfig::Sqlx {
460                url,
461                enable_logging,
462                ..
463            } => {
464                if *enable_logging {
465                    tracing::info!("SQL query logging enabled");
466                }
467
468                let storage = SqlxTaskStorage::with_migrations(url, migrations)
469                    .await
470                    .map_err(|e| {
471                        BuildError::StorageError(format!("Failed to create SQLx storage: {}", e))
472                    })?;
473
474                AutoStorage::Sqlx(storage)
475            }
476        };
477
478        // Initialize MCP client if configured
479        #[cfg(feature = "mcp-client")]
480        if self.config.features.mcp_client.enabled {
481            info!("Initializing MCP client...");
482            let mcp_client = McpClientManager::new();
483
484            // Initialize connections to configured servers
485            if let Err(e) = mcp_client
486                .initialize(&self.config.features.mcp_client)
487                .await
488            {
489                return Err(BuildError::RuntimeError(format!(
490                    "Failed to initialize MCP client: {}",
491                    e
492                )));
493            }
494
495            return Ok(AgentRuntime::with_mcp_client(
496                self.config,
497                Arc::new(handler),
498                Arc::new(storage),
499                mcp_client,
500            ));
501        }
502
503        Ok(AgentRuntime::new(
504            self.config,
505            Arc::new(handler),
506            Arc::new(storage),
507        ))
508    }
509}
510
511/// Errors that can occur during agent building
512#[derive(Debug, thiserror::Error)]
513pub enum BuildError {
514    #[error("Handler must be set before building")]
515    MissingHandler,
516
517    #[error("Storage must be set before building")]
518    MissingStorage,
519
520    #[error("Configuration error: {0}")]
521    ConfigError(#[from] ConfigError),
522
523    #[error("Storage error: {0}")]
524    StorageError(String),
525
526    #[error("Runtime error: {0}")]
527    RuntimeError(String),
528}
529
530#[cfg(test)]
531mod tests {
532    use super::*;
533
534    #[test]
535    fn test_builder_from_toml() {
536        let toml = r#"
537            [agent]
538            name = "Test Agent"
539
540            [server]
541            http_port = 9000
542        "#;
543
544        let builder = AgentBuilder::from_toml(toml).unwrap();
545        assert_eq!(builder.config().agent.name, "Test Agent");
546        assert_eq!(builder.config().server.http_port, 9000);
547    }
548
549    #[test]
550    fn test_builder_config_modification() {
551        let toml = r#"
552            [agent]
553            name = "Test Agent"
554        "#;
555
556        let builder = AgentBuilder::from_toml(toml)
557            .unwrap()
558            .with_config(|config| {
559                config.server.http_port = 7000;
560            });
561
562        assert_eq!(builder.config().server.http_port, 7000);
563    }
564}