@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.
Files changed (82) hide show
  1. package/lib/browser/ai-configuration-service-impl.d.ts +32 -0
  2. package/lib/browser/ai-configuration-service-impl.d.ts.map +1 -0
  3. package/lib/browser/ai-configuration-service-impl.js +120 -0
  4. package/lib/browser/ai-configuration-service-impl.js.map +1 -0
  5. package/lib/browser/ai-configuration-service-impl.spec.d.ts +2 -0
  6. package/lib/browser/ai-configuration-service-impl.spec.d.ts.map +1 -0
  7. package/lib/browser/ai-configuration-service-impl.spec.js +214 -0
  8. package/lib/browser/ai-configuration-service-impl.spec.js.map +1 -0
  9. package/lib/browser/ai-core-frontend-module.d.ts.map +1 -1
  10. package/lib/browser/ai-core-frontend-module.js +4 -0
  11. package/lib/browser/ai-core-frontend-module.js.map +1 -1
  12. package/lib/browser/ai-settings-service.d.ts +2 -5
  13. package/lib/browser/ai-settings-service.d.ts.map +1 -1
  14. package/lib/browser/ai-settings-service.js +8 -15
  15. package/lib/browser/ai-settings-service.js.map +1 -1
  16. package/lib/browser/frontend-language-model-alias-registry.d.ts +2 -4
  17. package/lib/browser/frontend-language-model-alias-registry.d.ts.map +1 -1
  18. package/lib/browser/frontend-language-model-alias-registry.js +11 -16
  19. package/lib/browser/frontend-language-model-alias-registry.js.map +1 -1
  20. package/lib/browser/frontend-language-model-service.d.ts +2 -3
  21. package/lib/browser/frontend-language-model-service.d.ts.map +1 -1
  22. package/lib/browser/frontend-language-model-service.js +6 -6
  23. package/lib/browser/frontend-language-model-service.js.map +1 -1
  24. package/lib/browser/frontend-prompt-customization-service.d.ts +38 -0
  25. package/lib/browser/frontend-prompt-customization-service.d.ts.map +1 -1
  26. package/lib/browser/frontend-prompt-customization-service.js +189 -76
  27. package/lib/browser/frontend-prompt-customization-service.js.map +1 -1
  28. package/lib/browser/frontend-prompt-customization-service.spec.js +103 -0
  29. package/lib/browser/frontend-prompt-customization-service.spec.js.map +1 -1
  30. package/lib/browser/index.d.ts +1 -0
  31. package/lib/browser/index.d.ts.map +1 -1
  32. package/lib/browser/index.js +1 -0
  33. package/lib/browser/index.js.map +1 -1
  34. package/lib/browser/trust-aware-preference-reader.d.ts +13 -1
  35. package/lib/browser/trust-aware-preference-reader.d.ts.map +1 -1
  36. package/lib/browser/trust-aware-preference-reader.js +21 -2
  37. package/lib/browser/trust-aware-preference-reader.js.map +1 -1
  38. package/lib/browser/trust-aware-preference-reader.spec.js +26 -0
  39. package/lib/browser/trust-aware-preference-reader.spec.js.map +1 -1
  40. package/lib/common/ai-configuration-service.d.ts +104 -0
  41. package/lib/common/ai-configuration-service.d.ts.map +1 -0
  42. package/lib/common/ai-configuration-service.js +20 -0
  43. package/lib/common/ai-configuration-service.js.map +1 -0
  44. package/lib/common/ai-core-preferences.d.ts +4 -0
  45. package/lib/common/ai-core-preferences.d.ts.map +1 -1
  46. package/lib/common/ai-core-preferences.js +20 -1
  47. package/lib/common/ai-core-preferences.js.map +1 -1
  48. package/lib/common/index.d.ts +1 -0
  49. package/lib/common/index.d.ts.map +1 -1
  50. package/lib/common/index.js +1 -0
  51. package/lib/common/index.js.map +1 -1
  52. package/lib/common/language-model.d.ts +56 -2
  53. package/lib/common/language-model.d.ts.map +1 -1
  54. package/lib/common/language-model.js +53 -3
  55. package/lib/common/language-model.js.map +1 -1
  56. package/lib/common/language-model.spec.js +75 -0
  57. package/lib/common/language-model.spec.js.map +1 -1
  58. package/lib/common/prompt-service.d.ts +13 -3
  59. package/lib/common/prompt-service.d.ts.map +1 -1
  60. package/lib/common/prompt-service.js.map +1 -1
  61. package/lib/node/backend-language-model-registry.d.ts.map +1 -1
  62. package/lib/node/backend-language-model-registry.js +1 -0
  63. package/lib/node/backend-language-model-registry.js.map +1 -1
  64. package/package.json +11 -11
  65. package/src/browser/ai-configuration-service-impl.spec.ts +254 -0
  66. package/src/browser/ai-configuration-service-impl.ts +120 -0
  67. package/src/browser/ai-core-frontend-module.ts +7 -1
  68. package/src/browser/ai-settings-service.ts +8 -16
  69. package/src/browser/frontend-language-model-alias-registry.ts +12 -16
  70. package/src/browser/frontend-language-model-service.ts +5 -6
  71. package/src/browser/frontend-prompt-customization-service.spec.ts +130 -1
  72. package/src/browser/frontend-prompt-customization-service.ts +204 -84
  73. package/src/browser/index.ts +1 -0
  74. package/src/browser/trust-aware-preference-reader.spec.ts +29 -0
  75. package/src/browser/trust-aware-preference-reader.ts +23 -3
  76. package/src/common/ai-configuration-service.ts +128 -0
  77. package/src/common/ai-core-preferences.ts +23 -1
  78. package/src/common/index.ts +1 -0
  79. package/src/common/language-model.spec.ts +98 -1
  80. package/src/common/language-model.ts +107 -4
  81. package/src/common/prompt-service.ts +14 -3
  82. package/src/node/backend-language-model-registry.ts +1 -0
