@swizzy_ai/kit 1.0.3 → 1.0.5

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') {
@@ -803,24 +829,33 @@ class BungeeExecutor {
803
829
  // Launch worker
804
830
  const workerPromise = this.launchBungeeWorker(plan, i);
805
831
  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
- }
832
+ }
833
+ // Wait for all workers to complete unless optimistic
834
+ if (!plan.optimistic) {
835
+ try {
836
+ await Promise.all(activeWorkers);
837
+ }
838
+ catch (error) {
839
+ if (plan.failWizardOnFailure !== false) { // Default true
840
+ throw error; // Re-throw to stop wizard
814
841
  }
842
+ // If failWizardOnFailure is false, just log and continue
843
+ console.error(`Bungee plan ${plan.id} had failures but continuing:`, error);
815
844
  }
816
845
  }
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}`);
846
+ if (plan.onComplete) {
847
+ return plan.onComplete(this.wizard);
848
+ }
849
+ if (plan.returnToAnchor !== false) { // Default true
850
+ console.log(`✅ Bungee plan ${plan.id} completed, returning to anchor ${plan.anchorId}`);
851
+ }
852
+ else {
853
+ console.log(`✅ Bungee plan ${plan.id} completed, proceeding to next step`);
854
+ }
820
855
  }
821
856
  async launchBungeeWorker(plan, index) {
822
857
  const destination = plan.destinations[index];
823
- const telescope = plan.configFn ? plan.configFn(index) : {};
858
+ const telescope = destination.config || {};
824
859
  const workerId = `${plan.id}_${destination.targetId}_${index}_${Date.now()}`;
825
860
  const telescopeContext = this.createTelescopeContext(this.wizard.workflowContext, telescope);
826
861
  const promise = this.executeWorkerStep(destination.targetId, telescopeContext);
@@ -837,10 +872,6 @@ class BungeeExecutor {
837
872
  try {
838
873
  await promise;
839
874
  }
840
- catch (error) {
841
- console.error(`Bungee worker ${workerId} failed:`, error);
842
- this.wizard.workflowContext[`${workerId}_error`] = error.message;
843
- }
844
875
  finally {
845
876
  // Clean up
846
877
  const planWorkers = this.bungeeWorkers.get(plan.id);
@@ -848,8 +879,10 @@ class BungeeExecutor {
848
879
  planWorkers.delete(workerId);
849
880
  if (planWorkers.size === 0) {
850
881
  this.bungeeWorkers.delete(plan.id);
851
- // Trigger reentry to anchor
852
- this.pendingReentry.add(plan.anchorId);
882
+ // Trigger reentry to anchor if configured
883
+ if (plan.returnToAnchor !== false) {
884
+ this.pendingReentry.add(plan.anchorId);
885
+ }
853
886
  }
854
887
  }
855
888
  }
@@ -872,9 +905,7 @@ class BungeeExecutor {
872
905
  return await step.update(stepData, telescopeContext, actions);
873
906
  }
874
907
  mergeWorkerResults(updates, telescope) {
875
- Object.entries(updates).forEach(([key, value]) => {
876
- this.wizard.workflowContext[key] = value;
877
- });
908
+ this.wizard.updateContext(updates);
878
909
  }
879
910
  async retriggerAnchor(anchorId) {
880
911
  const anchorStep = this.wizard.findStep(anchorId);
@@ -893,19 +924,38 @@ class BungeeExecutor {
893
924
 
894
925
  class Logger {
895
926
  constructor(id) {
896
- // Create logs directory if it doesn't exist
897
- const logsDir = path__namespace.join(process.cwd(), '.wizard');
898
- if (!fs__namespace.existsSync(logsDir)) {
899
- fs__namespace.mkdirSync(logsDir, { recursive: true });
927
+ this.isFileSystemAvailable = false;
928
+ // Try to set up file logging, but gracefully handle environments without file system
929
+ try {
930
+ const logsDir = path__namespace.join(process.cwd(), '.wizard');
931
+ // Check if we can access the directory
932
+ try {
933
+ fs__namespace.accessSync(logsDir, fs__namespace.constants.F_OK);
934
+ }
935
+ catch {
936
+ // Directory doesn't exist, try to create it
937
+ fs__namespace.mkdirSync(logsDir, { recursive: true });
938
+ }
939
+ // If we get here, file system is available
940
+ this.isFileSystemAvailable = true;
941
+ this.logFilePath = path__namespace.join(logsDir, `${id}.log`);
942
+ }
943
+ catch (error) {
944
+ // File system operations failed (e.g., Cloudflare Workers, restricted environments)
945
+ this.isFileSystemAvailable = false;
946
+ // Don't log here to avoid recursion - console logging will be used as fallback
900
947
  }
901
- this.logFilePath = path__namespace.join(logsDir, `${id}.log`);
902
948
  }
903
949
  log(messageOrFn) {
904
- if (!this.logFilePath)
905
- return; // Early exit if logging disabled
906
950
  const message = typeof messageOrFn === 'function' ? messageOrFn() : messageOrFn;
907
951
  const content = `${new Date().toISOString()}: ${message}\n`;
908
- this.appendToFile(content);
952
+ if (this.isFileSystemAvailable && this.logFilePath) {
953
+ this.appendToFile(content);
954
+ }
955
+ else {
956
+ // Fallback to console logging when file system is not available
957
+ console.log('Wizard log:', content.trim());
958
+ }
909
959
  }
910
960
  appendToFile(content) {
911
961
  if (!this.logFilePath)
@@ -918,7 +968,7 @@ class Logger {
918
968
  }
919
969
  }
920
970
  async getLog() {
921
- if (!this.logFilePath || !fs__namespace.existsSync(this.logFilePath))
971
+ if (!this.isFileSystemAvailable || !this.logFilePath || !fs__namespace.existsSync(this.logFilePath))
922
972
  return '';
923
973
  try {
924
974
  return await fs__namespace.promises.readFile(this.logFilePath, 'utf8');
@@ -1124,6 +1174,7 @@ class Wizard {
1124
1174
  this.currentStepIndex++;
1125
1175
  return true;
1126
1176
  case Wizard.STOP:
1177
+ this.isRunning = false; // Stop the entire wizard execution
1127
1178
  return false;
1128
1179
  case Wizard.RETRY:
1129
1180
  return true;
@@ -1132,11 +1183,7 @@ class Wizard {
1132
1183
  this.currentStepIndex++;
1133
1184
  return true;
1134
1185
  default:
1135
- if (this.isBungeeJumpSignal(signal)) {
1136
- await this.bungeeExecutor.executeBungeePlan(signal.plan);
1137
- return true;
1138
- }
1139
- else if (this.isStringSignal(signal) && signal.startsWith('GOTO ')) {
1186
+ if (this.isStringSignal(signal) && signal.startsWith('GOTO ')) {
1140
1187
  const targetStepId = signal.substring(5);
1141
1188
  const targetIndex = this.findStepIndex(targetStepId);
1142
1189
  if (targetIndex !== -1) {
@@ -1147,10 +1194,33 @@ class Wizard {
1147
1194
  }
1148
1195
  return true;
1149
1196
  }
1197
+ else if (this.isBungeeJumpSignal(signal)) {
1198
+ try {
1199
+ const result = await this.bungeeExecutor.executeBungeePlan(signal.plan);
1200
+ if (result) {
1201
+ return await this.handleFlowControlSignal(result);
1202
+ }
1203
+ if (signal.plan.returnToAnchor === false) {
1204
+ this.currentStepIndex++; // Proceed to next step when not returning to anchor
1205
+ }
1206
+ return true;
1207
+ }
1208
+ catch (error) {
1209
+ console.error('Bungee plan failed:', error);
1210
+ this.workflowContext[`bungee_error`] = error.message;
1211
+ if (signal.plan.failWizardOnFailure !== false) { // Default true
1212
+ this.isRunning = false; // Stop the wizard on bungee failure
1213
+ return false;
1214
+ }
1215
+ // If failWizardOnFailure is false, continue
1216
+ return true;
1217
+ }
1218
+ }
1150
1219
  }
1151
1220
  return true;
1152
1221
  }
1153
1222
  async initializeRun() {
1223
+ // Only wait for UI command if visualization server is actually running
1154
1224
  if (this.visualizationServer) {
1155
1225
  console.log('🎯 Waiting for UI to start wizard execution...');
1156
1226
  this.sendToClients({ type: 'status_update', status: { waitingForStart: true, isStepMode: false } });
@@ -1303,7 +1373,7 @@ class Wizard {
1303
1373
  instruction: step.instruction,
1304
1374
  timestamp: stepStartTime
1305
1375
  });
1306
- const stepContext = step.getContext(this.contextManager.getContext());
1376
+ const stepContext = await step.getContext(this.contextManager.getContext());
1307
1377
  let processedInstruction = step.instruction;
1308
1378
  if (step.contextType === 'template' || step.contextType === 'both') {
1309
1379
  processedInstruction = this.applyTemplate(step.instruction, stepContext);
@@ -1498,124 +1568,98 @@ class Wizard {
1498
1568
  * Uses streaming for regular steps to provide real-time parsing and UI updates.
1499
1569
  * TextStep and ComputeStep use different approaches (non-streaming).
1500
1570
  */
1571
+ /**
1572
+ * Generates data for a wizard step by calling the LLM.
1573
+ * CLEANED VERSION: Separates formatting rules from logic to prevent hallucination.
1574
+ */
1501
1575
  async generateStepData(step, stepContext) {
1502
1576
  const systemContext = this.systemPrompt ? `${this.systemPrompt}\n\n` : '';
1577
+ // Build context strings
1503
1578
  const errorContext = this.workflowContext[`${step.id}_error`] ?
1504
- `\n\nPREVIOUS ERROR (attempt ${this.workflowContext[`${step.id}_retryCount`] || 1}):\n${this.workflowContext[`${step.id}_error`]}\nPlease fix this.` : '';
1579
+ `\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.` : '';
1505
1580
  let processedInstruction = step.instruction;
1506
1581
  if (step.contextType === 'template' || step.contextType === 'both') {
1507
1582
  processedInstruction = this.applyTemplate(step.instruction, stepContext);
1508
1583
  }
1509
- this.log(() => `Processed instruction for step ${step.id}: ${processedInstruction}`);
1510
1584
  let contextSection = '';
1511
1585
  if (step.contextType === 'xml' || step.contextType === 'both' || !step.contextType) {
1512
- contextSection = `\n\nSTEP CONTEXT:\n${this.objectToXml(stepContext)}`;
1586
+ contextSection = `\n\n### CURRENT CONTEXT ###\n${this.objectToXml(stepContext)}`;
1513
1587
  }
1514
- this.log(() => `Context section for step ${step.id}: ${contextSection}`);
1588
+ // --- Text Step Handling ---
1515
1589
  if (step instanceof TextStep) {
1516
- const prompt = `${systemContext}You are executing a wizard step. Generate text for this step.
1590
+ const prompt = `${systemContext}
1591
+ TASK: Generate content for step "${step.id}".
1517
1592
 
1518
- STEP: ${step.id}
1519
- INSTRUCTION: ${processedInstruction}${errorContext}${contextSection}
1593
+ INSTRUCTION:
1594
+ ${processedInstruction}
1595
+ ${contextSection}
1596
+ ${errorContext}
1520
1597
 
1598
+ OUTPUT:
1521
1599
  Generate the text response now.`;
1522
- this.log(() => `Full prompt for step ${step.id}: ${prompt}`);
1523
1600
  let fullText = '';
1524
- await this.llmClient.complete({
1601
+ console.log("full prompt", prompt);
1602
+ const result = await this.llmClient.complete({
1525
1603
  prompt,
1526
1604
  model: step.model,
1527
1605
  maxTokens: 1000,
1528
1606
  temperature: 0.3,
1529
- stream: true,
1607
+ stream: step.stream,
1530
1608
  onChunk: (chunk) => {
1531
1609
  fullText += chunk;
1532
- // Emit raw chunk event for streaming text
1533
- this.events.emit('step:chunk', {
1534
- stepId: step.id,
1535
- chunk: chunk,
1536
- timestamp: Date.now()
1537
- });
1610
+ this.events.emit('step:chunk', { stepId: step.id, chunk, timestamp: Date.now() });
1538
1611
  },
1539
1612
  onUsage: this.usageTracker.updateUsage.bind(this.usageTracker)
1540
1613
  });
1541
- this.log(() => `LLM response for step ${step.id}: ${fullText}`);
1542
- console.log(`LLM response for step ${step.id}:`, fullText);
1614
+ if (result.text) {
1615
+ fullText = result.text;
1616
+ }
1543
1617
  return fullText;
1544
1618
  }
1545
- // For regular steps, use streaming XML parsing
1546
- // This allows real-time processing of LLM responses as they arrive
1619
+ // --- Regular XML Step Handling ---
1547
1620
  const parser = this.createStreamingXmlParser();
1548
1621
  let latestResult = {};
1549
1622
  const schemaDescription = SchemaUtils.describeSchema(step.schema, step.id);
1550
- const prompt = `${systemContext}You are executing a wizard step. Generate data for this step.
1623
+ // CLEANER PROMPT STRUCTURE
1624
+ const prompt = `${systemContext}
1625
+ === GOAL ===
1626
+ You are an intelligent agent executing step: "${step.id}".
1627
+ Your task is to generate data that satisfies the INSTRUCTION below based on the CONTEXT.
1551
1628
 
1552
- STEP: ${step.id}
1553
- INSTRUCTION: ${processedInstruction}${errorContext}${contextSection}
1629
+ === INSTRUCTION ===
1630
+ ${processedInstruction}
1631
+ ${contextSection}
1632
+ ${errorContext}
1554
1633
 
1555
- SCHEMA REQUIREMENTS:
1556
- ${schemaDescription}
1557
-
1558
- REQUIRED OUTPUT FORMAT:
1559
- Return a plain XML response with a root <response> tag.
1560
- CRITICAL: Every field MUST include tag-category="wizard" attribute. This is MANDATORY.
1561
- Every field MUST also include a type attribute (e.g., type="string", type="number", type="boolean", type="array").
1562
-
1563
- ARRAY FORMATTING RULES (CRITICAL):
1564
- - Arrays MUST be valid JSON on a SINGLE line
1565
- - Use double quotes, not single quotes: ["a", "b"] NOT ['a', 'b']
1566
- - NO trailing commas: ["a", "b"] NOT ["a", "b",]
1567
- - NO line breaks inside arrays
1568
- - Example: <items tag-category="wizard" type="array">["apple", "banana", "orange"]
1569
-
1570
- IMPORTANT PARSING RULES:
1571
- - Fields with tag-category="wizard" do NOT need closing tags
1572
- - Content ends when the next tag with tag-category="wizard" begins, OR when </response> is reached
1573
- - This means you can include ANY content (including code with <>, XML snippets, etc.) without worrying about breaking the parser
1574
- - Only fields marked with tag-category="wizard" will be parsed
1634
+ === RESPONSE FORMAT ===
1635
+ You must output a VALID XML object inside a <response> tag.
1636
+ 1. Every field must have: tag-category="wizard" and a type attribute (string, number, boolean, array).
1637
+ 2. Arrays must be single-line JSON: <tags tag-category="wizard" type="array">["a", "b"]
1575
1638
 
1576
- Example:
1577
- <response>
1578
- <name tag-category="wizard" type="string">John Smith
1579
- <age tag-category="wizard" type="number">25
1580
- <tags tag-category="wizard" type="array">["a", "b", "c"]
1581
- </response>
1639
+ === SCHEMA DEFINITION ===
1640
+ ${schemaDescription}
1582
1641
 
1583
- Notice: Arrays are compact JSON on one line! No closing tags needed for wizard fields.
1642
+ *** CRITICAL RULES ***
1643
+ 1. Do NOT copy values from the schema definition or examples above.
1644
+ 2. Generate NEW values based strictly on the "INSTRUCTION" and "CURRENT CONTEXT".
1645
+ 3. If the instruction implies a selection (like an ID), ensure the ID exists in the Context.
1584
1646
 
1585
- Generate the XML response now.`;
1586
- this.log(() => `Full prompt for step ${step.id}: ${prompt}`);
1587
- // Initiate streaming LLM call with chunk processing
1588
- // Each chunk is pushed to the parser for incremental XML parsing
1589
- await this.llmClient.complete({
1647
+ Generate the XML <response> now.`;
1648
+ // console.log(prompt); // Uncomment to debug the cleaner prompt
1649
+ step.stream !== false;
1650
+ const result = await this.llmClient.complete({
1590
1651
  prompt,
1591
1652
  model: step.model,
1592
1653
  maxTokens: 1000,
1593
- temperature: 0.3,
1594
- stream: true,
1654
+ temperature: 0.3, // Lower temp for precision
1655
+ stream: step.stream,
1595
1656
  onChunk: (chunk) => {
1596
- // Emit raw chunk event
1597
- this.events.emit('step:chunk', {
1598
- stepId: step.id,
1599
- chunk: chunk,
1600
- timestamp: Date.now()
1601
- });
1602
- // Process each incoming chunk through the streaming parser
1657
+ this.events.emit('step:chunk', { stepId: step.id, chunk, timestamp: Date.now() });
1603
1658
  const parseResult = parser.push(chunk);
1604
1659
  if (parseResult && !parseResult.done) {
1605
1660
  latestResult = parseResult.result;
1606
- // Emit parsed streaming event
1607
- this.events.emit('step:streaming', {
1608
- stepId: step.id,
1609
- data: latestResult,
1610
- timestamp: Date.now()
1611
- });
1612
- // Stream partial results out to connected clients via WebSocket
1613
- // This broadcasts real-time updates to UI and external consumers
1614
- this.visualizationManager.sendStepUpdate({
1615
- stepId: step.id,
1616
- status: 'streaming',
1617
- data: latestResult
1618
- });
1661
+ this.events.emit('step:streaming', { stepId: step.id, data: latestResult, timestamp: Date.now() });
1662
+ this.visualizationManager.sendStepUpdate({ stepId: step.id, status: 'streaming', data: latestResult });
1619
1663
  }
1620
1664
  else if (parseResult?.done) {
1621
1665
  latestResult = parseResult.result;
@@ -1623,20 +1667,19 @@ Generate the XML response now.`;
1623
1667
  },
1624
1668
  onUsage: this.usageTracker.updateUsage.bind(this.usageTracker)
1625
1669
  });
