diff --git a/src/cli/App.tsx b/src/cli/App.tsx index b69ea00..7ce533f 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -985,7 +985,7 @@ export default function App({ approvals, apiDurationMs, lastRunId, - streamError, + fallbackError, } = await drainStreamWithResume( stream, buffersRef.current, @@ -1411,8 +1411,10 @@ export default function App({ // Track the error in telemetry telemetry.trackError( - streamError ? "StreamError" : stopReason || "unknown_stop_reason", - streamError || `Stream stopped with reason: ${stopReason}`, + fallbackError + ? "FallbackError" + : stopReason || "unknown_stop_reason", + fallbackError || `Stream stopped with reason: ${stopReason}`, "message_stream", { modelId: currentModelId || undefined, @@ -1421,10 +1423,11 @@ export default function App({ ); // If we have a client-side stream error (e.g., JSON parse error), show it directly - if (streamError) { + // Fallback error: no run_id available, show whatever error message we have + if (fallbackError) { const errorMsg = lastRunId - ? `Stream error: ${streamError}\n(run_id: ${lastRunId})` - : `Stream error: ${streamError}`; + ? `Stream error: ${fallbackError}\n(run_id: ${lastRunId})` + : `Stream error: ${fallbackError}`; appendError(errorMsg, true); // Skip telemetry - already tracked above setStreaming(false); refreshDerived(); diff --git a/src/cli/helpers/stream.ts b/src/cli/helpers/stream.ts index 9f5504a..53d07fa 100644 --- a/src/cli/helpers/stream.ts +++ b/src/cli/helpers/stream.ts @@ -1,3 +1,4 @@ +import { APIError } from "@letta-ai/letta-client/core/error"; import type { Stream } from "@letta-ai/letta-client/core/streaming"; import type { LettaStreamingResponse } from "@letta-ai/letta-client/resources/agents/messages"; import type { StopReasonType } from "@letta-ai/letta-client/resources/runs/runs"; @@ -24,7 +25,7 @@ type DrainResult = { approval?: ApprovalRequest | null; // DEPRECATED: kept for backward compat approvals?: ApprovalRequest[]; // NEW: supports parallel approvals apiDurationMs: number; // time spent in API call - streamError?: string | null; // Client-side error message (e.g., JSON parse error) + fallbackError?: string | null; // Error message for when we can't fetch details from server (no run_id) }; export async function drainStream( @@ -50,7 +51,7 @@ export async function drainStream( let lastRunId: string | null = null; let lastSeqId: number | null = null; let hasCalledFirstMessage = false; - let streamError: string | null = null; + let fallbackError: string | null = null; try { for await (const chunk of stream) { @@ -63,14 +64,12 @@ export async function drainStream( queueMicrotask(refresh); break; } - // Store the run_id and seq_id to re-connect if stream is interrupted - if ( - "run_id" in chunk && - "seq_id" in chunk && - chunk.run_id && - chunk.seq_id - ) { + // Store the run_id (for error reporting) and seq_id (for stream resumption) + // Capture run_id even if seq_id is missing - we need it for error details + if ("run_id" in chunk && chunk.run_id) { lastRunId = chunk.run_id; + } + if ("seq_id" in chunk && chunk.seq_id) { lastSeqId = chunk.seq_id; } @@ -166,8 +165,20 @@ export async function drainStream( const errorMessage = e instanceof Error ? e.message : String(e); debugWarn("drainStream", "Stream error caught:", errorMessage); - // Capture the error message for display - streamError = errorMessage; + // Try to extract run_id from APIError if we don't have one yet + if (!lastRunId && e instanceof APIError && e.error) { + const errorObj = e.error as Record; + if ("run_id" in errorObj && typeof errorObj.run_id === "string") { + lastRunId = errorObj.run_id; + debugWarn("drainStream", "Extracted run_id from error:", lastRunId); + } + } + + // Only set fallbackError if we don't have a run_id - if we have a run_id, + // App.tsx will fetch detailed error info from the server which is better + if (!lastRunId) { + fallbackError = errorMessage; + } // Set error stop reason so drainStreamWithResume can try to reconnect stopReason = "error"; @@ -235,7 +246,7 @@ export async function drainStream( lastRunId, lastSeqId, apiDurationMs, - streamError, + fallbackError, }; } @@ -279,7 +290,7 @@ export async function drainStreamWithResume( !abortSignal?.aborted ) { // Preserve the original error in case resume fails - const originalStreamError = result.streamError; + const originalFallbackError = result.fallbackError; try { const client = await getClient(); @@ -304,7 +315,7 @@ export async function drainStreamWithResume( } catch (_e) { // Resume failed - stick with the error stop_reason // Restore the original stream error for display - result.streamError = originalStreamError; + result.fallbackError = originalFallbackError; } }