@covibes/zeroshot 2.0.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -75,6 +75,11 @@ function validateConfig(config, depth = 0) {
75
75
  errors.push(...logicResult.errors);
76
76
  warnings.push(...logicResult.warnings);
77
77
 
78
+ // === PHASE 5: Template variable validation ===
79
+ const templateResult = validateTemplateVariables(config, depth);
80
+ errors.push(...templateResult.errors);
81
+ warnings.push(...templateResult.warnings);
82
+
78
83
  return {
79
84
  valid: errors.length === 0,
80
85
  errors,
@@ -559,6 +564,194 @@ function validateLogicScripts(config) {
559
564
  return { errors, warnings };
560
565
  }
561
566
 
567
+ /**
568
+ * Phase 5: Validate template variables against jsonSchema
569
+ * Ensures {{result.*}} references in hooks match defined schema properties
570
+ */
571
+ function validateTemplateVariables(config, depth = 0) {
572
+ const errors = [];
573
+ const warnings = [];
574
+
575
+ if (!config.agents || !Array.isArray(config.agents)) {
576
+ return { errors, warnings };
577
+ }
578
+
579
+ const prefix = depth > 0 ? `Sub-cluster (depth ${depth}): ` : '';
580
+
581
+ for (const agent of config.agents) {
582
+ // Skip subclusters - they have their own validation
583
+ if (agent.type === 'subcluster') {
584
+ // Recursively validate subcluster config
585
+ if (agent.config?.agents) {
586
+ const subResult = validateTemplateVariables(agent.config, depth + 1);
587
+ // Prefix sub-cluster errors with agent ID
588
+ errors.push(...subResult.errors.map((e) => `Sub-cluster '${agent.id}': ${e}`));
589
+ warnings.push(...subResult.warnings.map((w) => `Sub-cluster '${agent.id}': ${w}`));
590
+ }
591
+ continue;
592
+ }
593
+
594
+ const result = validateAgentTemplateVariables(agent, agent.id);
595
+ errors.push(...result.errors.map((e) => `${prefix}${e}`));
596
+ warnings.push(...result.warnings.map((w) => `${prefix}${w}`));
597
+ }
598
+
599
+ return { errors, warnings };
600
+ }
601
+
602
+ /**
603
+ * Validate template variables for a single agent
604
+ * @param {Object} agent - Agent configuration
605
+ * @param {String} agentId - Agent ID for error messages
606
+ * @returns {{ errors: string[], warnings: string[] }}
607
+ */
608
+ function validateAgentTemplateVariables(agent, agentId) {
609
+ const errors = [];
610
+ const warnings = [];
611
+
612
+ // Extract schema properties (null if non-JSON output or text output)
613
+ const schemaProps = extractSchemaProperties(agent);
614
+
615
+ // If schemaProps is null, this agent doesn't use JSON output - skip validation
616
+ if (schemaProps === null) {
617
+ return { errors, warnings };
618
+ }
619
+
620
+ // Extract template variables from hooks
621
+ const templateVars = extractTemplateVariables(agent);
622
+
623
+ // Check for undefined references (ERROR)
624
+ for (const varName of templateVars) {
625
+ if (!schemaProps.has(varName)) {
626
+ const availableProps = Array.from(schemaProps).join(', ');
627
+ errors.push(
628
+ `Agent '${agentId}': Template uses '{{result.${varName}}}' but '${varName}' is not defined in jsonSchema. ` +
629
+ `Available properties: [${availableProps}]`
630
+ );
631
+ }
632
+ }
633
+
634
+ // Check for unused schema properties (WARNING)
635
+ for (const prop of schemaProps) {
636
+ if (!templateVars.has(prop)) {
637
+ warnings.push(
638
+ `Agent '${agentId}': Schema property '${prop}' is defined but never referenced in hooks. ` +
639
+ `Consider removing it to save tokens.`
640
+ );
641
+ }
642
+ }
643
+
644
+ return { errors, warnings };
645
+ }
646
+
647
+ /**
648
+ * Extract all template variables ({{result.*}}) from agent hooks
649
+ * Searches hooks.onComplete.config (recursive) and hooks.onComplete.transform.script
650
+ * Also searches triggers[].onComplete patterns
651
+ * @param {Object} agent - Agent configuration
652
+ * @returns {Set<string>} Set of variable names referenced
653
+ */
654
+ function extractTemplateVariables(agent) {
655
+ const variables = new Set();
656
+
657
+ // Regex patterns - reset lastIndex before each use to avoid state pollution
658
+ const mustachePattern = /\{\{result\.([^}]+)\}\}/g;
659
+ const directPattern = /\bresult\.([a-zA-Z_][a-zA-Z0-9_]*)/g;
660
+
661
+ /**
662
+ * Recursively traverse an object/array and extract template variables from strings
663
+ */
664
+ function traverseAndExtract(obj) {
665
+ if (obj === null || obj === undefined) {
666
+ return;
667
+ }
668
+
669
+ if (typeof obj === 'string') {
670
+ // Extract mustache-style {{result.field}}
671
+ mustachePattern.lastIndex = 0;
672
+ let match;
673
+ while ((match = mustachePattern.exec(obj)) !== null) {
674
+ variables.add(match[1]);
675
+ }
676
+ return;
677
+ }
678
+
679
+ if (Array.isArray(obj)) {
680
+ for (const item of obj) {
681
+ traverseAndExtract(item);
682
+ }
683
+ return;
684
+ }
685
+
686
+ if (typeof obj === 'object') {
687
+ for (const value of Object.values(obj)) {
688
+ traverseAndExtract(value);
689
+ }
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Extract variables from transform script (direct result.field access)
695
+ */
696
+ function extractFromScript(script) {
697
+ if (typeof script !== 'string') {
698
+ return;
699
+ }
700
+
701
+ directPattern.lastIndex = 0;
702
+ let match;
703
+ while ((match = directPattern.exec(script)) !== null) {
704
+ variables.add(match[1]);
705
+ }
706
+ }
707
+
708
+ // Extract from hooks.onComplete.config
709
+ if (agent.hooks?.onComplete?.config) {
710
+ traverseAndExtract(agent.hooks.onComplete.config);
711
+ }
712
+
713
+ // Extract from hooks.onComplete.transform.script
714
+ if (agent.hooks?.onComplete?.transform?.script) {
715
+ extractFromScript(agent.hooks.onComplete.transform.script);
716
+ }
717
+
718
+ // Extract from triggers[].onComplete (some agents define hooks per-trigger)
719
+ if (agent.triggers && Array.isArray(agent.triggers)) {
720
+ for (const trigger of agent.triggers) {
721
+ if (trigger.onComplete?.config) {
722
+ traverseAndExtract(trigger.onComplete.config);
723
+ }
724
+ if (trigger.onComplete?.transform?.script) {
725
+ extractFromScript(trigger.onComplete.transform.script);
726
+ }
727
+ }
728
+ }
729
+
730
+ return variables;
731
+ }
732
+
733
+ /**
734
+ * Extract schema properties from agent's jsonSchema
735
+ * @param {Object} agent - Agent configuration
736
+ * @returns {Set<string>|null} Set of property names, or null if agent doesn't use JSON output
737
+ */
738
+ function extractSchemaProperties(agent) {
739
+ // Non-JSON agents don't need validation
740
+ // Both 'json' and 'stream-json' use jsonSchema and need validation
741
+ if (!['json', 'stream-json'].includes(agent.outputFormat)) {
742
+ return null;
743
+ }
744
+
745
+ // If explicit schema is provided, use its properties
746
+ if (agent.jsonSchema?.properties) {
747
+ return new Set(Object.keys(agent.jsonSchema.properties));
748
+ }
749
+
750
+ // Default schema when outputFormat is 'json' but no explicit schema
751
+ // See: agent-config.js:62-69
752
+ return new Set(['summary', 'result']);
753
+ }
754
+
562
755
  /**
563
756
  * Check if iteration pattern is valid
564
757
  */
@@ -608,4 +801,9 @@ module.exports = {
608
801
  validateLogicScripts,
609
802
  isValidIterationPattern,
610
803
  formatValidationResult,
804
+ // Phase 5: Template variable validation
805
+ validateTemplateVariables,
806
+ extractTemplateVariables,
807
+ extractSchemaProperties,
808
+ validateAgentTemplateVariables,
611
809
  };
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Copy Worker - Worker thread for parallel file copying
3
+ *
4
+ * Handles copying a batch of files from source to destination.
5
+ * Used by IsolationManager._copyDirExcluding() for parallel copying.
6
+ */
7
+
8
+ const { parentPort, workerData } = require('worker_threads');
9
+ const fs = require('fs');
10
+ const path = require('path');
11
+
12
+ const { files, sourceBase, destBase } = workerData;
13
+
14
+ let copied = 0;
15
+ let skipped = 0;
16
+ const errors = [];
17
+
18
+ for (const relativePath of files) {
19
+ const srcPath = path.join(sourceBase, relativePath);
20
+ const destPath = path.join(destBase, relativePath);
21
+
22
+ try {
23
+ // Ensure parent directory exists
24
+ const destDir = path.dirname(destPath);
25
+ if (!fs.existsSync(destDir)) {
26
+ fs.mkdirSync(destDir, { recursive: true });
27
+ }
28
+
29
+ // Copy the file
30
+ fs.copyFileSync(srcPath, destPath);
31
+ copied++;
32
+ } catch (err) {
33
+ // Skip files we can't copy (permission denied, broken symlinks, etc.)
34
+ if (err.code === 'EACCES' || err.code === 'EPERM' || err.code === 'ENOENT') {
35
+ skipped++;
36
+ continue;
37
+ }
38
+ errors.push({ file: relativePath, error: err.message });
39
+ }
40
+ }
41
+
42
+ // Report results back to main thread
43
+ parentPort.postMessage({ copied, skipped, errors });