pi-subagents 0.29.0 → 0.31.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 +43 -0
  2. package/README.md +125 -19
  3. package/agents/context-builder.md +3 -3
  4. package/agents/planner.md +1 -1
  5. package/agents/researcher.md +1 -1
  6. package/agents/scout.md +1 -1
  7. package/package.json +7 -7
  8. package/skills/pi-subagents/SKILL.md +30 -0
  9. package/src/agents/agent-management.ts +189 -8
  10. package/src/agents/agent-serializer.ts +35 -12
  11. package/src/agents/agents.ts +243 -24
  12. package/src/agents/frontmatter.ts +66 -2
  13. package/src/agents/proactive-skills.ts +191 -0
  14. package/src/agents/skills.ts +117 -20
  15. package/src/extension/doctor.ts +20 -0
  16. package/src/extension/fanout-child.ts +2 -1
  17. package/src/extension/index.ts +50 -5
  18. package/src/extension/schemas.ts +40 -79
  19. package/src/intercom/intercom-bridge.ts +2 -3
  20. package/src/runs/background/async-execution.ts +180 -67
  21. package/src/runs/background/async-job-tracker.ts +56 -11
  22. package/src/runs/background/async-resume.ts +53 -5
  23. package/src/runs/background/async-status.ts +4 -1
  24. package/src/runs/background/chain-append.ts +282 -0
  25. package/src/runs/background/chain-root-attachment.ts +161 -0
  26. package/src/runs/background/result-watcher.ts +11 -2
  27. package/src/runs/background/run-status.ts +1 -0
  28. package/src/runs/background/stale-run-reconciler.ts +9 -4
  29. package/src/runs/background/subagent-runner.ts +158 -11
  30. package/src/runs/foreground/chain-execution.ts +26 -2
  31. package/src/runs/foreground/execution.ts +114 -8
  32. package/src/runs/foreground/subagent-executor.ts +611 -87
  33. package/src/runs/shared/acceptance.ts +285 -34
  34. package/src/runs/shared/chain-outputs.ts +23 -8
  35. package/src/runs/shared/completion-guard.ts +1 -1
  36. package/src/runs/shared/dynamic-fanout.ts +5 -3
  37. package/src/runs/shared/mcp-direct-tool-allowlist.ts +2 -2
  38. package/src/runs/shared/parallel-utils.ts +13 -1
  39. package/src/runs/shared/pi-args.ts +12 -3
  40. package/src/runs/shared/single-output.ts +15 -1
  41. package/src/runs/shared/subagent-control.ts +8 -11
  42. package/src/shared/settings.ts +1 -0
  43. package/src/shared/types.ts +17 -2
  44. package/src/shared/utils.ts +19 -1
  45. package/src/slash/prompt-template-bridge.ts +26 -3
  46. package/src/slash/slash-bridge.ts +3 -1
  47. package/src/slash/slash-commands.ts +34 -4
  48. package/src/tui/render.ts +265 -13
@@ -23,6 +23,8 @@ import {
23
23
  DEFAULT_MAX_OUTPUT,
24
24
  INTERCOM_DETACH_REQUEST_EVENT,
25
25
  INTERCOM_DETACH_RESPONSE_EVENT,
26
+ type AcceptanceLedger,
27
+ type ResolvedAcceptanceConfig,
26
28
  truncateOutput,
27
29
  getSubagentDepthEnv,
28
30
  } from "../../shared/types.ts";
@@ -47,7 +49,7 @@ import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
47
49
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
48
50
  import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/pi-args.ts";
49
51
  import { readStructuredOutput } from "../shared/structured-output.ts";
