tool update; multiple SEARCH/REPLACE blocks

This commit is contained in:
Andrew Pareles 2025-02-17 23:57:59 -08:00
parent d3547134e7
commit 667769c987
6 changed files with 206 additions and 114 deletions

View file

@ -44,7 +44,7 @@ export const IVoidFastApplyService = createDecorator<IFastApplyService>('voidFas
class VoidFastApplyService extends Disposable implements IFastApplyService {
_serviceBrand: undefined;
static readonly ID = 'voidFastApplyService';
// static readonly ID = 'voidFastApplyService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;

View file

@ -304,9 +304,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// agent loop
const agentLoop = async () => {
let shouldContinue = false
do {
shouldContinue = false
let shouldSendAnotherMessage = true
let nMessagesSent = 0
while (shouldSendAnotherMessage) {
shouldSendAnotherMessage = false
nMessagesSent += 1
let res_: () => void
const awaitable = new Promise<void>((res, rej) => { res_ = res })
@ -329,8 +332,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// make sure all tool names are valid so we can cast to ToolName below
const toolCalls = toolCalls_?.filter(tool => tool.name in this._toolsService.toolFns)
console.log('FINAL MESSAGE', fullText, toolCalls)
if ((toolCalls?.length ?? 0) === 0) {
this._finishStreamingTextMessage(threadId, fullText)
}
@ -346,7 +347,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolResult = await this._toolsService.toolFns[toolName](tool.params)
} catch (error) {
this._setStreamState(threadId, { error })
shouldContinue = false
shouldSendAnotherMessage = false
break
}
@ -356,12 +357,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
toolResultStr = this._toolsService.toolResultToString[toolName](toolResult as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
} catch (error) {
this._setStreamState(threadId, { error })
shouldContinue = false
shouldSendAnotherMessage = false
break
}
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: toolResult, })
shouldContinue = true
shouldSendAnotherMessage = true
}
}
@ -377,7 +378,6 @@ class ChatThreadService extends Disposable implements IChatThreadService {
await awaitable
}
while (shouldContinue);
}
agentLoop() // DO NOT AWAIT THIS, this fn should resolve when ready to clear inputs

View file

