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 +1 -1
- package/lib/graphai.js +27 -9
- package/lib/node.d.ts +1 -0
- package/lib/node.js +96 -51
- package/lib/task_manager.d.ts +8 -3
- package/lib/task_manager.js +153 -14
- package/lib/transaction_log.js +18 -0
- package/lib/type.d.ts +8 -1
- package/lib/utils/GraphAILogger.d.ts +1 -2
- package/lib/utils/utils.d.ts +2 -1
- package/lib/utils/utils.js +16 -2
- package/lib/validators/common.js +1 -0
- package/lib/validators/computed_node_validator.js +3 -0
- package/lib/validators/graph_data_validator.js +41 -6
- package/package.json +8 -4
package/README.md
CHANGED
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
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
|
-
//
|
|
271
|
-
//
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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;
|
package/lib/task_manager.d.ts
CHANGED
|
@@ -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(
|
|
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[];
|
package/lib/task_manager.js
CHANGED
|
@@ -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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
//
|
|
16
|
-
// and
|
|
17
|
-
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/lib/transaction_log.js
CHANGED
|
@@ -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
|
|
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;
|
package/lib/utils/utils.d.ts
CHANGED
|
@@ -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: {
|
package/lib/utils/utils.js
CHANGED
|
@@ -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
|
};
|
package/lib/validators/common.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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.
|
|
31
|
-
"typedoc-plugin-markdown": "^4.
|
|
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
|
}
|