50
- import { captureSingleOutputSnapshot, formatSavedOutputReference, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
52
+ import { captureSingleOutputSnapshot, formatSavedOutputReference, injectOutputPathSystemPrompt, resolveSingleOutput, validateFileOnlyOutputMode, type SingleOutputSnapshot } from "../shared/single-output.ts";
51
53
  import {
52
54
  buildModelCandidates,
53
55
  formatModelAttemptNote,
@@ -82,6 +84,34 @@ function sumUsage(target: Usage, source: Usage): void {
82
84
  target.turns += source.turns;
83
85
  }
84
86
 
87
+ function formatTimeoutMessage(timeoutMs: number): string {
88
+ return `Subagent timed out after ${timeoutMs}ms.`;
89
+ }
90
+
91
+ function resolveAttemptTimeout(options: RunSyncOptions): { timeoutMs: number; remainingMs: number; message: string } | undefined {
92
+ if (options.timeoutMs === undefined) return undefined;
93
+ const deadlineAt = options.deadlineAt ?? Date.now() + options.timeoutMs;
94
+ return {
95
+ timeoutMs: options.timeoutMs,
96
+ remainingMs: Math.max(0, deadlineAt - Date.now()),
97
+ message: formatTimeoutMessage(options.timeoutMs),
98
+ };
99
+ }
100
+
101
+ function buildTimedOutAcceptanceLedger(acceptance: ResolvedAcceptanceConfig): AcceptanceLedger {
102
+ return {
103
+ status: acceptance.level === "none" ? "not-required" : "rejected",
104
+ explicit: acceptance.explicit,
105
+ effectiveAcceptance: acceptance,
106
+ inferredReason: acceptance.inferredReason,
107
+ criteria: acceptance.criteria,
108
+ runtimeChecks: acceptance.level === "none"
109
+ ? []
110
+ : [{ id: "timeout", status: "failed", message: "Acceptance was not evaluated because the subagent timed out." }],
111
+ verifyRuns: [],
112
+ };
113
+ }
114
+
85
115
  function appendRecentOutput(progress: AgentProgress, lines: string[]): void {
86
116
  if (lines.length === 0) return;
87
117
  progress.recentOutput.push(...lines.filter((line) => line.trim()));
@@ -162,8 +192,10 @@ async function runSingleAttempt(
162
192
  systemPromptMode: agent.systemPromptMode,
163
193
  inheritProjectContext: agent.inheritProjectContext,
164
194
  inheritSkills: agent.inheritSkills,
195
+ requireReadTool: Boolean(shared.resolvedSkillNames?.length),
165
196
  tools: agent.tools,
166
197
  extensions: agent.extensions,
198
+ subagentOnlyExtensions: agent.subagentOnlyExtensions,
167
199
  systemPrompt: shared.systemPrompt,
168
200
  mcpDirectTools: agent.mcpDirectTools,
169
201
  cwd: options.cwd ?? runtimeCwd,
@@ -177,6 +209,7 @@ async function runSingleAttempt(
177
209
  parentControlInbox: options.nestedRoute?.controlInbox,
178
210
  parentRootRunId: options.nestedRoute?.rootRunId,
179
211
  parentCapabilityToken: options.nestedRoute?.capabilityToken,
212
+ parentSessionId: options.parentSessionId,
180
213
  structuredOutput: options.structuredOutput,
181
214
  });
182
215
 
@@ -226,6 +259,21 @@ async function runSingleAttempt(
226
259
  lastActivityAt: startTime,
227
260
  };
228
261
  result.progress = progress;
262
+ const attemptTimeout = resolveAttemptTimeout(options);
263
+ if (attemptTimeout?.remainingMs === 0) {
264
+ result.exitCode = 1;
265
+ result.timedOut = true;
266
+ result.error = attemptTimeout.message;
267
+ result.finalOutput = attemptTimeout.message;
268
+ progress.status = "failed";
269
+ progress.error = attemptTimeout.message;
270
+ result.progressSummary = {
271
+ toolCount: progress.toolCount,
272
+ tokens: progress.tokens,
273
+ durationMs: progress.durationMs,
274
+ };
275
+ return result;
276
+ }
229
277
  const spawnEnv = { ...process.env, ...sharedEnv, ...getSubagentDepthEnv(options.maxSubagentDepth) };
230
278
  let observedMutationAttempt = false;
231
279
 
@@ -247,6 +295,23 @@ async function runSingleAttempt(
247
295
  let removeAbortListener: (() => void) | undefined;
248
296
  let removeInterruptListener: (() => void) | undefined;
249
297
  let activityTimer: NodeJS.Timeout | undefined;
298
+ let timeoutTimer: NodeJS.Timeout | undefined;
299
+ let timeoutTerminationTimer: NodeJS.Timeout | undefined;
300
+ let timeoutHardKillTimer: NodeJS.Timeout | undefined;
301
+ const clearTimeoutTimers = () => {
302
+ if (timeoutTimer) {
303
+ clearTimeout(timeoutTimer);
304
+ timeoutTimer = undefined;
305
+ }
306
+ if (timeoutTerminationTimer) {
307
+ clearTimeout(timeoutTerminationTimer);
308
+ timeoutTerminationTimer = undefined;
309
+ }
310
+ if (timeoutHardKillTimer) {
311
+ clearTimeout(timeoutHardKillTimer);
312
+ timeoutHardKillTimer = undefined;
313
+ }
314
+ };
250
315
 
251
316
  const detachForIntercom = () => {
252
317
  detached = true;
@@ -315,6 +380,7 @@ async function runSingleAttempt(
315
380
  settled = true;
316
381
  clearFinalDrainTimers();
317
382
  clearStdioGuard();
383
+ clearTimeoutTimers();
318
384
  if (activityTimer) {
319
385
  clearInterval(activityTimer);
320
386
  activityTimer = undefined;
@@ -428,7 +494,8 @@ async function runSingleAttempt(
428
494
  const fireUpdate = () => {
429
495
  if (!options.onUpdate || processClosed) return;
430
496
  progress.durationMs = Date.now() - startTime;
431
- emitUpdateSnapshot(getFinalOutput(result.messages) || "(running...)");
497
+ const output = result.timedOut && result.finalOutput ? result.finalOutput : getFinalOutput(result.messages);
498
+ emitUpdateSnapshot(output || "(running...)");
432
499
  };
433
500
 
434
501
  const processLine = (line: string) => {
@@ -554,6 +621,31 @@ async function runSingleAttempt(
554
621
  activityTimer.unref?.();
555
622
  }
556
623
 
624
+ if (attemptTimeout) {
625
+ timeoutTimer = setTimeout(() => {
626
+ if (processClosed || settled || detached || interruptedByControl) return;
627
+ result.timedOut = true;
628
+ result.error = attemptTimeout.message;
629
+ result.finalOutput = attemptTimeout.message;
630
+ progress.status = "failed";
631
+ progress.error = attemptTimeout.message;
632
+ progress.durationMs = Date.now() - startTime;
633
+ fireUpdate();
634
+ trySignalChild(proc, "SIGINT");
635
+ timeoutTerminationTimer = setTimeout(() => {
636
+ if (processClosed || settled || detached) return;
637
+ trySignalChild(proc, "SIGTERM");
638
+ }, 1000);
639
+ timeoutTerminationTimer.unref?.();
640
+ timeoutHardKillTimer = setTimeout(() => {
641
+ if (processClosed || settled || detached) return;
642
+ trySignalChild(proc, "SIGKILL");
643
+ }, 4000);
644
+ timeoutHardKillTimer.unref?.();
645
+ }, attemptTimeout.remainingMs);
646
+ timeoutTimer.unref?.();
647
+ }
648
+
557
649
  let stderrBuf = "";
558
650
 
559
651
  const clearStdioGuard = attachPostExitStdioGuard(proc, { idleMs: 2000, hardMs: 8000 });
@@ -624,7 +716,9 @@ async function runSingleAttempt(
624
716
  if (options.interruptSignal) {
625
717
  const interrupt = () => {
626
718
  if (processClosed || detached || settled) return;
719
+ if (result.timedOut) return;
627
720
  interruptedByControl = true;
721
+ clearTimeoutTimers();
628
722
  progress.status = "running";
629
723
  progress.durationMs = Date.now() - startTime;
630
724
  result.interrupted = true;
@@ -709,8 +803,14 @@ async function runSingleAttempt(
709
803
  durationMs: progress.durationMs,
710
804
  };
711
805
 
712
- const acceptanceOutput = getFinalOutput(result.messages);
713
- let fullOutput = stripAcceptanceReport(acceptanceOutput);
806
+ const acceptanceOutput = getFinalOutput(result.messages);
807
+ let fullOutput = stripAcceptanceReport(acceptanceOutput);
808
+ if (result.timedOut) {
809
+ const timeoutMessage = formatTimeoutMessage(options.timeoutMs ?? 0);
810
+ fullOutput = fullOutput.trim()
811
+ ? `${timeoutMessage}\n\nPartial output before timeout:\n${fullOutput}`
812
+ : timeoutMessage;
813
+ }
714
814
  const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
715
815
  ? evaluateCompletionMutationGuard({
716
816
  agent: agent.name,
@@ -834,6 +934,7 @@ export async function runSync(
834
934
  const skillInjection = buildSkillInjection(resolvedSkills);
835
935
  systemPrompt = systemPrompt ? `${systemPrompt}\n\n${skillInjection}` : skillInjection;
836
936
  }
937
+ systemPrompt = injectOutputPathSystemPrompt(systemPrompt, options.outputPath);
837
938
 
838
939
  const candidates = buildModelCandidates(
839
940
  options.modelOverride ?? agent.model,
@@ -891,6 +992,9 @@ export async function runSync(
891
992
  usage: { ...result.usage },
892
993
  };
893
994
  modelAttempts.push(attempt);
995
+ if (result.timedOut) {
996
+ break;
997
+ }
894
998
  if (attemptSucceeded) {
895
999
  break;
896
1000
  }
@@ -966,14 +1070,16 @@ export async function runSync(
966
1070
  if (sessionFile) result.sessionFile = sessionFile;
967
1071
  }
968
1072
 
969
- result.acceptance = await evaluateAcceptance({
1073
+ result.acceptance = result.timedOut
1074
+ ? buildTimedOutAcceptanceLedger(effectiveAcceptance)
1075
+ : await evaluateAcceptance({
970
1076
  acceptance: effectiveAcceptance,
971
1077
  output: acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "",
972
1078
  cwd: options.cwd ?? runtimeCwd,
973
1079
  });
974
- const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
975
- stripAcceptanceReportsFromMessages(result.messages);
976
- if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted) {
1080
+ const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
1081
+ stripAcceptanceReportsFromMessages(result.messages);
1082
+ if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted && !result.timedOut) {
977
1083
  result.exitCode = 1;
978
1084
  result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
979
1085
  if (result.progress) {