jsfe 0.8.0 → 0.9.0

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
  }
@@ -1212,15 +1273,49 @@ function evaluateCondition(data, condition) {
1212
1273
  return false;
1213
1274
  }
1214
1275
  }
1215
- function interpolateObject(obj, data, args = {}) {
1276
+ function interpolateObject(obj, data, args = {}, engine) {
1216
1277
  if (typeof obj === 'string') {
1217
- // Handle template strings
1278
+ // Check if the string is ONLY a single template expression (e.g., '{{cargo}}')
1279
+ const singleTemplateMatch = obj.match(/^\{\{([^}]+)\}\}$/);
1280
+ if (singleTemplateMatch) {
1281
+ // This is a single template expression - return the actual value, not a string conversion
1282
+ const path = singleTemplateMatch[1].trim();
1283
+ try {
1284
+ let value = undefined;
1285
+ if (isPathTraversableObject(data)) {
1286
+ value = extractByPath(data, path);
1287
+ }
1288
+ // If not found in data and engine is available, check engine session variables
1289
+ if ((value === undefined || value === null)) {
1290
+ try {
1291
+ value = resolveEngineSessionVariable(path, engine);
1292
+ }
1293
+ catch (error) {
1294
+ // Ignore engine session variable resolution errors
1295
+ }
1296
+ }
1297
+ return value !== undefined && value !== null ? value : '';
1298
+ }
1299
+ catch (error) {
1300
+ return obj; // Keep original on error
1301
+ }
1302
+ }
1303
+ // Handle template strings with multiple expressions or mixed content
1218
1304
  return obj.replace(/\{\{([^}]+)\}\}/g, (match, path) => {
1219
1305
  try {
1220
1306
  let value = undefined;
1221
1307
  if (isPathTraversableObject(data)) {
1222
1308
  value = extractByPath(data, path.trim());
1223
1309
  }
1310
+ // If not found in data and engine is available, check engine session variables
1311
+ if ((value === undefined || value === null)) {
1312
+ try {
1313
+ value = resolveEngineSessionVariable(path.trim(), engine);
1314
+ }
1315
+ catch (error) {
1316
+ // Ignore engine session variable resolution errors
1317
+ }
1318
+ }
1224
1319
  return value !== null && value !== undefined ? String(value) : '';
1225
1320
  }
1226
1321
  catch (error) {
@@ -1237,24 +1332,34 @@ function interpolateObject(obj, data, args = {}) {
1237
1332
  });
1238
1333
  }
1239
1334
  else if (Array.isArray(obj)) {
1240
- return obj.map(item => interpolateObject(item, data, args));
1335
+ return obj.map(item => interpolateObject(item, data, args, engine));
1241
1336
  }
1242
1337
  else if (typeof obj === 'object' && obj !== null) {
1243
1338
  const result = {};
1244
1339
  for (const [key, value] of Object.entries(obj)) {
1245
- result[key] = interpolateObject(value, data, args);
1340
+ result[key] = interpolateObject(value, data, args, engine);
1246
1341
  }
1247
1342
  return result;
1248
1343
  }
1249
1344
  return obj;
1250
1345
  }
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
- };
1346
+ function makeLogger(level = process.env.LOG_LEVEL || "warn") {
1347
+ const ORDER = { debug: 10, info: 20, warn: 30, error: 40 };
1348
+ let current = ORDER[level] ?? ORDER.warn;
1349
+ const allow = (lvl) => ORDER[lvl] >= current;
1350
+ return {
1351
+ setLevel: (lvl) => { current = ORDER[lvl] ?? current; },
1352
+ debug: (...a) => { if (allow("debug"))
1353
+ console.debug(...a); },
1354
+ info: (...a) => { if (allow("info"))
1355
+ console.info(...a); },
1356
+ warn: (...a) => { if (allow("warn"))
1357
+ console.warn(...a); },
1358
+ error: (...a) => { if (allow("error"))
1359
+ console.error(...a); },
1360
+ };
1361
+ }
1362
+ let logger = makeLogger();
1258
1363
  // Fallback for any remaining console calls
