pi-subagents 0.27.0 → 0.29.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 (33) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +16 -15
  3. package/package.json +1 -1
  4. package/skills/pi-subagents/SKILL.md +3 -6
  5. package/src/agents/agent-management.ts +10 -6
  6. package/src/agents/agent-selection.ts +2 -0
  7. package/src/agents/agents.ts +303 -6
  8. package/src/agents/chain-serializer.ts +4 -9
  9. package/src/extension/doctor.ts +4 -3
  10. package/src/extension/fanout-child.ts +0 -1
  11. package/src/extension/index.ts +1 -4
  12. package/src/extension/schemas.ts +31 -28
  13. package/src/intercom/intercom-bridge.ts +11 -1
  14. package/src/runs/background/async-execution.ts +20 -7
  15. package/src/runs/background/run-status.ts +1 -7
  16. package/src/runs/background/subagent-runner.ts +73 -146
  17. package/src/runs/foreground/chain-execution.ts +61 -13
  18. package/src/runs/foreground/execution.ts +28 -172
  19. package/src/runs/foreground/subagent-executor.ts +25 -40
  20. package/src/runs/shared/acceptance.ts +605 -22
  21. package/src/runs/shared/completion-guard.ts +3 -26
  22. package/src/runs/shared/model-fallback.ts +38 -0
  23. package/src/runs/shared/parallel-utils.ts +6 -8
  24. package/src/runs/shared/subagent-prompt-runtime.ts +3 -2
  25. package/src/shared/atomic-json.ts +68 -11
  26. package/src/shared/settings.ts +1 -0
  27. package/src/shared/types.ts +8 -32
  28. package/src/shared/utils.ts +2 -1
  29. package/src/tui/render.ts +1 -11
  30. package/src/runs/shared/acceptance-contract.ts +0 -291
  31. package/src/runs/shared/acceptance-evaluation.ts +0 -221
  32. package/src/runs/shared/acceptance-finalization.ts +0 -161
  33. package/src/runs/shared/acceptance-reports.ts +0 -127
@@ -59,12 +59,13 @@ import {
59
59
  MAX_CONCURRENCY,
60
60
  resolveChildMaxSubagentDepth,
61
61
  } from "../../shared/types.ts";
62
- import { resolveModelCandidate } from "../shared/model-fallback.ts";
62
+ import { resolveSubagentModelOverride } from "../shared/model-fallback.ts";
63
63
  import { validateFileOnlyOutputMode } from "../shared/single-output.ts";
64
64
  import { buildWorkflowGraphSnapshot } from "../shared/workflow-graph.ts";
65
65
  import { ChainOutputValidationError, outputEntryFromResult, resolveOutputReferences, validateChainOutputBindings } from "../shared/chain-outputs.ts";
66
66
  import { createStructuredOutputRuntime } from "../shared/structured-output.ts";
67
67
  import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection, type DynamicCollectedResult } from "../shared/dynamic-fanout.ts";
68
+ import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, resolveEffectiveAcceptance } from "../shared/acceptance.ts";
68
69
  import type { ChainOutputMap } from "../../shared/types.ts";
69
70
 
70
71
  interface ChainExecutionDetailsInput {
@@ -231,9 +232,12 @@ async function runParallelChainTasks(input: ParallelChainRunInput): Promise<Sing
231
232
  taskStr = prefix + taskStr + suffix;
232
233
 
233
234
  const taskAgentConfig = input.agents.find((agent) => agent.name === task.agent);
234
- const effectiveModel =
235
- (task.model ? resolveModelCandidate(task.model, input.availableModels, input.ctx.model?.provider) : null)
236
- ?? resolveModelCandidate(taskAgentConfig?.model, input.availableModels, input.ctx.model?.provider);
235
+ const effectiveModel = resolveSubagentModelOverride(
236
+ task.model ?? taskAgentConfig?.model,
237
+ input.ctx.model,
238
+ input.availableModels,
239
+ input.ctx.model?.provider,
240
+ );
237
241
  const maxSubagentDepth = resolveChildMaxSubagentDepth(input.maxSubagentDepth, taskAgentConfig?.maxSubagentDepth);
238
242
 
239
243
  const taskCwd = input.worktreeSetup
@@ -750,11 +754,6 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
750
754
  if (worktreeSetup) cleanupWorktrees(worktreeSetup);
751
755
  }
752
756
  } else if (isDynamicParallelStep(step)) {
753
- if (Object.hasOwn(step, "acceptance")) {
754
- const message = `Dynamic fanout step ${stepIndex + 1} does not support group-level acceptance; set acceptance on the child template instead.`;
755
- dynamicGroupStatuses[stepIndex] = { status: "failed", error: message };
756
- return buildChainExecutionErrorResult(message, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
757
- }
758
757
  let materialized: ReturnType<typeof materializeDynamicParallelStep>;
759
758
  try {
760
759
  materialized = materializeDynamicParallelStep(step, outputs, stepIndex, { maxItems: params.dynamicFanoutMaxItems });
@@ -788,6 +787,30 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
788
787
  stepIndex,
789
788
  };
790
789
  dynamicGroupStatuses[stepIndex] = { status: "completed" };
790
+ if (step.acceptance !== undefined) {
791
+ const effectiveGroupAcceptance = resolveEffectiveAcceptance({
792
+ explicit: step.acceptance,
793
+ agentName: step.parallel.agent,
794
+ task: step.parallel.task ?? originalTask,
795
+ mode: "chain",
796
+ dynamicGroup: true,
797
+ });
798
+ const groupAcceptance = await evaluateAcceptance({
799
+ acceptance: effectiveGroupAcceptance,
800
+ output: "",
801
+ report: aggregateAcceptanceReport({
802
+ results: [],
803
+ notes: "Dynamic fanout produced 0 results.",
804
+ }),
805
+ cwd: cwd ?? ctx.cwd,
806
+ });
807
+ dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
808
+ const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
809
+ if (groupAcceptanceFailure) {
810
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
811
+ return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex }));
812
+ }
813
+ }
791
814
  prev = "Dynamic fanout produced 0 results.";
792
815
  continue;
793
816
  }
@@ -919,6 +942,28 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
919
942
  stepIndex,
920
943
  };
921
944
  dynamicGroupStatuses[stepIndex] = { status: "completed" };
