@gotgenes/pi-subagents 6.15.0 → 6.16.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 +14 -0
- package/docs/architecture/architecture.md +3 -5
- package/docs/plans/0144-consolidate-observation-model.md +263 -0
- package/docs/retro/0145-decompose-execute-push-ctx-to-boundary.md +56 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +3 -3
- package/src/agent-record.ts +11 -0
- package/src/index.ts +6 -6
- package/src/notification.ts +21 -24
- package/src/service-adapter.ts +1 -1
- package/src/tools/agent-tool.ts +1 -1
- package/src/tools/background-spawner.ts +1 -1
- package/src/tools/foreground-runner.ts +7 -4
- package/src/tools/get-result-tool.ts +3 -3
- package/src/tools/steer-tool.ts +1 -1
- package/src/ui/agent-activity-tracker.ts +3 -27
- package/src/ui/agent-menu.ts +1 -1
- package/src/ui/agent-widget.ts +3 -4
- package/src/ui/conversation-viewer.ts +3 -3
- package/src/ui/ui-observer.ts +1 -12
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,20 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [6.16.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.15.0...pi-subagents-v6.16.0) (2026-05-23)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* add session and outputFile convenience getters to AgentRecord ([#144](https://github.com/gotgenes/pi-packages/issues/144)) ([b451894](https://github.com/gotgenes/pi-packages/commit/b451894d101b43be9115dcf6d45725a09da81df8))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* mark Step L complete, remove resolved smells from architecture doc ([#144](https://github.com/gotgenes/pi-packages/issues/144)) ([36ab7a5](https://github.com/gotgenes/pi-packages/commit/36ab7a51d0320666b71d6c9e20c3bbf63b7c43c5))
|
|
19
|
+
* plan consolidate observation model ([#144](https://github.com/gotgenes/pi-packages/issues/144)) ([9aa2c85](https://github.com/gotgenes/pi-packages/commit/9aa2c8508079c6ae847662631afd223e8966e12e))
|
|
20
|
+
* **retro:** add retro notes for issue [#145](https://github.com/gotgenes/pi-packages/issues/145) ([2d23081](https://github.com/gotgenes/pi-packages/commit/2d230817d53357a62eb752b56f8b1c8ce4af718c))
|
|
21
|
+
|
|
8
22
|
## [6.15.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.14.1...pi-subagents-v6.15.0) (2026-05-23)
|
|
9
23
|
|
|
10
24
|
|
|
@@ -617,13 +617,11 @@ Phase 9 targets the next layer: observation model consolidation, `ExtensionConte
|
|
|
617
617
|
|
|
618
618
|
| Smell | Location | Evidence | Severity |
|
|
619
619
|
| ------------------------------------------------ | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- |
|
|
620
|
-
| Dual observation | `record-observer.ts`, `ui-observer.ts` | Both independently count tool uses and accumulate lifetime usage from the same session events; consumers use `activity?.toolUses ?? record.toolUses` fallbacks | High |
|
|
621
620
|
| `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 |
|
|
622
621
|
| Wide `ctx` in menu handlers | `agent-menu.ts`, `agent-config-editor.ts`, `agent-creation-wizard.ts` | Functions declare `ctx: ExtensionContext` but only call `ctx.ui.select/confirm/input/notify/editor`; 43 `ctx as any` casts across 3 test files | Medium |
|
|
623
|
-
| `record.execution?.session` traversal | 15+ callsites across tools, notification, widget, menu | Callers reach through `ExecutionState` to access session and outputFile - Law of Demeter violation | Medium |
|
|
624
622
|
| Direct SDK import in `conversation-viewer.ts` | `conversation-viewer.test.ts` | Hoisted `vi.mock("@earendil-works/pi-tui")` to intercept `wrapTextWithAnsi` | Low |
|
|
625
623
|
| Widget mixes rendering, lifecycle, and state | `agent-widget.ts` (370 lines) | `renderWidget` is ~109 lines mixing data collection, formatting, and overflow layout; constructor takes 3 concrete collaborators | Low |
|
|
626
|
-
| `deps.` prefix noise in function bodies |
|
|
624
|
+
| `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 |
|
|
627
625
|
|
|
628
626
|
### Dependency bag convention
|
|
629
627
|
|
|
@@ -634,9 +632,9 @@ Applied incrementally as each step touches a module:
|
|
|
634
632
|
|
|
635
633
|
This eliminates the `deps.` prefix noise across ~124 callsites in 12 modules.
|
|
636
634
|
|
|
637
|
-
### Step L: Consolidate observation model (#144)
|
|
635
|
+
### Step L: Consolidate observation model (#144) ✓
|
|
638
636
|
|
|
639
|
-
|
|
637
|
+
Removed `_toolUses` and `_lifetimeUsage` from `AgentActivityTracker`.
|
|
640
638
|
UI consumers read stats from `AgentRecord` instead of the tracker.
|
|
641
639
|
The UI observer retains event subscriptions for re-render triggers but no longer accumulates stats independently.
|
|
642
640
|
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 144
|
|
3
|
+
issue_title: "Consolidate observation model (Phase 9, Step L)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Consolidate observation model
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`record-observer.ts` and `ui-observer.ts` independently count tool uses and accumulate lifetime usage from the same session events.
|
|
11
|
+
`AgentRecord` owns `_toolUses` and `_lifetimeUsage` (accumulated by the record observer), while `AgentActivityTracker` maintains its own `_toolUses` and `_lifetimeUsage` (accumulated by the UI observer).
|
|
12
|
+
Consumers use `activity?.toolUses ?? record.toolUses` fallbacks to paper over the ambiguity.
|
|
13
|
+
|
|
14
|
+
Separately, 15+ callsites navigate `record.execution?.session` and `record.execution?.outputFile`, leaking the `ExecutionState` structure to every consumer.
|
|
15
|
+
|
|
16
|
+
Finally, `NotificationDeps` (4 fields) is a dependency bag on a class where every method uses every field — plain constructor parameters are simpler.
|
|
17
|
+
|
|
18
|
+
## Goals
|
|
19
|
+
|
|
20
|
+
- Remove `_toolUses` and `_lifetimeUsage` from `AgentActivityTracker` so stats have a single source of truth on `AgentRecord`.
|
|
21
|
+
- Update UI consumers (widget, conversation viewer, notification, foreground runner) to read stats from `AgentRecord` instead of the tracker.
|
|
22
|
+
- Remove the `onToolEnd` counter and `onUsageUpdate` accumulator from `AgentActivityTracker`, keeping only live UI state (active tools, response text, turn count).
|
|
23
|
+
- Stop the UI observer from duplicating `tool_execution_end` counting and `message_end` usage accumulation.
|
|
24
|
+
- Add `session` and `outputFile` convenience getters on `AgentRecord` to hide the `execution?.` traversal.
|
|
25
|
+
- Replace `NotificationDeps` interface with plain parameters on `NotificationManager` constructor.
|
|
26
|
+
|
|
27
|
+
## Non-Goals
|
|
28
|
+
|
|
29
|
+
- Splitting `AgentWidget` rendering into pure functions (Step P / #148 — depends on this work).
|
|
30
|
+
- Narrowing `ExtensionContext` for menu handlers (Step N / #146 — independent track).
|
|
31
|
+
- Injecting text wrapping into `ConversationViewer` (Step O / #147 — independent track).
|
|
32
|
+
|
|
33
|
+
## Background
|
|
34
|
+
|
|
35
|
+
### Dual counting
|
|
36
|
+
|
|
37
|
+
Both observers subscribe to the same session events:
|
|
38
|
+
|
|
39
|
+
| Event | `record-observer.ts` | `ui-observer.ts` |
|
|
40
|
+
| --------------------- | ---------------------------- | ------------------------------------- |
|
|
41
|
+
| `tool_execution_end` | `record.incrementToolUses()` | `tracker.onToolEnd()` → `_toolUses++` |
|
|
42
|
+
| `message_end` (usage) | `record.addUsage(delta)` | `tracker.onUsageUpdate(delta)` |
|
|
43
|
+
|
|
44
|
+
The widget reads `bg?.toolUses ?? a.toolUses` and the conversation viewer reads `this.activity?.toolUses ?? this.record.toolUses`, preferring the tracker when alive and falling back to the record.
|
|
45
|
+
After this change, there is one source: `AgentRecord`.
|
|
46
|
+
|
|
47
|
+
### `execution?.` traversal
|
|
48
|
+
|
|
49
|
+
`ExecutionState` holds `session` and `outputFile`.
|
|
50
|
+
These are set once when the agent session is created (`agent-manager.ts` line 276).
|
|
51
|
+
Consumers reach through `record.execution?.session` and `record.execution?.outputFile` in 12 distinct locations across 7 files.
|
|
52
|
+
Convenience getters on `AgentRecord` eliminate the traversal and hide the `ExecutionState` structure.
|
|
53
|
+
|
|
54
|
+
### `NotificationDeps` bag
|
|
55
|
+
|
|
56
|
+
`NotificationManager` receives 4 fields via `NotificationDeps`: `sendMessage`, `agentActivity`, `markFinished`, `updateWidget`.
|
|
57
|
+
Every method on the class uses every field.
|
|
58
|
+
Per code-design convention (dependency width), a 4-field interface where every consumer uses every field is fine as a bag, but the architecture doc explicitly calls for plain parameters here.
|
|
59
|
+
This is a mechanical change.
|
|
60
|
+
|
|
61
|
+
## Design Overview
|
|
62
|
+
|
|
63
|
+
### AgentActivityTracker changes
|
|
64
|
+
|
|
65
|
+
Remove `_toolUses`, `_lifetimeUsage`, `onToolEnd`, `onUsageUpdate`, `toolUses` getter, and `lifetimeUsage` getter.
|
|
66
|
+
Retain: `_activeTools`, `_toolKeySeq`, `_responseText`, `_session`, `_turnCount`, `_maxTurns`, and their associated transition methods and getters.
|
|
67
|
+
|
|
68
|
+
`onToolStart` stays (tracks active tools for the widget spinner).
|
|
69
|
+
A new `onToolDone(toolName)` method replaces `onToolEnd` — it removes the tool from `_activeTools` without incrementing any counter.
|
|
70
|
+
This rename clarifies that the method only manages the active-tool display set.
|
|
71
|
+
|
|
72
|
+
### UI observer changes
|
|
73
|
+
|
|
74
|
+
The UI observer stops calling counter/accumulator methods:
|
|
75
|
+
|
|
76
|
+
- `tool_execution_end` handler: calls `tracker.onToolDone(name)` (active-tool tracking only).
|
|
77
|
+
- `message_end` handler: removed entirely (no more usage accumulation).
|
|
78
|
+
The `onUpdate?.()` call for usage re-render moves to the record observer path via the existing widget update flow.
|
|
79
|
+
|
|
80
|
+
### AgentRecord convenience getters
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
get session(): AgentSession | undefined {
|
|
84
|
+
return this.execution?.session;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
get outputFile(): string | undefined {
|
|
88
|
+
return this.execution?.outputFile;
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Callers change from `record.execution?.session` to `record.session`.
|
|
93
|
+
The `execution` field remains writable (set by `AgentManager`), but consumers no longer need to know about `ExecutionState`.
|
|
94
|
+
|
|
95
|
+
### Consumer migration
|
|
96
|
+
|
|
97
|
+
| File | Before | After |
|
|
98
|
+
| -------------------------- | -------------------------------------------------- | ------------------------------------------------ |
|
|
99
|
+
| `agent-widget.ts` | `bg?.toolUses ?? a.toolUses` | `a.toolUses` |
|
|
100
|
+
| `agent-widget.ts` | `getLifetimeTotal(bg?.lifetimeUsage)` | `getLifetimeTotal(a.lifetimeUsage)` |
|
|
101
|
+
| `agent-widget.ts` | `getSessionContextPercent(bg?.session)` | `getSessionContextPercent(a.session)` |
|
|
102
|
+
| `conversation-viewer.ts` | `this.activity?.toolUses ?? this.record.toolUses` | `this.record.toolUses` |
|
|
103
|
+
| `conversation-viewer.ts` | `getLifetimeTotal(this.activity?.lifetimeUsage)` | `getLifetimeTotal(this.record.lifetimeUsage)` |
|
|
104
|
+
| `conversation-viewer.ts` | `getSessionContextPercent(this.activity?.session)` | `getSessionContextPercent(this.record.session)` |
|
|
105
|
+
| `notification.ts` | `record.execution?.session` | `record.session` |
|
|
106
|
+
| `notification.ts` | `record.execution?.outputFile` | `record.outputFile` |
|
|
107
|
+
| `tools/get-result-tool.ts` | `record.execution?.session` | `record.session` |
|
|
108
|
+
| `tools/steer-tool.ts` | `record.execution?.session` | `record.session` |
|
|
109
|
+
| `agent-menu.ts` | `record.execution?.session` | `record.session` |
|
|
110
|
+
| `service-adapter.ts` | `record.execution?.session` | `record.session` |
|
|
111
|
+
| `agent-manager.ts` | `record.execution?.session` | `record.session` (in dispose paths) |
|
|
112
|
+
| `foreground-runner.ts` | `fgState.toolUses` | `record.toolUses` (in final result) |
|
|
113
|
+
| `foreground-runner.ts` | `formatLifetimeTokens(fgState)` | `formatLifetimeTokens(record)` (in final result) |
|
|
114
|
+
|
|
115
|
+
### NotificationManager refactoring
|
|
116
|
+
|
|
117
|
+
Replace:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
export interface NotificationDeps { … }
|
|
121
|
+
constructor(private deps: NotificationDeps) {}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
With:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
constructor(
|
|
128
|
+
private sendMessage: NotificationDeps["sendMessage"],
|
|
129
|
+
private agentActivity: Map<string, AgentActivityTracker>,
|
|
130
|
+
private markFinished: (id: string) => void,
|
|
131
|
+
private updateWidget: () => void,
|
|
132
|
+
) {}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Internal references change from `this.deps.sendMessage(…)` to `this.sendMessage(…)`.
|
|
136
|
+
The `NotificationDeps` interface is removed.
|
|
137
|
+
|
|
138
|
+
### Foreground runner streaming
|
|
139
|
+
|
|
140
|
+
During foreground execution, `streamUpdate` reads `fgState.toolUses` and `formatLifetimeTokens(fgState)` for live spinner updates.
|
|
141
|
+
After removing these from the tracker, `streamUpdate` must read from the record instead.
|
|
142
|
+
The record is available only after `spawnAndWait` returns the record reference via `onSessionCreated`.
|
|
143
|
+
Before that callback fires, tool uses and usage are both zero — the tracker never had meaningful data at that point either.
|
|
144
|
+
`streamUpdate` will capture a `let recordRef: AgentRecord | undefined` and read `recordRef?.toolUses ?? 0` during the spinner phase.
|
|
145
|
+
After `onSessionCreated`, `recordRef` is set and subsequent spinner ticks read live values from the record.
|
|
146
|
+
|
|
147
|
+
## Module-Level Changes
|
|
148
|
+
|
|
149
|
+
### Modified files
|
|
150
|
+
|
|
151
|
+
1. `src/ui/agent-activity-tracker.ts` — Remove `_toolUses`, `_lifetimeUsage`, `onToolEnd`, `onUsageUpdate`, `toolUses` getter, `lifetimeUsage` getter.
|
|
152
|
+
Rename remaining tool-end logic to `onToolDone`.
|
|
153
|
+
Remove `addUsage` and `UsageDelta` imports.
|
|
154
|
+
2. `src/ui/ui-observer.ts` — Call `tracker.onToolDone` instead of `tracker.onToolEnd`.
|
|
155
|
+
Remove `message_end` usage accumulation block.
|
|
156
|
+
3. `src/agent-record.ts` — Add `get session()` and `get outputFile()` convenience getters.
|
|
157
|
+
Import `AgentSession` type for the return type.
|
|
158
|
+
4. `src/notification.ts` — Remove `NotificationDeps` interface.
|
|
159
|
+
Change `NotificationManager` constructor to plain parameters.
|
|
160
|
+
Replace `this.deps.*` with `this.*`.
|
|
161
|
+
Change `record.execution?.session` → `record.session`, `record.execution?.outputFile` → `record.outputFile`.
|
|
162
|
+
5. `src/ui/agent-widget.ts` — Read `a.toolUses`, `a.lifetimeUsage`, `a.session` instead of `bg?.toolUses ?? a.toolUses` etc.
|
|
163
|
+
6. `src/ui/conversation-viewer.ts` — Read `this.record.toolUses`, `this.record.lifetimeUsage`, `this.record.session` instead of fallback pattern.
|
|
164
|
+
7. `src/tools/get-result-tool.ts` — `record.execution?.session` → `record.session`.
|
|
165
|
+
8. `src/tools/steer-tool.ts` — `record.execution?.session` → `record.session`.
|
|
166
|
+
9. `src/ui/agent-menu.ts` — `record.execution?.session` → `record.session`.
|
|
167
|
+
10. `src/service-adapter.ts` — `record.execution?.session` → `record.session`.
|
|
168
|
+
11. `src/agent-manager.ts` — `record.execution?.session` → `record.session` in dispose paths.
|
|
169
|
+
Keep the `record.execution = { session, outputFile }` assignment unchanged (that's the write path).
|
|
170
|
+
12. `src/tools/foreground-runner.ts` — Read `record.toolUses` and `formatLifetimeTokens(record)` for final result.
|
|
171
|
+
Capture `recordRef` for streaming phase.
|
|
172
|
+
13. `src/tools/helpers.ts` — `buildDetails`: read `record.toolUses` and `record.lifetimeUsage` (already does); remove optional `activity` parameter's usage of `toolUses`/`lifetimeUsage` if present (verify).
|
|
173
|
+
14. `src/index.ts` — Update `NotificationManager` construction from bag to plain parameters.
|
|
174
|
+
15. `src/tools/background-spawner.ts` — `record?.execution?.outputFile` → `record?.outputFile`.
|
|
175
|
+
|
|
176
|
+
### Test files modified
|
|
177
|
+
|
|
178
|
+
1. `test/ui/agent-activity-tracker.test.ts` — Remove tests for `toolUses`, `lifetimeUsage`, `onToolEnd`, `onUsageUpdate`.
|
|
179
|
+
Add tests for `onToolDone` (active-tool removal without counting).
|
|
180
|
+
2. `test/ui/ui-observer.test.ts` — Update `tool_execution_end` expectations (calls `onToolDone` not `onToolEnd`).
|
|
181
|
+
Remove `message_end` usage accumulation tests.
|
|
182
|
+
3. `test/notification.test.ts` — Update `makeDeps()` factory to use plain parameters.
|
|
183
|
+
Or update `NotificationManager` construction call to match new signature.
|
|
184
|
+
4. `test/agent-record.test.ts` — Add tests for `session` and `outputFile` getters.
|
|
185
|
+
5. `test/conversation-viewer.test.ts` — Remove mock activity tracker usage for stats; tests use record stats directly.
|
|
186
|
+
|
|
187
|
+
## Test Impact Analysis
|
|
188
|
+
|
|
189
|
+
### New unit tests enabled
|
|
190
|
+
|
|
191
|
+
1. `AgentRecord.session` and `AgentRecord.outputFile` getters — straightforward getter tests on the record class.
|
|
192
|
+
2. `AgentActivityTracker.onToolDone` — verifies active-tool removal without side effects on a counter.
|
|
193
|
+
|
|
194
|
+
### Existing tests that become simpler
|
|
195
|
+
|
|
196
|
+
1. `agent-activity-tracker.test.ts` — 4 test blocks for `toolUses`, `lifetimeUsage`, `onToolEnd`, `onUsageUpdate` can be removed.
|
|
197
|
+
The `onToolDone` replacement needs fewer assertions (no counter check).
|
|
198
|
+
2. `ui-observer.test.ts` — The `message_end` usage accumulation tests can be removed.
|
|
199
|
+
The `tool_execution_end` test simplifies (verifies active-tool removal only).
|
|
200
|
+
3. `notification.test.ts` — The `makeDeps()` helper changes shape but stays the same size.
|
|
201
|
+
4. `conversation-viewer.test.ts` — Tests that mock `activity.toolUses` and `activity.lifetimeUsage` can simplify to reading from the record.
|
|
202
|
+
|
|
203
|
+
### Existing tests that must stay
|
|
204
|
+
|
|
205
|
+
1. `record-observer.test.ts` — All tests remain as-is.
|
|
206
|
+
The record observer is the sole source of `incrementToolUses` and `addUsage` calls now.
|
|
207
|
+
2. `agent-record.test.ts` — All existing tests for `incrementToolUses`, `addUsage`, `incrementCompactions` remain.
|
|
208
|
+
3. `notification.test.ts` — Pure helper tests (`escapeXml`, `getStatusLabel`, `formatTaskNotification`, `buildNotificationDetails`, `buildEventData`) remain unchanged.
|
|
209
|
+
|
|
210
|
+
## TDD Order
|
|
211
|
+
|
|
212
|
+
1. **Red→Green: `AgentRecord` convenience getters.**
|
|
213
|
+
Add tests for `record.session` and `record.outputFile` (returns `undefined` when no execution, delegates when set).
|
|
214
|
+
Implement the getters.
|
|
215
|
+
Commit: `feat: add session and outputFile convenience getters to AgentRecord (#144)`
|
|
216
|
+
|
|
217
|
+
2. **Green→Green: Migrate callsites from `execution?.` to convenience getters.**
|
|
218
|
+
Update all 12 callsites across 7 files (`notification.ts`, `agent-widget.ts`, `agent-menu.ts`, `get-result-tool.ts`, `steer-tool.ts`, `service-adapter.ts`, `agent-manager.ts`, `background-spawner.ts`).
|
|
219
|
+
No test changes needed — existing tests pass.
|
|
220
|
+
Run `pnpm run check` to verify.
|
|
221
|
+
Commit: `refactor: use AgentRecord.session and .outputFile convenience getters (#144)`
|
|
222
|
+
|
|
223
|
+
3. **Red→Green: Rename `onToolEnd` to `onToolDone` and remove counter.**
|
|
224
|
+
Update `agent-activity-tracker.test.ts`: remove `onToolEnd` counter tests, add `onToolDone` tests (active-tool removal only, no `toolUses` assertion).
|
|
225
|
+
Implement: rename method, remove `_toolUses++`.
|
|
226
|
+
Update `ui-observer.ts` to call `onToolDone`.
|
|
227
|
+
Update `ui-observer.test.ts` expectations.
|
|
228
|
+
Commit: `refactor: rename AgentActivityTracker.onToolEnd to onToolDone (#144)`
|
|
229
|
+
|
|
230
|
+
4. **Red→Green: Remove `_toolUses` and `_lifetimeUsage` from tracker.**
|
|
231
|
+
Update `agent-activity-tracker.test.ts`: remove `toolUses` getter, `lifetimeUsage` getter, and `onUsageUpdate` test blocks.
|
|
232
|
+
Implement: remove `_toolUses`, `_lifetimeUsage`, `toolUses` getter, `lifetimeUsage` getter, `onUsageUpdate` method, `UsageDelta` type, `addUsage` import.
|
|
233
|
+
Update `ui-observer.ts`: remove `message_end` usage accumulation block.
|
|
234
|
+
Update `ui-observer.test.ts`: remove `message_end` usage tests.
|
|
235
|
+
Run `pnpm run check` to catch any remaining references.
|
|
236
|
+
Commit: `refactor: remove duplicate stats from AgentActivityTracker (#144)`
|
|
237
|
+
|
|
238
|
+
5. **Green→Green: Migrate UI consumers to read stats from `AgentRecord`.**
|
|
239
|
+
Update `agent-widget.ts`: replace `bg?.toolUses ?? a.toolUses` → `a.toolUses`, `bg?.lifetimeUsage` → `a.lifetimeUsage`, `bg?.session` → `a.session`.
|
|
240
|
+
Update `conversation-viewer.ts`: replace `activity?.toolUses ?? record.toolUses` → `record.toolUses`, `activity?.lifetimeUsage` → `record.lifetimeUsage`, `activity?.session` → `record.session`.
|
|
241
|
+
Update `foreground-runner.ts`: read stats from record instead of tracker for final result.
|
|
242
|
+
Capture `recordRef` for streaming phase.
|
|
243
|
+
Update `conversation-viewer.test.ts` if any tests mock activity stats.
|
|
244
|
+
Commit: `refactor: read stats from AgentRecord in UI consumers (#144)`
|
|
245
|
+
|
|
246
|
+
6. **Red→Green: Replace `NotificationDeps` with plain parameters.**
|
|
247
|
+
Update `notification.test.ts`: change `makeDeps()` → individual parameters in `NotificationManager` constructor calls.
|
|
248
|
+
Implement: remove `NotificationDeps` interface, change constructor to plain parameters, replace `this.deps.*` with `this.*`.
|
|
249
|
+
Update `index.ts`: change `NotificationManager` construction to pass individual arguments.
|
|
250
|
+
Commit: `refactor: dissolve NotificationDeps into plain constructor parameters (#144)`
|
|
251
|
+
|
|
252
|
+
## Risks and Mitigations
|
|
253
|
+
|
|
254
|
+
| Risk | Mitigation |
|
|
255
|
+
| ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
256
|
+
| Foreground streaming reads stale `recordRef` before `onSessionCreated` fires | Before `onSessionCreated`, both tracker and record have zero stats — behavior is unchanged. Capture `recordRef` as `undefined` initially and guard with `??`. |
|
|
257
|
+
| Widget reads `a.toolUses` but `a` is the snapshot from `listAgents()` — might be stale | `listAgents()` returns live `AgentRecord` references (not copies), so `a.toolUses` reflects the latest record-observer increment. |
|
|
258
|
+
| Removing `lifetimeUsage` from tracker breaks `formatLifetimeTokens(fgState)` in foreground runner | Step 5 explicitly migrates this callsite to `formatLifetimeTokens(record)`. The TDD order places the removal (step 4) before the migration (step 5), but step 4's `pnpm run check` will catch the type error and both steps can be merged if needed. |
|
|
259
|
+
| `buildDetails` in `helpers.ts` accepts an `activity` parameter for `turnCount`/`maxTurns` | `turnCount` and `maxTurns` remain on the tracker — only `toolUses` and `lifetimeUsage` are removed. `buildDetails` reads `record.toolUses` and `record.lifetimeUsage` already; it reads `activity?.turnCount` and `activity?.maxTurns` which remain valid. |
|
|
260
|
+
|
|
261
|
+
## Open Questions
|
|
262
|
+
|
|
263
|
+
- None — the architecture doc prescribes the exact changes and the design is straightforward.
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 145
|
|
3
|
+
issue_title: "Decompose execute and push ExtensionContext to the boundary (Phase 9, Step M)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #145 — Decompose execute and push ExtensionContext to the boundary
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-23)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Extracted config resolution into a pure `resolveSpawnConfig` function, injected three collaborators (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) into `createAgentTool` to eliminate `ctx` reads from `execute`, pushed `ParentSnapshot` to `AgentManager`'s public API, and dissolved three small dependency bags (`ForegroundDeps`, `BackgroundDeps`, `AdapterDeps`) into plain parameters.
|
|
13
|
+
Released as `pi-subagents-v6.15.0`.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
#### What went well
|
|
18
|
+
|
|
19
|
+
- User's two escalating questions ("Are there any other missing collaborators?"
|
|
20
|
+
→ "Hiding dependencies in an object bag still counts as dependencies!") caught a `premature-convergence` before it landed as committed code.
|
|
21
|
+
The reverted partial step 3 attempt was ~4 files of changes that would have needed rework.
|
|
22
|
+
The resulting design (injected collaborators) is meaningfully better than the original plan's mechanical relocation.
|
|
23
|
+
- Folding tightly-coupled TDD steps (ctx elimination + params shrinking + deps dissolution) into fewer commits avoided intermediate states with broken types.
|
|
24
|
+
The plan's 12-step sequence would have required lift-and-shift gymnastics; the actual 7-commit sequence was cleaner.
|
|
25
|
+
|
|
26
|
+
#### What caused friction (agent side)
|
|
27
|
+
|
|
28
|
+
- `premature-convergence` — the original plan relocated `buildParentSnapshot` calls to `execute` without questioning whether `execute` should read `ctx` at all.
|
|
29
|
+
The existing `code-design` skill has DIP and parameter-relay rules that should have flagged this.
|
|
30
|
+
The `service-adapter.ts` module already demonstrated the getter-injection pattern (`getCtx`, `getModelRegistry`), but I didn't search for it during plan writing.
|
|
31
|
+
Impact: one plan rewrite commit (76bb57b), one reverted partial implementation (~15 minutes of rework).
|
|
32
|
+
User-caught.
|
|
33
|
+
|
|
34
|
+
- `missing-context` — didn't use `colgrep` during initial plan writing to discover the established getter-injection convention in `service-adapter.ts`.
|
|
35
|
+
Used `grep` exclusively for exact symbol matching.
|
|
36
|
+
When prompted by the user to use `colgrep`, the results were confirmatory rather than revelatory because I'd already read the relevant files by that point.
|
|
37
|
+
The miss was not using it *earlier* for intent-based exploration ("how do existing modules inject session-scoped state?").
|
|
38
|
+
Impact: added friction but no rework — the user's questions surfaced the pattern before code was committed.
|
|
39
|
+
User-caught.
|
|
40
|
+
|
|
41
|
+
- `instruction-violation` — wrote an inline `import()` type assertion (`session.ctx as import("@earendil-works/pi-coding-agent").ExtensionContext`) in `service-adapter.ts`.
|
|
42
|
+
AGENTS.md says "Use standard top-level imports only."
|
|
43
|
+
Impact: one extra edit round, caught before committing.
|
|
44
|
+
User-caught.
|
|
45
|
+
|
|
46
|
+
#### What caused friction (user side)
|
|
47
|
+
|
|
48
|
+
- The user's redirecting questions were well-timed and effective.
|
|
49
|
+
The escalation from "Are there any other missing collaborators?"
|
|
50
|
+
to the more pointed "Hiding dependencies in an object bag still counts as dependencies!"
|
|
51
|
+
was the right amount of pressure.
|
|
52
|
+
No friction observed on the user side.
|
|
53
|
+
|
|
54
|
+
### Changes made
|
|
55
|
+
|
|
56
|
+
1. `.pi/prompts/plan-issue.md` — added `colgrep` skill loading to the "Load skills" section for code-change plans, so convention discovery happens during exploration rather than after committing to a design.
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -350,7 +350,7 @@ export class AgentManager {
|
|
|
350
350
|
signal?: AbortSignal,
|
|
351
351
|
): Promise<AgentRecord | undefined> {
|
|
352
352
|
const record = this.agents.get(id);
|
|
353
|
-
const session = record?.
|
|
353
|
+
const session = record?.session;
|
|
354
354
|
if (!session) return undefined;
|
|
355
355
|
|
|
356
356
|
record.resetForResume(Date.now());
|
|
@@ -402,7 +402,7 @@ export class AgentManager {
|
|
|
402
402
|
|
|
403
403
|
/** Dispose a record's session and remove it from the map. */
|
|
404
404
|
private removeRecord(id: string, record: AgentRecord): void {
|
|
405
|
-
record.
|
|
405
|
+
record.session?.dispose?.();
|
|
406
406
|
this.agents.delete(id);
|
|
407
407
|
this.pendingSteers.delete(id);
|
|
408
408
|
}
|
|
@@ -480,7 +480,7 @@ export class AgentManager {
|
|
|
480
480
|
// Clear queue
|
|
481
481
|
this.queue = [];
|
|
482
482
|
for (const record of this.agents.values()) {
|
|
483
|
-
record.
|
|
483
|
+
record.session?.dispose();
|
|
484
484
|
}
|
|
485
485
|
this.agents.clear();
|
|
486
486
|
// Prune any orphaned git worktrees (crash recovery)
|
package/src/agent-record.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* after construction as lifecycle information becomes available.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import type { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
15
16
|
import type { ExecutionState } from "./execution-state.js";
|
|
16
17
|
import type { NotificationState } from "./notification-state.js";
|
|
17
18
|
import type { AgentInvocation, SubagentType } from "./types.js";
|
|
@@ -85,6 +86,16 @@ export class AgentRecord {
|
|
|
85
86
|
worktreeState?: WorktreeState;
|
|
86
87
|
notification?: NotificationState;
|
|
87
88
|
|
|
89
|
+
/** The active agent session, or undefined before the session is created. */
|
|
90
|
+
get session(): AgentSession | undefined {
|
|
91
|
+
return this.execution?.session;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Path to the agent's session JSONL file, or undefined if not yet available. */
|
|
95
|
+
get outputFile(): string | undefined {
|
|
96
|
+
return this.execution?.outputFile;
|
|
97
|
+
}
|
|
98
|
+
|
|
88
99
|
constructor(init: AgentRecordInit) {
|
|
89
100
|
this.id = init.id;
|
|
90
101
|
this.type = init.type;
|
package/src/index.ts
CHANGED
|
@@ -62,12 +62,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
62
62
|
// ---- Notification system ----
|
|
63
63
|
// runtime.widget is assigned after AgentManager construction; arrow closures
|
|
64
64
|
// capture `runtime` by reference so they always read the current value.
|
|
65
|
-
const notifications = new NotificationManager(
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
65
|
+
const notifications = new NotificationManager(
|
|
66
|
+
(msg, opts) => pi.sendMessage(msg, opts),
|
|
67
|
+
runtime.agentActivity,
|
|
68
|
+
(id) => runtime.markFinished(id),
|
|
69
|
+
() => runtime.updateWidget(),
|
|
70
|
+
);
|
|
71
71
|
|
|
72
72
|
// Settings: owns all three in-memory values and handles load/save/emit.
|
|
73
73
|
// onMaxConcurrentChanged is wired after manager is constructed (closure captures by reference).
|
package/src/notification.ts
CHANGED
|
@@ -46,7 +46,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
|
|
|
46
46
|
const status = getStatusLabel(record.status, record.error);
|
|
47
47
|
const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
|
|
48
48
|
const totalTokens = getLifetimeTotal(record.lifetimeUsage);
|
|
49
|
-
const contextPercent = getSessionContextPercent(record.
|
|
49
|
+
const contextPercent = getSessionContextPercent(record.session);
|
|
50
50
|
const ctxXml = contextPercent !== null ? `<context_percent>${Math.round(contextPercent)}</context_percent>` : "";
|
|
51
51
|
const compactXml = record.compactionCount ? `<compactions>${record.compactionCount}</compactions>` : "";
|
|
52
52
|
|
|
@@ -57,7 +57,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
|
|
|
57
57
|
: "No output.";
|
|
58
58
|
|
|
59
59
|
const toolCallId = record.notification?.toolCallId;
|
|
60
|
-
const outputFile = record.
|
|
60
|
+
const outputFile = record.outputFile;
|
|
61
61
|
return [
|
|
62
62
|
"<task-notification>",
|
|
63
63
|
`<task-id>${record.id}</task-id>`,
|
|
@@ -90,7 +90,7 @@ export function buildNotificationDetails(
|
|
|
90
90
|
maxTurns: activity?.maxTurns,
|
|
91
91
|
totalTokens,
|
|
92
92
|
durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
|
|
93
|
-
outputFile: record.
|
|
93
|
+
outputFile: record.outputFile,
|
|
94
94
|
error: record.error,
|
|
95
95
|
resultPreview: record.result
|
|
96
96
|
? record.result.length > resultMaxLen
|
|
@@ -124,17 +124,6 @@ export function buildEventData(record: AgentRecord) {
|
|
|
124
124
|
|
|
125
125
|
// ---- Notification system factory ----
|
|
126
126
|
|
|
127
|
-
/** Narrow deps for the notification system — only the methods it actually calls. */
|
|
128
|
-
export interface NotificationDeps {
|
|
129
|
-
sendMessage: (
|
|
130
|
-
msg: { customType: string; content: string; display: boolean; details?: unknown },
|
|
131
|
-
opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
132
|
-
) => void;
|
|
133
|
-
agentActivity: Map<string, AgentActivityTracker>;
|
|
134
|
-
markFinished: (id: string) => void;
|
|
135
|
-
updateWidget: () => void;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
127
|
export interface NotificationSystem {
|
|
139
128
|
cancelNudge: (key: string) => void;
|
|
140
129
|
sendCompletion: (record: AgentRecord) => void;
|
|
@@ -147,7 +136,15 @@ const NUDGE_HOLD_MS = 200;
|
|
|
147
136
|
export class NotificationManager implements NotificationSystem {
|
|
148
137
|
private pendingNudges = new Map<string, ReturnType<typeof setTimeout>>();
|
|
149
138
|
|
|
150
|
-
constructor(
|
|
139
|
+
constructor(
|
|
140
|
+
private sendMessage: (
|
|
141
|
+
msg: { customType: string; content: string; display: boolean; details?: unknown },
|
|
142
|
+
opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
|
|
143
|
+
) => void,
|
|
144
|
+
private agentActivity: Map<string, AgentActivityTracker>,
|
|
145
|
+
private markFinished: (id: string) => void,
|
|
146
|
+
private updateWidget: () => void,
|
|
147
|
+
) {}
|
|
151
148
|
|
|
152
149
|
cancelNudge(key: string): void {
|
|
153
150
|
const timer = this.pendingNudges.get(key);
|
|
@@ -158,16 +155,16 @@ export class NotificationManager implements NotificationSystem {
|
|
|
158
155
|
}
|
|
159
156
|
|
|
160
157
|
sendCompletion(record: AgentRecord): void {
|
|
161
|
-
this.
|
|
162
|
-
this.
|
|
158
|
+
this.agentActivity.delete(record.id);
|
|
159
|
+
this.markFinished(record.id);
|
|
163
160
|
this.scheduleNudge(record.id, () => this.emitIndividualNudge(record));
|
|
164
|
-
this.
|
|
161
|
+
this.updateWidget();
|
|
165
162
|
}
|
|
166
163
|
|
|
167
164
|
cleanupCompleted(id: string): void {
|
|
168
|
-
this.
|
|
169
|
-
this.
|
|
170
|
-
this.
|
|
165
|
+
this.agentActivity.delete(id);
|
|
166
|
+
this.markFinished(id);
|
|
167
|
+
this.updateWidget();
|
|
171
168
|
}
|
|
172
169
|
|
|
173
170
|
dispose(): void {
|
|
@@ -194,15 +191,15 @@ export class NotificationManager implements NotificationSystem {
|
|
|
194
191
|
if (record.notification?.resultConsumed) return;
|
|
195
192
|
|
|
196
193
|
const notification = formatTaskNotification(record, 500);
|
|
197
|
-
const outputFile = record.
|
|
194
|
+
const outputFile = record.outputFile;
|
|
198
195
|
const footer = outputFile ? `\nFull transcript available at: ${outputFile}` : "";
|
|
199
196
|
|
|
200
|
-
this.
|
|
197
|
+
this.sendMessage(
|
|
201
198
|
{
|
|
202
199
|
customType: "subagent-notification",
|
|
203
200
|
content: notification + footer,
|
|
204
201
|
display: true,
|
|
205
|
-
details: buildNotificationDetails(record, 500, this.
|
|
202
|
+
details: buildNotificationDetails(record, 500, this.agentActivity.get(record.id)),
|
|
206
203
|
},
|
|
207
204
|
{ deliverAs: "followUp", triggerTurn: true },
|
|
208
205
|
);
|
package/src/service-adapter.ts
CHANGED
|
@@ -88,7 +88,7 @@ export function createSubagentsService(
|
|
|
88
88
|
if (!record || record.status !== "running") {
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
|
-
const session = record.
|
|
91
|
+
const session = record.session;
|
|
92
92
|
if (!session) {
|
|
93
93
|
// Session not ready yet — queue via manager for delivery once initialized
|
|
94
94
|
return manager.queueSteer(id, message);
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -312,7 +312,7 @@ Guidelines:
|
|
|
312
312
|
`Agent not found: "${params.resume}". It may have been cleaned up.`,
|
|
313
313
|
);
|
|
314
314
|
}
|
|
315
|
-
if (!existing.
|
|
315
|
+
if (!existing.session) {
|
|
316
316
|
return textResult(
|
|
317
317
|
`Agent "${params.resume}" has no active session to resume.`,
|
|
318
318
|
);
|
|
@@ -79,7 +79,7 @@ export function spawnBackground(
|
|
|
79
79
|
`Agent ID: ${id}\n` +
|
|
80
80
|
`Type: ${config.displayName}\n` +
|
|
81
81
|
`Description: ${config.description}\n` +
|
|
82
|
-
(record?.
|
|
82
|
+
(record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
|
|
83
83
|
(isQueued
|
|
84
84
|
? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n`
|
|
85
85
|
: "") +
|
|
@@ -63,12 +63,14 @@ export async function runForeground(
|
|
|
63
63
|
|
|
64
64
|
const fgState = new AgentActivityTracker(config.effectiveMaxTurns);
|
|
65
65
|
let unsubUI: (() => void) | undefined;
|
|
66
|
+
let recordRef: AgentRecord | undefined;
|
|
66
67
|
|
|
67
68
|
const streamUpdate = () => {
|
|
69
|
+
const toolUses = recordRef?.toolUses ?? 0;
|
|
68
70
|
const details: AgentDetails = {
|
|
69
71
|
...config.detailBase,
|
|
70
|
-
toolUses
|
|
71
|
-
tokens: formatLifetimeTokens(
|
|
72
|
+
toolUses,
|
|
73
|
+
tokens: recordRef ? formatLifetimeTokens(recordRef) : "",
|
|
72
74
|
turnCount: fgState.turnCount,
|
|
73
75
|
maxTurns: fgState.maxTurns,
|
|
74
76
|
durationMs: Date.now() - startedAt,
|
|
@@ -77,7 +79,7 @@ export async function runForeground(
|
|
|
77
79
|
spinnerFrame: spinnerFrame % SPINNER.length,
|
|
78
80
|
};
|
|
79
81
|
onUpdate?.({
|
|
80
|
-
content: [{ type: "text", text: `${
|
|
82
|
+
content: [{ type: "text", text: `${toolUses} tool uses...` }],
|
|
81
83
|
details: details as any,
|
|
82
84
|
});
|
|
83
85
|
};
|
|
@@ -110,6 +112,7 @@ export async function runForeground(
|
|
|
110
112
|
parentSessionId: params.parentSessionId,
|
|
111
113
|
onSessionCreated: (session, record) => {
|
|
112
114
|
fgState.setSession(session);
|
|
115
|
+
recordRef = record;
|
|
113
116
|
unsubUI = subscribeUIObserver(session, fgState, streamUpdate);
|
|
114
117
|
fgId = record.id;
|
|
115
118
|
agentActivity.set(record.id, fgState);
|
|
@@ -132,7 +135,7 @@ export async function runForeground(
|
|
|
132
135
|
widget.markFinished(fgId);
|
|
133
136
|
}
|
|
134
137
|
|
|
135
|
-
const tokenText = formatLifetimeTokens(
|
|
138
|
+
const tokenText = formatLifetimeTokens(record);
|
|
136
139
|
const details = buildDetails(config.detailBase, record, fgState, { tokens: tokenText });
|
|
137
140
|
|
|
138
141
|
const fallbackNote = config.fellBack
|
|
@@ -65,7 +65,7 @@ export function createGetResultTool(deps: GetResultDeps) {
|
|
|
65
65
|
const displayName = getDisplayName(record.type, deps.registry);
|
|
66
66
|
const duration = formatDuration(record.startedAt, record.completedAt);
|
|
67
67
|
const tokens = formatLifetimeTokens(record);
|
|
68
|
-
const contextPercent = getSessionContextPercent(record.
|
|
68
|
+
const contextPercent = getSessionContextPercent(record.session);
|
|
69
69
|
const statsParts = [`Tool uses: ${record.toolUses}`];
|
|
70
70
|
if (tokens) statsParts.push(tokens);
|
|
71
71
|
if (contextPercent !== null) statsParts.push(`Context: ${Math.round(contextPercent)}%`);
|
|
@@ -92,8 +92,8 @@ export function createGetResultTool(deps: GetResultDeps) {
|
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
// Verbose: include full conversation
|
|
95
|
-
if (params.verbose && record.
|
|
96
|
-
const conversation = deps.getConversation(record.
|
|
95
|
+
if (params.verbose && record.session) {
|
|
96
|
+
const conversation = deps.getConversation(record.session);
|
|
97
97
|
if (conversation) {
|
|
98
98
|
output += `\n\n--- Agent Conversation ---\n${conversation}`;
|
|
99
99
|
}
|
package/src/tools/steer-tool.ts
CHANGED
|
@@ -49,7 +49,7 @@ export function createSteerTool(deps: SteerToolDeps) {
|
|
|
49
49
|
`Agent "${params.agent_id}" is not running (status: ${record.status}). Cannot steer a non-running agent.`,
|
|
50
50
|
);
|
|
51
51
|
}
|
|
52
|
-
const session = record.
|
|
52
|
+
const session = record.session;
|
|
53
53
|
if (!session) {
|
|
54
54
|
// Session not ready yet — queue via manager for delivery once initialized
|
|
55
55
|
deps.queueSteer(record.id, params.message);
|
|
@@ -5,24 +5,15 @@
|
|
|
5
5
|
* in `ui-observer.ts`. Callers use named transition methods; readers use read-only accessors.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
/** Usage delta accepted by onUsageUpdate — matches the LifetimeUsage accumulator shape. */
|
|
11
|
-
export interface UsageDelta {
|
|
12
|
-
input: number;
|
|
13
|
-
output: number;
|
|
14
|
-
cacheWrite: number;
|
|
15
|
-
}
|
|
8
|
+
import type { SessionLike } from "../usage.js";
|
|
16
9
|
|
|
17
10
|
/** Per-agent live activity state with explicit transition methods and read-only accessors. */
|
|
18
11
|
export class AgentActivityTracker {
|
|
19
12
|
private _activeTools = new Map<string, string>();
|
|
20
13
|
private _toolKeySeq = 0;
|
|
21
|
-
private _toolUses = 0;
|
|
22
14
|
private _responseText = "";
|
|
23
15
|
private _session: SessionLike | undefined = undefined;
|
|
24
16
|
private _turnCount = 1;
|
|
25
|
-
private _lifetimeUsage: LifetimeUsage = { input: 0, output: 0, cacheWrite: 0 };
|
|
26
17
|
|
|
27
18
|
constructor(private readonly _maxTurns?: number) {}
|
|
28
19
|
|
|
@@ -33,12 +24,11 @@ export class AgentActivityTracker {
|
|
|
33
24
|
this._activeTools.set(toolName + "_" + (++this._toolKeySeq), toolName);
|
|
34
25
|
}
|
|
35
26
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
27
|
+
/** Remove a tool from active tools (called when tool execution ends). No-op when no matching tool is active. */
|
|
28
|
+
onToolDone(toolName: string): void {
|
|
38
29
|
for (const [key, name] of this._activeTools) {
|
|
39
30
|
if (name === toolName) {
|
|
40
31
|
this._activeTools.delete(key);
|
|
41
|
-
this._toolUses++;
|
|
42
32
|
break;
|
|
43
33
|
}
|
|
44
34
|
}
|
|
@@ -59,11 +49,6 @@ export class AgentActivityTracker {
|
|
|
59
49
|
this._turnCount++;
|
|
60
50
|
}
|
|
61
51
|
|
|
62
|
-
/** Accumulate a usage delta into the lifetime usage totals. */
|
|
63
|
-
onUsageUpdate(delta: UsageDelta): void {
|
|
64
|
-
addUsage(this._lifetimeUsage, delta);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
52
|
/** Bind the session reference (called once when the agent session is created). */
|
|
68
53
|
setSession(session: SessionLike): void {
|
|
69
54
|
this._session = session;
|
|
@@ -76,11 +61,6 @@ export class AgentActivityTracker {
|
|
|
76
61
|
return this._activeTools;
|
|
77
62
|
}
|
|
78
63
|
|
|
79
|
-
/** Total completed tool invocations. */
|
|
80
|
-
get toolUses(): number {
|
|
81
|
-
return this._toolUses;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
64
|
/** The agent's latest partial response text (reset at each message start). */
|
|
85
65
|
get responseText(): string {
|
|
86
66
|
return this._responseText;
|
|
@@ -101,8 +81,4 @@ export class AgentActivityTracker {
|
|
|
101
81
|
return this._maxTurns;
|
|
102
82
|
}
|
|
103
83
|
|
|
104
|
-
/** Accumulated lifetime token usage (survives compaction). */
|
|
105
|
-
get lifetimeUsage(): Readonly<LifetimeUsage> {
|
|
106
|
-
return this._lifetimeUsage;
|
|
107
|
-
}
|
|
108
84
|
}
|
package/src/ui/agent-menu.ts
CHANGED
|
@@ -207,7 +207,7 @@ export function createAgentsMenuHandler(deps: AgentMenuDeps) {
|
|
|
207
207
|
}
|
|
208
208
|
|
|
209
209
|
async function viewAgentConversation(ctx: ExtensionContext, record: AgentRecord) {
|
|
210
|
-
const session = record.
|
|
210
|
+
const session = record.session;
|
|
211
211
|
if (!session) {
|
|
212
212
|
ctx.ui.notify(
|
|
213
213
|
`Agent is ${record.status === "queued" ? "queued" : "expired"} — no session available.`,
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -191,14 +191,13 @@ export class AgentWidget {
|
|
|
191
191
|
const elapsed = formatMs(Date.now() - a.startedAt);
|
|
192
192
|
|
|
193
193
|
const bg = this.agentActivity.get(a.id);
|
|
194
|
-
const
|
|
195
|
-
const
|
|
196
|
-
const contextPercent = getSessionContextPercent(bg?.session);
|
|
194
|
+
const tokens = getLifetimeTotal(a.lifetimeUsage);
|
|
195
|
+
const contextPercent = getSessionContextPercent(a.session);
|
|
197
196
|
const tokenText = tokens > 0 ? formatSessionTokens(tokens, contextPercent, theme, a.compactionCount) : "";
|
|
198
197
|
|
|
199
198
|
const parts: string[] = [];
|
|
200
199
|
if (bg) parts.push(formatTurns(bg.turnCount, bg.maxTurns));
|
|
201
|
-
if (toolUses > 0) parts.push(`${toolUses} tool use${toolUses === 1 ? "" : "s"}`);
|
|
200
|
+
if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
|
|
202
201
|
if (tokenText) parts.push(tokenText);
|
|
203
202
|
parts.push(elapsed);
|
|
204
203
|
const statsText = parts.join(" · ");
|
|
@@ -155,11 +155,11 @@ export class ConversationViewer implements Component {
|
|
|
155
155
|
const duration = formatDuration(this.record.startedAt, this.record.completedAt);
|
|
156
156
|
|
|
157
157
|
const headerParts: string[] = [duration];
|
|
158
|
-
const toolUses = this.
|
|
158
|
+
const toolUses = this.record.toolUses;
|
|
159
159
|
if (toolUses > 0) headerParts.unshift(`${toolUses} tool${toolUses === 1 ? "" : "s"}`);
|
|
160
|
-
const tokens = getLifetimeTotal(this.
|
|
160
|
+
const tokens = getLifetimeTotal(this.record.lifetimeUsage);
|
|
161
161
|
if (tokens > 0) {
|
|
162
|
-
const percent = getSessionContextPercent(this.
|
|
162
|
+
const percent = getSessionContextPercent(this.record.session);
|
|
163
163
|
headerParts.push(formatSessionTokens(tokens, percent, th, this.record.compactionCount));
|
|
164
164
|
}
|
|
165
165
|
|
package/src/ui/ui-observer.ts
CHANGED
|
@@ -40,7 +40,7 @@ export function subscribeUIObserver(
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (event.type === "tool_execution_end") {
|
|
43
|
-
tracker.
|
|
43
|
+
tracker.onToolDone(event.toolName);
|
|
44
44
|
onUpdate?.();
|
|
45
45
|
}
|
|
46
46
|
|
|
@@ -61,16 +61,5 @@ export function subscribeUIObserver(
|
|
|
61
61
|
onUpdate?.();
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
if (event.type === "message_end" && event.message?.role === "assistant") {
|
|
65
|
-
const u = event.message.usage;
|
|
66
|
-
if (u) {
|
|
67
|
-
tracker.onUsageUpdate({
|
|
68
|
-
input: u.input ?? 0,
|
|
69
|
-
output: u.output ?? 0,
|
|
70
|
-
cacheWrite: u.cacheWrite ?? 0,
|
|
71
|
-
});
|
|
72
|
-
onUpdate?.();
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
64
|
});
|
|
76
65
|
}
|