diff --git a/deploy/runtime/launcher/hwlab-env-reuse-launcher.ts b/deploy/runtime/launcher/hwlab-env-reuse-launcher.ts index 63c5c003..5d7a099d 100644 --- a/deploy/runtime/launcher/hwlab-env-reuse-launcher.ts +++ b/deploy/runtime/launcher/hwlab-env-reuse-launcher.ts @@ -24,6 +24,7 @@ rmSync(checkoutDir, { recursive: true, force: true }); run("git", ["clone", "--no-checkout", "--single-branch", "--branch", bootRef, readUrl, checkoutDir], "/"); run("git", ["checkout", "--detach", bootCommit], checkoutDir); linkNodeModules(); +ensureRuntimeDependencies(); installHwpodAliases(); const scriptPath = path.join(checkoutDir, bootSh); @@ -49,6 +50,30 @@ function linkNodeModules(): void { symlinkSync(runtimeNodeModules, target, "dir"); } +function ensureRuntimeDependencies(): void { + const packageJson = path.join(checkoutDir, "package.json"); + if (!existsSync(packageJson)) return; + if (canResolvePackage("yaml")) { + process.stdout.write(`${JSON.stringify({ event: "hwlab-env-reuse-dependencies", status: "cached", package: "yaml" })}\n`); + return; + } + + const npmCommand = process.env.HWLAB_NPM_COMMAND || "npm"; + process.stdout.write(`${JSON.stringify({ event: "hwlab-env-reuse-dependencies", status: "installing", package: "yaml", manager: npmCommand })}\n`); + run(npmCommand, ["install", "--omit=dev", "--include=optional", "--ignore-scripts", "--prefer-offline"], checkoutDir); + if (!canResolvePackage("yaml")) fail("runtime dependency install completed but package yaml is still unavailable"); + process.stdout.write(`${JSON.stringify({ event: "hwlab-env-reuse-dependencies", status: "installed", package: "yaml", manager: npmCommand })}\n`); +} + +function canResolvePackage(packageName: string): boolean { + const result = spawnSync(process.execPath, ["-e", `require.resolve(${JSON.stringify(packageName)})`], { + cwd: checkoutDir, + env: process.env, + stdio: "ignore" + }); + return !result.error && !result.signal && result.status === 0; +} + function installHwpodAliases(): void { installHwpodAlias("hwpod", "tools/hwpod-cli.ts"); installHwpodAlias("hwpod-ctl", "tools/hwpod-ctl.ts"); diff --git a/tools/src/hwlab-caserun-lib.ts b/tools/src/hwlab-caserun-lib.ts index 9e86bf47..24c73292 100644 --- a/tools/src/hwlab-caserun-lib.ts +++ b/tools/src/hwlab-caserun-lib.ts @@ -1799,7 +1799,7 @@ async function writeCaseAggregateFromRegistry(context: CaseContext, caseRepo: st if (!evidence && !run && !manifest) throw cliError("case_registry_run_not_found", "case registry run artifacts were not found", { caseRepoRunDir, caseId, runId }); const outputRel = aggregateOutputRel(context); const outputPath = path.join(caseRepoRunDir, outputRel); - const markdown = await renderCaseAggregateMarkdown({ caseRepoRunDir, caseId, runId, evidence, run, manifest }); + const markdown = await renderCaseAggregateMarkdown(context, { caseRepoRunDir, caseId, runId, evidence, run, manifest }); await mkdir(path.dirname(outputPath), { recursive: true }); await writeFile(outputPath, markdown, "utf8"); const file = await registryAggregateFileRecord(outputPath); @@ -1829,16 +1829,16 @@ function aggregateOutputRel(context: CaseContext) { return rel; } -async function renderCaseAggregateMarkdown(input: { caseRepoRunDir: string; caseId: string; runId: string; evidence: any; run: any; manifest: any }) { +async function renderCaseAggregateMarkdown(context: CaseContext, input: { caseRepoRunDir: string; caseId: string; runId: string; evidence: any; run: any; manifest: any }) { const prompt = await readTextIfExists(path.join(input.caseRepoRunDir, "agent-prompt.md")); const finalResponse = await readTextIfExists(path.join(input.caseRepoRunDir, "final-response.md")); const diffPatch = await readTextIfExists(path.join(input.caseRepoRunDir, "agent-diff.patch")); const traceMarkdown = await readTextIfExists(path.join(input.caseRepoRunDir, "agent-trace.md")); const specText = await readTextIfExists(path.join(input.caseRepoRunDir, ".hwlab", "hwpod-spec.yaml")); - const messages = await readJsonIfExists(path.join(input.caseRepoRunDir, "agent-messages.json")); const evidence = input.evidence ?? {}; const run = input.run ?? {}; const manifest = input.manifest ?? {}; + const archivedMessages = await readJsonIfExists(path.join(input.caseRepoRunDir, "agent-messages.json")); const traceLookup = manifest.traceLookup ?? evidence.traceLookup ?? run.agent?.traceLookup; const commands = traceLookup?.commands ?? {}; const agent = manifest.agent ?? manifest.trace ?? evidence.agent ?? run.agent ?? {}; @@ -1847,6 +1847,7 @@ async function renderCaseAggregateMarkdown(input: { caseRepoRunDir: string; case const diff = manifest.diff ?? evidence.agentDiff ?? run.agentDiff ?? {}; const keilJob = manifest.keilJob ?? evidence.keilJob ?? null; const agentStage = manifest.agentStage ?? agentStageSummary(evidence); + const messages = await aggregateMessagesWithFreshFullTrace(context, { archivedMessages, traceLookup, traceId: agent.traceId ?? trace.traceId ?? traceLookup?.traceId, run, evidence, manifest }); const traceBody = aggregateTraceBody(messages, traceMarkdown); return `${[ `# HWPOD CaseRun Aggregate: ${input.caseId}`, @@ -1952,6 +1953,44 @@ function aggregateTraceBody(messages: any, traceMarkdown: string) { return `_No readable trace artifact was found._`; } +async function aggregateMessagesWithFreshFullTrace(context: CaseContext, input: { archivedMessages: any; traceLookup: any; traceId: unknown; run: any; evidence: any; manifest: any }) { + const archivedMessages = input.archivedMessages; + const traceId = text(input.traceId ?? input.traceLookup?.traceId ?? input.run?.agent?.traceId ?? input.evidence?.agent?.traceId ?? input.manifest?.agent?.traceId); + const baseUrl = text(input.traceLookup?.baseUrl ?? input.run?.agent?.baseUrl ?? input.evidence?.agent?.baseUrl); + const apiKey = text(context.env.HWLAB_API_KEY ?? process.env.HWLAB_API_KEY); + if (!traceId || !baseUrl || !apiKey.startsWith(HWLAB_API_KEY_PREFIX)) return archivedMessages; + const command = [process.execPath, path.join(context.cwd, "tools/hwlab-cli/bin/hwlab-cli.mjs"), "client", "agent", "trace", traceId, "--render", "web", "--full"]; + const result = await (context.runProcess ?? runProcess)(command, context.cwd, { ...process.env, ...context.env, HWLAB_RUNTIME_WEB_URL: baseUrl }); + if (result.exitCode !== 0) return archivedMessages; + const parsed = parseJsonMaybe(result.stdout); + const rendered = freshRenderedTraceBody(parsed); + const rows = arrayOfObjects(rendered?.rows); + if (rows.length === 0) return archivedMessages; + return clean({ + ...(archivedMessages && typeof archivedMessages === "object" ? archivedMessages : {}), + renderer: rendered.renderer ?? archivedMessages?.renderer ?? CASE_TRACE_RENDERER, + traceSource: rendered.source ?? archivedMessages?.traceSource, + traceLookup: archivedMessages?.traceLookup ?? input.traceLookup, + traceId: rendered.traceId ?? traceId, + status: rendered.traceStatus ?? rendered.status ?? archivedMessages?.status, + sourceEventCount: rendered.sourceEventCount ?? archivedMessages?.sourceEventCount, + renderedRowCount: rendered.renderedRowCount ?? rows.length, + returnedRowCount: rendered.returnedRowCount ?? rows.length, + noiseEventCount: rendered.noiseEventCount ?? archivedMessages?.noiseEventCount, + finalResponse: rendered.finalResponse ?? archivedMessages?.finalResponse, + rows + }); +} + +function freshRenderedTraceBody(parsed: any) { + if (!parsed || typeof parsed !== "object") return null; + const body = parsed.body && typeof parsed.body === "object" ? parsed.body : null; + if (Array.isArray(body?.rows)) return body; + const data = parsed.data && typeof parsed.data === "object" ? parsed.data : null; + if (Array.isArray(data?.rows)) return data; + return Array.isArray(parsed.rows) ? parsed : null; +} + function traceEventRowFromAggregate(row: Record): TraceEventRow { const bodyFormat = text(row.bodyFormat); return {