@theia/ai-core 1.75.0-next.8 → 1.75.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.
Files changed (134) hide show
  1. package/lib/browser/agent-plugin-ui-bridge.d.ts +26 -0
  2. package/lib/browser/agent-plugin-ui-bridge.d.ts.map +1 -0
  3. package/lib/browser/agent-plugin-ui-bridge.js +20 -0
  4. package/lib/browser/agent-plugin-ui-bridge.js.map +1 -0
  5. package/lib/browser/ai-core-command-contribution.d.ts.map +1 -1
  6. package/lib/browser/ai-core-command-contribution.js +4 -1
  7. package/lib/browser/ai-core-command-contribution.js.map +1 -1
  8. package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
  9. package/lib/browser/ai-core-frontend-module.js +6 -0
  10. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  11. package/lib/browser/ai-settings-service.js +1 -0
  12. package/lib/browser/ai-settings-service.js.map +1 -1
  13. package/lib/browser/frontend-language-model-alias-registry.js +1 -1
  14. package/lib/browser/frontend-language-model-registry.d.ts +2 -0
  15. package/lib/browser/frontend-language-model-registry.d.ts.map +1 -1
  16. package/lib/browser/frontend-language-model-registry.js +22 -1
  17. package/lib/browser/frontend-language-model-registry.js.map +1 -1
  18. package/lib/browser/generic-capabilities-variable-contribution.d.ts.map +1 -1
  19. package/lib/browser/generic-capabilities-variable-contribution.js +4 -1
  20. package/lib/browser/generic-capabilities-variable-contribution.js.map +1 -1
  21. package/lib/browser/hide-ai-preferences-contribution.d.ts +30 -0
  22. package/lib/browser/hide-ai-preferences-contribution.d.ts.map +1 -0
  23. package/lib/browser/hide-ai-preferences-contribution.js +90 -0
  24. package/lib/browser/hide-ai-preferences-contribution.js.map +1 -0
  25. package/lib/browser/hide-ai-preferences-contribution.spec.d.ts +2 -0
  26. package/lib/browser/hide-ai-preferences-contribution.spec.d.ts.map +1 -0
  27. package/lib/browser/hide-ai-preferences-contribution.spec.js +95 -0
  28. package/lib/browser/hide-ai-preferences-contribution.spec.js.map +1 -0
  29. package/lib/browser/index.d.ts +3 -0
  30. package/lib/browser/index.d.ts.map +1 -1
  31. package/lib/browser/index.js +3 -0
  32. package/lib/browser/index.js.map +1 -1
  33. package/lib/browser/prompt-variable-contribution.d.ts +1 -1
  34. package/lib/browser/prompt-variable-contribution.d.ts.map +1 -1
  35. package/lib/browser/prompt-variable-contribution.js +1 -0
  36. package/lib/browser/prompt-variable-contribution.js.map +1 -1
  37. package/lib/browser/skill-prompt-coordinator.d.ts +9 -1
  38. package/lib/browser/skill-prompt-coordinator.d.ts.map +1 -1
  39. package/lib/browser/skill-prompt-coordinator.js +29 -14
  40. package/lib/browser/skill-prompt-coordinator.js.map +1 -1
  41. package/lib/browser/skill-prompt-coordinator.spec.d.ts +2 -0
  42. package/lib/browser/skill-prompt-coordinator.spec.d.ts.map +1 -0
  43. package/lib/browser/skill-prompt-coordinator.spec.js +98 -0
  44. package/lib/browser/skill-prompt-coordinator.spec.js.map +1 -0
  45. package/lib/browser/skill-registry-ui-bridge.d.ts +25 -0
  46. package/lib/browser/skill-registry-ui-bridge.d.ts.map +1 -0
  47. package/lib/browser/skill-registry-ui-bridge.js +20 -0
  48. package/lib/browser/skill-registry-ui-bridge.js.map +1 -0
  49. package/lib/browser/skill-service.d.ts +33 -8
  50. package/lib/browser/skill-service.d.ts.map +1 -1
  51. package/lib/browser/skill-service.js +95 -39
  52. package/lib/browser/skill-service.js.map +1 -1
  53. package/lib/browser/skill-service.spec.js +181 -2
  54. package/lib/browser/skill-service.spec.js.map +1 -1
  55. package/lib/browser/skills-variable-contribution.d.ts +4 -0
  56. package/lib/browser/skills-variable-contribution.d.ts.map +1 -1
  57. package/lib/browser/skills-variable-contribution.js +10 -1
  58. package/lib/browser/skills-variable-contribution.js.map +1 -1
  59. package/lib/browser/skills-variable-contribution.spec.js +6 -0
  60. package/lib/browser/skills-variable-contribution.spec.js.map +1 -1
  61. package/lib/browser/theia-variable-contribution.js +6 -12
  62. package/lib/browser/theia-variable-contribution.js.map +1 -1
  63. package/lib/common/ai-core-preferences.d.ts +17 -0
  64. package/lib/common/ai-core-preferences.d.ts.map +1 -1
  65. package/lib/common/ai-core-preferences.js +9 -1
  66. package/lib/common/ai-core-preferences.js.map +1 -1
  67. package/lib/common/ai-variable-resource.d.ts +2 -1
  68. package/lib/common/ai-variable-resource.d.ts.map +1 -1
  69. package/lib/common/ai-variable-resource.js +6 -1
  70. package/lib/common/ai-variable-resource.js.map +1 -1
  71. package/lib/common/capability-variable-contribution.d.ts +1 -1
  72. package/lib/common/capability-variable-contribution.d.ts.map +1 -1
  73. package/lib/common/capability-variable-contribution.js +1 -0
  74. package/lib/common/capability-variable-contribution.js.map +1 -1
  75. package/lib/common/language-model.d.ts +16 -2
  76. package/lib/common/language-model.d.ts.map +1 -1
  77. package/lib/common/language-model.js +39 -3
  78. package/lib/common/language-model.js.map +1 -1
  79. package/lib/common/language-model.spec.js +75 -0
  80. package/lib/common/language-model.spec.js.map +1 -1
  81. package/lib/common/prompt-service.d.ts.map +1 -1
  82. package/lib/common/prompt-service.js +7 -4
  83. package/lib/common/prompt-service.js.map +1 -1
  84. package/lib/common/prompt-service.spec.js +36 -0
  85. package/lib/common/prompt-service.spec.js.map +1 -1
  86. package/lib/common/skill.d.ts +31 -7
  87. package/lib/common/skill.d.ts.map +1 -1
  88. package/lib/common/skill.js +37 -14
  89. package/lib/common/skill.js.map +1 -1
  90. package/lib/common/skill.spec.js +70 -0
  91. package/lib/common/skill.spec.js.map +1 -1
  92. package/lib/common/today-variable-contribution.d.ts.map +1 -1
  93. package/lib/common/today-variable-contribution.js +1 -1
  94. package/lib/common/today-variable-contribution.js.map +1 -1
  95. package/lib/common/tool-invocation-registry.d.ts +2 -1
  96. package/lib/common/tool-invocation-registry.d.ts.map +1 -1
  97. package/lib/common/tool-invocation-registry.js +6 -1
  98. package/lib/common/tool-invocation-registry.js.map +1 -1
  99. package/lib/node/language-model-frontend-delegate.d.ts +2 -2
  100. package/lib/node/language-model-frontend-delegate.d.ts.map +1 -1
  101. package/lib/node/language-model-frontend-delegate.js +1 -0
  102. package/lib/node/language-model-frontend-delegate.js.map +1 -1
  103. package/package.json +12 -12
  104. package/src/browser/agent-plugin-ui-bridge.ts +43 -0
  105. package/src/browser/ai-core-command-contribution.ts +7 -1
  106. package/src/browser/ai-core-frontend-module.ts +11 -2
  107. package/src/browser/ai-settings-service.ts +2 -2
  108. package/src/browser/frontend-language-model-alias-registry.ts +1 -1
  109. package/src/browser/frontend-language-model-registry.ts +6 -2
  110. package/src/browser/generic-capabilities-variable-contribution.ts +4 -1
  111. package/src/browser/hide-ai-preferences-contribution.spec.ts +117 -0
  112. package/src/browser/hide-ai-preferences-contribution.ts +88 -0
  113. package/src/browser/index.ts +3 -0
  114. package/src/browser/prompt-variable-contribution.ts +3 -3
  115. package/src/browser/skill-prompt-coordinator.spec.ts +114 -0
  116. package/src/browser/skill-prompt-coordinator.ts +29 -17
  117. package/src/browser/skill-registry-ui-bridge.ts +41 -0
  118. package/src/browser/skill-service.spec.ts +229 -4
  119. package/src/browser/skill-service.ts +120 -43
  120. package/src/browser/skills-variable-contribution.spec.ts +6 -0
  121. package/src/browser/skills-variable-contribution.ts +14 -4
  122. package/src/browser/theia-variable-contribution.ts +12 -12
  123. package/src/common/ai-core-preferences.ts +19 -0
  124. package/src/common/ai-variable-resource.ts +6 -3
  125. package/src/common/capability-variable-contribution.ts +3 -3
  126. package/src/common/language-model.spec.ts +83 -0
  127. package/src/common/language-model.ts +35 -5
  128. package/src/common/prompt-service.spec.ts +43 -0
  129. package/src/common/prompt-service.ts +8 -6
  130. package/src/common/skill.spec.ts +99 -0
  131. package/src/common/skill.ts +52 -17
  132. package/src/common/today-variable-contribution.ts +2 -1
  133. package/src/common/tool-invocation-registry.ts +5 -2
  134. package/src/node/language-model-frontend-delegate.ts +3 -3
