This commit is contained in:
Andrew Pareles 2025-04-07 23:05:03 -07:00
parent 1c5adb96d3
commit 6f693c4d0a
10 changed files with 65 additions and 49 deletions

View file

@ -11,13 +11,13 @@ 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, } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, ParsedToolCallObj, ParsedToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { chat_userMessageContent, chat_systemMessage, ToolName, } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, RawToolCallObj, ParsedToolParamsObj } 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 { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
import { ToolCallParams, ToolResultType, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
import { IToolsService } from './toolsService.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
@ -148,7 +148,7 @@ export type ThreadStreamState = {
streamingToken?: string;
messageSoFar?: string;
reasoningSoFar?: string;
toolCallSoFar?: ParsedToolCallObj;
toolCallSoFar?: RawToolCallObj;
}
}
@ -700,8 +700,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
isRunningWhenEnd = undefined
nMessagesSent += 1
let resMessageIsDonePromise: (toolCall?: ParsedToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<ParsedToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
// send llm message
this._setStreamState(threadId, { isRunning: 'LLM' }, 'merge')
@ -752,7 +752,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
// call tool if there is one
const tool: ParsedToolCallObj | undefined = toolCall
const tool: RawToolCallObj | undefined = toolCall
if (tool) {
const { awaitingUserApproval, interrupted } = await handleToolCall(tool.name, { preapproved: false, unvalidatedToolParams: tool.rawParams })

View file

@ -23,9 +23,10 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, Undo, Undo2, X } from 'lucide-react';
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
import { ToolCallParams } from '../../../../common/toolsServiceTypes.js';
import { ApplyButtonsHTML, CopyButton, JumpToFileButton, JumpToTerminalButton, StatusIndicatorHTML, useApplyButtonState } from '../markdown/ApplyBlockHoverButtons.js';
import { IsRunningType } from '../../../chatThreadService.js';
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
@ -2007,9 +2008,8 @@ export const SidebarChat = () => {
const messageSoFar = currThreadStreamState?.messageSoFar
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
const toolNameSoFar = currThreadStreamState?.toolNameSoFar
const toolParamsSoFar = currThreadStreamState?.toolParamsSoFar
const toolIsGenerating = !!toolNameSoFar && toolNameSoFar === 'edit_file' // show loading for slow tools (right now just edit)
const toolCallSoFar = currThreadStreamState?.toolCallSoFar
const toolIsGenerating = !!toolCallSoFar && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit)
// ----- SIDEBAR CHAT state (local) -----
@ -2101,7 +2101,7 @@ export const SidebarChat = () => {
/> : null
const generatingToolTitle = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar
const generatingToolTitle = toolCallSoFar && toolNames.includes(toolCallSoFar.name as ToolName) ? titleOfToolName[toolCallSoFar.name as ToolName]?.proposed : toolCallSoFar?.name
const messagesHTML = <ScrollToBottomContainer
key={'messages' + chatThreadsState.currentThreadId} // force rerender on all children if id changes

View file

@ -90,7 +90,7 @@ export const SidebarThreadSelector = () => {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter((msg) => msg.role !== 'tool_request').length;
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
return (
<li key={pastThread.id}>

View file

@ -8,7 +8,7 @@ import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
import { ISearchService } from '../../../services/search/common/search.js'
import { IEditCodeService } from './editCodeServiceInterface.js'
import { ITerminalToolService } from './terminalToolService.js'
import { ToolCallParams, ToolName, ToolResultType } from '../common/toolsServiceTypes.js'
import { ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'
import { IVoidModelService } from '../common/voidModelService.js'
import { EndOfLinePreference } from '../../../../editor/common/model.js'
import { basename } from '../../../../base/common/path.js'
@ -17,6 +17,7 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree
import { IMarkerService } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { ParsedToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { ToolName } from '../common/prompt/prompts.js'
// tool use for AI

View file

@ -5,8 +5,9 @@
import { URI } from '../../../../base/common/uri.js';
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
import { ToolName } from './prompt/prompts.js';
import { AnthropicReasoning } from './sendLLMMessageTypes.js';
import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
export type ToolMessage<T extends ToolName> = {
role: 'tool';

View file

@ -6,7 +6,7 @@
import { os } from '../helpers/systemInfo.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { ChatMode } from '../voidSettingsTypes.js';
import { ToolName, toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
import { toolNamesThatRequireApproval } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
@ -153,7 +153,7 @@ Here's an example of a good description:\n${editToolDescriptionExample}.`
name: 'run_terminal_command',
description: `Executes a terminal command.`,
params: {
command: { description: 'The terminal command to execute. Typically you should pipe to cat to avoid pagination.' },
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.' },
},
@ -166,6 +166,16 @@ Here's an example of a good description:\n${editToolDescriptionExample}.`
} satisfies { [name: string]: InternalToolInfo }
export type ToolName = keyof typeof voidTools
export const toolNames = Object.keys(voidTools) as ToolName[]
const toolNamesSet = new Set<string>(toolNames)
export const isAToolName = (toolName: string): toolName is ToolName => {
const isAToolName = toolNamesSet.has(toolName)
return isAToolName
}
export const availableTools = (chatMode: ChatMode) => {
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName))

View file

@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ToolName } from './toolsServiceTypes.js'
import { ToolName } from './prompt/prompts.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
@ -43,7 +43,7 @@ export type LLMChatMessage = {
export type ParsedToolParamsObj = {
[paramName: string]: string;
}
export type ParsedToolCallObj = {
export type RawToolCallObj = {
name: ToolName;
rawParams: ParsedToolParamsObj;
doneParams: string[];
@ -53,8 +53,8 @@ export type ParsedToolCallObj = {
export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any })
export type OnText = (p: { fullText: string; fullReasoning: string; toolCall?: ParsedToolCallObj }) => void
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCall?: ParsedToolCallObj; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
export type OnText = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj }) => void
export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCall?: RawToolCallObj; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id
export type OnError = (p: { message: string; fullError: Error | null }) => void
export type OnAbort = () => void
export type AbortRef = { current: (() => void) | null }

View file

@ -1,5 +1,5 @@
import { URI } from '../../../../base/common/uri.js'
import { voidTools } from './prompt/prompts.js';
import { ToolName } from './prompt/prompts.js';
@ -14,15 +14,6 @@ export type ShallowDirectoryItem = {
}
export type ToolName = keyof typeof voidTools
export const toolNames = Object.keys(voidTools) as ToolName[]
const toolNamesSet = new Set<string>(toolNames)
export const isAToolName = (toolName: string): toolName is ToolName => {
const isAToolName = toolNamesSet.has(toolName)
return isAToolName
}
const toolNamesWithApproval = ['create_file_or_folder', 'delete_file_or_folder', 'edit_file', 'run_terminal_command'] as const satisfies readonly ToolName[]
export type ToolNameWithApproval = typeof toolNamesWithApproval[number]

View file

@ -1,8 +1,8 @@
import { endsWithAnyPrefixOf } from '../../common/helpers/extractCodeFromResult.js'
import { InternalToolInfo } from '../../common/prompt/prompts.js'
import { OnText, ParsedToolCallObj } from '../../common/sendLLMMessageTypes.js'
import { availableTools, InternalToolInfo, ToolName } from '../../common/prompt/prompts.js'
import { OnText, RawToolCallObj } from '../../common/sendLLMMessageTypes.js'
import { ChatMode } from '../../common/voidSettingsTypes.js'
import sax from 'sax'
import { ToolName } from '../../common/toolsServiceTypes.js'
// =============== reasoning ===============
@ -123,22 +123,25 @@ type ToolsState = {
} | {
level: 'tool',
toolName: string,
currentToolCall: ParsedToolCallObj,
currentToolCall: RawToolCallObj,
} | {
level: 'param',
toolName: string,
paramName: string,
currentToolCall: ParsedToolCallObj,
currentToolCall: RawToolCallObj,
}
export const extractToolsOnTextWrapper = (onText: OnText, availableTools: InternalToolInfo[]) => {
export const extractToolsOnTextWrapper = (onText: OnText, chatMode: ChatMode) => {
const tools = availableTools(chatMode)
if (!tools) return onText
const toolOfToolName: { [toolName: string]: InternalToolInfo | undefined } = {}
for (const t of availableTools) { toolOfToolName[t.name] = t }
for (const t of tools) { toolOfToolName[t.name] = t }
// detect <availableTools[0]></availableTools[0]>, etc
let fullText = '';
let trueFullText = ''
const currentToolCalls: ParsedToolCallObj[] = []; // the answer
const currentToolCalls: RawToolCallObj[] = []; // the answer
let state: ToolsState = { level: 'normal' }
@ -146,7 +149,9 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern
const getRawNewText = () => {
return trueFullText.substring(parser.startTagPosition, parser.position + 1)
}
const parser = sax.parser(false);
const parser = sax.parser(false, {
lowercase: true,
});
// when see open tag <tagName>
@ -186,7 +191,7 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern
};
parser.ontext = (text) => {
console.log('TEXT!', text)
console.log('TEXT!', JSON.stringify(text))
if (state.level === 'normal') {
fullText += text
}
@ -227,16 +232,23 @@ export const extractToolsOnTextWrapper = (onText: OnText, availableTools: Intern
currentToolCall: state.currentToolCall,
}
}
else {
fullText += rawNewText
}
}
};
let prevFullTextLen = 0
const newOnText: OnText = (params) => {
const newText = params.fullText.substring(fullText.length);
console.log('newText', state.level, newText)
const newText = params.fullText.substring(prevFullTextLen)
prevFullTextLen = params.fullText.length
trueFullText = params.fullText
console.log('newText', newText.length)
parser.write(newText)
console.log('calling ontext...')
onText({
...params,
fullText,

View file

@ -12,7 +12,6 @@ import { ChatMode, defaultProviderSettings, displayInfoOfProviderName, ModelSele
import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js';
import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper, extractToolsOnTextWrapper } from './extractGrammar.js';
import { availableTools } from '../../common/prompt/prompts.js';
type InternalCommonMessageParams = {
@ -162,10 +161,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage
// manually parse out tool results
if (chatMode) {
const tools = availableTools(chatMode)
if (tools) {
onText = extractToolsOnTextWrapper(onText, tools)
}
onText = extractToolsOnTextWrapper(onText, chatMode)
}
let fullReasoningSoFar = ''
@ -250,7 +246,7 @@ const _openaiCompatibleList = async ({ onSuccess: onSuccess_, onError: onError_,
// ------------ ANTHROPIC ------------
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions }: SendChatParams_Internal) => {
const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, chatMode }: SendChatParams_Internal) => {
const {
modelName,
supportsSystemMessage,
@ -284,6 +280,11 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM
...includeInPayload,
})
// manually parse out tool results
if (chatMode) {
onText = extractToolsOnTextWrapper(onText, chatMode)
}
// when receive text
let fullText = ''
let fullReasoning = ''