@@ -0,0 +1,128 @@
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 { Event } from '@theia/core';
18
+ import { JSONValue } from '@theia/core/shared/@lumino/coreutils';
19
+ import { PreferenceInspection, PreferenceScope } from '@theia/core/lib/common/preferences';
20
+
21
+ export const AiConfigurationService = Symbol('AiConfigurationService');
22
+
23
+ /**
24
+ * A {@link PreferenceInspection} augmented with a derived, workspace-trust-aware source scope.
25
+ */
26
+ export interface AiConfigurationInspection<T extends JSONValue = JSONValue> extends PreferenceInspection<T> {
27
+ /**
28
+ * The narrowest scope (honoring workspace trust) in which a value is explicitly set, i.e. the
29
+ * scope that determines the effective {@link PreferenceInspection.value}. Walked in precedence
30
+ * order `Folder -> Workspace -> User`; `undefined` when only the default value applies. In an
31
+ * untrusted workspace the folder/workspace scopes are ignored, so `sourceScope` is either
32
+ * `User` or `undefined`.
33
+ */
34
+ sourceScope?: PreferenceScope;
35
+ }
36
+
37
+ /**
38
+ * Payload of {@link AiConfigurationService.onDidChange}.
39
+ */
40
+ export interface AiConfigurationChange {
41
+ /**
42
+ * The preference key that changed. When the change is caused by a workspace-trust transition
43
+ * (which can change the effective value of any trust-gated key) this is `undefined` and
44
+ * {@link affects} returns `true` for every resource. Listeners tracking a specific key should
45
+ * re-query the value when `preferenceName` is `undefined` or equals their key.
46
+ */
47
+ readonly preferenceName?: string;
48
+ /**
49
+ * Tests whether the given resource is affected by the change.
50
+ * @param resourceUri the uri of the resource to test.
51
+ */
52
+ affects(resourceUri?: string): boolean;
53
+ /**
54
+ * Tests whether the given preference key is affected by this change, treating a workspace-trust
55
+ * transition (where {@link preferenceName} is `undefined`) as affecting every trust-gated key.
56
+ * Prefer this over comparing {@link preferenceName} directly so listeners do not silently miss
57
+ * trust transitions.
58
+ * @param preferenceName the preference key to test.
59
+ */
60
+ affectsPreference(preferenceName: string): boolean;
61
+ }
62
+
63
+ /**
64
+ * Framework API for reading and writing AI-related (`ai-features.*`) preferences.
65
+ *
66
+ * `AiConfigurationService` wraps the core `PreferenceService` and is the intended extension point
67
+ * for adopters and extensions that configure AI features: prefer it over talking to
68
+ * `PreferenceService` directly for `ai-features.*` keys. Routing all AI configuration through this
69
+ * seam keeps consumers insulated from future changes to how AI preferences are stored.
70
+ *
71
+ * Reads ({@link get}, {@link inspect}) are **workspace-trust-aware**: workspace and folder scope
72
+ * values are suppressed while the workspace is untrusted (failing closed until the trust state
73
+ * resolves), matching how AI features read preferences today. Writes ({@link set}, {@link update})
74
+ * are never gated by trust.
75
+ */
76
+ export interface AiConfigurationService {
77
+ /**
78
+ * Resolves once the underlying preference service and the initial workspace-trust state are
79
+ * ready. Await this before the first read for a deterministic, trust-aware result.
80
+ */
81
+ readonly ready: Promise<void>;
82
+
83
+ /**
84
+ * Retrieves the effective, trust-aware value for the given preference.
85
+ *
86
+ * @param key the preference identifier.
87
+ * @param defaultValue the value to return when no value is stored.
88
+ * @param resourceUri the uri of the resource for which the preference is read.
89
+ */
90
+ get<T>(key: string, defaultValue?: T, resourceUri?: string): T | undefined;
91
+
92
+ /**
93
+ * Writes `value` to the given scope. Maps to `PreferenceService.set`.
94
+ *
95
+ * @param key the preference identifier.
96
+ * @param value the new value (must be JSON-serializable). `undefined` clears the value in the given scope.
97
+ * @param scope the scope to write to. For {@link PreferenceScope.Folder} a `resourceUri` is required.
98
+ * @param resourceUri the uri of the resource for which the preference is stored.
99
+ */
100
+ set(key: string, value: unknown, scope: PreferenceScope, resourceUri?: string): Promise<void>;
101
+
102
+ /**
103
+ * "Smart write": picks the scope so that the effective value becomes `value`. Maps to
104
+ * `PreferenceService.updateValue`.
105
+ *
106
+ * @param key the preference identifier.
107
+ * @param value the value to apply (must be JSON-serializable). `undefined` resets the preference to its default value.
108
+ * @param resourceUri the uri of the resource to which the change applies.
109
+ */
110
+ update(key: string, value: unknown, resourceUri?: string): Promise<void>;
111
+
112
+ /**
113
+ * Retrieves the per-scope values for the given preference, enriched with a derived,
114
+ * trust-aware {@link AiConfigurationInspection.sourceScope} and effective
115
+ * {@link PreferenceInspection.value}.
116
+ *
117
+ * @param key the preference identifier.
118
+ * @param resourceUri the uri of the resource for which the preference is inspected.
119
+ */
120
+ inspect<T extends JSONValue>(key: string, resourceUri?: string): AiConfigurationInspection<T> | undefined;
121
+
122
+ /**
123
+ * Fires when a `ai-features.*` preference changes or when the workspace-trust state transitions
124
+ * (which can change the effective value of trust-gated keys). The effective value is not carried
125
+ * on the event; listeners re-query it via {@link get}/{@link inspect}.
126
+ */
127
+ onDidChange: Event<AiConfigurationChange>;
128
+ }
@@ -24,7 +24,7 @@ import {
24
24
  NOTIFICATION_TYPE_DESCRIPTIONS,
25
25
  NotificationType
26
26
  } from './notification-types';