945
+ const effectiveGroupAcceptance = resolveEffectiveAcceptance({
946
+ explicit: step.acceptance,
947
+ agentName: step.parallel.agent,
948
+ task: step.parallel.task ?? originalTask,
949
+ mode: "chain",
950
+ dynamicGroup: true,
951
+ });
952
+ const groupAcceptance = await evaluateAcceptance({
953
+ acceptance: effectiveGroupAcceptance,
954
+ output: "",
955
+ report: aggregateAcceptanceReport({
956
+ results: parallelResults,
957
+ notes: `Dynamic fanout collected ${collected.length} result(s) into ${step.collect.as}.`,
958
+ }),
959
+ cwd: cwd ?? ctx.cwd,
960
+ });
961
+ dynamicGroupStatuses[stepIndex].acceptance = groupAcceptance;
962
+ const groupAcceptanceFailure = acceptanceFailureMessage(groupAcceptance);
963
+ if (groupAcceptanceFailure) {
964
+ dynamicGroupStatuses[stepIndex] = { status: "failed", error: groupAcceptanceFailure, acceptance: groupAcceptance };
965
+ return buildChainExecutionErrorResult(groupAcceptanceFailure, makeDetailsInput({ currentStepIndex: stepIndex, currentFlatIndex: globalTaskIndex - dynamicParallelStep.parallel.length }));
966
+ }
922
967
  const taskResults: ParallelTaskResult[] = parallelResults.map((result, i) => ({
923
968
  agent: result.agent,
924
969
  taskIndex: i,
@@ -974,10 +1019,13 @@ export async function executeChain(params: ChainExecutionParams): Promise<ChainE
974
1019
  const cleanTask = stepTask;
975
1020
  stepTask = prefix + stepTask + suffix;
976
1021
 
977
- const effectiveModel =
978
- tuiOverride?.model
979
- ?? (seqStep.model ? resolveModelCandidate(seqStep.model, availableModels, ctx.model?.provider) : null)
980
- ?? resolveModelCandidate(agentConfig.model, availableModels, ctx.model?.provider);
1022
+ const effectiveModel = tuiOverride?.model
1023
+ ?? resolveSubagentModelOverride(
1024
+ seqStep.model ?? agentConfig.model,
1025
+ ctx.model,
1026
+ availableModels,
1027
+ ctx.model?.provider,
1028
+ );
981
1029
 
982
1030
  const outputPath = typeof behavior.output === "string"
983
1031
  ? (path.isAbsolute(behavior.output) ? behavior.output : path.join(chainDir, behavior.output))
@@ -3,9 +3,7 @@
3
3
  */
4
4
 
5
5
  import { spawn } from "node:child_process";
6
- import { existsSync, mkdtempSync, unlinkSync } from "node:fs";
7
- import * as os from "node:os";
8
- import * as path from "node:path";
6
+ import { existsSync, unlinkSync } from "node:fs";
9
7
  import type { Message } from "@earendil-works/pi-ai";
10
8
  import type { AgentConfig } from "../../agents/agents.ts";
11
9
  import {
@@ -15,13 +13,10 @@ import {
15
13
  writeMetadata,
16
14
  } from "../../shared/artifacts.ts";
17
15
  import {
18
- type AcceptanceFinalizationTurn,
19
- type AcceptanceLedger,
20
16
  type AgentProgress,
21
17
  type ArtifactPaths,
22
18
  type ControlEvent,
23
19
  type ModelAttempt,
24
- type ResolvedAcceptanceConfig,
25
20
  type RunSyncOptions,
26
21
  type SingleResult,
27
22
  type Usage,
@@ -46,7 +41,7 @@ import {
46
41
  extractTextFromContent,
47
42
  } from "../../shared/utils.ts";
48
43
  import { buildSkillInjection, resolveSkillsWithFallback } from "../../agents/skills.ts";
49
- import { evaluateCompletionMutationGuard, resolveCompletionPolicy, type CompletionPolicy } from "../shared/completion-guard.ts";
44
+ import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
50
45
  import { getPiSpawnCommand } from "../shared/pi-spawn.ts";
51
46
  import { createJsonlWriter } from "../../shared/jsonl-writer.ts";
52
47
  import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
@@ -69,20 +64,7 @@ import {
69
64
  shouldEscalateMutatingFailures,
70
65
  summarizeRecentMutatingFailures,
71
66
  } 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";
67
+ import { acceptanceFailureMessage, evaluateAcceptance, formatAcceptancePrompt, resolveEffectiveAcceptance, stripAcceptanceReport } from "../shared/acceptance.ts";
86
68
 
87
69
  const artifactOutputByResult = new WeakMap<SingleResult, string>();
88
70
  const acceptanceOutputByResult = new WeakMap<SingleResult, string>();
@@ -166,11 +148,10 @@ async function runSingleAttempt(
166
148
  attemptNotes: string[];
167
149
  outputSnapshot?: SingleOutputSnapshot;
168
150
  originalTask?: string;
169
- completionPolicy: CompletionPolicy;
170
151
  },
171
152
  ): Promise<SingleResult> {
172
153
  const modelArg = applyThinkingSuffix(model, agent.thinking);
173
- const { args, env: sharedEnv, tempDir } = buildPiArgs({
154
+ const { args, env: sharedEnv, tempDir } = buildPiArgs({
174
155
  baseArgs: ["--mode", "json", "-p"],
175
156
  task,
176
157
  sessionEnabled: shared.sessionEnabled,
@@ -194,10 +175,10 @@ async function runSingleAttempt(
194
175
  childIndex: options.index ?? 0,
195
176
  parentEventSink: options.nestedRoute?.eventSink,
196
177
  parentControlInbox: options.nestedRoute?.controlInbox,
197
- parentRootRunId: options.nestedRoute?.rootRunId,
198
- parentCapabilityToken: options.nestedRoute?.capabilityToken,
199
- structuredOutput: options.structuredOutput,
200
- });
178
+ parentRootRunId: options.nestedRoute?.rootRunId,
179
+ parentCapabilityToken: options.nestedRoute?.capabilityToken,
180
+ structuredOutput: options.structuredOutput,
181
+ });
201
182
 
202
183
  const result: SingleResult = {
203
184
  agent: agent.name,
@@ -728,9 +709,9 @@ async function runSingleAttempt(
728
709
  durationMs: progress.durationMs,
729
710
  };
730
711
 
731
- const acceptanceOutput = getFinalOutput(result.messages);
732
- let fullOutput = stripAcceptanceReport(acceptanceOutput);
733
- const completionGuard = result.exitCode === 0 && !result.error && shared.completionPolicy === "mutation-guard"
712
+ const acceptanceOutput = getFinalOutput(result.messages);
713
+ let fullOutput = stripAcceptanceReport(acceptanceOutput);
714
+ const completionGuard = result.exitCode === 0 && !result.error && agent.completionGuard !== false
734
715
  ? evaluateCompletionMutationGuard({
735
716
  agent: agent.name,
736
717
  task: shared.originalTask ?? task,
@@ -739,8 +720,7 @@ async function runSingleAttempt(
739
720
  mcpDirectTools: agent.mcpDirectTools,
740
721
  })
741
722
  : undefined;
742
- const completionGuardTriggered = completionGuard?.triggered === true && !observedMutationAttempt;
743
- if (completionGuardTriggered) {
723
+ if (completionGuard?.triggered && !observedMutationAttempt) {
744
724
  result.exitCode = 1;
745
725
  result.error = "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes.";
746
726
  progress.status = "failed";
@@ -756,17 +736,17 @@ async function runSingleAttempt(
756
736
  reason: "completion_guard",
757
737
  }));
758
738
  }
759
- if (options.outputPath && result.exitCode === 0) {
760
- const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
761
- fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
762
- result.savedOutputPath = resolvedOutput.savedPath;
763
- result.outputSaveError = resolvedOutput.saveError;
764
- if (resolvedOutput.savedPath) {
765
- result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
766
- }
739
+ if (options.outputPath && result.exitCode === 0) {
740
+ const resolvedOutput = resolveSingleOutput(options.outputPath, fullOutput, shared.outputSnapshot);
741
+ fullOutput = stripAcceptanceReport(resolvedOutput.fullOutput);
742
+ result.savedOutputPath = resolvedOutput.savedPath;
743
+ result.outputSaveError = resolvedOutput.saveError;
744
+ if (resolvedOutput.savedPath) {
745
+ result.outputReference = formatSavedOutputReference(resolvedOutput.savedPath, fullOutput);
746
+ }
767
747
  }
768
- artifactOutputByResult.set(result, fullOutput);
769
- acceptanceOutputByResult.set(result, acceptanceOutput);
748
+ artifactOutputByResult.set(result, fullOutput);
749
+ acceptanceOutputByResult.set(result, acceptanceOutput);
770
750
  result.outputMode = options.outputMode ?? "inline";
771
751
  result.finalOutput = options.outputMode === "file-only" && result.savedOutputPath && result.outputReference
772
752
  ? result.outputReference.message
@@ -789,99 +769,6 @@ async function runSingleAttempt(
789
769
  return result;
790
770
  }
791
771
 
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
-
885
772
  /**
886
773
  * Run a subagent synchronously (blocking until complete)
887
774
  */
@@ -926,10 +813,6 @@ export async function runSync(
926
813
  dynamic: options.acceptanceContext?.dynamic,
927
814
  dynamicGroup: options.acceptanceContext?.dynamicGroup,
928
815
  });
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
816
  const acceptancePrompt = formatAcceptancePrompt(effectiveAcceptance);
934
817
  const taskWithAcceptance = acceptancePrompt ? `${task}\n${acceptancePrompt}` : task;
935
818
  const sessionEnabled = Boolean(options.sessionFile || options.sessionDir) || shareEnabled;
@@ -994,14 +877,6 @@ export async function runSync(
994
877
  attemptNotes,
995
878
  outputSnapshot,
996
879
  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
- }),
1005
880
  });
1006
881
  lastResult = result;
1007
882
  sumUsage(aggregateUsage, result.usage);
@@ -1091,33 +966,14 @@ export async function runSync(
1091
966
  if (sessionFile) result.sessionFile = sessionFile;
1092
967
  }
1093
968
 
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,
969
+ result.acceptance = await evaluateAcceptance({
1111
970
  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(", ")}` } : {}),
971
+ output: acceptanceOutputByResult.get(result) ?? result.finalOutput ?? "",
972
+ cwd: options.cwd ?? runtimeCwd,
1116
973
  });
1117
- }
1118
- const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
1119
- stripAcceptanceReportsFromMessages(result.messages);
1120
- if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted) {
974
+ const acceptanceFailure = acceptanceFailureMessage(result.acceptance);
975
+ stripAcceptanceReportsFromMessages(result.messages);
976
+ if (acceptanceFailure && result.acceptance.explicit && result.exitCode === 0 && !result.detached && !result.interrupted) {
1121
977
  result.exitCode = 1;
1122
978
  result.error = result.error ? `${result.error}\n${acceptanceFailure}` : acceptanceFailure;
1123
979
  if (result.progress) {
@@ -13,7 +13,7 @@ import { handleManagementAction } from "../../agents/agent-management.ts";
13
13
  import { buildDoctorReport } from "../../extension/doctor.ts";
14
14
  import { clearPendingForegroundControlNotices } from "../../extension/control-notices.ts";
15
15
  import { runSync } from "./execution.ts";
16
- import { resolveModelCandidate } from "../shared/model-fallback.ts";
16
+ import { resolveModelCandidate, resolveSubagentModelOverride } from "../shared/model-fallback.ts";
17
17
  import { aggregateParallelOutputs } from "../shared/parallel-utils.ts";
18
18
  import { recordRun } from "../shared/run-history.ts";
19
19
  import {
@@ -53,7 +53,6 @@ import { resolveSubagentRunId, type ResolvedSubagentRunId } from "../background/
53
53
  import { formatNestedRunStatusLines } from "../shared/nested-render.ts";
54
54
  import { inspectSubagentStatus } from "../background/run-status.ts";
55
55
  import { applyForceTopLevelAsyncOverride } from "../background/top-level-async.ts";
56
- import { validateAcceptanceInput } from "../shared/acceptance.ts";
57
56
  import {
58
57
  cleanupWorktrees,
59
58
  createWorktrees,
@@ -650,6 +649,7 @@ async function resumeAsyncRun(input: {
650
649
  cwd: input.requestCwd,
651
650
  currentSessionId: input.deps.state.currentSessionId,
652
651
  currentModelProvider: input.ctx.model?.provider,
652
+ currentModel: input.ctx.model,
653
653
  },
654
654
  cwd: effectiveCwd,
655
655
  maxOutput: input.params.maxOutput,
@@ -691,6 +691,19 @@ function resultSummaryForIntercom(result: SingleResult): string {
691
691
  return output || result.error || "(no output)";
692
692
  }
693
693
 
694
+ function formatFailedSingleRunOutput(result: SingleResult, displayOutput: string): string {
695
+ const error = result.error || "Failed";
696
+ const output = displayOutput.trim();
697
+ const lines = [error];
698
+ if (output && output !== error.trim()) {
699
+ lines.push("", "Output:", output);
700
+ }
701
+ if (result.artifactPaths?.outputPath) {
702
+ lines.push("", `Output artifact: ${result.artifactPaths.outputPath}`);
703
+ }
704
+ return lines.join("\n");
705
+ }
706
+
694
707
  function createForegroundControlNotifier(data: Pick<ExecutionContextData, "controlConfig" | "intercomBridge">, deps: Pick<ExecutorDeps, "pi">): (event: ControlEvent) => void {
695
708
  return (event) => emitControlNotification({
696
709
  pi: deps.pi,
@@ -761,36 +774,6 @@ async function maybeBuildForegroundIntercomReceipt(input: {
761
774
  };
762
775
  }
763
776
 
764
- function validationErrorResult(mode: Details["mode"], text: string): AgentToolResult<Details> {
765
- return { content: [{ type: "text", text }], isError: true, details: { mode, results: [] } };
766
- }
767
-
768
- function validateAcceptanceForExecution(params: SubagentParamsLike): AgentToolResult<Details> | null {
769
- const topLevelErrors = validateAcceptanceInput(params.acceptance);
770
- if (topLevelErrors.length > 0) return validationErrorResult("single", topLevelErrors.join(" "));
771
- for (const [index, task] of (params.tasks ?? []).entries()) {
772
- const errors = validateAcceptanceInput(task.acceptance, `tasks[${index}].acceptance`);
773
- if (errors.length > 0) return validationErrorResult("parallel", errors.join(" "));
774
- }
775
- for (const [stepIndex, step] of (params.chain ?? []).entries()) {
776
- if (isParallelStep(step)) {
777
- if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on static parallel groups; set acceptance on each parallel task.`);
778
- for (const [taskIndex, task] of step.parallel.entries()) {
779
- const errors = validateAcceptanceInput(task.acceptance, `chain[${stepIndex}].parallel[${taskIndex}].acceptance`);
780
- if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
781
- }
782
- } else if (isDynamicParallelStep(step)) {
783
- if (Object.hasOwn(step, "acceptance")) return validationErrorResult("chain", `chain[${stepIndex}].acceptance is not supported on dynamic fanout groups; set acceptance on chain[${stepIndex}].parallel.acceptance for each materialized child.`);
784
- const errors = validateAcceptanceInput(step.parallel.acceptance, `chain[${stepIndex}].parallel.acceptance`);
785
- if (errors.length > 0) return validationErrorResult("chain", errors.join(" "));
786
- } else {
787
- const stepErrors = validateAcceptanceInput(step.acceptance, `chain[${stepIndex}].acceptance`);
788
- if (stepErrors.length > 0) return validationErrorResult("chain", stepErrors.join(" "));
789
- }
790
- }
791
- return null;
792
- }
793
-
794
777
  function validateExecutionInput(
795
778
  params: SubagentParamsLike,
796
779
  agents: AgentConfig[],
@@ -799,9 +782,6 @@ function validateExecutionInput(
799
782
  hasSingle: boolean,
800
783
  allowClarifyTaskPrompt: boolean,
801
784
  ): AgentToolResult<Details> | null {
802
- const acceptanceError = validateAcceptanceForExecution(params);
803
- if (acceptanceError) return acceptanceError;
804
-
805
785
  if (Number(hasChain) + Number(hasTasks) + Number(hasSingle) !== 1) {
806
786
  return {
807
787
  content: [
@@ -1116,6 +1096,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1116
1096
  cwd: ctx.cwd,
1117
1097
  currentSessionId: deps.state.currentSessionId!,
1118
1098
  currentModelProvider: ctx.model?.provider,
1099
+ currentModel: ctx.model,
1119
1100
  };
1120
1101
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
1121
1102
  const currentMaxSubagentDepth = resolveCurrentMaxSubagentDepth(deps.config.maxSubagentDepth);
@@ -1126,7 +1107,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1126
1107
  if (hasTasks && params.tasks) {
1127
1108
  const agentConfigs = params.tasks.map((task) => agents.find((agent) => agent.name === task.agent));
1128
1109
  const modelOverrides = params.tasks.map((task, index) =>
1129
- resolveModelCandidate(task.model ?? agentConfigs[index]?.model, availableModels, currentProvider),
1110
+ resolveSubagentModelOverride(task.model ?? agentConfigs[index]?.model, ctx.model, availableModels, currentProvider),
1130
1111
  );
1131
1112
  const skillOverrides = params.tasks.map((task) => normalizeSkillInput(task.skill));
1132
1113
  const parallelTasks = params.tasks.map((task, index) => ({
@@ -1213,7 +1194,7 @@ function runAsyncPath(data: ExecutionContextData, deps: ExecutorDeps): AgentTool
1213
1194
  const normalizedSkills = normalizeSkillInput(params.skill);
1214
1195
  const skills = normalizedSkills === false ? [] : normalizedSkills;
1215
1196
  const maxSubagentDepth = resolveChildMaxSubagentDepth(currentMaxSubagentDepth, a.maxSubagentDepth);
1216
- const modelOverride = resolveModelCandidate((params.model as string | undefined) ?? a.model, availableModels, currentProvider);
1197
+ const modelOverride = resolveSubagentModelOverride((params.model as string | undefined) ?? a.model, ctx.model, availableModels, currentProvider);
1217
1198
  return executeAsyncSingle(id, {
1218
1199
  agent: params.agent!,
1219
1200
  task: params.context === "fork" ? wrapForkTask(params.task ?? "") : (params.task ?? ""),
@@ -1314,6 +1295,7 @@ async function runChainPath(data: ExecutionContextData, deps: ExecutorDeps): Pro
1314
1295
  cwd: ctx.cwd,
1315
1296
  currentSessionId: deps.state.currentSessionId!,
1316
1297
  currentModelProvider: ctx.model?.provider,
1298
+ currentModel: ctx.model,
1317
1299
  };
1318
1300
  const asyncChain = wrapChainTasksForFork(chainResult.requestedAsync.chain, params.context);
1319
1301
  return executeAsyncChain(id, {
@@ -1669,7 +1651,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1669
1651
  ...(task.model ? { model: task.model } : {}),
1670
1652
  }));
1671
1653
  const modelOverrides: (string | undefined)[] = tasks.map((_, i) =>
1672
- resolveModelCandidate(behaviorOverrides[i]?.model ?? agentConfigs[i]?.model, availableModels, currentProvider),
1654
+ resolveSubagentModelOverride(behaviorOverrides[i]?.model ?? agentConfigs[i]?.model, ctx.model, availableModels, currentProvider),
1673
1655
  );
1674
1656
 
1675
1657
  if (params.clarify === true && ctx.hasUI) {
@@ -1730,6 +1712,7 @@ async function runParallelPath(data: ExecutionContextData, deps: ExecutorDeps):
1730
1712
  cwd: ctx.cwd,
1731
1713
  currentSessionId: deps.state.currentSessionId!,
1732
1714
  currentModelProvider: ctx.model?.provider,
1715
+ currentModel: ctx.model,
1733
1716
  };
1734
1717
  const parallelTasks = tasks.map((t, i) => {
1735
1718
  const taskText = params.context === "fork" ? wrapForkTask(taskTexts[i]!) : taskTexts[i]!;
@@ -1952,8 +1935,9 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
1952
1935
  const currentProvider = ctx.model?.provider;
1953
1936
  const availableModels: ModelInfo[] = ctx.modelRegistry.getAvailable().map(toModelInfo);
1954
1937
  let task = params.task ?? "";
1955
- let modelOverride: string | undefined = resolveModelCandidate(
1938
+ let modelOverride: string | undefined = resolveSubagentModelOverride(
1956
1939
  (params.model as string | undefined) ?? agentConfig.model,
1940
+ ctx.model,
1957
1941
  availableModels,
1958
1942
  currentProvider,
1959
1943
  );
@@ -2010,6 +1994,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2010
1994
  cwd: ctx.cwd,
2011
1995
  currentSessionId: deps.state.currentSessionId!,
2012
1996
  currentModelProvider: ctx.model?.provider,
1997
+ currentModel: ctx.model,
2013
1998
  };
2014
1999
  return executeAsyncSingle(id, {
2015
2000
  agent: params.agent!,
@@ -2194,7 +2179,7 @@ async function runSinglePath(data: ExecutionContextData, deps: ExecutorDeps): Pr
2194
2179
 
2195
2180
  if (r.exitCode !== 0)
2196
2181
  return {
2197
- content: [{ type: "text", text: r.error || "Failed" }],
2182
+ content: [{ type: "text", text: formatFailedSingleRunOutput(r, finalizedOutput.displayOutput) }],
2198
2183
  details,
2199
2184
  isError: true,
2200
2185
  };