mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
Merge pull request #443 from voideditor/model-selection
Chat interruption state
This commit is contained in:
commit
cf0728f4c6
7 changed files with 100 additions and 68 deletions
5
.voidrules
Normal file
5
.voidrules
Normal 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.
|
||||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 { }
|
||||
|
||||
|
|
|
|||
|
|
@ -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).'
|
||||
|
|
|
|||
Loading…
Reference in a new issue