use std::sync::Arc;
use nexo_config::types::llm::ResolvedContextOptimization;
use nexo_config::{AgentConfig, LlmConfig};
use nexo_llm::{LlmClient, LlmRegistry};
use crate::agent::effective::EffectiveBindingPolicy;
use crate::agent::tool_registry::ToolRegistry;
use crate::agent::tool_registry_cache::ToolRegistryCache;
#[derive(Clone)]
pub struct RuntimeSnapshot {
pub nexo_config: Arc<AgentConfig>,
pub effective_policies: Arc<dashmap::DashMap<Option<usize>, Arc<EffectiveBindingPolicy>>>,
pub tool_cache: Arc<ToolRegistryCache>,
pub llm_client: Option<Arc<dyn LlmClient>>,
pub version: u64,
pub context_optimization: ResolvedContextOptimization,
}
impl std::fmt::Debug for RuntimeSnapshot {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RuntimeSnapshot")
.field("agent_id", &self.nexo_config.id)
.field("version", &self.version)
.field("bindings", &self.nexo_config.inbound_bindings.len())
.field("effective_slots", &self.effective_policies.len())
.field("tool_cache_entries", &self.tool_cache.len())
.finish()
}
}
impl RuntimeSnapshot {
fn resolve_co(
nexo_config: &AgentConfig,
global: &nexo_config::types::llm::ContextOptimizationConfig,
) -> ResolvedContextOptimization {
ResolvedContextOptimization::resolve(global, nexo_config.context_optimization.as_ref())
}
pub fn bare(nexo_config: Arc<AgentConfig>, version: u64) -> Self {
let effective_policies: dashmap::DashMap<Option<usize>, Arc<EffectiveBindingPolicy>> =
dashmap::DashMap::new();
if nexo_config.inbound_bindings.is_empty() {
effective_policies.insert(
None,
Arc::new(EffectiveBindingPolicy::from_agent_defaults(&nexo_config)),
);
} else {
for idx in 0..nexo_config.inbound_bindings.len() {
effective_policies.insert(
Some(idx),
EffectiveBindingPolicy::resolved(&nexo_config, idx),
);
}
}
let context_optimization = Self::resolve_co(
&nexo_config,
&nexo_config::types::llm::ContextOptimizationConfig::default(),
);
Self {
nexo_config,
effective_policies: Arc::new(effective_policies),
tool_cache: Arc::new(ToolRegistryCache::new()),
llm_client: None,
version,
context_optimization,
}
}
pub fn build(
nexo_config: Arc<AgentConfig>,
llm_registry: &LlmRegistry,
llm_cfg: &LlmConfig,
version: u64,
) -> anyhow::Result<Self> {
let effective_policies: dashmap::DashMap<Option<usize>, Arc<EffectiveBindingPolicy>> =
dashmap::DashMap::new();
if nexo_config.inbound_bindings.is_empty() {
effective_policies.insert(
None,
Arc::new(EffectiveBindingPolicy::from_agent_defaults(&nexo_config)),
);
} else {
for idx in 0..nexo_config.inbound_bindings.len() {
effective_policies.insert(
Some(idx),
EffectiveBindingPolicy::resolved(&nexo_config, idx),
);
}
}
let llm_client = llm_registry
.build_for_tenant(
llm_cfg,
&nexo_config.model,
nexo_config.tenant_id.as_deref(),
)
.map_err(|e| {
anyhow::anyhow!(
"snapshot build: LLM client for agent '{}' failed: {}",
nexo_config.id,
e
)
})?;
let context_optimization = Self::resolve_co(&nexo_config, &llm_cfg.context_optimization);
Ok(Self {
nexo_config,
effective_policies: Arc::new(effective_policies),
tool_cache: Arc::new(ToolRegistryCache::new()),
llm_client: Some(llm_client),
version,
context_optimization,
})
}
pub fn policy_for(&self, binding_index: Option<usize>) -> Option<Arc<EffectiveBindingPolicy>> {
self.effective_policies
.get(&binding_index)
.map(|e| Arc::clone(e.value()))
}
pub fn tools_for(
&self,
agent_id: &str,
binding_index: Option<usize>,
base: &ToolRegistry,
allowed_tools: &[String],
) -> Arc<ToolRegistry> {
self.tool_cache
.get_or_build(agent_id, binding_index, base, allowed_tools)
}
pub fn tools_for_with_dispatch(
&self,
agent_id: &str,
binding_index: Option<usize>,
base: &ToolRegistry,
allowed_tools: &[String],
dispatch_policy: &nexo_config::DispatchPolicy,
is_admin: bool,
) -> Arc<ToolRegistry> {
self.tool_cache.get_or_build_with_dispatch(
agent_id,
binding_index,
base,
allowed_tools,
dispatch_policy,
is_admin,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use nexo_config::{
AgentRuntimeConfig, DreamingYamlConfig, HeartbeatConfig, ModelConfig,
OutboundAllowlistConfig, WorkspaceGitConfig,
};
fn empty_llm_cfg() -> LlmConfig {
LlmConfig {
providers: std::collections::HashMap::new(),
retry: Default::default(),
context_optimization: Default::default(),
tenants: std::collections::HashMap::new(),
}
}
fn minimal_agent(id: &str) -> Arc<AgentConfig> {
Arc::new(AgentConfig {
id: id.into(),
model: ModelConfig {
provider: "stub".into(),
model: "m1".into(),
},
plugins: Vec::new(),
heartbeat: HeartbeatConfig::default(),
config: AgentRuntimeConfig::default(),
system_prompt: String::new(),
workspace: String::new(),
skills: Vec::new(),
skills_dir: "./skills".into(),
skill_overrides: Default::default(),
transcripts_dir: String::new(),
dreaming: DreamingYamlConfig::default(),
workspace_git: WorkspaceGitConfig::default(),
tool_rate_limits: None,
tool_args_validation: None,
extra_docs: Vec::new(),
inbound_bindings: Vec::new(),
allowed_tools: Vec::new(),
sender_rate_limit: None,
allowed_delegates: Vec::new(),
accept_delegates_from: Vec::new(),
description: String::new(),
google_auth: None,
credentials: Default::default(),
link_understanding: serde_json::Value::Null,
web_search: serde_json::Value::Null,
pairing_policy: serde_json::Value::Null,
language: None,
locale_prompts: Default::default(),
outbound_allowlist: OutboundAllowlistConfig::default(),
context_optimization: None,
dispatch_policy: Default::default(),
plan_mode: Default::default(),
remote_triggers: Vec::new(),
lsp: nexo_config::types::lsp::LspPolicy::default(),
config_tool: nexo_config::types::config_tool::ConfigToolPolicy::default(),
team: nexo_config::types::team::TeamPolicy::default(),
proactive: Default::default(),
repl: Default::default(),
auto_dream: None,
assistant_mode: None,
away_summary: None,
brief: None,
channels: None,
auto_approve: false,
extract_memories: None,
event_subscribers: Vec::new(),
tenant_id: None,
extensions_config: std::collections::BTreeMap::new(),
active: true,
})
}
#[test]
fn build_fails_for_unknown_provider() {
let registry = LlmRegistry::with_builtins();
let err = RuntimeSnapshot::build(minimal_agent("ana"), ®istry, &empty_llm_cfg(), 1)
.unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("stub") || msg.contains("not registered") || msg.contains("agent 'ana'"),
"error should mention the offending provider: {msg}"
);
}
#[test]
fn policy_for_returns_legacy_slot_on_bindingless_agent() {
let agent = minimal_agent("ana");
let policy = EffectiveBindingPolicy::from_agent_defaults(&agent);
assert_eq!(policy.binding_index, None);
}
}