@gotgenes/pi-subagents 18.0.0 → 18.0.2

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 (257) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +22 -31
  3. package/docs/architecture/architecture.md +157 -204
  4. package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
  5. package/package.json +5 -6
  6. package/src/lifecycle/subagent.ts +5 -9
  7. package/src/observation/notification.ts +23 -10
  8. package/src/observation/subagent-events-observer.ts +1 -3
  9. package/src/tools/get-result-report.ts +60 -0
  10. package/src/tools/get-result-tool.ts +25 -40
  11. package/src/types.ts +1 -1
  12. package/src/ui/session-navigator.ts +0 -2
  13. package/.prettierignore +0 -5
  14. package/AGENTS.md +0 -7
  15. package/docs/plans/0048-implement-subagents-api.md +0 -304
  16. package/docs/plans/0049-remove-group-join-output-file-rpc.md +0 -180
  17. package/docs/plans/0051-update-adr-0001-hard-fork.md +0 -77
  18. package/docs/plans/0052-remove-scheduled-subagents.md +0 -133
  19. package/docs/plans/0053-extract-model-resolution-from-execute.md +0 -181
  20. package/docs/plans/0054-decompose-index-into-modules.md +0 -302
  21. package/docs/plans/0057-structured-debug-logging.md +0 -154
  22. package/docs/plans/0061-session-format-transcript.md +0 -284
  23. package/docs/plans/0066-replace-as-any-with-sdk-types.md +0 -215
  24. package/docs/plans/0069-create-subagent-runtime.md +0 -345
  25. package/docs/plans/0070-extract-event-handlers.md +0 -306
  26. package/docs/plans/0071-extract-session-config-assembler.md +0 -362
  27. package/docs/plans/0072-inject-agent-manager-collaborators.md +0 -329
  28. package/docs/plans/0076-inject-cwd-into-agent-manager.md +0 -102
  29. package/docs/plans/0077-inject-project-agents-dir.md +0 -115
  30. package/docs/plans/0080-consolidate-agent-config-lookup.md +0 -247
  31. package/docs/plans/0084-extract-git-worktree-manager.md +0 -142
  32. package/docs/plans/0087-evolve-subagent-runtime-methods.md +0 -240
  33. package/docs/plans/0098-extract-agent-record-state-machine.md +0 -435
  34. package/docs/plans/0099-replace-ctx-with-parent-snapshot.md +0 -488
  35. package/docs/plans/0100-replace-callback-threading-with-session-subscription.md +0 -454
  36. package/docs/plans/0102-consolidate-test-record-factory.md +0 -176
  37. package/docs/plans/0108-extract-agent-type-registry.md +0 -322
  38. package/docs/plans/0109-extract-settings-manager.md +0 -276
  39. package/docs/plans/0110-agent-activity-tracker.md +0 -297
  40. package/docs/plans/0111-split-agent-record-lifecycle.md +0 -582
  41. package/docs/plans/0112-replace-agent-manager-callbacks.md +0 -241
  42. package/docs/plans/0113-disambiguate-spawn-options.md +0 -155
  43. package/docs/plans/0114-narrow-agent-tool-menu-deps.md +0 -279
  44. package/docs/plans/0115-decompose-agent-tool.md +0 -337
  45. package/docs/plans/0116-type-housekeeping.md +0 -351
  46. package/docs/plans/0118-settings-manager-apply-methods.md +0 -271
  47. package/docs/plans/0123-remove-vi-fn-cast-smell.md +0 -179
  48. package/docs/plans/0131-consolidate-shared-test-fixtures.md +0 -207
  49. package/docs/plans/0132-inject-io-into-session-config.md +0 -219
  50. package/docs/plans/0133-inject-sdk-boundary-into-agent-runner.md +0 -373
  51. package/docs/plans/0134-reduce-as-any-casts.md +0 -366
  52. package/docs/plans/0135-extract-display-helpers.md +0 -182
  53. package/docs/plans/0136-decompose-agent-menu.md +0 -300
  54. package/docs/plans/0144-consolidate-observation-model.md +0 -263
  55. package/docs/plans/0145-decompose-execute-push-ctx-to-boundary.md +0 -290
  56. package/docs/plans/0146-narrow-ui-context.md +0 -319
  57. package/docs/plans/0147-inject-wrap-text-into-conversation-viewer.md +0 -166
  58. package/docs/plans/0148-split-agent-widget-rendering.md +0 -255
  59. package/docs/plans/0152-add-prompt-snippet.md +0 -91
  60. package/docs/plans/0164-reorganize-into-domain-directories.md +0 -409
  61. package/docs/plans/0165-decompose-resolved-spawn-config.md +0 -157
  62. package/docs/plans/0166-extract-parent-session-info.md +0 -231
  63. package/docs/plans/0167-narrow-runner-io.md +0 -150
  64. package/docs/plans/0168-extract-tool-filter-config.md +0 -173
  65. package/docs/plans/0169-extract-run-context-from-run-options.md +0 -194
  66. package/docs/plans/0170-reduce-build-content-lines-complexity.md +0 -225
  67. package/docs/plans/0171-reduce-render-result-complexity.md +0 -200
  68. package/docs/plans/0172-extract-turn-formatting.md +0 -206
  69. package/docs/plans/0180-reorder-append-prompt-for-kv-cache.md +0 -100
  70. package/docs/plans/0185-remove-persistent-agent-memory.md +0 -161
  71. package/docs/plans/0188-replace-any-casts-with-sdk-types.md +0 -162
  72. package/docs/plans/0192-define-session-context-interface.md +0 -107
  73. package/docs/plans/0193-runtime-owns-context-queries.md +0 -245
  74. package/docs/plans/0194-align-tool-interfaces-for-structural-typing.md +0 -139
  75. package/docs/plans/0195-convert-tool-factories-to-classes.md +0 -264
  76. package/docs/plans/0196-convert-runner-menu-to-classes.md +0 -268
  77. package/docs/plans/0205-decompose-render-widget-lines.md +0 -140
  78. package/docs/plans/0206-decompose-show-agent-detail.md +0 -211
  79. package/docs/plans/0207-decompose-agent-widget-update.md +0 -242
  80. package/docs/plans/0208-extract-shared-test-fixtures.md +0 -298
  81. package/docs/plans/0214-convert-remaining-closure-factories-to-classes.md +0 -261
  82. package/docs/plans/0215-decompose-build-parent-context.md +0 -166
  83. package/docs/plans/0216-decompose-start-agent.md +0 -255
  84. package/docs/plans/0217-extract-overwrite-guard.md +0 -176
  85. package/docs/plans/0218-push-sdk-boundary-in-settings.md +0 -172
  86. package/docs/plans/0219-reduce-test-duplication-top-3-clone-families.md +0 -162
  87. package/docs/plans/0227-evolve-agent-record-into-agent.md +0 -322
  88. package/docs/plans/0228-async-start-agent-dissolve-run-handle.md +0 -288
  89. package/docs/plans/0229-agent-born-complete.md +0 -564
  90. package/docs/plans/0230-extract-concurrency-queue.md +0 -265
  91. package/docs/plans/0231-push-exec-registry-to-runner.md +0 -245
  92. package/docs/plans/0232-agent-resume-internal-observer-lifecycle.md +0 -180
  93. package/docs/plans/0237-remove-disallowed-tools.md +0 -180
  94. package/docs/plans/0238-remove-extensions-filtering.md +0 -191
  95. package/docs/plans/0239-collapse-filter-active-tools.md +0 -217
  96. package/docs/plans/0242-rename-agent-tool-to-subagent.md +0 -165
  97. package/docs/plans/0256-extract-worktree-isolation.md +0 -256
  98. package/docs/plans/0257-extract-child-session-factory.md +0 -285
  99. package/docs/plans/0262-add-workspace-provider-seam.md +0 -264
  100. package/docs/plans/0264-remove-extension-lifecycle-control.md +0 -277
  101. package/docs/plans/0265-born-complete-subagent-session.md +0 -333
  102. package/docs/plans/0270-type-consumable-public-surface.md +0 -204
  103. package/docs/plans/0272-export-workspace-collaborator-types.md +0 -147
  104. package/docs/plans/0277-encapsulate-agent-session.md +0 -304
  105. package/docs/plans/0280-rename-agent-to-subagent.md +0 -197
  106. package/docs/plans/0360-custom-agents-default-append.md +0 -132
  107. package/docs/plans/0373-extract-subagent-state.md +0 -250
  108. package/docs/plans/0374-encapsulate-subagent-start-notification.md +0 -268
  109. package/docs/plans/0375-extract-run-listener-workspace-bracket.md +0 -300
  110. package/docs/plans/0376-extract-manager-observer.md +0 -232
  111. package/docs/plans/0377-split-widget-delegation-from-runtime.md +0 -254
  112. package/docs/plans/0378-consolidate-lifecycle-test-fixtures.md +0 -243
  113. package/docs/plans/0379-consolidate-ui-tools-test-fixtures.md +0 -335
  114. package/docs/plans/0380-extract-layered-settings-loader.md +0 -223
  115. package/docs/plans/0381-replace-concurrency-queue-with-limiter.md +0 -267
  116. package/docs/plans/0400-include-parent-prompt-in-replace-mode.md +0 -199
  117. package/docs/plans/0403-abort-subagents-on-interrupt.md +0 -180
  118. package/docs/plans/0412-unify-session-mock-builders.md +0 -146
  119. package/docs/plans/0420-fold-run-metrics-live-activity-onto-record.md +0 -228
  120. package/docs/plans/0421-migrate-activity-readers-to-record-getters.md +0 -309
  121. package/docs/plans/0422-delete-activity-tracker-ui-observer.md +0 -204
  122. package/docs/plans/0423-widget-self-drive-from-lifecycle.md +0 -264
  123. package/docs/plans/0424-drop-widget-dep-from-subagent-tool.md +0 -166
  124. package/docs/plans/0425-reconcile-subagent-events-contract.md +0 -144
  125. package/docs/plans/0426-consolidate-residual-test-clone-families.md +0 -235
  126. package/docs/plans/0427-reconsider-ui-direction.md +0 -212
  127. package/docs/plans/0441-remove-agent-definition-management-subtree.md +0 -180
  128. package/docs/plans/0442-dissolve-agents-conversation-viewer.md +0 -195
  129. package/docs/plans/0444-shrink-widget-to-background-agents.md +0 -156
  130. package/docs/plans/0445-native-session-navigation.md +0 -365
  131. package/docs/plans/0446-spike-session-navigation-entry-criteria.md +0 -171
  132. package/docs/plans/0447-extract-subagents-settings-command.md +0 -220
  133. package/docs/plans/0448-block-disabled-agent-spawn.md +0 -207
  134. package/docs/plans/0462-navigation-renderer-tui-components.md +0 -338
  135. package/docs/plans/0463-file-snapshot-source-evicted-agents.md +0 -306
  136. package/docs/retro/0048-implement-subagents-api.md +0 -44
  137. package/docs/retro/0049-remove-group-join-output-file-rpc.md +0 -49
  138. package/docs/retro/0051-update-adr-0001-hard-fork.md +0 -39
  139. package/docs/retro/0053-extract-model-resolution-from-execute.md +0 -40
  140. package/docs/retro/0054-decompose-index-into-modules.md +0 -53
  141. package/docs/retro/0057-structured-debug-logging.md +0 -77
  142. package/docs/retro/0061-session-format-transcript.md +0 -41
  143. package/docs/retro/0066-replace-as-any-with-sdk-types.md +0 -44
  144. package/docs/retro/0069-create-subagent-runtime.md +0 -43
  145. package/docs/retro/0070-extract-event-handlers.md +0 -49
  146. package/docs/retro/0071-extract-session-config-assembler.md +0 -60
  147. package/docs/retro/0072-inject-agent-manager-collaborators.md +0 -46
  148. package/docs/retro/0076-inject-cwd-into-agent-manager.md +0 -35
  149. package/docs/retro/0077-inject-project-agents-dir.md +0 -33
  150. package/docs/retro/0080-consolidate-agent-config-lookup.md +0 -35
  151. package/docs/retro/0084-extract-git-worktree-manager.md +0 -37
  152. package/docs/retro/0087-evolve-subagent-runtime-methods.md +0 -42
  153. package/docs/retro/0098-extract-agent-record-state-machine.md +0 -46
  154. package/docs/retro/0099-replace-ctx-with-parent-snapshot.md +0 -37
  155. package/docs/retro/0100-replace-callback-threading-with-session-subscription.md +0 -36
  156. package/docs/retro/0102-consolidate-test-record-factory.md +0 -30
  157. package/docs/retro/0108-extract-agent-type-registry.md +0 -41
  158. package/docs/retro/0109-extract-settings-manager.md +0 -55
  159. package/docs/retro/0110-agent-activity-tracker.md +0 -44
  160. package/docs/retro/0111-split-agent-record-lifecycle.md +0 -61
  161. package/docs/retro/0112-replace-agent-manager-callbacks.md +0 -35
  162. package/docs/retro/0113-disambiguate-spawn-options.md +0 -29
  163. package/docs/retro/0114-narrow-agent-tool-menu-deps.md +0 -38
  164. package/docs/retro/0115-decompose-agent-tool.md +0 -51
  165. package/docs/retro/0116-type-housekeeping.md +0 -42
  166. package/docs/retro/0118-settings-manager-apply-methods.md +0 -40
  167. package/docs/retro/0123-remove-vi-fn-cast-smell.md +0 -49
  168. package/docs/retro/0131-consolidate-shared-test-fixtures.md +0 -46
  169. package/docs/retro/0132-inject-io-into-session-config.md +0 -33
  170. package/docs/retro/0133-inject-sdk-boundary-into-agent-runner.md +0 -45
  171. package/docs/retro/0134-reduce-as-any-casts.md +0 -56
  172. package/docs/retro/0135-extract-display-helpers.md +0 -38
  173. package/docs/retro/0136-decompose-agent-menu.md +0 -43
  174. package/docs/retro/0144-consolidate-observation-model.md +0 -39
  175. package/docs/retro/0145-decompose-execute-push-ctx-to-boundary.md +0 -56
  176. package/docs/retro/0146-narrow-ui-context.md +0 -70
  177. package/docs/retro/0147-inject-wrap-text-into-conversation-viewer.md +0 -90
  178. package/docs/retro/0148-split-agent-widget-rendering.md +0 -39
  179. package/docs/retro/0152-add-prompt-snippet.md +0 -34
  180. package/docs/retro/0164-reorganize-into-domain-directories.md +0 -82
  181. package/docs/retro/0165-decompose-resolved-spawn-config.md +0 -78
  182. package/docs/retro/0166-extract-parent-session-info.md +0 -66
  183. package/docs/retro/0167-narrow-runner-io.md +0 -63
  184. package/docs/retro/0168-extract-tool-filter-config.md +0 -76
  185. package/docs/retro/0169-extract-run-context-from-run-options.md +0 -68
  186. package/docs/retro/0170-reduce-build-content-lines-complexity.md +0 -72
  187. package/docs/retro/0171-reduce-render-result-complexity.md +0 -77
  188. package/docs/retro/0172-extract-turn-formatting.md +0 -80
  189. package/docs/retro/0180-reorder-append-prompt-for-kv-cache.md +0 -62
  190. package/docs/retro/0185-remove-persistent-agent-memory.md +0 -73
  191. package/docs/retro/0188-replace-any-casts-with-sdk-types.md +0 -69
  192. package/docs/retro/0192-define-session-context-interface.md +0 -59
  193. package/docs/retro/0193-runtime-owns-context-queries.md +0 -75
  194. package/docs/retro/0194-align-tool-interfaces-for-structural-typing.md +0 -62
  195. package/docs/retro/0195-convert-tool-factories-to-classes.md +0 -84
  196. package/docs/retro/0196-convert-runner-menu-to-classes.md +0 -73
  197. package/docs/retro/0205-decompose-render-widget-lines.md +0 -64
  198. package/docs/retro/0206-decompose-show-agent-detail.md +0 -67
  199. package/docs/retro/0207-decompose-agent-widget-update.md +0 -92
  200. package/docs/retro/0208-extract-shared-test-fixtures.md +0 -102
  201. package/docs/retro/0214-convert-remaining-closure-factories-to-classes.md +0 -66
  202. package/docs/retro/0215-decompose-build-parent-context.md +0 -61
  203. package/docs/retro/0216-decompose-start-agent.md +0 -80
  204. package/docs/retro/0217-extract-overwrite-guard.md +0 -63
  205. package/docs/retro/0218-push-sdk-boundary-in-settings.md +0 -72
  206. package/docs/retro/0219-reduce-test-duplication-top-3-clone-families.md +0 -64
  207. package/docs/retro/0227-evolve-agent-record-into-agent.md +0 -80
  208. package/docs/retro/0228-async-start-agent-dissolve-run-handle.md +0 -80
  209. package/docs/retro/0229-agent-born-complete.md +0 -89
  210. package/docs/retro/0230-extract-concurrency-queue.md +0 -38
  211. package/docs/retro/0231-push-exec-registry-to-runner.md +0 -71
  212. package/docs/retro/0232-agent-resume-internal-observer-lifecycle.md +0 -109
  213. package/docs/retro/0237-remove-disallowed-tools.md +0 -78
  214. package/docs/retro/0238-remove-extensions-filtering.md +0 -72
  215. package/docs/retro/0239-collapse-filter-active-tools.md +0 -70
  216. package/docs/retro/0242-rename-agent-tool-to-subagent.md +0 -82
  217. package/docs/retro/0256-extract-worktree-isolation.md +0 -89
  218. package/docs/retro/0257-extract-child-session-factory.md +0 -33
  219. package/docs/retro/0262-add-workspace-provider-seam.md +0 -89
  220. package/docs/retro/0264-remove-extension-lifecycle-control.md +0 -91
  221. package/docs/retro/0265-born-complete-subagent-session.md +0 -97
  222. package/docs/retro/0270-type-consumable-public-surface.md +0 -108
  223. package/docs/retro/0272-export-workspace-collaborator-types.md +0 -38
  224. package/docs/retro/0277-encapsulate-agent-session.md +0 -80
  225. package/docs/retro/0280-rename-agent-to-subagent.md +0 -96
  226. package/docs/retro/0360-custom-agents-default-append.md +0 -81
  227. package/docs/retro/0373-extract-subagent-state.md +0 -94
  228. package/docs/retro/0374-encapsulate-subagent-start-notification.md +0 -171
  229. package/docs/retro/0375-extract-run-listener-workspace-bracket.md +0 -104
  230. package/docs/retro/0376-extract-manager-observer.md +0 -81
  231. package/docs/retro/0377-split-widget-delegation-from-runtime.md +0 -105
  232. package/docs/retro/0378-consolidate-lifecycle-test-fixtures.md +0 -100
  233. package/docs/retro/0379-consolidate-ui-tools-test-fixtures.md +0 -52
  234. package/docs/retro/0380-extract-layered-settings-loader.md +0 -97
  235. package/docs/retro/0381-replace-concurrency-queue-with-limiter.md +0 -95
  236. package/docs/retro/0400-include-parent-prompt-in-replace-mode.md +0 -84
  237. package/docs/retro/0403-abort-subagents-on-interrupt.md +0 -90
  238. package/docs/retro/0412-unify-session-mock-builders.md +0 -80
  239. package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +0 -90
  240. package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +0 -105
  241. package/docs/retro/0422-delete-activity-tracker-ui-observer.md +0 -97
  242. package/docs/retro/0423-widget-self-drive-from-lifecycle.md +0 -112
  243. package/docs/retro/0424-drop-widget-dep-from-subagent-tool.md +0 -89
  244. package/docs/retro/0425-reconcile-subagent-events-contract.md +0 -101
  245. package/docs/retro/0426-consolidate-residual-test-clone-families.md +0 -100
  246. package/docs/retro/0427-reconsider-ui-direction.md +0 -102
  247. package/docs/retro/0441-remove-agent-definition-management-subtree.md +0 -45
  248. package/docs/retro/0442-dissolve-agents-conversation-viewer.md +0 -108
  249. package/docs/retro/0444-shrink-widget-to-background-agents.md +0 -81
  250. package/docs/retro/0445-native-session-navigation.md +0 -122
  251. package/docs/retro/0446-spike-session-navigation-entry-criteria.md +0 -99
  252. package/docs/retro/0447-extract-subagents-settings-command.md +0 -91
  253. package/docs/retro/0448-block-disabled-agent-spawn.md +0 -75
  254. package/docs/retro/0462-navigation-renderer-tui-components.md +0 -105
  255. package/docs/retro/0463-file-snapshot-source-evicted-agents.md +0 -103
  256. package/src/observation/notification-state.ts +0 -27
  257. package/vitest.config.ts +0 -14
