mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
redesign how tool use loop works
This commit is contained in:
parent
4a9af17736
commit
97b44a94e4
12 changed files with 292 additions and 301 deletions
|
|
@ -637,6 +637,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
|||
token: CancellationToken,
|
||||
): Promise<InlineCompletion[]> {
|
||||
|
||||
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
|
||||
if (!isEnabled) return []
|
||||
|
||||
const testMode = false
|
||||
|
||||
const docUriStr = model.uri.toString();
|
||||
|
|
@ -792,10 +795,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
|
|||
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
|
||||
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
|
||||
|
||||
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
|
||||
|
||||
// set parameters of `newAutocompletion` appropriately
|
||||
newAutocompletion.llmPromise = isEnabled ? new Promise((resolve, reject) => reject('Autocomplete is disabled')) : new Promise((resolve, reject) => {
|
||||
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
|
||||
|
||||
const requestId = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'FIMMessage',
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { ITextModelService } from '../../../../editor/common/services/resolverSe
|
|||
import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../common/chatThreadServiceTypes.js';
|
||||
import { Position } from '../../../../editor/common/core/position.js';
|
||||
import { ITerminalToolService } from './terminalToolService.js';
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
|
||||
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
|
||||
for (let i = arr.length - 1; i >= 0; i--) {
|
||||
|
|
@ -104,10 +105,14 @@ export type ThreadsState = {
|
|||
|
||||
export type ThreadStreamState = {
|
||||
[threadId: string]: undefined | {
|
||||
// state related
|
||||
isRunning?: undefined | true; // whether or not actually running the agent loop (can be running and not streaming, like if it's calling a tool and awaiting user response)
|
||||
error?: { message: string, fullError: Error | null, };
|
||||
|
||||
// streaming related
|
||||
streamingToken?: string;
|
||||
messageSoFar?: string;
|
||||
reasoningSoFar?: string;
|
||||
streamingToken?: string;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +140,7 @@ export interface IChatThreadService {
|
|||
readonly _serviceBrand: undefined;
|
||||
|
||||
readonly state: ThreadsState;
|
||||
readonly streamState: ThreadStreamState;
|
||||
readonly streamState: ThreadStreamState; // not persistent
|
||||
|
||||
onDidChangeCurrentThread: Event<void>;
|
||||
onDidChangeStreamState: Event<{ threadId: string }>
|
||||
|
|
@ -167,18 +172,18 @@ export interface IChatThreadService {
|
|||
closeStagingSelectionsInMessage(messageIdx: number): void;
|
||||
|
||||
|
||||
cancelStreaming(threadId: string): void;
|
||||
stopRunning(threadId: string): void;
|
||||
dismissStreamError(threadId: string): void;
|
||||
|
||||
// call to edit a message - CAN THROW
|
||||
editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise<void>;
|
||||
editUserMessageAndStreamResponse({ userMessage, messageIdx }: { userMessage: string, messageIdx: number }): Promise<void>;
|
||||
|
||||
// call to add a message - CAN THROW
|
||||
addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise<void>;
|
||||
addUserMessageAndStreamResponse({ userMessage }: { userMessage: string }): Promise<void>;
|
||||
|
||||
// approve/reject - CAN THROW
|
||||
approveTool(toolId: string): void;
|
||||
rejectTool(toolId: string): void;
|
||||
approveTool(threadId: string): void;
|
||||
rejectTool(threadId: string): void;
|
||||
}
|
||||
|
||||
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
|
||||
|
|
@ -205,6 +210,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
@ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService,
|
||||
@ITextModelService private readonly _textModelService: ITextModelService,
|
||||
@ITerminalToolService private readonly _terminalToolService: ITerminalToolService,
|
||||
@IMetricsService private readonly _metricsService: IMetricsService,
|
||||
) {
|
||||
super()
|
||||
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
|
||||
|
|
@ -266,7 +272,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// name: 'pathname_search',
|
||||
// params: { queryStr: 'hello', pageNumber: 0 },
|
||||
// paramsStr: '{"query": "hello", "pageNumber": 0}',
|
||||
// voidToolId: 'request-1',
|
||||
// id: 'request-1',
|
||||
// } satisfies ToolRequestApproval<'pathname_search'>,
|
||||
|
||||
{
|
||||
|
|
@ -295,7 +301,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// name: 'list_dir',
|
||||
// params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 },
|
||||
// paramsStr: '{"uri": "/Users/username/Documents"}',
|
||||
// voidToolId: 'request-2',
|
||||
// id: 'request-2',
|
||||
// } satisfies ToolRequestApproval<'list_dir'>,
|
||||
|
||||
{
|
||||
|
|
@ -316,7 +322,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// name: 'read_file',
|
||||
// params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 },
|
||||
// paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}',
|
||||
// voidToolId: 'request-3',
|
||||
// id: 'request-3',
|
||||
// } satisfies ToolRequestApproval<'read_file'>,
|
||||
|
||||
{
|
||||
|
|
@ -344,7 +350,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// name: 'search',
|
||||
// params: { queryStr: 'function main', pageNumber: 0 },
|
||||
// paramsStr: '{"query": "function main"}',
|
||||
// voidToolId: 'request-4',
|
||||
// id: 'request-4',
|
||||
// } satisfies ToolRequestApproval<'search'>,
|
||||
|
||||
// ---
|
||||
|
|
@ -366,7 +372,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
name: 'edit',
|
||||
params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' },
|
||||
paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}',
|
||||
voidToolId: 'request-5',
|
||||
id: 'request-5',
|
||||
} satisfies ToolRequestApproval<'edit'>,
|
||||
|
||||
{
|
||||
|
|
@ -386,7 +392,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
name: 'create_uri',
|
||||
params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false },
|
||||
paramsStr: '{"uri": "/Users/username/Project/new-file.js"}',
|
||||
voidToolId: 'request-6',
|
||||
id: 'request-6',
|
||||
} satisfies ToolRequestApproval<'create_uri'>,
|
||||
|
||||
{
|
||||
|
|
@ -406,7 +412,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
name: 'delete_uri',
|
||||
params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false },
|
||||
paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}',
|
||||
voidToolId: 'request-7',
|
||||
id: 'request-7',
|
||||
} satisfies ToolRequestApproval<'delete_uri'>,
|
||||
|
||||
{
|
||||
|
|
@ -431,7 +437,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
name: 'terminal_command',
|
||||
params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true },
|
||||
paramsStr: '{"command": "npm install", "waitForCompletion": "true"}',
|
||||
voidToolId: 'request-8',
|
||||
id: 'request-8',
|
||||
} satisfies ToolRequestApproval<'terminal_command'>,
|
||||
|
||||
|
||||
|
|
@ -493,11 +499,23 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
return prevMessages.flatMap(m => m.role === 'user' && m.selections || [])
|
||||
}
|
||||
|
||||
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>) {
|
||||
this.streamState[threadId] = {
|
||||
...this.streamState[threadId],
|
||||
...state
|
||||
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>, behavior: 'set' | 'merge') {
|
||||
if (state === undefined)
|
||||
delete this.streamState[threadId]
|
||||
|
||||
else {
|
||||
if (behavior === 'merge') {
|
||||
this.streamState[threadId] = {
|
||||
...this.streamState[threadId],
|
||||
...state
|
||||
}
|
||||
}
|
||||
else if (behavior === 'set') {
|
||||
this.streamState[threadId] = state
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
this._onDidChangeStreamState.fire({ threadId })
|
||||
}
|
||||
|
||||
|
|
@ -506,7 +524,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
|
||||
|
||||
async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) {
|
||||
async editUserMessageAndStreamResponse({ userMessage, messageIdx }: { userMessage: string, messageIdx: number }) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
|
||||
|
|
@ -531,46 +549,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}, true)
|
||||
|
||||
// re-add the message and stream it
|
||||
this.addUserMessageAndStreamResponse({ userMessage, chatMode, _chatSelections: { prevSelns, currSelns } })
|
||||
this.addUserMessageAndStreamResponse({ userMessage, _chatSelections: { prevSelns, currSelns } })
|
||||
|
||||
}
|
||||
|
||||
|
||||
private resRejOfToolAwaitingApproval: { [toolId: string]: { res: () => void, rej: () => void } } = {}
|
||||
|
||||
// CAN THROW ERRORS
|
||||
approveTool(toolId: string) {
|
||||
const chatMode = this._settingsService.state.globalSettings.chatMode
|
||||
|
||||
// if not streaming, approveToolAndStreamResponse
|
||||
const threadId = this.getCurrentThread().id
|
||||
const isStreaming = !!this.streamState[threadId]?.streamingToken
|
||||
if (!isStreaming) {
|
||||
this._approveToolAndStreamResponse_NotStreamingNow({ chatMode })
|
||||
}
|
||||
else {
|
||||
const resRej = this.resRejOfToolAwaitingApproval[toolId]
|
||||
delete this.resRejOfToolAwaitingApproval[toolId]
|
||||
resRej?.res()
|
||||
}
|
||||
}
|
||||
rejectTool(toolId: string) {
|
||||
// if not streaming, rejecttool
|
||||
const threadId = this.getCurrentThread().id
|
||||
const isStreaming = !!this.streamState[threadId]?.streamingToken
|
||||
if (!isStreaming) {
|
||||
this._rejectTool_NotStreamingNow({})
|
||||
}
|
||||
else {
|
||||
const resRej = this.resRejOfToolAwaitingApproval[toolId]
|
||||
delete this.resRejOfToolAwaitingApproval[toolId]
|
||||
resRej?.rej()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
private _currentModelSelectionProps = () => {
|
||||
// these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools)
|
||||
const featureName: FeatureName = 'Chat'
|
||||
|
|
@ -579,8 +562,63 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
return { modelSelection, modelSelectionOptions }
|
||||
}
|
||||
|
||||
|
||||
approveTool(threadId: string) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
|
||||
const lastMessage = thread.messages[thread.messages.length - 1]
|
||||
if (lastMessage.role !== 'tool_request') return // should never happen
|
||||
|
||||
const lastUserMsgIdx = findLastIndex(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 prevSelns: StagingSelectionItem[] = this._getAllSelections()
|
||||
const currSelns: StagingSelectionItem[] = []
|
||||
|
||||
const callThisToolFirst: ToolRequestApproval<ToolName> = lastMessage
|
||||
|
||||
this._chatAgentLoop({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() })
|
||||
}
|
||||
rejectTool(threadId: string) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
const lastMessage = thread.messages[thread.messages.length - 1]
|
||||
if (lastMessage.role !== 'tool_request') return // should never happen
|
||||
const { name, params, paramsStr, id } = lastMessage
|
||||
|
||||
const errorMessage = this.errMsgs.rejected
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, })
|
||||
}
|
||||
stopRunning(threadId: string) {
|
||||
const thread = this.state.allThreads[threadId]
|
||||
if (!thread) return // should never happen
|
||||
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
|
||||
const lastMessage = thread.messages[thread.messages.length - 1]
|
||||
if (lastMessage.role === 'tool_request') {
|
||||
// interrupt tool request
|
||||
this.rejectTool(threadId)
|
||||
}
|
||||
else {
|
||||
// interrupt assistant message
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
}
|
||||
this._setStreamState(threadId, {}, 'set')
|
||||
}
|
||||
|
||||
|
||||
|
||||
private _tools = (chatMode: ChatMode) => {
|
||||
const toolNames: ToolName[] | undefined = chatMode === 'chat' ? undefined
|
||||
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
|
||||
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName))
|
||||
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
|
||||
: undefined
|
||||
|
|
@ -597,20 +635,26 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
}
|
||||
|
||||
|
||||
// CAN THROW ERRORS
|
||||
private async _agentLoop({ threadId, tools, prevSelns, currSelns, modelSelection, modelSelectionOptions, chatMode, userMessageContent, callThisTool }: {
|
||||
tools: InternalToolInfo[] | undefined,
|
||||
private async _chatAgentLoop({
|
||||
threadId,
|
||||
prevSelns,
|
||||
currSelns,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
userMessageContent,
|
||||
callThisToolFirst,
|
||||
}: {
|
||||
threadId: string,
|
||||
prevSelns: StagingSelectionItem[],
|
||||
currSelns: StagingSelectionItem[],
|
||||
modelSelection: ModelSelection | null,
|
||||
modelSelectionOptions: ModelSelectionOptions | undefined,
|
||||
chatMode: ChatMode,
|
||||
userMessageContent: string, // content of LATEST user message
|
||||
|
||||
callThisTool?: ToolRequestApproval<ToolName>
|
||||
callThisToolFirst?: ToolRequestApproval<ToolName>
|
||||
}) {
|
||||
|
||||
// define helper functions so we can tell what's going on
|
||||
const getLatestMessages = async () => {
|
||||
// recompute files in last message
|
||||
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped
|
||||
|
|
@ -619,7 +663,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// replace last userMessage with userMessageFullContent (which contains all the files too)
|
||||
const messages_ = toLLMChatMessages(this.getCurrentThread().messages)
|
||||
const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user')
|
||||
if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1
|
||||
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)
|
||||
|
|
@ -638,9 +682,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
|
||||
|
||||
const handleToolCall = async (tool: ToolCallType) => {
|
||||
shouldSendAnotherMessage = true
|
||||
|
||||
// returns true when the tool call is waiting for user approval
|
||||
const handleToolCall = async (
|
||||
tool: ToolCallType,
|
||||
opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] },
|
||||
): Promise<boolean> => {
|
||||
const toolName: ToolName = tool.name
|
||||
const toolParamsStr = tool.paramsStr
|
||||
const toolId = tool.id
|
||||
|
|
@ -650,31 +696,26 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
let toolResult: ToolResultType[typeof toolName]
|
||||
let toolResultStr: string
|
||||
|
||||
// 1. validate tool params
|
||||
try {
|
||||
const params = await this._toolsService.validateParams[toolName](toolParamsStr)
|
||||
toolParams = params
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, })
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. if tool requires approval, await the approval
|
||||
if (toolNamesThatRequireApproval.has(toolName)) {
|
||||
const voidToolId = generateUuid()
|
||||
const toolApprovalPromise = new Promise<void>((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } })
|
||||
this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, voidToolId: voidToolId })
|
||||
if (!opts?.preapproved) { // skip this if pre-approved
|
||||
// 1. validate tool params
|
||||
try {
|
||||
await toolApprovalPromise
|
||||
// accepted tool
|
||||
}
|
||||
catch (e) {
|
||||
shouldSendAnotherMessage = false // interrupt flow by rejecting
|
||||
const errorMessage = this.errMsgs.rejected
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'rejected', params: toolParams }, })
|
||||
const params = await this._toolsService.validateParams[toolName](toolParamsStr)
|
||||
toolParams = params
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, })
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. if tool requires approval, break from the loop, awaiting approval
|
||||
const requiresApproval = true // TODO!!!
|
||||
if (requiresApproval && toolNamesThatRequireApproval.has(toolName)) {
|
||||
this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId })
|
||||
return true
|
||||
}
|
||||
}
|
||||
else {
|
||||
toolParams = opts.toolParams
|
||||
}
|
||||
|
||||
// 3. call the tool
|
||||
|
|
@ -697,61 +738,32 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
|
||||
// 5. add to history and keep going
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, })
|
||||
return true
|
||||
return false
|
||||
};
|
||||
|
||||
// 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
|
||||
const tools = this._tools(chatMode)
|
||||
|
||||
|
||||
// CALL GIVEN TOOL before entering agent loop
|
||||
const handleFirstToolCall = async (callThisTool: ToolRequestApproval<ToolName>) => {
|
||||
const toolName: ToolName = callThisTool.name
|
||||
const toolParamsStr = callThisTool.paramsStr
|
||||
const toolId = callThisTool.voidToolId
|
||||
|
||||
const toolParams = callThisTool.params
|
||||
let toolResult: ToolResultType[typeof toolName]
|
||||
let toolResultStr: string
|
||||
|
||||
// 3. call the tool
|
||||
try {
|
||||
toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad...
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error)
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, })
|
||||
return false
|
||||
}
|
||||
|
||||
// 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._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, })
|
||||
return false
|
||||
}
|
||||
|
||||
// 5. add to history and keep going
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, })
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
this._setStreamState(threadId, { error: undefined }) // clear any previous error
|
||||
// clear any previous error + set running
|
||||
this._setStreamState(threadId, { isRunning: true, error: undefined }, 'set')
|
||||
|
||||
let nMessagesSent = 0
|
||||
let shouldSendAnotherMessage = true
|
||||
let exitReason: 'end' | 'awaitingToolApproval' = 'end' as 'end' | 'awaitingToolApproval'
|
||||
|
||||
if (callThisTool) {
|
||||
const keepGoing = await handleFirstToolCall(callThisTool)
|
||||
if (!keepGoing) {
|
||||
this._setStreamState(threadId, { streamingToken: undefined })
|
||||
return
|
||||
}
|
||||
// before enter loop, call tool
|
||||
if (callThisToolFirst) {
|
||||
await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params })
|
||||
}
|
||||
|
||||
// tool use loop
|
||||
while (shouldSendAnotherMessage) {
|
||||
|
||||
shouldSendAnotherMessage = false // false by default
|
||||
// false by default each iteration
|
||||
shouldSendAnotherMessage = false
|
||||
exitReason = 'end'
|
||||
|
||||
nMessagesSent += 1
|
||||
|
||||
let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval)
|
||||
|
|
@ -765,33 +777,34 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
tools: tools,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: `Agent` },
|
||||
onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }) },
|
||||
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
|
||||
onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }, 'merge') },
|
||||
onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => {
|
||||
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, }) // added to history, so clear messages so far
|
||||
// added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning)
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, }, 'merge')
|
||||
|
||||
// if no tool, finish
|
||||
// call tool if there is one
|
||||
const tool: ToolCallType | undefined = toolCalls?.[0]
|
||||
if (!tool) {
|
||||
this._setStreamState(threadId, { streamingToken: undefined })
|
||||
resMessageIsDonePromise()
|
||||
return
|
||||
}
|
||||
else {
|
||||
const keepGoing = await handleToolCall(tool)
|
||||
if (!keepGoing) { this._setStreamState(threadId, { streamingToken: undefined }) }
|
||||
resMessageIsDonePromise()
|
||||
return
|
||||
if (tool) {
|
||||
const awaitingUserApproval = await handleToolCall(tool)
|
||||
if (awaitingUserApproval) {
|
||||
exitReason = 'awaitingToolApproval'
|
||||
} else {
|
||||
shouldSendAnotherMessage = true
|
||||
}
|
||||
|
||||
}
|
||||
resMessageIsDonePromise()
|
||||
return
|
||||
},
|
||||
onError: (error) => {
|
||||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
// add assistant's message to chat history, and clear selection
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error })
|
||||
this._setStreamState(threadId, { error }, 'set')
|
||||
resMessageIsDonePromise()
|
||||
},
|
||||
})
|
||||
|
|
@ -799,62 +812,31 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
// should never happen, just for safety
|
||||
if (llmCancelToken === null) {
|
||||
this._setStreamState(threadId, {
|
||||
messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined,
|
||||
error: { message: 'There was an unexpected error when sending your chat message.', fullError: null }
|
||||
})
|
||||
}, 'set')
|
||||
break
|
||||
}
|
||||
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }) // new stream token for the new message
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
||||
|
||||
await messageIsDonePromise
|
||||
} // end while
|
||||
|
||||
// if awaiting user approval, keep isRunning true, else end isRunning
|
||||
if (exitReason === 'end')
|
||||
this._setStreamState(threadId, { isRunning: undefined }, 'merge')
|
||||
|
||||
// TODO!!! metrics on nMessagesSent and all the file extensions sent here
|
||||
// capture number of messages sent
|
||||
this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode })
|
||||
|
||||
}
|
||||
|
||||
|
||||
private async _rejectTool_NotStreamingNow({ }) {
|
||||
const thread = this.getCurrentThread()
|
||||
const threadId = thread.id
|
||||
|
||||
const lastMessage = thread.messages[thread.messages.length - 1]
|
||||
if (lastMessage.role !== 'tool_request') return // should never happen
|
||||
const { name, params, paramsStr, voidToolId, } = lastMessage
|
||||
|
||||
const errorMessage = this.errMsgs.rejected
|
||||
this._addMessageToThread(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id: voidToolId, content: errorMessage, result: { type: 'rejected', params: params }, })
|
||||
|
||||
}
|
||||
|
||||
// called if we stopped streaming but want to accept the tool afterwards, lets us jump back into the loop as if no interruption happened
|
||||
private async _approveToolAndStreamResponse_NotStreamingNow({ chatMode }: { chatMode: ChatMode }) {
|
||||
const thread = this.getCurrentThread()
|
||||
const threadId = thread.id
|
||||
|
||||
const lastMessage = thread.messages[thread.messages.length - 1]
|
||||
if (lastMessage.role !== 'tool_request') return // should never happen
|
||||
|
||||
const lastUserMsgIdx = findLastIndex(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 prevSelns: StagingSelectionItem[] = this._getAllSelections()
|
||||
const currSelns: StagingSelectionItem[] = []
|
||||
|
||||
const tools = this._tools(chatMode)
|
||||
|
||||
|
||||
const callThisTool: ToolRequestApproval<ToolName> = lastMessage
|
||||
this._agentLoop({ callThisTool, tools, prevSelns, currSelns, threadId, chatMode, userMessageContent: instructions, ...this._currentModelSelectionProps() })
|
||||
}
|
||||
|
||||
|
||||
|
||||
async addUserMessageAndStreamResponse({ userMessage, chatMode, _chatSelections }: { userMessage: string, chatMode: ChatMode, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) {
|
||||
|
||||
async addUserMessageAndStreamResponse({ userMessage, _chatSelections }: { userMessage: string, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) {
|
||||
|
||||
const thread = this.getCurrentThread()
|
||||
const threadId = thread.id
|
||||
|
|
@ -870,22 +852,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
|
||||
this._addMessageToThread(threadId, userHistoryElt)
|
||||
|
||||
const tools = this._tools(chatMode)
|
||||
|
||||
this._agentLoop({ tools, prevSelns, currSelns, threadId, chatMode, userMessageContent, ...this._currentModelSelectionProps(), })
|
||||
}
|
||||
|
||||
cancelStreaming(threadId: string) {
|
||||
const llmCancelToken = this.streamState[threadId]?.streamingToken
|
||||
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
|
||||
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
|
||||
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined })
|
||||
this._chatAgentLoop({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), })
|
||||
}
|
||||
|
||||
dismissStreamError(threadId: string): void {
|
||||
this._setStreamState(threadId, { error: undefined })
|
||||
this._setStreamState(threadId, { error: undefined }, 'merge')
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -901,10 +872,10 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
const functionParensPattern = /^([^\s(]+)\([^)]*\)$/; // `functionName( args )`
|
||||
|
||||
let target = _codespanStr // the string to search for
|
||||
let codespanType: 'file' | 'function-or-class' | 'unsearchable' = 'unsearchable';
|
||||
if (target.includes('.')) {
|
||||
let codespanType: 'file-or-folder' | 'function-or-class' | 'unsearchable' = 'unsearchable';
|
||||
if (target.includes('.') || target.includes('/')) {
|
||||
|
||||
codespanType = 'file'
|
||||
codespanType = 'file-or-folder'
|
||||
target = _codespanStr
|
||||
|
||||
} else if (functionOrMethodPattern.test(target)) {
|
||||
|
|
@ -933,7 +904,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
.reverse()
|
||||
|
||||
|
||||
if (codespanType === 'file') {
|
||||
if (codespanType === 'file-or-folder') {
|
||||
|
||||
|
||||
const doesUriMatchTarget = (uri: URI) => uri.path.includes(target)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
|
|||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { LucideIcon, RotateCw } from 'lucide-react'
|
||||
import { Check, X, Square, Copy, Play, } from 'lucide-react'
|
||||
import { getBasename, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { getBasename, ListableToolItem, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
|
||||
|
||||
enum CopyButtonText {
|
||||
|
|
@ -278,17 +278,27 @@ export const BlockCodeApplyWrapper = ({
|
|||
|
||||
|
||||
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri })
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
|
||||
return <div
|
||||
className='border border-void-border-3 rounded overflow-hidden bg-void-bg-3 my-1'
|
||||
>
|
||||
const name = uri !== 'current' ?
|
||||
<ListableToolItem
|
||||
name={<span className='not-italic'>{getBasename(uri.fsPath)}</span>}
|
||||
isSmall={true}
|
||||
showDot={false}
|
||||
// TODO!!! this uri is not correct, it is not recognized as an actual file for some stupid reason
|
||||
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
|
||||
/>
|
||||
: <span>{language}</span>
|
||||
|
||||
|
||||
return <div className='border border-void-border-3 rounded overflow-hidden bg-void-bg-3 my-1'>
|
||||
{/* header */}
|
||||
<div className=" select-none flex justify-between items-center py-1 px-2 border-b border-void-border-3 cursor-default">
|
||||
<div className="flex items-center">
|
||||
{statusIndicatorHTML}
|
||||
<span className="text-[13px] font-light text-void-fg-3">
|
||||
{uri !== 'current' ? getBasename(uri.fsPath) : language || 'text'}
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { useAccessor } from '../util/services.js'
|
|||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
|
||||
import { URI } from '../../../../../../../base/common/uri.js'
|
||||
import { getBasename } from '../sidebar-tsx/SidebarChat.js'
|
||||
import { isAbsolute } from '../../../../../../../base/common/path.js'
|
||||
|
||||
|
||||
export type ChatMessageLocation = {
|
||||
|
|
@ -25,6 +26,9 @@ export const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocati
|
|||
return `${threadId}-${messageIdx}-${tokenIdx}`
|
||||
}
|
||||
|
||||
function isValidUri(s: string): boolean {
|
||||
return s.includes('/') && s.length > 5 && isAbsolute(s)
|
||||
}
|
||||
|
||||
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
|
||||
|
||||
|
|
@ -122,25 +126,24 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
|
|||
|
||||
if (t.type === "code") {
|
||||
const [firstLine, remainingContents] = getFirstLine(t.text)
|
||||
const firstLineIsURI = URI.isUri(firstLine)
|
||||
const contents = firstLineIsURI ? (remainingContents || '') : t.text // exclude first-line URI from contents
|
||||
const firstLineIsURI = isValidUri(firstLine)
|
||||
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents
|
||||
|
||||
// figure out langauge and URI
|
||||
let uri: URI | null
|
||||
let language: string | undefined = undefined
|
||||
let uri: URI | undefined = undefined
|
||||
if (firstLineIsURI) { // get lang from the uri in the first line of the markdown
|
||||
uri = codeURI ?? URI.from(URI.file(firstLine))
|
||||
}
|
||||
else {
|
||||
uri = codeURI || null
|
||||
}
|
||||
|
||||
if (t.lang) { // a language was provided. empty string is common so check truthy, not just undefined
|
||||
uri = codeURI
|
||||
language = convertToVscodeLang(languageService, t.lang) // convert markdown language to language that vscode recognizes (eg markdown doesn't know bash but it does know shell)
|
||||
}
|
||||
else { // no language provided - fallback
|
||||
if (firstLineIsURI) { // get lang from the uri in the markdown
|
||||
uri = codeURI ?? URI.file(firstLine)
|
||||
language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined })
|
||||
}
|
||||
else { // get lang from the given URI and contents
|
||||
uri = codeURI
|
||||
language = getLanguage(languageService, { uri: codeURI ?? null, fileContents: remainingContents ?? undefined })
|
||||
}
|
||||
else { // no language provided - fallback - get lang from the uri and contents
|
||||
language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined })
|
||||
}
|
||||
|
||||
if (options.isApplyEnabled && chatMessageLocation) {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,6 @@ export const QuickEditChat = ({
|
|||
isStreaming={isStreamingRef.current}
|
||||
loadingIcon={loadingIcon}
|
||||
isDisabled={isDisabled}
|
||||
className="py-2 w-full"
|
||||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
>
|
||||
<VoidInputBox2
|
||||
|
|
|
|||
|
|
@ -26,11 +26,10 @@ import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js';
|
|||
import { ChatMode, FeatureName, isFeatureNameDisabled } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js';
|
||||
import { WarningBox } from '../void-settings-tsx/WarningBox.js';
|
||||
import { getModelSelectionState, getModelCapabilities } from '../../../../common/modelCapabilities.js';
|
||||
import { AlertTriangle, ChevronRight, Dot, Pencil, X } from 'lucide-react';
|
||||
import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react';
|
||||
import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js';
|
||||
import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js';
|
||||
import { getLanguageFromModel } from '../../../../common/helpers/getLanguage.js';
|
||||
import { dirname } from '../../../../../../../base/common/resources.js';
|
||||
import { useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js';
|
||||
import { DiffZone } from '../../../editCodeService.js';
|
||||
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
|
||||
|
|
@ -211,15 +210,15 @@ const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) =>
|
|||
|
||||
|
||||
const nameOfChatMode = {
|
||||
'chat': 'Chat',
|
||||
'normal': 'Normal',
|
||||
'gather': 'Gather',
|
||||
'agent': 'Agent',
|
||||
}
|
||||
|
||||
const detailOfChatMode = {
|
||||
'chat': 'Normal chat',
|
||||
'gather': 'Read and search only',
|
||||
'agent': 'Full tool use',
|
||||
'normal': 'Normal chat',
|
||||
'gather': 'Discover relevant files',
|
||||
'agent': 'Edit files and use tools',
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -227,9 +226,8 @@ const ChatModeDropdown = ({ className }: { className: string }) => {
|
|||
const accessor = useAccessor()
|
||||
|
||||
const voidSettingsService = accessor.get('IVoidSettingsService')
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
const options: ChatMode[] = useMemo(() => ['chat', 'gather', 'agent'], [])
|
||||
const options: ChatMode[] = useMemo(() => ['normal', 'gather', 'agent'], [])
|
||||
|
||||
const onChangeOption = useCallback((newVal: ChatMode) => {
|
||||
voidSettingsService.setGlobalSetting('chatMode', newVal)
|
||||
|
|
@ -304,7 +302,7 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
|||
<div
|
||||
ref={divRef}
|
||||
className={`
|
||||
gap-1
|
||||
gap-x-1
|
||||
flex flex-col p-2 relative input text-left shrink-0
|
||||
transition-all duration-200
|
||||
rounded-md
|
||||
|
|
@ -351,12 +349,12 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
|
|||
{/* Bottom row */}
|
||||
<div className='flex flex-row justify-between items-end gap-1'>
|
||||
{showModelDropdown && (
|
||||
<div className='flex flex-col gap-1'>
|
||||
<div className='flex flex-col gap-y-1'>
|
||||
<ReasoningOptionSlider featureName={featureName} />
|
||||
|
||||
<div className='flex items-center flex-wrap gap-x-1 gap-y-1'>
|
||||
<ModelDropdown featureName={featureName} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
<ChatModeDropdown className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
<div className='flex items-center flex-wrap gap-x-2 gap-y-1'>
|
||||
{featureName === 'Chat' && <ChatModeDropdown className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-2 rounded py-0.5 px-1' />}
|
||||
<ModelDropdown featureName={featureName} className='text-xs text-void-fg-3 bg-void-bg-1 rounded' />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -667,6 +665,7 @@ type ToolHeaderParams = {
|
|||
desc1: React.ReactNode;
|
||||
desc2?: React.ReactNode;
|
||||
isError?: boolean;
|
||||
isRejected?: boolean;
|
||||
numResults?: number;
|
||||
children?: React.ReactNode;
|
||||
onClick?: () => void;
|
||||
|
|
@ -683,6 +682,7 @@ const ToolHeaderWrapper = ({
|
|||
isError,
|
||||
onClick,
|
||||
isOpen,
|
||||
isRejected,
|
||||
}: ToolHeaderParams) => {
|
||||
|
||||
const [isExpanded_, setIsExpanded] = useState(false);
|
||||
|
|
@ -692,7 +692,7 @@ const ToolHeaderWrapper = ({
|
|||
const isClickable = !!(isDropdown || onClick)
|
||||
|
||||
return (<div className=''>
|
||||
<div className="w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden">
|
||||
<div className="w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ">
|
||||
{/* header */}
|
||||
<div
|
||||
className={`select-none flex items-center min-h-[24px] ${isClickable ? 'cursor-pointer hover:brightness-125 transition-all duration-150' : ''} ${!isDropdown ? 'mx-1' : ''}`}
|
||||
|
|
@ -706,7 +706,7 @@ const ToolHeaderWrapper = ({
|
|||
className={`text-void-fg-3 mr-0.5 h-4 w-4 flex-shrink-0 transition-transform duration-100 ease-[cubic-bezier(0.4,0,0.2,1)] ${isExpanded ? 'rotate-90' : ''}`}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center w-full gap-x-2 overflow-hidden justify-between">
|
||||
<div className={`flex items-center w-full gap-x-2 overflow-hidden justify-between ${isRejected ? 'line-through' : ''}`}>
|
||||
{/* left */}
|
||||
<div className="flex items-center gap-x-2 min-w-0 overflow-hidden">
|
||||
<span className="text-void-fg-3 flex-shrink-0">{title}</span>
|
||||
|
|
@ -723,7 +723,8 @@ const ToolHeaderWrapper = ({
|
|||
{`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`}
|
||||
</span>
|
||||
)}
|
||||
{isError && <AlertTriangle className='text-void-warning opacity-90 flex-shrink-0' size={12} />}
|
||||
{isError && <AlertTriangle className='text-void-warning opacity-90 flex-shrink-0' size={14} />}
|
||||
{isRejected && <Ban className='text-void-fg-4 opacity-90 flex-shrink-0' size={14} />}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -820,7 +821,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
|
|||
|
||||
// cancel any streams on this thread
|
||||
const thread = chatThreadsService.getCurrentThread()
|
||||
chatThreadsService.cancelStreaming(thread.id)
|
||||
chatThreadsService.stopRunning(thread.id)
|
||||
|
||||
// update state
|
||||
setIsBeingEdited(false)
|
||||
|
|
@ -830,7 +831,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
|
|||
// stream the edit
|
||||
const userMessage = textAreaRefState.value;
|
||||
try {
|
||||
await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, })
|
||||
await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, messageIdx, })
|
||||
} catch (e) {
|
||||
console.error('Error while editing message:', e)
|
||||
}
|
||||
|
|
@ -838,7 +839,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble
|
|||
|
||||
const onAbort = () => {
|
||||
const threadId = chatThreadsService.state.currentThreadId
|
||||
chatThreadsService.cancelStreaming(threadId)
|
||||
chatThreadsService.stopRunning(threadId)
|
||||
}
|
||||
|
||||
const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
|
|
@ -1014,15 +1015,15 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast
|
|||
|
||||
|
||||
// should either be past or "-ing" tense, not present tense. Eg. when the LLM searches for something, the user expects it to say "I searched for X" or "I am searching for X". Not "I search X".
|
||||
const toolNameToTitle: Record<ToolName, { past: string, current: string, proposed: string }> = {
|
||||
'read_file': { past: 'Read file', current: 'Reading file', proposed: 'Read file' },
|
||||
'list_dir': { past: 'Inspected folder', current: 'Inspecting folder', proposed: 'Inspect folder' },
|
||||
'pathname_search': { past: 'Searched by file name', current: 'Searching by file name', proposed: 'Search by file name' },
|
||||
'search': { past: 'Searched', current: 'Searching', proposed: 'Search' },
|
||||
'create_uri': { past: 'Created file', current: 'Creating file', proposed: 'Create file' },
|
||||
'delete_uri': { past: 'Deleted file', current: 'Deleting file', proposed: 'Delete file' },
|
||||
'edit': { past: 'Edited file', current: 'Editing file', proposed: 'Edit file' },
|
||||
'terminal_command': { past: 'Ran terminal command', current: 'Running terminal command', proposed: 'Run terminal command' }
|
||||
const toolNameToTitle: Record<ToolName, { past: string, proposed: string }> = {
|
||||
'read_file': { past: 'Read file', proposed: 'Read file' },
|
||||
'list_dir': { past: 'Inspected folder', proposed: 'Inspect folder' },
|
||||
'pathname_search': { past: 'Searched by file name', proposed: 'Search by file name' },
|
||||
'search': { past: 'Searched', proposed: 'Search' },
|
||||
'create_uri': { past: 'Created file', proposed: 'Create file' },
|
||||
'delete_uri': { past: 'Deleted file', proposed: 'Delete file' },
|
||||
'edit': { past: 'Edited file', proposed: 'Edit file' },
|
||||
'terminal_command': { past: 'Ran terminal command', proposed: 'Run terminal command' }
|
||||
}
|
||||
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => {
|
||||
|
||||
|
|
@ -1060,24 +1061,27 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
|
|||
}
|
||||
|
||||
|
||||
const ToolRequestAcceptRejectButtons = ({ voidToolId }: { voidToolId: string }) => {
|
||||
const ToolRequestAcceptRejectButtons = () => {
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const metricsService = accessor.get('IMetricsService')
|
||||
|
||||
|
||||
const onAccept = useCallback(() => {
|
||||
try {
|
||||
chatThreadsService.approveTool(voidToolId)
|
||||
try { // this doesn't need to be wrapped in try/catch anymore
|
||||
const threadId = chatThreadsService.state.currentThreadId
|
||||
chatThreadsService.approveTool(threadId)
|
||||
metricsService.capture('Tool Request Accepted', {})
|
||||
} catch (e) { console.error('Error while approving message in chat:', e) }
|
||||
}, [chatThreadsService, voidToolId, metricsService])
|
||||
}, [chatThreadsService, metricsService])
|
||||
|
||||
const onReject = useCallback(() => {
|
||||
try {
|
||||
chatThreadsService.rejectTool(voidToolId)
|
||||
const threadId = chatThreadsService.state.currentThreadId
|
||||
chatThreadsService.rejectTool(threadId)
|
||||
} catch (e) { console.error('Error while approving message in chat:', e) }
|
||||
metricsService.capture('Tool Request Rejected', {})
|
||||
}, [chatThreadsService, voidToolId, metricsService])
|
||||
}, [chatThreadsService, metricsService])
|
||||
|
||||
const approveButton = (
|
||||
<button
|
||||
|
|
@ -1126,7 +1130,7 @@ export const ToolContentsWrapper = ({ children, className }: { children: React.R
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => {
|
||||
export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => {
|
||||
return <div
|
||||
className={`
|
||||
${onClick ? 'hover:brightness-125 hover:cursor-pointer transition-all duration-200 ' : ''}
|
||||
|
|
@ -1159,10 +1163,8 @@ const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescript
|
|||
className='w-full overflow-auto py-1'
|
||||
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
|
||||
/>
|
||||
<div className='border border-void-border-1 rounded p-1'>
|
||||
<div className='!select-text cursor-auto'>
|
||||
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
<div className='!select-text cursor-auto my-4'>
|
||||
<ChatMarkdownRender string={changeDescription} codeURI={uri} chatMessageLocation={undefined} />
|
||||
</div>
|
||||
</ToolContentsWrapper>
|
||||
}
|
||||
|
|
@ -1183,12 +1185,10 @@ const TerminalToolChildren = ({ command, terminalId, result, resolveReason }: {
|
|||
className='w-full overflow-auto py-1'
|
||||
onClick={() => terminalToolsService.openTerminal(terminalId)}
|
||||
/>
|
||||
<div className='border border-void-border-1 rounded p-1'>
|
||||
<div className='!select-text cursor-auto'>
|
||||
{resolveReason.type === 'bgtask' ? 'Result so far:\n' : null}
|
||||
{result}
|
||||
{resultStr}
|
||||
</div>
|
||||
<div className='!select-text cursor-auto my-4'>
|
||||
{resolveReason.type === 'bgtask' ? 'Result so far:\n' : null}
|
||||
{result}
|
||||
{resultStr}
|
||||
</div>
|
||||
</ToolContentsWrapper>
|
||||
}
|
||||
|
|
@ -1203,7 +1203,7 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe
|
|||
if (!isWriting) setIsOpen(isWriting) // if just finished reasoning, close
|
||||
}, [isWriting])
|
||||
return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen}>
|
||||
<ToolContentsWrapper className='bg-void-bg-3'>
|
||||
<ToolContentsWrapper className='bg-void-bg-3 prose-sm'>
|
||||
<div className='!select-text cursor-auto'>
|
||||
{children}
|
||||
</div>
|
||||
|
|
@ -1396,7 +1396,8 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
|
||||
|
||||
const isError = toolMessage.result.type === 'error'
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
|
||||
const isRejected = toolMessage.result.type === 'rejected'
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
|
||||
|
||||
if (toolMessage.result.type === 'success') {
|
||||
const { params } = toolMessage.result
|
||||
|
|
@ -1434,12 +1435,13 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
resultWrapper: ({ toolMessage }) => {
|
||||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const title = toolNameToTitle[toolMessage.name].past
|
||||
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
const icon = null
|
||||
|
||||
const isError = toolMessage.result.type === 'error'
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
|
||||
const isRejected = toolMessage.result.type === 'rejected'
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
|
||||
|
||||
if (toolMessage.result.type === 'success') {
|
||||
const { params } = toolMessage.result
|
||||
|
|
@ -1480,12 +1482,13 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
resultWrapper: ({ toolMessage, messageIdx }) => {
|
||||
const accessor = useAccessor()
|
||||
const chatThreadsService = accessor.get('IChatThreadService')
|
||||
const title = toolNameToTitle[toolMessage.name].past
|
||||
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
const icon = null
|
||||
|
||||
const isError = toolMessage.result.type === 'error'
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
|
||||
const isRejected = toolMessage.result.type === 'rejected'
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
|
||||
|
||||
if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected') {
|
||||
const { params } = toolMessage.result
|
||||
|
|
@ -1540,12 +1543,13 @@ const toolNameToComponent: { [T in ToolName]: {
|
|||
const accessor = useAccessor()
|
||||
const commandService = accessor.get('ICommandService')
|
||||
const terminalToolsService = accessor.get('ITerminalToolService')
|
||||
const title = toolNameToTitle[toolMessage.name].past
|
||||
const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed
|
||||
const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params)
|
||||
const icon = null
|
||||
|
||||
const isError = toolMessage.result.type === 'error'
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon }
|
||||
const isRejected = toolMessage.result.type === 'rejected'
|
||||
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected }
|
||||
|
||||
if (toolMessage.result.type === 'success') {
|
||||
const { command } = toolMessage.result.params
|
||||
|
|
@ -1605,11 +1609,11 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr
|
|||
}
|
||||
else if (role === 'tool_request') {
|
||||
const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough...
|
||||
// if (!isLast) return null
|
||||
if (!isLast) return null
|
||||
if (!ToolRequestWrapper) return null
|
||||
return <>
|
||||
<ToolRequestWrapper toolRequest={chatMessage} />
|
||||
<ToolRequestAcceptRejectButtons voidToolId={chatMessage.voidToolId} />
|
||||
<ToolRequestAcceptRejectButtons />
|
||||
</>
|
||||
}
|
||||
else if (role === 'tool') {
|
||||
|
|
@ -1838,7 +1842,7 @@ export const SidebarChat = () => {
|
|||
|
||||
// stream state
|
||||
const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId)
|
||||
const isStreaming = !!currThreadStreamState?.streamingToken
|
||||
const isRunning = !!currThreadStreamState?.isRunning
|
||||
const latestError = currThreadStreamState?.error
|
||||
const messageSoFar = currThreadStreamState?.messageSoFar
|
||||
const reasoningSoFar = currThreadStreamState?.reasoningSoFar
|
||||
|
|
@ -1861,7 +1865,7 @@ export const SidebarChat = () => {
|
|||
const onSubmit = useCallback(async () => {
|
||||
|
||||
if (isDisabled) return
|
||||
if (isStreaming) return
|
||||
if (isRunning) return
|
||||
|
||||
// update state
|
||||
chatThreadsService.closeStagingSelectionsInCurrentThread() // close all selections
|
||||
|
|
@ -1871,10 +1875,8 @@ export const SidebarChat = () => {
|
|||
|
||||
// getModelCapabilities() // TODO!!! check if can go into agent mode
|
||||
|
||||
const chatMode = settingsState.globalSettings.chatMode
|
||||
|
||||
try {
|
||||
await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode })
|
||||
await chatThreadsService.addUserMessageAndStreamResponse({ userMessage })
|
||||
} catch (e) {
|
||||
console.error('Error while sending message in chat:', e)
|
||||
}
|
||||
|
|
@ -1883,11 +1885,11 @@ export const SidebarChat = () => {
|
|||
textAreaFnsRef.current?.setValue('')
|
||||
textAreaRef.current?.focus() // focus input after submit
|
||||
|
||||
}, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, setSelections])
|
||||
}, [chatThreadsService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState])
|
||||
|
||||
const onAbort = () => {
|
||||
const threadId = currentThread.id
|
||||
chatThreadsService.cancelStreaming(threadId)
|
||||
chatThreadsService.stopRunning(threadId)
|
||||
}
|
||||
|
||||
// const [_test_messages, _set_test_messages] = useState<string[]>([])
|
||||
|
|
@ -1910,7 +1912,7 @@ export const SidebarChat = () => {
|
|||
}, [previousMessages, currentThread, numMessages])
|
||||
|
||||
const streamingChatIdx = previousMessagesHTML.length
|
||||
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ?
|
||||
const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ?
|
||||
<ChatBubble key={getChatBubbleId(currentThread.id, streamingChatIdx)}
|
||||
messageIdx={streamingChatIdx}
|
||||
chatMessage={{
|
||||
|
|
@ -1919,7 +1921,7 @@ export const SidebarChat = () => {
|
|||
reasoning: reasoningSoFar ?? '',
|
||||
anthropicReasoning: null,
|
||||
}}
|
||||
isLoading={isStreaming}
|
||||
isLoading={isRunning}
|
||||
isLast={true}
|
||||
/> : null
|
||||
|
||||
|
|
@ -1970,10 +1972,10 @@ export const SidebarChat = () => {
|
|||
const onKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
onSubmit()
|
||||
} else if (e.key === 'Escape' && isStreaming) {
|
||||
} else if (e.key === 'Escape' && isRunning) {
|
||||
onAbort()
|
||||
}
|
||||
}, [onSubmit, onAbort, isStreaming])
|
||||
}, [onSubmit, onAbort, isRunning])
|
||||
const inputForm = <div
|
||||
key={'input' + chatThreadsState.currentThreadId}
|
||||
className={`right-0 left-0 m-2 z-[999] overflow-hidden ${previousMessages.length > 0 ? 'absolute bottom-0' : ''}`}>
|
||||
|
|
@ -1982,7 +1984,7 @@ export const SidebarChat = () => {
|
|||
divRef={chatAreaRef}
|
||||
onSubmit={onSubmit}
|
||||
onAbort={onAbort}
|
||||
isStreaming={isStreaming}
|
||||
isStreaming={isRunning}
|
||||
isDisabled={isDisabled}
|
||||
showSelections={true}
|
||||
showProspectiveSelections={previousMessagesHTML.length === 0}
|
||||
|
|
@ -1991,7 +1993,8 @@ export const SidebarChat = () => {
|
|||
onClickAnywhere={() => { textAreaRef.current?.focus() }}
|
||||
>
|
||||
<VoidInputBox2
|
||||
className={`${previousMessages.length > 0 ? 'min-h-[9px]' : 'min-h-[81px]'} px-0.5`}
|
||||
// className={`${previousMessages.length > 0 ? 'min-h-[9px]' : 'min-h-[81px]'} px-0.5`}
|
||||
className={`min-h-[81px] px-0.5 py-0.5`}
|
||||
placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`}
|
||||
onChangeText={onChangeText}
|
||||
onKeyDown={onKeyDown}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ export type ToolRequestApproval<T extends ToolName> = {
|
|||
name: T; // internal use
|
||||
params: ToolCallParams[T]; // internal use
|
||||
paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
|
||||
voidToolId: string; // internal id Void uses
|
||||
id: string; // proposed tool's id
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import { URI } from '../../../../../base/common/uri.js';
|
|||
import { os } from '../helpers/systemInfo.js';
|
||||
import { IVoidFileService } from '../voidFileService.js';
|
||||
import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js';
|
||||
import { ChatMode } from '../voidSettingsTypes.js';
|
||||
|
||||
|
||||
// this is just for ease of readability
|
||||
|
|
@ -21,8 +22,8 @@ Do NOT output the whole file here if possible, and try to write as LITTLE code a
|
|||
|
||||
|
||||
|
||||
export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\
|
||||
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase'}.
|
||||
export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\
|
||||
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase by providing specific references to files and content'}.
|
||||
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${mode === 'agent' ? `, bringing the task to completion (make all necessary changes, and do not be lazy)` : ''}. The user's query is never invalid.
|
||||
|
||||
|
|
@ -56,6 +57,7 @@ If you think it's appropriate to suggest an edit to a file, then you must descri
|
|||
|
||||
Misc:
|
||||
- Do not make things up.
|
||||
- Do not be lazy.
|
||||
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\
|
||||
`
|
||||
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ export type ServiceSendLLMMessageParams = {
|
|||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
logging: { loggingName: string, };
|
||||
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
|
||||
modelSelection: ModelSelection | null;
|
||||
modelSelectionOptions: ModelSelectionOptions | undefined;
|
||||
} & SendLLMType;
|
||||
|
|
@ -91,7 +91,7 @@ export type SendLLMMessageParams = {
|
|||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
logging: { loggingName: string, };
|
||||
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
|
||||
abortRef: AbortRef;
|
||||
|
||||
aiInstructions: string;
|
||||
|
|
|
|||
|
|
@ -99,7 +99,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt
|
|||
|
||||
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = {
|
||||
'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
|
||||
'Chat': { filter: (o, { chatMode }) => chatMode === 'chat' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } },
|
||||
'Chat': { filter: (o, { chatMode }) => chatMode === 'normal' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } },
|
||||
'Ctrl+K': { filter: o => true, emptyMessage: null, },
|
||||
'Apply': { filter: o => true, emptyMessage: null, },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -378,7 +378,7 @@ export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: V
|
|||
|
||||
|
||||
|
||||
export type ChatMode = 'agent' | 'gather' | 'chat'
|
||||
export type ChatMode = 'agent' | 'gather' | 'normal'
|
||||
|
||||
|
||||
export type GlobalSettings = {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ export const sendLLMMessage = ({
|
|||
onFinalMessage: onFinalMessage_,
|
||||
onError: onError_,
|
||||
abortRef: abortRef_,
|
||||
logging: { loggingName },
|
||||
logging: { loggingName, loggingExtras },
|
||||
settingsOfProvider,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
|
|
@ -48,6 +48,7 @@ export const sendLLMMessage = ({
|
|||
suffixLength: messages_.suffix.length,
|
||||
} : {},
|
||||
|
||||
...loggingExtras,
|
||||
...extras,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue