pi-subagents 0.24.4 → 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 (48) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +145 -27
  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/prompts/review-loop.md +1 -1
  7. package/skills/pi-subagents/SKILL.md +71 -20
  8. package/src/agents/agent-management.ts +57 -15
  9. package/src/agents/agent-serializer.ts +3 -2
  10. package/src/agents/agents.ts +47 -16
  11. package/src/agents/chain-serializer.ts +120 -0
  12. package/src/extension/fanout-child.ts +171 -0
  13. package/src/extension/index.ts +7 -2
  14. package/src/extension/schemas.ts +138 -5
  15. package/src/intercom/result-intercom.ts +108 -0
  16. package/src/runs/background/async-execution.ts +185 -10
  17. package/src/runs/background/async-job-tracker.ts +41 -6
  18. package/src/runs/background/async-resume.ts +28 -15
  19. package/src/runs/background/async-status.ts +71 -31
  20. package/src/runs/background/result-watcher.ts +111 -54
  21. package/src/runs/background/run-id-resolver.ts +83 -0
  22. package/src/runs/background/run-status.ts +89 -4
  23. package/src/runs/background/stale-run-reconciler.ts +46 -1
  24. package/src/runs/background/subagent-runner.ts +648 -42
  25. package/src/runs/foreground/chain-execution.ts +331 -118
  26. package/src/runs/foreground/execution.ts +226 -10
  27. package/src/runs/foreground/subagent-executor.ts +377 -14
  28. package/src/runs/shared/acceptance-contract.ts +291 -0
  29. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  30. package/src/runs/shared/acceptance-finalization.ts +161 -0
  31. package/src/runs/shared/acceptance-reports.ts +127 -0
  32. package/src/runs/shared/acceptance.ts +22 -0
  33. package/src/runs/shared/chain-outputs.ts +101 -0
  34. package/src/runs/shared/completion-guard.ts +26 -3
  35. package/src/runs/shared/dynamic-fanout.ts +293 -0
  36. package/src/runs/shared/nested-events.ts +819 -0
  37. package/src/runs/shared/nested-path.ts +52 -0
  38. package/src/runs/shared/nested-render.ts +115 -0
  39. package/src/runs/shared/parallel-utils.ts +31 -1
  40. package/src/runs/shared/pi-args.ts +73 -5
  41. package/src/runs/shared/structured-output.ts +77 -0
  42. package/src/runs/shared/subagent-prompt-runtime.ts +77 -7
  43. package/src/runs/shared/workflow-graph.ts +206 -0
  44. package/src/shared/formatters.ts +2 -2
  45. package/src/shared/settings.ts +53 -4
  46. package/src/shared/types.ts +345 -0
  47. package/src/slash/slash-commands.ts +41 -3
  48. package/src/tui/render.ts +268 -43
@@ -3,7 +3,9 @@
3
3
  */
4
4
 
5
5
  import { spawn } from "node:child_process";
6
- import { existsSync } from "node:fs";
6
+ import { existsSync, mkdtempSync, unlinkSync } from "node:fs";
7
+ import * as os from "node:os";
8
+ import * as path from "node:path";
7
9
  import type { Message } from "@earendil-works/pi-ai";
8
10
  import type { AgentConfig } from "../../agents/agents.ts";
9
11
  import {
@@ -13,10 +15,13 @@ import {
13
15
  writeMetadata,
14
16
  } from "../../shared/artifacts.ts";
15
17
  import {
18
+ type AcceptanceFinalizationTurn,
19
+ type AcceptanceLedger,
16
20
  type AgentProgress,
17
21
  type ArtifactPaths,
18
22
  type ControlEvent,
19
23
  type ModelAttempt,
24
+ type ResolvedAcceptanceConfig,
20
25
  type RunSyncOptions,
21
26
  type SingleResult,
22
27
  type Usage,
@@ -41,11 +46,12 @@ import {
41
46
  extractTextFromContent,
42
47
  } from "../../shared/utils.ts";
43
48
  import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
44
- import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
49
+ import { evaluateCompletionMutationGuard, resolveCompletionPolicy, type CompletionPolicy } from "../shared/completion-guard.ts";
45
50
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
46
51
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
47
52
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
48
53
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
54
+ import { readStructuredOutput } from "../shared/structured-output.ts";
49
55
  import { captureSingleOutputSnapshot, formatSavedOutputReference, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
50
56
  import {
51
57
  buildModelCandidates,
@@ -63,8 +69,23 @@ import {
63
69
  shouldEscalateMutatingFailures,
64
70
  summarizeRecentMutatingFailures,
65
71
  } from "../shared/long-running-guard.ts";
72
+ import {
73
+ acceptanceFailureMessage,
74
+ acceptanceSelfReviewConfig,
75
+ attachFinalizationToLedger,
76
+ buildFinalizationProcessFailureLedger,
77
+ createFinalizationProcessFailureTurn,
78
+ createFinalizationTurn,
79
+ evaluateAcceptance,
80
+ formatAcceptanceFinalizationPrompt,
81
+ formatAcceptancePrompt,
82
+ resolveEffectiveAcceptance,
83
+ shouldRunAcceptanceFinalization,
84
+ stripAcceptanceReport,
85
+ } from "../shared/acceptance.ts";
66
86
 
67
87
  const artifactOutputByResult = new WeakMap<SingleResult, string>();
88
+ const acceptanceOutputByResult = new WeakMap<SingleResult, string>();
68
89
 
69
90
  function emptyUsage(): Usage {
70
91
  return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
@@ -87,6 +108,17 @@ function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
87
108
  }
88
109
  }
89
110
 
111
+ function stripAcceptanceReportsFromMessages(messages: Message[] | undefined): void {
112
+ for (const message of messages ?? []) {
113
+ if (message.role !== "assistant" || !Array.isArray(message.content)) continue;
114
+ for (const part of message.content) {
115
+ if (part.type === "text" && "text" in part && typeof part.text === "string") {
116
+ part.text = stripAcceptanceReport(part.text);
117
+ }
118
+ }
119
+ }
120
+ }
121
+
90
122
  function snapshotProgress(progress: AgentProgress): AgentProgress {
91
123
  return {
92
124
  ...progress,
@@ -133,6 +165,8 @@ async function runSingleAttempt(
133
165
  artifactPaths?: ArtifactPaths;
134
166
  attemptNotes: string[];
135
167
  outputSnapshot?: SingleOutputSnapshot;
168
+ originalTask?: string;
169
+ completionPolicy: CompletionPolicy;
136
170
  },
137
171
  ): Promise<SingleResult> {
138
172
  const modelArg = applyThinkingSuffix(model, agent.thinking);
@@ -158,11 +192,16 @@ async function runSingleAttempt(
158
192
  runId: options.runId,
159
193
  childAgentName: agent.name,
160
194
  childIndex: options.index ?? 0,
195
+ parentEventSink: options.nestedRoute?.eventSink,
196
+ parentControlInbox: options.nestedRoute?.controlInbox,
197
+ parentRootRunId: options.nestedRoute?.rootRunId,
198
+ parentCapabilityToken: options.nestedRoute?.capabilityToken,
199
+ structuredOutput: options.structuredOutput,
161
200
  });
162
201
 
163
202
  const result: SingleResult = {
164
203
  agent: agent.name,
165
- task,
204
+ task: shared.originalTask ?? task,
166
205
  exitCode: 0,
167
206
  messages: [],
168
207
  usage: emptyUsage(),
@@ -172,6 +211,13 @@ async function runSingleAttempt(
172
211
  skillsWarning: shared.skillsWarning,
173
212
  };
174
213
  const startTime = Date.now();
214
+ if (options.structuredOutput) {
215
+ try {
216
+ if (existsSync(options.structuredOutput.outputPath)) unlinkSync(options.structuredOutput.outputPath);
217
+ } catch {
218
+ // Missing/stale structured-output files are handled after the child exits.
219
+ }
220
+ }
175
221
  const controlConfig = options.controlConfig ?? DEFAULT_CONTROL_CONFIG;
176
222
  let interruptedByControl = false;
177
223
  const allControlEvents: ControlEvent[] = [];
@@ -651,6 +697,21 @@ async function runSingleAttempt(
651
697
  : `${errInfo.errorType} failed with exit code ${errInfo.exitCode}`;
652
698
  }
653
699
  }
700
+ if (options.structuredOutput && result.exitCode === 0 && !result.error) {
701
+ const structured = readStructuredOutput({
702
+ schema: options.structuredOutput.schema,
703
+ schemaPath: options.structuredOutput.schemaPath,
704
+ outputPath: options.structuredOutput.outputPath,
705
+ });
706
+ result.structuredOutputSchemaPath = options.structuredOutput.schemaPath;
707
+ result.structuredOutputPath = options.structuredOutput.outputPath;
708
+ if (structured.error) {
709
+ result.exitCode = 1;
710
+ result.error = structured.error;
711
+ } else {
712
+ result.structuredOutput = structured.value;
713
+ }
714
+ }
654
715
 
655
716
  progress.status = result.exitCode === 0 ? "completed" : "failed";
656
717
  progress.durationMs = Date.now() - startTime;
@@ -667,17 +728,19 @@ async function runSingleAttempt(
667
728
  durationMs: progress.durationMs,
668
729
  };
669
730
 
670
- let fullOutput = getFinalOutput(result.messages);
671
- const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
731
+ const acceptanceOutput = getFinalOutput(result.messages);
732
+ let fullOutput = stripAcceptanceReport(acceptanceOutput);
733
+ const completionGuard = result.exitCode === 0 && !result.error && shared.completionPolicy === "mutation-guard"
672
734
  ? evaluateCompletionMutationGuard({
673
735
  agent: agent.name,
674
- task,
736
+ task: shared.originalTask ?? task,
675
737
  messages: result.messages,
676
738
  tools: agent.tools,
677
739
  mcpDirectTools: agent.mcpDirectTools,
678
740
  })
679
741
  : undefined;
680
- if (completionGuard?.triggered && !observedMutationAttempt) {
742
+ const completionGuardTriggered = completionGuard?.triggered === true && !observedMutationAttempt;
743
+ if (completionGuardTriggered) {
681
744
  result.exitCode = 1;
682
745
  result.error = "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes.";
683
746
  progress.status = "failed";
@@ -695,7 +758,7 @@ async function runSingleAttempt(
695
758
  }
696
759
  if (options.outputPath && result.exitCode === 0) {
697
760
  const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
698
- fullOutput = resolvedOutput.fullOutput;
761
+ fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
699
762
  result.savedOutputPath = resolvedOutput.savedPath;
700
763
  result.outputSaveError = resolvedOutput.saveError;
701
764
  if (resolvedOutput.savedPath) {
@@ -703,6 +766,7 @@ async function runSingleAttempt(
703
766
  }
704
767
  }
705
768
  artifactOutputByResult.set(result, fullOutput);
769
+ acceptanceOutputByResult.set(result, acceptanceOutput);
706
770
  result.outputMode = options.outputMode ?? "inline";
707
771
  result.finalOutput = options.outputMode === "file-only" && result.savedOutputPath && result.outputReference
708
772
  ? result.outputReference.message
@@ -725,6 +789,99 @@ async function runSingleAttempt(
725
789
  return result;
726
790
  }
727
791
 
792
+ async function runAcceptanceFinalizationLoop(input: {
793
+ runtimeCwd: string;
794
+ agent: AgentConfig;
795
+ result: SingleResult;
796
+ initialLedger: AcceptanceLedger;
797
+ initialOutput: string;
798
+ acceptance: ResolvedAcceptanceConfig;
799
+ options: RunSyncOptions;
800
+ systemPrompt: string;
801
+ resolvedSkillNames?: string[];
802
+ skillsWarning?: string;
803
+ }): Promise<AcceptanceLedger> {
804
+ const sessionFile = input.result.sessionFile ?? input.options.sessionFile;
805
+ const maxTurns = input.acceptance.finalization.maxTurns;
806
+ const turns: AcceptanceFinalizationTurn[] = [];
807
+ if (!sessionFile) {
808
+ const message = "Acceptance finalization requires a session file for same-session continuation.";
809
+ turns.push(createFinalizationProcessFailureTurn({ turn: 1, prompt: "", message }));
810
+ return buildFinalizationProcessFailureLedger({ initialLedger: input.initialLedger, turns, maxTurns, message });
811
+ }
812
+
813
+ const selfReviewAcceptance = acceptanceSelfReviewConfig(input.acceptance);
814
+ let previousFailure = acceptanceFailureMessage(input.initialLedger);
815
+ let authoritativeLedger = input.initialLedger;
816
+ for (let turn = 1; turn <= maxTurns; turn++) {
817
+ const prompt = formatAcceptanceFinalizationPrompt({
818
+ acceptance: input.acceptance,
819
+ initialOutput: input.initialOutput,
820
+ initialLedger: input.initialLedger,
821
+ turn,
822
+ maxTurns,
823
+ ...(previousFailure ? { previousFailure } : {}),
824
+ });
825
+ const finalizationOptions: RunSyncOptions = { ...input.options, sessionFile, outputMode: "inline" };
826
+ delete finalizationOptions.sessionDir;
827
+ delete finalizationOptions.outputPath;
828
+ delete finalizationOptions.structuredOutput;
829
+ delete finalizationOptions.onUpdate;
830
+ finalizationOptions.allowIntercomDetach = false;
831
+ const finalizationResult = await runSingleAttempt(
832
+ input.runtimeCwd,
833
+ input.agent,
834
+ prompt,
835
+ input.result.model,
836
+ finalizationOptions,
837
+ {
838
+ sessionEnabled: true,
839
+ systemPrompt: input.systemPrompt,
840
+ resolvedSkillNames: input.resolvedSkillNames,
841
+ skillsWarning: input.skillsWarning,
842
+ attemptNotes: [],
843
+ originalTask: prompt,
844
+ completionPolicy: "acceptance-contract",
845
+ },
846
+ );
847
+ sumUsage(input.result.usage, finalizationResult.usage);
848
+ input.result.progressSummary = {
849
+ toolCount: (input.result.progressSummary?.toolCount ?? 0) + (finalizationResult.progressSummary?.toolCount ?? 0),
850
+ tokens: input.result.usage.input + input.result.usage.output,
851
+ durationMs: (input.result.progressSummary?.durationMs ?? 0) + (finalizationResult.progressSummary?.durationMs ?? 0),
852
+ };
853
+ if (finalizationResult.controlEvents?.length) {
854
+ input.result.controlEvents = [...(input.result.controlEvents ?? []), ...finalizationResult.controlEvents];
855
+ }
856
+ const rawOutput = acceptanceOutputByResult.get(finalizationResult) ?? getFinalOutput(finalizationResult.messages) ?? finalizationResult.finalOutput ?? "";
857
+ if (finalizationResult.exitCode !== 0 || finalizationResult.error || finalizationResult.detached || finalizationResult.interrupted) {
858
+ const message = finalizationResult.error ?? "Acceptance finalization turn did not complete successfully.";
859
+ turns.push(createFinalizationProcessFailureTurn({ turn, prompt, rawOutput, message }));
860
+ return buildFinalizationProcessFailureLedger({ initialLedger: input.initialLedger, turns, maxTurns, message });
861
+ }
862
+ const selfReviewLedger = await evaluateAcceptance({
863
+ acceptance: selfReviewAcceptance,
864
+ output: rawOutput,
865
+ cwd: input.options.cwd ?? input.runtimeCwd,
866
+ });
867
+ authoritativeLedger = selfReviewLedger;
868
+ turns.push(createFinalizationTurn({ turn, prompt, rawOutput, ledger: selfReviewLedger }));
869
+ const failure = acceptanceFailureMessage(selfReviewLedger);
870
+ if (!failure) {
871
+ authoritativeLedger = input.acceptance === selfReviewAcceptance
872
+ ? selfReviewLedger
873
+ : await evaluateAcceptance({
874
+ acceptance: input.acceptance,
875
+ output: rawOutput,
876
+ cwd: input.options.cwd ?? input.runtimeCwd,
877
+ });
878
+ return attachFinalizationToLedger({ initialLedger: input.initialLedger, authoritativeLedger, turns, status: "completed", maxTurns });
879
+ }
880
+ previousFailure = failure;
881
+ }
882
+ return attachFinalizationToLedger({ initialLedger: input.initialLedger, authoritativeLedger, turns, status: "failed", maxTurns });
883
+ }
884
+
728
885
  /**
729
886
  * Run a subagent synchronously (blocking until complete)
730
887
  */
@@ -760,6 +917,21 @@ export async function runSync(
760
917
  }
761
918
 
762
919
  const shareEnabled = options.share === true;
920
+ const effectiveAcceptance = resolveEffectiveAcceptance({
921
+ explicit: options.acceptance,
922
+ agentName,
923
+ task,
924
+ mode: options.acceptanceContext?.mode ?? "single",
925
+ async: options.acceptanceContext?.async,
926
+ dynamic: options.acceptanceContext?.dynamic,
927
+ dynamicGroup: options.acceptanceContext?.dynamicGroup,
928
+ });
929
+ if (shouldRunAcceptanceFinalization(effectiveAcceptance) && !options.sessionFile) {
930
+ const sessionDir = options.sessionDir ?? mkdtempSync(path.join(os.tmpdir(), "pi-subagent-finalization-"));
931
+ options.sessionFile = path.join(sessionDir, "session.jsonl");
932
+ }
933
+ const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance);
934
+ const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
763
935
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
764
936
  const skillNames = options.skills ?? agent.skills ?? [];
765
937
  const skillCwd = options.cwd ?? runtimeCwd;
@@ -799,7 +971,7 @@ export async function runSync(
799
971
  artifactPathsResult = getArtifactPaths(options.artifactsDir, options.runId, agentName, options.index);
800
972
  ensureArtifactsDir(options.artifactsDir);
801
973
  if (options.artifactConfig?.includeInput !== false) {
802
- writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${task}`);
974
+ writeArtifact(artifactPathsResult.inputPath, `# Task for ${agentName}\n\n${taskWithAcceptance}`);
803
975
  }
804
976
  if (options.artifactConfig?.includeJsonl !== false) {
805
977
  jsonlPath = artifactPathsResult.jsonlPath;
@@ -812,7 +984,7 @@ export async function runSync(
812
984
  const candidate = modelsToTry[i];
813
985
  if (candidate) attemptedModels.push(candidate);
814
986
  const outputSnapshot = captureSingleOutputSnapshot(options.outputPath);
815
- const result = await runSingleAttempt(runtimeCwd, agent, task, candidate, options, {
987
+ const result = await runSingleAttempt(runtimeCwd, agent, taskWithAcceptance, candidate, options, {
816
988
  sessionEnabled,
817
989
  systemPrompt,
818
990
  resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
@@ -821,6 +993,15 @@ export async function runSync(
821
993
  artifactPaths: artifactPathsResult,
822
994
  attemptNotes,
823
995
  outputSnapshot,
996
+ originalTask: task,
997
+ completionPolicy: resolveCompletionPolicy({
998
+ agent: agent.name,
999
+ task,
1000
+ completionGuardEnabled: agent.completionGuard !== false,
1001
+ usesAcceptanceContract: effectiveAcceptance.explicit,
1002
+ tools: agent.tools,
1003
+ mcpDirectTools: agent.mcpDirectTools,
1004
+ }),
824
1005
  });
825
1006
  lastResult = result;
826
1007
  sumUsage(aggregateUsage, result.usage);
@@ -910,5 +1091,40 @@ export async function runSync(
910
1091
  if (sessionFile) result.sessionFile = sessionFile;
911
1092
  }
912
1093
 
1094
+ const initialAcceptanceOutput = acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "";
1095
+ const acceptanceForInitialReport = shouldRunAcceptanceFinalization(effectiveAcceptance)
1096
+ ? acceptanceSelfReviewConfig(effectiveAcceptance)
1097
+ : effectiveAcceptance;
1098
+ const initialAcceptance = await evaluateAcceptance({
1099
+ acceptance: acceptanceForInitialReport,
1100
+ output: initialAcceptanceOutput,
1101
+ cwd: options.cwd ?? runtimeCwd,
1102
+ });
1103
+ result.acceptance = initialAcceptance;
1104
+ if (shouldRunAcceptanceFinalization(effectiveAcceptance) && result.exitCode === 0 && !result.detached && !result.interrupted) {
1105
+ result.acceptance = await runAcceptanceFinalizationLoop({
1106
+ runtimeCwd,
1107
+ agent,
1108
+ result,
1109
+ initialLedger: initialAcceptance,
1110
+ initialOutput: initialAcceptanceOutput,
1111
+ acceptance: effectiveAcceptance,
1112
+ options,
1113
+ systemPrompt,
1114
+ resolvedSkillNames: resolvedSkills.length > 0 ? resolvedSkills.map((skill) => skill.name) : undefined,
1115
+ ...(missingSkills.length > 0 ? { skillsWarning: `Skills not found: ${missingSkills.join(", ")}` } : {}),
1116
+ });
1117
+ }
1118
+ const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
1119
+ stripAcceptanceReportsFromMessages(result.messages);
1120
+ if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted) {
1121
+ result.exitCode = 1;
1122
+ result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
1123
+ if (result.progress) {
1124
+ result.progress.status = "failed";
1125
+ result.progress.error = result.error;
1126
+ }
1127
+ }
1128
+
913
1129
  return result;
914
1130
  }