@@ -1,180 +0,0 @@
1
- ---
2
- issue: 403
3
- issue_title: "Pressing Escape does not stop subagent/background agent"
4
- ---
5
-
6
- # Abort subagents on parent interrupt (ESC)
7
-
8
- ## Problem Statement
9
-
10
- A user reports that pressing Escape in the Pi terminal to cancel the current work does not stop a running subagent — the agent keeps going despite the cancel request.
11
- The reporter is a third party (`khalid244`); the operator confirmed the direction is to implement ESC-to-abort for both foreground and background subagents, aborting all running and queued background agents on a single ESC.
12
-
13
- The root cause splits cleanly by execution mode:
14
-
15
- 1. Foreground subagents already receive the parent's abort signal through the tool boundary (`tool.execute(signal)` → `Subagent.wireSignal` → `abort()` → child `session.abort()`), so they should already stop on ESC.
16
- 2. Background subagents are detached by design: `spawnBackground()` never forwards the parent signal, and `manager.abortAll()` runs only on `session_shutdown`.
17
- There is no wiring from a parent interrupt to background-agent abort, so ESC does nothing to them.
18
- This is the reproducible bug.
19
-
20
- ## Goals
21
-
22
- - Pressing ESC (the parent agent-loop interrupt) aborts all running and queued background subagents.
23
- - Add a regression guard test proving a foreground subagent's child session is aborted when the parent signal fires.
24
- - Reuse the existing `manager.abortAll()` semantics (abort running, mark queued stopped, clear the limiter) so ESC stops every active subagent in one action.
25
-
26
- This is an intentional behavior change: background subagents that previously survived ESC will now stop.
27
- It is a bug fix (`fix:`), not a breaking change — no config key, default value, or output shape changes, and detached-survives-ESC was a limitation rather than a contract.
28
-
29
- ## Non-Goals
30
-
31
- - Selective or interactive abort (choosing which agent to stop) — out of scope.
32
- - A dedicated `abortBackground()` that excludes foreground agents — `abortAll()` is reused; foreground agents are already aborted by their own signal wiring, so the overlap is redundant-but-harmless.
33
- - Changing background-agent detachment for any path other than the ESC interrupt (e.g., the tool still returns immediately on spawn).
34
- - Confirmation prompts or status messaging on abort.
35
-
36
- ## Background
37
-
38
- Relevant modules and the verified runtime facts behind the design:
39
-
40
- - `src/tools/foreground-runner.ts` — `runForeground(..., signal, ...)` forwards the parent `signal` into `manager.spawnAndWait({ signal })`.
41
- - `src/lifecycle/subagent.ts` — `run()` calls `this.wireSignal(this.execution.signal, () => this.abort())`; `abort()` fires `abortController.abort()` and marks the record stopped.
42
- - `src/lifecycle/subagent-session.ts` — `runTurnLoop` calls `forwardAbortSignal(session, opts.signal)`, which calls `session.abort()` when the signal fires.
43
- - `src/tools/background-spawner.ts` — `spawnBackground()` omits `signal` entirely; background agents are detached.
44
- - `src/lifecycle/subagent-manager.ts` — `abortAll()` aborts running, marks queued stopped, and clears the limiter; currently called only from `src/handlers/lifecycle.ts` on shutdown.
45
- - `src/handlers/tool-start.ts`, `src/handlers/lifecycle.ts`, `src/handlers/index.ts` — the existing `handlers/` pattern: small classes with a narrow injected interface, registered in `index.ts`.
46
-
47
- Verified SDK facts (from the pinned peer deps under `node_modules/@earendil-works/`):
48
-
49
- - The interactive ESC handler calls `agent.abort()` while streaming (`pi-coding-agent` `interactive-mode.js`, `restoreQueuedMessagesToEditor({ abort: true })`).
50
- - `pi-agent-core` `agent.js`: each run creates a fresh `AbortController`; `agent.abort()` calls `activeRun.abortController.abort()`; on normal completion `finishRun()` discards the controller **without** aborting it.
51
- Therefore the parent signal's `abort` event fires only on a real interrupt, never on normal turn completion — latching `abortAll()` to it will not spuriously kill background agents at turn end.
52
- - The signal passed to `tool.execute(...)` (`agent-loop.js` line ~419) is that same per-run signal.
53
- - Extensions read the live per-run parent signal via `ctx.signal` (`ExtensionContext.signal: AbortSignal | undefined`, undefined when idle).
54
- - `pi.on("turn_start", (event, ctx) => ...)` is a registered event whose handler receives `ExtensionContext`; `turn_start` fires once at the start of every turn while streaming, so its `ctx.signal` is always the current run's signal.
55
-
56
- AGENTS.md constraint: pi-subagents is a minimal core with dependency arrows pointing inward.
57
- The new handler depends only on a narrow manager interface; no consumer knowledge leaks into the manager.
58
-
59
- ## Design Overview
60
-
61
- Add a small `InterruptHandler` that latches the current parent abort signal and, on abort, tells the manager to abort all subagents.
62
- Drive it from `turn_start` so the latch always tracks the live per-run signal — including across runs and turns that execute no tools.
63
-
64
- Why `turn_start` rather than `tool_execution_start`: a background agent can outlive the run that spawned it.
65
- If the user later interrupts a turn that ran no subagent tool, only a turn-level latch still holds that run's signal.
66
- `turn_start` fires every turn with the current `ctx.signal`, so the latch is always current.
67
-
68
- The latch dedups by reference: most turns reuse the same signal (no-op); a new run's signal triggers a detach-and-rewire.
69
- The `abort` listener is `{ once: true }`; on normal completion the run's `AbortController` is discarded and garbage-collected with its listener, and the next `turn_start` detaches the stale reference.
70
-
71
- ### Manager interface (narrow, Tell-Don't-Ask)
72
-
73
- ```typescript
74
- /** Narrow manager interface — only the method the interrupt handler calls. */
75
- export interface InterruptManager {
76
- abortAll(): number;
77
- }
78
-
79
- /** Minimal context shape — only the field the handler reads. */
80
- interface InterruptCtx {
81
- signal: AbortSignal | undefined;
82
- }
83
- ```
84
-
85
- ### Handler
86
-
87
- ```typescript
88
- export class InterruptHandler {
89
- private latched?: AbortSignal;
90
- private detach?: () => void;
91
-
92
- constructor(private readonly manager: InterruptManager) {}
93
-
94
- handleTurnStart(ctx: InterruptCtx): void {
95
- const signal = ctx.signal;
96
- if (signal === this.latched) return;
97
- this.detach?.();
98
- this.detach = undefined;
99
- this.latched = signal;
100
- if (!signal) return;
101
- const onAbort = (): void => {
102
- this.manager.abortAll();
103
- };
104
- signal.addEventListener("abort", onAbort, { once: true });
105
- this.detach = () => signal.removeEventListener("abort", onAbort);
106
- }
107
- }
108
- ```
109
-
110
- ### Consumer call site (`index.ts`)
111
-
112
- ```typescript
113
- const interrupt = new InterruptHandler(manager);
114
- pi.on("turn_start", (_event, ctx) => interrupt.handleTurnStart(ctx));
115
- ```
116
-
117
- The handler talks to `manager` through a one-method interface, reads one field of `ctx`, and performs no chained access — no Law-of-Demeter or output-argument smells.
118
- The latch state (current signal, detach handle) is owned by the handler.
119
-
120
- ### Edge cases
121
-
122
- - Same signal across consecutive turns → reference equality short-circuits; no listener churn.
123
- - `ctx.signal` undefined (idle, defensive) → detach the old listener and hold no signal.
124
- - Signal already aborted when latched → `{ once: true }` listener does not fire; the prior signal's listener already ran `abortAll()`, so no agent is missed.
125
- - ESC during a foreground subagent → the foreground agent is aborted twice (once via its own `wireSignal`, once via `abortAll`); `abort()` is guarded by status and `markStopped` is idempotent, so this is harmless.
126
-
127
- ## Module-Level Changes
128
-
129
- - `src/handlers/interrupt.ts` (new) — `InterruptHandler` class and `InterruptManager` interface.
130
- - `src/handlers/index.ts` — add `export { InterruptHandler } from "#src/handlers/interrupt";`.
131
- - `src/index.ts` — instantiate `new InterruptHandler(manager)` and register `pi.on("turn_start", (_event, ctx) => interrupt.handleTurnStart(ctx))`.
132
- - `src/lifecycle/subagent-manager.ts` — no code change; `abortAll()` is reused.
133
- Its `// fallow-ignore-next-line unused-class-member` comment stays (it is still reached only through narrow interfaces that fallow does not trace); the pre-completion `fallow dead-code` check will confirm.
134
- - `docs/architecture/architecture.md` — extend the `handlers/` directory listing (around line 354) with `interrupt.ts` (turn_start handler → abort all subagents on interrupt).
135
- Check the same file for any handler file-count or complexity row that names the `handlers/` domain and update if present.
136
-
137
- No exports are removed or renamed.
138
- Grep confirms `.pi/skills/package-pi-subagents/SKILL.md` does not mention `abortAll`, interrupt, or ESC, so no skill update is required.
139
-
140
- ## Test Impact Analysis
141
-
142
- This is a feature/fix addition, not an extraction, so no existing tests become redundant.
143
-
144
- 1. New unit tests enabled — `InterruptHandler`: latches the current signal, fires `abortAll()` on abort, dedups the same signal reference, re-wires on a new signal, and handles an undefined signal.
145
- 2. New integration guard — foreground abort: aborting the parent signal passed to `runTurnLoop` invokes the child `session.abort()`.
146
- This pins the currently-untested foreground link in `forwardAbortSignal`.
147
- 3. Existing tests stay as-is — `test/lifecycle/subagent.test.ts` (`wireSignal`, `abort`), `test/lifecycle/subagent-session.test.ts` (max-turns abort path), and `test/handlers/lifecycle.test.ts` (`abortAll` on shutdown) continue to exercise their layers unchanged.
148
-
149
- ## TDD Order
150
-
151
- 1. Foreground guard — `test/lifecycle/subagent-session.test.ts`.
152
- Add a test: when the `signal` passed to `runTurnLoop` aborts while `session.prompt` is in flight, `session.abort()` is called.
153
- Expected to pass immediately (proving the foreground chain already works); if the trace is wrong and it fails, fix `forwardAbortSignal` in `src/lifecycle/subagent-session.ts`.
154
- Commit `test: guard foreground subagent abort on parent signal (#403)` (or `fix:` if a code fix is needed).
155
- 2. Interrupt handler + wiring — `test/handlers/interrupt.test.ts` (new) → `src/handlers/interrupt.ts`, `src/handlers/index.ts`, `src/index.ts`.
156
- Red: write the handler unit tests (latch, abort→abortAll, dedup, re-wire, undefined signal) against the not-yet-existing class.
157
- Green: implement `InterruptHandler` + `InterruptManager`, export from the barrel, and register `pi.on("turn_start", ...)` in `index.ts`.
158
- The handler, its test, and the composition-root wiring land together because the handler is inert without the registration.
159
- Commit `fix: abort all subagents on parent interrupt (#403)`.
160
- 3. Architecture doc — `docs/architecture/architecture.md`.
161
- Add `interrupt.ts` to the `handlers/` directory listing and update any handler-domain count/row if present.
162
- Commit `docs: note interrupt handler in subagents architecture (#403)`.
163
-
164
- ## Risks and Mitigations
165
-
166
- - ESC now stops background agents the user might have wanted to keep running.
167
- Mitigation: this is the operator's explicit choice (abort all running + queued); the behavior is documented in the plan and reflected in the `fix:` commit body.
168
- - Re-latching on every `turn_start` could add overhead.
169
- Mitigation: the latch is a single reference comparison and short-circuits on the common same-signal case.
170
- - A `{ once: true }` listener lingers on a signal that completes normally.
171
- Mitigation: the run's `AbortController` is discarded and GC'd with its listener; the next `turn_start` detaches the stale handle.
172
- - Non-interactive modes (print/rpc) may not emit `turn_start` the same way.
173
- Mitigation: ESC interrupt is an interactive concern; the handler is a no-op when no signal is present.
174
-
175
- ## Open Questions
176
-
177
- - Should a dedicated `abortBackground()` (excluding foreground) replace `abortAll()` here?
178
- Deferred: `abortAll()` is simpler and foreground is already signal-aborted; revisit only if the redundant double-abort proves problematic.
179
- - Should ESC abort surface a confirmation or status message?
180
- Deferred: out of scope for this fix.
@@ -1,146 +0,0 @@
1
- ---
2
- issue: 412
3
- issue_title: "Unify the three overlapping session-mock builders in pi-subagents tests"
4
- ---
5
-
6
- # Unify the overlapping session-mock builders (targeted reuse)
7
-
8
- ## Problem Statement
9
-
10
- `test/helpers/` carries three session-mock builders whose shapes overlap on four fields — `messages`, `subscribe`, `dispose`, `steer`:
11
-
12
- - `createMockSession` (`mock-session.ts`) — a subscribable event bus (`messages`, `subscribe`, `emit`, `dispose`, `steer`, `sessionManager`).
13
- - `createSubagentSessionStub` (`mock-session.ts`) — a born-complete `SubagentSession` wrapper (turn-loop spies plus `messages`/`subscribe`/`dispose`/`steer` delegation).
14
- - `createFactorySession` (`subagent-session-io.ts`) — the `createSubagentSession`-factory session (`bindExtensions`, `setActiveToolsByName`, `getActiveToolNames`, plus `messages`/`subscribe`/`prompt`/`abort`/`steer`/`dispose`).
15
-
16
- Issue [#412] asks whether to unify the three behind one configurable builder.
17
- The issue itself flags the risk — "trading three small honest stubs for one over-parameterized factory" — and quotes Sandi Metz: "duplication is far cheaper than the wrong abstraction."
18
-
19
- ## Goals
20
-
21
- - Remove the one genuine independent redeclaration of the four shared base fields, which lives in `createFactorySession`.
22
- - Make `createMockSession` the explicit shared core, and have `createFactorySession` layer its factory facet on top of that core (working event bus as the core default).
23
- - Keep the change test-only and behavior-preserving for production code.
24
-
25
- This change is **not breaking** — it touches only `test/helpers/` and a doc note; no published surface, default, or runtime behavior changes.
26
-
27
- ## Non-Goals
28
-
29
- - The full composable-factory design from the issue's "Proposed change" (one `createSessionMock()` with opt-in `withTurnLoop()` / `withBindFacet()` extensions).
30
- Rejected at the `Decide` gate (operator chose targeted reuse) because it forces a multi-facet parameterized factory the issue itself warns against.
31
- - Changing `createSubagentSessionStub`.
32
- It already **composes** a `createMockSession` internally and delegates `steer`/`dispose`/`subscribe`/`messages` to it, so its overlap is intrinsic delegation glue, not duplication.
33
- - Renaming any builder or changing any builder's public signature.
34
- - Touching production `src/` modules.
35
-
36
- ## Background
37
-
38
- Relevant modules:
39
-
40
- - `test/helpers/mock-session.ts` — defines `createMockSession` (the event-bus core), `createSubagentSessionStub` (the `SubagentSession` wrapper, which calls `createMockSession()` as its default `session`), plus the `toAgentSession`/`toSubagentSession` casts.
41
- - `test/helpers/subagent-session-io.ts` — defines `createFactorySession` (and the IO/lookup/lifecycle stubs).
42
- `createFactorySession` independently redeclares `messages: []` and an **inert** `subscribe: vi.fn(() => () => {})`, plus its own `steer`/`dispose`, then adds the factory facet.
43
- - `src/lifecycle/create-subagent-session.ts` — production consumer of the raw factory session: calls `getActiveToolNames()`, `setActiveToolsByName()`, `bindExtensions()`, and `dispose()`.
44
- - `src/lifecycle/subagent-session.ts` — wraps the raw session and calls `session.subscribe(...)`, `session.prompt(...)`, `session.abort(...)`, `session.steer(...)`, `session.dispose()`, `session.messages`.
45
-
46
- Structural reading (per the `code-design` "structural reasons before extracting duplication" heuristic): the three builders sit on two axes — AgentSession-vs-`SubagentSession` (type) and event-bus-vs-factory (facet).
47
- `createMockSession` and `createFactorySession` are both AgentSession stubs; `createSubagentSessionStub` is the `SubagentSession` wrapper.
48
- The only honest, non-delegation duplication is `createFactorySession`'s independent base, so that is the entire target.
49
-
50
- AGENTS.md constraint: this is the `@gotgenes/pi-subagents` package; run package-scoped scripts via `pnpm --filter @gotgenes/pi-subagents exec vitest run` and type-check with `pnpm run check`.
51
-
52
- ## Design Overview
53
-
54
- `createMockSession` is already the core shape, with the working event bus the operator chose as the default.
55
- `createFactorySession` is rebuilt to spread that core and add only the factory facet:
56
-
57
- ```typescript
58
- export function createFactorySession(options: FactorySessionOptions = {}) {
59
- const before = options.toolsBeforeBind ?? ["read"];
60
- const after = options.toolsAfterBind ?? before;
61
- let bound = false;
62
- return {
63
- ...createMockSession(), // working event bus + messages/steer/dispose/sessionManager core
64
- prompt: vi.fn().mockResolvedValue(undefined),
65
- abort: vi.fn(),
66
- getActiveToolNames: vi.fn(() => (bound ? after : before)),
67
- setActiveToolsByName: vi.fn(),
68
- bindExtensions: vi.fn(async () => {
69
- bound = true;
70
- }),
71
- };
72
- }
73
- ```
74
-
75
- Mock-typing is preserved through the spread.
76
- `createMockSession()` returns `MockSession & Record<string, unknown>`; the explicit facet properties added in the literal intersect with the `Record`'s `unknown` index signature, and `unknown & Mock<...>` narrows to `Mock<...>`.
77
- Verified with a throwaway `tsc --noEmit` probe: `session.setActiveToolsByName.mock.calls[0][0]` type-checks after the spread.
78
-
79
- Facet semantics are unchanged: `getActiveToolNames` closes over a `bound` flag flipped by `bindExtensions`, returning `before` until bind and `after` after.
80
-
81
- Behavioral delta (intentional, per the operator's "working bus is the core default" choice): `createFactorySession`'s `subscribe` changes from inert (`() => () => {}`) to the core's real bus, and the session gains `emit` and `sessionManager`.
82
- No factory test emits events, and production `subagent-session.ts` only registers a subscriber (never emits during these tests), so the change is inert in practice — the disposer now truly unregisters instead of being a no-op.
83
-
84
- Edge cases:
85
-
86
- - `create-subagent-session.test.ts:194` asserts `expect(session.dispose).toHaveBeenCalledOnce()` — `dispose` remains a `vi.fn()` spy supplied by the core, so the assertion holds.
87
- - The factory session is returned through `io.createSession.mockResolvedValue({ session })`, not assigned as a fresh literal to a typed slot, so the extra `emit`/`sessionManager` fields raise no excess-property error.
88
-
89
- ## Module-Level Changes
90
-
91
- - `test/helpers/subagent-session-io.ts` — rewrite `createFactorySession` to spread `...createMockSession()` and add the factory facet; import `createMockSession` from `#test/helpers/mock-session`.
92
- Update its docstring to state it layers the factory facet on the shared `createMockSession` core.
93
- - `test/helpers/subagent-session-io.test.ts` — add one self-test asserting `createFactorySession` exposes the core's working event bus (subscribe a fn, `emit`, assert receipt).
94
- Existing `createFactorySession` self-tests remain unchanged.
95
- - `test/helpers/mock-session.ts` — docstring touch-up only: note that `createMockSession` is the shared session-mock core that `createFactorySession` builds on (no code change).
96
- - `docs/architecture/architecture.md` — update the Phase 17 Step 7 "Landed" closing sentence (currently "The three overlapping session-mock builders this surfaced are tracked separately [#412].") to record the resolution: targeted reuse — `createFactorySession` now layers on the `createMockSession` core; `createSubagentSessionStub`'s overlap is intrinsic delegation and was left as-is.
97
-
98
- Grep confirmation (no other consumers break): `createFactorySession` is referenced only in `create-subagent-session.test.ts` and its own self-test/source; `.pi/skills/package-*/SKILL.md` mention none of the three builders by name; the `[#412]:` reference-link definition already exists at the bottom of `architecture.md`.
99
-
100
- ## Test Impact Analysis
101
-
102
- 1. **New tests enabled** — one self-test for `createFactorySession`'s newly-inherited working event bus (subscribe/emit), previously impossible because its `subscribe` was inert.
103
- 2. **Tests made redundant** — none.
104
- The existing `createFactorySession` self-tests exercise the factory facet (the eight methods, the before/after bind flip), which is unchanged and still its own concern.
105
- 3. **Tests that must stay as-is** — the `createMockSession` event-bus self-tests in `mock-session.test.ts` (they pin the core the factory now reuses); the `createFactorySession` facet self-tests; `create-subagent-session.test.ts` lifecycle tests (they exercise the production consumer of the factory session).
106
-
107
- ## Invariants at risk
108
-
109
- The change touches the lifecycle test fixtures refactored in Phase 17 Step 7 ([#378]).
110
- That step's documented `Outcome`/`Landed` invariants and the tests that pin them:
111
-
112
- - "AAA structure: `createSubagentSession(...)` act kept explicit per test" — pinned by `create-subagent-session.test.ts` (the act calls remain in each `it`; this plan does not touch them).
113
- - "`createFactorySession` flips `getActiveToolNames` from before-bind to after-bind" — pinned by the existing `subagent-session-io.test.ts` facet self-tests, which stay green through the rewrite.
114
-
115
- No earlier outcome is regressed: the rewrite preserves every facet assertion and only adds the event-bus surface.
116
-
117
- ## TDD Order
118
-
119
- 1. **`refactor(test):` fold `createFactorySession` onto the shared core** (#412).
120
- - Red: add `it("exposes the core's working event bus (subscribe/emit)")` to the `createFactorySession` describe in `subagent-session-io.test.ts` — fails to compile/pass because the current return has no `emit` and an inert `subscribe`.
121
- - Green: rewrite `createFactorySession` to `{ ...createMockSession(), <factory facet> }`; import `createMockSession`; refresh the docstrings in both helper files.
122
- - Verify: `pnpm --filter @gotgenes/pi-subagents exec vitest run test/helpers/subagent-session-io.test.ts test/helpers/mock-session.test.ts test/lifecycle/create-subagent-session.test.ts`, then `pnpm run check` (spread-typing) and the full package suite.
123
- - Commit: `refactor(test): build createFactorySession on the shared createMockSession core (#412)`.
124
- 2. **`docs:` record the resolution in the architecture doc** (#412).
125
- - Update the Phase 17 Step 7 closing sentence in `docs/architecture/architecture.md` to describe the targeted-reuse outcome (no roadmap-table or metric-row change required; rerun `pnpm fallow:dupes` only to report, not to gate).
126
- - Commit: `docs: record session-mock builder unification outcome (#412)`.
127
-
128
- Both steps are test/doc-only with single, isolated surfaces, so no lift-and-shift staging is needed.
129
-
130
- ## Risks and Mitigations
131
-
132
- - **Risk:** the spread erases `Mock<...>` typing on the facet methods, breaking `.mock.calls` assertions.
133
- **Mitigation:** verified with a throwaway `tsc --noEmit` probe that `setActiveToolsByName.mock.calls[0][0]` type-checks after the spread (intersection narrows `unknown & Mock<...>` to `Mock<...>`); Step 1 reruns `pnpm run check`.
134
- - **Risk:** the inert→working `subscribe` change alters a lifecycle test's behavior.
135
- **Mitigation:** no factory test emits events; production only registers a subscriber during these tests; `session.dispose` remains a spy.
136
- Step 1's verify command runs the lifecycle suite.
137
- - **Risk:** the extra `emit`/`sessionManager` fields trip excess-property checks at a consumer.
138
- **Mitigation:** the factory session flows through `mockResolvedValue({ session })`, not a fresh typed literal, so excess-property checking does not apply.
139
-
140
- ## Open Questions
141
-
142
- None.
143
- The operator confirmed targeted reuse with a working-bus core default at the `Decide` gate.
144
-
145
- [#378]: https://github.com/gotgenes/pi-packages/issues/378
146
- [#412]: https://github.com/gotgenes/pi-packages/issues/412
@@ -1,228 +0,0 @@
1
- ---
2
- issue: 420
3
- issue_title: "pi-subagents: fold subagent run metrics and live activity onto the core record"
4
- ---
5
-
6
- # Fold subagent run metrics and live activity onto the core record
7
-
8
- ## Problem Statement
9
-
10
- The run-metric domain is split across two parallel observers, each subscribed to every child session.
11
- `record-observer` accumulates `SubagentState` (tool uses, lifetime usage, compaction count); `ui-observer` accumulates a separate `AgentActivityTracker` (active tools, response text, turn count).
12
- `turnCount` is a genuine run metric that lives only in the UI tracker, so `notification.ts` and the foreground result text reach into the tracker to recover it (Phase 18 Finding 4).
13
- Consolidating the live-activity fields onto the single owned run-state value object (`SubagentState`) removes the duplication and is the first move in disentangling the UI's activity tier from the core.
14
-
15
- This is Phase 18 Step 1 of the architecture roadmap (`docs/architecture/architecture.md`).
16
- It is a **pure addition, tidy-first**: `AgentActivityTracker` and `ui-observer` still exist, both observers still run, and nothing reads the new getters yet.
17
- The reader migration is Step 2 ([#421]) and the tracker deletion is Step 3 ([#422]).
18
-
19
- ## Goals
20
-
21
- - Extend `SubagentState` (`src/lifecycle/subagent-state.ts`) with the live-activity fields — `turnCount`, `activeTools`, `responseText` — and their transition methods, behind read-only getters, owning all their own mutations (no field written from outside).
22
- - Have `subscribeSubagentObserver` (`src/observation/record-observer.ts`) handle `turn_end`, `tool_execution_start`, `message_start`, and `message_update` (text_delta) alongside the events it already handles, mutating the same `SubagentState`.
23
- - Add read-only `turnCount` / `maxTurns` / `activeTools` / `responseText` getters to `Subagent` (`src/lifecycle/subagent.ts`); the first three plus `responseText` delegate to the owned state, `maxTurns` delegates to `this.execution.maxTurns` (the effective max-turns already wired at spawn).
24
- - Preserve the `AgentActivityTracker` semantics the readers depend on so Step 2 is a clean swap: `turnCount` starts at 1 and increments on each `turn_end`; `activeTools` is a `ReadonlyMap<string, string>` keyed `name_seq` to disambiguate concurrent same-name tools; `responseText` resets at each `message_start` and appends each text delta.
25
-
26
- This change is **not breaking**.
27
- The published service surface (`src/service/service.ts`) exposes `SubagentRecord`/`SubagentStatus`, not `SubagentState` or the `Subagent` constructor; the change only adds getters and value-object fields.
28
- No observable behavior changes — both observers still run and no consumer reads the new getters.
29
-
30
- ## Non-Goals
31
-
32
- - Migrating any reader (`widget-renderer`, `conversation-viewer`, `agent-menu`, `foreground-runner`, `notification`) off `AgentActivityTracker` — Step 2 ([#421]).
33
- - Deleting `AgentActivityTracker` / `ui-observer` or dropping `SubagentRuntime.agentActivity` — Step 3 ([#422]).
34
- - Removing the second (`ui-observer`) session subscription — Step 3.
35
- - Adding a `session` reference to `SubagentState` — the tracker's `_session`/`setSession` exists only for the UI's polling reads and is migrated/removed in Steps 2–3, not folded here.
36
- - The deeper "metrics as a pure observer projection" target — deliberately deferred per the architecture doc's first-principles refinement; folding onto the record (consistent with Phase 17's `SubagentState`) avoids inventing an asynchronous-observation seam.
37
- - Resetting the new live-activity fields in `resetForResume` — see Design Overview; left unchanged to preserve tracker parity.
38
-
39
- ## Background
40
-
41
- Relevant modules and how they relate:
42
-
43
- - `src/lifecycle/subagent-state.ts` — the value object under change.
44
- Already owns status/result/error/timestamps and stats (`toolUses`, `lifetimeUsage`, `compactionCount`) behind getters, mutated only via transition/accumulation methods (`markRunning`, `incrementToolUses`, `addUsage`, `incrementCompactions`).
45
- Pure value object — imports only `LifetimeUsage`/`addUsage` from `usage.ts`; no Pi SDK imports (keep it that way).
46
- - `src/observation/record-observer.ts` — `subscribeSubagentObserver(session, state, { onCompact })` subscribes once and currently handles `tool_execution_end`, `message_end` (assistant), and `compaction_end`.
47
- - `src/ui/agent-activity-tracker.ts` — the parallel `AgentActivityTracker` whose live fields (`_activeTools`, `_toolKeySeq`, `_responseText`, `_turnCount` starting at 1) and transition methods (`onToolStart`, `onToolDone`, `onMessageStart`, `onMessageUpdate`, `onTurnEnd`) are the behavior being folded onto `SubagentState`.
48
- It also carries `_session`/`setSession` and `maxTurns` — `maxTurns` is folded (via `execution`), `_session` is not.
49
- - `src/ui/ui-observer.ts` — `subscribeUIObserver` maps `tool_execution_start`/`tool_execution_end`/`message_start`/`message_update`/`turn_end` onto the tracker.
50
- The event-shape reference for the new `record-observer` branches (`event.toolName`; `event.assistantMessageEvent.type === "text_delta"` → `event.assistantMessageEvent.delta`).
51
- - `src/lifecycle/subagent.ts` — holds one private `SubagentState`; its getters and mutation methods are one-line delegations.
52
- `run()` and `resume()` both attach `subscribeSubagentObserver(session, this.state, …)`.
53
- `this.execution.maxTurns` is the effective max-turns (both spawners pass `execution.effectiveMaxTurns` as `options.maxTurns`, threaded into `SubagentExecution.maxTurns` by `SubagentManager.spawn`).
54
- - `src/tools/spawn-config.ts` — `effectiveMaxTurns = normalizeMaxTurns(resolvedConfig.maxTurns ?? settings.defaultMaxTurns)`.
55
- `AgentActivityTracker` is constructed with `effectiveMaxTurns`; `SubagentExecution.maxTurns` carries the same value, so the new `maxTurns` getter matches the tracker.
56
-
57
- AGENTS.md / code-design constraints that apply:
58
-
59
- - Keep Pi SDK imports out of `SubagentState` — the new fields and methods stay SDK-free.
60
- - The new `record-observer` branches consume SDK event payloads via the existing lean structural reads (`event.toolName`, `event.assistantMessageEvent`) — no new SDK type imports needed.
61
- - `SubagentState` and `Subagent` are reached through the `types.ts`/internal barrels; adding getters does not change any export.
62
-
63
- ## Design Overview
64
-
65
- ### `SubagentState` additions
66
-
67
- Three new private fields with read-only getters and five transition methods, mirroring the `AgentActivityTracker` semantics exactly so the Step 2 swap is byte-equivalent:
68
-
69
- ```typescript
70
- // Live activity — accumulated via transition methods, readable via getters
71
- private _turnCount = 1;
72
- get turnCount(): number { return this._turnCount; }
73
-
74
- private _activeTools = new Map<string, string>();
75
- get activeTools(): ReadonlyMap<string, string> { return this._activeTools; }
76
-
77
- private _toolKeySeq = 0;
78
-
79
- private _responseText = "";
80
- get responseText(): string { return this._responseText; }
81
-
82
- /** Record a turn boundary. Called by record-observer on turn_end. */
83
- incrementTurnCount(): void { this._turnCount++; }
84
-
85
- /** Record a tool starting. Called by record-observer on tool_execution_start. */
86
- addActiveTool(toolName: string): void {
87
- this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
88
- }
89
-
90
- /** Remove one active tool by name (first match). Called by record-observer on tool_execution_end. */
91
- removeActiveTool(toolName: string): void {
92
- for (const [key, name] of this._activeTools) {
93
- if (name === toolName) { this._activeTools.delete(key); break; }
94
- }
95
- }
96
-
97
- /** Reset the current response text. Called by record-observer on message_start. */
98
- resetResponseText(): void { this._responseText = ""; }
99
-
100
- /** Append a text delta to the current response text. Called by record-observer on message_update. */
101
- appendResponseText(delta: string): void { this._responseText += delta; }
102
- ```
103
-
104
- The `_toolKeySeq` counter has no getter — it is internal disambiguation state, exactly as in the tracker.
105
- `turnCount` initializes to `1` (not `0`) to match the tracker; the readers (`notification.ts` `turnCount ?? 0`, `result-renderer.ts` `turnCount > 0`) already assume the at-least-1 invariant once an agent exists.
106
-
107
- `resetForResume` is left **unchanged**.
108
- The tracker is not reset on resume today (the resume path in `agent-tool.ts` does not reconstruct it), so the record-observer accumulating onto the surviving `SubagentState` across a resume preserves parity (turn count continues, response text carries the last message, active tools are empty after a completed run).
109
- Touching `resetForResume` here would be a behavior change, contradicting the pure-addition contract.
110
- Flagged in Risks for Step 2 to revisit if a reader demands a reset.
111
-
112
- ### `record-observer` additions
113
-
114
- Three new branches alongside the existing ones, reading the same SDK payload shapes `ui-observer` reads:
115
-
116
- ```typescript
117
- if (event.type === "tool_execution_start") state.addActiveTool(event.toolName);
118
- if (event.type === "turn_end") state.incrementTurnCount();
119
- if (event.type === "message_start") state.resetResponseText();
120
- if (
121
- event.type === "message_update" &&
122
- event.assistantMessageEvent.type === "text_delta"
123
- ) {
124
- state.appendResponseText(event.assistantMessageEvent.delta);
125
- }
126
- ```
127
-
128
- The existing `tool_execution_end` branch already fires `state.incrementToolUses()`; it gains a paired `state.removeActiveTool(event.toolName)` so the active-tool map drains symmetrically (the tracker did this in `onToolDone`).
129
- The observer takes no new options and emits no callbacks for these branches — the live fields are read-only state, polled by consumers later.
130
-
131
- ### `Subagent` getters
132
-
133
- Four one-line getters added next to the existing `toolUses`/`lifetimeUsage`/`compactionCount` delegations:
134
-
135
- ```typescript
136
- get turnCount(): number { return this.state.turnCount; }
137
- get activeTools(): ReadonlyMap<string, string> { return this.state.activeTools; }
138
- get responseText(): string { return this.state.responseText; }
139
- get maxTurns(): number | undefined { return this.execution.maxTurns; }
140
- ```
141
-
142
- `maxTurns` is the one getter that does not delegate to `state` — max-turns is execution config, not accumulated run state, and `this.execution.maxTurns` already holds the effective value (`effectiveMaxTurns`) the tracker was constructed with.
143
-
144
- ### Extracted-module interaction check (record-observer ↔ SubagentState)
145
-
146
- The observer already targets `SubagentState` directly (Phase 17) and carries no `Subagent` dependency.
147
- The new branches are Tell-Don't-Ask: the observer tells the state to mutate (`addActiveTool`, `incrementTurnCount`, …) and never reads-then-writes a field on the state.
148
- No output arguments, no reach-through, no reverse search — the same shape as the existing three branches.
149
-
150
- ## Module-Level Changes
151
-
152
- - `src/lifecycle/subagent-state.ts` — add `_turnCount`/`_activeTools`/`_toolKeySeq`/`_responseText` fields, their getters (`turnCount`, `activeTools`, `responseText`), and five transition methods (`incrementTurnCount`, `addActiveTool`, `removeActiveTool`, `resetResponseText`, `appendResponseText`).
153
- Update the file header doc comment to note the live-activity fields.
154
- - `src/observation/record-observer.ts` — add `tool_execution_start`, `turn_end`, `message_start`, `message_update` (text_delta) branches; add `removeActiveTool` to the existing `tool_execution_end` branch.
155
- Update the JSDoc "Handles:" list.
156
- - `src/lifecycle/subagent.ts` — add `turnCount`, `activeTools`, `responseText`, `maxTurns` getters.
157
- - `src/lifecycle/subagent-state.ts` is referenced in `.pi/skills/package-pi-subagents/SKILL.md` and `docs/architecture/architecture.md` only as a value object / domain entry; no symbol is removed or renamed, so no skill/doc prose update is required.
158
- The architecture doc's Phase 18 Step 1 entry already describes this work — no edit needed.
159
-
160
- No existing export is removed or renamed; no call site changes.
161
- The `AgentActivityTracker` / `ui-observer` modules are untouched (they are deleted in Step 3).
162
-
163
- ## Test Impact Analysis
164
-
165
- This is an additive change, so the analysis is mostly about new coverage:
166
-
167
- 1. **New unit tests enabled.**
168
- `subagent-state.test.ts` gains direct tests for the new fields/methods (turn count starts at 1 and increments; active-tool add/remove with concurrent same-name handling; response-text reset/append) — testable on a bare `SubagentState` with no session or executor.
169
- `record-observer.test.ts` gains tests that emitting `tool_execution_start`/`turn_end`/`message_start`/`message_update`/`tool_execution_end` drives the new state fields, using the existing `createMockSession` `emit` harness.
170
- 2. **Tests that become redundant.**
171
- None yet — `ui-observer.test.ts` and the tracker tests still pin live behavior until Step 3 deletes those modules.
172
- The new tests overlap conceptually with `ui-observer.test.ts` but exercise a different module (`record-observer` → `SubagentState`), so both stay until the tracker is removed.
173
- 3. **Tests that must stay as-is.**
174
- All existing `record-observer.test.ts` cases (tool uses, lifetime usage, compaction) genuinely exercise the observer→state path being extended and must keep passing unchanged.
175
- `subagent.test.ts` getter/delegation tests stay; new getter tests are added beside them.
176
-
177
- ## Invariants at risk
178
-
179
- Phase 17 Step 1 ([#373]) established that `SubagentState` owns all its own mutations (no field written from outside) and that `record-observer` targets `SubagentState` directly with no `Subagent` dependency.
180
-
181
- - **Invariant: the value object owns every mutation** — pinned by the encapsulation tests in `subagent-state.test.ts` (fields are read-only getters; mutation only via methods).
182
- The new fields follow the same private-field/getter/transition-method shape; new tests assert mutation flows only through the new methods.
183
- - **Invariant: `record-observer` carries no `Subagent` dependency** — pinned by `record-observer.test.ts` constructing a bare `SubagentState` (never a `Subagent`).
184
- The new branches keep that property (they call `state.*` only); the existing test setup (`makeState()` returns `new SubagentState(...)`) already guards it.
185
-
186
- No earlier phase step's `Outcome:` is regressed — this step only adds to the record both prior steps consolidated onto.
187
-
188
- ## TDD Order
189
-
190
- 1. **`SubagentState` live-activity fields and transition methods.**
191
- Surface: `test/lifecycle/subagent-state.test.ts`.
192
- Red→green: new `describe` blocks for turn count (defaults to 1, `incrementTurnCount` increments), active tools (`addActiveTool` adds with unique keys for concurrent same-name tools, `removeActiveTool` removes first match and is a no-op when absent, getter is read-only), and response text (defaults to `""`, `resetResponseText` clears, `appendResponseText` concatenates).
193
- Implement the fields/getters/methods in `src/lifecycle/subagent-state.ts`.
194
- Commit: `feat: add live-activity fields to SubagentState (#420)`.
195
- 2. **`record-observer` populates the live-activity fields.**
196
- Surface: `test/observation/record-observer.test.ts`.
197
- Red→green: new tests that `tool_execution_start` adds an active tool, paired `tool_execution_end` removes it (extending the existing tool-uses test or a sibling case), `turn_end` increments `turnCount`, `message_start` resets `responseText`, and `message_update` text_delta appends; assert non-text_delta `message_update` is ignored.
198
- Implement the new branches in `src/observation/record-observer.ts` and the `removeActiveTool` call in the existing `tool_execution_end` branch.
199
- Commit: `feat: accumulate live activity in record-observer (#420)`.
200
- 3. **`Subagent` exposes the live-activity getters.**
201
- Surface: `test/lifecycle/subagent.test.ts`.
202
- Red→green: tests that `turnCount`/`activeTools`/`responseText` delegate to the owned state (seed via a supplied `SubagentState` or by driving the attached observer), and `maxTurns` returns `execution.maxTurns`.
203
- Implement the four getters in `src/lifecycle/subagent.ts`.
204
- Run `pnpm run check` after this commit — the getters are the public-facing surface this step adds.
205
- Commit: `feat: expose live-activity getters on Subagent (#420)`.
206
-
207
- Each step is independently green: the field exists before the observer writes it, and the observer writes it before `Subagent` exposes it.
208
- No existing test breaks at any step (pure addition, no removed/renamed symbol).
209
-
210
- ## Risks and Mitigations
211
-
212
- - **Risk: the folded fields drift from `AgentActivityTracker` semantics, making Step 2's swap not behavior-preserving.**
213
- Mitigation: the transition methods are copied field-for-field (turn count starts at 1, `name_seq` keying, first-match removal, reset-then-append response text); Step 2's reader-migration tests will compare record getters against the tracker before deletion.
214
- - **Risk: resume parity — the surviving `SubagentState` accumulates turn count across a resume while a fresh tracker would not.**
215
- Mitigation: the tracker is *not* reset on resume today either (it is not reconstructed), so the record matches.
216
- `resetForResume` is left unchanged and the decision is documented; Step 2 revisits if a reader needs a per-resume reset.
217
- - **Risk: a second observer now writes the same conceptual fields (record-observer + ui-observer), briefly duplicating active-tool/turn-count bookkeeping.**
218
- Mitigation: this is the intended tidy-first overlap; the duplicate (`ui-observer`) is removed in Step 3 ([#422]).
219
- No consumer reads the record's copy yet, so the duplication is inert.
220
-
221
- ## Open Questions
222
-
223
- - Whether `resetForResume` should clear the live-activity fields is deferred to Step 2 ([#421]), where the actual reader behavior on resume is observable.
224
- Resolving it now would be speculative and would break the pure-addition contract.
225
-
226
- [#421]: https://github.com/gotgenes/pi-packages/issues/421
227
- [#422]: https://github.com/gotgenes/pi-packages/issues/422
228
- [#373]: https://github.com/gotgenes/pi-packages/issues/373