PicoBot/src/gateway/agent_factory.rs
oudecheng dc693fa80b feat(model): 主代理/子代理模型解析链 topic>session>expert>config + TaskToolResult 附带实际模型
- 主代理解析链新增 topic 级选择(最高优先级),session 级首命中时物化固化为话题选择
- 子代理解析链共享 helper:def frontmatter > 话题级 > session 级 > 全局基础配置
- TaskToolResult 携带子代理实际使用的 provider/model,供前端差异展示
2026-08-15 15:34:18 +08:00

354 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<dyn PromptInjectionRepository>,
skills: Arc<SkillRuntime>,
experts: Arc<ExpertRuntime>,
subagent_runtime: Arc<SubagentRuntime>,
) -> Arc<dyn SystemPromptProvider> {
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<ToolRegistry>,
skills: Arc<SkillRuntime>,
experts: Arc<ExpertRuntime>,
subagent_runtime: Arc<SubagentRuntime>,
reinject_every: usize,
prompt_repository: Arc<dyn PromptInjectionRepository>,
/// Provider/Model 解析器:按专家 frontmatter 中的 provider/model 字段覆盖基础配置
model_resolver: Arc<ModelResolver>,
/// per-session 的用户模型选择(覆盖专家配置)
model_selections: Arc<ModelSelectionStore>,
/// per-topic 的用户模型选择(最高优先级;物化后话题模型不再随 session 选择漂移)
topic_model_selections: Arc<ModelSelectionStore>,
/// 持久化存储session 级选择首次被话题命中时物化回写 topics 行
store: Arc<SessionStore>,
/// 上下文压缩算法配置(所有 agent 共享)
compaction_config: CompactionConfig,
/// 可观测性 Observer依赖注入到 AgentLoop业务层不感知具体实现
observer: Option<Arc<dyn Observer>>,
/// 实例创建时间戳(用于区分新旧 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<String>,
/// 取消信号接收端可选Agent 在每次迭代时检查是否被取消
pub(crate) cancel_token: Option<tokio::sync::watch::Receiver<()>>,
/// 端到端追踪 ID从 InboundMessage 继承,注入 ToolContext 供 tool 执行路径日志关联)
pub(crate) trace_id: Option<String>,
/// 异步子代理完成队列的 sender按 topic 隔离)。
/// 仅主 agent 有值TaskTool 据此在子代理完成时发送 SubagentResult。
pub(crate) sub_done_sender: Option<mpsc::Sender<SubagentResult>>,
/// wait_for_subagents 工具的协调器(仅主 agent 有值)。
pub(crate) wait_coordinator: Option<Arc<dyn WaitCoordinator>>,
}
impl AgentFactory {
pub(crate) fn new(
tools: Arc<ToolRegistry>,
skills: Arc<SkillRuntime>,
experts: Arc<ExpertRuntime>,
subagent_runtime: Arc<SubagentRuntime>,
reinject_every: usize,
prompt_repository: Arc<dyn PromptInjectionRepository>,
model_resolver: Arc<ModelResolver>,
model_selections: Arc<ModelSelectionStore>,
topic_model_selections: Arc<ModelSelectionStore>,
store: Arc<SessionStore>,
compaction_config: CompactionConfig,
observer: Option<Arc<dyn Observer>>,
) -> 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参数内聚到 ContextCompressorCompactionConfig 注入)。
/// AgentLoopin-loop 压缩)和 Sessionsync 兜底压缩)共用此方法,
/// 确保两条压缩路径使用同一套用户配置的压缩参数。
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<AgentLoop, AgentError> {
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<ToolRegistry>(零拷贝)。
let base_tool_count = self.tools.tool_names().len();
let tools: Arc<ToolRegistry> = 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);
// 构建上下文压缩器(参数内聚到 ContextCompressorCompactionConfig 注入)
// 注入取消信号 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,
// 注入专家 capabilityTaskTool 据此强制校验子代理白/黑名单
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<dyn ToolTrait>,克隆廉价。
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)
}
}
}