mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-21 09:17:53 +00:00
chore: sync gemini-cli v0.1.19
This commit is contained in:
@@ -202,6 +202,80 @@ describe('LoopDetectionService', () => {
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not detect loops when content transitions into a code block', () => {
|
||||
service.reset('');
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
|
||||
// Add some repetitive content outside of code block
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 2; i++) {
|
||||
service.addAndCheck(createContentEvent(repeatedContent));
|
||||
}
|
||||
|
||||
// Now transition into a code block - this should prevent loop detection
|
||||
// even though we were already close to the threshold
|
||||
const codeBlockStart = '```javascript\n';
|
||||
const isLoop = service.addAndCheck(createContentEvent(codeBlockStart));
|
||||
expect(isLoop).toBe(false);
|
||||
|
||||
// Continue adding repetitive content inside the code block - should not trigger loop
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD; i++) {
|
||||
const isLoopInside = service.addAndCheck(
|
||||
createContentEvent(repeatedContent),
|
||||
);
|
||||
expect(isLoopInside).toBe(false);
|
||||
}
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip loop detection when already inside a code block (this.inCodeBlock)', () => {
|
||||
service.reset('');
|
||||
|
||||
// Start with content that puts us inside a code block
|
||||
service.addAndCheck(createContentEvent('Here is some code:\n```\n'));
|
||||
|
||||
// Verify we are now inside a code block and any content should be ignored for loop detection
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD + 5; i++) {
|
||||
const isLoop = service.addAndCheck(createContentEvent(repeatedContent));
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should correctly track inCodeBlock state with multiple fence transitions', () => {
|
||||
service.reset('');
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
|
||||
// Outside code block - should track content
|
||||
service.addAndCheck(createContentEvent('Normal text '));
|
||||
|
||||
// Enter code block (1 fence) - should stop tracking
|
||||
const enterResult = service.addAndCheck(createContentEvent('```\n'));
|
||||
expect(enterResult).toBe(false);
|
||||
|
||||
// Inside code block - should not track loops
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const insideResult = service.addAndCheck(
|
||||
createContentEvent(repeatedContent),
|
||||
);
|
||||
expect(insideResult).toBe(false);
|
||||
}
|
||||
|
||||
// Exit code block (2nd fence) - should reset tracking but still return false
|
||||
const exitResult = service.addAndCheck(createContentEvent('```\n'));
|
||||
expect(exitResult).toBe(false);
|
||||
|
||||
// Enter code block again (3rd fence) - should stop tracking again
|
||||
const reenterResult = service.addAndCheck(
|
||||
createContentEvent('```python\n'),
|
||||
);
|
||||
expect(reenterResult).toBe(false);
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should detect a loop when repetitive content is outside a code block', () => {
|
||||
service.reset('');
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
@@ -281,6 +355,200 @@ describe('LoopDetectionService', () => {
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
it('should reset tracking when a table is detected', () => {
|
||||
service.reset('');
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
service.addAndCheck(createContentEvent(repeatedContent));
|
||||
}
|
||||
|
||||
// This should reset tracking and not trigger a loop
|
||||
service.addAndCheck(createContentEvent('| Column 1 | Column 2 |'));
|
||||
|
||||
// Add more repeated content after table - should not trigger loop
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
const isLoop = service.addAndCheck(createContentEvent(repeatedContent));
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset tracking when a list item is detected', () => {
|
||||
service.reset('');
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
service.addAndCheck(createContentEvent(repeatedContent));
|
||||
}
|
||||
|
||||
// This should reset tracking and not trigger a loop
|
||||
service.addAndCheck(createContentEvent('* List item'));
|
||||
|
||||
// Add more repeated content after list - should not trigger loop
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
const isLoop = service.addAndCheck(createContentEvent(repeatedContent));
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset tracking when a heading is detected', () => {
|
||||
service.reset('');
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
service.addAndCheck(createContentEvent(repeatedContent));
|
||||
}
|
||||
|
||||
// This should reset tracking and not trigger a loop
|
||||
service.addAndCheck(createContentEvent('## Heading'));
|
||||
|
||||
// Add more repeated content after heading - should not trigger loop
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
const isLoop = service.addAndCheck(createContentEvent(repeatedContent));
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset tracking when a blockquote is detected', () => {
|
||||
service.reset('');
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
service.addAndCheck(createContentEvent(repeatedContent));
|
||||
}
|
||||
|
||||
// This should reset tracking and not trigger a loop
|
||||
service.addAndCheck(createContentEvent('> Quote text'));
|
||||
|
||||
// Add more repeated content after blockquote - should not trigger loop
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
const isLoop = service.addAndCheck(createContentEvent(repeatedContent));
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset tracking for various list item formats', () => {
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
|
||||
// Test different list formats - make sure they start at beginning of line
|
||||
const listFormats = [
|
||||
'* Bullet item',
|
||||
'- Dash item',
|
||||
'+ Plus item',
|
||||
'1. Numbered item',
|
||||
'42. Another numbered item',
|
||||
];
|
||||
|
||||
listFormats.forEach((listFormat, index) => {
|
||||
service.reset('');
|
||||
|
||||
// Build up to near threshold
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
service.addAndCheck(createContentEvent(repeatedContent));
|
||||
}
|
||||
|
||||
// Reset should occur with list item - add newline to ensure it starts at beginning
|
||||
service.addAndCheck(createContentEvent('\n' + listFormat));
|
||||
|
||||
// Should not trigger loop after reset - use different content to avoid any cached state issues
|
||||
const newRepeatedContent = createRepetitiveContent(
|
||||
index + 100,
|
||||
CONTENT_CHUNK_SIZE,
|
||||
);
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
const isLoop = service.addAndCheck(
|
||||
createContentEvent(newRepeatedContent),
|
||||
);
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset tracking for various table formats', () => {
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
|
||||
const tableFormats = [
|
||||
'| Column 1 | Column 2 |',
|
||||
'|---|---|',
|
||||
'|++|++|',
|
||||
'+---+---+',
|
||||
];
|
||||
|
||||
tableFormats.forEach((tableFormat, index) => {
|
||||
service.reset('');
|
||||
|
||||
// Build up to near threshold
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
service.addAndCheck(createContentEvent(repeatedContent));
|
||||
}
|
||||
|
||||
// Reset should occur with table format - add newline to ensure it starts at beginning
|
||||
service.addAndCheck(createContentEvent('\n' + tableFormat));
|
||||
|
||||
// Should not trigger loop after reset - use different content to avoid any cached state issues
|
||||
const newRepeatedContent = createRepetitiveContent(
|
||||
index + 200,
|
||||
CONTENT_CHUNK_SIZE,
|
||||
);
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
const isLoop = service.addAndCheck(
|
||||
createContentEvent(newRepeatedContent),
|
||||
);
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reset tracking for various heading levels', () => {
|
||||
const repeatedContent = createRepetitiveContent(1, CONTENT_CHUNK_SIZE);
|
||||
|
||||
const headingFormats = [
|
||||
'# H1 Heading',
|
||||
'## H2 Heading',
|
||||
'### H3 Heading',
|
||||
'#### H4 Heading',
|
||||
'##### H5 Heading',
|
||||
'###### H6 Heading',
|
||||
];
|
||||
|
||||
headingFormats.forEach((headingFormat, index) => {
|
||||
service.reset('');
|
||||
|
||||
// Build up to near threshold
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
service.addAndCheck(createContentEvent(repeatedContent));
|
||||
}
|
||||
|
||||
// Reset should occur with heading - add newline to ensure it starts at beginning
|
||||
service.addAndCheck(createContentEvent('\n' + headingFormat));
|
||||
|
||||
// Should not trigger loop after reset - use different content to avoid any cached state issues
|
||||
const newRepeatedContent = createRepetitiveContent(
|
||||
index + 300,
|
||||
CONTENT_CHUNK_SIZE,
|
||||
);
|
||||
for (let i = 0; i < CONTENT_LOOP_THRESHOLD - 1; i++) {
|
||||
const isLoop = service.addAndCheck(
|
||||
createContentEvent(newRepeatedContent),
|
||||
);
|
||||
expect(isLoop).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
expect(loggers.logLoopDetected).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,6 @@ import { GeminiEventType, ServerGeminiStreamEvent } from '../core/turn.js';
|
||||
import { logLoopDetected } from '../telemetry/loggers.js';
|
||||
import { LoopDetectedEvent, LoopType } from '../telemetry/types.js';
|
||||
import { Config, DEFAULT_GEMINI_FLASH_MODEL } from '../config/config.js';
|
||||
import { SchemaUnion, Type } from '@google/genai';
|
||||
|
||||
const TOOL_CALL_LOOP_THRESHOLD = 5;
|
||||
const CONTENT_LOOP_THRESHOLD = 10;
|
||||
@@ -161,20 +160,26 @@ export class LoopDetectionService {
|
||||
* as repetitive code structures are common and not necessarily loops.
|
||||
*/
|
||||
private checkContentLoop(content: string): boolean {
|
||||
// Code blocks can often contain repetitive syntax that is not indicative of a loop.
|
||||
// To avoid false positives, we detect when we are inside a code block and
|
||||
// temporarily disable loop detection.
|
||||
// Different content elements can often contain repetitive syntax that is not indicative of a loop.
|
||||
// To avoid false positives, we detect when we encounter different content types and
|
||||
// reset tracking to avoid analyzing content that spans across different element boundaries.
|
||||
const numFences = (content.match(/```/g) ?? []).length;
|
||||
if (numFences) {
|
||||
// Reset tracking when a code fence is detected to avoid analyzing content
|
||||
// that spans across code block boundaries.
|
||||
const hasTable = /(^|\n)\s*(\|.*\||[|+-]{3,})/.test(content);
|
||||
const hasListItem =
|
||||
/(^|\n)\s*[*-+]\s/.test(content) || /(^|\n)\s*\d+\.\s/.test(content);
|
||||
const hasHeading = /(^|\n)#+\s/.test(content);
|
||||
const hasBlockquote = /(^|\n)>\s/.test(content);
|
||||
|
||||
if (numFences || hasTable || hasListItem || hasHeading || hasBlockquote) {
|
||||
// Reset tracking when different content elements are detected to avoid analyzing content
|
||||
// that spans across different element boundaries.
|
||||
this.resetContentTracking();
|
||||
}
|
||||
|
||||
const wasInCodeBlock = this.inCodeBlock;
|
||||
this.inCodeBlock =
|
||||
numFences % 2 === 0 ? this.inCodeBlock : !this.inCodeBlock;
|
||||
if (wasInCodeBlock) {
|
||||
if (wasInCodeBlock || this.inCodeBlock) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -335,16 +340,16 @@ Please analyze the conversation history to determine the possibility that the co
|
||||
...recentHistory,
|
||||
{ role: 'user', parts: [{ text: prompt }] },
|
||||
];
|
||||
const schema: SchemaUnion = {
|
||||
type: Type.OBJECT,
|
||||
const schema: Record<string, unknown> = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
reasoning: {
|
||||
type: Type.STRING,
|
||||
type: 'string',
|
||||
description:
|
||||
'Your reasoning on if the conversation is looping without forward progress.',
|
||||
},
|
||||
confidence: {
|
||||
type: Type.NUMBER,
|
||||
type: 'number',
|
||||
description:
|
||||
'A number between 0.0 and 1.0 representing your confidence that the conversation is in an unproductive state.',
|
||||
},
|
||||
|
||||
@@ -185,6 +185,16 @@ describe('ShellExecutionService', () => {
|
||||
expect(result.error).toBe(spawnError);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
|
||||
it('handles errors that do not fire the exit event', async () => {
|
||||
const error = new Error('spawn abc ENOENT');
|
||||
const { result } = await simulateExecution('touch cat.jpg', (cp) => {
|
||||
cp.emit('error', error); // No exit event is fired.
|
||||
});
|
||||
|
||||
expect(result.error).toBe(error);
|
||||
expect(result.exitCode).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Aborting Commands', () => {
|
||||
|
||||
@@ -174,7 +174,19 @@ export class ShellExecutionService {
|
||||
child.stdout.on('data', (data) => handleOutput(data, 'stdout'));
|
||||
child.stderr.on('data', (data) => handleOutput(data, 'stderr'));
|
||||
child.on('error', (err) => {
|
||||
const { stdout, stderr, finalBuffer } = cleanup();
|
||||
error = err;
|
||||
resolve({
|
||||
error,
|
||||
stdout,
|
||||
stderr,
|
||||
rawOutput: finalBuffer,
|
||||
output: stdout + (stderr ? `\n${stderr}` : ''),
|
||||
exitCode: 1,
|
||||
signal: null,
|
||||
aborted: false,
|
||||
pid: child.pid,
|
||||
});
|
||||
});
|
||||
|
||||
const abortHandler = async () => {
|
||||
@@ -200,18 +212,8 @@ export class ShellExecutionService {
|
||||
|
||||
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
||||
|
||||
child.on('exit', (code, signal) => {
|
||||
exited = true;
|
||||
abortSignal.removeEventListener('abort', abortHandler);
|
||||
|
||||
if (stdoutDecoder) {
|
||||
stdout += stripAnsi(stdoutDecoder.decode());
|
||||
}
|
||||
if (stderrDecoder) {
|
||||
stderr += stripAnsi(stderrDecoder.decode());
|
||||
}
|
||||
|
||||
const finalBuffer = Buffer.concat(outputChunks);
|
||||
child.on('exit', (code: number, signal: NodeJS.Signals) => {
|
||||
const { stdout, stderr, finalBuffer } = cleanup();
|
||||
|
||||
resolve({
|
||||
rawOutput: finalBuffer,
|
||||
@@ -225,6 +227,25 @@ export class ShellExecutionService {
|
||||
pid: child.pid,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Cleans up a process (and it's accompanying state) that is exiting or
|
||||
* erroring and returns output formatted output buffers and strings
|
||||
*/
|
||||
function cleanup() {
|
||||
exited = true;
|
||||
abortSignal.removeEventListener('abort', abortHandler);
|
||||
if (stdoutDecoder) {
|
||||
stdout += stripAnsi(stdoutDecoder.decode());
|
||||
}
|
||||
if (stderrDecoder) {
|
||||
stderr += stripAnsi(stderrDecoder.decode());
|
||||
}
|
||||
|
||||
const finalBuffer = Buffer.concat(outputChunks);
|
||||
|
||||
return { stdout, stderr, finalBuffer };
|
||||
}
|
||||
});
|
||||
|
||||
return { pid: child.pid, result };
|
||||
|
||||
Reference in New Issue
Block a user