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
6use crate::core::config::{AgentConfig, ConfigError, StorageConfig};
7use crate::core::server::AgentServer;
8use a2a_rs::domain::{A2AError, ContextId, Task, TaskId, TaskPushNotificationConfig, TaskState};
9use a2a_rs::port::{
10    AsyncMessageHandler, AsyncNotificationManager, AsyncStreamingHandler, AsyncTaskLifecycle,
11    AsyncTaskQuery,
12};
13use a2a_rs::{HttpPushNotificationSender, InMemoryTaskStorage};
14use async_trait::async_trait;
15use std::path::Path;
16use std::sync::Arc;
17
18#[cfg(feature = "sqlx")]
19use a2a_rs::adapter::storage::SqlxTaskStorage;
20
21/// Storage wrapper that can hold either in-memory or SQLx storage
22/// This allows us to return different storage types from the builder
23#[derive(Clone)]
24pub enum AutoStorage {
25    InMemory(InMemoryTaskStorage),
26    #[cfg(feature = "sqlx")]
27    Sqlx(SqlxTaskStorage),
28}
29
30#[async_trait]
31impl AsyncTaskLifecycle for AutoStorage {
32    async fn create(&self, id: &TaskId, context_id: &ContextId) -> Result<Task, A2AError> {
33        match self {
34            AutoStorage::InMemory(s) => s.create(id, context_id).await,
35            #[cfg(feature = "sqlx")]
36            AutoStorage::Sqlx(s) => s.create(id, context_id).await,
37        }
38    }
39
40    async fn get(&self, id: &TaskId, history_length: Option<u32>) -> Result<Task, A2AError> {
41        match self {
42            AutoStorage::InMemory(s) => s.get(id, history_length).await,
43            #[cfg(feature = "sqlx")]
44            AutoStorage::Sqlx(s) => s.get(id, history_length).await,
45        }
46    }
47
48    async fn update_status(
49        &self,
50        id: &TaskId,
51        state: TaskState,
52        message: Option<a2a_rs::domain::Message>,
53    ) -> Result<Task, A2AError> {
54        match self {
55            AutoStorage::InMemory(s) => s.update_status(id, state, message).await,
56            #[cfg(feature = "sqlx")]
57            AutoStorage::Sqlx(s) => s.update_status(id, state, message).await,
58        }
59    }
60
61    async fn cancel(&self, id: &TaskId) -> Result<Task, A2AError> {
62        match self {
63            AutoStorage::InMemory(s) => s.cancel(id).await,
64            #[cfg(feature = "sqlx")]
65            AutoStorage::Sqlx(s) => s.cancel(id).await,
66        }
67    }
68
69    async fn exists(&self, id: &TaskId) -> Result<bool, A2AError> {
70        match self {
71            AutoStorage::InMemory(s) => s.exists(id).await,
72            #[cfg(feature = "sqlx")]
73            AutoStorage::Sqlx(s) => s.exists(id).await,
74        }
75    }
76}
77
78#[async_trait]
79impl AsyncTaskQuery for AutoStorage {
80    async fn list(
81        &self,
82        params: &a2a_rs::domain::ListTasksParams,
83    ) -> Result<a2a_rs::domain::ListTasksResult, A2AError> {
84        match self {
85            AutoStorage::InMemory(s) => s.list(params).await,
86            #[cfg(feature = "sqlx")]
87            AutoStorage::Sqlx(s) => s.list(params).await,
88        }
89    }
90}
91
92#[async_trait]
93impl AsyncNotificationManager for AutoStorage {
94    async fn set_config(
95        &self,
96        config: &TaskPushNotificationConfig,
97    ) -> Result<TaskPushNotificationConfig, A2AError> {
98        match self {
99            AutoStorage::InMemory(s) => s.set_config(config).await,
100            #[cfg(feature = "sqlx")]
101            AutoStorage::Sqlx(s) => s.set_config(config).await,
102        }
103    }
104
105    async fn get_config(
106        &self,
107        params: &a2a_rs::domain::GetTaskPushNotificationConfigParams,
108    ) -> Result<TaskPushNotificationConfig, A2AError> {
109        match self {
110            AutoStorage::InMemory(s) => s.get_config(params).await,
111            #[cfg(feature = "sqlx")]
112            AutoStorage::Sqlx(s) => s.get_config(params).await,
113        }
114    }
115
116    async fn list_configs(
117        &self,
118        params: &a2a_rs::domain::ListTaskPushNotificationConfigsParams,
119    ) -> Result<Vec<TaskPushNotificationConfig>, A2AError> {
120        match self {
121            AutoStorage::InMemory(s) => s.list_configs(params).await,
122            #[cfg(feature = "sqlx")]
123            AutoStorage::Sqlx(s) => s.list_configs(params).await,
124        }
125    }
126
127    async fn delete_config(
128        &self,
129        params: &a2a_rs::domain::DeleteTaskPushNotificationConfigParams,
130    ) -> Result<(), A2AError> {
131        match self {
132            AutoStorage::InMemory(s) => s.delete_config(params).await,
133            #[cfg(feature = "sqlx")]
134            AutoStorage::Sqlx(s) => s.delete_config(params).await,
135        }
136    }
137}
138
139impl AutoStorage {
140    /// Create auto storage from server configuration
141    pub async fn from_config(config: &StorageConfig) -> Result<Self, BuildError> {
142        match config {
143            StorageConfig::InMemory => {
144                let push_sender = a2a_rs::adapter::HttpPushNotificationSender::new()
145                    .with_timeout(30)
146                    .with_max_retries(3);
147                let storage = a2a_rs::InMemoryTaskStorage::with_push_sender(push_sender);
148                Ok(AutoStorage::InMemory(storage))
149            }
150            #[cfg(feature = "sqlx")]
151            StorageConfig::Sqlx {
152                url,
153                enable_logging,
154                ..
155            } => {
156                if *enable_logging {
157                    tracing::info!("SQL query logging enabled");
158                }
159
160                let storage = a2a_rs::adapter::storage::SqlxTaskStorage::new(url)
161                    .await
162                    .map_err(|e| {
163                        BuildError::StorageError(format!("Failed to create SQLx storage: {}", e))
164                    })?;
165
166                Ok(AutoStorage::Sqlx(storage))
167            }
168            #[cfg(not(feature = "sqlx"))]
169            StorageConfig::Sqlx { .. } => Err(BuildError::StorageError(
170                "SQLx storage requested but 'sqlx' feature is not enabled".to_string(),
171            )),
172        }
173    }
174
175    /// Hand out the inner store's push notifier (shares its config registry).
176    pub fn push_notifier(&self) -> Arc<dyn a2a_rs::port::AsyncPushNotifier> {
177        match self {
178            AutoStorage::InMemory(s) => s.push_notifier(),
179            #[cfg(feature = "sqlx")]
180            AutoStorage::Sqlx(s) => s.push_notifier(),
181        }
182    }
183}
184
185/// Builder for creating A2A agents with declarative configuration
186pub struct AgentBuilder<H = (), S = ()> {
187    config: AgentConfig,
188    handler: Option<H>,
189    storage: Option<S>,
190    streaming: Option<Arc<dyn AsyncStreamingHandler>>,
191}
192
193impl AgentBuilder<(), ()> {
194    /// Create a new builder from a TOML configuration file
195    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, ConfigError> {
196        let config = AgentConfig::from_file(path)?;
197        Ok(Self {
198            config,
199            handler: None,
200            storage: None,
201            streaming: None,
202        })
203    }
204
205    /// Create a new builder from a TOML string
206    pub fn from_toml(toml: &str) -> Result<Self, ConfigError> {
207        let config = AgentConfig::from_toml(toml)?;
208        Ok(Self {
209            config,
210            handler: None,
211            storage: None,
212            streaming: None,
213        })
214    }
215
216    /// Create a new builder with programmatic configuration
217    pub fn new(config: AgentConfig) -> Self {
218        Self {
219            config,
220            handler: None,
221            storage: None,
222            streaming: None,
223        }
224    }
225}
226
227impl<H, S> AgentBuilder<H, S> {
228    /// Set the message handler for this agent
229    pub fn with_handler<NewH>(self, handler: NewH) -> AgentBuilder<NewH, S>
230    where
231        NewH: AsyncMessageHandler + Clone + Send + Sync + 'static,
232    {
233        AgentBuilder {
234            config: self.config,
235            handler: Some(handler),
236            storage: self.storage,
237            streaming: self.streaming,
238        }
239    }
240
241    /// Set custom storage for this agent
242    pub fn with_storage<NewS>(self, storage: NewS) -> AgentBuilder<H, NewS>
243    where
244        NewS: AsyncTaskLifecycle
245            + AsyncTaskQuery
246            + AsyncNotificationManager
247            + Clone
248            + Send
249            + Sync
250            + 'static,
251    {
252        AgentBuilder {
253            config: self.config,
254            handler: self.handler,
255            storage: Some(storage),
256            streaming: self.streaming,
257        }
258    }
259
260    /// Attach a shared streaming backend for real-time updates.
261    ///
262    /// Pass the *same* [`AsyncStreamingHandler`] instance your handler
263    /// broadcasts to (clones of an `InMemoryStreamingHandler` share their
264    /// subscriber registry). The built [`AgentServer`] injects it into the
265    /// transport so `tasks/subscribe` SSE streams observe those broadcasts —
266    /// without it, the transport defaults to a no-op and updates never reach
267    /// clients.
268    pub fn with_streaming(mut self, streaming: impl AsyncStreamingHandler + 'static) -> Self {
269        self.streaming = Some(Arc::new(streaming));
270        self
271    }
272
273    /// Access the configuration
274    pub fn config(&self) -> &AgentConfig {
275        &self.config
276    }
277
278    /// Build this agent's [`AgentCard`](a2a_rs::domain::AgentCard) from its
279    /// configuration, without starting a server. Used to self-register the
280    /// agent with an [`AgentRegistry`](crate::registry::AgentRegistry) before it
281    /// runs, so peers can discover it by skill.
282    pub async fn agent_card(&self) -> Result<a2a_rs::domain::AgentCard, a2a_rs::domain::A2AError> {
283        use a2a_rs::services::AgentInfoProvider;
284        crate::core::server::agent_info_from_config(&self.config, self.config.agent_url())
285            .get_agent_card()
286            .await
287    }
288
289    /// Modify the configuration
290    pub fn with_config<F>(mut self, f: F) -> Self
291    where
292        F: FnOnce(&mut AgentConfig),
293    {
294        f(&mut self.config);
295        self
296    }
297}
298
299impl<H, S> AgentBuilder<H, S>
300where
301    H: AsyncMessageHandler + Clone + Send + Sync + 'static,
302    S: AsyncTaskLifecycle
303        + AsyncTaskQuery
304        + AsyncNotificationManager
305        + Clone
306        + Send
307        + Sync
308        + 'static,
309{
310    /// Build the agent runtime
311    pub fn build(self) -> Result<AgentServer<H, S>, BuildError> {
312        let handler = self.handler.ok_or(BuildError::MissingHandler)?;
313        let storage = self.storage.ok_or(BuildError::MissingStorage)?;
314
315        let mut runtime = AgentServer::new(self.config, Arc::new(handler), Arc::new(storage));
316        if let Some(streaming) = self.streaming {
317            runtime = runtime.with_streaming(streaming);
318        }
319        Ok(runtime)
320    }
321}
322
323impl<H> AgentBuilder<H, ()>
324where
325    H: AsyncMessageHandler + Clone + Send + Sync + 'static,
326{
327    /// Create storage from the configuration
328    /// This is a convenience method that automatically creates the appropriate storage
329    /// based on what's configured in the TOML file
330    pub async fn build_with_auto_storage(self) -> Result<AgentServer<H, AutoStorage>, BuildError> {
331        let handler = self.handler.ok_or(BuildError::MissingHandler)?;
332        let streaming = self.streaming;
333
334        let storage = match &self.config.server.storage {
335            StorageConfig::InMemory => {
336                let push_sender = HttpPushNotificationSender::new()
337                    .with_timeout(30)
338                    .with_max_retries(3);
339                let storage = InMemoryTaskStorage::with_push_sender(push_sender);
340                AutoStorage::InMemory(storage)
341            }
342            #[cfg(feature = "sqlx")]
343            StorageConfig::Sqlx {
344                url,
345                enable_logging,
346                ..
347            } => {
348                if *enable_logging {
349                    tracing::info!("SQL query logging enabled");
350                }
351
352                let storage = SqlxTaskStorage::new(url).await.map_err(|e| {
353                    BuildError::StorageError(format!("Failed to create SQLx storage: {}", e))
354                })?;
355
356                AutoStorage::Sqlx(storage)
357            }
358            #[cfg(not(feature = "sqlx"))]
359            StorageConfig::Sqlx { .. } => {
360                return Err(BuildError::StorageError(
361                    "SQLx storage requested but 'sqlx' feature is not enabled".to_string(),
362                ));
363            }
364        };
365
366        let mut runtime = AgentServer::new(self.config, Arc::new(handler), Arc::new(storage));
367        if let Some(streaming) = streaming {
368            runtime = runtime.with_streaming(streaming);
369        }
370        Ok(runtime)
371    }
372
373    /// Create storage from configuration with custom migrations
374    /// This is useful when you need to run agent-specific database migrations
375    #[cfg(feature = "sqlx")]
376    pub async fn build_with_auto_storage_and_migrations(
377        self,
378        migrations: &'static [&'static str],
379    ) -> Result<AgentServer<H, AutoStorage>, BuildError> {
380        let handler = self.handler.ok_or(BuildError::MissingHandler)?;
381        let streaming = self.streaming;
382
383        let storage = match &self.config.server.storage {
384            StorageConfig::InMemory => {
385                tracing::warn!(
386                    "Migrations provided but using in-memory storage - migrations ignored"
387                );
388                let push_sender = HttpPushNotificationSender::new()
389                    .with_timeout(30)
390                    .with_max_retries(3);
391                let storage = InMemoryTaskStorage::with_push_sender(push_sender);
392                AutoStorage::InMemory(storage)
393            }
394            StorageConfig::Sqlx {
395                url,
396                enable_logging,
397                ..
398            } => {
399                if *enable_logging {
400                    tracing::info!("SQL query logging enabled");
401                }
402
403                let storage = SqlxTaskStorage::with_migrations(url, migrations)
404                    .await
405                    .map_err(|e| {
406                        BuildError::StorageError(format!("Failed to create SQLx storage: {}", e))
407                    })?;
408
409                AutoStorage::Sqlx(storage)
410            }
411        };
412
413        let mut runtime = AgentServer::new(self.config, Arc::new(handler), Arc::new(storage));
414        if let Some(streaming) = streaming {
415            runtime = runtime.with_streaming(streaming);
416        }
417        Ok(runtime)
418    }
419}
420
421/// Errors that can occur during agent building
422#[derive(Debug, thiserror::Error)]
423pub enum BuildError {
424    #[error("Handler must be set before building")]
425    MissingHandler,
426
427    #[error("Storage must be set before building")]
428    MissingStorage,
429
430    #[error("Configuration error: {0}")]
431    ConfigError(#[from] ConfigError),
432
433    #[error("Storage error: {0}")]
434    StorageError(String),
435
436    #[error("Runtime error: {0}")]
437    RuntimeError(String),
438}
439
440#[cfg(test)]
441mod tests {
442    use super::*;
443
444    #[test]
445    fn test_builder_from_toml() {
446        let toml = r#"
447            [agent]
448            name = "Test Agent"
449
450            [server]
451            http_port = 9000
452        "#;
453
454        let builder = AgentBuilder::from_toml(toml).unwrap();
455        assert_eq!(builder.config().agent.name, "Test Agent");
456        assert_eq!(builder.config().server.http_port, 9000);
457    }
458
459    #[test]
460    fn test_builder_config_modification() {
461        let toml = r#"
462            [agent]
463            name = "Test Agent"
464        "#;
465
466        let builder = AgentBuilder::from_toml(toml)
467            .unwrap()
468            .with_config(|config| {
469                config.server.http_port = 7000;
470            });
471
472        assert_eq!(builder.config().server.http_port, 7000);
473    }
474}