@gotgenes/pi-subagents 16.5.0 → 17.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 +18 -0
- package/dist/public.d.ts +4 -1
- package/docs/architecture/architecture.md +39 -31
- package/docs/plans/0421-migrate-activity-readers-to-record-getters.md +309 -0
- package/docs/plans/0422-delete-activity-tracker-ui-observer.md +204 -0
- package/docs/plans/0423-widget-self-drive-from-lifecycle.md +264 -0
- package/docs/plans/0424-drop-widget-dep-from-subagent-tool.md +166 -0
- package/docs/plans/0425-reconcile-subagent-events-contract.md +144 -0
- package/docs/retro/0420-fold-run-metrics-live-activity-onto-record.md +45 -0
- package/docs/retro/0421-migrate-activity-readers-to-record-getters.md +105 -0
- package/docs/retro/0422-delete-activity-tracker-ui-observer.md +97 -0
- package/docs/retro/0423-widget-self-drive-from-lifecycle.md +112 -0
- package/docs/retro/0424-drop-widget-dep-from-subagent-tool.md +89 -0
- package/docs/retro/0425-reconcile-subagent-events-contract.md +46 -0
- package/package.json +1 -1
- package/src/index.ts +12 -6
- package/src/observation/composite-subagent-observer.ts +49 -0
- package/src/observation/notification.ts +3 -12
- package/src/observation/subagent-events-observer.ts +1 -4
- package/src/runtime.ts +0 -6
- package/src/service/service.ts +4 -1
- package/src/tools/agent-tool.ts +1 -36
- package/src/tools/background-spawner.ts +1 -25
- package/src/tools/foreground-runner.ts +9 -34
- package/src/tools/helpers.ts +5 -4
- package/src/types.ts +1 -1
- package/src/ui/agent-menu.ts +0 -12
- package/src/ui/agent-widget.ts +56 -17
- package/src/ui/conversation-viewer.ts +4 -9
- package/src/ui/widget-renderer.ts +14 -22
- package/src/ui/agent-activity-tracker.ts +0 -84
- package/src/ui/ui-observer.ts +0 -61
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 422
|
|
3
|
+
issue_title: "pi-subagents: delete AgentActivityTracker and ui-observer, drop the activity map from the core"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Delete AgentActivityTracker and ui-observer, drop the activity map from the core
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
The activity tier is the UI's live-streaming bookkeeping, but today it lives inside the core.
|
|
11
|
+
A `Map<string, AgentActivityTracker>` sits on the core composition root (`SubagentRuntime`), and the two spawn tools construct trackers, open a second session subscription (`subscribeUIObserver`), and populate or delete the map.
|
|
12
|
+
After Phase 18 Step 2 ([#421]) migrated every reader to the record getters, nothing reads the trackers or the map any longer — they are dead bookkeeping the core still maintains.
|
|
13
|
+
This is the third step of the Phase 18 spine (activity-tier disentanglement): delete `AgentActivityTracker` and `ui-observer`, and remove the activity map from `SubagentRuntime` and the spawn tools.
|
|
14
|
+
|
|
15
|
+
## Goals
|
|
16
|
+
|
|
17
|
+
- Delete `src/ui/agent-activity-tracker.ts` and `src/ui/ui-observer.ts` (~145 LOC).
|
|
18
|
+
- Remove `agentActivity` from `SubagentRuntime` (`src/runtime.ts`) and from the `AgentToolRuntime` slice (`src/tools/agent-tool.ts`).
|
|
19
|
+
- Stop constructing trackers, calling `subscribeUIObserver` / `setSession`, and populating or deleting the map in `src/tools/foreground-runner.ts` and `src/tools/background-spawner.ts`.
|
|
20
|
+
- Remove the now-unused `AgentActivityAccess` interface and the `agentActivity` parameter from both spawner functions.
|
|
21
|
+
- Leave each child session with exactly one session subscription (the core's own `record-observer`) and zero UI state on the runtime.
|
|
22
|
+
- Keep the existing test suite green at every commit.
|
|
23
|
+
|
|
24
|
+
This is **not** a breaking change.
|
|
25
|
+
`AgentActivityTracker`, `ui-observer`, and `SubagentRuntime.agentActivity` are internal symbols — none appear in the public service surface (`src/service/service.ts`) or the settings entry (`src/layered-settings.ts`).
|
|
26
|
+
Observable behavior is unchanged; commits use `refactor:` / `docs:`.
|
|
27
|
+
|
|
28
|
+
## Non-Goals
|
|
29
|
+
|
|
30
|
+
- Changing how the widget starts/stops its timer — the spawn tools still call `widget.ensureTimer` / `update` / `markFinished` after this step; that decoupling is Phase 18 Step 4 ([#423]).
|
|
31
|
+
- Dropping the `widget` or activity-map constructor params from the `subagent` tool — that is Phase 18 Step 5 ([#424]).
|
|
32
|
+
- Touching `record-observer.ts`, `SubagentState`, or the record getters added in Steps 1–2.
|
|
33
|
+
- Reconciling the public event contract (Step 6) or any UI-distribution decision (Step 8).
|
|
34
|
+
|
|
35
|
+
## Background
|
|
36
|
+
|
|
37
|
+
Relevant modules and their current relationships:
|
|
38
|
+
|
|
39
|
+
- `src/ui/agent-activity-tracker.ts` — `AgentActivityTracker` class (active tools, response text, turn count, session ref).
|
|
40
|
+
Constructed per-spawn in both spawner tools; written via transition methods; no longer read after [#421].
|
|
41
|
+
- `src/ui/ui-observer.ts` — `subscribeUIObserver(session, tracker, onUpdate?)` opens a second session subscription parallel to `record-observer` and streams events into the tracker.
|
|
42
|
+
- `src/runtime.ts` — `SubagentRuntime.agentActivity: Map<string, AgentActivityTracker>`, the UI streaming-state map on the core composition root.
|
|
43
|
+
- `src/tools/agent-tool.ts` — declares the `AgentActivityAccess` interface and the `AgentToolRuntime.agentActivity` field; passes `this.runtime.agentActivity` into both spawners.
|
|
44
|
+
- `src/tools/foreground-runner.ts` — constructs `fgState`, calls `setSession` + `subscribeUIObserver`, populates and deletes the map; its `streamUpdate` already reads activity off the record (`recordRef`), not the tracker.
|
|
45
|
+
- `src/tools/background-spawner.ts` — constructs `bgState`, calls `setSession` + `subscribeUIObserver` inside an `observer.onSessionCreated`, populates the map.
|
|
46
|
+
|
|
47
|
+
The architecture doc records this as Phase 18 Step 3 (`docs/architecture/architecture.md`, roadmap and step-dependency diagram), with the explicit outcome "−145 LOC, one session subscription per child, runtime holds zero UI state."
|
|
48
|
+
|
|
49
|
+
AGENTS.md constraints that apply:
|
|
50
|
+
|
|
51
|
+
- Run `pnpm fallow dead-code` locally before pushing — deleting modules can orphan sibling exports.
|
|
52
|
+
- pi-subagents is a narrow core with no UI policy; removing UI bookkeeping from the core moves it in the documented direction (dependency arrows point inward).
|
|
53
|
+
|
|
54
|
+
## Design Overview
|
|
55
|
+
|
|
56
|
+
This step is a deletion enabled by Step 2, not a new abstraction.
|
|
57
|
+
The two structural wins are removing an output-argument pattern (`agentActivity.set(...)` / `.delete(...)` writes into a received map) and narrowing two function signatures by dropping the `agentActivity` parameter.
|
|
58
|
+
|
|
59
|
+
### Foreground runner
|
|
60
|
+
|
|
61
|
+
The `observer.onSessionCreated` callback stays — it is still the only place `recordRef` and `fgId` are bound mid-flight (while `spawnAndWait` is in progress) and where `widget.ensureTimer()` fires.
|
|
62
|
+
Only the tracker lines are removed.
|
|
63
|
+
The post-change callback:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
observer: {
|
|
67
|
+
onSessionCreated: (agent) => {
|
|
68
|
+
recordRef = agent;
|
|
69
|
+
fgId = agent.id;
|
|
70
|
+
widget.ensureTimer();
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The `fgState` construction, the `unsubUI` variable and its two `unsubUI?.()` cleanup calls, and `agentActivity.set` / `agentActivity.delete` all go.
|
|
76
|
+
`streamUpdate` is unchanged — it already reads `recordRef?.turnCount`, `recordRef?.activeTools`, `recordRef?.responseText`.
|
|
77
|
+
Re-renders are driven by the existing 80 ms spinner interval, which reads fresh record state populated by the core's `record-observer`.
|
|
78
|
+
|
|
79
|
+
### Background spawner
|
|
80
|
+
|
|
81
|
+
`onSessionCreated` in the background spawner did **only** tracker work (`bgState.setSession`, `subscribeUIObserver`), so the entire `observer` block is removed from the spawn opts.
|
|
82
|
+
The post-spawn `widget.ensureTimer()` and `widget.update()` calls (outside the observer) stay.
|
|
83
|
+
The `bgState` construction and `agentActivity.set(id, bgState)` go.
|
|
84
|
+
|
|
85
|
+
### Agent tool
|
|
86
|
+
|
|
87
|
+
`AgentTool` stops passing `this.runtime.agentActivity` to both spawners (Step "remove param").
|
|
88
|
+
The `AgentActivityAccess` interface, the `AgentToolRuntime.agentActivity` field, and the `import { AgentActivityTracker }` are removed once no caller references them.
|
|
89
|
+
|
|
90
|
+
### Runtime
|
|
91
|
+
|
|
92
|
+
`SubagentRuntime.agentActivity` and its `import type { AgentActivityTracker }` are removed, along with the field's doc comment.
|
|
93
|
+
The session-context methods and `createSubagentRuntime` are untouched.
|
|
94
|
+
|
|
95
|
+
## Module-Level Changes
|
|
96
|
+
|
|
97
|
+
Source:
|
|
98
|
+
|
|
99
|
+
- `src/tools/foreground-runner.ts` — remove `AgentActivityTracker` import, `subscribeUIObserver` import, `AgentActivityAccess` import, the `agentActivity` parameter, `fgState`, `unsubUI` (+ both `unsubUI?.()` calls), `agentActivity.set`, `agentActivity.delete`; trim the doc comment that lists "AgentActivityTracker creation, UI observer subscription".
|
|
100
|
+
- `src/tools/background-spawner.ts` — remove `AgentActivityTracker` import, `subscribeUIObserver` import, `AgentActivityAccess` import, the `agentActivity` parameter, `bgState`, the entire `observer` block, `agentActivity.set`; trim the doc comment.
|
|
101
|
+
- `src/tools/agent-tool.ts` — remove `AgentActivityAccess` interface, `AgentToolRuntime.agentActivity` field, `import { AgentActivityTracker }`, and the two `this.runtime.agentActivity` arguments to `spawnBackground` / `runForeground`.
|
|
102
|
+
- `src/runtime.ts` — remove `agentActivity` field (+ doc comment) and `import type { AgentActivityTracker }`.
|
|
103
|
+
- `src/types.ts` — reword the `SubscribableSession` doc comment from "Used by record-observer and ui-observer" to reference `record-observer` only.
|
|
104
|
+
- `src/ui/agent-activity-tracker.ts` — **delete**.
|
|
105
|
+
- `src/ui/ui-observer.ts` — **delete**.
|
|
106
|
+
|
|
107
|
+
Tests:
|
|
108
|
+
|
|
109
|
+
- `test/ui/agent-activity-tracker.test.ts` — **delete**.
|
|
110
|
+
- `test/ui/ui-observer.test.ts` — **delete**.
|
|
111
|
+
- `test/tools/foreground-runner.test.ts` — drop the `runtime.agentActivity` argument from every `runForeground(...)` call; remove the "registers activity tracker in agentActivity on session creation" test (redundant with the ensureTimer/markFinished test).
|
|
112
|
+
- `test/tools/background-spawner.test.ts` — drop the `runtime.agentActivity` argument from every `spawnBackground(...)` call; remove the "registers an AgentActivityTracker in agentActivity map" test and the `AgentActivityTracker` import.
|
|
113
|
+
- `test/tools/agent-tool.test.ts` — remove the "registers activity in agentActivity map" test.
|
|
114
|
+
- `test/runtime.test.ts` — remove the "agentActivity map is independently mutable" and "multiple instances are isolated" tests, the two `agentActivity` assertions in "returns correct defaults", and the `AgentActivityTracker` import.
|
|
115
|
+
- `test/helpers/make-deps.ts` — remove the `agentActivity` map construction, the `agentActivity` field on the runtime stub, and the `AgentActivityTracker` import; update the runtime doc comment.
|
|
116
|
+
- `test/helpers/make-deps.test.ts` — remove the "agentActivity is an empty Map on the runtime" test, the "runtime.agentActivity satisfies AgentActivityAccess" test, and the `AgentActivityTracker` import.
|
|
117
|
+
|
|
118
|
+
Docs:
|
|
119
|
+
|
|
120
|
+
- `docs/architecture/architecture.md`:
|
|
121
|
+
- File tree (around line 350) — remove the `agent-activity-tracker.ts` and `ui-observer.ts` entries.
|
|
122
|
+
- "Observation model" prose (around lines 365–368) — drop the "UI streaming … is handled by `ui/ui-observer.ts`" sentence and the stale "widget reads agent state by polling a shared `Map<string, AgentActivityTracker>`" sentence (the widget reads off records since [#421]).
|
|
123
|
+
- System diagram (around line 68) — remove the `ui-observer` node.
|
|
124
|
+
- Sequence-diagram note (around line 261) — drop "+ ui-observer" so it reads "agent-observer subscribes to session events".
|
|
125
|
+
- Phase 18 roadmap Step 3 entry — mark complete (✅) with a `Landed:` bullet, matching the Steps 1–2 format.
|
|
126
|
+
- Phase 18 health-metrics table — the "Activity-tier modules slated for removal" row becomes historical; leave the snapshot but ensure the Step 3 `Landed:` bullet records the realized LOC delta.
|
|
127
|
+
- `.pi/skills/package-pi-subagents/SKILL.md`:
|
|
128
|
+
- Module-dependency-flow block — remove `←─subscribes─ ui-observer` from the `record-observer ─subscribes─→ AgentSession` line.
|
|
129
|
+
- Domain table — UI directory count `12 → 10`; domain header "seven domains (59 files)" → "(57 files)".
|
|
130
|
+
|
|
131
|
+
Historical docs under `docs/plans/`, `docs/retro/`, and `docs/architecture/history/` reference these symbols as records of past work and are **not** edited.
|
|
132
|
+
|
|
133
|
+
## Test Impact Analysis
|
|
134
|
+
|
|
135
|
+
This is a deletion, not an extraction, so it enables no new lower-level tests.
|
|
136
|
+
|
|
137
|
+
1. New tests enabled: none — the deleted modules' behavior is gone, not relocated.
|
|
138
|
+
2. Tests that become redundant: the tracker/observer unit suites (`agent-activity-tracker.test.ts`, `ui-observer.test.ts`) test deleted code and are removed wholesale; the "registers in agentActivity map" tests across `foreground-runner`, `background-spawner`, `agent-tool`, `runtime`, and `make-deps` test removed state and are deleted.
|
|
139
|
+
3. Tests that must stay: the spawner result-text tests, the `ensureTimer` / `markFinished` / `update` widget-driving tests, the foreground streaming-`onUpdate` test, and the runtime session-context tests — they exercise behavior that survives this step.
|
|
140
|
+
|
|
141
|
+
## Invariants at risk
|
|
142
|
+
|
|
143
|
+
This step touches surfaces refactored by Phase 18 Steps 1–2.
|
|
144
|
+
The relevant prior `Landed:` invariants and the tests that pin them:
|
|
145
|
+
|
|
146
|
+
- "no consumer references `AgentActivityTracker`" (Step 2 outcome) — pinned by the type checker: after this step the symbol is deleted, so any surviving reference fails `pnpm run check`.
|
|
147
|
+
The foreground `streamUpdate` reading off `recordRef` (Step 2) must remain — covered by `test/tools/foreground-runner.test.ts` "calls onUpdate with streaming details while running".
|
|
148
|
+
- "`Subagent` is the single home for all run state; getters available" (Step 1 outcome) — unchanged here; the getters keep their existing coverage in `test/lifecycle/` and `test/observation/`.
|
|
149
|
+
- Foreground re-render after dropping the second subscription — pinned by the streaming-`onUpdate` test (spinner-driven tick), confirming re-renders survive without `subscribeUIObserver`.
|
|
150
|
+
|
|
151
|
+
No new test is required; the type checker plus the surviving streaming test cover the at-risk invariants.
|
|
152
|
+
|
|
153
|
+
## TDD Order
|
|
154
|
+
|
|
155
|
+
This is primarily code deletion; each step is a `refactor:`/`docs:` commit that keeps `pnpm run check` and the full suite green.
|
|
156
|
+
Steps are ordered so no commit leaves a dangling type reference.
|
|
157
|
+
|
|
158
|
+
1. **Remove tracker wiring + the `agentActivity` parameter from the spawners.**
|
|
159
|
+
Edit `foreground-runner.ts` and `background-spawner.ts` to drop tracker construction, `subscribeUIObserver`/`setSession`, map writes, and the `agentActivity` parameter; update the two `AgentTool` call sites; update `foreground-runner.test.ts` and `background-spawner.test.ts` (drop the arg, remove the "registers tracker" tests and the `AgentActivityTracker` import in the background test).
|
|
160
|
+
The parameter removal cascades to call sites and tests at the type level, so they land together.
|
|
161
|
+
Run `pnpm run check` immediately after — shared-signature change.
|
|
162
|
+
Commit: `refactor: stop wiring activity trackers in the spawn tools (#422)`.
|
|
163
|
+
|
|
164
|
+
2. **Remove the activity map from the runtime and the agent-tool slice.**
|
|
165
|
+
Delete `SubagentRuntime.agentActivity` (`runtime.ts`), the `AgentActivityAccess` interface, the `AgentToolRuntime.agentActivity` field, and the `AgentActivityTracker` import in `agent-tool.ts`; update `make-deps.ts` (drop the map + field), and all tests reading the field (`runtime.test.ts`, `make-deps.test.ts`, `agent-tool.test.ts`).
|
|
166
|
+
Removing the field and `AgentActivityAccess` export breaks every reader at the type level, so all consumer + test updates land in this commit.
|
|
167
|
+
Run `pnpm run check` immediately after.
|
|
168
|
+
Commit: `refactor: drop the activity map from the runtime and agent tool (#422)`.
|
|
169
|
+
|
|
170
|
+
3. **Delete the dead modules and their tests.**
|
|
171
|
+
Delete `src/ui/agent-activity-tracker.ts`, `src/ui/ui-observer.ts`, `test/ui/agent-activity-tracker.test.ts`, `test/ui/ui-observer.test.ts`; reword the `SubscribableSession` comment in `types.ts`.
|
|
172
|
+
Run `pnpm fallow dead-code` to confirm no sibling export (e.g. `SessionLike`, `SubscribableSession`) was orphaned.
|
|
173
|
+
Commit: `refactor: delete AgentActivityTracker and ui-observer (#422)`.
|
|
174
|
+
|
|
175
|
+
4. **Update the architecture doc and package skill.**
|
|
176
|
+
Apply the doc edits listed in Module-Level Changes (file tree, observation-model prose, system + sequence diagrams, Phase 18 Step 3 ✅ + `Landed:` bullet, SKILL.md flow diagram and domain counts).
|
|
177
|
+
Verify any touched Mermaid renders (load the `mermaid` skill).
|
|
178
|
+
Commit: `docs: mark Phase 18 Step 3 complete and remove activity-tier references (#422)`.
|
|
179
|
+
|
|
180
|
+
## Risks and Mitigations
|
|
181
|
+
|
|
182
|
+
- **Foreground re-render cadence.**
|
|
183
|
+
Dropping `subscribeUIObserver` removes event-driven re-renders; updates now rely solely on the 80 ms spinner poll.
|
|
184
|
+
The displayed content is identical within ≤80 ms latency (the poll reads the same record the core observer populates), and the spinner already ran at this cadence — no perceptible regression.
|
|
185
|
+
Pinned by the streaming-`onUpdate` test.
|
|
186
|
+
- **Stale doc references.**
|
|
187
|
+
The architecture doc and SKILL.md describe the activity map as current state.
|
|
188
|
+
Mitigation: Step 4 grep-driven sweep; the file tree, prose, and two diagrams are all listed explicitly.
|
|
189
|
+
- **Orphaned sibling exports.**
|
|
190
|
+
Deleting the modules could orphan `SessionLike` or `SubscribableSession`.
|
|
191
|
+
Verified during planning: both remain used (`SessionLike` by `subagent-session.ts`; `SubscribableSession` by `record-observer.ts`, `subagent-session.ts`, `types.ts`).
|
|
192
|
+
Mitigation: `pnpm fallow dead-code` in Step 3.
|
|
193
|
+
- **Commit ordering.**
|
|
194
|
+
Removing the field before the spawners stop passing it would break the build.
|
|
195
|
+
Mitigation: Step 1 (stop passing) strictly precedes Step 2 (remove field).
|
|
196
|
+
|
|
197
|
+
## Open Questions
|
|
198
|
+
|
|
199
|
+
None blocking.
|
|
200
|
+
Whether the widget should self-drive from lifecycle events (so the spawners no longer call `ensureTimer`/`update`/`markFinished`) is deferred to Phase 18 Step 4 ([#423]).
|
|
201
|
+
|
|
202
|
+
[#421]: https://github.com/gotgenes/pi-packages/issues/421
|
|
203
|
+
[#423]: https://github.com/gotgenes/pi-packages/issues/423
|
|
204
|
+
[#424]: https://github.com/gotgenes/pi-packages/issues/424
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 423
|
|
3
|
+
issue_title: "pi-subagents: make the agent widget self-drive from lifecycle events"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Make the agent widget self-drive from lifecycle events
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Today the dependency arrow points the wrong way: the core spawn tools reach into the widget to drive it.
|
|
11
|
+
`foreground-runner.ts` calls `widget.ensureTimer()` (inside its `onSessionCreated` callback) and `widget.markFinished(fgId)` after completion; `background-spawner.ts` calls `widget.ensureTimer()` and `widget.update()` after spawn.
|
|
12
|
+
For the widget to be a clean reactive consumer of the core — and for the spawn tools to shed their widget dependency — the widget must subscribe to lifecycle signals and manage its own update timer.
|
|
13
|
+
This is the fourth step of the Phase 18 spine (widget/tool decoupling track), enabled by Step 3 ([#422]) removing the activity map and the second session subscription.
|
|
14
|
+
|
|
15
|
+
## Goals
|
|
16
|
+
|
|
17
|
+
- Make `AgentWidget` a `SubagentManagerObserver`: start its 80 ms update timer in response to `onSubagentStarted` / `onSubagentCreated`, and re-render on `onSubagentCompleted` / `onSubagentCompacted`.
|
|
18
|
+
- Wire the widget as a second lifecycle consumer alongside `SubagentEventsObserver` via a new `CompositeSubagentObserver` that fans out manager notifications — keeping `SubagentManager`'s single-observer contract closed for modification.
|
|
19
|
+
- Remove all inbound widget calls from the spawn tools (`ensureTimer` / `update` / `markFinished`) and drop the `ForegroundWidgetDeps` / `BackgroundWidgetDeps` parameters from both spawners.
|
|
20
|
+
- Narrow `AgentToolWidget` to the single method the tool still uses (`setUICtx`); UICtx capture stays in `ToolStartHandler` and `AgentTool`.
|
|
21
|
+
- Delete the now-redundant `AgentWidget.markFinished` (its bookkeeping is already covered by `seedFinishedAgents`) and make `ensureTimer` private.
|
|
22
|
+
|
|
23
|
+
This is **not** a breaking change.
|
|
24
|
+
The widget, the spawn tools, the observer interface, and `AgentToolWidget` are all internal symbols — none appear in the public service surface (`src/service/service.ts`) or the settings entry (`src/layered-settings.ts`).
|
|
25
|
+
Observable widget behavior is unchanged (the timer starts when an agent appears, renders, and self-clears when idle); commits use `refactor:` / `docs:`.
|
|
26
|
+
|
|
27
|
+
## Non-Goals
|
|
28
|
+
|
|
29
|
+
- Dropping the `widget` constructor param from `AgentTool` entirely and the widget/map stubs from `createToolDeps` — that is Phase 18 Step 5 ([#424]).
|
|
30
|
+
This step only narrows `AgentToolWidget` to `setUICtx`; the param stays.
|
|
31
|
+
- Migrating the widget to subscribe to the public `pi.events` (`subagents:*`) broadcast channels instead of the internal observer.
|
|
32
|
+
The event-bus surface is the substitutable-UI direction, but it depends on the public event-contract reconciliation in Step 6 ([#425]) and the UI-distribution decision in Step 8 ([#427]); using it now would front-run an unreconciled contract.
|
|
33
|
+
Option A (the composite observer) keeps a later `pi.events` migration to a localized `index.ts` swap.
|
|
34
|
+
- Touching `record-observer.ts`, `SubagentState`, the record getters, or the `SubagentEventsObserver` event/notification dispatch.
|
|
35
|
+
- Changing the foreground streaming path: `runForeground`'s `onSessionCreated` keeps binding `recordRef` for the `onUpdate` stream.
|
|
36
|
+
|
|
37
|
+
## Background
|
|
38
|
+
|
|
39
|
+
Relevant modules and their current relationships:
|
|
40
|
+
|
|
41
|
+
- `src/ui/agent-widget.ts` — `AgentWidget` polls `manager.listAgents()` every 80 ms via a `setInterval` started by `ensureTimer()`.
|
|
42
|
+
`update()` is idempotent: it self-seeds any finished agent into `finishedTurnAge` via `seedFinishedAgents()` (added in [#421] / [#422]), renders, and self-clears the timer through `clearWidget()` when no agents are active or lingering.
|
|
43
|
+
`markFinished(id)` and `ensureTimer()` carry `fallow-ignore-next-line unused-class-member` because their only callers are the spawn tools (via narrow interfaces).
|
|
44
|
+
- `src/lifecycle/subagent-manager.ts` — exposes a single `observer?: SubagentManagerObserver` slot (four methods: `onSubagentStarted`, `onSubagentCreated`, `onSubagentCompleted`, `onSubagentCompacted`).
|
|
45
|
+
`buildObserver()` adapts it into a per-agent `SubagentLifecycleObserver`.
|
|
46
|
+
`onSubagentStarted` fires for both foreground and background (on `record.start()`); `onSubagentCreated` fires only for background (in `spawn()`); `onSubagentCompleted` fires **only for background** (`onRunFinished` guards on `options.isBackground`).
|
|
47
|
+
- `src/observation/subagent-events-observer.ts` — the sole current observer; dispatches `pi.events` lifecycle events, session-entry persistence, and completion notifications.
|
|
48
|
+
- `src/tools/foreground-runner.ts` / `src/tools/background-spawner.ts` — drive the widget via `ForegroundWidgetDeps` / `BackgroundWidgetDeps`.
|
|
49
|
+
- `src/tools/agent-tool.ts` — holds a 4-method `AgentToolWidget` (`setUICtx`, `ensureTimer`, `update`, `markFinished`), passes the widget into both spawners, and calls `this.widget.setUICtx(ctx.ui)`.
|
|
50
|
+
- `src/index.ts` — constructs `observer` (the events observer), passes it to the manager, then constructs `widget` after the manager (the widget needs `manager.listAgents()`).
|
|
51
|
+
|
|
52
|
+
Key behavioral facts that shape the design:
|
|
53
|
+
|
|
54
|
+
- **Foreground completion is not observed.**
|
|
55
|
+
The manager never calls `onSubagentCompleted` for foreground agents, so the widget cannot learn of foreground completion through the observer.
|
|
56
|
+
It does not need to: `seedFinishedAgents()` already seeds any agent with `completedAt` on the next poll tick, so `markFinished` is fully redundant and is removed.
|
|
57
|
+
- **The widget keeps its `manager` reference.**
|
|
58
|
+
Rendering still reads `manager.listAgents()`; the observer only changes the *trigger* that starts the timer, not the data source.
|
|
59
|
+
Full broadcast-plus-query decoupling is the Step 8 concern, deliberately out of scope here.
|
|
60
|
+
|
|
61
|
+
AGENTS.md / package constraints that apply:
|
|
62
|
+
|
|
63
|
+
- pi-subagents is a narrow core that is open for extension, closed for modification — dependency arrows point inward.
|
|
64
|
+
A composite observer in the composition root (`index.ts`) keeps `SubagentManager` untouched, rather than teaching the core to fan out to N consumers.
|
|
65
|
+
- Run `pnpm fallow dead-code` before pushing — privatizing `ensureTimer` and deleting `markFinished` change the dead-code surface.
|
|
66
|
+
|
|
67
|
+
## Design Overview
|
|
68
|
+
|
|
69
|
+
### CompositeSubagentObserver (new collaborator)
|
|
70
|
+
|
|
71
|
+
A small fan-out observer lets the manager keep its single-observer contract while several independent consumers subscribe.
|
|
72
|
+
It lives next to the existing observer in `src/observation/`.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// src/observation/composite-subagent-observer.ts
|
|
76
|
+
export class CompositeSubagentObserver implements SubagentManagerObserver {
|
|
77
|
+
private readonly delegates: SubagentManagerObserver[];
|
|
78
|
+
constructor(delegates: SubagentManagerObserver[]) {
|
|
79
|
+
this.delegates = [...delegates];
|
|
80
|
+
}
|
|
81
|
+
/** Register an additional observer (breaks the widget↔manager construction cycle). */
|
|
82
|
+
add(observer: SubagentManagerObserver): void {
|
|
83
|
+
this.delegates.push(observer);
|
|
84
|
+
}
|
|
85
|
+
onSubagentStarted(record: Subagent): void {
|
|
86
|
+
this.forEach((o) => o.onSubagentStarted(record), "onSubagentStarted");
|
|
87
|
+
}
|
|
88
|
+
// …onSubagentCreated / onSubagentCompleted / onSubagentCompacted follow the same shape…
|
|
89
|
+
private forEach(call: (o: SubagentManagerObserver) => void, label: string): void {
|
|
90
|
+
for (const o of this.delegates) {
|
|
91
|
+
try { call(o); } catch (err) { debugLog(`CompositeSubagentObserver.${label}`, err); }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Each delegate is isolated in a `try`/`catch` so a widget render throw cannot suppress event emission, and vice versa.
|
|
98
|
+
The `add` method exists to break the construction cycle (see wiring below), not as speculative flexibility — `index.ts` is its only caller.
|
|
99
|
+
|
|
100
|
+
### Consumer call site (index.ts wiring)
|
|
101
|
+
|
|
102
|
+
The widget needs the manager (for `listAgents()`) and the manager needs the observer, so the composite is constructed first, the widget is registered after construction:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const eventsObserver = new SubagentEventsObserver({ /* emit, appendEntry, notifications */ });
|
|
106
|
+
const observer = new CompositeSubagentObserver([eventsObserver]);
|
|
107
|
+
const manager = new SubagentManager({ /* …, */ observer /* , … */ });
|
|
108
|
+
// …
|
|
109
|
+
const widget = new AgentWidget(manager, registry);
|
|
110
|
+
observer.add(widget); // the manager consults `observer` only at spawn time, after this runs
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The manager reads `this.observer` lazily (only at `spawn` / `spawnAndWait`), so registering the widget after construction is safe — no notification can fire before `observer.add(widget)` executes.
|
|
114
|
+
|
|
115
|
+
### AgentWidget as a reactive observer
|
|
116
|
+
|
|
117
|
+
`AgentWidget implements SubagentManagerObserver`.
|
|
118
|
+
It ignores the `record` argument (it re-reads `listAgents()`), reacting by intent rather than by payload:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
// react to lifecycle, self-drive the timer
|
|
122
|
+
onSubagentStarted(_record: Subagent): void { this.startLoop(); }
|
|
123
|
+
onSubagentCreated(_record: Subagent): void { this.startLoop(); }
|
|
124
|
+
onSubagentCompleted(_record: Subagent): void { this.update(); }
|
|
125
|
+
onSubagentCompacted(_record: Subagent, _info: CompactionInfo): void { this.update(); }
|
|
126
|
+
|
|
127
|
+
private startLoop(): void { this.ensureTimer(); this.update(); }
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`startLoop` (started / created) starts the timer and renders; completed / compacted only `update()` because the timer is already running by then (started fires at spawn, created fires before the queued run).
|
|
131
|
+
`update()` self-seeds the finished agent and keeps the timer alive for the linger window, then `clearWidget()` stops it once idle — unchanged from today.
|
|
132
|
+
`ensureTimer()` becomes `private` (now called from `startLoop`, so it loses its `fallow-ignore`); `markFinished` is deleted.
|
|
133
|
+
|
|
134
|
+
This follows the `SubagentEventsObserver` precedent: a class that implements `SubagentManagerObserver` and is invoked polymorphically by the manager counts as used, so the new methods need no `fallow-ignore`.
|
|
135
|
+
|
|
136
|
+
### Extracted-from / upstream check
|
|
137
|
+
|
|
138
|
+
This extraction introduces no Tell-Don't-Ask or output-argument violations: the widget reads `listAgents()` (a query it already owns) and mutates only its own `finishedTurnAge` / timer state.
|
|
139
|
+
The composite only forwards calls; it holds no state beyond its delegate list and never reaches through a delegate.
|
|
140
|
+
|
|
141
|
+
## Module-Level Changes
|
|
142
|
+
|
|
143
|
+
Source:
|
|
144
|
+
|
|
145
|
+
- `src/observation/composite-subagent-observer.ts` — **new.**
|
|
146
|
+
`CompositeSubagentObserver implements SubagentManagerObserver`; constructor takes a delegate array; `add()` appends; each method fans out through a private `forEach` with per-delegate `try`/`catch` + `debugLog`.
|
|
147
|
+
- `src/ui/agent-widget.ts` — add `implements SubagentManagerObserver`; add `onSubagentStarted` / `onSubagentCreated` / `onSubagentCompleted` / `onSubagentCompacted` and the private `startLoop`; make `ensureTimer` `private` and drop its `fallow-ignore`; delete `markFinished` and its `fallow-ignore`; import `SubagentManagerObserver` and `CompactionInfo` types; reword the `seedFinishedAgents` doc comment so it no longer references the removed `markFinished` method.
|
|
148
|
+
- `src/index.ts` — rename the events-observer local to `eventsObserver`; construct `const observer = new CompositeSubagentObserver([eventsObserver])` and pass it to the manager; add `observer.add(widget)` after widget construction; import `CompositeSubagentObserver`; update the widget-construction comment to note the post-construction observer registration.
|
|
149
|
+
- `src/tools/foreground-runner.ts` — remove the `ForegroundWidgetDeps` interface and the `widget` parameter; remove `widget.ensureTimer()` from `onSessionCreated` (keep the `recordRef` binding) and the `fgId` variable + the post-completion `widget.markFinished(fgId)` block.
|
|
150
|
+
- `src/tools/background-spawner.ts` — remove the `BackgroundWidgetDeps` interface and the `widget` parameter; remove the `widget.ensureTimer()` / `widget.update()` calls; trim the doc comment ("Owns: widget update and launch message formatting" → "Owns: launch message formatting").
|
|
151
|
+
- `src/tools/agent-tool.ts` — narrow `AgentToolWidget` to `{ setUICtx(ctx: UICtx): void }`; reword its doc comment; drop the `this.widget` argument from the `spawnBackground` / `runForeground` calls; keep `this.widget.setUICtx(ctx.ui)` and the constructor param.
|
|
152
|
+
|
|
153
|
+
Tests:
|
|
154
|
+
|
|
155
|
+
- `test/observation/composite-subagent-observer.test.ts` — **new.**
|
|
156
|
+
Fan-out to all delegates in registration order for each of the four methods; `add()` registers a late delegate; a throwing delegate does not suppress the others.
|
|
157
|
+
- `test/ui/agent-widget.test.ts` — new `describe` for the observer methods: `onSubagentStarted` / `onSubagentCreated` start the timer and render (advancing fake timers keeps re-rendering); `onSubagentCompleted` / `onSubagentCompacted` trigger a render; the widget self-clears once idle.
|
|
158
|
+
- `test/tools/foreground-runner.test.ts` — drop the `widget` arg from every `runForeground(...)` call; remove the "calls runtime.ensureTimer and runtime.markFinished after completion" test and the `spawnAndWaitRegistering` widget-binding framing (keep the streaming-`onUpdate` test).
|
|
159
|
+
- `test/tools/background-spawner.test.ts` — drop the `widget` arg from every `spawnBackground(...)` call; remove the "calls runtime.ensureTimer and runtime.update after spawn" test.
|
|
160
|
+
- `test/helpers/make-deps.ts` — narrow the `widget` fixture to `{ setUICtx: vi.fn() }`; reword the `AgentToolFixture.widget` doc comment (drop the `BackgroundWidgetDeps` / `ForegroundWidgetDeps` mention).
|
|
161
|
+
- `test/helpers/make-deps.test.ts` — rewrite the "all widget methods are vi.fn stubs" test to assert only `setUICtx`; remove the `BackgroundWidgetDeps` / `ForegroundWidgetDeps` structural-compatibility tests and their imports.
|
|
162
|
+
|
|
163
|
+
Docs:
|
|
164
|
+
|
|
165
|
+
- `docs/architecture/architecture.md`:
|
|
166
|
+
- File tree (around line 318) — add `composite-subagent-observer.ts` under `observation/` with a one-line description.
|
|
167
|
+
- System diagram (around line 64) — add a `SubagentManager -.->|notifies| Widget` edge (the inverted, reactive direction) alongside the existing `Widget -.->|polls| SubagentManager`.
|
|
168
|
+
- Widget prose (around line 363) — note the 80 ms poll loop is now *started by lifecycle notifications* rather than by spawn-tool calls.
|
|
169
|
+
- Phase 18 roadmap Step 4 entry — mark ✅ with a `Landed:` bullet matching the Steps 1–3 format.
|
|
170
|
+
- Step-dependency Mermaid — `S4` node gets the ✅ marker.
|
|
171
|
+
- `.pi/skills/package-pi-subagents/SKILL.md`:
|
|
172
|
+
- Domain table — Observation `5 → 6` modules (add `composite-subagent-observer.ts` to the list); header "seven domains (57 files)" → "(58 files)".
|
|
173
|
+
- Module-dependency-flow block — note the widget is now a lifecycle observer fanned out through the composite alongside `subagent-events-observer`.
|
|
174
|
+
|
|
175
|
+
Verify the exact current counts with `grep` before editing (the Step 3 sweep set Observation to 5 and the total to 57); the architecture health-metrics snapshot table (`7,751 (62 files)`) is the Phase 18 *findings* snapshot and is left as-is, per the Step 3 precedent.
|
|
176
|
+
|
|
177
|
+
Historical docs under `docs/plans/`, `docs/retro/`, and `docs/architecture/history/` are records of past work and are **not** edited.
|
|
178
|
+
|
|
179
|
+
## Test Impact Analysis
|
|
180
|
+
|
|
181
|
+
1. **New tests enabled.**
|
|
182
|
+
The composite observer becomes unit-testable in isolation (fan-out order, `add`, per-delegate fault isolation) — impossible before, since there was no fan-out unit.
|
|
183
|
+
The widget's timer-start logic becomes directly testable through its observer methods, rather than only indirectly through the spawn tools.
|
|
184
|
+
2. **Tests that become redundant.**
|
|
185
|
+
The spawner widget-driving assertions (`foreground-runner.test.ts` "calls … ensureTimer and … markFinished", `background-spawner.test.ts` "calls … ensureTimer and … update") test inbound calls that no longer exist — removed.
|
|
186
|
+
`make-deps.test.ts`'s "all widget methods are vi.fn stubs" shrinks to `setUICtx`; the `BackgroundWidgetDeps` / `ForegroundWidgetDeps` structural-compat tests are removed with their interfaces.
|
|
187
|
+
3. **Tests that must stay.**
|
|
188
|
+
The spawner result-text tests (launch message, queued/started, output-file, error), the foreground "calls onUpdate with streaming details while running" test (pins the surviving `recordRef` binding), the `AgentWidget.update` self-seed tests (the mechanism that replaces `markFinished`), and the `agent-tool` `setUICtx` test all exercise behavior that survives this step.
|
|
189
|
+
|
|
190
|
+
## Invariants at risk
|
|
191
|
+
|
|
192
|
+
This step touches surfaces refactored by Phase 18 Steps 1–3.
|
|
193
|
+
Relevant prior `Landed:` invariants and the tests that pin them:
|
|
194
|
+
|
|
195
|
+
- "The foreground `observer.onSessionCreated` keeps `recordRef`/`fgId` binding" (Step 3 outcome).
|
|
196
|
+
This step removes `fgId` (only ever used by the deleted `markFinished` call) but **keeps `recordRef`** for the streaming `onUpdate`.
|
|
197
|
+
Pinned by `test/tools/foreground-runner.test.ts` "calls onUpdate with streaming details while running" — must stay green.
|
|
198
|
+
- "The widget owns detection of completions via `seedFinishedAgents`" (Steps 1–2 outcome).
|
|
199
|
+
Removing `markFinished` relies on this; pinned by `test/ui/agent-widget.test.ts` "AgentWidget.update self-seeds finished agents" and "does not advance the linger age on repeated update() without a turn".
|
|
200
|
+
- "One session subscription per child; runtime holds zero UI state" (Step 3 outcome).
|
|
201
|
+
Unchanged here — the widget subscribes to *manager lifecycle notifications*, not a second *session* subscription.
|
|
202
|
+
|
|
203
|
+
No new test is required to protect these; the surviving streaming and self-seed tests plus the type checker cover them.
|
|
204
|
+
|
|
205
|
+
## TDD Order
|
|
206
|
+
|
|
207
|
+
Non-breaking refactor; every commit keeps `pnpm run check` and the full suite green.
|
|
208
|
+
Steps are ordered so the widget is wired as an observer *before* the spawn-tool calls are removed — no commit leaves the widget without a timer-start signal.
|
|
209
|
+
|
|
210
|
+
1. **Add `CompositeSubagentObserver` (pure addition).**
|
|
211
|
+
Write `test/observation/composite-subagent-observer.test.ts` (red), then `src/observation/composite-subagent-observer.ts` (green): fan-out in registration order, `add()`, per-delegate fault isolation.
|
|
212
|
+
Not yet wired.
|
|
213
|
+
Run `pnpm run check`.
|
|
214
|
+
Commit: `refactor: add CompositeSubagentObserver to fan out lifecycle notifications (#423)`.
|
|
215
|
+
|
|
216
|
+
2. **Subscribe the widget to lifecycle notifications.**
|
|
217
|
+
Add the four observer methods + `implements SubagentManagerObserver` + `startLoop` to `AgentWidget` (red→green with the new widget observer tests); wire `index.ts` (rename `eventsObserver`, wrap in `CompositeSubagentObserver`, `observer.add(widget)`).
|
|
218
|
+
The spawn tools still call the widget in this commit — the resulting double-drive is idempotent (`ensureTimer` uses `??=`; `update` and finished-seeding are idempotent), so behavior is unchanged.
|
|
219
|
+
The widget's observer methods are now invoked polymorphically by the manager, so they need no `fallow-ignore`.
|
|
220
|
+
Run `pnpm run check` and the full suite.
|
|
221
|
+
Commit: `refactor: subscribe the widget to lifecycle notifications via a composite observer (#423)`.
|
|
222
|
+
|
|
223
|
+
3. **Stop driving the widget from the spawn tools (atomic removal).**
|
|
224
|
+
Remove the `widget` parameter and `ForegroundWidgetDeps` / `BackgroundWidgetDeps` interfaces from both spawners; drop the `ensureTimer` / `update` / `markFinished` call sites (and the now-orphaned `fgId`); narrow `AgentToolWidget` to `setUICtx` and drop the `this.widget` args from the spawner calls in `AgentTool`; delete `AgentWidget.markFinished` and make `ensureTimer` private.
|
|
225
|
+
Update the spawner tests (drop the `widget` arg, remove the two widget-driving tests), `make-deps.ts` (narrow the fixture), and `make-deps.test.ts` (rewrite the widget test, remove the structural-compat tests).
|
|
226
|
+
Removing the exported interfaces and the `widget` params breaks every call site and test at the type level, so they land together.
|
|
227
|
+
The widget keeps self-driving via the composite wired in Step 2, so there is no behavior gap.
|
|
228
|
+
Run `pnpm run check`, the full suite, and `pnpm fallow dead-code`.
|
|
229
|
+
Commit: `refactor: stop driving the widget from the spawn tools (#423)`.
|
|
230
|
+
|
|
231
|
+
4. **Update the architecture doc and package skill.**
|
|
232
|
+
Apply the doc edits in Module-Level Changes (file tree, system-diagram edge, widget prose, Step 4 ✅ + `Landed:` bullet, `S4` Mermaid marker; SKILL.md Observation count `5 → 6` and total `57 → 58`, flow block).
|
|
233
|
+
Verify any touched Mermaid renders (load the `mermaid` skill).
|
|
234
|
+
Commit: `docs: mark Phase 18 Step 4 complete and record the widget self-drive (#423)`.
|
|
235
|
+
|
|
236
|
+
## Risks and Mitigations
|
|
237
|
+
|
|
238
|
+
- **Behavior gap between commits.**
|
|
239
|
+
Removing the spawn-tool calls before the widget is an observer would leave the widget with no timer-start signal.
|
|
240
|
+
Mitigation: Step 2 wires the composite (widget self-drives, alongside the still-present spawn-tool calls) strictly before Step 3 removes those calls.
|
|
241
|
+
- **Construction cycle (widget ↔ manager).**
|
|
242
|
+
The widget needs the manager; the manager needs the observer; the observer includes the widget.
|
|
243
|
+
Mitigation: construct the composite with `[eventsObserver]`, pass it to the manager, then `observer.add(widget)` after the widget is built — the manager consults the observer only at spawn time.
|
|
244
|
+
- **Transient double-drive in Step 2.**
|
|
245
|
+
Both the spawn tools and the observer drive the widget in that commit.
|
|
246
|
+
Mitigation: every driven method is idempotent (`ensureTimer` `??=`; `update` re-renders; finished-seeding only seeds when absent), so the doubled calls are harmless.
|
|
247
|
+
- **Dead-code surface change.**
|
|
248
|
+
Privatizing `ensureTimer` and deleting `markFinished` alter what `fallow` sees.
|
|
249
|
+
Mitigation: Step 3 runs `pnpm fallow dead-code`; the new observer methods are invoked via the interface (the `SubagentEventsObserver` precedent), so they are not flagged.
|
|
250
|
+
- **Foreground completion is unobserved.**
|
|
251
|
+
The manager does not fire `onSubagentCompleted` for foreground agents.
|
|
252
|
+
Mitigation: `seedFinishedAgents` already covers foreground completion through polling — pinned by the existing self-seed tests — so removing `markFinished` is safe.
|
|
253
|
+
|
|
254
|
+
## Open Questions
|
|
255
|
+
|
|
256
|
+
None blocking.
|
|
257
|
+
Whether the widget should ultimately subscribe to the public `pi.events` (`subagents:*`) broadcast instead of the internal observer — the substitutable-UI direction — is deferred to the public-event-contract reconciliation ([#425]) and the UI-distribution decision ([#427]).
|
|
258
|
+
Option A keeps that future migration to a localized `index.ts` swap.
|
|
259
|
+
|
|
260
|
+
[#421]: https://github.com/gotgenes/pi-packages/issues/421
|
|
261
|
+
[#422]: https://github.com/gotgenes/pi-packages/issues/422
|
|
262
|
+
[#424]: https://github.com/gotgenes/pi-packages/issues/424
|
|
263
|
+
[#425]: https://github.com/gotgenes/pi-packages/issues/425
|
|
264
|
+
[#427]: https://github.com/gotgenes/pi-packages/issues/427
|