@swizzy_ai/kit 1.0.4 → 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);
@@ -1152,11 +1183,7 @@ class Wizard {
1152
1183
  this.currentStepIndex++;
1153
1184
  return true;
1154
1185
  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 ')) {
1186
+ if (this.isStringSignal(signal) && signal.startsWith('GOTO ')) {
1160
1187
  const targetStepId = signal.substring(5);
1161
1188
  const targetIndex = this.findStepIndex(targetStepId);
1162
1189
  if (targetIndex !== -1) {
@@ -1167,6 +1194,28 @@ class Wizard {
1167
1194
  }
1168
1195
  return true;
1169
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
+ }
1170
1219
  }
1171
1220
  return true;
1172
1221
  }
@@ -1324,7 +1373,7 @@ class Wizard {
1324
1373
  instruction: step.instruction,
1325
1374
  timestamp: stepStartTime
1326
1375
  });
1327
- const stepContext = step.getContext(this.contextManager.getContext());
1376
+ const stepContext = await step.getContext(this.contextManager.getContext());
1328
1377
  let processedInstruction = step.instruction;
1329
1378
  if (step.contextType === 'template' || step.contextType === 'both') {
1330
1379
  processedInstruction = this.applyTemplate(step.instruction, stepContext);
@@ -1519,124 +1568,98 @@ class Wizard {
1519
1568
  * Uses streaming for regular steps to provide real-time parsing and UI updates.
1520
1569
  * TextStep and ComputeStep use different approaches (non-streaming).
1521
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
+ */
1522
1575
  async generateStepData(step, stepContext) {
1523
1576
  const systemContext = this.systemPrompt ? `${this.systemPrompt}\n\n` : '';
1577
+ // Build context strings
1524
1578
  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.` : '';
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.` : '';
1526
1580
  let processedInstruction = step.instruction;
1527
1581
  if (step.contextType === 'template' || step.contextType === 'both') {
1528
1582
  processedInstruction = this.applyTemplate(step.instruction, stepContext);
1529
1583
  }
1530
- this.log(() => `Processed instruction for step ${step.id}: ${processedInstruction}`);
1531
1584
  let contextSection = '';
1532
1585
  if (step.contextType === 'xml' || step.contextType === 'both' || !step.contextType) {
1533
- contextSection = `\n\nSTEP CONTEXT:\n${this.objectToXml(stepContext)}`;
1586
+ contextSection = `\n\n### CURRENT CONTEXT ###\n${this.objectToXml(stepContext)}`;
1534
1587
  }
1535
- this.log(() => `Context section for step ${step.id}: ${contextSection}`);
1588
+ // --- Text Step Handling ---
1536
1589
  if (step instanceof TextStep) {
1537
- 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}".
1538
1592
 
1539
- STEP: ${step.id}
1540
- INSTRUCTION: ${processedInstruction}${errorContext}${contextSection}
1593
+ INSTRUCTION:
1594
+ ${processedInstruction}
1595
+ ${contextSection}
1596
+ ${errorContext}
1541
1597
 
1598
+ OUTPUT:
1542
1599
  Generate the text response now.`;
1543
- this.log(() => `Full prompt for step ${step.id}: ${prompt}`);
1544
1600
  let fullText = '';
1545
- await this.llmClient.complete({
1601
+ console.log("full prompt", prompt);
1602
+ const result = await this.llmClient.complete({
1546
1603
  prompt,
1547
1604
  model: step.model,
1548
1605
  maxTokens: 1000,
1549
1606
  temperature: 0.3,
1550
- stream: true,
1607
+ stream: step.stream,
1551
1608
  onChunk: (chunk) => {
1552
1609
  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
- });
1610
+ this.events.emit('step:chunk', { stepId: step.id, chunk, timestamp: Date.now() });
1559
1611
  },
1560
1612
  onUsage: this.usageTracker.updateUsage.bind(this.usageTracker)
1561
1613
  });
1562
- this.log(() => `LLM response for step ${step.id}: ${fullText}`);
1563
- console.log(`LLM response for step ${step.id}:`, fullText);
1614
+ if (result.text) {
1615
+ fullText = result.text;
1616
+ }
1564
1617
  return fullText;
1565
1618
  }
1566
- // For regular steps, use streaming XML parsing
1567
- // This allows real-time processing of LLM responses as they arrive
1619
+ // --- Regular XML Step Handling ---
1568
1620
  const parser = this.createStreamingXmlParser();
1569
1621
  let latestResult = {};
1570
1622
  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").
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.
1583
1628
 
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"]
1629
+ === INSTRUCTION ===
1630
+ ${processedInstruction}
1631
+ ${contextSection}
1632
+ ${errorContext}
1590
1633
 
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
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"]
1596
1638
 
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>
1639
+ === SCHEMA DEFINITION ===
1640
+ ${schemaDescription}
1603
1641
 
1604
- 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.
1605
1646
 
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({
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({
1611
1651
  prompt,
1612
1652
  model: step.model,
1613
1653
  maxTokens: 1000,
1614
- temperature: 0.3,
1615
- stream: true,
1654
+ temperature: 0.3, // Lower temp for precision
1655
+ stream: step.stream,
1616
1656
  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
1657
+ this.events.emit('step:chunk', { stepId: step.id, chunk, timestamp: Date.now() });
1624
1658
  const parseResult = parser.push(chunk);
1625
1659
  if (parseResult && !parseResult.done) {
1626
1660
  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
- });
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 });
1640
1663
  }
1641
1664
  else if (parseResult?.done) {
1642
1665
  latestResult = parseResult.result;
@@ -1644,20 +1667,19 @@ Generate the XML response now.`;
1644
1667
  },
1645
1668
  onUsage: this.usageTracker.updateUsage.bind(this.usageTracker)
1646
1669
  });
