ultravisor 1.0.15 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ultravisor",
3
- "version": "1.0.15",
3
+ "version": "1.0.17",
4
4
  "description": "Cyclic process execution with ai integration.",
5
5
  "main": "source/Ultravisor.cjs",
6
6
  "bin": {
@@ -30,8 +30,8 @@
30
30
  "cron": "^4.4.0",
31
31
  "orator": "^6.0.4",
32
32
  "orator-authentication": "^1.0.0",
33
- "orator-serviceserver-restify": "^2.0.9",
34
- "pict": "^1.0.359",
33
+ "orator-serviceserver-restify": "^2.0.10",
34
+ "pict": "^1.0.361",
35
35
  "pict-service-commandlineutility": "^1.0.19",
36
36
  "pict-serviceproviderbase": "^1.0.4",
37
37
  "ultravisor-beacon": "^0.0.8",
@@ -989,6 +989,23 @@ class UltravisorBeaconCoordinator extends libPictService
989
989
  {
990
990
  this._tryPushToWebSocketBeacon(tmpWorkItem);
991
991
  }
992
+ else if (tmpWorkItem.Status === 'Assigned' && tmpWorkItem.AssignedBeaconID && this._WorkItemPushHandler)
993
+ {
994
+ // Affinity pre-assigned — push directly to the assigned beacon via WebSocket
995
+ tmpWorkItem.Status = 'Running';
996
+ let tmpPushed = this._WorkItemPushHandler(tmpWorkItem.AssignedBeaconID,
997
+ this._sanitizeWorkItemForBeacon(tmpWorkItem));
998
+
999
+ if (tmpPushed)
1000
+ {
1001
+ this.log.info(`BeaconCoordinator: pushed affinity-assigned work item [${tmpWorkItemHash}] to WebSocket beacon [${tmpWorkItem.AssignedBeaconID}].`);
1002
+ }
1003
+ else
1004
+ {
1005
+ // WebSocket push failed — revert to Assigned for HTTP poll pickup
1006
+ tmpWorkItem.Status = 'Assigned';
1007
+ }
1008
+ }
992
1009
 
993
1010
  return tmpWorkItem;
994
1011
  }
@@ -1005,6 +1005,20 @@ class UltravisorExecutionEngine extends libPictService
1005
1005
 
1006
1006
  if (typeof(tmpVal) === 'string' && tmpVal.indexOf('{~') >= 0)
1007
1007
  {
1008
+ // When the entire value is a single {~D:Record.X~} expression,
1009
+ // resolve via StateManager to preserve non-scalar types
1010
+ // (arrays, objects). parseTemplate always returns strings.
1011
+ let tmpDataMatch = tmpVal.match(/^\{~D:Record\.(.+?)~\}$/);
1012
+ if (tmpDataMatch)
1013
+ {
1014
+ let tmpAddress = tmpDataMatch[1];
1015
+ let tmpResolved = tmpStateManager.resolveAddress(tmpAddress, pContext);
1016
+ if (tmpResolved !== undefined)
1017
+ {
1018
+ tmpSettings[tmpKey] = tmpResolved;
1019
+ continue;
1020
+ }
1021
+ }
1008
1022
  tmpSettings[tmpKey] = this._resolveTemplate(tmpVal, tmpTemplateContext);
1009
1023
  }
1010
1024
  }
@@ -174,6 +174,24 @@ function _handleStepComplete(pContext, fCallback)
174
174
  }
175
175
 
176
176
 
177
+ /**
178
+ * Flatten a parameter set object into string-valued output fields.
179
+ * { seed: 42, guidance: 5.0 } → { seed: "42", guidance: "5.0" }
180
+ * This allows direct state wiring: ParameterSweep.seed → Denoise.seed
181
+ */
182
+ function _flattenParams(pObj)
183
+ {
184
+ let tmpResult = {};
185
+ let tmpKeys = Object.keys(pObj || {});
186
+ for (let i = 0; i < tmpKeys.length; i++)
187
+ {
188
+ let tmpVal = pObj[tmpKeys[i]];
189
+ tmpResult[tmpKeys[i]] = (tmpVal === null || tmpVal === undefined) ? '' : String(tmpVal);
190
+ }
191
+ return tmpResult;
192
+ }
193
+
194
+
177
195
  // ═══════════════════════════════════════════════════════════════════
