@travisliu/open-dynamic-workflow 0.3.0 → 0.3.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.
Files changed (131) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +45 -17
  3. package/dist/agents/codex-exec.js +3 -1
  4. package/dist/agents/codex-exec.js.map +1 -1
  5. package/dist/agents/cursor-agent.d.ts +21 -0
  6. package/dist/agents/cursor-agent.js +171 -0
  7. package/dist/agents/cursor-agent.js.map +1 -0
  8. package/dist/agents/execute-agent.js +1 -1
  9. package/dist/agents/execute-agent.js.map +1 -1
  10. package/dist/agents/registry.js +2 -0
  11. package/dist/agents/registry.js.map +1 -1
  12. package/dist/artifacts/call-cache.d.ts +27 -3
  13. package/dist/artifacts/call-cache.js +160 -59
  14. package/dist/artifacts/call-cache.js.map +1 -1
  15. package/dist/artifacts/run-store.js.map +1 -1
  16. package/dist/cli/commands/doctor.js +0 -4
  17. package/dist/cli/commands/doctor.js.map +1 -1
  18. package/dist/cli/commands/init.js.map +1 -1
  19. package/dist/cli/commands/run.js +2 -1
  20. package/dist/cli/commands/run.js.map +1 -1
  21. package/dist/cli/commands/validate.js +2 -1
  22. package/dist/cli/commands/validate.js.map +1 -1
  23. package/dist/cli/index.d.ts +1 -0
  24. package/dist/cli/index.js +51 -0
  25. package/dist/cli/index.js.map +1 -1
  26. package/dist/cli/init/prompts.d.ts +1 -1
  27. package/dist/cli/init/prompts.js +4 -4
  28. package/dist/cli/init/prompts.js.map +1 -1
  29. package/dist/cli/init/renderer.js +2 -1
  30. package/dist/cli/init/renderer.js.map +1 -1
  31. package/dist/config/defaults.js +2 -1
  32. package/dist/config/defaults.js.map +1 -1
  33. package/dist/config/load.js +1 -1
  34. package/dist/config/load.js.map +1 -1
  35. package/dist/config/schema.js +3 -0
  36. package/dist/config/schema.js.map +1 -1
  37. package/dist/config/types.d.ts +1 -0
  38. package/dist/discovery/collect-files.js +2 -2
  39. package/dist/discovery/collect-files.js.map +1 -1
  40. package/dist/discovery/definition-call.js +1 -1
  41. package/dist/discovery/definition-call.js.map +1 -1
  42. package/dist/discovery/file-patterns.js +1 -1
  43. package/dist/discovery/file-patterns.js.map +1 -1
  44. package/dist/discovery/schema-summary.js.map +1 -1
  45. package/dist/loop/artifacts.d.ts +39 -0
  46. package/dist/loop/artifacts.js +58 -0
  47. package/dist/loop/artifacts.js.map +1 -0
  48. package/dist/loop/context.d.ts +55 -0
  49. package/dist/loop/context.js +134 -0
  50. package/dist/loop/context.js.map +1 -0
  51. package/dist/loop/events.d.ts +62 -0
  52. package/dist/loop/events.js +68 -0
  53. package/dist/loop/events.js.map +1 -0
  54. package/dist/loop/id.d.ts +24 -0
  55. package/dist/loop/id.js +65 -0
  56. package/dist/loop/id.js.map +1 -0
  57. package/dist/loop/index.d.ts +8 -0
  58. package/dist/loop/index.js +9 -0
  59. package/dist/loop/index.js.map +1 -0
  60. package/dist/loop/replay.d.ts +47 -0
  61. package/dist/loop/replay.js +104 -0
  62. package/dist/loop/replay.js.map +1 -0
  63. package/dist/loop/results.d.ts +74 -0
  64. package/dist/loop/results.js +101 -0
  65. package/dist/loop/results.js.map +1 -0
  66. package/dist/loop/run.d.ts +22 -0
  67. package/dist/loop/run.js +671 -0
  68. package/dist/loop/run.js.map +1 -0
  69. package/dist/loop/summary.d.ts +5 -0
  70. package/dist/loop/summary.js +16 -0
  71. package/dist/loop/summary.js.map +1 -0
  72. package/dist/loop/types.d.ts +120 -0
  73. package/dist/loop/types.js +2 -0
  74. package/dist/loop/types.js.map +1 -0
  75. package/dist/loop/validate.d.ts +9 -0
  76. package/dist/loop/validate.js +122 -0
  77. package/dist/loop/validate.js.map +1 -0
  78. package/dist/orchestration/scheduler.js +1 -1
  79. package/dist/orchestration/scheduler.js.map +1 -1
  80. package/dist/output/events.d.ts +49 -1
  81. package/dist/output/events.js.map +1 -1
  82. package/dist/output/failed-artifacts.js +5 -0
  83. package/dist/output/failed-artifacts.js.map +1 -1
  84. package/dist/output/json-reporter.d.ts +2 -2
  85. package/dist/output/json-reporter.js +1 -1
  86. package/dist/output/json-reporter.js.map +1 -1
  87. package/dist/output/jsonl-reporter.d.ts +3 -4
  88. package/dist/output/jsonl-reporter.js +2 -2
  89. package/dist/output/jsonl-reporter.js.map +1 -1
  90. package/dist/output/pretty-renderer.js +15 -0
  91. package/dist/output/pretty-renderer.js.map +1 -1
  92. package/dist/output/pretty-reporter.js +35 -4
  93. package/dist/output/pretty-reporter.js.map +1 -1
  94. package/dist/output/pretty-view-builder.js +70 -1
  95. package/dist/output/pretty-view-builder.js.map +1 -1
  96. package/dist/output/pretty-view.d.ts +12 -2
  97. package/dist/output/verbose-formatter.d.ts +4 -1
  98. package/dist/output/verbose-formatter.js +67 -0
  99. package/dist/output/verbose-formatter.js.map +1 -1
  100. package/dist/pipeline/stage-barrier.js.map +1 -1
  101. package/dist/pipeline/validate.js +1 -1
  102. package/dist/pipeline/validate.js.map +1 -1
  103. package/dist/shared-agents/load.js +0 -2
  104. package/dist/shared-agents/load.js.map +1 -1
  105. package/dist/tools/load.js.map +1 -1
  106. package/dist/types/config.d.ts +1 -0
  107. package/dist/types/workflow.d.ts +13 -0
  108. package/dist/workflow/discovery.d.ts +1 -0
  109. package/dist/workflow/discovery.js +6 -4
  110. package/dist/workflow/discovery.js.map +1 -1
  111. package/dist/workflow/dsl.d.ts +1 -0
  112. package/dist/workflow/dsl.js +281 -137
  113. package/dist/workflow/dsl.js.map +1 -1
  114. package/dist/workflow/errors.js +1 -1
  115. package/dist/workflow/errors.js.map +1 -1
  116. package/dist/workflow/invocation-manager.js +14 -2
  117. package/dist/workflow/invocation-manager.js.map +1 -1
  118. package/dist/workflow/resolve-target.js +1 -1
  119. package/dist/workflow/resolve-target.js.map +1 -1
  120. package/dist/workflow/runtime.js +6 -1
  121. package/dist/workflow/runtime.js.map +1 -1
  122. package/dist/workflow/sandbox.js +1 -0
  123. package/dist/workflow/sandbox.js.map +1 -1
  124. package/dist/workflow/scope.d.ts +1 -1
  125. package/dist/workflow/scope.js +2 -15
  126. package/dist/workflow/scope.js.map +1 -1
  127. package/dist/workflow/types.d.ts +3 -0
  128. package/dist/workflow/validate.d.ts +2 -0
  129. package/dist/workflow/validate.js +369 -46
  130. package/dist/workflow/validate.js.map +1 -1
  131. package/package.json +16 -2