1626
- this.log(() => `Final parsed data: ${JSON.stringify(latestResult)}`);
1627
- this.log(() => `Parsed JSON data for step ${step.id}: ${JSON.stringify(latestResult)}`);
1670
+ if (result.text) {
1671
+ latestResult = result.text;
1672
+ }
1628
1673
  try {
1629
1674
  return step.validate(latestResult);
1630
1675
  }
1631
1676
  catch (validationError) {
1632
- this.log(() => `Validation failed for step ${step.id}: ${validationError.message}`);
1677
+ // Logic for repair remains the same...
1633
1678
  try {
1634
1679
  const repairedData = await this.repairSchemaData(latestResult, step.schema, validationError.message, step.id);
1635
- this.log(() => `Repaired data for step ${step.id}: ${JSON.stringify(repairedData)}`);
1636
1680
  return step.validate(repairedData);
1637
1681
  }
1638
1682
  catch (repairError) {
1639
- this.log(() => `Repair failed for step ${step.id}: ${repairError.message}`);
1640
1683
  return { __validationFailed: true, error: validationError.message };
1641
1684
  }
1642
1685
  }
@@ -1679,16 +1722,17 @@ Fix the data to match the schema and generate the XML response now.`;
1679
1722
  return repairedJsonData;
1680
1723
  }
1681
1724
  /**
1682
- * Creates a streaming XML parser for incremental processing of LLM responses.
1725
+ * Creates an improved streaming XML parser for incremental processing of LLM responses.
1683
1726
  *
1684
- * This parser processes XML chunks as they arrive from the LLM, extracting
1685
- * fields marked with tag-category="wizard" and parsing them based on their type attributes.
1727
+ * This parser is designed to handle partial chunks robustly and provides better error recovery.
1728
+ * It processes XML chunks as they arrive, extracting fields marked with tag-category="wizard".
1686
1729
  *
1687
- * Key features:
1688
- * - Incremental parsing: processes data as it streams in
1689
- * - Buffer management: accumulates chunks until complete fields are available
1690
- * - Type-aware parsing: handles strings, numbers, booleans, arrays, objects
1691
- * - Real-time updates: returns partial results as fields complete
1730
+ * Key improvements:
1731
+ * - Better partial tag handling
1732
+ * - More robust regex matching
1733
+ * - Improved buffer management
1734
+ * - Better error recovery for malformed chunks
1735
+ * - State machine approach for parsing
1692
1736
  *
1693
1737
  * @returns An object with a push method that accepts text chunks and returns parse results
1694
1738
  */
@@ -1697,57 +1741,131 @@ Fix the data to match the schema and generate the XML response now.`;
1697
1741
  let inResponse = false; // Tracks if we've entered the <response> tag
