@llblab/pi-actors 0.37.0 → 0.38.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.
Files changed (74) hide show
  1. package/BACKLOG.md +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/README.md +6 -5
  4. package/dist/index.js +23 -3
  5. package/dist/lib/async-runs.d.ts +5 -0
  6. package/dist/lib/async-runs.js +87 -8
  7. package/dist/lib/command-templates.d.ts +2 -0
  8. package/dist/lib/execution.d.ts +4 -0
  9. package/dist/lib/execution.js +154 -13
  10. package/dist/lib/model-context.d.ts +56 -0
  11. package/dist/lib/model-context.js +220 -0
  12. package/dist/lib/observability.d.ts +2 -0
  13. package/dist/lib/observability.js +32 -6
  14. package/dist/lib/preflight-diagnostics.d.ts +27 -0
  15. package/dist/lib/preflight-diagnostics.js +86 -0
  16. package/dist/lib/prompts.d.ts +1 -1
  17. package/dist/lib/prompts.js +2 -2
  18. package/dist/lib/recipes-context.d.ts +7 -0
  19. package/dist/lib/recipes-context.js +75 -5
  20. package/dist/lib/recipes-discovery.js +15 -0
  21. package/dist/lib/recipes-references.d.ts +2 -0
  22. package/dist/lib/recipes-references.js +10 -0
  23. package/dist/lib/tools-local.d.ts +2 -1
  24. package/dist/lib/tools-local.js +6 -4
  25. package/dist/lib/tools-response.js +22 -1
  26. package/dist/lib/tools-spawn.d.ts +2 -1
  27. package/dist/lib/tools-spawn.js +2 -1
  28. package/dist/recipes/lens-swarm.json +15 -2
  29. package/dist/recipes/pipeline-release-readiness.json +22 -3
  30. package/dist/recipes/pipeline-review-readiness.json +22 -3
  31. package/dist/recipes/subagent-judge.json +2 -1
  32. package/dist/recipes/subagent-merge.json +5 -2
  33. package/dist/recipes/subagent-normalize.json +2 -1
  34. package/dist/recipes/subagent-preflight.json +29 -0
  35. package/dist/recipes/subagent-review-coordinator.json +79 -7
  36. package/dist/recipes/subagent-review.json +2 -1
  37. package/dist/recipes/subagent-verify.json +2 -1
  38. package/dist/scripts/async-runner.mjs +84 -10
  39. package/dist/scripts/conformance.mjs +1 -0
  40. package/dist/skills/actors/SKILL.md +5 -3
  41. package/dist/skills/swarm/SKILL.md +2 -2
  42. package/docs/async-runs.md +3 -1
  43. package/docs/command-templates.md +4 -1
  44. package/docs/recipe-library.md +5 -2
  45. package/docs/template-recipes.md +2 -2
  46. package/index.ts +24 -3
  47. package/lib/async-runs.ts +157 -8
  48. package/lib/command-templates.ts +2 -0
  49. package/lib/execution.ts +218 -24
  50. package/lib/model-context.ts +359 -0
  51. package/lib/observability.ts +35 -6
  52. package/lib/preflight-diagnostics.ts +132 -0
  53. package/lib/prompts.ts +2 -2
  54. package/lib/recipes-context.ts +86 -5
  55. package/lib/recipes-discovery.ts +15 -0
  56. package/lib/recipes-references.ts +12 -0
  57. package/lib/tools-local.ts +22 -8
  58. package/lib/tools-response.ts +26 -1
  59. package/lib/tools-spawn.ts +6 -2
  60. package/package.json +1 -1
  61. package/recipes/lens-swarm.json +15 -2
  62. package/recipes/pipeline-release-readiness.json +22 -3
  63. package/recipes/pipeline-review-readiness.json +22 -3
  64. package/recipes/subagent-judge.json +2 -1
  65. package/recipes/subagent-merge.json +5 -2
  66. package/recipes/subagent-normalize.json +2 -1
  67. package/recipes/subagent-preflight.json +29 -0
  68. package/recipes/subagent-review-coordinator.json +79 -7
  69. package/recipes/subagent-review.json +2 -1
  70. package/recipes/subagent-verify.json +2 -1
  71. package/scripts/async-runner.mjs +84 -10
  72. package/scripts/conformance.mjs +1 -0
  73. package/skills/actors/SKILL.md +5 -3
  74. package/skills/swarm/SKILL.md +2 -2
