Files
vscode/extensions/copilot/src/extension/tools/node/codebaseTool.tsx
T
Anisha Agarwal 0af20a790c Search Subagent support (#2736)
* search subagent tool added

* cleaning up description of search subagent

* additional changes

* update linting issue

* Exit early on search subagent call

* search subagent tool added

* cleaning up description of search subagent

* additional changes

* update linting issue

* Add fixes for subagent

* describe read file tool in its prompt

* fixing copilot cli issues?

* resolve merge conflicts with main

* explicit any pt 2

* update explicit any to unknown

* demo

* updating prompt to include description

* fixing newline bug

* added correct input params for subagent

* update to add final turn warning injection

* code snippet hydration

* adding details to toolMetadata (untested)

* commented out until testing

* remove exit from main PR

* actuallly terminate loop

* end loop after round is added and run

* remove early exit handling

* add experiment flags

* update code to check for exp + auto mode

* update to only use gpt 5 mini for search subagent

* Update src/extension/prompts/node/agent/searchSubagentPrompt.tsx

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update docs/tools.md

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update package.nls.json

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* update tests to handle new prompts

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* deleting vestigial file

* fix merge conflict

* update to default to using the main agent model as fallback for subagent, rather than hardcoding gpt4.1

* remove extra whitespace

* remove runSubagent from package.json and update prompt for final snippet clarity

* reset default to false

* add clearer injection prompt

* updating to work with main branch changes

* rewrite search subagent to have its own tool calling loop file + ensure no nested loops with codebase

* remove copilot-added search subagent doc

* update toolResultMessage

* handle exp configuration for search subagent in the right place

* use searchSubagentLoop instead of subagentLoop

* update to be in line with main

* remove CCA agents

* Some minor cleanup

* Update import for ChatToolInvocationPart in SearchSubagentTool

* Replace inSubAgent flag with subAgentInvocationId for tool calling loop checks

---------

Co-authored-by: Anisha Agarwal <anishaagarwal@Anishas-MacBook-Pro.local>
Co-authored-by: Vritant Bhardwaj <vrtoku@gmail.com>
Co-authored-by: root <root@perflens-vm7.e4rbrboag42enkzhvodo1frcqh.xx.internal.cloudapp.net>
Co-authored-by: Anisha Agarwal <anishaagarwal@MacBookPro.hsd1.ma.comcast.net>
Co-authored-by: Zhichao Li <zhichli@microsoft.com>
Co-authored-by: vritant24 <vritoku@gmail.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: bhavyaus <bhavyau@microsoft.com>
2026-01-15 05:59:57 +00:00

178 lines
7.6 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as l10n from '@vscode/l10n';
import { PromptElement, PromptReference, TokenLimit } from '@vscode/prompt-tsx';
import type * as vscode from 'vscode';
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { TelemetryCorrelationId } from '../../../util/common/telemetryCorrelationId';
import { isLocation, isUri } from '../../../util/common/types';
import { CancellationToken } from '../../../util/vs/base/common/cancellation';
import { basename } from '../../../util/vs/base/common/path';
import { URI } from '../../../util/vs/base/common/uri';
import { generateUuid } from '../../../util/vs/base/common/uuid';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString } from '../../../vscodeTypes';
import { ChatVariablesCollection } from '../../prompt/common/chatVariablesCollection';
import { getUniqueReferences } from '../../prompt/common/conversation';
import { IBuildPromptContext } from '../../prompt/common/intents';
import { CodebaseToolCallingLoop } from '../../prompt/node/codebaseToolCalling';
import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer';
import { ToolCallResultWrapper } from '../../prompts/node/panel/toolCalling';
import { WorkspaceContext, WorkspaceContextProps } from '../../prompts/node/panel/workspace/workspaceContext';
import { ToolName } from '../common/toolNames';
import { ToolRegistry } from '../common/toolsRegistry';
import { checkCancellation } from './toolUtils';
export interface ICodebaseToolParams {
query: string;
// Internal parameter only.
includeFileStructure?: boolean;
scopedDirectories?: string[]; // Allows to scope the search to a specific set of directories.
}
export class CodebaseTool implements vscode.LanguageModelTool<ICodebaseToolParams> {
public static readonly toolName = ToolName.Codebase;
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
) { }
async invoke(options: vscode.LanguageModelToolInvocationOptions<ICodebaseToolParams>, token: CancellationToken) {
if (this._input && this._isCodebaseAgentCall(options)) {
const input = this._input;
this._input = undefined; // consumed
return this.invokeCodebaseAgent(input, token);
}
if (!options.input.query) {
throw new Error('Invalid input');
}
checkCancellation(token);
let references: PromptReference[] = [];
const id = generateUuid();
const promptTsxResult = await renderPromptElementJSON(this.instantiationService, WorkspaceContextWrapper, {
telemetryInfo: new TelemetryCorrelationId('codebaseTool', id),
promptContext: {
requestId: id,
chatVariables: new ChatVariablesCollection([]),
query: options.input.query,
history: [],
},
maxResults: 32,
include: {
workspaceChunks: true,
workspaceStructure: options.input.includeFileStructure ?? false
},
scopedDirectories: options.input.scopedDirectories?.map(dir => URI.file(dir)),
referencesOut: references,
isToolCall: true,
lines1Indexed: true,
absolutePaths: true,
priority: 100,
}, undefined, token);
const result = new ExtendedLanguageModelToolResult([
new LanguageModelPromptTsxPart(promptTsxResult)
]);
references = getUniqueReferences(references);
result.toolResultMessage = references.length === 0 ?
new MarkdownString(l10n.t`Searched ${this.getDisplaySearchTarget(options.input)} for "${options.input.query}", no results`) :
references.length === 1 ?
new MarkdownString(l10n.t`Searched ${this.getDisplaySearchTarget(options.input)} for "${options.input.query}", 1 result`) :
new MarkdownString(l10n.t`Searched ${this.getDisplaySearchTarget(options.input)} for "${options.input.query}", ${references.length} results`);
result.toolResultDetails = references
.map(r => r.anchor)
.filter(r => isUri(r) || isLocation(r));
return result;
}
private async invokeCodebaseAgent(input: IBuildPromptContext, token: CancellationToken) {
if (!input.request || !input.conversation) {
throw new Error('Invalid input');
}
const codebaseTool = this.instantiationService.createInstance(CodebaseToolCallingLoop, {
toolCallLimit: 5,
conversation: input.conversation,
request: input.request,
location: input.request.location,
});
const toolCallLoopResult = await codebaseTool.run(undefined, token);
const promptElement = await renderPromptElementJSON(this.instantiationService, ToolCallResultWrapper, { toolCallResults: toolCallLoopResult.toolCallResults });
return { content: [new LanguageModelPromptTsxPart(promptElement)] };
}
private _input: IBuildPromptContext | undefined;
async provideInput(promptContext: IBuildPromptContext): Promise<IBuildPromptContext> {
this._input = promptContext; // TODO@joyceerhl @roblourens HACK: Avoid types in the input being serialized and not deserialized when they go through invokeTool
return promptContext;
}
prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<ICodebaseToolParams>, token: vscode.CancellationToken): vscode.ProviderResult<vscode.PreparedToolInvocation> {
if (this._input && this._isCodebaseAgentCall(options)) {
return {
presentation: 'hidden'
};
}
return {
invocationMessage: new MarkdownString(l10n.t`Searching ${this.getDisplaySearchTarget(options.input)} for "${options.input.query}"`),
};
}
private getDisplaySearchTarget(input: ICodebaseToolParams): string {
let targetSearch;
if (input.scopedDirectories && input.scopedDirectories.length === 1) {
targetSearch = `${basename(input.scopedDirectories[0])}`;
} else if (input.scopedDirectories && input.scopedDirectories.length > 1) {
targetSearch = l10n.t("{0} directories", input.scopedDirectories.length);
} else {
targetSearch = l10n.t("codebase");
}
return targetSearch;
}
private _isCodebaseAgentCall(options: vscode.LanguageModelToolInvocationPrepareOptions<ICodebaseToolParams> | vscode.LanguageModelToolInvocationOptions<ICodebaseToolParams>): boolean {
const input = options.input;
const agentEnabled = this.configurationService.getConfig(ConfigKey.CodeSearchAgentEnabled);
const noScopedDirectories = input.scopedDirectories === undefined || input.scopedDirectories.length === 0;
// When anonymous (no GitHub session), always force agent path so we avoid relying on semantic index features.
const isAnonymous = !this.authenticationService.anyGitHubSession;
// Don't trigger nested tool calling loop if we're already in a subagent
if (this._input?.tools?.subAgentInvocationId) {
return false;
}
return (isAnonymous || agentEnabled) && noScopedDirectories;
}
}
ToolRegistry.registerTool(CodebaseTool);
class WorkspaceContextWrapper extends PromptElement<WorkspaceContextProps> {
constructor(
props: WorkspaceContextProps,
) {
super(props);
}
render() {
// Main limit is set via maxChunks. Set a TokenLimit just to be sure.
return <TokenLimit max={28_000}>
<WorkspaceContext {...this.props} />
</TokenLimit>;
}
}