Fix localized tool markdownDescriptions (#277589)

* Fix localized tool markdownDescriptions
And add a lint rule

* Just keep this the same

* Fixes
This commit is contained in:
Rob Lourens
2025-11-15 13:12:37 -08:00
committed by GitHub
parent b327ee766f
commit 676ae78fa5
8 changed files with 135 additions and 7 deletions

View File

@@ -0,0 +1,128 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { TSESTree } from '@typescript-eslint/utils';
import * as eslint from 'eslint';
import * as visitorKeys from 'eslint-visitor-keys';
import type * as ESTree from 'estree';
const MESSAGE_ID = 'noLocalizedModelDescription';
type NodeWithChildren = TSESTree.Node & {
[key: string]: TSESTree.Node | TSESTree.Node[] | null | undefined;
};
type PropertyKeyNode = TSESTree.Property['key'] | TSESTree.MemberExpression['property'];
type AssignmentTarget = TSESTree.AssignmentExpression['left'];
export default new class NoLocalizedModelDescriptionRule implements eslint.Rule.RuleModule {
meta: eslint.Rule.RuleMetaData = {
messages: {
[MESSAGE_ID]: 'modelDescription values describe behavior to the language model and must not use localized strings.'
},
type: 'problem',
schema: false
};
create(context: eslint.Rule.RuleContext): eslint.Rule.RuleListener {
const reportIfLocalized = (expression: TSESTree.Expression | null | undefined) => {
if (expression && containsLocalizedCall(expression)) {
context.report({ node: expression, messageId: MESSAGE_ID });
}
};
return {
Property: (node: ESTree.Property) => {
const propertyNode = node as TSESTree.Property;
if (!isModelDescriptionKey(propertyNode.key, propertyNode.computed)) {
return;
}
reportIfLocalized(propertyNode.value as TSESTree.Expression);
},
AssignmentExpression: (node: ESTree.AssignmentExpression) => {
const assignment = node as TSESTree.AssignmentExpression;
if (!isModelDescriptionAssignmentTarget(assignment.left)) {
return;
}
reportIfLocalized(assignment.right);
}
};
}
};
function isModelDescriptionKey(key: PropertyKeyNode, computed: boolean | undefined): boolean {
if (!computed && key.type === 'Identifier') {
return key.name === 'modelDescription';
}
if (key.type === 'Literal' && key.value === 'modelDescription') {
return true;
}
return false;
}
function isModelDescriptionAssignmentTarget(target: AssignmentTarget): target is TSESTree.MemberExpression {
if (target.type === 'MemberExpression') {
return isModelDescriptionKey(target.property, target.computed);
}
return false;
}
function containsLocalizedCall(expression: TSESTree.Expression): boolean {
let found = false;
const visit = (node: TSESTree.Node) => {
if (found) {
return;
}
if (isLocalizeCall(node)) {
found = true;
return;
}
for (const key of visitorKeys.KEYS[node.type] ?? []) {
const value = (node as NodeWithChildren)[key];
if (Array.isArray(value)) {
for (const child of value) {
if (child) {
visit(child);
if (found) {
return;
}
}
}
} else if (value) {
visit(value);
}
}
};
visit(expression);
return found;
}
function isLocalizeCall(node: TSESTree.Node): boolean {
if (node.type === 'CallExpression') {
return isLocalizeCallee(node.callee);
}
if (node.type === 'ChainExpression') {
return isLocalizeCall(node.expression);
}
return false;
}
function isLocalizeCallee(callee: TSESTree.CallExpression['callee']): boolean {
if (callee.type === 'Identifier') {
return callee.name === 'localize';
}
if (callee.type === 'MemberExpression') {
if (!callee.computed && callee.property.type === 'Identifier') {
return callee.property.name === 'localize';
}
if (callee.property.type === 'Literal' && callee.property.value === 'localize') {
return true;
}
}
return false;
}

View File

@@ -89,6 +89,7 @@ export default tseslint.config(
'local/code-declare-service-brand': 'warn', 'local/code-declare-service-brand': 'warn',
'local/code-no-reader-after-await': 'warn', 'local/code-no-reader-after-await': 'warn',
'local/code-no-observable-get-in-reactive-context': 'warn', 'local/code-no-observable-get-in-reactive-context': 'warn',
'local/code-no-localized-model-description': 'warn',
'local/code-policy-localization-key-match': 'warn', 'local/code-policy-localization-key-match': 'warn',
'local/code-no-localization-template-literals': 'error', 'local/code-no-localization-template-literals': 'error',
'local/code-no-deep-import-of-internal': ['error', { '.*Internal': true, 'searchExtTypesInternal': false }], 'local/code-no-deep-import-of-internal': ['error', { '.*Internal': true, 'searchExtTypesInternal': false }],

View File

@@ -178,7 +178,7 @@ class SetupAgent extends Disposable implements IChatAgentImplementation {
source: ToolDataSource.Internal, source: ToolDataSource.Internal,
icon: Codicon.newFolder, icon: Codicon.newFolder,
displayName: localize('setupToolDisplayName', "New Workspace"), displayName: localize('setupToolDisplayName', "New Workspace"),
modelDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"), modelDescription: 'Scaffold a new workspace in VS Code',
userDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"), userDescription: localize('setupToolsDescription', "Scaffold a new workspace in VS Code"),
canBeReferencedInPrompt: true, canBeReferencedInPrompt: true,
toolReferenceName: 'new', toolReferenceName: 'new',

View File

@@ -86,6 +86,7 @@ const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.r
description: localize('toolUserDescription', "A description of this tool that may be shown to the user."), description: localize('toolUserDescription', "A description of this tool that may be shown to the user."),
type: 'string' type: 'string'
}, },
// eslint-disable-next-line local/code-no-localized-model-description
modelDescription: { modelDescription: {
description: localize('toolModelDescription', "A description of this tool that may be used by a language model to select it."), description: localize('toolModelDescription', "A description of this tool that may be used by a language model to select it."),
type: 'string' type: 'string'

View File

@@ -25,7 +25,7 @@ export const FetchWebPageToolData: IToolData = {
id: InternalFetchWebPageToolId, id: InternalFetchWebPageToolId,
displayName: 'Fetch Web Page', displayName: 'Fetch Web Page',
canBeReferencedInPrompt: false, canBeReferencedInPrompt: false,
modelDescription: localize('fetchWebPage.modelDescription', 'Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.'), modelDescription: 'Fetches the main content from a web page. This tool is useful for summarizing or analyzing the content of a webpage.',
source: ToolDataSource.Internal, source: ToolDataSource.Internal,
canRequestPostApproval: true, canRequestPostApproval: true,
canRequestPreApproval: true, canRequestPreApproval: true,

View File

@@ -17,7 +17,7 @@ export const InstallExtensionsToolData: IToolData = {
toolReferenceName: 'installExtensions', toolReferenceName: 'installExtensions',
canBeReferencedInPrompt: true, canBeReferencedInPrompt: true,
displayName: localize('installExtensionsTool.displayName', 'Install Extensions'), displayName: localize('installExtensionsTool.displayName', 'Install Extensions'),
modelDescription: localize('installExtensionsTool.modelDescription', "This is a tool for installing extensions in Visual Studio Code. You should provide the list of extension ids to install. The identifier of an extension is '\${ publisher }.\${ name }' for example: 'vscode.csharp'."), modelDescription: 'This is a tool for installing extensions in Visual Studio Code. You should provide the list of extension ids to install. The identifier of an extension is \'\${ publisher }.\${ name }\' for example: \'vscode.csharp\'.',
userDescription: localize('installExtensionsTool.userDescription', 'Tool for installing extensions'), userDescription: localize('installExtensionsTool.userDescription', 'Tool for installing extensions'),
source: ToolDataSource.Internal, source: ToolDataSource.Internal,
inputSchema: { inputSchema: {

View File

@@ -20,7 +20,7 @@ export const SearchExtensionsToolData: IToolData = {
canBeReferencedInPrompt: true, canBeReferencedInPrompt: true,
icon: ThemeIcon.fromId(Codicon.extensions.id), icon: ThemeIcon.fromId(Codicon.extensions.id),
displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'), displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'),
modelDescription: localize('searchExtensionsTool.modelDescription', "This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended."), modelDescription: 'This is a tool for browsing Visual Studio Code Extensions Marketplace. It allows the model to search for extensions and retrieve detailed information about them. The model should use this tool whenever it needs to discover extensions or resolve information about known ones. To use the tool, the model has to provide the category of the extensions, relevant search keywords, or known extension IDs. Note that search results may include false positives, so reviewing and filtering is recommended.',
userDescription: localize('searchExtensionsTool.userDescription', 'Search for VS Code extensions'), userDescription: localize('searchExtensionsTool.userDescription', 'Search for VS Code extensions'),
source: ToolDataSource.Internal, source: ToolDataSource.Internal,
inputSchema: { inputSchema: {

View File

@@ -194,7 +194,7 @@ export const CreateAndRunTaskToolData: IToolData = {
id: 'create_and_run_task', id: 'create_and_run_task',
toolReferenceName: 'createAndRunTask', toolReferenceName: 'createAndRunTask',
displayName: localize('createAndRunTask.displayName', 'Create and run Task'), displayName: localize('createAndRunTask.displayName', 'Create and run Task'),
modelDescription: localize('createAndRunTask.modelDescription', 'Creates and runs a build, run, or custom task for the workspace by generating or adding to a tasks.json file based on the project structure (such as package.json or README.md). If the user asks to build, run, launch and they have no tasks.json file, use this tool. If they ask to create or add a task, use this tool.'), modelDescription: 'Creates and runs a build, run, or custom task for the workspace by generating or adding to a tasks.json file based on the project structure (such as package.json or README.md). If the user asks to build, run, launch and they have no tasks.json file, use this tool. If they ask to create or add a task, use this tool.',
userDescription: localize('createAndRunTask.userDescription', "Create and run a task in the workspace"), userDescription: localize('createAndRunTask.userDescription', "Create and run a task in the workspace"),
source: ToolDataSource.Internal, source: ToolDataSource.Internal,
inputSchema: { inputSchema: {
@@ -259,5 +259,3 @@ export const CreateAndRunTaskToolData: IToolData = {
] ]
}, },
}; };