新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理
后端: - 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展 - 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通 - 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦 - AI planner/plan_hint/intent:aichat B 路线并行多轮基础 - patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环 - 压缩超时兜底(F-15 卡死根治) - F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本 - 知识注入 DRY/skills/audit 扩展 清理: - aichat 技术债(误报 allow/死导入/过时注释 30 项) - URGENT.md 删除(11 项加急全解决/迁 todo) - 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
This commit is contained in:
@@ -8,13 +8,19 @@ use tokio::sync::Mutex;
|
||||
|
||||
use df_ai::ai_tools::AiToolRegistry;
|
||||
use df_ai::context::TokenEstimator;
|
||||
// 阶段2 占位配对完整性:agentic 出口第二道防线断言(深度防御)。
|
||||
use df_ai::context::ContextManager;
|
||||
// 改进3 B: 压缩失败兜底关键词摘要(纯函数 extract_keyword_summary)。
|
||||
// 改进4: 工具结果 view-only 摘要(should_summarize_tool_result / extract_key_info)。
|
||||
use df_ai::context_helpers::{
|
||||
extract_key_info, extract_keyword_summary, should_summarize_tool_result,
|
||||
PLACEHOLDER_INTEGRITY_ENABLED,
|
||||
};
|
||||
// 改进2 B:意图收敛工具(LLM 可见 tool_defs 按 intent 过滤,执行路径仍走完整 registry)
|
||||
use df_ai::intent::{filter_tool_defs, IntentRecognizer};
|
||||
// B 路线 Phase 1:plan_hint 接入主 loop——filter_tool_defs_planned 在 filter_tool_defs
|
||||
// 收敛的扁平子集之上叠加 plan_hint 编排(并行组同批聚拢/顺序依赖源在前),供 LLM 看到
|
||||
// 一份按编排意图排序的工具列表。feature flag PLANNING_ENABLED(false 默认关)门控接入。
|
||||
use df_ai::intent::{filter_tool_defs, filter_tool_defs_planned, IntentRecognizer};
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider};
|
||||
// CR-30-1: 复用 retry::backoff_delay(jitter 1s→2s→4s) + is_status_retryable(Fatal 分类)
|
||||
// 实现流前失败重试退避对齐(决策 F-260616-07 a1),避免重写退避逻辑。
|
||||
@@ -36,12 +42,22 @@ use crate::state::{AppState, LlmConcurrency};
|
||||
use super::audit::process_tool_calls;
|
||||
use super::compress::compress_via_llm;
|
||||
use super::conversation::{save_conversation, TokenAccumulator};
|
||||
use super::knowledge_inject::maybe_spawn_extraction;
|
||||
use super::knowledge_inject::{inject_knowledge_into_prompt, maybe_spawn_extraction};
|
||||
use super::prompt::{build_system_prompt, get_active_provider};
|
||||
use super::stream_recv::{stream_llm, StreamResult};
|
||||
use super::title::{ensure_conversation_title, spawn_ensure_title};
|
||||
|
||||
use super::{AiChatEvent, AiSession, ErrorType};
|
||||
use super::{AiChatEvent, AiSession, ErrorType, SessionState};
|
||||
|
||||
/// L1 补丁:run_agentic_loop 入口 provider 解析超时保护的内部错误类型。
|
||||
///
|
||||
/// 用于把 provider 解析块(list_all + select + resolve + ensure + build)包入
|
||||
/// `tokio::time::timeout` 后的内部 Err 路由——区分 ensure_resolved_key 失败(走 Auth
|
||||
/// 错误)与整体超时(走 Unknown 错误)。超时由外层 timeout 的 Err(Elapsed) 单独匹配。
|
||||
enum ProviderResolveError {
|
||||
/// ensure_resolved_key 失败(key 缺失/钥匙串损坏):走 Auth 错误分支(对齐原 :412 处理)。
|
||||
EnsureKeyFailed(String),
|
||||
}
|
||||
|
||||
/// Agentic 循环默认最大迭代次数(可配置项的默认值)
|
||||
///
|
||||
@@ -90,6 +106,24 @@ pub const TOOL_RESULT_COMPRESS_ENABLED: bool = true;
|
||||
/// 保守:双高置信才标(任一 topic None 不标),不强制 LLM(软提示非硬约束)。
|
||||
pub const TOPIC_MARKER_ENABLED: bool = true;
|
||||
|
||||
// 阶段2(path_auth 审批链重构):占位配对完整性开关(解 400 orphan)。
|
||||
//
|
||||
// 单一真相源:`df_ai::context_helpers::PLACEHOLDER_INTEGRITY_ENABLED`(本模块顶部已 use)。
|
||||
// 删除本地副本(B 路线改进:避免与 df-ai 同名 const 双源,改一处易漏改另一处)。
|
||||
//
|
||||
// 根因:审批挂起占位 tool_result(内容 audit/cache.rs:pending_placeholder_for,带
|
||||
// `__PENDING__:tc_id` 标记)与其 tool_call 头经 sanitize/裁剪后可能丢配对头 → orphan
|
||||
// tool_result → deepseek-v4-pro 等端点 400。df-ai 的 sanitize_messages step3.5(反向
|
||||
// orphan)已豁免保留占位,build_for_request 出口已用 assert_placeholder_pairing 自愈补头。
|
||||
//
|
||||
// 该开关控制 agentic loop 末尾(build_for_request + tool_result 压缩后)的**第二道防线**
|
||||
// 出口断言:在 messages 送 stream 前再过一次 assert_placeholder_pairing(depth-defense,
|
||||
// 防 build_for_request 到送 stream 之间的转换引入新 orphan)。
|
||||
//
|
||||
// true(默认):agentic 出口再断言一次占位配对,失败自愈补头。
|
||||
// false(回退):agentic 出口不断言(仅依赖 df-ai build_for_request 内部一次自愈,旧行为)。
|
||||
// 兜底:flag 关→等价改动前(仅 df-ai 内部自愈);view-only 不改持久化。
|
||||
|
||||
// ============================================================
|
||||
// 重构第一批(2026-06-19):GeneratingGuard 抽离到 guard.rs(纯结构搬迁,行为零变更)。
|
||||
// run_agentic_loop 内仍 `GeneratingGuard::new(...)`,路径从本模块改 super::guard。
|
||||
@@ -185,7 +219,7 @@ async fn stream_one_provider(
|
||||
// 否则用 resolved_model(绝不因 override 致无模型)。
|
||||
let resolved_model = match model_override.as_deref() {
|
||||
Some(id) if !id.is_empty()
|
||||
&& candidate.model_configs.iter().any(|m| m.model_id == id) =>
|
||||
&& candidate.model_configs.iter().any(|m| m.model_id == id && m.enabled) =>
|
||||
{
|
||||
id.to_string()
|
||||
}
|
||||
@@ -326,7 +360,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
|
||||
// F-260616-09 B 批2 入口桥接:loop 启动前确保 per_conv 存在(已存在则保留累积,不存在则建)。
|
||||
//
|
||||
// 批2 把所有调用方(IPC commands.rs 写路径 + agentic.rs loop + audit.rs process_tool_calls +
|
||||
// 批2 把所有调用方(IPC commands.rs 写路径 + agentic/mod.rs loop + audit.rs process_tool_calls +
|
||||
// conversation.rs save + title.rs + knowledge_inject.rs)迁移到 per_conv 真相源。IPC 在 spawn
|
||||
// loop 前已通过 `session.conv(active_conversation_id).*` 建立 per_conv 并写入初始状态
|
||||
// (messages push user / generating=true / stop_flag=false / iteration_used=0 等),故 loop
|
||||
@@ -363,53 +397,63 @@ pub(crate) async fn run_agentic_loop(
|
||||
// AiProviderRepo::new 仅 clone Arc<Database>(廉价),不复用 AppState.ai_providers
|
||||
// (run_agentic_loop 签名只传 Arc<Database>,改签名会牵动 3 调用点 + try_continue)。
|
||||
let provider_repo = df_storage::crud::AiProviderRepo::new(&db);
|
||||
let pool_providers: Vec<AiProviderRecord> = match provider_repo.list_all().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "[ai] list_all providers 失败,负载均衡池退化为入参默认 provider(空池兜底)");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
let ranked_candidates: Vec<AiProviderRecord> = super::provider_pool::ProviderPool::select(
|
||||
&pool_providers,
|
||||
// specify 模式(用户指定 model):传 override 作亲和键,ProviderPool 优先选
|
||||
// 「池中含该 model 的 provider」作 primary,打破下方「router 选模型需 provider_config」
|
||||
// 的鸡生蛋——override 此时已知(入参 ← session.model_override),无须等 router。
|
||||
// auto 模式(override=None)→ 全亲和纯权重排序,行为不变(向后兼容)。
|
||||
model_override.as_deref(),
|
||||
);
|
||||
let (primary_provider, candidates): (AiProviderRecord, Vec<AiProviderRecord>) =
|
||||
match ranked_candidates.split_first() {
|
||||
Some((first, rest)) => (first.clone(), rest.to_vec()),
|
||||
None => {
|
||||
// 空池兜底:用调用方传入的 provider_config 作唯一候选(启动行为不变)。
|
||||
// candidates 空 → fallback 循环仅跑 primary 一次,等同单 provider 路径。
|
||||
(provider_config.clone(), Vec::new())
|
||||
}
|
||||
};
|
||||
// 用主候选覆盖入参 provider_config(下游 build_provider / 路由 / 日志均用此)。
|
||||
// mut:F-04b 切换 candidate 后更新为实际成功所用 provider(供后续 push/save/标题 spawn)。
|
||||
let mut provider_config = primary_provider;
|
||||
|
||||
// FR-S1: resolve→ensure_resolved_key(空 key 早失败)→build_provider 三步统一走工厂
|
||||
// 空 key 早失败(逻辑见 secret::ensure_resolved_key 单测):避免空 key 发请求吃 401,错误伪装成"API Key 无效"
|
||||
//
|
||||
// B-260615-17:resolve 一次复用——原实现 build_provider_for 成功后又独立调 resolve_provider_secret
|
||||
// 取 key_len(重复 keyring resolve)。现 resolve 一次:既供 key_len 诊断日志,又供 build_provider,
|
||||
// 去重复 keyring resolve 调用。逻辑等价于 secret::build_provider_for(resolve→ensure→build 三步),
|
||||
// 仅因 build_provider_for 隐藏 resolved key 无法复用而在此内联(未改 secret.rs 锁边界)。
|
||||
let resolved_key = super::secret::resolve_provider_secret(&provider_config);
|
||||
let key_len = resolved_key.len();
|
||||
let provider: Box<dyn LlmProvider> = match super::secret::ensure_resolved_key(
|
||||
&provider_config.name, &resolved_key,
|
||||
) {
|
||||
Ok(()) => df_ai::build_provider(
|
||||
&provider_config.provider_type,
|
||||
&provider_config.base_url,
|
||||
&resolved_key,
|
||||
&provider_config.default_model,
|
||||
),
|
||||
Err(msg) => {
|
||||
// L1 补丁:provider 解析(list_all + select + resolve + ensure + build)整体包 30s timeout。
|
||||
// 原实现无超时,数据库/keyring 卡死时 run_agentic_loop 入口卡住,generating 永真 + 前端看门狗
|
||||
// 超时静默吞消息。超时走 AiError 分支(对齐 :412 ensure_resolved_key 失败处理),guard.reset 复位。
|
||||
// 内部 list_all 失败仍容忍(空池兜底,行为不变);仅整体超时(如 DB 挂死无响应)才走 Err 分支。
|
||||
let provider_resolve = tokio::time::timeout(
|
||||
std::time::Duration::from_secs(30),
|
||||
async {
|
||||
let pool_providers: Vec<AiProviderRecord> = match provider_repo.list_all().await {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
tracing::warn!(error = %e, "[ai] list_all providers 失败,负载均衡池退化为入参默认 provider(空池兜底)");
|
||||
Vec::new()
|
||||
}
|
||||
};
|
||||
let ranked_candidates: Vec<AiProviderRecord> = super::provider_pool::ProviderPool::select(
|
||||
&pool_providers,
|
||||
// specify 模式(用户指定 model):传 override 作亲和键,ProviderPool 优先选
|
||||
// 「池中含该 model 的 provider」作 primary,打破下方「router 选模型需 provider_config」
|
||||
// 的鸡生蛋——override 此时已知(入参 ← session.model_override),无须等 router。
|
||||
// auto 模式(override=None)→ 全亲和纯权重排序,行为不变(向后兼容)。
|
||||
model_override.as_deref(),
|
||||
);
|
||||
let (primary_provider, candidates): (AiProviderRecord, Vec<AiProviderRecord>) =
|
||||
match ranked_candidates.split_first() {
|
||||
Some((first, rest)) => (first.clone(), rest.to_vec()),
|
||||
None => {
|
||||
// 空池兜底:用调用方传入的 provider_config 作唯一候选(启动行为不变)。
|
||||
// candidates 空 → fallback 循环仅跑 primary 一次,等同单 provider 路径。
|
||||
(provider_config.clone(), Vec::new())
|
||||
}
|
||||
};
|
||||
// FR-S1: resolve→ensure_resolved_key(空 key 早失败)→build_provider 三步统一走工厂
|
||||
// 空 key 早失败(逻辑见 secret::ensure_resolved_key 单测):避免空 key 发请求吃 401,错误伪装成"API Key 无效"
|
||||
//
|
||||
// B-260615-17:resolve 一次复用——原实现 build_provider_for 成功后又独立调 resolve_provider_secret
|
||||
// 取 key_len(重复 keyring resolve)。现 resolve 一次:既供 key_len 诊断日志,又供 build_provider,
|
||||
// 去重复 keyring resolve 调用。逻辑等价于 secret::build_provider_for(resolve→ensure→build 三步),
|
||||
// 仅因 build_provider_for 隐藏 resolved key 无法复用而在此内联(未改 secret.rs 锁边界)。
|
||||
let resolved_key = super::secret::resolve_provider_secret(&primary_provider);
|
||||
let key_len = resolved_key.len();
|
||||
let provider: Box<dyn LlmProvider> = match super::secret::ensure_resolved_key(
|
||||
&primary_provider.name, &resolved_key,
|
||||
) {
|
||||
Ok(()) => df_ai::build_provider(
|
||||
&primary_provider.provider_type,
|
||||
&primary_provider.base_url,
|
||||
&resolved_key,
|
||||
&primary_provider.default_model,
|
||||
),
|
||||
Err(msg) => return Err(ProviderResolveError::EnsureKeyFailed(msg)),
|
||||
};
|
||||
Ok::<_, ProviderResolveError>((primary_provider, candidates, provider, key_len))
|
||||
},
|
||||
).await;
|
||||
let (primary_provider, candidates, provider, key_len) = match provider_resolve {
|
||||
Ok(Ok((pc, cands, prov, kl))) => (pc, cands, prov, kl),
|
||||
Ok(Err(ProviderResolveError::EnsureKeyFailed(msg))) => {
|
||||
guard.reset().await;
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError {
|
||||
error: msg,
|
||||
@@ -419,7 +463,22 @@ pub(crate) async fn run_agentic_loop(
|
||||
});
|
||||
return;
|
||||
}
|
||||
Err(_elapsed) => {
|
||||
// L1 补丁:provider 解析 30s 超时(DB list_all / keyring resolve 卡死)。
|
||||
// 走 AiError 分支复位 generating,对齐 ensure_resolved_key 失败处理口径。
|
||||
guard.reset().await;
|
||||
tracing::error!(conv_id = %conv_id, "[ai] provider 解析超时(30s),可能 DB/keyring 卡死");
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiError {
|
||||
error: "Provider 解析超时(30s),请检查数据库/钥匙串状态后重试".to_string(),
|
||||
error_type: Some(ErrorType::Unknown),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
});
|
||||
return;
|
||||
}
|
||||
};
|
||||
// 用主候选覆盖入参 provider_config(下游 build_provider / 路由 / 日志均用此)。
|
||||
// mut:F-04b 切换 candidate 后更新为实际成功所用 provider(供后续 push/save/标题 spawn)。
|
||||
let mut provider_config = primary_provider;
|
||||
// 诊断日志:401/错误时据此定位是 url/type/model/key 哪项问题(只记长度不记明文)
|
||||
tracing::info!(
|
||||
provider = %provider_config.name,
|
||||
@@ -447,7 +506,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
// (新 candidate 模型池不同,必须重选);此后 push/save 均用迭代最新值。
|
||||
let mut resolved_model = match model_override.as_deref() {
|
||||
Some(id) if !id.is_empty()
|
||||
&& provider_config.model_configs.iter().any(|m| m.model_id == id) =>
|
||||
&& provider_config.model_configs.iter().any(|m| m.model_id == id && m.enabled) =>
|
||||
{
|
||||
id.to_string()
|
||||
}
|
||||
@@ -484,8 +543,28 @@ pub(crate) async fn run_agentic_loop(
|
||||
const INTENT_CONF_THRESHOLD: f32 = 0.7;
|
||||
let all_defs = tools_arc.tool_definitions();
|
||||
let total = all_defs.len(); // 提前记录全量数(all_defs 将 move 进 tool_defs)
|
||||
// B 路线 Phase 1 接入:PLANNING_ENABLED(false 默认关)门控 plan_hint 编排。
|
||||
//
|
||||
// **零行为变更(关时)**:flag 关时走 filter_tool_defs(intent 收敛扁平子集),
|
||||
// 与 Phase 1 接入前完全一致——现有 intent/agentic 测试全绿无回归。
|
||||
//
|
||||
// **开启时**:调 filter_tool_defs_planned,它内部先 filter_tool_defs 收敛再叠加
|
||||
// plan_hint 编排(并行组同批聚拢/顺序依赖源在前)。三重 fallback 与 filter_tool_defs
|
||||
// 同语义(plan_hint 空/非法/registry 漂移均退 filter_tool_defs 扁平结果)。
|
||||
//
|
||||
// 关键安全(与 filter_tool_defs 同):本段只改 LLM 可见 tool_defs 的可见性/顺序,
|
||||
// 不改执行(audit 走 tools_arc 完整 registry)。PLAN_HINT_ENABLED(plan_hint 函数开关,
|
||||
// Phase0a 就绪 true)与 PLANNING_ENABLED(planner.rs 主 loop 规划开关,本批仍是 false)
|
||||
// 分离:即使将来 PLANNING_ENABLED 翻 true,plan_hint 内部 PLAN_HINT_ENABLED 关闭时
|
||||
// filter_tool_defs_planned 仍退扁平(双层开关,任一关闭均退旧行为)。
|
||||
let tool_defs = if conf >= INTENT_CONF_THRESHOLD {
|
||||
let filtered = filter_tool_defs(&all_defs, &intent);
|
||||
let filtered = if df_ai::planner::PLANNING_ENABLED {
|
||||
// Phase 1:plan_hint 编排排序。intent_label 供 plan_hint 备用(当前规则纯关键词驱动)。
|
||||
filter_tool_defs_planned(&all_defs, &intent, intent.as_str(), &user_text)
|
||||
} else {
|
||||
// 旧行为:intent 收敛扁平子集(零行为变更,flag 默认关走此路)。
|
||||
filter_tool_defs(&all_defs, &intent)
|
||||
};
|
||||
if filtered.len() < 3 {
|
||||
all_defs // 兜底:过滤<3(漂移/误收敛)回全量
|
||||
} else {
|
||||
@@ -500,6 +579,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
conf,
|
||||
filtered = tool_defs.len(),
|
||||
total,
|
||||
planning_enabled = df_ai::planner::PLANNING_ENABLED,
|
||||
"[ai] 意图收敛工具"
|
||||
);
|
||||
// 停止信号副本:stream_llm 与每轮迭代共享读取,避免重复加锁
|
||||
@@ -539,7 +619,9 @@ pub(crate) async fn run_agentic_loop(
|
||||
// retry 同 loop 内,持 per_conv 合理(F-260616-12 核验)。
|
||||
let _conv_per_conv_permit = llm_concurrency.acquire_per_conv(&conv_id).await;
|
||||
|
||||
for iteration in start_iteration..max_iterations {
|
||||
// 0 = 不限:effective_max=usize::MAX,for 到不了上界,靠 stop_flag/收敛/审批退出(下方达上限暂停分支不触发)
|
||||
let effective_max = if max_iterations == 0 { usize::MAX } else { max_iterations };
|
||||
for iteration in start_iteration..effective_max {
|
||||
// 用户请求停止 → 收尾退出(已生成文本已在上一轮入库)
|
||||
if stop_flag.load(Ordering::SeqCst) {
|
||||
let usage = df_ai::provider::TokenUsage {
|
||||
@@ -893,6 +975,11 @@ pub(crate) async fn run_agentic_loop(
|
||||
messages
|
||||
};
|
||||
|
||||
// 阶段2 占位配对完整性(第二道防线,depth-defense):build_for_request 已在 df-ai 内部
|
||||
// 自愈一次,此处 tool_result 压缩/系统提示插入后再断言一次,防转换引入新 orphan。
|
||||
// view-only:messages 是 clone,assert_placeholder_pairing 仅改本 Vec,不改 ContextManager 持久化。
|
||||
let messages = ContextManager::assert_placeholder_pairing(messages, PLACEHOLDER_INTEGRITY_ENABLED);
|
||||
|
||||
// 预估输入 token(兜底:部分 provider 如 GLM 流式 usage 不报 prompt_tokens,后段用它补)
|
||||
// 注:stream_one_provider 内每次重试重建 request(因 provider.stream 消费 body),
|
||||
// 此处不再预构建 request(旧 request 变量已废弃),仅保留 messages 供 estimated_prompt。
|
||||
@@ -1145,6 +1232,13 @@ pub(crate) async fn run_agentic_loop(
|
||||
let mut session = session_arc.lock().await;
|
||||
process_tool_calls(&mut session, tool_calls_acc, &tools_arc, &db, &app_handle, &conv_id).await
|
||||
};
|
||||
// F-260620 卡死已根治(DIRAUTH 审批链已闭环)。原 eprintln 诊断降级为 tracing::debug,
|
||||
// 避免污染 stderr(用户可见),保留排障能力(RUST_LOG=debug 可见)。
|
||||
tracing::debug!(
|
||||
conv_id = %conv_id,
|
||||
pending_count,
|
||||
"[AI-DIRAUTH-DIAG] agentic loop 收到 pending"
|
||||
);
|
||||
|
||||
// 有待审批 → 暂停循环,等待用户审批后通过 ai_approve → try_continue_agent_loop 恢复
|
||||
if pending_count > 0 {
|
||||
@@ -1169,6 +1263,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 或点停止(ai_stop_loop → 走完成流程)。try_continue 续跑 iteration 由调用方传 start_iteration 决定:
|
||||
// 审批续跑累计(F-260616-11 决策 a,防多次审批反复跑满 max 致 token 失控,传 session.iteration_used),
|
||||
// 达 max 续跑重计(F-260616-03 决策 a,用户点继续=授权重来,传 0 + 重置 iteration_used)。
|
||||
// 注:max_iterations=0(不限)时 effective_max=usize::MAX,for 不会正常结束至此,故不触发暂停(靠 stop/收敛/审批退出)
|
||||
if !converged {
|
||||
tracing::warn!(
|
||||
conv_id = %conv_id,
|
||||
@@ -1264,10 +1359,18 @@ pub(crate) async fn try_continue_agent_loop(
|
||||
// 传 IPC 参数 conversation_id。
|
||||
let snap = {
|
||||
let session = state.ai_session.lock().await;
|
||||
// has_pending:仅本 conv 的未决审批算续跑阻塞(决策 e 真并发准备)。
|
||||
let has_pending = session.pending_approvals.values()
|
||||
.any(|a| a.conversation_id.as_deref() == Some(conv_id));
|
||||
// path_auth 审批链阶段1:has_pending 改调 session_state(conv_id) 收敛状态机判定,
|
||||
// 替代手写 path_auth+risk 两表 any 合并(mod.rs 已统一封装)。
|
||||
// 阶段3a 单真相源合并后:两表合一进 pending_approvals,session_state 单表 any 判定。
|
||||
// 两表语义不变——任一类挂起都阻塞续跑(agentic loop 在 path_auth 挂起时也已 return 等待,
|
||||
// 漏任一会致 loop 误续跑空转)。
|
||||
//
|
||||
// 兜底/快速回退:若需切回手写 has_pending,原双表组合保留如下(改一行即可):
|
||||
// let has_pending = session.pending_approvals.values()
|
||||
// .any(|a| a.conversation_id.as_deref() == Some(conv_id));
|
||||
let has_pending = session.session_state(conv_id) == SessionState::AwaitingApproval;
|
||||
// pending_conv_id 保留(should_continue=false 路径的 emit conv_id 回退逻辑)。
|
||||
// 阶段3a:单表 find_map(原两表合一)。
|
||||
let pending_conv_id = session.pending_approvals.values()
|
||||
.find_map(|a| a.conversation_id.clone());
|
||||
let conv = session.conv_read(conv_id);
|
||||
@@ -1345,6 +1448,16 @@ pub(crate) async fn try_continue_agent_loop(
|
||||
let app_handle = app.clone();
|
||||
let knowledge_config = state.knowledge_config.lock().await.clone();
|
||||
let llm_concurrency = state.llm_concurrency.clone();
|
||||
|
||||
// 知识注入:DRY(B):收敛至 inject_knowledge_into_prompt 单一入口。
|
||||
// P1 修复(审批恢复路径缺知识注入):try_continue 续跑轮此前用裸 build_system_prompt,
|
||||
// 不调 build_knowledge_context 致续跑轮丢知识库上下文。现与 chat.rs 四处同款走 helper。
|
||||
// 同消息取 text+id(②口径修复):原 last_user_text 过滤 is_active / user_message_id 走
|
||||
// last_user_message_id 不过滤 is_active,末条 user 压缩后两值取自不同消息;helper 单次
|
||||
// 反向扫描同一条消息取两值。复用上方已 clone 的 knowledge_config 快照(避免重复加锁)。
|
||||
let system_prompt = inject_knowledge_into_prompt(state, conv_id, system_prompt, &knowledge_config).await;
|
||||
|
||||
|
||||
// F-260616-01: loop 入口 load 快照,当前续生成 loop 锁定边界(热改下次发消息生效)
|
||||
let max_iterations = state.agent_max_iterations.load(Ordering::SeqCst);
|
||||
// F-260616-07: 流式失败重试次数快照
|
||||
|
||||
Reference in New Issue
Block a user