@advicenxt/sbp-server 0.1.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.
Files changed (63) hide show
  1. package/benchmarks/bench.ts +272 -0
  2. package/dist/auth.d.ts +20 -0
  3. package/dist/auth.d.ts.map +1 -0
  4. package/dist/auth.js +69 -0
  5. package/dist/auth.js.map +1 -0
  6. package/dist/blackboard.d.ts +84 -0
  7. package/dist/blackboard.d.ts.map +1 -0
  8. package/dist/blackboard.js +502 -0
  9. package/dist/blackboard.js.map +1 -0
  10. package/dist/cli.d.ts +7 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +102 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/conditions.d.ts +27 -0
  15. package/dist/conditions.d.ts.map +1 -0
  16. package/dist/conditions.js +240 -0
  17. package/dist/conditions.js.map +1 -0
  18. package/dist/decay.d.ts +21 -0
  19. package/dist/decay.d.ts.map +1 -0
  20. package/dist/decay.js +88 -0
  21. package/dist/decay.js.map +1 -0
  22. package/dist/index.d.ts +13 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +13 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/rate-limiter.d.ts +21 -0
  27. package/dist/rate-limiter.d.ts.map +1 -0
  28. package/dist/rate-limiter.js +75 -0
  29. package/dist/rate-limiter.js.map +1 -0
  30. package/dist/server.d.ts +63 -0
  31. package/dist/server.d.ts.map +1 -0
  32. package/dist/server.js +401 -0
  33. package/dist/server.js.map +1 -0
  34. package/dist/store.d.ts +54 -0
  35. package/dist/store.d.ts.map +1 -0
  36. package/dist/store.js +55 -0
  37. package/dist/store.js.map +1 -0
  38. package/dist/types.d.ts +247 -0
  39. package/dist/types.d.ts.map +1 -0
  40. package/dist/types.js +26 -0
  41. package/dist/types.js.map +1 -0
  42. package/dist/validation.d.ts +296 -0
  43. package/dist/validation.d.ts.map +1 -0
  44. package/dist/validation.js +205 -0
  45. package/dist/validation.js.map +1 -0
  46. package/eslint.config.js +26 -0
  47. package/package.json +66 -0
  48. package/src/auth.ts +89 -0
  49. package/src/blackboard.test.ts +287 -0
  50. package/src/blackboard.ts +651 -0
  51. package/src/cli.ts +116 -0
  52. package/src/conditions.ts +305 -0
  53. package/src/conformance.test.ts +686 -0
  54. package/src/decay.ts +103 -0
  55. package/src/index.ts +24 -0
  56. package/src/rate-limiter.ts +104 -0
  57. package/src/server.integration.test.ts +436 -0
  58. package/src/server.ts +500 -0
  59. package/src/store.ts +108 -0
  60. package/src/types.ts +314 -0
  61. package/src/validation.ts +251 -0
  62. package/tsconfig.eslint.json +5 -0
  63. package/tsconfig.json +20 -0
