Skip to main content

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    workspace_id: Option<String>,
28    api_url: Option<String>,
29}
30
31impl AgentBuilder {
32    /// Create a new agent builder with default settings
33    pub(crate) fn new() -> Self {
34        Self {
35            storage_path: None,
36            identity_path: None,
37            dashboard_port: None,
38            workspace_id: None,
39            api_url: None,
40        }
41    }
42
43    /// Set the storage path for databases and persistent data
44    ///
45    /// This is required. If not set, build() will return an error.
46    ///
47    /// # Example
48    ///
49    /// ```no_run
50    /// use elo::Agent;
51    ///
52    /// # async fn example() -> anyhow::Result<()> {
53    /// let agent = Agent::builder()
54    ///     .storage_path("/var/lib/loa")
55    ///     .build()
56    ///     .await?;
57    /// # Ok(())
58    /// # }
59    /// ```
60    pub fn storage_path<P: AsRef<Path>>(mut self, path: P) -> Self {
61        self.storage_path = Some(path.as_ref().to_path_buf());
62        self
63    }
64
65    /// Set the identity path for agent identity key
66    ///
67    /// If not specified, defaults to `{storage_path}/agent_id.key`
68    ///
69    /// # Example
70    ///
71    /// ```no_run
72    /// use elo::Agent;
73    ///
74    /// # async fn example() -> anyhow::Result<()> {
75    /// let agent = Agent::builder()
76    ///     .storage_path("/var/lib/loa")
77    ///     .identity_path("/etc/loa/agent_id.key")
78    ///     .build()
79    ///     .await?;
80    /// # Ok(())
81    /// # }
82    /// ```
83    pub fn identity_path<P: AsRef<Path>>(mut self, path: P) -> Self {
84        self.identity_path = Some(path.as_ref().to_path_buf());
85        self
86    }
87
88    /// Set the dashboard port
89    ///
90    /// Optional. If set, a dashboard will be served on this port (future feature).
91    ///
92    /// # Example
93    ///
94    /// ```no_run
95    /// use elo::Agent;
96    ///
97    /// # async fn example() -> anyhow::Result<()> {
98    /// let agent = Agent::builder()
99    ///     .storage_path("/var/lib/loa")
100    ///     .dashboard_port(3000)
101    ///     .build()
102    ///     .await?;
103    /// # Ok(())
104    /// # }
105    /// ```
106    pub fn dashboard_port(mut self, port: u16) -> Self {
107        self.dashboard_port = Some(port);
108        self
109    }
110
111    /// Claim this agent for a workspace using a workspace ID
112    ///
113    /// This allows immediate agent claiming during startup. The workspace_id is sent
114    /// during registration, and the server returns a unique claim_token that the agent
115    /// persists for future registrations.
116    ///
117    /// - First registration: workspace_id → server creates claim_token → agent saves it
118    /// - Subsequent registrations: agent sends claim_token from file
119    /// - Transfer: agent sends both claim_token AND new workspace_id
120    ///
121    /// # Example
122    ///
123    /// ```no_run
124    /// use elo::Agent;
125    ///
126    /// # async fn example() -> anyhow::Result<()> {
127    /// let agent = Agent::builder()
128    ///     .storage_path("/var/lib/loa")
129    ///     .claim("j57abc123def456...")  // Convex workspace ID
130    ///     .build()
131    ///     .await?;
132    /// # Ok(())
133    /// # }
134    /// ```
135    pub fn claim<S: Into<String>>(mut self, workspace_id: S) -> Self {
136        self.workspace_id = Some(workspace_id.into());
137        self
138    }
139
140    /// Set a custom API URL for the backend
141    ///
142    /// If not specified, uses the default production URL (https://api.loa.sh).
143    /// Use this for local development or custom deployments.
144    ///
145    /// # Example
146    ///
147    /// ```no_run
148    /// use elo::Agent;
149    ///
150    /// # async fn example() -> anyhow::Result<()> {
151    /// let agent = Agent::builder()
152    ///     .storage_path("/var/lib/loa")
153    ///     .api_url("http://localhost:8787")
154    ///     .build()
155    ///     .await?;
156    /// # Ok(())
157    /// # }
158    /// ```
159    pub fn api_url<S: Into<String>>(mut self, url: S) -> Self {
160        self.api_url = Some(url.into());
161        self
162    }
163
164    /// Build the agent and spawn all internal actors
165    ///
166    /// This will:
167    /// 1. Validate configuration
168    /// 2. Create storage directories if needed
169    /// 3. Spawn the root supervisor
170    /// 4. Spawn all core actors (currently empty)
171    /// 5. Return an Agent handle
172    ///
173    /// # Errors
174    ///
175    /// Returns an error if:
176    /// - storage_path is not set
177    /// - storage directories cannot be created
178    /// - supervisor fails to spawn
179    ///
180    /// # Example
181    ///
182    /// ```no_run
183    /// use elo::Agent;
184    ///
185    /// # async fn example() -> anyhow::Result<()> {
186    /// let agent = Agent::builder()
187    ///     .storage_path("/var/lib/loa")
188    ///     .build()
189    ///     .await?;
190    ///
191    /// agent.run().await?;
192    /// # Ok(())
193    /// # }
194    /// ```
195    pub async fn build(self) -> Result<Agent> {
196        // Validate required fields
197        let storage_path = self
198            .storage_path
199            .ok_or_else(|| Error::Config("storage_path is required".to_string()))?;
200
201        // Apply defaults
202        let identity_path = self.identity_path.or_else(|| {
203            Some(storage_path.join("agent_id.key"))
204        });
205
206        tracing::debug!("Building agent...");
207        tracing::debug!("  Storage: {}", storage_path.display());
208        if let Some(ref identity_path) = identity_path {
209            tracing::debug!("  Identity: {}", identity_path.display());
210        }
211        if let Some(port) = self.dashboard_port {
212            tracing::debug!("  Dashboard: localhost:{}", port);
213        }
214
215        // Create storage directory if it doesn't exist
216        if !storage_path.exists() {
217            tracing::debug!("Creating storage directory: {}", storage_path.display());
218            std::fs::create_dir_all(&storage_path)?;
219        }
220
221        // Load or generate agent identity (wrapped in Arc for efficient sharing)
222        tracing::debug!("Loading agent identity from {}", storage_path.display());
223        let identity = std::sync::Arc::new(crate::AgentIdentity::from_storage_or_generate_new(&storage_path)?);
224        let peer_id = identity.peer_id().clone();
225        tracing::debug!("  Agent PeerId: {}", peer_id);
226
227        // Initialize global identity singleton for access from all actors
228        crate::identity::init_global_identity(identity.clone());
229
230        // Initialize global API URL if custom URL is configured (e.g., --dev flag)
231        // This allows constants::api_url() and constants::ingest_url() to use the runtime URL
232        if let Some(ref url) = self.api_url {
233            crate::constants::init_global_api_url(url.clone());
234            tracing::debug!("  API URL: {}", url);
235        }
236
237        // Spawn root supervisor with shared identity, workspace_id, api_url, and storage_path
238        tracing::debug!("Spawning root supervisor...");
239        let (supervisor, supervisor_handle) = supervisor::spawn_root(identity, self.workspace_id, self.api_url, storage_path.clone()).await?;
240        tracing::debug!("  \u{2713} Root supervisor spawned");
241
242        tracing::info!("Agent ready (PeerId: {}, Storage: {})", peer_id, storage_path.display());
243
244        Ok(Agent::new(
245            storage_path,
246            identity_path,
247            self.dashboard_port,
248            supervisor,
249            supervisor_handle,
250        ))
251    }
252}