256 lines
15 KiB
TypeScript
256 lines
15 KiB
TypeScript
import { readFile, readdir, stat } from "node:fs/promises";
|
|
import path from "node:path";
|
|
|
|
import {
|
|
archiveCaseRunArtifacts,
|
|
syncCaseRegistryRel,
|
|
writeCaseAggregateFromRegistry,
|
|
} from "./hwlab-caserun-closeout.ts";
|
|
import { agentToolCallSummaryFromResult, arrayOfObjects, clean, clipText, text } from "./hwlab-caserun-evidence.ts";
|
|
import type { CaseContext, PreparedCaseRun } from "./hwlab-caserun-shared.ts";
|
|
|
|
const MAX_DIAGNOSTIC_ROWS = 30;
|
|
|
|
export async function refreshCaseRegistryArtifacts(context: CaseContext, caseRepo: string) {
|
|
const caseId = text(context.parsed.caseId ?? context.rest[1]);
|
|
const runId = text(context.parsed.runId ?? context.rest[2]);
|
|
if (!caseId) throw cliError("missing_required_value", "caseId is required", { field: "caseId" });
|
|
if (!runId) throw cliError("missing_required_value", "runId is required", { field: "runId" });
|
|
const caseRepoRunDir = path.join(caseRepo, "runs", caseId, runId);
|
|
const stored = await loadRegistryArtifacts(caseRepo, caseId, runId);
|
|
const source = text(context.parsed.source ?? context.parsed.from) || "local";
|
|
if (source !== "local" && source !== "runtime") throw cliError("unsupported_case_refresh_source", "case refresh source must be local or runtime", { source });
|
|
const run = registryPreparedRun(caseRepo, caseId, runId, caseRepoRunDir, stored.run, stored.evidence, stored.manifest, context.now());
|
|
const evidence = {
|
|
...stored.evidence,
|
|
caseId,
|
|
runId,
|
|
autoEvaluation: false,
|
|
refresh: { source, authority: "registry", runtimeAccess: source === "runtime" },
|
|
};
|
|
const offlineContext = { ...context, env: { ...context.env, HWLAB_API_KEY: undefined }, parsed: { ...context.parsed, noCaseRepoGitSync: true } };
|
|
const archiveContext = source === "runtime" ? { ...context, parsed: { ...context.parsed, caseRefreshSource: "runtime" } } : offlineContext;
|
|
const first = await archiveCaseRunArtifacts(archiveContext, run, evidence, { sync: false });
|
|
const aggregateContext = { ...offlineContext, parsed: { ...offlineContext.parsed, caseId, runId }, rest: ["aggregate", caseId, runId] };
|
|
await writeCaseAggregateFromRegistry(aggregateContext, caseRepo, { sync: false });
|
|
const final = await archiveCaseRunArtifacts(offlineContext, run, evidence, { sync: false });
|
|
const sync = context.parsed.noCaseRepoGitSync === true
|
|
? null
|
|
: await syncCaseRegistryRel(context, caseRepo, `runs/${caseId}/${runId}`, `data: refresh caserun artifacts ${runId}`);
|
|
const trace = (final.manifest.trace ?? first.manifest.trace ?? {}) as Record<string, any>;
|
|
return ok("case.refresh", {
|
|
caseId,
|
|
runId,
|
|
caseRepo,
|
|
caseRepoRunDir,
|
|
source,
|
|
refreshed: {
|
|
files: final.caseRepoFiles,
|
|
traceId: trace.traceId,
|
|
sourceEventCount: trace.sourceEventCount,
|
|
renderedRowCount: trace.renderedRowCount,
|
|
commandCount: trace.commandCount,
|
|
hwpodCommandCount: trace.hwpodCommandCount,
|
|
hwpodBuildCommandCount: trace.hwpodBuildCommandCount,
|
|
manifestPath: final.artifactManifestPath,
|
|
registrySync: sync,
|
|
provenance: evidence.refresh,
|
|
},
|
|
aggregationOnly: false,
|
|
autoEvaluation: false,
|
|
});
|
|
}
|
|
|
|
export async function inspectCaseRegistryRun(context: CaseContext, caseRepo: string, view: "inspect" | "trace-summary") {
|
|
const artifacts = await loadDiagnosticByRunId(caseRepo, text(context.parsed.runId ?? context.rest[2]), text(context.parsed.caseId ?? context.parsed.case));
|
|
const diagnostic = diagnosticFromArtifacts(artifacts);
|
|
const payload = view === "trace-summary" ? traceSummary(diagnostic) : diagnostic;
|
|
return ok(`case.run.${view}`, { ...payload, inspectionOnly: true, serviceRuntime: false, autoEvaluation: false });
|
|
}
|
|
|
|
export async function skillUsageCaseRegistryRun(context: CaseContext, caseRepo: string) {
|
|
const skill = text(context.parsed.skill ?? context.parsed.skillName ?? context.rest[3]);
|
|
if (!skill) throw cliError("missing_required_value", "skill is required", { field: "skill" });
|
|
const artifacts = await loadDiagnosticByRunId(caseRepo, text(context.parsed.runId ?? context.rest[2]), text(context.parsed.caseId ?? context.parsed.case));
|
|
const pattern = new RegExp(`(?:\\.agents[\\\\/]skills[\\\\/]${escapeRegExp(skill)}[\\\\/]SKILL\\.md|\\b${escapeRegExp(skill)}\\b)`, "iu");
|
|
const bundleSources = [
|
|
["artifact-manifest.json", artifacts.manifest.resourceBundle ?? artifacts.manifest.provenance?.resourceBundle],
|
|
["evidence.json", artifacts.evidence.resourceBundle ?? artifacts.evidence.provenance?.resourceBundle],
|
|
] as const;
|
|
const evidence = bundleSources
|
|
.filter(([, value]) => value && pattern.test(JSON.stringify(value)))
|
|
.map(([source, value]) => ({ source, materialized: value.materialized === true, preview: clipText(JSON.stringify(value).replace(/\s+/gu, " "), 500) }));
|
|
const rows = traceRows(artifacts);
|
|
const allMatches = rows.filter((row) => pattern.test(`${row.header ?? ""}\n${row.body ?? ""}`));
|
|
const matches = allMatches.slice(0, MAX_DIAGNOSTIC_ROWS).map((row) => ({ rowId: row.rowId, seq: row.seq, header: row.header, preview: clipText(row.body, 500) }));
|
|
return ok("case.run.skill-usage", {
|
|
caseId: artifacts.caseId,
|
|
runId: artifacts.runId,
|
|
caseRepo,
|
|
caseRepoRunDir: artifacts.caseRepoRunDir,
|
|
skill,
|
|
prompt: { mentionsSkill: pattern.test(artifacts.promptText), path: path.join(artifacts.caseRepoRunDir, "agent-prompt.md") },
|
|
trace: { readSkillFile: allMatches.length > 0, matchCount: allMatches.length, matches },
|
|
resourceBundle: { materialized: evidence.some((item) => item.materialized), evidence },
|
|
inspectionOnly: true,
|
|
serviceRuntime: false,
|
|
autoEvaluation: false,
|
|
});
|
|
}
|
|
|
|
async function loadDiagnosticByRunId(caseRepo: string, runId: string, explicitCaseId: string) {
|
|
if (!runId) throw cliError("missing_required_value", "runId is required", { field: "runId" });
|
|
const caseId = explicitCaseId || await resolveCaseId(caseRepo, runId);
|
|
return loadRegistryArtifacts(caseRepo, caseId, runId);
|
|
}
|
|
|
|
async function loadRegistryArtifacts(caseRepo: string, caseId: string, runId: string) {
|
|
const caseRepoRunDir = path.join(caseRepo, "runs", caseId, runId);
|
|
const [run, evidence, manifest, agentTrace, agentMessages, finalResponseText, promptText] = await Promise.all([
|
|
readJson(path.join(caseRepoRunDir, "run.json")),
|
|
readJson(path.join(caseRepoRunDir, "evidence.json")),
|
|
readJson(path.join(caseRepoRunDir, "artifact-manifest.json")),
|
|
readJson(path.join(caseRepoRunDir, "agent-trace.json")),
|
|
readJson(path.join(caseRepoRunDir, "agent-messages.json")),
|
|
readText(path.join(caseRepoRunDir, "final-response.md")),
|
|
readText(path.join(caseRepoRunDir, "agent-prompt.md")),
|
|
]);
|
|
if (!run && !evidence && !manifest) throw cliError("case_registry_run_not_found", "case registry run artifacts were not found", { caseRepoRunDir, caseId, runId });
|
|
return { caseRepo, caseId, runId, caseRepoRunDir, run: run ?? {}, evidence: evidence ?? {}, manifest: manifest ?? {}, agentTrace, agentMessages, finalResponseText, promptText };
|
|
}
|
|
|
|
async function resolveCaseId(caseRepo: string, runId: string) {
|
|
const root = path.join(caseRepo, "runs");
|
|
const entries = await readdir(root).catch(() => []);
|
|
const matches: string[] = [];
|
|
for (const entry of entries.filter((item) => !item.startsWith("."))) {
|
|
if (await isDirectory(path.join(root, entry, runId))) matches.push(entry);
|
|
}
|
|
if (matches.length === 0) throw cliError("case_registry_run_not_found", "runId was not found in case registry runs", { caseRepo, runId });
|
|
if (matches.length > 1) throw cliError("case_registry_run_ambiguous", "runId exists under multiple caseIds; pass --case-id", { caseRepo, runId, caseIds: matches });
|
|
return matches[0];
|
|
}
|
|
|
|
function registryPreparedRun(caseRepo: string, caseId: string, runId: string, caseRepoRunDir: string, rawRun: any, rawEvidence: any, manifest: any, now: string): PreparedCaseRun {
|
|
const run = rawRun ?? {};
|
|
const evidence = rawEvidence ?? {};
|
|
const subject = { ...(run.subject ?? {}), ...(evidence.subject ?? {}), ...(manifest?.subject ?? {}) };
|
|
const agent = { ...(run.agent ?? {}), ...(evidence.agent ?? {}), ...(manifest?.agent ?? {}) };
|
|
return {
|
|
...run,
|
|
caseId,
|
|
runId,
|
|
runDir: caseRepoRunDir,
|
|
caseRepo,
|
|
caseDir: run.caseDir ?? "",
|
|
caseFile: run.caseFile ?? "",
|
|
sourceSpecPath: run.sourceSpecPath ?? path.join(caseRepoRunDir, ".hwlab", "hwpod-spec.yaml"),
|
|
specPath: run.specPath ?? path.join(caseRepoRunDir, ".hwlab", "hwpod-spec.yaml"),
|
|
apiUrl: run.apiUrl ?? evidence.apiUrl ?? "",
|
|
compileOnly: run.compileOnly !== false,
|
|
createdAt: run.createdAt ?? now,
|
|
updatedAt: now,
|
|
definition: run.definition ?? {},
|
|
subject: { repoLocalPath: subject.repoLocalPath ?? "", commitId: subject.commitId ?? "", subdir: subject.subdir ?? "", worktreePath: subject.worktreePath ?? "", workspacePath: subject.workspacePath ?? subject.worktreePath ?? "", setup: subject.setup ?? {} },
|
|
agent: agent.traceId ? agent : null,
|
|
agentTrace: evidence.agentTrace ?? manifest?.trace ?? run.agentTrace,
|
|
agentDiff: evidence.agentDiff ?? manifest?.diff ?? run.agentDiff,
|
|
} as PreparedCaseRun;
|
|
}
|
|
|
|
function diagnosticFromArtifacts(artifacts: any) {
|
|
const agent = { ...(artifacts.run.agent ?? {}), ...(artifacts.evidence.agent ?? {}), ...(artifacts.manifest.agent ?? {}) };
|
|
const trace = { ...(artifacts.run.agentTrace ?? {}), ...(artifacts.evidence.agentTrace ?? {}), ...(artifacts.manifest.trace ?? {}) };
|
|
const summary = boundedToolCallSummary(trace.toolCallSummary ?? agent.toolCallSummary ?? agentToolCallSummaryFromResult(artifacts.agentTrace?.body ?? artifacts.agentTrace) ?? null);
|
|
const rows = traceRows(artifacts);
|
|
const agentFinal = clean({ ...(artifacts.evidence.agentFinal ?? {}), ...(artifacts.manifest.agentFinal ?? {}) });
|
|
const commands = summary?.items ?? rows.filter((row) => /commandExecution|tool_call|^tool:/iu.test(`${row.rowId ?? ""} ${row.header ?? ""}`)).map((row) => ({ rowId: row.rowId, seq: row.seq, command: row.command ?? row.body, status: row.status ?? row.tone }));
|
|
return clean({
|
|
caseId: artifacts.caseId,
|
|
runId: artifacts.runId,
|
|
caseRepo: artifacts.caseRepo,
|
|
caseRepoRunDir: artifacts.caseRepoRunDir,
|
|
status: artifacts.run.status ?? artifacts.evidence.status ?? "recorded",
|
|
stage: artifacts.run.stage ?? artifacts.run._control?.stage,
|
|
compileOnly: artifacts.evidence.compileOnly ?? artifacts.run.compileOnly,
|
|
traceId: agent.traceId ?? trace.traceId,
|
|
sessionId: agent.sessionId ?? trace.sessionId,
|
|
conversationId: agent.conversationId ?? trace.conversationId,
|
|
threadId: agent.threadId ?? trace.threadId,
|
|
providerProfile: agent.providerProfile,
|
|
requestedProviderProfile: agent.requestedProviderProfile,
|
|
resolvedBackendProfile: agent.resolvedBackendProfile,
|
|
provider: agent.provider,
|
|
model: agent.model,
|
|
backend: agent.backend,
|
|
terminalStatus: trace.terminalStatus ?? agent.terminalStatus,
|
|
agentResultStatus: agent.result?.status ?? trace.status,
|
|
commandStatus: agent.commandStatus ?? summary?.commandStatus,
|
|
agentRunStatus: agent.agentRunStatus ?? summary?.agentRunStatus,
|
|
toolCallSummary: summary,
|
|
sourceEventCount: trace.sourceEventCount ?? artifacts.agentMessages?.sourceEventCount,
|
|
renderedRowCount: trace.renderedRowCount ?? artifacts.agentMessages?.renderedRowCount ?? rows.length,
|
|
commandCount: trace.commandCount ?? summary?.count ?? commands.length,
|
|
hwpodCommandCount: trace.hwpodCommandCount,
|
|
hwpodBuildCommandCount: trace.hwpodBuildCommandCount,
|
|
agentFinal,
|
|
postValidation: artifacts.manifest.postValidation ?? artifacts.evidence.postValidation,
|
|
refresh: artifacts.manifest.refresh ?? artifacts.evidence.refresh,
|
|
finalResponse: { present: agentFinal.present === true, reason: agentFinal.reason, path: path.join(artifacts.caseRepoRunDir, "final-response.md"), textPreview: clipText(artifacts.finalResponseText, 800) },
|
|
traceCommands: commands.slice(0, MAX_DIAGNOSTIC_ROWS),
|
|
paths: { runJson: path.join(artifacts.caseRepoRunDir, "run.json"), evidenceJson: path.join(artifacts.caseRepoRunDir, "evidence.json"), agentTraceJson: path.join(artifacts.caseRepoRunDir, "agent-trace.json"), agentMessagesJson: path.join(artifacts.caseRepoRunDir, "agent-messages.json"), artifactManifestJson: path.join(artifacts.caseRepoRunDir, "artifact-manifest.json"), aggregateMd: path.join(artifacts.caseRepoRunDir, "aggregate.md") },
|
|
});
|
|
}
|
|
|
|
function traceSummary(diagnostic: any) {
|
|
return clean({
|
|
caseId: diagnostic.caseId,
|
|
runId: diagnostic.runId,
|
|
caseRepo: diagnostic.caseRepo,
|
|
caseRepoRunDir: diagnostic.caseRepoRunDir,
|
|
traceId: diagnostic.traceId,
|
|
providerProfile: diagnostic.providerProfile,
|
|
requestedProviderProfile: diagnostic.requestedProviderProfile,
|
|
resolvedBackendProfile: diagnostic.resolvedBackendProfile,
|
|
provider: diagnostic.provider,
|
|
model: diagnostic.model,
|
|
backend: diagnostic.backend,
|
|
terminalStatus: diagnostic.terminalStatus,
|
|
agentResultStatus: diagnostic.agentResultStatus,
|
|
commandStatus: diagnostic.commandStatus,
|
|
agentRunStatus: diagnostic.agentRunStatus,
|
|
toolCallSummary: diagnostic.toolCallSummary,
|
|
sourceEventCount: diagnostic.sourceEventCount,
|
|
renderedRowCount: diagnostic.renderedRowCount,
|
|
commandCount: diagnostic.commandCount,
|
|
hwpodCommandCount: diagnostic.hwpodCommandCount,
|
|
hwpodBuildCommandCount: diagnostic.hwpodBuildCommandCount,
|
|
traceCommands: diagnostic.traceCommands,
|
|
paths: diagnostic.paths,
|
|
});
|
|
}
|
|
|
|
function boundedToolCallSummary(summary: any) {
|
|
if (!summary || typeof summary !== "object") return null;
|
|
return clean({
|
|
...summary,
|
|
items: arrayOfObjects(summary.items).slice(0, MAX_DIAGNOSTIC_ROWS).map((item) => clean({
|
|
...item,
|
|
command: clipText(item.command, 1000),
|
|
detail: clipText(item.detail, 1000),
|
|
})),
|
|
});
|
|
}
|
|
|
|
function traceRows(artifacts: any) {
|
|
const body = artifacts.agentTrace?.body && typeof artifacts.agentTrace.body === "object" ? artifacts.agentTrace.body : artifacts.agentTrace ?? {};
|
|
return arrayOfObjects(artifacts.agentMessages?.rows ?? body.rows ?? body.renderedRows ?? body.traceRows);
|
|
}
|
|
|
|
async function readJson(file: string) { try { return JSON.parse(await readFile(file, "utf8")); } catch { return null; } }
|
|
async function readText(file: string) { try { return await readFile(file, "utf8"); } catch { return ""; } }
|
|
async function isDirectory(file: string) { try { return (await stat(file)).isDirectory(); } catch { return false; } }
|
|
function escapeRegExp(value: string) { return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); }
|
|
function cliError(code: string, message: string, details: Record<string, unknown> = {}) { return Object.assign(new Error(message), { code, details }); }
|
|
function ok<T extends Record<string, unknown>>(action: string, extra: T) { return { ok: true, action, status: "completed", ...extra }; }
|