Fix workbench trace projection authority (#1696)
This commit is contained in:
@@ -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"}`,
|
||||
|
||||
Reference in New Issue
Block a user