核心架构变更:
1. Agent 多模式系统替代 Coordinator
- 移除 src/agent/coordinator/(Coordinator Agent/Worker/Tools,946 行)
- 新建 src/agent/modes/:声明式模式抽象(AgentMode/ModeConfig/ToolSet)
- 三种内置模式:
- default:通用科研助手,零覆盖保持现有行为
- deep-research:16 步、启用思考、research 权限、系统性调研
- literature-reader:白名单工具、只读沙箱、结构化阅读
- ModeRegistry + ModeConfig 预设 + ToolSet 过滤 + 身份/原则覆盖
- AgentRuntime::with_mode() 统一入口,模式持久化到 session.mode 字段
- GET /api/chat/modes 提供模式列表给前端选择器
2. 视觉模型与图片分析
- 新增 analyze_image 工具(340 行):本地/URL 图片 → 视觉模型流式分析
- LlmClient::analyze_image_stream():SSE 增量实时推送
- 配置:LLM_VISION_MODEL / LLM_VISION_API_KEY / LLM_VISION_API_BASE
- 前端:粘贴/选择图片附件,重试时复用文件路径
- Service 层移除 /chat/rag 和 /chat/figure 端点,统一走 Agent SSE
- Body limit 提升至 100MB 适配大图上传
3. LLM 三级能力分层
- Tier 1 (Core) → Tier 2 (Medium) → Tier 3 (Fast),级联回退
- medium_llm / fast_llm / vision_llm 注入 AppState
- 资产批量翻译 → Medium LLM + Semaphore(3) 并发控制
- 记忆提取/上下文压缩子代理 → Fast LLM
- SubAgentRunner::with_llm_client() 支持注入专用 LLM
4. 数据库与性能优化
- SQLite 启用 WAL + busy_timeout(10s) 处理并发写入
- RAG ingest:DELETE 合并为原子语句 + 批量事务写入
- Meta sync:save_paper_to_db_tx() 事务化批量插入
- 翻译词典:first_words HashSet 预过滤 + next_valid_index 跳跃优化
- read_file 不截断输出 + skip_persist 防止级联磁盘持久化
5. 工具系统增强
- ToolContext 增加 tool_call_id + max_output_chars
- ToolOutput 增加 skip_persist 标记
- TextDelta SSE 携带可选 tool_call_id 支持工具的流式输出
- ChatMessage::text() 辅助方法
1892 lines
72 KiB
Rust
1892 lines
72 KiB
Rust
// src/agent/runtime/mod.rs
|
||
//
|
||
// 科研智能体运行时核心模块。
|
||
// 实现 ReAct 循环:Thought -> Action (工具调用) -> Observation -> Thought...
|
||
// 支持会话持久化、上下文压缩、死循环检测和 SSE 流式输出。
|
||
//
|
||
// 子模块结构:
|
||
// session — 会话创建/恢复、历史加载
|
||
// context — 上下文构建、任务状态恢复
|
||
// streaming — LLM 流式响应处理
|
||
// executor — 工具调用验证与并行执行
|
||
// finalize — 会话收尾、指标持久化
|
||
|
||
pub mod checkpoint;
|
||
pub mod circuit_breaker;
|
||
pub mod context;
|
||
pub mod denial_tracker;
|
||
pub mod error_recovery;
|
||
pub mod executor;
|
||
pub mod file_cache;
|
||
pub mod finalize;
|
||
pub mod hardline;
|
||
pub mod partitioner;
|
||
pub mod permission;
|
||
pub mod permission_explainer;
|
||
pub mod permission_profile;
|
||
pub mod session;
|
||
pub mod streaming;
|
||
pub mod streaming_executor;
|
||
pub mod system_prompt;
|
||
pub mod token_budget;
|
||
pub mod untrusted;
|
||
|
||
use serde::Serialize;
|
||
use sqlx::SqlitePool;
|
||
use std::collections::HashMap;
|
||
use std::sync::Arc;
|
||
use tokio::sync::mpsc;
|
||
use tracing::{error, info, warn};
|
||
|
||
use super::background::BgNotificationQueue;
|
||
use super::compact;
|
||
use super::hooks::{
|
||
HookRegistry, SessionStartContext, StepCompleteContext, UserPromptSubmitContext,
|
||
};
|
||
use super::modes::{self, AgentMode, ModeRegistry};
|
||
use super::terminal::TurnTerminal;
|
||
use super::tools::ToolRegistry;
|
||
use crate::api::AppState;
|
||
use crate::clients::llm::{ChatMessage, LlmClient, MessageRole, StreamEvent};
|
||
|
||
use self::error_recovery::{classify_error, ErrorKind, ErrorRecovery};
|
||
use self::session::SessionInfo;
|
||
use self::streaming::{StreamOutput, StreamStatus};
|
||
use self::system_prompt::SystemPromptCache;
|
||
use self::token_budget::TokenBudget;
|
||
|
||
/// Agent 配置参数
|
||
#[derive(Debug, Clone)]
|
||
pub struct AgentConfig {
|
||
/// 最大 ReAct 迭代次数
|
||
pub max_steps: usize,
|
||
/// 同质调用检测阈值(连续相同调用次数)
|
||
pub duplicate_call_threshold: usize,
|
||
/// 工具执行超时时间(秒)
|
||
pub tool_timeout_secs: u64,
|
||
/// 工具输出最大字符数
|
||
pub max_tool_output_chars: usize,
|
||
/// 上下文 Token 估算上限(触发自动摘要压缩)
|
||
pub context_char_limit: usize,
|
||
/// Token 预算软限制(触发 nudging 提醒)
|
||
pub token_soft_limit: usize,
|
||
/// Token 预算硬限制(触发强制动作)
|
||
pub token_hard_limit: usize,
|
||
/// 最大消息数(超过此阈值触发 snip_compact 层压缩)
|
||
pub max_messages: usize,
|
||
/// 是否启用 LLM 思考模式(前端可控,默认关闭)
|
||
pub enable_thinking: bool,
|
||
/// 权限拒绝规则(逗号分隔,格式: ToolName 或 ToolName(content_pattern))
|
||
pub permission_deny_rules: Vec<String>,
|
||
/// 权限允许规则(逗号分隔)
|
||
pub permission_allow_rules: Vec<String>,
|
||
/// 权限询问规则(逗号分隔)
|
||
pub permission_ask_rules: Vec<String>,
|
||
/// 权限模式: "default" | "accept_edits" | "bypass" | "dont_ask"
|
||
pub permission_mode: String,
|
||
/// 拒绝追踪:连续拒绝上限(默认 3)
|
||
pub denial_max_consecutive: usize,
|
||
/// 拒绝追踪:总拒绝上限(默认 20)
|
||
pub denial_max_total: usize,
|
||
/// 附加允许目录(逗号分隔,扩展文件沙箱范围)
|
||
pub additional_allowed_dirs: Vec<String>,
|
||
/// 子代理工具白名单(逗号分隔,空=全部工具可用)
|
||
pub subagent_allowed_tools: Vec<String>,
|
||
/// Agent 运行模式 ID("default" / "deep-research" / "literature-reader")
|
||
pub mode: String,
|
||
}
|
||
|
||
impl AgentConfig {
|
||
/// 从环境变量加载配置,缺失时使用默认值。
|
||
pub fn from_env_optional() -> Self {
|
||
let mut config = AgentConfig {
|
||
max_steps: std::env::var("AGENT_MAX_STEPS")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(8),
|
||
duplicate_call_threshold: 3,
|
||
tool_timeout_secs: std::env::var("AGENT_TOOL_TIMEOUT_SECS")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(120),
|
||
max_tool_output_chars: std::env::var("AGENT_MAX_TOOL_OUTPUT_CHARS")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(4000),
|
||
context_char_limit: std::env::var("AGENT_CONTEXT_CHAR_LIMIT")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(16000),
|
||
token_soft_limit: std::env::var("AGENT_TOKEN_SOFT_LIMIT")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(32000),
|
||
token_hard_limit: std::env::var("AGENT_TOKEN_HARD_LIMIT")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(40000),
|
||
max_messages: std::env::var("AGENT_MAX_MESSAGES")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(50),
|
||
enable_thinking: false,
|
||
permission_deny_rules: parse_comma_list("AGENT_PERMISSIONS_DENY"),
|
||
permission_allow_rules: parse_comma_list("AGENT_PERMISSIONS_ALLOW"),
|
||
permission_ask_rules: parse_comma_list("AGENT_PERMISSIONS_ASK"),
|
||
permission_mode: std::env::var("AGENT_PERMISSION_MODE")
|
||
.unwrap_or_else(|_| "default".to_string()),
|
||
denial_max_consecutive: std::env::var("AGENT_DENIAL_MAX_CONSECUTIVE")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(3),
|
||
denial_max_total: std::env::var("AGENT_DENIAL_MAX_TOTAL")
|
||
.ok()
|
||
.and_then(|v| v.parse().ok())
|
||
.unwrap_or(20),
|
||
additional_allowed_dirs: parse_comma_list("AGENT_ADDITIONAL_DIRS"),
|
||
subagent_allowed_tools: parse_comma_list("AGENT_SUBAGENT_ALLOWED_TOOLS"),
|
||
mode: std::env::var("AGENT_MODE").unwrap_or_else(|_| "default".to_string()),
|
||
};
|
||
|
||
// 加载权限档案(AGENT_PERMISSION_PROFILE),追加到现有规则
|
||
let profile_name = std::env::var("AGENT_PERMISSION_PROFILE").unwrap_or_default();
|
||
if !profile_name.is_empty() {
|
||
if let Some(profile) = permission_profile::load_profile(&profile_name) {
|
||
info!(
|
||
"[AgentConfig] 加载权限档案: {} — {}",
|
||
profile.name, profile.description
|
||
);
|
||
permission_profile::apply_profile_to_config(
|
||
&profile,
|
||
&mut config.permission_deny_rules,
|
||
&mut config.permission_allow_rules,
|
||
&mut config.permission_ask_rules,
|
||
&mut config.permission_mode,
|
||
);
|
||
} else {
|
||
warn!(
|
||
"[AgentConfig] 未知的权限档案: {}(可用: {:?})",
|
||
profile_name,
|
||
permission_profile::list_available_profiles()
|
||
);
|
||
}
|
||
}
|
||
|
||
config
|
||
}
|
||
}
|
||
|
||
/// 解析逗号分隔的环境变量为字符串列表
|
||
fn parse_comma_list(env_key: &str) -> Vec<String> {
|
||
std::env::var(env_key)
|
||
.ok()
|
||
.map(|v| {
|
||
v.split(',')
|
||
.map(|s| s.trim().to_string())
|
||
.filter(|s| !s.is_empty())
|
||
.collect()
|
||
})
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
impl Default for AgentConfig {
|
||
fn default() -> Self {
|
||
Self::from_env_optional()
|
||
}
|
||
}
|
||
|
||
// ── SSE Stream Events ──
|
||
|
||
/// SSE 流式事件(发送给前端)
|
||
#[derive(Debug, Clone, Serialize)]
|
||
#[serde(tag = "type")]
|
||
pub enum AgentStreamEvent {
|
||
/// 会话创建/恢复
|
||
#[serde(rename = "session")]
|
||
Session { session_id: String, title: String },
|
||
/// 智能体思考过程
|
||
#[serde(rename = "thought")]
|
||
Thought { content: String, step: usize },
|
||
/// 工具调用开始
|
||
#[serde(rename = "tool_call")]
|
||
ToolCall {
|
||
/// LLM 生成的工具调用 ID,用于全链路关联(前端/审计/持久化)
|
||
id: String,
|
||
name: String,
|
||
arguments: serde_json::Value,
|
||
step: usize,
|
||
},
|
||
/// 工具执行结果(Observation)
|
||
#[serde(rename = "tool_result")]
|
||
ToolResult {
|
||
/// 对应的工具调用 ID,前端凭此精确匹配 tool_call 条目
|
||
tool_call_id: String,
|
||
name: String,
|
||
output: String,
|
||
is_error: bool,
|
||
metadata: serde_json::Value,
|
||
step: usize,
|
||
},
|
||
/// 文本增量流式输出(最终回答或工具流式输出)
|
||
#[serde(rename = "text_delta")]
|
||
TextDelta {
|
||
content: String,
|
||
/// 可选:工具调用 ID。当 set 时,此增量属于对应工具的流式输出,
|
||
/// 前端应将其渲染到工具结果区域而非主文本区。
|
||
#[serde(skip_serializing_if = "Option::is_none")]
|
||
tool_call_id: Option<String>,
|
||
},
|
||
/// Token 使用统计
|
||
#[serde(rename = "usage")]
|
||
Usage {
|
||
prompt_tokens: u32,
|
||
completion_tokens: u32,
|
||
total_tokens: u32,
|
||
},
|
||
/// 错误通知
|
||
#[serde(rename = "error")]
|
||
Error { message: String },
|
||
/// 权限请求(需要用户确认工具执行)
|
||
#[serde(rename = "permission_request")]
|
||
PermissionRequest {
|
||
tool_call_id: String,
|
||
tool_name: String,
|
||
message: String,
|
||
arguments: serde_json::Value,
|
||
/// 可选的权限风险解释(参考 Claude Code permissionExplainer)
|
||
explanation: Option<serde_json::Value>,
|
||
},
|
||
/// 权限响应已处理
|
||
#[serde(rename = "permission_response")]
|
||
PermissionResponse { tool_call_id: String, allowed: bool },
|
||
/// 完成标记
|
||
#[serde(rename = "done")]
|
||
Done,
|
||
}
|
||
|
||
// ── Metrics & Detection ──
|
||
|
||
/// Agent 运行指标
|
||
#[derive(Debug, Default, Serialize)]
|
||
pub struct AgentMetrics {
|
||
pub total_steps: usize,
|
||
pub compression_count: usize,
|
||
pub duplicate_detections: usize,
|
||
/// 各工具调用次数统计
|
||
pub tool_calls: HashMap<String, usize>,
|
||
}
|
||
|
||
/// 同质调用检测器
|
||
#[derive(Debug, Default)]
|
||
pub struct DuplicateDetector {
|
||
last_call: Option<(String, String)>, // (tool_name, arguments)
|
||
consecutive_count: usize,
|
||
}
|
||
|
||
impl DuplicateDetector {
|
||
/// 记录一次调用,返回是否检测到死循环
|
||
pub fn record(&mut self, tool_name: &str, arguments: &str, threshold: usize) -> bool {
|
||
let key = (tool_name.to_string(), arguments.to_string());
|
||
if self.last_call.as_ref() == Some(&key) {
|
||
self.consecutive_count += 1;
|
||
if self.consecutive_count >= threshold {
|
||
return true;
|
||
}
|
||
} else {
|
||
self.last_call = Some(key);
|
||
self.consecutive_count = 1;
|
||
}
|
||
false
|
||
}
|
||
}
|
||
|
||
// ── Agent Runtime ──
|
||
|
||
/// 智能体运行时
|
||
pub struct AgentRuntime {
|
||
app_state: Arc<AppState>,
|
||
config: AgentConfig,
|
||
tool_registry: ToolRegistry,
|
||
/// 后台任务通知队列(支持 bg_task_run/bg_task_check)
|
||
bg_notification_queue: Arc<BgNotificationQueue>,
|
||
/// 指标采集 hook 的数据引用(供 API 查询)
|
||
metrics_data: Arc<std::sync::Mutex<super::hooks::MetricsData>>,
|
||
/// 压缩熔断器(跨 turn 共享,防止无限压缩循环)
|
||
compaction_breaker: Arc<std::sync::Mutex<circuit_breaker::CompactionCircuitBreaker>>,
|
||
/// 权限检查器
|
||
permission_checker: Arc<permission::PermissionChecker>,
|
||
/// 拒绝追踪器(跨 turn 共享)
|
||
denial_tracker: Arc<std::sync::Mutex<denial_tracker::DenialTracker>>,
|
||
/// 文件状态缓存(跨 turn 共享,用于 Read 去重)
|
||
read_file_state: Arc<std::sync::Mutex<file_cache::FileStateCache>>,
|
||
/// 系统提示词 section 缓存(跨 turn 共享,避免每 turn 重建静态/低频变动内容)
|
||
prompt_cache: std::sync::Mutex<SystemPromptCache>,
|
||
/// 上下文压缩折叠日志(跨 turn 共享,追踪压缩历史并触发溢出合并)
|
||
collapse_log: Arc<std::sync::Mutex<compact::collapse::CollapseLog>>,
|
||
/// Checkpoint 管理器(跨 turn 共享,文件变更操作前自动快照)
|
||
checkpoint_manager: Arc<checkpoint::CheckpointManager>,
|
||
/// 当前运行模式(从 ModeRegistry 解析的静态引用)
|
||
mode: &'static AgentMode,
|
||
/// 模式注册表(持有所有已注册模式)
|
||
mode_registry: ModeRegistry,
|
||
}
|
||
|
||
impl AgentRuntime {
|
||
/// 创建新的运行时实例
|
||
pub fn new(app_state: Arc<AppState>) -> Self {
|
||
let mut config = AgentConfig::default();
|
||
let mode_registry = ModeRegistry::builtins();
|
||
let mode = mode_registry.get(&config.mode).copied().unwrap_or_else(|| {
|
||
tracing::warn!("[AgentRuntime] 未知模式 '{}',回退到默认模式", config.mode);
|
||
mode_registry
|
||
.get(ModeRegistry::default_id())
|
||
.copied()
|
||
.unwrap_or(&modes::default::DEFAULT_MODE)
|
||
});
|
||
// 合并模式配置预设到 AgentConfig
|
||
apply_mode_config(&mut config, mode);
|
||
|
||
let queue = Arc::new(BgNotificationQueue::new());
|
||
let metrics_data = Arc::new(std::sync::Mutex::new(super::hooks::MetricsData::default()));
|
||
let permission_checker = Arc::new(permission::PermissionChecker::from_config(&config));
|
||
let denial_tracker = Arc::new(std::sync::Mutex::new(denial_tracker::DenialTracker::new(
|
||
config.denial_max_consecutive,
|
||
config.denial_max_total,
|
||
)));
|
||
let skill_registry = app_state.skill_registry.clone();
|
||
let mut tool_registry = ToolRegistry::new_with_queue(Some(queue.clone()), skill_registry);
|
||
// 注册记忆工具
|
||
tool_registry.add_tool(Box::new(crate::agent::tools::memory::SaveMemoryTool::new(
|
||
app_state.memory_manager.clone(),
|
||
)));
|
||
// 替换 DelegateResearchTool 为带有 permission_checker 的版本(SSE 通道通过 ToolContext 注入)
|
||
tool_registry.replace_tool(Box::new(
|
||
crate::agent::tools::subagent::SubAgentTool::new_with_hooks(
|
||
None,
|
||
permission_checker.clone(),
|
||
),
|
||
));
|
||
// 初始化会话级权限检查器(与 AgentRuntime 使用相同的环境变量规则)
|
||
if let Ok(mut session_checker) = app_state.session_permission_checker.write() {
|
||
*session_checker = (*permission_checker).clone();
|
||
}
|
||
|
||
// 视觉模型可用时注册 analyze_image 工具
|
||
if app_state.vision_llm.is_some() {
|
||
tool_registry.add_tool(Box::new(crate::agent::tools::astro::AnalyzeImageTool));
|
||
}
|
||
|
||
// ── 应用模式的工具集过滤 ──
|
||
apply_mode_tool_filter(&mut tool_registry, mode);
|
||
|
||
// 初始化 checkpoint 管理器
|
||
let checkpoint_enabled = std::env::var("AGENT_CHECKPOINT_ENABLED")
|
||
.unwrap_or_else(|_| "true".to_string())
|
||
.to_lowercase()
|
||
!= "false";
|
||
let checkpoint_store = app_state.config.library_dir.join("..").join(".checkpoints");
|
||
let checkpoint_manager = Arc::new(checkpoint::CheckpointManager::new(
|
||
std::fs::canonicalize(&checkpoint_store).unwrap_or(checkpoint_store),
|
||
checkpoint_enabled,
|
||
));
|
||
|
||
AgentRuntime {
|
||
app_state,
|
||
config,
|
||
tool_registry,
|
||
bg_notification_queue: queue,
|
||
metrics_data,
|
||
compaction_breaker: Arc::new(std::sync::Mutex::new(
|
||
circuit_breaker::CompactionCircuitBreaker::new(),
|
||
)),
|
||
permission_checker,
|
||
denial_tracker,
|
||
read_file_state: Arc::new(std::sync::Mutex::new(file_cache::FileStateCache::new())),
|
||
prompt_cache: std::sync::Mutex::new(SystemPromptCache::new()),
|
||
collapse_log: Arc::new(std::sync::Mutex::new(compact::collapse::CollapseLog::new())),
|
||
checkpoint_manager,
|
||
mode,
|
||
mode_registry,
|
||
}
|
||
}
|
||
|
||
/// 创建带自定义配置的运行时实例
|
||
pub fn with_config(app_state: Arc<AppState>, mut config: AgentConfig) -> Self {
|
||
let mode_registry = ModeRegistry::builtins();
|
||
let mode = mode_registry.get(&config.mode).copied().unwrap_or_else(|| {
|
||
tracing::warn!("[AgentRuntime] 未知模式 '{}',回退到默认模式", config.mode);
|
||
mode_registry
|
||
.get(ModeRegistry::default_id())
|
||
.copied()
|
||
.unwrap_or(&modes::default::DEFAULT_MODE)
|
||
});
|
||
// 合并模式配置预设
|
||
apply_mode_config(&mut config, mode);
|
||
|
||
let queue = Arc::new(BgNotificationQueue::new());
|
||
let metrics_data = Arc::new(std::sync::Mutex::new(super::hooks::MetricsData::default()));
|
||
let permission_checker = Arc::new(permission::PermissionChecker::from_config(&config));
|
||
let denial_tracker = Arc::new(std::sync::Mutex::new(denial_tracker::DenialTracker::new(
|
||
config.denial_max_consecutive,
|
||
config.denial_max_total,
|
||
)));
|
||
let skill_registry = app_state.skill_registry.clone();
|
||
let mut tool_registry = ToolRegistry::new_with_queue(Some(queue.clone()), skill_registry);
|
||
tool_registry.add_tool(Box::new(crate::agent::tools::memory::SaveMemoryTool::new(
|
||
app_state.memory_manager.clone(),
|
||
)));
|
||
tool_registry.replace_tool(Box::new(
|
||
crate::agent::tools::subagent::SubAgentTool::new_with_hooks(
|
||
None,
|
||
permission_checker.clone(),
|
||
),
|
||
));
|
||
// 初始化会话级权限检查器
|
||
if let Ok(mut session_checker) = app_state.session_permission_checker.write() {
|
||
*session_checker = (*permission_checker).clone();
|
||
}
|
||
|
||
// 视觉模型可用时注册 analyze_image 工具
|
||
if app_state.vision_llm.is_some() {
|
||
tool_registry.add_tool(Box::new(crate::agent::tools::astro::AnalyzeImageTool));
|
||
}
|
||
|
||
// ── 应用模式的工具集过滤 ──
|
||
apply_mode_tool_filter(&mut tool_registry, mode);
|
||
|
||
// 初始化 checkpoint 管理器
|
||
let checkpoint_enabled = std::env::var("AGENT_CHECKPOINT_ENABLED")
|
||
.unwrap_or_else(|_| "true".to_string())
|
||
.to_lowercase()
|
||
!= "false";
|
||
let checkpoint_store = app_state.config.library_dir.join("..").join(".checkpoints");
|
||
let checkpoint_manager = Arc::new(checkpoint::CheckpointManager::new(
|
||
std::fs::canonicalize(&checkpoint_store).unwrap_or(checkpoint_store),
|
||
checkpoint_enabled,
|
||
));
|
||
|
||
AgentRuntime {
|
||
app_state,
|
||
config,
|
||
tool_registry,
|
||
bg_notification_queue: queue,
|
||
metrics_data,
|
||
compaction_breaker: Arc::new(std::sync::Mutex::new(
|
||
circuit_breaker::CompactionCircuitBreaker::new(),
|
||
)),
|
||
permission_checker,
|
||
denial_tracker,
|
||
read_file_state: Arc::new(std::sync::Mutex::new(file_cache::FileStateCache::new())),
|
||
prompt_cache: std::sync::Mutex::new(SystemPromptCache::new()),
|
||
collapse_log: Arc::new(std::sync::Mutex::new(compact::collapse::CollapseLog::new())),
|
||
checkpoint_manager,
|
||
mode,
|
||
mode_registry,
|
||
}
|
||
}
|
||
|
||
/// 返回当前运行指标快照(锁异常时返回默认值)
|
||
pub fn get_metrics(&self) -> super::hooks::MetricsData {
|
||
self.metrics_data
|
||
.lock()
|
||
.ok()
|
||
.map(|m| m.clone())
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
/// 设置是否启用 LLM 思考模式(向后兼容,优先使用 mode 设置)
|
||
pub fn with_thinking(mut self, enable: bool) -> Self {
|
||
self.config.enable_thinking = enable;
|
||
self
|
||
}
|
||
|
||
/// 设置运行模式(覆盖 AgentConfig 中的 mode 字段)。
|
||
///
|
||
/// 调用此方法会重新解析模式并应用对应的配置预设、工具过滤和权限档案。
|
||
pub fn with_mode(mut self, mode_id: &str) -> Self {
|
||
self.config.mode = mode_id.to_string();
|
||
let mode = self.mode_registry.get(mode_id).copied().unwrap_or_else(|| {
|
||
tracing::warn!(
|
||
"[AgentRuntime] with_mode: 未知模式 '{}',回退到默认模式",
|
||
mode_id
|
||
);
|
||
self.mode_registry
|
||
.get(ModeRegistry::default_id())
|
||
.copied()
|
||
.unwrap_or(&modes::default::DEFAULT_MODE)
|
||
});
|
||
apply_mode_config(&mut self.config, mode);
|
||
apply_mode_tool_filter(&mut self.tool_registry, mode);
|
||
self.mode = mode;
|
||
self
|
||
}
|
||
|
||
/// 返回当前模式的 ID。
|
||
pub fn mode_id(&self) -> &str {
|
||
self.mode.id
|
||
}
|
||
|
||
/// 返回模式是否强制固定了 thinking。`Some(true/false)` 表示模式已锁定,用户不可覆盖。
|
||
pub fn mode_fixed_thinking(&self) -> Option<bool> {
|
||
self.mode.mode_config.enable_thinking
|
||
}
|
||
|
||
// ── Private Helpers ──
|
||
|
||
/// 执行文件缓存快照 → 压缩 → 恢复 → 上下文注入 的完整周期。
|
||
/// 返回压缩前的消息数(用于调用者判断压缩是否有效)。
|
||
async fn snapshot_compress_restore(
|
||
&self,
|
||
messages: &mut Vec<ChatMessage>,
|
||
llm: &LlmClient,
|
||
session_id: &str,
|
||
hook_registry: &HookRegistry,
|
||
) -> usize {
|
||
let before_len = messages.len();
|
||
|
||
// ── 文件缓存快照(压缩前)──
|
||
let file_snapshot = {
|
||
if let Ok(mut cache) = self.read_file_state.lock() {
|
||
let snap = cache.to_snapshot();
|
||
cache.clear();
|
||
snap
|
||
} else {
|
||
Vec::new()
|
||
}
|
||
};
|
||
|
||
// 压缩前捕获消息快照(用于记忆提取桥接,P3)
|
||
let pre_compact_snapshot: Vec<crate::clients::llm::ChatMessage> = messages.to_vec();
|
||
|
||
compact::compress_context_with_hooks_and_log(
|
||
messages,
|
||
llm,
|
||
self.config.context_char_limit,
|
||
session_id,
|
||
Some(hook_registry),
|
||
Some(&self.collapse_log),
|
||
)
|
||
.await;
|
||
|
||
// 压缩后提取记忆(P3 桥接:将丢弃的消息内容喂给记忆提取子代理)
|
||
compact::extract_memories_from_compaction(
|
||
&pre_compact_snapshot,
|
||
session_id,
|
||
self.app_state.memory_manager.clone(),
|
||
self.app_state.clone(),
|
||
);
|
||
|
||
// ── 文件缓存恢复(压缩后:重新注入最近文件 + 恢复缓存)──
|
||
{
|
||
if let Ok(mut cache) = self.read_file_state.lock() {
|
||
cache.restore_from_snapshot(
|
||
&file_snapshot,
|
||
file_cache::POST_COMPACT_MAX_FILES_TO_RESTORE,
|
||
);
|
||
}
|
||
let restore_ctx = file_cache::FileStateCache::build_restore_context(
|
||
&file_snapshot,
|
||
file_cache::POST_COMPACT_MAX_FILES_TO_RESTORE,
|
||
);
|
||
for block in restore_ctx {
|
||
messages.push(ChatMessage::user(format!("[压缩后上下文恢复]\n{}", block)));
|
||
}
|
||
}
|
||
|
||
before_len
|
||
}
|
||
|
||
// ── Public API ──
|
||
|
||
/// 执行完整的智能体对话回合(流式 SSE 输出)。
|
||
///
|
||
/// 流程:
|
||
/// 1. 创建/恢复会话
|
||
/// 2. 构建消息上下文
|
||
/// 3. ReAct 循环
|
||
/// 4. 会话收尾
|
||
pub async fn run_turn(
|
||
&self,
|
||
session_id: Option<String>,
|
||
question: &str,
|
||
tx: mpsc::UnboundedSender<AgentStreamEvent>,
|
||
) -> anyhow::Result<String> {
|
||
self.run_turn_with_image_context(session_id, question, None, None, tx)
|
||
.await
|
||
}
|
||
|
||
/// 带图片上下文的对话回合。`image_context` 会在用户消息前注入为 system-reminder。
|
||
/// `image_path` 会存入用户消息的 metadata 以便前端渲染。
|
||
pub async fn run_turn_with_image_context(
|
||
&self,
|
||
session_id: Option<String>,
|
||
question: &str,
|
||
image_context: Option<String>,
|
||
image_path: Option<String>,
|
||
tx: mpsc::UnboundedSender<AgentStreamEvent>,
|
||
) -> anyhow::Result<String> {
|
||
let db = &self.app_state.db;
|
||
let llm = &self.app_state.llm;
|
||
|
||
// Phase 1: 创建或恢复会话
|
||
let session_info =
|
||
session::create_or_resume_session(db, session_id.clone(), llm, &self.config.mode)
|
||
.await?;
|
||
|
||
// 构建 hook 注册表(注入依赖,复用 AgentRuntime 的 metrics_data)
|
||
let hook_registry = HookRegistry::with_builtins(
|
||
db.clone(),
|
||
self.app_state.cancelled_runs.clone(),
|
||
Some(self.metrics_data.clone()),
|
||
);
|
||
|
||
// 触发 OnSessionStart
|
||
hook_registry
|
||
.run_on_session_start(&SessionStartContext {
|
||
session_id: session_info.session_id.clone(),
|
||
turn_index: session_info.turn_index,
|
||
is_resume: session_id.is_some(),
|
||
})
|
||
.await;
|
||
|
||
// 触发 UserPromptSubmit —— hooks 可在上下文构建前检查/记录用户输入
|
||
hook_registry
|
||
.run_on_user_prompt_submit(&UserPromptSubmitContext {
|
||
session_id: session_info.session_id.clone(),
|
||
prompt: question.to_string(),
|
||
turn_index: session_info.turn_index,
|
||
})
|
||
.await;
|
||
|
||
let _ = tx.send(AgentStreamEvent::Session {
|
||
session_id: session_info.session_id.clone(),
|
||
title: String::new(),
|
||
});
|
||
|
||
// Phase 2: 构建初始上下文(历史 + system prompt + 用户消息 + 任务恢复)
|
||
let mut messages = context::build_initial_context(
|
||
db,
|
||
&session_info.session_id,
|
||
&self.system_prompt(),
|
||
question,
|
||
session_info.turn_index,
|
||
)
|
||
.await?;
|
||
|
||
// 图片上下文:在用户消息前注入 system-reminder
|
||
if let Some(ref img_ctx) = image_context {
|
||
let reminder = format!("<system-reminder>\n{}\n</system-reminder>", img_ctx);
|
||
// 插入到倒数第二条位置(用户消息之前)
|
||
let user_msg = messages.pop().unwrap(); // 用户消息
|
||
messages.push(ChatMessage::user(&reminder));
|
||
messages.push(user_msg);
|
||
}
|
||
|
||
// 保存用户消息到数据库(含图片路径元数据,供前端历史渲染)
|
||
let user_metadata = image_path
|
||
.as_ref()
|
||
.map(|p| serde_json::json!({"image_path": p}));
|
||
self.save_message(
|
||
db,
|
||
&session_info.session_id,
|
||
session_info.turn_index,
|
||
0,
|
||
&ChatMessage::user(question),
|
||
None,
|
||
user_metadata,
|
||
)
|
||
.await?;
|
||
|
||
// Phase 3: ReAct 循环
|
||
let (metrics, loop_terminal) = self
|
||
.run_react_loop(&session_info, &mut messages, &tx, &hook_registry)
|
||
.await?;
|
||
|
||
// Phase 4: 会话收尾(传入实际的终止原因 + trajectory 导出参数)
|
||
let system_prompt = self.system_prompt();
|
||
let model_name = self.app_state.llm.model();
|
||
finalize::finalize_turn(
|
||
db,
|
||
&session_info.session_id,
|
||
&metrics,
|
||
question,
|
||
&tx,
|
||
&hook_registry,
|
||
loop_terminal,
|
||
Some(&self.app_state.config.library_dir),
|
||
Some(&model_name),
|
||
Some(&system_prompt),
|
||
Some(self.app_state.clone()),
|
||
)
|
||
.await?;
|
||
|
||
Ok(session_info.session_id)
|
||
}
|
||
|
||
// ── ReAct Loop ──
|
||
|
||
/// ReAct 循环核心:LLM 调用 → 工具执行 → 结果注入 → 循环...
|
||
async fn run_react_loop(
|
||
&self,
|
||
session_info: &SessionInfo,
|
||
messages: &mut Vec<ChatMessage>,
|
||
tx: &mpsc::UnboundedSender<AgentStreamEvent>,
|
||
hook_registry: &HookRegistry,
|
||
) -> anyhow::Result<(AgentMetrics, Option<TurnTerminal>)> {
|
||
let db = &self.app_state.db;
|
||
let llm = &self.app_state.llm;
|
||
let sid = &session_info.session_id;
|
||
let turn_index = session_info.turn_index;
|
||
|
||
let tool_defs = self.tool_registry.definitions();
|
||
let mut duplicate_detector = DuplicateDetector::default();
|
||
let mut metrics = AgentMetrics::default();
|
||
let mut step = 0;
|
||
let mut loop_terminal: Option<TurnTerminal> = None;
|
||
|
||
// Token 追踪(API 精确值优先,字符估算作近似值)
|
||
let mut last_api_prompt_tokens: Option<u32> = None;
|
||
let mut msg_count_at_last_call: usize = messages.len();
|
||
let mut steps_since_last_todo: usize = 0;
|
||
let nag_after_steps: usize = 3;
|
||
let mut pending_manual_compress: bool = false;
|
||
|
||
// Token 预算管理器(用于 diminishing returns 检测和 error recovery)
|
||
let mut token_budget =
|
||
TokenBudget::new(self.config.token_soft_limit, self.config.token_hard_limit);
|
||
|
||
loop {
|
||
step += 1;
|
||
|
||
// ── Checkpoint: 每个 ReAct 迭代开始时重置去重状态 ──
|
||
self.checkpoint_manager.new_turn();
|
||
|
||
// 检查用户取消
|
||
let is_cancelled = {
|
||
if let Ok(mut cancelled) = self.app_state.cancelled_runs.lock() {
|
||
cancelled.remove(sid)
|
||
} else {
|
||
false
|
||
}
|
||
};
|
||
|
||
if is_cancelled {
|
||
warn!("[AgentRuntime] 用户手动中止了会话 {} 的智能体执行", sid);
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: "用户已手动中止执行。".to_string(),
|
||
});
|
||
loop_terminal = Some(TurnTerminal::CancelledByUser {
|
||
session_id: sid.clone(),
|
||
at_step: step,
|
||
});
|
||
break;
|
||
}
|
||
|
||
// ── 上下文压缩检查 ──
|
||
// ── Token 感知的压缩触发 ──
|
||
// 优先使用 API 返回的精确 prompt_tokens,辅以简单的增量估算
|
||
let estimated_tokens = match last_api_prompt_tokens {
|
||
Some(last_tokens) => {
|
||
let new_msg_count = messages.len().saturating_sub(msg_count_at_last_call);
|
||
let new_tokens_estimate: u32 = messages
|
||
.iter()
|
||
.rev()
|
||
.take(new_msg_count)
|
||
.map(|m| (m.content.as_ref().map_or(0, |c| c.len()) + 4) as u32)
|
||
.sum();
|
||
(last_tokens + new_tokens_estimate) as usize
|
||
}
|
||
None => compact::rough_estimate_tokens(messages),
|
||
};
|
||
|
||
// 使用 token 预算的软限制作为压缩触发点(而非粗糙的 context_char_limit * 1.5)
|
||
let token_limit = token_budget.soft_limit;
|
||
|
||
let mut did_compress = false;
|
||
|
||
// 熔断器检查:如果连续压缩失败多次,跳过自动压缩
|
||
let breaker_ok = match self.compaction_breaker.lock() {
|
||
Ok(mut breaker) => breaker.can_attempt(),
|
||
Err(e) => {
|
||
warn!("[AgentRuntime] 熔断器锁异常,跳过自动压缩: {:?}", e);
|
||
false
|
||
}
|
||
};
|
||
|
||
if estimated_tokens > token_limit && breaker_ok {
|
||
info!(
|
||
"[AgentRuntime] 上下文超限 (est. {} tokens > {} limit),触发压缩",
|
||
estimated_tokens, token_limit
|
||
);
|
||
let before_len = self
|
||
.snapshot_compress_restore(
|
||
messages,
|
||
llm,
|
||
&session_info.session_id,
|
||
hook_registry,
|
||
)
|
||
.await;
|
||
|
||
// 熔断器反馈:压缩后消息数减少 = 成功
|
||
if let Ok(mut breaker) = self.compaction_breaker.lock() {
|
||
if messages.len() < before_len {
|
||
breaker.record_success();
|
||
} else {
|
||
breaker.record_failure();
|
||
}
|
||
} else {
|
||
warn!("[AgentRuntime] 熔断器反馈写入失败(锁异常)");
|
||
}
|
||
last_api_prompt_tokens = None;
|
||
msg_count_at_last_call = messages.len();
|
||
metrics.compression_count += 1;
|
||
did_compress = true;
|
||
} else if estimated_tokens > token_limit && !breaker_ok {
|
||
warn!("[AgentRuntime] 熔断器已打开,跳过自动压缩");
|
||
}
|
||
|
||
// 处理手动压缩请求(跳过刚自动压缩过的情况,避免双重压缩)
|
||
// 手动压缩不受熔断器限制
|
||
if pending_manual_compress && !did_compress {
|
||
pending_manual_compress = false;
|
||
info!("[AgentRuntime] 执行手动压缩(compress_context 工具触发)");
|
||
|
||
self.snapshot_compress_restore(
|
||
messages,
|
||
llm,
|
||
&session_info.session_id,
|
||
hook_registry,
|
||
)
|
||
.await;
|
||
|
||
// 手动压缩成功后重置熔断器
|
||
if let Ok(mut breaker) = self.compaction_breaker.lock() {
|
||
breaker.reset();
|
||
}
|
||
last_api_prompt_tokens = None;
|
||
msg_count_at_last_call = messages.len();
|
||
metrics.compression_count += 1;
|
||
} else if pending_manual_compress {
|
||
pending_manual_compress = false;
|
||
info!("[AgentRuntime] 跳过手动压缩(刚已完成自动压缩)");
|
||
}
|
||
|
||
// Token 预算 diminishing returns 检测 + 渐进式 nudge 提醒
|
||
token_budget.record_continuation();
|
||
|
||
let mut should_nudge = false;
|
||
|
||
// TodoWrite nag reminder
|
||
if steps_since_last_todo >= nag_after_steps {
|
||
messages.push(ChatMessage::user(
|
||
"提醒:你已经连续多步未更新任务计划。建议调用 todo_write 工具复盘当前进度并规划后续步骤。",
|
||
));
|
||
steps_since_last_todo = 0;
|
||
should_nudge = true;
|
||
}
|
||
|
||
// Token 预算 nudge(仅在无 nag 时注入,避免消息过多)
|
||
if !should_nudge {
|
||
if let Some(nudge) = token_budget.nudge_message() {
|
||
messages.push(ChatMessage::user(nudge));
|
||
}
|
||
}
|
||
|
||
// Diminishing returns 检测 — 强制结束
|
||
if token_budget.diminishing_returns {
|
||
warn!("[AgentRuntime] 检测到 diminishing returns,强制结束循环");
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: "检测到重复操作模式,已自动停止。请查看已收集的信息。".to_string(),
|
||
});
|
||
messages.push(ChatMessage::user(
|
||
"检测到你的后续步骤未产生新信息(diminishing returns)。\
|
||
请基于已收集的全部信息直接给出最终答案,不要再调用任何工具。",
|
||
));
|
||
let _ = self
|
||
.final_answer_without_tools(llm, messages, sid, turn_index, step, tx)
|
||
.await;
|
||
break;
|
||
}
|
||
|
||
// 最大步数检查
|
||
if step > self.config.max_steps {
|
||
warn!(
|
||
"[AgentRuntime] 达到最大步数限制 ({} steps)",
|
||
self.config.max_steps
|
||
);
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: format!(
|
||
"已达到最大推理步数 ({}),请根据已收集的信息给出最终回答。",
|
||
self.config.max_steps
|
||
),
|
||
});
|
||
messages.push(ChatMessage::user(format!(
|
||
"你已经执行了 {} 步(最大 {} 步)。请根据已有信息直接给出最终答案,不要再调用工具。",
|
||
step, self.config.max_steps
|
||
)));
|
||
let _ = self
|
||
.final_answer_without_tools(llm, messages, sid, turn_index, step, tx)
|
||
.await;
|
||
break;
|
||
}
|
||
|
||
// ── 后台任务通知注入 ──
|
||
let bg_results = self.bg_notification_queue.drain().await;
|
||
for result in bg_results {
|
||
let status = if result.is_error { "❌" } else { "✅" };
|
||
messages.push(ChatMessage::user(format!(
|
||
"[后台任务完成] {} {}: {} ({}): {}",
|
||
status, result.tool_name, result.bibcode, result.task_id, result.summary,
|
||
)));
|
||
}
|
||
|
||
// ── LLM 流式调用(含错误恢复) ──
|
||
let stream_output_opt = self
|
||
.call_llm_with_recovery(llm, messages, &tool_defs, tx, step, sid, &mut token_budget)
|
||
.await;
|
||
|
||
let stream_output = match stream_output_opt {
|
||
Some(output) => output,
|
||
None => {
|
||
// 所有恢复尝试均失败
|
||
loop_terminal = Some(TurnTerminal::ModelError {
|
||
session_id: sid.clone(),
|
||
message: "LLM 调用失败,所有恢复步骤已尝试完毕".to_string(),
|
||
});
|
||
break;
|
||
}
|
||
};
|
||
|
||
// 更新 API 精确 token 计数 + token 预算
|
||
if let Some(ref u) = stream_output.usage {
|
||
last_api_prompt_tokens = Some(u.prompt_tokens);
|
||
msg_count_at_last_call = messages.len();
|
||
token_budget.spend_input(u.prompt_tokens as usize);
|
||
token_budget.spend_output(u.completion_tokens as usize);
|
||
}
|
||
|
||
// ── 处理 Thought/Reasoning ──
|
||
let mut thought_content = stream_output.reasoning.clone();
|
||
|
||
if thought_content.is_none()
|
||
&& stream_output.is_tool_call_step
|
||
&& !stream_output.content.is_empty()
|
||
{
|
||
thought_content = Some(stream_output.content.clone());
|
||
}
|
||
|
||
if stream_output.is_tool_call_step {
|
||
if let Some(ref thought_text) = thought_content {
|
||
let _ = tx.send(AgentStreamEvent::Thought {
|
||
content: thought_text.clone(),
|
||
step,
|
||
});
|
||
}
|
||
}
|
||
|
||
// ── 无工具调用 = 最终回答 ──
|
||
let mut tool_calls = match stream_output.tool_calls {
|
||
Some(ref tc) if !tc.is_empty() => tc.clone(),
|
||
_ => {
|
||
// 保存最终回答
|
||
let assistant_msg = ChatMessage::assistant_with_reasoning(
|
||
if stream_output.content.is_empty() {
|
||
None
|
||
} else {
|
||
Some(stream_output.content.clone())
|
||
},
|
||
stream_output.reasoning.clone(),
|
||
None,
|
||
);
|
||
self.save_message(
|
||
db,
|
||
sid,
|
||
turn_index,
|
||
step as i32,
|
||
&assistant_msg,
|
||
stream_output.reasoning.as_deref(),
|
||
None,
|
||
)
|
||
.await?;
|
||
messages.push(assistant_msg);
|
||
|
||
// 发送 reasoning(当模型思考后直接给出答案、未调用工具时,
|
||
// thought 尚未在上面的 is_tool_call_step 块中发送)
|
||
if let Some(ref thought_text) = stream_output.reasoning {
|
||
let _ = tx.send(AgentStreamEvent::Thought {
|
||
content: thought_text.clone(),
|
||
step,
|
||
});
|
||
}
|
||
|
||
// Token 使用统计
|
||
if let Some(u) = stream_output.usage {
|
||
let _ = tx.send(AgentStreamEvent::Usage {
|
||
prompt_tokens: u.prompt_tokens,
|
||
completion_tokens: u.completion_tokens,
|
||
total_tokens: u.total_tokens,
|
||
});
|
||
}
|
||
break;
|
||
}
|
||
};
|
||
|
||
// 修复空 ID(LLM 可能不返回 tool_call id)
|
||
for tc in tool_calls.iter_mut() {
|
||
if tc.id.is_empty() {
|
||
tc.id = format!("call_{}", &uuid::Uuid::new_v4().to_string()[..8]);
|
||
}
|
||
}
|
||
|
||
// ── 工具调用处理 ──
|
||
// 检测 todo_write 和 compress_context
|
||
let called_todo_write = tool_calls.iter().any(|tc| tc.function.name == "todo_write");
|
||
if called_todo_write {
|
||
steps_since_last_todo = 0;
|
||
} else {
|
||
steps_since_last_todo += 1;
|
||
}
|
||
|
||
if tool_calls
|
||
.iter()
|
||
.any(|tc| tc.function.name == "compress_context")
|
||
{
|
||
pending_manual_compress = true;
|
||
}
|
||
|
||
// 更新指标
|
||
metrics.total_steps = step;
|
||
for tc in &tool_calls {
|
||
*metrics
|
||
.tool_calls
|
||
.entry(tc.function.name.clone())
|
||
.or_insert(0) += 1;
|
||
}
|
||
|
||
// 构建 assistant 消息(含 tool_calls)
|
||
let assistant_msg = ChatMessage::assistant_with_reasoning(
|
||
if stream_output.content.is_empty() {
|
||
None
|
||
} else {
|
||
Some(stream_output.content.clone())
|
||
},
|
||
stream_output.reasoning.clone(),
|
||
Some(tool_calls.clone()),
|
||
);
|
||
self.save_message(
|
||
db,
|
||
sid,
|
||
turn_index,
|
||
step as i32,
|
||
&assistant_msg,
|
||
stream_output.reasoning.as_deref(),
|
||
None,
|
||
)
|
||
.await?;
|
||
messages.push(assistant_msg);
|
||
|
||
// 验证 + 准备工具调用
|
||
let (prepared_calls, has_duplicate) = executor::validate_and_prepare(
|
||
&tool_calls,
|
||
&mut duplicate_detector,
|
||
self.config.duplicate_call_threshold,
|
||
messages,
|
||
tx,
|
||
db,
|
||
sid,
|
||
turn_index,
|
||
step,
|
||
);
|
||
|
||
if has_duplicate {
|
||
metrics.duplicate_detections += 1;
|
||
continue;
|
||
}
|
||
|
||
if prepared_calls.is_empty() {
|
||
continue;
|
||
}
|
||
|
||
// 并行执行工具(带权限检查、checkpoint 和分区器)
|
||
let exec_result = executor::execute_parallel(
|
||
&prepared_calls,
|
||
&self.tool_registry,
|
||
self.app_state.clone(),
|
||
hook_registry,
|
||
Some(&self.permission_checker),
|
||
Some(&self.app_state.session_permission_checker),
|
||
Some(&self.denial_tracker),
|
||
Some(&self.checkpoint_manager),
|
||
tx,
|
||
db,
|
||
sid,
|
||
"lead",
|
||
turn_index,
|
||
step,
|
||
self.config.tool_timeout_secs,
|
||
self.config.max_tool_output_chars,
|
||
self.read_file_state.clone(),
|
||
self.config.enable_thinking,
|
||
self.config.additional_allowed_dirs.clone(),
|
||
)
|
||
.await;
|
||
|
||
// 拒绝追踪熔断检查:连续/累计拒绝达到阈值则终止循环
|
||
if let Ok(dt) = self.denial_tracker.lock() {
|
||
if dt.should_terminate() {
|
||
let reason = dt.termination_reason();
|
||
warn!("[AgentRuntime] 拒绝熔断触发: {}", reason);
|
||
let _ = tx.send(AgentStreamEvent::Error { message: reason });
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 将工具结果推入消息上下文
|
||
for tm in exec_result.tool_messages {
|
||
messages.push(tm.chat_message);
|
||
}
|
||
|
||
// Hook 注入的附加上下文:包装为 system-reminder 注入 LLM 消息列表
|
||
// 使用 ContextDeduplicator 在单步内去重(多个 hook 注入相同内容时只保留一份)
|
||
let mut dedup = crate::agent::hooks::ContextDeduplicator::new();
|
||
for ctx in &exec_result.hook_contexts {
|
||
if dedup.is_duplicate(ctx) {
|
||
continue;
|
||
}
|
||
let reminder = format!(
|
||
"<system-reminder>\n[Hook 注入上下文]\n{}\n</system-reminder>",
|
||
ctx
|
||
);
|
||
messages.push(ChatMessage::user(&reminder));
|
||
}
|
||
|
||
// Hook 阻塞错误:记录到日志用于诊断
|
||
for be in &exec_result.blocking_errors {
|
||
warn!("[AgentRuntime] Hook 阻塞错误: {}", be);
|
||
}
|
||
|
||
// 持久化 todo_write 任务状态到数据库
|
||
if called_todo_write {
|
||
for prep in &prepared_calls {
|
||
if prep.tool_name == "todo_write" {
|
||
if let Some(todos) = prep.args.get("todos").and_then(|t| t.as_array()) {
|
||
let todos_vec: Vec<serde_json::Value> = todos.to_vec();
|
||
let _ = crate::agent::tools::persist_tasks(db, sid, &todos_vec, "lead")
|
||
.await;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if exec_result.was_cancelled {
|
||
if let Ok(mut locked) = self.app_state.cancelled_runs.lock() {
|
||
locked.remove(sid);
|
||
}
|
||
warn!(
|
||
"[AgentRuntime] 工具执行期间被用户手动中止,会话 ID: {}",
|
||
sid
|
||
);
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: "用户已手动中止执行。".to_string(),
|
||
});
|
||
loop_terminal = Some(TurnTerminal::CancelledByUser {
|
||
session_id: sid.clone(),
|
||
at_step: step,
|
||
});
|
||
break;
|
||
}
|
||
|
||
// OnStepComplete hook
|
||
let step_ctx = StepCompleteContext {
|
||
session_id: sid.clone(),
|
||
step,
|
||
max_steps: self.config.max_steps,
|
||
messages_count: messages.len(),
|
||
estimated_tokens,
|
||
token_limit,
|
||
};
|
||
hook_registry.run_on_step_complete(&step_ctx).await;
|
||
}
|
||
|
||
Ok((metrics, loop_terminal))
|
||
}
|
||
|
||
/// LLM 流式调用,含完整的错误恢复阶梯。
|
||
///
|
||
/// 首次调用失败后,按顺序尝试:
|
||
/// 1. AggressiveCompact (keep_recent=2)
|
||
/// 2. ReactiveCompact (LLM 摘要)
|
||
/// 3. EscalateTokens (提升 hard_limit → 64k)
|
||
/// 4. MultiTurn (注入分步消息)
|
||
/// 5. Surface (放弃)
|
||
///
|
||
/// 每一步后重试 LLM 调用。返回 Some(StreamOutput) 表示成功(可能经过恢复),
|
||
/// None 表示所有步骤均已尝试且失败。
|
||
#[allow(clippy::too_many_arguments)]
|
||
async fn call_llm_with_recovery(
|
||
&self,
|
||
llm: &LlmClient,
|
||
messages: &mut Vec<ChatMessage>,
|
||
tool_defs: &[crate::clients::llm::ToolDefinition],
|
||
tx: &mpsc::UnboundedSender<AgentStreamEvent>,
|
||
step: usize,
|
||
session_id: &str,
|
||
token_budget: &mut TokenBudget,
|
||
) -> Option<StreamOutput> {
|
||
// 首次尝试
|
||
let output = streaming::process_llm_stream(
|
||
llm,
|
||
messages,
|
||
tool_defs,
|
||
tx,
|
||
step,
|
||
session_id,
|
||
self.app_state.cancelled_runs.clone(),
|
||
self.config.enable_thinking,
|
||
)
|
||
.await;
|
||
|
||
match output.status {
|
||
StreamStatus::Success => return Some(output),
|
||
StreamStatus::Cancelled => {
|
||
if let Ok(mut cancelled) = self.app_state.cancelled_runs.lock() {
|
||
cancelled.remove(session_id);
|
||
}
|
||
warn!(
|
||
"[AgentRuntime] 流式调用期间被用户手动中止,会话 ID: {}",
|
||
session_id
|
||
);
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: "用户已手动中止执行。".to_string(),
|
||
});
|
||
return None;
|
||
}
|
||
StreamStatus::Error(ref e_str) => {
|
||
error!("[AgentRuntime] 流式读取错误: {}", e_str);
|
||
}
|
||
}
|
||
|
||
// 提取错误字符串(用于分类)
|
||
let e_str = match &output.status {
|
||
StreamStatus::Error(s) => s.clone(),
|
||
_ => return Some(output), // 不应到达,但安全起见
|
||
};
|
||
|
||
let error_kind = classify_error(&e_str);
|
||
|
||
// ── 429/529 瞬态错误:指数退避重试(独立的快速路径) ──
|
||
if matches!(error_kind, ErrorKind::RateLimited | ErrorKind::Overloaded) {
|
||
let retry_after_secs = error_recovery::parse_retry_after(&e_str);
|
||
let mut consecutive_overloads: u32 = 0;
|
||
const MAX_BACKOFF_RETRIES: u32 = 10;
|
||
|
||
for attempt in 0..MAX_BACKOFF_RETRIES {
|
||
let delay_ms = error_recovery::backoff_delay(attempt, retry_after_secs);
|
||
info!(
|
||
"[AgentRuntime] 退避重试 {}/{} ({}ms, error={:?})",
|
||
attempt + 1,
|
||
MAX_BACKOFF_RETRIES,
|
||
delay_ms,
|
||
error_kind
|
||
);
|
||
|
||
let _ = tx.send(AgentStreamEvent::Thought {
|
||
content: format!(
|
||
"⏳ 模型服务暂时不可用,正在重试 ({}/{})...",
|
||
attempt + 1,
|
||
MAX_BACKOFF_RETRIES
|
||
),
|
||
step,
|
||
});
|
||
|
||
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
|
||
|
||
// 529 连续过载检测:3 次后尝试切换备用模型
|
||
if matches!(error_kind, ErrorKind::Overloaded) {
|
||
consecutive_overloads += 1;
|
||
if consecutive_overloads >= 3 {
|
||
let fallback = &self.app_state.config.llm_fallback_model;
|
||
if !fallback.is_empty() {
|
||
warn!(
|
||
"[AgentRuntime] 连续 {} 次过载,切换到备用模型: {}",
|
||
consecutive_overloads, fallback
|
||
);
|
||
llm.set_model(fallback.clone());
|
||
consecutive_overloads = 0;
|
||
} else if !self.app_state.config.llm_fallback_chain.is_empty() {
|
||
let idx = ((consecutive_overloads as usize - 3)
|
||
% self.app_state.config.llm_fallback_chain.len())
|
||
.min(self.app_state.config.llm_fallback_chain.len() - 1);
|
||
let alt = &self.app_state.config.llm_fallback_chain[idx];
|
||
warn!(
|
||
"[AgentRuntime] 连续 {} 次过载,从链中切换: {}",
|
||
consecutive_overloads, alt
|
||
);
|
||
llm.set_model(alt.clone());
|
||
consecutive_overloads = 0;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检查用户取消
|
||
if let Ok(cancelled) = self.app_state.cancelled_runs.lock() {
|
||
if cancelled.contains(session_id) {
|
||
warn!("[AgentRuntime] 退避重试期间被用户取消");
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: "用户已手动中止执行。".to_string(),
|
||
});
|
||
return None;
|
||
}
|
||
}
|
||
|
||
// 重试 LLM 调用
|
||
let retry_output = streaming::process_llm_stream(
|
||
llm,
|
||
messages,
|
||
tool_defs,
|
||
tx,
|
||
step,
|
||
session_id,
|
||
self.app_state.cancelled_runs.clone(),
|
||
self.config.enable_thinking,
|
||
)
|
||
.await;
|
||
|
||
match retry_output.status {
|
||
StreamStatus::Success => {
|
||
info!("[AgentRuntime] 退避重试成功!(尝试 {})", attempt + 1);
|
||
return Some(retry_output);
|
||
}
|
||
StreamStatus::Cancelled => {
|
||
if let Ok(mut cancelled) = self.app_state.cancelled_runs.lock() {
|
||
cancelled.remove(session_id);
|
||
}
|
||
return None;
|
||
}
|
||
StreamStatus::Error(_) => {
|
||
// 继续重试
|
||
continue;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 所有退避重试失败
|
||
warn!(
|
||
"[AgentRuntime] {} 次退避重试后仍然失败",
|
||
MAX_BACKOFF_RETRIES
|
||
);
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: format!(
|
||
"模型服务暂时不可用(已重试 {} 次)。请稍后再试或检查模型服务状态。",
|
||
MAX_BACKOFF_RETRIES
|
||
),
|
||
});
|
||
return None;
|
||
}
|
||
|
||
if !ErrorRecovery::is_recoverable(&error_kind) {
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: format!("大模型流式读取失败: {}", e_str),
|
||
});
|
||
return None;
|
||
}
|
||
|
||
let mut recovery = ErrorRecovery::new(token_budget.clone());
|
||
|
||
// 尝试从错误消息中解析 ContextOverflow 信息(参考 Claude Code 自动修复)
|
||
let overflow_info = error_recovery::parse_context_overflow(&e_str);
|
||
|
||
while let Some(recovery_step) = recovery.try_recover(&error_kind, overflow_info.as_ref()) {
|
||
match recovery_step {
|
||
error_recovery::RecoveryStep::AdjustMaxTokens { new_max_tokens } => {
|
||
info!(
|
||
"[AgentRuntime] 错误恢复: AdjustMaxTokens → {} (从错误消息自动计算)",
|
||
new_max_tokens
|
||
);
|
||
// token_budget.hard_limit 已由 try_recover 下调
|
||
}
|
||
error_recovery::RecoveryStep::RetryWithBackoff { attempt, delay_ms } => {
|
||
// 429/529 本应在 streaming 层处理,若到达此处说明分类逻辑有变更,
|
||
// 安全降级为 sleep + 直接重试(不依赖 streaming 层重试)。
|
||
warn!(
|
||
"[AgentRuntime] RetryWithBackoff 在 error_recovery 层触发 (attempt={}, delay={}ms),执行降级重试",
|
||
attempt, delay_ms
|
||
);
|
||
tokio::time::sleep(std::time::Duration::from_millis(delay_ms)).await;
|
||
// 不计入 recovery 计数,由外层循环自然重试
|
||
}
|
||
error_recovery::RecoveryStep::AggressiveCompact => {
|
||
info!("[AgentRuntime] 错误恢复: 激进压缩 (snip + micro with keep_recent=2)");
|
||
compact::snip_compact(messages, self.config.max_messages);
|
||
compact::micro_compact(messages, 2);
|
||
}
|
||
error_recovery::RecoveryStep::ReactiveCompact => {
|
||
info!("[AgentRuntime] 错误恢复: LLM 摘要压缩");
|
||
compact::compress_context(
|
||
messages,
|
||
llm,
|
||
self.config.context_char_limit,
|
||
session_id,
|
||
)
|
||
.await;
|
||
}
|
||
error_recovery::RecoveryStep::EscalateTokens { .. } => {
|
||
info!(
|
||
"[AgentRuntime] 错误恢复: 提升 token 硬限制到 {}",
|
||
recovery.token_budget.hard_limit
|
||
);
|
||
}
|
||
error_recovery::RecoveryStep::MultiTurn => {
|
||
info!("[AgentRuntime] 错误恢复: 注入多轮消息");
|
||
messages.push(ChatMessage::user(ErrorRecovery::multi_turn_message()));
|
||
}
|
||
error_recovery::RecoveryStep::Surface => {
|
||
warn!("[AgentRuntime] 错误恢复: 所有步骤失败,暴露错误");
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 重试 LLM 调用
|
||
let retry_output = streaming::process_llm_stream(
|
||
llm,
|
||
messages,
|
||
tool_defs,
|
||
tx,
|
||
step,
|
||
session_id,
|
||
self.app_state.cancelled_runs.clone(),
|
||
self.config.enable_thinking,
|
||
)
|
||
.await;
|
||
|
||
match retry_output.status {
|
||
StreamStatus::Success => {
|
||
info!("[AgentRuntime] 错误恢复成功!");
|
||
// 将恢复后的 token_budget 状态同步回去
|
||
*token_budget = recovery.token_budget.clone();
|
||
return Some(retry_output);
|
||
}
|
||
StreamStatus::Cancelled => {
|
||
if let Ok(mut cancelled) = self.app_state.cancelled_runs.lock() {
|
||
cancelled.remove(session_id);
|
||
}
|
||
warn!("[AgentRuntime] 恢复期间被用户中止");
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: "用户已手动中止执行。".to_string(),
|
||
});
|
||
return None;
|
||
}
|
||
StreamStatus::Error(retry_err) => {
|
||
info!(
|
||
"[AgentRuntime] 恢复步骤 {:?} 未能解决,继续下一阶梯: {}",
|
||
recovery_step, retry_err
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 所有恢复步骤均已尝试
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: format!("大模型流式读取失败,且所有恢复步骤均未能解决: {}", e_str),
|
||
});
|
||
None
|
||
}
|
||
|
||
// ── Helpers ──
|
||
|
||
/// 系统提示词(模块化组装)。
|
||
///
|
||
/// 设计原则:
|
||
/// 1. 所有静态 section 在前 → 内容不变,服务端自然缓存
|
||
/// 2. 动态 section(environment/tools/skills/memory)在后
|
||
/// 3. 使用 SystemPromptCache:首次计算后永久复用,/clear 时失效
|
||
/// 4. 模式(AgentMode)可覆盖 identity 和 principles section
|
||
fn system_prompt(&self) -> String {
|
||
use self::system_prompt::{
|
||
SystemPrompt, IDENTITY_SECTION, PRINCIPLES_SECTION, SAFETY_SECTION,
|
||
SYSTEM_CONTEXT_SECTION, TOOL_USAGE_SECTION,
|
||
};
|
||
|
||
let mut sp = SystemPrompt::new();
|
||
|
||
// ═══════ 静态 section(首次计算后永久缓存)═══════
|
||
// 注意:identity 和 principles 可能被 mode 覆盖
|
||
|
||
let mut cache = self.prompt_cache.lock().unwrap_or_else(|e| {
|
||
tracing::warn!("[SystemPrompt] 缓存锁异常: {:?}", e);
|
||
e.into_inner()
|
||
});
|
||
|
||
// Identity:优先使用模式的覆盖,否则使用默认
|
||
let identity = match self.mode.identity_override {
|
||
Some(override_text) => override_text.to_string(),
|
||
None => cache.get_or_compute("identity", || IDENTITY_SECTION.to_string()),
|
||
};
|
||
sp.add_section("identity", identity);
|
||
|
||
// Principles:优先使用模式的覆盖,否则使用默认
|
||
let principles = match self.mode.principles_override {
|
||
Some(override_text) => override_text.to_string(),
|
||
None => cache.get_or_compute("principles", || PRINCIPLES_SECTION.to_string()),
|
||
};
|
||
sp.add_section("principles", principles);
|
||
|
||
// 模式的额外 section(追加在静态 section 之后)
|
||
for (name, content) in self.mode.extra_sections {
|
||
sp.add_section(name, content.to_string());
|
||
}
|
||
|
||
sp.add_section(
|
||
"system_context",
|
||
cache.get_or_compute("system_context", || SYSTEM_CONTEXT_SECTION.to_string()),
|
||
);
|
||
sp.add_section(
|
||
"tool_usage",
|
||
cache.get_or_compute("tool_usage", || TOOL_USAGE_SECTION.to_string()),
|
||
);
|
||
sp.add_section(
|
||
"safety",
|
||
cache.get_or_compute("safety", || SAFETY_SECTION.to_string()),
|
||
);
|
||
|
||
// ═══════ 动态 section(首次计算后缓存,session 内不变)═══════
|
||
|
||
// 环境上下文:CWD/platform/OS/model 在 session 内不变
|
||
let env_section = cache.get_or_compute("environment", || self.build_environment_section());
|
||
sp.add_section("environment", env_section);
|
||
|
||
// 工具目录:使用 tool_catalog() 列出常驻+延迟工具(P3 defer_loading 集成)
|
||
let tools_section = cache.get_or_compute("tools", || {
|
||
let catalog = self.tool_registry.tool_catalog();
|
||
format!(
|
||
"你可以使用以下工具([deferred] 标记的工具需要通过 load_skill 发现详情):\n{}",
|
||
catalog
|
||
)
|
||
});
|
||
sp.add_section("tools", tools_section);
|
||
|
||
drop(cache);
|
||
|
||
// 技能列表:通过文件监听热更新,不缓存
|
||
if let Some(skills) = self
|
||
.app_state
|
||
.skill_registry
|
||
.read()
|
||
.ok()
|
||
.and_then(|r| r.build_reminder())
|
||
{
|
||
sp.add_section("skills", skills);
|
||
}
|
||
|
||
// 项目记忆:受 save_memory 工具实时影响,不缓存
|
||
if let Some(memory) = self
|
||
.app_state
|
||
.memory_manager
|
||
.try_lock()
|
||
.ok()
|
||
.and_then(|mgr| mgr.build_system_reminder(5))
|
||
{
|
||
sp.add_section("memory", memory);
|
||
}
|
||
|
||
sp.assemble()
|
||
}
|
||
|
||
/// 使提示词缓存中指定 section 失效。
|
||
pub fn invalidate_prompt_cache(&self, section_name: &'static str) {
|
||
if let Ok(mut cache) = self.prompt_cache.lock() {
|
||
cache.invalidate(section_name);
|
||
}
|
||
}
|
||
|
||
/// 使所有提示词缓存失效(`/clear` 或 `/compact` 事件触发)。
|
||
pub fn invalidate_all_prompt_cache(&self) {
|
||
if let Ok(mut cache) = self.prompt_cache.lock() {
|
||
cache.invalidate_all();
|
||
}
|
||
}
|
||
|
||
/// 构建环境上下文 section(参考 Claude Code `computeEnvInfo()`)。
|
||
///
|
||
/// 包含:工作目录、git 状态、平台、OS 版本、日期、模型信息。
|
||
fn build_environment_section(&self) -> String {
|
||
let cwd = std::env::current_dir()
|
||
.map(|p| p.display().to_string())
|
||
.unwrap_or_else(|_| "(unknown)".to_string());
|
||
|
||
let is_git = std::process::Command::new("git")
|
||
.args(["rev-parse", "--is-inside-work-tree"])
|
||
.output()
|
||
.map(|o| o.status.success())
|
||
.unwrap_or(false);
|
||
|
||
let platform = std::env::consts::OS;
|
||
let os_version = {
|
||
let output = std::process::Command::new("uname")
|
||
.args(["-s", "-r"])
|
||
.output()
|
||
.ok()
|
||
.map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
|
||
.unwrap_or_default();
|
||
if output.is_empty() {
|
||
std::env::consts::ARCH.to_string()
|
||
} else {
|
||
output
|
||
}
|
||
};
|
||
|
||
let today = chrono::Utc::now().format("%Y-%m-%d").to_string();
|
||
let model_name = self.app_state.llm.model().to_string();
|
||
|
||
let mut lines = vec![
|
||
"# 环境信息".to_string(),
|
||
format!("- 工作目录: {}", cwd),
|
||
format!("- Git 仓库: {}", if is_git { "是" } else { "否" }),
|
||
format!("- 平台: {}", platform),
|
||
format!("- OS 版本: {}", os_version),
|
||
format!("- 日期: {}", today),
|
||
format!("- 当前模型: {}", model_name),
|
||
];
|
||
|
||
// Agent 配置摘要(最大步数、超时等)
|
||
lines.push(format!("- 最大推理步数: {}", self.config.max_steps));
|
||
lines.push(format!("- 工具超时: {} 秒", self.config.tool_timeout_secs));
|
||
|
||
lines.join("\n")
|
||
}
|
||
|
||
/// 步数耗尽时的最终答案生成(不带工具调用,强制 LLM 直接回答)
|
||
async fn final_answer_without_tools(
|
||
&self,
|
||
llm: &LlmClient,
|
||
messages: &[ChatMessage],
|
||
session_id: &str,
|
||
turn_index: i32,
|
||
step: usize,
|
||
tx: &mpsc::UnboundedSender<AgentStreamEvent>,
|
||
) -> anyhow::Result<()> {
|
||
let empty_tools: Vec<crate::clients::llm::ToolDefinition> = Vec::new();
|
||
let mut stream_rx = match llm
|
||
.chat_stream(messages, &empty_tools, self.config.enable_thinking)
|
||
.await
|
||
{
|
||
Ok(rx) => rx,
|
||
Err(e) => {
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: format!("最终回答生成失败: {}", e),
|
||
});
|
||
return Err(anyhow::anyhow!("final_answer LLM call failed: {}", e));
|
||
}
|
||
};
|
||
|
||
let mut accumulated = String::new();
|
||
while let Some(event) = stream_rx.recv().await {
|
||
match event {
|
||
StreamEvent::TextDelta(delta) => {
|
||
accumulated.push_str(&delta);
|
||
let _ = tx.send(AgentStreamEvent::TextDelta {
|
||
content: delta,
|
||
tool_call_id: None,
|
||
});
|
||
}
|
||
StreamEvent::Usage(u) => {
|
||
let _ = tx.send(AgentStreamEvent::Usage {
|
||
prompt_tokens: u.prompt_tokens,
|
||
completion_tokens: u.completion_tokens,
|
||
total_tokens: u.total_tokens,
|
||
});
|
||
}
|
||
StreamEvent::Done => break,
|
||
StreamEvent::Error(e) => {
|
||
let _ = tx.send(AgentStreamEvent::Error {
|
||
message: format!("最终回答流式错误: {}", e),
|
||
});
|
||
break;
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
let assistant_msg = ChatMessage::assistant(accumulated.clone());
|
||
self.save_message(
|
||
&self.app_state.db,
|
||
session_id,
|
||
turn_index,
|
||
step as i32,
|
||
&assistant_msg,
|
||
None,
|
||
None,
|
||
)
|
||
.await?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 保存消息到数据库
|
||
#[allow(clippy::too_many_arguments)]
|
||
async fn save_message(
|
||
&self,
|
||
db: &SqlitePool,
|
||
session_id: &str,
|
||
turn_index: i32,
|
||
step_index: i32,
|
||
msg: &ChatMessage,
|
||
thought: Option<&str>,
|
||
extra_metadata: Option<serde_json::Value>,
|
||
) -> anyhow::Result<()> {
|
||
self.save_message_as(
|
||
db,
|
||
session_id,
|
||
turn_index,
|
||
step_index,
|
||
msg,
|
||
thought,
|
||
"lead",
|
||
extra_metadata,
|
||
)
|
||
.await
|
||
}
|
||
|
||
/// 保存消息到数据库(指定 agent 身份)
|
||
#[allow(clippy::too_many_arguments)]
|
||
async fn save_message_as(
|
||
&self,
|
||
db: &SqlitePool,
|
||
session_id: &str,
|
||
turn_index: i32,
|
||
step_index: i32,
|
||
msg: &ChatMessage,
|
||
thought: Option<&str>,
|
||
agent_name: &str,
|
||
extra_metadata: Option<serde_json::Value>,
|
||
) -> anyhow::Result<()> {
|
||
let role = match msg.role {
|
||
MessageRole::System => "system",
|
||
MessageRole::User => "user",
|
||
MessageRole::Assistant => "assistant",
|
||
MessageRole::Tool => "tool",
|
||
};
|
||
|
||
let content = msg.content.clone().unwrap_or_default();
|
||
let tool_calls_json = msg
|
||
.tool_calls
|
||
.as_ref()
|
||
.map(|tc| serde_json::to_string(tc).unwrap_or_default());
|
||
let tool_call_id = msg.tool_call_id.as_deref();
|
||
let token_count = content.len() as i32 / 4;
|
||
|
||
// metadata: 存储结构化的消息元信息(thought/tool_calls/tool_call_id 等)
|
||
let mut metadata = serde_json::json!({
|
||
"has_thought": thought.is_some(),
|
||
"has_tool_calls": tool_calls_json.is_some(),
|
||
"step_index": step_index,
|
||
});
|
||
// 合并额外元数据(如图片路径等)
|
||
if let Some(ref extra) = extra_metadata {
|
||
if let (Some(base), Some(extra_obj)) = (metadata.as_object_mut(), extra.as_object()) {
|
||
for (k, v) in extra_obj {
|
||
base.insert(k.clone(), v.clone());
|
||
}
|
||
}
|
||
}
|
||
let metadata_str = serde_json::to_string(&metadata).unwrap_or_default();
|
||
|
||
// raw_json: 存储完整消息的 JSON 序列化(调试/审计用)
|
||
let raw_json = serde_json::to_string(msg).unwrap_or_default();
|
||
|
||
sqlx::query(
|
||
"INSERT INTO agent_messages (session_id, turn_index, step_index, role, content, thought, tool_calls, tool_call_id, token_count, metadata, raw_json, agent_name) \
|
||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||
)
|
||
.bind(session_id)
|
||
.bind(turn_index)
|
||
.bind(step_index)
|
||
.bind(role)
|
||
.bind(content)
|
||
.bind(thought)
|
||
.bind(&tool_calls_json)
|
||
.bind(tool_call_id)
|
||
.bind(token_count)
|
||
.bind(&metadata_str)
|
||
.bind(&raw_json)
|
||
.bind(agent_name)
|
||
.execute(db)
|
||
.await?;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
/// 非致命保存:数据库写入失败时记录日志但不终止 turn
|
||
#[allow(dead_code)]
|
||
#[allow(clippy::too_many_arguments)]
|
||
async fn save_message_non_fatal(
|
||
&self,
|
||
db: &SqlitePool,
|
||
session_id: &str,
|
||
turn_index: i32,
|
||
step_index: i32,
|
||
msg: &ChatMessage,
|
||
thought: Option<&str>,
|
||
agent_name: &str,
|
||
) {
|
||
if let Err(e) = self
|
||
.save_message_as(
|
||
db, session_id, turn_index, step_index, msg, thought, agent_name, None,
|
||
)
|
||
.await
|
||
{
|
||
warn!("[AgentRuntime] 消息持久化失败(非致命): {}", e);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── Mode helper functions ────────────────────────────────────────────────
|
||
|
||
/// 将模式的 AgentConfig 预设合并到给定的 config 中。
|
||
///
|
||
/// 仅覆盖 mode_config 中 Some 的字段,None 保持原值不变。
|
||
fn apply_mode_config(config: &mut AgentConfig, mode: &AgentMode) {
|
||
if let Some(max_steps) = mode.mode_config.max_steps {
|
||
config.max_steps = max_steps;
|
||
}
|
||
if let Some(enable_thinking) = mode.mode_config.enable_thinking {
|
||
config.enable_thinking = enable_thinking;
|
||
}
|
||
if let Some(tool_timeout_secs) = mode.mode_config.tool_timeout_secs {
|
||
config.tool_timeout_secs = tool_timeout_secs;
|
||
}
|
||
|
||
// 加载权限档案(模式绑定的 permission_profile)
|
||
if let Some(profile_name) = mode.mode_config.permission_profile {
|
||
if let Some(profile) = permission_profile::load_profile(profile_name) {
|
||
tracing::info!(
|
||
"[AgentRuntime] 模式 '{}' 加载权限档案: {} — {}",
|
||
mode.id,
|
||
profile.name,
|
||
profile.description
|
||
);
|
||
permission_profile::apply_profile_to_config(
|
||
&profile,
|
||
&mut config.permission_deny_rules,
|
||
&mut config.permission_allow_rules,
|
||
&mut config.permission_ask_rules,
|
||
&mut config.permission_mode,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 根据模式的 ToolSet 设置工具注册表的定义过滤器。
|
||
fn apply_mode_tool_filter(tool_registry: &mut ToolRegistry, mode: &AgentMode) {
|
||
let all_names = tool_registry.tool_names();
|
||
if let Some(filter) = modes::tool_set_to_filter(&mode.tool_set, &all_names) {
|
||
tracing::info!(
|
||
"[AgentRuntime] 模式 '{}' 工具过滤: {} → {} 个工具",
|
||
mode.id,
|
||
all_names.len(),
|
||
filter.len()
|
||
);
|
||
tool_registry.set_definition_filter(filter);
|
||
}
|
||
}
|