fix: reduce trace rendering noise
This commit is contained in:
@@ -76,7 +76,10 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
|
||||
if (lastAssistantRow) rows[lastAssistantRowIndex] = markAssistantRowTerminal(trace, lastAssistantRow, completionEvent);
|
||||
else rows.push(traceCompletionSummaryRow(trace, completionEvent));
|
||||
}
|
||||
return rows.length > 0 ? rows : events.map((event) => traceDisplayRow(trace, event));
|
||||
if (rows.length > 0) return rows;
|
||||
if (events.length === 0) return [];
|
||||
if (traceNoiseEventCount(events) === events.length) return [traceNoiseSummaryRow(trace, events)];
|
||||
return events.filter((event) => !isNoisyTraceEvent(event)).map((event) => traceDisplayRow(trace, event));
|
||||
}
|
||||
|
||||
export function renderTraceRowsMarkdown(rows: TraceEventRow[] = []): string {
|
||||
@@ -350,6 +353,20 @@ function traceCompletionSummaryRow(trace: Record<string, unknown>, event: TraceE
|
||||
};
|
||||
}
|
||||
|
||||
function traceNoiseSummaryRow(trace: Record<string, unknown>, events: TraceEvent[]): TraceEventRow {
|
||||
const lastEvent = events.at(-1);
|
||||
const status = nonEmptyString(trace.status ?? trace.traceStatus ?? lastEvent?.status) ?? "running";
|
||||
const lastLabel = lastEvent ? readableTraceLabel(lastEvent) : "未观测";
|
||||
return {
|
||||
rowId: "trace-noise-summary",
|
||||
seq: numberOrNull(lastEvent?.seq),
|
||||
tone: lastEvent ? traceEventTone(lastEvent) : "source",
|
||||
header: `${traceClock(lastEvent?.createdAt)} Trace ${status},等待可读事件`,
|
||||
body: `已隐藏 ${events.length} 条 AgentRun backend 状态事件。最新原始事件:${lastLabel}。`,
|
||||
bodyFormat: "text"
|
||||
};
|
||||
}
|
||||
|
||||
function traceDisplayRow(trace: Record<string, unknown>, event: TraceEvent): TraceEventRow {
|
||||
const label = readableTraceLabel(event);
|
||||
return {
|
||||
|
||||
@@ -3,6 +3,7 @@ import test from "node:test";
|
||||
|
||||
import type { AgentRunProvenance, ChatMessage, TraceEvent } from "../src/types/index.ts";
|
||||
import { canCancelMessage, canReplayTrace, canRetryMessage, messageTraceId, renderSafeMarkdown, traceEventBody, traceEventLabel, traceIdentityText, visibleTraceEvents } from "../src/components/workbench/message-rendering.ts";
|
||||
import { traceDisplayRows, traceNoiseEventCount } from "../../../tools/src/hwlab-cli/trace-renderer.ts";
|
||||
|
||||
test("R1 markdown rendering keeps structure and strips unsafe HTML", () => {
|
||||
const html = renderSafeMarkdown("# 标题\n\n- 项目\n\n<script>alert('x')</script>\n\n[link](https://example.com)");
|
||||
@@ -48,6 +49,20 @@ test("R1 trace rendering hides chunk noise without dropping meaningful events",
|
||||
assert.equal(visibleTraceEvents(events, false).length, 3);
|
||||
});
|
||||
|
||||
test("R1 shared trace renderer collapses pure AgentRun backend noise", () => {
|
||||
const events = Array.from({ length: 12 }, (_, index) => ({
|
||||
label: index % 3 === 0 ? "agentrun:backend:run-created" : index % 3 === 1 ? "agentrun:backend:command-created" : "agentrun:backend:runner-job-created",
|
||||
status: "running",
|
||||
createdAt: "2026-06-15T02:00:00.000Z"
|
||||
}));
|
||||
const rows = traceDisplayRows({ traceId: "trc_noise", status: "running" }, events);
|
||||
assert.equal(traceNoiseEventCount(events), 12);
|
||||
assert.equal(rows.length, 1);
|
||||
assert.equal(rows[0]?.rowId, "trace-noise-summary");
|
||||
assert.match(rows[0]?.body ?? "", /已隐藏 12 条 AgentRun backend 状态事件/u);
|
||||
assert.doesNotMatch(rows[0]?.body ?? "", /run-created\ncommand-created/u);
|
||||
});
|
||||
|
||||
test("R1 status summary keeps the restored 23-row floor", () => {
|
||||
const message = agentMessage({
|
||||
status: "failed",
|
||||
|
||||
@@ -3,21 +3,35 @@ import { computed, nextTick, ref, watch } from "vue";
|
||||
import type { RunnerTrace, TraceEvent } from "@/types";
|
||||
import { isTerminalStatus } from "@/composables/useTraceSubscription";
|
||||
import { firstNonEmptyString } from "@/utils";
|
||||
import { traceEventBody, traceEventLabel, traceEventMeta, visibleTraceEvents } from "@/components/workbench/message-rendering";
|
||||
import MessageMarkdown from "@/components/workbench/MessageMarkdown.vue";
|
||||
import { traceEventBody, traceEventLabel, traceEventMeta } from "@/components/workbench/message-rendering";
|
||||
import { traceDisplayRows, traceNoiseEventCount, type TraceEventRow } from "../../../../../tools/src/hwlab-cli/trace-renderer.ts";
|
||||
|
||||
const props = defineProps<{ trace?: RunnerTrace | null; defaultExpanded?: boolean; storageKey?: string }>();
|
||||
|
||||
const listRef = ref<HTMLOListElement | null>(null);
|
||||
const following = ref(true);
|
||||
const expanded = ref(false);
|
||||
const compact = ref(true);
|
||||
const rawMode = ref(false);
|
||||
|
||||
const events = computed(() => Array.isArray(props.trace?.events) ? props.trace.events : []);
|
||||
const eventCount = computed(() => props.trace?.eventCount ?? events.value.length);
|
||||
const visibleEvents = computed(() => visibleTraceEvents(events.value, compact.value));
|
||||
const noiseCount = computed(() => Math.max(0, events.value.length - visibleEvents.value.length));
|
||||
const readableRows = computed(() => traceDisplayRows(traceRecord(props.trace), events.value as Record<string, unknown>[]));
|
||||
const noiseCount = computed(() => traceNoiseEventCount(events.value as Record<string, unknown>[]));
|
||||
const lastEvent = computed(() => events.value.at(-1) ?? null);
|
||||
const lastEventLabel = computed(() => firstNonEmptyString(props.trace?.lastEventLabel, lastEvent.value?.label, lastEvent.value?.type, lastEvent.value?.kind) ?? "未观测");
|
||||
const lastReadableRow = computed(() => readableRows.value.at(-1) ?? null);
|
||||
const lastEventLabel = computed(() => firstNonEmptyString(lastReadableRow.value?.header, props.trace?.lastEventLabel, lastEvent.value?.label, lastEvent.value?.type, lastEvent.value?.kind) ?? "未观测");
|
||||
const summaryItems = computed(() => {
|
||||
const agentRun = recordValue(props.trace?.agentRun);
|
||||
return [
|
||||
{ label: "status", value: firstNonEmptyString(props.trace?.status, props.trace?.traceStatus) },
|
||||
{ label: "session", value: props.trace?.sessionId },
|
||||
{ label: "thread", value: props.trace?.threadId },
|
||||
{ label: "run", value: firstNonEmptyString(agentRun?.runId, props.trace?.runId) },
|
||||
{ label: "command", value: firstNonEmptyString(agentRun?.commandId, props.trace?.commandId) },
|
||||
{ label: "waiting", value: props.trace?.waitingFor }
|
||||
].filter((item): item is { label: string; value: string } => Boolean(item.value));
|
||||
});
|
||||
|
||||
watch(() => [eventCount.value, props.trace?.status, following.value, expanded.value], async () => {
|
||||
if (!following.value || !expanded.value) return;
|
||||
@@ -50,6 +64,18 @@ function eventKey(event: TraceEvent, index: number): string {
|
||||
return `${event.ts ?? "no-ts"}-${event.label ?? event.type ?? event.kind ?? "event"}-${index}`;
|
||||
}
|
||||
|
||||
function rowKey(row: TraceEventRow, index: number): string {
|
||||
return `${row.rowId}-${row.seq ?? "no-seq"}-${index}`;
|
||||
}
|
||||
|
||||
function traceRecord(trace: RunnerTrace | null | undefined): Record<string, unknown> {
|
||||
return trace && typeof trace === "object" ? trace as Record<string, unknown> : {};
|
||||
}
|
||||
|
||||
function recordValue(value: unknown): Record<string, unknown> | null {
|
||||
return value && typeof value === "object" ? value as Record<string, unknown> : null;
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -58,21 +84,31 @@ function eventKey(event: TraceEvent, index: number): string {
|
||||
<button class="trace-toggle" type="button" @click="expanded = !expanded">{{ expanded ? "收起" : "展开" }}</button>
|
||||
<strong>Trace</strong>
|
||||
<span class="trace-id">{{ trace.traceId || "pending" }}</span>
|
||||
<small>{{ visibleEvents.length }}/{{ eventCount }} events</small>
|
||||
<small>{{ readableRows.length }} readable / {{ eventCount }} events</small>
|
||||
<small v-if="noiseCount > 0">hidden={{ noiseCount }}</small>
|
||||
<small>last={{ lastEventLabel }}</small>
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="following = !following">{{ following ? "跟随" : "暂停" }}</button>
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="jumpToBottom">到底部</button>
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="compact = !compact">{{ compact ? "显示全部" : "低噪声" }}</button>
|
||||
<button class="btn btn-secondary btn-sm" type="button" @click="rawMode = !rawMode">{{ rawMode ? "可读视图" : "原始事件" }}</button>
|
||||
</header>
|
||||
<div v-if="summaryItems.length > 0" class="trace-summary-strip" aria-label="Trace identity summary">
|
||||
<span v-for="item in summaryItems" :key="item.label" class="trace-summary-chip"><b>{{ item.label }}</b>{{ item.value }}</span>
|
||||
</div>
|
||||
<p v-if="trace.eventsCompacted" class="trace-warning">当前 result 返回被压缩,已通过 trace replay 入口请求完整事件;完成前不要把压缩窗口当完整 trace。</p>
|
||||
<p v-if="noiseCount > 0 && compact" class="trace-warning">已聚合隐藏 {{ noiseCount }} 条 chunk/delta/token 噪声事件。</p>
|
||||
<p v-if="noiseCount > 0 && !rawMode" class="trace-warning">已按共享 trace renderer 聚合隐藏 {{ noiseCount }} 条 backend/chunk/token 噪声事件;需要审计原始 payload 时可切换到原始事件。</p>
|
||||
<ol v-show="expanded" ref="listRef" @scroll="onScroll">
|
||||
<li v-for="(event, index) in visibleEvents" :key="eventKey(event, index)" :data-event-status="event.status || 'source'">
|
||||
<li v-for="(row, index) in readableRows" v-if="!rawMode" :key="rowKey(row, index)" class="trace-render-row" :data-event-status="row.tone" :data-terminal="row.terminal ? 'true' : 'false'">
|
||||
<code>{{ row.header }}</code>
|
||||
<MessageMarkdown v-if="row.body && row.bodyFormat === 'markdown'" class="trace-row-body trace-row-markdown" :source="row.body" />
|
||||
<pre v-else-if="row.body" class="trace-row-body">{{ row.body }}</pre>
|
||||
<span v-else class="trace-row-body muted">已记录</span>
|
||||
</li>
|
||||
<li v-for="(event, index) in events" v-else :key="eventKey(event, index)" class="trace-raw-row" :data-event-status="event.status || 'source'">
|
||||
<code>{{ traceEventLabel(event) }}</code>
|
||||
<span>{{ traceEventBody(event) }}</span>
|
||||
<small v-if="traceEventMeta(event)">{{ traceEventMeta(event) }}</small>
|
||||
</li>
|
||||
</ol>
|
||||
<p v-if="expanded && visibleEvents.length === 0" class="trace-empty">trace={{ trace.traceId || "pending" }};等待后端事件。</p>
|
||||
<p v-if="expanded && readableRows.length === 0 && !rawMode" class="trace-empty">trace={{ trace.traceId || "pending" }};等待后端事件。</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -752,6 +752,33 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.trace-summary-strip {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.trace-summary-chip {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border: 1px solid #d8e1eb;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
padding: 5px 8px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.trace-summary-chip b {
|
||||
color: #64748b;
|
||||
font-family: inherit;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.trace-toolbar strong,
|
||||
.trace-id {
|
||||
min-width: 0;
|
||||
@@ -811,12 +838,46 @@
|
||||
|
||||
.trace-timeline li {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(96px, 160px) minmax(0, 1fr) auto;
|
||||
grid-template-columns: minmax(180px, 280px) minmax(0, 1fr) auto;
|
||||
gap: 8px;
|
||||
color: #475569;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.trace-timeline li[data-terminal="true"] {
|
||||
border-left: 3px solid #22c55e;
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
.trace-render-row .trace-row-body {
|
||||
min-width: 0;
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.trace-render-row .trace-row-markdown {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.trace-render-row .muted {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.trace-render-row pre.trace-row-body {
|
||||
max-height: 120px;
|
||||
overflow: auto;
|
||||
border: 1px solid #d8e1eb;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
color: #334155;
|
||||
padding: 8px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.trace-timeline li span,
|
||||
.trace-timeline li small {
|
||||
min-width: 0;
|
||||
|
||||
Reference in New Issue
Block a user