project-graph-mcp 2.3.1 → 2.3.2
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/package.json +1 -1
- package/vendor/symbiote-node/engine/AgentUICommands.js +100 -0
- package/vendor/symbiote-node/engine/Executor.js +371 -0
- package/vendor/symbiote-node/engine/Graph.js +314 -0
- package/vendor/symbiote-node/engine/GraphServer.js +353 -0
- package/vendor/symbiote-node/engine/HandlerLoader.js +145 -0
- package/vendor/symbiote-node/engine/History.js +83 -0
- package/vendor/symbiote-node/engine/Lifecycle.js +118 -0
- package/vendor/symbiote-node/engine/Persistence.js +84 -0
- package/vendor/symbiote-node/engine/Registry.js +264 -0
- package/vendor/symbiote-node/engine/SocketTypes.js +79 -0
- package/vendor/symbiote-node/engine/cli.js +404 -0
- package/vendor/symbiote-node/engine/index.js +56 -0
- package/vendor/symbiote-node/engine/nanoid.js +28 -0
- package/vendor/symbiote-node/engine/package.json +26 -0
- package/vendor/symbiote-node/engine/packs/ai/beat-detect.handler.js +215 -0
- package/vendor/symbiote-node/engine/packs/ai/content-adapt.handler.js +238 -0
- package/vendor/symbiote-node/engine/packs/ai/face-detect.handler.js +287 -0
- package/vendor/symbiote-node/engine/packs/ai/grok-generate.handler.js +565 -0
- package/vendor/symbiote-node/engine/packs/ai/kling-lipsync.handler.js +414 -0
- package/vendor/symbiote-node/engine/packs/ai/lesson-generate.handler.js +343 -0
- package/vendor/symbiote-node/engine/packs/ai/opencode.handler.js +164 -0
- package/vendor/symbiote-node/engine/packs/ai/replicate-lipsync.handler.js +341 -0
- package/vendor/symbiote-node/engine/packs/ai/tts.handler.js +241 -0
- package/vendor/symbiote-node/engine/packs/ai/whisper.handler.js +191 -0
- package/vendor/symbiote-node/engine/packs/data/db-query.handler.js +67 -0
- package/vendor/symbiote-node/engine/packs/data/news-accumulate.handler.js +281 -0
- package/vendor/symbiote-node/engine/packs/data/personas.handler.js +160 -0
- package/vendor/symbiote-node/engine/packs/data/prompt-loader.handler.js +193 -0
- package/vendor/symbiote-node/engine/packs/data/roles.handler.js +216 -0
- package/vendor/symbiote-node/engine/packs/data/rss-feed.handler.js +244 -0
- package/vendor/symbiote-node/engine/packs/debug/inject.handler.js +52 -0
- package/vendor/symbiote-node/engine/packs/flow/agent.handler.js +73 -0
- package/vendor/symbiote-node/engine/packs/flow/if.handler.js +107 -0
- package/vendor/symbiote-node/engine/packs/flow/loop.handler.js +58 -0
- package/vendor/symbiote-node/engine/packs/flow/merge.handler.js +60 -0
- package/vendor/symbiote-node/engine/packs/flow/retry.handler.js +65 -0
- package/vendor/symbiote-node/engine/packs/flow/switch.handler.js +64 -0
- package/vendor/symbiote-node/engine/packs/flow/wait-all.handler.js +39 -0
- package/vendor/symbiote-node/engine/packs/io/http-request.handler.js +82 -0
- package/vendor/symbiote-node/engine/packs/io/read-file.handler.js +60 -0
- package/vendor/symbiote-node/engine/packs/io/write-file.handler.js +63 -0
- package/vendor/symbiote-node/engine/packs/transform/anchor-match.handler.js +494 -0
- package/vendor/symbiote-node/engine/packs/transform/effects-skeleton.handler.js +417 -0
- package/vendor/symbiote-node/engine/packs/transform/json-parse.handler.js +43 -0
- package/vendor/symbiote-node/engine/packs/transform/lipsync-select.handler.js +339 -0
- package/vendor/symbiote-node/engine/packs/transform/riopla-adapt.handler.js +432 -0
- package/vendor/symbiote-node/engine/packs/transform/set.handler.js +57 -0
- package/vendor/symbiote-node/engine/packs/transform/template-builder.handler.js +134 -0
- package/vendor/symbiote-node/engine/packs/transform/template.handler.js +79 -0
- package/vendor/symbiote-node/engine/packs/transform/timeline-build.handler.js +399 -0
- package/vendor/symbiote-node/engine/packs/util/delay.handler.js +39 -0
- package/vendor/symbiote-node/engine/packs/util/log.handler.js +44 -0
- package/vendor/symbiote-node/engine/packs/video-pack.js +323 -0
package/package.json
CHANGED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentUICommands.js - Agent UI control command builders
|
|
3
|
+
*
|
|
4
|
+
* Pure data builders for WebSocket messages that control the UI.
|
|
5
|
+
* No WebSocket dependency — produces message objects for P23 bridge to send.
|
|
6
|
+
*
|
|
7
|
+
* @module agi-graph/AgentUICommands
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} UICommand
|
|
12
|
+
* @property {string} type - Command type (ui:layout, ui:focus, etc.)
|
|
13
|
+
* @property {object} payload - Command payload
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Switch UI layout — control which panels are visible
|
|
18
|
+
* @param {string[]} panels - Panel names to show
|
|
19
|
+
* @param {string} [split='horizontal'] - Split direction
|
|
20
|
+
* @returns {UICommand}
|
|
21
|
+
*/
|
|
22
|
+
export function layout(panels, split = 'horizontal') {
|
|
23
|
+
return { type: 'ui:layout', payload: { panels, split } };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Focus on a specific panel or node
|
|
28
|
+
* @param {string} panel - Panel name to focus
|
|
29
|
+
* @param {string} [nodeId] - Optional node to zoom to
|
|
30
|
+
* @returns {UICommand}
|
|
31
|
+
*/
|
|
32
|
+
export function focus(panel, nodeId) {
|
|
33
|
+
return { type: 'ui:focus', payload: { panel, nodeId } };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Select nodes on canvas
|
|
38
|
+
* @param {string[]} nodeIds - Node IDs to select
|
|
39
|
+
* @returns {UICommand}
|
|
40
|
+
*/
|
|
41
|
+
export function select(nodeIds) {
|
|
42
|
+
return { type: 'ui:select', payload: { nodeIds } };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Navigate into a compound node
|
|
47
|
+
* @param {string} compoundId - Compound node ID to enter
|
|
48
|
+
* @returns {UICommand}
|
|
49
|
+
*/
|
|
50
|
+
export function navigate(compoundId) {
|
|
51
|
+
return { type: 'ui:navigate', payload: { compoundId } };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Control timeline playback
|
|
56
|
+
* @param {'play'|'stop'|'seek'} action - Playback action
|
|
57
|
+
* @param {number} [frame] - Frame to seek to (for 'seek' action)
|
|
58
|
+
* @returns {UICommand}
|
|
59
|
+
*/
|
|
60
|
+
export function playback(action, frame) {
|
|
61
|
+
const payload = { action };
|
|
62
|
+
if (frame !== undefined) payload.frame = frame;
|
|
63
|
+
return { type: 'ui:playback', payload };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Show notification to user
|
|
68
|
+
* @param {string} message - Notification text
|
|
69
|
+
* @param {'info'|'success'|'warning'|'error'} [type='info'] - Notification type
|
|
70
|
+
* @returns {UICommand}
|
|
71
|
+
*/
|
|
72
|
+
export function notify(message, type = 'info') {
|
|
73
|
+
return { type: 'ui:notify', payload: { message, type } };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Move virtual agent cursor on canvas
|
|
78
|
+
* @param {number} x - X position
|
|
79
|
+
* @param {number} y - Y position
|
|
80
|
+
* @param {string} [label] - Cursor label text
|
|
81
|
+
* @returns {UICommand}
|
|
82
|
+
*/
|
|
83
|
+
export function cursor(x, y, label) {
|
|
84
|
+
const payload = { x, y };
|
|
85
|
+
if (label) payload.label = label;
|
|
86
|
+
return { type: 'ui:cursor', payload };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* All command types for reference
|
|
91
|
+
*/
|
|
92
|
+
export const COMMAND_TYPES = [
|
|
93
|
+
'ui:layout',
|
|
94
|
+
'ui:focus',
|
|
95
|
+
'ui:select',
|
|
96
|
+
'ui:navigate',
|
|
97
|
+
'ui:playback',
|
|
98
|
+
'ui:notify',
|
|
99
|
+
'ui:cursor',
|
|
100
|
+
];
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executor.js - Topological sort execution engine
|
|
3
|
+
*
|
|
4
|
+
* Executes a directed acyclic graph (DAG) of nodes using
|
|
5
|
+
* Kahn's algorithm. Supports incremental execution,
|
|
6
|
+
* cache, async node processing, dynamic sockets,
|
|
7
|
+
* and compound node sub-graph execution.
|
|
8
|
+
*
|
|
9
|
+
* @module symbiote-node/Executor
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getNodeType } from './Registry.js';
|
|
13
|
+
import { Graph } from './Graph.js';
|
|
14
|
+
import { runLifecycle } from './Lifecycle.js';
|
|
15
|
+
|
|
16
|
+
export class Executor {
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
/** @type {Map<string, any>} Cached outputs per node ID */
|
|
20
|
+
this._cache = new Map();
|
|
21
|
+
|
|
22
|
+
/** @type {Map<string, {key: string, outputs: object}>} Lifecycle cache store */
|
|
23
|
+
this._lifecycleCache = new Map();
|
|
24
|
+
|
|
25
|
+
/** @type {Set<string>} Nodes marked dirty (need re-execution) */
|
|
26
|
+
this._dirty = new Set();
|
|
27
|
+
|
|
28
|
+
/** @type {string|null} Currently executing node ID */
|
|
29
|
+
this.currentNode = null;
|
|
30
|
+
|
|
31
|
+
/** @type {Array<{nodeId: string, time: number, skipped: boolean, cached?: boolean, error?: string|null}>} */
|
|
32
|
+
this.executionLog = [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Execute a graph
|
|
37
|
+
* @param {import('./Graph.js').Graph} graph
|
|
38
|
+
* @param {object} [options={}]
|
|
39
|
+
* @param {boolean} [options.cache=false] - Use incremental execution (skip unchanged)
|
|
40
|
+
* @param {function} [options.onNodeStart] - Callback(nodeId, node)
|
|
41
|
+
* @param {function} [options.onNodeComplete] - Callback(nodeId, output, timeMs)
|
|
42
|
+
* @param {function} [options.onNodeSkipped] - Callback(nodeId) for cached nodes
|
|
43
|
+
* @param {function} [options.onNodeCached] - Callback(nodeId, cacheHash) for lifecycle-cached
|
|
44
|
+
* @returns {Promise<{outputs: object, executionOrder: string[], log: Array, totalTime: number}>}
|
|
45
|
+
*/
|
|
46
|
+
async run(graph, options = {}) {
|
|
47
|
+
const { cache = false, onNodeStart, onNodeComplete, onNodeSkipped, onNodeCached } = options;
|
|
48
|
+
const nodes = graph.nodes;
|
|
49
|
+
// Duck-typing: Editor has connections as Map, Graph has array
|
|
50
|
+
const connections = graph.connections instanceof Map
|
|
51
|
+
? [...graph.connections.values()]
|
|
52
|
+
: graph.connections;
|
|
53
|
+
|
|
54
|
+
// Topological sort
|
|
55
|
+
const order = this._topologicalSort(nodes, connections);
|
|
56
|
+
|
|
57
|
+
// Execute in order
|
|
58
|
+
const results = new Map();
|
|
59
|
+
this.executionLog = [];
|
|
60
|
+
|
|
61
|
+
for (const nodeId of order) {
|
|
62
|
+
const node = nodes.get(nodeId);
|
|
63
|
+
|
|
64
|
+
// Skip cached clean nodes
|
|
65
|
+
if (cache && !this._dirty.has(nodeId) && this._cache.has(nodeId)) {
|
|
66
|
+
results.set(nodeId, this._cache.get(nodeId));
|
|
67
|
+
this.executionLog.push({ nodeId, time: 0, skipped: true });
|
|
68
|
+
if (onNodeSkipped) onNodeSkipped(nodeId);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (onNodeStart) onNodeStart(nodeId, node);
|
|
73
|
+
this.currentNode = nodeId;
|
|
74
|
+
const startTime = performance.now();
|
|
75
|
+
|
|
76
|
+
// Resolve inputs from upstream connections
|
|
77
|
+
const inputs = this._resolveInputs(nodeId, connections, results);
|
|
78
|
+
|
|
79
|
+
// P22: Branch skipping — if node has incoming connections and
|
|
80
|
+
// all connected inputs are null, this node is on an inactive branch
|
|
81
|
+
const incomingConns = connections.filter(c => c.to === nodeId);
|
|
82
|
+
if (incomingConns.length > 0) {
|
|
83
|
+
const allNull = incomingConns.every(c => inputs[c.in] === null || inputs[c.in] === undefined);
|
|
84
|
+
// Skip merge nodes — they expect null from one branch
|
|
85
|
+
const isMergeType = node.type === 'flow/merge' || node.type === 'flow/wait-all';
|
|
86
|
+
if (allNull && !isMergeType) {
|
|
87
|
+
node._output = null;
|
|
88
|
+
results.set(nodeId, null);
|
|
89
|
+
const elapsed = performance.now() - startTime;
|
|
90
|
+
this.executionLog.push({ nodeId, time: elapsed, skipped: true, branchSkipped: true });
|
|
91
|
+
if (onNodeSkipped) onNodeSkipped(nodeId);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Execute node processor
|
|
97
|
+
// Check for lifecycle hooks first, then fall back to process()
|
|
98
|
+
let output;
|
|
99
|
+
const typeDef = getNodeType(node.type);
|
|
100
|
+
const lifecycleHooks = typeDef?.lifecycle;
|
|
101
|
+
|
|
102
|
+
if (lifecycleHooks) {
|
|
103
|
+
// Lifecycle path: validate → cache → execute → postProcess
|
|
104
|
+
const cacheState = {
|
|
105
|
+
mode: node.cacheMode || 'auto',
|
|
106
|
+
store: this._lifecycleCache,
|
|
107
|
+
nodeId,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const lifecycleResult = await runLifecycle(lifecycleHooks, inputs, node.params, cacheState);
|
|
111
|
+
|
|
112
|
+
if (lifecycleResult.error) {
|
|
113
|
+
node._output = { _error: lifecycleResult.error };
|
|
114
|
+
node._cacheHash = lifecycleResult.cacheHash;
|
|
115
|
+
results.set(nodeId, node._output);
|
|
116
|
+
const elapsed = performance.now() - startTime;
|
|
117
|
+
this.executionLog.push({ nodeId, time: elapsed, skipped: false, cached: false, error: lifecycleResult.error });
|
|
118
|
+
if (onNodeComplete) onNodeComplete(nodeId, node._output, elapsed);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
output = lifecycleResult.outputs;
|
|
123
|
+
node._cacheHash = lifecycleResult.cacheHash;
|
|
124
|
+
|
|
125
|
+
if (lifecycleResult.cached) {
|
|
126
|
+
if (onNodeCached) onNodeCached(nodeId, lifecycleResult.cacheHash);
|
|
127
|
+
}
|
|
128
|
+
} else {
|
|
129
|
+
// Legacy path: direct process() call
|
|
130
|
+
// Node-level process overrides type-level (for per-instance composition)
|
|
131
|
+
const processFn = node.process || typeDef?.process;
|
|
132
|
+
|
|
133
|
+
if (typeof processFn === 'function') {
|
|
134
|
+
output = await processFn(inputs, node.params);
|
|
135
|
+
} else {
|
|
136
|
+
// Passthrough: merge params with inputs
|
|
137
|
+
output = { ...node.params, ...inputs };
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Compound node: execute sub-graph if returned
|
|
142
|
+
if (output && output._subGraph) {
|
|
143
|
+
output = await this._executeSubGraph(output._subGraph, inputs, node.params);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Dynamic sockets: process exposes runtime-generated outputs
|
|
147
|
+
if (output && output.dynamicOutputs && Array.isArray(output.dynamicOutputs)) {
|
|
148
|
+
node._dynamicSockets = output.dynamicOutputs;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Store output in node
|
|
152
|
+
node._output = output;
|
|
153
|
+
|
|
154
|
+
results.set(nodeId, output);
|
|
155
|
+
this._cache.set(nodeId, output);
|
|
156
|
+
this._dirty.delete(nodeId);
|
|
157
|
+
|
|
158
|
+
const elapsed = performance.now() - startTime;
|
|
159
|
+
this.executionLog.push({ nodeId, time: elapsed, skipped: false });
|
|
160
|
+
|
|
161
|
+
if (onNodeComplete) onNodeComplete(nodeId, output, elapsed);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.currentNode = null;
|
|
165
|
+
|
|
166
|
+
// Collect output nodes (no outgoing connections)
|
|
167
|
+
const outputNodeIds = this._findOutputNodes(nodes, connections);
|
|
168
|
+
const outputs = {};
|
|
169
|
+
for (const id of outputNodeIds) {
|
|
170
|
+
outputs[id] = results.get(id);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
outputs,
|
|
175
|
+
executionOrder: order,
|
|
176
|
+
log: this.executionLog,
|
|
177
|
+
totalTime: this.executionLog.reduce((sum, e) => sum + e.time, 0),
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Mark a node as dirty (needs re-execution)
|
|
183
|
+
* Propagates downstream
|
|
184
|
+
* @param {string} nodeId
|
|
185
|
+
* @param {import('./Graph.js').Connection[]} connections
|
|
186
|
+
*/
|
|
187
|
+
markDirty(nodeId, connections) {
|
|
188
|
+
if (this._dirty.has(nodeId)) return;
|
|
189
|
+
this._dirty.add(nodeId);
|
|
190
|
+
for (const conn of connections) {
|
|
191
|
+
if (conn.from === nodeId) {
|
|
192
|
+
this.markDirty(conn.to, connections);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Clear all caches
|
|
199
|
+
*/
|
|
200
|
+
clearCache() {
|
|
201
|
+
this._cache.clear();
|
|
202
|
+
this._lifecycleCache.clear();
|
|
203
|
+
this._dirty.clear();
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Topological sort using Kahn's algorithm
|
|
208
|
+
* @param {Map<string, object>} nodes
|
|
209
|
+
* @param {Array<{from: string, to: string}>} connections
|
|
210
|
+
* @returns {string[]} Node IDs in execution order
|
|
211
|
+
* @private
|
|
212
|
+
*/
|
|
213
|
+
_topologicalSort(nodes, connections) {
|
|
214
|
+
const inDegree = new Map();
|
|
215
|
+
const adjacency = new Map();
|
|
216
|
+
|
|
217
|
+
// Only include connected nodes (skip orphans)
|
|
218
|
+
const connectedIds = new Set();
|
|
219
|
+
for (const conn of connections) {
|
|
220
|
+
connectedIds.add(conn.from);
|
|
221
|
+
connectedIds.add(conn.to);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Include source nodes (no incoming connections but exist in graph)
|
|
225
|
+
for (const id of nodes.keys()) {
|
|
226
|
+
if (connectedIds.has(id) || !connections.some(c => c.to === id || c.from === id)) {
|
|
227
|
+
// Include connected nodes; orphans are skipped
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
for (const id of connectedIds) {
|
|
232
|
+
if (!nodes.has(id)) continue;
|
|
233
|
+
inDegree.set(id, 0);
|
|
234
|
+
adjacency.set(id, []);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Also include source nodes (nodes with outgoing but no incoming)
|
|
238
|
+
for (const id of nodes.keys()) {
|
|
239
|
+
if (!connectedIds.has(id)) continue;
|
|
240
|
+
if (!inDegree.has(id)) {
|
|
241
|
+
inDegree.set(id, 0);
|
|
242
|
+
adjacency.set(id, []);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
for (const conn of connections) {
|
|
247
|
+
if (!adjacency.has(conn.from) || !inDegree.has(conn.to)) continue;
|
|
248
|
+
adjacency.get(conn.from).push(conn.to);
|
|
249
|
+
inDegree.set(conn.to, (inDegree.get(conn.to) || 0) + 1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Kahn's algorithm
|
|
253
|
+
const queue = [];
|
|
254
|
+
for (const [id, degree] of inDegree) {
|
|
255
|
+
if (degree === 0) queue.push(id);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const result = [];
|
|
259
|
+
while (queue.length > 0) {
|
|
260
|
+
const nodeId = queue.shift();
|
|
261
|
+
result.push(nodeId);
|
|
262
|
+
for (const neighbor of (adjacency.get(nodeId) || [])) {
|
|
263
|
+
const nd = inDegree.get(neighbor) - 1;
|
|
264
|
+
inDegree.set(neighbor, nd);
|
|
265
|
+
if (nd === 0) queue.push(neighbor);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Cycle detection
|
|
270
|
+
const connectedCount = inDegree.size;
|
|
271
|
+
if (result.length < connectedCount) {
|
|
272
|
+
const remaining = [...inDegree.keys()].filter(id => !result.includes(id));
|
|
273
|
+
throw new Error(`Graph contains cycle(s). Nodes involved: ${remaining.join(', ')}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Resolve inputs for a node from upstream connections
|
|
281
|
+
* @param {string} nodeId
|
|
282
|
+
* @param {Array<{from: string, out: string, to: string, in: string}>} connections
|
|
283
|
+
* @param {Map<string, any>} results
|
|
284
|
+
* @returns {object}
|
|
285
|
+
* @private
|
|
286
|
+
*/
|
|
287
|
+
_resolveInputs(nodeId, connections, results) {
|
|
288
|
+
const inputs = {};
|
|
289
|
+
for (const conn of connections) {
|
|
290
|
+
if (conn.to !== nodeId) continue;
|
|
291
|
+
const upstream = results.get(conn.from);
|
|
292
|
+
if (upstream === undefined) continue;
|
|
293
|
+
|
|
294
|
+
let value;
|
|
295
|
+
if (upstream && typeof upstream === 'object' && conn.out in upstream) {
|
|
296
|
+
value = upstream[conn.out];
|
|
297
|
+
} else if (upstream && typeof upstream === 'object' && upstream.dynamicOutputs) {
|
|
298
|
+
// Dynamic routing node (switch): missing key = inactive branch
|
|
299
|
+
value = null;
|
|
300
|
+
} else {
|
|
301
|
+
value = upstream;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Multiple connections to same input: first non-null wins
|
|
305
|
+
if (inputs[conn.in] !== undefined && inputs[conn.in] !== null) continue;
|
|
306
|
+
inputs[conn.in] = value;
|
|
307
|
+
}
|
|
308
|
+
return inputs;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Find output nodes (no outgoing connections)
|
|
313
|
+
* @param {Map<string, object>} nodes
|
|
314
|
+
* @param {Array<{from: string}>} connections
|
|
315
|
+
* @returns {string[]}
|
|
316
|
+
* @private
|
|
317
|
+
*/
|
|
318
|
+
_findOutputNodes(nodes, connections) {
|
|
319
|
+
const hasOutgoing = new Set();
|
|
320
|
+
for (const conn of connections) {
|
|
321
|
+
hasOutgoing.add(conn.from);
|
|
322
|
+
}
|
|
323
|
+
const connected = new Set();
|
|
324
|
+
for (const conn of connections) {
|
|
325
|
+
connected.add(conn.from);
|
|
326
|
+
connected.add(conn.to);
|
|
327
|
+
}
|
|
328
|
+
return [...connected].filter(id => !hasOutgoing.has(id) && nodes.has(id));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Execute a compound node's sub-graph
|
|
333
|
+
* @param {object} subGraphData - Sub-graph JSON definition
|
|
334
|
+
* @param {object} parentInputs - Inputs from parent graph
|
|
335
|
+
* @param {object} parentParams - Parent node params
|
|
336
|
+
* @returns {Promise<object>} Merged outputs from sub-graph output nodes
|
|
337
|
+
* @private
|
|
338
|
+
*/
|
|
339
|
+
async _executeSubGraph(subGraphData, parentInputs, parentParams) {
|
|
340
|
+
const subGraph = new Graph(subGraphData);
|
|
341
|
+
|
|
342
|
+
// Inject parent inputs into sub-graph input nodes
|
|
343
|
+
for (const node of subGraph.nodes.values()) {
|
|
344
|
+
if (node.type === 'compound/input') {
|
|
345
|
+
const injectedOutput = { ...parentInputs, ...parentParams };
|
|
346
|
+
node._output = injectedOutput;
|
|
347
|
+
node.process = () => injectedOutput;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Execute sub-graph with a fresh executor
|
|
352
|
+
const subExecutor = new Executor();
|
|
353
|
+
const result = await subExecutor.run(subGraph);
|
|
354
|
+
|
|
355
|
+
// Merge all output node results
|
|
356
|
+
const merged = {};
|
|
357
|
+
for (const [id, output] of Object.entries(result.outputs)) {
|
|
358
|
+
const node = subGraph.getNode(id);
|
|
359
|
+
if (node.type === 'compound/output' && output) {
|
|
360
|
+
Object.assign(merged, output);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// Include dynamic socket info if sub-graph produces segments
|
|
365
|
+
if (Object.keys(merged).length > 0) {
|
|
366
|
+
merged.dynamicOutputs = Object.keys(merged);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return merged;
|
|
370
|
+
}
|
|
371
|
+
}
|