178
196
  // FLOW CONTROL TASK CONFIGS
179
197
  // ═══════════════════════════════════════════════════════════════════
@@ -244,6 +262,104 @@ module.exports =
244
262
  }
245
263
  },
246
264
 
265
+ // ── parameter-sweep ───────────────────────────────────────
266
+ {
267
+ Definition: require('./definitions/parameter-sweep.json'),
268
+ Execute: function (pTask, pResolvedSettings, pExecutionContext, fCallback)
269
+ {
270
+ if (pExecutionContext.TriggeringEventName === 'StepComplete')
271
+ {
272
+ // Read stored iteration state from prior invocations
273
+ let tmpStoredState = pExecutionContext.TaskOutputs[pExecutionContext.NodeHash] || {};
274
+ let tmpSets = tmpStoredState._ParameterSets;
275
+
276
+ if (!Array.isArray(tmpSets))
277
+ {
278
+ return fCallback(null, {
279
+ EventToFire: 'Error',
280
+ Outputs: { CurrentParameters: '{}', CurrentIndex: 0, TotalCount: 0, CompletedCount: 0 },
281
+ Log: ['StepComplete fired but no stored parameter sets found. Was BeginSweep called?']
282
+ });
283
+ }
284
+
285
+ let tmpCurrentIndex = tmpStoredState.CurrentIndex || 0;
286
+ let tmpCompletedCount = (tmpStoredState.CompletedCount || 0) + 1;
287
+ let tmpNextIndex = tmpCurrentIndex + 1;
288
+ let tmpTotalCount = tmpSets.length;
289
+
290
+ if (tmpNextIndex >= tmpTotalCount)
291
+ {
292
+ let tmpLast = tmpSets[tmpTotalCount - 1] || {};
293
+ let tmpOutputs = Object.assign(
294
+ { _ParameterSets: tmpSets, CurrentParameters: JSON.stringify(tmpLast), CurrentIndex: tmpTotalCount - 1, TotalCount: tmpTotalCount, CompletedCount: tmpCompletedCount },
295
+ _flattenParams(tmpLast));
296
+ return fCallback(null, {
297
+ EventToFire: 'SweepComplete',
298
+ Outputs: tmpOutputs,
299
+ Log: ['Parameter sweep complete. Processed ' + tmpCompletedCount + '/' + tmpTotalCount + ' set(s).']
300
+ });
301
+ }
302
+
303
+ let tmpNext = tmpSets[tmpNextIndex] || {};
304
+ let tmpOutputs = Object.assign(
305
+ { _ParameterSets: tmpSets, CurrentParameters: JSON.stringify(tmpNext), CurrentIndex: tmpNextIndex, TotalCount: tmpTotalCount, CompletedCount: tmpCompletedCount },
306
+ _flattenParams(tmpNext));
307
+ return fCallback(null, {
308
+ EventToFire: 'ParameterSetReady',
309
+ Outputs: tmpOutputs,
310
+ Log: ['Emitting parameter set ' + (tmpNextIndex + 1) + '/' + tmpTotalCount + '.']
311
+ });
312
+ }
313
+
314
+ // BeginSweep — parse the array and emit the first set
315
+ let tmpRawSets = pResolvedSettings.ParameterSets;
316
+ let tmpSets;
317
+ if (typeof tmpRawSets === 'string')
318
+ {
319
+ try { tmpSets = JSON.parse(tmpRawSets); }
320
+ catch (pErr)
321
+ {
322
+ return fCallback(null, {
323
+ EventToFire: 'Error',
324
+ Outputs: { CurrentParameters: '{}', CurrentIndex: 0, TotalCount: 0, CompletedCount: 0 },
325
+ Log: ['ParameterSets is not valid JSON: ' + pErr.message]
326
+ });
327
+ }
328
+ }
329
+ else if (Array.isArray(tmpRawSets))
330
+ {
331
+ tmpSets = tmpRawSets;
332
+ }
333
+ else
334
+ {
335
+ return fCallback(null, {
336
+ EventToFire: 'Error',
337
+ Outputs: { CurrentParameters: '{}', CurrentIndex: 0, TotalCount: 0, CompletedCount: 0 },
338
+ Log: ['ParameterSets must be a JSON array.']
339
+ });
340
+ }
341
+
342
+ if (!Array.isArray(tmpSets) || tmpSets.length === 0)
343
+ {
344
+ return fCallback(null, {
345
+ EventToFire: 'SweepComplete',
346
+ Outputs: { CurrentParameters: '{}', CurrentIndex: 0, TotalCount: 0, CompletedCount: 0 },
347
+ Log: ['ParameterSets is empty. Nothing to sweep.']
348
+ });
349
+ }
350
+
351
+ let tmpFirst = tmpSets[0] || {};
352
+ let tmpOutputs = Object.assign(
353
+ { _ParameterSets: tmpSets, CurrentParameters: JSON.stringify(tmpFirst), CurrentIndex: 0, TotalCount: tmpSets.length, CompletedCount: 0 },
354
+ _flattenParams(tmpFirst));
355
+ return fCallback(null, {
356
+ EventToFire: 'ParameterSetReady',
357
+ Outputs: tmpOutputs,
358
+ Log: ['Parameter sweep started: ' + tmpSets.length + ' set(s). Emitting set 1/' + tmpSets.length + '.']
359
+ });
360
+ }
361
+ },
362
+
247
363
  // ── launch-operation ───────────────────────────────────────