@@ -534,13 +534,21 @@ export const isCompactionResponsePart = (part: unknown): part is CompactionRespo
534
534
  export interface ToolCallTextResult { type: 'text', text: string; };
535
535
  export interface ToolCallImageResult extends Base64ImageContent { type: 'image' };
536
536
  export interface ToolCallAudioResult { type: 'audio', data: string; mimeType: string };
537
+ export interface ToolCallHtmlAppResult { type: 'html'; html: string; title?: string };
537
538
  export type ToolCallErrorKind = 'tool-not-available';
538
539
  export interface ToolCallErrorResult { type: 'error', data: string; errorKind?: ToolCallErrorKind; };
539
- export type ToolCallContentResult = ToolCallTextResult | ToolCallImageResult | ToolCallAudioResult | ToolCallErrorResult;
540
+ export type ToolCallContentResult = ToolCallTextResult | ToolCallImageResult | ToolCallAudioResult | ToolCallHtmlAppResult | ToolCallErrorResult;
540
541
  export interface ToolCallContent {
541
542
  content: ToolCallContentResult[];
542
543
  }
543
544
 
545
+ export const isToolCallHtmlAppResult = (item: unknown): item is ToolCallHtmlAppResult =>
546
+ !!(item &&
547
+ typeof item === 'object' &&
548
+ 'type' in item && (item as ToolCallHtmlAppResult).type === 'html' &&
549
+ 'html' in item &&
550
+ typeof (item as ToolCallHtmlAppResult).html === 'string');
551
+
544
552
  export const isToolCallContent = (result: unknown): result is ToolCallContent =>
545
553
  !!(result && typeof result === 'object' && 'content' in result && Array.isArray((result as ToolCallContent).content));
546
554
 
@@ -560,6 +568,26 @@ export const createToolCallError = (message: string, errorKind?: ToolCallErrorKi
560
568
  content: [errorKind ? { type: 'error', data: message, errorKind } : { type: 'error', data: message }]
561
569
  });
562
570
 
571
+ /**
572
+ * Serializes a {@link ToolCallResult} to a string suitable for sending back to the model.
573
+ *
574
+ * HTML app results are replaced with a compact placeholder so that large bundled HTML
575
+ * (e.g. Plotly charts) does not blow the model's context window. The full HTML is still
576
+ * available in the structured result for rendering in the UI (e.g. via McpAppFrame).
577
+ */
578
+ export function formatToolCallContentForModel(result: ToolCallResult): string {
579
+ if (isToolCallContent(result)) {
580
+ return result.content.map(c => {
581
+ if (c.type === 'text') { return c.text; }
582
+ if (c.type === 'html') { return `[interactive app displayed to the user${c.title ? ': ' + c.title : ''}]`; }
583
+ if (c.type === 'error') { return c.data; }
584
+ return JSON.stringify(c);
585
+ }).join('\n');
586
+ }
587
+ if (typeof result === 'string') { return result; }
588
+ return JSON.stringify(result);
589
+ }
590
+
563
591
  export type ToolCallResult = undefined | object | string | ToolCallContent;
564
592
  export interface ToolCall {
565
593
  id?: string;
@@ -703,8 +731,10 @@ export interface FrontendLanguageModelRegistry extends LanguageModelRegistry {
703
731
 
704
732
  @injectable()
705
733
  export class DefaultLanguageModelRegistryImpl implements LanguageModelRegistry {
706
- @inject(ILogger)
707
- protected logger: ILogger;
734
+
735
+ @inject(ILogger) @named('ai-core:DefaultLanguageModelRegistryImpl')
736
+ protected readonly logger: ILogger;
737
+
708
738
  @inject(ContributionProvider) @named(LanguageModelProvider)
709
739
  protected readonly languageModelContributions: ContributionProvider<LanguageModelProvider>;
710
740
 
@@ -735,7 +765,7 @@ export class DefaultLanguageModelRegistryImpl implements LanguageModelRegistry {
735
765
  addLanguageModels(models: LanguageModel[]): void {
736
766
  models.forEach(model => {
737
767
  if (this.languageModels.find(lm => lm.id === model.id)) {
738
- console.warn(`Tried to add already existing language model with id ${model.id}. The new model will be ignored.`);
768
+ this.logger.warn(`Tried to add already existing language model with id ${model.id}. The new model will be ignored.`);
739
769
  return;
740
770
  }
741
771
  this.languageModels.push(model);
@@ -762,7 +792,7 @@ export class DefaultLanguageModelRegistryImpl implements LanguageModelRegistry {
762
792
  this.languageModels.splice(index, 1);
763
793
  this.changeEmitter.fire({ models: this.languageModels });
764
794
  } else {
765
- console.warn(`Language model with id ${id} was requested to be removed, however it does not exist`);
795
+ this.logger.warn(`Language model with id ${id} was requested to be removed, however it does not exist`);
766
796
  }
767
797
  });
768
798
  }
@@ -669,3 +669,46 @@ describe('CustomAgentDescription', () => {
669
669
  });
670
670
  });
671
671
  });
