graphai 2.0.15 → 2.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,7 +24,7 @@ nodes:
24
24
  query: describe the final sentence by the court for Sam Bank-Fried
25
25
  wikipedia:
26
26
  console:
27
- before: ...fetching data from wikkpedia
27
+ before: ...fetching data from wikipedia
28
28
  agent: wikipediaAgent
29
29
  inputs:
30
30
  query: :source.name
package/lib/graphai.js CHANGED
@@ -12,6 +12,28 @@ const GraphAILogger_1 = require("./utils/GraphAILogger");
12
12
  exports.defaultConcurrency = 8;
13
13
  exports.graphDataLatestVersion = 0.5;
14
14
  class GraphAI {
15
+ version;
16
+ graphId;
17
+ graphData;
18
+ staticNodeInitData = {};
19
+ loop;
20
+ forceLoop;
21
+ logs = [];
22
+ mapIndex;
23
+ bypassAgentIds;
24
+ config = {};
25
+ agentFunctionInfoDictionary;
26
+ taskManager;
27
+ agentFilters;
28
+ retryLimit;
29
+ propFunctions;
30
+ graphLoader;
31
+ nodes;
32
+ onLogCallback = (__log, __isUpdate) => { };
33
+ callbacks = [];
34
+ verbose; // REVIEW: Do we need this?
35
+ onComplete;
36
+ repeatCount = 0;
15
37
  // This method is called when either the GraphAI obect was created,
16
38
  // or we are about to start n-th iteration (n>2).
17
39
  createNodes(graphData) {
@@ -90,12 +112,6 @@ class GraphAI {
90
112
  forceLoop: false,
91
113
  mapIndex: undefined,
92
114
  }) {
93
- this.staticNodeInitData = {};
94
- this.logs = [];
95
- this.config = {};
96
- this.onLogCallback = (__log, __isUpdate) => { };
97
- this.callbacks = [];
98
- this.repeatCount = 0;
99
115
  if (!graphData.version && !options.taskManager) {
100
116
  GraphAILogger_1.GraphAILogger.warn("------------ missing version number");
101
117
  }
@@ -107,9 +123,13 @@ class GraphAI {
107
123
  this.graphId = `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}`; // URL.createObjectURL(new Blob()).slice(-36);
108
124
  this.agentFunctionInfoDictionary = agentFunctionInfoDictionary;
109
125
  this.propFunctions = prop_function_1.propFunctions;
126
+ this.bypassAgentIds = options.bypassAgentIds ?? [];
127
+ // Validate before constructing TaskManager so user-facing ValidationError
128
+ // is reported instead of TaskManager's internal guard message.
129
+ (0, validator_1.validateGraphData)(graphData, [...Object.keys(agentFunctionInfoDictionary), ...this.bypassAgentIds]);
130
+ (0, validator_1.validateAgent)(agentFunctionInfoDictionary);
110
131
  this.taskManager = options.taskManager ?? new task_manager_1.TaskManager(graphData.concurrency ?? exports.defaultConcurrency);
111
132
  this.agentFilters = options.agentFilters ?? [];
112
- this.bypassAgentIds = options.bypassAgentIds ?? [];
113
133
  this.config = options.config;
114
134
  this.graphLoader = options.graphLoader;
115
135
  this.forceLoop = options.forceLoop ?? false;
@@ -119,8 +139,6 @@ class GraphAI {
119
139
  this.onComplete = (__isAbort) => {
120
140
  throw new Error("SOMETHING IS WRONG: onComplete is called without run()");
121
141
  };
122
- (0, validator_1.validateGraphData)(graphData, [...Object.keys(agentFunctionInfoDictionary), ...this.bypassAgentIds]);
123
- (0, validator_1.validateAgent)(agentFunctionInfoDictionary);
124
142
  this.graphData = {
125
143
  ...graphData,
126
144
  nodes: {
package/lib/node.d.ts CHANGED
@@ -27,6 +27,7 @@ export declare class ComputedNode extends Node {
27
27
  private agentFunction?;
28
28
  readonly timeout?: number;
29
29
  readonly priority: number;
30
+ readonly label?: string;
30
31
  error?: Error;
31
32
  transactionId: undefined | number;
32
33
  private readonly passThrough?;
package/lib/node.js CHANGED
@@ -9,10 +9,14 @@ const transaction_log_1 = require("./transaction_log");
9
9
  const result_1 = require("./utils/result");
10
10
  const GraphAILogger_1 = require("./utils/GraphAILogger");
11
11
  class Node {
12
+ nodeId;
13
+ waitlist = new Set(); // List of nodes which need data from this node.
14
+ state = type_1.NodeState.Waiting;
15
+ result = undefined;
16
+ graph;
17
+ log;
18
+ console; // console output option (before and/or after)
12
19
  constructor(nodeId, graph) {
13
- this.waitlist = new Set(); // List of nodes which need data from this node.
14
- this.state = type_1.NodeState.Waiting;
15
- this.result = undefined;
16
20
  this.nodeId = nodeId;
17
21
  this.graph = graph;
18
22
  this.log = new transaction_log_1.TransactionLog(nodeId, this.graph.mapIndex);
@@ -51,13 +55,36 @@ class Node {
51
55
  }
52
56
  exports.Node = Node;
53
57
  class ComputedNode extends Node {
58
+ graphId;
59
+ isResult;
60
+ params; // Agent-specific parameters
61
+ filterParams;
62
+ nestedGraph;
63
+ retryLimit;
64
+ retryCount = 0;
65
+ repeatUntil;
66
+ agentId;
67
+ agentFunction;
68
+ timeout; // msec
69
+ priority;
70
+ label;
71
+ error;
72
+ transactionId; // To reject callbacks from timed-out transactions
73
+ passThrough;
74
+ anyInput; // any input makes this node ready
75
+ dataSources = []; // no longer needed. This is for transaction log.
76
+ inputs;
77
+ output;
78
+ pendings; // List of nodes this node is waiting data from.
79
+ ifSource; // conditional execution
80
+ unlessSource; // conditional execution
81
+ defaultValue;
82
+ isSkip = false;
83
+ debugInfo;
84
+ isStaticNode = false;
85
+ isComputedNode = true;
54
86
  constructor(graphId, nodeId, data, graph) {
55
87
  super(nodeId, graph);
56
- this.retryCount = 0;
57
- this.dataSources = []; // no longer needed. This is for transaction log.
58
- this.isSkip = false;
59
- this.isStaticNode = false;
60
- this.isComputedNode = true;
61
88
  this.graphId = graphId;
62
89
  this.params = data.params ?? {};
63
90
  this.console = data.console ?? {};
@@ -68,6 +95,10 @@ class ComputedNode extends Node {
68
95
  this.timeout = data.timeout;
69
96
  this.isResult = data.isResult ?? false;
70
97
  this.priority = data.priority ?? 0;
98
+ // Defensive: graph data may originate from YAML/JSON without strict typing.
99
+ // Keep label only when it is actually a string so TaskManager's label-keyed
100
+ // bookkeeping cannot be silently bypassed by a non-string value.
101
+ this.label = typeof data.label === "string" ? data.label : undefined;
71
102
  (0, utils_2.assert)(["function", "string"].includes(typeof data.agent), "agent must be either string or function");
72
103
  if (typeof data.agent === "string") {
73
104
  this.agentId = data.agent;
@@ -267,50 +298,61 @@ class ComputedNode extends Node {
267
298
  const agentFunction = this.agentFunction ?? this.graph.getAgentFunctionInfo(agentId, this.nodeId).agent;
268
299
  const localLog = [];
269
300
  const context = this.getContext(previousResults, localLog, agentId, config);
270
- // NOTE: We use the existence of graph object in the agent-specific params to determine
271
- // if this is a nested agent or not.
272
- if (hasNestedGraph) {
273
- this.graph.taskManager.prepareForNesting();
274
- context.forNestedGraph = {
275
- graphData: this.nestedGraph
276
- ? "nodes" in this.nestedGraph
277
- ? this.nestedGraph
278
- : this.graph.resultOf(this.nestedGraph) // HACK: compiler work-around
279
- : { version: 0, nodes: {} },
280
- agents: this.graph.agentFunctionInfoDictionary,
281
- graphOptions: {
282
- agentFilters: this.graph.agentFilters,
283
- taskManager: this.graph.taskManager,
284
- bypassAgentIds: this.graph.bypassAgentIds,
285
- config,
286
- graphLoader: this.graph.graphLoader,
287
- },
288
- onLogCallback: this.graph.onLogCallback,
289
- callbacks: this.graph.callbacks,
290
- };
291
- }
292
- this.beforeConsoleLog(context);
293
- const result = await this.agentFilterHandler(context, agentFunction, agentId);
294
- this.afterConsoleLog(result);
295
- if (hasNestedGraph) {
296
- this.graph.taskManager.restoreAfterNesting();
297
- }
298
- if (!this.isCurrentTransaction(transactionId)) {
299
- // This condition happens when the agent function returns
300
- // after the timeout (either retried or not).
301
- GraphAILogger_1.GraphAILogger.log(`-- transactionId mismatch with ${this.nodeId} (probably timeout)`);
302
- return;
303
- }
304
- if (this.repeatUntil?.exists) {
305
- const dummyResult = { self: { result: this.getResult(result) } };
306
- const repeatResult = (0, result_1.resultsOf)({ data: this.repeatUntil?.exists }, dummyResult, [], true);
307
- if ((0, utils_1.isNull)(repeatResult?.data)) {
308
- this.retry(type_1.NodeState.Failed, Error("Repeat Until"));
301
+ // The `nestingPrepared` flag tracks whether prepareForNesting has actually
302
+ // run. If anything throws between prepareForNesting and the agent call --
303
+ // e.g. resultOf() during forNestedGraph construction -- we still need to
304
+ // restore. Conversely, if prepareForNesting itself throws, we must NOT
305
+ // restore (no bump to undo).
306
+ let nestingPrepared = false;
307
+ try {
308
+ // NOTE: We use the existence of graph object in the agent-specific params to determine
309
+ // if this is a nested agent or not.
310
+ if (hasNestedGraph) {
311
+ this.graph.taskManager.prepareForNesting(this.label, this.graphId);
312
+ nestingPrepared = true;
313
+ context.forNestedGraph = {
314
+ graphData: this.nestedGraph
315
+ ? "nodes" in this.nestedGraph
316
+ ? this.nestedGraph
317
+ : this.graph.resultOf(this.nestedGraph) // HACK: compiler work-around
318
+ : { version: 0, nodes: {} },
319
+ agents: this.graph.agentFunctionInfoDictionary,
320
+ graphOptions: {
321
+ agentFilters: this.graph.agentFilters,
322
+ taskManager: this.graph.taskManager,
323
+ bypassAgentIds: this.graph.bypassAgentIds,
324
+ config,
325
+ graphLoader: this.graph.graphLoader,
326
+ },
327
+ onLogCallback: this.graph.onLogCallback,
328
+ callbacks: this.graph.callbacks,
329
+ };
330
+ }
331
+ this.beforeConsoleLog(context);
332
+ const result = await this.agentFilterHandler(context, agentFunction, agentId);
333
+ this.afterConsoleLog(result);
334
+ if (!this.isCurrentTransaction(transactionId)) {
335
+ // This condition happens when the agent function returns
336
+ // after the timeout (either retried or not).
337
+ GraphAILogger_1.GraphAILogger.log(`-- transactionId mismatch with ${this.nodeId} (probably timeout)`);
309
338
  return;
310
339
  }
340
+ if (this.repeatUntil?.exists) {
341
+ const dummyResult = { self: { result: this.getResult(result) } };
342
+ const repeatResult = (0, result_1.resultsOf)({ data: this.repeatUntil?.exists }, dummyResult, [], true);
343
+ if ((0, utils_1.isNull)(repeatResult?.data)) {
344
+ this.retry(type_1.NodeState.Failed, Error("Repeat Until"));
345
+ return;
346
+ }
347
+ }
348
+ // after process
349
+ this.afterExecute(result, localLog);
350
+ }
351
+ finally {
352
+ if (nestingPrepared) {
353
+ this.graph.taskManager.restoreAfterNesting(this.label, this.graphId);
354
+ }
311
355
  }
312
- // after process
313
- this.afterExecute(result, localLog);
314
356
  }
315
357
  catch (error) {
316
358
  this.errorProcess(error, transactionId, previousResults);
@@ -419,10 +461,13 @@ class ComputedNode extends Node {
419
461
  }
420
462
  exports.ComputedNode = ComputedNode;
421
463
  class StaticNode extends Node {
464
+ value;
465
+ update;
466
+ isResult;
467
+ isStaticNode = true;
468
+ isComputedNode = false;
422
469
  constructor(nodeId, data, graph) {
423
470
  super(nodeId, graph);
424
- this.isStaticNode = true;
425
- this.isComputedNode = false;
426
471
  this.value = data.value;
427
472
  this.update = data.update ? (0, utils_2.parseNodeName)(data.update) : undefined;
428
473
  this.isResult = data.isResult ?? false;
@@ -1,15 +1,20 @@
1
1
  import { ComputedNode } from "./node";
2
+ import { ConcurrencyConfig } from "./type";
2
3
  export declare class TaskManager {
3
4
  private concurrency;
5
+ private labelLimits;
6
+ private runningByLabel;
7
+ private nestingBypassByLabel;
4
8
  private taskQueue;
5
9
  private runningNodes;
6
- constructor(concurrency: number);
10
+ constructor(config: number | ConcurrencyConfig);
11
+ private canRun;
7
12
  private dequeueTaskIfPossible;
8
13
  addTask(node: ComputedNode, graphId: string, callback: (node: ComputedNode) => void): void;
9
14
  isRunning(graphId: string): boolean;
10
15
  onComplete(node: ComputedNode): void;
11
- prepareForNesting(): void;
12
- restoreAfterNesting(): void;
16
+ prepareForNesting(label?: string, parentGraphId?: string): void;
17
+ restoreAfterNesting(label?: string, parentGraphId?: string): void;
13
18
  getStatus(verbose?: boolean): {
14
19
  runningNodes: string[];
15
20
  queuedNodes: string[];
@@ -2,32 +2,113 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.TaskManager = void 0;
4
4
  const utils_1 = require("./utils/utils");
5
+ const assertPositiveInteger = (value, field) => {
6
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
7
+ throw new Error(`TaskManager: ${field} must be a positive integer (got ${String(value)})`);
8
+ }
9
+ return value;
10
+ };
11
+ // Mirrors the strictness of validateConcurrencyConfig() so that direct
12
+ // `new TaskManager(...)` calls (e.g. tests, advanced consumers that bypass
13
+ // validateGraphData) cannot silently disable label enforcement with a
14
+ // malformed shape such as Map / Date / class instances / arrays.
15
+ const normalizeConcurrencyConfig = (config) => {
16
+ if (typeof config === "number") {
17
+ return { global: assertPositiveInteger(config, "concurrency"), labels: new Map() };
18
+ }
19
+ if (!(0, utils_1.isPlainObject)(config)) {
20
+ throw new Error("TaskManager: concurrency must be a positive integer or a ConcurrencyConfig object");
21
+ }
22
+ const global = assertPositiveInteger(config.global, "concurrency.global");
23
+ const labels = new Map();
24
+ if (config.labels !== undefined) {
25
+ if (!(0, utils_1.isPlainObject)(config.labels)) {
26
+ throw new Error("TaskManager: concurrency.labels must be a plain object");
27
+ }
28
+ for (const [labelKey, labelValue] of Object.entries(config.labels)) {
29
+ labels.set(labelKey, assertPositiveInteger(labelValue, `concurrency.labels.${labelKey}`));
30
+ }
31
+ }
32
+ return { global, labels };
33
+ };
5
34
  // TaskManage object controls the concurrency of ComputedNode execution.
6
35
  //
7
36
  // NOTE: A TaskManager instance will be shared between parent graph and its children
8
37
  // when nested agents are involved.
9
38
  class TaskManager {
10
- constructor(concurrency) {
11
- this.taskQueue = [];
12
- this.runningNodes = new Set();
13
- this.concurrency = concurrency;
39
+ concurrency;
40
+ labelLimits;
41
+ runningByLabel = new Map();
42
+ // Per-label bypass capacity granted to nested children. Keyed by parentGraphId
43
+ // so that only tasks whose graphId differs from the parent (i.e. those running
44
+ // inside the nested graph) can consume the extra slot. Unrelated siblings on
45
+ // the same graphId as the parent are not affected.
46
+ nestingBypassByLabel = new Map();
47
+ taskQueue = [];
48
+ runningNodes = new Set();
49
+ constructor(config) {
50
+ const normalized = normalizeConcurrencyConfig(config);
51
+ this.concurrency = normalized.global;
52
+ this.labelLimits = normalized.labels;
14
53
  }
15
- // This internal method dequeus a task from the task queue
16
- // and call the associated callback method, if the number of
17
- // running task is lower than the spcified limit.
54
+ // Returns true if the task can run right now under both the global limit
55
+ // and (if specified) its label-specific limit.
56
+ canRun(task) {
57
+ if (this.runningNodes.size >= this.concurrency) {
58
+ return false;
59
+ }
60
+ const label = task.node.label;
61
+ if (label === undefined) {
62
+ return true;
63
+ }
64
+ const limit = this.labelLimits.get(label);
65
+ if (limit === undefined) {
66
+ return true;
67
+ }
68
+ const running = this.runningByLabel.get(label) ?? 0;
69
+ if (running < limit) {
70
+ return true;
71
+ }
72
+ // Bypass path: if a labeled parent has prepared for nesting and this task
73
+ // belongs to a different graph (i.e. the nested graph), grant +1 per such
74
+ // outstanding bump. Unrelated siblings on the parent's graphId do NOT get
75
+ // this allowance, so the per-label cap is preserved for them.
76
+ const bypass = this.nestingBypassByLabel.get(label);
77
+ if (!bypass) {
78
+ return false;
79
+ }
80
+ let extra = 0;
81
+ for (const [parentGraphId, count] of bypass) {
82
+ if (parentGraphId !== task.graphId) {
83
+ extra += count;
84
+ }
85
+ }
86
+ return running < limit + extra;
87
+ }
88
+ // Walk the queue (already sorted by priority desc) and dispatch the first task
89
+ // whose label still has capacity. This is the "head-of-line skip" policy.
18
90
  dequeueTaskIfPossible() {
19
- if (this.runningNodes.size < this.concurrency) {
20
- const task = this.taskQueue.shift();
21
- if (task) {
91
+ if (this.runningNodes.size >= this.concurrency) {
92
+ return;
93
+ }
94
+ for (let i = 0; i < this.taskQueue.length; i++) {
95
+ const task = this.taskQueue[i];
96
+ if (this.canRun(task)) {
97
+ this.taskQueue.splice(i, 1);
22
98
  this.runningNodes.add(task.node);
99
+ const label = task.node.label;
100
+ if (label !== undefined) {
101
+ this.runningByLabel.set(label, (this.runningByLabel.get(label) ?? 0) + 1);
102
+ }
23
103
  task.callback(task.node);
104
+ return;
24
105
  }
25
106
  }
26
107
  }
27
108
  // Node will call this method to put itself in the execution queue.
28
109
  // We call the associated callback function when it is dequeued.
29
110
  addTask(node, graphId, callback) {
30
- // Finder tasks in the queue, which has either the same or higher priority.
111
+ // Find tasks in the queue, which has either the same or higher priority.
31
112
  const count = this.taskQueue.filter((task) => {
32
113
  return task.node.priority >= node.priority;
33
114
  }).length;
@@ -46,16 +127,72 @@ class TaskManager {
46
127
  onComplete(node) {
47
128
  (0, utils_1.assert)(this.runningNodes.has(node), `TaskManager.onComplete node(${node.nodeId}) is not in list`);
48
129
  this.runningNodes.delete(node);
49
- this.dequeueTaskIfPossible();
130
+ const label = node.label;
131
+ if (label !== undefined) {
132
+ const running = this.runningByLabel.get(label) ?? 0;
133
+ if (running <= 1) {
134
+ this.runningByLabel.delete(label);
135
+ }
136
+ else {
137
+ this.runningByLabel.set(label, running - 1);
138
+ }
139
+ }
140
+ // A label slot may have just opened, so try to dispatch as many newly-eligible
141
+ // tasks as possible (the freed label could allow several queued tasks to run if
142
+ // the global limit is not yet reached).
143
+ let progressed = true;
144
+ while (progressed) {
145
+ const before = this.runningNodes.size;
146
+ this.dequeueTaskIfPossible();
147
+ progressed = this.runningNodes.size > before;
148
+ }
50
149
  }
51
150
  // Node will call this method before it hands the task manager from the graph
52
151
  // to a nested agent. We need to make it sure that there is enough room to run
53
152
  // computed nodes inside the nested graph to avoid a deadlock.
54
- prepareForNesting() {
153
+ //
154
+ // When the parent carries a label that has a configured per-label limit,
155
+ // a nested child sharing that label would otherwise stay queued forever
156
+ // (parent waits for child, child blocked by parent's label slot). To avoid
157
+ // this without widening the cap for unrelated siblings, we record a per-
158
+ // parent-graphId bypass that canRun() applies only to tasks whose graphId
159
+ // differs from the parent's.
160
+ prepareForNesting(label, parentGraphId) {
55
161
  this.concurrency++;
162
+ if (label !== undefined && parentGraphId !== undefined && this.labelLimits.has(label)) {
163
+ let perParent = this.nestingBypassByLabel.get(label);
164
+ if (!perParent) {
165
+ perParent = new Map();
166
+ this.nestingBypassByLabel.set(label, perParent);
167
+ }
168
+ perParent.set(parentGraphId, (perParent.get(parentGraphId) ?? 0) + 1);
169
+ }
170
+ // Both the global slot bump and the optional label bypass can free capacity
171
+ // for already-queued tasks; drain the queue while progress is being made.
172
+ let progressed = true;
173
+ while (progressed) {
174
+ const before = this.runningNodes.size;
175
+ this.dequeueTaskIfPossible();
176
+ progressed = this.runningNodes.size > before;
177
+ }
56
178
  }
57
- restoreAfterNesting() {
179
+ restoreAfterNesting(label, parentGraphId) {
58
180
  this.concurrency--;
181
+ if (label !== undefined && parentGraphId !== undefined) {
182
+ const perParent = this.nestingBypassByLabel.get(label);
183
+ if (perParent) {
184
+ const next = (perParent.get(parentGraphId) ?? 0) - 1;
185
+ if (next <= 0) {
186
+ perParent.delete(parentGraphId);
187
+ }
188
+ else {
189
+ perParent.set(parentGraphId, next);
190
+ }
191
+ if (perParent.size === 0) {
192
+ this.nestingBypassByLabel.delete(label);
193
+ }
194
+ }
195
+ }
59
196
  }
60
197
  getStatus(verbose = false) {
61
198
  const runningNodes = Array.from(this.runningNodes).map((node) => node.nodeId);
@@ -71,6 +208,8 @@ class TaskManager {
71
208
  reset() {
72
209
  this.taskQueue.length = 0;
73
210
  this.runningNodes.clear();
211
+ this.runningByLabel.clear();
212
+ this.nestingBypassByLabel.clear();
74
213
  }
75
214
  }
76
215
  exports.TaskManager = TaskManager;
@@ -5,6 +5,24 @@ const type_1 = require("./type");
5
5
  const utils_1 = require("./utils/utils");
6
6
  const nodeUtils_1 = require("./utils/nodeUtils");
7
7
  class TransactionLog {
8
+ nodeId;
9
+ state;
10
+ startTime;
11
+ endTime;
12
+ retryCount;
13
+ agentId;
14
+ params;
15
+ inputs;
16
+ inputsData;
17
+ namedInputs;
18
+ injectFrom;
19
+ errorMessage;
20
+ result;
21
+ resultKeys;
22
+ mapIndex;
23
+ isLoop;
24
+ repeatCount;
25
+ log;
8
26
  constructor(nodeId, mapIndex) {
9
27
  this.nodeId = nodeId;
10
28
  this.state = type_1.NodeState.Waiting;
package/lib/type.d.ts CHANGED
@@ -65,6 +65,7 @@ export type ComputedNodeData = {
65
65
  graphLoader?: GraphDataLoaderOption;
66
66
  isResult?: boolean;
67
67
  priority?: number;
68
+ label?: string;
68
69
  passThrough?: PassThrough;
69
70
  console?: ConsoleElement;
70
71
  };
@@ -73,10 +74,14 @@ export type LoopData = {
73
74
  count?: number;
74
75
  while?: string | boolean;
75
76
  };
77
+ export type ConcurrencyConfig = {
78
+ global: number;
79
+ labels?: Record<string, number>;
80
+ };
76
81
  export type GraphData = {
77
82
  version?: number;
78
83
  nodes: Record<string, NodeData>;
79
- concurrency?: number;
84
+ concurrency?: number | ConcurrencyConfig;
80
85
  loop?: LoopData;
81
86
  verbose?: boolean;
82
87
  retry?: number;
@@ -164,4 +169,6 @@ export type AgentFunctionInfo = {
164
169
  export type AgentFunctionInfoDictionary = Record<string, AgentFunctionInfo>;
165
170
  export type PropFunction = (result: ResultData, propId: string) => ResultData;
166
171
  export type CallbackFunction = (log: TransactionLog, isUpdate: boolean) => void;
172
+ export type LogLevel = "debug" | "info" | "log" | "warn" | "error";
173
+ export type LoggerFunction = (level: LogLevel, ...args: any[]) => void;
167
174
  export {};
@@ -1,5 +1,4 @@
1
- type LogLevel = "debug" | "info" | "log" | "warn" | "error";
2
- type LoggerFunction = (level: LogLevel, ...args: any[]) => void;
1
+ import { type LogLevel, type LoggerFunction } from "../type";
3
2
  declare function setLevelEnabled(level: LogLevel, enabled: boolean): void;
4
3
  declare function setLogger(logger: LoggerFunction): void;
5
4
  declare function debug(...args: any[]): void;
@@ -2,8 +2,9 @@ import { DataSource, AgentFunction, AgentFunctionInfo, NodeData, StaticNodeData,
2
2
  import type { GraphNodes } from "../node";
3
3
  export declare const sleep: (milliseconds: number) => Promise<unknown>;
4
4
  export declare const parseNodeName: (inputNodeId: any, isSelfNode?: boolean, nodes?: GraphNodes) => DataSource;
5
- export declare function assert(condition: boolean, message: string, isWarn?: boolean): asserts condition;
5
+ export declare function assert(condition: boolean, message: string, isWarn?: boolean, cause?: unknown): asserts condition;
6
6
  export declare const isObject: <Values = unknown>(x: unknown) => x is Record<string, Values>;
7
+ export declare const isPlainObject: <Values = unknown>(x: unknown) => x is Record<string, Values>;
7
8
  export declare const isNull: (data: unknown) => data is null | undefined;
8
9
  export declare const strIntentionalError = "Intentional Error for Debugging";
9
10
  export declare const defaultAgentInfo: {
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.loopCounterKey = exports.isStaticNodeData = exports.isComputedNodeData = exports.isNamedInputs = exports.defaultTestContext = exports.isLogicallyTrue = exports.debugResultKey = exports.agentInfoWrapper = exports.defaultAgentInfo = exports.strIntentionalError = exports.isNull = exports.isObject = exports.parseNodeName = exports.sleep = void 0;
3
+ exports.loopCounterKey = exports.isStaticNodeData = exports.isComputedNodeData = exports.isNamedInputs = exports.defaultTestContext = exports.isLogicallyTrue = exports.debugResultKey = exports.agentInfoWrapper = exports.defaultAgentInfo = exports.strIntentionalError = exports.isNull = exports.isPlainObject = exports.isObject = exports.parseNodeName = exports.sleep = void 0;
4
4
  exports.assert = assert;
5
5
  const type_1 = require("../type");
6
6
  const GraphAILogger_1 = require("./GraphAILogger");
@@ -37,9 +37,12 @@ const parseNodeName = (inputNodeId, isSelfNode = false, nodes) => {
37
37
  return { value: inputNodeId }; // non-string literal
38
38
  };
39
39
  exports.parseNodeName = parseNodeName;
40
- function assert(condition, message, isWarn = false) {
40
+ function assert(condition, message, isWarn = false, cause) {
41
41
  if (!condition) {
42
42
  if (!isWarn) {
43
+ if (cause) {
44
+ throw new Error(message, { cause });
45
+ }
43
46
  throw new Error(message);
44
47
  }
45
48
  GraphAILogger_1.GraphAILogger.warn("warn: " + message);
@@ -49,6 +52,17 @@ const isObject = (x) => {
49
52
  return x !== null && typeof x === "object";
50
53
  };
51
54
  exports.isObject = isObject;
55
+ // Stricter than isObject: rejects arrays, Map, Date, class instances, etc., that
56
+ // would otherwise pass `typeof === "object"` and confuse `Object.entries()`.
57
+ // Realm-sensitive (compares against the current realm's Object.prototype),
58
+ // which is fine for graph data sourced from JSON.parse or in-process construction.
59
+ const isPlainObject = (x) => {
60
+ if (!(0, exports.isObject)(x) || Array.isArray(x))
61
+ return false;
62
+ const proto = Object.getPrototypeOf(x);
63
+ return proto === null || proto === Object.prototype;
64
+ };
65
+ exports.isPlainObject = isPlainObject;
52
66
  const isNull = (data) => {
53
67
  return data === null || data === undefined;
54
68
  };
@@ -15,6 +15,7 @@ exports.computedNodeAttributeKeys = [
15
15
  "graphLoader",
16
16
  "isResult",
17
17
  "priority",
18
+ "label",
18
19
  "if",
19
20
  "unless",
20
21
  "defaultValue",
@@ -8,6 +8,9 @@ const computedNodeValidator = (nodeData) => {
8
8
  throw new common_1.ValidationError("Computed node does not allow " + key);
9
9
  }
10
10
  });
11
+ if (nodeData.label !== undefined && typeof nodeData.label !== "string") {
12
+ throw new common_1.ValidationError("Computed node label must be a string");
13
+ }
11
14
  return true;
12
15
  };
13
16
  exports.computedNodeValidator = computedNodeValidator;
@@ -2,6 +2,46 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.graphDataValidator = exports.graphNodesValidator = void 0;
4
4
  const common_1 = require("./common");
5
+ const utils_1 = require("../utils/utils");
6
+ const concurrencyConfigKeys = ["global", "labels"];
7
+ const validateConcurrencyValue = (value, fieldDescription) => {
8
+ if (typeof value !== "number" || !Number.isInteger(value)) {
9
+ throw new common_1.ValidationError(`${fieldDescription} must be an integer`);
10
+ }
11
+ if (value < 1) {
12
+ throw new common_1.ValidationError(`${fieldDescription} must be a positive integer`);
13
+ }
14
+ };
15
+ const validateConcurrencyConfig = (concurrency) => {
16
+ if (typeof concurrency === "number") {
17
+ validateConcurrencyValue(concurrency, "Concurrency");
18
+ return;
19
+ }
20
+ if (!(0, utils_1.isPlainObject)(concurrency)) {
21
+ throw new common_1.ValidationError("Concurrency must be an integer");
22
+ }
23
+ for (const key of Object.keys(concurrency)) {
24
+ if (!concurrencyConfigKeys.includes(key)) {
25
+ throw new common_1.ValidationError(`Concurrency object does not allow ${key}`);
26
+ }
27
+ }
28
+ if (!("global" in concurrency)) {
29
+ throw new common_1.ValidationError("Concurrency object must have a global field");
30
+ }
31
+ validateConcurrencyValue(concurrency.global, "Concurrency.global");
32
+ // The schema declares labels?: Record<string, number>. undefined is the only
33
+ // sentinel for "absent"; null, arrays, Maps, Dates and other non-plain-object
34
+ // shapes are malformed and would silently disable label enforcement (their
35
+ // Object.entries() yields no string keys).
36
+ if (concurrency.labels !== undefined) {
37
+ if (!(0, utils_1.isPlainObject)(concurrency.labels)) {
38
+ throw new common_1.ValidationError("Concurrency.labels must be an object");
39
+ }
40
+ for (const [labelKey, labelValue] of Object.entries(concurrency.labels)) {
41
+ validateConcurrencyValue(labelValue, `Concurrency.labels.${labelKey}`);
42
+ }
43
+ }
44
+ };
5
45
  const graphNodesValidator = (data) => {
6
46
  if (data.nodes === undefined) {
7
47
  throw new common_1.ValidationError("Invalid Graph Data: no nodes");
@@ -32,12 +72,7 @@ const graphDataValidator = (data) => {
32
72
  }
33
73
  }
34
74
  if (data.concurrency !== undefined) {
35
- if (!Number.isInteger(data.concurrency)) {
36
- throw new common_1.ValidationError("Concurrency must be an integer");
37
- }
38
- if (data.concurrency < 1) {
39
- throw new common_1.ValidationError("Concurrency must be a positive integer");
40
- }
75
+ validateConcurrencyConfig(data.concurrency);
41
76
  }
42
77
  };
43
78
  exports.graphDataValidator = graphDataValidator;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphai",
3
- "version": "2.0.15",
3
+ "version": "2.0.17",
4
4
  "description": "Asynchronous data flow execution engine for agentic AI apps.",
5
5
  "main": "lib/index.js",
6
6
  "files": [
@@ -14,7 +14,8 @@
14
14
  "doc": "echo nothing",
15
15
  "format": "prettier --write '{src,tests}/**/*.ts' *.mjs",
16
16
  "test": "node --test --require ts-node/register ./tests/**/test_*.ts",
17
- "b": "yarn run format && yarn run eslint && yarn run build"
17
+ "b": "yarn run format && yarn run eslint && yarn run build",
18
+ "prepack": "yarn build"
18
19
  },
19
20
  "repository": {
20
21
  "type": "git",
@@ -27,12 +28,15 @@
27
28
  },
28
29
  "homepage": "https://github.com/receptron/graphai#readme",
29
30
  "devDependencies": {
30
- "typedoc": "^0.28.12",
31
- "typedoc-plugin-markdown": "^4.8.1"
31
+ "typedoc": "^0.28.18",
32
+ "typedoc-plugin-markdown": "^4.11.0"
32
33
  },
33
34
  "types": "./lib/index.d.ts",
34
35
  "directories": {
35
36
  "lib": "lib",
36
37
  "test": "tests"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public"
37
41
  }
38
42
  }