From 28ffe3cd5de819e3314f3952533c64e162d66324 Mon Sep 17 00:00:00 2001 From: Codex Agent Date: Sat, 6 Jun 2026 17:25:32 +0800 Subject: [PATCH] fix: archive CaseRun agent transcript artifacts --- docs/reference/spec-hwpod-harness.md | 2 +- tools/hwlab-cli/caserun.test.ts | 120 +++++++++++++++++ tools/src/hwlab-caserun-lib.ts | 186 ++++++++++++++++++++++++++- 3 files changed, 305 insertions(+), 3 deletions(-) diff --git a/docs/reference/spec-hwpod-harness.md b/docs/reference/spec-hwpod-harness.md index e3a930bb..a9844368 100644 --- a/docs/reference/spec-hwpod-harness.md +++ b/docs/reference/spec-hwpod-harness.md @@ -222,7 +222,7 @@ bun tools/hwlab-cli/bin/hwlab-cli.ts case run result bun tools/hwlab-cli/bin/hwlab-cli.ts case run logs --tail 8000 ``` -`case run` 在当前版本只负责编排流程和记录原始证据,不做 evidence 自动评价、自动打分或门禁判断。流程是:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-` 隔离 worktree并写入 run-local spec,然后生成 Code Agent prompt/context,创建或调用 HWLAB Code Agent session,把 isolated worktree、run-local HWPOD spec、任务目标和约束交给 Code Agent;agent 阶段结束或超时后,CaseRun 采集 agent trace/terminal state/commandExecution 摘要、subject worktree 的 `git status` / `git diff --stat` / `git diff --binary`,再按 case 定义执行 runner 后置 `hwpod-cli build` 和 `cmd.run` Keil 异步 job-status 查询,最后把 `evidence.json`、`summary.md`、`run.json`、`result.json`、`agent-prompt.md`、`agent-diff.patch`、run-local `.hwlab/hwpod-spec.yaml`、worker stdout/stderr 和 `artifact-manifest.json` 归档到 case registry repo 的 `runs///`。下载不作为 compile-only CaseRun 的隐式步骤;后续需要下载时,应作为独立 action 显式加入 case 定义和流程记录。 +`case run` 在当前版本只负责编排流程和记录原始证据,不做 evidence 自动评价、自动打分或门禁判断。流程是:prepare 先在 subject repo 本地 checkout 下创建 `.worktree/caserun-` 隔离 worktree并写入 run-local spec,然后生成 Code Agent prompt/context,创建或调用 HWLAB Code Agent session,把 isolated worktree、run-local HWPOD spec、任务目标和约束交给 Code Agent;agent 阶段结束或超时后,CaseRun 采集 agent trace/terminal state/commandExecution 摘要、subject worktree 的 `git status` / `git diff --stat` / `git diff --binary`,再按 case 定义执行 runner 后置 `hwpod-cli build` 和 `cmd.run` Keil 异步 job-status 查询,最后把原始证据和可读归档写入 case registry repo 的 `runs///`。固定归档包括 `evidence.json`、`summary.md`、`run.json`、`result.json`、`agent-prompt.md`、`agent-diff.patch`、run-local `.hwlab/hwpod-spec.yaml`、worker stdout/stderr、`artifact-manifest.json`,以及共享 Web/CLI trace renderer 生成的 `agent-messages.json`、`agent-trace.md`、`agent-transcript.md` 和 `final-response.md`。`agent-trace.md` / `agent-transcript.md` 必须包含 rendered trace rows、renderer 标识和 subject diff 区块;`final-response.md` 在存在权威 finalResponse 时保存正文,不存在时显式记录 `finalResponse=null`、terminal/error rows 和缺失原因。下载不作为 compile-only CaseRun 的隐式步骤;后续需要下载时,应作为独立 action 显式加入 case 定义和流程记录。 长时间 CaseRun 必须优先使用 `case run start/status/result/logs` 短连接形态,而不是用 shell/nohup 包装同步 `case run`。`start` 只负责创建 `.state/hwlab-cli/caserun//`、启动后台 worker 并立即返回 `runId`、PID、stdout/stderr 路径和下一条查询命令;`status`、`result`、`logs` 都是短查询。`run.json` 是阶段状态权威,阶段开始时就要刷新 `status`、`stage`、`traceId/sessionId/conversationId/threadId`、AgentRun/trace 轮询信息、stdout/stderr byte count 和下一条轮询命令;长等待期间不得出现无输出、无 trace、无状态文件更新的黑洞窗口。 diff --git a/tools/hwlab-cli/caserun.test.ts b/tools/hwlab-cli/caserun.test.ts index 2e2179ef..ed839a37 100644 --- a/tools/hwlab-cli/caserun.test.ts +++ b/tools/hwlab-cli/caserun.test.ts @@ -350,6 +350,11 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit "summary.md", "run.json", "result.json", + "agent-trace.json", + "agent-trace.md", + "agent-messages.json", + "agent-transcript.md", + "final-response.md", "agent-prompt.md", "agent-diff.patch", path.join(".hwlab", "hwpod-spec.yaml"), @@ -366,9 +371,30 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit assert.equal(manifest.trace.agentRun.runId, "run_agent_trace"); assert.equal(manifest.prompt.sha256, result.payload.evidence.agent.promptSha256); assert.equal(manifest.diff.sha256, result.payload.evidence.agentDiff.diffPatchSha256); + assert.equal(manifest.readableAgent.renderer, "tools/src/hwlab-cli/trace-renderer:traceDisplayRows"); + assert.equal(manifest.readableAgent.messagesPath, "agent-messages.json"); + assert.equal(manifest.readableAgent.tracePath, "agent-trace.md"); + assert.equal(manifest.readableAgent.transcriptPath, "agent-transcript.md"); + assert.equal(manifest.readableAgent.finalResponsePath, "final-response.md"); + assert.equal(manifest.readableAgent.finalResponse.present, true); + assert.equal(manifest.readableAgent.finalResponse.text, "done"); assert.equal(manifest.decisions.autoEvaluation, false); + assert.equal(manifest.files.some((item: any) => item.path === "agent-messages.json"), true); + assert.equal(manifest.files.some((item: any) => item.path === "agent-trace.md"), true); + assert.equal(manifest.files.some((item: any) => item.path === "final-response.md"), true); assert.equal(manifest.files.some((item: any) => item.path === "worker.stdout.log"), true); assert.equal(manifest.files.some((item: any) => item.path === ".hwlab/hwpod-spec.yaml"), true); + const messages = JSON.parse(await readFile(path.join(registryRunDir, "agent-messages.json"), "utf8")); + assert.equal(messages.renderer, "tools/src/hwlab-cli/trace-renderer:traceDisplayRows"); + assert.equal(messages.finalResponse.present, true); + assert.match(JSON.stringify(messages.rows), /hwpod build --spec \.hwlab\/hwpod-spec\.yaml/u); + const traceMarkdown = await readFile(path.join(registryRunDir, "agent-trace.md"), "utf8"); + assert.match(traceMarkdown, /renderer: tools\/src\/hwlab-cli\/trace-renderer:traceDisplayRows/u); + assert.match(traceMarkdown, /## Subject Diff/u); + assert.match(traceMarkdown, /\+printf\("hello"\);/u); + const finalResponse = await readFile(path.join(registryRunDir, "final-response.md"), "utf8"); + assert.match(finalResponse, /present: true/u); + assert.match(finalResponse, /done/u); assert.equal(result.payload.collect.caseRepoRunDir, registryRunDir); assert.equal(result.payload.collect.artifactManifestPath, path.join(registryRunDir, "artifact-manifest.json")); assert.equal(result.payload.collect.caseRepoFiles.some((item: string) => item.endsWith("artifact-manifest.json")), true); @@ -394,6 +420,100 @@ test("case run orchestrates agent prompt, diff capture, and compile evidence wit } }); +test("case run archives rendered trace transcript when finalResponse is null", async () => { + const root = await mkdtempCaseRoot(); + const caseRepo = path.join(root, "hwlab-case-registry"); + const cwd = path.join(root, "hwlab"); + const runId = "run-final-null"; + const runDir = path.join(cwd, ".state", "hwlab-cli", "caserun", runId); + const registryRunDir = path.join(caseRepo, "runs", "d601-f103-v2-compile", runId); + try { + await mkdir(path.join(caseRepo, ".git"), { recursive: true }); + await writeFile(path.join(caseRepo, ".git", "HEAD"), "ref: refs/heads/main\n", "utf8"); + await mkdir(path.join(runDir, ".hwlab"), { recursive: true }); + await writeFile(path.join(runDir, ".hwlab", "hwpod-spec.yaml"), hwpodSpecText(), "utf8"); + await writeFile(path.join(runDir, "agent-prompt.md"), "prompt\n", "utf8"); + await writeFile(path.join(runDir, "agent-diff.patch"), "diff --git a/main.c b/main.c\n+printf(\"hello\");\n", "utf8"); + await writeFile(path.join(runDir, "worker.stdout.log"), "worker complete\n", "utf8"); + await writeFile(path.join(runDir, "worker.stderr.log"), "", "utf8"); + const run = { + caseId: "d601-f103-v2-compile", + runId, + runDir, + caseRepo, + caseDir: "", + caseFile: "", + sourceSpecPath: "", + specPath: path.join(runDir, ".hwlab", "hwpod-spec.yaml"), + apiUrl: "http://api.test", + compileOnly: true, + createdAt: "2026-06-06T00:00:00.000Z", + updatedAt: "2026-06-06T00:00:10.000Z", + status: "completed", + stage: "completed", + definition: {}, + subject: { repoLocalPath: SUBJECT_REPO_LOCAL_PATH, commitId: SUBJECT_COMMIT_ID, subdir: "projects/01_baseline", worktreePath: "F:\\Work\\HWLAB-CASE-F103\\.worktree\\caserun-run-final-null", workspacePath: "", setup: {} }, + agent: { stageStatus: "failed", baseUrl: "http://web.test", projectId: "prj_hwpod_workbench", providerProfile: "deepseek", conversationId: "cnv_final_null", sessionId: "ses_final_null", threadId: "thread-final-null", traceId: "trc_final_null", promptPath: path.join(runDir, "agent-prompt.md"), promptSha256: "prompt-sha", result: { ok: true, status: "failed", finalResponse: null }, polls: 2, timedOut: false, sessionResponse: null }, + agentTrace: { traceId: "trc_final_null", status: "failed", httpStatus: 200, sourceEventCount: 3, renderedRowCount: 0, commandCount: 0, hwpodCommandCount: 0, hwpodBuildCommandCount: 0, keilJobCandidates: [], terminalStatus: "failed", agentRun: { runId: "run_agent_final_null", commandId: "cmd_final_null" }, commands: [] }, + agentDiff: { statusShort: " M main.c\n", diffStat: " main.c | 1 +\n", diffPatchPath: path.join(runDir, "agent-diff.patch"), diffPatchSha256: "diff-sha", sourceRootStatusShort: "", requests: [] }, + evidencePath: path.join(runDir, "evidence.json"), + _control: { status: "completed", stage: "completed", stdoutPath: path.join(runDir, "worker.stdout.log"), stderrPath: path.join(runDir, "worker.stderr.log"), completedAt: "2026-06-06T00:00:10.000Z", resultPath: path.join(runDir, "result.json") } + }; + const trace = { + ok: true, + status: "failed", + traceId: "trc_final_null", + finalResponse: null, + events: [ + { traceId: "trc_final_null", seq: 1, label: "agentrun:assistant:message", type: "assistant", status: "running", message: "我先检查当前 HWPOD。", createdAt: "2026-06-06T00:00:01.000Z" }, + { traceId: "trc_final_null", seq: 2, label: "item/commandExecution:completed", type: "tool_call", toolName: "commandExecution", status: "completed", command: "/bin/sh -lc 'hwpod build --spec .hwlab/hwpod-spec.yaml'", stdoutSummary: "build completed", exitCode: 0, createdAt: "2026-06-06T00:00:02.000Z" }, + { traceId: "trc_final_null", seq: 3, label: "agentrun:terminal:failed", type: "result", terminal: true, status: "failed", message: "Error running remote compact task: unexpected status 404 Not Found", createdAt: "2026-06-06T00:00:03.000Z" } + ], + error: { message: "Error running remote compact task: unexpected status 404 Not Found" } + }; + const evidence = { + contractVersion: "hwpod-case-run-evidence-v1", + caseId: run.caseId, + runId, + compileOnly: true, + autoEvaluation: false, + status: "recorded", + subject: run.subject, + agent: { traceId: "trc_final_null", sessionId: "ses_final_null", conversationId: "cnv_final_null", threadId: "thread-final-null", providerProfile: "deepseek", projectId: "prj_hwpod_workbench", promptSha256: "prompt-sha" }, + agentTrace: run.agentTrace, + agentDiff: run.agentDiff, + hwpod: { source: "case-run-runner-post-agent-compile-check", exitCode: 0 }, + artifacts: [], + decisions: { downloadSkipped: true } + }; + await writeJsonFile(path.join(runDir, "run.json"), run); + await writeJsonFile(path.join(runDir, "agent-trace.json"), trace); + await writeJsonFile(path.join(runDir, "evidence.json"), evidence); + await writeJsonFile(path.join(runDir, "result.json"), { ok: true, action: "case.run", status: "completed", caseId: run.caseId, runId, runDir, collect: { caseRepoFiles: [] }, evidence: {} }); + + const result = await runHwlabCli(["case", "run", "result", runId], { cwd, now: () => "2026-06-06T00:00:11.000Z" }); + + assert.equal(result.exitCode, 0); + const manifest = JSON.parse(await readFile(path.join(registryRunDir, "artifact-manifest.json"), "utf8")); + assert.equal(manifest.readableAgent.finalResponse.present, false); + assert.match(manifest.readableAgent.finalResponse.reason, /404 Not Found/u); + const messages = JSON.parse(await readFile(path.join(registryRunDir, "agent-messages.json"), "utf8")); + assert.equal(messages.finalResponse.present, false); + assert.match(JSON.stringify(messages.rows), /我先检查当前 HWPOD/u); + assert.match(JSON.stringify(messages.rows), /hwpod build --spec \.hwlab\/hwpod-spec\.yaml/u); + const traceMarkdown = await readFile(path.join(registryRunDir, "agent-trace.md"), "utf8"); + assert.match(traceMarkdown, /finalResponse=null/u); + assert.match(traceMarkdown, /Error running remote compact task/u); + assert.match(traceMarkdown, /## Subject Diff/u); + assert.match(traceMarkdown, /\+printf\("hello"\);/u); + const finalMarkdown = await readFile(path.join(registryRunDir, "final-response.md"), "utf8"); + assert.match(finalMarkdown, /present: false/u); + assert.match(finalMarkdown, /404 Not Found/u); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test("case run status reports latest run stage when async control stage is stale", async () => { const root = await mkdtempCaseRoot(); const cwd = path.join(root, "hwlab"); diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index 64b11a8f..20b369b3 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -4,6 +4,7 @@ import { closeSync, openSync } from "node:fs"; import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises"; import path from "node:path"; +import { traceDisplayRows, traceNoiseEventCount, type TraceEventRow } from "./hwlab-cli/trace-renderer.ts"; import { readHwpodSpec } from "./hwpod-harness-lib.ts"; const DEFAULT_API_URL = "http://74.48.78.17:19667"; @@ -17,6 +18,7 @@ const MAX_AGENT_TRACE_COMMANDS = 30; const HWLAB_API_KEY_PREFIX = "hwl_live_"; const STANDARD_CASE_REPO_PATH = "/root/hwlab-case-registry"; const REMOVED_CASE_REPO_PATH = "/root/hwpod-cases"; +const CASE_TRACE_RENDERER = "tools/src/hwlab-cli/trace-renderer:traceDisplayRows"; type EnvLike = Record; type ParsedArgs = Record & { _: string[] }; @@ -156,6 +158,16 @@ type CaseRegistryArchive = { manifest: Record; }; +type CaseReadableAgentArchive = { + messagesRel: string; + traceRel: string; + transcriptRel: string; + finalResponseRel: string; + generatedFiles: string[]; + traceRender: Record; + finalResponse: Record; +}; + export async function caseCommand(context: CaseContext) { const subcommand = context.rest[0] || "help"; if (["help", "--help", "-h"].includes(subcommand) || context.parsed.help === true) return caseHelp(); @@ -718,6 +730,7 @@ async function collectAgentTraceEvidence(context: CaseContext, run: PreparedCase const nextRun = await updateRun(context, run, { agentTrace: trace, stage: "agent-trace-collected" }); return { run: nextRun, trace }; } + await writeJson(path.join(run.runDir, "agent-trace.json"), response.body); const trace = summarizeAgentTrace(traceId, response.status, response.body); const nextRun = await updateRun(context, run, { agentTrace: trace, stage: "agent-trace-collected" }); return { run: nextRun, trace }; @@ -1389,6 +1402,8 @@ async function archiveCaseRunArtifacts(context: CaseContext, run: PreparedCaseRu ["evidence.json", path.join(run.runDir, "evidence.json")], ["summary.md", undefined] ]); + const readableAgent = await writeReadableAgentArtifacts(caseRepoRunDir, run, evidence); + for (const rel of readableAgent.generatedFiles) materialized.set(rel, undefined); const skippedFiles: string[] = []; for (const entry of caseRunArtifactEntries(run)) { const rel = artifactRel(entry.rel); @@ -1399,7 +1414,7 @@ async function archiveCaseRunArtifacts(context: CaseContext, run: PreparedCaseRu const files: CaseRegistryArchiveFile[] = []; for (const [rel, source] of materialized) files.push(await registryArtifactFileRecord(run, caseRepoRunDir, rel, source)); - const manifest = caseRunArtifactManifest(context, run, evidence, files, skippedFiles); + const manifest = caseRunArtifactManifest(context, run, evidence, files, skippedFiles, readableAgent); const artifactManifestPath = path.join(caseRepoRunDir, "artifact-manifest.json"); await writeJson(artifactManifestPath, manifest); const caseRepoFiles = [...files.map((file) => artifactRel(path.join(runRel, file.path))), artifactRel(path.join(runRel, "artifact-manifest.json"))]; @@ -1420,6 +1435,159 @@ function collectSummaryFromArchive(run: PreparedCaseRun, evidence: any, archive: }; } +async function writeReadableAgentArtifacts(caseRepoRunDir: string, run: PreparedCaseRun, evidence: any): Promise { + const messagesRel = "agent-messages.json"; + const traceRel = "agent-trace.md"; + const transcriptRel = "agent-transcript.md"; + const finalResponseRel = "final-response.md"; + const traceBody = await archivedAgentTraceBody(run, evidence); + const events = arrayOfObjects(traceBody?.events ?? traceBody?.trace?.events ?? traceBody?.sourceEvents); + const rows = traceDisplayRows(traceBody ?? {}, events); + const finalResponse = agentFinalResponse(run, evidence, traceBody, rows); + const diffPatch = await readTextIfExists(evidence.agentDiff?.diffPatchPath ?? run.agentDiff?.diffPatchPath ?? path.join(run.runDir, "agent-diff.patch")); + const messages = clean({ + contractVersion: "hwpod-case-run-agent-messages-v1", + renderer: CASE_TRACE_RENDERER, + traceId: evidence.agent?.traceId ?? run.agent?.traceId ?? traceBody?.traceId ?? run.agentTrace?.traceId, + conversationId: evidence.agent?.conversationId ?? run.agent?.conversationId, + sessionId: evidence.agent?.sessionId ?? run.agent?.sessionId, + threadId: evidence.agent?.threadId ?? run.agent?.threadId, + status: traceBody?.traceStatus ?? traceBody?.status ?? run.agentTrace?.status ?? evidence.agentTrace?.status, + sourceEventCount: traceBody?.eventCount ?? traceBody?.sourceEventCount ?? events.length, + renderedRowCount: rows.length, + noiseEventCount: traceNoiseEventCount(events), + finalResponse, + rows: rows.map(agentMessageRowForArchive), + diff: clean({ path: "agent-diff.patch", sha256: evidence.agentDiff?.diffPatchSha256 ?? run.agentDiff?.diffPatchSha256, statusShort: evidence.agentDiff?.statusShort ?? run.agentDiff?.statusShort, diffStat: evidence.agentDiff?.diffStat ?? run.agentDiff?.diffStat, patchIncludedInTranscript: true }) + }); + const transcript = renderAgentTranscript(run, evidence, messages, rows, diffPatch); + await writeJson(path.join(caseRepoRunDir, messagesRel), messages); + await writeFile(path.join(caseRepoRunDir, traceRel), transcript, "utf8"); + await writeFile(path.join(caseRepoRunDir, transcriptRel), transcript, "utf8"); + await writeFile(path.join(caseRepoRunDir, finalResponseRel), renderFinalResponseArtifact(run, evidence, finalResponse, rows), "utf8"); + return { + messagesRel, + traceRel, + transcriptRel, + finalResponseRel, + generatedFiles: [messagesRel, traceRel, transcriptRel, finalResponseRel], + traceRender: clean({ renderer: CASE_TRACE_RENDERER, status: messages.status, sourceEventCount: messages.sourceEventCount, renderedRowCount: messages.renderedRowCount, noiseEventCount: messages.noiseEventCount }), + finalResponse + }; +} + +async function archivedAgentTraceBody(run: PreparedCaseRun, evidence: any) { + const rawTrace = await readJsonIfExists(path.join(run.runDir, "agent-trace.json")); + return rawTrace ?? agentTraceBody(run, evidence); +} + +function agentTraceBody(run: PreparedCaseRun, evidence: any) { + const result = run.agent?.result ?? evidence.agent?.result ?? null; + return result?.body && typeof result.body === "object" ? result.body : result; +} + +function agentFinalResponse(run: PreparedCaseRun, evidence: any, traceBody: any, rows: TraceEventRow[]) { + const candidates = [ + traceBody?.finalResponse, + traceBody?.terminalEvidence?.finalResponse, + run.agent?.result?.finalResponse, + run.agent?.result?.reply, + evidence.agent?.finalResponse + ]; + for (const candidate of candidates) { + const body = text((candidate as any)?.text ?? (candidate as any)?.content ?? (candidate as any)?.message); + if (body) return clean({ present: true, status: text((candidate as any)?.status) || "completed", source: text((candidate as any)?.source) || "agent-result", text: body, textChars: body.length, traceId: text((candidate as any)?.traceId ?? traceBody?.traceId ?? run.agent?.traceId) }); + } + const terminal = rows.filter((row) => row.terminal === true || row.tone === "blocked").slice(-5).map(agentMessageRowForArchive); + return clean({ + present: false, + value: null, + reason: finalResponseMissingReason(traceBody, rows), + status: text(traceBody?.status ?? traceBody?.traceStatus ?? run.agent?.stageStatus), + terminalRows: terminal, + error: compactObject(traceBody?.error ?? run.agent?.error ?? null) + }); +} + +function finalResponseMissingReason(traceBody: any, rows: TraceEventRow[]) { + const errorText = text(traceBody?.error?.message ?? traceBody?.terminalEvidence?.error?.message); + if (errorText) return errorText; + const terminalBody = rows.slice().reverse().map((row) => text(row.body)).find((body) => /error|failed|404|not found|blocked|timeout/iu.test(body)); + return terminalBody ? clipText(terminalBody, 1200) : "finalResponse=null; no authoritative final assistant response was returned by the trace/result payload"; +} + +function agentMessageRowForArchive(row: TraceEventRow) { + return clean({ rowId: row.rowId, seq: row.seq, tone: row.tone, header: row.header, terminal: row.terminal === true ? true : undefined, bodyFormat: row.bodyFormat, body: row.body }); +} + +function renderAgentTranscript(run: PreparedCaseRun, evidence: any, messages: any, rows: TraceEventRow[], diffPatch: string) { + const sections = [ + `# CaseRun Agent Transcript`, + ``, + `- caseId: ${run.caseId}`, + `- runId: ${run.runId}`, + `- traceId: ${messages.traceId ?? ""}`, + `- conversationId: ${messages.conversationId ?? ""}`, + `- sessionId: ${messages.sessionId ?? ""}`, + `- threadId: ${messages.threadId ?? ""}`, + `- renderer: ${CASE_TRACE_RENDERER}`, + `- finalResponse: ${messages.finalResponse?.present === true ? "present" : "null"}`, + `- autoEvaluation: false`, + ``, + `## Messages`, + rows.length > 0 ? rows.map(renderTranscriptRow).join("\n\n") : `_No rendered trace rows were returned._`, + ``, + `## Final Response`, + messages.finalResponse?.present === true ? text(messages.finalResponse.text) : [`finalResponse=null`, `reason: ${text(messages.finalResponse?.reason)}`].join("\n"), + ``, + `## Subject Diff`, + ``, + `statusShort:`, + `\`\`\`text`, + text(evidence.agentDiff?.statusShort ?? run.agentDiff?.statusShort) || "(empty)", + `\`\`\``, + ``, + `diffStat:`, + `\`\`\`text`, + text(evidence.agentDiff?.diffStat ?? run.agentDiff?.diffStat) || "(empty)", + `\`\`\``, + ``, + `patch:`, + `\`\`\`diff`, + diffPatch || "(empty)", + `\`\`\`` + ]; + return `${sections.join("\n")}\n`; +} + +function renderTranscriptRow(row: TraceEventRow) { + const body = text(row.body); + return [`### ${row.header}`, ``, `- rowId: ${row.rowId}`, row.terminal === true ? `- terminal: true` : null, ``, body ? body : `_No body._`].filter((item) => item !== null).join("\n"); +} + +function renderFinalResponseArtifact(run: PreparedCaseRun, evidence: any, finalResponse: any, rows: TraceEventRow[]) { + const lines = [ + `# CaseRun Final Response`, + ``, + `- caseId: ${run.caseId}`, + `- runId: ${run.runId}`, + `- traceId: ${evidence.agent?.traceId ?? run.agent?.traceId ?? ""}`, + `- present: ${finalResponse.present === true}`, + `` + ]; + if (finalResponse.present === true) return `${[...lines, text(finalResponse.text)].join("\n")}\n`; + const terminalRows = rows.filter((row) => row.terminal === true || row.tone === "blocked").slice(-5); + return `${[ + ...lines, + `finalResponse=null`, + ``, + `reason: ${text(finalResponse.reason)}`, + ``, + `## Terminal/Error Rows`, + terminalRows.length > 0 ? terminalRows.map(renderTranscriptRow).join("\n\n") : `_No terminal/error rows were rendered._` + ].join("\n")}\n`; +} + async function refreshCompletedRunArchive(context: CaseContext, run: PreparedCaseRun, evidence?: any) { if (context.parsed.noCaseRepoRecord === true) return null; if (!text(run.caseRepo)) return null; @@ -1433,6 +1601,7 @@ function caseRunArtifactEntries(run: PreparedCaseRun) { return [ { rel: "run.json", source: path.join(run.runDir, "run.json") }, { rel: "result.json", source: path.join(run.runDir, "result.json") }, + { rel: "agent-trace.json", source: path.join(run.runDir, "agent-trace.json") }, { rel: "agent-prompt.md", source: path.join(run.runDir, "agent-prompt.md") }, { rel: "agent-diff.patch", source: path.join(run.runDir, "agent-diff.patch") }, { rel: path.join(".hwlab", "hwpod-spec.yaml"), source: run.specPath }, @@ -1464,7 +1633,7 @@ async function registryArtifactFileRecord(run: PreparedCaseRun, caseRepoRunDir: }); } -function caseRunArtifactManifest(context: CaseContext, run: PreparedCaseRun, evidence: any, files: CaseRegistryArchiveFile[], skippedFiles: string[]) { +function caseRunArtifactManifest(context: CaseContext, run: PreparedCaseRun, evidence: any, files: CaseRegistryArchiveFile[], skippedFiles: string[], readableAgent: CaseReadableAgentArchive) { return clean({ contractVersion: "hwpod-case-run-artifact-manifest-v1", caseId: run.caseId, @@ -1482,6 +1651,15 @@ function caseRunArtifactManifest(context: CaseContext, run: PreparedCaseRun, evi threadId: evidence.agent?.threadId ?? run.agent?.threadId, traceId: evidence.agent?.traceId ?? run.agent?.traceId }), + readableAgent: clean({ + renderer: CASE_TRACE_RENDERER, + messagesPath: readableAgent.messagesRel, + tracePath: readableAgent.traceRel, + transcriptPath: readableAgent.transcriptRel, + finalResponsePath: readableAgent.finalResponseRel, + traceRender: readableAgent.traceRender, + finalResponse: readableAgent.finalResponse + }), prompt: clean({ path: "agent-prompt.md", sha256: evidence.agent?.promptSha256 ?? run.agent?.promptSha256 }), diff: clean({ path: "agent-diff.patch", sha256: evidence.agentDiff?.diffPatchSha256 ?? run.agentDiff?.diffPatchSha256, statusShort: evidence.agentDiff?.statusShort ?? run.agentDiff?.statusShort, diffStat: evidence.agentDiff?.diffStat ?? run.agentDiff?.diffStat }), keilJob: evidence.keilJob ?? null, @@ -1586,6 +1764,10 @@ async function readJsonIfExists(file: string) { try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; } } +async function readTextIfExists(file: string) { + try { return await readFile(file, "utf8"); } catch { return ""; } +} + function controlFromRun(run: any) { return run?._control && typeof run._control === "object" ? run._control : {}; }