@fitchmultz/pi-subagents 0.0.0-stage → 0.44.4

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 (342) hide show
  1. package/CHANGELOG.md +1645 -0
  2. package/LICENSE +21 -0
  3. package/README.md +1529 -2
  4. package/agents/context-builder.md +97 -0
  5. package/agents/debugger.md +51 -0
  6. package/agents/delegate.md +13 -0
  7. package/agents/fixer.md +37 -0
  8. package/agents/oracle.md +71 -0
  9. package/agents/planner.md +72 -0
  10. package/agents/researcher.md +51 -0
  11. package/agents/reviewer-claude.md +69 -0
  12. package/agents/reviewer-gpt.md +68 -0
  13. package/agents/reviewer-ponytail.md +82 -0
  14. package/agents/reviewer-security.md +81 -0
  15. package/agents/reviewer.md +69 -0
  16. package/agents/scout.md +66 -0
  17. package/agents/ui-designer.md +51 -0
  18. package/agents/watcher.md +26 -0
  19. package/agents/worker.md +46 -0
  20. package/agents/writer.md +36 -0
  21. package/dist/agents/agent-management.js +923 -0
  22. package/dist/agents/agent-scope.js +7 -0
  23. package/dist/agents/agent-selection.js +20 -0
  24. package/dist/agents/agent-serializer.js +100 -0
  25. package/dist/agents/agents.js +752 -0
  26. package/dist/agents/chain-serializer.js +367 -0
  27. package/dist/agents/frontmatter.js +21 -0
  28. package/dist/agents/identity.js +29 -0
  29. package/dist/agents/skills.js +570 -0
  30. package/dist/extension/build-info.js +1 -0
  31. package/dist/extension/compact-tools.js +112 -0
  32. package/dist/extension/config.js +22 -0
  33. package/dist/extension/control-notices.js +29 -0
  34. package/dist/extension/doctor.js +143 -0
  35. package/dist/extension/fanout-child.js +326 -0
  36. package/dist/extension/index.js +577 -0
  37. package/dist/extension/schemas.js +416 -0
  38. package/dist/extension/tool-input.js +20 -0
  39. package/dist/extension/tool-result.js +14 -0
  40. package/dist/history/canonical-result.js +81 -0
  41. package/dist/history/database.js +72 -0
  42. package/dist/history/index.js +194 -0
  43. package/dist/history/ingest.js +447 -0
  44. package/dist/history/queries.js +350 -0
  45. package/dist/history/store.js +216 -0
  46. package/dist/history/types.js +4 -0
  47. package/dist/history/worker.js +461 -0
  48. package/dist/intercom/intercom-bridge.js +112 -0
  49. package/dist/intercom/live-intercom.js +94 -0
  50. package/dist/intercom/result-intercom.js +373 -0
  51. package/dist/pi-intercom/agent-dir.js +10 -0
  52. package/dist/pi-intercom/broker/broker.js +426 -0
  53. package/dist/pi-intercom/broker/client.js +492 -0
  54. package/dist/pi-intercom/broker/framing.js +65 -0
  55. package/dist/pi-intercom/broker/paths.js +42 -0
  56. package/dist/pi-intercom/broker/pid.js +66 -0
  57. package/dist/pi-intercom/broker/spawn.js +221 -0
  58. package/dist/pi-intercom/config.js +102 -0
  59. package/dist/pi-intercom/index.js +2942 -0
  60. package/dist/pi-intercom/reply-tracker.js +182 -0
  61. package/dist/pi-intercom/session-targets.js +128 -0
  62. package/dist/pi-intercom/subagent-live-events.js +132 -0
  63. package/dist/pi-intercom/topics.js +157 -0
  64. package/dist/pi-intercom/types.js +163 -0
  65. package/dist/pi-intercom/ui/compose.js +269 -0
  66. package/dist/pi-intercom/ui/inline-message.js +80 -0
  67. package/dist/pi-intercom/ui/session-list.js +124 -0
  68. package/dist/runs/background/async-control.js +95 -0
  69. package/dist/runs/background/async-execution.js +676 -0
  70. package/dist/runs/background/async-job-tracker.js +383 -0
  71. package/dist/runs/background/async-result-file.js +58 -0
  72. package/dist/runs/background/async-resume.js +305 -0
  73. package/dist/runs/background/async-status.js +442 -0
  74. package/dist/runs/background/completion-dedupe.js +69 -0
  75. package/dist/runs/background/completion-delivery.js +245 -0
  76. package/dist/runs/background/notify.js +71 -0
  77. package/dist/runs/background/parallel-groups.js +55 -0
  78. package/dist/runs/background/result-watcher.js +426 -0
  79. package/dist/runs/background/run-id-resolver.js +64 -0
  80. package/dist/runs/background/run-status.js +382 -0
  81. package/dist/runs/background/stale-run-reconciler.js +335 -0
  82. package/dist/runs/background/subagent-runner-launcher.js +55 -0
  83. package/dist/runs/background/subagent-runner.js +1872 -0
  84. package/dist/runs/background/top-level-async.js +5 -0
  85. package/dist/runs/foreground/chain-clarify.js +1152 -0
  86. package/dist/runs/foreground/clarify-invocation.js +72 -0
  87. package/dist/runs/foreground/execution-input.js +423 -0
  88. package/dist/runs/foreground/foreground-control.js +817 -0
  89. package/dist/runs/foreground/question-control.js +77 -0
  90. package/dist/runs/foreground/run-async-path.js +205 -0
  91. package/dist/runs/foreground/subagent-executor.js +442 -0
  92. package/dist/runs/foreground/subagent-params.js +165 -0
  93. package/dist/runs/foreground/wait-run.js +139 -0
  94. package/dist/runs/shared/acceptance-contract.js +253 -0
  95. package/dist/runs/shared/acceptance-evaluation.js +256 -0
  96. package/dist/runs/shared/acceptance-finalization.js +280 -0
  97. package/dist/runs/shared/acceptance-reports.js +147 -0
  98. package/dist/runs/shared/acceptance.js +4 -0
  99. package/dist/runs/shared/chain-outputs.js +110 -0
  100. package/dist/runs/shared/child-attempt.js +693 -0
  101. package/dist/runs/shared/child-execution-cwd.js +96 -0
  102. package/dist/runs/shared/child-observations.js +114 -0
  103. package/dist/runs/shared/claude-code.js +199 -0
  104. package/dist/runs/shared/completion-guard.js +31 -0
  105. package/dist/runs/shared/dynamic-fanout.js +274 -0
  106. package/dist/runs/shared/history-index.js +72 -0
  107. package/dist/runs/shared/mcp-direct-tool-allowlist.js +334 -0
  108. package/dist/runs/shared/model-fallback.js +185 -0
  109. package/dist/runs/shared/mutating-tool-guard.js +170 -0
  110. package/dist/runs/shared/native-finalization.js +101 -0
  111. package/dist/runs/shared/native-usage.js +153 -0
  112. package/dist/runs/shared/nested-events.js +700 -0
  113. package/dist/runs/shared/nested-path.js +48 -0
  114. package/dist/runs/shared/nested-render.js +108 -0
  115. package/dist/runs/shared/parallel-utils.js +61 -0
  116. package/dist/runs/shared/parent-receipts.js +68 -0
  117. package/dist/runs/shared/parent-usage.js +121 -0
  118. package/dist/runs/shared/pi-args.js +310 -0
  119. package/dist/runs/shared/pi-spawn.js +52 -0
  120. package/dist/runs/shared/run-history-queries.js +45 -0
  121. package/dist/runs/shared/run-history.js +103 -0
  122. package/dist/runs/shared/run-id-ambiguity.js +4 -0
  123. package/dist/runs/shared/run-records.js +679 -0
  124. package/dist/runs/shared/session-cwd-preload.js +33 -0
  125. package/dist/runs/shared/single-output.js +222 -0
  126. package/dist/runs/shared/streaming-text.js +15 -0
  127. package/dist/runs/shared/structured-output.js +55 -0
  128. package/dist/runs/shared/subagent-control.js +179 -0
  129. package/dist/runs/shared/subagent-prompt-runtime.js +197 -0
  130. package/dist/runs/shared/subagent-tool-loop-guard.js +66 -0
  131. package/dist/runs/shared/supervisor-questions.js +320 -0
  132. package/dist/runs/shared/workflow-graph.js +214 -0
  133. package/dist/runs/shared/workflow-policy.js +84 -0
  134. package/dist/runs/shared/worktree.js +525 -0
  135. package/dist/shared/agent-context-policy.js +126 -0
  136. package/dist/shared/artifacts.js +96 -0
  137. package/dist/shared/atomic-json.js +13 -0
  138. package/dist/shared/cooperative.js +20 -0
  139. package/dist/shared/execution-cwd.js +14 -0
  140. package/dist/shared/file-coalescer.js +21 -0
  141. package/dist/shared/fork-context.js +49 -0
  142. package/dist/shared/formatters.js +85 -0
  143. package/dist/shared/journal-reader.js +407 -0
  144. package/dist/shared/lazy-tools.js +32 -0
  145. package/dist/shared/model-info.js +68 -0
  146. package/dist/shared/native-session.js +15 -0
  147. package/dist/shared/native-tui.js +18 -0
  148. package/dist/shared/native-typebox.js +20 -0
  149. package/dist/shared/post-exit-stdio-guard.js +197 -0
  150. package/dist/shared/prompt-sections.js +8 -0
  151. package/dist/shared/session-entries.js +52 -0
  152. package/dist/shared/session-identity.js +14 -0
  153. package/dist/shared/settings.js +254 -0
  154. package/dist/shared/status-format.js +111 -0
  155. package/dist/shared/temp-root.js +114 -0
  156. package/dist/shared/types.js +207 -0
  157. package/dist/shared/ui-mode.js +3 -0
  158. package/dist/shared/utils.js +334 -0
  159. package/dist/slash/prompt-template-bridge.js +298 -0
  160. package/dist/slash/slash-bridge.js +116 -0
  161. package/dist/slash/slash-commands.js +607 -0
  162. package/dist/slash/slash-live-state.js +246 -0
  163. package/dist/tui/action-hints.js +75 -0
  164. package/dist/tui/agent-history.js +197 -0
  165. package/dist/tui/agent-view.js +1656 -0
  166. package/dist/tui/render.js +1515 -0
  167. package/docs/intercom.md +579 -0
  168. package/docs/performance.md +121 -0
  169. package/package.json +101 -4
  170. package/prompts/gather-context-and-clarify.md +14 -0
  171. package/prompts/parallel-cleanup.md +62 -0
  172. package/prompts/parallel-context-build.md +56 -0
  173. package/prompts/parallel-handoff-plan.md +62 -0
  174. package/prompts/parallel-research.md +51 -0
  175. package/prompts/parallel-review.md +59 -0
  176. package/prompts/review-loop.md +42 -0
  177. package/scripts/build.mjs +134 -0
  178. package/scripts/check-compat.mjs +48 -0
  179. package/scripts/compat-host.mjs +19 -0
  180. package/scripts/linux-smoke.sh +51 -0
  181. package/scripts/local-install-smoke.mjs +66 -0
  182. package/scripts/native-package-smoke.mjs +82 -0
  183. package/scripts/package-smoke.mjs +205 -0
  184. package/scripts/prepare.mjs +67 -0
  185. package/scripts/real-pi-smoke.mjs +415 -0
  186. package/scripts/run-tests.mjs +149 -0
  187. package/skills/pi-intercom/SKILL.md +113 -0
  188. package/skills/pi-intercom/evals/evals.json +29 -0
  189. package/skills/pi-intercom/evals/trigger-evals.json +47 -0
  190. package/skills/pi-intercom/references/peer-sessions.md +69 -0
  191. package/skills/pi-subagents/SKILL.md +76 -0
  192. package/skills/pi-subagents/evals/evals.json +29 -0
  193. package/skills/pi-subagents/evals/trigger-evals.json +42 -0
  194. package/skills/pi-subagents/references/full-orchestration-guide.md +863 -0
  195. package/src/agents/agent-management.ts +813 -0
  196. package/src/agents/agent-scope.ts +7 -0
  197. package/src/agents/agent-selection.ts +23 -0
  198. package/src/agents/agent-serializer.ts +99 -0
  199. package/src/agents/agents.ts +916 -0
  200. package/src/agents/chain-serializer.ts +329 -0
  201. package/src/agents/frontmatter.ts +21 -0
  202. package/src/agents/identity.ts +30 -0
  203. package/src/agents/skills.ts +637 -0
  204. package/src/extension/build-info.ts +2 -0
  205. package/src/extension/compact-tools.ts +114 -0
  206. package/src/extension/config.ts +23 -0
  207. package/src/extension/control-notices.ts +47 -0
  208. package/src/extension/doctor.ts +171 -0
  209. package/src/extension/fanout-child.ts +312 -0
  210. package/src/extension/index.ts +649 -0
  211. package/src/extension/schemas.ts +444 -0
  212. package/src/extension/tool-input.ts +19 -0
  213. package/src/extension/tool-result.ts +16 -0
  214. package/src/history/canonical-result.ts +62 -0
  215. package/src/history/database.ts +59 -0
  216. package/src/history/index.ts +152 -0
  217. package/src/history/ingest.ts +332 -0
  218. package/src/history/queries.ts +241 -0
  219. package/src/history/store.ts +203 -0
  220. package/src/history/types.ts +166 -0
  221. package/src/history/worker.ts +317 -0
  222. package/src/intercom/intercom-bridge.ts +123 -0
  223. package/src/intercom/live-intercom.ts +104 -0
  224. package/src/intercom/result-intercom.ts +429 -0
  225. package/src/pi-intercom/agent-dir.ts +9 -0
  226. package/src/pi-intercom/broker/broker.ts +463 -0
  227. package/src/pi-intercom/broker/client.ts +558 -0
  228. package/src/pi-intercom/broker/framing.ts +77 -0
  229. package/src/pi-intercom/broker/paths.ts +47 -0
  230. package/src/pi-intercom/broker/pid.ts +59 -0
  231. package/src/pi-intercom/broker/spawn.ts +248 -0
  232. package/src/pi-intercom/config.ts +142 -0
  233. package/src/pi-intercom/index.ts +2929 -0
  234. package/src/pi-intercom/reply-tracker.ts +211 -0
  235. package/src/pi-intercom/session-targets.ts +172 -0
  236. package/src/pi-intercom/subagent-live-events.ts +133 -0
  237. package/src/pi-intercom/topics.ts +125 -0
  238. package/src/pi-intercom/types.ts +271 -0
  239. package/src/pi-intercom/ui/compose.ts +289 -0
  240. package/src/pi-intercom/ui/inline-message.ts +92 -0
  241. package/src/pi-intercom/ui/session-list.ts +139 -0
  242. package/src/runs/background/async-control.ts +89 -0
  243. package/src/runs/background/async-execution.ts +850 -0
  244. package/src/runs/background/async-job-tracker.ts +385 -0
  245. package/src/runs/background/async-result-file.ts +62 -0
  246. package/src/runs/background/async-resume.ts +333 -0
  247. package/src/runs/background/async-status.ts +481 -0
  248. package/src/runs/background/completion-dedupe.ts +79 -0
  249. package/src/runs/background/completion-delivery.ts +195 -0
  250. package/src/runs/background/notify.ts +123 -0
  251. package/src/runs/background/parallel-groups.ts +53 -0
  252. package/src/runs/background/result-watcher.ts +423 -0
  253. package/src/runs/background/run-id-resolver.ts +69 -0
  254. package/src/runs/background/run-status.ts +384 -0
  255. package/src/runs/background/stale-run-reconciler.ts +369 -0
  256. package/src/runs/background/subagent-runner-launcher.ts +56 -0
  257. package/src/runs/background/subagent-runner.ts +2053 -0
  258. package/src/runs/background/top-level-async.ts +13 -0
  259. package/src/runs/foreground/chain-clarify.ts +1340 -0
  260. package/src/runs/foreground/clarify-invocation.ts +77 -0
  261. package/src/runs/foreground/execution-input.ts +467 -0
  262. package/src/runs/foreground/foreground-control.ts +821 -0
  263. package/src/runs/foreground/question-control.ts +70 -0
  264. package/src/runs/foreground/run-async-path.ts +243 -0
  265. package/src/runs/foreground/subagent-executor.ts +501 -0
  266. package/src/runs/foreground/subagent-params.ts +320 -0
  267. package/src/runs/foreground/wait-run.ts +121 -0
  268. package/src/runs/shared/acceptance-contract.ts +257 -0
  269. package/src/runs/shared/acceptance-evaluation.ts +275 -0
  270. package/src/runs/shared/acceptance-finalization.ts +353 -0
  271. package/src/runs/shared/acceptance-reports.ts +152 -0
  272. package/src/runs/shared/acceptance.ts +33 -0
  273. package/src/runs/shared/chain-outputs.ts +114 -0
  274. package/src/runs/shared/child-attempt.ts +623 -0
  275. package/src/runs/shared/child-execution-cwd.ts +91 -0
  276. package/src/runs/shared/child-observations.ts +99 -0
  277. package/src/runs/shared/claude-code.ts +243 -0
  278. package/src/runs/shared/completion-guard.ts +35 -0
  279. package/src/runs/shared/dynamic-fanout.ts +295 -0
  280. package/src/runs/shared/history-index.ts +69 -0
  281. package/src/runs/shared/mcp-direct-tool-allowlist.ts +376 -0
  282. package/src/runs/shared/model-fallback.ts +219 -0
  283. package/src/runs/shared/mutating-tool-guard.ts +194 -0
  284. package/src/runs/shared/native-finalization.ts +128 -0
  285. package/src/runs/shared/native-usage.ts +113 -0
  286. package/src/runs/shared/nested-events.ts +752 -0
  287. package/src/runs/shared/nested-path.ts +52 -0
  288. package/src/runs/shared/nested-render.ts +119 -0
  289. package/src/runs/shared/parallel-utils.ts +153 -0
  290. package/src/runs/shared/parent-receipts.ts +59 -0
  291. package/src/runs/shared/parent-usage.ts +121 -0
  292. package/src/runs/shared/pi-args.ts +350 -0
  293. package/src/runs/shared/pi-spawn.ts +52 -0
  294. package/src/runs/shared/run-history-queries.ts +44 -0
  295. package/src/runs/shared/run-history.ts +93 -0
  296. package/src/runs/shared/run-id-ambiguity.ts +4 -0
  297. package/src/runs/shared/run-records.ts +607 -0
  298. package/src/runs/shared/session-cwd-preload.ts +27 -0
  299. package/src/runs/shared/single-output.ts +267 -0
  300. package/src/runs/shared/streaming-text.ts +18 -0
  301. package/src/runs/shared/structured-output.ts +68 -0
  302. package/src/runs/shared/subagent-control.ts +219 -0
  303. package/src/runs/shared/subagent-prompt-runtime.ts +201 -0
  304. package/src/runs/shared/subagent-tool-loop-guard.ts +89 -0
  305. package/src/runs/shared/supervisor-questions.ts +372 -0
  306. package/src/runs/shared/workflow-graph.ts +220 -0
  307. package/src/runs/shared/workflow-policy.ts +125 -0
  308. package/src/runs/shared/worktree.ts +635 -0
  309. package/src/shared/agent-context-policy.ts +184 -0
  310. package/src/shared/artifacts.ts +95 -0
  311. package/src/shared/atomic-json.ts +16 -0
  312. package/src/shared/cooperative.ts +21 -0
  313. package/src/shared/execution-cwd.ts +14 -0
  314. package/src/shared/file-coalescer.ts +29 -0
  315. package/src/shared/fork-context.ts +68 -0
  316. package/src/shared/formatters.ts +84 -0
  317. package/src/shared/journal-reader.ts +333 -0
  318. package/src/shared/lazy-tools.ts +36 -0
  319. package/src/shared/model-info.ts +81 -0
  320. package/src/shared/native-session.ts +8 -0
  321. package/src/shared/native-tui.ts +10 -0
  322. package/src/shared/native-typebox.ts +14 -0
  323. package/src/shared/post-exit-stdio-guard.ts +204 -0
  324. package/src/shared/prompt-sections.ts +10 -0
  325. package/src/shared/session-entries.ts +49 -0
  326. package/src/shared/session-identity.ts +18 -0
  327. package/src/shared/settings.ts +384 -0
  328. package/src/shared/status-format.ts +123 -0
  329. package/src/shared/temp-root.ts +107 -0
  330. package/src/shared/types.ts +1259 -0
  331. package/src/shared/ui-mode.ts +5 -0
  332. package/src/shared/utils.ts +342 -0
  333. package/src/slash/prompt-template-bridge.ts +408 -0
  334. package/src/slash/slash-bridge.ts +178 -0
  335. package/src/slash/slash-commands.ts +617 -0
  336. package/src/slash/slash-live-state.ts +292 -0
  337. package/src/tui/action-hints.ts +69 -0
  338. package/src/tui/agent-history.ts +210 -0
  339. package/src/tui/agent-view.ts +1323 -0
  340. package/src/tui/render.ts +1550 -0
  341. package/tsconfig.build.json +12 -0
  342. package/tsconfig.json +19 -0
