fix: always fetch detailed error info from server when run_id exists (#385)

Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
Charles Packer
2025-12-24 14:42:01 -08:00
committed by GitHub
parent 50598233f0
commit 1f35f403c0
2 changed files with 34 additions and 20 deletions

View File

@@ -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();

View File

@@ -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<string, unknown>;
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;
}
}