1698
1742
  const result = {}; // The final parsed JSON object
1699
1743
  let currentField = null; // Currently parsing field
1744
+ let parseErrors = 0; // Track consecutive parse errors
1700
1745
  return {
1701
1746
  push: (chunk) => {
1702
- buffer += chunk;
1703
- // Wait for <response> tag to start parsing
1704
- if (!inResponse && buffer.includes('<response>')) {
1705
- inResponse = true;
1706
- buffer = buffer.slice(buffer.indexOf('<response>') + 10); // Remove <response> from buffer
1707
- }
1708
- if (!inResponse)
1709
- return null; // Nothing to parse yet
1710
- // Look for wizard-tagged fields using regex pattern
1711
- const tagMatch = buffer.match(Wizard.WIZARD_TAG_PATTERN);
1712
- if (tagMatch) {
1713
- // If we were parsing a field, finalize it before starting new one
1714
- if (currentField) {
1715
- result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
1747
+ try {
1748
+ buffer += chunk;
1749
+ // Wait for <response> tag to start parsing
1750
+ if (!inResponse) {
1751
+ const responseStart = buffer.indexOf('<response>');
1752
+ if (responseStart !== -1) {
1753
+ inResponse = true;
1754
+ buffer = buffer.slice(responseStart + 10); // Remove <response> from buffer
1755
+ }
1756
+ else {
1757
+ return null; // Still waiting for response start
1758
+ }
1716
1759
  }
1717
- // Extract field name and type from the matched tag
1718
- const typeMatch = tagMatch[2].match(/type=["']([^"']+)["']/);
1719
- currentField = {
1720
- name: tagMatch[1], // Field name from tag
1721
- type: typeMatch?.[1]?.toLowerCase() || 'string', // Type attribute, default to string
1722
- content: '' // Will accumulate field content
1723
- };
1724
- // Remove the processed tag from buffer
1725
- buffer = buffer.slice(tagMatch.index + tagMatch[0].length);
1726
- }
1727
- // Accumulate content for the current field
1728
- if (currentField) {
1729
- // Find the next wizard tag or end of response
1730
- const nextTagIndex = buffer.search(/<\w+\s+[^>]*tag-category=["']wizard["']/);
1731
- if (nextTagIndex !== -1) {
1732
- // Found next tag, accumulate content up to it
1733
- currentField.content += buffer.slice(0, nextTagIndex);
1734
- buffer = buffer.slice(nextTagIndex);
1760
+ // Process buffer for wizard tags
1761
+ let processedSomething = false;
1762
+ // Continue processing while we have data
1763
+ while (buffer.length > 0) {
1764
+ // If we have a current field, try to accumulate content
1765
+ if (currentField) {
1766
+ // Look for the next wizard tag or end of response
1767
+ const nextWizardTag = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*>/);
1768
+ const responseEnd = buffer.indexOf('</response>');
1769
+ if (nextWizardTag && nextWizardTag.index !== undefined) {
1770
+ // Found next tag, finalize current field
1771
+ const contentEnd = nextWizardTag.index;
1772
+ currentField.content += buffer.slice(0, contentEnd);
1773
+ result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
1774
+ currentField = null;
1775
+ buffer = buffer.slice(contentEnd);
1776
+ processedSomething = true;
1777
+ }
1778
+ else if (responseEnd !== -1) {
1779
+ // End of response, finalize current field
1780
+ currentField.content += buffer.slice(0, responseEnd);
1781
+ result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
1782
+ return { done: true, result }; // Parsing complete
1783
+ }
1784
+ else {
1785
+ // No complete field yet, but we might have a partial tag at the end
1786
+ // Check if buffer ends with a partial wizard tag
1787
+ const partialTagMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*$/);
1788
+ if (partialTagMatch) {
1789
+ // Buffer ends with partial tag, keep it for next chunk
1790
+ break;
1791
+ }
1792
+ else {
1793
+ // Safe to accumulate entire buffer
1794
+ currentField.content += buffer;
1795
+ buffer = '';
1796
+ processedSomething = true;
1797
+ }
1798
+ }
1799
+ }
1800
+ else {
1801
+ // No current field, look for a new wizard tag
1802
+ const tagMatch = buffer.match(Wizard.WIZARD_TAG_PATTERN);
1803
+ if (tagMatch && tagMatch.index === 0) {
1804
+ // Tag starts at beginning of buffer
1805
+ const typeMatch = tagMatch[2].match(/type=["']([^"']+)["']/);
1806
+ currentField = {
1807
+ name: tagMatch[1],
1808
+ type: typeMatch?.[1]?.toLowerCase() || 'string',
1809
+ content: ''
1810
+ };
1811
+ buffer = buffer.slice(tagMatch[0].length);
1812
+ processedSomething = true;
1813
+ }
1814
+ else if (tagMatch && tagMatch.index !== undefined && tagMatch.index > 0) {
1815
+ // Tag exists but not at start - might be partial
1816
+ const partialTagMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*$/);
1817
+ if (partialTagMatch) {
1818
+ // Buffer ends with partial tag, wait for more data
1819
+ break;
1820
+ }
1821
+ else {
1822
+ // Tag is in middle, process up to it
1823
+ // This shouldn't happen in well-formed XML, but handle gracefully
1824
+ buffer = buffer.slice(tagMatch.index);
1825
+ continue;
1826
+ }
1827
+ }
1828
+ else {
1829
+ // No wizard tag found
1830
+ if (buffer.includes('</response>')) {
1831
+ // End of response without finalizing a field
1832
+ return { done: true, result };
1833
+ }
1834
+ // Check for partial tag at end
1835
+ const partialTagMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*$/);
1836
+ if (partialTagMatch) {
1837
+ break; // Wait for more data
1838
+ }
1839
+ // No partial tag, buffer might contain non-wizard content
1840
+ // This is unusual but we'll keep it for now
1841
+ break;
1842
+ }
1843
+ }
1735
1844
  }
1736
- else if (buffer.includes('</response>')) {
1737
- // End of response reached, finalize current field
1738
- const endIndex = buffer.indexOf('</response>');
1739
- currentField.content += buffer.slice(0, endIndex);
1740
- result[currentField.name] = this.parseValueByType(currentField.content.trim(), currentField.type);
1741
- return { done: true, result }; // Parsing complete
1845
+ // Reset error counter on successful processing
1846
+ if (processedSomething) {
1847
+ parseErrors = 0;
1742
1848
  }
1743
- else {
1744
- // No complete field yet, accumulate entire buffer
1745
- currentField.content += buffer;
1746
- buffer = '';
1849
+ // Return partial result for UI updates
1850
+ return { done: false, result: { ...result } };
1851
+ }
1852
+ catch (error) {
1853
+ parseErrors++;
1854
+ console.warn(`Streaming XML parse error (attempt ${parseErrors}):`, error instanceof Error ? error.message : String(error));
1855
+ // If we have too many consecutive errors, try to recover
1856
+ if (parseErrors > 3) {
1857
+ console.error('Too many parse errors, attempting recovery');
1858
+ // Try to find next valid wizard tag and restart from there
1859
+ const recoveryMatch = buffer.match(/<\w+\s+[^>]*tag-category=["']wizard["'][^>]*>/);
1860
+ if (recoveryMatch && recoveryMatch.index !== undefined && recoveryMatch.index > 0) {
1861
+ buffer = buffer.slice(recoveryMatch.index);
1862
+ parseErrors = 0; // Reset error counter
1863
+ return { done: false, result: { ...result } };
1864
+ }
1747
1865
  }
1866
+ // Return current result even with errors
1867
+ return { done: false, result: { ...result } };
1748
1868
  }
1749
- // Return partial result for UI updates
1750
- return { done: false, result: { ...result } };
1751
1869
  }
1752
1870
  };
1753
1871
  }