Skip to content

Commit 018376a

Browse files
authored
Add execute_prompt and execute_task tools (#858)
* Try execute_prompt tool * Clean up * fix * Add execute_task and execute_prompt tools Towards microsoft/vscode#263917
1 parent cba1ac2 commit 018376a

File tree

9 files changed

+341
-6
lines changed

9 files changed

+341
-6
lines changed

package.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,53 @@
163163
]
164164
}
165165
},
166+
{
167+
"name": "execute_task",
168+
"toolReferenceName": "executeTask",
169+
"displayName": "Execute Task",
170+
"when": "config.github.copilot.chat.advanced.taskTools.enabled",
171+
"canBeReferencedInPrompt": true,
172+
"modelDescription": "Launch a new agent to handle complex, multi-step tasks autonomously. This tool is good at researching complex questions, searching for code, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries, use this agent to perform the search for you.\n\n- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n - Each agent invocation is stateless. You will not be able to send additional messages to the agent, nor will the agent be able to communicate with you outside of its final report. Therefore, your prompt should contain a highly detailed task description for the agent to perform autonomously and you should specify exactly what information the agent should return back to you in its final and only message to you.\n - The agent's outputs should generally be trusted\n - Clearly tell the agent whether you expect it to write code or just to do research (search, file reads, web fetches, etc.), since it is not aware of the user's intent",
173+
"tags": [],
174+
"inputSchema": {
175+
"type": "object",
176+
"properties": {
177+
"prompt": {
178+
"type": "string",
179+
"description": "A detailed description of the task for the agent to perform"
180+
},
181+
"description": {
182+
"type": "string",
183+
"description": "A short (3-5 word) description of the task"
184+
}
185+
},
186+
"required": [
187+
"prompt",
188+
"description"
189+
]
190+
}
191+
},
192+
{
193+
"name": "execute_prompt",
194+
"toolReferenceName": "executePrompt",
195+
"displayName": "Execute Prompt",
196+
"when": "config.github.copilot.chat.advanced.taskTools.enabled",
197+
"canBeReferencedInPrompt": true,
198+
"modelDescription": "This tool can take a path to a user's prompt file as input, and execute it autonomously. If the user's prompt includes multiple references to .prompt.md files, then you should use this tool to execute those prompts in sequence.\n\n- When the agent is done, it will return a single message back to you. The result returned by the agent is not visible to the user. To show the user the result, you should send a text message back to the user with a concise summary of the result.\n - The agent's outputs should generally be trusted",
199+
"tags": [],
200+
"inputSchema": {
201+
"type": "object",
202+
"properties": {
203+
"filePath": {
204+
"type": "string",
205+
"description": "The absolute path to the prompt file to execute"
206+
}
207+
},
208+
"required": [
209+
"filePath"
210+
]
211+
}
212+
},
166213
{
167214
"name": "copilot_searchWorkspaceSymbols",
168215
"toolReferenceName": "symbols",

src/extension/intents/node/agentIntent.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { addCacheBreakpoints } from './cacheBreakpoints';
4949
import { EditCodeIntent, EditCodeIntentInvocation, EditCodeIntentInvocationOptions, mergeMetadata, toNewChatReferences } from './editCodeIntent';
5050
import { getRequestedToolCallIterationLimit, IContinueOnErrorConfirmation } from './toolCallingLoop';
5151

52-
const getTools = (instaService: IInstantiationService, request: vscode.ChatRequest) =>
52+
export const getAgentTools = (instaService: IInstantiationService, request: vscode.ChatRequest) =>
5353
instaService.invokeFunction(async accessor => {
5454
const toolsService = accessor.get<IToolsService>(IToolsService);
5555
const testService = accessor.get<ITestProvider>(ITestProvider);
@@ -144,7 +144,7 @@ export class AgentIntent extends EditCodeIntent {
144144
}
145145

146146
private async listTools(conversation: Conversation, request: vscode.ChatRequest, stream: vscode.ChatResponseStream, token: CancellationToken) {
147-
const editingTools = await getTools(this.instantiationService, request);
147+
const editingTools = await getAgentTools(this.instantiationService, request);
148148
const grouping = this._toolGroupingService.create(conversation.sessionId, editingTools);
149149
if (!grouping.isEnabled) {
150150
stream.markdown(`Available tools: \n${editingTools.map(tool => `- ${tool.name}`).join('\n')}\n`);
@@ -226,7 +226,7 @@ export class AgentIntentInvocation extends EditCodeIntentInvocation {
226226
}
227227

228228
public override getAvailableTools(): Promise<vscode.LanguageModelToolInformation[]> {
229-
return getTools(this.instantiationService, this.request);
229+
return getAgentTools(this.instantiationService, this.request);
230230
}
231231

232232
override async buildPrompt(
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { randomUUID } from 'crypto';
7+
import type { CancellationToken, ChatRequest, LanguageModelToolInformation, Progress } from 'vscode';
8+
import { IAuthenticationChatUpgradeService } from '../../../platform/authentication/common/authenticationUpgrade';
9+
import { ChatLocation, ChatResponse } from '../../../platform/chat/common/commonTypes';
10+
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
11+
import { ILogService } from '../../../platform/log/common/logService';
12+
import { IRequestLogger } from '../../../platform/requestLogger/node/requestLogger';
13+
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
14+
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
15+
import { ChatResponseProgressPart, ChatResponseReferencePart } from '../../../vscodeTypes';
16+
import { getAgentTools } from '../../intents/node/agentIntent';
17+
import { IToolCallingLoopOptions, ToolCallingLoop, ToolCallingLoopFetchOptions } from '../../intents/node/toolCallingLoop';
18+
import { AgentPrompt } from '../../prompts/node/agent/agentPrompt';
19+
import { PromptRenderer } from '../../prompts/node/base/promptRenderer';
20+
import { ToolName } from '../../tools/common/toolNames';
21+
import { IBuildPromptContext } from '../common/intents';
22+
import { IBuildPromptResult } from './intents';
23+
24+
export interface IExecutePromptToolCallingLoopOptions extends IToolCallingLoopOptions {
25+
request: ChatRequest;
26+
location: ChatLocation;
27+
promptText: string;
28+
}
29+
30+
export class ExecutePromptToolCallingLoop extends ToolCallingLoop<IExecutePromptToolCallingLoopOptions> {
31+
32+
public static readonly ID = 'executePromptTool';
33+
34+
constructor(
35+
options: IExecutePromptToolCallingLoopOptions,
36+
@IInstantiationService private readonly instantiationService: IInstantiationService,
37+
@ILogService logService: ILogService,
38+
@IRequestLogger requestLogger: IRequestLogger,
39+
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
40+
@IAuthenticationChatUpgradeService authenticationChatUpgradeService: IAuthenticationChatUpgradeService,
41+
@ITelemetryService telemetryService: ITelemetryService,
42+
) {
43+
super(options, instantiationService, endpointProvider, logService, requestLogger, authenticationChatUpgradeService, telemetryService);
44+
}
45+
46+
private async getEndpoint(request: ChatRequest) {
47+
let endpoint = await this.endpointProvider.getChatEndpoint(this.options.request);
48+
if (!endpoint.supportsToolCalls) {
49+
endpoint = await this.endpointProvider.getChatEndpoint('gpt-4.1');
50+
}
51+
return endpoint;
52+
}
53+
54+
protected async buildPrompt(buildPromptContext: IBuildPromptContext, progress: Progress<ChatResponseReferencePart | ChatResponseProgressPart>, token: CancellationToken): Promise<IBuildPromptResult> {
55+
const endpoint = await this.getEndpoint(this.options.request);
56+
const promptContext: IBuildPromptContext = {
57+
...buildPromptContext,
58+
query: this.options.promptText,
59+
conversation: undefined
60+
};
61+
const renderer = PromptRenderer.create(
62+
this.instantiationService,
63+
endpoint,
64+
AgentPrompt,
65+
{
66+
endpoint,
67+
promptContext,
68+
location: this.options.location,
69+
enableCacheBreakpoints: false,
70+
}
71+
);
72+
return await renderer.render(progress, token);
73+
}
74+
75+
protected async getAvailableTools(): Promise<LanguageModelToolInformation[]> {
76+
const excludedTools = new Set([ToolName.ExecutePrompt, ToolName.ExecuteTask, ToolName.CoreManageTodoList]);
77+
return (await getAgentTools(this.instantiationService, this.options.request))
78+
.filter(tool => !excludedTools.has(tool.name as ToolName))
79+
// TODO can't do virtual tools at this level
80+
.slice(0, 128);
81+
}
82+
83+
protected async fetch({ messages, finishedCb, requestOptions }: ToolCallingLoopFetchOptions, token: CancellationToken): Promise<ChatResponse> {
84+
const endpoint = await this.getEndpoint(this.options.request);
85+
return endpoint.makeChatRequest(
86+
ExecutePromptToolCallingLoop.ID,
87+
messages,
88+
finishedCb,
89+
token,
90+
this.options.location,
91+
undefined,
92+
{
93+
...requestOptions,
94+
temperature: 0
95+
},
96+
// This loop is inside a tool called from another request, so never user initiated
97+
false,
98+
{
99+
messageId: randomUUID(),
100+
messageSource: ExecutePromptToolCallingLoop.ID
101+
},
102+
);
103+
}
104+
}

src/extension/tools/common/toolNames.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,9 @@ export enum ToolName {
5252
CoreGetTaskOutput = 'get_task_output',
5353
CoreRunTest = 'runTests',
5454
ToolReplay = 'tool_replay',
55-
EditFilesPlaceholder = 'edit_files'
55+
EditFilesPlaceholder = 'edit_files',
56+
ExecutePrompt = 'execute_prompt',
57+
ExecuteTask = 'execute_task',
5658
}
5759

5860
export enum ContributedToolName {
@@ -95,7 +97,9 @@ export enum ContributedToolName {
9597
CreateDirectory = 'copilot_createDirectory',
9698
RunVscodeCmd = 'copilot_runVscodeCommand',
9799
ToolReplay = 'copilot_toolReplay',
98-
EditFilesPlaceholder = 'copilot_editFiles'
100+
EditFilesPlaceholder = 'copilot_editFiles',
101+
ExecutePrompt = 'execute_prompt',
102+
ExecuteTask = 'execute_task',
99103
}
100104

101105
const toolNameToContributedToolNames = new Map<ToolName, ContributedToolName>();

src/extension/tools/node/allTools.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import './createDirectoryTool';
99
import './createFileTool';
1010
import './docTool';
1111
import './editNotebookTool';
12+
import './executePromptTool';
13+
import './executeTaskTool';
1214
import './findFilesTool';
1315
import './findTestsFilesTool';
1416
import './findTextInFilesTool';
@@ -38,4 +40,3 @@ import './userPreferencesTool';
3840
import './vscodeAPITool';
3941
import './vscodeCmdTool';
4042
import './toolReplayTool';
41-
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as l10n from '@vscode/l10n';
7+
import type * as vscode from 'vscode';
8+
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
9+
import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
10+
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
11+
import { ChatResponseMarkdownPart, ExtendedLanguageModelToolResult, LanguageModelTextPart, MarkdownString } from '../../../vscodeTypes';
12+
import { Conversation, Turn } from '../../prompt/common/conversation';
13+
import { IBuildPromptContext } from '../../prompt/common/intents';
14+
import { ExecutePromptToolCallingLoop } from '../../prompt/node/executePromptToolCalling';
15+
import { ToolName } from '../common/toolNames';
16+
import { CopilotToolMode, ICopilotTool, ToolRegistry } from '../common/toolsRegistry';
17+
import { assertFileOkForTool, formatUriForFileWidget, resolveToolInputPath } from './toolUtils';
18+
import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl';
19+
20+
export interface IExecutePromptParams {
21+
filePath: string;
22+
}
23+
24+
class ExecutePromptTool implements ICopilotTool<IExecutePromptParams> {
25+
public static readonly toolName = ToolName.ExecutePrompt;
26+
private _inputContext: IBuildPromptContext | undefined;
27+
28+
constructor(
29+
@IInstantiationService private readonly instantiationService: IInstantiationService,
30+
@IFileSystemService private readonly fileSystemService: IFileSystemService,
31+
@IPromptPathRepresentationService private readonly promptPathRepresentationService: IPromptPathRepresentationService,
32+
) { }
33+
34+
async invoke(options: vscode.LanguageModelToolInvocationOptions<IExecutePromptParams>, token: vscode.CancellationToken) {
35+
if (!options.input.filePath) {
36+
throw new Error('Invalid input');
37+
}
38+
39+
// Read the prompt file as text and include a reference
40+
const uri = resolveToolInputPath(options.input.filePath, this.promptPathRepresentationService);
41+
await this.instantiationService.invokeFunction(accessor => assertFileOkForTool(accessor, uri));
42+
const promptText = (await this.fileSystemService.readFile(uri)).toString();
43+
44+
const loop = this.instantiationService.createInstance(ExecutePromptToolCallingLoop, {
45+
toolCallLimit: 5,
46+
conversation: new Conversation('', [new Turn('', { type: 'user', message: promptText })]),
47+
request: {
48+
...this._inputContext!.request!,
49+
references: [],
50+
prompt: promptText,
51+
toolReferences: [],
52+
modeInstructions: '',
53+
editedFileEvents: []
54+
},
55+
location: this._inputContext!.request!.location,
56+
promptText,
57+
});
58+
59+
// TODO This also prevents codeblock pills from being rendered
60+
// I want to render this content as thinking blocks but couldn't get it to work
61+
const stream = this._inputContext?.stream && ChatResponseStreamImpl.filter(
62+
this._inputContext.stream,
63+
part => !(part instanceof ChatResponseMarkdownPart)
64+
);
65+
const loopResult = await loop.run(stream, token);
66+
// Return the text of the last assistant response from the tool calling loop
67+
const lastRoundResponse = loopResult.toolCallRounds.at(-1)?.response ?? loopResult.round.response ?? '';
68+
const result = new ExtendedLanguageModelToolResult([new LanguageModelTextPart(lastRoundResponse)]);
69+
return result;
70+
}
71+
72+
prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<IExecutePromptParams>, token: vscode.CancellationToken): vscode.ProviderResult<vscode.PreparedToolInvocation> {
73+
const { input } = options;
74+
if (!input.filePath) {
75+
return;
76+
}
77+
try {
78+
const uri = resolveToolInputPath(input.filePath, this.promptPathRepresentationService);
79+
return {
80+
invocationMessage: new MarkdownString(l10n.t`Executing prompt file ${formatUriForFileWidget(uri)}`),
81+
pastTenseMessage: new MarkdownString(l10n.t`Executed prompt file ${formatUriForFileWidget(uri)}`),
82+
};
83+
} catch {
84+
return;
85+
}
86+
}
87+
88+
async resolveInput(input: IExecutePromptParams, promptContext: IBuildPromptContext, mode: CopilotToolMode): Promise<IExecutePromptParams> {
89+
this._inputContext = promptContext;
90+
return input;
91+
}
92+
}
93+
94+
ToolRegistry.registerTool(ExecutePromptTool);
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import type * as vscode from 'vscode';
7+
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
8+
import { ChatResponseMarkdownPart, ExtendedLanguageModelToolResult, LanguageModelTextPart } from '../../../vscodeTypes';
9+
import { Conversation, Turn } from '../../prompt/common/conversation';
10+
import { IBuildPromptContext } from '../../prompt/common/intents';
11+
import { ExecutePromptToolCallingLoop } from '../../prompt/node/executePromptToolCalling';
12+
import { ToolName } from '../common/toolNames';
13+
import { CopilotToolMode, ICopilotTool, ToolRegistry } from '../common/toolsRegistry';
14+
import { ChatResponseStreamImpl } from '../../../util/common/chatResponseStreamImpl';
15+
16+
export interface IExecuteTaskParams {
17+
prompt: string;
18+
description: string;
19+
}
20+
21+
class ExecuteTaskTool implements ICopilotTool<IExecuteTaskParams> {
22+
public static readonly toolName = ToolName.ExecuteTask;
23+
private _inputContext: IBuildPromptContext | undefined;
24+
25+
constructor(
26+
@IInstantiationService private readonly instantiationService: IInstantiationService,
27+
) { }
28+
29+
async invoke(options: vscode.LanguageModelToolInvocationOptions<IExecuteTaskParams>, token: vscode.CancellationToken) {
30+
31+
const loop = this.instantiationService.createInstance(ExecutePromptToolCallingLoop, {
32+
toolCallLimit: 25,
33+
conversation: new Conversation('', [new Turn('', { type: 'user', message: options.input.prompt })]),
34+
request: this._inputContext!.request!,
35+
location: this._inputContext!.request!.location,
36+
promptText: options.input.prompt,
37+
});
38+
39+
// TODO This also prevents codeblock pills from being rendered
40+
// I want to render this content as thinking blocks but couldn't get it to work
41+
const stream = this._inputContext?.stream && ChatResponseStreamImpl.filter(
42+
this._inputContext.stream,
43+
part => !(part instanceof ChatResponseMarkdownPart)
44+
);
45+
46+
const loopResult = await loop.run(stream, token);
47+
// Return the text of the last assistant response from the tool calling loop
48+
const lastRoundResponse = loopResult.toolCallRounds.at(-1)?.response ?? loopResult.round.response ?? '';
49+
const result = new ExtendedLanguageModelToolResult([new LanguageModelTextPart(lastRoundResponse)]);
50+
return result;
51+
}
52+
53+
prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<IExecuteTaskParams>, token: vscode.CancellationToken): vscode.ProviderResult<vscode.PreparedToolInvocation> {
54+
const { input } = options;
55+
try {
56+
return {
57+
invocationMessage: input.description,
58+
};
59+
} catch {
60+
return;
61+
}
62+
}
63+
64+
async resolveInput(input: IExecuteTaskParams, promptContext: IBuildPromptContext, mode: CopilotToolMode): Promise<IExecuteTaskParams> {
65+
this._inputContext = promptContext;
66+
return input;
67+
}
68+
}
69+
70+
ToolRegistry.registerTool(ExecuteTaskTool);

src/platform/configuration/common/configurationService.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,7 @@ export namespace ConfigKey {
730730

731731
export const EnableClaudeCodeAgent = defineSetting<boolean | string | undefined>('chat.advanced.claudeCode.enabled', false, INTERNAL);
732732
export const ClaudeCodeDebugEnabled = defineSetting<boolean>('chat.advanced.claudeCode.debug', false, INTERNAL);
733+
export const TaskToolsEnabled = defineSetting<boolean>('chat.advanced.taskTools.enabled', true);
733734
}
734735

735736
export const AgentThinkingTool = defineSetting<boolean>('chat.agent.thinkingTool', false);

0 commit comments

Comments
 (0)