Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion authenticationprovider-sample/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
"js/ts.tsc.autoDetect": "off"
}
2 changes: 1 addition & 1 deletion basic-multi-root-sample/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
"typescript.tsc.autoDetect": "off" // Turn off tsc task auto detection since we have the necessary task as npm scripts
"js/ts.tsc.autoDetect": "off" // Turn off tsc task auto detection since we have the necessary task as npm scripts
}
2 changes: 1 addition & 1 deletion chat-context-sample/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
"js/ts.tsc.autoDetect": "off"
}
2 changes: 1 addition & 1 deletion chat-sample/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This GitHub Copilot Extension sample shows:

- How to contribute a simple chat participant to the GitHub Copilot Chat view. (`@cat`, [simple.ts](src/simple.ts))
- How to use the Language Model API to request access to the Language Model.
- How to use the `@vscode/chat-extension-utils` library to easily create a chat participant that uses tools. (`@catTools`, [chatUtilsSample.ts](src/chatUtilsSample.ts))
- How to create a chat participant that uses tools with a custom persona. (`@catTools`, [chatUtilsSample.ts](src/chatUtilsSample.ts))
- How to contribute a more sophisticated chat participant that uses the LanguageModelTool API to contribute and invoke tools. (`@tool`, [toolParticipant.ts](src/toolParticipant.ts))

![demo](./demo.png)
Expand Down
10 changes: 0 additions & 10 deletions chat-sample/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 2 additions & 3 deletions chat-sample/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"id": "chat-tools-sample.catTools",
"fullName": "Cat (Tools)",
"name": "catTools",
"description": "I use tools, implemented using @vscode/chat-extension-utils, and am also a cat",
"description": "I use tools and am also a cat",
"isSticky": true,
"commands": [
{
Expand Down Expand Up @@ -172,7 +172,6 @@
"watch": "tsc -watch -p ./"
},
"dependencies": {
"@vscode/chat-extension-utils": "^0.0.0-alpha.1",
"@vscode/prompt-tsx": "^0.3.0-alpha.12"
},
"devDependencies": {
Expand All @@ -184,4 +183,4 @@
"typescript": "^5.9.2",
"typescript-eslint": "^8.39.0"
}
}
}
137 changes: 120 additions & 17 deletions chat-sample/src/chatUtilsSample.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,26 @@
import { renderPrompt } from '@vscode/prompt-tsx';
import * as vscode from 'vscode';
import * as chatUtils from '@vscode/chat-extension-utils';
import { ToolCallRound, ToolResultMetadata, ToolUserPrompt } from './toolsPrompt';
import { TsxToolUserMetadata } from './toolParticipant';
import { toLanguageModelChatTools, getToolsForRequest } from './lmTools';

const FIND_FILES_TOOL = 'chat-tools-sample_findFiles';

function inferRequiredToolFromPrompt(
prompt: string,
tools: readonly vscode.LanguageModelToolInformation[]
): string | undefined {
const findFilesTool = tools.find(tool => tool.name === FIND_FILES_TOOL);
if (!findFilesTool) {
return undefined;
}

if (/\b(search|find|look\s+for|locate)\b/i.test(prompt) && /\.\w{1,10}\b/.test(prompt)) {
return findFilesTool.name;
}

return undefined;
}

