feat(agent): 抽屉与会话页统一成一个对话界面,子会话在父会话里实时可见
- 抽屉和会话页渲染同一套 GoalComposer / AgentRunPanel;抽屉标题即最近会话切换, 头部只有新对话、在页面打开、关闭;去掉右下角悬浮按钮和最小化小窗 - 会话列表合一:对话与巡检混排带类型小标签,搜索 + 一个状态下拉,只列顶层会话 (GET /agent/runs?roots=true,新增可选参数) - 子会话卡片:由镜像的 subagent_* 事件折成,显示任务、状态、当前步骤、用时、调用次数、费用, 可展开看过程、可打开;顶部「N 个子会话运行中」跳到卡片;subagent_completed 加可选 durationMs - 回答去内部字段:汇报规则禁止照抄键值与字段表;展示层 stripInternalFields 把字段行 / 字段表移进技术信息 - 开场建议按页面给 2 条完整问法、点了直接发;指令只留 /先给方案,/重跑 改成「重新运行」按钮 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
dd0f6e2c09
commit
3692793dff
35 files changed
+1454
-1137
No files matched your search
@@ -1,4 +1,4 @@
|
||||
import type { AgentPlanStep, AgentRunEvent, AgentRunEventBody, AgentUsageTotals } from "@umt/contracts";
|
||||
import type { AgentPlanStep, AgentRunEvent, AgentRunEventBody, AgentRunNode, AgentUsageTotals } from "@umt/contracts";
|
||||
import type { AgentRunView, AgentSessionOptions, GoalReport, RunMessage, RunSnapshot } from "~/api/agent.ts";
|
||||
|
||||
// Agent 会话 mock(W2-D 端点就绪前开发用;?mock=1 打开)。事件按时间逐步"到达",模拟流式输出。
|
||||
@@ -14,6 +14,55 @@ type Entry = {
|
||||
waiting?: string | null;
|
||||
};
|
||||
|
||||
/** 子会话事件镜像进父会话(和服务端 subagents.ts 一样:spawned → 每条工具 / 开始 / 结束事件 → completed) */
|
||||
function mirrorChild(parent: Step[], child: Entry, offsetMs: number): Step[] {
|
||||
const out: Step[] = [...parent];
|
||||
const at = (d: number) => offsetMs + d + 100;
|
||||
out.push({
|
||||
delayMs: offsetMs,
|
||||
body: {
|
||||
type: "subagent_spawned",
|
||||
childRunId: child.run.id,
|
||||
goal: child.run.goal,
|
||||
mode: "execute",
|
||||
depth: 1,
|
||||
capabilities: ["site.list", "site.health_check"],
|
||||
budget: {},
|
||||
},
|
||||
});
|
||||
child.steps.forEach((st, i) => {
|
||||
const b = st.body;
|
||||
if (b.type === "tool_call_start" || b.type === "tool_call_end" || b.type === "run_started") {
|
||||
out.push({
|
||||
delayMs: at(st.delayMs),
|
||||
body: {
|
||||
type: "subagent_event",
|
||||
childRunId: child.run.id,
|
||||
childSeq: i,
|
||||
childType: b.type,
|
||||
...(b.type === "run_started" ? {} : { capabilityId: b.capabilityId }),
|
||||
summary: "",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (b.type === "run_completed" || b.type === "run_failed") {
|
||||
out.push({
|
||||
delayMs: at(st.delayMs) + 50,
|
||||
body: {
|
||||
type: "subagent_completed",
|
||||
childRunId: child.run.id,
|
||||
status: b.type === "run_completed" ? "succeeded" : "failed",
|
||||
summary: b.type === "run_completed" ? b.finalText : b.message,
|
||||
usage: b.usage,
|
||||
toolCalls: child.steps.filter((x) => x.body.type === "tool_call_end").length,
|
||||
durationMs: st.delayMs,
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
return out.sort((a, b) => a.delayMs - b.delayMs);
|
||||
}
|
||||
|
||||
const store = new Map<string, Entry>();
|
||||
let seeded = false;
|
||||
|
||||
@@ -470,6 +519,130 @@ function seed() {
|
||||
29 * 60_000,
|
||||
);
|
||||
child.run = { ...child.run, parentRunId: parent.run.id, trigger: "subagent", depth: 1 };
|
||||
{
|
||||
// 父会话第一次看站点之后派出子会话(子会话比父会话晚 1 分钟开始)
|
||||
const spawnAt = parent.steps.findIndex(
|
||||
(x) => x.body.type === "tool_call_end" && x.body.capabilityId === "site.list",
|
||||
);
|
||||
const base = parent.steps[spawnAt]?.delayMs ?? 1000;
|
||||
parent.steps = mirrorChild(
|
||||
[
|
||||
...parent.steps.slice(0, spawnAt + 1),
|
||||
...at0([
|
||||
[base + 50, { type: "text_delta", delta: "派一个子会话去看「憨憨」的抓取记录。" }],
|
||||
[
|
||||
60,
|
||||
{
|
||||
type: "tool_call_start",
|
||||
toolCallId: "sp1",
|
||||
capabilityId: "agent.spawn",
|
||||
inputSummary: '{"goal":"看站点「憨憨」最近的抓取记录"}',
|
||||
recovery: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
60,
|
||||
{
|
||||
type: "tool_call_end",
|
||||
toolCallId: "sp1",
|
||||
capabilityId: "agent.spawn",
|
||||
isError: false,
|
||||
outputSummary: `{"runId":"${child.run.id}"}`,
|
||||
recovery: false,
|
||||
},
|
||||
],
|
||||
]),
|
||||
...parent.steps.slice(spawnAt + 1).map((x) => ({ ...x, delayMs: x.delayMs + 60_000 })),
|
||||
],
|
||||
child,
|
||||
base + 200,
|
||||
);
|
||||
}
|
||||
// 运行中的会话:派出两个子会话,一个很快查完,一个还在跑(看子会话卡片的实时样子)
|
||||
{
|
||||
const goal = "检查所有站点的登录状态和最近的抓取";
|
||||
const live = fixture(15, goal, [], 0);
|
||||
const quick = fixture(16, "看站点「憨憨」最近的抓取记录", [], 0);
|
||||
const slow = fixture(17, "逐个检测站点「观众」「憨憨」的登录状态", [], 0);
|
||||
for (const c of [quick, slow]) c.run = { ...c.run, parentRunId: live.run.id, trigger: "subagent", depth: 1 };
|
||||
quick.startedAt = live.startedAt + 2_000;
|
||||
slow.startedAt = live.startedAt + 2_100;
|
||||
quick.steps = at0([
|
||||
[0, { type: "run_started", goal: quick.run.goal, resumed: false }],
|
||||
...toolPair("q1", "site.list", "{}", '{"items":[{"name":"憨憨"}]}'),
|
||||
[2_000, { type: "text_delta", delta: "「憨憨」最近 24 小时抓取 96 次,全部成功。" }],
|
||||
...toolPair("q2", "site.list", "{}", '{"items":[{"name":"憨憨"}]}'),
|
||||
[
|
||||
3_000,
|
||||
{
|
||||
type: "run_completed",
|
||||
finalText: "「憨憨」最近 24 小时抓取 96 次,全部成功。",
|
||||
usage: { ...USAGE, totalTokens: 8_200, costUsd: 0.006 },
|
||||
},
|
||||
],
|
||||
]);
|
||||
slow.steps = at0([
|
||||
[0, { type: "run_started", goal: slow.run.goal, resumed: false }],
|
||||
...toolPair("w1", "site.list", "{}", '{"items":[{"name":"观众"},{"name":"憨憨"}]}'),
|
||||
[400, { type: "text_delta", delta: "先测「观众」。" }],
|
||||
[
|
||||
100,
|
||||
{
|
||||
type: "tool_call_start",
|
||||
toolCallId: "w2",
|
||||
capabilityId: "site.health_check",
|
||||
inputSummary: '{"site":"观众"}',
|
||||
recovery: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
600_000,
|
||||
{
|
||||
type: "tool_call_end",
|
||||
toolCallId: "w2",
|
||||
capabilityId: "site.health_check",
|
||||
isError: false,
|
||||
outputSummary: "{}",
|
||||
recovery: false,
|
||||
},
|
||||
],
|
||||
[1_000, { type: "run_completed", finalText: "两个站点登录都正常。", usage: USAGE }],
|
||||
]);
|
||||
const spawn = (id: string, g: string, out: string): [number, AgentRunEventBody][] => [
|
||||
[
|
||||
100,
|
||||
{
|
||||
type: "tool_call_start",
|
||||
toolCallId: id,
|
||||
capabilityId: "agent.spawn",
|
||||
inputSummary: `{"goal":"${g}"}`,
|
||||
recovery: false,
|
||||
},
|
||||
],
|
||||
[
|
||||
100,
|
||||
{
|
||||
type: "tool_call_end",
|
||||
toolCallId: id,
|
||||
capabilityId: "agent.spawn",
|
||||
isError: false,
|
||||
outputSummary: `{"runId":"${out}"}`,
|
||||
recovery: false,
|
||||
},
|
||||
],
|
||||
];
|
||||
let steps: Step[] = at0([
|
||||
[0, { type: "run_started", goal, resumed: false }],
|
||||
...textSteps("两件事互不相关,各派一个子会话去查。"),
|
||||
...spawn("sa", quick.run.goal, quick.run.id),
|
||||
...spawn("sb", slow.run.goal, slow.run.id),
|
||||
[620_000, { type: "text_delta", delta: "两个站点登录都正常,「憨憨」最近抓取全部成功。" }],
|
||||
[100, { type: "run_completed", finalText: "两个站点登录都正常。", usage: USAGE }],
|
||||
]);
|
||||
steps = mirrorChild(steps, quick, 2_000);
|
||||
steps = mirrorChild(steps, slow, 2_100);
|
||||
live.steps = steps;
|
||||
}
|
||||
fixture(
|
||||
13,
|
||||
"[上下文 作品:沙丘:第二部 · entityId=74beab51-0000-4000-8000-000000000000] 把 1080p 收进来",
|
||||
@@ -619,7 +792,32 @@ export function mockDetail(id: string, op?: { abort?: boolean; append?: string }
|
||||
e.steps.sort((a, b) => a.delayMs - b.delayMs);
|
||||
}
|
||||
const run = view(e);
|
||||
return { run, messages: e.appended, report: report(e, run) };
|
||||
const children = [...store.values()].filter((c) => c.run.parentRunId === id).map((c) => node(view(c)));
|
||||
return {
|
||||
run,
|
||||
messages: e.appended,
|
||||
report: report(e, run),
|
||||
children,
|
||||
...(run.parentRunId && store.get(run.parentRunId) ? { parent: node(view(store.get(run.parentRunId)!)) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function node(r: AgentRunView): AgentRunNode {
|
||||
return {
|
||||
id: r.id,
|
||||
parentRunId: r.parentRunId,
|
||||
goal: r.goal,
|
||||
status: r.status,
|
||||
depth: r.depth,
|
||||
spawnReason: r.spawnReason,
|
||||
toolCallCount: r.toolCallCount,
|
||||
usage: r.usage,
|
||||
summary: r.summary,
|
||||
capabilities: [],
|
||||
budget: r.budget,
|
||||
createdAt: r.createdAt,
|
||||
finishedAt: r.finishedAt,
|
||||
};
|
||||
}
|
||||
|
||||
/** 服务端目标汇报的 mock:只按"业务表"口径给数,模型文本单独放 modelText */
|
||||
|
||||
Reference in new issue
Block a user