use std::sync::Arc; use tokio::sync::mpsc; use crate::agent::context_compressor::ContextCompressor; use crate::agent::{AgentError, AgentLoop, AgentRuntimeConfig, CompositeSystemPromptProvider, SystemPromptProvider}; use crate::config::{CompactionConfig, LLMProviderConfig, ModelResolver}; use crate::domain::CapabilityPolicy; use crate::experts::ExpertPromptProvider; use crate::experts::ExpertRuntime; use crate::gateway::agent_prompt_provider::AgentPromptProvider; use crate::gateway::model_selection::ModelSelectionStore; use crate::gateway::tool_prompt_provider::ToolPromptProvider; use crate::observability::Observer; use crate::skills::{SkillPromptProvider, SkillRuntime}; use crate::storage::PromptInjectionRepository; use crate::storage::persistent_session_id; use crate::storage::SessionStore; use crate::tools::task::runtime::{SubagentPromptProvider, SubagentRuntime}; use crate::tools::task::SubagentResult; use crate::tools::{ToolContext, ToolRegistry, WaitCoordinator}; /// 构建与 Agent 实际使用的完全一致的组合系统提示词 Provider。 /// /// 单一来源:AgentFactory::create 与命令侧(/save、/save-session、/current) /// 都调用此函数,确保保存到文件的系统提示词与 LLM 实际接收的提示词一致。 /// /// Provider 顺序:AgentPrompt → SkillPrompt → ExpertPrompt → SubagentPrompt → TodoPrompt pub(crate) fn build_system_prompt_provider( reinject_every: usize, provider_config: LLMProviderConfig, prompt_repository: Arc, skills: Arc, experts: Arc, subagent_runtime: Arc, ) -> Arc { Arc::new(CompositeSystemPromptProvider::new(vec![ Box::new(AgentPromptProvider::new( reinject_every, provider_config, prompt_repository, )), Box::new(SkillPromptProvider::new(skills, experts.clone())), Box::new(ExpertPromptProvider::new(experts.clone())), Box::new(SubagentPromptProvider::new(subagent_runtime, experts)), Box::new(ToolPromptProvider::new()), ])) } #[derive(Clone)] pub(crate) struct AgentFactory { tools: Arc, skills: Arc, experts: Arc, subagent_runtime: Arc, reinject_every: usize, prompt_repository: Arc, /// Provider/Model 解析器:按专家 frontmatter 中的 provider/model 字段覆盖基础配置 model_resolver: Arc, /// per-session 的用户模型选择(覆盖专家配置) model_selections: Arc, /// per-topic 的用户模型选择(最高优先级;物化后话题模型不再随 session 选择漂移) topic_model_selections: Arc, /// 持久化存储:session 级选择首次被话题命中时物化回写 topics 行 store: Arc, /// 上下文压缩算法配置(所有 agent 共享) compaction_config: CompactionConfig, /// 可观测性 Observer(依赖注入到 AgentLoop,业务层不感知具体实现) observer: Option>, /// 实例创建时间戳(用于区分新旧 AgentFactory 实例) instance_id: u64, } pub(crate) struct AgentBuildRequest<'a> { pub(crate) channel_name: &'a str, pub(crate) session_chat_id: &'a str, pub(crate) notification_chat_id: Option<&'a str>, pub(crate) sender_id: Option<&'a str>, pub(crate) message_id: Option<&'a str>, pub(crate) provider_config: LLMProviderConfig, /// 当前话题 ID(可选):用于 todo 等按 topic 隔离的工具 pub(crate) topic_id: Option, /// 取消信号接收端(可选):Agent 在每次迭代时检查是否被取消 pub(crate) cancel_token: Option>, /// 端到端追踪 ID(从 InboundMessage 继承,注入 ToolContext 供 tool 执行路径日志关联) pub(crate) trace_id: Option, /// 异步子代理完成队列的 sender(按 topic 隔离)。 /// 仅主 agent 有值:TaskTool 据此在子代理完成时发送 SubagentResult。 pub(crate) sub_done_sender: Option>, /// wait_for_subagents 工具的协调器(仅主 agent 有值)。 pub(crate) wait_coordinator: Option>, } impl AgentFactory { pub(crate) fn new( tools: Arc, skills: Arc, experts: Arc, subagent_runtime: Arc, reinject_every: usize, prompt_repository: Arc, model_resolver: Arc, model_selections: Arc, topic_model_selections: Arc, store: Arc, compaction_config: CompactionConfig, observer: Option>, ) -> Self { // 使用 Arc 指针地址作为实例标识符,用于区分新旧 AgentFactory 实例 let instance_id = Arc::as_ptr(&tools) as u64; tracing::info!( instance_id = instance_id, tool_count = tools.tool_names().len(), "AgentFactory::new created" ); Self { tools, skills, experts, subagent_runtime, reinject_every, prompt_repository, model_resolver, model_selections, topic_model_selections, store, compaction_config, observer, instance_id, } } /// 构造 ContextCompressor(参数内聚到 ContextCompressor,CompactionConfig 注入)。 /// AgentLoop(in-loop 压缩)和 Session(sync 兜底压缩)共用此方法, /// 确保两条压缩路径使用同一套用户配置的压缩参数。 pub(crate) fn build_compressor(&self, runtime_config: &AgentRuntimeConfig) -> ContextCompressor { ContextCompressor::with_compaction_config( runtime_config.context_window_tokens, runtime_config.context_summary_char_budget, &self.compaction_config, ) } pub(crate) fn create(&self, request: AgentBuildRequest<'_>) -> Result { let session_id = persistent_session_id(request.channel_name, request.session_chat_id); // 读取所选专家(用于工具过滤 + 子代理策略 + 模型覆盖) let expert = self.experts.selected_expert_for(&session_id); let expert_capability = expert.as_ref().map(|e| e.capability.clone()); // 按专家 frontmatter 中的 provider/model 字段解析覆盖基础 provider_config。 // 引用不存在的 provider/model 名时报错并阻止会话(用户主动选择的角色,配置错误应明确反馈)。 let expert_provider_config = match &expert { Some(e) if e.provider.is_some() || e.model.is_some() => { let resolved = self .model_resolver .resolve( e.provider.as_deref(), e.model.as_deref(), &request.provider_config, ) .map_err(|e| AgentError::Other(e.to_string()))?; tracing::info!( instance_id = self.instance_id, session_id = %session_id, expert = %e.name, provider = %resolved.name, model_id = %resolved.model_id, "AgentFactory: applied expert model override" ); resolved } _ => request.provider_config.clone(), }; // 用户手动选择的 provider/model 覆盖(最高优先级,覆盖专家配置)。 // 优先级:topic 级选择 > session 级选择;均未设置时保持专家/基础配置。 // 物化规则:session 级选择首次被话题命中时回写 topics 行固化——此后该话题的 // 模型只能被"在该话题内显式改选"改变,不再随 session 级选择漂移; // 专家/config 默认不物化(保持继承活性)。 // 引用不存在的 provider/model 名时报错并阻止会话(用户主动选择,配置错误应明确反馈)。 let topic_selection = request .topic_id .as_deref() .and_then(|tid| self.topic_model_selections.get(tid)); let from_topic = topic_selection.is_some(); let user_selection = topic_selection.or_else(|| self.model_selections.get(&session_id)); let effective_provider_config = match user_selection { Some((user_provider, user_model)) if user_provider.is_some() || user_model.is_some() => { let resolved = self .model_resolver .resolve( user_provider.as_deref(), user_model.as_deref(), &expert_provider_config, ) .map_err(|e| AgentError::Other(e.to_string()))?; // 物化:命中 session 级选择且话题无固化值时,将解析后的具体 // (provider, model) 写入 topics 行(持久化 + 内存缓存) if !from_topic { if let Some(tid) = request.topic_id.as_deref() { let provider = resolved.name.clone(); let model = resolved.model_id.clone(); self.topic_model_selections .set(tid, Some(provider.clone()), Some(model.clone())); if let Err(err) = self.store.update_topic_model(tid, Some(&provider), Some(&model)) { tracing::warn!( error = %err, topic_id = %tid, "AgentFactory: failed to materialize topic model selection" ); } } } tracing::info!( instance_id = self.instance_id, session_id = %session_id, topic_id = request.topic_id.as_deref().unwrap_or(""), source = if from_topic { "topic" } else { "session" }, provider = %resolved.name, model_id = %resolved.model_id, "AgentFactory: applied user model override" ); resolved } _ => expert_provider_config, }; // 诊断日志:记录 agent 实际使用的配置和实例 ID tracing::info!( instance_id = self.instance_id, channel = %request.channel_name, session_id = %session_id, provider = %effective_provider_config.name, model_id = %effective_provider_config.model_id, tool_count = self.tools.tool_names().len(), "AgentFactory: creating agent with config" ); // 创建组合的系统提示词提供者(与命令侧 /save 等共享同一构建逻辑) let system_prompt_provider = build_system_prompt_provider( self.reinject_every, effective_provider_config.clone(), self.prompt_repository.clone(), self.skills.clone(), self.experts.clone(), self.subagent_runtime.clone(), ); // 按所选专家的工具策略过滤工具集(含内置 + MCP 工具)。 // 无专家或专家未声明工具策略时,复用共享的 Arc(零拷贝)。 let base_tool_count = self.tools.tool_names().len(); let tools: Arc = match &expert_capability { Some(cap) if cap.has_tool_policy() => { let filtered = self.build_filtered_registry(cap); let filtered_count = filtered.tool_names().len(); tracing::info!( instance_id = self.instance_id, session_id = %session_id, base_tool_count, filtered_tool_count = filtered_count, "AgentFactory: applied expert tool policy" ); Arc::new(filtered) } _ => self.tools.clone(), }; AgentLoop::with_tools_and_system_prompt_provider( effective_provider_config.clone(), tools, system_prompt_provider, Some(self.skills.clone()), ) .map(|agent| { // notification_chat_id 优先,否则使用 session_chat_id let tool_chat_id = request .notification_chat_id .unwrap_or(request.session_chat_id); // 构建上下文压缩器(参数内聚到 ContextCompressor,CompactionConfig 注入) // 注入取消信号 receiver 的 clone 到 ToolContext, // 供 wait_for_subagents 工具传递给 coordinator.wait() 的 select!。 // watch::Receiver::clone() 创建共享同一 sender 的新 receiver, // 各 receiver 的 has_changed()/changed() 状态独立,互不影响。 let cancel_rx_for_context = request.cancel_token.as_ref().map(|rx| rx.clone()); let runtime_config = AgentRuntimeConfig::from(effective_provider_config.clone()); let compressor = Arc::new(self.build_compressor(&runtime_config)); let mut agent = agent .with_tool_context(ToolContext { channel_name: Some(request.channel_name.to_string()), sender_id: request.sender_id.map(str::to_string), chat_id: Some(tool_chat_id.to_string()), session_id: Some(session_id), topic_id: request.topic_id.clone(), message_id: request.message_id.map(str::to_string), message_seq: None, subagent_description: None, nesting_depth: 0, task_id: None, parent_task_id: None, tool_call_id: None, // 注入专家 capability,TaskTool 据此强制校验子代理白/黑名单 parent_capability: expert_capability.clone(), trace_id: request.trace_id.clone(), // 注入异步子代理完成队列 sender(按 topic 隔离) sub_done_sender: request.sub_done_sender.clone(), // 注入 wait 协调器(封装释放/重获取 serial_lock 逻辑) wait_coordinator: request.wait_coordinator.clone(), // 注入取消信号 receiver clone(供 wait 工具的 cancel 检查) cancel_rx: cancel_rx_for_context, }) .with_compressor(Some(compressor)); // 注入观测器(依赖注入,agent_loop 只认 Observer trait) if let Some(ref observer) = self.observer { agent = agent.with_observer(observer.clone()); } // 如果有取消信号接收端,注入 Agent if let Some(token) = request.cancel_token { agent = agent.with_cancel_token(token); } agent }) } /// 按专家 CapabilityPolicy 构建过滤后的 ToolRegistry 副本。 /// 生效顺序:先白名单取交集,再黑名单扣除(与 subagent filter_tool_registry 语义一致)。 /// 底层工具为 Arc,克隆廉价。 fn build_filtered_registry(&self, policy: &CapabilityPolicy) -> ToolRegistry { // 1. 白名单(取交集);None 表示不限,复制一份以便后续黑名单过滤 let after_allow: ToolRegistry = match &policy.allowed_tools { Some(allowed) => { let refs: Vec<&str> = allowed.iter().map(|s| s.as_str()).collect(); self.tools.only(&refs) } None => self.tools.without(&[]), }; // 2. 黑名单(扣除) if policy.denied_tools.is_empty() { after_allow } else { let refs: Vec<&str> = policy.denied_tools.iter().map(|s| s.as_str()).collect(); after_allow.without(&refs) } } }