fixed events and interrupts during agent mode

This commit is contained in:
Andrew Pareles 2025-03-21 02:47:31 -07:00
parent 4dff3e216a
commit 8511681710
10 changed files with 401 additions and 363 deletions

View file

@ -11,13 +11,13 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from '../common/prompt/prompts.js';
import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString, voidTools } from '../common/prompt/prompts.js';
import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ToolName, ToolCallParams, ToolResultType, InternalToolInfo, voidTools, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js';
import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval, InternalToolInfo } from '../common/toolsServiceTypes.js';
import { IToolsService } from './toolsService.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
@ -109,11 +109,11 @@ export type ThreadsState = {
export type IsRunningType = undefined | 'message' | 'tool' | 'awaiting_user'
export type ThreadStreamState = {
[threadId: string]: undefined | {
// state related
// state related to streaming (not just when streaming)
isRunning?: IsRunningType; // whether or not actually running the agent loop (can be running and not streaming, like if it's calling a tool and awaiting user response)
error?: { message: string, fullError: Error | null, };
// streaming related
// streaming related - when streaming message
streamingToken?: string;
messageSoFar?: string;
reasoningSoFar?: string;
@ -426,7 +426,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
result: {
type: 'success',
params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'I think we should do this:\n```typescript\n//Add console.log statement\n for i in ...\n\t\tdo:\nabc\n```' },
value: {}
value: Promise.resolve()
},
} satisfies ToolMessage<'edit'>,
{
@ -645,14 +645,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const callThisToolFirst: ToolRequestApproval<ToolName> = lastMessage
this._chatAgentLoop({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() })
this._runChatAgent({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() })
}
rejectTool(threadId: string) {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
this._cancelToolOfThreadId[threadId]?.()
const lastMessage = thread.messages[thread.messages.length - 1]
if (lastMessage.role !== 'tool_request') return // should never happen
const { name, params, paramsStr, id } = lastMessage
@ -665,21 +663,24 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
// abort the stream first so it doesn't change any state
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
// add the correct message to the state
const lastMessage = thread.messages[thread.messages.length - 1]
if (lastMessage.role === 'tool_request') {
// interrupt tool request
const isRunning = this.streamState[threadId]?.isRunning
// reject the tool for the user
if (isRunning === 'awaiting_user') {
this.rejectTool(threadId)
}
else {
// interrupt assistant message
// interrupt the tool
else if (isRunning === 'tool') {
this._currentlyRunningToolInterruptor[threadId]?.()
}
// interrupt assistant message
else if (isRunning === 'message') {
// abort the stream first so it doesn't change any state
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) }
this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
}
@ -706,9 +707,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
private readonly _cancelToolOfThreadId: { [threadId: string]: (() => void) | undefined } = {}
private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {}
private async _chatAgentLoop({
private async _runChatAgent({
threadId,
prevSelns,
currSelns,
@ -761,7 +762,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const handleToolCall = async (
tool: ToolCallType,
opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] },
): Promise<{ awaitingUserApproval?: boolean, canceled?: boolean }> => {
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
const toolName: ToolName = tool.name
const toolParamsStr = tool.paramsStr
const toolId = tool.id
@ -799,17 +800,22 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 3. call the tool
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
let canceled = false
let interrupted = false
try {
const { result, cancel } = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad...
this._cancelToolOfThreadId[threadId] = cancel
let cancelRes: () => void = () => { }
const resolveIfCancel = new Promise<never>((res, rej) => { cancelRes = rej })
this._cancelToolOfThreadId[threadId] = () => { cancel?.(); canceled = true; delete this._cancelToolOfThreadId[threadId]; cancelRes() }
toolResult = await Promise.race([result, resolveIfCancel]) // this await is needed, typescript is bad...
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
this._currentlyRunningToolInterruptor[threadId] = () => {
interrupted = true;
interruptTool?.();
delete this._currentlyRunningToolInterruptor[threadId];
}
toolResult = await result // ts is bad... await is needed
}
catch (error) {
if (canceled) return { canceled: true }
if (interrupted) {
// ideally this should have same implementation as abort - addMessage should get called in stopRunning
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: this.errMsgs.rejected, result: { type: 'rejected', params: toolParams }, })
return { interrupted: true }
}
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, })
return {}
@ -843,25 +849,22 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// before enter loop, call tool
if (callThisToolFirst) {
this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
const { canceled } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params })
if (canceled) return
const { interrupted } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params })
if (interrupted) return
}
// tool use loop
while (shouldSendAnotherMessage) {
this._setStreamState(threadId, { isRunning: 'message' }, 'merge')
// false by default each iteration
shouldSendAnotherMessage = false
isRunningWhenEnd = undefined
nMessagesSent += 1
let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<void>((res, rej) => { resMessageIsDonePromise = res })
let resMessageIsDonePromise: (toolCalls?: ToolCallType[] | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<ToolCallType[] | undefined>((res, rej) => { resMessageIsDonePromise = res })
// send llm message
this._setStreamState(threadId, { isRunning: 'message' }, 'merge')
const messages = await getLatestMessages()
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
@ -872,23 +875,11 @@ class ChatThreadService extends Disposable implements IChatThreadService {
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }, 'merge') },
onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => {
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning })
// added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning)
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, }, 'merge')
// call tool if there is one
const tool: ToolCallType | undefined = toolCalls?.[0]
if (tool) {
const { awaitingUserApproval } = await handleToolCall(tool) // things happen correctly if canceled is true here, because canceled calls onAbort
if (awaitingUserApproval) {
isRunningWhenEnd = 'awaiting_user'
} else {
shouldSendAnotherMessage = true
}
}
resMessageIsDonePromise()
// resolve with tool calls
resMessageIsDonePromise(toolCalls)
},
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
@ -913,11 +904,28 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}, 'set')
break
}
this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message
await messageIsDonePromise
const toolCalls = await messageIsDonePromise // wait for message to complete
if (aborted) { return }
this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done
// call tool if there is one
const tool: ToolCallType | undefined = toolCalls?.[0]
if (tool) {
const { awaitingUserApproval, interrupted } = await handleToolCall(tool)
// 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
@ -940,9 +948,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// if the current thread is already streaming, stop it (this simply resolves the promise to free up space)
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken) {
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
}
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
// selections in all past chats, then in current chat (can have many duplicates here)
const prevSelns: StagingSelectionItem[] = _chatSelections?.prevSelns ?? this._getAllSelections(threadId)
@ -955,7 +961,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
this._addMessageToThread(threadId, userHistoryElt)
this._chatAgentLoop({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), })
this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), })
}
dismissStreamError(threadId: string): void {

View file

@ -722,7 +722,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
private _addToHistory(uri: URI, opts?: { onUndo?: () => void }) {
private _addToHistory(uri: URI, opts?: { onWillUndo?: () => void }) {
const getCurrentSnapshot = (): HistorySnapshot => {
@ -807,7 +807,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
resource: uri,
label: 'Void Changes',
code: 'undoredo.editCode',
undo: () => { restoreDiffAreas(beforeSnapshot); opts?.onUndo?.() },
undo: () => { opts?.onWillUndo?.(); restoreDiffAreas(beforeSnapshot); },
redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) }
}
this._undoRedoService.pushElement(elt)
@ -1163,7 +1163,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// the applyDonePromise this returns can throw an error (reject)
// the applyDonePromise this returns can reject, and should be caught with .catch
public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null> {
let res: [DiffZone, Promise<void>] | undefined = undefined
@ -1212,14 +1212,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
uri,
startBehavior,
streamRequestIdRef,
onUndo,
linkedCtrlKZone,
onWillUndo,
}: {
uri: URI,
startBehavior: 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts',
streamRequestIdRef: { current: string | null },
linkedCtrlKZone: CtrlKZone | null,
onUndo: () => void,
onWillUndo: () => void,
}) {
const { model } = this._voidModelService.getModel(uri)
if (!model) return
@ -1235,8 +1235,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
const originalFileStr = model.getValue(EndOfLinePreference.LF)
let originalCode = model.getValueInRange(range, EndOfLinePreference.LF)
// add to history as a checkpoint, before we start modifying
const { onFinishEdit } = this._addToHistory(uri, { onUndo })
const { onFinishEdit } = this._addToHistory(uri, { onWillUndo })
// clear diffZones so no conflict
if (startBehavior === 'keep-conflicts') {
@ -1328,21 +1329,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
throw new Error(`Void: diff.type not recognized on: ${from}`)
}
console.log('q1', this.diffAreaOfId)
await this._voidModelService.initializeModel(uri)
console.log('q2', this.diffAreaOfId)
const { model } = this._voidModelService.getModel(uri)
if (!model) return
console.log('q3', this.diffAreaOfId)
// promise that resolves when the apply is done
let resApplyPromise: () => void
let rejApplyPromise: (e: any) => void
const applyPromise = new Promise<void>((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ })
let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId
// build messages
const quickEditFIMTags = defaultQuickEditFimTags // TODO can eventually let users customize modelFimTags
const originalFileCode = model.getValue(EndOfLinePreference.LF)
@ -1357,13 +1349,9 @@ class EditCodeService extends Disposable implements IEditCodeService {
]
}
else if (from === 'QuickEdit') {
console.log('aaa')
if (!ctrlKZoneIfQuickEdit) return
console.log('bbb', ctrlKZoneIfQuickEdit)
const { _mountInfo } = ctrlKZoneIfQuickEdit
console.log('ccc', _mountInfo)
const instructions = _mountInfo?.textAreaRef.current?.value ?? ''
console.log('ddd', instructions)
const startLine = startRange === 'fullFile' ? 1 : startRange[0]
const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1]
@ -1378,17 +1366,22 @@ class EditCodeService extends Disposable implements IEditCodeService {
else { throw new Error(`featureName ${from} is invalid`) }
// start diffzone
const res = this._startStreamingDiffZone({
uri,
streamRequestIdRef,
startBehavior: opts.startBehavior,
onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) },
linkedCtrlKZone: ctrlKZoneIfQuickEdit,
onWillUndo: () => {
if (streamRequestIdRef.current) {
this._llmMessageService.abort(streamRequestIdRef.current)
}
},
})
if (!res) return
const { diffZone, onFinishEdit } = res
const { diffZone, onFinishEdit, } = res
// helpers
@ -1408,6 +1401,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
onFinishEdit()
}
// throws
const onError = (e: { message: string; fullError: Error | null; }) => {
this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError
}
const extractText = (fullText: string, recentlyAddedTextLen: number) => {
if (from === 'QuickEdit') {
return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag })
@ -1418,7 +1419,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
throw new Error('Void 1')
}
// refresh now in case onText takes a while to get 1st message
this._refreshStylesAndDiffsInURI(uri)
@ -1428,7 +1428,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
const writeover = async () => {
// allowed to throw errors - this is called inside a promise that handles everything
const runWriteover = async () => {
let shouldSendAnotherMessage = true
while (shouldSendAnotherMessage) {
shouldSendAnotherMessage = false
@ -1440,6 +1441,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
let fullTextSoFar = '' // so far (INCLUDING ignored suffix)
let prevIgnoredSuffix = ''
let aborted = false
let weAreAborting = false
streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
@ -1476,31 +1479,27 @@ class EditCodeService extends Disposable implements IEditCodeService {
resMessageDonePromise()
},
onError: (e) => {
this._notifyError(e)
onDone()
this._undoHistory(uri)
resMessageDonePromise()
onError(e)
},
onAbort: () => {
if (weAreAborting) return
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
resMessageDonePromise()
aborted = true
resMessageDonePromise()
},
})
// should never happen, just for safety
if (streamRequestIdRef.current === null) { return }
await messageDonePromise
if (aborted) { return }
if (aborted) {
throw new Error(`Edit was interrupted by the user.`)
}
} // end while
} // end writeover
writeover().then(() => {
resApplyPromise()
// this._noLongerNeedModelReference(uri)
}).catch((e) => rejApplyPromise(e))
return [diffZone, applyPromise]
const applyDonePromise = runWriteover()
return [diffZone, applyDonePromise]
}
@ -1523,10 +1522,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
const { model } = this._voidModelService.getModel(uri)
if (!model) return
// promise that resolves when the apply is done
let resApplyDonePromise: () => void
let rejApplyDonePromise: (e: any) => void
const applyDonePromise = new Promise<void>((res_, rej_) => { resApplyDonePromise = res_; rejApplyDonePromise = rej_ })
let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId
@ -1544,7 +1539,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
streamRequestIdRef,
startBehavior: opts.startBehavior,
linkedCtrlKZone: null,
onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by user pressing undo.')) },
onWillUndo: () => {
if (streamRequestIdRef.current) {
this._llmMessageService.abort(streamRequestIdRef.current) // triggers onAbort()
}
},
})
if (!res) return
const { diffZone, onFinishEdit } = res
@ -1573,7 +1572,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
const errMsgOfInvalidStr = (str: string & ReturnType<typeof findTextInCode>, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => {
const errContentOfInvalidStr = (str: string & ReturnType<typeof findTextInCode>, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => {
const descStr = str === `Not found` ?
`The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
@ -1604,6 +1603,13 @@ class EditCodeService extends Disposable implements IEditCodeService {
onFinishEdit()
}
const onError = (e: { message: string; fullError: Error | null; }) => {
this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError // throw error h
}
// refresh now in case onText takes a while to get 1st message
this._refreshStylesAndDiffsInURI(uri)
@ -1614,12 +1620,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
const addedTrackingZoneOfBlockNum: TrackingZone<SearchReplaceDiffAreaMetadata>[] = []
diffZone._streamState.line = 1
const featureName: FeatureName = 'Apply'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
const retryLoop = async () => {
const N_RETRIES = 5
// allowed to throw errors - this is called inside a promise that handles everything
const runSearchReplace = async () => {
// this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it
let shouldSendAnotherMessage = true
let nMessagesSent = 0
@ -1629,10 +1637,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
while (shouldSendAnotherMessage) {
shouldSendAnotherMessage = false
nMessagesSent += 1
if (nMessagesSent >= 5) {
this._notifyError({ message: 'Tried to Fast Apply 5 times but failed. Please try again with a smarter model or disable Fast Apply.', fullError: null })
onDone()
this._undoHistory(uri)
if (nMessagesSent >= N_RETRIES) {
const e = {
message: `Tried to Fast Apply ${N_RETRIES} times but failed. This may be related to model intelligence, or it may an edit that's too complex. Please retry or disable Fast Apply.`,
fullError: null
}
onError(e)
break
}
@ -1693,7 +1703,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
console.log('fullText', { fullText })
console.log('error:', originalBounds)
console.log('block.orig:', block.orig)
const content = errMsgOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks)
messages.push(
{ role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output
{ role: 'user', content: content } // user explanation of what's wrong
@ -1769,8 +1779,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
// diffZone._streamState.line = currentEndLine
diffZone._streamState.line = latestStreamLocationMutable.line
} // end for
this._refreshStylesAndDiffsInURI(uri)
@ -1822,41 +1830,45 @@ class EditCodeService extends Disposable implements IEditCodeService {
resMessageDonePromise()
},
onError: (e) => {
this._notifyError(e)
onDone()
this._undoHistory(uri)
resMessageDonePromise()
onError(e)
},
onAbort: () => {
if (weAreAborting) return
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
resMessageDonePromise()
aborted = true
resMessageDonePromise()
},
})
// should never happen, just for safety
if (streamRequestIdRef.current === null) { break }
console.log('awaiting...')
await messageDonePromise
console.log('done awaiting, aborted=', aborted)
if (aborted) { return }
if (aborted) {
throw new Error(`Edit was interrupted by the user.`)
}
} // end while
} // end retryLoop
retryLoop().then(() => {
console.log('resolving Apply Done')
resApplyDonePromise()
// this._noLongerNeedModelReference(uri)
}).catch((e) => rejApplyDonePromise(e))
const applyDonePromise = runSearchReplace()
return [diffZone, applyDonePromise]
}
_undoHistory(uri: URI) {
this._undoRedoService.undo(uri)
}
isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) {
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (!ctrlKZone) return false
if (ctrlKZone.type !== 'CtrlKZone') return false
return !!ctrlKZone._linkedStreamingDiffZone
}
private _stopIfStreaming(diffZone: DiffZone) {
const uri = diffZone._URI
@ -1870,31 +1882,6 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
}
_undoHistory(uri: URI) {
this._undoRedoService.undo(uri)
}
_interruptSingleDiffZoneStreaming({ diffareaid }: { diffareaid: number }) {
const diffZone = this.diffAreaOfId[diffareaid]
if (diffZone?.type !== 'DiffZone') return
if (!diffZone._streamState.isStreaming) return
this._stopIfStreaming(diffZone)
this._undoHistory(diffZone._URI)
}
isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) {
const ctrlKZone = this.diffAreaOfId[diffareaid]
if (!ctrlKZone) return false
if (ctrlKZone.type !== 'CtrlKZone') return false
return !!ctrlKZone._linkedStreamingDiffZone
}
// diffareaid of the ctrlKZone (even though the stream state is dictated by the linked diffZone)
interruptCtrlKStreaming({ diffareaid }: { diffareaid: number }) {
@ -1906,7 +1893,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (!linkedStreamingDiffZone) return
if (linkedStreamingDiffZone.type !== 'DiffZone') return
this._interruptSingleDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid })
this._stopIfStreaming(linkedStreamingDiffZone)
this._undoHistory(linkedStreamingDiffZone._URI)
}

