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; 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 = {}, 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(); const renderedToolIdentities = new Set(); 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): 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 = [ `${escapeHtml(row.header || `trace row ${index}`)}`, summary ? escapeHtml(summary) : null ].filter(Boolean).join(" "); return [ `-
`, ` ${visibleSummary}`, ``, indentTraceMarkdownBlock(traceMarkdownFence(row.bodyFormat === "markdown" ? "markdown" : "text", body)), ``, `
` ].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, 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, 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 | null { const response = trace.finalResponse && typeof trace.finalResponse === "object" ? trace.finalResponse as Record : 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, 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, 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, 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, "&").replace(//gu, ">"); } function escapeHtmlAttribute(value: string): string { return escapeHtml(value).replace(/"/gu, """).replace(/'/gu, "'"); } 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, events: TraceEvent[]): Record { 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 { return Boolean(value && typeof value === "object"); }