export function registerChatLibChatParticipant(context: vscode.ExtensionContext) {
const handler: vscode.ChatRequestHandler = async (request: vscode.ChatRequest, chatContext: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => {
Expand All @@ -8,28 +29,110 @@ export function registerChatLibChatParticipant(context: vscode.ExtensionContext)
return;
}

const tools = request.command === 'all' ?
vscode.lm.tools :
vscode.lm.tools.filter(tool => tool.tags.includes('chat-tools-sample'));
let model = request.model;
if (model.vendor === 'copilot' && model.family.startsWith('o1')) {
const models = await vscode.lm.selectChatModels({
vendor: 'copilot',
family: 'gpt-4o'
});
model = models[0];
}

const tools = getToolsForRequest(request.command);
const options: vscode.LanguageModelChatRequestOptions = {
justification: 'To make a request to @catTools',
};

const libResult = chatUtils.sendChatParticipantRequest(
request,
chatContext,
const result = await renderPrompt(
ToolUserPrompt,
{
prompt: 'You are a cat! Answer as a cat.',
responseStreamOptions: {
stream,
references: true,
responseText: true
},
tools
context: chatContext,
request,
toolCallRounds: [],
toolCallResults: {},
additionalInstructions: 'You are a cat! Answer as a cat, but still use tools whenever they help complete the user\'s request.'
},
token);
{ modelMaxPromptTokens: model.maxInputTokens },
model);
let messages = result.messages;
result.references.forEach(ref => {
if (ref.anchor instanceof vscode.Uri || ref.anchor instanceof vscode.Location) {
stream.reference(ref.anchor);
}
});

const toolReferences = [...request.toolReferences];
const inferredTool = inferRequiredToolFromPrompt(request.prompt, tools);
if (inferredTool && !toolReferences.some(ref => ref.name === inferredTool)) {
toolReferences.unshift({ name: inferredTool } as vscode.ChatLanguageModelToolReference);
}

const accumulatedToolResults: Record<string, vscode.LanguageModelToolResult> = {};
const toolCallRounds: ToolCallRound[] = [];
const runWithTools = async (): Promise<void> => {
const requestedTool = toolReferences.shift();
if (requestedTool) {
options.toolMode = vscode.LanguageModelChatToolMode.Required;
options.tools = toLanguageModelChatTools(
vscode.lm.tools.filter(tool => tool.name === requestedTool.name)
);
} else {
options.toolMode = undefined;
options.tools = toLanguageModelChatTools(tools);
}

const response = await model.sendRequest(messages, options, token);

const toolCalls: vscode.LanguageModelToolCallPart[] = [];
let responseStr = '';
for await (const part of response.stream) {
if (part instanceof vscode.LanguageModelTextPart) {
stream.markdown(part.value);
responseStr += part.value;
} else if (part instanceof vscode.LanguageModelToolCallPart) {
toolCalls.push(part);
}
}

if (toolCalls.length) {
toolCallRounds.push({
response: responseStr,
toolCalls
});
const nextResult = (await renderPrompt(
ToolUserPrompt,
{
context: chatContext,
request,
toolCallRounds,
toolCallResults: accumulatedToolResults,
additionalInstructions: 'You are a cat! Answer as a cat, but still use tools whenever they help complete the user\'s request.'
},
{ modelMaxPromptTokens: model.maxInputTokens },
model));
messages = nextResult.messages;
const toolResultMetadata = nextResult.metadatas.getAll(ToolResultMetadata);
if (toolResultMetadata?.length) {
toolResultMetadata.forEach(meta => accumulatedToolResults[meta.toolCallId] = meta.result);
}

return runWithTools();
}
};

await runWithTools();

return await libResult.result;
return {
metadata: {
toolCallsMetadata: {
toolCallResults: accumulatedToolResults,
toolCallRounds
}
} satisfies TsxToolUserMetadata,
};
};

const chatLibParticipant = vscode.chat.createChatParticipant('chat-tools-sample.catTools', handler);
chatLibParticipant.iconPath = vscode.Uri.joinPath(context.extensionUri, 'cat.jpeg');
context.subscriptions.push(chatLibParticipant);
}
}
24 changes: 24 additions & 0 deletions chat-sample/src/lmTools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as vscode from 'vscode';

/**
* Map registered tools to the shape expected by {@link vscode.LanguageModelChat.sendRequest}.
* Ensures every tool carries a valid JSON schema object for `inputSchema` — some models (e.g.
* Claude) reject tool definitions where `inputSchema` is `undefined`.
*/
export function toLanguageModelChatTools(
tools: readonly vscode.LanguageModelToolInformation[]
): vscode.LanguageModelChatTool[] {
return tools.map(tool => ({
name: tool.name,
description: tool.description,
inputSchema: tool.inputSchema ?? { type: 'object', properties: {} },
}));
}

