@theia/ai-core 1.73.0-next.7 → 1.73.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/lib/browser/{agent-completion-notification-service.d.ts → agent-notification-service.d.ts} +12 -10
- package/lib/browser/agent-notification-service.d.ts.map +1 -0
- package/lib/browser/{agent-completion-notification-service.js → agent-notification-service.js} +37 -27
- package/lib/browser/agent-notification-service.js.map +1 -0
- package/lib/browser/ai-core-frontend-module.js +2 -2
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/frontend-language-model-alias-registry.js +3 -3
- package/lib/browser/frontend-prompt-customization-service.d.ts +193 -16
- package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.js +686 -46
- package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.spec.js +259 -0
- package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -1
- package/lib/browser/frontend-variable-service.d.ts +2 -1
- package/lib/browser/frontend-variable-service.d.ts.map +1 -1
- package/lib/browser/frontend-variable-service.js +6 -1
- package/lib/browser/frontend-variable-service.js.map +1 -1
- package/lib/browser/index.d.ts +1 -1
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -1
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/os-notification-service.d.ts +10 -6
- package/lib/browser/os-notification-service.d.ts.map +1 -1
- package/lib/browser/os-notification-service.js +34 -20
- package/lib/browser/os-notification-service.js.map +1 -1
- package/lib/browser/prompt-variable-contribution.spec.js +2 -1
- package/lib/browser/prompt-variable-contribution.spec.js.map +1 -1
- package/lib/browser/skill-service.d.ts +6 -1
- package/lib/browser/skill-service.d.ts.map +1 -1
- package/lib/browser/skill-service.js +47 -18
- package/lib/browser/skill-service.js.map +1 -1
- package/lib/browser/skill-service.spec.js +153 -31
- package/lib/browser/skill-service.spec.js.map +1 -1
- package/lib/browser/token-usage-frontend-service-impl.d.ts +2 -0
- package/lib/browser/token-usage-frontend-service-impl.d.ts.map +1 -1
- package/lib/browser/token-usage-frontend-service-impl.js +6 -1
- package/lib/browser/token-usage-frontend-service-impl.js.map +1 -1
- package/lib/browser/window-blink-service.d.ts +2 -0
- package/lib/browser/window-blink-service.d.ts.map +1 -1
- package/lib/browser/window-blink-service.js +8 -3
- package/lib/browser/window-blink-service.js.map +1 -1
- package/lib/common/agent-preferences.d.ts.map +1 -1
- package/lib/common/agent-preferences.js +12 -2
- package/lib/common/agent-preferences.js.map +1 -1
- package/lib/common/ai-core-preferences.d.ts.map +1 -1
- package/lib/common/ai-core-preferences.js +4 -2
- package/lib/common/ai-core-preferences.js.map +1 -1
- package/lib/common/capability-variable-contribution.spec.js +2 -1
- package/lib/common/capability-variable-contribution.spec.js.map +1 -1
- package/lib/common/frontmatter.d.ts +33 -0
- package/lib/common/frontmatter.d.ts.map +1 -0
- package/lib/common/frontmatter.js +80 -0
- package/lib/common/frontmatter.js.map +1 -0
- package/lib/common/frontmatter.spec.d.ts +2 -0
- package/lib/common/frontmatter.spec.d.ts.map +1 -0
- package/lib/common/frontmatter.spec.js +73 -0
- package/lib/common/frontmatter.spec.js.map +1 -0
- package/lib/common/index.d.ts +1 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +1 -0
- package/lib/common/index.js.map +1 -1
- package/lib/common/language-model-service.js +1 -1
- package/lib/common/language-model-service.js.map +1 -1
- package/lib/common/language-model-service.spec.d.ts +2 -0
- package/lib/common/language-model-service.spec.d.ts.map +1 -0
- package/lib/common/language-model-service.spec.js +62 -0
- package/lib/common/language-model-service.spec.js.map +1 -0
- package/lib/common/language-model.d.ts +64 -2
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +8 -2
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/notification-types.d.ts +7 -0
- package/lib/common/notification-types.d.ts.map +1 -1
- package/lib/common/notification-types.js +7 -1
- package/lib/common/notification-types.js.map +1 -1
- package/lib/common/prompt-service-util.d.ts +22 -1
- package/lib/common/prompt-service-util.d.ts.map +1 -1
- package/lib/common/prompt-service-util.js +24 -2
- package/lib/common/prompt-service-util.js.map +1 -1
- package/lib/common/prompt-service-util.spec.d.ts +2 -0
- package/lib/common/prompt-service-util.spec.d.ts.map +1 -0
- package/lib/common/prompt-service-util.spec.js +54 -0
- package/lib/common/prompt-service-util.spec.js.map +1 -0
- package/lib/common/prompt-service.d.ts +72 -8
- package/lib/common/prompt-service.d.ts.map +1 -1
- package/lib/common/prompt-service.js +36 -4
- package/lib/common/prompt-service.js.map +1 -1
- package/lib/common/prompt-service.spec.js +49 -2
- package/lib/common/prompt-service.spec.js.map +1 -1
- package/lib/common/settings-service.d.ts +5 -0
- package/lib/common/settings-service.d.ts.map +1 -1
- package/lib/common/skill.d.ts +17 -4
- package/lib/common/skill.d.ts.map +1 -1
- package/lib/common/skill.js +22 -35
- package/lib/common/skill.js.map +1 -1
- package/lib/common/tool-invocation-registry.d.ts +7 -0
- package/lib/common/tool-invocation-registry.d.ts.map +1 -1
- package/lib/common/tool-invocation-registry.js +5 -0
- package/lib/common/tool-invocation-registry.js.map +1 -1
- package/lib/common/variable-service.d.ts +2 -2
- package/lib/common/variable-service.d.ts.map +1 -1
- package/lib/common/variable-service.js +7 -4
- package/lib/common/variable-service.js.map +1 -1
- package/lib/common/variable-service.spec.js +2 -1
- package/lib/common/variable-service.spec.js.map +1 -1
- package/lib/node/backend-language-model-registry.d.ts.map +1 -1
- package/lib/node/backend-language-model-registry.js +1 -0
- package/lib/node/backend-language-model-registry.js.map +1 -1
- package/lib/node/backend-language-model-registry.spec.d.ts +2 -0
- package/lib/node/backend-language-model-registry.spec.d.ts.map +1 -0
- package/lib/node/backend-language-model-registry.spec.js +48 -0
- package/lib/node/backend-language-model-registry.spec.js.map +1 -0
- package/lib/node/token-usage-service-impl.d.ts +2 -0
- package/lib/node/token-usage-service-impl.d.ts.map +1 -1
- package/lib/node/token-usage-service-impl.js +7 -1
- package/lib/node/token-usage-service-impl.js.map +1 -1
- package/package.json +12 -12
- package/src/browser/{agent-completion-notification-service.ts → agent-notification-service.ts} +52 -27
- package/src/browser/ai-core-frontend-module.ts +2 -2
- package/src/browser/frontend-language-model-alias-registry.ts +3 -3
- package/src/browser/frontend-prompt-customization-service.spec.ts +319 -1
- package/src/browser/frontend-prompt-customization-service.ts +810 -59
- package/src/browser/frontend-variable-service.ts +5 -3
- package/src/browser/index.ts +1 -1
- package/src/browser/os-notification-service.ts +39 -24
- package/src/browser/prompt-variable-contribution.spec.ts +2 -1
- package/src/browser/skill-service.spec.ts +191 -38
- package/src/browser/skill-service.ts +61 -33
- package/src/browser/token-usage-frontend-service-impl.ts +6 -3
- package/src/browser/window-blink-service.ts +7 -4
- package/src/common/agent-preferences.ts +13 -2
- package/src/common/ai-core-preferences.ts +4 -2
- package/src/common/capability-variable-contribution.spec.ts +2 -1
- package/src/common/frontmatter.spec.ts +82 -0
- package/src/common/frontmatter.ts +99 -0
- package/src/common/index.ts +1 -0
- package/src/common/language-model-service.spec.ts +70 -0
- package/src/common/language-model-service.ts +1 -1
- package/src/common/language-model.ts +72 -3
- package/src/common/notification-types.ts +10 -0
- package/src/common/prompt-service-util.spec.ts +60 -0
- package/src/common/prompt-service-util.ts +31 -1
- package/src/common/prompt-service.spec.ts +59 -2
- package/src/common/prompt-service.ts +104 -8
- package/src/common/settings-service.ts +5 -0
- package/src/common/skill.ts +37 -39
- package/src/common/tool-invocation-registry.ts +13 -0
- package/src/common/variable-service.spec.ts +2 -2
- package/src/common/variable-service.ts +3 -1
- package/src/node/backend-language-model-registry.spec.ts +52 -0
- package/src/node/backend-language-model-registry.ts +1 -0
- package/src/node/token-usage-service-impl.ts +7 -2
- package/lib/browser/agent-completion-notification-service.d.ts.map +0 -1
- package/lib/browser/agent-completion-notification-service.js.map +0 -1
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
16
|
// *****************************************************************************
|
|
17
17
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
-
exports.DefaultPromptFragmentCustomizationService = exports.CustomizationSource = void 0;
|
|
18
|
+
exports.DefaultPromptFragmentCustomizationService = exports.CustomizationSource = exports.CUSTOM_AGENT_WORKSPACE_DIRECTORIES = exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM = exports.CUSTOM_AGENT_FILE_NAME = exports.CUSTOM_AGENTS_DIRECTORY = void 0;
|
|
19
19
|
exports.getCustomizationSourceString = getCustomizationSourceString;
|
|
20
20
|
const tslib_1 = require("tslib");
|
|
21
21
|
const core_1 = require("@theia/core");
|
|
@@ -23,6 +23,7 @@ const browser_1 = require("@theia/core/lib/browser");
|
|
|
23
23
|
const inversify_1 = require("@theia/core/shared/inversify");
|
|
24
24
|
const common_1 = require("../common");
|
|
25
25
|
const configurable_in_memory_resources_1 = require("../common/configurable-in-memory-resources");
|
|
26
|
+
const frontmatter_1 = require("../common/frontmatter");
|
|
26
27
|
const buffer_1 = require("@theia/core/lib/common/buffer");
|
|
27
28
|
const file_service_1 = require("@theia/filesystem/lib/browser/file-service");
|
|
28
29
|
const ai_core_preferences_1 = require("../common/ai-core-preferences");
|
|
@@ -31,6 +32,51 @@ const js_yaml_1 = require("js-yaml");
|
|
|
31
32
|
const prompttemplate_contribution_1 = require("./prompttemplate-contribution");
|
|
32
33
|
const prompttemplate_parser_1 = require("./prompttemplate-parser");
|
|
33
34
|
const browser_2 = require("@theia/workspace/lib/browser");
|
|
35
|
+
/**
|
|
36
|
+
* Subdirectory (relative to a prompt-templates scope) holding one folder per custom agent.
|
|
37
|
+
*/
|
|
38
|
+
exports.CUSTOM_AGENTS_DIRECTORY = 'agents';
|
|
39
|
+
/**
|
|
40
|
+
* Filename of the per-agent definition file (frontmatter + prompt body) inside `agents/<id>/`.
|
|
41
|
+
*/
|
|
42
|
+
exports.CUSTOM_AGENT_FILE_NAME = 'agent.md';
|
|
43
|
+
/**
|
|
44
|
+
* Filename stem (without extension) reserved for the customization of a custom agent's
|
|
45
|
+
* default prompt. Lives at `<scope>/agents/<id>/prompt.prompttemplate` and maps to
|
|
46
|
+
* fragment id `<agent-name>_prompt`. Excluded from the variant loader so it doesn't double
|
|
47
|
+
* as an extra variant.
|
|
48
|
+
*/
|
|
49
|
+
exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM = 'prompt';
|
|
50
|
+
/**
|
|
51
|
+
* Workspace-relative parent folders scanned for custom agents, independent of the configurable
|
|
52
|
+
* prompt-templates directories. Scanning both folders mirrors the skills convention introduced
|
|
53
|
+
* in #17553, but the duplicate-id precedence is intentionally inverted: here `.agents` is listed
|
|
54
|
+
* first, so it becomes the default location for newly created agents and wins over `.prompts`,
|
|
55
|
+
* whereas for skills `.prompts` wins over `.agents` (see `combineSkillDirectories` in
|
|
56
|
+
* `skill-service.ts`). `.prompts` is retained for backward compatibility with agents authored
|
|
57
|
+
* before the move to `.agents`.
|
|
58
|
+
*/
|
|
59
|
+
exports.CUSTOM_AGENT_WORKSPACE_DIRECTORIES = ['.agents', '.prompts'];
|
|
60
|
+
var CustomAgentFrontmatter;
|
|
61
|
+
(function (CustomAgentFrontmatter) {
|
|
62
|
+
function is(value) {
|
|
63
|
+
if (!value || typeof value !== 'object') {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
const entry = value;
|
|
67
|
+
if (typeof entry.name !== 'string' || typeof entry.description !== 'string' || typeof entry.defaultLLM !== 'string') {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if ('showInChat' in entry && typeof entry.showInChat !== 'boolean') {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
if ('id' in entry && typeof entry.id !== 'string') {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
CustomAgentFrontmatter.is = is;
|
|
79
|
+
})(CustomAgentFrontmatter || (CustomAgentFrontmatter = {}));
|
|
34
80
|
/**
|
|
35
81
|
* Default template entry for creating custom agents
|
|
36
82
|
*/
|
|
@@ -78,18 +124,31 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
78
124
|
this.allCustomizations = new Map();
|
|
79
125
|
/** Stores additional directory paths for loading template files. */
|
|
80
126
|
this.additionalTemplateDirs = new Set();
|
|
127
|
+
/**
|
|
128
|
+
* Built-in parent directories scanned for custom agents (`.agents`, `.prompts`), independent of
|
|
129
|
+
* {@link additionalTemplateDirs}. Insertion order is preserved so the first entry (`.agents`)
|
|
130
|
+
* acts as the default location for newly created agents.
|
|
131
|
+
*/
|
|
132
|
+
this.customAgentDirs = new Set();
|
|
81
133
|
/** Contains file extensions that identify prompt template files. */
|
|
82
134
|
this.templateExtensions = new Set([prompttemplate_contribution_1.PROMPT_TEMPLATE_EXTENSION]);
|
|
83
135
|
/** Stores specific file paths, provided by the settings, that should be treated as templates. */
|
|
84
136
|
this.workspaceTemplateFiles = new Set();
|
|
85
137
|
/** Maps URI strings to WatchedFileInfo objects for individually watched template files. */
|
|
86
138
|
this.watchedFiles = new Map();
|
|
139
|
+
/**
|
|
140
|
+
* For each known custom-agent prompt fragment id (i.e. `<name>_prompt`), the URI of the
|
|
141
|
+
* agent's folder (`<scope>/agents/<id>/`). Populated as agents are loaded and used to
|
|
142
|
+
* route customization writes/reads into the agent folder.
|
|
143
|
+
*/
|
|
144
|
+
this.customAgentFolderByFragmentId = new Map();
|
|
87
145
|
/** Collection of disposable resources for cleanup when the service updates or is disposed. */
|
|
88
146
|
this.toDispose = new core_1.DisposableCollection();
|
|
89
147
|
this.onDidChangePromptFragmentCustomizationEmitter = new core_1.Emitter();
|
|
90
148
|
this.onDidChangePromptFragmentCustomization = this.onDidChangePromptFragmentCustomizationEmitter.event;
|
|
91
149
|
this.onDidChangeCustomAgentsEmitter = new core_1.Emitter();
|
|
92
150
|
this.onDidChangeCustomAgents = this.onDidChangeCustomAgentsEmitter.event;
|
|
151
|
+
this.warnedLegacyYamlUris = new Set();
|
|
93
152
|
}
|
|
94
153
|
init() {
|
|
95
154
|
this.preferences.onPreferenceChanged(event => {
|
|
@@ -113,11 +172,28 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
113
172
|
// First process the main templates directory (lowest priority)
|
|
114
173
|
const templatesURI = await this.getTemplatesDirectoryURI();
|
|
115
174
|
await this.processTemplateDirectory(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, templatesURI, 1, CustomizationSource.CUSTOMIZED); // Priority 1 for customized fragments
|
|
116
|
-
// Process additional template directories (medium priority)
|
|
117
|
-
|
|
118
|
-
|
|
175
|
+
// Process additional template directories (medium priority). Skip any directory that
|
|
176
|
+
// resolves to the main templates directory — a user can set both prefs to the same
|
|
177
|
+
// path; re-processing the same dir overwrites the priority-1 entries with priority-2,
|
|
178
|
+
// which then hides them from `editBuiltIn`'s priority-1-only lookup.
|
|
179
|
+
const processedScopeKeys = new Set([templatesURI.toString()]);
|
|
180
|
+
for (const dirURI of this.getDedupedAdditionalScopes(templatesURI)) {
|
|
181
|
+
processedScopeKeys.add(dirURI.toString());
|
|
119
182
|
await this.processTemplateDirectory(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, 2, CustomizationSource.FOLDER); // Priority 2 for folder fragments
|
|
120
183
|
}
|
|
184
|
+
// Process built-in custom-agent directories (`.agents`/`.prompts`) not already covered as
|
|
185
|
+
// template directories above: register the prompt fragments inside their `agents/<id>/`
|
|
186
|
+
// folders and watch them, so agents under these scopes get the same live refresh and
|
|
187
|
+
// prompt-fragment editing as those under template directories — without loading loose
|
|
188
|
+
// prompt templates from these built-in scopes themselves.
|
|
189
|
+
for (const dirPath of this.customAgentDirs) {
|
|
190
|
+
const scopeURI = core_1.URI.fromFilePath(dirPath);
|
|
191
|
+
if (processedScopeKeys.has(scopeURI.toString())) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
processedScopeKeys.add(scopeURI.toString());
|
|
195
|
+
await this.processCustomAgentScope(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, scopeURI, 2, CustomizationSource.FOLDER);
|
|
196
|
+
}
|
|
121
197
|
// Process specific template files (highest priority)
|
|
122
198
|
await this.processTemplateFiles(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, watchedFilesCopy);
|
|
123
199
|
this.activeCustomizations = activeCustomizationsCopy;
|
|
@@ -388,10 +464,115 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
388
464
|
// Process existing files if directory exists
|
|
389
465
|
if (dirExists) {
|
|
390
466
|
await this.processExistingTemplateDirectory(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, priority, customizationSource);
|
|
467
|
+
await this.processCustomAgentFolders(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, priority, customizationSource);
|
|
391
468
|
}
|
|
392
469
|
// Set up file watching for the directory (works for both existing and non-existing directories)
|
|
393
470
|
this.setupDirectoryWatcher(dirURI, priority, customizationSource);
|
|
394
471
|
}
|
|
472
|
+
/**
|
|
473
|
+
* Processes a built-in custom-agent scope (`.agents`/`.prompts`): registers the prompt fragments
|
|
474
|
+
* inside its `agents/<id>/` folders and watches the scope for changes. Unlike
|
|
475
|
+
* {@link processTemplateDirectory} it does not scan loose `*.prompttemplate` files in the scope
|
|
476
|
+
* root, so `.agents` does not become a general prompt-template directory.
|
|
477
|
+
* @param dirURI URI of the agent scope directory
|
|
478
|
+
* @param priority Priority level for customizations in this scope
|
|
479
|
+
* @param customizationSource Source type of the customization
|
|
480
|
+
*/
|
|
481
|
+
async processCustomAgentScope(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, priority, customizationSource) {
|
|
482
|
+
await this.processCustomAgentFolders(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, dirURI, priority, customizationSource);
|
|
483
|
+
// Watch for changes (works for both existing and non-existing directories), restricted to
|
|
484
|
+
// agent folders so newly created/edited agents under `.agents` are picked up live.
|
|
485
|
+
this.setupDirectoryWatcher(dirURI, priority, customizationSource, true);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Scan `<dirURI>/agents/<id>/*.prompttemplate` files and register each as a customized
|
|
489
|
+
* prompt fragment so they appear in the Prompt Fragments configuration view and so
|
|
490
|
+
* `editPromptFragmentCustomization` / `removePromptFragmentCustomization` can find their
|
|
491
|
+
* source URIs.
|
|
492
|
+
*
|
|
493
|
+
* The reserved filename `prompt.prompttemplate` maps to the agent's default-variant
|
|
494
|
+
* fragment id (`<agent-name>_prompt` read from the sibling `agent.md`'s frontmatter).
|
|
495
|
+
* Any other `.prompttemplate` file uses its filename stem as the fragment id (matching
|
|
496
|
+
* how variants are registered via {@link readCustomAgentPromptVariants}).
|
|
497
|
+
*/
|
|
498
|
+
async processCustomAgentFolders(activeCustomizationsCopy, trackedTemplateURIsCopy, allCustomizationsCopy, scopeDir, priority, customizationSource) {
|
|
499
|
+
const agentsDirURI = scopeDir.resolve(exports.CUSTOM_AGENTS_DIRECTORY);
|
|
500
|
+
let agentsStat;
|
|
501
|
+
try {
|
|
502
|
+
agentsStat = await this.fileService.resolve(agentsDirURI);
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (!agentsStat.isDirectory || !agentsStat.children?.length) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
for (const agentChild of agentsStat.children) {
|
|
511
|
+
if (!agentChild.isDirectory) {
|
|
512
|
+
continue;
|
|
513
|
+
}
|
|
514
|
+
const defaultFragmentId = await this.readCustomAgentDefaultFragmentId(agentChild.resource);
|
|
515
|
+
if (defaultFragmentId) {
|
|
516
|
+
this.customAgentFolderByFragmentId.set(defaultFragmentId, agentChild.resource);
|
|
517
|
+
}
|
|
518
|
+
const folderStat = await this.fileService.resolve(agentChild.resource).catch(() => undefined);
|
|
519
|
+
if (!folderStat?.children?.length) {
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
for (const file of folderStat.children) {
|
|
523
|
+
if (!file.isFile || !this.isPromptTemplateExtension(file.resource.path.ext)) {
|
|
524
|
+
continue;
|
|
525
|
+
}
|
|
526
|
+
const stem = this.removePromptTemplateSuffix(file.resource.path.name);
|
|
527
|
+
const fragmentId = stem === exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM ? defaultFragmentId : stem;
|
|
528
|
+
if (!fragmentId) {
|
|
529
|
+
continue;
|
|
530
|
+
}
|
|
531
|
+
trackedTemplateURIsCopy.add(file.resource.toString());
|
|
532
|
+
try {
|
|
533
|
+
const fileContent = await this.fileService.read(file.resource);
|
|
534
|
+
const parsed = this.parseTemplateWithMetadata(fileContent.value);
|
|
535
|
+
this.addTemplate(activeCustomizationsCopy, fragmentId, parsed.template, file.resource.toString(), allCustomizationsCopy, priority, customizationSource, parsed.metadata);
|
|
536
|
+
}
|
|
537
|
+
catch (e) {
|
|
538
|
+
this.logger.debug(`Failed to load custom-agent customization ${file.resource.toString()}: ${e?.message ?? e}`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Return the parent URI when `resource` lives in `<scopeDirURI>/agents/<id>/`; otherwise
|
|
545
|
+
* undefined. Used to decide whether a newly-added file should be processed as a
|
|
546
|
+
* custom-agent prompt customization.
|
|
547
|
+
*/
|
|
548
|
+
matchesCustomAgentFolder(resource, scopeDirURI) {
|
|
549
|
+
const parent = resource.parent;
|
|
550
|
+
const grandParent = parent.parent;
|
|
551
|
+
if (!grandParent) {
|
|
552
|
+
return undefined;
|
|
553
|
+
}
|
|
554
|
+
if (grandParent.path.base !== exports.CUSTOM_AGENTS_DIRECTORY) {
|
|
555
|
+
return undefined;
|
|
556
|
+
}
|
|
557
|
+
if (!grandParent.parent.isEqual(scopeDirURI)) {
|
|
558
|
+
return undefined;
|
|
559
|
+
}
|
|
560
|
+
return parent;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Read `<agentFolderURI>/agent.md`'s frontmatter and return the implied default
|
|
564
|
+
* fragment id (`<name>_prompt`). Returns undefined if the file is missing or invalid.
|
|
565
|
+
*/
|
|
566
|
+
async readCustomAgentDefaultFragmentId(agentFolderURI) {
|
|
567
|
+
try {
|
|
568
|
+
const content = (await this.fileService.read(agentFolderURI.resolve(exports.CUSTOM_AGENT_FILE_NAME), { encoding: 'utf-8' })).value;
|
|
569
|
+
const { metadata } = (0, frontmatter_1.parseFrontmatter)(content, { isValid: CustomAgentFrontmatter.is });
|
|
570
|
+
return metadata ? `${metadata.name}_prompt` : undefined;
|
|
571
|
+
}
|
|
572
|
+
catch {
|
|
573
|
+
return undefined;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
395
576
|
/**
|
|
396
577
|
* Processes an existing directory for template files
|
|
397
578
|
* @param activeCustomizationsCopy Map to store active customizations
|
|
@@ -424,13 +605,27 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
424
605
|
this.onDidChangePromptFragmentCustomizationEmitter.fire(Array.from(parsedPromptFragments));
|
|
425
606
|
this.onDidChangeCustomAgentsEmitter.fire();
|
|
426
607
|
}
|
|
608
|
+
/**
|
|
609
|
+
* Whether a changed resource path affects custom agents, i.e. it is (or is inside) an `agents/`
|
|
610
|
+
* directory, or a legacy `customAgents.yml`. Matches the `agents` directory itself (e.g. when the
|
|
611
|
+
* whole folder is deleted), not only files within it, so removing `agents/` triggers a reload.
|
|
612
|
+
* @param path The string form of the changed resource URI
|
|
613
|
+
*/
|
|
614
|
+
isCustomAgentChange(path) {
|
|
615
|
+
return path.endsWith('customAgents.yml')
|
|
616
|
+
|| path.includes(`/${exports.CUSTOM_AGENTS_DIRECTORY}/`)
|
|
617
|
+
|| path.endsWith(`/${exports.CUSTOM_AGENTS_DIRECTORY}`);
|
|
618
|
+
}
|
|
427
619
|
/**
|
|
428
620
|
* Sets up file watching for a template directory (works for both existing and non-existing directories)
|
|
429
621
|
* @param dirURI URI of the directory to watch
|
|
430
622
|
* @param priority Priority level for customizations in this directory
|
|
431
623
|
* @param customizationSource Source type of the customization
|
|
624
|
+
* @param agentsOnly When true, only `agents/<id>/` prompt files are registered on add; loose
|
|
625
|
+
* `*.prompttemplate` files in the scope root are ignored. Used for built-in agent scopes
|
|
626
|
+
* (`.agents`) that must not become general prompt-template directories.
|
|
432
627
|
*/
|
|
433
|
-
setupDirectoryWatcher(dirURI, priority, customizationSource) {
|
|
628
|
+
setupDirectoryWatcher(dirURI, priority, customizationSource, agentsOnly = false) {
|
|
434
629
|
this.toDispose.push(this.fileService.watch(dirURI, { recursive: true, excludes: [] }));
|
|
435
630
|
this.toDispose.push(this.fileService.onDidFilesChange(async (event) => {
|
|
436
631
|
// Filter for changes within the watched directory
|
|
@@ -444,7 +639,7 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
444
639
|
await this.update();
|
|
445
640
|
return;
|
|
446
641
|
}
|
|
447
|
-
if (event.changes.some(change => change.resource.toString()
|
|
642
|
+
if (event.changes.some(change => this.isCustomAgentChange(change.resource.toString()))) {
|
|
448
643
|
this.onDidChangeCustomAgentsEmitter.fire();
|
|
449
644
|
}
|
|
450
645
|
// Track changes for batched notification
|
|
@@ -465,23 +660,44 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
465
660
|
if (this.trackedTemplateURIs.has(uriString)) {
|
|
466
661
|
const fileContent = await this.fileService.read(updatedFile.resource);
|
|
467
662
|
const parsed = this.parseTemplateWithMetadata(fileContent.value);
|
|
468
|
-
|
|
663
|
+
// Prefer the already-tracked fragment id over the filename stem so that
|
|
664
|
+
// `<scope>/agents/<id>/prompt.prompttemplate` keeps mapping to `<name>_prompt`
|
|
665
|
+
// instead of becoming a fragment called `prompt`.
|
|
666
|
+
const fragmentId = this.allCustomizations.get(uriString)?.id
|
|
667
|
+
?? this.removePromptTemplateSuffix(updatedFile.resource.path.name);
|
|
469
668
|
this.addTemplate(this.activeCustomizations, fragmentId, parsed.template, uriString, this.allCustomizations, priority, customizationSource, parsed.metadata);
|
|
470
669
|
changedFragmentIds.add(fragmentId);
|
|
471
670
|
}
|
|
472
671
|
}
|
|
473
672
|
// Handle new templates
|
|
474
673
|
for (const addedFile of event.getAdded()) {
|
|
475
|
-
if (addedFile.resource.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
674
|
+
if (!this.isPromptTemplateExtension(addedFile.resource.path.ext)) {
|
|
675
|
+
continue;
|
|
676
|
+
}
|
|
677
|
+
const isScopeRoot = !agentsOnly && addedFile.resource.parent.toString() === dirURI.toString();
|
|
678
|
+
const agentFolderURI = this.matchesCustomAgentFolder(addedFile.resource, dirURI);
|
|
679
|
+
if (!isScopeRoot && !agentFolderURI) {
|
|
680
|
+
continue;
|
|
681
|
+
}
|
|
682
|
+
const uriString = addedFile.resource.toString();
|
|
683
|
+
this.trackedTemplateURIs.add(uriString);
|
|
684
|
+
const fileContent = await this.fileService.read(addedFile.resource);
|
|
685
|
+
const parsed = this.parseTemplateWithMetadata(fileContent.value);
|
|
686
|
+
let fragmentId;
|
|
687
|
+
if (agentFolderURI) {
|
|
688
|
+
const stem = this.removePromptTemplateSuffix(addedFile.resource.path.name);
|
|
689
|
+
fragmentId = stem === exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM
|
|
690
|
+
? await this.readCustomAgentDefaultFragmentId(agentFolderURI)
|
|
691
|
+
: stem;
|
|
692
|
+
}
|
|
693
|
+
else {
|
|
694
|
+
fragmentId = this.removePromptTemplateSuffix(addedFile.resource.path.name);
|
|
695
|
+
}
|
|
696
|
+
if (!fragmentId) {
|
|
697
|
+
continue;
|
|
484
698
|
}
|
|
699
|
+
this.addTemplate(this.activeCustomizations, fragmentId, parsed.template, uriString, this.allCustomizations, priority, customizationSource, parsed.metadata);
|
|
700
|
+
changedFragmentIds.add(fragmentId);
|
|
485
701
|
}
|
|
486
702
|
const changedFragmentIdsArray = Array.from(changedFragmentIds);
|
|
487
703
|
if (changedFragmentIdsArray.length > 0) {
|
|
@@ -530,6 +746,12 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
530
746
|
this.additionalTemplateDirs.add(path);
|
|
531
747
|
}
|
|
532
748
|
}
|
|
749
|
+
if (properties.agentDirectoryPaths !== undefined) {
|
|
750
|
+
this.customAgentDirs.clear();
|
|
751
|
+
for (const path of properties.agentDirectoryPaths) {
|
|
752
|
+
this.customAgentDirs.add(path);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
533
755
|
if (properties.extensions !== undefined) {
|
|
534
756
|
this.templateExtensions.clear();
|
|
535
757
|
for (const ext of properties.extensions) {
|
|
@@ -559,12 +781,62 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
559
781
|
const theiaConfigDir = await this.envVariablesServer.getConfigDirUri();
|
|
560
782
|
return new core_1.URI(theiaConfigDir).resolve('prompt-templates');
|
|
561
783
|
}
|
|
784
|
+
/**
|
|
785
|
+
* The additional (workspace) template directories as URIs, excluding any that resolves to the
|
|
786
|
+
* same location as `excluding` (typically the global templates directory). A user can point
|
|
787
|
+
* both the `promptTemplates` preference and a workspace template directory at the same path;
|
|
788
|
+
* processing that scope twice would hide priority-1 customizations behind priority-2 copies
|
|
789
|
+
* (see {@link update}) or list the same scope twice in the agent-location picker.
|
|
790
|
+
*/
|
|
791
|
+
getDedupedAdditionalScopes(excluding) {
|
|
792
|
+
const result = [];
|
|
793
|
+
for (const dirPath of this.additionalTemplateDirs) {
|
|
794
|
+
const dirURI = core_1.URI.fromFilePath(dirPath);
|
|
795
|
+
if (!dirURI.isEqual(excluding)) {
|
|
796
|
+
result.push(dirURI);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
return result;
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* The deduplicated parent directories scanned for custom agents, in precedence order:
|
|
803
|
+
* the built-in {@link customAgentDirs} (`.agents` then `.prompts`) first, so `.agents` is both
|
|
804
|
+
* the discovery winner and the default creation target; then any configured
|
|
805
|
+
* {@link additionalTemplateDirs} that may also hold agents; then the global templates directory.
|
|
806
|
+
* Independent of the prompt-templates preference, mirroring how skills resolve their folders.
|
|
807
|
+
*/
|
|
808
|
+
async getCustomAgentScopes() {
|
|
809
|
+
const seen = new Set();
|
|
810
|
+
const scopes = [];
|
|
811
|
+
const add = (uri) => {
|
|
812
|
+
const key = uri.toString();
|
|
813
|
+
if (!seen.has(key)) {
|
|
814
|
+
seen.add(key);
|
|
815
|
+
scopes.push(uri);
|
|
816
|
+
}
|
|
817
|
+
};
|
|
818
|
+
for (const dirPath of this.customAgentDirs) {
|
|
819
|
+
add(core_1.URI.fromFilePath(dirPath));
|
|
820
|
+
}
|
|
821
|
+
for (const dirPath of this.additionalTemplateDirs) {
|
|
822
|
+
add(core_1.URI.fromFilePath(dirPath));
|
|
823
|
+
}
|
|
824
|
+
add(await this.getTemplatesDirectoryURI());
|
|
825
|
+
return scopes;
|
|
826
|
+
}
|
|
562
827
|
/**
|
|
563
828
|
* Gets the URI for a specific template file
|
|
564
829
|
* @param fragmentId The fragment ID
|
|
565
830
|
* @returns URI for the template file
|
|
566
831
|
*/
|
|
567
832
|
async getTemplateURI(fragmentId) {
|
|
833
|
+
// Custom-agent default prompts live inside their own folder under the reserved filename
|
|
834
|
+
// `prompt.prompttemplate`. Variants and other fragments fall back to scope-root behavior
|
|
835
|
+
// (where filename = fragment id), matching how Theia tracks all other customizations.
|
|
836
|
+
const agentFolder = this.customAgentFolderByFragmentId.get(fragmentId);
|
|
837
|
+
if (agentFolder) {
|
|
838
|
+
return agentFolder.resolve(`${exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM}${prompttemplate_contribution_1.PROMPT_TEMPLATE_EXTENSION}`);
|
|
839
|
+
}
|
|
568
840
|
return (await this.getTemplatesDirectoryURI()).resolve(`${fragmentId}${prompttemplate_contribution_1.PROMPT_TEMPLATE_EXTENSION}`);
|
|
569
841
|
}
|
|
570
842
|
/**
|
|
@@ -825,21 +1097,197 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
825
1097
|
}
|
|
826
1098
|
async getCustomAgents() {
|
|
827
1099
|
const agentsById = new Map();
|
|
828
|
-
//
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
await this.
|
|
1100
|
+
// Process scopes in precedence order (`.agents`/`.prompts` first, global last). The map only
|
|
1101
|
+
// keeps the first agent seen per id, so earlier scopes win on conflicts.
|
|
1102
|
+
for (const scope of await this.getCustomAgentScopes()) {
|
|
1103
|
+
await this.loadCustomAgentsFromAgentsDirectory(scope, agentsById);
|
|
1104
|
+
await this.loadCustomAgentsFromDirectory(scope, agentsById);
|
|
832
1105
|
}
|
|
833
|
-
//
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
//
|
|
1106
|
+
// Note: customAgentFolderByFragmentId is intentionally not cleared here. It grows
|
|
1107
|
+
// monotonically — stale entries (renamed/removed agents) at worst point at a folder
|
|
1108
|
+
// that no longer exists, which the file-create path mkdirps if needed. Clearing it
|
|
1109
|
+
// up front would create a race where a concurrent `getTemplateURI` call sees an empty
|
|
1110
|
+
// map and falls back to the scope root.
|
|
837
1111
|
return Array.from(agentsById.values());
|
|
838
1112
|
}
|
|
839
1113
|
/**
|
|
840
|
-
* Load custom agents from
|
|
841
|
-
*
|
|
842
|
-
*
|
|
1114
|
+
* Load custom agents from `<parentDirectory>/agents/<id>/agent.md`. Each immediate
|
|
1115
|
+
* subdirectory under `agents/` defines one custom agent: the folder name is the
|
|
1116
|
+
* agent id (single source of truth), the file's YAML frontmatter holds the metadata,
|
|
1117
|
+
* and the body is the prompt text. Folders without a readable `agent.md` are skipped.
|
|
1118
|
+
*/
|
|
1119
|
+
async loadCustomAgentsFromAgentsDirectory(parentDirectory, agentsById) {
|
|
1120
|
+
const agentsDirURI = parentDirectory.resolve(exports.CUSTOM_AGENTS_DIRECTORY);
|
|
1121
|
+
let directoryStat;
|
|
1122
|
+
try {
|
|
1123
|
+
directoryStat = await this.fileService.resolve(agentsDirURI);
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
if (!directoryStat.isDirectory || !directoryStat.children?.length) {
|
|
1129
|
+
return;
|
|
1130
|
+
}
|
|
1131
|
+
for (const child of directoryStat.children) {
|
|
1132
|
+
if (!child.isDirectory) {
|
|
1133
|
+
continue;
|
|
1134
|
+
}
|
|
1135
|
+
const agent = await this.readCustomAgentFile(child.resource);
|
|
1136
|
+
if (!agent) {
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (!agentsById.has(agent.id)) {
|
|
1140
|
+
agentsById.set(agent.id, agent);
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
async readCustomAgentFile(agentFolderURI) {
|
|
1145
|
+
const id = agentFolderURI.path.base;
|
|
1146
|
+
const fileURI = agentFolderURI.resolve(exports.CUSTOM_AGENT_FILE_NAME);
|
|
1147
|
+
let content;
|
|
1148
|
+
try {
|
|
1149
|
+
content = (await this.fileService.read(fileURI, { encoding: 'utf-8' })).value;
|
|
1150
|
+
}
|
|
1151
|
+
catch {
|
|
1152
|
+
return undefined;
|
|
1153
|
+
}
|
|
1154
|
+
const { metadata, body } = (0, frontmatter_1.parseFrontmatter)(content, { isValid: CustomAgentFrontmatter.is });
|
|
1155
|
+
if (!metadata) {
|
|
1156
|
+
this.logger.debug(`Invalid or missing frontmatter in ${fileURI.toString()}`);
|
|
1157
|
+
return undefined;
|
|
1158
|
+
}
|
|
1159
|
+
if (metadata.id !== undefined && metadata.id !== id) {
|
|
1160
|
+
this.logger.debug(`Frontmatter id '${metadata.id}' in ${fileURI.toString()} does not match folder name '${id}'. Skipping.`);
|
|
1161
|
+
return undefined;
|
|
1162
|
+
}
|
|
1163
|
+
const promptVariants = await this.readCustomAgentPromptVariants(agentFolderURI);
|
|
1164
|
+
const fragmentId = `${metadata.name}_prompt`;
|
|
1165
|
+
this.customAgentFolderByFragmentId.set(fragmentId, agentFolderURI);
|
|
1166
|
+
return {
|
|
1167
|
+
id,
|
|
1168
|
+
name: metadata.name,
|
|
1169
|
+
description: metadata.description,
|
|
1170
|
+
prompt: body,
|
|
1171
|
+
defaultLLM: metadata.defaultLLM,
|
|
1172
|
+
showInChat: metadata.showInChat,
|
|
1173
|
+
...(promptVariants.length > 0 ? { promptVariants } : {})
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
/**
|
|
1177
|
+
* Remove `<scopeDir>/agents/<id>/` directories that contain no `agent.md`. They are usually
|
|
1178
|
+
* the leftover side effect of a previous migration whose `agent.md` write failed after the
|
|
1179
|
+
* parent directory had already been created. Removing them lets the next run re-attempt.
|
|
1180
|
+
*/
|
|
1181
|
+
async cleanupEmptyAgentFolders(scopeDir) {
|
|
1182
|
+
const agentsDirURI = scopeDir.resolve(exports.CUSTOM_AGENTS_DIRECTORY);
|
|
1183
|
+
let stat;
|
|
1184
|
+
try {
|
|
1185
|
+
stat = await this.fileService.resolve(agentsDirURI);
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
if (!stat.children?.length) {
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
for (const child of stat.children) {
|
|
1194
|
+
if (!child.isDirectory) {
|
|
1195
|
+
continue;
|
|
1196
|
+
}
|
|
1197
|
+
const agentMdURI = child.resource.resolve(exports.CUSTOM_AGENT_FILE_NAME);
|
|
1198
|
+
if (await this.fileService.exists(agentMdURI)) {
|
|
1199
|
+
continue;
|
|
1200
|
+
}
|
|
1201
|
+
const childStat = await this.fileService.resolve(child.resource).catch(() => undefined);
|
|
1202
|
+
if (childStat?.children?.length) {
|
|
1203
|
+
continue;
|
|
1204
|
+
}
|
|
1205
|
+
try {
|
|
1206
|
+
await this.fileService.delete(child.resource, { recursive: true });
|
|
1207
|
+
this.logger.info(`Removed empty agent folder ${child.resource.toString()} left behind by a previous run.`);
|
|
1208
|
+
}
|
|
1209
|
+
catch (e) {
|
|
1210
|
+
this.logger.warn(`Failed to remove empty agent folder ${child.resource.toString()}: ${e?.message ?? e}`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* List `.prompttemplate` files directly inside the given scope directory (one level only).
|
|
1216
|
+
* Returns each file's URI along with its filename stem to enable cheap prefix matching
|
|
1217
|
+
* during migration.
|
|
1218
|
+
*/
|
|
1219
|
+
async listScopeRootPromptTemplates(scopeDir) {
|
|
1220
|
+
let stat;
|
|
1221
|
+
try {
|
|
1222
|
+
stat = await this.fileService.resolve(scopeDir);
|
|
1223
|
+
}
|
|
1224
|
+
catch {
|
|
1225
|
+
return [];
|
|
1226
|
+
}
|
|
1227
|
+
if (!stat.children?.length) {
|
|
1228
|
+
return [];
|
|
1229
|
+
}
|
|
1230
|
+
const result = [];
|
|
1231
|
+
for (const child of stat.children) {
|
|
1232
|
+
if (!child.isFile) {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
if (!this.isPromptTemplateExtension(child.resource.path.ext)) {
|
|
1236
|
+
continue;
|
|
1237
|
+
}
|
|
1238
|
+
result.push({ uri: child.resource, stem: this.removePromptTemplateSuffix(child.resource.path.name) });
|
|
1239
|
+
}
|
|
1240
|
+
return result;
|
|
1241
|
+
}
|
|
1242
|
+
/**
|
|
1243
|
+
* Scan an agent folder for sibling `.prompttemplate` files; each becomes a variant
|
|
1244
|
+
* of the agent's prompt. Variant id = filename stem (e.g. `concise.prompttemplate`
|
|
1245
|
+
* yields a variant with id `concise`). Files inside subdirectories are ignored.
|
|
1246
|
+
*
|
|
1247
|
+
* The reserved filename `prompt.prompttemplate` is excluded: it represents the
|
|
1248
|
+
* default-variant customization (overrides `agent.md`'s body), not an extra variant,
|
|
1249
|
+
* and is registered by the prompt-fragment customization scan instead.
|
|
1250
|
+
*/
|
|
1251
|
+
async readCustomAgentPromptVariants(agentFolderURI) {
|
|
1252
|
+
let folderStat;
|
|
1253
|
+
try {
|
|
1254
|
+
folderStat = await this.fileService.resolve(agentFolderURI);
|
|
1255
|
+
}
|
|
1256
|
+
catch {
|
|
1257
|
+
return [];
|
|
1258
|
+
}
|
|
1259
|
+
if (!folderStat.children?.length) {
|
|
1260
|
+
return [];
|
|
1261
|
+
}
|
|
1262
|
+
const variants = [];
|
|
1263
|
+
for (const child of folderStat.children) {
|
|
1264
|
+
if (!child.isFile) {
|
|
1265
|
+
continue;
|
|
1266
|
+
}
|
|
1267
|
+
if (!this.isPromptTemplateExtension(child.resource.path.ext)) {
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
const variantId = this.removePromptTemplateSuffix(child.resource.path.name);
|
|
1271
|
+
if (variantId === exports.CUSTOM_AGENT_DEFAULT_PROMPT_STEM) {
|
|
1272
|
+
continue;
|
|
1273
|
+
}
|
|
1274
|
+
try {
|
|
1275
|
+
const variantContent = (await this.fileService.read(child.resource, { encoding: 'utf-8' })).value;
|
|
1276
|
+
const parsed = this.parseTemplateWithMetadata(variantContent);
|
|
1277
|
+
variants.push({ id: variantId, template: parsed.template });
|
|
1278
|
+
}
|
|
1279
|
+
catch (e) {
|
|
1280
|
+
this.logger.debug(`Failed to read prompt variant ${child.resource.toString()}: ${e?.message ?? e}`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
// Stable order to keep equality checks deterministic between loads.
|
|
1284
|
+
variants.sort((a, b) => a.id.localeCompare(b.id));
|
|
1285
|
+
return variants;
|
|
1286
|
+
}
|
|
1287
|
+
/**
|
|
1288
|
+
* @deprecated Reads legacy `customAgents.yml` files. New agents should live under
|
|
1289
|
+
* `<scope>/agents/<id>/agent.md`. Kept as a fallback until existing files have been
|
|
1290
|
+
* auto-migrated; loader logs a one-time warning per scope when it finds one.
|
|
843
1291
|
*/
|
|
844
1292
|
async loadCustomAgentsFromDirectory(directoryURI, agentsById) {
|
|
845
1293
|
const customAgentYamlUri = directoryURI.resolve('customAgents.yml');
|
|
@@ -847,11 +1295,12 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
847
1295
|
if (!yamlExists) {
|
|
848
1296
|
return;
|
|
849
1297
|
}
|
|
1298
|
+
this.warnOnceLegacyYaml(customAgentYamlUri);
|
|
850
1299
|
try {
|
|
851
1300
|
const fileContent = await this.fileService.read(customAgentYamlUri, { encoding: 'utf-8' });
|
|
852
1301
|
const doc = (0, js_yaml_1.load)(fileContent.value);
|
|
853
1302
|
if (!Array.isArray(doc) || !doc.every(entry => common_1.CustomAgentDescription.is(entry))) {
|
|
854
|
-
|
|
1303
|
+
this.logger.debug(`Invalid customAgents.yml file content in ${directoryURI.toString()}`);
|
|
855
1304
|
return;
|
|
856
1305
|
}
|
|
857
1306
|
const readAgents = doc;
|
|
@@ -863,35 +1312,195 @@ let DefaultPromptFragmentCustomizationService = class DefaultPromptFragmentCusto
|
|
|
863
1312
|
}
|
|
864
1313
|
}
|
|
865
1314
|
catch (e) {
|
|
866
|
-
|
|
1315
|
+
this.logger.debug(`Error loading customAgents.yml from ${directoryURI.toString()}: ${e.message}`, e);
|
|
867
1316
|
}
|
|
868
1317
|
}
|
|
1318
|
+
warnOnceLegacyYaml(uri) {
|
|
1319
|
+
const key = uri.toString();
|
|
1320
|
+
if (this.warnedLegacyYamlUris.has(key)) {
|
|
1321
|
+
return;
|
|
1322
|
+
}
|
|
1323
|
+
this.warnedLegacyYamlUris.add(key);
|
|
1324
|
+
this.logger.warn(`Loading custom agents from legacy '${key}'. ` +
|
|
1325
|
+
`Files in this format are auto-migrated to the '${exports.CUSTOM_AGENTS_DIRECTORY}/' folder on startup. ` +
|
|
1326
|
+
'If migration did not run, invoke the command \'AI: Re-run custom-agent migration\'.');
|
|
1327
|
+
}
|
|
869
1328
|
/**
|
|
870
|
-
* Returns all locations of existing customAgents.yml files and
|
|
871
|
-
* new
|
|
872
|
-
*
|
|
873
|
-
* @returns An array of objects containing the URI and whether the file exists
|
|
1329
|
+
* Returns all locations of existing customAgents.yml files and `agents/` directories,
|
|
1330
|
+
* plus the canonical locations where new agents would be created (one per scope). Scopes are
|
|
1331
|
+
* returned in precedence order, so the first `agents-dir` entry is the default creation target.
|
|
874
1332
|
*/
|
|
875
1333
|
async getCustomAgentsLocations() {
|
|
876
1334
|
const locations = [];
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
// Check additional (workspace) template directories
|
|
883
|
-
for (const dirPath of this.additionalTemplateDirs) {
|
|
884
|
-
const dirURI = core_1.URI.fromFilePath(dirPath);
|
|
885
|
-
const agentsUri = dirURI.resolve('customAgents.yml');
|
|
886
|
-
const exists = await this.fileService.exists(agentsUri);
|
|
887
|
-
locations.push({ uri: agentsUri, exists: exists });
|
|
1335
|
+
for (const parentDir of await this.getCustomAgentScopes()) {
|
|
1336
|
+
const agentsDirURI = parentDir.resolve(exports.CUSTOM_AGENTS_DIRECTORY);
|
|
1337
|
+
locations.push({ uri: agentsDirURI, exists: await this.fileService.exists(agentsDirURI), kind: 'agents-dir' });
|
|
1338
|
+
const yamlURI = parentDir.resolve('customAgents.yml');
|
|
1339
|
+
locations.push({ uri: yamlURI, exists: await this.fileService.exists(yamlURI), kind: 'legacy-yaml' });
|
|
888
1340
|
}
|
|
889
1341
|
return locations;
|
|
890
1342
|
}
|
|
891
1343
|
/**
|
|
892
|
-
*
|
|
1344
|
+
* Creates `<parentDirectory>/agents/<id>/agent.md` from a `CustomAgentDescription` and opens it.
|
|
1345
|
+
* The agent id determines the folder name; the prompt body is written verbatim under the YAML frontmatter.
|
|
1346
|
+
*/
|
|
1347
|
+
async createCustomAgentFile(parentDirectory, agent) {
|
|
1348
|
+
const fileURI = parentDirectory.resolve(exports.CUSTOM_AGENTS_DIRECTORY).resolve(agent.id).resolve(exports.CUSTOM_AGENT_FILE_NAME);
|
|
1349
|
+
const content = serializeCustomAgentFile(agent);
|
|
1350
|
+
if (!(await this.fileService.exists(fileURI))) {
|
|
1351
|
+
await this.fileService.createFile(fileURI, buffer_1.BinaryBuffer.fromString(content));
|
|
1352
|
+
}
|
|
1353
|
+
else {
|
|
1354
|
+
await this.fileService.writeFile(fileURI, buffer_1.BinaryBuffer.fromString(content));
|
|
1355
|
+
}
|
|
1356
|
+
const openHandler = await this.openerService.getOpener(fileURI);
|
|
1357
|
+
openHandler.open(fileURI);
|
|
1358
|
+
return fileURI;
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Auto-migrate every legacy `customAgents.yml` reachable from the configured scopes to the new
|
|
1362
|
+
* `agents/<id>/agent.md` layout. The original content is never deleted:
|
|
1363
|
+
* - on full success the YAML is renamed to `customAgents.yml.bak`, replacing any previous backup;
|
|
1364
|
+
* - on partial failure the YAML is renamed to `customAgents.yml.bak` only if no backup exists yet;
|
|
1365
|
+
* if one already exists the YAML is left in place, so the loader keeps serving it and the next
|
|
1366
|
+
* startup retries the migration.
|
|
893
1367
|
*
|
|
894
|
-
*
|
|
1368
|
+
* Idempotent: rerunning never overwrites an already-migrated agent file.
|
|
1369
|
+
*/
|
|
1370
|
+
async migrateCustomAgentsYaml() {
|
|
1371
|
+
// Coalesce concurrent calls. refreshCustomAgents() can re-fire during startup when
|
|
1372
|
+
// additionalTemplateDirs is populated asynchronously; without this guard each call
|
|
1373
|
+
// launches a parallel migration that races on the same `customAgents.yml` and on
|
|
1374
|
+
// sibling `*_prompt*.prompttemplate` files.
|
|
1375
|
+
if (this.migrationInFlight) {
|
|
1376
|
+
return this.migrationInFlight;
|
|
1377
|
+
}
|
|
1378
|
+
this.migrationInFlight = this.doMigrateCustomAgentsYaml().finally(() => {
|
|
1379
|
+
this.migrationInFlight = undefined;
|
|
1380
|
+
});
|
|
1381
|
+
return this.migrationInFlight;
|
|
1382
|
+
}
|
|
1383
|
+
async doMigrateCustomAgentsYaml() {
|
|
1384
|
+
const scopes = await this.getCustomAgentScopes();
|
|
1385
|
+
const reports = [];
|
|
1386
|
+
for (const scope of scopes) {
|
|
1387
|
+
const report = await this.migrateSingleScope(scope);
|
|
1388
|
+
if (report) {
|
|
1389
|
+
reports.push(report);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (reports.some(r => r.migrated > 0)) {
|
|
1393
|
+
this.onDidChangeCustomAgentsEmitter.fire();
|
|
1394
|
+
}
|
|
1395
|
+
return reports;
|
|
1396
|
+
}
|
|
1397
|
+
async migrateSingleScope(scopeDir) {
|
|
1398
|
+
const yamlURI = scopeDir.resolve('customAgents.yml');
|
|
1399
|
+
if (!(await this.fileService.exists(yamlURI))) {
|
|
1400
|
+
return undefined;
|
|
1401
|
+
}
|
|
1402
|
+
this.logger.info(`Migrating custom agents from ${yamlURI.toString()}`);
|
|
1403
|
+
let entries;
|
|
1404
|
+
try {
|
|
1405
|
+
const fileContent = await this.fileService.read(yamlURI, { encoding: 'utf-8' });
|
|
1406
|
+
const doc = (0, js_yaml_1.load)(fileContent.value);
|
|
1407
|
+
if (!Array.isArray(doc) || !doc.every(common_1.CustomAgentDescription.is)) {
|
|
1408
|
+
this.logger.warn(`Skipping migration of ${yamlURI.toString()}: file content is not a valid CustomAgentDescription[]`);
|
|
1409
|
+
return { scope: scopeDir, yamlURI, migrated: 0, alreadyPresent: 0, failed: 0, yamlBackedUp: false, promptOverridesMigrated: 0 };
|
|
1410
|
+
}
|
|
1411
|
+
entries = doc;
|
|
1412
|
+
}
|
|
1413
|
+
catch (e) {
|
|
1414
|
+
this.logger.warn(`Skipping migration of ${yamlURI.toString()}: ${e.message}`);
|
|
1415
|
+
return { scope: scopeDir, yamlURI, migrated: 0, alreadyPresent: 0, failed: 0, yamlBackedUp: false, promptOverridesMigrated: 0 };
|
|
1416
|
+
}
|
|
1417
|
+
let migrated = 0;
|
|
1418
|
+
let alreadyPresent = 0;
|
|
1419
|
+
let failed = 0;
|
|
1420
|
+
let promptOverridesMigrated = 0;
|
|
1421
|
+
// A previous failed migration may have created `agents/<id>/` directories without an
|
|
1422
|
+
// `agent.md` inside. Remove those empty placeholders so this run can recreate them cleanly.
|
|
1423
|
+
await this.cleanupEmptyAgentFolders(scopeDir);
|
|
1424
|
+
// Snapshot scope-root template files once so we can match per agent without re-listing.
|
|
1425
|
+
const scopeRootTemplateFiles = await this.listScopeRootPromptTemplates(scopeDir);
|
|
1426
|
+
// Prompt-fragment files are matched by agent *name* (the fragment id is `<name>_prompt`)
|
|
1427
|
+
// while folders are keyed by the unique *id*. When two entries share a name, only the first
|
|
1428
|
+
// can own the name-based files, so track seen names and skip the move for the duplicates.
|
|
1429
|
+
const seenNames = new Set();
|
|
1430
|
+
for (const entry of entries) {
|
|
1431
|
+
const agentFolderURI = scopeDir.resolve(exports.CUSTOM_AGENTS_DIRECTORY).resolve(entry.id);
|
|
1432
|
+
const targetURI = agentFolderURI.resolve(exports.CUSTOM_AGENT_FILE_NAME);
|
|
1433
|
+
if (await this.fileService.exists(targetURI)) {
|
|
1434
|
+
alreadyPresent++;
|
|
1435
|
+
}
|
|
1436
|
+
else {
|
|
1437
|
+
try {
|
|
1438
|
+
await this.fileService.createFile(targetURI, buffer_1.BinaryBuffer.fromString(serializeCustomAgentFile(entry)), { overwrite: true });
|
|
1439
|
+
migrated++;
|
|
1440
|
+
}
|
|
1441
|
+
catch (e) {
|
|
1442
|
+
this.logger.warn(`Failed to migrate agent '${entry.id}' from ${yamlURI.toString()}: ${e?.message ?? e}`, e);
|
|
1443
|
+
failed++;
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
if (seenNames.has(entry.name)) {
|
|
1448
|
+
this.logger.warn(`Multiple custom agents in ${yamlURI.toString()} share the name '${entry.name}'; ` +
|
|
1449
|
+
'prompt fragment files were migrated to the first matching agent only.');
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
seenNames.add(entry.name);
|
|
1453
|
+
// Move any scope-root .prompttemplate files that look like they belong to this agent
|
|
1454
|
+
// (filename stem starts with `<name>_prompt`, followed by EOF or a separator).
|
|
1455
|
+
// They become variants under `<scope>/agents/<id>/`.
|
|
1456
|
+
for (const file of scopeRootTemplateFiles) {
|
|
1457
|
+
if (!matchesAgentPromptPrefix(file.stem, entry.name)) {
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
const destURI = agentFolderURI.resolve(file.uri.path.base);
|
|
1461
|
+
if (await this.fileService.exists(destURI)) {
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
try {
|
|
1465
|
+
await this.fileService.move(file.uri, destURI);
|
|
1466
|
+
promptOverridesMigrated++;
|
|
1467
|
+
}
|
|
1468
|
+
catch (e) {
|
|
1469
|
+
this.logger.warn(`Failed to move prompt fragment ${file.uri.toString()} into ${destURI.toString()}: ${e?.message ?? e}`);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
let yamlBackedUp = false;
|
|
1474
|
+
const backupURI = scopeDir.resolve('customAgents.yml.bak');
|
|
1475
|
+
if (failed === 0) {
|
|
1476
|
+
try {
|
|
1477
|
+
await this.fileService.move(yamlURI, backupURI, { overwrite: true });
|
|
1478
|
+
yamlBackedUp = true;
|
|
1479
|
+
}
|
|
1480
|
+
catch (e) {
|
|
1481
|
+
this.logger.warn(`Migrated ${migrated} agents but failed to back up ${yamlURI.toString()}: ${e.message}`);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
else {
|
|
1485
|
+
try {
|
|
1486
|
+
if (!(await this.fileService.exists(backupURI))) {
|
|
1487
|
+
await this.fileService.move(yamlURI, backupURI);
|
|
1488
|
+
yamlBackedUp = true;
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
catch (e) {
|
|
1492
|
+
this.logger.warn(`Failed to back up ${yamlURI.toString()} to ${backupURI.toString()}: ${e.message}`);
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
|
+
this.logger.info(`Migration done for ${yamlURI.toString()}: ` +
|
|
1496
|
+
`migrated=${migrated}, alreadyPresent=${alreadyPresent}, failed=${failed}, ` +
|
|
1497
|
+
`yamlBackedUp=${yamlBackedUp}, promptOverridesMigrated=${promptOverridesMigrated}`);
|
|
1498
|
+
return { scope: scopeDir, yamlURI, migrated, alreadyPresent, failed, yamlBackedUp, promptOverridesMigrated };
|
|
1499
|
+
}
|
|
1500
|
+
/**
|
|
1501
|
+
* @deprecated Use {@link createCustomAgentFile} to author agents in the new
|
|
1502
|
+
* `<scope>/agents/<id>/agent.md` layout. Retained so existing UI affordances keep
|
|
1503
|
+
* working until they are migrated.
|
|
895
1504
|
*/
|
|
896
1505
|
async openCustomAgentYaml(uri) {
|
|
897
1506
|
const content = (0, js_yaml_1.dump)([newCustomAgentEntry]);
|
|
@@ -931,6 +1540,11 @@ tslib_1.__decorate([
|
|
|
931
1540
|
(0, inversify_1.inject)(browser_2.WorkspaceService),
|
|
932
1541
|
tslib_1.__metadata("design:type", browser_2.WorkspaceService)
|
|
933
1542
|
], DefaultPromptFragmentCustomizationService.prototype, "workspaceService", void 0);
|
|
1543
|
+
tslib_1.__decorate([
|
|
1544
|
+
(0, inversify_1.inject)(core_1.ILogger),
|
|
1545
|
+
(0, inversify_1.named)('ai-core:DefaultPromptFragmentCustomizationService'),
|
|
1546
|
+
tslib_1.__metadata("design:type", Object)
|
|
1547
|
+
], DefaultPromptFragmentCustomizationService.prototype, "logger", void 0);
|
|
934
1548
|
tslib_1.__decorate([
|
|
935
1549
|
(0, inversify_1.postConstruct)(),
|
|
936
1550
|
tslib_1.__metadata("design:type", Function),
|
|
@@ -940,4 +1554,30 @@ tslib_1.__decorate([
|
|
|
940
1554
|
exports.DefaultPromptFragmentCustomizationService = DefaultPromptFragmentCustomizationService = tslib_1.__decorate([
|
|
941
1555
|
(0, inversify_1.injectable)()
|
|
942
1556
|
], DefaultPromptFragmentCustomizationService);
|
|
1557
|
+
/**
|
|
1558
|
+
* Returns true when a filename stem looks like a prompt fragment owned by the named agent —
|
|
1559
|
+
* i.e. it begins with `<name>_prompt` followed by end-of-string or a separator
|
|
1560
|
+
* (`-`, `_`, ` `, `.`). Used to scoop variant files like `Foo_prompt_old.prompttemplate` into
|
|
1561
|
+
* the agent's folder during migration without grabbing unrelated fragments such as
|
|
1562
|
+
* `FooBar_prompt.prompttemplate` for a different agent.
|
|
1563
|
+
*/
|
|
1564
|
+
function matchesAgentPromptPrefix(stem, agentName) {
|
|
1565
|
+
const prefix = `${agentName}_prompt`;
|
|
1566
|
+
if (!stem.startsWith(prefix)) {
|
|
1567
|
+
return false;
|
|
1568
|
+
}
|
|
1569
|
+
const next = stem.charAt(prefix.length);
|
|
1570
|
+
return next === '' || next === '-' || next === '_' || next === ' ' || next === '.';
|
|
1571
|
+
}
|
|
1572
|
+
function serializeCustomAgentFile(agent) {
|
|
1573
|
+
const metadata = {
|
|
1574
|
+
name: agent.name,
|
|
1575
|
+
description: agent.description,
|
|
1576
|
+
defaultLLM: agent.defaultLLM,
|
|
1577
|
+
};
|
|
1578
|
+
if (agent.showInChat !== undefined) {
|
|
1579
|
+
metadata.showInChat = agent.showInChat;
|
|
1580
|
+
}
|
|
1581
|
+
return (0, frontmatter_1.serializeFrontmatter)(metadata, agent.prompt);
|
|
1582
|
+
}
|
|
943
1583
|
//# sourceMappingURL=frontend-prompt-customization-service.js.map
|