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. diff --git a/packages/agents-runtime/src/process-wake.ts b/packages/agents-runtime/src/process-wake.ts index c419a3114a..f9d94ee5b3 100644 --- a/packages/agents-runtime/src/process-wake.ts +++ b/packages/agents-runtime/src/process-wake.ts @@ -146,6 +146,15 @@ function toError(err: unknown): Error { return err instanceof Error ? err : new Error(String(err)) } +function latestNewRunKey( + db: EntityStreamDBWithActions, + existingRunKeys: ReadonlySet +): string | undefined { + return db.collections.runs.toArray + .filter((run) => !existingRunKeys.has(run.key)) + .at(-1)?.key +} + async function resolveHeadersProvider( provider: ProcessWakeConfig[`claimHeaders`] ): Promise | undefined> { @@ -2158,6 +2167,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 +2220,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.tsx b/packages/agents-server-ui/src/components/AgentResponse.tsx index 673e9cb496..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,6 +336,41 @@ function errorText(errors: Array): string | undefined { return errors.length > 0 ? errors.map(formatError).join(`; `) : undefined } +const RUN_ERROR_SUMMARY_LENGTH = 180 + +function isLongRunError(message: string): boolean { + return message.length > RUN_ERROR_SUMMARY_LENGTH || message.includes(`\n`) +} + +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 ( + + ✗ {message} + + ) +} + +function RunErrorCard({ message }: { message: string }): React.ReactElement { + return ( + +
{message}
+
+ ) +} + function failedRunText( run: EntityTimelineRunRow, items: Array @@ -521,6 +558,10 @@ export const AgentResponseLive = memo(function AgentResponseLive({ ) })} + {failureText && isLongRunError(failureText) && ( + + )} + {showThinking && } {done && ( @@ -530,10 +571,8 @@ export const AgentResponseLive = memo(function AgentResponseLive({ : `✓ done`} )} - {failureText && ( - - ✗ {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 @@ -739,6 +778,10 @@ export const AgentResponse = memo(function AgentResponse({ return })} + {section.error && isLongRunError(section.error) && ( + + )} + {showThinking && } {section.done && ( @@ -748,10 +791,8 @@ export const AgentResponse = memo(function AgentResponse({ : `✓ done`} )} - {section.error && ( - - ✗ {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