27
- import { ReasoningSettings } from './language-model';
27
+ import { ReasoningSettings, SERVER_SIDE_COMPACTION_TOKEN_THRESHOLD_MINIMUM } from './language-model';
28
28
  import { PreferenceSchema } from '@theia/core/lib/common/preferences/preference-schema';
29
29
 
30
30
  export const AI_CORE_PREFERENCES_TITLE = nls.localize('theia/ai-core/preferences/title', 'AI Features');
@@ -34,11 +34,31 @@ export const PREFERENCE_NAME_REASONING = 'ai-features.reasoning.defaults';
34
34
  export const PREFERENCE_NAME_MAX_RETRIES = 'ai-features.modelSettings.maxRetries';
35
35
  export const PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE = 'ai-features.notifications.default';
36
36
  export const PREFERENCE_NAME_SKILL_DIRECTORIES = 'ai-features.skills.skillDirectories';
37
+ export const PREFERENCE_NAME_SERVER_SIDE_COMPACTION = 'ai-features.chat.serverSideCompaction';
38
+ export const PREFERENCE_NAME_SERVER_SIDE_COMPACTION_TOKEN_THRESHOLD = 'ai-features.chat.serverSideCompactionTokenThreshold';
37
39
 
38
40
  export const LANGUAGE_MODEL_ALIASES_PREFERENCE = 'ai-features.languageModelAliases';
