Skip to content

Commit dfae091

Browse files
authored
agent files: add 'target' property' (microsoft#273066)
1 parent 92ff7ba commit dfae091

File tree

7 files changed

+170
-5
lines changed

7 files changed

+170
-5
lines changed

src/vs/workbench/contrib/chat/common/chatModes.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export class ChatModeService extends Disposable implements IChatModeService {
103103
argumentHint: cachedMode.argumentHint,
104104
agentInstructions: cachedMode.modeInstructions ?? { content: cachedMode.body ?? '', toolReferences: [] },
105105
handOffs: cachedMode.handOffs,
106+
target: cachedMode.target,
106107
source: reviveChatModeSource(cachedMode.source) ?? { storage: PromptsStorage.local }
107108
};
108109
const instance = new CustomChatMode(customChatMode);
@@ -210,6 +211,7 @@ export interface IChatModeData {
210211
readonly handOffs?: readonly IHandOff[];
211212
readonly uri?: URI;
212213
readonly source?: IChatModeSourceData;
214+
readonly target?: string;
213215
}
214216

215217
export interface IChatMode {
@@ -226,6 +228,7 @@ export interface IChatMode {
226228
readonly modeInstructions?: IObservable<IChatModeInstructions>;
227229
readonly uri?: IObservable<URI>;
228230
readonly source?: IAgentSource;
231+
readonly target?: IObservable<string | undefined>;
229232
}
230233

231234
export interface IVariableReference {
@@ -255,7 +258,8 @@ function isCachedChatModeData(data: unknown): data is IChatModeData {
255258
(mode.argumentHint === undefined || typeof mode.argumentHint === 'string') &&
256259
(mode.handOffs === undefined || Array.isArray(mode.handOffs)) &&
257260
(mode.uri === undefined || (typeof mode.uri === 'object' && mode.uri !== null)) &&
258-
(mode.source === undefined || isChatModeSourceData(mode.source));
261+
(mode.source === undefined || isChatModeSourceData(mode.source)) &&
262+
(mode.target === undefined || typeof mode.target === 'string');
259263
}
260264

261265
export class CustomChatMode implements IChatMode {
@@ -266,6 +270,7 @@ export class CustomChatMode implements IChatMode {
266270
private readonly _modelObservable: ISettableObservable<string | undefined>;
267271
private readonly _argumentHintObservable: ISettableObservable<string | undefined>;
268272
private readonly _handoffsObservable: ISettableObservable<readonly IHandOff[] | undefined>;
273+
private readonly _targetObservable: ISettableObservable<string | undefined>;
269274
private _source: IAgentSource;
270275

271276
public readonly id: string;
@@ -311,6 +316,10 @@ export class CustomChatMode implements IChatMode {
311316
return this._source;
312317
}
313318

319+
get target(): IObservable<string | undefined> {
320+
return this._targetObservable;
321+
}
322+
314323
public readonly kind = ChatModeKind.Agent;
315324

316325
constructor(
@@ -323,6 +332,7 @@ export class CustomChatMode implements IChatMode {
323332
this._modelObservable = observableValue('model', customChatMode.model);
324333
this._argumentHintObservable = observableValue('argumentHint', customChatMode.argumentHint);
325334
this._handoffsObservable = observableValue('handOffs', customChatMode.handOffs);
335+
this._targetObservable = observableValue('target', customChatMode.target);
326336
this._modeInstructions = observableValue('_modeInstructions', customChatMode.agentInstructions);
327337
this._uriObservable = observableValue('uri', customChatMode.uri);
328338
this._source = customChatMode.source;
@@ -339,6 +349,7 @@ export class CustomChatMode implements IChatMode {
339349
this._modelObservable.set(newData.model, tx);
340350
this._argumentHintObservable.set(newData.argumentHint, tx);
341351
this._handoffsObservable.set(newData.handOffs, tx);
352+
this._targetObservable.set(newData.target, tx);
342353
this._modeInstructions.set(newData.agentInstructions, tx);
343354
this._uriObservable.set(newData.uri, tx);
344355
this._source = newData.source;
@@ -357,7 +368,8 @@ export class CustomChatMode implements IChatMode {
357368
modeInstructions: this.modeInstructions.get(),
358369
uri: this.uri.get(),
359370
handOffs: this.handOffs.get(),
360-
source: serializeChatModeSource(this._source)
371+
source: serializeChatModeSource(this._source),
372+
target: this.target.get()
361373
};
362374
}
363375
}
@@ -421,6 +433,10 @@ export class BuiltinChatMode implements IChatMode {
421433
return this.kind;
422434
}
423435

436+
get target(): IObservable<string | undefined> {
437+
return observableValue('target', undefined);
438+
}
439+
424440
/**
425441
* Getters are not json-stringified
426442
*/

src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptHovers.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,10 @@ export class PromptHoverProvider implements HoverProvider {
9494
if (tools?.range.containsPosition(position)) {
9595
return this.getToolHover(tools, position, localize('promptHeader.agent.tools', 'The set of tools that the custom agent has access to.'));
9696
}
97+
const targetRange = header.getAttribute(PromptHeaderAttributes.target)?.range;
98+
if (targetRange?.containsPosition(position)) {
99+
return this.createHover(localize('promptHeader.agent.target', 'The target to which the header attributes like tools apply to.'), targetRange);
100+
}
97101
} else {
98102
const descriptionRange = header.getAttribute(PromptHeaderAttributes.description)?.range;
99103
if (descriptionRange?.containsPosition(position)) {

src/vs/workbench/contrib/chat/common/promptSyntax/languageProviders/promptValidator.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,7 @@ export class PromptValidator {
150150
break;
151151

152152
case PromptsType.agent:
153+
this.validateTarget(attributes, report);
153154
this.validateTools(attributes, ChatModeKind.Agent, report);
154155
this.validateModel(attributes, ChatModeKind.Agent, report);
155156
this.validateHandoffs(attributes, report);
@@ -391,12 +392,32 @@ export class PromptValidator {
391392
}
392393
}
393394
}
395+
396+
private validateTarget(attributes: IHeaderAttribute[], report: (markers: IMarkerData) => void): undefined {
397+
const attribute = attributes.find(attr => attr.key === PromptHeaderAttributes.target);
398+
if (!attribute) {
399+
return;
400+
}
401+
if (attribute.value.type !== 'string') {
402+
report(toMarker(localize('promptValidator.targetMustBeString', "The 'target' attribute must be a string."), attribute.value.range, MarkerSeverity.Error));
403+
return;
404+
}
405+
const targetValue = attribute.value.value.trim();
406+
if (targetValue.length === 0) {
407+
report(toMarker(localize('promptValidator.targetMustBeNonEmpty', "The 'target' attribute must be a non-empty string."), attribute.value.range, MarkerSeverity.Error));
408+
return;
409+
}
410+
const validTargets = ['github-copilot', 'vscode'];
411+
if (!validTargets.includes(targetValue)) {
412+
report(toMarker(localize('promptValidator.targetInvalidValue', "The 'target' attribute must be one of: {0}.", validTargets.join(', ')), attribute.value.range, MarkerSeverity.Error));
413+
}
414+
}
394415
}
395416

396417
const allAttributeNames = {
397418
[PromptsType.prompt]: [PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.mode, PromptHeaderAttributes.agent, PromptHeaderAttributes.argumentHint],
398419
[PromptsType.instructions]: [PromptHeaderAttributes.description, PromptHeaderAttributes.applyTo, PromptHeaderAttributes.excludeAgent],
399-
[PromptsType.agent]: [PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint]
420+
[PromptsType.agent]: [PromptHeaderAttributes.description, PromptHeaderAttributes.model, PromptHeaderAttributes.tools, PromptHeaderAttributes.advancedOptions, PromptHeaderAttributes.handOffs, PromptHeaderAttributes.argumentHint, PromptHeaderAttributes.target]
400421
};
401422
const recommendedAttributeNames = {
402423
[PromptsType.prompt]: allAttributeNames[PromptsType.prompt].filter(name => !isNonRecommendedAttribute(name)),

src/vs/workbench/contrib/chat/common/promptSyntax/promptFileParser.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export namespace PromptHeaderAttributes {
7272
export const advancedOptions = 'advancedOptions';
7373
export const argumentHint = 'argument-hint';
7474
export const excludeAgent = 'excludeAgent';
75+
export const target = 'target';
7576
}
7677

7778
export class PromptHeader {
@@ -168,6 +169,10 @@ export class PromptHeader {
168169
return this.getStringAttribute(PromptHeaderAttributes.argumentHint);
169170
}
170171

172+
public get target(): string | undefined {
173+
return this.getStringAttribute(PromptHeaderAttributes.target);
174+
}
175+
171176
public get tools(): string[] | undefined {
172177
const toolsAttribute = this._parsedHeader.attributes.find(attr => attr.key === PromptHeaderAttributes.tools);
173178
if (!toolsAttribute) {

src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsService.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ export interface ICustomAgent {
112112
*/
113113
readonly argumentHint?: string;
114114

115+
/**
116+
* Target metadata in the prompt header.
117+
*/
118+
readonly target?: string;
119+
115120
/**
116121
* Contents of the custom agent file body and other agent instructions.
117122
*/

src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,8 +345,8 @@ export class PromptsService extends Disposable implements IPromptsService {
345345
if (!ast.header) {
346346
return { uri, name, agentInstructions, source };
347347
}
348-
const { description, model, tools, handOffs, argumentHint } = ast.header;
349-
return { uri, name, description, model, tools, handOffs, argumentHint, agentInstructions, source };
348+
const { description, model, tools, handOffs, argumentHint, target } = ast.header;
349+
return { uri, name, description, model, tools, handOffs, argumentHint, target, agentInstructions, source };
350350
})
351351
);
352352
return customAgents;

src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,7 @@ suite('PromptsService', () => {
787787
model: undefined,
788788
argumentHint: undefined,
789789
tools: undefined,
790+
target: undefined,
790791
uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'),
791792
source: { storage: PromptsStorage.local }
792793
},
@@ -850,6 +851,7 @@ suite('PromptsService', () => {
850851
handOffs: undefined,
851852
model: undefined,
852853
argumentHint: undefined,
854+
target: undefined,
853855
uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'),
854856
source: { storage: PromptsStorage.local },
855857
},
@@ -930,6 +932,7 @@ suite('PromptsService', () => {
930932
},
931933
handOffs: undefined,
932934
model: undefined,
935+
target: undefined,
933936
uri: URI.joinPath(rootFolderUri, '.github/agents/agent1.agent.md'),
934937
source: { storage: PromptsStorage.local }
935938
},
@@ -945,6 +948,7 @@ suite('PromptsService', () => {
945948
handOffs: undefined,
946949
model: undefined,
947950
tools: undefined,
951+
target: undefined,
948952
uri: URI.joinPath(rootFolderUri, '.github/agents/agent2.agent.md'),
949953
source: { storage: PromptsStorage.local }
950954
},
@@ -956,6 +960,116 @@ suite('PromptsService', () => {
956960
'Must get custom agents with argumentHint.',
957961
);
958962
});
963+
964+
test('header with target', async () => {
965+
const rootFolderName = 'custom-agents-with-target';
966+
const rootFolder = `/${rootFolderName}`;
967+
const rootFolderUri = URI.file(rootFolder);
968+
969+
workspaceContextService.setWorkspace(testWorkspace(rootFolderUri));
970+
971+
await (instaService.createInstance(MockFilesystem,
972+
[{
973+
name: rootFolderName,
974+
children: [
975+
{
976+
name: '.github/agents',
977+
children: [
978+
{
979+
name: 'github-agent.agent.md',
980+
contents: [
981+
'---',
982+
'description: \'GitHub Copilot specialized agent.\'',
983+
'target: \'github-copilot\'',
984+
'tools: [ github-api, code-search ]',
985+
'---',
986+
'I am optimized for GitHub Copilot workflows.',
987+
],
988+
},
989+
{
990+
name: 'vscode-agent.agent.md',
991+
contents: [
992+
'---',
993+
'description: \'VS Code specialized agent.\'',
994+
'target: \'vscode\'',
995+
'model: \'gpt-4\'',
996+
'---',
997+
'I am specialized for VS Code editor tasks.',
998+
],
999+
},
1000+
{
1001+
name: 'generic-agent.agent.md',
1002+
contents: [
1003+
'---',
1004+
'description: \'Generic agent without target.\'',
1005+
'---',
1006+
'I work everywhere.',
1007+
],
1008+
}
1009+
],
1010+
1011+
},
1012+
],
1013+
}])).mock();
1014+
1015+
const result = (await service.getCustomAgents(CancellationToken.None)).map(agent => ({ ...agent, uri: URI.from(agent.uri) }));
1016+
const expected: ICustomAgent[] = [
1017+
{
1018+
name: 'github-agent',
1019+
description: 'GitHub Copilot specialized agent.',
1020+
target: 'github-copilot',
1021+
tools: ['github-api', 'code-search'],
1022+
agentInstructions: {
1023+
content: 'I am optimized for GitHub Copilot workflows.',
1024+
toolReferences: [],
1025+
metadata: undefined
1026+
},
1027+
handOffs: undefined,
1028+
model: undefined,
1029+
argumentHint: undefined,
1030+
uri: URI.joinPath(rootFolderUri, '.github/agents/github-agent.agent.md'),
1031+
source: { storage: PromptsStorage.local }
1032+
},
1033+
{
1034+
name: 'vscode-agent',
1035+
description: 'VS Code specialized agent.',
1036+
target: 'vscode',
1037+
model: 'gpt-4',
1038+
agentInstructions: {
1039+
content: 'I am specialized for VS Code editor tasks.',
1040+
toolReferences: [],
1041+
metadata: undefined
1042+
},
1043+
handOffs: undefined,
1044+
argumentHint: undefined,
1045+
tools: undefined,
1046+
uri: URI.joinPath(rootFolderUri, '.github/agents/vscode-agent.agent.md'),
1047+
source: { storage: PromptsStorage.local }
1048+
},
1049+
{
1050+
name: 'generic-agent',
1051+
description: 'Generic agent without target.',
1052+
agentInstructions: {
1053+
content: 'I work everywhere.',
1054+
toolReferences: [],
1055+
metadata: undefined
1056+
},
1057+
handOffs: undefined,
1058+
model: undefined,
1059+
argumentHint: undefined,
1060+
tools: undefined,
1061+
target: undefined,
1062+
uri: URI.joinPath(rootFolderUri, '.github/agents/generic-agent.agent.md'),
1063+
source: { storage: PromptsStorage.local }
1064+
},
1065+
];
1066+
1067+
assert.deepEqual(
1068+
result,
1069+
expected,
1070+
'Must get custom agents with target attribute.',
1071+
);
1072+
});
9591073
});
9601074

9611075
suite('listPromptFiles - extensions', () => {

0 commit comments

Comments
 (0)