@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,345 +0,0 @@
1
- ---
2
- issue: 69
3
- issue_title: "refactor: eliminate module-scope mutable state in pi-subagents — create SubagentRuntime"
4
- ---
5
-
6
- # Create SubagentRuntime
7
-
8
- ## Problem Statement
9
-
10
- `pi-subagents` still uses pre-refactor patterns that `pi-permission-system` eliminated in #43.
11
- `agent-runner.ts` holds module-scope mutable `let` variables (`defaultMaxTurns`, `graceTurns`) with getter/setter pairs that are called from `settings.ts` via callback injection.
12
- `index.ts` holds closure-scoped `let` variables (`currentCtx`, `widget`) and a `Map` (`agentActivity`) that are captured by arrow closures and cannot be tested in isolation.
13
- Both patterns hide real dependencies behind module-scope and closure-scope state, making isolated testing impossible.
14
-
15
- ## Goals
16
-
17
- - Introduce a `SubagentRuntime` interface and `createSubagentRuntime()` factory in a new `src/runtime.ts`.
18
- - Move `defaultMaxTurns`, `graceTurns`, `agentActivity`, `currentCtx`, and the widget reference into the runtime.
19
- - Thread `defaultMaxTurns` and `graceTurns` through `RunOptions` so `agent-runner.ts` reads them from its call-time options — not from module scope.
20
- - Give `AgentManager` a config getter so it can pass runtime values in `RunOptions`.
21
- - Reduce `index.ts` to a composition root that creates the runtime and passes it to factories — no closure-scoped mutable `let` variables remain.
22
- - Remove the module-scope `let` declarations and getter/setter exports from `agent-runner.ts`.
23
- - No behavior change; pure structural refactor.
24
-
25
- ## Non-Goals
26
-
27
- - Refactoring `AgentManager` into an options-object constructor (follow-up cleanup).
28
- - Extracting event handlers into separate files.
29
- - Changing tool behavior or the `SubagentsService` interface.
30
- - Changing the `SettingsAppliers` interface in `settings.ts` — the callback pattern is already clean; only the closure targets change.
31
-
32
- ## Background
33
-
34
- ### Prior art
35
-
36
- `pi-permission-system` solved the identical problem in #43.
37
- `src/runtime.ts` there defines an `ExtensionRuntime` interface with all mutable state, a `createExtensionRuntime()` factory, and pure helper functions like `refreshExtensionConfig(runtime, ctx)` that write to the runtime instead of module-scope variables.
38
- The extension's `index.ts` calls `createExtensionRuntime()` once and passes the runtime to handlers and factories.
39
- This plan follows the same pattern.
40
-
41
- ### Module-scope state in agent-runner.ts
42
-
43
- Two `let` variables and four getter/setter exports:
44
-
45
- ```typescript
46
- let defaultMaxTurns: number | undefined;
47
- let graceTurns = 5;
48
-
49
- export function getDefaultMaxTurns(): number | undefined { ... }
50
- export function setDefaultMaxTurns(n: number | undefined): void { ... }
51
- export function getGraceTurns(): number { ... }
52
- export function setGraceTurns(n: number): void { ... }
53
- ```
54
-
55
- `runAgent` reads both from module scope during the turn-limit subscription callback:
56
-
57
- ```typescript
58
- const maxTurns = normalizeMaxTurns(
59
- options.maxTurns ?? agentConfig?.maxTurns ?? defaultMaxTurns,
60
- );
61
- // ...
62
- } else if (softLimitReached && turnCount >= maxTurns + graceTurns) {
63
- ```
64
-
65
- ### Closure-scoped state in index.ts
66
-
67
- ```typescript
68
- const agentActivity = new Map<string, AgentActivity>();
69
- let widget: AgentWidget;
70
- let currentCtx: { pi: unknown; ctx: unknown } | undefined;
71
- ```
72
-
73
- `widget` is assigned *after* `AgentManager` construction, but `notifications` closes over it immediately via arrow callbacks (`(id) => widget.markFinished(id)`).
74
- `currentCtx` is written by `session_start` and read by `createSubagentsService`.
75
- `agentActivity` is shared across the notification system, widget, agent tool, and menu handler.
76
-
77
- ### Settings flow
78
-
79
- `settings.ts` defines `SettingsAppliers` with three setter callbacks.
80
- `applyAndEmitLoaded(appliers, emit)` loads persisted settings and calls them.
81
- `index.ts` wires the appliers to `setDefaultMaxTurns` / `setGraceTurns` from `agent-runner.ts` and `manager.setMaxConcurrent`.
82
- After this refactor, the appliers closure targets change to the runtime — the `SettingsAppliers` interface itself stays the same.
83
-
84
- ### Data flow for defaultMaxTurns / graceTurns
85
-
86
- Current: `settings.ts → setDefaultMaxTurns() → module-scope let → runAgent reads module scope`.
87
-
88
- After: `settings.ts → applier closure → runtime.defaultMaxTurns → AgentManager.getRunConfig() → RunOptions → runAgent reads options`.
89
-
90
- ### Relevant constraints from AGENTS.md
91
-
92
- - Keep modules focused and composable (one concern per file).
93
- - Prefer explicit configuration over hidden behavior.
94
- - Pi SDK imports stay out of business-logic modules — `runtime.ts` must not import Pi SDK types.
95
- - Do not read `process.env` / `process.cwd()` inside library functions — accept as parameter.
96
- - Narrow interfaces per consumer — do not pass a shared dependency bag when a function only uses a subset.
97
-
98
- ## Design Overview
99
-
100
- ### SubagentRuntime interface
101
-
102
- ```typescript
103
- export interface SubagentRuntime {
104
- // ── Execution config (was module-scope in agent-runner.ts) ──
105
- defaultMaxTurns: number | undefined;
106
- graceTurns: number;
107
-
108
- // ── Session state (was closure-scoped in index.ts) ──
109
- currentCtx: { pi: unknown; ctx: unknown } | undefined;
110
- readonly agentActivity: Map<string, AgentActivity>;
111
- widget: AgentWidget | null;
112
- }
113
- ```
114
-
115
- The interface is flat (no sub-objects) to match the prior art in `pi-permission-system`.
116
- `agentActivity` is `readonly` because the Map itself is never replaced — only its entries change.
117
- `widget` is nullable because it is constructed after `AgentManager` and assigned later.
118
-
119
- ### createSubagentRuntime factory
120
-
121
- ```typescript
122
- export function createSubagentRuntime(): SubagentRuntime {
123
- return {
124
- defaultMaxTurns: undefined,
125
- graceTurns: 5,
126
- currentCtx: undefined,
127
- agentActivity: new Map(),
128
- widget: null,
129
- };
130
- }
131
- ```
132
-
133
- No parameters needed — the factory returns defaults.
134
- Tests construct a fresh runtime per test for isolation.
135
-
136
- ### RunConfig — narrow interface for agent-manager
137
-
138
- ```typescript
139
- export interface RunConfig {
140
- readonly defaultMaxTurns: number | undefined;
141
- readonly graceTurns: number;
142
- }
143
- ```
144
-
145
- `AgentManager` receives `getRunConfig?: () => RunConfig` as a constructor parameter.
146
- When constructing `RunOptions` for `runAgent`, it calls `getRunConfig()` and spreads the values.
147
- During the lift-and-shift phase (before module-scope removal), `runAgent` falls back to the module-scope values when the RunOptions fields are absent.
148
-
149
- ### RunOptions changes
150
-
151
- Two new optional fields:
152
-
153
- ```typescript
154
- export interface RunOptions {
155
- // ... existing fields ...
156
- /** Default max turns from runtime config. Overridden by per-agent maxTurns. */
157
- defaultMaxTurns?: number;
158
- /** Grace turns after soft limit steer. */
159
- graceTurns?: number;
160
- }
161
- ```
162
-
163
- `runAgent` changes its resolution chain from:
164
-
165
- ```typescript
166
- const maxTurns = normalizeMaxTurns(
167
- options.maxTurns ?? agentConfig?.maxTurns ?? defaultMaxTurns,
168
- );
169
- ```
170
-
171
- To:
172
-
173
- ```typescript
174
- const maxTurns = normalizeMaxTurns(
175
- options.maxTurns ?? agentConfig?.maxTurns ?? options.defaultMaxTurns,
176
- );
177
- const effectiveGraceTurns = options.graceTurns ?? 5;
178
- ```
179
-
180
- ### normalizeMaxTurns stays in agent-runner.ts
181
-
182
- `normalizeMaxTurns` is a pure function used by both the runtime setter logic (in `index.ts` wire-up) and `runAgent`'s maxTurns resolution.
183
- It stays exported from `agent-runner.ts`.
184
-
185
- ### index.ts wire-up changes
186
-
187
- After refactoring, the extension factory:
188
-
189
- 1. Calls `createSubagentRuntime()` to get the runtime.
190
- 2. Wires `applyAndEmitLoaded` appliers to write to `runtime.defaultMaxTurns` and `runtime.graceTurns` (with normalization).
191
- 3. Passes `getRunConfig: () => ({ defaultMaxTurns: runtime.defaultMaxTurns, graceTurns: runtime.graceTurns })` to `AgentManager`.
192
- 4. Uses `runtime.agentActivity` instead of a local `const agentActivity`.
193
- 5. Uses `runtime.currentCtx` instead of a local `let currentCtx`.
194
- 6. Sets `runtime.widget = new AgentWidget(...)` instead of a local `let widget`.
195
- 7. Arrow closures in notification deps, tool deps, and menu deps reference `runtime.widget!` / `runtime.agentActivity` / `runtime.currentCtx` by capturing `runtime` by reference.
196
-
197
- No closure-scoped `let` variables remain.
198
-
199
- ### Edge cases
200
-
201
- - **Widget null access**: Notification system callbacks reference `runtime.widget!.markFinished(id)`.
202
- This is safe because notifications only fire after agents complete, which is always after widget construction.
203
- The `!` assertion documents the invariant.
204
- - **currentCtx undefined**: `getCtx: () => runtime.currentCtx` behaves identically to the current `() => currentCtx` — the arrow closure captures the runtime object by reference and reads the field at call time.
205
- - **Backward compatibility during lift-and-shift**: During intermediate steps, `runAgent` falls back to module-scope state when `options.defaultMaxTurns` / `options.graceTurns` are absent, so the test suite stays green throughout.
206
-
207
- ## Module-Level Changes
208
-
209
- ### `src/runtime.ts` (new)
210
-
211
- - `SubagentRuntime` interface — all mutable state fields.
212
- - `RunConfig` interface — narrow config subset for `AgentManager`.
213
- - `createSubagentRuntime()` factory — returns a fresh runtime with defaults.
214
-
215
- ### `src/agent-runner.ts` (modified)
216
-
217
- - Add `defaultMaxTurns?: number` and `graceTurns?: number` to `RunOptions`.
218
- - Update `runAgent`'s maxTurns resolution to prefer `options.defaultMaxTurns` over module scope (step 2), then remove module scope entirely (step 6).
219
- - Update `graceTurns` usage in the turn-limit callback to prefer `options.graceTurns` over module scope (step 2), then remove fallback (step 6).
220
- - Remove `let defaultMaxTurns`, `let graceTurns`, `getDefaultMaxTurns`, `setDefaultMaxTurns`, `getGraceTurns`, `setGraceTurns` exports (step 6).
221
- - `normalizeMaxTurns` stays exported (pure function, no state dependency).
222
-
223
- ### `src/agent-manager.ts` (modified)
224
-
225
- - Add optional `getRunConfig?: () => RunConfig` parameter to constructor.
226
- - In `startAgent`, call `getRunConfig?.()` and pass `defaultMaxTurns` and `graceTurns` in the `RunOptions` object given to `runAgent`.
227
-
228
- ### `src/index.ts` (modified)
229
-
230
- - Import `createSubagentRuntime` from `./runtime.js`.
231
- - Create `const runtime = createSubagentRuntime()` at the top of the factory.
232
- - Replace `const agentActivity = new Map<>()` with `runtime.agentActivity`.
233
- - Replace `let widget: AgentWidget` with `runtime.widget`.
234
- - Replace `let currentCtx` with `runtime.currentCtx`.
235
- - Wire `applyAndEmitLoaded` appliers to `runtime.defaultMaxTurns` / `runtime.graceTurns` with normalization.
236
- - Pass `getRunConfig` to `AgentManager` constructor.
237
- - Update `snapshotSettings` to read from `runtime.defaultMaxTurns` / `runtime.graceTurns`.
238
- - Remove imports of `getDefaultMaxTurns`, `setDefaultMaxTurns`, `getGraceTurns`, `setGraceTurns` from `agent-runner.js`.
239
- - All arrow closures in notification, tool, menu, and service deps capture `runtime` by reference.
240
-
241
- ### `test/runtime.test.ts` (new)
242
-
243
- - Factory returns expected defaults.
244
- - Fields are independently mutable.
245
- - Multiple instances are isolated.
246
-
247
- ### `test/agent-runner-settings.test.ts` (modified → removed or substantially rewritten)
248
-
249
- - Current tests exercise `setDefaultMaxTurns` / `getDefaultMaxTurns` / `setGraceTurns` / `getGraceTurns` as module-scope getters/setters.
250
- - After step 6 removes those exports, these tests must migrate.
251
- - `normalizeMaxTurns` tests stay as-is (the function remains exported).
252
- - Setter-behavior tests (clamping, unlimited marker) become tests of the normalization logic applied in `index.ts` wire-up or `runtime.test.ts`.
253
- - The `runAgent` integration with `defaultMaxTurns` / `graceTurns` is tested via RunOptions in `agent-runner.test.ts`.
254
-
255
- ### `test/agent-manager.test.ts` (modified)
256
-
257
- - Constructor calls gain `getRunConfig` parameter (or omit it — default is no-op).
258
- - Existing tests pass `undefined` for `getRunConfig` (backward compatible).
259
- - New tests verify that `runAgent` receives `defaultMaxTurns` / `graceTurns` from `getRunConfig`.
260
-
261
- ## Test Impact Analysis
262
-
263
- ### New unit tests enabled by the extraction
264
-
265
- 1. `test/runtime.test.ts` — `createSubagentRuntime` factory returns correct defaults, fields are independently mutable, multiple instances don't share state.
266
- 2. `test/agent-runner.test.ts` additions — `runAgent` uses `options.defaultMaxTurns` and `options.graceTurns` when provided, with correct fallback behavior.
267
- 3. `test/agent-manager.test.ts` additions — `AgentManager` calls `getRunConfig()` and passes values in `RunOptions`.
268
-
269
- ### Existing tests that become redundant
270
-
271
- - `test/agent-runner-settings.test.ts` tests for `setDefaultMaxTurns` / `getDefaultMaxTurns` / `setGraceTurns` / `getGraceTurns` — these getter/setter pairs are removed.
272
- The normalization behavior they test is preserved via `normalizeMaxTurns` (which stays) and runtime wire-up tests.
273
-
274
- ### Existing tests that stay as-is
275
-
276
- - `test/settings.test.ts` — tests `SettingsAppliers` via mock callbacks; interface unchanged.
277
- - `test/service-adapter.test.ts` — tests `AdapterDeps` via mock callbacks; `getCtx` interface unchanged.
278
- - `test/agent-runner.test.ts` — existing final-output-capture and usage-callback tests are unaffected (they don't test maxTurns/graceTurns state).
279
- - All other test files (agent-types, custom-agents, notification, renderer, tools, UI, etc.) — no dependency on the moved state.
280
-
281
- ## TDD Order
282
-
283
- 1. **Create `src/runtime.ts` with SubagentRuntime interface and factory.**
284
- Write `test/runtime.test.ts` testing factory defaults and instance isolation.
285
- Commit: `feat: add SubagentRuntime interface and factory`
286
-
287
- 2. **Add `defaultMaxTurns` and `graceTurns` to RunOptions; update `runAgent` to prefer them over module scope.**
288
- In `agent-runner.ts`, add two optional fields to `RunOptions`.
289
- Change maxTurns resolution to `options.maxTurns ?? agentConfig?.maxTurns ?? options.defaultMaxTurns ?? defaultMaxTurns` (backward compatible — module-scope fallback retained).
290
- Change graceTurns usage to `options.graceTurns ?? graceTurns` (module-scope fallback retained).
291
- Add tests in `agent-runner.test.ts` verifying that when `options.defaultMaxTurns` / `options.graceTurns` are provided, they are used.
292
- Run `pnpm run check` to verify types.
293
- Commit: `feat: thread defaultMaxTurns and graceTurns through RunOptions`
294
-
295
- 3. **Wire `AgentManager` to pass runtime config in RunOptions.**
296
- Add `getRunConfig?: () => RunConfig` as the 5th constructor parameter (optional, backward compatible).
297
- In `startAgent`, call `getRunConfig?.()` and spread into the RunOptions for `runAgent`.
298
- Add agent-manager test verifying `runAgent` receives the config values.
299
- Existing tests omit the param — green with no changes.
300
- Commit: `refactor: agent-manager threads run config into RunOptions`
301
-
302
- 4. **Wire SubagentRuntime into index.ts — replace closure-scoped state.**
303
- Import `createSubagentRuntime` and call it at the top of the factory.
304
- Replace `const agentActivity`, `let widget`, and `let currentCtx` with runtime fields.
305
- Wire settings appliers to `runtime.defaultMaxTurns` (via `normalizeMaxTurns`) and `runtime.graceTurns` (via `Math.max(1, n)`).
306
- Pass `getRunConfig` callback to `AgentManager`.
307
- Update `snapshotSettings` to read from runtime.
308
- Remove imports of `getDefaultMaxTurns`, `setDefaultMaxTurns`, `getGraceTurns`, `setGraceTurns`.
309
- Run full test suite.
310
- Commit: `refactor: wire SubagentRuntime into extension factory`
311
-
312
- 5. **Remove module-scope state from `agent-runner.ts`.**
313
- Delete `let defaultMaxTurns`, `let graceTurns`, and all four getter/setter functions.
314
- Remove the module-scope fallback from `runAgent`'s resolution chain — `options.defaultMaxTurns` and `options.graceTurns` are now the sole source (with hardcoded defaults as a safety net: `undefined` and `5`).
315
- Update `test/agent-runner-settings.test.ts`: remove tests for deleted getters/setters, keep `normalizeMaxTurns` tests.
316
- Run `pnpm run check` and full test suite.
317
- Commit: `refactor: remove module-scope mutable state from agent-runner`
318
-
319
- 6. **Final cleanup and acceptance verification.**
320
- Verify acceptance criteria: `agent-runner.ts` contains no module-scope mutable state.
321
- `index.ts` contains no closure-scoped `let` variables that outlive their initialization block.
322
- `SubagentRuntime` interface exists with all mutable session state.
323
- Tests can construct a runtime and pass it to factories without importing `index.ts`.
324
- Full test suite passes.
325
- Remove any dead imports or vestigial code.
326
- Commit: `refactor: finalize SubagentRuntime migration (#69)`
327
-
328
- ## Risks and Mitigations
329
-
330
- | Risk | Mitigation |
331
- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
332
- | Backward-compatibility break during incremental migration — removing module-scope state before all consumers switch to RunOptions | Lift-and-shift: steps 2–3 introduce the new path alongside the old (module-scope fallback); step 5 removes the old path only after all consumers use the new path. |
333
- | AgentManager constructor gains a 5th positional parameter — fragile and hard to read | Parameter is optional with no default behavior change. Plan notes this as a follow-up cleanup (convert to options object). |
334
- | `runtime.widget!` non-null assertions in notification closures could NPE if initialization order changes | Assertion documents the invariant; widget is always constructed before any agent can complete. Add a defensive `if (!runtime.widget) return;` guard in the notification callbacks as a safety net. |
335
- | `normalizeMaxTurns` stays in `agent-runner.ts` after getter/setter removal — unclear ownership | `normalizeMaxTurns` is a pure function used by the turn-limit logic in `runAgent`. It belongs in the module that uses it. If a future refactor moves turn-limit logic, the function moves with it. |
336
- | Test file `agent-runner-settings.test.ts` needs substantial rewrite — risk of losing coverage | Keep `normalizeMaxTurns` tests intact (they test the same pure function). The setter/getter behavior tests are replaced by runtime factory tests and RunOptions integration tests that cover the same normalization logic. |
337
-
338
- ## Open Questions
339
-
340
- - Should `AgentManager`'s constructor be converted from positional parameters to a named-options object?
341
- This is natural cleanup but widens the blast radius.
342
- Defer to a follow-up issue if the 5th positional parameter feels too fragile during implementation.
343
- - Should `SubagentRuntime` include utility methods (e.g., `reset()`, `shutdown()`) for session lifecycle?
344
- The issue's acceptance criteria focus on state ownership, not lifecycle methods.
345
- Defer until a pattern of scattered resets emerges in practice.
@@ -1,306 +0,0 @@
1
- ---
2
- issue: 70
3
- issue_title: "refactor: extract event handlers from pi-subagents index.ts into src/handlers/"
4
- ---
5
-
6
- # Extract event handlers from index.ts
7
-
8
- ## Problem Statement
9
-
10
- After #54 and #69, `src/index.ts` is still ~281 lines.
11
- Four event handlers (`session_start`, `session_before_switch`, `session_shutdown`, `tool_execution_start`) are inline lambdas inside the factory closure.
12
- They cannot be tested in isolation because they reach into closure-scoped objects (`runtime`, `manager`, `notifications`, `pi`).
13
- `pi-permission-system` solved the same problem in #42 by extracting all handlers into `src/handlers/` as classes receiving narrow constructor-injected dependencies.
14
-
15
- ## Goals
16
-
17
- - Create `src/handlers/` with dedicated modules for each handler group.
18
- - Define handler classes with constructor-injected narrow interfaces that replace closure-captured state with explicit, testable contracts.
19
- - Reduce `src/index.ts` by moving handler bodies out — target ≤ 200 lines (the remaining bulk is tool/menu wiring, which is out of scope).
20
- - Enable unit testing of each handler module via mocked deps.
21
- - No behavior change — pure structural refactor.
22
-
23
- ## Non-Goals
24
-
25
- - Extracting tool registration or menu wiring from `index.ts` (separate concern; not in scope).
26
- - Changing the `SubagentsService` interface.
27
- - Refactoring `AgentManager` constructor signature.
28
- - Consolidating notification callbacks — the notification system's shape is unchanged.
29
-
30
- ## Background
31
-
32
- ### Current handler bodies in index.ts
33
-
34
- ```typescript
35
- // session_start (~3 lines)
36
- runtime.currentCtx = { pi, ctx };
37
- manager.clearCompleted();
38
-
39
- // session_before_switch (~1 line)
40
- manager.clearCompleted();
41
-
42
- // session_shutdown (~5 lines)
43
- unpublishSubagentsService();
44
- runtime.currentCtx = undefined;
45
- manager.abortAll();
46
- notifications.dispose();
47
- manager.dispose();
48
-
49
- // tool_execution_start (~2 lines)
50
- runtime.widget!.setUICtx(ctx.ui as UICtx);
51
- runtime.widget!.onTurnStart();
52
- ```
53
-
54
- ### Prior art: pi-permission-system src/handlers/
55
-
56
- Issue #42 extracted lifecycle, agent-prep, and permission-gate handlers into classes that receive narrow constructor deps.
57
- Each class exposes handler methods matching the Pi SDK event signatures.
58
- `index.ts` constructs the handler objects and wires `pi.on(event, handler.method)`.
59
-
60
- This plan follows the same class-based pattern.
61
- Although the individual handler bodies are small (1–5 lines), the lifecycle handlers share `runtime` and `manager` as collaborators — shared state that a class captures naturally via constructor injection rather than threading through each call.
62
-
63
- ### Prerequisite status
64
-
65
- - Issue #69 (SubagentRuntime) — **closed / implemented**.
66
- The runtime object exists in `src/runtime.ts` and is already wired into `index.ts`.
67
- - Issue #87 (evolve SubagentRuntime from data bag to object with methods) — **open / must land first**.
68
- Adds `setSessionContext()` / `clearSessionContext()` and widget delegation methods (`setUICtx()`, `onTurnStart()`, `markFinished()`, `updateWidget()`, `ensureTimer()`).
69
- Without #87, extracted handlers would just move the output-argument and LoD smells from `index.ts` into handler classes.
70
- With #87, handlers call methods on narrow runtime interfaces — no raw field writes, no `widget!` reach-throughs.
71
-
72
- ### Relevant constraints from AGENTS.md / code-style skill
73
-
74
- - Keep modules focused and composable (one concern per file).
75
- - Do not pass a shared dependency bag to functions that only use a subset — define narrow interfaces per consumer.
76
- - Keep Pi SDK imports out of business-logic modules — handler modules should accept lean local payload interfaces, not full SDK event types.
77
- - Prefer explicit configuration over hidden behavior.
78
-
79
- ## Design Overview
80
-
81
- ### Module layout
82
-
83
- ```text
84
- src/handlers/
85
- lifecycle.ts # session_start, session_before_switch, session_shutdown
86
- tool-start.ts # tool_execution_start
87
- index.ts # barrel re-export
88
- ```
89
-
90
- No shared `types.ts` file — each module defines its own narrow constructor interfaces.
91
- This follows the code-style guidance ("do not pass a shared dependency bag to functions that only use a subset") and matches the permission-system's prior art where each handler class takes its own narrow constructor deps.
92
-
93
- ### Narrow constructor interfaces
94
-
95
- Each class defines local interfaces for its collaborators, exposing only the methods the class actually calls.
96
- This keeps tests simple (mock only what's used) and decouples handlers from concrete types.
97
-
98
- #### SessionLifecycleHandler (in lifecycle.ts)
99
-
100
- ```typescript
101
- /** Narrow manager interface — only the methods lifecycle handlers call. */
102
- export interface LifecycleManager {
103
- clearCompleted(): void;
104
- abortAll(): void;
105
- dispose(): void;
106
- }
107
-
108
- /** Narrow runtime interface — only the methods lifecycle handlers call. */
109
- export interface LifecycleRuntime {
110
- setSessionContext(pi: unknown, ctx: unknown): void;
111
- clearSessionContext(): void;
112
- }
113
-
114
- export class SessionLifecycleHandler {
115
- constructor(
116
- private readonly pi: unknown,
117
- private readonly runtime: LifecycleRuntime,
118
- private readonly manager: LifecycleManager,
119
- private readonly disposeNotifications: () => void,
120
- private readonly unpublishService: () => void,
121
- ) {}
122
-
123
- handleSessionStart(_event: unknown, ctx: unknown): void { ... }
124
- handleSessionBeforeSwitch(): void { ... }
125
- async handleSessionShutdown(): Promise<void> { ... }
126
- }
127
- ```
128
-
129
- Five constructor params — `runtime` and `manager` are shared across all three methods (the key insight the plain-function design missed), while `disposeNotifications` and `unpublishService` are shutdown-only callbacks.
130
-
131
- `LifecycleRuntime` exposes methods, not mutable fields — the handler *tells* the runtime to set/clear session context instead of writing raw fields (the output-argument smell that #87 eliminates).
132
-
133
- #### ToolStartHandler (in tool-start.ts)
134
-
135
- ```typescript
136
- /** Narrow runtime interface — only the widget-delegation methods the handler calls. */
137
- export interface ToolStartRuntime {
138
- setUICtx(ctx: UICtx): void;
139
- onTurnStart(): void;
140
- }
141
-
142
- export class ToolStartHandler {
143
- constructor(
144
- private readonly runtime: ToolStartRuntime,
145
- ) {}
146
-
147
- handleToolExecutionStart(_event: unknown, ctx: ToolStartCtx): void { ... }
148
- }
149
- ```
150
-
151
- After #87, the runtime owns widget delegation — `runtime.setUICtx()` delegates to `this.widget?.setUICtx()` internally, handling the null check.
152
- The handler takes a narrow `ToolStartRuntime` interface (just the two methods it calls) and does not know about `widget` or `SubagentRuntime` at all.
153
-
154
- ### Event payload interfaces
155
-
156
- Following the code-style skill ("prefer lean local payload interfaces over full SDK event types"), each handler defines minimal payload types for the events it consumes.
157
-
158
- `session_start` receives `(event, ctx)` — but the current handler ignores the event payload entirely and only reads `ctx`.
159
- `tool_execution_start` receives `(event, ctx)` — the handler reads `ctx.ui`.
160
-
161
- Since handlers ignore event payloads, the method signatures use `_event: unknown`.
162
-
163
- ### index.ts wire-up
164
-
165
- After extraction, `index.ts` constructs handler instances and binds:
166
-
167
- ```typescript
168
- import { SessionLifecycleHandler } from "./handlers/index.js";
169
- import { ToolStartHandler } from "./handlers/index.js";
170
-
171
- const lifecycle = new SessionLifecycleHandler(
172
- pi,
173
- runtime,
174
- manager,
175
- () => notifications.dispose(),
176
- unpublishSubagentsService,
177
- );
178
-
179
- pi.on("session_start", (event, ctx) => lifecycle.handleSessionStart(event, ctx));
180
- pi.on("session_before_switch", () => lifecycle.handleSessionBeforeSwitch());
181
- pi.on("session_shutdown", () => lifecycle.handleSessionShutdown());
182
-
183
- const toolStart = new ToolStartHandler(runtime);
184
-
185
- pi.on("tool_execution_start", (event, ctx) => toolStart.handleToolExecutionStart(event, ctx));
186
- ```
187
-
188
- Handler instances are constructed once and hold their collaborators for the extension's lifetime — the same pattern as pi-permission-system's `SessionLifecycleHandler`.
189
- `runtime` satisfies both `LifecycleRuntime` and `ToolStartRuntime` structurally — TypeScript matches the narrow interface without an explicit cast.
190
-
191
- ### Edge cases
192
-
193
- - `session_shutdown` calls `unpublishSubagentsService()` which is a module-level import — the handler receives it as a constructor callback, keeping the handler SDK-free.
194
- - Constructor params are narrow interfaces (`LifecycleManager`, `LifecycleRuntime`, `ToolStartRuntime`) rather than concrete types (`AgentManager`, `SubagentRuntime`), so tests construct mocks without importing production classes.
195
- - Widget null safety: after #87, the runtime's delegation methods handle null internally (`this.widget?.setUICtx(ctx)`), so handlers never see the null case.
196
- `ToolStartHandler` tests mock the narrow `ToolStartRuntime` interface directly — no widget null logic to test in the handler.
197
-
198
- ## Module-Level Changes
199
-
200
- ### `src/handlers/lifecycle.ts` (new)
201
-
202
- - `LifecycleManager` interface (narrow: `clearCompleted`, `abortAll`, `dispose`).
203
- - `LifecycleRuntime` interface (narrow: `setSessionContext`, `clearSessionContext`).
204
- - `SessionLifecycleHandler` class — constructor takes `(pi, runtime, manager, disposeNotifications, unpublishService)`.
205
- - `handleSessionStart(_event, ctx)` — calls `this.runtime.setSessionContext(this.pi, ctx)`, calls `this.manager.clearCompleted()`.
206
- - `handleSessionBeforeSwitch()` — calls `this.manager.clearCompleted()`.
207
- - `handleSessionShutdown()` — calls `this.unpublishService()`, `this.runtime.clearSessionContext()`, `this.manager.abortAll()`, `this.disposeNotifications()`, `this.manager.dispose()`.
208
-
209
- ### `src/handlers/tool-start.ts` (new)
210
-
211
- - `ToolStartRuntime` interface (narrow: `setUICtx`, `onTurnStart`).
212
- - `ToolStartCtx` local interface (`{ ui: UICtx }`).
213
- - `ToolStartHandler` class — constructor takes `runtime: ToolStartRuntime`.
214
- - `handleToolExecutionStart(_event, ctx)` — calls `this.runtime.setUICtx(ctx.ui)`, `this.runtime.onTurnStart()`.
215
-
216
- ### `src/handlers/index.ts` (new)
217
-
218
- - Barrel re-export of handler classes and their narrow interfaces.
219
-
220
- ### `src/index.ts` (modified)
221
-
222
- - Add imports from `./handlers/index.js`.
223
- - Construct `SessionLifecycleHandler` with `(pi, runtime, manager, () => notifications.dispose(), unpublishSubagentsService)`.
224
- - Construct `ToolStartHandler` with `runtime`.
225
- - Replace inline `pi.on("session_start", ...)` lambda with `lifecycle.handleSessionStart` delegation.
226
- - Replace inline `pi.on("session_before_switch", ...)` lambda with `lifecycle.handleSessionBeforeSwitch` delegation.
227
- - Replace inline `pi.on("session_shutdown", ...)` lambda with `lifecycle.handleSessionShutdown` delegation.
228
- - Replace inline `pi.on("tool_execution_start", ...)` lambda with `toolStart.handleToolExecutionStart` delegation.
229
- - `unpublishSubagentsService` import stays — passed to the handler constructor.
230
-
231
- ### `test/handlers/lifecycle.test.ts` (new)
232
-
233
- - Construct `SessionLifecycleHandler` with mocked `LifecycleManager`, `LifecycleRuntime`, and stub callbacks.
234
- - `handleSessionStart`: verify `runtime.setSessionContext` called with `(pi, ctx)`, `manager.clearCompleted` called.
235
- - `handleSessionBeforeSwitch`: verify `manager.clearCompleted` called.
236
- - `handleSessionShutdown`: verify all five cleanup calls in correct order (unpublish → clearSessionContext → abortAll → disposeNotifications → dispose manager).
237
-
238
- ### `test/handlers/tool-start.test.ts` (new)
239
-
240
- - Construct `ToolStartHandler` with a mock `ToolStartRuntime`.
241
- - Verify `setUICtx` and `onTurnStart` called with correct arguments.
242
-
243
- ### `test/print-mode.test.ts` (unchanged)
244
-
245
- - This integration test calls `handlers.get("session_shutdown")` on the extension's registered handlers.
246
- The extraction is transparent — the Pi event registration is still in `index.ts`, just delegating to extracted functions.
247
- No changes needed.
248
-
249
- ## Test Impact Analysis
250
-
251
- ### New unit tests enabled by the extraction
252
-
253
- 1. `test/handlers/lifecycle.test.ts` — Each lifecycle handler tested in isolation with mocked deps.
254
- Previously impossible because handlers were inline lambdas with closure-captured `runtime`, `manager`, `notifications`, and `pi`.
255
- 2. `test/handlers/tool-start.test.ts` — `tool_execution_start` handler tested with mocked widget.
256
- Previously untestable because the handler was an inline lambda closing over `runtime.widget`.
257
- 3. Widget null-safety is not the handler's concern — the runtime handles it internally after #87.
258
-
259
- ### Existing tests that become redundant
260
-
261
- None.
262
- No existing tests directly test the event handler bodies — they were untestable inline lambdas.
263
- The extraction creates *new* coverage, not duplicate coverage.
264
-
265
- ### Existing tests that stay as-is
266
-
267
- - `test/print-mode.test.ts` — calls `session_shutdown` via the extension's registered handler map; still works because `index.ts` still registers the event, just delegates to extracted functions.
268
- - All other test files — no dependency on handler internals.
269
-
270
- ## TDD Order
271
-
272
- 1. **Create `src/handlers/lifecycle.ts` with `SessionLifecycleHandler` class.**
273
- Write `test/handlers/lifecycle.test.ts` constructing the handler with mocked narrow interfaces.
274
- Verify: `handleSessionStart` sets `currentCtx` and calls `clearCompleted`; `handleSessionBeforeSwitch` calls `clearCompleted`; `handleSessionShutdown` calls all five cleanup steps in order.
275
- Commit: `feat: add SessionLifecycleHandler`
276
-
277
- 2. **Create `src/handlers/tool-start.ts` with `ToolStartHandler` class.**
278
- Write `test/handlers/tool-start.test.ts` constructing the handler with a mock getter.
279
- Verify: calls `setUICtx` and `onTurnStart` on widget; no-op when widget is null.
280
- Commit: `feat: add ToolStartHandler`
281
-
282
- 3. **Create `src/handlers/index.ts` barrel and wire handlers into `src/index.ts`.**
283
- Add barrel re-export in `src/handlers/index.ts`.
284
- Replace inline lambdas in `src/index.ts` with handler instance method delegation.
285
- Construct `SessionLifecycleHandler` and `ToolStartHandler` in the factory.
286
- Run full test suite to verify no regressions.
287
- Run `pnpm run check` to verify types.
288
- Commit: `refactor: wire extracted handlers into extension factory (#70)`
289
-
290
- ## Risks and Mitigations
291
-
292
- | Risk | Mitigation |
293
- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
294
- | Handler extraction changes call order in `session_shutdown` | Test explicitly asserts call order; current order is preserved exactly. |
295
- | `SessionLifecycleHandler` has 5 constructor params | Reviewed against design-review: `runtime` and `manager` are shared across all three methods (the core shared-state argument for a class). The two callbacks (`disposeNotifications`, `unpublishService`) are shutdown-only but small enough to not warrant a second class. Acceptable. |
296
- | `print-mode.test.ts` integration test breaks | Test calls `session_shutdown` via the extension's handler map, not the extracted function directly. The delegation is transparent. Verified no changes needed. |
297
- | `UICtx` type import in `tool-start.ts` couples handler to widget module | `UICtx` is a lean interface already exported from `ui/agent-widget.ts`. The handler only references it in the `ToolStartCtx` local type. Acceptable coupling. |
298
-
299
- ## Open Questions
300
-
301
- - Should the `session_shutdown` handler call cleanup in a specific guaranteed order (e.g., unpublish → abort → dispose)?
302
- The current inline code uses a specific order; the extraction preserves it.
303
- If order matters for correctness, add a code comment documenting it.
304
- Decide during implementation.
305
- - Should the `ToolStartHandler` also handle `UICtx` type re-export, or should `tool-start.ts` import `UICtx` from `ui/agent-widget.ts` directly?
306
- Decide during implementation — if the handler defines its own `ToolStartCtx` with `{ ui: unknown }`, it avoids the import entirely.