重构: aichat agent 能力系统化(L1元能力+L2/L3后端+list去重)

L1 agent 元能力层(治痛①死循环零交付):
- env_profile 环境姿势注入 + shell 默认 PowerShell(防引号地狱)
- 断路器:同类工具失败≥3熔断 + guard.reset
- detect_environment 主动探测工具(python/node/shell)
- 求助协议 AiHelpRequired 事件 + 前端求助卡

L2 统一状态机后端(治痛②,前端批2):
- ConvState enum 5态 + 合法转换守卫(conv_state.rs)
- GeneratingGuard 接入视图层(guard.rs)

L3 事件总线后端骨架(治痛③④⑤,接入批2):
- EventBus pub-sub + AiBusEvent 8变体(event_bus.rs)

list 工具调用重复治理第一步:
- build_system_prompt_with_excluded 去重被@实体 + 清单注明语
This commit is contained in:
2026-06-22 00:04:14 +08:00
parent bd6a41fe6e
commit d2cada97cd
16 changed files with 1752 additions and 31 deletions

View File

@@ -9,6 +9,8 @@ use tokio::sync::Mutex;
use crate::commands::ai::AiSession;
use super::conv_state::{self, ConvState};
/// generating 复位 RAII guard,取代散布的手动 `session.generating = false`。
///
/// 两路复位:
@@ -22,32 +24,87 @@ use crate::commands::ai::AiSession;
/// F-260616-09 B 批4:guard 持 `conv_id`,复位改写 `session.conv(&conv_id).generating = false`
/// (per-conv 唯一真相源)。顶层 `session.generating` 字段已在批4 删除,reset/Drop 仅写 per_conv;
/// IPC(ai_is_generating/ai_chat_send 等)亦改读 per_conv,无需双写桥接。
///
/// L2 统一状态机(渐进第一步,2026-06-21):guard 内嵌 `ConvState` 作**写收敛视图层**。
///
/// - `new` 时按 `conv_state::CONV_STATE_ENABLED` 门控,把内部 `state` 从 `Idle` 经守卫迁移到
/// `Generating`(非法转换记 warn 不阻断,对齐「机制优先,失败兜底」原则)。
/// - `reset` / `drop` 收尾时同步迁移 `Generating → Idle`(状态机视图与 `generating` bool 双轨,
/// bool 仍是核心真相源,enum 仅视图)。off 降级:guard 不持/不迁 enum,纯旧 bool 行为(可回退)。
///
/// **渐进取舍(为什么 enum 放 guard 而非 PerConvState 字段)**:guard 是 loop 生命周期的天然
/// 边界(set/reset/Drop 成对),把 enum 绑 guard 即覆盖全部生成态进入/退出点,无需改 PerConvState
/// 字段(改字段会牵动所有读写点,属批2+ 范围)。本批 enum 仅作 guard 内部视图,不对外持久化;
/// 批2+ 把 enum 提升到 PerConvState.conv_state 字段后,guard 仍可保留作写收敛入口(枚举源切换)。
pub(super) struct GeneratingGuard {
session: Arc<Mutex<AiSession>>,
/// guard 所属会话(loop 启动时快照的 conv_id,来自 run_agentic_loop 入参)。
conv_id: String,
done: bool,
/// L2 状态机视图(CONV_STATE_ENABLED 门控)。None = 开关 off 降级,guard 不持视图。
/// 仅 guard 生命周期内的本地视图,不持久化(批2+ 提升至 PerConvState 字段)。
state: Option<ConvState>,
}
impl GeneratingGuard {
pub(super) fn new(session: Arc<Mutex<AiSession>>, conv_id: String) -> Self {
Self { session, conv_id, done: false }
// CONV_STATE_ENABLED 门控:off → state=None,guard 不持/不迁 enum(纯旧 bool 行为)。
// on → state 从 Idle 起,入口即经守卫迁移到 Generating(写收敛:状态机入口校验)。
let mut state = if conv_state::CONV_STATE_ENABLED {
Some(ConvState::Idle)
} else {
None
};
if let Some(s) = state.as_mut() {
// Idle → Generating 守卫迁移。正常路径(self-loop 幂等 / 首次进入)合法;理论非法不发生
// (新 guard 必 Idle),失败记 warn 不阻断(对齐失败兜底原则)。
match s.transition_to(ConvState::Generating) {
Ok(new_state) => *s = new_state,
Err(e) => {
tracing::warn!(
conv_id = %conv_id,
error = %e,
"[ai] guard.new ConvState Idle→Generating 非法(状态机视图层,不阻断核心生成)"
);
// 状态机层失败:enum 保留 Idle(不写非法态),核心 generating 仍由调用方 set true。
// 即 enum 视图与 bool 暂时不同步,但 enum 仅视图不影响核心复位语义。
}
}
}
Self { session, conv_id, done: false, state }
}
/// 显式复位 generating=false。emit 前调用保证顺序。幂等。
///
/// 仅写 per_conv.conv_id.generating(唯一真相源)。
/// L2:同步迁移 `ConvState → Idle`(状态机视图,CONV_STATE_ENABLED 门控)。
pub(super) async fn reset(&mut self) {
if !self.done {
let mut session = self.session.lock().await;
session.conv(&self.conv_id).generating = false;
self.done = true;
}
// 状态机视图同步:Generating → Idle(正常收敛退出)。仅记日志验证迁移合法(批2+ 接入
// PerConvState.conv_state 字段后此处写字段持久化)。done=true 后再次 reset 幂等,enum 已 Idle
// 自环通过。开关 off → state=None 跳过(纯旧 bool 行为)。
if let Some(s) = self.state.as_mut() {
match s.transition_to(ConvState::Idle) {
Ok(new_state) => *s = new_state,
Err(e) => tracing::warn!(
conv_id = %self.conv_id,
error = %e,
"[ai] guard.reset ConvState →Idle 非法(状态机视图层,不阻断核心复位)"
),
}
}
}
/// 解除 Drop 兜底复位但不复位 generating。审批等待 return 路径调用:
/// 保持 generating=true 留 try_continue 续生成,同时 Drop 因 done=true 跳过复位 spawn。
/// (B-260615-26: 修复审批执行后对话不续生成回归)
///
/// L2:disarm 不迁移 ConvState(审批等待是 Generating 内的暂停点,ConvState 仍 Generating,
/// 与「generating 保持 true」语义一致)。续跑时新 guard.new 重新迁移(Generating 自环幂等)。
pub(super) fn disarm(&mut self) {
self.done = true;
}
@@ -55,7 +112,19 @@ impl GeneratingGuard {
impl Drop for GeneratingGuard {
fn drop(&mut self) {
// 状态机视图同步(panic/异常退出路径):未 done 即异常退出,enum 应从 Generating→Idle 收敛。
// 仅记日志(无 await 上下文,不写持久化——批2+ 接入字段后此处可写)。
// 开关 off / done=true(正常 reset/disarm 已走) → 跳过。
if !self.done {
if let Some(s) = self.state.as_mut() {
if let Err(e) = s.transition_to(ConvState::Idle) {
tracing::warn!(
conv_id = %self.conv_id,
error = %e,
"[ai] guard.drop ConvState →Idle 非法(异常退出兜底,核心 generating 仍 spawn 复位)"
);
}
}
let session = self.session.clone();
let conv_id = self.conv_id.clone();
tauri::async_runtime::spawn(async move {
@@ -65,3 +134,52 @@ impl Drop for GeneratingGuard {
}
}
}
// ============================================================
// 单元测试(guard 内嵌 ConvState 视图层)
// ============================================================
#[cfg(test)]
mod tests {
use super::*;
/// guard.new 默认迁移 Idle→Generating,内部视图应为 Generating(CONV_STATE_ENABLED on)。
///
/// 注:不构造真实 AiSession(lock/Mutex 建会话状态成本高),仅校验 state 字段的迁移语义——
/// new 内部 transition_to 是纯函数,直接断言逻辑路径。此处用相同的纯函数路径验证 guard 语义。
#[test]
fn test_guard_new_migrates_to_generating() {
// 模拟 guard.new 内部状态机迁移路径(纯逻辑,无 session):
// CONV_STATE_ENABLED on → Idle → Generating。
assert!(conv_state::CONV_STATE_ENABLED, "开关默认 on,测试假设");
let mut s = ConvState::Idle;
s = s.transition_to(ConvState::Generating).unwrap();
assert_eq!(s, ConvState::Generating);
}
/// reset 迁移 Generating→Idle(正常收敛),验证迁移合法。
#[test]
fn test_guard_reset_migrates_to_idle() {
let mut s = ConvState::Generating;
s = s.transition_to(ConvState::Idle).unwrap();
assert_eq!(s, ConvState::Idle);
}
/// 异常 Drop 路径同样 Generating→Idle(兜底),验证迁移合法。
#[test]
fn test_guard_drop_path_migrates_to_idle() {
// guard.drop 异常路径(!done)迁移 Generating→Idle。
let mut s = ConvState::Generating;
s = s.transition_to(ConvState::Idle).unwrap();
assert_eq!(s, ConvState::Idle);
}
/// disarm 不迁移状态(审批等待保持 Generating)。
/// 验证:disarm 后 enum 仍 Generating(下次 new 时 Generating 自环幂等通过)。
#[test]
fn test_guard_disarm_keeps_generating_then_self_loop_on_new() {
let s = ConvState::Generating;
// disarm 不动 enum;续跑新 guard.new 时 Generating→Generating 自环通过。
assert_eq!(s.transition_to(ConvState::Generating), Ok(ConvState::Generating));
}
}