diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index 98a807d1..2eea5e83 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -19,7 +19,12 @@ import { callGeminiChat, streamGeminiChat } from '@/lib/ai/gemini-chat'; import { VIBN_TOOL_DEFINITIONS, executeMcpTool } from '@/lib/ai/vibn-tools'; import type { ChatMessage, ToolCall } from '@/lib/ai/gemini-chat'; -const MAX_TOOL_ROUNDS = 6; +// Bumped from 6 to 12 because Path B chains (devcontainer.ensure → +// fs.read → fs.edit → kill → start → curl → logs) routinely fire 7-10 +// tool calls in one user turn. When the cap IS hit, we still emit a +// narrative summary instead of leaving the user staring at a tool tray +// (see the no-tools follow-up call below). +const MAX_TOOL_ROUNDS = 12; let chatTablesReady = false; async function ensureChatTables() { @@ -286,6 +291,32 @@ export async function POST(request: Request) { } } + // If the loop exited because we hit MAX_TOOL_ROUNDS while the + // model still wanted to call tools, the user has only seen a + // tray of ✓ icons with no narrative. Force one final no-tools + // call so we always end on a human-readable summary. + const lastTurnHadTools = + messages.length > 0 && + messages[messages.length - 1].role === 'tool'; + if (round >= MAX_TOOL_ROUNDS && lastTurnHadTools) { + try { + const summary = await callGeminiChat({ + systemPrompt: + systemPrompt + + '\n\nYou have just executed a chain of tool calls. Summarize the result for the user in 1-3 sentences. Do NOT call any more tools.', + messages, + tools: [], + temperature: 0.3, + }); + if (summary.text) { + assistantText += summary.text; + emit({ type: 'text', text: summary.text }); + } + } catch { + // Don't let a failed summary kill the stream. + } + } + // Persist final assistant message const finalMsg: ChatMessage = { role: 'assistant',