672
+
673
+ describe('PromptService variable arguments', () => {
674
+ let promptService: PromptService;
675
+
676
+ beforeEach(() => {
677
+ const container = new Container();
678
+ container.bind<PromptService>(PromptService).to(PromptServiceImpl).inSingletonScope();
679
+
680
+ const variableService = new DefaultAIVariableService({ getContributions: () => [] });
681
+ (variableService as unknown as Record<string, unknown>)['logger'] = sinon.createStubInstance(Logger);
682
+ // A resolver that echoes what it was given, so that the test asserts on the split of the
683
+ // reference into variable name and argument rather than on any resolver behaviour.
684
+ const fileVariable = { id: 'file', name: 'file', description: 'Echoes its argument' };
685
+ variableService.registerResolver(fileVariable, {
686
+ canResolve: () => 100,
687
+ resolve: async request => ({ variable: fileVariable, value: `arg=${request.arg}` })
688
+ });
689
+ container.bind<AIVariableService>(AIVariableService).toConstantValue(variableService);
690
+ container.bind<ILogger>(ILogger).toConstantValue(new MockLogger);
691
+
692
+ promptService = container.get<PromptService>(PromptService);
693
+ });
694
+
695
+ it('keeps the whole argument of a variable reference whose argument contains further colons', async () => {
696
+ // Regression test: the reference used to be split with `split(':', 2)`, which truncates, so a
697
+ // Windows path lost everything from its drive-letter colon onwards.
698
+ promptService.addBuiltInPromptFragment({ id: 'windows-path', template: 'Read {{file:C:\\some\\path}}' });
699
+ const prompt = await promptService.getResolvedPromptFragment('windows-path');
700
+ expect(prompt?.text).to.equal('Read arg=C:\\some\\path');
701
+ });
702
+
703
+ it('keeps a single-colon argument intact', async () => {
704
+ promptService.addBuiltInPromptFragment({ id: 'simple-arg', template: 'Read {{file:workspace/path/name.ext}}' });
705
+ const prompt = await promptService.getResolvedPromptFragment('simple-arg');
706
+ expect(prompt?.text).to.equal('Read arg=workspace/path/name.ext');
707
+ });
708
+
709
+ it('resolves a reference without an argument with an undefined argument', async () => {
710
+ promptService.addBuiltInPromptFragment({ id: 'no-arg', template: 'Read {{file}}' });
711
+ const prompt = await promptService.getResolvedPromptFragment('no-arg');
712
+ expect(prompt?.text).to.equal('Read arg=undefined');
713
+ });
714
+ });
@@ -15,7 +15,7 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { Event, Emitter, URI, ILogger, DisposableCollection } from '@theia/core';
18
- import { inject, injectable, optional, postConstruct } from '@theia/core/shared/inversify';
18
+ import { inject, injectable, optional, postConstruct, named } from '@theia/core/shared/inversify';
19
19
  import { AIVariableArg, AIVariableContext, AIVariableService, createAIResolveVariableCache, ResolvedAIVariable } from './variable-service';
20
20
  import { ToolInvocationRegistry } from './tool-invocation-registry';
21
21
  import { toolRequestToPromptText } from './language-model-util';
