pi-subagents 0.28.0 → 0.30.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 (47) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +26 -62
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +29 -35
  5. package/src/agents/agent-management.ts +29 -22
  6. package/src/agents/agent-selection.ts +2 -0
  7. package/src/agents/agent-serializer.ts +5 -10
  8. package/src/agents/agents.ts +339 -47
  9. package/src/agents/chain-serializer.ts +4 -9
  10. package/src/agents/proactive-skills.ts +191 -0
  11. package/src/extension/doctor.ts +4 -3
  12. package/src/extension/fanout-child.ts +1 -3
  13. package/src/extension/index.ts +6 -9
  14. package/src/extension/schemas.ts +63 -26
  15. package/src/intercom/intercom-bridge.ts +11 -1
  16. package/src/intercom/result-intercom.ts +0 -5
  17. package/src/runs/background/async-execution.ts +186 -74
  18. package/src/runs/background/async-resume.ts +53 -5
  19. package/src/runs/background/async-status.ts +4 -1
  20. package/src/runs/background/chain-append.ts +282 -0
  21. package/src/runs/background/chain-root-attachment.ts +161 -0
  22. package/src/runs/background/run-status.ts +2 -7
  23. package/src/runs/background/subagent-runner.ts +160 -219
  24. package/src/runs/foreground/chain-execution.ts +62 -58
  25. package/src/runs/foreground/execution.ts +39 -343
  26. package/src/runs/foreground/subagent-executor.ts +316 -111
  27. package/src/runs/shared/acceptance.ts +605 -22
  28. package/src/runs/shared/chain-outputs.ts +23 -8
  29. package/src/runs/shared/completion-guard.ts +3 -26
  30. package/src/runs/shared/dynamic-fanout.ts +1 -1
  31. package/src/runs/shared/model-fallback.ts +38 -0
  32. package/src/runs/shared/parallel-utils.ts +13 -10
  33. package/src/runs/shared/pi-args.ts +3 -2
  34. package/src/runs/shared/subagent-control.ts +8 -11
  35. package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
  36. package/src/runs/shared/workflow-graph.ts +2 -6
  37. package/src/shared/atomic-json.ts +68 -11
  38. package/src/shared/settings.ts +1 -0
  39. package/src/shared/types.ts +20 -49
  40. package/src/shared/utils.ts +2 -8
  41. package/src/slash/slash-bridge.ts +3 -1
  42. package/src/slash/slash-commands.ts +1 -1
  43. package/src/tui/render.ts +14 -29
  44. package/src/runs/shared/acceptance-contract.ts +0 -318
  45. package/src/runs/shared/acceptance-evaluation.ts +0 -221
  46. package/src/runs/shared/acceptance-finalization.ts +0 -173
  47. package/src/runs/shared/acceptance-reports.ts +0 -127
