@ai.ntellect/core 0.6.16 → 0.6.19

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 (83) hide show
  1. package/.mocharc.json +1 -2
  2. package/README.md +123 -178
  3. package/dist/graph/controller.js +29 -6
  4. package/dist/graph/index.js +402 -0
  5. package/dist/index.js +22 -7
  6. package/dist/interfaces/index.js +15 -0
  7. package/dist/modules/agenda/adapters/node-cron/index.js +29 -0
  8. package/dist/modules/agenda/index.js +140 -0
  9. package/dist/{services/embedding.js → modules/embedding/adapters/ai/index.js} +24 -7
  10. package/dist/modules/embedding/index.js +59 -0
  11. package/dist/modules/memory/adapters/in-memory/index.js +210 -0
  12. package/dist/{memory → modules/memory}/adapters/meilisearch/index.js +97 -2
  13. package/dist/{memory → modules/memory}/adapters/redis/index.js +77 -15
  14. package/dist/modules/memory/index.js +103 -0
  15. package/dist/utils/{stringifiy-zod-schema.js → generate-action-schema.js} +5 -5
  16. package/graph/controller.ts +38 -14
  17. package/graph/index.ts +468 -0
  18. package/index.ts +25 -7
  19. package/interfaces/index.ts +346 -28
  20. package/modules/agenda/adapters/node-cron/index.ts +25 -0
  21. package/modules/agenda/index.ts +159 -0
  22. package/modules/embedding/adapters/ai/index.ts +42 -0
  23. package/modules/embedding/index.ts +45 -0
  24. package/modules/memory/adapters/in-memory/index.ts +203 -0
  25. package/{memory → modules/memory}/adapters/meilisearch/index.ts +114 -8
  26. package/modules/memory/adapters/redis/index.ts +164 -0
  27. package/modules/memory/index.ts +93 -0
  28. package/package.json +4 -4
  29. package/test/graph/index.test.ts +646 -0
  30. package/test/modules/agenda/node-cron.test.ts +286 -0
  31. package/test/modules/embedding/ai.test.ts +78 -0
  32. package/test/modules/memory/adapters/in-memory.test.ts +153 -0
  33. package/test/{memory → modules/memory}/adapters/meilisearch.test.ts +80 -94
  34. package/test/modules/memory/adapters/redis.test.ts +169 -0
  35. package/test/modules/memory/base.test.ts +230 -0
  36. package/test/services/agenda.test.ts +279 -280
  37. package/tsconfig.json +0 -3
  38. package/types/index.ts +82 -203
  39. package/utils/{stringifiy-zod-schema.ts → generate-action-schema.ts} +3 -3
  40. package/app/README.md +0 -36
  41. package/app/app/favicon.ico +0 -0
  42. package/app/app/globals.css +0 -21
  43. package/app/app/gun.ts +0 -0
  44. package/app/app/layout.tsx +0 -18
  45. package/app/app/page.tsx +0 -321
  46. package/app/eslint.config.mjs +0 -16
  47. package/app/next.config.ts +0 -7
  48. package/app/package-lock.json +0 -5912
  49. package/app/package.json +0 -31
  50. package/app/pnpm-lock.yaml +0 -4031
  51. package/app/postcss.config.mjs +0 -8
  52. package/app/public/file.svg +0 -1
  53. package/app/public/globe.svg +0 -1
  54. package/app/public/next.svg +0 -1
  55. package/app/public/vercel.svg +0 -1
  56. package/app/public/window.svg +0 -1
  57. package/app/tailwind.config.ts +0 -18
  58. package/app/tsconfig.json +0 -27
  59. package/dist/graph/graph.js +0 -162
  60. package/dist/memory/index.js +0 -9
  61. package/dist/services/agenda.js +0 -115
  62. package/dist/services/queue.js +0 -142
  63. package/dist/utils/experimental-graph-rag.js +0 -152
  64. package/dist/utils/generate-object.js +0 -111
  65. package/dist/utils/inject-actions.js +0 -16
  66. package/dist/utils/queue-item-transformer.js +0 -24
  67. package/dist/utils/sanitize-results.js +0 -60
  68. package/graph/graph.ts +0 -193
  69. package/memory/adapters/redis/index.ts +0 -103
  70. package/memory/index.ts +0 -22
  71. package/services/agenda.ts +0 -118
  72. package/services/embedding.ts +0 -26
  73. package/services/queue.ts +0 -145
  74. package/test/.env.test +0 -4
  75. package/test/graph/engine.test.ts +0 -533
  76. package/test/memory/adapters/redis.test.ts +0 -160
  77. package/test/memory/base.test.ts +0 -229
  78. package/test/services/queue.test.ts +0 -286
  79. package/utils/experimental-graph-rag.ts +0 -170
  80. package/utils/generate-object.ts +0 -117
  81. package/utils/inject-actions.ts +0 -19
  82. package/utils/queue-item-transformer.ts +0 -38
  83. package/utils/sanitize-results.ts +0 -66
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.Memory = void 0;
13
+ const interfaces_1 = require("../../interfaces");
14
+ /**
15
+ * @module Memory
16
+ * @description A module for managing memory storage and retrieval operations.
17
+ * Implements the BaseMemory abstract class and provides concrete implementations
18
+ * for memory-related operations using the provided adapter.
19
+ * @extends {BaseMemory}
20
+ */
21
+ class Memory extends interfaces_1.BaseMemory {
22
+ /**
23
+ * Creates an instance of Memory
24
+ * @param {IMemoryAdapter} adapter - The memory adapter implementation to use
25
+ */
26
+ constructor(adapter) {
27
+ super(adapter);
28
+ }
29
+ /**
30
+ * Initializes the memory module with default room
31
+ * @returns {Promise<void>}
32
+ */
33
+ init() {
34
+ return __awaiter(this, void 0, void 0, function* () {
35
+ yield this.adapter.init("default");
36
+ });
37
+ }
38
+ /**
39
+ * Creates a new memory entry
40
+ * @param {CreateMemoryInput & { embedding?: number[] }} input - Memory data with optional embedding
41
+ * @returns {Promise<BaseMemoryType | undefined>} Created memory or undefined
42
+ */
43
+ createMemory(input) {
44
+ return __awaiter(this, void 0, void 0, function* () {
45
+ return this.adapter.createMemory(input);
46
+ });
47
+ }
48
+ /**
49
+ * Retrieves a memory by ID and room ID
50
+ * @param {string} id - Memory identifier
51
+ * @param {string} roomId - Room identifier
52
+ * @returns {Promise<BaseMemoryType | null>} Memory entry or null if not found
53
+ */
54
+ getMemoryById(id, roomId) {
55
+ return __awaiter(this, void 0, void 0, function* () {
56
+ return this.adapter.getMemoryById(id, roomId);
57
+ });
58
+ }
59
+ /**
60
+ * Searches for memories based on query and options
61
+ * @param {string} query - Search query
62
+ * @param {Object} options - Search options
63
+ * @param {string} options.roomId - Room identifier
64
+ * @param {number} [options.limit] - Maximum number of results to return
65
+ * @returns {Promise<BaseMemoryType[]>} Array of matching memories
66
+ */
67
+ getMemoryByIndex(query, options) {
68
+ return __awaiter(this, void 0, void 0, function* () {
69
+ return this.adapter.getMemoryByIndex(query, options);
70
+ });
71
+ }
72
+ /**
73
+ * Retrieves all memories for a specific room
74
+ * @param {string} roomId - Room identifier
75
+ * @returns {Promise<BaseMemoryType[]>} Array of all memories in the room
76
+ */
77
+ getAllMemories(roomId) {
78
+ return __awaiter(this, void 0, void 0, function* () {
79
+ return this.adapter.getAllMemories(roomId);
80
+ });
81
+ }
82
+ /**
83
+ * Deletes a specific memory
84
+ * @param {string} id - Memory identifier
85
+ * @param {string} roomId - Room identifier
86
+ * @returns {Promise<void>}
87
+ */
88
+ clearMemoryById(id, roomId) {
89
+ return __awaiter(this, void 0, void 0, function* () {
90
+ yield this.adapter.clearMemoryById(id, roomId);
91
+ });
92
+ }
93
+ /**
94
+ * Clears all memories across all rooms
95
+ * @returns {Promise<void>}
96
+ */
97
+ clearAllMemories() {
98
+ return __awaiter(this, void 0, void 0, function* () {
99
+ yield this.adapter.clearAllMemories();
100
+ });
101
+ }
102
+ }
103
+ exports.Memory = Memory;
@@ -1,18 +1,18 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getSchemaString = exports.stringifyZodSchema = void 0;
3
+ exports.getSchemaString = exports.generateActionSchema = void 0;
4
4
  const zod_1 = require("zod");
