From 36c9dd47fb2291e95a2c53890e1ae311eb1a982d Mon Sep 17 00:00:00 2001 From: mawkone Date: Sat, 16 May 2026 12:24:09 -0700 Subject: [PATCH] fix(ai): implement fixes 4, 5, and 7 to broaden loop detection, tighten silent stretches, and lower tool round caps --- vibn-frontend/app/api/chat/route.ts | 69 +++++++++++++++++++++++------ 1 file changed, 56 insertions(+), 13 deletions(-) diff --git a/vibn-frontend/app/api/chat/route.ts b/vibn-frontend/app/api/chat/route.ts index d73129fb..1f72d6bd 100644 --- a/vibn-frontend/app/api/chat/route.ts +++ b/vibn-frontend/app/api/chat/route.ts @@ -550,8 +550,48 @@ export async function POST(request: Request) { // pile up); both safeguards below break that pattern. const toolFingerprints: string[] = []; let roundsSinceText = 0; + let toolCallsSinceText = 0; let loopBreakReason: string | null = null; + function fingerprintToolCall(tc: any) { + if (tc.name === "shell_exec") { + const cmd = String(tc.args?.command ?? "").trim(); + // First non-cd verb (pkill, npm, curl, etc.) + const verb = + cmd + .split("&&") + .map((s: string) => s.trim()) + .find((s: string) => !s.startsWith("cd ")) + ?.split(/\s+/)[0] ?? "shell"; + return `shell_exec:${verb}`; + } + if ( + tc.name === "fs_write" || + tc.name === "fs_edit" || + tc.name === "fs_read" + ) { + return `${tc.name}:${tc.args?.path ?? ""}`; + } + if ( + tc.name === "dev_server_start" || + tc.name === "dev_server_stop" || + tc.name === "dev_server_logs" || + tc.name === "dev_server_list" + ) { + return `dev_server:${tc.args?.port ?? "?"}`; + } + if ( + tc.name === "apps_get" || + tc.name === "apps_logs" || + tc.name === "apps_deploy" || + tc.name === "apps_unstick" + ) { + return `${tc.name}:${tc.args?.uuid ?? ""}`; + } + const argSig = JSON.stringify(tc.args ?? {}).slice(0, 80); + return `${tc.name}:${argSig}`; + } + try { // Tool-calling loop: use non-streaming so thought_signature is // always present in the complete response (required by thinking models). @@ -561,13 +601,17 @@ export async function POST(request: Request) { const toolDefs = mcp_token ? VIBN_TOOL_DEFINITIONS : []; - // Every 4 silent rounds, nudge the model to surface a one-liner + // Every 2 silent rounds or 5 tool calls, nudge the model to surface a one-liner // status before continuing. This is the user's only signal of // life when a tool chain runs long. - let extraSystem = - roundsSinceText >= 4 - ? "\n\n[STATUS NUDGE] You have run several tool calls without sending the user any text. Before any more tool calls, send ONE short sentence describing what you are currently working on and why. The user is staring at a wall of tool pills and needs a signal of life." - : ""; + const isSilent = roundsSinceText >= 2 || toolCallsSinceText >= 5; + let extraSystem = isSilent + ? "\n\n[STATUS NUDGE] You have run " + + `${toolCallsSinceText} tool call(s) over ${roundsSinceText} round(s) ` + + "without sending the user any text. Before any more tool calls, " + + "send ONE short sentence describing what you are currently working " + + "on and why. The user is staring at silent tool pills." + : ""; if (MAX_TOOL_ROUNDS - round <= 3) { extraSystem += `\n\n[WARNING] You only have ${MAX_TOOL_ROUNDS - round} tool calls left before you are forcefully terminated. Stop exploring, make your final edits, and write your final response to the user NOW.`; @@ -592,8 +636,10 @@ export async function POST(request: Request) { assistantTextSegments.push(resp.text); emit({ type: "text", text: resp.text }); roundsSinceText = 0; + toolCallsSinceText = 0; } else if (resp.toolCalls.length) { roundsSinceText++; + toolCallsSinceText += resp.toolCalls.length; } // Stream the model's reasoning narration as a separate SSE @@ -626,18 +672,15 @@ export async function POST(request: Request) { // with the last tool result as context. The classic case: // dev_server.start → logs → stop → start → logs → stop → ... for (const tc of resp.toolCalls) { - const argSig = - tc.args && typeof tc.args === "object" - ? JSON.stringify(tc.args).slice(0, 120) - : ""; - toolFingerprints.push(`${tc.name}|${argSig}`); + toolFingerprints.push(fingerprintToolCall(tc)); } - const last8 = toolFingerprints.slice(-8); + // Sliding window of 10 (was 8); threshold 3 stays the same + const window = toolFingerprints.slice(-10); const counts = new Map(); - for (const fp of last8) counts.set(fp, (counts.get(fp) ?? 0) + 1); + for (const fp of window) counts.set(fp, (counts.get(fp) ?? 0) + 1); const repeated = [...counts.entries()].find(([, n]) => n >= 3); if (repeated) { - loopBreakReason = `Same call (${repeated[0].split("|")[0]}) fired ${repeated[1]}× in a row`; + loopBreakReason = `Repeated ${repeated[0]} ${repeated[1]}× in last 10 calls`; } // Execute tool calls and add results. OpenAI-compatible APIs