Files
pikasTech-HWLAB/internal/harnessrl/activities.ts
T
2026-07-17 04:48:32 +02:00

221 lines
15 KiB
TypeScript

import { heartbeat } from "@temporalio/activity";
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { buildCaseRun, collectAgentDiff, collectAgentTraceEvidence, collectCaseRun, prepareCaseRun, recoverAgentTaskStage, recoverBuildCaseRun, runAgentTaskStage } from "../../tools/src/hwlab-caserun-lib.ts";
import type { HarnessRLRegistry } from "./contracts.ts";
import { caseRunInternalFetch } from "../cloud/server-caserun-http.ts";
export function createHarnessRLActivities(options: {
registry: HarnessRLRegistry;
env?: Record<string, string | undefined>;
stages?: Partial<{
prepare(context: any, action: string): Promise<any>;
agent(context: any, run: any): Promise<any>;
recoverAgent(context: any, run: any): Promise<any>;
trace(context: any, run: any): Promise<any>;
diff(context: any, run: any): Promise<any>;
build(context: any, run: any): Promise<any>;
recoverBuild(context: any, run: any): Promise<any>;
collect(context: any, run: any, evidence: any): Promise<any>;
}>;
}) {
const env = options.env ?? process.env;
const registry = options.registry;
const stages = {
prepare: options.stages?.prepare ?? prepareCaseRun,
agent: options.stages?.agent ?? runAgentTaskStage,
recoverAgent: options.stages?.recoverAgent ?? recoverAgentTaskStage,
trace: options.stages?.trace ?? collectAgentTraceEvidence,
diff: options.stages?.diff ?? collectAgentDiff,
build: options.stages?.build ?? buildCaseRun,
recoverBuild: options.stages?.recoverBuild ?? recoverBuildCaseRun,
collect: options.stages?.collect ?? collectCaseRun
};
return {
async markRunning({ runId }: { runId: string }) {
await transition(registry, runId, "running", "prepare", { worker: "hwlab-harnessrl-worker" });
},
async prepare(input: { runId: string; identity: string }) {
return idempotent(registry, input, async () => {
const record = required(await registry.getRun(input.runId));
const definition = await loadDefinition(record.caseRepo, record.caseId);
activityHeartbeat({ runId: input.runId, stage: "prepare" });
if (definition.mode === "software-smoke") {
await mkdir(record.runDir, { recursive: true });
const output = { mode: "software-smoke", run: { runId: record.runId, caseId: record.caseId, mode: "software-smoke", hardwareRequired: false } };
await transition(registry, input.runId, "running", "prepared", { mode: "software-smoke" });
return output;
}
const context = contextForRecord(record, env, input.identity);
const prepared = await stages.prepare(context, "harnessrl-worker");
const output = { mode: definition.mode, prepared } as Record<string, unknown>;
await transition(registry, input.runId, "running", "prepared", { runDir: prepared.runDir, specPath: prepared.specPath });
return output;
});
},
async agent(input: { runId: string; identity: string }) {
return idempotent(registry, input, async () => {
const record = required(await registry.getRun(input.runId));
const prepared = activityOutput(required(await registry.getActivityResult(input.runId, "prepare", `${input.runId}:prepare:v1`))) as any;
if (prepared.mode === "software-smoke") return { mode: prepared.mode, run: prepared.run, authority: { agentRun: "not-applicable" } };
const stage = await stages.agent(contextForRecord(record, env, input.identity), prepared.prepared.run);
await transition(registry, input.runId, "running", "agent-completed", { traceId: stage.agent?.traceId ?? null, sessionId: stage.agent?.sessionId ?? null });
return { mode: prepared.mode, run: stage.run, agent: stage.agent };
}, async () => {
const record = required(await registry.getRun(input.runId));
const prepared = activityOutput(required(await registry.getActivityResult(input.runId, "prepare", `${input.runId}:prepare:v1`))) as any;
const stage = await stages.recoverAgent(contextForRecord(record, env, input.identity), prepared.prepared.run);
await transition(registry, input.runId, "running", "agent-completed", { traceId: stage.agent?.traceId ?? null, sessionId: stage.agent?.sessionId ?? null, recovered: true });
return { mode: prepared.mode, run: stage.run, agent: stage.agent };
});
},
async trace(input: { runId: string; identity: string }) {
return idempotent(registry, input, async () => {
const record = required(await registry.getRun(input.runId));
const agent = activityOutput(required(await registry.getActivityResult(input.runId, "agent", `${input.runId}:agent:v1`))) as any;
if (agent.mode === "software-smoke") return agent;
const stage = await stages.trace(contextForRecord(record, env, input.identity), agent.run);
await transition(registry, input.runId, "running", "trace-completed", { traceId: stage.trace?.traceId ?? null });
return { ...agent, run: stage.run, trace: stage.trace };
});
},
async diff(input: { runId: string; identity: string }) {
return idempotent(registry, input, async () => {
const record = required(await registry.getRun(input.runId));
const traced = activityOutput(required(await registry.getActivityResult(input.runId, "trace", `${input.runId}:trace:v1`))) as any;
if (traced.mode === "software-smoke") return traced;
const stage = await stages.diff(contextForRecord(record, env, input.identity), traced.run);
await transition(registry, input.runId, "running", "diff-completed", { diffSha256: stage.diff?.diffPatchSha256 ?? null });
return { ...traced, run: stage.run, diff: stage.diff };
});
},
async build(input: { runId: string; identity: string }) {
return idempotent(registry, input, async () => {
const record = required(await registry.getRun(input.runId));
const prepared = activityOutput(required(await registry.getActivityResult(input.runId, "diff", `${input.runId}:diff:v1`))) as any;
activityHeartbeat({ runId: input.runId, stage: "build" });
if (prepared.mode === "software-smoke") {
const artifact = {
contractVersion: "hwlab-caserun-software-smoke-v1", caseId: record.caseId, runId: record.runId,
mode: "software-smoke", status: "completed", hardwareRequired: false,
checks: { caseDefinitionLoaded: true, runDirectoryWritable: true, temporalWorkerRunning: true }
};
const text = `${JSON.stringify(artifact, null, 2)}\n`;
await writeFile(path.join(record.runDir, "software-smoke.json"), text, "utf8");
const artifactRef = { path: "software-smoke.json", bytes: Buffer.byteLength(text), sha256: createHash("sha256").update(text).digest("hex") };
const output = { mode: "software-smoke", run: prepared.run, summary: { status: "completed", mode: "software-smoke", artifactCount: 1, artifacts: [artifactRef] }, evidence: { status: "recorded", mode: "software-smoke", artifacts: [artifactRef] } };
await transition(registry, input.runId, "running", "build-completed", { artifactSha256: artifactRef.sha256 });
return output;
}
const context = contextForRecord(record, env, input.identity);
const build = await stages.build(context, prepared.run);
await transition(registry, input.runId, "running", "build-completed", { jobId: build.summary?.jobId ?? build.evidence?.keilJob?.jobId ?? null });
return { mode: prepared.mode, run: build.run ?? prepared.run, build, agent: prepared.agent, trace: prepared.trace, diff: prepared.diff };
}, async () => {
const record = required(await registry.getRun(input.runId));
const prepared = activityOutput(required(await registry.getActivityResult(input.runId, "diff", `${input.runId}:diff:v1`))) as any;
const build = await stages.recoverBuild(contextForRecord(record, env, input.identity), prepared.run);
await transition(registry, input.runId, "running", "build-completed", { jobId: build.summary?.jobId ?? build.evidence?.keilJob?.jobId ?? null, recovered: true });
return { mode: prepared.mode, run: build.run ?? prepared.run, build, agent: prepared.agent, trace: prepared.trace, diff: prepared.diff };
});
},
async collect(input: { runId: string; identity: string }) {
return idempotent(registry, input, async () => {
const record = required(await registry.getRun(input.runId));
const built = activityOutput(required(await registry.getActivityResult(input.runId, "build", `${input.runId}:build:v1`))) as any;
activityHeartbeat({ runId: input.runId, stage: "collect" });
let result: Record<string, unknown>;
if (built.mode === "software-smoke") result = { summary: built.summary, evidence: built.evidence, run: built.run };
else {
const context = contextForRecord(record, env, input.identity);
const collected = await stages.collect(context, built.run, built.build.evidence);
const manifest = await manifestRefs(collected.summary);
result = {
summary: built.build.summary, evidence: built.build.evidence, run: collected.run ?? built.run,
authority: { agentRun: authorityRefs(built), hwpod: built.build.evidence?.operation ?? built.build.evidence?.validation ?? null },
artifactManifestPath: manifest.path, artifactManifestSha256: manifest.sha256,
aggregate: manifest.aggregate,
collected: { status: collected.status, summary: collected.summary ?? null }
};
}
await registry.updateRun(input.runId, { result });
await transition(registry, input.runId, "running", "collect-completed", {});
return result;
});
},
async markCompleted({ runId }: { runId: string }) { await transition(registry, runId, "completed", "completed", {}); },
async markCanceled({ runId }: { runId: string }) { await transition(registry, runId, "canceled", "canceled", {}); },
async markFailed(input: { runId: string; code: string; message: string }) {
await registry.updateRun(input.runId, { status: "failed", stage: "failed", error: { code: input.code, message: input.message } });
await registry.appendEvent(input.runId, "failed", "failed", { code: input.code, message: input.message.slice(0, 500) });
}
};
}
async function idempotent(registry: HarnessRLRegistry, input: { runId: string; identity: string }, execute: () => Promise<Record<string, unknown>>, recover?: () => Promise<Record<string, unknown>>) {
const activityName = input.identity.split(":").at(-2) ?? "activity";
const existing = await registry.getActivityResult(input.runId, activityName, input.identity);
if ((existing?.output as any)?.state === "completed") return (existing!.output as any).result;
if (["started", "accepted"].includes((existing?.output as any)?.state) && recover) {
let output: Record<string, unknown>;
try {
output = await recover();
} catch (error: any) {
if (!authorityNotFound(error)) throw error;
output = await execute();
}
const saved = await registry.saveActivityResult({ runId: input.runId, activityName, identity: input.identity, output: { state: "completed", authorityIdentity: input.identity, result: output } });
return activityOutput(saved);
}
if (!existing) await registry.saveActivityResult({ runId: input.runId, activityName, identity: input.identity, output: { state: "started", authorityIdentity: input.identity } });
const output = await execute();
const saved = await registry.saveActivityResult({ runId: input.runId, activityName, identity: input.identity, output: { state: "completed", authorityIdentity: input.identity, result: output } });
return activityOutput(saved);
}
async function transition(registry: HarnessRLRegistry, runId: string, status: any, stage: string, payload: Record<string, unknown>) {
await registry.updateRun(runId, { status, stage });
await registry.appendEvent(runId, status, stage, payload);
}
function contextForRecord(record: any, env: Record<string, string | undefined>, identity: string) {
const apiUrl = String(env.HWLAB_CASERUN_INTERNAL_API_URL ?? env.HWLAB_RUNTIME_INTERNAL_API_URL ?? record.runtimeApiUrl).replace(/\/+$/u, "");
const traceId = `trc_harnessrl_${createHash("sha256").update(identity).digest("hex").slice(0, 24)}`;
return {
parsed: { _: [], caseId: record.caseId, runId: record.runId, runDir: record.runDir, caseRepo: record.caseRepo, apiUrl, traceId, operationIdentity: identity, noCaseRepoRecord: false, stateDir: path.join(path.dirname(record.runDir), "cli") },
env: { ...process.env, ...env, HWLAB_CASE_REPO: record.caseRepo, HWLAB_RUNTIME_API_URL: apiUrl, HWLAB_CASERUN_PUBLIC_RUNTIME_API_URL: record.runtimeApiUrl, HWLAB_RUNTIME_ENDPOINT_LOCKED: "1", HWLAB_HWPOD_OPERATION_IDENTITY: identity },
fetchImpl: caseRunInternalFetch(globalThis.fetch, apiUrl, env), cwd: record.caseRepo,
now: () => new Date().toISOString(), sleep: (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)), argv: [], rest: ["worker", record.caseId]
};
}
function activityOutput(result: { output: Record<string, unknown> }) {
return (result.output as any)?.state === "completed" ? (result.output as any).result : result.output;
}
function authorityNotFound(error: any) {
const code = String(error?.code ?? "");
return code === "hwpod_operation_not_found" || code.endsWith("_not_found");
}
async function manifestRefs(summary: any) {
const manifestPath = String(summary?.artifactManifestPath ?? "");
if (!manifestPath) return { path: null, sha256: null, aggregate: null };
const body = await readFile(manifestPath);
const manifest = JSON.parse(body.toString("utf8"));
return { path: manifestPath, sha256: createHash("sha256").update(body).digest("hex"), aggregate: manifest.aggregate ?? null };
}
function authorityRefs(built: any) {
const agent = built.agent ?? built.run?.agent ?? {};
return { conversationId: agent.conversationId ?? null, sessionId: agent.sessionId ?? null, threadId: agent.threadId ?? null, traceId: agent.traceId ?? null, resultUrl: agent.resultUrl ?? null, commandStatus: agent.commandStatus ?? null, final: agent.finalResponse ?? agent.result ?? null, diffSha256: built.diff?.diffPatchSha256 ?? built.run?.agentDiff?.diffPatchSha256 ?? null };
}
async function loadDefinition(caseRepo: string, caseId: string) {
return JSON.parse(await readFile(path.join(caseRepo, "cases", caseId, "case.json"), "utf8"));
}
function required<T>(value: T | null): T { if (value === null) throw Object.assign(new Error("required registry record was not found"), { code: "registry_record_not_found" }); return value; }
function activityHeartbeat(details: Record<string, unknown>) { try { heartbeat(details); } catch {} }