@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pooder/core",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "Core logic for Pooder editor",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
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>(serviceName: string): T | undefined;
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
- "ConfigurationService",
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>("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
- this.context.services.get<ToolRegistryService>("ToolRegistryService");
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 { Service, ServiceRegistry } from "./service";
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, "CommandService");
57
+ this.registerService(commandService, CORE_SERVICE_TOKENS.COMMAND);
40
58
 
41
59
  const configurationService = new ConfigurationService();
42
- this.registerService(configurationService, "ConfigurationService");
60
+ this.registerService(configurationService, CORE_SERVICE_TOKENS.CONFIGURATION);
43
61
 
44
62
  const toolRegistryService = new ToolRegistryService();
45
- this.registerService(toolRegistryService, "ToolRegistryService");
63
+ this.registerService(toolRegistryService, CORE_SERVICE_TOKENS.TOOL_REGISTRY);
46
64
 
47
- const toolSessionService = new ToolSessionService();
48
- toolSessionService.setCommandService(commandService);
49
- toolSessionService.setToolRegistry(toolRegistryService);
50
- this.registerService(toolSessionService, "ToolSessionService");
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
- workbenchService.setEventBus(this.eventBus);
54
- workbenchService.setToolRegistry(toolRegistryService);
55
- workbenchService.setToolSessionService(toolSessionService);
56
- this.registerService(workbenchService, "WorkbenchService");
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>(serviceName: string) =>
63
- this.services.get<T>(serviceName),
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(service: Service, id?: string) {
105
- const serviceId = id || service.constructor.name;
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?.init?.();
109
- } catch (e) {
110
- console.error(`Error initializing service ${serviceId}:`, e);
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
- this.services.register(serviceId, service);
115
- this.eventBus.emit("service:register", service);
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
- unregisterService(service: Service, id?: string) {
120
- const serviceId = id || service.constructor.name;
121
- if (!this.services.has(serviceId)) {
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
- service?.dispose?.();
128
- } catch (e) {
129
- console.error(`Error disposing service ${serviceId}:`, e);
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(serviceId);
134
- this.eventBus.emit("service:unregister", service);
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>(id: string): T | undefined {
139
- return this.services.get<T>(id);
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 services: Map<string, Service> = new Map();
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
- register<T extends Service>(name: string, service: T): T {
10
- this.services.set(name, service);
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>(serviceName: string): T | undefined {
15
- return this.services.get(serviceName) as T | undefined;
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(serviceName: string): boolean {
19
- return this.services.has(serviceName);
139
+ has(identifier: ServiceIdentifier<Service>): boolean {
140
+ const normalized = this.normalizeIdentifier(identifier);
141
+ return Boolean(this.findEntry(normalized));
20
142
  }
21
143
 
22
- delete(serviceName: string): void {
23
- this.services.delete(serviceName);
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.toolRegistry?.getTool(toolId);
110
+ return this.getToolRegistry().getTool(toolId);
88
111
  }
89
112
 
90
113
  private async runCommand(commandId: string | undefined, ...args: any[]) {
91
- if (!commandId || !this.commandService) return undefined;
92
- return await this.commandService.executeCommand(commandId, ...args);
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> {