proper handling of tool call result, and improved prompting

This commit is contained in:
Andrew Pareles 2025-04-08 23:55:57 -07:00
parent ad5a77dc55
commit 49fbb62259
10 changed files with 420 additions and 385 deletions

View file

@ -8,8 +8,7 @@ import { ILanguageFeaturesService } from '../../../../editor/common/services/lan
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { Position } from '../../../../editor/common/core/position.js';
import { InlineCompletion, InlineCompletionContext, } from '../../../../editor/common/languages.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { InlineCompletion, } from '../../../../editor/common/languages.js';
import { Range } from '../../../../editor/common/core/range.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
@ -633,8 +632,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
async _provideInlineCompletionItems(
model: ITextModel,
position: Position,
context: InlineCompletionContext,
token: CancellationToken,
): Promise<InlineCompletion[]> {
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
@ -852,7 +849,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
newAutocompletion.status = 'error'
reject(message)
},
onAbort: () => { },
onAbort: () => { reject('Aborted autocomplete') },
})
newAutocompletion.requestId = requestId
@ -897,9 +894,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
) {
super()
this._langFeatureService.inlineCompletionsProvider.register('*', {
this._register(this._langFeatureService.inlineCompletionsProvider.register('*', {
provideInlineCompletions: async (model, position, context, token) => {
const items = await this._provideInlineCompletionItems(model, position, context, token)
const items = await this._provideInlineCompletionItems(model, position)
// console.log('item: ', items?.[0]?.insertText)
return { items: items, }
@ -936,7 +933,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
});
},
})
}))
}

View file

