@theia/ai-core 1.74.0-next.5 → 1.74.0-next.53
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/lib/browser/ai-configuration-service-impl.d.ts +32 -0
- package/lib/browser/ai-configuration-service-impl.d.ts.map +1 -0
- package/lib/browser/ai-configuration-service-impl.js +120 -0
- package/lib/browser/ai-configuration-service-impl.js.map +1 -0
- package/lib/browser/ai-configuration-service-impl.spec.d.ts +2 -0
- package/lib/browser/ai-configuration-service-impl.spec.d.ts.map +1 -0
- package/lib/browser/ai-configuration-service-impl.spec.js +214 -0
- package/lib/browser/ai-configuration-service-impl.spec.js.map +1 -0
- package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-core-frontend-module.js +4 -0
- package/lib/browser/ai-core-frontend-module.js.map +1 -1
- package/lib/browser/ai-settings-service.d.ts +2 -5
- package/lib/browser/ai-settings-service.d.ts.map +1 -1
- package/lib/browser/ai-settings-service.js +8 -15
- package/lib/browser/ai-settings-service.js.map +1 -1
- package/lib/browser/frontend-language-model-alias-registry.d.ts +2 -4
- package/lib/browser/frontend-language-model-alias-registry.d.ts.map +1 -1
- package/lib/browser/frontend-language-model-alias-registry.js +11 -16
- package/lib/browser/frontend-language-model-alias-registry.js.map +1 -1
- package/lib/browser/frontend-language-model-service.d.ts +2 -3
- package/lib/browser/frontend-language-model-service.d.ts.map +1 -1
- package/lib/browser/frontend-language-model-service.js +6 -6
- package/lib/browser/frontend-language-model-service.js.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.d.ts +38 -0
- package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.js +189 -76
- package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
- package/lib/browser/frontend-prompt-customization-service.spec.js +103 -0
- package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -1
- package/lib/browser/index.d.ts +1 -0
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +1 -0
- package/lib/browser/index.js.map +1 -1
- package/lib/browser/trust-aware-preference-reader.d.ts +13 -1
- package/lib/browser/trust-aware-preference-reader.d.ts.map +1 -1
- package/lib/browser/trust-aware-preference-reader.js +21 -2
- package/lib/browser/trust-aware-preference-reader.js.map +1 -1
- package/lib/browser/trust-aware-preference-reader.spec.js +26 -0
- package/lib/browser/trust-aware-preference-reader.spec.js.map +1 -1
- package/lib/common/ai-configuration-service.d.ts +104 -0
- package/lib/common/ai-configuration-service.d.ts.map +1 -0
- package/lib/common/ai-configuration-service.js +20 -0
- package/lib/common/ai-configuration-service.js.map +1 -0
- package/lib/common/ai-core-preferences.d.ts +4 -0
- package/lib/common/ai-core-preferences.d.ts.map +1 -1
- package/lib/common/ai-core-preferences.js +20 -1
- package/lib/common/ai-core-preferences.js.map +1 -1
- 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.d.ts +56 -2
- package/lib/common/language-model.d.ts.map +1 -1
- package/lib/common/language-model.js +53 -3
- package/lib/common/language-model.js.map +1 -1
- package/lib/common/language-model.spec.js +75 -0
- package/lib/common/language-model.spec.js.map +1 -1
- package/lib/common/prompt-service.d.ts +13 -3
- package/lib/common/prompt-service.d.ts.map +1 -1
- package/lib/common/prompt-service.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/package.json +11 -11
- package/src/browser/ai-configuration-service-impl.spec.ts +254 -0
- package/src/browser/ai-configuration-service-impl.ts +120 -0
- package/src/browser/ai-core-frontend-module.ts +7 -1
- package/src/browser/ai-settings-service.ts +8 -16
- package/src/browser/frontend-language-model-alias-registry.ts +12 -16
- package/src/browser/frontend-language-model-service.ts +5 -6
- package/src/browser/frontend-prompt-customization-service.spec.ts +130 -1
- package/src/browser/frontend-prompt-customization-service.ts +204 -84
- package/src/browser/index.ts +1 -0
- package/src/browser/trust-aware-preference-reader.spec.ts +29 -0
- package/src/browser/trust-aware-preference-reader.ts +23 -3
- package/src/common/ai-configuration-service.ts +128 -0
- package/src/common/ai-core-preferences.ts +23 -1
- package/src/common/index.ts +1 -0
- package/src/common/language-model.spec.ts +98 -1
- package/src/common/language-model.ts +107 -4
- package/src/common/prompt-service.ts +14 -3
- package/src/node/backend-language-model-registry.ts +1 -0
|
@@ -23,9 +23,10 @@ FrontendApplicationConfigProvider.set({});
|
|
|
23
23
|
import 'reflect-metadata';
|
|
24
24
|
|
|
25
25
|
import { expect } from 'chai';
|
|
26
|
-
import { dump } from 'js-yaml';
|
|
26
|
+
import { dump, load } from 'js-yaml';
|
|
27
27
|
import URI from '@theia/core/lib/common/uri';
|
|
28
28
|
import { parseTemplateWithMetadata, ParsedTemplate } from './prompttemplate-parser';
|
|
29
|
+
import { serializeFrontmatter } from '../common/frontmatter';
|
|
29
30
|
import {
|
|
30
31
|
CustomizationSource, DefaultPromptFragmentCustomizationService, CUSTOM_AGENTS_DIRECTORY, CUSTOM_AGENT_FILE_NAME
|
|
31
32
|
} from './frontend-prompt-customization-service';
|
|
@@ -518,6 +519,12 @@ describe('DefaultPromptFragmentCustomizationService - customAgents.yml migration
|
|
|
518
519
|
protected override getTemplatesDirectoryURI(): Promise<URI> {
|
|
519
520
|
return Promise.resolve(this.templatesDir);
|
|
520
521
|
}
|
|
522
|
+
testParse(raw: string): unknown {
|
|
523
|
+
return this.parseCustomAgentsYaml(raw);
|
|
524
|
+
}
|
|
525
|
+
testUnfold(raw: string): string {
|
|
526
|
+
return this.unfoldPromptBlockScalars(raw);
|
|
527
|
+
}
|
|
521
528
|
}
|
|
522
529
|
|
|
523
530
|
const scope = new URI('file:///ws/.prompts');
|
|
@@ -617,6 +624,128 @@ describe('DefaultPromptFragmentCustomizationService - customAgents.yml migration
|
|
|
617
624
|
// ...and because a backup already exists, the yaml is left untouched for inspection.
|
|
618
625
|
expect(await fileService.exists(yamlURI)).to.be.true;
|
|
619
626
|
});
|
|
627
|
+
|
|
628
|
+
describe('folded block-scalar prompts', () => {
|
|
629
|
+
// A folded YAML scalar (`>-`) collapses single newlines into spaces, which merges markdown
|
|
630
|
+
// headings into the paragraph below them. Migration must recover the original line breaks.
|
|
631
|
+
const foldedYaml =
|
|
632
|
+
'- id: rel\n' +
|
|
633
|
+
' name: Rel\n' +
|
|
634
|
+
' description: Release agent\n' +
|
|
635
|
+
' defaultLLM: default/code\n' +
|
|
636
|
+
' prompt: >-\n' +
|
|
637
|
+
' ## Task\n' +
|
|
638
|
+
' Do the thing.\n' +
|
|
639
|
+
'\n' +
|
|
640
|
+
' ## Workflow\n' +
|
|
641
|
+
' Step one.\n';
|
|
642
|
+
|
|
643
|
+
it('migrates a folded prompt with its headings on separate lines', async () => {
|
|
644
|
+
fileService.write(yamlURI, foldedYaml);
|
|
645
|
+
|
|
646
|
+
const [report] = await service.migrateCustomAgentsYaml();
|
|
647
|
+
|
|
648
|
+
expect(report.migrated).to.equal(1);
|
|
649
|
+
const content = fileService.content(agentMd('rel'))!;
|
|
650
|
+
// js-yaml would have folded these into '## Task Do the thing.'; the fix keeps them split.
|
|
651
|
+
expect(content).to.contain('## Task\nDo the thing.');
|
|
652
|
+
expect(content).to.contain('## Workflow\nStep one.');
|
|
653
|
+
expect(content).to.not.contain('## Task Do the thing.');
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('loads an un-migrated folded prompt with separated headings at runtime', async () => {
|
|
657
|
+
fileService.write(yamlURI, foldedYaml);
|
|
658
|
+
|
|
659
|
+
const [agent] = await service.getCustomAgents();
|
|
660
|
+
|
|
661
|
+
expect(agent.prompt).to.contain('## Task\nDo the thing.');
|
|
662
|
+
expect(agent.prompt).to.not.contain('## Task Do the thing.');
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('leaves plain and quoted prompt values untouched', () => {
|
|
666
|
+
const plain = 'prompt: just a short prompt';
|
|
667
|
+
const quoted = 'prompt: "> not a header"';
|
|
668
|
+
expect(service.testUnfold(plain)).to.equal(plain);
|
|
669
|
+
expect(service.testUnfold(quoted)).to.equal(quoted);
|
|
670
|
+
// Folded headers (with or without indicators) become literal.
|
|
671
|
+
expect(service.testUnfold('prompt: >-')).to.equal('prompt: |-');
|
|
672
|
+
expect(service.testUnfold('prompt: >2-')).to.equal('prompt: |2-');
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
|
|
676
|
+
describe('correcting already-migrated agents', () => {
|
|
677
|
+
// Reproduces the bytes the v1.73.0 migration wrote: parse the backup the old (folded) way and
|
|
678
|
+
// serialize it with the same frontmatter format the migration used.
|
|
679
|
+
const buggyAgentMd = (raw: string, id: string): string => {
|
|
680
|
+
const entry = (load(raw) as Array<{ id: string; name: string; description: string; prompt: string; defaultLLM: string }>).find(e => e.id === id)!;
|
|
681
|
+
return serializeFrontmatter({ name: entry.name, description: entry.description, defaultLLM: entry.defaultLLM }, entry.prompt);
|
|
682
|
+
};
|
|
683
|
+
const foldedYaml =
|
|
684
|
+
'- id: rel\n' +
|
|
685
|
+
' name: Rel\n' +
|
|
686
|
+
' description: Release agent\n' +
|
|
687
|
+
' defaultLLM: default/code\n' +
|
|
688
|
+
' prompt: >-\n' +
|
|
689
|
+
' ## Task\n' +
|
|
690
|
+
' Do the thing.\n';
|
|
691
|
+
|
|
692
|
+
it('rewrites an untouched agent.md to recover folded headings', async () => {
|
|
693
|
+
// Simulate the post-v1.73.0 state: yaml already backed up, agent.md written with merged headings.
|
|
694
|
+
fileService.write(backupURI, foldedYaml);
|
|
695
|
+
fileService.write(agentMd('rel'), buggyAgentMd(foldedYaml, 'rel'));
|
|
696
|
+
|
|
697
|
+
const [report] = await service.migrateCustomAgentsYaml();
|
|
698
|
+
|
|
699
|
+
expect(report.corrected).to.equal(1);
|
|
700
|
+
expect(fileService.content(agentMd('rel'))!).to.contain('## Task\nDo the thing.');
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
it('does not touch an agent.md the user edited after migration', async () => {
|
|
704
|
+
fileService.write(backupURI, foldedYaml);
|
|
705
|
+
const edited = buggyAgentMd(foldedYaml, 'rel') + '\n\nManually added section.';
|
|
706
|
+
fileService.write(agentMd('rel'), edited);
|
|
707
|
+
|
|
708
|
+
const reports = await service.migrateCustomAgentsYaml();
|
|
709
|
+
|
|
710
|
+
// Nothing is migrated and the edited file is not eligible for correction, so no report.
|
|
711
|
+
expect(reports).to.have.lengthOf(0);
|
|
712
|
+
expect(fileService.content(agentMd('rel'))).to.equal(edited);
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
it('reports nothing to do when the backup prompt has no folded headings', async () => {
|
|
716
|
+
fileService.write(backupURI, dump(agents));
|
|
717
|
+
fileService.write(agentMd('foo'), buggyAgentMd(dump(agents), 'foo'));
|
|
718
|
+
|
|
719
|
+
const reports = await service.migrateCustomAgentsYaml();
|
|
720
|
+
|
|
721
|
+
// Nothing to migrate and nothing correctable -> no report for the scope.
|
|
722
|
+
expect(reports).to.have.lengthOf(0);
|
|
723
|
+
});
|
|
724
|
+
});
|
|
725
|
+
|
|
726
|
+
describe('hasPendingCustomAgentMigration', () => {
|
|
727
|
+
it('is true while a legacy customAgents.yml exists', async () => {
|
|
728
|
+
fileService.write(yamlURI, dump(agents));
|
|
729
|
+
expect(await service.hasPendingCustomAgentMigration()).to.be.true;
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
it('is true when a backup has correctable agents', async () => {
|
|
733
|
+
const foldedYaml =
|
|
734
|
+
'- id: rel\n name: Rel\n description: d\n defaultLLM: default/code\n prompt: >-\n ## Task\n Body.\n';
|
|
735
|
+
fileService.write(backupURI, foldedYaml);
|
|
736
|
+
const entry = (load(foldedYaml) as Array<{ name: string; description: string; prompt: string; defaultLLM: string }>)[0];
|
|
737
|
+
fileService.write(agentMd('rel'), serializeFrontmatter({ name: entry.name, description: entry.description, defaultLLM: entry.defaultLLM }, entry.prompt));
|
|
738
|
+
|
|
739
|
+
expect(await service.hasPendingCustomAgentMigration()).to.be.true;
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
it('is false once everything is migrated and correct', async () => {
|
|
743
|
+
fileService.write(yamlURI, dump(agents));
|
|
744
|
+
await service.migrateCustomAgentsYaml();
|
|
745
|
+
|
|
746
|
+
expect(await service.hasPendingCustomAgentMigration()).to.be.false;
|
|
747
|
+
});
|
|
748
|
+
});
|
|
620
749
|
});
|
|
621
750
|
|
|
622
751
|
describe('DefaultPromptFragmentCustomizationService - custom agent scopes', () => {
|
|
@@ -1669,7 +1669,7 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
1669
1669
|
|
|
1670
1670
|
try {
|
|
1671
1671
|
const fileContent = await this.fileService.read(customAgentYamlUri, { encoding: 'utf-8' });
|
|
1672
|
-
const doc =
|
|
1672
|
+
const doc = this.parseCustomAgentsYaml(fileContent.value);
|
|
1673
1673
|
|
|
1674
1674
|
if (!Array.isArray(doc) || !doc.every(entry => CustomAgentDescription.is(entry))) {
|
|
1675
1675
|
this.logger.debug(`Invalid customAgents.yml file content in ${directoryURI.toString()}`);
|
|
@@ -1736,6 +1736,47 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
1736
1736
|
return fileURI;
|
|
1737
1737
|
}
|
|
1738
1738
|
|
|
1739
|
+
/**
|
|
1740
|
+
* Parse a `customAgents.yml` document, preserving newlines in folded-block-scalar prompts.
|
|
1741
|
+
*
|
|
1742
|
+
* `js-yaml` folds the single newlines of a folded scalar (`>` / `>-`) into spaces, which merges
|
|
1743
|
+
* a markdown heading (`## Task`) into the paragraph below it. Markdown is newline-sensitive, so
|
|
1744
|
+
* we re-interpret folded `prompt:` scalars as literal (`|`) before parsing. Only the block-scalar
|
|
1745
|
+
* header is rewritten; quoted, plain and already-literal values are left untouched.
|
|
1746
|
+
*
|
|
1747
|
+
* Known limitation: prompts stored as a plain or quoted multi-line scalar (rare) still fold.
|
|
1748
|
+
*/
|
|
1749
|
+
protected parseCustomAgentsYaml(content: string): unknown {
|
|
1750
|
+
return load(this.unfoldPromptBlockScalars(content));
|
|
1751
|
+
}
|
|
1752
|
+
|
|
1753
|
+
/**
|
|
1754
|
+
* Rewrite a folded `prompt:` block-scalar header (`>`, `>-`, `>+`, `>2-`, ...) to its literal
|
|
1755
|
+
* equivalent (`|`...). The lookahead ensures only a block-scalar header is matched (indentation
|
|
1756
|
+
* and chomping indicators plus an optional trailing comment), so plain/quoted prompt values and
|
|
1757
|
+
* any `>` inside the prompt body are never touched.
|
|
1758
|
+
*/
|
|
1759
|
+
protected unfoldPromptBlockScalars(content: string): string {
|
|
1760
|
+
return content.replace(/^([ \t]*prompt:[ \t]*)>(?=[-+0-9]*[ \t]*(?:#.*)?$)/mg, '$1|');
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
/**
|
|
1764
|
+
* Returns `true` if migration would write anything: a scope still holds a legacy
|
|
1765
|
+
* `customAgents.yml`, or a previously migrated scope has `agent.md` files that can be corrected
|
|
1766
|
+
* (see {@link computeScopeCorrections}). Read-only; used to decide whether to prompt the user.
|
|
1767
|
+
*/
|
|
1768
|
+
async hasPendingCustomAgentMigration(): Promise<boolean> {
|
|
1769
|
+
for (const scope of await this.getCustomAgentScopes()) {
|
|
1770
|
+
if (await this.fileService.exists(scope.resolve('customAgents.yml'))) {
|
|
1771
|
+
return true;
|
|
1772
|
+
}
|
|
1773
|
+
if ((await this.computeScopeCorrections(scope)).length > 0) {
|
|
1774
|
+
return true;
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
return false;
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1739
1780
|
/**
|
|
1740
1781
|
* Auto-migrate every legacy `customAgents.yml` reachable from the configured scopes to the new
|
|
1741
1782
|
* `agents/<id>/agent.md` layout. The original content is never deleted:
|
|
@@ -1769,7 +1810,7 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
1769
1810
|
reports.push(report);
|
|
1770
1811
|
}
|
|
1771
1812
|
}
|
|
1772
|
-
if (reports.some(r => r.migrated > 0)) {
|
|
1813
|
+
if (reports.some(r => r.migrated > 0 || r.corrected > 0)) {
|
|
1773
1814
|
this.onDidChangeCustomAgentsEmitter.fire();
|
|
1774
1815
|
}
|
|
1775
1816
|
return reports;
|
|
@@ -1777,116 +1818,193 @@ export class DefaultPromptFragmentCustomizationService implements PromptFragment
|
|
|
1777
1818
|
|
|
1778
1819
|
protected async migrateSingleScope(scopeDir: URI): Promise<MigrationReport | undefined> {
|
|
1779
1820
|
const yamlURI = scopeDir.resolve('customAgents.yml');
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
}
|
|
1783
|
-
this.logger.info(`Migrating custom agents from ${yamlURI.toString()}`);
|
|
1784
|
-
let entries: CustomAgentDescription[];
|
|
1785
|
-
try {
|
|
1786
|
-
const fileContent = await this.fileService.read(yamlURI, { encoding: 'utf-8' });
|
|
1787
|
-
const doc = load(fileContent.value);
|
|
1788
|
-
if (!Array.isArray(doc) || !doc.every(CustomAgentDescription.is)) {
|
|
1789
|
-
this.logger.warn(`Skipping migration of ${yamlURI.toString()}: file content is not a valid CustomAgentDescription[]`);
|
|
1790
|
-
return { scope: scopeDir, yamlURI, migrated: 0, alreadyPresent: 0, failed: 0, yamlBackedUp: false, promptOverridesMigrated: 0 };
|
|
1791
|
-
}
|
|
1792
|
-
entries = doc;
|
|
1793
|
-
} catch (e) {
|
|
1794
|
-
this.logger.warn(`Skipping migration of ${yamlURI.toString()}: ${e.message}`);
|
|
1795
|
-
return { scope: scopeDir, yamlURI, migrated: 0, alreadyPresent: 0, failed: 0, yamlBackedUp: false, promptOverridesMigrated: 0 };
|
|
1796
|
-
}
|
|
1821
|
+
const backupURI = scopeDir.resolve('customAgents.yml.bak');
|
|
1822
|
+
const yamlExists = await this.fileService.exists(yamlURI);
|
|
1797
1823
|
|
|
1798
1824
|
let migrated = 0;
|
|
1799
1825
|
let alreadyPresent = 0;
|
|
1800
1826
|
let failed = 0;
|
|
1801
1827
|
let promptOverridesMigrated = 0;
|
|
1828
|
+
let yamlBackedUp = false;
|
|
1802
1829
|
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1830
|
+
const emptyReport = (): MigrationReport => ({
|
|
1831
|
+
scope: scopeDir, yamlURI, migrated: 0, alreadyPresent: 0, failed: 0,
|
|
1832
|
+
yamlBackedUp: false, promptOverridesMigrated: 0, corrected: 0
|
|
1833
|
+
});
|
|
1806
1834
|
|
|
1807
|
-
|
|
1808
|
-
|
|
1835
|
+
if (yamlExists) {
|
|
1836
|
+
this.logger.info(`Migrating custom agents from ${yamlURI.toString()}`);
|
|
1837
|
+
let entries: CustomAgentDescription[];
|
|
1838
|
+
try {
|
|
1839
|
+
const fileContent = await this.fileService.read(yamlURI, { encoding: 'utf-8' });
|
|
1840
|
+
const doc = this.parseCustomAgentsYaml(fileContent.value);
|
|
1841
|
+
if (!Array.isArray(doc) || !doc.every(CustomAgentDescription.is)) {
|
|
1842
|
+
this.logger.warn(`Skipping migration of ${yamlURI.toString()}: file content is not a valid CustomAgentDescription[]`);
|
|
1843
|
+
return emptyReport();
|
|
1844
|
+
}
|
|
1845
|
+
entries = doc;
|
|
1846
|
+
} catch (e) {
|
|
1847
|
+
this.logger.warn(`Skipping migration of ${yamlURI.toString()}: ${e.message}`);
|
|
1848
|
+
return emptyReport();
|
|
1849
|
+
}
|
|
1809
1850
|
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
const seenNames = new Set<string>();
|
|
1851
|
+
// A previous failed migration may have created `agents/<id>/` directories without an
|
|
1852
|
+
// `agent.md` inside. Remove those empty placeholders so this run can recreate them cleanly.
|
|
1853
|
+
await this.cleanupEmptyAgentFolders(scopeDir);
|
|
1814
1854
|
|
|
1815
|
-
|
|
1816
|
-
const
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1855
|
+
// Snapshot scope-root template files once so we can match per agent without re-listing.
|
|
1856
|
+
const scopeRootTemplateFiles = await this.listScopeRootPromptTemplates(scopeDir);
|
|
1857
|
+
|
|
1858
|
+
// Prompt-fragment files are matched by agent *name* (the fragment id is `<name>_prompt`)
|
|
1859
|
+
// while folders are keyed by the unique *id*. When two entries share a name, only the first
|
|
1860
|
+
// can own the name-based files, so track seen names and skip the move for the duplicates.
|
|
1861
|
+
const seenNames = new Set<string>();
|
|
1862
|
+
|
|
1863
|
+
for (const entry of entries) {
|
|
1864
|
+
const agentFolderURI = scopeDir.resolve(CUSTOM_AGENTS_DIRECTORY).resolve(entry.id);
|
|
1865
|
+
const targetURI = agentFolderURI.resolve(CUSTOM_AGENT_FILE_NAME);
|
|
1866
|
+
if (await this.fileService.exists(targetURI)) {
|
|
1867
|
+
alreadyPresent++;
|
|
1868
|
+
} else {
|
|
1869
|
+
try {
|
|
1870
|
+
await this.fileService.createFile(
|
|
1871
|
+
targetURI,
|
|
1872
|
+
BinaryBuffer.fromString(serializeCustomAgentFile(entry)),
|
|
1873
|
+
{ overwrite: true }
|
|
1874
|
+
);
|
|
1875
|
+
migrated++;
|
|
1876
|
+
} catch (e) {
|
|
1877
|
+
this.logger.warn(`Failed to migrate agent '${entry.id}' from ${yamlURI.toString()}: ${e?.message ?? e}`, e);
|
|
1878
|
+
failed++;
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
|
|
1883
|
+
if (seenNames.has(entry.name)) {
|
|
1884
|
+
this.logger.warn(
|
|
1885
|
+
`Multiple custom agents in ${yamlURI.toString()} share the name '${entry.name}'; ` +
|
|
1886
|
+
'prompt fragment files were migrated to the first matching agent only.'
|
|
1826
1887
|
);
|
|
1827
|
-
migrated++;
|
|
1828
|
-
} catch (e) {
|
|
1829
|
-
this.logger.warn(`Failed to migrate agent '${entry.id}' from ${yamlURI.toString()}: ${e?.message ?? e}`, e);
|
|
1830
|
-
failed++;
|
|
1831
1888
|
continue;
|
|
1832
1889
|
}
|
|
1890
|
+
seenNames.add(entry.name);
|
|
1891
|
+
|
|
1892
|
+
// Move any scope-root .prompttemplate files that look like they belong to this agent
|
|
1893
|
+
// (filename stem starts with `<name>_prompt`, followed by EOF or a separator).
|
|
1894
|
+
// They become variants under `<scope>/agents/<id>/`.
|
|
1895
|
+
for (const file of scopeRootTemplateFiles) {
|
|
1896
|
+
if (!matchesAgentPromptPrefix(file.stem, entry.name)) {
|
|
1897
|
+
continue;
|
|
1898
|
+
}
|
|
1899
|
+
const destURI = agentFolderURI.resolve(file.uri.path.base);
|
|
1900
|
+
if (await this.fileService.exists(destURI)) {
|
|
1901
|
+
continue;
|
|
1902
|
+
}
|
|
1903
|
+
try {
|
|
1904
|
+
await this.fileService.move(file.uri, destURI);
|
|
1905
|
+
promptOverridesMigrated++;
|
|
1906
|
+
} catch (e) {
|
|
1907
|
+
this.logger.warn(`Failed to move prompt fragment ${file.uri.toString()} into ${destURI.toString()}: ${e?.message ?? e}`);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1833
1910
|
}
|
|
1834
1911
|
|
|
1835
|
-
if (
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
)
|
|
1840
|
-
|
|
1841
|
-
}
|
|
1842
|
-
seenNames.add(entry.name);
|
|
1843
|
-
|
|
1844
|
-
// Move any scope-root .prompttemplate files that look like they belong to this agent
|
|
1845
|
-
// (filename stem starts with `<name>_prompt`, followed by EOF or a separator).
|
|
1846
|
-
// They become variants under `<scope>/agents/<id>/`.
|
|
1847
|
-
for (const file of scopeRootTemplateFiles) {
|
|
1848
|
-
if (!matchesAgentPromptPrefix(file.stem, entry.name)) {
|
|
1849
|
-
continue;
|
|
1850
|
-
}
|
|
1851
|
-
const destURI = agentFolderURI.resolve(file.uri.path.base);
|
|
1852
|
-
if (await this.fileService.exists(destURI)) {
|
|
1853
|
-
continue;
|
|
1912
|
+
if (failed === 0) {
|
|
1913
|
+
try {
|
|
1914
|
+
await this.fileService.move(yamlURI, backupURI, { overwrite: true });
|
|
1915
|
+
yamlBackedUp = true;
|
|
1916
|
+
} catch (e) {
|
|
1917
|
+
this.logger.warn(`Migrated ${migrated} agents but failed to back up ${yamlURI.toString()}: ${e.message}`);
|
|
1854
1918
|
}
|
|
1919
|
+
} else {
|
|
1855
1920
|
try {
|
|
1856
|
-
await this.fileService.
|
|
1857
|
-
|
|
1921
|
+
if (!(await this.fileService.exists(backupURI))) {
|
|
1922
|
+
await this.fileService.move(yamlURI, backupURI);
|
|
1923
|
+
yamlBackedUp = true;
|
|
1924
|
+
}
|
|
1858
1925
|
} catch (e) {
|
|
1859
|
-
this.logger.warn(`Failed to
|
|
1926
|
+
this.logger.warn(`Failed to back up ${yamlURI.toString()} to ${backupURI.toString()}: ${e.message}`);
|
|
1860
1927
|
}
|
|
1861
1928
|
}
|
|
1862
1929
|
}
|
|
1863
1930
|
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1931
|
+
// Correct any `agent.md` left with merged headings by an earlier (v1.73.0) migration. Anything
|
|
1932
|
+
// just migrated above is already correct, so only stale files from a previous run are rewritten.
|
|
1933
|
+
let corrected = 0;
|
|
1934
|
+
for (const { uri, content } of await this.computeScopeCorrections(scopeDir)) {
|
|
1867
1935
|
try {
|
|
1868
|
-
await this.fileService.
|
|
1869
|
-
|
|
1936
|
+
await this.fileService.createFile(uri, BinaryBuffer.fromString(content), { overwrite: true });
|
|
1937
|
+
corrected++;
|
|
1870
1938
|
} catch (e) {
|
|
1871
|
-
this.logger.warn(`
|
|
1872
|
-
}
|
|
1873
|
-
} else {
|
|
1874
|
-
try {
|
|
1875
|
-
if (!(await this.fileService.exists(backupURI))) {
|
|
1876
|
-
await this.fileService.move(yamlURI, backupURI);
|
|
1877
|
-
yamlBackedUp = true;
|
|
1878
|
-
}
|
|
1879
|
-
} catch (e) {
|
|
1880
|
-
this.logger.warn(`Failed to back up ${yamlURI.toString()} to ${backupURI.toString()}: ${e.message}`);
|
|
1939
|
+
this.logger.warn(`Failed to correct migrated agent file ${uri.toString()}: ${e?.message ?? e}`);
|
|
1881
1940
|
}
|
|
1882
1941
|
}
|
|
1883
1942
|
|
|
1943
|
+
if (!yamlExists && corrected === 0) {
|
|
1944
|
+
return undefined; // nothing to migrate and nothing to correct in this scope
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1884
1947
|
this.logger.info(
|
|
1885
|
-
`Migration done for ${
|
|
1886
|
-
`
|
|
1887
|
-
`yamlBackedUp=${yamlBackedUp}, promptOverridesMigrated=${promptOverridesMigrated}`
|
|
1948
|
+
`Migration done for ${scopeDir.toString()}: migrated=${migrated}, alreadyPresent=${alreadyPresent}, ` +
|
|
1949
|
+
`failed=${failed}, yamlBackedUp=${yamlBackedUp}, promptOverridesMigrated=${promptOverridesMigrated}, corrected=${corrected}`
|
|
1888
1950
|
);
|
|
1889
|
-
return { scope: scopeDir, yamlURI, migrated, alreadyPresent, failed, yamlBackedUp, promptOverridesMigrated };
|
|
1951
|
+
return { scope: scopeDir, yamlURI, migrated, alreadyPresent, failed, yamlBackedUp, promptOverridesMigrated, corrected };
|
|
1952
|
+
}
|
|
1953
|
+
|
|
1954
|
+
/**
|
|
1955
|
+
* Compute the `agent.md` rewrites needed to recover headings folded by an earlier (v1.73.0)
|
|
1956
|
+
* migration. The backup (`customAgents.yml.bak`) is the source that migration used: parsing it
|
|
1957
|
+
* the old (folded) way reproduces the exact bytes it wrote, and parsing it the new (literal) way
|
|
1958
|
+
* produces the corrected content. Only files still byte-identical to the buggy output are eligible,
|
|
1959
|
+
* so files the user edited (and already-correct files) are left untouched. Read-only; used both to
|
|
1960
|
+
* detect pending work and to perform the rewrite.
|
|
1961
|
+
*/
|
|
1962
|
+
protected async computeScopeCorrections(scopeDir: URI): Promise<Array<{ uri: URI; content: string }>> {
|
|
1963
|
+
const backupURI = scopeDir.resolve('customAgents.yml.bak');
|
|
1964
|
+
if (!(await this.fileService.exists(backupURI))) {
|
|
1965
|
+
return [];
|
|
1966
|
+
}
|
|
1967
|
+
let oldEntries: unknown;
|
|
1968
|
+
let newEntries: unknown;
|
|
1969
|
+
try {
|
|
1970
|
+
const raw = (await this.fileService.read(backupURI, { encoding: 'utf-8' })).value;
|
|
1971
|
+
oldEntries = load(raw); // reproduces the folded (buggy) output
|
|
1972
|
+
newEntries = this.parseCustomAgentsYaml(raw); // produces the heading-preserving fix
|
|
1973
|
+
} catch {
|
|
1974
|
+
return [];
|
|
1975
|
+
}
|
|
1976
|
+
if (!Array.isArray(oldEntries) || !oldEntries.every(CustomAgentDescription.is)
|
|
1977
|
+
|| !Array.isArray(newEntries) || !newEntries.every(CustomAgentDescription.is)) {
|
|
1978
|
+
return [];
|
|
1979
|
+
}
|
|
1980
|
+
const fixedById = new Map((newEntries as CustomAgentDescription[]).map(e => [e.id, e] as const));
|
|
1981
|
+
const corrections: Array<{ uri: URI; content: string }> = [];
|
|
1982
|
+
for (const oldEntry of oldEntries as CustomAgentDescription[]) {
|
|
1983
|
+
const fixed = fixedById.get(oldEntry.id);
|
|
1984
|
+
if (!fixed) {
|
|
1985
|
+
continue;
|
|
1986
|
+
}
|
|
1987
|
+
const buggyContent = serializeCustomAgentFile(oldEntry);
|
|
1988
|
+
const fixedContent = serializeCustomAgentFile(fixed);
|
|
1989
|
+
if (buggyContent === fixedContent) {
|
|
1990
|
+
continue; // prompt had no folded headings to recover
|
|
1991
|
+
}
|
|
1992
|
+
const agentMdURI = scopeDir.resolve(CUSTOM_AGENTS_DIRECTORY).resolve(oldEntry.id).resolve(CUSTOM_AGENT_FILE_NAME);
|
|
1993
|
+
if (!(await this.fileService.exists(agentMdURI))) {
|
|
1994
|
+
continue;
|
|
1995
|
+
}
|
|
1996
|
+
let current: string;
|
|
1997
|
+
try {
|
|
1998
|
+
current = (await this.fileService.read(agentMdURI, { encoding: 'utf-8' })).value;
|
|
1999
|
+
} catch {
|
|
2000
|
+
continue;
|
|
2001
|
+
}
|
|
2002
|
+
// Only rewrite files untouched since migration; current bytes still matching the buggy output.
|
|
2003
|
+
if (current === buggyContent) {
|
|
2004
|
+
corrections.push({ uri: agentMdURI, content: fixedContent });
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
return corrections;
|
|
1890
2008
|
}
|
|
1891
2009
|
|
|
1892
2010
|
/**
|
|
@@ -1923,6 +2041,8 @@ export interface MigrationReport {
|
|
|
1923
2041
|
yamlBackedUp: boolean;
|
|
1924
2042
|
/** Number of scope-root prompt customization files (`<name>_prompt.prompttemplate`) folded into agent.md and deleted. */
|
|
1925
2043
|
promptOverridesMigrated: number;
|
|
2044
|
+
/** Number of already-migrated `agent.md` files rewritten to recover folded headings. */
|
|
2045
|
+
corrected: number;
|
|
1926
2046
|
}
|
|
1927
2047
|
|
|
1928
2048
|
/**
|
package/src/browser/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ export * from './ai-core-frontend-application-contribution';
|
|
|
23
23
|
export * from './ai-core-frontend-module';
|
|
24
24
|
export * from '../common/ai-core-preferences';
|
|
25
25
|
export * from './ai-settings-service';
|
|
26
|
+
export * from './ai-configuration-service-impl';
|
|
26
27
|
export * from './ai-view-contribution';
|
|
27
28
|
export * from './frontend-language-model-registry';
|
|
28
29
|
export * from './frontend-language-model-alias-registry';
|
|
@@ -165,6 +165,35 @@ describe('TrustAwarePreferenceReader', () => {
|
|
|
165
165
|
});
|
|
166
166
|
});
|
|
167
167
|
|
|
168
|
+
describe('suppressUntrusted', () => {
|
|
169
|
+
const inspection = {
|
|
170
|
+
preferenceName: PREFERENCE_NAME,
|
|
171
|
+
defaultValue: 'default',
|
|
172
|
+
globalValue: 'user',
|
|
173
|
+
workspaceValue: 'workspace',
|
|
174
|
+
workspaceFolderValue: 'folder',
|
|
175
|
+
value: 'folder'
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
it('clears the workspace and folder values when untrusted', async () => {
|
|
179
|
+
trust.trustDeferred.resolve(false);
|
|
180
|
+
await reader.ready;
|
|
181
|
+
const result = reader.suppressUntrusted({ ...inspection });
|
|
182
|
+
expect(result.workspaceValue).to.equal(undefined);
|
|
183
|
+
expect(result.workspaceFolderValue).to.equal(undefined);
|
|
184
|
+
expect(result.globalValue).to.equal('user');
|
|
185
|
+
expect(result.defaultValue).to.equal('default');
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('returns the inspection unchanged when trusted', async () => {
|
|
189
|
+
trust.trustDeferred.resolve(true);
|
|
190
|
+
await reader.ready;
|
|
191
|
+
const result = reader.suppressUntrusted({ ...inspection });
|
|
192
|
+
expect(result.workspaceValue).to.equal('workspace');
|
|
193
|
+
expect(result.workspaceFolderValue).to.equal('folder');
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
168
197
|
describe('onDidChangeTrust', () => {
|
|
169
198
|
beforeEach(async () => {
|
|
170
199
|
trust.trustDeferred.resolve(false);
|
|
@@ -18,7 +18,7 @@ import { inject, injectable, postConstruct } from '@theia/core/shared/inversify'
|
|
|
18
18
|
import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
|
|
19
19
|
import { Emitter, Event } from '@theia/core';
|
|
20
20
|
import { Deferred } from '@theia/core/lib/common/promise-util';
|
|
21
|
-
import { PreferenceService } from '@theia/core/lib/common/preferences';
|
|
21
|
+
import { PreferenceInspection, PreferenceService } from '@theia/core/lib/common/preferences';
|
|
22
22
|
import { WorkspaceTrustService } from '@theia/workspace/lib/browser/workspace-trust-service';
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -27,6 +27,9 @@ import { WorkspaceTrustService } from '@theia/workspace/lib/browser/workspace-tr
|
|
|
27
27
|
*
|
|
28
28
|
* Writes should continue to go through `PreferenceService` directly; trust only
|
|
29
29
|
* affects reads.
|
|
30
|
+
*
|
|
31
|
+
* @internal This is an implementation detail of {@link AiConfigurationService}. AI configuration
|
|
32
|
+
* consumers should go through `AiConfigurationService` rather than injecting this reader directly.
|
|
30
33
|
*/
|
|
31
34
|
@injectable()
|
|
32
35
|
export class TrustAwarePreferenceReader {
|
|
@@ -102,7 +105,24 @@ export class TrustAwarePreferenceReader {
|
|
|
102
105
|
return this.preferences.get<T>(preferenceName, fallback, resourceUri);
|
|
103
106
|
}
|
|
104
107
|
const inspection = this.preferences.inspect<JSONValue>(preferenceName, resourceUri);
|
|
105
|
-
|
|
106
|
-
|
|
108
|
+
if (!inspection) {
|
|
109
|
+
return fallback;
|
|
110
|
+
}
|
|
111
|
+
const suppressed = this.suppressUntrusted(inspection);
|
|
112
|
+
return ((suppressed.globalValue ?? suppressed.defaultValue) as T | undefined) ?? fallback;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Narrows a raw {@link PreferenceInspection} according to workspace trust: when the workspace is
|
|
117
|
+
* untrusted the folder and workspace scope values are dropped so that only the user and default
|
|
118
|
+
* scopes contribute (failing closed until the trust state resolves). A trusted inspection is
|
|
119
|
+
* returned unchanged. This is the single definition of the trust-suppression rule, shared by
|
|
120
|
+
* {@link get} and by inspection-based consumers, so value reads and inspections cannot drift.
|
|
121
|
+
*/
|
|
122
|
+
suppressUntrusted<T extends JSONValue>(inspection: PreferenceInspection<T>): PreferenceInspection<T> {
|
|
123
|
+
if (this.trusted) {
|
|
124
|
+
return inspection;
|
|
125
|
+
}
|
|
126
|
+
return { ...inspection, workspaceValue: undefined, workspaceFolderValue: undefined };
|
|
107
127
|
}
|
|
108
128
|
}
|