@pooder/core 2.1.0 → 2.2.0
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/CHANGELOG.md +6 -0
- package/dist/index.d.mts +97 -18
- package/dist/index.d.ts +97 -18
- package/dist/index.js +473 -188
- package/dist/index.mjs +464 -187
- package/package.json +1 -1
- package/src/context.ts +7 -2
- package/src/extension.ts +10 -4
- package/src/index.ts +209 -32
- package/src/service.ts +191 -11
- package/src/services/ToolSessionService.ts +41 -4
- package/src/services/WorkbenchService.ts +65 -15
- package/src/services/index.ts +14 -0
- package/src/services/tokens.ts +27 -0
package/package.json
CHANGED
package/src/context.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import EventBus from "./event";
|
|
2
2
|
import { Contribution } from "./contribution";
|
|
3
|
-
import { Service } from "./service";
|
|
3
|
+
import { Service, ServiceIdentifier } from "./service";
|
|
4
4
|
import Disposable from "./disposable";
|
|
5
5
|
|
|
6
6
|
interface ExtensionContext {
|
|
7
7
|
readonly eventBus: EventBus;
|
|
8
8
|
readonly services: {
|
|
9
|
-
get<T extends Service>(
|
|
9
|
+
get<T extends Service>(identifier: ServiceIdentifier<T>): T | undefined;
|
|
10
|
+
getOrThrow<T extends Service>(
|
|
11
|
+
identifier: ServiceIdentifier<T>,
|
|
12
|
+
errorMessage?: string,
|
|
13
|
+
): T;
|
|
14
|
+
has(identifier: ServiceIdentifier<Service>): boolean;
|
|
10
15
|
};
|
|
11
16
|
readonly contributions: {
|
|
12
17
|
get<T>(pointId: string): Contribution<T>[];
|
package/src/extension.ts
CHANGED
|
@@ -4,6 +4,11 @@ import Disposable from "./disposable";
|
|
|
4
4
|
import CommandService from "./services/CommandService";
|
|
5
5
|
import { ConfigurationService } from "./services";
|
|
6
6
|
import ToolRegistryService from "./services/ToolRegistryService";
|
|
7
|
+
import {
|
|
8
|
+
COMMAND_SERVICE,
|
|
9
|
+
CONFIGURATION_SERVICE,
|
|
10
|
+
TOOL_REGISTRY_SERVICE,
|
|
11
|
+
} from "./services";
|
|
7
12
|
|
|
8
13
|
interface ExtensionMetadata {
|
|
9
14
|
name: string;
|
|
@@ -101,19 +106,20 @@ class ExtensionManager {
|
|
|
101
106
|
// If registering configurations, update ConfigurationService defaults
|
|
102
107
|
if (pointId === ContributionPointIds.CONFIGURATIONS) {
|
|
103
108
|
const configService = this.context.services.get<ConfigurationService>(
|
|
104
|
-
|
|
109
|
+
CONFIGURATION_SERVICE,
|
|
105
110
|
);
|
|
106
111
|
configService?.initializeDefaults([item.data]);
|
|
107
112
|
}
|
|
108
113
|
if (pointId === ContributionPointIds.COMMANDS && item.data.handler) {
|
|
109
114
|
const commandService =
|
|
110
|
-
this.context.services.get<CommandService>(
|
|
115
|
+
this.context.services.get<CommandService>(COMMAND_SERVICE)!;
|
|
111
116
|
|
|
112
117
|
return commandService.registerCommand(item.id, item.data.handler);
|
|
113
118
|
}
|
|
114
119
|
if (pointId === ContributionPointIds.TOOLS) {
|
|
115
|
-
const toolRegistry =
|
|
116
|
-
|
|
120
|
+
const toolRegistry = this.context.services.get<ToolRegistryService>(
|
|
121
|
+
TOOL_REGISTRY_SERVICE,
|
|
122
|
+
);
|
|
117
123
|
if (!toolRegistry) return;
|
|
118
124
|
return toolRegistry.registerTool(item.data);
|
|
119
125
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
isServiceToken,
|
|
3
|
+
RegisterServiceOptions,
|
|
4
|
+
Service,
|
|
5
|
+
ServiceContext,
|
|
6
|
+
ServiceIdentifier,
|
|
7
|
+
ServiceRegistry,
|
|
8
|
+
} from "./service";
|
|
2
9
|
import EventBus from "./event";
|
|
3
10
|
import { ExtensionManager } from "./extension";
|
|
4
11
|
import Disposable from "./disposable";
|
|
@@ -9,6 +16,7 @@ import {
|
|
|
9
16
|
ContributionRegistry,
|
|
10
17
|
} from "./contribution";
|
|
11
18
|
import {
|
|
19
|
+
CORE_SERVICE_TOKENS,
|
|
12
20
|
CommandService,
|
|
13
21
|
ConfigurationService,
|
|
14
22
|
ToolRegistryService,
|
|
@@ -27,6 +35,16 @@ export { default as EventBus } from "./event";
|
|
|
27
35
|
export class Pooder {
|
|
28
36
|
readonly eventBus: EventBus = new EventBus();
|
|
29
37
|
private readonly services: ServiceRegistry = new ServiceRegistry();
|
|
38
|
+
private readonly serviceContext: ServiceContext = {
|
|
39
|
+
eventBus: this.eventBus,
|
|
40
|
+
get: <T extends Service>(identifier: ServiceIdentifier<T>) =>
|
|
41
|
+
this.services.get(identifier),
|
|
42
|
+
getOrThrow: <T extends Service>(
|
|
43
|
+
identifier: ServiceIdentifier<T>,
|
|
44
|
+
errorMessage?: string,
|
|
45
|
+
) => this.services.getOrThrow(identifier, errorMessage),
|
|
46
|
+
has: (identifier: ServiceIdentifier<Service>) => this.services.has(identifier),
|
|
47
|
+
};
|
|
30
48
|
private readonly contributions: ContributionRegistry =
|
|
31
49
|
new ContributionRegistry();
|
|
32
50
|
readonly extensionManager: ExtensionManager;
|
|
@@ -36,31 +54,39 @@ export class Pooder {
|
|
|
36
54
|
this.initDefaultContributionPoints();
|
|
37
55
|
|
|
38
56
|
const commandService = new CommandService();
|
|
39
|
-
this.registerService(commandService,
|
|
57
|
+
this.registerService(commandService, CORE_SERVICE_TOKENS.COMMAND);
|
|
40
58
|
|
|
41
59
|
const configurationService = new ConfigurationService();
|
|
42
|
-
this.registerService(configurationService,
|
|
60
|
+
this.registerService(configurationService, CORE_SERVICE_TOKENS.CONFIGURATION);
|
|
43
61
|
|
|
44
62
|
const toolRegistryService = new ToolRegistryService();
|
|
45
|
-
this.registerService(toolRegistryService,
|
|
63
|
+
this.registerService(toolRegistryService, CORE_SERVICE_TOKENS.TOOL_REGISTRY);
|
|
46
64
|
|
|
47
|
-
const toolSessionService = new ToolSessionService(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
65
|
+
const toolSessionService = new ToolSessionService({
|
|
66
|
+
commandService,
|
|
67
|
+
toolRegistry: toolRegistryService,
|
|
68
|
+
});
|
|
69
|
+
this.registerService(toolSessionService, CORE_SERVICE_TOKENS.TOOL_SESSION);
|
|
51
70
|
|
|
52
|
-
const workbenchService = new WorkbenchService(
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
71
|
+
const workbenchService = new WorkbenchService({
|
|
72
|
+
eventBus: this.eventBus,
|
|
73
|
+
toolRegistry: toolRegistryService,
|
|
74
|
+
sessionService: toolSessionService,
|
|
75
|
+
});
|
|
76
|
+
this.registerService(workbenchService, CORE_SERVICE_TOKENS.WORKBENCH);
|
|
57
77
|
|
|
58
78
|
// Create a restricted context for extensions
|
|
59
79
|
const context: ExtensionContext = {
|
|
60
80
|
eventBus: this.eventBus,
|
|
61
81
|
services: {
|
|
62
|
-
get: <T extends Service>(
|
|
63
|
-
this.services.get
|
|
82
|
+
get: <T extends Service>(identifier: ServiceIdentifier<T>) =>
|
|
83
|
+
this.services.get(identifier),
|
|
84
|
+
getOrThrow: <T extends Service>(
|
|
85
|
+
identifier: ServiceIdentifier<T>,
|
|
86
|
+
errorMessage?: string,
|
|
87
|
+
) => this.services.getOrThrow(identifier, errorMessage),
|
|
88
|
+
has: (identifier: ServiceIdentifier<Service>) =>
|
|
89
|
+
this.services.has(identifier),
|
|
64
90
|
},
|
|
65
91
|
contributions: {
|
|
66
92
|
get: <T>(pointId: string) => this.getContributions<T>(pointId),
|
|
@@ -101,42 +127,138 @@ export class Pooder {
|
|
|
101
127
|
|
|
102
128
|
// --- Service Management ---
|
|
103
129
|
|
|
104
|
-
registerService
|
|
105
|
-
|
|
130
|
+
registerService<T extends Service>(
|
|
131
|
+
service: T,
|
|
132
|
+
identifier?: ServiceIdentifier<T>,
|
|
133
|
+
options: RegisterServiceOptions = {},
|
|
134
|
+
): boolean {
|
|
135
|
+
const serviceIdentifier = this.resolveServiceIdentifier(service, identifier);
|
|
136
|
+
const serviceId = this.getServiceLabel(serviceIdentifier);
|
|
106
137
|
|
|
107
138
|
try {
|
|
108
|
-
service
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
const initResult = this.invokeServiceHook(service, "init");
|
|
140
|
+
if (this.isPromiseLike(initResult)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Service "${serviceId}" init() is async. Use registerServiceAsync() instead.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
this.services.register(serviceIdentifier, service, options);
|
|
147
|
+
this.eventBus.emit("service:register", service, { id: serviceId });
|
|
148
|
+
return true;
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error(`Error initializing service ${serviceId}:`, error);
|
|
111
151
|
return false;
|
|
112
152
|
}
|
|
153
|
+
}
|
|
113
154
|
|
|
114
|
-
|
|
115
|
-
|
|
155
|
+
async registerServiceAsync<T extends Service>(
|
|
156
|
+
service: T,
|
|
157
|
+
identifier?: ServiceIdentifier<T>,
|
|
158
|
+
options: RegisterServiceOptions = {},
|
|
159
|
+
): Promise<boolean> {
|
|
160
|
+
const serviceIdentifier = this.resolveServiceIdentifier(service, identifier);
|
|
161
|
+
const serviceId = this.getServiceLabel(serviceIdentifier);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await this.invokeServiceHookAsync(service, "init");
|
|
165
|
+
this.services.register(serviceIdentifier, service, options);
|
|
166
|
+
this.eventBus.emit("service:register", service, { id: serviceId });
|
|
167
|
+
return true;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
console.error(`Error initializing service ${serviceId}:`, error);
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
unregisterService(service: Service, id?: ServiceIdentifier<Service>): boolean;
|
|
175
|
+
unregisterService(identifier: ServiceIdentifier<Service>): boolean;
|
|
176
|
+
unregisterService(
|
|
177
|
+
serviceOrIdentifier: Service | ServiceIdentifier<Service>,
|
|
178
|
+
id?: ServiceIdentifier<Service>,
|
|
179
|
+
): boolean {
|
|
180
|
+
const resolvedIdentifier = this.resolveUnregisterIdentifier(
|
|
181
|
+
serviceOrIdentifier,
|
|
182
|
+
id,
|
|
183
|
+
);
|
|
184
|
+
const serviceId = this.getServiceLabel(resolvedIdentifier);
|
|
185
|
+
const registeredService = this.services.get(resolvedIdentifier);
|
|
186
|
+
|
|
187
|
+
if (!registeredService) {
|
|
188
|
+
console.warn(`Service ${serviceId} is not registered.`);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
const disposeResult = this.invokeServiceHook(registeredService, "dispose");
|
|
194
|
+
if (this.isPromiseLike(disposeResult)) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`Service "${serviceId}" dispose() is async. Use unregisterServiceAsync() instead.`,
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
console.error(`Error disposing service ${serviceId}:`, error);
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
this.services.delete(resolvedIdentifier);
|
|
205
|
+
this.eventBus.emit("service:unregister", registeredService, { id: serviceId });
|
|
116
206
|
return true;
|
|
117
207
|
}
|
|
118
208
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
209
|
+
async unregisterServiceAsync(
|
|
210
|
+
serviceOrIdentifier: Service | ServiceIdentifier<Service>,
|
|
211
|
+
id?: ServiceIdentifier<Service>,
|
|
212
|
+
): Promise<boolean> {
|
|
213
|
+
const resolvedIdentifier = this.resolveUnregisterIdentifier(
|
|
214
|
+
serviceOrIdentifier,
|
|
215
|
+
id,
|
|
216
|
+
);
|
|
217
|
+
const serviceId = this.getServiceLabel(resolvedIdentifier);
|
|
218
|
+
const registeredService = this.services.get(resolvedIdentifier);
|
|
219
|
+
|
|
220
|
+
if (!registeredService) {
|
|
122
221
|
console.warn(`Service ${serviceId} is not registered.`);
|
|
123
222
|
return true;
|
|
124
223
|
}
|
|
125
224
|
|
|
126
225
|
try {
|
|
127
|
-
|
|
128
|
-
} catch (
|
|
129
|
-
console.error(`Error disposing service ${serviceId}:`,
|
|
226
|
+
await this.invokeServiceHookAsync(registeredService, "dispose");
|
|
227
|
+
} catch (error) {
|
|
228
|
+
console.error(`Error disposing service ${serviceId}:`, error);
|
|
130
229
|
return false;
|
|
131
230
|
}
|
|
132
231
|
|
|
133
|
-
this.services.delete(
|
|
134
|
-
this.eventBus.emit("service:unregister",
|
|
232
|
+
this.services.delete(resolvedIdentifier);
|
|
233
|
+
this.eventBus.emit("service:unregister", registeredService, { id: serviceId });
|
|
135
234
|
return true;
|
|
136
235
|
}
|
|
137
236
|
|
|
138
|
-
getService<T extends Service>(
|
|
139
|
-
return this.services.get<T>(
|
|
237
|
+
getService<T extends Service>(identifier: ServiceIdentifier<T>): T | undefined {
|
|
238
|
+
return this.services.get<T>(identifier);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
getServiceOrThrow<T extends Service>(
|
|
242
|
+
identifier: ServiceIdentifier<T>,
|
|
243
|
+
errorMessage?: string,
|
|
244
|
+
): T {
|
|
245
|
+
return this.services.getOrThrow(identifier, errorMessage);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
hasService(identifier: ServiceIdentifier<Service>): boolean {
|
|
249
|
+
return this.services.has(identifier);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async dispose(): Promise<void> {
|
|
253
|
+
this.extensionManager.destroy();
|
|
254
|
+
|
|
255
|
+
const registrations = this.services.list().slice().reverse();
|
|
256
|
+
for (const item of registrations) {
|
|
257
|
+
const identifier = item.token ?? item.id;
|
|
258
|
+
await this.unregisterServiceAsync(identifier);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
this.services.clear();
|
|
140
262
|
}
|
|
141
263
|
|
|
142
264
|
// --- Contribution Management ---
|
|
@@ -158,4 +280,59 @@ export class Pooder {
|
|
|
158
280
|
getContributions<T>(pointId: string): Contribution<T>[] {
|
|
159
281
|
return this.contributions.get<T>(pointId);
|
|
160
282
|
}
|
|
283
|
+
|
|
284
|
+
private resolveServiceIdentifier<T extends Service>(
|
|
285
|
+
service: T,
|
|
286
|
+
identifier?: ServiceIdentifier<T>,
|
|
287
|
+
): ServiceIdentifier<T> {
|
|
288
|
+
return identifier ?? service.constructor.name;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
private resolveUnregisterIdentifier(
|
|
292
|
+
serviceOrIdentifier: Service | ServiceIdentifier<Service>,
|
|
293
|
+
id?: ServiceIdentifier<Service>,
|
|
294
|
+
): ServiceIdentifier<Service> {
|
|
295
|
+
if (
|
|
296
|
+
typeof serviceOrIdentifier === "string" ||
|
|
297
|
+
isServiceToken(serviceOrIdentifier)
|
|
298
|
+
) {
|
|
299
|
+
return serviceOrIdentifier;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return id ?? serviceOrIdentifier.constructor.name;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private getServiceLabel(identifier: ServiceIdentifier<Service>): string {
|
|
306
|
+
if (typeof identifier === "string") {
|
|
307
|
+
return identifier;
|
|
308
|
+
}
|
|
309
|
+
return identifier.name;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
private invokeServiceHook(
|
|
313
|
+
service: Service,
|
|
314
|
+
hook: "init" | "dispose",
|
|
315
|
+
): void | Promise<void> {
|
|
316
|
+
const handler = service[hook];
|
|
317
|
+
if (!handler) {
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
return handler.call(service, this.serviceContext);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
private async invokeServiceHookAsync(
|
|
324
|
+
service: Service,
|
|
325
|
+
hook: "init" | "dispose",
|
|
326
|
+
): Promise<void> {
|
|
327
|
+
await this.invokeServiceHook(service, hook);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private isPromiseLike(value: unknown): value is Promise<unknown> {
|
|
331
|
+
return (
|
|
332
|
+
typeof value === "object" &&
|
|
333
|
+
value !== null &&
|
|
334
|
+
"then" in value &&
|
|
335
|
+
typeof (value as { then?: unknown }).then === "function"
|
|
336
|
+
);
|
|
337
|
+
}
|
|
161
338
|
}
|
package/src/service.ts
CHANGED
|
@@ -1,25 +1,205 @@
|
|
|
1
|
+
import type EventBus from "./event";
|
|
2
|
+
|
|
1
3
|
export interface Service {
|
|
2
|
-
init?(): void
|
|
3
|
-
dispose?(): void
|
|
4
|
+
init?(context: ServiceContext): void | Promise<void>;
|
|
5
|
+
dispose?(context: ServiceContext): void | Promise<void>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface ServiceToken<T extends Service = Service> {
|
|
9
|
+
readonly kind: "service-token";
|
|
10
|
+
readonly key: symbol;
|
|
11
|
+
readonly name: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type ServiceIdentifier<T extends Service = Service> =
|
|
15
|
+
| string
|
|
16
|
+
| ServiceToken<T>;
|
|
17
|
+
|
|
18
|
+
export interface ServiceContext {
|
|
19
|
+
readonly eventBus: EventBus;
|
|
20
|
+
get<T extends Service>(identifier: ServiceIdentifier<T>): T | undefined;
|
|
21
|
+
getOrThrow<T extends Service>(
|
|
22
|
+
identifier: ServiceIdentifier<T>,
|
|
23
|
+
errorMessage?: string,
|
|
24
|
+
): T;
|
|
25
|
+
has(identifier: ServiceIdentifier<Service>): boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RegisterServiceOptions {
|
|
29
|
+
allowOverride?: boolean;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface RegisteredService<T extends Service = Service> {
|
|
33
|
+
readonly id: string;
|
|
34
|
+
readonly token?: ServiceToken<T>;
|
|
35
|
+
readonly service: T;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ServiceEntry<T extends Service = Service> {
|
|
39
|
+
token?: ServiceToken<T>;
|
|
40
|
+
name: string;
|
|
41
|
+
service: T;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface NormalizedIdentifier<T extends Service = Service> {
|
|
45
|
+
token?: ServiceToken<T>;
|
|
46
|
+
name: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createServiceToken<T extends Service = Service>(
|
|
50
|
+
name: string,
|
|
51
|
+
): ServiceToken<T> {
|
|
52
|
+
if (!name) {
|
|
53
|
+
throw new Error("Service token name is required.");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Object.freeze({
|
|
57
|
+
kind: "service-token" as const,
|
|
58
|
+
key: Symbol(name),
|
|
59
|
+
name,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function isServiceToken<T extends Service = Service>(
|
|
64
|
+
identifier: unknown,
|
|
65
|
+
): identifier is ServiceToken<T> {
|
|
66
|
+
return (
|
|
67
|
+
typeof identifier === "object" &&
|
|
68
|
+
identifier !== null &&
|
|
69
|
+
"kind" in identifier &&
|
|
70
|
+
(identifier as { kind?: unknown }).kind === "service-token"
|
|
71
|
+
);
|
|
4
72
|
}
|
|
5
73
|
|
|
6
74
|
export class ServiceRegistry {
|
|
7
|
-
private
|
|
75
|
+
private readonly servicesByName: Map<string, ServiceEntry> = new Map();
|
|
76
|
+
private readonly servicesByToken: Map<symbol, ServiceEntry> = new Map();
|
|
77
|
+
private readonly registrationOrder: ServiceEntry[] = [];
|
|
78
|
+
|
|
79
|
+
register<T extends Service>(
|
|
80
|
+
identifier: ServiceIdentifier<T>,
|
|
81
|
+
service: T,
|
|
82
|
+
options: RegisterServiceOptions = {},
|
|
83
|
+
): T {
|
|
84
|
+
const normalized = this.normalizeIdentifier(identifier);
|
|
85
|
+
const existing = this.findEntry(normalized);
|
|
86
|
+
|
|
87
|
+
if (existing && !options.allowOverride) {
|
|
88
|
+
throw new Error(`Service "${normalized.name}" is already registered.`);
|
|
89
|
+
}
|
|
90
|
+
if (existing) {
|
|
91
|
+
this.removeEntry(existing);
|
|
92
|
+
}
|
|
8
93
|
|
|
9
|
-
|
|
10
|
-
|
|
94
|
+
const entry: ServiceEntry<T> = {
|
|
95
|
+
token: normalized.token,
|
|
96
|
+
name: normalized.name,
|
|
97
|
+
service,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
this.servicesByName.set(entry.name, entry);
|
|
101
|
+
if (entry.token) {
|
|
102
|
+
this.servicesByToken.set(entry.token.key, entry);
|
|
103
|
+
}
|
|
104
|
+
this.registrationOrder.push(entry as ServiceEntry);
|
|
11
105
|
return service;
|
|
12
106
|
}
|
|
13
107
|
|
|
14
|
-
get<T extends Service>(
|
|
15
|
-
|
|
108
|
+
get<T extends Service>(identifier: ServiceIdentifier<T>): T | undefined;
|
|
109
|
+
get<T extends Service>(identifier: ServiceToken<T>): T | undefined;
|
|
110
|
+
get<T extends Service>(identifier: string): T | undefined;
|
|
111
|
+
get<T extends Service>(identifier: ServiceIdentifier<T>): T | undefined {
|
|
112
|
+
const normalized = this.normalizeIdentifier(identifier);
|
|
113
|
+
const entry = this.findEntry(normalized);
|
|
114
|
+
return entry?.service as T | undefined;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getOrThrow<T extends Service>(
|
|
118
|
+
identifier: ServiceIdentifier<T>,
|
|
119
|
+
errorMessage?: string,
|
|
120
|
+
): T;
|
|
121
|
+
getOrThrow<T extends Service>(
|
|
122
|
+
identifier: ServiceToken<T>,
|
|
123
|
+
errorMessage?: string,
|
|
124
|
+
): T;
|
|
125
|
+
getOrThrow<T extends Service>(identifier: string, errorMessage?: string): T;
|
|
126
|
+
getOrThrow<T extends Service>(
|
|
127
|
+
identifier: ServiceIdentifier<T>,
|
|
128
|
+
errorMessage?: string,
|
|
129
|
+
): T {
|
|
130
|
+
const service = this.get(identifier);
|
|
131
|
+
if (service) {
|
|
132
|
+
return service;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const normalized = this.normalizeIdentifier(identifier);
|
|
136
|
+
throw new Error(errorMessage ?? `Service "${normalized.name}" not found.`);
|
|
16
137
|
}
|
|
17
138
|
|
|
18
|
-
has(
|
|
19
|
-
|
|
139
|
+
has(identifier: ServiceIdentifier<Service>): boolean {
|
|
140
|
+
const normalized = this.normalizeIdentifier(identifier);
|
|
141
|
+
return Boolean(this.findEntry(normalized));
|
|
20
142
|
}
|
|
21
143
|
|
|
22
|
-
delete(
|
|
23
|
-
this.
|
|
144
|
+
delete(identifier: ServiceIdentifier<Service>): boolean {
|
|
145
|
+
const normalized = this.normalizeIdentifier(identifier);
|
|
146
|
+
const entry = this.findEntry(normalized);
|
|
147
|
+
if (!entry) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
this.removeEntry(entry);
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
list(): RegisteredService[] {
|
|
155
|
+
return this.registrationOrder.map((entry) => ({
|
|
156
|
+
id: entry.name,
|
|
157
|
+
token: entry.token,
|
|
158
|
+
service: entry.service,
|
|
159
|
+
}));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
clear() {
|
|
163
|
+
this.servicesByName.clear();
|
|
164
|
+
this.servicesByToken.clear();
|
|
165
|
+
this.registrationOrder.length = 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private findEntry(
|
|
169
|
+
identifier: NormalizedIdentifier,
|
|
170
|
+
): ServiceEntry | undefined {
|
|
171
|
+
if (identifier.token) {
|
|
172
|
+
return (
|
|
173
|
+
this.servicesByToken.get(identifier.token.key) ??
|
|
174
|
+
this.servicesByName.get(identifier.name)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
return this.servicesByName.get(identifier.name);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private normalizeIdentifier<T extends Service>(
|
|
181
|
+
identifier: ServiceIdentifier<T>,
|
|
182
|
+
): NormalizedIdentifier<T> {
|
|
183
|
+
if (isServiceToken(identifier)) {
|
|
184
|
+
return { token: identifier, name: identifier.name };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const name = identifier.trim();
|
|
188
|
+
if (!name) {
|
|
189
|
+
throw new Error("Service identifier must be a non-empty string.");
|
|
190
|
+
}
|
|
191
|
+
return { name };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private removeEntry(entry: ServiceEntry) {
|
|
195
|
+
this.servicesByName.delete(entry.name);
|
|
196
|
+
if (entry.token) {
|
|
197
|
+
this.servicesByToken.delete(entry.token.key);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const index = this.registrationOrder.lastIndexOf(entry);
|
|
201
|
+
if (index >= 0) {
|
|
202
|
+
this.registrationOrder.splice(index, 1);
|
|
203
|
+
}
|
|
24
204
|
}
|
|
25
205
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { ToolContribution } from "../contribution";
|
|
2
2
|
import Disposable from "../disposable";
|
|
3
|
-
import { Service } from "../service";
|
|
3
|
+
import { Service, ServiceContext } from "../service";
|
|
4
4
|
import CommandService from "./CommandService";
|
|
5
5
|
import ToolRegistryService from "./ToolRegistryService";
|
|
6
|
+
import { COMMAND_SERVICE, TOOL_REGISTRY_SERVICE } from "./tokens";
|
|
6
7
|
|
|
7
8
|
export type ToolSessionStatus = "idle" | "active";
|
|
8
9
|
|
|
@@ -21,11 +22,33 @@ export interface LeaveResult {
|
|
|
21
22
|
reason?: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
interface ToolSessionServiceDependencies {
|
|
26
|
+
commandService?: CommandService;
|
|
27
|
+
toolRegistry?: ToolRegistryService;
|
|
28
|
+
}
|
|
29
|
+
|
|
24
30
|
export default class ToolSessionService implements Service {
|
|
25
31
|
private readonly sessions = new Map<string, ToolSessionState>();
|
|
26
32
|
private commandService?: CommandService;
|
|
27
33
|
private toolRegistry?: ToolRegistryService;
|
|
28
34
|
|
|
35
|
+
constructor(dependencies: ToolSessionServiceDependencies = {}) {
|
|
36
|
+
this.commandService = dependencies.commandService;
|
|
37
|
+
this.toolRegistry = dependencies.toolRegistry;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
init(context: ServiceContext) {
|
|
41
|
+
this.commandService ??= context.get(COMMAND_SERVICE);
|
|
42
|
+
this.toolRegistry ??= context.get(TOOL_REGISTRY_SERVICE);
|
|
43
|
+
|
|
44
|
+
if (!this.commandService) {
|
|
45
|
+
throw new Error("ToolSessionService requires CommandService.");
|
|
46
|
+
}
|
|
47
|
+
if (!this.toolRegistry) {
|
|
48
|
+
throw new Error("ToolSessionService requires ToolRegistryService.");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
29
52
|
setCommandService(commandService: CommandService) {
|
|
30
53
|
this.commandService = commandService;
|
|
31
54
|
}
|
|
@@ -84,12 +107,26 @@ export default class ToolSessionService implements Service {
|
|
|
84
107
|
}
|
|
85
108
|
|
|
86
109
|
private resolveTool(toolId: string): ToolContribution | undefined {
|
|
87
|
-
return this.
|
|
110
|
+
return this.getToolRegistry().getTool(toolId);
|
|
88
111
|
}
|
|
89
112
|
|
|
90
113
|
private async runCommand(commandId: string | undefined, ...args: any[]) {
|
|
91
|
-
if (!commandId
|
|
92
|
-
return await this.
|
|
114
|
+
if (!commandId) return undefined;
|
|
115
|
+
return await this.getCommandService().executeCommand(commandId, ...args);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private getCommandService(): CommandService {
|
|
119
|
+
if (!this.commandService) {
|
|
120
|
+
throw new Error("ToolSessionService is not initialized.");
|
|
121
|
+
}
|
|
122
|
+
return this.commandService;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private getToolRegistry(): ToolRegistryService {
|
|
126
|
+
if (!this.toolRegistry) {
|
|
127
|
+
throw new Error("ToolSessionService is not initialized.");
|
|
128
|
+
}
|
|
129
|
+
return this.toolRegistry;
|
|
93
130
|
}
|
|
94
131
|
|
|
95
132
|
async begin(toolId: string): Promise<void> {
|