新增: 断点续跑与日志租约底座
This commit is contained in:
@@ -181,6 +181,22 @@ pub const SKILL_LIST_MAX_ITEMS: usize = 120;
|
||||
/// 清单只做提示,截断防长描述(部分技能 description 数百字)撑爆 prompt,截断后接省略号。
|
||||
pub const SKILL_DESC_MAX_CHARS: usize = 80;
|
||||
|
||||
/// 派工纪律母本(N1 通用能力内置沉淀):主 agent 派工纪律 8 行,措辞可微调不可删项(用户拍板)。
|
||||
///
|
||||
/// 为什么内置而非放知识库:知识库是分发面数据——新实例/新机器/知识库为空时纪律即失传(分发裸奔);
|
||||
/// 编译进二进制随代码分发,零配置生效,且位于 system 稳定段(跨轮缓存命中,只付一次 token 成本)。
|
||||
/// 双路径同源:build_runtime_prompt(主会话,派工方)+ sub_agent_system_prompt(子代理,被派方)
|
||||
/// 均拼接本母本,两侧共享同一套纪律文本,禁在别处另抄副本(改纪律只改这里)。
|
||||
pub(crate) const DISPATCH_DISCIPLINE: &str = r#"## 派工纪律
|
||||
1. 派工先估颗粒:单任务一轮内自证完成,装不下就拆批;独立子任务并行派。
|
||||
2. 任务书三要素:目标文件边界(热点文件单点编辑权)+预期时限+标准验证命令。
|
||||
3. 验收认证据:子代理须交"命令+结果",不收"我做完了";主代理独立抽查裁决。
|
||||
4. 子代理报告限长(约4千字),超长落盘只给指针;主代理消化后紧凑透出,禁全量转贴。
|
||||
5. 超时处置:到点发收敛令→无响应试探存活→确认死则主代理砍维度亲自收尾,勿重派同颗粒。
|
||||
6. 改代码禁全量验证:单文件快检,阶段收官统一验证一次。
|
||||
7. 派工前查重在途执行体,同类故障只委托一头。
|
||||
8. 结果透出结论先行,证据随附,过程不铺陈。"#;
|
||||
|
||||
/// G4 目标感知降级:话题标记 insert 当 pinned_goals 存在时跳过(默认 true)。
|
||||
///
|
||||
/// 治 R2(话题标记反向误导):G1 目标钉扎生效后每轮 system_prompt 已含目标,topic marker 的
|
||||
@@ -457,6 +473,17 @@ pub(crate) async fn run_agentic_loop(
|
||||
return;
|
||||
}
|
||||
|
||||
// V49 断点续跑底座:loop 启动置 loop_state='running'(fire-and-forget,不阻塞启动路径;
|
||||
// 正常收敛出口 emit_ai_completed_once 清理;进程重启时残留 running 由启动扫描改写 parked)。
|
||||
{
|
||||
let db2 = db.clone();
|
||||
let cid = conv_id.clone();
|
||||
tokio::spawn(async move {
|
||||
let repo = df_storage::crud::AiConversationRepo::new(&db2);
|
||||
let _ = repo.set_loop_state(&cid, Some("running")).await;
|
||||
});
|
||||
}
|
||||
|
||||
// 治本:LLM 生成间隙向前端发 AiHeartbeat,防前端 watchdog 误断流。
|
||||
// 后端 emit AiHeartbeat → useAiEvents.ts resetStreamWatchdog(convId) → per-conv timer 重置。
|
||||
// 解决:LLM 重试循环(~60s)期间无事件到达前端 → watchdog 45s/90s 到期误杀。
|
||||
@@ -840,8 +867,8 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 推理结果存入 pinned_goals: Vec<GoalEntry>,含 text+status 状态追踪。
|
||||
// 新目标加入时自动标记之前的 active 为 completed。
|
||||
//
|
||||
// 入口 system_prompt 五段拼接(G1 目标钉扎 / T5 工作上下文 / Env+behavior / 技能清单 /
|
||||
// T4 工作流 DAG)收敛至 build_runtime_prompt helper:一次锁读 per_conv 三字段
|
||||
// 入口 system_prompt 六段拼接(G1 目标钉扎 / T5 工作上下文 / Env+behavior / 派工纪律 /
|
||||
// 技能清单 / T4 工作流 DAG)收敛至 build_runtime_prompt helper:一次锁读 per_conv 三字段
|
||||
// (pinned_goals / working_context / workflow_dag_summary)消除原 G1/T5/T4 三处独立 lock,
|
||||
// 且返回 pinned_goals 复用作 AiCompleted 快照(消除原 L1157 二次读)。拼接顺序+内容逐字节等价。
|
||||
let (system_prompt, pinned_goals_snapshot) = build_runtime_prompt(
|
||||
@@ -1078,7 +1105,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
// ── 进度可观测段(设计 §4.3):每 N 轮 emit AiTaskProgress ──
|
||||
// round 1 起算对齐 AiAgentRound 口径;max_rounds=0=不限轮次(前端按无上限处理)。
|
||||
// owner 守卫:被接管的旧 loop 不发(防串会话进度)。事件即逝不落库,终局不补发。
|
||||
if iteration % PROGRESS_EVERY_N_ROUNDS == 0
|
||||
if (iteration % PROGRESS_EVERY_N_ROUNDS == 0 || iteration == start_iteration)
|
||||
&& loop_epoch_arc.load(Ordering::SeqCst) == my_epoch
|
||||
{
|
||||
emit_ai_chat_event(&app_handle, AiChatEvent::AiTaskProgress {
|
||||
@@ -2570,6 +2597,17 @@ pub(crate) async fn emit_ai_completed_once(
|
||||
super::commands::chat::finalize_pending_placeholders(&mut *session_guard, conv_id, "生成完成");
|
||||
session_guard.pending_approvals.retain(|_, a| a.conversation_id.as_deref() != Some(conv_id));
|
||||
}
|
||||
// V49:正常收敛出口清理 loop_state(fire-and-forget 独立 spawn,不与上面的锁段耦合;
|
||||
// 进程内 loop 死亡的残留 running 由启动扫描兜底)。
|
||||
{
|
||||
let app_state = app_handle.state::<AppState>();
|
||||
let db2 = app_state.db.clone();
|
||||
let cid = conv_id.to_string();
|
||||
tokio::spawn(async move {
|
||||
let repo = df_storage::crud::AiConversationRepo::new(&db2);
|
||||
let _ = repo.set_loop_state(&cid, None).await;
|
||||
});
|
||||
}
|
||||
// emit 端(前端通道):incomplete 用 emit_incomplete(MidStream 传 Some(true))。
|
||||
// token 分项(2026-08-02):cache_hit/cache_miss/reasoning 透传前端分计费展示。
|
||||
// publish 端:do_publish=false(max_iterations)跳过 publish(保留原无 publish 行为);
|
||||
@@ -2578,6 +2616,9 @@ pub(crate) async fn emit_ai_completed_once(
|
||||
let ev = completed_event(usage, is_estimated, emit_incomplete, incomplete_reason, conv_id, pinned_goals);
|
||||
if do_publish && emit_incomplete == publish_incomplete {
|
||||
emit_ai_chat_event(app_handle, ev);
|
||||
// P0 修复(2026-10-05):正常完成恒走此早退,完成通知桥必须在此投递——
|
||||
// 否则等待方(编排节点/飞书渠道)永远收不到完成通知(实证:等待方 30 分钟超时)。
|
||||
crate::commands::ai::commands::chat::deliver_completion_report_if_waited(app_handle, conv_id).await;
|
||||
return;
|
||||
}
|
||||
// 有意不走 emit_ai_chat_event(emit/publish 两端 incomplete 标志分叉,见上注释)——
|
||||
@@ -2602,18 +2643,8 @@ pub(crate) async fn emit_ai_completed_once(
|
||||
// 本函数是全部正常出口(5 类 teardown_round 出口 + 正常完成直调)的统一单点,
|
||||
// 到达者必为 owner loop(epoch 守卫在各调用方:teardown_round 入口 / 正常完成双检),
|
||||
// force_send 接管后的旧 loop 不触发。无等待方(普通用户会话)has 判空短路,零会话锁开销。
|
||||
{
|
||||
let app_state = app_handle.state::<AppState>();
|
||||
if app_state.has_agent_completion_waiter(conv_id) {
|
||||
let report = last_active_assistant_text(&app_state.ai_session, conv_id).await;
|
||||
let delivered = app_state.notify_agent_completion(conv_id, report);
|
||||
tracing::info!(
|
||||
conv_id = %conv_id,
|
||||
delivered,
|
||||
"[ai] 编排会话收敛,完成通知桥投递最终报告"
|
||||
);
|
||||
}
|
||||
}
|
||||
// review ④:内联块收敛到 deliver_completion_report_if_waited 单实现(与上方早退分支同源)。
|
||||
crate::commands::ai::commands::chat::deliver_completion_report_if_waited(app_handle, conv_id).await;
|
||||
}
|
||||
|
||||
/// 取会话末条 active assistant 消息文本(编排会话最终报告投递用)。
|
||||
@@ -2621,7 +2652,7 @@ pub(crate) async fn emit_ai_completed_once(
|
||||
/// conv 不存在 / 无 assistant 消息返空串,原样投递,空文本由节点侧判「无输出」失败
|
||||
/// (用户 stop 在任何输出前的场景)。读内存 per_conv 非常规出口:正常完成路径 save 落库
|
||||
/// 在后台 spawn 里,时序上可能晚于本函数,内存消息是唯一已确定就绪的真相源。
|
||||
async fn last_active_assistant_text(
|
||||
pub(crate) async fn last_active_assistant_text(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
conv_id: &str,
|
||||
) -> String {
|
||||
@@ -2663,9 +2694,10 @@ fn completed_event(
|
||||
}
|
||||
}
|
||||
|
||||
// ── build_runtime_prompt: run_agentic_loop 入口 system_prompt 五段拼接 ──
|
||||
// ── build_runtime_prompt: run_agentic_loop 入口 system_prompt 六段拼接 ──
|
||||
//
|
||||
// 抽自原 L1024-1152(G1 目标钉扎 / T5 工作上下文 / Env+behavior / 技能清单 / T4 工作流 DAG)。
|
||||
// N1 增量:新增派工纪律段(DISPATCH_DISCIPLINE 母本内置,拼在 behavior 后),非原五段内容。
|
||||
// **行为逐字节等价**:拼接顺序与内容不变,开关(GOAL_PIN_ENABLED / SKILL_LIST_INJECT_ENABLED /
|
||||
// GOAL_INJECT_BANNER)与截断(GOAL_MAX_CHARS / SKILL_LIST_MAX_ITEMS / SKILL_DESC_MAX_CHARS)常量原样引用。
|
||||
//
|
||||
@@ -2757,6 +2789,11 @@ async fn build_runtime_prompt(
|
||||
);
|
||||
system_prompt = format!("{}\n\n{}\n\n{}", system_prompt, env_prompt, behavior_prompt);
|
||||
|
||||
// N1 派工纪律内置注入:母本编译进二进制(知识库为分发面数据,新实例知识库为空即裸奔,
|
||||
// 内置零配置生效)。与 sub_agent_system_prompt 同源引用 DISPATCH_DISCIPLINE,
|
||||
// 主 agent(派工方)受约束,子代理(被派方)同样知晓纪律。
|
||||
system_prompt = format!("{}\n\n{}", system_prompt, DISPATCH_DISCIPLINE);
|
||||
|
||||
// 技能清单注入:skills_cached() 进程内缓存(懒扫描一次,后续零开销),无锁需求。
|
||||
if SKILL_LIST_INJECT_ENABLED {
|
||||
let skills = crate::commands::ai::skills::skills_cached().await;
|
||||
|
||||
Reference in New Issue
Block a user