@ -1182,14 +1182,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
const uri = uri_
// generate search/replace block text
const fileContents = await VSReadFile(this._modelService, uri)
if (fileContents === null) return
const origFileContents = await VSReadFile(this._modelService, uri)
if (origFileContents === null) return
// reject all diffZones on this URI, adding to history (there can't possibly be overlap after this)
this.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: true })
const userMessageContent = searchReplace_userMessage({ originalCode: fileContents, applyStr: applyStr })
const userMessageContent = searchReplace_userMessage({ originalCode: origFileContents, applyStr: applyStr })
const messages: LLMChatMessage[] = [
{ role: 'system', content: searchReplace_systemMessage },
{ role: 'user', content: userMessageContent },
@ -1197,6 +1197,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
let streamRequestIdRef: { current: string | null } = { current: null }
const diffareaidOfBlockNum: number[] = []
const diffAreaOriginalLines: [number, number][] = []
// TODO replace all these with whatever block we're on initially if already started
let latestStreamLocationMutable: StreamLocationMutable | null = null
@ -1215,10 +1216,15 @@ class EditCodeService extends Disposable implements IEditCodeService {
return [startLine, endLine]
}
const { onFinishEdit } = this._addToHistory(uri)
let { onFinishEdit } = this._addToHistory(uri)
const revertAndContinueHistory = () => {
this._undoHistory(uri)
const { onFinishEdit: onFinishEdit_ } = this._addToHistory(uri)
onFinishEdit = onFinishEdit_
}
const onDone = (hadError: boolean) => {
const onDone = (errorMessage: false | string) => {
for (const blockNum in diffareaidOfBlockNum) {
const diffareaid = diffareaidOfBlockNum[blockNum]
const diffZone = this.diffAreaOfId[diffareaid]
@ -1227,106 +1233,175 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
}
this._refreshStylesAndDiffsInURI(uri)
if (hadError) this._undoHistory(uri)
if (errorMessage) {
this._notificationService.info(`Void had an error when running Apply: ${errorMessage}.\nIf this persists, feel free to [report](https://github.com/voideditor/void/issues/new) this error.`)
this._metricsService.capture('Error - Apply', { errorMessage })
this._undoHistory(uri)
}
onFinishEdit()
}
// any time there's an error, add assistant's message, then user message saying the problem and to retry
const onNewBlockStart = (blockNum: number, block: ExtractedSearchReplaceBlock): { errorStartingBlock?: undefined } | { errorStartingBlock: string } => {
console.log('STARTING BLOCK', JSON.stringify(block, null, 2))
// TODO!!! turn this into a service and provide it
const foundInCode = findTextInCode(block.orig, origFileContents)
if (typeof foundInCode === 'string') {
console.log('Apply error:', foundInCode, '; trying again.')
return { errorStartingBlock: foundInCode }
}
const [originalStart, originalEnd] = foundInCode
let lineOffset = 0
// compute line offset given multiple changes
for (let i = 0; i < blockNum; i += 1) {
const [diffAreaOriginalStart, diffAreaOriginalEnd] = diffAreaOriginalLines[i]
console.log('ROIGGINAL!!!', diffAreaOriginalStart, diffAreaOriginalEnd)
if (diffAreaOriginalStart > originalEnd) continue
const diffareaid = diffareaidOfBlockNum[i]
const diffArea = this.diffAreaOfId[diffareaid]
const numNewLines = diffArea.endLine - diffArea.startLine
const numOldLines = diffAreaOriginalEnd - diffAreaOriginalStart
console.log('NUM NEW', numNewLines, numOldLines)
lineOffset += numNewLines - numOldLines
}
const startLine = originalStart + lineOffset
const endLine = originalEnd + lineOffset
console.log('adding to', startLine, endLine)
const adding: Omit<DiffZone, 'diffareaid'> = {
type: 'DiffZone',
originalCode: block.orig,
startLine,
endLine,
_URI: uri,
_streamState: {
isStreaming: true,
streamRequestIdRef,
line: startLine,
},
_diffOfId: {}, // added later
_removeStylesFns: new Set(),
}
const diffZone = this._addDiffArea(adding)
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidAddOrDeleteDiffZones.fire({ uri })
diffareaidOfBlockNum.push(diffZone.diffareaid)
diffAreaOriginalLines.push([originalStart, originalEnd])
latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
return { errorStartingBlock: undefined }
}
let shouldSendAnotherMessage = true
let nMessagesSent = 0
// this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
useProviderFor: 'Apply',
logging: { loggingName: `generateSearchAndReplace` },
messages,
onText: ({ fullText }) => {
const blocks = extractSearchReplaceBlocks(fullText)
while (shouldSendAnotherMessage) {
shouldSendAnotherMessage = false
nMessagesSent += 1
for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) {
const block = blocks[blockNum]
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
useProviderFor: 'Apply',
logging: { loggingName: `generateSearchAndReplace` },
messages,
onText: ({ fullText }) => {
const blocks = extractSearchReplaceBlocks(fullText)
if (block.state === 'done')
currStreamingBlockNum = blockNum
for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) {
const block = blocks[blockNum]
if (block.state === 'writingOriginal') // must be done writing original
continue
if (block.state === 'done')
currStreamingBlockNum = blockNum
// if should add new diffarea
if (!(blockNum in diffareaidOfBlockNum)) {
const foundInCode = findTextInCode(block.orig, fileContents)
if (typeof foundInCode === 'string') {
// TODO!!! log and retry
console.log('NOT FOUND IN CODE!!!!', foundInCode)
if (block.state === 'writingOriginal') // must be done writing original
continue
// if this is the first time we're seeing this block, add it as a diffarea
if (!(blockNum in diffareaidOfBlockNum)) {
console.log('FULLTEXT!!!!!\n', fullText)
const { errorStartingBlock } = onNewBlockStart(blockNum, block)
if (errorStartingBlock) {
console.log('ERROR STARTING BLOCK SPOT!!!!!', errorStartingBlock)
const errMsgForLLM = errorStartingBlock === 'Not found' ?
'I interrupted you because the latest ORIGINAL code could not be found in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in ORIGINAL is identical to a code snippet in the file.'
: errorStartingBlock === 'Not unique' ?
'I interrupted you because the latest ORIGINAL code shows up multiple times in the file. Please output all SEARCH/REPLACE blocks again, making sure the code in each ORIGINAL section is unique in the file.'
: ''
messages.push(
{ role: 'assistant', content: fullText }, // latest output
{ role: 'user', content: errMsgForLLM } // user explanation of what's wrong
)
if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current)
shouldSendAnotherMessage = true
revertAndContinueHistory()
return
}
}
const [startLine, endLine] = foundInCode
const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity)
oldBlocks = blocks
const adding: Omit<DiffZone, 'diffareaid'> = {
type: 'DiffZone',
originalCode: block.orig,
startLine,
endLine,
_URI: uri,
_streamState: {
isStreaming: true,
streamRequestIdRef,
line: startLine,
},
_diffOfId: {}, // added later
_removeStylesFns: new Set(),
}
const diffZone = this._addDiffArea(adding)
this._onDidChangeStreaming.fire({ uri, diffareaid: diffZone.diffareaid })
this._onDidAddOrDeleteDiffZones.fire({ uri })
// write new text to diffarea
const diffareaid = diffareaidOfBlockNum[blockNum]
const diffZone = this.diffAreaOfId[diffareaid]
if (diffZone?.type !== 'DiffZone') continue
diffareaidOfBlockNum.push(diffZone.diffareaid)
latestStreamLocationMutable = { line: diffZone.startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 }
if (!latestStreamLocationMutable) continue
this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable)
} // end for
this._refreshStylesAndDiffsInURI(uri)
},
onFinalMessage: async ({ fullText }) => {
console.log('final message!!', fullText)
// 1. wait 500ms and fix lint errors - call lint error workflow
// (update react state to say "Fixing errors")
const blocks = extractSearchReplaceBlocks(fullText)
if (blocks.length === 0) {
this._notificationService.info(`Void: When running Apply, your model didn't output any changes we recognized. You might need to use a smarter model for Apply.`)
}
const deltaFinalText = block.final.substring((oldBlocks[blockNum]?.final ?? '').length, Infinity)
oldBlocks = blocks
// write new text to diffarea
const diffareaid = diffareaidOfBlockNum[blockNum]
const diffZone = this.diffAreaOfId[diffareaid]
if (diffZone?.type !== 'DiffZone') continue
for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) {
const block = blocks[blockNum]
const diffareaid = diffareaidOfBlockNum[blockNum]
const diffZone = this.diffAreaOfId[diffareaid]
if (diffZone?.type !== 'DiffZone') continue
this._writeText(uri, block.final,
{ startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
{ shouldRealignDiffAreas: true }
)
}
onDone(false)
},
onError: (e) => {
console.log('ERROR in SearchReplace:', e.message)
onDone(e.message)
},
if (!latestStreamLocationMutable) continue
this._writeStreamedDiffZoneLLMText(diffZone, block.final, deltaFinalText, latestStreamLocationMutable)
} // end for
})
}
this._refreshStylesAndDiffsInURI(uri)
},
onFinalMessage: async ({ fullText }) => {
console.log('/* ON FINALMESSAGE */', fullText)
// 1. wait 500ms and fix lint errors - call lint error workflow
// (update react state to say "Fixing errors")
const blocks = extractSearchReplaceBlocks(fullText)
for (let blockNum = 0; blockNum < blocks.length; blockNum += 1) {
const block = blocks[blockNum]
const diffareaid = diffareaidOfBlockNum[blockNum]
const diffZone = this.diffAreaOfId[diffareaid]
if (diffZone?.type !== 'DiffZone') continue
this._writeText(uri, block.final,
{ startLineNumber: diffZone.startLine, startColumn: 1, endLineNumber: diffZone.endLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed
{ shouldRealignDiffAreas: true }
)
}
onDone(false)
},
onError: (e) => {
console.log('ERROR', e);
onDone(true)
},
})
}