@@ -607,7 +607,7 @@ export interface PromptService {
607
607
 
608
608
  @injectable()
609
609
  export class PromptServiceImpl implements PromptService {
610
- @inject(ILogger)
610
+ @inject(ILogger) @named('ai-core:PromptServiceImpl')
611
611
  protected readonly logger: ILogger;
612
612
 
613
613
  @inject(AISettingsService) @optional()
@@ -983,10 +983,12 @@ export class PromptServiceImpl implements PromptService {
983
983
  let variableName = variableAndArg;
984
984
  let argument: string | undefined;
985
985
 
986
- const parts = variableAndArg.split(':', 2);
987
- if (parts.length > 1) {
988
- variableName = parts[0];
989
- argument = parts[1];
986
+ // First colon only, keeping the whole remainder: `split(':', 2)` truncates instead, which
987
+ // mangles every argument containing a colon, e.g. `{{file:C:\some\path}}`.
988
+ const separatorIndex = variableAndArg.indexOf(':');
989
+ if (separatorIndex >= 0) {
990
+ variableName = variableAndArg.substring(0, separatorIndex);
991
+ argument = variableAndArg.substring(separatorIndex + 1);
990
992
  }
991
993
 
992
994
  let replacementValue: string;
@@ -17,7 +17,10 @@
17
17
  import { expect } from 'chai';
18
18
  import {
19
19
  SKILL_FILE_NAME,
20
+ Skill,
20
21
  SkillDescription,
22
+ SkillDirectoryEntry,
23
+ combineSkillDirectories,
21
24
  isValidSkillName,
22
25
  validateSkillDescription
23
26
  } from './skill';
@@ -218,5 +221,101 @@ describe('Skill Types', () => {
218
221
  const errors = validateSkillDescription(description, 'my-skill');
219
222
  expect(errors).to.be.empty;
220
223
  });
224
+
225
+ it('does not require the frontmatter name of an owned skill to carry its qualifier', () => {
226
+ // Qualification is layered on top of the name; it never changes what an artifact has to
227
+ // write into its SKILL.md, which would change the artifact's content hash.
228
+ const description: SkillDescription = {
229
+ name: 'query-builder',
230
+ description: 'Build SQL'
231
+ };
232
+ expect(validateSkillDescription(description, 'query-builder')).to.be.empty;
233
+ expect(Skill.qualifyName(description.name, 'bq')).to.equal('bq:query-builder');
234
+ });
235
+ });
236
+
237
+ describe('Skill.qualifyName', () => {
238
+ it('returns the plain name when there is no qualifier', () => {
239
+ expect(Skill.qualifyName('code-review')).to.equal('code-review');
240
+ });
241
+
242
+ it('prefixes the name with the qualifier', () => {
243
+ expect(Skill.qualifyName('query-builder', 'bigquery-data-analytics')).to.equal('bigquery-data-analytics:query-builder');
244
+ });
245
+ });
246
+
247
+ describe('Skill.qualifierOf', () => {
248
+ const skill = (name: string, qualifiedName: string): Skill =>
249
+ ({ name, description: 'd', location: '/skills/' + name + '/SKILL.md', qualifiedName });
250
+
251
+ it('returns undefined for a skill from a built-in root', () => {
252
+ expect(Skill.qualifierOf(skill('code-review', 'code-review'))).to.be.undefined;
253
+ });
254
+
255
+ it('returns the qualifier of a contributed skill', () => {
256
+ expect(Skill.qualifierOf(skill('query-builder', 'bq:query-builder'))).to.equal('bq');
257
+ });
258
+
259
+ it('round-trips with qualifyName', () => {
260
+ const qualifiedName = Skill.qualifyName('query-builder', 'io.github.acme_tools');
261
+ expect(Skill.qualifierOf(skill('query-builder', qualifiedName))).to.equal('io.github.acme_tools');
262
+ });
263
+ });
264
+
265
+ describe('combineSkillDirectories with plugin directories', () => {
266
+ const pluginEntry: SkillDirectoryEntry = {
267
+ path: '/home/user/.agents/plugins/io.example.bq/skills',
268
+ tier: 'plugin',
269
+ qualifier: 'bq'
270
+ };
271
+
272
+ it('appends plugin directories after every user-controlled tier', () => {
273
+ const result = combineSkillDirectories(
274
+ ['/workspace/.prompts/skills'],
275
+ ['/custom/skills'],
276
+ ['/home/user/.agents/skills'],
277
+ [pluginEntry]
278
+ );
279
+
280
+ expect(result).to.deep.equal([
281
+ { path: '/workspace/.prompts/skills', tier: 'workspace' },
282
+ { path: '/custom/skills', tier: 'configured' },
283
+ { path: '/home/user/.agents/skills', tier: 'default' },
284
+ pluginEntry
285
+ ]);
286
+ });
287
+
288
+ it('keeps the qualifier of a plugin directory', () => {
289
+ const result = combineSkillDirectories([], [], [], [pluginEntry]);
290
+ expect(result[0].qualifier).to.equal('bq');
291
+ });
292
+
293
+ it('drops a plugin directory whose path a user-controlled tier already claimed, so the user wins the collision', () => {
294
+ const result = combineSkillDirectories(
295
+ [],
296
+ [pluginEntry.path],
297
+ [],
298
+ [pluginEntry]
299
+ );
300
+
301
+ expect(result).to.deep.equal([
302
+ { path: pluginEntry.path, tier: 'configured' }
303
+ ]);
304
+ });
305
+
306
+ it('deduplicates two plugin directories with the same path, keeping the first', () => {
307
+ const second: SkillDirectoryEntry = {
308
+ ...pluginEntry,
309
+ qualifier: 'other'
310
+ };
311
+ const result = combineSkillDirectories([], [], [], [pluginEntry, second]);
312
+ expect(result).to.deep.equal([pluginEntry]);
313
+ });
314
+
315
+ it('behaves exactly as before when no plugin directories are passed', () => {
316
+ expect(combineSkillDirectories(['/workspace/skills'], [], [])).to.deep.equal([
317
+ { path: '/workspace/skills', tier: 'workspace' }
318
+ ]);
319
+ });
221
320
  });
222
321
  });
