jsfe 0.8.0 → 0.9.1
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 +403 -17
- package/dist/index.d.ts +26 -40
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +856 -1021
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -302,6 +302,66 @@ export function getFlowPrompt(engine, flowName) {
|
|
|
302
302
|
// Fallback to default prompt or flow name
|
|
303
303
|
return flow.prompt || flow.name;
|
|
304
304
|
}
|
|
305
|
+
// Pure function-based transaction management (no methods, always serializable)
|
|
306
|
+
export class TransactionManager {
|
|
307
|
+
static create(flowName, initiator, userId) {
|
|
308
|
+
return {
|
|
309
|
+
id: crypto.randomUUID(),
|
|
310
|
+
flowName: flowName,
|
|
311
|
+
initiator: initiator,
|
|
312
|
+
userId: userId,
|
|
313
|
+
steps: [],
|
|
314
|
+
state: 'active',
|
|
315
|
+
createdAt: new Date(),
|
|
316
|
+
metadata: {}
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
static addStep(transaction, step, result, duration, status = 'success') {
|
|
320
|
+
const transactionStep = {
|
|
321
|
+
stepId: step.id || crypto.randomUUID(),
|
|
322
|
+
stepType: step.type || 'unknown',
|
|
323
|
+
timestamp: new Date(),
|
|
324
|
+
duration: duration,
|
|
325
|
+
status: status,
|
|
326
|
+
result: TransactionManager.sanitizeForLog(result)
|
|
327
|
+
};
|
|
328
|
+
if (status === 'error' && result instanceof Error) {
|
|
329
|
+
transactionStep.error = result.message;
|
|
330
|
+
}
|
|
331
|
+
transaction.steps.push(transactionStep);
|
|
332
|
+
}
|
|
333
|
+
static addError(transaction, step, error, duration) {
|
|
334
|
+
TransactionManager.addStep(transaction, step, error, duration, 'error');
|
|
335
|
+
}
|
|
336
|
+
static sanitizeForLog(data) {
|
|
337
|
+
if (typeof data === 'string') {
|
|
338
|
+
return data.length > 500 ? data.substring(0, 500) + '...' : data;
|
|
339
|
+
}
|
|
340
|
+
if (typeof data === 'object' && data !== null) {
|
|
341
|
+
try {
|
|
342
|
+
const jsonStr = JSON.stringify(data);
|
|
343
|
+
return jsonStr.length > 500 ? jsonStr.substring(0, 500) + '...' : data;
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return '[Circular or non-serializable object]';
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return data;
|
|
350
|
+
}
|
|
351
|
+
static rollback(transaction) {
|
|
352
|
+
transaction.state = 'rolled_back';
|
|
353
|
+
transaction.rolledBackAt = new Date();
|
|
354
|
+
}
|
|
355
|
+
static complete(transaction) {
|
|
356
|
+
transaction.state = 'completed';
|
|
357
|
+
transaction.completedAt = new Date();
|
|
358
|
+
}
|
|
359
|
+
static fail(transaction, reason) {
|
|
360
|
+
transaction.state = 'failed';
|
|
361
|
+
transaction.failedAt = new Date();
|
|
362
|
+
transaction.failureReason = reason;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
305
365
|
// Initialize JSON Schema validator
|
|
306
366
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
307
367
|
addFormats(ajv);
|
|
@@ -318,6 +378,7 @@ function isPathTraversableObject(val) {
|
|
|
318
378
|
* @param data - Source data to transform (typically API response)
|
|
319
379
|
* @param mappingConfig - Mapping configuration specifying how to transform the data
|
|
320
380
|
* @param args - Arguments available for template variables and $args references
|
|
381
|
+
* @param engine - Engine instance for context and utilities
|
|
321
382
|
* @returns Transformed data according to mapping configuration
|
|
322
383
|
*
|
|
323
384
|
* @example
|
|
@@ -335,7 +396,7 @@ function isPathTraversableObject(val) {
|
|
|
335
396
|
* ) // "Name: Alice, Age: 25"
|
|
336
397
|
* ```
|
|
337
398
|
*/
|
|
338
|
-
function applyResponseMapping(data, mappingConfig, args = {}) {
|
|
399
|
+
function applyResponseMapping(data, mappingConfig, args = {}, engine) {
|
|
339
400
|
logger.debug(`applyResponseMapping called with: dataType=${typeof data}, data=${JSON.stringify(data)}, mappingConfig=${JSON.stringify(mappingConfig)}, args=${JSON.stringify(args)}`);
|
|
340
401
|
if (!mappingConfig) {
|
|
341
402
|
logger.debug(`No mapping config provided, returning original data`);
|
|
@@ -355,13 +416,13 @@ function applyResponseMapping(data, mappingConfig, args = {}) {
|
|
|
355
416
|
case 'jsonPath':
|
|
356
417
|
return applyJsonPathMapping(data, mappingConfig, args);
|
|
357
418
|
case 'object':
|
|
358
|
-
return applyObjectMapping(data, mappingConfig, args);
|
|
419
|
+
return applyObjectMapping(data, mappingConfig, args, engine);
|
|
359
420
|
case 'array':
|
|
360
|
-
return applyArrayMapping(data, mappingConfig, args);
|
|
421
|
+
return applyArrayMapping(data, mappingConfig, args, engine);
|
|
361
422
|
case 'template':
|
|
362
|
-
return applyTemplateMapping(data, mappingConfig, args);
|
|
423
|
+
return applyTemplateMapping(data, mappingConfig, args, engine);
|
|
363
424
|
case 'conditional':
|
|
364
|
-
return applyConditionalMapping(data, mappingConfig, args);
|
|
425
|
+
return applyConditionalMapping(data, mappingConfig, args, engine);
|
|
365
426
|
}
|
|
366
427
|
}
|
|
367
428
|
// Handle PathConfig (has path property but not type)
|
|
@@ -468,7 +529,7 @@ function applyJsonPathMapping(data, config, args) {
|
|
|
468
529
|
return result;
|
|
469
530
|
}
|
|
470
531
|
// Object structure mapping with nested transformations
|
|
471
|
-
function applyObjectMapping(data, config, args) {
|
|
532
|
+
function applyObjectMapping(data, config, args, engine) {
|
|
472
533
|
const result = {};
|
|
473
534
|
for (const [key, value] of Object.entries(config.mappings)) {
|
|
474
535
|
try {
|
|
@@ -478,7 +539,7 @@ function applyObjectMapping(data, config, args) {
|
|
|
478
539
|
}
|
|
479
540
|
else if (typeof value === 'object' && value !== null && 'type' in value) {
|
|
480
541
|
// Nested mapping with type
|
|
481
|
-
result[key] = applyResponseMapping(data, value, args);
|
|
542
|
+
result[key] = applyResponseMapping(data, value, args, engine);
|
|
482
543
|
}
|
|
483
544
|
else if (typeof value === 'object' && value !== null && 'path' in value) {
|
|
484
545
|
// Object with path and optional transform
|
|
@@ -496,7 +557,7 @@ function applyObjectMapping(data, config, args) {
|
|
|
496
557
|
}
|
|
497
558
|
else if (typeof value === 'object' && value !== null) {
|
|
498
559
|
// Static object with potential interpolation
|
|
499
|
-
result[key] = interpolateObject(value, data, args);
|
|
560
|
+
result[key] = interpolateObject(value, data, args, engine);
|
|
500
561
|
}
|
|
501
562
|
else {
|
|
502
563
|
// Literal value
|
|
@@ -517,7 +578,7 @@ function applyObjectMapping(data, config, args) {
|
|
|
517
578
|
return result;
|
|
518
579
|
}
|
|
519
580
|
// Array transformation and filtering
|
|
520
|
-
function applyArrayMapping(data, config, args) {
|
|
581
|
+
function applyArrayMapping(data, config, args, engine) {
|
|
521
582
|
const sourceArray = config.source ? extractByPath(data, config.source) : data;
|
|
522
583
|
if (!Array.isArray(sourceArray)) {
|
|
523
584
|
logger.warn(`Array mapping source is not an array:`, sourceArray);
|
|
@@ -541,7 +602,7 @@ function applyArrayMapping(data, config, args) {
|
|
|
541
602
|
if (config.itemMapping) {
|
|
542
603
|
result = result.map((item, index) => {
|
|
543
604
|
try {
|
|
544
|
-
return applyResponseMapping(item, config.itemMapping, { ...args, index });
|
|
605
|
+
return applyResponseMapping(item, config.itemMapping, { ...args, index }, engine);
|
|
545
606
|
}
|
|
546
607
|
catch (error) {
|
|
547
608
|
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
@@ -584,7 +645,7 @@ function applyArrayMapping(data, config, args) {
|
|
|
584
645
|
return result;
|
|
585
646
|
}
|
|
586
647
|
// Template-based string interpolation
|
|
587
|
-
function applyTemplateMapping(data, config, args) {
|
|
648
|
+
function applyTemplateMapping(data, config, args, engine) {
|
|
588
649
|
let template = config.template;
|
|
589
650
|
// Check if template contains complex Handlebars syntax
|
|
590
651
|
const hasHandlebarsIteration = /\{\{#each\s+[^}]+\}\}/.test(template);
|
|
@@ -606,7 +667,7 @@ function applyTemplateMapping(data, config, args) {
|
|
|
606
667
|
return template;
|
|
607
668
|
}
|
|
608
669
|
// Conditional mapping based on data content
|
|
609
|
-
function applyConditionalMapping(data, config, args) {
|
|
670
|
+
function applyConditionalMapping(data, config, args, engine) {
|
|
610
671
|
if (!config.conditions || !Array.isArray(config.conditions)) {
|
|
611
672
|
throw new Error('Conditional mapping requires a conditions array');
|
|
612
673
|
}
|
|
@@ -625,7 +686,7 @@ function applyConditionalMapping(data, config, args) {
|
|
|
625
686
|
conditionResult = evaluateCondition(data, condition.if);
|
|
626
687
|
}
|
|
627
688
|
if (conditionResult) {
|
|
628
|
-
return applyResponseMapping(data, condition.then, args);
|
|
689
|
+
return applyResponseMapping(data, condition.then, args, engine);
|
|
629
690
|
}
|
|
630
691
|
}
|
|
631
692
|
catch (error) {
|
|
@@ -636,7 +697,7 @@ function applyConditionalMapping(data, config, args) {
|
|
|
636
697
|
}
|
|
637
698
|
// Default case
|
|
638
699
|
if (config.else) {
|
|
639
|
-
return applyResponseMapping(data, config.else, args);
|
|
700
|
+
return applyResponseMapping(data, config.else, args, engine);
|
|
640
701
|
}
|
|
641
702
|
return data;
|
|
642
703
|
}
|
|
@@ -1153,6 +1214,7 @@ function evaluateCondition(data, condition) {
|
|
|
1153
1214
|
if (!condition || typeof condition !== 'object')
|
|
1154
1215
|
return false;
|
|
1155
1216
|
const { field, operator, value } = condition;
|
|
1217
|
+
logger.debug(`evaluateCondition called with field: ${field}, operator: ${operator}, value: ${value}`);
|
|
1156
1218
|
try {
|
|
1157
1219
|
let fieldValue = undefined;
|
|
1158
1220
|
if (isPathTraversableObject(data)) {
|
|
@@ -1161,44 +1223,61 @@ function evaluateCondition(data, condition) {
|
|
|
1161
1223
|
switch (operator) {
|
|
1162
1224
|
case 'equals':
|
|
1163
1225
|
case 'eq':
|
|
1226
|
+
logger.debug(`evaluateCondition: checking equality for field ${field} with value ${value}`);
|
|
1164
1227
|
return fieldValue === value;
|
|
1165
1228
|
case 'notEquals':
|
|
1166
1229
|
case 'ne':
|
|
1230
|
+
logger.debug(`evaluateCondition: checking inequality for field ${field} with value ${value}`);
|
|
1167
1231
|
return fieldValue !== value;
|
|
1168
1232
|
case 'contains':
|
|
1233
|
+
logger.debug(`evaluateCondition: checking containment for field ${field} with value ${value}`);
|
|
1169
1234
|
return String(fieldValue).includes(String(value));
|
|
1170
1235
|
case 'exists':
|
|
1236
|
+
logger.debug(`evaluateCondition: checking existence for field ${field}`);
|
|
1171
1237
|
return fieldValue !== null && fieldValue !== undefined;
|
|
1172
1238
|
case 'notExists':
|
|
1239
|
+
logger.debug(`evaluateCondition: checking non-existence for field ${field}`);
|
|
1173
1240
|
return fieldValue === null || fieldValue === undefined;
|
|
1174
1241
|
case 'greaterThan':
|
|
1175
1242
|
case 'gt':
|
|
1243
|
+
logger.debug(`evaluateCondition: checking greaterThan for field ${field} with value ${value}`);
|
|
1176
1244
|
return Number(fieldValue) > Number(value);
|
|
1177
1245
|
case 'lessThan':
|
|
1178
1246
|
case 'lt':
|
|
1247
|
+
logger.debug(`evaluateCondition: checking lessThan for field ${field} with value ${value}`);
|
|
1179
1248
|
return Number(fieldValue) < Number(value);
|
|
1180
1249
|
case 'greaterThanOrEqual':
|
|
1181
1250
|
case 'gte':
|
|
1251
|
+
logger.debug(`evaluateCondition: checking greaterThanOrEqual for field ${field} with value ${value}`);
|
|
1182
1252
|
return Number(fieldValue) >= Number(value);
|
|
1183
1253
|
case 'lessThanOrEqual':
|
|
1184
1254
|
case 'lte':
|
|
1255
|
+
logger.debug(`evaluateCondition: checking lessThanOrEqual for field ${field} with value ${value}`);
|
|
1185
1256
|
return Number(fieldValue) <= Number(value);
|
|
1186
1257
|
case 'startsWith':
|
|
1258
|
+
logger.debug(`evaluateCondition: checking startsWith for field ${field} with value ${value}`);
|
|
1187
1259
|
return String(fieldValue).startsWith(String(value));
|
|
1188
1260
|
case 'endsWith':
|
|
1261
|
+
logger.debug(`evaluateCondition: checking endsWith for field ${field} with value ${value}`);
|
|
1189
1262
|
return String(fieldValue).endsWith(String(value));
|
|
1190
1263
|
case 'matches':
|
|
1264
|
+
logger.debug(`evaluateCondition: checking regex match for field ${field} with value ${value}`);
|
|
1191
1265
|
return new RegExp(String(value)).test(String(fieldValue));
|
|
1192
1266
|
case 'in':
|
|
1267
|
+
logger.debug(`evaluateCondition: checking inclusion for field ${field} with value ${value}`);
|
|
1193
1268
|
return Array.isArray(value) && value.includes(fieldValue);
|
|
1194
1269
|
case 'hasLength':
|
|
1195
1270
|
const length = Array.isArray(fieldValue) ? fieldValue.length : String(fieldValue).length;
|
|
1271
|
+
logger.debug(`evaluateCondition: checking length for field ${field} with value ${value}, actual length: ${length}`);
|
|
1196
1272
|
return length === Number(value);
|
|
1197
1273
|
case 'isArray':
|
|
1274
|
+
logger.debug(`evaluateCondition: checking if field ${field} is an array`);
|
|
1198
1275
|
return Array.isArray(fieldValue);
|
|
1199
1276
|
case 'isObject':
|
|
1277
|
+
logger.debug(`evaluateCondition: checking if field ${field} is an object`);
|
|
1200
1278
|
return typeof fieldValue === 'object' && fieldValue !== null && !Array.isArray(fieldValue);
|
|
1201
1279
|
case 'isString':
|
|
1280
|
+
logger.debug(`evaluateCondition: checking if field ${field} is a string`);
|
|
1202
1281
|
return typeof fieldValue === 'string';
|
|
1203
1282
|
case 'isNumber':
|
|
1204
1283
|
return typeof fieldValue === 'number' && !isNaN(fieldValue);
|
|
@@ -1212,14 +1291,58 @@ function evaluateCondition(data, condition) {
|
|
|
1212
1291
|
return false;
|
|
1213
1292
|
}
|
|
1214
1293
|
}
|
|
1215
|
-
function interpolateObject(obj, data, args = {}) {
|
|
1294
|
+
function interpolateObject(obj, data, args = {}, engine) {
|
|
1216
1295
|
if (typeof obj === 'string') {
|
|
1217
|
-
//
|
|
1296
|
+
// Check if the string is ONLY a single template expression (e.g., '{{cargo}}')
|
|
1297
|
+
const singleTemplateMatch = obj.match(/^\{\{([^}]+)\}\}$/);
|
|
1298
|
+
if (singleTemplateMatch) {
|
|
1299
|
+
// This is a single template expression - return the actual value, not a string conversion
|
|
1300
|
+
const path = singleTemplateMatch[1].trim();
|
|
1301
|
+
try {
|
|
1302
|
+
let value = undefined;
|
|
1303
|
+
if (isPathTraversableObject(data)) {
|
|
1304
|
+
value = extractByPath(data, path);
|
|
1305
|
+
// Handle user input wrapper objects
|
|
1306
|
+
if (isUserInputVariable(value)) {
|
|
1307
|
+
value = value.value;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
// If not found in data and engine is available, use the new unified resolver
|
|
1311
|
+
if ((value === undefined || value === null)) {
|
|
1312
|
+
try {
|
|
1313
|
+
// Use the new resolveVariablePath function which handles all variable types
|
|
1314
|
+
value = resolveVariablePath(path, args, [], engine);
|
|
1315
|
+
}
|
|
1316
|
+
catch (error) {
|
|
1317
|
+
// Ignore variable resolution errors
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return value !== undefined && value !== null ? value : '';
|
|
1321
|
+
}
|
|
1322
|
+
catch (error) {
|
|
1323
|
+
return obj; // Keep original on error
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
// Handle template strings with multiple expressions or mixed content
|
|
1218
1327
|
return obj.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
|
|
1219
1328
|
try {
|
|
1220
1329
|
let value = undefined;
|
|
1221
1330
|
if (isPathTraversableObject(data)) {
|
|
1222
1331
|
value = extractByPath(data, path.trim());
|
|
1332
|
+
// Handle user input wrapper objects
|
|
1333
|
+
if (isUserInputVariable(value)) {
|
|
1334
|
+
value = value.value;
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
// If not found in data and engine is available, use the new unified resolver
|
|
1338
|
+
if ((value === undefined || value === null)) {
|
|
1339
|
+
try {
|
|
1340
|
+
// Use the new resolveVariablePath function which handles all variable types
|
|
1341
|
+
value = resolveVariablePath(path.trim(), args, [], engine);
|
|
1342
|
+
}
|
|
1343
|
+
catch (error) {
|
|
1344
|
+
// Ignore variable resolution errors
|
|
1345
|
+
}
|
|
1223
1346
|
}
|
|
1224
1347
|
return value !== null && value !== undefined ? String(value) : '';
|
|
1225
1348
|
}
|
|
@@ -1237,24 +1360,34 @@ function interpolateObject(obj, data, args = {}) {
|
|
|
1237
1360
|
});
|
|
1238
1361
|
}
|
|
1239
1362
|
else if (Array.isArray(obj)) {
|
|
1240
|
-
return obj.map(item => interpolateObject(item, data, args));
|
|
1363
|
+
return obj.map(item => interpolateObject(item, data, args, engine));
|
|
1241
1364
|
}
|
|
1242
1365
|
else if (typeof obj === 'object' && obj !== null) {
|
|
1243
1366
|
const result = {};
|
|
1244
1367
|
for (const [key, value] of Object.entries(obj)) {
|
|
1245
|
-
result[key] = interpolateObject(value, data, args);
|
|
1368
|
+
result[key] = interpolateObject(value, data, args, engine);
|
|
1246
1369
|
}
|
|
1247
1370
|
return result;
|
|
1248
1371
|
}
|
|
1249
1372
|
return obj;
|
|
1250
1373
|
}
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1374
|
+
function makeLogger(level = process.env.LOG_LEVEL || "warn") {
|
|
1375
|
+
const ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
1376
|
+
let current = ORDER[level] ?? ORDER.warn;
|
|
1377
|
+
const allow = (lvl) => ORDER[lvl] >= current;
|
|
1378
|
+
return {
|
|
1379
|
+
setLevel: (lvl) => { current = ORDER[lvl] ?? current; },
|
|
1380
|
+
debug: (...a) => { if (allow("debug"))
|
|
1381
|
+
console.debug(...a); },
|
|
1382
|
+
info: (...a) => { if (allow("info"))
|
|
1383
|
+
console.info(...a); },
|
|
1384
|
+
warn: (...a) => { if (allow("warn"))
|
|
1385
|
+
console.warn(...a); },
|
|
1386
|
+
error: (...a) => { if (allow("error"))
|
|
1387
|
+
console.error(...a); },
|
|
1388
|
+
};
|
|
1389
|
+
}
|
|
1390
|
+
let logger = makeLogger();
|
|
1258
1391
|
// Fallback for any remaining console calls
|
|
1259
1392
|
if (!global.console) {
|
|
1260
1393
|
global.console = {
|
|
@@ -1264,73 +1397,6 @@ if (!global.console) {
|
|
|
1264
1397
|
info: (...args) => logger.info(args.join(' '))
|
|
1265
1398
|
};
|
|
1266
1399
|
}
|
|
1267
|
-
// === TRANSACTION MANAGEMENT CLASS ===
|
|
1268
|
-
class FlowTransaction {
|
|
1269
|
-
constructor(flowName, initiator, userId = 'anonymous') {
|
|
1270
|
-
this.steps = [];
|
|
1271
|
-
this.state = 'active';
|
|
1272
|
-
this.metadata = {};
|
|
1273
|
-
this.id = crypto.randomUUID();
|
|
1274
|
-
this.flowName = flowName;
|
|
1275
|
-
this.initiator = initiator;
|
|
1276
|
-
this.userId = userId;
|
|
1277
|
-
this.steps = [];
|
|
1278
|
-
this.state = 'active';
|
|
1279
|
-
this.createdAt = new Date();
|
|
1280
|
-
this.metadata = {};
|
|
1281
|
-
}
|
|
1282
|
-
addStep(step, result, duration, status = 'success') {
|
|
1283
|
-
this.steps.push({
|
|
1284
|
-
stepId: step.id || step.type,
|
|
1285
|
-
stepType: step.type,
|
|
1286
|
-
tool: step.tool,
|
|
1287
|
-
result: this.sanitizeForLog(result),
|
|
1288
|
-
duration,
|
|
1289
|
-
status,
|
|
1290
|
-
timestamp: new Date(),
|
|
1291
|
-
retryCount: step.retryCount || 0
|
|
1292
|
-
});
|
|
1293
|
-
}
|
|
1294
|
-
addError(step, error, duration) {
|
|
1295
|
-
this.steps.push({
|
|
1296
|
-
stepId: step.id || step.type,
|
|
1297
|
-
stepType: step.type,
|
|
1298
|
-
tool: step.tool,
|
|
1299
|
-
error: error.message,
|
|
1300
|
-
duration,
|
|
1301
|
-
status: 'error',
|
|
1302
|
-
timestamp: new Date()
|
|
1303
|
-
});
|
|
1304
|
-
}
|
|
1305
|
-
sanitizeForLog(data) {
|
|
1306
|
-
if (typeof data === 'object' && data !== null) {
|
|
1307
|
-
const sanitized = { ...data };
|
|
1308
|
-
// Remove sensitive fields
|
|
1309
|
-
delete sanitized.signature;
|
|
1310
|
-
delete sanitized.password;
|
|
1311
|
-
delete sanitized.token;
|
|
1312
|
-
return sanitized;
|
|
1313
|
-
}
|
|
1314
|
-
return data;
|
|
1315
|
-
}
|
|
1316
|
-
rollback() {
|
|
1317
|
-
// In a real implementation, this would execute compensating actions
|
|
1318
|
-
this.state = 'rolled_back';
|
|
1319
|
-
this.rolledBackAt = new Date();
|
|
1320
|
-
auditLogger.logTransactionRollback(this);
|
|
1321
|
-
}
|
|
1322
|
-
complete() {
|
|
1323
|
-
this.state = 'completed';
|
|
1324
|
-
this.completedAt = new Date();
|
|
1325
|
-
auditLogger.logTransactionComplete(this);
|
|
1326
|
-
}
|
|
1327
|
-
fail(reason) {
|
|
1328
|
-
this.state = 'failed';
|
|
1329
|
-
this.failedAt = new Date();
|
|
1330
|
-
this.failureReason = reason;
|
|
1331
|
-
auditLogger.logTransactionFailed(this);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
1400
|
// === COMPREHENSIVE AUDIT LOGGING ===
|
|
1335
1401
|
const auditLogger = {
|
|
1336
1402
|
logFlowStart(flowName, input, userId, transactionId) {
|
|
@@ -1463,11 +1529,8 @@ function checkRateLimit(engine, userId, toolId) {
|
|
|
1463
1529
|
function sanitizeInput(input) {
|
|
1464
1530
|
if (typeof input !== 'string')
|
|
1465
1531
|
return input;
|
|
1466
|
-
//
|
|
1467
|
-
return input.trim()
|
|
1468
|
-
const escapeMap = { '<': '<', '>': '>', '"': '"', "'": ''', '&': '&' };
|
|
1469
|
-
return escapeMap[char];
|
|
1470
|
-
});
|
|
1532
|
+
// Simple trim - no HTML encoding since user input is treated as literal data
|
|
1533
|
+
return input.trim();
|
|
1471
1534
|
}
|
|
1472
1535
|
function validateToolArgs(tool, args) {
|
|
1473
1536
|
if (!tool.parameters)
|
|
@@ -1616,7 +1679,8 @@ function exportConversationHistory(contextStack, format = 'simple') {
|
|
|
1616
1679
|
// Stored in engine.flowStacks for proper context isolation
|
|
1617
1680
|
function initializeFlowStacks(engine) {
|
|
1618
1681
|
try {
|
|
1619
|
-
engine
|
|
1682
|
+
// Use the engine method directly since Engine is now an alias for WorkflowEngine
|
|
1683
|
+
engine.initializeFlowStacks();
|
|
1620
1684
|
}
|
|
1621
1685
|
catch (error) {
|
|
1622
1686
|
logger.error("Failed to initialize flow stacks:", error.message);
|
|
@@ -1631,7 +1695,11 @@ function pushToCurrentStack(engine, flowFrame) {
|
|
|
1631
1695
|
getCurrentStack(engine).push(flowFrame);
|
|
1632
1696
|
}
|
|
1633
1697
|
function popFromCurrentStack(engine) {
|
|
1634
|
-
|
|
1698
|
+
const result = getCurrentStack(engine).pop();
|
|
1699
|
+
if (!result) {
|
|
1700
|
+
throw new Error('Cannot pop from empty stack');
|
|
1701
|
+
}
|
|
1702
|
+
return result;
|
|
1635
1703
|
}
|
|
1636
1704
|
function createNewStack(engine) {
|
|
1637
1705
|
// Create new stack and switch to it
|
|
@@ -1703,7 +1771,7 @@ async function isFlowActivated(input, engine, userId = 'anonymous') {
|
|
|
1703
1771
|
const flowsMenu = engine.flowsMenu;
|
|
1704
1772
|
const flow = await getFlowForInput(input, engine);
|
|
1705
1773
|
if (flow) {
|
|
1706
|
-
const transaction =
|
|
1774
|
+
const transaction = TransactionManager.create(flow.name, 'user-input', userId);
|
|
1707
1775
|
// Prepare tentative flow_init message that will be replaced by SAY-GET guidance if present
|
|
1708
1776
|
const tentativeFlowInit = getSystemMessage(engine, 'flow_init', {
|
|
1709
1777
|
flowName: flow.name,
|
|
@@ -1741,7 +1809,7 @@ async function playFlowFrame(engine) {
|
|
|
1741
1809
|
auditLogger.logFlowExit(currentFlowFrame.flowName, currentFlowFrame.userId, currentFlowFrame.transaction.id, 'max_recursion_depth');
|
|
1742
1810
|
// Do we need this?
|
|
1743
1811
|
//currentFlowFrame.contextStack.push(error.message);
|
|
1744
|
-
currentFlowFrame.transaction
|
|
1812
|
+
TransactionManager.fail(currentFlowFrame.transaction, "Max recursion depth exceeded");
|
|
1745
1813
|
}
|
|
1746
1814
|
throw error;
|
|
1747
1815
|
}
|
|
@@ -1764,10 +1832,11 @@ async function playFlowFrame(engine) {
|
|
|
1764
1832
|
delete currentFlowFrame.pendingVariable;
|
|
1765
1833
|
}
|
|
1766
1834
|
else {
|
|
1767
|
-
// Store user input as variable value
|
|
1835
|
+
// Store user input as variable value with proper sanitization
|
|
1768
1836
|
// (System commands like 'cancel' are already handled before this point)
|
|
1769
|
-
currentFlowFrame.variables
|
|
1770
|
-
|
|
1837
|
+
setUserInputVariable(currentFlowFrame.variables, currentFlowFrame.pendingVariable, userInput, true // Sanitize user input
|
|
1838
|
+
);
|
|
1839
|
+
logger.info(`Stored sanitized user input in variable '${currentFlowFrame.pendingVariable}': "${userInput}"`);
|
|
1771
1840
|
delete currentFlowFrame.pendingVariable;
|
|
1772
1841
|
// Pop the SAY-GET step now that variable assignment is complete
|
|
1773
1842
|
currentFlowFrame.flowStepsStack.pop();
|
|
@@ -1778,7 +1847,7 @@ async function playFlowFrame(engine) {
|
|
|
1778
1847
|
if (currentFlowFrame.flowStepsStack.length === 0) {
|
|
1779
1848
|
logger.info(`Flow ${currentFlowFrame.flowName} completed, popping from stack (steps length: ${currentFlowFrame.flowStepsStack.length})`);
|
|
1780
1849
|
const completedFlow = popFromCurrentStack(engine);
|
|
1781
|
-
completedFlow.transaction
|
|
1850
|
+
TransactionManager.complete(completedFlow.transaction);
|
|
1782
1851
|
// When a flow completes, it doesn't "return" a value in the traditional sense.
|
|
1783
1852
|
// It communicates results by setting variables in the shared `variables` object,
|
|
1784
1853
|
// which are then accessible to the parent flow.
|
|
@@ -1832,6 +1901,9 @@ async function playFlowFrame(engine) {
|
|
|
1832
1901
|
}
|
|
1833
1902
|
continue;
|
|
1834
1903
|
}
|
|
1904
|
+
else {
|
|
1905
|
+
logger.error(`Failed to resume flow - no flow frames available after switching stacks.`);
|
|
1906
|
+
}
|
|
1835
1907
|
}
|
|
1836
1908
|
else {
|
|
1837
1909
|
logger.info(`No more flow frames to process, all flows completed.`);
|
|
@@ -1846,7 +1918,7 @@ async function playFlowFrame(engine) {
|
|
|
1846
1918
|
try {
|
|
1847
1919
|
const result = await playStep(currentFlowFrame, engine);
|
|
1848
1920
|
const duration = Date.now() - startTime;
|
|
1849
|
-
currentFlowFrame.transaction
|
|
1921
|
+
TransactionManager.addStep(currentFlowFrame.transaction, step, result, duration, 'success');
|
|
1850
1922
|
logger.info(`Step ${step.type} executed successfully, result: ${typeof result === 'object' ? '[object]' : result}`);
|
|
1851
1923
|
// If this was a SAY-GET step, return and wait for user input
|
|
1852
1924
|
if (step.type === 'SAY-GET') {
|
|
@@ -1854,7 +1926,7 @@ async function playFlowFrame(engine) {
|
|
|
1854
1926
|
if (currentFlowFrame.flowStepsStack.length === 0) {
|
|
1855
1927
|
logger.info(`SAY-GET step was final step, flow ${currentFlowFrame.flowName} completed`);
|
|
1856
1928
|
const completedFlow = popFromCurrentStack(engine);
|
|
1857
|
-
completedFlow.transaction
|
|
1929
|
+
TransactionManager.complete(completedFlow.transaction);
|
|
1858
1930
|
return result;
|
|
1859
1931
|
}
|
|
1860
1932
|
return result;
|
|
@@ -1864,7 +1936,7 @@ async function playFlowFrame(engine) {
|
|
|
1864
1936
|
}
|
|
1865
1937
|
catch (error) {
|
|
1866
1938
|
const duration = Date.now() - startTime;
|
|
1867
|
-
currentFlowFrame.transaction
|
|
1939
|
+
TransactionManager.addError(currentFlowFrame.transaction, step, error, duration);
|
|
1868
1940
|
logger.error(`Step ${step.type} failed: ${error.message}`);
|
|
1869
1941
|
logger.info(`Stack trace: ${error.stack}`);
|
|
1870
1942
|
throw error;
|
|
@@ -2078,7 +2150,7 @@ async function getFlowForInput(input, engine) {
|
|
|
2078
2150
|
// === SMART DEFAULT ONFAIL GENERATOR ===
|
|
2079
2151
|
function generateSmartRetryDefaultOnFail(step, error, currentFlowFrame) {
|
|
2080
2152
|
const toolName = step.tool || 'unknown';
|
|
2081
|
-
const errorMessage = error.message || '
|
|
2153
|
+
const errorMessage = error.message.toLocaleLowerCase() || 'unknown error';
|
|
2082
2154
|
const flowName = currentFlowFrame.flowName;
|
|
2083
2155
|
logger.info(`Generating smart default onFail for tool ${toolName}, in flow ${flowName} for error: ${errorMessage}`);
|
|
2084
2156
|
// Categorize error types for intelligent handling
|
|
@@ -2106,7 +2178,8 @@ function generateSmartRetryDefaultOnFail(step, error, currentFlowFrame) {
|
|
|
2106
2178
|
errorMessage.includes('bad request') ||
|
|
2107
2179
|
errorMessage.includes('invalid request') ||
|
|
2108
2180
|
errorMessage.includes('malformed request') ||
|
|
2109
|
-
errorMessage.includes('syntax error')
|
|
2181
|
+
errorMessage.includes('syntax error') ||
|
|
2182
|
+
errorMessage.includes('rate limit');
|
|
2110
2183
|
// Provoke cancelation of the current flow if unrecoverable
|
|
2111
2184
|
const isAuthError = errorMessage.includes('401') || // Unauthorized
|
|
2112
2185
|
errorMessage.includes('402') || // Payment Required
|
|
@@ -2132,7 +2205,7 @@ function generateSmartRetryDefaultOnFail(step, error, currentFlowFrame) {
|
|
|
2132
2205
|
doCancel = true;
|
|
2133
2206
|
}
|
|
2134
2207
|
else {
|
|
2135
|
-
logger.
|
|
2208
|
+
logger.debug(`Unrecognized error type for tool ${toolName} in flow ${flowName}: ${errorMessage}`);
|
|
2136
2209
|
// Default to Cancel for unexpected errors
|
|
2137
2210
|
doCancel = true;
|
|
2138
2211
|
}
|
|
@@ -2258,7 +2331,7 @@ async function performStepInputValidation(step, currentFlowFrame, engine) {
|
|
|
2258
2331
|
// Custom validation function
|
|
2259
2332
|
if (step.inputValidation.customValidator && engine.APPROVED_FUNCTIONS) {
|
|
2260
2333
|
try {
|
|
2261
|
-
const validator = engine.APPROVED_FUNCTIONS
|
|
2334
|
+
const validator = engine.APPROVED_FUNCTIONS[step.inputValidation.customValidator];
|
|
2262
2335
|
if (typeof validator === 'function') {
|
|
2263
2336
|
const customResult = await validator(currentFlowFrame.variables, currentFlowFrame);
|
|
2264
2337
|
if (customResult && typeof customResult === 'object') {
|
|
@@ -2494,18 +2567,29 @@ async function handleToolStep(currentFlowFrame, engine) {
|
|
|
2494
2567
|
const onFailStep = Array.isArray(effectiveOnFail) ? effectiveOnFail[0] : effectiveOnFail;
|
|
2495
2568
|
const callType = onFailStep.callType || "replace"; // Default to current behavior
|
|
2496
2569
|
if (callType === "reboot") {
|
|
2497
|
-
// Clear
|
|
2498
|
-
logger.info(`
|
|
2499
|
-
// Clean up
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2570
|
+
// Clear ALL flows across ALL stacks and start completely fresh with the onFail flow
|
|
2571
|
+
logger.info(`Rebooted due to 'reboot' type of onFail step: ${onFailStep.name} in flow ${currentFlowFrame.flowName} for tool ${step.tool}`);
|
|
2572
|
+
// Clean up ALL existing flows across ALL stacks (using proven user exit pattern)
|
|
2573
|
+
const exitedFlows = [];
|
|
2574
|
+
while (engine.flowStacks.length > 0) {
|
|
2575
|
+
const poppedStack = engine.flowStacks.pop();
|
|
2576
|
+
if (!poppedStack || poppedStack.length === 0) {
|
|
2577
|
+
continue;
|
|
2578
|
+
}
|
|
2579
|
+
// Process all flows in this stack
|
|
2580
|
+
for (const flow of poppedStack) {
|
|
2581
|
+
TransactionManager.fail(flow.transaction, `Rebooted due to 'reboot' type of onFail step: ${onFailStep.name} in flow ${flow?.flowName} for tool ${step.tool}`);
|
|
2582
|
+
exitedFlows.push(flow.flowName);
|
|
2583
|
+
auditLogger.logFlowExit(flow.flowName, currentFlowFrame.userId, flow.transaction.id, 'onFail_reboot');
|
|
2584
|
+
}
|
|
2503
2585
|
}
|
|
2586
|
+
// Initialize completely fresh stack system (using proven pattern)
|
|
2587
|
+
initializeFlowStacks(engine);
|
|
2504
2588
|
// Start the onFail flow as a new root flow
|
|
2505
2589
|
if (onFailStep.type === "FLOW") {
|
|
2506
2590
|
const rebootFlow = flowsMenu?.find(f => f.name === onFailStep.name);
|
|
2507
2591
|
if (rebootFlow) {
|
|
2508
|
-
const transaction =
|
|
2592
|
+
const transaction = TransactionManager.create(rebootFlow.name, 'reboot-recovery', currentFlowFrame.userId);
|
|
2509
2593
|
pushToCurrentStack(engine, {
|
|
2510
2594
|
flowName: rebootFlow.name,
|
|
2511
2595
|
flowId: rebootFlow.id,
|
|
@@ -2525,7 +2609,8 @@ async function handleToolStep(currentFlowFrame, engine) {
|
|
|
2525
2609
|
// Handle non-FLOW onFail steps in reboot mode
|
|
2526
2610
|
currentFlowFrame.flowStepsStack = [onFailStep];
|
|
2527
2611
|
}
|
|
2528
|
-
|
|
2612
|
+
logger.info(`System completely rebooted due to onFail step for tool ${step.tool}. Exited flows: ${exitedFlows.join(', ')}`);
|
|
2613
|
+
return `System rebooted due to onFail step for tool ${step.tool}. Starting recovery flow ${onFailStep.name}`;
|
|
2529
2614
|
}
|
|
2530
2615
|
else if (callType === "replace") {
|
|
2531
2616
|
// Current behavior - replace remaining steps in current flow
|
|
@@ -2543,7 +2628,7 @@ async function handleToolStep(currentFlowFrame, engine) {
|
|
|
2543
2628
|
// Push onFail flow as sub-flow
|
|
2544
2629
|
const onFailFlow = flowsMenu?.find(f => f.name === onFailStep.name);
|
|
2545
2630
|
if (onFailFlow) {
|
|
2546
|
-
const transaction =
|
|
2631
|
+
const transaction = TransactionManager.create(onFailFlow.name, 'onFail-recovery', currentFlowFrame.userId);
|
|
2547
2632
|
pushToCurrentStack(engine, {
|
|
2548
2633
|
flowName: onFailFlow.name,
|
|
2549
2634
|
flowId: onFailFlow.id,
|
|
@@ -2764,8 +2849,13 @@ function handleSetStep(currentFlowFrame, engine) {
|
|
|
2764
2849
|
throw new Error(`SET step requires both 'variable' and 'value' attributes`);
|
|
2765
2850
|
}
|
|
2766
2851
|
// Support interpolation in SET values
|
|
2852
|
+
// Use JavaScript evaluation context for security - strings will be properly quoted
|
|
2767
2853
|
const interpolatedValue = typeof step.value === 'string'
|
|
2768
|
-
?
|
|
2854
|
+
? evaluateExpression(step.value, currentFlowFrame?.variables || {}, [], {
|
|
2855
|
+
securityLevel: 'basic',
|
|
2856
|
+
context: 'javascript-evaluation', // Force JavaScript context for SET values
|
|
2857
|
+
returnType: 'auto'
|
|
2858
|
+
}, engine)
|
|
2769
2859
|
: step.value;
|
|
2770
2860
|
if (currentFlowFrame && currentFlowFrame.variables !== undefined) {
|
|
2771
2861
|
currentFlowFrame.variables[step.variable] = interpolatedValue;
|
|
@@ -2780,17 +2870,21 @@ async function handleSwitchStep(currentFlowFrame, engine) {
|
|
|
2780
2870
|
throw new Error(`SWITCH step requires both 'variable' and 'branches' attributes`);
|
|
2781
2871
|
}
|
|
2782
2872
|
// Get the variable value to switch on
|
|
2783
|
-
|
|
2873
|
+
let switchValue = currentFlowFrame.variables ? currentFlowFrame.variables[step.variable] : undefined;
|
|
2874
|
+
// Handle user input wrapper objects
|
|
2875
|
+
if (isUserInputVariable(switchValue)) {
|
|
2876
|
+
switchValue = switchValue.value;
|
|
2877
|
+
}
|
|
2784
2878
|
logger.info(`SWITCH step: evaluating variable '${step.variable}' with value '${switchValue}'`);
|
|
2785
2879
|
// Find the matching branch (exact value matching only)
|
|
2786
2880
|
let selectedStep = null;
|
|
2787
2881
|
let selectedBranch = null;
|
|
2788
|
-
// SWITCH now
|
|
2882
|
+
// SWITCH now supports exact value matching for strings, booleans, and numbers
|
|
2789
2883
|
// For conditional logic, use the CASE step instead
|
|
2790
|
-
if (switchValue !== undefined &&
|
|
2791
|
-
selectedStep = step.branches[switchValue];
|
|
2884
|
+
if (switchValue !== undefined && step.branches[String(switchValue)]) {
|
|
2885
|
+
selectedStep = step.branches[String(switchValue)];
|
|
2792
2886
|
selectedBranch = String(switchValue);
|
|
2793
|
-
logger.info(`SWITCH: selected exact match branch '${switchValue}'`);
|
|
2887
|
+
logger.info(`SWITCH: selected exact match branch '${switchValue}' (converted to string key '${String(switchValue)}')`);
|
|
2794
2888
|
}
|
|
2795
2889
|
// If no exact match found, use default
|
|
2796
2890
|
if (!selectedStep && step.branches.default) {
|
|
@@ -2880,26 +2974,33 @@ async function handleSubFlowStep(currentFlowFrame, engine) {
|
|
|
2880
2974
|
const input = currentFlowFrame.inputStack[currentFlowFrame.inputStack.length - 1];
|
|
2881
2975
|
const flowsMenu = engine.flowsMenu; // Access the global flows menu
|
|
2882
2976
|
const subFlowName = step.value || step.name || step.nextFlow;
|
|
2883
|
-
const subFlow = flowsMenu?.find(f => f.name === subFlowName);
|
|
2977
|
+
const subFlow = flowsMenu?.find(f => f.name === subFlowName || f.id === subFlowName);
|
|
2884
2978
|
if (!subFlow) {
|
|
2885
2979
|
return getSystemMessage(engine, 'subflow_not_found', { subFlowName });
|
|
2886
2980
|
}
|
|
2887
2981
|
const callType = step.callType || "call"; // Default to normal sub-flow call
|
|
2888
2982
|
logger.info(`Starting sub-flow ${subFlow.name} with callType: ${callType}, input: ${input}`);
|
|
2889
2983
|
if (callType === "reboot") {
|
|
2890
|
-
// Clear
|
|
2891
|
-
logger.info(`Rebooting with flow: ${subFlow.name}`);
|
|
2892
|
-
// Clean up
|
|
2893
|
-
|
|
2894
|
-
|
|
2895
|
-
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
2899
|
-
|
|
2984
|
+
// Clear ALL flows across ALL stacks and start completely fresh
|
|
2985
|
+
logger.info(`Rebooting system with flow: ${subFlow.name}`);
|
|
2986
|
+
// Clean up ALL existing flows across ALL stacks (using proven user exit pattern)
|
|
2987
|
+
const exitedFlows = [];
|
|
2988
|
+
while (engine.flowStacks.length > 0) {
|
|
2989
|
+
const poppedStack = engine.flowStacks.pop();
|
|
2990
|
+
if (!poppedStack || poppedStack.length === 0) {
|
|
2991
|
+
continue;
|
|
2992
|
+
}
|
|
2993
|
+
// Process all flows in this stack
|
|
2994
|
+
for (const flow of poppedStack) {
|
|
2995
|
+
TransactionManager.fail(flow.transaction, `System rebooted to flow ${subFlow.name}`);
|
|
2996
|
+
exitedFlows.push(flow.flowName);
|
|
2997
|
+
auditLogger.logFlowExit(flow.flowName, currentFlowFrame.userId, flow.transaction.id, 'system_reboot');
|
|
2998
|
+
}
|
|
2999
|
+
}
|
|
3000
|
+
// Initialize completely fresh stack system (using proven pattern)
|
|
2900
3001
|
initializeFlowStacks(engine);
|
|
2901
|
-
// Start the
|
|
2902
|
-
const transaction =
|
|
3002
|
+
// Start the reboot flow as the only flow in the system
|
|
3003
|
+
const transaction = TransactionManager.create(subFlow.name, 'reboot', currentFlowFrame.userId);
|
|
2903
3004
|
pushToCurrentStack(engine, {
|
|
2904
3005
|
flowName: subFlow.name,
|
|
2905
3006
|
flowId: subFlow.id,
|
|
@@ -2913,6 +3014,7 @@ async function handleSubFlowStep(currentFlowFrame, engine) {
|
|
|
2913
3014
|
startTime: Date.now()
|
|
2914
3015
|
});
|
|
2915
3016
|
auditLogger.logFlowStart(subFlow.name, input, currentFlowFrame.userId, transaction.id);
|
|
3017
|
+
logger.info(`System completely rebooted to flow ${subFlow.name}. Exited flows: ${exitedFlows.join(', ')}`);
|
|
2916
3018
|
return `System rebooted to flow ${subFlow.name}`;
|
|
2917
3019
|
}
|
|
2918
3020
|
else if (callType === "replace") {
|
|
@@ -2927,15 +3029,15 @@ async function handleSubFlowStep(currentFlowFrame, engine) {
|
|
|
2927
3029
|
addToContextStack(currentFlowFrame.contextStack, 'user', input);
|
|
2928
3030
|
currentFlowFrame.inputStack.push(input);
|
|
2929
3031
|
// Update transaction
|
|
2930
|
-
currentFlowFrame.transaction
|
|
2931
|
-
currentFlowFrame.transaction =
|
|
3032
|
+
TransactionManager.fail(currentFlowFrame.transaction, `Replaced by flow ${subFlow.name}`);
|
|
3033
|
+
currentFlowFrame.transaction = TransactionManager.create(subFlow.name, 'replacement', currentFlowFrame.userId);
|
|
2932
3034
|
auditLogger.logFlowStart(subFlow.name, input, currentFlowFrame.userId, currentFlowFrame.transaction.id);
|
|
2933
3035
|
logger.debug(`About to return from handleSubFlowStep replacement, flowStepsStack length: ${currentFlowFrame.flowStepsStack.length}`);
|
|
2934
3036
|
return `Flow replaced with ${subFlow.name}`;
|
|
2935
3037
|
}
|
|
2936
3038
|
else { // callType === "call" (default)
|
|
2937
3039
|
// Normal sub-flow call - create new transaction for sub-flow
|
|
2938
|
-
const subTransaction =
|
|
3040
|
+
const subTransaction = TransactionManager.create(subFlow.name, 'sub-flow', currentFlowFrame.userId);
|
|
2939
3041
|
// Push sub-flow onto stack - INHERIT parent's variables for unified scope
|
|
2940
3042
|
pushToCurrentStack(engine, {
|
|
2941
3043
|
flowName: subFlow.name,
|
|
@@ -2963,7 +3065,7 @@ async function handleSubFlowStep(currentFlowFrame, engine) {
|
|
|
2963
3065
|
}
|
|
2964
3066
|
}
|
|
2965
3067
|
// === TOOL CALLING AND ARGUMENT GENERATION SYSTEM ===
|
|
2966
|
-
async function generateToolCallAndResponse(engine, toolName, input, contextStack = [], userId = 'anonymous', transactionId = null, flowFrame
|
|
3068
|
+
async function generateToolCallAndResponse(engine, toolName, input, contextStack = [], userId = 'anonymous', transactionId = null, flowFrame, explicitArgs) {
|
|
2967
3069
|
try {
|
|
2968
3070
|
const toolsRegistry = engine.toolsRegistry;
|
|
2969
3071
|
if (!toolsRegistry) {
|
|
@@ -2991,7 +3093,7 @@ async function generateToolCallAndResponse(engine, toolName, input, contextStack
|
|
|
2991
3093
|
const contextStack = flowFrame.contextStack || [];
|
|
2992
3094
|
logger.debug(`Interpolating args templates:`, rawArgs);
|
|
2993
3095
|
logger.debug(`Available variables:`, variables);
|
|
2994
|
-
rawArgs = interpolateObject(rawArgs, variables,
|
|
3096
|
+
rawArgs = interpolateObject(rawArgs, variables, {}, engine);
|
|
2995
3097
|
logger.debug(`Interpolated args:`, rawArgs);
|
|
2996
3098
|
}
|
|
2997
3099
|
catch (error) {
|
|
@@ -3007,7 +3109,7 @@ async function generateToolCallAndResponse(engine, toolName, input, contextStack
|
|
|
3007
3109
|
throw error;
|
|
3008
3110
|
}
|
|
3009
3111
|
}
|
|
3010
|
-
async function generateToolArgs(schema, input, contextStack = [], flowFrame
|
|
3112
|
+
async function generateToolArgs(schema, input, contextStack = [], flowFrame, engine) {
|
|
3011
3113
|
try {
|
|
3012
3114
|
if (!schema || typeof schema !== 'object') {
|
|
3013
3115
|
logger.warn(`Invalid schema provided for argument generation: ${schema}`);
|
|
@@ -3047,7 +3149,7 @@ async function generateToolArgs(schema, input, contextStack = [], flowFrame = nu
|
|
|
3047
3149
|
throw new Error(`Failed to generate tool arguments: ${error.message}`);
|
|
3048
3150
|
}
|
|
3049
3151
|
}
|
|
3050
|
-
async function generateArgsWithAI(schema, input, contextStack, flowFrame
|
|
3152
|
+
async function generateArgsWithAI(schema, input, contextStack, flowFrame, engine) {
|
|
3051
3153
|
const properties = schema.properties || {};
|
|
3052
3154
|
const required = schema.required || [];
|
|
3053
3155
|
const schemaDescription = Object.entries(properties)
|
|
@@ -3134,7 +3236,7 @@ Context Extraction Examples:
|
|
|
3134
3236
|
}
|
|
3135
3237
|
}
|
|
3136
3238
|
// Enhanced pattern-based fallback that uses variables and input parsing
|
|
3137
|
-
function generateEnhancedFallbackArgs(schema, input, flowFrame
|
|
3239
|
+
function generateEnhancedFallbackArgs(schema, input, flowFrame) {
|
|
3138
3240
|
const properties = schema.properties || {};
|
|
3139
3241
|
const required = schema.required || [];
|
|
3140
3242
|
const args = {};
|
|
@@ -3146,6 +3248,11 @@ function generateEnhancedFallbackArgs(schema, input, flowFrame = null) {
|
|
|
3146
3248
|
// Direct variable match
|
|
3147
3249
|
if (flowFrame.variables[key] !== undefined) {
|
|
3148
3250
|
let value = flowFrame.variables[key];
|
|
3251
|
+
// Handle user input wrapper objects
|
|
3252
|
+
if (isUserInputVariable(value)) {
|
|
3253
|
+
const userInputObj = value;
|
|
3254
|
+
value = userInputObj.value;
|
|
3255
|
+
}
|
|
3149
3256
|
// Type conversion if needed
|
|
3150
3257
|
if (prop.type === 'number' && typeof value === 'string') {
|
|
3151
3258
|
const num = parseFloat(value);
|
|
@@ -3156,37 +3263,6 @@ function generateEnhancedFallbackArgs(schema, input, flowFrame = null) {
|
|
|
3156
3263
|
args[key] = value;
|
|
3157
3264
|
continue;
|
|
3158
3265
|
}
|
|
3159
|
-
// Smart matching for account numbers
|
|
3160
|
-
if (key.includes('account')) {
|
|
3161
|
-
const accountVar = Object.keys(flowFrame.variables).find(varName => varName.includes('account') || (typeof flowFrame.variables[varName] === 'object' &&
|
|
3162
|
-
flowFrame.variables[varName] !== null &&
|
|
3163
|
-
flowFrame.variables[varName]?.accountId));
|
|
3164
|
-
if (accountVar) {
|
|
3165
|
-
const accountData = flowFrame.variables[accountVar];
|
|
3166
|
-
if (typeof accountData === 'object' && accountData !== null && 'accountId' in accountData) {
|
|
3167
|
-
args[key] = accountData.accountId;
|
|
3168
|
-
}
|
|
3169
|
-
else if (typeof accountData === 'string') {
|
|
3170
|
-
args[key] = accountData;
|
|
3171
|
-
}
|
|
3172
|
-
}
|
|
3173
|
-
}
|
|
3174
|
-
// Smart matching for amounts
|
|
3175
|
-
if (key.includes('amount')) {
|
|
3176
|
-
const amountVar = Object.keys(flowFrame.variables).find(varName => varName.includes('amount') || varName.includes('payment'));
|
|
3177
|
-
if (amountVar) {
|
|
3178
|
-
const amountData = flowFrame.variables[amountVar];
|
|
3179
|
-
if (typeof amountData === 'number') {
|
|
3180
|
-
args[key] = amountData;
|
|
3181
|
-
}
|
|
3182
|
-
else if (typeof amountData === 'string') {
|
|
3183
|
-
const num = parseFloat(amountData);
|
|
3184
|
-
if (!isNaN(num)) {
|
|
3185
|
-
args[key] = num;
|
|
3186
|
-
}
|
|
3187
|
-
}
|
|
3188
|
-
}
|
|
3189
|
-
}
|
|
3190
3266
|
}
|
|
3191
3267
|
}
|
|
3192
3268
|
// If enhanced fallback didn't find anything useful, try simple pattern matching
|
|
@@ -3249,18 +3325,102 @@ async function callTool(engine, tool, args, userId = 'anonymous', transactionId
|
|
|
3249
3325
|
if (!APPROVED_FUNCTIONS) {
|
|
3250
3326
|
throw new Error(`Approved functions registry not available`);
|
|
3251
3327
|
}
|
|
3252
|
-
const fn = APPROVED_FUNCTIONS
|
|
3328
|
+
const fn = APPROVED_FUNCTIONS[tool.implementation.function];
|
|
3253
3329
|
if (!fn) {
|
|
3254
3330
|
throw new Error(`Function "${tool.implementation.function}" not found in approved functions registry`);
|
|
3255
3331
|
}
|
|
3256
3332
|
// Apply timeout if configured
|
|
3257
3333
|
const timeout = tool.implementation.timeout || 5000;
|
|
3258
|
-
|
|
3334
|
+
let timeoutId;
|
|
3335
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
3336
|
+
timeoutId = setTimeout(() => reject(new Error(`Tool execution timeout after ${timeout}ms - ${tool.implementation.function}`)), timeout);
|
|
3337
|
+
});
|
|
3259
3338
|
try {
|
|
3260
|
-
|
|
3339
|
+
// Determine how to call the function based on its signature and tool schema
|
|
3340
|
+
let result;
|
|
3341
|
+
// Check if the tool schema indicates individual parameters vs object parameter
|
|
3342
|
+
const toolSchema = tool.schema || tool.parameters || tool;
|
|
3343
|
+
// Handle different schema formats:
|
|
3344
|
+
// Format 1: JSONSchema with type="object" -> single object parameter
|
|
3345
|
+
// Format 2: Direct parameters object -> individual parameters
|
|
3346
|
+
let schemaProperties = {};
|
|
3347
|
+
let useObjectParameter = false;
|
|
3348
|
+
if (tool.parameters?.type === "object" && tool.parameters?.properties) {
|
|
3349
|
+
// Format 1: JSONSchema format { type: "object", properties: {...} }
|
|
3350
|
+
// Function expects single object parameter
|
|
3351
|
+
schemaProperties = tool.parameters.properties;
|
|
3352
|
+
useObjectParameter = true;
|
|
3353
|
+
logger.debug(`Detected JSONSchema format (type: object) - will use object parameter`);
|
|
3354
|
+
}
|
|
3355
|
+
else if (tool.parameters && typeof tool.parameters === 'object' && !tool.parameters.type) {
|
|
3356
|
+
// Format 2: Direct parameters { param1: {...}, param2: {...} }
|
|
3357
|
+
// Function expects individual parameters
|
|
3358
|
+
schemaProperties = tool.parameters;
|
|
3359
|
+
useObjectParameter = false;
|
|
3360
|
+
logger.debug(`Detected direct parameters format - will use individual parameters`);
|
|
3361
|
+
}
|
|
3362
|
+
else if (toolSchema?.properties) {
|
|
3363
|
+
// Fallback: Standard JSONSchema format
|
|
3364
|
+
schemaProperties = toolSchema.properties;
|
|
3365
|
+
useObjectParameter = toolSchema.type === "object";
|
|
3366
|
+
logger.debug(`Detected schema.properties format - object parameter: ${useObjectParameter}`);
|
|
3367
|
+
}
|
|
3368
|
+
const propertyNames = Object.keys(schemaProperties);
|
|
3369
|
+
// Get function parameter count
|
|
3370
|
+
const fnParamCount = fn.length;
|
|
3371
|
+
logger.debug(`Function ${tool.implementation.function} parameter count: ${fnParamCount}`);
|
|
3372
|
+
logger.debug(`Schema properties count: ${propertyNames.length}`);
|
|
3373
|
+
logger.debug(`Schema properties: ${propertyNames.join(', ')}`);
|
|
3374
|
+
logger.debug(`Use object parameter: ${useObjectParameter}`);
|
|
3375
|
+
// Strategy 1: If schema explicitly indicates object parameter OR function expects 1 param
|
|
3376
|
+
if (useObjectParameter || fnParamCount === 1) {
|
|
3377
|
+
logger.debug(`Calling function with single object parameter`);
|
|
3378
|
+
result = await Promise.race([fn(args), timeoutPromise]);
|
|
3379
|
+
}
|
|
3380
|
+
// Strategy 2: If function expects multiple parameters and we have multiple schema properties
|
|
3381
|
+
else if (fnParamCount > 1 && propertyNames.length > 1) {
|
|
3382
|
+
logger.debug(`Calling function with individual parameters (${fnParamCount} params expected)`);
|
|
3383
|
+
// For tools with 'parameters' property, use the required array or property order
|
|
3384
|
+
let orderedParamNames;
|
|
3385
|
+
if (tool.required && Array.isArray(tool.required)) {
|
|
3386
|
+
// Use required array order first, then add any optional parameters
|
|
3387
|
+
const optionalParams = propertyNames.filter(name => !tool.required.includes(name));
|
|
3388
|
+
orderedParamNames = [...tool.required, ...optionalParams];
|
|
3389
|
+
}
|
|
3390
|
+
else {
|
|
3391
|
+
// Fall back to property definition order (note: this may not be reliable in all JS engines)
|
|
3392
|
+
orderedParamNames = propertyNames;
|
|
3393
|
+
}
|
|
3394
|
+
logger.debug(`Parameter order: ${orderedParamNames.join(', ')}`);
|
|
3395
|
+
const orderedArgs = orderedParamNames.map(propName => {
|
|
3396
|
+
const value = args[propName];
|
|
3397
|
+
logger.debug(`Parameter ${propName}: ${JSON.stringify(value)}`);
|
|
3398
|
+
return value;
|
|
3399
|
+
});
|
|
3400
|
+
logger.debug(`Calling ${tool.implementation.function}(${orderedArgs.map(arg => typeof arg === 'object' ? '[object]' : arg).join(', ')})`);
|
|
3401
|
+
result = await Promise.race([fn(...orderedArgs), timeoutPromise]);
|
|
3402
|
+
}
|
|
3403
|
+
// Strategy 3: Function expects 0 parameters, call without arguments
|
|
3404
|
+
else if (fnParamCount === 0) {
|
|
3405
|
+
logger.debug(`Calling function with no parameters`);
|
|
3406
|
+
result = await Promise.race([fn(), timeoutPromise]);
|
|
3407
|
+
}
|
|
3408
|
+
// Fallback: Use object approach
|
|
3409
|
+
else {
|
|
3410
|
+
logger.debug(`Calling function with object parameter (fallback)`);
|
|
3411
|
+
result = await Promise.race([fn(args), timeoutPromise]);
|
|
3412
|
+
}
|
|
3413
|
+
// Clear the timeout since function completed successfully
|
|
3414
|
+
if (timeoutId) {
|
|
3415
|
+
clearTimeout(timeoutId);
|
|
3416
|
+
}
|
|
3261
3417
|
return result;
|
|
3262
3418
|
}
|
|
3263
3419
|
catch (error) {
|
|
3420
|
+
// Clear the timeout on error as well
|
|
3421
|
+
if (timeoutId) {
|
|
3422
|
+
clearTimeout(timeoutId);
|
|
3423
|
+
}
|
|
3264
3424
|
// Unconditional Retry logic for local functions
|
|
3265
3425
|
const retries = tool.implementation.retries || 0;
|
|
3266
3426
|
if (retries > 0) {
|
|
@@ -3297,7 +3457,7 @@ async function callHttpTool(tool, args, userId = 'anonymous', transactionId = nu
|
|
|
3297
3457
|
// Apply response mapping if configured
|
|
3298
3458
|
if (implementation.responseMapping) {
|
|
3299
3459
|
try {
|
|
3300
|
-
const mappedResult = applyResponseMapping(mockData, implementation.responseMapping, args);
|
|
3460
|
+
const mappedResult = applyResponseMapping(mockData, implementation.responseMapping, args, engine);
|
|
3301
3461
|
logger.info(`[MOCK] Response mapping applied for ${tool.name}`);
|
|
3302
3462
|
return mappedResult;
|
|
3303
3463
|
}
|
|
@@ -3571,7 +3731,7 @@ async function callHttpTool(tool, args, userId = 'anonymous', transactionId = nu
|
|
|
3571
3731
|
// Apply declarative response mapping if configured (preferred)
|
|
3572
3732
|
if (implementation.responseMapping) {
|
|
3573
3733
|
try {
|
|
3574
|
-
return applyResponseMapping(data, implementation.responseMapping, mappingArgs);
|
|
3734
|
+
return applyResponseMapping(data, implementation.responseMapping, mappingArgs, engine);
|
|
3575
3735
|
}
|
|
3576
3736
|
catch (error) {
|
|
3577
3737
|
logger.error(`Response mapping failed:`, error.message);
|
|
@@ -3735,10 +3895,115 @@ export async function portableHash(data, secret, algorithm = 'SHA-256', encoding
|
|
|
3735
3895
|
return encoding === 'base64' ? arrayBufferToBase64(hash) : Array.from(new Uint8Array(hash)).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
3736
3896
|
}
|
|
3737
3897
|
}
|
|
3738
|
-
|
|
3898
|
+
/**
|
|
3899
|
+
* Resolve variable path like "cargo.callerId" or "user.profile.name"
|
|
3900
|
+
* Supports variables, context stack, engine session variables, and global variables
|
|
3901
|
+
*/
|
|
3902
|
+
function resolveVariablePath(path, variables, contextStack, engine) {
|
|
3903
|
+
const parts = path.split('.');
|
|
3904
|
+
const rootVar = parts[0];
|
|
3905
|
+
// 1. Try to find in flow variables first
|
|
3906
|
+
let rootValue = variables[rootVar];
|
|
3907
|
+
// Handle user input variables as pure literals
|
|
3908
|
+
if (rootValue && typeof rootValue === 'object' && rootValue !== null) {
|
|
3909
|
+
const userInputObj = rootValue;
|
|
3910
|
+
if (userInputObj.__userInput === true && userInputObj.__literal === true) {
|
|
3911
|
+
// Extract the literal value and treat it as a normal variable
|
|
3912
|
+
// This allows normal property access like userInput.length
|
|
3913
|
+
rootValue = userInputObj.value;
|
|
3914
|
+
}
|
|
3915
|
+
else {
|
|
3916
|
+
logger.debug(`resolveVariablePath - Found user input object, but not a literal: ${JSON.stringify(userInputObj)}`);
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
// 2. If not found in variables, try context stack
|
|
3920
|
+
if (rootValue === undefined && contextStack.length > 0) {
|
|
3921
|
+
for (const context of contextStack) {
|
|
3922
|
+
if (context && typeof context === 'object' && rootVar in context) {
|
|
3923
|
+
rootValue = context[rootVar];
|
|
3924
|
+
break;
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
}
|
|
3928
|
+
// 3. If not found and engine available, try engine session variables
|
|
3929
|
+
if (rootValue === undefined && engine) {
|
|
3930
|
+
// Use the centralized session variable logic
|
|
3931
|
+
const sessionVars = getEngineSessionVariables(engine, contextStack);
|
|
3932
|
+
// Handle property access (e.g., cargo.someVar)
|
|
3933
|
+
if (path.includes('.')) {
|
|
3934
|
+
const parts = path.split('.');
|
|
3935
|
+
const baseVariable = parts[0];
|
|
3936
|
+
const propertyPath = parts.slice(1).join('.');
|
|
3937
|
+
// Get the base session variable
|
|
3938
|
+
const baseValue = sessionVars[baseVariable];
|
|
3939
|
+
// If we found a base value, navigate the remaining path
|
|
3940
|
+
if (baseValue !== undefined) {
|
|
3941
|
+
return resolveVariablePath(propertyPath, { root: baseValue }, [], engine);
|
|
3942
|
+
}
|
|
3943
|
+
}
|
|
3944
|
+
else {
|
|
3945
|
+
// Handle direct variable access (no dots)
|
|
3946
|
+
rootValue = sessionVars[path];
|
|
3947
|
+
}
|
|
3948
|
+
}
|
|
3949
|
+
// 4. If not found and engine available, try global variables
|
|
3950
|
+
if (rootValue === undefined && engine?.globalVariables) {
|
|
3951
|
+
rootValue = engine.globalVariables[rootVar];
|
|
3952
|
+
}
|
|
3953
|
+
// If still not found, return undefined
|
|
3954
|
+
if (rootValue === undefined) {
|
|
3955
|
+
return undefined;
|
|
3956
|
+
}
|
|
3957
|
+
// Navigate the remaining path safely
|
|
3958
|
+
let current = rootValue;
|
|
3959
|
+
for (let i = 1; i < parts.length; i++) {
|
|
3960
|
+
if (current === null || current === undefined) {
|
|
3961
|
+
return undefined;
|
|
3962
|
+
}
|
|
3963
|
+
if (typeof current === 'object' && current !== null) {
|
|
3964
|
+
current = current[parts[i]];
|
|
3965
|
+
}
|
|
3966
|
+
else {
|
|
3967
|
+
return undefined;
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
return current;
|
|
3971
|
+
}
|
|
3972
|
+
/**
|
|
3973
|
+
* Check if a variable value is marked as user input (literal data)
|
|
3974
|
+
* User input should never be evaluated as code for security
|
|
3975
|
+
*/
|
|
3976
|
+
function isUserInputVariable(value) {
|
|
3977
|
+
return (value !== null &&
|
|
3978
|
+
typeof value === 'object' &&
|
|
3979
|
+
value.__userInput === true &&
|
|
3980
|
+
value.__literal === true);
|
|
3981
|
+
}
|
|
3982
|
+
/**
|
|
3983
|
+
* Input sanitization for user-provided values
|
|
3984
|
+
* Escapes special characters that could break expression syntax
|
|
3985
|
+
* NOTE: This is secondary defense - primary security is treating user input as literal data
|
|
3986
|
+
*/
|
|
3987
|
+
function sanitizeUserInput(input) {
|
|
3988
|
+
if (typeof input !== 'string') {
|
|
3989
|
+
return String(input);
|
|
3990
|
+
}
|
|
3991
|
+
// Escape special characters that could break expression syntax
|
|
3992
|
+
return input
|
|
3993
|
+
.replace(/\\/g, '\\\\') // Escape backslashes
|
|
3994
|
+
.replace(/'/g, "\\'") // Escape single quotes
|
|
3995
|
+
.replace(/"/g, '\\"') // Escape double quotes
|
|
3996
|
+
.replace(/\n/g, '\\n') // Escape newlines
|
|
3997
|
+
.replace(/\r/g, '\\r') // Escape carriage returns
|
|
3998
|
+
.replace(/\t/g, '\\t'); // Escape tabs
|
|
3999
|
+
}
|
|
4000
|
+
/**
|
|
4001
|
+
* Simplified Expression Evaluator
|
|
4002
|
+
* Replaced complex two-phase parser with regex + JavaScript evaluation for 100% reliability
|
|
4003
|
+
*/
|
|
3739
4004
|
function evaluateExpression(expression, variables = {}, contextStack = [], options = {}, engine) {
|
|
3740
4005
|
const opts = {
|
|
3741
|
-
securityLevel: '
|
|
4006
|
+
securityLevel: 'basic',
|
|
3742
4007
|
allowLogicalOperators: true,
|
|
3743
4008
|
allowMathOperators: true,
|
|
3744
4009
|
allowComparisons: true,
|
|
@@ -3748,330 +4013,56 @@ function evaluateExpression(expression, variables = {}, contextStack = [], optio
|
|
|
3748
4013
|
...options
|
|
3749
4014
|
};
|
|
3750
4015
|
try {
|
|
3751
|
-
logger.debug(`
|
|
3752
|
-
//
|
|
3753
|
-
if (
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
}
|
|
3757
|
-
|
|
3758
|
-
//
|
|
3759
|
-
|
|
3760
|
-
|
|
3761
|
-
|
|
3762
|
-
|
|
3763
|
-
|
|
3764
|
-
|
|
3765
|
-
|
|
3766
|
-
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
}
|
|
3774
|
-
// Now check the processed expression for operators (after template interpolation)
|
|
3775
|
-
// Handle logical AND/OR expressions (if allowed)
|
|
3776
|
-
if (opts.allowLogicalOperators && (processedExpression.includes('&&') || processedExpression.includes('||'))) {
|
|
3777
|
-
const result = evaluateLogicalExpression(processedExpression, variables, contextStack, opts, engine);
|
|
3778
|
-
return convertReturnType(result, opts.returnType);
|
|
3779
|
-
}
|
|
3780
|
-
// Handle comparison expressions (if allowed)
|
|
3781
|
-
if (opts.allowComparisons && containsComparisonOperators(processedExpression)) {
|
|
3782
|
-
const result = evaluateComparisonExpression(processedExpression, variables, contextStack, opts, engine);
|
|
3783
|
-
return convertReturnType(result, opts.returnType);
|
|
3784
|
-
}
|
|
3785
|
-
// Handle ternary expressions (if allowed)
|
|
3786
|
-
if (opts.allowTernary && processedExpression.includes('?') && processedExpression.includes(':')) {
|
|
3787
|
-
const result = evaluateSafeTernaryExpression(processedExpression, variables, contextStack, engine);
|
|
3788
|
-
return convertReturnType(result, opts.returnType);
|
|
3789
|
-
}
|
|
3790
|
-
// Handle mathematical expressions (if allowed)
|
|
3791
|
-
if (opts.allowMathOperators && isMathematicalExpression(processedExpression)) {
|
|
3792
|
-
const result = evaluateSafeMathematicalExpression(processedExpression, variables, contextStack, engine);
|
|
3793
|
-
return convertReturnType(result, opts.returnType);
|
|
3794
|
-
}
|
|
3795
|
-
// Handle simple variable paths (e.g., "user.name", "data.items.length")
|
|
3796
|
-
if (isSimpleVariablePath(processedExpression)) {
|
|
3797
|
-
const result = resolveSimpleVariable(processedExpression, variables, contextStack, engine);
|
|
3798
|
-
return convertReturnType(result, opts.returnType);
|
|
3799
|
-
}
|
|
3800
|
-
// Handle function calls (e.g., "currentTime()", "extractCryptoFromInput(...)")
|
|
3801
|
-
if (processedExpression.includes('(') && processedExpression.includes(')')) {
|
|
3802
|
-
const result = evaluateFunctionCall(processedExpression, variables, contextStack, engine);
|
|
3803
|
-
if (result !== undefined) {
|
|
3804
|
-
return convertReturnType(result, opts.returnType);
|
|
3805
|
-
}
|
|
3806
|
-
}
|
|
3807
|
-
// If no pattern matches and we had template variables, return the interpolated result
|
|
3808
|
-
if (processedExpression !== expression) {
|
|
3809
|
-
return convertReturnType(processedExpression, opts.returnType);
|
|
3810
|
-
}
|
|
3811
|
-
// Fallback: treat as literal
|
|
3812
|
-
const result = expression;
|
|
3813
|
-
return convertReturnType(result, opts.returnType);
|
|
3814
|
-
}
|
|
3815
|
-
catch (error) {
|
|
3816
|
-
logger.warn(`Expression evaluation error in ${opts.context}: ${error.message}`);
|
|
3817
|
-
return opts.returnType === 'boolean' ? false : `[error: ${expression}]`;
|
|
3818
|
-
}
|
|
3819
|
-
}
|
|
3820
|
-
// Unified security pattern checking with configurable levels
|
|
3821
|
-
function containsUnsafePatterns(expression, options, engine) {
|
|
3822
|
-
const { securityLevel = 'standard', allowLogicalOperators = true, allowComparisons = true } = options;
|
|
3823
|
-
// Safe string methods that are allowed in expressions
|
|
3824
|
-
const safeStringMethods = [
|
|
3825
|
-
'toLowerCase', 'toUpperCase', 'trim', 'charAt', 'charCodeAt', 'indexOf', 'lastIndexOf',
|
|
3826
|
-
'substring', 'substr', 'slice', 'split', 'replace', 'match', 'search', 'includes',
|
|
3827
|
-
'startsWith', 'endsWith', 'padStart', 'padEnd', 'repeat', 'toString', 'valueOf',
|
|
3828
|
-
'length', 'concat', 'localeCompare', 'normalize'
|
|
3829
|
-
];
|
|
3830
|
-
// Safe array methods that are allowed in expressions
|
|
3831
|
-
const safeArrayMethods = [
|
|
3832
|
-
'length', 'join', 'indexOf', 'lastIndexOf', 'includes', 'slice', 'toString', 'valueOf'
|
|
3833
|
-
];
|
|
3834
|
-
// Safe math methods that are allowed in expressions
|
|
3835
|
-
const safeMathMethods = [
|
|
3836
|
-
'abs', 'ceil', 'floor', 'round', 'max', 'min', 'pow', 'sqrt', 'random'
|
|
3837
|
-
];
|
|
3838
|
-
// Safe standalone functions that are allowed
|
|
3839
|
-
const safeStandaloneFunctions = [
|
|
3840
|
-
'isNaN', 'isFinite', 'parseInt', 'parseFloat',
|
|
3841
|
-
'encodeURIComponent', 'decodeURIComponent', 'encodeURI', 'decodeURI',
|
|
3842
|
-
'String', 'Number', 'Boolean' // Type conversion functions (not new constructors)
|
|
3843
|
-
];
|
|
3844
|
-
// Check if expression contains only safe function calls
|
|
3845
|
-
const functionCallPattern = /(\w+)\.(\w+)\s*\(/g;
|
|
3846
|
-
const standaloneFunctionPattern = /(?:^|[^.\w])(\w+)\s*\(/g;
|
|
3847
|
-
let match;
|
|
3848
|
-
const foundFunctionCalls = [];
|
|
3849
|
-
// First, check for any standalone function calls (not methods)
|
|
3850
|
-
standaloneFunctionPattern.lastIndex = 0; // Reset regex
|
|
3851
|
-
while ((match = standaloneFunctionPattern.exec(expression)) !== null) {
|
|
3852
|
-
const functionName = match[1];
|
|
3853
|
-
// Skip if this is actually a method call (preceded by a dot)
|
|
3854
|
-
const beforeFunction = expression.substring(0, match.index + match[0].indexOf(functionName));
|
|
3855
|
-
if (!beforeFunction.endsWith('.')) {
|
|
3856
|
-
// Check if this standalone function is in our safe list or approved functions
|
|
3857
|
-
const isApprovedFunction = engine.APPROVED_FUNCTIONS?.get &&
|
|
3858
|
-
typeof engine.APPROVED_FUNCTIONS.get(functionName) === 'function';
|
|
3859
|
-
if (!safeStandaloneFunctions.includes(functionName) && !isApprovedFunction) {
|
|
3860
|
-
logger.debug(`Blocking unsafe standalone function call: ${functionName}`);
|
|
3861
|
-
return true; // Unsafe standalone function calls are not allowed
|
|
3862
|
-
}
|
|
3863
|
-
logger.debug(`Allowing safe standalone function call: ${functionName}${isApprovedFunction ? ' (approved)' : ' (built-in)'}`);
|
|
3864
|
-
}
|
|
3865
|
-
}
|
|
3866
|
-
// Then, check method calls and verify they're safe
|
|
3867
|
-
functionCallPattern.lastIndex = 0; // Reset regex
|
|
3868
|
-
while ((match = functionCallPattern.exec(expression)) !== null) {
|
|
3869
|
-
const methodName = match[2];
|
|
3870
|
-
foundFunctionCalls.push(methodName);
|
|
3871
|
-
// If this method is not in our safe lists, it's potentially unsafe
|
|
3872
|
-
if (!safeStringMethods.includes(methodName) &&
|
|
3873
|
-
!safeArrayMethods.includes(methodName) &&
|
|
3874
|
-
!safeMathMethods.includes(methodName)) {
|
|
3875
|
-
logger.debug(`Blocking unsafe method call: ${methodName}`);
|
|
3876
|
-
return true; // Unsafe method found
|
|
3877
|
-
}
|
|
3878
|
-
}
|
|
3879
|
-
// Core dangerous patterns (blocked at all levels) - removed the problematic function call pattern
|
|
3880
|
-
const coreDangerousPatterns = [
|
|
3881
|
-
/eval\s*\(/, // eval() calls
|
|
3882
|
-
/Function\s*\(/, // Function constructor
|
|
3883
|
-
/constructor/, // Constructor access
|
|
3884
|
-
/prototype/, // Prototype manipulation
|
|
3885
|
-
/__proto__/, // Prototype access
|
|
3886
|
-
/import\s*\(/, // Dynamic imports
|
|
3887
|
-
/require\s*\(/, // CommonJS requires
|
|
3888
|
-
/process\./, // Process access
|
|
3889
|
-
/global\./, // Global access
|
|
3890
|
-
/window\./, // Window access (browser)
|
|
3891
|
-
/document\./, // Document access (browser)
|
|
3892
|
-
/console\./, // Console access
|
|
3893
|
-
/setTimeout/, // Timer functions
|
|
3894
|
-
/setInterval/, // Timer functions
|
|
3895
|
-
/fetch\s*\(/, // Network requests
|
|
3896
|
-
/XMLHttpRequest/, // Network requests
|
|
3897
|
-
/localStorage/, // Storage access
|
|
3898
|
-
/sessionStorage/, // Storage access
|
|
3899
|
-
/\+\s*\+/, // Increment operators
|
|
3900
|
-
/--/, // Decrement operators
|
|
3901
|
-
/(?<![=!<>])=(?!=)/, // Assignment operators = (but not ==, !=, <=, >=)
|
|
3902
|
-
/delete\s+/, // Delete operator
|
|
3903
|
-
/new\s+/, // Constructor calls
|
|
3904
|
-
/throw\s+/, // Throw statements
|
|
3905
|
-
/try\s*\{/, // Try blocks
|
|
3906
|
-
/catch\s*\(/, // Catch blocks
|
|
3907
|
-
/finally\s*\{/, // Finally blocks
|
|
3908
|
-
/for\s*\(/, // For loops
|
|
3909
|
-
/while\s*\(/, // While loops
|
|
3910
|
-
/do\s*\{/, // Do-while loops
|
|
3911
|
-
/switch\s*\(/, // Switch statements
|
|
3912
|
-
/return\s+/, // Return statements
|
|
3913
|
-
];
|
|
3914
|
-
// Additional strict patterns (only blocked in strict mode)
|
|
3915
|
-
const strictOnlyPatterns = [
|
|
3916
|
-
/\[.*\]/, // Array/object bracket notation
|
|
3917
|
-
/this\./, // This access
|
|
3918
|
-
/arguments\./, // Arguments access
|
|
3919
|
-
];
|
|
3920
|
-
// Check core patterns (function call validation is handled separately above)
|
|
3921
|
-
if (coreDangerousPatterns.some(pattern => pattern.test(expression))) {
|
|
3922
|
-
return true;
|
|
3923
|
-
}
|
|
3924
|
-
// Check strict-only patterns if in strict mode
|
|
3925
|
-
if (securityLevel === 'strict' && strictOnlyPatterns.some(pattern => pattern.test(expression))) {
|
|
3926
|
-
return true;
|
|
3927
|
-
}
|
|
3928
|
-
return false;
|
|
3929
|
-
}
|
|
3930
|
-
// Template variable interpolation ({{variable}} format) with nested support
|
|
3931
|
-
// Inside-out approach: process innermost {{}} expressions first, then repeat until no more changes
|
|
3932
|
-
function interpolateTemplateVariables(template, variables, contextStack, options, engine) {
|
|
3933
|
-
logger.debug(`Starting template interpolation: ${template}`);
|
|
3934
|
-
let result = template;
|
|
3935
|
-
let iterations = 0;
|
|
3936
|
-
const maxIterations = 10; // Prevent infinite loops
|
|
3937
|
-
while (result.includes('{{') && result.includes('}}') && iterations < maxIterations) {
|
|
3938
|
-
iterations++;
|
|
3939
|
-
logger.debug(`Template interpolation iteration ${iterations}: ${result}`);
|
|
3940
|
-
// Find the LAST (rightmost) {{ - this is the innermost opening
|
|
3941
|
-
const lastOpenIndex = result.lastIndexOf('{{');
|
|
3942
|
-
if (lastOpenIndex === -1)
|
|
3943
|
-
break;
|
|
3944
|
-
// From that position, find the FIRST }} - this is the matching closing
|
|
3945
|
-
const closeIndex = result.indexOf('}}', lastOpenIndex);
|
|
3946
|
-
if (closeIndex === -1) {
|
|
3947
|
-
logger.error(`Found {{ at ${lastOpenIndex} but no matching }} in: ${result}`);
|
|
3948
|
-
return 'Invalid template!';
|
|
3949
|
-
}
|
|
3950
|
-
// Extract the innermost expression
|
|
3951
|
-
const expression = result.substring(lastOpenIndex + 2, closeIndex).trim();
|
|
3952
|
-
logger.debug(`Found innermost template expression: {{${expression}}}`);
|
|
3953
|
-
// Evaluate the innermost expression
|
|
3954
|
-
const evaluatedContent = evaluateExpression(expression, variables, contextStack, {
|
|
3955
|
-
...options,
|
|
3956
|
-
returnType: 'string'
|
|
3957
|
-
}, engine);
|
|
3958
|
-
const replacement = typeof evaluatedContent === 'string' ? evaluatedContent : String(evaluatedContent);
|
|
3959
|
-
logger.debug(`Evaluated to: ${replacement}`);
|
|
3960
|
-
// Replace the template expression with its evaluated result
|
|
3961
|
-
result = result.substring(0, lastOpenIndex) + replacement + result.substring(closeIndex + 2);
|
|
3962
|
-
logger.debug(`After replacement: ${result}`);
|
|
3963
|
-
}
|
|
3964
|
-
if (iterations >= maxIterations) {
|
|
3965
|
-
logger.warn(`Template interpolation stopped after ${maxIterations} iterations to prevent infinite loop`);
|
|
3966
|
-
}
|
|
3967
|
-
logger.debug(`Final template result: ${result}`);
|
|
3968
|
-
return result;
|
|
3969
|
-
}
|
|
3970
|
-
// Logical expression evaluator (&&, ||)
|
|
3971
|
-
function evaluateLogicalExpression(expression, variables, contextStack, options, engine) {
|
|
3972
|
-
// Handle OR expressions
|
|
3973
|
-
if (expression.includes('||')) {
|
|
3974
|
-
return evaluateSafeOrExpression(expression, variables, contextStack, engine);
|
|
3975
|
-
}
|
|
3976
|
-
// Handle AND expressions
|
|
3977
|
-
if (expression.includes('&&')) {
|
|
3978
|
-
const parts = expression.split('&&').map(part => part.trim());
|
|
3979
|
-
for (const part of parts) {
|
|
3980
|
-
const partResult = evaluateExpression(part, variables, contextStack, {
|
|
3981
|
-
...options,
|
|
3982
|
-
returnType: 'boolean'
|
|
3983
|
-
}, engine);
|
|
3984
|
-
if (!partResult) {
|
|
3985
|
-
return false;
|
|
4016
|
+
logger.debug(`Simplified evaluation starting: ${expression}`);
|
|
4017
|
+
// If not a string, return as-is
|
|
4018
|
+
if (typeof expression !== 'string') {
|
|
4019
|
+
return expression;
|
|
4020
|
+
}
|
|
4021
|
+
// Simple regex to find {{expression}} patterns
|
|
4022
|
+
const expressionRegex = /\{\{([^}]+)\}\}/g;
|
|
4023
|
+
// Create evaluation context by merging all available variables
|
|
4024
|
+
const context = createSimplifiedEvaluationContext(variables, contextStack, engine);
|
|
4025
|
+
// Check if the entire expression is a single interpolation
|
|
4026
|
+
const singleExpressionMatch = expression.match(/^\{\{([^}]+)\}\}$/);
|
|
4027
|
+
if (singleExpressionMatch) {
|
|
4028
|
+
// Single expression - return the evaluated result directly to preserve type
|
|
4029
|
+
try {
|
|
4030
|
+
const evaluationResult = evaluateJavaScriptExpression(singleExpressionMatch[1].trim(), context);
|
|
4031
|
+
logger.debug(`Simplified evaluation complete (single): ${expression} -> ${evaluationResult}`);
|
|
4032
|
+
return convertReturnType(evaluationResult, opts.returnType);
|
|
4033
|
+
}
|
|
4034
|
+
catch (error) {
|
|
4035
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
4036
|
+
logger.warn(`Single expression evaluation failed: ${singleExpressionMatch[1]} - ${errorMessage}`);
|
|
4037
|
+
return opts.returnType === 'boolean' ? false : `[error: ${expression}]`;
|
|
3986
4038
|
}
|
|
3987
4039
|
}
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
// For comparison expressions, we need to handle variable substitution properly
|
|
3995
|
-
let processedExpression = expression;
|
|
3996
|
-
// Find all variable references (not in template format) and replace them
|
|
3997
|
-
// This handles cases like "input.toLowerCase()" where input is a variable
|
|
3998
|
-
const variableNames = Object.keys(variables);
|
|
3999
|
-
for (const varName of variableNames) {
|
|
4000
|
-
const varValue = variables[varName];
|
|
4001
|
-
// Create a regex to match the variable name as a whole word
|
|
4002
|
-
const varRegex = new RegExp(`\\b${varName}\\b`, 'g');
|
|
4003
|
-
// Replace variable references with their values, properly quoted for strings
|
|
4004
|
-
processedExpression = processedExpression.replace(varRegex, (match) => {
|
|
4005
|
-
if (typeof varValue === 'string') {
|
|
4006
|
-
logger.debug(`Replacing variable '${varName}' with string value: ${varValue}`);
|
|
4007
|
-
return `"${varValue}"`;
|
|
4008
|
-
}
|
|
4009
|
-
else if (typeof varValue === 'boolean' || typeof varValue === 'number') {
|
|
4010
|
-
logger.debug(`Replacing variable '${varName}' with value: ${varValue}`);
|
|
4011
|
-
return varValue.toString();
|
|
4012
|
-
}
|
|
4013
|
-
else if (varValue === null || varValue === undefined) {
|
|
4014
|
-
logger.debug(`Replacing variable '${varName}' with null value`);
|
|
4015
|
-
return 'null';
|
|
4040
|
+
// Multiple expressions or mixed content - convert to string
|
|
4041
|
+
const result = expression.replace(expressionRegex, (match, expr) => {
|
|
4042
|
+
try {
|
|
4043
|
+
// Direct JavaScript evaluation with injected variables
|
|
4044
|
+
const evaluationResult = evaluateJavaScriptExpression(expr.trim(), context);
|
|
4045
|
+
return String(evaluationResult);
|
|
4016
4046
|
}
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4047
|
+
catch (error) {
|
|
4048
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
4049
|
+
logger.warn(`Expression evaluation failed: ${expr} - ${errorMessage}`);
|
|
4050
|
+
return match; // Return original if evaluation fails
|
|
4020
4051
|
}
|
|
4021
4052
|
});
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
const variableMatches = processedExpression.match(/\{\{([^}]+)\}\}/g);
|
|
4025
|
-
if (variableMatches) {
|
|
4026
|
-
for (const match of variableMatches) {
|
|
4027
|
-
const varName = match.slice(2, -2).trim();
|
|
4028
|
-
let varValue = getNestedValue(variables, varName);
|
|
4029
|
-
logger.debug(`Resolving template variable '${varName}' with value:`, varValue);
|
|
4030
|
-
// Check engine session variables if not found
|
|
4031
|
-
if (varValue === undefined && engine) {
|
|
4032
|
-
varValue = resolveEngineSessionVariable(varName, engine);
|
|
4033
|
-
}
|
|
4034
|
-
// Convert value to appropriate type for comparison
|
|
4035
|
-
let replacementValue;
|
|
4036
|
-
if (varValue === undefined || varValue === null) {
|
|
4037
|
-
replacementValue = 'null';
|
|
4038
|
-
}
|
|
4039
|
-
else if (typeof varValue === 'string') {
|
|
4040
|
-
replacementValue = `"${varValue}"`;
|
|
4041
|
-
}
|
|
4042
|
-
else if (typeof varValue === 'boolean') {
|
|
4043
|
-
replacementValue = varValue.toString();
|
|
4044
|
-
}
|
|
4045
|
-
else {
|
|
4046
|
-
replacementValue = varValue.toString();
|
|
4047
|
-
}
|
|
4048
|
-
processedExpression = processedExpression.replace(match, replacementValue);
|
|
4049
|
-
}
|
|
4050
|
-
}
|
|
4051
|
-
logger.debug(`Comparison expression after variable substitution: ${processedExpression}`);
|
|
4052
|
-
try {
|
|
4053
|
-
// Use Function constructor for safe evaluation
|
|
4054
|
-
const result = new Function('return ' + processedExpression)();
|
|
4055
|
-
logger.debug(`Comparison evaluation result: ${result}`);
|
|
4056
|
-
return !!result; // Convert to boolean
|
|
4053
|
+
logger.debug(`Simplified evaluation complete: ${expression} -> ${result}`);
|
|
4054
|
+
return convertReturnType(result, opts.returnType);
|
|
4057
4055
|
}
|
|
4058
4056
|
catch (error) {
|
|
4059
|
-
|
|
4060
|
-
|
|
4057
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
4058
|
+
logger.warn(`Simplified evaluation error: ${errorMessage}`);
|
|
4059
|
+
return opts.returnType === 'boolean' ? false : `[error: ${expression}]`;
|
|
4061
4060
|
}
|
|
4062
4061
|
}
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4066
|
-
|
|
4067
|
-
}
|
|
4068
|
-
// Helper function to check for mathematical expressions
|
|
4069
|
-
function isMathematicalExpression(expression) {
|
|
4070
|
-
return /[\+\-\*\/\%]/.test(expression) &&
|
|
4071
|
-
!expression.includes('++') &&
|
|
4072
|
-
!expression.includes('--'); // Exclude increment/decrement
|
|
4073
|
-
}
|
|
4074
|
-
// Convert result to requested return type
|
|
4062
|
+
/**
|
|
4063
|
+
* Convert result to requested return type
|
|
4064
|
+
* Enhanced version with better type handling
|
|
4065
|
+
*/
|
|
4075
4066
|
function convertReturnType(value, returnType) {
|
|
4076
4067
|
switch (returnType) {
|
|
4077
4068
|
case 'boolean':
|
|
@@ -4083,447 +4074,158 @@ function convertReturnType(value, returnType) {
|
|
|
4083
4074
|
return value;
|
|
4084
4075
|
}
|
|
4085
4076
|
}
|
|
4086
|
-
|
|
4077
|
+
/**
|
|
4078
|
+
* Get engine session variables for simplified evaluator
|
|
4079
|
+
* This is the SINGLE SOURCE OF TRUTH for session variable logic
|
|
4080
|
+
*/
|
|
4081
|
+
function getEngineSessionVariables(engine, contextStack) {
|
|
4082
|
+
const sessionVars = {};
|
|
4083
|
+
try {
|
|
4084
|
+
const currentFlowFrame = getCurrentFlowFrame(engine);
|
|
4085
|
+
// Get userInput from the most recent user entry in context stack
|
|
4086
|
+
const userEntries = currentFlowFrame.contextStack.filter(entry => entry.role === 'user');
|
|
4087
|
+
if (userEntries.length > 0) {
|
|
4088
|
+
const lastUserEntry = userEntries[userEntries.length - 1];
|
|
4089
|
+
const userInputValue = typeof lastUserEntry.content === 'string' ? lastUserEntry.content : String(lastUserEntry.content);
|
|
4090
|
+
sessionVars.userInput = userInputValue;
|
|
4091
|
+
sessionVars.lastUserInput = userInputValue; // alias
|
|
4092
|
+
}
|
|
4093
|
+
// Add other engine session variables
|
|
4094
|
+
sessionVars.cargo = engine.cargo || {};
|
|
4095
|
+
sessionVars.sessionId = engine.sessionId;
|
|
4096
|
+
sessionVars.userId = currentFlowFrame.userId;
|
|
4097
|
+
sessionVars.flowName = currentFlowFrame.flowName;
|
|
4098
|
+
// Special function-like variables
|
|
4099
|
+
sessionVars['currentTime()'] = new Date().toISOString();
|
|
4100
|
+
// Add global variables if available
|
|
4101
|
+
if (engine.globalVariables) {
|
|
4102
|
+
Object.assign(sessionVars, engine.globalVariables);
|
|
4103
|
+
}
|
|
4104
|
+
}
|
|
4105
|
+
catch (error) {
|
|
4106
|
+
logger.debug(`Error getting engine session variables: ${error}`);
|
|
4107
|
+
}
|
|
4108
|
+
return sessionVars;
|
|
4109
|
+
}
|
|
4110
|
+
/**
|
|
4111
|
+
* Create evaluation context with all available variables for simplified evaluator
|
|
4112
|
+
*/
|
|
4113
|
+
function createSimplifiedEvaluationContext(variables, contextStack, engine) {
|
|
4114
|
+
// Just use the provided variables - contextStack is primarily for conversation history
|
|
4115
|
+
const allVars = variables || {};
|
|
4116
|
+
// Get engine session variables
|
|
4117
|
+
const engineSessionVars = getEngineSessionVariables(engine, contextStack);
|
|
4118
|
+
const context = {
|
|
4119
|
+
// All variables as top-level properties
|
|
4120
|
+
...allVars,
|
|
4121
|
+
// Engine session variables (userInput, cargo, sessionId, etc.)
|
|
4122
|
+
...engineSessionVars,
|
|
4123
|
+
// Special variables that should always be available
|
|
4124
|
+
$args: variables?.$args || {},
|
|
4125
|
+
xml_result: variables?.xml_result,
|
|
4126
|
+
json_result: variables?.json_result,
|
|
4127
|
+
// Add context object for Object.keys() patterns (avoid 'this' keyword issues)
|
|
4128
|
+
currentContext: { ...allVars, ...engineSessionVars },
|
|
4129
|
+
// Include approved functions so they can be called in expressions
|
|
4130
|
+
...engine.APPROVED_FUNCTIONS,
|
|
4131
|
+
// Utility objects available in expressions
|
|
4132
|
+
Object,
|
|
4133
|
+
Date,
|
|
4134
|
+
Math,
|
|
4135
|
+
String,
|
|
4136
|
+
Number,
|
|
4137
|
+
Boolean,
|
|
4138
|
+
Array,
|
|
4139
|
+
JSON
|
|
4140
|
+
};
|
|
4141
|
+
return context;
|
|
4142
|
+
}
|
|
4143
|
+
/**
|
|
4144
|
+
* Safely evaluate JavaScript expression with injected context
|
|
4145
|
+
*/
|
|
4146
|
+
function evaluateJavaScriptExpression(expression, context) {
|
|
4147
|
+
// Filter to only valid JavaScript identifiers (root variables only)
|
|
4148
|
+
const validParamNames = Object.keys(context).filter(key => /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key));
|
|
4149
|
+
// Unwrap user input variables before passing to function
|
|
4150
|
+
const paramValues = validParamNames.map(name => {
|
|
4151
|
+
const value = context[name];
|
|
4152
|
+
// Unwrap user input wrapper objects
|
|
4153
|
+
if (value && typeof value === 'object' &&
|
|
4154
|
+
value.__userInput === true &&
|
|
4155
|
+
value.__literal === true) {
|
|
4156
|
+
return value.value;
|
|
4157
|
+
}
|
|
4158
|
+
return value;
|
|
4159
|
+
});
|
|
4160
|
+
// Build function body that evaluates the expression
|
|
4161
|
+
const functionBody = `
|
|
4162
|
+
"use strict";
|
|
4163
|
+
try {
|
|
4164
|
+
const result = (${expression});
|
|
4165
|
+
// Auto-stringify complex data structures when used in string contexts
|
|
4166
|
+
if (typeof result === 'object' && result !== null &&
|
|
4167
|
+
!(result.__userInput === true && result.__literal === true)) {
|
|
4168
|
+
return JSON.stringify(result);
|
|
4169
|
+
}
|
|
4170
|
+
return result;
|
|
4171
|
+
} catch (e) {
|
|
4172
|
+
throw new Error('Expression evaluation failed: ' + e.message);
|
|
4173
|
+
}
|
|
4174
|
+
`;
|
|
4175
|
+
// Create and execute function with only valid parameter names
|
|
4176
|
+
const evaluatorFunction = new Function(...validParamNames, functionBody);
|
|
4177
|
+
return evaluatorFunction(...paramValues);
|
|
4178
|
+
}
|
|
4179
|
+
/**
|
|
4180
|
+
* Security-aware variable setter for user input
|
|
4181
|
+
* Integrates with the variable resolution system
|
|
4182
|
+
*/
|
|
4183
|
+
function setUserInputVariable(variables, key, value, sanitize = true) {
|
|
4184
|
+
// Mark user input as static literal data that should never be evaluated as code
|
|
4185
|
+
variables[key] = {
|
|
4186
|
+
__userInput: true,
|
|
4187
|
+
__literal: true,
|
|
4188
|
+
value: sanitize && typeof value === 'string' ? sanitizeUserInput(value) : value
|
|
4189
|
+
};
|
|
4190
|
+
}
|
|
4191
|
+
// ===============================================
|
|
4192
|
+
// LEGACY COMPATIBILITY FUNCTIONS
|
|
4193
|
+
// ===============================================
|
|
4194
|
+
/**
|
|
4195
|
+
* Clean unified expression interface functions
|
|
4196
|
+
* Maintains backward compatibility with existing code
|
|
4197
|
+
*/
|
|
4087
4198
|
function interpolateMessage(template, contextStack, variables = {}, engine) {
|
|
4088
4199
|
logger.debug(`Interpolating message template: ${template} with variables: ${JSON.stringify(variables)}`);
|
|
4089
4200
|
if (!template)
|
|
4090
4201
|
return template;
|
|
4091
|
-
//
|
|
4092
|
-
|
|
4093
|
-
|
|
4202
|
+
// For template interpolation, we should NOT apply security restrictions
|
|
4203
|
+
// Templates are developer-controlled, not user input
|
|
4204
|
+
const result = evaluateExpression(template, variables, contextStack, {
|
|
4205
|
+
securityLevel: 'none', // No security restrictions for templates
|
|
4094
4206
|
allowLogicalOperators: true,
|
|
4095
4207
|
allowMathOperators: true,
|
|
4096
4208
|
allowComparisons: true,
|
|
4097
4209
|
allowTernary: true,
|
|
4098
4210
|
context: 'template-interpolation',
|
|
4099
|
-
returnType: '
|
|
4211
|
+
returnType: 'string'
|
|
4100
4212
|
}, engine);
|
|
4213
|
+
return String(result);
|
|
4101
4214
|
}
|
|
4215
|
+
/**
|
|
4216
|
+
* Safe condition evaluation for boolean contexts
|
|
4217
|
+
* Maintains backward compatibility
|
|
4218
|
+
*/
|
|
4102
4219
|
function evaluateSafeCondition(condition, variables, engine) {
|
|
4103
4220
|
logger.debug(`Evaluating safe condition: ${condition} with variables: ${JSON.stringify(variables)}`);
|
|
4104
|
-
|
|
4105
|
-
securityLevel: '
|
|
4221
|
+
const result = evaluateExpression(condition, variables, [], {
|
|
4222
|
+
securityLevel: 'basic',
|
|
4106
4223
|
context: 'condition-evaluation',
|
|
4107
4224
|
returnType: 'boolean',
|
|
4108
4225
|
allowComparisons: true,
|
|
4109
4226
|
allowLogicalOperators: true
|
|
4110
4227
|
}, engine);
|
|
4111
|
-
|
|
4112
|
-
// Essential helper functions for the unified evaluator
|
|
4113
|
-
function isSimpleVariablePath(expression) {
|
|
4114
|
-
logger.debug(`Checking if expression is a simple variable path: ${expression}`);
|
|
4115
|
-
return /^[a-zA-Z_$][a-zA-Z0-9_$.]*$/.test(expression);
|
|
4116
|
-
}
|
|
4117
|
-
function resolveSimpleVariable(expression, variables, contextStack, engine) {
|
|
4118
|
-
// Debug logging to track variable resolution issues
|
|
4119
|
-
logger.debug(`Resolving variable '${expression}' - Available variables: ${JSON.stringify(Object.keys(variables || {}))}`);
|
|
4120
|
-
// Try variables first
|
|
4121
|
-
if (variables && Object.keys(variables).length > 0) {
|
|
4122
|
-
const val = expression.split('.').reduce((obj, part) => obj?.[part], variables);
|
|
4123
|
-
if (val !== undefined) {
|
|
4124
|
-
logger.debug(`Variable '${expression}' resolved to: ${val}`);
|
|
4125
|
-
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
4126
|
-
}
|
|
4127
|
-
}
|
|
4128
|
-
// If not found in variables, check for engine session variables
|
|
4129
|
-
if (engine) {
|
|
4130
|
-
const sessionValue = resolveEngineSessionVariable(expression, engine);
|
|
4131
|
-
if (sessionValue !== undefined) {
|
|
4132
|
-
logger.debug(`Variable '${expression}' resolved from engine session: ${sessionValue}`);
|
|
4133
|
-
return typeof sessionValue === 'object' ? JSON.stringify(sessionValue) : String(sessionValue);
|
|
4134
|
-
}
|
|
4135
|
-
}
|
|
4136
|
-
logger.debug(`Variable '${expression}' not found in variables or engine session`);
|
|
4137
|
-
return `[undefined: ${expression}]`;
|
|
4138
|
-
}
|
|
4139
|
-
// Helper function to resolve engine session variables
|
|
4140
|
-
function resolveEngineSessionVariable(expression, engine) {
|
|
4141
|
-
try {
|
|
4142
|
-
const currentFlowFrame = getCurrentFlowFrame(engine);
|
|
4143
|
-
logger.debug(`Resolving engine session variable: ${expression} in flow frame: ${currentFlowFrame.flowName}`);
|
|
4144
|
-
// Handle property access (e.g., cargo.someVar)
|
|
4145
|
-
if (expression.includes('.')) {
|
|
4146
|
-
const parts = expression.split('.');
|
|
4147
|
-
const baseVariable = parts[0];
|
|
4148
|
-
const propertyPath = parts.slice(1).join('.');
|
|
4149
|
-
let baseValue;
|
|
4150
|
-
// Get the base engine session variable
|
|
4151
|
-
switch (baseVariable) {
|
|
4152
|
-
case 'cargo':
|
|
4153
|
-
baseValue = engine.cargo || {};
|
|
4154
|
-
break;
|
|
4155
|
-
case 'userInput':
|
|
4156
|
-
case 'lastUserInput':
|
|
4157
|
-
const userEntries = currentFlowFrame.contextStack.filter(entry => entry.role === 'user');
|
|
4158
|
-
if (userEntries.length > 0) {
|
|
4159
|
-
const lastUserEntry = userEntries[userEntries.length - 1];
|
|
4160
|
-
baseValue = typeof lastUserEntry.content === 'string' ? lastUserEntry.content : String(lastUserEntry.content);
|
|
4161
|
-
}
|
|
4162
|
-
else {
|
|
4163
|
-
baseValue = undefined;
|
|
4164
|
-
}
|
|
4165
|
-
break;
|
|
4166
|
-
case 'sessionId':
|
|
4167
|
-
baseValue = engine.sessionId;
|
|
4168
|
-
break;
|
|
4169
|
-
case 'userId':
|
|
4170
|
-
baseValue = currentFlowFrame.userId;
|
|
4171
|
-
break;
|
|
4172
|
-
case 'flowName':
|
|
4173
|
-
baseValue = currentFlowFrame.flowName;
|
|
4174
|
-
break;
|
|
4175
|
-
default:
|
|
4176
|
-
logger.debug(`Unknown base engine session variable: ${baseVariable}`);
|
|
4177
|
-
return undefined;
|
|
4178
|
-
}
|
|
4179
|
-
// If base value is undefined, return undefined
|
|
4180
|
-
if (baseValue === undefined || baseValue === null) {
|
|
4181
|
-
logger.debug(`Base engine session variable '${baseVariable}' is undefined/null`);
|
|
4182
|
-
return undefined;
|
|
4183
|
-
}
|
|
4184
|
-
// Navigate through the property path using getNestedValue
|
|
4185
|
-
const result = getNestedValue(baseValue, propertyPath);
|
|
4186
|
-
logger.debug(`Engine session variable '${expression}' resolved to:`, result);
|
|
4187
|
-
return result;
|
|
4188
|
-
}
|
|
4189
|
-
// Handle direct variable access (no dots)
|
|
4190
|
-
switch (expression) {
|
|
4191
|
-
case 'cargo':
|
|
4192
|
-
logger.debug(`Returning cargo from engine session variable: ${expression}`);
|
|
4193
|
-
return engine.cargo || {};
|
|
4194
|
-
case 'userInput':
|
|
4195
|
-
case 'lastUserInput':
|
|
4196
|
-
// Get the most recent user input from context stack
|
|
4197
|
-
const userEntries = currentFlowFrame.contextStack.filter(entry => entry.role === 'user');
|
|
4198
|
-
if (userEntries.length > 0) {
|
|
4199
|
-
logger.debug(`Returning last user input from engine session variable: ${expression}`);
|
|
4200
|
-
const lastUserEntry = userEntries[userEntries.length - 1];
|
|
4201
|
-
return typeof lastUserEntry.content === 'string' ? lastUserEntry.content : String(lastUserEntry.content);
|
|
4202
|
-
}
|
|
4203
|
-
logger.debug(`No user input found in context stack for engine session variable: ${expression}`);
|
|
4204
|
-
return undefined;
|
|
4205
|
-
case 'currentTime()':
|
|
4206
|
-
logger.debug(`Returning current time for engine session variable: ${expression}`);
|
|
4207
|
-
return new Date().toISOString();
|
|
4208
|
-
case 'sessionId':
|
|
4209
|
-
logger.debug(`Returning session ID for engine session variable: ${expression}`);
|
|
4210
|
-
return engine.sessionId;
|
|
4211
|
-
case 'userId':
|
|
4212
|
-
logger.debug(`Returning user ID for engine session variable: ${expression}`);
|
|
4213
|
-
return currentFlowFrame.userId;
|
|
4214
|
-
case 'flowName':
|
|
4215
|
-
logger.debug(`Returning flow name for engine session variable: ${expression}`);
|
|
4216
|
-
return currentFlowFrame.flowName;
|
|
4217
|
-
default:
|
|
4218
|
-
logger.debug(`No specific engine session variable found for: ${expression}`);
|
|
4219
|
-
return undefined;
|
|
4220
|
-
}
|
|
4221
|
-
}
|
|
4222
|
-
catch (error) {
|
|
4223
|
-
logger.debug(`Failed to resolve engine session variable '${expression}': ${error}`);
|
|
4224
|
-
return undefined;
|
|
4225
|
-
}
|
|
4226
|
-
}
|
|
4227
|
-
// Helper function to evaluate function calls
|
|
4228
|
-
function evaluateFunctionCall(expression, variables, contextStack, engine) {
|
|
4229
|
-
try {
|
|
4230
|
-
logger.debug(`Evaluating function call: ${expression}`);
|
|
4231
|
-
// Handle currentTime() function call
|
|
4232
|
-
if (expression === 'currentTime()') {
|
|
4233
|
-
logger.debug(`Returning current time for function call: ${expression}`);
|
|
4234
|
-
return new Date().toISOString();
|
|
4235
|
-
}
|
|
4236
|
-
// Handle safe standalone functions
|
|
4237
|
-
const safeStandaloneFunctions = ['isNaN', 'parseInt', 'parseFloat', 'Boolean', 'Number', 'String'];
|
|
4238
|
-
// Parse function call with arguments
|
|
4239
|
-
const functionMatch = expression.match(/^(\w+)\((.*)\)$/);
|
|
4240
|
-
if (functionMatch) {
|
|
4241
|
-
const funcName = functionMatch[1];
|
|
4242
|
-
const argsString = functionMatch[2];
|
|
4243
|
-
if (safeStandaloneFunctions.includes(funcName)) {
|
|
4244
|
-
logger.debug(`Evaluating safe standalone function: ${funcName}`);
|
|
4245
|
-
// Parse and evaluate arguments
|
|
4246
|
-
const args = [];
|
|
4247
|
-
if (argsString.trim()) {
|
|
4248
|
-
// Simple argument parsing (supports literals and variables)
|
|
4249
|
-
const argParts = argsString.split(',').map(arg => arg.trim());
|
|
4250
|
-
for (const argPart of argParts) {
|
|
4251
|
-
let argValue;
|
|
4252
|
-
// Handle string literals
|
|
4253
|
-
if ((argPart.startsWith('"') && argPart.endsWith('"')) ||
|
|
4254
|
-
(argPart.startsWith("'") && argPart.endsWith("'"))) {
|
|
4255
|
-
argValue = argPart.slice(1, -1);
|
|
4256
|
-
}
|
|
4257
|
-
// Handle number literals
|
|
4258
|
-
else if (!isNaN(Number(argPart))) {
|
|
4259
|
-
argValue = Number(argPart);
|
|
4260
|
-
}
|
|
4261
|
-
// Handle boolean literals
|
|
4262
|
-
else if (argPart === 'true' || argPart === 'false') {
|
|
4263
|
-
argValue = argPart === 'true';
|
|
4264
|
-
}
|
|
4265
|
-
// Handle variables
|
|
4266
|
-
else {
|
|
4267
|
-
const varResult = resolveSimpleVariable(argPart, variables, contextStack, engine);
|
|
4268
|
-
if (varResult && !varResult.startsWith('[undefined:')) {
|
|
4269
|
-
argValue = varResult;
|
|
4270
|
-
}
|
|
4271
|
-
else if (engine) {
|
|
4272
|
-
const sessionResult = resolveEngineSessionVariable(argPart, engine);
|
|
4273
|
-
argValue = sessionResult !== undefined ? sessionResult : argPart;
|
|
4274
|
-
}
|
|
4275
|
-
else {
|
|
4276
|
-
argValue = argPart;
|
|
4277
|
-
}
|
|
4278
|
-
}
|
|
4279
|
-
args.push(argValue);
|
|
4280
|
-
}
|
|
4281
|
-
}
|
|
4282
|
-
// Execute the safe function
|
|
4283
|
-
try {
|
|
4284
|
-
switch (funcName) {
|
|
4285
|
-
case 'isNaN':
|
|
4286
|
-
logger.debug(`Executing isNaN with args: ${args}`);
|
|
4287
|
-
return isNaN(args[0]);
|
|
4288
|
-
case 'parseInt':
|
|
4289
|
-
logger.debug(`Executing parseInt with args: ${args}`);
|
|
4290
|
-
return parseInt(args[0], args[1] || 10);
|
|
4291
|
-
case 'parseFloat':
|
|
4292
|
-
logger.debug(`Executing parseFloat with args: ${args}`);
|
|
4293
|
-
return parseFloat(args[0]);
|
|
4294
|
-
case 'Boolean':
|
|
4295
|
-
logger.debug(`Executing Boolean with args: ${args}`);
|
|
4296
|
-
return Boolean(args[0]);
|
|
4297
|
-
case 'Number':
|
|
4298
|
-
logger.debug(`Executing Number with args: ${args}`);
|
|
4299
|
-
return Number(args[0]);
|
|
4300
|
-
case 'String':
|
|
4301
|
-
logger.debug(`Executing String with args: ${args}`);
|
|
4302
|
-
return String(args[0]);
|
|
4303
|
-
default:
|
|
4304
|
-
logger.warn(`Safe function ${funcName} not implemented`);
|
|
4305
|
-
return undefined;
|
|
4306
|
-
}
|
|
4307
|
-
}
|
|
4308
|
-
catch (error) {
|
|
4309
|
-
logger.debug(`Error executing safe function ${funcName}: ${error.message}`);
|
|
4310
|
-
return undefined;
|
|
4311
|
-
}
|
|
4312
|
-
}
|
|
4313
|
-
}
|
|
4314
|
-
// Handle extractCryptoFromInput(...) function call
|
|
4315
|
-
const extractCryptoMatch = expression.match(/^extractCryptoFromInput\((.+)\)$/);
|
|
4316
|
-
if (extractCryptoMatch) {
|
|
4317
|
-
const argExpression = extractCryptoMatch[1].trim();
|
|
4318
|
-
// Evaluate the argument expression (could be a variable or literal)
|
|
4319
|
-
let argValue;
|
|
4320
|
-
if (argExpression.startsWith('"') && argExpression.endsWith('"')) {
|
|
4321
|
-
argValue = argExpression.slice(1, -1);
|
|
4322
|
-
}
|
|
4323
|
-
else if (argExpression.startsWith("'") && argExpression.endsWith("'")) {
|
|
4324
|
-
argValue = argExpression.slice(1, -1);
|
|
4325
|
-
}
|
|
4326
|
-
else {
|
|
4327
|
-
// Try to resolve as variable (could be userInput or other variable)
|
|
4328
|
-
const varResult = resolveSimpleVariable(argExpression, variables, contextStack, engine);
|
|
4329
|
-
if (varResult && !varResult.startsWith('[undefined:')) {
|
|
4330
|
-
argValue = varResult;
|
|
4331
|
-
}
|
|
4332
|
-
else if (engine) {
|
|
4333
|
-
// Try engine session variables
|
|
4334
|
-
const sessionResult = resolveEngineSessionVariable(argExpression, engine);
|
|
4335
|
-
if (sessionResult !== undefined) {
|
|
4336
|
-
argValue = String(sessionResult);
|
|
4337
|
-
}
|
|
4338
|
-
else {
|
|
4339
|
-
argValue = argExpression; // Use as literal if can't resolve
|
|
4340
|
-
}
|
|
4341
|
-
}
|
|
4342
|
-
else {
|
|
4343
|
-
argValue = argExpression; // Use as literal if can't resolve
|
|
4344
|
-
}
|
|
4345
|
-
}
|
|
4346
|
-
logger.debug(`extractCryptoFromInput called with: ${argValue}`);
|
|
4347
|
-
// Extract crypto name from input text
|
|
4348
|
-
const cryptoNames = {
|
|
4349
|
-
'bitcoin': 'bitcoin',
|
|
4350
|
-
'btc': 'bitcoin',
|
|
4351
|
-
'ethereum': 'ethereum',
|
|
4352
|
-
'eth': 'ethereum',
|
|
4353
|
-
'litecoin': 'litecoin',
|
|
4354
|
-
'ltc': 'litecoin',
|
|
4355
|
-
'dogecoin': 'dogecoin',
|
|
4356
|
-
'doge': 'dogecoin',
|
|
4357
|
-
'cardano': 'cardano',
|
|
4358
|
-
'ada': 'cardano'
|
|
4359
|
-
};
|
|
4360
|
-
const lowerInput = argValue.toLowerCase();
|
|
4361
|
-
for (const [key, value] of Object.entries(cryptoNames)) {
|
|
4362
|
-
if (lowerInput.includes(key)) {
|
|
4363
|
-
logger.debug(`Extracted crypto: ${value}`);
|
|
4364
|
-
return value;
|
|
4365
|
-
}
|
|
4366
|
-
}
|
|
4367
|
-
logger.debug(`No crypto found in input, defaulting to bitcoin`);
|
|
4368
|
-
return 'bitcoin'; // Default fallback
|
|
4369
|
-
}
|
|
4370
|
-
return undefined; // Function not recognized
|
|
4371
|
-
}
|
|
4372
|
-
catch (error) {
|
|
4373
|
-
logger.warn(`Function call evaluation error: ${error.message}`);
|
|
4374
|
-
return undefined;
|
|
4375
|
-
}
|
|
4376
|
-
}
|
|
4377
|
-
function getNestedValue(obj, path) {
|
|
4378
|
-
if (!obj || typeof path !== 'string') {
|
|
4379
|
-
return undefined;
|
|
4380
|
-
}
|
|
4381
|
-
if (!path.includes('.')) {
|
|
4382
|
-
return obj[path];
|
|
4383
|
-
}
|
|
4384
|
-
const keys = path.split('.');
|
|
4385
|
-
let current = obj;
|
|
4386
|
-
for (const key of keys) {
|
|
4387
|
-
if (current === null || current === undefined) {
|
|
4388
|
-
return undefined;
|
|
4389
|
-
}
|
|
4390
|
-
current = current[key];
|
|
4391
|
-
}
|
|
4392
|
-
return current;
|
|
4393
|
-
}
|
|
4394
|
-
function evaluateSafeOrExpression(expression, variables, contextStack, engine) {
|
|
4395
|
-
const parts = expression.split('||').map(part => part.trim());
|
|
4396
|
-
for (const part of parts) {
|
|
4397
|
-
if ((part.startsWith('"') && part.endsWith('"')) ||
|
|
4398
|
-
(part.startsWith("'") && part.endsWith("'"))) {
|
|
4399
|
-
return part.slice(1, -1);
|
|
4400
|
-
}
|
|
4401
|
-
if (isSimpleVariablePath(part)) {
|
|
4402
|
-
if (variables && Object.keys(variables).length > 0) {
|
|
4403
|
-
const val = part.split('.').reduce((obj, partKey) => obj?.[partKey], variables);
|
|
4404
|
-
if (val !== undefined && val !== null && String(val) !== '') {
|
|
4405
|
-
return typeof val === 'object' ? JSON.stringify(val) : String(val);
|
|
4406
|
-
}
|
|
4407
|
-
}
|
|
4408
|
-
// Check engine session variables if not found in variables
|
|
4409
|
-
if (engine) {
|
|
4410
|
-
const sessionValue = resolveEngineSessionVariable(part, engine);
|
|
4411
|
-
if (sessionValue !== undefined && sessionValue !== null && String(sessionValue) !== '') {
|
|
4412
|
-
return typeof sessionValue === 'object' ? JSON.stringify(sessionValue) : String(sessionValue);
|
|
4413
|
-
}
|
|
4414
|
-
}
|
|
4415
|
-
}
|
|
4416
|
-
}
|
|
4417
|
-
const lastPart = parts[parts.length - 1];
|
|
4418
|
-
if ((lastPart.startsWith('"') && lastPart.endsWith('"')) ||
|
|
4419
|
-
(lastPart.startsWith("'") && lastPart.endsWith("'"))) {
|
|
4420
|
-
return lastPart.slice(1, -1);
|
|
4421
|
-
}
|
|
4422
|
-
// Final fallback - check for session variables in the last part
|
|
4423
|
-
if (engine && isSimpleVariablePath(lastPart)) {
|
|
4424
|
-
const sessionValue = resolveEngineSessionVariable(lastPart, engine);
|
|
4425
|
-
if (sessionValue !== undefined) {
|
|
4426
|
-
return typeof sessionValue === 'object' ? JSON.stringify(sessionValue) : String(sessionValue);
|
|
4427
|
-
}
|
|
4428
|
-
}
|
|
4429
|
-
return '';
|
|
4430
|
-
}
|
|
4431
|
-
function evaluateSafeTernaryExpression(expression, variables, contextStack, engine) {
|
|
4432
|
-
const ternaryMatch = expression.match(/^([a-zA-Z_$.]+)\s*(===|!==|==|!=|>=|<=|>|<)\s*(\d+|'[^']*'|"[^"]*"|true|false)\s*\?\s*('([^']*)'|"([^"]*)"|[a-zA-Z_$.]+)\s*:\s*('([^']*)'|"([^"]*)"|[a-zA-Z_$.]+)$/);
|
|
4433
|
-
if (!ternaryMatch) {
|
|
4434
|
-
return `[invalid-ternary: ${expression}]`;
|
|
4435
|
-
}
|
|
4436
|
-
const [, leftVar, operator, rightValue, , trueStr1, trueStr2, , falseStr1, falseStr2] = ternaryMatch;
|
|
4437
|
-
let leftVal;
|
|
4438
|
-
if (variables && Object.keys(variables).length > 0) {
|
|
4439
|
-
leftVal = leftVar.split('.').reduce((obj, part) => obj?.[part], variables);
|
|
4440
|
-
}
|
|
4441
|
-
// Check engine session variables if not found
|
|
4442
|
-
if (leftVal === undefined && engine) {
|
|
4443
|
-
leftVal = resolveEngineSessionVariable(leftVar, engine);
|
|
4444
|
-
}
|
|
4445
|
-
if (leftVal === undefined) {
|
|
4446
|
-
return `[undefined: ${leftVar}]`;
|
|
4447
|
-
}
|
|
4448
|
-
let rightVal;
|
|
4449
|
-
if (rightValue === 'true')
|
|
4450
|
-
rightVal = true;
|
|
4451
|
-
else if (rightValue === 'false')
|
|
4452
|
-
rightVal = false;
|
|
4453
|
-
else if (/^\d+$/.test(rightValue))
|
|
4454
|
-
rightVal = parseInt(rightValue);
|
|
4455
|
-
else if (/^\d*\.\d+$/.test(rightValue))
|
|
4456
|
-
rightVal = parseFloat(rightValue);
|
|
4457
|
-
else if (rightValue.startsWith("'") && rightValue.endsWith("'"))
|
|
4458
|
-
rightVal = rightValue.slice(1, -1);
|
|
4459
|
-
else if (rightValue.startsWith('"') && rightValue.endsWith('"'))
|
|
4460
|
-
rightVal = rightValue.slice(1, -1);
|
|
4461
|
-
else
|
|
4462
|
-
rightVal = rightValue;
|
|
4463
|
-
let condition = false;
|
|
4464
|
-
switch (operator) {
|
|
4465
|
-
case '===':
|
|
4466
|
-
condition = leftVal === rightVal;
|
|
4467
|
-
break;
|
|
4468
|
-
case '!==':
|
|
4469
|
-
condition = leftVal !== rightVal;
|
|
4470
|
-
break;
|
|
4471
|
-
case '==':
|
|
4472
|
-
condition = leftVal == rightVal;
|
|
4473
|
-
break;
|
|
4474
|
-
case '!=':
|
|
4475
|
-
condition = leftVal != rightVal;
|
|
4476
|
-
break;
|
|
4477
|
-
case '>':
|
|
4478
|
-
condition = leftVal > rightVal;
|
|
4479
|
-
break;
|
|
4480
|
-
case '<':
|
|
4481
|
-
condition = leftVal < rightVal;
|
|
4482
|
-
break;
|
|
4483
|
-
case '>=':
|
|
4484
|
-
condition = leftVal >= rightVal;
|
|
4485
|
-
break;
|
|
4486
|
-
case '<=':
|
|
4487
|
-
condition = leftVal <= rightVal;
|
|
4488
|
-
break;
|
|
4489
|
-
default: return `[invalid-operator: ${operator}]`;
|
|
4490
|
-
}
|
|
4491
|
-
if (condition) {
|
|
4492
|
-
return trueStr1 || trueStr2 || resolveSimpleVariable(trueStr2, variables, contextStack, engine) || '';
|
|
4493
|
-
}
|
|
4494
|
-
else {
|
|
4495
|
-
return falseStr1 || falseStr2 || resolveSimpleVariable(falseStr2, variables, contextStack, engine) || '';
|
|
4496
|
-
}
|
|
4497
|
-
}
|
|
4498
|
-
function evaluateSafeMathematicalExpression(expression, variables, contextStack, engine) {
|
|
4499
|
-
try {
|
|
4500
|
-
let evaluatedExpression = expression;
|
|
4501
|
-
const variablePattern = /[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)*/g;
|
|
4502
|
-
const variables_found = expression.match(variablePattern) || [];
|
|
4503
|
-
for (const varPath of [...new Set(variables_found)]) {
|
|
4504
|
-
if (/^\d+(\.\d+)?$/.test(varPath))
|
|
4505
|
-
continue;
|
|
4506
|
-
let value = resolveSimpleVariable(varPath, variables, contextStack, engine);
|
|
4507
|
-
const numValue = Number(value);
|
|
4508
|
-
if (!isNaN(numValue)) {
|
|
4509
|
-
value = String(numValue);
|
|
4510
|
-
}
|
|
4511
|
-
else if (value === undefined || value === null) {
|
|
4512
|
-
value = '0';
|
|
4513
|
-
}
|
|
4514
|
-
const regex = new RegExp(`\\b${varPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'g');
|
|
4515
|
-
evaluatedExpression = evaluatedExpression.replace(regex, String(value));
|
|
4516
|
-
}
|
|
4517
|
-
if (!/^[\d+\-*/%().\s]+$/.test(evaluatedExpression)) {
|
|
4518
|
-
return `[unsafe-math: ${expression}]`;
|
|
4519
|
-
}
|
|
4520
|
-
const result = new Function(`"use strict"; return (${evaluatedExpression})`)();
|
|
4521
|
-
return String(result);
|
|
4522
|
-
}
|
|
4523
|
-
catch (error) {
|
|
4524
|
-
logger.warn(`Mathematical expression evaluation error: ${error.message}`);
|
|
4525
|
-
return `[math-error: ${expression}]`;
|
|
4526
|
-
}
|
|
4228
|
+
return Boolean(result);
|
|
4527
4229
|
}
|
|
4528
4230
|
// === ENHANCED FLOW CONTROL COMMANDS ===
|
|
4529
4231
|
// Universal commands that work during any flow
|
|
@@ -4727,7 +4429,7 @@ async function handlePendingInterruptionSwitch(engine, userId) {
|
|
|
4727
4429
|
return getSystemMessage(engine, 'flow_switch_error', { targetFlow: 'unknown' });
|
|
4728
4430
|
}
|
|
4729
4431
|
// Clean up current flow
|
|
4730
|
-
currentFlowFrame.transaction
|
|
4432
|
+
TransactionManager.fail(currentFlowFrame.transaction, "User confirmed flow switch");
|
|
4731
4433
|
popFromCurrentStack(engine);
|
|
4732
4434
|
logger.info(`🔄 User confirmed switch from "${currentFlowName}" to "${targetFlow}"`);
|
|
4733
4435
|
// Find and activate the target flow
|
|
@@ -4735,7 +4437,7 @@ async function handlePendingInterruptionSwitch(engine, userId) {
|
|
|
4735
4437
|
if (!targetFlowDefinition) {
|
|
4736
4438
|
return getSystemMessage(engine, 'flow_switch_error', { targetFlow });
|
|
4737
4439
|
}
|
|
4738
|
-
const newTransaction =
|
|
4440
|
+
const newTransaction = TransactionManager.create(targetFlow, 'confirmed-switch', userId);
|
|
4739
4441
|
const newFlowFrame = {
|
|
4740
4442
|
flowName: targetFlow,
|
|
4741
4443
|
flowId: targetFlowDefinition.id,
|
|
@@ -4781,7 +4483,7 @@ async function handleRegularFlowInterruption(intentAnalysis, engine, userId) {
|
|
|
4781
4483
|
const flowsMenu = engine.flowsMenu; // Access the global flows menu
|
|
4782
4484
|
logger.info(`🔄 Processing flow interruption: "${userInput}" -> ${targetFlow}`);
|
|
4783
4485
|
// For non-financial flows, allow graceful switching with option to resume
|
|
4784
|
-
currentFlowFrame.transaction
|
|
4486
|
+
TransactionManager.complete(currentFlowFrame.transaction);
|
|
4785
4487
|
// IMPORTANT: Clear pendingVariable from interrupted flow to avoid stale state when resuming
|
|
4786
4488
|
if (currentFlowFrame.pendingVariable) {
|
|
4787
4489
|
logger.info(`🧹 Clearing stale pendingVariable '${currentFlowFrame.pendingVariable}' from interrupted flow`);
|
|
@@ -4800,7 +4502,7 @@ async function handleRegularFlowInterruption(intentAnalysis, engine, userId) {
|
|
|
4800
4502
|
// Activate the new flow
|
|
4801
4503
|
const targetFlowDefinition = flowsMenu?.find(f => f.name === targetFlow);
|
|
4802
4504
|
if (targetFlowDefinition) {
|
|
4803
|
-
const newTransaction =
|
|
4505
|
+
const newTransaction = TransactionManager.create(targetFlow, 'intent-switch', userId);
|
|
4804
4506
|
const newFlowFrame = {
|
|
4805
4507
|
flowName: targetFlow,
|
|
4806
4508
|
flowId: targetFlowDefinition.id,
|
|
@@ -4848,9 +4550,14 @@ async function handleFlowExit(engine, userId, input) {
|
|
|
4848
4550
|
// Clean up all flows with proper transaction logging
|
|
4849
4551
|
const exitedFlows = [];
|
|
4850
4552
|
let flow;
|
|
4851
|
-
while (
|
|
4553
|
+
while (engine.flowStacks.length > 0) {
|
|
4554
|
+
const poppedStack = engine.flowStacks.pop();
|
|
4555
|
+
if (!poppedStack || poppedStack.length === 0) {
|
|
4556
|
+
break;
|
|
4557
|
+
}
|
|
4558
|
+
flow = poppedStack[0];
|
|
4852
4559
|
logger.info(`Exiting flow: ${flow.flowName} due to user request`);
|
|
4853
|
-
flow.transaction
|
|
4560
|
+
TransactionManager.fail(flow.transaction, `User requested exit: ${input}`);
|
|
4854
4561
|
exitedFlows.push(flow.flowName);
|
|
4855
4562
|
auditLogger.logFlowExit(flow.flowName, userId, flow.transaction.id, 'user_requested');
|
|
4856
4563
|
}
|
|
@@ -4880,10 +4587,28 @@ async function processActivity(input, userId, engine) {
|
|
|
4880
4587
|
if (flowControlResult) {
|
|
4881
4588
|
return flowControlResult;
|
|
4882
4589
|
}
|
|
4883
|
-
// Check for strong intent interruption (new flows)
|
|
4884
|
-
const
|
|
4885
|
-
|
|
4886
|
-
|
|
4590
|
+
// Check for strong intent interruption (new flows) - only if current flow AND all parent flows are interruptable
|
|
4591
|
+
const flowStacks = engine.flowStacks;
|
|
4592
|
+
const currentStackIndex = flowStacks.length - 1;
|
|
4593
|
+
const currentStack = flowStacks[currentStackIndex] || [];
|
|
4594
|
+
let isAnyFlowNonInterruptable = false;
|
|
4595
|
+
for (const flowFrame of currentStack) {
|
|
4596
|
+
const flowDefinition = engine.flowsMenu.find((f) => f.name === flowFrame.flowName);
|
|
4597
|
+
const isFlowInterruptable = flowDefinition?.interruptable ?? false; // Default to false
|
|
4598
|
+
if (!isFlowInterruptable) {
|
|
4599
|
+
isAnyFlowNonInterruptable = true;
|
|
4600
|
+
logger.debug(`Flow "${flowFrame.flowName}" in stack is not interruptable`);
|
|
4601
|
+
break;
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
if (!isAnyFlowNonInterruptable) {
|
|
4605
|
+
const interruptionResult = await handleIntentInterruption(String(sanitizedInput), engine, userId);
|
|
4606
|
+
if (interruptionResult) {
|
|
4607
|
+
return interruptionResult;
|
|
4608
|
+
}
|
|
4609
|
+
}
|
|
4610
|
+
else {
|
|
4611
|
+
logger.debug(`Skipping intent interruption check - one or more flows in the stack are not interruptable`);
|
|
4887
4612
|
}
|
|
4888
4613
|
// Clear the last SAY message if we had one
|
|
4889
4614
|
if (currentFlowFrame.lastSayMessage) {
|
|
@@ -4905,7 +4630,7 @@ async function processActivity(input, userId, engine) {
|
|
|
4905
4630
|
// Clean up failed flow
|
|
4906
4631
|
if (getCurrentStackLength(engine) > 0) {
|
|
4907
4632
|
const failedFrame = popFromCurrentStack(engine);
|
|
4908
|
-
failedFrame.transaction
|
|
4633
|
+
TransactionManager.fail(failedFrame.transaction, error.message);
|
|
4909
4634
|
}
|
|
4910
4635
|
return `I encountered an error: ${error.message}. Please try again or contact support if the issue persists.`;
|
|
4911
4636
|
}
|
|
@@ -4917,7 +4642,12 @@ async function processActivity(input, userId, engine) {
|
|
|
4917
4642
|
if (activatedFlow) {
|
|
4918
4643
|
logger.info(`Flow activated: ${activatedFlow.name}`);
|
|
4919
4644
|
// Clear lastChatTurn since we now have flow context
|
|
4920
|
-
|
|
4645
|
+
const lastChatTurn = engine.lastChatTurn;
|
|
4646
|
+
if (lastChatTurn) {
|
|
4647
|
+
// Clear the existing properties
|
|
4648
|
+
delete lastChatTurn.user;
|
|
4649
|
+
delete lastChatTurn.assistant;
|
|
4650
|
+
}
|
|
4921
4651
|
logger.debug(`Cleared lastChatTurn - now using flow context for AI operations`);
|
|
4922
4652
|
const response = await playFlowFrame(engine);
|
|
4923
4653
|
logger.info(`Initial flow response: ${response}`);
|
|
@@ -4940,6 +4670,16 @@ async function processActivity(input, userId, engine) {
|
|
|
4940
4670
|
}
|
|
4941
4671
|
// === WORKFLOW ENGINE CLASS ===
|
|
4942
4672
|
export class WorkflowEngine {
|
|
4673
|
+
// Session-specific properties as getters - work directly with session data
|
|
4674
|
+
get flowStacks() {
|
|
4675
|
+
return this.sessionContext?.flowStacks || [[]];
|
|
4676
|
+
}
|
|
4677
|
+
get globalAccumulatedMessages() {
|
|
4678
|
+
return this.sessionContext?.globalAccumulatedMessages || [];
|
|
4679
|
+
}
|
|
4680
|
+
get lastChatTurn() {
|
|
4681
|
+
return this.sessionContext?.lastChatTurn || {};
|
|
4682
|
+
}
|
|
4943
4683
|
/**
|
|
4944
4684
|
* Initialize a new EngineSessionContext for a user session.
|
|
4945
4685
|
* If hostLogger is null, uses the global default logger.
|
|
@@ -4956,7 +4696,8 @@ export class WorkflowEngine {
|
|
|
4956
4696
|
*/
|
|
4957
4697
|
constructor(hostLogger, aiCallback, flowsMenu, toolsRegistry, APPROVED_FUNCTIONS, globalVariables, // Optional global variables shared across all new flows
|
|
4958
4698
|
validateOnInit, language, messageRegistry, guidanceConfig) {
|
|
4959
|
-
|
|
4699
|
+
// Private session context - engine works directly with session data (no copying!)
|
|
4700
|
+
this.sessionContext = null;
|
|
4960
4701
|
hostLogger = hostLogger || logger; // Fallback to global fake logger if none provided
|
|
4961
4702
|
logger = hostLogger; // Assign to global logger
|
|
4962
4703
|
this.aiCallback = aiCallback;
|
|
@@ -4974,9 +4715,7 @@ export class WorkflowEngine {
|
|
|
4974
4715
|
separator: '\n\n',
|
|
4975
4716
|
contextSelector: 'auto'
|
|
4976
4717
|
};
|
|
4977
|
-
//
|
|
4978
|
-
this.flowStacks = [[]]; // Stack of flowFrames stacks for proper flow interruption/resumption
|
|
4979
|
-
this.globalAccumulatedMessages = []; // Global SAY message accumulation across all stacks
|
|
4718
|
+
// No longer initialize session-specific data in constructor - it's now in sessionContext
|
|
4980
4719
|
this.sessionId = crypto.randomUUID();
|
|
4981
4720
|
this.createdAt = new Date();
|
|
4982
4721
|
this.lastActivity = new Date();
|
|
@@ -4999,117 +4738,190 @@ export class WorkflowEngine {
|
|
|
4999
4738
|
* const session = engine.initSession(yourLogger, 'user-123', 'session-456');
|
|
5000
4739
|
*/
|
|
5001
4740
|
initSession(hostLogger, userId, sessionId) {
|
|
5002
|
-
hostLogger = hostLogger || logger; // Fallback to global fake logger if none provided
|
|
5003
4741
|
// Validate logger compatibility
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
4742
|
+
if (hostLogger) {
|
|
4743
|
+
const requiredMethods = ['info', 'warn', 'error', 'debug'];
|
|
4744
|
+
for (const method of requiredMethods) {
|
|
4745
|
+
if (typeof hostLogger[method] !== 'function') {
|
|
4746
|
+
throw new Error(`Logger is missing required method: ${method}`);
|
|
4747
|
+
}
|
|
5008
4748
|
}
|
|
5009
4749
|
}
|
|
5010
4750
|
// Assign the session logger to the global logger
|
|
5011
4751
|
logger = hostLogger || logger;
|
|
5012
4752
|
const engineSessionContext = {
|
|
5013
|
-
hostLogger: hostLogger,
|
|
4753
|
+
hostLogger: hostLogger || logger,
|
|
5014
4754
|
sessionId: sessionId || crypto.randomUUID(),
|
|
5015
4755
|
userId: userId,
|
|
5016
4756
|
createdAt: new Date(),
|
|
5017
4757
|
lastActivity: new Date(),
|
|
5018
|
-
flowStacks: [[]],
|
|
4758
|
+
flowStacks: [[]], // Initialize as array with one empty stack (pure data)
|
|
5019
4759
|
globalAccumulatedMessages: [],
|
|
5020
4760
|
lastChatTurn: {},
|
|
5021
4761
|
globalVariables: this.globalVariables ? { ...this.globalVariables } : {},
|
|
5022
|
-
cargo: {}
|
|
4762
|
+
cargo: {},
|
|
4763
|
+
response: null, // Initialize with no response
|
|
4764
|
+
completedTransactions: [] // Initialize with no completed transactions
|
|
5023
4765
|
};
|
|
5024
4766
|
logger.info(`Engine session initialized: ${engineSessionContext.sessionId} for user: ${userId}`);
|
|
5025
4767
|
return engineSessionContext;
|
|
5026
4768
|
}
|
|
5027
4769
|
async updateActivity(contextEntry, engineSessionContext) {
|
|
5028
|
-
|
|
5029
|
-
|
|
5030
|
-
|
|
4770
|
+
try {
|
|
4771
|
+
// Load session context if provided
|
|
4772
|
+
let hostLogger = engineSessionContext.hostLogger || logger; // Fallback to global logger if not provided
|
|
4773
|
+
if (engineSessionContext.hostLogger) {
|
|
4774
|
+
const requiredMethods = ['info', 'warn', 'error', 'debug'];
|
|
4775
|
+
for (const method of requiredMethods) {
|
|
4776
|
+
if (typeof engineSessionContext.hostLogger[method] !== 'function') {
|
|
4777
|
+
hostLogger = logger; // Fallback to global logger if hostLogger is missing methods
|
|
4778
|
+
break; // No need to throw error, just use global logger
|
|
4779
|
+
//throw new Error(`Logger is missing required method: ${method}`);
|
|
4780
|
+
}
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
4783
|
+
logger = hostLogger; // Assign to global logger
|
|
5031
4784
|
this.cargo = engineSessionContext.cargo;
|
|
4785
|
+
logger.info(`Received Cargo: ${JSON.stringify(this.cargo)}`);
|
|
5032
4786
|
this.sessionId = engineSessionContext.sessionId;
|
|
5033
4787
|
this.createdAt = engineSessionContext.createdAt;
|
|
5034
|
-
|
|
5035
|
-
this.
|
|
5036
|
-
|
|
5037
|
-
this.
|
|
5038
|
-
|
|
5039
|
-
|
|
5040
|
-
|
|
5041
|
-
|
|
5042
|
-
|
|
5043
|
-
|
|
5044
|
-
|
|
5045
|
-
|
|
5046
|
-
|
|
5047
|
-
|
|
5048
|
-
|
|
5049
|
-
|
|
5050
|
-
//
|
|
5051
|
-
if (
|
|
5052
|
-
|
|
5053
|
-
|
|
5054
|
-
|
|
4788
|
+
// Store reference to session context - no copying needed! Engine works directly with session data
|
|
4789
|
+
this.sessionContext = engineSessionContext;
|
|
4790
|
+
// Ensure session context has proper initialization
|
|
4791
|
+
if (!this.sessionContext.flowStacks || !Array.isArray(this.sessionContext.flowStacks)) {
|
|
4792
|
+
this.sessionContext.flowStacks = [[]];
|
|
4793
|
+
logger.warn('engineSessionContext.flowStacks was invalid, initialized fresh flowStacks');
|
|
4794
|
+
}
|
|
4795
|
+
if (!this.sessionContext.globalAccumulatedMessages) {
|
|
4796
|
+
this.sessionContext.globalAccumulatedMessages = [];
|
|
4797
|
+
}
|
|
4798
|
+
if (!this.sessionContext.lastChatTurn) {
|
|
4799
|
+
this.sessionContext.lastChatTurn = {};
|
|
4800
|
+
}
|
|
4801
|
+
if (!this.sessionContext.globalVariables) {
|
|
4802
|
+
this.sessionContext.globalVariables = {};
|
|
4803
|
+
}
|
|
4804
|
+
// Safety check: ensure flowStacks is always properly initialized
|
|
4805
|
+
if (this.sessionContext.flowStacks.length === 0) {
|
|
4806
|
+
logger.warn('flowStacks was empty, adding initial stack...');
|
|
4807
|
+
this.sessionContext.flowStacks.push([]); // Add empty stack
|
|
4808
|
+
}
|
|
4809
|
+
// Get userId from session context
|
|
4810
|
+
const userId = engineSessionContext?.userId || 'anonymous';
|
|
4811
|
+
this.lastActivity = new Date();
|
|
4812
|
+
// Update session context with latest activity time
|
|
5055
4813
|
if (engineSessionContext) {
|
|
5056
|
-
engineSessionContext.
|
|
5057
|
-
|
|
5058
|
-
|
|
5059
|
-
|
|
4814
|
+
engineSessionContext.lastActivity = this.lastActivity;
|
|
4815
|
+
}
|
|
4816
|
+
// Role-based processing logic
|
|
4817
|
+
if (contextEntry.role === 'user') {
|
|
4818
|
+
// Detect intent to activate a flow or switch flows
|
|
4819
|
+
const responseOrNull = await processActivity(String(contextEntry.content), userId, this);
|
|
4820
|
+
// Store user turn in lastChatTurn if not in a flow
|
|
4821
|
+
if (responseOrNull === null) {
|
|
4822
|
+
this.sessionContext.lastChatTurn.user = contextEntry;
|
|
4823
|
+
}
|
|
4824
|
+
// No copying needed! Session context already contains the latest data
|
|
4825
|
+
if (engineSessionContext) {
|
|
4826
|
+
engineSessionContext.response = responseOrNull; // Store the response from flow processing
|
|
4827
|
+
// Extract and store completed transactions for host access
|
|
4828
|
+
const newCompletedTransactions = [];
|
|
4829
|
+
for (const stack of this.flowStacks) {
|
|
4830
|
+
for (const frame of stack) {
|
|
4831
|
+
if (frame.transaction && (frame.transaction.state === 'completed' || frame.transaction.state === 'failed')) {
|
|
4832
|
+
newCompletedTransactions.push(frame.transaction);
|
|
4833
|
+
}
|
|
4834
|
+
}
|
|
4835
|
+
}
|
|
4836
|
+
if (!engineSessionContext.completedTransactions) {
|
|
4837
|
+
engineSessionContext.completedTransactions = [];
|
|
4838
|
+
}
|
|
4839
|
+
// Add any new completed transactions
|
|
4840
|
+
for (const transaction of newCompletedTransactions) {
|
|
4841
|
+
const existingTransaction = engineSessionContext.completedTransactions.find(t => t.id === transaction.id);
|
|
4842
|
+
if (!existingTransaction) {
|
|
4843
|
+
engineSessionContext.completedTransactions.push(transaction);
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4846
|
+
}
|
|
4847
|
+
return engineSessionContext;
|
|
5060
4848
|
}
|
|
5061
|
-
|
|
5062
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
4849
|
+
else if (contextEntry.role === 'assistant') {
|
|
4850
|
+
// Check if we're in a flow or not
|
|
4851
|
+
if (getCurrentStackLength(this) === 0) {
|
|
4852
|
+
// Not in a flow - store in lastChatTurn for context
|
|
4853
|
+
this.sessionContext.lastChatTurn.assistant = contextEntry;
|
|
4854
|
+
}
|
|
4855
|
+
else {
|
|
4856
|
+
// In a flow - add to current flow's context stack
|
|
4857
|
+
const currentFlowFrame = getCurrentFlowFrame(this);
|
|
4858
|
+
addToContextStack(currentFlowFrame.contextStack, 'assistant', contextEntry.content, contextEntry.stepId, contextEntry.toolName, contextEntry.metadata);
|
|
4859
|
+
}
|
|
4860
|
+
// No copying needed! Session context already contains the latest data
|
|
4861
|
+
if (engineSessionContext) {
|
|
4862
|
+
engineSessionContext.response = null; // No response for assistant turns
|
|
4863
|
+
// Extract and store completed transactions for host access
|
|
4864
|
+
const newCompletedTransactions = [];
|
|
4865
|
+
for (const stack of this.flowStacks) {
|
|
4866
|
+
for (const frame of stack) {
|
|
4867
|
+
if (frame.transaction && (frame.transaction.state === 'completed' || frame.transaction.state === 'failed')) {
|
|
4868
|
+
newCompletedTransactions.push(frame.transaction);
|
|
4869
|
+
}
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
if (!engineSessionContext.completedTransactions) {
|
|
4873
|
+
engineSessionContext.completedTransactions = [];
|
|
4874
|
+
}
|
|
4875
|
+
// Add any new completed transactions
|
|
4876
|
+
for (const transaction of newCompletedTransactions) {
|
|
4877
|
+
const existingTransaction = engineSessionContext.completedTransactions.find(t => t.id === transaction.id);
|
|
4878
|
+
if (!existingTransaction) {
|
|
4879
|
+
engineSessionContext.completedTransactions.push(transaction);
|
|
4880
|
+
}
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
return engineSessionContext;
|
|
5068
4884
|
}
|
|
5069
4885
|
else {
|
|
5070
|
-
//
|
|
5071
|
-
|
|
5072
|
-
|
|
4886
|
+
// Throw error for unsupported roles
|
|
4887
|
+
throw new Error(`Unsupported role '${contextEntry.role}' in updateActivity. Only 'user' and 'assistant' roles are supported.`);
|
|
4888
|
+
}
|
|
4889
|
+
}
|
|
4890
|
+
catch (error) {
|
|
4891
|
+
if (logger && logger.error) {
|
|
4892
|
+
logger.error(`Error in updateActivity: ${error.message}`);
|
|
5073
4893
|
}
|
|
5074
|
-
//
|
|
4894
|
+
// Return session context with error information
|
|
5075
4895
|
if (engineSessionContext) {
|
|
5076
|
-
engineSessionContext.
|
|
5077
|
-
engineSessionContext
|
|
5078
|
-
engineSessionContext.lastChatTurn = this.lastChatTurn;
|
|
5079
|
-
engineSessionContext.globalVariables = this.globalVariables || {};
|
|
4896
|
+
engineSessionContext.response = `Error: ${error.message}`;
|
|
4897
|
+
return engineSessionContext;
|
|
5080
4898
|
}
|
|
5081
|
-
//
|
|
5082
|
-
|
|
5083
|
-
}
|
|
5084
|
-
else {
|
|
5085
|
-
// Throw error for unsupported roles
|
|
5086
|
-
throw new Error(`Unsupported role '${contextEntry.role}' in updateActivity. Only 'user' and 'assistant' roles are supported.`);
|
|
4899
|
+
// If no session context provided, we can't return it - this should not happen
|
|
4900
|
+
throw error;
|
|
5087
4901
|
}
|
|
5088
4902
|
}
|
|
5089
4903
|
// Add a SAY message to global accumulation
|
|
5090
|
-
addAccumulatedMessage(message
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
4904
|
+
addAccumulatedMessage(message) {
|
|
4905
|
+
if (!this.sessionContext) {
|
|
4906
|
+
logger.warn('No session context available for addAccumulatedMessage');
|
|
4907
|
+
return;
|
|
5094
4908
|
}
|
|
5095
|
-
this.globalAccumulatedMessages
|
|
5096
|
-
|
|
5097
|
-
if (engineSessionContext) {
|
|
5098
|
-
engineSessionContext.globalAccumulatedMessages = this.globalAccumulatedMessages;
|
|
4909
|
+
if (!this.sessionContext.globalAccumulatedMessages) {
|
|
4910
|
+
this.sessionContext.globalAccumulatedMessages = [];
|
|
5099
4911
|
}
|
|
4912
|
+
this.sessionContext.globalAccumulatedMessages.push(message);
|
|
5100
4913
|
}
|
|
5101
4914
|
// Get and clear all accumulated messages
|
|
5102
|
-
getAndClearAccumulatedMessages(
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
4915
|
+
getAndClearAccumulatedMessages() {
|
|
4916
|
+
if (!this.sessionContext) {
|
|
4917
|
+
logger.warn('No session context available for getAndClearAccumulatedMessages');
|
|
4918
|
+
return [];
|
|
5106
4919
|
}
|
|
5107
|
-
|
|
5108
|
-
|
|
5109
|
-
// Update session context with cleared messages
|
|
5110
|
-
if (engineSessionContext) {
|
|
5111
|
-
engineSessionContext.globalAccumulatedMessages = this.globalAccumulatedMessages;
|
|
4920
|
+
if (!this.sessionContext.globalAccumulatedMessages) {
|
|
4921
|
+
this.sessionContext.globalAccumulatedMessages = [];
|
|
5112
4922
|
}
|
|
4923
|
+
const messages = [...this.sessionContext.globalAccumulatedMessages];
|
|
4924
|
+
this.sessionContext.globalAccumulatedMessages = [];
|
|
5113
4925
|
return messages;
|
|
5114
4926
|
}
|
|
5115
4927
|
// Check if there are accumulated messages
|
|
@@ -5117,7 +4929,12 @@ export class WorkflowEngine {
|
|
|
5117
4929
|
return this.globalAccumulatedMessages.length > 0;
|
|
5118
4930
|
}
|
|
5119
4931
|
initializeFlowStacks() {
|
|
5120
|
-
|
|
4932
|
+
if (this.sessionContext) {
|
|
4933
|
+
this.sessionContext.flowStacks = [[]];
|
|
4934
|
+
}
|
|
4935
|
+
else {
|
|
4936
|
+
logger.warn('No session context available for initializeFlowStacks');
|
|
4937
|
+
}
|
|
5121
4938
|
}
|
|
5122
4939
|
getCurrentStack() {
|
|
5123
4940
|
return getCurrentStack(this);
|
|
@@ -5226,7 +5043,8 @@ export class WorkflowEngine {
|
|
|
5226
5043
|
flowCallGraph: new Map(),
|
|
5227
5044
|
currentDepth: 0,
|
|
5228
5045
|
variableScopes: new Map(),
|
|
5229
|
-
toolRegistry: new Set(this.toolsRegistry.map((t) => t.id))
|
|
5046
|
+
toolRegistry: new Set(this.toolsRegistry.map((t) => t.id)),
|
|
5047
|
+
flowCallStack: [] // Track parent flow chain for variable inheritance
|
|
5230
5048
|
};
|
|
5231
5049
|
try {
|
|
5232
5050
|
this._validateFlowRecursive(flowName, validationState, opts);
|
|
@@ -5266,7 +5084,7 @@ export class WorkflowEngine {
|
|
|
5266
5084
|
return;
|
|
5267
5085
|
}
|
|
5268
5086
|
// Find the flow definition
|
|
5269
|
-
const flowDef = this.flowsMenu.find((f) => f.name === flowName);
|
|
5087
|
+
const flowDef = this.flowsMenu.find((f) => f.id === flowName || f.name === flowName);
|
|
5270
5088
|
if (!flowDef) {
|
|
5271
5089
|
state.errors.push(`Flow not found: ${flowName}`);
|
|
5272
5090
|
return;
|
|
@@ -5274,6 +5092,8 @@ export class WorkflowEngine {
|
|
|
5274
5092
|
// Mark as visited
|
|
5275
5093
|
state.visitedFlows.add(flowName);
|
|
5276
5094
|
state.currentDepth++;
|
|
5095
|
+
// Add to flow call stack for variable inheritance tracking
|
|
5096
|
+
state.flowCallStack.push(flowName);
|
|
5277
5097
|
// Initialize flow call graph
|
|
5278
5098
|
if (!state.flowCallGraph.has(flowName)) {
|
|
5279
5099
|
state.flowCallGraph.set(flowName, []);
|
|
@@ -5303,6 +5123,8 @@ export class WorkflowEngine {
|
|
|
5303
5123
|
this._validateFlowRecursive(calledFlow, state, opts);
|
|
5304
5124
|
}
|
|
5305
5125
|
}
|
|
5126
|
+
// Remove from flow call stack when exiting this flow
|
|
5127
|
+
state.flowCallStack.pop();
|
|
5306
5128
|
state.currentDepth--;
|
|
5307
5129
|
}
|
|
5308
5130
|
/**
|
|
@@ -5692,7 +5514,7 @@ export class WorkflowEngine {
|
|
|
5692
5514
|
// PHASE 2: Validate only top-level flows (not referenced as sub-flows)
|
|
5693
5515
|
logger.info('🔍 Phase 2: Validating top-level flows...');
|
|
5694
5516
|
for (const flow of this.flowsMenu) {
|
|
5695
|
-
if (referencedSubFlows.has(flow.name)) {
|
|
5517
|
+
if (referencedSubFlows.has(flow.name) || referencedSubFlows.has(flow.id)) {
|
|
5696
5518
|
// Skip validation of sub-flows - they'll be validated in parent context
|
|
5697
5519
|
logger.info(`⏭️ Skipping sub-flow "${flow.name}" - will be validated in parent context`);
|
|
5698
5520
|
results.skippedSubFlows.push(flow.name);
|
|
@@ -5741,11 +5563,24 @@ export class WorkflowEngine {
|
|
|
5741
5563
|
// === ENHANCED VARIABLE SCOPE TRACKING ===
|
|
5742
5564
|
/**
|
|
5743
5565
|
* Gets the current variable scope available at a specific step
|
|
5744
|
-
* This includes variables defined in flow definition + variables created by previous steps
|
|
5566
|
+
* This includes variables defined in flow definition + variables created by previous steps + parent flow variables
|
|
5745
5567
|
*/
|
|
5746
5568
|
_getCurrentStepScope(flowDef, stepIndex, state) {
|
|
5747
5569
|
const scope = new Set();
|
|
5748
|
-
// Add flow
|
|
5570
|
+
// Add parent flow variables (variable inheritance from call stack)
|
|
5571
|
+
if (state.flowCallStack && state.flowCallStack.length > 0) {
|
|
5572
|
+
// Iterate through parent flows in the call stack (excluding current flow)
|
|
5573
|
+
for (let i = 0; i < state.flowCallStack.length - 1; i++) {
|
|
5574
|
+
const parentFlowName = state.flowCallStack[i];
|
|
5575
|
+
const parentFlowDef = this.flowsMenu.find((f) => f.id === parentFlowName || f.name === parentFlowName);
|
|
5576
|
+
if (parentFlowDef && parentFlowDef.variables) {
|
|
5577
|
+
for (const varName of Object.keys(parentFlowDef.variables)) {
|
|
5578
|
+
scope.add(varName);
|
|
5579
|
+
}
|
|
5580
|
+
}
|
|
5581
|
+
}
|
|
5582
|
+
}
|
|
5583
|
+
// Add flow-defined variables (current flow)
|
|
5749
5584
|
if (flowDef.variables) {
|
|
5750
5585
|
for (const varName of Object.keys(flowDef.variables)) {
|
|
5751
5586
|
scope.add(varName);
|
|
@@ -5914,7 +5749,7 @@ export class WorkflowEngine {
|
|
|
5914
5749
|
const variableNames = extractVariableNames(varPath);
|
|
5915
5750
|
for (const rootVar of variableNames) {
|
|
5916
5751
|
// Check if it's a registered function first (global scope)
|
|
5917
|
-
const isApprovedFunction = this.APPROVED_FUNCTIONS && this.APPROVED_FUNCTIONS
|
|
5752
|
+
const isApprovedFunction = this.APPROVED_FUNCTIONS && this.APPROVED_FUNCTIONS[rootVar];
|
|
5918
5753
|
// If not an approved function, check if it's in current scope as a variable
|
|
5919
5754
|
if (!isApprovedFunction && !currentScope.has(rootVar)) {
|
|
5920
5755
|
state.errors.push(`${context} in step "${step.id}" references undefined variable: ${rootVar} (full path: ${varPath})`);
|