graphai 2.0.16 → 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
@@ -123,9 +123,13 @@ class GraphAI {
123
123
  this.graphId = `${Date.now().toString(36)}-${Math.random().toString(36).substr(2, 9)}`; // URL.createObjectURL(new Blob()).slice(-36);
124
124
  this.agentFunctionInfoDictionary = agentFunctionInfoDictionary;
125
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);
126
131
  this.taskManager = options.taskManager ?? new task_manager_1.TaskManager(graphData.concurrency ?? exports.defaultConcurrency);
127
132
  this.agentFilters = options.agentFilters ?? [];
128
- this.bypassAgentIds = options.bypassAgentIds ?? [];
129
133
  this.config = options.config;
130
134
  this.graphLoader = options.graphLoader;
131
135
  this.forceLoop = options.forceLoop ?? false;
@@ -135,8 +139,6 @@ class GraphAI {
135
139
  this.onComplete = (__isAbort) => {
136
140
  throw new Error("SOMETHING IS WRONG: onComplete is called without run()");
137
141
  };
138
- (0, validator_1.validateGraphData)(graphData, [...Object.keys(agentFunctionInfoDictionary), ...this.bypassAgentIds]);
139
- (0, validator_1.validateAgent)(agentFunctionInfoDictionary);
140
142
  this.graphData = {
141
143
  ...graphData,
142
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
@@ -67,6 +67,7 @@ class ComputedNode extends Node {
67
67
  agentFunction;
68
68
  timeout; // msec
69
69
  priority;
70
+ label;
70
71
  error;
71
72
  transactionId; // To reject callbacks from timed-out transactions
72
73
  passThrough;
@@ -94,6 +95,10 @@ class ComputedNode extends Node {
94
95
  this.timeout = data.timeout;
95
96
  this.isResult = data.isResult ?? false;
96
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;
97
102
  (0, utils_2.assert)(["function", "string"].includes(typeof data.agent), "agent must be either string or function");
98
103
  if (typeof data.agent === "string") {
99
104
  this.agentId = data.agent;
@@ -293,50 +298,61 @@ class ComputedNode extends Node {
293
298
  const agentFunction = this.agentFunction ?? this.graph.getAgentFunctionInfo(agentId, this.nodeId).agent;
294
299
  const localLog = [];
295
300
  const context = this.getContext(previousResults, localLog, agentId, config);
296
- // NOTE: We use the existence of graph object in the agent-specific params to determine
297
- // if this is a nested agent or not.
298
- if (hasNestedGraph) {
299
- this.graph.taskManager.prepareForNesting();
300
- context.forNestedGraph = {
301
- graphData: this.nestedGraph
302
- ? "nodes" in this.nestedGraph
303
- ? this.nestedGraph
304
- : this.graph.resultOf(this.nestedGraph) // HACK: compiler work-around
305
- : { version: 0, nodes: {} },
306
- agents: this.graph.agentFunctionInfoDictionary,
307
- graphOptions: {
308
- agentFilters: this.graph.agentFilters,
309
- taskManager: this.graph.taskManager,
310
- bypassAgentIds: this.graph.bypassAgentIds,
311
- config,
312
- graphLoader: this.graph.graphLoader,
313
- },
314
- onLogCallback: this.graph.onLogCallback,
315
- callbacks: this.graph.callbacks,
316
- };
317
- }
318
- this.beforeConsoleLog(context);
319
- const result = await this.agentFilterHandler(context, agentFunction, agentId);
320
- this.afterConsoleLog(result);
321
- if (hasNestedGraph) {
322
- this.graph.taskManager.restoreAfterNesting();
323
- }
324
- if (!this.isCurrentTransaction(transactionId)) {
325
- // This condition happens when the agent function returns
326
- // after the timeout (either retried or not).
327
- GraphAILogger_1.GraphAILogger.log(`-- transactionId mismatch with ${this.nodeId} (probably timeout)`);
328
- return;
329
- }
330
- if (this.repeatUntil?.exists) {
331
- const dummyResult = { self: { result: this.getResult(result) } };
332
- const repeatResult = (0, result_1.resultsOf)({ data: this.repeatUntil?.exists }, dummyResult, [], true);
333
- if ((0, utils_1.isNull)(repeatResult?.data)) {
334
- 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)`);
335
338
  return;
336
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
+ }
337
355
  }
338
- // after process
339
- this.afterExecute(result, localLog);
340
356
  }
341
357
  catch (error) {
342
358
  this.errorProcess(error, transactionId, previousResults);
@@ -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,33 +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
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();
11
47
  taskQueue = [];
12
48
  runningNodes = new Set();
13
- constructor(concurrency) {
14
- this.concurrency = concurrency;
49
+ constructor(config) {
50
+ const normalized = normalizeConcurrencyConfig(config);
51
+ this.concurrency = normalized.global;
52
+ this.labelLimits = normalized.labels;
15
53
  }
16
- // This internal method dequeus a task from the task queue
17
- // and call the associated callback method, if the number of
18
- // 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.
19
90
  dequeueTaskIfPossible() {
20
- if (this.runningNodes.size < this.concurrency) {
21
- const task = this.taskQueue.shift();
22
- 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);
23
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
+ }
24
103
  task.callback(task.node);
104
+ return;
25
105
  }
26
106
  }
27
107
  }
28
108
  // Node will call this method to put itself in the execution queue.
29
109
  // We call the associated callback function when it is dequeued.
30
110
  addTask(node, graphId, callback) {
31
- // 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.
32
112
  const count = this.taskQueue.filter((task) => {
33
113
  return task.node.priority >= node.priority;
34
114
  }).length;
@@ -47,16 +127,72 @@ class TaskManager {
47
127
  onComplete(node) {
48
128
  (0, utils_1.assert)(this.runningNodes.has(node), `TaskManager.onComplete node(${node.nodeId}) is not in list`);
49
129
  this.runningNodes.delete(node);
50
- 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
+ }
51
149
  }
52
150
  // Node will call this method before it hands the task manager from the graph
53
151
  // to a nested agent. We need to make it sure that there is enough room to run
54
152
  // computed nodes inside the nested graph to avoid a deadlock.
55
- 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) {
56
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
+ }
57
178
  }
58
- restoreAfterNesting() {
179
+ restoreAfterNesting(label, parentGraphId) {
59
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
+ }
60
196
  }
61
197
  getStatus(verbose = false) {
62
198
  const runningNodes = Array.from(this.runningNodes).map((node) => node.nodeId);
@@ -72,6 +208,8 @@ class TaskManager {
72
208
  reset() {
73
209
  this.taskQueue.length = 0;
74
210
  this.runningNodes.clear();
211
+ this.runningByLabel.clear();
212
+ this.nestingBypassByLabel.clear();
75
213
  }
76
214
  }
77
215
  exports.TaskManager = TaskManager;
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;
@@ -4,6 +4,7 @@ export declare const sleep: (milliseconds: number) => Promise<unknown>;
4
4
  export declare const parseNodeName: (inputNodeId: any, isSelfNode?: boolean, nodes?: GraphNodes) => DataSource;
5
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");
@@ -52,6 +52,17 @@ const isObject = (x) => {
52
52
  return x !== null && typeof x === "object";
53
53
  };
54
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;
55
66
  const isNull = (data) => {
56
67
  return data === null || data === undefined;
57
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.16",
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.13",
31
- "typedoc-plugin-markdown": "^4.9.0"
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
  }