From 493b0f924f15e57cc45f53b2ed7ee65eba5b74ea Mon Sep 17 00:00:00 2001 From: Gab Date: Sun, 29 Mar 2026 19:24:02 +1100 Subject: [PATCH] fix: enforced loop detection --- .../sdk/toothfairyai/toothfairyai-provider.ts | 102 +++++++++--------- packages/tfcode/src/session/processor.ts | 5 + .../tfcode/src/session/prompt/default.txt | 9 +- 3 files changed, 62 insertions(+), 54 deletions(-) diff --git a/packages/tfcode/src/provider/sdk/toothfairyai/toothfairyai-provider.ts b/packages/tfcode/src/provider/sdk/toothfairyai/toothfairyai-provider.ts index d2ac33354..ad415eb91 100644 --- a/packages/tfcode/src/provider/sdk/toothfairyai/toothfairyai-provider.ts +++ b/packages/tfcode/src/provider/sdk/toothfairyai/toothfairyai-provider.ts @@ -1,5 +1,5 @@ import type { LanguageModelV2 } from "@ai-sdk/provider" -import { type FetchFunction, withoutTrailingSlash } from "@ai-sdk/provider-utils" +import { type FetchFunction, withoutTrailingSlash, safeParseJSON, EventSourceParserStream } from "@ai-sdk/provider-utils" import { OpenAICompatibleChatLanguageModel } from "../copilot/chat/openai-compatible-chat-language-model" import { Log } from "@/util/log" @@ -117,57 +117,59 @@ export function createToothFairyAI(options: ToothFairyAIProviderSettings = {}): } if (res.body && res.headers.get("content-type")?.includes("text/event-stream")) { - const decoder = new TextDecoder() - const encoder = new TextEncoder() - let buffer = "" - - const filteredStream = res.body.pipeThrough( - new TransformStream({ - transform(chunk, controller) { - buffer += decoder.decode(chunk, { stream: true }) - const lines = buffer.split("\n") - buffer = lines.pop() || "" - - const filtered: string[] = [] - for (const line of lines) { - if (line.startsWith("data: ")) { - const json = line.slice(6).trim() - if (json) { - try { - const parsed = JSON.parse(json) - if (parsed.status === "initialising" || parsed.status === "connected") { - log.debug("filtered connection status", { status: parsed.status }) - continue - } - if (parsed.choices?.[0]?.finish_reason) { - log.info("stream finish_reason", { - finish_reason: parsed.choices[0].finish_reason, - }) - } - if (parsed.usage) { - log.info("stream usage", { - prompt_tokens: parsed.usage.prompt_tokens, - completion_tokens: parsed.usage.completion_tokens, - total_tokens: parsed.usage.total_tokens, - }) - } - } catch {} - } + const filteredStream = res.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new EventSourceParserStream()) + .pipeThrough( + new TransformStream({ + async transform({ data }, controller) { + if (data === "[DONE]") { + return } - filtered.push(line) - } - if (filtered.length > 0) { - controller.enqueue(encoder.encode(filtered.join("\n") + "\n")) - } - }, - flush(controller) { - if (buffer) { - controller.enqueue(encoder.encode(buffer)) - } - }, - }), - ) + const parsed = await safeParseJSON({ text: data, schema: null }) + + if (!parsed.success) { + log.error("Failed to parse SSE chunk", { + chunk: data.slice(0, 100), + error: parsed.error, + }) + controller.enqueue({ data }) + return + } + + const value = parsed.value + + if (value.status === "initialising" || value.status === "connected") { + log.debug("filtered connection status", { status: value.status }) + return + } + + if (value.choices?.[0]?.finish_reason) { + log.info("stream finish_reason", { + finish_reason: value.choices[0].finish_reason, + }) + } + + if (value.usage) { + log.info("stream usage", { + prompt_tokens: value.usage.prompt_tokens, + completion_tokens: value.usage.completion_tokens, + total_tokens: value.usage.total_tokens, + }) + } + + controller.enqueue({ data }) + }, + }), + ) + .pipeThrough( + new TransformStream({ + transform({ data }, controller) { + controller.enqueue(new TextEncoder().encode(`data: ${data}\n\n`)) + }, + }), + ) return new Response(filteredStream, { headers: res.headers, diff --git a/packages/tfcode/src/session/processor.ts b/packages/tfcode/src/session/processor.ts index 6a5ddb7fb..b5e9993a3 100644 --- a/packages/tfcode/src/session/processor.ts +++ b/packages/tfcode/src/session/processor.ts @@ -198,6 +198,11 @@ export namespace SessionProcessor { always: [value.toolName], ruleset: agent.permission, }) + // STOP the loop - do not execute the tool again + blocked = true + throw new Error( + `Verification loop detected: Tool "${value.toolName}" called 3+ times with ${isSimilarLoop ? "similar" : "identical"} parameters. Move to next step or ask for guidance.`, + ) } } break diff --git a/packages/tfcode/src/session/prompt/default.txt b/packages/tfcode/src/session/prompt/default.txt index 53a63efef..80e536f42 100644 --- a/packages/tfcode/src/session/prompt/default.txt +++ b/packages/tfcode/src/session/prompt/default.txt @@ -86,10 +86,11 @@ The user will primarily request you perform software engineering tasks. This inc NEVER commit changes unless the user explicitly asks you to. It is VERY IMPORTANT to only commit when explicitly asked, otherwise the user will feel that you are being too proactive. # Avoiding loops -- CRITICAL: Avoid verification loops. After making edits, do NOT re-read the same file to verify your changes. Trust that your edits were applied correctly. -- If you need to verify something, read the file ONCE before editing, not repeatedly after each edit. -- If you believe an edit was not applied correctly, check the file ONCE. If it looks correct, move on. Do not re-read the same file multiple times. -- Reading the same file 3 or more times is a sign of a loop - STOP and ask the user for guidance. +- **ONE VERIFICATION MAXIMUM**: After making edits, you may verify ONCE by reading the file. After this single verification, NEVER read the same file again for verification purposes. +- **TRUST TOOL EXECUTION**: When a tool returns success, the action is complete. The system executes tools correctly. +- **VERIFICATION IS NOT RE-EXECUTION**: Do not re-run tools to "verify". If you need to verify an edit, read the file ONCE, then move on. +- **READING LIMIT**: Reading the same file 3+ times indicates a malfunction. STOP immediately and ask the user for guidance. +- **NO REPEATED CALLS**: Never call the same tool with the same parameters more than once. Plan ahead and gather all needed information in a single pass. - Tool results and user messages may include tags. tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result.