diff --git a/docs/reference/code-agent-chat-readiness.md b/docs/reference/code-agent-chat-readiness.md index eb019a65..63542ad5 100644 --- a/docs/reference/code-agent-chat-readiness.md +++ b/docs/reference/code-agent-chat-readiness.md @@ -158,6 +158,12 @@ Forwarder 不需要人工维护长驻进程。正式固化后,它应由 Kubern 只有“真实 DEV 路由 + `completed` + 非空 assistant reply”能作为 DEV-LIVE 回复通过依据。 不得把 mock、fixture、本地 echo、source report、静态检查或前端状态当作通过。 +AgentRun 接入后,`agentrun-v01` 只能表示执行基础设施,不等同于真实 provider/model。Code Agent result `completed` 要被 Web 或 CLI 标记为真实完成,必须同时具备 final assistant response、真实 provider/model、traceId、conversationId/sessionId 和可回放 trace;否则应显示“完成证据不足”,不能用 SOURCE、fixture、echo、mock 或 stub 补齐。 + +同一 conversation/session 的后续消息应优先进入既有 runner/session 的新 turn,避免每次消息都重新 bundle、重新启动 runner、再把历史 thread 当作新 runner 可 resume 的 rollout。必须新建 runner 时,trace/result 要说明原因,便于区分 runner 生命周期缺口和业务回答失败。 + +Trace 展示问题按“同源 row 转换优先”排查:先用 `hwlab-cli client agent trace --render web` 输出 Cloud Web 同一 trace row 摘要;CLI 也乱说明 row 转换逻辑问题,CLI 正常而浏览器乱再查 DOM、CSS、滚动和增量 patch。 + ## 自然语言单一路由 `/v1/agent/chat` 的自然语言请求唯一执行路径是 repo-owned Codex stdio long-lived diff --git a/docs/reference/spec-v02-hwlab-cli.md b/docs/reference/spec-v02-hwlab-cli.md index 7101e7bf..c6b7e926 100644 --- a/docs/reference/spec-v02-hwlab-cli.md +++ b/docs/reference/spec-v02-hwlab-cli.md @@ -23,6 +23,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 的继续会话路径。 - `client agent send` 默认先恢复账号 workspace,再向 `/v1/agent/chat` 发送 `workspaceId` 和 `expectedWorkspaceRevision`;服务端接受后 CLI 保存新的 workspace revision,终态轮询后再 PATCH workspace 清理终态 `activeTraceId`。只有显式 `--no-workspace` 才跳过这一默认恢复路径。 +- `client agent trace --render web` 必须调用 Cloud Web trace row 的同一纯转换路径,输出 `render="web"`、renderer 标识、source event count、rendered row count 和 row 摘要。浏览器 trace 展示错乱时,必须先用该 CLI 入口确认 Web 渲染转换是否已经乱序、重复、缺 final response 或吞掉关键 row,再继续修浏览器 DOM/CSS。 - AgentRun v0.1 短连接 runner 不保证历史 Web/Codex `threadId` 可在新 runner Job 内 resume;CLI 仍应把 Web 提交的 `threadId` 原样送到 Cloud Web API,以便验证 adapter 是否正确把它降级为 `requestedThreadId` 元数据,而不是让 runner 执行旧 thread resume。 - `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 的运行面写路径。 @@ -53,7 +54,7 @@ | `hwlab-cli client gateway invoke` | `POST Cloud API /v1/rpc/hardware.invoke.shell` | 显式 Cloud API 诊断入口,执行一次 bounded shell dispatch 并返回结构化 dispatch 摘要。 | | `hwlab-cli client gateway pressure` | `POST Cloud API /v1/rpc/hardware.invoke.shell` | 显式 Cloud API 压测入口,真实验证大输出、长单行、stderr、timeout 和并发超容量不会造成黑洞。 | | `hwlab-cli client agent send` | `GET /v1/agent/chat/inspect` + `POST /v1/agent/chat` + `GET /result/{trace}` | 以 short connection 提交 Code Agent 消息并轮询结果,默认输出 assistant 回复文本摘要;支持 `--from-trace` 自动复现 Web continuation,也支持手动 `--conversation-id`、`--session-id`、`--thread-id`、`--retry-of`。 | -| `hwlab-cli client agent trace TRACE` | `GET /v1/agent/chat/trace/{trace}` | 回放 trace,默认输出状态、事件摘要和 assistant stream 文本。 | +| `hwlab-cli client agent trace TRACE [--render web]` | `GET /v1/agent/chat/trace/{trace}` | 回放 trace,默认输出状态、事件摘要和 assistant stream 文本;`--render web` 复用 Cloud Web trace row 转换,便于 CLI 复现 Web trace 渲染问题。 | | `hwlab-cli client agent cancel TRACE` | `POST /v1/agent/chat/cancel` | 取消当前 Code Agent 请求。 | | `hwlab-cli client harness submit` | `POST /v1/agent/chat` | G14 harness-ops 的短连接提交入口,默认 provider profile 为 `deepseek`,返回 trace/result URL;`harness-ops` 和 `harness-opt` 是同义别名。 | | `hwlab-cli client harness wait/result/trace` | `GET /v1/agent/chat/result/{trace}`、`GET /trace/{trace}` | 轮询或读取一次 Code Agent 结果和 trace;单次 wait 最长 60 秒。 | @@ -87,6 +88,10 @@ 阅读 docs/reference/spec-v02-hwlab-cli.md,然后在 `G14:/root/hwlab-v02` 用 cli 手动测试以下内容:分别用 `--profile admin-a` 和 `--profile admin-a-second` 登录同一个账号,运行 `client workbench restore/status`,确认两个 profile 看到相同 `workspaceId` 和 revision;再用 `--profile other-user` 登录另一个账号,确认 workspace 不同;最后切回 `admin-a`,确认原 workspace、conversation/session/thread 仍能恢复。该验收必须使用真实 `http://74.48.78.17:19666`,不能 mock。 +## T3.2 + +阅读 docs/reference/spec-v02-hwlab-cli.md,然后在 `G14:/root/hwlab-v02` 用 cli 手动测试以下内容:对一个来自 Cloud Web 的真实 trace 运行 `client agent trace --render web --limit 80`,确认输出 `renderer=web/hwlab-cloud-web/app-trace:traceDisplayRows`、source event count、rendered row count 和 row 摘要;如果 Web trace 展示错乱,应先用该命令复现并定位到 Web row 转换还是浏览器 DOM/CSS 层。 + ## T4 阅读 docs/reference/spec-v02-hwlab-cli.md,然后在 `G14:/root/hwlab-v02` 用 cli 手动测试以下内容:运行 `client request GET /v1/access/status`、`client request GET /v1/diagnostics/gate` 和 `client rpc system.health`,确认它们全部走 `19666` Cloud Web 同源 API,返回 JSON route/httpStatus/body,且 `client request` 传入绝对 URL 会被拒绝。 diff --git a/docs/reference/spec-v02-hwlab-cloud-web.md b/docs/reference/spec-v02-hwlab-cloud-web.md index adb01468..7d48a775 100644 --- a/docs/reference/spec-v02-hwlab-cloud-web.md +++ b/docs/reference/spec-v02-hwlab-cloud-web.md @@ -9,6 +9,9 @@ - 为浏览器提供同源代理,避免前端直接跨域调用内部 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/滚动状态调查。 +- Code Agent result `completed` 只有在同时包含真实 provider/model/trace/conversation 元数据和可展示的 final assistant response 时,才能被 Web 标记为真实完成;`provider=agentrun-v01` 只是执行基础设施标识,不得替代上游 provider/model,也不得把 SOURCE、fixture、echo、mock 或 stub 当成 DEV-LIVE 完成。 +- 同一 conversation/session 的后续用户消息应优先复用已存在的 AgentRun runner/session 继续 turn;只有 runner 不可用、已过期或协议明确要求新 runner 时才重新 bundle 和启动 runner。每条消息都重新 bundle/runner 属于 v0.2 AgentRun 接入缺口,必须在 trace 中显式可见。 ## 内部架构 @@ -48,6 +51,14 @@ Cloud Web check 通过后仍需执行 bundle build 和 dist freshness 校验, 阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:从同源 `19666` 提交 Code Agent 短连接请求并轮询 result,确认请求经 cloud-web proxy 到 `hwlab-cloud-api`,且 trace 可回放。 +## T2.1 + +阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:对浏览器暴露的 Code Agent trace 运行 `hwlab-cli client agent trace --render web --limit 80`,确认 CLI 与 Web 使用同一 trace row 转换;若 final response 缺失、assistant row 顺序错乱或噪声事件过多,先用 CLI 固定复现再修实现。 + +## T2.2 + +阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:在同一 conversation/session 连续发送两条 Code Agent 消息,确认第二条不重新 materialize bundle/启动新 runner,或在 trace 中明确输出不能复用 runner 的原因;result completed 必须包含真实 provider/model/trace/conversation 和 final assistant response。 + ## T3 阅读 docs/reference/spec-v02-hwlab-cloud-web.md,然后用 cli 手动测试以下内容:打开 Workbench device-pod 面板,确认 status/freshness/blocker 显示来自 `/v1/device-pods`,未登录或未授权时必须显示认证/授权 blocker,不得把 fixture 或 blocked fallback 写成真实硬件 DEV-LIVE。 diff --git a/tools/hwlab-cli/client.test.ts b/tools/hwlab-cli/client.test.ts index 2b5d8a4e..d122efa4 100644 --- a/tools/hwlab-cli/client.test.ts +++ b/tools/hwlab-cli/client.test.ts @@ -377,6 +377,28 @@ test("hwlab-cli client agent trace shows assistant stream text in compact output assert.equal(result.payload.body.eventsCount, 1); }); +test("hwlab-cli client agent trace can render with the Web trace row path", async () => { + const result = await runHwlabCli(["client", "agent", "trace", "trc_render", "--base-url", "http://web.test", "--cookie", "hwlab_session=session-a", "--render", "web"], { + fetchImpl: async () => new Response(JSON.stringify({ + status: "completed", + traceId: "trc_render", + events: [ + { traceId: "trc_render", seq: 1, label: "request:accepted", status: "accepted", createdAt: "2026-06-01T13:00:00.000Z", promptSummary: "看看有什么device-pod能用?" }, + { traceId: "trc_render", seq: 2, label: "session:reused", status: "observed", createdAt: "2026-06-01T13:00:01.000Z" }, + { traceId: "trc_render", seq: 3, label: "assistant:completed", type: "assistant_message", terminal: true, status: "completed", itemId: "assistant-final", createdAt: "2026-06-01T13:00:02.000Z", message: "当前有两个 device-pod 可用。" } + ] + }), { status: 200 }) + }); + + assert.equal(result.exitCode, 0); + assert.equal(result.payload.body.render, "web"); + assert.equal(result.payload.body.renderer, "web/hwlab-cloud-web/app-trace:traceDisplayRows"); + assert.equal(result.payload.body.sourceEventCount, 3); + assert.ok(result.payload.body.renderedRowCount >= 2); + assert.ok(result.payload.body.rows.some((row: any) => /请求接受/u.test(row.header))); + assert.ok(result.payload.body.rows.some((row: any) => /助手最后一条消息/u.test(row.header))); +}); + 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[] = []; diff --git a/tools/src/hwlab-cli-lib.ts b/tools/src/hwlab-cli-lib.ts index 66b6adfa..7edf5053 100644 --- a/tools/src/hwlab-cli-lib.ts +++ b/tools/src/hwlab-cli-lib.ts @@ -1,6 +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"; const VERSION = "0.2.0-client"; const CLI_NAME = "hwlab-cli"; @@ -103,7 +104,7 @@ function help() { "hwlab-cli client rpc system.health [--full]", "hwlab-cli client agent send --message TEXT|--message-file PATH [--from-trace TRACE] [--conversation-id ID] [--session-id ID] [--thread-id ID] [--retry-of TRACE] --provider-profile deepseek --timeout-ms 120000", "hwlab-cli client agent result TRACE_ID", - "hwlab-cli client agent trace TRACE_ID", + "hwlab-cli client agent trace TRACE_ID [--render web]", "hwlab-cli client agent inspect --trace-id TRACE_ID", "hwlab-cli client agent cancel TRACE_ID", "hwlab-cli client harness submit --message TEXT --provider-profile deepseek", @@ -533,7 +534,8 @@ async function agentCommand(context: any) { const traceId = requiredTraceId(context.rest[1] ?? context.parsed.traceId); const pathName = `/v1/agent/chat/trace/${encodeURIComponent(traceId)}`; const response = await requestJson({ ...context, method: "GET", path: pathName }); - return responsePayload("client.agent.trace", response, context, { route: route("GET", pathName), traceId, body: responseBodyForCli(response.body, context.parsed) }); + const traceBody = traceBodyForCli(response.body, context.parsed); + return responsePayload("client.agent.trace", response, context, { route: route("GET", pathName), traceId, body: traceBody }); } if (subcommand === "inspect") { const pathName = agentInspectPath(context); @@ -1565,6 +1567,46 @@ function responseBodyForCli(body: any, parsed: ParsedArgs) { return parsed.full === true ? body : compactApiBody(body); } +function traceBodyForCli(body: any, parsed: ParsedArgs) { + if (text(parsed.render) === "web") return webTraceRenderBody(body, parsed); + return responseBodyForCli(body, parsed); +} + +function webTraceRenderBody(traceObject: any, parsed: ParsedArgs) { + const events = Array.isArray(traceObject?.events) ? traceObject.events : []; + const rows = traceDisplayRows(traceObject ?? {}, events); + const rowLimit = Math.max(1, Math.min(numberOption(parsed.limit) ?? 80, 500)); + const rowTail = parsed.full === true ? rows : rows.slice(-rowLimit); + return pruneUndefined({ + traceId: traceObject?.traceId ?? null, + status: traceObject?.status ?? null, + render: "web", + renderer: "web/hwlab-cloud-web/app-trace:traceDisplayRows", + sourceEventCount: traceObject?.eventCount ?? events.length, + renderedRowCount: rows.length, + returnedRowCount: rowTail.length, + rowLimit: parsed.full === true ? null : rowLimit, + eventCountMismatch: Number.isInteger(traceObject?.eventCount) && traceObject.eventCount !== events.length ? { + declared: traceObject.eventCount, + loaded: events.length + } : undefined, + assistantText: assistantText(traceObject), + rows: rowTail.map(compactTraceRenderRow), + fullBodyAvailable: true + }); +} + +function compactTraceRenderRow(row: any) { + return pruneUndefined({ + rowId: row?.rowId ?? null, + seq: row?.seq ?? null, + tone: row?.tone ?? null, + header: row?.header ?? null, + bodyFormat: row?.bodyFormat ?? null, + body: row?.body ? preview(String(row.body), 1200) : undefined + }); +} + function compactApiBody(body: any): any { if (Array.isArray(body)) return { itemCount: body.length, items: body.slice(0, 20).map(compactApiBody), fullBodyAvailable: true }; if (!body || typeof body !== "object") return body;