@@ -8,8 +8,6 @@ import { appendJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
8
8
  import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/pi-spawn.ts";
9
9
  import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
10
10
  import {
11
- type AcceptanceFinalizationTurn,
12
- type AcceptanceLedger,
13
11
  type ActivityState,
14
12
  type ArtifactConfig,
15
13
  type ArtifactPaths,
@@ -19,9 +17,7 @@ import {
19
17
  type ModelAttempt,
20
18
  type NestedRouteInfo,
21
19
  type ResolvedControlConfig,
22
- type ResourceLimitExceeded,
23
20
  type SubagentRunMode,
24
- type TokenUsage,
25
21
  type Usage,
26
22
  type WorkflowGraphSnapshot,
27
23
  DEFAULT_MAX_OUTPUT,
@@ -54,8 +50,8 @@ import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelSt
54
50
  import { nestedSummaryFromAsyncStatus, writeNestedEvent } from "../shared/nested-events.ts";
55
51
  import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
56
52
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
57
- import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, formatResourceLimitExceeded, getFinalOutput } from "../../shared/utils.ts";
58
- import { evaluateCompletionMutationGuard, resolveCompletionPolicy } from "../shared/completion-guard.ts";
53
+ import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput } from "../../shared/utils.ts";
54
+ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
59
55
  import {
60
56
  createMutatingFailureState,
61
57
  didMutatingToolFail,
@@ -68,6 +64,7 @@ import {
68
64
  summarizeRecentMutatingFailures,
69
65
  } from "../shared/long-running-guard.ts";
70
66
  import { parseSessionTokens } from "../../shared/session-tokens.ts";
67
+ import type { TokenUsage } from "../../shared/types.ts";
71
68
  import {
72
69
  cleanupWorktrees,
73
70
  createWorktrees,
@@ -80,19 +77,9 @@ import {
80
77
  import { resolveEffectiveThinking } from "../../shared/model-info.ts";
81
78
  import { writeInitialProgressFile } from "../../shared/settings.ts";
82
79
  import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
83
- import {
84
- acceptanceFailureMessage,
85
- acceptanceSelfReviewConfig,
86
- attachFinalizationToLedger,
87
- buildFinalizationProcessFailureLedger,
88
- createFinalizationProcessFailureTurn,
89
- createFinalizationTurn,
90
- evaluateAcceptance,
91
- formatAcceptanceFinalizationPrompt,
92
- formatAcceptancePrompt,
93
- shouldRunAcceptanceFinalization,
94
- stripAcceptanceReport,
95
- } from "../shared/acceptance.ts";
80
+ import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
81
+ import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
82
+ import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
96
83
 
97
84
  interface SubagentRunConfig {
98
85
  id: string;
@@ -140,8 +127,7 @@ interface StepResult {
140
127
  structuredOutput?: unknown;
141
128
  structuredOutputPath?: string;
142
129
  structuredOutputSchemaPath?: string;
143
- acceptance?: AcceptanceLedger;
144
- resourceLimitExceeded?: ResourceLimitExceeded;
130
+ acceptance?: import("../../shared/types.ts").AcceptanceLedger;
145
131
  }
146
132
 
147
133
  const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
@@ -236,7 +222,6 @@ interface RunPiStreamingResult {
236
222
  finalOutput: string;
237
223
  interrupted?: boolean;
238
224
  observedMutationAttempt?: boolean;
239
- resourceLimitExceeded?: ResourceLimitExceeded;
240
225
  }
241
226
 
242
227
  function runPiStreaming(
@@ -250,8 +235,6 @@ function runPiStreaming(
250
235
  childEventContext?: ChildEventContext,
251
236
  registerInterrupt?: (interrupt: (() => void) | undefined) => void,
252
237
  onChildEvent?: (event: ChildEvent) => void,
253
- maxExecutionTimeMs?: number,
254
- maxTokens?: number,
255
238
  ): Promise<RunPiStreamingResult> {
256
239
  return new Promise((resolve) => {
257
240
  const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
@@ -275,10 +258,7 @@ function runPiStreaming(
275
258
  let error: string | undefined;
276
259
  let assistantError: string | undefined;
277
260
  let interrupted = false;
278
- let resourceLimitExceeded: ResourceLimitExceeded | undefined;
279
261
  let observedMutationAttempt = false;
280
- let resourceLimitTimer: NodeJS.Timeout | undefined;
281
- let resourceLimitEscalationTimer: NodeJS.Timeout | undefined;
282
262
  const rawStdoutLines: string[] = [];
283
263
 
284
264
  const writeOutputLine = (line: string) => {
@@ -292,19 +272,6 @@ function runPiStreaming(
292
272
  }
293
273
  };
294
274
 
295
- const triggerResourceLimit = (kind: ResourceLimitExceeded["kind"], limit: number, observed?: number) => {
296
- if (settled || resourceLimitExceeded) return;
297
- const message = formatResourceLimitExceeded({ agent: childEventContext?.agent ?? "subagent", kind, limit, observed });
298
- resourceLimitExceeded = { kind, limit, ...(observed !== undefined ? { observed } : {}), message };
299
- error = message;
300
- writeOutputLine(message);
301
- trySignalChild(child, "SIGINT");
302
- resourceLimitEscalationTimer = setTimeout(() => {
303
- if (!settled) trySignalChild(child, "SIGTERM");
304
- }, 1000);
305
- resourceLimitEscalationTimer.unref?.();
306
- };
307
-
308
275
  const appendChildEvent = (event: Record<string, unknown>) => {
309
276
  if (!childEventContext) return;
310
277
  appendJsonl(childEventContext.eventsPath, JSON.stringify({
@@ -359,10 +326,6 @@ function runPiStreaming(
359
326
  usage.cacheRead += eventUsage.cacheRead ?? 0;
360
327
  usage.cacheWrite += eventUsage.cacheWrite ?? 0;
361
328
  usage.cost += eventUsage.cost?.total ?? 0;
362
- const observedTokens = usage.input + usage.output;
363
- if (maxTokens !== undefined && observedTokens >= maxTokens) {
364
- triggerResourceLimit("maxTokens", maxTokens, observedTokens);
365
- }
366
329
  }
367
330
  const stopReason = (event.message as { stopReason?: string }).stopReason;
368
331
  const hasToolCall = Array.isArray(event.message.content)
@@ -398,12 +361,6 @@ function runPiStreaming(
398
361
  let finalDrainTimer: NodeJS.Timeout | undefined;
399
362
  let finalHardKillTimer: NodeJS.Timeout | undefined;
400
363
  let settled = false;
401
- if (maxExecutionTimeMs !== undefined) {
402
- resourceLimitTimer = setTimeout(() => {
403
- triggerResourceLimit("maxExecutionTimeMs", maxExecutionTimeMs);
404
- }, maxExecutionTimeMs);
405
- resourceLimitTimer.unref?.();
406
- }
407
364
  const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
408
365
  child.stdout.on("data", (chunk: Buffer) => {
409
366
  const text = chunk.toString();
@@ -417,7 +374,7 @@ function runPiStreaming(
417
374
  processStderrText(chunk.toString());
418
375
  });
419
376
  registerInterrupt?.(() => {
420
- if (settled || resourceLimitExceeded) return;
377
+ if (settled) return;
421
378
  interrupted = true;
422
379
  if (!error) error = "Interrupted. Waiting for explicit next action.";
423
380
  trySignalChild(child, "SIGINT");
@@ -434,14 +391,6 @@ function runPiStreaming(
434
391
  clearTimeout(finalHardKillTimer);
435
392
  finalHardKillTimer = undefined;
436
393
  }
437
- if (resourceLimitTimer) {
438
- clearTimeout(resourceLimitTimer);
439
- resourceLimitTimer = undefined;
440
- }
441
- if (resourceLimitEscalationTimer) {
442
- clearTimeout(resourceLimitEscalationTimer);
443
- resourceLimitEscalationTimer = undefined;
444
- }
445
394
  };
446
395
  function startFinalDrain(): void {
447
396
  if (childExited || finalDrainTimer || settled) return;
@@ -473,12 +422,12 @@ function runPiStreaming(
473
422
  if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
474
423
  if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
475
424
  outputStream.end();
476
- const finalOutput = resourceLimitExceeded?.message ?? (getFinalOutput(messages) || rawStdoutLines.join("\n").trim());
477
- const finalError = resourceLimitExceeded?.message ?? error ?? assistantError;
425
+ const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
426
+ const finalError = error ?? assistantError;
478
427
  const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
479
428
  resolve({
480
429
  stderr,
481
- exitCode: resourceLimitExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
430
+ exitCode: interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
482
431
  messages,
483
432
  usage,
484
433
  model,
@@ -486,7 +435,6 @@ function runPiStreaming(
486
435
  finalOutput,
487
436
  interrupted,
488
437
  observedMutationAttempt,
489
- resourceLimitExceeded,
490
438
  });
491
439
  });
492
440
 
@@ -496,9 +444,9 @@ function runPiStreaming(
496
444
  clearDrainTimers();
497
445
  clearStdioGuard();
498
446
  outputStream.end();
499
- const finalOutput = resourceLimitExceeded?.message ?? (getFinalOutput(messages) || rawStdoutLines.join("\n").trim());
447
+ const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
500
448
  const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
501
- resolve({ stderr, exitCode: 1, messages, usage, model, error: resourceLimitExceeded?.message ?? error ?? assistantError ?? spawnErrorMessage, finalOutput, observedMutationAttempt, resourceLimitExceeded });
449
+ resolve({ stderr, exitCode: 1, messages, usage, model, error: error ?? assistantError ?? spawnErrorMessage, finalOutput, observedMutationAttempt });
502
450
  });
503
451
  });
504
452
  }
@@ -653,9 +601,32 @@ async function runSingleStep(
653
601
  structuredOutput?: unknown;
654
602
  structuredOutputPath?: string;
655
603
  structuredOutputSchemaPath?: string;
656
- acceptance?: AcceptanceLedger;
657
- resourceLimitExceeded?: ResourceLimitExceeded;
604
+ acceptance?: import("../../shared/types.ts").AcceptanceLedger;
658
605
  }> {
606
+ if (step.importAsyncRoot) {
607
+ const imported = await waitForImportedAsyncRoot(step.importAsyncRoot);
608
+ try {
609
+ fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
610
+ } catch {
611
+ // Output files are observability only for imported roots.
612
+ }
613
+ return {
614
+ agent: imported.agent,
615
+ output: imported.output,
616
+ exitCode: imported.exitCode,
617
+ error: imported.error,
618
+ sessionFile: imported.sessionFile,
619
+ intercomTarget: imported.intercomTarget,
620
+ model: imported.model,
621
+ attemptedModels: imported.attemptedModels,
622
+ modelAttempts: imported.modelAttempts,
623
+ structuredOutput: imported.structuredOutput,
624
+ structuredOutputPath: imported.structuredOutputPath,
625
+ structuredOutputSchemaPath: imported.structuredOutputSchemaPath,
626
+ acceptance: imported.acceptance,
627
+ };
628
+ }
629
+
659
630
  const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
660
631
  ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
661
632
  : undefined);
@@ -715,6 +686,7 @@ async function runSingleStep(
715
686
  inheritSkills: step.inheritSkills,
716
687
  tools: step.tools,
717
688
  extensions: step.extensions,
689
+ subagentOnlyExtensions: step.subagentOnlyExtensions,
718
690
  systemPrompt: step.systemPrompt,
719
691
  systemPromptMode: step.systemPromptMode,
720
692
  mcpDirectTools: step.mcpDirectTools,
@@ -742,8 +714,6 @@ async function runSingleStep(
742
714
  { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
743
715
  ctx.registerInterrupt,
744
716
  ctx.onChildEvent,
745
- step.maxExecutionTimeMs,
746
- step.maxTokens,
747
717
  );
748
718
  cleanupTempDir(tempDir);
749
719
 
@@ -759,15 +729,7 @@ async function runSingleStep(
759
729
  if (structured.error) structuredError = structured.error;
760
730
  else structuredOutput = structured.value;
761
731
  }
762
- const completionPolicy = resolveCompletionPolicy({
763
- agent: step.agent,
764
- task: taskForCompletionGuard,
765
- completionGuardEnabled: step.completionGuard !== false,
766
- usesAcceptanceContract: step.effectiveAcceptance?.explicit === true,
767
- tools: step.tools,
768
- mcpDirectTools: step.mcpDirectTools,
769
- });
770
- const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && completionPolicy === "mutation-guard"
732
+ const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && step.completionGuard !== false
771
733
  ? evaluateCompletionMutationGuard({
772
734
  agent: step.agent,
773
735
  task: taskForCompletionGuard,
@@ -780,7 +742,7 @@ async function runSingleStep(
780
742
  const completionGuardError = completionGuardTriggered
781
743
  ? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
782
744
  : undefined;
783
- const effectiveExitCode = completionGuardError
745
+ const effectiveExitCode = completionGuardTriggered
784
746
  ? 1
785
747
  : structuredError
786
748
  ? 1
@@ -808,8 +770,8 @@ async function runSingleStep(
808
770
  completionGuardTriggeredFinal = completionGuardTriggered;
809
771
  finalOutputSnapshot = outputSnapshot;
810
772
  finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
811
- if (attempt.success || completionGuardError) break;
812
- if (run.resourceLimitExceeded || !isRetryableModelFailure(error) || index === candidates.length - 1) break;
773
+ if (attempt.success || completionGuardTriggered) break;
774
+ if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
813
775
  attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
814
776
  }
815
777
 
@@ -821,12 +783,12 @@ async function runSingleStep(
821
783
  const output = resolvedOutput.fullOutput;
822
784
  const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
823
785
  let outputForSummary = output;
824
- if (attemptNotes.length > 0) {
825
- outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
826
- }
786
+ if (attemptNotes.length > 0) {
787
+ outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
788
+ }
827
789
  const outputForAcceptance = rawOutput;
828
- const finalizedOutput = finalizeSingleOutput({
829
- fullOutput: outputForSummary,
790
+ const finalizedOutput = finalizeSingleOutput({
791
+ fullOutput: outputForSummary,
830
792
  outputPath: step.outputPath,
831
793
  outputMode: step.outputMode,
832
794
  exitCode: finalResult?.exitCode ?? 1,
@@ -835,117 +797,13 @@ async function runSingleStep(
835
797
  saveError: resolvedOutput.saveError,
836
798
  });
837
799
  outputForSummary = finalizedOutput.displayOutput;
838
- const acceptanceForInitialReport = step.effectiveAcceptance && shouldRunAcceptanceFinalization(step.effectiveAcceptance)
839
- ? acceptanceSelfReviewConfig(step.effectiveAcceptance)
840
- : step.effectiveAcceptance;
841
- let acceptance = acceptanceForInitialReport
842
- ? await evaluateAcceptance({
843
- acceptance: acceptanceForInitialReport,
844
- output: outputForAcceptance,
845
- cwd: step.cwd ?? ctx.cwd,
846
- })
800
+ const acceptance = step.effectiveAcceptance
801
+ ? await evaluateAcceptance({
802
+ acceptance: step.effectiveAcceptance,
803
+ output: outputForAcceptance,
804
+ cwd: step.cwd ?? ctx.cwd,
805
+ })
847
806
  : undefined;
848
- if (acceptance && step.effectiveAcceptance && shouldRunAcceptanceFinalization(step.effectiveAcceptance) && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted) {
849
- const sessionFile = step.sessionFile ?? (sessionDir ? findLatestSessionFile(sessionDir) ?? undefined : undefined);
850
- const maxTurns = step.effectiveAcceptance.finalization.maxTurns;
851
- const turns: AcceptanceFinalizationTurn[] = [];
852
- if (!sessionFile) {
853
- const message = "Acceptance finalization requires a session file for same-session continuation.";
854
- turns.push(createFinalizationProcessFailureTurn({ turn: 1, prompt: "", message }));
855
- acceptance = buildFinalizationProcessFailureLedger({ initialLedger: acceptance, turns, maxTurns, message });
856
- } else {
857
- const selfReviewAcceptance = acceptanceSelfReviewConfig(step.effectiveAcceptance);
858
- let previousFailure = acceptanceFailureMessage(acceptance);
859
- let authoritativeLedger = acceptance;
860
- for (let turn = 1; turn <= maxTurns; turn++) {
861
- const prompt = formatAcceptanceFinalizationPrompt({
862
- acceptance: step.effectiveAcceptance,
863
- initialOutput: outputForAcceptance,
864
- initialLedger: acceptance,
865
- turn,
866
- maxTurns,
867
- ...(previousFailure ? { previousFailure } : {}),
868
- });
869
- const { args, env, tempDir } = buildPiArgs({
870
- baseArgs: ["--mode", "json", "-p"],
871
- task: prompt,
872
- sessionEnabled: true,
873
- sessionFile,
874
- model: finalResult?.model ?? step.model,
875
- thinking: step.thinking,
876
- inheritProjectContext: step.inheritProjectContext,
877
- inheritSkills: step.inheritSkills,
878
- tools: step.tools,
879
- extensions: step.extensions,
880
- systemPrompt: step.systemPrompt,
881
- systemPromptMode: step.systemPromptMode,
882
- mcpDirectTools: step.mcpDirectTools,
883
- cwd: step.cwd ?? ctx.cwd,
884
- promptFileStem: `${step.agent}-acceptance-finalization`,
885
- intercomSessionName: ctx.childIntercomTarget,
886
- orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
887
- runId: ctx.id,
888
- childAgentName: step.agent,
889
- childIndex: ctx.flatIndex,
890
- parentEventSink: ctx.nestedRoute?.eventSink,
891
- parentControlInbox: ctx.nestedRoute?.controlInbox,
892
- parentRootRunId: ctx.nestedRoute?.rootRunId,
893
- parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
894
- });
895
- ctx.onAttemptStart?.({ model: finalResult?.model ?? step.model, thinking: resolveEffectiveThinking(finalResult?.model ?? step.model, step.thinking) });
896
- const finalizationRun = await runPiStreaming(
897
- args,
898
- step.cwd ?? ctx.cwd,
899
- `${ctx.outputFile}.finalization-${turn}.log`,
900
- env,
901
- ctx.piPackageRoot,
902
- ctx.piArgv1,
903
- step.maxSubagentDepth,
904
- { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
905
- ctx.registerInterrupt,
906
- ctx.onChildEvent,
907
- step.maxExecutionTimeMs,
908
- step.maxTokens,
909
- );
910
- cleanupTempDir(tempDir);
911
- modelAttempts.push({
912
- model: finalResult?.model ?? finalizationRun.model ?? step.model ?? "default",
913
- success: finalizationRun.exitCode === 0 && !finalizationRun.error,
914
- exitCode: finalizationRun.exitCode,
915
- error: finalizationRun.error,
916
- usage: finalizationRun.usage,
917
- });
918
- const finalizationOutput = finalizationRun.finalOutput;
919
- if (finalizationRun.exitCode !== 0 || finalizationRun.error || finalizationRun.interrupted) {
920
- const message = finalizationRun.error ?? "Acceptance finalization turn did not complete successfully.";
921
- turns.push(createFinalizationProcessFailureTurn({ turn, prompt, rawOutput: finalizationOutput, message }));
922
- acceptance = buildFinalizationProcessFailureLedger({ initialLedger: acceptance, turns, maxTurns, message });
923
- break;
924
- }
925
- const selfReviewLedger = await evaluateAcceptance({
926
- acceptance: selfReviewAcceptance,
927
- output: finalizationOutput,
928
- cwd: step.cwd ?? ctx.cwd,
929
- });
930
- authoritativeLedger = selfReviewLedger;
931
- turns.push(createFinalizationTurn({ turn, prompt, rawOutput: finalizationOutput, ledger: selfReviewLedger }));
932
- const failure = acceptanceFailureMessage(selfReviewLedger);
933
- if (!failure) {
934
- authoritativeLedger = step.effectiveAcceptance === selfReviewAcceptance
935
- ? selfReviewLedger
936
- : await evaluateAcceptance({
937
- acceptance: step.effectiveAcceptance,
938
- output: finalizationOutput,
939
- cwd: step.cwd ?? ctx.cwd,
940
- });
941
- acceptance = attachFinalizationToLedger({ initialLedger: acceptance, authoritativeLedger, turns, status: "completed", maxTurns });
942
- break;
943
- }
944
- previousFailure = failure;
945
- if (turn === maxTurns) acceptance = attachFinalizationToLedger({ initialLedger: acceptance, authoritativeLedger, turns, status: "failed", maxTurns });
946
- }
947
- }
948
- }
949
807
  const acceptanceFailure = acceptance ? acceptanceFailureMessage(acceptance) : undefined;
950
808
  const acceptanceCanFailRun = acceptanceFailure && acceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted;
951
809
  const effectiveFinalExitCode = acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
@@ -968,7 +826,6 @@ async function runSingleStep(
968
826
  model: finalResult?.model,
969
827
  attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
970
828
  modelAttempts,
971
- resourceLimitExceeded: finalResult?.resourceLimitExceeded,
972
829
  skills: step.skills,
973
830
  timestamp: Date.now(),
974
831
  }, null, 2),
@@ -994,7 +851,6 @@ async function runSingleStep(
994
851
  structuredOutputPath: effectiveStructuredOutput?.outputPath,
995
852
  structuredOutputSchemaPath: effectiveStructuredOutput?.schemaPath,
996
853
  acceptance,
997
- resourceLimitExceeded: finalResult?.resourceLimitExceeded,
998
854
  };
999
855
  }
1000
856
 
@@ -1283,7 +1139,46 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1283
1139
  writeAtomicJson(statusPath, statusPayload);
1284
1140
  emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
1285
1141
  };
1286
- const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: AcceptanceLedger): void => {
1142
+ const consumePendingAppendRequests = (): void => {
1143
+ if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
1144
+ const requests = consumeChainAppendRequests(asyncDir);
1145
+ if (requests.length === 0) {
1146
+ const pendingAppends = countPendingChainAppendRequests(asyncDir);
1147
+ if ((statusPayload.pendingAppends ?? 0) !== pendingAppends) {
1148
+ statusPayload.pendingAppends = pendingAppends;
1149
+ statusPayload.lastUpdate = Date.now();
1150
+ writeStatusPayload();
1151
+ }
1152
+ return;
1153
+ }
1154
+ const appendedSteps = requests.flatMap((request) => request.steps);
1155
+ steps.push(...appendedSteps);
1156
+ const now = Date.now();
1157
+ const pendingAppends = countPendingChainAppendRequests(asyncDir);
1158
+ const added = appendRunnerStepsToStatus({
1159
+ status: statusPayload,
1160
+ steps: appendedSteps,
1161
+ now,
1162
+ pendingAppends,
1163
+ });
1164
+ mutatingFailureStates.push(...Array.from({ length: added.addedFlatSteps }, () => createMutatingFailureState()));
1165
+ pendingToolResults.push(...Array.from({ length: added.addedFlatSteps }, () => undefined));
1166
+ if (config.childIntercomTargets) {
1167
+ config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
1168
+ }
1169
+ writeStatusPayload();
1170
+ for (const request of requests) {
1171
+ appendJsonl(eventsPath, JSON.stringify({
1172
+ type: "subagent.chain.append.accepted",
1173
+ ts: now,
1174
+ runId: id,
1175
+ requestId: request.id,
1176
+ stepCount: request.steps.length,
1177
+ pendingAppends,
1178
+ }));
1179
+ }
1180
+ };
1181
+ const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
1287
1182
  const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
1288
1183
  if (!groupNode) return;
1289
1184
  groupNode.status = status;
@@ -1574,10 +1469,14 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1574
1469
  );
1575
1470
 
1576
1471
  let flatIndex = 0;
1472
+ let stepCursor = 0;
1577
1473
 
1578
- for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
1474
+ while (true) {
1579
1475
  if (interrupted) break;
1580
- const step = steps[stepIndex];
1476
+ consumePendingAppendRequests();
1477
+ if (stepCursor >= steps.length) break;
1478
+ const stepIndex = stepCursor++;
1479
+ const step = steps[stepIndex]!;
1581
1480
 
1582
1481
  if (isDynamicRunnerGroup(step)) {
1583
1482
  const groupStartFlatIndex = flatIndex;
@@ -1625,9 +1524,36 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1625
1524
  placeholder.durationMs = 0;
1626
1525
  }
1627
1526
  previousOutput = "Dynamic fanout produced 0 results.";
1527
+ const groupAcceptance = step.effectiveAcceptance?.explicit
1528
+ ? await evaluateAcceptance({
1529
+ acceptance: step.effectiveAcceptance,
1530
+ output: "",
1531
+ report: aggregateAcceptanceReport({
1532
+ results: [],
1533
+ notes: "Dynamic fanout produced 0 results.",
1534
+ }),
1535
+ cwd,
1536
+ })
1537
+ : undefined;
1538
+ if (placeholder && groupAcceptance) placeholder.acceptance = groupAcceptance;
1539
+ const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1540
+ if (groupAcceptanceFailure) {
1541
+ statusPayload.state = "failed";
1542
+ statusPayload.error = groupAcceptanceFailure;
1543
+ if (placeholder) {
1544
+ placeholder.status = "failed";
1545
+ placeholder.error = groupAcceptanceFailure;
1546
+ placeholder.exitCode = 1;
1547
+ }
1548
+ markDynamicGraphGroup(stepIndex, "failed", groupAcceptanceFailure, groupAcceptance);
1549
+ statusPayload.lastUpdate = now;
1550
+ writeStatusPayload();
1551
+ results.push({ agent: step.parallel.agent, output: groupAcceptanceFailure, error: groupAcceptanceFailure, success: false, exitCode: 1, acceptance: groupAcceptance });
1552
+ break;
1553
+ }
1628
1554
  flatIndex++;
1629
1555
  statusPayload.lastUpdate = now;
1630
- markDynamicGraphGroup(stepIndex, "completed");
1556
+ markDynamicGraphGroup(stepIndex, "completed", undefined, groupAcceptance);
1631
1557
  writeStatusPayload();
1632
1558
  continue;
1633
1559
  }
@@ -1759,14 +1685,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1759
1685
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
1760
1686
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
1761
1687
  statusPayload.steps[fi].acceptance = singleResult.acceptance;
1762
- statusPayload.steps[fi].resourceLimitExceeded = singleResult.resourceLimitExceeded;
1763
1688
  statusPayload.lastUpdate = taskEndTime;
1764
1689
  writeStatusPayload();
1765
1690
  appendJsonl(eventsPath, JSON.stringify({
1766
1691
  type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
1767
1692
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
1768
1693
  exitCode: singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
1769
- resourceLimitExceeded: singleResult.resourceLimitExceeded,
1770
1694
  }));
1771
1695
  if (singleResult.exitCode !== 0 && failFast) aborted = true;
1772
1696
  return { ...singleResult, skipped: false };
@@ -1791,7 +1715,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1791
1715
  structuredOutputPath: pr.structuredOutputPath,
1792
1716
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
1793
1717
  acceptance: pr.acceptance,
1794
- resourceLimitExceeded: pr.resourceLimitExceeded,
1795
1718
  });
1796
1719
  }
1797
1720
  const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
@@ -1806,7 +1729,31 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1806
1729
  stepIndex,
1807
1730
  };
1808
1731
  statusPayload.outputs = outputs;
1809
- markDynamicGraphGroup(stepIndex, "completed");
1732
+ const groupAcceptance = step.effectiveAcceptance
1733
+ ? await evaluateAcceptance({
1734
+ acceptance: step.effectiveAcceptance,
1735
+ output: "",
1736
+ report: aggregateAcceptanceReport({
1737
+ results: parallelResults,
1738
+ notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
1739
+ }),
1740
+ cwd,
1741
+ })
1742
+ : undefined;
1743
+ const groupAcceptanceFailure = groupAcceptance ? acceptanceFailureMessage(groupAcceptance) : undefined;
1744
+ markDynamicGraphGroup(stepIndex, groupAcceptanceFailure ? "failed" : "completed", groupAcceptanceFailure, groupAcceptance);
1745
+ if (groupAcceptanceFailure) {
1746
+ results.push({
1747
+ agent: step.parallel.agent,
1748
+ output: groupAcceptanceFailure,
1749
+ error: groupAcceptanceFailure,
1750
+ success: false,
1751
+ exitCode: 1,
1752
+ structuredOutput: collection,
1753
+ acceptance: groupAcceptance,
1754
+ });
1755
+ statusPayload.error = groupAcceptanceFailure;
1756
+ }
1810
1757
  } catch (error) {
1811
1758
  const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
1812
1759
  results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
@@ -1958,7 +1905,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1958
1905
  outputs,
1959
1906
  sessionDir: taskSessionDir,
1960
1907
  artifactsDir, artifactConfig, id,
1961
- flatIndex: fi, flatStepCount: flatSteps.length,
1908
+ flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
1962
1909
  outputFile: path.join(asyncDir, `output-${fi}.log`),
1963
1910
  piPackageRoot: config.piPackageRoot,
1964
1911
  piArgv1: config.piArgv1,
@@ -1991,7 +1938,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1991
1938
  statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
1992
1939
  statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
1993
1940
  statusPayload.steps[fi].acceptance = singleResult.acceptance;
1994
- statusPayload.steps[fi].resourceLimitExceeded = singleResult.resourceLimitExceeded;
1995
1941
  statusPayload.lastUpdate = taskEndTime;
1996
1942
  writeStatusPayload();
1997
1943
 
@@ -1999,7 +1945,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
1999
1945
  type: singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2000
1946
  ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
2001
1947
  exitCode: singleResult.exitCode, durationMs: taskDuration,
2002
- resourceLimitExceeded: singleResult.resourceLimitExceeded,
2003
1948
  }));
2004
1949
  if (singleResult.completionGuardTriggered) {
2005
1950
  const event = buildControlEvent({
@@ -2058,7 +2003,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2058
2003
  structuredOutputPath: pr.structuredOutputPath,
2059
2004
  structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
2060
2005
  acceptance: pr.acceptance,
2061
- resourceLimitExceeded: pr.resourceLimitExceeded,
2062
2006
  });
2063
2007
  }
2064
2008
  for (let t = 0; t < group.parallel.length; t++) {
@@ -2126,7 +2070,7 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2126
2070
  outputs,
2127
2071
  sessionDir: config.sessionDir,
2128
2072
  artifactsDir, artifactConfig, id,
2129
- flatIndex, flatStepCount: flatSteps.length,
2073
+ flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
2130
2074
  outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
2131
2075
  piPackageRoot: config.piPackageRoot,
2132
2076
  piArgv1: config.piArgv1,
@@ -2160,7 +2104,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2160
2104
  structuredOutputPath: singleResult.structuredOutputPath,
2161
2105
  structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
2162
2106
  acceptance: singleResult.acceptance,
2163
- resourceLimitExceeded: singleResult.resourceLimitExceeded,
2164
2107
  });
2165
2108
  if (seqStep.outputName) {
2166
2109
  outputs[seqStep.outputName] = outputEntryFromAsyncResult({
@@ -2206,7 +2149,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2206
2149
  statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
2207
2150
  statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2208
2151
  statusPayload.steps[flatIndex].acceptance = singleResult.acceptance;
2209
- statusPayload.steps[flatIndex].resourceLimitExceeded = singleResult.resourceLimitExceeded;
2210
2152
  if (stepTokens) {
2211
2153
  statusPayload.steps[flatIndex].tokens = stepTokens;
2212
2154
  statusPayload.totalTokens = { ...previousCumulativeTokens };
@@ -2223,7 +2165,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2223
2165
  exitCode: singleResult.exitCode,
2224
2166
  durationMs: stepEndTime - stepStartTime,
2225
2167
  tokens: stepTokens,
2226
- resourceLimitExceeded: singleResult.resourceLimitExceeded,
2227
2168
  }));
2228
2169
  if (singleResult.completionGuardTriggered) {
2229
2170
  const event = buildControlEvent({
@@ -2260,11 +2201,12 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2260
2201
  }
2261
2202
 
2262
2203
  const resultMode = config.resultMode ?? statusPayload.mode;
2263
- const agentName = flatSteps.length === 1
2264
- ? flatSteps[0].agent
2204
+ const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
2205
+ const agentName = finalFlatAgents.length === 1
2206
+ ? finalFlatAgents[0]!
2265
2207
  : resultMode === "parallel"
2266
- ? `parallel:${flatSteps.map((s) => s.agent).join("+")}`
2267
- : `chain:${flatSteps.map((s) => s.agent).join("->")}`;
2208
+ ? `parallel:${finalFlatAgents.join("+")}`
2209
+ : `chain:${finalFlatAgents.join("->")}`;
2268
2210
  let sessionFile: string | undefined;
2269
2211
  let shareUrl: string | undefined;
2270
2212
  let gistUrl: string | undefined;
@@ -2370,7 +2312,6 @@ async function runSubagent(config: SubagentRunConfig): Promise<void> {
2370
2312
  structuredOutputPath: r.structuredOutputPath,
2371
2313
  structuredOutputSchemaPath: r.structuredOutputSchemaPath,
2372
2314
  acceptance: r.acceptance,
2373
- resourceLimitExceeded: r.resourceLimitExceeded,
2374
2315
  })),
2375
2316
  outputs,
2376
2317
  workflowGraph: statusPayload.workflowGraph,