@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.
- package/CHANGELOG.md +14 -0
- package/README.md +22 -31
- package/docs/architecture/architecture.md +157 -204
- package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
- package/package.json +5 -6
- package/src/lifecycle/subagent.ts +5 -9
- package/src/observation/notification.ts +23 -10
- package/src/observation/subagent-events-observer.ts +1 -3
- package/src/tools/get-result-report.ts +60 -0
- package/src/tools/get-result-tool.ts +25 -40
- package/src/types.ts +1 -1
- package/src/ui/session-navigator.ts +0 -2
- package/.prettierignore +0 -5
- package/AGENTS.md +0 -7
- package/docs/plans/0048-implement-subagents-api.md +0 -304
- package/docs/plans/0049-remove-group-join-output-file-rpc.md +0 -180
- package/docs/plans/0051-update-adr-0001-hard-fork.md +0 -77
- package/docs/plans/0052-remove-scheduled-subagents.md +0 -133
- package/docs/plans/0053-extract-model-resolution-from-execute.md +0 -181
- package/docs/plans/0054-decompose-index-into-modules.md +0 -302
- package/docs/plans/0057-structured-debug-logging.md +0 -154
- package/docs/plans/0061-session-format-transcript.md +0 -284
- package/docs/plans/0066-replace-as-any-with-sdk-types.md +0 -215
- package/docs/plans/0069-create-subagent-runtime.md +0 -345
- package/docs/plans/0070-extract-event-handlers.md +0 -306
- package/docs/plans/0071-extract-session-config-assembler.md +0 -362
- package/docs/plans/0072-inject-agent-manager-collaborators.md +0 -329
- package/docs/plans/0076-inject-cwd-into-agent-manager.md +0 -102
- package/docs/plans/0077-inject-project-agents-dir.md +0 -115
- package/docs/plans/0080-consolidate-agent-config-lookup.md +0 -247
- package/docs/plans/0084-extract-git-worktree-manager.md +0 -142
- package/docs/plans/0087-evolve-subagent-runtime-methods.md +0 -240
- package/docs/plans/0098-extract-agent-record-state-machine.md +0 -435
- package/docs/plans/0099-replace-ctx-with-parent-snapshot.md +0 -488
- package/docs/plans/0100-replace-callback-threading-with-session-subscription.md +0 -454
- package/docs/plans/0102-consolidate-test-record-factory.md +0 -176
- package/docs/plans/0108-extract-agent-type-registry.md +0 -322
- package/docs/plans/0109-extract-settings-manager.md +0 -276
- package/docs/plans/0110-agent-activity-tracker.md +0 -297
- package/docs/plans/0111-split-agent-record-lifecycle.md +0 -582
- package/docs/plans/0112-replace-agent-manager-callbacks.md +0 -241
- package/docs/plans/0113-disambiguate-spawn-options.md +0 -155
- package/docs/plans/0114-narrow-agent-tool-menu-deps.md +0 -279
- package/docs/plans/0115-decompose-agent-tool.md +0 -337
- package/docs/plans/0116-type-housekeeping.md +0 -351
- package/docs/plans/0118-settings-manager-apply-methods.md +0 -271
- package/docs/plans/0123-remove-vi-fn-cast-smell.md +0 -179
- package/docs/plans/0131-consolidate-shared-test-fixtures.md +0 -207
- package/docs/plans/0132-inject-io-into-session-config.md +0 -219
- package/docs/plans/0133-inject-sdk-boundary-into-agent-runner.md +0 -373
- package/docs/plans/0134-reduce-as-any-casts.md +0 -366
- package/docs/plans/0135-extract-display-helpers.md +0 -182
- package/docs/plans/0136-decompose-agent-menu.md +0 -300
- package/docs/plans/0144-consolidate-observation-model.md +0 -263
- package/docs/plans/0145-decompose-execute-push-ctx-to-boundary.md +0 -290
- package/docs/plans/0146-narrow-ui-context.md +0 -319
- package/docs/plans/0147-inject-wrap-text-into-conversation-viewer.md +0 -166
- package/docs/plans/0148-split-agent-widget-rendering.md +0 -255
- package/docs/plans/0152-add-prompt-snippet.md +0 -91
- package/docs/plans/0164-reorganize-into-domain-directories.md +0 -409
- package/docs/plans/0165-decompose-resolved-spawn-config.md +0 -157
- package/docs/plans/0166-extract-parent-session-info.md +0 -231
- package/docs/plans/0167-narrow-runner-io.md +0 -150
- package/docs/plans/0168-extract-tool-filter-config.md +0 -173
- package/docs/plans/0169-extract-run-context-from-run-options.md +0 -194
- package/docs/plans/0170-reduce-build-content-lines-complexity.md +0 -225
- package/docs/plans/0171-reduce-render-result-complexity.md +0 -200
- package/docs/plans/0172-extract-turn-formatting.md +0 -206
- package/docs/plans/0180-reorder-append-prompt-for-kv-cache.md +0 -100
- package/docs/plans/0185-remove-persistent-agent-memory.md +0 -161
- package/docs/plans/0188-replace-any-casts-with-sdk-types.md +0 -162
- package/docs/plans/0192-define-session-context-interface.md +0 -107
- package/docs/plans/0193-runtime-owns-context-queries.md +0 -245
- package/docs/plans/0194-align-tool-interfaces-for-structural-typing.md +0 -139
- package/docs/plans/0195-convert-tool-factories-to-classes.md +0 -264
- package/docs/plans/0196-convert-runner-menu-to-classes.md +0 -268
- package/docs/plans/0205-decompose-render-widget-lines.md +0 -140
- package/docs/plans/0206-decompose-show-agent-detail.md +0 -211
- package/docs/plans/0207-decompose-agent-widget-update.md +0 -242
- package/docs/plans/0208-extract-shared-test-fixtures.md +0 -298
- package/docs/plans/0214-convert-remaining-closure-factories-to-classes.md +0 -261
- package/docs/plans/0215-decompose-build-parent-context.md +0 -166
- package/docs/plans/0216-decompose-start-agent.md +0 -255
- package/docs/plans/0217-extract-overwrite-guard.md +0 -176
- package/docs/plans/0218-push-sdk-boundary-in-settings.md +0 -172
- package/docs/plans/0219-reduce-test-duplication-top-3-clone-families.md +0 -162
- package/docs/plans/0227-evolve-agent-record-into-agent.md +0 -322
- package/docs/plans/0228-async-start-agent-dissolve-run-handle.md +0 -288
- package/docs/plans/0229-agent-born-complete.md +0 -564
- package/docs/plans/0230-extract-concurrency-queue.md +0 -265
- package/docs/plans/0231-push-exec-registry-to-runner.md +0 -245
- package/docs/plans/0232-agent-resume-internal-observer-lifecycle.md +0 -180
- package/docs/plans/0237-remove-disallowed-tools.md +0 -180
- package/docs/plans/0238-remove-extensions-filtering.md +0 -191
- package/docs/plans/0239-collapse-filter-active-tools.md +0 -217
- package/docs/plans/0242-rename-agent-tool-to-subagent.md +0 -165
- package/docs/plans/0256-extract-worktree-isolation.md +0 -256
- package/docs/plans/0257-extract-child-session-factory.md +0 -285
- package/docs/plans/0262-add-workspace-provider-seam.md +0 -264
- package/docs/plans/0264-remove-extension-lifecycle-control.md +0 -277
- package/docs/plans/0265-born-complete-subagent-session.md +0 -333
- package/docs/plans/0270-type-consumable-public-surface.md +0 -204
- package/docs/plans/0272-export-workspace-collaborator-types.md +0 -147
- package/docs/plans/0277-encapsulate-agent-session.md +0 -304
- package/docs/plans/0280-rename-agent-to-subagent.md +0 -197
- package/docs/plans/0360-custom-agents-default-append.md +0 -132
- package/docs/plans/0373-extract-subagent-state.md +0 -250
- package/docs/plans/0374-encapsulate-subagent-start-notification.md +0 -268
- package/docs/plans/0375-extract-run-listener-workspace-bracket.md +0 -300
- package/docs/plans/0376-extract-manager-observer.md +0 -232
- package/docs/plans/0377-split-widget-delegation-from-runtime.md +0 -254
- package/docs/plans/0378-consolidate-lifecycle-test-fixtures.md +0 -243
- package/docs/plans/0379-consolidate-ui-tools-test-fixtures.md +0 -335
- package/docs/plans/0380-extract-layered-settings-loader.md +0 -223
- package/docs/plans/0381-replace-concurrency-queue-with-limiter.md +0 -267
- package/docs/plans/0400-include-parent-prompt-in-replace-mode.md +0 -199
- package/docs/plans/0403-abort-subagents-on-interrupt.md +0 -180
- package/docs/plans/0412-unify-session-mock-builders.md +0 -146
- package/docs/plans/0420-fold-run-metrics-live-activity-onto-record.md +0 -228
- package/docs/plans/0421-migrate-activity-readers-to-record-getters.md +0 -309
- package/docs/plans/0422-delete-activity-tracker-ui-observer.md +0 -204
- package/docs/plans/0423-widget-self-drive-from-lifecycle.md +0 -264
- package/docs/plans/0424-drop-widget-dep-from-subagent-tool.md +0 -166
- package/docs/plans/0425-reconcile-subagent-events-contract.md +0 -144
- package/docs/plans/0426-consolidate-residual-test-clone-families.md +0 -235
- package/docs/plans/0427-reconsider-ui-direction.md +0 -212
- package/docs/plans/0441-remove-agent-definition-management-subtree.md +0 -180
- package/docs/plans/0442-dissolve-agents-conversation-viewer.md +0 -195
- package/docs/plans/0444-shrink-widget-to-background-agents.md +0 -156
- package/docs/plans/0445-native-session-navigation.md +0 -365
- package/docs/plans/0446-spike-session-navigation-entry-criteria.md +0 -171
- package/docs/plans/0447-extract-subagents-settings-command.md +0 -220
- package/docs/plans/0448-block-disabled-agent-spawn.md +0 -207
- package/docs/plans/0462-navigation-renderer-tui-components.md +0 -338
- package/docs/plans/0463-file-snapshot-source-evicted-agents.md +0 -306
- package/docs/retro/0048-implement-subagents-api.md +0 -44
- package/docs/retro/0049-remove-group-join-output-file-rpc.md +0 -49
- package/docs/retro/0051-update-adr-0001-hard-fork.md +0 -39
- package/docs/retro/0053-extract-model-resolution-from-execute.md +0 -40
- package/docs/retro/0054-decompose-index-into-modules.md +0 -53
- package/docs/retro/0057-structured-debug-logging.md +0 -77
- package/docs/retro/0061-session-format-transcript.md +0 -41
- package/docs/retro/0066-replace-as-any-with-sdk-types.md +0 -44
- package/docs/retro/0069-create-subagent-runtime.md +0 -43
- package/docs/retro/0070-extract-event-handlers.md +0 -49
- package/docs/retro/0071-extract-session-config-assembler.md +0 -60
- package/docs/retro/0072-inject-agent-manager-collaborators.md +0 -46
- package/docs/retro/0076-inject-cwd-into-agent-manager.md +0 -35
- package/docs/retro/0077-inject-project-agents-dir.md +0 -33
- package/docs/retro/0080-consolidate-agent-config-lookup.md +0 -35
- package/docs/retro/0084-extract-git-worktree-manager.md +0 -37
- package/docs/retro/0087-evolve-subagent-runtime-methods.md +0 -42
- package/docs/retro/0098-extract-agent-record-state-machine.md +0 -46
- package/docs/retro/0099-replace-ctx-with-parent-snapshot.md +0 -37
- package/docs/retro/0100-replace-callback-threading-with-session-subscription.md +0 -36
- package/docs/retro/0102-consolidate-test-record-factory.md +0 -30
- package/docs/retro/0108-extract-agent-type-registry.md +0 -41
- package/docs/retro/0109-extract-settings-manager.md +0 -55
- package/docs/retro/0110-agent-activity-tracker.md +0 -44
- package/docs/retro/0111-split-agent-record-lifecycle.md +0 -61
- package/docs/retro/0112-replace-agent-manager-callbacks.md +0 -35
- package/docs/retro/0113-disambiguate-spawn-options.md +0 -29
- package/docs/retro/0114-narrow-agent-tool-menu-deps.md +0 -38
- package/docs/retro/0115-decompose-agent-tool.md +0 -51
- package/docs/retro/0116-type-housekeeping.md +0 -42
- package/docs/retro/0118-settings-manager-apply-methods.md +0 -40
- package/docs/retro/0123-remove-vi-fn-cast-smell.md +0 -49
- package/docs/retro/0131-consolidate-shared-test-fixtures.md +0 -46
- package/docs/retro/0132-inject-io-into-session-config.md +0 -33
- package/docs/retro/0133-inject-sdk-boundary-into-agent-runner.md +0 -45
- package/docs/retro/0134-reduce-as-any-casts.md +0 -56
- package/docs/retro/0135-extract-display-helpers.md +0 -38
- package/docs/retro/0136-decompose-agent-menu.md +0 -43
- package/docs/retro/0144-consolidate-observation-model.md +0 -39
- package/docs/retro/0145-decompose-execute-push-ctx-to-boundary.md +0 -56
- package/docs/retro/0146-narrow-ui-context.md +0 -70
- package/docs/retro/0147-inject-wrap-text-into-conversation-viewer.md +0 -90
- package/docs/retro/0148-split-agent-widget-rendering.md +0 -39
- package/docs/retro/0152-add-prompt-snippet.md +0 -34
- package/docs/retro/0164-reorganize-into-domain-directories.md +0 -82
- package/docs/retro/0165-decompose-resolved-spawn-config.md +0 -78
- package/docs/retro/0166-extract-parent-session-info.md +0 -66
- package/docs/retro/0167-narrow-runner-io.md +0 -63
- package/docs/retro/0168-extract-tool-filter-config.md +0 -76
- package/docs/retro/0169-extract-run-context-from-run-options.md +0 -68
- package/docs/retro/0170-reduce-build-content-lines-complexity.md +0 -72
- package/docs/retro/0171-reduce-render-result-complexity.md +0 -77
- package/docs/retro/0172-extract-turn-formatting.md +0 -80
- package/docs/retro/0180-reorder-append-prompt-for-kv-cache.md +0 -62
- package/docs/retro/0185-remove-persistent-agent-memory.md +0 -73
- package/docs/retro/0188-replace-any-casts-with-sdk-types.md +0 -69
- package/docs/retro/0192-define-session-context-interface.md +0 -59
- package/docs/retro/0193-runtime-owns-context-queries.md +0 -75
- package/docs/retro/0194-align-tool-interfaces-for-structural-typing.md +0 -62
- package/docs/retro/0195-convert-tool-factories-to-classes.md +0 -84
- package/docs/retro/0196-convert-runner-menu-to-classes.md +0 -73
- package/docs/retro/0205-decompose-render-widget-lines.md +0 -64
- package/docs/retro/0206-decompose-show-agent-detail.md +0 -67
- package/docs/retro/0207-decompose-agent-widget-update.md +0 -92
- package/docs/retro/0208-extract-shared-test-fixtures.md +0 -102
- package/docs/retro/0214-convert-remaining-closure-factories-to-classes.md +0 -66
- package/docs/retro/0215-decompose-build-parent-context.md +0 -61
- package/docs/retro/0216-decompose-start-agent.md +0 -80
- package/docs/retro/0217-extract-overwrite-guard.md +0 -63
- package/docs/retro/0218-push-sdk-boundary-in-settings.md +0 -72
- package/docs/retro/0219-reduce-test-duplication-top-3-clone-families.md +0 -64
- package/docs/retro/0227-evolve-agent-record-into-agent.md +0 -80
- package/docs/retro/0228-async-start-agent-dissolve-run-handle.md +0 -80
- package/docs/retro/0229-agent-born-complete.md +0 -89
- package/docs/retro/0230-extract-concurrency-queue.md +0 -38
- package/docs/retro/0231-push-exec-registry-to-runner.md +0 -71
- package/docs/retro/0232-agent-resume-internal-observer-lifecycle.md +0 -109
- package/docs/retro/0237-remove-disallowed-tools.md +0 -78
- package/docs/retro/0238-remove-extensions-filtering.md +0 -72
- package/docs/retro/0239-collapse-filter-active-tools.md +0 -70
- package/docs/retro/0242-rename-agent-tool-to-subagent.md +0 -82
- package/docs/retro/0256-extract-worktree-isolation.md +0 -89
- package/docs/retro/0257-extract-child-session-factory.md +0 -33
- package/docs/retro/0262-add-workspace-provider-seam.md +0 -89
- package/docs/retro/0264-remove-extension-lifecycle-control.md +0 -91
- package/docs/retro/0265-born-complete-subagent-session.md +0 -97
- package/docs/retro/0270-type-consumable-public-surface.md +0 -108
- package/docs/retro/0272-export-workspace-collaborator-types.md +0 -38
- package/docs/retro/0277-encapsulate-agent-session.md +0 -80
- package/docs/retro/0280-rename-agent-to-subagent.md +0 -96
- package/docs/retro/0360-custom-agents-default-append.md +0 -81
- package/docs/retro/0373-extract-subagent-state.md +0 -94
- package/docs/retro/0374-encapsulate-subagent-start-notification.md +0 -171
- package/docs/retro/0375-extract-run-listener-workspace-bracket.md +0 -104
- package/docs/retro/0376-extract-manager-observer.md +0 -81
- package/docs/retro/0377-split-widget-delegation-from-runtime.md +0 -105
- package/docs/retro/0378-consolidate-lifecycle-test-fixtures.md +0 -100
- package/docs/retro/0379-consolidate-ui-tools-test-fixtures.md +0 -52
- package/docs/retro/0380-extract-layered-settings-loader.md +0 -97
- package/docs/retro/0381-replace-concurrency-queue-with-limiter.md +0 -95
- package/docs/retro/0400-include-parent-prompt-in-replace-mode.md +0 -84
- package/docs/retro/0403-abort-subagents-on-interrupt.md +0 -90
- package/docs/retro/0412-unify-session-mock-builders.md +0 -80
- package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +0 -90
- package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +0 -105
- package/docs/retro/0422-delete-activity-tracker-ui-observer.md +0 -97
- package/docs/retro/0423-widget-self-drive-from-lifecycle.md +0 -112
- package/docs/retro/0424-drop-widget-dep-from-subagent-tool.md +0 -89
- package/docs/retro/0425-reconcile-subagent-events-contract.md +0 -101
- package/docs/retro/0426-consolidate-residual-test-clone-families.md +0 -100
- package/docs/retro/0427-reconsider-ui-direction.md +0 -102
- package/docs/retro/0441-remove-agent-definition-management-subtree.md +0 -45
- package/docs/retro/0442-dissolve-agents-conversation-viewer.md +0 -108
- package/docs/retro/0444-shrink-widget-to-background-agents.md +0 -81
- package/docs/retro/0445-native-session-navigation.md +0 -122
- package/docs/retro/0446-spike-session-navigation-entry-criteria.md +0 -99
- package/docs/retro/0447-extract-subagents-settings-command.md +0 -91
- package/docs/retro/0448-block-disabled-agent-spawn.md +0 -75
- package/docs/retro/0462-navigation-renderer-tui-components.md +0 -105
- package/docs/retro/0463-file-snapshot-source-evicted-agents.md +0 -103
- package/src/observation/notification-state.ts +0 -27
- 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
|