fix: handle mid-stream errors properly using sdk typing (#54)
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
// src/cli/App.tsx
|
||||
|
||||
import { APIError } from "@letta-ai/letta-client/core/error";
|
||||
import type {
|
||||
AgentState,
|
||||
MessageCreate,
|
||||
@@ -535,7 +536,17 @@ export default function App({
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
appendError(String(e));
|
||||
// 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}`);
|
||||
} else {
|
||||
// Fallback for non-API errors
|
||||
appendError(e instanceof Error ? e.message : String(e));
|
||||
}
|
||||
setStreaming(false);
|
||||
} finally {
|
||||
abortControllerRef.current = null;
|
||||
@@ -560,13 +571,14 @@ export default function App({
|
||||
const client = await getClient();
|
||||
|
||||
// Send cancel request to backend
|
||||
await client.agents.messages.cancel(agentId);
|
||||
const cancelResult = await client.agents.messages.cancel(agentId);
|
||||
// console.error("cancelResult", JSON.stringify(cancelResult, null, 2));
|
||||
|
||||
// WORKAROUND: Also abort the stream immediately since backend cancellation is buggy
|
||||
// TODO: Once backend is fixed, comment out the immediate abort below and uncomment the timeout version
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
// if (abortControllerRef.current) {
|
||||
// abortControllerRef.current.abort();
|
||||
// }
|
||||
|
||||
// FUTURE: Use this timeout-based abort once backend properly sends "cancelled" stop reason
|
||||
// This gives the backend 5 seconds to gracefully close the stream before forcing abort
|
||||
|
||||
@@ -197,26 +197,8 @@ function extractTextPart(v: unknown): string {
|
||||
export function onChunk(b: Buffers, chunk: LettaStreamingResponse) {
|
||||
// TODO remove once SDK v1 has proper typing for in-stream errors
|
||||
// Check for streaming error objects (not typed in SDK but emitted by backend)
|
||||
// These are emitted when LLM errors occur during streaming (rate limits, timeouts, etc.)
|
||||
const chunkWithError = chunk as typeof chunk & {
|
||||
error?: { message?: string; detail?: string };
|
||||
};
|
||||
if (chunkWithError.error && !chunk.message_type) {
|
||||
const errorId = `err-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const errorMsg = chunkWithError.error.message || "An error occurred";
|
||||
const errorDetail = chunkWithError.error.detail || "";
|
||||
const fullErrorText = errorDetail
|
||||
? `${errorMsg}: ${errorDetail}`
|
||||
: errorMsg;
|
||||
|
||||
b.byId.set(errorId, {
|
||||
kind: "error",
|
||||
id: errorId,
|
||||
text: `⚠ ${fullErrorText}`,
|
||||
});
|
||||
b.order.push(errorId);
|
||||
return;
|
||||
}
|
||||
// Note: Error handling moved to catch blocks in App.tsx and headless.ts
|
||||
// The SDK now throws APIError when it sees event: error, so chunks never have error property
|
||||
|
||||
switch (chunk.message_type) {
|
||||
case "reasoning_message": {
|
||||
|
||||
@@ -117,6 +117,11 @@ export async function drainStream(
|
||||
stopReason = "error";
|
||||
}
|
||||
|
||||
// Mark incomplete tool calls as cancelled if stream was cancelled
|
||||
if (stopReason === "cancelled") {
|
||||
markIncompleteToolsAsCancelled(buffers);
|
||||
}
|
||||
|
||||
// Mark the final line as finished now that stream has ended
|
||||
markCurrentLineAsFinished(buffers);
|
||||
queueMicrotask(refresh);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { parseArgs } from "node:util";
|
||||
import { APIError } from "@letta-ai/letta-client/core/error";
|
||||
import type {
|
||||
AgentState,
|
||||
MessageCreate,
|
||||
@@ -575,7 +576,17 @@ export async function handleHeadlessCommand(argv: string[], model?: string) {
|
||||
process.exit(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error: ${error}`);
|
||||
// Handle APIError from streaming (event: error)
|
||||
if (error instanceof APIError && error.error?.error) {
|
||||
const { type, message, detail } = error.error.error;
|
||||
const errorType = type ? `[${type}] ` : "";
|
||||
const errorMessage = message || "An error occurred";
|
||||
const errorDetail = detail ? `: ${detail}` : "";
|
||||
console.error(`Error: ${errorType}${errorMessage}${errorDetail}`);
|
||||
} else {
|
||||
// Fallback for non-API errors
|
||||
console.error(`Error: ${error}`);
|
||||
}
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user