package/src/server.ts ADDED
@@ -0,0 +1,500 @@
1
+ /**
2
+ * SBP HTTP Server
3
+ * Streamable HTTP with SSE (following MCP transport patterns)
4
+ */
5
+
6
+ import Fastify, { FastifyInstance, FastifyRequest, FastifyReply } from "fastify";
7
+ import { Blackboard, BlackboardOptions } from "./blackboard.js";
8
+ import type {
9
+ JsonRpcRequest,
10
+ JsonRpcResponse,
11
+ EmitParams,
12
+ SniffParams,
13
+ RegisterScentParams,
14
+ DeregisterScentParams,
15
+ EvaporateParams,
16
+ InspectParams,
17
+ TriggerPayload,
18
+ } from "./types.js";
19
+ import { v7 as uuidv7 } from "uuid";
20
+ import { validateEnvelope, validateParams } from "./validation.js";
21
+ import { createAuthHook, type AuthOptions } from "./auth.js";
22
+ import { createRateLimitHook, type RateLimitOptions } from "./rate-limiter.js";
23
+
24
+ export interface ServerOptions extends BlackboardOptions {
25
+ /** HTTP port (default: 3000) */
26
+ port?: number;
27
+ /** Host to bind to (default: localhost) */
28
+ host?: string;
29
+ /** Enable CORS (default: true) */
30
+ cors?: boolean;
31
+ /** Request logging (default: false) */
32
+ logging?: boolean;
33
+ /** Authentication options */
34
+ auth?: AuthOptions;
35
+ /** Rate limiting options */
36
+ rateLimit?: RateLimitOptions;
37
+ }
38
+
39
+ interface SSEClient {
40
+ id: string;
41
+ sessionId: string;
42
+ reply: FastifyReply;
43
+ scents: Set<string>;
44
+ lastEventId: number;
45
+ }
46
+
47
+ export class SbpServer {
48
+ private app: FastifyInstance;
49
+ public readonly blackboard: Blackboard;
50
+ private options: Required<Omit<ServerOptions, "auth" | "rateLimit" | "store">> & {
51
+ auth?: AuthOptions;
52
+ rateLimit?: RateLimitOptions;
53
+ };
54
+ private sseClients = new Map<string, SSEClient>();
55
+ private sessions = new Map<string, { agentId: string; createdAt: number }>();
56
+ private eventCounter = 0;
57
+
58
+ constructor(options: ServerOptions = {}) {
59
+ this.options = {
60
+ port: options.port ?? 3000,
61
+ host: options.host ?? "localhost",
62
+ cors: options.cors ?? true,
63
+ logging: options.logging ?? false,
64
+ evaluationInterval: options.evaluationInterval ?? 100,
65
+ defaultDecay: options.defaultDecay ?? { type: "exponential", half_life_ms: 300000 },
66
+ defaultTtlFloor: options.defaultTtlFloor ?? 0.01,
67
+ maxPheromones: options.maxPheromones ?? 100000,
68
+ trackEmissionHistory: options.trackEmissionHistory ?? true,
69
+ emissionHistoryWindow: options.emissionHistoryWindow ?? 60000,
70
+ auth: options.auth,
71
+ rateLimit: options.rateLimit,
72
+ };
73
+
74
+ this.blackboard = new Blackboard(this.options);
75
+ this.app = Fastify({ logger: this.options.logging });
76
+
77
+ this.setupRoutes();
78
+ }
79
+
80
+ private setupRoutes(): void {
81
+ // Authentication hook
82
+ if (this.options.auth?.requireAuth) {
83
+ this.app.addHook("onRequest", createAuthHook(this.options.auth));
84
+ }
85
+
86
+ // Rate limiting hook
87
+ if (this.options.rateLimit) {
88
+ this.app.addHook("onRequest", createRateLimitHook(this.options.rateLimit));
89
+ }
90
+
91
+ // CORS
92
+ if (this.options.cors) {
93
+ this.app.addHook("onRequest", async (request, reply) => {
94
+ reply.header("Access-Control-Allow-Origin", "*");
95
+ reply.header("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS");
96
+ reply.header(
97
+ "Access-Control-Allow-Headers",
98
+ "Content-Type, Accept, Authorization, Sbp-Protocol-Version, Sbp-Session-Id, Sbp-Agent-Id, Last-Event-ID"
99
+ );
100
+
101
+ if (request.method === "OPTIONS") {
102
+ reply.status(204).send();
103
+ }
104
+ });
105
+ }
106
+
107
+ // Health check
108
+ this.app.get("/health", async () => {
109
+ const stats = this.blackboard.inspect({ include: ["stats"] });
110
+ return {
111
+ status: "ok",
112
+ version: "0.1.0",
113
+ transport: "streamable-http-sse",
114
+ ...stats.stats,
115
+ };
116
+ });
117
+
118
+ // Main SBP endpoint - POST for client->server messages
119
+ this.app.post("/sbp", async (request: FastifyRequest, reply: FastifyReply) => {
120
+ return this.handlePost(request, reply);
121
+ });
122
+
123
+ // Main SBP endpoint - GET for SSE stream (server->client)
124
+ this.app.get("/sbp", async (request: FastifyRequest, reply: FastifyReply) => {
125
+ return this.handleSSE(request, reply);
126
+ });
127
+
128
+ // Legacy JSON-RPC endpoint (for backwards compatibility)
129
+ this.app.post("/rpc", async (request: FastifyRequest, reply: FastifyReply) => {
130
+ const body = request.body as JsonRpcRequest;
131
+ const response = await this.handleRpc(body, request);
132
+ reply.header("Content-Type", "application/json");
133
+ return response;
134
+ });
135
+
136
+ // Convenience REST endpoints
137
+ this.app.post("/emit", async (request) => {
138
+ const params = request.body as EmitParams;
139
+ return this.blackboard.emit(params);
140
+ });
141
+
142
+ this.app.post("/sniff", async (request) => {
143
+ const params = request.body as SniffParams;
144
+ return this.blackboard.sniff(params);
145
+ });
146
+
147
+ this.app.post("/scents", async (request) => {
148
+ const params = request.body as RegisterScentParams;
149
+ return this.blackboard.registerScent(params);
150
+ });
151
+
152
+ this.app.delete("/scents/:scent_id", async (request) => {
153
+ const { scent_id } = request.params as { scent_id: string };
154
+ return this.blackboard.deregisterScent({ scent_id });
155
+ });
156
+
157
+ this.app.get("/inspect", async (request) => {
158
+ const query = request.query as { include?: string };
159
+ const include = query.include?.split(",") as InspectParams["include"];
160
+ return this.blackboard.inspect({ include });
161
+ });
162
+ }
163
+
164
+ /**
165
+ * Handle POST requests (client -> server messages)
166
+ */
167
+ private async handlePost(request: FastifyRequest, reply: FastifyReply): Promise<unknown> {
168
+ // Validate JSON-RPC envelope
169
+ const envelopeResult = validateEnvelope(request.body);
170
+ if (!envelopeResult.ok) {
171
+ reply.header("Content-Type", "application/json");
172
+ return {
173
+ jsonrpc: "2.0",
174
+ id: null,
175
+ error: envelopeResult.error,
176
+ };
177
+ }
178
+
179
+ const body = envelopeResult.request;
180
+
181
+ // Validate method-specific params
182
+ const paramsResult = validateParams(body.method, body.params);
183
+ if (!paramsResult.ok) {
184
+ reply.header("Content-Type", "application/json");
185
+ return {
186
+ jsonrpc: "2.0",
187
+ id: body.id,
188
+ error: paramsResult.error,
189
+ };
190
+ }
191
+
192
+ // Replace params with validated (parsed) params
193
+ const validatedRequest: JsonRpcRequest = {
194
+ ...body,
195
+ params: paramsResult.params,
196
+ };
197
+
198
+ // Get or create session
199
+ let sessionId = request.headers["sbp-session-id"] as string | undefined;
200
+ if (!sessionId) {
201
+ sessionId = uuidv7();
202
+ this.sessions.set(sessionId, {
203
+ agentId: (request.headers["sbp-agent-id"] as string) || "unknown",
204
+ createdAt: Date.now(),
205
+ });
206
+ }
207
+
208
+ // Handle JSON-RPC request
209
+ const response = await this.handleRpc(validatedRequest, request);
210
+
211
+ // Set session header
212
+ reply.header("Sbp-Session-Id", sessionId);
213
+ reply.header("Content-Type", "application/json");
214
+
215
+ return response;
216
+ }
217
+
218
+ /**
219
+ * Handle GET requests - open SSE stream for triggers
220
+ */
221
+ private async handleSSE(request: FastifyRequest, reply: FastifyReply): Promise<void> {
222
+ const accept = request.headers.accept || "";
223
+
224
+ if (!accept.includes("text/event-stream")) {
225
+ reply.status(406).send({ error: "Accept header must include text/event-stream" });
226
+ return;
227
+ }
228
+
229
+ const sessionId = (request.headers["sbp-session-id"] as string) || uuidv7();
230
+ const lastEventId = request.headers["last-event-id"] as string | undefined;
231
+ const clientId = uuidv7();
232
+
233
+ // Set up SSE headers
234
+ reply.raw.writeHead(200, {
235
+ "Content-Type": "text/event-stream",
236
+ "Cache-Control": "no-cache",
237
+ Connection: "keep-alive",
238
+ "Sbp-Session-Id": sessionId,
239
+ "Access-Control-Allow-Origin": "*",
240
+ });
241
+
242
+ // Register SSE client
243
+ const client: SSEClient = {
244
+ id: clientId,
245
+ sessionId,
246
+ reply,
247
+ scents: new Set(),
248
+ lastEventId: lastEventId ? parseInt(lastEventId, 10) : 0,
249
+ };
250
+ this.sseClients.set(clientId, client);
251
+
252
+ // Send initial connection event
253
+ this.sendSSEEvent(client, "connected", { client_id: clientId, session_id: sessionId });
254
+
255
+ // Handle client disconnect
256
+ request.raw.on("close", () => {
257
+ this.sseClients.delete(clientId);
258
+ // Unregister scent handlers for this client
259
+ for (const scentId of client.scents) {
260
+ this.blackboard.offTrigger(scentId);
261
+ }
262
+ });
263
+
264
+ // Keep connection alive with periodic comments
265
+ const keepAlive = setInterval(() => {
266
+ if (reply.raw.writable) {
267
+ reply.raw.write(": keepalive\n\n");
268
+ } else {
269
+ clearInterval(keepAlive);
270
+ }
271
+ }, 30000);
272
+
273
+ request.raw.on("close", () => clearInterval(keepAlive));
274
+ }
275
+
276
+ /**
277
+ * Send an SSE event to a client
278
+ */
279
+ private sendSSEEvent(client: SSEClient, event: string, data: unknown): void {
280
+ if (!client.reply.raw.writable) return;
281
+
282
+ const eventId = ++this.eventCounter;
283
+ const payload = JSON.stringify(data);
284
+
285
+ client.reply.raw.write(`event: ${event}\n`);
286
+ client.reply.raw.write(`id: ${eventId}\n`);
287
+ client.reply.raw.write(`data: ${payload}\n\n`);
288
+
289
+ client.lastEventId = eventId;
290
+ }
291
+
292
+ /**
293
+ * Send a JSON-RPC notification via SSE
294
+ */
295
+ private sendSSENotification(client: SSEClient, method: string, params: unknown): void {
296
+ const message = {
297
+ jsonrpc: "2.0",
298
+ method,
299
+ params,
300
+ };
301
+ this.sendSSEEvent(client, "message", message);
302
+ }
303
+
304
+ /**
305
+ * Handle JSON-RPC requests
306
+ */
307
+ private async handleRpc(request: JsonRpcRequest, httpRequest: FastifyRequest): Promise<JsonRpcResponse> {
308
+ const { id, method, params } = request;
309
+ const sessionId = httpRequest.headers["sbp-session-id"] as string | undefined;
310
+
311
+ try {
312
+ let result: unknown;
313
+
314
+ switch (method) {
315
+ case "sbp/emit":
316
+ result = this.blackboard.emit(params as EmitParams);
317
+ break;
318
+
319
+ case "sbp/sniff":
320
+ result = this.blackboard.sniff(params as SniffParams);
321
+ break;
322
+
323
+ case "sbp/register_scent": {
324
+ const scentParams = params as RegisterScentParams;
325
+ result = this.blackboard.registerScent(scentParams);
326
+
327
+ // Set up trigger forwarding
328
+ if (sessionId) {
329
+ this.setupSSETrigger(scentParams.scent_id, sessionId);
330
+ }
331
+
332
+ // If agent_endpoint is provided, set up webhook delivery
333
+ if (scentParams.agent_endpoint) {
334
+ this.setupWebhookTrigger(scentParams.scent_id, scentParams.agent_endpoint);
335
+ }
336
+ break;
337
+ }
338
+
339
+ case "sbp/deregister_scent":
340
+ result = this.blackboard.deregisterScent(params as DeregisterScentParams);
341
+ break;
342
+
343
+ case "sbp/evaporate":
344
+ result = this.blackboard.evaporate(params as EvaporateParams);
345
+ break;
346
+
347
+ case "sbp/inspect":
348
+ result = this.blackboard.inspect(params as InspectParams);
349
+ break;
350
+
351
+ case "sbp/subscribe": {
352
+ // Subscribe to scent triggers (used after SSE stream is open)
353
+ const { scent_id } = params as { scent_id: string };
354
+ if (sessionId) {
355
+ this.setupSSETrigger(scent_id, sessionId);
356
+ // Mark scent subscription for all clients in this session
357
+ for (const client of this.sseClients.values()) {
358
+ if (client.sessionId === sessionId) {
359
+ client.scents.add(scent_id);
360
+ }
361
+ }
362
+ }
363
+ result = { subscribed: scent_id };
364
+ break;
365
+ }
366
+
367
+ case "sbp/unsubscribe": {
368
+ const { scent_id } = params as { scent_id: string };
369
+ this.blackboard.offTrigger(scent_id);
370
+ // Remove from client scent sets
371
+ for (const client of this.sseClients.values()) {
372
+ if (client.sessionId === sessionId) {
373
+ client.scents.delete(scent_id);
374
+ }
375
+ }
376
+ result = { unsubscribed: scent_id };
377
+ break;
378
+ }
379
+
380
+ default:
381
+ return {
382
+ jsonrpc: "2.0",
383
+ id,
384
+ error: {
385
+ code: -32601,
386
+ message: "Method not found",
387
+ data: { method },
388
+ },
389
+ };
390
+ }
391
+
392
+ return {
393
+ jsonrpc: "2.0",
394
+ id,
395
+ result,
396
+ };
397
+ } catch (err) {
398
+ const error = err as Error;
399
+ return {
400
+ jsonrpc: "2.0",
401
+ id,
402
+ error: {
403
+ code: -32603,
404
+ message: error.message,
405
+ },
406
+ };
407
+ }
408
+ }
409
+
410
+ /**
411
+ * Set up trigger forwarding to SSE clients
412
+ */
413
+ private setupSSETrigger(scentId: string, sessionId: string): void {
414
+ this.blackboard.onTrigger(scentId, async (payload: TriggerPayload) => {
415
+ // Send to all SSE clients in this session
416
+ for (const client of this.sseClients.values()) {
417
+ if (client.sessionId === sessionId || client.scents.has(scentId)) {
418
+ this.sendSSENotification(client, "sbp/trigger", payload);
419
+ }
420
+ }
421
+ });
422
+ }
423
+
424
+ /**
425
+ * Set up webhook trigger delivery to an agent endpoint
426
+ */
427
+ private setupWebhookTrigger(scentId: string, endpoint: string): void {
428
+ this.blackboard.onTrigger(scentId, async (payload: TriggerPayload) => {
429
+ // Attempt webhook delivery with one retry
430
+ for (let attempt = 0; attempt < 2; attempt++) {
431
+ try {
432
+ const response = await fetch(endpoint, {
433
+ method: "POST",
434
+ headers: {
435
+ "Content-Type": "application/json",
436
+ "Sbp-Protocol-Version": "0.1",
437
+ },
438
+ body: JSON.stringify({
439
+ jsonrpc: "2.0",
440
+ method: "sbp/trigger",
441
+ params: payload,
442
+ }),
443
+ signal: AbortSignal.timeout(10000),
444
+ });
445
+
446
+ if (response.ok) return; // Success
447
+
448
+ // Retry on 5xx
449
+ if (response.status >= 500 && attempt === 0) {
450
+ await new Promise((r) => setTimeout(r, 1000));
451
+ continue;
452
+ }
453
+ break; // Don't retry on 4xx
454
+ } catch {
455
+ // Network error — retry once
456
+ if (attempt === 0) {
457
+ await new Promise((r) => setTimeout(r, 1000));
458
+ }
459
+ }
460
+ }
461
+ });
462
+ }
463
+
464
+ async start(): Promise<void> {
465
+ // Start blackboard evaluation loop
466
+ this.blackboard.start();
467
+
468
+ // Start HTTP server
469
+ await this.app.listen({ port: this.options.port, host: this.options.host });
470
+
471
+ console.log(`[SBP] Server listening on http://${this.options.host}:${this.options.port}`);
472
+ console.log(`[SBP] Streamable HTTP endpoint: POST/GET ${this.address}/sbp`);
473
+ console.log(`[SBP] Transport: SSE (Server-Sent Events)`);
474
+ if (this.options.auth?.requireAuth) {
475
+ console.log(`[SBP] Authentication: API key required`);
476
+ }
477
+ if (this.options.rateLimit) {
478
+ console.log(`[SBP] Rate limiting: ${this.options.rateLimit.maxRequests ?? 1000} req/${(this.options.rateLimit.windowMs ?? 60000) / 1000}s`);
479
+ }
480
+ }
481
+
482
+ async stop(): Promise<void> {
483
+ this.blackboard.stop();
484
+
485
+ // Close all SSE connections
486
+ for (const client of this.sseClients.values()) {
487
+ if (client.reply.raw.writable) {
488
+ client.reply.raw.end();
489
+ }
490
+ }
491
+ this.sseClients.clear();
492
+
493
+ await this.app.close();
494
+ console.log("[SBP] Server stopped");
495
+ }
496
+
497
+ get address(): string {
498
+ return `http://${this.options.host}:${this.options.port}`;
499
+ }
500
+ }
package/src/store.ts ADDED
@@ -0,0 +1,108 @@
1
+ /**
2
+ * SBP Pheromone Store - Persistence Adapter Interface
3
+ *
4
+ * Abstraction layer for pluggable storage backends.
5
+ * Default implementation: MemoryStore (in-process Map).
6
+ */
7
+
8
+ import type { Pheromone } from "./types.js";
9
+
10
+ // ============================================================================
11
+ // STORE INTERFACE
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Abstract storage interface for pheromones.
16
+ * Implementations MUST be synchronous-safe for the core Blackboard operations.
17
+ * Async-capable stores (Redis, SQLite) should pre-load into a local cache.
18
+ */
19
+ export interface PheromoneStore {
20
+ /** Get a pheromone by ID. Returns undefined if not found. */
21
+ get(id: string): Pheromone | undefined;
22
+
23
+ /** Store or update a pheromone. */
24
+ set(id: string, pheromone: Pheromone): void;
25
+
26
+ /** Delete a pheromone by ID. Returns true if existed. */
27
+ delete(id: string): boolean;
28
+
29
+ /** Check if a pheromone exists. */
30
+ has(id: string): boolean;
31
+
32
+ /** Iterate over all stored pheromones. */
33
+ values(): IterableIterator<Pheromone>;
34
+
35
+ /** Iterate over all [id, pheromone] pairs. */
36
+ entries(): IterableIterator<[string, Pheromone]>;
37
+
38
+ /** Number of stored pheromones. */
39
+ readonly size: number;
40
+
41
+ /** Remove all pheromones. */
42
+ clear(): void;
43
+ }
44
+
45
+ // ============================================================================
46
+ // MEMORY STORE
47
+ // ============================================================================
48
+
49
+ /**
50
+ * In-memory pheromone store backed by a Map.
51
+ * This is the default store and provides the fastest access.
52
+ * Data is lost on process restart — acceptable for ephemeral signals.
53
+ */
54
+ export class MemoryStore implements PheromoneStore {
55
+ private data = new Map<string, Pheromone>();
56
+
57
+ get(id: string): Pheromone | undefined {
58
+ return this.data.get(id);
59
+ }
60
+
61
+ set(id: string, pheromone: Pheromone): void {
62
+ this.data.set(id, pheromone);
63
+ }
64
+
65
+ delete(id: string): boolean {
66
+ return this.data.delete(id);
67
+ }
68
+
69
+ has(id: string): boolean {
70
+ return this.data.has(id);
71
+ }
72
+
73
+ values(): IterableIterator<Pheromone> {
74
+ return this.data.values();
75
+ }
76
+
77
+ entries(): IterableIterator<[string, Pheromone]> {
78
+ return this.data.entries();
79
+ }
80
+
81
+ get size(): number {
82
+ return this.data.size;
83
+ }
84
+
85
+ clear(): void {
86
+ this.data.clear();
87
+ }
88
+ }
89
+
90
+ // ============================================================================
91
+ // FACTORY
92
+ // ============================================================================
93
+
94
+ export type StoreType = "memory";
95
+
96
+ /**
97
+ * Create a pheromone store of the specified type.
98
+ * Currently only "memory" is built-in. Additional stores (Redis, SQLite)
99
+ * can be added by implementing the PheromoneStore interface.
100
+ */
101
+ export function createStore(type: StoreType = "memory"): PheromoneStore {
102
+ switch (type) {
103
+ case "memory":
104
+ return new MemoryStore();
105
+ default:
106
+ throw new Error(`Unknown store type: ${type}`);
107
+ }
108
+ }