@gotgenes/pi-subagents 6.16.0 → 6.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,255 @@
1
+ ---
2
+ issue: 148
3
+ issue_title: "Split AgentWidget rendering from lifecycle (Phase 9, Step P)"
4
+ ---
5
+
6
+ # Split AgentWidget rendering from lifecycle
7
+
8
+ ## Problem Statement
9
+
10
+ `AgentWidget` (374 lines) mixes rendering, lifecycle management, spinner animation, state filtering, and status bar management in a single class.
11
+ `renderWidget` alone is ~109 lines, and `renderFinishedLine` adds another ~40.
12
+ The constructor takes 3 concrete collaborators (`AgentManager`, `Map<string, AgentActivityTracker>`, `AgentTypeRegistry`) with no interface extraction.
13
+ Rendering logic cannot be unit-tested without instantiating the full widget with its lifecycle machinery.
14
+
15
+ ## Goals
16
+
17
+ - Extract pure rendering functions from `AgentWidget` into `ui/widget-renderer.ts`.
18
+ - Make `AgentWidget` a thin lifecycle/polling wrapper that delegates to pure render functions.
19
+ - Enable direct unit testing of rendering logic with plain data — no widget lifecycle, no mocks for `setInterval`/`setWidget`/`requestRender`.
20
+
21
+ ## Non-Goals
22
+
23
+ - Changing the visual output of the widget (this is a pure refactor).
24
+ - Extracting the status bar logic into a separate module (could follow up).
25
+ - Narrowing the `AgentManager` dependency to an interface (tracked separately in the architecture doc).
26
+ - Injecting `truncateToWidth` (tracked as #147, Step O — an independent track).
27
+
28
+ ## Background
29
+
30
+ ### Dependency: #144 (Step L) — Consolidate observation model
31
+
32
+ Issue #144 is **closed/implemented**.
33
+ The renderer now reads stats (`toolUses`, `lifetimeUsage`, `compactionCount`) from `AgentRecord` and live UI state (`activeTools`, `responseText`, `turnCount`, `maxTurns`) from `AgentActivityTracker`.
34
+ No dual-counting fallback exists.
35
+
36
+ ### Existing pure helpers
37
+
38
+ `ui/display.ts` already contains stateless formatting functions (`formatMs`, `formatTurns`, `formatSessionTokens`, `describeActivity`, `getDisplayName`, `getPromptModeLabel`, `SPINNER`, `ERROR_STATUSES`, `Theme`).
39
+ The new `widget-renderer.ts` will consume these — it does not duplicate them.
40
+
41
+ ### Rendering data flow
42
+
43
+ The widget's `renderWidget` currently:
44
+
45
+ 1. Calls `this.manager.listAgents()` to get `AgentRecord[]`.
46
+ 2. Categorizes into running/queued/finished.
47
+ 3. Filters finished agents via `this.shouldShowFinished()`.
48
+ 4. Looks up `this.agentActivity.get(a.id)` for live stats.
49
+ 5. Calls `this.registry` for display names.
50
+ 6. Reads `this.widgetFrame` for spinner animation.
51
+ 7. Assembles tree-style lines with overflow logic.
52
+
53
+ Steps 3–7 are pure given the right inputs.
54
+ Steps 1–2 are also pure categorization.
55
+
56
+ ## Design Overview
57
+
58
+ ### Separation of concerns
59
+
60
+ The rendering extraction splits the widget into two layers:
61
+
62
+ 1. **`widget-renderer.ts`** — Pure functions that accept data and return `string[]`.
63
+ No `this`, no timers, no SDK types, no side effects.
64
+ 2. **`agent-widget.ts`** — Thin lifecycle wrapper that owns timers, UICtx, finished-turn aging, and calls the renderer with live data.
65
+
66
+ ### Renderer input shape
67
+
68
+ Rather than passing the full `AgentRecord` class (which carries mutation methods and phase collaborators), the renderer receives a plain data slice:
69
+
70
+ ```typescript
71
+ /** Minimal agent snapshot for rendering — no class methods, no mutation surface. */
72
+ export interface WidgetAgent {
73
+ readonly id: string;
74
+ readonly type: SubagentType;
75
+ readonly status: string;
76
+ readonly description: string;
77
+ readonly toolUses: number;
78
+ readonly startedAt: number;
79
+ readonly completedAt?: number;
80
+ readonly error?: string;
81
+ readonly lifetimeUsage?: Readonly<LifetimeUsage>;
82
+ readonly compactionCount: number;
83
+ }
84
+ ```
85
+
86
+ This is structurally compatible with `AgentRecord` (the class satisfies it), so no mapping code is needed at the call site — `listAgents()` returns `AgentRecord[]` which satisfies `WidgetAgent[]`.
87
+
88
+ ### Renderer input for activity
89
+
90
+ Activity state is read from `AgentActivityTracker`.
91
+ The renderer needs a read-only view per agent:
92
+
93
+ ```typescript
94
+ /** Read-only activity snapshot for widget rendering. */
95
+ export interface WidgetActivity {
96
+ readonly activeTools: ReadonlyMap<string, string>;
97
+ readonly responseText: string;
98
+ readonly turnCount: number;
99
+ readonly maxTurns?: number;
100
+ readonly session?: SessionLike;
101
+ }
102
+ ```
103
+
104
+ `AgentActivityTracker` already satisfies this structurally (it exposes these as getters).
105
+
106
+ ### Agent config lookup
107
+
108
+ The renderer needs `getDisplayName` and `getPromptModeLabel`, which take a `SubagentType` and an `AgentConfigLookup`.
109
+ The renderer accepts `AgentConfigLookup` (the existing interface from `agent-types.ts`) — not the concrete `AgentTypeRegistry` class.
110
+
111
+ ### Renderer API
112
+
113
+ ```typescript
114
+ /** Pure rendering of the widget body. Returns lines to display. */
115
+ export function renderWidgetLines(params: {
116
+ agents: readonly WidgetAgent[];
117
+ activityMap: ReadonlyMap<string, WidgetActivity>;
118
+ registry: AgentConfigLookup;
119
+ spinnerFrame: number;
120
+ terminalWidth: number;
121
+ shouldShowFinished: (agentId: string, status: string) => boolean;
122
+ }): string[];
123
+
124
+ /** Pure rendering of a single finished agent line (no tree connector prefix). */
125
+ export function renderFinishedLine(
126
+ agent: WidgetAgent,
127
+ activity: WidgetActivity | undefined,
128
+ registry: AgentConfigLookup,
129
+ theme: Theme,
130
+ ): string;
131
+
132
+ /** Pure rendering of a single running agent (header + activity lines, no tree connector prefix). */
133
+ export function renderRunningLines(
134
+ agent: WidgetAgent,
135
+ activity: WidgetActivity | undefined,
136
+ registry: AgentConfigLookup,
137
+ spinnerFrame: number,
138
+ theme: Theme,
139
+ ): [header: string, activity: string];
140
+ ```
141
+
142
+ The top-level `renderWidgetLines` encapsulates the full categorization, overflow logic, and tree-connector fixup.
143
+ The per-agent functions are exported for fine-grained testing.
144
+
145
+ The `shouldShowFinished` callback is injected rather than re-implementing the aging logic inside the renderer, keeping the renderer pure and the aging state in the widget.
146
+
147
+ ### Call site in AgentWidget
148
+
149
+ ```typescript
150
+ // Inside renderWidget(tui, theme):
151
+ const w = tui.terminal.columns;
152
+ return renderWidgetLines({
153
+ agents: this.manager.listAgents(),
154
+ activityMap: this.agentActivity,
155
+ registry: this.registry,
156
+ spinnerFrame: this.widgetFrame,
157
+ terminalWidth: w,
158
+ shouldShowFinished: (id, status) => this.shouldShowFinished(id, status),
159
+ });
160
+ ```
161
+
162
+ The widget's `renderWidget` method shrinks to ~5 lines.
163
+
164
+ ### Tell-Don't-Ask verification
165
+
166
+ The renderer receives pre-collected data and returns formatted strings.
167
+ It does not reach through collaborators — it reads flat fields from `WidgetAgent` and `WidgetActivity`.
168
+ The widget tells the renderer "render this data"; the renderer returns lines.
169
+ No Law of Demeter violations.
170
+
171
+ ## Module-Level Changes
172
+
173
+ ### New file: `src/ui/widget-renderer.ts`
174
+
175
+ - `WidgetAgent` interface (structural subset of `AgentRecord`).
176
+ - `WidgetActivity` interface (structural subset of `AgentActivityTracker`).
177
+ - `renderWidgetLines()` — top-level rendering with categorization, overflow, tree connectors.
178
+ - `renderFinishedLine()` — single finished-agent line.
179
+ - `renderRunningLines()` — single running-agent header + activity pair.
180
+ - Imports from `display.ts` (`SPINNER`, `ERROR_STATUSES`, `formatMs`, `formatTurns`, `formatSessionTokens`, `describeActivity`, `getDisplayName`, `getPromptModeLabel`, `Theme`), from `usage.ts` (`getLifetimeTotal`, `getSessionContextPercent`, `LifetimeUsage`, `SessionLike`), and from `@earendil-works/pi-tui` (`truncateToWidth`).
181
+
182
+ ### Modified: `src/ui/agent-widget.ts`
183
+
184
+ - Remove `renderWidget()` method body — replace with call to `renderWidgetLines()`.
185
+ - Remove `renderFinishedLine()` method entirely.
186
+ - Remove direct imports of display helpers and usage helpers that are now only consumed by `widget-renderer.ts`.
187
+ - Keep: constructor, `setUICtx`, `onTurnStart`, `ensureTimer`, `shouldShowFinished`, `markFinished`, `update`, `dispose`, `UICtx` type, `MAX_WIDGET_LINES`.
188
+ - The inline type on `renderFinishedLine`'s parameter `a` is replaced by the `WidgetAgent` import.
189
+
190
+ ### New file: `test/widget-renderer.test.ts`
191
+
192
+ - Unit tests for `renderWidgetLines`, `renderFinishedLine`, `renderRunningLines`.
193
+ - Uses plain data objects (no mocks for `AgentManager`, `setInterval`, or SDK).
194
+ - Stub `Theme` matching the pattern in `test/renderer.test.ts`.
195
+
196
+ ### No changes to
197
+
198
+ - `src/index.ts` — the widget is constructed the same way; renderer is internal to the widget module.
199
+ - `src/ui/display.ts` — unchanged; consumed by the new renderer.
200
+ - `src/usage.ts` — unchanged.
201
+
202
+ ## Test Impact Analysis
203
+
204
+ 1. The extraction enables direct unit testing of widget rendering that was previously impossible — testing `renderWidget` required constructing a full `AgentWidget` with mocked `AgentManager`, fake timers, and a stubbed UICtx.
205
+ The new tests cover: finished-agent line formatting (all status variants), running-agent header/activity rendering, overflow logic, tree-connector fixup, empty-state handling, and `shouldShowFinished` filtering.
206
+ 2. No existing tests become redundant — there are currently **no** unit tests for `AgentWidget` rendering.
207
+ The existing `display.test.ts` tests lower-level formatters and remains as-is.
208
+ 3. `renderer.test.ts` tests the notification renderer — unrelated, stays as-is.
209
+
210
+ ## TDD Order
211
+
212
+ 1. **Red → Green:** Test `renderFinishedLine` for a completed agent (success icon, stats, duration).
213
+ Commit: `test: add renderFinishedLine tests for completed status`
214
+
215
+ 2. **Red → Green:** Test `renderFinishedLine` for error/aborted/steered/stopped statuses (icon and status text variations).
216
+ Commit: `test: renderFinishedLine error and terminal status variants`
217
+
218
+ 3. **Red → Green:** Test `renderRunningLines` (spinner frame, stats, activity description, token display).
219
+ Commit: `test: add renderRunningLines tests`
220
+
221
+ 4. **Red → Green:** Test `renderWidgetLines` — basic case with one running agent (heading, tree connectors).
222
+ Commit: `test: renderWidgetLines single running agent`
223
+
224
+ 5. **Red → Green:** Test `renderWidgetLines` — mixed running + finished + queued, verifying categorization and ordering.
225
+ Commit: `test: renderWidgetLines mixed agent states`
226
+
227
+ 6. **Red → Green:** Test `renderWidgetLines` — overflow cap with many agents, verifying the priority (running > queued > finished) and overflow summary line.
228
+ Commit: `test: renderWidgetLines overflow behavior`
229
+
230
+ 7. **Red → Green:** Test `renderWidgetLines` — empty state returns `[]`; finished-only state uses dim heading.
231
+ Commit: `test: renderWidgetLines empty and finished-only states`
232
+
233
+ 8. **Green → Refactor:** Extract `renderFinishedLine`, `renderRunningLines`, and `renderWidgetLines` into `src/ui/widget-renderer.ts`.
234
+ All tests pass against the extracted module.
235
+ Commit: `refactor: extract widget rendering into widget-renderer`
236
+
237
+ 9. **Green → Refactor:** Wire `AgentWidget.renderWidget()` to delegate to `renderWidgetLines()`.
238
+ Remove the inlined rendering logic from `agent-widget.ts`.
239
+ Remove unused imports.
240
+ Commit: `refactor: AgentWidget delegates rendering to widget-renderer`
241
+
242
+ 10. **Verify:** Run full test suite (`pnpm vitest run`) and type check (`pnpm run check`).
243
+ Commit: none (verification only).
244
+
245
+ ## Risks and Mitigations
246
+
247
+ | Risk | Mitigation |
248
+ | -------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
249
+ | Structural compatibility between `AgentRecord` and `WidgetAgent` could drift if `AgentRecord` renames a field. | TypeScript's structural checking catches this at the call site in `agent-widget.ts` — `listAgents()` returns `AgentRecord[]` which must satisfy `readonly WidgetAgent[]`. |
250
+ | `truncateToWidth` is an external dependency (`@earendil-works/pi-tui`) in the renderer. | Step O (#147) will inject it; for now, the renderer imports it directly, matching the current widget behavior. |
251
+ | Overflow logic is complex and hand-tested — extraction could introduce subtle line-count bugs. | TDD steps 4–7 exercise overflow edge cases before the extraction step. The extraction is a mechanical move with tests already passing. |
252
+
253
+ ## Open Questions
254
+
255
+ - None — the design follows the architecture doc's Step P specification and the dependency (#144) is already implemented.
@@ -0,0 +1,39 @@
1
+ ---
2
+ issue: 144
3
+ issue_title: "Consolidate observation model (Phase 9, Step L)"
4
+ ---
5
+
6
+ # Retro: #144 — Consolidate observation model
7
+
8
+ ## Final Retrospective (2026-05-23)
9
+
10
+ ### Session summary
11
+
12
+ Planned and implemented the Phase 9 Step L observation model consolidation.
13
+ Removed dual `_toolUses`/`_lifetimeUsage` counting from `AgentActivityTracker`, added `session`/`outputFile` convenience getters to `AgentRecord`, migrated 14 callsites, and dissolved `NotificationDeps` into plain constructor parameters.
14
+ Released as `pi-subagents-v6.16.0`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - The plan correctly anticipated that TDD steps 4 (remove tracker stats) and 5 (migrate UI consumers) would be type-coupled and need merging.
21
+ This played out exactly as predicted — no surprise rework.
22
+ - Step 2's grep sweep during `execution?.` migration found two callsites (`agent-tool.ts:315`, `agent-manager.ts:353`) that the plan's file list missed.
23
+ Systematic grep at migration time caught them before commit.
24
+
25
+ #### What caused friction (agent side)
26
+
27
+ - `instruction-violation` — Did not load the `colgrep` skill during the planning phase despite two explicit instructions: AGENTS.md ("Use `colgrep` for intent-based codebase exploration") and the `/plan-issue` prompt ("load the `code-design` skill and the `colgrep` skill for convention discovery").
28
+ Loaded 4 other skills but skipped colgrep.
29
+ User-caught ("I noticed you didn't load or use `colgrep`").
30
+ Impact: one extra round-trip with the user; no rework since the plan hadn't been committed yet.
31
+ The colgrep searches proved useful once run — highest-scoring hit for "dependency bag converted to plain constructor parameters" was `notification.ts`, directly confirming the target.
32
+ - `wrong-abstraction` — When editing `src/ui/ui-observer.ts` to remove the `message_end` accumulation block, the replacement text closed the `session.subscribe(...)` callback but also added the function's closing brace, producing a duplicate `}`.
33
+ Autoformat caught the parse error immediately.
34
+ Impact: one follow-up edit, no downstream rework.
35
+
36
+ #### What caused friction (user side)
37
+
38
+ - None observed.
39
+ The user's intervention on colgrep was timely — caught before plan commit, not after.
@@ -0,0 +1,39 @@
1
+ ---
2
+ issue: 148
3
+ issue_title: "Split AgentWidget rendering from lifecycle (Phase 9, Step P)"
4
+ ---
5
+
6
+ # Retro: #148 — Split AgentWidget rendering from lifecycle
7
+
8
+ ## Final Retrospective (2026-05-23T06:20:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Extracted pure rendering functions (`renderWidgetLines`, `renderFinishedLine`, `renderRunningLines`) from `AgentWidget` into `ui/widget-renderer.ts`.
13
+ The widget shrank from 374 to 198 lines — now a thin lifecycle wrapper. 23 new unit tests cover all status variants, overflow, tree connectors, and empty states.
14
+ Released as `pi-subagents-v6.16.1`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - TDD cycles were fast and clean: 9 commits, all tests green on first or second try, zero test regressions across the full 806-test suite.
21
+ - The `WidgetAgent` / `WidgetActivity` interfaces worked well as structural subsets — `AgentRecord` and `AgentActivityTracker` satisfy them without mapping code.
22
+ - The stub `Theme` pattern from `test/renderer.test.ts` (`fg: (c, t) => \`[\${c}:\${t}]\``) transferred cleanly to the new test file, keeping assertions readable.
23
+
24
+ #### What caused friction (agent side)
25
+
26
+ - `missing-context` — The plan's `renderWidgetLines` API spec omitted `theme` from its parameters, even though the heading, tree connectors, and per-agent render calls all require it.
27
+ Caught immediately at step 4 (first `renderWidgetLines` test) and fixed by adding `theme` to the params.
28
+ Impact: deviation note in commit body; no rework.
29
+
30
+ - `missing-context` — Step 3 (`renderRunningLines` implementation) initially missed importing `SPINNER` from `display.ts`.
31
+ The test caught it as a runtime `ReferenceError`, fixed in the same Red→Green cycle.
32
+ Impact: added friction but no rework.
33
+
34
+ - `wrong-abstraction` — Step 8 ("Extract into `widget-renderer.ts`") was a no-op because the module was created incrementally during steps 1–7 (tests must import from the new module to run).
35
+ Impact: step skipped; noted in summary.
36
+
37
+ #### What caused friction (user side)
38
+
39
+ - None observed — the session ran autonomously from plan through ship with no user corrections needed.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.16.0",
3
+ "version": "6.16.2",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
package/src/index.ts CHANGED
@@ -227,21 +227,21 @@ export default function (pi: ExtensionAPI) {
227
227
 
228
228
  // ---- get_subagent_result tool ----
229
229
 
230
- pi.registerTool(defineTool(createGetResultTool({
231
- getRecord: (id) => manager.getRecord(id),
232
- cancelNudge: (key) => notifications.cancelNudge(key),
233
- getConversation: (session) => getAgentConversation(session),
230
+ pi.registerTool(defineTool(createGetResultTool(
231
+ (id) => manager.getRecord(id),
232
+ (key) => notifications.cancelNudge(key),
233
+ (session) => getAgentConversation(session),
234
234
  registry,
235
- })));
235
+ )));
236
236
 
237
237
  // ---- steer_subagent tool ----
238
238
 
239
- pi.registerTool(defineTool(createSteerTool({
240
- getRecord: (id) => manager.getRecord(id),
241
- emitEvent: (name, data) => pi.events.emit(name, data),
242
- steerAgent: (session, message) => steerAgent(session, message),
243
- queueSteer: (id, message) => manager.queueSteer(id, message),
244
- })));
239
+ pi.registerTool(defineTool(createSteerTool(
240
+ (id) => manager.getRecord(id),
241
+ (name, data) => pi.events.emit(name, data),
242
+ (session, message) => steerAgent(session, message),
243
+ (id, message) => manager.queueSteer(id, message),
244
+ )));
245
245
 
246
246
  // ---- /agents interactive menu ----
247
247
 
@@ -249,7 +249,7 @@ export default function (pi: ExtensionAPI) {
249
249
  manager: {
250
250
  listAgents: () => manager.listAgents(),
251
251
  getRecord: (id) => manager.getRecord(id),
252
- spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(buildParentSnapshot(ctx), type, prompt, opts),
252
+ spawnAndWait: (snapshot, type, prompt, opts) => manager.spawnAndWait(snapshot, type, prompt, opts),
253
253
  },
254
254
  registry,
255
255
  agentActivity: runtime.agentActivity,
@@ -270,6 +270,12 @@ export default function (pi: ExtensionAPI) {
270
270
 
271
271
  pi.registerCommand('agents', {
272
272
  description: 'Manage agents',
273
- handler: async (_args, ctx) => { await agentsMenuHandler(ctx); },
273
+ handler: async (_args, ctx) => {
274
+ await agentsMenuHandler({
275
+ ui: ctx.ui,
276
+ modelRegistry: ctx.modelRegistry,
277
+ parentSnapshot: buildParentSnapshot(ctx),
278
+ });
279
+ },
274
280
  });
275
281
  }
@@ -6,16 +6,13 @@ import { formatDuration, getDisplayName } from "../ui/display.js";
6
6
  import { getSessionContextPercent } from "../usage.js";
7
7
  import { formatLifetimeTokens, textResult } from "./helpers.js";
8
8
 
9
- /** Narrow deps — only the methods this tool's execute callback calls. */
10
- export interface GetResultDeps {
11
- getRecord: (id: string) => AgentRecord | undefined;
12
- cancelNudge: (key: string) => void;
13
- getConversation: (session: AgentSession) => string | undefined;
14
- registry: AgentConfigLookup;
15
- }
16
-
17
9
  /** Create the get_subagent_result tool definition (without Pi SDK wrapper). */
18
- export function createGetResultTool(deps: GetResultDeps) {
10
+ export function createGetResultTool(
11
+ getRecord: (id: string) => AgentRecord | undefined,
12
+ cancelNudge: (key: string) => void,
13
+ getConversation: (session: AgentSession) => string | undefined,
14
+ registry: AgentConfigLookup,
15
+ ) {
19
16
  return {
20
17
  name: "get_subagent_result" as const,
21
18
  label: "Get Agent Result",
@@ -45,7 +42,7 @@ export function createGetResultTool(deps: GetResultDeps) {
45
42
  _onUpdate: unknown,
46
43
  _ctx: unknown,
47
44
  ) => {
48
- const record = deps.getRecord(params.agent_id);
45
+ const record = getRecord(params.agent_id);
49
46
  if (!record) {
50
47
  return textResult(`Agent not found: "${params.agent_id}". It may have been cleaned up.`);
51
48
  }
@@ -58,11 +55,11 @@ export function createGetResultTool(deps: GetResultDeps) {
58
55
  // Pre-mark consumed BEFORE awaiting — onComplete fires inside .then() and
59
56
  // always runs before this await resumes. Prevents a redundant notification.
60
57
  record.notification?.markConsumed();
61
- deps.cancelNudge(params.agent_id);
58
+ cancelNudge(params.agent_id);
62
59
  await record.promise;
63
60
  }
64
61
 
65
- const displayName = getDisplayName(record.type, deps.registry);
62
+ const displayName = getDisplayName(record.type, registry);
66
63
  const duration = formatDuration(record.startedAt, record.completedAt);
67
64
  const tokens = formatLifetimeTokens(record);
68
65
  const contextPercent = getSessionContextPercent(record.session);
@@ -88,12 +85,12 @@ export function createGetResultTool(deps: GetResultDeps) {
88
85
  // Mark result as consumed — suppresses the completion notification
89
86
  if (record.status !== "running" && record.status !== "queued") {
90
87
  record.notification?.markConsumed();
91
- deps.cancelNudge(params.agent_id);
88
+ cancelNudge(params.agent_id);
92
89
  }
93
90
 
94
91
  // Verbose: include full conversation
95
92
  if (params.verbose && record.session) {
96
- const conversation = deps.getConversation(record.session);
93
+ const conversation = getConversation(record.session);
97
94
  if (conversation) {
98
95
  output += `\n\n--- Agent Conversation ---\n${conversation}`;
99
96
  }
@@ -4,17 +4,14 @@ import type { AgentRecord } from "../types.js";
4
4
  import { getSessionContextPercent } from "../usage.js";
5
5
  import { formatLifetimeTokens, textResult } from "./helpers.js";
6
6
 
7
- /** Narrow deps — only the methods this tool's execute callback calls. */
8
- export interface SteerToolDeps {
9
- getRecord: (id: string) => AgentRecord | undefined;
10
- emitEvent: (name: string, data: unknown) => void;
11
- steerAgent: (session: AgentSession, message: string) => Promise<void>;
12
- /** Buffer a steer for an agent whose session isn't ready yet. */
13
- queueSteer: (id: string, message: string) => boolean;
14
- }
15
-
16
7
  /** Create the steer_subagent tool definition (without Pi SDK wrapper). */
17
- export function createSteerTool(deps: SteerToolDeps) {
8
+ export function createSteerTool(
9
+ getRecord: (id: string) => AgentRecord | undefined,
10
+ emitEvent: (name: string, data: unknown) => void,
11
+ steerAgent: (session: AgentSession, message: string) => Promise<void>,
12
+ /** Buffer a steer for an agent whose session isn't ready yet. */
13
+ queueSteer: (id: string, message: string) => boolean,
14
+ ) {
18
15
  return {
19
16
  name: "steer_subagent" as const,
20
17
  label: "Steer Agent",
@@ -38,7 +35,7 @@ export function createSteerTool(deps: SteerToolDeps) {
38
35
  _onUpdate: unknown,
39
36
  _ctx: unknown,
40
37
  ) => {
41
- const record = deps.getRecord(params.agent_id);
38
+ const record = getRecord(params.agent_id);
42
39
  if (!record) {
43
40
  return textResult(
44
41
  `Agent not found: "${params.agent_id}". It may have been cleaned up.`,
@@ -52,16 +49,16 @@ export function createSteerTool(deps: SteerToolDeps) {
52
49
  const session = record.session;
53
50
  if (!session) {
54
51
  // Session not ready yet — queue via manager for delivery once initialized
55
- deps.queueSteer(record.id, params.message);
56
- deps.emitEvent("subagents:steered", { id: record.id, message: params.message });
52
+ queueSteer(record.id, params.message);
53
+ emitEvent("subagents:steered", { id: record.id, message: params.message });
57
54
  return textResult(
58
55
  `Steering message queued for agent ${record.id}. It will be delivered once the session initializes.`,
59
56
  );
60
57
  }
61
58
 
62
59
  try {
63
- await deps.steerAgent(session, params.message);
64
- deps.emitEvent("subagents:steered", { id: record.id, message: params.message });
60
+ await steerAgent(session, params.message);
61
+ emitEvent("subagents:steered", { id: record.id, message: params.message });
65
62
  const tokens = formatLifetimeTokens(record);
66
63
  const contextPercent = getSessionContextPercent(session);
67
64
  const stateParts: string[] = [];