pi-subagents 0.25.0 → 0.27.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 (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +129 -17
  3. package/package.json +1 -1
  4. package/prompts/parallel-context-build.md +3 -1
  5. package/prompts/parallel-handoff-plan.md +3 -1
  6. package/skills/pi-subagents/SKILL.md +32 -17
  7. package/src/agents/agent-management.ts +57 -15
  8. package/src/agents/agent-serializer.ts +3 -2
  9. package/src/agents/agents.ts +47 -16
  10. package/src/agents/chain-serializer.ts +120 -0
  11. package/src/extension/fanout-child.ts +1 -0
  12. package/src/extension/index.ts +1 -0
  13. package/src/extension/schemas.ts +138 -5
  14. package/src/runs/background/async-execution.ts +84 -6
  15. package/src/runs/background/async-status.ts +11 -1
  16. package/src/runs/background/run-status.ts +10 -1
  17. package/src/runs/background/subagent-runner.ts +600 -31
  18. package/src/runs/foreground/chain-execution.ts +325 -118
  19. package/src/runs/foreground/execution.ts +222 -10
  20. package/src/runs/foreground/subagent-executor.ts +67 -0
  21. package/src/runs/shared/acceptance-contract.ts +291 -0
  22. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  23. package/src/runs/shared/acceptance-finalization.ts +161 -0
  24. package/src/runs/shared/acceptance-reports.ts +127 -0
  25. package/src/runs/shared/acceptance.ts +22 -0
  26. package/src/runs/shared/chain-outputs.ts +101 -0
  27. package/src/runs/shared/completion-guard.ts +26 -3
  28. package/src/runs/shared/dynamic-fanout.ts +293 -0
  29. package/src/runs/shared/parallel-utils.ts +31 -1
  30. package/src/runs/shared/pi-args.ts +11 -0
  31. package/src/runs/shared/structured-output.ts +77 -0
  32. package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
  33. package/src/runs/shared/workflow-graph.ts +206 -0
  34. package/src/shared/formatters.ts +2 -2
  35. package/src/shared/settings.ts +53 -4
  36. package/src/shared/types.ts +250 -0
  37. package/src/slash/slash-commands.ts +41 -3
  38. package/src/tui/render.ts +162 -34
@@ -12,7 +12,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
12
12
  import type { AgentConfig } from "../../agents/agents.ts";
13
13
  import { applyThinkingSuffix } from "../shared/pi-args.ts";
14
14
  import { injectSingleOutputInstruction, normalizeSingleOutputOverride, resolveSingleOutputPath, validateFileOnlyOutputMode } from "../shared/single-output.ts";
15
- import { buildChainInstructions, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
15
+ import { buildChainInstructions, isDynamicParallelStep, isParallelStep, resolveStepBehavior, suppressProgressForReadOnlyTask, writeInitialProgressFile, type ChainStep, type ResolvedStepBehavior, type SequentialStep, type StepOverrides } from "../../shared/settings.ts";
16
16
  import type { RunnerStep } from "../shared/parallel-utils.ts";
17
17
  import { resolvePiPackageRoot } from "../shared/pi-spawn.ts";
18
18
  import { buildSkillInjection, normalizeSkillInput, resolveSkillsWithFallback } from "../../agents/skills.ts";
@@ -20,7 +20,12 @@ import { resolveChildCwd } from "../../shared/utils.ts";
20
20
  import { buildModelCandidates, resolveModelCandidate, type AvailableModelInfo } from "../shared/model-fallback.ts";
21
21
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
22
22
  import { resolveExpectedWorktreeAgentCwd } from "../shared/worktree.ts";
23
+ import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
24
+ import { ChainOutputValidationError, validateChainOutputBindings } from "../shared/chain-outputs.ts";
25
+ import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
26
+ import { resolveEffectiveAcceptance } from "../shared/acceptance.ts";
23
27
  import {
28
+ type AcceptanceInput,
24
29
  type ArtifactConfig,
25
30
  type Details,
26
31
  type MaxOutputConfig,
@@ -107,6 +112,7 @@ interface AsyncChainParams {
107
112
  sessionRoot?: string;
108
113
  chainSkills?: string[];
109
114
  sessionFilesByFlatIndex?: (string | undefined)[];
115
+ dynamicFanoutMaxItems?: number;
110
116
  maxSubagentDepth: number;
111
117
  worktreeSetupHook?: string;
112
118
  worktreeSetupHookTimeoutMs?: number;
@@ -114,6 +120,7 @@ interface AsyncChainParams {
114
120
  controlIntercomTarget?: string;
115
121
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
116
122
  nestedRoute?: NestedRouteInfo;
123
+ acceptance?: AcceptanceInput;
117
124
  }
118
125
 
119
126
  interface AsyncSingleParams {
@@ -140,6 +147,7 @@ interface AsyncSingleParams {
140
147
  controlIntercomTarget?: string;
141
148
  childIntercomTarget?: (agent: string, index: number) => string | undefined;
142
149
  nestedRoute?: NestedRouteInfo;
150
+ acceptance?: AcceptanceInput;
143
151
  }
144
152
 
145
153
  interface AsyncExecutionResult {
@@ -248,12 +256,25 @@ export function executeAsyncChain(
248
256
  const runnerCwd = resolveChildCwd(ctx.cwd, cwd);
249
257
  const firstStep = chain[0];
250
258
  const originalTask = params.task ?? (firstStep
251
- ? (isParallelStep(firstStep) ? firstStep.parallel[0]?.task : (firstStep as SequentialStep).task)
259
+ ? (isParallelStep(firstStep)
260
+ ? firstStep.parallel[0]?.task
261
+ : isDynamicParallelStep(firstStep)
262
+ ? firstStep.parallel.task
263
+ : (firstStep as SequentialStep).task)
252
264
  : undefined);
265
+ try {
266
+ validateChainOutputBindings(chain, { maxItems: params.dynamicFanoutMaxItems });
267
+ } catch (error) {
268
+ if (error instanceof ChainOutputValidationError) return formatAsyncStartError(resultMode, error.message);
269
+ throw error;
270
+ }
271
+ const workflowGraph = buildWorkflowGraphSnapshot({ runId: id, mode: resultMode, steps: chain });
253
272
 
254
273
  for (const s of chain) {
255
274
  const stepAgents = isParallelStep(s)
256
275
  ? s.parallel.map((t) => t.agent)
276
+ : isDynamicParallelStep(s)
277
+ ? [s.parallel.agent]
257
278
  : [(s as SequentialStep).agent];
258
279
  for (const agentName of stepAgents) {
259
280
  if (!agents.find((x) => x.name === agentName)) {
@@ -316,13 +337,20 @@ export function executeAsyncChain(
316
337
  const outputPath = resolveSingleOutputPath(behavior.output, ctx.cwd, instructionCwd);
317
338
  const validationError = validateFileOnlyOutputMode(behavior.outputMode, outputPath, `Async step (${s.agent})`);
318
339
  if (validationError) throw new AsyncStartValidationError(validationError);
319
- const task = injectSingleOutputInstruction(`${readInstructions.prefix}${s.task ?? "{previous}"}${progressInstructions.suffix}`, outputPath);
340
+ let taskTemplate = s.task ?? "{previous}";
341
+ taskTemplate = taskTemplate.replace(/\{task\}/g, originalTask ?? "");
342
+ taskTemplate = taskTemplate.replace(/\{chain_dir\}/g, runnerCwd);
343
+ const task = injectSingleOutputInstruction(`${readInstructions.prefix}${taskTemplate}${progressInstructions.suffix}`, outputPath);
320
344
 
321
345
  const primaryModel = resolveModelCandidate(behavior.model ?? a.model, availableModels, ctx.currentModelProvider);
322
346
  const model = applyThinkingSuffix(primaryModel, a.thinking);
323
347
  return {
324
348
  agent: s.agent,
325
349
  task,
350
+ phase: s.phase,
351
+ label: s.label,
352
+ outputName: s.as,
353
+ structured: Boolean(s.outputSchema),
326
354
  cwd: stepCwd,
327
355
  model,
328
356
  thinking: resolveEffectiveThinking(model, a.thinking),
@@ -342,6 +370,16 @@ export function executeAsyncChain(
342
370
  outputMode: behavior.outputMode,
343
371
  sessionFile,
344
372
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, a.maxSubagentDepth),
373
+ effectiveAcceptance: resolveEffectiveAcceptance({
374
+ explicit: s.acceptance,
375
+ agentName: s.agent,
376
+ task: s.task,
377
+ mode: resultMode,
378
+ async: true,
379
+ dynamic: false,
380
+ }),
381
+ ...(s.outputSchema ? { structuredOutputSchema: s.outputSchema } : {}),
382
+ ...(s.outputSchema ? { structuredOutput: createStructuredOutputRuntime(s.outputSchema, path.join(asyncDir, "structured-output")) } : {}),
345
383
  };
346
384
  };
347
385
 
@@ -382,6 +420,24 @@ export function executeAsyncChain(
382
420
  worktree: s.worktree,
383
421
  };
384
422
  }
423
+ if (isDynamicParallelStep(s)) {
424
+ const agent = agents.find((candidate) => candidate.name === s.parallel.agent)!;
425
+ const behavior = suppressProgressForReadOnlyTask(resolveStepBehavior(agent, buildStepOverrides(s.parallel), chainSkills), s.parallel.task, originalTask);
426
+ const progressPrecreated = behavior.progress;
427
+ if (progressPrecreated) {
428
+ writeInitialProgressFile(runnerCwd);
429
+ progressInstructionCreated = true;
430
+ }
431
+ return {
432
+ expand: s.expand,
433
+ parallel: buildSeqStep(s.parallel as SequentialStep, undefined, undefined, progressPrecreated, behavior),
434
+ collect: s.collect,
435
+ concurrency: s.concurrency,
436
+ failFast: s.failFast,
437
+ phase: s.phase,
438
+ label: s.label,
439
+ };
440
+ }
385
441
  return buildSeqStep(s as SequentialStep, nextSessionFile());
386
442
  });
387
443
  } catch (error) {
@@ -391,6 +447,10 @@ export function executeAsyncChain(
391
447
  let childTargetIndex = 0;
392
448
  const childIntercomTargets = childIntercomTarget ? steps.flatMap((step) => {
393
449
  if ("parallel" in step) {
450
+ if (!Array.isArray(step.parallel)) {
451
+ childTargetIndex++;
452
+ return [undefined];
453
+ }
394
454
  return step.parallel.map((task) => childIntercomTarget(task.agent, childTargetIndex++));
395
455
  }
396
456
  return [childIntercomTarget(step.agent, childTargetIndex++)];
@@ -420,6 +480,8 @@ export function executeAsyncChain(
420
480
  controlIntercomTarget,
421
481
  childIntercomTargets,
422
482
  resultMode,
483
+ dynamicFanoutMaxItems: params.dynamicFanoutMaxItems,
484
+ workflowGraph,
423
485
  nestedRoute: nestedRoute ?? inheritedNestedRoute,
424
486
  nestedSelf: inheritedNestedRoute && nestedAddress ? {
425
487
  parentRunId: nestedAddress.parentRunId,
@@ -444,6 +506,8 @@ export function executeAsyncChain(
444
506
  const firstStep = chain[0];
445
507
  const firstAgents = isParallelStep(firstStep)
446
508
  ? firstStep.parallel.map((t) => t.agent)
509
+ : isDynamicParallelStep(firstStep)
510
+ ? [firstStep.parallel.agent]
447
511
  : [(firstStep as SequentialStep).agent];
448
512
  const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
449
513
  const flatAgents: string[] = [];
@@ -454,6 +518,10 @@ export function executeAsyncChain(
454
518
  parallelGroups.push({ start: flatStepStart, count: step.parallel.length, stepIndex });
455
519
  flatAgents.push(...step.parallel.map((task) => task.agent));
456
520
  flatStepStart += step.parallel.length;
521
+ } else if (isDynamicParallelStep(step)) {
522
+ parallelGroups.push({ start: flatStepStart, count: 1, stepIndex });
523
+ flatAgents.push(step.parallel.agent);
524
+ flatStepStart++;
457
525
  } else {
458
526
  flatAgents.push((step as SequentialStep).agent);
459
527
  flatStepStart++;
@@ -502,12 +570,15 @@ export function executeAsyncChain(
502
570
  agents: flatAgents,
503
571
  task: isParallelStep(firstStep)
504
572
  ? firstStep.parallel[0]?.task?.slice(0, 50)
573
+ : isDynamicParallelStep(firstStep)
574
+ ? firstStep.parallel.task?.slice(0, 50)
505
575
  : (firstStep as SequentialStep).task?.slice(0, 50),
506
576
  chain: chain.map((s) =>
507
- isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : (s as SequentialStep).agent,
577
+ isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
508
578
  ),
509
579
  chainStepCount: chain.length,
510
580
  parallelGroups,
581
+ workflowGraph,
511
582
  cwd: runnerCwd,
512
583
  asyncDir,
513
584
  nestedRoute,
@@ -516,13 +587,13 @@ export function executeAsyncChain(
516
587
 
517
588
  const chainDesc = chain
518
589
  .map((s) =>
519
- isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : (s as SequentialStep).agent,
590
+ isParallelStep(s) ? `[${s.parallel.map((t) => t.agent).join("+")}]` : isDynamicParallelStep(s) ? `expand:${s.parallel.agent}` : (s as SequentialStep).agent,
520
591
  )
521
592
  .join(" -> ");
522
593
 
523
594
  return {
524
595
  content: [{ type: "text", text: formatAsyncStartedMessage(`Async ${resultMode}: ${chainDesc} [${id}]`) }],
525
- details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir },
596
+ details: { mode: resultMode, runId: id, results: [], asyncId: id, asyncDir, workflowGraph },
526
597
  };
527
598
  }
528
599
 
@@ -618,6 +689,13 @@ export function executeAsyncSingle(
618
689
  outputMode,
619
690
  sessionFile,
620
691
  maxSubagentDepth: resolveChildMaxSubagentDepth(maxSubagentDepth, agentConfig.maxSubagentDepth),
692
+ effectiveAcceptance: resolveEffectiveAcceptance({
693
+ explicit: params.acceptance,
694
+ agentName: agent,
695
+ task,
696
+ mode: "single",
697
+ async: true,
698
+ }),
621
699
  },
622
700
  ],
623
701
  resultPath: inheritedNestedRoute ? nestedResultsPath(inheritedNestedRoute.rootRunId, id) : path.join(RESULTS_DIR, `${id}.json`),
@@ -12,6 +12,10 @@ import { reconcileAsyncRun, reconcileNestedAsyncDescendants } from "./stale-run-
12
12
  interface AsyncRunStepSummary {
13
13
  index: number;
14
14
  agent: string;
15
+ label?: string;
16
+ phase?: string;
17
+ outputName?: string;
18
+ structured?: boolean;
15
19
  status: AsyncJobStep["status"];
16
20
  activityState?: ActivityState;
17
21
  lastActivityAt?: number;
@@ -139,6 +143,10 @@ function statusToSummary(asyncDir: string, status: AsyncStatus & { cwd?: string
139
143
  return {
140
144
  index,
141
145
  agent: step.agent,
146
+ ...(step.label ? { label: step.label } : {}),
147
+ ...(step.phase ? { phase: step.phase } : {}),
148
+ ...(step.outputName ? { outputName: step.outputName } : {}),
149
+ ...(step.structured ? { structured: step.structured } : {}),
142
150
  status: step.status,
143
151
  ...(stepActivityState ? { activityState: stepActivityState } : {}),
144
152
  ...(stepLastActivityAt ? { lastActivityAt: stepLastActivityAt } : {}),
@@ -259,7 +267,9 @@ function formatActivityFacts(input: { activityState?: ActivityState; lastActivit
259
267
  }
260
268
 
261
269
  function formatStepLine(step: AsyncRunStepSummary): string {
262
- const parts = [`${step.index + 1}. ${step.agent}`, step.status];
270
+ const display = step.label ? `${step.label} (${step.agent})` : step.agent;
271
+ const phase = step.phase ? `[${step.phase}] ` : "";
272
+ const parts = [`${step.index + 1}. ${phase}${display}`, step.status];
263
273
  const activity = formatActivityFacts(step);
264
274
  if (activity) parts.push(activity);
265
275
  const modelThinking = formatModelThinking(step.model, step.thinking);
@@ -49,6 +49,11 @@ function formatResumeGuidance(runId: string | undefined, children: Array<{ agent
49
49
  return "Resume: unavailable; no child session file was persisted.";
50
50
  }
51
51
 
52
+ function formatAcceptanceFinalizationSummary(finalization: NonNullable<NonNullable<AsyncStatus["steps"]>[number]["acceptance"]>["finalization"] | undefined): string {
53
+ if (!finalization) return "";
54
+ return `, finalization: ${finalization.status} after ${finalization.turns.length}/${finalization.maxTurns} turns`;
55
+ }
56
+
52
57
  function stepLineLabel(status: AsyncStatus, index: number): string {
53
58
  const steps = status.steps ?? [];
54
59
  if (status.mode === "parallel") return `Agent ${index + 1}/${steps.length || 1}`;
@@ -217,7 +222,11 @@ export function inspectSubagentStatus(params: RunStatusParams, deps: RunStatusDe
217
222
  const modelThinking = formatModelThinking(step.model, step.thinking);
218
223
  const modelText = modelThinking ? ` (${modelThinking})` : "";
219
224
  const errorText = step.error ? `, error: ${step.error}` : "";
220
- lines.push(`${stepLineLabel(status, index)}: ${step.agent} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${errorText}`);
225
+ const finalizationText = formatAcceptanceFinalizationSummary(step.acceptance?.finalization);
226
+ const acceptanceText = step.acceptance?.status ? `, acceptance: ${step.acceptance.status}${finalizationText}` : "";
227
+ const display = step.label ? `${step.label} (${step.agent})` : step.agent;
228
+ const phase = step.phase ? `[${step.phase}] ` : "";
229
+ lines.push(`${stepLineLabel(status, index)}: ${phase}${display} ${step.status}${modelText}${stepActivityText ? `, ${stepActivityText}` : ""}${acceptanceText}${errorText}`);
221
230
  lines.push(...formatNestedRunStatusLines(step.children, { indent: " ", commandHints: true, maxLines: 20 }));
222
231
  const stepOutputPath = path.join(asyncDir, `output-${index}.log`);
223
232
  if (stepOutputPath !== outputPath && fs.existsSync(stepOutputPath)) lines.push(` Output: ${stepOutputPath}`);