将 4 个压缩参数(threshold_ratio、llm_compaction_threshold_ratio、 truncate_max_tokens、preserve_count)从硬编码提取到 config.json 顶层 compaction 节,前端新增 CompactionTab 供用户调整。 后端: - config: 新增 CompactionConfig 结构体,Config 新增 compaction 字段 - context_compressor: 4 个参数内聚到 ContextCompressor 实例字段; 新增 with_compaction_config() 构造函数和 truncate_tool_results() 方法; 对用户配置做防御性 clamp(ratio ∈ [0.1,1.0],整数 ≥ 1) - agent_loop: 调用 compressor.truncate_tool_results() 实现零参数耦合 - agent_factory: 新增 build_compressor() 方法,in-loop 与 sync 兜底 两条压缩路径共用同一套用户配置 - session: with_factories 改用 agent_factory.build_compressor(),修复 sync 压缩路径忽略用户配置的 P0 问题 前端: - types: 新增 CompactionConfig 接口,TabId 新增 'compaction' - 新建 CompactionTab.tsx,4 个 number input 分两组 SectionCard - constants + ConfigPage: 注册"上下文压缩" Tab(Archive 图标) 清理:删除无调用方的 from_provider_config/from_runtime_config/with_config 方法及 DEFAULT_THRESHOLD_RATIO/LLM_COMPACTION_THRESHOLD_RATIO 常量。 测试:context_compressor 18 + session 31 + agent_loop 42 全通过。
279 lines
12 KiB
Rust
279 lines
12 KiB
Rust
use std::sync::Arc;
|
||
|
||
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::skills::{SkillPromptProvider, SkillRuntime};
|
||
use crate::storage::PromptInjectionRepository;
|
||
use crate::storage::persistent_session_id;
|
||
use crate::tools::task::runtime::{SubagentPromptProvider, SubagentRuntime};
|
||
use crate::tools::{ToolContext, ToolRegistry};
|
||
|
||
/// 构建与 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>,
|
||
/// 上下文压缩算法配置(所有 agent 共享)
|
||
compaction_config: CompactionConfig,
|
||
/// 实例创建时间戳(用于区分新旧 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<()>>,
|
||
}
|
||
|
||
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>,
|
||
compaction_config: CompactionConfig,
|
||
) -> 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,
|
||
compaction_config,
|
||
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<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 覆盖(最高优先级,覆盖专家配置)。
|
||
// 引用不存在的 provider/model 名时报错并阻止会话(用户主动选择,配置错误应明确反馈)。
|
||
let effective_provider_config = match self.model_selections.get(&session_id) {
|
||
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()))?;
|
||
tracing::info!(
|
||
instance_id = self.instance_id,
|
||
session_id = %session_id,
|
||
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);
|
||
// 构建上下文压缩器(参数内聚到 ContextCompressor,CompactionConfig 注入)
|
||
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(),
|
||
})
|
||
.with_compressor(Some(compressor));
|
||
// 如果有取消信号接收端,注入 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)
|
||
}
|
||
}
|
||
}
|