diff --git a/extensions/copilot/src/extension/xtab/common/promptCrafting.ts b/extensions/copilot/src/extension/xtab/common/promptCrafting.ts index 55f6c53a25c..9af66e630ff 100644 --- a/extensions/copilot/src/extension/xtab/common/promptCrafting.ts +++ b/extensions/copilot/src/extension/xtab/common/promptCrafting.ts @@ -367,9 +367,10 @@ function appendWithNewLineIfNeeded(base: string, toAppend: string, minNewLines: return (base + '\n'.repeat(newLinesToAdd) + toAppend).trim(); } -function getPostScript(opts: PromptOptions, currentFilePath: string, aggressivenessLevel: AggressivenessLevel) { - const { promptingStrategy } = opts; +function getPostScript(options: PromptOptions, currentFilePath: string, aggressivenessLevel: AggressivenessLevel) { + const { promptingStrategy, eagernessPrompt } = options; const xtab275BasePostScript = `The developer was working on a section of code within the tags \`code_to_edit\` in the file located at \`${currentFilePath}\`. Using the given \`recently_viewed_code_snippets\`, \`current_file_content\`, \`edit_diff_history\`, \`area_around_code_to_edit\`, and the cursor position marked as \`${PromptTags.CURSOR}\`, please continue the developer's work. Update the \`code_to_edit\` section by predicting and completing the changes they would have made next. Provide the revised code that was between the \`${PromptTags.EDIT_WINDOW.start}\` and \`${PromptTags.EDIT_WINDOW.end}\` tags, but do not include the tags themselves. Avoid undoing or reverting the developer's last change unless there are obvious typos or errors. Don't include the line numbers or the form #| in your response. Do not skip any lines. Do not be lazy.`; + const patchBased02PostScript = `The developer was working on a section of code within the \`current_file_content\` - carefully note their \`cursor_location\` marked with \`<|cursor|>\`. Using the given \`recently_viewed_code_snippets\`, \`current_file_content\`, \`edit_diff_history\`, and \`cursor_location\`, please continue the developer's work. Output a modified diff format with a sequence of intuitive next changes, where each patch must start with \`:\`. Order changes by priority and flow; for instance, edits adjacent to the user's cursor should always be prioritized, followed by lines near the cursor, followed by lines farther away. If there are no good edit candidates, output the empty string "". Avoid undoing or reverting the developer's last change unless there are obvious typos or errors. Adhere meticulously to the diff format.`; let postScript: string | undefined; switch (promptingStrategy) { @@ -377,11 +378,13 @@ function getPostScript(opts: PromptOptions, currentFilePath: string, aggressiven case PromptingStrategy.Codexv21NesUnified: break; case PromptingStrategy.PatchBased02: - postScript = `The developer was working on a section of code within the \`current_file_content\` - carefully note their \`cursor_location\` marked with \`<|cursor|>\`. Using the given \`recently_viewed_code_snippets\`, \`current_file_content\`, \`edit_diff_history\`, and \`cursor_location\`, please continue the developer's work. Output a modified diff format with a sequence of intuitive next changes, where each patch must start with \`:\`. Order changes by priority and flow; for instance, edits adjacent to the user's cursor should always be prioritized, followed by lines near the cursor, followed by lines farther away. If there are no good edit candidates, output the empty string "". Avoid undoing or reverting the developer's last change unless there are obvious typos or errors. Adhere meticulously to the diff format.`; - break; case PromptingStrategy.PatchBased02WithRecentLineNumbers: case PromptingStrategy.PatchBased02WithoutRecentLineNumbers: - postScript = `The developer was working on a section of code within the \`current_file_content\` - carefully note their \`cursor_location\` marked with \`<|cursor|>\`. Using the given \`recently_viewed_code_snippets\`, \`current_file_content\`, \`edit_diff_history\`, and \`cursor_location\`, please continue the developer's work. Output a modified diff format with a sequence of intuitive next changes, where each patch must start with \`:\`. Order changes by priority and flow; for instance, edits adjacent to the user's cursor should always be prioritized, followed by lines near the cursor, followed by lines farther away. If there are no good edit candidates, output the empty string "". Avoid undoing or reverting the developer's last change unless there are obvious typos or errors. Adhere meticulously to the diff format.`; + postScript = eagernessPrompt === 'aggressionHighLow' + ? aggressivenessLevel === AggressivenessLevel.Medium + ? patchBased02PostScript + : `<|aggression|>${aggressivenessLevel}<|/aggression|>\n\n${patchBased02PostScript}` + : patchBased02PostScript; break; case PromptingStrategy.UnifiedModel: postScript = `The developer was working on a section of code within the tags \`code_to_edit\` in the file located at \`${currentFilePath}\`. Using the given \`recently_viewed_code_snippets\`, \`current_file_content\`, \`edit_diff_history\`, \`area_around_code_to_edit\`, and the cursor position marked as \`${PromptTags.CURSOR}\`, please continue the developer's work. Update the \`code_to_edit\` section by predicting and completing the changes they would have made next. Start your response with , , or . If you are making an edit, start with and then provide the rewritten code window followed by . If you are inserting new code, start with and then provide only the new code that will be inserted at the cursor position followed by . If no changes are necessary, reply only with . Avoid undoing or reverting the developer's last change unless there are obvious typos or errors.`; diff --git a/extensions/copilot/src/extension/xtab/node/xtabProvider.ts b/extensions/copilot/src/extension/xtab/node/xtabProvider.ts index daf5b42d560..5a7e8095f2e 100644 --- a/extensions/copilot/src/extension/xtab/node/xtabProvider.ts +++ b/extensions/copilot/src/extension/xtab/node/xtabProvider.ts @@ -19,7 +19,7 @@ import { LanguageContextEntry, LanguageContextResponse } from '../../../platform import { LanguageId } from '../../../platform/inlineEdits/common/dataTypes/languageId'; import { NextCursorLinePrediction } from '../../../platform/inlineEdits/common/dataTypes/nextCursorLinePrediction'; import * as xtabPromptOptions from '../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions'; -import { AggressivenessSetting, EarlyDivergenceCancellationMode, isAggressivenessStrategy, LanguageContextLanguages, LanguageContextOptions } from '../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions'; +import { AggressivenessSetting, EarlyDivergenceCancellationMode, isEagernessPrompt, LanguageContextLanguages, LanguageContextOptions } from '../../../platform/inlineEdits/common/dataTypes/xtabPromptOptions'; import { InlineEditRequestLogContext } from '../../../platform/inlineEdits/common/inlineEditLogContext'; import { IInlineEditsModelService } from '../../../platform/inlineEdits/common/inlineEditsModelService'; import { ResponseProcessor } from '../../../platform/inlineEdits/common/responseProcessor'; @@ -617,7 +617,7 @@ export class XtabProvider implements IStatelessNextEditProvider { } // Adjust debounce based on user aggressiveness setting for non-aggressiveness models - if (!isAggressivenessStrategy(promptOptions.promptingStrategy)) { + if (!isEagernessPrompt(promptOptions)) { this._applyAggressivenessSettings(delaySession, tracer); } } @@ -1574,6 +1574,7 @@ export class XtabProvider implements IStatelessNextEditProvider { }, memory: undefined, lintOptions: undefined, + eagernessPrompt: undefined, includePostScript: true, globalBudget: this.getGlobalBudget(), }; @@ -1782,6 +1783,7 @@ export function overrideModelConfig(modelConfig: ModelConfig, overridingConfig: ...modelConfig, modelName: overridingConfig.modelName, promptingStrategy: overridingConfig.promptingStrategy, + eagernessPrompt: overridingConfig.eagernessPrompt ?? modelConfig.eagernessPrompt, includePostScript: overridingConfig.includePostScript ?? modelConfig.includePostScript, currentFile: { ...modelConfig.currentFile, diff --git a/extensions/copilot/src/extension/xtab/test/common/promptCrafting.spec.ts b/extensions/copilot/src/extension/xtab/test/common/promptCrafting.spec.ts index ef4646c7a99..560041892d3 100644 --- a/extensions/copilot/src/extension/xtab/test/common/promptCrafting.spec.ts +++ b/extensions/copilot/src/extension/xtab/test/common/promptCrafting.spec.ts @@ -614,6 +614,7 @@ describe('getUserPrompt', () => { strategy: PromptingStrategy | undefined; includeLineNumbers?: IncludeLineNumbersOption; includePostScript?: boolean; + eagernessPrompt?: 'aggressionHighLow'; aggressivenessLevel?: AggressivenessLevel; rejectedEditsMemory?: RejectedEditsMemoryMode; }): PromptPieces { @@ -635,6 +636,7 @@ describe('getUserPrompt', () => { const promptOptions: PromptOptions = { ...DEFAULT_OPTIONS, promptingStrategy: opts.strategy, + eagernessPrompt: opts.eagernessPrompt, ...(opts.includePostScript !== undefined ? { includePostScript: opts.includePostScript } : {}), ...(opts.rejectedEditsMemory !== undefined ? { memory: { rejectedEdits: opts.rejectedEditsMemory } } : {}), currentFile: { @@ -816,6 +818,32 @@ describe('getUserPrompt', () => { expect(prompt).toContain(PromptTags.CURSOR_LOCATION.start + '\n' + ' const ' + PromptTags.CURSOR + 'x = 1;' + '\n' + PromptTags.CURSOR_LOCATION.end); }); + test.each([ + [AggressivenessLevel.Medium, ''], + [AggressivenessLevel.High, '<|aggression|>high<|/aggression|>'], + [AggressivenessLevel.Low, '<|aggression|>low<|/aggression|>'], + ])('PatchBased02 aggression prompt places the %s tag before the postscript', (aggressivenessLevel, aggressionTag) => { + const pieces = createTestPromptPieces({ + cursorLine: 2, + cursorColumn: 9, + strategy: PromptingStrategy.PatchBased02, + eagernessPrompt: 'aggressionHighLow', + aggressivenessLevel, + }); + const { prompt } = getUserPrompt(pieces); + + const cursorLocation = `${PromptTags.CURSOR_LOCATION.start}\n const ${PromptTags.CURSOR}x = 1;\n${PromptTags.CURSOR_LOCATION.end}`; + const postScript = 'The developer was working on a section of code within the `current_file_content`'; + expect(prompt).toContain(cursorLocation); + expect(prompt.indexOf(cursorLocation)).toBeLessThan(prompt.indexOf(postScript)); + if (aggressivenessLevel === AggressivenessLevel.Medium) { + expect(prompt).not.toContain('<|aggression|>'); + expect(prompt).toContain(`${PromptTags.CURSOR_LOCATION.end}\n\n${postScript}`); + } else { + expect(prompt).toContain(`${PromptTags.CURSOR_LOCATION.end}\n\n${aggressionTag}\n\n${postScript}`); + } + }); + describe('Xtab275AggressivenessHighLow', () => { test('medium level does not include aggressive tag', () => { const pieces = createTestPromptPieces({ diff --git a/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts b/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts index eb48769041f..ba571126085 100644 --- a/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts +++ b/extensions/copilot/src/platform/inlineEdits/common/dataTypes/xtabPromptOptions.ts @@ -492,6 +492,8 @@ export namespace EditIntent { } } +export type EagernessPrompt = 'aggressionHighLow'; + export type PromptOptions = { readonly promptingStrategy: PromptingStrategy | undefined /* default */; readonly currentFile: CurrentFileOptions; @@ -503,6 +505,7 @@ export type PromptOptions = { readonly memory: PromptMemoryOptions | undefined; readonly includePostScript: boolean; readonly lintOptions: LintOptions | undefined; + readonly eagernessPrompt: EagernessPrompt | undefined; /** * When set, parts share a single pool of `totalTokens` and unused budget from * earlier parts in `order` cascades to later parts. When `undefined`, each @@ -559,12 +562,21 @@ export function isPromptingStrategy(value: string): value is PromptingStrategy { return (Object.values(PromptingStrategy) as string[]).includes(value); } -export function isAggressivenessStrategy(strategy: PromptingStrategy | undefined): boolean { - return strategy === PromptingStrategy.XtabAggressiveness - || strategy === PromptingStrategy.Xtab275Aggressiveness - || strategy === PromptingStrategy.Xtab275AggressivenessHighLow - || strategy === PromptingStrategy.Xtab275EditIntent - || strategy === PromptingStrategy.Xtab275EditIntentShort; +export function isEagernessPrompt(options: PromptOptions): boolean { + if (options.promptingStrategy === undefined) { + return false; + } + return (options.eagernessPrompt !== undefined && [ + PromptingStrategy.PatchBased02, + PromptingStrategy.PatchBased02WithRecentLineNumbers, + PromptingStrategy.PatchBased02WithoutRecentLineNumbers, + ].includes(options.promptingStrategy)) // eagerness prompt option is only supported for patch-based strategies + || [PromptingStrategy.XtabAggressiveness, + PromptingStrategy.Xtab275Aggressiveness, + PromptingStrategy.Xtab275AggressivenessHighLow, + PromptingStrategy.Xtab275EditIntent, + PromptingStrategy.Xtab275EditIntentShort, + ].includes(options.promptingStrategy); // first-class aggressiveness strategies } export function isRejectedEditMemoryEnabled(options: { readonly memory?: PromptMemoryOptions }): boolean { @@ -614,6 +626,7 @@ export namespace ResponseFormat { export const DEFAULT_OPTIONS: PromptOptions = { promptingStrategy: undefined, + eagernessPrompt: undefined, currentFile: { maxTokens: 1500, includeTags: true, @@ -673,6 +686,7 @@ export const LANGUAGE_CONTEXT_ENABLED_LANGUAGES: LanguageContextLanguages = { export interface ModelConfiguration { modelName: string; promptingStrategy: PromptingStrategy | undefined /* default */; + eagernessPrompt?: EagernessPrompt; includeTagsInCurrentFile: boolean; includePostScript?: boolean; currentFile?: Partial; @@ -742,6 +756,7 @@ export const LINT_OPTIONS_VALIDATOR: IValidator> = vObj({ export const MODEL_CONFIGURATION_VALIDATOR: IValidator = vObj({ 'modelName': vRequired(vString()), 'promptingStrategy': vUnion(vEnum(...Object.values(PromptingStrategy)), vUndefined()), + 'eagernessPrompt': vUnion(vEnum('aggressionHighLow'), vUndefined()), 'includeTagsInCurrentFile': vRequired(vBoolean()), 'includePostScript': vUnion(vBoolean(), vUndefined()), 'currentFile': vUnion(CurrentFileOptions.VALIDATOR, vUndefined()), diff --git a/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts b/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts index 76cf202b1cb..d6a6d7f41a3 100644 --- a/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts +++ b/extensions/copilot/src/platform/inlineEdits/test/common/xtabPromptOptions.spec.ts @@ -5,7 +5,7 @@ import { describe, expect, it } from 'vitest'; import { ImportChanges } from '../../common/dataTypes/importFilteringOptions'; -import { applyStrategyConfig, DEFAULT_OPTIONS, GlobalBudgetOptions, IncludeLineNumbersOption, MODEL_CONFIGURATION_VALIDATOR, ModelConfiguration, PromptingStrategy, RejectedEditsMemoryMode } from '../../common/dataTypes/xtabPromptOptions'; +import { applyStrategyConfig, DEFAULT_OPTIONS, GlobalBudgetOptions, IncludeLineNumbersOption, isEagernessPrompt, MODEL_CONFIGURATION_VALIDATOR, ModelConfiguration, PromptingStrategy, RejectedEditsMemoryMode } from '../../common/dataTypes/xtabPromptOptions'; function baseConfig(overrides: Partial = {}): ModelConfiguration { return { @@ -85,6 +85,12 @@ describe('applyStrategyConfig', () => { describe('MODEL_CONFIGURATION_VALIDATOR', () => { + it('accepts a config with eagernessPrompt', () => { + const result = MODEL_CONFIGURATION_VALIDATOR.validate(baseConfig({ eagernessPrompt: 'aggressionHighLow' })); + expect(result.error).toBeUndefined(); + expect(result.content?.eagernessPrompt).toBe('aggressionHighLow'); + }); + it('keeps rejected-edit memory off by default', () => { expect(DEFAULT_OPTIONS.memory).toBeUndefined(); expect(MODEL_CONFIGURATION_VALIDATOR.validate(baseConfig()).content?.memory).toBeUndefined(); @@ -114,6 +120,13 @@ describe('MODEL_CONFIGURATION_VALIDATOR', () => { }); }); +describe('isEagernessPrompt', () => { + it('recognizes the PatchBased02 aggression prompt option', () => { + expect(isEagernessPrompt({ ...DEFAULT_OPTIONS, promptingStrategy: PromptingStrategy.PatchBased02, eagernessPrompt: 'aggressionHighLow' })).toBe(true); + expect(isEagernessPrompt({ ...DEFAULT_OPTIONS, promptingStrategy: PromptingStrategy.PatchBased02 })).toBe(false); + }); +}); + describe('GlobalBudgetOptions', () => { function gb(overrides: Partial = {}): GlobalBudgetOptions {