fix: prevent diagram regeneration loop after successful display (#206)

- Changed sendAutomaticallyWhen to only auto-resubmit on tool errors
- Extracted logic to shouldAutoResubmit() function with JSDoc
- Added TypeScript interfaces for type safety (MessagePart, ChatMessage)
- Wrapped debug logs with DEBUG flag for production readiness
- Added TOOL_ERROR_STATE constant to avoid hardcoded strings

Problem: AI was regenerating diagrams 3+ times even after successful display
Root cause: lastAssistantMessageIsCompleteWithToolCalls auto-resubmits on both success AND error
Solution: Custom logic that only auto-resubmits on errors, stops on success
This commit is contained in:
Dayuan Jiang
2025-12-11 09:47:30 +09:00
committed by GitHub
parent a047a6ff97
commit c0347dd55d

View File

@@ -1,10 +1,7 @@
"use client" "use client"
import { useChat } from "@ai-sdk/react" import { useChat } from "@ai-sdk/react"
import { import { DefaultChatTransport } from "ai"
DefaultChatTransport,
lastAssistantMessageIsCompleteWithToolCalls,
} from "ai"
import { import {
AlertTriangle, AlertTriangle,
PanelRightClose, PanelRightClose,
@@ -54,6 +51,20 @@ import {
import { formatXML, wrapWithMxFile } from "@/lib/utils" import { formatXML, wrapWithMxFile } from "@/lib/utils"
import { ChatMessageDisplay } from "./chat-message-display" import { ChatMessageDisplay } from "./chat-message-display"
// Type for message parts (tool calls and their states)
interface MessagePart {
type: string
state?: string
toolName?: string
[key: string]: unknown
}
interface ChatMessage {
role: string
parts?: MessagePart[]
[key: string]: unknown
}
interface ChatPanelProps { interface ChatPanelProps {
isVisible: boolean isVisible: boolean
onToggleVisibility: () => void onToggleVisibility: () => void
@@ -65,6 +76,70 @@ interface ChatPanelProps {
onCloseProtectionChange?: (enabled: boolean) => void onCloseProtectionChange?: (enabled: boolean) => void
} }
// Constants for tool states
const TOOL_ERROR_STATE = "output-error" as const
const DEBUG = process.env.NODE_ENV === "development"
/**
* Custom auto-resubmit logic for the AI chat.
*
* Strategy:
* - When tools return errors (e.g., invalid XML), automatically resubmit
* the conversation to let the AI retry with corrections
* - When tools succeed (e.g., diagram displayed), stop without AI acknowledgment
* to prevent unnecessary regeneration cycles
*
* This fixes the issue where successful diagrams were being regenerated
* multiple times because the previous logic (lastAssistantMessageIsCompleteWithToolCalls)
* auto-resubmitted on BOTH success and error.
*
* @param messages - Current conversation messages from AI SDK
* @returns true to auto-resubmit (for error recovery), false to stop
*/
function shouldAutoResubmit(messages: ChatMessage[]): boolean {
const lastMessage = messages[messages.length - 1]
if (!lastMessage || lastMessage.role !== "assistant") {
if (DEBUG) {
console.log(
"[sendAutomaticallyWhen] No assistant message, returning false",
)
}
return false
}
const toolParts =
(lastMessage.parts as MessagePart[] | undefined)?.filter((part) =>
part.type?.startsWith("tool-"),
) || []
if (toolParts.length === 0) {
if (DEBUG) {
console.log(
"[sendAutomaticallyWhen] No tool parts, returning false",
)
}
return false
}
// Only auto-resubmit if ANY tool has an error
const hasError = toolParts.some((part) => part.state === TOOL_ERROR_STATE)
if (DEBUG) {
if (hasError) {
console.log(
"[sendAutomaticallyWhen] Retrying due to errors in tools:",
toolParts
.filter((p) => p.state === TOOL_ERROR_STATE)
.map((p) => p.toolName),
)
} else {
console.log("[sendAutomaticallyWhen] No errors, stopping")
}
}
return hasError
}
export default function ChatPanel({ export default function ChatPanel({
isVisible, isVisible,
onToggleVisibility, onToggleVisibility,
@@ -369,8 +444,19 @@ export default function ChatPanel({
api: "/api/chat", api: "/api/chat",
}), }),
async onToolCall({ toolCall }) { async onToolCall({ toolCall }) {
if (DEBUG) {
console.log(
`[onToolCall] Tool: ${toolCall.toolName}, CallId: ${toolCall.toolCallId}`,
)
}
if (toolCall.toolName === "display_diagram") { if (toolCall.toolName === "display_diagram") {
const { xml } = toolCall.input as { xml: string } const { xml } = toolCall.input as { xml: string }
if (DEBUG) {
console.log(
`[display_diagram] Received XML length: ${xml.length}`,
)
}
// Wrap raw XML with full mxfile structure for draw.io // Wrap raw XML with full mxfile structure for draw.io
const fullXml = wrapWithMxFile(xml) const fullXml = wrapWithMxFile(xml)
@@ -392,6 +478,11 @@ Your failed XML:
\`\`\`xml \`\`\`xml
${xml} ${xml}
\`\`\`` \`\`\``
if (DEBUG) {
console.log(
"[display_diagram] Adding tool output with state: output-error",
)
}
addToolOutput({ addToolOutput({
tool: "display_diagram", tool: "display_diagram",
toolCallId: toolCall.toolCallId, toolCallId: toolCall.toolCallId,
@@ -400,11 +491,21 @@ ${xml}
}) })
} else { } else {
// Success - diagram will be rendered by chat-message-display // Success - diagram will be rendered by chat-message-display
if (DEBUG) {
console.log(
"[display_diagram] Success! Adding tool output with state: output-available",
)
}
addToolOutput({ addToolOutput({
tool: "display_diagram", tool: "display_diagram",
toolCallId: toolCall.toolCallId, toolCallId: toolCall.toolCallId,
output: "Successfully displayed the diagram.", output: "Successfully displayed the diagram.",
}) })
if (DEBUG) {
console.log(
"[display_diagram] Tool output added. Diagram should be visible now.",
)
}
} }
} else if (toolCall.toolName === "edit_diagram") { } else if (toolCall.toolName === "edit_diagram") {
const { edits } = toolCall.input as { const { edits } = toolCall.input as {
@@ -549,9 +650,8 @@ Please retry with an adjusted search pattern or use display_diagram if retries a
} }
} }
}, },
// Auto-resubmit when all tool results are available (including errors) sendAutomaticallyWhen: ({ messages }) =>
// This enables the model to retry when a tool returns an error shouldAutoResubmit(messages as unknown as ChatMessage[]),
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
}) })
// Update stopRef so onToolCall can access it // Update stopRef so onToolCall can access it