Files
pikasTech-HWLAB/tools/src/hwlab-cli/trace-renderer.ts
T
2026-07-12 18:06:39 +02:00

843 lines
38 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export interface TraceEventRow {
rowId: string;
seq: number | null;
sequenceAuthority: TraceSequenceAuthority;
tone: "ok" | "blocked" | "warn" | "source";
toolState?: TraceToolState;
statusLabel?: string;
header: string;
body: string | null;
preview?: string | null;
terminal?: boolean;
bodyFormat?: "markdown" | "text";
}
export type TraceToolState = "running" | "success" | "failure" | "neutral";
export type TraceEvent = Record<string, unknown>;
type TraceClockFormatter = (value: string) => string;
export type TraceSequenceAuthority = "projected" | "source";
export type TraceFinalResponsePlacement = "trace" | "outer";
export interface TraceDisplayRowsOptions {
formatClock?: TraceClockFormatter;
finalResponsePlacement?: TraceFinalResponsePlacement;
outerFinalResponseText?: string | null;
}
interface ResolvedTraceDisplayRowsOptions extends TraceDisplayRowsOptions {
sequenceAuthority: TraceSequenceAuthority;
finalResponsePlacement: TraceFinalResponsePlacement;
}
export function traceDisplayRows(trace: Record<string, unknown> = {}, events: TraceEvent[] = [], options: TraceDisplayRowsOptions = {}): TraceEventRow[] {
const displayOptions: ResolvedTraceDisplayRowsOptions = {
...options,
sequenceAuthority: traceSequenceAuthority(trace),
finalResponsePlacement: options.finalResponsePlacement ?? "trace"
};
const orderedEvents = traceEventsForDisplay(
events.filter((event) => hasTraceSequence(event, displayOptions.sequenceAuthority)),
displayOptions.sequenceAuthority
);
const effectiveTrace = traceWithInferredStart(trace, orderedEvents);
const finalResponseText = nonEmptyString(options.outerFinalResponseText) ?? traceFinalResponseText(effectiveTrace);
const outerFinalAssistantEvent = displayOptions.finalResponsePlacement === "outer" && finalResponseText
? traceOuterFinalAssistantEvent(orderedEvents)
: null;
const hasCompletionEvent = orderedEvents.some(isCompletionTraceEvent);
const rows: TraceEventRow[] = [];
let completionEvent: TraceEvent | null = null;
const renderedSourceEvents = new Set<string>();
const renderedToolIdentities = new Set<string>();
for (let index = 0; index < orderedEvents.length; index += 1) {
const event = orderedEvents[index];
const sourceEventKey = traceSourceEventKey(event);
if (sourceEventKey) {
if (renderedSourceEvents.has(sourceEventKey)) continue;
renderedSourceEvents.add(sourceEventKey);
}
if (!event || isNoisyTraceEvent(event)) continue;
if (isSupersededTraceItemLifecycleEvent(event, orderedEvents, index)) continue;
if (isToolTraceEvent(event)) {
const identity = toolIdentity(event);
if (identity) {
if (renderedToolIdentities.has(identity)) continue;
renderedToolIdentities.add(identity);
}
rows.push(traceToolCallRow(event, displayOptions));
continue;
}
if (isDiffTraceEvent(event)) {
rows.push(traceDiffRow(event, displayOptions));
continue;
}
if (isRequestTraceEvent(event)) {
continue;
}
if (isSetupTraceEvent(event)) {
continue;
}
if (isTerminalAssistantTraceEvent(event)) {
if (isOuterFinalResponseTraceEvent(event, displayOptions, outerFinalAssistantEvent)) continue;
if (hasCompletionEvent) continue;
if (finalResponseText) {
rows.push(traceFinalResponseRow(event, finalResponseText, displayOptions));
} else {
rows.push(traceAssistantMessageRow(effectiveTrace, event, { terminal: true }, displayOptions));
}
continue;
}
if (isAssistantTraceEvent(event)) {
if (isOuterFinalResponseTraceEvent(event, displayOptions, outerFinalAssistantEvent)) continue;
rows.push(traceAssistantMessageRow(effectiveTrace, event, { terminal: false }, displayOptions));
continue;
}
if (isCompletionTraceEvent(event)) {
completionEvent ??= event;
continue;
}
rows.push(traceDisplayRow(event, displayOptions));
}
if (completionEvent) rows.push(traceCompletionSummaryRow(completionEvent, displayOptions));
const progressRow = traceBackendProgressSummaryRow(effectiveTrace, rows, orderedEvents, displayOptions);
if (progressRow) rows.push(progressRow);
if (rows.length > 0) return rows;
if (orderedEvents.length === 0) return [];
return orderedEvents
.filter((event, index) => !isSupersededTraceItemLifecycleEvent(event, orderedEvents, index))
.filter((event) => !isSuppressedTraceEvent(event))
.filter((event) => !isOuterFinalResponseTraceEvent(event, displayOptions, outerFinalAssistantEvent))
.map((event) => traceDisplayRow(event, displayOptions));
}
function isOuterFinalResponseTraceEvent(
event: TraceEvent,
options: ResolvedTraceDisplayRowsOptions,
outerFinalAssistantEvent: TraceEvent | null
): boolean {
if (options.finalResponsePlacement !== "outer") return false;
return event === outerFinalAssistantEvent;
}
function traceOuterFinalAssistantEvent(events: TraceEvent[]): TraceEvent | null {
const visible = events
.map((event, index) => ({ event, index }))
.filter(({ event, index }) => isAssistantTraceEvent(event) && !traceAssistantLifecycleEventIsSuperseded(event, events, index));
if (visible.length === 0) return null;
const authoritative = [...visible].reverse().find(({ event }) => event.replyAuthority === true || event.final === true);
if (authoritative) return authoritative.event;
const seal = [...events].reverse().find((event) => event.finalSeal === true || nonEmptyString(event.label) === "agentrun:backend:assistant-message-final-sealed");
if (seal) {
const replyRef = isRecord(seal.replyRef) ? seal.replyRef : null;
const itemId = nonEmptyString(replyRef?.itemId ?? seal.itemId);
if (itemId) {
const matched = [...visible].reverse().find(({ event }) => nonEmptyString(event.itemId) === itemId);
if (matched) return matched.event;
}
const sealIndex = events.indexOf(seal);
const preceding = [...visible].reverse().find(({ index }) => index < sealIndex);
if (preceding) return preceding.event;
}
let terminalIndex = -1;
for (let index = events.length - 1; index >= 0; index -= 1) {
const event = events[index];
if (!event || !(isCompletionTraceEvent(event) || event.terminal === true || event.eventType === "terminal" || event.type === "result")) continue;
terminalIndex = index;
break;
}
const precedingTerminal = terminalIndex >= 0 ? [...visible].reverse().find(({ index }) => index < terminalIndex) : null;
return precedingTerminal?.event ?? visible.at(-1)?.event ?? null;
}
function traceSequenceAuthority(trace: Record<string, unknown>): TraceSequenceAuthority {
return nonEmptyString(trace.eventSource) === "hwlab-kafka-sse" ? "source" : "projected";
}
function hasTraceSequence(event: TraceEvent | null | undefined, authority: TraceSequenceAuthority): boolean {
return traceEventDisplaySeq(event, authority) !== null;
}
export function renderTraceRowsMarkdown(rows: TraceEventRow[] = []): string {
if (!Array.isArray(rows) || rows.length === 0) return `_No rendered trace rows were returned._`;
return rows.map((row, index) => renderTraceRowMarkdown(row, index + 1)).join("\n\n");
}
export function renderTraceRowMarkdown(row: TraceEventRow, index = 1): string {
if (!isToolTraceRow(row)) return renderTraceMessageRowMarkdown(row);
const body = renderTraceToolDetailBody(row) || row.header || `trace row ${index}`;
const summary = traceToolSummary(row);
const accessibleLabel = traceToolAccessibilityLabel(row);
const visibleSummary = [
`<code>${escapeHtml(row.header || `trace row ${index}`)}</code>`,
summary ? escapeHtml(summary) : null
].filter(Boolean).join(" ");
return [
`- <details>`,
` <summary aria-label="${escapeHtmlAttribute(accessibleLabel)}" title="${escapeHtmlAttribute(accessibleLabel)}">${visibleSummary}</summary>`,
``,
indentTraceMarkdownBlock(traceMarkdownFence(row.bodyFormat === "markdown" ? "markdown" : "text", body)),
``,
` </details>`
].join("\n");
}
function renderTraceMessageRowMarkdown(row: TraceEventRow): string {
const body = row.body?.trimEnd();
if (body) return traceBoldMarkdown(body);
return traceBoldMarkdown(row.header || `_No readable trace row body._`);
}
function traceEventsForDisplay(events: TraceEvent[] = [], authority: TraceSequenceAuthority): TraceEvent[] {
if (authority === "source") return [...events];
return [...events].sort((left, right) => {
const leftSeq = traceEventDisplaySeq(left, authority);
const rightSeq = traceEventDisplaySeq(right, authority);
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, authority: TraceSequenceAuthority): number | null {
return numberOrNull(authority === "source" ? event?.sourceSeq : event?.projectedSeq);
}
function traceEventIdentityToken(event: TraceEvent, authority: TraceSequenceAuthority): string | number | null {
if (authority === "source") {
const sourceEventId = nonEmptyString(event.sourceEventId);
if (sourceEventId) return sourceEventId;
const runId = nonEmptyString(event.runId);
const sourceSeq = traceEventDisplaySeq(event, authority);
if (runId && sourceSeq !== null) return `${runId}:${sourceSeq}`;
return sourceSeq;
}
const seq = traceEventDisplaySeq(event, authority);
return seq ?? nonEmptyString(event.projectedSeq);
}
function isToolTraceRow(row: TraceEventRow): boolean {
return row.rowId.startsWith("tool:") || /commandExecution/u.test(row.header);
}
export function traceToolSummary(row: TraceEventRow): string {
if (row.preview) return compactTraceOneLine(row.preview, 140);
const firstLine = traceMarkdownPreviewLines(row.body).find((line) => !/^(stdout|stderr|exitCode|rowId|terminal):/iu.test(line.trim()));
return compactTraceOneLine(stripTraceToolOutputFromSummary(firstLine || row.header || "tool call"), 140);
}
export function traceToolAccessibilityLabel(row: TraceEventRow): string {
const statusLabel = row.statusLabel ?? traceToolStatusLabel(traceToolStateFromTone(row.tone));
return `${statusLabel}${[row.header, traceToolSummary(row)].filter(Boolean).join("")}`;
}
function stripTraceToolOutputFromSummary(value: string): string {
return value.replace(/\s+(stdout:|stderr:|exitCode=).*$/isu, "").trim();
}
function traceToolCallRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOptions): TraceEventRow {
const command = cleanShellCommand(event.command);
const toolState = traceToolState(event);
const toolName = traceToolName(event, command);
const body = isFileChangeTraceEvent(event) ? traceFileChangeBody(event) : traceToolCallBody(event, command);
const eventKey = traceEventIdentityToken(event, options.sequenceAuthority);
return {
rowId: `tool:${event.itemId ?? eventKey ?? `${event.label ?? event.type ?? "tool"}:${event.createdAt ?? "unknown"}`}`,
seq: traceEventDisplaySeq(event, options.sequenceAuthority),
sequenceAuthority: options.sequenceAuthority,
tone: traceEventTone(event),
toolState,
statusLabel: traceToolStatusLabel(toolState),
header: traceEventClock(event, options),
body,
preview: isFileChangeTraceEvent(event) ? traceFileChangePreview(event) : traceToolPreview(command, toolName),
bodyFormat: "text"
};
}
function isFileChangeTraceEvent(event: TraceEvent): boolean {
return nonEmptyString(event.toolName ?? event.toolType) === "fileChange";
}
function traceFileChangePreview(event: TraceEvent): string {
const paths = (Array.isArray(event.changes) ? event.changes : [])
.filter(isRecord)
.map((change) => nonEmptyString(change.path))
.filter((path): path is string => Boolean(path));
return compactTraceOneLine(paths.length > 0 ? `fileChange ${paths.join(", ")}` : "fileChange", 140);
}
function traceFileChangeBody(event: TraceEvent): string | null {
const changes = (Array.isArray(event.changes) ? event.changes : []).filter(isRecord);
if (changes.length === 0) return null;
return changes.map((change) => {
const path = nonEmptyString(change.path) ?? "(path missing)";
const kind = nonEmptyString(change.kind) ?? "update";
const diff = nonEmptyString(change.diff) ?? "";
return [`path: ${path}`, `kind: ${kind}`, "diff:", diff].filter(Boolean).join("\n");
}).join("\n\n");
}
function isDiffTraceEvent(event: TraceEvent): boolean {
return event.type === "diff" || event.eventType === "diff" || nonEmptyString(event.label) === "agentrun:diff";
}
function traceDiffRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOptions): TraceEventRow {
const eventKey = traceEventIdentityToken(event, options.sequenceAuthority);
return {
rowId: `tool:diff:${eventKey ?? event.createdAt ?? "updated"}`,
seq: traceEventDisplaySeq(event, options.sequenceAuthority),
sequenceAuthority: options.sequenceAuthority,
tone: traceEventTone(event),
toolState: "success",
statusLabel: "文件 diff 已更新",
header: traceEventClock(event, options),
body: nonEmptyString(event.diff ?? event.message),
preview: "turn/diff/updated",
bodyFormat: "text"
};
}
function traceToolPreview(command: string | null, fallbackToolName: string): string {
return compactTraceOneLine(command ?? fallbackToolName, 140);
}
function traceToolName(event: TraceEvent, command: string | null): string {
const direct = nonEmptyString(event.toolName);
if (direct && !isProtocolToolLifecycleName(direct) && !isGenericToolName(direct)) return direct;
const label = String(event.label ?? "")
.replace(/^agentrun:tool:/u, "")
.replace(/^tool:/u, "")
.replace(/^item\//u, "")
.replace(/\/outputDelta$/iu, "")
.replace(/:(?:started|running|completed|succeeded|failed|blocked|error|timeout|canceled|cancelled)$/iu, "");
if (label && !isProtocolToolLifecycleName(label) && !isGenericToolName(label)) return label;
const commandName = traceToolNameFromCommand(command);
if (commandName) return commandName;
if (direct && !isProtocolToolLifecycleName(direct)) return direct;
const previewName = traceToolNameFromPreview(event);
if (previewName) return previewName;
return "tool call";
}
function isGenericToolName(value: unknown): boolean {
return /^(commandExecution|tool|tool_call|tool-call|shell|command|unknown)$/iu.test(String(value ?? "").trim());
}
function isProtocolToolLifecycleName(value: unknown): boolean {
return /^(?:item\/)?(?:started|running|completed|succeeded|failed|blocked|error|timeout|canceled|cancelled)$/iu.test(String(value ?? "").trim());
}
function traceToolNameFromCommand(command: string | null): string | null {
const text = cleanTraceText(command);
const segments = text.split(/\s*(?:&&|\|\||;)\s*/u).map((value) => value.trim()).filter(Boolean);
const candidate = segments.find((value) => !/^(?:cd|export|set|source|umask)\b/u.test(value)) ?? segments[0] ?? text;
const normalized = candidate
.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)+/u, "")
.replace(/^(?:sudo\s+|env\s+|command\s+)+/u, "")
.replace(/^timeout\s+\S+\s+/u, "");
const match = normalized.match(/^([A-Za-z./][A-Za-z0-9_./:-]{0,80})(?:\s|$)/u);
return match?.[1]?.split("/").filter(Boolean).at(-1) ?? null;
}
function traceToolNameFromPreview(event: TraceEvent): string | null {
const preview = traceEventText(event, ["output", "outputText", "outputSummary", "stdoutSummary", "message"]);
return jsonPreviewStringField(preview, "toolName") ?? jsonPreviewStringField(preview, "type") ?? jsonPreviewStringField(preview, "name");
}
function jsonPreviewStringField(text: string | null, key: string): string | null {
const body = String(text ?? "");
if (!body || !/^[A-Za-z][A-Za-z0-9_]*$/u.test(key)) return null;
const match = body.match(new RegExp(`"${key}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`, "u"));
if (!match) return null;
try {
const parsed = JSON.parse(`"${match[1]}"`);
return typeof parsed === "string" && parsed.trim() ? parsed.trim() : null;
} catch {
return match[1]?.trim() || null;
}
}
function traceToolCallBody(event: TraceEvent, command: string | null): string | null {
const stdout = traceEventText(event, ["stdout", "stdoutText", "stdoutSummary"]);
const stderr = traceEventText(event, ["stderr", "stderrText", "stderrSummary"]);
const output = traceEventText(event, ["output", "outputText", "outputSummary"]);
const distinctOutput = output && output !== stdout && output !== stderr ? output : null;
const outputBytes = numberOrNull(event.outputBytes);
const lines = [
command,
traceDetailBlock("stdout", stdout),
traceDetailBlock("stderr", stderr),
traceDetailBlock("output", distinctOutput),
event.commandTruncated === true ? "commandTruncated=true" : null,
outputBytes !== null ? `outputBytes=${outputBytes}` : null,
event.outputTruncated === true ? "outputTruncated=true" : null,
event.exitCode !== undefined ? `exitCode=${event.exitCode}` : null
].map(cleanTraceDetailText).filter(Boolean);
return lines.length > 0 ? lines.join("\n") : null;
}
export function traceNoiseEventCount(events: TraceEvent[] = []): number {
return Array.isArray(events) ? events.filter((event) => isNoisyTraceEvent(event)).length : 0;
}
function traceAssistantMessageRow(trace: Record<string, unknown>, event: TraceEvent, { terminal }: { terminal: boolean }, options: ResolvedTraceDisplayRowsOptions): TraceEventRow {
const text = traceAssistantEventText(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, options.sequenceAuthority);
return {
rowId: `event:${eventKey ?? `${event.label ?? event.type ?? "assistant"}:${event.createdAt ?? "unknown"}`}`,
seq: traceEventDisplaySeq(event, options.sequenceAuthority),
sequenceAuthority: options.sequenceAuthority,
tone: "ok",
header: `${traceEventClock(event, options)} ${terminal ? "助手最终消息" : "助手消息"}${index ? ` ${index}${count ? `/${count}` : ""}` : ""}`,
terminal: terminal ? true : undefined,
body: text || null,
bodyFormat: "markdown"
};
}
function traceAssistantEventText(trace: Record<string, unknown>, event: TraceEvent): string {
return cleanTraceDetailText(event.message ?? event.outputSummary ?? assistantStreamText(trace, event) ?? "");
}
function traceFinalResponseRow(event: TraceEvent, finalText: string, options: ResolvedTraceDisplayRowsOptions): TraceEventRow {
const eventKey = traceEventIdentityToken(event, options.sequenceAuthority);
return {
rowId: `trace-final-response:${eventKey ?? "completed"}`,
seq: traceEventDisplaySeq(event, options.sequenceAuthority),
sequenceAuthority: options.sequenceAuthority,
tone: "ok",
header: `${traceEventClock(event, options)} 助手最终消息`,
terminal: true,
body: finalText,
bodyFormat: "markdown"
};
}
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 traceCompletionSummaryRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOptions): TraceEventRow {
const eventKey = traceEventIdentityToken(event, options.sequenceAuthority);
return {
rowId: `trace-completion:${eventKey ?? "turn"}`,
seq: traceEventDisplaySeq(event, options.sequenceAuthority),
sequenceAuthority: options.sequenceAuthority,
tone: traceEventTone(event),
header: `${traceEventClock(event, options)} 轮次完成`,
body: null
};
}
function traceNoiseSummaryRow(trace: Record<string, unknown>, events: TraceEvent[], options: ResolvedTraceDisplayRowsOptions): TraceEventRow {
const lastEvent = events.at(-1);
const status = nonEmptyString(trace.status ?? trace.traceStatus) ?? "running";
const lastLabel = lastEvent ? readableTraceLabel(lastEvent) : "未观测";
return {
rowId: "trace-noise-summary",
seq: traceEventDisplaySeq(lastEvent, options.sequenceAuthority),
sequenceAuthority: options.sequenceAuthority,
tone: lastEvent ? traceEventTone(lastEvent) : "source",
header: `Trace ${status},等待可读事件`,
body: `已隐藏 ${events.length} 条 AgentRun backend 状态事件。最新原始事件:${lastLabel}。`,
bodyFormat: "text"
};
}
function traceBackendProgressSummaryRow(trace: Record<string, unknown>, rows: TraceEventRow[], events: TraceEvent[], options: ResolvedTraceDisplayRowsOptions): TraceEventRow | null {
if (!Array.isArray(events) || events.length === 0) return null;
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 = traceEventDisplaySeq(event, options.sequenceAuthority) ?? 0;
return seq > lastRenderedSeq && (isRequestTraceEvent(event) || isSetupTraceEvent(event) || isNoisyTraceEvent(event));
});
return hiddenProgress.length > 0 ? traceNoiseSummaryRow(trace, hiddenProgress, options) : null;
}
function traceDisplayRow(event: TraceEvent, options: ResolvedTraceDisplayRowsOptions): TraceEventRow {
const label = readableTraceLabel(event);
const eventKey = traceEventIdentityToken(event, options.sequenceAuthority);
return {
rowId: `event:${eventKey ?? `${event.label ?? event.type ?? "event"}:${event.createdAt ?? "unknown"}`}`,
seq: traceEventDisplaySeq(event, options.sequenceAuthority),
sequenceAuthority: options.sequenceAuthority,
tone: traceEventTone(event),
header: `${traceEventClock(event, options)} ${label}`.trim(),
body: traceDisplayBody(event)
};
}
function assistantStreamText(trace: Record<string, unknown>, event: TraceEvent): string {
const streams = Array.isArray(trace.assistantStreams) ? trace.assistantStreams.filter(isRecord) : [];
const itemId = nonEmptyString(event.itemId ?? event.messageId ?? event.id);
const matched = itemId ? streams.find((stream) => [stream.itemId, stream.messageId, stream.id].map(nonEmptyString).includes(itemId)) : null;
return nonEmptyString(matched?.text ?? streams.at(-1)?.text) ?? "";
}
function isRequestTraceEvent(event: TraceEvent): boolean {
const label = String(event.label ?? "");
return label === "agentrun:request:accepted" || label === "request:accepted-short-connection" || label === "request:accepted" || label === "request:received";
}
function isSetupTraceEvent(event: TraceEvent): boolean {
const label = String(event.label ?? "");
return /^session:|^stdio:|^prompt:|^thread:|^turn:started|^turn:start|turn\/start|codex turn|tool:codex-app-server/iu.test(label) ||
/^agentrun:backend:(codex-app-server-starting|initialize:completed|thread\/start|thread\/resume|thread\/started|turn\/start|turn\/started)/iu.test(label) ||
/^agentrun:run:(reuse-skipped|reused)$/iu.test(label) ||
/^agentrun:command:created$/iu.test(label) ||
/^agentrun:runner-job:(created|ensured|reused)$/iu.test(label) ||
/^agentrun:session-storage-recover-warning$/iu.test(label);
}
function isCompletionTraceEvent(event: TraceEvent): boolean {
const label = String(event.label ?? "");
return label.startsWith("turn:completed") || label.startsWith("result:completed") || label === "agentrun:backend:turn/completed" || label === "agentrun:terminal:completed" || label === "agentrun:result:completed";
}
function isAssistantTraceEvent(event: TraceEvent): boolean {
const label = String(event.label ?? "");
return label === "agentrun:assistant:message"
|| label === "agentrun:assistant:progress"
|| label === "assistant:message"
|| event.type === "assistant_progress"
|| event.eventType === "assistant_progress"
|| event.type === "assistant_message"
|| event.type === "assistant";
}
function isTerminalAssistantTraceEvent(event: TraceEvent): boolean {
const label = String(event.label ?? "");
if (label === "assistant:completed") return true;
if (label === "agentrun:assistant:message") return event.replyAuthority === true || event.final === true;
return event.type === "assistant_message" && (event.status === "completed" || event.final === true || event.terminal === true);
}
function isToolTraceEvent(event: TraceEvent): boolean {
const label = String(event.label ?? "");
const type = String(event.type ?? "");
const toolName = String(event.toolName ?? "");
return type === "tool_call" || toolName === "commandExecution" || /commandExecution|^agentrun:tool:/u.test(label);
}
function isToolStartTraceEvent(event: TraceEvent): boolean {
const status = String(event.status ?? "").toLowerCase();
const label = String(event.label ?? "");
return ["started", "running", "inprogress", "in-progress", "output_chunk"].includes(status) || /:started$|\/started$/u.test(label);
}
function isToolCompleteTraceEvent(event: TraceEvent): boolean {
const status = String(event.status ?? "").toLowerCase();
const label = String(event.label ?? "");
return ["completed", "succeeded", "failed", "blocked", "timeout", "canceled", "cancelled"].includes(status) || /:completed$|\/completed$/u.test(label);
}
function toolIdentity(event: TraceEvent): string | null {
return nonEmptyString(event.itemId ?? event.id ?? event.command);
}
function traceSourceEventKey(event: TraceEvent | null | undefined): string | null {
if (!event || typeof event !== "object") return null;
const sourceEventId = nonEmptyString(event.sourceEventId);
if (sourceEventId) return `source-event:${sourceEventId}`;
const runId = nonEmptyString(event.runId);
const sourceSeq = nonEmptyString(event.sourceSeq);
if (runId && sourceSeq) return `run-source:${runId}:${sourceSeq}`;
const source = nonEmptyString(event.source);
if (!source || !sourceSeq) return null;
return `${source}:${sourceSeq}:${nonEmptyString(event.label ?? event.type) ?? "event"}`;
}
function isSupersededTraceItemLifecycleEvent(event: TraceEvent, events: TraceEvent[], index: number): boolean {
return isSupersededToolStart(event, events, index) || traceAssistantLifecycleEventIsSuperseded(event, events, index);
}
function isSupersededToolStart(event: TraceEvent, events: TraceEvent[], index: number): boolean {
if (!isToolStartTraceEvent(event)) return false;
const identity = toolIdentity(event);
if (!identity) return false;
return events.slice(index + 1).some((candidate) => candidate && isToolTraceEvent(candidate) && isToolCompleteTraceEvent(candidate) && toolIdentity(candidate) === identity);
}
export function traceAssistantLifecycleEventIsSuperseded(event: TraceEvent, events: TraceEvent[], index: number): boolean {
if (!isAssistantTraceEvent(event)) return false;
const identity = nonEmptyString(event.itemId);
if (!identity) return false;
const rank = traceAssistantLifecycleRank(event);
const sequence = traceAssistantLifecycleSequence(event);
return events.some((candidate, candidateIndex) => {
if (candidateIndex === index || !candidate || !isAssistantTraceEvent(candidate) || nonEmptyString(candidate.itemId) !== identity) return false;
const candidateRank = traceAssistantLifecycleRank(candidate);
if (candidateRank !== rank) return candidateRank > rank;
const candidateSequence = traceAssistantLifecycleSequence(candidate);
if (sequence !== null && candidateSequence !== null && candidateSequence !== sequence) return candidateSequence > sequence;
return candidateIndex > index;
});
}
function traceAssistantLifecycleRank(event: TraceEvent): number {
if (event.replyAuthority === true || event.final === true) return 3;
if (traceAssistantLifecycleProgress(event)) return 1;
return 2;
}
function traceAssistantLifecycleProgress(event: TraceEvent): boolean {
return event.progress === true
|| event.type === "assistant_progress"
|| event.eventType === "assistant_progress"
|| nonEmptyString(event.label) === "agentrun:assistant:progress"
|| nonEmptyString(event.assistantSource) === "agent-message-delta-progress";
}
function traceAssistantLifecycleSequence(event: TraceEvent): number | null {
return numberOrNull(event.sourceSeq ?? event.projectedSeq ?? event.seq);
}
function isNoisyTraceEvent(event: TraceEvent): boolean {
const label = String(event.label ?? "");
if (isRequestTraceEvent(event) || isSetupTraceEvent(event) || isCompletionTraceEvent(event) || isTerminalAssistantTraceEvent(event) || isAssistantTraceEvent(event)) return false;
if (/^agentrun:backend:/u.test(label) && !event.errorCode) return true;
if (/token_count|outputDelta:chunk/iu.test(label)) return true;
if (/^agentrun:output:(stdout|stderr)$/u.test(label)) return true;
if (/^agentrun:backend:(run-created|command-created|runner-job-created|thread\/status\/changed|thread\/tokenUsage\/updated|account\/rateLimits\/updated|remoteControl\/status\/changed|configWarning|codex-app-server-closed|codex-app-server-notifications-suppressed|session-updated|command-terminal|backend-turn-finished|item\/agentMessage:(started|completed)|thread\/goal\/cleared)$/u.test(label)) return true;
return event.type === "event" && !event.outputSummary && !event.message && !event.errorCode;
}
function isSuppressedTraceEvent(event: TraceEvent): boolean {
return isNoisyTraceEvent(event) || isRequestTraceEvent(event) || isSetupTraceEvent(event);
}
function readableTraceLabel(event: TraceEvent): string {
const label = String(event.label ?? `${event.type ?? "event"}:${event.status ?? "observed"}`);
if (label === "request:accepted-short-connection") return "submit short-connection";
if (label === "request:accepted") return "request accepted";
if (label.startsWith("turn:completed")) return "turn completed";
if (label.startsWith("result:completed")) return "result ready";
return label.replace(/^tool:/u, "").replace(/^assistant:/u, "assistant ").replace(/:completed$/u, " completed").replace(/:started$/u, " started");
}
function traceDisplayBody(event: TraceEvent): string | null {
const command = cleanShellCommand(event.command);
const lines = [
command,
traceDetailBlock("stdout", traceEventText(event, ["stdout", "stdoutText", "stdoutSummary"])),
traceDetailBlock("stderr", traceEventText(event, ["stderr", "stderrText", "stderrSummary"])),
traceEventText(event, ["output", "outputText", "outputSummary"]),
traceEventText(event, ["prompt", "promptText", "promptSummary"]),
traceEventText(event, ["message"]),
traceEventText(event, ["chunk"])
].map(cleanTraceDetailText).filter(Boolean);
return lines.length > 0 ? lines.join("\n") : null;
}
function traceMarkdownPreviewLines(body: string | null | undefined): string[] {
return String(body ?? "")
.replace(/\r\n|\r/gu, "\n")
.trim()
.split("\n")
.map((line) => line.trimEnd())
.filter((line) => Boolean(line.trim()));
}
function renderTraceRowDetailBody(row: TraceEventRow): string {
return [
row.body?.trimEnd() ?? "",
row.rowId ? `rowId: ${row.rowId}` : "",
row.terminal === true ? "terminal: true" : ""
].filter(Boolean).join("\n");
}
function renderTraceToolDetailBody(row: TraceEventRow): string {
return normalizeTraceToolDetailBody(renderTraceRowDetailBody(row));
}
function normalizeTraceToolDetailBody(value: string): string {
return String(value ?? "")
.replace(/\r\n|\r/gu, "\n")
.replace(/[^\S\n]+(stdout:)[^\S\n]*/giu, "\n$1\n")
.replace(/[^\S\n]+(stderr:)[^\S\n]*/giu, "\n$1\n")
.replace(/[^\S\n]+(exitCode=)/giu, "\n$1")
.replace(/\n{3,}/gu, "\n\n")
.trimEnd();
}
function traceBoldMarkdown(value: string): string {
const body = String(value ?? "").trimEnd();
if (!body) return "";
return body.split("\n").map((line) => {
if (!line.trim()) return "";
return `**${escapeMarkdownStrongText(line)}**`;
}).join("\n");
}
function escapeMarkdownStrongText(value: string): string {
return escapeHtml(value).replace(/\\/gu, "\\\\").replace(/\*/gu, "\\*");
}
function indentTraceMarkdownBlock(value: string): string {
return String(value).split("\n").map((line) => ` ${line}`).join("\n");
}
function escapeHtml(value: string): string {
return String(value).replace(/&/gu, "&amp;").replace(/</gu, "&lt;").replace(/>/gu, "&gt;");
}
function escapeHtmlAttribute(value: string): string {
return escapeHtml(value).replace(/"/gu, "&quot;").replace(/'/gu, "&#39;");
}
function traceMarkdownFence(language: string, body: string): string {
const fence = body.includes("```") ? "````" : "```";
return `${fence}${language}\n${body.trimEnd()}\n${fence}`;
}
function traceStatusToken(event: TraceEvent): string {
const rawStatus = String(event.status ?? "");
const status = rawStatus.toLowerCase();
const exitCode = numberOrNull(event.exitCode);
if (["failed", "blocked", "error", "timeout", "canceled", "cancelled"].includes(status) || event.errorCode || (exitCode !== null && exitCode !== 0)) return "fail";
if (status === "completed" || status === "succeeded" || exitCode === 0) return "ok";
if (["started", "running", "accepted", "inprogress", "in-progress", "output_chunk"].includes(status)) return "run";
return rawStatus || "event";
}
function traceToolState(event: TraceEvent): TraceToolState {
const status = traceStatusToken(event);
if (status === "ok") return "success";
if (status === "fail") return "failure";
if (status === "run") return "running";
return "neutral";
}
function traceToolStateFromTone(tone: TraceEventRow["tone"]): TraceToolState {
if (tone === "ok") return "success";
if (tone === "blocked") return "failure";
if (tone === "warn") return "running";
return "neutral";
}
function traceToolStatusLabel(state: TraceToolState): string {
if (state === "success") return "工具调用成功";
if (state === "failure") return "工具调用失败";
if (state === "running") return "工具调用中";
return "工具调用状态未知";
}
function traceEventTone(event: TraceEvent): TraceEventRow["tone"] {
const status = traceStatusToken(event);
if (status === "ok") return "ok";
if (status === "fail") return "blocked";
if (status === "run") return "warn";
return "source";
}
function traceClock(value: unknown, options: TraceDisplayRowsOptions): string {
const timestamp = nonEmptyString(value);
if (timestamp && options.formatClock) return options.formatClock(timestamp);
const date = new Date(String(value ?? ""));
return Number.isNaN(date.getTime()) ? "--:--:--" : date.toISOString().slice(11, 19);
}
function traceEventClock(event: TraceEvent | null | undefined, options: TraceDisplayRowsOptions): string {
return traceClock(traceEventTimestamp(event), options);
}
function traceWithInferredStart(trace: Record<string, unknown>, events: TraceEvent[]): Record<string, unknown> {
if (nonEmptyString(trace.startedAt ?? trace.createdAt)) return trace;
const firstTimestamp = events.map((event) => traceEventTimestamp(event)).find(Boolean);
return firstTimestamp ? { ...trace, startedAt: firstTimestamp } : trace;
}
function traceEventTimestamp(event: TraceEvent | null | undefined): string | null {
if (!event) return null;
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 cleanTraceText(value: unknown): string {
return String(value ?? "").replace(/\u0000/gu, "").replace(/\r\n|\r/gu, "\n").replace(/[ \t]{2,}/gu, " ").replace(/\n{3,}/gu, "\n\n").trim();
}
function cleanTraceDetailText(value: unknown): string {
return String(value ?? "").replace(/\u0000/gu, "").replace(/\r\n|\r/gu, "\n").replace(/\n{4,}/gu, "\n\n\n").trim();
}
function traceEventText(event: TraceEvent, keys: string[]): string | null {
for (const key of keys) {
const value = event[key];
if (value === undefined || value === null) continue;
const body = cleanTraceDetailText(value);
if (body) return body;
}
return null;
}
function traceDetailBlock(label: string, value: string | null): string | null {
return value ? `${label}:\n${value}` : null;
}
function cleanShellCommand(value: unknown): string | null {
const raw = cleanTraceDetailText(value);
if (!raw) return null;
const stripped = stripShellWrapper(raw);
const unescaped = stripped.replace(/\\(["`$\\])/gu, "$1");
return cleanTraceDetailText(unescaped);
}
function stripShellWrapper(value: string): string {
const match = value.match(/^\s*(?:\/usr\/bin\/env\s+)?(?:\/bin\/sh|sh|bash|\/bin\/bash)\s+-lc\s+([\s\S]*)$/u);
if (!match) return value;
return unwrapShellArgument(match[1] ?? "");
}
function unwrapShellArgument(value: string): string {
const text = value.trim();
if (text.length >= 2) {
const quote = text[0];
if ((quote === "'" || quote === '"') && text[text.length - 1] === quote) {
return text.slice(1, -1);
}
}
return text;
}
function compactTraceOneLine(value: unknown, limit = 220): string {
const text = cleanTraceText(value).replace(/\s+/gu, " ");
return text.length <= limit ? text : `${text.slice(0, Math.max(0, limit - 3))}...`;
}
function numberOrNull(value: unknown): number | null {
if (value === null || value === undefined || value === "") return null;
const number = Number(value);
return Number.isInteger(number) ? number : null;
}
function nonEmptyString(value: unknown): string | null {
const text = String(value ?? "").trim();
return text ? text : null;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value && typeof value === "object");
}