View file

@ -159,15 +159,13 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri
const onClickSubmit = useCallback(async () => {
if (isDisabled) return
if (getStreamState() === 'streaming') return
const [newApplyingUri, _] = await editCodeService.startApplying({
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
from: 'ClickApply',
applyStr: codeStr,
uri: uri,
startBehavior: 'keep-conflicts',
}) ?? []
if (!newApplyingUri) console.log('NOT new applying')
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
rerender(c => c + 1)

View file

@ -63,7 +63,7 @@ export const QuickEditChat = ({
if (isStreamingRef.current) return
textAreaFnsRef.current?.disable()
editCodeService.startApplying({
const res = editCodeService.startApplying({
from: 'QuickEdit',
diffareaid,
startBehavior: 'keep-conflicts',

View file

@ -1040,7 +1040,7 @@ const ProseWrapper = ({ children }: { children: React.ReactNode }) => {
{children}
</div>
}
const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, }) => {
const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType }) => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
@ -1058,7 +1058,7 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas
}
const isEmpty = !chatMessage.content && !chatMessage.reasoning
const isLastAndLoading = !isCommitted && isLast
const isLastAndLoading = !isCommitted && isLast && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user')
if (isEmpty && !isLastAndLoading) return null
return <>
@ -1770,6 +1770,7 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin
chatMessage={chatMessage}
messageIdx={messageIdx}
isCommitted={isCommitted}
chatIsRunning={chatIsRunning}
isLast={isLast}
/>
}
@ -1893,7 +1894,7 @@ export const SidebarChat = () => {
const previousMessagesHTML = useMemo(() => {
const threadId = currentThread.id
return previousMessages.map((message, i) => {
const isLast = i === numMessages - 1
const isLast = i === numMessages - 1 && (isRunning === 'tool' || isRunning === 'awaiting_user')
return <ChatBubble key={getChatBubbleId(currentThread.id, i)}
chatMessage={message}
messageIdx={i}
@ -1902,8 +1903,7 @@ export const SidebarChat = () => {
isLast={isLast}
threadId={threadId}
/>
}
)
})
}, [previousMessages, isRunning, currentThread, numMessages])
const threadId = currentThread.id

View file

@ -48,16 +48,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
// changes if the user clicks left/right or if the user goes on a uri with changes
const [currUriIdx, setUriIdx] = useState<number | null>(null)
const [currUriHasChanges, setCurrUriHasChanges] = useState(false)
useEffect(() => {
const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath)
if (i !== -1) {
setUriIdx(i)
setCurrUriHasChanges(true)
}
else {
setCurrUriHasChanges(false)
}
if (i !== -1) { setUriIdx(i) }
}, [sortedCommandBarURIs, uri])
const getNextDiffIdx = (step: 1 | -1) => {
@ -131,6 +124,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0
const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0
const streamState = uri ? commandBarService.getStreamState(uri) : null
const showAcceptRejectAll = streamState === 'idle-has-changes'
if (!isADiffZoneInAnyFile) { // no changes for the user to accept
return null
@ -303,7 +299,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
// dummy container due to annoyances with VS Code mounting the widget
return <div className='px-2 pt-1 pb-1 gap-1 pointer-events-auto flex flex-col items-start bg-void-bg-1 rounded shadow-md border border-void-border-1'>
{currUriHasChanges && <>
{showAcceptRejectAll && <>
<div className="flex gap-2">
{acceptAllButton}
{rejectAllButton}

View file

@ -9,7 +9,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { ResolveReason } from '../common/toolsServiceTypes.js';
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
import { MAX_TERMINAL_CHARS_PAGE, TERMINAL_BG_WAIT_TIME, TERMINAL_TIMEOUT_TIME } from './toolsService.js';
@ -18,7 +18,7 @@ export interface ITerminalToolService {
readonly _serviceBrand: undefined;
listTerminalIds(): string[];
runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: ResolveReason }>;
runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: TerminalResolveReason }>;
openTerminal(terminalId: string): Promise<void>
terminalExists(terminalId: string): boolean
}
@ -162,7 +162,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
await this.terminalService.focusActiveInstance()
let result: string = ''
let resolveReason: ResolveReason | undefined = undefined
let resolveReason: TerminalResolveReason | undefined = undefined
const disposables: IDisposable[] = []

View file

@ -20,7 +20,7 @@ import { basename } from '../../../../base/common/path.js'
type ValidateParams = { [T in ToolName]: (p: string) => Promise<ToolCallParams[T]> }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], cancel?: () => void }> }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string }
@ -357,9 +357,10 @@ export class ToolsService implements IToolsService {
if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`)
const [diffZoneURI, applyDonePromise] = res
const cancel = () => editCodeService.interruptURIStreaming({ uri: diffZoneURI })
return { result: applyDonePromise, cancel }
const interruptTool = () => { // must reject the applyPromiseDone promise
editCodeService.interruptURIStreaming({ uri: diffZoneURI })
}
return { result: applyDonePromise, interruptTool }
},
terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => {
const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
@ -393,6 +394,7 @@ export class ToolsService implements IToolsService {
return `URI ${params.uri.fsPath} successfully deleted.`
},
edit: (params, result) => {
console.log('STR OF RESULT', params)
return `Change successfully made to ${params.uri.fsPath}.`
},
terminal_command: (params, result) => {

View file

@ -10,17 +10,137 @@ import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThrea
import { ChatMode } from '../voidSettingsTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { InternalToolInfo } from '../toolsServiceTypes.js';
// this is just for ease of readability
export const tripleTick = ['```', '```']
export const editToolDesc_toolDescription = `\
Your description should be of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \
Do NOT re-write the whole file, and instead use comments like // ... existing code ... . Write as little as possible. \
Your description will be handed to a dumber, faster model that will quickly apply the change, so try to be brief, but also make sure to include enough information to accurately describe the change. \
Include the triple ticks in your output.`
const changesExampleContent = `\
// ... existing code ...
// {{change 1}}
// // ... existing code ...
// {{change 2}}
// // ... existing code ...
// {{change 3}}
// ... existing code ...`
const editToolDescription = `\
${tripleTick[0]}
${changesExampleContent}
${tripleTick[1]}`
const fileNameEdit = `${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
${changesExampleContent}
${tripleTick[1]}`
// ======================================================== tools ========================================================
const paginationHelper = {
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, }
} as const
export const voidTools = {
// --- context-gathering (read/search/list) ---
read_file: {
name: 'read_file',
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
params: {
uri: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
list_dir: {
name: 'list_dir',
description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`,
params: {
uri: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
pathname_search: {
name: 'pathname_search',
description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`,
params: {
query: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
text_search: {
name: 'text_search',
description: `Returns pathnames of files with an exact match of the query. The query can be any regex. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`,
params: {
query: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
// --- editing (create/delete) ---
create_uri: {
name: 'create_uri',
description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`,
params: {
uri: { type: 'string', description: undefined },
},
},
delete_uri: {
name: 'delete_uri',
description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
params: {
uri: { type: 'string', description: undefined },
params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' }
},
},
edit: { // APPLY TOOL
name: 'edit',
description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`,
params: {
uri: { type: 'string', description: undefined },
changeDescription: {
type: 'string', description: `\
- Your changeDescription should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.
- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
- You must output your description in triple backticks.
Here's an example of a good description:\n${editToolDescription}.`
}
},
},
terminal_command: {
name: 'terminal_command',
description: `Executes a terminal command.`,
params: {
command: { type: 'string', description: 'The terminal command to execute.' },
waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
},
},
// go_to_definition
// go_to_usages
} satisfies { [name: string]: InternalToolInfo }
// ======================================================== chat (normal, gather, agent) ========================================================
@ -32,17 +152,21 @@ ${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to
: ''}
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`.
Please assist the user with their query. The user's query is never invalid.
${/* system info */''}
The user's system information is as follows:
- ${os}
- Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'}
${(mode === 'agent') && runningTerminalIds.length !== 0 ? `\
- Existing terminal IDs: ${runningTerminalIds.join(', ')}
`: '\n'}
${mode === 'agent' || mode === 'gather' /* tool use */ ? `\
${/* tool use */ mode === 'agent' || mode === 'gather' ? `\
You will be given tools you can call.
${mode === 'agent' ? `\
- Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.
- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool.
- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool.`
: mode === 'gather' ? `\
- Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.`
: ''}
- If you think you should use tools, you do not need to ask for permission. Feel free to call tools whenever you'd like. You can use them to understand the codebase, ${mode === 'agent' ? 'run terminal commands, edit files, ' : 'gather relevant files and information, '}etc.
- NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results.
- Some tools only work if the user has a workspace open.${mode === 'agent' ? `
@ -52,20 +176,23 @@ You will be given tools you can call.
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.
\
`}
${mode === 'agent' /* code blocks */ ? `\
${/* code blocks */ mode === 'agent' ? `\
Behavior:
- Prioritize editing files and running commands over simply making suggestions.
- Always use tools (edit, terminal, etc) to take actions and implement changes. Don't just describe them.
- Prioritize taking as many steps as you need to complete your request over stopping early.\
`: `\
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks).
- The first line of the code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created.
- The remaining contents of the code block will be given to a dumber, faster model that will quickly apply the change.
- Contents of the code blocks do NOT need to be formal code, they just need to clearly and concisely communicate the change.
- Do NOT re-write the entire file in the code block(s). Instead, write comments like "// ... existing code" to indicate how to change the existing code.`}
- The first line of the code block must be the FULL PATH of the file you want to change.
- The remaining contents should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.
- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
Here's an example of a good code block:\n${fileNameEdit}.\
`}
${/* misc */''}
Misc:
- Do not make things up.
- Do not be lazy.
- NEVER re-write the entire file.
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\
`
// agent mode doesn't know about 1st line paths yet
@ -181,6 +308,7 @@ Directions:
// ======================================================== apply (writeover) ========================================================
export const rewriteCode_userMessage = ({ originalCode, applyStr, language }: { originalCode: string, applyStr: string, language: string }) => {
@ -202,69 +330,7 @@ Please finish writing the new file by applying the change to the original file.
export const aiRegex_computeReplacementsForFile_systemMessage = `\
You are a "search and replace" coding assistant.
You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE.
The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for.
The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace.
The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes.
## Instructions
1. If you do not want to make any changes, you should respond with the word "no".
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`
// export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, voidFileService: IVoidFileService }) => {
// // we may want to do this in batches
// const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } }
// const file = await stringifyFileSelections([fileSelection], voidFileService)
// return `\
// ## FILE
// ${file}
// ## SEARCH_CLAUSE
// Here is what the user is searching for:
// ${searchClause}
// ## REPLACE_CLAUSE
// Here is what the user wants to replace it with:
// ${replaceClause}
// ## INSTRUCTIONS
// Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.`
// }
// // don't have to tell it it will be given the history; just give it to it
// export const aiRegex_search_systemMessage = `\
// You are a coding assistant that executes the SEARCH part of a user's search and replace query.
// You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context.
// Output
// - Regex query
// - Files to Include (optional)
// - Files to Exclude? (optional)
// `
// ======================================================== apply (fast apply - search/replace) ========================================================
@ -387,6 +453,8 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF
}
// ======================================================== quick edit (ctrl+K) ========================================================
export type QuickEditFimTagsType = {
preTag: string,
sufTag: string,
@ -444,10 +512,82 @@ ${tripleTick[1]}).`
/*
// ======================================================== ai search/replace ========================================================
OLD CHAT EXAMPLES:
export const aiRegex_computeReplacementsForFile_systemMessage = `\
You are a "search and replace" coding assistant.
You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE.
The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for.
The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace.
The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes.
## Instructions
1. If you do not want to make any changes, you should respond with the word "no".
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`
// export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, voidFileService: IVoidFileService }) => {
// // we may want to do this in batches
// const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } }
// const file = await stringifyFileSelections([fileSelection], voidFileService)
// return `\
// ## FILE
// ${file}
// ## SEARCH_CLAUSE
// Here is what the user is searching for:
// ${searchClause}
// ## REPLACE_CLAUSE
// Here is what the user wants to replace it with:
// ${replaceClause}
// ## INSTRUCTIONS
// Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.`
// }
// // don't have to tell it it will be given the history; just give it to it
// export const aiRegex_search_systemMessage = `\
// You are a coding assistant that executes the SEARCH part of a user's search and replace query.
// You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context.
// Output
// - Regex query
// - Files to Include (optional)
// - Files to Exclude? (optional)
// `
// ======================================================== old examples ========================================================
Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below.

View file

@ -1,5 +1,16 @@
import { URI } from '../../../../base/common/uri.js'
import { editToolDesc_toolDescription } from './prompt/prompts.js';
import { voidTools } from './prompt/prompts.js';
export type ToolDirectoryItem = {
uri: URI;
name: string;
isDirectory: boolean;
isSymbolicLink: boolean;
}
export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number }
@ -15,109 +26,6 @@ export type InternalToolInfo = {
export type ToolDirectoryItem = {
uri: URI;
name: string;
isDirectory: boolean;
isSymbolicLink: boolean;
}
export type ResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number }
const paginationHelper = {
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, }
} as const
export const voidTools = {
// --- context-gathering (read/search/list) ---
read_file: {
name: 'read_file',
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
params: {
uri: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
list_dir: {
name: 'list_dir',
description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`,
params: {
uri: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
pathname_search: {
name: 'pathname_search',
description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`,
params: {
query: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
text_search: {
name: 'text_search',
description: `Returns pathnames of files with an exact match of the query. The query can be any regex. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`,
params: {
query: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
// --- editing (create/delete) ---
create_uri: {
name: 'create_uri',
description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`,
params: {
uri: { type: 'string', description: undefined },
},
},
delete_uri: {
name: 'delete_uri',
description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
params: {
uri: { type: 'string', description: undefined },
params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' }
},
},
edit: { // APPLY TOOL
name: 'edit',
description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`,
params: {
uri: { type: 'string', description: undefined },
changeDescription: { type: 'string', description: editToolDesc_toolDescription } // long description here
},
},
terminal_command: {
name: 'terminal_command',
description: `Executes a terminal command.`,
params: {
command: { type: 'string', description: 'The terminal command to execute.' },
waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
},
},
// go_to_definition
// go_to_usages
} satisfies { [name: string]: InternalToolInfo }
export type ToolName = keyof typeof voidTools
export const toolNames = Object.keys(voidTools) as ToolName[]
@ -151,9 +59,9 @@ export type ToolResultType = {
'pathname_search': { uris: URI[], hasNextPage: boolean },
'text_search': { uris: URI[], hasNextPage: boolean },
// ---
'edit': {},
'edit': Promise<void>,
'create_uri': {},
'delete_uri': {},
'terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: ResolveReason; },
'terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; },
}