重构: 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:
@@ -0,0 +1,468 @@
|
||||
//! L2 统一状态机 — `ConvState` enum(单一真相源)+ 合法转换守卫。
|
||||
//!
|
||||
//! 来源:`generating状态机加固-2026-06-15.md` §3(轻量状态机决策)+ `aichat体验与
|
||||
//! agent能力系统化重构-2026-06-21.md` §3(L2 统一状态机)。
|
||||
//!
|
||||
//! # 背景(为什么单独建这个 enum)
|
||||
//!
|
||||
//! 旧实现会话状态由三个标志组合隐式表达(`generating: bool` + `stop_flag: AtomicBool`
|
||||
//! + `pending_approvals` HashMap),判别逻辑散落 `try_continue_agent_loop` /
|
||||
//! `ai_chat_stop` / `ai_conversation_create` 三处(写侧散布 + 读侧散布双根因,见
|
||||
//! generating状态机加固-2026-06-15.md §2.3)。本模块提供**显式的、单一真相的**状态枚举:
|
||||
//!
|
||||
//! - **写侧收敛**:`ConvState` 仅经 [`ConvState::transition_to`] 守卫方法迁移,非法转换
|
||||
//! 直接拒绝(`Err(InvalidTransition)`),调用方无法绕过守卫写入非法组合。
|
||||
//! - **读侧收敛**:停止按钮三态(可停 / 停中 / 停失败可重试)、MaxRoundsCard 是否弹等
|
||||
//! 判别逻辑由 `ConvState` 变体直接表达,不再靠多变量组合反推。
|
||||
//!
|
||||
//! # 渐进第一步(本批范围)
|
||||
//!
|
||||
//! 本模块是**纯逻辑、无 IO**的 enum + 转换守卫,不替代 `PerConvState.generating` 等
|
||||
//! 字段(渐进不一次全换)。guard 接入点(`GeneratingGuard` set/reset 时同步 `ConvState`
|
||||
//! 转换)作为视图层;开关 [`CONV_STATE_ENABLED`](constant.CONV_STATE_ENABLED.html)
|
||||
//! 默认 on,off 降级纯旧 bool 行为(可回退)。
|
||||
//!
|
||||
//! 后续(批2+):把散落判别点(`try_continue` / `ai_chat_stop` / MaxRoundsCard)逐个迁到
|
||||
//! 读 `ConvState`,并视情况把 `generating` bool 退役(状态字段由 enum 单一持有)。
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// ============================================================
|
||||
// 开关
|
||||
// ============================================================
|
||||
|
||||
/// L2 状态机总开关(默认 on)。
|
||||
///
|
||||
/// - **on(默认)**:`GeneratingGuard` set/reset 时同步迁移 [`ConvState`](enum.ConvState.html)
|
||||
/// 作视图层,前后端可读统一状态;guard 失败兜底仍走旧 bool 复位(`generating=false`),
|
||||
/// 不影响语义(状态机层失败不阻断核心复位)。
|
||||
/// - **off(降级)**:guard 不写 `ConvState`,纯旧 `generating` bool 行为(排障 / 对比 / 临时
|
||||
/// 回退用)。设为 false 即可观察无状态机接入的效果,语义等价改动前。
|
||||
///
|
||||
/// 机制优先 prompt 说教:每改进配开关 + 兜底,分阶段可回退(对齐 AI 改进方案三原则)。
|
||||
pub const CONV_STATE_ENABLED: bool = true;
|
||||
|
||||
// ============================================================
|
||||
// ConvState enum
|
||||
// ============================================================
|
||||
|
||||
/// 对话生命周期状态(单一真相源,5 态)。
|
||||
///
|
||||
/// 设计取舍(对齐 generating状态机加固-2026-06-15.md §3.2 「轻量状态机,不引入框架」):
|
||||
///
|
||||
/// - **不引入状态机框架**(codegen / FSM 库):5 态 7 边转换简单,枚举 + 显式 `transition_to`
|
||||
/// 守卫即可表达,过度封装得不偿失。
|
||||
/// - **派生关系明确**:`Compressed` 是 `Generating`/`Idle` 期间的瞬时派生态(压缩跑完
|
||||
/// 回到原态),非终端态;`Error` 可经用户「重试」回到 `Idle` 再 `Generating`。
|
||||
/// - **与读视图 [`SessionState`](../enum.SessionState.html) 区分**:`SessionState`
|
||||
/// (Streaming/AwaitingApproval/Idle)是面向「是否有审批挂起」的只读视图(读侧收敛①),
|
||||
/// `ConvState` 是面向「生成生命周期」的写侧真相(写收敛)。两者正交:如 `Generating` 态
|
||||
/// 同时有审批挂起时,`ConvState=Generating` 而 `SessionState=AwaitingApproval`。
|
||||
///
|
||||
/// 序列化(`Serialize`/`Deserialize`):批3 前端经事件总线读 enum 视图时使用(本批未接,
|
||||
/// 预留,避免后续改动序列化兼容性)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum ConvState {
|
||||
/// 空闲:无 agentic loop 活跃,可接新请求。
|
||||
///
|
||||
/// 终态目标:所有正常退出路径(完成 / 停止完成 / 错误恢复)的归处。
|
||||
Idle,
|
||||
/// 生成中:agentic loop 流式生成 + 工具执行活跃中(`generating=true`)。
|
||||
///
|
||||
/// 审批挂起期间 `ConvState` 仍为 `Generating`(审批是 loop 内的暂停点,非独立状态)。
|
||||
Generating,
|
||||
/// 停止中:用户点了停止(`stop_flag` 已置位),loop 检测中或正在收尾退出。
|
||||
///
|
||||
/// 瞬态:loop 检测到 `stop_flag` 后迁移到 `Idle`(完成收尾),或停止过程出错到 `Error`。
|
||||
Stopping,
|
||||
/// 错误态:生成过程出错(provider 失败 / Fatal / 断路器熔断等),loop 已退出。
|
||||
///
|
||||
/// 非终态:用户可「重试」从 `Error` → `Idle` → `Generating` 重新发起,或继续空闲。
|
||||
Error,
|
||||
/// 压缩派生态:loop 期间触发上下文压缩(LLM 摘要),生成流程的瞬时派生。
|
||||
///
|
||||
/// 派生而非独立态:压缩完成(成功 / 失败兜底)后回到原 `Generating`(loop 内压缩)或
|
||||
/// `Idle`(手动压缩 IPC 触发)。前端可据此展示「压缩中」spinner(本批未接)。
|
||||
Compressed,
|
||||
}
|
||||
|
||||
impl Default for ConvState {
|
||||
/// 新会话默认 `Idle`(无 loop 活跃)。
|
||||
fn default() -> Self {
|
||||
ConvState::Idle
|
||||
}
|
||||
}
|
||||
|
||||
impl ConvState {
|
||||
/// 尝试迁移到目标态。合法返回 `Ok(新态)`,非法拒绝返回 `Err(InvalidTransition)`。
|
||||
///
|
||||
/// 调用方持锁后经本方法迁移状态(写收敛):不允许直接写字段绕过守卫。
|
||||
/// 非法转换(如 `Idle → Stopping`,无活跃 loop 不可能停止)直接拒绝——调用方应视为
|
||||
/// 逻辑 bug 并记录 warn 日志,不静默忽略(状态机完整性优先)。
|
||||
///
|
||||
/// # 合法转换表(7 边)
|
||||
///
|
||||
/// | from | to | 触发场景 |
|
||||
/// |------|----|---------|
|
||||
/// | Idle | Generating | `run_agentic_loop` 入口,guard set |
|
||||
/// | Generating | Stopping | 用户点停止,置 `stop_flag` |
|
||||
/// | Generating | Idle | loop 正常收敛退出,guard reset |
|
||||
/// | Generating | Error | provider Fatal / 断路器熔断等 |
|
||||
/// | Generating | Compressed | loop 内自动压缩派生 |
|
||||
/// | Stopping | Idle | stop 收尾完成,guard reset |
|
||||
/// | Stopping | Error | 停止过程出错 |
|
||||
/// | Error | Idle | 用户重试前置位(准备再 `Generating`) |
|
||||
/// | Error | Generating | 直接从错误态重新生成(重试) |
|
||||
/// | Compressed | Generating | 压缩完成,loop 内继续 |
|
||||
/// | Compressed | Idle | 手动压缩 IPC 完成(无 loop 活跃) |
|
||||
///
|
||||
/// 自环(同态 → 同态)允许(幂等写,如 guard 多次 set 同态),返回 `Ok(self)`。
|
||||
pub fn transition_to(self, target: ConvState) -> Result<ConvState, InvalidTransition> {
|
||||
// 自环幂等:同态迁移直接通过(防调用方重复 set 报错)。
|
||||
if self == target {
|
||||
return Ok(target);
|
||||
}
|
||||
// 合法转换白名单(显式列举,非 `match _ => Ok` 兜底——新加态必须显式补边,
|
||||
// 编译器不会漏)。
|
||||
let valid = matches!(
|
||||
(self, target),
|
||||
// Idle 起步
|
||||
(ConvState::Idle, ConvState::Generating)
|
||||
// Generating 分流:停 / 完成 / 错误 / 压缩派生
|
||||
| (ConvState::Generating, ConvState::Stopping)
|
||||
| (ConvState::Generating, ConvState::Idle)
|
||||
| (ConvState::Generating, ConvState::Error)
|
||||
| (ConvState::Generating, ConvState::Compressed)
|
||||
// Stopping 收尾:完成 / 出错
|
||||
| (ConvState::Stopping, ConvState::Idle)
|
||||
| (ConvState::Stopping, ConvState::Error)
|
||||
// Error 恢复:重试前置位 / 直接重新生成
|
||||
| (ConvState::Error, ConvState::Idle)
|
||||
| (ConvState::Error, ConvState::Generating)
|
||||
// Compressed 派生回退:loop 继续 / 手动压缩完成
|
||||
| (ConvState::Compressed, ConvState::Generating)
|
||||
| (ConvState::Compressed, ConvState::Idle)
|
||||
);
|
||||
if valid {
|
||||
Ok(target)
|
||||
} else {
|
||||
Err(InvalidTransition { from: self, to: target })
|
||||
}
|
||||
}
|
||||
|
||||
/// 是否处于活跃生成态(`Generating` / 压缩派生 `Compressed`)。
|
||||
///
|
||||
/// 读侧便利方法:压缩派生期间 loop 仍活跃(只是临时跑压缩 LLM),归「活跃」。
|
||||
/// 审批挂起(读视图 `SessionState::AwaitingApproval`)期间 `ConvState` 仍是 `Generating`,
|
||||
/// 故本方法返回 true——审批挂起不算「停止生成」。
|
||||
///
|
||||
/// 预留:批2+ 读侧迁移(try_continue / ai_chat_stop / 前端停止三态)消费。本批仅建状态机 +
|
||||
/// 入口校验,无消费方,标 `allow(dead_code)` 保留(对齐零调用方区分真死 vs 预留原则)。
|
||||
#[allow(dead_code)]
|
||||
pub fn is_active(self) -> bool {
|
||||
matches!(self, ConvState::Generating | ConvState::Compressed)
|
||||
}
|
||||
|
||||
/// 是否可接受新请求(非活跃、非停止中、非压缩派生)。
|
||||
///
|
||||
/// 读侧便利方法:用于 `ai_conversation_create` / `ai_chat_send` 等入口判断
|
||||
/// 「能否接新请求」。`Error` 态视为可接(用户重试即从 Error 起步)。
|
||||
///
|
||||
/// 预留:批2+ 读侧迁移(入口能否接新请求判断)消费。本批无消费方,标 `allow(dead_code)` 保留。
|
||||
#[allow(dead_code)]
|
||||
pub fn can_accept_request(self) -> bool {
|
||||
matches!(self, ConvState::Idle | ConvState::Error)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// InvalidTransition(非法转换错误)
|
||||
// ============================================================
|
||||
|
||||
/// 状态机非法转换错误。
|
||||
///
|
||||
/// 携带 `from` / `to` 供调用方记录诊断日志(不 panic:状态机完整性违反应记 warn +
|
||||
/// 兜底走旧 bool 行为,不阻断核心生成流程——对齐「机制优先,失败兜底」原则)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct InvalidTransition {
|
||||
pub from: ConvState,
|
||||
pub to: ConvState,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for InvalidTransition {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "ConvState 非法转换:{:?} → {:?}", self.from, self.to)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for InvalidTransition {}
|
||||
|
||||
// ============================================================
|
||||
// 单元测试(纯逻辑无 IO)
|
||||
// ============================================================
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// ---- 合法转换通过 ----
|
||||
|
||||
#[test]
|
||||
fn test_idle_to_generating() {
|
||||
assert_eq!(
|
||||
ConvState::Idle.transition_to(ConvState::Generating),
|
||||
Ok(ConvState::Generating)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generating_to_stopping() {
|
||||
assert_eq!(
|
||||
ConvState::Generating.transition_to(ConvState::Stopping),
|
||||
Ok(ConvState::Stopping)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generating_to_idle_normal_complete() {
|
||||
// loop 正常收敛退出
|
||||
assert_eq!(
|
||||
ConvState::Generating.transition_to(ConvState::Idle),
|
||||
Ok(ConvState::Idle)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generating_to_error_provider_fatal() {
|
||||
// provider Fatal / 断路器熔断
|
||||
assert_eq!(
|
||||
ConvState::Generating.transition_to(ConvState::Error),
|
||||
Ok(ConvState::Error)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_generating_to_compressed_loop_auto_compress() {
|
||||
// loop 内自动压缩派生
|
||||
assert_eq!(
|
||||
ConvState::Generating.transition_to(ConvState::Compressed),
|
||||
Ok(ConvState::Compressed)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stopping_to_idle_complete() {
|
||||
// stop 收尾完成
|
||||
assert_eq!(
|
||||
ConvState::Stopping.transition_to(ConvState::Idle),
|
||||
Ok(ConvState::Idle)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stopping_to_error() {
|
||||
assert_eq!(
|
||||
ConvState::Stopping.transition_to(ConvState::Error),
|
||||
Ok(ConvState::Error)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_to_idle_retry_prep() {
|
||||
// 用户重试前置位
|
||||
assert_eq!(
|
||||
ConvState::Error.transition_to(ConvState::Idle),
|
||||
Ok(ConvState::Idle)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_to_generating_retry() {
|
||||
// 直接从错误态重新生成
|
||||
assert_eq!(
|
||||
ConvState::Error.transition_to(ConvState::Generating),
|
||||
Ok(ConvState::Generating)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compressed_to_generating_loop_continue() {
|
||||
// 压缩完成 loop 内继续
|
||||
assert_eq!(
|
||||
ConvState::Compressed.transition_to(ConvState::Generating),
|
||||
Ok(ConvState::Generating)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compressed_to_idle_manual_compress_done() {
|
||||
// 手动压缩 IPC 完成(无 loop 活跃)
|
||||
assert_eq!(
|
||||
ConvState::Compressed.transition_to(ConvState::Idle),
|
||||
Ok(ConvState::Idle)
|
||||
);
|
||||
}
|
||||
|
||||
// ---- 自环幂等 ----
|
||||
|
||||
#[test]
|
||||
fn test_self_transition_idempotent() {
|
||||
// 同态迁移直接通过(防调用方重复 set 报错)
|
||||
for s in [
|
||||
ConvState::Idle,
|
||||
ConvState::Generating,
|
||||
ConvState::Stopping,
|
||||
ConvState::Error,
|
||||
ConvState::Compressed,
|
||||
] {
|
||||
assert_eq!(s.transition_to(s), Ok(s), "{:?} 自环应通过", s);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 非法转换拒绝 ----
|
||||
|
||||
#[test]
|
||||
fn test_idle_to_stopping_rejected() {
|
||||
// 无活跃 loop 不可能停止
|
||||
let err = ConvState::Idle
|
||||
.transition_to(ConvState::Stopping)
|
||||
.unwrap_err();
|
||||
assert_eq!(err.from, ConvState::Idle);
|
||||
assert_eq!(err.to, ConvState::Stopping);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idle_to_error_rejected() {
|
||||
// Idle 直接到 Error 无意义(错误必经活跃态产生)
|
||||
assert!(ConvState::Idle.transition_to(ConvState::Error).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idle_to_compressed_rejected() {
|
||||
// 无 loop 活跃不会触发压缩派生(手动压缩 IPC 不经状态机派生)
|
||||
assert!(ConvState::Idle.transition_to(ConvState::Compressed).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stopping_to_generating_rejected() {
|
||||
// 停止中不能直接回生成(须先回 Idle 再起)
|
||||
assert!(ConvState::Stopping
|
||||
.transition_to(ConvState::Generating)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_stopping_to_compressed_rejected() {
|
||||
assert!(ConvState::Stopping
|
||||
.transition_to(ConvState::Compressed)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_to_stopping_rejected() {
|
||||
// 错误态已停止,不能再停
|
||||
assert!(ConvState::Error.transition_to(ConvState::Stopping).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_to_compressed_rejected() {
|
||||
assert!(ConvState::Error.transition_to(ConvState::Compressed).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compressed_to_stopping_rejected() {
|
||||
// 压缩派生期间不直接到停止(压缩完成后由原态分流)
|
||||
assert!(ConvState::Compressed
|
||||
.transition_to(ConvState::Stopping)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compressed_to_error_rejected() {
|
||||
assert!(ConvState::Compressed
|
||||
.transition_to(ConvState::Error)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
// ---- 便利方法 ----
|
||||
|
||||
#[test]
|
||||
fn test_is_active() {
|
||||
assert!(!ConvState::Idle.is_active());
|
||||
assert!(ConvState::Generating.is_active());
|
||||
assert!(!ConvState::Stopping.is_active());
|
||||
assert!(!ConvState::Error.is_active());
|
||||
// 压缩派生期间 loop 仍活跃(临时跑压缩 LLM)
|
||||
assert!(ConvState::Compressed.is_active());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_can_accept_request() {
|
||||
assert!(ConvState::Idle.can_accept_request());
|
||||
assert!(!ConvState::Generating.can_accept_request());
|
||||
assert!(!ConvState::Stopping.can_accept_request());
|
||||
// Error 态可接(用户重试即从 Error 起步)
|
||||
assert!(ConvState::Error.can_accept_request());
|
||||
assert!(!ConvState::Compressed.can_accept_request());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_default_is_idle() {
|
||||
assert_eq!(ConvState::default(), ConvState::Idle);
|
||||
}
|
||||
|
||||
// ---- 完整生命周期链 ----
|
||||
|
||||
#[test]
|
||||
fn test_full_lifecycle_normal_complete() {
|
||||
// 正常生命周期:Idle → Generating → Idle
|
||||
let s = ConvState::Idle;
|
||||
let s = s.transition_to(ConvState::Generating).unwrap();
|
||||
let s = s.transition_to(ConvState::Idle).unwrap();
|
||||
assert_eq!(s, ConvState::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_lifecycle_with_stop() {
|
||||
// 停止生命周期:Idle → Generating → Stopping → Idle
|
||||
let s = ConvState::Idle;
|
||||
let s = s.transition_to(ConvState::Generating).unwrap();
|
||||
let s = s.transition_to(ConvState::Stopping).unwrap();
|
||||
let s = s.transition_to(ConvState::Idle).unwrap();
|
||||
assert_eq!(s, ConvState::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_lifecycle_with_compress() {
|
||||
// 压缩派生:Idle → Generating → Compressed → Generating → Idle
|
||||
let s = ConvState::Idle;
|
||||
let s = s.transition_to(ConvState::Generating).unwrap();
|
||||
let s = s.transition_to(ConvState::Compressed).unwrap();
|
||||
let s = s.transition_to(ConvState::Generating).unwrap();
|
||||
let s = s.transition_to(ConvState::Idle).unwrap();
|
||||
assert_eq!(s, ConvState::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_full_lifecycle_error_retry() {
|
||||
// 错误恢复:Idle → Generating → Error → Generating → Idle
|
||||
let s = ConvState::Idle;
|
||||
let s = s.transition_to(ConvState::Generating).unwrap();
|
||||
let s = s.transition_to(ConvState::Error).unwrap();
|
||||
let s = s.transition_to(ConvState::Generating).unwrap();
|
||||
let s = s.transition_to(ConvState::Idle).unwrap();
|
||||
assert_eq!(s, ConvState::Idle);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_transition_error_display() {
|
||||
// Display 包含 from / to,供日志诊断
|
||||
let err = ConvState::Idle
|
||||
.transition_to(ConvState::Stopping)
|
||||
.unwrap_err();
|
||||
let msg = format!("{}", err);
|
||||
assert!(msg.contains("Idle"), "Display 应含 from: {}", msg);
|
||||
assert!(msg.contains("Stopping"), "Display 应含 to: {}", msg);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user