@gotgenes/pi-subagents 6.18.1 → 6.18.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/docs/architecture/architecture.md +22 -22
- package/docs/plans/0166-extract-parent-session-info.md +231 -0
- package/docs/retro/0165-decompose-resolved-spawn-config.md +38 -0
- package/docs/retro/0166-extract-parent-session-info.md +39 -0
- package/package.json +1 -1
- package/src/lifecycle/agent-manager.ts +14 -10
- package/src/lifecycle/agent-runner.ts +5 -6
- package/src/tools/agent-tool.ts +4 -3
- package/src/tools/background-spawner.ts +3 -7
- package/src/tools/foreground-runner.ts +3 -5
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [6.18.2](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.18.1...pi-subagents-v6.18.2) (2026-05-24)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* plan extract ParentSessionInfo from AgentSpawnConfig ([#166](https://github.com/gotgenes/pi-packages/issues/166)) ([aff7b35](https://github.com/gotgenes/pi-packages/commit/aff7b35c98503fbb3da6a287631a2aa5c4d498fd))
|
|
14
|
+
* **retro:** add planning stage notes for issue [#166](https://github.com/gotgenes/pi-packages/issues/166) ([6138473](https://github.com/gotgenes/pi-packages/commit/613847313d56a9df8b479726d831679391bd0c1a))
|
|
15
|
+
* **retro:** add retro notes for issue [#165](https://github.com/gotgenes/pi-packages/issues/165) ([2a3e70d](https://github.com/gotgenes/pi-packages/commit/2a3e70dc6b903b4c053e7f6ebc09169bc3e34bf6))
|
|
16
|
+
* **retro:** add TDD stage notes for issue [#166](https://github.com/gotgenes/pi-packages/issues/166) ([2696da5](https://github.com/gotgenes/pi-packages/commit/2696da599de72f1a881577a4de8fedc57472a695))
|
|
17
|
+
* update architecture doc — AgentSpawnConfig step 3 complete ([125450b](https://github.com/gotgenes/pi-packages/commit/125450ba9ea5753b4cad07ed4d1675dcdbc7e319))
|
|
18
|
+
|
|
8
19
|
## [6.18.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.18.0...pi-subagents-v6.18.1) (2026-05-24)
|
|
9
20
|
|
|
10
21
|
|
|
@@ -500,20 +500,20 @@ These are fire-and-forget broadcast events — no request IDs, no reply channels
|
|
|
500
500
|
These interfaces carry hidden dependencies that obscure true coupling.
|
|
501
501
|
Bags with 10+ fields are the highest priority for decomposition.
|
|
502
502
|
|
|
503
|
-
| Interface | Fields
|
|
504
|
-
| --------------------------- |
|
|
505
|
-
| `ResolvedSpawnConfig` |
|
|
506
|
-
| `AgentSpawnConfig` | 13
|
|
507
|
-
| `RunOptions` | 12
|
|
508
|
-
| `SessionConfig` | 11
|
|
509
|
-
| `NotificationDetails` | 10
|
|
510
|
-
| `ResourceLoaderOptions` | 10
|
|
511
|
-
| `RunnerIO` | 9 methods
|
|
512
|
-
| `CreateSessionOptions` | 9
|
|
513
|
-
| `AgentToolDeps` | 8
|
|
514
|
-
| `AgentMenuDeps` | 8
|
|
515
|
-
| `ConversationViewerOptions` | 8
|
|
516
|
-
| `AgentRecordInit` | 8
|
|
503
|
+
| Interface | Fields | Consumers | Severity |
|
|
504
|
+
| --------------------------- | ---------------------------------- | ------------------------------------------------- | -------- |
|
|
505
|
+
| `ResolvedSpawnConfig` | 3 nested | foreground-runner, background-spawner, agent-tool | ✓ done |
|
|
506
|
+
| `AgentSpawnConfig` | 13 → 13 (ParentSessionInfo nested) | agent-manager (internal) | ✓ done |
|
|
507
|
+
| `RunOptions` | 12 | agent-runner | High |
|
|
508
|
+
| `SessionConfig` | 11 | agent-runner (output of assembler) | High |
|
|
509
|
+
| `NotificationDetails` | 10 | notification | Medium |
|
|
510
|
+
| `ResourceLoaderOptions` | 10 | agent-runner (SDK bridge) | Medium |
|
|
511
|
+
| `RunnerIO` | 9 methods | agent-runner | Medium |
|
|
512
|
+
| `CreateSessionOptions` | 9 | agent-runner (SDK bridge) | Medium |
|
|
513
|
+
| `AgentToolDeps` | 8 | agent-tool | Low |
|
|
514
|
+
| `AgentMenuDeps` | 8 | agent-menu | Low |
|
|
515
|
+
| `ConversationViewerOptions` | 8 | conversation-viewer | Low |
|
|
516
|
+
| `AgentRecordInit` | 8 | agent-record | Low |
|
|
517
517
|
|
|
518
518
|
### Complexity hotspots
|
|
519
519
|
|
|
@@ -586,20 +586,20 @@ interface SpawnPresentation {
|
|
|
586
586
|
`agent-tool` uses all three to build the `AgentSpawnConfig` and the result text.
|
|
587
587
|
After decomposition, each consumer declares its real dependencies explicitly.
|
|
588
588
|
|
|
589
|
-
#### AgentSpawnConfig
|
|
589
|
+
#### AgentSpawnConfig — ParentSessionInfo extracted (done, [#166][166])
|
|
590
590
|
|
|
591
|
-
|
|
591
|
+
The `parentSessionFile`, `parentSessionId`, and `toolCallId` fields were grouped into `ParentSessionInfo`:
|
|
592
592
|
|
|
593
593
|
```typescript
|
|
594
|
-
/** Parent session identity — always travel together. */
|
|
595
|
-
interface ParentSessionInfo {
|
|
594
|
+
/** Parent session identity — always travel together from the tool boundary. */
|
|
595
|
+
export interface ParentSessionInfo {
|
|
596
596
|
parentSessionFile?: string;
|
|
597
597
|
parentSessionId?: string;
|
|
598
598
|
toolCallId?: string;
|
|
599
599
|
}
|
|
600
600
|
```
|
|
601
601
|
|
|
602
|
-
|
|
602
|
+
`AgentSpawnConfig` now carries `parentSession?: ParentSessionInfo` instead of three flat optional fields.
|
|
603
603
|
|
|
604
604
|
#### RunOptions (12 fields → extract RunContext)
|
|
605
605
|
|
|
@@ -676,10 +676,10 @@ Split the 15-field bag into `SpawnIdentity`, `SpawnExecution`, and `SpawnPresent
|
|
|
676
676
|
Each consumer declares its real dependencies.
|
|
677
677
|
Enables Step 3 (narrowing AgentSpawnConfig, [#166][166]).
|
|
678
678
|
|
|
679
|
-
### Step 3: Extract ParentSessionInfo from AgentSpawnConfig ([#166][166])
|
|
679
|
+
### Step 3: Extract ParentSessionInfo from AgentSpawnConfig ([#166][166]) — Complete
|
|
680
680
|
|
|
681
|
-
|
|
682
|
-
|
|
681
|
+
Extracted `parentSessionFile`, `parentSessionId`, `toolCallId` into `ParentSessionInfo`.
|
|
682
|
+
`AgentSpawnConfig`, `BackgroundParams`, `ForegroundParams`, and `RunOptions` all carry the nested group.
|
|
683
683
|
|
|
684
684
|
### Step 4: Narrow RunnerIO ([#167][167])
|
|
685
685
|
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 166
|
|
3
|
+
issue_title: "refactor(pi-subagents): extract ParentSessionInfo from AgentSpawnConfig (13 fields)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extract ParentSessionInfo from AgentSpawnConfig
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`AgentSpawnConfig` in `agent-manager.ts` has 13 fields.
|
|
11
|
+
Three of those fields — `parentSessionFile`, `parentSessionId`, and `toolCallId` — form a natural cluster around parent session identity.
|
|
12
|
+
They always travel together through `agent-tool.ts` → `foreground-runner.ts` / `background-spawner.ts` → `AgentManager.spawn()`.
|
|
13
|
+
Extracting them into a named value object reduces `AgentSpawnConfig` from 13 to 11 fields (10 + 1 nested) and introduces the `ParentSessionInfo` domain concept.
|
|
14
|
+
|
|
15
|
+
## Goals
|
|
16
|
+
|
|
17
|
+
- Extract `ParentSessionInfo` interface with `parentSessionFile`, `parentSessionId`, and `toolCallId`.
|
|
18
|
+
- Replace the three flat optional fields on `AgentSpawnConfig` with a single optional `parentSession?: ParentSessionInfo` field.
|
|
19
|
+
- Replace the flat fields on `BackgroundParams`, `ForegroundParams`, and `RunOptions` with the same grouped type.
|
|
20
|
+
- Update all callers (agent-tool, foreground-runner, background-spawner, agent-manager, agent-runner) and their tests.
|
|
21
|
+
- Non-breaking refactor — no public API changes (the `SubagentsService` boundary does not expose these fields).
|
|
22
|
+
|
|
23
|
+
## Non-Goals
|
|
24
|
+
|
|
25
|
+
- Changing the `NotificationState` or `notification` module — they remain as-is; `toolCallId` is just extracted from the group at the `AgentManager.spawn` boundary.
|
|
26
|
+
- Further decomposition of `AgentSpawnConfig` (e.g., extracting execution or callback clusters) — tracked separately.
|
|
27
|
+
- Modifying `session-dir.ts` or `deriveSubagentSessionDir` — the function signature stays the same; callers just unwrap `parentSession.parentSessionFile` before calling it.
|
|
28
|
+
|
|
29
|
+
## Background
|
|
30
|
+
|
|
31
|
+
Issue #165 (closed) decomposed `ResolvedSpawnConfig` into `SpawnIdentity`, `SpawnExecution`, and `SpawnPresentation`.
|
|
32
|
+
This issue continues that structural improvement by grouping the parent-session fields that flow from `agent-tool.ts` through to `agent-runner.ts`.
|
|
33
|
+
|
|
34
|
+
The three fields are:
|
|
35
|
+
|
|
36
|
+
- `parentSessionFile` — path to the parent session's JSONL file, used by `deriveSubagentSessionDir` to place child sessions next to the parent.
|
|
37
|
+
- `parentSessionId` — session ID of the parent agent, stored in the child session's `parentSession` header via `sessionManager.newSession()`.
|
|
38
|
+
- `toolCallId` — tool call ID for background notification wiring; when set, `AgentManager.spawn` creates a `NotificationState`.
|
|
39
|
+
|
|
40
|
+
All three originate in `agent-tool.ts`'s `execute` function and are threaded unchanged through intermediate modules.
|
|
41
|
+
|
|
42
|
+
### Current flow
|
|
43
|
+
|
|
44
|
+
```text
|
|
45
|
+
agent-tool execute → getSessionInfo() + toolCallId param
|
|
46
|
+
→ BackgroundParams { parentSessionFile, parentSessionId, toolCallId }
|
|
47
|
+
→ spawnBackground → manager.spawn(opts: AgentSpawnConfig { ...flat fields })
|
|
48
|
+
→ AgentManager.spawn → options.toolCallId → NotificationState
|
|
49
|
+
→ startAgent → runner.run(RunOptions { parentSessionFile, parentSessionId })
|
|
50
|
+
→ deriveSessionDir(parentSessionFile, ...)
|
|
51
|
+
→ sessionManager.newSession({ parentSession: parentSessionId })
|
|
52
|
+
|
|
53
|
+
→ ForegroundParams { parentSessionFile, parentSessionId }
|
|
54
|
+
→ runForeground → manager.spawnAndWait(opts: AgentSpawnConfig { ...flat fields })
|
|
55
|
+
→ (same AgentManager path)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### After extraction
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
agent-tool execute → getSessionInfo() + toolCallId param
|
|
62
|
+
→ parentSession: ParentSessionInfo { parentSessionFile, parentSessionId, toolCallId }
|
|
63
|
+
→ BackgroundParams { parentSession }
|
|
64
|
+
→ spawnBackground → manager.spawn(opts: AgentSpawnConfig { parentSession })
|
|
65
|
+
→ AgentManager.spawn → parentSession.toolCallId → NotificationState
|
|
66
|
+
→ startAgent → runner.run(RunOptions { parentSession })
|
|
67
|
+
→ deriveSessionDir(parentSession?.parentSessionFile, ...)
|
|
68
|
+
→ sessionManager.newSession({ parentSession: parentSession?.parentSessionId })
|
|
69
|
+
|
|
70
|
+
→ ForegroundParams { parentSession }
|
|
71
|
+
→ runForeground → manager.spawnAndWait(opts: AgentSpawnConfig { parentSession })
|
|
72
|
+
→ (same AgentManager path)
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Design Overview
|
|
76
|
+
|
|
77
|
+
### `ParentSessionInfo` interface
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
export interface ParentSessionInfo {
|
|
81
|
+
/** Path to the parent session's JSONL file (for deriving the subagent session directory). */
|
|
82
|
+
parentSessionFile?: string;
|
|
83
|
+
/** Session ID of the parent agent (stored in the child session's parentSession header). */
|
|
84
|
+
parentSessionId?: string;
|
|
85
|
+
/** Tool call ID for background notification wiring. When set, spawn attaches NotificationState. */
|
|
86
|
+
toolCallId?: string;
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
All three fields remain optional — they are only available when spawning from an active session (the `SubagentsService` boundary omits them entirely).
|
|
91
|
+
|
|
92
|
+
The interface lives in `lifecycle/agent-manager.ts` alongside `AgentSpawnConfig` since that is the primary consumer.
|
|
93
|
+
If a future refactoring moves `AgentSpawnConfig` to its own file, `ParentSessionInfo` should move with it.
|
|
94
|
+
|
|
95
|
+
### Consumer call-site sketch
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
// agent-tool.ts execute():
|
|
99
|
+
const parentSession: ParentSessionInfo = {
|
|
100
|
+
parentSessionFile: sessionInfo.parentSessionFile,
|
|
101
|
+
parentSessionId: sessionInfo.parentSessionId,
|
|
102
|
+
toolCallId,
|
|
103
|
+
};
|
|
104
|
+
// ...
|
|
105
|
+
spawnBackground(manager, widget, agentActivity, { config, snapshot, parentSession });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
The grouped object eliminates the three-field spread that was repeated in both `spawnBackground` and `runForeground` call sites.
|
|
109
|
+
|
|
110
|
+
### `AgentSpawnConfig` change
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
export interface AgentSpawnConfig {
|
|
114
|
+
// ... existing fields (description, model, maxTurns, etc.)
|
|
115
|
+
/** Parent session identity — grouped fields that travel together from the tool boundary. */
|
|
116
|
+
parentSession?: ParentSessionInfo;
|
|
117
|
+
// Remove: parentSessionFile, parentSessionId, toolCallId
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### `RunOptions` change
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
export interface RunOptions {
|
|
125
|
+
// ... existing fields
|
|
126
|
+
/** Parent session identity (file path + session ID). */
|
|
127
|
+
parentSession?: ParentSessionInfo;
|
|
128
|
+
// Remove: parentSessionFile, parentSessionId
|
|
129
|
+
}
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Note: `RunOptions` does not use `toolCallId` — it was never threaded to the runner.
|
|
133
|
+
The runner only reads `parentSessionFile` and `parentSessionId` from the group.
|
|
134
|
+
|
|
135
|
+
### `getSessionInfo` return type update
|
|
136
|
+
|
|
137
|
+
The `getSessionInfo` callback in `AgentToolDeps` currently returns `{ parentSessionFile: string; parentSessionId: string }`.
|
|
138
|
+
It should remain unchanged — it does not include `toolCallId` (which comes from the `execute` callback's first argument).
|
|
139
|
+
The `agent-tool.ts` execute function constructs a `ParentSessionInfo` by merging `getSessionInfo()` output with `toolCallId`.
|
|
140
|
+
|
|
141
|
+
## Module-Level Changes
|
|
142
|
+
|
|
143
|
+
### New types
|
|
144
|
+
|
|
145
|
+
1. `src/lifecycle/agent-manager.ts` — add `ParentSessionInfo` interface (exported).
|
|
146
|
+
|
|
147
|
+
### Modified interfaces
|
|
148
|
+
|
|
149
|
+
1. `src/lifecycle/agent-manager.ts` — `AgentSpawnConfig`: replace `parentSessionFile?`, `parentSessionId?`, `toolCallId?` with `parentSession?: ParentSessionInfo`.
|
|
150
|
+
2. `src/lifecycle/agent-runner.ts` — `RunOptions`: replace `parentSessionFile?`, `parentSessionId?` with `parentSession?: ParentSessionInfo`.
|
|
151
|
+
3. `src/tools/background-spawner.ts` — `BackgroundParams`: replace `parentSessionFile`, `parentSessionId`, `toolCallId` with `parentSession: ParentSessionInfo`.
|
|
152
|
+
4. `src/tools/foreground-runner.ts` — `ForegroundParams`: replace `parentSessionFile`, `parentSessionId` with `parentSession: ParentSessionInfo`.
|
|
153
|
+
|
|
154
|
+
### Modified implementations
|
|
155
|
+
|
|
156
|
+
1. `src/lifecycle/agent-manager.ts` — `AgentManager.spawn()`: read `options.parentSession?.toolCallId` instead of `options.toolCallId`; pass `parentSession` to `RunOptions`.
|
|
157
|
+
2. `src/lifecycle/agent-runner.ts` — `runAgent()`: read `options.parentSession?.parentSessionFile` and `options.parentSession?.parentSessionId`.
|
|
158
|
+
3. `src/tools/agent-tool.ts` — `createAgentTool` execute: construct `ParentSessionInfo` from `getSessionInfo()` + `toolCallId`, pass as `parentSession` to both spawners.
|
|
159
|
+
4. `src/tools/background-spawner.ts` — `spawnBackground()`: read `params.parentSession` and pass fields to `AgentSpawnConfig`.
|
|
160
|
+
5. `src/tools/foreground-runner.ts` — `runForeground()`: read `params.parentSession` and pass fields to `AgentSpawnConfig`.
|
|
161
|
+
|
|
162
|
+
### No changes needed
|
|
163
|
+
|
|
164
|
+
- `src/tools/agent-tool.ts` — `AgentToolDeps.getSessionInfo` return type stays the same.
|
|
165
|
+
- `src/session/session-dir.ts` — `deriveSubagentSessionDir` signature unchanged.
|
|
166
|
+
- `src/observation/notification-state.ts` — constructor signature unchanged.
|
|
167
|
+
- `src/service/service-adapter.ts` — does not pass parent session fields.
|
|
168
|
+
- `src/index.ts` — `getSessionInfo` callback unchanged.
|
|
169
|
+
|
|
170
|
+
## Test Impact Analysis
|
|
171
|
+
|
|
172
|
+
### New tests enabled
|
|
173
|
+
|
|
174
|
+
No new unit tests are enabled — this is a structural grouping, not new behavior.
|
|
175
|
+
|
|
176
|
+
### Existing tests that need updates
|
|
177
|
+
|
|
178
|
+
1. `test/lifecycle/agent-manager.test.ts` — update spawn calls from flat `parentSessionFile`/`parentSessionId`/`toolCallId` to nested `parentSession: { ... }` form; update assertions to read from `parentSession`.
|
|
179
|
+
2. `test/lifecycle/agent-runner.test.ts` — update `RunOptions` construction from flat to nested `parentSession`.
|
|
180
|
+
3. `test/tools/agent-tool.test.ts` — update assertion checking `toolCallId` on spawn opts to check `parentSession.toolCallId`.
|
|
181
|
+
4. `test/tools/background-spawner.test.ts` — update `makeParams` factory from flat fields to `parentSession: { ... }`.
|
|
182
|
+
5. `test/tools/foreground-runner.test.ts` — update params construction from flat fields to `parentSession: { ... }`.
|
|
183
|
+
6. `test/helpers/make-deps.ts` — `getSessionInfo` mock stays unchanged (returns flat `{ parentSessionFile, parentSessionId }`).
|
|
184
|
+
|
|
185
|
+
### Tests that stay as-is
|
|
186
|
+
|
|
187
|
+
- `test/session/session-dir.test.ts` — tests `deriveSubagentSessionDir` directly, no interface change.
|
|
188
|
+
- `test/observation/notification-state.test.ts` — tests `NotificationState` constructor directly.
|
|
189
|
+
- `test/observation/notification.test.ts` — tests notification formatting with `record.notification`, not spawn config.
|
|
190
|
+
|
|
191
|
+
## TDD Order
|
|
192
|
+
|
|
193
|
+
1. **Define `ParentSessionInfo` and update `AgentSpawnConfig`** — add interface, replace three flat fields with `parentSession?`.
|
|
194
|
+
Update `AgentManager.spawn` and `startAgent` to read from the nested group.
|
|
195
|
+
Update `agent-manager.test.ts` to use nested form.
|
|
196
|
+
Run `pnpm run check` to verify no downstream type errors remain.
|
|
197
|
+
Commit: `refactor: define ParentSessionInfo and nest in AgentSpawnConfig`
|
|
198
|
+
|
|
199
|
+
2. **Update `RunOptions` in `agent-runner.ts`** — replace flat `parentSessionFile?`/`parentSessionId?` with `parentSession?`.
|
|
200
|
+
Update `runAgent` to read `options.parentSession?.parentSessionFile` and `options.parentSession?.parentSessionId`.
|
|
201
|
+
Update `agent-runner.test.ts`.
|
|
202
|
+
Commit: `refactor: nest ParentSessionInfo in RunOptions`
|
|
203
|
+
|
|
204
|
+
3. **Update `BackgroundParams` and `spawnBackground`** — replace three flat fields with `parentSession: ParentSessionInfo`.
|
|
205
|
+
Update `spawnBackground` to pass `parentSession` to spawn opts.
|
|
206
|
+
Update `background-spawner.test.ts`.
|
|
207
|
+
Commit: `refactor: nest ParentSessionInfo in BackgroundParams`
|
|
208
|
+
|
|
209
|
+
4. **Update `ForegroundParams` and `runForeground`** — replace two flat fields with `parentSession: ParentSessionInfo`.
|
|
210
|
+
Update `runForeground` to pass `parentSession` to spawn opts.
|
|
211
|
+
Update `foreground-runner.test.ts`.
|
|
212
|
+
Commit: `refactor: nest ParentSessionInfo in ForegroundParams`
|
|
213
|
+
|
|
214
|
+
5. **Update `agent-tool.ts` execute** — construct `ParentSessionInfo` from `getSessionInfo()` + `toolCallId`, pass as `parentSession` to both spawner call sites.
|
|
215
|
+
Update `agent-tool.test.ts`.
|
|
216
|
+
Commit: `refactor: construct ParentSessionInfo in agent-tool execute`
|
|
217
|
+
|
|
218
|
+
6. **Final verification** — run full test suite (`pnpm vitest run`) and type check (`pnpm run check`).
|
|
219
|
+
No separate commit unless adjustments are needed.
|
|
220
|
+
|
|
221
|
+
## Risks and Mitigations
|
|
222
|
+
|
|
223
|
+
| Risk | Mitigation |
|
|
224
|
+
| ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
|
|
225
|
+
| Deep-merge trap: test factories using `Partial<BackgroundParams>` spread may silently ignore nested overrides | Audit `makeParams` in `background-spawner.test.ts` — convert from flat-field spread to nested `parentSession` construction |
|
|
226
|
+
| `toolCallId` conditionally absent for foreground calls | `ParentSessionInfo.toolCallId` is optional; `ForegroundParams.parentSession` includes it but won't set it — matches current behavior |
|
|
227
|
+
| Type check passes but runtime breaks due to nested access on undefined | `parentSession?` is optional on `AgentSpawnConfig`; all reads use optional chaining (`options.parentSession?.toolCallId`) |
|
|
228
|
+
|
|
229
|
+
## Open Questions
|
|
230
|
+
|
|
231
|
+
None — the extraction is mechanical and the issue description is unambiguous.
|
|
@@ -38,3 +38,41 @@ The decomposition touched 7 files (4 source, 3 test) and kept the test count fla
|
|
|
38
38
|
- Step 1 breaking all consumers simultaneously was handled smoothly by completing all steps before pushing, as planned.
|
|
39
39
|
No transitional alias was needed.
|
|
40
40
|
- The `background-spawner.test.ts` description-override test was the only unexpected friction point — the flat spread issue wasn't caught by the plan.
|
|
41
|
+
|
|
42
|
+
## Stage: Final Retrospective (2026-05-24T15:00:14Z)
|
|
43
|
+
|
|
44
|
+
### Session summary
|
|
45
|
+
|
|
46
|
+
Shipped issue #165 (CI green, released as `pi-subagents-v6.18.1`) and ran the final retrospective.
|
|
47
|
+
The most impactful outcome across all three sessions was the skill description improvements (commit `51f52ef`), which addressed a recurring `instruction-violation` pattern.
|
|
48
|
+
|
|
49
|
+
### Observations
|
|
50
|
+
|
|
51
|
+
#### What went well
|
|
52
|
+
|
|
53
|
+
- The user's probing question ("This is consistent, though.
|
|
54
|
+
Why?") turned a simple skill-loading skip into a generalizable improvement to three skill descriptions and two prompt instructions.
|
|
55
|
+
This is a good example of the user investing a redirecting question instead of a correction.
|
|
56
|
+
- TDD execution was clean — 4 cycles, no rework, no type errors at the end.
|
|
57
|
+
The plan's risk mitigation ("land steps 1–4 on the same branch") worked as intended.
|
|
58
|
+
- Ship stage had zero friction: push, CI, close, release-please merge, tag — all first-try.
|
|
59
|
+
|
|
60
|
+
#### What caused friction (agent side)
|
|
61
|
+
|
|
62
|
+
- `instruction-violation` — Skipped loading the `colgrep` skill during planning despite explicit instructions.
|
|
63
|
+
Root cause: skill descriptions that read like tool reference manuals get deprioritized because the agent perceives them as redundant with the tool schema already in context.
|
|
64
|
+
Impact: no rework on the plan itself, but triggered a productive detour to improve skill descriptions.
|
|
65
|
+
User-caught.
|
|
66
|
+
- `missing-context` — The plan didn't anticipate that `Partial<ResolvedSpawnConfig>` spread in test factories would silently break after nesting.
|
|
67
|
+
The `testing` skill already warns about spread-related pitfalls, but not this specific variant (flat keys ignored by top-level spread on a nested structure).
|
|
68
|
+
Impact: one test failure during step 4 that required a verbose inline fix (writing out the full `execution` sub-object).
|
|
69
|
+
Self-identified during implementation.
|
|
70
|
+
|
|
71
|
+
#### What caused friction (user side)
|
|
72
|
+
|
|
73
|
+
- None observed.
|
|
74
|
+
The user's intervention on the `colgrep` skill was well-timed and produced a higher-value outcome than skipping it would have.
|
|
75
|
+
|
|
76
|
+
### Changes made
|
|
77
|
+
|
|
78
|
+
1. Added a TDD planning rule to `.pi/skills/testing/SKILL.md` warning about `Partial<T>` spread not deep-merging into nested interfaces after a flat-to-nested refactor.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 166
|
|
3
|
+
issue_title: "refactor(pi-subagents): extract ParentSessionInfo from AgentSpawnConfig (13 fields)"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #166 — Extract ParentSessionInfo from AgentSpawnConfig
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-24T16:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Produced a 6-step TDD plan to extract `ParentSessionInfo` from `AgentSpawnConfig`.
|
|
13
|
+
The refactoring groups three co-traveling fields (`parentSessionFile`, `parentSessionId`, `toolCallId`) into a named value object, reducing `AgentSpawnConfig` from 13 to 11 fields.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- The `SubagentsService` boundary (`service-adapter.ts`) does not pass any of the three fields, so this is a purely internal refactoring with no public API impact.
|
|
18
|
+
- `getSessionInfo` in `AgentToolDeps` returns only `parentSessionFile` and `parentSessionId`; `toolCallId` comes from the `execute` callback's first argument — the plan keeps this separation and merges them at the `agent-tool.ts` boundary.
|
|
19
|
+
- `RunOptions` in `agent-runner.ts` never carried `toolCallId` (it was consumed in `AgentManager.spawn` before reaching the runner), so the nested `parentSession` on `RunOptions` only holds the two session fields.
|
|
20
|
+
- The deep-merge trap from the testing skill is relevant: `background-spawner.test.ts` has a `makeParams` factory that spreads flat fields — must be converted to nested `parentSession` construction.
|
|
21
|
+
- Issue #165 (decompose `ResolvedSpawnConfig`) is closed, so this plan builds on stable ground.
|
|
22
|
+
|
|
23
|
+
## Stage: Implementation — TDD (2026-05-24T17:00:00Z)
|
|
24
|
+
|
|
25
|
+
### Session summary
|
|
26
|
+
|
|
27
|
+
All 5 TDD cycles completed across `agent-manager.ts`, `agent-runner.ts`, `background-spawner.ts`, `foreground-runner.ts`, and `agent-tool.ts`.
|
|
28
|
+
Test count held steady at 805 (no net new tests — refactor only).
|
|
29
|
+
Type check and lint both clean after all steps.
|
|
30
|
+
|
|
31
|
+
### Observations
|
|
32
|
+
|
|
33
|
+
- The `AgentSpawnConfig` field count went from 15 to 13 (not 13 → 10 as originally estimated) — the architecture doc quoted the issue's stale count; the actual pre-refactor interface had 15 fields (`bypassQueue` and others were already present).
|
|
34
|
+
The architecture doc was updated to reflect "done" with a note about the nested group rather than a specific before/after number.
|
|
35
|
+
- The deep-merge trap (noted in planning) did materialise: `background-spawner.test.ts`'s `makeParams` spread `Partial<BackgroundParams>` with flat fields.
|
|
36
|
+
Fixed by replacing the three flat fields with a single `parentSession` object at the factory level — top-level spread still works correctly since `parentSession` is one field.
|
|
37
|
+
- `RunOptions` in `agent-runner.ts` needed a new import of `ParentSessionInfo` from `agent-manager.ts`; no circular dependency since `agent-runner.ts` already imports from `agent-manager.ts`.
|
|
38
|
+
- `agent-tool.ts` still imports `AgentSpawnConfig` (needed by `AgentToolManager` interface) — the new `ParentSessionInfo` import was added alongside it.
|
|
39
|
+
- All 5 commits are clean `refactor:` messages; architecture doc update is a separate `docs:` commit.
|
package/package.json
CHANGED
|
@@ -53,6 +53,15 @@ interface SpawnArgs {
|
|
|
53
53
|
options: AgentSpawnConfig;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export interface ParentSessionInfo {
|
|
57
|
+
/** Path to the parent session's JSONL file (for deriving the subagent session directory). */
|
|
58
|
+
parentSessionFile?: string;
|
|
59
|
+
/** Session ID of the parent agent (stored in the child session's parentSession header). */
|
|
60
|
+
parentSessionId?: string;
|
|
61
|
+
/** Tool call ID for background notification wiring. When set, spawn attaches NotificationState. */
|
|
62
|
+
toolCallId?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
56
65
|
export interface AgentSpawnConfig {
|
|
57
66
|
description: string;
|
|
58
67
|
model?: Model<any>;
|
|
@@ -75,12 +84,8 @@ export interface AgentSpawnConfig {
|
|
|
75
84
|
signal?: AbortSignal;
|
|
76
85
|
/** Called when the agent session is created — receives the session and the agent's record. */
|
|
77
86
|
onSessionCreated?: (session: AgentSession, record: AgentRecord) => void;
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
/** Session ID of the parent agent (stored in the child session's parentSession header). */
|
|
81
|
-
parentSessionId?: string;
|
|
82
|
-
/** Tool call ID for background notification wiring. When set, spawn attaches NotificationState. */
|
|
83
|
-
toolCallId?: string;
|
|
87
|
+
/** Parent session identity — grouped fields that travel together from the tool boundary. */
|
|
88
|
+
parentSession?: ParentSessionInfo;
|
|
84
89
|
}
|
|
85
90
|
|
|
86
91
|
export class AgentManager {
|
|
@@ -158,8 +163,8 @@ export class AgentManager {
|
|
|
158
163
|
});
|
|
159
164
|
this.agents.set(id, record);
|
|
160
165
|
|
|
161
|
-
if (options.toolCallId) {
|
|
162
|
-
record.notification = new NotificationState(options.toolCallId);
|
|
166
|
+
if (options.parentSession?.toolCallId) {
|
|
167
|
+
record.notification = new NotificationState(options.parentSession.toolCallId);
|
|
163
168
|
}
|
|
164
169
|
|
|
165
170
|
if (options.isBackground) {
|
|
@@ -228,8 +233,7 @@ export class AgentManager {
|
|
|
228
233
|
isolated: options.isolated,
|
|
229
234
|
thinkingLevel: options.thinkingLevel,
|
|
230
235
|
cwd: worktreeCwd,
|
|
231
|
-
|
|
232
|
-
parentSessionId: options.parentSessionId,
|
|
236
|
+
parentSession: options.parentSession,
|
|
233
237
|
signal: record.abortController!.signal,
|
|
234
238
|
registry: this.registry,
|
|
235
239
|
onSessionCreated: (session) => {
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
type SettingsManager,
|
|
10
10
|
} from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import type { AgentConfigLookup } from "#src/config/agent-types";
|
|
12
|
+
import type { ParentSessionInfo } from "#src/lifecycle/agent-manager";
|
|
12
13
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
13
14
|
import { extractText } from "#src/session/context";
|
|
14
15
|
import type { EnvInfo } from "#src/session/env";
|
|
@@ -146,10 +147,8 @@ export interface RunOptions {
|
|
|
146
147
|
thinkingLevel?: ThinkingLevel;
|
|
147
148
|
/** Override working directory (e.g. for worktree isolation). */
|
|
148
149
|
cwd?: string;
|
|
149
|
-
/**
|
|
150
|
-
|
|
151
|
-
/** Session ID of the parent agent (stored in the child session's parentSession header). */
|
|
152
|
-
parentSessionId?: string;
|
|
150
|
+
/** Parent session identity (file path + session ID). */
|
|
151
|
+
parentSession?: ParentSessionInfo;
|
|
153
152
|
/** Called once after session creation — session delivery mechanism. */
|
|
154
153
|
onSessionCreated?: (session: AgentSession) => void;
|
|
155
154
|
/**
|
|
@@ -308,9 +307,9 @@ export async function runAgent(
|
|
|
308
307
|
// Create a persisted SessionManager so transcripts are written in Pi's
|
|
309
308
|
// official JSONL format. Falls back to a temp directory when the parent
|
|
310
309
|
// session is not persisted (e.g. headless/API mode).
|
|
311
|
-
const sessionDir = io.deriveSessionDir(options.parentSessionFile, cfg.effectiveCwd);
|
|
310
|
+
const sessionDir = io.deriveSessionDir(options.parentSession?.parentSessionFile, cfg.effectiveCwd);
|
|
312
311
|
const sessionManager = io.createSessionManager(cfg.effectiveCwd, sessionDir);
|
|
313
|
-
sessionManager.newSession({ parentSession: options.parentSessionId });
|
|
312
|
+
sessionManager.newSession({ parentSession: options.parentSession?.parentSessionId });
|
|
314
313
|
|
|
315
314
|
const { session } = await io.createSession({
|
|
316
315
|
cwd: cfg.effectiveCwd,
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
|
3
3
|
import { Text } from "@earendil-works/pi-tui";
|
|
4
4
|
import { Type } from "@sinclair/typebox";
|
|
5
5
|
import { AgentTypeRegistry } from "#src/config/agent-types";
|
|
6
|
-
import type { AgentSpawnConfig } from "#src/lifecycle/agent-manager";
|
|
6
|
+
import type { AgentSpawnConfig, ParentSessionInfo } from "#src/lifecycle/agent-manager";
|
|
7
7
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
8
8
|
import { spawnBackground } from "#src/tools/background-spawner";
|
|
9
9
|
import { runForeground } from "#src/tools/foreground-runner";
|
|
@@ -303,6 +303,7 @@ Guidelines:
|
|
|
303
303
|
// ---- Boundary extraction (after config so inheritContext is resolved) ----
|
|
304
304
|
const snapshot = buildSnapshot(config.execution.inheritContext);
|
|
305
305
|
const { parentSessionFile, parentSessionId } = getSessionInfo();
|
|
306
|
+
const parentSession: ParentSessionInfo = { parentSessionFile, parentSessionId, toolCallId };
|
|
306
307
|
|
|
307
308
|
// ---- Resume existing agent ----
|
|
308
309
|
if (params.resume) {
|
|
@@ -337,7 +338,7 @@ Guidelines:
|
|
|
337
338
|
manager,
|
|
338
339
|
widget,
|
|
339
340
|
agentActivity,
|
|
340
|
-
{ config, snapshot,
|
|
341
|
+
{ config, snapshot, parentSession },
|
|
341
342
|
);
|
|
342
343
|
}
|
|
343
344
|
|
|
@@ -346,7 +347,7 @@ Guidelines:
|
|
|
346
347
|
manager,
|
|
347
348
|
widget,
|
|
348
349
|
agentActivity,
|
|
349
|
-
{ config, snapshot,
|
|
350
|
+
{ config, snapshot, parentSession },
|
|
350
351
|
signal,
|
|
351
352
|
onUpdate,
|
|
352
353
|
);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentSpawnConfig } from "#src/lifecycle/agent-manager";
|
|
1
|
+
import type { AgentSpawnConfig, ParentSessionInfo } from "#src/lifecycle/agent-manager";
|
|
2
2
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
3
3
|
import type { AgentActivityAccess } from "#src/tools/agent-tool";
|
|
4
4
|
import { textResult } from "#src/tools/helpers";
|
|
@@ -24,9 +24,7 @@ export interface BackgroundWidgetDeps {
|
|
|
24
24
|
export interface BackgroundParams {
|
|
25
25
|
config: ResolvedSpawnConfig;
|
|
26
26
|
snapshot: ParentSnapshot;
|
|
27
|
-
|
|
28
|
-
parentSessionId: string;
|
|
29
|
-
toolCallId: string;
|
|
27
|
+
parentSession: ParentSessionInfo;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
30
|
/**
|
|
@@ -46,8 +44,7 @@ export function spawnBackground(
|
|
|
46
44
|
let id: string;
|
|
47
45
|
try {
|
|
48
46
|
id = manager.spawn(params.snapshot, identity.subagentType, execution.prompt, {
|
|
49
|
-
|
|
50
|
-
parentSessionId: params.parentSessionId,
|
|
47
|
+
parentSession: params.parentSession,
|
|
51
48
|
description: execution.description,
|
|
52
49
|
model: execution.model,
|
|
53
50
|
maxTurns: execution.effectiveMaxTurns,
|
|
@@ -57,7 +54,6 @@ export function spawnBackground(
|
|
|
57
54
|
isBackground: true,
|
|
58
55
|
isolation: execution.isolation,
|
|
59
56
|
invocation: execution.agentInvocation,
|
|
60
|
-
toolCallId: params.toolCallId,
|
|
61
57
|
onSessionCreated: (session) => {
|
|
62
58
|
bgState.setSession(session);
|
|
63
59
|
subscribeUIObserver(session, bgState);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type { AgentSpawnConfig } from "#src/lifecycle/agent-manager";
|
|
2
|
+
import type { AgentSpawnConfig, ParentSessionInfo } from "#src/lifecycle/agent-manager";
|
|
3
3
|
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
4
4
|
import type { AgentActivityAccess } from "#src/tools/agent-tool";
|
|
5
5
|
import {
|
|
@@ -39,8 +39,7 @@ export interface ForegroundWidgetDeps {
|
|
|
39
39
|
export interface ForegroundParams {
|
|
40
40
|
config: ResolvedSpawnConfig;
|
|
41
41
|
snapshot: ParentSnapshot;
|
|
42
|
-
|
|
43
|
-
parentSessionId: string;
|
|
42
|
+
parentSession: ParentSessionInfo;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
/**
|
|
@@ -109,8 +108,7 @@ export async function runForeground(
|
|
|
109
108
|
isolation: execution.isolation,
|
|
110
109
|
invocation: execution.agentInvocation,
|
|
111
110
|
signal,
|
|
112
|
-
|
|
113
|
-
parentSessionId: params.parentSessionId,
|
|
111
|
+
parentSession: params.parentSession,
|
|
114
112
|
onSessionCreated: (session, record) => {
|
|
115
113
|
fgState.setSession(session);
|
|
116
114
|
recordRef = record;
|