修复: 工作流推进链路与执行入口优化
This commit is contained in:
@@ -13,7 +13,7 @@ use df_ai::provider::LlmProvider;
|
|||||||
// AiNode 路由 — 节点 config.model_id 优先;否则按 TaskRequirements 路由
|
// AiNode 路由 — 节点 config.model_id 优先;否则按 TaskRequirements 路由
|
||||||
// (默认 Standard + needs_tool_use=true)。池空/无匹配兜底 record.default_model。
|
// (默认 Standard + needs_tool_use=true)。池空/无匹配兜底 record.default_model。
|
||||||
use df_ai::router::{select_model_id, TaskRequirements};
|
use df_ai::router::{select_model_id, TaskRequirements};
|
||||||
use df_storage::crud::AiProviderRepo;
|
use df_storage::crud::{AiProviderRepo, TaskRepo};
|
||||||
use df_storage::db::Database;
|
use df_storage::db::Database;
|
||||||
use df_storage::models::AiProviderRecord;
|
use df_storage::models::AiProviderRecord;
|
||||||
use df_storage::secret::{ensure_resolved_key, resolve_provider_secret};
|
use df_storage::secret::{ensure_resolved_key, resolve_provider_secret};
|
||||||
@@ -72,7 +72,66 @@ pub(crate) async fn resolve_and_parse(
|
|||||||
inputs: &HashMap<String, NodeOutput>,
|
inputs: &HashMap<String, NodeOutput>,
|
||||||
) -> anyhow::Result<AiNodeParams> {
|
) -> anyhow::Result<AiNodeParams> {
|
||||||
let provider_cfg = resolve_provider(db, config).await?;
|
let provider_cfg = resolve_provider(db, config).await?;
|
||||||
parse_params(config, inputs, provider_cfg)
|
// P0-B: parse_params 缺 prompt 报错时兜底 —— 工作流推进链模板 AiNode config 无 prompt
|
||||||
|
// (in_progress/testing/done 三链首节点 AiNode 仅带 provider_id, prompt 留空),
|
||||||
|
// 原行为直接 Err 致整链必失败。兜底:从 config.task_id 读 TaskRecord 生成基于
|
||||||
|
// title/description 的执行 prompt;无 task_id 用通用默认。不改 LLM 调用结构,
|
||||||
|
// 仅 parse_params 失败时补 prompt 重解析(provider_cfg 已 resolve 不重复)。
|
||||||
|
// 实现:parse_params 按值吃 provider_cfg,失败需重建 → 先 clone 一份供兜底重解析用,
|
||||||
|
// 避免 ResolvedProvider(api_key 等)无法从 Err 取回(原签名为 by-value 消费)。
|
||||||
|
match parse_params(config, inputs, provider_cfg.clone()) {
|
||||||
|
Ok(p) => Ok(p),
|
||||||
|
Err(e) => {
|
||||||
|
let prompt = resolve_fallback_prompt(db, config)
|
||||||
|
.await
|
||||||
|
.unwrap_or_else(|| default_fallback_prompt());
|
||||||
|
// 把兜底 prompt 注入 config 后重解析(prompt 字段补齐后 parse_params 不再缺 prompt)。
|
||||||
|
// 用 mutable config copy 避免改原 config 借用(config: &serde_json::Value 不可变借)。
|
||||||
|
let mut cfg = config.clone();
|
||||||
|
if let Some(obj) = cfg.as_object_mut() {
|
||||||
|
obj.insert("prompt".to_string(), serde_json::Value::String(prompt));
|
||||||
|
} else {
|
||||||
|
// config 非 object(理论不会,防御):构造 {prompt: ...} 替代。
|
||||||
|
cfg = serde_json::json!({ "prompt": prompt });
|
||||||
|
}
|
||||||
|
// tracing 兜底可见性(用户/排查:看到 AiNode 用了默认 prompt 而非报错)。
|
||||||
|
tracing::warn!(
|
||||||
|
error = %e,
|
||||||
|
"AiNode 缺 prompt 兜底: 从 task_id/通用默认生成执行 prompt"
|
||||||
|
);
|
||||||
|
parse_params(&cfg, inputs, provider_cfg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0-B 兜底 prompt 构建:从 config.task_id 读 TaskRecord.title/description 生成执行 prompt。
|
||||||
|
///
|
||||||
|
/// 无 task_id / 查询失败 / task 不存在 → None,调用方降级到 `default_fallback_prompt`。
|
||||||
|
/// description 空只取 title;非空拼「标题(描述)」。truncate 防 token 滥用。
|
||||||
|
async fn resolve_fallback_prompt(db: &Arc<Database>, config: &serde_json::Value) -> Option<String> {
|
||||||
|
let task_id = config.get("task_id").and_then(|v| v.as_str())?;
|
||||||
|
if task_id.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let repo = TaskRepo::new(db);
|
||||||
|
let task = repo.get_by_id(task_id).await.ok()??;
|
||||||
|
let title = task.title.trim();
|
||||||
|
let desc = task.description.trim();
|
||||||
|
let body = if desc.is_empty() {
|
||||||
|
title.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{}({})", title, truncate_for_summary(desc))
|
||||||
|
};
|
||||||
|
Some(format!(
|
||||||
|
"根据任务「{}」完成开发,产出可执行代码/方案并自检。要求:逐步推理、\
|
||||||
|
关键决策给出依据、产出可直接落地。",
|
||||||
|
body
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// P0-B 通用兜底 prompt:无 task_id 时用(独立 AiNode 调用 / config 无 task 关联)。
|
||||||
|
fn default_fallback_prompt() -> String {
|
||||||
|
"请根据上下文完成当前任务,产出可执行结果并自检。若需更多信息请说明。".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// SW-260618-09: build_provider 5 行封装 DRY(AiNode/AiSelfReviewNode execute 逐字重复)。
|
/// SW-260618-09: build_provider 5 行封装 DRY(AiNode/AiSelfReviewNode execute 逐字重复)。
|
||||||
|
|||||||
@@ -1021,146 +1021,14 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
// 不再从用户消息规则提取,也不依赖 LLM 输出结构化标记。
|
// 不再从用户消息规则提取,也不依赖 LLM 输出结构化标记。
|
||||||
// 推理结果存入 pinned_goals: Vec<GoalEntry>,含 text+status 状态追踪。
|
// 推理结果存入 pinned_goals: Vec<GoalEntry>,含 text+status 状态追踪。
|
||||||
// 新目标加入时自动标记之前的 active 为 completed。
|
// 新目标加入时自动标记之前的 active 为 completed。
|
||||||
let mut system_prompt = system_prompt;
|
|
||||||
if GOAL_PIN_ENABLED {
|
|
||||||
let goals: Vec<super::GoalEntry> = {
|
|
||||||
let session = session_arc.lock().await;
|
|
||||||
session
|
|
||||||
.conv_read(&conv_id)
|
|
||||||
.map(|c| c.pinned_goals.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
};
|
|
||||||
// 只把 active 目标注入 system_prompt(completed 不干扰 LLM 注意力)
|
|
||||||
let active_goals: Vec<&super::GoalEntry> = goals.iter().filter(|g| matches!(g.status, super::GoalStatus::Active)).collect();
|
|
||||||
if !active_goals.is_empty() {
|
|
||||||
let goal_lines: Vec<String> = active_goals.iter().enumerate().map(|(i, g)| {
|
|
||||||
let trimmed = g.text.trim();
|
|
||||||
let truncated: String = trimmed.chars().take(GOAL_MAX_CHARS).collect();
|
|
||||||
let truncated = if truncated.chars().count() >= GOAL_MAX_CHARS {
|
|
||||||
format!("{}…", truncated)
|
|
||||||
} else {
|
|
||||||
truncated
|
|
||||||
};
|
|
||||||
format!("{}. {}", i + 1, truncated)
|
|
||||||
}).collect();
|
|
||||||
let goals_text = goal_lines.join("\n");
|
|
||||||
system_prompt = if GOAL_INJECT_BANNER {
|
|
||||||
format!(
|
|
||||||
"{}\n\n## 当前目标(全程锚定,所有动作须服务于它们)\n{}",
|
|
||||||
system_prompt, goals_text
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
format!("{}\n\n{}", system_prompt, goals_text)
|
|
||||||
};
|
|
||||||
tracing::info!(
|
|
||||||
conv_id = %conv_id,
|
|
||||||
count = goals.len(),
|
|
||||||
first_goal = %goals.first().map(|g| &g.text[..std::cmp::min(120, g.text.len())]).unwrap_or(""),
|
|
||||||
"[ai] G1 目标钉扎:已把 {} 个 pinned_goals 拼进 system_prompt",
|
|
||||||
goals.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// T5: WorkingContext L1 注入(L1a 常驻 + L1b 条件注入)
|
|
||||||
// 读取工作上下文,构建 L1 注入文本并拼入 system_prompt
|
|
||||||
{
|
|
||||||
let session = session_arc.lock().await;
|
|
||||||
if let Some(conv) = session.conv_read(&conv_id) {
|
|
||||||
let injection = conv.working_context.build_injection(3); // dirty_window=3
|
|
||||||
if !injection.is_empty() {
|
|
||||||
system_prompt = format!("{}\n\n## 工作上下文\n{}", system_prompt, injection);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// EnvSnapshot 环境感知:入口把当前平台的真实环境(OS/shell/工具版本)拼进 system_prompt 尾部。
|
|
||||||
//
|
//
|
||||||
// 治「LLM 跨平台命令幻觉」:LLM 训练数据 Unix 多,易生成 macOS/Linux 语法命令(PowerShell 5
|
// 入口 system_prompt 五段拼接(G1 目标钉扎 / T5 工作上下文 / Env+behavior / 技能清单 /
|
||||||
// 不支持 `&&`、Windows 路径分隔符 `\`、GBK 终端中文乱码),把真实环境塞 prompt 即可锚定
|
// T4 工作流 DAG)收敛至 build_runtime_prompt helper:一次锁读 per_conv 三字段
|
||||||
// 输出平台一致性。detect() 是 OnceLock 全局缓存(启动时探一次,后续零开销),与 G1 一样
|
// (pinned_goals / working_context / workflow_dag_summary)消除原 G1/T5/T4 三处独立 lock,
|
||||||
// 是 loop 不变量(整个会话不重探),与目标钉扎拼接次序无强约束(放其后,语义自然)。
|
// 且返回 pinned_goals 复用作 AiCompleted 快照(消除原 L1157 二次读)。拼接顺序+内容逐字节等价。
|
||||||
let env_prompt = df_execute::EnvSnapshot::detect().await.to_prompt();
|
let (system_prompt, pinned_goals_snapshot) = build_runtime_prompt(
|
||||||
let behavior_prompt = concat!(
|
&session_arc, &conv_id, system_prompt, "",
|
||||||
"\n## AI 定位\n",
|
).await;
|
||||||
"你是 DevFlow 的 AI 助手,拥有完整的工具链。用户只负责提需求和审批,",
|
|
||||||
"所有执行由你完成——读写文件、运行命令、创建项目、搜索代码等都是你直接调用工具完成的。",
|
|
||||||
"**绝不输出请在终端执行以下命令这类指令——你自己用 run_command 工具执行即可。**\n",
|
|
||||||
"\n## 行为准则\n",
|
|
||||||
"- 所有操作都通过工具完成,用户不参与执行\n",
|
|
||||||
"- 优先使用开发工具 IPC,非必要不写独立脚本\n",
|
|
||||||
"- 脚本需要审批通过才执行,会拖慢工作流\n",
|
|
||||||
"- 已有 40+ 工具覆盖绝大多数场景,先查工具列表再决定\n",
|
|
||||||
"- 如果现有工具无法完成任务,告知用户缺少什么能力,建议向 DevFlow 反馈以开发新工具\n",
|
|
||||||
"- **直接回答用户问题,不要以评论开头**——严禁用好问题等无信息量的开场白,直接输出答案或执行操作\n",
|
|
||||||
"- **及时收敛**:工具执行完目标达成后立即输出结果,不要继续调工具做无关操作",
|
|
||||||
);
|
|
||||||
system_prompt = format!("{}\n\n{}\n\n{}", system_prompt, env_prompt, behavior_prompt);
|
|
||||||
|
|
||||||
// 技能清单注入:把本机扫描到的技能(名称+描述)追加到 system_prompt,让 LLM 感知可用技能。
|
|
||||||
//
|
|
||||||
// 治「AI 不知道有技能可引导」:skills.rs 联想已做(前端 / 浮层),但 AI 本身对技能无感知,
|
|
||||||
// 用户需求命中某技能(如"发布flux")时 AI 不会引导。机制化——只注入清单(不执行技能逻辑,
|
|
||||||
// 执行仍由用户在 Claude Code 侧 / 技能名 触发),LLM 据此提示用户可用技能。
|
|
||||||
// 清单来自 skills_cached() 进程内缓存(懒扫描一次,后续零开销);截断条数+描述防占预算。
|
|
||||||
if SKILL_LIST_INJECT_ENABLED {
|
|
||||||
let skills = crate::commands::ai::skills::skills_cached().await;
|
|
||||||
if !skills.is_empty() {
|
|
||||||
let lines: Vec<String> = skills
|
|
||||||
.iter()
|
|
||||||
.take(SKILL_LIST_MAX_ITEMS)
|
|
||||||
.map(|s| {
|
|
||||||
let desc_flat = s.description.replace('\n', " ");
|
|
||||||
let desc: String = desc_flat.chars().take(SKILL_DESC_MAX_CHARS).collect();
|
|
||||||
let desc = if desc_flat.chars().count() > SKILL_DESC_MAX_CHARS {
|
|
||||||
format!("{}…", desc)
|
|
||||||
} else {
|
|
||||||
desc
|
|
||||||
};
|
|
||||||
format!("- {}: {}", s.name, desc)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
if !lines.is_empty() {
|
|
||||||
system_prompt = format!(
|
|
||||||
"{}\n\n## 可用技能\n以下为本机 Claude 技能/命令(名称: 描述)。用户请求命中某技能用途时,提示用户输入 /技能名 触发,或引导其按需使用:\n{}",
|
|
||||||
system_prompt,
|
|
||||||
lines.join("\n")
|
|
||||||
);
|
|
||||||
tracing::debug!(
|
|
||||||
conv_id = %conv_id,
|
|
||||||
count = lines.len(),
|
|
||||||
"[ai] 技能清单注入:已追加 {} 条技能到 system_prompt",
|
|
||||||
lines.len()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// T4: 工作流 DAG 注入 — 当会话关联工作流时,将活跃路径注入 system prompt
|
|
||||||
{
|
|
||||||
let session = session_arc.lock().await;
|
|
||||||
if let Some(conv) = session.conv_read(&conv_id) {
|
|
||||||
if let Some(ref dag_summary) = conv.workflow_dag_summary {
|
|
||||||
system_prompt = format!("{}[工作流]\n{}\n", system_prompt, dag_summary);
|
|
||||||
tracing::debug!(
|
|
||||||
conv_id = %conv_id,
|
|
||||||
workflow_id = ?conv.workflow_id,
|
|
||||||
"[ai] T4: 已注入工作流 DAG 上下文"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 多 Agent 并行执行:Coordinator 分解(plan_execution_enabled 时) ──
|
|
||||||
// 对话透明化 L1:拍快照供 AiCompleted 事件携带(coordinator 路径出口也用)
|
|
||||||
// 只取 text(前端不需要状态信息)
|
|
||||||
let pinned_goals_snapshot: Vec<super::GoalEntry> = {
|
|
||||||
let session = session_arc.lock().await;
|
|
||||||
session
|
|
||||||
.conv_read(&conv_id)
|
|
||||||
.map(|c| c.pinned_goals.clone())
|
|
||||||
.unwrap_or_default()
|
|
||||||
};
|
|
||||||
|
|
||||||
// ── Plan-driven Phase 1:LLM 规划端(AICHAT_PLAN_ENABLED 门控) ──
|
// ── Plan-driven Phase 1:LLM 规划端(AICHAT_PLAN_ENABLED 门控) ──
|
||||||
//
|
//
|
||||||
@@ -1312,14 +1180,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
// 落库 + generating 复位 + emit AiCompleted(统一走 finish_round_exit,行为零变更)
|
// 落库 + generating 复位 + emit AiCompleted(统一走 finish_round_exit,行为零变更)
|
||||||
// Coordinator 路径:save(None,None) + 不 spawn_title + emit(None,None,publish=true)
|
// Coordinator 路径:save(None,None) + 不 spawn_title + emit(None,None,publish=true)
|
||||||
// emit_usage=tokens 快照(本路径无 round, tokens 为空, total=0 对齐原 tokens.total())
|
// emit_usage=tokens 快照(本路径无 round, tokens 为空, total=0 对齐原 tokens.total())
|
||||||
let coord_usage = df_ai::provider::TokenUsage {
|
let coord_usage = tokens_snapshot(&tokens);
|
||||||
prompt_tokens: tokens.prompt(),
|
|
||||||
completion_tokens: tokens.completion(),
|
|
||||||
total_tokens: tokens.total(),
|
|
||||||
prompt_cache_hit_tokens: tokens.cache_hit(),
|
|
||||||
prompt_cache_miss_tokens: tokens.cache_miss(),
|
|
||||||
reasoning_tokens: tokens.reasoning(),
|
|
||||||
};
|
|
||||||
finish_round_exit(
|
finish_round_exit(
|
||||||
&session_arc, &db, &conv_id,
|
&session_arc, &db, &conv_id,
|
||||||
None, None,
|
None, None,
|
||||||
@@ -1364,14 +1225,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
if stop_flag.load(Ordering::SeqCst) || loop_epoch_arc.load(Ordering::SeqCst) != my_epoch {
|
if stop_flag.load(Ordering::SeqCst) || loop_epoch_arc.load(Ordering::SeqCst) != my_epoch {
|
||||||
// save_usage 增量(自上次 save 后新增,首轮即停为 0);emit_usage 累计快照
|
// save_usage 增量(自上次 save 后新增,首轮即停为 0);emit_usage 累计快照
|
||||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||||
let emit_usage = df_ai::provider::TokenUsage {
|
let emit_usage = tokens_snapshot(&tokens);
|
||||||
prompt_tokens: tokens.prompt(),
|
|
||||||
completion_tokens: tokens.completion(),
|
|
||||||
total_tokens: tokens.total(),
|
|
||||||
prompt_cache_hit_tokens: tokens.cache_hit(),
|
|
||||||
prompt_cache_miss_tokens: tokens.cache_miss(),
|
|
||||||
reasoning_tokens: tokens.reasoning(),
|
|
||||||
};
|
|
||||||
// 入口 stop:本轮可能尚未 stream(首轮即停),不记 model——避免把未实际生成的 model 写入 models 数组
|
// 入口 stop:本轮可能尚未 stream(首轮即停),不记 model——避免把未实际生成的 model 写入 models 数组
|
||||||
// 统一走 finish_round_exit:save(Some usage, None model) + spawn_title + emit(None,None,publish=true)
|
// 统一走 finish_round_exit:save(Some usage, None model) + spawn_title + emit(None,None,publish=true)
|
||||||
// (stale 时 finish_round_exit 内部仅 disarm 跳过 save/reset/emit,不干扰新 loop)
|
// (stale 时 finish_round_exit 内部仅 disarm 跳过 save/reset/emit,不干扰新 loop)
|
||||||
@@ -1884,14 +1738,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
// MidStream 分叉:emit_incomplete=Some(true)(前端标不完整),publish_incomplete=None(总线消费方),
|
// MidStream 分叉:emit_incomplete=Some(true)(前端标不完整),publish_incomplete=None(总线消费方),
|
||||||
// do_publish=true(publish 走总线)。spawn_title=true(后台标题,失败 extract 兜底)。
|
// do_publish=true(publish 走总线)。spawn_title=true(后台标题,失败 extract 兜底)。
|
||||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||||
let emit_usage = df_ai::provider::TokenUsage {
|
let emit_usage = tokens_snapshot(&tokens);
|
||||||
prompt_tokens: tokens.prompt(),
|
|
||||||
completion_tokens: tokens.completion(),
|
|
||||||
total_tokens: tokens.total(),
|
|
||||||
prompt_cache_hit_tokens: tokens.cache_hit(),
|
|
||||||
prompt_cache_miss_tokens: tokens.cache_miss(),
|
|
||||||
reasoning_tokens: tokens.reasoning(),
|
|
||||||
};
|
|
||||||
finish_round_exit(
|
finish_round_exit(
|
||||||
&session_arc, &db, &conv_id,
|
&session_arc, &db, &conv_id,
|
||||||
Some(&save_usage), Some(&resolved_model),
|
Some(&save_usage), Some(&resolved_model),
|
||||||
@@ -1968,14 +1815,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
if stop_flag.load(Ordering::SeqCst) || loop_epoch_arc.load(Ordering::SeqCst) != my_epoch {
|
if stop_flag.load(Ordering::SeqCst) || loop_epoch_arc.load(Ordering::SeqCst) != my_epoch {
|
||||||
// save_usage 增量(自上次 save 后新增);emit_usage 累计快照(前端展示本 loop 总量)
|
// save_usage 增量(自上次 save 后新增);emit_usage 累计快照(前端展示本 loop 总量)
|
||||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||||
let emit_usage = df_ai::provider::TokenUsage {
|
let emit_usage = tokens_snapshot(&tokens);
|
||||||
prompt_tokens: tokens.prompt(),
|
|
||||||
completion_tokens: tokens.completion(),
|
|
||||||
total_tokens: tokens.total(),
|
|
||||||
prompt_cache_hit_tokens: tokens.cache_hit(),
|
|
||||||
prompt_cache_miss_tokens: tokens.cache_miss(),
|
|
||||||
reasoning_tokens: tokens.reasoning(),
|
|
||||||
};
|
|
||||||
// 统一走 finish_round_exit:save(Some usage, Some model) + spawn_title + emit(None,None,publish=true)
|
// 统一走 finish_round_exit:save(Some usage, Some model) + spawn_title + emit(None,None,publish=true)
|
||||||
finish_round_exit(
|
finish_round_exit(
|
||||||
&session_arc, &db, &conv_id,
|
&session_arc, &db, &conv_id,
|
||||||
@@ -2094,14 +1934,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
);
|
);
|
||||||
// save_usage 增量;emit_usage 累计快照(前端展示本 loop 总量)
|
// save_usage 增量;emit_usage 累计快照(前端展示本 loop 总量)
|
||||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||||
let emit_usage = df_ai::provider::TokenUsage {
|
let emit_usage = tokens_snapshot(&tokens);
|
||||||
prompt_tokens: tokens.prompt(),
|
|
||||||
completion_tokens: tokens.completion(),
|
|
||||||
total_tokens: tokens.total(),
|
|
||||||
prompt_cache_hit_tokens: tokens.cache_hit(),
|
|
||||||
prompt_cache_miss_tokens: tokens.cache_miss(),
|
|
||||||
reasoning_tokens: tokens.reasoning(),
|
|
||||||
};
|
|
||||||
// 统一走 finish_round_exit:save(Some usage, Some model) + 不 spawn_title(对齐原无 title) +
|
// 统一走 finish_round_exit:save(Some usage, Some model) + 不 spawn_title(对齐原无 title) +
|
||||||
// emit(Some(true), publish=false)。**do_publish=false 保留原 max_iterations 不 publish 行为**
|
// emit(Some(true), publish=false)。**do_publish=false 保留原 max_iterations 不 publish 行为**
|
||||||
// (与其他 5 路径不一致是历史现状,本次仅收敛重复代码不改 publish 策略,语义零变更)。
|
// (与其他 5 路径不一致是历史现状,本次仅收敛重复代码不改 publish 策略,语义零变更)。
|
||||||
@@ -2131,7 +1964,6 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
// save_usage 增量(自上次 save 后新增,正常收敛时上轮 save 已落,此处通常为 0);
|
// save_usage 增量(自上次 save 后新增,正常收敛时上轮 save 已落,此处通常为 0);
|
||||||
// emit_usage 用 tokens 累计快照。normal_usage.total 直接取 tokens.total() 保持三字段一致。
|
// emit_usage 用 tokens 累计快照。normal_usage.total 直接取 tokens.total() 保持三字段一致。
|
||||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||||
let usage_total = tokens.total();
|
|
||||||
{
|
{
|
||||||
let session_arc = session_arc.clone();
|
let session_arc = session_arc.clone();
|
||||||
let db = db.clone();
|
let db = db.clone();
|
||||||
@@ -2161,14 +1993,7 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
// generating 复位后再 emit Completed:落库/标题/提炼已在后台,前端立即感知完成
|
// generating 复位后再 emit Completed:落库/标题/提炼已在后台,前端立即感知完成
|
||||||
// (正常完成路径 save+extract+title 已在上方 spawn 异步,此处仅 emit,故直接调 emit_ai_completed_once)
|
// (正常完成路径 save+extract+title 已在上方 spawn 异步,此处仅 emit,故直接调 emit_ai_completed_once)
|
||||||
// emit_usage=tokens 快照(prompt/completion/total 全从 tokens 取,对齐原 usage_total=tokens.total())
|
// emit_usage=tokens 快照(prompt/completion/total 全从 tokens 取,对齐原 usage_total=tokens.total())
|
||||||
let normal_usage = df_ai::provider::TokenUsage {
|
let normal_usage = tokens_snapshot(&tokens);
|
||||||
prompt_tokens: tokens.prompt(),
|
|
||||||
completion_tokens: tokens.completion(),
|
|
||||||
total_tokens: usage_total,
|
|
||||||
prompt_cache_hit_tokens: tokens.cache_hit(),
|
|
||||||
prompt_cache_miss_tokens: tokens.cache_miss(),
|
|
||||||
reasoning_tokens: tokens.reasoning(),
|
|
||||||
};
|
|
||||||
// F1 并发 epoch:emit 前最后一次 stale 检查(guard.reset 已幂等,此处防 emit 重复 AiCompleted)。
|
// F1 并发 epoch:emit 前最后一次 stale 检查(guard.reset 已幂等,此处防 emit 重复 AiCompleted)。
|
||||||
if loop_epoch_arc.load(Ordering::SeqCst) != my_epoch {
|
if loop_epoch_arc.load(Ordering::SeqCst) != my_epoch {
|
||||||
return;
|
return;
|
||||||
@@ -2704,6 +2529,165 @@ async fn emit_ai_completed_once(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── build_runtime_prompt: run_agentic_loop 入口 system_prompt 五段拼接 ──
|
||||||
|
//
|
||||||
|
// 抽自原 L1024-1152(G1 目标钉扎 / T5 工作上下文 / Env+behavior / 技能清单 / T4 工作流 DAG)。
|
||||||
|
// **行为逐字节等价**:拼接顺序与内容不变,开关(GOAL_PIN_ENABLED / SKILL_LIST_INJECT_ENABLED /
|
||||||
|
// GOAL_INJECT_BANNER)与截断(GOAL_MAX_CHARS / SKILL_LIST_MAX_ITEMS / SKILL_DESC_MAX_CHARS)常量原样引用。
|
||||||
|
//
|
||||||
|
// **关键重构(消除 pinned_goals 二次读)**:原代码 G1/T5/T4 各自独立加锁读 per_conv,且
|
||||||
|
// pinned_goals 被 G1(L1026 拼接)与调用方 snapshot(L1157 AiCompleted 携带)读两遍。本 helper
|
||||||
|
// 一次锁内取 pinned_goals / working_context.build_injection / workflow_dag_summary / workflow_id,
|
||||||
|
// 释放锁后再做 Env(.await OnceLock)/技能(.await 进程缓存)拼接——后两者无锁需求,不在锁内 await
|
||||||
|
// (避免持锁 await 死锁/长持锁)。返回 (拼好的 system_prompt, pinned_goals),调用方复用 pinned_goals
|
||||||
|
// 做 snapshot,消除原 L1157 第二次 lock 读。
|
||||||
|
//
|
||||||
|
// 注:`_lang` 参数为未来 i18n 行为准则预留(当前 system_prompt 拼接未用 lang),保持调用点签名稳定。
|
||||||
|
async fn build_runtime_prompt(
|
||||||
|
session_arc: &Arc<Mutex<AiSession>>,
|
||||||
|
conv_id: &str,
|
||||||
|
base_prompt: String,
|
||||||
|
_lang: &str,
|
||||||
|
) -> (String, Vec<super::GoalEntry>) {
|
||||||
|
// 一次锁读四字段(pinned_goals / working_injection / workflow_dag_summary / workflow_id)。
|
||||||
|
// conv 已删(None)→ goals=空 / injection=空 / dag=None,等价原各块 conv_read.unwrap_or_default 短路。
|
||||||
|
let (goals, working_injection, workflow_dag_summary, workflow_id) = {
|
||||||
|
let session = session_arc.lock().await;
|
||||||
|
match session.conv_read(conv_id) {
|
||||||
|
Some(conv) => (
|
||||||
|
conv.pinned_goals.clone(),
|
||||||
|
conv.working_context.build_injection(3),
|
||||||
|
conv.workflow_dag_summary.clone(),
|
||||||
|
conv.workflow_id.clone(),
|
||||||
|
),
|
||||||
|
None => (Vec::new(), String::new(), None, None),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut system_prompt = base_prompt;
|
||||||
|
|
||||||
|
// G1 目标钉扎:仅 active 目标拼进 system_prompt(completed 不干扰 LLM 注意力)。
|
||||||
|
if GOAL_PIN_ENABLED {
|
||||||
|
let active_goals: Vec<&super::GoalEntry> = goals.iter().filter(|g| matches!(g.status, super::GoalStatus::Active)).collect();
|
||||||
|
if !active_goals.is_empty() {
|
||||||
|
let goal_lines: Vec<String> = active_goals.iter().enumerate().map(|(i, g)| {
|
||||||
|
let trimmed = g.text.trim();
|
||||||
|
let truncated: String = trimmed.chars().take(GOAL_MAX_CHARS).collect();
|
||||||
|
let truncated = if truncated.chars().count() >= GOAL_MAX_CHARS {
|
||||||
|
format!("{}…", truncated)
|
||||||
|
} else {
|
||||||
|
truncated
|
||||||
|
};
|
||||||
|
format!("{}. {}", i + 1, truncated)
|
||||||
|
}).collect();
|
||||||
|
let goals_text = goal_lines.join("\n");
|
||||||
|
system_prompt = if GOAL_INJECT_BANNER {
|
||||||
|
format!(
|
||||||
|
"{}\n\n## 当前目标(全程锚定,所有动作须服务于它们)\n{}",
|
||||||
|
system_prompt, goals_text
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
format!("{}\n\n{}", system_prompt, goals_text)
|
||||||
|
};
|
||||||
|
tracing::info!(
|
||||||
|
conv_id = %conv_id,
|
||||||
|
count = goals.len(),
|
||||||
|
first_goal = %goals.first().map(|g| &g.text[..std::cmp::min(120, g.text.len())]).unwrap_or(""),
|
||||||
|
"[ai] G1 目标钉扎:已把 {} 个 pinned_goals 拼进 system_prompt",
|
||||||
|
goals.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// T5: WorkingContext L1 注入(dirty_window=3,空 injection 跳过)。
|
||||||
|
if !working_injection.is_empty() {
|
||||||
|
system_prompt = format!("{}\n\n## 工作上下文\n{}", system_prompt, working_injection);
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnvSnapshot 环境感知:detect() 是 OnceLock 全局缓存(启动探一次,后续零开销),与 G1 一样是
|
||||||
|
// loop 不变量,与目标钉扎拼接次序无强约束(放其后,语义自然)。无锁需求。
|
||||||
|
let env_prompt = df_execute::EnvSnapshot::detect().await.to_prompt();
|
||||||
|
let behavior_prompt = concat!(
|
||||||
|
"\n## AI 定位\n",
|
||||||
|
"你是 DevFlow 的 AI 助手,拥有完整的工具链。用户只负责提需求和审批,",
|
||||||
|
"所有执行由你完成——读写文件、运行命令、创建项目、搜索代码等都是你直接调用工具完成的。",
|
||||||
|
"**绝不输出请在终端执行以下命令这类指令——你自己用 run_command 工具执行即可。**\n",
|
||||||
|
"\n## 行为准则\n",
|
||||||
|
"- 所有操作都通过工具完成,用户不参与执行\n",
|
||||||
|
"- 优先使用开发工具 IPC,非必要不写独立脚本\n",
|
||||||
|
"- 脚本需要审批通过才执行,会拖慢工作流\n",
|
||||||
|
"- 已有 40+ 工具覆盖绝大多数场景,先查工具列表再决定\n",
|
||||||
|
"- 如果现有工具无法完成任务,告知用户缺少什么能力,建议向 DevFlow 反馈以开发新工具\n",
|
||||||
|
"- **直接回答用户问题,不要以评论开头**——严禁用好问题等无信息量的开场白,直接输出答案或执行操作\n",
|
||||||
|
"- **及时收敛**:工具执行完目标达成后立即输出结果,不要继续调工具做无关操作",
|
||||||
|
);
|
||||||
|
system_prompt = format!("{}\n\n{}\n\n{}", system_prompt, env_prompt, behavior_prompt);
|
||||||
|
|
||||||
|
// 技能清单注入:skills_cached() 进程内缓存(懒扫描一次,后续零开销),无锁需求。
|
||||||
|
if SKILL_LIST_INJECT_ENABLED {
|
||||||
|
let skills = crate::commands::ai::skills::skills_cached().await;
|
||||||
|
if !skills.is_empty() {
|
||||||
|
let lines: Vec<String> = skills
|
||||||
|
.iter()
|
||||||
|
.take(SKILL_LIST_MAX_ITEMS)
|
||||||
|
.map(|s| {
|
||||||
|
let desc_flat = s.description.replace('\n', " ");
|
||||||
|
let desc: String = desc_flat.chars().take(SKILL_DESC_MAX_CHARS).collect();
|
||||||
|
let desc = if desc_flat.chars().count() > SKILL_DESC_MAX_CHARS {
|
||||||
|
format!("{}…", desc)
|
||||||
|
} else {
|
||||||
|
desc
|
||||||
|
};
|
||||||
|
format!("- {}: {}", s.name, desc)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
if !lines.is_empty() {
|
||||||
|
system_prompt = format!(
|
||||||
|
"{}\n\n## 可用技能\n以下为本机 Claude 技能/命令(名称: 描述)。用户请求命中某技能用途时,提示用户输入 /技能名 触发,或引导其按需使用:\n{}",
|
||||||
|
system_prompt,
|
||||||
|
lines.join("\n")
|
||||||
|
);
|
||||||
|
tracing::debug!(
|
||||||
|
conv_id = %conv_id,
|
||||||
|
count = lines.len(),
|
||||||
|
"[ai] 技能清单注入:已追加 {} 条技能到 system_prompt",
|
||||||
|
lines.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// T4: 工作流 DAG 注入 — 当会话关联工作流时,将活跃路径注入 system prompt。
|
||||||
|
if let Some(ref dag_summary) = workflow_dag_summary {
|
||||||
|
system_prompt = format!("{}[工作流]\n{}\n", system_prompt, dag_summary);
|
||||||
|
tracing::debug!(
|
||||||
|
conv_id = %conv_id,
|
||||||
|
workflow_id = ?workflow_id,
|
||||||
|
"[ai] T4: 已注入工作流 DAG 上下文"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
(system_prompt, goals)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── tokens_snapshot: TokenAccumulator → TokenUsage 累计快照 helper ──
|
||||||
|
//
|
||||||
|
// 抽自 run_agentic_loop 内 6 处(Coordinator 路径/入口 stop/MidStream/push 后 stop/
|
||||||
|
// max_iterations/正常完成)重复的 TokenUsage{prompt/completion/total/cache_hit/cache_miss/
|
||||||
|
// reasoning} 字面量组装。total = tokens.total() = prompt.saturating_add(completion)
|
||||||
|
// (TokenAccumulator::total 语义),与原 6 处逐字节等价。usage_delta_since 内部「取 cur 快照」
|
||||||
|
// 也复用本 helper(cur 字段同构)。
|
||||||
|
fn tokens_snapshot(tokens: &TokenAccumulator) -> df_ai::provider::TokenUsage {
|
||||||
|
df_ai::provider::TokenUsage {
|
||||||
|
prompt_tokens: tokens.prompt(),
|
||||||
|
completion_tokens: tokens.completion(),
|
||||||
|
total_tokens: tokens.total(),
|
||||||
|
prompt_cache_hit_tokens: tokens.cache_hit(),
|
||||||
|
prompt_cache_miss_tokens: tokens.cache_miss(),
|
||||||
|
reasoning_tokens: tokens.reasoning(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── usage_delta_since: 会话级 token 落库「增量」口径 helper ──
|
// ── usage_delta_since: 会话级 token 落库「增量」口径 helper ──
|
||||||
//
|
//
|
||||||
// save_conversation 对 usage 做 old+add(accumulate_tokens),故每次 save 只能传
|
// save_conversation 对 usage 做 old+add(accumulate_tokens),故每次 save 只能传
|
||||||
@@ -2714,14 +2698,7 @@ fn usage_delta_since(
|
|||||||
tokens: &TokenAccumulator,
|
tokens: &TokenAccumulator,
|
||||||
last_saved: &mut df_ai::provider::TokenUsage,
|
last_saved: &mut df_ai::provider::TokenUsage,
|
||||||
) -> df_ai::provider::TokenUsage {
|
) -> df_ai::provider::TokenUsage {
|
||||||
let cur = df_ai::provider::TokenUsage {
|
let cur = tokens_snapshot(tokens);
|
||||||
prompt_tokens: tokens.prompt(),
|
|
||||||
completion_tokens: tokens.completion(),
|
|
||||||
total_tokens: tokens.total(),
|
|
||||||
prompt_cache_hit_tokens: tokens.cache_hit(),
|
|
||||||
prompt_cache_miss_tokens: tokens.cache_miss(),
|
|
||||||
reasoning_tokens: tokens.reasoning(),
|
|
||||||
};
|
|
||||||
let delta = df_ai::provider::TokenUsage {
|
let delta = df_ai::provider::TokenUsage {
|
||||||
prompt_tokens: cur.prompt_tokens.saturating_sub(last_saved.prompt_tokens),
|
prompt_tokens: cur.prompt_tokens.saturating_sub(last_saved.prompt_tokens),
|
||||||
completion_tokens: cur.completion_tokens.saturating_sub(last_saved.completion_tokens),
|
completion_tokens: cur.completion_tokens.saturating_sub(last_saved.completion_tokens),
|
||||||
|
|||||||
@@ -292,6 +292,16 @@ pub async fn run_workflow_inner(
|
|||||||
.lock()
|
.lock()
|
||||||
.await
|
.await
|
||||||
.insert(exec_id.clone(), executor.state_machine());
|
.insert(exec_id.clone(), executor.state_machine());
|
||||||
|
// P0-A: 把 task_id 注入 config,使 AiSelfReviewNode(ctx.config.get("task_id")) 与
|
||||||
|
// AiNode parse_params 兜底(ctx.config.get("task_id") 读 task 生成默认 prompt) 可读。
|
||||||
|
// config 是 serde_json::Value,仅 object 时 insert 安全;task_id None 跳过(向后兼容)。
|
||||||
|
// executor.run 之前的 config 是 move 进 run 的,此处 mutate 后整体 move,零额外开销。
|
||||||
|
let mut config = config;
|
||||||
|
if let Some(tid) = cb_task_id.as_ref() {
|
||||||
|
if let Some(obj) = config.as_object_mut() {
|
||||||
|
obj.insert("task_id".to_string(), serde_json::Value::String(tid.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
let result = executor.run(&runtime_dag, config).await;
|
let result = executor.run(&runtime_dag, config).await;
|
||||||
|
|
||||||
let (status, error) = match &result {
|
let (status, error) = match &result {
|
||||||
|
|||||||
Reference in New Issue
Block a user