tool use progress (partial)

This commit is contained in:
Andrew Pareles 2025-03-02 20:31:02 -08:00
parent b6ea05948f
commit fcbad101a4
8 changed files with 473 additions and 180 deletions

View file

@ -132,6 +132,7 @@ export type StartApplyingOpts = {
from: 'ClickApply';
type: 'searchReplace' | 'rewrite';
applyStr: string;
uri: 'current' | URI;
}
@ -1226,6 +1227,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// throws if there's an error
public startApplying(opts: StartApplyingOpts) {
if (opts.type === 'rewrite') {
const addedDiffArea = this._initializeWriteoverStream(opts)
@ -1448,11 +1450,18 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }) {
const { applyStr } = opts
const { applyStr, uri: givenURI } = opts
let uri: URI
if (givenURI === 'current') {
const uri_ = this._getActiveEditorURI()
if (!uri_) return
uri = uri_
}
else {
uri = givenURI
}
const uri_ = this._getActiveEditorURI()
if (!uri_) return
const uri = uri_
// generate search/replace block text
const originalFileCode = this._voidFileService.readModel(uri)

View file

@ -15,6 +15,14 @@ import { IVoidFileService } from '../../common/voidFileService.js';
// this is just for ease of readability
export const tripleTick = ['```', '```']
export const editToolDesc_toolDescription = `\
A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change. \
Typically the best description you can give here is a high level view of the final code you'd like to see. For example, code excerpt(s) with "// ... existing code ..." comments to help you write less. \
However, you are allowed to describe the change using whatever text/language you like, especially if the change is better described without code. \
Do NOT output the whole file if possible, and try to write as LITTLE as needed to describe the change.`
export const chat_systemMessage = (workspaces: string[]) => `\
You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`.

View file

@ -86,6 +86,7 @@ export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: strin
from: 'ClickApply',
type: 'searchReplace',
applyStr: codeStr,
uri: 'current',
})
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
rerender(c => c + 1)

View file

