mirror of
https://github.com/voideditor/void
synced 2026-05-22 17:08:25 +00:00
add retries, fix key, X on autodetected
This commit is contained in:
parent
7bd52438f5
commit
10949b44d1
6 changed files with 113 additions and 104 deletions
3
package-lock.json
generated
3
package-lock.json
generated
|
|
@ -46,6 +46,7 @@
|
|||
"cross-spawn": "^7.0.6",
|
||||
"diff": "^7.0.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"google-auth-library": "^9.15.1",
|
||||
"groq-sdk": "^0.15.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
|
|
@ -8067,7 +8068,7 @@
|
|||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
||||
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
||||
"dev": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-levenshtein": {
|
||||
"version": "2.0.6",
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@
|
|||
"cross-spawn": "^7.0.6",
|
||||
"diff": "^7.0.0",
|
||||
"eslint-plugin-react": "^7.37.4",
|
||||
"fast-json-stable-stringify": "^2.1.0",
|
||||
"google-auth-library": "^9.15.1",
|
||||
"groq-sdk": "^0.15.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
|
|
|
|||
|
|
@ -59,6 +59,6 @@ class DummyService extends Disposable implements IWorkbenchContribution, IDummyS
|
|||
|
||||
|
||||
// pick one and delete the other:
|
||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager);
|
||||
registerSingleton(IDummyService, DummyService, InstantiationType.Eager); // lazily loaded, even if Eager
|
||||
|
||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore);
|
||||
registerWorkbenchContribution2(DummyService.ID, DummyService, WorkbenchPhase.BlockRestore); // mounts on start
|
||||
|
|
|
|||
|
|
@ -32,6 +32,9 @@ import { INotificationService, Severity } from '../../../../platform/notificatio
|
|||
import { truncate } from '../../../../base/common/strings.js';
|
||||
import { THREAD_STORAGE_KEY } from '../common/storageKeys.js';
|
||||
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
|
||||
import { timeout } from '../../../../base/common/async.js';
|
||||
|
||||
const CHAT_RETRIES = 3
|
||||
|
||||
export const findStagingSelectionIndex = (currentSelections: StagingSelectionItem[] | undefined, newSelection: StagingSelectionItem): number | null => {
|
||||
if (!currentSelections) return null
|
||||
|
|
@ -565,7 +568,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
let nMessagesSent = 0
|
||||
let shouldSendAnotherMessage = true
|
||||
let isRunningWhenEnd: IsRunningType = undefined
|
||||
let aborted = false
|
||||
|
||||
// before enter loop, call tool
|
||||
if (callThisToolFirst) {
|
||||
|
|
@ -593,69 +595,94 @@ class ChatThreadService extends Disposable implements IChatThreadService {
|
|||
chatMode
|
||||
})
|
||||
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
chatMode,
|
||||
messages: messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
|
||||
separateSystemMessage: separateSystemMessage,
|
||||
onText: ({ fullText, fullReasoning, toolCall }) => {
|
||||
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._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
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._setStreamState(threadId, { error }, 'set')
|
||||
resMessageIsDonePromise()
|
||||
},
|
||||
onAbort: () => {
|
||||
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
|
||||
resMessageIsDonePromise()
|
||||
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
|
||||
aborted = true
|
||||
},
|
||||
})
|
||||
|
||||
// should never happen, just for safety
|
||||
if (llmCancelToken === null) {
|
||||
this._setStreamState(threadId, {
|
||||
error: { message: 'There was an unexpected error when sending your chat message.', fullError: null }
|
||||
}, 'set')
|
||||
break
|
||||
}
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
||||
const toolCall = await messageIsDonePromise // wait for message to complete
|
||||
if (aborted) { return }
|
||||
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
|
||||
let aborted = false
|
||||
|
||||
// call tool if there is one
|
||||
const tool: RawToolCallObj | undefined = toolCall
|
||||
if (tool) {
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { preapproved: false, unvalidatedToolParams: tool.rawParams })
|
||||
let shouldRetry = true
|
||||
let nAttempts = 0
|
||||
|
||||
// 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
|
||||
if (interrupted) { return }
|
||||
while (shouldRetry) {
|
||||
shouldRetry = false
|
||||
|
||||
if (awaitingUserApproval) {
|
||||
isRunningWhenEnd = 'awaiting_user'
|
||||
const llmCancelToken = this._llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
chatMode,
|
||||
messages: messages,
|
||||
modelSelection,
|
||||
modelSelectionOptions,
|
||||
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
|
||||
separateSystemMessage: separateSystemMessage,
|
||||
onText: ({ fullText, fullReasoning, toolCall }) => {
|
||||
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._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
resMessageIsDonePromise(toolCall) // resolve with tool calls
|
||||
|
||||
},
|
||||
onError: (error) => {
|
||||
const messageSoFar = this.streamState[threadId]?.displayContentSoFar ?? ''
|
||||
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
|
||||
|
||||
if (nAttempts < CHAT_RETRIES) {
|
||||
nAttempts += 1
|
||||
shouldRetry = true
|
||||
this._setStreamState(threadId, { displayContentSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolCallSoFar: undefined }, 'merge')
|
||||
timeout(2500).then(() => { resMessageIsDonePromise() })
|
||||
}
|
||||
else {
|
||||
// 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._setStreamState(threadId, { error }, 'set')
|
||||
resMessageIsDonePromise()
|
||||
}
|
||||
},
|
||||
onAbort: () => {
|
||||
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
|
||||
resMessageIsDonePromise()
|
||||
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
|
||||
aborted = true
|
||||
},
|
||||
})
|
||||
|
||||
// should never happen, just for safety
|
||||
if (llmCancelToken === null) {
|
||||
this._setStreamState(threadId, {
|
||||
error: { message: 'There was an unexpected error when sending your chat message.', fullError: null }
|
||||
}, 'set')
|
||||
break
|
||||
}
|
||||
else {
|
||||
shouldSendAnotherMessage = true
|
||||
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
|
||||
const toolCall = await messageIsDonePromise // wait for message to complete
|
||||
if (shouldRetry) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if (aborted) {
|
||||
return
|
||||
}
|
||||
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
|
||||
|
||||
} // end while
|
||||
// call tool if there is one
|
||||
const tool: RawToolCallObj | undefined = toolCall
|
||||
if (tool) {
|
||||
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, tool.name, tool.id, { 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
|
||||
if (interrupted) { return }
|
||||
|
||||
if (awaitingUserApproval) {
|
||||
isRunningWhenEnd = 'awaiting_user'
|
||||
}
|
||||
else {
|
||||
shouldSendAnotherMessage = true
|
||||
}
|
||||
}
|
||||
|
||||
} // end while (attempts)
|
||||
} // end while (send message)
|
||||
|
||||
|
||||
// if awaiting user approval, keep isRunning true, else end isRunning
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg
|
|||
import { ToolName, toolNames } from '../../../../common/prompt/prompts.js';
|
||||
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
|
||||
import { MAX_FILE_CHARS_PAGE } from '../../../toolsService.js';
|
||||
import jsonStringify from 'fast-json-stable-stringify'
|
||||
import ErrorBoundary from './ErrorBoundary.js';
|
||||
|
||||
|
||||
|
|
@ -143,9 +144,6 @@ export const IconLoading = ({ className = '' }: { className?: string }) => {
|
|||
}
|
||||
|
||||
|
||||
const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-${messageIdx}`;
|
||||
|
||||
|
||||
|
||||
// SLIDER ONLY:
|
||||
const ReasoningOptionSlider = ({ featureName }: { featureName: FeatureName }) => {
|
||||
|
|
@ -554,8 +552,7 @@ export const SelectedFiles = (
|
|||
{allSelections.map((selection, i) => {
|
||||
|
||||
const isThisSelectionProspective = i > selections.length - 1
|
||||
|
||||
const thisKey = `${isThisSelectionProspective}-${i}-${selections.length}`
|
||||
const thisKey = jsonStringify(selection)
|
||||
|
||||
return <div // container for summarybox and code
|
||||
key={thisKey}
|
||||
|
|
@ -1979,7 +1976,13 @@ type ChatBubbleProps = {
|
|||
_scrollToBottom: (() => void) | null,
|
||||
}
|
||||
|
||||
const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => {
|
||||
const ChatBubble = (props: ChatBubbleProps) => {
|
||||
return <ErrorBoundary>
|
||||
<_ChatBubble {...props} />
|
||||
</ErrorBoundary>
|
||||
}
|
||||
|
||||
const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, messageIdx, chatIsRunning, _scrollToBottom }: ChatBubbleProps) => {
|
||||
const role = chatMessage.role
|
||||
|
||||
const isCheckpointGhost = messageIdx > (currCheckpointIdx ?? Infinity) && !chatIsRunning // whether to show as gray (if chat is running, for good measure just dont show any ghosts)
|
||||
|
|
@ -2001,33 +2004,6 @@ const ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, mes
|
|||
isCommitted={isCommitted}
|
||||
/>
|
||||
}
|
||||
// else if (role === 'tool_request') {
|
||||
// const ToolRequestWrapper = toolNameToComponent[chatMessage.name]?.requestWrapper as RequestWrapper<ToolName>
|
||||
// const toolRequestState = (
|
||||
// chatIsRunning === 'awaiting_user' ? 'awaiting_user'
|
||||
// : chatIsRunning === 'tool' ? 'running'
|
||||
// : chatIsRunning === 'message' ? null
|
||||
// : null
|
||||
// )
|
||||
// if (ToolRequestWrapper && canAcceptReject) { // if it's the last message
|
||||
// return <>
|
||||
// {toolRequestState !== null &&
|
||||
// <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
|
||||
// <ToolRequestWrapper
|
||||
// toolRequestState={toolRequestState}
|
||||
// toolRequest={chatMessage}
|
||||
// messageIdx={messageIdx}
|
||||
// threadId={threadId}
|
||||
// />
|
||||
// </div>}
|
||||
// {chatIsRunning === 'awaiting_user' &&
|
||||
// <div className={`${isCheckpointGhost ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||
// <ToolRequestAcceptRejectButtons />
|
||||
// </div>}
|
||||
// </>
|
||||
// }
|
||||
// return null
|
||||
// }
|
||||
else if (role === 'tool') {
|
||||
|
||||
if (chatMessage.type === 'invalid_params') {
|
||||
|
|
@ -2537,8 +2513,8 @@ export const SidebarChat = () => {
|
|||
// const lastMessageIdx = previousMessages.findLastIndex(v => v.role !== 'checkpoint')
|
||||
// tool request shows up as Editing... if in progress
|
||||
return previousMessages.map((message, i) => {
|
||||
return <ErrorBoundary><ChatBubble
|
||||
key={getChatBubbleId(threadId, i)}
|
||||
return <ChatBubble
|
||||
key={i}
|
||||
currCheckpointIdx={currCheckpointIdx}
|
||||
chatMessage={message}
|
||||
messageIdx={i}
|
||||
|
|
@ -2546,14 +2522,14 @@ export const SidebarChat = () => {
|
|||
chatIsRunning={isRunning}
|
||||
threadId={threadId}
|
||||
_scrollToBottom={() => scrollToBottom(scrollContainerRef)}
|
||||
/></ErrorBoundary>
|
||||
/>
|
||||
})
|
||||
}, [previousMessages, threadId, currCheckpointIdx, isRunning])
|
||||
|
||||
const streamingChatIdx = previousMessagesHTML.length
|
||||
const currStreamingMessageHTML = reasoningSoFar || displayContentSoFar || isRunning ?
|
||||
<ErrorBoundary><ChatBubble
|
||||
key={getChatBubbleId(threadId, streamingChatIdx)}
|
||||
<ChatBubble
|
||||
key={'curr-streaming-msg'}
|
||||
currCheckpointIdx={currCheckpointIdx}
|
||||
chatMessage={{
|
||||
role: 'assistant',
|
||||
|
|
@ -2567,13 +2543,13 @@ export const SidebarChat = () => {
|
|||
|
||||
threadId={threadId}
|
||||
_scrollToBottom={null}
|
||||
/></ErrorBoundary> : null
|
||||
/> : null
|
||||
|
||||
|
||||
// the tool currently being generated
|
||||
const generatingTool = toolIsGenerating ?
|
||||
toolCallSoFar.name === 'edit_file' ? <EditToolSoFar
|
||||
key={getChatBubbleId(threadId, streamingChatIdx + 1)}
|
||||
key={'curr-streaming-tool'}
|
||||
toolCallSoFar={toolCallSoFar}
|
||||
/>
|
||||
: null
|
||||
|
|
@ -2722,9 +2698,13 @@ export const SidebarChat = () => {
|
|||
</div>
|
||||
|
||||
|
||||
return (isLandingPage ?
|
||||
landingPageContent
|
||||
: threadPageContent
|
||||
return (
|
||||
<Fragment key={threadId} // force rerender when change thread
|
||||
>
|
||||
{isLandingPage ?
|
||||
landingPageContent
|
||||
: threadPageContent}
|
||||
</Fragment>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -332,7 +332,7 @@ export const ModelDump = () => {
|
|||
/>
|
||||
|
||||
<div className={`w-5 flex items-center justify-center`}>
|
||||
{type === 'default' ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
|
||||
{type === 'default' || type === 'autodetected' ? null : <button onClick={() => { settingsStateService.deleteModel(providerName, modelName) }}><X className='size-4' /></button>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue