pi-subagents 0.31.1 → 0.32.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 (39) hide show
  1. package/CHANGELOG.md +40 -4
  2. package/README.md +97 -7
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +6 -1
  5. package/src/agents/agent-management.ts +6 -1
  6. package/src/agents/agents.ts +55 -11
  7. package/src/extension/companion-suggestions.ts +359 -0
  8. package/src/extension/config.ts +27 -4
  9. package/src/extension/doctor.ts +2 -0
  10. package/src/extension/fanout-child.ts +1 -0
  11. package/src/extension/index.ts +58 -4
  12. package/src/extension/schemas.ts +2 -2
  13. package/src/runs/background/async-execution.ts +138 -33
  14. package/src/runs/background/async-job-tracker.ts +77 -1
  15. package/src/runs/background/async-status.ts +41 -9
  16. package/src/runs/background/chain-root-attachment.ts +34 -4
  17. package/src/runs/background/control-channel.ts +50 -0
  18. package/src/runs/background/run-status.ts +1 -0
  19. package/src/runs/background/stale-run-reconciler.ts +28 -1
  20. package/src/runs/background/subagent-runner.ts +454 -115
  21. package/src/runs/foreground/chain-execution.ts +29 -7
  22. package/src/runs/foreground/execution.ts +24 -6
  23. package/src/runs/foreground/subagent-executor.ts +209 -35
  24. package/src/runs/shared/acceptance.ts +45 -22
  25. package/src/runs/shared/dynamic-fanout.ts +1 -1
  26. package/src/runs/shared/model-fallback.ts +4 -0
  27. package/src/runs/shared/nested-events.ts +58 -0
  28. package/src/runs/shared/parallel-utils.ts +49 -1
  29. package/src/runs/shared/pi-args.ts +5 -3
  30. package/src/runs/shared/pi-spawn.ts +52 -20
  31. package/src/runs/shared/single-output.ts +2 -0
  32. package/src/runs/shared/subagent-prompt-runtime.ts +2 -2
  33. package/src/runs/shared/worktree.ts +28 -5
  34. package/src/shared/artifacts.ts +15 -1
  35. package/src/shared/fork-context.ts +133 -22
  36. package/src/shared/types.ts +81 -3
  37. package/src/shared/utils.ts +99 -14
  38. package/src/slash/slash-commands.ts +117 -0
  39. package/src/tui/render.ts +16 -4
@@ -34,7 +34,8 @@ import { discoverAvailableSkills, normalizeSkillInput } from "../../agents/skill
34
34
  import { INTERCOM_BRIDGE_MARKER } from "../../intercom/intercom-bridge.ts";
35
35
  import { runSync } from "./execution.ts";
36
36
  import { buildChainSummary } from "../../shared/formatters.ts";
37
- import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd } from "../../shared/utils.ts";
37
+ import { compactForegroundDetails, getSingleResultOutput, mapConcurrent, resolveChildCwd, sumResultsCost, sumResultsUsage } from "../../shared/utils.ts";
38
+ import { DEFAULT_GLOBAL_CONCURRENCY_LIMIT, Semaphore } from "../shared/parallel-utils.ts";
38
39
  import { recordRun } from "../shared/run-history.ts";