@@ -0,0 +1,114 @@
1
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "../shared/native-typebox.ts";
3
+ import { createSubagentExecutor, normalizeSubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
4
+ import type { SubagentExecutionResult, SubagentState } from "../shared/types.ts";
5
+ import { activateTools, restoreLazyTools } from "../shared/lazy-tools.ts";
6
+ import { runHistoryIndex } from "../runs/shared/history-index.ts";
7
+ import type { HistoryRunOptions } from "../history/types.ts";
8
+ import { listSupervisorQuestionsAsync } from "../runs/shared/supervisor-questions.ts";
9
+ import { renderSubagentResult } from "../tui/render.ts";
10
+ import { AgentRunsParams, DelegateParams } from "./schemas.ts";
11
+ import { normalizeEverydayParams } from "./tool-input.ts";
12
+
13
+ type Executor = ReturnType<typeof createSubagentExecutor>;
14
+ type AdaptResult = (result: SubagentExecutionResult, ctx: ExtensionContext) => SubagentExecutionResult;
15
+
16
+ export function registerCompactSubagentTools(pi: ExtensionAPI, options: {
17
+ executor: Executor;
18
+ state: SubagentState;
19
+ adapt: AdaptResult;
20
+ guidelines: readonly string[];
21
+ childSafe?: boolean;
22
+ keepAdvancedActive?: boolean;
23
+ listRuns?: (params: HistoryRunOptions, ctx: ExtensionContext) => Promise<SubagentExecutionResult>;
24
+ asyncByDefault: boolean;
25
+ }): () => Promise<void> {
26
+ const { executor, adapt, guidelines, childSafe, asyncByDefault, state } = options;
27
+ let checking: { owner: string | null | undefined; promise: Promise<void> } | undefined;
28
+ const checkRuns = (): Promise<void> => {
29
+ if (pi.getActiveTools().includes("agent_runs") || !state.ownedRuns?.size) return Promise.resolve();
30
+ const owner = state.lastUiContext?.sessionManager.getSessionId() ?? state.currentSessionId;
31
+ if (checking?.owner === owner) return checking.promise;
32
+ const promise = Promise.resolve().then(async () => {
33
+ try {
34
+ const index = await runHistoryIndex(state);
35
+ const needed = await index.needsControls();
36
+ if (state.historyIndex === index && needed) activateTools(pi, ["agent_runs"]);
37
+ } catch {
38
+ // An unavailable browse index cannot establish that genuine owned work is inert.
39
+ if ((state.lastUiContext?.sessionManager.getSessionId() ?? state.currentSessionId) === owner && state.ownedRuns?.size) activateTools(pi, ["agent_runs"]);
40
+ } finally { if (checking?.promise === promise) checking = undefined; }
41
+ });
42
+ checking = { owner, promise };
43
+ return promise;
44
+ };
45
+ const reconcileRuns = () => checkRuns();
46
+ const onRunsChanged = state.onRunsChanged;
47
+ state.onRunsChanged = () => { reconcileRuns(); onRunsChanged?.(); };
48
+ const asyncDescription = asyncByDefault ? "Background by default; false waits for the result." : "Foreground by default; true detaches work. Use false when the result must appear in your report.";
49
+ pi.registerTool({
50
+ name: "delegate",
51
+ label: "Delegate",
52
+ description: `Delegate one bounded task to a configured agent. For profiles/history, load_subagent({advanced:false}) enables agent_runs. Delegation enables run controls automatically. ${asyncDescription} Use worktree for an isolated writer, acceptance for explicit requirements, and fresh context for independent review. Fresh handoffs must include relevant exact user instructions and settled decisions or readable source references, not just summaries, alongside the bounded task. Advanced workflows remain behind load_subagent.`,
53
+ ...(childSafe ? { promptGuidelines: [...guidelines] } : {}),
54
+ parameters: Type.Object({ ...DelegateParams.properties, async: Type.Optional(Type.Boolean({ description: asyncDescription })) }, { additionalProperties: false }),
55
+ async execute(id, params, signal, onUpdate, ctx) {
56
+ const { worktree, context, async: background, ...task } = normalizeEverydayParams(params);
57
+ const request = worktree
58
+ ? { tasks: [task], worktree: true, context, async: background, cwd: task.cwd }
59
+ : { ...task, context, async: background };
60
+ const result = await executor.execute(id, normalizeSubagentParamsLike(request), signal, onUpdate, ctx);
61
+ await checkRuns();
62
+ return adapt(result, ctx);
63
+ },
64
+ renderResult: renderSubagentResult,
65
+ });
66
+ pi.registerTool({
67
+ name: "agent_runs",
68
+ defaultActive: false,
69
+ label: "Agent Runs",
70
+ description: `List ${childSafe ? "only this child's directly owned" : "your delegated"} runs across working directories (questions/failures, then live work, then unreviewed results; 20 per page). Filter globally by agent/state/text, sort, and page with the returned cursor. history reads 100 bounded native-entry previews; search finds saved visible text using words or quoted phrases, not operators or prefixes. Browse freshness is not completion or delivery proof. Inspect concise results, paths and continuations; full:true includes the full task/configuration. Answer durable questions, nudge, stop, continue, or save parent-only review. Review notes are not sent to children; put actionable instructions in continue/nudge. Inspect/review/nudge never restart finished work. Continue/answer can launch a saved child; async:false waits for its actual result. Saved continuations keep settings unless agent selects a current profile; model overrides win. Live guidance never mutates model or acceptance. profiles lists roles, sources, context and model/thinking/fallback defaults. History survives reload.`,
71
+ parameters: AgentRunsParams,
72
+ async execute(id, params, signal, onUpdate, ctx) {
73
+ const normalized = normalizeEverydayParams(params, true);
74
+ if (params.action === "list" && !params.id && options.listRuns) return adapt(await options.listRuns({ ...normalized, signal }, ctx), ctx);
75
+ const actions = { list: "status", inspect: "status", history: "history", search: "search", nudge: "nudge", stop: "interrupt", continue: "resume", profiles: "list", questions: "questions", answer: "answer", review: "review" };
76
+ return adapt(await executor.execute(id, normalizeSubagentParamsLike({ ...normalized, action: actions[params.action] }), signal, onUpdate, ctx), ctx);
77
+ },
78
+ renderResult: renderSubagentResult,
79
+ });
80
+ pi.registerTool({
81
+ name: "load_subagent",
82
+ label: "Load Subagent",
83
+ description: `Enable agent_runs for profiles, history and run controls; advanced:false loads only those controls. By default also enable advanced subagent orchestration: parallel groups, chains, saved workflows, detailed overrides, get, extend and doctor.${childSafe ? " Agent-definition mutations remain blocked." : " Includes agent-definition management."} Ordinary delegation and control use delegate and agent_runs. After loading advanced workflows, call subagent with { action: "list" } before execution.`,
84
+ promptSnippet: "Discover profiles/history with advanced:false, or load full subagent orchestration by default.",
85
+ parameters: Type.Object({ advanced: Type.Optional(Type.Boolean({ description: "Also enable advanced orchestration (default true); false loads only agent_runs for profiles, history and controls." })) }),
86
+ async execute(_id, params) {
87
+ const advanced = params.advanced !== false;
88
+ if (advanced && !pi.getAllTools().some((tool) => tool.name === "subagent" && !("namespace" in tool && tool.namespace))) {
89
+ throw new Error("Subagent is unavailable because the full tool is excluded from this session.");
90
+ }
91
+ if (!advanced && !pi.getAllTools().some((tool) => tool.name === "agent_runs" && !("namespace" in tool && tool.namespace))) throw new Error("Run controls are excluded from this session.");
92
+ const added = !pi.getActiveTools().includes(advanced ? "subagent" : "agent_runs");
93
+ activateTools(pi, advanced ? ["agent_runs", "subagent"] : ["agent_runs"]);
94
+ return {
95
+ content: [{ type: "text" as const, text: advanced ? [`Subagent ${added ? "enabled" : "already enabled"}.`, ...guidelines.map((line) => `- ${line}`)].join("\n") : "Run controls enabled. Use agent_runs({action:'profiles'}) to discover agents; list, history, or search browses owned saved work." }],
96
+ details: {},
97
+ };
98
+ },
99
+ });
100
+ const reconcile = async (ctx: ExtensionContext) => {
101
+ if (options.keepAdvancedActive) activateTools(pi, ["subagent"]);
102
+ await checkRuns();
103
+ if (!pi.getActiveTools().includes("agent_runs") && (await listSupervisorQuestionsAsync(ctx.sessionManager.getSessionId())).some((question) => question.state === "awaiting_input" || question.state === "answer_pending")) activateTools(pi, ["agent_runs"]);
104
+ };
105
+ const restore = async (event: { reason?: string }, ctx: ExtensionContext) => {
106
+ if (event.reason !== "reload") restoreLazyTools(pi, ctx, "load_subagent", ["subagent", "agent_runs"]);
107
+ await reconcile(ctx);
108
+ };
109
+ pi.on("session_start", restore);
110
+ pi.on("session_tree", (_event, ctx) => reconcile(ctx));
111
+ pi.on("session_compact", (_event, ctx) => reconcile(ctx));
112
+ pi.on("before_agent_start", (_event, ctx) => reconcile(ctx));
113
+ return reconcileRuns;
114
+ }
@@ -0,0 +1,23 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import type { ExtensionConfig } from "../shared/types.ts";
4
+ import { getAgentDir } from "../shared/utils.ts";
5
+
6
+ export function loadConfig(): ExtensionConfig {
7
+ const configPath = path.join(getAgentDir(), "extensions", "subagent", "config.json");
8
+ try {
9
+ if (fs.existsSync(configPath)) {
10
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8")) as ExtensionConfig;
11
+ const trust = config.projectTrust;
12
+ const childRuns = typeof trust === "object" && trust !== null ? trust.childRuns : trust;
13
+ if (childRuns !== undefined && childRuns !== "inherit" && childRuns !== "approve" && childRuns !== "no-approve") {
14
+ console.error(`Invalid projectTrust in '${configPath}'; child runs will use no-approve.`);
15
+ config.projectTrust = "no-approve";
16
+ }
17
+ return config;
18
+ }
19
+ } catch (error) {
20
+ console.error(`Failed to load subagent config from '${configPath}':`, error);
21
+ }
22
+ return {};
23
+ }
@@ -0,0 +1,47 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { controlNotificationKey, formatControlNoticeMessage, isObsoleteIdleNotice } from "../runs/shared/subagent-control.ts";
3
+ import type { ControlEvent } from "../shared/types.ts";
4
+
5
+ export const SUBAGENT_CONTROL_MESSAGE_TYPE = "subagent_control_notice";
6
+
7
+ export interface SubagentControlMessageDetails {
8
+ event: ControlEvent;
9
+ source?: "foreground" | "async";
10
+ asyncDir?: string;
11
+ childIntercomTarget?: string;
12
+ noticeText?: string;
13
+ }
14
+
15
+ export function controlNoticeTarget(details: SubagentControlMessageDetails): string | undefined {
16
+ return details.childIntercomTarget;
17
+ }
18
+
19
+ export function formatSubagentControlNotice(details: SubagentControlMessageDetails, content?: string): string {
20
+ return details.noticeText ?? content ?? formatControlNoticeMessage(details.event, controlNoticeTarget(details));
21
+ }
22
+
23
+ export function handleSubagentControlNotice(input: {
24
+ pi: Pick<ExtensionAPI, "sendMessage">;
25
+ visibleControlNotices: Set<string>;
26
+ details: SubagentControlMessageDetails;
27
+ }): void {
28
+ if (!input.details?.event || isObsoleteIdleNotice(input.details)) return;
29
+ const childIntercomTarget = controlNoticeTarget(input.details);
30
+ const key = controlNotificationKey(input.details.event, childIntercomTarget);
31
+ if (input.visibleControlNotices.has(key)) return;
32
+ input.visibleControlNotices.add(key);
33
+ const noticeText = input.details.noticeText ?? formatControlNoticeMessage(input.details.event, childIntercomTarget);
34
+ input.pi.sendMessage(
35
+ {
36
+ customType: SUBAGENT_CONTROL_MESSAGE_TYPE,
37
+ content: noticeText,
38
+ display: true,
39
+ details: { ...input.details, childIntercomTarget, noticeText },
40
+ },
41
+ // Async completion-guard notices stay visible but let the matching completion
42
+ // result deliver the single automatic wakeup. In a parallel group that result
43
+ // arrives only after all siblings finish, so reaction to a mid-group guard
44
+ // notice is bounded by the longest-running sibling.
45
+ { triggerTurn: !(input.details.source === "async" && input.details.event.reason === "completion_guard") },
46
+ );
47
+ }
@@ -0,0 +1,171 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { VERSION as PI_VERSION, getPackageDir } from "@earendil-works/pi-coding-agent";
5
+ import { EXTENSION_BUILD } from "./build-info.ts";
6
+ import { discoverAgentsAll, type AgentSource } from "../agents/agents.ts";
7
+ import { discoverAvailableSkills, type SkillSource } from "../agents/skills.ts";
8
+ import {
9
+ ASYNC_DIR,
10
+ CHAIN_RUNS_DIR,
11
+ RESULTS_DIR,
12
+ TEMP_ROOT_DIR,
13
+ type ExtensionConfig,
14
+ type SubagentState,
15
+ type SubagentIntercomConnection,
16
+ } from "../shared/types.ts";
17
+
18
+ interface DoctorReportInput {
19
+ cwd: string;
20
+ nativeSessionCwd?: string;
21
+ config: ExtensionConfig;
22
+ state: SubagentState;
23
+ requestedSessionDir?: string;
24
+ currentSessionFile?: string | null;
25
+ currentSessionId?: string | null;
26
+ orchestratorTarget?: string;
27
+ connection?: SubagentIntercomConnection;
28
+ sessionError?: string;
29
+ expandTilde?: (value: string) => string;
30
+ projectTrusted?: boolean;
31
+ }
32
+
33
+ const PI_PACKAGE_DIR = getPackageDir();
34
+ const EXTENSION_MODULE = fileURLToPath(import.meta.url);
35
+
36
+ function errorText(error: unknown): string {
37
+ return error instanceof Error ? `${error.name}: ${error.message}` : String(error);
38
+ }
39
+
40
+ function lineFromCheck(label: string, check: () => string): string {
41
+ try {
42
+ return check();
43
+ } catch (error) {
44
+ return `- ${label}: failed — ${errorText(error)}`;
45
+ }
46
+ }
47
+
48
+ function formatExistingDirectory(label: string, dirPath: string): string {
49
+ try {
50
+ if (!fs.existsSync(dirPath)) return `- ${label}: missing (${dirPath})`;
51
+ const stats = fs.statSync(dirPath);
52
+ if (!stats.isDirectory()) throw new Error(`not a directory: ${dirPath}`);
53
+ fs.accessSync(dirPath, fs.constants.R_OK | fs.constants.W_OK);
54
+ return `- ${label}: ok (${dirPath})`;
55
+ } catch (error) {
56
+ return `- ${label}: failed (${dirPath}) — ${errorText(error)}`;
57
+ }
58
+ }
59
+
60
+ function formatSourceCounts(counts: Record<AgentSource, number>): string {
61
+ return `builtin ${counts.builtin}, package ${counts.package}, user ${counts.user}, project ${counts.project}`;
62
+ }
63
+
64
+ function formatSkillSourceCounts(skills: Array<{ source: SkillSource }>): string {
65
+ const counts = new Map<SkillSource, number>();
66
+ for (const skill of skills) counts.set(skill.source, (counts.get(skill.source) ?? 0) + 1);
67
+ const ordered: SkillSource[] = [
68
+ "project",
69
+ "project-settings",
70
+ "project-package",
71
+ "user",
72
+ "user-settings",
73
+ "user-package",
74
+ "extension",
75
+ "builtin",
76
+ "unknown",
77
+ ];
78
+ const parts = ordered
79
+ .map((source) => `${source} ${counts.get(source) ?? 0}`)
80
+ .filter((part) => !part.endsWith(" 0"));
81
+ return parts.length > 0 ? parts.join(", ") : "none";
82
+ }
83
+
84
+ function formatConfiguredSessionDir(input: DoctorReportInput): string {
85
+ if (input.requestedSessionDir) {
86
+ return path.resolve(input.expandTilde?.(input.requestedSessionDir) ?? input.requestedSessionDir);
87
+ }
88
+ if (input.config.defaultSessionDir) {
89
+ return path.resolve(input.expandTilde?.(input.config.defaultSessionDir) ?? input.config.defaultSessionDir);
90
+ }
91
+ return "not configured";
92
+ }
93
+
94
+ function formatSessionLines(input: DoctorReportInput): string[] {
95
+ const sessionFile = input.currentSessionFile ?? null;
96
+ const lines = [
97
+ lineFromCheck("configured session dir", () => `- configured session dir: ${formatConfiguredSessionDir(input)}`),
98
+ `- current session file: ${sessionFile ?? "not available"}`,
99
+ `- current session dir: ${sessionFile ? path.dirname(sessionFile) : "not available"}`,
100
+ `- current session id: ${input.currentSessionId ?? input.state.currentSessionId ?? "not available"}`,
101
+ ];
102
+ if (input.sessionError) lines.push(`- session manager: failed — ${input.sessionError}`);
103
+ return lines;
104
+ }
105
+
106
+ function formatDiscovery(input: DoctorReportInput): string[] {
107
+ return [
108
+ lineFromCheck("agents/chains", () => {
109
+ const discovered = discoverAgentsAll(input.cwd, { projectTrusted: input.projectTrusted ?? true });
110
+ const agentCounts = {
111
+ builtin: discovered.builtin.length,
112
+ package: discovered.package.length,
113
+ user: discovered.user.length,
114
+ project: discovered.project.length,
115
+ };
116
+ const chainCounts = discovered.chains.reduce<Record<AgentSource, number>>((counts, chain) => {
117
+ counts[chain.source] += 1;
118
+ return counts;
119
+ }, { builtin: 0, package: 0, user: 0, project: 0 });
120
+ return [
121
+ `- agents: total ${agentCounts.builtin + agentCounts.package + agentCounts.user + agentCounts.project} (${formatSourceCounts(agentCounts)})`,
122
+ `- chains: total ${discovered.chains.length} (${formatSourceCounts(chainCounts)})`,
123
+ ].join("\n");
124
+ }),
125
+ lineFromCheck("skills", () => {
126
+ const skills = discoverAvailableSkills(input.cwd, { projectTrusted: input.projectTrusted ?? true });
127
+ return `- skills: total ${skills.length} (${formatSkillSourceCounts(skills)})`;
128
+ }),
129
+ ];
130
+ }
131
+
132
+ function formatIntercomSection(input: DoctorReportInput): string[] {
133
+ return [
134
+ `- bridge: ${input.connection ? "responding" : "unavailable (no live health response)"}`,
135
+ `- connection: ${input.connection?.status ?? "unknown"}${input.connection?.reason ? ` — ${input.connection.reason}` : ""}`,
136
+ `- broker session id: ${input.connection?.sessionId ?? "not available"}`,
137
+ `- orchestrator target: ${input.orchestratorTarget?.trim() || "not available"}`,
138
+ ];
139
+ }
140
+
141
+ export function buildDoctorReport(input: DoctorReportInput): string {
142
+ const lines = [
143
+ "Subagents doctor report",
144
+ "",
145
+ "Runtime",
146
+ `- Native session cwd: ${input.nativeSessionCwd ?? input.cwd}`,
147
+ ...(input.nativeSessionCwd && input.nativeSessionCwd !== input.cwd ? [`- Requested cwd: ${input.cwd}`] : []),
148
+ `- Node: ${process.version}`,
149
+ `- process: ${process.pid} (${process.execPath})`,
150
+ `- loaded Pi version: ${PI_VERSION}`,
151
+ `- Pi package directory: ${PI_PACKAGE_DIR} (Pi resource path; may be overridden)`,
152
+ "- native queue contract: not verified (version alone does not identify fork patches)",
153
+ `- loaded pi-subagents build: ${EXTENSION_BUILD.version && EXTENSION_BUILD.sha256 ? `${EXTENSION_BUILD.version} (runtime SHA-256 ${EXTENSION_BUILD.sha256})` : "unknown (unbuilt source)"}`,
154
+ `- extension module: ${EXTENSION_MODULE}`,
155
+ "- async support: available (Node >=24)",
156
+ ...formatSessionLines(input),
157
+ "",
158
+ "Filesystem",
159
+ formatExistingDirectory("temp root", TEMP_ROOT_DIR),
160
+ formatExistingDirectory("async runs", ASYNC_DIR),
161
+ formatExistingDirectory("results", RESULTS_DIR),
162
+ formatExistingDirectory("chain runs", CHAIN_RUNS_DIR),
163
+ "",
164
+ "Discovery",
165
+ ...formatDiscovery(input),
166
+ "",
167
+ "Intercom",
168
+ ...formatIntercomSection(input),
169
+ ];
170
+ return lines.join("\n");
171
+ }
@@ -0,0 +1,312 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
5
+ import { discoverAgents } from "../agents/agents.ts";
6
+ import { getArtifactsDir } from "../shared/artifacts.ts";
7
+ import { createSubagentExecutor, normalizeSubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
8
+ import { interruptAsyncRun } from "../runs/foreground/foreground-control.ts";
9
+ import { SUBAGENT_CHILD_ENV, SUBAGENT_FANOUT_CHILD_ENV, SUBAGENT_EAGER_TOOL_ENV, SUBAGENT_PARENT_CHILD_INDEX_ENV } from "../runs/shared/pi-args.ts";
10
+ import { readNestedControlRequests, resolveNestedRouteFromEnv, writeNestedControlResult } from "../runs/shared/nested-events.ts";
11
+ import { deliverSubagentIntercomMessageEvent } from "../intercom/result-intercom.ts";
12
+ import { resolveSubagentIntercomTarget } from "../intercom/intercom-bridge.ts";
13
+ import { readStatus } from "../shared/utils.ts";
14
+ import { SubagentParams } from "./schemas.ts";
15
+ import { loadConfig } from "./config.ts";
16
+ import { registerCompactSubagentTools } from "./compact-tools.ts";
17
+ import { registerToolResultAdapter } from "./tool-result.ts";
18
+ import { renderSubagentResult } from "../tui/render.ts";
19
+ import { type Details, type SubagentExecutionResult, type SubagentState } from "../shared/types.ts";
20
+ import { finalizedChildUsage, registerParentUsage } from "../runs/shared/parent-usage.ts";
21
+ import { resolveCurrentSessionId } from "../shared/session-identity.ts";
22
+ import { OWNED_RUN_ENTRY, ownedRunList, restoreOwnedRunsAsync } from "../runs/shared/run-records.ts";
23
+ import { closeRunHistory, startRunHistory } from "../runs/shared/history-index.ts";
24
+ import { createCompletionDelivery } from "../runs/background/completion-delivery.ts";
25
+
26
+ function getSubagentSessionRoot(parentSessionFile: string | null): string {
27
+ if (parentSessionFile) {
28
+ const baseName = path.basename(parentSessionFile, ".jsonl");
29
+ const sessionsDir = path.dirname(parentSessionFile);
30
+ return path.join(sessionsDir, baseName);
31
+ }
32
+ return fs.mkdtempSync(path.join(os.tmpdir(), "pi-subagent-session-"));
33
+ }
34
+
35
+ function expandTilde(p: string): string {
36
+ return p.startsWith("~/") ? path.join(os.homedir(), p.slice(2)) : p;
37
+ }
38
+
39
+ function createChildSafeState(): SubagentState {
40
+ return {
41
+ baseCwd: "",
42
+ currentSessionId: null,
43
+ asyncJobs: new Map(),
44
+ foregroundRuns: new Map(),
45
+ cleanupTimers: new Map(),
46
+ lastUiContext: null,
47
+ poller: null,
48
+ completionSeen: new Map(),
49
+ watcher: null,
50
+ watcherRestartTimer: null,
51
+ resultFileCoalescer: {
52
+ schedule: () => false,
53
+ clear: () => {},
54
+ },
55
+ };
56
+ }
57
+
58
+ function addBounded(set: Set<string>, value: string, max = 1000): void {
59
+ set.add(value);
60
+ while (set.size > max) set.delete(set.values().next().value!);
61
+ }
62
+
63
+ function startNestedControlInboxListener(pi: ExtensionAPI, state: SubagentState, isRestoring: () => boolean): NodeJS.Timeout | undefined {
64
+ let route;
65
+ try {
66
+ route = resolveNestedRouteFromEnv();
67
+ } catch {
68
+ return undefined;
69
+ }
70
+ if (!route) return undefined;
71
+ const seen = new Set<string>();
72
+ const seenFiles = new Set<string>();
73
+ const inFlight = new Set<string>();
74
+ const pendingResults = new Map<string, Parameters<typeof writeNestedControlResult>[1]>();
75
+ const parsedChildIndex = Number(process.env[SUBAGENT_PARENT_CHILD_INDEX_ENV]);
76
+ const localChildIndex = Number.isInteger(parsedChildIndex) && parsedChildIndex >= 0 ? parsedChildIndex : undefined;
77
+ const timer = setInterval(() => {
78
+ if (isRestoring()) return;
79
+ try {
80
+ for (const request of readNestedControlRequests(route, seenFiles)) {
81
+ if (seen.has(request.requestId) || inFlight.has(request.requestId)) continue;
82
+ if (request.targetChildIndex !== undefined && request.targetChildIndex !== localChildIndex) continue;
83
+ const owned = state.ownedRuns?.get(request.targetRunId);
84
+ const asyncJob = state.asyncJobs.get(request.targetRunId);
85
+ if (!owned && !asyncJob && (request.targetChildIndex !== undefined || Date.now() - request.ts < 400)) continue;
86
+ inFlight.add(request.requestId);
87
+ void (async () => {
88
+ const claimPath = `${request.filePath}.claimed`;
89
+ try {
90
+ let result = pendingResults.get(request.requestId);
91
+ if (!result && pendingResults.size >= 100) {
92
+ result = {
93
+ ts: Date.now(),
94
+ requestId: request.requestId,
95
+ targetRunId: request.targetRunId,
96
+ ok: false,
97
+ message: "Nested control result queue is full; retry after pending results are delivered.",
98
+ };
99
+ }
100
+ if (!result) {
101
+ try {
102
+ const claimFd = fs.openSync(claimPath, "wx", 0o600);
103
+ try { fs.writeFileSync(claimFd, `${request.requestId}\n`, "utf-8"); } finally { fs.closeSync(claimFd); }
104
+ } catch (error) {
105
+ const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
106
+ result = {
107
+ ts: Date.now(),
108
+ requestId: request.requestId,
109
+ targetRunId: request.targetRunId,
110
+ ok: false,
111
+ message: code === "EEXIST"
112
+ ? "Nested control request was already claimed; refusing to execute it again because the original outcome is unavailable."
113
+ : `Failed to claim nested control request safely: ${error instanceof Error ? error.message : String(error)}`,
114
+ };
115
+ }
116
+ }
117
+ if (!result) {
118
+ let ok = false;
119
+ let message = "Control request failed.";
120
+ try {
121
+ const asyncDir = state.ownedRuns?.get(request.targetRunId)?.asyncDir ?? state.asyncJobs.get(request.targetRunId)?.asyncDir;
122
+ const status = asyncDir ? readStatus(asyncDir) : null;
123
+ if (!status || status.state !== "running") {
124
+ message = `Nested run ${request.targetRunId} is not active in this fanout child.`;
125
+ } else if (request.action === "interrupt") {
126
+ const receipt = interruptAsyncRun(state, request.targetRunId, request.index);
127
+ ok = Boolean(receipt && !receipt.isError);
128
+ message = receipt?.content.map((part) => part.type === "text" ? part.text : "").join("\n") ?? "Nested run is not interruptible.";
129
+ } else if (!request.message?.trim()) {
130
+ message = "Nested resume requires message.";
131
+ } else {
132
+ const index = request.index ?? status.steps?.findIndex((step) => step.status === "running") ?? -1;
133
+ const step = status.steps?.[index];
134
+ const agent = step?.status === "running" ? step.agent : undefined;
135
+ if (!agent) {
136
+ message = `Nested run ${request.targetRunId} has no active child message route.`;
137
+ } else {
138
+ const target = resolveSubagentIntercomTarget(request.targetRunId, agent, index);
139
+ ok = await deliverSubagentIntercomMessageEvent(
140
+ pi.events,
141
+ target,
142
+ `Follow-up for nested run ${request.targetRunId} (${agent}):\n\n${request.message.trim()}`,
143
+ 500,
144
+ { source: "nested-resume", runId: request.targetRunId, agent, index },
145
+ );
146
+ message = ok
147
+ ? `Delivered follow-up to live nested run ${request.targetRunId}.`
148
+ : `Nested child intercom target is not registered: ${target}`;
149
+ }
150
+ }
151
+ } catch (error) {
152
+ message = error instanceof Error ? error.message : String(error);
153
+ }
154
+ result = { ts: Date.now(), requestId: request.requestId, targetRunId: request.targetRunId, ok, message };
155
+ }
156
+ try {
157
+ writeNestedControlResult(route, result);
158
+ } catch (error) {
159
+ if (pendingResults.size < 100 || pendingResults.has(request.requestId)) pendingResults.set(request.requestId, result);
160
+ console.error(`Failed to write nested control result for request '${request.requestId}' targeting '${request.targetRunId}' via inbox '${route.controlInbox}'; keeping request for retry:`, error);
161
+ return;
162
+ }
163
+ pendingResults.delete(request.requestId);
164
+ addBounded(seen, request.requestId);
165
+ addBounded(seenFiles, path.basename(request.filePath));
166
+ let requestRemoved = false;
167
+ try {
168
+ fs.unlinkSync(request.filePath);
169
+ requestRemoved = true;
170
+ } catch (error) {
171
+ const code = error && typeof error === "object" && "code" in error ? (error as { code?: unknown }).code : undefined;
172
+ requestRemoved = code === "ENOENT";
173
+ }
174
+ if (requestRemoved) {
175
+ try { fs.unlinkSync(claimPath); } catch {}
176
+ }
177
+ } finally {
178
+ inFlight.delete(request.requestId);
179
+ }
180
+ })();
181
+ }
182
+ } catch (error) {
183
+ console.error(`Failed to poll nested control inbox '${route.controlInbox}' for root '${route.rootRunId}':`, error);
184
+ }
185
+ }, 200);
186
+ timer.unref?.();
187
+ return timer;
188
+ }
189
+
190
+ export default function registerFanoutChildSubagentExtension(pi: ExtensionAPI): void {
191
+ if (process.env[SUBAGENT_CHILD_ENV] !== "1" || process.env[SUBAGENT_FANOUT_CHILD_ENV] !== "1") return;
192
+
193
+ const globalStore = globalThis as Record<string, unknown>;
194
+ const registeredKey = "__piSubagentFanoutChildRegisteredApis";
195
+ const registeredApis = globalStore[registeredKey] instanceof WeakSet
196
+ ? globalStore[registeredKey] as WeakSet<ExtensionAPI>
197
+ : new WeakSet<ExtensionAPI>();
198
+ globalStore[registeredKey] = registeredApis;
199
+ if (registeredApis.has(pi)) return;
200
+ registeredApis.add(pi);
201
+
202
+ const controlInboxCleanupStoreKey = "__piSubagentFanoutChildControlInboxCleanup";
203
+ const previousControlInboxCleanup = globalStore[controlInboxCleanupStoreKey];
204
+ if (typeof previousControlInboxCleanup === "function") {
205
+ try {
206
+ previousControlInboxCleanup();
207
+ } catch {
208
+ // Best effort cleanup for stale timers from an older reload.
209
+ }
210
+ }
211
+
212
+ const config = loadConfig();
213
+ const state = createChildSafeState();
214
+ state.persistOwnedRun = (run) => pi.appendEntry(OWNED_RUN_ENTRY, run);
215
+ let sessionReset: Promise<void> | undefined;
216
+ const ensureSessionState = (ctx: ExtensionContext): void | Promise<void> => {
217
+ const sessionId = resolveCurrentSessionId(ctx.sessionManager);
218
+ if (sessionReset) return sessionReset;
219
+ if (state.currentSessionId === sessionId) { state.lastUiContext = ctx; return; }
220
+ sessionReset = (async () => {
221
+ await completionDelivery.stopAndJoin();
222
+ state.lastUiContext = ctx;
223
+ void closeRunHistory(state).catch((error) => console.error("Could not close child history:", error));
224
+ state.foregroundRuns?.clear();
225
+ await restoreOwnedRunsAsync(state, ctx);
226
+ state.currentSessionId = sessionId;
227
+ startRunHistory(state, ctx);
228
+ completionDelivery.start();
229
+ })().finally(() => { sessionReset = undefined; });
230
+ return sessionReset;
231
+ };
232
+ pi.on("session_start", async (_event, ctx) => {
233
+ await ensureSessionState(ctx);
234
+ });
235
+ const executor = createSubagentExecutor({
236
+ pi,
237
+ state,
238
+ config,
239
+ // Keep the stock nested default foreground so evidence returns before this caller exits.
240
+ asyncByDefault: config.asyncByDefault === true,
241
+ tempArtifactsDir: getArtifactsDir(null),
242
+ getSubagentSessionRoot,
243
+ expandTilde,
244
+ discoverAgents,
245
+ allowMutatingManagementActions: false,
246
+ ensureSessionState,
247
+ });
248
+
249
+ const compact = config.compactChildTools !== false;
250
+ const toolNames = compact ? ["subagent", "delegate", "agent_runs"] : ["subagent"];
251
+ const parentUsage = registerParentUsage(pi, toolNames);
252
+ const completionDelivery = createCompletionDelivery(pi, state, parentUsage);
253
+ const adaptToolResult = registerToolResultAdapter(pi, toolNames);
254
+ const toRegisteredToolResult = (result: SubagentExecutionResult, ctx: ExtensionContext) => adaptToolResult(
255
+ result.details.wait?.status === "completed" && result.details.run?.ownerSessionId === ctx.sessionManager.getSessionId()
256
+ ? parentUsage.attach(result, finalizedChildUsage(result.details.run.children, result.details.wait.index), ctx)
257
+ : result,
258
+ );
259
+ const acceptanceGuidelines = [
260
+ "For goal-style requests such as /goal, goal, active goal, or work until evidence says done, use explicit acceptance on the delegated run: criteria for the target, evidence/verify for proof, stopRules for constraints, and maxFinalizationTurns for the bounded loop.",
261
+ "For implementation handoffs from a plan, PRD, spec, issue, or broad fix, put implementation instructions and plan paths in task, and put the definition of done, evidence, verification commands, constraints, and loop cap in acceptance.",
262
+ ];
263
+ const guidelines = [
264
+ "Delegate useful helper work within your assigned task when it saves time or improves quality; the original parent owns integration and final delivery.",
265
+ "Nested execution defaults to foreground unless configuration explicitly opts into async. Set async:false whenever the nested result must appear in this child's report; use async:true only for intentionally detached work.",
266
+ compact
267
+ ? "Use load_subagent({advanced:false}), then agent_runs({action:'profiles'}) before delegation unless the executable agent is already known. Use load_subagent for advanced workflows."
268
+ : "Use subagent action:list before nested execution unless the executable nested agent is already known from the task context.",
269
+ "Do not use subagent child-safe mode for agent config mutation actions; create, update, and delete are blocked here.",
270
+ ];
271
+ if (compact) registerCompactSubagentTools(pi, { executor, state, adapt: toRegisteredToolResult, guidelines: [...guidelines, ...acceptanceGuidelines], childSafe: true,
272
+ asyncByDefault: config.asyncByDefault === true, keepAdvancedActive: process.env[SUBAGENT_EAGER_TOOL_ENV] === "1",
273
+ listRuns: async (params, ctx) => { await ensureSessionState(ctx); return ownedRunList(state, params); },
274
+ });
275
+ const tool: ToolDefinition<typeof SubagentParams, Details> = {
276
+ name: "subagent",
277
+ defaultActive: !compact,
278
+ label: "Subagent",
279
+ description: [
280
+ "Delegate to subagents from child-safe fanout mode.",
281
+ ...acceptanceGuidelines,
282
+ "Allowed management/control actions: list, get, status, history, search, nudge, interrupt, extend, resume, questions, answer, review, doctor. History/search browse only directly owned saved work; bounded previews and index freshness are not canonical proof. Exact status is concise; full:true includes the full task/configuration. Review notes are parent-only, not sent to children. Put actionable instructions in resume/nudge. Resume/answer overrides do not amend live acceptance.",
283
+ "Agent config mutation actions create, update, and delete are blocked in this mode.",
284
+ ].join("\n"),
285
+ promptSnippet: "Delegate nested child-safe subagent work from an explicitly allowed fanout child.",
286
+ promptGuidelines: guidelines,
287
+ parameters: SubagentParams,
288
+ async execute(id, params, signal, onUpdate, ctx) {
289
+ return toRegisteredToolResult(await executor.execute(id, normalizeSubagentParamsLike(params), signal, onUpdate, ctx), ctx);
290
+ },
291
+ renderResult: renderSubagentResult,
292
+ };
293
+
294
+ pi.registerTool(tool);
295
+
296
+ const controlInboxTimer = startNestedControlInboxListener(pi, state, () => sessionReset !== undefined);
297
+ const clearControlInboxTimer = (): Promise<void> => {
298
+ if (controlInboxTimer) clearInterval(controlInboxTimer);
299
+ completionDelivery.stop();
300
+ return closeRunHistory(state);
301
+ };
302
+ globalStore[controlInboxCleanupStoreKey] = clearControlInboxTimer;
303
+
304
+ pi.on("session_shutdown", async () => {
305
+ if (controlInboxTimer) clearInterval(controlInboxTimer);
306
+ await completionDelivery.stopAndJoin({ preservePending: true });
307
+ await closeRunHistory(state);
308
+ if (globalStore[controlInboxCleanupStoreKey] === clearControlInboxTimer) {
309
+ delete globalStore[controlInboxCleanupStoreKey];
310
+ }
311
+ });
312
+ }