Merge pull request #443 from voideditor/model-selection

Chat interruption state
This commit is contained in:
Andrew Pareles 2025-05-01 00:45:57 -07:00 committed by GitHub
commit cf0728f4c6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 100 additions and 68 deletions

5
.voidrules Normal file
View file

@ -0,0 +1,5 @@
This is a fork of the VSCode repo called Void.
Most code we care about lives in src/vs/workbench/contrib/void.
You may sometimes need to explore the full repo to find relevant parts of code.

View file

@ -180,7 +180,7 @@ export type ThreadStreamState = {
error?: undefined;
llmInfo?: undefined;
toolInfo?: undefined;
interrupt?: undefined;
interrupt: 'not_needed' | Promise<() => void>; // calling this should have no effect on state - would be too confusing. it just cancels the tool
}
}
@ -491,7 +491,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._addUserCheckpoint({ threadId })
}
// add tool that's running
else if (this.streamState[threadId]?.isRunning === 'tool') {
@ -502,10 +501,16 @@ class ChatThreadService extends Disposable implements IChatThreadService {
else if (this.streamState[threadId]?.isRunning === 'awaiting_user') {
this.rejectLatestToolRequest(threadId)
}
else if (this.streamState[threadId]?.isRunning === 'idle') {
// do nothing
}
this._addUserCheckpoint({ threadId })
// interrupt any effects
const interrupt = await this.streamState[threadId]?.interrupt
interrupt?.()
if (typeof interrupt === 'function')
interrupt()
this._setStreamState(threadId, undefined)
@ -598,9 +603,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
}
finally {
this._setStreamState(threadId, undefined)
}
// 4. stringify the result to give to the LLM
try {
@ -633,12 +635,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}) {
let interruptedWhenIdle = false
const idleInterruptor = Promise.resolve(() => { interruptedWhenIdle = true })
// _runToolCall does not need setStreamState({idle}) before it, but it needs it after it. (handles its own setStreamState)
// 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
// not running at start, clear state
this._setStreamState(threadId, { isRunning: 'idle' })
let nMessagesSent = 0
let shouldSendAnotherMessage = true
let isRunningWhenEnd: IsRunningType = undefined
@ -646,9 +649,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// before enter loop, call tool
if (callThisToolFirst) {
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
this._setStreamState(threadId, undefined)
if (interrupted) { return }
if (interrupted) {
this._setStreamState(threadId, undefined)
this._addUserCheckpoint({ threadId })
}
}
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
// tool use loop
while (shouldSendAnotherMessage) {
@ -657,6 +665,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
isRunningWhenEnd = undefined
nMessagesSent += 1
this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor })
const chatMessages = this.state.allThreads[threadId]?.messages ?? []
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
chatMessages,
@ -664,20 +674,20 @@ class ChatThreadService extends Disposable implements IChatThreadService {
chatMode
})
if (interruptedWhenIdle) {
this._setStreamState(threadId, undefined)
return
}
let shouldRetryLLM = true
let nAttempts = 0
while (shouldRetryLLM) {
// if (this.streamState[threadId]?.isRunning === 'LLM' || ) {
// // if already streaming, stop
// console.log('returning...', this.streamState[threadId])
// return
// }
shouldRetryLLM = false
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
let resMessageIsDonePromise: (res: { type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<{ type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }>((res, rej) => { resMessageIsDonePromise = res })
let aborted = false
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
@ -691,36 +701,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, undefined)
resMessageIsDonePromise(toolCall) // resolve with tool calls
resMessageIsDonePromise({ type: 'llmDone', toolCall }) // resolve with tool calls
},
onError: async (error) => {
if (this.streamState[threadId]?.isRunning !== 'LLM') {
console.log('Unexpected onError when', this.streamState[threadId]?.isRunning)
return
}
if (nAttempts < CHAT_RETRIES) {
nAttempts += 1
shouldRetryLLM = true
this._setStreamState(threadId, undefined) // clear later so can be interrupted
resMessageIsDonePromise()
}
else {
// add assistant's message to chat history, and clear selection
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._setStreamState(threadId, { isRunning: undefined, error })
resMessageIsDonePromise()
}
resMessageIsDonePromise({ type: 'llmError', error: error })
},
onAbort: () => {
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
aborted = true
this._setStreamState(threadId, { isRunning: 'idle' })
resMessageIsDonePromise()
resMessageIsDonePromise({ type: 'llmAborted' })
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
},
})
@ -732,31 +721,63 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: '', reasoningSoFar: '', toolCallSoFar: null }, interrupt: Promise.resolve(() => this._llmMessageService.abort(llmCancelToken)) })
const toolCall = await messageIsDonePromise // wait for message to complete
if (aborted) {
const llmRes = await messageIsDonePromise // wait for message to complete
if (this.streamState[threadId]?.isRunning !== 'LLM') {
console.log('Unexpected chat agent state when', this.streamState[threadId]?.isRunning)
this._setStreamState(threadId, undefined)
return
}
if (shouldRetryLLM) {
this._setStreamState(threadId, { isRunning: 'idle' })
await timeout(RETRY_DELAY)
continue
// llm res aborted
if (llmRes.type === 'llmAborted') {
this._setStreamState(threadId, undefined)
return
}
this._setStreamState(threadId, { isRunning: 'idle' })
// llm res error
else if (llmRes.type === 'llmError') {
// error, should retry
if (nAttempts < CHAT_RETRIES) {
nAttempts += 1
shouldRetryLLM = true
this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor })
await timeout(RETRY_DELAY)
if (interruptedWhenIdle) {
this._setStreamState(threadId, undefined)
return
}
else
continue // retry
}
// error, but too many attempts
else {
const { error } = llmRes
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._setStreamState(threadId, { isRunning: undefined, error })
this._addUserCheckpoint({ threadId })
return
}
}
// llm res success
const { toolCall } = llmRes
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
// call tool if there is one
if (toolCall) {
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
if (interrupted) {
this._setStreamState(threadId, undefined)
return
}
this._setStreamState(threadId, { isRunning: 'idle' })
if (awaitingUserApproval) { isRunningWhenEnd = 'awaiting_user' }
else { shouldSendAnotherMessage = true }
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
}
} // end while (attempts)
@ -766,8 +787,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { isRunning: isRunningWhenEnd })
// add checkpoint before the next user message
if (!isRunningWhenEnd)
this._addUserCheckpoint({ threadId })
if (!isRunningWhenEnd) this._addUserCheckpoint({ threadId })
// capture number of messages sent
this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode })

