fix(desktop,dispatcher): 从编排执行的多 agent 图看不到「团队」tab

现象:编排里并排三个 agent(研究/撰写/审查)跑完,运行页没有团队 tab。

两处卡住,不是一处:
1) isMultiAgent 只认 coordinator: 节点。用户自己在图里并排多个 agent 也是
   团队,却被整个漏掉。改成:有协调者,或 ≥2 个 agent: 节点。
2) 就算放宽 1,deriveTeam 仍按 kind==="agent" 挑工位——而两条产生 agent 的
   路径 kind 并不一致:协调者派发的专家是 kind=agent,图里的 agent 节点是
   kind=model。改成按节点名前缀(agent:/tool:)判,这本来就是后端一直遵守的
   约定;顺带天然把 retriever:/map:/render: 这些同为 kind=tool 的节点挡在
   工位之外(之前它们会混进来当工位)。

连带修一个更要命的:runAgent 把轨迹标签写死成"模型流式推理"、runReactAgent
写死成"ReAct 智能体(自主调工具)",用户在编排里给节点起的名字(研究 Agent /
撰写 Agent / 审查 Agent)整个丢了。后果不止办公室:执行轨迹里三行同名,根本
分不出谁是谁;团队视图只能退回节点 ID,工位显示成 r/w/rev。
改成一律 labelOf(n, 兜底) 由调用方传入,+2 单测钉住。

另:没有协调者时不再凭空画一个"协调者"小人(白板改挂「任务产出」),也不演
递简报那一程(没人可递),✓ 气泡改为收工即冒。

注意:已存的历史轨迹是落库的,仍是旧标签;只有新跑的任务才有节点名。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-15 17:18:52 +08:00
parent 02608bb6a4
commit c2811e79a3
10 changed files with 188 additions and 51 deletions
+34 -1
View File
@@ -86,13 +86,26 @@ describe("pendingApprovalHITL 待审批中断)", () => {
// 后端约定:协调者 coordinator:<id>/kind=model;专家 agent:<名>/kind=agent
// 收尾 detail="简报 X → Y";工具 tool:<名>/kind=tool。
describe("isMultiAgent是否走了多智能体协调", () => {
describe("isMultiAgent值不值得看团队", () => {
it("有 coordinator: 节点 → true", () => {
expect(isMultiAgent([ev("coordinator:c", "start", { kind: "model" })])).toBe(true);
});
it("普通任务(model/tool) → false,不显示团队 tab", () => {
expect(isMultiAgent([ev("model", "start", { kind: "model" }), ev("tool:wiki", "end", { kind: "tool" })])).toBe(false);
});
// 编排里并排多个 agent 的图没有协调者,且 agent 节点发的是 kind=model(不是 kind=agent)。
// 按 kind 判会把这种图整个漏掉——用户从编排点执行后就看不到团队 tab。
it("图里 ≥2 个 agent 节点(kind=model,无协调者)→ true", () => {
expect(
isMultiAgent([
ev("agent:a", "start", { kind: "model" }),
ev("agent:b", "start", { kind: "model" }),
]),
).toBe(true);
});
it("只有 1 个 agent → false(一个人不叫团队)", () => {
expect(isMultiAgent([ev("agent:a", "start", { kind: "model" }), ev("agent:a", "end", { kind: "model" })])).toBe(false);
});
});
describe("deriveTeam(协调者 + 工位)", () => {
@@ -134,6 +147,26 @@ describe("deriveTeam(协调者 + 工位)", () => {
expect(t.seats[0].output).toBe("命中 4 段");
});
it("图里的 agent 节点(kind=model)也算工位", () => {
const t = deriveTeam([
ev("agent:writer", "start", { kind: "model", ts: 0 }),
ev("agent:writer", "end", { kind: "model", ts: 300, ms: 300, detail: "成稿 800 字" }),
]);
expect(t.seats.map((s) => s.name)).toEqual(["writer"]);
expect(t.seats[0].status).toBe("done");
expect(t.lead).toBeNull(); // 没有协调者 → 不能凭空造一个
});
it("retriever/map/render 不当工位(同为 kind=tool,但不是 tool: 前缀)", () => {
const t = deriveTeam([
ev("retriever:r", "start", { kind: "tool", ts: 0 }),
ev("retriever:r", "end", { kind: "tool", ts: 50, ms: 50 }),
ev("map:m", "start", { kind: "plan", ts: 10 }),
ev("render:o", "end", { kind: "render", ts: 60 }),
]);
expect(t.seats).toEqual([]);
});
it("协调者被识别为 lead,且不混进工位", () => {
const t = deriveTeam([
ev("coordinator:c", "start", { kind: "model", label: "多智能体协调", detail: "2 个专家可派发" }),
+13 -4
View File
@@ -128,9 +128,14 @@ export interface TeamModel {
t1: number; // 最晚结束(未完则为 now
}
// isMultiAgent 判定这次运行是否走了多智能体协调(据此才显示「团队」视图)。
// isMultiAgent 判定这次运行值不值得看「团队」:
// 1) 走了协调者(coordinator:)——orchestrator 自主派发;或
// 2) 图里挂了 ≥2 个专家(agent:)——用户自己在编排里并排/接力多个 agent,那也是个团队。
// 只有 1 个 agent 的普通任务不显示(一个人不叫团队,看轨迹就够)。
export function isMultiAgent(events: ExecEvent[]): boolean {
return events.some((e) => e.node.startsWith("coordinator:"));
if (events.some((e) => e.node.startsWith("coordinator:"))) return true;
const agents = new Set(events.filter((e) => e.node.startsWith("agent:")).map((e) => e.node));
return agents.size >= 2;
}
// splitBrief 解析专家收尾 detail"简报 X → Y" → {brief:X, output:Y};不匹配则整段当产出。
@@ -164,8 +169,12 @@ export function deriveTeam(events: ExecEvent[], now = Date.now()): TeamModel {
else if (e.phase === "error") lead.status = "error";
continue;
}
const isAgent = e.kind === "agent";
const isTool = e.kind === "tool";
// 按节点名前缀挑工位,不按 kind:两条产生 agent 的路径 kind 并不一致
// —— 协调者派发的专家是 kind=agent,图里的 agent 节点是 kind=model。
// 前缀才是后端一直遵守的约定(agent:<名字> / tool:<名字>),
// 而且它天然把 retriever:/map:/render: 这些同为 kind=tool 的节点挡在工位之外。
const isAgent = e.node.startsWith("agent:");
const isTool = e.node.startsWith("tool:");
if (!isAgent && !isTool) continue;
let s = map.get(e.node);
if (!s) {