fix: 修正 CaseRun 模块审查问题
This commit is contained in:
@@ -5,24 +5,80 @@ import { createHash } from "node:crypto";
|
||||
import { copyFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { renderTraceRowsMarkdown, traceDisplayRows, traceNoiseEventCount, type TraceEventRow } from "./hwlab-cli/trace-renderer.ts";
|
||||
import { agentTerminalEvidence, commandsFromToolCallSummary, hwpodCommandKind, mentionsHwpod, summarizeAgentTrace } from "./hwlab-caserun-evidence.ts";
|
||||
import {
|
||||
agentTerminalEvidence,
|
||||
arrayOfObjects,
|
||||
clean,
|
||||
clipText,
|
||||
commandsFromToolCallSummary,
|
||||
compactObject,
|
||||
hwpodCommandKind,
|
||||
mentionsHwpod,
|
||||
numberOption,
|
||||
parseJsonMaybe,
|
||||
summarizeAgentTrace,
|
||||
text,
|
||||
uniqueStrings,
|
||||
} from "./hwlab-caserun-evidence.ts";
|
||||
import type { ArchivedAgentTraceSnapshot, AgentTraceCommand, AgentTraceStage, CaseContext, CaseReadableAgentArchive, CaseRegistryArchive, CaseRegistryArchiveFile, PreparedCaseRun } from "./hwlab-caserun-shared.ts";
|
||||
const CASE_TRACE_RENDERER = "tools/src/hwlab-cli/trace-renderer:traceDisplayRows"; const MAX_AGENT_TRACE_COMMANDS = 30; const HWLAB_API_KEY_PREFIX = "hwl_live_";
|
||||
function text(value: unknown) { return String(value ?? "").trim(); }
|
||||
function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; }
|
||||
function clean(value: any) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)); }
|
||||
function compactObject(value: any) { const json = JSON.stringify(value ?? null); return json.length <= 4000 ? value : { clipped: true, bytes: Buffer.byteLength(json), preview: json.slice(0, 4000) }; }
|
||||
function sha256Buffer(value: Buffer) { return createHash("sha256").update(value).digest("hex"); }
|
||||
function parseJsonMaybe(value: unknown) { if (value && typeof value === "object") return value as any; try { return JSON.parse(String(value ?? "").trim()); } catch { return null; } }
|
||||
function requiredText(value: unknown, field: string) { const result = text(value); if (!result) throw Object.assign(new Error(`${field} is required`), { code: "missing_required_value", details: { field } }); return result; }
|
||||
function clipText(value: unknown, maxBytes = 2000) { const raw = String(value ?? ""); const buffer = Buffer.from(raw, "utf8"); return buffer.length <= maxBytes ? raw : `${buffer.subarray(0, maxBytes).toString("utf8")}\n... clipped ...`; }
|
||||
function arrayOfObjects(value: unknown): any[] { return Array.isArray(value) ? value.filter((item) => item && typeof item === "object" && !Array.isArray(item)) : []; }
|
||||
function uniqueStrings(values: string[]) { return Array.from(new Set(values.map(text).filter(Boolean))); }
|
||||
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 ""; } }
|
||||
async function fileExists(file: string) { try { await stat(file); return true; } catch { return false; } }
|
||||
async function writeJson(file: string, value: any) { await mkdir(path.dirname(file), { recursive: true }); await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8"); }
|
||||
async function runProcess(command: string[], cwd: string, env: Record<string, string | undefined>) { const proc = Bun.spawn(command, { cwd, env, stdout: "pipe", stderr: "pipe" }); const [stdout, stderr, exitCode] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text(), proc.exited]); return { command, stdout, stderr, exitCode }; }
|
||||
const CASE_TRACE_RENDERER = "tools/src/hwlab-cli/trace-renderer:traceDisplayRows";
|
||||
const MAX_AGENT_TRACE_COMMANDS = 30;
|
||||
const HWLAB_API_KEY_PREFIX = "hwl_live_";
|
||||
|
||||
function sha256Buffer(value: Buffer) {
|
||||
return createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function requiredText(value: unknown, field: string) {
|
||||
const result = text(value);
|
||||
if (!result) {
|
||||
throw Object.assign(new Error(`${field} is required`), {
|
||||
code: "missing_required_value",
|
||||
details: { field },
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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 "";
|
||||
}
|
||||
}
|
||||
|
||||
async function fileExists(file: string) {
|
||||
try {
|
||||
await stat(file);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function writeJson(file: string, value: any) {
|
||||
await mkdir(path.dirname(file), { recursive: true });
|
||||
await writeFile(file, `${JSON.stringify(value, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
async function runProcess(command: string[], cwd: string, env: Record<string, string | undefined>) {
|
||||
const proc = Bun.spawn(command, { cwd, env, stdout: "pipe", stderr: "pipe" });
|
||||
const [stdout, stderr, exitCode] = await Promise.all([
|
||||
new Response(proc.stdout).text(),
|
||||
new Response(proc.stderr).text(),
|
||||
proc.exited,
|
||||
]);
|
||||
return { command, stdout, stderr, exitCode };
|
||||
}
|
||||
export function agentTraceLookupForRun(run: any, evidence?: any) {
|
||||
const agent = run.agent ?? evidence?.agent;
|
||||
const traceId = text(agent?.traceId ?? evidence?.agentTrace?.traceId ?? run.agentTrace?.traceId);
|
||||
|
||||
@@ -112,11 +112,44 @@ export function firstTextOption(...values: any[]) { return values.map(text).find
|
||||
export function firstNumberOption(...values: any[]) { for (const value of values) { const number = numberOption(value); if (number !== undefined) return number; } return undefined; }
|
||||
function countedRecord(value: any, fallbacks: Array<string | undefined>) { if (value && typeof value === "object" && !Array.isArray(value)) return Object.fromEntries(Object.entries(value).map(([key, count]) => [key, Number(count) || 0])); return fallbacks.filter(Boolean).reduce<Record<string, number>>((result, key) => ({ ...result, [key as string]: (result[key as string] ?? 0) + 1 }), {}); }
|
||||
function sumRecordValues(value: Record<string, number>) { return Object.values(value).reduce((sum, count) => sum + count, 0); }
|
||||
function compactObject(value: any) { const json = JSON.stringify(value ?? null); return json.length <= 4000 ? value : { clipped: true, bytes: Buffer.byteLength(json), preview: json.slice(0, 4000) }; }
|
||||
function clipText(value: unknown, maxBytes: number) { const raw = String(value ?? ""); const buffer = Buffer.from(raw, "utf8"); return buffer.length <= maxBytes ? raw : `${buffer.subarray(0, maxBytes).toString("utf8")}\n... clipped ...`; }
|
||||
function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; }
|
||||
function text(value: unknown) { return String(value ?? "").trim(); }
|
||||
function clean<T extends Record<string, unknown>>(value: T): T { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)) as T; }
|
||||
export function compactObject(value: any) {
|
||||
const json = JSON.stringify(value ?? null);
|
||||
return json.length <= 4000
|
||||
? value
|
||||
: { clipped: true, bytes: Buffer.byteLength(json), preview: json.slice(0, 4000) };
|
||||
}
|
||||
|
||||
export function clipText(value: unknown, maxBytes = 2000) {
|
||||
const raw = String(value ?? "");
|
||||
const buffer = Buffer.from(raw, "utf8");
|
||||
return buffer.length <= maxBytes
|
||||
? raw
|
||||
: `${buffer.subarray(0, maxBytes).toString("utf8")}\n... clipped ...`;
|
||||
}
|
||||
|
||||
export function numberOption(value: unknown) {
|
||||
const parsed = Number.parseInt(String(value ?? ""), 10);
|
||||
return Number.isFinite(parsed) ? parsed : undefined;
|
||||
}
|
||||
|
||||
export function parseJsonMaybe(value: unknown) {
|
||||
if (value && typeof value === "object") return value as any;
|
||||
try {
|
||||
return JSON.parse(String(value ?? "").trim());
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function text(value: unknown) {
|
||||
return String(value ?? "").trim();
|
||||
}
|
||||
|
||||
export function clean<T extends Record<string, unknown>>(value: T): T {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null),
|
||||
) as T;
|
||||
}
|
||||
|
||||
type AgentTraceCommand = { source: string; seq?: number; rowId?: string; header?: string; toolName?: string; status?: string; command?: string; normalizedCommand?: string; bodyPreview?: string; exitCode?: number };
|
||||
const MAX_AGENT_TRACE_COMMANDS = 30;
|
||||
@@ -231,7 +264,7 @@ function selectRepresentativeHwpodCommands(commands: AgentTraceCommand[]) {
|
||||
});
|
||||
}
|
||||
|
||||
function arrayOfObjects(value: unknown): Record<string, unknown>[] {
|
||||
export function arrayOfObjects(value: unknown): Record<string, unknown>[] {
|
||||
return Array.isArray(value) ? value.filter((item) => item && typeof item === "object" && !Array.isArray(item)) as Record<string, unknown>[] : [];
|
||||
}
|
||||
|
||||
@@ -271,6 +304,6 @@ function unquoteShellCommand(value: string) {
|
||||
return value.replace(/^\/bin\/sh\s+-lc\s+/u, "").replace(/^['"]|['"]$/gu, "").trim();
|
||||
}
|
||||
|
||||
function uniqueStrings(values: string[]) {
|
||||
export function uniqueStrings(values: string[]) {
|
||||
return Array.from(new Set(values.map((value) => text(value)).filter(Boolean)));
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
import type { AgentDiffCollectionSummary } from "./hwlab-caserun-diff.ts";
|
||||
import type { AgentToolCallSummary } from "./hwlab-caserun-evidence.ts";
|
||||
import type { TraceEventRow } from "./hwlab-cli/trace-renderer.ts";
|
||||
|
||||
export type EnvLike = Record<string, string | undefined>;
|
||||
export type ParsedArgs = Record<string, unknown> & { _: string[] };
|
||||
|
||||
@@ -5,17 +5,27 @@ import { createHash, randomUUID } from "node:crypto";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { readHwpodSpec } from "./hwpod-harness-lib.ts";
|
||||
import { firstNumberOption } from "./hwlab-caserun-evidence.ts";
|
||||
import {
|
||||
clean,
|
||||
clipText,
|
||||
compactObject,
|
||||
firstNumberOption,
|
||||
numberOption,
|
||||
parseJsonMaybe,
|
||||
text,
|
||||
} from "./hwlab-caserun-evidence.ts";
|
||||
import type { CaseContext, PreparedCaseRun, PreparedSubjectRun, SourceRootSnapshot } from "./hwlab-caserun-shared.ts";
|
||||
const DEFAULT_POLL_INTERVAL_MS = 1000; const DEFAULT_JOB_TIMEOUT_MS = 50000; const MAX_JOB_TIMEOUT_MS = 300000;
|
||||
function text(value: unknown) { return String(value ?? "").trim(); }
|
||||
function numberOption(value: unknown) { const parsed = Number.parseInt(String(value ?? ""), 10); return Number.isFinite(parsed) ? parsed : undefined; }
|
||||
function clean(value: any) { return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)); }
|
||||
function compactObject(value: any) { const json = JSON.stringify(value ?? null); return json.length <= 4000 ? value : { clipped: true, bytes: Buffer.byteLength(json), preview: json.slice(0, 4000) }; }
|
||||
function clipText(value: unknown, maxBytes = 2000) { const raw = String(value ?? ""); const buffer = Buffer.from(raw, "utf8"); return buffer.length <= maxBytes ? raw : `${buffer.subarray(0, maxBytes).toString("utf8")}\n... clipped ...`; }
|
||||
function parseJsonMaybe(value: unknown) { if (value && typeof value === "object") return value as any; try { return JSON.parse(String(value ?? "").trim()); } catch { return null; } }
|
||||
function cliError(code: string, message: string, details: any = {}) { return Object.assign(new Error(message), { code, details }); }
|
||||
function slug(value: string) { return value.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "") || "case"; }
|
||||
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
||||
const DEFAULT_JOB_TIMEOUT_MS = 50000;
|
||||
const MAX_JOB_TIMEOUT_MS = 300000;
|
||||
|
||||
function cliError(code: string, message: string, details: any = {}) {
|
||||
return Object.assign(new Error(message), { code, details });
|
||||
}
|
||||
|
||||
function slug(value: string) {
|
||||
return value.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-|-$/gu, "") || "case";
|
||||
}
|
||||
export async function prepareSubjectWorktree(context: CaseContext, input: { caseId: string; runId: string; runDir: string; apiUrl: string; definition: Record<string, unknown>; sourceSpecPath: string }, collectSnapshot: (context: CaseContext, run: PreparedCaseRun, label: string) => Promise<SourceRootSnapshot>) {
|
||||
const subject = subjectFromDefinition(input.definition);
|
||||
const worktreePath = subjectWorktreePath(subject.repoLocalPath, input.runId);
|
||||
|
||||
Reference in New Issue
Block a user