AstroResearch/src/agent/runtime/mod.rs
Asfmq 698d007f39 feat: Agent 安全纵深防御、Checkpoint 快照、会话 Rewind/Branch、自进化
Skill、流式执行优化与系统架构全面升级

  本次提交对标 Claude Code 与 Hermes-Agent 的工程细节,在安全、可靠性、
  会话管理、自我进化四个维度进行了系统性加固,变更总量 48 文件 / +12680 -2292 行。

  ═══════ 安全纵深防御 ═══════

  1. Hardline 硬阻止层 (src/agent/runtime/hardline.rs, +534 行)
     - 不可绕过的危险命令拦截(关重启、磁盘擦除、Fork 炸弹、rm -rf /、kill -1)
     - 反规避标准化管线: ANSI 序列剥离 → Unicode NFKC → shell 反斜杠还原 → 空字面量清理
     - 在 PermissionChecker 之前执行,YOLO/Bypass 模式下同样生效
     - 集成到 executor Phase 2,被拒绝工具直接注入错误结果

  2. Permission 优先级裁决器 (src/agent/runtime/permission.rs, +200 行)
     - 7 层正式优先级规则 (P0 Deny → P7 Allow),带冲突日志
     - explain() 方法支持审计追溯
     - Hook PermissionRequired 与 Checker 结果的正确叠加逻辑

  ═══════ Checkpoint 文件快照系统 ═══════

  3. git2 原生快照 (src/agent/runtime/checkpoint.rs, +920 行)
     - 基于 git2 bare repo,内容寻址自动去重
     - 文件变更操作前自动触发 (file_write/file_edit/run_bash)
     - 每目录每 turn 最多一次快照,防止同一轮重复
     - 支持 list/diff/restore API + pre-rollback 安全快照
     - 旧快照自动 prune(保留最近 N 个)+ 按目录隔离 ref
     - 排除规则自动过滤 node_modules/target/.git/*.pdf 等
     - 集成到 executor: 文件操作前 ckpt.ensure_checkpoint()

  ═══════ 错误恢复系统大升级 ═══════

  4. 21 种 FailoverReason 分类 (src/agent/runtime/error_recovery.rs, +1200 行)
     - 参考 Hermes-Agent error_classifier.py
     - 8 步分类管线: provider-specific → HTTP status → text pattern → error body → fallback
     - is_retryable / should_compress / should_failover / is_permanent 方法
     - Context Overflow 自动修复: 从错误消息提取 token 限制,自动下调预算
     - RecoveryStep::AdjustMaxTokens 实现 (参考 Claude Code 自动修复)
     - 向后兼容 ErrorKind 别名

  ═══════ 会话 Rewind / Branch / Retry 体系 ═══════

  5. 完整 undo 栈 (src/agent/runtime/session.rs, +800 行 + 2 迁移脚本)
     - Rewind (软删除): active=0 标记,审计 trail 保留,LLM 不可见
     - Restore (撤销回退): 冲突检测——回退后有新消息则拒绝,引导使用 Branch
     - Branch: 分叉会话,复制所有 active=1 消息到新会话
     - Retry: 硬删除最后一轮对话,返回原消息文本供前端重提交
     - 数据库: agent_messages.active 列 + agent_sessions.rewind_count + parent_session_id
     - API: 4 个新端点 (/branch, /retry, /rewind, /rewind/restore)
     - load_history_for_agent 全面使用 active=1 过滤

  ═══════ Hooks 系统模块化重构 ═══════

  6. 单文件 → 7 模块体系 (src/agent/hooks/)
     hooks.rs (994 行) 拆分为:
     - mod.rs    — 入口 + HookRegistry + SessionHookManager
     - types.rs  — 类型定义 (Context, TaggedContext, PermissionRequestAction 等)
     - traits.rs — AgentHook + AsyncAgentHook + 15 种生命周期事件
     - matcher.rs — 工具名/参数匹配 + session 作用域过滤
     - dispatch.rs — 并行调度引擎 (run_pre/post_tool_use 等)
     - registry.rs — 注册/注销/查询
     - builtins.rs — CancellationHook + MetricsHook + AuditLogHook + ContextDeduplicator

     关键改进:
     - run_pre_tool_use 并行执行所有匹配 hooks,聚合 Block/MutateInput/Continue
     - TaggedContext 带完整来源标记的上下文注入 (hook_name + event)
     - ContextDeduplicator 单 dispatch cycle 内内容哈希去重
     - AsyncAgentHook 支持 fire-and-forget 异步 hooks

  ═══════ Executor 并发执行升级 ═══════

  7. 三阶段管道重写 (src/agent/runtime/executor.rs, +600 行)
     - Phase 1: 死循环检测 + 参数解析 (不变)
     - Phase 2: Hardline 预检查 (新增) → PermissionChecker (改进)
     - Phase 3: ToolPartitioner 分区 → 逐批次执行 (重写)
       - 并行批次内 FuturesUnordered 并发
       - 串行批次确保非并发安全工具独占执行
       - Checkpoint 预触发集成
     - Hook 上下文注入: system-reminder 格式 + ContextDeduplicator 去重
     - Hook 阻塞错误详细记录

  ═══════ 流式执行真正的流式调度 ═══════

  8. StreamingExecutor 重写 (src/agent/runtime/streaming_executor.rs, ~400 行变更)
     - on_tool_use 中对并发安全工具立即 tokio::spawn,不等待 flush
     - executing_non_concurrent 标志阻塞后继工具直到独占工具完成
     - JoinHandle 管理替代自定义 cancel channel
     - completed_queue 按流顺序 yield
     - Sibling Abort 通过 broadcast channel + tokio::select! 竞速
     - ToolContext 实现 Clone (支持 per-task 上下文复制)

  ═══════ 自改进 Skill 系统 ═══════

  9. PatternDetector + SkillCreator + Curator (src/agent/skills/, +1500 行)
     - PatternDetector: 扫描 agent_messages 表,检测跨 session 重复工具调用模式
     - SkillCreator: 将高置信度模式自动生成 SKILL.md (YAML frontmatter + 工作流步骤)
     - SelfImprovePipeline: 一站式 模式检测 → 创建 → 质量审查
     - Curator: 分析 skill 使用统计,标记 stale/deprecated,建议清理
     - Skill frontmatter 新增 pinned 字段 (禁止 Curator 自动清理)

  ═══════ 基础设施优化 ═══════

  10. 系统提示词缓存 (src/agent/runtime/system_prompt.rs + mod.rs)
      - SystemPromptCache: 首次计算后永久复用,/clear 时失效
      - 新增 SAFETY / SYSTEM_CONTEXT / TOOL_USAGE 静态 section
      - 环境/tools/skills/memory 动态 section 通过 get_or_compute 缓存

  11. ToolRegistry schema 缓存 (src/agent/tools/mod.rs)
      - schema_cache + schema_generation 版本号
      - 工具变更/过滤器变更时自动失效
      - precompute_definitions() 预计算 (AgentRuntime 初始化时调用)

  12. 迭代摘要融合 (src/agent/compact.rs, +100 行)
      - 参考 Hermes context_compressor.py
      - CollapseLog 追踪压缩历史,支持溢出合并
      - extract_prior_summary: 提取已有摘要融入新压缩

  13. SubAgent 系统提示词模块化 (src/agent/tools/subagent.rs)
      - 复用 5 个标准 section + 子代理专有上下文 section
      - 独立 ToolRegistry 构建工具列表

  ═══════ 前端 — CSS 变量主题系统 ═══════

  14. 全新主题变量体系 (dashboard/src/index.css + App.tsx + 各面板)
      - CSS 自定义属性: --bg-card, --text-main, --text-muted, --border-precision
      - 语义化颜色: --accent-blueprint, --accent-star
      - 全面替换硬编码 Tailwind 颜色 (slate-xxx → var(--xxx))
      - 文献入库提示优化 ("核心知识节点" 替代 "向量块")
      - ReaderPanel 样式变量化
2026-06-22 20:29:37 +08:00

1662 lines
63 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};
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>,
}
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"),
};
// 加载权限档案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 },
/// 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>,
}
impl AgentRuntime {
/// 创建新的运行时实例
pub fn new(app_state: Arc<AppState>) -> Self {
let config = AgentConfig::default();
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();
}
// 初始化 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,
}
}
/// 创建带自定义配置的运行时实例
pub fn with_config(app_state: Arc<AppState>, config: AgentConfig) -> Self {
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();
}
// 初始化 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,
}
}
/// 返回当前运行指标快照(锁异常时返回默认值)
pub fn get_metrics(&self) -> super::hooks::MetricsData {
self.metrics_data
.lock()
.ok()
.map(|m| m.clone())
.unwrap_or_default()
}
/// 设置是否启用 LLM 思考模式
pub fn with_thinking(mut self, enable: bool) -> Self {
self.config.enable_thinking = enable;
self
}
// ── 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()
}
};
compact::compress_context_with_hooks_and_log(
messages,
llm,
self.config.context_char_limit,
session_id,
Some(hook_registry),
Some(&self.collapse_log),
)
.await;
// ── 文件缓存恢复(压缩后:重新注入最近文件 + 恢复缓存)──
{
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> {
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).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;
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?;
// 保存用户消息到数据库
self.save_message(
db,
&session_info.session_id,
session_info.turn_index,
0,
&ChatMessage::user(question),
None,
)
.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();
finalize::finalize_turn(
db,
&session_info.session_id,
&metrics,
question,
&tx,
&hook_registry,
loop_terminal,
Some(&self.app_state.config.library_dir),
Some(self.app_state.llm.model()),
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(),
)
.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(),
)
.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 {
if let Ok(fallback) = std::env::var("FALLBACK_MODEL") {
warn!(
"[AgentRuntime] 连续 {} 次过载,切换到备用模型: {}",
consecutive_overloads, fallback
);
// Note: The LlmClient model is immutable. In production,
// this would require a model-override capable client.
// For now, log and continue retrying with current model.
}
}
}
// 检查用户取消
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 时失效
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首次计算后永久缓存═══════
let mut cache = self.prompt_cache.lock().unwrap_or_else(|e| {
tracing::warn!("[SystemPrompt] 缓存锁异常: {:?}", e);
e.into_inner()
});
sp.add_section(
"identity",
cache.get_or_compute("identity", || IDENTITY_SECTION.to_string()),
);
sp.add_section(
"principles",
cache.get_or_compute("principles", || PRINCIPLES_SECTION.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);
// 工具列表ToolRegistry 在 session 内不变
let tools_section = cache.get_or_compute("tools", || {
let mut tools_desc = String::from("你可以使用以下工具:\n");
for def in self.tool_registry.definitions() {
let short_desc: String = def
.function
.description
.split('。')
.next()
.unwrap_or(&def.function.description)
.chars()
.take(80)
.collect();
tools_desc.push_str(&format!("- {}: {}\n", def.function.name, short_desc));
}
tools_desc
});
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 });
}
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,
)
.await?;
Ok(())
}
/// 保存消息到数据库
async fn save_message(
&self,
db: &SqlitePool,
session_id: &str,
turn_index: i32,
step_index: i32,
msg: &ChatMessage,
thought: Option<&str>,
) -> anyhow::Result<()> {
self.save_message_as(db, session_id, turn_index, step_index, msg, thought, "lead")
.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,
) -> anyhow::Result<()> {
let role = match msg.role {
MessageRole::System => "system",
MessageRole::User => "user",
MessageRole::Assistant => "assistant",
MessageRole::Tool => "tool",
};
let content = msg.content.as_deref().unwrap_or("");
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 metadata = serde_json::json!({
"has_thought": thought.is_some(),
"has_tool_calls": tool_calls_json.is_some(),
"step_index": step_index,
});
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,
)
.await
{
warn!("[AgentRuntime] 消息持久化失败(非致命): {}", e);
}
}
}