feat: 补齐 HWLAB 手动调度能力
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
import type { AgentRunStore } from "./store.js";
|
||||
import type { CommandRecord, JsonRecord, JsonValue, RunEvent, RunRecord, RunnerJobRecord, TerminalStatus } from "../common/types.js";
|
||||
|
||||
export async function buildRunResult(store: AgentRunStore, runId: string, commandId?: string): Promise<JsonRecord> {
|
||||
const run = await store.getRun(runId);
|
||||
const command = await selectCommand(store, runId, commandId);
|
||||
const events = await store.listEvents(runId, 0, 500);
|
||||
const jobs = await store.listRunnerJobs(runId, command?.id);
|
||||
const latestJob = jobs.at(-1) ?? null;
|
||||
const terminal = terminalFromEvents(events) ?? run.terminalStatus;
|
||||
const failureKind = run.failureKind ?? failureKindFromEvents(events);
|
||||
const reply = assistantReply(events);
|
||||
const blocker = terminal === "blocked" || terminal === "failed" ? { failureKind, message: run.failureMessage ?? messageFromEvents(events) } : null;
|
||||
return {
|
||||
runId: run.id,
|
||||
commandId: command?.id ?? commandId ?? null,
|
||||
attemptId: latestJob?.attemptId ?? attemptFromEvents(events),
|
||||
runnerId: latestJob?.runnerId ?? null,
|
||||
jobName: latestJob?.jobName ?? null,
|
||||
namespace: latestJob?.namespace ?? null,
|
||||
status: command?.state ?? run.status,
|
||||
runStatus: run.status,
|
||||
commandState: command?.state ?? null,
|
||||
terminalStatus: terminal,
|
||||
reply,
|
||||
failureKind,
|
||||
failureMessage: run.failureMessage ?? messageFromEvents(events),
|
||||
blocker,
|
||||
lastSeq: events.at(-1)?.seq ?? 0,
|
||||
eventCount: events.length,
|
||||
artifactSummary: artifactSummary(events),
|
||||
sessionRef: sessionSummary(run),
|
||||
resourceBundleRef: resourceBundleSummary(run, events),
|
||||
runnerJobCount: jobs.length,
|
||||
};
|
||||
}
|
||||
|
||||
async function selectCommand(store: AgentRunStore, runId: string, commandId?: string): Promise<CommandRecord | null> {
|
||||
if (commandId) {
|
||||
const command = await store.getCommand(commandId);
|
||||
return command.runId === runId ? command : null;
|
||||
}
|
||||
const commands = await store.listCommands(runId, 0, 100);
|
||||
return commands.at(-1) ?? null;
|
||||
}
|
||||
|
||||
function terminalFromEvents(events: RunEvent[]): TerminalStatus | null {
|
||||
for (const event of [...events].reverse()) {
|
||||
if (event.type !== "terminal_status") continue;
|
||||
const value = event.payload.terminalStatus;
|
||||
if (value === "completed" || value === "failed" || value === "blocked" || value === "cancelled") return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function failureKindFromEvents(events: RunEvent[]): string | null {
|
||||
for (const event of [...events].reverse()) {
|
||||
const value = event.payload.failureKind;
|
||||
if (typeof value === "string") return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function messageFromEvents(events: RunEvent[]): string | null {
|
||||
for (const event of [...events].reverse()) {
|
||||
const value = event.payload.message;
|
||||
if (typeof value === "string" && value.length > 0) return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function assistantReply(events: RunEvent[]): string {
|
||||
return events
|
||||
.filter((event) => event.type === "assistant_message")
|
||||
.map((event) => textPayload(event.payload))
|
||||
.filter((text) => text.length > 0)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function textPayload(payload: JsonRecord): string {
|
||||
for (const key of ["text", "content", "delta"]) {
|
||||
const value = payload[key];
|
||||
if (typeof value === "string") return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function artifactSummary(events: RunEvent[]): JsonRecord {
|
||||
let commandOutputEvents = 0;
|
||||
let diffEvents = 0;
|
||||
let toolCallEvents = 0;
|
||||
let outputChars = 0;
|
||||
let truncatedEvents = 0;
|
||||
for (const event of events) {
|
||||
if (event.type === "command_output") {
|
||||
commandOutputEvents += 1;
|
||||
const text = textPayload(event.payload);
|
||||
outputChars += text.length;
|
||||
if (event.payload.truncated === true) truncatedEvents += 1;
|
||||
}
|
||||
if (event.type === "diff") diffEvents += 1;
|
||||
if (event.type === "tool_call") toolCallEvents += 1;
|
||||
}
|
||||
return { commandOutputEvents, diffEvents, toolCallEvents, outputChars, truncatedEvents };
|
||||
}
|
||||
|
||||
function attemptFromEvents(events: RunEvent[]): string | null {
|
||||
for (const event of [...events].reverse()) {
|
||||
const value = event.payload.attemptId;
|
||||
if (typeof value === "string") return value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function sessionSummary(run: RunRecord): JsonRecord | null {
|
||||
if (!run.sessionRef) return null;
|
||||
return {
|
||||
sessionId: run.sessionRef.sessionId,
|
||||
conversationId: run.sessionRef.conversationId ?? null,
|
||||
threadId: run.sessionRef.threadId ?? null,
|
||||
expiresAt: run.sessionRef.expiresAt ?? null,
|
||||
metadataKeys: Object.keys(run.sessionRef.metadata ?? {}).sort(),
|
||||
valuesPrinted: false,
|
||||
};
|
||||
}
|
||||
|
||||
function resourceBundleSummary(run: RunRecord, events: RunEvent[]): JsonRecord | null {
|
||||
if (!run.resourceBundleRef) return null;
|
||||
const materialized = events.find((event) => event.type === "backend_status" && event.payload.phase === "resource-bundle-materialized")?.payload ?? null;
|
||||
return {
|
||||
kind: run.resourceBundleRef.kind,
|
||||
repoUrl: run.resourceBundleRef.repoUrl,
|
||||
commitId: run.resourceBundleRef.commitId,
|
||||
subdir: run.resourceBundleRef.subdir ?? null,
|
||||
materialized: materialized as JsonValue,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user