@ -11,11 +11,11 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, chat_systemMessage, ToolName, } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, RawToolCallObj, ParsedToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { chat_userMessageContent, chat_systemMessage, ToolName, toolCallXMLStr, } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
import { IToolsService } from './toolsService.js';
@ -37,6 +37,7 @@ import { IModelService } from '../../../../editor/common/services/model.js';
import { IDirectoryStrService } from './directoryStrService.js';
import { truncate } from '../../../../base/common/strings.js';
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
import { deepClone } from '../../../../base/common/objects.js';
/*
@ -61,37 +62,6 @@ A checkpoint appears before every LLM message, and before every user message (be
*/
const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => {
const llmChatMessages: LLMChatMessage[] = []
// merge tools into user message
for (const c of chatMessages) {
if (c.role === 'assistant')
llmChatMessages.push({ role: c.role, content: c.displayContent, anthropicReasoning: c.anthropicReasoning })
// merge all tool/user messages into one big user message
else if (c.role === 'user' || c.role === 'tool') {
if (c.role === 'tool')
c.content = `TOOL_RESULT (${c.name}):\n${c.content}`
if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user')
llmChatMessages.push({ role: 'user', content: c.content })
else
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
}
else if (c.role === 'interrupted_streaming_tool') { // pass
}
else if (c.role === 'checkpoint') { // pass
}
else {
throw new Error(`Role ${(c as any).role} not recognized.`)
}
}
return llmChatMessages
}
type UserMessageType = ChatMessage & { role: 'user' }
type UserMessageState = UserMessageType['state']
const defaultMessageState: UserMessageState = {
@ -466,12 +436,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
lastMsg.role === 'tool' && (lastMsg.type === 'tool_request')
)) return // should never happen
const lastUserMsgIdx = findLastIdx(thread.messages, m => m.role === 'user')
const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' }
if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen
const instructions = lastUserMessage.displayContent || ''
const callThisToolFirst: ToolMessage<ToolName> = lastMsg
this._updateLatestToolTo(threadId, {
@ -484,7 +448,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
})
this._wrapRunAgentToNotify(
this._runChatAgent({ callThisToolFirst, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() })
this._runChatAgent({ callThisToolFirst, threadId, ...this._currentModelSelectionProps() })
, threadId
)
}
@ -529,7 +493,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
if (toolCallSoFar) {
this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
@ -549,136 +513,159 @@ class ChatThreadService extends Disposable implements IChatThreadService {
private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {}
// system message
private _generateSystemMessage = async (chatMode: ChatMode) => {
const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
const activeURI = this._editorService.activeEditor?.resource?.fsPath;
const directoryStr = await this._directoryStrService.getAllDirectoriesStr({
cutOffMessage: chatMode === 'agent' || chatMode === 'gather' ? `...Directories string cut off, use tools to read more...`
: `...Directories string cut off, ask user for more if necessary...`
})
const runningTerminalIds = this._terminalToolService.listTerminalIds()
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode })
return systemMessage
}
private _generateLLMMessages = async (threadId: string) => {
const thread = this.state.allThreads[threadId]
if (!thread) return []
const chatMessages = deepClone(thread.messages)
const llmChatMessages: LLMChatMessage[] = []
// merge tools into user message
for (const c of chatMessages) {
if (c.role === 'assistant') {
// if called a tool, re-add its XML to the message
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
let content = c.displayContent
if (c.toolCall) {
content = `${content}\n\n${toolCallXMLStr(c.toolCall)}`
}
llmChatMessages.push({ role: c.role, content: content, anthropicReasoning: c.anthropicReasoning })
}
else if (c.role === 'user' || c.role === 'tool') {
if (c.role === 'tool')
c.content = `<${c.name}_result>\n${c.content}\n</${c.name}_result>`
if (llmChatMessages.length === 0 || llmChatMessages[llmChatMessages.length - 1].role !== 'user')
llmChatMessages.push({ role: 'user', content: c.content })
else
llmChatMessages[llmChatMessages.length - 1].content += '\n\n' + c.content
}
else if (c.role === 'interrupted_streaming_tool') { // pass
}
else if (c.role === 'checkpoint') { // pass
}
else {
throw new Error(`Role ${(c as any).role} not recognized.`)
}
}
return llmChatMessages
}
// returns true when the tool call is waiting for user approval
private _runToolCall = async (
threadId: string,
toolName: ToolName,
opts: { preapproved: true, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
// compute these below
let toolParams: ToolCallParams[ToolName]
let toolResult: ToolResultType[typeof toolName]
let toolResultStr: string
if (!opts.preapproved) { // skip this if pre-approved
// 1. validate tool params
try {
console.log('VALIDATING PARAMS!!!', opts.unvalidatedToolParams)
const params = await this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
toolParams = params
} catch (error) {
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, content: errorMessage, })
return {}
}
// once validated, add checkpoint for edit
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
// 2. if tool requires approval, break from the loop, awaiting approval
const requiresApproval = toolNamesThatRequireApproval.has(toolName)
if (requiresApproval) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams })
if (!autoApprove) {
return { awaitingUserApproval: true }
}
}
}
else {
toolParams = opts.validatedParams
}
// 3. call the tool
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
let interrupted = false
try {
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
this._currentlyRunningToolInterruptor[threadId] = () => {
interrupted = true;
interruptTool?.();
delete this._currentlyRunningToolInterruptor[threadId];
}
toolResult = await result // ts is bad... await is needed
}
catch (error) {
if (interrupted) {
// the tool result is added when we stop running
return { interrupted: true }
}
const errorMessage = getErrorMessage(error)
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
return {}
}
// 4. stringify the result to give to the LLM
try {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(error)
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
return {}
}
// 5. add to history and keep going
this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, })
return {}
};
private async _runChatAgent({
threadId,
modelSelection,
modelSelectionOptions,
userMessageContent,
callThisToolFirst,
}: {
threadId: string,
modelSelection: ModelSelection | null,
modelSelectionOptions: ModelSelectionOptions | undefined,
userMessageContent: string, // content of LATEST user message
callThisToolFirst?: ToolMessage<ToolName> & { type: 'tool_request' }
}) {
const userMessageFullContent = userMessageContent
const getLatestMessages = async () => {
// replace last userMessage with userMessageFullContent (which contains all the files too)
const thread = this.state.allThreads[threadId]
const latestMessages = thread?.messages ?? []
const messages_ = toLLMChatMessages(latestMessages)
const lastUserMsgIdx = findLastIdx(messages_, m => m.role === 'user')
if (lastUserMsgIdx === -1) return [] // should never happen (or how did they send the message?!)
// system message
const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
const openedURIs = this._modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
const activeURI = this._editorService.activeEditor?.resource?.fsPath;
const { wasCutOff, str: directoryStr_ } = await this._directoryStrService.getAllDirectoriesStr()
const directoryStr = wasCutOff ? (
chatMode === 'agent' || chatMode === 'gather' ? `${directoryStr_}\nString cut off, use tools to read more.`
: `${directoryStr_}\nString cut off, ask user for more if necessary.`
) : directoryStr_
const runningTerminalIds = this._terminalToolService.listTerminalIds()
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, runningTerminalIds, chatMode })
// console.log('SYSTEM MESSAGE', systemMessage)
// all messages so far in the chat history (including tools)
const messages: LLMChatMessage[] = [
{ role: 'system', content: systemMessage, },
...messages_.slice(0, lastUserMsgIdx),
{ role: 'user', content: userMessageFullContent },
...messages_.slice(lastUserMsgIdx + 1, Infinity),
]
// console.log('MESSAGES!!!', messages)
return messages
}
// returns true when the tool call is waiting for user approval
const handleToolCall = async (
toolName: ToolName,
opts: { preapproved: true, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: ParsedToolParamsObj },
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
// compute these below
let toolParams: ToolCallParams[ToolName]
let toolResult: ToolResultType[typeof toolName]
let toolResultStr: string
if (!opts.preapproved) { // skip this if pre-approved
// 1. validate tool params
try {
console.log('VALIDATING PARAMS!!!', opts.unvalidatedToolParams)
const params = await this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
toolParams = params
} catch (error) {
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', params: null, result: null, name: toolName, content: errorMessage, })
return {}
}
// once validated, add checkpoint for edit
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
// 2. if tool requires approval, break from the loop, awaiting approval
const requiresApproval = toolNamesThatRequireApproval.has(toolName)
if (requiresApproval) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(never)', result: null, name: toolName, params: toolParams })
if (!autoApprove) {
return { awaitingUserApproval: true }
}
}
}
else {
toolParams = opts.validatedParams
}
// 3. call the tool
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
let interrupted = false
try {
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
this._currentlyRunningToolInterruptor[threadId] = () => {
interrupted = true;
interruptTool?.();
delete this._currentlyRunningToolInterruptor[threadId];
}
toolResult = await result // ts is bad... await is needed
}
catch (error) {
if (interrupted) {
// the tool result is added when we stop running
return { interrupted: true }
}
const errorMessage = getErrorMessage(error)
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
return {}
}
// 4. stringify the result to give to the LLM
try {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(error)
this._updateLatestToolTo(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, })
return {}
}
// 5. add to history and keep going
this._updateLatestToolTo(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, })
return {}
};
// above just defines helpers, below starts the actual function
const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
@ -693,7 +680,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// before enter loop, call tool
if (callThisToolFirst) {
const { interrupted } = await handleToolCall(callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params })
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, { preapproved: true, validatedParams: callThisToolFirst.params })
if (interrupted) return
}
@ -709,7 +696,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// send llm message
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
const messages = await getLatestMessages()
const systemMessage = await this._generateSystemMessage(chatMode)
const llmMessages = await this._generateLLMMessages(threadId)
const messages: LLMChatMessage[] = [
{ role: 'system', content: systemMessage },
...llmMessages
]
console.log('SENDING!!', messages)
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
@ -721,16 +715,17 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall }, 'merge')
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, toolCall, anthropicReasoning })
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
console.log('tool call!!', toolCall)
console.log('tool call!!', JSON.stringify(toolCall))
resMessageIsDonePromise(toolCall) // resolve with tool calls
},
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
const toolCallSoFar = this.streamState[threadId]?.toolCallSoFar
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
this._addMessageToThread(threadId, { role: 'assistant', displayContent: messageSoFar, reasoning: reasoningSoFar, toolCall: toolCallSoFar, anthropicReasoning: null })
this._setStreamState(threadId, { error }, 'set')
resMessageIsDonePromise()
},
@ -757,7 +752,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// call tool if there is one
const tool: RawToolCallObj | undefined = toolCall
if (tool) {
const { awaitingUserApproval, interrupted } = await handleToolCall(tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams })
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams })
// stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools.
// just detect tool interruption which is the same as chat interruption right now
@ -1117,7 +1112,7 @@ We only need to do it for files that were edited since `from`, ie files between
this._setThreadState(threadId, { currCheckpointIdx: null }) // no longer at a checkpoint because started streaming
this._wrapRunAgentToNotify(
this._runChatAgent({ threadId, userMessageContent, ...this._currentModelSelectionProps(), }),
this._runChatAgent({ threadId, ...this._currentModelSelectionProps(), }),
threadId,
)
}
@ -1221,7 +1216,7 @@ We only need to do it for files that were edited since `from`, ie files between
// else search codebase for `target`
let uris: URI[] = []
try {
const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, include: null, pageNumber: 0 })
const { result } = await this._toolsService.callTool['search_pathnames_only']({ queryStr: target, searchInFolder: null, pageNumber: 0 })
uris = result.uris
} catch (e) {
return null

View file

@ -15,18 +15,17 @@ import { IExplorerService } from '../../files/browser/files.js';
import { SortOrder } from '../../files/common/files.js';
import { ExplorerItem } from '../../files/common/explorerModel.js';
import { VoidDirectoryItem } from '../common/directoryStrTypes.js';
import { MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
const MAX_CHARS_TOTAL_BEGINNING = 20_000
const MAX_CHARS_TOTAL_TOOL = 20_000
// const MAX_FILES_TOTAL = 200
export interface IDirectoryStrService {
readonly _serviceBrand: undefined;
getDirectoryStrTool(uri: URI): Promise<{ wasCutOff: boolean, str: string }>
getAllDirectoriesStr(): Promise<{ wasCutOff: boolean, str: string }>
getDirectoryStrTool(uri: URI): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise<string>
}
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
@ -275,20 +274,21 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
if (!eRoot) throw new Error(`There was a problem reading the URI: ${uri.fsPath}.`)
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_CHARS_TOTAL_TOOL);
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_TOOL);
return {
str: `Directory of ${uri.fsPath}:\n${content}`,
wasCutOff,
}
let c = content.substring(0, MAX_DIRSTR_CHARS_TOTAL_TOOL)
c = `Directory of ${uri.fsPath}:\n${content}`
if (wasCutOff) c = `${c}\n...Result was truncated...`
return c
}
async getAllDirectoriesStr() {
async getAllDirectoriesStr({ cutOffMessage }: { cutOffMessage: string }) {
let str: string = '';
let cutOff = false;
const folders = this.workspaceContextService.getWorkspace().folders;
if (folders.length === 0)
return { str: '(NO WORKSPACE OPEN)', wasCutOff: false };
return '(NO WORKSPACE OPEN)';
for (let i = 0; i < folders.length; i += 1) {
if (i > 0) str += '\n';
@ -304,7 +304,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
// Use our new approach with direct explorer service
const dirTree = await computeDirectoryTree(eRoot, this.explorerService);
console.log('dirtree', dirTree)
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_CHARS_TOTAL_BEGINNING - str.length);
const { content, wasCutOff } = stringifyDirectoryTree(dirTree, MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length);
str += content;
if (wasCutOff) {
cutOff = true;
@ -312,7 +312,10 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
}
}
return { wasCutOff: cutOff, str };
if (cutOff) {
return `${str}\n${cutOffMessage}`
}
return str
}
}

