fix(roundtable): 优化 Step2 总控派活稳定性与并发编排
限制总控工具白名单、关闭圆桌记忆注入、综合轮 synthesis_mode 与编排 epoch 防双发,并优化广播异步化及 Postgres checkpointer 连接池。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
9c9c912657
commit
924c8f2fac
8 files changed
+767
-302
No files matched your search
@@ -203,6 +203,12 @@ export interface StreamMultiAgentRequest {
|
||||
newMessage: string;
|
||||
model?: string;
|
||||
skillStopNames?: string[];
|
||||
/**
|
||||
* 仅 leader 用。置 true 表示这是「派活后让总控判断继续/综合」的轮次:后端会把各已交付
|
||||
* 席位的**完整交付正文**额外注入总控输入,供其写最终方案。派活决策轮(首轮等)传 false/省略,
|
||||
* 后端只给摘要,使总控上下文精简、响应快。
|
||||
*/
|
||||
synthesisMode?: boolean;
|
||||
signal?: AbortSignal;
|
||||
/** Fires for every non-status data frame the upstream emits. */
|
||||
onUpstreamFrame?: (frame: unknown) => void;
|
||||
@@ -415,6 +421,8 @@ export async function streamMultiAgent(
|
||||
new_message: req.newMessage,
|
||||
model: req.model,
|
||||
skill_stop_names: req.skillStopNames ?? [],
|
||||
// 仅 leader 用;综合轮让后端注入各席位完整交付正文。省略 → 后端按 false 处理。
|
||||
synthesis_mode: req.synthesisMode ?? false,
|
||||
}),
|
||||
signal: req.signal,
|
||||
});
|
||||
|
||||
@@ -381,6 +381,12 @@ export function useStep2Orchestration(
|
||||
|
||||
// ─── Refs ────────────────────────────────────────────────────────
|
||||
const orchestrationCancelRef = useRef<boolean>(false);
|
||||
// 编排「世代号」。每次启动一轮编排(runOrchestration / runChainOrchestration)就 +1。
|
||||
// 旧循环捕获自己的 epoch,在每个取消检查点比对 —— 一旦 epoch 变了(说明已有新循环
|
||||
// 接管,例如人工干预),旧循环立即退出。这能根治「新循环把 orchestrationCancelRef
|
||||
// 重置为 false 反而把正在退出的旧循环『反取消』,导致旧循环继续又发一个 leader run」
|
||||
// 的并发双发 bug:epoch 是单调递增的,新循环无法把它『重置』回旧循环的值。
|
||||
const orchestrationEpochRef = useRef<number>(0);
|
||||
const step2InitRef = useRef<boolean>(false);
|
||||
const activeAbortersRef = useRef<Set<AbortController>>(new Set());
|
||||
/** 干预研审:恢复会商后待执行的 leader prompt。 */
|
||||
@@ -993,17 +999,26 @@ export function useStep2Orchestration(
|
||||
const runOrchestration = useCallback(
|
||||
async (seedMessage: string, ids: ThreadIdMap, coord: string) => {
|
||||
if (isOrchestratingRef.current) return;
|
||||
// **同步**抢占编排锁。isOrchestratingRef 平时由 [isOrchestrating] 的 effect 异步
|
||||
// 回写(见上方),滞后一拍;若只靠它,人工干预里 setIsRoundTableRunning(true) 触发的
|
||||
// 「恢复/干预」effect 会在 ref 尚未变 true 的同一 tick 内再调一次本函数 —— 两个 leader
|
||||
// run 并发打到同一总控 thread(rollback 抢占 → 截断的那个旧报 502、存活的那个重复出现)。
|
||||
// 这里立刻把 ref 置 true,让重入守卫即时生效,堵住二次触发。
|
||||
isOrchestratingRef.current = true;
|
||||
setIsOrchestrating(true);
|
||||
orchestrationCancelRef.current = false;
|
||||
// 认领本轮 epoch。任何更晚启动的循环都会让 epoch 自增 → 本循环 isStale() 立即为真。
|
||||
const myEpoch = (orchestrationEpochRef.current += 1);
|
||||
// 单一「该退出了吗」判定:被显式取消,**或**已被更新的循环接管(epoch 变了)。
|
||||
// 后者专治新循环重置 orchestrationCancelRef=false 把旧循环反取消的并发双发。
|
||||
const isStale = () =>
|
||||
orchestrationCancelRef.current || orchestrationEpochRef.current !== myEpoch;
|
||||
|
||||
let nextLeaderTask = seedMessage;
|
||||
let safetyCounter = 0;
|
||||
|
||||
try {
|
||||
while (
|
||||
!orchestrationCancelRef.current &&
|
||||
safetyCounter < MAX_CYCLES
|
||||
) {
|
||||
while (!isStale() && safetyCounter < MAX_CYCLES) {
|
||||
safetyCounter += 1;
|
||||
// Leader(协调智能体)正在派活,**不应该**让任何 sub agent 显示"研讨中"——
|
||||
// 协调者本身不在右侧"参与角色"面板里(COMMANDER_SEAT_ID=1 是人类主持人,
|
||||
@@ -1034,6 +1049,9 @@ export function useStep2Orchestration(
|
||||
dAgentThreadId: ids,
|
||||
agentName: coord,
|
||||
newMessage: nextLeaderTask,
|
||||
// 第 2 轮起(已有席位交付)才是「综合轮」:让后端注入各席位完整交付供总控收口。
|
||||
// 首轮纯派活、无交付,传 false 保持上下文精简。
|
||||
synthesisMode: safetyCounter > 1,
|
||||
model: selectedModelRef.current || undefined,
|
||||
signal: leaderAborter.signal,
|
||||
onTextDelta: (text) => appendToDialogue(leaderBubbleId, text),
|
||||
@@ -1064,7 +1082,7 @@ export function useStep2Orchestration(
|
||||
}
|
||||
activeAbortersRef.current.delete(leaderAborter);
|
||||
|
||||
if (orchestrationCancelRef.current) break;
|
||||
if (isStale()) break;
|
||||
|
||||
if (leaderRes.clarification) {
|
||||
finalizeDialogue(
|
||||
@@ -1101,7 +1119,7 @@ export function useStep2Orchestration(
|
||||
|
||||
// **顺序**派活 —— 不要改回 Promise.all,详见 §5.4。
|
||||
for (const [subName, task] of leaderRes.dispatched as LeaderDispatch[]) {
|
||||
if (orchestrationCancelRef.current) break;
|
||||
if (isStale()) break;
|
||||
const meta = agentNameToRoleRef.current[subName.toLowerCase()];
|
||||
if (meta) markSpeakerStreaming(meta.speakerId);
|
||||
const perSeatModel = meta
|
||||
@@ -1149,7 +1167,7 @@ export function useStep2Orchestration(
|
||||
onUpstreamFrame: (frame) =>
|
||||
handleUpstreamFrame(frame, seatThreadId),
|
||||
});
|
||||
if (orchestrationCancelRef.current) {
|
||||
if (isStale()) {
|
||||
finalizeDialogue(
|
||||
subBubbleId,
|
||||
specRes.content || "(已停止)",
|
||||
@@ -1181,7 +1199,7 @@ export function useStep2Orchestration(
|
||||
if (aborted) break;
|
||||
}
|
||||
|
||||
if (orchestrationCancelRef.current) break;
|
||||
if (isStale()) break;
|
||||
|
||||
setConsensusPercentage((prev) =>
|
||||
Math.min(95, parseFloat((prev + 8).toFixed(1))),
|
||||
@@ -1229,8 +1247,15 @@ export function useStep2Orchestration(
|
||||
const runChainOrchestration = useCallback(
|
||||
async (seedMessage: string, ids: ThreadIdMap, coord: string) => {
|
||||
if (isOrchestratingRef.current) return;
|
||||
// 同步抢占编排锁(原因同 runOrchestration):堵住人工干预时 isRoundTableRunning
|
||||
// toggle effect 在同一 tick 内的二次触发,避免并发双 leader run。
|
||||
isOrchestratingRef.current = true;
|
||||
setIsOrchestrating(true);
|
||||
orchestrationCancelRef.current = false;
|
||||
// epoch 接管:新循环启动即自增,旧链条循环据此退出(详见 runOrchestration / 声明处注释)。
|
||||
const myEpoch = (orchestrationEpochRef.current += 1);
|
||||
const isStale = () =>
|
||||
orchestrationCancelRef.current || orchestrationEpochRef.current !== myEpoch;
|
||||
|
||||
const chainSeats = selectedAgentsRef.current;
|
||||
const total = chainSeats.length;
|
||||
@@ -1239,7 +1264,7 @@ export function useStep2Orchestration(
|
||||
|
||||
try {
|
||||
for (let i = 0; i < total; i += 1) {
|
||||
if (orchestrationCancelRef.current) {
|
||||
if (isStale()) {
|
||||
finishedAllSeats = false;
|
||||
break;
|
||||
}
|
||||
@@ -1295,7 +1320,7 @@ export function useStep2Orchestration(
|
||||
throw err;
|
||||
}
|
||||
activeAbortersRef.current.delete(leaderAborter);
|
||||
if (orchestrationCancelRef.current) {
|
||||
if (isStale()) {
|
||||
finalizeDialogue(leaderBubbleId, leaderRes.content || "(已停止)", "已停止");
|
||||
finishedAllSeats = false;
|
||||
break;
|
||||
@@ -1357,7 +1382,7 @@ export function useStep2Orchestration(
|
||||
},
|
||||
onUpstreamFrame: (frame) => handleUpstreamFrame(frame, seatThreadId),
|
||||
});
|
||||
if (orchestrationCancelRef.current) {
|
||||
if (isStale()) {
|
||||
finalizeDialogue(subBubbleId, specRes.content || "(已停止)", "已停止");
|
||||
aborted = true;
|
||||
} else {
|
||||
@@ -1384,7 +1409,7 @@ export function useStep2Orchestration(
|
||||
}
|
||||
|
||||
// ── (c) 总控收口:综合全链交付,产出最终结论 ──
|
||||
if (!orchestrationCancelRef.current && finishedAllSeats && total > 0) {
|
||||
if (!isStale() && finishedAllSeats && total > 0) {
|
||||
highlightSpeaker(null);
|
||||
const closeoutBubbleId = appendStreamingDialogue({
|
||||
sender: "总控协调",
|
||||
@@ -1402,6 +1427,8 @@ export function useStep2Orchestration(
|
||||
agentName: coord,
|
||||
newMessage:
|
||||
"各链条席位已按顺序完成发言,请你综合所有席位的交付,输出最终方案与结论。不要再派活、不要再向任何席位分配任务。",
|
||||
// 链条收口就是综合轮:让后端注入各席位完整交付正文。
|
||||
synthesisMode: true,
|
||||
model: selectedModelRef.current || undefined,
|
||||
signal: closeoutAborter.signal,
|
||||
onTextDelta: (text) => appendToDialogue(closeoutBubbleId, text),
|
||||
@@ -1604,7 +1631,7 @@ export function useStep2Orchestration(
|
||||
content: `💡 (人工指令介入) ${userText}`,
|
||||
confidence: "最高指令",
|
||||
});
|
||||
pendingInjectPromptRef.current = `用户人工干预新指令:${userText}\n\n请结合当前讨论状态,决定是否继续派活或直接给出更新后的结论。`;
|
||||
pendingInjectPromptRef.current = `用户人工干预新指令:${userText}\n\n请优先按这条新指令,通过 agent_orchestration 把活派给相关的真实席位去获取交付;若用户点名了某个席位(如"前端实现"),必须派给清单里对应的那个 agent_name。**不要**自己代答,也**不要**在没有真正看到该席位「完成…交付内容为:…」的情况下臆造交付或直接收口。只有当用户明确要求收口、或相关席位都已真实交付时,才综合输出结论。`;
|
||||
triggerToast("干预指令已记录,会商已自动恢复执行");
|
||||
}, [appendDialogue, customAgentInput, triggerToast]);
|
||||
|
||||
@@ -1631,7 +1658,7 @@ export function useStep2Orchestration(
|
||||
confidence: "最高指令",
|
||||
});
|
||||
|
||||
const prompt = `用户人工干预新指令:${userText}\n\n请结合当前讨论状态,决定是否继续派活或直接给出更新后的结论。`;
|
||||
const prompt = `用户人工干预新指令:${userText}\n\n请优先按这条新指令,通过 agent_orchestration 把活派给相关的真实席位去获取交付;若用户点名了某个席位(如"前端实现"),必须派给清单里对应的那个 agent_name。**不要**自己代答,也**不要**在没有真正看到该席位「完成…交付内容为:…」的情况下臆造交付或直接收口。只有当用户明确要求收口、或相关席位都已真实交付时,才综合输出结论。`;
|
||||
|
||||
// 确保处于运行状态(同步更新按钮 UI)
|
||||
setIsRoundTableRunning(true);
|
||||
|
||||
Reference in new issue
Block a user