use std::future::Future;
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc;
use thiserror::Error;
use tokio::sync::mpsc;
use tokio_util::sync::CancellationToken;
use nexo_broker::AnyBroker;
use nexo_config::types::agents::AgentConfig;
use nexo_config::LlmConfig;
use nexo_llm::LlmRegistry;
use nexo_memory::LongTermMemory;
use super::admin_rpc::domains::pairing::PairingChallengeStore;
use super::admin_rpc::domains::processing::ProcessingControlStore;
use super::agent::Agent;
use super::agent_events::AgentEventEmitter;
use super::dispatch_handlers::DispatchToolContext;
use super::peer_directory::PeerDirectory;
use super::plan_mode_tool::PlanApprovalRegistry;
use super::redaction::Redactor;
use super::runtime::{AgentRuntime, ReloadCommand};
use super::tool_registry::ToolRegistry;
use super::transcripts_index::TranscriptsIndex;
use crate::link_understanding::LinkExtractor;
use crate::session::SessionManager;
#[derive(Clone)]
pub struct SharedRuntimeContext {
pub broker: AnyBroker,
pub llm_registry: Arc<LlmRegistry>,
pub llm_config: Arc<LlmConfig>,
pub memory: Option<Arc<LongTermMemory>>,
pub session_mgr: Arc<SessionManager>,
pub pairing_store: Option<Arc<dyn PairingChallengeStore>>,
pub config_dir: PathBuf,
pub dream_shutdown: CancellationToken,
pub heartbeat_shutdown: CancellationToken,
}
impl std::fmt::Debug for SharedRuntimeContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("SharedRuntimeContext")
.field("broker", &"<broker>")
.field("llm_registry", &"<llm-registry>")
.field("memory", &self.memory.as_ref().map(|_| "<memory>"))
.field("session_mgr", &"<session-mgr>")
.field(
"pairing_store",
&self.pairing_store.as_ref().map(|_| "<pairing-store>"),
)
.field("config_dir", &self.config_dir)
.finish()
}
}
pub struct SpawnedAgent {
pub agent_id: String,
pub reload_tx: mpsc::Sender<ReloadCommand>,
pub known_tools: Arc<Vec<String>>,
pub shutdown_token: CancellationToken,
pub runtime: AgentRuntime,
}
#[derive(Debug, Error)]
pub enum SpawnError {
#[error("validation: {0}")]
Validation(String),
#[error("llm bind: {0}")]
LlmBind(String),
#[error("workspace: {0}")]
Workspace(String),
#[error("broker subscribe: {0}")]
BrokerSubscribe(String),
#[error("plugin handle missing for binding {plugin}:{instance:?}")]
PluginMissing {
plugin: String,
instance: Option<String>,
},
#[error("internal: {0}")]
Internal(String),
}
pub async fn spawn_agent_runtime(
cfg: &AgentConfig,
shared: &SharedRuntimeContext,
) -> Result<SpawnedAgent, SpawnError> {
let _ = (cfg, shared);
todo!(
"Phase 81.32 c2-c7 — extracting boot loop body into spawn_agent_runtime. \
Until extraction completes, src/main.rs uses the inline boot loop."
)
}
pub fn resolve_llm_client(
cfg: &AgentConfig,
llm_registry: &nexo_llm::LlmRegistry,
llm_config: &nexo_config::LlmConfig,
) -> Result<Arc<dyn nexo_llm::LlmClient>, SpawnError> {
llm_registry.build(llm_config, &cfg.model).map_err(|e| {
SpawnError::LlmBind(format!(
"agent `{}` model `{}/{}`: {e}",
cfg.id, cfg.model.provider, cfg.model.model,
))
})
}
pub fn resolve_workspace_dir(
cfg: &AgentConfig,
default_root: Option<&std::path::Path>,
) -> Option<std::path::PathBuf> {
let trimmed = cfg.workspace.trim();
if trimmed.is_empty() {
default_root.map(|p| p.to_path_buf())
} else {
Some(std::path::PathBuf::from(trimmed))
}
}
pub fn validate_agent_config(
cfg: &AgentConfig,
plugins: &nexo_config::types::plugins::PluginsConfig,
known_tool_names: &[&str],
) -> Result<(), SpawnError> {
let catalog = crate::agent::KnownTools::new(known_tool_names.to_vec());
crate::agent::validate_agent(cfg, plugins, &catalog)
.map_err(|e| SpawnError::Validation(format!("agent `{}`: {e}", cfg.id)))
}
pub struct AgentSpawnerFn(
pub Box<
dyn Fn(
AgentConfig,
)
-> Pin<Box<dyn Future<Output = Result<SpawnedAgent, SpawnError>> + Send>>
+ Send
+ Sync,
>,
);
impl AgentSpawnerFn {
pub fn call(
&self,
cfg: AgentConfig,
) -> Pin<Box<dyn Future<Output = Result<SpawnedAgent, SpawnError>> + Send>> {
(self.0)(cfg)
}
}
pub struct RuntimeAssemblyDeps {
pub tools: Arc<ToolRegistry>,
pub memory: Option<Arc<LongTermMemory>>,
pub peers: Arc<PeerDirectory>,
pub redactor: Arc<Redactor>,
pub transcripts_index: Option<Arc<TranscriptsIndex>>,
pub credentials: Option<Arc<nexo_auth::AgentCredentialResolver>>,
pub breakers: Option<Arc<nexo_auth::BreakerRegistry>>,
pub link_extractor: Arc<LinkExtractor>,
pub pairing_gate: Arc<nexo_pairing::PairingGate>,
pub pairing_adapters: nexo_pairing::PairingAdapterRegistry,
pub plan_approval_registry: Arc<PlanApprovalRegistry>,
pub dispatch_ctx: Option<Arc<DispatchToolContext>>,
pub processing_store: Arc<dyn ProcessingControlStore>,
pub event_emitter: Option<Arc<dyn AgentEventEmitter>>,
}
pub fn assemble_agent_runtime(
agent: Arc<Agent>,
broker: AnyBroker,
sessions: Arc<SessionManager>,
deps: RuntimeAssemblyDeps,
) -> AgentRuntime {
let mut runtime = AgentRuntime::new(agent, broker, sessions);
runtime = runtime.with_tool_base(deps.tools);
if let Some(mem) = deps.memory {
runtime = runtime.with_memory(mem);
}
runtime = runtime.with_peers(deps.peers);
runtime = runtime.with_redactor(deps.redactor);
if let Some(idx) = deps.transcripts_index {
runtime = runtime.with_transcripts_index(idx);
}
if let Some(creds) = deps.credentials {
runtime = runtime.with_credentials(creds);
}
if let Some(brk) = deps.breakers {
runtime = runtime.with_breakers(brk);
}
runtime = runtime.with_link_extractor(deps.link_extractor);
runtime = runtime.with_pairing_gate(deps.pairing_gate);
runtime = runtime.with_pairing_adapters(deps.pairing_adapters);
runtime = runtime.with_plan_approval_registry(deps.plan_approval_registry);
if let Some(dc) = deps.dispatch_ctx {
runtime = runtime.with_dispatch_ctx(dc);
}
runtime = runtime.with_processing_store(deps.processing_store);
if let Some(emitter) = deps.event_emitter {
runtime = runtime.with_event_emitter(emitter);
}
runtime
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn spawn_error_display_is_actionable() {
let cases = [
SpawnError::Validation("bad tool".into()),
SpawnError::LlmBind("provider 'typo' unknown".into()),
SpawnError::Workspace("/missing/dir not found".into()),
SpawnError::BrokerSubscribe("topic refused".into()),
SpawnError::PluginMissing {
plugin: "whatsapp".into(),
instance: Some("personal".into()),
},
SpawnError::Internal("session register".into()),
];
for case in cases {
let s = case.to_string();
assert!(!s.is_empty(), "error display must produce text");
assert!(
s.len() > 4,
"error display must be operator-readable: {s:?}"
);
}
}
#[test]
fn shared_runtime_context_debug_omits_secrets() {
let src = include_str!("./spawn.rs");
assert!(
src.contains("<broker>") && src.contains("<llm-registry>"),
"Debug impl must redact broker + llm_registry"
);
}
}