@gotgenes/pi-subagents 6.18.0 → 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 CHANGED
@@ -5,6 +5,26 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [6.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
+
19
+ ## [6.18.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.18.0...pi-subagents-v6.18.1) (2026-05-24)
20
+
21
+
22
+ ### Documentation
23
+
24
+ * plan decompose ResolvedSpawnConfig ([#165](https://github.com/gotgenes/pi-packages/issues/165)) ([1b14d56](https://github.com/gotgenes/pi-packages/commit/1b14d56aaada427a7344ec22adb4bbf8d7ce0bf7))
25
+ * **retro:** add planning stage notes for issue [#165](https://github.com/gotgenes/pi-packages/issues/165) ([8e0476a](https://github.com/gotgenes/pi-packages/commit/8e0476afa991953884455c7dd09c7ffb742cb329))
26
+ * **retro:** add TDD stage notes for issue [#165](https://github.com/gotgenes/pi-packages/issues/165) ([68248e5](https://github.com/gotgenes/pi-packages/commit/68248e572d38ad6e0cdb61bdd22f2b46193eaac6))
27
+
8
28
  ## [6.18.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.17.2...pi-subagents-v6.18.0) (2026-05-24)
9
29
 
10
30
 
@@ -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 | Consumers | Severity |
504
- | --------------------------- | --------- | ------------------------------------------------- | -------- |
505
- | `ResolvedSpawnConfig` | 15 | foreground-runner, background-spawner, agent-tool | Critical |
506
- | `AgentSpawnConfig` | 13 | agent-manager (internal) | Critical |
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 |
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 (13 fields extract ParentSessionInfo)
589
+ #### AgentSpawnConfig ParentSessionInfo extracted (done, [#166][166])
590
590
 
591
- Several fields form a natural cluster around parent session identity:
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
- Extracting this from `AgentSpawnConfig` reduces it from 13 to 10 fields and introduces a named concept that currently exists only as scattered optional fields.
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
- Extract `parentSessionFile`, `parentSessionId`, `toolCallId` into a `ParentSessionInfo` value object.
682
- Reduces AgentSpawnConfig from 13 to 10 fields.
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,157 @@
1
+ ---
2
+ issue: 165
3
+ issue_title: "refactor(pi-subagents): decompose ResolvedSpawnConfig (15 fields)"
4
+ ---
5
+
6
+ # Decompose ResolvedSpawnConfig into domain-aligned sub-interfaces
7
+
8
+ ## Problem Statement
9
+
10
+ `ResolvedSpawnConfig` in `tools/spawn-config.ts` has 15 fields mixing identity, execution, and presentation concerns.
11
+ Each consumer uses a different subset but receives the full bag — violating ISP and making the real dependencies of `foreground-runner` and `background-spawner` invisible.
12
+
13
+ ## Goals
14
+
15
+ - Split `ResolvedSpawnConfig` into three focused interfaces: `SpawnIdentity`, `SpawnExecution`, `SpawnPresentation`.
16
+ - Each consumer declares its real dependencies explicitly.
17
+ - Preserve existing behavior — pure structural refactor with no behavioral changes.
18
+
19
+ ## Non-Goals
20
+
21
+ - Extracting `ParentSessionInfo` from `AgentSpawnConfig` — that's #166.
22
+ - Changing how `resolveSpawnConfig` computes values internally.
23
+ - Modifying the `AgentSpawnConfig` interface passed to `AgentManager`.
24
+
25
+ ## Background
26
+
27
+ `resolveSpawnConfig` is called by `agent-tool.ts` and produces a single flat config object.
28
+ Three consumers read from it:
29
+
30
+ 1. `agent-tool.ts` — reads `inheritContext` (to build snapshot), `runInBackground` (to branch), and `detailBase` (for resume result).
31
+ 2. `foreground-runner.ts` — reads identity fields for fallback messages, execution fields for spawn options, and `detailBase` for result formatting.
32
+ 3. `background-spawner.ts` — reads identity fields for launch message, execution fields for spawn options, and `detailBase` for result formatting.
33
+
34
+ Two fields (`modelName`, `agentTags`) are never accessed by any external consumer — they're intermediate values used only inside `resolveSpawnConfig` to build `detailBase`.
35
+ They belong on `SpawnPresentation` for transparency but could also be made internal-only.
36
+
37
+ The `code-design` skill's ISP and dependency-width guidance both apply: clients should not depend on properties they don't use, and a shared bag where each consumer only touches a subset hides real dependencies.
38
+
39
+ ## Design Overview
40
+
41
+ ### New interfaces
42
+
43
+ ```typescript
44
+ /** Identity: who is being spawned. */
45
+ export interface SpawnIdentity {
46
+ subagentType: string;
47
+ rawType: SubagentType;
48
+ fellBack: boolean;
49
+ displayName: string;
50
+ }
51
+
52
+ /** Execution: how the agent will run. */
53
+ export interface SpawnExecution {
54
+ prompt: string;
55
+ description: string;
56
+ model: Model<any> | undefined;
57
+ effectiveMaxTurns: number | undefined;
58
+ thinking: ThinkingLevel | undefined;
59
+ inheritContext: boolean;
60
+ runInBackground: boolean;
61
+ isolated: boolean;
62
+ isolation: IsolationMode | undefined;
63
+ agentInvocation: AgentInvocation;
64
+ }
65
+
66
+ /** Presentation: display/UI values derived from identity + execution. */
67
+ export interface SpawnPresentation {
68
+ modelName: string | undefined;
69
+ agentTags: string[];
70
+ detailBase: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">;
71
+ }
72
+
73
+ /** Fully resolved config — now a composition of domain-aligned sub-interfaces. */
74
+ export interface ResolvedSpawnConfig {
75
+ identity: SpawnIdentity;
76
+ execution: SpawnExecution;
77
+ presentation: SpawnPresentation;
78
+ }
79
+ ```
80
+
81
+ ### Consumer interaction pattern
82
+
83
+ ```typescript
84
+ // agent-tool.ts — uses execution for routing, presentation for resume
85
+ const config = resolveSpawnConfig(params, registry, modelInfo, settings);
86
+ if ("error" in config) return textResult(config.error);
87
+ const snapshot = buildSnapshot(config.execution.inheritContext);
88
+ if (config.execution.runInBackground) { /* ... */ }
89
+ return buildDetails(config.presentation.detailBase, record);
90
+
91
+ // foreground-runner.ts — destructures what it needs
92
+ const { identity, execution, presentation } = params.config;
93
+ record = await manager.spawnAndWait(snapshot, identity.subagentType, execution.prompt, { ... });
94
+ const fallbackNote = identity.fellBack ? `Note: Unknown agent type "${identity.rawType}"...` : "";
95
+ ```
96
+
97
+ This follows Tell-Don't-Ask — callers pick the sub-object relevant to their concern rather than reaching through a flat bag.
98
+ The one-level nesting (`config.execution.inheritContext`) is acceptable because it names the domain the field belongs to.
99
+
100
+ ### Test factory migration
101
+
102
+ Both test files (`foreground-runner.test.ts`, `background-spawner.test.ts`) have `makeConfig()` factories that construct the full 15-field flat object.
103
+ These will be updated to construct the nested structure.
104
+ The `spawn-config.test.ts` assertions will shift from `result.subagentType` to `result.identity.subagentType` etc.
105
+
106
+ ## Module-Level Changes
107
+
108
+ | File | Change |
109
+ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
110
+ | `src/tools/spawn-config.ts` | Add `SpawnIdentity`, `SpawnExecution`, `SpawnPresentation` interfaces. Change `ResolvedSpawnConfig` to nest them. Update `resolveSpawnConfig` return to build nested structure. |
111
+ | `src/tools/agent-tool.ts` | Update config access: `config.execution.inheritContext`, `config.execution.runInBackground`, `config.presentation.detailBase`. |
112
+ | `src/tools/foreground-runner.ts` | Destructure `config` into `identity`, `execution`, `presentation`. Update all field accesses. |
113
+ | `src/tools/background-spawner.ts` | Destructure `config` into `identity`, `execution`, `presentation`. Update all field accesses. |
114
+ | `test/tools/spawn-config.test.ts` | Update all assertions to use nested paths (`result.identity.subagentType`, etc.). |
115
+ | `test/tools/foreground-runner.test.ts` | Update `makeConfig()` factory to build nested structure. |
116
+ | `test/tools/background-spawner.test.ts` | Update `makeConfig()` factory to build nested structure. |
117
+
118
+ ## Test Impact Analysis
119
+
120
+ 1. No new unit tests are needed — this is a structural refactor, not new behavior.
121
+ 2. No existing tests become redundant — all current `spawn-config.test.ts` assertions still verify the same computation.
122
+ 3. All existing tests must be updated to match the new nested access paths, but they continue to exercise the same logic.
123
+
124
+ ## TDD Order
125
+
126
+ 1. **Red→Green: introduce sub-interfaces and nest `ResolvedSpawnConfig`** — change `spawn-config.ts` to export the three sub-interfaces and restructure `ResolvedSpawnConfig`.
127
+ Update `resolveSpawnConfig` to return the nested shape.
128
+ Update `spawn-config.test.ts` assertions to match.
129
+ Commit: `refactor(pi-subagents): introduce SpawnIdentity, SpawnExecution, SpawnPresentation`
130
+
131
+ 2. **Green: update `agent-tool.ts`** — migrate the three field accesses (`inheritContext`, `runInBackground`, `detailBase`) to nested paths.
132
+ Run `pnpm run check` to confirm types pass.
133
+ Commit: `refactor(pi-subagents): update agent-tool to use nested spawn config`
134
+
135
+ 3. **Green: update `foreground-runner.ts` and its test** — destructure config and update all field accesses.
136
+ Update `makeConfig()` factory in `foreground-runner.test.ts`.
137
+ Commit: `refactor(pi-subagents): update foreground-runner to use nested spawn config`
138
+
139
+ 4. **Green: update `background-spawner.ts` and its test** — destructure config and update all field accesses.
140
+ Update `makeConfig()` factory in `background-spawner.test.ts`.
141
+ Commit: `refactor(pi-subagents): update background-spawner to use nested spawn config`
142
+
143
+ 5. **Verify: full suite** — run `pnpm vitest run` and `pnpm run check` to confirm no regressions.
144
+ Commit (if any lint/type cleanup needed): `chore(pi-subagents): post-decomposition cleanup`
145
+
146
+ ## Risks and Mitigations
147
+
148
+ | Risk | Mitigation |
149
+ | -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
150
+ | Step 1 breaks type checking for all consumers simultaneously | Steps 2–4 must land in the same branch before pushing; or use a transitional type alias that spreads the sub-interfaces flat, removing it in the final step. |
151
+ | Test factories diverge from production shape | Each step updates the test factory in the same commit as the source change. |
152
+ | `modelName` and `agentTags` on `SpawnPresentation` look unused | They are unused by external consumers today but provide inspection affordance for debugging/logging. Keep them; #166 or later work may consume them. |
153
+
154
+ ## Open Questions
155
+
156
+ - None — the issue's proposed shape aligns with actual field usage patterns.
157
+ The only observation is that `modelName` and `agentTags` are only consumed internally, but exposing them on `SpawnPresentation` is harmless and aids debuggability.
@@ -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.
@@ -0,0 +1,78 @@
1
+ ---
2
+ issue: 165
3
+ issue_title: "refactor(pi-subagents): decompose ResolvedSpawnConfig (15 fields)"
4
+ ---
5
+
6
+ # Retro: #165 — decompose ResolvedSpawnConfig (15 fields)
7
+
8
+ ## Stage: Planning (2026-05-24T13:41:41Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 5-step TDD plan to decompose the 15-field `ResolvedSpawnConfig` into three nested sub-interfaces (`SpawnIdentity`, `SpawnExecution`, `SpawnPresentation`).
13
+ Also improved skill descriptions for `colgrep` and `markdown-conventions` to signal decision-relevant content rather than tool reference material.
14
+
15
+ ### Observations
16
+
17
+ - The proposed decomposition in the issue aligns well with actual field usage patterns — no adjustments needed.
18
+ - `modelName` and `agentTags` are never accessed by external consumers; they're intermediate computation exposed on the return type.
19
+ Keeping them on `SpawnPresentation` is harmless and aids debuggability.
20
+ - Step 1 (interface change + return restructure) will break type checking for all consumers simultaneously.
21
+ The plan addresses this by landing steps 1–4 in rapid succession on the same branch.
22
+ - Both test files have `makeConfig()` factories that must be updated in lock-step with their respective source files.
23
+ - Issue #164 (directory reorganization) is closed, so import paths are already in their final `#src/<domain>/` form.
24
+
25
+ ## Stage: Implementation — TDD (2026-05-24T14:32:58Z)
26
+
27
+ ### Session summary
28
+
29
+ Completed all 4 TDD cycles plus full-suite verification in one session.
30
+ The decomposition touched 7 files (4 source, 3 test) and kept the test count flat at 805 — no new tests needed for a pure structural refactor.
31
+
32
+ ### Observations
33
+
34
+ - The `Partial<ResolvedSpawnConfig>` spread pattern in `makeConfig` factories doesn't deep-merge into nested sub-objects.
35
+ Two tests (`foreground-runner.test.ts` and `background-spawner.test.ts`) used flat field overrides (`{ fellBack: true }`, `{ description: "my task" }`) that silently stopped working after nesting.
36
+ Fixed by writing out the full nested sub-object at the override call site.
37
+ Future factories for nested config types should either deep-merge or avoid the `Partial<T>` spread pattern — see the `testing` skill's warning about this.
38
+ - Step 1 breaking all consumers simultaneously was handled smoothly by completing all steps before pushing, as planned.
39
+ No transitional alias was needed.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "6.18.0",
3
+ "version": "6.18.2",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
@@ -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
- /** Path to the parent session's JSONL file (for deriving the subagent session directory). */
79
- parentSessionFile?: string;
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
- parentSessionFile: options.parentSessionFile,
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
- /** Path to the parent session's JSONL file (for deriving the subagent session directory). */
150
- parentSessionFile?: string;
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,
@@ -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";
@@ -301,8 +301,9 @@ Guidelines:
301
301
  if ("error" in config) return textResult(config.error);
302
302
 
303
303
  // ---- Boundary extraction (after config so inheritContext is resolved) ----
304
- const snapshot = buildSnapshot(config.inheritContext);
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) {
@@ -327,17 +328,17 @@ Guidelines:
327
328
  }
328
329
  return textResult(
329
330
  record.result?.trim() ?? record.error?.trim() ?? "No output.",
330
- buildDetails(config.detailBase, record),
331
+ buildDetails(config.presentation.detailBase, record),
331
332
  );
332
333
  }
333
334
 
334
335
  // ---- Background execution ----
335
- if (config.runInBackground) {
336
+ if (config.execution.runInBackground) {
336
337
  return spawnBackground(
337
338
  manager,
338
339
  widget,
339
340
  agentActivity,
340
- { config, snapshot, parentSessionFile, parentSessionId, toolCallId },
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, parentSessionFile, parentSessionId },
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
- parentSessionFile: string;
28
- parentSessionId: string;
29
- toolCallId: string;
27
+ parentSession: ParentSessionInfo;
30
28
  }
31
29
 
32
30
  /**
@@ -40,24 +38,22 @@ export function spawnBackground(
40
38
  agentActivity: AgentActivityAccess,
41
39
  params: BackgroundParams,
42
40
  ) {
43
- const { config } = params;
44
- const bgState = new AgentActivityTracker(config.effectiveMaxTurns);
41
+ const { identity, execution, presentation } = params.config;
42
+ const bgState = new AgentActivityTracker(execution.effectiveMaxTurns);
45
43
 
46
44
  let id: string;
47
45
  try {
48
- id = manager.spawn(params.snapshot, config.subagentType, config.prompt, {
49
- parentSessionFile: params.parentSessionFile,
50
- parentSessionId: params.parentSessionId,
51
- description: config.description,
52
- model: config.model,
53
- maxTurns: config.effectiveMaxTurns,
54
- isolated: config.isolated,
55
- inheritContext: config.inheritContext,
56
- thinkingLevel: config.thinking,
46
+ id = manager.spawn(params.snapshot, identity.subagentType, execution.prompt, {
47
+ parentSession: params.parentSession,
48
+ description: execution.description,
49
+ model: execution.model,
50
+ maxTurns: execution.effectiveMaxTurns,
51
+ isolated: execution.isolated,
52
+ inheritContext: execution.inheritContext,
53
+ thinkingLevel: execution.thinking,
57
54
  isBackground: true,
58
- isolation: config.isolation,
59
- invocation: config.agentInvocation,
60
- toolCallId: params.toolCallId,
55
+ isolation: execution.isolation,
56
+ invocation: execution.agentInvocation,
61
57
  onSessionCreated: (session) => {
62
58
  bgState.setSession(session);
63
59
  subscribeUIObserver(session, bgState);
@@ -77,8 +73,8 @@ export function spawnBackground(
77
73
  return textResult(
78
74
  `Agent ${isQueued ? "queued" : "started"} in background.\n` +
79
75
  `Agent ID: ${id}\n` +
80
- `Type: ${config.displayName}\n` +
81
- `Description: ${config.description}\n` +
76
+ `Type: ${identity.displayName}\n` +
77
+ `Description: ${execution.description}\n` +
82
78
  (record?.outputFile ? `Output file: ${record.outputFile}\n` : "") +
83
79
  (isQueued
84
80
  ? `Position: queued (max ${manager.getMaxConcurrent()} concurrent)\n`
@@ -87,7 +83,7 @@ export function spawnBackground(
87
83
  `Use get_subagent_result to retrieve full results, or steer_subagent to send it messages.\n` +
88
84
  `Do not duplicate this agent's work.`,
89
85
  {
90
- ...config.detailBase,
86
+ ...presentation.detailBase,
91
87
  toolUses: 0,
92
88
  tokens: "",
93
89
  durationMs: 0,
@@ -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
- parentSessionFile: string;
43
- parentSessionId: string;
42
+ parentSession: ParentSessionInfo;
44
43
  }
45
44
 
46
45
  /**
@@ -56,19 +55,19 @@ export async function runForeground(
56
55
  signal: AbortSignal | undefined,
57
56
  onUpdate: ((update: AgentToolResult<any>) => void) | undefined,
58
57
  ) {
59
- const { config } = params;
58
+ const { identity, execution, presentation } = params.config;
60
59
  let spinnerFrame = 0;
61
60
  const startedAt = Date.now();
62
61
  let fgId: string | undefined;
63
62
 
64
- const fgState = new AgentActivityTracker(config.effectiveMaxTurns);
63
+ const fgState = new AgentActivityTracker(execution.effectiveMaxTurns);
65
64
  let unsubUI: (() => void) | undefined;
66
65
  let recordRef: AgentRecord | undefined;
67
66
 
68
67
  const streamUpdate = () => {
69
68
  const toolUses = recordRef?.toolUses ?? 0;
70
69
  const details: AgentDetails = {
71
- ...config.detailBase,
70
+ ...presentation.detailBase,
72
71
  toolUses,
73
72
  tokens: recordRef ? formatLifetimeTokens(recordRef) : "",
74
73
  turnCount: fgState.turnCount,
@@ -97,20 +96,19 @@ export async function runForeground(
97
96
  try {
98
97
  record = await manager.spawnAndWait(
99
98
  params.snapshot,
100
- config.subagentType,
101
- config.prompt,
99
+ identity.subagentType,
100
+ execution.prompt,
102
101
  {
103
- description: config.description,
104
- model: config.model,
105
- maxTurns: config.effectiveMaxTurns,
106
- isolated: config.isolated,
107
- inheritContext: config.inheritContext,
108
- thinkingLevel: config.thinking,
109
- isolation: config.isolation,
110
- invocation: config.agentInvocation,
102
+ description: execution.description,
103
+ model: execution.model,
104
+ maxTurns: execution.effectiveMaxTurns,
105
+ isolated: execution.isolated,
106
+ inheritContext: execution.inheritContext,
107
+ thinkingLevel: execution.thinking,
108
+ isolation: execution.isolation,
109
+ invocation: execution.agentInvocation,
111
110
  signal,
112
- parentSessionFile: params.parentSessionFile,
113
- parentSessionId: params.parentSessionId,
111
+ parentSession: params.parentSession,
114
112
  onSessionCreated: (session, record) => {
115
113
  fgState.setSession(session);
116
114
  recordRef = record;
@@ -137,10 +135,10 @@ export async function runForeground(
137
135
  }
138
136
 
139
137
  const tokenText = formatLifetimeTokens(record);
140
- const details = buildDetails(config.detailBase, record, fgState, { tokens: tokenText });
138
+ const details = buildDetails(presentation.detailBase, record, fgState, { tokens: tokenText });
141
139
 
142
- const fallbackNote = config.fellBack
143
- ? `Note: Unknown agent type "${config.rawType}" — using general-purpose.\n\n`
140
+ const fallbackNote = identity.fellBack
141
+ ? `Note: Unknown agent type "${identity.rawType}" — using general-purpose.\n\n`
144
142
  : "";
145
143
 
146
144
  if (record.status === "error") {
@@ -26,12 +26,16 @@ export interface ModelInfo {
26
26
  modelRegistry: unknown;
27
27
  }
28
28
 
29
- /** Fully resolved config for spawning an agent. */
30
- export interface ResolvedSpawnConfig {
29
+ /** Identity: who is being spawned. */
30
+ export interface SpawnIdentity {
31
31
  subagentType: string;
32
32
  rawType: SubagentType;
33
33
  fellBack: boolean;
34
34
  displayName: string;
35
+ }
36
+
37
+ /** Execution: how the agent will run. */
38
+ export interface SpawnExecution {
35
39
  prompt: string;
36
40
  description: string;
37
41
  model: Model<any> | undefined;
@@ -41,12 +45,23 @@ export interface ResolvedSpawnConfig {
41
45
  runInBackground: boolean;
42
46
  isolated: boolean;
43
47
  isolation: IsolationMode | undefined;
44
- modelName: string | undefined;
45
48
  agentInvocation: AgentInvocation;
49
+ }
50
+
51
+ /** Presentation: display/UI values derived from identity and execution. */
52
+ export interface SpawnPresentation {
53
+ modelName: string | undefined;
46
54
  agentTags: string[];
47
55
  detailBase: Pick<AgentDetails, "displayName" | "description" | "subagentType" | "modelName" | "tags">;
48
56
  }
49
57
 
58
+ /** Fully resolved config for spawning an agent — composed of domain-aligned sub-interfaces. */
59
+ export interface ResolvedSpawnConfig {
60
+ identity: SpawnIdentity;
61
+ execution: SpawnExecution;
62
+ presentation: SpawnPresentation;
63
+ }
64
+
50
65
  /** Error result when model resolution fails. */
51
66
  export interface SpawnConfigError {
52
67
  error: string;
@@ -126,22 +141,19 @@ export function resolveSpawnConfig(
126
141
  };
127
142
 
128
143
  return {
129
- subagentType,
130
- rawType,
131
- fellBack,
132
- displayName,
133
- prompt: params.prompt as string,
134
- description: params.description as string,
135
- model,
136
- effectiveMaxTurns,
137
- thinking,
138
- inheritContext,
139
- runInBackground,
140
- isolated,
141
- isolation,
142
- modelName,
143
- agentInvocation,
144
- agentTags,
145
- detailBase,
144
+ identity: { subagentType, rawType, fellBack, displayName },
145
+ execution: {
146
+ prompt: params.prompt as string,
147
+ description: params.description as string,
148
+ model,
149
+ effectiveMaxTurns,
150
+ thinking,
151
+ inheritContext,
152
+ runInBackground,
153
+ isolated,
154
+ isolation,
155
+ agentInvocation,
156
+ },
157
+ presentation: { modelName, agentTags, detailBase },
146
158
  };
147
159
  }