export function getToolsForRequest(
command: string | undefined
): readonly vscode.LanguageModelToolInformation[] {
return command === 'all'
? vscode.lm.tools
: vscode.lm.tools.filter(tool => tool.tags.includes('chat-tools-sample'));
}
11 changes: 6 additions & 5 deletions chat-sample/src/toolParticipant.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { renderPrompt } from '@vscode/prompt-tsx';
import * as vscode from 'vscode';
import { ToolCallRound, ToolResultMetadata, ToolUserPrompt } from './toolsPrompt';
import { toLanguageModelChatTools, getToolsForRequest } from './lmTools';

export interface TsxToolUserMetadata {
toolCallsMetadata: ToolCallsMetadata;
Expand Down Expand Up @@ -36,9 +37,7 @@ export function registerToolUserChatParticipant(context: vscode.ExtensionContext
}

// Use all tools, or tools with the tags that are relevant.
const tools = request.command === 'all' ?
vscode.lm.tools :
vscode.lm.tools.filter(tool => tool.tags.includes('chat-tools-sample'));
const tools = getToolsForRequest(request.command);
const options: vscode.LanguageModelChatRequestOptions = {
justification: 'To make a request to @toolsTSX',
};
Expand Down Expand Up @@ -69,10 +68,12 @@ export function registerToolUserChatParticipant(context: vscode.ExtensionContext
const requestedTool = toolReferences.shift();
if (requestedTool) {
options.toolMode = vscode.LanguageModelChatToolMode.Required;
options.tools = vscode.lm.tools.filter(tool => tool.name === requestedTool.name);
options.tools = toLanguageModelChatTools(
vscode.lm.tools.filter(tool => tool.name === requestedTool.name)
);
} else {
options.toolMode = undefined;
options.tools = [...tools];
options.tools = toLanguageModelChatTools(tools);
}

// Send the request to the LanguageModelChat
Expand Down
7 changes: 7 additions & 0 deletions chat-sample/src/toolsPrompt.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface ToolUserProps extends BasePromptElementProps {
context: vscode.ChatContext;
toolCallRounds: ToolCallRound[];
toolCallResults: Record<string, vscode.LanguageModelToolResult>;
additionalInstructions?: string;
}

export class ToolUserPrompt extends PromptElement<ToolUserProps, void> {
Expand All @@ -47,6 +48,12 @@ export class ToolUserPrompt extends PromptElement<ToolUserProps, void> {
- Don't make assumptions about the situation- gather context first, then
perform the task or answer the question. <br />
- Don't ask the user for confirmation to use tools, just use them.
{this.props.additionalInstructions ? (
<>
<br />
{this.props.additionalInstructions}
</>
) : undefined}
</UserMessage>
<History context={this.props.context} priority={10} />
<PromptReferences
Expand Down
2 changes: 1 addition & 1 deletion chat-tutorial/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
"js/ts.tsc.autoDetect": "off"
}
2 changes: 1 addition & 1 deletion configuration-sample/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@
"out": true // set this to false to include "out" folder in search results
},
"typescript.tsdk": "./node_modules/typescript/lib", // we want to use the TS server from our node_modules folder to control its version
"typescript.tsc.autoDetect": "off",
"js/ts.tsc.autoDetect": "off",
"editor.insertSpaces": false,
}
11 changes: 9 additions & 2 deletions helloworld-sample/.vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"problemMatcher": {
"base": "$tsc-watch",
"background": {
"activeOnStart": true,
"beginsPattern": "^\\s*\\[.*\\]\\s*Starting compilation in watch mode.*$",
"endsPattern": "^\\s*\\[.*\\]\\s*Found 0 errors\\. Watching for file changes\\.$"
}
},
"presentation": {
"reveal": "never"
},
Expand All @@ -17,4 +24,4 @@
}
}
]
}
}
2 changes: 1 addition & 1 deletion jupyter-kernel-execution-sample/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"search.exclude": {
"out": true
},
"typescript.tsc.autoDetect": "off"
"js/ts.tsc.autoDetect": "off"
}
2 changes: 1 addition & 1 deletion jupyter-server-provider-sample/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
"search.exclude": {
"out": true
},
"typescript.tsc.autoDetect": "off"
"js/ts.tsc.autoDetect": "off"
}
2 changes: 1 addition & 1 deletion l10n-sample/.vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off"
"js/ts.tsc.autoDetect": "off"
}
Loading