@@ -74,6 +74,7 @@ function getObjectLiteralProperty(node, name) {
74
74
  }
75
75
  export function validateWorkflow(workflow, options) {
76
76
  const issues = [];
77
+ const loopLabelsSeen = new Set();
77
78
  const sourceFile = ts.createSourceFile(workflow.sourcePath, workflow.sourceText, ts.ScriptTarget.Latest, true);
78
79
  // Find the exported default workflow function to get its context parameter name
79
80
  const contextParameterNames = new Set(["ctx", "context"]);
@@ -139,7 +140,7 @@ export function validateWorkflow(workflow, options) {
139
140
  report(idArg, "Shared agent ID must be a string literal.");
140
141
  }
141
142
  }
142
- function validateSharedAgentInput(idArg, inputArg, isDefinitionForm = false) {
143
+ function validateSharedAgentInput(idArg, inputArg) {
143
144
  if (!options.sharedAgentRegistry || !idArg || !ts.isStringLiteral(idArg)) {
144
145
  return;
145
146
  }
@@ -149,7 +150,7 @@ export function validateWorkflow(workflow, options) {
149
150
  return;
150
151
  }
151
152
  const schema = entry.definition.inputSchema;
152
- let parsedInput = parseStaticProperties(inputArg);
153
+ const parsedInput = parseStaticProperties(inputArg);
153
154
  if (parsedInput === undefined) {
154
155
  if (!inputArg) {
155
156
  try {
@@ -162,7 +163,9 @@ export function validateWorkflow(workflow, options) {
162
163
  }
163
164
  }
164
165
  }
165
- catch (err) { }
166
+ catch {
167
+ // ignore validation errors
168
+ }
166
169
  }
167
170
  return;
168
171
  }
@@ -194,7 +197,9 @@ export function validateWorkflow(workflow, options) {
194
197
  }
195
198
  }
196
199
  }
197
- catch (err) { }
200
+ catch {
201
+ // ignore validation errors
202
+ }
198
203
  }
