重构: agentic 架构(run_agentic_loop 抽 finish_round_exit + router 调用点 + provider_pool 亲和 enabled)
- run_agentic_loop: 抽 finish_round_exit + emit_ai_completed_once(6 退出点收敛) - compress/title/knowledge_inject/project: router estimated_context+tier 调用点对齐 - provider_pool: 亲和加 model.enabled 过滤
This commit is contained in:
@@ -21,7 +21,7 @@ use df_ai::context_helpers::{
|
||||
// B 路线 Phase 1:plan_hint 接入主 loop——filter_tool_defs_planned 在 filter_tool_defs
|
||||
// 收敛的扁平子集之上叠加 plan_hint 编排(并行组同批聚拢/顺序依赖源在前),供 LLM 看到
|
||||
// 一份按编排意图排序的工具列表。feature flag PLANNING_ENABLED(false 默认关)门控接入。
|
||||
use df_ai::intent::{filter_tool_defs, filter_tool_defs_planned, IntentRecognizer};
|
||||
use df_ai::intent::{filter_tool_defs, filter_tool_defs_planned, suggested_model_tier, IntentRecognizer};
|
||||
use df_ai::coordinator::{Coordinator, ExecutionResult};
|
||||
use df_ai::persona::PersonaRegistry;
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest, LlmProvider, MessageRole};
|
||||
@@ -736,10 +736,16 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 主对话路由 — TaskRequirements(needs_tool_use=true)。
|
||||
// 模态当前仅 Text(图像消息类型未实现,后续多模态接入时检测 Part/Image 追加 Vision)。
|
||||
// select_model_id None(池空/无匹配)→ 兜底 default_model,行为与接入前一致。
|
||||
//
|
||||
// **子项 1+2 根因修复**:此处是 pre-loop 初始路由(算 resolved_model 兜底用),estimated_context=0
|
||||
// (loop 内 messages 此处尚未构建,无法估值)+ tier=None(intent 在 739 行之后才识别)。
|
||||
// 真实 estimated_context + tier 在 loop 内(line ~1395,estimated_prompt 算出后)重建 agentic_req
|
||||
// shadow 此绑定,candidate chain 用 loop 内的真实估值版本(主路由路径)。
|
||||
let agentic_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: true,
|
||||
estimated_context: 0,
|
||||
tier: None,
|
||||
};
|
||||
let resolved_model = select_model_id(&agentic_req, &provider_config.model_configs)
|
||||
.unwrap_or_else(|| provider_config.default_model.clone());
|
||||
@@ -1122,29 +1128,25 @@ pub(crate) async fn run_agentic_loop(
|
||||
}
|
||||
}
|
||||
|
||||
// 落库
|
||||
save_conversation(&session_arc, &db, &conv_id, None, None, true).await;
|
||||
|
||||
// generating 复位并 emit AiCompleted(对齐现有退出路径模式)
|
||||
guard.reset().await;
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
||||
total_tokens: tokens.total(),
|
||||
// 落库 + 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(),
|
||||
incomplete: None,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
pinned_goals: pinned_goals_snapshot.clone(),
|
||||
});
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(
|
||||
AiChatEvent::AiCompleted {
|
||||
total_tokens: tokens.total(),
|
||||
prompt_tokens: tokens.prompt(),
|
||||
completion_tokens: tokens.completion(),
|
||||
incomplete: None,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
pinned_goals: pinned_goals_snapshot.clone(),
|
||||
}
|
||||
);
|
||||
total_tokens: tokens.total(),
|
||||
};
|
||||
finish_round_exit(
|
||||
&session_arc, &db, &conv_id,
|
||||
None, None,
|
||||
false,
|
||||
&provider_config, &llm_concurrency,
|
||||
&mut guard,
|
||||
&coord_usage,
|
||||
None, None, true,
|
||||
&pinned_goals_snapshot,
|
||||
&app_handle,
|
||||
).await;
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -1172,21 +1174,18 @@ pub(crate) async fn run_agentic_loop(
|
||||
total_tokens: tokens.total(),
|
||||
};
|
||||
// 入口 stop:本轮可能尚未 stream(首轮即停),不记 model——避免把未实际生成的 model 写入 models 数组
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), None, true).await;
|
||||
// 标题生成后台化:不阻塞 Completed emit(失败有 extract_title 兜底)
|
||||
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
|
||||
guard.reset().await;
|
||||
// generating 复位后再 emit Completed:保证前端收事件时后端已可接下一条(发送队列续发不被"正在生成中"拒绝)
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage.total_tokens, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()), pinned_goals: pinned_goals_snapshot.clone() });
|
||||
// L3 emit 双写:入口 stop 的 AiCompleted publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
||||
total_tokens: usage.total_tokens,
|
||||
prompt_tokens: tokens.prompt(),
|
||||
completion_tokens: tokens.completion(),
|
||||
incomplete: None,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
pinned_goals: pinned_goals_snapshot.clone(),
|
||||
});
|
||||
// 统一走 finish_round_exit:save(Some usage, None model) + spawn_title + emit(None,None,publish=true)
|
||||
finish_round_exit(
|
||||
&session_arc, &db, &conv_id,
|
||||
Some(&usage), None,
|
||||
true,
|
||||
&provider_config, &llm_concurrency,
|
||||
&mut guard,
|
||||
&usage,
|
||||
None, None, true,
|
||||
&pinned_goals_snapshot,
|
||||
&app_handle,
|
||||
).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1392,6 +1391,28 @@ pub(crate) async fn run_agentic_loop(
|
||||
messages.iter().map(|m| est.estimate_message(m)).sum()
|
||||
};
|
||||
|
||||
// 子项 1+2 根因修复:loop 内重建 agentic_req,真实 estimated_context + 意图 tier。
|
||||
//
|
||||
// **子项 1(estimated_context 死代码)**:原 pre-loop agentic_req 传 0,
|
||||
// 上下文窗口过滤维度(context_window >= estimated_context)恒过,失效。此处用本轮
|
||||
// estimated_prompt(system+history 全量 token 估值)作 estimated_context → 窗口过滤生效
|
||||
// (小窗口模型如 8K 被大上下文任务正确滤掉,不再误选)。
|
||||
//
|
||||
// **子项 2(weight tier tiebreak)**:tier 接 intent→ModelTier(Code/Debug→Heavy,
|
||||
// Chat→Fast 等),同 weight 候选间按 tier tiebreak(满足档位下限的候选胜,见 router.rs)。
|
||||
// intent 在 loop 外(786 行)已识别,loop 内每轮复用同一 intent(用户末条 active 消息
|
||||
// 在单轮 LLM 调用内不变;多轮对话 intent 演化由用户后续消息触发,下轮 recognize 更新)。
|
||||
//
|
||||
// **变量 shadow**:此绑定覆盖 pre-loop 的 agentic_req(line ~744 算初始 resolved_model
|
||||
// 用过),candidate chain(下方 stream_one_provider)取此 loop 内版本。Rust shadow 安全:
|
||||
// pre-loop 版本在 line 744 用完即弃,resolved_model 已落到 mut 变量。
|
||||
let agentic_req = TaskRequirements {
|
||||
modalities: vec![Modality::Text],
|
||||
needs_tool_use: true,
|
||||
estimated_context: estimated_prompt as usize,
|
||||
tier: suggested_model_tier(&intent),
|
||||
};
|
||||
|
||||
// LLM 并发限流:
|
||||
// per_conv 由 loop 入口(_conv_per_conv_permit)整 loop 持有(含工具执行/审批等待/重试),
|
||||
// 防单对话内并发 LLM 调用失控(单对话内 permits=2,非会话数限制)。
|
||||
@@ -1552,28 +1573,27 @@ pub(crate) async fn run_agentic_loop(
|
||||
}
|
||||
}
|
||||
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model), true).await;
|
||||
// 标题生成后台化(失败有 extract_title 兜底)
|
||||
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
|
||||
guard.reset().await;
|
||||
// generating 复位后再 emit Completed(incomplete=true):前端据此标记消息为不完整
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
||||
total_tokens: usage.total_tokens,
|
||||
// 统一走 finish_round_exit 收尾(save + spawn_title + reset + emit)。
|
||||
// 注意:partial 文本+系统提示已先 push(上方 block),此 save 落库含本轮 partial,幂等覆盖。
|
||||
// emit_usage 用 tokens 快照(tokens.add 已累加本轮):total_tokens/prompt/completion 对齐原 emit 三元组。
|
||||
// MidStream 分叉:emit_incomplete=Some(true)(前端标不完整),publish_incomplete=None(总线消费方),
|
||||
// do_publish=true(publish 走总线)。spawn_title=true(后台标题,失败 extract 兜底)。
|
||||
let emit_usage = df_ai::provider::TokenUsage {
|
||||
prompt_tokens: tokens.prompt(),
|
||||
completion_tokens: tokens.completion(),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
incomplete: Some(true),
|
||||
pinned_goals: pinned_goals_snapshot.clone(),
|
||||
});
|
||||
// L3 emit 双写:MidStream 保文 AiCompleted publish 到事件总线(门控在 publish 内)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
||||
total_tokens: usage.total_tokens,
|
||||
prompt_tokens: tokens.prompt(),
|
||||
completion_tokens: tokens.completion(),
|
||||
incomplete: None,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
pinned_goals: pinned_goals_snapshot.clone(),
|
||||
});
|
||||
};
|
||||
finish_round_exit(
|
||||
&session_arc, &db, &conv_id,
|
||||
Some(&usage), Some(&resolved_model),
|
||||
true,
|
||||
&provider_config, &llm_concurrency,
|
||||
&mut guard,
|
||||
&emit_usage,
|
||||
Some(true), None, true,
|
||||
&pinned_goals_snapshot,
|
||||
&app_handle,
|
||||
).await;
|
||||
return;
|
||||
}
|
||||
// global/per_conv permit 已上移 loop 入口整 loop 持有,
|
||||
@@ -1624,21 +1644,18 @@ pub(crate) async fn run_agentic_loop(
|
||||
completion_tokens: tokens.completion(),
|
||||
total_tokens: tokens.total(),
|
||||
};
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model), true).await;
|
||||
// 标题生成后台化:不阻塞 Completed emit(失败有 extract_title 兜底)
|
||||
spawn_ensure_title(&provider_config, &db, &conv_id, &app_handle, &session_arc, &llm_concurrency);
|
||||
guard.reset().await;
|
||||
// generating 复位后再 emit Completed:保证前端收事件时后端已可接下一条(发送队列续发不被"正在生成中"拒绝)
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage.total_tokens, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()), pinned_goals: pinned_goals_snapshot.clone() });
|
||||
// L3 emit 双写:stream 后 stop 的 AiCompleted publish 到事件总线(门控在 publish 内)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
||||
total_tokens: usage.total_tokens,
|
||||
prompt_tokens: tokens.prompt(),
|
||||
completion_tokens: tokens.completion(),
|
||||
incomplete: None,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
pinned_goals: pinned_goals_snapshot.clone(),
|
||||
});
|
||||
// 统一走 finish_round_exit:save(Some usage, Some model) + spawn_title + emit(None,None,publish=true)
|
||||
finish_round_exit(
|
||||
&session_arc, &db, &conv_id,
|
||||
Some(&usage), Some(&resolved_model),
|
||||
true,
|
||||
&provider_config, &llm_concurrency,
|
||||
&mut guard,
|
||||
&usage,
|
||||
None, None, true,
|
||||
&pinned_goals_snapshot,
|
||||
&app_handle,
|
||||
).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1725,16 +1742,20 @@ pub(crate) async fn run_agentic_loop(
|
||||
completion_tokens: tokens.completion(),
|
||||
total_tokens: tokens.total(),
|
||||
};
|
||||
save_conversation(&session_arc, &db, &conv_id, Some(&usage), Some(&resolved_model), true).await;
|
||||
guard.reset().await;
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted {
|
||||
total_tokens: usage.total_tokens,
|
||||
prompt_tokens: tokens.prompt(),
|
||||
completion_tokens: tokens.completion(),
|
||||
incomplete: Some(true),
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
pinned_goals: pinned_goals_snapshot.clone(),
|
||||
});
|
||||
// 统一走 finish_round_exit:save(Some usage, Some model) + 不 spawn_title(对齐原无 title) +
|
||||
// emit(Some(true), publish=false)。**do_publish=false 保留原 max_iterations 不 publish 行为**
|
||||
// (与其他 5 路径不一致是历史现状,本次仅收敛重复代码不改 publish 策略,语义零变更)。
|
||||
finish_round_exit(
|
||||
&session_arc, &db, &conv_id,
|
||||
Some(&usage), Some(&resolved_model),
|
||||
false,
|
||||
&provider_config, &llm_concurrency,
|
||||
&mut guard,
|
||||
&usage,
|
||||
Some(true), None, false,
|
||||
&pinned_goals_snapshot,
|
||||
&app_handle,
|
||||
).await;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1770,17 +1791,18 @@ pub(crate) async fn run_agentic_loop(
|
||||
|
||||
guard.reset().await;
|
||||
// generating 复位后再 emit Completed:落库/标题/提炼已在后台,前端立即感知完成
|
||||
let _ = app_handle.emit("ai-chat-event", AiChatEvent::AiCompleted { total_tokens: usage_total, prompt_tokens: tokens.prompt(), completion_tokens: tokens.completion(), incomplete: None, conversation_id: Some(conv_id.clone()), pinned_goals: pinned_goals_snapshot.clone() });
|
||||
// L3 emit 双写:正常完成 AiCompleted publish 到事件总线(EVENT_BUS_ENABLED 门控在 publish 内)。
|
||||
// AiTextDelta/AiToolCall* 高频事件不双写(无消费者空转)。
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(AiChatEvent::AiCompleted {
|
||||
total_tokens: usage_total,
|
||||
// (正常完成路径 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(),
|
||||
incomplete: None,
|
||||
conversation_id: Some(conv_id.clone()),
|
||||
pinned_goals: pinned_goals_snapshot.clone(),
|
||||
});
|
||||
total_tokens: usage_total,
|
||||
};
|
||||
emit_ai_completed_once(
|
||||
&app_handle, &conv_id, &normal_usage,
|
||||
None, None, true,
|
||||
&pinned_goals_snapshot,
|
||||
).await;
|
||||
}
|
||||
|
||||
// ── heartbeat_loop: 后台心跳任务(emit AiHeartbeat 防前端 watchdog 误杀) ──
|
||||
@@ -2192,6 +2214,112 @@ async fn emit_fatal_error(app_handle: &AppHandle, conv_id: &str, error: &str) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── emit_ai_completed_once: AiCompleted 事件 emit+publish 双写 helper(对齐 emit_fatal_error 模式) ──
|
||||
//
|
||||
// 抽自 6 处退出路径(stop_flag 入口 / Coordinator merge / MidStream 保文 / push 后 stop /
|
||||
// max_iterations / 正常完成)中「emit AiCompleted + publish_event AiCompleted」近重复代码。
|
||||
//
|
||||
// **incomplete 语义分叉**(memory: 语义不变):
|
||||
// MidStream 保文路径 emit 端 incomplete=Some(true)(前端据 标记不完整),publish 端 incomplete=None
|
||||
// (事件总线消费方无需此标记);max_iterations 路径仅 emit 不 publish(原实现即无 publish,保留)。
|
||||
// 其余 4 路径 emit/publish 的 incomplete 一致(均 None 或均 Some)。
|
||||
// 故本 helper 拆 emit_incomplete / publish_incomplete / do_publish 三参,精确镜像原各路径差异。
|
||||
//
|
||||
// usage 字段(对齐 run_agentic_loop 各退出点用法):emit 端的 token 三元组从 `usage` 取;
|
||||
// prompt/completion 与 usage 一致(各退出点原样从 tokens 或 round-derived usage 传入)。
|
||||
//
|
||||
// pinned_goals:前端直接读取刷新(G1 目标钉扎),原样透传快照(不 clone,借用调用方)。
|
||||
async fn emit_ai_completed_once(
|
||||
app_handle: &AppHandle,
|
||||
conv_id: &str,
|
||||
usage: &df_ai::provider::TokenUsage,
|
||||
emit_incomplete: Option<bool>,
|
||||
publish_incomplete: Option<bool>,
|
||||
do_publish: bool,
|
||||
pinned_goals: &[super::GoalEntry],
|
||||
) {
|
||||
// emit 端(前端通道):incomplete 用 emit_incomplete(MidStream 传 Some(true))。
|
||||
let _ = app_handle.emit(
|
||||
"ai-chat-event",
|
||||
AiChatEvent::AiCompleted {
|
||||
total_tokens: usage.total_tokens,
|
||||
prompt_tokens: usage.prompt_tokens,
|
||||
completion_tokens: usage.completion_tokens,
|
||||
incomplete: emit_incomplete,
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
pinned_goals: pinned_goals.to_vec(),
|
||||
},
|
||||
);
|
||||
// publish 端(事件总线):EVENT_BUS_ENABLED 门控在 publish 内;incomplete 用 publish_incomplete
|
||||
// (MidStream 传 None);do_publish=false(max_iterations)跳过整段 publish(保留原无 publish 行为)。
|
||||
if do_publish {
|
||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(
|
||||
AiChatEvent::AiCompleted {
|
||||
total_tokens: usage.total_tokens,
|
||||
prompt_tokens: usage.prompt_tokens,
|
||||
completion_tokens: usage.completion_tokens,
|
||||
incomplete: publish_incomplete,
|
||||
conversation_id: Some(conv_id.to_string()),
|
||||
pinned_goals: pinned_goals.to_vec(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── finish_round_exit: run_agentic_loop 收尾统一入口(抽自 5 处退出路径重复代码) ──
|
||||
//
|
||||
// 收敛各退出点的「save_conversation + spawn_ensure_title + guard.reset + emit AiCompleted」序列。
|
||||
// 退出点(行为零变更):
|
||||
// - stop_flag 入口:save(Some usage, None model) + spawn_title + reset + emit(None,None,publish=true)
|
||||
// - Coordinator merge:save(None, None) + no title + reset + emit(None,None,publish=true)
|
||||
// - MidStream 保文:save(Some usage, Some model) + spawn_title + reset + emit(Some(true),None,publish=true)
|
||||
// - push 后 stop:save(Some usage, Some model) + spawn_title + reset + emit(None,None,publish=true)
|
||||
// - max_iterations:save(Some usage, Some model) + no title + reset + emit(Some(true),-,publish=false)
|
||||
//
|
||||
// **不在范围**(模式异构,保留原状):
|
||||
// - 正常完成(spawn 异步 save+extract+ensure_title 阻塞式,与 spawn_ensure_title 后台式不同):
|
||||
// 仅复用 emit_ai_completed_once,不走本 helper。
|
||||
// - Fatal / Exhausted:emit AiError(已由 emit_fatal_error 统一),不走本 helper。
|
||||
// - stale_conv return:无 emit/guard 显式 reset(依赖 guard Drop),不走本 helper。
|
||||
//
|
||||
// 参数口径(对齐各退出点原代码):
|
||||
// - save_usage/save_model:透传 save_conversation;Coordinator 传 (None, None) 仍会调 save(幂等)
|
||||
// - emit_usage:emit AiCompleted 的 token 三元组来源(各退出点经 tokens.add/round-derived 后传入)
|
||||
// - spawn_title:true → spawn_ensure_title(provider_config 后台生成,失败有 extract_title 兜底)
|
||||
// - emit_incomplete/publish_incomplete/do_publish:透传 emit_ai_completed_once(见该 helper 文档)
|
||||
async fn finish_round_exit(
|
||||
session_arc: &Arc<Mutex<AiSession>>,
|
||||
db: &Arc<Database>,
|
||||
conv_id: &str,
|
||||
save_usage: Option<&df_ai::provider::TokenUsage>,
|
||||
save_model: Option<&str>,
|
||||
spawn_title: bool,
|
||||
provider_config: &AiProviderRecord,
|
||||
llm_concurrency: &LlmConcurrency,
|
||||
guard: &mut GeneratingGuard,
|
||||
emit_usage: &df_ai::provider::TokenUsage,
|
||||
emit_incomplete: Option<bool>,
|
||||
publish_incomplete: Option<bool>,
|
||||
do_publish: bool,
|
||||
pinned_goals: &[super::GoalEntry],
|
||||
app_handle: &AppHandle,
|
||||
) {
|
||||
// 落库:save_conversation 幂等(每轮重复覆盖落库),Coordinator 传 (None,None) 也调用(对齐原 :1126)
|
||||
save_conversation(session_arc, db, conv_id, save_usage, save_model, true).await;
|
||||
// 标题生成后台化(不阻塞 Completed emit):Coordinator / max_iterations 不 spawn(对齐原行为)
|
||||
if spawn_title {
|
||||
spawn_ensure_title(provider_config, db, conv_id, app_handle, session_arc, llm_concurrency);
|
||||
}
|
||||
// generating 复位(guard.reset 幂等):保证前端收 AiCompleted 时后端已 Idle(发送队列续发不被「正在生成中」拒绝)
|
||||
guard.reset().await;
|
||||
// generating 复位后再 emit Completed(对齐原各路径顺序)
|
||||
emit_ai_completed_once(
|
||||
app_handle, conv_id, emit_usage,
|
||||
emit_incomplete, publish_incomplete, do_publish,
|
||||
pinned_goals,
|
||||
).await;
|
||||
}
|
||||
|
||||
// ── push_assistant_message: 轮结束后向 session.messages 推入 assistant 消息(扁平抽自原嵌套块) ──
|
||||
// has_tool_calls=true: assistant_with_tools(文本+工具调用占位), false 且文本非空: assistant(纯文本)。
|
||||
// 两分支都不为空且都不需 push 时(no tool + empty text),返回(no-op)。
|
||||
|
||||
Reference in New Issue
Block a user