@swizzy_ai/kit 1.0.4 → 1.0.6

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
@@ -47,6 +47,7 @@ class Step {
47
47
  this.beforeRun = config.beforeRun;
48
48
  this.afterRun = config.afterRun;
49
49
  this.model = config.model;
50
+ this.stream = config.stream ?? true; // Default to streaming enabled
50
51
  }
51
52
  validate(data) {
52
53
  const result = this.schema.safeParse(data);
@@ -55,8 +56,8 @@ class Step {
55
56
  }
56
57
  return result.data;
57
58
  }
58
- getContext(workflowContext) {
59
- return this.context ? this.context(workflowContext) : workflowContext;
59
+ async getContext(workflowContext) {
60
+ return this.context ? await this.context(workflowContext) : workflowContext;
60
61
  }
61
62
  }
62
63
 
@@ -380,18 +381,27 @@ class BungeeBuilder {
380
381
  /**
381
382
  * Add a single step execution.
382
383
  */
383
- this.add = (stepId) => {
384
- this._plan.destinations.push({ type: 'step', targetId: stepId });
384
+ this.add = (stepId, config) => {
385
+ this._plan.destinations.push({ type: 'step', targetId: stepId, config });
385
386
  return this;
386
387
  };
387
388
  /**
388
389
  * Add multiple executions based on count with config function.
389
390
  */
390
- this.batch = (stepId, count, configFn) => {
391
+ this.batch = (stepId, count, configFn, options) => {
391
392
  for (let i = 0; i < count; i++) {
392
- this._plan.destinations.push({ type: 'step', targetId: stepId });
393
+ this._plan.destinations.push({ type: 'step', targetId: stepId, config: configFn(i) });
394
+ }
395
+ // Apply batch-specific options
396
+ if (options?.optimistic !== undefined) {
397
+ this._plan.optimistic = options.optimistic;
398
+ }
399
+ if (options?.returnToAnchor !== undefined) {
400
+ this._plan.returnToAnchor = options.returnToAnchor;
401
+ }
402
+ if (options?.failWizardOnFailure !== undefined) {
403
+ this._plan.failWizardOnFailure = options.failWizardOnFailure;
393
404
  }
394
- this._plan.configFn = configFn;
395
405
  return this;
396
406
  };
397
407
  /**
@@ -401,6 +411,22 @@ class BungeeBuilder {
401
411
  if (options.concurrency !== undefined) {
402
412
  this._plan.concurrency = options.concurrency;
403
413
  }
414
+ if (options.optimistic !== undefined) {
415
+ this._plan.optimistic = options.optimistic;
416
+ }
417
+ if (options.returnToAnchor !== undefined) {
418
+ this._plan.returnToAnchor = options.returnToAnchor;
419
+ }
420
+ if (options.failWizardOnFailure !== undefined) {
421
+ this._plan.failWizardOnFailure = options.failWizardOnFailure;
422
+ }
423
+ return this;
424
+ };
425
+ /**
426
+ * Set completion callback.
427
+ */
428
+ this.onComplete = (callback) => {
429
+ this._plan.onComplete = callback;
404
430
  return this;
405
431
  };
406
432
  /**
@@ -428,8 +454,8 @@ class SchemaUtils {
428
454
  const shape = schema._def.shape();
429
455
  const fields = Object.entries(shape).map(([key, fieldSchema]) => {
430
456
  const type = this.getSchemaType(fieldSchema);
431
- const xmlExample = this.getXmlExample(key, type);
432
- return `${key}: ${type} - ${xmlExample}`;
457
+ this.getXmlExample(key, type);
458
+ return `${key}: ${type}`;
433
459
  });
434
460
  description = `Object with fields:\n${fields.join('\n')}`;
435
461
  }
@@ -479,8 +505,8 @@ class SchemaUtils {
479
505
  }
480
506
  static getXmlExample(key, type) {
481
507
  switch (type) {
482
- case 'string': return `<${key} tag-category="wizard" type="string">example`;
483
- case 'number': return `<${key} tag-category="wizard" type="number">123`;
508
+ case 'string': return `<${key} tag-category="wizard" type="string">[your text should be here]`;
509
+ case 'number': return `<${key} tag-category="wizard" type="number">[number should be here]`;
484
510
  case 'boolean': return `<${key} tag-category="wizard" type="boolean">true`;
485
511
  case 'array': return `<${key} tag-category="wizard" type="array">["item1", "item2"]`;
486
512
  default:
@@ -488,7 +514,7 @@ class SchemaUtils {
488
514
  const values = type.split(': ')[1].split(', ');
489
515
  return `<${key} tag-category="wizard" type="string">${values[0]}`;
490
516
  }
491
- return `<${key} tag-category="wizard" type="object"><subfield type="string">value</subfield>`;
517
+ return `<${key} tag-category="wizard" type="object"><subfield type="string">[text value should be here]</subfield>`;
492
518
  }
493
519
  }
494
520
  static objectToXml(obj, rootName = 'context') {
@@ -533,6 +559,20 @@ class VisualizationManager {
533
559
  this.connectedClients = new Set();
534
560
  this.maxWebSocketConnections = 10;
535
561
  this.wsIntervals = new WeakMap();
562
+ // Listen for state update events
563
+ this.wizard.on('state:update', (data) => {
564
+ this.sendToClients({
565
+ type: 'state_update',
566
+ ...data
567
+ });
568
+ });
569
+ // Listen for wizard stop events
570
+ this.wizard.on('wizard:stop', (data) => {
571
+ this.sendToClients({
572
+ type: 'wizard_stop',
573
+ ...data
574
+ });
575
+ });
536
576
  } // Wizard instance for callbacks
537
577
  getStepsInfo() {
538
578
  return this.wizard.steps.map((item) => {
@@ -696,6 +736,12 @@ class VisualizationManager {
696
736
  this.wizard.isPaused = false;
697
737
  this.wizard.isStepMode = false;
698
738
  this.sendToClients({ type: 'status_update', status: { isRunning: false, isPaused: false, isStepMode: false } });
739
+ // Emit wizard:stop event for manual stops
740
+ this.wizard.events.emit('wizard:stop', {
741
+ reason: 'manual_stop',
742
+ finalState: this.wizard.stateManager.getState(),
743
+ timestamp: Date.now()
744
+ });
699
745
  break;
700
746
  case 'replay':
701
747
  console.log('🔄 Replaying wizard - resetting state');
@@ -803,24 +849,33 @@ class BungeeExecutor {
803
849
  // Launch worker
804
850
  const workerPromise = this.launchBungeeWorker(plan, i);
805
851
  activeWorkers.add(workerPromise);
806
- // Respect concurrency limit
807
- if (activeWorkers.size >= plan.concurrency) {
808
- await Promise.race(activeWorkers);
809
- // Clean up completed workers
810
- for (const promise of activeWorkers) {
811
- if (promise !== workerPromise) {
812
- activeWorkers.delete(promise);
813
- }
852
+ }
853
+ // Wait for all workers to complete unless optimistic
854
+ if (!plan.optimistic) {
855
+ try {
856
+ await Promise.all(activeWorkers);
857
+ }
858
+ catch (error) {
859
+ if (plan.failWizardOnFailure !== false) { // Default true
860
+ throw error; // Re-throw to stop wizard
814
861
  }
862
+ // If failWizardOnFailure is false, just log and continue
863
+ console.error(`Bungee plan ${plan.id} had failures but continuing:`, error);
815
864
  }
816
865
  }
817
- // Wait for all workers to complete
818
- await Promise.all(activeWorkers);
819
- console.log(`✅ Bungee plan ${plan.id} completed, returning to anchor ${plan.anchorId}`);
866
+ if (plan.onComplete) {
867
+ return plan.onComplete(this.wizard);
868
+ }
869
+ if (plan.returnToAnchor !== false) { // Default true
870
+ console.log(`✅ Bungee plan ${plan.id} completed, returning to anchor ${plan.anchorId}`);
871
+ }
872
+ else {
873
+ console.log(`✅ Bungee plan ${plan.id} completed, proceeding to next step`);
874
+ }
820
875
  }
821
876
  async launchBungeeWorker(plan, index) {
822
877
  const destination = plan.destinations[index];
823
- const telescope = plan.configFn ? plan.configFn(index) : {};
878
+ const telescope = destination.config || {};
824
879
  const workerId = `${plan.id}_${destination.targetId}_${index}_${Date.now()}`;
825
880
  const telescopeContext = this.createTelescopeContext(this.wizard.workflowContext, telescope);
826
881
  const promise = this.executeWorkerStep(destination.targetId, telescopeContext);
@@ -837,10 +892,6 @@ class BungeeExecutor {
837
892
  try {
838
893
  await promise;
839
894
  }
840
- catch (error) {
841
- console.error(`Bungee worker ${workerId} failed:`, error);
842
- this.wizard.workflowContext[`${workerId}_error`] = error.message;
843
- }
844
895
  finally {
845
896
  // Clean up
846
897
  const planWorkers = this.bungeeWorkers.get(plan.id);
@@ -848,8 +899,10 @@ class BungeeExecutor {
848
899
  planWorkers.delete(workerId);
849
900
  if (planWorkers.size === 0) {
850
901
  this.bungeeWorkers.delete(plan.id);
851
- // Trigger reentry to anchor
852
- this.pendingReentry.add(plan.anchorId);
902
+ // Trigger reentry to anchor if configured
903
+ if (plan.returnToAnchor !== false) {
904
+ this.pendingReentry.add(plan.anchorId);
905
+ }
853
906
  }
854
907
  }
855
908
  }
@@ -872,9 +925,7 @@ class BungeeExecutor {
872
925
  return await step.update(stepData, telescopeContext, actions);
873
926
  }
874
927
  mergeWorkerResults(updates, telescope) {
875
- Object.entries(updates).forEach(([key, value]) => {
876
- this.wizard.workflowContext[key] = value;
877
- });
928
+ this.wizard.updateContext(updates);
878
929
  }
879
930
  async retriggerAnchor(anchorId) {
880
931
  const anchorStep = this.wizard.findStep(anchorId);
@@ -993,24 +1044,49 @@ class UsageTracker {
993
1044
  }
994
1045
  }
995
1046
 
996
- class ContextManager {
997
- constructor() {
998
- this.workflowContext = {};
1047
+ class StateManager {
1048
+ constructor(events) {
1049
+ this.events = events;
1050
+ this.state = {};
1051
+ }
1052
+ setState(updates) {
1053
+ let newUpdates;
1054
+ if (typeof updates === 'function') {
1055
+ // Higher-order function pattern like React setState
1056
+ newUpdates = updates(this.state);
1057
+ }
1058
+ else {
1059
+ // Object pattern
1060
+ newUpdates = updates;
1061
+ }
1062
+ const previousState = { ...this.state };
1063
+ this.state = { ...this.state, ...newUpdates };
1064
+ // Emit state update event
1065
+ this.events.emit('state:update', {
1066
+ previousState,
1067
+ newState: this.state,
1068
+ updates: newUpdates,
1069
+ timestamp: Date.now()
1070
+ });
1071
+ }
1072
+ getState() {
1073
+ return this.state;
999
1074
  }
1075
+ // Legacy methods for backward compatibility
1000
1076
  setContext(context) {
1001
- this.workflowContext = { ...this.workflowContext, ...context };
1077
+ this.setState(context);
1002
1078
  }
1003
1079
  getContext() {
1004
- return this.workflowContext;
1080
+ return this.getState();
1005
1081
  }
1006
1082
  updateContext(updates) {
1007
- this.workflowContext = { ...this.workflowContext, ...updates };
1083
+ this.setState(updates);
1008
1084
  }
1009
1085
  getWorkflowContext() {
1010
- return this.workflowContext;
1086
+ return this.getState();
1011
1087
  }
1012
- setWorkflowContext(context) {
1013
- this.workflowContext = context;
1088
+ setWorkflowContext(state) {
1089
+ this.state = state;
1014
1090
  }
1015
1091
  }
1016
1092
 
@@ -1042,10 +1118,10 @@ class Wizard {
1042
1118
  }
1043
1119
  // Getters for manager methods
1044
1120
  get workflowContext() {
1045
- return this.contextManager.getContext();
1121
+ return this.stateManager.getContext();
1046
1122
  }
1047
1123
  set workflowContext(value) {
1048
- this.contextManager.setWorkflowContext(value);
1124
+ this.stateManager.setWorkflowContext(value);
1049
1125
  }
1050
1126
  get log() {
1051
1127
  return this.isLoggingEnabled ? this.logger.log.bind(this.logger) : () => { };
@@ -1065,7 +1141,7 @@ class Wizard {
1065
1141
  this.isStepMode = false;
1066
1142
  this.skipStartWait = false;
1067
1143
  this.debouncedSendContextUpdate = this.debounce(() => {
1068
- this.visualizationManager.sendContextUpdate(this.contextManager.getContext());
1144
+ this.visualizationManager.sendContextUpdate(this.stateManager.getContext());
1069
1145
  }, 100);
1070
1146
  this.id = config.id;
1071
1147
  const registry = new ProviderRegistry();
@@ -1076,13 +1152,13 @@ class Wizard {
1076
1152
  this.isLoggingEnabled = config.logging ?? (process.env.NODE_ENV === 'development' || !process.env.NODE_ENV);
1077
1153
  // Initialize managers
1078
1154
  this.logger = new Logger(this.id);
1079
- this.contextManager = new ContextManager();
1155
+ this.events = new EventEmitter();
1156
+ this.stateManager = new StateManager(this.events);
1080
1157
  this.visualizationManager = new VisualizationManager(this);
1081
1158
  this.usageTracker = new UsageTracker(config.onUsage, (totalTokens, rate) => {
1082
1159
  this.visualizationManager.sendTokenUpdate(totalTokens, rate);
1083
1160
  });
1084
1161
  this.bungeeExecutor = new BungeeExecutor(this);
1085
- this.events = new EventEmitter();
1086
1162
  }
1087
1163
  addStep(config) {
1088
1164
  const step = new Step(config);
@@ -1126,10 +1202,12 @@ class Wizard {
1126
1202
  return this;
1127
1203
  }
1128
1204
  clearStepError(stepId) {
1129
- const context = this.contextManager.getContext();
1130
- delete context[`${stepId}_error`];
1131
- delete context[`${stepId}_retryCount`];
1132
- this.contextManager.setWorkflowContext(context);
1205
+ this.stateManager.setState((prevState) => {
1206
+ const newState = { ...prevState };
1207
+ delete newState[`${stepId}_error`];
1208
+ delete newState[`${stepId}_retryCount`];
1209
+ return newState;
1210
+ });
1133
1211
  }
1134
1212
  isStringSignal(signal) {
1135
1213
  return typeof signal === 'string';
@@ -1152,11 +1230,7 @@ class Wizard {
1152
1230
  this.currentStepIndex++;
1153
1231
  return true;
1154
1232
  default:
1155
- if (this.isBungeeJumpSignal(signal)) {
1156
- await this.bungeeExecutor.executeBungeePlan(signal.plan);
1157
- return true;
1158
- }
1159
- else if (this.isStringSignal(signal) && signal.startsWith('GOTO ')) {
1233
+ if (this.isStringSignal(signal) && signal.startsWith('GOTO ')) {
1160
1234
  const targetStepId = signal.substring(5);
1161
1235
  const targetIndex = this.findStepIndex(targetStepId);
1162
1236
  if (targetIndex !== -1) {
@@ -1167,6 +1241,28 @@ class Wizard {
1167
1241
  }
1168
1242
  return true;
1169
1243
  }
1244
+ else if (this.isBungeeJumpSignal(signal)) {
1245
+ try {
1246
+ const result = await this.bungeeExecutor.executeBungeePlan(signal.plan);
1247
+ if (result) {
1248
+ return await this.handleFlowControlSignal(result);
1249
+ }
1250
+ if (signal.plan.returnToAnchor === false) {
1251
+ this.currentStepIndex++; // Proceed to next step when not returning to anchor
1252
+ }
1253
+ return true;
1254
+ }
1255
+ catch (error) {
1256
+ console.error('Bungee plan failed:', error);
1257
+ this.workflowContext[`bungee_error`] = error.message;
1258
+ if (signal.plan.failWizardOnFailure !== false) { // Default true
1259
+ this.isRunning = false; // Stop the wizard on bungee failure
1260
+ return false;
1261
+ }
1262
+ // If failWizardOnFailure is false, continue
1263
+ return true;
1264
+ }
1265
+ }
1170
1266
  }
1171
1267
  return true;
1172
1268
  }
@@ -1261,10 +1357,17 @@ class Wizard {
1261
1357
  totalSteps: this.steps.length,
1262
1358
  timestamp: endTime
1263
1359
  });
1360
+ // Emit wizard:stop event
1361
+ this.events.emit('wizard:stop', {
1362
+ reason: 'completed',
1363
+ finalState: this.stateManager.getState(),
1364
+ timestamp: endTime
1365
+ });
1264
1366
  }
1265
1367
  createBaseActions() {
1266
1368
  return {
1267
1369
  updateContext: (updates) => this.updateContext(updates),
1370
+ setState: (updates) => this.stateManager.setState(updates),
1268
1371
  llmClient: this.llmClient,
1269
1372
  goto: (stepId) => this.goto(stepId),
1270
1373
  next: () => this.next(),
@@ -1276,6 +1379,7 @@ class Wizard {
1276
1379
  createWizardActions(anchorStepId = '') {
1277
1380
  return {
1278
1381
  ...this.createBaseActions(),
1382
+ setState: (updates) => this.stateManager.setState(updates),
1279
1383
  bungee: {
1280
1384
  init: () => new BungeeBuilder(anchorStepId)
1281
1385
  }
@@ -1286,6 +1390,9 @@ class Wizard {
1286
1390
  updateContext: (updates) => {
1287
1391
  this.bungeeExecutor.mergeWorkerResults(updates, telescope);
1288
1392
  },
1393
+ setState: (updates) => {
1394
+ this.bungeeExecutor.mergeWorkerResults(updates, telescope);
1395
+ },
1289
1396
  llmClient: this.llmClient,
1290
1397
  goto: () => Wizard.STOP,
1291
1398
  next: () => Wizard.STOP,
@@ -1324,7 +1431,7 @@ class Wizard {
1324
1431
  instruction: step.instruction,
1325
1432
  timestamp: stepStartTime
1326
1433
  });
1327
- const stepContext = step.getContext(this.contextManager.getContext());
1434
+ const stepContext = await step.getContext(this.stateManager.getContext());
1328
1435
  let processedInstruction = step.instruction;
1329
1436
  if (step.contextType === 'template' || step.contextType === 'both') {
1330
1437
  processedInstruction = this.applyTemplate(step.instruction, stepContext);
@@ -1519,124 +1626,98 @@ class Wizard {
1519
1626
  * Uses streaming for regular steps to provide real-time parsing and UI updates.
1520
1627
  * TextStep and ComputeStep use different approaches (non-streaming).
1521
1628
  */
1629
+ /**
1630
+ * Generates data for a wizard step by calling the LLM.
1631
+ * CLEANED VERSION: Separates formatting rules from logic to prevent hallucination.
1632
+ */
1522
1633
  async generateStepData(step, stepContext) {
1523
1634
  const systemContext = this.systemPrompt ? `${this.systemPrompt}\n\n` : '';
1635
+ // Build context strings
1524
1636
  const errorContext = this.workflowContext[`${step.id}_error`] ?
1525
- `\n\nPREVIOUS ERROR (attempt ${this.workflowContext[`${step.id}_retryCount`] || 1}):\n${this.workflowContext[`${step.id}_error`]}\nPlease fix this.` : '';
1637
+ `\n\n!!! PREVIOUS ERROR (Attempt ${this.workflowContext[`${step.id}_retryCount`] || 1}) !!!\nThe previous output caused this error: ${this.workflowContext[`${step.id}_error`]}\nYOU MUST FIX THIS.` : '';
1526
1638
  let processedInstruction = step.instruction;
1527
1639
  if (step.contextType === 'template' || step.contextType === 'both') {
1528
1640
  processedInstruction = this.applyTemplate(step.instruction, stepContext);
1529
1641
  }
1530
- this.log(() => `Processed instruction for step ${step.id}: ${processedInstruction}`);
1531
1642
  let contextSection = '';
1532
1643
  if (step.contextType === 'xml' || step.contextType === 'both' || !step.contextType) {
1533
- contextSection = `\n\nSTEP CONTEXT:\n${this.objectToXml(stepContext)}`;
1644
+ contextSection = `\n\n### CURRENT CONTEXT ###\n${this.objectToXml(stepContext)}`;
1534
1645
  }
1535
- this.log(() => `Context section for step ${step.id}: ${contextSection}`);
1646
+ // --- Text Step Handling ---
1536
1647
  if (step instanceof TextStep) {
1537
- const prompt = `${systemContext}You are executing a wizard step. Generate text for this step.
1648
+ const prompt = `${systemContext}
1649
+ TASK: Generate content for step "${step.id}".
1538
1650
 
1539
- STEP: ${step.id}
1540
- INSTRUCTION: ${processedInstruction}${errorContext}${contextSection}
1651
+ INSTRUCTION:
1652
+ ${processedInstruction}
1653
+ ${contextSection}
1654
+ ${errorContext}
1541
1655
 
1656
+ OUTPUT:
1542
1657
  Generate the text response now.`;
1543
- this.log(() => `Full prompt for step ${step.id}: ${prompt}`);
1544
1658
  let fullText = '';
1545
- await this.llmClient.complete({
1659
+ console.log("full prompt", prompt);
1660
+ const result = await this.llmClient.complete({
1546
1661
  prompt,
1547
1662
  model: step.model,
1548
1663
  maxTokens: 1000,
1549
1664
  temperature: 0.3,
1550
- stream: true,
1665
+ stream: step.stream,
1551
1666
  onChunk: (chunk) => {
1552
1667
  fullText += chunk;
1553
- // Emit raw chunk event for streaming text
1554
- this.events.emit('step:chunk', {
1555
- stepId: step.id,
1556
- chunk: chunk,
1557
- timestamp: Date.now()
1558
- });
1668
+ this.events.emit('step:chunk', { stepId: step.id, chunk, timestamp: Date.now() });
1559
1669
  },
1560
1670
  onUsage: this.usageTracker.updateUsage.bind(this.usageTracker)
1561
1671
  });
1562
- this.log(() => `LLM response for step ${step.id}: ${fullText}`);
1563
- console.log(`LLM response for step ${step.id}:`, fullText);
1672
+ if (result.text) {
1673
+ fullText = result.text;
1674
+ }
1564
1675
  return fullText;
1565
1676
  }
1566
- // For regular steps, use streaming XML parsing
1567
- // This allows real-time processing of LLM responses as they arrive
1677
+ // --- Regular XML Step Handling ---
1568
1678
  const parser = this.createStreamingXmlParser();
1569
1679
  let latestResult = {};
1570
1680
  const schemaDescription = SchemaUtils.describeSchema(step.schema, step.id);
1571
- const prompt = `${systemContext}You are executing a wizard step. Generate data for this step.
1572
-
1573
- STEP: ${step.id}
1574
- INSTRUCTION: ${processedInstruction}${errorContext}${contextSection}
1575
-
1576
- SCHEMA REQUIREMENTS:
1577
- ${schemaDescription}
1578
-
1579
- REQUIRED OUTPUT FORMAT:
1580
- Return a plain XML response with a root <response> tag.
1581
- CRITICAL: Every field MUST include tag-category="wizard" attribute. This is MANDATORY.
1582
- Every field MUST also include a type attribute (e.g., type="string", type="number", type="boolean", type="array").
1681
+ // CLEANER PROMPT STRUCTURE
1682
+ const prompt = `${systemContext}
1683
+ === GOAL ===
1684
+ You are an intelligent agent executing step: "${step.id}".
1685
+ Your task is to generate data that satisfies the INSTRUCTION below based on the CONTEXT.
1583
1686
 
1584
- ARRAY FORMATTING RULES (CRITICAL):
1585
- - Arrays MUST be valid JSON on a SINGLE line
1586
- - Use double quotes, not single quotes: ["a", "b"] NOT ['a', 'b']
1587
- - NO trailing commas: ["a", "b"] NOT ["a", "b",]
1588
- - NO line breaks inside arrays
1589
- - Example: <items tag-category="wizard" type="array">["apple", "banana", "orange"]
1687
+ === INSTRUCTION ===
1688
+ ${processedInstruction}
1689
+ ${contextSection}
1690
+ ${errorContext}
1590
1691
 
1591
- IMPORTANT PARSING RULES:
1592
- - Fields with tag-category="wizard" do NOT need closing tags
1593
- - Content ends when the next tag with tag-category="wizard" begins, OR when </response> is reached
1594
- - This means you can include ANY content (including code with <>, XML snippets, etc.) without worrying about breaking the parser
1595
- - Only fields marked with tag-category="wizard" will be parsed
1692
+ === RESPONSE FORMAT ===
1693
+ You must output a VALID XML object inside a <response> tag.
1694
+ 1. Every field must have: tag-category="wizard" and a type attribute (string, number, boolean, array).
1695
+ 2. Arrays must be single-line JSON: <tags tag-category="wizard" type="array">["a", "b"]
1596
1696
 
1597
- Example:
1598
- <response>
1599
- <name tag-category="wizard" type="string">John Smith
1600
- <age tag-category="wizard" type="number">25
1601
- <tags tag-category="wizard" type="array">["a", "b", "c"]
1602
- </response>
1697
+ === SCHEMA DEFINITION ===
1698
+ ${schemaDescription}
1603
1699
 
1604
- Notice: Arrays are compact JSON on one line! No closing tags needed for wizard fields.
1700
+ *** CRITICAL RULES ***
1701
+ 1. Do NOT copy values from the schema definition or examples above.
1702
+ 2. Generate NEW values based strictly on the "INSTRUCTION" and "CURRENT CONTEXT".
1703
+ 3. If the instruction implies a selection (like an ID), ensure the ID exists in the Context.
1605
1704
 
1606
- Generate the XML response now.`;
1607
- this.log(() => `Full prompt for step ${step.id}: ${prompt}`);
1608
- // Initiate streaming LLM call with chunk processing
1609
- // Each chunk is pushed to the parser for incremental XML parsing
1610
- await this.llmClient.complete({
1705
+ Generate the XML <response> now.`;
1706
+ // console.log(prompt); // Uncomment to debug the cleaner prompt
1707
+ step.stream !== false;
1708
+ const result = await this.llmClient.complete({
1611
1709
  prompt,
1612
1710
  model: step.model,
1613
1711
  maxTokens: 1000,
1614
- temperature: 0.3,
1615
- stream: true,
1712
+ temperature: 0.3, // Lower temp for precision
1713
+ stream: step.stream,
1616
1714
  onChunk: (chunk) => {
1617
- // Emit raw chunk event
1618
- this.events.emit('step:chunk', {
1619
- stepId: step.id,
1620
- chunk: chunk,
1621
- timestamp: Date.now()
1622
- });
1623
- // Process each incoming chunk through the streaming parser
1715
+ this.events.emit('step:chunk', { stepId: step.id, chunk, timestamp: Date.now() });
1624
1716
  const parseResult = parser.push(chunk);
1625
1717
  if (parseResult && !parseResult.done) {
1626
1718
  latestResult = parseResult.result;
1627
- // Emit parsed streaming event
1628
- this.events.emit('step:streaming', {
1629
- stepId: step.id,
1630
- data: latestResult,
1631
- timestamp: Date.now()
1632
- });
1633
- // Stream partial results out to connected clients via WebSocket
1634
- // This broadcasts real-time updates to UI and external consumers
1635
- this.visualizationManager.sendStepUpdate({
1636
- stepId: step.id,
1637
- status: 'streaming',
1638
- data: latestResult
1639
- });
1719
+ this.events.emit('step:streaming', { stepId: step.id, data: latestResult, timestamp: Date.now() });
1720
+ this.visualizationManager.sendStepUpdate({ stepId: step.id, status: 'streaming', data: latestResult });
1640
1721
  }
1641
1722
  else if (parseResult?.done) {
1642
1723
  latestResult = parseResult.result;
@@ -1644,20 +1725,19 @@ Generate the XML response now.`;
1644
1725
  },
1645
1726
  onUsage: this.usageTracker.updateUsage.bind(this.usageTracker)
1646
1727
  });
1647
- this.log(() => `Final parsed data: ${JSON.stringify(latestResult)}`);
1648
- this.log(() => `Parsed JSON data for step ${step.id}: ${JSON.stringify(latestResult)}`);
1728
+ if (result.text) {
1729
+ latestResult = result.text;
1730
+ }
1649
1731
  try {
1650
1732
  return step.validate(latestResult);
1651
1733
  }
1652
1734
  catch (validationError) {
1653
- this.log(() => `Validation failed for step ${step.id}: ${validationError.message}`);
1735
+ // Logic for repair remains the same...
1654
1736
  try {
1655
1737
  const repairedData = await this.repairSchemaData(latestResult, step.schema, validationError.message, step.id);
1656
- this.log(() => `Repaired data for step ${step.id}: ${JSON.stringify(repairedData)}`);
1657
1738
  return step.validate(repairedData);
1658
1739
  }
1659
1740
  catch (repairError) {
1660
- this.log(() => `Repair failed for step ${step.id}: ${repairError.message}`);
1661
1741
  return { __validationFailed: true, error: validationError.message };
1662
1742
  }
1663
1743
  }
@@ -1700,16 +1780,17 @@ Fix the data to match the schema and generate the XML response now.`;
1700
1780
  return repairedJsonData;
1701
1781
  }
1702
1782
  /**
1703
- * Creates a streaming XML parser for incremental processing of LLM responses.
1783
+ * Creates an improved streaming XML parser for incremental processing of LLM responses.
1704
1784
  *
1705
- * This parser processes XML chunks as they arrive from the LLM, extracting
1706
- * fields marked with tag-category="wizard" and parsing them based on their type attributes.
1785
+ * This parser is designed to handle partial chunks robustly and provides better error recovery.
1786
+ * It processes XML chunks as they arrive, extracting fields marked with tag-category="wizard".
1707
1787
  *
1708
- * Key features:
1709
- * - Incremental parsing: processes data as it streams in
1710
- * - Buffer management: accumulates chunks until complete fields are available
1711
- * - Type-aware parsing: handles strings, numbers, booleans, arrays, objects
1712
- * - Real-time updates: returns partial results as fields complete
1788
+ * Key improvements:
1789
+ * - Better partial tag handling
1790
+ * - More robust regex matching
1791
+ * - Improved buffer management
1792
+ * - Better error recovery for malformed chunks
1793
+ * - State machine approach for parsing
1713
1794
  *
1714
1795
  * @returns An object with a push method that accepts text chunks and returns parse results
1715
1796
  */
@@ -1718,57 +1799,131 @@ Fix the data to match the schema and generate the XML response now.`;
1718
1799
  let inResponse = false; // Tracks if we've entered the <response> tag
1719
1800
  const result = {}; // The final parsed JSON object
1720
1801
  let currentField = null; // Currently parsing field
1802
+ let parseErrors = 0; // Track consecutive parse errors
1721
1803
  return {
1722
1804
  push: (chunk) => {
1723
- buffer += chunk;
1724
- // Wait for <response> tag to start parsing
1725
- if (!inResponse && buffer.includes('<response>')) {
1726
- inResponse = true;
1727
- buffer = buffer.slice(buffer.indexOf('<response>') + 10); // Remove <response> from buffer
1728
- }
1729
- if (!inResponse)
1730
- return null; // Nothing to parse yet
1731
- // Look for wizard-tagged fields using regex pattern
1732
- const tagMatch = buffer.match(Wizard.WIZARD_TAG_PATTERN);
1733
- if (tagMatch) {
1734
- // If we were parsing a field, finalize it before starting new one
1735
- if (currentField) {
1736
- result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
1805
+ try {
1806
+ buffer += chunk;
1807
+ // Wait for <response> tag to start parsing
1808
+ if (!inResponse) {
1809
+ const responseStart = buffer.indexOf('<response>');
1810
+ if (responseStart !== -1) {
1811
+ inResponse = true;
1812
+ buffer = buffer.slice(responseStart + 10); // Remove <response> from buffer
1813
+ }
1814
+ else {
1815
+ return null; // Still waiting for response start
1816
+ }
1737
1817
  }
1738
- // Extract field name and type from the matched tag
1739
- const typeMatch = tagMatch[2].match(/type=["']([^"']+)["']/);
1740
- currentField = {
1741
- name: tagMatch[1], // Field name from tag
1742
- type: typeMatch?.[1]?.toLowerCase() || 'string', // Type attribute, default to string
1743
- content: '' // Will accumulate field content
1744
- };
1745
- // Remove the processed tag from buffer
1746
- buffer = buffer.slice(tagMatch.index + tagMatch[0].length);
1747
- }
1748
- // Accumulate content for the current field
1749
- if (currentField) {
1750
- // Find the next wizard tag or end of response
1751
- const nextTagIndex = buffer.search(/<\w+\s+[^>]*tag-category=["']wizard["']/);
1752
- if (nextTagIndex !== -1) {
1753
- // Found next tag, accumulate content up to it
1754
- currentField.content += buffer.slice(0, nextTagIndex);
1755
- buffer = buffer.slice(nextTagIndex);
1818
+ // Process buffer for wizard tags
1819
+ let processedSomething = false;
1820
+ // Continue processing while we have data
1821
+ while (buffer.length > 0) {
1822
+ // If we have a current field, try to accumulate content
1823
+ if (currentField) {
1824
+ // Look for the next wizard tag or end of response
1825
+ const nextWizardTag = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*>/);
1826
+ const responseEnd = buffer.indexOf('</response>');
1827
+ if (nextWizardTag && nextWizardTag.index !== undefined) {
1828
+ // Found next tag, finalize current field
1829
+ const contentEnd = nextWizardTag.index;
1830
+ currentField.content += buffer.slice(0, contentEnd);
1831
+ result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
1832
+ currentField = null;
1833
+ buffer = buffer.slice(contentEnd);
1834
+ processedSomething = true;
1835
+ }
1836
+ else if (responseEnd !== -1) {
1837
+ // End of response, finalize current field
1838
+ currentField.content += buffer.slice(0, responseEnd);
1839
+ result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
1840
+ return { done: true, result }; // Parsing complete
1841
+ }
1842
+ else {
1843
+ // No complete field yet, but we might have a partial tag at the end
1844
+ // Check if buffer ends with a partial wizard tag
1845
+ const partialTagMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*$/);
1846
+ if (partialTagMatch) {
1847
+ // Buffer ends with partial tag, keep it for next chunk
1848
+ break;
1849
+ }
1850
+ else {
1851
+ // Safe to accumulate entire buffer
1852
+ currentField.content += buffer;
1853
+ buffer = '';
1854
+ processedSomething = true;
1855
+ }
1856
+ }
1857
+ }
1858
+ else {
1859
+ // No current field, look for a new wizard tag
1860
+ const tagMatch = buffer.match(Wizard.WIZARD_TAG_PATTERN);
1861
+ if (tagMatch && tagMatch.index === 0) {
1862
+ // Tag starts at beginning of buffer
1863
+ const typeMatch = tagMatch[2].match(/type=["']([^"']+)["']/);
1864
+ currentField = {
1865
+ name: tagMatch[1],
1866
+ type: typeMatch?.[1]?.toLowerCase() || 'string',
1867
+ content: ''
1868
+ };
1869
+ buffer = buffer.slice(tagMatch[0].length);
1870
+ processedSomething = true;
1871
+ }
1872
+ else if (tagMatch && tagMatch.index !== undefined && tagMatch.index > 0) {
1873
+ // Tag exists but not at start - might be partial
1874
+ const partialTagMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*$/);
1875
+ if (partialTagMatch) {
1876
+ // Buffer ends with partial tag, wait for more data
1877
+ break;
1878
+ }
1879
+ else {
1880
+ // Tag is in middle, process up to it
1881
+ // This shouldn't happen in well-formed XML, but handle gracefully
1882
+ buffer = buffer.slice(tagMatch.index);
1883
+ continue;
1884
+ }
1885
+ }
1886
+ else {
1887
+ // No wizard tag found
1888
+ if (buffer.includes('</response>')) {
1889
+ // End of response without finalizing a field
1890
+ return { done: true, result };
1891
+ }
1892
+ // Check for partial tag at end
1893
+ const partialTagMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*$/);
1894
+ if (partialTagMatch) {
1895
+ break; // Wait for more data
1896
+ }
1897
+ // No partial tag, buffer might contain non-wizard content
1898
+ // This is unusual but we'll keep it for now
1899
+ break;
1900
+ }
1901
+ }
1756
1902
  }
1757
- else if (buffer.includes('</response>')) {
1758
- // End of response reached, finalize current field
1759
- const endIndex = buffer.indexOf('</response>');
1760
- currentField.content += buffer.slice(0, endIndex);
1761
- result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
1762
- return { done: true, result }; // Parsing complete
1903
+ // Reset error counter on successful processing
1904
+ if (processedSomething) {
1905
+ parseErrors = 0;
1763
1906
  }
1764
- else {
1765
- // No complete field yet, accumulate entire buffer
1766
- currentField.content += buffer;
1767
- buffer = '';
1907
+ // Return partial result for UI updates
1908
+ return { done: false, result: { ...result } };
1909
+ }
1910
+ catch (error) {
1911
+ parseErrors++;
1912
+ console.warn(`Streaming XML parse error (attempt ${parseErrors}):`, error instanceof Error ? error.message : String(error));
1913
+ // If we have too many consecutive errors, try to recover
1914
+ if (parseErrors > 3) {
1915
+ console.error('Too many parse errors, attempting recovery');
1916
+ // Try to find next valid wizard tag and restart from there
1917
+ const recoveryMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*>/);
1918
+ if (recoveryMatch && recoveryMatch.index !== undefined && recoveryMatch.index > 0) {
1919
+ buffer = buffer.slice(recoveryMatch.index);
1920
+ parseErrors = 0; // Reset error counter
1921
+ return { done: false, result: { ...result } };
1922
+ }
1768
1923
  }
1924
+ // Return current result even with errors
1925
+ return { done: false, result: { ...result } };
1769
1926
  }
1770
- // Return partial result for UI updates
1771
- return { done: false, result: { ...result } };
1772
1927
  }
1773
1928
  };
1774
1929
  }