@llblab/pi-actors 0.12.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 (86) hide show
  1. package/AGENTS.md +72 -0
  2. package/BACKLOG.md +38 -0
  3. package/CHANGELOG.md +179 -0
  4. package/README.md +338 -0
  5. package/docs/README.md +21 -0
  6. package/docs/actor-messages.md +149 -0
  7. package/docs/async-runs.md +335 -0
  8. package/docs/command-templates.md +424 -0
  9. package/docs/component-recipes.md +148 -0
  10. package/docs/recipe-library.md +176 -0
  11. package/docs/task-first-recipes.md +233 -0
  12. package/docs/template-recipes.md +285 -0
  13. package/docs/tool-registry.md +142 -0
  14. package/index.ts +198 -0
  15. package/lib/actor-messages.ts +120 -0
  16. package/lib/async-runs.ts +688 -0
  17. package/lib/command-templates.ts +795 -0
  18. package/lib/config.ts +266 -0
  19. package/lib/execution.ts +720 -0
  20. package/lib/file-state.ts +24 -0
  21. package/lib/identity.ts +29 -0
  22. package/lib/observability.ts +525 -0
  23. package/lib/output.ts +123 -0
  24. package/lib/paths.ts +35 -0
  25. package/lib/prompts.ts +75 -0
  26. package/lib/recipe-references.ts +586 -0
  27. package/lib/registry.ts +302 -0
  28. package/lib/runtime.ts +101 -0
  29. package/lib/schema.ts +402 -0
  30. package/lib/temp.ts +44 -0
  31. package/lib/tools.ts +651 -0
  32. package/package.json +52 -0
  33. package/recipes/music-player.json +25 -0
  34. package/recipes/pipeline-architect-coordinator.json +88 -0
  35. package/recipes/pipeline-artifact-report.json +52 -0
  36. package/recipes/pipeline-artifact-write.json +66 -0
  37. package/recipes/pipeline-async-run-ops.json +67 -0
  38. package/recipes/pipeline-checkpoint-continuation.json +57 -0
  39. package/recipes/pipeline-development-tasking.json +73 -0
  40. package/recipes/pipeline-docs-maintenance.json +72 -0
  41. package/recipes/pipeline-media-library.json +51 -0
  42. package/recipes/pipeline-quorum-review.json +72 -0
  43. package/recipes/pipeline-release-readiness.json +83 -0
  44. package/recipes/pipeline-repo-health.json +81 -0
  45. package/recipes/pipeline-research-synthesis.json +87 -0
  46. package/recipes/pipeline-review-readiness.json +49 -0
  47. package/recipes/subagent-artifact.json +26 -0
  48. package/recipes/subagent-checkpoint.json +27 -0
  49. package/recipes/subagent-conflict-report.json +25 -0
  50. package/recipes/subagent-contradiction-map.json +26 -0
  51. package/recipes/subagent-critic.json +28 -0
  52. package/recipes/subagent-evidence-map.json +26 -0
  53. package/recipes/subagent-followup.json +27 -0
  54. package/recipes/subagent-judge.json +26 -0
  55. package/recipes/subagent-merge.json +26 -0
  56. package/recipes/subagent-message.json +29 -0
  57. package/recipes/subagent-normalize.json +24 -0
  58. package/recipes/subagent-plan.json +26 -0
  59. package/recipes/subagent-prompt.json +22 -0
  60. package/recipes/subagent-quorum.json +41 -0
  61. package/recipes/subagent-review-coordinator.json +107 -0
  62. package/recipes/subagent-review.json +30 -0
  63. package/recipes/subagent-task-card.json +28 -0
  64. package/recipes/subagent-tools.json +17 -0
  65. package/recipes/subagent-verify.json +27 -0
  66. package/recipes/subagents-prompts.json +32 -0
  67. package/recipes/utility-actor-message.json +24 -0
  68. package/recipes/utility-artifact-manifest.json +17 -0
  69. package/recipes/utility-artifact-write.json +17 -0
  70. package/recipes/utility-changelog-head.json +12 -0
  71. package/recipes/utility-changelog-section.json +14 -0
  72. package/recipes/utility-git-log.json +12 -0
  73. package/recipes/utility-git-status.json +10 -0
  74. package/recipes/utility-jsonl-tail.json +11 -0
  75. package/recipes/utility-markdown-index.json +15 -0
  76. package/recipes/utility-package-summary.json +12 -0
  77. package/recipes/utility-playlist-build.json +18 -0
  78. package/recipes/utility-playlist-scan.json +12 -0
  79. package/recipes/utility-run-state-files.json +14 -0
  80. package/recipes/utility-run-summary.json +12 -0
  81. package/recipes/utility-validate-recipe.json +14 -0
  82. package/recipes/utility-validation-wrapper.json +14 -0
  83. package/scripts/async-runner.mjs +170 -0
  84. package/scripts/music-player.mjs +637 -0
  85. package/scripts/recipe-utils.mjs +273 -0
  86. package/scripts/validate-recipe.mjs +89 -0
