Fix workbench trace projection authority (#1696)

This commit is contained in:
Lyon
2026-06-20 12:47:18 +08:00
committed by GitHub
parent c12e488680
commit acd9831149
8 changed files with 429 additions and 175 deletions
+9 -157
View File
@@ -16,9 +16,6 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
const rows: TraceEventRow[] = [];
const renderedSourceEvents = new Set<string>();
const renderedToolIdentities = new Set<string>();
const assistantRows: AssistantRowState[] = [];
let lastAssistantRowIndex = -1;
let completionEvent: TraceEvent | null = null;
for (let index = 0; index < events.length; index += 1) {
const event = events[index];
const sourceEventKey = traceSourceEventKey(event);
@@ -44,30 +41,20 @@ export function traceDisplayRows(trace: Record<string, unknown> = {}, events: Tr
continue;
}
if (isTerminalAssistantTraceEvent(event)) {
const assistantRowIndex = upsertAssistantMessageRow(rows, assistantRows, effectiveTrace, event, { terminal: true });
if (assistantRowIndex >= 0) lastAssistantRowIndex = assistantRowIndex;
rows.push(traceAssistantMessageRow(effectiveTrace, event, { terminal: true }));
continue;
}
if (isAssistantTraceEvent(event)) {
const assistantRowIndex = upsertAssistantMessageRow(rows, assistantRows, effectiveTrace, event, { terminal: false });
if (assistantRowIndex >= 0) lastAssistantRowIndex = assistantRowIndex;
rows.push(traceAssistantMessageRow(effectiveTrace, event, { terminal: false }));
continue;
}
if (isCompletionTraceEvent(event)) {
completionEvent = event;
const finalResponseText = traceFinalResponseText(effectiveTrace);
rows.push(finalResponseText ? traceFinalResponseRow(event, finalResponseText) : traceCompletionSummaryRow(effectiveTrace, event));
continue;
}
rows.push(traceDisplayRow(effectiveTrace, event));
}
if (completionEvent) {
const finalResponseText = traceFinalResponseText(effectiveTrace);
if (finalResponseText) {
lastAssistantRowIndex = upsertAuthoritativeFinalResponseRow(rows, assistantRows, completionEvent, finalResponseText);
}
const lastAssistantRow = lastAssistantRowIndex >= 0 ? rows[lastAssistantRowIndex] : undefined;
if (lastAssistantRow) rows[lastAssistantRowIndex] = markAssistantRowTerminal(effectiveTrace, lastAssistantRow, completionEvent);
else rows.push(traceCompletionSummaryRow(effectiveTrace, completionEvent));
}
const progressRow = traceBackendProgressSummaryRow(effectiveTrace, rows, events);
if (progressRow) rows.push(progressRow);
if (rows.length > 0) return rows;
@@ -113,18 +100,6 @@ function stripTraceToolOutputFromSummary(value: string): string {
return value.replace(/\s+(stdout:|stderr:|exitCode=).*$/isu, "").trim();
}
interface AssistantRowState {
rowIndex: number;
comparableText: string;
sourceEvent: TraceEvent;
derivedSnapshotSuffix?: boolean;
}
interface AssistantSnapshotDecision {
action: "replace" | "append-suffix" | "keep-existing";
suffixText?: string;
}
function traceToolCallRow(trace: Record<string, unknown>, event: TraceEvent): TraceEventRow {
const command = cleanShellCommand(event.command);
const status = traceStatusToken(event);
@@ -211,146 +186,23 @@ function traceAssistantMessageRow(trace: Record<string, unknown>, event: TraceEv
};
}
function upsertAssistantMessageRow(rows: TraceEventRow[], assistantRows: AssistantRowState[], trace: Record<string, unknown>, event: TraceEvent, options: { terminal: boolean }): number {
const row = traceAssistantMessageRow(trace, event, options);
const comparableText = comparableAssistantText(row.body);
if (!comparableText) {
rows.push(row);
assistantRows.push({ rowIndex: rows.length - 1, comparableText, sourceEvent: event });
return rows.length - 1;
}
for (let index = assistantRows.length - 1; index >= 0; index -= 1) {
const state = assistantRows[index];
if (!state) continue;
const existingRow = rows[state.rowIndex];
if (!existingRow) continue;
if (state.comparableText === comparableText) {
rows[state.rowIndex] = mergeAssistantRows(existingRow, row);
state.sourceEvent = event;
state.derivedSnapshotSuffix = false;
return state.rowIndex;
}
if (comparableText.includes(state.comparableText)) {
const previousIndex = comparableText.indexOf(state.comparableText);
if (state.derivedSnapshotSuffix === true && previousIndex === 0) {
rows[state.rowIndex] = mergeAssistantRows(row, existingRow);
state.comparableText = comparableText;
state.sourceEvent = event;
state.derivedSnapshotSuffix = false;
return state.rowIndex;
}
const snapshotDecision = assistantSnapshotDecision(comparableText, state.comparableText, event, state.sourceEvent);
if (snapshotDecision.action === "append-suffix" && snapshotDecision.suffixText) {
const suffixRow = { ...row, body: snapshotDecision.suffixText };
rows.push(suffixRow);
assistantRows.push({ rowIndex: rows.length - 1, comparableText: snapshotDecision.suffixText, sourceEvent: event, derivedSnapshotSuffix: true });
return rows.length - 1;
}
if (snapshotDecision.action === "keep-existing") return state.rowIndex;
rows[state.rowIndex] = mergeAssistantRows(row, existingRow);
state.comparableText = comparableText;
state.sourceEvent = event;
state.derivedSnapshotSuffix = false;
return state.rowIndex;
}
if (state.comparableText.includes(comparableText)) {
rows[state.rowIndex] = mergeAssistantRows(existingRow, row);
return state.rowIndex;
}
}
rows.push(row);
assistantRows.push({ rowIndex: rows.length - 1, comparableText, sourceEvent: event });
return rows.length - 1;
}
function upsertAuthoritativeFinalResponseRow(rows: TraceEventRow[], assistantRows: AssistantRowState[], completionEvent: TraceEvent, finalText: string): number {
const comparableText = comparableAssistantText(finalText);
let matchedState: AssistantRowState | null = null;
for (const state of assistantRows) {
const row = rows[state.rowIndex];
if (!row || row.bodyFormat !== "markdown") continue;
row.terminal = undefined;
if (!matchedState && state.comparableText === comparableText) matchedState = state;
}
if (matchedState) {
const row = rows[matchedState.rowIndex];
if (row) {
rows[matchedState.rowIndex] = {
...row,
body: finalText,
terminal: true,
tone: "ok",
header: `${traceEventClock(completionEvent)} 助手最终消息`
};
matchedState.comparableText = comparableText;
matchedState.derivedSnapshotSuffix = false;
return matchedState.rowIndex;
}
}
rows.push({
rowId: `trace-final-response:${completionEvent.seq ?? "completed"}`,
seq: numberOrNull(completionEvent.seq),
function traceFinalResponseRow(event: TraceEvent, finalText: string): TraceEventRow {
return {
rowId: `trace-final-response:${event.seq ?? "completed"}`,
seq: numberOrNull(event.seq),
tone: "ok",
header: `${traceEventClock(completionEvent)} 助手最终消息`,
header: `${traceEventClock(event)} 助手最终消息`,
terminal: true,
body: finalText,
bodyFormat: "markdown"
});
assistantRows.push({ rowIndex: rows.length - 1, comparableText, sourceEvent: completionEvent });
return rows.length - 1;
}
function assistantSnapshotDecision(nextText: string, previousText: string, nextEvent: TraceEvent, previousEvent: TraceEvent): AssistantSnapshotDecision {
if (!sameAssistantSnapshotIdentity(nextEvent, previousEvent)) return { action: "replace" };
if (isAuthoritativeAssistantEvent(nextEvent) || isAuthoritativeAssistantEvent(previousEvent)) return { action: "replace" };
const previousIndex = nextText.indexOf(previousText);
if (previousIndex < 0) return { action: "replace" };
const suffixText = nextText.slice(previousIndex + previousText.length).trim();
if (suffixText && suffixText.length >= Math.max(80, previousText.length / 3)) return { action: "append-suffix", suffixText };
if (previousIndex > 0 || !suffixText) return { action: "keep-existing" };
return { action: "replace" };
}
function sameAssistantSnapshotIdentity(left: TraceEvent, right: TraceEvent): boolean {
const leftItem = nonEmptyString(left.itemId ?? left.messageId ?? left.id);
const rightItem = nonEmptyString(right.itemId ?? right.messageId ?? right.id);
if (leftItem && rightItem) return leftItem === rightItem;
return false;
}
function isAuthoritativeAssistantEvent(event: TraceEvent): boolean {
return event.replyAuthority === true || event.final === true || event.terminal === true || String(event.status ?? "") === "completed";
}
function mergeAssistantRows(preferred: TraceEventRow, metadata: TraceEventRow): TraceEventRow {
return {
...preferred,
terminal: preferred.terminal || metadata.terminal ? true : undefined,
header: metadata.terminal && !preferred.terminal ? metadata.header : preferred.header,
tone: preferred.tone === "ok" || metadata.tone === "ok" ? "ok" : preferred.tone
};
}
function comparableAssistantText(value: unknown): string {
return cleanTraceText(value).replace(/\s+/gu, " ");
}
function traceFinalResponseText(trace: Record<string, unknown>): string | null {
const response = trace.finalResponse && typeof trace.finalResponse === "object" ? trace.finalResponse as Record<string, unknown> : null;
return nonEmptyString(response?.text ?? response?.content ?? response?.message);
}
function markAssistantRowTerminal(trace: Record<string, unknown>, row: TraceEventRow, completionEvent: TraceEvent): TraceEventRow {
return {
...row,
tone: "ok",
terminal: true,
header: `${row.header},轮次完成(总耗时 ${formatTraceDuration(traceRelativeMs(trace, completionEvent))}`
};
}
function traceCompletionSummaryRow(trace: Record<string, unknown>, event: TraceEvent): TraceEventRow {
return {
rowId: `trace-completion:${event.seq ?? "turn"}`,