@duckmind/dm-darwin-x64 0.43.4 → 0.43.8

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 (251) hide show
  1. package/dm +0 -0
  2. package/extensions/.dm-extensions.json +241 -49
  3. package/extensions/dm-9router-ext/package.json +2 -19
  4. package/extensions/dm-9router-ext/src/index.js +5 -0
  5. package/extensions/dm-ask-user/index.js +10 -0
  6. package/extensions/dm-ask-user/package.json +1 -21
  7. package/extensions/dm-cli-anything/package.json +0 -6
  8. package/extensions/dm-context/package.json +2 -12
  9. package/extensions/dm-context/src/context.js +1 -0
  10. package/extensions/dm-context/src/index.js +7 -0
  11. package/extensions/dm-context/src/utils.js +1 -0
  12. package/extensions/dm-cua/package.json +0 -7
  13. package/extensions/dm-fff/package.json +1 -28
  14. package/extensions/dm-fff/src/index.js +15 -0
  15. package/extensions/dm-fff/src/query.js +1 -0
  16. package/extensions/dm-goal/package.json +2 -31
  17. package/extensions/dm-goal/src/goal.js +43 -0
  18. package/extensions/dm-grill-me/index.js +174 -0
  19. package/extensions/dm-grill-me/package.json +1 -26
  20. package/extensions/dm-image2/package.json +0 -7
  21. package/extensions/dm-subagents/package.json +1 -32
  22. package/extensions/dm-subagents/src/agents/agent-management.js +37 -0
  23. package/extensions/dm-subagents/src/agents/agent-memory.js +6 -0
  24. package/extensions/dm-subagents/src/agents/agent-scope.js +1 -0
  25. package/extensions/dm-subagents/src/agents/agent-selection.js +1 -0
  26. package/extensions/dm-subagents/src/agents/agent-serializer.js +7 -0
  27. package/extensions/dm-subagents/src/agents/agents.js +11 -0
  28. package/extensions/dm-subagents/src/agents/chain-serializer.js +12 -0
  29. package/extensions/dm-subagents/src/agents/frontmatter.js +6 -0
  30. package/extensions/dm-subagents/src/agents/identity.js +1 -0
  31. package/extensions/dm-subagents/src/agents/proactive-skills.js +1 -0
  32. package/extensions/dm-subagents/src/agents/skills.js +6 -0
  33. package/extensions/dm-subagents/src/extension/config.js +2 -0
  34. package/extensions/dm-subagents/src/extension/control-notices.js +4 -0
  35. package/extensions/dm-subagents/src/extension/doctor.js +16 -0
  36. package/extensions/dm-subagents/src/extension/fanout-child.js +231 -0
  37. package/extensions/dm-subagents/src/extension/index.js +367 -0
  38. package/extensions/dm-subagents/src/extension/rpc.js +8 -0
  39. package/extensions/dm-subagents/src/extension/schemas.js +1 -0
  40. package/extensions/dm-subagents/src/extension/{tool-description.ts → tool-description.js} +6 -126
  41. package/extensions/dm-subagents/src/intercom/intercom-bridge.js +13 -0
  42. package/extensions/dm-subagents/src/intercom/native-supervisor-channel.js +5 -0
  43. package/extensions/dm-subagents/src/intercom/result-intercom.js +3 -0
  44. package/extensions/dm-subagents/src/profiles/profiles.js +3 -0
  45. package/extensions/dm-subagents/src/runs/background/async-execution.js +46 -0
  46. package/extensions/dm-subagents/src/runs/background/async-job-tracker.js +14 -0
  47. package/extensions/dm-subagents/src/runs/background/async-resume.js +8 -0
  48. package/extensions/dm-subagents/src/runs/background/async-status.js +12 -0
  49. package/extensions/dm-subagents/src/runs/background/chain-append.js +2 -0
  50. package/extensions/dm-subagents/src/runs/background/chain-root-attachment.js +1 -0
  51. package/extensions/dm-subagents/src/runs/background/completion-batcher.js +1 -0
  52. package/extensions/dm-subagents/src/runs/background/completion-dedupe.js +1 -0
  53. package/extensions/dm-subagents/src/runs/background/control-channel.js +1 -0
  54. package/extensions/dm-subagents/src/runs/background/fleet-view.js +17 -0
  55. package/extensions/dm-subagents/src/runs/background/notify.js +3 -0
  56. package/extensions/dm-subagents/src/runs/background/parallel-groups.js +1 -0
  57. package/extensions/dm-subagents/src/runs/background/result-watcher.js +8 -0
  58. package/extensions/dm-subagents/src/runs/background/run-id-resolver.js +4 -0
  59. package/extensions/dm-subagents/src/runs/background/run-status.js +23 -0
  60. package/extensions/dm-subagents/src/runs/background/scheduled-runs.js +4 -0
  61. package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.js +11 -0
  62. package/extensions/dm-subagents/src/runs/background/subagent-runner.js +78 -0
  63. package/extensions/dm-subagents/src/runs/background/top-level-async.js +1 -0
  64. package/extensions/dm-subagents/src/runs/background/wait.js +11 -0
  65. package/extensions/dm-subagents/src/runs/foreground/chain-clarify.js +12 -0
  66. package/extensions/dm-subagents/src/runs/foreground/chain-execution.js +102 -0
  67. package/extensions/dm-subagents/src/runs/foreground/execution.js +51 -0
  68. package/extensions/dm-subagents/src/runs/foreground/subagent-executor.js +228 -0
  69. package/extensions/dm-subagents/src/runs/shared/acceptance.js +3 -0
  70. package/extensions/dm-subagents/src/runs/shared/chain-outputs.js +1 -0
  71. package/extensions/dm-subagents/src/runs/shared/completion-guard.js +3 -0
  72. package/extensions/dm-subagents/src/runs/shared/dm-args.js +1 -0
  73. package/extensions/dm-subagents/src/runs/shared/dm-spawn.js +1 -0
  74. package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.js +1 -0
  75. package/extensions/dm-subagents/src/runs/shared/long-running-guard.js +1 -0
  76. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.js +1 -0
  77. package/extensions/dm-subagents/src/runs/shared/model-fallback.js +1 -0
  78. package/extensions/dm-subagents/src/runs/shared/model-scope.js +1 -0
  79. package/extensions/dm-subagents/src/runs/shared/nested-events.js +8 -0
  80. package/extensions/dm-subagents/src/runs/shared/nested-path.js +1 -0
  81. package/extensions/dm-subagents/src/runs/shared/nested-render.js +1 -0
  82. package/extensions/dm-subagents/src/runs/shared/parallel-utils.js +5 -0
  83. package/extensions/dm-subagents/src/runs/shared/run-history.js +5 -0
  84. package/extensions/dm-subagents/src/runs/shared/single-output.js +14 -0
  85. package/extensions/dm-subagents/src/runs/shared/structured-output.js +1 -0
  86. package/extensions/dm-subagents/src/runs/shared/subagent-control.js +5 -0
  87. package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.js +22 -0
  88. package/extensions/dm-subagents/src/runs/shared/tool-budget.js +1 -0
  89. package/extensions/dm-subagents/src/runs/shared/turn-budget.js +7 -0
  90. package/extensions/dm-subagents/src/runs/shared/workflow-graph.js +1 -0
  91. package/extensions/dm-subagents/src/runs/shared/worktree.js +3 -0
  92. package/extensions/dm-subagents/src/shared/artifacts.js +2 -0
  93. package/extensions/dm-subagents/src/shared/atomic-json.js +1 -0
  94. package/extensions/dm-subagents/src/shared/child-transcript.js +5 -0
  95. package/extensions/dm-subagents/src/shared/file-coalescer.js +1 -0
  96. package/extensions/dm-subagents/src/shared/fork-context.js +6 -0
  97. package/extensions/dm-subagents/src/shared/formatters.js +9 -0
  98. package/extensions/dm-subagents/src/shared/jsonl-writer.js +2 -0
  99. package/extensions/dm-subagents/src/shared/model-info.js +1 -0
  100. package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.js +1 -0
  101. package/extensions/dm-subagents/src/shared/session-identity.js +1 -0
  102. package/extensions/dm-subagents/src/shared/session-tokens.js +2 -0
  103. package/extensions/dm-subagents/src/shared/settings.js +23 -0
  104. package/extensions/dm-subagents/src/shared/status-format.js +1 -0
  105. package/extensions/dm-subagents/src/shared/types.js +8 -0
  106. package/extensions/dm-subagents/src/shared/utils.js +2 -0
  107. package/extensions/dm-subagents/src/slash/prompt-template-bridge.js +1 -0
  108. package/extensions/dm-subagents/src/slash/prompt-workflows.js +7 -0
  109. package/extensions/dm-subagents/src/slash/slash-bridge.js +1 -0
  110. package/extensions/dm-subagents/src/slash/slash-commands.js +38 -0
  111. package/extensions/dm-subagents/src/slash/slash-live-state.js +6 -0
  112. package/extensions/dm-subagents/src/tui/render-helpers.js +1 -0
  113. package/extensions/dm-subagents/src/tui/render.js +4 -0
  114. package/extensions/dm-tasks/package.json +1 -30
  115. package/extensions/dm-tasks/src/auto-clear.js +1 -0
  116. package/extensions/dm-tasks/src/index.js +212 -0
  117. package/extensions/dm-tasks/src/process-tracker.js +1 -0
  118. package/extensions/dm-tasks/src/task-store.js +1 -0
  119. package/extensions/dm-tasks/src/tasks-config.js +1 -0
  120. package/extensions/dm-tasks/src/types.js +0 -0
  121. package/extensions/dm-tasks/src/ui/settings-menu.js +1 -0
  122. package/extensions/dm-tasks/src/ui/task-widget.js +1 -0
  123. package/extensions/dm-usage/index.js +9 -0
  124. package/extensions/dm-usage/package.json +1 -18
  125. package/extensions/greedysearch-dm/index.js +20 -0
  126. package/extensions/greedysearch-dm/package.json +1 -22
  127. package/extensions/greedysearch-dm/src/formatters/results.js +8 -0
  128. package/extensions/greedysearch-dm/src/formatters/sources.js +1 -0
  129. package/extensions/greedysearch-dm/src/formatters/synthesis.js +1 -0
  130. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.js +20 -0
  131. package/extensions/greedysearch-dm/src/tools/shared.js +9 -0
  132. package/extensions/greedysearch-dm/src/types.js +0 -0
  133. package/extensions/greedysearch-dm/src/utils/helpers.js +1 -0
  134. package/package.json +1 -1
  135. package/extensions/dm-9router-ext/src/index.ts +0 -541
  136. package/extensions/dm-ask-user/index.ts +0 -857
  137. package/extensions/dm-context/src/context.ts +0 -158
  138. package/extensions/dm-context/src/index.ts +0 -439
  139. package/extensions/dm-context/src/utils.ts +0 -6
  140. package/extensions/dm-fff/src/index.ts +0 -1023
  141. package/extensions/dm-fff/src/query.ts +0 -87
  142. package/extensions/dm-goal/src/goal.ts +0 -1073
  143. package/extensions/dm-grill-me/index.ts +0 -1085
  144. package/extensions/dm-subagents/src/agents/agent-management.ts +0 -1052
  145. package/extensions/dm-subagents/src/agents/agent-memory.ts +0 -254
  146. package/extensions/dm-subagents/src/agents/agent-scope.ts +0 -6
  147. package/extensions/dm-subagents/src/agents/agent-selection.ts +0 -25
  148. package/extensions/dm-subagents/src/agents/agent-serializer.ts +0 -121
  149. package/extensions/dm-subagents/src/agents/agents.ts +0 -1553
  150. package/extensions/dm-subagents/src/agents/chain-serializer.ts +0 -277
  151. package/extensions/dm-subagents/src/agents/frontmatter.ts +0 -93
  152. package/extensions/dm-subagents/src/agents/identity.ts +0 -30
  153. package/extensions/dm-subagents/src/agents/proactive-skills.ts +0 -191
  154. package/extensions/dm-subagents/src/agents/skills.ts +0 -729
  155. package/extensions/dm-subagents/src/extension/config.ts +0 -39
  156. package/extensions/dm-subagents/src/extension/control-notices.ts +0 -92
  157. package/extensions/dm-subagents/src/extension/doctor.ts +0 -214
  158. package/extensions/dm-subagents/src/extension/fanout-child.ts +0 -172
  159. package/extensions/dm-subagents/src/extension/index.ts +0 -656
  160. package/extensions/dm-subagents/src/extension/rpc.ts +0 -369
  161. package/extensions/dm-subagents/src/extension/schemas.ts +0 -309
  162. package/extensions/dm-subagents/src/intercom/intercom-bridge.ts +0 -180
  163. package/extensions/dm-subagents/src/intercom/native-supervisor-channel.ts +0 -519
  164. package/extensions/dm-subagents/src/intercom/result-intercom.ts +0 -377
  165. package/extensions/dm-subagents/src/profiles/profiles.ts +0 -637
  166. package/extensions/dm-subagents/src/runs/background/async-execution.ts +0 -1065
  167. package/extensions/dm-subagents/src/runs/background/async-job-tracker.ts +0 -441
  168. package/extensions/dm-subagents/src/runs/background/async-resume.ts +0 -391
  169. package/extensions/dm-subagents/src/runs/background/async-status.ts +0 -395
  170. package/extensions/dm-subagents/src/runs/background/chain-append.ts +0 -282
  171. package/extensions/dm-subagents/src/runs/background/chain-root-attachment.ts +0 -191
  172. package/extensions/dm-subagents/src/runs/background/completion-batcher.ts +0 -166
  173. package/extensions/dm-subagents/src/runs/background/completion-dedupe.ts +0 -63
  174. package/extensions/dm-subagents/src/runs/background/control-channel.ts +0 -332
  175. package/extensions/dm-subagents/src/runs/background/fleet-view.ts +0 -515
  176. package/extensions/dm-subagents/src/runs/background/notify.ts +0 -225
  177. package/extensions/dm-subagents/src/runs/background/parallel-groups.ts +0 -45
  178. package/extensions/dm-subagents/src/runs/background/result-watcher.ts +0 -315
  179. package/extensions/dm-subagents/src/runs/background/run-id-resolver.ts +0 -84
  180. package/extensions/dm-subagents/src/runs/background/run-status.ts +0 -434
  181. package/extensions/dm-subagents/src/runs/background/scheduled-runs.ts +0 -514
  182. package/extensions/dm-subagents/src/runs/background/stale-run-reconciler.ts +0 -368
  183. package/extensions/dm-subagents/src/runs/background/subagent-runner.ts +0 -3171
  184. package/extensions/dm-subagents/src/runs/background/top-level-async.ts +0 -13
  185. package/extensions/dm-subagents/src/runs/background/wait.ts +0 -353
  186. package/extensions/dm-subagents/src/runs/foreground/chain-clarify.ts +0 -1333
  187. package/extensions/dm-subagents/src/runs/foreground/chain-execution.ts +0 -1313
  188. package/extensions/dm-subagents/src/runs/foreground/execution.ts +0 -1239
  189. package/extensions/dm-subagents/src/runs/foreground/subagent-executor.ts +0 -3613
  190. package/extensions/dm-subagents/src/runs/shared/acceptance.ts +0 -879
  191. package/extensions/dm-subagents/src/runs/shared/chain-outputs.ts +0 -116
  192. package/extensions/dm-subagents/src/runs/shared/completion-guard.ts +0 -147
  193. package/extensions/dm-subagents/src/runs/shared/dm-args.ts +0 -271
  194. package/extensions/dm-subagents/src/runs/shared/dm-spawn.ts +0 -147
  195. package/extensions/dm-subagents/src/runs/shared/dynamic-fanout.ts +0 -295
  196. package/extensions/dm-subagents/src/runs/shared/long-running-guard.ts +0 -175
  197. package/extensions/dm-subagents/src/runs/shared/mcp-direct-tool-allowlist.ts +0 -365
  198. package/extensions/dm-subagents/src/runs/shared/model-fallback.ts +0 -292
  199. package/extensions/dm-subagents/src/runs/shared/model-scope.ts +0 -128
  200. package/extensions/dm-subagents/src/runs/shared/nested-events.ts +0 -908
  201. package/extensions/dm-subagents/src/runs/shared/nested-path.ts +0 -52
  202. package/extensions/dm-subagents/src/runs/shared/nested-render.ts +0 -115
  203. package/extensions/dm-subagents/src/runs/shared/parallel-utils.ts +0 -198
  204. package/extensions/dm-subagents/src/runs/shared/run-history.ts +0 -60
  205. package/extensions/dm-subagents/src/runs/shared/single-output.ts +0 -180
  206. package/extensions/dm-subagents/src/runs/shared/structured-output.ts +0 -77
  207. package/extensions/dm-subagents/src/runs/shared/subagent-control.ts +0 -223
  208. package/extensions/dm-subagents/src/runs/shared/subagent-prompt-runtime.ts +0 -342
  209. package/extensions/dm-subagents/src/runs/shared/tool-budget.ts +0 -74
  210. package/extensions/dm-subagents/src/runs/shared/turn-budget.ts +0 -52
  211. package/extensions/dm-subagents/src/runs/shared/workflow-graph.ts +0 -206
  212. package/extensions/dm-subagents/src/runs/shared/worktree.ts +0 -600
  213. package/extensions/dm-subagents/src/shared/artifacts.ts +0 -113
  214. package/extensions/dm-subagents/src/shared/atomic-json.ts +0 -86
  215. package/extensions/dm-subagents/src/shared/child-transcript.ts +0 -212
  216. package/extensions/dm-subagents/src/shared/file-coalescer.ts +0 -40
  217. package/extensions/dm-subagents/src/shared/fork-context.ts +0 -194
  218. package/extensions/dm-subagents/src/shared/formatters.ts +0 -133
  219. package/extensions/dm-subagents/src/shared/jsonl-writer.ts +0 -81
  220. package/extensions/dm-subagents/src/shared/model-info.ts +0 -78
  221. package/extensions/dm-subagents/src/shared/post-exit-stdio-guard.ts +0 -85
  222. package/extensions/dm-subagents/src/shared/session-identity.ts +0 -10
  223. package/extensions/dm-subagents/src/shared/session-tokens.ts +0 -44
  224. package/extensions/dm-subagents/src/shared/settings.ts +0 -450
  225. package/extensions/dm-subagents/src/shared/status-format.ts +0 -49
  226. package/extensions/dm-subagents/src/shared/types.ts +0 -1257
  227. package/extensions/dm-subagents/src/shared/utils.ts +0 -554
  228. package/extensions/dm-subagents/src/slash/prompt-template-bridge.ts +0 -420
  229. package/extensions/dm-subagents/src/slash/prompt-workflows.ts +0 -330
  230. package/extensions/dm-subagents/src/slash/slash-bridge.ts +0 -176
  231. package/extensions/dm-subagents/src/slash/slash-commands.ts +0 -1296
  232. package/extensions/dm-subagents/src/slash/slash-live-state.ts +0 -292
  233. package/extensions/dm-subagents/src/tui/render-helpers.ts +0 -80
  234. package/extensions/dm-subagents/src/tui/render.ts +0 -1748
  235. package/extensions/dm-tasks/src/auto-clear.ts +0 -91
  236. package/extensions/dm-tasks/src/index.ts +0 -1145
  237. package/extensions/dm-tasks/src/process-tracker.ts +0 -140
  238. package/extensions/dm-tasks/src/task-store.ts +0 -305
  239. package/extensions/dm-tasks/src/tasks-config.ts +0 -23
  240. package/extensions/dm-tasks/src/types.ts +0 -40
  241. package/extensions/dm-tasks/src/ui/settings-menu.ts +0 -100
  242. package/extensions/dm-tasks/src/ui/task-widget.ts +0 -265
  243. package/extensions/dm-usage/index.ts +0 -1718
  244. package/extensions/greedysearch-dm/index.ts +0 -177
  245. package/extensions/greedysearch-dm/src/formatters/results.ts +0 -163
  246. package/extensions/greedysearch-dm/src/formatters/sources.ts +0 -116
  247. package/extensions/greedysearch-dm/src/formatters/synthesis.ts +0 -87
  248. package/extensions/greedysearch-dm/src/tools/greedy-search-handler.ts +0 -370
  249. package/extensions/greedysearch-dm/src/tools/shared.ts +0 -187
  250. package/extensions/greedysearch-dm/src/types.ts +0 -110
  251. package/extensions/greedysearch-dm/src/utils/helpers.ts +0 -40
