loa_core/
builder.rs

1//! Builder pattern for constructing Agent instances
2
3use crate::{agent::Agent, supervisor, Error, Result};
4use std::path::{Path, PathBuf};
5
6/// Builder for configuring and creating an Agent
7///
8/// # Example
9///
10/// ```no_run
11/// use elo::Agent;
12///
13/// # async fn example() -> anyhow::Result<()> {
14/// let agent = Agent::builder()
15///     .storage_path("/var/lib/loa")
16///     .identity_path("/etc/loa/agent_id.key")
17///     .dashboard_port(3000)
18///     .build()
19///     .await?;
20/// # Ok(())
21/// # }
22/// ```
23pub struct AgentBuilder {
24    storage_path: Option<PathBuf>,
25    identity_path: Option<PathBuf>,
26    dashboard_port: Option<u16>,
27    claim_token_id: Option<String>,
28}
29
30impl AgentBuilder {
31    /// Create a new agent builder with default settings
32    pub(crate) fn new() -> Self {
33        Self {
34            storage_path: None,
35            identity_path: None,
36            dashboard_port: None,
37            claim_token_id: None,
38        }
39    }
40
41    /// Set the storage path for databases and persistent data
42    ///
43    /// This is required. If not set, build() will return an error.
44    ///
45    /// # Example
46    ///
47    /// ```no_run
48    /// use elo::Agent;
49    ///
50    /// # async fn example() -> anyhow::Result<()> {
51    /// let agent = Agent::builder()
52    ///     .storage_path("/var/lib/loa")
53    ///     .build()
54    ///     .await?;
55    /// # Ok(())
56    /// # }
57    /// ```
58    pub fn storage_path<P: AsRef<Path>>(mut self, path: P) -> Self {
59        self.storage_path = Some(path.as_ref().to_path_buf());
60        self
61    }
62
63    /// Set the identity path for agent identity key
64    ///
65    /// If not specified, defaults to `{storage_path}/agent_id.key`
66    ///
67    /// # Example
68    ///
69    /// ```no_run
70    /// use elo::Agent;
71    ///
72    /// # async fn example() -> anyhow::Result<()> {
73    /// let agent = Agent::builder()
74    ///     .storage_path("/var/lib/loa")
75    ///     .identity_path("/etc/loa/agent_id.key")
76    ///     .build()
77    ///     .await?;
78    /// # Ok(())
79    /// # }
80    /// ```
81    pub fn identity_path<P: AsRef<Path>>(mut self, path: P) -> Self {
82        self.identity_path = Some(path.as_ref().to_path_buf());
83        self
84    }
85
86    /// Set the dashboard port
87    ///
88    /// Optional. If set, a dashboard will be served on this port (future feature).
89    ///
90    /// # Example
91    ///
92    /// ```no_run
93    /// use elo::Agent;
94    ///
95    /// # async fn example() -> anyhow::Result<()> {
96    /// let agent = Agent::builder()
97    ///     .storage_path("/var/lib/loa")
98    ///     .dashboard_port(3000)
99    ///     .build()
100    ///     .await?;
101    /// # Ok(())
102    /// # }
103    /// ```
104    pub fn dashboard_port(mut self, port: u16) -> Self {
105        self.dashboard_port = Some(port);
106        self
107    }
108
109    /// Claim this agent for a workspace using a claim token
110    ///
111    /// This allows immediate agent claiming during startup. The claim_token_id is sent
112    /// with the heartbeat for self-registration, linking the agent to a workspace.
113    ///
114    /// # Example
115    ///
116    /// ```no_run
117    /// use elo::Agent;
118    ///
119    /// # async fn example() -> anyhow::Result<()> {
120    /// let agent = Agent::builder()
121    ///     .storage_path("/var/lib/loa")
122    ///     .claim("550e8400-e29b-41d4-a716-446655440000")
123    ///     .build()
124    ///     .await?;
125    /// # Ok(())
126    /// # }
127    /// ```
128    pub fn claim<S: Into<String>>(mut self, claim_token_id: S) -> Self {
129        self.claim_token_id = Some(claim_token_id.into());
130        self
131    }
132
133    /// Build the agent and spawn all internal actors
134    ///
135    /// This will:
136    /// 1. Validate configuration
137    /// 2. Create storage directories if needed
138    /// 3. Spawn the root supervisor
139    /// 4. Spawn all core actors (currently empty)
140    /// 5. Return an Agent handle
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if:
145    /// - storage_path is not set
146    /// - storage directories cannot be created
147    /// - supervisor fails to spawn
148    ///
149    /// # Example
150    ///
151    /// ```no_run
152    /// use elo::Agent;
153    ///
154    /// # async fn example() -> anyhow::Result<()> {
155    /// let agent = Agent::builder()
156    ///     .storage_path("/var/lib/loa")
157    ///     .build()
158    ///     .await?;
159    ///
160    /// agent.run().await?;
161    /// # Ok(())
162    /// # }
163    /// ```
164    pub async fn build(self) -> Result<Agent> {
165        // Validate required fields
166        let storage_path = self
167            .storage_path
168            .ok_or_else(|| Error::Config("storage_path is required".to_string()))?;
169
170        // Apply defaults
171        let identity_path = self.identity_path.or_else(|| {
172            Some(storage_path.join("agent_id.key"))
173        });
174
175        tracing::debug!("Building agent...");
176        tracing::debug!("  Storage: {}", storage_path.display());
177        if let Some(ref identity_path) = identity_path {
178            tracing::debug!("  Identity: {}", identity_path.display());
179        }
180        if let Some(port) = self.dashboard_port {
181            tracing::debug!("  Dashboard: localhost:{}", port);
182        }
183
184        // Create storage directory if it doesn't exist
185        if !storage_path.exists() {
186            tracing::debug!("Creating storage directory: {}", storage_path.display());
187            std::fs::create_dir_all(&storage_path)?;
188        }
189
190        // Load or generate agent identity (wrapped in Arc for efficient sharing)
191        let identity_file = identity_path.as_ref().unwrap();
192        tracing::debug!("Loading agent identity from {}", identity_file.display());
193        let identity = std::sync::Arc::new(crate::AgentIdentity::from_file_or_generate_new(identity_file)?);
194        let peer_id = identity.peer_id().clone();
195        tracing::debug!("  Agent PeerId: {}", peer_id);
196
197        // Initialize global identity singleton for access from all actors
198        crate::identity::init_global_identity(identity.clone());
199
200        // Spawn root supervisor with shared identity, claim_token_id, and storage_path
201        tracing::debug!("Spawning root supervisor...");
202        let (supervisor, supervisor_handle) = supervisor::spawn_root(identity, self.claim_token_id, storage_path.clone()).await?;
203        tracing::debug!("  \u{2713} Root supervisor spawned");
204
205        tracing::info!("Agent ready (PeerId: {}, Storage: {})", peer_id, storage_path.display());
206
207        Ok(Agent::new(
208            storage_path,
209            identity_path,
210            self.dashboard_port,
211            supervisor,
212            supervisor_handle,
213        ))
214    }
215}