use crate::events::TokenUsage;
use crate::mcp_server::{ScopedMcpServers, merge_scoped_mcp_servers};
use crate::message::Message;
use crate::message_filter::MessageFilterProvider;
use crate::runtime_agent::RuntimeAgent;
use crate::tool_types::ToolDefinition;
use crate::tools::{Tool, ToolRegistry};
use crate::typed_id::SessionId;
use everruns_capability::is_plugin_capability;
use std::collections::HashMap;
use std::sync::Arc;
use super::*;
pub struct ModelViewContext<'a> {
pub session_id: SessionId,
pub prior_usage: Option<&'a TokenUsage>,
}
pub trait ModelViewProvider: Send + Sync {
fn apply_model_view(
&self,
messages: Vec<Message>,
config: &serde_json::Value,
context: &ModelViewContext<'_>,
) -> Vec<Message>;
fn priority(&self) -> i32 {
0
}
}
pub struct CollectedCapabilities {
pub system_prompt_parts: Vec<String>,
pub system_prompt_attributions: Vec<SystemPromptAttribution>,
pub conversation_context_parts: Vec<String>,
pub conversation_context_attributions: Vec<SystemPromptAttribution>,
pub tools: Vec<Box<dyn Tool>>,
pub tool_definitions: Vec<ToolDefinition>,
pub mounts: Vec<MountPoint>,
pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
pub applied_ids: Vec<String>,
pub tool_search: Option<crate::driver_registry::ToolSearchConfig>,
pub prompt_cache: Option<crate::driver_registry::PromptCacheConfig>,
pub driver_options: HashMap<String, serde_json::Value>,
pub parallel_tool_calls: Option<bool>,
pub tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>>,
pub tool_call_hooks: Vec<Arc<dyn ToolCallHook>>,
pub mcp_servers: ScopedMcpServers,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemPromptAttribution {
pub capability_id: String,
pub content: String,
}
impl CollectedCapabilities {
pub fn system_prompt_prefix(&self) -> Option<String> {
if self.system_prompt_parts.is_empty() {
None
} else {
Some(self.system_prompt_parts.join("\n\n"))
}
}
pub fn conversation_context(&self) -> Option<String> {
if self.conversation_context_parts.is_empty() {
None
} else {
Some(self.conversation_context_parts.join("\n\n"))
}
}
pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
for (provider, config) in &self.message_filter_providers {
provider.apply_filters(query, config);
}
}
pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
for (provider, config) in &self.message_filter_providers {
provider.post_load(messages, config);
}
}
pub fn has_message_filters(&self) -> bool {
!self.message_filter_providers.is_empty()
}
}
pub fn compose_system_prompt(base_system_prompt: &str, additions: Option<&str>) -> String {
let Some(additions) = additions.filter(|value| !value.is_empty()) else {
return base_system_prompt.to_string();
};
if base_system_prompt.is_empty() {
return additions.to_string();
}
if base_system_prompt.contains("<system-prompt>") {
format!("{base_system_prompt}\n\n{additions}")
} else {
format!("<system-prompt>\n{base_system_prompt}\n</system-prompt>\n\n{additions}")
}
}
pub struct CollectedMessageFilters {
pub message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)>,
}
pub struct CollectedModelViewProviders {
pub model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)>,
}
impl CollectedMessageFilters {
pub fn apply_message_filters(&self, query: &mut crate::message_filter::MessageQuery) {
for (provider, config) in &self.message_filter_providers {
provider.apply_filters(query, config);
}
}
pub fn apply_post_load_filters(&self, messages: &mut Vec<crate::message::Message>) {
for (provider, config) in &self.message_filter_providers {
provider.post_load(messages, config);
}
}
}
impl CollectedModelViewProviders {
pub fn apply_model_view(
&self,
mut messages: Vec<Message>,
context: &ModelViewContext<'_>,
) -> Vec<Message> {
for (provider, config) in &self.model_view_providers {
messages = provider.apply_model_view(messages, config, context);
}
messages
}
}
pub(crate) fn compaction_is_enabled(
capability_configs: &[AgentCapabilityConfig],
registry: &CapabilityRegistry,
) -> bool {
capability_configs.iter().any(|cap_config| {
registry.get(cap_config.capability_id()).is_some_and(|cap| {
cap.status().is_active() && cap.compaction_policy(cap_config.config_value()).is_some()
})
})
}
pub fn collect_message_filters_only(
capability_configs: &[AgentCapabilityConfig],
registry: &CapabilityRegistry,
) -> CollectedMessageFilters {
let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
Vec::new();
let compaction_on = compaction_is_enabled(capability_configs, registry);
for cap_config in capability_configs {
let cap_id = cap_config.capability_id();
if let Some(capability) = registry.get(cap_id) {
if !capability.status().is_active() {
continue;
}
let effective: &dyn Capability = capability
.resolve_for_model(None)
.unwrap_or_else(|| capability.as_ref());
if let Some(provider) = effective.message_filter_provider() {
let config =
effective.message_filter_config(cap_config.config_value(), compaction_on);
message_filter_providers.push((provider, config));
}
}
}
message_filter_providers.sort_by_key(|(p, _)| p.priority());
CollectedMessageFilters {
message_filter_providers,
}
}
pub fn collect_model_view_providers(
capability_configs: &[AgentCapabilityConfig],
registry: &CapabilityRegistry,
model: Option<&str>,
) -> CollectedModelViewProviders {
let mut model_view_providers: Vec<(Arc<dyn ModelViewProvider>, serde_json::Value)> = Vec::new();
for cap_config in capability_configs {
let cap_id = cap_config.capability_id();
if let Some(capability) = registry.get(cap_id) {
if !capability.status().is_active() {
continue;
}
let effective: &dyn Capability = capability
.resolve_for_model(model)
.unwrap_or_else(|| capability.as_ref());
if let Some(provider) = effective.model_view_provider() {
model_view_providers.push((provider, cap_config.config_value().clone()));
}
}
}
model_view_providers.sort_by_key(|(p, _)| p.priority());
CollectedModelViewProviders {
model_view_providers,
}
}
pub fn collect_dynamic_facts(
capability_configs: &[AgentCapabilityConfig],
registry: &CapabilityRegistry,
model: Option<&str>,
ctx: &FactsContext,
) -> Vec<Fact> {
let mut dynamic = Vec::new();
for cap_config in capability_configs {
let cap_id = cap_config.capability_id();
if let Some(capability) = registry.get(cap_id) {
if !capability.status().is_active() {
continue;
}
let effective: &dyn Capability = capability
.resolve_for_model(model)
.unwrap_or_else(|| capability.as_ref());
for fact in effective.facts(cap_config.config_value(), ctx) {
if fact.volatility == Volatility::Dynamic {
dynamic.push(fact);
}
}
}
}
dynamic
}
pub fn collect_capability_mcp_servers(
capability_configs: &[AgentCapabilityConfig],
registry: &CapabilityRegistry,
) -> ScopedMcpServers {
let mut servers = ScopedMcpServers::default();
for cap_config in capability_configs {
let cap_id = cap_config.capability_id();
if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
if let Ok(definition) = serde_json::from_value::<DeclarativeCapabilityDefinition>(
cap_config.config_value().clone(),
) {
if !definition.status.is_active() {
continue;
}
if let Some(contributed) = definition.mcp_servers {
servers = merge_scoped_mcp_servers(&servers, &contributed);
}
}
continue;
}
if let Some(capability) = registry.get(cap_id) {
if !capability.status().is_active() {
continue;
}
servers = merge_scoped_mcp_servers(
&servers,
&capability.mcp_servers_with_config(cap_config.config_value()),
);
}
}
servers
}
pub async fn collect_capabilities(
capability_ids: &[String],
registry: &CapabilityRegistry,
ctx: &SystemPromptContext,
) -> CollectedCapabilities {
let resolved_ids = match resolve_dependencies(capability_ids, registry) {
Ok(resolved) => resolved.resolved_ids,
Err(e) => {
tracing::warn!("Failed to resolve capability dependencies: {}", e);
capability_ids.to_vec()
}
};
let configs: Vec<AgentCapabilityConfig> = resolved_ids
.iter()
.map(|id| {
AgentCapabilityConfig::with_config(
CapabilityId::new(id),
serde_json::Value::Object(serde_json::Map::new()),
)
})
.collect();
collect_capabilities_with_configs(&configs, registry, ctx).await
}
pub async fn collect_capabilities_with_configs(
capability_configs: &[AgentCapabilityConfig],
registry: &CapabilityRegistry,
ctx: &SystemPromptContext,
) -> CollectedCapabilities {
let mut system_prompt_parts: Vec<String> = Vec::new();
let mut system_prompt_attributions: Vec<SystemPromptAttribution> = Vec::new();
let mut conversation_context_parts: Vec<String> = Vec::new();
let mut conversation_context_attributions: Vec<SystemPromptAttribution> = Vec::new();
let mut tools: Vec<Box<dyn Tool>> = Vec::new();
let mut tool_definitions: Vec<ToolDefinition> = Vec::new();
let mut mounts: Vec<MountPoint> = Vec::new();
let mut message_filter_providers: Vec<(Arc<dyn MessageFilterProvider>, serde_json::Value)> =
Vec::new();
let mut applied_ids: Vec<String> = Vec::new();
let mut tool_search: Option<crate::driver_registry::ToolSearchConfig> = None;
let mut prompt_cache: Option<crate::driver_registry::PromptCacheConfig> = None;
let mut driver_options: HashMap<String, serde_json::Value> = HashMap::new();
let mut parallel_tool_calls: Option<bool> = None;
let mut tool_definition_hooks: Vec<Arc<dyn ToolDefinitionHook>> = Vec::new();
let mut tool_call_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
let mut narration_hooks: Vec<Arc<dyn ToolCallHook>> = Vec::new();
let mut mcp_servers = ScopedMcpServers::default();
let mut static_facts: Vec<Fact> = Vec::new();
let mut has_dynamic_facts = false;
let facts_ctx = FactsContext::new(ctx.session_id);
let compaction_on = compaction_is_enabled(capability_configs, registry);
let mut delegation_targets: Vec<DelegationTargetProvider> = Vec::new();
for cap_config in capability_configs {
let cap_id = cap_config.capability_id();
if is_declarative_capability(cap_id) || is_plugin_capability(cap_id) {
match serde_json::from_value::<DeclarativeCapabilityDefinition>(
cap_config.config_value().clone(),
) {
Ok(definition) => {
if !definition.status.is_active() {
continue;
}
if let Some(prompt) = definition.system_prompt.as_deref() {
let contribution =
format!("<capability id=\"{}\">\n{}\n</capability>", cap_id, prompt);
system_prompt_attributions.push(SystemPromptAttribution {
capability_id: cap_id.to_string(),
content: contribution.clone(),
});
system_prompt_parts.push(contribution);
}
mounts.extend(definition.mounts(cap_id));
if let Some(ref servers) = definition.mcp_servers {
mcp_servers = merge_scoped_mcp_servers(&mcp_servers, servers);
}
for skill in definition.skill_contributions() {
mounts.push(skill.to_mount(cap_id));
}
applied_ids.push(cap_id.to_string());
}
Err(error) => {
tracing::warn!(
capability_id = %cap_id,
error = %error,
"Skipping invalid declarative/plugin capability config"
);
}
}
continue;
}
if let Some(capability) = registry.get(cap_id) {
if !capability.status().is_active() {
continue;
}
let effective: &dyn Capability =
match capability.resolve_for_model(ctx.model.as_deref()) {
Some(inner) => inner,
None => capability.as_ref(),
};
let delegation_target =
effective.delegation_target_with_config(cap_config.config_value());
if let Some(contribution) = effective
.system_prompt_contribution_with_config(ctx, cap_config.config_value())
.await
{
system_prompt_attributions.push(SystemPromptAttribution {
capability_id: cap_id.to_string(),
content: contribution.clone(),
});
system_prompt_parts.push(contribution);
}
if let Some(contribution) = effective
.conversation_context_contribution_with_config(ctx, cap_config.config_value())
.await
{
conversation_context_attributions.push(SystemPromptAttribution {
capability_id: cap_id.to_string(),
content: contribution.clone(),
});
conversation_context_parts.push(contribution);
}
for fact in effective.facts(cap_config.config_value(), &facts_ctx) {
match fact.volatility {
Volatility::Static => static_facts.push(fact),
Volatility::Dynamic => has_dynamic_facts = true,
}
}
tools.extend(effective.tools_with_config(cap_config.config_value()));
if let Some(target) = delegation_target {
delegation_targets.push(target);
}
tool_definition_hooks.extend(
effective.tool_definition_hooks_with_context(ctx, cap_config.config_value()),
);
tool_call_hooks.extend(effective.tool_call_hooks());
narration_hooks.push(Arc::new(CapabilityNarrationHook(capability.clone())));
let cap_category = effective.category();
for def in effective.tool_definitions() {
let def = match (def.category(), cap_category) {
(None, Some(cat)) => def.with_category(cat),
_ => def,
}
.with_capability_attribution(cap_id, Some(capability.name()));
tool_definitions.push(def);
}
tool_search = effective
.tool_search_config(cap_config.config_value())
.or(tool_search);
prompt_cache = effective
.prompt_cache_config(cap_config.config_value())
.or(prompt_cache);
parallel_tool_calls = effective
.parallel_tool_calls_preference(cap_config.config_value())
.or(parallel_tool_calls);
for (key, value) in effective.driver_options(cap_config.config_value()) {
driver_options.entry(key).or_insert(value);
}
mounts.extend(effective.mounts());
mcp_servers = merge_scoped_mcp_servers(
&mcp_servers,
&effective.mcp_servers_with_config(cap_config.config_value()),
);
for skill in effective.contribute_skills() {
mounts.push(skill.to_mount(cap_id));
}
if let Some(provider) = effective.message_filter_provider() {
let config =
effective.message_filter_config(cap_config.config_value(), compaction_on);
message_filter_providers.push((provider, config));
}
applied_ids.push(cap_id.to_string());
}
}
if !tools.iter().any(|tool| tool.name() == "spawn_agent") && !delegation_targets.is_empty() {
let tool = UnifiedSpawnAgentTool::new(delegation_targets);
let def = tool
.to_definition()
.with_category("Orchestration")
.with_capability_attribution("agent_delegation", Some("Agent Delegation"));
tools.push(Box::new(tool));
tool_definitions.push(def);
}
let auto_activated: Vec<_> = registry
.list()
.into_iter()
.filter(|cap| {
!applied_ids.iter().any(|id| id == cap.id())
&& cap.status().is_active()
&& cap.auto_activates_for(&tool_definitions)
})
.cloned()
.collect();
for cap in auto_activated {
tools.extend(cap.tools());
let cap_category = cap.category();
for def in cap.tool_definitions() {
let def = match (def.category(), cap_category) {
(None, Some(cat)) => def.with_category(cat),
_ => def,
}
.with_capability_attribution(cap.id(), Some(cap.name()));
tool_definitions.push(def);
}
narration_hooks.push(Arc::new(CapabilityNarrationHook(cap.clone())));
applied_ids.push(cap.id().to_string());
}
if let Some(block) = facts::render_facts_block(&static_facts) {
system_prompt_attributions.push(SystemPromptAttribution {
capability_id: "facts".to_string(),
content: block.clone(),
});
system_prompt_parts.push(block);
}
if has_dynamic_facts {
system_prompt_attributions.push(SystemPromptAttribution {
capability_id: "facts".to_string(),
content: FACTS_DYNAMIC_NOTE.to_string(),
});
system_prompt_parts.push(FACTS_DYNAMIC_NOTE.to_string());
}
tool_call_hooks.extend(narration_hooks);
message_filter_providers.sort_by_key(|(p, _)| p.priority());
CollectedCapabilities {
system_prompt_parts,
system_prompt_attributions,
conversation_context_parts,
conversation_context_attributions,
tools,
tool_definitions,
mounts,
message_filter_providers,
applied_ids,
tool_search,
prompt_cache,
driver_options,
parallel_tool_calls,
tool_definition_hooks,
tool_call_hooks,
mcp_servers,
}
}
pub struct AppliedCapabilities {
pub runtime_agent: RuntimeAgent,
pub tool_registry: ToolRegistry,
pub applied_ids: Vec<String>,
}
pub async fn apply_capabilities(
base_runtime_agent: RuntimeAgent,
capability_ids: &[String],
registry: &CapabilityRegistry,
ctx: &SystemPromptContext,
) -> AppliedCapabilities {
let collected = collect_capabilities(capability_ids, registry, ctx).await;
let final_system_prompt = compose_system_prompt(
&base_runtime_agent.system_prompt,
collected.system_prompt_prefix().as_deref(),
);
let conversation_context = collected.conversation_context();
let mut tool_registry = ToolRegistry::new();
for tool in collected.tools {
tool_registry.register_boxed(tool);
}
let mut tools = collected.tool_definitions;
for hook in &collected.tool_definition_hooks {
tools = hook.transform(tools);
}
let runtime_agent = RuntimeAgent {
system_prompt: final_system_prompt,
model: base_runtime_agent.model,
tools,
max_iterations: base_runtime_agent.max_iterations,
temperature: base_runtime_agent.temperature,
max_tokens: base_runtime_agent.max_tokens,
tool_search: collected.tool_search,
prompt_cache: collected.prompt_cache,
driver_options: collected.driver_options,
network_access: base_runtime_agent.network_access,
parallel_tool_calls: base_runtime_agent
.parallel_tool_calls
.or(collected.parallel_tool_calls),
conversation_context,
};
AppliedCapabilities {
runtime_agent,
tool_registry,
applied_ids: collected.applied_ids,
}
}