AstroResearch/src/agent/runtime/mod.rs
Asfmq 85b6429c30 feat: Agent 多模式系统、视觉模型集成、LLM 能力分层与 P3 性能收尾
核心架构变更:

  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() 辅助方法
2026-06-24 19:52:27 +08:00

1892 lines
72 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.

// 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;
}
};
// 修复空 IDLLM 可能不返回 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. 动态 sectionenvironment/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);
}
}