feat(llm): report tool call errors

This commit is contained in:
Elian Doran
2026-03-30 17:45:58 +03:00
parent 12797293f0
commit 8a8143167f
8 changed files with 47 additions and 12 deletions

View File

@@ -13,7 +13,7 @@ export interface StreamCallbacks {
onChunk: (text: string) => void;
onThinking?: (text: string) => void;
onToolUse?: (toolName: string, input: Record<string, unknown>) => void;
onToolResult?: (toolName: string, result: string) => void;
onToolResult?: (toolName: string, result: string, isError?: boolean) => void;
onCitation?: (citation: LlmCitation) => void;
onUsage?: (usage: LlmUsage) => void;
onError: (error: string) => void;
@@ -78,7 +78,7 @@ export async function streamChatCompletion(
callbacks.onToolUse?.(data.toolName, data.toolInput);
break;
case "tool_result":
callbacks.onToolResult?.(data.toolName, data.result);
callbacks.onToolResult?.(data.toolName, data.result, data.isError);
break;
case "citation":
if (data.citation) {

View File

@@ -1627,6 +1627,8 @@
"tool_calls": "{{count}} tool call(s)",
"input": "Input",
"result": "Result",
"error": "Error",
"tool_error": "failed",
"tokens_used": "{{prompt}} prompt + {{completion}} completion = {{total}} tokens",
"tokens_used_with_cost": "{{prompt}} prompt + {{completion}} completion = {{total}} tokens (~${{cost}})",
"tokens_used_with_model": "{{model}}: {{prompt}} prompt + {{completion}} completion = {{total}} tokens",

View File

@@ -18,11 +18,17 @@ interface Props {
}
function ToolCallCard({ toolCall }: { toolCall: ToolCall }) {
const classes = [
"llm-chat-tool-call-inline",
toolCall.isError && "llm-chat-tool-call-error"
].filter(Boolean).join(" ");
return (
<details className="llm-chat-tool-call-inline">
<details className={classes}>
<summary className="llm-chat-tool-call-inline-summary">
<span className="bx bx-wrench" />
<span className={toolCall.isError ? "bx bx-error-circle" : "bx bx-wrench"} />
{toolCall.toolName}
{toolCall.isError && <span className="llm-chat-tool-call-error-badge">{t("llm_chat.tool_error")}</span>}
</summary>
<div className="llm-chat-tool-call-inline-body">
<div className="llm-chat-tool-call-input">
@@ -30,8 +36,8 @@ function ToolCallCard({ toolCall }: { toolCall: ToolCall }) {
<pre>{JSON.stringify(toolCall.input, null, 2)}</pre>
</div>
{toolCall.result && (
<div className="llm-chat-tool-call-result">
<strong>{t("llm_chat.result")}:</strong>
<div className={`llm-chat-tool-call-result ${toolCall.isError ? "llm-chat-tool-call-result-error" : ""}`}>
<strong>{toolCall.isError ? t("llm_chat.error") : t("llm_chat.result")}:</strong>
<pre>{(() => {
if (typeof toolCall.result === "string" && (toolCall.result.startsWith("{") || toolCall.result.startsWith("["))) {
try {

View File

@@ -588,6 +588,27 @@
margin-top: 0.5rem;
}
/* Tool call error styling */
.llm-chat-tool-call-error {
border-left-color: var(--danger-color, #dc3545);
}
.llm-chat-tool-call-error .llm-chat-tool-call-inline-summary {
color: var(--danger-color, #dc3545);
}
.llm-chat-tool-call-error-badge {
font-size: 0.75rem;
font-weight: 400;
font-family: var(--main-font-family);
color: var(--danger-color, #dc3545);
opacity: 0.8;
}
.llm-chat-tool-call-result-error pre {
color: var(--danger-color, #dc3545);
}
/* Token usage display */
.llm-chat-usage {
display: flex;

View File

@@ -7,6 +7,7 @@ export interface ToolCall {
toolName: string;
input: Record<string, unknown>;
result?: string;
isError?: boolean;
}
/** A block of text content (rendered as Markdown for assistant messages). */

View File

@@ -270,12 +270,13 @@ export function useLlmChat(
}
});
},
onToolResult: (toolName, result) => {
onToolResult: (toolName, result, isError) => {
// Find the most recent tool_call block for this tool without a result
for (let i = contentBlocks.length - 1; i >= 0; i--) {
const block = contentBlocks[i];
if (block.type === "tool_call" && block.toolCall.toolName === toolName && !block.toolCall.result) {
block.toolCall.result = result;
block.toolCall.isError = isError;
break;
}
}

View File

@@ -49,15 +49,19 @@ export async function* streamToChunks(result: StreamResult, options: StreamOptio
};
break;
case "tool-result":
case "tool-result": {
const output = part.output;
const isError = typeof output === "object" && output !== null && "error" in output;
yield {
type: "tool_result",
toolName: part.toolName,
result: typeof part.output === "string"
? part.output
: JSON.stringify(part.output)
result: typeof output === "string"
? output
: JSON.stringify(output),
isError
};
break;
}
case "source":
// Citation from web search (only URL sources have url property)

View File

@@ -94,7 +94,7 @@ export type LlmStreamChunk =
| { type: "text"; content: string }
| { type: "thinking"; content: string }
| { type: "tool_use"; toolName: string; toolInput: Record<string, unknown> }
| { type: "tool_result"; toolName: string; result: string }
| { type: "tool_result"; toolName: string; result: string; isError?: boolean }
| { type: "citation"; citation: LlmCitation }
| { type: "usage"; usage: LlmUsage }
| { type: "error"; error: string }