@gotgenes/pi-subagents 6.14.1 → 6.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,290 @@
1
+ ---
2
+ issue: 145
3
+ issue_title: "Decompose execute and push ExtensionContext to the boundary (Phase 9, Step M)"
4
+ ---
5
+
6
+ # Decompose execute and push ctx to the boundary
7
+
8
+ ## Problem Statement
9
+
10
+ `agent-tool.ts` `execute` is ~140 lines mixing three concerns: boundary extraction (~5 lines reading `ctx`), config resolution (~60 lines unpacking `resolvedConfig` field by field), and dispatch (~80 lines building 14–16 field parameter bags for `spawnBackground` and `runForeground`).
11
+ The large parameter bags exist because config resolution happens inline instead of in a dedicated function.
12
+ Meanwhile, `ExtensionContext` is threaded from `execute` through `ForegroundParams.ctx` / `BackgroundParams.ctx` into `foreground-runner` and `background-spawner`, where the only thing consumed is `sessionManager.getSessionFile()` and `sessionManager.getSessionId()`.
13
+ `AgentManager.spawn()` and `spawnAndWait()` accept `ExtensionContext` directly and call `buildParentSnapshot(ctx)` internally — but this is already a pure boundary concern that belongs at the call site.
14
+ Additionally, `execute` reaches into `ctx` for model info and session identity — these are session-scoped values that `index.ts` already captures and could inject as collaborators, removing `execute`'s need to read `ctx` beyond the UI context it already delegates to `widget.setUICtx()`.
15
+
16
+ ## Goals
17
+
18
+ - Extract config resolution into a pure function (`resolveSpawnConfig`) so `execute` becomes: resolve config → dispatch.
19
+ - Inject three missing collaborators into `createAgentTool` so `execute` no longer extracts values from `ctx`:
20
+ - `buildSnapshot: (inheritContext: boolean) => ParentSnapshot` — closure over `ctx`, wired in `index.ts`.
21
+ - `getModelInfo: () => ModelInfo` — provides `parentModel` and `modelRegistry` for `resolveSpawnConfig`.
22
+ - `getSessionInfo: () => { parentSessionFile: string; parentSessionId: string }` — parent session identity.
23
+ - Replace `ForegroundParams.ctx` and `BackgroundParams.ctx` with plain domain values (`parentSessionFile`, `parentSessionId`, `snapshot`).
24
+ - Change `AgentManager.spawn()` and `spawnAndWait()` to accept `ParentSnapshot` instead of `ExtensionContext`.
25
+ - Move `buildParentSnapshot(ctx)` calls to the two boundaries: `index.ts` (via closure) and `service-adapter.ts`.
26
+ - Eliminate the `vi.mock("../src/parent-snapshot.js")` in `agent-manager.test.ts`.
27
+ - Apply the dependency bag convention: dissolve `ForegroundDeps`, `BackgroundDeps`, `AdapterDeps` (each ≤3 fields) into plain parameters.
28
+ - This is a breaking internal refactor — no public API changes.
29
+
30
+ ## Non-Goals
31
+
32
+ - Narrowing menu handler ctx (Step N, #146) — deferred.
33
+ - Injecting text wrapping into ConversationViewer (Step O, #147) — unrelated track.
34
+ - Observation model consolidation (Step L, #144) — independent track.
35
+ - Changing the `SubagentsService` public API in `service.ts`.
36
+
37
+ ## Background
38
+
39
+ ### Relevant modules
40
+
41
+ | Module | Current role |
42
+ | ----------------------------- | ------------------------------------------------------------------------------------------------ |
43
+ | `tools/agent-tool.ts` | `execute` callback — 140 lines, mixes boundary extraction, config resolution, dispatch |
44
+ | `tools/foreground-runner.ts` | `runForeground()` — receives 14-field `ForegroundParams` including `ctx` with `sessionManager` |
45
+ | `tools/background-spawner.ts` | `spawnBackground()` — receives 14-field `BackgroundParams` including `ctx` with `sessionManager` |
46
+ | `agent-manager.ts` | `spawn()` / `spawnAndWait()` accept `ExtensionContext`, call `buildParentSnapshot()` internally |
47
+ | `parent-snapshot.ts` | `buildParentSnapshot(ctx)` — pure function capturing `ParentSnapshot` from ctx |
48
+ | `service-adapter.ts` | Cross-extension boundary — calls `manager.spawn(session.ctx, ...)` |
49
+ | `invocation-config.ts` | `resolveAgentInvocationConfig()` — merges agent config with tool params |
50
+ | `model-resolver.ts` | `resolveInvocationModel()` — resolves model strings to model instances |
51
+ | `index.ts` | Extension entry point — wires `createAgentTool` deps, captures `runtime.currentCtx` |
52
+ | `runtime.ts` | `SubagentRuntime` — holds session-scoped mutable state including `currentCtx` |
53
+
54
+ ### Constraints from AGENTS.md
55
+
56
+ - Keep modules focused and composable (one concern per file).
57
+ - Prefer explicit configuration over hidden behavior.
58
+ - Keep Pi SDK imports out of business-logic modules.
59
+ - Business logic should be pure functions — keep IO at the edges.
60
+
61
+ ### Phase 9 context
62
+
63
+ This is Step M of Phase 9.
64
+ It has no blockers and blocks Step N (#146), which narrows menu handler ctx.
65
+ After this step, `ExtensionContext` appears only at true SDK/extension boundaries: `index.ts` closures, `service-adapter.ts`, and menu handlers.
66
+
67
+ ## Design Overview
68
+
69
+ ### Part 1: Extract config resolution (done)
70
+
71
+ A new pure function `resolveSpawnConfig` in `spawn-config.ts` encapsulates all config resolution logic previously inline in `execute`.
72
+ `execute` calls `resolveSpawnConfig(params, registry, modelInfo, settings)` and dispatches on the result.
73
+ This is already committed.
74
+
75
+ ### Part 2: Inject collaborators and push ctx out of execute
76
+
77
+ `execute` currently reads `ctx.model`, `ctx.modelRegistry`, `ctx.sessionManager`, and passes `ctx` to `buildParentSnapshot`.
78
+ These are all session-scoped values that `index.ts` captures at session start.
79
+ Three collaborators replace the `ctx` reads:
80
+
81
+ ```typescript
82
+ // Injected as plain parameters into createAgentTool:
83
+ buildSnapshot: (inheritContext: boolean) => ParentSnapshot,
84
+ getModelInfo: () => ModelInfo,
85
+ getSessionInfo: () => { parentSessionFile: string; parentSessionId: string },
86
+ ```
87
+
88
+ `index.ts` wires them as closures over `runtime.currentCtx`:
89
+
90
+ ```typescript
91
+ createAgentTool({
92
+ // ... existing params ...
93
+ buildSnapshot: (inheritContext) => buildParentSnapshot(ctx, inheritContext),
94
+ getModelInfo: () => ({
95
+ parentModel: ctx.model,
96
+ modelRegistry: ctx.modelRegistry,
97
+ }),
98
+ getSessionInfo: () => ({
99
+ parentSessionFile: ctx.sessionManager.getSessionFile(),
100
+ parentSessionId: ctx.sessionManager.getSessionId(),
101
+ }),
102
+ })
103
+ ```
104
+
105
+ After this, `execute` touches `ctx` only for `ctx.ui` — which is already delegated via `widget.setUICtx()`.
106
+ The `ExtensionContext` import in `agent-tool.ts` is removed entirely.
107
+
108
+ ### Part 3: Push ctx out of AgentManager
109
+
110
+ `AgentManager.spawn()` and `spawnAndWait()` accept `ParentSnapshot` instead of `ExtensionContext`.
111
+ The internal `buildParentSnapshot(ctx, ...)` call is removed — `snapshot` arrives pre-built from the call sites.
112
+ `service-adapter.ts` calls `buildParentSnapshot(session.ctx, ...)` at its boundary before delegating.
113
+
114
+ ### Part 4: Push ctx out of foreground-runner and background-spawner
115
+
116
+ `ForegroundParams.ctx` and `BackgroundParams.ctx` are replaced by `snapshot: ParentSnapshot`, `parentSessionFile: string`, `parentSessionId: string`.
117
+ The narrow manager interfaces change from `ctx: any` to `snapshot: ParentSnapshot`.
118
+
119
+ ### Part 5: Shrink params bags with ResolvedSpawnConfig
120
+
121
+ `ForegroundParams` and `BackgroundParams` carry `ResolvedSpawnConfig` instead of 10+ individual fields that were computed during config resolution.
122
+ Only dispatch-specific fields (`rawType`, `fellBack`, `toolCallId`, `displayName`) remain as separate params fields.
123
+
124
+ ### Part 6: Dissolve small dependency bags
125
+
126
+ Per the dependency bag convention:
127
+
128
+ - `ForegroundDeps` (3 fields) → plain parameters on `runForeground`.
129
+ - `BackgroundDeps` (3 fields) → plain parameters on `spawnBackground`.
130
+ - `AdapterDeps` (4 fields) → plain parameters on `createSubagentsService`.
131
+ - `AgentToolDeps` → destructured in the `createAgentTool` signature; the interface stays as a named type for the test factory.
132
+
133
+ The narrow `*ManagerDeps` and `*WidgetDeps` interfaces stay — they define the contract each function needs from its collaborators.
134
+
135
+ ## Module-Level Changes
136
+
137
+ ### New file: `src/tools/spawn-config.ts` (done)
138
+
139
+ - `ResolvedSpawnConfig` interface.
140
+ - `ModelInfo` interface.
141
+ - `resolveSpawnConfig()` pure function.
142
+
143
+ ### Modified: `src/tools/agent-tool.ts`
144
+
145
+ - `execute` shrinks from ~140 to ~20 lines.
146
+ - `ExtensionContext` import removed — `execute` no longer reads `ctx` directly (beyond `ctx.ui` via widget).
147
+ - Three new collaborator parameters: `buildSnapshot`, `getModelInfo`, `getSessionInfo`.
148
+ - Calls `resolveSpawnConfig(params, registry, getModelInfo(), settings)`.
149
+ - Calls `buildSnapshot(config.inheritContext)` for the snapshot.
150
+ - Calls `getSessionInfo()` for parent session identity.
151
+ - Passes domain values (not `ctx`) to `runForeground` / `spawnBackground`.
152
+ - `AgentToolManager.spawn` and `spawnAndWait` signatures change to accept `ParentSnapshot`.
153
+ - `AgentToolDeps` stays as a named type (used by test factory) but its fields are destructured in `createAgentTool`.
154
+
155
+ ### Modified: `src/tools/foreground-runner.ts`
156
+
157
+ - `ForegroundDeps` interface removed — `runForeground` accepts `manager`, `widget`, `agentActivity` as plain parameters.
158
+ - `ForegroundParams.ctx` removed — replaced by `snapshot`, `parentSessionFile`, `parentSessionId`.
159
+ - `ForegroundManagerDeps.spawnAndWait` signature changes from `ctx: any` to `snapshot: ParentSnapshot`.
160
+ - Individual config fields move into `ResolvedSpawnConfig`.
161
+
162
+ ### Modified: `src/tools/background-spawner.ts`
163
+
164
+ - `BackgroundDeps` interface removed — `spawnBackground` accepts `manager`, `widget`, `agentActivity` as plain parameters.
165
+ - `BackgroundParams.ctx` removed — replaced by `snapshot`, `parentSessionFile`, `parentSessionId`.
166
+ - `BackgroundManagerDeps.spawn` signature changes from `ctx: any` to `snapshot: ParentSnapshot`.
167
+ - Individual config fields move into `ResolvedSpawnConfig`.
168
+
169
+ ### Modified: `src/agent-manager.ts`
170
+
171
+ - `spawn()` signature changes from `ctx: ExtensionContext` to `snapshot: ParentSnapshot`.
172
+ - `spawnAndWait()` signature changes from `ctx: ExtensionContext` to `snapshot: ParentSnapshot`.
173
+ - Internal `buildParentSnapshot(ctx, ...)` call removed.
174
+ - Imports of `ExtensionContext` and `buildParentSnapshot` removed.
175
+
176
+ ### Modified: `src/service-adapter.ts`
177
+
178
+ - `AdapterDeps` interface removed — `createSubagentsService` accepts plain parameters.
179
+ - `AgentManagerLike.spawn` signature changes from `ctx: unknown` to `snapshot: ParentSnapshot`.
180
+ - `spawn()` method calls `buildParentSnapshot(session.ctx, options?.inheritContext)` before delegating.
181
+ - Adds imports of `buildParentSnapshot` and `ParentSnapshot`.
182
+
183
+ ### Modified: `src/index.ts`
184
+
185
+ - Wiring for `createAgentTool` adds three collaborator closures: `buildSnapshot`, `getModelInfo`, `getSessionInfo`.
186
+ - `manager.spawn` / `spawnAndWait` wiring adapters removed (closures no longer need to relay `ctx`).
187
+ - Wiring for `createSubagentsService` changes from bag to plain arguments.
188
+
189
+ ## Test Impact Analysis
190
+
191
+ ### New unit tests enabled
192
+
193
+ - `spawn-config.test.ts` (done) — pure-function tests for `resolveSpawnConfig`.
194
+
195
+ ### Existing tests that simplify
196
+
197
+ - `agent-manager.test.ts` — the `vi.mock("../src/parent-snapshot.js")` block is removed.
198
+ All tests pass a plain `ParentSnapshot` object directly instead of `mockCtx`.
199
+ - `foreground-runner.test.ts` — `makeCtx()` helper removed; plain strings for session identity.
200
+ - `background-spawner.test.ts` — same as foreground.
201
+ - `agent-tool.test.ts` — `makeCtx()` simplified; collaborator stubs replace `ctx.model` / `ctx.modelRegistry` reads.
202
+ - `service-adapter.test.ts` — adapter test setup changes from bag to plain parameters.
203
+
204
+ ### Existing tests that stay
205
+
206
+ - `parent-snapshot.test.ts` — unchanged; `buildParentSnapshot` is still a standalone pure function.
207
+
208
+ ## TDD Order
209
+
210
+ ### Step 1: Extract resolveSpawnConfig (done)
211
+
212
+ 1. ~~Write `spawn-config.test.ts`, implement `spawn-config.ts`.~~
213
+ Commit: `feat: extract resolveSpawnConfig pure function (#145)` ✓
214
+
215
+ 2. ~~Rewire `execute` to call `resolveSpawnConfig`.~~
216
+ Commit: `refactor: use resolveSpawnConfig in execute (#145)` ✓
217
+
218
+ ### Step 2: Push ctx out of AgentManager
219
+
220
+ 3. Red: update `agent-manager.test.ts` — replace `mockCtx` with a plain `ParentSnapshot` object, remove `vi.mock("../src/parent-snapshot.js")`.
221
+ Green: change `AgentManager.spawn()` and `spawnAndWait()` to accept `ParentSnapshot`.
222
+ Update `agent-tool.ts` manager interface, `service-adapter.ts` to call `buildParentSnapshot` at its boundary, and `index.ts` wiring.
223
+ Commit: `refactor: AgentManager accepts ParentSnapshot instead of ExtensionContext (#145)`
224
+
225
+ ### Step 3: Inject collaborators into createAgentTool
226
+
227
+ 4. Red: update `agent-tool.test.ts` — add `buildSnapshot`, `getModelInfo`, `getSessionInfo` stubs to `createToolDeps`; simplify `makeCtx()`.
228
+ Green: add three collaborator parameters to `createAgentTool`; rewrite `execute` to use them instead of `ctx.model` / `ctx.modelRegistry` / `ctx.sessionManager`.
229
+ Remove `ExtensionContext` import from `agent-tool.ts`.
230
+ Update `index.ts` wiring to provide closures.
231
+ Commit: `refactor: inject collaborators into createAgentTool, eliminate ctx reads (#145)`
232
+
233
+ ### Step 4: Push ctx out of foreground-runner and background-spawner
234
+
235
+ 5. Red: update `foreground-runner.test.ts` — remove `makeCtx()`, replace `ForegroundParams.ctx` with `snapshot` / `parentSessionFile` / `parentSessionId`.
236
+ Green: change `ForegroundParams` to use plain domain values, update `runForeground` accordingly.
237
+ Commit: `refactor: foreground-runner receives domain values instead of ctx (#145)`
238
+
239
+ 6. Red: update `background-spawner.test.ts` — remove `makeCtx()`, replace `BackgroundParams.ctx` with `snapshot` / `parentSessionFile` / `parentSessionId`.
240
+ Green: change `BackgroundParams` to use plain domain values, update `spawnBackground` accordingly.
241
+ Commit: `refactor: background-spawner receives domain values instead of ctx (#145)`
242
+
243
+ ### Step 5: Shrink params bags with ResolvedSpawnConfig
244
+
245
+ 7. Red: update `foreground-runner.test.ts` `makeParams()` to use `ResolvedSpawnConfig` fields.
246
+ Green: change `ForegroundParams` to carry `ResolvedSpawnConfig`.
247
+ Update `agent-tool.ts` dispatch to pass the config through.
248
+ Commit: `refactor: ForegroundParams carries ResolvedSpawnConfig (#145)`
249
+
250
+ 8. Red: update `background-spawner.test.ts` `makeParams()` to use `ResolvedSpawnConfig` fields.
251
+ Green: change `BackgroundParams` to carry `ResolvedSpawnConfig`.
252
+ Update `agent-tool.ts` dispatch to pass the config through.
253
+ Commit: `refactor: BackgroundParams carries ResolvedSpawnConfig (#145)`
254
+
255
+ ### Step 6: Dissolve small dependency bags
256
+
257
+ 9. Red: update `foreground-runner.test.ts` calls to pass `manager`, `widget`, `agentActivity` as plain args.
258
+ Green: remove `ForegroundDeps` interface, change `runForeground` signature.
259
+ Commit: `refactor: dissolve ForegroundDeps into plain parameters (#145)`
260
+
261
+ 10. Red: update `background-spawner.test.ts` calls to pass plain args.
262
+ Green: remove `BackgroundDeps` interface, change `spawnBackground` signature.
263
+ Commit: `refactor: dissolve BackgroundDeps into plain parameters (#145)`
264
+
265
+ 11. Red: update `service-adapter.test.ts` to pass plain parameters instead of `AdapterDeps` bag.
266
+ Green: remove `AdapterDeps` interface, change `createSubagentsService` signature.
267
+ Update `index.ts` wiring call site.
268
+ Commit: `refactor: dissolve AdapterDeps into plain parameters (#145)`
269
+
270
+ 12. Refactor: destructure `AgentToolDeps` in `createAgentTool` signature (keep the named type for test factory).
271
+ Commit: `refactor: destructure AgentToolDeps in createAgentTool (#145)`
272
+
273
+ ### Step 7: Final verification
274
+
275
+ 13. Run full test suite and type check.
276
+
277
+ ## Risks and Mitigations
278
+
279
+ | Risk | Mitigation |
280
+ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
281
+ | Wide blast radius — touches 7+ source files and 5+ test files | Incremental TDD steps; each commit leaves the repo green |
282
+ | `service-adapter.ts` now imports `buildParentSnapshot` — new coupling | Acceptable: the adapter is already a boundary module that bridges `ExtensionContext` to domain types |
283
+ | `ResolvedSpawnConfig` could become a new "god object" | It is a pure data return from a single function; consumers destructure what they need |
284
+ | Three new collaborators grow `AgentToolDeps` from 6 to 9 fields | The deps bag is destructured at the signature; the named type exists only for the test factory. The real dependency count stays the same — previously hidden behind `ctx` reads |
285
+ | `index.ts` closures capture `ctx` — stale reference risk | Same pattern `service-adapter.ts` already uses via `runtime.currentCtx`; session lifecycle clears on shutdown |
286
+
287
+ ## Open Questions
288
+
289
+ - The exact boundary between fields that stay in `ForegroundParams` / `BackgroundParams` vs. fields that move into `ResolvedSpawnConfig` may shift during implementation.
290
+ The guiding principle: if the field is computed during config resolution, it belongs in `ResolvedSpawnConfig`; if it is dispatch-specific (e.g., `toolCallId`, `signal`, `onUpdate`), it stays in the params type.
@@ -0,0 +1,56 @@
1
+ ---
2
+ issue: 145
3
+ issue_title: "Decompose execute and push ExtensionContext to the boundary (Phase 9, Step M)"
4
+ ---
5
+
6
+ # Retro: #145 — Decompose execute and push ExtensionContext to the boundary
7
+
8
+ ## Final Retrospective (2026-05-23)
9
+
10
+ ### Session summary
11
+
12
+ Extracted config resolution into a pure `resolveSpawnConfig` function, injected three collaborators (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) into `createAgentTool` to eliminate `ctx` reads from `execute`, pushed `ParentSnapshot` to `AgentManager`'s public API, and dissolved three small dependency bags (`ForegroundDeps`, `BackgroundDeps`, `AdapterDeps`) into plain parameters.
13
+ Released as `pi-subagents-v6.15.0`.
14
+
15
+ ### Observations
16
+
17
+ #### What went well
18
+
19
+ - User's two escalating questions ("Are there any other missing collaborators?"
20
+ → "Hiding dependencies in an object bag still counts as dependencies!") caught a `premature-convergence` before it landed as committed code.
21
+ The reverted partial step 3 attempt was ~4 files of changes that would have needed rework.
22
+ The resulting design (injected collaborators) is meaningfully better than the original plan's mechanical relocation.
23
+ - Folding tightly-coupled TDD steps (ctx elimination + params shrinking + deps dissolution) into fewer commits avoided intermediate states with broken types.
24
+ The plan's 12-step sequence would have required lift-and-shift gymnastics; the actual 7-commit sequence was cleaner.
25
+
26
+ #### What caused friction (agent side)
27
+
28
+ - `premature-convergence` — the original plan relocated `buildParentSnapshot` calls to `execute` without questioning whether `execute` should read `ctx` at all.
29
+ The existing `code-design` skill has DIP and parameter-relay rules that should have flagged this.
30
+ The `service-adapter.ts` module already demonstrated the getter-injection pattern (`getCtx`, `getModelRegistry`), but I didn't search for it during plan writing.
31
+ Impact: one plan rewrite commit (76bb57b), one reverted partial implementation (~15 minutes of rework).
32
+ User-caught.
33
+
34
+ - `missing-context` — didn't use `colgrep` during initial plan writing to discover the established getter-injection convention in `service-adapter.ts`.
35
+ Used `grep` exclusively for exact symbol matching.
36
+ When prompted by the user to use `colgrep`, the results were confirmatory rather than revelatory because I'd already read the relevant files by that point.
37
+ The miss was not using it *earlier* for intent-based exploration ("how do existing modules inject session-scoped state?").
38
+ Impact: added friction but no rework — the user's questions surfaced the pattern before code was committed.
39
+ User-caught.
40
+
41
+ - `instruction-violation` — wrote an inline `import()` type assertion (`session.ctx as import("@earendil-works/pi-coding-agent").ExtensionContext`) in `service-adapter.ts`.
42
+ AGENTS.md says "Use standard top-level imports only."
43
+ Impact: one extra edit round, caught before committing.
44
+ User-caught.
45
+
46
+ #### What caused friction (user side)
47
+
48
+ - The user's redirecting questions were well-timed and effective.
49
+ The escalation from "Are there any other missing collaborators?"
50
+ to the more pointed "Hiding dependencies in an object bag still counts as dependencies!"
51
+ was the right amount of pressure.
52
+ No friction observed on the user side.
53
+
54
+ ### Changes made
55
+
56
+ 1. `.pi/prompts/plan-issue.md` — added `colgrep` skill loading to the "Load skills" section for code-change plans, so convention discovery happens during exploration rather than after committing to a design.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.14.1",
3
+ "version": "6.16.0",
4
4
  "exports": {
5
5
  ".": "./src/service.ts"
6
6
  },
@@ -8,14 +8,13 @@
8
8
 
9
9
  import { randomUUID } from "node:crypto";
10
10
  import type { Model } from "@earendil-works/pi-ai";
11
- import type { AgentSession, ExtensionContext } from "@earendil-works/pi-coding-agent";
11
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
12
12
  import { AgentRecord } from "./agent-record.js";
13
13
  import type { AgentRunner } from "./agent-runner.js";
14
14
  import { AgentTypeRegistry } from "./agent-types.js";
15
15
  import { debugLog } from "./debug.js";
16
16
  import { NotificationState } from "./notification-state.js";
17
17
  import type { ParentSnapshot } from "./parent-snapshot.js";
18
- import { buildParentSnapshot } from "./parent-snapshot.js";
19
18
  import { subscribeRecordObserver } from "./record-observer.js";
20
19
  import type { RunConfig } from "./runtime.js";
21
20
  import type { AgentInvocation, IsolationMode, ShellExec, SubagentType, ThinkingLevel } from "./types.js";
@@ -141,7 +140,7 @@ export class AgentManager {
141
140
  * If the concurrency limit is reached, the agent is queued.
142
141
  */
143
142
  spawn(
144
- ctx: ExtensionContext,
143
+ snapshot: ParentSnapshot,
145
144
  type: SubagentType,
146
145
  prompt: string,
147
146
  options: AgentSpawnConfig,
@@ -167,7 +166,6 @@ export class AgentManager {
167
166
  this.observer?.onAgentCreated(record);
168
167
  }
169
168
 
170
- const snapshot = buildParentSnapshot(ctx, options.inheritContext);
171
169
  const args: SpawnArgs = { snapshot, type, prompt, options };
172
170
 
173
171
  if (options.isBackground && !options.bypassQueue && this.runningBackground >= this._getMaxConcurrent()) {
@@ -332,12 +330,12 @@ export class AgentManager {
332
330
  * Foreground agents bypass the concurrency queue.
333
331
  */
334
332
  async spawnAndWait(
335
- ctx: ExtensionContext,
333
+ snapshot: ParentSnapshot,
336
334
  type: SubagentType,
337
335
  prompt: string,
338
336
  options: Omit<AgentSpawnConfig, "isBackground">,
339
337
  ): Promise<AgentRecord> {
340
- const id = this.spawn(ctx, type, prompt, { ...options, isBackground: false });
338
+ const id = this.spawn(snapshot, type, prompt, { ...options, isBackground: false });
341
339
  const record = this.agents.get(id)!;
342
340
  await record.promise;
343
341
  return record;
@@ -352,7 +350,7 @@ export class AgentManager {
352
350
  signal?: AbortSignal,
353
351
  ): Promise<AgentRecord | undefined> {
354
352
  const record = this.agents.get(id);
355
- const session = record?.execution?.session;
353
+ const session = record?.session;
356
354
  if (!session) return undefined;
357
355
 
358
356
  record.resetForResume(Date.now());
@@ -404,7 +402,7 @@ export class AgentManager {
404
402
 
405
403
  /** Dispose a record's session and remove it from the map. */
406
404
  private removeRecord(id: string, record: AgentRecord): void {
407
- record.execution?.session?.dispose?.();
405
+ record.session?.dispose?.();
408
406
  this.agents.delete(id);
409
407
  this.pendingSteers.delete(id);
410
408
  }
@@ -482,7 +480,7 @@ export class AgentManager {
482
480
  // Clear queue
483
481
  this.queue = [];
484
482
  for (const record of this.agents.values()) {
485
- record.execution?.session?.dispose();
483
+ record.session?.dispose();
486
484
  }
487
485
  this.agents.clear();
488
486
  // Prune any orphaned git worktrees (crash recovery)
@@ -12,6 +12,7 @@
12
12
  * after construction as lifecycle information becomes available.
13
13
  */
14
14
 
15
+ import type { AgentSession } from "@earendil-works/pi-coding-agent";
15
16
  import type { ExecutionState } from "./execution-state.js";
16
17
  import type { NotificationState } from "./notification-state.js";
17
18
  import type { AgentInvocation, SubagentType } from "./types.js";
@@ -85,6 +86,16 @@ export class AgentRecord {
85
86
  worktreeState?: WorktreeState;
86
87
  notification?: NotificationState;
87
88
 
89
+ /** The active agent session, or undefined before the session is created. */
90
+ get session(): AgentSession | undefined {
91
+ return this.execution?.session;
92
+ }
93
+
94
+ /** Path to the agent's session JSONL file, or undefined if not yet available. */
95
+ get outputFile(): string | undefined {
96
+ return this.execution?.outputFile;
97
+ }
98
+
88
99
  constructor(init: AgentRecordInit) {
89
100
  this.id = init.id;
90
101
  this.type = init.type;
package/src/index.ts CHANGED
@@ -29,6 +29,7 @@ import { SessionLifecycleHandler, ToolStartHandler } from "./handlers/index.js";
29
29
  import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
30
30
  import { type ModelRegistry, resolveModel } from "./model-resolver.js";
31
31
  import { buildEventData, type NotificationDetails, NotificationManager } from "./notification.js";
32
+ import { buildParentSnapshot } from "./parent-snapshot.js";
32
33
  import { buildAgentPrompt } from "./prompts.js";
33
34
  import { createNotificationRenderer } from "./renderer.js";
34
35
  import { createSubagentRuntime } from "./runtime.js";
@@ -61,12 +62,12 @@ export default function (pi: ExtensionAPI) {
61
62
  // ---- Notification system ----
62
63
  // runtime.widget is assigned after AgentManager construction; arrow closures
63
64
  // capture `runtime` by reference so they always read the current value.
64
- const notifications = new NotificationManager({
65
- sendMessage: (msg, opts) => pi.sendMessage(msg, opts),
66
- agentActivity: runtime.agentActivity,
67
- markFinished: (id) => runtime.markFinished(id),
68
- updateWidget: () => runtime.updateWidget(),
69
- });
65
+ const notifications = new NotificationManager(
66
+ (msg, opts) => pi.sendMessage(msg, opts),
67
+ runtime.agentActivity,
68
+ (id) => runtime.markFinished(id),
69
+ () => runtime.updateWidget(),
70
+ );
70
71
 
71
72
  // Settings: owns all three in-memory values and handles load/save/emit.
72
73
  // onMaxConcurrentChanged is wired after manager is constructed (closure captures by reference).
@@ -162,12 +163,12 @@ export default function (pi: ExtensionAPI) {
162
163
 
163
164
  // Typed service published via Symbol.for() for cross-extension access.
164
165
  // Consumers: const { getSubagentsService } = await import("@gotgenes/pi-subagents");
165
- const service = createSubagentsService({
166
+ const service = createSubagentsService(
166
167
  manager,
167
168
  resolveModel,
168
- getCtx: () => runtime.currentCtx,
169
- getModelRegistry: () => (runtime.currentCtx?.ctx as { modelRegistry?: ModelRegistry } | undefined)?.modelRegistry,
170
- });
169
+ () => runtime.currentCtx,
170
+ () => (runtime.currentCtx?.ctx as { modelRegistry?: ModelRegistry } | undefined)?.modelRegistry,
171
+ );
171
172
  publishSubagentsService(service);
172
173
 
173
174
  const lifecycle = new SessionLifecycleHandler(
@@ -193,8 +194,8 @@ export default function (pi: ExtensionAPI) {
193
194
 
194
195
  pi.registerTool(defineTool(createAgentTool({
195
196
  manager: {
196
- spawn: (ctx, type, prompt, opts) => manager.spawn(ctx, type, prompt, opts),
197
- spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(ctx, type, prompt, opts),
197
+ spawn: (snapshot, type, prompt, opts) => manager.spawn(snapshot, type, prompt, opts),
198
+ spawnAndWait: (snapshot, type, prompt, opts) => manager.spawnAndWait(snapshot, type, prompt, opts),
198
199
  resume: (id, prompt, signal) => manager.resume(id, prompt, signal),
199
200
  getRecord: (id) => manager.getRecord(id),
200
201
  getMaxConcurrent: () => settings.maxConcurrent,
@@ -209,6 +210,19 @@ export default function (pi: ExtensionAPI) {
209
210
  registry,
210
211
  agentDir: getAgentDir(),
211
212
  settings,
213
+ buildSnapshot: (inheritContext) =>
214
+ buildParentSnapshot(
215
+ runtime.currentCtx?.ctx as import("@earendil-works/pi-coding-agent").ExtensionContext,
216
+ inheritContext,
217
+ ),
218
+ getModelInfo: () => ({
219
+ parentModel: (runtime.currentCtx?.ctx as any)?.model,
220
+ modelRegistry: (runtime.currentCtx?.ctx as any)?.modelRegistry,
221
+ }),
222
+ getSessionInfo: () => ({
223
+ parentSessionFile: (runtime.currentCtx?.ctx as any)?.sessionManager?.getSessionFile() ?? "",
224
+ parentSessionId: (runtime.currentCtx?.ctx as any)?.sessionManager?.getSessionId() ?? "",
225
+ }),
212
226
  })));
213
227
 
214
228
  // ---- get_subagent_result tool ----
@@ -235,7 +249,7 @@ export default function (pi: ExtensionAPI) {
235
249
  manager: {
236
250
  listAgents: () => manager.listAgents(),
237
251
  getRecord: (id) => manager.getRecord(id),
238
- spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(ctx, type, prompt, opts),
252
+ spawnAndWait: (ctx, type, prompt, opts) => manager.spawnAndWait(buildParentSnapshot(ctx), type, prompt, opts),
239
253
  },
240
254
  registry,
241
255
  agentActivity: runtime.agentActivity,
@@ -46,7 +46,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
46
46
  const status = getStatusLabel(record.status, record.error);
47
47
  const durationMs = record.completedAt ? record.completedAt - record.startedAt : 0;
48
48
  const totalTokens = getLifetimeTotal(record.lifetimeUsage);
49
- const contextPercent = getSessionContextPercent(record.execution?.session);
49
+ const contextPercent = getSessionContextPercent(record.session);
50
50
  const ctxXml = contextPercent !== null ? `<context_percent>${Math.round(contextPercent)}</context_percent>` : "";
51
51
  const compactXml = record.compactionCount ? `<compactions>${record.compactionCount}</compactions>` : "";
52
52
 
@@ -57,7 +57,7 @@ export function formatTaskNotification(record: AgentRecord, resultMaxLen: number
57
57
  : "No output.";
58
58
 
59
59
  const toolCallId = record.notification?.toolCallId;
60
- const outputFile = record.execution?.outputFile;
60
+ const outputFile = record.outputFile;
61
61
  return [
62
62
  "<task-notification>",
63
63
  `<task-id>${record.id}</task-id>`,
@@ -90,7 +90,7 @@ export function buildNotificationDetails(
90
90
  maxTurns: activity?.maxTurns,
91
91
  totalTokens,
92
92
  durationMs: record.completedAt ? record.completedAt - record.startedAt : 0,
93
- outputFile: record.execution?.outputFile,
93
+ outputFile: record.outputFile,
94
94
  error: record.error,
95
95
  resultPreview: record.result
96
96
  ? record.result.length > resultMaxLen
@@ -124,17 +124,6 @@ export function buildEventData(record: AgentRecord) {
124
124
 
125
125
  // ---- Notification system factory ----
126
126
 
127
- /** Narrow deps for the notification system — only the methods it actually calls. */
128
- export interface NotificationDeps {
129
- sendMessage: (
130
- msg: { customType: string; content: string; display: boolean; details?: unknown },
131
- opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
132
- ) => void;
133
- agentActivity: Map<string, AgentActivityTracker>;
134
- markFinished: (id: string) => void;
135
- updateWidget: () => void;
136
- }
137
-
138
127
  export interface NotificationSystem {
139
128
  cancelNudge: (key: string) => void;
140
129
  sendCompletion: (record: AgentRecord) => void;
@@ -147,7 +136,15 @@ const NUDGE_HOLD_MS = 200;
147
136
  export class NotificationManager implements NotificationSystem {
148
137
  private pendingNudges = new Map<string, ReturnType<typeof setTimeout>>();
149
138
 
150
- constructor(private deps: NotificationDeps) {}
139
+ constructor(
140
+ private sendMessage: (
141
+ msg: { customType: string; content: string; display: boolean; details?: unknown },
142
+ opts?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
143
+ ) => void,
144
+ private agentActivity: Map<string, AgentActivityTracker>,
145
+ private markFinished: (id: string) => void,
146
+ private updateWidget: () => void,
147
+ ) {}
151
148
 
152
149
  cancelNudge(key: string): void {
153
150
  const timer = this.pendingNudges.get(key);
@@ -158,16 +155,16 @@ export class NotificationManager implements NotificationSystem {
158
155
  }
159
156
 
160
157
  sendCompletion(record: AgentRecord): void {
161
- this.deps.agentActivity.delete(record.id);
162
- this.deps.markFinished(record.id);
158
+ this.agentActivity.delete(record.id);
159
+ this.markFinished(record.id);
163
160
  this.scheduleNudge(record.id, () => this.emitIndividualNudge(record));
164
- this.deps.updateWidget();
161
+ this.updateWidget();
165
162
  }
166
163
 
167
164
  cleanupCompleted(id: string): void {
168
- this.deps.agentActivity.delete(id);
169
- this.deps.markFinished(id);
170
- this.deps.updateWidget();
165
+ this.agentActivity.delete(id);
166
+ this.markFinished(id);
167
+ this.updateWidget();
171
168
  }
172
169
 
173
170
  dispose(): void {
@@ -194,15 +191,15 @@ export class NotificationManager implements NotificationSystem {
194
191
  if (record.notification?.resultConsumed) return;
195
192
 
196
193
  const notification = formatTaskNotification(record, 500);
197
- const outputFile = record.execution?.outputFile;
194
+ const outputFile = record.outputFile;
198
195
  const footer = outputFile ? `\nFull transcript available at: ${outputFile}` : "";
199
196
 
200
- this.deps.sendMessage(
197
+ this.sendMessage(
201
198
  {
202
199
  customType: "subagent-notification",
203
200
  content: notification + footer,
204
201
  display: true,
205
- details: buildNotificationDetails(record, 500, this.deps.agentActivity.get(record.id)),
202
+ details: buildNotificationDetails(record, 500, this.agentActivity.get(record.id)),
206
203
  },
207
204
  { deliverAs: "followUp", triggerTurn: true },
208
205
  );