diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index f030d092..44ad4cb2 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -55,6 +55,7 @@ import { FileCardProvider } from '@/contexts/file-card-context' import { MarkdownPreOverride } from '@/components/ai-elements/markdown-code-override' import { AgentScheduleConfig } from '@x/shared/dist/agent-schedule.js' import { AgentScheduleState } from '@x/shared/dist/agent-schedule-state.js' +import { toast } from "sonner" type DirEntry = z.infer type RunEventType = z.infer @@ -81,12 +82,20 @@ interface ToolCall { timestamp: number; } -type ConversationItem = ChatMessage | ToolCall; +interface ErrorMessage { + id: string; + kind: 'error'; + message: string; + timestamp: number; +} + +type ConversationItem = ChatMessage | ToolCall | ErrorMessage; type ToolState = 'input-streaming' | 'input-available' | 'output-available' | 'output-error'; const isChatMessage = (item: ConversationItem): item is ChatMessage => 'role' in item const isToolCall = (item: ConversationItem): item is ToolCall => 'name' in item +const isErrorMessage = (item: ConversationItem): item is ErrorMessage => 'kind' in item && item.kind === 'error' const toToolState = (status: ToolCall['status']): ToolState => { switch (status) { @@ -1102,6 +1111,15 @@ function App() { } break } + case 'error': { + items.push({ + id: `error-${Date.now()}-${Math.random()}`, + kind: 'error', + message: event.error, + timestamp: event.ts ? new Date(event.ts).getTime() : Date.now(), + }) + break + } case 'llm-stream-event': { // We don't need to reconstruct streaming events for history // Reasoning is captured in the final message @@ -1439,6 +1457,13 @@ function App() { setIsProcessing(false) setIsStopping(false) setStopClickedAt(null) + setConversation(prev => [...prev, { + id: `error-${Date.now()}`, + kind: 'error', + message: event.error, + timestamp: Date.now(), + }]) + toast.error(event.error.split('\n')[0] || 'Model error') console.error('Run error:', event.error) break } @@ -2226,6 +2251,16 @@ function App() { ) } + if (isErrorMessage(item)) { + return ( + + +
{item.message}
+
+
+ ) + } + return null } diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index ab04c886..8d2a005f 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -52,12 +52,20 @@ interface ToolCall { timestamp: number } -type ConversationItem = ChatMessage | ToolCall +interface ErrorMessage { + id: string + kind: 'error' + message: string + timestamp: number +} + +type ConversationItem = ChatMessage | ToolCall | ErrorMessage type ToolState = 'input-streaming' | 'input-available' | 'output-available' | 'output-error' const isChatMessage = (item: ConversationItem): item is ChatMessage => 'role' in item const isToolCall = (item: ConversationItem): item is ToolCall => 'name' in item +const isErrorMessage = (item: ConversationItem): item is ErrorMessage => 'kind' in item && item.kind === 'error' const toToolState = (status: ToolCall['status']): ToolState => { switch (status) { @@ -417,6 +425,16 @@ export function ChatSidebar({ ) } + if (isErrorMessage(item)) { + return ( + + +
{item.message}
+
+
+ ) + } + return null } diff --git a/apps/x/packages/core/src/agents/runtime.ts b/apps/x/packages/core/src/agents/runtime.ts index ce34d732..09d1c721 100644 --- a/apps/x/packages/core/src/agents/runtime.ts +++ b/apps/x/packages/core/src/agents/runtime.ts @@ -265,6 +265,9 @@ export class StreamStepMessageBuilder { case "finish-step": this.providerOptions = event.providerOptions; break; + case "error": + this.flushBuffers(); + break; } } @@ -278,6 +281,30 @@ export class StreamStepMessageBuilder { } } +function formatLlmStreamError(rawError: unknown): string { + let name: string | undefined; + let responseBody: string | undefined; + if (rawError && typeof rawError === "object") { + const err = rawError as Record; + const nested = (err.error && typeof err.error === "object") ? err.error as Record : null; + const nameValue = err.name ?? nested?.name; + const responseBodyValue = err.responseBody ?? nested?.responseBody; + if (nameValue !== undefined) { + name = String(nameValue); + } + if (responseBodyValue !== undefined) { + responseBody = String(responseBodyValue); + } + } else if (typeof rawError === "string") { + responseBody = rawError; + } + + const lines: string[] = []; + if (name) lines.push(`name: ${name}`); + if (responseBody) lines.push(`responseBody: ${responseBody}`); + return lines.length ? lines.join("\n") : "Model stream error"; +} + export async function loadAgent(id: string): Promise> { if (id === "copilot" || id === "rowboatx") { return CopilotAgent; @@ -792,6 +819,7 @@ export async function* streamAgent({ timeZoneName: 'short' }); const instructionsWithDateTime = `Current date and time: ${currentDateTime}\n\n${agent.instructions}`; + let streamError: string | null = null; for await (const event of streamLlm( model, state.messages, @@ -810,6 +838,16 @@ export async function* streamAgent({ event: event, subflow: [], }); + if (event.type === "error") { + streamError = event.error; + yield* processEvent({ + runId, + type: "error", + error: streamError, + subflow: [], + }); + break; + } } // build and emit final message from agent response @@ -822,6 +860,10 @@ export async function* streamAgent({ subflow: [], }); + if (streamError) { + return; + } + // if there were any ask-human calls, emit those events if (message.content instanceof Array) { for (const part of message.content) { @@ -895,6 +937,12 @@ async function* streamLlm( signal?.throwIfAborted(); // console.log("\n\n\t>>>>\t\tstream event", JSON.stringify(event)); switch (event.type) { + case "error": + yield { + type: "error", + error: formatLlmStreamError((event as { error?: unknown }).error ?? event), + }; + return; case "reasoning-start": yield { type: "reasoning-start", @@ -945,7 +993,7 @@ async function* streamLlm( }; break; default: - // console.warn("Unknown event type", event); + console.log('unknown stream event:', JSON.stringify(event)); continue; } } diff --git a/apps/x/packages/shared/src/llm-step-events.ts b/apps/x/packages/shared/src/llm-step-events.ts index b9c428cf..8f9ff12c 100644 --- a/apps/x/packages/shared/src/llm-step-events.ts +++ b/apps/x/packages/shared/src/llm-step-events.ts @@ -51,6 +51,11 @@ export const LlmStepStreamFinishStepEvent = z.object({ providerOptions: ProviderOptions.optional(), }); +export const LlmStepStreamErrorEvent = BaseEvent.extend({ + type: z.literal("error"), + error: z.string(), +}); + export const LlmStepStreamEvent = z.union([ LlmStepStreamReasoningStartEvent, LlmStepStreamReasoningDeltaEvent, @@ -60,4 +65,5 @@ export const LlmStepStreamEvent = z.union([ LlmStepStreamTextEndEvent, LlmStepStreamToolCallEvent, LlmStepStreamFinishStepEvent, -]); \ No newline at end of file + LlmStepStreamErrorEvent, +]);