mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
feat(core): add timeout to llm edit fix (#12393)
This commit is contained in:
@@ -426,6 +426,18 @@ class EditToolInvocation
|
|||||||
abortSignal,
|
abortSignal,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If the self-correction attempt timed out, return the original error.
|
||||||
|
if (fixedEdit === null) {
|
||||||
|
return {
|
||||||
|
currentContent: contentForLlmEditFixer,
|
||||||
|
newContent: currentContent,
|
||||||
|
occurrences: 0,
|
||||||
|
isNewFile: false,
|
||||||
|
error: initialError,
|
||||||
|
originalLineEnding,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (fixedEdit.noChangesRequired) {
|
if (fixedEdit.noChangesRequired) {
|
||||||
return {
|
return {
|
||||||
currentContent,
|
currentContent,
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ describe('FixLLMEditWithInstruction', () => {
|
|||||||
const abortSignal = abortController.signal;
|
const abortSignal = abortController.signal;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.useFakeTimers();
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
resetLlmEditFixerCaches_TEST_ONLY(); // Ensure cache is cleared before each test
|
resetLlmEditFixerCaches_TEST_ONLY(); // Ensure cache is cleared before each test
|
||||||
});
|
});
|
||||||
@@ -319,4 +320,46 @@ describe('FixLLMEditWithInstruction', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it(
|
||||||
|
'should return null if the LLM call times out',
|
||||||
|
{ timeout: 60000 },
|
||||||
|
async () => {
|
||||||
|
mockGenerateJson.mockImplementation(
|
||||||
|
async ({ abortSignal }) =>
|
||||||
|
// Simulate a long-running operation that never resolves on its own.
|
||||||
|
// It will only reject when the abort signal is triggered by the timeout.
|
||||||
|
new Promise((_resolve, reject) => {
|
||||||
|
if (abortSignal?.aborted) {
|
||||||
|
return reject(new DOMException('Aborted', 'AbortError'));
|
||||||
|
}
|
||||||
|
abortSignal?.addEventListener('abort', () => {
|
||||||
|
reject(new DOMException('Aborted', 'AbortError'));
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const testPromptId = 'test-prompt-id-timeout';
|
||||||
|
|
||||||
|
const fixPromise = promptIdContext.run(testPromptId, () =>
|
||||||
|
FixLLMEditWithInstruction(
|
||||||
|
instruction,
|
||||||
|
old_string,
|
||||||
|
new_string,
|
||||||
|
error,
|
||||||
|
current_content,
|
||||||
|
mockBaseLlmClient,
|
||||||
|
abortSignal,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Let the timers advance just past the 40000ms default timeout.
|
||||||
|
await vi.advanceTimersByTimeAsync(40001);
|
||||||
|
|
||||||
|
const result = await fixPromise;
|
||||||
|
|
||||||
|
expect(result).toBeNull();
|
||||||
|
expect(mockGenerateJson).toHaveBeenCalledOnce();
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { promptIdContext } from './promptIdContext.js';
|
|||||||
import { debugLogger } from './debugLogger.js';
|
import { debugLogger } from './debugLogger.js';
|
||||||
|
|
||||||
const MAX_CACHE_SIZE = 50;
|
const MAX_CACHE_SIZE = 50;
|
||||||
|
const GENERATE_JSON_TIMEOUT_MS = 40000; // 40 seconds
|
||||||
|
|
||||||
const EDIT_SYS_PROMPT = `
|
const EDIT_SYS_PROMPT = `
|
||||||
You are an expert code-editing assistant specializing in debugging and correcting failed search-and-replace operations.
|
You are an expert code-editing assistant specializing in debugging and correcting failed search-and-replace operations.
|
||||||
@@ -89,6 +90,32 @@ const editCorrectionWithInstructionCache = new LruCache<
|
|||||||
SearchReplaceEdit
|
SearchReplaceEdit
|
||||||
>(MAX_CACHE_SIZE);
|
>(MAX_CACHE_SIZE);
|
||||||
|
|
||||||
|
async function generateJsonWithTimeout<T>(
|
||||||
|
client: BaseLlmClient,
|
||||||
|
params: Parameters<BaseLlmClient['generateJson']>[0],
|
||||||
|
timeoutMs: number,
|
||||||
|
): Promise<T | null> {
|
||||||
|
try {
|
||||||
|
// Create a signal that aborts automatically after the specified timeout.
|
||||||
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
||||||
|
|
||||||
|
const result = await client.generateJson({
|
||||||
|
...params,
|
||||||
|
// The operation will be aborted if either the original signal is aborted
|
||||||
|
// or if the timeout is reached.
|
||||||
|
abortSignal: AbortSignal.any([
|
||||||
|
params.abortSignal ?? new AbortController().signal,
|
||||||
|
timeoutSignal,
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
return result as T;
|
||||||
|
} catch (_err) {
|
||||||
|
// An AbortError will be thrown on timeout.
|
||||||
|
// We catch it and return null to signal that the operation timed out.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to fix a failed edit by using an LLM to generate a new search and replace pair.
|
* Attempts to fix a failed edit by using an LLM to generate a new search and replace pair.
|
||||||
* @param instruction The instruction for what needs to be done.
|
* @param instruction The instruction for what needs to be done.
|
||||||
@@ -109,7 +136,7 @@ export async function FixLLMEditWithInstruction(
|
|||||||
current_content: string,
|
current_content: string,
|
||||||
baseLlmClient: BaseLlmClient,
|
baseLlmClient: BaseLlmClient,
|
||||||
abortSignal: AbortSignal,
|
abortSignal: AbortSignal,
|
||||||
): Promise<SearchReplaceEdit> {
|
): Promise<SearchReplaceEdit | null> {
|
||||||
let promptId = promptIdContext.getStore();
|
let promptId = promptIdContext.getStore();
|
||||||
if (!promptId) {
|
if (!promptId) {
|
||||||
promptId = `llm-fixer-fallback-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
promptId = `llm-fixer-fallback-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||||
@@ -146,17 +173,23 @@ export async function FixLLMEditWithInstruction(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const result = (await baseLlmClient.generateJson({
|
const result = await generateJsonWithTimeout<SearchReplaceEdit>(
|
||||||
contents,
|
baseLlmClient,
|
||||||
schema: SearchReplaceEditSchema,
|
{
|
||||||
abortSignal,
|
contents,
|
||||||
model: DEFAULT_GEMINI_FLASH_MODEL,
|
schema: SearchReplaceEditSchema,
|
||||||
systemInstruction: EDIT_SYS_PROMPT,
|
abortSignal,
|
||||||
promptId,
|
model: DEFAULT_GEMINI_FLASH_MODEL,
|
||||||
maxAttempts: 1,
|
systemInstruction: EDIT_SYS_PROMPT,
|
||||||
})) as unknown as SearchReplaceEdit;
|
promptId,
|
||||||
|
maxAttempts: 1,
|
||||||
|
},
|
||||||
|
GENERATE_JSON_TIMEOUT_MS,
|
||||||
|
);
|
||||||
|
|
||||||
editCorrectionWithInstructionCache.set(cacheKey, result);
|
if (result) {
|
||||||
|
editCorrectionWithInstructionCache.set(cacheKey, result);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user