package/lib/execution.ts CHANGED
@@ -32,10 +32,13 @@ export interface ToolExecResult {
32
32
  export interface BranchReport {
33
33
  code: number;
34
34
  command: string;
35
+ failureReason?: string;
35
36
  killed: boolean;
36
37
  label: string;
37
38
  status: "done" | "failed" | "timeout";
38
39
  stderr?: string;
40
+ stderrBytes: number;
41
+ stdout?: string;
39
42
  stdoutBytes: number;
40
43
  }
41
44
 
@@ -62,6 +65,7 @@ export interface RegisteredToolExecutionResult {
62
65
  killed: boolean;
63
66
  }>;
64
67
  softQuorum?: SoftQuorumReport;
68
+ failureReason?: string;
65
69
  template: CommandTemplates.CommandTemplateValue;
66
70
  templateWarnings?: string[];
67
71
  tool: string;
@@ -123,6 +127,20 @@ function formatCommandDetail(commands: string[]): string {
123
127
  return commands.length === 1 ? commands[0] : commands.join(" && ");
124
128
  }
125
129
 
130
+ function getExecutionFailureReason(
131
+ branches: BranchReport[],
132
+ softQuorum: SoftQuorumReport | undefined,
133
+ stderr: string,
134
+ ): string {
135
+ if (/parallel branch quorum not met/.test(stderr)) {
136
+ return "parallel_quorum_not_met";
137
+ }
138
+ if (branches.length > 0 && softQuorum?.usable === false) {
139
+ return "all_branches_unusable";
140
+ }
141
+ return "command_failed";
142
+ }
143
+
126
144
  function mergeDefaults(
127
145
  inherited: Record<string, unknown> | undefined,
128
146
  own: Record<string, unknown> | undefined,
@@ -145,22 +163,40 @@ function getBranchStatus(result: ToolExecResult): BranchReport["status"] {
145
163
  return result.killed ? "timeout" : "failed";
146
164
  }
147
165
 
166
+ function getBranchFailureReason(
167
+ result: ToolExecResult,
168
+ ): string | undefined {
169
+ if (result.code === 0) {
170
+ return result.stdout.trim() ? undefined : "empty_output";
171
+ }
172
+ if (result.killed) return "timeout_or_killed";
173
+ return result.stderr.trim() ? "nonzero_exit" : "nonzero_exit_empty_stderr";
174
+ }
175
+
148
176
  function createBranchReport(
149
177
  label: string,
150
178
  command: string,
151
179
  result: ToolExecResult,
152
180
  ): BranchReport {
181
+ const failureReason = getBranchFailureReason(result);
153
182
  return {
154
183
  code: result.code,
155
184
  command,
185
+ ...(failureReason ? { failureReason } : {}),
156
186
  killed: result.killed,
157
187
  label,
158
188
  status: getBranchStatus(result),
159
- ...(result.stderr ? { stderr: result.stderr.slice(0, 1000) } : {}),
189
+ ...(result.stderr ? { stderr: result.stderr.slice(-1000) } : {}),
190
+ stderrBytes: Buffer.byteLength(result.stderr),
191
+ ...(result.stdout ? { stdout: result.stdout.slice(-1000) } : {}),
160
192
  stdoutBytes: Buffer.byteLength(result.stdout),
161
193
  };
162
194
  }
163
195
 
196
+ function isUsableBranch(branch: BranchReport): boolean {
197
+ return branch.status === "done" && branch.stdoutBytes > 0;
198
+ }
199
+
164
200
  function createSoftQuorum(
165
201
  branches: BranchReport[],
166
202
  ): SoftQuorumReport | undefined {
@@ -173,10 +209,14 @@ function createSoftQuorum(
173
209
  done,
174
210
  expected: branches.length,
175
211
  failed,
176
- usable: done > 0,
212
+ usable: branches.some(isUsableBranch),
177
213
  };
178
214
  }
179
215
 
216
+ function countUsableBranches(branches: BranchReport[]): number {
217
+ return branches.filter(isUsableBranch).length;
218
+ }
219
+
180
220
  function getMaxParallelBranches(): number {
181
221
  const raw = Number(process.env.PI_ACTORS_MAX_PARALLEL_BRANCHES ?? "");
182
222
  return Number.isInteger(raw) && raw > 0 ? raw : DEFAULT_MAX_PARALLEL_BRANCHES;
@@ -230,6 +270,34 @@ function normalizeRetry(
230
270
  return resolved;
231
271
  }
232
272
 
273
+ function normalizeConcurrency(
274
+ value: number | string | undefined,
275
+ values: Record<string, unknown>,
276
+ branchCount: number,
277
+ ): number {
278
+ const resolved = resolveNumericControlField(value, values, "concurrency");
279
+ if (resolved === undefined) return branchCount;
280
+ if (!Number.isInteger(resolved) || resolved < 1)
281
+ throw new Error("Command template concurrency must be a positive integer.");
282
+ return Math.min(resolved, branchCount);
283
+ }
284
+
285
+ function normalizeMinSuccessful(
286
+ value: number | string | undefined,
287
+ values: Record<string, unknown>,
288
+ branchCount: number,
289
+ ): number | undefined {
290
+ const resolved = resolveNumericControlField(value, values, "min_successful");
291
+ if (resolved === undefined) return undefined;
292
+ if (!Number.isInteger(resolved) || resolved < 0)
293
+ throw new Error("Command template min_successful must be a non-negative integer.");
294
+ if (resolved > branchCount)
295
+ throw new Error(
296
+ `Command template min_successful ${resolved} exceeds branch count ${branchCount}.`,
297
+ );
298
+ return resolved;
299
+ }
300
+
233
301
  function getRecoverConfig(
234
302
  config: CommandTemplates.CommandTemplateValue,
235
303
  ): CommandTemplates.CommandTemplateConfig {
@@ -309,11 +377,34 @@ async function applyDelay(
309
377
  await sleep(resolved, signal);
310
378
  }
311
379
 
380
+ function getParallelStatus(
381
+ branches: BranchReport[],
382
+ minSuccessful: number | undefined,
383
+ ): "complete" | "degraded" | "insufficient_data" {
384
+ const usable = countUsableBranches(branches);
385
+ if (minSuccessful !== undefined && usable < minSuccessful) {
386
+ return "insufficient_data";
387
+ }
388
+ return branches.every(isUsableBranch) ? "complete" : "degraded";
389
+ }
390
+
391
+ function formatParallelStatusHeader(
392
+ branches: BranchReport[],
393
+ minSuccessful: number | undefined,
394
+ ): string | undefined {
395
+ if (minSuccessful === undefined) return undefined;
396
+ return `--- parallel_status: ${getParallelStatus(
397
+ branches,
398
+ minSuccessful,
399
+ )} usable: ${countUsableBranches(branches)} expected: ${branches.length} minimum: ${minSuccessful} ---`;
400
+ }
401
+
312
402
  function joinParallelStdout(
313
403
  branches: BranchReport[],
314
404
  results: ToolExecResult[],
405
+ minSuccessful?: number,
315
406
  ): string {
316
- return results
407
+ const body = results
317
408
  .map((result, index) => {
318
409
  const branch = branches[index];
319
410
  const header = `--- branch: ${branch.label} status: ${branch.status} ---`;
@@ -322,6 +413,30 @@ function joinParallelStdout(
322
413
  return `${header}\nexit: ${branch.code}${stderr}`;
323
414
  })
324
415
  .join("\n");
416
+ return [formatParallelStatusHeader(branches, minSuccessful), body]
417
+ .filter(Boolean)
418
+ .join("\n");
419
+ }
420
+
421
+ async function mapConcurrent<T, TResult>(
422
+ items: readonly T[],
423
+ concurrency: number,
424
+ fn: (item: T, index: number) => Promise<TResult>,
425
+ ): Promise<TResult[]> {
426
+ const results = new Array<TResult>(items.length);
427
+ let nextIndex = 0;
428
+ async function worker(): Promise<void> {
429
+ for (;;) {
430
+ const index = nextIndex;
431
+ nextIndex += 1;
432
+ if (index >= items.length) return;
433
+ results[index] = await fn(items[index]!, index);
434
+ }
435
+ }
436
+ await Promise.all(
437
+ Array.from({ length: Math.min(concurrency, items.length) }, () => worker()),
438
+ );
439
+ return results;
325
440
  }
326
441
 
327
442
  async function executeRetriableTemplateConfig(
@@ -466,7 +581,17 @@ async function executeTemplateConfig(
466
581
  };
467
582
  });
468
583
  return executeTemplateConfig(
469
- { parallel: normalized.parallel === true, template: repeatedSteps },
584
+ {
585
+ parallel: normalized.parallel === true,
586
+ ...(normalized.concurrency !== undefined
587
+ ? { concurrency: normalized.concurrency }
588
+ : {}),
589
+ ...(normalized.min_successful !== undefined
590
+ ? { min_successful: normalized.min_successful }
591
+ : {}),
592
+ ...(normalized.failure !== undefined ? { failure: normalized.failure } : {}),
593
+ template: repeatedSteps,
594
+ },
470
595
  context,
471
596
  params,
472
597
  exec,
@@ -552,19 +677,27 @@ async function executeTemplateConfig(
552
677
  throw new Error(formatToolText("Tool template produced no command steps."));
553
678
  if (normalized.parallel === true) {
554
679
  assertParallelBranchLimit(steps.length);
555
- const branchResults = await Promise.all(
556
- steps.map((step) =>
557
- executeTemplateConfig(
558
- step,
559
- context,
560
- params,
561
- exec,
562
- cwd,
563
- signal,
564
- stdin,
565
- false,
566
- actorRecipeContext,
567
- ),
680
+ const concurrency = normalizeConcurrency(
681
+ normalized.concurrency,
682
+ controlValues,
683
+ steps.length,
684
+ );
685
+ const minSuccessful = normalizeMinSuccessful(
686
+ normalized.min_successful,
687
+ controlValues,
688
+ steps.length,
689
+ );
690
+ const branchResults = await mapConcurrent(steps, concurrency, (step) =>
691
+ executeTemplateConfig(
692
+ step,
693
+ context,
694
+ params,
695
+ exec,
696
+ cwd,
697
+ signal,
698
+ stdin,
699
+ false,
700
+ actorRecipeContext,
568
701
  ),
569
702
  );
570
703
  const commands = branchResults.flatMap((item) => item.commands);
@@ -576,6 +709,12 @@ async function executeTemplateConfig(
576
709
  item.result,
577
710
  ),
578
711
  );
712
+ const usableBranches = countUsableBranches(branches);
713
+ const quorumUnmet =
714
+ minSuccessful !== undefined && usableBranches < minSuccessful;
715
+ const quorumFailureMessage = quorumUnmet
716
+ ? `parallel branch quorum not met: usable ${usableBranches}/${branches.length}, minimum ${minSuccessful}`
717
+ : "";
579
718
  const nodeFailure = getFailureScope(normalized);
580
719
  const rootFailure = branchResults.find((item, index) => {
581
720
  if (item.result.code === 0) return false;
@@ -603,6 +742,9 @@ async function executeTemplateConfig(
603
742
  const firstFailedBranch = branchResults.find(
604
743
  (item) => item.result.code !== 0,
605
744
  );
745
+ const allBranchesUnusable = branches.every(
746
+ (branch) => !isUsableBranch(branch),
747
+ );
606
748
  const successful = branchResults.map((item) => {
607
749
  if (item.result.code === 0) return item.result;
608
750
  addResultFailure(failures, item);
@@ -615,9 +757,31 @@ async function executeTemplateConfig(
615
757
  .map((item) => item.stderr)
616
758
  .filter(Boolean)
617
759
  .join("\n"),
618
- stdout: joinParallelStdout(branches, successful),
760
+ stdout: joinParallelStdout(branches, successful, minSuccessful),
619
761
  };
620
- if (firstFailedBranch && nodeFailure === "branch") {
762
+ if (quorumUnmet && nodeFailure === "root") {
763
+ return {
764
+ commands,
765
+ branches: [
766
+ ...branchResults.flatMap((item) => item.branches),
767
+ ...branches,
768
+ ],
769
+ criticalFailure: true,
770
+ failureScope: "root",
771
+ failures,
772
+ result: {
773
+ ...result,
774
+ code: firstFailedBranch?.result.code || 1,
775
+ stderr: [result.stderr, quorumFailureMessage]
776
+ .filter(Boolean)
777
+ .join("\n"),
778
+ },
779
+ };
780
+ }
781
+ const branchFailureTriggered = minSuccessful === undefined
782
+ ? firstFailedBranch || allBranchesUnusable
783
+ : quorumUnmet;
784
+ if (branchFailureTriggered && nodeFailure === "branch") {
621
785
  return {
622
786
  commands,
623
787
  branches: [
@@ -626,7 +790,19 @@ async function executeTemplateConfig(
626
790
  ],
627
791
  failureScope: "branch",
628
792
  failures,
629
- result: { ...result, code: firstFailedBranch.result.code || 1 },
793
+ result: {
794
+ ...result,
795
+ code: firstFailedBranch?.result.code || 1,
796
+ stderr: [
797
+ result.stderr,
798
+ allBranchesUnusable
799
+ ? "all parallel branches failed or produced empty output"
800
+ : "",
801
+ quorumFailureMessage,
802
+ ]
803
+ .filter(Boolean)
804
+ .join("\n"),
805
+ },
630
806
  };
631
807
  }
632
808
  return {
@@ -735,6 +911,7 @@ export async function executeRegisteredTool(
735
911
  );
736
912
  const command = formatCommandDetail(executed.commands);
737
913
  const result = executed.result;
914
+ const softQuorum = createSoftQuorum(executed.branches);
738
915
  if (result.code !== 0) {
739
916
  const formatted = formatFailureOutput(
740
917
  cfg.name,
@@ -743,7 +920,26 @@ export async function executeRegisteredTool(
743
920
  result.stdout,
744
921
  result.stderr,
745
922
  );
746
- throw new Error(formatted.text);
923
+ throw Object.assign(new Error(formatted.text), {
924
+ details: {
925
+ branches: executed.branches,
926
+ code: result.code,
927
+ command,
928
+ killed: result.killed,
929
+ ...(executed.failures.length > 0
930
+ ? { nonCriticalFailures: executed.failures }
931
+ : {}),
932
+ ...(softQuorum ? { softQuorum } : {}),
933
+ failureReason: getExecutionFailureReason(
934
+ executed.branches,
935
+ softQuorum,
936
+ result.stderr,
937
+ ),
938
+ template: cfg.template!,
939
+ tool: cfg.name,
940
+ truncated: formatted.truncated,
941
+ },
942
+ });
747
943
  }
748
944
  const formatted = formatOutput(cfg.name, "stdout", result.stdout);
749
945
  const templateWarnings = CommandTemplates.getCommandTemplateWarnings(
@@ -760,9 +956,7 @@ export async function executeRegisteredTool(
760
956
  ...(executed.failures.length > 0
761
957
  ? { nonCriticalFailures: executed.failures }
762
958
  : {}),
763
- ...(createSoftQuorum(executed.branches)
764
- ? { softQuorum: createSoftQuorum(executed.branches) }
765
- : {}),
959
+ ...(softQuorum ? { softQuorum } : {}),
766
960
  template: cfg.template!,
767
961
  ...(templateWarnings.length > 0 ? { templateWarnings } : {}),
768
962
  tool: cfg.name,