39
41
 
40
42
  export const aiCorePreferenceSchema: PreferenceSchema = {
41
43
  properties: {
44
+ [PREFERENCE_NAME_SERVER_SIDE_COMPACTION]: {
45
+ title: AI_CORE_PREFERENCES_TITLE,
46
+ type: 'boolean',
47
+ description: nls.localize('theia/ai/core/serverSideCompaction/description',
48
+ 'Enable provider-native server-side compaction by default for chat requests. When on, models that support it ' +
49
+ 'may automatically summarize older turns once the conversation grows past their own threshold, letting long ' +
50
+ 'sessions continue past the context limit. Each provider offers its own override, and individual chat sessions ' +
51
+ 'can override this in their session settings. Models without support ignore it.'),
52
+ default: true,
53
+ },
54
+ [PREFERENCE_NAME_SERVER_SIDE_COMPACTION_TOKEN_THRESHOLD]: {
55
+ title: AI_CORE_PREFERENCES_TITLE,
56
+ type: 'integer',
57
+ minimum: SERVER_SIDE_COMPACTION_TOKEN_THRESHOLD_MINIMUM,
58
+ description: nls.localize('theia/ai/core/serverSideCompactionTokenThreshold/description',
59
+ 'Default input-token threshold at which provider-native server-side compaction should run. Providers and individual chat sessions ' +
60
+ 'can override this value. When unset, the provider chooses its default threshold. If set, the value must be at least 50,000 tokens.'),
61
+ },
42
62
  [PREFERENCE_NAME_PROMPT_TEMPLATES]: {
43
63
  title: AI_CORE_PREFERENCES_TITLE,
44
64
  description: nls.localize('theia/ai/core/promptTemplates/description',
@@ -239,6 +259,8 @@ export interface AICoreConfiguration {
239
259
  [PREFERENCE_NAME_MAX_RETRIES]: number | undefined;
240
260
  [PREFERENCE_NAME_DEFAULT_NOTIFICATION_TYPE]: NotificationType | undefined;
241
261
  [PREFERENCE_NAME_SKILL_DIRECTORIES]: string[] | undefined;
262
+ [PREFERENCE_NAME_SERVER_SIDE_COMPACTION]: boolean | undefined;
263
+ [PREFERENCE_NAME_SERVER_SIDE_COMPACTION_TOKEN_THRESHOLD]: number | undefined;
242
264
  }
243
265
 
244
266
  export interface RequestSetting {
@@ -31,6 +31,7 @@ export * from './protocol';
31
31
  export * from './today-variable-contribution';
32
32
  export * from './variable-service';
33
33
  export * from './settings-service';
34
+ export * from './ai-configuration-service';
34
35
  export * from './language-model-service';
35
36
  export * from './token-usage-service';
36
37
  export * from './ai-variable-resource';
@@ -15,7 +15,19 @@
15
15
  // *****************************************************************************
16
16
 
17
17
  import { expect } from 'chai';
18
- import { isModelMatching, isToolCallContent, LanguageModel, LanguageModelSelector } from './language-model';
18
+ import {
19
+ DefaultLanguageModelRegistryImpl,
20
+ isCompactionResponsePart,
21
+ isLanguageModelStreamResponsePart,
22
+ isModelMatching,
23
+ isToolCallContent,
24
+ LanguageModel,
25
+ LanguageModelSelector,
26
+ resolveCompactionDefault,
27
+ resolveCompactionTokenThreshold,
28
+ resolveCompactionTokenThresholdDefault,
29
+ resolveServerSideCompaction
30
+ } from './language-model';
19
31
 
20
32
  describe('isModelMatching', () => {
21
33
  it('returns false with one of two parameter mismatches', () => {
@@ -163,3 +175,88 @@ describe('isToolCallContent', () => {
163
175
  });
164
176
 
165
177
  });
178
+
179
+ describe('DefaultLanguageModelRegistryImpl', () => {
180
+ function createRegistry(): DefaultLanguageModelRegistryImpl {
181
+ const registry = new DefaultLanguageModelRegistryImpl();
182
+ // No DI container in this unit test, so resolve the internal `initialized` gate manually.
183
+ (registry as unknown as { markInitialized: () => void }).markInitialized();
184
+ return registry;
185
+ }
186
+
187
+ function model(id: string): LanguageModel {
188
+ return { id, status: { status: 'ready' } } as LanguageModel;
189
+ }
190
+
191
+ it('getLanguageModels returns a fresh array so consumers can detect changes by reference', async () => {
192
+ const registry = createRegistry();
193
+ registry.addLanguageModels([model('a')]);
194
+
195
+ const first = await registry.getLanguageModels();
196
+ expect(first).to.have.length(1);
197
+
198
+ registry.addLanguageModels([model('b')]);
199
+ const second = await registry.getLanguageModels();
200
+
201
+ // A new snapshot must be a different array reference (React memoization relies on this).
202
+ expect(second).to.not.equal(first);
203
+ // The earlier snapshot must not be mutated in place by later registry changes.
204
+ expect(first.map(m => m.id)).to.deep.equal(['a']);
205
+ expect(second.map(m => m.id)).to.deep.equal(['a', 'b']);
206
+ });
207
+
208
+ it('reflects removals in a fresh snapshot without mutating an earlier one', async () => {
209
+ const registry = createRegistry();
210
+ registry.addLanguageModels([model('a'), model('b')]);
211
+ const before = await registry.getLanguageModels();
212
+
213
+ registry.removeLanguageModels(['a']);
214
+ const after = await registry.getLanguageModels();
215
+
216
+ expect(before.map(m => m.id)).to.deep.equal(['a', 'b']);
217
+ expect(after.map(m => m.id)).to.deep.equal(['b']);
218
+ });
219
+ });
220
+
221
+ describe('compaction contract', () => {
222
+ it('recognizes a compaction response part', () => {
223
+ const part = { compaction: { provider: 'anthropic', data: { foo: 1 } } };
224
+ expect(isCompactionResponsePart(part)).to.equal(true);
225
+ expect(isLanguageModelStreamResponsePart(part)).to.equal(true);
226
+ });
227
+ it('rejects a non-compaction part', () => {
228
+ expect(isCompactionResponsePart({ content: 'hi' })).to.equal(false);
229
+ // eslint-disable-next-line no-null/no-null
230
+ expect(isCompactionResponsePart(null)).to.equal(false);
231
+ expect(isCompactionResponsePart(undefined)).to.equal(false);
232
+ // eslint-disable-next-line no-null/no-null
233
+ expect(isCompactionResponsePart({ compaction: null })).to.equal(false);
234
+ expect(isCompactionResponsePart({ compaction: { provider: 42 } })).to.equal(false);
235
+ });
236
+ it('resolves a model default from the global preference and the per-provider override', () => {
237
+ expect(resolveCompactionDefault(true, 'default')).to.equal(true);
238
+ expect(resolveCompactionDefault(false, 'default')).to.equal(false);
239
+ expect(resolveCompactionDefault(false, 'enabled')).to.equal(true);
240
+ expect(resolveCompactionDefault(true, 'disabled')).to.equal(false);
241
+ });
242
+ it('resolves compaction token thresholds with session, provider, global precedence', () => {
243
+ expect(resolveCompactionTokenThresholdDefault(undefined, undefined)).to.equal(undefined);
244
+ expect(resolveCompactionTokenThresholdDefault(100_000, undefined)).to.equal(100_000);
245
+ expect(resolveCompactionTokenThresholdDefault(100_000, 200_000)).to.equal(200_000);
246
+ expect(resolveCompactionTokenThreshold(undefined, undefined)).to.equal(undefined);
247
+ expect(resolveCompactionTokenThreshold(200_000, undefined)).to.equal(200_000);
248
+ expect(resolveCompactionTokenThreshold(200_000, { tokenThreshold: 300_000 })).to.equal(300_000);
249
+ });
250
+ it('resolves server-side compaction with capability gate and session-wins precedence', () => {
251
+ // capability gate
252
+ expect(resolveServerSideCompaction(false, true, { enabled: true })).to.equal(false);
253
+ expect(resolveServerSideCompaction(undefined, true, undefined)).to.equal(false);
254
+ // no per-session setting -> model default
255
+ expect(resolveServerSideCompaction(true, true, undefined)).to.equal(true);
256
+ expect(resolveServerSideCompaction(true, false, undefined)).to.equal(false);
257
+ expect(resolveServerSideCompaction(true, true, {})).to.equal(true);
258
+ // explicit per-session setting wins over the model default (which already folds in the per-provider override)
259
+ expect(resolveServerSideCompaction(true, false, { enabled: true })).to.equal(true);
260
+ expect(resolveServerSideCompaction(true, true, { enabled: false })).to.equal(false);
261
+ });
262
+ });
@@ -42,7 +42,8 @@ export interface ReasoningSupport {
42
42
  readonly defaultLevel?: ReasoningLevel;
43
43
  }
44
44
 
45
- export type LanguageModelMessage = TextMessage | ThinkingMessage | ToolUseMessage | ToolResultMessage | ServerToolUseMessage | ImageMessage;
45
+ export type LanguageModelMessage =
46
+ TextMessage | ThinkingMessage | ToolUseMessage | ToolResultMessage | ServerToolUseMessage | ImageMessage | CompactionMessage;
46
47
  export namespace LanguageModelMessage {
47
48
 
48
49
  export function isTextMessage(obj: LanguageModelMessage): obj is TextMessage {
@@ -63,6 +64,9 @@ export namespace LanguageModelMessage {
63
64
  export function isImageMessage(obj: LanguageModelMessage): obj is ImageMessage {
64
65
  return obj.type === 'image';
65
66
  }
67
+ export function isCompactionMessage(obj: LanguageModelMessage): obj is CompactionMessage {
68
+ return obj.type === 'compaction';
69
+ }
66
70
  }
67
71
  export interface TextMessage {
68
72
  actor: MessageActor;
@@ -127,6 +131,17 @@ export interface ImageMessage {
127
131
  image: ImageContent;
128
132
  }
129
133
 
134
+ export interface CompactionMessage {
135
+ actor: 'ai';
136
+ type: 'compaction';
137
+ /** Originating provider tag; a backend replays the payload only when this matches its own provider. */
138
+ provider: string;
139
+ /** Opaque provider payload to replay. */
140
+ data: unknown;
141
+ /** Human-readable summary, when the provider exposes one. */
142
+ summary?: string;
143
+ }
144
+
130
145
  export const isLanguageModelRequestMessage = (obj: unknown): obj is LanguageModelMessage =>
131
146
  !!(obj && typeof obj === 'object' &&
132
147
  'type' in obj &&
@@ -304,6 +319,69 @@ export namespace ToolRequest {
304
319
  (!('required' in obj) || (Array.isArray(obj.required) && obj.required.every(prop => typeof prop === 'string')));
305
320
  }
306
321
  }
322
+ // Anthropic requires at least 50,000 tokens, so use one conservative minimum for all compaction settings.
323
+ export const SERVER_SIDE_COMPACTION_TOKEN_THRESHOLD_MINIMUM = 50_000;
324
+
325
+ /**
326
+ * Per-session/per-request server-side compaction settings, carried verbatim from the chat
327
+ * session's common settings to the request. Kept as an object so further parameters can be
328
+ * added later.
329
+ */
330
+ export interface CompactionSettings {
331
+ /** Explicit enablement for this session; when set it wins over the model's default. `undefined` means "no explicit choice". */
332
+ enabled?: boolean;
333
+ /** Input-token threshold for this session; when set it wins over the model's default. `undefined` preserves the provider default. */
334
+ tokenThreshold?: number;
335
+ }
336
+
337
+ /** Per-provider override for server-side compaction; combined with the global preference by {@link resolveCompactionDefault}. */
338
+ export type ServerSideCompactionSetting = 'default' | 'enabled' | 'disabled';
339
+
340
+ /**
341
+ * Resolves a model's default server-side compaction enablement from the global preference and
342
+ * the per-provider override. `'enabled'`/`'disabled'` force the result; `'default'` defers to
343
+ * the global preference. Intended to be called where the preferences are read (the provider's
344
+ * frontend contribution) and stored on the model.
345
+ */
346
+ export function resolveCompactionDefault(globalEnabled: boolean, perProviderOverride: ServerSideCompactionSetting): boolean {
347
+ if (perProviderOverride === 'enabled') {
348
+ return true;
349
+ }
350
+ if (perProviderOverride === 'disabled') {
351
+ return false;
352
+ }
353
+ return globalEnabled;
354
+ }
355
+
356
+ export function resolveCompactionTokenThresholdDefault(
357
+ globalThreshold: number | undefined,
358
+ perProviderThreshold: number | undefined
359
+ ): number | undefined {
360
+ return perProviderThreshold ?? globalThreshold;
361
+ }
362
+
363
+ export function resolveCompactionTokenThreshold(
364
+ thresholdByDefault: number | undefined,
365
+ compaction: CompactionSettings | undefined
366
+ ): number | undefined {
367
+ return compaction?.tokenThreshold ?? thresholdByDefault;
368
+ }
369
+
370
+ /**
371
+ * Resolves whether server-side compaction is effective for a request: the model must support it
372
+ * (capability), then an explicit per-session setting wins, otherwise the model's resolved default applies.
373
+ */
374
+ export function resolveServerSideCompaction(
375
+ capability: boolean | undefined,
376
+ enabledByDefault: boolean,
377
+ compaction: CompactionSettings | undefined
378
+ ): boolean {
379
+ if (!capability) {
380
+ return false;
381
+ }
382
+ return compaction?.enabled ?? enabledByDefault;
383
+ }
384
+
307
385
  export interface LanguageModelRequest {
308
386
  messages: LanguageModelMessage[],
309
387
  tools?: ToolRequest[];
@@ -323,6 +401,8 @@ export interface LanguageModelRequest {
323
401
  clientSettings?: { keepToolCalls: boolean; keepThinking: boolean };
324
402
  /** Provider-agnostic reasoning configuration; providers translate it to their native API. */
325
403
  reasoning?: ReasoningSettings;
404
+ /** Provider-agnostic server-side compaction settings, copied verbatim from the chat session. Resolved against the model's capability and default in the backend. */
405
+ compaction?: CompactionSettings;
326
406
  }
327
407
  export interface ResponseFormatJsonSchema {
328
408
  type: 'json_schema';
@@ -378,10 +458,12 @@ export interface LanguageModelTextResponse {
378
458
  export const isLanguageModelTextResponse = (obj: unknown): obj is LanguageModelTextResponse =>
379
459
  !!(obj && typeof obj === 'object' && 'text' in obj && typeof (obj as { text: unknown }).text === 'string');
380
460
 
381
- export type LanguageModelStreamResponsePart = TextResponsePart | ToolCallResponsePart | ServerToolCallResponsePart | ThinkingResponsePart | UsageResponsePart;
461
+ export type LanguageModelStreamResponsePart =
462
+ TextResponsePart | ToolCallResponsePart | ServerToolCallResponsePart | ThinkingResponsePart | UsageResponsePart | CompactionResponsePart;
382
463
 
383
464
  export const isLanguageModelStreamResponsePart = (part: unknown): part is LanguageModelStreamResponsePart =>
384
- isUsageResponsePart(part) || isTextResponsePart(part) || isThinkingResponsePart(part) || isToolCallResponsePart(part) || isServerToolCallResponsePart(part);
465
+ isUsageResponsePart(part) || isTextResponsePart(part) || isThinkingResponsePart(part) ||
466
+ isToolCallResponsePart(part) || isServerToolCallResponsePart(part) || isCompactionResponsePart(part);
385
467
 
386
468
  export interface UsageResponsePart {
387
469
  input_tokens: number;
@@ -432,6 +514,23 @@ export interface ThinkingResponsePart {
432
514
  export const isThinkingResponsePart = (part: unknown): part is ThinkingResponsePart =>
433
515
  !!(part && typeof part === 'object' && 'thought' in part && typeof part.thought === 'string');
434
516
 
517
+ export interface CompactionResponsePart {
518
+ compaction: {
519
+ /** Originating provider tag, e.g. 'anthropic' or 'openai-responses'. */
520
+ provider: string;
521
+ /** Opaque provider payload (Anthropic compaction block(s) / OpenAI compaction item). Never interpreted outside the originating backend. */
522
+ data: unknown;
523
+ /** Human-readable summary, when the provider exposes one. */
524
+ summary?: string;
525
+ };
526
+ }
527
+ export const isCompactionResponsePart = (part: unknown): part is CompactionResponsePart =>
528
+ !!(part && typeof part === 'object' && 'compaction' in part &&
529
+ typeof (part as CompactionResponsePart).compaction === 'object' &&
530
+ (part as CompactionResponsePart).compaction &&
531
+ 'provider' in (part as CompactionResponsePart).compaction &&
532
+ typeof (part as CompactionResponsePart).compaction.provider === 'string');
533
+
435
534
  export interface ToolCallTextResult { type: 'text', text: string; };
436
535
  export interface ToolCallImageResult extends Base64ImageContent { type: 'image' };
437
536
  export interface ToolCallAudioResult { type: 'audio', data: string; mimeType: string };
@@ -530,6 +629,8 @@ export interface LanguageModelMetaData {
530
629
  * **Note:** If you provide these, you must also provide `vendor` because server tools are vendor-specific.
531
630
  */
532
631
  readonly serverTools?: ServerToolDescriptor[];
632
+ /** Whether this model supports provider-native server-side compaction (capability, distinct from whether it is activated). */
633
+ readonly serverSideCompactionSupport?: boolean;
533
634
  }
534
635
 
535
636
  export namespace LanguageModelMetaData {
@@ -644,7 +745,9 @@ export class DefaultLanguageModelRegistryImpl implements LanguageModelRegistry {
644
745
 
645
746
  async getLanguageModels(): Promise<LanguageModel[]> {
646
747
  await this.initialized;
647
- return this.languageModels;
748
+ // Return a fresh array (not the internal, mutated-in-place list) so consumers relying on
749
+ // reference equality - e.g. React memoization in the chat model selector - detect changes.
750
+ return [...this.languageModels];
648
751
  }
649
752
 
650
753
  async getLanguageModel(id: string): Promise<LanguageModel | undefined> {
@@ -385,11 +385,21 @@ export interface PromptFragmentCustomizationService {
385
385
  createCustomAgentFile(parentDirectory: URI, agent: CustomAgentDescription): Promise<URI>;
386
386
 
387
387
  /**
388
- * Migrates every reachable `customAgents.yml` to the per-agent `agents/<id>/agent.md` layout.
388
+ * Returns `true` if migration would write anything: a scope still holds a legacy
389
+ * `customAgents.yml`, or a previously migrated scope has `agent.md` files that can be corrected
390
+ * (e.g. headings that were folded by an earlier migration). Read-only; performs no writes and is
391
+ * intended for deciding whether to prompt the user before migrating.
392
+ */
393
+ hasPendingCustomAgentMigration(): Promise<boolean>;
394
+
395
+ /**
396
+ * Migrates every reachable `customAgents.yml` to the per-agent `agents/<id>/agent.md` layout and
397
+ * corrects already-migrated `agent.md` files whose headings were folded by an earlier migration.
389
398
  * The user's original content is never deleted: on success (or on partial failure when no backup
390
399
  * exists yet) the YAML is renamed to `customAgents.yml.bak`; if a `.bak` already exists it is not
391
- * overwritten and the YAML is left in place.
392
- * Idempotent rerunning never overwrites an already-migrated agent file.
400
+ * overwritten and the YAML is left in place. Corrections only overwrite `agent.md` files the user
401
+ * has not edited since migration.
402
+ * Idempotent — rerunning never overwrites an already up-to-date agent file.
393
403
  */
394
404
  migrateCustomAgentsYaml(): Promise<Array<{
395
405
  scope: URI;
@@ -399,6 +409,7 @@ export interface PromptFragmentCustomizationService {
399
409
  failed: number;
400
410
  yamlBackedUp: boolean;
401
411
  promptOverridesMigrated: number;
412
+ corrected: number;
402
413
  }>>;
403
414
 
404
415
  /**
@@ -64,6 +64,7 @@ export class BackendLanguageModelRegistryImpl extends DefaultLanguageModelRegist
64
64
  maxOutputTokens: model.maxOutputTokens,
65
65
  reasoningSupport: model.reasoningSupport,
66
66
  serverTools: model.serverTools,
67
+ serverSideCompactionSupport: model.serverSideCompactionSupport,
67
68
  };
68
69
  }
69
70
  }