fix(会商第二步): 恢复总控派活实时展示并修正沙箱预览状态
- 流式解析改为跟踪当前 tool call id,连续多次 agent_orchestration 派活时前端即时展示最新席位和任务,不再等模型完整回复后一次性出现。 - 总控派活步骤按 tool call 独立归档并合并增量参数,避免同名工具调用互相覆盖或只显示半截任务。 - 沙箱自动打开改为按单个产物预览键管理:用户关闭当前文件后不反复弹出,但后续新文件仍可自动预览。 - 为圆桌虚拟 write_file 补齐工具完成结果,并在席位收尾和历史草稿恢复时兜底收口,防止完成文件长期显示“正在生成”。
This commit is contained in:
1 parent
74fc849241
commit
2254841822
3 files changed
+214
-58
No files matched your search
@@ -290,12 +290,14 @@ export interface StreamMultiAgentRequest {
|
||||
* show progressive status messages during the inevitable first-token wait.
|
||||
* `name` is the tool name when phase is `tool_calling` (or empty otherwise).
|
||||
* `args` 是 tool_calling 阶段的工具入参(取自上游 LangGraph messages-tuple
|
||||
* 帧的 tool_calls[0].args),供 UI 渲染路径 chip / 代码块等子节点。
|
||||
* 帧中当前 tool call 的 args),供 UI 渲染路径 chip / 代码块等子节点。
|
||||
* `toolCallId` 可将同名工具的连续调用与同一次调用的增量参数区分开。
|
||||
*/
|
||||
onPhaseChange?: (
|
||||
phase: StreamPhase,
|
||||
name?: string,
|
||||
args?: Record<string, unknown>,
|
||||
toolCallId?: string,
|
||||
) => void;
|
||||
/**
|
||||
* 模型自动容错:后端检测到当前模型出问题(兜底错误文案 / 交付为空等),已自动切到下一个
|
||||
@@ -619,27 +621,32 @@ export async function streamMultiAgent(
|
||||
let currentPhase: StreamPhase = "connecting";
|
||||
let currentPhaseName = "";
|
||||
let currentPhaseArgsSignature = "";
|
||||
let currentPhaseToolCallId = "";
|
||||
req.onPhaseChange?.("connecting");
|
||||
const setPhase = (
|
||||
next: StreamPhase,
|
||||
name?: string,
|
||||
args?: Record<string, unknown>,
|
||||
toolCallId?: string,
|
||||
) => {
|
||||
const nextName = name ?? "";
|
||||
const nextArgsSignature = phaseArgsSignature(args);
|
||||
const nextToolCallId = toolCallId ?? "";
|
||||
// 同 phase、同工具、同身份入参的帧只是流式重复帧,不通知 UI。保留 path/filepaths
|
||||
// 等身份变化,确保工具参数从不完整变完整时仍可自动打开沙箱。
|
||||
if (
|
||||
next === currentPhase &&
|
||||
nextName === currentPhaseName &&
|
||||
nextArgsSignature === currentPhaseArgsSignature
|
||||
nextArgsSignature === currentPhaseArgsSignature &&
|
||||
nextToolCallId === currentPhaseToolCallId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
currentPhase = next;
|
||||
currentPhaseName = nextName;
|
||||
currentPhaseArgsSignature = nextArgsSignature;
|
||||
req.onPhaseChange?.(next, name, args);
|
||||
currentPhaseToolCallId = nextToolCallId;
|
||||
req.onPhaseChange?.(next, name, args, toolCallId);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -811,24 +818,29 @@ export async function streamMultiAgent(
|
||||
if (chunkType === "tool") {
|
||||
setPhase("tool_result");
|
||||
} else if (hasToolCall) {
|
||||
// Pick the most informative name + args we have so far.
|
||||
// Pick the active (latest) call's name + args. A leader can dispatch
|
||||
// several seats with the same `agent_orchestration` tool in one run;
|
||||
// choosing the first accumulated call would leave the UI stuck on an
|
||||
// earlier assignment until the final response arrives.
|
||||
// 优先用累积出的 / 完整的 tool_calls(含可读 args 对象);若都还不到火候,
|
||||
// 回退到 tool_call_chunks 拿名字(args 还是 string,不透传)。
|
||||
const firstCall =
|
||||
const activeCall =
|
||||
effectiveToolCalls && effectiveToolCalls.length > 0
|
||||
? (effectiveToolCalls[0] as Record<string, unknown>)
|
||||
? (effectiveToolCalls[effectiveToolCalls.length - 1] as Record<string, unknown>)
|
||||
: Array.isArray(toolCallChunks) && toolCallChunks.length > 0
|
||||
? (toolCallChunks[0] as Record<string, unknown>)
|
||||
? (toolCallChunks[toolCallChunks.length - 1] as Record<string, unknown>)
|
||||
: null;
|
||||
const toolName =
|
||||
firstCall && typeof firstCall.name === "string" ? firstCall.name : "";
|
||||
activeCall && typeof activeCall.name === "string" ? activeCall.name : "";
|
||||
const args =
|
||||
firstCall &&
|
||||
typeof firstCall.args === "object" &&
|
||||
firstCall.args !== null
|
||||
? (firstCall.args as Record<string, unknown>)
|
||||
activeCall &&
|
||||
typeof activeCall.args === "object" &&
|
||||
activeCall.args !== null
|
||||
? (activeCall.args as Record<string, unknown>)
|
||||
: undefined;
|
||||
setPhase("tool_calling", toolName, args);
|
||||
const toolCallId =
|
||||
activeCall && typeof activeCall.id === "string" ? activeCall.id : undefined;
|
||||
setPhase("tool_calling", toolName, args, toolCallId);
|
||||
} else if (
|
||||
(chunkType === "ai" || chunkType === "aimessagechunk" || chunkType === "aimessage") &&
|
||||
((typeof content === "string" && content.length > 0) || (Array.isArray(content) && content.length > 0))
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
*/
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
import { useArtifacts } from "@/components/workspace/artifacts";
|
||||
import {
|
||||
artifactPreviewKey,
|
||||
useArtifacts,
|
||||
} from "@/components/workspace/artifacts";
|
||||
import {
|
||||
initMultiAgent,
|
||||
streamMultiAgent,
|
||||
@@ -37,6 +40,21 @@ type RunFileRef = NonNullable<StreamMultiAgentRequest["files"]>[number];
|
||||
|
||||
type SeatDelivery = { name: string; content: string };
|
||||
|
||||
/**
|
||||
* `agent_orchestration` 可在一轮总控中重复调用。用 tool call id 区分每一笔派活,
|
||||
* 同一 id 的 task 参数则持续覆盖更新,供步骤卡实时显示。
|
||||
*/
|
||||
function streamStepKey(
|
||||
phase: StreamPhase,
|
||||
toolName?: string,
|
||||
toolCallId?: string,
|
||||
): string {
|
||||
if (phase === "tool_calling" && toolName === "agent_orchestration" && toolCallId) {
|
||||
return `${phase}:${toolName}:${toolCallId}`;
|
||||
}
|
||||
return `${phase}:${toolName ?? ""}`;
|
||||
}
|
||||
|
||||
/** In-memory cursor for a business-chain human-validation pause. */
|
||||
type HumanValidationContinuation =
|
||||
| { mode: "chain"; nextSeatIndex: number }
|
||||
@@ -179,7 +197,7 @@ export interface ParallelSeatView {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stub AI message —— 用于把 special seat SSE 拿到的工具调用注入到 ThreadContext
|
||||
* Stub thread message —— 用于把 special seat SSE 拿到的工具调用(及其合成 tool result)注入到 ThreadContext
|
||||
* 的 `thread.messages` 里,让 `useArtifactContent` 在沙箱里通过虚拟 URL
|
||||
* `write-file:<path>?message_id=...&tool_call_id=...` 直接读 `args.content`,
|
||||
* 完全对齐主聊天 `chats/:threadId` 的 write_file 实时预览体验。
|
||||
@@ -191,9 +209,59 @@ export interface StubToolCall {
|
||||
}
|
||||
export interface StubAiMessage {
|
||||
id: string;
|
||||
type: "ai";
|
||||
type: "ai" | "tool";
|
||||
content: string;
|
||||
tool_calls: StubToolCall[];
|
||||
/** tool 类型消息对应的原始 tool call;供沙箱判定 write_file 已完成。 */
|
||||
tool_call_id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 圆桌流为了实时预览会合成一条带 write_file 的 ai 消息。主聊天真实流在工具执行结束后
|
||||
* 还会收到对应的 tool 消息;圆桌 special 流此前漏了这一步,导致沙箱永久认为文件仍在写。
|
||||
*
|
||||
* 这里只补齐已经存在的 write_file 调用,绝不根据整条会商/其它并行流的 running 状态推断,
|
||||
* 因而每个席位、报告、流程图和大屏预览的完成态彼此独立。
|
||||
*/
|
||||
export function completeStubWriteFile(
|
||||
messages: StubAiMessage[],
|
||||
toolCallId: string,
|
||||
): StubAiMessage[] {
|
||||
if (!toolCallId || messages.some((message) => message.type === "tool" && message.tool_call_id === toolCallId)) {
|
||||
return messages;
|
||||
}
|
||||
const source = messages.find(
|
||||
(message) =>
|
||||
message.type === "ai" &&
|
||||
message.tool_calls.some(
|
||||
(toolCall) => toolCall.id === toolCallId && toolCall.name === "write_file",
|
||||
),
|
||||
);
|
||||
if (!source) return messages;
|
||||
return [
|
||||
...messages,
|
||||
{
|
||||
id: `roundtable-stub-tool-result-${source.id}-${toolCallId}`,
|
||||
type: "tool",
|
||||
content: "",
|
||||
tool_calls: [],
|
||||
tool_call_id: toolCallId,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 补齐一段已正常收尾的圆桌 stub 中全部 write_file 的工具返回。 */
|
||||
export function completeAllStubWriteFiles(messages: StubAiMessage[]): StubAiMessage[] {
|
||||
let next = messages;
|
||||
for (const message of messages) {
|
||||
if (message.type !== "ai") continue;
|
||||
for (const toolCall of message.tool_calls) {
|
||||
if (toolCall.name === "write_file") {
|
||||
next = completeStubWriteFile(next, toolCall.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
/** Persona 抽屉的 per-seat 覆盖(prompt / temp 仅前端记录,model 真实生效)。 */
|
||||
@@ -515,19 +583,12 @@ export function useStep2Orchestration(
|
||||
select: selectArtifact,
|
||||
setOpen: setArtifactsOpen,
|
||||
deselect: deselectArtifact,
|
||||
autoOpen,
|
||||
autoSelect,
|
||||
openStreamedArtifact,
|
||||
} = useArtifacts();
|
||||
const artifactPathsRef = useRef<string[]>([]);
|
||||
useEffect(() => {
|
||||
artifactPathsRef.current = artifacts ?? [];
|
||||
}, [artifacts]);
|
||||
// 用户手动关掉沙箱后,autoOpen 会被 ArtifactsProvider 置 false —— 把它们打到 ref
|
||||
// 上,让 openSeatVirtualArtifact 不用进 useCallback deps,避免每次 toggle 重建函数。
|
||||
const autoOpenFlagsRef = useRef({ autoOpen, autoSelect });
|
||||
useEffect(() => {
|
||||
autoOpenFlagsRef.current = { autoOpen, autoSelect };
|
||||
}, [autoOpen, autoSelect]);
|
||||
/** 已自动打开过的 virtualUrl 集合 —— 流式期间一秒会回调几十次,防止重复 schedule 开沙箱。 */
|
||||
const hasAutoOpenedRef = useRef<Set<string>>(new Set());
|
||||
|
||||
@@ -562,6 +623,9 @@ export function useStep2Orchestration(
|
||||
const [step2RoundtableDialogues, setStep2RoundtableDialogues] = useState<
|
||||
Step2Dialogue[]
|
||||
>([]);
|
||||
// 由气泡 id 反查真实席位线程。完成一条气泡时只收口该线程的虚拟 write_file,
|
||||
// 不会误影响仍在并行运行的其它席位。
|
||||
const dialogueThreadIdRef = useRef<Record<number, string>>({});
|
||||
const [pendingClarification, setPendingClarification] =
|
||||
useState<ClarificationPayload | null>(null);
|
||||
const [clarificationDraft, setClarificationDraft] = useState<string>("");
|
||||
@@ -837,13 +901,42 @@ export function useStep2Orchestration(
|
||||
[scheduleSeatStubMessagesFlush],
|
||||
);
|
||||
|
||||
/** 将某次真实 tool 返回同步到对应的虚拟 write_file 预览。 */
|
||||
const completeSeatStubWriteFile = useCallback(
|
||||
(seatThreadId: string, toolCallId: string) => {
|
||||
const current = seatStubMessagesRef.current[seatThreadId] ?? [];
|
||||
const nextMessages = completeStubWriteFile(current, toolCallId);
|
||||
if (nextMessages === current) return;
|
||||
const nextMap = { ...seatStubMessagesRef.current, [seatThreadId]: nextMessages };
|
||||
seatStubMessagesRef.current = nextMap;
|
||||
pendingSeatStubMessagesRef.current = nextMap;
|
||||
scheduleSeatStubMessagesFlush();
|
||||
},
|
||||
[scheduleSeatStubMessagesFlush],
|
||||
);
|
||||
|
||||
/** special 流正常收尾的兜底:补齐该席位尚未透传出来的 write_file tool 返回。 */
|
||||
const completeSeatStubWriteFiles = useCallback(
|
||||
(seatThreadId: string | undefined) => {
|
||||
if (!seatThreadId) return;
|
||||
const current = seatStubMessagesRef.current[seatThreadId] ?? [];
|
||||
const nextMessages = completeAllStubWriteFiles(current);
|
||||
if (nextMessages === current) return;
|
||||
const nextMap = { ...seatStubMessagesRef.current, [seatThreadId]: nextMessages };
|
||||
seatStubMessagesRef.current = nextMap;
|
||||
pendingSeatStubMessagesRef.current = nextMap;
|
||||
scheduleSeatStubMessagesFlush();
|
||||
},
|
||||
[scheduleSeatStubMessagesFlush],
|
||||
);
|
||||
|
||||
/**
|
||||
* 用「虚拟 URL」(write-file: scheme)打开沙箱 —— 内容来自 stub messages 的 args.content,
|
||||
* 不污染 `artifacts[]` 真实路径列表(列表后续可由 present_files / 显式 setArtifacts 填充)。
|
||||
*
|
||||
* auto=true 流式期间:
|
||||
* - 已自动打开过该 virtualUrl 就直接跳过(防止 onPhaseChange / onUpstreamFrame 每帧重复 schedule);
|
||||
* - 用户手动关闭过沙箱(`autoOpen` 已被 ArtifactsProvider 置 false)也尊重决定,不再自动开。
|
||||
* - 用户手动关闭过**当前文件**时不反复弹开;后续新的 write_file 调用仍允许自动预览。
|
||||
* auto=false(用户点击步骤 / 文件卡)始终强制打开。
|
||||
*/
|
||||
const openSeatVirtualArtifact = useCallback(
|
||||
@@ -861,8 +954,6 @@ export function useStep2Orchestration(
|
||||
const manual = opts?.manual ?? !auto;
|
||||
if (auto) {
|
||||
if (hasAutoOpenedRef.current.has(virtualUrl)) return;
|
||||
const flags = autoOpenFlagsRef.current;
|
||||
if (!flags.autoOpen || !flags.autoSelect) return;
|
||||
// 快速模式(双列)DAG 并行批次:**完全不**自动开沙箱 —— 双列只看正文,多席位并发写
|
||||
// 文件(现已放开 write_file)会让单沙箱预览反复串台。文件仍写盘,Step3 / 文件卡可查看。
|
||||
if (
|
||||
@@ -882,6 +973,11 @@ export function useStep2Orchestration(
|
||||
return;
|
||||
}
|
||||
hasAutoOpenedRef.current.add(virtualUrl);
|
||||
setActiveArtifactThreadId(seatThreadId);
|
||||
// 由 ArtifactsProvider 以 tool-call 维度管理「手动关闭后不再弹同一份文件」:
|
||||
// 当前文件被关闭过 → false;下一次 write_file 的 previewKey 不同 → 仍会自动打开。
|
||||
openStreamedArtifact(virtualUrl, artifactPreviewKey(virtualUrl));
|
||||
return;
|
||||
}
|
||||
setActiveArtifactThreadId(seatThreadId);
|
||||
const open = () => {
|
||||
@@ -889,13 +985,9 @@ export function useStep2Orchestration(
|
||||
selectArtifact(virtualUrl, !manual);
|
||||
setArtifactsOpen(true);
|
||||
};
|
||||
if (auto) {
|
||||
window.setTimeout(open, 100);
|
||||
} else {
|
||||
open();
|
||||
}
|
||||
open();
|
||||
},
|
||||
[selectArtifact, setArtifactsOpen],
|
||||
[openStreamedArtifact, selectArtifact, setArtifactsOpen],
|
||||
);
|
||||
|
||||
const buildWriteFileVirtualUrl = useCallback(
|
||||
@@ -1156,6 +1248,13 @@ export function useStep2Orchestration(
|
||||
const chunk = frame[0];
|
||||
if (!chunk || typeof chunk !== "object") return;
|
||||
const obj = chunk as Record<string, unknown>;
|
||||
// special 流透传真实工具返回时,必须同步收口虚拟 write_file;否则右侧沙箱会把
|
||||
// 已完成的文件永久当成「正在生成」。
|
||||
if (String(obj.type ?? "").toLowerCase() === "tool") {
|
||||
const toolCallId = typeof obj.tool_call_id === "string" ? obj.tool_call_id : "";
|
||||
if (toolCallId) completeSeatStubWriteFile(seatThreadId, toolCallId);
|
||||
return;
|
||||
}
|
||||
const toolCalls = obj.tool_calls;
|
||||
if (!Array.isArray(toolCalls) || toolCalls.length === 0) return;
|
||||
for (const tc of toolCalls) {
|
||||
@@ -1171,7 +1270,7 @@ export function useStep2Orchestration(
|
||||
upsertSeatToolCall(seatThreadId, name, argsObj);
|
||||
}
|
||||
},
|
||||
[upsertSeatToolCall],
|
||||
[completeSeatStubWriteFile, upsertSeatToolCall],
|
||||
);
|
||||
|
||||
// ── 「多智能体问答」子智能体任务卡片(参照主聊天 message-list 的 task 识别)──────────
|
||||
@@ -1402,15 +1501,28 @@ export function useStep2Orchestration(
|
||||
let nextDialogue: Step2Dialogue;
|
||||
|
||||
if (last && last.key === key) {
|
||||
// 同一个工具调用在流式期间会反复带来增量入参。步骤卡只需在「首次拿到
|
||||
// 可见参数」时补一次;文件正文由 upstream frame 同步给沙箱,不能随每个
|
||||
// content chunk 更新对话 state。
|
||||
const hasVisibleArgs = (value?: Record<string, unknown>) =>
|
||||
!!value && Object.keys(value).some((argKey) => argKey !== "content");
|
||||
if (hasVisibleArgs(last.args) || !hasVisibleArgs(args)) return prev;
|
||||
const nextSteps = [...steps];
|
||||
nextSteps[nextSteps.length - 1] = { ...last, args };
|
||||
nextDialogue = { ...dialogue, steps: nextSteps };
|
||||
// 总控派活的 task 是 tool_call JSON 的流式增量。不能沿用普通工具的
|
||||
// 「首次可见参数即停」策略,否则只显示 agent_name,整段任务会在收尾时
|
||||
// 才被最终消息一次性补上。
|
||||
if (key.startsWith("tool_calling:agent_orchestration:") && args) {
|
||||
const mergedArgs = { ...(last.args ?? {}), ...args };
|
||||
if (JSON.stringify(last.args ?? {}) === JSON.stringify(mergedArgs)) {
|
||||
return prev;
|
||||
}
|
||||
const nextSteps = [...steps];
|
||||
nextSteps[nextSteps.length - 1] = { ...last, args: mergedArgs };
|
||||
nextDialogue = { ...dialogue, steps: nextSteps };
|
||||
} else {
|
||||
// 同一个工具调用在流式期间会反复带来增量入参。步骤卡只需在「首次拿到
|
||||
// 可见参数」时补一次;文件正文由 upstream frame 同步给沙箱,不能随每个
|
||||
// content chunk 更新对话 state。
|
||||
const hasVisibleArgs = (value?: Record<string, unknown>) =>
|
||||
!!value && Object.keys(value).some((argKey) => argKey !== "content");
|
||||
if (hasVisibleArgs(last.args) || !hasVisibleArgs(args)) return prev;
|
||||
const nextSteps = [...steps];
|
||||
nextSteps[nextSteps.length - 1] = { ...last, args };
|
||||
nextDialogue = { ...dialogue, steps: nextSteps };
|
||||
}
|
||||
} else {
|
||||
nextDialogue = {
|
||||
...dialogue,
|
||||
@@ -1438,6 +1550,9 @@ export function useStep2Orchestration(
|
||||
agentThreadId?: string;
|
||||
}): number => {
|
||||
const id = Date.now() + Math.random();
|
||||
if (card.agentThreadId) {
|
||||
dialogueThreadIdRef.current[id] = card.agentThreadId;
|
||||
}
|
||||
setStep2RoundtableDialogues((prev) => [
|
||||
...prev,
|
||||
{
|
||||
@@ -1641,10 +1756,8 @@ export function useStep2Orchestration(
|
||||
const opened = openSeatLatestArtifact(tid);
|
||||
if (!opened) {
|
||||
// 当前显示席位**暂时**还没有沙箱产物(常见:并行批次第一个席位刚选中、还没写文件)→
|
||||
// 关闭沙箱面板,但**只用 deselect**(它走 raw setOpen(false),**不**关掉 autoOpen);
|
||||
// 千万别再调 setArtifactsOpen(false) —— 那个 wrapped setter 会把 autoOpen 置 false
|
||||
// (context.tsx:73),导致该席位**随后 live 写文件时 openSeatVirtualArtifact 的
|
||||
// `!autoOpen` 守卫拦掉自动打开**(正是「第一个席位不自动开沙箱、要手动开」的 bug)。
|
||||
// 用 deselect 收起并清空旧预览,不把别的席位当前文件标记为「手动关闭」。这样该席位
|
||||
// 随后的新 write_file 仍能按自己的 previewKey 自动打开。
|
||||
setActiveArtifactThreadId(null);
|
||||
deselectArtifact();
|
||||
}
|
||||
@@ -1658,6 +1771,9 @@ export function useStep2Orchestration(
|
||||
|
||||
const finalizeDialogue = useCallback(
|
||||
(id: number, finalContent: string, confidence?: string) => {
|
||||
// 本气泡对应的 special 流已收尾:为该席位(仅该席位)补齐 write_file tool result。
|
||||
// 这是对少数没有把 tool 帧透传到 onUpstreamFrame 的后端流的安全兜底。
|
||||
completeSeatStubWriteFiles(dialogueThreadIdRef.current[id]);
|
||||
// 最终结果已经是完整文本,丢弃尚未 flush 的 token 缓冲,避免收尾后旧增量反写。
|
||||
directStreamDeltaRef.current.delete(id);
|
||||
if (
|
||||
@@ -1693,7 +1809,7 @@ export function useStep2Orchestration(
|
||||
return changed ? next : prev;
|
||||
});
|
||||
},
|
||||
[],
|
||||
[completeSeatStubWriteFiles],
|
||||
);
|
||||
|
||||
const highlightSpeaker = useCallback(
|
||||
@@ -1991,13 +2107,13 @@ export function useStep2Orchestration(
|
||||
signal: leaderAborter.signal,
|
||||
onModelSwitch: makeModelSwitchHandler(leaderBubbleId, false),
|
||||
onTextDelta: (text) => appendToDialogue(leaderBubbleId, text),
|
||||
onPhaseChange: (phase, toolName, args) => {
|
||||
onPhaseChange: (phase, toolName, args, toolCallId) => {
|
||||
const label = phaseLabel(phase, "leader", toolName);
|
||||
setDialogueConfidence(leaderBubbleId, label);
|
||||
appendStepIfNew(
|
||||
leaderBubbleId,
|
||||
label,
|
||||
`${phase}:${toolName ?? ""}`,
|
||||
streamStepKey(phase, toolName, toolCallId),
|
||||
args,
|
||||
);
|
||||
handleArtifactToolPhase(phase, toolName, args, leaderThreadId);
|
||||
@@ -2274,10 +2390,15 @@ export function useStep2Orchestration(
|
||||
signal: leaderAborter.signal,
|
||||
onModelSwitch: makeModelSwitchHandler(leaderBubbleId, false),
|
||||
onTextDelta: (text) => appendToDialogue(leaderBubbleId, text),
|
||||
onPhaseChange: (phase, toolName, args) => {
|
||||
onPhaseChange: (phase, toolName, args, toolCallId) => {
|
||||
const label = phaseLabel(phase, "leader", toolName);
|
||||
setDialogueConfidence(leaderBubbleId, label);
|
||||
appendStepIfNew(leaderBubbleId, label, `${phase}:${toolName ?? ""}`, args);
|
||||
appendStepIfNew(
|
||||
leaderBubbleId,
|
||||
label,
|
||||
streamStepKey(phase, toolName, toolCallId),
|
||||
args,
|
||||
);
|
||||
handleArtifactToolPhase(phase, toolName, args, leaderThreadId);
|
||||
},
|
||||
onUpstreamFrame: (frame) => handleUpstreamFrame(frame, leaderThreadId),
|
||||
@@ -2460,10 +2581,15 @@ export function useStep2Orchestration(
|
||||
signal: closeoutAborter.signal,
|
||||
onModelSwitch: makeModelSwitchHandler(closeoutBubbleId, false),
|
||||
onTextDelta: (text) => appendToDialogue(closeoutBubbleId, text),
|
||||
onPhaseChange: (phase, toolName, args) => {
|
||||
onPhaseChange: (phase, toolName, args, toolCallId) => {
|
||||
const label = phaseLabel(phase, "leader", toolName);
|
||||
setDialogueConfidence(closeoutBubbleId, label);
|
||||
appendStepIfNew(closeoutBubbleId, label, `${phase}:${toolName ?? ""}`, args);
|
||||
appendStepIfNew(
|
||||
closeoutBubbleId,
|
||||
label,
|
||||
streamStepKey(phase, toolName, toolCallId),
|
||||
args,
|
||||
);
|
||||
handleArtifactToolPhase(phase, toolName, args, leaderThreadId);
|
||||
},
|
||||
onUpstreamFrame: (frame) => handleUpstreamFrame(frame, leaderThreadId),
|
||||
@@ -2639,10 +2765,15 @@ export function useStep2Orchestration(
|
||||
signal: leaderAborter.signal,
|
||||
onModelSwitch: makeModelSwitchHandler(leaderBubbleId, false),
|
||||
onTextDelta: (text) => appendToDialogue(leaderBubbleId, text),
|
||||
onPhaseChange: (phase, toolName, args) => {
|
||||
onPhaseChange: (phase, toolName, args, toolCallId) => {
|
||||
const label = phaseLabel(phase, "leader", toolName);
|
||||
setDialogueConfidence(leaderBubbleId, label);
|
||||
appendStepIfNew(leaderBubbleId, label, `${phase}:${toolName ?? ""}`, args);
|
||||
appendStepIfNew(
|
||||
leaderBubbleId,
|
||||
label,
|
||||
streamStepKey(phase, toolName, toolCallId),
|
||||
args,
|
||||
);
|
||||
handleArtifactToolPhase(phase, toolName, args, leaderThreadId);
|
||||
},
|
||||
onUpstreamFrame: (frame) => handleUpstreamFrame(frame, leaderThreadId),
|
||||
@@ -3431,8 +3562,20 @@ export function useStep2Orchestration(
|
||||
setHasConsensus(snapshot.hasConsensus);
|
||||
setConsensusPercentage(snapshot.consensusPercentage);
|
||||
setBudgetLimit(snapshot.budgetLimit);
|
||||
const restoredStubs = snapshot.seatStubMessages ?? {};
|
||||
// 历史草稿是静态完成记录;旧版本未持久化 tool result 的 write_file 也必须收口,
|
||||
// 否则从「最终方案展示」回看时会错误显示仍在生成。
|
||||
const restoredStubs = Object.fromEntries(
|
||||
Object.entries(snapshot.seatStubMessages ?? {}).map(([threadId, messages]) => [
|
||||
threadId,
|
||||
completeAllStubWriteFiles(messages),
|
||||
]),
|
||||
);
|
||||
replaceSeatStubMessages(restoredStubs);
|
||||
dialogueThreadIdRef.current = Object.fromEntries(
|
||||
(snapshot.step2RoundtableDialogues ?? [])
|
||||
.filter((dialogue) => Boolean(dialogue.agentThreadId))
|
||||
.map((dialogue) => [dialogue.id, dialogue.agentThreadId!]),
|
||||
);
|
||||
// 草稿里历史 write_file 的虚拟 URL 与当前会话无关 —— 清空守卫,允许新 run 触发自动打开。
|
||||
hasAutoOpenedRef.current = new Set();
|
||||
// 调用方根据 furthestStep 决定是否置 init 闸门为已 init;这里默认置上,
|
||||
@@ -3499,6 +3642,7 @@ export function useStep2Orchestration(
|
||||
setCoordinatorName(null);
|
||||
setInitError(null);
|
||||
setStep2RoundtableDialogues([]);
|
||||
dialogueThreadIdRef.current = {};
|
||||
setLastLeaderContent("");
|
||||
setHasConsensus(false);
|
||||
setConsensusPercentage(0);
|
||||
|
||||
@@ -29,7 +29,7 @@ import { MarkdownContent } from "@/roundtable-planning/components/MessageBubble"
|
||||
|
||||
/**
|
||||
* 根据工具名 + args 渲染 ChainOfThoughtStep 的子节点(路径 chip / 命令代码块等)。
|
||||
* 数据来自 streamMultiAgent 解析 LangGraph messages-tuple 帧的 tool_calls[0].args。
|
||||
* 数据来自 streamMultiAgent 解析 LangGraph messages-tuple 帧中当前工具调用的 args。
|
||||
*
|
||||
* 主聊天的 `ToolCall` 还会消费 tool result(走 `findToolCallResult`),我们这边
|
||||
* 拿不到 result(只走 tool_calls 增量帧),所以只渲染 args。
|
||||
|
||||
Reference in new issue
Block a user