2024-10-28 18:49:56 -07:00
import { renderPrompt } from '@vscode/prompt-tsx' ;
import * as vscode from 'vscode' ;
import { ToolCallRound , ToolResultMetadata , ToolUserPrompt } from './toolsPrompt' ;
export interface TsxToolUserMetadata {
2025-09-23 21:57:41 -07:00
toolCallsMetadata : ToolCallsMetadata ;
2024-10-28 18:49:56 -07:00
}
export interface ToolCallsMetadata {
2025-09-23 21:57:41 -07:00
toolCallRounds : ToolCallRound [ ] ;
toolCallResults : Record < string , vscode.LanguageModelToolResult > ;
2024-10-28 18:49:56 -07:00
}
export function isTsxToolUserMetadata ( obj : unknown ) : obj is TsxToolUserMetadata {
2025-09-23 21:57:41 -07:00
// If you change the metadata format, you would have to make this stricter or handle old objects in old ChatRequest metadata
return ! ! obj &&
! ! ( obj as TsxToolUserMetadata ) . toolCallsMetadata &&
Array . isArray ( ( obj as TsxToolUserMetadata ) . toolCallsMetadata . toolCallRounds ) ;
2024-10-28 18:49:56 -07:00
}
export function registerToolUserChatParticipant ( context : vscode.ExtensionContext ) {
2025-09-23 21:57:41 -07:00
const handler : vscode.ChatRequestHandler = async ( request : vscode.ChatRequest , chatContext : vscode.ChatContext , stream : vscode.ChatResponseStream , token : vscode.CancellationToken ) = > {
if ( request . command === 'list' ) {
stream . markdown ( ` Available tools: ${ vscode . lm . tools . map ( tool = > tool . name ) . join ( ', ' ) } \ n \ n ` ) ;
return ;
}
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
let model = request . model ;
if ( model . vendor === 'copilot' && model . family . startsWith ( 'o1' ) ) {
// The o1 models do not currently support tools
const models = await vscode . lm . selectChatModels ( {
vendor : 'copilot' ,
family : 'gpt-4o'
} ) ;
model = models [ 0 ] ;
}
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
// 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 options : vscode.LanguageModelChatRequestOptions = {
justification : 'To make a request to @toolsTSX' ,
} ;
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
// Render the initial prompt
const result = await renderPrompt (
ToolUserPrompt ,
{
context : chatContext ,
request ,
toolCallRounds : [ ] ,
toolCallResults : { }
} ,
{ 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 ) ;
}
} ) ;
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
const toolReferences = [ . . . request . toolReferences ] ;
const accumulatedToolResults : Record < string , vscode.LanguageModelToolResult > = { } ;
const toolCallRounds : ToolCallRound [ ] = [ ] ;
const runWithTools = async ( ) : Promise < void > = > {
// If a toolReference is present, force the model to call that tool
const requestedTool = toolReferences . shift ( ) ;
if ( requestedTool ) {
options . toolMode = vscode . LanguageModelChatToolMode . Required ;
options . tools = vscode . lm . tools . filter ( tool = > tool . name === requestedTool . name ) ;
} else {
options . toolMode = undefined ;
options . tools = [ . . . tools ] ;
}
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
// Send the request to the LanguageModelChat
const response = await model . sendRequest ( messages , options , token ) ;
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
// Stream text output and collect tool calls from the response
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 ) ;
}
}
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
if ( toolCalls . length ) {
// If the model called any tools, then we do another round- render the prompt with those tool calls (rendering the PromptElements will invoke the tools)
// and include the tool results in the prompt for the next request.
toolCallRounds . push ( {
response : responseStr ,
toolCalls
} ) ;
const result = ( await renderPrompt (
ToolUserPrompt ,
{
context : chatContext ,
request ,
toolCallRounds ,
toolCallResults : accumulatedToolResults
} ,
{ modelMaxPromptTokens : model.maxInputTokens } ,
model ) ) ;
messages = result . messages ;
const toolResultMetadata = result . metadatas . getAll ( ToolResultMetadata ) ;
if ( toolResultMetadata ? . length ) {
// Cache tool results for later, so they can be incorporated into later prompts without calling the tool again
toolResultMetadata . forEach ( meta = > accumulatedToolResults [ meta . toolCallId ] = meta . result ) ;
}
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
// This loops until the model doesn't want to call any more tools, then the request is done.
return runWithTools ( ) ;
}
} ;
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
await runWithTools ( ) ;
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
return {
metadata : {
// Return tool call metadata so it can be used in prompt history on the next request
toolCallsMetadata : {
toolCallResults : accumulatedToolResults ,
toolCallRounds
}
} satisfies TsxToolUserMetadata ,
} ;
} ;
2024-10-28 18:49:56 -07:00
2025-09-23 21:57:41 -07:00
const toolUser = vscode . chat . createChatParticipant ( 'chat-tools-sample.tools' , handler ) ;
toolUser . iconPath = new vscode . ThemeIcon ( 'tools' ) ;
context . subscriptions . push ( toolUser ) ;
2024-10-28 18:49:56 -07:00
}