objectiveai_cli/command/daemon/
spawn.rs1use std::pin::Pin;
21
22use futures::{Stream, StreamExt};
23use objectiveai_sdk::cli::command::daemon::spawn::{Request, ResponseItem};
24use objectiveai_sdk::cli::command::plugins::run::{Path as RunPath, Request as RunRequest};
25
26use crate::context::Context;
27use crate::error::Error;
28
29type ItemStream = Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>>;
30
31pub async fn execute(ctx: &Context, request: Request) -> Result<ItemStream, Error> {
32 let foreground = request
33 .dangerous_advanced
34 .as_ref()
35 .and_then(|a| a.foreground)
36 .unwrap_or(false);
37 if foreground {
38 execute_foreground(ctx).await
39 } else {
40 spawn(ctx).await?;
44 Ok(Box::pin(futures::stream::once(async move {
45 Ok::<ResponseItem, Error>(ResponseItem { ok: true })
46 })))
47 }
48}
49
50pub async fn spawn(ctx: &Context) -> Result<String, Error> {
55 let lock_dir = ctx.filesystem.state_dir().join("locks");
56 let exe = std::env::current_exe().map_err(|e| Error::Spawn("current_exe".into(), e))?;
57 crate::spawn::spawn_until_lock_published(
58 &exe,
59 &lock_dir,
60 super::DAEMON_LOCK_KEY,
61 |cmd| {
62 cmd.arg("daemon")
63 .arg("spawn")
64 .arg("--dangerous-advanced")
65 .arg("{\"foreground\":true}");
66 crate::spawn::apply_config_env(cmd, &ctx.config);
67 for var in [
77 "OBJECTIVEAI_AGENT_INSTANCE_HIERARCHY",
78 "OBJECTIVEAI_AGENT_ID",
79 "OBJECTIVEAI_AGENT_FULL_ID",
80 "OBJECTIVEAI_AGENT_REMOTE",
81 "OBJECTIVEAI_RESPONSE_ID",
82 "OBJECTIVEAI_RESPONSE_IDS",
83 "OBJECTIVEAI_PLUGIN_OWNER",
84 "OBJECTIVEAI_PLUGIN_REPOSITORY",
85 "OBJECTIVEAI_PLUGIN_VERSION",
86 ] {
87 cmd.env_remove(var);
88 }
89 cmd.env_remove(objectiveai_sdk::mcp::MCP_SESSION_ID_ENV);
90 },
91 )
92 .await
93}
94
95async fn execute_foreground(ctx: &Context) -> Result<ItemStream, Error> {
97 let lock_dir = ctx.filesystem.state_dir().join("locks");
98 let lock_err = |e: std::io::Error| Error::Lockfile {
99 key: super::DAEMON_LOCK_KEY.to_string(),
100 source: e,
101 };
102
103 let init = objectiveai_sdk::lockfile::wait_acquire(
105 &lock_dir,
106 super::DAEMON_INIT_LOCK_KEY,
107 "initializing",
108 )
109 .await
110 .map_err(lock_err)?;
111
112 let ws_listener = match tokio::net::TcpListener::bind((
117 ctx.config.daemon_address.as_str(),
118 ctx.config.daemon_port,
119 ))
120 .await
121 {
122 Ok(listener) => listener,
123 Err(e) => {
124 let _ = init.release();
125 return Err(Error::Spawn("daemon ws bind".into(), e));
126 }
127 };
128 let bound = match ws_listener.local_addr() {
133 Ok(addr) => {
134 let connect_ip = match addr.ip() {
135 std::net::IpAddr::V4(v4) if v4.is_unspecified() => {
136 std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
137 }
138 std::net::IpAddr::V6(v6) if v6.is_unspecified() => {
139 std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)
140 }
141 ip => ip,
142 };
143 format!("ws://{}", std::net::SocketAddr::new(connect_ip, addr.port()))
144 }
145 Err(e) => {
146 let _ = init.release();
147 return Err(Error::Spawn("daemon ws local_addr".into(), e));
148 }
149 };
150
151 let state_dir = ctx.filesystem.state_dir();
155 let socket_listener =
156 match crate::websockets::daemon_stream::bind_socket_listener(&state_dir) {
157 Ok(listener) => listener,
158 Err(e) => {
159 drop(ws_listener);
160 let _ = init.release();
161 return Err(Error::Spawn("daemon socket bind".into(), e));
162 }
163 };
164 let agents_socket_listener =
167 match crate::websockets::websocket_agents::bind_agents_socket_listener(&state_dir) {
168 Ok(listener) => listener,
169 Err(e) => {
170 drop(ws_listener);
171 drop(socket_listener);
172 let _ = init.release();
173 return Err(Error::Spawn("daemon agents socket bind".into(), e));
174 }
175 };
176 let conversation_socket_listener =
179 match crate::websockets::websocket_agent_instance::bind_conversation_socket_listener(
180 &state_dir,
181 ) {
182 Ok(listener) => listener,
183 Err(e) => {
184 drop(ws_listener);
185 drop(socket_listener);
186 drop(agents_socket_listener);
187 let _ = init.release();
188 return Err(Error::Spawn("daemon conversation socket bind".into(), e));
189 }
190 };
191
192 let claim = match objectiveai_sdk::lockfile::try_acquire(
197 &lock_dir,
198 super::DAEMON_LOCK_KEY,
199 &bound,
200 )
201 .await
202 {
203 None => {
206 drop(ws_listener);
207 drop(socket_listener);
208 drop(agents_socket_listener);
209 drop(conversation_socket_listener);
210 let _ = init.release();
211 return Ok(Box::pin(futures::stream::empty()));
212 }
213 Some(claim) => claim,
214 };
215 init.release().map_err(lock_err)?;
216
217 let (tx, _rx) = tokio::sync::broadcast::channel::<String>(1024);
223 let secret = ctx.config.daemon_secret.clone().map(std::sync::Arc::new);
227 let (agents_tx, _agents_rx) = tokio::sync::broadcast::channel::<
232 crate::websockets::websocket_agents::StatusChange,
233 >(1024);
234 let active = crate::websockets::websocket_agents::ActiveAgents::new(
235 state_dir.clone(),
236 agents_tx,
237 ctx.clone(),
238 );
239 let (conversation_tx, _conversation_rx) = tokio::sync::broadcast::channel::<(
243 std::sync::Arc<str>,
244 std::sync::Arc<str>,
245 )>(1024);
246 let conversations = crate::websockets::websocket_agent_instance::ConversationHub::new(
247 conversation_tx,
248 ctx.clone(),
249 );
250 crate::websockets::daemon_stream::serve_ws(
251 ws_listener,
252 tx.clone(),
253 secret,
254 ctx.clone(),
255 active.clone(),
256 conversations.clone(),
257 );
258 crate::websockets::daemon_stream::serve_socket_listener(socket_listener, tx.clone());
259 crate::websockets::websocket_agents::serve_agents_socket_listener(
260 agents_socket_listener,
261 active.clone(),
262 );
263 crate::websockets::websocket_agent_instance::serve_conversation_socket_listener(
264 conversation_socket_listener,
265 conversations.clone(),
266 );
267 tokio::spawn({
270 let active = active.clone();
271 async move {
272 active.reconcile_startup().await;
273 }
274 });
275 tokio::spawn(active.clone().watch_tag_changes());
279
280 let manifests: Vec<crate::filesystem::plugins::Manifest> = ctx
285 .filesystem
286 .list_plugins(0, usize::MAX)
287 .await
288 .into_iter()
289 .filter(|m| m.daemon)
290 .collect();
291 let mut streams = Vec::new();
292 for manifest in manifests {
293 let request = RunRequest {
294 path_type: RunPath::PluginsRun,
295 owner: manifest.owner,
296 name: manifest.name,
297 version: manifest.version,
298 args: vec!["daemon".to_string(), "begin".to_string()],
299 base: Default::default(),
300 };
301 let stream = crate::command::plugins::run::execute(ctx, request).await?;
302 streams.push(stream);
303 }
304
305 let stream = async_stream::stream! {
306 let _claim = claim;
310
311 let mut drains = futures::stream::FuturesUnordered::new();
312 for plugin_stream in streams {
313 drains.push(async move {
314 let mut plugin_stream = plugin_stream;
315 while plugin_stream.next().await.is_some() {}
319 });
320 }
321
322 yield Ok::<ResponseItem, Error>(ResponseItem { ok: true });
325
326 if drains.is_empty() {
327 std::future::pending::<()>().await;
329 } else {
330 let _ = drains.next().await;
332 }
333 };
334 Ok(Box::pin(stream))
335}
336
337pub mod request_schema {
338 use objectiveai_sdk::cli::command::daemon::spawn as sdk;
339 use objectiveai_sdk::cli::command::daemon::spawn::request_schema::{Request, Response};
340
341 use crate::context::Context;
342 use crate::error::Error;
343
344 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
345 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
346 }
347}
348
349pub mod response_schema {
350 use objectiveai_sdk::cli::command::daemon::spawn as sdk;
351 use objectiveai_sdk::cli::command::daemon::spawn::response_schema::{Request, Response};
352
353 use crate::context::Context;
354 use crate::error::Error;
355
356 pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
357 Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::ResponseItem)))
358 }
359}