@theia/ai-chat 1.66.0-next.44 → 1.66.0-next.73
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/agent-delegation-tool.d.ts.map +1 -1
- package/lib/browser/agent-delegation-tool.js +4 -2
- package/lib/browser/agent-delegation-tool.js.map +1 -1
- package/lib/browser/ai-chat-frontend-module.d.ts.map +1 -1
- package/lib/browser/ai-chat-frontend-module.js +15 -0
- package/lib/browser/ai-chat-frontend-module.js.map +1 -1
- package/lib/browser/change-set-file-element-deserializer.d.ts +7 -0
- package/lib/browser/change-set-file-element-deserializer.d.ts.map +1 -0
- package/lib/browser/change-set-file-element-deserializer.js +61 -0
- package/lib/browser/change-set-file-element-deserializer.js.map +1 -0
- package/lib/browser/change-set-file-element.d.ts +3 -1
- package/lib/browser/change-set-file-element.d.ts.map +1 -1
- package/lib/browser/change-set-file-element.js +23 -2
- package/lib/browser/change-set-file-element.js.map +1 -1
- package/lib/browser/change-set-file-service.d.ts.map +1 -1
- package/lib/browser/change-set-file-service.js +2 -2
- package/lib/browser/change-set-file-service.js.map +1 -1
- package/lib/browser/chat-session-store-impl.d.ts +36 -0
- package/lib/browser/chat-session-store-impl.d.ts.map +1 -0
- package/lib/browser/chat-session-store-impl.js +287 -0
- package/lib/browser/chat-session-store-impl.js.map +1 -0
- package/lib/browser/file-chat-variable-contribution.d.ts.map +1 -1
- package/lib/browser/file-chat-variable-contribution.js +1 -1
- package/lib/browser/file-chat-variable-contribution.js.map +1 -1
- package/lib/browser/image-context-variable-contribution.js +1 -1
- package/lib/browser/image-context-variable-contribution.js.map +1 -1
- package/lib/browser/task-context-service.d.ts.map +1 -1
- package/lib/browser/task-context-service.js +8 -2
- package/lib/browser/task-context-service.js.map +1 -1
- package/lib/browser/task-context-variable.d.ts.map +1 -1
- package/lib/browser/task-context-variable.js +7 -3
- package/lib/browser/task-context-variable.js.map +1 -1
- package/lib/common/change-set-element-deserializer.d.ts +30 -0
- package/lib/common/change-set-element-deserializer.d.ts.map +1 -0
- package/lib/common/change-set-element-deserializer.js +81 -0
- package/lib/common/change-set-element-deserializer.js.map +1 -0
- package/lib/common/change-set.d.ts +7 -0
- package/lib/common/change-set.d.ts.map +1 -1
- package/lib/common/change-set.js +1 -1
- package/lib/common/change-set.js.map +1 -1
- package/lib/common/chat-agents-variable-contribution.d.ts.map +1 -1
- package/lib/common/chat-agents-variable-contribution.js +17 -1
- package/lib/common/chat-agents-variable-contribution.js.map +1 -1
- package/lib/common/chat-agents.d.ts.map +1 -1
- package/lib/common/chat-agents.js +3 -3
- package/lib/common/chat-agents.js.map +1 -1
- package/lib/common/chat-auto-save.spec.d.ts +2 -0
- package/lib/common/chat-auto-save.spec.d.ts.map +1 -0
- package/lib/common/chat-auto-save.spec.js +304 -0
- package/lib/common/chat-auto-save.spec.js.map +1 -0
- package/lib/common/chat-content-deserializer.d.ts +161 -0
- package/lib/common/chat-content-deserializer.d.ts.map +1 -0
- package/lib/common/chat-content-deserializer.js +166 -0
- package/lib/common/chat-content-deserializer.js.map +1 -0
- package/lib/common/chat-content-deserializer.spec.d.ts +2 -0
- package/lib/common/chat-content-deserializer.spec.d.ts.map +1 -0
- package/lib/common/chat-content-deserializer.spec.js +307 -0
- package/lib/common/chat-content-deserializer.spec.js.map +1 -0
- package/lib/common/chat-model-serialization.d.ts +110 -0
- package/lib/common/chat-model-serialization.d.ts.map +1 -0
- package/lib/common/chat-model-serialization.js +20 -0
- package/lib/common/chat-model-serialization.js.map +1 -0
- package/lib/common/chat-model-serialization.spec.d.ts +2 -0
- package/lib/common/chat-model-serialization.spec.d.ts.map +1 -0
- package/lib/common/chat-model-serialization.spec.js +278 -0
- package/lib/common/chat-model-serialization.spec.js.map +1 -0
- package/lib/common/chat-model.d.ts +163 -14
- package/lib/common/chat-model.d.ts.map +1 -1
- package/lib/common/chat-model.js +444 -36
- package/lib/common/chat-model.js.map +1 -1
- package/lib/common/chat-request-parser.d.ts +8 -1
- package/lib/common/chat-request-parser.d.ts.map +1 -1
- package/lib/common/chat-request-parser.js +29 -1
- package/lib/common/chat-request-parser.js.map +1 -1
- package/lib/common/chat-request-parser.spec.js +51 -0
- package/lib/common/chat-request-parser.spec.js.map +1 -1
- package/lib/common/chat-service-deletion.spec.d.ts +2 -0
- package/lib/common/chat-service-deletion.spec.d.ts.map +1 -0
- package/lib/common/chat-service-deletion.spec.js +221 -0
- package/lib/common/chat-service-deletion.spec.js.map +1 -0
- package/lib/common/chat-service.d.ts +30 -2
- package/lib/common/chat-service.d.ts.map +1 -1
- package/lib/common/chat-service.js +182 -10
- package/lib/common/chat-service.js.map +1 -1
- package/lib/common/chat-session-naming-service.d.ts.map +1 -1
- package/lib/common/chat-session-naming-service.js +11 -3
- package/lib/common/chat-session-naming-service.js.map +1 -1
- package/lib/common/chat-session-store.d.ts +43 -0
- package/lib/common/chat-session-store.d.ts.map +1 -0
- package/lib/common/chat-session-store.js +20 -0
- package/lib/common/chat-session-store.js.map +1 -0
- package/lib/common/chat-session-summary-agent-prompt.js +1 -1
- package/lib/common/chat-session-summary-agent-prompt.js.map +1 -1
- package/lib/common/chat-session-summary-agent.d.ts.map +1 -1
- package/lib/common/chat-session-summary-agent.js +2 -1
- package/lib/common/chat-session-summary-agent.js.map +1 -1
- package/lib/common/chat-tool-preferences.js +1 -1
- package/lib/common/chat-tool-preferences.js.map +1 -1
- package/lib/common/image-context-variable.d.ts.map +1 -1
- package/lib/common/image-context-variable.js +21 -6
- package/lib/common/image-context-variable.js.map +1 -1
- package/lib/common/index.d.ts +3 -0
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +3 -0
- package/lib/common/index.js.map +1 -1
- package/package.json +9 -9
- package/src/browser/agent-delegation-tool.ts +4 -2
- package/src/browser/ai-chat-frontend-module.ts +27 -0
- package/src/browser/change-set-file-element-deserializer.ts +62 -0
- package/src/browser/change-set-file-element.ts +29 -4
- package/src/browser/change-set-file-service.ts +3 -3
- package/src/browser/chat-session-store-impl.ts +326 -0
- package/src/browser/file-chat-variable-contribution.ts +2 -2
- package/src/browser/image-context-variable-contribution.ts +1 -1
- package/src/browser/task-context-service.ts +9 -3
- package/src/browser/task-context-variable.ts +8 -3
- package/src/common/change-set-element-deserializer.ts +90 -0
- package/src/common/change-set.ts +10 -2
- package/src/common/chat-agents-variable-contribution.ts +2 -2
- package/src/common/chat-agents.ts +4 -4
- package/src/common/chat-auto-save.spec.ts +372 -0
- package/src/common/chat-content-deserializer.spec.ts +375 -0
- package/src/common/chat-content-deserializer.ts +327 -0
- package/src/common/chat-model-serialization.spec.ts +343 -0
- package/src/common/chat-model-serialization.ts +133 -0
- package/src/common/chat-model.ts +644 -41
- package/src/common/chat-request-parser.spec.ts +61 -0
- package/src/common/chat-request-parser.ts +40 -1
- package/src/common/chat-service-deletion.spec.ts +269 -0
- package/src/common/chat-service.ts +227 -10
- package/src/common/chat-session-naming-service.ts +12 -4
- package/src/common/chat-session-store.ts +63 -0
- package/src/common/chat-session-summary-agent-prompt.ts +1 -1
- package/src/common/chat-session-summary-agent.ts +2 -1
- package/src/common/chat-tool-preferences.ts +1 -2
- package/src/common/image-context-variable.ts +21 -6
- package/src/common/index.ts +3 -0
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 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 {
|
|
19
|
+
ChatResponseContent,
|
|
20
|
+
CodeChatResponseContentImpl,
|
|
21
|
+
CommandChatResponseContentImpl,
|
|
22
|
+
ErrorChatResponseContentImpl,
|
|
23
|
+
HorizontalLayoutChatResponseContentImpl,
|
|
24
|
+
InformationalChatResponseContentImpl,
|
|
25
|
+
MarkdownChatResponseContentImpl,
|
|
26
|
+
ProgressChatResponseContentImpl,
|
|
27
|
+
QuestionResponseContentImpl,
|
|
28
|
+
TextChatResponseContentImpl,
|
|
29
|
+
ThinkingChatResponseContentImpl,
|
|
30
|
+
ToolCallChatResponseContentImpl,
|
|
31
|
+
UnknownChatResponseContentImpl,
|
|
32
|
+
TextContentData,
|
|
33
|
+
ThinkingContentData,
|
|
34
|
+
MarkdownContentData,
|
|
35
|
+
InformationalContentData,
|
|
36
|
+
CodeContentData,
|
|
37
|
+
ToolCallContentData,
|
|
38
|
+
CommandContentData,
|
|
39
|
+
HorizontalLayoutContentData,
|
|
40
|
+
ProgressContentData,
|
|
41
|
+
ErrorContentData,
|
|
42
|
+
QuestionContentData
|
|
43
|
+
} from './chat-model';
|
|
44
|
+
import { SerializableChatResponseContentData } from './chat-model-serialization';
|
|
45
|
+
import { ContributionProvider, ILogger, MaybePromise } from '@theia/core';
|
|
46
|
+
|
|
47
|
+
export const ChatContentDeserializer = Symbol('ChatContentDeserializer');
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* A deserializer for a specific kind of chat response content.
|
|
51
|
+
*
|
|
52
|
+
* Deserializers are responsible for reconstructing `ChatResponseContent` instances
|
|
53
|
+
* from their serialized data representations. Each deserializer handles a single
|
|
54
|
+
* content type identified by its `kind` property.
|
|
55
|
+
*
|
|
56
|
+
* @template T The type of the data object that this deserializer can process.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const textDeserializer: ChatContentDeserializer<TextContentData> = {
|
|
61
|
+
* kind: 'text',
|
|
62
|
+
* deserialize: (data) => new TextChatResponseContentImpl(data.content)
|
|
63
|
+
* };
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export interface ChatContentDeserializer<T = unknown> {
|
|
67
|
+
/**
|
|
68
|
+
* The unique identifier for the content type this deserializer handles.
|
|
69
|
+
* This must match the `kind` property of the serialized content data.
|
|
70
|
+
*/
|
|
71
|
+
readonly kind: string;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Deserializes the given data into a `ChatResponseContent` instance.
|
|
75
|
+
*
|
|
76
|
+
* @param data The serialized data to deserialize. The structure depends on the content kind.
|
|
77
|
+
* @returns The deserialized content, or a Promise that resolves to the deserialized content.
|
|
78
|
+
*/
|
|
79
|
+
deserialize(data: T): MaybePromise<ChatResponseContent>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Contribution point for registering chat content deserializers.
|
|
84
|
+
*
|
|
85
|
+
* Implement this interface to contribute custom deserializers for application-specific
|
|
86
|
+
* or extension-specific chat response content types. Multiple contributions can be
|
|
87
|
+
* registered, and all will be collected via the contribution provider pattern.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
* ```typescript
|
|
91
|
+
* @injectable()
|
|
92
|
+
* export class MyDeserializerContribution implements ChatContentDeserializerContribution {
|
|
93
|
+
* registerDeserializers(registry: ChatContentDeserializerRegistry): void {
|
|
94
|
+
* registry.register({
|
|
95
|
+
* kind: 'customContent',
|
|
96
|
+
* deserialize: (data: CustomContentData) =>
|
|
97
|
+
* new CustomContentImpl(data.title, data.items)
|
|
98
|
+
* });
|
|
99
|
+
* }
|
|
100
|
+
* }
|
|
101
|
+
*
|
|
102
|
+
* // In your module:
|
|
103
|
+
* bind(ChatContentDeserializerContribution).to(MyDeserializerContribution).inSingletonScope();
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* @see {@link ChatContentDeserializerRegistry} for the registry that collects deserializers
|
|
107
|
+
* @see {@link DefaultChatContentDeserializerContribution} for built-in content type deserializers
|
|
108
|
+
*/
|
|
109
|
+
export interface ChatContentDeserializerContribution {
|
|
110
|
+
/**
|
|
111
|
+
* Registers one or more deserializers with the provided registry.
|
|
112
|
+
*
|
|
113
|
+
* This method is called during the registry's initialization phase (at `@postConstruct()` time).
|
|
114
|
+
*
|
|
115
|
+
* @param registry The registry to register deserializers with
|
|
116
|
+
*/
|
|
117
|
+
registerDeserializers(registry: ChatContentDeserializerRegistry): void;
|
|
118
|
+
}
|
|
119
|
+
export const ChatContentDeserializerContribution = Symbol('ChatContentDeserializerContribution');
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Registry for chat content deserializers.
|
|
123
|
+
*
|
|
124
|
+
* This registry maintains a collection of deserializers for different content types
|
|
125
|
+
* and provides methods to register new deserializers and deserialize content data.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* // Usage in a service:
|
|
130
|
+
* @inject(ChatContentDeserializerRegistry)
|
|
131
|
+
* protected deserializerRegistry: ChatContentDeserializerRegistry;
|
|
132
|
+
*
|
|
133
|
+
* async restoreContent(): Promise<void> {
|
|
134
|
+
* const restoredContent = this.deserializerRegistry.deserialize(serializedData);
|
|
135
|
+
* }
|
|
136
|
+
* ```
|
|
137
|
+
*
|
|
138
|
+
* @see {@link ChatContentDeserializerContribution} for how to contribute deserializers
|
|
139
|
+
* @see {@link ChatContentDeserializerRegistryImpl} for the default implementation
|
|
140
|
+
*/
|
|
141
|
+
export interface ChatContentDeserializerRegistry {
|
|
142
|
+
/**
|
|
143
|
+
* Registers a deserializer for a specific content kind.
|
|
144
|
+
*
|
|
145
|
+
* If a deserializer for the same kind is already registered, it will be replaced.
|
|
146
|
+
*
|
|
147
|
+
* @param deserializer The deserializer to register
|
|
148
|
+
*/
|
|
149
|
+
register(deserializer: ChatContentDeserializer<unknown>): void;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Deserializes the given serialized content data into a `ChatResponseContent` instance.
|
|
153
|
+
*
|
|
154
|
+
* The registry looks up the appropriate deserializer based on the `kind` property
|
|
155
|
+
* of the serialized data and delegates to that deserializer's `deserialize` method.
|
|
156
|
+
*
|
|
157
|
+
* If no deserializer is found for the content kind, an `UnknownChatResponseContentImpl`
|
|
158
|
+
* instance is returned with the original data and fallback message preserved.
|
|
159
|
+
* A warning is also logged with the missing kind and available kinds.
|
|
160
|
+
*
|
|
161
|
+
* @param serialized The serialized content data to deserialize
|
|
162
|
+
* @returns The deserialized content, or a Promise that resolves to the deserialized content
|
|
163
|
+
*/
|
|
164
|
+
deserialize(serialized: SerializableChatResponseContentData): MaybePromise<ChatResponseContent>;
|
|
165
|
+
}
|
|
166
|
+
export const ChatContentDeserializerRegistry = Symbol('ChatContentDeserializerRegistry');
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Default implementation of the chat content deserializer registry.
|
|
170
|
+
*
|
|
171
|
+
* This registry collects deserializers from all bound `ChatContentDeserializerContribution`
|
|
172
|
+
* instances during its post-construction initialization phase. Deserializers are stored
|
|
173
|
+
* in a map keyed by their content kind.
|
|
174
|
+
*
|
|
175
|
+
* The registry handles unknown content types gracefully by returning an
|
|
176
|
+
* `UnknownChatResponseContentImpl` instance when no deserializer is found,
|
|
177
|
+
* ensuring that chat sessions can still be loaded even if some content types
|
|
178
|
+
* are no longer supported or available.
|
|
179
|
+
*
|
|
180
|
+
* @see {@link ChatContentDeserializerRegistry} for the interface definition
|
|
181
|
+
*/
|
|
182
|
+
@injectable()
|
|
183
|
+
export class ChatContentDeserializerRegistryImpl implements ChatContentDeserializerRegistry {
|
|
184
|
+
/**
|
|
185
|
+
* Map of registered deserializers, keyed by content kind.
|
|
186
|
+
*/
|
|
187
|
+
protected deserializers = new Map<string, ChatContentDeserializer>();
|
|
188
|
+
|
|
189
|
+
@inject(ContributionProvider) @named(ChatContentDeserializerContribution)
|
|
190
|
+
protected readonly deserializerContributions: ContributionProvider<ChatContentDeserializerContribution>;
|
|
191
|
+
|
|
192
|
+
@inject(ILogger) @named('ChatContentDeserializerRegistry')
|
|
193
|
+
protected readonly logger: ILogger;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Initializes the registry by collecting deserializers from all contributions.
|
|
197
|
+
* This method is automatically called after construction due to the `@postConstruct` decorator.
|
|
198
|
+
*/
|
|
199
|
+
@postConstruct()
|
|
200
|
+
protected initDeserializers(): void {
|
|
201
|
+
for (const contribution of this.deserializerContributions.getContributions()) {
|
|
202
|
+
contribution.registerDeserializers(this);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
register(deserializer: ChatContentDeserializer): void {
|
|
207
|
+
this.deserializers.set(deserializer.kind, deserializer);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
deserialize(serialized: SerializableChatResponseContentData): MaybePromise<ChatResponseContent> {
|
|
211
|
+
const deserializer = this.deserializers.get(serialized.kind);
|
|
212
|
+
if (!deserializer) {
|
|
213
|
+
this.logger.warn('No deserializer found for kind:', serialized.kind, 'Available kinds:', Array.from(this.deserializers.keys()));
|
|
214
|
+
return new UnknownChatResponseContentImpl(
|
|
215
|
+
serialized.kind,
|
|
216
|
+
serialized.fallbackMessage,
|
|
217
|
+
serialized.data
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
return deserializer.deserialize(serialized.data);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Default implementation of the chat content deserializer contribution.
|
|
226
|
+
*
|
|
227
|
+
* This contribution registers deserializers for all built-in content types supported
|
|
228
|
+
* by Theia AI.
|
|
229
|
+
*
|
|
230
|
+
* Note that some content types have limitations when deserialized from persistence.
|
|
231
|
+
*
|
|
232
|
+
* @see {@link ChatContentDeserializerContribution} for the contribution interface
|
|
233
|
+
*/
|
|
234
|
+
@injectable()
|
|
235
|
+
export class DefaultChatContentDeserializerContribution implements ChatContentDeserializerContribution {
|
|
236
|
+
registerDeserializers(registry: ChatContentDeserializerRegistry): void {
|
|
237
|
+
registry.register({
|
|
238
|
+
kind: 'text',
|
|
239
|
+
deserialize: (data: TextContentData) => new TextChatResponseContentImpl(data.content)
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
registry.register({
|
|
243
|
+
kind: 'thinking',
|
|
244
|
+
deserialize: (data: ThinkingContentData) => new ThinkingChatResponseContentImpl(
|
|
245
|
+
data.content,
|
|
246
|
+
data.signature
|
|
247
|
+
)
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
registry.register({
|
|
251
|
+
kind: 'markdownContent',
|
|
252
|
+
deserialize: (data: MarkdownContentData) => new MarkdownChatResponseContentImpl(data.content)
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
registry.register({
|
|
256
|
+
kind: 'informational',
|
|
257
|
+
deserialize: (data: InformationalContentData) => new InformationalChatResponseContentImpl(data.content)
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
registry.register({
|
|
261
|
+
kind: 'code',
|
|
262
|
+
deserialize: (data: CodeContentData) => new CodeChatResponseContentImpl(
|
|
263
|
+
data.code,
|
|
264
|
+
data.language,
|
|
265
|
+
data.location
|
|
266
|
+
)
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
registry.register({
|
|
270
|
+
kind: 'toolCall',
|
|
271
|
+
deserialize: (data: ToolCallContentData) => new ToolCallChatResponseContentImpl(
|
|
272
|
+
data.id,
|
|
273
|
+
data.name,
|
|
274
|
+
data.arguments,
|
|
275
|
+
data.finished,
|
|
276
|
+
data.result
|
|
277
|
+
)
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
registry.register({
|
|
281
|
+
kind: 'command',
|
|
282
|
+
deserialize: (data: CommandContentData) => {
|
|
283
|
+
const command = data.commandId ? { id: data.commandId } : undefined;
|
|
284
|
+
// Cannot restore customCallback since it contains a function
|
|
285
|
+
return new CommandChatResponseContentImpl(command, undefined, data.arguments);
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
registry.register({
|
|
290
|
+
kind: 'horizontal',
|
|
291
|
+
deserialize: async (data: HorizontalLayoutContentData) => {
|
|
292
|
+
const childContentPromises = data.content.map(child => registry.deserialize(child));
|
|
293
|
+
const childContent = Promise.all(childContentPromises);
|
|
294
|
+
return new HorizontalLayoutChatResponseContentImpl(await childContent);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
registry.register({
|
|
299
|
+
kind: 'progress',
|
|
300
|
+
deserialize: (data: ProgressContentData) => new ProgressChatResponseContentImpl(data.message)
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
registry.register({
|
|
304
|
+
kind: 'error',
|
|
305
|
+
deserialize: (data: ErrorContentData) => {
|
|
306
|
+
const error = new Error(data.message);
|
|
307
|
+
if (data.stack) {
|
|
308
|
+
error.stack = data.stack;
|
|
309
|
+
}
|
|
310
|
+
return new ErrorChatResponseContentImpl(error);
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
registry.register({
|
|
315
|
+
kind: 'question',
|
|
316
|
+
deserialize: (data: QuestionContentData) =>
|
|
317
|
+
// Restore in read-only mode (no handler/request)
|
|
318
|
+
new QuestionResponseContentImpl(
|
|
319
|
+
data.question,
|
|
320
|
+
data.options,
|
|
321
|
+
undefined,
|
|
322
|
+
undefined,
|
|
323
|
+
data.selectedOption
|
|
324
|
+
)
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2025 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 { expect } from 'chai';
|
|
18
|
+
import { ChatAgentLocation } from './chat-agents';
|
|
19
|
+
import { MutableChatModel } from './chat-model';
|
|
20
|
+
import { ParsedChatRequest } from './parsed-chat-request';
|
|
21
|
+
|
|
22
|
+
describe('ChatModel Serialization and Restoration', () => {
|
|
23
|
+
|
|
24
|
+
function createParsedRequest(text: string): ParsedChatRequest {
|
|
25
|
+
return {
|
|
26
|
+
request: { text },
|
|
27
|
+
parts: [{
|
|
28
|
+
kind: 'text',
|
|
29
|
+
text,
|
|
30
|
+
promptText: text,
|
|
31
|
+
range: { start: 0, endExclusive: text.length }
|
|
32
|
+
}],
|
|
33
|
+
toolRequests: new Map(),
|
|
34
|
+
variables: []
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('Simple tree serialization', () => {
|
|
39
|
+
it('should serialize a chat with a single request', () => {
|
|
40
|
+
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
|
41
|
+
model.addRequest(createParsedRequest('Hello'));
|
|
42
|
+
|
|
43
|
+
const serialized = model.toSerializable();
|
|
44
|
+
|
|
45
|
+
expect(serialized.hierarchy).to.be.an('object');
|
|
46
|
+
expect(serialized.hierarchy!.rootBranchId).to.be.a('string');
|
|
47
|
+
expect(serialized.hierarchy!.branches).to.be.an('object');
|
|
48
|
+
expect(serialized.requests).to.have.lengthOf(1);
|
|
49
|
+
expect(serialized.requests[0].text).to.equal('Hello');
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should serialize a chat with multiple sequential requests', () => {
|
|
53
|
+
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
|
54
|
+
model.addRequest(createParsedRequest('First'));
|
|
55
|
+
model.addRequest(createParsedRequest('Second'));
|
|
56
|
+
model.addRequest(createParsedRequest('Third'));
|
|
57
|
+
|
|
58
|
+
const serialized = model.toSerializable();
|
|
59
|
+
|
|
60
|
+
expect(serialized.hierarchy).to.be.an('object');
|
|
61
|
+
expect(serialized.requests).to.have.lengthOf(3);
|
|
62
|
+
|
|
63
|
+
// Verify the hierarchy has 3 branches (one for each request)
|
|
64
|
+
const branches = Object.values(serialized.hierarchy!.branches);
|
|
65
|
+
expect(branches).to.have.lengthOf(3);
|
|
66
|
+
|
|
67
|
+
// Verify the active path through the tree
|
|
68
|
+
const rootBranch = serialized.hierarchy!.branches[serialized.hierarchy!.rootBranchId];
|
|
69
|
+
expect(rootBranch.items).to.have.lengthOf(1);
|
|
70
|
+
expect(rootBranch.items[0].requestId).to.equal(serialized.requests[0].id);
|
|
71
|
+
expect(rootBranch.items[0].nextBranchId).to.be.a('string');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('Tree serialization with alternatives (edited messages)', () => {
|
|
76
|
+
it('should serialize a chat with edited messages', () => {
|
|
77
|
+
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
|
78
|
+
|
|
79
|
+
// Add first request
|
|
80
|
+
const req1 = model.addRequest(createParsedRequest('Original message'));
|
|
81
|
+
req1.response.complete();
|
|
82
|
+
|
|
83
|
+
// Add second request
|
|
84
|
+
model.addRequest(createParsedRequest('Follow-up'));
|
|
85
|
+
|
|
86
|
+
// Edit the first request (creating an alternative)
|
|
87
|
+
const branch1 = model.getBranch(req1.id);
|
|
88
|
+
expect(branch1).to.not.be.undefined;
|
|
89
|
+
branch1!.add(model.addRequest(createParsedRequest('Edited message'), 'agent-1'));
|
|
90
|
+
|
|
91
|
+
const serialized = model.toSerializable();
|
|
92
|
+
|
|
93
|
+
// Should have 3 requests: original, edited, and follow-up
|
|
94
|
+
expect(serialized.requests).to.have.lengthOf(3);
|
|
95
|
+
|
|
96
|
+
// The root branch should have 2 items (original and edited alternatives)
|
|
97
|
+
const rootBranch = serialized.hierarchy!.branches[serialized.hierarchy!.rootBranchId];
|
|
98
|
+
expect(rootBranch.items).to.have.lengthOf(2);
|
|
99
|
+
expect(rootBranch.items[0].requestId).to.equal(serialized.requests[0].id);
|
|
100
|
+
expect(rootBranch.items[1].requestId).to.equal(serialized.requests[2].id);
|
|
101
|
+
|
|
102
|
+
// The active branch index should point to the most recent alternative
|
|
103
|
+
expect(rootBranch.activeBranchIndex).to.be.at.least(0);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should serialize nested alternatives (edited multiple times)', () => {
|
|
107
|
+
const model = new MutableChatModel(ChatAgentLocation.Panel);
|
|
108
|
+
|
|
109
|
+
// Add first request
|
|
110
|
+
const req1 = model.addRequest(createParsedRequest('First'));
|
|
111
|
+
req1.response.complete();
|
|
112
|
+
|
|
113
|
+
// Add second request
|
|
114
|
+
const req2 = model.addRequest(createParsedRequest('Second'));
|
|
115
|
+
req2.response.complete();
|
|
116
|
+
|
|
117
|
+
// Edit the second request (creating an alternative)
|
|
118
|
+
const branch2 = model.getBranch(req2.id);
|
|
119
|
+
expect(branch2).to.not.be.undefined;
|
|
120
|
+
const req2edited = model.addRequest(createParsedRequest('Second (edited)'), 'agent-1');
|
|
121
|
+
branch2!.add(req2edited);
|
|
122
|
+
|
|
123
|
+
// Add third request after the edited version
|
|
124
|
+
model.addRequest(createParsedRequest('Third'));
|
|
125
|
+
|
|
126
|
+
const serialized = model.toSerializable();
|
|
127
|
+
|
|
128
|
+
// Should have 4 requests total
|
|
129
|
+
expect(serialized.requests).to.have.lengthOf(4);
|
|
130
|
+
|
|
131
|
+
// Find the second-level branch
|
|
132
|
+
const rootBranch = serialized.hierarchy!.branches[serialized.hierarchy!.rootBranchId];
|
|
133
|
+
const nextBranchId = rootBranch.items[rootBranch.activeBranchIndex].nextBranchId;
|
|
134
|
+
expect(nextBranchId).to.be.a('string');
|
|
135
|
+
|
|
136
|
+
const secondBranch = serialized.hierarchy!.branches[nextBranchId!];
|
|
137
|
+
expect(secondBranch.items).to.have.lengthOf(2); // Original and edited
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('Tree restoration from serialized data', () => {
|
|
142
|
+
it('should restore a simple chat session', () => {
|
|
143
|
+
// Create and serialize
|
|
144
|
+
const model1 = new MutableChatModel(ChatAgentLocation.Panel);
|
|
145
|
+
model1.addRequest(createParsedRequest('Hello'));
|
|
146
|
+
const serialized = model1.toSerializable();
|
|
147
|
+
|
|
148
|
+
// Restore
|
|
149
|
+
const model2 = new MutableChatModel(serialized);
|
|
150
|
+
|
|
151
|
+
expect(model2.getRequests()).to.have.lengthOf(1);
|
|
152
|
+
expect(model2.getRequests()[0].request.text).to.equal('Hello');
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('should restore chat with multiple sequential requests', () => {
|
|
156
|
+
// Create and serialize
|
|
157
|
+
const model1 = new MutableChatModel(ChatAgentLocation.Panel);
|
|
158
|
+
model1.addRequest(createParsedRequest('First'));
|
|
159
|
+
model1.addRequest(createParsedRequest('Second'));
|
|
160
|
+
model1.addRequest(createParsedRequest('Third'));
|
|
161
|
+
const serialized = model1.toSerializable();
|
|
162
|
+
|
|
163
|
+
// Restore
|
|
164
|
+
const model2 = new MutableChatModel(serialized);
|
|
165
|
+
|
|
166
|
+
const requests = model2.getRequests();
|
|
167
|
+
expect(requests).to.have.lengthOf(3);
|
|
168
|
+
expect(requests[0].request.text).to.equal('First');
|
|
169
|
+
expect(requests[1].request.text).to.equal('Second');
|
|
170
|
+
expect(requests[2].request.text).to.equal('Third');
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should restore chat with edited messages (alternatives)', () => {
|
|
174
|
+
// Create and serialize
|
|
175
|
+
const model1 = new MutableChatModel(ChatAgentLocation.Panel);
|
|
176
|
+
const req1 = model1.addRequest(createParsedRequest('Original'));
|
|
177
|
+
req1.response.complete();
|
|
178
|
+
|
|
179
|
+
const branch1 = model1.getBranch(req1.id);
|
|
180
|
+
const req1edited = model1.addRequest(createParsedRequest('Edited'), 'agent-1');
|
|
181
|
+
branch1!.add(req1edited);
|
|
182
|
+
|
|
183
|
+
const serialized = model1.toSerializable();
|
|
184
|
+
|
|
185
|
+
// Verify serialization includes both alternatives
|
|
186
|
+
expect(serialized.requests).to.have.lengthOf(2);
|
|
187
|
+
|
|
188
|
+
// Restore
|
|
189
|
+
const model2 = new MutableChatModel(serialized);
|
|
190
|
+
|
|
191
|
+
// Check that both alternatives are restored
|
|
192
|
+
const restoredBranch = model2.getBranch(serialized.requests[0].id);
|
|
193
|
+
expect(restoredBranch).to.not.be.undefined;
|
|
194
|
+
expect(restoredBranch!.items).to.have.lengthOf(2);
|
|
195
|
+
expect(restoredBranch!.items[0].element.request.text).to.equal('Original');
|
|
196
|
+
expect(restoredBranch!.items[1].element.request.text).to.equal('Edited');
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('should restore the correct active branch indices', () => {
|
|
200
|
+
// Create and serialize
|
|
201
|
+
const model1 = new MutableChatModel(ChatAgentLocation.Panel);
|
|
202
|
+
const req1 = model1.addRequest(createParsedRequest('Original'));
|
|
203
|
+
req1.response.complete();
|
|
204
|
+
|
|
205
|
+
const branch1 = model1.getBranch(req1.id);
|
|
206
|
+
const req1edited = model1.addRequest(createParsedRequest('Edited'), 'agent-1');
|
|
207
|
+
branch1!.add(req1edited);
|
|
208
|
+
|
|
209
|
+
// Switch to the edited version
|
|
210
|
+
branch1!.enable(req1edited);
|
|
211
|
+
|
|
212
|
+
const activeBranchIndex1 = branch1!.activeBranchIndex;
|
|
213
|
+
const serialized = model1.toSerializable();
|
|
214
|
+
|
|
215
|
+
// Restore
|
|
216
|
+
const model2 = new MutableChatModel(serialized);
|
|
217
|
+
|
|
218
|
+
const restoredBranch = model2.getBranch(serialized.requests[0].id);
|
|
219
|
+
expect(restoredBranch).to.not.be.undefined;
|
|
220
|
+
expect(restoredBranch!.activeBranchIndex).to.equal(activeBranchIndex1);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('should restore a simple session with hierarchy', () => {
|
|
224
|
+
// Create serialized data with hierarchy
|
|
225
|
+
const serializedData = {
|
|
226
|
+
sessionId: 'simple-session',
|
|
227
|
+
location: ChatAgentLocation.Panel,
|
|
228
|
+
hierarchy: {
|
|
229
|
+
rootBranchId: 'branch-root',
|
|
230
|
+
branches: {
|
|
231
|
+
'branch-root': {
|
|
232
|
+
id: 'branch-root',
|
|
233
|
+
items: [{ requestId: 'request-1' }],
|
|
234
|
+
activeBranchIndex: 0
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
requests: [
|
|
239
|
+
{
|
|
240
|
+
id: 'request-1',
|
|
241
|
+
text: 'Hello'
|
|
242
|
+
}
|
|
243
|
+
],
|
|
244
|
+
responses: [
|
|
245
|
+
{
|
|
246
|
+
id: 'response-1',
|
|
247
|
+
requestId: 'request-1',
|
|
248
|
+
isComplete: true,
|
|
249
|
+
isError: false,
|
|
250
|
+
content: []
|
|
251
|
+
}
|
|
252
|
+
]
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
// Should restore without errors
|
|
256
|
+
const model = new MutableChatModel(serializedData);
|
|
257
|
+
expect(model.getRequests()).to.have.lengthOf(1);
|
|
258
|
+
expect(model.getRequests()[0].request.text).to.equal('Hello');
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
describe('Complete round-trip with complex tree', () => {
|
|
263
|
+
it('should serialize and restore a complex tree structure', () => {
|
|
264
|
+
// Create a complex chat with multiple edits
|
|
265
|
+
const model1 = new MutableChatModel(ChatAgentLocation.Panel);
|
|
266
|
+
|
|
267
|
+
// Level 1
|
|
268
|
+
const req1 = model1.addRequest(createParsedRequest('Level 1 - Original'));
|
|
269
|
+
req1.response.complete();
|
|
270
|
+
|
|
271
|
+
// Level 2
|
|
272
|
+
const req2 = model1.addRequest(createParsedRequest('Level 2 - Original'));
|
|
273
|
+
req2.response.complete();
|
|
274
|
+
|
|
275
|
+
// Edit Level 1
|
|
276
|
+
const branch1 = model1.getBranch(req1.id);
|
|
277
|
+
const req1edited = model1.addRequest(createParsedRequest('Level 1 - Edited'), 'agent-1');
|
|
278
|
+
branch1!.add(req1edited);
|
|
279
|
+
|
|
280
|
+
// Add Level 2 alternative after edited Level 1
|
|
281
|
+
const req2alt = model1.addRequest(createParsedRequest('Level 2 - Alternative'));
|
|
282
|
+
req2alt.response.complete();
|
|
283
|
+
|
|
284
|
+
// Edit Level 2 alternative
|
|
285
|
+
const branch2alt = model1.getBranch(req2alt.id);
|
|
286
|
+
const req2altEdited = model1.addRequest(createParsedRequest('Level 2 - Alternative Edited'), 'agent-1');
|
|
287
|
+
branch2alt!.add(req2altEdited);
|
|
288
|
+
|
|
289
|
+
const serialized = model1.toSerializable();
|
|
290
|
+
|
|
291
|
+
// Verify serialization
|
|
292
|
+
expect(serialized.requests).to.have.lengthOf(5);
|
|
293
|
+
expect(serialized.hierarchy).to.be.an('object');
|
|
294
|
+
|
|
295
|
+
// Restore
|
|
296
|
+
const model2 = new MutableChatModel(serialized);
|
|
297
|
+
|
|
298
|
+
// Verify all requests are present
|
|
299
|
+
const allRequests = model2.getAllRequests();
|
|
300
|
+
expect(allRequests).to.have.lengthOf(5);
|
|
301
|
+
|
|
302
|
+
// Verify branch structure
|
|
303
|
+
const restoredBranch1 = model2.getBranches()[0];
|
|
304
|
+
expect(restoredBranch1.items).to.have.lengthOf(2); // Original + Edited
|
|
305
|
+
|
|
306
|
+
// Verify we can navigate the alternatives
|
|
307
|
+
expect(restoredBranch1.items[0].element.request.text).to.equal('Level 1 - Original');
|
|
308
|
+
expect(restoredBranch1.items[1].element.request.text).to.equal('Level 1 - Edited');
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should preserve all requests across multiple serialization/restoration cycles', () => {
|
|
312
|
+
// Create initial model
|
|
313
|
+
let model = new MutableChatModel(ChatAgentLocation.Panel);
|
|
314
|
+
const req1 = model.addRequest(createParsedRequest('Request 1'));
|
|
315
|
+
req1.response.complete();
|
|
316
|
+
|
|
317
|
+
// Cycle 1
|
|
318
|
+
let serialized = model.toSerializable();
|
|
319
|
+
model = new MutableChatModel(serialized);
|
|
320
|
+
|
|
321
|
+
// Add more requests
|
|
322
|
+
model.addRequest(createParsedRequest('Request 2'));
|
|
323
|
+
|
|
324
|
+
// Cycle 2
|
|
325
|
+
serialized = model.toSerializable();
|
|
326
|
+
model = new MutableChatModel(serialized);
|
|
327
|
+
|
|
328
|
+
// Add an edit
|
|
329
|
+
const branch = model.getBranches()[0];
|
|
330
|
+
const reqEdited = model.addRequest(createParsedRequest('Request 1 - Edited'), 'agent-1');
|
|
331
|
+
branch.add(reqEdited);
|
|
332
|
+
|
|
333
|
+
// Final cycle
|
|
334
|
+
serialized = model.toSerializable();
|
|
335
|
+
const finalModel = new MutableChatModel(serialized);
|
|
336
|
+
|
|
337
|
+
// Verify all requests are preserved
|
|
338
|
+
expect(finalModel.getBranches()[0].items).to.have.lengthOf(2);
|
|
339
|
+
const allRequests = finalModel.getAllRequests();
|
|
340
|
+
expect(allRequests).to.have.lengthOf(3);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|