@jopqior/pi-subagents 1.0.0
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 +2705 -0
- package/LICENSE +21 -0
- package/README.md +503 -0
- package/dist/public.d.ts +331 -0
- package/dist/settings.d.ts +82 -0
- package/docs/architecture/architecture.md +1566 -0
- package/docs/architecture/client-server-opportunities.md +127 -0
- package/docs/architecture/history/phase-1-api-boundary.md +8 -0
- package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
- package/docs/architecture/history/phase-11-closure-to-class.md +100 -0
- package/docs/architecture/history/phase-12-complexity-test-fixtures.md +55 -0
- package/docs/architecture/history/phase-13-remaining-smells.md +88 -0
- package/docs/architecture/history/phase-14-strip-policy.md +49 -0
- package/docs/architecture/history/phase-15-domain-model-evolution.md +73 -0
- package/docs/architecture/history/phase-16-invert-dependencies.md +144 -0
- package/docs/architecture/history/phase-17-core-consolidation.md +214 -0
- package/docs/architecture/history/phase-18-reconsider-ui.md +166 -0
- package/docs/architecture/history/phase-19-implement-ui-decisions.md +282 -0
- package/docs/architecture/history/phase-2-remove-scheduling.md +9 -0
- package/docs/architecture/history/phase-20-result-delivery.md +245 -0
- package/docs/architecture/history/phase-21-classification-model-boundary.md +107 -0
- package/docs/architecture/history/phase-3-remove-rpc-groupjoin.md +11 -0
- package/docs/architecture/history/phase-4-implement-service.md +8 -0
- package/docs/architecture/history/phase-5-decompose-index.md +42 -0
- package/docs/architecture/history/phase-7-encapsulation.md +173 -0
- package/docs/architecture/history/phase-8-testability.md +103 -0
- package/docs/architecture/history/phase-9-observation-ctx.md +122 -0
- package/docs/comparison-with-upstream.md +77 -0
- package/docs/configuration.md +364 -0
- package/docs/decisions/0001-deferred-patches.md +80 -0
- package/docs/decisions/0002-extensions-on-a-minimal-core.md +125 -0
- package/docs/decisions/0003-publish-bundled-type-declarations.md +71 -0
- package/docs/decisions/0004-reconsider-ui-direction.md +279 -0
- package/docs/decisions/0005-subagent-record-admission-policy.md +106 -0
- package/docs/decisions/0006-inherited-prompt-is-identity-only.md +104 -0
- package/docs/decisions/0007-transcript-viewer-is-not-an-overlay.md +228 -0
- package/docs/decisions/0008-inherited-region-is-shared-parts.md +81 -0
- package/docs/decisions/0009-portable-inheritance-is-provider-scoped.md +116 -0
- package/package.json +91 -0
- package/src/config/agent-types.ts +135 -0
- package/src/config/custom-agents.ts +151 -0
- package/src/config/default-agents.ts +121 -0
- package/src/config/invocation-config.ts +167 -0
- package/src/config/thinking-level.ts +58 -0
- package/src/debug.ts +14 -0
- package/src/handlers/index.ts +3 -0
- package/src/handlers/interrupt.ts +58 -0
- package/src/handlers/lifecycle.ts +71 -0
- package/src/handlers/widget-events.ts +49 -0
- package/src/index.ts +292 -0
- package/src/layered-settings.ts +105 -0
- package/src/lifecycle/child-lifecycle.ts +115 -0
- package/src/lifecycle/child-shutdown.ts +105 -0
- package/src/lifecycle/concurrency-limiter.ts +55 -0
- package/src/lifecycle/create-subagent-session.ts +335 -0
- package/src/lifecycle/parent-snapshot.ts +119 -0
- package/src/lifecycle/run-listeners.ts +37 -0
- package/src/lifecycle/selection-scope.ts +116 -0
- package/src/lifecycle/spawn-selection.ts +259 -0
- package/src/lifecycle/subagent-manager.ts +546 -0
- package/src/lifecycle/subagent-session.ts +347 -0
- package/src/lifecycle/subagent-state.ts +404 -0
- package/src/lifecycle/subagent.ts +885 -0
- package/src/lifecycle/turn-limits.ts +13 -0
- package/src/lifecycle/usage.ts +60 -0
- package/src/lifecycle/workspace-bracket.ts +76 -0
- package/src/lifecycle/workspace.ts +46 -0
- package/src/observation/composite-subagent-observer.ts +74 -0
- package/src/observation/notification.ts +430 -0
- package/src/observation/outcome-delivery.ts +239 -0
- package/src/observation/record-observer.ts +78 -0
- package/src/observation/renderer.ts +161 -0
- package/src/observation/subagent-events-observer.ts +148 -0
- package/src/runtime.ts +137 -0
- package/src/service/service-adapter.ts +201 -0
- package/src/service/service.ts +246 -0
- package/src/session/ask-parent-tool.ts +69 -0
- package/src/session/content-items.ts +53 -0
- package/src/session/context.ts +80 -0
- package/src/session/conversation.ts +49 -0
- package/src/session/env.ts +40 -0
- package/src/session/model-resolver.ts +126 -0
- package/src/session/notify-parent-tool.ts +83 -0
- package/src/session/package-exclusions.ts +75 -0
- package/src/session/prompts.ts +231 -0
- package/src/session/provider-inheritance.ts +56 -0
- package/src/session/selection-catalogue.ts +143 -0
- package/src/session/session-config.ts +202 -0
- package/src/session/session-dir.ts +38 -0
- package/src/settings.ts +447 -0
- package/src/tools/agent-tool.ts +305 -0
- package/src/tools/background-spawner.ts +83 -0
- package/src/tools/foreground-runner.ts +159 -0
- package/src/tools/get-result-renderer.ts +119 -0
- package/src/tools/get-result-report.ts +84 -0
- package/src/tools/get-result-tool.ts +192 -0
- package/src/tools/helpers.ts +118 -0
- package/src/tools/result-renderer.ts +153 -0
- package/src/tools/spawn-config.ts +192 -0
- package/src/tools/steer-tool.ts +109 -0
- package/src/types.ts +143 -0
- package/src/ui/agent-widget.ts +333 -0
- package/src/ui/bounded-lines.ts +45 -0
- package/src/ui/display.ts +180 -0
- package/src/ui/glyphs.ts +62 -0
- package/src/ui/session-navigation.ts +150 -0
- package/src/ui/session-navigator.ts +255 -0
- package/src/ui/subagents-settings.ts +179 -0
- package/src/ui/transcript-content.ts +374 -0
- package/src/ui/widget-renderer.ts +301 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# Phase 21: Classification predicates, resume completion, model boundary
|
|
2
|
+
|
|
3
|
+
Phase 21 is a lean, three-step phase.
|
|
4
|
+
Discovery (2026-07-17: architecture-doc reading, issue sweep, fallow baseline, repeated-discriminator sweep, entry-point trace, craftsmanship scout) found the declared target architecture essentially complete: fallow reports 0 refactoring targets, 0 dead code, 0 duplication, and the craftsmanship scout refuted both fallow "giant test file" flags and found only scattered boy-scout polish.
|
|
5
|
+
Three cause-level Category C findings survived — two already filed as issues, one the explicitly deferred remainder of Phase 20 Step 4 — and they are the whole phase.
|
|
6
|
+
No polish step is manufactured to fill the ceiling; the scout's scattered findings (`mock.calls[N][idx]` indexing in the two lifecycle test suites, the `settings.ts` `sanitize()` range-check triplication, `createManager`'s nullish-coalescing density) are handed to the `tidy-first` boy-scout path.
|
|
7
|
+
|
|
8
|
+
## Health metrics
|
|
9
|
+
|
|
10
|
+
| Metric | Phase 20 (end) | Phase 21 target | Recompute |
|
|
11
|
+
| ----------------------------------------------------------------- | -------------- | --------------- | --------------------------------------------------------------------------------------------------------------------- |
|
|
12
|
+
| Health score | 78/100 (B) | ≥ 78 (B) | `pnpm fallow health --score --workspace @gotgenes/pi-subagents` |
|
|
13
|
+
| Multi-status classification groupings outside `subagent-state.ts` | 11 | ≤ 2 → 2 ✅ | `grep -rEn 'status [!=]== "[a-z]+" (\|\||&&) ' packages/pi-subagents/src --include="*.ts" \| grep -vc subagent-state` |
|
|
14
|
+
| `model`/`parentModel` typed `unknown` in `src/` | 7 | 0 ✅ | `grep -rEn "model\??: unknown\|parentModel\??: unknown" packages/pi-subagents/src --include="*.ts" \| wc -l` |
|
|
15
|
+
| Direct `this.mark*` calls inside `resume()` | 2 | 0 | `sed -n '/async resume(/,/^\t}/p' packages/pi-subagents/src/lifecycle/subagent.ts \| grep -c 'this\.mark'` |
|
|
16
|
+
| Dead code / production duplication | 0 / 0 | 0 / 0 | `pnpm fallow dead-code --workspace @gotgenes/pi-subagents` / `pnpm fallow dupes --workspace @gotgenes/pi-subagents` |
|
|
17
|
+
|
|
18
|
+
All three targeted metrics were confirmed delivered at archive time (recomputed 2026-07): the classification-grouping grep lands at 2, the `model`/`parentModel` `unknown` grep lands at 0, and the `resume()` `this.mark*` grep lands at 0.
|
|
19
|
+
The recomputed health score reproduces 78/100 (B) using the `--hotspots --targets` form (the bare `--score` form reports 88 A on this workspace — the two forms diverge, and 78 B is the form the baseline was computed with).
|
|
20
|
+
|
|
21
|
+
## ✅ Step 1 — Add classification predicates to `SubagentState` ([#563])
|
|
22
|
+
|
|
23
|
+
Cause: the state machine owns its six status transitions (design principle 9, "state owns its mutations") but not what a status _means_ — consumers re-derive the is-active (`running \|\| queued`), terminal-error (`error \|\| stopped \|\| aborted`), and steer/run-eligibility groupings at 11 sites across 8 files, so adding a status means finding every grouping and a missed one diverges silently.
|
|
24
|
+
Fallow is structurally blind to this smell (scattered one-line conditionals never form a token-run clone); the repeated-discriminator sweep is the detector, and it corroborates [#563]'s site list exactly.
|
|
25
|
+
Smell: Category C (repeated discriminator / anemic classification).
|
|
26
|
+
Target files: `src/lifecycle/subagent-state.ts` (instance predicates such as `isActive()` / `isTerminalError()` / `canBeSteered()`, plus exported status-level predicate functions for DTO consumers, with the instance predicates delegating so the module stays the single owner); consumers in `src/lifecycle/subagent.ts`, `src/lifecycle/subagent-manager.ts`, `src/tools/get-result-tool.ts`, `src/tools/background-spawner.ts`, `src/ui/widget-renderer.ts`, `src/ui/agent-widget.ts`, `src/ui/session-navigation.ts`, `src/observation/renderer.ts`, `src/observation/subagent-events-observer.ts`.
|
|
27
|
+
The per-status renderer arms (`result-renderer.ts`, `widget-renderer.ts` status→icon maps, `resolveStatusPresentation`) are legitimate presentation dispatch and stay.
|
|
28
|
+
Outcome: multi-status classification groupings outside `subagent-state.ts` drop 11 → ≤ 2 (any residual site is a single-status presentation or wait check, not a re-derived grouping).
|
|
29
|
+
Impact 3 / Risk 1 / Priority 15.
|
|
30
|
+
|
|
31
|
+
Landed: `isActiveStatus` / `isTerminalErrorStatus` / `isRunningStatus` are exported from `subagent-state.ts` as the single decision point; `SubagentState` (and the delegating `Subagent`) gained `isActive()` / `isTerminalError()` / `isRunning()` / `canBeSteered()`, both instance and status-level predicates delegating to the same functions.
|
|
32
|
+
Record holders (`subagent-manager.ts` ×4, `get-result-tool.ts`, `subagent.ts` `guardedRun`/`steer`/`abort`, `subagent-events-observer.ts`) call the instance methods; DTO holders (`widget-renderer.ts`, `renderer.ts`, `session-navigation.ts`) call the exported functions, with `NotificationDetails.status` and `WidgetAgent.status` tightened to `SubagentStatus`.
|
|
33
|
+
The three targeted groupings are gone outside `subagent-state.ts`; the grep lands at 2 (meets ≤ 2), both residuals being presentation/single-status the phase excludes — `result-renderer.ts`'s `completed \|\| steered` renderer arm and `get-result-tool.ts`'s `running` wait guard.
|
|
34
|
+
|
|
35
|
+
Release: independent
|
|
36
|
+
|
|
37
|
+
## ✅ Step 2 — Route resume termination through the completion channel ([#466])
|
|
38
|
+
|
|
39
|
+
Cause: dual completion channels — `Subagent.resume()` terminates via direct `markCompleted`/`markError` and never invokes `onRunFinished`, so the manager-level observer chain (public `subagents:completed`/`failed` events, `subagents:record` persistence, completion notification) never observes a resumed completion; completion signalling is fused to the _first_ run instead of owned by run termination generally.
|
|
40
|
+
This is a user-visible bug: after a resume, the persisted history shows the pre-resume result and external `SUBAGENT_EVENTS` subscribers never see the second finish.
|
|
41
|
+
Smell: Category C (coupling/boundary flaw), plus `bug`.
|
|
42
|
+
Design decision to resolve at `/plan-issue` time: a distinct resumed-completion event versus a `resumed: true` discriminator on the existing channels — the issue leans distinct-event because the once-per-session `session-created` → `disposed` child-lifecycle arc is load-bearing for pi-permission-system's registry bracket.
|
|
43
|
+
Invariant: do not perturb the child-lifecycle event ordering.
|
|
44
|
+
Target files: `src/lifecycle/subagent.ts` (share the termination path with `completeRun`/`failRun`), `src/lifecycle/subagent-session.ts` (`resumeTurnLoop` result shape), `src/observation/subagent-events-observer.ts`, `src/service/service.ts` (`SUBAGENT_EVENTS`, if a new channel is chosen), and this document's lifecycle-events table.
|
|
45
|
+
Soft-depends on Step 1: both edit `subagent.ts`'s status guards, and Step 1's predicates make the shared termination guard cleaner.
|
|
46
|
+
Outcome: direct `this.mark*` calls inside `resume()` drop 2 → 0; a resumed completion produces a notification, an updated persisted record, and a public event, each pinned by a regression test.
|
|
47
|
+
Impact 4 / Risk 2 / Priority 16.
|
|
48
|
+
|
|
49
|
+
Landed: the design decision resolved to a **distinct** `subagents:resumed` channel (single channel — the payload's `status`/`error` discriminate completed from error) and a **silent** child-lifecycle (no `subagents:child:*` on resume), so `subagent-session.ts` was untouched (`resumeTurnLoop` stays `Promise<string>`).
|
|
50
|
+
`Subagent.resume()` now routes through new `completeResume`/`failResume` methods (the two direct `this.mark*` calls in `resume()` are gone), which fire a new optional `SubagentLifecycleObserver.onResumeFinished`; `buildObserver()` maps it, background-gated, onto a new required `SubagentManagerObserver.onSubagentResumed`.
|
|
51
|
+
`SubagentEventsObserver.onSubagentResumed` emits `subagents:resumed` (via the shared `persistAndNotify` extracted from `onSubagentCompleted`) and re-appends `subagents:record`; the notification stays consumption-gated.
|
|
52
|
+
Regression tests pin the emit/append/notify chain, the observer wiring, and the background-only gate.
|
|
53
|
+
|
|
54
|
+
Release: independent
|
|
55
|
+
|
|
56
|
+
## ✅ Step 3 — Finish typing the model boundary ([#611])
|
|
57
|
+
|
|
58
|
+
Cause: the SDK model boundary is half-typed — Phase 20 Step 4 typed the resolver/tools layer against `Model<any>` but explicitly deferred the snapshot/session-assembly thread, leaving `model: unknown` at 7 sites, an `as Model<any>` cast in `src/runtime.ts`, and a `ctx.modelRegistry!` assertion in `src/lifecycle/parent-snapshot.ts`.
|
|
59
|
+
Feasibility probed against the real surface: `ExtensionContext.model: Model<any> \| undefined` and `ModelRegistry` are exported by `@earendil-works/pi-coding-agent` (`dist/core/extensions/types.d.ts`), and `src/session/model-resolver.ts` already imports `Model<any>` from `@earendil-works/pi-ai`.
|
|
60
|
+
Smell: Category C (platform type threading).
|
|
61
|
+
Target files: `src/lifecycle/parent-snapshot.ts`, `src/types.ts` (`SessionContext.model`), `src/session/session-config.ts`, `src/lifecycle/create-subagent-session.ts`, `src/runtime.ts`.
|
|
62
|
+
Outcome: `model`/`parentModel` `unknown` sites drop 7 → 0; the `runtime.ts` cast and the `parent-snapshot.ts` non-null assertion are removed.
|
|
63
|
+
Impact 3 / Risk 1 / Priority 15.
|
|
64
|
+
|
|
65
|
+
Landed: `SessionContext.model` / `ParentSnapshot.model` / `AssemblerContext.parentModel` / `AssemblerOptions.model` / `SessionConfig.model` and `resolveDefaultModel`'s parameter and return are typed against `Model<any>` (imported from `@earendil-works/pi-ai`); the `modelRegistry` fields on `SessionContext`, `ParentSnapshot`, `AssemblerContext`, and `CreateSessionOptions` are typed against the shared `ModelRegistry` (from `model-resolver`), collapsing the two duplicated inline structural types.
|
|
66
|
+
`SessionContext.modelRegistry` is now non-optional (matching the SDK's `ExtensionContext`), which let the `parent-snapshot.ts` `ctx.modelRegistry!` assertion and the `runtime.ts` `as Model<any>` cast both drop.
|
|
67
|
+
The recompute grep lands at 0.
|
|
68
|
+
|
|
69
|
+
Release: independent
|
|
70
|
+
|
|
71
|
+
## Step dependencies
|
|
72
|
+
|
|
73
|
+
```mermaid
|
|
74
|
+
flowchart LR
|
|
75
|
+
S1["✅ Step 1 (#563)<br/>Classification predicates"] -.soft.-> S2["✅ Step 2 (#466)<br/>Resume completion channel"]
|
|
76
|
+
S3["✅ Step 3 (#611)<br/>Type the model boundary"]
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Parallel tracks
|
|
80
|
+
|
|
81
|
+
- **Track A — Tell-don't-ask:** Steps 1 → 2 (soft ordering; both edit `subagent.ts`).
|
|
82
|
+
- **Track B — SDK boundary:** Step 3 (fully independent).
|
|
83
|
+
|
|
84
|
+
## Release batches
|
|
85
|
+
|
|
86
|
+
- No batches; every step is independently releasable.
|
|
87
|
+
- Independently releasable: Steps 1, 2, 3.
|
|
88
|
+
- Step 2 lands as `fix:` — the phase's unhidden release vehicle; Steps 1 and 3 land as `refactor:` (hidden changelog types) and auto-batch into the next unhidden release.
|
|
89
|
+
|
|
90
|
+
## Deferred work (explicit dispositions, 2026-07-17)
|
|
91
|
+
|
|
92
|
+
- [#451] (CI/lint gate validating Mermaid diagrams with `mmdc`) — deferred with rationale: repo-level CI tooling, not pi-subagents structure; it does not belong to a package structural phase.
|
|
93
|
+
Second consecutive sweep, so this is an explicit decision, not a silent re-defer.
|
|
94
|
+
- [#465], [#482], [#608] (feature requests) and [#519], [#600], [#610] (cross-package pi-permission-system tracks) — deferred with rationale: feature and cross-package work that does not gate this package's structural phase.
|
|
95
|
+
Step 2 ([#466]) is a prerequisite for [#465]'s ask-back design, so landing this phase unblocks that track.
|
|
96
|
+
- Craftsmanship polish (scout inventory: `mock.calls[N][idx]` → `toHaveBeenCalledWith` in `test/lifecycle/subagent.test.ts` and `test/lifecycle/subagent-manager.test.ts`, `settings.ts` `sanitize()` range-check triplication, `createManager` fixture density, two `as any` private-state reaches) — deferred to the `tidy-first` boy-scout path; all clusters scored below the phase-step bar and the scout recommends incidental pickup.
|
|
97
|
+
|
|
98
|
+
[#451]: https://github.com/gotgenes/pi-packages/issues/451
|
|
99
|
+
[#465]: https://github.com/gotgenes/pi-packages/issues/465
|
|
100
|
+
[#466]: https://github.com/gotgenes/pi-packages/issues/466
|
|
101
|
+
[#482]: https://github.com/gotgenes/pi-packages/issues/482
|
|
102
|
+
[#519]: https://github.com/gotgenes/pi-packages/issues/519
|
|
103
|
+
[#563]: https://github.com/gotgenes/pi-packages/issues/563
|
|
104
|
+
[#600]: https://github.com/gotgenes/pi-packages/issues/600
|
|
105
|
+
[#608]: https://github.com/gotgenes/pi-packages/issues/608
|
|
106
|
+
[#610]: https://github.com/gotgenes/pi-packages/issues/610
|
|
107
|
+
[#611]: https://github.com/gotgenes/pi-packages/issues/611
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Phase 3: Remove group-join, ad-hoc RPC; replace output-file
|
|
2
|
+
|
|
3
|
+
Deleted `group-join.ts`, `cross-extension-rpc.ts` (#49).
|
|
4
|
+
Replaced `output-file.ts` with `SessionManager.create()` + `session-dir.ts` (#61).
|
|
5
|
+
Simplified `index.ts` to use direct individual notifications.
|
|
6
|
+
Lifecycle events emitted on `pi.events` for external consumers.
|
|
7
|
+
|
|
8
|
+
## Related issues
|
|
9
|
+
|
|
10
|
+
- #49 — Remove group-join and ad-hoc RPC
|
|
11
|
+
- #61 — Replace output-file with JSONL session transcripts
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Phase 4: Implement and publish SubagentsService
|
|
2
|
+
|
|
3
|
+
Wired `service-adapter.ts` to wrap `AgentManager` and call `publishSubagentsService()` at extension init.
|
|
4
|
+
Model strings are resolved inside the adapter.
|
|
5
|
+
|
|
6
|
+
## Related issues
|
|
7
|
+
|
|
8
|
+
- #48 — Implement and publish SubagentsService
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Phase 5: Decompose index.ts
|
|
2
|
+
|
|
3
|
+
Extracted tools, notifications, activity tracking, event handlers, and the `/agents` command into separate modules.
|
|
4
|
+
Created `SubagentRuntime` factory to hold session-scoped state.
|
|
5
|
+
|
|
6
|
+
## index.ts decomposition
|
|
7
|
+
|
|
8
|
+
The original monolithic `index.ts` has been decomposed into focused modules:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
src/
|
|
12
|
+
├── index.ts - slimmed entry point: init, tool registration
|
|
13
|
+
├── runtime.ts - SubagentRuntime: session-scoped state + methods
|
|
14
|
+
├── tools/
|
|
15
|
+
│ ├── agent-tool.ts - Agent tool definition, parameter validation, dispatch
|
|
16
|
+
│ ├── foreground-runner.ts - foreground execution loop (spinner, streaming, result)
|
|
17
|
+
│ ├── background-spawner.ts - background spawn (activity setup, notification wiring)
|
|
18
|
+
│ ├── get-result-tool.ts - get_subagent_result tool
|
|
19
|
+
│ ├── steer-tool.ts - steer_subagent tool
|
|
20
|
+
│ └── helpers.ts - shared tool utilities (textResult, buildDetails, getStatusNote, ...)
|
|
21
|
+
├── handlers/
|
|
22
|
+
│ ├── lifecycle.ts - session_start, session_before_switch, session_shutdown
|
|
23
|
+
│ └── tool-start.ts - tool_execution_start handler
|
|
24
|
+
├── notification.ts - completion nudges, custom renderer
|
|
25
|
+
├── renderer.ts - notification TUI component
|
|
26
|
+
├── ui/agent-menu.ts - /agents slash command menu (orchestration, listing, settings)
|
|
27
|
+
├── ui/agent-config-editor.ts - agent detail view (edit/delete/eject/disable/enable)
|
|
28
|
+
├── ui/agent-creation-wizard.ts - agent creation (AI-generation and manual-form)
|
|
29
|
+
├── ui/agent-file-ops.ts - AgentFileOps interface + FsAgentFileOps implementation
|
|
30
|
+
├── service-adapter.ts - SubagentsService implementation wrapping AgentManager
|
|
31
|
+
└── (existing domain modules unchanged)
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Each extracted module receives narrow constructor-injected dependencies rather than closing over module-level state.
|
|
35
|
+
Handlers call methods on narrow runtime interfaces - no raw field writes, no `widget!` reach-throughs.
|
|
36
|
+
|
|
37
|
+
## Related issues
|
|
38
|
+
|
|
39
|
+
- #54 — Decompose index.ts
|
|
40
|
+
- #69 — SubagentRuntime factory
|
|
41
|
+
- #70 — Handler extraction
|
|
42
|
+
- #87 — Runtime methods
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Phase 7: Encapsulation and dependency narrowing
|
|
2
|
+
|
|
3
|
+
Every mutable state bag became a class, every dependency bag narrowed to what its consumer uses, every callback became either a method on a collaborator or an event on an observable.
|
|
4
|
+
|
|
5
|
+
## AgentManager decomposition
|
|
6
|
+
|
|
7
|
+
AgentManager was decomposed in three steps to untangle record management, concurrency control, and execution orchestration.
|
|
8
|
+
|
|
9
|
+
### Step 1: Record state machine (#98, #102)
|
|
10
|
+
|
|
11
|
+
Extracted status-transition methods (`markRunning`, `markCompleted`, `markAborted`, `markSteered`, `markError`, `markStopped`, `resetForResume`) onto `AgentRecord`.
|
|
12
|
+
Replaced scattered field writes across 6 sites with encapsulated transition methods.
|
|
13
|
+
Issue #102 consolidated test `AgentRecord` construction into a shared factory.
|
|
14
|
+
|
|
15
|
+
### Step 2: Parent snapshot (#99)
|
|
16
|
+
|
|
17
|
+
Replaced live `ctx: ExtensionContext` capture in `SpawnArgs` with an immutable `ParentSnapshot` data object.
|
|
18
|
+
The snapshot is taken once at spawn time; queued agents execute against frozen state rather than a potentially stale session reference.
|
|
19
|
+
`runAgent()` accepts `ParentSnapshot` instead of `ctx`.
|
|
20
|
+
`pi: ExtensionAPI` was removed from `SpawnArgs` - `runAgent()` accepts a `ShellExec` function instead.
|
|
21
|
+
|
|
22
|
+
### Step 3: Session-event observation (#100)
|
|
23
|
+
|
|
24
|
+
Replaced three-layer callback threading with direct session subscriptions.
|
|
25
|
+
`record-observer.ts` subscribes to the session to update record statistics (tool uses, lifetime usage, compaction count).
|
|
26
|
+
`ui/ui-observer.ts` subscribes to the session to stream UI state (active tools, response text, turn count).
|
|
27
|
+
`SpawnOptions` and `RunOptions` dropped all `on*` callback fields except `onSessionCreated` (which delivers the session object to enable external subscriptions).
|
|
28
|
+
|
|
29
|
+
### Realized impact
|
|
30
|
+
|
|
31
|
+
| Metric | Before | After |
|
|
32
|
+
| --------------------------------- | ------ | ----------------------- |
|
|
33
|
+
| `SpawnOptions` callback fields | 6 | 1 (`onSessionCreated`) |
|
|
34
|
+
| `RunOptions` callback fields | 6 | 1 (`onSessionCreated`) |
|
|
35
|
+
| Callback layers | 3 | 0 (direct subscription) |
|
|
36
|
+
| Live `ctx` references in queue | 1 | 0 (snapshot) |
|
|
37
|
+
| Scattered status-transition sites | 6 | 1 (state machine) |
|
|
38
|
+
|
|
39
|
+
## Encapsulation roadmap
|
|
40
|
+
|
|
41
|
+
Phase 7 encapsulated mutable state into classes, replaced callbacks with semantic components, and narrowed dependency bags.
|
|
42
|
+
|
|
43
|
+
Each step was sequenced so it made the next step easier.
|
|
44
|
+
|
|
45
|
+
### Resolved smells
|
|
46
|
+
|
|
47
|
+
All nine smells identified at the start of Phase 7 were resolved:
|
|
48
|
+
|
|
49
|
+
| Smell | Resolution |
|
|
50
|
+
| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
51
|
+
| Global mutable state | `AgentTypeRegistry` class (#108); `reloadCustomAgents` callback removed from dep bags |
|
|
52
|
+
| Closure bag as class | `NotificationManager` class (#116); `pendingNudges` and timer state are private fields |
|
|
53
|
+
| Mutable state bag | `AgentActivityTracker` class (#110); transition methods replace external writes |
|
|
54
|
+
| Settings relay | `SettingsManager` class (#109); 6 callback fields collapsed to one object |
|
|
55
|
+
| Post-construction mutation | `ExecutionState`, `WorktreeState`, `NotificationState` collaborators (#111); stats behind mutation methods |
|
|
56
|
+
| Fire-and-forget callbacks | `AgentManagerObserver` interface (#112); one observer object replaces 3 closure lambdas |
|
|
57
|
+
| Duplicate `SpawnOptions` | Internal type renamed to `AgentSpawnConfig` (#113); public `SpawnOptions` unchanged |
|
|
58
|
+
| Type dumping ground | `NotificationDetails`, `ParentSnapshot`, `EnvInfo` moved to their natural modules (#116); narrow subsets defined |
|
|
59
|
+
| Wide dependency bags | `AgentToolDeps` 9 → 6, `AgentMenuDeps` 8 → 7 (#114); `emitEvent` removed; description text derived from registry; `agentActivity` narrowed |
|
|
60
|
+
|
|
61
|
+
### Step A: Extract state into classes (foundation, parallel)
|
|
62
|
+
|
|
63
|
+
These three extractions are independent and can proceed in any order.
|
|
64
|
+
Each eliminates a category of global/closure state and gives orphaned callbacks a natural home.
|
|
65
|
+
|
|
66
|
+
#### A1. AgentTypeRegistry class (#108)
|
|
67
|
+
|
|
68
|
+
Wrapped the module-scoped `agents` Map and free functions in `agent-types.ts` into an injectable class.
|
|
69
|
+
`reloadCustomAgents` callback removed from `AgentToolDeps` and `AgentMenuDeps`; replaced by `registry.reload()`.
|
|
70
|
+
`DEFAULT_AGENT_NAMES` moved from `types.ts` to the registry.
|
|
71
|
+
|
|
72
|
+
#### A2. SettingsManager class (#109, #118)
|
|
73
|
+
|
|
74
|
+
Encapsulated settings load/save/apply cycle into `SettingsManager` (in `settings.ts`).
|
|
75
|
+
Owns `defaultMaxTurns`, `graceTurns`, `maxConcurrent` with normalizing property accessors.
|
|
76
|
+
Added `applyMaxConcurrent(n)`, `applyDefaultMaxTurns(n)`, `applyGraceTurns(n)` - each owns the full consequence chain: normalize → set in memory → notify callback → persist → emit event → return toast.
|
|
77
|
+
The 6 settings-related fields in `AgentMenuDeps` collapsed to `settings: AgentMenuSettings`.
|
|
78
|
+
|
|
79
|
+
#### A3. AgentActivityTracker class (#110)
|
|
80
|
+
|
|
81
|
+
Wrapped the 7-field mutable `AgentActivity` interface in an `AgentActivityTracker` class (`src/ui/agent-activity-tracker.ts`).
|
|
82
|
+
`ui-observer.ts` calls transition methods; consumers use read-only accessors.
|
|
83
|
+
The shared map on `SubagentRuntime` is `Map<string, AgentActivityTracker>`.
|
|
84
|
+
|
|
85
|
+
### Step B: Split AgentRecord lifecycle state (#111)
|
|
86
|
+
|
|
87
|
+
Split post-construction mutation into phase-specific collaborators, each born complete:
|
|
88
|
+
|
|
89
|
+
- **`ExecutionState`** (`session`, `outputFile`) - constructed in `onSessionCreated`.
|
|
90
|
+
- **`WorktreeState`** (`path`, `branch`, `cleanupResult`) - constructed at worktree setup.
|
|
91
|
+
- **`NotificationState`** (`toolCallId`, `resultConsumed`) - constructed by `AgentManager.spawn()` when `toolCallId` is provided.
|
|
92
|
+
- **`pendingSteers`** moved to `Map<string, string[]>` on `AgentManager`.
|
|
93
|
+
- Stats encapsulated behind mutation methods with read-only getters.
|
|
94
|
+
- `AgentRecordInit` trimmed from 19 optional fields to 4 construction-time fields.
|
|
95
|
+
|
|
96
|
+
### Step C: Replace AgentManager callbacks with observer (#112)
|
|
97
|
+
|
|
98
|
+
`AgentManagerObserver` interface replaces `onStart`/`onComplete`/`onCompact`.
|
|
99
|
+
`index.ts` constructs one observer object instead of 3 closure lambdas.
|
|
100
|
+
`AgentManagerOptions` drops from 9 → 7 fields.
|
|
101
|
+
|
|
102
|
+
### Step D: Disambiguate SpawnOptions and narrow dependency bags
|
|
103
|
+
|
|
104
|
+
#### D1. Disambiguate SpawnOptions (#113)
|
|
105
|
+
|
|
106
|
+
Internal `SpawnOptions` in `agent-manager.ts` renamed to `AgentSpawnConfig`.
|
|
107
|
+
Public `SpawnOptions` in `service.ts` unchanged.
|
|
108
|
+
|
|
109
|
+
#### D2. Narrow AgentToolDeps and AgentMenuDeps (#114)
|
|
110
|
+
|
|
111
|
+
| Bag | Before | After | How |
|
|
112
|
+
| --------------- | -------- | ----- | ----------------------------------------------------------------------------------------------------------- |
|
|
113
|
+
| `AgentToolDeps` | 9 fields | 6 | `emitEvent` → observer; `typeListText`/`availableTypesText` derived from registry; `agentActivity` narrowed |
|
|
114
|
+
| `AgentMenuDeps` | 8 fields | 7 | Dead `emitEvent` removed; `agentActivity` narrowed to read-only `AgentActivityReader` |
|
|
115
|
+
|
|
116
|
+
### Step E: Decompose large files and relocate types
|
|
117
|
+
|
|
118
|
+
#### E1. Split agent-tool.ts foreground/background (#115)
|
|
119
|
+
|
|
120
|
+
Extracted `foreground-runner.ts` (~175 lines) and `background-spawner.ts` (~116 lines).
|
|
121
|
+
`agent-tool.ts` reduced from 579 → 411 lines.
|
|
122
|
+
|
|
123
|
+
#### E2. Type housekeeping (#116)
|
|
124
|
+
|
|
125
|
+
- Moved `NotificationDetails`, `ParentSnapshot`, `EnvInfo` to their natural modules.
|
|
126
|
+
- Converted `createNotificationSystem` closure to `NotificationManager` class.
|
|
127
|
+
- Converted `ConversationViewer` constructor from 7 positional parameters to `ConversationViewerOptions` bag.
|
|
128
|
+
- Defined `AgentIdentity` and `AgentPromptConfig` narrow subsets; `buildAgentPrompt` narrowed to `AgentPromptConfig`.
|
|
129
|
+
|
|
130
|
+
### Phase 7 results
|
|
131
|
+
|
|
132
|
+
| Metric | Before | After |
|
|
133
|
+
| ------------------------------------------ | ------ | ----- |
|
|
134
|
+
| Module-scoped mutable state | 1 | 0 |
|
|
135
|
+
| Closure-bag "classes" | 2 | 0 |
|
|
136
|
+
| Externally-mutated state bags | 2 | 0 |
|
|
137
|
+
| `AgentManagerOptions` fields | 9 | 7 |
|
|
138
|
+
| `AgentToolDeps` fields | 9 | 6 |
|
|
139
|
+
| `AgentMenuDeps` fields | 13 | 7 |
|
|
140
|
+
| `SpawnOptions` callback fields | 6 | 1 |
|
|
141
|
+
| `RunOptions` callback fields | 6 | 1 |
|
|
142
|
+
| Callbacks threaded through deps | 8 | 0 |
|
|
143
|
+
| Types in `types.ts` without a natural home | 4 | 0 |
|
|
144
|
+
|
|
145
|
+
### Dependency graph
|
|
146
|
+
|
|
147
|
+
```mermaid
|
|
148
|
+
flowchart LR
|
|
149
|
+
A1["A1: Registry"] --> D2["D2: Narrow deps"]
|
|
150
|
+
A2["A2: Settings"] --> A2b["A2b: Apply"] --> D2
|
|
151
|
+
A3["A3: Activity Tracker"] --> D2
|
|
152
|
+
B["B: Record lifecycle"] --> D2
|
|
153
|
+
B --> C["C: Observer"] --> D1["D1: SpawnOptions"] --> D2
|
|
154
|
+
D2 --> E1["E1: agent-tool split"]
|
|
155
|
+
A1 --> E2["E2: Type housekeeping"]
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Related issues
|
|
159
|
+
|
|
160
|
+
- #98 — Record state machine
|
|
161
|
+
- #99 — Parent snapshot
|
|
162
|
+
- #100 — Session-event observation
|
|
163
|
+
- #102 — Consolidated test AgentRecord construction
|
|
164
|
+
- #108 — AgentTypeRegistry class
|
|
165
|
+
- #109 — SettingsManager class
|
|
166
|
+
- #110 — AgentActivityTracker class
|
|
167
|
+
- #111 — Split AgentRecord lifecycle state
|
|
168
|
+
- #112 — Replace AgentManager callbacks with observer
|
|
169
|
+
- #113 — Disambiguate SpawnOptions
|
|
170
|
+
- #114 — Narrow AgentToolDeps and AgentMenuDeps
|
|
171
|
+
- #115 — Split agent-tool.ts foreground/background
|
|
172
|
+
- #116 — Type housekeeping
|
|
173
|
+
- #118 — SettingsManager apply methods
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Phase 8: Testability, display extraction, and menu decomposition
|
|
2
|
+
|
|
3
|
+
Eliminated `vi.mock()` module mocking in the two most fragile test suites by injecting IO-touching collaborators; consolidated shared test fixtures; extracted display helpers into a reusable module; decomposed the largest UI file.
|
|
4
|
+
|
|
5
|
+
Steps G and H eliminated 11 of the original 12 `vi.mock()` calls in the runner tests, removing fragile call-sequence assertions in favour of injected stubs.
|
|
6
|
+
Step G resolved `session-config.test.ts`; Step H resolved both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`.
|
|
7
|
+
|
|
8
|
+
The display and menu improvements were identified during Phase 7 but deferred because they did not gate encapsulation work.
|
|
9
|
+
The display extraction unblocked menu decomposition.
|
|
10
|
+
|
|
11
|
+
## Test pain points (resolved)
|
|
12
|
+
|
|
13
|
+
| Symptom | Resolution |
|
|
14
|
+
| ------------------------------------------------------------- | -------------------------------------------------------------- |
|
|
15
|
+
| 7 `vi.mock()` calls in `agent-runner.test.ts` | Step H (#133): injected `RunnerIO` stubs |
|
|
16
|
+
| 7 `vi.mock()` calls in `agent-runner-extension-tools.test.ts` | Step H (#133): same |
|
|
17
|
+
| 52 `as any` casts across test suite | Step I (#134): reduced to 15 |
|
|
18
|
+
| 3× duplicated `mockSession()` | Step F (#131): shared `createMockSession()` in `test/helpers/` |
|
|
19
|
+
| 3× duplicated `makeDeps()` | Step F (#131): shared `createToolDeps()` in `test/helpers/` |
|
|
20
|
+
|
|
21
|
+
The well-designed test suites - `agent-manager.test.ts` (1 mock, DI via `AgentRunner` interface), `notification.test.ts` (0 mocks, pure functions + DI), and `agent-tool.test.ts` (0 mocks, tests via deps bag) - confirmed the pattern: modules that accept collaborators through injection produce resilient tests; modules that import collaborators directly produce fragile mock-heavy tests.
|
|
22
|
+
|
|
23
|
+
## Step F: Shared test fixtures (#131)
|
|
24
|
+
|
|
25
|
+
Consolidated duplicated mock factories into `test/helpers/`.
|
|
26
|
+
|
|
27
|
+
1. `createMockSession()` - subscribable event bus with `emit()` helper; replaced 3 hand-rolled copies.
|
|
28
|
+
2. `createToolDeps()` - builds `AgentToolDeps` with sensible defaults and override support; replaced 3 `makeDeps()` copies.
|
|
29
|
+
3. `makeRecord()` - `AgentRecord` factory with sensible defaults; replaced scattered inline construction.
|
|
30
|
+
4. `STUB_CTX` - shared stub `ExtensionContext` constant; centralised unavoidable bridge casts.
|
|
31
|
+
|
|
32
|
+
Impact: reduced test boilerplate; single source of truth for mock shapes; changes to dep interfaces propagate automatically.
|
|
33
|
+
|
|
34
|
+
## Step G: Inject IO collaborators into session-config (#132)
|
|
35
|
+
|
|
36
|
+
`assembleSessionConfig` now accepts `io: AssemblerIO` as a required parameter.
|
|
37
|
+
`index.ts` constructs the real `AssemblerIO` from direct imports via the `RunnerIO.assemblerIO` field (wired in Step H).
|
|
38
|
+
`session-config.test.ts` injects stubs - all 4 `vi.mock()` calls eliminated, assertions shifted to `SessionConfig` output properties.
|
|
39
|
+
|
|
40
|
+
## Step H: Inject SDK boundary into agent-runner (#133)
|
|
41
|
+
|
|
42
|
+
`runAgent()` now accepts `io: RunnerIO` as a required parameter bundling all IO collaborators: `detectEnv`, `getAgentDir`, `createResourceLoader`, `deriveSessionDir`, `createSessionManager`, `createSettingsManager`, `createSession`, and `assemblerIO`.
|
|
43
|
+
|
|
44
|
+
`createAgentRunner(io: RunnerIO): AgentRunner` factory captures the boundary at construction time so `AgentManager` and the `AgentRunner` interface remain unchanged.
|
|
45
|
+
`index.ts` constructs the real `RunnerIO` from Pi SDK imports and sibling modules.
|
|
46
|
+
|
|
47
|
+
Impact: all 7 `vi.mock()` calls eliminated from both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`; tests verify behavior (turn limits, tool filtering, response collection) through injected stubs; SDK imports moved to the extension entry point.
|
|
48
|
+
|
|
49
|
+
## Step I: Reduce `as any` casts in tests (#134)
|
|
50
|
+
|
|
51
|
+
Reduced `as any` count from 93 to 15 (plus 13 explicit `as unknown as T` bridge casts).
|
|
52
|
+
|
|
53
|
+
Production changes:
|
|
54
|
+
|
|
55
|
+
- `ResourceLoaderOptions.appendSystemPromptOverride` typed to match `DefaultResourceLoaderOptions`; `createResourceLoader` factory cast removed from `index.ts`.
|
|
56
|
+
- `CreateSessionOptions.settingsManager` / `RunnerIO.createSettingsManager` typed as `SettingsManager`.
|
|
57
|
+
- `WidgetLike` interface in `runtime.ts` narrows the widget field.
|
|
58
|
+
- Local `ToolCallContent` / `BashExecutionMessage` type guards replace `as any` duck-typing in `conversation-viewer.ts` and `agent-runner.ts`.
|
|
59
|
+
- `textResult()` return no longer casts `details as any`.
|
|
60
|
+
- `toAgentSession()` helper and `STUB_CTX` constant centralise unavoidable bridge casts.
|
|
61
|
+
|
|
62
|
+
Remaining 15 `as any` casts are: 8 menu-handler `ctx as any` (deferred - requires `AgentManager.spawn` to accept `ParentSnapshot` directly), 2 `print-mode.test.ts` (same ExtensionContext/API pattern), 2 private-field test access, 1 `createSession` SDK bridge in `index.ts`, 1 `foreground-runner.ts` `AgentToolResult<any>` detail, 1 `stub-ctx.ts` comment.
|
|
63
|
+
|
|
64
|
+
## Step J: Extract display helpers (#135)
|
|
65
|
+
|
|
66
|
+
`ui/display.ts` now contains all pure formatters, display helpers, constants, and shared types (`Theme`, `AgentDetails`).
|
|
67
|
+
`agent-widget.ts` dropped from 522 → ~340 lines.
|
|
68
|
+
All consumer modules (menu, tools, renderer, conversation viewer) import from `ui/display.ts` directly.
|
|
69
|
+
`test/agent-widget.test.ts` renamed to `test/display.test.ts`.
|
|
70
|
+
|
|
71
|
+
## Step K: Decompose agent-menu.ts (#136)
|
|
72
|
+
|
|
73
|
+
`agent-menu.ts` (668 lines) decomposed into four modules:
|
|
74
|
+
|
|
75
|
+
1. `ui/agent-file-ops.ts` - `AgentFileOps` interface (`exists`, `read`, `write`, `remove`, `ensureDir`, `findAgentFile`) + `FsAgentFileOps` production implementation.
|
|
76
|
+
2. `ui/agent-config-editor.ts` - `showAgentDetail` with edit/delete/reset/eject/disable/enable transitions (~200 lines).
|
|
77
|
+
3. `ui/agent-creation-wizard.ts` - AI-generation and manual-form creation paths (~250 lines).
|
|
78
|
+
4. `ui/agent-menu.ts` - menu orchestration, agent listing, running-agent viewer, settings form (~300 lines).
|
|
79
|
+
|
|
80
|
+
Impact: `agent-menu.ts` dropped from 668 → 296 lines; extracted modules receive `AgentFileOps` via injection; `vi.mock("node:fs")` eliminated from `agent-menu.test.ts`.
|
|
81
|
+
|
|
82
|
+
## Step dependencies
|
|
83
|
+
|
|
84
|
+
```mermaid
|
|
85
|
+
flowchart LR
|
|
86
|
+
subgraph testability["Testability track"]
|
|
87
|
+
F["F: Shared fixtures"] --> G["G: session-config IO"] --> H["H: agent-runner SDK"] --> I["I: Reduce as-any"]
|
|
88
|
+
end
|
|
89
|
+
subgraph display["Display track"]
|
|
90
|
+
J["J: Display extraction"] --> K["K: Menu decomposition"]
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
The two tracks are independent and can proceed in parallel.
|
|
95
|
+
|
|
96
|
+
## Related issues
|
|
97
|
+
|
|
98
|
+
- #131 — Shared test fixtures
|
|
99
|
+
- #132 — Inject IO collaborators into session-config
|
|
100
|
+
- #133 — Inject SDK boundary into agent-runner
|
|
101
|
+
- #134 — Reduce as-any casts in tests
|
|
102
|
+
- #135 — Extract display helpers
|
|
103
|
+
- #136 — Decompose agent-menu.ts
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Phase 9: Observation consolidation, ctx elimination, and remaining mocks
|
|
2
|
+
|
|
3
|
+
Target: consolidate the dual observation model so stats live in one place; remove `ExtensionContext` from all internal APIs; eliminate remaining `vi.mock()` calls and `as any` casts; split widget rendering from lifecycle; apply dependency bag convention.
|
|
4
|
+
|
|
5
|
+
## Current smells
|
|
6
|
+
|
|
7
|
+
| Smell | Location | Evidence | Severity |
|
|
8
|
+
| ------------------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ | -------- |
|
|
9
|
+
| `execute` does config resolution for its callees | `agent-tool.ts` (145-line `execute`) | ~60 lines unpack config, resolve model, compute metadata, repack into 16-field bags for spawners; `ctx` threaded 4 layers deep | Medium |
|
|
10
|
+
| ~~Wide `ctx` in menu handlers~~ | ~~`agent-menu.ts`, `agent-config-editor.ts`, `agent-creation-wizard.ts`~~ | Resolved by #146: `MenuUI` interface introduced; 42 `ctx as any` casts eliminated across 5 test files | Done |
|
|
11
|
+
| ~~Direct SDK import in `conversation-viewer.ts`~~ | ~~`conversation-viewer.test.ts`~~ | Resolved by #147: `wrapText` injected via `ConversationViewerOptions`; `vi.mock("@earendil-works/pi-tui")` eliminated | Done |
|
|
12
|
+
| ~~Widget mixes rendering, lifecycle, and state~~ | ~~`agent-widget.ts` (370 lines)~~ | Resolved by #148: rendering extracted to `widget-renderer.ts`; widget is now 198 lines | Done |
|
|
13
|
+
| `deps.` prefix noise in function bodies | remaining modules across tools, UI, service-adapter | Functions accept a `deps` bag and access every field as `deps.foo`; hides real dependencies and lengthens every call line | Low |
|
|
14
|
+
|
|
15
|
+
## Dependency bag convention
|
|
16
|
+
|
|
17
|
+
Applied incrementally as each step touches a module:
|
|
18
|
+
|
|
19
|
+
- **≤4 fields** - accept as plain parameters; drop the interface.
|
|
20
|
+
- **≥5 fields** - keep a named interface but destructure in the function signature (`{ manager, widget }: ForegroundDeps`) so the function body uses bare names, not `deps.foo`.
|
|
21
|
+
|
|
22
|
+
This eliminates the `deps.` prefix noise across ~124 callsites in 12 modules.
|
|
23
|
+
|
|
24
|
+
## Step L: Consolidate observation model (#144)
|
|
25
|
+
|
|
26
|
+
Removed `_toolUses` and `_lifetimeUsage` from `AgentActivityTracker`.
|
|
27
|
+
UI consumers read stats from `AgentRecord` instead of the tracker.
|
|
28
|
+
The UI observer retains event subscriptions for re-render triggers but no longer accumulates stats independently.
|
|
29
|
+
|
|
30
|
+
Add `session` and `outputFile` convenience getters on `AgentRecord` to hide the `execution?.` traversal.
|
|
31
|
+
The 15+ callsites that navigate `record.execution?.session` simplify to `record.session`.
|
|
32
|
+
|
|
33
|
+
Apply the dependency bag convention to touched modules: `NotificationDeps` (4 fields) becomes plain parameters on `NotificationManager` constructor.
|
|
34
|
+
|
|
35
|
+
Impact: eliminates dual counting; removes `??` fallback pattern from widget and conversation viewer; hides `ExecutionState` structure from consumers.
|
|
36
|
+
|
|
37
|
+
## Step M: Decompose execute and push ExtensionContext to the boundary (#145)
|
|
38
|
+
|
|
39
|
+
Extracted config resolution into `resolveSpawnConfig` (pure function in `spawn-config.ts`).
|
|
40
|
+
Injected three collaborators (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) into `createAgentTool` so `execute` no longer reads `ctx` beyond `ctx.ui` (already delegated to `widget.setUICtx`).
|
|
41
|
+
`AgentManager.spawn()` and `spawnAndWait()` accept `ParentSnapshot` instead of `ExtensionContext`.
|
|
42
|
+
`service-adapter.ts` calls `buildParentSnapshot(session.ctx)` at its boundary.
|
|
43
|
+
`foreground-runner` and `background-spawner` receive `ResolvedSpawnConfig` + domain values (`snapshot`, `parentSessionFile`, `parentSessionId`) instead of `ctx`.
|
|
44
|
+
|
|
45
|
+
Dissolved `ForegroundDeps`, `BackgroundDeps`, and `AdapterDeps` into plain parameters.
|
|
46
|
+
`AgentToolDeps` is destructured in the `createAgentTool` signature.
|
|
47
|
+
|
|
48
|
+
After this step, `ExtensionContext` appears only in:
|
|
49
|
+
|
|
50
|
+
- `index.ts` closures (wired at extension startup)
|
|
51
|
+
- `service-adapter.ts` (cross-extension boundary)
|
|
52
|
+
- Menu handlers (addressed by Step N)
|
|
53
|
+
|
|
54
|
+
Impact: `execute` dropped from ~145 to ~25 lines; eliminated 16-field parameter bags; eliminated `vi.mock("../src/parent-snapshot.js")` in `agent-manager.test.ts`; foreground/background runner tests no longer need `ctx` mocks; `AgentManager` operates entirely on domain types.
|
|
55
|
+
|
|
56
|
+
## Step N: Narrow UI context for menu handlers (#146)
|
|
57
|
+
|
|
58
|
+
Defined `MenuUI` interface (exported from `agent-menu.ts`) with `select`, `confirm`, `input`, `notify`, `editor`, and `custom` methods — the exact subset `ctx.ui` methods used by menu handlers.
|
|
59
|
+
All inner functions in `agent-menu.ts`, `agent-config-editor.ts`, and `agent-creation-wizard.ts` now accept `(ui: MenuUI)` instead of `(ctx: ExtensionContext)`.
|
|
60
|
+
`index.ts` passes `ctx.ui`, `ctx.modelRegistry`, and `buildParentSnapshot(ctx)` to the handler.
|
|
61
|
+
|
|
62
|
+
`AgentMenuManager.spawnAndWait` and `WizardManager.spawnAndWait` both accept `ParentSnapshot` (enabled by Step M).
|
|
63
|
+
Creation wizard threads `parentSnapshot` from `showCreateWizard(ui, parentSnapshot)` → `showGenerateWizard(ui, parentSnapshot, targetDir)` → `manager.spawnAndWait(parentSnapshot, ...)`.
|
|
64
|
+
|
|
65
|
+
Applied the dependency bag convention:
|
|
66
|
+
|
|
67
|
+
- `AgentConfigEditorDeps` (4 fields), `GetResultDeps` (4 fields), `SteerToolDeps` (4 fields) dissolved into plain parameters.
|
|
68
|
+
- `AgentMenuDeps` (8 fields) and `AgentCreationWizardDeps` (5 fields) kept as interfaces, destructured in the function signature.
|
|
69
|
+
|
|
70
|
+
After Steps M and N, `ExtensionContext` appears only at true boundaries: `index.ts` closures and `service-adapter.ts` (cross-extension bridge).
|
|
71
|
+
|
|
72
|
+
Impact: eliminated 42 `ctx as any` casts across 5 test files (`agent-menu.test.ts`: 8, `agent-config-editor.test.ts`: 20, `agent-creation-wizard.test.ts`: 14); tests construct plain `MenuUI`-shaped objects with no cast.
|
|
73
|
+
|
|
74
|
+
## Step O: Inject text wrapping into ConversationViewer (#147)
|
|
75
|
+
|
|
76
|
+
Accepted `wrapText: (text: string, width: number) => string[]` via `ConversationViewerOptions`.
|
|
77
|
+
`agent-menu.ts` passes the real `wrapTextWithAnsi` import at the `ConversationViewer` construction site.
|
|
78
|
+
Tests inject a stub or the real function directly via options — no module-level mock needed.
|
|
79
|
+
|
|
80
|
+
Applied the dependency bag convention: `ConversationViewerOptions` is now destructured in the constructor signature.
|
|
81
|
+
|
|
82
|
+
Impact: eliminated the hoisted `vi.mock("@earendil-works/pi-tui")` from `conversation-viewer.test.ts`; 1 test deleted (mock-mechanism sentinel); net −1 test.
|
|
83
|
+
|
|
84
|
+
## Step P: Split AgentWidget rendering (#148)
|
|
85
|
+
|
|
86
|
+
Extracted pure rendering functions (`renderWidgetLines`, `renderFinishedLine`, `renderRunningLines`) from `AgentWidget` into `ui/widget-renderer.ts`.
|
|
87
|
+
The widget is now a thin lifecycle/polling wrapper (198 lines, down from 374) that delegates to pure render functions.
|
|
88
|
+
Rendering functions receive data (agent list, activity map, registry) and return formatted strings - testable without widget lifecycle. 23 new unit tests cover all status variants, overflow, tree connectors, and empty states.
|
|
89
|
+
|
|
90
|
+
## Step dependencies
|
|
91
|
+
|
|
92
|
+
```mermaid
|
|
93
|
+
flowchart LR
|
|
94
|
+
subgraph observation["Observation track"]
|
|
95
|
+
L["L: Consolidate observation #144"] --> P["P: Split widget rendering #148"]
|
|
96
|
+
end
|
|
97
|
+
subgraph ctx["ctx elimination track"]
|
|
98
|
+
M["M: Decompose execute / push ctx #145"] --> N["N: Narrow UI context #146"]
|
|
99
|
+
end
|
|
100
|
+
O["O: Inject text wrapping #147"]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The three tracks are independent of each other.
|
|
104
|
+
|
|
105
|
+
## Projected impact
|
|
106
|
+
|
|
107
|
+
| Metric | Before | After |
|
|
108
|
+
| ---------------------------------- | ------------------------ | ------------------------ |
|
|
109
|
+
| `vi.mock()` calls remaining | 4 | 1 (`print-mode.test.ts`) |
|
|
110
|
+
| `as any` casts remaining | 45 | ~5 |
|
|
111
|
+
| Independent tool-use counters | 2 | 1 |
|
|
112
|
+
| `record.execution?.` traversals | 15+ | 0 |
|
|
113
|
+
| `ExtensionContext` in domain types | 1 (`AgentManager.spawn`) | 0 |
|
|
114
|
+
| `deps.` prefix accesses | ~124 | 0 |
|
|
115
|
+
|
|
116
|
+
## Related issues
|
|
117
|
+
|
|
118
|
+
- #144 — Consolidate observation model
|
|
119
|
+
- #145 — Decompose execute and push ExtensionContext to boundary
|
|
120
|
+
- #146 — Narrow UI context for menu handlers
|
|
121
|
+
- #147 — Inject text wrapping into ConversationViewer
|
|
122
|
+
- #148 — Split AgentWidget rendering
|