From 7c481b09b294a2a61dcac42f8313065dc51dfb92 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 11 Jun 2026 08:45:56 -0600 Subject: [PATCH 1/4] Fix persisted run error rendering --- packages/agents-runtime/src/process-wake.ts | 34 +++++++++++++++ .../src/components/AgentResponse.module.css | 36 ++++++++++++++++ .../src/components/AgentResponse.tsx | 41 ++++++++++++++----- 3 files changed, 101 insertions(+), 10 deletions(-) diff --git a/packages/agents-runtime/src/process-wake.ts b/packages/agents-runtime/src/process-wake.ts index c419a3114a..a5b193ff9b 100644 --- a/packages/agents-runtime/src/process-wake.ts +++ b/packages/agents-runtime/src/process-wake.ts @@ -146,6 +146,35 @@ function toError(err: unknown): Error { return err instanceof Error ? err : new Error(String(err)) } +function compareRunOrder( + left: { key: string; _timeline_order?: string; _seq?: number }, + right: { key: string; _timeline_order?: string; _seq?: number } +): number { + if (left._timeline_order && right._timeline_order) { + return left._timeline_order.localeCompare(right._timeline_order) + } + if (left._timeline_order) return 1 + if (right._timeline_order) return -1 + + if (left._seq !== undefined && right._seq !== undefined) { + return left._seq - right._seq + } + if (left._seq !== undefined) return 1 + if (right._seq !== undefined) return -1 + + return left.key.localeCompare(right.key) +} + +function latestNewRunKey( + db: EntityStreamDBWithActions, + existingRunKeys: ReadonlySet +): string | undefined { + return db.collections.runs.toArray + .filter((run) => !existingRunKeys.has(run.key)) + .sort(compareRunOrder) + .at(-1)?.key +} + async function resolveHeadersProvider( provider: ProcessWakeConfig[`claimHeaders`] ): Promise | undefined> { @@ -2158,6 +2187,9 @@ export async function processWake( }) let sleepRequested = false + const existingRunKeys = new Set( + db.collections.runs.toArray.map((run) => run.key) + ) try { await wirePendingSharedStates() @@ -2208,12 +2240,14 @@ export async function processWake( ? setupErr.code : `HANDLER_FAILED` log.error(`handler failed for ${entityUrl}:`, errMsg) + const failedRunKey = latestNewRunKey(db, existingRunKeys) writeEvent( entityStateSchema.errors.insert({ key: `error-${epoch}-${crypto.randomUUID()}`, value: { error_code: errCode, message: errMsg, + ...(failedRunKey ? { run_id: failedRunKey } : {}), } as never, }) as ChangeEvent ) diff --git a/packages/agents-server-ui/src/components/AgentResponse.module.css b/packages/agents-server-ui/src/components/AgentResponse.module.css index 36141e7b86..e80c0ec2cf 100644 --- a/packages/agents-server-ui/src/components/AgentResponse.module.css +++ b/packages/agents-server-ui/src/components/AgentResponse.module.css @@ -48,3 +48,39 @@ .metaActionButton:hover { opacity: 1; } + +.errorText { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.errorDetails { + min-width: 0; + max-width: 100%; +} + +.errorDetails summary { + cursor: pointer; + list-style-position: outside; + padding-left: 2px; +} + +.errorSummaryText { + display: inline; +} + +.errorPre { + margin: 8px 0 0; + max-width: 100%; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + color: var(--ds-red-11); + background: var(--ds-red-2); + border: 1px solid var(--ds-red-6); + border-radius: 8px; + padding: 8px 10px; + font: 12px/1.45 var(--ds-font-mono); +} diff --git a/packages/agents-server-ui/src/components/AgentResponse.tsx b/packages/agents-server-ui/src/components/AgentResponse.tsx index 673e9cb496..8a49681df7 100644 --- a/packages/agents-server-ui/src/components/AgentResponse.tsx +++ b/packages/agents-server-ui/src/components/AgentResponse.tsx @@ -334,6 +334,35 @@ function errorText(errors: Array): string | undefined { return errors.length > 0 ? errors.map(formatError).join(`; `) : undefined } +function RunErrorMessage({ message }: { message: string }): React.ReactElement { + const maxSummaryLength = 180 + const isLong = message.length > maxSummaryLength || message.includes(`\n`) + if (!isLong) { + return ( + + ✗ {message} + + ) + } + + const normalizedSummary = message.replace(/\s+/g, ` `) + const isTruncated = normalizedSummary.length > maxSummaryLength + const summary = isTruncated + ? normalizedSummary.slice(0, maxSummaryLength) + : normalizedSummary + return ( +
+ + + ✗ {summary} + {isTruncated ? `…` : ``} + + +
{message}
+
+ ) +} + function failedRunText( run: EntityTimelineRunRow, items: Array @@ -530,11 +559,7 @@ export const AgentResponseLive = memo(function AgentResponseLive({ : `✓ done`} )} - {failureText && ( - - ✗ {failureText} - - )} + {failureText && } {/* Elapsed-time ticker — visible while the response is still in flight so the user can see how long the model has been working ("Thinking · 12s", or just "12s" once tokens are @@ -748,11 +773,7 @@ export const AgentResponse = memo(function AgentResponse({ : `✓ done`} )} - {section.error && ( - - ✗ {section.error} - - )} + {section.error && } {/* Elapsed-time ticker — kept in sync with the live variant above so cached sections (rare during streaming, but the type permits it) render the same meta row. */} From 8e6120c7383e5f607631e372e719f4ed3b5ee381 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 11 Jun 2026 08:48:03 -0600 Subject: [PATCH 2/4] Add run error rendering changeset --- .changeset/run-error-rendering.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/run-error-rendering.md diff --git a/.changeset/run-error-rendering.md b/.changeset/run-error-rendering.md new file mode 100644 index 0000000000..a70db787cb --- /dev/null +++ b/.changeset/run-error-rendering.md @@ -0,0 +1,6 @@ +--- +'@electric-ax/agents-runtime': patch +'@electric-ax/agents-server-ui': patch +--- + +Persist handler errors on the run that produced them and render long run error messages in the agents UI as expandable details. From b20a74414eb4f1b09e17c030f898e011dff940e7 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 11 Jun 2026 08:53:57 -0600 Subject: [PATCH 3/4] Simplify failed run selection --- packages/agents-runtime/src/process-wake.ts | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/packages/agents-runtime/src/process-wake.ts b/packages/agents-runtime/src/process-wake.ts index a5b193ff9b..f9d94ee5b3 100644 --- a/packages/agents-runtime/src/process-wake.ts +++ b/packages/agents-runtime/src/process-wake.ts @@ -146,32 +146,12 @@ function toError(err: unknown): Error { return err instanceof Error ? err : new Error(String(err)) } -function compareRunOrder( - left: { key: string; _timeline_order?: string; _seq?: number }, - right: { key: string; _timeline_order?: string; _seq?: number } -): number { - if (left._timeline_order && right._timeline_order) { - return left._timeline_order.localeCompare(right._timeline_order) - } - if (left._timeline_order) return 1 - if (right._timeline_order) return -1 - - if (left._seq !== undefined && right._seq !== undefined) { - return left._seq - right._seq - } - if (left._seq !== undefined) return 1 - if (right._seq !== undefined) return -1 - - return left.key.localeCompare(right.key) -} - function latestNewRunKey( db: EntityStreamDBWithActions, existingRunKeys: ReadonlySet ): string | undefined { return db.collections.runs.toArray .filter((run) => !existingRunKeys.has(run.key)) - .sort(compareRunOrder) .at(-1)?.key } From 65f6dcf651ec57cba7b4459fc482d1f2fc227eb9 Mon Sep 17 00:00:00 2001 From: Kyle Mathews Date: Thu, 11 Jun 2026 09:01:14 -0600 Subject: [PATCH 4/4] Use event card for long run errors --- .../src/components/AgentResponse.module.css | 36 --------- .../src/components/AgentResponse.tsx | 74 ++++++++++++------- 2 files changed, 47 insertions(+), 63 deletions(-) diff --git a/packages/agents-server-ui/src/components/AgentResponse.module.css b/packages/agents-server-ui/src/components/AgentResponse.module.css index e80c0ec2cf..36141e7b86 100644 --- a/packages/agents-server-ui/src/components/AgentResponse.module.css +++ b/packages/agents-server-ui/src/components/AgentResponse.module.css @@ -48,39 +48,3 @@ .metaActionButton:hover { opacity: 1; } - -.errorText { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.errorDetails { - min-width: 0; - max-width: 100%; -} - -.errorDetails summary { - cursor: pointer; - list-style-position: outside; - padding-left: 2px; -} - -.errorSummaryText { - display: inline; -} - -.errorPre { - margin: 8px 0 0; - max-width: 100%; - overflow: auto; - white-space: pre-wrap; - word-break: break-word; - color: var(--ds-red-11); - background: var(--ds-red-2); - border: 1px solid var(--ds-red-6); - border-radius: 8px; - padding: 8px 10px; - font: 12px/1.45 var(--ds-font-mono); -} diff --git a/packages/agents-server-ui/src/components/AgentResponse.tsx b/packages/agents-server-ui/src/components/AgentResponse.tsx index 8a49681df7..373d8ea64a 100644 --- a/packages/agents-server-ui/src/components/AgentResponse.tsx +++ b/packages/agents-server-ui/src/components/AgentResponse.tsx @@ -1,4 +1,4 @@ -import { Check, Copy, GitFork } from 'lucide-react' +import { Check, Copy, CircleAlert, GitFork } from 'lucide-react' import { memo, useEffect, @@ -23,12 +23,14 @@ import { } from '../lib/streamdownConfig' import { Icon, IconButton, Stack, Text, Tooltip } from '../ui' import { ToolCallView } from './ToolCallView' +import { InlineEventCard } from './InlineEventCard' import { TimeText } from './TimeText' import { ThinkingIndicator } from './ThinkingIndicator' import { ElapsedTime } from './ElapsedTime' import { TokenUsage } from './TokenUsage' import { formatElapsedDuration, toMillis } from '../lib/formatTime' import styles from './AgentResponse.module.css' +import toolBlock from './toolBlock.module.css' import type { ForkFromHereAction } from './UserMessage' import type { EntityTimelineContentItem, @@ -334,32 +336,38 @@ function errorText(errors: Array): string | undefined { return errors.length > 0 ? errors.map(formatError).join(`; `) : undefined } -function RunErrorMessage({ message }: { message: string }): React.ReactElement { - const maxSummaryLength = 180 - const isLong = message.length > maxSummaryLength || message.includes(`\n`) - if (!isLong) { - return ( - - ✗ {message} - - ) - } +const RUN_ERROR_SUMMARY_LENGTH = 180 + +function isLongRunError(message: string): boolean { + return message.length > RUN_ERROR_SUMMARY_LENGTH || message.includes(`\n`) +} - const normalizedSummary = message.replace(/\s+/g, ` `) - const isTruncated = normalizedSummary.length > maxSummaryLength - const summary = isTruncated - ? normalizedSummary.slice(0, maxSummaryLength) - : normalizedSummary +function runErrorSummary(message: string): string { + const normalized = message.replace(/\s+/g, ` `) + return normalized.length > RUN_ERROR_SUMMARY_LENGTH + ? `${normalized.slice(0, RUN_ERROR_SUMMARY_LENGTH)}…` + : normalized +} + +function RunErrorInline({ message }: { message: string }): React.ReactElement { return ( -
- - - ✗ {summary} - {isTruncated ? `…` : ``} - - -
{message}
-
+ + ✗ {message} + + ) +} + +function RunErrorCard({ message }: { message: string }): React.ReactElement { + return ( + +
{message}
+
) } @@ -550,6 +558,10 @@ export const AgentResponseLive = memo(function AgentResponseLive({ ) })} + {failureText && isLongRunError(failureText) && ( + + )} + {showThinking && } {done && ( @@ -559,7 +571,9 @@ export const AgentResponseLive = memo(function AgentResponseLive({ : `✓ done`} )} - {failureText && } + {failureText && !isLongRunError(failureText) && ( + + )} {/* Elapsed-time ticker — visible while the response is still in flight so the user can see how long the model has been working ("Thinking · 12s", or just "12s" once tokens are @@ -764,6 +778,10 @@ export const AgentResponse = memo(function AgentResponse({ return })} + {section.error && isLongRunError(section.error) && ( + + )} + {showThinking && } {section.done && ( @@ -773,7 +791,9 @@ export const AgentResponse = memo(function AgentResponse({ : `✓ done`} )} - {section.error && } + {section.error && !isLongRunError(section.error) && ( + + )} {/* Elapsed-time ticker — kept in sync with the live variant above so cached sections (rare during streaming, but the type permits it) render the same meta row. */}