@gotgenes/pi-subagents 7.1.0 → 7.2.1

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,33 @@ 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
+ ## [7.2.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.2.0...pi-subagents-v7.2.1) (2026-05-25)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * mark Layer 2 done and update health metrics in architecture doc ([ad00426](https://github.com/gotgenes/pi-packages/commit/ad00426f0f29ceff0915033419fdc2f1b53755b0))
14
+ * plan align tool interfaces for structural typing ([#194](https://github.com/gotgenes/pi-packages/issues/194)) ([36e56b0](https://github.com/gotgenes/pi-packages/commit/36e56b08351893e3c2d63569e7cfa140c172e20b))
15
+ * **retro:** add planning stage notes for issue [#194](https://github.com/gotgenes/pi-packages/issues/194) ([63a5763](https://github.com/gotgenes/pi-packages/commit/63a5763ed2bf4c2a6e2e9a19fdcdce71a2a9905a))
16
+ * **retro:** add retro notes for issue [#193](https://github.com/gotgenes/pi-packages/issues/193) ([8987f90](https://github.com/gotgenes/pi-packages/commit/8987f907e00bb70429782c947a2afbdb1db5faa9))
17
+ * **retro:** add TDD stage notes for issue [#194](https://github.com/gotgenes/pi-packages/issues/194) ([f692323](https://github.com/gotgenes/pi-packages/commit/f69232395882c07d6d95273e08021b94800f0e43))
18
+
19
+ ## [7.2.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.1.0...pi-subagents-v7.2.0) (2026-05-25)
20
+
21
+
22
+ ### Features
23
+
24
+ * SubagentRuntime stores typed SessionContext and owns context queries ([#193](https://github.com/gotgenes/pi-packages/issues/193)) ([4ca5319](https://github.com/gotgenes/pi-packages/commit/4ca531934e36c37c7cbc8fef8314a483e7dec479))
25
+
26
+
27
+ ### Documentation
28
+
29
+ * mark Phase 11 Layer 1 complete, update metrics ([#193](https://github.com/gotgenes/pi-packages/issues/193)) ([32233ed](https://github.com/gotgenes/pi-packages/commit/32233ed89f27dca854ce858978c3acc029b1e801))
30
+ * plan SubagentRuntime owns context queries ([#193](https://github.com/gotgenes/pi-packages/issues/193)) ([6ea475a](https://github.com/gotgenes/pi-packages/commit/6ea475af94f8456c1d665adcd007dd2833ab7a4b))
31
+ * **retro:** add planning stage notes for issue [#193](https://github.com/gotgenes/pi-packages/issues/193) ([7da6d5a](https://github.com/gotgenes/pi-packages/commit/7da6d5abac82bdea0cbdbc8677dda04d38a7887d))
32
+ * **retro:** add retro notes for issue [#192](https://github.com/gotgenes/pi-packages/issues/192) ([1223de4](https://github.com/gotgenes/pi-packages/commit/1223de4a68d4514eb504c5c95d64fb35500d286b))
33
+ * **retro:** add TDD stage notes for issue [#193](https://github.com/gotgenes/pi-packages/issues/193) ([3950b81](https://github.com/gotgenes/pi-packages/commit/3950b81228a3db57eb4c24236fa7d75c638a335a))
34
+
8
35
  ## [7.1.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.0.0...pi-subagents-v7.1.0) (2026-05-24)
9
36
 
10
37
 
@@ -608,26 +608,30 @@ The approach is layered: each step makes the next step trivial.
608
608
 
609
609
  ### Findings
610
610
 
611
- | Metric | Value |
612
- | ------------------------- | --------------------------------------- |
613
- | Health score | 75/100 (B) |
614
- | #1 hotspot | `index.ts` (128 commits, accelerating) |
615
- | Dead exports | 1 (`getToolCallName` re-export) |
616
- | Production duplication | 0 |
617
- | Test duplication | 1,396 lines (69 clone groups, 22 files) |
618
- | `as any` casts in index | 5 |
619
- | Adapter closures in index | 44 |
620
- | Index fan-out | 25 imports |
611
+ | Metric | Value |
612
+ | ------------------------- | ------------------------------------------ |
613
+ | Health score | 75/100 (B) |
614
+ | #1 hotspot | `index.ts` (128 commits, accelerating) |
615
+ | Dead exports | 0 (down from 1; Layer 2 removed re-export) |
616
+ | Production duplication | 0 |
617
+ | Test duplication | 1,396 lines (69 clone groups, 22 files) |
618
+ | `as any` casts in index | 1 (down from 5; Layer 1 resolved 4) |
619
+ | Adapter closures in index | 40 (down from 44; Layers 1–2 resolved 4) |
620
+ | Index fan-out | 25 imports |
621
621
 
622
622
  ### Root cause
623
623
 
624
624
  The 44 adapter closures in `index.ts` exist because the tool factories accept narrow interfaces that don't structurally match the real objects.
625
625
  The real objects can't satisfy the interfaces because:
626
626
 
627
- 1. `SubagentRuntime.currentCtx` is typed `{ pi: unknown; ctx: unknown }` — so every consumer must `as any` cast to read fields.
628
- 2. Context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) live as closures in index.ts instead of methods on the state holder.
629
- 3. `AgentToolManager` mixes fields from `AgentManager` and `SettingsManager` (source mismatch).
630
- 4. `AgentToolWidget` uses different method names than `SubagentRuntime` (name mismatch).
627
+ 1. ~~`SubagentRuntime.currentCtx` is typed `{ pi: unknown; ctx: unknown }` — so every consumer must `as any` cast to read fields.~~
628
+ Resolved by Layer 0 (#192) + Layer 1 (#193).
629
+ 2. ~~Context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) live as closures in index.ts instead of methods on the state holder.~~
630
+ Resolved by Layer 1 (#193).
631
+ 3. ~~`AgentToolManager` mixes fields from `AgentManager` and `SettingsManager` (source mismatch).~~
632
+ Resolved by Layer 2 (#194).
633
+ 4. ~~`AgentToolWidget` uses different method names than `SubagentRuntime` (name mismatch).~~
634
+ Resolved by Layer 2 (#194).
631
635
 
632
636
  Fix these structural misalignments and the class conversions become mechanical.
633
637
 
@@ -655,18 +659,18 @@ export interface SessionContext {
655
659
  - Smell: Category C (platform type threading)
656
660
  - Outcome: typed foundation for Layers 1–4; no `as any` needed by consumers of `SubagentRuntime`
657
661
 
658
- ### Layer 1: `SubagentRuntime` stores typed context, owns its queries ([#193][193])
662
+ ### Layer 1: `SubagentRuntime` stores typed context, owns its queries ([#193][193]) ✓ done
659
663
 
660
664
  Change `currentCtx` from `{ pi: unknown; ctx: unknown }` to `SessionContext | undefined`.
661
665
  The single `as SessionContext` cast moves into `handleSessionStart` — the boundary where the SDK hands us the value.
662
666
  Add typed methods: `buildSnapshot(inheritContext)`, `getModelInfo()`, `getSessionInfo()`.
663
667
 
664
- - Target: `src/runtime.ts`, `src/handlers/lifecycle.ts`
668
+ - Target: `src/runtime.ts`, `src/handlers/lifecycle.ts`, `src/service/service-adapter.ts`, `src/index.ts`
665
669
  - Smell: Category C (closure queries on mutable field → methods on state owner)
666
670
  - Outcome: 3 closure queries in index.ts → 0; `SubagentRuntime` is self-sufficient for tool deps
667
671
  - Enables: Layer 3 (tools accept `SubagentRuntime` directly)
668
672
 
669
- ### Layer 2: Align interfaces so real objects satisfy tool deps structurally ([#194][194])
673
+ ### Layer 2: Align interfaces so real objects satisfy tool deps structurally ([#194][194]) ✓ done
670
674
 
671
675
  Three alignment changes:
672
676
 
@@ -679,7 +683,7 @@ After this step, `AgentManager` structurally satisfies `AgentToolManager` and `S
679
683
 
680
684
  - Target: `src/tools/agent-tool.ts` (interface), `src/runtime.ts` (method names), `src/ui/message-formatters.ts`
681
685
  - Smell: Category C (source mismatch, name mismatch) + Category A (dead export)
682
- - Outcome: structural typing connects real objects to tool interfaces without adapters
686
+ - Outcome: structural typing connects real objects to tool interfaces without adapters; 0 dead exports (fallow clean)
683
687
  - Enables: Layer 3 (class constructors accept real objects directly)
684
688
 
685
689
  ### Layer 3: Convert closure factories to classes ([#195][195], [#196][196])
@@ -0,0 +1,245 @@
1
+ ---
2
+ issue: 193
3
+ issue_title: "SubagentRuntime owns context queries"
4
+ ---
5
+
6
+ # SubagentRuntime owns context queries
7
+
8
+ ## Problem Statement
9
+
10
+ Three closure queries in `index.ts` reach into `runtime.currentCtx?.ctx` with `as any` casts to extract model, modelRegistry, and sessionManager values.
11
+ These closures exist because `SubagentRuntime` stores `unknown` and doesn't provide typed accessors.
12
+ The queries belong on the state holder — `SubagentRuntime` owns `currentCtx`, so it should own the queries on that state.
13
+
14
+ ## Goals
15
+
16
+ - Type `SubagentRuntime.currentCtx` as `SessionContext | undefined` (eliminating the `{ pi: unknown; ctx: unknown }` shape).
17
+ - Move the `as SessionContext` cast into `handleSessionStart` — the single SDK boundary.
18
+ - Add typed methods on `SubagentRuntime`: `buildSnapshot()`, `getModelInfo()`, `getSessionInfo()`.
19
+ - Remove 4 `as any` casts from `index.ts`.
20
+ - Remove 3 closure queries from the composition root.
21
+ - Update `service-adapter.ts` to delegate to `SubagentRuntime` instead of holding its own `as ExtensionContext` cast.
22
+
23
+ ## Non-Goals
24
+
25
+ - Converting closure factories to classes (Layer 3, #195/#196).
26
+ - Aligning interface names so real objects satisfy tool deps (Layer 2, #194).
27
+ - Changing `buildParentContext` in `session/context.ts` (it will continue to accept `ExtensionContext`; `SessionContext` is structurally compatible).
28
+
29
+ ## Background
30
+
31
+ Issue #192 (closed, shipped as v7.1.0) added the `SessionContext` interface to `src/types.ts`.
32
+ This plan builds on that foundation.
33
+
34
+ The architecture doc (Phase 11, Layer 1) specifies this exact change: "Change `currentCtx` from `{ pi: unknown; ctx: unknown }` to `SessionContext | undefined`."
35
+
36
+ Key modules:
37
+
38
+ - `src/runtime.ts` — `SubagentRuntime` class with `currentCtx`, `setSessionContext()`, `clearSessionContext()`
39
+ - `src/handlers/lifecycle.ts` — `SessionLifecycleHandler.handleSessionStart()` receives raw SDK `ctx`
40
+ - `src/index.ts` — composition root with inline `buildSnapshot`, `getModelInfo`, `getSessionInfo` closures
41
+ - `src/service/service-adapter.ts` — `createSubagentsService()` with `getCtx()` and `getModelRegistry()` closures
42
+ - `src/lifecycle/parent-snapshot.ts` — `buildParentSnapshot(ctx: ExtensionContext, inheritContext?)` function
43
+ - `src/session/context.ts` — `buildParentContext(ctx: ExtensionContext)` function
44
+
45
+ AGENTS.md constraint: keep modules focused and composable.
46
+ The queries belong on the state owner per Law of Demeter (code-design skill).
47
+
48
+ ## Design Overview
49
+
50
+ ### Type change
51
+
52
+ `SubagentRuntime.currentCtx` becomes `SessionContext | undefined` (previously `{ pi: unknown; ctx: unknown } | undefined`).
53
+ The `pi` field is dropped from the stored context — it is only used in `SessionLifecycleHandler` which already stores it as a constructor param.
54
+
55
+ ### Cast boundary
56
+
57
+ `handleSessionStart` receives `ctx: unknown` from the SDK event.
58
+ The single `as SessionContext` cast lives here:
59
+
60
+ ```typescript
61
+ handleSessionStart(_event: unknown, ctx: unknown): void {
62
+ this.runtime.setSessionContext(ctx as SessionContext);
63
+ this.manager.clearCompleted();
64
+ }
65
+ ```
66
+
67
+ ### New methods on `SubagentRuntime`
68
+
69
+ ```typescript
70
+ import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
71
+ import type { ModelInfo } from "#src/tools/spawn-config";
72
+ import type { SessionContext } from "#src/types";
73
+
74
+ class SubagentRuntime {
75
+ currentCtx: SessionContext | undefined = undefined;
76
+
77
+ setSessionContext(ctx: SessionContext): void {
78
+ this.currentCtx = ctx;
79
+ }
80
+
81
+ clearSessionContext(): void {
82
+ this.currentCtx = undefined;
83
+ }
84
+
85
+ buildSnapshot(inheritContext: boolean): ParentSnapshot {
86
+ return buildParentSnapshot(this.currentCtx!, inheritContext);
87
+ }
88
+
89
+ getModelInfo(): ModelInfo {
90
+ return {
91
+ parentModel: this.currentCtx?.model as ModelInfo["parentModel"],
92
+ modelRegistry: this.currentCtx?.modelRegistry,
93
+ };
94
+ }
95
+
96
+ getSessionInfo(): { parentSessionFile: string; parentSessionId: string } {
97
+ return {
98
+ parentSessionFile: this.currentCtx?.sessionManager?.getSessionFile() ?? "",
99
+ parentSessionId: this.currentCtx?.sessionManager?.getSessionId() ?? "",
100
+ };
101
+ }
102
+ }
103
+ ```
104
+
105
+ ### `buildParentSnapshot` signature change
106
+
107
+ Change the parameter type from `ExtensionContext` to `SessionContext`:
108
+
109
+ ```typescript
110
+ export function buildParentSnapshot(
111
+ ctx: SessionContext,
112
+ inheritContext?: boolean,
113
+ ): ParentSnapshot { ... }
114
+ ```
115
+
116
+ `ExtensionContext` structurally satisfies `SessionContext` (all 5 fields match), so existing callers (the `/agents` command handler) continue to work without change.
117
+
118
+ Similarly, `buildParentContext` changes from `ExtensionContext` to `SessionContext`.
119
+ The `sessionManager.getBranch()` returns `unknown[]` in `SessionContext`, which is what `buildParentContext` already treats the entries as (it accesses `.type`, `.message`, `.summary` via runtime checks without type narrowing).
120
+
121
+ ### `service-adapter.ts` change
122
+
123
+ The adapter currently receives `getCtx: () => { pi: unknown; ctx: unknown } | undefined`.
124
+ After this change, it receives the runtime directly and calls `runtime.buildSnapshot()`:
125
+
126
+ ```typescript
127
+ export function createSubagentsService(
128
+ manager: AgentManagerLike,
129
+ resolveModel: (input: string, registry: ModelRegistry) => unknown,
130
+ runtime: ServiceRuntimeLike,
131
+ ): SubagentsService { ... }
132
+ ```
133
+
134
+ Where `ServiceRuntimeLike` is a narrow interface:
135
+
136
+ ```typescript
137
+ export interface ServiceRuntimeLike {
138
+ readonly currentCtx: SessionContext | undefined;
139
+ buildSnapshot(inheritContext: boolean): ParentSnapshot;
140
+ getModelInfo(): { modelRegistry: unknown };
141
+ }
142
+ ```
143
+
144
+ ### Impact on `LifecycleRuntime` interface
145
+
146
+ The narrow interface in `handlers/lifecycle.ts` changes from:
147
+
148
+ ```typescript
149
+ interface LifecycleRuntime {
150
+ setSessionContext(pi: unknown, ctx: unknown): void;
151
+ clearSessionContext(): void;
152
+ }
153
+ ```
154
+
155
+ to:
156
+
157
+ ```typescript
158
+ interface LifecycleRuntime {
159
+ setSessionContext(ctx: SessionContext): void;
160
+ clearSessionContext(): void;
161
+ }
162
+ ```
163
+
164
+ ## Module-Level Changes
165
+
166
+ 1. `src/runtime.ts` — Change `currentCtx` type, update `setSessionContext()` signature (drop `pi` param), add `buildSnapshot()`, `getModelInfo()`, `getSessionInfo()` methods.
167
+ Add imports for `SessionContext`, `ParentSnapshot`, `ModelInfo`, `buildParentSnapshot`.
168
+ 2. `src/handlers/lifecycle.ts` — Update `LifecycleRuntime` interface (single `ctx: SessionContext` param).
169
+ Cast `ctx as SessionContext` in `handleSessionStart`.
170
+ Remove `pi` from `setSessionContext` call.
171
+ Import `SessionContext`.
172
+ 3. `src/lifecycle/parent-snapshot.ts` — Change `buildParentSnapshot` param from `ExtensionContext` to `SessionContext`.
173
+ Update import.
174
+ 4. `src/session/context.ts` — Change `buildParentContext` param from `ExtensionContext` to `SessionContext`.
175
+ Update import.
176
+ 5. `src/service/service-adapter.ts` — Replace `getCtx`/`getModelRegistry` closures with a `ServiceRuntimeLike` interface.
177
+ Use `runtime.buildSnapshot()` and `runtime.currentCtx?.modelRegistry`.
178
+ Remove `ExtensionContext` import.
179
+ 6. `src/index.ts` — Remove inline `buildSnapshot`, `getModelInfo`, `getSessionInfo` closures from `createAgentTool` deps.
180
+ Pass `runtime.buildSnapshot.bind(runtime)`, `runtime.getModelInfo.bind(runtime)`, `runtime.getSessionInfo.bind(runtime)`.
181
+ Update `createSubagentsService` call to pass `runtime` instead of two closures.
182
+ Update `lifecycle.handleSessionStart` call (drop `pi` from `setSessionContext`).
183
+ Remove `as any` eslint-disable for the eliminated casts.
184
+ 7. `test/runtime.test.ts` — Update session-context method tests (single param).
185
+ Add tests for `buildSnapshot()`, `getModelInfo()`, `getSessionInfo()`.
186
+ 8. `test/handlers/lifecycle.test.ts` — Update `setSessionContext` mock expectations (single param).
187
+ 9. `test/service/service-adapter.test.ts` — Update `createSubagentsService` calls to pass a runtime-like mock instead of two closures.
188
+ 10. `test/helpers/stub-ctx.ts` (or equivalent) — Verify the stub ctx satisfies `SessionContext`.
189
+
190
+ ## Test Impact Analysis
191
+
192
+ 1. **New unit tests enabled:** `SubagentRuntime.buildSnapshot()`, `.getModelInfo()`, `.getSessionInfo()` — previously untestable as anonymous closures.
193
+ 2. **Existing tests that simplify:** `service-adapter.test.ts` no longer needs to wire `getCtx`/`getModelRegistry` closures; a simple runtime stub replaces them.
194
+ 3. **Tests that stay as-is:** `agent-tool.test.ts` (via `make-deps.ts`) — the tool deps interface still has `buildSnapshot`, `getModelInfo`, `getSessionInfo` fields; only the wiring in `index.ts` changes.
195
+ The tool tests use mocks and are unaffected.
196
+
197
+ ## TDD Order
198
+
199
+ 1. **Red → Green:** Add `buildSnapshot()`, `getModelInfo()`, `getSessionInfo()` method tests to `runtime.test.ts`.
200
+ Update `setSessionContext` tests to use single param.
201
+ Tests fail because the methods don't exist yet.
202
+ Commit: `test: add SubagentRuntime context-query method tests (#193)`
203
+
204
+ 2. **Green:** Change `SubagentRuntime.currentCtx` type to `SessionContext | undefined`.
205
+ Update `setSessionContext` to single param.
206
+ Add three query methods.
207
+ Update imports.
208
+ Run `pnpm run check` to verify type coherence.
209
+ Commit: `feat: SubagentRuntime stores typed SessionContext and owns context queries (#193)`
210
+
211
+ 3. **Green:** Change `buildParentSnapshot` and `buildParentContext` to accept `SessionContext` instead of `ExtensionContext`.
212
+ Update imports.
213
+ Run `pnpm run check`.
214
+ Commit: `refactor: narrow buildParentSnapshot param to SessionContext (#193)`
215
+
216
+ 4. **Green:** Update `handlers/lifecycle.ts` — change `LifecycleRuntime` interface, cast `ctx as SessionContext` in handler.
217
+ Update lifecycle test expectations.
218
+ Commit: `refactor: move SessionContext cast to handleSessionStart boundary (#193)`
219
+
220
+ 5. **Green:** Update `service-adapter.ts` — introduce `ServiceRuntimeLike`, replace closure params with runtime.
221
+ Update service-adapter tests.
222
+ Commit: `refactor: service-adapter delegates to SubagentRuntime for context (#193)`
223
+
224
+ 6. **Green:** Update `index.ts` — wire `runtime.buildSnapshot.bind(runtime)` etc. into agent tool deps.
225
+ Update `createSubagentsService` call.
226
+ Remove `as any` casts and corresponding eslint-disable comment.
227
+ Clean up unused imports.
228
+ Commit: `refactor: index.ts delegates context queries to SubagentRuntime (#193)`
229
+
230
+ 7. **Verify:** Run full test suite (`pnpm vitest run`) and type check (`pnpm run check`).
231
+ Fix any remaining lint issues.
232
+ Commit: `chore: cleanup lint after SubagentRuntime context migration (#193)` (if needed)
233
+
234
+ ## Risks and Mitigations
235
+
236
+ | Risk | Mitigation |
237
+ | ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
238
+ | `buildParentSnapshot` callers outside this package break when param changes from `ExtensionContext` to `SessionContext` | `ExtensionContext` structurally satisfies `SessionContext` — no source-level changes needed at call sites. The `/agents` command handler passes `ctx` which is still `ExtensionContext` from the SDK. |
239
+ | `runtime.currentCtx` is `undefined` when `buildSnapshot()` is called | Same risk exists today — the closure reads `runtime.currentCtx?.ctx` which may be undefined. The `!` assertion documents the invariant: methods are only called during an active session. |
240
+ | Dropping `pi` from `currentCtx` breaks something that reads it | Grep confirms `pi` is only stored and never read back from `currentCtx`. `SessionLifecycleHandler` already stores `pi` as its own constructor param. |
241
+ | Test fixture `make-deps.ts` still mocks `buildSnapshot`/`getModelInfo`/`getSessionInfo` on `AgentToolDeps` | Correct — the tool interface doesn't change in this issue. The closures in `index.ts` are replaced with bound methods, but the deps shape stays the same. |
242
+
243
+ ## Open Questions
244
+
245
+ - None — the issue's proposed change and the architecture doc's Layer 1 spec are fully aligned.
@@ -0,0 +1,139 @@
1
+ ---
2
+ issue: 194
3
+ issue_title: "Align tool interfaces for structural typing"
4
+ ---
5
+
6
+ # Align tool interfaces for structural typing
7
+
8
+ ## Problem Statement
9
+
10
+ The narrow interfaces that tool factories accept don't structurally match the real objects (`AgentManager`, `SubagentRuntime`, `SettingsManager`).
11
+ This forces `index.ts` to build adapter closures bridging the gap — each a one-liner that exists only because names or ownership don't align.
12
+ Three specific mismatches prevent structural typing from connecting real objects to tool interfaces directly.
13
+
14
+ ## Goals
15
+
16
+ - Remove `getMaxConcurrent()` from `AgentToolManager` — it belongs on the settings accessor.
17
+ - Rename `SubagentRuntime.updateWidget()` → `update()` so `SubagentRuntime` structurally satisfies `AgentToolWidget`.
18
+ - Remove the dead `getToolCallName` re-export from `ui/message-formatters.ts`.
19
+ - After these changes, `AgentManager` structurally satisfies `AgentToolManager` and `SubagentRuntime` structurally satisfies `AgentToolWidget` — no adapter closures needed in `index.ts`.
20
+
21
+ ## Non-Goals
22
+
23
+ - Converting tool factories to classes (that's #195).
24
+ - Simplifying `index.ts` wiring (that's #195/#196, after this layer).
25
+ - Changing `NotificationManager`'s constructor parameter name (`updateWidget` callback) — it's a positional callback, not a structural interface member.
26
+
27
+ ## Background
28
+
29
+ This is Phase 11, Layer 2 in `docs/architecture/architecture.md`.
30
+ Layer 0 (#192, done) and Layer 1 (#193, done) established the typed `SessionContext` and moved context queries onto `SubagentRuntime`.
31
+ Layer 2 (this issue) aligns the remaining structural mismatches.
32
+ Layer 3 (#195) depends on this layer.
33
+
34
+ Relevant modules:
35
+
36
+ - `src/tools/agent-tool.ts` — defines `AgentToolManager` and `AgentToolWidget` interfaces.
37
+ - `src/tools/background-spawner.ts` — defines `BackgroundManagerDeps` with `getMaxConcurrent()`.
38
+ - `src/runtime.ts` — defines `SubagentRuntime` class with `updateWidget()` delegation method.
39
+ - `src/ui/message-formatters.ts` — has the dead `getToolCallName` re-export.
40
+ - `src/index.ts` — composition root that builds adapter closures.
41
+ - `src/settings.ts` — `SettingsManager` owns `maxConcurrent`.
42
+
43
+ ## Design Overview
44
+
45
+ ### 1. Move `getMaxConcurrent` off manager interfaces → settings
46
+
47
+ The `BackgroundManagerDeps` and `AgentToolManager` interfaces both declare `getMaxConcurrent(): number`.
48
+ In reality, the value comes from `SettingsManager.maxConcurrent`.
49
+ The fix:
50
+
51
+ - Remove `getMaxConcurrent` from `AgentToolManager`.
52
+ - Remove `getMaxConcurrent` from `BackgroundManagerDeps`.
53
+ - Widen `AgentToolDeps.settings` from `{ readonly defaultMaxTurns: number | undefined }` to also include `readonly maxConcurrent: number`.
54
+ - Pass `settings` (or a narrow settings interface) to `spawnBackground` so it can read `maxConcurrent` directly.
55
+ - `SettingsManager` already exposes a `get maxConcurrent(): number` property, so it structurally satisfies the widened interface.
56
+
57
+ After this, `AgentManager` (which has `spawn`, `spawnAndWait`, `resume`, `getRecord` but NOT `getMaxConcurrent`) structurally satisfies `AgentToolManager`.
58
+
59
+ ### 2. Rename `SubagentRuntime.updateWidget()` → `update()`
60
+
61
+ The `AgentToolWidget` interface declares `update(): void`.
62
+ `SubagentRuntime` has `updateWidget(): void` which delegates to `this.widget?.update()`.
63
+ Renaming the delegation method to `update()` makes `SubagentRuntime` structurally satisfy `AgentToolWidget` (it already has `setUICtx`, `ensureTimer`, and `markFinished`).
64
+
65
+ Callers of `runtime.updateWidget()`:
66
+
67
+ - `src/index.ts` line 70: `() => runtime.updateWidget()` → `() => runtime.update()`
68
+ - `src/index.ts` line 199: `update: () => runtime.updateWidget()` → can now pass `runtime` directly (but that's a #195 concern — for now just rename the call).
69
+
70
+ The `WidgetLike` interface in `runtime.ts` already uses `update()` — no conflict.
71
+
72
+ ### 3. Remove dead re-export
73
+
74
+ `src/ui/message-formatters.ts` line 24 exports `getToolCallName` from `#src/session/content-items`.
75
+ No consumer imports `getToolCallName` from `message-formatters` — all uses go directly to `content-items.ts`.
76
+ Delete the re-export line.
77
+
78
+ ### After all three changes
79
+
80
+ ```typescript
81
+ // AgentToolManager (after removing getMaxConcurrent):
82
+ interface AgentToolManager {
83
+ spawn(...): string;
84
+ spawnAndWait(...): Promise<AgentRecord>;
85
+ resume(...): Promise<AgentRecord | undefined>;
86
+ getRecord(id: string): AgentRecord | undefined;
87
+ }
88
+ // AgentManager has all four methods → structural match ✓
89
+
90
+ // AgentToolWidget (unchanged):
91
+ interface AgentToolWidget {
92
+ setUICtx(ctx: unknown): void;
93
+ ensureTimer(): void;
94
+ update(): void;
95
+ markFinished(id: string): void;
96
+ }
97
+ // SubagentRuntime has all four methods (after rename) → structural match ✓
98
+ ```
99
+
100
+ ## Module-Level Changes
101
+
102
+ | File | Change |
103
+ | --------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
104
+ | `src/tools/agent-tool.ts` | Remove `getMaxConcurrent` from `AgentToolManager`. Widen `settings` type in `AgentToolDeps` to include `readonly maxConcurrent: number`. |
105
+ | `src/tools/background-spawner.ts` | Remove `getMaxConcurrent` from `BackgroundManagerDeps`. Add a `settings: { readonly maxConcurrent: number }` parameter (or add to `BackgroundParams`). Read `settings.maxConcurrent` instead of `manager.getMaxConcurrent()`. |
106
+ | `src/runtime.ts` | Rename `updateWidget()` → `update()`. |
107
+ | `src/ui/message-formatters.ts` | Remove the `export { getToolCallName } from ...` line. |
108
+ | `src/index.ts` | Update `runtime.updateWidget()` → `runtime.update()` at both call sites. Remove `getMaxConcurrent` from the `manager` adapter object passed to `createAgentTool`. Pass `settings` through to `spawnBackground` via the tool deps. |
109
+ | `test/tools/background-spawner.test.ts` | Remove `getMaxConcurrent` from mock manager objects. Add `settings` mock with `maxConcurrent`. |
110
+ | `test/runtime.test.ts` | Rename `updateWidget` → `update` in test descriptions and call sites. |
111
+ | `docs/architecture/architecture.md` | Update Layer 2 status and health metrics (adapter closures count, dead exports count). |
112
+
113
+ ## Test Impact Analysis
114
+
115
+ 1. No new unit tests are strictly needed — this is interface alignment, not new behavior.
116
+ 2. `test/tools/background-spawner.test.ts` needs mock shape updates (remove `getMaxConcurrent` from manager mock, add settings mock).
117
+ 3. `test/runtime.test.ts` needs the method name updated from `updateWidget` to `update`.
118
+ 4. Existing tests for `agent-tool`, `notification`, and `message-formatters` remain as-is (no behavior change).
119
+
120
+ ## TDD Order
121
+
122
+ 1. `refactor:` Rename `SubagentRuntime.updateWidget()` → `update()` — update `runtime.ts`, `test/runtime.test.ts`, and both call sites in `index.ts`.
123
+ Run `pnpm run check` to verify no type errors remain.
124
+ 2. `refactor:` Move `getMaxConcurrent` off manager interfaces — remove from `AgentToolManager` and `BackgroundManagerDeps`, widen `AgentToolDeps.settings`, add settings parameter to `spawnBackground`, update `index.ts` call site and `test/tools/background-spawner.test.ts`.
125
+ Run `pnpm run check`.
126
+ 3. `refactor:` Remove dead `getToolCallName` re-export from `ui/message-formatters.ts`.
127
+ 4. `docs:` Update architecture doc — mark Layer 2 as done, update health metrics (dead exports: 0, adapter closures reduced).
128
+
129
+ ## Risks and Mitigations
130
+
131
+ | Risk | Mitigation |
132
+ | ---------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
133
+ | `update()` is a generic name on `SubagentRuntime` — could confuse readers about what's being updated | The method is a documented widget delegation like its siblings (`markFinished`, `ensureTimer`); the JSDoc comment clarifies it delegates to `widget.update()`. |
134
+ | `background-spawner` signature change could break other callers | Grep confirms only `agent-tool.ts` calls `spawnBackground` — no other consumers. |
135
+ | Renaming method in runtime could miss a call site | Grep and `pnpm run check` after each step catch all references. |
136
+
137
+ ## Open Questions
138
+
139
+ None — the issue's proposed direction is unambiguous and the architecture doc confirms the design.
@@ -33,3 +33,27 @@ Baseline: 53 test files, 848 tests; final: unchanged.
33
33
 
34
34
  - Pre-existing lint failure in `docs/architecture/architecture.md` (5 unused MD053 link references for issues #164, #165, #170, #171, #172) was fixed as part of the baseline verification and included in the feat commit.
35
35
  - The interface landed exactly as planned — no deviations from the plan's Design Overview.
36
+
37
+ ## Stage: Final Retrospective (2026-05-24T20:00:00Z)
38
+
39
+ ### Session summary
40
+
41
+ All three stages (plan, TDD, ship) completed in a single session.
42
+ Released as `pi-subagents-v7.1.0`.
43
+ No rework, no deviations from plan.
44
+
45
+ ### Observations
46
+
47
+ #### What went well
48
+
49
+ - The issue was fully specified — no ambiguity, no `ask_user` needed at any stage.
50
+ - Trivial scope (one interface, no consumers) made the plan-to-ship pipeline fast and mechanical.
51
+ - Pre-existing lint failures in `architecture.md` were caught during baseline verification and fixed without disrupting the flow.
52
+
53
+ #### What caused friction (agent side)
54
+
55
+ - None — clean execution with no rework or corrections.
56
+
57
+ #### What caused friction (user side)
58
+
59
+ - None — no intervention needed beyond invoking the three workflow commands.
@@ -0,0 +1,75 @@
1
+ ---
2
+ issue: 193
3
+ issue_title: "SubagentRuntime owns context queries"
4
+ ---
5
+
6
+ # Retro: #193 — SubagentRuntime owns context queries
7
+
8
+ ## Stage: Planning (2026-05-24T21:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Planned the Layer 1 change that types `SubagentRuntime.currentCtx` as `SessionContext`, adds three query methods (`buildSnapshot`, `getModelInfo`, `getSessionInfo`), and eliminates 4 `as any` casts from `index.ts`.
13
+ The plan covers 7 TDD steps touching `runtime.ts`, `handlers/lifecycle.ts`, `parent-snapshot.ts`, `context.ts`, `service-adapter.ts`, and `index.ts`.
14
+
15
+ ### Observations
16
+
17
+ - The `pi` field in `currentCtx` is never read back — only stored.
18
+ Dropping it is safe; `SessionLifecycleHandler` already holds `pi` as a constructor param.
19
+ - `ExtensionContext` structurally satisfies `SessionContext`, so changing `buildParentSnapshot`'s param type is source-compatible with the `/agents` command handler that passes raw SDK `ctx`.
20
+ - `service-adapter.ts` gets the biggest structural change: its two closure params (`getCtx`, `getModelRegistry`) collapse into a single `ServiceRuntimeLike` interface.
21
+ - No design ambiguity — the architecture doc's Layer 1 spec and the issue body are fully aligned.
22
+ - Test fixtures in `make-deps.ts` are unaffected because the `AgentToolDeps` interface shape doesn't change — only the wiring in `index.ts` that supplies the implementations changes.
23
+
24
+ ## Stage: Implementation — TDD (2026-05-24T20:30:00Z)
25
+
26
+ ### Session summary
27
+
28
+ Completed all 6 implementation TDD steps plus an architecture doc update in one session.
29
+ The `getSessionInfo` implementation needed `?.sessionManager.getSessionFile()` (not `?.sessionManager?.getSessionFile()`) since `sessionManager` is a required field of `SessionContext` — ESLint's `no-unnecessary-condition` caught this at the pre-commit hook.
30
+ Final test count: 854 (up from 848 baseline, +6 new tests for `buildSnapshot`, `getModelInfo`, `getSessionInfo`).
31
+
32
+ ### Observations
33
+
34
+ - The plan's Non-Goals section incorrectly said `buildParentContext` would NOT change.
35
+ In practice it had to accept `SessionContext` instead of `ExtensionContext` — they are not substitutable in that direction.
36
+ The Module-Level Changes list was correct; only the Non-Goals prose was wrong.
37
+ - `context.ts` needed a local `BranchEntry` union type to handle `getBranch(): unknown[]`.
38
+ TypeScript's discriminated union narrowing doesn't work when the union includes a catch-all `{ type: string }` arm — explicit casts within each `if` branch were required.
39
+ - `service-adapter.ts` ended up using `runtime.currentCtx.modelRegistry` directly (no `getModelInfo()` call needed in the service adapter) — `ServiceRuntimeLike` only needs `currentCtx` and `buildSnapshot`.
40
+ This is cleaner than the plan's `getModelInfo(): { modelRegistry: unknown }` approach.
41
+ - Biome's `noUnusedPrivateClassMembers` warning caught the leftover `private readonly pi: unknown` in `SessionLifecycleHandler`.
42
+ Removed `pi` from the constructor entirely (rather than adding `_` prefix), which also cleaned up `index.ts`.
43
+ - The `eslint-disable` directive at the top of `index.ts` had two now-unused entries (`no-unsafe-member-access`, `no-unsafe-call`) removed by `eslint --fix`.
44
+
45
+ ## Stage: Final Retrospective (2026-05-24T20:45:00Z)
46
+
47
+ ### Session summary
48
+
49
+ All three stages (plan, TDD, ship) completed in a single session.
50
+ Released as `pi-subagents-v7.2.0`.
51
+ One plan contradiction required a judgment call during implementation; otherwise clean mechanical execution.
52
+
53
+ ### Observations
54
+
55
+ #### What went well
56
+
57
+ - The architecture doc's Phase 11 Layer 1 spec was precise enough that no `ask_user` was needed at any stage — the issue body, architecture doc, and #192 retro were fully aligned.
58
+ - `ServiceRuntimeLike` ended up simpler than planned (only `currentCtx` + `buildSnapshot` instead of also requiring `getModelInfo()`) — the implementation found a cleaner design than the plan specified.
59
+ - Test count increase (+6) validates the design: methods that were previously untestable as anonymous closures now have dedicated unit tests.
60
+
61
+ #### What caused friction (agent side)
62
+
63
+ - `missing-context` — The plan's Non-Goals section claimed `buildParentContext` would not change, contradicting Module-Level Changes item #4 which explicitly listed the file.
64
+ The planning stage didn't cross-check these sections before committing.
65
+ Impact: brief confusion during step 3 about which section to trust (resolved by following Module-Level Changes); no rework, added ~30s of deliberation.
66
+ - `missing-context` — TypeScript's discriminated union narrowing limitation with a `{ type: string }` catch-all arm was not anticipated.
67
+ Impact: required adding a local `BranchEntry` union type and explicit casts in `context.ts`; no rework but ~2 min of debugging the type error.
68
+
69
+ #### What caused friction (user side)
70
+
71
+ - None — no user intervention was needed at any point across all three stages.
72
+
73
+ ### Changes made
74
+
75
+ 1. `.pi/prompts/plan-issue.md` — added a Non-Goals vs Module-Level Changes cross-check instruction under the Module-Level Changes bullet.
@@ -0,0 +1,36 @@
1
+ ---
2
+ issue: 194
3
+ issue_title: "Align tool interfaces for structural typing"
4
+ ---
5
+
6
+ # Retro: #194 — Align tool interfaces for structural typing
7
+
8
+ ## Stage: Planning (2026-05-24T12:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced an implementation plan for three targeted alignment changes: moving `getMaxConcurrent` off manager interfaces to the settings accessor, renaming `SubagentRuntime.updateWidget()` → `update()`, and removing the dead `getToolCallName` re-export.
13
+ The plan includes a 4-step TDD order with type-check gates after each refactoring step.
14
+
15
+ ### Observations
16
+
17
+ - Issue #193 (Layer 1) is already closed/implemented, confirming this layer can proceed immediately.
18
+
19
+ ## Stage: Implementation — TDD (2026-05-24T21:00:00Z)
20
+
21
+ ### Session summary
22
+
23
+ Completed all 4 TDD steps: renamed `SubagentRuntime.updateWidget()` → `update()`, moved `getMaxConcurrent` from manager interfaces to the `settings` narrow type, removed the dead `getToolCallName` re-export, and updated the architecture doc.
24
+ Test count stayed flat at 854 (53 files) — all green.
25
+ The `pnpm run check` type-gate caught a previously-unnoticed `test/helpers/make-deps.test.ts` that also validated `getMaxConcurrent` and the old `settings` shape; this file was updated as part of step 2.
26
+
27
+ ### Observations
28
+
29
+ - An unexpected file `test/helpers/make-deps.test.ts` had three type errors after removing `getMaxConcurrent` (one test asserting `manager.getMaxConcurrent()`, one structural compatibility check referencing it, and one settings override that only passed `defaultMaxTurns`).
30
+ All three were fixed in the same commit as step 2 — no deviation from the plan.
31
+ - Adding `settings` to `BackgroundParams` (instead of as a 5th function parameter) was the right call: it keeps `spawnBackground` at 4 arguments and groups all spawn-context values together.
32
+ - The health metric update: dead exports 1 → 0, adapter closures 41 → 40 (only `getMaxConcurrent` was removed in this layer; the remaining 8 widget/manager adapter closures need #195 class conversion to collapse).
33
+ - The `background-spawner.ts` module is the only consumer of `getMaxConcurrent` — grep confirms no other call sites beyond `agent-tool.ts`'s interface definition.
34
+ - The `NotificationManager` constructor takes `updateWidget` as a positional callback parameter name — this does NOT need renaming (it's not a structural interface member).
35
+ - The rename from `updateWidget` → `update` is safe because the `WidgetLike` interface in `runtime.ts` already uses `update()` — no naming conflict within the class.
36
+ - All three changes are independent of each other and could be committed in any order, but the plan sequences them for clean `pnpm run check` passes at each step.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.1.0",
3
+ "version": "7.2.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
@@ -1,3 +1,5 @@
1
+ import type { SessionContext } from "#src/types";
2
+
1
3
  /**
2
4
  * Session lifecycle event handlers: session_start, session_before_switch, session_shutdown.
3
5
  *
@@ -14,7 +16,7 @@ export interface LifecycleManager {
14
16
 
15
17
  /** Narrow runtime interface — only the methods lifecycle handlers call. */
16
18
  export interface LifecycleRuntime {
17
- setSessionContext(pi: unknown, ctx: unknown): void;
19
+ setSessionContext(ctx: SessionContext): void;
18
20
  clearSessionContext(): void;
19
21
  }
20
22
 
@@ -22,7 +24,6 @@ export interface LifecycleRuntime {
22
24
  * Handles session lifecycle events.
23
25
  *
24
26
  * Constructor deps:
25
- * - `pi` — the ExtensionAPI instance, stored in runtime on session_start
26
27
  * - `runtime` — owns session context state
27
28
  * - `manager` — manages agent lifecycle (clear, abort, dispose)
28
29
  * - `disposeNotifications` — tears down the notification system on shutdown
@@ -30,7 +31,6 @@ export interface LifecycleRuntime {
30
31
  */
31
32
  export class SessionLifecycleHandler {
32
33
  constructor(
33
- private readonly pi: unknown,
34
34
  private readonly runtime: LifecycleRuntime,
35
35
  private readonly manager: LifecycleManager,
36
36
  private readonly disposeNotifications: () => void,
@@ -38,7 +38,7 @@ export class SessionLifecycleHandler {
38
38
  ) {}
39
39
 
40
40
  handleSessionStart(_event: unknown, ctx: unknown): void {
41
- this.runtime.setSessionContext(this.pi, ctx);
41
+ this.runtime.setSessionContext(ctx as SessionContext);
42
42
  this.manager.clearCompleted();
43
43
  }
44
44
 
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- /* eslint-disable @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
1
+ /* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
2
2
  /**
3
3
  * pi-agents — A pi extension providing Claude Code-style autonomous sub-agents.
4
4
  *
@@ -35,7 +35,7 @@ import { publishSubagentsService, unpublishSubagentsService } from "#src/service
35
35
  import { createSubagentsService } from "#src/service/service-adapter";
36
36
  import { detectEnv } from "#src/session/env";
37
37
 
38
- import { type ModelRegistry, resolveModel } from "#src/session/model-resolver";
38
+ import { resolveModel } from "#src/session/model-resolver";
39
39
  import { buildAgentPrompt } from "#src/session/prompts";
40
40
  import { deriveSubagentSessionDir } from "#src/session/session-dir";
41
41
  import { preloadSkills } from "#src/session/skill-loader";
@@ -67,7 +67,7 @@ export default function (pi: ExtensionAPI) {
67
67
  (msg, opts) => pi.sendMessage(msg, opts),
68
68
  runtime.agentActivity,
69
69
  (id) => runtime.markFinished(id),
70
- () => runtime.updateWidget(),
70
+ () => runtime.update(),
71
71
  );
72
72
 
73
73
  // Settings: owns all three in-memory values and handles load/save/emit.
@@ -162,16 +162,10 @@ export default function (pi: ExtensionAPI) {
162
162
 
163
163
  // Typed service published via Symbol.for() for cross-extension access.
164
164
  // Consumers: const { getSubagentsService } = await import("@gotgenes/pi-subagents");
165
- const service = createSubagentsService(
166
- manager,
167
- resolveModel,
168
- () => runtime.currentCtx,
169
- () => (runtime.currentCtx?.ctx as { modelRegistry?: ModelRegistry } | undefined)?.modelRegistry,
170
- );
165
+ const service = createSubagentsService(manager, resolveModel, runtime);
171
166
  publishSubagentsService(service);
172
167
 
173
168
  const lifecycle = new SessionLifecycleHandler(
174
- pi,
175
169
  runtime,
176
170
  manager,
177
171
  () => notifications.dispose(),
@@ -197,31 +191,20 @@ export default function (pi: ExtensionAPI) {
197
191
  spawnAndWait: (snapshot, type, prompt, opts) => manager.spawnAndWait(snapshot, type, prompt, opts),
198
192
  resume: (id, prompt, signal) => manager.resume(id, prompt, signal),
199
193
  getRecord: (id) => manager.getRecord(id),
200
- getMaxConcurrent: () => settings.maxConcurrent,
201
194
  },
202
195
  widget: {
203
196
  setUICtx: (ctx) => runtime.setUICtx(ctx as UICtx),
204
197
  ensureTimer: () => runtime.ensureTimer(),
205
- update: () => runtime.updateWidget(),
198
+ update: () => runtime.update(),
206
199
  markFinished: (id) => runtime.markFinished(id),
207
200
  },
208
201
  agentActivity: runtime.agentActivity,
209
202
  registry,
210
203
  agentDir: getAgentDir(),
211
204
  settings,
212
- buildSnapshot: (inheritContext) =>
213
- buildParentSnapshot(
214
- runtime.currentCtx?.ctx as import("@earendil-works/pi-coding-agent").ExtensionContext,
215
- inheritContext,
216
- ),
217
- getModelInfo: () => ({
218
- parentModel: (runtime.currentCtx?.ctx as any)?.model,
219
- modelRegistry: (runtime.currentCtx?.ctx as any)?.modelRegistry,
220
- }),
221
- getSessionInfo: () => ({
222
- parentSessionFile: (runtime.currentCtx?.ctx as any)?.sessionManager?.getSessionFile() ?? "",
223
- parentSessionId: (runtime.currentCtx?.ctx as any)?.sessionManager?.getSessionId() ?? "",
224
- }),
205
+ buildSnapshot: runtime.buildSnapshot.bind(runtime),
206
+ getModelInfo: runtime.getModelInfo.bind(runtime),
207
+ getSessionInfo: runtime.getSessionInfo.bind(runtime),
225
208
  })));
226
209
 
227
210
  // ---- get_subagent_result tool ----
@@ -2,8 +2,8 @@
2
2
  * parent-snapshot.ts — Capture parent session state as a plain data snapshot.
3
3
  */
4
4
 
5
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
5
  import { buildParentContext } from "#src/session/context";
6
+ import type { SessionContext } from "#src/types";
7
7
 
8
8
  /**
9
9
  * Plain data snapshot of the parent session state captured at spawn time.
@@ -32,7 +32,7 @@ export interface ParentSnapshot {
32
32
  * when the user requested the agent, not when a queue slot opens.
33
33
  */
34
34
  export function buildParentSnapshot(
35
- ctx: ExtensionContext,
35
+ ctx: SessionContext,
36
36
  inheritContext?: boolean,
37
37
  ): ParentSnapshot {
38
38
  const parentContext = inheritContext ? buildParentContext(ctx) : undefined;
@@ -40,7 +40,8 @@ export function buildParentSnapshot(
40
40
  cwd: ctx.cwd,
41
41
  systemPrompt: ctx.getSystemPrompt(),
42
42
  model: ctx.model,
43
- modelRegistry: ctx.modelRegistry,
43
+
44
+ modelRegistry: ctx.modelRegistry!,
44
45
  // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: converts empty string to undefined as well as null/undefined
45
46
  parentContext: parentContext || undefined,
46
47
  };
package/src/runtime.ts CHANGED
@@ -6,6 +6,9 @@
6
6
  * Follows the same pattern as pi-permission-system's ExtensionRuntime.
7
7
  */
8
8
 
9
+ import { buildParentSnapshot, type ParentSnapshot } from "#src/lifecycle/parent-snapshot";
10
+ import type { ModelInfo } from "#src/tools/spawn-config";
11
+ import type { SessionContext } from "#src/types";
9
12
  import type { AgentActivityTracker } from "#src/ui/agent-activity-tracker";
10
13
  import type { UICtx } from "#src/ui/agent-widget";
11
14
 
@@ -39,7 +42,7 @@ export interface RunConfig {
39
42
  export class SubagentRuntime {
40
43
  // ── Session state (was closure-scoped in index.ts) ───────────────────────
41
44
  /** Active Pi session context — set on session_start, cleared on session_shutdown. */
42
- currentCtx: { pi: unknown; ctx: unknown } | undefined = undefined;
45
+ currentCtx: SessionContext | undefined = undefined;
43
46
  /**
44
47
  * Per-agent live activity state shared across the notification system,
45
48
  * widget, and tool handlers. The Map itself is never replaced.
@@ -54,8 +57,8 @@ export class SubagentRuntime {
54
57
  // ── Session-context methods ──────────────────────────────────────────────
55
58
 
56
59
  /** Store the active Pi session context (called from session_start). */
57
- setSessionContext(pi: unknown, ctx: unknown): void {
58
- this.currentCtx = { pi, ctx };
60
+ setSessionContext(ctx: SessionContext): void {
61
+ this.currentCtx = ctx;
59
62
  }
60
63
 
61
64
  /** Clear the session context (called from session_shutdown). */
@@ -63,6 +66,31 @@ export class SubagentRuntime {
63
66
  this.currentCtx = undefined;
64
67
  }
65
68
 
69
+ /**
70
+ * Build a parent snapshot from the current session context.
71
+ * Only valid during an active session (currentCtx is defined).
72
+ */
73
+ buildSnapshot(inheritContext: boolean): ParentSnapshot {
74
+
75
+ return buildParentSnapshot(this.currentCtx!, inheritContext);
76
+ }
77
+
78
+ /** Extract model info from the current session context. */
79
+ getModelInfo(): ModelInfo {
80
+ return {
81
+ parentModel: this.currentCtx?.model as ModelInfo["parentModel"],
82
+ modelRegistry: this.currentCtx?.modelRegistry,
83
+ };
84
+ }
85
+
86
+ /** Extract session identity from the current session context. */
87
+ getSessionInfo(): { parentSessionFile: string; parentSessionId: string } {
88
+ return {
89
+ parentSessionFile: this.currentCtx?.sessionManager.getSessionFile() ?? "",
90
+ parentSessionId: this.currentCtx?.sessionManager.getSessionId() ?? "",
91
+ };
92
+ }
93
+
66
94
  // ── Widget delegation methods ─────────────────────────────────────────────
67
95
 
68
96
  /** Delegate to widget.setUICtx — no-op when widget is null. */
@@ -81,7 +109,7 @@ export class SubagentRuntime {
81
109
  }
82
110
 
83
111
  /** Delegate to widget.update — no-op when widget is null. */
84
- updateWidget(): void {
112
+ update(): void {
85
113
  this.widget?.update();
86
114
  }
87
115
 
@@ -5,12 +5,10 @@
5
5
  * (stripping non-serializable fields), and session gating.
6
6
  */
7
7
 
8
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
9
8
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
10
- import { buildParentSnapshot } from "#src/lifecycle/parent-snapshot";
11
9
  import type { SubagentRecord, SubagentsService } from "#src/service/service";
12
10
  import type { ModelRegistry } from "#src/session/model-resolver";
13
- import type { AgentRecord } from "#src/types";
11
+ import type { AgentRecord, SessionContext } from "#src/types";
14
12
 
15
13
  /** Narrow interface for the AgentManager — avoids coupling to the concrete class. */
16
14
  export interface AgentManagerLike {
@@ -23,23 +21,30 @@ export interface AgentManagerLike {
23
21
  queueSteer(id: string, message: string): boolean;
24
22
  }
25
23
 
24
+ /**
25
+ * Narrow runtime interface consumed by the service adapter.
26
+ * `SubagentRuntime` satisfies this structurally; tests use plain stubs.
27
+ */
28
+ export interface ServiceRuntimeLike {
29
+ readonly currentCtx: SessionContext | undefined;
30
+ buildSnapshot(inheritContext: boolean): ParentSnapshot;
31
+ }
32
+
26
33
  /** Create a SubagentsService backed by the given dependencies. */
27
34
  export function createSubagentsService(
28
35
  manager: AgentManagerLike,
29
36
  resolveModel: (input: string, registry: ModelRegistry) => unknown,
30
- getCtx: () => { pi: unknown; ctx: unknown } | undefined,
31
- getModelRegistry: () => ModelRegistry | undefined,
37
+ runtime: ServiceRuntimeLike,
32
38
  ): SubagentsService {
33
39
  return {
34
40
  spawn(type: string, prompt: string, options?) {
35
- const session = getCtx();
36
- if (!session) {
41
+ if (!runtime.currentCtx) {
37
42
  throw new Error("No active session — cannot spawn agents outside a session.");
38
43
  }
39
44
 
40
45
  let model: unknown;
41
46
  if (options?.model) {
42
- const registry = getModelRegistry();
47
+ const registry = runtime.currentCtx.modelRegistry;
43
48
  if (!registry) {
44
49
  throw new Error("No model registry available.");
45
50
  }
@@ -53,10 +58,7 @@ export function createSubagentsService(
53
58
  const description = options?.description ?? prompt.slice(0, 80);
54
59
  const isBackground = !(options?.foreground ?? false);
55
60
 
56
- const snapshot = buildParentSnapshot(
57
- session.ctx as ExtensionContext,
58
- options?.inheritContext,
59
- );
61
+ const snapshot = runtime.buildSnapshot(options?.inheritContext ?? false);
60
62
  return manager.spawn(snapshot, type, prompt, {
61
63
  description,
62
64
  model,
@@ -3,7 +3,19 @@
3
3
  */
4
4
 
5
5
  import type { TextContent } from "@earendil-works/pi-ai";
6
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
+ import type { SessionContext } from "#src/types";
7
+
8
+ /**
9
+ * Minimal structural types for session branch entries consumed by buildParentContext.
10
+ * `getBranch()` returns `unknown[]` in SessionContext (ISP), so we cast to these
11
+ * local shapes instead of coupling to the SDK's SessionEntry type.
12
+ */
13
+ type MessageEntry = {
14
+ type: "message";
15
+ message: { role: string; content: string | { type: string }[] };
16
+ };
17
+ type CompactionEntry = { type: "compaction"; summary?: string };
18
+ type BranchEntry = MessageEntry | CompactionEntry | { type: string };
7
19
 
8
20
  /** Type predicate: narrow an unknown content block to TextContent. */
9
21
  function isTextContent(c: unknown): c is TextContent {
@@ -23,15 +35,16 @@ export function extractText(content: unknown[]): string {
23
35
  * Used when inherit_context is true to give the subagent visibility
24
36
  * into what has been discussed/done so far.
25
37
  */
26
- export function buildParentContext(ctx: ExtensionContext): string {
38
+ export function buildParentContext(ctx: SessionContext): string {
27
39
  const entries = ctx.sessionManager.getBranch();
28
40
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- getBranch() may return undefined at runtime despite its type
29
41
  if (!entries || entries.length === 0) return "";
30
42
 
31
43
  const parts: string[] = [];
32
44
 
33
- for (const entry of entries) {
34
- if (entry.type === "message") {
45
+ for (const rawEntry of entries as BranchEntry[]) {
46
+ if (rawEntry.type === "message") {
47
+ const entry = rawEntry as MessageEntry;
35
48
  const msg = entry.message;
36
49
  if (msg.role === "user") {
37
50
  const text = typeof msg.content === "string"
@@ -39,12 +52,13 @@ export function buildParentContext(ctx: ExtensionContext): string {
39
52
  : extractText(msg.content);
40
53
  if (text.trim()) parts.push(`[User]: ${text.trim()}`);
41
54
  } else if (msg.role === "assistant") {
42
- const text = extractText(msg.content);
55
+ const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
43
56
  if (text.trim()) parts.push(`[Assistant]: ${text.trim()}`);
44
57
  }
45
58
  // Skip toolResult messages — too verbose for context
46
- } else if (entry.type === "compaction") {
59
+ } else if (rawEntry.type === "compaction") {
47
60
  // Include compaction summaries — they're already condensed
61
+ const entry = rawEntry as CompactionEntry;
48
62
  if (entry.summary) {
49
63
  parts.push(`[Summary]: ${entry.summary}`);
50
64
  }
@@ -23,7 +23,6 @@ export interface AgentToolManager {
23
23
  spawnAndWait: (snapshot: ParentSnapshot, type: string, prompt: string, opts: Omit<AgentSpawnConfig, "isBackground">) => Promise<AgentRecord>;
24
24
  resume: (id: string, prompt: string, signal: AbortSignal) => Promise<AgentRecord | undefined>;
25
25
  getRecord: (id: string) => AgentRecord | undefined;
26
- getMaxConcurrent: () => number;
27
26
  }
28
27
 
29
28
  /** Narrow widget interface — only the methods the Agent tool calls. */
@@ -50,8 +49,8 @@ export interface AgentToolDeps {
50
49
  agentActivity: AgentActivityAccess;
51
50
  registry: AgentTypeRegistry;
52
51
  agentDir: string;
53
- /** Narrow settings accessor — only the default max turns is needed here. */
54
- settings: { readonly defaultMaxTurns: number | undefined };
52
+ /** Narrow settings accessor — only the fields the Agent tool reads. */
53
+ settings: { readonly defaultMaxTurns: number | undefined; readonly maxConcurrent: number };
55
54
  /** Build a ParentSnapshot from the current session context. */
56
55
  buildSnapshot: (inheritContext: boolean) => ParentSnapshot;
57
56
  /** Model info from the current session context. */
@@ -252,7 +251,7 @@ Guidelines:
252
251
  manager,
253
252
  widget,
254
253
  agentActivity,
255
- { config, snapshot, parentSession },
254
+ { config, snapshot, parentSession, settings },
256
255
  );
257
256
  }
258
257
 
@@ -11,7 +11,6 @@ import { subscribeUIObserver } from "#src/ui/ui-observer";
11
11
  export interface BackgroundManagerDeps {
12
12
  spawn(snapshot: ParentSnapshot, type: string, prompt: string, opts: AgentSpawnConfig): string;
13
13
  getRecord(id: string): AgentRecord | undefined;
14
- getMaxConcurrent(): number;
15
14
  }
16
15
 
17
16
  /** Narrow widget interface for the background spawner. */
@@ -25,6 +24,7 @@ export interface BackgroundParams {
25
24
  config: ResolvedSpawnConfig;
26
25
  snapshot: ParentSnapshot;
27
26
  parentSession: ParentSessionInfo;
27
+ settings: { readonly maxConcurrent: number };
28
28
  }
29
29
 
30
30
  /**
@@ -77,7 +77,7 @@ export function spawnBackground(
77
77
  `Description: ${execution.description}\n` +
78
78
  (record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
79
79
  (isQueued
80
- ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n`
80
+ ? `Position: queued (max ${params.settings.maxConcurrent} concurrent)\n`
81
81
  : "") +
82
82
  `\nYou will be notified when this agent completes.\n` +
83
83
  `Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
@@ -21,8 +21,6 @@ export interface FormatterContext {
21
21
 
22
22
  // ── File-local types and guards ─────────────────────────────────────────────
23
23
 
24
- export { getToolCallName } from "#src/session/content-items";
25
-
26
24
  /** Bash execution message — 'bashExecution' role is not in the SDK's AgentSession message role union. */
27
25
  export interface BashExecutionMessage {
28
26
  role: "bashExecution";