Merge pull request #1299 from pikasTech/fix-1286-error-observability

fix: improve code agent failure diagnostics
This commit is contained in:
Lyon
2026-06-16 02:31:47 +08:00
committed by GitHub
4 changed files with 241 additions and 44 deletions
@@ -7,39 +7,58 @@ import { firstNonEmptyString } from "@/utils";
const props = defineProps<{ message: ChatMessage }>(); const props = defineProps<{ message: ChatMessage }>();
const trace = computed(() => props.message.runnerTrace ?? null); const trace = computed(() => props.message.runnerTrace ?? null);
const agentRun = computed(() => asAgentRun(trace.value?.agentRun)); const traceRecord = computed(() => recordValue(trace.value));
const error = computed(() => props.message.error ?? null); const agentRun = computed(() => asAgentRun(traceRecord.value?.agentRun ?? (props.message as Record<string, unknown>).agentRun));
const error = computed(() => normalizeSummaryError(props.message.error ?? traceRecord.value?.error));
const eventCount = computed(() => trace.value?.eventCount ?? trace.value?.events?.length ?? 0); const eventCount = computed(() => trace.value?.eventCount ?? trace.value?.events?.length ?? 0);
const errorTitle = computed(() => firstNonEmptyString(error.value?.code, error.value?.category, "失败原因"));
const errorMessage = computed(() => firstNonEmptyString(error.value?.message, error.value?.userMessage, error.value?.reason));
const rows = computed(() => [ const rows = computed(() => [
{ key: "messageStatus", label: "消息状态", value: props.message.status }, summaryRow("messageStatus", "消息状态", props.message.status),
{ key: "traceStatus", label: "Trace 状态", value: firstNonEmptyString(trace.value?.status, trace.value?.traceStatus) }, summaryRow("traceStatus", "Trace 状态", firstNonEmptyString(trace.value?.status, trace.value?.traceStatus)),
{ key: "traceId", label: "Trace", value: props.message.traceId ?? trace.value?.traceId }, summaryRow("traceId", "Trace", props.message.traceId ?? trace.value?.traceId),
{ key: "conversationId", label: "Conversation", value: props.message.conversationId }, summaryRow("conversationId", "Conversation", props.message.conversationId),
{ key: "sessionId", label: "Session", value: props.message.sessionId ?? trace.value?.sessionId }, summaryRow("sessionId", "Session", props.message.sessionId ?? trace.value?.sessionId),
{ key: "threadId", label: "Thread", value: props.message.threadId ?? trace.value?.threadId }, summaryRow("threadId", "Thread", props.message.threadId ?? trace.value?.threadId),
{ key: "runnerKind", label: "Runner", value: trace.value?.runnerKind ?? agentRun.value?.adapter }, summaryRow("runnerKind", "Runner", trace.value?.runnerKind ?? agentRun.value?.adapter),
{ key: "sessionMode", label: "Session Mode", value: trace.value?.sessionMode ?? agentRun.value?.backendProfile }, summaryRow("sessionMode", "Session Mode", trace.value?.sessionMode ?? agentRun.value?.backendProfile),
{ key: "waitingFor", label: "Waiting For", value: trace.value?.waitingFor }, summaryRow("waitingFor", "Waiting For", trace.value?.waitingFor),
{ key: "lastEvent", label: "Last Event", value: trace.value?.lastEventLabel }, summaryRow("lastEvent", "Last Event", trace.value?.lastEventLabel),
{ key: "eventCount", label: "Events", value: String(eventCount.value) }, summaryRow("eventCount", "Events", eventCount.value > 0 ? eventCount.value : null),
{ key: "eventsCompacted", label: "Compacted", value: trace.value?.eventsCompacted === true ? "true" : "false" }, summaryRow("eventsCompacted", "Compacted", trace.value?.eventsCompacted === true ? "true" : null),
{ key: "fullTraceLoaded", label: "Full Trace", value: trace.value?.fullTraceLoaded === true ? "loaded" : "unverified" }, summaryRow("fullTraceLoaded", "Full Trace", trace.value?.fullTraceLoaded === true ? "loaded" : null),
{ key: "runId", label: "AgentRun", value: agentRun.value?.runId }, summaryRow("runId", "AgentRun", agentRun.value?.runId),
{ key: "commandId", label: "Command", value: agentRun.value?.commandId }, summaryRow("commandId", "Command", agentRun.value?.commandId),
{ key: "attemptId", label: "Attempt", value: agentRun.value?.attemptId }, summaryRow("attemptId", "Attempt", agentRun.value?.attemptId),
{ key: "runnerId", label: "Runner ID", value: agentRun.value?.runnerId }, summaryRow("runnerId", "Runner ID", agentRun.value?.runnerId),
{ key: "jobName", label: "Job", value: agentRun.value?.jobName }, summaryRow("jobName", "Job", agentRun.value?.jobName),
{ key: "namespace", label: "Namespace", value: agentRun.value?.namespace }, summaryRow("namespace", "Namespace", agentRun.value?.namespace),
{ key: "terminalStatus", label: "Terminal", value: agentRun.value?.terminalStatus }, summaryRow("terminalStatus", "Terminal", agentRun.value?.terminalStatus),
{ key: "errorCode", label: "Error Code", value: error.value?.code }, summaryRow("errorCode", "Error Code", error.value?.code),
{ key: "errorCategory", label: "Error Category", value: error.value?.category }, summaryRow("errorCategory", "Error Category", error.value?.category),
{ key: "providerStatus", label: "Provider Status", value: error.value?.providerStatus == null ? null : String(error.value.providerStatus) } summaryRow("providerStatus", "Provider Status", error.value?.providerStatus)
].map((row) => ({ ...row, value: row.value == null || row.value === "" ? "未观测" : String(row.value) }))); ].filter((row): row is { key: string; label: string; value: string } => Boolean(row)));
function asAgentRun(value: unknown): AgentRunProvenance | null { function asAgentRun(value: unknown): AgentRunProvenance | null {
return value && typeof value === "object" ? value as AgentRunProvenance : null; return value && typeof value === "object" ? value as AgentRunProvenance : null;
} }
function recordValue(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? value as Record<string, unknown> : null;
}
function summaryRow(key: string, label: string, value: unknown): { key: string; label: string; value: string } | null {
const text = value == null ? "" : String(value).trim();
return text ? { key, label, value: text } : null;
}
function normalizeSummaryError(value: unknown): Record<string, unknown> | null {
if (!value) return null;
if (typeof value === "string") return value.trim() ? { message: value.trim() } : null;
return value && typeof value === "object" ? value as Record<string, unknown> : null;
}
</script> </script>
<template> <template>
@@ -48,7 +67,11 @@ function asAgentRun(value: unknown): AgentRunProvenance | null {
<span>Code Agent 状态摘要</span> <span>Code Agent 状态摘要</span>
<StatusBadge :status="message.status" /> <StatusBadge :status="message.status" />
</summary> </summary>
<dl> <div v-if="error" class="code-agent-error-summary">
<strong>{{ errorTitle }}</strong>
<p v-if="errorMessage">{{ errorMessage }}</p>
</div>
<dl v-if="rows.length > 0">
<div v-for="row in rows" :key="row.key" class="code-agent-status-row" :data-status-row="row.key"> <div v-for="row in rows" :key="row.key" class="code-agent-status-row" :data-status-row="row.key">
<dt>{{ row.label }}</dt> <dt>{{ row.label }}</dt>
<dd>{{ row.value }}</dd> <dd>{{ row.value }}</dd>
@@ -16,6 +16,7 @@ export interface TraceSnapshot {
terminalEvidence?: unknown; terminalEvidence?: unknown;
finalResponse?: unknown; finalResponse?: unknown;
traceSummary?: unknown; traceSummary?: unknown;
error?: AgentChatResultResponse["error"];
lastEventLabel?: string; lastEventLabel?: string;
waitingFor?: string; waitingFor?: string;
eventSource?: string; eventSource?: string;
@@ -62,6 +63,7 @@ export function snapshotToRunnerTrace(snapshot: TraceSnapshot): NonNullable<Chat
finalResponse: snapshot.finalResponse, finalResponse: snapshot.finalResponse,
traceSummary: snapshot.traceSummary, traceSummary: snapshot.traceSummary,
agentRun: snapshot.agentRun, agentRun: snapshot.agentRun,
error: snapshot.error,
eventSource: snapshot.eventSource ?? "trace-api", eventSource: snapshot.eventSource ?? "trace-api",
runnerKind: snapshot.agentRun?.adapter, runnerKind: snapshot.agentRun?.adapter,
sessionMode: snapshot.agentRun?.backendProfile, sessionMode: snapshot.agentRun?.backendProfile,
@@ -108,30 +110,41 @@ function traceEventIdentity(event: TraceEvent): string | null {
export function mergeTraceResults(terminal: AgentChatResultResponse, trace: TraceSnapshot | null): AgentChatResultResponse { export function mergeTraceResults(terminal: AgentChatResultResponse, trace: TraceSnapshot | null): AgentChatResultResponse {
if (!trace) return terminal; if (!trace) return terminal;
const mergedTrace: TraceSnapshot = {
...trace,
sessionId: trace.sessionId ?? terminal.sessionId,
threadId: trace.threadId ?? terminal.threadId,
agentRun: trace.agentRun ?? terminal.agentRun,
terminalEvidence: trace.terminalEvidence ?? terminal.terminalEvidence,
finalResponse: trace.finalResponse ?? terminal.finalResponse,
traceSummary: trace.traceSummary ?? terminal.traceSummary,
error: trace.error ?? terminal.error
};
const events = Array.isArray(trace.events) ? trace.events : []; const events = Array.isArray(trace.events) ? trace.events : [];
const assistantText = bestAssistantTextCandidate([ const assistantText = bestAssistantTextCandidate([
finalResponseText(trace.finalResponse), finalResponseText(mergedTrace.finalResponse),
terminalAssistantTextFromTraceEvents(events), terminalAssistantTextFromTraceEvents(events),
firstNonEmptyResultText(terminal), firstNonEmptyResultText(terminal),
assistantTextFromTraceEvents(events) assistantTextFromTraceEvents(events)
]); ]);
return { return {
...terminal, ...terminal,
traceId: trace.traceId ?? terminal.traceId, traceId: mergedTrace.traceId ?? terminal.traceId,
sessionId: trace.sessionId ?? terminal.sessionId, sessionId: mergedTrace.sessionId ?? terminal.sessionId,
threadId: trace.threadId ?? terminal.threadId, threadId: mergedTrace.threadId ?? terminal.threadId,
agentRun: trace.agentRun ?? terminal.agentRun, agentRun: mergedTrace.agentRun ?? terminal.agentRun,
error: mergedTrace.error ?? terminal.error,
assistantText: assistantText ?? terminal.assistantText, assistantText: assistantText ?? terminal.assistantText,
runnerTrace: snapshotToRunnerTrace(trace), runnerTrace: snapshotToRunnerTrace(mergedTrace),
traceEvents: terminal.traceEvents ?? events, traceEvents: terminal.traceEvents ?? events,
events, events,
eventCount: trace.eventCount ?? events.length, eventCount: mergedTrace.eventCount ?? events.length,
traceStatus: trace.traceStatus ?? terminal.traceStatus, traceStatus: mergedTrace.traceStatus ?? terminal.traceStatus,
retention: trace.retention ?? terminal.retention, retention: mergedTrace.retention ?? terminal.retention,
terminalEvidence: trace.terminalEvidence ?? terminal.terminalEvidence, terminalEvidence: mergedTrace.terminalEvidence ?? terminal.terminalEvidence,
finalResponse: trace.finalResponse ?? terminal.finalResponse, finalResponse: mergedTrace.finalResponse ?? terminal.finalResponse,
traceSummary: trace.traceSummary ?? terminal.traceSummary, traceSummary: mergedTrace.traceSummary ?? terminal.traceSummary,
lastEventLabel: trace.lastEventLabel ?? terminal.lastEventLabel ?? events.at(-1)?.label lastEventLabel: mergedTrace.lastEventLabel ?? terminal.lastEventLabel ?? events.at(-1)?.label
}; };
} }
@@ -227,6 +240,7 @@ function resultToTraceSnapshot(traceId: string, result: AgentChatResultResponse)
terminalEvidence: result.terminalEvidence, terminalEvidence: result.terminalEvidence,
finalResponse: result.finalResponse, finalResponse: result.finalResponse,
traceSummary: result.traceSummary, traceSummary: result.traceSummary,
error: result.error,
lastEventLabel: result.lastEventLabel ?? lastEvent?.label ?? lastEvent?.type, lastEventLabel: result.lastEventLabel ?? lastEvent?.label ?? lastEvent?.type,
eventSource: "trace-api", eventSource: "trace-api",
updatedAt: new Date().toISOString() updatedAt: new Date().toISOString()
+142 -4
View File
@@ -2,7 +2,7 @@ import { computed, ref } from "vue";
import { defineStore } from "pinia"; import { defineStore } from "pinia";
import { api } from "@/api"; import { api } from "@/api";
import { mergeRunnerTrace, snapshotToRunnerTrace, subscribeToTrace, type TraceSnapshot } from "@/composables/useTraceSubscription"; import { mergeRunnerTrace, snapshotToRunnerTrace, subscribeToTrace, type TraceSnapshot } from "@/composables/useTraceSubscription";
import type { AgentChatResponse, AgentChatResultResponse, ApiResult, ChatMessage, ConversationRecord, LiveSurface, ProviderProfile, WorkspaceRecord } from "@/types"; import type { AgentChatResponse, AgentChatResultResponse, AgentRunProvenance, ApiResult, ChatMessage, ConversationRecord, LiveSurface, ProviderProfile, TraceEvent, WorkspaceRecord } from "@/types";
import { DEFAULT_WORKBENCH_PROJECT_ID, firstNonEmptyString, nextProtocolId, rememberWorkbenchProjectId, resolveInitialWorkbenchProjectId, workspaceProjectId } from "@/utils"; import { DEFAULT_WORKBENCH_PROJECT_ID, firstNonEmptyString, nextProtocolId, rememberWorkbenchProjectId, resolveInitialWorkbenchProjectId, workspaceProjectId } from "@/utils";
import { RECENT_DRAFTS_STORAGE_KEY, activeTraceIdFromWorkspace, defaultProviderProfileOptions, normalizeRecentDrafts, providerProfileOptionsFromPayload, recordRecentDraft, resolveComposerState, sortSessionTabs, workspaceWithClearedActiveTrace, type DraftEntry, type ProviderProfileOption } from "./workbench-session"; import { RECENT_DRAFTS_STORAGE_KEY, activeTraceIdFromWorkspace, defaultProviderProfileOptions, normalizeRecentDrafts, providerProfileOptionsFromPayload, recordRecentDraft, resolveComposerState, sortSessionTabs, workspaceWithClearedActiveTrace, type DraftEntry, type ProviderProfileOption } from "./workbench-session";
@@ -55,6 +55,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
rememberWorkbenchProjectId(workspaceProjectId(workspace.value, projectId.value)); rememberWorkbenchProjectId(workspaceProjectId(workspace.value, projectId.value));
conversations.value = conversationsResult.ok ? conversationsResult.data?.conversations ?? [] : []; conversations.value = conversationsResult.ok ? conversationsResult.data?.conversations ?? [] : [];
messages.value = messagesFromWorkspace(workspace.value); messages.value = messagesFromWorkspace(workspace.value);
void hydrateTerminalMessageDiagnostics();
await refreshProviderOptions(); await refreshProviderOptions();
const activeTraceId = activeTraceIdFromWorkspace(workspace.value); const activeTraceId = activeTraceIdFromWorkspace(workspace.value);
if (activeTraceId) void validateAndReattachTrace(activeTraceId); if (activeTraceId) void validateAndReattachTrace(activeTraceId);
@@ -103,6 +104,7 @@ export const useWorkbenchStore = defineStore("workbench", () => {
if (response.ok) { if (response.ok) {
workspace.value = response.data?.workspace ?? current; workspace.value = response.data?.workspace ?? current;
messages.value = messagesFromWorkspace(workspace.value); messages.value = messagesFromWorkspace(workspace.value);
void hydrateTerminalMessageDiagnostics();
currentRequest.value = null; currentRequest.value = null;
} else { } else {
workspace.value = previousWorkspace; workspace.value = previousWorkspace;
@@ -288,14 +290,37 @@ export const useWorkbenchStore = defineStore("workbench", () => {
const text = firstNonEmptyString(result.assistantText, finalResponseText(result.finalResponse), typeof result.reply === "string" ? result.reply : result.reply?.content, result.text, result.summary) ?? "Code Agent 已完成,但没有返回可展示的 final response。"; const text = firstNonEmptyString(result.assistantText, finalResponseText(result.finalResponse), typeof result.reply === "string" ? result.reply : result.reply?.content, result.text, result.summary) ?? "Code Agent 已完成,但没有返回可展示的 final response。";
messages.value = messages.value.map((message) => { messages.value = messages.value.map((message) => {
if (message.traceId !== traceId || message.role !== "agent") return message; if (message.traceId !== traceId || message.role !== "agent") return message;
const runnerTrace = result.runnerTrace ? mergeRunnerTrace(message.runnerTrace, result.runnerTrace) : message.runnerTrace ?? null; const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result);
return { ...message, status: result.status === "completed" ? "completed" : statusFromResult(result.status), text, runnerTrace, updatedAt: new Date().toISOString() }; const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
return { ...message, status: result.status === "completed" ? "completed" : statusFromResult(result.status), text, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
}); });
chatPending.value = false; chatPending.value = false;
currentRequest.value = null; currentRequest.value = null;
void clearActiveTrace(traceId, "trace-terminal"); void clearActiveTrace(traceId, "trace-terminal");
} }
async function hydrateTerminalMessageDiagnostics(): Promise<void> {
const targets = messages.value.filter(messageNeedsTerminalDiagnostics).slice(-6);
await Promise.all(targets.map(async (message) => {
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
if (!traceId) return;
const result = await api.agent.getAgentChatResult(`/v1/agent/chat/result/${encodeURIComponent(traceId)}?projectId=${encodeURIComponent(activeProjectId.value)}`, 8000, () => activityRef.value);
if (!result.ok || !result.data) return;
applyTerminalResultDiagnostics(traceId, result.data);
}));
}
function applyTerminalResultDiagnostics(traceId: string, result: AgentChatResultResponse): void {
messages.value = messages.value.map((message) => {
if (message.traceId !== traceId || message.role !== "agent") return message;
const runnerTrace = mergeTerminalResultTrace(message.runnerTrace, result);
const error = normalizeAgentError(result.error ?? runnerTrace?.error ?? message.error);
const agentRun = agentRunFromResult(result, runnerTrace) ?? agentRunFromMessage(message);
return { ...message, runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined, updatedAt: new Date().toISOString() };
});
}
function failTrace(traceId: string, message: string): void { function failTrace(traceId: string, message: string): void {
markMessage(traceId, { status: "failed", text: message }); markMessage(traceId, { status: "failed", text: message });
chatPending.value = false; chatPending.value = false;
@@ -364,7 +389,120 @@ function messagesFromWorkspace(workspace: WorkspaceRecord | null): ChatMessage[]
function normalizeChatMessage(message: ChatMessage): ChatMessage { function normalizeChatMessage(message: ChatMessage): ChatMessage {
const text = firstNonEmptyString(message.text, messageText((message as Record<string, unknown>).content), messageText((message as Record<string, unknown>).message), finalResponseText((message as Record<string, unknown>).finalResponse)) ?? ""; const text = firstNonEmptyString(message.text, messageText((message as Record<string, unknown>).content), messageText((message as Record<string, unknown>).message), finalResponseText((message as Record<string, unknown>).finalResponse)) ?? "";
return { ...message, text, id: message.id ?? nextProtocolId("msg"), title: message.title ?? (message.role === "user" ? "用户" : "Code Agent"), createdAt: message.createdAt ?? new Date().toISOString(), status: message.status ?? "source" }; const runnerTrace = normalizeMessageRunnerTrace(message);
const error = normalizeAgentError(message.error ?? runnerTrace?.error);
const agentRun = agentRunFromMessage(message) ?? asAgentRun(runnerTrace?.agentRun);
return { ...message, text, id: message.id ?? nextProtocolId("msg"), title: message.title ?? (message.role === "user" ? "用户" : "Code Agent"), createdAt: message.createdAt ?? new Date().toISOString(), status: message.status ?? "source", runnerTrace, error: error ?? message.error ?? null, agentRun: agentRun ?? undefined };
}
function mergeTerminalResultTrace(previous: ChatMessage["runnerTrace"], result: AgentChatResultResponse): NonNullable<ChatMessage["runnerTrace"]> {
const resultTrace = recordValue(result.runnerTrace);
const events = firstArray(result.events, result.traceEvents, resultTrace?.events, previous?.events);
const agentRun = asAgentRun(result.agentRun ?? resultTrace?.agentRun ?? previous?.agentRun);
return mergeRunnerTrace(previous, {
...resultTrace,
traceId: firstNonEmptyString(result.traceId, resultTrace?.traceId, previous?.traceId) ?? undefined,
status: firstNonEmptyString(result.traceStatus, resultTrace?.status, result.status, previous?.status) ?? undefined,
sessionId: firstNonEmptyString(result.sessionId, resultTrace?.sessionId, previous?.sessionId) ?? undefined,
threadId: firstNonEmptyString(result.threadId, resultTrace?.threadId, previous?.threadId) ?? undefined,
events,
eventCount: firstFiniteNumber(result.eventCount, resultTrace?.eventCount, previous?.eventCount, events.length),
eventsCompacted: firstBoolean(resultTrace?.eventsCompacted, previous?.eventsCompacted),
traceStatus: firstNonEmptyString(result.traceStatus, resultTrace?.traceStatus, previous?.traceStatus) ?? undefined,
retention: result.retention ?? resultTrace?.retention ?? previous?.retention,
terminalEvidence: result.terminalEvidence ?? resultTrace?.terminalEvidence ?? previous?.terminalEvidence,
finalResponse: result.finalResponse ?? resultTrace?.finalResponse ?? previous?.finalResponse,
traceSummary: result.traceSummary ?? resultTrace?.traceSummary ?? previous?.traceSummary,
agentRun: agentRun ?? undefined,
error: normalizeAgentError(result.error ?? resultTrace?.error ?? previous?.error) ?? undefined,
runnerKind: firstNonEmptyString(agentRun?.adapter, resultTrace?.runnerKind, previous?.runnerKind) ?? undefined,
sessionMode: firstNonEmptyString(agentRun?.backendProfile, resultTrace?.sessionMode, previous?.sessionMode) ?? undefined,
lastEventLabel: firstNonEmptyString(result.lastEventLabel, resultTrace?.lastEventLabel, previous?.lastEventLabel, events.at(-1)?.label, events.at(-1)?.type) ?? undefined,
updatedAt: new Date().toISOString()
} as NonNullable<ChatMessage["runnerTrace"]>);
}
function normalizeMessageRunnerTrace(message: ChatMessage): ChatMessage["runnerTrace"] {
const trace = recordValue(message.runnerTrace);
const agentRun = agentRunFromMessage(message);
const error = normalizeAgentError(message.error ?? trace?.error);
if (!trace && !agentRun && !error) return message.runnerTrace ?? null;
return {
...(trace ?? {}),
traceId: message.traceId ?? trace?.traceId,
sessionId: message.sessionId ?? trace?.sessionId,
threadId: message.threadId ?? trace?.threadId,
agentRun: agentRun ?? trace?.agentRun,
error: error ?? trace?.error,
runnerKind: agentRun?.adapter ?? trace?.runnerKind,
sessionMode: agentRun?.backendProfile ?? trace?.sessionMode
} as ChatMessage["runnerTrace"];
}
function messageNeedsTerminalDiagnostics(message: ChatMessage): boolean {
if (message.role !== "agent") return false;
if (!isTerminalMessageStatus(message.status)) return false;
const traceId = firstNonEmptyString(message.traceId, message.runnerTrace?.traceId);
if (!traceId) return false;
const agentRun = agentRunFromMessage(message);
const error = normalizeAgentError(message.error ?? message.runnerTrace?.error);
return !agentRun || (message.status !== "completed" && !error);
}
function agentRunFromResult(result: AgentChatResultResponse, runnerTrace: ChatMessage["runnerTrace"]): AgentRunProvenance | null {
return asAgentRun(result.agentRun ?? runnerTrace?.agentRun);
}
function agentRunFromMessage(message: ChatMessage): AgentRunProvenance | null {
return asAgentRun((message as Record<string, unknown>).agentRun ?? message.runnerTrace?.agentRun);
}
function asAgentRun(value: unknown): AgentRunProvenance | null {
return value && typeof value === "object" ? value as AgentRunProvenance : null;
}
function normalizeAgentError(value: unknown): ChatMessage["error"] | null {
if (!value) return null;
if (typeof value === "string") return value.trim() ? { message: value.trim() } : null;
if (value && typeof value === "object") {
const record = value as Record<string, unknown>;
const message = firstNonEmptyString(record.message, record.userMessage, record.reason, record.detail, record.error);
const code = firstNonEmptyString(record.code, record.failureKind, record.name);
const category = firstNonEmptyString(record.category, record.layer, record.type);
const providerStatus = typeof record.providerStatus === "number" ? record.providerStatus : Number.isFinite(Number(record.providerStatus)) ? Number(record.providerStatus) : undefined;
return { ...record, code: code ?? undefined, message: message ?? undefined, category: category ?? undefined, providerStatus };
}
return { message: String(value) };
}
function isTerminalMessageStatus(value: unknown): boolean {
return ["completed", "failed", "blocked", "timeout", "canceled", "cancelled", "stale", "thread-resume-failed"].includes(String(value ?? "").trim().toLowerCase().replace(/_/gu, "-"));
}
function recordValue(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? value as Record<string, unknown> : null;
}
function firstArray(...values: unknown[]): TraceEvent[] {
for (const value of values) {
if (Array.isArray(value)) return value as TraceEvent[];
}
return [];
}
function firstFiniteNumber(...values: unknown[]): number | undefined {
for (const value of values) {
const number = typeof value === "number" ? value : Number(value);
if (Number.isFinite(number)) return number;
}
return undefined;
}
function firstBoolean(...values: unknown[]): boolean | undefined {
for (const value of values) {
if (typeof value === "boolean") return value;
}
return undefined;
} }
function finalResponseText(value: unknown): string | null { function finalResponseText(value: unknown): string | null {
@@ -1230,6 +1230,28 @@
border-top: 1px solid #d8e1eb; border-top: 1px solid #d8e1eb;
} }
.code-agent-error-summary {
display: grid;
gap: 4px;
border-top: 1px solid #d8e1eb;
background: #fff7ed;
padding: 9px 10px;
color: #7c2d12;
}
.code-agent-error-summary strong {
font-size: 12px;
font-weight: 800;
}
.code-agent-error-summary p {
margin: 0;
overflow-wrap: anywhere;
color: #7f1d1d;
font-size: 12px;
line-height: 1.45;
}
.code-agent-status-row { .code-agent-status-row {
display: grid; display: grid;
grid-template-columns: minmax(110px, 0.8fr) minmax(0, 1.2fr); grid-template-columns: minmax(110px, 0.8fr) minmax(0, 1.2fr);