@theia/ai-core 1.77.0-next.6 → 1.77.0-next.63

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 (172) hide show
  1. package/lib/browser/agents-md-frontend-application-contribution.d.ts +65 -0
  2. package/lib/browser/agents-md-frontend-application-contribution.d.ts.map +1 -0
  3. package/lib/browser/agents-md-frontend-application-contribution.js +269 -0
  4. package/lib/browser/agents-md-frontend-application-contribution.js.map +1 -0
  5. package/lib/browser/agents-md-frontend-application-contribution.spec.d.ts +2 -0
  6. package/lib/browser/agents-md-frontend-application-contribution.spec.d.ts.map +1 -0
  7. package/lib/browser/agents-md-frontend-application-contribution.spec.js +225 -0
  8. package/lib/browser/agents-md-frontend-application-contribution.spec.js.map +1 -0
  9. package/lib/browser/agents-md-migration-service.d.ts +78 -0
  10. package/lib/browser/agents-md-migration-service.d.ts.map +1 -0
  11. package/lib/browser/agents-md-migration-service.js +195 -0
  12. package/lib/browser/agents-md-migration-service.js.map +1 -0
  13. package/lib/browser/agents-md-migration-service.spec.d.ts +2 -0
  14. package/lib/browser/agents-md-migration-service.spec.d.ts.map +1 -0
  15. package/lib/browser/agents-md-migration-service.spec.js +278 -0
  16. package/lib/browser/agents-md-migration-service.spec.js.map +1 -0
  17. package/lib/browser/agents-md-prompt-fragment-contribution.d.ts +13 -0
  18. package/lib/browser/agents-md-prompt-fragment-contribution.d.ts.map +1 -0
  19. package/lib/browser/agents-md-prompt-fragment-contribution.js +76 -0
  20. package/lib/browser/agents-md-prompt-fragment-contribution.js.map +1 -0
  21. package/lib/browser/agents-md-service.d.ts +58 -0
  22. package/lib/browser/agents-md-service.d.ts.map +1 -0
  23. package/lib/browser/agents-md-service.js +191 -0
  24. package/lib/browser/agents-md-service.js.map +1 -0
  25. package/lib/browser/agents-md-service.spec.d.ts +2 -0
  26. package/lib/browser/agents-md-service.spec.d.ts.map +1 -0
  27. package/lib/browser/agents-md-service.spec.js +220 -0
  28. package/lib/browser/agents-md-service.spec.js.map +1 -0
  29. package/lib/browser/agents-md-variable-contribution.d.ts +66 -0
  30. package/lib/browser/agents-md-variable-contribution.d.ts.map +1 -0
  31. package/lib/browser/agents-md-variable-contribution.js +169 -0
  32. package/lib/browser/agents-md-variable-contribution.js.map +1 -0
  33. package/lib/browser/agents-md-variable-contribution.spec.d.ts +2 -0
  34. package/lib/browser/agents-md-variable-contribution.spec.d.ts.map +1 -0
  35. package/lib/browser/agents-md-variable-contribution.spec.js +148 -0
  36. package/lib/browser/agents-md-variable-contribution.spec.js.map +1 -0
  37. package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
  38. package/lib/browser/ai-core-frontend-module.js +17 -0
  39. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  40. package/lib/browser/file-variable-contribution.d.ts +2 -0
  41. package/lib/browser/file-variable-contribution.d.ts.map +1 -1
  42. package/lib/browser/file-variable-contribution.js +8 -23
  43. package/lib/browser/file-variable-contribution.js.map +1 -1
  44. package/lib/browser/file-variable-contribution.spec.d.ts +2 -0
  45. package/lib/browser/file-variable-contribution.spec.d.ts.map +1 -0
  46. package/lib/browser/file-variable-contribution.spec.js +98 -0
  47. package/lib/browser/file-variable-contribution.spec.js.map +1 -0
  48. package/lib/browser/frontend-language-model-service.d.ts.map +1 -1
  49. package/lib/browser/frontend-language-model-service.js +0 -8
  50. package/lib/browser/frontend-language-model-service.js.map +1 -1
  51. package/lib/browser/frontend-prompt-customization-service.d.ts +2 -1
  52. package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
  53. package/lib/browser/frontend-prompt-customization-service.js +10 -0
  54. package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
  55. package/lib/browser/frontend-prompt-customization-service.spec.js +19 -1
  56. package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -1
  57. package/lib/browser/frontend-variable-service.js +1 -1
  58. package/lib/browser/frontend-variable-service.js.map +1 -1
  59. package/lib/browser/index.d.ts +6 -0
  60. package/lib/browser/index.d.ts.map +1 -1
  61. package/lib/browser/index.js +6 -0
  62. package/lib/browser/index.js.map +1 -1
  63. package/lib/browser/prompt-variable-contribution.js +1 -1
  64. package/lib/browser/prompt-variable-contribution.js.map +1 -1
  65. package/lib/browser/prompt-variable-contribution.spec.js +2 -1
  66. package/lib/browser/prompt-variable-contribution.spec.js.map +1 -1
  67. package/lib/browser/prompttemplate-contribution.js +1 -1
  68. package/lib/browser/prompttemplate-contribution.js.map +1 -1
  69. package/lib/browser/reasoning-labels.d.ts +4 -0
  70. package/lib/browser/reasoning-labels.d.ts.map +1 -0
  71. package/lib/browser/reasoning-labels.js +41 -0
  72. package/lib/browser/reasoning-labels.js.map +1 -0
  73. package/lib/browser/reasoning-labels.spec.d.ts +2 -0
  74. package/lib/browser/reasoning-labels.spec.d.ts.map +1 -0
  75. package/lib/browser/reasoning-labels.spec.js +28 -0
  76. package/lib/browser/reasoning-labels.spec.js.map +1 -0
  77. package/lib/browser/skill-service.spec.js +2 -1
  78. package/lib/browser/skill-service.spec.js.map +1 -1
  79. package/lib/browser/skills-variable-contribution.spec.js +2 -1
  80. package/lib/browser/skills-variable-contribution.spec.js.map +1 -1
  81. package/lib/browser/theia-variable-contribution.js +3 -3
  82. package/lib/browser/theia-variable-contribution.js.map +1 -1
  83. package/lib/browser/workspace-relative-path-resolver.d.ts +20 -0
  84. package/lib/browser/workspace-relative-path-resolver.d.ts.map +1 -0
  85. package/lib/browser/workspace-relative-path-resolver.js +78 -0
  86. package/lib/browser/workspace-relative-path-resolver.js.map +1 -0
  87. package/lib/common/agents-md.d.ts +20 -0
  88. package/lib/common/agents-md.d.ts.map +1 -0
  89. package/lib/common/agents-md.js +38 -0
  90. package/lib/common/agents-md.js.map +1 -0
  91. package/lib/common/ai-core-preferences.d.ts.map +1 -1
  92. package/lib/common/ai-core-preferences.js +6 -3
  93. package/lib/common/ai-core-preferences.js.map +1 -1
  94. package/lib/common/ai-variable-resource.js +3 -3
  95. package/lib/common/ai-variable-resource.js.map +1 -1
  96. package/lib/common/capability-variable-contribution.js +1 -1
  97. package/lib/common/capability-variable-contribution.js.map +1 -1
  98. package/lib/common/index.d.ts +2 -0
  99. package/lib/common/index.d.ts.map +1 -1
  100. package/lib/common/index.js +2 -0
  101. package/lib/common/index.js.map +1 -1
  102. package/lib/common/language-model-service.d.ts.map +1 -1
  103. package/lib/common/language-model-service.js +7 -0
  104. package/lib/common/language-model-service.js.map +1 -1
  105. package/lib/common/language-model-service.spec.js +21 -0
  106. package/lib/common/language-model-service.spec.js.map +1 -1
  107. package/lib/common/language-model.d.ts +10 -3
  108. package/lib/common/language-model.d.ts.map +1 -1
  109. package/lib/common/language-model.js +17 -4
  110. package/lib/common/language-model.js.map +1 -1
  111. package/lib/common/language-model.spec.js +18 -0
  112. package/lib/common/language-model.spec.js.map +1 -1
  113. package/lib/common/prompt-service.d.ts +12 -0
  114. package/lib/common/prompt-service.d.ts.map +1 -1
  115. package/lib/common/prompt-service.js.map +1 -1
  116. package/lib/common/prompt-service.spec.js +2 -1
  117. package/lib/common/prompt-service.spec.js.map +1 -1
  118. package/lib/common/tool-call-execution.d.ts +89 -0
  119. package/lib/common/tool-call-execution.d.ts.map +1 -0
  120. package/lib/common/tool-call-execution.js +67 -0
  121. package/lib/common/tool-call-execution.js.map +1 -0
  122. package/lib/common/tool-call-execution.spec.d.ts +2 -0
  123. package/lib/common/tool-call-execution.spec.d.ts.map +1 -0
  124. package/lib/common/tool-call-execution.spec.js +114 -0
  125. package/lib/common/tool-call-execution.spec.js.map +1 -0
  126. package/lib/common/tool-request-parameters.spec.d.ts +2 -0
  127. package/lib/common/tool-request-parameters.spec.d.ts.map +1 -0
  128. package/lib/common/tool-request-parameters.spec.js +62 -0
  129. package/lib/common/tool-request-parameters.spec.js.map +1 -0
  130. package/lib/common/variable-service.spec.js +2 -1
  131. package/lib/common/variable-service.spec.js.map +1 -1
  132. package/lib/node/ai-core-backend-module.d.ts.map +1 -1
  133. package/lib/node/ai-core-backend-module.js +2 -0
  134. package/lib/node/ai-core-backend-module.js.map +1 -1
  135. package/lib/node/model-snapshot-store.js +2 -2
  136. package/lib/node/model-snapshot-store.js.map +1 -1
  137. package/lib/node/proxy-util.js +2 -1
  138. package/lib/node/proxy-util.js.map +1 -1
  139. package/package.json +9 -9
  140. package/src/browser/agents-md-frontend-application-contribution.spec.ts +244 -0
  141. package/src/browser/agents-md-frontend-application-contribution.ts +291 -0
  142. package/src/browser/agents-md-migration-service.spec.ts +338 -0
  143. package/src/browser/agents-md-migration-service.ts +226 -0
  144. package/src/browser/agents-md-prompt-fragment-contribution.ts +73 -0
  145. package/src/browser/agents-md-service.spec.ts +258 -0
  146. package/src/browser/agents-md-service.ts +212 -0
  147. package/src/browser/agents-md-variable-contribution.spec.ts +180 -0
  148. package/src/browser/agents-md-variable-contribution.ts +182 -0
  149. package/src/browser/ai-core-frontend-module.ts +19 -0
  150. package/src/browser/file-variable-contribution.spec.ts +107 -0
  151. package/src/browser/file-variable-contribution.ts +8 -27
  152. package/src/browser/frontend-language-model-service.ts +1 -10
  153. package/src/browser/frontend-prompt-customization-service.spec.ts +24 -0
  154. package/src/browser/frontend-prompt-customization-service.ts +13 -1
  155. package/src/browser/index.ts +6 -0
  156. package/src/browser/reasoning-labels.spec.ts +28 -0
  157. package/src/browser/reasoning-labels.ts +41 -0
  158. package/src/browser/theia-variable-contribution.ts +3 -3
  159. package/src/browser/workspace-relative-path-resolver.ts +71 -0
  160. package/src/common/agents-md.ts +37 -0
  161. package/src/common/ai-core-preferences.ts +6 -3
  162. package/src/common/ai-variable-resource.ts +1 -1
  163. package/src/common/index.ts +2 -0
  164. package/src/common/language-model-service.spec.ts +23 -1
  165. package/src/common/language-model-service.ts +11 -1
  166. package/src/common/language-model.spec.ts +21 -0
  167. package/src/common/language-model.ts +26 -6
  168. package/src/common/prompt-service.ts +14 -0
  169. package/src/common/tool-call-execution.spec.ts +162 -0
  170. package/src/common/tool-call-execution.ts +161 -0
  171. package/src/common/tool-request-parameters.spec.ts +81 -0
  172. package/src/node/ai-core-backend-module.ts +5 -1