248
364
  {
249
365
  Definition: require('./definitions/launch-operation.json'),
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Parameter Sweep — Iterates over a JSON array of parameter sets.
3
+ *
4
+ * Each entry in the array becomes one execution of downstream tasks.
5
+ * Individual fields from each parameter set are exposed as state outputs
6
+ * so they can be wired into downstream card settings.
7
+ *
8
+ * Graph pattern (identical to split-execute):
9
+ * ParameterSweep.ParameterSetReady → [downstream tasks] → ParameterSweep.StepComplete
10
+ * ParameterSweep.SweepComplete → [whatever follows the loop]
11
+ *
12
+ * Example ParameterSets input:
13
+ * [{"seed": 42, "guidance": 5.0}, {"seed": 123, "guidance": 7.0}]
14
+ *
15
+ * State outputs per iteration:
16
+ * CurrentParameters: '{"seed": 42, "guidance": 5.0}' (full object as JSON string)
17
+ * CurrentIndex: 0
18
+ * TotalCount: 2
19
+ * CompletedCount: 0
20
+ * Plus: each key from the current parameter set is also exposed directly
21
+ * e.g., "seed" = "42", "guidance" = "5.0" (all coerced to strings)
22
+ *
23
+ * @license MIT
24
+ * @author Steven Velozo <steven@velozo.com>
25
+ */
26
+
27
+ 'use strict';
28
+
29
+ const libTaskTypeBase = require('../Ultravisor-TaskType-Base.cjs');
30
+ const libPath = require('path');
31
+
32
+ class TaskTypeParameterSweep extends libTaskTypeBase
33
+ {
34
+ constructor(pFable, pOptions, pServiceHash)
35
+ {
36
+ super(pFable, pOptions, pServiceHash);
37
+ this.serviceType = 'TaskType-ParameterSweep';
38
+ }
39
+
40
+ get definition()
41
+ {
42
+ return require('./definitions/parameter-sweep.json');
43
+ }
44
+
45
+ execute(pResolvedSettings, pExecutionContext, fCallback, fFireIntermediateEvent)
46
+ {
47
+ let tmpTrigger = pExecutionContext.TriggeringEventName || 'BeginSweep';
48
+
49
+ if (tmpTrigger === 'StepComplete')
50
+ {
51
+ return this._handleStepComplete(pResolvedSettings, pExecutionContext, fCallback);
52
+ }
53
+
54
+ return this._handleBeginSweep(pResolvedSettings, pExecutionContext, fCallback);
55
+ }
56
+
57
+ _handleBeginSweep(pSettings, pContext, fCallback)
58
+ {
59
+ let tmpRawSets = pSettings.ParameterSets;
60
+
61
+ // Parse the parameter array
62
+ let tmpSets;
63
+ if (typeof tmpRawSets === 'string')
64
+ {
65
+ try
66
+ {
67
+ tmpSets = JSON.parse(tmpRawSets);
68
+ }
69
+ catch (pError)
70
+ {
71
+ return fCallback(null,
72
+ {
73
+ EventToFire: 'Error',
74
+ Outputs: { CurrentParameters: '{}', CurrentIndex: 0, TotalCount: 0, CompletedCount: 0 },
75
+ Log: ['ParameterSets is not valid JSON: ' + pError.message]
76
+ });
77
+ }
78
+ }
79
+ else if (Array.isArray(tmpRawSets))
80
+ {
81
+ tmpSets = tmpRawSets;
82
+ }
83
+ else
84
+ {
85
+ return fCallback(null,
86
+ {
87
+ EventToFire: 'Error',
88
+ Outputs: { CurrentParameters: '{}', CurrentIndex: 0, TotalCount: 0, CompletedCount: 0 },
89
+ Log: ['ParameterSets must be a JSON array string or an array.']
90
+ });
91
+ }
92
+
93
+ if (!Array.isArray(tmpSets) || tmpSets.length === 0)
94
+ {
95
+ return fCallback(null,
96
+ {
97
+ EventToFire: 'Error',
98
+ Outputs: { CurrentParameters: '{}', CurrentIndex: 0, TotalCount: 0, CompletedCount: 0 },
99
+ Log: ['ParameterSets is empty or not an array.']
100
+ });
101
+ }
102
+
103
+ // Emit the first parameter set
104
+ let tmpFirst = tmpSets[0] || {};
105
+ let tmpOutputs = this._buildOutputs(tmpSets, tmpFirst, 0, 0);
106
+
107
+ return fCallback(null,
108
+ {
109
+ EventToFire: 'ParameterSetReady',
110
+ Outputs: tmpOutputs,
111
+ Log: ['Parameter sweep started: ' + tmpSets.length + ' set(s). Emitting set 1/' + tmpSets.length + '.']
112
+ });
113
+ }
114
+
115
+ _handleStepComplete(pSettings, pContext, fCallback)
116
+ {
117
+ // Read stored iteration state from prior invocations
118
+ let tmpStoredState = pContext.TaskOutputs[pContext.NodeHash] || {};
119
+ let tmpSets = tmpStoredState._ParameterSets;
120
+
121
+ if (!tmpSets || !Array.isArray(tmpSets))
122
+ {
123
+ return fCallback(null,
124
+ {
125
+ EventToFire: 'Error',
126
+ Outputs: { CurrentParameters: '{}', CurrentIndex: 0, TotalCount: 0, CompletedCount: 0 },
127
+ Log: ['StepComplete fired but no stored parameter sets found. Was BeginSweep called?']
128
+ });
129
+ }
130
+
131
+ let tmpCurrentIndex = (tmpStoredState.CurrentIndex || 0);
132
+ let tmpCompletedCount = (tmpStoredState.CompletedCount || 0) + 1;
133
+ let tmpNextIndex = tmpCurrentIndex + 1;
134
+ let tmpTotalCount = tmpSets.length;
135
+
136
+ if (tmpNextIndex >= tmpTotalCount)
137
+ {
138
+ // All parameter sets have been processed
139
+ let tmpLast = tmpSets[tmpTotalCount - 1] || {};
140
+ let tmpOutputs = this._buildOutputs(tmpSets, tmpLast, tmpTotalCount - 1, tmpCompletedCount);
141
+
142
+ return fCallback(null,
143
+ {
144
+ EventToFire: 'SweepComplete',
145
+ Outputs: tmpOutputs,
146
+ Log: ['Parameter sweep complete. Processed ' + tmpCompletedCount + '/' + tmpTotalCount + ' set(s).']
147
+ });
148
+ }
149
+
150
+ // Emit the next parameter set
151
+ let tmpNext = tmpSets[tmpNextIndex] || {};
152
+ let tmpOutputs = this._buildOutputs(tmpSets, tmpNext, tmpNextIndex, tmpCompletedCount);
153
+
154
+ return fCallback(null,
155
+ {
156
+ EventToFire: 'ParameterSetReady',
157
+ Outputs: tmpOutputs,
158
+ Log: ['Emitting parameter set ' + (tmpNextIndex + 1) + '/' + tmpTotalCount + '.']
159
+ });
160
+ }
161
+
162
+ /**
163
+ * Build the output object for a given parameter set.
164
+ * Includes internal state (_ParameterSets), iteration counters,
165
+ * the full current set as JSON, and each individual field flattened.
166
+ */
167
+ _buildOutputs(pSets, pCurrentSet, pIndex, pCompletedCount)
168
+ {
169
+ let tmpOutputs =
170
+ {
171
+ // Internal state (prefixed with _ so it doesn't show in the flow editor)
172
+ _ParameterSets: pSets,
173
+
174
+ // Iteration counters
175
+ CurrentParameters: JSON.stringify(pCurrentSet),
176
+ CurrentIndex: pIndex,
177
+ TotalCount: pSets.length,
178
+ CompletedCount: pCompletedCount
179
+ };
180
+
181
+ // Flatten each field from the current parameter set as a top-level output.
182
+ // This allows direct state wiring: ParameterSweep.seed → Denoise.seed
183
+ // without the downstream card needing to parse JSON.
184
+ let tmpKeys = Object.keys(pCurrentSet);
185
+ for (let i = 0; i < tmpKeys.length; i++)
186
+ {
187
+ let tmpKey = tmpKeys[i];
188
+ let tmpVal = pCurrentSet[tmpKey];
189
+ // Coerce to string for consistent state wiring (the engine handles type coercion downstream)
190
+ tmpOutputs[tmpKey] = (tmpVal === null || tmpVal === undefined) ? '' : String(tmpVal);
191
+ }
192
+
193
+ return tmpOutputs;
194
+ }
195
+ }
196
+
197
+ module.exports = TaskTypeParameterSweep;
@@ -0,0 +1,36 @@
1
+ {
2
+ "Hash": "parameter-sweep",
3
+ "Type": "parameter-sweep",
4
+ "Name": "Parameter Sweep",
5
+ "Description": "Iterates over a JSON array of parameter sets, firing one execution per entry. Each entry's fields are exposed as state outputs for downstream cards. Use for batch experiments with different seeds, models, LoRAs, guidance scales, etc.",
6
+ "Category": "flow-control",
7
+ "Capability": "Flow Control",
8
+ "Action": "ParameterSweep",
9
+ "Tier": "Engine",
10
+
11
+ "EventInputs": [
12
+ { "Name": "BeginSweep", "Description": "Start iterating through the parameter array" },
13
+ { "Name": "StepComplete", "Description": "Signal that the current parameter set has been fully processed by downstream tasks" }
14
+ ],
15
+
16
+ "EventOutputs": [
17
+ { "Name": "ParameterSetReady", "Description": "Fired for each parameter set in the array. Downstream tasks receive the current parameters via state connections." },
18
+ { "Name": "SweepComplete", "Description": "Fired after all parameter sets have been processed." },
19
+ { "Name": "Error", "Description": "Fired if the parameter array is invalid or empty.", "IsError": true }
20
+ ],
21
+
22
+ "SettingsInputs": [
23
+ { "Name": "ParameterSets", "DataType": "String", "Required": true, "Default": "[{\"seed\": 42}, {\"seed\": 123}, {\"seed\": 456}]", "Description": "JSON array of objects. Each object is one set of parameters to sweep through. Example: [{\"seed\": 42, \"guidance\": 5.0}, {\"seed\": 123, \"guidance\": 7.0}]" }
24
+ ],
25
+
26
+ "StateOutputs": [
27
+ { "Name": "CurrentParameters", "DataType": "String", "Description": "JSON string of the current parameter set object (e.g., {\"seed\": 42, \"guidance\": 5.0})" },
28
+ { "Name": "CurrentIndex", "DataType": "Number", "Description": "0-based index of the current parameter set" },
29
+ { "Name": "TotalCount", "DataType": "Number", "Description": "Total number of parameter sets in the array" },
30
+ { "Name": "CompletedCount", "DataType": "Number", "Description": "Number of parameter sets fully processed so far" }
31
+ ],
32
+
33
+ "DefaultSettings": {
34
+ "ParameterSets": "[{\"seed\": 42}, {\"seed\": 123}, {\"seed\": 456}]"
35
+ }
36
+ }
@@ -94,7 +94,26 @@ module.exports =
94
94
  let tmpPromptMessage = pResolvedSettings.PromptMessage || 'Please provide a value:';
95
95
  let tmpOutputAddress = pResolvedSettings.OutputAddress || '';
96
96
 
97
- // Options: accept array (accumulator pattern) or JSON string (legacy)
97
+ // Auto-resolve: if the output address already has a value in state
98
+ // (e.g., pre-seeded via /Operation/:Hash/Trigger with Parameters),
99
+ // skip the pause and fire immediately. This lets operations work both
100
+ // interactively (flow editor — pauses for input) and programmatically
101
+ // (API trigger / retold-labs experiment runner — runs straight through).
102
+ if (tmpOutputAddress && pExecutionContext.StateManager)
103
+ {
104
+ let tmpExistingValue = pExecutionContext.StateManager.resolveAddress(
105
+ tmpOutputAddress, pExecutionContext, pExecutionContext.NodeHash);
106
+ if (tmpExistingValue !== undefined && tmpExistingValue !== null && tmpExistingValue !== '')
107
+ {
108
+ return fCallback(null, {
109
+ EventToFire: 'ValueInputComplete',
110
+ Outputs: { InputValue: tmpExistingValue },
111
+ Log: [`Auto-resolved from pre-seeded state: "${tmpOutputAddress}" = "${String(tmpExistingValue).substring(0, 100)}"`]
112
+ });
113
+ }
114
+ }
115
+
116
+ // No pre-seeded value — pause and wait for interactive input
98
117
  let tmpOptions = pResolvedSettings.Options || '';
99
118
  if (Array.isArray(tmpOptions))
100
119
  {
@@ -190,7 +190,7 @@ suite
190
190
  Expect(tmpInstance.definition.Hash).to.equal('read-file');
191
191
 
192
192
  let tmpDefs = tmpRegistry.listDefinitions();
193
- Expect(tmpDefs.length).to.equal(53);
193
+ Expect(tmpDefs.length).to.equal(54);
194
194
 
195
195
  // Verify all registered definitions have Capability, Action, and Tier
196
196
  for (let i = 0; i < tmpDefs.length; i++)
@@ -1806,7 +1806,7 @@ suite
1806
1806
  let tmpBuiltInConfigs = require('../source/services/tasks/Ultravisor-BuiltIn-TaskConfigs.cjs');
1807
1807
  let tmpCount = tmpRegistry.registerTaskTypesFromConfigArray(tmpBuiltInConfigs);
1808
1808
 
1809
- Expect(tmpCount).to.equal(53);
1809
+ Expect(tmpCount).to.equal(54);
1810
1810
 
1811
1811
  // Spot-check a few
1812
1812
  Expect(tmpRegistry.hasTaskType('error-message')).to.equal(true);
@@ -1991,7 +1991,7 @@ suite
1991
1991
 
1992
1992
  // Configs already registered by createTestFable — verify all present
1993
1993
  let tmpDefs = tmpRegistry.listDefinitions();
1994
- Expect(tmpDefs.length).to.equal(53);
1994
+ Expect(tmpDefs.length).to.equal(54);
1995
1995
  }
1996
1996
  );
1997
1997
  }
@@ -36,10 +36,6 @@
36
36
  "whenFileExists": "overwrite"
37
37
  },
38
38
  "copyFiles": [
39
- {
40
- "from": "./ultravisor-webinterface*",
41
- "to": "./dist/"
42
- },
43
39
  {
44
40
  "from": "./html/*",
45
41
  "to": "./dist/"