fix: handle mid-stream errors properly using sdk typing (#54)
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
// src/cli/App.tsx
|
// src/cli/App.tsx
|
||||||
|
|
||||||
|
import { APIError } from "@letta-ai/letta-client/core/error";
|
||||||
import type {
|
import type {
|
||||||
AgentState,
|
AgentState,
|
||||||
MessageCreate,
|
MessageCreate,
|
||||||
@@ -535,7 +536,17 @@ export default function App({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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);
|
setStreaming(false);
|
||||||
} finally {
|
} finally {
|
||||||
abortControllerRef.current = null;
|
abortControllerRef.current = null;
|
||||||
@@ -560,13 +571,14 @@ export default function App({
|
|||||||
const client = await getClient();
|
const client = await getClient();
|
||||||
|
|
||||||
// Send cancel request to backend
|
// 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
|
// 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
|
// TODO: Once backend is fixed, comment out the immediate abort below and uncomment the timeout version
|
||||||
if (abortControllerRef.current) {
|
// if (abortControllerRef.current) {
|
||||||
abortControllerRef.current.abort();
|
// abortControllerRef.current.abort();
|
||||||
}
|
// }
|
||||||
|
|
||||||
// FUTURE: Use this timeout-based abort once backend properly sends "cancelled" stop reason
|
// 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
|
// 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) {
|
export function onChunk(b: Buffers, chunk: LettaStreamingResponse) {
|
||||||
// TODO remove once SDK v1 has proper typing for in-stream errors
|
// 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)
|
// 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.)
|
// Note: Error handling moved to catch blocks in App.tsx and headless.ts
|
||||||
const chunkWithError = chunk as typeof chunk & {
|
// The SDK now throws APIError when it sees event: error, so chunks never have error property
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (chunk.message_type) {
|
switch (chunk.message_type) {
|
||||||
case "reasoning_message": {
|
case "reasoning_message": {
|
||||||
|
|||||||
@@ -117,6 +117,11 @@ export async function drainStream(
|
|||||||
stopReason = "error";
|
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
|
// Mark the final line as finished now that stream has ended
|
||||||
markCurrentLineAsFinished(buffers);
|
markCurrentLineAsFinished(buffers);
|
||||||
queueMicrotask(refresh);
|
queueMicrotask(refresh);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { parseArgs } from "node:util";
|
import { parseArgs } from "node:util";
|
||||||
|
import { APIError } from "@letta-ai/letta-client/core/error";
|
||||||
import type {
|
import type {
|
||||||
AgentState,
|
AgentState,
|
||||||
MessageCreate,
|
MessageCreate,
|
||||||
@@ -575,7 +576,17 @@ export async function handleHeadlessCommand(argv: string[], model?: string) {
|
|||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} 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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user