@theia/ai-external-api 1.76.0-next.33
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/README.md +83 -0
- package/lib/browser/ai-external-api-frontend-contribution.d.ts +28 -0
- package/lib/browser/ai-external-api-frontend-contribution.d.ts.map +1 -0
- package/lib/browser/ai-external-api-frontend-contribution.js +88 -0
- package/lib/browser/ai-external-api-frontend-contribution.js.map +1 -0
- package/lib/browser/ai-external-api-frontend-contribution.spec.d.ts +2 -0
- package/lib/browser/ai-external-api-frontend-contribution.spec.d.ts.map +1 -0
- package/lib/browser/ai-external-api-frontend-contribution.spec.js +102 -0
- package/lib/browser/ai-external-api-frontend-contribution.spec.js.map +1 -0
- package/lib/browser/ai-external-api-frontend-module.d.ts +4 -0
- package/lib/browser/ai-external-api-frontend-module.d.ts.map +1 -0
- package/lib/browser/ai-external-api-frontend-module.js +47 -0
- package/lib/browser/ai-external-api-frontend-module.js.map +1 -0
- package/lib/browser/frontend-external-chat-session-provider.d.ts +63 -0
- package/lib/browser/frontend-external-chat-session-provider.d.ts.map +1 -0
- package/lib/browser/frontend-external-chat-session-provider.js +271 -0
- package/lib/browser/frontend-external-chat-session-provider.js.map +1 -0
- package/lib/browser/frontend-external-chat-session-provider.spec.d.ts +2 -0
- package/lib/browser/frontend-external-chat-session-provider.spec.d.ts.map +1 -0
- package/lib/browser/frontend-external-chat-session-provider.spec.js +421 -0
- package/lib/browser/frontend-external-chat-session-provider.spec.js.map +1 -0
- package/lib/browser/index.d.ts +3 -0
- package/lib/browser/index.d.ts.map +1 -0
- package/lib/browser/index.js +21 -0
- package/lib/browser/index.js.map +1 -0
- package/lib/common/external-chat-session-provider.d.ts +213 -0
- package/lib/common/external-chat-session-provider.d.ts.map +1 -0
- package/lib/common/external-chat-session-provider.js +147 -0
- package/lib/common/external-chat-session-provider.js.map +1 -0
- package/lib/common/index.d.ts +2 -0
- package/lib/common/index.d.ts.map +1 -0
- package/lib/common/index.js +20 -0
- package/lib/common/index.js.map +1 -0
- package/lib/node/ai-external-api-backend-module.d.ts +4 -0
- package/lib/node/ai-external-api-backend-module.d.ts.map +1 -0
- package/lib/node/ai-external-api-backend-module.js +38 -0
- package/lib/node/ai-external-api-backend-module.js.map +1 -0
- package/lib/node/ai-external-api-endpoint.d.ts +31 -0
- package/lib/node/ai-external-api-endpoint.d.ts.map +1 -0
- package/lib/node/ai-external-api-endpoint.js +189 -0
- package/lib/node/ai-external-api-endpoint.js.map +1 -0
- package/lib/node/ai-external-api-endpoint.spec.d.ts +2 -0
- package/lib/node/ai-external-api-endpoint.spec.d.ts.map +1 -0
- package/lib/node/ai-external-api-endpoint.spec.js +233 -0
- package/lib/node/ai-external-api-endpoint.spec.js.map +1 -0
- package/lib/node/external-chat-session-registry.d.ts +83 -0
- package/lib/node/external-chat-session-registry.d.ts.map +1 -0
- package/lib/node/external-chat-session-registry.js +266 -0
- package/lib/node/external-chat-session-registry.js.map +1 -0
- package/lib/node/external-chat-session-registry.spec.d.ts +2 -0
- package/lib/node/external-chat-session-registry.spec.d.ts.map +1 -0
- package/lib/node/external-chat-session-registry.spec.js +366 -0
- package/lib/node/external-chat-session-registry.spec.js.map +1 -0
- package/lib/node/index.d.ts +3 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +21 -0
- package/lib/node/index.js.map +1 -0
- package/package.json +52 -0
- package/src/browser/ai-external-api-frontend-contribution.spec.ts +128 -0
- package/src/browser/ai-external-api-frontend-contribution.ts +90 -0
- package/src/browser/ai-external-api-frontend-module.ts +48 -0
- package/src/browser/frontend-external-chat-session-provider.spec.ts +491 -0
- package/src/browser/frontend-external-chat-session-provider.ts +277 -0
- package/src/browser/index.ts +18 -0
- package/src/common/external-chat-session-provider.ts +330 -0
- package/src/common/index.ts +17 -0
- package/src/node/ai-external-api-backend-module.ts +41 -0
- package/src/node/ai-external-api-endpoint.spec.ts +272 -0
- package/src/node/ai-external-api-endpoint.ts +195 -0
- package/src/node/external-chat-session-registry.spec.ts +413 -0
- package/src/node/external-chat-session-registry.ts +270 -0
- package/src/node/index.ts +18 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.ExternalChatSessionRegistry = void 0;
|
|
19
|
+
const tslib_1 = require("tslib");
|
|
20
|
+
const chat_model_1 = require("@theia/ai-chat/lib/common/chat-model");
|
|
21
|
+
const core_1 = require("@theia/core");
|
|
22
|
+
const logger_1 = require("@theia/core/lib/common/logger");
|
|
23
|
+
const inversify_1 = require("@theia/core/shared/inversify");
|
|
24
|
+
/**
|
|
25
|
+
* Aggregates the {@link ExternalChatSessionProvider}s of all connected frontends.
|
|
26
|
+
*
|
|
27
|
+
* Providers register when a frontend connects and are removed when its connection closes.
|
|
28
|
+
* Queries fan out to all providers; a failing or unresponsive provider is logged and skipped
|
|
29
|
+
* so that a broken, busy, or disconnecting frontend does not break the external API. Sessions
|
|
30
|
+
* reported by more than one provider (e.g. a session restored in several frontends) are
|
|
31
|
+
* deduplicated, preferring restored over persisted-only, in-progress over finished, and more
|
|
32
|
+
* recent over older reports. Session actions (open, restore) are routed to the preferred
|
|
33
|
+
* provider in the same order.
|
|
34
|
+
*/
|
|
35
|
+
let ExternalChatSessionRegistry = class ExternalChatSessionRegistry {
|
|
36
|
+
constructor() {
|
|
37
|
+
/**
|
|
38
|
+
* Timeout in milliseconds after which an unresponsive provider is skipped in the query
|
|
39
|
+
* fan-outs. Only the read-only queries are bounded: an action call may legitimately take
|
|
40
|
+
* longer, and abandoning it could send it twice.
|
|
41
|
+
*/
|
|
42
|
+
this.queryTimeout = 10_000;
|
|
43
|
+
this.providers = new Set();
|
|
44
|
+
this.onDidChangeSessionsEmitter = new core_1.Emitter();
|
|
45
|
+
/** Fired when the set of connected frontends changes or a frontend reports a session change. */
|
|
46
|
+
this.onDidChangeSessions = this.onDidChangeSessionsEmitter.event;
|
|
47
|
+
}
|
|
48
|
+
addProvider(provider) {
|
|
49
|
+
this.providers.add(provider);
|
|
50
|
+
this.notifySessionsChanged();
|
|
51
|
+
}
|
|
52
|
+
removeProvider(provider) {
|
|
53
|
+
if (this.providers.delete(provider)) {
|
|
54
|
+
this.notifySessionsChanged();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
/** Announces that the sessions visible through this registry changed, see {@link onDidChangeSessions}. */
|
|
58
|
+
notifySessionsChanged() {
|
|
59
|
+
this.onDidChangeSessionsEmitter.fire();
|
|
60
|
+
}
|
|
61
|
+
async getSessions() {
|
|
62
|
+
const results = await this.queryProviders(provider => provider.getSessions());
|
|
63
|
+
const merged = new Map();
|
|
64
|
+
for (const summary of results.flat()) {
|
|
65
|
+
const existing = merged.get(summary.id);
|
|
66
|
+
if (!existing || this.prefer(summary, existing)) {
|
|
67
|
+
merged.set(summary.id, summary);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return Array.from(merged.values())
|
|
71
|
+
.sort((a, b) => (b.lastInteraction ?? 0) - (a.lastInteraction ?? 0));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Returns the detail of the session from the preferred frontend that knows it, or
|
|
75
|
+
* `undefined` if no connected frontend knows it. The frontends are ranked by their session
|
|
76
|
+
* summaries, so that only the preferred frontend has to report the conversation.
|
|
77
|
+
*/
|
|
78
|
+
async getSession(sessionId) {
|
|
79
|
+
for (const provider of await this.getProvidersFor(sessionId)) {
|
|
80
|
+
try {
|
|
81
|
+
const session = await this.withQueryTimeout(provider.getSession(sessionId), 'get a session');
|
|
82
|
+
if (session) {
|
|
83
|
+
return session;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
this.logger.warn('Failed to read a session from a frontend, trying the next one.', error);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Opens the session in the preferred frontend that knows it, restoring it if necessary.
|
|
94
|
+
* Returns `false` if no connected frontend knows the session.
|
|
95
|
+
*/
|
|
96
|
+
async openSession(sessionId) {
|
|
97
|
+
for (const provider of await this.getProvidersFor(sessionId)) {
|
|
98
|
+
try {
|
|
99
|
+
if (await provider.openSession(sessionId)) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
this.logger.warn('Failed to open a session in a frontend, trying the next one.', error);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Restores the session in the preferred frontend that knows it and returns its detail.
|
|
111
|
+
* Returns `undefined` if no connected frontend knows the session.
|
|
112
|
+
*/
|
|
113
|
+
async restoreSession(sessionId) {
|
|
114
|
+
for (const provider of await this.getProvidersFor(sessionId)) {
|
|
115
|
+
try {
|
|
116
|
+
const session = await provider.restoreSession(sessionId);
|
|
117
|
+
if (session) {
|
|
118
|
+
return session;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
this.logger.warn('Failed to restore a session in a frontend, trying the next one.', error);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Sends the prompt to the given session in the preferred frontend that knows it.
|
|
129
|
+
* The first definitive answer wins: in particular, a `busy` rejection by the preferred
|
|
130
|
+
* frontend is not retried against less preferred copies of the session.
|
|
131
|
+
* Returns `undefined` if no connected frontend knows the session.
|
|
132
|
+
*/
|
|
133
|
+
async sendPrompt(sessionId, prompt) {
|
|
134
|
+
for (const provider of await this.getProvidersFor(sessionId)) {
|
|
135
|
+
try {
|
|
136
|
+
const result = await provider.sendPrompt(sessionId, prompt);
|
|
137
|
+
if (result) {
|
|
138
|
+
return result;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
this.logger.warn('Failed to send a prompt to a frontend, trying the next one.', error);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Creates a session in a frontend matching the requested workspace. Without a requested
|
|
149
|
+
* workspace, the session is created in the only connected frontend, and if the connected
|
|
150
|
+
* frontends have different workspaces open, the request is rejected as ambiguous.
|
|
151
|
+
* Rejected with `noFrontend` when no frontend is connected or none responds.
|
|
152
|
+
*/
|
|
153
|
+
async createSession(request) {
|
|
154
|
+
if (this.providers.size === 0) {
|
|
155
|
+
return { failure: 'noFrontend' };
|
|
156
|
+
}
|
|
157
|
+
const candidates = await this.getProvidersForWorkspace(request.workspace);
|
|
158
|
+
if (candidates.length === 0) {
|
|
159
|
+
return { failure: request.workspace ? 'workspaceNotFound' : 'noFrontend' };
|
|
160
|
+
}
|
|
161
|
+
if (!request.workspace && new Set(candidates.map(candidate => candidate.workspace)).size > 1) {
|
|
162
|
+
return { failure: 'ambiguousWorkspace' };
|
|
163
|
+
}
|
|
164
|
+
for (const candidate of candidates) {
|
|
165
|
+
try {
|
|
166
|
+
return await candidate.provider.createSession(request);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
this.logger.warn('Failed to create a session in a frontend, trying the next one.', error);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw new Error('All matching frontends failed to create the session.');
|
|
173
|
+
}
|
|
174
|
+
/** Returns the providers whose workspace matches the given one, or all providers if none is given. */
|
|
175
|
+
async getProvidersForWorkspace(workspace) {
|
|
176
|
+
const requested = workspace === undefined ? undefined : this.normalizeWorkspace(workspace);
|
|
177
|
+
const candidates = [];
|
|
178
|
+
await Promise.all(Array.from(this.providers.values(), async (provider) => {
|
|
179
|
+
try {
|
|
180
|
+
const providerWorkspace = await this.withQueryTimeout(provider.getWorkspace(), 'query a workspace');
|
|
181
|
+
const normalized = providerWorkspace === undefined ? undefined : this.normalizeWorkspace(providerWorkspace);
|
|
182
|
+
if (requested === undefined || requested === normalized) {
|
|
183
|
+
candidates.push({ provider, workspace: normalized });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
this.logger.warn('Failed to query a frontend session provider, skipping it.', error);
|
|
188
|
+
}
|
|
189
|
+
}));
|
|
190
|
+
return candidates;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Normalizes a workspace URI for comparison: URIs that denote the same workspace but differ
|
|
194
|
+
* in their path encoding or in a trailing separator normalize to the same string.
|
|
195
|
+
*/
|
|
196
|
+
normalizeWorkspace(workspace) {
|
|
197
|
+
const uri = new core_1.URI(workspace).normalizePath();
|
|
198
|
+
const path = uri.path.toString();
|
|
199
|
+
return path.length > 1 && path.endsWith('/') ? uri.withPath(path.slice(0, -1)).toString() : uri.toString();
|
|
200
|
+
}
|
|
201
|
+
/** Returns the providers that know the given session, most preferred report first. */
|
|
202
|
+
async getProvidersFor(sessionId) {
|
|
203
|
+
const candidates = [];
|
|
204
|
+
await Promise.all(Array.from(this.providers.values(), async (provider) => {
|
|
205
|
+
try {
|
|
206
|
+
const session = await this.withQueryTimeout(provider.getSessionSummary(sessionId), 'query a session summary');
|
|
207
|
+
if (session) {
|
|
208
|
+
candidates.push({ provider, session });
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
this.logger.warn('Failed to query a frontend session provider, skipping it.', error);
|
|
213
|
+
}
|
|
214
|
+
}));
|
|
215
|
+
return candidates
|
|
216
|
+
.sort((a, b) => this.prefer(a.session, b.session) ? -1 : this.prefer(b.session, a.session) ? 1 : 0)
|
|
217
|
+
.map(candidate => candidate.provider);
|
|
218
|
+
}
|
|
219
|
+
async queryProviders(query) {
|
|
220
|
+
const results = [];
|
|
221
|
+
await Promise.all(Array.from(this.providers.values(), async (provider) => {
|
|
222
|
+
try {
|
|
223
|
+
const result = await this.withQueryTimeout(query(provider), 'query a frontend');
|
|
224
|
+
if (result !== undefined) {
|
|
225
|
+
results.push(result);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
this.logger.warn('Failed to query a frontend session provider, skipping it.', error);
|
|
230
|
+
}
|
|
231
|
+
}));
|
|
232
|
+
return results;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Rejects when the query does not answer within {@link queryTimeout}. Theia's RPC has no
|
|
236
|
+
* request timeout of its own, so an unresponsive frontend would otherwise stall the
|
|
237
|
+
* external API request that fans out to it indefinitely.
|
|
238
|
+
*/
|
|
239
|
+
withQueryTimeout(query, description) {
|
|
240
|
+
let timer;
|
|
241
|
+
const timeout = new Promise((_, reject) => {
|
|
242
|
+
timer = setTimeout(() => reject(new Error(`A frontend did not ${description} within ${this.queryTimeout} ms.`)), this.queryTimeout);
|
|
243
|
+
});
|
|
244
|
+
return Promise.race([query, timeout]).finally(() => clearTimeout(timer));
|
|
245
|
+
}
|
|
246
|
+
prefer(candidate, existing) {
|
|
247
|
+
if (candidate.restored !== existing.restored) {
|
|
248
|
+
return candidate.restored;
|
|
249
|
+
}
|
|
250
|
+
const candidateInProgress = chat_model_1.ChatSessionStatus.isInProgress(candidate.status);
|
|
251
|
+
if (candidateInProgress !== chat_model_1.ChatSessionStatus.isInProgress(existing.status)) {
|
|
252
|
+
return candidateInProgress;
|
|
253
|
+
}
|
|
254
|
+
return (candidate.lastInteraction ?? 0) > (existing.lastInteraction ?? 0);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
exports.ExternalChatSessionRegistry = ExternalChatSessionRegistry;
|
|
258
|
+
tslib_1.__decorate([
|
|
259
|
+
(0, inversify_1.inject)(logger_1.ILogger),
|
|
260
|
+
(0, inversify_1.named)('ai-external-api:ExternalChatSessionRegistry'),
|
|
261
|
+
tslib_1.__metadata("design:type", Object)
|
|
262
|
+
], ExternalChatSessionRegistry.prototype, "logger", void 0);
|
|
263
|
+
exports.ExternalChatSessionRegistry = ExternalChatSessionRegistry = tslib_1.__decorate([
|
|
264
|
+
(0, inversify_1.injectable)()
|
|
265
|
+
], ExternalChatSessionRegistry);
|
|
266
|
+
//# sourceMappingURL=external-chat-session-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-chat-session-registry.js","sourceRoot":"","sources":["../../src/node/external-chat-session-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external-chat-session-registry.spec.d.ts","sourceRoot":"","sources":["../../src/node/external-chat-session-registry.spec.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// *****************************************************************************
|
|
3
|
+
// Copyright (C) 2026 EclipseSource GmbH.
|
|
4
|
+
//
|
|
5
|
+
// This program and the accompanying materials are made available under the
|
|
6
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
7
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
8
|
+
//
|
|
9
|
+
// This Source Code may also be made available under the following Secondary
|
|
10
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
11
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
12
|
+
// with the GNU Classpath Exception which is available at
|
|
13
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
14
|
+
//
|
|
15
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
16
|
+
// *****************************************************************************
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
const mock_logger_1 = require("@theia/core/lib/common/test/mock-logger");
|
|
19
|
+
const chai_1 = require("chai");
|
|
20
|
+
const external_api_test_support_1 = require("@theia/external-api/lib/node/test/external-api-test-support");
|
|
21
|
+
const external_chat_session_registry_1 = require("./external-chat-session-registry");
|
|
22
|
+
describe('ExternalChatSessionRegistry', () => {
|
|
23
|
+
function createRegistry(queryTimeout) {
|
|
24
|
+
return external_api_test_support_1.ExternalApiTestSupport.inject(new external_chat_session_registry_1.ExternalChatSessionRegistry(), {
|
|
25
|
+
logger: new mock_logger_1.MockLogger(),
|
|
26
|
+
...queryTimeout === undefined ? {} : { queryTimeout }
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** A provider whose queries never answer, standing in for an unresponsive frontend. */
|
|
30
|
+
function unresponsiveProvider() {
|
|
31
|
+
const hang = () => new Promise(() => { });
|
|
32
|
+
return {
|
|
33
|
+
getWorkspace: hang,
|
|
34
|
+
getSessions: hang,
|
|
35
|
+
getSessionSummary: hang,
|
|
36
|
+
getSession: hang,
|
|
37
|
+
openSession: hang,
|
|
38
|
+
restoreSession: hang,
|
|
39
|
+
sendPrompt: hang,
|
|
40
|
+
createSession: hang
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function summary(id, status = 'idle', lastInteraction, restored = true) {
|
|
44
|
+
return { id, status, lastInteraction, restored };
|
|
45
|
+
}
|
|
46
|
+
function detail(id, status = 'idle', lastInteraction, restored = true) {
|
|
47
|
+
return { ...summary(id, status, lastInteraction, restored), messages: [{ actor: 'user', text: `question for ${id}` }] };
|
|
48
|
+
}
|
|
49
|
+
function provider(sessions, details = [], actions = {}) {
|
|
50
|
+
return {
|
|
51
|
+
getWorkspace: async () => undefined,
|
|
52
|
+
getSessions: async () => sessions,
|
|
53
|
+
getSessionSummary: async (sessionId) => details.find(candidate => candidate.id === sessionId) ?? sessions.find(candidate => candidate.id === sessionId),
|
|
54
|
+
getSession: async (sessionId) => details.find(candidate => candidate.id === sessionId),
|
|
55
|
+
openSession: async () => false,
|
|
56
|
+
restoreSession: async () => undefined,
|
|
57
|
+
sendPrompt: async () => undefined,
|
|
58
|
+
createSession: async () => ({ created: { session: summary('created') } }),
|
|
59
|
+
...actions
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function failingProvider() {
|
|
63
|
+
const fail = async () => { throw new Error('connection lost'); };
|
|
64
|
+
return {
|
|
65
|
+
getWorkspace: fail,
|
|
66
|
+
getSessions: fail,
|
|
67
|
+
getSessionSummary: fail,
|
|
68
|
+
getSession: fail,
|
|
69
|
+
openSession: fail,
|
|
70
|
+
restoreSession: fail,
|
|
71
|
+
sendPrompt: fail,
|
|
72
|
+
createSession: fail
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
describe('getSessions', () => {
|
|
76
|
+
it('returns an empty list without providers', async () => {
|
|
77
|
+
const registry = createRegistry();
|
|
78
|
+
(0, chai_1.expect)(await registry.getSessions()).to.deep.equal([]);
|
|
79
|
+
});
|
|
80
|
+
it('aggregates sessions of all providers sorted by last interaction', async () => {
|
|
81
|
+
const registry = createRegistry();
|
|
82
|
+
registry.addProvider(provider([summary('1', 'idle', 100)]));
|
|
83
|
+
registry.addProvider(provider([summary('2', 'idle', 300), summary('3', 'idle', 200)]));
|
|
84
|
+
const sessions = await registry.getSessions();
|
|
85
|
+
(0, chai_1.expect)(sessions.map(session => session.id)).to.deep.equal(['2', '3', '1']);
|
|
86
|
+
});
|
|
87
|
+
it('deduplicates sessions preferring restored reports', async () => {
|
|
88
|
+
const registry = createRegistry();
|
|
89
|
+
registry.addProvider(provider([summary('1', 'running', 300, false)]));
|
|
90
|
+
registry.addProvider(provider([summary('1', 'idle', 100, true)]));
|
|
91
|
+
const sessions = await registry.getSessions();
|
|
92
|
+
(0, chai_1.expect)(sessions).to.have.length(1);
|
|
93
|
+
(0, chai_1.expect)(sessions[0].restored).to.equal(true);
|
|
94
|
+
});
|
|
95
|
+
it('deduplicates sessions preferring in-progress reports', async () => {
|
|
96
|
+
const registry = createRegistry();
|
|
97
|
+
registry.addProvider(provider([summary('1', 'idle', 200)]));
|
|
98
|
+
registry.addProvider(provider([summary('1', 'running', 100)]));
|
|
99
|
+
const sessions = await registry.getSessions();
|
|
100
|
+
(0, chai_1.expect)(sessions).to.have.length(1);
|
|
101
|
+
(0, chai_1.expect)(sessions[0].status).to.equal('running');
|
|
102
|
+
});
|
|
103
|
+
it('deduplicates sessions preferring more recent reports', async () => {
|
|
104
|
+
const registry = createRegistry();
|
|
105
|
+
registry.addProvider(provider([summary('1', 'idle', 100)]));
|
|
106
|
+
registry.addProvider(provider([summary('1', 'failed', 200)]));
|
|
107
|
+
const sessions = await registry.getSessions();
|
|
108
|
+
(0, chai_1.expect)(sessions).to.have.length(1);
|
|
109
|
+
(0, chai_1.expect)(sessions[0].status).to.equal('failed');
|
|
110
|
+
});
|
|
111
|
+
it('no longer reports sessions of removed providers', async () => {
|
|
112
|
+
const registry = createRegistry();
|
|
113
|
+
const removed = provider([summary('1')]);
|
|
114
|
+
registry.addProvider(removed);
|
|
115
|
+
registry.addProvider(provider([summary('2')]));
|
|
116
|
+
registry.removeProvider(removed);
|
|
117
|
+
const sessions = await registry.getSessions();
|
|
118
|
+
(0, chai_1.expect)(sessions.map(session => session.id)).to.deep.equal(['2']);
|
|
119
|
+
});
|
|
120
|
+
it('skips failing providers', async () => {
|
|
121
|
+
const registry = createRegistry();
|
|
122
|
+
registry.addProvider(failingProvider());
|
|
123
|
+
registry.addProvider(provider([summary('1')]));
|
|
124
|
+
const sessions = await registry.getSessions();
|
|
125
|
+
(0, chai_1.expect)(sessions.map(session => session.id)).to.deep.equal(['1']);
|
|
126
|
+
});
|
|
127
|
+
it('skips unresponsive providers instead of waiting for them', async () => {
|
|
128
|
+
const registry = createRegistry(20);
|
|
129
|
+
registry.addProvider(unresponsiveProvider());
|
|
130
|
+
registry.addProvider(provider([summary('1')]));
|
|
131
|
+
const sessions = await registry.getSessions();
|
|
132
|
+
(0, chai_1.expect)(sessions.map(session => session.id)).to.deep.equal(['1']);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('getSession', () => {
|
|
136
|
+
it('returns undefined for unknown sessions', async () => {
|
|
137
|
+
const registry = createRegistry();
|
|
138
|
+
registry.addProvider(provider([]));
|
|
139
|
+
(0, chai_1.expect)(await registry.getSession('missing')).to.equal(undefined);
|
|
140
|
+
});
|
|
141
|
+
it('returns the session from the provider that knows it', async () => {
|
|
142
|
+
const registry = createRegistry();
|
|
143
|
+
registry.addProvider(provider([]));
|
|
144
|
+
registry.addProvider(provider([], [detail('1', 'running')]));
|
|
145
|
+
const session = await registry.getSession('1');
|
|
146
|
+
(0, chai_1.expect)(session?.status).to.equal('running');
|
|
147
|
+
(0, chai_1.expect)(session?.messages).to.deep.equal([{ actor: 'user', text: 'question for 1' }]);
|
|
148
|
+
});
|
|
149
|
+
it('prefers the in-progress duplicate', async () => {
|
|
150
|
+
const registry = createRegistry();
|
|
151
|
+
registry.addProvider(provider([], [detail('1', 'idle', 200)]));
|
|
152
|
+
registry.addProvider(provider([], [detail('1', 'awaitingApproval', 100)]));
|
|
153
|
+
const session = await registry.getSession('1');
|
|
154
|
+
(0, chai_1.expect)(session?.status).to.equal('awaitingApproval');
|
|
155
|
+
});
|
|
156
|
+
it('prefers the restored duplicate', async () => {
|
|
157
|
+
const registry = createRegistry();
|
|
158
|
+
registry.addProvider(provider([], [detail('1', 'idle', 100, true)]));
|
|
159
|
+
registry.addProvider(provider([], [detail('1', 'failed', 200, false)]));
|
|
160
|
+
const session = await registry.getSession('1');
|
|
161
|
+
(0, chai_1.expect)(session?.restored).to.equal(true);
|
|
162
|
+
});
|
|
163
|
+
it('skips failing providers', async () => {
|
|
164
|
+
const registry = createRegistry();
|
|
165
|
+
registry.addProvider(failingProvider());
|
|
166
|
+
registry.addProvider(provider([], [detail('1')]));
|
|
167
|
+
const session = await registry.getSession('1');
|
|
168
|
+
(0, chai_1.expect)(session?.id).to.equal('1');
|
|
169
|
+
});
|
|
170
|
+
it('reads the conversation from the preferred frontend only', async () => {
|
|
171
|
+
const registry = createRegistry();
|
|
172
|
+
const read = [];
|
|
173
|
+
const tracking = (name, known) => provider([], [known], { getSession: async (sessionId) => { read.push(name); return sessionId === known.id ? known : undefined; } });
|
|
174
|
+
registry.addProvider(tracking('idle', detail('1', 'idle', 200)));
|
|
175
|
+
registry.addProvider(tracking('running', detail('1', 'running', 100)));
|
|
176
|
+
const session = await registry.getSession('1');
|
|
177
|
+
(0, chai_1.expect)(session?.status).to.equal('running');
|
|
178
|
+
(0, chai_1.expect)(read).to.deep.equal(['running']);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe('openSession', () => {
|
|
182
|
+
it('returns false when no provider knows the session', async () => {
|
|
183
|
+
const registry = createRegistry();
|
|
184
|
+
registry.addProvider(provider([]));
|
|
185
|
+
(0, chai_1.expect)(await registry.openSession('missing')).to.equal(false);
|
|
186
|
+
});
|
|
187
|
+
it('opens the session in the frontend that knows it', async () => {
|
|
188
|
+
const registry = createRegistry();
|
|
189
|
+
const opened = [];
|
|
190
|
+
registry.addProvider(provider([]));
|
|
191
|
+
registry.addProvider(provider([], [detail('1')], {
|
|
192
|
+
openSession: async (sessionId) => { opened.push(sessionId); return true; }
|
|
193
|
+
}));
|
|
194
|
+
(0, chai_1.expect)(await registry.openSession('1')).to.equal(true);
|
|
195
|
+
(0, chai_1.expect)(opened).to.deep.equal(['1']);
|
|
196
|
+
});
|
|
197
|
+
it('prefers the frontend with the restored session', async () => {
|
|
198
|
+
const registry = createRegistry();
|
|
199
|
+
const opened = [];
|
|
200
|
+
registry.addProvider(provider([], [detail('1', 'idle', 300, false)], {
|
|
201
|
+
openSession: async () => { opened.push('persisted-only'); return true; }
|
|
202
|
+
}));
|
|
203
|
+
registry.addProvider(provider([], [detail('1', 'idle', 100, true)], {
|
|
204
|
+
openSession: async () => { opened.push('restored'); return true; }
|
|
205
|
+
}));
|
|
206
|
+
(0, chai_1.expect)(await registry.openSession('1')).to.equal(true);
|
|
207
|
+
(0, chai_1.expect)(opened).to.deep.equal(['restored']);
|
|
208
|
+
});
|
|
209
|
+
it('falls back to the next frontend when the preferred one fails', async () => {
|
|
210
|
+
const registry = createRegistry();
|
|
211
|
+
registry.addProvider(provider([], [detail('1', 'running', 200, true)], {
|
|
212
|
+
openSession: async () => { throw new Error('connection lost'); }
|
|
213
|
+
}));
|
|
214
|
+
registry.addProvider(provider([], [detail('1', 'idle', 100, true)], {
|
|
215
|
+
openSession: async () => true
|
|
216
|
+
}));
|
|
217
|
+
(0, chai_1.expect)(await registry.openSession('1')).to.equal(true);
|
|
218
|
+
});
|
|
219
|
+
it('keeps the registration order when neither report is preferred', async () => {
|
|
220
|
+
const registry = createRegistry();
|
|
221
|
+
const opened = [];
|
|
222
|
+
registry.addProvider(provider([], [detail('1', 'idle', 100, true)], {
|
|
223
|
+
openSession: async () => { opened.push('first'); return true; }
|
|
224
|
+
}));
|
|
225
|
+
registry.addProvider(provider([], [detail('1', 'idle', 100, true)], {
|
|
226
|
+
openSession: async () => { opened.push('second'); return true; }
|
|
227
|
+
}));
|
|
228
|
+
(0, chai_1.expect)(await registry.openSession('1')).to.equal(true);
|
|
229
|
+
(0, chai_1.expect)(opened).to.deep.equal(['first']);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
describe('restoreSession', () => {
|
|
233
|
+
it('returns undefined when no provider knows the session', async () => {
|
|
234
|
+
const registry = createRegistry();
|
|
235
|
+
registry.addProvider(provider([]));
|
|
236
|
+
(0, chai_1.expect)(await registry.restoreSession('missing')).to.equal(undefined);
|
|
237
|
+
});
|
|
238
|
+
it('restores the session in the frontend that knows it', async () => {
|
|
239
|
+
const registry = createRegistry();
|
|
240
|
+
const restored = detail('1', 'idle', 100, true);
|
|
241
|
+
registry.addProvider(provider([]));
|
|
242
|
+
registry.addProvider(provider([], [detail('1', 'idle', 100, false)], {
|
|
243
|
+
restoreSession: async () => restored
|
|
244
|
+
}));
|
|
245
|
+
(0, chai_1.expect)(await registry.restoreSession('1')).to.deep.equal(restored);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
describe('sendPrompt', () => {
|
|
249
|
+
it('returns undefined when no provider knows the session', async () => {
|
|
250
|
+
const registry = createRegistry();
|
|
251
|
+
registry.addProvider(provider([]));
|
|
252
|
+
(0, chai_1.expect)(await registry.sendPrompt('missing', { text: 'hello' })).to.equal(undefined);
|
|
253
|
+
});
|
|
254
|
+
it('sends the prompt via the frontend that knows the session', async () => {
|
|
255
|
+
const registry = createRegistry();
|
|
256
|
+
const prompts = [];
|
|
257
|
+
registry.addProvider(provider([]));
|
|
258
|
+
registry.addProvider(provider([], [detail('1')], {
|
|
259
|
+
sendPrompt: async (sessionId, prompt) => { prompts.push(prompt.text); return { sent: { sessionId, requestId: 'r1' } }; }
|
|
260
|
+
}));
|
|
261
|
+
(0, chai_1.expect)(await registry.sendPrompt('1', { text: 'hello' })).to.deep.equal({ sent: { sessionId: '1', requestId: 'r1' } });
|
|
262
|
+
(0, chai_1.expect)(prompts).to.deep.equal(['hello']);
|
|
263
|
+
});
|
|
264
|
+
it('does not retry a busy rejection on other frontends', async () => {
|
|
265
|
+
const registry = createRegistry();
|
|
266
|
+
const prompted = [];
|
|
267
|
+
registry.addProvider(provider([], [detail('1', 'running', 100, true)], {
|
|
268
|
+
sendPrompt: async () => { prompted.push('restored'); return { failure: 'busy' }; }
|
|
269
|
+
}));
|
|
270
|
+
registry.addProvider(provider([], [detail('1', 'idle', 300, false)], {
|
|
271
|
+
sendPrompt: async (sessionId) => { prompted.push('persisted-only'); return { sent: { sessionId, requestId: 'r1' } }; }
|
|
272
|
+
}));
|
|
273
|
+
(0, chai_1.expect)(await registry.sendPrompt('1', { text: 'hello' })).to.deep.equal({ failure: 'busy' });
|
|
274
|
+
(0, chai_1.expect)(prompted).to.deep.equal(['restored']);
|
|
275
|
+
});
|
|
276
|
+
it('falls back to the next frontend when the preferred one fails', async () => {
|
|
277
|
+
const registry = createRegistry();
|
|
278
|
+
registry.addProvider(provider([], [detail('1', 'running', 200, true)], {
|
|
279
|
+
sendPrompt: async () => { throw new Error('connection lost'); }
|
|
280
|
+
}));
|
|
281
|
+
registry.addProvider(provider([], [detail('1', 'idle', 100, true)], {
|
|
282
|
+
sendPrompt: async (sessionId) => ({ sent: { sessionId, requestId: 'r1' } })
|
|
283
|
+
}));
|
|
284
|
+
(0, chai_1.expect)(await registry.sendPrompt('1', { text: 'hello' })).to.deep.equal({ sent: { sessionId: '1', requestId: 'r1' } });
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
describe('createSession', () => {
|
|
288
|
+
it('rejects when no frontend is connected', async () => {
|
|
289
|
+
const registry = createRegistry();
|
|
290
|
+
(0, chai_1.expect)(await registry.createSession({})).to.deep.equal({ failure: 'noFrontend' });
|
|
291
|
+
(0, chai_1.expect)(await registry.createSession({ workspace: 'file:///project' })).to.deep.equal({ failure: 'noFrontend' });
|
|
292
|
+
});
|
|
293
|
+
it('creates in the frontend with the requested workspace', async () => {
|
|
294
|
+
const registry = createRegistry();
|
|
295
|
+
const creators = [];
|
|
296
|
+
registry.addProvider(provider([], [], {
|
|
297
|
+
getWorkspace: async () => 'file:///other',
|
|
298
|
+
createSession: async () => { creators.push('a'); return { created: { session: summary('a') } }; }
|
|
299
|
+
}));
|
|
300
|
+
registry.addProvider(provider([], [], {
|
|
301
|
+
getWorkspace: async () => 'file:///project',
|
|
302
|
+
createSession: async () => { creators.push('b'); return { created: { session: summary('b') } }; }
|
|
303
|
+
}));
|
|
304
|
+
const result = await registry.createSession({ workspace: 'file:///project', prompt: 'hello' });
|
|
305
|
+
(0, chai_1.expect)(result).to.deep.equal({ created: { session: summary('b') } });
|
|
306
|
+
(0, chai_1.expect)(creators).to.deep.equal(['b']);
|
|
307
|
+
});
|
|
308
|
+
it('matches workspaces ignoring trailing slashes', async () => {
|
|
309
|
+
const registry = createRegistry();
|
|
310
|
+
registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///project/' }));
|
|
311
|
+
const result = await registry.createSession({ workspace: 'file:///project' });
|
|
312
|
+
(0, chai_1.expect)('created' in result).to.equal(true);
|
|
313
|
+
});
|
|
314
|
+
it('rejects an unknown workspace', async () => {
|
|
315
|
+
const registry = createRegistry();
|
|
316
|
+
registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///other' }));
|
|
317
|
+
(0, chai_1.expect)(await registry.createSession({ workspace: 'file:///project' })).to.deep.equal({ failure: 'workspaceNotFound' });
|
|
318
|
+
});
|
|
319
|
+
it('creates in the only connected frontend when no workspace is given', async () => {
|
|
320
|
+
const registry = createRegistry();
|
|
321
|
+
registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///project' }));
|
|
322
|
+
const result = await registry.createSession({});
|
|
323
|
+
(0, chai_1.expect)('created' in result).to.equal(true);
|
|
324
|
+
});
|
|
325
|
+
it('rejects when no workspace is given and the frontends have different workspaces', async () => {
|
|
326
|
+
const registry = createRegistry();
|
|
327
|
+
registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///one' }));
|
|
328
|
+
registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///two' }));
|
|
329
|
+
(0, chai_1.expect)(await registry.createSession({})).to.deep.equal({ failure: 'ambiguousWorkspace' });
|
|
330
|
+
});
|
|
331
|
+
it('creates when no workspace is given and the frontends share a workspace', async () => {
|
|
332
|
+
const registry = createRegistry();
|
|
333
|
+
registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///project' }));
|
|
334
|
+
registry.addProvider(provider([], [], { getWorkspace: async () => 'file:///project/' }));
|
|
335
|
+
const result = await registry.createSession({});
|
|
336
|
+
(0, chai_1.expect)('created' in result).to.equal(true);
|
|
337
|
+
});
|
|
338
|
+
it('passes provider failures through', async () => {
|
|
339
|
+
const registry = createRegistry();
|
|
340
|
+
registry.addProvider(provider([], [], { createSession: async () => ({ failure: 'unknownAgent' }) }));
|
|
341
|
+
(0, chai_1.expect)(await registry.createSession({ agentId: 'ghost' })).to.deep.equal({ failure: 'unknownAgent' });
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
describe('onDidChangeSessions', () => {
|
|
345
|
+
it('fires when providers are added, removed, or report changes', () => {
|
|
346
|
+
const registry = createRegistry();
|
|
347
|
+
let events = 0;
|
|
348
|
+
registry.onDidChangeSessions(() => events++);
|
|
349
|
+
const added = provider([]);
|
|
350
|
+
registry.addProvider(added);
|
|
351
|
+
(0, chai_1.expect)(events).to.equal(1);
|
|
352
|
+
registry.notifySessionsChanged();
|
|
353
|
+
(0, chai_1.expect)(events).to.equal(2);
|
|
354
|
+
registry.removeProvider(added);
|
|
355
|
+
(0, chai_1.expect)(events).to.equal(3);
|
|
356
|
+
});
|
|
357
|
+
it('does not fire when removing an unknown provider', () => {
|
|
358
|
+
const registry = createRegistry();
|
|
359
|
+
let events = 0;
|
|
360
|
+
registry.onDidChangeSessions(() => events++);
|
|
361
|
+
registry.removeProvider(provider([]));
|
|
362
|
+
(0, chai_1.expect)(events).to.equal(0);
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
});
|
|
366
|
+
//# sourceMappingURL=external-chat-session-registry.spec.js.map
|