5
- const stringifyZodSchema = (nodes) => {
5
+ const generateActionSchema = (nodes) => {
6
6
  return nodes
7
7
  .map((node) => {
8
- const schemaStr = node.parameters
9
- ? (0, exports.getSchemaString)(node.parameters)
8
+ const schemaStr = node.inputs
9
+ ? (0, exports.getSchemaString)(node.inputs)
10
10
  : "No parameters";
11
11
  return `Workflow: ${node.name}\nParameters: ${schemaStr}`;
12
12
  })
13
13
  .join("\n\n");
14
14
  };
15
- exports.stringifyZodSchema = stringifyZodSchema;
15
+ exports.generateActionSchema = generateActionSchema;
16
16
  const getSchemaString = (schema) => {
17
17
  if (schema instanceof zod_1.z.ZodObject) {
18
18
  const entries = Object.entries(schema.shape);
@@ -1,10 +1,22 @@
1
- import { GraphContext } from "@/types";
2
1
  import { ZodSchema } from "zod";
3
- import { Graph } from "./graph";
2
+ import { GraphContext } from "../types";
3
+ import { GraphFlow } from "./index";
4
4
 
5
+ /**
6
+ * Controller class for managing the execution of graph flows
7
+ * Handles both sequential and parallel execution of multiple graphs
8
+ */
5
9
  export class GraphController {
10
+ /**
11
+ * Executes multiple graphs sequentially
12
+ * @param graphs - Array of GraphFlow instances to execute
13
+ * @param startNodes - Array of starting node identifiers for each graph
14
+ * @param inputContexts - Optional array of initial contexts for each graph
15
+ * @returns Map containing results of each graph execution, keyed by graph name and index
16
+ * @template T - Zod schema type for graph context validation
17
+ */
6
18
  static async executeSequential<T extends ZodSchema>(
7
- graphs: Graph<T>[],
19
+ graphs: GraphFlow<T>[],
8
20
  startNodes: string[],
9
21
  inputContexts?: Partial<GraphContext<T>>[]
10
22
  ): Promise<Map<string, GraphContext<T>>> {
@@ -16,11 +28,21 @@ export class GraphController {
16
28
  return results;
17
29
  }
18
30
 
31
+ /**
32
+ * Executes multiple graphs in parallel with optional concurrency control
33
+ * @param graphs - Array of GraphFlow instances to execute
34
+ * @param startNodes - Array of starting node identifiers for each graph
35
+ * @param inputContexts - Optional array of initial contexts for each graph
36
+ * @param inputs - Optional array of additional inputs for each graph
37
+ * @param concurrencyLimit - Optional limit on number of concurrent graph executions
38
+ * @returns Map containing results of each graph execution, keyed by graph name
39
+ * @template T - Zod schema type for graph context validation
40
+ */
19
41
  static async executeParallel<T extends ZodSchema>(
20
- graphs: Graph<T>[],
42
+ graphs: GraphFlow<T>[],
21
43
  startNodes: string[],
22
44
  inputContexts?: Partial<GraphContext<T>>[],
23
- inputParams?: any[],
45
+ inputs?: any[],
24
46
  concurrencyLimit?: number
25
47
  ): Promise<Map<string, GraphContext<T>>> {
26
48
  const results = new Map<string, GraphContext<T>>();
@@ -29,20 +51,22 @@ export class GraphController {
29
51
  inputContexts = inputContexts.map((ctx) => ctx || {});
30
52
  }
31
53
 
32
- if (inputParams) {
33
- inputParams = inputParams.map((params) => params || {});
54
+ if (inputs) {
55
+ inputs = inputs.map((input) => input || {});
34
56
  }
35
57
 
36
58
  if (concurrencyLimit) {
37
59
  for (let i = 0; i < graphs.length; i += concurrencyLimit) {
38
60
  const batchResults = await Promise.all(
39
- graphs.slice(i, i + concurrencyLimit).map((graph, index) =>
40
- graph.execute(
41
- startNodes[i + index],
42
- inputContexts?.[i + index] || {},
43
- inputParams?.[i + index] || {} // ✅ Passe bien les paramètres
61
+ graphs
62
+ .slice(i, i + concurrencyLimit)
63
+ .map((graph, index) =>
64
+ graph.execute(
65
+ startNodes[i + index],
66
+ inputContexts?.[i + index] || {},
67
+ inputs?.[i + index]
68
+ )
44
69
  )
45
- )
46
70
  );
47
71
  batchResults.forEach((result, index) => {
48
72
  results.set(`${graphs[i + index].name}`, result);
@@ -54,7 +78,7 @@ export class GraphController {
54
78
  graph.execute(
55
79
  startNodes[index],
56
80
  inputContexts?.[index] || {},
57
- inputParams?.[index] || {}
81
+ inputs?.[index] || {}
58
82
  )
59
83
  )
60
84
  );
package/graph/index.ts ADDED
@@ -0,0 +1,468 @@
1
+ import { EventEmitter } from "events";
2
+ import { ZodSchema } from "zod";
3
+ import { IEventEmitter } from "../interfaces";
4
+ import { GraphContext, GraphDefinition, Node } from "../types";
5
+
6
+ /**
7
+ * @module GraphFlow
8
+ * @description A flexible workflow engine that manages the execution of nodes in a graph-like structure.
9
+ *
10
+ * Key features:
11
+ * - Multiple branches support
12
+ * - Conditional branching (runs first matching condition, or all if none have conditions)
13
+ * - Event-driven nodes
14
+ * - Zod validation of context/inputs/outputs
15
+ * - Automatic retry on node failures
16
+ *
17
+ * @template T - Extends ZodSchema for type validation
18
+ */
19
+ export class GraphFlow<T extends ZodSchema> {
20
+ private nodes: Map<string, Node<T>>;
21
+ private context: GraphContext<T>;
22
+ public validator?: T;
23
+ private eventEmitter: IEventEmitter;
24
+ private globalErrorHandler?: (error: Error, context: GraphContext<T>) => void;
25
+ private graphEvents?: string[];
26
+ private entryNode?: string;
27
+
28
+ /**
29
+ * Creates a new instance of GraphFlow
30
+ * @param {string} name - The name of the graph flow
31
+ * @param {GraphDefinition<T>} config - Configuration object containing nodes, schema, context, and error handlers
32
+ */
33
+ constructor(public name: string, config: GraphDefinition<T>) {
34
+ this.nodes = new Map(config.nodes.map((node) => [node.name, node]));
35
+ this.validator = config.schema;
36
+ this.context = config.schema.parse(config.context) as GraphContext<T>;
37
+ this.globalErrorHandler = config.onError;
38
+ this.eventEmitter = config.eventEmitter || new EventEmitter();
39
+ this.graphEvents = config.events;
40
+
41
+ this.setupEventListeners();
42
+ this.setupGraphEventListeners();
43
+ }
44
+
45
+ /**
46
+ * Creates a new context for execution
47
+ * @private
48
+ * @returns {GraphContext<T>} A cloned context to prevent pollution during parallel execution
49
+ */
50
+ private createNewContext(): GraphContext<T> {
51
+ return structuredClone(this.context);
52
+ }
53
+
54
+ /**
55
+ * Sets up event listeners for node-based events
56
+ * @private
57
+ * @description Attaches all node-based event triggers while preserving external listeners
58
+ */
59
+ private setupEventListeners(): void {
60
+ // First remove only the existing node-based listeners that we might have created previously
61
+ // We do NOT remove, for example, "nodeStarted" or "nodeCompleted" listeners that test code added.
62
+ for (const [eventName, listener] of this.eventEmitter
63
+ .rawListeners("*")
64
+ .entries()) {
65
+ // This can be tricky—EventEmitter doesn't directly let you remove by "type" of listener.
66
+ // Alternatively, we can store references in a separate structure.
67
+ // For simplicity, let's do a full removeAllListeners() on node-specified events (only),
68
+ // then re-add them below, but keep the test-based events like "nodeStarted" or "nodeCompleted".
69
+ }
70
+
71
+ // The simplest approach: removeAllListeners for each event that is declared as a node event
72
+ // so we don't stack up duplicates:
73
+ const allEvents = new Set<string>();
74
+ for (const node of this.nodes.values()) {
75
+ if (node.events) {
76
+ node.events.forEach((evt) => allEvents.add(evt));
77
+ }
78
+ }
79
+ for (const evt of allEvents) {
80
+ // remove only those events that are used by nodes
81
+ this.eventEmitter.removeAllListeners(evt);
82
+ }
83
+
84
+ // Now re-add the node-based event triggers
85
+ for (const node of this.nodes.values()) {
86
+ if (node.events && node.events.length > 0) {
87
+ node.events.forEach((event) => {
88
+ this.eventEmitter.on(
89
+ event,
90
+ async (data?: Partial<GraphContext<T>>) => {
91
+ const freshContext = this.createNewContext();
92
+ if (data) Object.assign(freshContext, data);
93
+
94
+ // If triggered by an event, we pass "true" so event-driven node will skip `next`.
95
+ await this.executeNode(
96
+ node.name,
97
+ freshContext,
98
+ undefined,
99
+ /* triggeredByEvent= */ true
100
+ );
101
+ }
102
+ );
103
+ });
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Executes a specific node in the graph
110
+ * @private
111
+ * @param {string} nodeName - Name of the node to execute
112
+ * @param {GraphContext<T>} context - Current execution context
113
+ * @param {any} inputs - Input parameters for the node
114
+ * @param {boolean} triggeredByEvent - Whether the execution was triggered by an event
115
+ * @returns {Promise<void>}
116
+ */
117
+ private async executeNode(
118
+ nodeName: string,
119
+ context: GraphContext<T>,
120
+ inputs?: any,
121
+ triggeredByEvent: boolean = false
122
+ ): Promise<void> {
123
+ const node = this.nodes.get(nodeName);
124
+ if (!node) throw new Error(`❌ Node "${nodeName}" not found.`);
125
+
126
+ if (node.condition && !node.condition(context)) {
127
+ return;
128
+ }
129
+
130
+ let attempts = 0;
131
+ const maxAttempts = node.retry?.maxAttempts || 1;
132
+ const delay = node.retry?.delay || 0;
133
+
134
+ while (attempts < maxAttempts) {
135
+ try {
136
+ let validatedInputs;
137
+ if (node.inputs) {
138
+ if (!inputs) {
139
+ throw new Error(
140
+ `❌ Inputs required for node "${nodeName}" but received: ${inputs}`
141
+ );
142
+ }
143
+ validatedInputs = node.inputs.parse(inputs);
144
+ }
145
+
146
+ this.eventEmitter.emit("nodeStarted", { name: nodeName, context });
147
+
148
+ // Execute the node
149
+ await node.execute(context, validatedInputs);
150
+
151
+ if (node.outputs) {
152
+ node.outputs.parse(context);
153
+ }
154
+
155
+ this.validateContext(context);
156
+ this.eventEmitter.emit("nodeCompleted", { name: nodeName, context });
157
+
158
+ // IMPORTANT: Si le nœud est déclenché par un événement et a des événements définis,
159
+ // on arrête ici et on ne suit pas la chaîne next
160
+ if (triggeredByEvent && node.events && node.events.length > 0) {
161
+ this.context = structuredClone(context);
162
+ return;
163
+ }
164
+
165
+ // Gérer les nœuds suivants
166
+ if (node.next && node.next.length > 0) {
167
+ const branchContexts: GraphContext<T>[] = [];
168
+
169
+ // Exécuter toutes les branches valides
170
+ for (const nextNodeName of node.next) {
171
+ const nextNode = this.nodes.get(nextNodeName);
172
+ if (!nextNode) continue;
173
+
174
+ const branchContext = structuredClone(context);
175
+
176
+ // Si le nœud a une condition et qu'elle n'est pas remplie, passer au suivant
177
+ if (nextNode.condition && !nextNode.condition(branchContext)) {
178
+ continue;
179
+ }
180
+
181
+ await this.executeNode(nextNodeName, branchContext);
182
+ branchContexts.push(branchContext);
183
+ }
184
+
185
+ // Fusionner les résultats des branches dans l'ordre
186
+ if (branchContexts.length > 0) {
187
+ const finalContext = branchContexts[branchContexts.length - 1];
188
+ Object.assign(context, finalContext);
189
+ }
190
+ }
191
+
192
+ // Mettre à jour le contexte global
193
+ this.context = structuredClone(context);
194
+ return;
195
+ } catch (error) {
196
+ attempts++;
197
+ if (attempts >= maxAttempts) {
198
+ this.eventEmitter.emit("nodeError", { nodeName, error });
199
+ node.onError?.(error as Error);
200
+ this.globalErrorHandler?.(error as Error, context);
201
+ throw error;
202
+ }
203
+
204
+ console.warn(
205
+ `[Graph ${this.name}] Retry attempt ${attempts} for node ${nodeName}`,
206
+ { error }
207
+ );
208
+ await new Promise((resolve) => setTimeout(resolve, delay));
209
+ }
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Validates the current context against the schema
215
+ * @private
216
+ * @param {GraphContext<T>} context - Context to validate
217
+ * @throws {Error} If validation fails
218
+ */
219
+ private validateContext(context: GraphContext<T>): void {
220
+ if (this.validator) {
221
+ this.validator.parse(context);
222
+ }
223
+ }
224
+
225
+ /**
226
+ * Executes the graph flow starting from a specific node
227
+ * @param {string} startNode - Name of the node to start execution from
228
+ * @param {Partial<GraphContext<T>>} inputContext - Optional partial context to merge with current context
229
+ * @param {any} inputParams - Optional input parameters for the start node
230
+ * @returns {Promise<GraphContext<T>>} Final context after execution
231
+ */
232
+ async execute(
233
+ startNode: string,
234
+ inputContext?: Partial<GraphContext<T>>,
235
+ inputParams?: any
236
+ ): Promise<GraphContext<T>> {
237
+ // Fresh local context from the global
238
+ const context = this.createNewContext();
239
+ if (inputContext) Object.assign(context, inputContext);
240
+
241
+ // Emit "graphStarted"
242
+ this.eventEmitter.emit("graphStarted", { name: this.name });
243
+
244
+ try {
245
+ // Because we're calling explicitly, it's NOT triggered by an event
246
+ await this.executeNode(
247
+ startNode,
248
+ context,
249
+ inputParams,
250
+ /* triggeredByEvent= */ false
251
+ );
252
+
253
+ // Emit "graphCompleted"
254
+ this.eventEmitter.emit("graphCompleted", {
255
+ name: this.name,
256
+ context: this.context,
257
+ });
258
+
259
+ // Return a snapshot of the final global context
260
+ return structuredClone(this.context);
261
+ } catch (error) {
262
+ // Emit "graphError"
263
+ this.eventEmitter.emit("graphError", { name: this.name, error });
264
+ this.globalErrorHandler?.(error as Error, context);
265
+ throw error;
266
+ }
267
+ }
268
+
269
+ /**
270
+ * Emits an event to trigger event-based nodes
271
+ * @param {string} eventName - Name of the event to emit
272
+ * @param {Partial<GraphContext<T>>} data - Optional data to merge with context
273
+ * @returns {Promise<GraphContext<T>>} Updated context after event handling
274
+ */
275
+ public async emit(
276
+ eventName: string,
277
+ data?: Partial<GraphContext<T>>
278
+ ): Promise<GraphContext<T>> {
279
+ // Merge data into a fresh copy of the global context if desired
280
+ const context = this.createNewContext();
281
+ if (data) Object.assign(context, data);
282
+
283
+ // Just emit the event; the node-based event listeners in setupEventListeners()
284
+ // will handle calling "executeNode(...)"
285
+ this.eventEmitter.emit(eventName, context);
286
+
287
+ // Return the updated global context
288
+ return this.getContext();
289
+ }
290
+
291
+ /**
292
+ * Registers an event handler
293
+ * @param {string} eventName - Name of the event to listen for
294
+ * @param {Function} handler - Handler function to execute when event is emitted
295
+ */
296
+ on(eventName: string, handler: (...args: any[]) => void): void {
297
+ this.eventEmitter.on(eventName, handler);
298
+ }
299
+
300
+ /**
301
+ * Updates the graph definition with new configuration
302
+ * @param {GraphDefinition<T>} definition - New graph definition
303
+ */
304
+ load(definition: GraphDefinition<T>): void {
305
+ // Clear all existing nodes
306
+ this.nodes.clear();
307
+ // Wipe out old node-based event listeners
308
+ // (We keep external test listeners like "nodeStarted" or "nodeCompleted".)
309
+ if (definition.nodes?.length) {
310
+ const allEvents = new Set<string>();
311
+ definition.nodes.forEach((n) =>
312
+ n.events?.forEach((evt) => allEvents.add(evt))
313
+ );
314
+ for (const evt of allEvents) {
315
+ this.eventEmitter.removeAllListeners(evt);
316
+ }
317
+ }
318
+
319
+ // Add in new nodes
320
+ definition.nodes.forEach((node) => this.nodes.set(node.name, node));
321
+
322
+ // Parse the new context
323
+ this.context = definition.schema.parse(
324
+ definition.context
325
+ ) as GraphContext<T>;
326
+ this.validator = definition.schema;
327
+
328
+ // Store entry node
329
+ this.entryNode = definition.entryNode;
330
+ // Store graph events
331
+ this.graphEvents = definition.events;
332
+
333
+ // Re-setup only node-based event triggers
334
+ for (const node of this.nodes.values()) {
335
+ if (node.events && node.events.length > 0) {
336
+ node.events.forEach((event) => {
337
+ this.eventEmitter.on(
338
+ event,
339
+ async (data?: Partial<GraphContext<T>>) => {
340
+ const freshContext = structuredClone(this.context);
341
+ if (data) Object.assign(freshContext, data);
342
+ await this.executeNode(node.name, freshContext, undefined, true);
343
+ }
344
+ );
345
+ });
346
+ }
347
+ }
348
+
349
+ // Re-setup graph event listeners
350
+ this.setupGraphEventListeners();
351
+ }
352
+
353
+ /**
354
+ * Returns the current context
355
+ * @returns {GraphContext<T>} Current graph context
356
+ */
357
+ getContext(): GraphContext<T> {
358
+ return structuredClone(this.context);
359
+ }
360
+
361
+ /**
362
+ * Logs a message with optional data
363
+ * @param {string} message - Message to log
364
+ * @param {any} data - Optional data to log
365
+ */
366
+ log(message: string, data?: any): void {
367
+ console.log(`[Graph ${this.name}] ${message}`, data);
368
+ }
369
+
370
+ /**
371
+ * Adds a new node to the graph
372
+ * @param {Node<T>} node - Node to add
373
+ * @throws {Error} If node with same name already exists
374
+ */
375
+ addNode(node: Node<T>): void {
376
+ this.nodes.set(node.name, node);
377
+ if (node.events && node.events.length > 0) {
378
+ for (const evt of node.events) {
379
+ this.eventEmitter.on(evt, async (data?: Partial<GraphContext<T>>) => {
380
+ const freshContext = this.createNewContext();
381
+ if (data) Object.assign(freshContext, data);
382
+ await this.executeNode(node.name, freshContext, undefined, true);
383
+ });
384
+ }
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Removes a node from the graph
390
+ * @param {string} nodeName - Name of the node to remove
391
+ */
392
+ removeNode(nodeName: string): void {
393
+ const node = this.nodes.get(nodeName);
394
+ if (!node) return;
395
+
396
+ // remove the node from the map
397
+ this.nodes.delete(nodeName);
398
+
399
+ // remove any of its event-based listeners
400
+ if (node.events && node.events.length > 0) {
401
+ for (const evt of node.events) {
402
+ // removeAllListeners(evt) would also remove other node listeners,
403
+ // so we need a more fine-grained approach. Ideally, we should keep a reference
404
+ // to the exact listener function we attached. For brevity, let's remove all for that event:
405
+ this.eventEmitter.removeAllListeners(evt);
406
+ }
407
+ // Then reattach the others that remain in the graph
408
+ for (const n of this.nodes.values()) {
409
+ if (n.events && n.events.length > 0) {
410
+ n.events.forEach((e) => {
411
+ this.eventEmitter.on(e, async (data?: Partial<GraphContext<T>>) => {
412
+ const freshContext = this.createNewContext();
413
+ if (data) Object.assign(freshContext, data);
414
+ await this.executeNode(n.name, freshContext, undefined, true);
415
+ });
416
+ });
417
+ }
418
+ }
419
+ }
420
+ }
421
+
422
+ /**
423
+ * Returns all nodes in the graph
424
+ * @returns {Node<T>[]} Array of all nodes
425
+ */
426
+ getNodes(): Node<T>[] {
427
+ return Array.from(this.nodes.values());
428
+ }
429
+
430
+ private setupGraphEventListeners(): void {
431
+ if (this.graphEvents && this.graphEvents.length > 0) {
432
+ this.graphEvents.forEach((event) => {
433
+ this.eventEmitter.on(event, async (data?: Partial<GraphContext<T>>) => {
434
+ const freshContext = this.createNewContext();
435
+ if (data) Object.assign(freshContext, data);
436
+
437
+ // Emit "graphStarted"
438
+ this.eventEmitter.emit("graphStarted", { name: this.name });
439
+
440
+ try {
441
+ // Execute the graph starting from the entry node
442
+ if (!this.entryNode) {
443
+ throw new Error("No entry node defined for graph event handling");
444
+ }
445
+
446
+ await this.executeNode(
447
+ this.entryNode,
448
+ freshContext,
449
+ undefined,
450
+ false
451
+ );
452
+
453
+ // Emit "graphCompleted"
454
+ this.eventEmitter.emit("graphCompleted", {
455
+ name: this.name,
456
+ context: this.context,
457
+ });
458
+ } catch (error) {
459
+ // Emit "graphError"
460
+ this.eventEmitter.emit("graphError", { name: this.name, error });
461
+ this.globalErrorHandler?.(error as Error, freshContext);
462
+ throw error;
463
+ }
464
+ });
465
+ });
466
+ }
467
+ }
468
+ }