@gotgenes/pi-subagents 18.0.1 → 18.0.3
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 +15 -0
- package/docs/architecture/architecture.md +165 -203
- package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
- package/package.json +6 -7
- package/src/config/default-agents.ts +3 -0
- package/src/lifecycle/subagent.ts +25 -14
- package/src/observation/notification.ts +23 -10
- package/src/observation/subagent-events-observer.ts +1 -3
- package/src/runtime.ts +2 -1
- package/src/service/service-adapter.ts +20 -17
- package/src/session/model-resolver.ts +31 -30
- package/src/tools/agent-tool.ts +17 -13
- package/src/tools/get-result-report.ts +60 -0
- package/src/tools/get-result-tool.ts +25 -40
- package/src/tools/helpers.ts +21 -5
- package/src/tools/spawn-config.ts +5 -5
- package/src/tools/steer-tool.ts +37 -27
- package/src/types.ts +4 -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/0443-consolidate-remaining-test-clone-families.md +0 -268
- 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/plans/0470-refresh-readme-command-surface.md +0 -159
- 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 -97
- package/docs/retro/0442-dissolve-agents-conversation-viewer.md +0 -108
- package/docs/retro/0443-consolidate-remaining-test-clone-families.md +0 -28
- 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/docs/retro/0470-refresh-readme-command-surface.md +0 -40
- package/src/observation/notification-state.ts +0 -27
- package/vitest.config.ts +0 -14
|
@@ -1,267 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
issue: 381
|
|
3
|
-
issue_title: "Replace ConcurrencyQueue with a thunk-based ConcurrencyLimiter"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Replace ConcurrencyQueue with a thunk-based ConcurrencyLimiter
|
|
7
|
-
|
|
8
|
-
## Problem Statement
|
|
9
|
-
|
|
10
|
-
The `ConcurrencyQueue` stores background-agent IDs and decides *when* to start them, but it cannot start an agent itself.
|
|
11
|
-
It compensates with a `startAgent(id)` callback that reaches back into the manager (`getRecord(id)`, status check, `run()`) — a dependency back-edge that forces forward-referenced bindings in both `index.ts` and the manager test helper.
|
|
12
|
-
The queue also keeps its own `running` counter, fed by `markStarted`/`markFinished` relays in the manager's observer, duplicating state the agents already carry.
|
|
13
|
-
A queued agent has `promise === undefined` until the queue starts it, which is the direct cause of `waitForAll`'s `while (true)` drain loop and its `eslint-disable`.
|
|
14
|
-
|
|
15
|
-
These are three symptoms of one root cause: the queue schedules *identifiers it cannot act on* instead of *work it can run*.
|
|
16
|
-
Scheduling thunks (`() => Promise<void>`) instead of IDs dissolves all three at the source.
|
|
17
|
-
|
|
18
|
-
This is Phase 17 Step 1 (core consolidation), recorded in `docs/architecture/architecture.md` under "Improvement roadmap (Phase 17 — core consolidation)".
|
|
19
|
-
It unblocks Phase 17 Step 3 ([#374], run-start encapsulation).
|
|
20
|
-
|
|
21
|
-
## Goals
|
|
22
|
-
|
|
23
|
-
- Replace `ConcurrencyQueue` (ID registry + back-edge callback) with a `ConcurrencyLimiter` that schedules run closures FIFO against a dynamic limit and knows nothing about agents, IDs, or the manager.
|
|
24
|
-
- Make the dependency direction strictly `SubagentManager → ConcurrencyLimiter`: no callback back-edge, no forward-referenced bindings.
|
|
25
|
-
- Derive the active count from the limiter's own task lifecycle (increment on task start, decrement on settle); delete the observer's `markStarted`/`markFinished` relays.
|
|
26
|
-
- Give every spawned agent a real `promise` at spawn time, collapsing `waitForAll`'s `while (true)` drain loop and its `eslint-disable`.
|
|
27
|
-
- This is a non-breaking internal refactor: the FIFO admission behavior against `maxConcurrent` is preserved, and no public API, config key, or observable behavior changes.
|
|
28
|
-
|
|
29
|
-
## Non-Goals
|
|
30
|
-
|
|
31
|
-
- Renaming the `bypassQueue` spawn option.
|
|
32
|
-
It is part of the published `SubagentsService` type surface (`src/service/service.ts`), so renaming it would churn the type bundle and break consumers — out of scope; track in Open Questions.
|
|
33
|
-
- Folding the queued-status guard into `Subagent.start()` — that is Phase 17 Step 3 ([#374]).
|
|
34
|
-
This plan keeps the guard inside the scheduled thunk.
|
|
35
|
-
- Extracting `SubagentState` or making execution deps mandatory ([#373], Step 2).
|
|
36
|
-
- Any change to foreground execution (`spawnAndWait`) or to `bypassQueue` runs — both continue to invoke `record.run()` directly, never touching the limiter.
|
|
37
|
-
- Touching `src/service/service.ts` or `src/service/service-adapter.ts` — `bypassQueue` flows through unchanged.
|
|
38
|
-
|
|
39
|
-
## Background
|
|
40
|
-
|
|
41
|
-
Relevant modules:
|
|
42
|
-
|
|
43
|
-
- `src/lifecycle/concurrency-queue.ts` — the current `ConcurrencyQueue`: `isFull`, `enqueue`, `dequeue`, `markStarted`, `markFinished`, `drain`, `clear`, `queuedIds`.
|
|
44
|
-
Stores IDs; `drain()` calls the injected `startAgent(id)` back-edge.
|
|
45
|
-
- `src/lifecycle/subagent-manager.ts` — injects the queue via `SubagentManagerOptions.queue`.
|
|
46
|
-
`buildObserver` relays `markStarted`/`markFinished`; `spawn` enqueues when `isFull()`; `abort` calls `dequeue`; `abortAll` iterates `queuedIds` + `clear()`; `waitForAll` loops `drain()` + `Promise.allSettled`; `dispose` calls `clear()`.
|
|
47
|
-
- `src/index.ts` — constructs the queue with a `startAgent` callback that forward-references the manager (`manager.getRecord(id)` then `agent.run()`); wires `settings.onMaxConcurrentChanged` to `queue.drain()`.
|
|
48
|
-
- `src/lifecycle/subagent.ts` — `run()` sets status to `running` synchronously (`markRunning`) before its first `await`; `run()` always resolves (errors captured internally).
|
|
49
|
-
`abort()` acts only on `running` agents; its docstring references `ConcurrencyQueue.dequeue()`.
|
|
50
|
-
- `test/lifecycle/subagent-manager.test.ts` — `createManager` helper replicates the `index.ts` start callback with a `prefer-const` `eslint-disable` for the forward reference.
|
|
51
|
-
- `test/lifecycle/concurrency-queue.test.ts` — unit tests for the queue (drain ordering, `markStarted`/`markFinished` counting, `enqueue`/`dequeue`).
|
|
52
|
-
|
|
53
|
-
Constraints from AGENTS.md and skills:
|
|
54
|
-
|
|
55
|
-
- ES2024 `Promise.withResolvers` is available and preferred (`code-design` skill).
|
|
56
|
-
- The `bypassQueue` field lives in the public type bundle (`exports`, `verify:public-types`); renaming public surface is breaking (`package-pi-subagents` skill).
|
|
57
|
-
- `@typescript-eslint/require-await` is enabled for `src/`; a thunk with no `await` must return a `Promise` without `async`.
|
|
58
|
-
- Where the old `drain()` used `while (… && !isFull())` with `this.queue.shift()!`, prefer a bounded loop without a non-null assertion (`code-design` Biome/ESLint notes).
|
|
59
|
-
|
|
60
|
-
The current observer-relay path (`buildObserver` → `queue.markStarted`/`markFinished`) confirmed: the queue's `running` counter mirrors the per-agent status the manager already tracks (the manager filters on `status === "running" || "queued"` in `cleanup`, `clearCompleted`, `hasRunning`, `waitForAll`).
|
|
61
|
-
No production caller awaits a *queued* agent's promise (`get-result-tool.ts` guards on `status === "running"`; `spawnAndWait` is foreground; `waitForAll` filters by status), so giving queued agents a settled-on-completion promise is safe.
|
|
62
|
-
|
|
63
|
-
## Design Overview
|
|
64
|
-
|
|
65
|
-
### `ConcurrencyLimiter`
|
|
66
|
-
|
|
67
|
-
A pure FIFO scheduler over thunks.
|
|
68
|
-
It owns the active count and the pending queue; it has no knowledge of agents, IDs, or the manager.
|
|
69
|
-
|
|
70
|
-
```typescript
|
|
71
|
-
export class ConcurrencyLimiter {
|
|
72
|
-
private active = 0;
|
|
73
|
-
private readonly pending: Array<{ start: () => void; settle: () => void }> = [];
|
|
74
|
-
|
|
75
|
-
constructor(private readonly getLimit: () => number) {}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Schedule a task to run FIFO once a slot is free.
|
|
79
|
-
* The returned promise always settles: it follows the task's settlement when
|
|
80
|
-
* the task runs, or resolves early if clear() drops it before it starts.
|
|
81
|
-
*/
|
|
82
|
-
schedule(task: () => Promise<void>): Promise<void> {
|
|
83
|
-
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
|
84
|
-
this.pending.push({
|
|
85
|
-
start: () => {
|
|
86
|
-
this.active++;
|
|
87
|
-
task().then(resolve, reject).finally(() => {
|
|
88
|
-
this.active--;
|
|
89
|
-
this.recheck();
|
|
90
|
-
});
|
|
91
|
-
},
|
|
92
|
-
settle: resolve,
|
|
93
|
-
});
|
|
94
|
-
this.recheck();
|
|
95
|
-
return promise;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Start pending tasks until the limit is reached. Call when the limit may have grown. */
|
|
99
|
-
recheck(): void {
|
|
100
|
-
while (this.active < this.getLimit()) {
|
|
101
|
-
const next = this.pending.shift();
|
|
102
|
-
if (!next) break;
|
|
103
|
-
next.start();
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/** Drop all pending tasks, resolving their promises without running them. */
|
|
108
|
-
clear(): void {
|
|
109
|
-
const dropped = this.pending.splice(0);
|
|
110
|
-
for (const task of dropped) task.settle();
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
Design decisions:
|
|
116
|
-
|
|
117
|
-
- **Active count derived from task lifecycle.**
|
|
118
|
-
`active++` happens synchronously inside `start()` before the task's first `await`; `active--` runs in `finally`.
|
|
119
|
-
This replaces the queue's `running` counter and the two observer relays.
|
|
120
|
-
- **`recheck()` is bounded.**
|
|
121
|
-
The loop terminates when the limit is reached or the pending queue empties — no `while (true)`, no `this.pending.shift()!` non-null assertion.
|
|
122
|
-
- **`clear()` settles dropped promises.**
|
|
123
|
-
Every `schedule()` promise becomes `record.promise`; the contract is that it always settles.
|
|
124
|
-
Dropping a thunk without resolving would leave a forever-pending `record.promise`.
|
|
125
|
-
`clear()` resolves dropped tasks so `dispose()`/`abortAll()` cannot strand a promise. (This is a few lines beyond the issue's "~40 lines" sketch; the extra `settle` handle is the deliberate cost of that invariant.)
|
|
126
|
-
- **Synchronous start.**
|
|
127
|
-
When a slot is free, `schedule()` runs the thunk synchronously inside `recheck()`, so `record.run()` executes its synchronous prefix (`markRunning`) immediately — preserving today's behavior where `record.promise = record.run()` flips status to `running` at once.
|
|
128
|
-
|
|
129
|
-
### Manager spawn call site
|
|
130
|
-
|
|
131
|
-
```typescript
|
|
132
|
-
// spawn(), background and not bypassQueue:
|
|
133
|
-
record.promise = this.limiter.schedule(() => {
|
|
134
|
-
// Guard: an abort-while-queued task is a no-op (Step 3 folds this into Subagent.start()).
|
|
135
|
-
if (record.status !== "queued") return Promise.resolve();
|
|
136
|
-
return record.run();
|
|
137
|
-
});
|
|
138
|
-
// foreground or bypassQueue:
|
|
139
|
-
record.promise = record.run();
|
|
140
|
-
```
|
|
141
|
-
|
|
142
|
-
This is Tell-Don't-Ask toward the limiter: the manager hands it work, the limiter decides timing.
|
|
143
|
-
The status guard replaces `dequeue` — an aborted queued agent (status `stopped`) becomes a no-op when its slot finally opens.
|
|
144
|
-
|
|
145
|
-
### Manager lifecycle methods
|
|
146
|
-
|
|
147
|
-
- `buildObserver` — drop the `markStarted` (in `onStarted`) and `markFinished` (in `onRunFinished`) relays; `onRunFinished` keeps the background `onSubagentCompleted` dispatch.
|
|
148
|
-
- `abort(id)` — for a `queued` agent, just `record.markStopped()` (no `dequeue`); otherwise `record.abort()`.
|
|
149
|
-
- `abortAll()` — iterate agents: `markStopped()` each `queued` agent (count it), else `record.abort()`; then `this.limiter.clear()` to drop pending thunks (their promises resolve).
|
|
150
|
-
- `waitForAll()` — every spawned agent has a `promise`, so the manual `drain()` loop collapses:
|
|
151
|
-
|
|
152
|
-
```typescript
|
|
153
|
-
async waitForAll(): Promise<void> {
|
|
154
|
-
let pending = this.pendingPromises();
|
|
155
|
-
while (pending.length > 0) {
|
|
156
|
-
await Promise.allSettled(pending);
|
|
157
|
-
pending = this.pendingPromises();
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
private pendingPromises(): Promise<void>[] {
|
|
162
|
-
return [...this.agents.values()]
|
|
163
|
-
.filter(r => r.status === "running" || r.status === "queued")
|
|
164
|
-
.map(r => r.promise)
|
|
165
|
-
.filter((p): p is Promise<void> => p != null);
|
|
166
|
-
}
|
|
167
|
-
```
|
|
168
|
-
|
|
169
|
-
The re-check loop is no longer `while (true)` and no longer drives scheduling — the limiter auto-starts queued agents as slots free, so a single `allSettled` covers the queued case.
|
|
170
|
-
The loop survives only to catch agents spawned *during* the wait.
|
|
171
|
-
The `eslint-disable @typescript-eslint/no-unnecessary-condition` is deleted.
|
|
172
|
-
- `dispose()` — `this.limiter.clear()` (unchanged in intent).
|
|
173
|
-
|
|
174
|
-
### `index.ts` wiring
|
|
175
|
-
|
|
176
|
-
```typescript
|
|
177
|
-
const settings = new SettingsManager({
|
|
178
|
-
// …
|
|
179
|
-
onMaxConcurrentChanged: () => limiter.recheck(), // forward-ref closure (settings → limiter); benign
|
|
180
|
-
});
|
|
181
|
-
settings.load();
|
|
182
|
-
// …
|
|
183
|
-
const limiter = new ConcurrencyLimiter(() => settings.maxConcurrent);
|
|
184
|
-
const manager = new SubagentManager({ /* … */ limiter, /* … */ });
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
The only surviving forward reference is `settings → limiter` (a runtime-only closure, the same shape as today's `settings → queue.drain`).
|
|
188
|
-
The `limiter → manager` back-edge (the `startAgent` callback and its explanatory comment) is **deleted entirely** — that is the structural win.
|
|
189
|
-
|
|
190
|
-
### Edge cases
|
|
191
|
-
|
|
192
|
-
- **Abort while queued** — `markStopped()` flips status; the scheduled thunk, when run, returns `Promise.resolve()` (no-op), settling `record.promise`.
|
|
193
|
-
- **Limit decreased below active count** — `recheck()` simply starts nothing (`active < getLimit()` is false); in-flight tasks finish normally.
|
|
194
|
-
- **Limit increased** — `onMaxConcurrentChanged → limiter.recheck()` starts newly-admissible pending tasks.
|
|
195
|
-
- **`clear()` with in-flight tasks** — only *pending* tasks are dropped; running tasks complete and `active--` on settle.
|
|
196
|
-
|
|
197
|
-
## Module-Level Changes
|
|
198
|
-
|
|
199
|
-
| File | Change |
|
|
200
|
-
| -------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
201
|
-
| `src/lifecycle/concurrency-limiter.ts` | Add — new `ConcurrencyLimiter` (`schedule`, `recheck`, `clear`). |
|
|
202
|
-
| `src/lifecycle/concurrency-queue.ts` | Remove — replaced by the limiter. |
|
|
203
|
-
| `src/lifecycle/subagent-manager.ts` | Change — import limiter; `SubagentManagerOptions.queue` → `limiter: ConcurrencyLimiter` and the private field; drop `markStarted`/`markFinished` from `buildObserver`; `spawn` schedules a status-guarded thunk; `abort` drops `dequeue`; `abortAll` iterates agents + `limiter.clear()`; `waitForAll` simplified (add `pendingPromises` helper, delete the `while (true)` loop and its `eslint-disable`); `dispose` calls `limiter.clear()`; update the file-header comment. |
|
|
204
|
-
| `src/lifecycle/subagent.ts` | Change — `abort()` docstring: remove the `ConcurrencyQueue.dequeue()` reference (queue removal is now a status-guard no-op). |
|
|
205
|
-
| `src/index.ts` | Change — import `ConcurrencyLimiter`; construct it as `new ConcurrencyLimiter(() => settings.maxConcurrent)`; `onMaxConcurrentChanged: () => limiter.recheck()`; delete the `startAgent` callback and its forward-ref comment; inject `limiter` into the manager. |
|
|
206
|
-
| `test/lifecycle/concurrency-limiter.test.ts` | Add — limiter unit tests (no `startAgent` mock). |
|
|
207
|
-
| `test/lifecycle/concurrency-queue.test.ts` | Remove — the queue is gone. |
|
|
208
|
-
| `test/lifecycle/subagent-manager.test.ts` | Change — `createManager` constructs a `ConcurrencyLimiter`; delete the forward-ref `let mgr` + `prefer-const` `eslint-disable`; drop the unused `queue` field from the returned object. |
|
|
209
|
-
| `docs/architecture/architecture.md` | Change — Mermaid lifecycle node (`ConcurrencyQueue<br/>(scheduling, drain)` → `ConcurrencyLimiter<br/>(thunk admission gate)`); layout listing (`concurrency-queue.ts` → `concurrency-limiter.ts`); "What the core owns" bullet; mark roadmap Step 1 done; fix the Step 7 ([#378]) target filename reference. |
|
|
210
|
-
| `.pi/skills/package-pi-subagents/SKILL.md` | Change — lifecycle-domain table: `concurrency-queue.ts` → `concurrency-limiter.ts` and adjust the "scheduling" wording to "concurrency admission". |
|
|
211
|
-
|
|
212
|
-
Verified by grep that no other `src/`, `test/`, `docs/` (excluding `docs/architecture/history/` and prior plans/retros, which are historical), or `.pi/skills/` file references `ConcurrencyQueue`, `concurrency-queue`, `enqueue`, `dequeue`, `markStarted`/`markFinished` (queue), `drain`, `isFull`, or `queuedIds` for this queue.
|
|
213
|
-
`SKILL.md` line 80 (Phase 15 history) keeps `ConcurrencyQueue` — it is a historical record, not current state.
|
|
214
|
-
|
|
215
|
-
## Test Impact Analysis
|
|
216
|
-
|
|
217
|
-
1. **New tests the change enables.**
|
|
218
|
-
`ConcurrencyLimiter` is a pure thunk scheduler with no agent/manager knowledge, so it is unit-testable with plain `() => Promise<void>` tasks and `Promise.withResolvers` gates — no `startAgent` mock, no re-entrant `markStarted` simulation.
|
|
219
|
-
New coverage: FIFO start order; slot gating (only `limit` tasks run concurrently); `active` decrement frees a slot for the next pending task on settle; `recheck()` starts newly-admissible tasks when the limit grows; dynamic limit re-evaluation; `clear()` resolves pending promises without running their tasks; a task that rejects still frees its slot.
|
|
220
|
-
2. **Tests that become redundant.**
|
|
221
|
-
The entire `test/lifecycle/concurrency-queue.test.ts` (`isFull`, `enqueue`/`dequeue`, `markStarted`/`markFinished`, `drain`, auto-drain, `clear`, `queuedIds`) — those methods no longer exist; the limiter tests replace them at a cleaner seam.
|
|
222
|
-
3. **Tests that stay as-is (genuinely exercise the layer).**
|
|
223
|
-
The `SubagentManager — queueing and concurrency with injected stubs` describe block asserts manager-level behavior (queued → running transition order, abort-while-queued never runs the factory, `onSubagentStarted` fires on the queued → running transition).
|
|
224
|
-
These remain valid against the manager + limiter integration and need only the `createManager` helper change (construct a `ConcurrencyLimiter`), not a behavioral rewrite.
|
|
225
|
-
The `clearCompleted does not remove running or queued agents` test (maxConcurrent=1, blocking factory) also stays.
|
|
226
|
-
|
|
227
|
-
## TDD Order
|
|
228
|
-
|
|
229
|
-
Priority = preparatory addition first, then the atomic interface swap, then docs.
|
|
230
|
-
|
|
231
|
-
1. **Add `ConcurrencyLimiter` (red → green).**
|
|
232
|
-
Surface: new `test/lifecycle/concurrency-limiter.test.ts` against new `src/lifecycle/concurrency-limiter.ts`.
|
|
233
|
-
Covers FIFO start order, slot gating, `active`-frees-slot-on-settle, `recheck()` on limit growth, dynamic limit, `clear()` resolves pending without running, reject-frees-slot.
|
|
234
|
-
Pure addition — `ConcurrencyQueue` still exists and its tests still pass; the suite stays green.
|
|
235
|
-
Commit: `feat(pi-subagents): add ConcurrencyLimiter (#381)`.
|
|
236
|
-
|
|
237
|
-
2. **Migrate `SubagentManager`, `index.ts`, and the manager test helper to the limiter; delete the queue (red → green).**
|
|
238
|
-
Surface: `src/lifecycle/subagent-manager.ts`, `src/index.ts`, `src/lifecycle/subagent.ts` (docstring), `test/lifecycle/subagent-manager.test.ts`, and deletion of `src/lifecycle/concurrency-queue.ts` + `test/lifecycle/concurrency-queue.test.ts`.
|
|
239
|
-
This is one atomic commit: changing `SubagentManagerOptions.queue` → `limiter` breaks both call sites (`index.ts` and the test helper) at the type level simultaneously, and the old test file imports the deleted source — all must land together.
|
|
240
|
-
Drop the observer relays, the `dequeue`/`drain`/`isFull`/`queuedIds` usage, the `while (true)` loop + its `eslint-disable`, and the test helper's forward-ref `eslint-disable`.
|
|
241
|
-
Run `pnpm run check` immediately after (shared-interface change with multiple call sites), then the full `pnpm --filter @gotgenes/pi-subagents exec vitest run` (the queueing/concurrency integration tests must still pass).
|
|
242
|
-
Commit: `refactor(pi-subagents): replace ConcurrencyQueue with thunk-based ConcurrencyLimiter (#381)`.
|
|
243
|
-
|
|
244
|
-
3. **Update architecture doc and package skill (docs).**
|
|
245
|
-
Surface: `docs/architecture/architecture.md` (Mermaid node, layout listing, "What the core owns" bullet, roadmap Step 1 marked done, Step 7 filename reference) and `.pi/skills/package-pi-subagents/SKILL.md` (lifecycle-domain table entry + wording).
|
|
246
|
-
Commit: `docs(pi-subagents): update architecture and skill for ConcurrencyLimiter (#381)`.
|
|
247
|
-
|
|
248
|
-
## Risks and Mitigations
|
|
249
|
-
|
|
250
|
-
| Risk | Mitigation |
|
|
251
|
-
| ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
252
|
-
| A dropped pending thunk leaves `record.promise` forever pending. | `clear()` resolves dropped tasks' promises; the limiter's contract is that every `schedule()` promise settles. |
|
|
253
|
-
| `waitForAll` could spin or miss queued agents. | Queued agents now carry real promises, so a single `Promise.allSettled` covers them; the bounded re-check loop only catches agents spawned during the wait, and terminates when `pendingPromises()` is empty. |
|
|
254
|
-
| An abort-while-queued no-op thunk briefly occupies a slot. | The thunk returns a synchronously-resolved promise; `active++`/`active--` round-trip in one microtask and `recheck()` immediately pulls the next task — negligible. |
|
|
255
|
-
| Renaming the file/class leaves stale references. | Grep-verified inventory in Module-Level Changes; the migration deletes the source and its test in the same commit; docs updated in step 3. |
|
|
256
|
-
| `bypassQueue` public-surface name now slightly misnames the mechanism. | Out of scope (breaking); recorded in Open Questions. |
|
|
257
|
-
|
|
258
|
-
## Open Questions
|
|
259
|
-
|
|
260
|
-
- Should `bypassQueue` be renamed (e.g. `bypassLimiter`) for accuracy?
|
|
261
|
-
It is public type surface, so a rename is breaking and belongs in its own change — defer.
|
|
262
|
-
- Should the `code-design` "narrow interface, not concrete class" guidance be applied to the manager's `limiter` field (typed as `{ schedule; clear }` rather than the concrete `ConcurrencyLimiter`)?
|
|
263
|
-
Tests construct a real limiter (it is pure and trivially constructible), so no mock-cast pressure exists today; keep the concrete type to match the issue and existing pattern, and revisit only if a test needs to substitute it.
|
|
264
|
-
|
|
265
|
-
[#373]: https://github.com/gotgenes/pi-packages/issues/373
|
|
266
|
-
[#374]: https://github.com/gotgenes/pi-packages/issues/374
|
|
267
|
-
[#378]: https://github.com/gotgenes/pi-packages/issues/378
|
|
@@ -1,199 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
issue: 400
|
|
3
|
-
issue_title: "perf(pi-subagents): include parent system prompt in replace mode for KV cache reuse"
|
|
4
|
-
---
|
|
5
|
-
|
|
6
|
-
# Include parent system prompt in replace mode for KV cache reuse
|
|
7
|
-
|
|
8
|
-
## Problem Statement
|
|
9
|
-
|
|
10
|
-
In replace mode, `buildAgentPrompt()` discards the parent system prompt entirely and substitutes a thin two-line header (`"You are a pi coding agent sub-agent. / You have been invoked to handle a specific task autonomously."`).
|
|
11
|
-
Replace-mode agents therefore lose the core identity, tool-usage guidelines, and AGENTS.md context the parent carries, and they share no prompt prefix with the parent or with each other — defeating LLM KV cache reuse.
|
|
12
|
-
The `parentSystemPrompt` parameter is already passed into `buildAgentPrompt()` but the replace branch ignores it.
|
|
13
|
-
|
|
14
|
-
## Goals
|
|
15
|
-
|
|
16
|
-
- Place the parent system prompt (or `genericBase` when no parent is available) at the front of the replace-mode prompt as a shared, cacheable prefix.
|
|
17
|
-
- Order the replace-mode prompt as: parent/`genericBase` → `<active_agent>` tag → env block → `config.systemPrompt`.
|
|
18
|
-
- Preserve the distinguishing feature of replace mode: it injects neither the `<sub_agent_context>` bridge nor the `<agent_instructions>` wrapper — the custom prompt keeps full control of the agent's instructions, placed last so it has the final say.
|
|
19
|
-
- Apply the change uniformly to every replace-mode agent, including the built-in `Explore` and `Plan` agents.
|
|
20
|
-
- This is a **breaking change**: replace-mode agents (including `Explore`/`Plan` and any custom `prompt_mode: replace` agent) now inherit the parent system prompt on upgrade with no user edit, and the thin two-line header is removed.
|
|
21
|
-
Ship it as `perf!:` with a `BREAKING CHANGE:` footer.
|
|
22
|
-
|
|
23
|
-
## Non-Goals
|
|
24
|
-
|
|
25
|
-
- No change to append-mode assembly (already reordered for KV cache in [#180]).
|
|
26
|
-
- No change to how `parentSystemPrompt` is sourced — `create-subagent-session.ts` already passes `snapshot.systemPrompt` through `session-config.ts`.
|
|
27
|
-
- No new mode or flag to distinguish "replace with parent" from "replace without parent" — the operator confirmed the change applies uniformly, so `Explore`/`Plan` are not special-cased.
|
|
28
|
-
- No change to `pi-permission-system` — its `<active_agent>` tag parsing is a full-string regex search, position-independent.
|
|
29
|
-
- No change to `pi-anthropic-auth` — its OAuth shaping is unaffected (see Background).
|
|
30
|
-
|
|
31
|
-
## Background
|
|
32
|
-
|
|
33
|
-
`buildAgentPrompt()` in `packages/pi-subagents/src/session/prompts.ts` assembles the child system prompt.
|
|
34
|
-
The append branch was reordered in [#180] (shipped in `pi-subagents-v6.18.3`) to place shared/stable content first; the parent prompt is placed verbatim (no wrapper tag) so it forms an identical byte prefix with the parent session, maximising KV cache hits.
|
|
35
|
-
The replace branch was left untouched and still emits the thin header.
|
|
36
|
-
|
|
37
|
-
Current replace branch:
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
// "replace" mode — env header + the config's full system prompt
|
|
41
|
-
const replaceHeader = `You are a pi coding agent sub-agent.
|
|
42
|
-
You have been invoked to handle a specific task autonomously.
|
|
43
|
-
|
|
44
|
-
${envBlock}`;
|
|
45
|
-
|
|
46
|
-
return activeAgentTag + replaceHeader + "\n\n" + config.systemPrompt;
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
`const identity = parentSystemPrompt ?? genericBase;` currently lives inside the append branch.
|
|
50
|
-
`genericBase` (a `# Role` / general-purpose coding agent blurb) is the shared fallback.
|
|
51
|
-
|
|
52
|
-
### Cross-extension interaction — `pi-anthropic-auth` OAuth
|
|
53
|
-
|
|
54
|
-
The operator asked how the `genericBase` fallback interacts with `@gotgenes/pi-anthropic-auth`.
|
|
55
|
-
Findings from reading that package's `src/system-prompt-shaping.ts` and `src/request-shaping.ts`:
|
|
56
|
-
|
|
57
|
-
- The OAuth de-fingerprinting (`shapeAnthropicOAuthSystemPrompt`) only activates when the system prompt contains `PI_DEFAULT_PROMPT_PREFIX` (Pi's default expert-coding-assistant preamble); otherwise it returns the prompt untouched.
|
|
58
|
-
- The `x-anthropic-billing-header` system block is prepended **unconditionally** for every OAuth request (`prependBillingHeader`), independent of the base prompt content — this is the primary Claude Code billing signal.
|
|
59
|
-
|
|
60
|
-
Implications for this change:
|
|
61
|
-
|
|
62
|
-
- Normal case (parent present): replace mode places the parent prompt verbatim at the front, structurally identical to append mode, which already works under the OAuth transport wrapper.
|
|
63
|
-
The inherited Pi preamble is de-fingerprinted exactly as it is for append-mode subagents and the main session today.
|
|
64
|
-
- `genericBase` fallback (only when the parent snapshot has no system prompt — effectively never in real sessions, since `parentSystemPrompt` is a required `string` at the `session-config` layer): `genericBase` carries no Pi fingerprint, so the OAuth shaping no-ops and the billing header is still prepended.
|
|
65
|
-
`genericBase` is already neutral, so nothing leaks.
|
|
66
|
-
|
|
67
|
-
Conclusion: #400 introduces no new OAuth interaction. `genericBase` remains the correct fallback and stays consistent with append mode.
|
|
68
|
-
|
|
69
|
-
### Constraints from AGENTS.md
|
|
70
|
-
|
|
71
|
-
- This package carries a type-declaration bundle for its public API, but `buildAgentPrompt` is internal — no `dist/public.d.ts` or `exports` impact, so `verify:public-types` is not required for this change.
|
|
72
|
-
- Conventional Commits; do not edit `CHANGELOG.md` (release-please owns it).
|
|
73
|
-
- The `BREAKING CHANGE:` footer text is reused verbatim in the release-please CHANGELOG and the issue close comment — name only real surface (`prompt_mode: replace`).
|
|
74
|
-
|
|
75
|
-
## Design Overview
|
|
76
|
-
|
|
77
|
-
Hoist the `identity` resolution above the branch so both modes share it, then rewrite the replace branch.
|
|
78
|
-
|
|
79
|
-
```typescript
|
|
80
|
-
const activeAgentTag = `<active_agent name="${config.name}"/>\n\n`;
|
|
81
|
-
const envBlock = `# Environment\n...`;
|
|
82
|
-
const identity = parentSystemPrompt ?? genericBase;
|
|
83
|
-
|
|
84
|
-
if (config.promptMode === "append") {
|
|
85
|
-
// ...unchanged...
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// "replace" mode — shared parent prompt (or generic base) first for KV cache
|
|
89
|
-
// reuse, then the active_agent tag, env block, and the config's full system
|
|
90
|
-
// prompt. Unlike append mode, replace mode injects neither the
|
|
91
|
-
// <sub_agent_context> bridge nor the <agent_instructions> wrapper — the custom
|
|
92
|
-
// prompt keeps full control of the agent's instructions.
|
|
93
|
-
return identity + "\n\n" + activeAgentTag + envBlock + "\n\n" + config.systemPrompt;
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
Resulting replace-mode order (`activeAgentTag` already ends with `\n\n`):
|
|
97
|
-
|
|
98
|
-
```text
|
|
99
|
-
1. parentSystemPrompt (or genericBase) ← SHARED, cacheable prefix
|
|
100
|
-
2. <active_agent name="${name}"/> ← varies per agent
|
|
101
|
-
3. # Environment ... ← varies per runtime
|
|
102
|
-
4. config.systemPrompt ← custom instructions (full control)
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
This mirrors append mode's prefix-first ordering, minus the bridge and the `<agent_instructions>` wrapper.
|
|
106
|
-
The change is a pure single-function edit — no new collaborator, no new module, no interface change — so the design-review structural checklist (dependency width, Law of Demeter, extraction seams) does not apply.
|
|
107
|
-
|
|
108
|
-
### Edge cases
|
|
109
|
-
|
|
110
|
-
- Empty `config.systemPrompt` (e.g. a replace agent with no body): the prompt ends with a trailing `\n\n` after the env block.
|
|
111
|
-
Acceptable and consistent with current behavior; no special-casing.
|
|
112
|
-
`genericBase` only substitutes on a nullish parent (the `??` operator), so an empty-string parent prompt is preserved as-is, matching append mode.
|
|
113
|
-
|
|
114
|
-
## Module-Level Changes
|
|
115
|
-
|
|
116
|
-
### `packages/pi-subagents/src/session/prompts.ts`
|
|
117
|
-
|
|
118
|
-
1. Hoist `const identity = parentSystemPrompt ?? genericBase;` from the append branch to before the `if (config.promptMode === "append")` check so both branches use it.
|
|
119
|
-
2. Replace the replace-branch `replaceHeader` template and return statement with the new ordering (`identity` → `activeAgentTag` → `envBlock` → `config.systemPrompt`); remove the thin two-line header.
|
|
120
|
-
3. Update the JSDoc summary: replace-mode bullet becomes "parent system prompt (or generic base) + active_agent tag + env header + config.systemPrompt; no bridge, no agent_instructions wrapper," and update the trailing note about tag position (it is included, not prepended, in either mode).
|
|
121
|
-
|
|
122
|
-
### `packages/pi-subagents/test/session/prompts.test.ts`
|
|
123
|
-
|
|
124
|
-
See Test Impact Analysis and TDD Order for the specific test changes.
|
|
125
|
-
|
|
126
|
-
### `packages/pi-subagents/README.md`
|
|
127
|
-
|
|
128
|
-
1. Lines 119–120 — the `Explore` and `Plan` rows: revise the `replace` (standalone) framing, since replace mode now inherits the parent prompt as its base.
|
|
129
|
-
2. Line 187 — the `prompt_mode` frontmatter table: `replace` no longer means "no AGENTS.md / CLAUDE.md inheritance."
|
|
130
|
-
Reword to describe the new semantics: replace inherits the parent prompt as the base, then the body takes full control (no `<sub_agent_context>` bridge, no `<agent_instructions>` wrapper), whereas append wraps the body and adds the bridge.
|
|
131
|
-
3. Line 494 (Patch 3, `<active_agent>` tag): change "prepends ... to every assembled child system prompt (both `replace` and `append` modes)" to "includes ... in every assembled child system prompt (both modes)" — the tag follows the cacheable parent prefix in both modes now, so "prepends" is inaccurate.
|
|
132
|
-
|
|
133
|
-
No `docs/architecture/` updates: the architecture doc references `prompts.ts` only as a one-line file listing (no prompt-assembly description, no complexity/health table entry tied to this change).
|
|
134
|
-
|
|
135
|
-
## Test Impact Analysis
|
|
136
|
-
|
|
137
|
-
This is a behavior change, not an extraction, so the extraction-specific questions are limited.
|
|
138
|
-
|
|
139
|
-
- New behavior to cover: replace mode now includes the parent prompt as a cacheable prefix; falls back to `genericBase` with no parent; still excludes the bridge and the `<agent_instructions>` wrapper.
|
|
140
|
-
- Existing replace-mode tests that assert the old behavior must change (they pin the removed thin header and the "ignores parent prompt" premise).
|
|
141
|
-
- `toContain`-based tests for cwd/git/env and the `genericBase` fallback remain valid where position-independent.
|
|
142
|
-
- No existing test becomes redundant beyond the ones being rewritten; no test must stay frozen for a layer being extracted (nothing is extracted).
|
|
143
|
-
|
|
144
|
-
Tests that change in `test/session/prompts.test.ts`:
|
|
145
|
-
|
|
146
|
-
1. `"replace mode uses config systemPrompt directly"` — asserts `toContain("You are a pi coding agent sub-agent")`; that header is removed.
|
|
147
|
-
Rewrite to assert the config prompt is present and the thin header is gone.
|
|
148
|
-
2. `"replace mode ignores parent prompt"` — asserts the parent content is absent.
|
|
149
|
-
The premise inverts: rename to `"replace mode includes parent prompt as base (no bridge/wrapper)"` and assert the parent content is present while `<sub_agent_context>` and `<agent_instructions>` are absent.
|
|
150
|
-
3. `"prepends <active_agent name=...> tag in replace mode"` — asserts `prompt.startsWith('<active_agent name="Explore"/>\n\n')`.
|
|
151
|
-
The tag no longer leads (parent/`genericBase` does); rewrite to assert the tag appears after the identity prefix and before the env block.
|
|
152
|
-
4. `"active_agent tag appears before envBlock in both modes"` — the replace assertions pin `tagIdx === 0`.
|
|
153
|
-
Update the replace assertions: the tag is no longer at index 0 but still precedes `# Environment`.
|
|
154
|
-
The append assertions stay as-is.
|
|
155
|
-
|
|
156
|
-
## TDD Order
|
|
157
|
-
|
|
158
|
-
All test and source changes live in two files that the type checker links (the replace branch and its tests).
|
|
159
|
-
Each cycle is a single commit that leaves the suite green.
|
|
160
|
-
|
|
161
|
-
1. **Red: rewrite replace-mode behavioral tests.**
|
|
162
|
-
Update tests 1–2 above to the new behavior (parent prompt included as base; thin header removed; no bridge/wrapper), and add a test for the `genericBase` fallback when no parent is supplied in replace mode, plus a test pinning the full order (`identity` → `<active_agent>` → `# Environment` → `config.systemPrompt`).
|
|
163
|
-
These fail against the current implementation.
|
|
164
|
-
Commit: `test: assert replace mode inherits parent prompt as cacheable prefix (#400)`
|
|
165
|
-
|
|
166
|
-
2. **Green: rewrite the replace branch.**
|
|
167
|
-
Hoist `identity`, replace the `replaceHeader` block with the new ordering, remove the thin header, and update the JSDoc.
|
|
168
|
-
Update the positional `<active_agent>` tests (3–4 above) in the same commit — they break at runtime the moment the branch changes.
|
|
169
|
-
Commit body carries the `BREAKING CHANGE:` footer.
|
|
170
|
-
Commit: `perf!: include parent system prompt in replace mode (#400)`
|
|
171
|
-
|
|
172
|
-
```text
|
|
173
|
-
BREAKING CHANGE: replace-mode subagents (built-in Explore/Plan and any
|
|
174
|
-
custom prompt_mode: replace agent) now inherit the parent system prompt as
|
|
175
|
-
their base instead of a thin standalone header. The custom prompt is
|
|
176
|
-
appended last and retains full control; the <sub_agent_context> bridge and
|
|
177
|
-
<agent_instructions> wrapper are still omitted in replace mode.
|
|
178
|
-
```
|
|
179
|
-
|
|
180
|
-
3. **Docs: update README replace-mode semantics.**
|
|
181
|
-
Apply the three README edits (Explore/Plan rows, `prompt_mode` table, Patch 3 `<active_agent>` wording).
|
|
182
|
-
Commit: `docs: describe replace-mode parent inheritance (#400)`
|
|
183
|
-
|
|
184
|
-
## Risks and Mitigations
|
|
185
|
-
|
|
186
|
-
| Risk | Mitigation |
|
|
187
|
-
| --------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
188
|
-
| `Explore`/`Plan` behavior shifts — they now carry the full parent prompt plus their read-only specialist instructions | Operator confirmed uniform application; specialist instructions are placed last so they have the final say; existing read-only assertions (`READ-ONLY`, `file search specialist`) still hold via `toContain`. |
|
|
189
|
-
| `pi-permission-system` depends on `<active_agent>` tag position | Tag parsing is a full-string regex search; position-independent (same basis as [#180]). |
|
|
190
|
-
| `pi-anthropic-auth` OAuth shaping breaks with the new base | No new interaction — billing header is prepended unconditionally; de-fingerprinting keys off `PI_DEFAULT_PROMPT_PREFIX` and `genericBase` is already neutral (see Background). |
|
|
191
|
-
| A custom replace agent relied on the clean-slate (no parent) behavior | Documented as breaking in the `BREAKING CHANGE:` footer and README; this aligns with the expectation reported in the issue ([@jeffutter] expected the parent identity to be present). |
|
|
192
|
-
| Stale README claims that replace = no inheritance | README edits in cycle 3 correct lines 119–120, 187, and 494. |
|
|
193
|
-
|
|
194
|
-
## Open Questions
|
|
195
|
-
|
|
196
|
-
None — the three design decisions (breaking classification, `genericBase` fallback, uniform application to built-ins) were resolved with the operator before planning.
|
|
197
|
-
|
|
198
|
-
[#180]: https://github.com/gotgenes/pi-packages/issues/180
|
|
199
|
-
[@jeffutter]: https://github.com/jeffutter
|