fix: enforced loop detection

This commit is contained in:
Gab
2026-03-29 19:24:02 +11:00
parent cecaf216c2
commit 493b0f924f
3 changed files with 62 additions and 54 deletions

View File

@@ -1,5 +1,5 @@
import type { LanguageModelV2 } from "@ai-sdk/provider" 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 { OpenAICompatibleChatLanguageModel } from "../copilot/chat/openai-compatible-chat-language-model"
import { Log } from "@/util/log" 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")) { if (res.body && res.headers.get("content-type")?.includes("text/event-stream")) {
const decoder = new TextDecoder() const filteredStream = res.body
const encoder = new TextEncoder() .pipeThrough(new TextDecoderStream())
let buffer = "" .pipeThrough(new EventSourceParserStream())
.pipeThrough(
const filteredStream = res.body.pipeThrough( new TransformStream({
new TransformStream({ async transform({ data }, controller) {
transform(chunk, controller) { if (data === "[DONE]") {
buffer += decoder.decode(chunk, { stream: true }) return
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 {}
}
} }
filtered.push(line)
}
if (filtered.length > 0) { const parsed = await safeParseJSON({ text: data, schema: null })
controller.enqueue(encoder.encode(filtered.join("\n") + "\n"))
} if (!parsed.success) {
}, log.error("Failed to parse SSE chunk", {
flush(controller) { chunk: data.slice(0, 100),
if (buffer) { error: parsed.error,
controller.enqueue(encoder.encode(buffer)) })
} 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, { return new Response(filteredStream, {
headers: res.headers, headers: res.headers,

View File

@@ -198,6 +198,11 @@ export namespace SessionProcessor {
always: [value.toolName], always: [value.toolName],
ruleset: agent.permission, 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 break

View File

@@ -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. 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 # 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. - **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.
- If you need to verify something, read the file ONCE before editing, not repeatedly after each edit. - **TRUST TOOL EXECUTION**: When a tool returns success, the action is complete. The system executes tools correctly.
- 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. - **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 the same file 3 or more times is a sign of a loop - STOP and ask the user for guidance. - **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 <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result. - Tool results and user messages may include <system-reminder> tags. <system-reminder> tags contain useful information and reminders. They are NOT part of the user's provided input or the tool result.