View file

@ -21,23 +21,25 @@ You are a coding assistant. You are given a list of instructions to follow \`INS
Please respond to the user's query. The user's query is never invalid.
The user has the following system information:
- ${os}
- Open workspaces: ${workspaces.join(', ')}
- ${os}
- Open workspaces: ${workspaces.join(', ')}
In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes.
For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer.
- Do not re-write the entire file in the code block.
- You can write comments like "// ... existing code" to indicate existing code.
- Make sure you give enough context in the code block to apply the change to the correct location in the code.
- Do not re-write the entire file in the code block.
- You can write comments like "// ... existing code" to indicate existing code.
- Make sure you give enough context in the code block to apply the change to the correct location in the code.
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
If you are given tools, you are allowed to use them without asking for permission. You do not have to use them if you don't want to, but you may use them to gather context, etc.
If you are given tools, NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc.
If you are given tools:
- You are allowed to use tools without asking for permission.
- Feel free to use tools to gather context, make suggestions, etc.
- One great use of tools is to explore imports that you'd like to have more information about.
- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc.
Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them.
Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below.
## EXAMPLE 1
FILES
math.ts
@ -249,9 +251,9 @@ The user's request may be "fuzzy" or not well-specified, and it is your job to i
2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make.
For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name.
- Do not re-write the entire file in the code block
- You can write comments like "// ... existing code" to indicate existing code
- Make sure you give enough context in the code block to apply the changes to the correct location in the code`
- Do not re-write the entire file in the code block
- You can write comments like "// ... existing code" to indicate existing code
- Make sure you give enough context in the code block to apply the changes to the correct location in the code`
export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, modelService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService }) => {
@ -303,23 +305,28 @@ export const searchReplace_systemMessage = `\
You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file.
A SEARCH/REPLACE block describes the code before and after a change. Here is the format:
${tripleTick[0]}
${ORIGINAL}
// ... original code goes here
${DIVIDER}
// ... final code goes here
${FINAL}
${tripleTick[1]}
You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make.
Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks.
Directions:
1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
2. The "original" code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file.
3. The "original" code in each SEARCH/REPLACE block should include enough text to uniquely identify the change in the file.
4. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
2. The original code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file.
3. The original code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file.
4. The original code in each SEARCH/REPLACE block must be disjoint from all other blocks.
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
- Make sure you add all necessary imports.
- Make sure the "final" code is complete and will not result in syntax/lint errors.
5. Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
## EXAMPLE 1
ORIGINAL_FILE

