feat: add workbench projection performance metrics (#1592)

This commit is contained in:
Lyon
2026-06-19 11:13:26 +08:00
committed by GitHub
parent 034b9b5017
commit 0169125f8f
6 changed files with 765 additions and 19 deletions
+105 -8
View File
@@ -441,6 +441,7 @@ function isAgentRunCommandAlreadyClaimed(error) {
export async function syncAgentRunChatResult({ traceId, currentResult = null, options = {}, traceStore = defaultCodeAgentTraceStore, appendResultEvent = true, refreshEvents = true, forceResultSync = false }) {
const initial = currentResult ?? await loadPersistedAgentRunResult(traceId, options);
const mapped = initial ? await resolveAgentRunTraceCommandMapping({ traceId, mapped: initial, options }) : initial;
const performanceStore = options.backendPerformanceStore ?? currentBackendPerformanceContext();
if (!forceResultSync && options.skipAgentRunTerminalRefreshIfComplete === true && agentRunTerminalResultRefreshSatisfied(mapped, traceId)) {
const runnerTrace = mapped.runnerTrace ?? traceStore.snapshot(traceId);
return { result: mapped, runnerTrace, found: true, eventsRefreshed: false, resultSynced: false, terminalRefreshSkipped: true };
@@ -450,12 +451,15 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
const fetchImpl = options.fetchImpl ?? globalThis.fetch;
const managerUrl = resolveAgentRunManagerUrl(env, mapped.agentRun.managerUrl);
const timeoutMs = parsePositiveInteger(env.HWLAB_CODE_AGENT_AGENTRUN_HTTP_TIMEOUT_MS, 20_000);
const eventsResponse = refreshEvents ? await fetchAgentRunEventsForTrace({ fetchImpl, managerUrl, timeoutMs, env, mapping: { ...mapped.agentRun, traceSummary: mapped.traceSummary } }) : null;
const previousLastSeq = Number(mapped.agentRun.lastSeq ?? 0);
const eventsResponse = refreshEvents ? await measuredAgentRunEventsFetch({ performanceStore, fetchImpl, managerUrl, timeoutMs, env, mapping: { ...mapped.agentRun, traceSummary: mapped.traceSummary } }) : null;
if (eventsResponse) appendAgentRunEventsToTrace(traceStore, traceId, eventsResponse.events, mapped.agentRun);
const nextLastSeq = eventsResponse ? agentRunTraceCursorSeq(eventsResponse, mapped.agentRun.lastSeq) : mapped.agentRun.lastSeq;
recordAgentRunEventsProjectionMetrics(performanceStore, eventsResponse, previousLastSeq, nextLastSeq);
if (!forceResultSync && !agentRunCommandResultSyncRequired(mapped, eventsResponse)) {
const nextMapping = withAgentRunRunnerJobCount({
...mapped.agentRun,
lastSeq: eventsResponse ? agentRunTraceCursorSeq(eventsResponse, mapped.agentRun.lastSeq) : mapped.agentRun.lastSeq,
lastSeq: nextLastSeq,
status: mapped.agentRun.status ?? "running",
runStatus: mapped.agentRun.runStatus ?? null,
commandState: mapped.agentRun.commandState ?? null,
@@ -467,15 +471,11 @@ export async function syncAgentRunChatResult({ traceId, currentResult = null, op
options.codeAgentChatResults?.set?.(traceId, payload);
return { result: payload, runnerTrace: payload.runnerTrace ?? traceStore.snapshot(traceId), found: true, eventsRefreshed: Boolean(eventsResponse), resultSynced: false };
}
const result = await agentRunJson(fetchImpl, managerUrl, `/api/v1/runs/${encodeURIComponent(mapped.agentRun.runId)}/commands/${encodeURIComponent(mapped.agentRun.commandId)}/result`, {
method: "GET",
timeoutMs,
env
});
const result = await measuredAgentRunResultFetch({ performanceStore, fetchImpl, managerUrl, timeoutMs, env, mapped, eventsResponse });
const nextMapping = withAgentRunRunnerJobCount({
...mapped.agentRun,
...agentRunResultRefs(result),
lastSeq: eventsResponse ? agentRunTraceCursorSeq(eventsResponse, mapped.agentRun.lastSeq) : mapped.agentRun.lastSeq,
lastSeq: nextLastSeq,
status: result?.status ?? mapped.agentRun.status ?? "running",
runStatus: result?.runStatus ?? mapped.agentRun.runStatus ?? null,
commandState: result?.commandState ?? mapped.agentRun.commandState ?? null,
@@ -1878,12 +1878,109 @@ async function fetchAgentRunEventsForTrace({ fetchImpl, managerUrl, timeoutMs, e
events,
afterSeq,
endSeq,
rawEventCount: rawEvents.length,
commandFiltered: Boolean(currentCommandId),
maxSeq: Math.max(afterSeq, ...rawEvents.map((event) => Number(event?.seq ?? 0))),
traceLastSeq: Math.max(afterSeq, ...events.map((event) => Number(event?.seq ?? 0)).filter(Number.isFinite))
};
}
async function measuredAgentRunEventsFetch({ performanceStore, fetchImpl, managerUrl, timeoutMs, env, mapping = {} }) {
const startedAt = nowMs();
try {
const response = await fetchAgentRunEventsForTrace({ fetchImpl, managerUrl, timeoutMs, env, mapping });
performanceStore?.recordWorkbenchProjectorBatch?.({ phase: "agentrun_events_fetch", status: "ok", durationMs: nowMs() - startedAt });
return response;
} catch (error) {
performanceStore?.recordWorkbenchProjectorBatch?.({ phase: "agentrun_events_fetch", status: agentRunMetricStatusForError(error), durationMs: nowMs() - startedAt });
performanceStore?.recordWorkbenchProjectorError?.({ reason: error?.code ?? "agentrun_events_fetch_failed" });
throw error;
}
}
async function measuredAgentRunResultFetch({ performanceStore, fetchImpl, managerUrl, timeoutMs, env, mapped = {}, eventsResponse = null }) {
const startedAt = nowMs();
const path = `/api/v1/runs/${encodeURIComponent(mapped.agentRun.runId)}/commands/${encodeURIComponent(mapped.agentRun.commandId)}/result`;
try {
const result = await agentRunJson(fetchImpl, managerUrl, path, { method: "GET", timeoutMs, env });
const eventCount = agentRunResultEventCount(result, eventsResponse);
performanceStore?.recordAgentRunResult?.({
durationMs: nowMs() - startedAt,
eventCount,
eventsScanned: eventCount,
pagesScanned: estimateAgentRunResultPages(eventCount),
status: result?.terminalStatus ?? result?.status ?? "ok",
budget: timeoutMs
});
return result;
} catch (error) {
const status = agentRunMetricStatusForError(error);
const eventCount = agentRunResultEventCount(null, eventsResponse);
performanceStore?.recordAgentRunResult?.({
durationMs: nowMs() - startedAt,
eventCount,
eventsScanned: eventCount,
pagesScanned: estimateAgentRunResultPages(eventCount),
status,
budget: timeoutMs,
timedOut: status === "timeout"
});
performanceStore?.recordWorkbenchProjectorError?.({ reason: error?.code ?? "agentrun_result_failed" });
throw error;
}
}
function recordAgentRunEventsProjectionMetrics(performanceStore, eventsResponse = null, previousLastSeq = 0, nextLastSeq = 0) {
if (!performanceStore || !eventsResponse) return;
const cursorAdvance = Math.max(0, Number(nextLastSeq ?? 0) - Number(previousLastSeq ?? 0));
if (cursorAdvance > 0) {
performanceStore.recordWorkbenchProjectionCursorAdvance?.({
status: "advanced",
reason: eventsResponse.commandFiltered ? "command_filtered_events" : "run_events",
projectionStatus: "projecting",
source: "agentrun_v01",
count: cursorAdvance
});
}
const counts = new Map();
for (const event of eventsResponse.events ?? []) {
const kind = String(event?.type ?? event?.payload?.phase ?? "unknown").trim() || "unknown";
counts.set(kind, (counts.get(kind) ?? 0) + 1);
}
if (counts.size === 0 && Number(eventsResponse.rawEventCount ?? 0) > 0) counts.set("filtered", Number(eventsResponse.rawEventCount));
for (const [eventKind, count] of counts.entries()) {
performanceStore.recordWorkbenchProjectorEvents?.({ eventKind, status: "ok", count });
}
}
function agentRunResultEventCount(result = null, eventsResponse = null) {
const candidates = [
result?.traceSummary?.sourceEventCount,
result?.traceSummary?.eventCount,
result?.sourceEventCount,
result?.eventCount,
result?.providerTrace?.eventCount,
eventsResponse?.maxSeq,
eventsResponse?.rawEventCount,
eventsResponse?.events?.length
];
for (const value of candidates) {
const count = Number(value);
if (Number.isFinite(count) && count >= 0) return Math.floor(count);
}
return 0;
}
function estimateAgentRunResultPages(eventCount) {
const count = Number(eventCount);
if (!Number.isFinite(count) || count <= 0) return 0;
return Math.max(1, Math.ceil(count / 500));
}
function agentRunMetricStatusForError(error) {
return error?.code === "agentrun_timeout" || error?.name === "AbortError" || Number(error?.statusCode) === 504 ? "timeout" : "error";
}
function agentRunTraceCursorSeq(eventsResponse = {}, previousLastSeq = 0) {
const afterSeq = Number(eventsResponse.afterSeq ?? 0);
const endSeq = Number(eventsResponse.endSeq ?? 0);