@ -151,7 +151,7 @@ interface VoidChatAreaProps {
onAbort: () => void;
isStreaming: boolean;
isDisabled?: boolean;
divRef?: React.RefObject<HTMLDivElement>;
divRef?: React.RefObject<HTMLDivElement | null>;
// UI customization
featureName: FeatureName;
@ -528,22 +528,36 @@ export const SelectedFiles = (
}
type ToolResultToComponent = { [T in ToolName]: (props: { message: ToolMessage<T> }) => React.ReactNode }
interface ToolResultProps {
actionTitle: string;
actionParam: string;
actionNumResults?: number;
children?: React.ReactNode;
onClick?: () => void;
const actionTitleOfToolName: { [T in ToolName]: string } = {
'read_file': 'Read file',
'list_dir': 'Inspected folder',
'pathname_search': 'Searched filename',
'search': 'Searched',
'create_uri': 'Created URI',
'delete_uri': 'Deleted URI',
'edit': 'Edited file',
'terminal_command': 'Ran terminal command',
}
type ToolResultToComponent = { [T in ToolName]: (props: { message: ToolMessage<T> }) => React.ReactNode }
const ToolResult = ({
actionTitle,
toolName,
actionParam,
actionNumResults,
children,
onClick,
}: ToolResultProps) => {
}: {
toolName: ToolName;
actionParam: string;
actionNumResults?: number;
children?: React.ReactNode;
onClick?: () => void;
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const isDropdown = !!children
@ -565,7 +579,7 @@ const ToolResult = ({
/>
)}
<div className="flex items-center flex-nowrap whitespace-nowrap gap-x-2">
<span className="text-void-fg-3">{actionTitle}</span>
<span className="text-void-fg-3">{actionTitleOfToolName[toolName]}</span>
<span className="text-void-fg-4 text-xs italic">{actionParam}</span>
{actionNumResults !== undefined && (
<span className="text-void-fg-4 text-xs">
@ -587,18 +601,38 @@ const ToolResult = ({
const ToolError = <T extends ToolName,>({ toolName, errorMessage }: { toolName: T, errorMessage: string }) => {
return <ToolResult
toolName={toolName}
actionParam={errorMessage}
/>
}
const toolResultToComponent: ToolResultToComponent = {
'read_file': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='read_file' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
actionTitle="Read file"
actionParam={getBasename(message.result.uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', message.result.uri, { preview: true }) }}
/>
toolName='read_file'
actionParam={'View file'}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{getBasename(value.uri.fsPath)}
</div>
{value.hasNextPage && (<div className="italic">AI can scroll for more content...</div>)}
</div>
</ToolResult>
)
},
'list_dir': ({ message }) => {
@ -607,16 +641,18 @@ const toolResultToComponent: ToolResultToComponent = {
const explorerService = accessor.get('IExplorerService')
// message.result.hasNextPage = true
// message.result.itemsRemaining = 400
if (message.result.type === 'error') return <ToolError toolName='list_dir' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
actionTitle="Inspected folder"
actionParam={`${getBasename(message.result.rootURI.fsPath)}/`}
actionNumResults={message.result.children?.length}
toolName='list_dir'
actionParam={`${getBasename(value.rootURI.fsPath)}/`}
actionNumResults={value.children?.length}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
{message.result.children?.map((child, i) => (
<div
key={i}
{value.children?.map((child, i) => (
<div key={i}
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => {
commandService.executeCommand('workbench.view.explorer');
@ -627,11 +663,7 @@ const toolResultToComponent: ToolResultToComponent = {
{`${child.name}${child.isDirectory ? '/' : ''}`}
</div>
))}
{message.result.hasNextPage && (
<div className="italic">
{message.result.itemsRemaining} more items...
</div>
)}
{value.hasNextPage && (<div className="italic">{value.itemsRemaining} more items...</div>)}
</div>
</ToolResult>
)
@ -640,74 +672,162 @@ const toolResultToComponent: ToolResultToComponent = {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='pathname_search' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
actionTitle="Searched filename"
actionParam={`"${message.result.queryStr}"`}
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
toolName='pathname_search'
actionParam={`"${value.queryStr}"`}
actionNumResults={value.uris.length}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
{Array.isArray(message.result.uris) ?
message.result.uris.map((uri, i) => (
<div
key={i}
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => {
commandService.executeCommand('vscode.open', uri, { preview: true })
}}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{uri.fsPath.split('/').pop()}
</div>
)) :
<div className="">{message.result.uris}</div>
}
{message.result.hasNextPage && (
<div className="italic">
More results available...
{value.uris.map((uri, i) => (
<div key={i}
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{uri.fsPath.split('/').pop()}
</div>
)}
))}
{value.hasNextPage && (<div className="italic">More results available...</div>)}
</div>
</ToolResult>
)
},
'search': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='search' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='search'
actionParam={`"${value.queryStr}"`}
actionNumResults={value.uris.length}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
{value.uris.map((uri, i) => (
<div key={i}
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{uri.fsPath.split('/').pop()}
</div>
))}
{value.hasNextPage && (<div className="italic">More results available...</div>)}
</div>
</ToolResult>
)
},
// ---
'create_uri': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='create_uri' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
actionTitle="Searched"
actionParam={`"${message.result.queryStr}"`}
actionNumResults={Array.isArray(message.result.uris) ? message.result.uris.length : 0}
toolName='create_uri'
actionParam={getBasename(value.uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
{Array.isArray(message.result.uris) ?
message.result.uris.map((uri, i) => (
<div
key={i}
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => {
commandService.executeCommand('vscode.open', uri, { preview: true })
}}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{uri.fsPath.split('/').pop()}
</div>
)) :
<div className="">{message.result.uris}</div>
}
{message.result.hasNextPage && (
<div className="italic">
More results available...
</div>
)}
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{value.uri.fsPath.split('/').pop()}
</div>
</div>
</ToolResult>
)
},
'delete_uri': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='delete_uri' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='delete_uri'
actionParam={getBasename(value.uri.fsPath) + ' (deleted)'}
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{value.uri.fsPath.split('/').pop()}
</div>
</div>
</ToolResult>
)
},
'edit': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='edit' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='edit'
actionParam={getBasename(value.uri.fsPath)}
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
onClick={() => { commandService.executeCommand('vscode.open', value.uri, { preview: true }) }}
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
{value.uri.fsPath.split('/').pop()}
</div>
</div>
</ToolResult>
)
},
'terminal_command': ({ message }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
if (message.result.type === 'error') return <ToolError toolName='terminal_command' errorMessage={message.result.value} />
const { value } = message.result
return (
<ToolResult
toolName='terminal_command'
actionParam={value.command}
>
<div className="text-void-fg-4 px-2 py-1 bg-black bg-opacity-20 border border-void-border-4 border-opacity-50 rounded-sm">
<div
className="hover:brightness-125 hover:cursor-pointer transition-all duration-200 flex items-center flex-nowrap"
// TODO!!! open terminal
>
<svg className="w-1 h-1 opacity-60 mr-1.5 fill-current" viewBox="0 0 100 40"><rect x="0" y="15" width="100" height="10" /></svg>
<ChatMarkdownRender string={''} />
</div>
</div>
</ToolResult>
)
}
};

View file

@ -0,0 +1,71 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
import { generateUuid } from '../../../../base/common/uuid.js';
export interface ITerminalToolService {
readonly _serviceBrand: undefined;
createNewTerminal(terminalId: string): Promise<string>;
runCommand(command: string, terminalId?: string): Promise<void>;
focus(terminalId: string): Promise<void>;
}
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
export class TerminalToolService extends Disposable implements ITerminalToolService {
readonly _serviceBrand: undefined;
private terminalInstances: Record<string, ITerminalInstance> = {}
constructor(
@ITerminalService private readonly terminalService: ITerminalService
) {
super();
}
async createNewTerminal() {
const terminalId = generateUuid();
this.terminalService.createTerminal({});
const terminal = await this.terminalService.createTerminal({
location: TerminalLocation.Editor,
config: { name: `Void Agent (${terminalId})`, }
});
this.terminalInstances[terminalId] = terminal
return terminalId;
}
async runCommand(command: string, terminalId?: string) {
if (!terminalId) {
terminalId = await this.createNewTerminal();
}
const terminal = this.terminalInstances[terminalId];
if (!terminal) throw new Error(`Terminal with ID ${terminalId} does not exist`);
terminal.sendText(command, true);
return;
}
async focus(terminalId: string) {
const terminal = this.terminalInstances[terminalId];
if (!terminal) throw new Error(`That terminal was closed.`);
terminal.focus(true);
return;
}
}
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Eager);

View file

@ -13,11 +13,12 @@ import { Emitter, Event } from '../../../../base/common/event.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from './llmMessageService.js';
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo, chat_selectionsString } from '../browser/prompt/prompts.js';
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolFns, ToolName, voidTools } from './toolsService.js';
import { toLLMChatMessage } from './llmMessageTypes.js';
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolName, voidTools } from './toolsService.js';
import { toLLMChatMessage, ToolCallType } from './llmMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IVoidFileService } from './voidFileService.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { getErrorMessage } from '../../../../base/common/errors.js';
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
@ -59,11 +60,9 @@ export type ToolMessage<T extends ToolName> = {
name: T; // internal use
params: string; // internal use
id: string; // apis require this tool use id
content: string; // result
result: ToolCallReturnType[T]; // text message of result
content: string; // give this result to LLM
result: { type: 'success'; value: ToolCallReturnType[T], } | { type: 'error'; value: string }; // give this result to user
}
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
{
@ -390,37 +389,44 @@ class ChatThreadService extends Disposable implements IChatThreadService {
else {
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning || null })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message
for (const tool of toolCalls ?? []) {
const toolName = tool.name as ToolName
// 1.
let toolResult: Awaited<ReturnType<ToolFns[ToolName]>>
let toolResultVal: ToolCallReturnType[ToolName]
try {
toolResult = await this._toolsService.toolFns[toolName](tool.params)
toolResultVal = toolResult
} catch (error) {
this._setStreamState(threadId, { error })
shouldSendAnotherMessage = false
break
}
// deal with the tool
const tool: ToolCallType | undefined = toolCalls?.[0]
if (!tool) {
res_()
return
}
const toolName = tool.name
// 2.
let toolResultStr: string
try {
toolResultStr = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
} catch (error) {
this._setStreamState(threadId, { error })
shouldSendAnotherMessage = false
break
}
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResultVal, })
// 1.
let toolResultVal: ToolCallReturnType[typeof toolName]
try {
const val = await this._toolsService.toolFns[toolName](tool.params)
toolResultVal = val
} catch (error) {
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
shouldSendAnotherMessage = true
res_()
return
}
// 2.
let toolResultStr: string
try {
toolResultStr = this._toolsService.toolResultToString[toolName](toolResultVal as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
} catch (error) {
// treat as irrecoverable error
this._setStreamState(threadId, { error })
res_()
return
}
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: { type: 'success', value: toolResultVal }, })
shouldSendAnotherMessage = true
res_()
}
res_()
},
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
@ -436,7 +442,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
}
agentLoop() // DO NOT AWAIT THIS, this fn should resolve when ready to clear inputs
agentLoop() // DO NOT AWAIT THIS, add fn should resolve when we've added message (this lets us interrupt the agent loop correctly instead of waiting for it to resolve)
}