View file

@ -8,6 +8,7 @@ import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js';
@ -15,12 +16,10 @@ export interface ISearchReplaceService {
readonly _serviceBrand: undefined;
}
export const ISearchReplaceService = createDecorator<ISearchReplaceService>('SearchReplaceService');
export const ISearchReplaceService = createDecorator<ISearchReplaceService>('SearchReplaceCacheService');
class SearchReplaceService extends Disposable implements ISearchReplaceService {
_serviceBrand: undefined;
static readonly ID = 'SearchReplaceService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
@ -28,8 +27,18 @@ class SearchReplaceService extends Disposable implements ISearchReplaceService {
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
) {
super()
// this.llmMessageService.sendLLMMessage({})
}
send(params: Omit<ServiceSendLLMMessageParams, 'onText'> & { onText: (p: { newText: string, fullText: string }) => { retry: boolean } }) {
this.llmMessageService.sendLLMMessage({
...params as ServiceSendLLMMessageParams,
onText: (p) => {
const { retry } = params.onText(p)
if (retry) {
}
}
})
}
}

View file

@ -552,7 +552,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
: '(never)',
subTextMd: providerName === 'ollama' ? '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).' :
undefined,
providerName === 'vLLM' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).' :
undefined,
}
}
else if (settingName === '_didFillInProviderSettings') {