feat(desktop): T0.2 多智能体进 Studio —— coordinator 节点 + 专家卡片 + 派发可观测
把已通的多智能体后端(MULTI_AGENT.md)接上 UI,用户可在画布拖出协调者图。
- nodeCatalog: 新增 `coordinator`「多智能体协调」节点(indigo) + 新字段类型
`agentList` + Specialist 接口({name,use,system,tools}),与后端 parseSpecialists 对齐。
- Inspector: AgentListField —— 可增删的子智能体卡片(名/用途/系统提示词/工具逗号分隔)。
- dsl 校验: agentList 需 ≥1 个有名字的专家、名字不重复。
- RunsView: 「工具调用」面板纳入专家派发(kind=agent),改名「工具/专家」,专家项
用 Users 图标 + indigo「专家」徽标区分(此前只筛 kind=tool,漏掉多智能体派发)。
- 导出: agents 数组经 exportDsl 原样透传进 DSL → 后端 parseSpecialists 直接消费。
tsc + vitest(19) 绿;UI 同款结构 DSL 后端可跑(协调链路 live 已验)。
DEPTH_ROADMAP T0.2 ✅,T0 激活 2/2 完成。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Activity, FileText, History, Wrench } from "lucide-react";
|
||||
import { Activity, FileText, History, Users, Wrench } from "lucide-react";
|
||||
import { ExecTrace } from "../components/ExecTrace";
|
||||
import { Markdown } from "../components/Markdown";
|
||||
import { ChartView } from "../components/ChartView";
|
||||
@@ -66,10 +66,11 @@ export function RunsView({ run }: { run: RunState }) {
|
||||
const liveActive = run.taskId && run.phase !== "idle";
|
||||
const cur = sel ? replay : run;
|
||||
const nodes = deriveNodes(cur.exec);
|
||||
const tools = nodes.filter((n) => n.kind === "tool");
|
||||
// 工具调用面板纳入专家派发:MCP 工具(kind=tool)与多智能体协调里的子智能体派发(kind=agent)都是「调用」。
|
||||
const calls = nodes.filter((n) => n.kind === "tool" || n.kind === "agent");
|
||||
const tabs: TabDef<DetailTab>[] = [
|
||||
{ key: "trace", label: "执行轨迹", count: nodes.length },
|
||||
{ key: "tools", label: "工具调用", count: tools.length },
|
||||
{ key: "tools", label: "工具/专家", count: calls.length },
|
||||
{ key: "eval", label: "评测" },
|
||||
];
|
||||
const empty = !cur.taskId && cur.exec.length === 0 && !cur.output;
|
||||
@@ -87,7 +88,7 @@ export function RunsView({ run }: { run: RunState }) {
|
||||
<h1 className="text-lg font-semibold text-slate-100">运行 · 观测</h1>
|
||||
<p className="mt-1 text-xs text-slate-500">运行历史复盘:选一次运行,左查轨迹/工具/评测(切换标签),右看模型输出。</p>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500">{nodes.length} 节点 · {tools.length} 次工具调用</span>
|
||||
<span className="text-xs text-slate-500">{nodes.length} 节点 · {calls.length} 次调用/派发</span>
|
||||
</header>
|
||||
|
||||
<div className="grid min-h-0 flex-1 grid-cols-[240px_1fr_1fr] gap-3">
|
||||
@@ -168,27 +169,32 @@ function OutputView({ output }: { output: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
// ToolCalls:从执行事件筛出工具调用节点,逐条展示入参 → 产出 + 耗时/状态。
|
||||
// ToolCalls:从执行事件筛出「调用」节点 —— MCP 工具(kind=tool)与多智能体协调里的子智能体派发
|
||||
// (kind=agent),逐条展示入参/简报 → 产出 + 耗时/状态。
|
||||
function ToolCalls({ run }: { run: RunState }) {
|
||||
const tools = deriveNodes(run.exec).filter((n) => n.kind === "tool");
|
||||
if (tools.length === 0) {
|
||||
return <p className="text-xs text-slate-600">本次运行暂无工具调用。图里挂了检索/工具节点,或报告挂了知识库时,每次工具调用会在此列出入参与产出。</p>;
|
||||
const calls = deriveNodes(run.exec).filter((n) => n.kind === "tool" || n.kind === "agent");
|
||||
if (calls.length === 0) {
|
||||
return <p className="text-xs text-slate-600">本次运行暂无工具调用或专家派发。图里挂了检索/工具节点、或多智能体协调派发专家时,每次调用会在此列出入参/简报与产出。</p>;
|
||||
}
|
||||
return (
|
||||
<ul className="space-y-1.5 text-xs">
|
||||
{tools.map((t) => (
|
||||
<li key={t.node} className="rounded-md border border-line bg-ink-950/60 px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Wrench className="h-3.5 w-3.5 text-warn" strokeWidth={2} />
|
||||
<span className="font-mono text-[11px] text-slate-200">{t.node.replace(/^tool:/, "")}</span>
|
||||
<Badge tone={t.status === "error" ? "danger" : t.status === "running" ? "accent" : "success"}>
|
||||
{t.status === "error" ? "失败" : t.status === "running" ? "调用中" : "成功"}
|
||||
</Badge>
|
||||
{t.ms != null && t.ms > 0 && <span className="ml-auto font-mono text-[10px] text-slate-500">{t.ms} ms</span>}
|
||||
</div>
|
||||
{t.detail && <p className="mt-1 break-words text-[11px] leading-relaxed text-slate-400">{t.detail}</p>}
|
||||
</li>
|
||||
))}
|
||||
{calls.map((t) => {
|
||||
const isAgent = t.kind === "agent";
|
||||
return (
|
||||
<li key={t.node} className="rounded-md border border-line bg-ink-950/60 px-3 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
{isAgent ? <Users className="h-3.5 w-3.5 text-indigo-400" strokeWidth={2} /> : <Wrench className="h-3.5 w-3.5 text-warn" strokeWidth={2} />}
|
||||
<span className="font-mono text-[11px] text-slate-200">{t.node.replace(/^(tool|agent):/, "")}</span>
|
||||
<Badge tone={isAgent ? "accent" : "neutral"}>{isAgent ? "专家" : "工具"}</Badge>
|
||||
<Badge tone={t.status === "error" ? "danger" : t.status === "running" ? "accent" : "success"}>
|
||||
{t.status === "error" ? "失败" : t.status === "running" ? "调用中" : "成功"}
|
||||
</Badge>
|
||||
{t.ms != null && t.ms > 0 && <span className="ml-auto font-mono text-[10px] text-slate-500">{t.ms} ms</span>}
|
||||
</div>
|
||||
{t.detail && <p className="mt-1 break-words text-[11px] leading-relaxed text-slate-400">{t.detail}</p>}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user