add retries, fix key, X on autodetected

This commit is contained in:
Andrew Pareles 2025-04-18 20:48:57 -07:00
parent 7bd52438f5
commit 10949b44d1
6 changed files with 113 additions and 104 deletions

3
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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

View file

@ -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

View file

@ -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>
)
}

View file

@ -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>