@@ -1,3171 +0,0 @@
1
- import { spawn, spawnSync } from "node:child_process";
2
- import * as fs from "node:fs";
3
- import * as path from "node:path";
4
- import { pathToFileURL } from "node:url";
5
- import type { Message } from "@duckmind/dm-ai";
6
- import { writeAtomicJson } from "../../shared/atomic-json.ts";
7
- import { createChildTranscriptWriter, type ChildTranscriptWriter } from "../../shared/child-transcript.ts";
8
- import { consumeInterruptRequest, deliverInterruptRequest, deliverTimeoutRequest, enqueueStepSteer, stepSteerInboxDir, watchAsyncControlInbox, type SteerRequest } from "./control-channel.ts";
9
- import { appendJsonl as appendRawJsonl, getArtifactPaths } from "../../shared/artifacts.ts";
10
- import { PI_CODING_AGENT_PACKAGE, getPiSpawnCommand, resolveInstalledPiPackageRoot } from "../shared/dm-spawn.ts";
11
- import { captureSingleOutputSnapshot, finalizeSingleOutput, formatSavedOutputReference, resolveSingleOutput, type SingleOutputSnapshot } from "../shared/single-output.ts";
12
- import {
13
- type ActivityState,
14
- type ArtifactConfig,
15
- type ArtifactPaths,
16
- type AsyncParallelGroupStatus,
17
- type AsyncStatus,
18
- type ChainOutputMap,
19
- type CostSummary,
20
- type ModelAttempt,
21
- type NestedRouteInfo,
22
- type NestedRunSummary,
23
- type ResolvedControlConfig,
24
- type ResolvedTurnBudget,
25
- type ResolvedToolBudget,
26
- type SubagentRunMode,
27
- type ToolBudgetState,
28
- type TurnBudgetState,
29
- type Usage,
30
- type WorkflowGraphSnapshot,
31
- DEFAULT_MAX_OUTPUT,
32
- type MaxOutputConfig,
33
- SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
34
- truncateOutput,
35
- getSubagentDepthEnv,
36
- } from "../../shared/types.ts";
37
- import {
38
- DEFAULT_CONTROL_CONFIG,
39
- buildControlEvent,
40
- deriveActivityState,
41
- claimControlNotification,
42
- formatControlIntercomMessage,
43
- formatControlNoticeMessage,
44
- } from "../shared/subagent-control.ts";
45
- import {
46
- type RunnerSubagentStep as SubagentStep,
47
- type RunnerStep,
48
- isDynamicRunnerGroup,
49
- isParallelGroup,
50
- flattenSteps,
51
- mapConcurrent,
52
- aggregateParallelOutputs,
53
- MAX_PARALLEL_CONCURRENCY,
54
- DEFAULT_GLOBAL_CONCURRENCY_LIMIT,
55
- Semaphore,
56
- } from "../shared/parallel-utils.ts";
57
- import { applyThinkingSuffix, buildPiArgs, cleanupTempDir } from "../shared/dm-args.ts";
58
- import { outputEntryFromAsyncResult, resolveOutputReferences } from "../shared/chain-outputs.ts";
59
- import { createStructuredOutputRuntime, readStructuredOutput } from "../shared/structured-output.ts";
60
- import { collectDynamicResults, DynamicFanoutError, materializeDynamicParallelStep, validateDynamicCollection } from "../shared/dynamic-fanout.ts";
61
- import { nestedSummaryFromAsyncStatus, projectNestedEvents, resolveNestedAsyncDir, writeNestedEvent } from "../shared/nested-events.ts";
62
- import { formatModelAttemptNote, isRetryableModelFailure } from "../shared/model-fallback.ts";
63
- import { attachPostExitStdioGuard, trySignalChild } from "../../shared/post-exit-stdio-guard.ts";
64
- import { detectSubagentError, extractTextFromContent, extractToolArgsPreview, getFinalOutput, readStatus } from "../../shared/utils.ts";
65
- import { evaluateCompletionMutationGuard } from "../shared/completion-guard.ts";
66
- import {
67
- createMutatingFailureState,
68
- didMutatingToolFail,
69
- isMutatingTool,
70
- nextLongRunningTrigger,
71
- recordMutatingFailure,
72
- resetMutatingFailureState,
73
- resolveCurrentPath,
74
- shouldEscalateMutatingFailures,
75
- summarizeRecentMutatingFailures,
76
- } from "../shared/long-running-guard.ts";
77
- import { parseSessionTokens } from "../../shared/session-tokens.ts";
78
- import type { TokenUsage } from "../../shared/types.ts";
79
- import {
80
- cleanupWorktrees,
81
- createWorktrees,
82
- diffWorktrees,
83
- findWorktreeTaskCwdConflict,
84
- formatWorktreeDiffSummary,
85
- formatWorktreeTaskCwdConflict,
86
- type WorktreeSetup,
87
- } from "../shared/worktree.ts";
88
- import { resolveEffectiveThinking } from "../../shared/model-info.ts";
89
- import { writeInitialProgressFile } from "../../shared/settings.ts";
90
- import { resolveSubagentIntercomTarget } from "../../intercom/intercom-bridge.ts";
91
- import { acceptanceFailureMessage, aggregateAcceptanceReport, evaluateAcceptance, formatAcceptancePrompt, stripAcceptanceReport } from "../shared/acceptance.ts";
92
- import { waitForImportedAsyncRoot } from "./chain-root-attachment.ts";
93
- import { appendRunnerStepsToStatus, consumeChainAppendRequests, countPendingChainAppendRequests } from "./chain-append.ts";
94
- import { appendTurnBudgetSystemPrompt, formatTurnBudgetOutput, initialTurnBudgetState, shouldAbortForTurnBudget, turnBudgetExceededMessage, turnBudgetSoftNote, turnBudgetState } from "../shared/turn-budget.ts";
95
- import { initialToolBudgetState, toolBudgetState } from "../shared/tool-budget.ts";
96
-
97
- interface SubagentRunConfig {
98
- id: string;
99
- steps: RunnerStep[];
100
- resultPath: string;
101
- cwd: string;
102
- placeholder: string;
103
- taskIndex?: number;
104
- totalTasks?: number;
105
- maxOutput?: MaxOutputConfig;
106
- artifactsDir?: string;
107
- artifactConfig?: Partial<ArtifactConfig>;
108
- share?: boolean;
109
- sessionDir?: string;
110
- asyncDir: string;
111
- sessionId?: string | null;
112
- piPackageRoot?: string;
113
- piArgv1?: string;
114
- worktreeSetupHook?: string;
115
- worktreeSetupHookTimeoutMs?: number;
116
- worktreeBaseDir?: string;
117
- controlConfig?: ResolvedControlConfig;
118
- controlIntercomTarget?: string;
119
- childIntercomTargets?: Array<string | undefined>;
120
- resultMode?: SubagentRunMode;
121
- dynamicFanoutMaxItems?: number;
122
- workflowGraph?: WorkflowGraphSnapshot;
123
- nestedRoute?: NestedRouteInfo;
124
- nestedSelf?: { parentRunId: string; parentStepIndex?: number; depth: number; path?: Array<{ runId: string; stepIndex?: number; agent?: string }> };
125
- timeoutMs?: number;
126
- deadlineAt?: number;
127
- turnBudget?: ResolvedTurnBudget;
128
- toolBudget?: ResolvedToolBudget;
129
- /** Global cap on simultaneously-running subagent tasks within this run. */
130
- globalConcurrencyLimit?: number;
131
- }
132
-
133
- interface StepResult {
134
- agent: string;
135
- output: string;
136
- error?: string;
137
- success: boolean;
138
- exitCode?: number | null;
139
- skipped?: boolean;
140
- interrupted?: boolean;
141
- timedOut?: boolean;
142
- turnBudget?: TurnBudgetState;
143
- turnBudgetExceeded?: boolean;
144
- wrapUpRequested?: boolean;
145
- toolBudget?: ToolBudgetState;
146
- toolBudgetBlocked?: boolean;
147
- sessionFile?: string;
148
- intercomTarget?: string;
149
- model?: string;
150
- attemptedModels?: string[];
151
- modelAttempts?: ModelAttempt[];
152
- totalCost?: CostSummary;
153
- artifactPaths?: ArtifactPaths;
154
- truncated?: boolean;
155
- transcriptPath?: string;
156
- transcriptError?: string;
157
- structuredOutput?: unknown;
158
- structuredOutputPath?: string;
159
- structuredOutputSchemaPath?: string;
160
- acceptance?: import("../../shared/types.ts").AcceptanceLedger;
161
- }
162
-
163
- const ASYNC_INTERRUPT_SIGNAL: NodeJS.Signals = process.platform === "win32" ? "SIGBREAK" : "SIGUSR2";
164
- const DEFAULT_MAX_ASYNC_EVENTS_BYTES = 50 * 1024 * 1024;
165
- const ASYNC_EVENTS_MAX_BYTES_ENV = "DM_SUBAGENT_ASYNC_EVENTS_MAX_BYTES";
166
- const TRUNCATED_EVENT_TYPE = "subagent.events.truncated";
167
- const TRUNCATION_MARKER_RESERVE_BYTES = 512;
168
-
169
- interface AsyncEventLogState {
170
- bytes: number;
171
- diagnosticsTruncated: boolean;
172
- }
173
-
174
- const asyncEventLogStates = new Map<string, AsyncEventLogState>();
175
-
176
- function maxAsyncEventsBytes(): number {
177
- const raw = process.env[ASYNC_EVENTS_MAX_BYTES_ENV];
178
- if (!raw) return DEFAULT_MAX_ASYNC_EVENTS_BYTES;
179
- const parsed = Number(raw);
180
- if (!Number.isFinite(parsed) || parsed < 0) return DEFAULT_MAX_ASYNC_EVENTS_BYTES;
181
- return Math.floor(parsed);
182
- }
183
-
184
- function eventLogState(filePath: string): AsyncEventLogState {
185
- let state = asyncEventLogStates.get(filePath);
186
- if (state) return state;
187
- let bytes = 0;
188
- try {
189
- bytes = fs.statSync(filePath).size;
190
- } catch (error) {
191
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
192
- // Diagnostic event accounting is best-effort; writes below are also safe.
193
- }
194
- }
195
- state = { bytes, diagnosticsTruncated: false };
196
- asyncEventLogStates.set(filePath, state);
197
- return state;
198
- }
199
-
200
- function appendJsonl(filePath: string, line: string): void {
201
- try {
202
- appendRawJsonl(filePath, line);
203
- const state = asyncEventLogStates.get(filePath);
204
- if (state) state.bytes += Buffer.byteLength(`${line}\n`, "utf-8");
205
- } catch {
206
- // Async event logging is diagnostic and must not fail the run.
207
- }
208
- }
209
-
210
- function appendDiagnosticJsonl(filePath: string, line: string, droppedEventType?: string): void {
211
- if (!line.trim()) return;
212
- const state = eventLogState(filePath);
213
- if (state.diagnosticsTruncated) return;
214
- const maxBytes = maxAsyncEventsBytes();
215
- const chunkBytes = Buffer.byteLength(`${line}\n`, "utf-8");
216
- const diagnosticBudget = Math.max(0, maxBytes - TRUNCATION_MARKER_RESERVE_BYTES);
217
- if (state.bytes + chunkBytes <= diagnosticBudget) {
218
- appendJsonl(filePath, line);
219
- return;
220
- }
221
-
222
- const marker = JSON.stringify({
223
- type: TRUNCATED_EVENT_TYPE,
224
- ts: Date.now(),
225
- maxBytes,
226
- droppedEventType,
227
- });
228
- if (state.bytes + Buffer.byteLength(`${marker}\n`, "utf-8") <= maxBytes) {
229
- appendJsonl(filePath, marker);
230
- }
231
- state.diagnosticsTruncated = true;
232
- }
233
-
234
- function shouldPersistChildEvent(event: Record<string, unknown>): boolean {
235
- return event.type !== "message_update";
236
- }
237
-
238
- function findLatestSessionFile(sessionDir: string): string | null {
239
- try {
240
- const files = fs
241
- .readdirSync(sessionDir)
242
- .filter((f) => f.endsWith(".jsonl"))
243
- .map((f) => path.join(sessionDir, f));
244
- if (files.length === 0) return null;
245
- files.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
246
- return files[0] ?? null;
247
- } catch {
248
- // Session lookup is optional metadata.
249
- return null;
250
- }
251
- }
252
-
253
- function emptyUsage(): Usage {
254
- return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, turns: 0 };
255
- }
256
-
257
- function tokenUsageFromAttempts(attempts: ModelAttempt[] | undefined): TokenUsage | null {
258
- if (!attempts || attempts.length === 0) return null;
259
- let input = 0;
260
- let output = 0;
261
- for (const attempt of attempts) {
262
- input += attempt.usage?.input ?? 0;
263
- output += attempt.usage?.output ?? 0;
264
- }
265
- const total = input + output;
266
- return total > 0 ? { input, output, total } : null;
267
- }
268
-
269
- function costSummaryFromAttempts(attempts: ModelAttempt[] | undefined): CostSummary | undefined {
270
- if (!attempts || attempts.length === 0) return undefined;
271
- let inputTokens = 0;
272
- let outputTokens = 0;
273
- let costUsd = 0;
274
- for (const attempt of attempts) {
275
- inputTokens += attempt.usage?.input ?? 0;
276
- outputTokens += attempt.usage?.output ?? 0;
277
- costUsd += attempt.usage?.cost ?? 0;
278
- }
279
- return inputTokens > 0 || outputTokens > 0 || costUsd > 0
280
- ? { inputTokens, outputTokens, costUsd }
281
- : undefined;
282
- }
283
-
284
- function appendRecentStepOutput(step: RunnerStatusStep, lines: string[]): void {
285
- const nonEmpty = lines.filter((line) => line.trim());
286
- if (nonEmpty.length === 0) return;
287
- step.recentOutput ??= [];
288
- step.recentOutput.push(...nonEmpty);
289
- if (step.recentOutput.length > 50) {
290
- step.recentOutput.splice(0, step.recentOutput.length - 50);
291
- }
292
- }
293
-
294
- function isTerminalAssistantStop(message: Message): boolean {
295
- const stopReason = (message as { stopReason?: string }).stopReason;
296
- const hasToolCall = Array.isArray(message.content)
297
- && message.content.some((part) => (part as { type?: string }).type === "toolCall");
298
- return stopReason === "stop" && !hasToolCall;
299
- }
300
-
301
- function resetStepLiveDetail(step: RunnerStatusStep): void {
302
- step.currentTool = undefined;
303
- step.currentToolArgs = undefined;
304
- step.currentToolStartedAt = undefined;
305
- step.currentPath = undefined;
306
- step.recentTools = [];
307
- step.recentOutput = [];
308
- }
309
-
310
- interface ChildEventContext {
311
- eventsPath: string;
312
- runId: string;
313
- stepIndex: number;
314
- agent: string;
315
- }
316
-
317
- interface ChildUsage {
318
- input?: number;
319
- inputTokens?: number;
320
- output?: number;
321
- outputTokens?: number;
322
- cacheRead?: number;
323
- cacheWrite?: number;
324
- cost?: { total?: number };
325
- }
326
-
327
- type ChildMessage = Message & {
328
- model?: string;
329
- errorMessage?: string;
330
- usage?: ChildUsage;
331
- };
332
-
333
- interface ChildEvent {
334
- type?: string;
335
- message?: ChildMessage;
336
- toolName?: string;
337
- args?: Record<string, unknown>;
338
- }
339
-
340
- interface RunPiStreamingResult {
341
- stderr: string;
342
- exitCode: number | null;
343
- messages: Message[];
344
- usage: Usage;
345
- model?: string;
346
- error?: string;
347
- finalOutput: string;
348
- interrupted?: boolean;
349
- timedOut?: boolean;
350
- turnBudget?: TurnBudgetState;
351
- turnBudgetExceeded?: boolean;
352
- wrapUpRequested?: boolean;
353
- toolBudget?: ToolBudgetState;
354
- toolBudgetBlocked?: boolean;
355
- observedMutationAttempt?: boolean;
356
- }
357
-
358
- function runPiStreaming(
359
- args: string[],
360
- cwd: string,
361
- outputFile: string,
362
- env?: Record<string, string | undefined>,
363
- piPackageRoot?: string,
364
- piArgv1?: string,
365
- maxSubagentDepth?: number,
366
- childEventContext?: ChildEventContext,
367
- registerInterrupt?: (interrupt: (() => void) | undefined) => void,
368
- onChildEvent?: (event: ChildEvent) => void,
369
- transcriptWriter?: ChildTranscriptWriter,
370
- registerTimeout?: (interrupt: (() => void) | undefined) => void,
371
- timeoutMessage?: string,
372
- registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void,
373
- ): Promise<RunPiStreamingResult> {
374
- return new Promise((resolve) => {
375
- const outputStream = fs.createWriteStream(outputFile, { flags: "w" });
376
- const spawnEnv = { ...process.env, ...(env ?? {}), ...getSubagentDepthEnv(maxSubagentDepth) };
377
- const spawnSpec = getPiSpawnCommand(args, {
378
- ...(piPackageRoot ? { piPackageRoot } : {}),
379
- ...(piArgv1 ? { argv1: piArgv1 } : {}),
380
- });
381
- const child = spawn(spawnSpec.command, spawnSpec.args, {
382
- cwd,
383
- stdio: ["ignore", "pipe", "pipe"],
384
- env: spawnEnv,
385
- windowsHide: true,
386
- });
387
- let stderr = "";
388
- let stdoutBuf = "";
389
- let stderrBuf = "";
390
- const messages: Message[] = [];
391
- const usage = emptyUsage();
392
- let model: string | undefined;
393
- let error: string | undefined;
394
- let assistantError: string | undefined;
395
- let interrupted = false;
396
- let timedOut = false;
397
- let turnBudgetExceeded = false;
398
- let turnBudgetMessage: string | undefined;
399
- let turnBudget: TurnBudgetState | undefined;
400
- let observedMutationAttempt = false;
401
- const rawStdoutLines: string[] = [];
402
-
403
- const writeOutputLine = (line: string) => {
404
- if (!line.trim()) return;
405
- outputStream.write(`${line}\n`);
406
- };
407
-
408
- const writeOutputText = (text: string) => {
409
- for (const line of text.split("\n")) {
410
- writeOutputLine(line);
411
- }
412
- };
413
-
414
- const appendChildEvent = (event: Record<string, unknown>) => {
415
- if (!childEventContext) return;
416
- if (!shouldPersistChildEvent(event)) return;
417
- appendDiagnosticJsonl(childEventContext.eventsPath, JSON.stringify({
418
- ...event,
419
- subagentSource: "child",
420
- subagentRunId: childEventContext.runId,
421
- subagentStepIndex: childEventContext.stepIndex,
422
- subagentAgent: childEventContext.agent,
423
- observedAt: Date.now(),
424
- }), typeof event.type === "string" ? event.type : undefined);
425
- };
426
-
427
- const appendChildLine = (type: "subagent.child.stdout" | "subagent.child.stderr", line: string) => {
428
- appendChildEvent({ type, line });
429
- if (type === "subagent.child.stdout") transcriptWriter?.writeStdoutLine(line);
430
- else transcriptWriter?.writeStderrLine(line);
431
- };
432
-
433
- const processStdoutLine = (line: string) => {
434
- if (!line.trim()) return;
435
- let event: ChildEvent;
436
- try {
437
- event = JSON.parse(line) as ChildEvent;
438
- } catch {
439
- rawStdoutLines.push(line);
440
- writeOutputLine(line);
441
- appendChildLine("subagent.child.stdout", line);
442
- return;
443
- }
444
-
445
- appendChildEvent(event);
446
- transcriptWriter?.writeChildEvent(event);
447
- onChildEvent?.(event);
448
-
449
- if (event.type === "tool_execution_start" && event.toolName) {
450
- observedMutationAttempt = observedMutationAttempt || isMutatingTool(event.toolName, event.args);
451
- const toolArgs = extractToolArgsPreview(event.args ?? {});
452
- writeOutputLine(toolArgs ? `${event.toolName}: ${toolArgs}` : event.toolName);
453
- return;
454
- }
455
-
456
- if ((event.type === "message_end" || event.type === "tool_result_end") && event.message) {
457
- messages.push(event.message);
458
- const text = extractTextFromContent(event.message.content);
459
- if (text) writeOutputText(text);
460
-
461
- if (event.type !== "message_end" || event.message.role !== "assistant") return;
462
- if (event.message.model) model = event.message.model;
463
- if (event.message.errorMessage) assistantError = event.message.errorMessage;
464
- const eventUsage = event.message.usage;
465
- if (eventUsage) {
466
- usage.turns++;
467
- usage.input += eventUsage.input ?? eventUsage.inputTokens ?? 0;
468
- usage.output += eventUsage.output ?? eventUsage.outputTokens ?? 0;
469
- usage.cacheRead += eventUsage.cacheRead ?? 0;
470
- usage.cacheWrite += eventUsage.cacheWrite ?? 0;
471
- usage.cost += eventUsage.cost?.total ?? 0;
472
- }
473
- if (isTerminalAssistantStop(event.message)) {
474
- if (!event.message.errorMessage && extractTextFromContent(event.message.content).trim()) assistantError = undefined;
475
- cleanTerminalAssistantStopReceived ||= !event.message.errorMessage;
476
- startFinalDrain();
477
- }
478
- }
479
- };
480
-
481
- const processStderrText = (text: string) => {
482
- stderr += text;
483
- stderrBuf += text;
484
- outputStream.write(text);
485
- if (!childEventContext) return;
486
- const lines = stderrBuf.split("\n");
487
- stderrBuf = lines.pop() || "";
488
- for (const line of lines) {
489
- if (!line.trim()) continue;
490
- appendChildLine("subagent.child.stderr", line);
491
- }
492
- };
493
-
494
- // Guard both cases that can leave the parent waiting on `close` forever:
495
- // a lingering stdio holder after `exit`, or a child that never exits.
496
- const FINAL_STOP_GRACE_MS = 1000;
497
- const HARD_KILL_MS = 3000;
498
- const TIMEOUT_HARD_KILL_MS = 3000;
499
- let childExited = false;
500
- let forcedTerminationSignal = false;
501
- let cleanTerminalAssistantStopReceived = false;
502
- let finalDrainTimer: NodeJS.Timeout | undefined;
503
- let finalHardKillTimer: NodeJS.Timeout | undefined;
504
- let timeoutHardKillTimer: NodeJS.Timeout | undefined;
505
- let turnBudgetTerminationTimer: NodeJS.Timeout | undefined;
506
- let turnBudgetHardKillTimer: NodeJS.Timeout | undefined;
507
- let settled = false;
508
- const clearStdioGuard = attachPostExitStdioGuard(child, { idleMs: 2000, hardMs: 8000 });
509
- child.stdout.on("data", (chunk: Buffer) => {
510
- const text = chunk.toString();
511
- stdoutBuf += text;
512
- const lines = stdoutBuf.split("\n");
513
- stdoutBuf = lines.pop() || "";
514
- for (const line of lines) processStdoutLine(line);
515
- });
516
-
517
- child.stderr.on("data", (chunk: Buffer) => {
518
- processStderrText(chunk.toString());
519
- });
520
- registerInterrupt?.(() => {
521
- if (settled || timedOut) return;
522
- interrupted = true;
523
- if (!error) error = "Interrupted. Waiting for explicit next action.";
524
- trySignalChild(child, "SIGINT");
525
- setTimeout(() => {
526
- if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
527
- }, 1000).unref?.();
528
- });
529
- registerTimeout?.(() => {
530
- if (settled || timedOut) return;
531
- timedOut = true;
532
- interrupted = false;
533
- error = timeoutMessage ?? "Subagent timed out.";
534
- trySignalChild(child, "SIGTERM");
535
- timeoutHardKillTimer = setTimeout(() => {
536
- if (!settled) trySignalChild(child, "SIGKILL");
537
- }, TIMEOUT_HARD_KILL_MS);
538
- timeoutHardKillTimer.unref?.();
539
- });
540
- registerTurnBudgetAbort?.((message, state) => {
541
- if (settled || timedOut || turnBudgetExceeded) return;
542
- turnBudgetExceeded = true;
543
- turnBudgetMessage = message;
544
- turnBudget = state;
545
- interrupted = false;
546
- error = message;
547
- trySignalChild(child, "SIGINT");
548
- turnBudgetTerminationTimer = setTimeout(() => {
549
- if (!settled && !timedOut) trySignalChild(child, "SIGTERM");
550
- }, 1000);
551
- turnBudgetTerminationTimer.unref?.();
552
- turnBudgetHardKillTimer = setTimeout(() => {
553
- if (!settled && !timedOut) trySignalChild(child, "SIGKILL");
554
- }, 4000);
555
- turnBudgetHardKillTimer.unref?.();
556
- });
557
- const clearDrainTimers = () => {
558
- if (finalDrainTimer) {
559
- clearTimeout(finalDrainTimer);
560
- finalDrainTimer = undefined;
561
- }
562
- if (finalHardKillTimer) {
563
- clearTimeout(finalHardKillTimer);
564
- finalHardKillTimer = undefined;
565
- }
566
- if (timeoutHardKillTimer) {
567
- clearTimeout(timeoutHardKillTimer);
568
- timeoutHardKillTimer = undefined;
569
- }
570
- if (turnBudgetTerminationTimer) {
571
- clearTimeout(turnBudgetTerminationTimer);
572
- turnBudgetTerminationTimer = undefined;
573
- }
574
- if (turnBudgetHardKillTimer) {
575
- clearTimeout(turnBudgetHardKillTimer);
576
- turnBudgetHardKillTimer = undefined;
577
- }
578
- };
579
- function startFinalDrain(): void {
580
- if (childExited || finalDrainTimer || settled) return;
581
- finalDrainTimer = setTimeout(() => {
582
- if (settled) return;
583
- const termSent = trySignalChild(child, "SIGTERM");
584
- if (!termSent) return;
585
- forcedTerminationSignal = true;
586
- if (!cleanTerminalAssistantStopReceived && !error && !assistantError) {
587
- error = `Subagent process did not exit within ${FINAL_STOP_GRACE_MS}ms after its final message. Forcing termination.`;
588
- }
589
- finalHardKillTimer = setTimeout(() => {
590
- if (settled) return;
591
- forcedTerminationSignal = trySignalChild(child, "SIGKILL") || forcedTerminationSignal;
592
- }, HARD_KILL_MS);
593
- finalHardKillTimer.unref?.();
594
- }, FINAL_STOP_GRACE_MS);
595
- finalDrainTimer.unref?.();
596
- }
597
- child.on("exit", () => {
598
- childExited = true;
599
- clearDrainTimers();
600
- });
601
- child.on("close", (exitCode, signal) => {
602
- settled = true;
603
- registerInterrupt?.(undefined);
604
- registerTimeout?.(undefined);
605
- registerTurnBudgetAbort?.(undefined);
606
- clearDrainTimers();
607
- clearStdioGuard();
608
- if (stdoutBuf.trim()) processStdoutLine(stdoutBuf);
609
- if (stderrBuf.trim()) appendChildLine("subagent.child.stderr", stderrBuf);
610
- outputStream.end();
611
- const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
612
- const finalError = error ?? assistantError;
613
- const forcedDrainAfterFinalSuccess = forcedTerminationSignal && cleanTerminalAssistantStopReceived && !finalError;
614
- resolve({
615
- stderr,
616
- exitCode: timedOut ? 1 : turnBudgetExceeded ? 1 : interrupted || forcedDrainAfterFinalSuccess ? 0 : forcedTerminationSignal || signal ? (exitCode ?? 1) : exitCode,
617
- messages,
618
- usage,
619
- model,
620
- error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : interrupted || forcedDrainAfterFinalSuccess ? undefined : finalError,
621
- finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput,
622
- interrupted,
623
- timedOut,
624
- turnBudget,
625
- turnBudgetExceeded,
626
- wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
627
- observedMutationAttempt,
628
- });
629
- });
630
-
631
- child.on("error", (spawnError) => {
632
- settled = true;
633
- registerInterrupt?.(undefined);
634
- registerTimeout?.(undefined);
635
- registerTurnBudgetAbort?.(undefined);
636
- clearDrainTimers();
637
- clearStdioGuard();
638
- outputStream.end();
639
- const finalOutput = getFinalOutput(messages) || rawStdoutLines.join("\n").trim();
640
- const spawnErrorMessage = spawnError instanceof Error ? spawnError.message : String(spawnError);
641
- resolve({ stderr, exitCode: 1, messages, usage, model, error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? turnBudgetMessage : error ?? assistantError ?? spawnErrorMessage, finalOutput: timedOut && !finalOutput.trim() ? (timeoutMessage ?? "Subagent timed out.") : finalOutput, timedOut, turnBudget, turnBudgetExceeded, wrapUpRequested: turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined, observedMutationAttempt });
642
- });
643
- });
644
- }
645
-
646
- function resolvePiPackageRootFallback(): string {
647
- const root = resolveInstalledPiPackageRoot();
648
- if (root) return root;
649
- throw new Error(`Could not resolve ${PI_CODING_AGENT_PACKAGE} package root`);
650
- }
651
-
652
- async function exportSessionHtml(sessionFile: string, outputDir: string, piPackageRoot?: string): Promise<string> {
653
- const pkgRoot = piPackageRoot ?? resolvePiPackageRootFallback();
654
- const exportModulePath = path.join(pkgRoot, "dist", "core", "export-html", "index.js");
655
- const moduleUrl = pathToFileURL(exportModulePath).href;
656
- const mod = await import(moduleUrl);
657
- const exportFromFile = (mod as { exportFromFile?: (inputPath: string, options?: { outputPath?: string }) => string })
658
- .exportFromFile;
659
- if (typeof exportFromFile !== "function") {
660
- throw new Error("exportFromFile not available");
661
- }
662
- const outputPath = path.join(outputDir, `${path.basename(sessionFile, ".jsonl")}.html`);
663
- return exportFromFile(sessionFile, { outputPath });
664
- }
665
-
666
- function createShareLink(htmlPath: string): { shareUrl: string; gistUrl: string } | { error: string } {
667
- try {
668
- const auth = spawnSync("gh", ["auth", "status"], { encoding: "utf-8" });
669
- if (auth.status !== 0) {
670
- return { error: "GitHub CLI is not logged in. Run 'gh auth login' first." };
671
- }
672
- } catch {
673
- return { error: "GitHub CLI (gh) is not installed." };
674
- }
675
-
676
- try {
677
- const result = spawnSync("gh", ["gist", "create", htmlPath], { encoding: "utf-8" });
678
- if (result.status !== 0) {
679
- const err = (result.stderr || "").trim() || "Failed to create gist.";
680
- return { error: err };
681
- }
682
- const gistUrl = (result.stdout || "").trim();
683
- const gistId = gistUrl.split("/").pop();
684
- if (!gistId) return { error: "Failed to parse gist ID." };
685
- const shareUrl = `https://shittycodingagent.ai/session/?${gistId}`;
686
- return { shareUrl, gistUrl };
687
- } catch (err) {
688
- return { error: String(err) };
689
- }
690
- }
691
-
692
- function formatDuration(ms: number): string {
693
- if (ms < 1000) return `${ms}ms`;
694
- if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`;
695
- const minutes = Math.floor(ms / 60000);
696
- const seconds = Math.floor((ms % 60000) / 1000);
697
- return `${minutes}m${seconds}s`;
698
- }
699
-
700
- function writeRunLog(
701
- logPath: string,
702
- input: {
703
- id: string;
704
- mode: SubagentRunMode;
705
- cwd: string;
706
- startedAt: number;
707
- endedAt: number;
708
- steps: Array<{
709
- agent: string;
710
- status: string;
711
- durationMs?: number;
712
- }>;
713
- summary: string;
714
- truncated: boolean;
715
- artifactsDir?: string;
716
- sessionFile?: string;
717
- shareUrl?: string;
718
- shareError?: string;
719
- },
720
- ): void {
721
- const lines: string[] = [];
722
- lines.push(`# Subagent run ${input.id}`);
723
- lines.push("");
724
- lines.push(`- **Mode:** ${input.mode}`);
725
- lines.push(`- **CWD:** ${input.cwd}`);
726
- lines.push(`- **Started:** ${new Date(input.startedAt).toISOString()}`);
727
- lines.push(`- **Ended:** ${new Date(input.endedAt).toISOString()}`);
728
- lines.push(`- **Duration:** ${formatDuration(input.endedAt - input.startedAt)}`);
729
- if (input.sessionFile) lines.push(`- **Session:** ${input.sessionFile}`);
730
- if (input.shareUrl) lines.push(`- **Share:** ${input.shareUrl}`);
731
- if (input.shareError) lines.push(`- **Share error:** ${input.shareError}`);
732
- if (input.artifactsDir) lines.push(`- **Artifacts:** ${input.artifactsDir}`);
733
- lines.push("");
734
- lines.push("## Steps");
735
- lines.push("| Step | Agent | Status | Duration |");
736
- lines.push("| --- | --- | --- | --- |");
737
- input.steps.forEach((step, i) => {
738
- const duration = step.durationMs !== undefined ? formatDuration(step.durationMs) : "-";
739
- lines.push(`| ${i + 1} | ${step.agent} | ${step.status} | ${duration} |`);
740
- });
741
- lines.push("");
742
- lines.push("## Summary");
743
- if (input.truncated) {
744
- lines.push("_Output truncated_");
745
- lines.push("");
746
- }
747
- lines.push(input.summary.trim() || "(no output)");
748
- lines.push("");
749
- fs.writeFileSync(logPath, lines.join("\n"), "utf-8");
750
- }
751
-
752
- /** Context for running a single step */
753
- interface SingleStepContext {
754
- previousOutput: string;
755
- outputs?: ChainOutputMap;
756
- placeholder: string;
757
- cwd: string;
758
- sessionEnabled: boolean;
759
- sessionDir?: string;
760
- artifactsDir?: string;
761
- artifactConfig?: Partial<ArtifactConfig>;
762
- id: string;
763
- flatIndex: number;
764
- flatStepCount: number;
765
- outputFile: string;
766
- steerInboxDir?: string;
767
- transcriptPath?: string;
768
- piPackageRoot?: string;
769
- piArgv1?: string;
770
- registerInterrupt?: (interrupt: (() => void) | undefined) => void;
771
- registerTimeout?: (interrupt: (() => void) | undefined) => void;
772
- registerTurnBudgetAbort?: (abort: ((message: string, state?: TurnBudgetState) => void) | undefined) => void;
773
- timeoutSignal?: AbortSignal;
774
- timeoutMessage?: string;
775
- turnBudget?: ResolvedTurnBudget;
776
- childIntercomTarget?: string;
777
- orchestratorIntercomTarget?: string;
778
- nestedRoute?: NestedRouteInfo;
779
- onAttemptStart?: (attempt: { model?: string; thinking?: string }) => void;
780
- onChildEvent?: (event: ChildEvent) => void;
781
- skipAcceptance?: () => boolean;
782
- }
783
-
784
- /** Run a single DM agent step, returning output and metadata */
785
- async function runSingleStep(
786
- step: SubagentStep,
787
- ctx: SingleStepContext,
788
- ): Promise<{
789
- agent: string;
790
- output: string;
791
- exitCode: number | null;
792
- error?: string;
793
- model?: string;
794
- attemptedModels?: string[];
795
- modelAttempts?: ModelAttempt[];
796
- artifactPaths?: ArtifactPaths;
797
- transcriptPath?: string;
798
- transcriptError?: string;
799
- interrupted?: boolean;
800
- timedOut?: boolean;
801
- turnBudget?: TurnBudgetState;
802
- turnBudgetExceeded?: boolean;
803
- wrapUpRequested?: boolean;
804
- toolBudget?: ToolBudgetState;
805
- toolBudgetBlocked?: boolean;
806
- sessionFile?: string;
807
- intercomTarget?: string;
808
- completionGuardTriggered?: boolean;
809
- structuredOutput?: unknown;
810
- structuredOutputPath?: string;
811
- structuredOutputSchemaPath?: string;
812
- acceptance?: import("../../shared/types.ts").AcceptanceLedger;
813
- }> {
814
- if (step.importAsyncRoot) {
815
- let importTimedOut = false;
816
- ctx.registerTimeout?.(() => {
817
- importTimedOut = true;
818
- let pid: number | undefined;
819
- try {
820
- pid = readStatus(step.importAsyncRoot!.asyncDir)?.pid;
821
- } catch {
822
- pid = undefined;
823
- }
824
- try {
825
- deliverTimeoutRequest({ asyncDir: step.importAsyncRoot!.asyncDir, pid, source: "ancestor-timeout" });
826
- } catch {
827
- // The parent runner's own timeout result is authoritative for the attached step.
828
- }
829
- });
830
- try {
831
- const imported = await waitForImportedAsyncRoot(step.importAsyncRoot, {
832
- shouldAbort: () => importTimedOut || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true,
833
- timeoutMessage: ctx.timeoutMessage,
834
- });
835
- try {
836
- fs.writeFileSync(ctx.outputFile, imported.output, "utf-8");
837
- } catch {
838
- // Output files are observability only for imported roots.
839
- }
840
- const timedOut = importTimedOut || imported.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
841
- return {
842
- agent: imported.agent,
843
- output: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.output,
844
- exitCode: timedOut ? 1 : imported.exitCode,
845
- error: timedOut ? ctx.timeoutMessage ?? "Subagent timed out." : imported.error,
846
- timedOut: timedOut ? true : undefined,
847
- sessionFile: imported.sessionFile,
848
- intercomTarget: imported.intercomTarget,
849
- model: imported.model,
850
- attemptedModels: imported.attemptedModels,
851
- modelAttempts: imported.modelAttempts,
852
- totalCost: imported.totalCost,
853
- structuredOutput: timedOut ? undefined : imported.structuredOutput,
854
- structuredOutputPath: timedOut ? undefined : imported.structuredOutputPath,
855
- structuredOutputSchemaPath: timedOut ? undefined : imported.structuredOutputSchemaPath,
856
- acceptance: timedOut ? undefined : imported.acceptance,
857
- };
858
- } finally {
859
- ctx.registerTimeout?.(undefined);
860
- }
861
- }
862
-
863
- const effectiveStructuredOutput = step.structuredOutput ?? (step.structuredOutputSchema
864
- ? createStructuredOutputRuntime(step.structuredOutputSchema, path.join(path.dirname(ctx.outputFile), "structured-output"))
865
- : undefined);
866
- const placeholderRegex = new RegExp(ctx.placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "g");
867
- let task = step.task.replace(placeholderRegex, () => ctx.previousOutput);
868
- task = resolveOutputReferences(task, ctx.outputs ?? {});
869
- const taskForCompletionGuard = task;
870
- if (step.effectiveAcceptance) {
871
- const acceptancePrompt = formatAcceptancePrompt(step.effectiveAcceptance);
872
- if (acceptancePrompt) task = `${task}\n${acceptancePrompt}`;
873
- }
874
- const sessionEnabled = Boolean(step.sessionFile) || ctx.sessionEnabled;
875
- const sessionDir = step.sessionFile ? undefined : ctx.sessionDir;
876
-
877
- let artifactPaths: ArtifactPaths | undefined;
878
- let transcriptWriter: ChildTranscriptWriter | undefined;
879
- if (ctx.artifactsDir && ctx.artifactConfig?.enabled !== false) {
880
- const index = ctx.flatStepCount > 1 ? ctx.flatIndex : undefined;
881
- artifactPaths = getArtifactPaths(ctx.artifactsDir, ctx.id, step.agent, index);
882
- fs.mkdirSync(ctx.artifactsDir, { recursive: true });
883
- if (ctx.artifactConfig?.includeInput !== false) {
884
- fs.writeFileSync(artifactPaths.inputPath, `# Task for ${step.agent}\n\n${task}`, "utf-8");
885
- }
886
- if (ctx.artifactConfig?.includeTranscript !== false) {
887
- transcriptWriter = createChildTranscriptWriter({
888
- transcriptPath: artifactPaths.transcriptPath,
889
- source: "async",
890
- runId: ctx.id,
891
- agent: step.agent,
892
- childIndex: ctx.flatIndex,
893
- cwd: step.cwd ?? ctx.cwd,
894
- });
895
- }
896
- }
897
- transcriptWriter?.writeInitialUserMessage(task);
898
-
899
- const candidates = step.modelCandidates && step.modelCandidates.length > 0
900
- ? step.modelCandidates
901
- : step.model
902
- ? [step.model]
903
- : [undefined];
904
- const attemptedModels: string[] = [];
905
- const modelAttempts: ModelAttempt[] = [];
906
- const attemptNotes: string[] = [];
907
- const eventsPath = path.join(path.dirname(ctx.outputFile), "events.jsonl");
908
- let finalResult: RunPiStreamingResult | undefined;
909
- let finalOutputSnapshot: SingleOutputSnapshot | undefined;
910
- let completionGuardTriggeredFinal = false;
911
- let turnBudget = ctx.turnBudget ? initialTurnBudgetState(ctx.turnBudget) : undefined;
912
- let toolBudget = step.toolBudget ? initialToolBudgetState(step.toolBudget) : undefined;
913
- let toolBudgetBlocked = false;
914
-
915
- for (let index = 0; index < candidates.length; index++) {
916
- if (ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
917
- const candidate = candidates[index];
918
- ctx.onAttemptStart?.({ model: candidate, thinking: resolveEffectiveThinking(candidate, step.thinking) });
919
- const outputSnapshot = captureSingleOutputSnapshot(step.outputPath);
920
- if (effectiveStructuredOutput) {
921
- try {
922
- if (fs.existsSync(effectiveStructuredOutput.outputPath)) fs.unlinkSync(effectiveStructuredOutput.outputPath);
923
- } catch {
924
- // Missing/stale structured-output files are handled after the child exits.
925
- }
926
- }
927
- const { args, env, tempDir } = buildPiArgs({
928
- parentSessionId: step.parentSessionId,
929
- baseArgs: ["--mode", "json", "-p"],
930
- task,
931
- sessionEnabled,
932
- sessionDir,
933
- sessionFile: step.sessionFile,
934
- model: candidate,
935
- inheritProjectContext: step.inheritProjectContext,
936
- inheritSkills: step.inheritSkills,
937
- requireReadTool: Boolean(step.skills?.length),
938
- tools: step.tools,
939
- extensions: step.extensions,
940
- subagentOnlyExtensions: step.subagentOnlyExtensions,
941
- systemPrompt: appendTurnBudgetSystemPrompt(step.systemPrompt ?? "", ctx.turnBudget),
942
- systemPromptMode: step.systemPromptMode,
943
- mcpDirectTools: step.mcpDirectTools,
944
- cwd: step.cwd ?? ctx.cwd,
945
- promptFileStem: step.agent,
946
- intercomSessionName: ctx.childIntercomTarget,
947
- orchestratorIntercomTarget: ctx.orchestratorIntercomTarget,
948
- runId: ctx.id,
949
- childAgentName: step.agent,
950
- childIndex: ctx.flatIndex,
951
- parentEventSink: ctx.nestedRoute?.eventSink,
952
- parentControlInbox: ctx.nestedRoute?.controlInbox,
953
- parentRootRunId: ctx.nestedRoute?.rootRunId,
954
- parentCapabilityToken: ctx.nestedRoute?.capabilityToken,
955
- steerInboxDir: ctx.steerInboxDir,
956
- structuredOutput: effectiveStructuredOutput,
957
- toolBudget: step.toolBudget,
958
- });
959
- const run = await runPiStreaming(
960
- args,
961
- step.cwd ?? ctx.cwd,
962
- ctx.outputFile,
963
- env,
964
- ctx.piPackageRoot,
965
- ctx.piArgv1,
966
- step.maxSubagentDepth,
967
- { eventsPath, runId: ctx.id, stepIndex: ctx.flatIndex, agent: step.agent },
968
- ctx.registerInterrupt,
969
- ctx.onChildEvent,
970
- transcriptWriter,
971
- ctx.registerTimeout,
972
- ctx.timeoutMessage,
973
- ctx.registerTurnBudgetAbort,
974
- );
975
- if (run.turnBudget) turnBudget = run.turnBudget;
976
- else if (ctx.turnBudget) {
977
- const assistantMessages = run.messages.filter((message) => message.role === "assistant");
978
- const turnCount = assistantMessages.length;
979
- const lastAssistantMessage = assistantMessages.at(-1);
980
- if (turnCount > 0 && turnCount < ctx.turnBudget.maxTurns) {
981
- turnBudget = { ...ctx.turnBudget, outcome: "within-budget", turnCount };
982
- } else if (turnCount >= ctx.turnBudget.maxTurns) {
983
- turnBudget = turnBudgetState(
984
- ctx.turnBudget,
985
- turnCount,
986
- shouldAbortForTurnBudget(ctx.turnBudget, turnCount, lastAssistantMessage ? isTerminalAssistantStop(lastAssistantMessage) : false),
987
- );
988
- }
989
- }
990
- cleanupTempDir(tempDir);
991
-
992
- const hiddenError = run.exitCode === 0 && !run.error ? detectSubagentError(run.messages) : null;
993
- const missingStructuredOutput = effectiveStructuredOutput
994
- ? !fs.existsSync(effectiveStructuredOutput.outputPath)
995
- : false;
996
- const emptyOutputError = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !run.finalOutput.trim() && (!effectiveStructuredOutput || missingStructuredOutput)
997
- ? "Subagent produced no output (possible model cold-start or empty response)."
998
- : undefined;
999
- let structuredOutput: unknown;
1000
- let structuredError: string | undefined;
1001
- if (effectiveStructuredOutput && run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError) {
1002
- const structured = readStructuredOutput({
1003
- schema: effectiveStructuredOutput.schema,
1004
- schemaPath: effectiveStructuredOutput.schemaPath,
1005
- outputPath: effectiveStructuredOutput.outputPath,
1006
- });
1007
- if (structured.error) structuredError = structured.error;
1008
- else structuredOutput = structured.value;
1009
- }
1010
- const completionGuard = run.exitCode === 0 && !run.error && !hiddenError?.hasError && !emptyOutputError && step.completionGuard !== false
1011
- ? evaluateCompletionMutationGuard({
1012
- agent: step.agent,
1013
- task: taskForCompletionGuard,
1014
- messages: run.messages,
1015
- tools: step.tools,
1016
- mcpDirectTools: step.mcpDirectTools,
1017
- })
1018
- : undefined;
1019
- const completionGuardTriggered = completionGuard?.triggered === true && !run.observedMutationAttempt;
1020
- const completionGuardError = completionGuardTriggered
1021
- ? "Subagent completed without making edits for an implementation task.\nIt appears to have returned planning or scratchpad output instead of applying changes."
1022
- : undefined;
1023
- const effectiveExitCode = completionGuardTriggered
1024
- ? 1
1025
- : structuredError
1026
- ? 1
1027
- : hiddenError?.hasError
1028
- ? (hiddenError.exitCode ?? 1)
1029
- : emptyOutputError
1030
- ? 1
1031
- : run.error && run.exitCode === 0
1032
- ? 1
1033
- : run.exitCode;
1034
- const error = completionGuardError
1035
- ?? structuredError
1036
- ?? (hiddenError?.hasError
1037
- ? hiddenError.details
1038
- ? `${hiddenError.errorType} failed (exit ${effectiveExitCode}): ${hiddenError.details}`
1039
- : `${hiddenError.errorType} failed with exit code ${effectiveExitCode}`
1040
- : emptyOutputError ?? (run.error || (run.exitCode !== 0 && run.stderr.trim() ? run.stderr.trim() : undefined)));
1041
- const attempt: ModelAttempt = {
1042
- model: candidate ?? run.model ?? step.model ?? "default",
1043
- success: effectiveExitCode === 0 && !error,
1044
- exitCode: effectiveExitCode,
1045
- error,
1046
- usage: run.usage,
1047
- };
1048
- modelAttempts.push(attempt);
1049
- if (candidate) attemptedModels.push(candidate);
1050
- completionGuardTriggeredFinal = completionGuardTriggered;
1051
- finalOutputSnapshot = outputSnapshot;
1052
- if (step.toolBudget) {
1053
- const toolMessages = run.messages.filter((message) => message.role === "toolResult");
1054
- const blockedMessage = toolMessages.find((message) => extractTextFromContent(message.content).includes("Tool budget hard limit reached"));
1055
- toolBudgetBlocked = Boolean(blockedMessage);
1056
- toolBudget = toolBudgetState(step.toolBudget, toolMessages.length, blockedMessage ? (blockedMessage as { toolName?: string }).toolName : undefined);
1057
- }
1058
- finalResult = { ...run, exitCode: effectiveExitCode, model: candidate ?? run.model, error, structuredOutput } as RunPiStreamingResult & { structuredOutput?: unknown };
1059
- if (run.turnBudgetExceeded) break;
1060
- if (run.timedOut || ctx.timeoutSignal?.aborted || ctx.skipAcceptance?.()) break;
1061
- if (attempt.success || completionGuardTriggered) break;
1062
- if (!isRetryableModelFailure(error) || index === candidates.length - 1) break;
1063
- attemptNotes.push(formatModelAttemptNote(attempt, candidates[index + 1]));
1064
- }
1065
-
1066
- const rawOutput = finalResult?.finalOutput ?? "";
1067
- const outputForPersistence = stripAcceptanceReport(rawOutput);
1068
- const resolvedOutput = step.outputPath && finalResult?.exitCode === 0
1069
- ? resolveSingleOutput(step.outputPath, outputForPersistence, finalOutputSnapshot)
1070
- : { fullOutput: outputForPersistence };
1071
- const output = resolvedOutput.fullOutput;
1072
- const outputReference = resolvedOutput.savedPath ? formatSavedOutputReference(resolvedOutput.savedPath, output) : undefined;
1073
- let outputForSummary = output;
1074
- if (attemptNotes.length > 0) {
1075
- outputForSummary = `${attemptNotes.join("\n")}\n\n${outputForSummary}`.trim();
1076
- }
1077
- if (!finalResult?.timedOut && finalResult?.turnBudgetExceeded && turnBudget) {
1078
- outputForSummary = formatTurnBudgetOutput(turnBudgetExceededMessage(turnBudget, turnBudget.turnCount), outputForSummary);
1079
- } else if (!finalResult?.timedOut && turnBudget?.outcome === "wrap-up-requested") {
1080
- const note = turnBudgetSoftNote(turnBudget, turnBudget.wrapUpRequestedAtTurn ?? turnBudget.turnCount);
1081
- outputForSummary = outputForSummary.trim() ? `${note}\n\n${outputForSummary}` : note;
1082
- }
1083
- const outputForAcceptance = rawOutput;
1084
- const finalizedOutput = finalizeSingleOutput({
1085
- fullOutput: outputForSummary,
1086
- outputPath: step.outputPath,
1087
- outputMode: step.outputMode,
1088
- exitCode: finalResult?.exitCode ?? 1,
1089
- savedPath: resolvedOutput.savedPath,
1090
- outputReference,
1091
- saveError: resolvedOutput.saveError,
1092
- });
1093
- outputForSummary = finalizedOutput.displayOutput;
1094
- const acceptance = step.effectiveAcceptance && !finalResult?.turnBudgetExceeded && !ctx.timeoutSignal?.aborted && !ctx.skipAcceptance?.()
1095
- ? await evaluateAcceptance({
1096
- acceptance: step.effectiveAcceptance,
1097
- output: outputForAcceptance,
1098
- cwd: step.cwd ?? ctx.cwd,
1099
- signal: ctx.timeoutSignal,
1100
- abortMessage: ctx.timeoutMessage ?? "Subagent timed out.",
1101
- })
1102
- : undefined;
1103
- const timedOutAfterAcceptance = finalResult?.timedOut === true || ctx.timeoutSignal?.aborted === true || ctx.skipAcceptance?.() === true;
1104
- const turnBudgetExceeded = finalResult?.turnBudgetExceeded === true;
1105
- const effectiveAcceptance = timedOutAfterAcceptance || turnBudgetExceeded ? undefined : acceptance;
1106
- const acceptanceFailure = effectiveAcceptance ? acceptanceFailureMessage(effectiveAcceptance) : undefined;
1107
- const acceptanceCanFailRun = acceptanceFailure && effectiveAcceptance?.explicit && (finalResult?.exitCode ?? 1) === 0 && !finalResult?.interrupted && !timedOutAfterAcceptance && !turnBudgetExceeded;
1108
- const effectiveFinalExitCode = timedOutAfterAcceptance || turnBudgetExceeded ? 1 : acceptanceCanFailRun ? 1 : finalResult?.exitCode ?? 1;
1109
- const effectiveFinalError = timedOutAfterAcceptance
1110
- ? ctx.timeoutMessage ?? "Subagent timed out."
1111
- : turnBudgetExceeded
1112
- ? finalResult?.error ?? (turnBudget ? turnBudgetExceededMessage(turnBudget, turnBudget.turnCount) : "Subagent exceeded turn budget.")
1113
- : acceptanceCanFailRun
1114
- ? (finalResult?.error ? `${finalResult.error}\n${acceptanceFailure}` : acceptanceFailure)
1115
- : finalResult?.error;
1116
-
1117
- if (artifactPaths && ctx.artifactConfig?.enabled !== false) {
1118
- if (ctx.artifactConfig?.includeOutput !== false) {
1119
- fs.writeFileSync(artifactPaths.outputPath, output, "utf-8");
1120
- }
1121
- if (ctx.artifactConfig?.includeMetadata !== false) {
1122
- fs.writeFileSync(
1123
- artifactPaths.metadataPath,
1124
- JSON.stringify({
1125
- runId: ctx.id,
1126
- agent: step.agent,
1127
- task,
1128
- exitCode: effectiveFinalExitCode,
1129
- model: finalResult?.model,
1130
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1131
- modelAttempts,
1132
- ...(transcriptWriter ? { transcriptPath: artifactPaths.transcriptPath } : {}),
1133
- transcriptError: transcriptWriter?.getError(),
1134
- skills: step.skills,
1135
- timestamp: Date.now(),
1136
- }, null, 2),
1137
- "utf-8",
1138
- );
1139
- }
1140
- }
1141
-
1142
- return {
1143
- agent: step.agent,
1144
- output: outputForSummary,
1145
- exitCode: effectiveFinalExitCode,
1146
- error: effectiveFinalError,
1147
- sessionFile: step.sessionFile,
1148
- intercomTarget: ctx.childIntercomTarget,
1149
- model: finalResult?.model,
1150
- attemptedModels: attemptedModels.length > 0 ? attemptedModels : undefined,
1151
- modelAttempts,
1152
- totalCost: costSummaryFromAttempts(modelAttempts),
1153
- artifactPaths,
1154
- transcriptPath: transcriptWriter ? artifactPaths?.transcriptPath : undefined,
1155
- transcriptError: transcriptWriter?.getError(),
1156
- interrupted: timedOutAfterAcceptance || turnBudgetExceeded ? false : finalResult?.interrupted,
1157
- timedOut: timedOutAfterAcceptance ? true : finalResult?.timedOut,
1158
- turnBudget,
1159
- turnBudgetExceeded: turnBudgetExceeded || undefined,
1160
- wrapUpRequested: finalResult?.wrapUpRequested || turnBudget?.outcome === "wrap-up-requested" || turnBudgetExceeded || undefined,
1161
- toolBudget,
1162
- toolBudgetBlocked: toolBudgetBlocked || undefined,
1163
- completionGuardTriggered: completionGuardTriggeredFinal,
1164
- structuredOutput: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : (finalResult as (RunPiStreamingResult & { structuredOutput?: unknown }) | undefined)?.structuredOutput,
1165
- structuredOutputPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.outputPath,
1166
- structuredOutputSchemaPath: timedOutAfterAcceptance || turnBudgetExceeded ? undefined : effectiveStructuredOutput?.schemaPath,
1167
- acceptance: effectiveAcceptance,
1168
- };
1169
- }
1170
-
1171
- type RunnerStatusStep = NonNullable<AsyncStatus["steps"]>[number] & {
1172
- exitCode?: number | null;
1173
- };
1174
-
1175
- type RunnerStatusPayload = Omit<AsyncStatus, "steps" | "parallelGroups" | "pid" | "cwd" | "currentStep" | "chainStepCount" | "lastUpdate"> & {
1176
- pid: number;
1177
- cwd: string;
1178
- currentStep: number;
1179
- chainStepCount: number;
1180
- parallelGroups: AsyncParallelGroupStatus[];
1181
- steps: RunnerStatusStep[];
1182
- lastUpdate: number;
1183
- artifactsDir?: string;
1184
- shareUrl?: string;
1185
- gistUrl?: string;
1186
- shareError?: string;
1187
- error?: string;
1188
- };
1189
-
1190
- function markParallelGroupSetupFailure(input: {
1191
- statusPayload: RunnerStatusPayload;
1192
- results: StepResult[];
1193
- group: Extract<RunnerStep, { parallel: SubagentStep[] }>;
1194
- groupStartFlatIndex: number;
1195
- setupError: string;
1196
- failedAt: number;
1197
- statusPath: string;
1198
- eventsPath: string;
1199
- asyncDir: string;
1200
- runId: string;
1201
- stepIndex: number;
1202
- }): void {
1203
- for (let taskIndex = 0; taskIndex < input.group.parallel.length; taskIndex++) {
1204
- const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
1205
- input.statusPayload.steps[flatTaskIndex].status = "failed";
1206
- input.statusPayload.steps[flatTaskIndex].startedAt = input.failedAt;
1207
- input.statusPayload.steps[flatTaskIndex].endedAt = input.failedAt;
1208
- input.statusPayload.steps[flatTaskIndex].durationMs = 0;
1209
- input.statusPayload.steps[flatTaskIndex].exitCode = 1;
1210
- input.results.push({ agent: input.group.parallel[taskIndex].agent, output: input.setupError, success: false, exitCode: 1, sessionFile: input.group.parallel[taskIndex].sessionFile });
1211
- }
1212
- input.statusPayload.currentStep = input.groupStartFlatIndex;
1213
- input.statusPayload.lastUpdate = input.failedAt;
1214
- input.statusPayload.outputFile = path.join(input.asyncDir, `output-${input.groupStartFlatIndex}.log`);
1215
- writeAtomicJson(input.statusPath, input.statusPayload);
1216
- appendJsonl(input.eventsPath, JSON.stringify({
1217
- type: "subagent.parallel.completed",
1218
- ts: input.failedAt,
1219
- runId: input.runId,
1220
- stepIndex: input.stepIndex,
1221
- success: false,
1222
- }));
1223
- }
1224
-
1225
- function markParallelGroupRunning(input: {
1226
- statusPayload: RunnerStatusPayload;
1227
- group: Extract<RunnerStep, { parallel: SubagentStep[] }>;
1228
- groupStartFlatIndex: number;
1229
- groupStartTime: number;
1230
- statusPath: string;
1231
- eventsPath: string;
1232
- asyncDir: string;
1233
- runId: string;
1234
- stepIndex: number;
1235
- }): void {
1236
- for (let taskIndex = 0; taskIndex < input.group.parallel.length; taskIndex++) {
1237
- const flatTaskIndex = input.groupStartFlatIndex + taskIndex;
1238
- input.statusPayload.steps[flatTaskIndex].status = "pending";
1239
- input.statusPayload.steps[flatTaskIndex].startedAt = undefined;
1240
- input.statusPayload.steps[flatTaskIndex].endedAt = undefined;
1241
- input.statusPayload.steps[flatTaskIndex].durationMs = undefined;
1242
- input.statusPayload.steps[flatTaskIndex].lastActivityAt = undefined;
1243
- input.statusPayload.steps[flatTaskIndex].activityState = undefined;
1244
- input.statusPayload.steps[flatTaskIndex].error = undefined;
1245
- }
1246
- input.statusPayload.currentStep = input.groupStartFlatIndex;
1247
- input.statusPayload.activityState = undefined;
1248
- input.statusPayload.lastActivityAt = input.groupStartTime;
1249
- input.statusPayload.lastUpdate = input.groupStartTime;
1250
- input.statusPayload.outputFile = path.join(input.asyncDir, `output-${input.groupStartFlatIndex}.log`);
1251
- writeAtomicJson(input.statusPath, input.statusPayload);
1252
- appendJsonl(input.eventsPath, JSON.stringify({
1253
- type: "subagent.parallel.started",
1254
- ts: input.groupStartTime,
1255
- runId: input.runId,
1256
- stepIndex: input.stepIndex,
1257
- agents: input.group.parallel.map((task) => task.agent),
1258
- count: input.group.parallel.length,
1259
- }));
1260
- }
1261
-
1262
- function prepareParallelTaskRun(
1263
- task: SubagentStep,
1264
- cwd: string,
1265
- worktreeSetup: WorktreeSetup | undefined,
1266
- taskIndex: number,
1267
- ): { taskForRun: SubagentStep; taskCwd: string } {
1268
- if (!worktreeSetup) return { taskForRun: task, taskCwd: cwd };
1269
- return {
1270
- taskForRun: { ...task, cwd: undefined },
1271
- taskCwd: worktreeSetup.worktrees[taskIndex]!.agentCwd,
1272
- };
1273
- }
1274
-
1275
- function appendParallelWorktreeSummary(
1276
- previousOutput: string,
1277
- worktreeSetup: WorktreeSetup | undefined,
1278
- asyncDir: string,
1279
- stepIndex: number,
1280
- group: Extract<RunnerStep, { parallel: SubagentStep[] }>,
1281
- ): string {
1282
- if (!worktreeSetup) return previousOutput;
1283
- const diffsDir = path.join(asyncDir, "worktree-diffs", `step-${stepIndex}`);
1284
- const diffs = diffWorktrees(worktreeSetup, group.parallel.map((task) => task.agent), diffsDir);
1285
- const diffSummary = formatWorktreeDiffSummary(diffs);
1286
- if (!diffSummary) return previousOutput;
1287
- return `${previousOutput}\n\n${diffSummary}`;
1288
- }
1289
-
1290
- function ensureParallelProgressFile(cwd: string, group: Extract<RunnerStep, { parallel: SubagentStep[] }>): void {
1291
- const progressPath = path.join(cwd, "progress.md");
1292
- if (!group.parallel.some((task) => task.task.includes(`Update progress at: ${progressPath}`))) return;
1293
- writeInitialProgressFile(cwd);
1294
- }
1295
-
1296
- function resolveAsyncStepTranscriptPath(input: {
1297
- artifactsDir?: string;
1298
- artifactConfig?: Partial<ArtifactConfig>;
1299
- runId: string;
1300
- agent: string;
1301
- flatIndex: number;
1302
- flatStepCount: number;
1303
- }): string | undefined {
1304
- if (!input.artifactsDir || input.artifactConfig?.enabled === false || input.artifactConfig?.includeTranscript === false) return undefined;
1305
- return getArtifactPaths(
1306
- input.artifactsDir,
1307
- input.runId,
1308
- input.agent,
1309
- input.flatStepCount > 1 ? input.flatIndex : undefined,
1310
- ).transcriptPath;
1311
- }
1312
-
1313
- type SingleStepResult = Awaited<ReturnType<typeof runSingleStep>>;
1314
-
1315
- async function runSubagent(config: SubagentRunConfig): Promise<void> {
1316
- const { id, steps, resultPath, cwd, placeholder, taskIndex, totalTasks, maxOutput, artifactsDir, artifactConfig } =
1317
- config;
1318
- const globalSemaphore = new Semaphore(config.globalConcurrencyLimit ?? DEFAULT_GLOBAL_CONCURRENCY_LIMIT);
1319
- let previousOutput = "";
1320
- const outputs: ChainOutputMap = {};
1321
- const results: StepResult[] = [];
1322
- const overallStartTime = Date.now();
1323
- const shareEnabled = config.share === true;
1324
- const asyncDir = config.asyncDir;
1325
- const statusPath = path.join(asyncDir, "status.json");
1326
- const eventsPath = path.join(asyncDir, "events.jsonl");
1327
- const logPath = path.join(asyncDir, `subagent-log-${id}.md`);
1328
- const controlConfig = config.controlConfig ?? DEFAULT_CONTROL_CONFIG;
1329
- const activeChildInterrupts = new Map<number, () => void>();
1330
- const activeChildTimeouts = new Map<number, () => void>();
1331
- const activeChildTurnBudgetAborts = new Map<number, (message: string, state?: TurnBudgetState) => void>();
1332
- const pendingStepSteers: SteerRequest[] = [];
1333
- let interrupted = false;
1334
- let currentActivityState: ActivityState | undefined;
1335
- let activityTimer: NodeJS.Timeout | undefined;
1336
- let timeoutTimer: NodeJS.Timeout | undefined;
1337
- let timedOut = false;
1338
- let turnBudgetExceeded = false;
1339
- const timeoutMessage = config.timeoutMs !== undefined ? `Subagent timed out after ${config.timeoutMs}ms.` : undefined;
1340
- const timeoutAbortController = new AbortController();
1341
- let previousCumulativeTokens: TokenUsage = { input: 0, output: 0, total: 0 };
1342
- let latestSessionFile: string | undefined;
1343
-
1344
- const flatSteps = flattenSteps(steps);
1345
- const initialFlatStepCount = flatSteps.length;
1346
- const parallelGroups: Array<{ start: number; count: number; stepIndex: number }> = [];
1347
- const initialStatusSteps: RunnerStatusStep[] = [];
1348
- let flatStepCount = 0;
1349
- for (let stepIndex = 0; stepIndex < steps.length; stepIndex++) {
1350
- const step = steps[stepIndex]!;
1351
- if (isParallelGroup(step)) {
1352
- parallelGroups.push({ start: flatStepCount, count: step.parallel.length, stepIndex });
1353
- for (const task of step.parallel) {
1354
- const taskFlatIndex = flatStepCount;
1355
- const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: taskFlatIndex, flatStepCount: initialFlatStepCount });
1356
- initialStatusSteps.push({
1357
- agent: task.agent,
1358
- phase: task.phase,
1359
- label: task.label,
1360
- outputName: task.outputName,
1361
- structured: task.structured,
1362
- status: "pending",
1363
- ...(task.toolBudget ? { toolBudget: initialToolBudgetState(task.toolBudget) } : {}),
1364
- ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
1365
- ...(transcriptPath ? { transcriptPath } : {}),
1366
- skills: task.skills,
1367
- model: task.model,
1368
- thinking: task.thinking,
1369
- attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
1370
- recentTools: [],
1371
- recentOutput: [],
1372
- });
1373
- flatStepCount++;
1374
- }
1375
- } else if (isDynamicRunnerGroup(step)) {
1376
- parallelGroups.push({ start: flatStepCount, count: 1, stepIndex });
1377
- initialStatusSteps.push({
1378
- agent: `expand:${step.parallel.agent}`,
1379
- phase: step.phase ?? step.parallel.phase,
1380
- label: step.label ?? step.parallel.label ?? `Dynamic fanout (${step.collect.as})`,
1381
- outputName: step.collect.as,
1382
- structured: Boolean(step.collect.outputSchema),
1383
- status: "pending",
1384
- ...(step.parallel.toolBudget ? { toolBudget: initialToolBudgetState(step.parallel.toolBudget) } : {}),
1385
- recentTools: [],
1386
- recentOutput: [],
1387
- });
1388
- flatStepCount++;
1389
- } else {
1390
- const stepFlatIndex = flatStepCount;
1391
- const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: step.agent, flatIndex: stepFlatIndex, flatStepCount: initialFlatStepCount });
1392
- initialStatusSteps.push({
1393
- agent: step.agent,
1394
- phase: step.phase,
1395
- label: step.label,
1396
- outputName: step.outputName,
1397
- structured: step.structured,
1398
- status: "pending",
1399
- ...(step.toolBudget ? { toolBudget: initialToolBudgetState(step.toolBudget) } : {}),
1400
- ...(step.sessionFile ? { sessionFile: step.sessionFile } : {}),
1401
- ...(transcriptPath ? { transcriptPath } : {}),
1402
- skills: step.skills,
1403
- model: step.model,
1404
- thinking: step.thinking,
1405
- attemptedModels: step.modelCandidates && step.modelCandidates.length > 0 ? step.modelCandidates : step.model ? [step.model] : undefined,
1406
- recentTools: [],
1407
- recentOutput: [],
1408
- });
1409
- flatStepCount++;
1410
- }
1411
- }
1412
- const sessionEnabled = Boolean(config.sessionDir)
1413
- || shareEnabled
1414
- || flatSteps.some((step) => Boolean(step.sessionFile));
1415
- const statusPayload: RunnerStatusPayload = {
1416
- lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
1417
- runId: id,
1418
- ...(config.sessionId ? { sessionId: config.sessionId } : {}),
1419
- mode: config.resultMode ?? (flatSteps.length > 1 ? "chain" : "single"),
1420
- state: "running",
1421
- lastActivityAt: overallStartTime,
1422
- startedAt: overallStartTime,
1423
- lastUpdate: overallStartTime,
1424
- ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
1425
- ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
1426
- ...(config.turnBudget ? { turnBudget: initialTurnBudgetState(config.turnBudget) } : {}),
1427
- ...(config.toolBudget ? { toolBudget: initialToolBudgetState(config.toolBudget) } : {}),
1428
- pid: process.pid,
1429
- cwd,
1430
- currentStep: 0,
1431
- chainStepCount: steps.length,
1432
- parallelGroups,
1433
- workflowGraph: config.workflowGraph,
1434
- steps: initialStatusSteps,
1435
- artifactsDir,
1436
- sessionDir: config.sessionDir,
1437
- outputFile: path.join(asyncDir, "output-0.log"),
1438
- };
1439
-
1440
- fs.mkdirSync(asyncDir, { recursive: true });
1441
- writeAtomicJson(statusPath, statusPayload);
1442
- const emitNestedSelfEvent = (type: "subagent.nested.updated" | "subagent.nested.completed"): void => {
1443
- if (!config.nestedRoute || !config.nestedSelf) return;
1444
- try {
1445
- writeNestedEvent(config.nestedRoute, {
1446
- type,
1447
- ts: Date.now(),
1448
- parentRunId: config.nestedSelf.parentRunId,
1449
- parentStepIndex: config.nestedSelf.parentStepIndex,
1450
- child: nestedSummaryFromAsyncStatus(statusPayload, asyncDir, {
1451
- id,
1452
- parentRunId: config.nestedSelf.parentRunId,
1453
- parentStepIndex: config.nestedSelf.parentStepIndex,
1454
- depth: config.nestedSelf.depth,
1455
- path: config.nestedSelf.path,
1456
- mode: statusPayload.mode,
1457
- ts: Date.now(),
1458
- }),
1459
- });
1460
- } catch (error) {
1461
- console.error("Failed to emit nested async status event:", error);
1462
- }
1463
- };
1464
- const refreshWorkflowGraph = (): void => {
1465
- if (!config.workflowGraph) return;
1466
- const graph = structuredClone(statusPayload.workflowGraph ?? config.workflowGraph);
1467
- const normalize = (status: RunnerStatusStep["status"]): "pending" | "running" | "completed" | "failed" | "paused" | "detached" => {
1468
- if (status === "complete" || status === "completed") return "completed";
1469
- if (status === "running" || status === "failed" || status === "paused" || status === "pending") return status;
1470
- return "pending";
1471
- };
1472
- const updateNode = (node: NonNullable<typeof graph.nodes>[number]): void => {
1473
- if (node.flatIndex !== undefined) {
1474
- const step = statusPayload.steps[node.flatIndex];
1475
- if (step) {
1476
- node.status = normalize(step.status);
1477
- node.error = step.error;
1478
- node.acceptanceStatus = step.acceptance?.status;
1479
- }
1480
- if (statusPayload.currentStep === node.flatIndex) graph.currentNodeId = node.id;
1481
- }
1482
- for (const child of node.children ?? []) updateNode(child);
1483
- if (node.children?.length) {
1484
- if (node.children.every((child) => child.status === "completed")) node.status = "completed";
1485
- else if (node.children.some((child) => child.status === "running")) node.status = "running";
1486
- else if (node.children.some((child) => child.status === "failed")) node.status = "failed";
1487
- else if (node.children.some((child) => child.status === "paused")) node.status = "paused";
1488
- }
1489
- if (node.error) node.status = "failed";
1490
- };
1491
- for (const node of graph.nodes) updateNode(node);
1492
- statusPayload.workflowGraph = graph;
1493
- };
1494
- const writeStatusPayload = (): void => {
1495
- refreshWorkflowGraph();
1496
- writeAtomicJson(statusPath, statusPayload);
1497
- emitNestedSelfEvent(statusPayload.state === "running" || statusPayload.state === "queued" ? "subagent.nested.updated" : "subagent.nested.completed");
1498
- };
1499
- const registerStepInterrupt = (flatIndex: number, interrupt: (() => void) | undefined): void => {
1500
- if (!interrupt) {
1501
- activeChildInterrupts.delete(flatIndex);
1502
- return;
1503
- }
1504
- activeChildInterrupts.set(flatIndex, interrupt);
1505
- if (interrupted) interrupt();
1506
- };
1507
- const registerStepTimeout = (flatIndex: number, interrupt: (() => void) | undefined): void => {
1508
- if (!interrupt) {
1509
- activeChildTimeouts.delete(flatIndex);
1510
- return;
1511
- }
1512
- activeChildTimeouts.set(flatIndex, interrupt);
1513
- if (timedOut) interrupt();
1514
- };
1515
- const registerStepTurnBudgetAbort = (flatIndex: number, abort: ((message: string, state?: TurnBudgetState) => void) | undefined): void => {
1516
- if (!abort) {
1517
- activeChildTurnBudgetAborts.delete(flatIndex);
1518
- return;
1519
- }
1520
- activeChildTurnBudgetAborts.set(flatIndex, abort);
1521
- };
1522
- const interruptActiveChildren = (): void => {
1523
- for (const interrupt of [...activeChildInterrupts.values()]) interrupt();
1524
- };
1525
- const timeoutActiveChildren = (): void => {
1526
- for (const interrupt of [...activeChildTimeouts.values()]) interrupt();
1527
- };
1528
- const nestedRuns = function* (children: NestedRunSummary[] | undefined): Generator<NestedRunSummary> {
1529
- for (const child of children ?? []) {
1530
- yield child;
1531
- yield* nestedRuns(child.children);
1532
- yield* nestedRuns(child.steps?.flatMap((step) => step.children ?? []));
1533
- }
1534
- };
1535
- const interruptNestedAsyncDescendants = (): void => {
1536
- if (!config.nestedRoute) return;
1537
- let registry: ReturnType<typeof projectNestedEvents>;
1538
- try {
1539
- registry = projectNestedEvents(config.nestedRoute);
1540
- } catch (error) {
1541
- appendJsonl(eventsPath, JSON.stringify({
1542
- type: "subagent.nested.interrupt_failed",
1543
- ts: Date.now(),
1544
- runId: id,
1545
- message: error instanceof Error ? error.message : String(error),
1546
- }));
1547
- return;
1548
- }
1549
- for (const run of nestedRuns(registry.children)) {
1550
- if (run.state !== "running" && run.state !== "queued") continue;
1551
- const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
1552
- if (!nestedAsyncDir) continue;
1553
- try {
1554
- deliverInterruptRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-interrupt" });
1555
- } catch (error) {
1556
- appendJsonl(eventsPath, JSON.stringify({
1557
- type: "subagent.nested.interrupt_failed",
1558
- ts: Date.now(),
1559
- runId: id,
1560
- targetRunId: run.id,
1561
- message: error instanceof Error ? error.message : String(error),
1562
- }));
1563
- }
1564
- }
1565
- };
1566
- const timeoutNestedAsyncDescendants = (): void => {
1567
- if (!config.nestedRoute) return;
1568
- let registry: ReturnType<typeof projectNestedEvents>;
1569
- try {
1570
- registry = projectNestedEvents(config.nestedRoute);
1571
- } catch (error) {
1572
- appendJsonl(eventsPath, JSON.stringify({
1573
- type: "subagent.nested.timeout_failed",
1574
- ts: Date.now(),
1575
- runId: id,
1576
- message: error instanceof Error ? error.message : String(error),
1577
- }));
1578
- return;
1579
- }
1580
- for (const run of nestedRuns(registry.children)) {
1581
- if (run.state !== "running" && run.state !== "queued") continue;
1582
- const nestedAsyncDir = run.asyncDir ?? resolveNestedAsyncDir(config.nestedRoute.rootRunId, run);
1583
- if (!nestedAsyncDir) continue;
1584
- try {
1585
- deliverTimeoutRequest({ asyncDir: nestedAsyncDir, pid: run.pid, source: "ancestor-timeout" });
1586
- } catch (error) {
1587
- appendJsonl(eventsPath, JSON.stringify({
1588
- type: "subagent.nested.timeout_failed",
1589
- ts: Date.now(),
1590
- runId: id,
1591
- targetRunId: run.id,
1592
- message: error instanceof Error ? error.message : String(error),
1593
- }));
1594
- }
1595
- }
1596
- };
1597
- const pausedStepResult = (agent: string): SingleStepResult => ({
1598
- agent,
1599
- output: "Paused after interrupt. Waiting for explicit next action.",
1600
- exitCode: 0,
1601
- interrupted: true,
1602
- });
1603
- const timedOutStepResult = (agent: string): SingleStepResult => ({
1604
- agent,
1605
- output: timeoutMessage ?? "Subagent timed out.",
1606
- error: timeoutMessage ?? "Subagent timed out.",
1607
- exitCode: 1,
1608
- timedOut: true,
1609
- });
1610
- const consumePendingAppendRequests = (): void => {
1611
- if (statusPayload.mode !== "chain" || statusPayload.state !== "running") return;
1612
- const requests = consumeChainAppendRequests(asyncDir);
1613
- if (requests.length === 0) {
1614
- const pendingAppends = countPendingChainAppendRequests(asyncDir);
1615
- if ((statusPayload.pendingAppends ?? 0) !== pendingAppends) {
1616
- statusPayload.pendingAppends = pendingAppends;
1617
- statusPayload.lastUpdate = Date.now();
1618
- writeStatusPayload();
1619
- }
1620
- return;
1621
- }
1622
- const appendedSteps = requests.flatMap((request) => request.steps);
1623
- steps.push(...appendedSteps);
1624
- const now = Date.now();
1625
- const pendingAppends = countPendingChainAppendRequests(asyncDir);
1626
- const added = appendRunnerStepsToStatus({
1627
- status: statusPayload,
1628
- steps: appendedSteps,
1629
- now,
1630
- pendingAppends,
1631
- });
1632
- mutatingFailureStates.push(...Array.from({ length: added.addedFlatSteps }, () => createMutatingFailureState()));
1633
- pendingToolResults.push(...Array.from({ length: added.addedFlatSteps }, () => undefined));
1634
- if (config.childIntercomTargets) {
1635
- config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
1636
- }
1637
- writeStatusPayload();
1638
- for (const request of requests) {
1639
- appendJsonl(eventsPath, JSON.stringify({
1640
- type: "subagent.chain.append.accepted",
1641
- ts: now,
1642
- runId: id,
1643
- requestId: request.id,
1644
- stepCount: request.steps.length,
1645
- pendingAppends,
1646
- }));
1647
- }
1648
- };
1649
- const markDynamicGraphGroup = (stepIndex: number, status: "completed" | "failed" | "running", error?: string, acceptance?: import("../../shared/types.ts").AcceptanceLedger): void => {
1650
- const groupNode = statusPayload.workflowGraph?.nodes.find((node) => node.id === `step-${stepIndex}`);
1651
- if (!groupNode) return;
1652
- groupNode.status = status;
1653
- groupNode.error = error;
1654
- groupNode.acceptanceStatus = acceptance?.status ?? groupNode.acceptanceStatus;
1655
- };
1656
-
1657
- const stepOutputActivityAt = (index: number): number => {
1658
- const step = statusPayload.steps[index];
1659
- let lastActivityAt = step?.lastActivityAt ?? step?.startedAt ?? overallStartTime;
1660
- const outputPath = path.join(asyncDir, `output-${index}.log`);
1661
- try {
1662
- lastActivityAt = Math.max(lastActivityAt, fs.statSync(outputPath).mtimeMs);
1663
- } catch (error) {
1664
- if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
1665
- console.error(`Failed to inspect async output file '${outputPath}':`, error);
1666
- }
1667
- }
1668
- return lastActivityAt;
1669
- };
1670
- const emittedControlEventKeys = new Set<string>();
1671
- const activeLongRunningSteps = new Set<number>();
1672
- const mutatingFailureStates = initialStatusSteps.map(() => createMutatingFailureState());
1673
- const pendingToolResults: Array<{ tool: string; path?: string; mutates: boolean; startedAt?: number } | undefined> = initialStatusSteps.map(() => undefined);
1674
- const mutatingFailureWindowMs = 5 * 60_000;
1675
- const appendControlEvent = (event: ReturnType<typeof buildControlEvent>) => {
1676
- if (!controlConfig.enabled) return;
1677
- const childIntercomTarget = config.childIntercomTargets?.[event.index ?? statusPayload.currentStep];
1678
- const channels = event.type === "active_long_running"
1679
- ? controlConfig.notifyChannels.filter((channel) => channel !== "intercom")
1680
- : controlConfig.notifyChannels;
1681
- if (channels.length === 0 || !claimControlNotification(controlConfig, event, emittedControlEventKeys, childIntercomTarget)) return;
1682
- appendJsonl(eventsPath, JSON.stringify({
1683
- type: "subagent.control",
1684
- event,
1685
- channels,
1686
- childIntercomTarget,
1687
- noticeText: formatControlNoticeMessage(event, childIntercomTarget),
1688
- ...(config.controlIntercomTarget && channels.includes("intercom") ? {
1689
- intercom: {
1690
- to: config.controlIntercomTarget,
1691
- message: formatControlIntercomMessage(event, childIntercomTarget),
1692
- },
1693
- } : {}),
1694
- }));
1695
- };
1696
- const syncTopLevelCurrentTool = (): void => {
1697
- const activeStep = statusPayload.steps
1698
- .filter((step) => step.status === "running" && typeof step.currentTool === "string" && step.currentTool.length > 0)
1699
- .sort((left, right) => (right.currentToolStartedAt ?? 0) - (left.currentToolStartedAt ?? 0))[0];
1700
- statusPayload.currentTool = activeStep?.currentTool;
1701
- statusPayload.currentToolStartedAt = activeStep?.currentToolStartedAt;
1702
- statusPayload.currentPath = activeStep?.currentPath;
1703
- };
1704
- const maybeEmitActiveLongRunning = (flatIndex: number, now: number): boolean => {
1705
- if (!controlConfig.enabled || activeLongRunningSteps.has(flatIndex)) return false;
1706
- const step = statusPayload.steps[flatIndex];
1707
- if (!step || step.status !== "running" || step.activityState === "needs_attention") return false;
1708
- const reason = nextLongRunningTrigger(controlConfig, {
1709
- startedAt: step.startedAt ?? overallStartTime,
1710
- now,
1711
- turns: step.turnCount ?? 0,
1712
- tokens: step.tokens?.total ?? 0,
1713
- });
1714
- if (!reason) return false;
1715
- activeLongRunningSteps.add(flatIndex);
1716
- const previous = step.activityState;
1717
- step.activityState = "active_long_running";
1718
- statusPayload.activityState = statusPayload.activityState === "needs_attention" ? "needs_attention" : "active_long_running";
1719
- const event = buildControlEvent({
1720
- type: "active_long_running",
1721
- from: previous,
1722
- to: "active_long_running",
1723
- runId: id,
1724
- agent: step.agent,
1725
- index: flatIndex,
1726
- ts: now,
1727
- message: `${step.agent} is still active but long-running`,
1728
- reason,
1729
- turns: step.turnCount,
1730
- tokens: step.tokens?.total,
1731
- toolCount: step.toolCount,
1732
- currentTool: step.currentTool,
1733
- currentToolDurationMs: step.currentToolStartedAt ? Math.max(0, now - step.currentToolStartedAt) : undefined,
1734
- currentPath: step.currentPath,
1735
- elapsedMs: now - (step.startedAt ?? overallStartTime),
1736
- });
1737
- appendControlEvent(event);
1738
- return true;
1739
- };
1740
- const deliverSteerRequest = (request: SteerRequest): void => {
1741
- if (statusPayload.state !== "running") return;
1742
- const runningIndexes = statusPayload.steps
1743
- .map((step, index) => ({ step, index }))
1744
- .filter(({ step }) => step.status === "running")
1745
- .map(({ index }) => index);
1746
- const targets = request.targetIndex !== undefined ? [request.targetIndex] : runningIndexes;
1747
- const now = Date.now();
1748
- const accepted: number[] = [];
1749
- const rejected: Array<{ index: number; reason: string }> = [];
1750
- for (const index of targets) {
1751
- const step = statusPayload.steps[index];
1752
- if (!step) {
1753
- rejected.push({ index, reason: "child index out of range" });
1754
- continue;
1755
- }
1756
- if (step.status !== "running") {
1757
- rejected.push({ index, reason: `child is ${step.status}` });
1758
- continue;
1759
- }
1760
- enqueueStepSteer(asyncDir, index, request);
1761
- step.steerCount = (step.steerCount ?? 0) + 1;
1762
- step.lastSteerAt = now;
1763
- accepted.push(index);
1764
- }
1765
- if (accepted.length > 0) {
1766
- statusPayload.steerCount = (statusPayload.steerCount ?? 0) + accepted.length;
1767
- statusPayload.lastSteerAt = now;
1768
- statusPayload.lastUpdate = now;
1769
- writeStatusPayload();
1770
- }
1771
- appendJsonl(eventsPath, JSON.stringify({
1772
- type: "subagent.steer.requested",
1773
- ts: now,
1774
- runId: id,
1775
- requestId: request.id,
1776
- message: request.message,
1777
- ...(request.source ? { source: request.source } : {}),
1778
- ...(request.targetIndex !== undefined ? { targetIndex: request.targetIndex } : {}),
1779
- acceptedIndexes: accepted,
1780
- ...(rejected.length ? { rejected } : {}),
1781
- }));
1782
- };
1783
- const flushPendingStepSteers = (flatIndex: number): void => {
1784
- const remaining: SteerRequest[] = [];
1785
- for (const request of pendingStepSteers.splice(0)) {
1786
- if (request.targetIndex === undefined) deliverSteerRequest({ ...request, targetIndex: flatIndex });
1787
- else if (request.targetIndex === flatIndex) deliverSteerRequest(request);
1788
- else remaining.push(request);
1789
- }
1790
- pendingStepSteers.push(...remaining);
1791
- };
1792
- const updateStepModel = (flatIndex: number, model: string | undefined, thinking: string | undefined, now = Date.now()): void => {
1793
- const step = statusPayload.steps[flatIndex];
1794
- if (!step) return;
1795
- step.model = model;
1796
- step.thinking = thinking;
1797
- statusPayload.lastUpdate = now;
1798
- writeStatusPayload();
1799
- };
1800
- const updateStepTurnBudget = (flatIndex: number, turnCount: number, now: number, terminalAssistantStop: boolean): void => {
1801
- const budget = config.turnBudget;
1802
- const step = statusPayload.steps[flatIndex];
1803
- if (!budget || !step || timedOut || turnBudgetExceeded || step.turnBudgetExceeded) return;
1804
- if (turnCount < budget.maxTurns) {
1805
- const state: TurnBudgetState = { ...budget, outcome: "within-budget", turnCount };
1806
- step.turnBudget = state;
1807
- statusPayload.turnBudget = state;
1808
- return;
1809
- }
1810
- const state = turnBudgetState(budget, turnCount, false);
1811
- step.turnBudget = state;
1812
- statusPayload.turnBudget = state;
1813
- if (!step.wrapUpRequested) {
1814
- step.wrapUpRequested = true;
1815
- statusPayload.wrapUpRequested = true;
1816
- appendRecentStepOutput(step, [turnBudgetSoftNote(budget, turnCount)]);
1817
- }
1818
- if (!shouldAbortForTurnBudget(budget, turnCount, terminalAssistantStop)) return;
1819
- const exceededState = turnBudgetState(budget, turnCount, true);
1820
- const message = turnBudgetExceededMessage(budget, turnCount);
1821
- step.turnBudget = exceededState;
1822
- step.turnBudgetExceeded = true;
1823
- step.wrapUpRequested = true;
1824
- step.error = message;
1825
- turnBudgetExceeded = true;
1826
- statusPayload.turnBudget = exceededState;
1827
- statusPayload.turnBudgetExceeded = true;
1828
- statusPayload.wrapUpRequested = true;
1829
- statusPayload.error = message;
1830
- statusPayload.lastUpdate = now;
1831
- appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.turn_budget_exceeded", ts: now, runId: id, stepIndex: flatIndex, agent: step.agent, turnCount, maxTurns: budget.maxTurns, graceTurns: budget.graceTurns, message }));
1832
- activeChildTurnBudgetAborts.get(flatIndex)?.(message, exceededState);
1833
- };
1834
- const updateStepFromChildEvent = (flatIndex: number, event: ChildEvent): void => {
1835
- const step = statusPayload.steps[flatIndex];
1836
- if (!step) return;
1837
- const now = Date.now();
1838
- statusPayload.currentStep = flatIndex;
1839
- if (event.type === "tool_execution_start" && event.toolName) {
1840
- const mutates = isMutatingTool(event.toolName, event.args);
1841
- const currentPath = resolveCurrentPath(event.toolName, event.args);
1842
- step.toolCount = (step.toolCount ?? 0) + 1;
1843
- const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
1844
- if (configuredToolBudget) {
1845
- step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount);
1846
- statusPayload.toolBudget = step.toolBudget;
1847
- }
1848
- step.currentTool = event.toolName;
1849
- step.currentToolArgs = extractToolArgsPreview(event.args ?? {});
1850
- step.currentToolStartedAt = now;
1851
- step.currentPath = currentPath;
1852
- pendingToolResults[flatIndex] = { tool: event.toolName, path: currentPath, mutates, startedAt: now };
1853
- statusPayload.toolCount = (statusPayload.toolCount ?? 0) + 1;
1854
- syncTopLevelCurrentTool();
1855
- } else if (event.type === "tool_execution_end") {
1856
- if (step.currentTool) {
1857
- step.recentTools ??= [];
1858
- step.recentTools.push({ tool: step.currentTool, args: step.currentToolArgs || "", endMs: now });
1859
- }
1860
- step.currentTool = undefined;
1861
- step.currentToolArgs = undefined;
1862
- step.currentToolStartedAt = undefined;
1863
- step.currentPath = undefined;
1864
- syncTopLevelCurrentTool();
1865
- } else if (event.type === "tool_result_end" && event.message) {
1866
- const toolSnapshot = pendingToolResults[flatIndex];
1867
- pendingToolResults[flatIndex] = undefined;
1868
- const resultText = extractTextFromContent(event.message.content);
1869
- if (toolSnapshot && resultText.includes("Tool budget hard limit reached")) {
1870
- const configuredToolBudget = flatSteps[flatIndex]?.toolBudget;
1871
- if (configuredToolBudget) {
1872
- step.toolBudget = toolBudgetState(configuredToolBudget, step.toolCount ?? 0, toolSnapshot.tool);
1873
- step.toolBudgetBlocked = true;
1874
- statusPayload.toolBudget = step.toolBudget;
1875
- statusPayload.toolBudgetBlocked = true;
1876
- }
1877
- }
1878
- appendRecentStepOutput(step, resultText.split("\n").slice(-10));
1879
- if (toolSnapshot?.mutates && didMutatingToolFail(resultText)) {
1880
- const state = mutatingFailureStates[flatIndex]!;
1881
- recordMutatingFailure(state, {
1882
- tool: toolSnapshot.tool,
1883
- path: toolSnapshot.path,
1884
- error: resultText.split("\n").find((line) => line.trim())?.trim().slice(0, 180) ?? "mutating tool failed",
1885
- ts: now,
1886
- }, mutatingFailureWindowMs);
1887
- if (controlConfig.enabled && shouldEscalateMutatingFailures(state, controlConfig.failedToolAttemptsBeforeAttention) && step.activityState !== "needs_attention") {
1888
- const previous = step.activityState;
1889
- step.activityState = "needs_attention";
1890
- statusPayload.activityState = "needs_attention";
1891
- appendControlEvent(buildControlEvent({
1892
- type: "needs_attention",
1893
- from: previous,
1894
- to: "needs_attention",
1895
- runId: id,
1896
- agent: step.agent,
1897
- index: flatIndex,
1898
- ts: now,
1899
- message: `${step.agent} needs attention after repeated mutating tool failures`,
1900
- reason: "tool_failures",
1901
- turns: step.turnCount,
1902
- tokens: step.tokens?.total,
1903
- toolCount: step.toolCount,
1904
- currentTool: toolSnapshot.tool,
1905
- currentToolDurationMs: toolSnapshot.startedAt ? Math.max(0, now - toolSnapshot.startedAt) : undefined,
1906
- currentPath: toolSnapshot.path,
1907
- recentFailureSummary: summarizeRecentMutatingFailures(state),
1908
- }));
1909
- }
1910
- } else if (toolSnapshot?.mutates) {
1911
- resetMutatingFailureState(mutatingFailureStates[flatIndex]!);
1912
- }
1913
- } else if (event.type === "message_end" && event.message?.role === "assistant") {
1914
- appendRecentStepOutput(step, stripAcceptanceReport(extractTextFromContent(event.message.content)).split("\n").slice(-10));
1915
- step.turnCount = (step.turnCount ?? 0) + 1;
1916
- const usage = event.message.usage;
1917
- if (usage) {
1918
- const input = usage.input ?? usage.inputTokens ?? 0;
1919
- const output = usage.output ?? usage.outputTokens ?? 0;
1920
- const previousInput = step.tokens?.input ?? 0;
1921
- const previousOutput = step.tokens?.output ?? 0;
1922
- step.tokens = { input: previousInput + input, output: previousOutput + output, total: previousInput + previousOutput + input + output };
1923
- const totalInput = statusPayload.totalTokens?.input ?? 0;
1924
- const totalOutput = statusPayload.totalTokens?.output ?? 0;
1925
- statusPayload.totalTokens = { input: totalInput + input, output: totalOutput + output, total: totalInput + totalOutput + input + output };
1926
- }
1927
- statusPayload.turnCount = Math.max(statusPayload.turnCount ?? 0, step.turnCount);
1928
- updateStepTurnBudget(flatIndex, step.turnCount, now, isTerminalAssistantStop(event.message));
1929
- }
1930
- syncTopLevelCurrentTool();
1931
- step.lastActivityAt = now;
1932
- statusPayload.lastActivityAt = now;
1933
- statusPayload.lastUpdate = now;
1934
- maybeEmitActiveLongRunning(flatIndex, now);
1935
- writeStatusPayload();
1936
- };
1937
- const updateRunnerActivityState = (now: number): boolean => {
1938
- if (!controlConfig.enabled) return false;
1939
- let changed = false;
1940
- let runLastActivityAt = statusPayload.lastActivityAt ?? overallStartTime;
1941
- for (let index = 0; index < statusPayload.steps.length; index++) {
1942
- const step = statusPayload.steps[index]!;
1943
- if (step.status !== "running") continue;
1944
- const lastActivityAt = stepOutputActivityAt(index);
1945
- runLastActivityAt = Math.max(runLastActivityAt, lastActivityAt);
1946
- if (step.lastActivityAt !== lastActivityAt) {
1947
- step.lastActivityAt = lastActivityAt;
1948
- changed = true;
1949
- }
1950
- const idleState = deriveActivityState({
1951
- config: controlConfig,
1952
- startedAt: step.startedAt ?? overallStartTime,
1953
- lastActivityAt,
1954
- now,
1955
- });
1956
- if (idleState === "needs_attention") {
1957
- const previous = step.activityState;
1958
- step.activityState = "needs_attention";
1959
- if (previous !== "needs_attention") {
1960
- appendControlEvent(buildControlEvent({
1961
- from: previous,
1962
- to: "needs_attention",
1963
- runId: id,
1964
- agent: step.agent,
1965
- index,
1966
- ts: now,
1967
- lastActivityAt,
1968
- }));
1969
- changed = true;
1970
- }
1971
- } else if (maybeEmitActiveLongRunning(index, now)) {
1972
- changed = true;
1973
- }
1974
- }
1975
- if (statusPayload.lastActivityAt !== runLastActivityAt) {
1976
- statusPayload.lastActivityAt = runLastActivityAt;
1977
- changed = true;
1978
- }
1979
- const nextRunState = statusPayload.steps.some((step) => step.activityState === "needs_attention")
1980
- ? "needs_attention"
1981
- : statusPayload.steps.some((step) => step.activityState === "active_long_running")
1982
- ? "active_long_running"
1983
- : undefined;
1984
- if (nextRunState !== currentActivityState) {
1985
- currentActivityState = nextRunState;
1986
- statusPayload.activityState = nextRunState;
1987
- changed = true;
1988
- }
1989
- statusPayload.lastUpdate = now;
1990
- if (changed) writeStatusPayload();
1991
- return changed;
1992
- };
1993
- if (controlConfig.enabled) {
1994
- activityTimer = setInterval(() => {
1995
- if (statusPayload.state !== "running") return;
1996
- const now = Date.now();
1997
- updateRunnerActivityState(now);
1998
- }, 1000);
1999
- activityTimer.unref?.();
2000
- }
2001
-
2002
- const interruptRunner = () => {
2003
- consumeInterruptRequest(asyncDir);
2004
- if (interrupted || statusPayload.state !== "running") return;
2005
- interrupted = true;
2006
- const now = Date.now();
2007
- statusPayload.state = "paused";
2008
- currentActivityState = undefined;
2009
- statusPayload.activityState = undefined;
2010
- statusPayload.lastUpdate = now;
2011
- for (const step of statusPayload.steps) {
2012
- if (step.status === "running") {
2013
- step.status = "paused";
2014
- step.activityState = undefined;
2015
- step.endedAt = now;
2016
- step.durationMs = step.startedAt ? now - step.startedAt : undefined;
2017
- step.lastActivityAt = now;
2018
- }
2019
- }
2020
- writeStatusPayload();
2021
- appendJsonl(eventsPath, JSON.stringify({
2022
- type: "subagent.run.paused",
2023
- ts: now,
2024
- runId: id,
2025
- }));
2026
- interruptNestedAsyncDescendants();
2027
- interruptActiveChildren();
2028
- };
2029
- const timeoutRunner = () => {
2030
- if (timedOut || interrupted || statusPayload.state !== "running") return;
2031
- timedOut = true;
2032
- const now = Date.now();
2033
- const message = timeoutMessage ?? "Subagent timed out.";
2034
- statusPayload.state = "failed";
2035
- statusPayload.timedOut = true;
2036
- statusPayload.error = message;
2037
- currentActivityState = undefined;
2038
- statusPayload.activityState = undefined;
2039
- statusPayload.lastUpdate = now;
2040
- for (const step of statusPayload.steps) {
2041
- if (step.status !== "running" && step.status !== "pending") continue;
2042
- step.status = "failed";
2043
- step.error = message;
2044
- step.exitCode = 1;
2045
- step.timedOut = true;
2046
- step.activityState = undefined;
2047
- step.endedAt = now;
2048
- step.durationMs = step.startedAt ? now - step.startedAt : 0;
2049
- step.lastActivityAt = now;
2050
- }
2051
- writeStatusPayload();
2052
- appendJsonl(eventsPath, JSON.stringify({
2053
- type: "subagent.run.timed_out",
2054
- ts: now,
2055
- runId: id,
2056
- timeoutMs: config.timeoutMs,
2057
- deadlineAt: config.deadlineAt,
2058
- message,
2059
- }));
2060
- timeoutAbortController.abort();
2061
- timeoutNestedAsyncDescendants();
2062
- timeoutActiveChildren();
2063
- };
2064
- process.on(ASYNC_INTERRUPT_SIGNAL, interruptRunner);
2065
- // Portable control inbox: the parent drops control request files here when
2066
- // it cannot deliver OS signals (e.g. ENOSYS on Windows) or when steering a
2067
- // live child. Interrupts still route into the same graceful interruptRunner().
2068
- const disposeControlInbox = watchAsyncControlInbox(asyncDir, {
2069
- onInterrupt: interruptRunner,
2070
- onTimeout: timeoutRunner,
2071
- onSteer: (request) => {
2072
- const targetStep = request.targetIndex !== undefined ? statusPayload.steps[request.targetIndex] : undefined;
2073
- if (targetStep?.status === "pending") pendingStepSteers.push(request);
2074
- else if (request.targetIndex !== undefined || statusPayload.steps.some((step) => step.status === "running")) deliverSteerRequest(request);
2075
- else pendingStepSteers.push(request);
2076
- },
2077
- });
2078
- if (config.deadlineAt !== undefined) {
2079
- const remainingMs = Math.max(0, config.deadlineAt - Date.now());
2080
- timeoutTimer = setTimeout(timeoutRunner, remainingMs);
2081
- timeoutTimer.unref?.();
2082
- }
2083
- appendJsonl(
2084
- eventsPath,
2085
- JSON.stringify({
2086
- type: "subagent.run.started",
2087
- lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
2088
- ts: overallStartTime,
2089
- runId: id,
2090
- mode: statusPayload.mode,
2091
- cwd,
2092
- pid: process.pid,
2093
- }),
2094
- );
2095
-
2096
- let flatIndex = 0;
2097
- let stepCursor = 0;
2098
-
2099
- while (true) {
2100
- if (interrupted || timedOut || turnBudgetExceeded) break;
2101
- consumePendingAppendRequests();
2102
- if (stepCursor >= steps.length) break;
2103
- const stepIndex = stepCursor++;
2104
- const step = steps[stepIndex]!;
2105
-
2106
- if (isDynamicRunnerGroup(step)) {
2107
- const groupStartFlatIndex = flatIndex;
2108
- let materialized: ReturnType<typeof materializeDynamicParallelStep>;
2109
- try {
2110
- materialized = materializeDynamicParallelStep(step as Parameters<typeof materializeDynamicParallelStep>[0], outputs, stepIndex, { maxItems: config.dynamicFanoutMaxItems, allowRunnerFields: true });
2111
- if (materialized.collectedOnEmpty) validateDynamicCollection(step.collect.outputSchema, materialized.collectedOnEmpty);
2112
- } catch (error) {
2113
- const now = Date.now();
2114
- const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
2115
- statusPayload.state = "failed";
2116
- statusPayload.error = message;
2117
- statusPayload.currentStep = flatIndex;
2118
- const placeholder = statusPayload.steps[groupStartFlatIndex];
2119
- if (placeholder) {
2120
- placeholder.status = "failed";
2121
- placeholder.error = message;
2122
- placeholder.startedAt = now;
2123
- placeholder.endedAt = now;
2124
- placeholder.durationMs = 0;
2125
- placeholder.exitCode = 1;
2126
- }
2127
- statusPayload.lastUpdate = now;
2128
- markDynamicGraphGroup(stepIndex, "failed", message);
2129
- writeStatusPayload();
2130
- results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1 });
2131
- break;
2132
- }
2133
-
2134
- if (materialized.parallel.length === 0) {
2135
- const now = Date.now();
2136
- const collection = materialized.collectedOnEmpty ?? [];
2137
- outputs[step.collect.as] = {
2138
- text: JSON.stringify(collection),
2139
- structured: collection,
2140
- agent: step.parallel.agent,
2141
- stepIndex,
2142
- };
2143
- statusPayload.outputs = outputs;
2144
- const placeholder = statusPayload.steps[groupStartFlatIndex];
2145
- if (placeholder) {
2146
- placeholder.status = "complete";
2147
- placeholder.startedAt = now;
2148
- placeholder.endedAt = now;
2149
- placeholder.durationMs = 0;
2150
- }
2151
- previousOutput = "Dynamic fanout produced 0 results.";
2152
- const groupAcceptance = step.effectiveAcceptance?.explicit && !timedOut
2153
- ? await evaluateAcceptance({
2154
- acceptance: step.effectiveAcceptance,
2155
- output: "",
2156
- report: aggregateAcceptanceReport({
2157
- results: [],
2158
- notes: "Dynamic fanout produced 0 results.",
2159
- }),
2160
- cwd,
2161
- signal: timeoutAbortController.signal,
2162
- abortMessage: timeoutMessage ?? "Subagent timed out.",
2163
- })
2164
- : undefined;
2165
- const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
2166
- const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
2167
- if (placeholder && effectiveGroupAcceptance) placeholder.acceptance = effectiveGroupAcceptance;
2168
- const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2169
- if (groupTimedOut || groupAcceptanceFailure) {
2170
- const errorMessage = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure!;
2171
- statusPayload.state = "failed";
2172
- statusPayload.error = errorMessage;
2173
- if (placeholder) {
2174
- placeholder.status = "failed";
2175
- placeholder.error = errorMessage;
2176
- placeholder.exitCode = 1;
2177
- placeholder.timedOut = groupTimedOut ? true : undefined;
2178
- }
2179
- markDynamicGraphGroup(stepIndex, "failed", errorMessage, effectiveGroupAcceptance);
2180
- statusPayload.lastUpdate = Date.now();
2181
- writeStatusPayload();
2182
- results.push({ agent: step.parallel.agent, output: errorMessage, error: errorMessage, success: false, exitCode: 1, timedOut: groupTimedOut ? true : undefined, acceptance: effectiveGroupAcceptance });
2183
- break;
2184
- }
2185
- flatIndex++;
2186
- statusPayload.lastUpdate = now;
2187
- markDynamicGraphGroup(stepIndex, "completed", undefined, effectiveGroupAcceptance);
2188
- writeStatusPayload();
2189
- continue;
2190
- }
2191
-
2192
- const dynamicSteps = materialized.parallel.map((task, itemIndex) => {
2193
- const thinkingOverride = step.thinkingOverrides?.[itemIndex];
2194
- const model = thinkingOverride ? applyThinkingSuffix(step.parallel.model, thinkingOverride, true) : step.parallel.model;
2195
- const thinking = thinkingOverride ? resolveEffectiveThinking(model, thinkingOverride) : undefined;
2196
- return {
2197
- ...step.parallel,
2198
- task: task.task ?? step.parallel.task,
2199
- label: task.label ?? step.parallel.label,
2200
- ...(step.sessionFiles?.[itemIndex] ? { sessionFile: step.sessionFiles[itemIndex] } : {}),
2201
- ...(thinkingOverride ? {
2202
- ...(model ? { model } : {}),
2203
- ...(thinking ? { thinking } : {}),
2204
- ...(step.parallel.modelCandidates ? { modelCandidates: step.parallel.modelCandidates.map((candidate) => applyThinkingSuffix(candidate, thinkingOverride, true)) } : {}),
2205
- } : {}),
2206
- structuredOutput: undefined,
2207
- structuredOutputSchema: step.parallel.structuredOutputSchema ?? step.parallel.structuredOutput?.schema,
2208
- };
2209
- });
2210
- const dynamicFlatStepCount = Math.max(statusPayload.steps.length - 1 + dynamicSteps.length, 1);
2211
- const dynamicStatusSteps: RunnerStatusStep[] = dynamicSteps.map((task, itemIndex) => {
2212
- const transcriptPath = resolveAsyncStepTranscriptPath({ artifactsDir, artifactConfig, runId: id, agent: task.agent, flatIndex: groupStartFlatIndex + itemIndex, flatStepCount: dynamicFlatStepCount });
2213
- return {
2214
- agent: task.agent,
2215
- phase: task.phase ?? step.phase,
2216
- label: task.label,
2217
- outputName: undefined,
2218
- structured: Boolean(task.structuredOutputSchema),
2219
- status: "pending",
2220
- ...(task.sessionFile ? { sessionFile: task.sessionFile } : {}),
2221
- ...(transcriptPath ? { transcriptPath } : {}),
2222
- skills: task.skills,
2223
- model: task.model,
2224
- thinking: task.thinking,
2225
- attemptedModels: task.modelCandidates && task.modelCandidates.length > 0 ? task.modelCandidates : task.model ? [task.model] : undefined,
2226
- recentTools: [],
2227
- recentOutput: [],
2228
- };
2229
- });
2230
- statusPayload.steps.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps);
2231
- if (config.childIntercomTargets) {
2232
- config.childIntercomTargets = statusPayload.steps.map((statusStep, index) => resolveSubagentIntercomTarget(id, statusStep.agent, index));
2233
- }
2234
- mutatingFailureStates.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps.map(() => createMutatingFailureState()));
2235
- pendingToolResults.splice(groupStartFlatIndex, 1, ...dynamicStatusSteps.map(() => undefined));
2236
- const materializedDelta = dynamicStatusSteps.length - 1;
2237
- for (const group of statusPayload.parallelGroups) {
2238
- if (group.stepIndex === stepIndex) {
2239
- group.start = groupStartFlatIndex;
2240
- group.count = dynamicStatusSteps.length;
2241
- } else if (group.start > groupStartFlatIndex) {
2242
- group.start += materializedDelta;
2243
- }
2244
- }
2245
- if (statusPayload.workflowGraph) {
2246
- const shiftFlatIndexes = (nodes: NonNullable<typeof statusPayload.workflowGraph>["nodes"]): void => {
2247
- for (const node of nodes) {
2248
- if (node.stepIndex !== undefined && node.stepIndex > stepIndex && node.flatIndex !== undefined && node.flatIndex >= groupStartFlatIndex) {
2249
- node.flatIndex += dynamicStatusSteps.length;
2250
- }
2251
- if (node.children) shiftFlatIndexes(node.children);
2252
- }
2253
- };
2254
- shiftFlatIndexes(statusPayload.workflowGraph.nodes);
2255
- const groupNode = statusPayload.workflowGraph.nodes.find((node) => node.id === `step-${stepIndex}`);
2256
- if (groupNode) {
2257
- groupNode.children = materialized.items.map((item, itemIndex) => ({
2258
- id: `step-${stepIndex}-item-${item.idKey}`,
2259
- kind: "agent",
2260
- agent: step.parallel.agent,
2261
- phase: dynamicSteps[itemIndex]?.phase ?? step.phase,
2262
- label: dynamicSteps[itemIndex]?.label?.trim() || `${step.parallel.agent} ${item.key}`,
2263
- status: "pending",
2264
- flatIndex: groupStartFlatIndex + itemIndex,
2265
- stepIndex,
2266
- itemKey: item.key,
2267
- structured: Boolean(dynamicSteps[itemIndex]?.structuredOutputSchema),
2268
- }));
2269
- }
2270
- }
2271
- writeStatusPayload();
2272
-
2273
- const concurrency = step.concurrency ?? MAX_PARALLEL_CONCURRENCY;
2274
- const failFast = step.failFast ?? false;
2275
- let aborted = false;
2276
- const parallelResults = await mapConcurrent(dynamicSteps, concurrency, async (task, taskIdx) => {
2277
- const fi = groupStartFlatIndex + taskIdx;
2278
- if (timedOut) return timedOutStepResult(task.agent);
2279
- if (interrupted) return pausedStepResult(task.agent);
2280
- if (aborted && failFast) {
2281
- const skippedAt = Date.now();
2282
- statusPayload.steps[fi].status = "failed";
2283
- statusPayload.steps[fi].error = "Skipped due to fail-fast";
2284
- statusPayload.steps[fi].startedAt = skippedAt;
2285
- statusPayload.steps[fi].endedAt = skippedAt;
2286
- statusPayload.steps[fi].durationMs = 0;
2287
- statusPayload.steps[fi].exitCode = -1;
2288
- statusPayload.lastUpdate = skippedAt;
2289
- writeStatusPayload();
2290
- return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
2291
- }
2292
- const taskStartTime = Date.now();
2293
- statusPayload.currentStep = fi;
2294
- statusPayload.steps[fi].status = "running";
2295
- statusPayload.steps[fi].error = undefined;
2296
- statusPayload.steps[fi].activityState = undefined;
2297
- resetStepLiveDetail(statusPayload.steps[fi]);
2298
- statusPayload.steps[fi].startedAt = taskStartTime;
2299
- statusPayload.steps[fi].lastActivityAt = taskStartTime;
2300
- statusPayload.outputFile = path.join(asyncDir, `output-${fi}.log`);
2301
- statusPayload.lastActivityAt = taskStartTime;
2302
- statusPayload.lastUpdate = taskStartTime;
2303
- writeStatusPayload();
2304
- appendJsonl(eventsPath, JSON.stringify({ type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent }));
2305
- flushPendingStepSteers(fi);
2306
- const singleResult = await runSingleStep(task, {
2307
- previousOutput, placeholder, cwd, sessionEnabled,
2308
- outputs,
2309
- sessionDir: config.sessionDir ? path.join(config.sessionDir, `dynamic-${stepIndex}-${taskIdx}`) : undefined,
2310
- artifactsDir, artifactConfig, id,
2311
- flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
2312
- outputFile: path.join(asyncDir, `output-${fi}.log`),
2313
- steerInboxDir: stepSteerInboxDir(asyncDir, fi),
2314
- piPackageRoot: config.piPackageRoot,
2315
- piArgv1: config.piArgv1,
2316
- childIntercomTarget: config.childIntercomTargets?.[fi],
2317
- orchestratorIntercomTarget: config.controlIntercomTarget,
2318
- nestedRoute: config.nestedRoute,
2319
- registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2320
- registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
2321
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
2322
- timeoutSignal: timeoutAbortController.signal,
2323
- timeoutMessage,
2324
- turnBudget: config.turnBudget,
2325
- onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
2326
- onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2327
- skipAcceptance: () => timedOut,
2328
- });
2329
- const taskEndTime = Date.now();
2330
- const childInterrupted = singleResult.interrupted === true;
2331
- statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2332
- statusPayload.steps[fi].endedAt = taskEndTime;
2333
- statusPayload.steps[fi].durationMs = taskEndTime - taskStartTime;
2334
- statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2335
- statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
2336
- statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
2337
- statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
2338
- statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
2339
- statusPayload.steps[fi].toolBudget = singleResult.toolBudget;
2340
- statusPayload.steps[fi].toolBudgetBlocked = singleResult.toolBudgetBlocked;
2341
- if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
2342
- if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
2343
- if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
2344
- if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
2345
- if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
2346
- statusPayload.steps[fi].model = singleResult.model;
2347
- statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
2348
- statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
2349
- statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
2350
- statusPayload.steps[fi].totalCost = singleResult.totalCost;
2351
- statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2352
- statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
2353
- statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
2354
- statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
2355
- statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
2356
- statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2357
- statusPayload.steps[fi].acceptance = singleResult.acceptance;
2358
- statusPayload.lastUpdate = taskEndTime;
2359
- writeStatusPayload();
2360
- appendJsonl(eventsPath, JSON.stringify({
2361
- type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2362
- ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
2363
- exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskEndTime - taskStartTime,
2364
- }));
2365
- if (singleResult.exitCode !== 0 && failFast) aborted = true;
2366
- return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
2367
- }, globalSemaphore);
2368
-
2369
- flatIndex += dynamicSteps.length;
2370
- for (const pr of parallelResults) {
2371
- results.push({
2372
- agent: pr.agent,
2373
- output: pr.output,
2374
- error: pr.error,
2375
- success: pr.interrupted !== true && pr.exitCode === 0,
2376
- exitCode: pr.interrupted === true ? 0 : pr.exitCode,
2377
- skipped: pr.skipped,
2378
- interrupted: pr.interrupted,
2379
- timedOut: pr.timedOut,
2380
- turnBudget: pr.turnBudget,
2381
- turnBudgetExceeded: pr.turnBudgetExceeded,
2382
- wrapUpRequested: pr.wrapUpRequested,
2383
- toolBudget: pr.toolBudget,
2384
- toolBudgetBlocked: pr.toolBudgetBlocked,
2385
- sessionFile: pr.sessionFile,
2386
- intercomTarget: pr.intercomTarget,
2387
- model: pr.model,
2388
- attemptedModels: pr.attemptedModels,
2389
- modelAttempts: pr.modelAttempts,
2390
- totalCost: pr.totalCost,
2391
- artifactPaths: pr.artifactPaths,
2392
- transcriptPath: pr.transcriptPath,
2393
- transcriptError: pr.transcriptError,
2394
- structuredOutput: pr.structuredOutput,
2395
- structuredOutputPath: pr.structuredOutputPath,
2396
- structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
2397
- acceptance: pr.acceptance,
2398
- });
2399
- }
2400
- const collection = collectDynamicResults(step as Parameters<typeof collectDynamicResults>[0], materialized.items, parallelResults);
2401
- const failures = parallelResults.filter((result) => result.exitCode !== 0 && result.exitCode !== -1);
2402
- if (failures.length === 0) {
2403
- try {
2404
- validateDynamicCollection(step.collect.outputSchema, collection);
2405
- outputs[step.collect.as] = {
2406
- text: JSON.stringify(collection),
2407
- structured: collection,
2408
- agent: step.parallel.agent,
2409
- stepIndex,
2410
- };
2411
- statusPayload.outputs = outputs;
2412
- const groupAcceptance = step.effectiveAcceptance && !timedOut
2413
- ? await evaluateAcceptance({
2414
- acceptance: step.effectiveAcceptance,
2415
- output: "",
2416
- report: aggregateAcceptanceReport({
2417
- results: parallelResults,
2418
- notes: `Dynamic fanout collected ${collection.length} result(s) into ${step.collect.as}.`,
2419
- }),
2420
- cwd,
2421
- signal: timeoutAbortController.signal,
2422
- abortMessage: timeoutMessage ?? "Subagent timed out.",
2423
- })
2424
- : undefined;
2425
- const groupTimedOut = timedOut || timeoutAbortController.signal.aborted;
2426
- const effectiveGroupAcceptance = groupTimedOut ? undefined : groupAcceptance;
2427
- const groupAcceptanceFailure = effectiveGroupAcceptance ? acceptanceFailureMessage(effectiveGroupAcceptance) : undefined;
2428
- const groupError = groupTimedOut ? timeoutMessage ?? "Subagent timed out." : groupAcceptanceFailure;
2429
- markDynamicGraphGroup(stepIndex, groupError ? "failed" : "completed", groupError, effectiveGroupAcceptance);
2430
- if (groupError) {
2431
- results.push({
2432
- agent: step.parallel.agent,
2433
- output: groupError,
2434
- error: groupError,
2435
- success: false,
2436
- exitCode: 1,
2437
- timedOut: groupTimedOut ? true : undefined,
2438
- structuredOutput: collection,
2439
- acceptance: effectiveGroupAcceptance,
2440
- });
2441
- statusPayload.error = groupError;
2442
- }
2443
- } catch (error) {
2444
- const message = error instanceof DynamicFanoutError ? error.message : error instanceof Error ? error.message : String(error);
2445
- results.push({ agent: step.parallel.agent, output: message, error: message, success: false, exitCode: 1, structuredOutput: collection });
2446
- statusPayload.error = message;
2447
- markDynamicGraphGroup(stepIndex, "failed", message);
2448
- }
2449
- }
2450
- previousOutput = aggregateParallelOutputs(
2451
- parallelResults.map((r, i) => ({
2452
- agent: r.agent,
2453
- taskIndex: i,
2454
- output: r.output,
2455
- exitCode: r.exitCode,
2456
- error: r.error,
2457
- })),
2458
- (i, agent) => `=== Dynamic Item ${i + 1} (${agent}, key ${materialized.items[i]?.key ?? i}) ===`,
2459
- );
2460
- appendJsonl(eventsPath, JSON.stringify({
2461
- type: "subagent.dynamic.completed",
2462
- ts: Date.now(),
2463
- runId: id,
2464
- stepIndex,
2465
- success: failures.length === 0,
2466
- }));
2467
- if (failures.length > 0) markDynamicGraphGroup(stepIndex, "failed", failures[0]?.error ?? "Dynamic fanout child failed.");
2468
- statusPayload.lastUpdate = Date.now();
2469
- writeStatusPayload();
2470
- if (failures.length > 0 || statusPayload.error) break;
2471
- continue;
2472
- }
2473
-
2474
- if (isParallelGroup(step)) {
2475
- const group = step;
2476
- const concurrency = group.concurrency ?? MAX_PARALLEL_CONCURRENCY;
2477
- const failFast = group.failFast ?? false;
2478
- const groupStartFlatIndex = flatIndex;
2479
- let aborted = false;
2480
- let worktreeSetup: WorktreeSetup | undefined;
2481
- if (group.worktree) {
2482
- const worktreeTaskCwdConflict = findWorktreeTaskCwdConflict(group.parallel, cwd);
2483
- if (worktreeTaskCwdConflict) {
2484
- const failedAt = Date.now();
2485
- markParallelGroupSetupFailure({
2486
- statusPayload,
2487
- results,
2488
- group,
2489
- groupStartFlatIndex,
2490
- setupError: formatWorktreeTaskCwdConflict(worktreeTaskCwdConflict, cwd),
2491
- failedAt,
2492
- statusPath,
2493
- eventsPath,
2494
- asyncDir,
2495
- runId: id,
2496
- stepIndex,
2497
- });
2498
- flatIndex += group.parallel.length;
2499
- break;
2500
- }
2501
- try {
2502
- worktreeSetup = createWorktrees(cwd, `${id}-s${stepIndex}`, group.parallel.length, {
2503
- agents: group.parallel.map((task) => task.agent),
2504
- setupHook: config.worktreeSetupHook
2505
- ? { hookPath: config.worktreeSetupHook, timeoutMs: config.worktreeSetupHookTimeoutMs }
2506
- : undefined,
2507
- baseDir: config.worktreeBaseDir,
2508
- });
2509
- } catch (error) {
2510
- const setupError = error instanceof Error ? error.message : String(error);
2511
- const failedAt = Date.now();
2512
- markParallelGroupSetupFailure({
2513
- statusPayload,
2514
- results,
2515
- group,
2516
- groupStartFlatIndex,
2517
- setupError,
2518
- failedAt,
2519
- statusPath,
2520
- eventsPath,
2521
- asyncDir,
2522
- runId: id,
2523
- stepIndex,
2524
- });
2525
- flatIndex += group.parallel.length;
2526
- break;
2527
- }
2528
- }
2529
-
2530
- try {
2531
- if (group.worktree) ensureParallelProgressFile(cwd, group);
2532
- const groupStartTime = Date.now();
2533
- markParallelGroupRunning({
2534
- statusPayload,
2535
- group,
2536
- groupStartFlatIndex,
2537
- groupStartTime,
2538
- statusPath,
2539
- eventsPath,
2540
- asyncDir,
2541
- runId: id,
2542
- stepIndex,
2543
- });
2544
- const parallelResults = await mapConcurrent(
2545
- group.parallel,
2546
- concurrency,
2547
- async (task, taskIdx) => {
2548
- const fi = groupStartFlatIndex + taskIdx;
2549
- if (timedOut) return timedOutStepResult(task.agent);
2550
- if (interrupted) return pausedStepResult(task.agent);
2551
- if (aborted && failFast) {
2552
- const skippedAt = Date.now();
2553
- statusPayload.steps[fi].status = "failed";
2554
- statusPayload.steps[fi].error = "Skipped due to fail-fast";
2555
- statusPayload.steps[fi].startedAt = skippedAt;
2556
- statusPayload.steps[fi].endedAt = skippedAt;
2557
- statusPayload.steps[fi].durationMs = 0;
2558
- statusPayload.steps[fi].exitCode = -1;
2559
- statusPayload.steps[fi].activityState = undefined;
2560
- statusPayload.lastUpdate = skippedAt;
2561
- writeStatusPayload();
2562
- appendJsonl(eventsPath, JSON.stringify({
2563
- type: "subagent.step.failed", ts: skippedAt, runId: id, stepIndex: fi, agent: task.agent, exitCode: -1, durationMs: 0,
2564
- }));
2565
- return { agent: task.agent, output: "(skipped — fail-fast)", exitCode: -1 as number | null, skipped: true };
2566
- }
2567
-
2568
- const taskStartTime = Date.now();
2569
- statusPayload.currentStep = fi;
2570
- statusPayload.steps[fi].status = "running";
2571
- statusPayload.steps[fi].error = undefined;
2572
- statusPayload.steps[fi].activityState = undefined;
2573
- resetStepLiveDetail(statusPayload.steps[fi]);
2574
- statusPayload.steps[fi].startedAt = taskStartTime;
2575
- statusPayload.steps[fi].endedAt = undefined;
2576
- statusPayload.steps[fi].durationMs = undefined;
2577
- statusPayload.steps[fi].lastActivityAt = taskStartTime;
2578
- statusPayload.outputFile = path.join(asyncDir, `output-${fi}.log`);
2579
- statusPayload.lastActivityAt = taskStartTime;
2580
- statusPayload.lastUpdate = taskStartTime;
2581
- writeStatusPayload();
2582
-
2583
- appendJsonl(eventsPath, JSON.stringify({
2584
- type: "subagent.step.started", ts: taskStartTime, runId: id, stepIndex: fi, agent: task.agent,
2585
- }));
2586
-
2587
- const taskSessionDir = config.sessionDir
2588
- ? path.join(config.sessionDir, `parallel-${taskIdx}`)
2589
- : undefined;
2590
- const { taskForRun, taskCwd } = prepareParallelTaskRun(task, cwd, worktreeSetup, taskIdx);
2591
- flushPendingStepSteers(fi);
2592
-
2593
- const singleResult = await runSingleStep(taskForRun, {
2594
- previousOutput, placeholder, cwd: taskCwd, sessionEnabled,
2595
- outputs,
2596
- sessionDir: taskSessionDir,
2597
- artifactsDir, artifactConfig, id,
2598
- flatIndex: fi, flatStepCount: Math.max(statusPayload.steps.length, 1),
2599
- outputFile: path.join(asyncDir, `output-${fi}.log`),
2600
- steerInboxDir: stepSteerInboxDir(asyncDir, fi),
2601
- piPackageRoot: config.piPackageRoot,
2602
- piArgv1: config.piArgv1,
2603
- childIntercomTarget: config.childIntercomTargets?.[fi],
2604
- orchestratorIntercomTarget: config.controlIntercomTarget,
2605
- nestedRoute: config.nestedRoute,
2606
- registerInterrupt: (interrupt) => registerStepInterrupt(fi, interrupt),
2607
- registerTimeout: (interrupt) => registerStepTimeout(fi, interrupt),
2608
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(fi, abort),
2609
- timeoutSignal: timeoutAbortController.signal,
2610
- timeoutMessage,
2611
- turnBudget: config.turnBudget,
2612
- onAttemptStart: (attempt) => updateStepModel(fi, attempt.model, attempt.thinking),
2613
- onChildEvent: (event) => updateStepFromChildEvent(fi, event),
2614
- skipAcceptance: () => timedOut,
2615
- });
2616
- if (task.sessionFile) {
2617
- latestSessionFile = task.sessionFile;
2618
- }
2619
-
2620
- const taskEndTime = Date.now();
2621
- const taskDuration = taskEndTime - taskStartTime;
2622
- const childInterrupted = singleResult.interrupted === true;
2623
-
2624
- statusPayload.steps[fi].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2625
- statusPayload.steps[fi].endedAt = taskEndTime;
2626
- statusPayload.steps[fi].durationMs = taskDuration;
2627
- statusPayload.steps[fi].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2628
- statusPayload.steps[fi].timedOut = timedOut || singleResult.timedOut ? true : undefined;
2629
- statusPayload.steps[fi].turnBudget = singleResult.turnBudget;
2630
- statusPayload.steps[fi].turnBudgetExceeded = singleResult.turnBudgetExceeded;
2631
- statusPayload.steps[fi].wrapUpRequested = singleResult.wrapUpRequested;
2632
- statusPayload.steps[fi].toolBudget = singleResult.toolBudget;
2633
- statusPayload.steps[fi].toolBudgetBlocked = singleResult.toolBudgetBlocked;
2634
- if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
2635
- if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
2636
- if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
2637
- if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
2638
- if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
2639
- statusPayload.steps[fi].model = singleResult.model;
2640
- statusPayload.steps[fi].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[fi].thinking);
2641
- statusPayload.steps[fi].attemptedModels = singleResult.attemptedModels;
2642
- statusPayload.steps[fi].modelAttempts = singleResult.modelAttempts;
2643
- statusPayload.steps[fi].totalCost = singleResult.totalCost;
2644
- statusPayload.steps[fi].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2645
- statusPayload.steps[fi].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[fi].transcriptPath;
2646
- statusPayload.steps[fi].transcriptError = singleResult.transcriptError;
2647
- statusPayload.steps[fi].structuredOutput = singleResult.structuredOutput;
2648
- statusPayload.steps[fi].structuredOutputPath = singleResult.structuredOutputPath;
2649
- statusPayload.steps[fi].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2650
- statusPayload.steps[fi].acceptance = singleResult.acceptance;
2651
- statusPayload.lastUpdate = taskEndTime;
2652
- writeStatusPayload();
2653
-
2654
- appendJsonl(eventsPath, JSON.stringify({
2655
- type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2656
- ts: taskEndTime, runId: id, stepIndex: fi, agent: task.agent,
2657
- exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode, durationMs: taskDuration,
2658
- }));
2659
- if (singleResult.completionGuardTriggered) {
2660
- const event = buildControlEvent({
2661
- from: statusPayload.steps[fi].activityState,
2662
- to: "needs_attention",
2663
- runId: id,
2664
- agent: task.agent,
2665
- index: fi,
2666
- ts: taskEndTime,
2667
- message: `${task.agent} completed without making edits for an implementation task`,
2668
- reason: "completion_guard",
2669
- });
2670
- appendControlEvent(event);
2671
- }
2672
-
2673
- if (singleResult.exitCode !== 0 && failFast) aborted = true;
2674
- return timedOut ? { ...singleResult, output: timeoutMessage ?? "Subagent timed out.", error: timeoutMessage ?? "Subagent timed out.", exitCode: 1, interrupted: false, timedOut: true, skipped: false } : { ...singleResult, skipped: false };
2675
- },
2676
- globalSemaphore,
2677
- );
2678
-
2679
- flatIndex += group.parallel.length;
2680
-
2681
- for (let t = 0; t < group.parallel.length; t++) {
2682
- const fi = groupStartFlatIndex + t;
2683
- const sessionTokens = config.sessionDir
2684
- ? parseSessionTokens(path.join(config.sessionDir, `parallel-${t}`))
2685
- : null;
2686
- const taskTokens = sessionTokens ?? tokenUsageFromAttempts(parallelResults[t]?.modelAttempts);
2687
- if (!taskTokens) continue;
2688
- statusPayload.steps[fi].tokens = taskTokens;
2689
- previousCumulativeTokens = {
2690
- input: previousCumulativeTokens.input + taskTokens.input,
2691
- output: previousCumulativeTokens.output + taskTokens.output,
2692
- total: previousCumulativeTokens.total + taskTokens.total,
2693
- };
2694
- }
2695
- statusPayload.totalTokens = { ...previousCumulativeTokens };
2696
- statusPayload.lastUpdate = Date.now();
2697
- writeStatusPayload();
2698
-
2699
- for (const pr of parallelResults) {
2700
- results.push({
2701
- agent: pr.agent,
2702
- output: pr.output,
2703
- error: pr.error,
2704
- success: pr.interrupted !== true && pr.exitCode === 0,
2705
- exitCode: pr.interrupted === true ? 0 : pr.exitCode,
2706
- skipped: pr.skipped,
2707
- interrupted: pr.interrupted,
2708
- timedOut: pr.timedOut,
2709
- turnBudget: pr.turnBudget,
2710
- turnBudgetExceeded: pr.turnBudgetExceeded,
2711
- wrapUpRequested: pr.wrapUpRequested,
2712
- toolBudget: pr.toolBudget,
2713
- toolBudgetBlocked: pr.toolBudgetBlocked,
2714
- sessionFile: pr.sessionFile,
2715
- intercomTarget: pr.intercomTarget,
2716
- model: pr.model,
2717
- attemptedModels: pr.attemptedModels,
2718
- modelAttempts: pr.modelAttempts,
2719
- totalCost: pr.totalCost,
2720
- artifactPaths: pr.artifactPaths,
2721
- transcriptPath: pr.transcriptPath,
2722
- transcriptError: pr.transcriptError,
2723
- structuredOutput: pr.structuredOutput,
2724
- structuredOutputPath: pr.structuredOutputPath,
2725
- structuredOutputSchemaPath: pr.structuredOutputSchemaPath,
2726
- acceptance: pr.acceptance,
2727
- });
2728
- }
2729
- for (let t = 0; t < group.parallel.length; t++) {
2730
- const outputName = group.parallel[t]?.outputName;
2731
- if (outputName) outputs[outputName] = outputEntryFromAsyncResult({
2732
- agent: parallelResults[t]!.agent,
2733
- output: parallelResults[t]!.output,
2734
- structuredOutput: parallelResults[t]!.structuredOutput,
2735
- }, stepIndex);
2736
- }
2737
- statusPayload.outputs = outputs;
2738
-
2739
- previousOutput = aggregateParallelOutputs(
2740
- parallelResults.map((r) => ({
2741
- agent: r.agent,
2742
- output: r.output,
2743
- exitCode: r.exitCode,
2744
- error: r.error,
2745
- model: r.model,
2746
- attemptedModels: r.attemptedModels,
2747
- })),
2748
- );
2749
- previousOutput = appendParallelWorktreeSummary(previousOutput, worktreeSetup, asyncDir, stepIndex, group);
2750
-
2751
- appendJsonl(eventsPath, JSON.stringify({
2752
- type: "subagent.parallel.completed",
2753
- ts: Date.now(),
2754
- runId: id,
2755
- stepIndex,
2756
- success: parallelResults.every((r) => r.exitCode === 0 || r.exitCode === -1),
2757
- }));
2758
-
2759
- if (parallelResults.some((r) => r.exitCode !== 0 && r.exitCode !== -1)) {
2760
- break;
2761
- }
2762
- } finally {
2763
- if (worktreeSetup) cleanupWorktrees(worktreeSetup);
2764
- }
2765
- } else {
2766
- const seqStep = step as SubagentStep;
2767
- const stepStartTime = Date.now();
2768
- statusPayload.currentStep = flatIndex;
2769
- statusPayload.steps[flatIndex].status = "running";
2770
- statusPayload.steps[flatIndex].activityState = undefined;
2771
- statusPayload.activityState = undefined;
2772
- resetStepLiveDetail(statusPayload.steps[flatIndex]);
2773
- statusPayload.steps[flatIndex].skills = seqStep.skills;
2774
- statusPayload.steps[flatIndex].startedAt = stepStartTime;
2775
- statusPayload.steps[flatIndex].lastActivityAt = stepStartTime;
2776
- statusPayload.lastActivityAt = stepStartTime;
2777
- statusPayload.lastUpdate = stepStartTime;
2778
- statusPayload.outputFile = path.join(asyncDir, `output-${flatIndex}.log`);
2779
- writeStatusPayload();
2780
-
2781
- appendJsonl(eventsPath, JSON.stringify({
2782
- type: "subagent.step.started",
2783
- ts: stepStartTime,
2784
- runId: id,
2785
- stepIndex: flatIndex,
2786
- agent: seqStep.agent,
2787
- }));
2788
-
2789
- flushPendingStepSteers(flatIndex);
2790
- const singleResult = await runSingleStep(seqStep, {
2791
- previousOutput, placeholder, cwd, sessionEnabled,
2792
- outputs,
2793
- sessionDir: config.sessionDir,
2794
- artifactsDir, artifactConfig, id,
2795
- flatIndex, flatStepCount: Math.max(statusPayload.steps.length, 1),
2796
- outputFile: path.join(asyncDir, `output-${flatIndex}.log`),
2797
- steerInboxDir: stepSteerInboxDir(asyncDir, flatIndex),
2798
- piPackageRoot: config.piPackageRoot,
2799
- piArgv1: config.piArgv1,
2800
- childIntercomTarget: config.childIntercomTargets?.[flatIndex],
2801
- orchestratorIntercomTarget: config.controlIntercomTarget,
2802
- nestedRoute: config.nestedRoute,
2803
- registerInterrupt: (interrupt) => registerStepInterrupt(flatIndex, interrupt),
2804
- registerTimeout: (interrupt) => registerStepTimeout(flatIndex, interrupt),
2805
- registerTurnBudgetAbort: (abort) => registerStepTurnBudgetAbort(flatIndex, abort),
2806
- timeoutSignal: timeoutAbortController.signal,
2807
- timeoutMessage,
2808
- turnBudget: config.turnBudget,
2809
- onAttemptStart: (attempt) => updateStepModel(flatIndex, attempt.model, attempt.thinking),
2810
- onChildEvent: (event) => updateStepFromChildEvent(flatIndex, event),
2811
- skipAcceptance: () => timedOut,
2812
- });
2813
- if (seqStep.sessionFile) {
2814
- latestSessionFile = seqStep.sessionFile;
2815
- }
2816
-
2817
- previousOutput = singleResult.output;
2818
- results.push({
2819
- agent: singleResult.agent,
2820
- output: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.output,
2821
- error: timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error,
2822
- success: !timedOut && singleResult.interrupted !== true && singleResult.exitCode === 0,
2823
- exitCode: timedOut ? 1 : singleResult.interrupted === true ? 0 : singleResult.exitCode,
2824
- sessionFile: singleResult.sessionFile,
2825
- intercomTarget: singleResult.intercomTarget,
2826
- model: singleResult.model,
2827
- attemptedModels: singleResult.attemptedModels,
2828
- modelAttempts: singleResult.modelAttempts,
2829
- totalCost: singleResult.totalCost,
2830
- artifactPaths: singleResult.artifactPaths,
2831
- transcriptPath: singleResult.transcriptPath,
2832
- transcriptError: singleResult.transcriptError,
2833
- structuredOutput: singleResult.structuredOutput,
2834
- structuredOutputPath: singleResult.structuredOutputPath,
2835
- structuredOutputSchemaPath: singleResult.structuredOutputSchemaPath,
2836
- acceptance: singleResult.acceptance,
2837
- interrupted: singleResult.interrupted,
2838
- timedOut: timedOut || singleResult.timedOut ? true : undefined,
2839
- turnBudget: singleResult.turnBudget,
2840
- turnBudgetExceeded: singleResult.turnBudgetExceeded,
2841
- wrapUpRequested: singleResult.wrapUpRequested,
2842
- toolBudget: singleResult.toolBudget,
2843
- toolBudgetBlocked: singleResult.toolBudgetBlocked,
2844
- });
2845
- if (seqStep.outputName) {
2846
- outputs[seqStep.outputName] = outputEntryFromAsyncResult({
2847
- agent: singleResult.agent,
2848
- output: singleResult.output,
2849
- structuredOutput: singleResult.structuredOutput,
2850
- }, stepIndex);
2851
- }
2852
- statusPayload.outputs = outputs;
2853
-
2854
- const cumulativeTokens = config.sessionDir ? parseSessionTokens(config.sessionDir) : null;
2855
- let stepTokens: TokenUsage | null = cumulativeTokens
2856
- ? {
2857
- input: cumulativeTokens.input - previousCumulativeTokens.input,
2858
- output: cumulativeTokens.output - previousCumulativeTokens.output,
2859
- total: cumulativeTokens.total - previousCumulativeTokens.total,
2860
- }
2861
- : null;
2862
- if (cumulativeTokens) {
2863
- previousCumulativeTokens = cumulativeTokens;
2864
- } else {
2865
- stepTokens = tokenUsageFromAttempts(singleResult.modelAttempts);
2866
- if (stepTokens) {
2867
- previousCumulativeTokens = {
2868
- input: previousCumulativeTokens.input + stepTokens.input,
2869
- output: previousCumulativeTokens.output + stepTokens.output,
2870
- total: previousCumulativeTokens.total + stepTokens.total,
2871
- };
2872
- }
2873
- }
2874
-
2875
- const stepEndTime = Date.now();
2876
- const childInterrupted = singleResult.interrupted === true;
2877
- statusPayload.steps[flatIndex].status = timedOut ? "failed" : childInterrupted ? "paused" : singleResult.exitCode === 0 ? "complete" : "failed";
2878
- statusPayload.steps[flatIndex].endedAt = stepEndTime;
2879
- statusPayload.steps[flatIndex].durationMs = stepEndTime - stepStartTime;
2880
- statusPayload.steps[flatIndex].exitCode = timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode;
2881
- statusPayload.steps[flatIndex].timedOut = timedOut || singleResult.timedOut ? true : undefined;
2882
- statusPayload.steps[flatIndex].turnBudget = singleResult.turnBudget;
2883
- statusPayload.steps[flatIndex].turnBudgetExceeded = singleResult.turnBudgetExceeded;
2884
- statusPayload.steps[flatIndex].wrapUpRequested = singleResult.wrapUpRequested;
2885
- statusPayload.steps[flatIndex].toolBudget = singleResult.toolBudget;
2886
- statusPayload.steps[flatIndex].toolBudgetBlocked = singleResult.toolBudgetBlocked;
2887
- if (singleResult.toolBudget) statusPayload.toolBudget = singleResult.toolBudget;
2888
- if (singleResult.toolBudgetBlocked) statusPayload.toolBudgetBlocked = true;
2889
- if (singleResult.turnBudget) statusPayload.turnBudget = singleResult.turnBudget;
2890
- if (singleResult.turnBudgetExceeded) statusPayload.turnBudgetExceeded = true;
2891
- if (singleResult.wrapUpRequested) statusPayload.wrapUpRequested = true;
2892
- statusPayload.steps[flatIndex].model = singleResult.model;
2893
- statusPayload.steps[flatIndex].thinking = resolveEffectiveThinking(singleResult.model, statusPayload.steps[flatIndex].thinking);
2894
- statusPayload.steps[flatIndex].attemptedModels = singleResult.attemptedModels;
2895
- statusPayload.steps[flatIndex].modelAttempts = singleResult.modelAttempts;
2896
- statusPayload.steps[flatIndex].totalCost = singleResult.totalCost;
2897
- statusPayload.steps[flatIndex].error = timedOut ? (timeoutMessage ?? "Subagent timed out.") : singleResult.error;
2898
- statusPayload.steps[flatIndex].transcriptPath = singleResult.transcriptPath ?? statusPayload.steps[flatIndex].transcriptPath;
2899
- statusPayload.steps[flatIndex].transcriptError = singleResult.transcriptError;
2900
- statusPayload.steps[flatIndex].structuredOutput = singleResult.structuredOutput;
2901
- statusPayload.steps[flatIndex].structuredOutputPath = singleResult.structuredOutputPath;
2902
- statusPayload.steps[flatIndex].structuredOutputSchemaPath = singleResult.structuredOutputSchemaPath;
2903
- statusPayload.steps[flatIndex].acceptance = singleResult.acceptance;
2904
- if (stepTokens) {
2905
- statusPayload.steps[flatIndex].tokens = stepTokens;
2906
- statusPayload.totalTokens = { ...previousCumulativeTokens };
2907
- }
2908
- statusPayload.lastUpdate = stepEndTime;
2909
- writeStatusPayload();
2910
-
2911
- appendJsonl(eventsPath, JSON.stringify({
2912
- type: timedOut ? "subagent.step.failed" : childInterrupted ? "subagent.step.paused" : singleResult.exitCode === 0 ? "subagent.step.completed" : "subagent.step.failed",
2913
- ts: stepEndTime,
2914
- runId: id,
2915
- stepIndex: flatIndex,
2916
- agent: seqStep.agent,
2917
- exitCode: timedOut ? 1 : childInterrupted ? 0 : singleResult.exitCode,
2918
- durationMs: stepEndTime - stepStartTime,
2919
- tokens: stepTokens,
2920
- }));
2921
- if (singleResult.completionGuardTriggered) {
2922
- const event = buildControlEvent({
2923
- from: statusPayload.steps[flatIndex].activityState,
2924
- to: "needs_attention",
2925
- runId: id,
2926
- agent: seqStep.agent,
2927
- index: flatIndex,
2928
- ts: stepEndTime,
2929
- message: `${seqStep.agent} completed without making edits for an implementation task`,
2930
- reason: "completion_guard",
2931
- });
2932
- appendControlEvent(event);
2933
- }
2934
-
2935
- flatIndex++;
2936
- if (singleResult.exitCode !== 0) {
2937
- break;
2938
- }
2939
- }
2940
- }
2941
-
2942
- let summary = results.map((r) => `${r.agent}:\n${r.output}`).join("\n\n");
2943
- let truncated = false;
2944
-
2945
- if (maxOutput) {
2946
- const config = { ...DEFAULT_MAX_OUTPUT, ...maxOutput };
2947
- const lastArtifactPath = results[results.length - 1]?.artifactPaths?.outputPath;
2948
- const truncResult = truncateOutput(summary, config, lastArtifactPath);
2949
- if (truncResult.truncated) {
2950
- summary = truncResult.text;
2951
- truncated = true;
2952
- }
2953
- }
2954
-
2955
- const resultMode = config.resultMode ?? statusPayload.mode;
2956
- const totalCost = results.reduce<CostSummary>((sum, result) => ({
2957
- inputTokens: sum.inputTokens + (result.totalCost?.inputTokens ?? 0),
2958
- outputTokens: sum.outputTokens + (result.totalCost?.outputTokens ?? 0),
2959
- costUsd: sum.costUsd + (result.totalCost?.costUsd ?? 0),
2960
- }), { inputTokens: 0, outputTokens: 0, costUsd: 0 });
2961
- const finalTotalCost = totalCost.inputTokens > 0 || totalCost.outputTokens > 0 || totalCost.costUsd > 0 ? totalCost : undefined;
2962
- const finalFlatAgents = statusPayload.steps.map((step) => step.agent);
2963
- const agentName = finalFlatAgents.length === 1
2964
- ? finalFlatAgents[0]!
2965
- : resultMode === "parallel"
2966
- ? `parallel:${finalFlatAgents.join("+")}`
2967
- : `chain:${finalFlatAgents.join("->")}`;
2968
- let sessionFile: string | undefined;
2969
- let shareUrl: string | undefined;
2970
- let gistUrl: string | undefined;
2971
- let shareError: string | undefined;
2972
-
2973
- if (shareEnabled) {
2974
- sessionFile = config.sessionDir
2975
- ? (findLatestSessionFile(config.sessionDir) ?? undefined)
2976
- : undefined;
2977
- if (!sessionFile && latestSessionFile) {
2978
- sessionFile = latestSessionFile;
2979
- }
2980
- if (sessionFile) {
2981
- try {
2982
- const exportDir = config.sessionDir ?? path.dirname(sessionFile);
2983
- const htmlPath = await exportSessionHtml(sessionFile, exportDir, config.piPackageRoot);
2984
- const share = createShareLink(htmlPath);
2985
- if ("error" in share) shareError = share.error;
2986
- else {
2987
- shareUrl = share.shareUrl;
2988
- gistUrl = share.gistUrl;
2989
- }
2990
- } catch (err) {
2991
- shareError = String(err);
2992
- }
2993
- } else {
2994
- shareError = "Session file not found.";
2995
- }
2996
- }
2997
-
2998
- if (activityTimer) {
2999
- clearInterval(activityTimer);
3000
- activityTimer = undefined;
3001
- }
3002
- if (timeoutTimer) {
3003
- clearTimeout(timeoutTimer);
3004
- timeoutTimer = undefined;
3005
- }
3006
- disposeControlInbox();
3007
- const effectiveSessionFile = sessionFile ?? latestSessionFile;
3008
- const runEndedAt = Date.now();
3009
- statusPayload.state = timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed";
3010
- statusPayload.activityState = undefined;
3011
- if (timedOut) {
3012
- statusPayload.timedOut = true;
3013
- statusPayload.error = timeoutMessage ?? "Subagent timed out.";
3014
- }
3015
- if (turnBudgetExceeded && !statusPayload.error) {
3016
- const budget = statusPayload.turnBudget;
3017
- statusPayload.error = budget ? turnBudgetExceededMessage(budget, budget.turnCount) : "Subagent exceeded turn budget.";
3018
- }
3019
- statusPayload.endedAt = runEndedAt;
3020
- statusPayload.lastUpdate = runEndedAt;
3021
- statusPayload.sessionFile = effectiveSessionFile;
3022
- statusPayload.totalCost = finalTotalCost;
3023
- statusPayload.shareUrl = shareUrl;
3024
- statusPayload.gistUrl = gistUrl;
3025
- statusPayload.shareError = shareError;
3026
- if (statusPayload.state === "failed" && !statusPayload.error) {
3027
- const failedStep = statusPayload.steps.find((s) => s.status === "failed");
3028
- if (failedStep?.agent) {
3029
- statusPayload.error = `Step failed: ${failedStep.agent}`;
3030
- }
3031
- }
3032
- writeStatusPayload();
3033
- appendJsonl(
3034
- eventsPath,
3035
- JSON.stringify({
3036
- type: "subagent.run.completed",
3037
- lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
3038
- ts: runEndedAt,
3039
- runId: id,
3040
- status: statusPayload.state,
3041
- durationMs: runEndedAt - overallStartTime,
3042
- totalTokens: statusPayload.totalTokens,
3043
- totalCost: finalTotalCost,
3044
- }),
3045
- );
3046
- writeRunLog(logPath, {
3047
- id,
3048
- mode: statusPayload.mode,
3049
- cwd,
3050
- startedAt: overallStartTime,
3051
- endedAt: runEndedAt,
3052
- steps: statusPayload.steps.map((step) => ({
3053
- agent: step.agent,
3054
- status: step.status,
3055
- durationMs: step.durationMs,
3056
- })),
3057
- summary,
3058
- truncated,
3059
- artifactsDir,
3060
- sessionFile: effectiveSessionFile,
3061
- shareUrl,
3062
- shareError,
3063
- });
3064
-
3065
- try {
3066
- writeAtomicJson(resultPath, {
3067
- lifecycleArtifactVersion: SUBAGENT_LIFECYCLE_ARTIFACT_VERSION,
3068
- id,
3069
- agent: agentName,
3070
- mode: resultMode,
3071
- success: !timedOut && !turnBudgetExceeded && !interrupted && results.every((r) => r.success),
3072
- state: timedOut || turnBudgetExceeded ? "failed" : interrupted ? "paused" : results.every((r) => r.success) ? "complete" : "failed",
3073
- summary: timedOut ? (timeoutMessage ?? "Subagent timed out.") : turnBudgetExceeded ? (statusPayload.error ?? "Subagent exceeded turn budget.") : interrupted ? "Paused after interrupt. Waiting for explicit next action." : summary,
3074
- ...(config.timeoutMs !== undefined ? { timeoutMs: config.timeoutMs } : {}),
3075
- ...(config.deadlineAt !== undefined ? { deadlineAt: config.deadlineAt } : {}),
3076
- ...(statusPayload.turnBudget ? { turnBudget: statusPayload.turnBudget } : {}),
3077
- ...(statusPayload.turnBudgetExceeded ? { turnBudgetExceeded: true } : {}),
3078
- ...(statusPayload.wrapUpRequested ? { wrapUpRequested: true } : {}),
3079
- ...(statusPayload.toolBudget ? { toolBudget: statusPayload.toolBudget } : {}),
3080
- ...(statusPayload.toolBudgetBlocked ? { toolBudgetBlocked: true } : {}),
3081
- ...(timedOut ? { timedOut: true, error: timeoutMessage ?? "Subagent timed out." } : turnBudgetExceeded ? { error: statusPayload.error ?? "Subagent exceeded turn budget." } : {}),
3082
- results: results.map((r) => ({
3083
- agent: r.agent,
3084
- output: r.output,
3085
- error: r.error,
3086
- success: r.success,
3087
- skipped: r.skipped || undefined,
3088
- interrupted: r.interrupted || undefined,
3089
- timedOut: r.timedOut || undefined,
3090
- turnBudget: r.turnBudget,
3091
- turnBudgetExceeded: r.turnBudgetExceeded || undefined,
3092
- wrapUpRequested: r.wrapUpRequested || undefined,
3093
- toolBudget: r.toolBudget,
3094
- toolBudgetBlocked: r.toolBudgetBlocked || undefined,
3095
- sessionFile: r.sessionFile,
3096
- intercomTarget: r.intercomTarget,
3097
- model: r.model,
3098
- attemptedModels: r.attemptedModels,
3099
- modelAttempts: r.modelAttempts,
3100
- totalCost: r.totalCost,
3101
- artifactPaths: r.artifactPaths,
3102
- truncated: r.truncated,
3103
- transcriptPath: r.transcriptPath,
3104
- transcriptError: r.transcriptError,
3105
- structuredOutput: r.structuredOutput,
3106
- structuredOutputPath: r.structuredOutputPath,
3107
- structuredOutputSchemaPath: r.structuredOutputSchemaPath,
3108
- acceptance: r.acceptance,
3109
- })),
3110
- outputs,
3111
- workflowGraph: statusPayload.workflowGraph,
3112
- exitCode: timedOut || turnBudgetExceeded ? 1 : interrupted || results.every((r) => r.success) ? 0 : 1,
3113
- timestamp: runEndedAt,
3114
- durationMs: runEndedAt - overallStartTime,
3115
- totalTokens: statusPayload.totalTokens,
3116
- totalCost: finalTotalCost,
3117
- truncated,
3118
- artifactsDir,
3119
- cwd,
3120
- asyncDir,
3121
- sessionId: config.sessionId,
3122
- sessionFile: effectiveSessionFile,
3123
- intercomTarget: config.controlIntercomTarget,
3124
- shareUrl,
3125
- gistUrl,
3126
- shareError,
3127
- ...(taskIndex !== undefined && { taskIndex }),
3128
- ...(totalTasks !== undefined && { totalTasks }),
3129
- });
3130
- } catch (err) {
3131
- console.error(`Failed to write result file ${resultPath}:`, err);
3132
- }
3133
- }
3134
-
3135
- const configArg = process.argv[2];
3136
- if (configArg) {
3137
- try {
3138
- const configJson = fs.readFileSync(configArg, "utf-8");
3139
- const config = JSON.parse(configJson) as SubagentRunConfig;
3140
- try {
3141
- fs.unlinkSync(configArg);
3142
- } catch {
3143
- // Temp config cleanup is best effort.
3144
- }
3145
- runSubagent(config).catch((runErr) => {
3146
- console.error("Subagent runner error:", runErr);
3147
- process.exit(1);
3148
- });
3149
- } catch (err) {
3150
- console.error("Subagent runner error:", err);
3151
- process.exit(1);
3152
- }
3153
- } else {
3154
- let input = "";
3155
- process.stdin.setEncoding("utf-8");
3156
- process.stdin.on("data", (chunk) => {
3157
- input += chunk;
3158
- });
3159
- process.stdin.on("end", () => {
3160
- try {
3161
- const config = JSON.parse(input) as SubagentRunConfig;
3162
- runSubagent(config).catch((runErr) => {
3163
- console.error("Subagent runner error:", runErr);
3164
- process.exit(1);
3165
- });
3166
- } catch (err) {
3167
- console.error("Subagent runner error:", err);
3168
- process.exit(1);
3169
- }
3170
- });
3171
- }