View file

@ -22,6 +22,11 @@ export const errorDetails = (fullError: Error | null): string | null => {
return null
}
export const getErrorMessage: (error: unknown) => string = (error) => {
if (error instanceof Error) return `${error.name}: ${error.message}`
return error + ''
}
export type LLMChatMessage = {
role: 'system' | 'user';
@ -60,7 +65,7 @@ export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage => {
else if (c.role === 'tool')
return { role: c.role, id: c.id, name: c.name, params: c.params, content: c.content || '(empty output)' }
else {
throw 1
throw new Error(`Role ${(c as any).role} not recognized.`)
}
}

View file

@ -6,7 +6,10 @@ import { createDecorator, IInstantiationService } from '../../../../platform/ins
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'
import { QueryBuilder } from '../../../../workbench/services/search/common/queryBuilder.js'
import { ISearchService } from '../../../../workbench/services/search/common/search.js'
import { IEditCodeService } from '../browser/editCodeService.js'
import { editToolDesc_toolDescription } from '../browser/prompt/prompts.js'
import { IVoidFileService } from './voidFileService.js'
import { ITerminalToolService } from '../browser/terminalToolService.js'
// tool use for AI
@ -29,6 +32,8 @@ const paginationHelper = {
} as const
export const voidTools = {
// --- context-gathering (read/search/list) ---
read_file: {
name: 'read_file',
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
@ -68,44 +73,50 @@ export const voidTools = {
required: ['query'],
},
// --- editing (create/delete) ---
// create_file: {
// name: 'create_file',
// description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.`,
// params: {
// uri: { type: 'string', description: undefined },
// },
// required: ['uri'],
// },
create_uri: {
name: 'create_uri',
description: `Creates a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`,
params: {
uri: { type: 'string', description: undefined },
},
required: ['uri'],
},
// create_folder: {
// name: 'create_folder',
// description: `Creates a folder at the given path. Fails gracefully if the folder already exists by doing nothing.`,
// params: {
// uri: { type: 'string', description: undefined },
// },
// required: ['uri'],
// },
delete_uri: {
name: 'delete_uri',
description: `Deletes the file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
params: {
uri: { type: 'string', description: undefined },
params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' }
},
required: ['uri', 'params'],
},
edit: { // APPLY TOOL
name: 'edit',
description: `Edits the contents of a file at the given URI. Fails gracefully if the file does not exist.`,
params: {
uri: { type: 'string', description: undefined },
changeDescription: { type: 'string', description: editToolDesc_toolDescription }
},
required: ['uri', 'changeDescription'],
},
terminal_command: {
name: 'terminal_command',
description: `Executes a terminal command.`,
params: {
command: { type: 'string', description: 'The terminal command to execute.' }
},
required: ['command'],
},
// go_to_definition: {
// go_to_definition
// go_to_usages
// },
// go_to_usages:
// create_file: {
// name: 'create_file',
// description: `Creates a file at the given path. Fails gracefully if the file already exists by doing nothing.`
// params: {
// uri: { type: 'string', description: undefined },
// }
// }
// semantic_search: {
// description: 'Searches files semantically for the given string query.',
// // RAG
// },
} satisfies { [name: string]: InternalToolInfo }
export type ToolName = keyof typeof voidTools
@ -124,9 +135,13 @@ export type ToolParamsObj<T extends ToolName> = { [paramName in ToolParamNames<T
export type ToolCallReturnType = {
'read_file': { uri: URI, fileContents: string, hasNextPage: boolean },
'list_dir': { rootURI: URI, children: DirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
'pathname_search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean },
'search': { queryStr: string, uris: URI[] | string, hasNextPage: boolean }
'create_file': {}
'pathname_search': { queryStr: string, uris: URI[], hasNextPage: boolean },
'search': { queryStr: string, uris: URI[], hasNextPage: boolean },
// ---
'edit': { uri: URI, changeDescription: string },
'create_uri': { uri: URI },
'delete_uri': { uri: URI },
'terminal_command': { command: string },
}
type DirectoryItem = {
@ -218,32 +233,41 @@ const validateJSON = (s: string): { [s: string]: unknown } => {
return o
}
catch (e) {
throw new Error(`Tool parameter was not a valid JSON: "${s}".`)
throw new Error(`Tool parameter was not a string of a valid JSON: "${s}".`)
}
}
const validateQueryStr = (queryStr: unknown) => {
if (typeof queryStr !== 'string') throw new Error('Error calling tool: provided query must be a string.')
return queryStr
const validateStr = (argName: string, value: unknown) => {
if (typeof value !== 'string') throw new Error(`Error: ${argName} must be a string.`)
return value
}
// TODO!!!! check to make sure in workspace
const validateURI = (uriStr: unknown) => {
if (typeof uriStr !== 'string') throw new Error('Error calling tool: provided uri must be a string.')
if (typeof uriStr !== 'string') throw new Error('Error: provided uri must be a string.')
const uri = URI.file(uriStr)
return uri
}
const validatePageNum = (pageNumberUnknown: unknown) => {
const proposedPageNum = Number.parseInt(pageNumberUnknown + '')
const num = Number.isInteger(proposedPageNum) ? proposedPageNum : 1
const pageNumber = num < 1 ? 1 : num
return pageNumber
if (!pageNumberUnknown) return 1
const parsedInt = Number.parseInt(pageNumberUnknown + '')
if (!Number.isInteger(parsedInt)) throw new Error(`Page number was not an integer: "${pageNumberUnknown}".`)
if (parsedInt < 1) throw new Error(`Specified page number must be 1 or greater: "${pageNumberUnknown}".`)
return parsedInt
}
const validateRecursiveParamStr = (paramsUnknown: unknown) => {
if (typeof paramsUnknown !== 'string') throw new Error('Error calling tool: provided params must be a string.')
const params = paramsUnknown
const isRecursive = params.includes('r')
return isRecursive
}
export interface IToolsService {
readonly _serviceBrand: undefined;
toolFns: ToolFns;
@ -256,8 +280,8 @@ export class ToolsService implements IToolsService {
readonly _serviceBrand: undefined;
public toolFns: ToolFns
public toolResultToString: ToolResultToString
public toolFns: ToolFns;
public toolResultToString: ToolResultToString;
constructor(
@ -266,15 +290,17 @@ export class ToolsService implements IToolsService {
@ISearchService searchService: ISearchService,
@IInstantiationService instantiationService: IInstantiationService,
@IVoidFileService voidFileService: IVoidFileService,
@IEditCodeService editCodeService: IEditCodeService,
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
) {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
this.toolFns = {
read_file: async (s: string) => {
read_file: async (params: string) => {
console.log('read_file')
const o = validateJSON(s)
const o = validateJSON(params)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
const uri = validateURI(uriStr)
@ -293,25 +319,21 @@ export class ToolsService implements IToolsService {
return { uri, fileContents, hasNextPage }
},
list_dir: async (s: string) => {
console.log('list_dir')
const o = validateJSON(s)
list_dir: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
const dirResult = await computeDirectoryResult(fileService, uri, pageNumber)
console.log('list_dir result:', dirResult)
return dirResult
},
pathname_search: async (s: string) => {
console.log('pathname_search')
const o = validateJSON(s)
pathname_search: async (params: string) => {
const o = validateJSON(params)
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
const queryStr = validateQueryStr(queryUnknown)
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), { filePattern: queryStr, })
@ -324,19 +346,13 @@ export class ToolsService implements IToolsService {
.map(({ resource, results }) => resource)
const hasNextPage = (data.results.length - 1) - toIdx >= 1
console.log('pathname_search result:', uris)
return { queryStr, uris, hasNextPage }
},
search: async (s: string) => {
console.log('search')
const o = validateJSON(s)
search: async (params: string) => {
const o = validateJSON(params)
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
const queryStr = validateQueryStr(queryUnknown)
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const query = queryBuilder.text({ pattern: queryStr, }, workspaceContextService.getWorkspace().folders.map(f => f.uri))
@ -349,17 +365,62 @@ export class ToolsService implements IToolsService {
.map(({ resource, results }) => resource)
const hasNextPage = (data.results.length - 1) - toIdx >= 1
console.log('search result:', uris)
return { queryStr, uris, hasNextPage }
},
// ---
create_uri: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr } = o
const uri = validateURI(uriStr)
await fileService.createFile(uri)
return { uri }
},
delete_uri: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, params: paramsStr } = o
const uri = validateURI(uriStr)
const isRecursive = validateRecursiveParamStr(paramsStr)
await fileService.del(uri, { recursive: isRecursive })
return { uri }
},
edit: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o
const uri = validateURI(uriStr)
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
const applyId = editCodeService.startApplying({ uri, applyStr: changeDescription, from: 'ClickApply', type: 'rewrite' })
// // TODO!!!
// await // await apply done before moving on
return { uri, changeDescription }
},
terminal_command: async (s: string) => {
const o = validateJSON(s)
const { command: commandUnknown } = o
const command = validateStr('command', commandUnknown)
// TODO!!!!
// await // Await user confirmation and then command execution before resolving
return { command }
},
}
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
// given to the LLM after the call
this.toolResultToString = {
read_file: (result) => {
return nextPageStr(result.hasNextPage)
@ -369,13 +430,25 @@ export class ToolsService implements IToolsService {
return dirTreeStr + nextPageStr(result.hasNextPage)
},
pathname_search: (result) => {
if (typeof result.uris === 'string') return result.uris
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
search: (result) => {
if (typeof result.uris === 'string') return result.uris
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
// ---
create_uri: (result) => {
return `URI ${result.uri.fsPath} successfully created.`
},
delete_uri: (result) => {
return `URI ${result.uri.fsPath} successfully deleted.`
},
edit: (result) => {
return `Change successfully made ${result.uri.fsPath} successfully deleted.`
},
terminal_command: (result) => {
return `Terminal command "${result.command}" successfully executed.`
},
}