use std::pin::Pin;
use futures::{Stream, StreamExt};
use objectiveai_sdk::cli::command::daemon::spawn::{Request, ResponseItem};
use objectiveai_sdk::cli::command::plugins::run::{Path as RunPath, Request as RunRequest};
use crate::context::Context;
use crate::error::Error;
type ItemStream = Pin<Box<dyn Stream<Item = Result<ResponseItem, Error>> + Send>>;
pub async fn execute(ctx: &Context, request: Request) -> Result<ItemStream, Error> {
let foreground = request
.dangerous_advanced
.as_ref()
.and_then(|a| a.foreground)
.unwrap_or(false);
if foreground {
execute_foreground(ctx).await
} else {
spawn(ctx).await?;
Ok(Box::pin(futures::stream::once(async move {
Ok::<ResponseItem, Error>(ResponseItem { ok: true })
})))
}
}
pub async fn spawn(ctx: &Context) -> Result<String, Error> {
let lock_dir = ctx.filesystem.state_dir().join("locks");
let exe = std::env::current_exe().map_err(|e| Error::Spawn("current_exe".into(), e))?;
crate::spawn::spawn_until_lock_published(
&exe,
&lock_dir,
super::DAEMON_LOCK_KEY,
|cmd| {
cmd.arg("daemon")
.arg("spawn")
.arg("--dangerous-advanced")
.arg("{\"foreground\":true}");
crate::spawn::apply_config_env(cmd, &ctx.config);
for var in [
"OBJECTIVEAI_AGENT_INSTANCE_HIERARCHY",
"OBJECTIVEAI_AGENT_ID",
"OBJECTIVEAI_AGENT_FULL_ID",
"OBJECTIVEAI_AGENT_REMOTE",
"OBJECTIVEAI_RESPONSE_ID",
"OBJECTIVEAI_RESPONSE_IDS",
"OBJECTIVEAI_PLUGIN_OWNER",
"OBJECTIVEAI_PLUGIN_REPOSITORY",
"OBJECTIVEAI_PLUGIN_VERSION",
] {
cmd.env_remove(var);
}
cmd.env_remove(objectiveai_sdk::mcp::MCP_SESSION_ID_ENV);
},
)
.await
}
async fn execute_foreground(ctx: &Context) -> Result<ItemStream, Error> {
let lock_dir = ctx.filesystem.state_dir().join("locks");
let lock_err = |e: std::io::Error| Error::Lockfile {
key: super::DAEMON_LOCK_KEY.to_string(),
source: e,
};
let init = objectiveai_sdk::lockfile::wait_acquire(
&lock_dir,
super::DAEMON_INIT_LOCK_KEY,
"initializing",
)
.await
.map_err(lock_err)?;
let ws_listener = match tokio::net::TcpListener::bind((
ctx.config.daemon_address.as_str(),
ctx.config.daemon_port,
))
.await
{
Ok(listener) => listener,
Err(e) => {
let _ = init.release();
return Err(Error::Spawn("daemon ws bind".into(), e));
}
};
let bound = match ws_listener.local_addr() {
Ok(addr) => {
let connect_ip = match addr.ip() {
std::net::IpAddr::V4(v4) if v4.is_unspecified() => {
std::net::IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)
}
std::net::IpAddr::V6(v6) if v6.is_unspecified() => {
std::net::IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)
}
ip => ip,
};
format!("ws://{}", std::net::SocketAddr::new(connect_ip, addr.port()))
}
Err(e) => {
let _ = init.release();
return Err(Error::Spawn("daemon ws local_addr".into(), e));
}
};
let state_dir = ctx.filesystem.state_dir();
let socket_listener =
match crate::websockets::daemon_stream::bind_socket_listener(&state_dir) {
Ok(listener) => listener,
Err(e) => {
drop(ws_listener);
let _ = init.release();
return Err(Error::Spawn("daemon socket bind".into(), e));
}
};
let agents_socket_listener =
match crate::websockets::websocket_agents::bind_agents_socket_listener(&state_dir) {
Ok(listener) => listener,
Err(e) => {
drop(ws_listener);
drop(socket_listener);
let _ = init.release();
return Err(Error::Spawn("daemon agents socket bind".into(), e));
}
};
let conversation_socket_listener =
match crate::websockets::websocket_agent_instance::bind_conversation_socket_listener(
&state_dir,
) {
Ok(listener) => listener,
Err(e) => {
drop(ws_listener);
drop(socket_listener);
drop(agents_socket_listener);
let _ = init.release();
return Err(Error::Spawn("daemon conversation socket bind".into(), e));
}
};
let claim = match objectiveai_sdk::lockfile::try_acquire(
&lock_dir,
super::DAEMON_LOCK_KEY,
&bound,
)
.await
{
None => {
drop(ws_listener);
drop(socket_listener);
drop(agents_socket_listener);
drop(conversation_socket_listener);
let _ = init.release();
return Ok(Box::pin(futures::stream::empty()));
}
Some(claim) => claim,
};
init.release().map_err(lock_err)?;
let (tx, _rx) = tokio::sync::broadcast::channel::<String>(1024);
let secret = ctx.config.daemon_secret.clone().map(std::sync::Arc::new);
let (agents_tx, _agents_rx) = tokio::sync::broadcast::channel::<
crate::websockets::websocket_agents::StatusChange,
>(1024);
let active = crate::websockets::websocket_agents::ActiveAgents::new(
state_dir.clone(),
agents_tx,
ctx.clone(),
);
let (conversation_tx, _conversation_rx) = tokio::sync::broadcast::channel::<(
std::sync::Arc<str>,
std::sync::Arc<str>,
)>(1024);
let conversations = crate::websockets::websocket_agent_instance::ConversationHub::new(
conversation_tx,
ctx.clone(),
);
crate::websockets::daemon_stream::serve_ws(
ws_listener,
tx.clone(),
secret,
ctx.clone(),
active.clone(),
conversations.clone(),
);
crate::websockets::daemon_stream::serve_socket_listener(socket_listener, tx.clone());
crate::websockets::websocket_agents::serve_agents_socket_listener(
agents_socket_listener,
active.clone(),
);
crate::websockets::websocket_agent_instance::serve_conversation_socket_listener(
conversation_socket_listener,
conversations.clone(),
);
tokio::spawn({
let active = active.clone();
async move {
active.reconcile_startup().await;
}
});
tokio::spawn(active.clone().watch_tag_changes());
let manifests: Vec<crate::filesystem::plugins::Manifest> = ctx
.filesystem
.list_plugins(0, usize::MAX)
.await
.into_iter()
.filter(|m| m.daemon)
.collect();
let mut streams = Vec::new();
for manifest in manifests {
let request = RunRequest {
path_type: RunPath::PluginsRun,
owner: manifest.owner,
name: manifest.name,
version: manifest.version,
args: vec!["daemon".to_string(), "begin".to_string()],
base: Default::default(),
};
let stream = crate::command::plugins::run::execute(ctx, request).await?;
streams.push(stream);
}
let stream = async_stream::stream! {
let _claim = claim;
let mut drains = futures::stream::FuturesUnordered::new();
for plugin_stream in streams {
drains.push(async move {
let mut plugin_stream = plugin_stream;
while plugin_stream.next().await.is_some() {}
});
}
yield Ok::<ResponseItem, Error>(ResponseItem { ok: true });
if drains.is_empty() {
std::future::pending::<()>().await;
} else {
let _ = drains.next().await;
}
};
Ok(Box::pin(stream))
}
pub mod request_schema {
use objectiveai_sdk::cli::command::daemon::spawn as sdk;
use objectiveai_sdk::cli::command::daemon::spawn::request_schema::{Request, Response};
use crate::context::Context;
use crate::error::Error;
pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::Request)))
}
}
pub mod response_schema {
use objectiveai_sdk::cli::command::daemon::spawn as sdk;
use objectiveai_sdk::cli::command::daemon::spawn::response_schema::{Request, Response};
use crate::context::Context;
use crate::error::Error;
pub async fn execute(_ctx: &Context, _request: Request) -> Result<Response, Error> {
Ok(objectiveai_sdk::cli::command::ResponseSchema(schemars::schema_for!(sdk::ResponseItem)))
}
}