@@ -87,6 +87,32 @@ export namespace SkillDescription {
87
87
  export interface Skill extends SkillDescription {
88
88
  /** Absolute file path to the SKILL.md file */
89
89
  location: string;
90
+ /**
91
+ * What the runtime and the model address this skill by: `name` for a built-in root,
92
+ * `<qualifier>:<name>` for a contributed one. Unlike `name` this is unique across all discovered
93
+ * skills, which is why it keys the registry.
94
+ */
95
+ qualifiedName: string;
96
+ }
97
+
98
+ export namespace Skill {
99
+ /**
100
+ * Never changes what is written in `SKILL.md` - the frontmatter `name` must still equal the
101
+ * skill's directory name; qualification is layered on top.
102
+ */
103
+ export function qualifyName(name: string, qualifier?: string): string {
104
+ return qualifier ? `${qualifier}:${name}` : name;
105
+ }
106
+
107
+ /**
108
+ * The qualifier of {@link Skill.qualifiedName}, or `undefined` for a skill from a built-in root.
109
+ * Unambiguous because neither part can contain a colon: a skill name is validated as lowercase
110
+ * kebab-case, and a qualifier has to be a single path segment.
111
+ */
112
+ export function qualifierOf(skill: Skill): string | undefined {
113
+ const separatorIndex = skill.qualifiedName.indexOf(':');
114
+ return separatorIndex < 0 ? undefined : skill.qualifiedName.substring(0, separatorIndex);
115
+ }
90
116
  }
91
117
 
92
118
  /**
@@ -144,10 +170,8 @@ export function parseSkillFile(content: string): { metadata: SkillDescription |
144
170
  return { metadata, content: body };
145
171
  }
146
172
 
147
- /**
148
- * Provenance tier of a skill directory, used to dispatch tier-specific processing.
149
- */
150
- export type SkillDirectoryTier = 'workspace' | 'configured' | 'default';
173
+ /** Provenance tier of a skill directory, used to dispatch tier-specific processing. */
174
+ export type SkillDirectoryTier = 'workspace' | 'configured' | 'default' | 'plugin';
151
175
 
152
176
  /**
153
177
  * A skill directory paired with the tier it originates from.
@@ -157,32 +181,43 @@ export interface SkillDirectoryEntry {
157
181
  path: string;
158
182
  /** Tier the directory belongs to */
159
183
  tier: SkillDirectoryTier;
184
+ /**
185
+ * Prefixes the names of skills beneath this root, as `<qualifier>:<name>`. Skills are addressed
186
+ * by one global name, so two artifacts shipping the same skill name could not otherwise coexist -
187
+ * and renaming the directory instead would change the artifact's content hash. Must be a single
188
+ * path segment, so that it can be told from the skill name it is joined to.
189
+ */
190
+ qualifier?: string;
160
191
  }
161
192
 
162
193
  /**
163
- * Combines skill directories with proper priority ordering and provenance.
164
- * Workspace directories have highest priority, followed by configured directories, then defaults.
194
+ * Combines skill directories with proper priority ordering and provenance: workspace, then
195
+ * configured, then defaults, then installed artifacts - so a directory the user controls always wins.
165
196
  * First occurrence of a path wins on duplicates; later occurrences (regardless of tier) are dropped.
166
197
  */
