修复: 工作流推进链路与执行入口优化
This commit is contained in:
@@ -13,7 +13,7 @@ use df_ai::provider::LlmProvider;
|
||||
// AiNode 路由 — 节点 config.model_id 优先;否则按 TaskRequirements 路由
|
||||
// (默认 Standard + needs_tool_use=true)。池空/无匹配兜底 record.default_model。
|
||||
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::models::AiProviderRecord;
|
||||
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>,
|
||||
) -> anyhow::Result<AiNodeParams> {
|
||||
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 逐字重复)。
|
||||
|
||||
@@ -1021,146 +1021,14 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 不再从用户消息规则提取,也不依赖 LLM 输出结构化标记。
|
||||
// 推理结果存入 pinned_goals: Vec<GoalEntry>,含 text+status 状态追踪。
|
||||
// 新目标加入时自动标记之前的 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
|
||||
// 不支持 `&&`、Windows 路径分隔符 `\`、GBK 终端中文乱码),把真实环境塞 prompt 即可锚定
|
||||
// 输出平台一致性。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);
|
||||
|
||||
// 技能清单注入:把本机扫描到的技能(名称+描述)追加到 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()
|
||||
};
|
||||
// 入口 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(
|
||||
&session_arc, &conv_id, system_prompt, "",
|
||||
).await;
|
||||
|
||||
// ── 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,行为零变更)
|
||||
// Coordinator 路径:save(None,None) + 不 spawn_title + emit(None,None,publish=true)
|
||||
// emit_usage=tokens 快照(本路径无 round, tokens 为空, total=0 对齐原 tokens.total())
|
||||
let coord_usage = 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(),
|
||||
};
|
||||
let coord_usage = tokens_snapshot(&tokens);
|
||||
finish_round_exit(
|
||||
&session_arc, &db, &conv_id,
|
||||
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 {
|
||||
// save_usage 增量(自上次 save 后新增,首轮即停为 0);emit_usage 累计快照
|
||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||
let emit_usage = 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(),
|
||||
};
|
||||
let emit_usage = tokens_snapshot(&tokens);
|
||||
// 入口 stop:本轮可能尚未 stream(首轮即停),不记 model——避免把未实际生成的 model 写入 models 数组
|
||||
// 统一走 finish_round_exit:save(Some usage, None model) + spawn_title + emit(None,None,publish=true)
|
||||
// (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(总线消费方),
|
||||
// do_publish=true(publish 走总线)。spawn_title=true(后台标题,失败 extract 兜底)。
|
||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||
let emit_usage = 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(),
|
||||
};
|
||||
let emit_usage = tokens_snapshot(&tokens);
|
||||
finish_round_exit(
|
||||
&session_arc, &db, &conv_id,
|
||||
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 {
|
||||
// save_usage 增量(自上次 save 后新增);emit_usage 累计快照(前端展示本 loop 总量)
|
||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||
let emit_usage = 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(),
|
||||
};
|
||||
let emit_usage = tokens_snapshot(&tokens);
|
||||
// 统一走 finish_round_exit:save(Some usage, Some model) + spawn_title + emit(None,None,publish=true)
|
||||
finish_round_exit(
|
||||
&session_arc, &db, &conv_id,
|
||||
@@ -2094,14 +1934,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
);
|
||||
// save_usage 增量;emit_usage 累计快照(前端展示本 loop 总量)
|
||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||
let emit_usage = 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(),
|
||||
};
|
||||
let emit_usage = tokens_snapshot(&tokens);
|
||||
// 统一走 finish_round_exit:save(Some usage, Some model) + 不 spawn_title(对齐原无 title) +
|
||||
// emit(Some(true), publish=false)。**do_publish=false 保留原 max_iterations 不 publish 行为**
|
||||
// (与其他 5 路径不一致是历史现状,本次仅收敛重复代码不改 publish 策略,语义零变更)。
|
||||
@@ -2131,7 +1964,6 @@ pub(crate) async fn run_agentic_loop(
|
||||
// save_usage 增量(自上次 save 后新增,正常收敛时上轮 save 已落,此处通常为 0);
|
||||
// emit_usage 用 tokens 累计快照。normal_usage.total 直接取 tokens.total() 保持三字段一致。
|
||||
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
|
||||
let usage_total = tokens.total();
|
||||
{
|
||||
let session_arc = session_arc.clone();
|
||||
let db = db.clone();
|
||||
@@ -2161,14 +1993,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
// generating 复位后再 emit Completed:落库/标题/提炼已在后台,前端立即感知完成
|
||||
// (正常完成路径 save+extract+title 已在上方 spawn 异步,此处仅 emit,故直接调 emit_ai_completed_once)
|
||||
// emit_usage=tokens 快照(prompt/completion/total 全从 tokens 取,对齐原 usage_total=tokens.total())
|
||||
let normal_usage = df_ai::provider::TokenUsage {
|
||||
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(),
|
||||
};
|
||||
let normal_usage = tokens_snapshot(&tokens);
|
||||
// F1 并发 epoch:emit 前最后一次 stale 检查(guard.reset 已幂等,此处防 emit 重复 AiCompleted)。
|
||||
if loop_epoch_arc.load(Ordering::SeqCst) != my_epoch {
|
||||
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 ──
|
||||
//
|
||||
// save_conversation 对 usage 做 old+add(accumulate_tokens),故每次 save 只能传
|
||||
@@ -2714,14 +2698,7 @@ fn usage_delta_since(
|
||||
tokens: &TokenAccumulator,
|
||||
last_saved: &mut df_ai::provider::TokenUsage,
|
||||
) -> df_ai::provider::TokenUsage {
|
||||
let cur = 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(),
|
||||
};
|
||||
let cur = tokens_snapshot(tokens);
|
||||
let delta = df_ai::provider::TokenUsage {
|
||||
prompt_tokens: cur.prompt_tokens.saturating_sub(last_saved.prompt_tokens),
|
||||
completion_tokens: cur.completion_tokens.saturating_sub(last_saved.completion_tokens),
|
||||
|
||||
@@ -292,6 +292,16 @@ pub async fn run_workflow_inner(
|
||||
.lock()
|
||||
.await
|
||||
.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 (status, error) = match &result {
|
||||
|
||||
Reference in New Issue
Block a user