mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-23 00:43:14 +00:00
Adds a ReasoningRepetitionDetector that monitors streaming reasoning/thinking content for repetitive patterns. When a line is repeated more than a threshold number of times (default 5), the stream is aborted early to save tokens and the model receives guidance to try a different approach. This addresses an issue where some models (particularly Gemini) get stuck in reasoning loops, repeating the same lines like "I'll use attempt_completion" or "I'll mention that I verified with tests" indefinitely. Changes: - New ReasoningRepetitionDetector class with streaming chunk support - Integration into Task.ts reasoning streaming handler - New reasoningRepetitionDetected response format for model guidance - 18 unit tests covering detection, normalization, reset, and edge cases Closes #11337
125 lines
3.4 KiB
TypeScript
125 lines
3.4 KiB
TypeScript
/**
|
|
* Detects repetitive patterns in model reasoning/thinking output during streaming.
|
|
*
|
|
* Some models (particularly Gemini) can get stuck in a loop where their
|
|
* thinking/reasoning output repeats the same lines over and over, e.g.:
|
|
* "I'll mention that I verified with tests."
|
|
* "I'll mention that I reverted the tests."
|
|
* "I'll mention that I verified with tests."
|
|
* "I'll mention that I reverted the tests."
|
|
* ...
|
|
*
|
|
* This detector tracks lines as they stream in and flags when any single
|
|
* line has been repeated more than the configured threshold.
|
|
*/
|
|
export class ReasoningRepetitionDetector {
|
|
private lineCounts: Map<string, number> = new Map()
|
|
private buffer: string = ""
|
|
private readonly repetitionThreshold: number
|
|
private readonly minLineLength: number
|
|
|
|
/**
|
|
* @param repetitionThreshold Number of times a line must repeat to be considered a loop (default: 5)
|
|
* @param minLineLength Minimum line length to track - short lines are ignored (default: 20)
|
|
*/
|
|
constructor(repetitionThreshold: number = 5, minLineLength: number = 20) {
|
|
this.repetitionThreshold = repetitionThreshold
|
|
this.minLineLength = minLineLength
|
|
}
|
|
|
|
/**
|
|
* Feed a new chunk of reasoning text and check for repetition.
|
|
*
|
|
* @param chunk A new piece of reasoning/thinking text from the stream
|
|
* @returns true if repetitive looping has been detected
|
|
*/
|
|
public addChunk(chunk: string): boolean {
|
|
this.buffer += chunk
|
|
|
|
// Split buffer into complete lines (keeping incomplete last line in buffer)
|
|
const lines = this.buffer.split("\n")
|
|
|
|
// Keep the last element as the buffer (it may be an incomplete line)
|
|
this.buffer = lines.pop() ?? ""
|
|
|
|
for (const rawLine of lines) {
|
|
const line = this.normalizeLine(rawLine)
|
|
|
|
if (line.length < this.minLineLength) {
|
|
continue
|
|
}
|
|
|
|
const count = (this.lineCounts.get(line) ?? 0) + 1
|
|
this.lineCounts.set(line, count)
|
|
|
|
if (count >= this.repetitionThreshold) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Check if any line in the accumulated reasoning has hit the repetition threshold.
|
|
* Useful for checking after a stream is complete but before tool processing.
|
|
*/
|
|
public isRepetitive(): boolean {
|
|
// Also process any remaining buffer content
|
|
if (this.buffer.length > 0) {
|
|
const line = this.normalizeLine(this.buffer)
|
|
if (line.length >= this.minLineLength) {
|
|
const count = (this.lineCounts.get(line) ?? 0) + 1
|
|
this.lineCounts.set(line, count)
|
|
if (count >= this.repetitionThreshold) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const count of this.lineCounts.values()) {
|
|
if (count >= this.repetitionThreshold) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Get the most repeated line and its count, useful for diagnostics.
|
|
*/
|
|
public getMostRepeatedLine(): { line: string; count: number } | undefined {
|
|
let maxLine: string | undefined
|
|
let maxCount = 0
|
|
|
|
for (const [line, count] of this.lineCounts.entries()) {
|
|
if (count > maxCount) {
|
|
maxCount = count
|
|
maxLine = line
|
|
}
|
|
}
|
|
|
|
if (maxLine !== undefined) {
|
|
return { line: maxLine, count: maxCount }
|
|
}
|
|
|
|
return undefined
|
|
}
|
|
|
|
/**
|
|
* Reset the detector state. Called at the start of each new API request.
|
|
*/
|
|
public reset(): void {
|
|
this.lineCounts.clear()
|
|
this.buffer = ""
|
|
}
|
|
|
|
/**
|
|
* Normalize a line for comparison: trim whitespace, collapse internal
|
|
* whitespace, and lowercase.
|
|
*/
|
|
private normalizeLine(line: string): string {
|
|
return line.trim().replace(/\s+/g, " ").toLowerCase()
|
|
}
|
|
}
|