View file

@ -1334,17 +1334,19 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin
const InvalidTool = ({ toolName }: { toolName: string }) => {
const InvalidTool = ({ toolName }: { toolName: ToolName }) => {
const accessor = useAccessor()
const title = getTitle({ name: toolName, type: 'invalid_params' })
const desc1 = 'Invalid parameters'
const icon = null
const isError = true
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
componentParams.children
return <ToolHeaderWrapper {...componentParams} />
}
const CanceledTool = ({ toolName }: { toolName: string }) => {
const CanceledTool = ({ toolName }: { toolName: ToolName }) => {
const accessor = useAccessor()
const title = getTitle({ name: toolName, type: 'rejected' })
const desc1 = ''
@ -2006,9 +2008,9 @@ export const SidebarChat = () => {
const isRunning = currThreadStreamState?.isRunning
const latestError = currThreadStreamState?.error
const displayContentSoFar = currThreadStreamState?.displayContentSoFar
const toolCallSoFar = currThreadStreamState?.toolCallSoFar
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
const toolCallSoFar = currThreadStreamState?.toolCallSoFar
const toolIsGenerating = !!toolCallSoFar && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit)
// ----- SIDEBAR CHAT state (local) -----
@ -2090,6 +2092,7 @@ export const SidebarChat = () => {
role: 'assistant',
displayContent: displayContentSoFar ?? '',
reasoning: reasoningSoFar ?? '',
toolCall: toolCallSoFar,
anthropicReasoning: null,
}}
messageIdx={streamingChatIdx}

View file

@ -16,7 +16,7 @@ import { IVoidCommandBarService } from './voidCommandBarService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
import { IMarkerService } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { ParsedToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { ToolName } from '../common/prompt/prompts.js'
@ -25,7 +25,7 @@ import { ToolName } from '../common/prompt/prompts.js'
type ValidateParams = { [T in ToolName]: (p: ParsedToolParamsObj) => Promise<ToolCallParams[T]> }
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => Promise<ToolCallParams[T]> }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
@ -45,7 +45,7 @@ const isFalsy = (u: unknown) => {
}
const validateStr = (argName: string, value: unknown) => {
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string.`)
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but it's a ${typeof value}. Value: ${value}.`)
return value
}
@ -53,7 +53,7 @@ const validateStr = (argName: string, value: unknown) => {
// We are NOT checking to make sure in workspace
// TODO!!!! check to make sure folder/file exists
const validateURI = (uriStr: unknown) => {
if (typeof uriStr !== 'string') throw new Error('Invalid LLM output format: Provided uri must be a string.')
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a ${typeof uriStr}. Value: ${uriStr}.`)
const uri = URI.file(uriStr)
return uri
}
@ -92,6 +92,7 @@ const validateNumber = (numStr: unknown, opts: { default: number | null }) => {
}
const validateRecursiveParamStr = (paramsUnknown: unknown) => {
if (!paramsUnknown) return false
if (typeof paramsUnknown !== 'string') throw new Error('Invalid LLM output format: Error calling tool: provided params must be a string.')
const params = paramsUnknown
const isRecursive = params.includes('r')
@ -155,8 +156,8 @@ export class ToolsService implements IToolsService {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
this.validateParams = {
read_file: async (params: ParsedToolParamsObj) => {
const { uri: uriStr, startLine: startLineUnknown, endLine: endLineUnknown, pageNumber: pageNumberUnknown } = params
read_file: async (params: RawToolParamsObj) => {
const { uri: uriStr, start_line: startLineUnknown, end_line: endLineUnknown, page_number: pageNumberUnknown } = params
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
@ -165,38 +166,38 @@ export class ToolsService implements IToolsService {
return { uri, startLine, endLine, pageNumber }
},
ls_dir: async (params: ParsedToolParamsObj) => {
const { uri: uriStr, pageNumber: pageNumberUnknown } = params
ls_dir: async (params: RawToolParamsObj) => {
const { uri: uriStr, page_number: pageNumberUnknown } = params
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
return { rootURI: uri, pageNumber }
},
get_dir_structure: async (params: ParsedToolParamsObj) => {
get_dir_structure: async (params: RawToolParamsObj) => {
const { uri: uriStr, } = params
const uri = validateURI(uriStr)
return { rootURI: uri }
},
search_pathnames_only: async (params: ParsedToolParamsObj) => {
search_pathnames_only: async (params: RawToolParamsObj) => {
const {
query: queryUnknown,
include: includeUnknown,
pageNumber: pageNumberUnknown
search_in_folder: includeUnknown,
page_number: pageNumberUnknown
} = params
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
const include = validateOptionalStr('include', includeUnknown)
const searchInFolder = validateOptionalStr('search_in_folder', includeUnknown)
return { queryStr, include, pageNumber }
return { queryStr, searchInFolder, pageNumber }
},
search_files: async (params: ParsedToolParamsObj) => {
search_files: async (params: RawToolParamsObj) => {
const {
query: queryUnknown,
searchInFolder: searchInFolderUnknown,
isRegex: isRegexUnknown,
pageNumber: pageNumberUnknown
search_in_folder: searchInFolderUnknown,
is_regex: isRegexUnknown,
page_number: pageNumberUnknown
} = params
const queryStr = validateStr('query', queryUnknown)
@ -210,7 +211,7 @@ export class ToolsService implements IToolsService {
// ---
create_file_or_folder: async (params: ParsedToolParamsObj) => {
create_file_or_folder: async (params: RawToolParamsObj) => {
const { uri: uriUnknown } = params
const uri = validateURI(uriUnknown)
const uriStr = validateStr('uri', uriUnknown)
@ -218,7 +219,7 @@ export class ToolsService implements IToolsService {
return { uri, isFolder }
},
delete_file_or_folder: async (params: ParsedToolParamsObj) => {
delete_file_or_folder: async (params: RawToolParamsObj) => {
const { uri: uriUnknown, params: paramsStr } = params
const uri = validateURI(uriUnknown)
const isRecursive = validateRecursiveParamStr(paramsStr)
@ -227,15 +228,15 @@ export class ToolsService implements IToolsService {
return { uri, isRecursive, isFolder }
},
edit_file: async (params: ParsedToolParamsObj) => {
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = params
edit_file: async (params: RawToolParamsObj) => {
const { uri: uriStr, change_description: changeDescriptionUnknown } = params
const uri = validateURI(uriStr)
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
return { uri, changeDescription }
},
run_terminal_command: async (params: ParsedToolParamsObj) => {
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = params
run_terminal_command: async (params: RawToolParamsObj) => {
const { command: commandUnknown, terminal_id: terminalIdUnknown, wait_for_completion: waitForCompletionUnknown } = params
const command = validateStr('command', commandUnknown)
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
const waitForCompletion = validateBoolean(waitForCompletionUnknown, { default: true })
@ -275,17 +276,15 @@ export class ToolsService implements IToolsService {
},
get_dir_structure: async ({ rootURI }) => {
const result = await this.directoryStrService.getDirectoryStrTool(rootURI)
let str = result.str
if (result.wasCutOff) str += '\n(Result was truncated)'
const str = await this.directoryStrService.getDirectoryStrTool(rootURI)
return { result: { str } }
},
search_pathnames_only: async ({ queryStr, include, pageNumber }) => {
search_pathnames_only: async ({ queryStr, searchInFolder, pageNumber }) => {
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
filePattern: queryStr,
includePattern: include ?? undefined,
includePattern: searchInFolder ?? undefined,
})
const data = await searchService.fileSearch(query, CancellationToken.None)

View file

@ -6,7 +6,7 @@
import { URI } from '../../../../base/common/uri.js';
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
import { ToolName } from './prompt/prompts.js';
import { AnthropicReasoning } from './sendLLMMessageTypes.js';
import { AnthropicReasoning, RawToolCallObj } from './sendLLMMessageTypes.js';
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
export type ToolMessage<T extends ToolName> = {
@ -14,7 +14,7 @@ export type ToolMessage<T extends ToolName> = {
content: string; // give this result to LLM (string of value)
} & (
// in order of events:
| { type: 'invalid_params', result: null, params: null, name: string }
| { type: 'invalid_params', result: null, name: T, params: RawToolCallObj | null, }
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
@ -27,7 +27,7 @@ export type ToolMessage<T extends ToolName> = {
export type DecorativeCanceledTool = {
role: 'interrupted_streaming_tool';
name: string;
name: ToolName;
}
@ -58,6 +58,7 @@ export type ChatMessage =
role: 'assistant';
displayContent: string; // content received from LLM - allowed to be '', will be replaced with (empty)
reasoning: string; // reasoning from the LLM, used for step-by-step thinking
toolCall: RawToolCallObj | undefined;
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
}

View file

@ -3,16 +3,24 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { os } from '../helpers/systemInfo.js';
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { ChatMode } from '../voidSettingsTypes.js';
import { os } from '../helpers/systemInfo.js';
import { RawToolCallObj } from '../sendLLMMessageTypes.js';
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { ChatMode } from '../voidSettingsTypes.js';
// this is just for ease of readability
export const tripleTick = ['```', '```']
export const MAX_DIRSTR_CHARS_TOTAL_BEGINNING = 20_000
export const MAX_DIRSTR_CHARS_TOTAL_TOOL = 20_000
export const MAX_PREFIX_SUFFIX_CHARS = 20_000
// ======================================================== tools ========================================================
const changesExampleContent = `\
// ... existing code ...
// {{change 1}}
@ -27,16 +35,13 @@ ${tripleTick[0]}
${changesExampleContent}
${tripleTick[1]}`
const fileNameEdit = `${tripleTick[0]}typescript
const fileNameEditExample = `${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
${changesExampleContent}
${tripleTick[1]}`
// ======================================================== tools ========================================================
export type InternalToolInfo = {
name: string,
description: string,
@ -47,19 +52,12 @@ export type InternalToolInfo = {
const paginationHelper = {
desc: `Very large results may be paginated (a note will always be included if pagination took place). Pagination fails gracefully if out of bounds or invalid page number.`,
param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, }
} as const
const uriParam = (object: string) => ({
uri: { description: `The FULL path to the ${object} from the root of the file system.` }
uri: { description: `The FULL path to the ${object}.` }
})
const searchParams = {
searchInFolder: { description: 'Only search files in this given folder. Leave as empty to search all available files.' },
isRegex: { description: 'Whether to treat the query as a regular expression. Default is "false".' },
const paginationParam = {
page_number: { description: 'Optional. The page number of the result. Default is 1.' }
} as const
@ -68,27 +66,27 @@ export const voidTools = {
read_file: {
name: 'read_file',
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
description: `Returns file contents of a given URI.`,
params: {
...uriParam('file'),
startLine: { description: 'Line to start reading from. Default is "null", treated as 1.' },
endLine: { description: 'Line to stop reading from (inclusive). Default is "null", treated as Infinity.' },
...paginationHelper.param,
start_line: { description: 'Optional. Default is 1. Start reading on this line.' },
end_line: { description: 'Optional. Default is Infinity. Stop reading after this line.' },
...paginationParam,
},
},
ls_dir: {
name: 'ls_dir',
description: `Returns all file names and folder names in a given folder. ${paginationHelper.desc}`,
description: `Lists all files and folders in the given URI.`,
params: {
...uriParam('folder'),
...paginationHelper.param,
...paginationParam,
},
},
get_dir_structure: {
name: 'get_dir_structure',
description: `This is a very effective way to learn about the user's codebase. You might want to use this instead of ls_dir. Returns a tree diagram of all the files and folders in the given folder URI. If results are large, the given string will be truncated (this will be indicated), in which case you might want to call this tool on a lower folder to get better results, or just use ls_dir which supports pagination.`,
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
params: {
...uriParam('folder')
}
@ -96,21 +94,22 @@ export const voidTools = {
search_pathnames_only: {
name: 'search_pathnames_only',
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
params: {
query: { description: `Your query for the search.` },
...searchParams,
...paginationHelper.param,
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
...paginationParam,
},
},
search_files: {
name: 'search_files',
description: `Returns all pathnames that match a given query (searches ONLY file contents). The query can be any substring or glob. This is often followed by the \`read_file\` tool to view the full file contents of results. ${paginationHelper.desc}`,
description: `Returns all pathnames that match a given query (searches ONLY file contents). The query can be any substring or glob. You can follow this with read_file to view result contents.`,
params: {
query: { description: `Your query for the search.` },
...searchParams,
...paginationHelper.param,
search_in_folder: { description: 'Optional. Only search files in this given folder glob.' },
is_regex: { description: 'Optional. Default is false. Whether query is a regex.' },
...paginationParam,
},
},
@ -118,7 +117,7 @@ export const voidTools = {
create_file_or_folder: {
name: 'create_file_or_folder',
description: `Create 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.`,
description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash.`,
params: {
...uriParam('file or folder'),
},
@ -126,25 +125,25 @@ export const voidTools = {
delete_file_or_folder: {
name: 'delete_file_or_folder',
description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
description: `Delete a file or folder at the given path.`,
params: {
...uriParam('file or folder'),
params: { description: 'Return -r here to delete recursively (if applicable). Default is the empty string.' }
params: { description: 'Optional. Return -r here to delete recursively.' }
},
},
edit_file: { // APPLY TOOL
name: 'edit_file',
description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`,
description: `Edits the contents of a file given the file's URI and a description.`,
params: {
...uriParam('file'),
changeDescription: {
change_description: {
description: `\
- Your changeDescription should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.
- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
- You must output your description in triple backticks.
Here's an example of a good description:\n${editToolDescriptionExample}.`
A brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
NEVER re-write the whole file. Instead, use comments like "// ... existing code ...". Bias towards writing as little as possible. \
Your description will be handed to a smaller model to make the change, so it must be clear and concise. \
Your description MUST be wrapped in triple backticks. \
Here's an example of a good description:\n${editToolDescriptionExample}`
}
},
},
@ -154,12 +153,11 @@ Here's an example of a good description:\n${editToolDescriptionExample}.`
description: `Executes a terminal command.`,
params: {
command: { description: 'The terminal command to execute.' },
waitForCompletion: { description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
terminalId: { description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
wait_for_completion: { description: `Optional. Default is true. Make this value false when you want a command to run without waiting for it to complete.` },
terminal_id: { description: 'Optional. The ID of the terminal instance that should execute the command (if not provided, defaults to the preferred terminal ID). The primary purpose of this is to let you open a new terminal for testing or background processes (e.g. running a dev server for the user in a separate terminal). Must be an integer >= 1.' },
},
},
// go_to_definition
// go_to_usages
@ -169,6 +167,9 @@ Here's an example of a good description:\n${editToolDescriptionExample}.`
export type ToolName = keyof typeof voidTools
export const toolNames = Object.keys(voidTools) as ToolName[]
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
export type ToolParamName = { [T in ToolName]: ToolParamNameOfTool<T> }[ToolName]
const toolNamesSet = new Set<string>(toolNames)
export const isAToolName = (toolName: string): toolName is ToolName => {
@ -186,11 +187,11 @@ export const availableTools = (chatMode: ChatMode) => {
return tools
}
const availableToolsStr = (tools: InternalToolInfo[]) => {
const availableXMLToolsStr = (tools: InternalToolInfo[]) => {
return `${tools.map((t, i) => {
const params = Object.keys(t.params).map(paramName => ` <${paramName}>\n${t.params[paramName].description}\n </${paramName}>`).join('\n')
const params = Object.keys(t.params).map(paramName => `<${paramName}>${t.params[paramName].description}</${paramName}>`).join('\n')
return `\
${i}. ${t.name}
${i + 1}. ${t.name}
Description: ${t.description}
Format:
<${t.name}>${!params ? '' : `\n${params}`}
@ -198,112 +199,152 @@ Format:
}).join('\n\n')}`
}
const systemToolsPrompt = (chatMode: ChatMode) => {
export const toolCallXMLStr = (toolCall: RawToolCallObj) => {
const t = toolCall
const params = Object.keys(t.rawParams).map(paramName => `<${paramName}>${t.rawParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
return `\
<${toolCall.name}>${!params ? '' : `\n${params}`}
</${toolCall.name}>`
.replace('\t', ' ')
}
/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
const tools = availableTools(chatMode)
if (!tools || tools.length === 0) return ''
return `\
You are allowed to call tools in your response.
Tool calling guidelines:
${chatMode === 'agent' ? `\
- Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.
- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool.
- You will OFTEN need to gather context before making a change. Do not immediately make a change unless you have ALL relevant context.
- ALWAYS have maximal certainty in a change BEFORE you make it. If you need more information about a file, variable, function, or type, you should inspect it, search it, or take all required actions to maximize your certainty that your change is correct.`
: chatMode === 'gather' ? `\
- Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.
- You should extensively read files, types, content, etc and gather relevant context.`
: chatMode === 'normal' ? ''
: ''}
- If you think you should use tools, you do not need to ask for permission.
- NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results.
- Some tools only work if the user has a workspace open.${chatMode === 'agent' ? `
- NEVER modify a file outside the user's workspace(s) without permission from the user.` : ''}\
const toolXMLDefinitions = (`\
Available tools:
${availableToolsStr(tools)}
Tool calling details: ${''/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */}
- Tool calling is optional.
- To call a tool, just write its name followed by any parameters in XML format. For example:
<tool_name>
<parameter1>
value1
</parameter1>
<parameter2>
value2
</parameter2>
</tool_name>
- You must write your tool call at the END of your response. The beginning of your response should be normal text, explanations, etc (if you decide to write anything), followed by the tool call at the END.
- You are only allowed to output one tool call per response.
- You may omit optional parameters.
- The tool call will be executed immediately, and you will have access to the results in your next response.`
${availableXMLToolsStr(tools)}`)
const toolCallXMLGuidelines = (`\
Tool calling details:
- Once you write a tool call, you must STOP and WAIT for the result.
- All parameters are REQUIRED unless noted otherwise.
- To call a tool, write its name and parameters in one of the XML formats specified above.
- You are only allowed to output ONE tool call, and it must be at the END of your response.
- Your tool call will be executed immediately, and the results will appear in the following user message.`)
return `\
${toolXMLDefinitions}
${toolCallXMLGuidelines}`
}
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
// ======================================================== chat (normal, gather, agent) ========================================================
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => `\
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the user's IDE called Void. Your job is \
${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes.`
: mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.`
: mode === 'normal' ? `to assist the user with their coding tasks.`
: ''}
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`.
Please assist the user with their query. The user's query is never invalid.
${/* tool use */ mode === 'agent' || mode === 'gather' ? `\
${systemToolsPrompt(mode)}
\
`: `\
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
\
`}
${/* code blocks */ mode === 'agent' ? `\
Behavior:
- Always use tools (edit, terminal, etc) to take actions and implement changes. Don't just describe them.
- Prioritize taking as many steps as you need to complete your request over stopping early.\
`: `\
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks).
- The first line of the code block must be the FULL PATH of the file you want to change.
- The remaining contents should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.
- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
Here's an example of a good code block:\n${fileNameEdit}.
If you write a code block that's related to a specific file, please use the same format as above:
- The first line of the code block must be the FULL PATH of the related file if known.
- The remaining contents of the file should proceed as usual.
\
`}
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, runningTerminalIds, directoryStr, chatMode: mode }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, runningTerminalIds: string[], chatMode: ChatMode }) => {
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
: mode === 'normal' ? `to assist the user with their coding tasks.`
: ''}
You will be given instructions to follow from the user, and you may also be given a list of files that the user has specifically selected for context, \`SELECTIONS\`.
Please assist the user with their query.`)
${/* misc */''}
Misc:
- Do not make things up.
- Do not be lazy.
- NEVER re-write the entire file.
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.
- Today's date is ${new Date().toDateString()}
${/* system info */''}
The user's system information is as follows:
const sysInfo = (`Here is the user's system information:
<system_info>
- ${os}
- Open workspace(s): ${workspaceFolders.join(', ') || 'NO WORKSPACE OPEN'}
- Open tab(s): ${openedURIs.join(', ') || 'NO OPENED EDITORS'}
- Active tab: ${activeURI}
${(mode === 'agent') && runningTerminalIds.length !== 0 ? `
- Open workspaces:
${workspaceFolders.join('\n') || 'NO WORKSPACE OPEN'}
- Active file:
${activeURI}
- Open files:
${openedURIs.join('\n') || 'NO OPENED EDITORS'}${''/* separator */}${mode === 'agent' && runningTerminalIds.length !== 0 ? `
- Existing terminal IDs: ${runningTerminalIds.join(', ')}` : ''}
- The user's codebase is structured as follows:\n${directoryStr}
</system_info>`)
\
`.trim().replace('\t', ' ')
const fsInfo = (`Here is an overview of the user's file system:
<files_overview>
${directoryStr}
</files_overview>`)
const toolDefinitions = systemToolsXMLPrompt(mode)
const details: string[] = []
if (mode === 'agent' || mode === 'gather') {
details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`)
details.push('Only use ONE tool call at a time, and always wait for the result before proceeding.') // XML
details.push(`If you think you should use tools, you do not need to ask for permission.`)
details.push(`NEVER say something like "I'm going to use \`tool_name\`". Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc.`)
details.push(`Many tools only work if the user has a workspace open.`)
}
else {
details.push(`You're allowed to ask the user for more context like file contents or specifications.`)
}
if (mode === 'agent') {
details.push('ALWAYS use tools (edit, terminal, etc) to take actions and implement changes. For example, if you would like to edit a file, you MUST use a tool.')
details.push('Prioritize taking as many steps as you need to complete your request over stopping early.')
details.push(`You will OFTEN need to gather context before making a change. Do not immediately make a change unless you have ALL relevant context.`)
details.push(`ALWAYS have maximal certainty in a change BEFORE you make it. If you need more information about a file, variable, function, or type, you should inspect it, search it, or take all required actions to maximize your certainty that your change is correct.`)
details.push(`NEVER modify a file outside the user's workspace(s) without permission from the user.`)
}
if (mode === 'gather') {
details.push(`Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.`)
details.push(`You should extensively read files, types, content, etc and gather relevant context.`)
}
if (mode === 'gather' || mode === 'normal') {
details.push(`If you write any code blocks, please use this format:
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents of the file should proceed as usual.`)
details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S).
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents should be \
a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. \
NEVER re-write the whole file. Instead, use comments like "// ... existing code ...". Bias towards writing as little as possible. \
Here's an example of a good edit suggestion:
${fileNameEditExample}.`)
}
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
details.push(`Today's date is ${new Date().toDateString()}.`)
const importantDetails = (`Important notes:
${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
// return answer
const ansStrs: string[] = []
ansStrs.push(header)
ansStrs.push(sysInfo)
ansStrs.push(fsInfo)
if (toolDefinitions) ansStrs.push(toolDefinitions)
ansStrs.push(importantDetails)
ansStrs.push('Now, please assist the user with their query.')
const fullSystemMsgStr = ansStrs
.join('\n\n\n')
.trim()
.replace('\t', ' ')
return fullSystemMsgStr
}
// log all prompts
for (const chatMode of ['agent', 'gather', 'normal'] satisfies ChatMode[]) {
console.log(`========================================= SYSTEM MESSAGE FOR ${chatMode} ===================================\n`,
chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', runningTerminalIds: [], directoryStr: 'lol', }))
}
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
@ -458,8 +499,6 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF
const fullFileLines = fullFileStr.split('\n')
// we can optimize this later
const MAX_PREFIX_SUFFIX_CHARS = 20_000
/*
a

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ToolName } from './prompt/prompts.js'
import { ToolName, ToolParamName } from './prompt/prompts.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
@ -40,13 +40,13 @@ export type LLMChatMessage = {
}
export type ParsedToolParamsObj = {
[paramName: string]: string | undefined;
export type RawToolParamsObj = {
[paramName in ToolParamName]?: string;
}
export type RawToolCallObj = {
name: ToolName;
rawParams: ParsedToolParamsObj;
doneParams: string[];
rawParams: RawToolParamsObj;
doneParams: ToolParamName[];
isDone: boolean;
};

View file

@ -24,7 +24,7 @@ export type ToolCallParams = {
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
'ls_dir': { rootURI: URI, pageNumber: number },
'get_dir_structure': { rootURI: URI },
'search_pathnames_only': { queryStr: string, include: string | null, pageNumber: number },
'search_pathnames_only': { queryStr: string, searchInFolder: string | null, pageNumber: number },
'search_files': { queryStr: string, isRegex: boolean, searchInFolder: URI | null, pageNumber: number },
// ---
'edit_file': { uri: URI, changeDescription: string },

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import { endsWithAnyPrefixOf } from '../../common/helpers/extractCodeFromResult.js'
import { availableTools, InternalToolInfo, ToolName } from '../../common/prompt/prompts.js'
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
import { OnFinalMessage, OnText, RawToolCallObj } from '../../common/sendLLMMessageTypes.js'
import { ChatMode } from '../../common/voidSettingsTypes.js'
import { createSaxParser } from './sax.js'
@ -141,12 +141,12 @@ type ToolsState = {
level: 'normal',
} | {
level: 'tool',
toolName: string,
toolName: ToolName,
currentToolCall: RawToolCallObj,
} | {
level: 'param',
toolName: string,
paramName: string,
toolName: ToolName,
paramName: ToolParamName,
currentToolCall: RawToolCallObj,
}
@ -162,7 +162,7 @@ export const extractToolsWrapper = (
// detect <availableTools[0]></availableTools[0]>, etc
let fullText = '';
let trueFullText = ''
const currentToolCalls: RawToolCallObj[] = []; // the answer
const firstToolCallRef: { current: RawToolCallObj | undefined } = { current: undefined }
let state: ToolsState = { level: 'normal' }
@ -170,7 +170,7 @@ export const extractToolsWrapper = (
const getRawNewText = () => {
return trueFullText.substring(parser.startTagPosition, parser.position + 1)
}
const parser = createSaxParser({ lowercase: true })
const parser = createSaxParser()
// when see open tag <tagName>
parser.onopentag = (node) => {
@ -183,9 +183,10 @@ export const extractToolsWrapper = (
if (tagName in toolOfToolName) { // valid toolName
state = {
level: 'tool',
toolName: tagName,
toolName: tagName as ToolName,
currentToolCall: { name: tagName as ToolName, rawParams: {}, doneParams: [], isDone: false }
}
firstToolCallRef.current = state.currentToolCall
}
else {
fullText += rawNewText // count as plaintext
@ -198,7 +199,7 @@ export const extractToolsWrapper = (
state = {
level: 'param',
toolName: state.toolName,
paramName: tagName,
paramName: tagName as ToolParamName,
currentToolCall: state.currentToolCall,
}
}
@ -229,7 +230,6 @@ export const extractToolsWrapper = (
else if (state.level === 'tool') {
if (tagName === state.toolName) { // closed the tool
state.currentToolCall.isDone = true
currentToolCalls.push(state.currentToolCall)
state = {
level: 'normal',
}
@ -287,33 +287,31 @@ export const extractToolsWrapper = (
onText({
...params,
fullText,
toolCall: currentToolCalls.length > 0 ? currentToolCalls[0] : undefined
toolCall: firstToolCallRef.current,
});
};
const newOnFinalMessage: OnFinalMessage = (params) => {
// treat like just got text before calling onFinalMessage (or else we sometimes miss the final chunk that's new to finalMessage)
console.log('final message!!!', trueFullText)
console.log('----- returning ----\n', fullText)
console.log('----- tools ----\n', JSON.stringify(currentToolCalls, null, 2))
newOnText({ ...params })
console.log('final message!!!', trueFullText)
console.log('----- returning ----\n', fullText)
console.log('----- tools ----\n', JSON.stringify(currentToolCalls, null, 2))
fullText = fullText.trimEnd()
const toolCall = currentToolCalls.length > 0 ? currentToolCalls[0] : undefined
const toolCall = firstToolCallRef.current
if (toolCall) {
// trim off all whitespace at and before first \n and after last \n for each param
for (const paramName in toolCall.rawParams) {
for (const p in toolCall.rawParams) {
const paramName = p as ToolParamName
const orig = toolCall.rawParams[paramName]
if (orig === undefined) continue
toolCall.rawParams[paramName] = trimBeforeAndAfterNewLines(orig)
}
}
console.log('----- toolCall ----\n', JSON.stringify(toolCall, null, 2))
// console.log('final message!!!', trueFullText)
// console.log('----- returning ----\n', fullText)
// console.log('----- tools ----\n', JSON.stringify(firstToolCallRef.current, null, 2))
// console.log('----- toolCall ----\n', JSON.stringify(toolCall, null, 2))
onFinalMessage({ ...params, fullText, toolCall: toolCall })
}