1259
1364
  if (!global.console) {
1260
1365
  global.console = {
@@ -1264,73 +1369,6 @@ if (!global.console) {
1264
1369
  info: (...args) => logger.info(args.join(' '))
1265
1370
  };
1266
1371
  }
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
1372
  // === COMPREHENSIVE AUDIT LOGGING ===
1335
1373
  const auditLogger = {
1336
1374
  logFlowStart(flowName, input, userId, transactionId) {
@@ -1616,7 +1654,8 @@ function exportConversationHistory(contextStack, format = 'simple') {
1616
1654
  // Stored in engine.flowStacks for proper context isolation
1617
1655
  function initializeFlowStacks(engine) {
1618
1656
  try {
1619
- engine.flowStacks = [[]];
1657
+ // Use the engine method directly since Engine is now an alias for WorkflowEngine
1658
+ engine.initializeFlowStacks();
1620
1659
  }
1621
1660
  catch (error) {
1622
1661
  logger.error("Failed to initialize flow stacks:", error.message);
@@ -1631,7 +1670,11 @@ function pushToCurrentStack(engine, flowFrame) {
1631
1670
  getCurrentStack(engine).push(flowFrame);
1632
1671
  }
1633
1672
  function popFromCurrentStack(engine) {
1634
- return getCurrentStack(engine).pop();
1673
+ const result = getCurrentStack(engine).pop();
1674
+ if (!result) {
1675
+ throw new Error('Cannot pop from empty stack');
1676
+ }
1677
+ return result;
1635
1678
  }
1636
1679
  function createNewStack(engine) {
1637
1680
  // Create new stack and switch to it
@@ -1703,7 +1746,7 @@ async function isFlowActivated(input, engine, userId = 'anonymous') {
1703
1746
  const flowsMenu = engine.flowsMenu;
1704
1747
  const flow = await getFlowForInput(input, engine);
1705
1748
  if (flow) {
1706
- const transaction = new FlowTransaction(flow.name, 'user-input', userId);
1749
+ const transaction = TransactionManager.create(flow.name, 'user-input', userId);
1707
1750
  // Prepare tentative flow_init message that will be replaced by SAY-GET guidance if present
1708
1751
  const tentativeFlowInit = getSystemMessage(engine, 'flow_init', {
1709
1752
  flowName: flow.name,
@@ -1741,7 +1784,7 @@ async function playFlowFrame(engine) {
1741
1784
  auditLogger.logFlowExit(currentFlowFrame.flowName, currentFlowFrame.userId, currentFlowFrame.transaction.id, 'max_recursion_depth');
1742
1785
  // Do we need this?
1743
1786
  //currentFlowFrame.contextStack.push(error.message);
1744
- currentFlowFrame.transaction.fail("Max recursion depth exceeded");
1787
+ TransactionManager.fail(currentFlowFrame.transaction, "Max recursion depth exceeded");
1745
1788
  }
1746
1789
  throw error;
1747
1790
  }
@@ -1778,7 +1821,7 @@ async function playFlowFrame(engine) {
1778
1821
  if (currentFlowFrame.flowStepsStack.length === 0) {
1779
1822
  logger.info(`Flow ${currentFlowFrame.flowName} completed, popping from stack (steps length: ${currentFlowFrame.flowStepsStack.length})`);
1780
1823
  const completedFlow = popFromCurrentStack(engine);
1781
- completedFlow.transaction.complete();
1824
+ TransactionManager.complete(completedFlow.transaction);
1782
1825
  // When a flow completes, it doesn't "return" a value in the traditional sense.
1783
1826
  // It communicates results by setting variables in the shared `variables` object,
1784
1827
  // which are then accessible to the parent flow.
@@ -1832,6 +1875,9 @@ async function playFlowFrame(engine) {
1832
1875
  }
1833
1876
  continue;
1834
1877
  }
1878
+ else {
1879
+ logger.error(`Failed to resume flow - no flow frames available after switching stacks.`);
1880
+ }
1835
1881
  }
1836
1882
  else {
1837
1883
  logger.info(`No more flow frames to process, all flows completed.`);
@@ -1846,7 +1892,7 @@ async function playFlowFrame(engine) {
1846
1892
  try {
1847
1893
  const result = await playStep(currentFlowFrame, engine);
1848
1894
  const duration = Date.now() - startTime;
1849
- currentFlowFrame.transaction.addStep(step, result, duration, 'success');
1895
+ TransactionManager.addStep(currentFlowFrame.transaction, step, result, duration, 'success');
1850
1896
  logger.info(`Step ${step.type} executed successfully, result: ${typeof result === 'object' ? '[object]' : result}`);
1851
1897
  // If this was a SAY-GET step, return and wait for user input
1852
1898
  if (step.type === 'SAY-GET') {
@@ -1854,7 +1900,7 @@ async function playFlowFrame(engine) {
1854
1900
  if (currentFlowFrame.flowStepsStack.length === 0) {
1855
1901
  logger.info(`SAY-GET step was final step, flow ${currentFlowFrame.flowName} completed`);
1856
1902
  const completedFlow = popFromCurrentStack(engine);
1857
- completedFlow.transaction.complete();
1903
+ TransactionManager.complete(completedFlow.transaction);
1858
1904
  return result;
1859
1905
  }
1860
1906
  return result;
@@ -1864,7 +1910,7 @@ async function playFlowFrame(engine) {
1864
1910
  }
1865
1911
  catch (error) {
1866
1912
  const duration = Date.now() - startTime;
1867
- currentFlowFrame.transaction.addError(step, error, duration);
1913
+ TransactionManager.addError(currentFlowFrame.transaction, step, error, duration);
1868
1914
  logger.error(`Step ${step.type} failed: ${error.message}`);
1869
1915
  logger.info(`Stack trace: ${error.stack}`);
1870
1916
  throw error;
@@ -2494,18 +2540,29 @@ async function handleToolStep(currentFlowFrame, engine) {
2494
2540
  const onFailStep = Array.isArray(effectiveOnFail) ? effectiveOnFail[0] : effectiveOnFail;
2495
2541
  const callType = onFailStep.callType || "replace"; // Default to current behavior
2496
2542
  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}`);
2543
+ // Clear ALL flows across ALL stacks and start completely fresh with the onFail flow
2544
+ logger.info(`Rebooted due to 'reboot' type of onFail step: ${onFailStep.name} in flow ${currentFlowFrame.flowName} for tool ${step.tool}`);
2545
+ // Clean up ALL existing flows across ALL stacks (using proven user exit pattern)
2546
+ const exitedFlows = [];
2547
+ while (engine.flowStacks.length > 0) {
2548
+ const poppedStack = engine.flowStacks.pop();
2549
+ if (!poppedStack || poppedStack.length === 0) {
2550
+ continue;
2551
+ }
2552
+ // Process all flows in this stack
2553
+ for (const flow of poppedStack) {
2554
+ TransactionManager.fail(flow.transaction, `Rebooted due to 'reboot' type of onFail step: ${onFailStep.name} in flow ${flow?.flowName} for tool ${step.tool}`);
2555
+ exitedFlows.push(flow.flowName);
2556
+ auditLogger.logFlowExit(flow.flowName, currentFlowFrame.userId, flow.transaction.id, 'onFail_reboot');
2557
+ }
2503
2558
  }
2559
+ // Initialize completely fresh stack system (using proven pattern)
2560
+ initializeFlowStacks(engine);
2504
2561
  // Start the onFail flow as a new root flow
2505
2562
  if (onFailStep.type === "FLOW") {
2506
2563
  const rebootFlow = flowsMenu?.find(f => f.name === onFailStep.name);
2507
2564
  if (rebootFlow) {
2508
- const transaction = new FlowTransaction(rebootFlow.name, 'reboot-recovery', currentFlowFrame.userId);
2565
+ const transaction = TransactionManager.create(rebootFlow.name, 'reboot-recovery', currentFlowFrame.userId);
2509
2566
  pushToCurrentStack(engine, {
2510
2567
  flowName: rebootFlow.name,
2511
2568
  flowId: rebootFlow.id,
@@ -2525,7 +2582,8 @@ async function handleToolStep(currentFlowFrame, engine) {
2525
2582
  // Handle non-FLOW onFail steps in reboot mode
2526
2583
  currentFlowFrame.flowStepsStack = [onFailStep];
2527
2584
  }
2528
- return `System rebooted due to critical failure in ${step.tool}`;
2585
+ logger.info(`System completely rebooted due to onFail step for tool ${step.tool}. Exited flows: ${exitedFlows.join(', ')}`);
2586
+ return `System rebooted due to onFail step for tool ${step.tool}. Starting recovery flow ${onFailStep.name}`;
2529
2587
  }
2530
2588
  else if (callType === "replace") {
2531
2589
  // Current behavior - replace remaining steps in current flow
@@ -2543,7 +2601,7 @@ async function handleToolStep(currentFlowFrame, engine) {
2543
2601
  // Push onFail flow as sub-flow
2544
2602
  const onFailFlow = flowsMenu?.find(f => f.name === onFailStep.name);
2545
2603
  if (onFailFlow) {
2546
- const transaction = new FlowTransaction(onFailFlow.name, 'onFail-recovery', currentFlowFrame.userId);
2604
+ const transaction = TransactionManager.create(onFailFlow.name, 'onFail-recovery', currentFlowFrame.userId);
2547
2605
  pushToCurrentStack(engine, {
2548
2606
  flowName: onFailFlow.name,
2549
2607
  flowId: onFailFlow.id,
@@ -2785,12 +2843,12 @@ async function handleSwitchStep(currentFlowFrame, engine) {
2785
2843
  // Find the matching branch (exact value matching only)
2786
2844
  let selectedStep = null;
2787
2845
  let selectedBranch = null;
2788
- // SWITCH now only supports exact value matching for optimal performance
2846
+ // SWITCH now supports exact value matching for strings, booleans, and numbers
2789
2847
  // For conditional logic, use the CASE step instead
2790
- if (switchValue !== undefined && typeof switchValue === 'string' && step.branches[switchValue]) {
2791
- selectedStep = step.branches[switchValue];
2848
+ if (switchValue !== undefined && step.branches[String(switchValue)]) {
2849
+ selectedStep = step.branches[String(switchValue)];
2792
2850
  selectedBranch = String(switchValue);
2793
- logger.info(`SWITCH: selected exact match branch '${switchValue}'`);
2851
+ logger.info(`SWITCH: selected exact match branch '${switchValue}' (converted to string key '${String(switchValue)}')`);
2794
2852
  }
2795
2853
  // If no exact match found, use default
2796
2854
  if (!selectedStep && step.branches.default) {
@@ -2887,19 +2945,26 @@ async function handleSubFlowStep(currentFlowFrame, engine) {
2887
2945
  const callType = step.callType || "call"; // Default to normal sub-flow call
2888
2946
  logger.info(`Starting sub-flow ${subFlow.name} with callType: ${callType}, input: ${input}`);
2889
2947
  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
2948
+ // Clear ALL flows across ALL stacks and start completely fresh
2949
+ logger.info(`Rebooting system with flow: ${subFlow.name}`);
2950
+ // Clean up ALL existing flows across ALL stacks (using proven user exit pattern)
2951
+ const exitedFlows = [];
2952
+ while (engine.flowStacks.length > 0) {
2953
+ const poppedStack = engine.flowStacks.pop();
2954
+ if (!poppedStack || poppedStack.length === 0) {
2955
+ continue;
2956
+ }
2957
+ // Process all flows in this stack
2958
+ for (const flow of poppedStack) {
2959
+ TransactionManager.fail(flow.transaction, `System rebooted to flow ${subFlow.name}`);
2960
+ exitedFlows.push(flow.flowName);
2961
+ auditLogger.logFlowExit(flow.flowName, currentFlowFrame.userId, flow.transaction.id, 'system_reboot');
2962
+ }
2963
+ }
2964
+ // Initialize completely fresh stack system (using proven pattern)
2900
2965
  initializeFlowStacks(engine);
2901
- // Start the sub-flow as a new root flow
2902
- const transaction = new FlowTransaction(subFlow.name, 'reboot', currentFlowFrame.userId);
2966
+ // Start the reboot flow as the only flow in the system
2967
+ const transaction = TransactionManager.create(subFlow.name, 'reboot', currentFlowFrame.userId);
2903
2968
  pushToCurrentStack(engine, {
2904
2969
  flowName: subFlow.name,
2905
2970
  flowId: subFlow.id,
@@ -2913,6 +2978,7 @@ async function handleSubFlowStep(currentFlowFrame, engine) {
2913
2978
  startTime: Date.now()
2914
2979
  });
2915
2980
  auditLogger.logFlowStart(subFlow.name, input, currentFlowFrame.userId, transaction.id);
2981
+ logger.info(`System completely rebooted to flow ${subFlow.name}. Exited flows: ${exitedFlows.join(', ')}`);
2916
2982
  return `System rebooted to flow ${subFlow.name}`;
2917
2983
  }
2918
2984
  else if (callType === "replace") {
@@ -2927,15 +2993,15 @@ async function handleSubFlowStep(currentFlowFrame, engine) {
2927
2993
  addToContextStack(currentFlowFrame.contextStack, 'user', input);
2928
2994
  currentFlowFrame.inputStack.push(input);
2929
2995
  // Update transaction
2930
- currentFlowFrame.transaction.fail(`Replaced by flow ${subFlow.name}`);
2931
- currentFlowFrame.transaction = new FlowTransaction(subFlow.name, 'replacement', currentFlowFrame.userId);
2996
+ TransactionManager.fail(currentFlowFrame.transaction, `Replaced by flow ${subFlow.name}`);
2997
+ currentFlowFrame.transaction = TransactionManager.create(subFlow.name, 'replacement', currentFlowFrame.userId);
2932
2998
  auditLogger.logFlowStart(subFlow.name, input, currentFlowFrame.userId, currentFlowFrame.transaction.id);
2933
2999
  logger.debug(`About to return from handleSubFlowStep replacement, flowStepsStack length: ${currentFlowFrame.flowStepsStack.length}`);
2934
3000
  return `Flow replaced with ${subFlow.name}`;
2935
3001
  }
2936
3002
  else { // callType === "call" (default)
2937
3003
  // Normal sub-flow call - create new transaction for sub-flow
2938
- const subTransaction = new FlowTransaction(subFlow.name, 'sub-flow', currentFlowFrame.userId);
3004
+ const subTransaction = TransactionManager.create(subFlow.name, 'sub-flow', currentFlowFrame.userId);
2939
3005
  // Push sub-flow onto stack - INHERIT parent's variables for unified scope
2940
3006
  pushToCurrentStack(engine, {
2941
3007
  flowName: subFlow.name,
@@ -2963,7 +3029,7 @@ async function handleSubFlowStep(currentFlowFrame, engine) {
2963
3029
  }
2964
3030
  }
2965
3031
  // === TOOL CALLING AND ARGUMENT GENERATION SYSTEM ===
2966
- async function generateToolCallAndResponse(engine, toolName, input, contextStack = [], userId = 'anonymous', transactionId = null, flowFrame = null, explicitArgs) {
3032
+ async function generateToolCallAndResponse(engine, toolName, input, contextStack = [], userId = 'anonymous', transactionId = null, flowFrame, explicitArgs) {
2967
3033
  try {
2968
3034
  const toolsRegistry = engine.toolsRegistry;
2969
3035
  if (!toolsRegistry) {
@@ -2991,7 +3057,7 @@ async function generateToolCallAndResponse(engine, toolName, input, contextStack
2991
3057
  const contextStack = flowFrame.contextStack || [];
2992
3058
  logger.debug(`Interpolating args templates:`, rawArgs);
2993
3059
  logger.debug(`Available variables:`, variables);
2994
- rawArgs = interpolateObject(rawArgs, variables, variables);
3060
+ rawArgs = interpolateObject(rawArgs, variables, {}, engine);
2995
3061
  logger.debug(`Interpolated args:`, rawArgs);
2996
3062
  }
2997
3063
  catch (error) {
@@ -3007,7 +3073,7 @@ async function generateToolCallAndResponse(engine, toolName, input, contextStack
3007
3073
  throw error;
3008
3074
  }
3009
3075
  }
3010
- async function generateToolArgs(schema, input, contextStack = [], flowFrame = null, engine) {
3076
+ async function generateToolArgs(schema, input, contextStack = [], flowFrame, engine) {
3011
3077
  try {
3012
3078
  if (!schema || typeof schema !== 'object') {
3013
3079
  logger.warn(`Invalid schema provided for argument generation: ${schema}`);
@@ -3047,7 +3113,7 @@ async function generateToolArgs(schema, input, contextStack = [], flowFrame = nu
3047
3113
  throw new Error(`Failed to generate tool arguments: ${error.message}`);
3048
3114
  }
3049
3115
  }
3050
- async function generateArgsWithAI(schema, input, contextStack, flowFrame = null, engine) {
3116
+ async function generateArgsWithAI(schema, input, contextStack, flowFrame, engine) {
3051
3117
  const properties = schema.properties || {};
3052
3118
  const required = schema.required || [];
3053
3119
  const schemaDescription = Object.entries(properties)
@@ -3134,7 +3200,7 @@ Context Extraction Examples:
3134
3200
  }
3135
3201
  }
3136
3202
  // Enhanced pattern-based fallback that uses variables and input parsing
3137
- function generateEnhancedFallbackArgs(schema, input, flowFrame = null) {
3203
+ function generateEnhancedFallbackArgs(schema, input, flowFrame) {
3138
3204
  const properties = schema.properties || {};
3139
3205
  const required = schema.required || [];
3140
3206
  const args = {};
@@ -3255,12 +3321,96 @@ async function callTool(engine, tool, args, userId = 'anonymous', transactionId
3255
3321
  }
3256
3322
  // Apply timeout if configured
3257
3323
  const timeout = tool.implementation.timeout || 5000;
3258
- const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`Tool execution timeout after ${timeout}ms`)), timeout));
3324
+ let timeoutId;
3325
+ const timeoutPromise = new Promise((_, reject) => {
3326
+ timeoutId = setTimeout(() => reject(new Error(`Tool execution timeout after ${timeout}ms - ${tool.implementation.function}`)), timeout);
3327
+ });
3259
3328
  try {
3260
- const result = await Promise.race([fn(args), timeoutPromise]);
3329
+ // Determine how to call the function based on its signature and tool schema
3330
+ let result;
3331
+ // Check if the tool schema indicates individual parameters vs object parameter
3332
+ const toolSchema = tool.schema || tool.parameters || tool;
3333
+ // Handle different schema formats:
3334
+ // Format 1: JSONSchema with type="object" -> single object parameter
3335
+ // Format 2: Direct parameters object -> individual parameters
3336
+ let schemaProperties = {};
3337
+ let useObjectParameter = false;
3338
+ if (tool.parameters?.type === "object" && tool.parameters?.properties) {
3339
+ // Format 1: JSONSchema format { type: "object", properties: {...} }
3340
+ // Function expects single object parameter
3341
+ schemaProperties = tool.parameters.properties;
3342
+ useObjectParameter = true;
3343
+ logger.debug(`Detected JSONSchema format (type: object) - will use object parameter`);
3344
+ }
3345
+ else if (tool.parameters && typeof tool.parameters === 'object' && !tool.parameters.type) {
3346
+ // Format 2: Direct parameters { param1: {...}, param2: {...} }
3347
+ // Function expects individual parameters
3348
+ schemaProperties = tool.parameters;
3349
+ useObjectParameter = false;
3350
+ logger.debug(`Detected direct parameters format - will use individual parameters`);
3351
+ }
3352
+ else if (toolSchema?.properties) {
3353
+ // Fallback: Standard JSONSchema format
3354
+ schemaProperties = toolSchema.properties;
3355
+ useObjectParameter = toolSchema.type === "object";
3356
+ logger.debug(`Detected schema.properties format - object parameter: ${useObjectParameter}`);
3357
+ }
3358
+ const propertyNames = Object.keys(schemaProperties);
3359
+ // Get function parameter count
3360
+ const fnParamCount = fn.length;
3361
+ logger.debug(`Function ${tool.implementation.function} parameter count: ${fnParamCount}`);
3362
+ logger.debug(`Schema properties count: ${propertyNames.length}`);
3363
+ logger.debug(`Schema properties: ${propertyNames.join(', ')}`);
3364
+ logger.debug(`Use object parameter: ${useObjectParameter}`);
3365
+ // Strategy 1: If schema explicitly indicates object parameter OR function expects 1 param
3366
+ if (useObjectParameter || fnParamCount === 1) {
3367
+ logger.debug(`Calling function with single object parameter`);
3368
+ result = await Promise.race([fn(args), timeoutPromise]);
3369
+ }
3370
+ // Strategy 2: If function expects multiple parameters and we have multiple schema properties
3371
+ else if (fnParamCount > 1 && propertyNames.length > 1) {
3372
+ logger.debug(`Calling function with individual parameters (${fnParamCount} params expected)`);
3373
+ // For tools with 'parameters' property, use the required array or property order
3374
+ let orderedParamNames;
3375
+ if (tool.required && Array.isArray(tool.required)) {
3376
+ // Use required array order first, then add any optional parameters
3377
+ const optionalParams = propertyNames.filter(name => !tool.required.includes(name));
3378
+ orderedParamNames = [...tool.required, ...optionalParams];
3379
+ }
3380
+ else {
3381
+ // Fall back to property definition order (note: this may not be reliable in all JS engines)
3382
+ orderedParamNames = propertyNames;
3383
+ }
3384
+ logger.debug(`Parameter order: ${orderedParamNames.join(', ')}`);
3385
+ const orderedArgs = orderedParamNames.map(propName => {
3386
+ const value = args[propName];
3387
+ logger.debug(`Parameter ${propName}: ${JSON.stringify(value)}`);
3388
+ return value;
3389
+ });
3390
+ logger.debug(`Calling ${tool.implementation.function}(${orderedArgs.map(arg => typeof arg === 'object' ? '[object]' : arg).join(', ')})`);
3391
+ result = await Promise.race([fn(...orderedArgs), timeoutPromise]);
3392
+ }
3393
+ // Strategy 3: Function expects 0 parameters, call without arguments
3394
+ else if (fnParamCount === 0) {
3395
+ logger.debug(`Calling function with no parameters`);
3396
+ result = await Promise.race([fn(), timeoutPromise]);
3397
+ }
3398
+ // Fallback: Use object approach
3399
+ else {
3400
+ logger.debug(`Calling function with object parameter (fallback)`);
3401
+ result = await Promise.race([fn(args), timeoutPromise]);
3402
+ }
3403
+ // Clear the timeout since function completed successfully
3404
+ if (timeoutId) {
3405
+ clearTimeout(timeoutId);
3406
+ }
3261
3407
  return result;
3262
3408
  }
3263
3409
  catch (error) {
3410
+ // Clear the timeout on error as well
3411
+ if (timeoutId) {
3412
+ clearTimeout(timeoutId);
3413
+ }
3264
3414
  // Unconditional Retry logic for local functions
3265
3415
  const retries = tool.implementation.retries || 0;
3266
3416
  if (retries > 0) {
@@ -3297,7 +3447,7 @@ async function callHttpTool(tool, args, userId = 'anonymous', transactionId = nu
3297
3447
  // Apply response mapping if configured
3298
3448
  if (implementation.responseMapping) {
3299
3449
  try {
3300
- const mappedResult = applyResponseMapping(mockData, implementation.responseMapping, args);
3450
+ const mappedResult = applyResponseMapping(mockData, implementation.responseMapping, args, engine);
3301
3451
  logger.info(`[MOCK] Response mapping applied for ${tool.name}`);
3302
3452
  return mappedResult;
3303
3453
  }
@@ -3571,7 +3721,7 @@ async function callHttpTool(tool, args, userId = 'anonymous', transactionId = nu
3571
3721
  // Apply declarative response mapping if configured (preferred)
3572
3722
  if (implementation.responseMapping) {
3573
3723
  try {
3574
- return applyResponseMapping(data, implementation.responseMapping, mappingArgs);
3724
+ return applyResponseMapping(data, implementation.responseMapping, mappingArgs, engine);
3575
3725
  }
3576
3726
  catch (error) {
3577
3727
  logger.error(`Response mapping failed:`, error.message);
@@ -3792,6 +3942,11 @@ function evaluateExpression(expression, variables = {}, contextStack = [], optio
3792
3942
  const result = evaluateSafeMathematicalExpression(processedExpression, variables, contextStack, engine);
3793
3943
  return convertReturnType(result, opts.returnType);
3794
3944
  }
3945
+ // Handle typeof operator (e.g., "typeof variable", "typeof container")
3946
+ if (processedExpression.trim().startsWith('typeof ')) {
3947
+ const result = evaluateTypeofExpression(processedExpression, variables, contextStack, engine);
3948
+ return convertReturnType(result, opts.returnType);
3949
+ }
3795
3950
  // Handle simple variable paths (e.g., "user.name", "data.items.length")
3796
3951
  if (isSimpleVariablePath(processedExpression)) {
3797
3952
  const result = resolveSimpleVariable(processedExpression, variables, contextStack, engine);
@@ -4525,6 +4680,37 @@ function evaluateSafeMathematicalExpression(expression, variables, contextStack,
4525
4680
  return `[math-error: ${expression}]`;
4526
4681
  }
4527
4682
  }
4683
+ // Handle typeof operator expressions (e.g., "typeof variable", "typeof container.prop")
4684
+ function evaluateTypeofExpression(expression, variables, contextStack, engine) {
4685
+ try {
4686
+ // Extract the variable name after "typeof "
4687
+ const typeofMatch = expression.trim().match(/^typeof\s+(.+)$/);
4688
+ if (!typeofMatch) {
4689
+ logger.warn(`Invalid typeof expression: ${expression}`);
4690
+ return 'undefined';
4691
+ }
4692
+ const variablePath = typeofMatch[1].trim();
4693
+ logger.debug(`Evaluating typeof for variable path: ${variablePath}`);
4694
+ // Resolve the variable value
4695
+ let value;
4696
+ // Try to resolve from variables first
4697
+ if (variables && Object.keys(variables).length > 0) {
4698
+ value = variablePath.split('.').reduce((obj, part) => obj?.[part], variables);
4699
+ }
4700
+ // If not found in variables, check engine session variables
4701
+ if (value === undefined && engine) {
4702
+ value = resolveEngineSessionVariable(variablePath, engine);
4703
+ }
4704
+ // Return the JavaScript typeof result
4705
+ const typeResult = typeof value;
4706
+ logger.debug(`typeof ${variablePath} = ${typeResult} (value: ${JSON.stringify(value)})`);
4707
+ return typeResult;
4708
+ }
4709
+ catch (error) {
4710
+ logger.warn(`Typeof expression evaluation error: ${error.message}`);
4711
+ return 'undefined';
4712
+ }
4713
+ }
4528
4714
  // === ENHANCED FLOW CONTROL COMMANDS ===
4529
4715
  // Universal commands that work during any flow
4530
4716
  async function handleFlowControlCommands(input, engine, userId) {
@@ -4727,7 +4913,7 @@ async function handlePendingInterruptionSwitch(engine, userId) {
4727
4913
  return getSystemMessage(engine, 'flow_switch_error', { targetFlow: 'unknown' });
4728
4914
  }
4729
4915
  // Clean up current flow
4730
- currentFlowFrame.transaction.fail("User confirmed flow switch");
4916
+ TransactionManager.fail(currentFlowFrame.transaction, "User confirmed flow switch");
4731
4917
  popFromCurrentStack(engine);
4732
4918
  logger.info(`🔄 User confirmed switch from "${currentFlowName}" to "${targetFlow}"`);
4733
4919
  // Find and activate the target flow
@@ -4735,7 +4921,7 @@ async function handlePendingInterruptionSwitch(engine, userId) {
4735
4921
  if (!targetFlowDefinition) {
4736
4922
  return getSystemMessage(engine, 'flow_switch_error', { targetFlow });
4737
4923
  }
4738
- const newTransaction = new FlowTransaction(targetFlow, 'confirmed-switch', userId);
4924
+ const newTransaction = TransactionManager.create(targetFlow, 'confirmed-switch', userId);
4739
4925
  const newFlowFrame = {
4740
4926
  flowName: targetFlow,
4741
4927
  flowId: targetFlowDefinition.id,
@@ -4781,7 +4967,7 @@ async function handleRegularFlowInterruption(intentAnalysis, engine, userId) {
4781
4967
  const flowsMenu = engine.flowsMenu; // Access the global flows menu
4782
4968
  logger.info(`🔄 Processing flow interruption: "${userInput}" -> ${targetFlow}`);
4783
4969
  // For non-financial flows, allow graceful switching with option to resume
4784
- currentFlowFrame.transaction.complete();
4970
+ TransactionManager.complete(currentFlowFrame.transaction);
4785
4971
  // IMPORTANT: Clear pendingVariable from interrupted flow to avoid stale state when resuming
4786
4972
  if (currentFlowFrame.pendingVariable) {
4787
4973
  logger.info(`🧹 Clearing stale pendingVariable '${currentFlowFrame.pendingVariable}' from interrupted flow`);
@@ -4800,7 +4986,7 @@ async function handleRegularFlowInterruption(intentAnalysis, engine, userId) {
4800
4986
  // Activate the new flow
4801
4987
  const targetFlowDefinition = flowsMenu?.find(f => f.name === targetFlow);
4802
4988
  if (targetFlowDefinition) {
4803
- const newTransaction = new FlowTransaction(targetFlow, 'intent-switch', userId);
4989
+ const newTransaction = TransactionManager.create(targetFlow, 'intent-switch', userId);
4804
4990
  const newFlowFrame = {
4805
4991
  flowName: targetFlow,
4806
4992
  flowId: targetFlowDefinition.id,
@@ -4848,9 +5034,14 @@ async function handleFlowExit(engine, userId, input) {
4848
5034
  // Clean up all flows with proper transaction logging
4849
5035
  const exitedFlows = [];
4850
5036
  let flow;
4851
- while ((flow = engine.flowStacks?.pop()?.[0])) {
5037
+ while (engine.flowStacks.length > 0) {
5038
+ const poppedStack = engine.flowStacks.pop();
5039
+ if (!poppedStack || poppedStack.length === 0) {
5040
+ break;
5041
+ }
5042
+ flow = poppedStack[0];
4852
5043
  logger.info(`Exiting flow: ${flow.flowName} due to user request`);
4853
- flow.transaction.fail(`User requested exit: ${input}`);
5044
+ TransactionManager.fail(flow.transaction, `User requested exit: ${input}`);
4854
5045
  exitedFlows.push(flow.flowName);
4855
5046
  auditLogger.logFlowExit(flow.flowName, userId, flow.transaction.id, 'user_requested');
4856
5047
  }
@@ -4905,7 +5096,7 @@ async function processActivity(input, userId, engine) {
4905
5096
  // Clean up failed flow
4906
5097
  if (getCurrentStackLength(engine) > 0) {
4907
5098
  const failedFrame = popFromCurrentStack(engine);
4908
- failedFrame.transaction.fail(error.message);
5099
+ TransactionManager.fail(failedFrame.transaction, error.message);
4909
5100
  }
4910
5101
  return `I encountered an error: ${error.message}. Please try again or contact support if the issue persists.`;
4911
5102
  }
@@ -4917,7 +5108,12 @@ async function processActivity(input, userId, engine) {
4917
5108
  if (activatedFlow) {
4918
5109
  logger.info(`Flow activated: ${activatedFlow.name}`);
4919
5110
  // Clear lastChatTurn since we now have flow context
4920
- engine.lastChatTurn = {};
5111
+ const lastChatTurn = engine.lastChatTurn;
5112
+ if (lastChatTurn) {
5113
+ // Clear the existing properties
5114
+ delete lastChatTurn.user;
5115
+ delete lastChatTurn.assistant;
5116
+ }
4921
5117
  logger.debug(`Cleared lastChatTurn - now using flow context for AI operations`);
4922
5118
  const response = await playFlowFrame(engine);
4923
5119
  logger.info(`Initial flow response: ${response}`);
@@ -4940,6 +5136,16 @@ async function processActivity(input, userId, engine) {
4940
5136
  }
4941
5137
  // === WORKFLOW ENGINE CLASS ===
4942
5138
  export class WorkflowEngine {
5139
+ // Session-specific properties as getters - work directly with session data
5140
+ get flowStacks() {
5141
+ return this.sessionContext?.flowStacks || [[]];
5142
+ }
5143
+ get globalAccumulatedMessages() {
5144
+ return this.sessionContext?.globalAccumulatedMessages || [];
5145
+ }
5146
+ get lastChatTurn() {
5147
+ return this.sessionContext?.lastChatTurn || {};
5148
+ }
4943
5149
  /**
4944
5150
  * Initialize a new EngineSessionContext for a user session.
4945
5151
  * If hostLogger is null, uses the global default logger.
@@ -4956,7 +5162,8 @@ export class WorkflowEngine {
4956
5162
  */
4957
5163
  constructor(hostLogger, aiCallback, flowsMenu, toolsRegistry, APPROVED_FUNCTIONS, globalVariables, // Optional global variables shared across all new flows
4958
5164
  validateOnInit, language, messageRegistry, guidanceConfig) {
4959
- this.lastChatTurn = {};
5165
+ // Private session context - engine works directly with session data (no copying!)
5166
+ this.sessionContext = null;
4960
5167
  hostLogger = hostLogger || logger; // Fallback to global fake logger if none provided
4961
5168
  logger = hostLogger; // Assign to global logger
4962
5169
  this.aiCallback = aiCallback;
@@ -4974,9 +5181,7 @@ export class WorkflowEngine {
4974
5181
  separator: '\n\n',
4975
5182
  contextSelector: 'auto'
4976
5183
  };
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
5184
+ // No longer initialize session-specific data in constructor - it's now in sessionContext
4980
5185
  this.sessionId = crypto.randomUUID();
4981
5186
  this.createdAt = new Date();
4982
5187
  this.lastActivity = new Date();
@@ -4999,117 +5204,190 @@ export class WorkflowEngine {
4999
5204
  * const session = engine.initSession(yourLogger, 'user-123', 'session-456');
5000
5205
  */
5001
5206
  initSession(hostLogger, userId, sessionId) {
5002
- hostLogger = hostLogger || logger; // Fallback to global fake logger if none provided
5003
5207
  // 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}`);
5208
+ if (hostLogger) {
5209
+ const requiredMethods = ['info', 'warn', 'error', 'debug'];
5210
+ for (const method of requiredMethods) {
5211
+ if (typeof hostLogger[method] !== 'function') {
5212
+ throw new Error(`Logger is missing required method: ${method}`);
5213
+ }
5008
5214
  }
5009
5215
  }
5010
5216
  // Assign the session logger to the global logger
5011
5217
  logger = hostLogger || logger;
5012
5218
  const engineSessionContext = {
5013
- hostLogger: hostLogger,
5219
+ hostLogger: hostLogger || logger,
5014
5220
  sessionId: sessionId || crypto.randomUUID(),
5015
5221
  userId: userId,
5016
5222
  createdAt: new Date(),
5017
5223
  lastActivity: new Date(),
5018
- flowStacks: [[]],
5224
+ flowStacks: [[]], // Initialize as array with one empty stack (pure data)
5019
5225
  globalAccumulatedMessages: [],
5020
5226
  lastChatTurn: {},
5021
5227
  globalVariables: this.globalVariables ? { ...this.globalVariables } : {},
5022
- cargo: {}
5228
+ cargo: {},
5229
+ response: null, // Initialize with no response
5230
+ completedTransactions: [] // Initialize with no completed transactions
5023
5231
  };
5024
5232
  logger.info(`Engine session initialized: ${engineSessionContext.sessionId} for user: ${userId}`);
5025
5233
  return engineSessionContext;
5026
5234
  }
5027
5235
  async updateActivity(contextEntry, engineSessionContext) {
5028
- // Load session context if provided
5029
- if (engineSessionContext) {
5030
- logger = engineSessionContext.hostLogger;
5236
+ try {
5237
+ // Load session context if provided
5238
+ let hostLogger = engineSessionContext.hostLogger || logger; // Fallback to global logger if not provided
5239
+ if (engineSessionContext.hostLogger) {
5240
+ const requiredMethods = ['info', 'warn', 'error', 'debug'];
5241
+ for (const method of requiredMethods) {
5242
+ if (typeof engineSessionContext.hostLogger[method] !== 'function') {
5243
+ hostLogger = logger; // Fallback to global logger if hostLogger is missing methods
5244
+ break; // No need to throw error, just use global logger
5245
+ //throw new Error(`Logger is missing required method: ${method}`);
5246
+ }
5247
+ }
5248
+ }
5249
+ logger = hostLogger; // Assign to global logger
5031
5250
  this.cargo = engineSessionContext.cargo;
5251
+ logger.info(`Received Cargo: ${JSON.stringify(this.cargo)}`);
5032
5252
  this.sessionId = engineSessionContext.sessionId;
5033
5253
  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
5254
+ // Store reference to session context - no copying needed! Engine works directly with session data
5255
+ this.sessionContext = engineSessionContext;
5256
+ // Ensure session context has proper initialization
5257
+ if (!this.sessionContext.flowStacks || !Array.isArray(this.sessionContext.flowStacks)) {
5258
+ this.sessionContext.flowStacks = [[]];
5259
+ logger.warn('engineSessionContext.flowStacks was invalid, initialized fresh flowStacks');
5260
+ }
5261
+ if (!this.sessionContext.globalAccumulatedMessages) {
5262
+ this.sessionContext.globalAccumulatedMessages = [];
5263
+ }
5264
+ if (!this.sessionContext.lastChatTurn) {
5265
+ this.sessionContext.lastChatTurn = {};
5266
+ }
5267
+ if (!this.sessionContext.globalVariables) {
5268
+ this.sessionContext.globalVariables = {};
5269
+ }
5270
+ // Safety check: ensure flowStacks is always properly initialized
5271
+ if (this.sessionContext.flowStacks.length === 0) {
5272
+ logger.warn('flowStacks was empty, adding initial stack...');
5273
+ this.sessionContext.flowStacks.push([]); // Add empty stack
5274
+ }
5275
+ // Get userId from session context
5276
+ const userId = engineSessionContext?.userId || 'anonymous';
5277
+ this.lastActivity = new Date();
5278
+ // Update session context with latest activity time
5055
5279
  if (engineSessionContext) {
5056
- engineSessionContext.flowStacks = this.flowStacks;
5057
- engineSessionContext.globalAccumulatedMessages = this.globalAccumulatedMessages;
5058
- engineSessionContext.lastChatTurn = this.lastChatTurn;
5059
- engineSessionContext.globalVariables = this.globalVariables || {};
5280
+ engineSessionContext.lastActivity = this.lastActivity;
5281
+ }
5282
+ // Role-based processing logic
5283
+ if (contextEntry.role === 'user') {
5284
+ // Detect intent to activate a flow or switch flows
5285
+ const responseOrNull = await processActivity(String(contextEntry.content), userId, this);
5286
+ // Store user turn in lastChatTurn if not in a flow
5287
+ if (responseOrNull === null) {
5288
+ this.sessionContext.lastChatTurn.user = contextEntry;
5289
+ }
5290
+ // No copying needed! Session context already contains the latest data
5291
+ if (engineSessionContext) {
5292
+ engineSessionContext.response = responseOrNull; // Store the response from flow processing
5293
+ // Extract and store completed transactions for host access
5294
+ const newCompletedTransactions = [];
5295
+ for (const stack of this.flowStacks) {
5296
+ for (const frame of stack) {
5297
+ if (frame.transaction && (frame.transaction.state === 'completed' || frame.transaction.state === 'failed')) {
5298
+ newCompletedTransactions.push(frame.transaction);
5299
+ }
5300
+ }
5301
+ }
5302
+ if (!engineSessionContext.completedTransactions) {
5303
+ engineSessionContext.completedTransactions = [];
5304
+ }
5305
+ // Add any new completed transactions
5306
+ for (const transaction of newCompletedTransactions) {
5307
+ const existingTransaction = engineSessionContext.completedTransactions.find(t => t.id === transaction.id);
5308
+ if (!existingTransaction) {
5309
+ engineSessionContext.completedTransactions.push(transaction);
5310
+ }
5311
+ }
5312
+ }
5313
+ return engineSessionContext;
5060
5314
  }
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;
5315
+ else if (contextEntry.role === 'assistant') {
5316
+ // Check if we're in a flow or not
5317
+ if (getCurrentStackLength(this) === 0) {
5318
+ // Not in a flow - store in lastChatTurn for context
5319
+ this.sessionContext.lastChatTurn.assistant = contextEntry;
5320
+ }
5321
+ else {
5322
+ // In a flow - add to current flow's context stack
5323
+ const currentFlowFrame = getCurrentFlowFrame(this);
5324
+ addToContextStack(currentFlowFrame.contextStack, 'assistant', contextEntry.content, contextEntry.stepId, contextEntry.toolName, contextEntry.metadata);
5325
+ }
5326
+ // No copying needed! Session context already contains the latest data
5327
+ if (engineSessionContext) {
5328
+ engineSessionContext.response = null; // No response for assistant turns
5329
+ // Extract and store completed transactions for host access
5330
+ const newCompletedTransactions = [];
5331
+ for (const stack of this.flowStacks) {
5332
+ for (const frame of stack) {
5333
+ if (frame.transaction && (frame.transaction.state === 'completed' || frame.transaction.state === 'failed')) {
5334
+ newCompletedTransactions.push(frame.transaction);
5335
+ }
5336
+ }
5337
+ }
5338
+ if (!engineSessionContext.completedTransactions) {
5339
+ engineSessionContext.completedTransactions = [];
5340
+ }
5341
+ // Add any new completed transactions
5342
+ for (const transaction of newCompletedTransactions) {
5343
+ const existingTransaction = engineSessionContext.completedTransactions.find(t => t.id === transaction.id);
5344
+ if (!existingTransaction) {
5345
+ engineSessionContext.completedTransactions.push(transaction);
5346
+ }
5347
+ }
5348
+ }
5349
+ return engineSessionContext;
5068
5350
  }
5069
5351
  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);
5352
+ // Throw error for unsupported roles
5353
+ throw new Error(`Unsupported role '${contextEntry.role}' in updateActivity. Only 'user' and 'assistant' roles are supported.`);
5354
+ }
5355
+ }
5356
+ catch (error) {
5357
+ if (logger && logger.error) {
5358
+ logger.error(`Error in updateActivity: ${error.message}`);
5073
5359
  }
5074
- // Update session context with current state
5360
+ // Return session context with error information
5075
5361
  if (engineSessionContext) {
5076
- engineSessionContext.flowStacks = this.flowStacks;
5077
- engineSessionContext.globalAccumulatedMessages = this.globalAccumulatedMessages;
5078
- engineSessionContext.lastChatTurn = this.lastChatTurn;
5079
- engineSessionContext.globalVariables = this.globalVariables || {};
5362
+ engineSessionContext.response = `Error: ${error.message}`;
5363
+ return engineSessionContext;
5080
5364
  }
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.`);
5365
+ // If no session context provided, we can't return it - this should not happen
5366
+ throw error;
5087
5367
  }
5088
5368
  }
5089
5369
  // 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;
5370
+ addAccumulatedMessage(message) {
5371
+ if (!this.sessionContext) {
5372
+ logger.warn('No session context available for addAccumulatedMessage');
5373
+ return;
5094
5374
  }
5095
- this.globalAccumulatedMessages.push(message);
5096
- // Update session context with new message
5097
- if (engineSessionContext) {
5098
- engineSessionContext.globalAccumulatedMessages = this.globalAccumulatedMessages;
5375
+ if (!this.sessionContext.globalAccumulatedMessages) {
5376
+ this.sessionContext.globalAccumulatedMessages = [];
5099
5377
  }
5378
+ this.sessionContext.globalAccumulatedMessages.push(message);
5100
5379
  }
5101
5380
  // Get and clear all accumulated messages
5102
- getAndClearAccumulatedMessages(engineSessionContext) {
5103
- // Load session context if provided
5104
- if (engineSessionContext) {
5105
- this.globalAccumulatedMessages = engineSessionContext.globalAccumulatedMessages;
5381
+ getAndClearAccumulatedMessages() {
5382
+ if (!this.sessionContext) {
5383
+ logger.warn('No session context available for getAndClearAccumulatedMessages');
5384
+ return [];
5106
5385
  }
5107
- const messages = [...this.globalAccumulatedMessages];
5108
- this.globalAccumulatedMessages = [];
5109
- // Update session context with cleared messages
5110
- if (engineSessionContext) {
5111
- engineSessionContext.globalAccumulatedMessages = this.globalAccumulatedMessages;
5386
+ if (!this.sessionContext.globalAccumulatedMessages) {
5387
+ this.sessionContext.globalAccumulatedMessages = [];
5112
5388
  }
5389
+ const messages = [...this.sessionContext.globalAccumulatedMessages];
5390
+ this.sessionContext.globalAccumulatedMessages = [];
5113
5391
  return messages;
5114
5392
  }
5115
5393
  // Check if there are accumulated messages
@@ -5117,7 +5395,12 @@ export class WorkflowEngine {
5117
5395
  return this.globalAccumulatedMessages.length > 0;
5118
5396
  }
5119
5397
  initializeFlowStacks() {
5120
- initializeFlowStacks(this);
5398
+ if (this.sessionContext) {
5399
+ this.sessionContext.flowStacks = [[]];
5400
+ }
5401
+ else {
5402
+ logger.warn('No session context available for initializeFlowStacks');
5403
+ }
5121
5404
  }
5122
5405
  getCurrentStack() {
5123
5406
  return getCurrentStack(this);