@gotgenes/pi-subagents 6.11.0 → 6.12.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 CHANGED
@@ -5,6 +5,26 @@ 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.12.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.11.0...pi-subagents-v6.12.0) (2026-05-22)
9
+
10
+
11
+ ### Features
12
+
13
+ * narrow runtime widget field to WidgetLike interface ([#134](https://github.com/gotgenes/pi-packages/issues/134)) ([afa70ab](https://github.com/gotgenes/pi-packages/commit/afa70ab430109248a8f61ccd182b0f3acd1fa7e1))
14
+ * use SDK types in CreateSessionOptions ([#134](https://github.com/gotgenes/pi-packages/issues/134)) ([c2452af](https://github.com/gotgenes/pi-packages/commit/c2452af0ee3d47d778878443a634ca787f8d0bfb))
15
+
16
+
17
+ ### Bug Fixes
18
+
19
+ * replace message-shape as-any casts with type guards ([#134](https://github.com/gotgenes/pi-packages/issues/134)) ([d7ad65a](https://github.com/gotgenes/pi-packages/commit/d7ad65a61267790ae1ae8414b0c2aa9ebc8ad59c))
20
+
21
+
22
+ ### Documentation
23
+
24
+ * plan as-any cast reduction in test suite ([#134](https://github.com/gotgenes/pi-packages/issues/134)) ([f7cb1aa](https://github.com/gotgenes/pi-packages/commit/f7cb1aac0963021ae0545b73c88f950a7adb5fd2))
25
+ * **retro:** add retro notes for issue [#133](https://github.com/gotgenes/pi-packages/issues/133) ([be32640](https://github.com/gotgenes/pi-packages/commit/be32640048943059a98fc79797a35dfefd70fc34))
26
+ * update architecture doc for Step I completion ([#134](https://github.com/gotgenes/pi-packages/issues/134)) ([fd4aca7](https://github.com/gotgenes/pi-packages/commit/fd4aca79c74da2b8c4e3c58e2376e0612941d7d9))
27
+
8
28
  ## [6.11.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.10.0...pi-subagents-v6.11.0) (2026-05-22)
9
29
 
10
30
 
@@ -517,7 +517,7 @@ They are included here because the display extraction unblocks menu decompositio
517
517
  | ----------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
518
518
  | ~~7 `vi.mock()` calls~~ | ~~`agent-runner.test.ts`~~ | ~~Resolved by Step H (#133)~~ |
519
519
  | ~~7 `vi.mock()` calls~~ | ~~`agent-runner-extension-tools.test.ts`~~ | ~~Resolved by Step H (#133)~~ |
520
- | 52 `as any` casts | Across test suite | SDK session/context interfaces too wide to construct in tests |
520
+ | ~~52 `as any` casts~~ | ~~Across test suite~~ | ~~Reduced to 15 by Step I (#134)~~ |
521
521
  | 3× duplicated `mockSession()` | agent-manager, record-observer, ui-observer tests | No shared test fixture |
522
522
  | 3× duplicated `makeDeps()` | agent-tool, background-spawner, foreground-runner tests | No shared tool-deps fixture |
523
523
  | Weak assertions | lifecycle, renderer, session-config tests | `toHaveBeenCalled()` without args, `toContain()` on large strings |
@@ -549,16 +549,20 @@ Impact: reduces test boilerplate; single source of truth for mock shapes; change
549
549
 
550
550
  Impact: all 7 `vi.mock()` calls eliminated from both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`; tests verify behavior (turn limits, tool filtering, response collection) through injected stubs; SDK imports moved to the extension entry point.
551
551
 
552
- ### Step I: Reduce `as any` casts in tests (#134)
552
+ ### Step I: Reduce `as any` casts in tests (#134) ✓ done
553
553
 
554
- With Steps G and H, many `as any` casts disappear because tests construct narrow injectable interfaces instead of wide SDK types.
555
- Remaining casts are addressed by:
554
+ Reduced `as any` count from 93 to 15 (plus 13 explicit `as unknown as T` bridge casts).
556
555
 
557
- 1. Defining a `TestSession` type in `test/helpers/` that satisfies `SubscribableSession` + the fields tests actually read.
558
- 2. Replacing `const mockCtx = { cwd: "/tmp" } as any` with properly typed `AssemblerContext` or `ParentSnapshot` objects.
559
- 3. Using `satisfies` assertions where possible instead of `as any`.
556
+ Production changes:
560
557
 
561
- Target: reduce `as any` count from 52 to under 10.
558
+ - `ResourceLoaderOptions.appendSystemPromptOverride` typed to match `DefaultResourceLoaderOptions`; `createResourceLoader` factory cast removed from `index.ts`.
559
+ - `CreateSessionOptions.settingsManager` / `RunnerIO.createSettingsManager` typed as `SettingsManager`.
560
+ - `WidgetLike` interface in `runtime.ts` narrows the widget field.
561
+ - Local `ToolCallContent` / `BashExecutionMessage` type guards replace `as any` duck-typing in `conversation-viewer.ts` and `agent-runner.ts`.
562
+ - `textResult()` return no longer casts `details as any`.
563
+ - `toAgentSession()` helper and `STUB_CTX` constant centralise unavoidable bridge casts.
564
+
565
+ Remaining 15 `as any` casts are: 8 menu-handler `ctx as any` (deferred — requires `AgentManager.spawn` to accept `ParentSnapshot` directly), 2 `print-mode.test.ts` (same ExtensionContext/API pattern), 2 private-field test access, 1 `createSession` SDK bridge in `index.ts`, 1 `foreground-runner.ts` `AgentToolResult<any>` detail, 1 `stub-ctx.ts` comment.
562
566
 
563
567
  ### Step J: Extract display helpers (#135)
564
568
 
@@ -0,0 +1,366 @@
1
+ ---
2
+ issue: 134
3
+ issue_title: "Reduce `as any` casts in test suite"
4
+ ---
5
+
6
+ # Reduce as-any casts in test suite
7
+
8
+ ## Problem Statement
9
+
10
+ The test suite contains 93 `as any` casts (plus 2 `as any[]`).
11
+ These casts silence the type checker, hiding real errors — if a production interface adds a required field, tests silently pass with incomplete mocks instead of failing at compile time.
12
+ The heaviest offenders are `renderer.test.ts` (29), `runtime.test.ts` (10), `agent-menu.test.ts` (8), and `helpers.test.ts` (7).
13
+ The production source has 8 `as any` casts — 2 SDK bridge casts in `index.ts` and 6 message-shape casts in `conversation-viewer.ts` and `agent-runner.ts`.
14
+
15
+ ## Goals
16
+
17
+ - Target near-zero `as any` casts across both source and test code.
18
+ - Widen `CreateSessionOptions` and `ResourceLoaderOptions` to use SDK types, eliminating the SDK bridge casts in `index.ts`.
19
+ - Define local message-content types and type guards in `conversation-viewer.ts` and `agent-runner.ts`, eliminating polymorphic duck-typing casts.
20
+ - Define narrow production interfaces (`MenuCtx`, `WidgetLike`) where SDK types are too wide for test construction.
21
+ - Define typed test factories where partial mocks currently require casting.
22
+ - Preserve existing test coverage — no behavior changes.
23
+
24
+ ## Non-Goals
25
+
26
+ - Changing production behavior or public API shapes.
27
+ - Reaching literally zero — 1–3 casts may remain for private-field test access (`(manager as any).cleanupInterval`) where the alternative (exposing internals) is worse than the cast.
28
+
29
+ ## Background
30
+
31
+ ### Prerequisites
32
+
33
+ Issues #132 and #133 (IO injection) are both closed.
34
+ These eliminated the `vi.mock()`-heavy test patterns and introduced narrow injectable interfaces (`AssemblerIO`, `RunnerIO`), which already removed some `as any` casts from `agent-runner.test.ts` and `session-config.test.ts`.
35
+
36
+ ### Current as-any inventory by pattern
37
+
38
+ | Pattern | Count | Primary files |
39
+ | ------------------------------------------------ | ----- | ------------------------------------------------------------------------------------------------------------------- |
40
+ | Renderer — theme, message, result access | 29 | `renderer.test.ts` |
41
+ | Context — `ctx as any` for handler/menu | 14 | `agent-menu.test.ts`, `agent-manager.test.ts`, `print-mode.test.ts`, `parent-snapshot.test.ts`, `make-deps.test.ts` |
42
+ | Session — `{ session: {} as any }` | 10 | tool tests, `service-adapter.test.ts`, `print-mode.test.ts`, `agent-manager.test.ts` |
43
+ | Runtime/widget — `fakeWidget as any` | 9 | `runtime.test.ts` |
44
+ | Conversation viewer — message shapes | 8 | `conversation-viewer.test.ts`, `src/conversation-viewer.ts` |
45
+ | Helpers — registry, activity, details | 7 | `helpers.test.ts` |
46
+ | RunOptions — `} as any, io)` | 3 | `agent-runner.test.ts` |
47
+ | Tool execute — `{} as any` for ctx | 5 | `steer-tool.test.ts`, `get-result-tool.test.ts`, `make-deps.test.ts` |
48
+ | SDK bridge — `opts as any` in index.ts | 2 | `src/index.ts` |
49
+ | Other — `messages: [] as any[]`, internal access | 6 | `agent-runner*.test.ts`, `usage.test.ts`, `agent-manager.test.ts` |
50
+
51
+ ### Constraints from AGENTS.md
52
+
53
+ - Avoid `any` unless absolutely necessary.
54
+ - Prefer explicit configuration over hidden behavior.
55
+ - Keep scope tight; prefer small, reversible changes.
56
+
57
+ ## Design Overview
58
+
59
+ ### Production changes that make the test changes easy
60
+
61
+ Three targeted source-code changes remove the structural blockers that force casts elsewhere.
62
+
63
+ #### 1. SDK-typed option interfaces (eliminates 2 source casts)
64
+
65
+ `CreateSessionOptions` and `ResourceLoaderOptions` currently use `unknown` for opaque fields (`settingsManager`, `modelRegistry`, `model`).
66
+ Tests never construct these option objects — they mock `io.createSession` and `io.createResourceLoader` at the function level.
67
+ The `unknown` fields don't buy testability; the function-level mock does.
68
+
69
+ The SDK exports `CreateAgentSessionOptions` with all fields optional, using `ModelRegistry`, `Model<any>`, `ResourceLoader`, `SessionManager`, `SettingsManager`.
70
+ Widening `CreateSessionOptions` to use these SDK types makes the `as any` cast in `index.ts` unnecessary while preserving full test mockability.
71
+
72
+ ```typescript
73
+ // Before (agent-runner.ts)
74
+ export interface CreateSessionOptions {
75
+ settingsManager: unknown;
76
+ modelRegistry: unknown;
77
+ model?: unknown;
78
+ // ...
79
+ }
80
+
81
+ // After — use SDK types (type-only imports)
82
+ import type { Model } from "@earendil-works/pi-ai";
83
+ import type {
84
+ ModelRegistry,
85
+ ResourceLoader,
86
+ SessionManager,
87
+ SettingsManager,
88
+ } from "@earendil-works/pi-coding-agent";
89
+
90
+ export interface CreateSessionOptions {
91
+ settingsManager: SettingsManager;
92
+ modelRegistry: ModelRegistry;
93
+ model?: Model<any>;
94
+ resourceLoader: ResourceLoader;
95
+ sessionManager: SessionManager;
96
+ // ...
97
+ }
98
+ ```
99
+
100
+ The `RunnerIO.createSettingsManager` return type also changes from `unknown` to `SettingsManager`.
101
+
102
+ For `ResourceLoaderOptions`, the options are all primitives and callbacks — the SDK's `DefaultResourceLoader` constructor accepts them structurally.
103
+ The fix is to ensure the option interface matches the constructor's parameter type so `new DefaultResourceLoader(opts)` works without `as any`.
104
+
105
+ #### 2. Local message-content types + type guards (eliminates 6 source casts)
106
+
107
+ `conversation-viewer.ts` and `agent-runner.ts` both do:
108
+
109
+ ```typescript
110
+ (c as any).name ?? (c as any).toolName ?? "unknown"
111
+ (msg as any).role === "bashExecution"
112
+ const bash = msg as any;
113
+ ```
114
+
115
+ Fix: define local discriminated-union types for the content items and message roles the code actually handles, plus type guards:
116
+
117
+ ```typescript
118
+ /** Tool-call content item — SDK doesn't export a narrow type for this variant. */
119
+ interface ToolCallContent {
120
+ type: "toolCall";
121
+ name?: string;
122
+ toolName?: string;
123
+ }
124
+
125
+ function getToolCallName(c: { type: string }): string | undefined {
126
+ if (c.type !== "toolCall") return undefined;
127
+ const tc = c as ToolCallContent;
128
+ return tc.name ?? tc.toolName;
129
+ }
130
+
131
+ /** Bash execution message — not in the standard role union. */
132
+ interface BashExecutionMessage {
133
+ role: "bashExecution";
134
+ command: string;
135
+ output?: string;
136
+ }
137
+
138
+ function isBashExecution(msg: { role: string }): msg is BashExecutionMessage {
139
+ return msg.role === "bashExecution";
140
+ }
141
+ ```
142
+
143
+ These are small, file-local types that document the shapes the code already handles at runtime.
144
+ They make the duck-typing explicit and compile-time checked.
145
+
146
+ #### 3. Narrow production interfaces (`MenuCtx`, `WidgetLike`)
147
+
148
+ Already described in the test-side patterns below.
149
+ These are production-code changes that make the test-side casts disappear.
150
+
151
+ ### Test-side patterns
152
+
153
+ #### Pattern A: renderer.test.ts (29 casts → 0)
154
+
155
+ The renderer already defines narrow interfaces (`RendererMessage`, `RendererTheme`, `RenderOptions`).
156
+ The test casts because it doesn't use them — `stubTheme()` and `{ details: makeDetails() }` already satisfy the interfaces structurally.
157
+ The return type `Text` has a `.text` property — `(result as any).text` can become `result!.text` (non-null assertion after `expect(result).toBeDefined()`).
158
+
159
+ Fix: remove the casts; structural typing handles it.
160
+ For result access, define a minimal `{ text: string }` type or use non-null assertion.
161
+
162
+ #### Pattern B: context casts (14 casts → 0)
163
+
164
+ - `agent-menu.test.ts` (8): define `MenuCtx` in production; type `makeCtx()` return.
165
+ - `agent-manager.test.ts` (1): `mockCtx` is consumed by a mocked `buildParentSnapshot` — type as `unknown`.
166
+ - `parent-snapshot.test.ts` (1): define a test-local interface matching the fields `buildParentSnapshot` reads.
167
+ - `print-mode.test.ts` (2): type `makeHeadlessCtx()` return.
168
+ - `runtime.test.ts` (1): pass a structurally valid `UICtx` (fixed by `WidgetLike` step).
169
+ - `make-deps.test.ts` (3): type ctx args or use `unknown`.
170
+
171
+ #### Pattern C: session casts (10 casts → ~1)
172
+
173
+ Most are `{ session: {} as any, outputFile: ... }` for `record.execution`.
174
+ Fix: expand `createMockSession()` to include the fields these tests need (`dispose`, `steer`, `getSessionStats`), then use it.
175
+ One cast may remain for truly minimal session stubs.
176
+
177
+ #### Pattern D: runtime/widget casts (9 casts → 0)
178
+
179
+ Define `WidgetLike` in `runtime.ts`; change the `widget` field type.
180
+ Use real `AgentActivityTracker` instances (constructor takes only `maxTurns?`).
181
+
182
+ #### Pattern E: tool-execute ctx casts (5 casts → 0)
183
+
184
+ Define a `STUB_CTX` constant in `test/helpers/` satisfying the tool execute's context parameter.
185
+
186
+ #### Pattern F: RunOptions casts (3 casts → 0)
187
+
188
+ Check whether `defaultMaxTurns` and `graceTurns` are on `RunOptions` — they are.
189
+ The `as any` casts are vestigial; remove them.
190
+
191
+ #### Pattern G: helpers.test.ts (7 casts → 0)
192
+
193
+ Construct typed `TypeListRegistry` mocks.
194
+ Fix `textResult` return type to avoid `details as any`.
195
+ Use real `AgentActivityTracker` instances.
196
+
197
+ #### Pattern H: conversation-viewer test casts (4 test casts → ~1)
198
+
199
+ Type message mock objects using the local types from production change #2.
200
+ Keep `(viewer as any).buildContentLines()` (private method access — the alternative of exposing the method is worse).
201
+
202
+ #### Pattern I: other (6 casts → ~2)
203
+
204
+ - `messages: [] as any[]` → type as `unknown[]`.
205
+ - `(manager as any).cleanupInterval` → keep (private field assertion is intentional).
206
+ - `usage.test.ts` (2) → type mock objects.
207
+
208
+ ## Module-Level Changes
209
+
210
+ ### Modified source files
211
+
212
+ 1. `src/agent-runner.ts`
213
+ - Import SDK types (`Model`, `ModelRegistry`, `SettingsManager`, `SessionManager`, `ResourceLoader`) as type-only imports.
214
+ - Widen `CreateSessionOptions` fields from `unknown` to SDK types.
215
+ - Change `RunnerIO.createSettingsManager` return type from `unknown` to `SettingsManager`.
216
+ - Define `getToolCallName()` helper and local `ToolCallContent` interface.
217
+ - Replace `(c as any).name ?? (c as any).toolName` with `getToolCallName(c)` in `getAgentConversation`.
218
+
219
+ 2. `src/ui/conversation-viewer.ts`
220
+ - Define local `ToolCallContent`, `BashExecutionMessage` interfaces and type guards.
221
+ - Replace `(c as any).name ?? (c as any).toolName` with typed helper.
222
+ - Replace `(msg as any).role === "bashExecution"` / `const bash = msg as any` with type guard.
223
+
224
+ 3. `src/ui/agent-menu.ts`
225
+ - Define and export `MenuCtx` interface.
226
+ - Change handler parameter type from `ExtensionContext` to `MenuCtx`.
227
+
228
+ 4. `src/runtime.ts`
229
+ - Define and export `WidgetLike` interface.
230
+ - Change `widget` field type from `AgentWidget | null` to `WidgetLike | null`.
231
+
232
+ 5. `src/tools/helpers.ts`
233
+ - Change `textResult` to avoid `details as any` — type the return properly.
234
+
235
+ 6. `src/index.ts`
236
+ - Remove `opts as any` casts in `createResourceLoader` and `createSession` factories (enabled by SDK-typed option interfaces).
237
+
238
+ ### Modified test files
239
+
240
+ 1. `test/renderer.test.ts` — remove all 29 casts.
241
+ 2. `test/runtime.test.ts` — use `WidgetLike` stubs and real `AgentActivityTracker`.
242
+ 3. `test/ui/agent-menu.test.ts` — type `makeCtx()` as `MenuCtx`.
243
+ 4. `test/tools/helpers.test.ts` — typed registry mocks, real `AgentActivityTracker`.
244
+ 5. `test/conversation-viewer.test.ts` — typed message mocks.
245
+ 6. `test/agent-runner.test.ts` — remove `as any` on RunOptions; type messages.
246
+ 7. `test/agent-runner-extension-tools.test.ts` — type messages.
247
+ 8. `test/agent-manager.test.ts` — type `mockCtx`.
248
+ 9. `test/print-mode.test.ts` — type `makeHeadlessCtx()`.
249
+ 10. `test/parent-snapshot.test.ts` — type mock context.
250
+ 11. `test/service-adapter.test.ts` — use `createMockSession()`.
251
+ 12. `test/tools/steer-tool.test.ts` — stub ctx, use `createMockSession()`.
252
+ 13. `test/tools/get-result-tool.test.ts` — stub ctx, use `createMockSession()`.
253
+ 14. `test/tools/foreground-runner.test.ts` — use `createMockSession()`.
254
+ 15. `test/tools/background-spawner.test.ts` — use `createMockSession()`.
255
+ 16. `test/tools/agent-tool.test.ts` — use `createMockSession()`.
256
+ 17. `test/helpers/make-deps.test.ts` — type ctx args.
257
+ 18. `test/usage.test.ts` — type mock objects.
258
+
259
+ ## Test Impact Analysis
260
+
261
+ 1. No new test coverage — this is a type-safety improvement on existing tests.
262
+ 2. No tests become redundant.
263
+ 3. All existing tests stay as-is in terms of assertions; only type annotations and mock construction change.
264
+ 4. `pnpm run check` is the primary validation — every step must pass type checking since the goal is eliminating type holes.
265
+ 5. Expanding `createMockSession()` affects multiple consumers — diff existing defaults before changing (per testing skill).
266
+
267
+ ## TDD Order
268
+
269
+ Steps are ordered by independence and impact (production changes first, then largest cast-count test reductions).
270
+
271
+ 1. **Widen option interfaces to SDK types; remove index.ts casts (2 source casts → 0).**
272
+ Import SDK types as type-only in `agent-runner.ts`.
273
+ Widen `CreateSessionOptions` fields (`settingsManager`, `modelRegistry`, `model`, `resourceLoader`, `sessionManager`) to SDK types.
274
+ Change `RunnerIO.createSettingsManager` return type to `SettingsManager`.
275
+ Remove `as any` casts from `index.ts` factories.
276
+ Run `pnpm run check` + full suite.
277
+ Commit: `feat: use SDK types in CreateSessionOptions (#134)`
278
+
279
+ 2. **Add message-content type guards; remove viewer and runner source casts (6 source casts → 0).**
280
+ Define `ToolCallContent` interface and `getToolCallName()` helper in `conversation-viewer.ts`.
281
+ Define `BashExecutionMessage` interface and `isBashExecution()` guard in `conversation-viewer.ts`.
282
+ Replace all source `as any` casts in `conversation-viewer.ts`.
283
+ Define the same `getToolCallName()` helper (or extract a shared one) in `agent-runner.ts` and replace its cast.
284
+ Run `pnpm run check` + affected tests.
285
+ Commit: `fix: replace message-shape as-any casts with type guards (#134)`
286
+
287
+ 3. **Remove renderer test casts (29 → 0).**
288
+ Remove all `as any` casts on `stubTheme()`, message objects, and result access.
289
+ Use `result!.text` (non-null assertion after `toBeDefined()` guard) for result access.
290
+ Run `pnpm run check` + `pnpm vitest run test/renderer.test.ts`.
291
+ Commit: `test: remove as-any casts in renderer tests (#134)`
292
+
293
+ 4. **Add `MenuCtx` interface; remove menu-test casts (8 → 0).**
294
+ Define `MenuCtx` in `agent-menu.ts`.
295
+ Change handler parameter from `ExtensionContext` to `MenuCtx`.
296
+ Type `makeCtx()` return in test.
297
+ Run `pnpm run check` + `pnpm vitest run test/ui/agent-menu.test.ts`.
298
+ Commit: `feat: narrow menu handler to MenuCtx interface (#134)`
299
+
300
+ 5. **Add `WidgetLike`; remove runtime casts (9 → 0).**
301
+ Define `WidgetLike` in `runtime.ts`.
302
+ Change `widget` field type.
303
+ Update `runtime.test.ts`: typed stubs, real `AgentActivityTracker` instances.
304
+ Run `pnpm run check` + `pnpm vitest run test/runtime.test.ts`.
305
+ Commit: `feat: narrow runtime widget field to WidgetLike interface (#134)`
306
+
307
+ 6. **Expand `createMockSession`; remove session casts (10 → ~1).**
308
+ Add fields to `createMockSession()` that tool/service tests need.
309
+ Use it in tool tests, service-adapter, agent-manager for `record.execution.session`.
310
+ Run `pnpm run check` + full suite.
311
+ Commit: `test: use createMockSession for session execution casts (#134)`
312
+
313
+ 7. **Remove helpers.test.ts casts (7 → 0).**
314
+ Typed `TypeListRegistry` mocks.
315
+ Fix `textResult` return type in `src/tools/helpers.ts`.
316
+ Real `AgentActivityTracker` instances.
317
+ Run `pnpm run check` + `pnpm vitest run test/tools/helpers.test.ts`.
318
+ Commit: `test: remove as-any casts in helpers tests (#134)`
319
+
320
+ 8. **Remove context casts across remaining test files (6 → 0).**
321
+ Type `mockCtx` in `agent-manager.test.ts`.
322
+ Type `makeHeadlessCtx()` in `print-mode.test.ts`.
323
+ Type mock context in `parent-snapshot.test.ts`.
324
+ Type `{} as any` in `make-deps.test.ts`.
325
+ Run `pnpm run check` + full suite.
326
+ Commit: `test: remove remaining context as-any casts (#134)`
327
+
328
+ 9. **Remove RunOptions and message-array casts (5 → 0).**
329
+ Remove vestigial `as any` on RunOptions objects.
330
+ Type `messages` arrays as `unknown[]`.
331
+ Run `pnpm run check` + affected tests.
332
+ Commit: `test: remove RunOptions and message-array casts (#134)`
333
+
334
+ 10. **Remove tool-execute ctx casts (5 → 0).**
335
+ Define `STUB_CTX` in `test/helpers/`.
336
+ Use it in `steer-tool.test.ts`, `get-result-tool.test.ts`, `make-deps.test.ts`.
337
+ Run `pnpm run check` + affected tests.
338
+ Commit: `test: remove tool-execute context casts (#134)`
339
+
340
+ 11. **Clean up conversation-viewer test and usage casts (6 → ~2).**
341
+ Type message mocks in `conversation-viewer.test.ts`.
342
+ Type usage mock objects in `usage.test.ts`.
343
+ Keep `(viewer as any).buildContentLines()` and `(manager as any).cleanupInterval` (private access, intentional).
344
+ Run `pnpm run check` + full suite.
345
+ Commit: `test: remove conversation-viewer and usage as-any casts (#134)`
346
+
347
+ ## Risks and Mitigations
348
+
349
+ | Risk | Mitigation |
350
+ | ----------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
351
+ | Widening `CreateSessionOptions` to SDK types re-couples `agent-runner.ts` to SDK | These are type-only imports — no runtime coupling. Tests still mock at the `RunnerIO` function level. The `RunnerIO` interface is the decoupling boundary, not the option types. |
352
+ | Narrowing `widget` to `WidgetLike` could break callers accessing `AgentWidget`-specific methods | Grep all `runtime.widget` access sites first; verify they only use `update()`, `markFinished()`, `setUICtx()`. |
353
+ | Narrowing menu handler to `MenuCtx` could break callers passing `ExtensionContext` | `ExtensionContext` is structurally a superset of `MenuCtx` — callers pass it unchanged. |
354
+ | Expanding `createMockSession()` could break consumers with different default expectations | Diff existing consumers' default expectations before adding fields (per testing skill). |
355
+ | Message type guards add code to conversation-viewer and agent-runner | Each guard is 2–4 lines; they replace unsafe `as any` access with documented, compile-checked types. Net code stays similar. |
356
+ | 11-step plan is large | Each step is independently committable. No step depends on a subsequent one. Steps can be reordered or skipped. |
357
+
358
+ ## Open Questions
359
+
360
+ - Should `getToolCallName()` be shared between `conversation-viewer.ts` and `agent-runner.ts`, or duplicated?
361
+ Both files handle the same SDK message shape.
362
+ A shared helper in a common module (e.g., `context.ts` which already has `extractText`) avoids duplication.
363
+ Alternatively, the duplication is cheap (3 lines) and the two files have different concerns.
364
+ - Should `STUB_CTX` live in `test/helpers/stub-ctx.ts` or inline?
365
+ Centralize — 3+ tool tests share the pattern.
366
+ - Estimated final count: 2–3 remaining casts (`(viewer as any).buildContentLines()`, `(manager as any).cleanupInterval`, possibly 1 more for an unexported SDK type).
@@ -0,0 +1,45 @@
1
+ ---
2
+ issue: 133
3
+ issue_title: "Inject SDK boundary into `agent-runner`"
4
+ ---
5
+
6
+ # Retro: #133 — Inject SDK boundary into agent-runner
7
+
8
+ ## Final Retrospective (2026-05-22T13:15:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Injected all SDK and IO dependencies into `runAgent()` via a `RunnerIO` interface and `createAgentRunner(io)` factory.
13
+ Eliminated all 14 `vi.mock()` calls across `agent-runner.test.ts` (7) and `agent-runner-extension-tools.test.ts` (7).
14
+ Released as `pi-subagents-v6.11.0`.
15
+
16
+ ### Observations
17
+
18
+ #### What went well
19
+
20
+ - The `createAgentRunner(io)` factory pattern was a clean design choice that kept the `AgentRunner` interface and `AgentManager` completely unchanged — zero downstream impact.
21
+ - Folding plan steps 1 and 2 into a single commit was the right call given `tsconfig.json` includes `test/` — recognized the constraint before attempting a broken intermediate commit.
22
+
23
+ #### What caused friction (agent side)
24
+
25
+ 1. `wrong-abstraction` — Annotated `createRunnerIO(): RunnerIO` in the test helper, which erased the `Mock<...>` type information from `vi.fn()` stubs.
26
+ TypeScript then rejected `.mockResolvedValue()` on `io.createSession` across 18 call sites.
27
+ Required removing the annotation plus a follow-up edit to remove the now-unused `type RunnerIO` import flagged by Biome.
28
+ Impact: two extra edit rounds and a type-check cycle before the fix landed.
29
+
30
+ 2. `missing-context` — Added `SettingsManager` to the SDK import block in `index.ts` without checking that the name was already imported from `./settings.js`.
31
+ Biome caught the redeclaration and the `noRedeclare` lint error required an alias fix (`SettingsManager as SdkSettingsManager`).
32
+ Impact: one extra edit round triggered by the autoformat failure.
33
+
34
+ 3. `premature-convergence` — Spent excessive reasoning time deliberating commit-boundary strategy (whether to combine steps 1+2, how to handle broken intermediate states, whether step 4 would have remaining work).
35
+ The answer was straightforward once the `tsconfig` `include` was checked, but the check came late in the deliberation.
36
+ Impact: added friction but no rework — the final decision was correct.
37
+
38
+ #### What caused friction (user side)
39
+
40
+ - None identified.
41
+ The plan and issue were well-specified, and the user's only intervention was "Please, continue" after a message boundary, which was appropriate.
42
+
43
+ ### Changes made
44
+
45
+ 1. `.pi/skills/testing/SKILL.md` — added rule: do not annotate test factory return types with production interface types (erases `Mock<...>` methods).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.11.0",
3
+ "version": "6.12.0",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -6,6 +6,7 @@ import type { Model } from "@earendil-works/pi-ai";
6
6
  import {
7
7
  type AgentSession,
8
8
  type AgentSessionEvent,
9
+ type SettingsManager,
9
10
  } from "@earendil-works/pi-coding-agent";
10
11
  import type { AgentConfigLookup } from "./agent-types.js";
11
12
  import { extractText } from "./context.js";
@@ -17,6 +18,23 @@ import type { ShellExec, SubagentType, ThinkingLevel } from "./types.js";
17
18
  /** Names of tools registered by this extension that subagents must NOT inherit. */
18
19
  const EXCLUDED_TOOL_NAMES = ["Agent", "get_subagent_result", "steer_subagent"];
19
20
 
21
+ // ── Local message-shape types ───────────────────────────────────────────────
22
+ // The Pi SDK does not export a narrow type for tool-call content variants.
23
+
24
+ /** Tool-call content item — SDK exposes this variant at runtime but doesn’t export the narrow type. */
25
+ interface ToolCallContent {
26
+ type: "toolCall";
27
+ name?: string;
28
+ toolName?: string;
29
+ }
30
+
31
+ /** Extracts the display name from a tool-call content item. */
32
+ function getToolCallName(c: { type: string }): string {
33
+ if (c.type !== "toolCall") return "unknown";
34
+ const tc = c as ToolCallContent;
35
+ return tc.name ?? tc.toolName ?? "unknown";
36
+ }
37
+
20
38
  /**
21
39
  * Filter the session's active tool names according to extension/denylist rules.
22
40
  *
@@ -82,7 +100,8 @@ export interface ResourceLoaderOptions {
82
100
  noThemes?: boolean;
83
101
  noContextFiles?: boolean;
84
102
  systemPromptOverride?: () => string;
85
- appendSystemPromptOverride?: () => unknown[];
103
+ /** Override the append system prompt. Receives the current base value; return the replacement. */
104
+ appendSystemPromptOverride?: (base: string[]) => string[];
86
105
  }
87
106
 
88
107
  /** Options passed to RunnerIO.createSession. */
@@ -90,7 +109,7 @@ export interface CreateSessionOptions {
90
109
  cwd: string;
91
110
  agentDir: string;
92
111
  sessionManager: SessionManagerLike;
93
- settingsManager: unknown;
112
+ settingsManager: SettingsManager;
94
113
  modelRegistry: unknown;
95
114
  model?: unknown;
96
115
  tools: string[];
@@ -110,7 +129,7 @@ export interface RunnerIO {
110
129
  createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
111
130
  deriveSessionDir: (parentSessionFile: string | undefined, effectiveCwd: string) => string;
112
131
  createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
113
- createSettingsManager: (cwd: string, agentDir: string) => unknown;
132
+ createSettingsManager: (cwd: string, agentDir: string) => SettingsManager;
114
133
  createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
115
134
  assemblerIO: AssemblerIO;
116
135
  }
@@ -446,9 +465,7 @@ export function getAgentConversation(session: AgentSession): string {
446
465
  for (const c of msg.content) {
447
466
  if (c.type === "text" && c.text) textParts.push(c.text);
448
467
  else if (c.type === "toolCall")
449
- toolCalls.push(
450
- ` Tool: ${(c as any).name ?? (c as any).toolName ?? "unknown"}`,
451
- );
468
+ toolCalls.push(` Tool: ${getToolCallName(c)}`);
452
469
  }
453
470
  if (textParts.length > 0)
454
471
  parts.push(`[Assistant]: ${textParts.join("\n")}`);
package/src/index.ts CHANGED
@@ -136,7 +136,7 @@ export default function (pi: ExtensionAPI) {
136
136
  const runnerIO: RunnerIO = {
137
137
  detectEnv,
138
138
  getAgentDir,
139
- createResourceLoader: (opts) => new DefaultResourceLoader(opts as any),
139
+ createResourceLoader: (opts) => new DefaultResourceLoader(opts),
140
140
  deriveSessionDir: deriveSubagentSessionDir,
141
141
  createSessionManager: (cwd, dir) => SessionManager.create(cwd, dir),
142
142
  createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
package/src/runtime.ts CHANGED
@@ -7,7 +7,19 @@
7
7
  */
8
8
 
9
9
  import type { AgentActivityTracker } from "./ui/agent-activity-tracker.js";
10
- import type { AgentWidget, UICtx } from "./ui/agent-widget.js";
10
+ import type { UICtx } from "./ui/agent-widget.js";
11
+
12
+ /**
13
+ * Narrow widget interface consumed by SubagentRuntime delegation methods.
14
+ * AgentWidget satisfies this structurally; tests use plain stubs.
15
+ */
16
+ export interface WidgetLike {
17
+ setUICtx(ctx: UICtx): void;
18
+ onTurnStart(): void;
19
+ markFinished(id: string): void;
20
+ update(): void;
21
+ ensureTimer(): void;
22
+ }
11
23
 
12
24
  /**
13
25
  * Narrow config subset read by AgentManager when constructing RunOptions.
@@ -37,7 +49,7 @@ export class SubagentRuntime {
37
49
  * Persistent widget reference. Null until constructed after AgentManager.
38
50
  * Delegation methods use optional chaining so callers never need `widget!`.
39
51
  */
40
- widget: AgentWidget | null = null;
52
+ widget: WidgetLike | null = null;
41
53
 
42
54
  // ── Session-context methods ──────────────────────────────────────────────
43
55
 
@@ -21,7 +21,7 @@ import {
21
21
  } from "../ui/agent-widget.js";
22
22
  import { spawnBackground } from "./background-spawner.js";
23
23
  import { runForeground } from "./foreground-runner.js";
24
- import { buildDetails, buildTypeListText, formatLifetimeTokens, getStatusNote, textResult } from "./helpers.js";
24
+ import { buildDetails, buildTypeListText, textResult } from "./helpers.js";
25
25
 
26
26
  // ---- Deps interface ----
27
27
 
@@ -48,7 +48,7 @@ export function buildDetails(
48
48
 
49
49
  /** Tool execute return value for a text response. */
50
50
  export function textResult(msg: string, details?: unknown) {
51
- return { content: [{ type: "text" as const, text: msg }], details: details as any };
51
+ return { content: [{ type: "text" as const, text: msg }], details };
52
52
  }
53
53
 
54
54
  /** Format an agent's lifetime token total, or "" when zero. */
@@ -14,6 +14,37 @@ import { getLifetimeTotal, getSessionContextPercent } from "../usage.js";
14
14
  import type { AgentActivityTracker } from "./agent-activity-tracker.js";
15
15
  import { buildInvocationTags, describeActivity, formatDuration, formatSessionTokens, getDisplayName, getPromptModeLabel, type Theme } from "./agent-widget.js";
16
16
 
17
+ // ── Local message-shape types ───────────────────────────────────────────────
18
+ // The Pi SDK does not export narrow types for all message content variants.
19
+ // These file-local types document the runtime shapes this module handles.
20
+
21
+ /** Tool-call content item — SDK exposes this variant at runtime but doesn't export the narrow type. */
22
+ interface ToolCallContent {
23
+ type: "toolCall";
24
+ name?: string;
25
+ toolName?: string;
26
+ }
27
+
28
+ /** Extracts the tool name from a content item, falling back to 'unknown'. */
29
+ function getToolCallName(c: { type: string }): string {
30
+ if (c.type !== "toolCall") return "unknown";
31
+ const tc = c as ToolCallContent;
32
+ return tc.name ?? tc.toolName ?? "unknown";
33
+ }
34
+
35
+ /** Bash execution message — 'bashExecution' role is not in the SDK's AgentSession message role union. */
36
+ interface BashExecutionMessage {
37
+ role: "bashExecution";
38
+ command: string;
39
+ output?: string;
40
+ }
41
+
42
+ function isBashExecution(msg: { role: string }): msg is BashExecutionMessage {
43
+ return msg.role === "bashExecution";
44
+ }
45
+
46
+ // ─────────────────────────────────────────────────────────────────────────────
47
+
17
48
  /** Base lines consumed by chrome: top border + header + header sep + footer sep + footer + bottom border. */
18
49
  const CHROME_LINES_BASE = 6;
19
50
  const MIN_VIEWPORT = 3;
@@ -228,7 +259,7 @@ export class ConversationViewer implements Component {
228
259
  for (const c of msg.content) {
229
260
  if (c.type === "text" && c.text) textParts.push(c.text);
230
261
  else if (c.type === "toolCall") {
231
- toolCalls.push((c as any).name ?? (c as any).toolName ?? "unknown");
262
+ toolCalls.push(getToolCallName(c));
232
263
  }
233
264
  }
234
265
  if (needsSeparator) lines.push(th.fg("dim", "───"));
@@ -250,14 +281,13 @@ export class ConversationViewer implements Component {
250
281
  for (const line of wrapTextWithAnsi(truncated.trim(), width)) {
251
282
  lines.push(th.fg("dim", line));
252
283
  }
253
- } else if ((msg as any).role === "bashExecution") {
254
- const bash = msg as any;
284
+ } else if (isBashExecution(msg)) {
255
285
  if (needsSeparator) lines.push(th.fg("dim", "───"));
256
- lines.push(truncateToWidth(th.fg("muted", ` $ ${bash.command}`), width));
257
- if (bash.output?.trim()) {
258
- const out = bash.output.length > 500
259
- ? bash.output.slice(0, 500) + "... (truncated)"
260
- : bash.output;
286
+ lines.push(truncateToWidth(th.fg("muted", ` $ ${msg.command}`), width));
287
+ if (msg.output?.trim()) {
288
+ const out = msg.output.length > 500
289
+ ? msg.output.slice(0, 500) + "... (truncated)"
290
+ : msg.output;
261
291
  for (const line of wrapTextWithAnsi(out.trim(), width)) {
262
292
  lines.push(th.fg("dim", line));
263
293
  }