fix: handle multiple error types (#178)
This commit is contained in:
171
src/cli/App.tsx
171
src/cli/App.tsx
@@ -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,
|
||||
|
||||
70
src/cli/helpers/errorFormatter.ts
Normal file
70
src/cli/helpers/errorFormatter.ts
Normal 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})`;
|
||||
}
|
||||
Reference in New Issue
Block a user