199
204
  function validateInputAgainstSchema(name, schema, argsExpr) {
200
205
  if (argsExpr === undefined) {
@@ -232,7 +237,9 @@ export function validateWorkflow(workflow, options) {
232
237
  }
233
238
  }
234
239
  }
235
- catch (err) { }
240
+ catch {
241
+ // ignore validation errors
242
+ }
236
243
  }
237
244
  function validateWorkflowCall(node, isContextForm, contextName = "workflow") {
238
245
  const firstArg = node.arguments[0];
@@ -321,7 +328,7 @@ export function validateWorkflow(workflow, options) {
321
328
  const firstArg = node.arguments[0];
322
329
  const callPrefix = isContextForm ? `${contextName}.tool()` : "tool()";
323
330
  if (isForbiddenContext) {
324
- report(node, `${callPrefix} is not allowed in this context (parallel, pipeline stage, or shared agent).`);
331
+ report(node, `${callPrefix} is not allowed in this context (parallel, pipeline stage, loop round, or shared agent).`);
325
332
  }
326
333
  if (!firstArg) {
327
334
  report(node, `${callPrefix} requires an object literal argument.`);
@@ -418,6 +425,277 @@ export function validateWorkflow(workflow, options) {
418
425
  report(firstArg, `${callPrefix} is missing required 'args' property.`);
419
426
  }
420
427
  }
428
+ function validateLoopCall(node, isContextForm, contextName = "ctx", isForbiddenContext = false, functionDepth = 0, isInsideParallel = false, isInsideLoopRun = false, isInsideMainWorkflow = false) {
429
+ const callPrefix = isContextForm ? `${contextName}.loop()` : "loop()";
430
+ if (isInsideParallel) {
431
+ report(node, `loop() inside parallel() is not supported to prevent state overwrites.`);
432
+ }
433
+ if (isInsideLoopRun) {
434
+ report(node, `Nested loops are not supported to prevent state overwrites.`);
435
+ }
436
+ if (functionDepth > 0 && !isInsideMainWorkflow && !isInsideLoopRun) {
437
+ report(node, `loop() is not allowed inside helper functions or recursive scopes to prevent state overwrites.`);
438
+ }
439
+ if (node.arguments.length !== 1) {
440
+ report(node, `${callPrefix} now accepts exactly one object argument.`);
441
+ return;
442
+ }
443
+ const firstArg = node.arguments[0];
444
+ if (!firstArg) {
445
+ report(node, `${callPrefix} now accepts exactly one object argument.`);
446
+ return;
447
+ }
448
+ if (!ts.isObjectLiteralExpression(firstArg)) {
449
+ if (isStaticValue(firstArg)) {
450
+ report(firstArg, `${callPrefix} argument must be an object literal.`);
451
+ }
452
+ visit(firstArg, isForbiddenContext, functionDepth, isInsideLoopRun, new Set(), isInsideParallel, isInsideMainWorkflow);
453
+ return;
454
+ }
455
+ const propsMap = new Map();
456
+ const seenKeys = new Set();
457
+ for (const prop of firstArg.properties) {
458
+ if (ts.isSpreadAssignment(prop)) {
459
+ report(prop, `${callPrefix} does not support spread properties.`);
460
+ continue;
461
+ }
462
+ if (ts.isShorthandPropertyAssignment(prop)) {
463
+ const key = prop.name.text;
464
+ report(prop, `${callPrefix} does not support shorthand property '${key}'.`);
465
+ continue;
466
+ }
467
+ if (ts.isMethodDeclaration(prop)) {
468
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
469
+ report(prop, `${callPrefix} does not support computed method names.`);
470
+ continue;
471
+ }
472
+ const key = prop.name.text;
473
+ if (key !== "run") {
474
+ report(prop, `${callPrefix} does not support method properties for '${key}'.`);
475
+ }
476
+ propsMap.set(key, prop);
477
+ seenKeys.add(key);
478
+ continue;
479
+ }
480
+ if (ts.isPropertyAssignment(prop)) {
481
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
482
+ report(prop, `${callPrefix} does not support computed property names.`);
483
+ continue;
484
+ }
485
+ const key = prop.name.text;
486
+ propsMap.set(key, prop.initializer);
487
+ seenKeys.add(key);
488
+ continue;
489
+ }
490
+ report(prop, `${callPrefix} contains unsupported property type.`);
491
+ }
492
+ const allowedKeys = new Set(["label", "initialState", "options", "run"]);
493
+ for (const key of seenKeys) {
494
+ if (key === "runRound") {
495
+ report(firstArg, `${callPrefix} does not support 'runRound'. Use 'run' instead.`);
496
+ }
497
+ else if (!allowedKeys.has(key)) {
498
+ report(firstArg, `${callPrefix} contains unsupported top-level key '${key}'.`);
499
+ }
500
+ }
501
+ const requiredKeys = ["label", "initialState", "options", "run"];
502
+ for (const reqKey of requiredKeys) {
503
+ if (!seenKeys.has(reqKey)) {
504
+ report(firstArg, `${callPrefix} is missing required '${reqKey}' property.`);
505
+ }
506
+ }
507
+ const labelInit = propsMap.get("label");
508
+ if (labelInit && isStaticValue(labelInit)) {
509
+ const labelVal = parseStaticProperties(labelInit);
510
+ if (typeof labelVal !== "string") {
511
+ report(labelInit, "label must be a string literal.");
512
+ }
513
+ else if (labelVal.trim() === "") {
514
+ report(labelInit, "label cannot be empty.");
515
+ }
516
+ else {
517
+ const normalizedLabel = labelVal
518
+ .toLowerCase()
519
+ .trim()
520
+ .replace(/[^a-z0-9_.:-]+/g, "-")
521
+ .replace(/^-+|-+$/g, "");
522
+ if (loopLabelsSeen.has(normalizedLabel)) {
523
+ report(labelInit, `Duplicate loop label detected: '${labelVal}'. All loop labels in a workflow must be unique.`);
524
+ }
525
+ else {
526
+ loopLabelsSeen.add(normalizedLabel);
527
+ }
528
+ }
529
+ }
530
+ if (labelInit) {
531
+ visit(labelInit, isForbiddenContext, functionDepth);
532
+ }
533
+ const initialStateInit = propsMap.get("initialState");
534
+ if (initialStateInit) {
535
+ visit(initialStateInit, isForbiddenContext, functionDepth);
536
+ }
537
+ const optionsInit = propsMap.get("options");
538
+ if (optionsInit) {
539
+ if (ts.isObjectLiteralExpression(optionsInit)) {
540
+ const allowedLoopOptionKeys = new Set(["failureMode", "maxRounds", "timeoutMs"]);
541
+ const deprecatedKeys = new Set(["stopWhen", "nextState", "onFailureState", "resultMode", "metadata"]);
542
+ const optPropsMap = new Map();
543
+ const optSeenKeys = new Set();
544
+ for (const prop of optionsInit.properties) {
545
+ if (ts.isSpreadAssignment(prop)) {
546
+ report(prop, `${callPrefix} does not support spread properties in options.`);
547
+ continue;
548
+ }
549
+ if (!ts.isPropertyAssignment(prop)) {
550
+ report(prop, `${callPrefix} does not support shorthand or method properties in options.`);
551
+ continue;
552
+ }
553
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) {
554
+ report(prop.name, `${callPrefix} does not support computed property names in options.`);
555
+ continue;
556
+ }
557
+ const key = prop.name.text;
558
+ if (deprecatedKeys.has(key)) {
559
+ report(prop.name, `${callPrefix} option '${key}' is deprecated or unsupported.`);
560
+ continue;
561
+ }
562
+ if (!allowedLoopOptionKeys.has(key)) {
563
+ report(prop.name, `${callPrefix} options contain unsupported key '${key}'.`);
564
+ continue;
565
+ }
566
+ optPropsMap.set(key, prop.initializer);
567
+ optSeenKeys.add(key);
568
+ }
569
+ if (!optSeenKeys.has("maxRounds")) {
570
+ report(optionsInit, `${callPrefix} options is missing required 'maxRounds'.`);
571
+ }
572
+ for (const [key, init] of optPropsMap.entries()) {
573
+ const isStatic = isStaticValue(init);
574
+ const staticVal = isStatic ? parseStaticProperties(init) : undefined;
575
+ if (key === "maxRounds") {
576
+ if (isStatic) {
577
+ const ceiling = options.maxLoopRounds ?? 20;
578
+ if (typeof staticVal !== "number" || isNaN(staticVal) || !Number.isInteger(staticVal) || staticVal < 1) {
579
+ report(init, "maxRounds must be a positive integer.");
580
+ }
581
+ else if (staticVal > ceiling) {
582
+ report(init, `maxRounds (${staticVal}) exceeds the configured ceiling of ${ceiling}.`);
583
+ }
584
+ }
585
+ }
586
+ else if (key === "timeoutMs") {
587
+ if (isStatic) {
588
+ if (typeof staticVal !== "number" || isNaN(staticVal) || !Number.isInteger(staticVal) || staticVal <= 0) {
589
+ report(init, "timeoutMs must be a positive integer.");
590
+ }
591
+ }
592
+ }
593
+ else if (key === "failureMode") {
594
+ if (isStatic) {
595
+ if (typeof staticVal !== "string") {
596
+ report(init, "failureMode must be a string literal.");
597
+ }
598
+ else if (staticVal !== "throw" && staticVal !== "settled") {
599
+ report(init, "failureMode must be 'throw' or 'settled'.");
600
+ }
601
+ }
602
+ }
603
+ }
604
+ }
605
+ else if (isStaticValue(optionsInit)) {
606
+ report(optionsInit, `${callPrefix} options must be an object literal.`);
607
+ }
608
+ visit(optionsInit, isForbiddenContext, functionDepth);
609
+ }
610
+ const runInit = propsMap.get("run");
611
+ if (runInit) {
612
+ if (!ts.isArrowFunction(runInit) && !ts.isFunctionExpression(runInit) && !ts.isMethodDeclaration(runInit)) {
613
+ report(runInit, `${callPrefix} 'run' property must be a function expression, arrow function, or method property.`);
614
+ }
615
+ else {
616
+ const params = runInit.parameters;
617
+ const localLoopContextNames = new Set();
618
+ if (params && params.length >= 2) {
619
+ const secondParam = params[1];
620
+ if (secondParam && ts.isIdentifier(secondParam.name)) {
621
+ localLoopContextNames.add(secondParam.name.text);
622
+ }
623
+ }
624
+ if (localLoopContextNames.size === 0) {
625
+ localLoopContextNames.add("ctx");
626
+ localLoopContextNames.add("context");
627
+ }
628
+ function validateReturnExpression(expr) {
629
+ let unwrapped = expr;
630
+ while (ts.isParenthesizedExpression(unwrapped)) {
631
+ unwrapped = unwrapped.expression;
632
+ }
633
+ if (ts.isObjectLiteralExpression(unwrapped)) {
634
+ const retKeys = new Set();
635
+ let doneInit;
636
+ for (const prop of unwrapped.properties) {
637
+ if (ts.isPropertyAssignment(prop) || ts.isMethodDeclaration(prop)) {
638
+ const propName = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name) ? prop.name.text : "";
639
+ if (propName) {
640
+ retKeys.add(propName);
641
+ if (propName === "done" && ts.isPropertyAssignment(prop)) {
642
+ doneInit = prop.initializer;
643
+ }
644
+ }
645
+ }
646
+ else if (ts.isShorthandPropertyAssignment(prop)) {
647
+ const propName = prop.name.text;
648
+ retKeys.add(propName);
649
+ }
650
+ }
651
+ if (retKeys.has("result")) {
652
+ report(unwrapped, "Loop run return must not contain 'result'.");
653
+ }
654
+ if (!retKeys.has("done")) {
655
+ report(unwrapped, "Loop run return must contain 'done'.");
656
+ }
657
+ if (!retKeys.has("nextState")) {
658
+ report(unwrapped, "Loop run return must contain 'nextState'.");
659
+ }
660
+ if (doneInit && isStaticValue(doneInit)) {
661
+ const doneVal = parseStaticProperties(doneInit);
662
+ if (typeof doneVal !== "boolean") {
663
+ report(doneInit, "done must be a boolean.");
664
+ }
665
+ }
666
+ }
667
+ }
668
+ function findReturnStatements(n, returns) {
669
+ if (ts.isReturnStatement(n)) {
670
+ returns.push(n);
671
+ return;
672
+ }
673
+ if (ts.isFunctionDeclaration(n) || ts.isFunctionExpression(n) || ts.isArrowFunction(n) || ts.isMethodDeclaration(n)) {
674
+ return;
675
+ }
676
+ ts.forEachChild(n, child => findReturnStatements(child, returns));
677
+ }
678
+ if (runInit.body) {
679
+ if (ts.isBlock(runInit.body)) {
680
+ const returns = [];
681
+ findReturnStatements(runInit.body, returns);
682
+ for (const ret of returns) {
683
+ if (ret.expression) {
684
+ validateReturnExpression(ret.expression);
685
+ }
686
+ else {
687
+ report(ret, "Loop run return must contain 'done' and 'nextState'.");
688
+ }
689
+ }
690
+ }
691
+ else {
692
+ validateReturnExpression(runInit.body);
693
+ }
694
+ }
695
+ visit(runInit, true, functionDepth, true, localLoopContextNames);
696
+ }
697
+ }
698
+ }
421
699
  function validateAgentCall(node, isContextForm, contextName = "ctx") {
422
700
  const firstArg = node.arguments[0];
423
701
  const callPrefix = isContextForm ? `${contextName}.agent()` : "agent()";
@@ -511,7 +789,7 @@ export function validateWorkflow(workflow, options) {
511
789
  if (definitionProp) {
512
790
  const definitionArg = ts.isPropertyAssignment(definitionProp) ? definitionProp.initializer : undefined;
513
791
  validateSharedAgentId(definitionArg);
514
- validateSharedAgentInput(definitionArg, firstArg, true);
792
+ validateSharedAgentInput(definitionArg, firstArg);
515
793
  }
516
794
  else {
517
795
  if (!promptProp && !hasSpread) {
@@ -536,19 +814,21 @@ export function validateWorkflow(workflow, options) {
536
814
  }
537
815
  }
538
816
  }
539
- function isToolOrCtxTool(node) {
817
+ function isToolOrCtxTool(node, currentLoopCtxNames) {
540
818
  // direct tool
541
819
  if (ts.isIdentifier(node) && node.text === "tool")
542
820
  return true;
543
821
  // ctx.tool
544
822
  if (ts.isPropertyAccessExpression(node) &&
545
- ts.isIdentifier(node.expression) && contextParameterNames.has(node.expression.text) &&
823
+ ts.isIdentifier(node.expression) &&
824
+ (contextParameterNames.has(node.expression.text) || (currentLoopCtxNames && currentLoopCtxNames.has(node.expression.text))) &&
546
825
  node.name.text === "tool") {
547
826
  return true;
548
827
  }
549
828
  // ctx["tool"]
550
829
  if (ts.isElementAccessExpression(node) &&
551
- ts.isIdentifier(node.expression) && contextParameterNames.has(node.expression.text) &&
830
+ ts.isIdentifier(node.expression) &&
831
+ (contextParameterNames.has(node.expression.text) || (currentLoopCtxNames && currentLoopCtxNames.has(node.expression.text))) &&
552
832
  ts.isStringLiteral(node.argumentExpression) && node.argumentExpression.text === "tool") {
553
833
  return true;
554
834
  }
@@ -557,39 +837,39 @@ export function validateWorkflow(workflow, options) {
557
837
  if (ts.isPropertyAccessExpression(node)) {
558
838
  const name = node.name.text;
559
839
  if (name === "bind" || name === "call" || name === "apply") {
560
- if (isToolOrCtxTool(node.expression))
840
+ if (isToolOrCtxTool(node.expression, currentLoopCtxNames))
561
841
  return true;
562
842
  }
563
843
  }
564
844
  if (ts.isCallExpression(node)) {
565
- if (isToolOrCtxTool(node.expression))
845
+ if (isToolOrCtxTool(node.expression, currentLoopCtxNames))
566
846
  return true;
567
847
  }
568
848
  return false;
569
849
  }
570
- function isLikelyWorkflowContext(node) {
571
- if (ts.isIdentifier(node) && contextParameterNames.has(node.text))
850
+ function isLikelyWorkflowContext(node, currentLoopCtxNames) {
851
+ if (ts.isIdentifier(node) && (contextParameterNames.has(node.text) || (currentLoopCtxNames && currentLoopCtxNames.has(node.text))))
572
852
  return true;
573
853
  return false;
574
854
  }
575
- function checkBindingForToolAlias(name, initializer) {
855
+ function checkBindingForToolAlias(name, initializer, currentLoopCtxNames) {
576
856
  if (ts.isObjectBindingPattern(name)) {
577
857
  for (const element of name.elements) {
578
858
  const propName = element.propertyName ? (ts.isIdentifier(element.propertyName) ? element.propertyName.text : undefined) : (ts.isIdentifier(element.name) ? element.name.text : undefined);
579
859
  if (propName === "tool") {
580
860
  // If we have an initializer, check if it's the context.
581
861
  // If no initializer (like in parameter), we assume it's aliasing if the parameter looks like a context.
582
- if (!initializer || isLikelyWorkflowContext(initializer)) {
862
+ if (!initializer || isLikelyWorkflowContext(initializer, currentLoopCtxNames)) {
583
863
  report(element, "Aliasing tool() is not allowed. Use it directly as tool() or ctx.tool().");
584
864
  }
585
865
  }
586
866
  if (element.name && ts.isObjectBindingPattern(element.name)) {
587
- checkBindingForToolAlias(element.name, initializer);
867
+ checkBindingForToolAlias(element.name, initializer, currentLoopCtxNames);
588
868
  }
589
869
  }
590
870
  }
591
871
  }
592
- function visit(node, isForbiddenContext = false, functionDepth = 0) {
872
+ function visit(node, isForbiddenContext = false, functionDepth = 0, isInsideLoopRun = false, loopContextNames = new Set(), isInsideParallel = false, isInsideMainWorkflow = false) {
593
873
  // Skip the metadata declaration statement (export const meta = { ... })
594
874
  if (sourceFile.statements.length > 0 && node === sourceFile.statements[0]) {
595
875
  if (ts.isVariableStatement(node) && node.modifiers?.some(m => m.kind === ts.SyntaxKind.ExportKeyword)) {
@@ -604,24 +884,27 @@ export function validateWorkflow(workflow, options) {
604
884
  }
605
885
  let nextForbiddenContext = isForbiddenContext;
606
886
  let nextFunctionDepth = functionDepth;
887
+ const nextInsideLoopRun = isInsideLoopRun;
888
+ let nextInsideMainWorkflow = isInsideMainWorkflow;
889
+ const nextLoopContextNames = new Set(loopContextNames);
607
890
  if (ts.isVariableDeclaration(node)) {
608
891
  const init = node.initializer;
609
892
  if (init) {
610
- if (isToolOrCtxTool(init)) {
893
+ if (isToolOrCtxTool(init, nextLoopContextNames)) {
611
894
  report(node, "Aliasing tool() is not allowed. Use it directly as tool() or ctx.tool().");
612
895
  }
613
- checkBindingForToolAlias(node.name, init);
896
+ checkBindingForToolAlias(node.name, init, nextLoopContextNames);
614
897
  }
615
898
  else {
616
- checkBindingForToolAlias(node.name);
899
+ checkBindingForToolAlias(node.name, undefined, nextLoopContextNames);
617
900
  }
618
901
  }
619
902
  if (ts.isParameter(node)) {
620
- checkBindingForToolAlias(node.name);
903
+ checkBindingForToolAlias(node.name, undefined, nextLoopContextNames);
621
904
  }
622
905
  if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
623
906
  const rhs = node.right;
624
- if (isToolOrCtxTool(rhs)) {
907
+ if (isToolOrCtxTool(rhs, nextLoopContextNames)) {
625
908
  report(node, "Aliasing tool() is not allowed. Use it directly as tool() or ctx.tool().");
626
909
  }
627
910
  }
@@ -629,6 +912,7 @@ export function validateWorkflow(workflow, options) {
629
912
  nextFunctionDepth++;
630
913
  if (nextFunctionDepth > 1) {
631
914
  nextForbiddenContext = true;
915
+ nextInsideMainWorkflow = false;
632
916
  }
633
917
  else if (nextFunctionDepth === 1) {
634
918
  // Only the default exported function (the main workflow) is allowed to contain tools.
@@ -643,6 +927,10 @@ export function validateWorkflow(workflow, options) {
643
927
  }
644
928
  if (!isMainWorkflow) {
645
929
  nextForbiddenContext = true;
930
+ nextInsideMainWorkflow = false;
931
+ }
932
+ else {
933
+ nextInsideMainWorkflow = true;
646
934
  }
647
935
  }
648
936
  }
@@ -650,7 +938,7 @@ export function validateWorkflow(workflow, options) {
650
938
  const callee = node.expression;
651
939
  // Reject tool being passed as an argument
652
940
  for (const arg of node.arguments) {
653
- if (isToolOrCtxTool(arg)) {
941
+ if (isToolOrCtxTool(arg, nextLoopContextNames)) {
654
942
  report(arg, "Aliasing tool() is not allowed. Use it directly as tool() or ctx.tool().");
655
943
  }
656
944
  }
@@ -742,21 +1030,29 @@ export function validateWorkflow(workflow, options) {
742
1030
  if (idx === 1) {
743
1031
  // stagesArg and its children (the stage objects and their 'run' methods)
744
1032
  // need to recursively forbid tools.
745
- visit(arg, true, nextFunctionDepth);
1033
+ visit(arg, true, nextFunctionDepth, nextInsideLoopRun, nextLoopContextNames, isInsideParallel, false);
746
1034
  }
747
1035
  else {
748
- visit(arg, nextForbiddenContext, nextFunctionDepth);
1036
+ visit(arg, nextForbiddenContext, nextFunctionDepth, nextInsideLoopRun, nextLoopContextNames, isInsideParallel, nextInsideMainWorkflow);
749
1037
  }
750
1038
  });
751
1039
  return;
752
1040
  }
753
1041
  else if (calleeText === "parallel") {
754
1042
  nextForbiddenContext = true;
1043
+ node.arguments.forEach((arg) => {
1044
+ visit(arg, true, nextFunctionDepth, nextInsideLoopRun, nextLoopContextNames, true, nextInsideMainWorkflow);
1045
+ });
1046
+ return;
1047
+ }
1048
+ else if (calleeText === "loop") {
1049
+ validateLoopCall(node, false, "ctx", isForbiddenContext, functionDepth, isInsideParallel, isInsideLoopRun, isInsideMainWorkflow);
1050
+ return;
755
1051
  }
756
1052
  else if (calleeText === "defineAgent") {
757
1053
  const firstArg = node.arguments[0];
758
1054
  if (firstArg && ts.isObjectLiteralExpression(firstArg)) {
759
- visit(firstArg, true, nextFunctionDepth);
1055
+ visit(firstArg, true, nextFunctionDepth, nextInsideLoopRun, nextLoopContextNames, isInsideParallel, false);
760
1056
  return;
761
1057
  }
762
1058
  }
@@ -782,33 +1078,59 @@ export function validateWorkflow(workflow, options) {
782
1078
  else if (ts.isPropertyAccessExpression(callee)) {
783
1079
  const obj = callee.expression;
784
1080
  const prop = callee.name;
785
- if (ts.isIdentifier(obj) && contextParameterNames.has(obj.text)) {
786
- if (prop.text === "agent") {
787
- validateAgentCall(node, true, obj.text);
788
- }
789
- else if (prop.text === "workflow") {
790
- validateWorkflowCall(node, true, obj.text);
1081
+ if (ts.isIdentifier(obj)) {
1082
+ const isContextParam = contextParameterNames.has(obj.text) || nextLoopContextNames.has(obj.text);
1083
+ if (isContextParam) {
1084
+ if (prop.text === "agent") {
1085
+ validateAgentCall(node, true, obj.text);
1086
+ }
1087
+ else if (prop.text === "workflow") {
1088
+ validateWorkflowCall(node, true, obj.text);
1089
+ }
1090
+ else if (prop.text === "loop") {
1091
+ validateLoopCall(node, true, obj.text, isForbiddenContext, functionDepth, isInsideParallel, isInsideLoopRun, isInsideMainWorkflow);
1092
+ return;
1093
+ }
1094
+ else if (prop.text === "tool") {
1095
+ validateToolCall(node, true, nextForbiddenContext, obj.text);
1096
+ }
791
1097
  }
792
- else if (prop.text === "tool") {
793
- validateToolCall(node, true, nextForbiddenContext, obj.text);
1098
+ if (nextLoopContextNames.has(obj.text) || (nextInsideLoopRun && (obj.text === "ctx" || obj.text === "context"))) {
1099
+ if (prop.text === "break") {
1100
+ report(node, `${obj.text}.break() is not supported inside loop run callback.`);
1101
+ }
1102
+ else if (prop.text === "parallel") {
1103
+ report(node, `${obj.text}.parallel() is not supported inside loop run callback. Use top-level parallel() around loop task thunks instead.`);
1104
+ }
794
1105
  }
795
1106
  }
796
1107
  }
797
1108
  else if (ts.isElementAccessExpression(callee)) {
798
1109
  const obj = callee.expression;
799
1110
  const arg = callee.argumentExpression;
800
- if (ts.isIdentifier(obj) && contextParameterNames.has(obj.text) && ts.isStringLiteral(arg)) {
1111
+ if (ts.isIdentifier(obj) && ts.isStringLiteral(arg)) {
801
1112
  const propName = arg.text;
802
- if (["agent", "workflow", "tool"].includes(propName)) {
803
- report(node, `Computed access forms like ${obj.text}["${propName}"]() are not allowed. Use direct property access like ${obj.text}.${propName}() instead.`);
804
- if (propName === "agent") {
805
- validateAgentCall(node, true, obj.text);
1113
+ const isContextParam = contextParameterNames.has(obj.text) || nextLoopContextNames.has(obj.text);
1114
+ if (isContextParam) {
1115
+ if (["agent", "workflow", "tool"].includes(propName)) {
1116
+ report(node, `Computed access forms like ${obj.text}["${propName}"]() are not allowed. Use direct property access like ${obj.text}.${propName}() instead.`);
1117
+ if (propName === "agent") {
1118
+ validateAgentCall(node, true, obj.text);
1119
+ }
1120
+ else if (propName === "workflow") {
1121
+ validateWorkflowCall(node, true, obj.text);
1122
+ }
1123
+ else if (propName === "tool") {
1124
+ validateToolCall(node, true, nextForbiddenContext, obj.text);
1125
+ }
806
1126
  }
807
- else if (propName === "workflow") {
808
- validateWorkflowCall(node, true, obj.text);
1127
+ }
1128
+ if (nextLoopContextNames.has(obj.text) || (nextInsideLoopRun && (obj.text === "ctx" || obj.text === "context"))) {
1129
+ if (propName === "break") {
1130
+ report(node, `Computed access forms like ${obj.text}["break"]() are not allowed.`);
809
1131
  }
810
- else if (propName === "tool") {
811
- validateToolCall(node, true, nextForbiddenContext, obj.text);
1132
+ else if (propName === "parallel") {
1133
+ report(node, `${obj.text}.parallel() is not supported inside loop run callback. Use top-level parallel() around loop task thunks instead.`);
812
1134
  }
813
1135
  }
814
1136
  }
@@ -874,7 +1196,7 @@ export function validateWorkflow(workflow, options) {
874
1196
  }
875
1197
  }
876
1198
  }
877
- ts.forEachChild(node, (child) => visit(child, nextForbiddenContext, nextFunctionDepth));
1199
+ ts.forEachChild(node, (child) => visit(child, nextForbiddenContext, nextFunctionDepth, nextInsideLoopRun, nextLoopContextNames, isInsideParallel, nextInsideMainWorkflow));
878
1200
  }
879
1201
  visit(sourceFile);
880
1202
  return issues;
@@ -1000,7 +1322,8 @@ export function validateRegistryDependencies(registry, options) {
1000
1322
  knownWorkflowNames,
1001
1323
  workflowInputSchemas,
1002
1324
  allowDynamicSharedAgentIds: options.allowDynamicSharedAgentIds,
1003
- toolRegistry: options.toolRegistry
1325
+ toolRegistry: options.toolRegistry,
1326
+ maxLoopRounds: options.maxLoopRounds
1004
1327
  });
1005
1328
  const localErrors = issues.filter(issue => issue.severity !== "warning").map(issue => issue.message);
1006
1329
  // Log warnings for checked workflows. To avoid duplicate warnings for root workflow (which was