重构: AI流式断线保文StreamResult三分支+重试对齐决策a1,推进链落df-nodes
This commit is contained in:
@@ -6,7 +6,10 @@ pub mod context;
|
||||
pub mod coordinator;
|
||||
pub mod openai_compat;
|
||||
pub mod provider;
|
||||
mod retry;
|
||||
// CR-30-1: 流前重试退避对外复用。complete() 的 retry_with_backoff 仍 crate 内用,
|
||||
// stream_recv/agentic 流前重试需复用 backoff_delay(jitter)+is_status_retryable(Fatal 分类)
|
||||
// 避免重写退避/分类逻辑(对齐决策 F-260616-07 a1)。改 pub mod 后对外仅暴露纯函数 + 常量。
|
||||
pub mod retry;
|
||||
|
||||
use provider::LlmProvider;
|
||||
|
||||
|
||||
@@ -37,8 +37,10 @@ const MAX_TOTAL_BUDGET: Duration = Duration::from_secs(30);
|
||||
const JITTER_RATIO: f64 = 0.2;
|
||||
|
||||
/// 一次尝试的分类结果 —— 在 anyhow 不透明化前决定是否值得重试。
|
||||
///
|
||||
/// CR-30-1: 暴露给 agentic.rs 流前重试复用(Init 失败分类 Fatal/Retryable 决定是否重试)。
|
||||
#[derive(Debug)]
|
||||
pub(super) enum AttemptOutcome<T> {
|
||||
pub enum AttemptOutcome<T> {
|
||||
/// 成功,携带结果。
|
||||
Ok(T),
|
||||
/// 可重试错误(timeout / connect / 5xx / 429)。携带人类可读错误描述。
|
||||
@@ -50,12 +52,14 @@ pub(super) enum AttemptOutcome<T> {
|
||||
/// 判定 reqwest::Error 是否可重试(仅 timeout / connect 类)。
|
||||
///
|
||||
/// body 解析错(reqwest::Error::is_decode)与请求构造错(is_builder)属 Fatal,不重试。
|
||||
pub(super) fn is_reqwest_error_retryable(e: &reqwest::Error) -> bool {
|
||||
pub fn is_reqwest_error_retryable(e: &reqwest::Error) -> bool {
|
||||
e.is_timeout() || e.is_connect() || (e.is_request() && !e.is_builder())
|
||||
}
|
||||
|
||||
/// 判定 HTTP 状态码是否可重试: 5xx 与 429 重试,其余(含 4xx)不重试。
|
||||
pub(super) fn is_status_retryable(status: u16) -> bool {
|
||||
///
|
||||
/// CR-30-1: 暴露给 agentic.rs 流前重试复用(4xx Fatal 立即放弃,5xx/429 重试)。
|
||||
pub fn is_status_retryable(status: u16) -> bool {
|
||||
status == 429 || (500..600).contains(&status)
|
||||
}
|
||||
|
||||
@@ -64,7 +68,10 @@ pub(super) fn is_status_retryable(status: u16) -> bool {
|
||||
///
|
||||
/// jitter 用 SystemTime 纳秒取模生成(无依赖),避免多客户端同步重试风暴。
|
||||
/// 以毫秒粒度计算后向下取整(避免秒级截断把 0.9s 砍成 0)。
|
||||
fn backoff_delay(attempt: u32) -> Duration {
|
||||
///
|
||||
/// CR-30-1: 暴露 pub 供 src-tauri/agentic.rs 流前重试复用(对齐决策 F-260616-07 a1
|
||||
/// "复用 retry.rs backoff_delay 退避 1s→2s→4s+jitter"),避免重写退避逻辑。
|
||||
pub fn backoff_delay(attempt: u32) -> Duration {
|
||||
let base_ms = BASE_BACKOFF_SECS.saturating_mul(1u64 << (attempt - 1)) * 1000;
|
||||
// 纳秒 → [0, 2000) 区间,再映射到 [-1.0, +1.0) 比例
|
||||
let nanos = SystemTime::now()
|
||||
@@ -85,7 +92,11 @@ fn backoff_delay(attempt: u32) -> Duration {
|
||||
///
|
||||
/// 调用方约定: `attempt_fn` 内部应保留完整请求重发能力(每次重建 RequestBuilder),
|
||||
/// 因为 reqwest::RequestBuilder 一次 `.send()` 后不可复用。
|
||||
pub(super) async fn retry_with_backoff<T, F, Fut>(
|
||||
///
|
||||
/// CR-30-1: 仍 pub(crate)(complete() 内部用),流前重试 stream_recv/agentic 不走此函数
|
||||
/// (流式 request 不可整体 retry_with_backoff 包裹,需在 agentic loop 内手写循环复用
|
||||
/// backoff_delay + is_status_retryable)。
|
||||
pub(crate) async fn retry_with_backoff<T, F, Fut>(
|
||||
label: &str,
|
||||
attempt_fn: F,
|
||||
) -> anyhow::Result<T>
|
||||
|
||||
@@ -379,10 +379,15 @@ mod tests {
|
||||
// 唯一未覆盖的是「闭包组装 + tracing」胶水代码(非业务逻辑)。
|
||||
// ============================================================
|
||||
|
||||
/// 镜像 workflow.rs::regression_target(workflow.rs L44-51 私有函数)的失败退回映射。
|
||||
/// 镜像 workflow.rs::regression_target(workflow.rs L44-54 私有函数)的失败退回映射。
|
||||
///
|
||||
/// 此处是契约镜像(非导入):若 workflow.rs 改映射而忘同步,这里会红,提醒两处一致。
|
||||
/// 映射:testing→in_review / in_review→in_progress / in_progress→todo / 其他→None。
|
||||
/// 映射:testing→in_review / in_review→in_progress / in_progress→None / 其他→None。
|
||||
///
|
||||
/// 为什么 in_progress → None? 状态机禁止 in_progress→todo
|
||||
/// (task_state_machine.rs backward_to_todo_rejected),失败退回 todo 会被
|
||||
/// advance_task_atomic 的 InvalidState 拦截;改为 None 则回调跳过推进,
|
||||
/// 留 in_progress 等人介入(决策 CR-13-O1-b)。
|
||||
///
|
||||
/// 为什么不 pub regression_target 复用? 它是 workflow.rs(app crate)的私有函数,
|
||||
/// df-nodes 不依赖 app crate(单向依赖:app → df-nodes)。把退回映射当 df-nodes 的
|
||||
@@ -391,7 +396,8 @@ mod tests {
|
||||
match target {
|
||||
"testing" => Some("in_review"),
|
||||
"in_review" => Some("in_progress"),
|
||||
"in_progress" => Some("todo"),
|
||||
// in_progress 失败不自动退回(状态机不允许→todo),留 in_progress 等人介入
|
||||
"in_progress" => None,
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -414,7 +420,8 @@ mod tests {
|
||||
// 锁定 workflow.rs ②-4 失败退回映射表(逐项)。
|
||||
assert_eq!(regression_target("testing"), Some("in_review"));
|
||||
assert_eq!(regression_target("in_review"), Some("in_progress"));
|
||||
assert_eq!(regression_target("in_progress"), Some("todo"));
|
||||
// CR-13-O1-b: in_progress 失败不退回(状态机禁止→todo),留 in_progress 等人介入
|
||||
assert_eq!(regression_target("in_progress"), None);
|
||||
// done 是终态前向目标,失败无可退态(workflow.rs 注释:done→None)
|
||||
assert_eq!(regression_target("done"), None);
|
||||
// todo 是起点态无可退;blocked/cancelled 非推进链目标 → None
|
||||
@@ -454,10 +461,8 @@ mod tests {
|
||||
callback_advance_target("failed", "in_review").as_deref(),
|
||||
Some("in_progress")
|
||||
);
|
||||
assert_eq!(
|
||||
callback_advance_target("failed", "in_progress").as_deref(),
|
||||
Some("todo")
|
||||
);
|
||||
// CR-13-O1-b: in_progress 失败不退回(状态机禁止→todo),回调返回 None 跳过推进
|
||||
assert_eq!(callback_advance_target("failed", "in_progress"), None);
|
||||
// failed + done:无退回映射 → None(回调跳过,workflow.rs 会 warn 跳过日志)
|
||||
assert_eq!(callback_advance_target("failed", "done"), None);
|
||||
}
|
||||
@@ -526,21 +531,15 @@ mod tests {
|
||||
assert_eq!(r.status, "in_progress");
|
||||
assert_eq!(r.review_rounds, 1);
|
||||
|
||||
// in_progress 模板失败:任务当前 in_progress → 退回 todo(rounds 不累加,
|
||||
// 因 todo→in_progress 是前向,in_progress→todo 是非法转换 —— 见状态机)
|
||||
// 关键:regression_target("in_progress")=todo,但状态机不允许 in_progress→todo,
|
||||
// 故回调 advance_task_atomic 会报 InvalidState。验证这一防御性行为:
|
||||
// in_progress 模板失败:regression_target("in_progress")=None(CR-13-O1-b),
|
||||
// 回调返回 None → 跳过推进,任务保留 in_progress 等人介入。
|
||||
// 验证:callback 返回 None,不调 advance_task_atomic。
|
||||
repo.insert(rec("f3", "in_progress")).await.unwrap();
|
||||
let to = callback_advance_target("failed", "in_progress").unwrap();
|
||||
assert_eq!(to, "todo");
|
||||
let err = advance_task_atomic(&repo, "f3", &to).await.unwrap_err();
|
||||
assert!(
|
||||
matches!(err, df_core::error::Error::InvalidState { .. }),
|
||||
"in_progress→todo 是非法转换,回调应被状态机拦截(InvalidState),实际: {err:?}"
|
||||
);
|
||||
let to = callback_advance_target("failed", "in_progress");
|
||||
assert_eq!(to, None, "in_progress 失败应跳过推进(None),实际: {to:?}");
|
||||
// 任务状态未被改动(仍是 in_progress)
|
||||
let still = repo.get_by_id("f3").await.unwrap().unwrap();
|
||||
assert_eq!(still.status, "in_progress", "非法退回不应改动 status");
|
||||
assert_eq!(still.status, "in_progress", "None 退回不应改动 status");
|
||||
}
|
||||
|
||||
/// ②-4 回调 failed+done 无退回映射验证:callback_advance_target 返回 None。
|
||||
|
||||
Reference in New Issue
Block a user