fix: align AgentRun trace rendering (#665)

Co-authored-by: Codex <codex@local>
This commit is contained in:
Lyon
2026-06-01 23:58:06 +08:00
committed by GitHub
parent 24060853e7
commit 5dc263e10a
8 changed files with 142 additions and 13 deletions
+1 -1
View File
@@ -25,7 +25,7 @@
- `client workbench restore/status/watch/reset` 是账号 workspace 的非视觉入口:`restore/status` 对应 `GET /v1/workbench/workspace``watch` 对应 `/events?afterRevision=``reset --confirm` 对应服务端 reset。输出必须显示 workspace revision、selected conversation/session、active trace 和本地 state file,且不得保存 password、session token 原文以外的 Secret 值。
- `client agent send` 是 Cloud Web Code Agent composer 的非视觉等价入口。它必须支持 `--from-trace``--conversation-id``--session-id``--thread-id``--retry-of`,并在输出中返回 redacted continuation 摘要,证明本次 CLI 请求是否覆盖 Web 的继续会话路径。浏览器 issue 中已经给出 traceId 时,复现命令优先使用 `--from-trace <traceId>`,让 CLI 先走 `/v1/agent/chat/inspect` 读取 Web 上下文,再提交同源 `/v1/agent/chat`
- `client agent send` 默认先恢复账号 workspace,再向 `/v1/agent/chat` 发送 `workspaceId``expectedWorkspaceRevision`;服务端接受后 CLI 保存新的 workspace revision,终态轮询后再 PATCH workspace 清理终态 `activeTraceId`。只有显式 `--no-workspace` 才跳过这一默认恢复路径。
- `client agent trace <traceId> --render web` 必须调用 Cloud Web trace row 的同一纯转换路径,输出 `render="web"`、renderer 标识、source event count、rendered row count 和 row 摘要。浏览器 trace 展示错乱时,必须先用该 CLI 入口确认 Web 渲染转换是否已经乱序、重复、缺 final response吞掉关键 row,再继续修浏览器 DOM/CSS。
- `client agent trace <traceId> --render web` 必须调用 Cloud Web trace row 的同一纯转换路径,输出 `render="web"`、renderer 标识、source event count、rendered row count、默认压制的 noise event count 和 row 摘要。浏览器 trace 展示错乱时,必须先用该 CLI 入口确认 Web 渲染转换是否已经乱序、重复、缺 final response吞掉关键 row 或只显示泛化 tool call,再继续修浏览器 DOM/CSS。
- AgentRun v0.1 短连接 runner 已要求支持同 run/runner 多轮 command。CLI 仍应把 Web 提交的 `conversationId/sessionId/threadId` 原样送到 Cloud Web API,用于验证 adapter 是否在 runner lease 有效时复用同一个 AgentRun `runId` / `jobName` 并创建新 `commandId`;每轮都新建 runner 或重新 bundle 不是通过状态,trace 中的原因说明只能用于定位。
- `client harness``client harness-ops``client harness-opt` 吸收 G14 harness-ops 的短连接业务能力:health、submit、result、trace、wait 和 audit。
- harness 系列命令只调用 Cloud Web/Code Agent 同源 API,不创建镜像、Job、常驻服务,也不执行 hot-sync、kubectl cp 或硬编码 namespace/pod 的运行面写路径。
+1 -1
View File
@@ -9,7 +9,7 @@
- 为浏览器提供同源代理,避免前端直接跨域调用内部 ClusterIP。
- Cloud Web 与 `hwlab-cli client` 必须共享同一组非视觉业务 API。浏览器遇到的 Code Agent continuation、trace/result、device-pod list/status 和 device-pod job 问题,必须能通过 `hwlab-cli client` 走同一 `19666` Cloud Web path 复现;不能让 CLI 长期绕到 `19667` Cloud API 后把 Web 路径缺口误判为业务已通过。
- 浏览器启动后必须从 `GET /v1/workbench/workspace` hydrate 账号 workspace;同一个账号在多个浏览器标签页、多个浏览器或 CLI profile 中应看到同一个 `workspaceId`、selected conversation/session/thread、provider profile 和 active trace。浏览器 localStorage 只能作为短期缓存,并必须绑定 actor,不能作为 workspace authority。
- Cloud Web trace 展示与 `hwlab-cli client agent trace --render web` 必须共享同一套 trace row 纯转换路径。Web 发生 row 顺序错乱、final response 缺失、assistant 消息被吞或噪声事件淹没时,先用 CLI 输出同一渲染 row 摘要复现;CLI 可复现说明是 trace row 转换问题,CLI 不可复现再进入 DOM/CSS/滚动状态调查。
- Cloud Web trace 展示与 `hwlab-cli client agent trace --render web` 必须共享同一套 trace row 纯转换路径。Web 发生 row 顺序错乱、final response 缺失、assistant 消息被吞、tool call 只显示泛化占位或噪声事件淹没时,先用 CLI 输出同一渲染 row 摘要和 noise event count 复现;CLI 可复现说明是 trace row 转换问题,CLI 不可复现再进入 DOM/CSS/滚动状态调查。默认展示应压制 AgentRun backend echo、token/rate-limit/status/terminal echo 等低价值事件,但原始 trace JSON 仍必须保留用于 `--full`/下载排障。
- Code Agent result `completed` 只有在同时包含真实 provider/model/trace/conversation 元数据、`providerTrace` 和可展示的 final assistant response 时,才能被 Web 标记为真实完成;`provider=agentrun-v01` 只是执行基础设施标识,不得替代上游 provider/model,也不得把 SOURCE、fixture、echo、mock 或 stub 当成 DEV-LIVE 完成。
- 同一 conversation/session 的后续用户消息必须在 AgentRun runner lease 有效时复用已存在的 AgentRun run/runner 继续新 command/turn;只有 runner 不可用、已过期或协议明确要求新 runner 时才重新 bundle 和启动 runner。每条消息都重新 bundle/runner 属于 v0.2 AgentRun 接入缺口,不能只靠 trace 显示原因当成已完成。
+37 -1
View File
@@ -798,7 +798,19 @@ function mapAgentRunEvent(event, mapping = {}) {
return { ...base, type: "assistant", status: "running", label: "agentrun:assistant:message", message: textPayload(payload, "assistant message"), text: textPayload(payload, "") };
}
if (event.type === "tool_call") {
return { ...base, type: "tool", status: String(payload.status ?? "running"), label: `agentrun:tool:${String(payload.name ?? payload.method ?? "call")}`, message: textPayload(payload, "tool call") };
const commandExecution = agentRunCommandExecutionEvent(base, payload);
if (commandExecution) return commandExecution;
return {
...base,
type: "tool_call",
status: String(payload.status ?? (payload.method === "item/completed" ? "completed" : "running")),
label: `agentrun:tool:${String(payload.name ?? payload.method ?? "call")}`,
itemId: payload.item?.id ?? payload.itemId ?? null,
toolName: payload.item?.type ?? payload.name ?? payload.method ?? "call",
message: textPayload(payload, "tool call"),
outputBytes: typeof payload.outputBytes === "number" ? payload.outputBytes : payload.summary?.outputBytes,
outputTruncated: payload.outputTruncated === true || payload.summary?.outputTruncated === true
};
}
if (event.type === "command_output") {
const stream = payload.stream === "stderr" ? "stderr" : "stdout";
@@ -817,6 +829,30 @@ function mapAgentRunEvent(event, mapping = {}) {
return { ...base, type: "backend", status: "running", label: `agentrun:event:${String(event.type ?? "unknown")}`, message: textPayload(payload, "AgentRun event") };
}
function agentRunCommandExecutionEvent(base, payload = {}) {
const item = payload.item && typeof payload.item === "object" ? payload.item : null;
if (item?.type !== "commandExecution") return null;
const method = String(payload.method ?? "");
const completed = method === "item/completed" || item.status === "completed";
const output = firstNonEmpty(item.aggregatedOutput, item.stdout, item.output, payload.summary?.text, payload.itemPreview);
return {
...base,
type: "tool_call",
status: completed ? "completed" : "started",
label: completed ? "item/commandExecution:completed" : "item/commandExecution:started",
toolName: "commandExecution",
itemId: item.id ?? null,
command: item.command ?? item.commandLine ?? null,
exitCode: Number.isInteger(item.exitCode) ? item.exitCode : undefined,
durationMs: typeof item.durationMs === "number" ? item.durationMs : undefined,
outputBytes: typeof payload.outputBytes === "number" ? payload.outputBytes : payload.summary?.outputBytes,
stdoutSummary: output,
outputSummary: output,
outputTruncated: payload.outputTruncated === true || payload.summary?.outputTruncated === true,
message: output || textPayload(payload, "commandExecution")
};
}
async function agentRunJson(fetchImpl, managerUrl, path, { method = "GET", body, timeoutMs = 20_000 } = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), timeoutMs);
+12 -4
View File
@@ -214,8 +214,12 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
const second = url.searchParams.get("afterSeq") === "3" || calls.some((call) => call.path === "/api/v1/runs/run_hwlab_adapter/commands/cmd_hwlab_adapter_second/result");
return send({ items: [
{ id: second ? "evt_4" : "evt_1", runId: "run_hwlab_adapter", seq: second ? 4 : 1, type: "backend_status", payload: { phase: second ? "turn-started" : "runner-job-created", commandId: second ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", attemptId: "attempt_hwlab_adapter", jobName: "agentrun-v01-runner-hwlab-adapter", namespace: "agentrun-v01" }, createdAt: "2026-06-01T00:00:00.000Z" },
{ id: second ? "evt_5" : "evt_2", runId: "run_hwlab_adapter", seq: second ? 5 : 2, type: "assistant_message", payload: { commandId: second ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", text: second ? "AgentRun adapter 复用已有 runner 完成第二轮。" : "AgentRun adapter 已接管 HWLAB Code Agent。" }, createdAt: "2026-06-01T00:00:01.000Z" },
{ id: second ? "evt_6" : "evt_3", runId: "run_hwlab_adapter", seq: second ? 6 : 3, type: "terminal_status", payload: { commandId: second ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:02.000Z" }
...(second ? [] : [
{ id: "evt_tool", runId: "run_hwlab_adapter", seq: 2, type: "tool_call", payload: { method: "item/completed", item: { type: "commandExecution", id: "call_agentrun_tool", command: "/bin/sh -lc 'bun tools/device-pod-cli.ts profile list --api-base-url http://74.48.78.17:19666'", status: "completed", exitCode: 0, durationMs: 708, aggregatedOutput: '{"ok":true,"action":"profile.list"}' }, summary: { outputBytes: 42, outputTruncated: false }, commandId: "cmd_hwlab_adapter", runnerId: "runner_hwlab_adapter", attemptId: "attempt_hwlab_adapter" }, createdAt: "2026-06-01T00:00:00.500Z" },
{ id: "evt_noise", runId: "run_hwlab_adapter", seq: 3, type: "backend_status", payload: { phase: "thread/status/changed", commandId: "cmd_hwlab_adapter" }, createdAt: "2026-06-01T00:00:00.750Z" }
]),
{ id: second ? "evt_5" : "evt_2", runId: "run_hwlab_adapter", seq: second ? 5 : 4, type: "assistant_message", payload: { commandId: second ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", text: second ? "AgentRun adapter 复用已有 runner 完成第二轮。" : "AgentRun adapter 已接管 HWLAB Code Agent。" }, createdAt: "2026-06-01T00:00:01.000Z" },
{ id: second ? "evt_6" : "evt_3", runId: "run_hwlab_adapter", seq: second ? 6 : 5, type: "terminal_status", payload: { commandId: second ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:02.000Z" }
] });
}
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands/cmd_hwlab_adapter/result") {
@@ -232,8 +236,8 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
terminalStatus: "completed",
completed: true,
reply: "AgentRun adapter 已接管 HWLAB Code Agent。",
lastSeq: 3,
eventCount: 3,
lastSeq: 5,
eventCount: 5,
sessionRef: { sessionId: "ses_server-test-agentrun", conversationId: "cnv_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" }
});
}
@@ -335,6 +339,10 @@ test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapte
const traceBody = await trace.json();
assert.ok(traceBody.events.some((event) => event.runId === "run_hwlab_adapter"));
assert.ok(traceBody.events.some((event) => event.label === "agentrun:assistant:message"));
const commandTraceEvent = traceBody.events.find((event) => event.label === "item/commandExecution:completed");
assert.equal(commandTraceEvent.toolName, "commandExecution");
assert.match(commandTraceEvent.command, /device-pod-cli\.ts profile list/u);
assert.match(commandTraceEvent.stdoutSummary, /profile\.list/u);
const secondTraceId = "trc_server-test-agentrun-adapter-second";
const second = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
+25
View File
@@ -443,6 +443,31 @@ test("hwlab-cli client agent trace can render with the Web trace row path", asyn
assert.ok(result.payload.body.rows.some((row: any) => //u.test(row.header)));
});
test("hwlab-cli Web trace render reports suppressed noise and command tool details", async () => {
const result = await runHwlabCli(["client", "agent", "trace", "trc_render_agentrun", "--base-url", "http://web.test", "--cookie", "hwlab_session=session-a", "--render", "web"], {
fetchImpl: async () => new Response(JSON.stringify({
status: "completed",
traceId: "trc_render_agentrun",
events: [
{ traceId: "trc_render_agentrun", seq: 1, label: "agentrun:backend:command-created", status: "running", type: "backend", createdAt: "2026-06-01T13:00:00.000Z" },
{ traceId: "trc_render_agentrun", seq: 2, label: "agentrun:backend:thread/status/changed", status: "running", type: "backend", createdAt: "2026-06-01T13:00:01.000Z" },
{ traceId: "trc_render_agentrun", seq: 3, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", itemId: "call_1", command: "/bin/sh -lc 'bun tools/device-pod-cli.ts profile list --api-base-url http://74.48.78.17:19666'", exitCode: 0, stdoutSummary: '{"ok":true,"action":"profile.list"}', createdAt: "2026-06-01T13:00:02.000Z" },
{ traceId: "trc_render_agentrun", seq: 4, label: "agentrun:assistant:message", type: "assistant", status: "running", message: "当前可见 device-pod。", createdAt: "2026-06-01T13:00:03.000Z" }
]
}), { status: 200 })
});
assert.equal(result.exitCode, 0);
assert.equal(result.payload.body.render, "web");
assert.equal(result.payload.body.sourceEventCount, 4);
assert.equal(result.payload.body.noiseEventCount, 2);
const text = JSON.stringify(result.payload.body.rows);
assert.equal(text.includes("thread/status/changed"), false);
assert.match(text, /device-pod-cli\.ts profile list/u);
assert.match(text, /profile\.list/u);
assert.match(text, / device-pod/u);
});
test("hwlab-cli client agent trace auto logs in when protected trace has no local session", async () => {
const cwd = await mkdtemp(path.join(os.tmpdir(), "hwlab-cli-client-auth-"));
const calls: any[] = [];
+4 -1
View File
@@ -1,7 +1,7 @@
import { createHash } from "node:crypto";
import { chmod, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import path from "node:path";
import { traceDisplayRows } from "../../web/hwlab-cloud-web/app-trace.ts";
import { traceDisplayRows, traceNoiseEventCount } from "../../web/hwlab-cloud-web/app-trace.ts";
const VERSION = "0.2.0-client";
const CLI_NAME = "hwlab-cli";
@@ -1697,6 +1697,7 @@ function traceBodyForCli(body: any, parsed: ParsedArgs) {
function webTraceRenderBody(traceObject: any, parsed: ParsedArgs) {
const events = Array.isArray(traceObject?.events) ? traceObject.events : [];
const rows = traceDisplayRows(traceObject ?? {}, events);
const noiseEventCount = traceNoiseEventCount(events);
const rowLimit = Math.max(1, Math.min(numberOption(parsed.limit) ?? 80, 500));
const rowTail = parsed.full === true ? rows : rows.slice(-rowLimit);
return pruneUndefined({
@@ -1707,6 +1708,8 @@ function webTraceRenderBody(traceObject: any, parsed: ParsedArgs) {
sourceEventCount: traceObject?.eventCount ?? events.length,
renderedRowCount: rows.length,
returnedRowCount: rowTail.length,
noiseEventCount,
omittedNoiseCount: noiseEventCount,
rowLimit: parsed.full === true ? null : rowLimit,
eventCountMismatch: Number.isInteger(traceObject?.eventCount) && traceObject.eventCount !== events.length ? {
declared: traceObject.eventCount,
+43 -1
View File
@@ -1,7 +1,7 @@
import assert from "node:assert/strict";
import test from "node:test";
import { traceDisplayRows } from "./app-trace.ts";
import { traceDisplayRows, traceNoiseEventCount } from "./app-trace.ts";
test("trace display rows render request, setup, command, assistant markdown, and completion path", () => {
const trace = {
@@ -67,6 +67,48 @@ test("trace display rows render assistant item text when stream snapshot is abse
assert.match(text, / device-pod/u);
});
test("trace display rows suppress low-value AgentRun backend noise", () => {
const events = [
event(1, "agentrun:request:accepted", { message: "accepted" }),
event(2, "agentrun:backend:command-created"),
event(3, "agentrun:backend:thread/status/changed"),
event(4, "agentrun:backend:thread/tokenUsage/updated"),
event(5, "agentrun:backend:account/rateLimits/updated"),
event(6, "agentrun:backend:codex-app-server-starting"),
commandEvent(7, "item/commandExecution:completed", {
itemId: "call_1",
command: "/bin/sh -lc 'bun tools/device-pod-cli.ts profile list --api-base-url http://74.48.78.17:19666'",
status: "completed",
exitCode: 0,
durationMs: 708,
stdoutSummary: '{"ok":true,"action":"profile.list","body":{"devicePods":[{"devicePodId":"D601-F103-V2"}]}}'
}),
event(8, "agentrun:output:stdout", {
type: "output",
status: "running",
message: '{"ok":true,"action":"profile.list"}'
}),
event(9, "agentrun:assistant:message", {
type: "assistant",
status: "running",
message: "当前可见 1 个 device-pod。"
}),
event(10, "agentrun:terminal:completed", { type: "result", terminal: true, status: "completed" })
];
const rows = traceDisplayRows({ traceId: "trc_agentrun_noise", events }, events);
const text = rows.map((row) => `${row.header}\n${row.body ?? ""}`).join("\n---\n");
assert.equal(traceNoiseEventCount(events), 4);
assert.equal(/thread\/status\/changed/u.test(text), false);
assert.equal(/tokenUsage/u.test(text), false);
assert.match(text, //u);
assert.match(text, /device-pod-cli\.ts profile list/u);
assert.match(text, /profile\.list/u);
assert.match(text, //u);
assert.match(text, / 1 device-pod/u);
});
function traceEvents() {
return [
event(1, "request:accepted", { promptSummary: "请检查 trace" }),
+19 -4
View File
@@ -345,6 +345,10 @@ export function traceDisplayRows(trace, events) {
return rows.length > 0 ? rows : events.map((event) => traceDisplayRow(trace, event, { includeNoise: true })).filter(Boolean);
}
export function traceNoiseEventCount(events) {
return Array.isArray(events) ? events.filter((event) => isNoisyTraceEvent(event)).length : 0;
}
function compactTraceTextTail(text, maxLength) {
const clean = cleanTraceText(text);
if (clean.length <= maxLength) return clean;
@@ -909,12 +913,12 @@ function isCommandExecutionTraceEvent(event) {
function isAssistantMessageTraceEvent(event) {
const label = String(event?.label ?? "");
return event?.type === "assistant_message" || label.startsWith("assistant:");
return event?.type === "assistant_message" || event?.type === "assistant" || label.startsWith("assistant:") || label === "agentrun:assistant:message";
}
function isTerminalAssistantTraceEvent(event) {
const label = String(event?.label ?? "");
return event?.terminal === true || label === "assistant:completed";
return event?.terminal === true || label === "assistant:completed" || label === "agentrun:assistant:message";
}
function isRequestTraceEvent(event) {
@@ -926,12 +930,13 @@ function isRequestTraceEvent(event) {
function isSetupTraceEvent(event) {
const label = String(event?.label ?? "");
return /^session:|^stdio:|^prompt:|^thread:|^turn:started|^turn:start|turn\/start|codex turn|tool:codex-app-server/iu.test(label);
return /^session:|^stdio:|^prompt:|^thread:|^turn:started|^turn:start|turn\/start|codex turn|tool:codex-app-server/iu.test(label) ||
/^agentrun:backend:(codex-app-server-starting|initialize:completed|thread\/start|thread\/resume|thread\/started|turn\/start|turn\/started)/iu.test(label);
}
function isCompletionTraceEvent(event) {
const label = String(event?.label ?? "");
return label.startsWith("turn:completed") || label.startsWith("result:completed");
return label.startsWith("turn:completed") || label.startsWith("result:completed") || label === "agentrun:backend:turn/completed" || label === "agentrun:result:completed";
}
function tracePromptText(event) {
@@ -946,14 +951,24 @@ function traceNonEmptyString(value) {
function isNoisyTraceEvent(event) {
const label = String(event?.label ?? "");
if (isToolOutputChunkTraceEvent(event)) return false;
if (isCommandExecutionTraceEvent(event) || isAssistantMessageTraceEvent(event)) return false;
if (isRequestTraceEvent(event) || isSetupTraceEvent(event) || isCompletionTraceEvent(event)) return false;
if (isLowValueAgentRunBackendEvent(label)) return true;
if (/^agentrun:tool:item\/(started|completed)$/u.test(label)) return true;
if (/token_count|outputDelta:chunk/iu.test(label)) return true;
if (event?.type === "event" && !event.outputSummary && !event.message && !event.errorCode) return true;
return false;
}
function isLowValueAgentRunBackendEvent(label) {
return /^agentrun:backend:(run-created|command-created|runner-job-created|thread\/status\/changed|thread\/tokenUsage\/updated|account\/rateLimits\/updated|remoteControl\/status\/changed|configWarning|codex-app-server-closed|session-updated|command-terminal)$/u.test(String(label ?? ""));
}
function isToolOutputChunkTraceEvent(event) {
const label = String(event?.label ?? "");
return label === "item/commandExecution/outputDelta" ||
label === "agentrun:output:stdout" ||
label === "agentrun:output:stderr" ||
(event?.type === "tool_call" && event?.status === "output_chunk" && !String(event?.toolName ?? "").startsWith("codex-app-server"));
}