diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 4b3b29a0ad0..fe32f172a41 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -16,7 +16,7 @@ import { IModelService } from '../../../../../../editor/common/services/model.js import { localize } from '../../../../../../nls.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; import { IExtensionDescription } from '../../../../../../platform/extensions/common/extensions.js'; -import { IFileService } from '../../../../../../platform/files/common/files.js'; +import { FileOperationError, FileOperationResult, IFileService } from '../../../../../../platform/files/common/files.js'; import { IExtensionService } from '../../../../../services/extensions/common/extensions.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; import { ILabelService } from '../../../../../../platform/label/common/label.js'; @@ -387,7 +387,7 @@ export class PromptsService extends Disposable implements IPromptsService { let agentFiles = await this.listPromptFiles(PromptsType.agent, token); const disabledAgents = this.getDisabledPromptFiles(PromptsType.agent); agentFiles = agentFiles.filter(promptPath => !disabledAgents.has(promptPath.uri)); - const customAgents = await Promise.all( + const customAgentsResults = await Promise.allSettled( agentFiles.map(async (promptPath): Promise => { const uri = promptPath.uri; const ast = await this.parseNew(uri, token); @@ -432,6 +432,23 @@ export class PromptsService extends Disposable implements IPromptsService { return { uri, name, description, model, tools, handOffs, argumentHint, target, infer, agentInstructions, source }; }) ); + + const customAgents: ICustomAgent[] = []; + for (let i = 0; i < customAgentsResults.length; i++) { + const result = customAgentsResults[i]; + if (result.status === 'fulfilled') { + customAgents.push(result.value); + } else { + const uri = agentFiles[i].uri; + const error = result.reason; + if (error instanceof FileOperationError && error.fileOperationResult === FileOperationResult.FILE_NOT_FOUND) { + this.logger.warn(`[computeCustomAgents] Skipping agent file that does not exist: ${uri}`, error.message); + } else { + this.logger.error(`[computeCustomAgents] Failed to parse agent file: ${uri}`, error); + } + } + } + return customAgents; } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 72a07f1235a..8c00fba93f6 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -1202,6 +1202,57 @@ suite('PromptsService', () => { registered.dispose(); }); + + test('Contributed agent file that does not exist should not crash', async () => { + const nonExistentUri = URI.parse('file://extensions/my-extension/nonexistent.agent.md'); + const existingUri = URI.parse('file://extensions/my-extension/existing.agent.md'); + const extension = { + identifier: { value: 'test.my-extension' } + } as unknown as IExtensionDescription; + + // Only create the existing file + await mockFiles(fileService, [ + { + path: existingUri.path, + contents: [ + '---', + 'name: \'Existing Agent\'', + 'description: \'An agent that exists\'', + '---', + 'I am an existing agent.', + ] + } + ]); + + // Register both agents (one exists, one doesn't) + const registered1 = service.registerContributedFile( + PromptsType.agent, + 'NonExistent Agent', + 'An agent that does not exist', + nonExistentUri, + extension + ); + + const registered2 = service.registerContributedFile( + PromptsType.agent, + 'Existing Agent', + 'An agent that exists', + existingUri, + extension + ); + + // Verify that getCustomAgents doesn't crash and returns only the valid agent + const agents = await service.getCustomAgents(CancellationToken.None); + + // Should only get the existing agent, not the non-existent one + assert.strictEqual(agents.length, 1, 'Should only return the agent that exists'); + assert.strictEqual(agents[0].name, 'Existing Agent'); + assert.strictEqual(agents[0].description, 'An agent that exists'); + assert.strictEqual(agents[0].uri.toString(), existingUri.toString()); + + registered1.dispose(); + registered2.dispose(); + }); }); suite('findClaudeSkills', () => {