feat(memory): P1 长期记忆升级 —— 异步攒批 Consolidate + 软删 + importance/last_seen #1
+9
-7
@@ -25,13 +25,15 @@
|
|||||||
- **验收 ✅ live(deepseek)**:跑题→0.40 poor→**触发纠偏(0.40→0.55)**;截断→0.55 warn;好答案→1.00 ok。
|
- **验收 ✅ live(deepseek)**:跑题→0.40 poor→**触发纠偏(0.40→0.55)**;截断→0.55 warn;好答案→1.00 ok。
|
||||||
校准前这三个全是 1.00→ok,恒温器从没触发。
|
校准前这三个全是 1.00→ok,恒温器从没触发。
|
||||||
|
|
||||||
### [ ] T0.2 多智能体进 Studio(多智能体 v2)
|
### [x] T0.2 多智能体进 Studio(多智能体 v2)✅
|
||||||
现状:coordinator 后端已通(见 MULTI_AGENT.md),但画布拖不出。
|
现状:coordinator 后端已通(见 MULTI_AGENT.md),但画布拖不出。
|
||||||
- [ ] nodeCatalog 加 `coordinator` 节点 + 新 `agentList` 字段(子智能体卡片:名/用途/系统提示词/工具多选)
|
- [x] nodeCatalog 加 `coordinator` 节点(「多智能体协调」,indigo)+ 新 `agentList` 字段类型 + Specialist 接口
|
||||||
- [ ] StudioView 渲染+编辑 agentList,校验(≥1 专家、名唯一)
|
- [x] Inspector `AgentListField`:可增删的专家卡片(名/用途/系统提示词/工具逗号分隔)
|
||||||
- [ ] 运行轨迹:专家派发渲染成可展开子节点(显示 brief + 精炼结论)
|
- [x] dsl 校验:agentList 需 ≥1 个有名字的专家 + 名字不重复
|
||||||
- [ ] preview/真机验证:UI 搭两专家协调任务跑通
|
- [x] 运行轨迹「工具调用」面板纳入专家派发(kind=agent)→ 改名「工具/专家」,专家项 indigo 标识
|
||||||
- 验收:用户能在 Studio 拖出协调者图并运行,轨迹看到并行派发 + 综合。
|
- [x] 后端验证:UI 同款 DSL 结构提交可跑(agents 数组经 exportDsl 透传 → parseSpecialists)
|
||||||
|
- 验收 ✅:Studio 左栏出现「多智能体协调」节点,检查器配专家卡片,导出 DSL 对齐后端;
|
||||||
|
轨迹/工具面板能看到专家派发。(真机由用户在 wails 窗口确认)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -89,7 +91,7 @@
|
|||||||
|
|
||||||
| Tier | 完成 / 总 |
|
| Tier | 完成 / 总 |
|
||||||
|---|---|
|
|---|---|
|
||||||
| T0 激活 | 1 / 2 |
|
| T0 激活 | 2 / 2 ✅ |
|
||||||
| T1 收口 | 0 / 2 |
|
| T1 收口 | 0 / 2 |
|
||||||
| T2 深化 | 0 / 3 |
|
| T2 深化 | 0 / 3 |
|
||||||
| T3 硬化 ⏸ | 0 / 5 |
|
| T3 硬化 ⏸ | 0 / 5 |
|
||||||
|
|||||||
@@ -52,6 +52,16 @@ export function validate(nodes: Node[], edges: Edge[]): Issue[] {
|
|||||||
.filter((f) => f.required)
|
.filter((f) => f.required)
|
||||||
.forEach((f) => {
|
.forEach((f) => {
|
||||||
const v = d.config?.[f.key];
|
const v = d.config?.[f.key];
|
||||||
|
if (f.type === "agentList") {
|
||||||
|
const arr = Array.isArray(v) ? (v as Array<{ name?: string }>) : [];
|
||||||
|
const names = arr.map((s) => s?.name?.trim()).filter(Boolean) as string[];
|
||||||
|
if (names.length === 0) {
|
||||||
|
issues.push({ level: "warn", msg: `节点「${d.label || k.label}」至少需要一个有名字的专家` });
|
||||||
|
} else if (new Set(names).size !== names.length) {
|
||||||
|
issues.push({ level: "warn", msg: `节点「${d.label || k.label}」专家名有重复` });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (v === undefined || v === "" || String(v).startsWith("(未")) {
|
if (v === undefined || v === "" || String(v).startsWith("(未")) {
|
||||||
issues.push({ level: "warn", msg: `节点「${d.label || k.label}」缺必填项:${f.label}` });
|
issues.push({ level: "warn", msg: `节点「${d.label || k.label}」缺必填项:${f.label}` });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,52 @@
|
|||||||
import type { Node } from "@xyflow/react";
|
import type { Node } from "@xyflow/react";
|
||||||
import { NODE_KINDS } from "./nodeCatalog";
|
import { NODE_KINDS, type Specialist } from "./nodeCatalog";
|
||||||
|
|
||||||
const inputCls =
|
const inputCls =
|
||||||
"mt-1 w-full rounded-md border border-line bg-ink-800 px-2 py-1 text-sm text-slate-200 focus:border-violet-500/60 focus:outline-none";
|
"mt-1 w-full rounded-md border border-line bg-ink-800 px-2 py-1 text-sm text-slate-200 focus:border-violet-500/60 focus:outline-none";
|
||||||
|
const cardCls =
|
||||||
|
"w-full rounded border border-line bg-ink-800 px-2 py-1 text-[12px] text-slate-200 placeholder:text-slate-600 focus:border-indigo-500/60 focus:outline-none";
|
||||||
|
|
||||||
|
// AgentListField:多智能体协调节点的「子智能体(专家)」配置 —— 可增删的专家卡片。
|
||||||
|
// 每张卡片对齐后端 parseSpecialists 的 {name, use, system, tools}。
|
||||||
|
function AgentListField({ value, onChange }: { value: Specialist[]; onChange: (next: Specialist[]) => void }) {
|
||||||
|
const list = Array.isArray(value) ? value : [];
|
||||||
|
const update = (i: number, patch: Partial<Specialist>) => onChange(list.map((s, idx) => (idx === i ? { ...s, ...patch } : s)));
|
||||||
|
const add = () => onChange([...list, { name: "", use: "", system: "", tools: [] }]);
|
||||||
|
const remove = (i: number) => onChange(list.filter((_, idx) => idx !== i));
|
||||||
|
return (
|
||||||
|
<div className="mt-1 space-y-2">
|
||||||
|
{list.map((s, i) => (
|
||||||
|
<div key={i} className="space-y-1.5 rounded-md border border-indigo-500/20 bg-ink-950/40 p-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input className={cardCls} placeholder="专家名(如 legal)" value={s.name} onChange={(e) => update(i, { name: e.target.value })} />
|
||||||
|
<button onClick={() => remove(i)} className="shrink-0 text-[11px] text-rose-400 hover:underline">
|
||||||
|
删除
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<input className={cardCls} placeholder="用途/擅长(lead 据此判断何时派给它)" value={s.use} onChange={(e) => update(i, { use: e.target.value })} />
|
||||||
|
<textarea
|
||||||
|
className={`${cardCls} h-12 resize-none`}
|
||||||
|
placeholder="系统提示词(专家人设)"
|
||||||
|
value={s.system}
|
||||||
|
onChange={(e) => update(i, { system: e.target.value })}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
className={cardCls}
|
||||||
|
placeholder="工具(逗号分隔,可空;如 wiki_search, kb_search)"
|
||||||
|
value={(s.tools ?? []).join(", ")}
|
||||||
|
onChange={(e) => update(i, { tools: e.target.value.split(",").map((t) => t.trim()).filter(Boolean) })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button
|
||||||
|
onClick={add}
|
||||||
|
className="w-full rounded border border-dashed border-line py-1 text-[11px] text-slate-400 hover:border-indigo-500/50 hover:text-indigo-300"
|
||||||
|
>
|
||||||
|
+ 添加专家
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 右检查器:按选中节点的类型渲染配置表单;空选时显示图级提示(深色)。
|
// 右检查器:按选中节点的类型渲染配置表单;空选时显示图级提示(深色)。
|
||||||
export function Inspector({
|
export function Inspector({
|
||||||
@@ -49,7 +93,9 @@ export function Inspector({
|
|||||||
<label key={f.key} className="text-xs text-slate-500">
|
<label key={f.key} className="text-xs text-slate-500">
|
||||||
{f.label}
|
{f.label}
|
||||||
{f.required && <span className="text-rose-400"> *</span>}
|
{f.required && <span className="text-rose-400"> *</span>}
|
||||||
{f.type === "select" ? (
|
{f.type === "agentList" ? (
|
||||||
|
<AgentListField value={(v as Specialist[]) ?? []} onChange={(next) => setConfig(f.key, next)} />
|
||||||
|
) : f.type === "select" ? (
|
||||||
(() => {
|
(() => {
|
||||||
const dyn = dynamicOptions?.[f.key];
|
const dyn = dynamicOptions?.[f.key];
|
||||||
const opts = dyn && dyn.length ? dyn : f.options ?? [];
|
const opts = dyn && dyn.length ? dyn : f.options ?? [];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// 节点类型目录 —— Studio 画布的"类型化节点"定义(决定面板、配色、检查器字段)。
|
// 节点类型目录 —— Studio 画布的"类型化节点"定义(决定面板、配色、检查器字段)。
|
||||||
// 与后端 DSL / Eino 图节点对齐:输入 / 检索(RAG) / Agent / 工具 / 记忆 / 分支 / 并行 / 汇聚 / 渲染 / 输出。
|
// 与后端 DSL / Eino 图节点对齐:输入 / 检索(RAG) / Agent / 工具 / 记忆 / 分支 / 并行 / 汇聚 / 渲染 / 输出。
|
||||||
|
|
||||||
export type FieldType = "text" | "textarea" | "number" | "select" | "checkbox";
|
export type FieldType = "text" | "textarea" | "number" | "select" | "checkbox" | "agentList";
|
||||||
|
|
||||||
export interface Field {
|
export interface Field {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -12,6 +12,14 @@ export interface Field {
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Specialist 是协调者节点里的一个子智能体(专家)配置,与后端 parseSpecialists 对齐。
|
||||||
|
export interface Specialist {
|
||||||
|
name: string; // 工具名(lead 据此调用)
|
||||||
|
use: string; // 用途/擅长,作为工具描述供 lead 判断何时派发
|
||||||
|
system: string; // 专家人设
|
||||||
|
tools: string[]; // 该专家可用的 MCP 工具名子集(空=纯对话专家)
|
||||||
|
}
|
||||||
|
|
||||||
export interface NodeKind {
|
export interface NodeKind {
|
||||||
kind: string;
|
kind: string;
|
||||||
label: string;
|
label: string;
|
||||||
@@ -62,6 +70,18 @@ export const NODE_KINDS: Record<string, NodeKind> = {
|
|||||||
],
|
],
|
||||||
defaults: { model: "占位 Pool", system: "", temperature: 0.7, autonomous: false },
|
defaults: { model: "占位 Pool", system: "", temperature: 0.7, autonomous: false },
|
||||||
},
|
},
|
||||||
|
coordinator: {
|
||||||
|
kind: "coordinator",
|
||||||
|
label: "多智能体协调",
|
||||||
|
accent: "border-l-indigo-500",
|
||||||
|
badge: "bg-indigo-100 text-indigo-700",
|
||||||
|
desc: "协调者自主把子任务派给专家(agent-as-tool)再综合",
|
||||||
|
fields: [
|
||||||
|
{ key: "system", label: "任务背景(可选)", type: "textarea", placeholder: "给协调者的任务背景…" },
|
||||||
|
{ key: "agents", label: "子智能体(专家)", type: "agentList", required: true },
|
||||||
|
],
|
||||||
|
defaults: { system: "", agents: [{ name: "", use: "", system: "", tools: [] }] },
|
||||||
|
},
|
||||||
tool: {
|
tool: {
|
||||||
kind: "tool",
|
kind: "tool",
|
||||||
label: "工具",
|
label: "工具",
|
||||||
@@ -159,6 +179,7 @@ export const NODE_ORDER = [
|
|||||||
"input",
|
"input",
|
||||||
"retriever",
|
"retriever",
|
||||||
"agent",
|
"agent",
|
||||||
|
"coordinator",
|
||||||
"tool",
|
"tool",
|
||||||
"memory",
|
"memory",
|
||||||
"branch",
|
"branch",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useCallback, useEffect, useState } from "react";
|
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 { ExecTrace } from "../components/ExecTrace";
|
||||||
import { Markdown } from "../components/Markdown";
|
import { Markdown } from "../components/Markdown";
|
||||||
import { ChartView } from "../components/ChartView";
|
import { ChartView } from "../components/ChartView";
|
||||||
@@ -66,10 +66,11 @@ export function RunsView({ run }: { run: RunState }) {
|
|||||||
const liveActive = run.taskId && run.phase !== "idle";
|
const liveActive = run.taskId && run.phase !== "idle";
|
||||||
const cur = sel ? replay : run;
|
const cur = sel ? replay : run;
|
||||||
const nodes = deriveNodes(cur.exec);
|
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>[] = [
|
const tabs: TabDef<DetailTab>[] = [
|
||||||
{ key: "trace", label: "执行轨迹", count: nodes.length },
|
{ key: "trace", label: "执行轨迹", count: nodes.length },
|
||||||
{ key: "tools", label: "工具调用", count: tools.length },
|
{ key: "tools", label: "工具/专家", count: calls.length },
|
||||||
{ key: "eval", label: "评测" },
|
{ key: "eval", label: "评测" },
|
||||||
];
|
];
|
||||||
const empty = !cur.taskId && cur.exec.length === 0 && !cur.output;
|
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>
|
<h1 className="text-lg font-semibold text-slate-100">运行 · 观测</h1>
|
||||||
<p className="mt-1 text-xs text-slate-500">运行历史复盘:选一次运行,左查轨迹/工具/评测(切换标签),右看模型输出。</p>
|
<p className="mt-1 text-xs text-slate-500">运行历史复盘:选一次运行,左查轨迹/工具/评测(切换标签),右看模型输出。</p>
|
||||||
</div>
|
</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>
|
</header>
|
||||||
|
|
||||||
<div className="grid min-h-0 flex-1 grid-cols-[240px_1fr_1fr] gap-3">
|
<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 }) {
|
function ToolCalls({ run }: { run: RunState }) {
|
||||||
const tools = deriveNodes(run.exec).filter((n) => n.kind === "tool");
|
const calls = deriveNodes(run.exec).filter((n) => n.kind === "tool" || n.kind === "agent");
|
||||||
if (tools.length === 0) {
|
if (calls.length === 0) {
|
||||||
return <p className="text-xs text-slate-600">本次运行暂无工具调用。图里挂了检索/工具节点,或报告挂了知识库时,每次工具调用会在此列出入参与产出。</p>;
|
return <p className="text-xs text-slate-600">本次运行暂无工具调用或专家派发。图里挂了检索/工具节点、或多智能体协调派发专家时,每次调用会在此列出入参/简报与产出。</p>;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ul className="space-y-1.5 text-xs">
|
<ul className="space-y-1.5 text-xs">
|
||||||
{tools.map((t) => (
|
{calls.map((t) => {
|
||||||
<li key={t.node} className="rounded-md border border-line bg-ink-950/60 px-3 py-2">
|
const isAgent = t.kind === "agent";
|
||||||
<div className="flex items-center gap-2">
|
return (
|
||||||
<Wrench className="h-3.5 w-3.5 text-warn" strokeWidth={2} />
|
<li key={t.node} className="rounded-md border border-line bg-ink-950/60 px-3 py-2">
|
||||||
<span className="font-mono text-[11px] text-slate-200">{t.node.replace(/^tool:/, "")}</span>
|
<div className="flex items-center gap-2">
|
||||||
<Badge tone={t.status === "error" ? "danger" : t.status === "running" ? "accent" : "success"}>
|
{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} />}
|
||||||
{t.status === "error" ? "失败" : t.status === "running" ? "调用中" : "成功"}
|
<span className="font-mono text-[11px] text-slate-200">{t.node.replace(/^(tool|agent):/, "")}</span>
|
||||||
</Badge>
|
<Badge tone={isAgent ? "accent" : "neutral"}>{isAgent ? "专家" : "工具"}</Badge>
|
||||||
{t.ms != null && t.ms > 0 && <span className="ml-auto font-mono text-[10px] text-slate-500">{t.ms} ms</span>}
|
<Badge tone={t.status === "error" ? "danger" : t.status === "running" ? "accent" : "success"}>
|
||||||
</div>
|
{t.status === "error" ? "失败" : t.status === "running" ? "调用中" : "成功"}
|
||||||
{t.detail && <p className="mt-1 break-words text-[11px] leading-relaxed text-slate-400">{t.detail}</p>}
|
</Badge>
|
||||||
</li>
|
{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>
|
</ul>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user