39
40
  import {
40
41
  cleanupWorktrees,
@@ -103,6 +104,7 @@ interface ParallelChainRunInput {
103
104
  sessionDirForIndex: (idx?: number) => string | undefined;
104
105
  sessionFileForIndex?: (idx?: number) => string | undefined;
105
106
  sessionFileForTask?: (agentName: string, idx?: number) => string | undefined;
107
+ thinkingOverrideForTask?: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
106
108
  shareEnabled: boolean;
107
109
  artifactConfig: ArtifactConfig;
108
110
  artifactsDir: string;
@@ -139,6 +141,7 @@ interface ParallelChainRunInput {
139
141
  nestedRoute?: NestedRouteInfo;
140
142
  timeoutMs?: number;
141
143
  deadlineAt?: number;
144
+ globalSemaphore?: Semaphore;
142
145
  }
143
146
 
144
147
  function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details {
@@ -151,7 +154,9 @@ function buildChainExecutionDetails(input: ChainExecutionDetailsInput): Details
151
154
  totalSteps: input.totalSteps,
152
155
  currentStepIndex: input.currentStepIndex,
153
156
  outputs: input.outputs,
154
- workflowGraph: buildWorkflowGraphSnapshot({
157
+ totalChildUsage: sumResultsUsage(input.results),
158
+ totalCost: sumResultsCost(input.results),
159
+ workflowGraph: buildWorkflowGraphSnapshot({
155
160
  runId: input.runId,
156
161
  mode: "chain",
157
162
  steps: input.chainSteps,
@@ -280,6 +285,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
280
285
  sessionDir: input.sessionDirForIndex(input.globalTaskIndex + taskIndex),
281
286
  sessionFile: input.sessionFileForTask?.(task.agent, input.globalTaskIndex + taskIndex)
282
287
  ?? input.sessionFileForIndex?.(input.globalTaskIndex + taskIndex),
288
+ thinkingOverride: input.thinkingOverrideForTask?.(task.agent, input.globalTaskIndex + taskIndex),
283
289
  share: input.shareEnabled,
284
290
  artifactsDir: input.artifactConfig.enabled ? input.artifactsDir : undefined,
285
291
  artifactConfig: input.artifactConfig,
@@ -355,6 +361,7 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
355
361
  recordRun(task.agent, cleanTask, result.exitCode, result.progressSummary?.durationMs ?? 0);
356
362
  return result;
357
363
  },
364
+ input.globalSemaphore,
358
365
  );
359
366
 
360
367
  return parallelResults;
@@ -373,6 +380,7 @@ interface ChainExecutionParams {
373
380
  sessionDirForIndex: (idx?: number) => string | undefined;
374
381
  sessionFileForIndex?: (idx?: number) => string | undefined;
375
382
  sessionFileForTask?: (agentName: string, idx?: number) => string | undefined;
383
+ thinkingOverrideForTask?: (agentName: string, idx?: number) => AgentConfig["thinking"] | undefined;
376
384
  artifactsDir: string;
377
385
  artifactConfig: ArtifactConfig;
378
386
  includeProgress?: boolean;
@@ -403,8 +411,11 @@ interface ChainExecutionParams {
403
411
  nestedRoute?: NestedRouteInfo;
404
412
  worktreeSetupHook?: string;
405
413
  worktreeSetupHookTimeoutMs?: number;
414
+ worktreeBaseDir?: string;
406
415
  timeoutMs?: number;
407
416
  deadlineAt?: number;
417
+ /** Global cap on simultaneously-running tasks within this chain. Defaults to DEFAULT_GLOBAL_CONCURRENCY_LIMIT. */
418
+ globalConcurrencyLimit?: number;
408
419
  }
409
420
 
410
421
  interface ChainExecutionResult {
@@ -433,6 +444,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
433
444
  sessionDirForIndex,
434
445
  sessionFileForIndex,
435
446
  sessionFileForTask,
447
+ thinkingOverrideForTask,
436
448
  artifactsDir,
437
449
  artifactConfig,
438
450
  includeProgress,
@@ -596,6 +608,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
596
608
  }
597
609
 
598
610
  const deadlineAt = params.deadlineAt ?? (params.timeoutMs !== undefined ? Date.now() + params.timeoutMs : undefined);
611
+ const globalSemaphore = new Semaphore(params.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
599
612
  let prev = "";
600
613
  let globalTaskIndex = 0;
601
614
  let progressCreated = false;
@@ -622,6 +635,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
622
635
  setupHook: params.worktreeSetupHook
623
636
  ? { hookPath: params.worktreeSetupHook, timeoutMs: params.worktreeSetupHookTimeoutMs }
624
637
  : undefined,
638
+ baseDir: params.worktreeBaseDir,
625
639
  });
626
640
  } catch (error) {
627
641
  const message = error instanceof Error ? error.message : String(error);
@@ -662,6 +676,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
662
676
  sessionDirForIndex,
663
677
  sessionFileForIndex,
664
678
  sessionFileForTask,
679
+ thinkingOverrideForTask,
665
680
  shareEnabled,
666
681
  artifactConfig,
667
682
  artifactsDir,
@@ -685,6 +700,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
685
700
  maxSubagentDepth: params.maxSubagentDepth,
686
701
  timeoutMs: params.timeoutMs,
687
702
  deadlineAt,
703
+ globalSemaphore,
688
704
  });
689
705
  globalTaskIndex += step.parallel.length;
690
706
 
@@ -770,6 +786,8 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
770
786
  if (worktreeSetup) cleanupWorktrees(worktreeSetup);
771
787
  }
772
788
  } else if (isDynamicParallelStep(step)) {
789
+ const dynamicStartIndex = globalTaskIndex;
790
+ const reservedDynamicItems = step.expand.maxItems ?? params.dynamicFanoutMaxItems ?? 0;
773
791
  let materialized: ReturnType<typeof materializeDynamicParallelStep>;
774
792
  try {
775
793
  materialized = materializeDynamicParallelStep(step, outputs, stepIndex, { maxItems: params.dynamicFanoutMaxItems });
@@ -828,6 +846,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
828
846
  }
829
847
  }
830
848
  prev = "Dynamic fanout produced 0 results.";
849
+ globalTaskIndex = dynamicStartIndex + reservedDynamicItems;
831
850
  continue;
832
851
  }
833
852
 
@@ -872,6 +891,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
872
891
  sessionDirForIndex,
873
892
  sessionFileForIndex,
874
893
  sessionFileForTask,
894
+ thinkingOverrideForTask,
875
895
  shareEnabled,
876
896
  artifactConfig,
877
897
  artifactsDir,
@@ -894,8 +914,9 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
894
914
  maxSubagentDepth: params.maxSubagentDepth,
895
915
  timeoutMs: params.timeoutMs,
896
916
  deadlineAt,
917
+ globalSemaphore,
897
918
  });
898
- globalTaskIndex += dynamicParallelStep.parallel.length;
919
+ globalTaskIndex = dynamicStartIndex + reservedDynamicItems;
899
920
 
900
921
  for (const result of parallelResults) {
901
922
  results.push(result);
@@ -910,7 +931,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
910
931
  content: [{ type: "text", text: `Chain paused after interrupt at step ${stepIndex + 1} (${interrupted.agent}). Waiting for explicit next action.` }],
911
932
  details: buildChainExecutionDetails(makeDetailsInput({
912
933
  currentStepIndex: stepIndex,
913
- currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + interruptedIndexInStep,
934
+ currentFlatIndex: dynamicStartIndex + interruptedIndexInStep,
914
935
  })),
915
936
  };
916
937
  }
@@ -921,7 +942,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
921
942
  content: [{ type: "text", text: `Chain detached for intercom coordination at step ${stepIndex + 1} (${detached.agent}). Reply to the supervisor request first. After the child exits, start a fresh follow-up if needed.` }],
922
943
  details: buildChainExecutionDetails(makeDetailsInput({
923
944
  currentStepIndex: stepIndex,
924
- currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + detachedIndexInStep,
945
+ currentFlatIndex: dynamicStartIndex + detachedIndexInStep,
925
946
  })),
926
947
  };
927
948
  }
@@ -943,7 +964,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
943
964
  isError: true,
944
965
  details: buildChainExecutionDetails(makeDetailsInput({
945
966
  currentStepIndex: stepIndex,
946
- currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length + failures[0]!.originalIndex,
967
+ currentFlatIndex: dynamicStartIndex + failures[0]!.originalIndex,
947
968
  })),
948
969
  };
949
970
  }
@@ -952,7 +973,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
952
973
  } catch (error) {
953
974
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
954
975
  dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
955
- return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }));
976
+ return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: dynamicStartIndex }));
956
977
  }
957
978
  outputs[step.collect.as] = {
958
979
  text: JSON.stringify(collected),
@@ -1085,6 +1106,7 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
1085
1106
  sessionDir: sessionDirForIndex(globalTaskIndex),
1086
1107
  sessionFile: sessionFileForTask?.(seqStep.agent, globalTaskIndex)
1087
1108
  ?? sessionFileForIndex?.(globalTaskIndex),
1109
+ thinkingOverride: thinkingOverrideForTask?.(seqStep.agent, globalTaskIndex),
1088
1110
  share: shareEnabled,
1089
1111
  artifactsDir: artifactConfig.enabled ? artifactsDir : undefined,
1090
1112
  artifactConfig,
@@ -180,15 +180,16 @@ async function runSingleAttempt(
180
180
  originalTask?: string;
181
181
  },
182
182
  ): Promise<SingleResult> {
183
- const modelArg = applyThinkingSuffix(model, agent.thinking);
184
- const { args, env: sharedEnv, tempDir } = buildPiArgs({
183
+ const effectiveThinking = options.thinkingOverride ?? agent.thinking;
184
+ const modelArg = applyThinkingSuffix(model, effectiveThinking, options.thinkingOverride !== undefined);
185
+ const { args, env: sharedEnv, tempDir } = buildPiArgs({
185
186
  baseArgs: ["--mode", "json", "-p"],
186
187
  task,
187
188
  sessionEnabled: shared.sessionEnabled,
188
189
  sessionDir: options.sessionDir,
189
190
  sessionFile: options.sessionFile,
190
- model,
191
- thinking: agent.thinking,
191
+ model: modelArg,
192
+ thinking: effectiveThinking,
192
193
  systemPromptMode: agent.systemPromptMode,
193
194
  inheritProjectContext: agent.inheritProjectContext,
194
195
  inheritSkills: agent.inheritSkills,
@@ -518,8 +519,11 @@ async function runSingleAttempt(
518
519
  const toolArgs = evt.args && typeof evt.args === "object" && !Array.isArray(evt.args)
519
520
  ? evt.args as Record<string, unknown>
520
521
  : {};
522
+ let shouldDetachForBlockingIntercom = false;
521
523
  if (options.allowIntercomDetach && (evt.toolName === "intercom" || evt.toolName === "contact_supervisor")) {
522
524
  intercomStarted = true;
525
+ shouldDetachForBlockingIntercom = (evt.toolName === "intercom" && toolArgs.action === "ask")
526
+ || (evt.toolName === "contact_supervisor" && (toolArgs.reason === "need_decision" || toolArgs.reason === "interview_request"));
523
527
  }
524
528
  progress.toolCount++;
525
529
  progress.currentTool = evt.toolName;
@@ -530,6 +534,9 @@ async function runSingleAttempt(
530
534
  observedMutationAttempt = observedMutationAttempt || mutates;
531
535
  pendingToolResult = { tool: evt.toolName ?? "tool", path: progress.currentPath, mutates, startedAt: now };
532
536
  fireUpdate();
537
+ if (shouldDetachForBlockingIntercom && !detached && !processClosed) {
538
+ detachForIntercom();
539
+ }
533
540
  }
534
541
 
535
542
  if (evt.type === "tool_execution_end") {
@@ -772,6 +779,16 @@ async function runSingleAttempt(
772
779
  : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
773
780
  }
774
781
  }
782
+ if (result.exitCode === 0 && !result.error) {
783
+ const finalText = getFinalOutput(result.messages);
784
+ const missingStructuredOutput = options.structuredOutput
785
+ ? !existsSync(options.structuredOutput.outputPath)
786
+ : false;
787
+ if (!finalText?.trim() && (!options.structuredOutput || missingStructuredOutput)) {
788
+ result.exitCode = 1;
789
+ result.error = "Subagent produced no output (possible model cold-start or empty response).";
790
+ }
791
+ }
775
792
  if (options.structuredOutput && result.exitCode === 0 && !result.error) {
776
793
  const structured = readStructuredOutput({
777
794
  schema: options.structuredOutput.schema,
@@ -966,7 +983,6 @@ export async function runSync(
966
983
  const modelsToTry = candidates.length > 0 ? candidates : [undefined];
967
984
  for (let i = 0; i < modelsToTry.length; i++) {
968
985
  const candidate = modelsToTry[i];
969
- if (candidate) attemptedModels.push(candidate);
970
986
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
971
987
  const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, options, {
972
988
  sessionEnabled,
@@ -980,12 +996,14 @@ export async function runSync(
980
996
  originalTask: task,
981
997
  });
982
998
  lastResult = result;
999
+ if (result.model) attemptedModels.push(result.model);
1000
+ else if (candidate) attemptedModels.push(candidate);
983
1001
  sumUsage(aggregateUsage, result.usage);
984
1002
  totalToolCount += result.progressSummary?.toolCount ?? 0;
985
1003
  totalDurationMs += result.progressSummary?.durationMs ?? 0;
986
1004
  const attemptSucceeded = result.exitCode === 0 && !result.error;
987
1005
  const attempt: ModelAttempt = {
988
- model: candidate ?? result.model ?? agent.model ?? "default",
1006
+ model: result.model ?? candidate ?? agent.model ?? "default",
989
1007
  success: attemptSucceeded,
990
1008
  exitCode: result.exitCode,
991
1009
  error: result.error,