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}