View file

@ -363,7 +363,7 @@ const prepareOpenAIOrAnthropicMessages = ({
// find system messages and concatenate them
const newSystemMessage = aiInstructions ?
`${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}`
`${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES (from the user's .voidrules file):\n${aiInstructions}`
: systemMessage
let separateSystemMessageStr: string | undefined = undefined

View file

@ -591,7 +591,7 @@ export const SelectedFiles = (
select-none
text-xs text-nowrap
border rounded-sm
${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-3 hover:brightness-95 text-void-fg-1'}
${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-1 hover:brightness-95 text-void-fg-1'}
${isThisSelectionProspective
? 'border-void-border-2'
: 'border-void-border-1'
@ -2881,7 +2881,7 @@ export const SidebarChat = () => {
{generatingTool}
{/* loading indicator */}
{isRunning === 'LLM' && !toolIsGenerating ? <ProseWrapper>
{isRunning === 'LLM' || isRunning === 'idle' && !toolIsGenerating ? <ProseWrapper>
{<IconLoading className='opacity-50 text-sm' />}
</ProseWrapper> : null}

View file

@ -818,19 +818,22 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
key={o.fullName}
className={`
flex items-center gap-2
px-3 py-1 cursor-pointer bg-void-bg-2-alt
${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}
px-3 py-1 cursor-pointer
${oIdx === optionIdx ? 'bg-blue-500 text-white/80' : 'bg-void-bg-2-alt text-void-fg-1'}
`}
onClick={() => { onSelectOption(); }}
onMouseMove={() => { setOptionIdx(oIdx) }}
>
{<o.iconInMenu size={12} />}
<span className="text-void-fg-1">{o.abbreviatedName}</span>
{o.fullName && o.fullName !== o.abbreviatedName && <span className="text-void-fg-1 opacity-60 text-sm">{o.fullName}</span>}
<span>{o.abbreviatedName}</span>
{o.fullName && o.fullName !== o.abbreviatedName && <span className="opacity-60 text-sm">{o.fullName}</span>}
{o.nextOptions || o.generateNextOptions ? (
<ChevronRight size={12} />
) : null}
</div>
)
})
@ -1379,7 +1382,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
key={optionName}
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
transition-all duration-100
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'}
${thisOptionIsSelected ? 'bg-blue-500 text-white/80' : 'hover:bg-blue-500 hover:text-white/80'}
`}
onClick={() => {
onChangeOption(option);
@ -1401,7 +1404,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
</div>
<span className="flex justify-between items-center w-full gap-x-1">
<span>{optionName}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
<span className='opacity-60'>{optionDetail}</span>
</span>
</div>
);

View file

@ -827,12 +827,16 @@ export const OneClickSwitchButton = ({ fromEditor = 'VS Code', className = '' }:
'rooveterinaryinc.roo-cline', // roo
];
for (const { from, to } of transferTheseFiles) {
console.log('Transferring...', from)
try {
// find a blacklisted item
const isBlacklisted = extensionBlacklist.find(blacklistItem => {
return from.fsPath?.includes(blacklistItem)
})
if (isBlacklisted) continue
if (isBlacklisted) {
console.log(`Skipping conflicting item (${isBlacklisted})`)
continue
}
} catch { }

View file

@ -83,7 +83,7 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
return { title: 'LM Studio', }
}
else if (providerName === 'openAICompatible') {
return { title: 'OpenAI-Compatible', }
return { title: 'Custom', }
}
else if (providerName === 'gemini') {
return { title: 'Gemini', }
@ -117,7 +117,7 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
if (providerName === 'groq') return 'Get your [API Key here](https://console.groq.com/keys).'
if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).'
if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).'
if (providerName === 'openAICompatible') return `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).`
if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (most popular ones are).`
// if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
if (providerName === 'ollama') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'