fix(workbench): keep trace timing monotonic (#1975)
This commit is contained in:
@@ -17,12 +17,13 @@ export interface TraceDisplayRowsOptions {
|
||||
}
|
||||
|
||||
export function traceDisplayRows(trace: Record<string, unknown> = {}, events: TraceEvent[] = [], options: TraceDisplayRowsOptions = {}): TraceEventRow[] {
|
||||
const effectiveTrace = traceWithInferredStart(trace, events);
|
||||
const orderedEvents = traceEventsForDisplay(events);
|
||||
const effectiveTrace = traceWithInferredStart(trace, orderedEvents);
|
||||
const rows: TraceEventRow[] = [];
|
||||
const renderedSourceEvents = new Set<string>();
|
||||
const renderedToolIdentities = new Set<string>();
|
||||
for (let index = 0; index < events.length; index += 1) {
|
||||
const event = events[index];
|
||||
for (let index = 0; index < orderedEvents.length; index += 1) {
|
||||
const event = orderedEvents[index];
|
||||
const sourceEventKey = traceSourceEventKey(event);
|
||||
if (sourceEventKey) {
|
||||
if (renderedSourceEvents.has(sourceEventKey)) continue;
|
||||
@@ -30,7 +31,7 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
|
||||
}
|
||||
if (!event || isNoisyTraceEvent(event)) continue;
|
||||
if (isToolTraceEvent(event)) {
|
||||
if (isSupersededToolStart(event, events, index)) continue;
|
||||
if (isSupersededToolStart(event, orderedEvents, index)) continue;
|
||||
const identity = toolIdentity(event);
|
||||
if (identity) {
|
||||
if (renderedToolIdentities.has(identity)) continue;
|
||||
@@ -60,11 +61,11 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
|
||||
}
|
||||
rows.push(traceDisplayRow(effectiveTrace, event, options));
|
||||
}
|
||||
const progressRow = traceBackendProgressSummaryRow(effectiveTrace, rows, events, options);
|
||||
const progressRow = traceBackendProgressSummaryRow(effectiveTrace, rows, orderedEvents, options);
|
||||
if (progressRow) rows.push(progressRow);
|
||||
if (rows.length > 0) return rows;
|
||||
if (events.length === 0) return [];
|
||||
return events.filter((event) => !isSuppressedTraceEvent(event)).map((event) => traceDisplayRow(effectiveTrace, event, options));
|
||||
if (orderedEvents.length === 0) return [];
|
||||
return orderedEvents.filter((event) => !isSuppressedTraceEvent(event)).map((event) => traceDisplayRow(effectiveTrace, event, options));
|
||||
}
|
||||
|
||||
export function renderTraceRowsMarkdown(rows: TraceEventRow[] = []): string {
|
||||
@@ -91,6 +92,35 @@ function renderTraceMessageRowMarkdown(row: TraceEventRow): string {
|
||||
return traceBoldMarkdown(row.header || `_No readable trace row body._`);
|
||||
}
|
||||
|
||||
function traceEventsForDisplay(events: TraceEvent[] = []): TraceEvent[] {
|
||||
return [...events].sort((left, right) => {
|
||||
const leftSeq = traceEventDisplaySeq(left);
|
||||
const rightSeq = traceEventDisplaySeq(right);
|
||||
if (leftSeq !== null || rightSeq !== null) {
|
||||
if (leftSeq === null) return 1;
|
||||
if (rightSeq === null) return -1;
|
||||
if (leftSeq !== rightSeq) return leftSeq - rightSeq;
|
||||
}
|
||||
const leftTime = traceEventTimestampMs(left);
|
||||
const rightTime = traceEventTimestampMs(right);
|
||||
if (leftTime !== null || rightTime !== null) {
|
||||
if (leftTime === null) return 1;
|
||||
if (rightTime === null) return -1;
|
||||
if (leftTime !== rightTime) return leftTime - rightTime;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
function traceEventDisplaySeq(event: TraceEvent | null | undefined): number | null {
|
||||
return numberOrNull(event?.projectedSeq) ?? numberOrNull(event?.seq);
|
||||
}
|
||||
|
||||
function traceEventIdentityToken(event: TraceEvent): string | number | null {
|
||||
const seq = traceEventDisplaySeq(event);
|
||||
return seq ?? nonEmptyString(event.projectedSeq ?? event.seq);
|
||||
}
|
||||
|
||||
function isToolTraceRow(row: TraceEventRow): boolean {
|
||||
return row.rowId.startsWith("tool:") || /commandExecution/u.test(row.header);
|
||||
}
|
||||
@@ -110,9 +140,10 @@ function traceToolCallRow(trace: Record<string, unknown>, event: TraceEvent, opt
|
||||
const status = traceStatusToken(event);
|
||||
const toolName = traceToolName(event, command);
|
||||
const body = traceToolCallBody(event, command);
|
||||
const eventKey = traceEventIdentityToken(event);
|
||||
return {
|
||||
rowId: `tool:${event.itemId ?? event.seq ?? `${event.label ?? event.type ?? "tool"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
rowId: `tool:${event.itemId ?? eventKey ?? `${event.label ?? event.type ?? "tool"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: traceEventDisplaySeq(event),
|
||||
tone: traceEventTone(event),
|
||||
header: `${traceEventClock(event, options)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${status} ${toolName}`.trim(),
|
||||
body,
|
||||
@@ -180,9 +211,10 @@ function traceAssistantMessageRow(trace: Record<string, unknown>, event: TraceEv
|
||||
const text = cleanTraceDetailText(event.message ?? event.outputSummary ?? assistantStreamText(trace, event) ?? "");
|
||||
const index = Number.isInteger(event.messageIndex) ? Number(event.messageIndex) : null;
|
||||
const count = Number.isInteger(event.messageCount) ? Number(event.messageCount) : null;
|
||||
const eventKey = traceEventIdentityToken(event);
|
||||
return {
|
||||
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "assistant"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
rowId: `event:${eventKey ?? `${event.label ?? event.type ?? "assistant"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: traceEventDisplaySeq(event),
|
||||
tone: "ok",
|
||||
header: `${traceEventClock(event, options)} ${terminal ? "助手最终消息" : "助手消息"}${index ? ` ${index}${count ? `/${count}` : ""}` : ""}`,
|
||||
terminal: terminal ? true : undefined,
|
||||
@@ -192,9 +224,10 @@ function traceAssistantMessageRow(trace: Record<string, unknown>, event: TraceEv
|
||||
}
|
||||
|
||||
function traceFinalResponseRow(event: TraceEvent, finalText: string, options: TraceDisplayRowsOptions): TraceEventRow {
|
||||
const eventKey = traceEventIdentityToken(event);
|
||||
return {
|
||||
rowId: `trace-final-response:${event.seq ?? "completed"}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
rowId: `trace-final-response:${eventKey ?? "completed"}`,
|
||||
seq: traceEventDisplaySeq(event),
|
||||
tone: "ok",
|
||||
header: `${traceEventClock(event, options)} 助手最终消息`,
|
||||
terminal: true,
|
||||
@@ -209,9 +242,10 @@ function traceFinalResponseText(trace: Record<string, unknown>): string | null {
|
||||
}
|
||||
|
||||
function traceCompletionSummaryRow(trace: Record<string, unknown>, event: TraceEvent, options: TraceDisplayRowsOptions): TraceEventRow {
|
||||
const eventKey = traceEventIdentityToken(event);
|
||||
return {
|
||||
rowId: `trace-completion:${event.seq ?? "turn"}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
rowId: `trace-completion:${eventKey ?? "turn"}`,
|
||||
seq: traceEventDisplaySeq(event),
|
||||
tone: traceEventTone(event),
|
||||
header: `${traceEventClock(event, options)} 轮次完成(总耗时 ${formatTraceDuration(traceRelativeMs(trace, event))})`,
|
||||
body: null
|
||||
@@ -224,7 +258,7 @@ function traceNoiseSummaryRow(trace: Record<string, unknown>, events: TraceEvent
|
||||
const lastLabel = lastEvent ? readableTraceLabel(lastEvent) : "未观测";
|
||||
return {
|
||||
rowId: "trace-noise-summary",
|
||||
seq: numberOrNull(lastEvent?.seq),
|
||||
seq: traceEventDisplaySeq(lastEvent),
|
||||
tone: lastEvent ? traceEventTone(lastEvent) : "source",
|
||||
header: `${traceEventClock(lastEvent, options)} Trace ${status},等待可读事件`,
|
||||
body: `已隐藏 ${events.length} 条 AgentRun backend 状态事件。最新原始事件:${lastLabel}。`,
|
||||
@@ -237,7 +271,7 @@ function traceBackendProgressSummaryRow(trace: Record<string, unknown>, rows: Tr
|
||||
if (rows.some((row) => row.rowId.startsWith("tool:") || /助手|轮次完成/u.test(row.header))) return null;
|
||||
const lastRenderedSeq = Math.max(0, ...rows.map((row) => row.seq ?? 0).filter(Number.isFinite));
|
||||
const hiddenProgress = events.filter((event) => {
|
||||
const seq = numberOrNull(event.seq) ?? 0;
|
||||
const seq = traceEventDisplaySeq(event) ?? 0;
|
||||
return seq > lastRenderedSeq && (isRequestTraceEvent(event) || isSetupTraceEvent(event) || isNoisyTraceEvent(event));
|
||||
});
|
||||
return hiddenProgress.length > 0 ? traceNoiseSummaryRow(trace, hiddenProgress, options) : null;
|
||||
@@ -245,9 +279,10 @@ function traceBackendProgressSummaryRow(trace: Record<string, unknown>, rows: Tr
|
||||
|
||||
function traceDisplayRow(trace: Record<string, unknown>, event: TraceEvent, options: TraceDisplayRowsOptions): TraceEventRow {
|
||||
const label = readableTraceLabel(event);
|
||||
const eventKey = traceEventIdentityToken(event);
|
||||
return {
|
||||
rowId: `event:${event.seq ?? `${event.label ?? event.type ?? "event"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: numberOrNull(event.seq),
|
||||
rowId: `event:${eventKey ?? `${event.label ?? event.type ?? "event"}:${event.createdAt ?? "unknown"}`}`,
|
||||
seq: traceEventDisplaySeq(event),
|
||||
tone: traceEventTone(event),
|
||||
header: `${traceEventClock(event, options)} total=${formatTraceDuration(traceRelativeMs(trace, event))} ${traceStatusToken(event)} ${label}`.trim(),
|
||||
body: traceDisplayBody(event)
|
||||
@@ -464,6 +499,13 @@ function traceEventTimestamp(event: TraceEvent | null | undefined): string | nul
|
||||
return nonEmptyString(event.createdAt ?? event.ts ?? event.timestamp ?? event.observedAt);
|
||||
}
|
||||
|
||||
function traceEventTimestampMs(event: TraceEvent | null | undefined): number | null {
|
||||
const timestamp = traceEventTimestamp(event);
|
||||
if (!timestamp) return null;
|
||||
const parsed = Date.parse(timestamp);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function traceRelativeMs(trace: Record<string, unknown>, event: TraceEvent): number {
|
||||
if (typeof event.elapsedMs === "number") return event.elapsedMs;
|
||||
const start = Date.parse(String(trace.startedAt ?? trace.createdAt ?? ""));
|
||||
@@ -529,7 +571,8 @@ function compactTraceOneLine(value: unknown, limit = 220): string {
|
||||
}
|
||||
|
||||
function numberOrNull(value: unknown): number | null {
|
||||
return Number.isInteger(value) ? Number(value) : null;
|
||||
const number = Number(value);
|
||||
return Number.isInteger(number) ? number : null;
|
||||
}
|
||||
|
||||
function nonEmptyString(value: unknown): string | null {
|
||||
|
||||
Reference in New Issue
Block a user