use std::sync::Arc;
use anyhow::Result;
use theway_contract::session::SessionStore;
use theway_core::ThinkingLevel;
use theway_core::multiagent::graph::engine::DagEngine;
use theway_core::multiagent::graph::persist::DagPersistSink;
use theway_core::multiagent::jobs::SubagentJobRegistry;
use theway_transport::feed::FeedUpdate;
use tokio::sync::mpsc;
use super::controller_storage::{canonical_work_dir, open_runtime_storage};
use super::settings::{
launch_thinking, provision_model_catalog, resolve_startup_model, startup_config_from_options,
};
use super::{DaemonOptions, SessionSelection};
use crate::control_plane_prompt::PendingControlPlanePrompt;
use crate::orchestration::DaemonServices;
use crate::runtime_storage::{RuntimeStorage, SessionRepository};
use crate::startup_config::StartupConfig;
use crate::stream_auth::stream_fn_with_auth_store;
pub(super) struct ProcessRuntime {
pub(super) cwd: std::path::PathBuf,
pub(super) paths: crate::DaemonPaths,
pub(super) storage: Arc<dyn RuntimeStorage>,
pub(super) repo: Arc<dyn SessionRepository>,
pub(super) startup: StartupConfig,
pub(super) model: Option<theway_llm_provider::Model>,
pub(super) thinking: ThinkingLevel,
pub(super) store: Arc<dyn SessionStore>,
pub(super) resumed: bool,
pub(super) session_id: String,
pub(super) logging: Option<crate::logging::LoggingHandle>,
pub(super) telemetry: Option<crate::observability::TelemetryHandle>,
pub(super) feed_tx: mpsc::UnboundedSender<(String, FeedUpdate)>,
pub(super) feed_rx: Option<mpsc::UnboundedReceiver<(String, FeedUpdate)>>,
pub(super) stream_fn: theway_core::StreamFn,
pub(super) services: DaemonServices,
pub(super) dag_engine: Arc<DagEngine>,
pub(super) subagent_registry: SubagentJobRegistry,
pub(super) executor: Arc<dyn theway_core::executor::ToolExecutor>,
pub(super) main_run_tx: mpsc::UnboundedSender<String>,
pub(super) main_run_rx: Option<mpsc::UnboundedReceiver<String>>,
pub(super) control_plane_hook: Option<theway_core::OnControlPlanePromptHook>,
pub(super) control_plane_prompt_tx: Option<mpsc::UnboundedSender<PendingControlPlanePrompt>>,
pub(super) control_plane_prompt_rx: Option<mpsc::UnboundedReceiver<PendingControlPlanePrompt>>,
pub(super) dag_persist: Option<Arc<dyn DagPersistSink>>,
}
impl ProcessRuntime {
pub(super) async fn start(options: &DaemonOptions) -> Result<Self> {
let paths = options.paths.clone();
let cwd = canonical_work_dir(&paths.work_dir)?;
let (storage, repo) =
open_runtime_storage(options.storage_service_addr.as_deref(), &cwd).await?;
let mut startup = startup_config_from_options(options)?;
let configured_api_keys = crate::stream_auth::ConfiguredApiKeys::default();
provision_model_catalog(
&mut startup,
options.base_url.as_deref(),
&configured_api_keys,
)
.await;
let model = resolve_startup_model(
options.provider.as_deref(),
options.model.as_deref(),
options.base_url.as_deref(),
&startup,
)
.await?;
let thinking = launch_thinking(options, &startup);
let (store, resumed) = select_session(&options.session, &repo, &cwd).await?;
let session_id = read_session_id(&store).await?;
let logging = crate::logging::init(&session_id);
let telemetry = crate::observability::TelemetryHandle::init().await;
let runtime_observer = telemetry.observer();
let (feed_tx, feed_rx) = tokio::sync::mpsc::unbounded_channel::<(String, FeedUpdate)>();
let stream_fn = stream_fn_with_auth_store(configured_api_keys.clone());
let services = start_process_services(
&startup,
&storage,
&cwd,
&paths.base,
&session_id,
&configured_api_keys,
&feed_tx,
)
.await;
let dag_engine = Arc::new(DagEngine::with_observer(runtime_observer.clone()));
let subagent_registry = SubagentJobRegistry::with_observer(runtime_observer.clone());
subagent_registry.set_transcript_store(Some(storage.job_transcript_store(&cwd)));
let executor: Arc<dyn theway_core::executor::ToolExecutor> =
crate::executor::executor_for_kind(startup.executor_kind, cwd.clone());
let (main_run_tx, main_run_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
let (control_plane_hook, control_plane_prompt_tx, control_plane_prompt_rx) =
if options.approve_control_plane {
(Some(crate::control_plane_prompt::allow_hook()), None, None)
} else {
let (tx, rx) = tokio::sync::mpsc::unbounded_channel();
(None, Some(tx), Some(rx))
};
Ok(Self {
cwd,
paths,
storage,
repo,
startup,
model,
thinking,
store,
resumed,
session_id,
logging,
telemetry: Some(telemetry),
feed_tx,
feed_rx: Some(feed_rx),
stream_fn,
services,
dag_engine,
subagent_registry,
executor,
main_run_tx,
main_run_rx: Some(main_run_rx),
control_plane_hook,
control_plane_prompt_tx,
control_plane_prompt_rx,
dag_persist: None,
})
}
pub(super) async fn shutdown(mut self, result: Result<()>, daemon_pid: u32) -> Result<()> {
if let Some(dag_persist) = self.dag_persist.take() {
dag_persist.flush().await;
}
self.dag_engine.abort_all_runs("daemon shutdown");
if let Some(telemetry) = self.telemetry.take() {
telemetry.shutdown().await;
}
drop(self.logging.take());
theway_transport::client::remove_port_file_if_owner(&self.cwd, daemon_pid);
result
}
}
async fn start_process_services(
startup: &StartupConfig,
storage: &Arc<dyn RuntimeStorage>,
cwd: &std::path::Path,
base: &std::path::Path,
session_id: &str,
configured_api_keys: &crate::stream_auth::ConfiguredApiKeys,
feed_tx: &mpsc::UnboundedSender<(String, FeedUpdate)>,
) -> DaemonServices {
let command_output = {
let tx = feed_tx.clone();
let session_id = session_id.to_string();
crate::commands::CommandOutput::new(move |line| {
let _ = tx.send((
session_id.clone(),
theway_transport::feed::FeedUpdate::Plain {
text: line,
level: theway_transport::feed::Level::Output,
},
));
})
};
let services = DaemonServices::new()
.with_command_output(command_output)
.with_tgrep_enabled(startup.tgrep_enabled)
.with_configured_api_keys(configured_api_keys.clone())
.with_attachments_base(base);
if let Err(err) = services
.dynamic_triggers
.load_from_storage(storage.clone(), cwd.to_path_buf(), session_id.to_string())
.await
{
tracing::warn!("dynamic triggers: {err}");
}
if let Err(err) = services
.cron
.load_from_storage(storage.clone(), cwd.to_path_buf(), session_id.to_string())
.await
{
tracing::warn!("cron: {err}");
}
services
}
async fn select_session(
selection: &SessionSelection,
repo: &Arc<dyn SessionRepository>,
cwd: &std::path::Path,
) -> Result<(Arc<dyn SessionStore>, bool)> {
match selection {
SessionSelection::New => Ok((repo.create_lazy(cwd).await?, false)),
SessionSelection::Latest => Ok((repo.resume(None).await?, true)),
SessionSelection::Id(id) => Ok((repo.resume(Some(id)).await?, true)),
}
}
async fn read_session_id(store: &Arc<dyn SessionStore>) -> Result<String> {
let session_metadata = store.get_metadata_json().await?;
Ok(session_metadata
.get("id")
.and_then(|v| v.as_str())
.unwrap_or("?")
.to_string())
}