@@ -0,0 +1,720 @@
1
+ /**
2
+ * Registered tool execution runtime
3
+ * Zones: tool execution, command templates, output formatting
4
+ * Owns command-template invocation execution and pi tool-result payload formatting
5
+ */
6
+
7
+ import type { RegisteredTool } from "./config.ts";
8
+ import { formatFailureOutput, formatOutput, formatToolText } from "./output.ts";
9
+ import * as CommandTemplates from "./command-templates.ts";
10
+ import * as Schema from "./schema.ts";
11
+
12
+ export interface ToolExecOptions {
13
+ cwd?: string;
14
+ signal?: AbortSignal;
15
+ stdin?: string;
16
+ timeout?: number;
17
+ retry?: number;
18
+ }
19
+
20
+ export interface ToolExecResult {
21
+ stdout: string;
22
+ stderr: string;
23
+ code: number;
24
+ killed: boolean;
25
+ }
26
+
27
+ export interface BranchReport {
28
+ code: number;
29
+ command: string;
30
+ killed: boolean;
31
+ label: string;
32
+ status: "done" | "failed" | "timeout";
33
+ stderr?: string;
34
+ stdoutBytes: number;
35
+ }
36
+
37
+ export interface SoftQuorumReport {
38
+ coverage: number;
39
+ degraded: boolean;
40
+ done: number;
41
+ expected: number;
42
+ failed: number;
43
+ usable: boolean;
44
+ }
45
+
46
+ export interface RegisteredToolExecutionResult {
47
+ content: Array<{ type: "text"; text: string }>;
48
+ details: {
49
+ branches?: BranchReport[];
50
+ code: number;
51
+ command: string;
52
+ fullOutputPath?: string;
53
+ killed: boolean;
54
+ nonCriticalFailures?: Array<{
55
+ code: number;
56
+ command: string;
57
+ killed: boolean;
58
+ }>;
59
+ softQuorum?: SoftQuorumReport;
60
+ template: CommandTemplates.CommandTemplateValue;
61
+ templateWarnings?: string[];
62
+ tool: string;
63
+ truncated: boolean;
64
+ };
65
+ }
66
+
67
+ export type RegisteredToolExec = (
68
+ command: string,
69
+ args: string[],
70
+ options?: ToolExecOptions,
71
+ ) => Promise<ToolExecResult>;
72
+
73
+ type TemplateExecution = {
74
+ branches: BranchReport[];
75
+ commands: string[];
76
+ criticalFailure?: boolean;
77
+ failureScope?: CommandTemplates.CommandTemplateFailureScope;
78
+ failures: Array<{ code: number; command: string; killed: boolean }>;
79
+ result: ToolExecResult;
80
+ };
81
+
82
+ function textContent(text: string) {
83
+ return { type: "text" as const, text };
84
+ }
85
+
86
+ function createTemplateConfig(
87
+ cfg: RegisteredTool,
88
+ ): CommandTemplates.CommandTemplateObjectConfig {
89
+ if (!cfg.template)
90
+ throw new Error(`Tool "${cfg.name}" has no command template.`);
91
+ if (typeof cfg.template === "object" && !Array.isArray(cfg.template)) {
92
+ return {
93
+ ...cfg.template,
94
+ args: cfg.template.args ?? cfg.args,
95
+ defaults: mergeDefaults(cfg.defaults, cfg.template.defaults),
96
+ };
97
+ }
98
+ return { args: cfg.args, defaults: cfg.defaults, template: cfg.template };
99
+ }
100
+
101
+ function formatCommandDetail(commands: string[]): string {
102
+ return commands.length === 1 ? commands[0] : commands.join(" && ");
103
+ }
104
+
105
+ function mergeDefaults(
106
+ inherited: Record<string, unknown> | undefined,
107
+ own: Record<string, unknown> | undefined,
108
+ ): Record<string, unknown> | undefined {
109
+ if (!inherited && !own) return undefined;
110
+ return { ...(inherited ?? {}), ...(own ?? {}) };
111
+ }
112
+
113
+ function getNodeLabel(
114
+ config: CommandTemplates.CommandTemplateConfig,
115
+ index?: number,
116
+ ): string {
117
+ const normalized = CommandTemplates.normalizeCommandTemplateConfig(config);
118
+ if (normalized.label) return normalized.label;
119
+ return index === undefined ? "command" : `branch ${index + 1}`;
120
+ }
121
+
122
+ function getBranchStatus(result: ToolExecResult): BranchReport["status"] {
123
+ if (result.code === 0) return "done";
124
+ return result.killed ? "timeout" : "failed";
125
+ }
126
+
127
+ function createBranchReport(
128
+ label: string,
129
+ command: string,
130
+ result: ToolExecResult,
131
+ ): BranchReport {
132
+ return {
133
+ code: result.code,
134
+ command,
135
+ killed: result.killed,
136
+ label,
137
+ status: getBranchStatus(result),
138
+ ...(result.stderr ? { stderr: result.stderr.slice(0, 1000) } : {}),
139
+ stdoutBytes: Buffer.byteLength(result.stdout),
140
+ };
141
+ }
142
+
143
+ function createSoftQuorum(
144
+ branches: BranchReport[],
145
+ ): SoftQuorumReport | undefined {
146
+ if (branches.length === 0) return undefined;
147
+ const done = branches.filter((branch) => branch.status === "done").length;
148
+ const failed = branches.length - done;
149
+ return {
150
+ coverage: done / branches.length,
151
+ degraded: failed > 0,
152
+ done,
153
+ expected: branches.length,
154
+ failed,
155
+ usable: done > 0,
156
+ };
157
+ }
158
+
159
+ function normalizeFailureScope(
160
+ value: CommandTemplates.CommandTemplateFailureScope | undefined,
161
+ ): CommandTemplates.CommandTemplateFailureScope {
162
+ if (value === undefined) return "continue";
163
+ if (value === "continue" || value === "branch" || value === "root")
164
+ return value;
165
+ throw new Error(
166
+ "Command template failure must be one of: continue, branch, root.",
167
+ );
168
+ }
169
+
170
+ function getFailureScope(
171
+ config: CommandTemplates.CommandTemplateConfig,
172
+ ): CommandTemplates.CommandTemplateFailureScope {
173
+ const normalized = CommandTemplates.normalizeCommandTemplateConfig(config);
174
+ return normalizeFailureScope(normalized.failure);
175
+ }
176
+
177
+ function maxFailureScope(
178
+ ...scopes: Array<CommandTemplates.CommandTemplateFailureScope | undefined>
179
+ ): CommandTemplates.CommandTemplateFailureScope {
180
+ const rank = { branch: 1, continue: 0, root: 2 } as const;
181
+ return scopes.reduce<CommandTemplates.CommandTemplateFailureScope>(
182
+ (current, scope) =>
183
+ rank[scope ?? "continue"] > rank[current] ? scope! : current,
184
+ "continue",
185
+ );
186
+ }
187
+
188
+ function normalizeRetry(
189
+ value: number | string | undefined,
190
+ values: Record<string, unknown>,
191
+ ): number {
192
+ const resolved = resolveNumericControlField(value, values, "retry");
193
+ if (resolved === undefined) return 1;
194
+ if (!Number.isInteger(resolved) || resolved < 1)
195
+ throw new Error("Command template retry must be a positive integer.");
196
+ return resolved;
197
+ }
198
+
199
+ function getRecoverConfig(
200
+ config: CommandTemplates.CommandTemplateValue,
201
+ ): CommandTemplates.CommandTemplateConfig {
202
+ const recovered = Array.isArray(config) ? { template: config } : config;
203
+ const normalized = CommandTemplates.normalizeCommandTemplateConfig(recovered);
204
+ if (normalized.failure !== undefined) return recovered;
205
+ return { ...normalized, failure: "root" };
206
+ }
207
+
208
+ function addResultFailure(
209
+ failures: Array<{ code: number; command: string; killed: boolean }>,
210
+ execution: TemplateExecution,
211
+ ): void {
212
+ if (execution.result.code === 0) return;
213
+ const failure = {
214
+ code: execution.result.code,
215
+ command: execution.commands.at(-1) ?? "<template>",
216
+ killed: execution.result.killed,
217
+ };
218
+ const last = failures.at(-1);
219
+ if (
220
+ last?.code === failure.code &&
221
+ last.command === failure.command &&
222
+ last.killed === failure.killed
223
+ )
224
+ return;
225
+ failures.push(failure);
226
+ }
227
+
228
+ function mergeExecution(
229
+ target: TemplateExecution,
230
+ source: TemplateExecution,
231
+ ): void {
232
+ target.branches.push(...source.branches);
233
+ target.commands.push(...source.commands);
234
+ target.failures.push(...source.failures);
235
+ }
236
+
237
+ function sleep(ms: number, signal: AbortSignal | undefined): Promise<void> {
238
+ return new Promise((resolve) => {
239
+ let timeoutId: NodeJS.Timeout | undefined;
240
+ const settle = (): void => {
241
+ if (timeoutId) clearTimeout(timeoutId);
242
+ if (signal) signal.removeEventListener("abort", settle);
243
+ resolve();
244
+ };
245
+ if (signal?.aborted) return settle();
246
+ timeoutId = setTimeout(settle, ms);
247
+ if (signal) signal.addEventListener("abort", settle, { once: true });
248
+ });
249
+ }
250
+
251
+ function resolveNumericControlField(
252
+ value: number | string | undefined,
253
+ values: Record<string, unknown>,
254
+ label: string,
255
+ ): number | undefined {
256
+ if (value === undefined) return undefined;
257
+ const resolved = typeof value === "string"
258
+ ? CommandTemplates.substituteCommandTemplateToken(value, values, label)
259
+ : value;
260
+ if (resolved === "") return undefined;
261
+ const numeric = Number(resolved);
262
+ if (!Number.isFinite(numeric) || numeric < 0)
263
+ throw new Error(`Command template ${label} must be a non-negative number.`);
264
+ return numeric;
265
+ }
266
+
267
+ async function applyDelay(
268
+ delay: number | string | undefined,
269
+ values: Record<string, unknown>,
270
+ signal: AbortSignal | undefined,
271
+ ): Promise<void> {
272
+ const resolved = resolveNumericControlField(delay, values, "delay");
273
+ if (resolved === undefined || resolved <= 0) return;
274
+ await sleep(resolved, signal);
275
+ }
276
+
277
+ function joinParallelStdout(
278
+ branches: BranchReport[],
279
+ results: ToolExecResult[],
280
+ ): string {
281
+ return results
282
+ .map((result, index) => {
283
+ const branch = branches[index];
284
+ const header = `--- branch: ${branch.label} status: ${branch.status} ---`;
285
+ if (branch.status === "done") return `${header}\n${result.stdout}`;
286
+ const stderr = branch.stderr ? `\nstderr: ${branch.stderr}` : "";
287
+ return `${header}\nexit: ${branch.code}${stderr}`;
288
+ })
289
+ .join("\n");
290
+ }
291
+
292
+ async function executeRetriableTemplateConfig(
293
+ normalized: CommandTemplates.CommandTemplateObjectConfig,
294
+ inherited: Pick<
295
+ CommandTemplates.CommandTemplateObjectConfig,
296
+ "args" | "defaults"
297
+ >,
298
+ params: Record<string, unknown>,
299
+ exec: RegisteredToolExec,
300
+ cwd: string,
301
+ signal: AbortSignal | undefined,
302
+ stdin: string | undefined,
303
+ isRoot: boolean,
304
+ ): Promise<TemplateExecution> {
305
+ const maxAttempts = normalizeRetry(normalized.retry, {
306
+ ...(inherited.defaults ?? {}),
307
+ ...params,
308
+ });
309
+ const attemptConfig = {
310
+ ...normalized,
311
+ delay: undefined,
312
+ recover: undefined,
313
+ retry: undefined,
314
+ };
315
+ const aggregate: TemplateExecution = {
316
+ branches: [],
317
+ commands: [],
318
+ failures: [],
319
+ result: { code: 1, killed: false, stderr: "", stdout: "" },
320
+ };
321
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
322
+ const executed = await executeTemplateConfig(
323
+ attemptConfig,
324
+ inherited,
325
+ params,
326
+ exec,
327
+ cwd,
328
+ signal,
329
+ stdin,
330
+ isRoot,
331
+ );
332
+ mergeExecution(aggregate, executed);
333
+ aggregate.result = executed.result;
334
+ aggregate.criticalFailure = executed.criticalFailure;
335
+ aggregate.failureScope = executed.failureScope;
336
+ if (executed.result.code === 0) return aggregate;
337
+ addResultFailure(aggregate.failures, executed);
338
+ if (attempt === maxAttempts) return aggregate;
339
+ if (normalized.recover === undefined) continue;
340
+ const recovered = await executeTemplateConfig(
341
+ getRecoverConfig(normalized.recover),
342
+ inherited,
343
+ params,
344
+ exec,
345
+ cwd,
346
+ signal,
347
+ executed.result.stdout,
348
+ false,
349
+ );
350
+ mergeExecution(aggregate, recovered);
351
+ if (recovered.result.code === 0) continue;
352
+ addResultFailure(aggregate.failures, recovered);
353
+ aggregate.result = recovered.result;
354
+ aggregate.criticalFailure = recovered.criticalFailure;
355
+ aggregate.failureScope = maxFailureScope(
356
+ recovered.failureScope,
357
+ getFailureScope(normalized),
358
+ );
359
+ return aggregate;
360
+ }
361
+ return aggregate;
362
+ }
363
+
364
+ async function executeTemplateConfig(
365
+ config: CommandTemplates.CommandTemplateConfig,
366
+ inherited: Pick<
367
+ CommandTemplates.CommandTemplateObjectConfig,
368
+ "args" | "defaults"
369
+ >,
370
+ params: Record<string, unknown>,
371
+ exec: RegisteredToolExec,
372
+ cwd: string,
373
+ signal: AbortSignal | undefined,
374
+ stdin: string | undefined,
375
+ isRoot: boolean,
376
+ ): Promise<TemplateExecution> {
377
+ const normalized = CommandTemplates.normalizeCommandTemplateConfig(config);
378
+ const normalizedDefaults = CommandTemplates.resolveInheritedDefaultReferences(
379
+ normalized.defaults,
380
+ inherited.defaults,
381
+ params,
382
+ );
383
+ const context = {
384
+ ...(inherited.args !== undefined ? { args: inherited.args } : {}),
385
+ ...(inherited.defaults !== undefined
386
+ ? { defaults: inherited.defaults }
387
+ : {}),
388
+ ...(normalized.args !== undefined ? { args: normalized.args } : {}),
389
+ ...(mergeDefaults(inherited.defaults, normalizedDefaults)
390
+ ? { defaults: mergeDefaults(inherited.defaults, normalizedDefaults) }
391
+ : {}),
392
+ };
393
+ const controlValues = { ...(context.defaults ?? {}), ...params };
394
+ await applyDelay(normalized.delay, controlValues, signal);
395
+ if (
396
+ !CommandTemplates.shouldRunCommandTemplateNode(normalized.when, controlValues)
397
+ ) {
398
+ return {
399
+ branches: [],
400
+ commands: [],
401
+ failures: [],
402
+ result: { code: 0, killed: false, stderr: "", stdout: stdin ?? "" },
403
+ };
404
+ }
405
+ getFailureScope(normalized);
406
+ if (normalized.repeat !== undefined) {
407
+ const repeat = CommandTemplates.resolveCommandTemplateRepeat(
408
+ normalized.repeat,
409
+ { ...(context.defaults ?? {}), ...params },
410
+ );
411
+ if (repeat === undefined)
412
+ throw new Error("Command template repeat could not be resolved.");
413
+ const repeatedSteps = Array.from({ length: repeat }, (_unused, index0) => {
414
+ const { repeat: _repeat, ...rest } = normalized;
415
+ return {
416
+ ...rest,
417
+ defaults: {
418
+ ...(context.defaults ?? {}),
419
+ ...(rest.defaults ?? {}),
420
+ ...CommandTemplates.getCommandTemplateRepeatDefaults(index0, repeat),
421
+ },
422
+ };
423
+ });
424
+ return executeTemplateConfig(
425
+ { parallel: normalized.parallel === true, template: repeatedSteps },
426
+ context,
427
+ params,
428
+ exec,
429
+ cwd,
430
+ signal,
431
+ stdin,
432
+ isRoot,
433
+ );
434
+ }
435
+ if (
436
+ normalized.retry !== undefined &&
437
+ (Array.isArray(normalized.template) || normalized.recover !== undefined)
438
+ ) {
439
+ return executeRetriableTemplateConfig(
440
+ normalized,
441
+ context,
442
+ params,
443
+ exec,
444
+ cwd,
445
+ signal,
446
+ stdin,
447
+ isRoot,
448
+ );
449
+ }
450
+ if (
451
+ normalized.template &&
452
+ typeof normalized.template === "object" &&
453
+ !Array.isArray(normalized.template)
454
+ ) {
455
+ return executeTemplateConfig(
456
+ normalized.template,
457
+ context,
458
+ params,
459
+ exec,
460
+ cwd,
461
+ signal,
462
+ stdin,
463
+ false,
464
+ );
465
+ }
466
+ if (!Array.isArray(normalized.template)) {
467
+ const leaf = { ...normalized, ...context };
468
+ const invocation = CommandTemplates.buildCommandTemplateInvocation(
469
+ leaf,
470
+ params as Record<string, string>,
471
+ cwd,
472
+ { emptyMessage: "Tool template produced an empty command." },
473
+ );
474
+ const result = await exec(invocation.command, invocation.args, {
475
+ cwd,
476
+ signal,
477
+ stdin,
478
+ ...(resolveNumericControlField(
479
+ normalized.timeout,
480
+ controlValues,
481
+ "timeout",
482
+ ) !== undefined
483
+ ? {
484
+ timeout: resolveNumericControlField(
485
+ normalized.timeout,
486
+ controlValues,
487
+ "timeout",
488
+ ),
489
+ }
490
+ : {}),
491
+ ...(normalized.retry !== undefined
492
+ ? { retry: normalizeRetry(normalized.retry, controlValues) }
493
+ : {}),
494
+ });
495
+ return {
496
+ branches: [],
497
+ commands: [invocation.command],
498
+ failures: [],
499
+ result,
500
+ };
501
+ }
502
+ const steps = normalized.template;
503
+ if (steps.length === 0)
504
+ throw new Error(formatToolText("Tool template produced no command steps."));
505
+ if (normalized.parallel === true) {
506
+ const branchResults = await Promise.all(
507
+ steps.map((step) =>
508
+ executeTemplateConfig(
509
+ step,
510
+ context,
511
+ params,
512
+ exec,
513
+ cwd,
514
+ signal,
515
+ stdin,
516
+ false,
517
+ ),
518
+ ),
519
+ );
520
+ const commands = branchResults.flatMap((item) => item.commands);
521
+ const failures = branchResults.flatMap((item) => item.failures);
522
+ const branches = branchResults.map((item, index) =>
523
+ createBranchReport(
524
+ getNodeLabel(steps[index], index),
525
+ item.commands.at(-1) ?? "<template>",
526
+ item.result,
527
+ ),
528
+ );
529
+ const nodeFailure = getFailureScope(normalized);
530
+ const rootFailure = branchResults.find((item, index) => {
531
+ if (item.result.code === 0) return false;
532
+ const branchFailure = maxFailureScope(
533
+ item.failureScope,
534
+ item.criticalFailure ? "root" : undefined,
535
+ getFailureScope(steps[index]),
536
+ nodeFailure,
537
+ );
538
+ return branchFailure === "root";
539
+ });
540
+ if (rootFailure) {
541
+ return {
542
+ branches: [
543
+ ...branchResults.flatMap((item) => item.branches),
544
+ ...branches,
545
+ ],
546
+ commands,
547
+ criticalFailure: true,
548
+ failureScope: "root",
549
+ failures,
550
+ result: rootFailure.result,
551
+ };
552
+ }
553
+ const firstFailedBranch = branchResults.find(
554
+ (item) => item.result.code !== 0,
555
+ );
556
+ const successful = branchResults.map((item) => {
557
+ if (item.result.code === 0) return item.result;
558
+ addResultFailure(failures, item);
559
+ return { ...item.result, code: 0, stdout: "" };
560
+ });
561
+ const result = {
562
+ code: 0,
563
+ killed: successful.some((item) => item.killed),
564
+ stderr: successful
565
+ .map((item) => item.stderr)
566
+ .filter(Boolean)
567
+ .join("\n"),
568
+ stdout: joinParallelStdout(branches, successful),
569
+ };
570
+ if (firstFailedBranch && nodeFailure === "branch") {
571
+ return {
572
+ commands,
573
+ branches: [
574
+ ...branchResults.flatMap((item) => item.branches),
575
+ ...branches,
576
+ ],
577
+ failureScope: "branch",
578
+ failures,
579
+ result: { ...result, code: firstFailedBranch.result.code || 1 },
580
+ };
581
+ }
582
+ return {
583
+ commands,
584
+ branches: [
585
+ ...branchResults.flatMap((item) => item.branches),
586
+ ...branches,
587
+ ],
588
+ failures,
589
+ result,
590
+ };
591
+ }
592
+ const branches: BranchReport[] = [];
593
+ const commands: string[] = [];
594
+ const failures: Array<{ code: number; command: string; killed: boolean }> =
595
+ [];
596
+ let nextStdin = stdin;
597
+ let result: ToolExecResult | undefined;
598
+ for (const step of steps) {
599
+ const executed = await executeTemplateConfig(
600
+ step,
601
+ context,
602
+ params,
603
+ exec,
604
+ cwd,
605
+ signal,
606
+ nextStdin,
607
+ false,
608
+ );
609
+ branches.push(...executed.branches);
610
+ commands.push(...executed.commands);
611
+ failures.push(...executed.failures);
612
+ result = executed.result;
613
+ if (result.code !== 0) {
614
+ const failureScope = maxFailureScope(
615
+ executed.failureScope,
616
+ executed.criticalFailure ? "root" : undefined,
617
+ getFailureScope(step),
618
+ getFailureScope(normalized),
619
+ isRoot && steps.length === 1 ? "root" : undefined,
620
+ );
621
+ if (failureScope === "root") {
622
+ return {
623
+ branches,
624
+ commands,
625
+ criticalFailure: true,
626
+ failureScope: "root",
627
+ failures,
628
+ result,
629
+ };
630
+ }
631
+ if (failureScope === "branch") {
632
+ addResultFailure(failures, executed);
633
+ return {
634
+ branches,
635
+ commands,
636
+ failureScope: "branch",
637
+ failures,
638
+ result,
639
+ };
640
+ }
641
+ addResultFailure(failures, executed);
642
+ result = { ...result, code: 0, stdout: "" };
643
+ nextStdin = "";
644
+ continue;
645
+ }
646
+ nextStdin = result.stdout;
647
+ }
648
+ return { branches, commands, failures, result: result! };
649
+ }
650
+
651
+ async function executeTemplateSteps(
652
+ cfg: RegisteredTool,
653
+ params: Record<string, unknown>,
654
+ exec: RegisteredToolExec,
655
+ cwd: string,
656
+ signal?: AbortSignal,
657
+ ): Promise<TemplateExecution> {
658
+ return executeTemplateConfig(
659
+ createTemplateConfig(cfg),
660
+ {},
661
+ params,
662
+ exec,
663
+ cwd,
664
+ signal,
665
+ undefined,
666
+ true,
667
+ );
668
+ }
669
+
670
+ export async function executeRegisteredTool(
671
+ cfg: RegisteredTool,
672
+ params: Record<string, unknown>,
673
+ exec: RegisteredToolExec,
674
+ cwd: string,
675
+ signal?: AbortSignal,
676
+ ): Promise<RegisteredToolExecutionResult> {
677
+ const executed = await executeTemplateSteps(
678
+ cfg,
679
+ Schema.normalizeRuntimeValues(params, cfg.argTypes),
680
+ exec,
681
+ cwd,
682
+ signal,
683
+ );
684
+ const command = formatCommandDetail(executed.commands);
685
+ const result = executed.result;
686
+ if (result.code !== 0) {
687
+ const formatted = formatFailureOutput(
688
+ cfg.name,
689
+ result.code,
690
+ result.killed,
691
+ result.stdout,
692
+ result.stderr,
693
+ );
694
+ throw new Error(formatted.text);
695
+ }
696
+ const formatted = formatOutput(cfg.name, "stdout", result.stdout);
697
+ const templateWarnings = CommandTemplates.getCommandTemplateWarnings(
698
+ createTemplateConfig(cfg),
699
+ );
700
+ return {
701
+ content: [textContent(formatted.text)],
702
+ details: {
703
+ code: result.code,
704
+ command,
705
+ fullOutputPath: formatted.fullOutputPath,
706
+ killed: result.killed,
707
+ ...(executed.branches.length > 0 ? { branches: executed.branches } : {}),
708
+ ...(executed.failures.length > 0
709
+ ? { nonCriticalFailures: executed.failures }
710
+ : {}),
711
+ ...(createSoftQuorum(executed.branches)
712
+ ? { softQuorum: createSoftQuorum(executed.branches) }
713
+ : {}),
714
+ template: cfg.template!,
715
+ ...(templateWarnings.length > 0 ? { templateWarnings } : {}),
716
+ tool: cfg.name,
717
+ truncated: formatted.truncated,
718
+ },
719
+ };
720
+ }