fix: handle mid-stream errors properly using sdk typing (#54)

This commit is contained in:
Charles Packer
2025-11-02 20:36:05 -08:00
committed by GitHub
parent 998f24aaf5
commit f7cea4a830
4 changed files with 36 additions and 26 deletions

View File

@@ -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

View File

@@ -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": {

View File

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

View File

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