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/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
- // Handle template strings
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
- // Fake logger that does nothing - in case hostLogger argument to engine.initSession is null
1252
- let logger = {
1253
- info: () => { },
1254
- warn: () => { },
1255
- error: () => { },
1256
- debug: () => { }
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
- // Basic HTML escape and trim
1467
- return input.trim().replace(/[<>'"&]/g, (char) => {
1468
- const escapeMap = { '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '&': '&amp;' };
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.flowStacks = [[]];
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
- return getCurrentStack(engine).pop();
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 = new FlowTransaction(flow.name, 'user-input', userId);
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.fail("Max recursion depth exceeded");
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[currentFlowFrame.pendingVariable] = userInput;
1770
- logger.info(`Stored user input in variable '${currentFlowFrame.pendingVariable}': "${userInput}"`);
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.complete();
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.addStep(step, result, duration, 'success');
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.complete();
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.addError(step, error, duration);
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 || 'Unknown error';
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.warn(`Unrecognized error type for tool ${toolName} in flow ${flowName}: ${errorMessage}`);
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.get(step.inputValidation.customValidator);
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 all flows and start fresh with the onFail flow
2498
- logger.info(`Rebooting with flow: ${onFailStep.name || onFailStep.type}`);
2499
- // Clean up all existing flows
2500
- while (getCurrentStackLength(engine) > 0) {
2501
- const flow = popFromCurrentStack(engine);
2502
- flow.transaction.fail(`Rebooted due to critical failure in ${step.tool}`);
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 = new FlowTransaction(rebootFlow.name, 'reboot-recovery', currentFlowFrame.userId);
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
- return `System rebooted due to critical failure in ${step.tool}`;
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 = new FlowTransaction(onFailFlow.name, 'onFail-recovery', currentFlowFrame.userId);
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
- ? interpolateMessage(step.value, [], currentFlowFrame?.variables, engine)
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
- const switchValue = currentFlowFrame.variables ? currentFlowFrame.variables[step.variable] : undefined;
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 only supports exact value matching for optimal performance
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 && typeof switchValue === 'string' && step.branches[switchValue]) {
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 all flows and start fresh
2891
- logger.info(`Rebooting with flow: ${subFlow.name}`);
2892
- // Clean up all existing flows
2893
- while (getCurrentStackLength(engine) > 0) {
2894
- const flow = popFromCurrentStack(engine);
2895
- flow.transaction.fail(`Rebooted to flow ${subFlow.name}`);
2896
- }
2897
- // Reset to empty stacks
2898
- // flowStacks = [[]];
2899
- // We must keep same reference to allow proper stack switching
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 sub-flow as a new root flow
2902
- const transaction = new FlowTransaction(subFlow.name, 'reboot', currentFlowFrame.userId);
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.fail(`Replaced by flow ${subFlow.name}`);
2931
- currentFlowFrame.transaction = new FlowTransaction(subFlow.name, 'replacement', currentFlowFrame.userId);
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 = new FlowTransaction(subFlow.name, 'sub-flow', currentFlowFrame.userId);
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 = null, explicitArgs) {
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, 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 = null, engine) {
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 = null, engine) {
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 = null) {
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.get(tool.implementation.function);
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
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool execution timeout after ${timeout}ms`)), timeout));
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
- const result = await Promise.race([fn(args), timeoutPromise]);
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
- // Unified expression evaluator - replaces both evaluateSafeExpression and evaluateSafeCondition
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: 'standard',
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(`Evaluating expression: ${expression} with options: ${JSON.stringify(opts)}`);
3752
- // Security check with configurable level
3753
- if (containsUnsafePatterns(expression, opts, engine)) {
3754
- logger.warn(`Blocked unsafe expression in ${opts.context}: ${expression}`);
3755
- return opts.returnType === 'boolean' ? false : `[blocked: ${expression}]`;
3756
- }
3757
- // Special handling for boolean return type with template expressions
3758
- // If the entire expression is wrapped in {{ }} and we want a boolean,
3759
- // evaluate the inner expression directly without string conversion
3760
- if (opts.returnType === 'boolean' && expression.startsWith('{{') && expression.endsWith('}}')) {
3761
- const innerExpression = expression.slice(2, -2).trim();
3762
- const result = evaluateExpression(innerExpression, variables, contextStack, {
3763
- ...opts,
3764
- returnType: 'boolean'
3765
- }, engine);
3766
- return result;
3767
- }
3768
- // First, check if this is a comparison or logical expression with template variables
3769
- let processedExpression = expression;
3770
- // If it has template variables, interpolate them first (for non-boolean contexts)
3771
- while (processedExpression.includes('{{') && processedExpression.includes('}}')) {
3772
- processedExpression = interpolateTemplateVariables(processedExpression, variables, contextStack, opts, engine);
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
- return true;
3989
- }
3990
- return false;
3991
- }
3992
- // Comparison expression evaluator (==, !=, <, >, <=, >=)
3993
- function evaluateComparisonExpression(expression, variables, contextStack, options, engine) {
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
- else {
4018
- logger.debug(`Replacing variable '${varName}' with JSON value:`, varValue);
4019
- return JSON.stringify(varValue);
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
- // Handle any remaining {{variable}} template expressions
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
- logger.info(`Comparison evaluation failed for '${expression}':`, error.message);
4060
- return false;
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
- // Helper function to check for comparison operators
4064
- function containsComparisonOperators(expression) {
4065
- return /[<>=!]+/.test(expression) &&
4066
- !(expression.includes('&&') || expression.includes('||')); // Not a logical expression
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
- // Clean unified expression interface functions
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
- // Use our enhanced template interpolation that supports nested {{ }} expressions
4092
- return interpolateTemplateVariables(template, variables, contextStack, {
4093
- securityLevel: 'standard',
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: 'auto'
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
- return evaluateExpression(condition, variables, [], {
4105
- securityLevel: 'standard',
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.fail("User confirmed flow switch");
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 = new FlowTransaction(targetFlow, 'confirmed-switch', userId);
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.complete();
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 = new FlowTransaction(targetFlow, 'intent-switch', userId);
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 ((flow = engine.flowStacks?.pop()?.[0])) {
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.fail(`User requested exit: ${input}`);
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 interruptionResult = await handleIntentInterruption(String(sanitizedInput), engine, userId);
4885
- if (interruptionResult) {
4886
- return interruptionResult;
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.fail(error.message);
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
- engine.lastChatTurn = {};
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
- this.lastChatTurn = {};
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
- // Initialize flow stacks and global accumulated messages
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
- const requiredMethods = ['info', 'warn', 'error', 'debug'];
5005
- for (const method of requiredMethods) {
5006
- if (typeof hostLogger[method] !== 'function') {
5007
- throw new Error(`Logger is missing required method: ${method}`);
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
- // Load session context if provided
5029
- if (engineSessionContext) {
5030
- logger = engineSessionContext.hostLogger;
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
- this.flowStacks = engineSessionContext.flowStacks;
5035
- this.globalAccumulatedMessages = engineSessionContext.globalAccumulatedMessages;
5036
- this.lastChatTurn = engineSessionContext.lastChatTurn;
5037
- this.globalVariables = engineSessionContext.globalVariables;
5038
- }
5039
- // Get userId from session context
5040
- const userId = engineSessionContext?.userId || 'anonymous';
5041
- this.lastActivity = new Date();
5042
- // Update session context with latest activity time
5043
- if (engineSessionContext) {
5044
- engineSessionContext.lastActivity = this.lastActivity;
5045
- }
5046
- // Role-based processing logic
5047
- if (contextEntry.role === 'user') {
5048
- // Detect intent to activate a flow or switch flows
5049
- const flowOrNull = await processActivity(String(contextEntry.content), userId, this);
5050
- // Store user turn in lastChatTurn if not in a flow
5051
- if (flowOrNull === null) {
5052
- this.lastChatTurn.user = contextEntry;
5053
- }
5054
- // Update session context with current state
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.flowStacks = this.flowStacks;
5057
- engineSessionContext.globalAccumulatedMessages = this.globalAccumulatedMessages;
5058
- engineSessionContext.lastChatTurn = this.lastChatTurn;
5059
- engineSessionContext.globalVariables = this.globalVariables || {};
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
- return flowOrNull;
5062
- }
5063
- else if (contextEntry.role === 'assistant') {
5064
- // Check if we're in a flow or not
5065
- if (getCurrentStackLength(this) === 0) {
5066
- // Not in a flow - store in lastChatTurn for context
5067
- this.lastChatTurn.assistant = contextEntry;
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
- // In a flow - add to current flow's context stack
5071
- const currentFlowFrame = getCurrentFlowFrame(this);
5072
- addToContextStack(currentFlowFrame.contextStack, 'assistant', contextEntry.content, contextEntry.stepId, contextEntry.toolName, contextEntry.metadata);
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
- // Update session context with current state
4894
+ // Return session context with error information
5075
4895
  if (engineSessionContext) {
5076
- engineSessionContext.flowStacks = this.flowStacks;
5077
- engineSessionContext.globalAccumulatedMessages = this.globalAccumulatedMessages;
5078
- engineSessionContext.lastChatTurn = this.lastChatTurn;
5079
- engineSessionContext.globalVariables = this.globalVariables || {};
4896
+ engineSessionContext.response = `Error: ${error.message}`;
4897
+ return engineSessionContext;
5080
4898
  }
5081
- // Return null to indicate no flow processing needed
5082
- return null;
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, engineSessionContext) {
5091
- // Load session context if provided
5092
- if (engineSessionContext) {
5093
- this.globalAccumulatedMessages = engineSessionContext.globalAccumulatedMessages;
4904
+ addAccumulatedMessage(message) {
4905
+ if (!this.sessionContext) {
4906
+ logger.warn('No session context available for addAccumulatedMessage');
4907
+ return;
5094
4908
  }
5095
- this.globalAccumulatedMessages.push(message);
5096
- // Update session context with new message
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(engineSessionContext) {
5103
- // Load session context if provided
5104
- if (engineSessionContext) {
5105
- this.globalAccumulatedMessages = engineSessionContext.globalAccumulatedMessages;
4915
+ getAndClearAccumulatedMessages() {
4916
+ if (!this.sessionContext) {
4917
+ logger.warn('No session context available for getAndClearAccumulatedMessages');
4918
+ return [];
5106
4919
  }
5107
- const messages = [...this.globalAccumulatedMessages];
5108
- this.globalAccumulatedMessages = [];
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
- initializeFlowStacks(this);
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-defined variables
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.get && this.APPROVED_FUNCTIONS.get(rootVar);
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})`);