fix(chat): always emit narrative summary, even when tool-round cap is hit

Surfaced by the live Path B test: AI fired 7 tool calls (fs.read,
fs.edit, kill, dev_server.start, curl, dev_server.logs, ...) in a single
turn, the loop exited at MAX_TOOL_ROUNDS, and the user saw only a tray
of ✓ icons — no text reply.

Two changes:

1. Bump MAX_TOOL_ROUNDS 6 → 12. Path B iteration chains routinely run
   long; 6 was tuned for Path A's much-shorter Coolify-orchestration
   sequences.

2. When the loop exits because of the cap (the last assistant turn was
   a tool call, not a finish), force one more no-tools Gemini call
   with an explicit "summarize the result, do NOT call tools" prompt.
   That gives the user a sentence or two of context instead of a wall
   of green checkmarks. Wrapped in try/catch so the stream still
   terminates cleanly if Gemini errors.

Made-with: Cursor
This commit is contained in:
2026-04-28 14:17:40 -07:00
parent fb31d111ef
commit 115cf7eb28

View File

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