@@ -0,0 +1,212 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { inject, injectable, named, postConstruct } from '@theia/core/shared/inversify';
18
+ import { Deferred } from '@theia/core/lib/common/promise-util';
19
+ import { Disposable, DisposableCollection, Emitter, Event, ILogger, URI } from '@theia/core';
20
+ import { FileService } from '@theia/filesystem/lib/browser/file-service';
21
+ import { FileChangesEvent, FileOperationEvent } from '@theia/filesystem/lib/common/files';
22
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
23
+ import { AGENTS_MD_FILE_NAME } from '../common/agents-md';
24
+
25
+ /** Debounce delay for coalescing rapid file system events. */
26
+ const UPDATE_DEBOUNCE_MS = 50;
27
+
28
+ export interface AgentsMdFile {
29
+ /** URI of the discovered `AGENTS.md`. */
30
+ uri: URI;
31
+ /** Content as of the last scan. */
32
+ content: string;
33
+ /** Name of the workspace root holding the file, used as provenance label when merging several roots. */
34
+ rootName: string;
35
+ }
36
+
37
+ export const AgentsMdService = Symbol('AgentsMdService');
38
+ export interface AgentsMdService {
39
+ /** The `AGENTS.md` files discovered in the workspace roots, in workspace root order. */
40
+ getAgentsMdFiles(): AgentsMdFile[];
41
+
42
+ /** Fired after every scan, whether or not the result changed. */
43
+ readonly onDidChange: Event<void>;
44
+
45
+ /** Resolves once the initial scan is complete. Always resolves, even if the scan failed. */
46
+ readonly ready: Promise<void>;
47
+ }
48
+
49
+ /**
50
+ * Discovers `<root>/AGENTS.md` for every workspace root and keeps the content up to date.
51
+ *
52
+ * Only the workspace roots themselves are scanned. A nested `AGENTS.md` cascade with proximity
53
+ * precedence, `@path` imports and a global `~/.agents/AGENTS.md` are deliberately out of scope.
54
+ */
55
+ @injectable()
56
+ export class DefaultAgentsMdService implements AgentsMdService, Disposable {
57
+
58
+ @inject(FileService)
59
+ protected readonly fileService: FileService;
60
+
61
+ @inject(WorkspaceService)
62
+ protected readonly workspaceService: WorkspaceService;
63
+
64
+ @inject(ILogger) @named('ai-core:DefaultAgentsMdService')
65
+ protected readonly logger: ILogger;
66
+
67
+ protected files: AgentsMdFile[] = [];
68
+
69
+ /** Watchers of the current scan; disposed and rebuilt on every rescan. */
70
+ protected toDispose = new DisposableCollection();
71
+ /** Kept apart from {@link toDispose}, which does not survive a rescan. */
72
+ protected readonly toDisposeOnServiceDispose = new DisposableCollection();
73
+
74
+ /** URIs of the `AGENTS.md` files we react to: one per workspace root, whether or not it exists. */
75
+ protected watchedFiles = new Set<string>();
76
+
77
+ protected updateDebounceTimeout: ReturnType<typeof setTimeout> | undefined;
78
+
79
+ /** True while {@link update} is running, so concurrent callers do not duplicate the scan. */
80
+ protected updateInProgress = false;
81
+ /** Set when {@link update} is called while another run is in progress; triggers a follow-up scan. */
82
+ protected updateRescheduled = false;
83
+
84
+ protected readonly onDidChangeEmitter = new Emitter<void>();
85
+ readonly onDidChange: Event<void> = this.onDidChangeEmitter.event;
86
+
87
+ protected _ready = new Deferred<void>();
88
+ get ready(): Promise<void> {
89
+ return this._ready.promise;
90
+ }
91
+
92
+ @postConstruct()
93
+ protected init(): void {
94
+ this.toDisposeOnServiceDispose.push(this.onDidChangeEmitter);
95
+ this.toDisposeOnServiceDispose.push(this.fileService.onDidFilesChange((event: FileChangesEvent) => {
96
+ if (event.changes.some(change => this.isWatched(change.resource))) {
97
+ this.scheduleUpdate();
98
+ }
99
+ }));
100
+ // The watcher above only reports what the backend sees, and `FileService.watch` registers
101
+ // that watcher asynchronously. An `AGENTS.md` written through the application right after
102
+ // startup - most notably by the migration - therefore lands in the gap before the watcher is
103
+ // live and would be missed for the rest of the session. Operations performed through the
104
+ // FileService are reported here synchronously, independent of any watcher.
105
+ this.toDisposeOnServiceDispose.push(this.fileService.onDidRunOperation((event: FileOperationEvent) => {
106
+ if (this.isWatched(event.resource) || (event.target && this.isWatched(event.target.resource))) {
107
+ this.scheduleUpdate();
108
+ }
109
+ }));
110
+ this.initialize();
111
+ }
112
+
113
+ protected async initialize(): Promise<void> {
114
+ try {
115
+ await this.workspaceService.ready;
116
+ } catch (error) {
117
+ this.logger.error('Failed to resolve the workspace, scanning the already known roots for AGENTS.md', error);
118
+ }
119
+ // Listen before the initial scan, otherwise a change landing while it runs is recorded as
120
+ // already applied and dropped. update()'s in-progress guard coalesces the overlap.
121
+ this.toDisposeOnServiceDispose.push(this.workspaceService.onWorkspaceChanged(() => this.scheduleUpdate()));
122
+ try {
123
+ await this.update();
124
+ } catch (error) {
125
+ this.logger.error('Initial AGENTS.md scan failed', error);
126
+ }
127
+ this._ready.resolve();
128
+ }
129
+
130
+ getAgentsMdFiles(): AgentsMdFile[] {
131
+ return [...this.files];
132
+ }
133
+
134
+ /** Whether `resource` is one of the `AGENTS.md` locations this service tracks. */
135
+ protected isWatched(resource: URI): boolean {
136
+ return this.watchedFiles.has(resource.toString());
137
+ }
138
+
139
+ dispose(): void {
140
+ if (this.updateDebounceTimeout) {
141
+ clearTimeout(this.updateDebounceTimeout);
142
+ this.updateDebounceTimeout = undefined;
143
+ }
144
+ this.toDisposeOnServiceDispose.dispose();
145
+ this.toDispose.dispose();
146
+ }
147
+
148
+ protected scheduleUpdate(): void {
149
+ if (this.updateDebounceTimeout) {
150
+ clearTimeout(this.updateDebounceTimeout);
151
+ }
152
+ this.updateDebounceTimeout = setTimeout(() => {
153
+ this.updateDebounceTimeout = undefined;
154
+ this.update();
155
+ }, UPDATE_DEBOUNCE_MS);
156
+ }
157
+
158
+ protected async update(): Promise<void> {
159
+ if (this.updateDebounceTimeout) {
160
+ clearTimeout(this.updateDebounceTimeout);
161
+ this.updateDebounceTimeout = undefined;
162
+ }
163
+ // Serialise concurrent calls: a workspace-ready trigger and a file-change-driven
164
+ // scheduleUpdate() can fire within the same async tick. The second caller records a pending
165
+ // request and lets the first finish; the follow-up is re-scheduled through the debouncer.
166
+ if (this.updateInProgress) {
167
+ this.updateRescheduled = true;
168
+ return;
169
+ }
170
+ this.updateInProgress = true;
171
+ try {
172
+ await this.doUpdate();
173
+ } finally {
174
+ this.updateInProgress = false;
175
+ if (this.updateRescheduled) {
176
+ this.updateRescheduled = false;
177
+ this.scheduleUpdate();
178
+ }
179
+ }
180
+ }
181
+
182
+ protected async doUpdate(): Promise<void> {
183
+ const newDisposables = new DisposableCollection();
184
+ const newFiles: AgentsMdFile[] = [];
185
+ const roots = this.workspaceService.tryGetRoots();
186
+
187
+ // Publish the tracked locations before doing any I/O. A file operation landing while this
188
+ // scan is still running has to be recognized so that the in-progress guard can reschedule;
189
+ // assigning only at the end would drop it and leave this scan's stale result in place.
190
+ this.watchedFiles = new Set(roots.map(root => root.resource.resolve(AGENTS_MD_FILE_NAME).toString()));
191
+
192
+ for (const root of roots) {
193
+ const fileUri = root.resource.resolve(AGENTS_MD_FILE_NAME);
194
+ // Watch the root, not the file: a watcher on a missing file cannot report its creation.
195
+ newDisposables.push(this.fileService.watch(root.resource, { recursive: false, excludes: [] }));
196
+ try {
197
+ if (await this.fileService.exists(fileUri)) {
198
+ const content = await this.fileService.read(fileUri);
199
+ newFiles.push({ uri: fileUri, content: content.value, rootName: root.name });
200
+ }
201
+ } catch (error) {
202
+ this.logger.error(`Failed to read '${fileUri.toString()}': ${error}`);
203
+ }
204
+ }
205
+
206
+ this.toDispose.dispose();
207
+ this.toDispose = newDisposables;
208
+ this.files = newFiles;
209
+
210
+ this.onDidChangeEmitter.fire();
211
+ }
212
+ }
@@ -0,0 +1,180 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { enableJSDOM } from '@theia/core/lib/browser/test/jsdom';
18
+ const disableJSDOM = enableJSDOM();
19
+
20
+ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/frontend-application-config-provider';
21
+ // Guarded: another spec in the same mocha process may already have set it, and `set` throws on a
22
+ // second call.
23
+ try {
24
+ FrontendApplicationConfigProvider.get();
25
+ } catch {
26
+ FrontendApplicationConfigProvider.set({});
27
+ }
28
+
29
+ import { expect } from 'chai';
30
+ import { URI } from '@theia/core';
31
+ import { AgentsMdFile, AgentsMdService } from './agents-md-service';
32
+ import { AGENTS_MD_AUTO_LOADED_VARIABLE, AGENTS_MD_CONTENT_VARIABLE, AGENTS_MD_VARIABLE, AgentsMdVariableContribution } from './agents-md-variable-contribution';
33
+ import { AIVariableContext } from '../common/variable-service';
34
+ import { matchFunctionsRegEx } from '../common/prompt-service-util';
35
+
36
+ disableJSDOM();
37
+
38
+ describe('AgentsMdVariableContribution', () => {
39
+
40
+ function file(rootName: string, content: string): AgentsMdFile {
41
+ return { uri: new URI(`file:///${rootName}/AGENTS.md`), content, rootName };
42
+ }
43
+
44
+ /**
45
+ * @param rootCount how many workspace roots are open. Defaults to one per discovered file, which
46
+ * is the ordinary case; pass it explicitly to build a workspace whose root count and file count
47
+ * differ, since auto-loading keys on roots rather than files.
48
+ */
49
+ function createContribution(files: AgentsMdFile[], rootCount = files.length): AgentsMdVariableContribution {
50
+ const contribution = new AgentsMdVariableContribution();
51
+ const service: AgentsMdService = {
52
+ getAgentsMdFiles: () => files,
53
+ onDidChange: () => ({ dispose: () => { } }),
54
+ ready: Promise.resolve()
55
+ };
56
+ (contribution as unknown as { agentsMdService: AgentsMdService }).agentsMdService = service;
57
+ (contribution as unknown as { workspaceService: unknown }).workspaceService = {
58
+ tryGetRoots: () => new Array(rootCount).fill({})
59
+ };
60
+ return contribution;
61
+ }
62
+
63
+ async function resolve(files: AgentsMdFile[], variable: typeof AGENTS_MD_VARIABLE, rootCount?: number): Promise<string> {
64
+ const resolved = await createContribution(files, rootCount).resolve({ variable }, {} as AIVariableContext);
65
+ return resolved!.value;
66
+ }
67
+
68
+ describe('agentsMdAutoLoaded', () => {
69
+ it('resolves to nothing when the workspace has no AGENTS.md', async () => {
70
+ expect(await resolve([], AGENTS_MD_AUTO_LOADED_VARIABLE, 1)).to.equal('');
71
+ });
72
+
73
+ it('inlines the root file when the workspace has a single root', async () => {
74
+ expect(await resolve([file('theia', '# Project\n\nUse npm.')], AGENTS_MD_AUTO_LOADED_VARIABLE)).to.equal(
75
+ '<project_instructions path="theia/AGENTS.md">\n# Project\n\nUse npm.\n</project_instructions>'
76
+ );
77
+ });
78
+
79
+ it('loads nothing once the workspace has more than one root', async () => {
80
+ expect(await resolve([file('a', 'x'), file('b', 'y')], AGENTS_MD_AUTO_LOADED_VARIABLE)).to.equal('');
81
+ });
82
+
83
+ it('loads nothing in a multi-root workspace where only one root has a file', async () => {
84
+ // Keyed on root count, not file count: a workspace with several roots has no single
85
+ // project root, even when only one of them carries an AGENTS.md.
86
+ expect(await resolve([file('a', 'x')], AGENTS_MD_AUTO_LOADED_VARIABLE, 2)).to.equal('');
87
+ });
88
+
89
+ it('escapes the path but leaves the Markdown content untouched', async () => {
90
+ const resolved = await resolve([file('a&b', '# Heading <not xml> & "quoted"')], AGENTS_MD_AUTO_LOADED_VARIABLE, 1);
91
+ expect(resolved).to.contain('path="a&amp;b/AGENTS.md"');
92
+ expect(resolved).to.contain('# Heading <not xml> & "quoted"');
93
+ });
94
+
95
+ it('keeps function references in the content from attaching tools', async () => {
96
+ const resolved = await resolve([file('a', 'Use ~{getFileContent} and {{selectedText}}.')], AGENTS_MD_AUTO_LOADED_VARIABLE, 1);
97
+ expect(matchFunctionsRegEx(resolved)).to.be.empty;
98
+ expect(resolved).to.contain('{{selectedText}}');
99
+ });
100
+ });
101
+
102
+ describe('agentsMd', () => {
103
+ it('resolves to nothing when the workspace has no AGENTS.md', async () => {
104
+ expect(await resolve([], AGENTS_MD_VARIABLE)).to.equal('');
105
+ });
106
+
107
+ it('advertises nothing when the single root file is already loaded', async () => {
108
+ // Otherwise the model is told to read a file it has just been handed.
109
+ expect(await resolve([file('theia', 'x')], AGENTS_MD_VARIABLE)).to.equal('');
110
+ });
111
+
112
+ it('advertises one entry per root once there is more than one root', async () => {
113
+ expect(await resolve([file('a', 'x'), file('b', 'y')], AGENTS_MD_VARIABLE)).to.equal(
114
+ '<agents_md_files>\n<file>a/AGENTS.md</file>\n<file>b/AGENTS.md</file>\n</agents_md_files>'
115
+ );
116
+ });
117
+
118
+ it('advertises the single file of a multi-root workspace rather than loading it', async () => {
119
+ expect(await resolve([file('a', 'x')], AGENTS_MD_VARIABLE, 2)).to.equal(
120
+ '<agents_md_files>\n<file>a/AGENTS.md</file>\n</agents_md_files>'
121
+ );
122
+ });
123
+
124
+ it('escapes XML in the root name', async () => {
125
+ expect(await resolve([file('a&b', 'x')], AGENTS_MD_VARIABLE, 2)).to.contain('<file>a&amp;b/AGENTS.md</file>');
126
+ });
127
+
128
+ it('never inlines the content', async () => {
129
+ expect(await resolve([file('a', 'secret project conventions'), file('b', 'y')], AGENTS_MD_VARIABLE))
130
+ .to.not.contain('secret project conventions');
131
+ });
132
+ });
133
+
134
+ describe('the two are mutually exclusive', () => {
135
+ it('never emits both for the same workspace', async () => {
136
+ for (const [files, roots] of [
137
+ [[file('theia', 'x')], 1],
138
+ [[file('a', 'x'), file('b', 'y')], 2],
139
+ [[file('a', 'x')], 2],
140
+ [[], 1]
141
+ ] as [AgentsMdFile[], number][]) {
142
+ const loaded = await resolve(files, AGENTS_MD_AUTO_LOADED_VARIABLE, roots);
143
+ const listed = await resolve(files, AGENTS_MD_VARIABLE, roots);
144
+ expect(loaded === '' || listed === '', `both were non-empty for ${roots} root(s)`).to.be.true;
145
+ }
146
+ });
147
+ });
148
+
149
+ describe('agentsMdContent', () => {
150
+ it('resolves to nothing when the workspace has no AGENTS.md', async () => {
151
+ expect(await resolve([], AGENTS_MD_CONTENT_VARIABLE)).to.equal('');
152
+ });
153
+
154
+ it('inlines a single root verbatim, without provenance framing', async () => {
155
+ expect(await resolve([file('theia', '# Project\n\nUse npm.')], AGENTS_MD_CONTENT_VARIABLE)).to.equal('# Project\n\nUse npm.');
156
+ });
157
+
158
+ it('labels the sources when several roots contribute', async () => {
159
+ expect(await resolve([file('a', 'first'), file('b', 'second')], AGENTS_MD_CONTENT_VARIABLE)).to.equal(
160
+ '### a\n\nfirst\n\n### b\n\nsecond'
161
+ );
162
+ });
163
+
164
+ it('keeps function references in the content from attaching tools', async () => {
165
+ expect(matchFunctionsRegEx(await resolve([file('a', '~{getFileContent}')], AGENTS_MD_CONTENT_VARIABLE))).to.be.empty;
166
+ expect(matchFunctionsRegEx(await resolve([file('a', '~{x}'), file('b', '~{y}')], AGENTS_MD_CONTENT_VARIABLE))).to.be.empty;
167
+ });
168
+ });
169
+
170
+ describe('canResolve', () => {
171
+ it('claims all three variables and nothing else', () => {
172
+ const contribution = createContribution([]);
173
+ const context = {} as AIVariableContext;
174
+ expect(contribution.canResolve({ variable: AGENTS_MD_AUTO_LOADED_VARIABLE }, context)).to.equal(1);
175
+ expect(contribution.canResolve({ variable: AGENTS_MD_VARIABLE }, context)).to.equal(1);
176
+ expect(contribution.canResolve({ variable: AGENTS_MD_CONTENT_VARIABLE }, context)).to.equal(1);
177
+ expect(contribution.canResolve({ variable: { id: 'other', name: 'other', description: '' } }, context)).to.equal(-1);
178
+ });
179
+ });
180
+ });
@@ -0,0 +1,182 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource GmbH.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { inject, injectable } from '@theia/core/shared/inversify';
18
+ import { MaybePromise, nls } from '@theia/core';
19
+ import {
20
+ AIVariable, AIVariableContext, AIVariableContribution, AIVariableResolutionRequest,
21
+ AIVariableResolver, AIVariableService, ResolvedAIVariable
22
+ } from '../common/variable-service';
23
+ import { WorkspaceService } from '@theia/workspace/lib/browser';
24
+ import { AGENTS_MD_FILE_NAME } from '../common/agents-md';
25
+ import { AgentsMdFile, AgentsMdService } from './agents-md-service';
26
+
27
+ export const AGENTS_MD_AUTO_LOADED_VARIABLE: AIVariable = {
28
+ id: 'agentsMdAutoLoaded',
29
+ name: 'agentsMdAutoLoaded',
30
+ description: nls.localize('theia/ai/core/agentsMdAutoLoadedVariable/description',
31
+ 'Returns the content of the workspace root AGENTS.md when the workspace has a single root, so that it is always in context')
32
+ };
33
+
34
+ export const AGENTS_MD_VARIABLE: AIVariable = {
35
+ id: 'agentsMd',
36
+ name: 'agentsMd',
37
+ description: nls.localize('theia/ai/core/agentsMdVariable/description',
38
+ 'Lists the AGENTS.md files an agent has to read on demand, i.e. the ones that are not already loaded')
39
+ };
40
+
41
+ export const AGENTS_MD_CONTENT_VARIABLE: AIVariable = {
42
+ id: 'agentsMdContent',
43
+ name: 'agentsMdContent',
44
+ description: nls.localize('theia/ai/core/agentsMdContentVariable/description',
45
+ 'Returns the full content of the AGENTS.md files of the workspace')
46
+ };
47
+
48
+ /**
49
+ * Resolves the three ways of consuming `AGENTS.md`.
50
+ *
51
+ * A **single-root** workspace has an unambiguous project root, so its `AGENTS.md` is always in
52
+ * context, mirroring how other tools implementing the standard treat the root file. A workspace with
53
+ * several roots has no such file, so nothing is loaded and every discovered file is advertised
54
+ * instead, to be read on demand. {@link AGENTS_MD_AUTO_LOADED_VARIABLE} and
55
+ * {@link AGENTS_MD_VARIABLE} are therefore mutually exclusive by construction: at most one of them is
56
+ * ever non-empty.
57
+ *
58
+ * - {@link AGENTS_MD_AUTO_LOADED_VARIABLE} inlines the single root's file, or nothing.
59
+ * - {@link AGENTS_MD_VARIABLE} advertises files by path so a tool-calling agent can fetch them with
60
+ * `getFileContent`, or nothing.
61
+ * - {@link AGENTS_MD_CONTENT_VARIABLE} always inlines everything discovered, regardless of root
62
+ * count, for consumers such as code completion that have no tool-calling loop and therefore cannot
63
+ * fetch anything on demand.
64
+ *
65
+ * All three resolve to the empty string when the workspace has no `AGENTS.md`.
66
+ */
67
+ @injectable()
68
+ export class AgentsMdVariableContribution implements AIVariableContribution, AIVariableResolver {
69
+
70
+ @inject(AgentsMdService)
71
+ protected readonly agentsMdService: AgentsMdService;
72
+
73
+ @inject(WorkspaceService)
74
+ protected readonly workspaceService: WorkspaceService;
75
+
76
+ registerVariables(service: AIVariableService): void {
77
+ service.registerResolver(AGENTS_MD_AUTO_LOADED_VARIABLE, this);
78
+ service.registerResolver(AGENTS_MD_VARIABLE, this);
79
+ service.registerResolver(AGENTS_MD_CONTENT_VARIABLE, this);
80
+ }
81
+
82
+ canResolve(request: AIVariableResolutionRequest, _context: AIVariableContext): MaybePromise<number> {
83
+ if (request.variable.name === AGENTS_MD_AUTO_LOADED_VARIABLE.name
84
+ || request.variable.name === AGENTS_MD_VARIABLE.name
85
+ || request.variable.name === AGENTS_MD_CONTENT_VARIABLE.name) {
86
+ return 1;
87
+ }
88
+ return -1;
89
+ }
90
+
91
+ async resolve(request: AIVariableResolutionRequest, _context: AIVariableContext): Promise<ResolvedAIVariable | undefined> {
92
+ // Without this a request issued before the initial scan completes would silently report that
93
+ // the workspace has no project context.
94
+ await this.agentsMdService.ready;
95
+ const files = this.agentsMdService.getAgentsMdFiles();
96
+
97
+ if (request.variable.name === AGENTS_MD_AUTO_LOADED_VARIABLE.name) {
98
+ return { variable: request.variable, value: this.generateAutoLoaded(files) };
99
+ }
100
+ if (request.variable.name === AGENTS_MD_VARIABLE.name) {
101
+ // Suppressed when the root file is already in context, so the model is never told to read
102
+ // a file it has just been handed.
103
+ return { variable: request.variable, value: this.isAutoLoading() ? '' : this.generateAgentsMdXML(files) };
104
+ }
105
+ if (request.variable.name === AGENTS_MD_CONTENT_VARIABLE.name) {
106
+ return { variable: request.variable, value: this.mergeContent(files) };
107
+ }
108
+ return undefined;
109
+ }
110
+
111
+ /**
112
+ * Whether the workspace root's `AGENTS.md` is put into context rather than advertised. Keyed on
113
+ * the number of workspace **roots**, not the number of discovered files: a workspace with several
114
+ * roots has no single project root whose file could stand for the whole session, even when only
115
+ * one of those roots happens to carry an `AGENTS.md`.
116
+ */
117
+ protected isAutoLoading(): boolean {
118
+ return this.workspaceService.tryGetRoots().length === 1;
119
+ }
120
+
121
+ /**
122
+ * Inlines the single root's file. The content is raw Markdown and not XML-escaped; only the path,
123
+ * which is an attribute, is.
124
+ */
125
+ protected generateAutoLoaded(files: AgentsMdFile[]): string {
126
+ if (!this.isAutoLoading() || files.length === 0) {
127
+ return '';
128
+ }
129
+ const file = files[0];
130
+ return `<project_instructions path="${this.escapeXml(this.toWorkspaceRelativePath(file))}">\n${this.escapeFunctionReferences(file.content)}\n</project_instructions>`;
131
+ }
132
+
133
+ /**
134
+ * Advertises the files by their `<rootName>/AGENTS.md` path, the path format the workspace tools
135
+ * accept and return, so the model can pass it to `getFileContent` verbatim.
136
+ */
137
+ protected generateAgentsMdXML(files: AgentsMdFile[]): string {
138
+ if (files.length === 0) {
139
+ return '';
140
+ }
141
+ const entries = files.map(file => `<file>${this.escapeXml(this.toWorkspaceRelativePath(file))}</file>`).join('\n');
142
+ return `<agents_md_files>\n${entries}\n</agents_md_files>`;
143
+ }
144
+
145
+ /**
146
+ * Inlines the content. A single root contributes its file verbatim; only a multi-root workspace
147
+ * gets provenance headings, mirroring how equal-priority prompt fragment sources are merged.
148
+ */
149
+ protected mergeContent(files: AgentsMdFile[]): string {
150
+ if (files.length === 0) {
151
+ return '';
152
+ }
153
+ if (files.length === 1) {
154
+ return this.escapeFunctionReferences(files[0].content);
155
+ }
156
+ return files.map(file => `### ${file.rootName}\n\n${this.escapeFunctionReferences(file.content)}`).join('\n\n');
157
+ }
158
+
159
+ /**
160
+ * Keeps `~{...}` in inlined content literal. Function references are matched on the resolved
161
+ * prompt text, so a `~{someTool}` in a file anyone in the repository can edit would otherwise
162
+ * attach that tool to every agent using the fragment. A zero-width space between `~` and `{`
163
+ * breaks the match without changing what the model reads. `{{...}}` needs no such treatment, as
164
+ * variables are not resolved recursively.
165
+ */
166
+ protected escapeFunctionReferences(content: string): string {
167
+ return content.replace(/~\{/g, '~\u200B{');
168
+ }
169
+
170
+ protected toWorkspaceRelativePath(file: AgentsMdFile): string {
171
+ return `${file.rootName}/${AGENTS_MD_FILE_NAME}`;
172
+ }
173
+
174
+ protected escapeXml(text: string): string {
175
+ return text
176
+ .replace(/&/g, '&amp;')
177
+ .replace(/</g, '&lt;')
178
+ .replace(/>/g, '&gt;')
179
+ .replace(/"/g, '&quot;')
180
+ .replace(/'/g, '&apos;');
181
+ }
182
+ }
@@ -67,6 +67,7 @@ import { DefaultPromptFragmentCustomizationService } from './frontend-prompt-cus
67
67
  import { DefaultFrontendVariableService, FrontendVariableService } from './frontend-variable-service';
68
68
  import { PromptTemplateContribution } from './prompttemplate-contribution';
69
69
  import { FileVariableContribution } from './file-variable-contribution';
70
+ import { WorkspaceRelativePathResolver } from './workspace-relative-path-resolver';
70
71
  import { TheiaVariableContribution } from './theia-variable-contribution';
71
72
  import { TodayVariableContribution } from '../common/today-variable-contribution';
72
73
  import { AgentsVariableContribution } from '../common/agents-variable-contribution';
@@ -84,6 +85,11 @@ import { ProductNameVariableContribution } from './product-name-variable-contrib
84
85
  import { CapabilityVariableContribution } from '../common/capability-variable-contribution';
85
86
  import { GenericCapabilitiesVariableContribution } from './generic-capabilities-variable-contribution';
86
87
  import { GenericCapabilitiesPromptFragmentContribution } from './generic-capabilities-prompt-fragment-contribution';
88
+ import { AgentsMdService, DefaultAgentsMdService } from './agents-md-service';
89
+ import { AgentsMdVariableContribution } from './agents-md-variable-contribution';
90
+ import { AgentsMdPromptFragmentContribution } from './agents-md-prompt-fragment-contribution';
91
+ import { AgentsMdMigrationService } from './agents-md-migration-service';
92
+ import { AgentsMdFrontendApplicationContribution } from './agents-md-frontend-application-contribution';
87
93
  import { LanguageModelService } from '../common/language-model-service';
88
94
  import { FrontendLanguageModelServiceImpl } from './frontend-language-model-service';
89
95
  import { TokenUsageFrontendService } from './token-usage-frontend-service';
@@ -142,6 +148,13 @@ export default new ContainerModule(bind => {
142
148
  bind(SkillPromptCoordinator).toSelf().inSingletonScope();
143
149
  bind(FrontendApplicationContribution).toService(SkillPromptCoordinator);
144
150
 
151
+ bind(DefaultAgentsMdService).toSelf().inSingletonScope();
152
+ bind(AgentsMdService).toService(DefaultAgentsMdService);
153
+ bind(AgentsMdMigrationService).toSelf().inSingletonScope();
154
+ bind(AgentsMdFrontendApplicationContribution).toSelf().inSingletonScope();
155
+ bind(FrontendApplicationContribution).toService(AgentsMdFrontendApplicationContribution);
156
+ bind(CommandContribution).toService(AgentsMdFrontendApplicationContribution);
157
+
145
158
  bindRootContributionProvider(bind, SkillDirectoryContribution);
146
159
 
147
160
  bindRootContributionProvider(bind, AIVariableContribution);
@@ -156,12 +169,15 @@ export default new ContainerModule(bind => {
156
169
  bind(PromptVariableContribution).toSelf().inSingletonScope();
157
170
  bind(AIVariableContribution).toService(PromptVariableContribution);
158
171
  bind(AIVariableContribution).to(TodayVariableContribution).inSingletonScope();
172
+ bind(WorkspaceRelativePathResolver).toSelf().inSingletonScope();
159
173
  bind(AIVariableContribution).to(FileVariableContribution).inSingletonScope();
160
174
  bind(AgentsVariableContribution).toSelf().inSingletonScope();
161
175
  bind(AIVariableContribution).toService(AgentsVariableContribution);
162
176
  bind(AIVariableContribution).to(OpenEditorsVariableContribution).inSingletonScope();
163
177
  bind(SkillsVariableContribution).toSelf().inSingletonScope();
164
178
  bind(AIVariableContribution).toService(SkillsVariableContribution);
179
+ bind(AgentsMdVariableContribution).toSelf().inSingletonScope();
180
+ bind(AIVariableContribution).toService(AgentsMdVariableContribution);
165
181
  bind(AIVariableContribution).to(CapabilityVariableContribution).inSingletonScope();
166
182
  bind(AIVariableContribution).to(ProductNameVariableContribution).inSingletonScope();
167
183
 
@@ -171,6 +187,9 @@ export default new ContainerModule(bind => {
171
187
  bind(GenericCapabilitiesPromptFragmentContribution).toSelf().inSingletonScope();
172
188
  bind(FrontendApplicationContribution).toService(GenericCapabilitiesPromptFragmentContribution);
173
189
 
190
+ bind(AgentsMdPromptFragmentContribution).toSelf().inSingletonScope();
191
+ bind(FrontendApplicationContribution).toService(AgentsMdPromptFragmentContribution);
192
+
174
193
  bind(FrontendApplicationContribution).to(AICoreFrontendApplicationContribution).inSingletonScope();
175
194
 
176
195
  bind(ToolInvocationRegistry).to(ToolInvocationRegistryImpl).inSingletonScope();