fix: handle multiple error types (#178)

This commit is contained in:
Christina Tong
2025-12-11 12:49:29 -08:00
committed by GitHub
parent 6b8215aa87
commit ead4203448
2 changed files with 174 additions and 67 deletions

View File

@@ -1,7 +1,7 @@
// src/cli/App.tsx
import { existsSync, readFileSync } from "node:fs";
import { APIError, APIUserAbortError } from "@letta-ai/letta-client/core/error";
import { APIUserAbortError } from "@letta-ai/letta-client/core/error";
import type {
AgentState,
MessageCreate,
@@ -57,6 +57,7 @@ import {
toLines,
} from "./helpers/accumulator";
import { backfillBuffers } from "./helpers/backfill";
import { formatErrorDetails } from "./helpers/errorFormatter";
import {
buildMessageContentFromDisplay,
clearPlaceholdersInText,
@@ -600,8 +601,6 @@ export default function App({
initialInput: Array<MessageCreate | ApprovalCreate>,
): Promise<void> => {
const currentInput = initialInput;
// Track lastRunId outside the while loop so it's available in catch block
let lastKnownRunId: string | null = null;
try {
// Check if user hit escape before we started
@@ -634,11 +633,6 @@ export default function App({
abortControllerRef.current?.signal,
);
// Update lastKnownRunId for error handling in catch block
if (lastRunId) {
lastKnownRunId = lastRunId;
}
// Track API duration
sessionStatsRef.current.endTurn(apiDurationMs);
sessionStatsRef.current.updateUsageFromBuffers(buffersRef.current);
@@ -800,6 +794,17 @@ export default function App({
// If all are auto-handled, continue immediately without showing dialog
if (needsUserInput.length === 0) {
// Check if user cancelled before continuing
if (
userCancelledRef.current ||
abortControllerRef.current?.signal.aborted
) {
setStreaming(false);
markIncompleteToolsAsCancelled(buffersRef.current);
refreshDerived();
return;
}
// Rotate to a new thinking message
setThinkingMessage(getRandomThinkingMessage());
refreshDerived();
@@ -848,13 +853,7 @@ export default function App({
// Mark incomplete tool calls as finished to prevent stuck blinking UI
markIncompleteToolsAsCancelled(buffersRef.current);
// Build run info suffix for debugging
const runInfoSuffix = lastRunId
? `\n(run_id: ${lastRunId}, stop_reason: ${stopReason})`
: `\n(stop_reason: ${stopReason})`;
// Fetch error details from the run if available
let errorDetails = `An error occurred during agent execution`;
if (lastRunId) {
try {
const client = await getClient();
@@ -862,27 +861,41 @@ export default function App({
// Check if run has error information in metadata
if (run.metadata?.error) {
const error = run.metadata.error as {
const errorData = run.metadata.error as {
type?: string;
message?: string;
detail?: string;
};
const errorType = error.type ? `[${error.type}] ` : "";
const errorMessage = error.message || "An error occurred";
const errorDetail = error.detail ? `\n${error.detail}` : "";
errorDetails = `${errorType}${errorMessage}${errorDetail}`;
// Pass structured error data to our formatter
const errorObject = {
error: {
error: errorData,
run_id: lastRunId,
},
};
const errorDetails = formatErrorDetails(errorObject, agentId);
appendError(errorDetails);
} else {
// No error metadata, show generic error with run info
appendError(
`An error occurred during agent execution\n(run_id: ${lastRunId}, stop_reason: ${stopReason})`,
);
}
} catch (_e) {
// If we can't fetch error details, let user know
// If we can't fetch error details, show generic error
appendError(
`${errorDetails}${runInfoSuffix}\n(Unable to fetch additional error details from server)`,
`An error occurred during agent execution\n(run_id: ${lastRunId}, stop_reason: ${stopReason})\n(Unable to fetch additional error details from server)`,
);
return;
}
} else {
// No run_id available - but this is unusual since errors should have run_ids
appendError(
`An error occurred during agent execution\n(stop_reason: ${stopReason})`,
);
}
appendError(`${errorDetails}${runInfoSuffix}`);
setStreaming(false);
refreshDerived();
return;
@@ -899,25 +912,9 @@ export default function App({
return;
}
// Build error message with run_id for debugging
const runIdSuffix = lastKnownRunId
? `\n(run_id: ${lastKnownRunId}, stop_reason: error)`
: "";
// Handle APIError from streaming (event: error)
if (e instanceof APIError && e.error?.error) {
const { type, message, detail } = e.error.error;
const errorType = type ? `[${type}] ` : "";
const errorMessage = message || "An error occurred";
const errorDetail = detail ? `:\n${detail}` : "";
appendError(
`${errorType}${errorMessage}${errorDetail}${runIdSuffix}`,
);
} else {
// Fallback for non-API errors
const errorMessage = e instanceof Error ? e.message : String(e);
appendError(`${errorMessage}${runIdSuffix}`);
}
// Use comprehensive error formatting
const errorDetails = formatErrorDetails(e, agentId);
appendError(errorDetails);
setStreaming(false);
refreshDerived();
} finally {
@@ -988,7 +985,8 @@ export default function App({
abortControllerRef.current.abort();
}
} catch (e) {
appendError(`Failed to interrupt stream: ${String(e)}`);
const errorDetails = formatErrorDetails(e, agentId);
appendError(`Failed to interrupt stream: ${errorDetails}`);
setInterruptRequested(false);
}
}
@@ -1134,11 +1132,12 @@ export default function App({
// Exit after a brief delay to show the message
setTimeout(() => process.exit(0), 500);
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1187,11 +1186,12 @@ export default function App({
refreshDerived();
} catch (error) {
// Mark command as failed
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1243,11 +1243,12 @@ export default function App({
buffersRef.current.order.push(cmdId);
refreshDerived();
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1286,11 +1287,12 @@ export default function App({
});
refreshDerived();
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1329,11 +1331,12 @@ export default function App({
});
refreshDerived();
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1392,11 +1395,12 @@ export default function App({
});
refreshDerived();
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1494,11 +1498,12 @@ export default function App({
buffersRef.current.order.push(successCmdId);
refreshDerived();
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1577,11 +1582,12 @@ export default function App({
});
refreshDerived();
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1650,13 +1656,12 @@ export default function App({
},
]);
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${
error instanceof Error ? error.message : String(error)
}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1771,11 +1776,12 @@ ${recentCommits}
},
]);
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1817,11 +1823,12 @@ ${recentCommits}
refreshDerived();
} catch (error) {
// Mark command as failed if executeCommand throws
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: msg,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -1986,6 +1993,21 @@ ${recentCommits}
| { type: "deny"; approval: ApprovalRequest; reason: string },
) => {
try {
// Don't send results if user has already cancelled
if (
userCancelledRef.current ||
abortControllerRef.current?.signal.aborted
) {
setStreaming(false);
setIsExecutingTool(false);
setPendingApprovals([]);
setApprovalContexts([]);
setApprovalResults([]);
setAutoHandledResults([]);
setAutoDeniedApprovals([]);
return;
}
// Snapshot current state before clearing dialog
const approvalResultsSnapshot = [...approvalResults];
const autoHandledSnapshot = [...autoHandledResults];
@@ -2086,10 +2108,15 @@ ${recentCommits}
refreshDerived();
const wasAborted = approvalAbortController.signal.aborted;
const userCancelled =
userCancelledRef.current ||
abortControllerRef.current?.signal.aborted;
if (wasAborted) {
// Queue results to send alongside the next user message
setQueuedApprovalResults(allResults as ApprovalResult[]);
if (wasAborted || userCancelled) {
// Queue results to send alongside the next user message (if not cancelled entirely)
if (!userCancelled) {
setQueuedApprovalResults(allResults as ApprovalResult[]);
}
setStreaming(false);
} else {
// Continue conversation with all results
@@ -2146,11 +2173,13 @@ ${recentCommits}
setIsExecutingTool(false);
}
} catch (e) {
appendError(String(e));
const errorDetails = formatErrorDetails(e, agentId);
appendError(errorDetails);
setStreaming(false);
setIsExecutingTool(false);
}
}, [
agentId,
pendingApprovals,
approvalResults,
sendAllResults,
@@ -2234,12 +2263,14 @@ ${recentCommits}
setIsExecutingTool(false);
}
} catch (e) {
appendError(String(e));
const errorDetails = formatErrorDetails(e, agentId);
appendError(errorDetails);
setStreaming(false);
setIsExecutingTool(false);
}
},
[
agentId,
pendingApprovals,
approvalResults,
sendAllResults,
@@ -2352,12 +2383,13 @@ ${recentCommits}
refreshDerived();
} catch (error) {
// Mark command as failed (only if cmdId was created)
const errorDetails = formatErrorDetails(error, agentId);
if (cmdId) {
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: `/model ${modelId}`,
output: `Failed to switch model: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed to switch model: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -2439,11 +2471,12 @@ ${recentCommits}
}
refreshDerived();
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: `/system ${promptId}`,
output: `Failed to switch system prompt: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed to switch system prompt: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -2499,11 +2532,12 @@ ${recentCommits}
});
refreshDerived();
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: `/toolset ${toolsetId}`,
output: `Failed to switch toolset: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed to switch toolset: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -2594,11 +2628,12 @@ ${recentCommits}
buffersRef.current.order.push(successCmdId);
refreshDerived();
} catch (error) {
const errorDetails = formatErrorDetails(error, agentId);
buffersRef.current.byId.set(cmdId, {
kind: "command",
id: cmdId,
input: `/swap ${targetAgentId}`,
output: `Failed: ${error instanceof Error ? error.message : String(error)}`,
output: `Failed: ${errorDetails}`,
phase: "finished",
success: false,
});
@@ -2607,7 +2642,7 @@ ${recentCommits}
setCommandRunning(false);
}
},
[refreshDerived, commitEligibleLines, columns],
[refreshDerived, commitEligibleLines, columns, agentId],
);
// Track permission mode changes for UI updates
@@ -2664,11 +2699,13 @@ ${recentCommits}
setApprovalResults((prev) => [...prev, decision]);
}
} catch (e) {
appendError(String(e));
const errorDetails = formatErrorDetails(e, agentId);
appendError(errorDetails);
setStreaming(false);
}
},
[
agentId,
pendingApprovals,
approvalResults,
sendAllResults,

View File

@@ -0,0 +1,70 @@
import { APIError } from "@letta-ai/letta-client/core/error";
/**
* Extract comprehensive error details from any error object
* Handles APIError, Error, and other error types consistently
* @param e The error object to format
* @param agentId Optional agent ID to create hyperlinks to the Letta dashboard
*/
export function formatErrorDetails(e: unknown, agentId?: string): string {
let runId: string | undefined;
// Handle APIError from streaming (event: error)
if (e instanceof APIError) {
// Check for nested error structure: e.error.error
if (e.error && typeof e.error === "object" && "error" in e.error) {
const errorData = e.error.error;
if (errorData && typeof errorData === "object") {
const type = "type" in errorData ? errorData.type : undefined;
const message =
"message" in errorData ? errorData.message : "An error occurred";
const detail = "detail" in errorData ? errorData.detail : undefined;
const errorType = type ? `[${type}] ` : "";
const errorDetail = detail ? `\nDetail: ${detail}` : "";
// Extract run_id from e.error
if ("run_id" in e.error && typeof e.error.run_id === "string") {
runId = e.error.run_id;
}
const baseError = `${errorType}${message}${errorDetail}`;
return runId && agentId
? `${baseError}\n${createAgentLink(runId, agentId)}`
: baseError;
}
}
// Handle APIError with direct error structure: e.error.detail
if (e.error && typeof e.error === "object") {
const detail = "detail" in e.error ? e.error.detail : undefined;
if ("run_id" in e.error && typeof e.error.run_id === "string") {
runId = e.error.run_id;
}
const baseError = detail ? `${e.message}\nDetail: ${detail}` : e.message;
return runId && agentId
? `${baseError}\n${createAgentLink(runId, agentId)}`
: baseError;
}
// Fallback for APIError with just message
return e.message;
}
// Handle regular Error objects
if (e instanceof Error) {
return e.message;
}
// Fallback for any other type
return String(e);
}
/**
* Create a terminal hyperlink to the agent with run ID displayed
*/
function createAgentLink(runId: string, agentId: string): string {
const url = `https://app.letta.com/agents/${agentId}`;
return `View agent: \x1b]8;;${url}\x1b\\${agentId}\x1b]8;;\x1b\\ (run: ${runId})`;
}