1647
- this.log(() => `Final parsed data: ${JSON.stringify(latestResult)}`);
1648
- this.log(() => `Parsed JSON data for step ${step.id}: ${JSON.stringify(latestResult)}`);
1670
+ if (result.text) {
1671
+ latestResult = result.text;
1672
+ }
1649
1673
  try {
1650
1674
  return step.validate(latestResult);
1651
1675
  }
1652
1676
  catch (validationError) {
1653
- this.log(() => `Validation failed for step ${step.id}: ${validationError.message}`);
1677
+ // Logic for repair remains the same...
1654
1678
  try {
1655
1679
  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
1680
  return step.validate(repairedData);
1658
1681
  }
1659
1682
  catch (repairError) {
1660
- this.log(() => `Repair failed for step ${step.id}: ${repairError.message}`);
1661
1683
  return { __validationFailed: true, error: validationError.message };
1662
1684
  }
1663
1685
  }
@@ -1700,16 +1722,17 @@ Fix the data to match the schema and generate the XML response now.`;
1700
1722
  return repairedJsonData;
1701
1723
  }
1702
1724
  /**
1703
- * Creates a streaming XML parser for incremental processing of LLM responses.
1725
+ * Creates an improved streaming XML parser for incremental processing of LLM responses.
1704
1726
  *
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.
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".
1707
1729
  *
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
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
1713
1736
  *
1714
1737
  * @returns An object with a push method that accepts text chunks and returns parse results
1715
1738
  */
@@ -1718,57 +1741,131 @@ Fix the data to match the schema and generate the XML response now.`;
1718
1741
  let inResponse = false; // Tracks if we've entered the <response> tag
1719
1742
  const result = {}; // The final parsed JSON object
1720
1743
  let currentField = null; // Currently parsing field
1744
+ let parseErrors = 0; // Track consecutive parse errors
1721
1745
  return {
1722
1746
  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);
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
+ }
1737
1759
  }
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);
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
+ }
1756
1844
  }
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
1845
+ // Reset error counter on successful processing
1846
+ if (processedSomething) {
1847
+ parseErrors = 0;
1763
1848
  }
1764
- else {
1765
- // No complete field yet, accumulate entire buffer
1766
- currentField.content += buffer;
1767
- 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
+ }
1768
1865
  }
1866
+ // Return current result even with errors
1867
+ return { done: false, result: { ...result } };
1769
1868
  }
1770
- // Return partial result for UI updates
1771
- return { done: false, result: { ...result } };
1772
1869
  }
1773
1870
  };
1774
1871
  }