167
198
  export function combineSkillDirectories(
168
199
  workspaceSkillsDirs: string[],
169
200
  configuredDirectories: string[],
170
- defaultSkillsDirs: string[]
201
+ defaultSkillsDirs: string[],
202
+ pluginSkillsDirs: SkillDirectoryEntry[] = []
171
203
  ): SkillDirectoryEntry[] {
172
204
  const seen = new Set<string>();
173
205
  const result: SkillDirectoryEntry[] = [];
174
- const tiers: Array<{ dirs: string[]; tier: SkillDirectoryTier }> = [
175
- { dirs: workspaceSkillsDirs, tier: 'workspace' },
176
- { dirs: configuredDirectories, tier: 'configured' },
177
- { dirs: defaultSkillsDirs, tier: 'default' }
206
+ const candidates: SkillDirectoryEntry[] = [
207
+ ...toSkillDirectoryEntries(workspaceSkillsDirs, 'workspace'),
208
+ ...toSkillDirectoryEntries(configuredDirectories, 'configured'),
209
+ ...toSkillDirectoryEntries(defaultSkillsDirs, 'default'),
210
+ ...pluginSkillsDirs
178
211
  ];
179
- for (const { dirs, tier } of tiers) {
180
- for (const dir of dirs) {
181
- if (!seen.has(dir)) {
182
- seen.add(dir);
183
- result.push({ path: dir, tier });
184
- }
212
+ for (const candidate of candidates) {
213
+ if (!seen.has(candidate.path)) {
214
+ seen.add(candidate.path);
215
+ result.push(candidate);
185
216
  }
186
217
  }
187
218
  return result;
188
219
  }
220
+
221
+ function toSkillDirectoryEntries(dirs: string[], tier: SkillDirectoryTier): SkillDirectoryEntry[] {
222
+ return dirs.map(dir => ({ path: dir, tier }));
223
+ }
@@ -24,7 +24,8 @@ export namespace TodayVariableArgs {
24
24
 
25
25
  export const TODAY_VARIABLE: AIVariable = {
26
26
  id: 'today-provider',
27
- description: nls.localize('theia/ai/core/todayVariable/description', 'Does something for today'),
27
+ description: nls.localize('theia/ai/core/todayVariable/dateDescription',
28
+ 'The current date. Resolves to a human-readable date string by default, or to ISO 8601 or Unix seconds via the format argument.'),
28
29
  name: 'today',
29
30
  args: [
30
31
  {
@@ -16,7 +16,7 @@
16
16
 
17
17
  import { inject, injectable, named, postConstruct, interfaces } from '@theia/core/shared/inversify';
18
18
  import { ToolRequest } from './language-model';
19
- import { ContributionProvider, Emitter, Event } from '@theia/core';
19
+ import { ContributionProvider, Emitter, Event, ILogger } from '@theia/core';
20
20
 
21
21
  export const ToolInvocationRegistry = Symbol('ToolInvocationRegistry');
22
22
 
@@ -99,6 +99,9 @@ export class ToolInvocationRegistryImpl implements ToolInvocationRegistry {
99
99
  @named(ToolProvider)
100
100
  private providers: ContributionProvider<ToolProvider>;
101
101
 
102
+ @inject(ILogger) @named('ai-core:ToolInvocationRegistryImpl')
103
+ protected readonly logger: ILogger;
104
+
102
105
  @postConstruct()
103
106
  init(): void {
104
107
  this.providers.getContributions().forEach(provider => {
@@ -135,7 +138,7 @@ export class ToolInvocationRegistryImpl implements ToolInvocationRegistry {
135
138
 
136
139
  registerTool(tool: ToolRequest): void {
137
140
  if (this.tools.has(tool.id)) {
138
- console.warn(`Function with id ${tool.id} is already registered.`);
141
+ this.logger.warn(`Function with id ${tool.id} is already registered.`);
139
142
  } else {
140
143
  this.tools.set(tool.id, tool);
141
144
  this.onDidChangeEmitter.fire();
@@ -14,7 +14,7 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
 
17
- import { inject, injectable } from '@theia/core/shared/inversify';
17
+ import { inject, injectable, named } from '@theia/core/shared/inversify';
18
18
  import { CancellationToken, CancellationTokenSource, ILogger, generateUuid } from '@theia/core';
19
19
  import {
20
20
  LanguageModelMetaData,
@@ -53,8 +53,8 @@ export class LanguageModelFrontendDelegateImpl implements LanguageModelFrontendD
53
53
  @inject(LanguageModelRegistry)
54
54
  private registry: LanguageModelRegistry;
55
55
 
56
- @inject(ILogger)
57
- private logger: ILogger;
56
+ @inject(ILogger) @named('ai-core:LanguageModelFrontendDelegateImpl')
57
+ protected readonly logger: ILogger;
58
58
 
59
59
  private frontendDelegateClient: LanguageModelDelegateClient;
60
60
  private requestCancellationTokenMap: Map<string, CancellationTokenSource> = new Map();