@gotgenes/pi-subagents 7.0.0 → 7.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +33 -0
- package/docs/architecture/architecture.md +190 -91
- package/docs/architecture/history/phase-10-structural-decomposition.md +141 -0
- package/docs/plans/0192-define-session-context-interface.md +107 -0
- package/docs/plans/0193-runtime-owns-context-queries.md +245 -0
- package/docs/retro/0185-remove-persistent-agent-memory.md +34 -0
- package/docs/retro/0192-define-session-context-interface.md +59 -0
- package/docs/retro/0193-runtime-owns-context-queries.md +43 -0
- package/package.json +1 -1
- package/src/handlers/lifecycle.ts +4 -4
- package/src/index.ts +6 -22
- package/src/lifecycle/parent-snapshot.ts +4 -3
- package/src/runtime.ts +31 -3
- package/src/service/service-adapter.ts +14 -12
- package/src/session/context.ts +20 -6
- package/src/types.ts +21 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 192
|
|
3
|
+
issue_title: "Define SessionContext narrow interface"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Define `SessionContext` narrow interface
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`SubagentRuntime.currentCtx` is typed `{ pi: unknown; ctx: unknown }`.
|
|
11
|
+
Every consumer must cast through `as any` to read fields from the SDK context.
|
|
12
|
+
This forces context queries (`buildSnapshot`, `getModelInfo`, `getSessionInfo`) to live as closures in `index.ts` with repeated `as any` casts, rather than as typed methods on the state holder.
|
|
13
|
+
|
|
14
|
+
The SDK exports `ExtensionContext` — the `unknown` typing is a historical choice, not a constraint.
|
|
15
|
+
|
|
16
|
+
## Goals
|
|
17
|
+
|
|
18
|
+
- Define a narrow `SessionContext` interface in `src/types.ts` capturing the 5 fields `SubagentRuntime` actually reads.
|
|
19
|
+
- Pure additive — no consumers change in this step.
|
|
20
|
+
- Provide the typed foundation for Layer 1 (#193) and subsequent closure-to-class conversion issues.
|
|
21
|
+
|
|
22
|
+
## Non-Goals
|
|
23
|
+
|
|
24
|
+
- Changing `SubagentRuntime.currentCtx` type (that's #193).
|
|
25
|
+
- Converting closure factories to classes (#195, #196).
|
|
26
|
+
- Removing any `as any` casts from `index.ts` (that's #193).
|
|
27
|
+
|
|
28
|
+
## Background
|
|
29
|
+
|
|
30
|
+
Phase 11, Layer 0 in `docs/architecture/architecture.md`.
|
|
31
|
+
This is the first step in a 5-issue sequence (issues #192–#196) that converts closure factories to classes, eliminating 44 adapter closures in `index.ts`.
|
|
32
|
+
|
|
33
|
+
The SDK's `ExtensionContext` interface (in `@earendil-works/pi-coding-agent`) is broad — it exposes `ui`, `abort()`, `shutdown()`, `compact()`, etc.
|
|
34
|
+
ISP (Interface Segregation Principle) from `code-design` mandates a narrow interface capturing only what `SubagentRuntime` needs.
|
|
35
|
+
|
|
36
|
+
The 5 fields consumed by runtime (traced from `index.ts` lines 214–223 and `lifecycle/parent-snapshot.ts`):
|
|
37
|
+
|
|
38
|
+
1. `cwd` — working directory for agent sessions.
|
|
39
|
+
2. `model` — parent model instance for fallback resolution.
|
|
40
|
+
3. `modelRegistry` — resolving config model strings.
|
|
41
|
+
4. `getSystemPrompt()` — system prompt for append-mode agents.
|
|
42
|
+
5. `sessionManager.getSessionFile()` / `.getSessionId()` / `.getBranch()` — session identification and context inheritance.
|
|
43
|
+
|
|
44
|
+
The local `ModelRegistry` interface (in `src/session/model-resolver.ts`) already exists as a narrow ISP interface.
|
|
45
|
+
`SessionContext` will reference it rather than redeclaring model-registry methods inline.
|
|
46
|
+
|
|
47
|
+
## Design Overview
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import type { ModelRegistry } from "#src/session/model-resolver";
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Narrow interface capturing the 5 ExtensionContext fields SubagentRuntime needs.
|
|
54
|
+
* Avoids coupling runtime to the full SDK ExtensionContext surface.
|
|
55
|
+
*/
|
|
56
|
+
export interface SessionContext {
|
|
57
|
+
readonly cwd: string;
|
|
58
|
+
readonly model: unknown;
|
|
59
|
+
readonly modelRegistry: ModelRegistry | undefined;
|
|
60
|
+
getSystemPrompt(): string;
|
|
61
|
+
readonly sessionManager: {
|
|
62
|
+
getSessionFile(): string | undefined;
|
|
63
|
+
getSessionId(): string;
|
|
64
|
+
getBranch(): unknown[];
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Design decisions:
|
|
70
|
+
|
|
71
|
+
1. `model` stays `unknown` — the runtime only passes it through to `resolveModel`; narrowing it gains nothing and would couple to `@earendil-works/pi-ai`'s `Model<Api>` generic.
|
|
72
|
+
2. `modelRegistry` is `ModelRegistry | undefined` — the SDK type says `ModelRegistry` (non-optional), but `SubagentRuntime.currentCtx` can be undefined, and the architecture doc specifies this signature.
|
|
73
|
+
The `| undefined` reflects reality at the cast boundary (pre-bind, the registry may not exist).
|
|
74
|
+
3. `sessionManager` uses an inline structural type rather than importing `ReadonlySessionManager` — we only need 3 of its 13 methods; a separate named type would be over-engineering for a nested structural slice.
|
|
75
|
+
4. `getBranch()` returns `unknown[]` — the runtime passes entries through to `buildParentContext()` which already type-narrows internally.
|
|
76
|
+
|
|
77
|
+
## Module-Level Changes
|
|
78
|
+
|
|
79
|
+
| File | Change |
|
|
80
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------- |
|
|
81
|
+
| `src/types.ts` | Add `SessionContext` interface export. Add `import type { ModelRegistry }` from `#src/session/model-resolver`. |
|
|
82
|
+
|
|
83
|
+
No other files change — this is pure additive.
|
|
84
|
+
|
|
85
|
+
## Test Impact Analysis
|
|
86
|
+
|
|
87
|
+
1. No new unit tests are needed — `SessionContext` is a pure type definition with no runtime behavior.
|
|
88
|
+
2. No existing tests become redundant.
|
|
89
|
+
3. A compile-time check (`pnpm run check`) verifies the interface is well-formed and the import resolves.
|
|
90
|
+
|
|
91
|
+
## TDD Order
|
|
92
|
+
|
|
93
|
+
1. **Add `SessionContext` interface to `src/types.ts`** — add the interface with its import.
|
|
94
|
+
Verify with `pnpm run check` (type-check passes).
|
|
95
|
+
Commit: `feat(pi-subagents): define SessionContext narrow interface (#192)`
|
|
96
|
+
|
|
97
|
+
## Risks and Mitigations
|
|
98
|
+
|
|
99
|
+
| Risk | Mitigation |
|
|
100
|
+
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| Interface shape doesn't match real `ExtensionContext` at runtime | Traced all 5 fields against SDK `.d.ts` declarations; shapes align exactly. |
|
|
102
|
+
| Circular import from `types.ts` → `session/model-resolver.ts` | `model-resolver.ts` does not import from `types.ts`; no cycle. |
|
|
103
|
+
| Future SDK changes break the narrow interface | The cast boundary (Layer 1, #193) will be the single enforcement point — structural typing ensures compile-time detection. |
|
|
104
|
+
|
|
105
|
+
## Open Questions
|
|
106
|
+
|
|
107
|
+
None — the issue's "Proposed change" section fully specifies the interface shape.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 193
|
|
3
|
+
issue_title: "SubagentRuntime owns context queries"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# SubagentRuntime owns context queries
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Three closure queries in `index.ts` reach into `runtime.currentCtx?.ctx` with `as any` casts to extract model, modelRegistry, and sessionManager values.
|
|
11
|
+
These closures exist because `SubagentRuntime` stores `unknown` and doesn't provide typed accessors.
|
|
12
|
+
The queries belong on the state holder — `SubagentRuntime` owns `currentCtx`, so it should own the queries on that state.
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Type `SubagentRuntime.currentCtx` as `SessionContext | undefined` (eliminating the `{ pi: unknown; ctx: unknown }` shape).
|
|
17
|
+
- Move the `as SessionContext` cast into `handleSessionStart` — the single SDK boundary.
|
|
18
|
+
- Add typed methods on `SubagentRuntime`: `buildSnapshot()`, `getModelInfo()`, `getSessionInfo()`.
|
|
19
|
+
- Remove 4 `as any` casts from `index.ts`.
|
|
20
|
+
- Remove 3 closure queries from the composition root.
|
|
21
|
+
- Update `service-adapter.ts` to delegate to `SubagentRuntime` instead of holding its own `as ExtensionContext` cast.
|
|
22
|
+
|
|
23
|
+
## Non-Goals
|
|
24
|
+
|
|
25
|
+
- Converting closure factories to classes (Layer 3, #195/#196).
|
|
26
|
+
- Aligning interface names so real objects satisfy tool deps (Layer 2, #194).
|
|
27
|
+
- Changing `buildParentContext` in `session/context.ts` (it will continue to accept `ExtensionContext`; `SessionContext` is structurally compatible).
|
|
28
|
+
|
|
29
|
+
## Background
|
|
30
|
+
|
|
31
|
+
Issue #192 (closed, shipped as v7.1.0) added the `SessionContext` interface to `src/types.ts`.
|
|
32
|
+
This plan builds on that foundation.
|
|
33
|
+
|
|
34
|
+
The architecture doc (Phase 11, Layer 1) specifies this exact change: "Change `currentCtx` from `{ pi: unknown; ctx: unknown }` to `SessionContext | undefined`."
|
|
35
|
+
|
|
36
|
+
Key modules:
|
|
37
|
+
|
|
38
|
+
- `src/runtime.ts` — `SubagentRuntime` class with `currentCtx`, `setSessionContext()`, `clearSessionContext()`
|
|
39
|
+
- `src/handlers/lifecycle.ts` — `SessionLifecycleHandler.handleSessionStart()` receives raw SDK `ctx`
|
|
40
|
+
- `src/index.ts` — composition root with inline `buildSnapshot`, `getModelInfo`, `getSessionInfo` closures
|
|
41
|
+
- `src/service/service-adapter.ts` — `createSubagentsService()` with `getCtx()` and `getModelRegistry()` closures
|
|
42
|
+
- `src/lifecycle/parent-snapshot.ts` — `buildParentSnapshot(ctx: ExtensionContext, inheritContext?)` function
|
|
43
|
+
- `src/session/context.ts` — `buildParentContext(ctx: ExtensionContext)` function
|
|
44
|
+
|
|
45
|
+
AGENTS.md constraint: keep modules focused and composable.
|
|
46
|
+
The queries belong on the state owner per Law of Demeter (code-design skill).
|
|
47
|
+
|
|
48
|
+
## Design Overview
|
|
49
|
+
|
|
50
|
+
### Type change
|
|
51
|
+
|
|
52
|
+
`SubagentRuntime.currentCtx` becomes `SessionContext | undefined` (previously `{ pi: unknown; ctx: unknown } | undefined`).
|
|
53
|
+
The `pi` field is dropped from the stored context — it is only used in `SessionLifecycleHandler` which already stores it as a constructor param.
|
|
54
|
+
|
|
55
|
+
### Cast boundary
|
|
56
|
+
|
|
57
|
+
`handleSessionStart` receives `ctx: unknown` from the SDK event.
|
|
58
|
+
The single `as SessionContext` cast lives here:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
handleSessionStart(_event: unknown, ctx: unknown): void {
|
|
62
|
+
this.runtime.setSessionContext(ctx as SessionContext);
|
|
63
|
+
this.manager.clearCompleted();
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### New methods on `SubagentRuntime`
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
|
|
71
|
+
import type { ModelInfo } from "#src/tools/spawn-config";
|
|
72
|
+
import type { SessionContext } from "#src/types";
|
|
73
|
+
|
|
74
|
+
class SubagentRuntime {
|
|
75
|
+
currentCtx: SessionContext | undefined = undefined;
|
|
76
|
+
|
|
77
|
+
setSessionContext(ctx: SessionContext): void {
|
|
78
|
+
this.currentCtx = ctx;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
clearSessionContext(): void {
|
|
82
|
+
this.currentCtx = undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
buildSnapshot(inheritContext: boolean): ParentSnapshot {
|
|
86
|
+
return buildParentSnapshot(this.currentCtx!, inheritContext);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
getModelInfo(): ModelInfo {
|
|
90
|
+
return {
|
|
91
|
+
parentModel: this.currentCtx?.model as ModelInfo["parentModel"],
|
|
92
|
+
modelRegistry: this.currentCtx?.modelRegistry,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
getSessionInfo(): { parentSessionFile: string; parentSessionId: string } {
|
|
97
|
+
return {
|
|
98
|
+
parentSessionFile: this.currentCtx?.sessionManager?.getSessionFile() ?? "",
|
|
99
|
+
parentSessionId: this.currentCtx?.sessionManager?.getSessionId() ?? "",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### `buildParentSnapshot` signature change
|
|
106
|
+
|
|
107
|
+
Change the parameter type from `ExtensionContext` to `SessionContext`:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
export function buildParentSnapshot(
|
|
111
|
+
ctx: SessionContext,
|
|
112
|
+
inheritContext?: boolean,
|
|
113
|
+
): ParentSnapshot { ... }
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`ExtensionContext` structurally satisfies `SessionContext` (all 5 fields match), so existing callers (the `/agents` command handler) continue to work without change.
|
|
117
|
+
|
|
118
|
+
Similarly, `buildParentContext` changes from `ExtensionContext` to `SessionContext`.
|
|
119
|
+
The `sessionManager.getBranch()` returns `unknown[]` in `SessionContext`, which is what `buildParentContext` already treats the entries as (it accesses `.type`, `.message`, `.summary` via runtime checks without type narrowing).
|
|
120
|
+
|
|
121
|
+
### `service-adapter.ts` change
|
|
122
|
+
|
|
123
|
+
The adapter currently receives `getCtx: () => { pi: unknown; ctx: unknown } | undefined`.
|
|
124
|
+
After this change, it receives the runtime directly and calls `runtime.buildSnapshot()`:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
export function createSubagentsService(
|
|
128
|
+
manager: AgentManagerLike,
|
|
129
|
+
resolveModel: (input: string, registry: ModelRegistry) => unknown,
|
|
130
|
+
runtime: ServiceRuntimeLike,
|
|
131
|
+
): SubagentsService { ... }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Where `ServiceRuntimeLike` is a narrow interface:
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
export interface ServiceRuntimeLike {
|
|
138
|
+
readonly currentCtx: SessionContext | undefined;
|
|
139
|
+
buildSnapshot(inheritContext: boolean): ParentSnapshot;
|
|
140
|
+
getModelInfo(): { modelRegistry: unknown };
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### Impact on `LifecycleRuntime` interface
|
|
145
|
+
|
|
146
|
+
The narrow interface in `handlers/lifecycle.ts` changes from:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
interface LifecycleRuntime {
|
|
150
|
+
setSessionContext(pi: unknown, ctx: unknown): void;
|
|
151
|
+
clearSessionContext(): void;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
to:
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
interface LifecycleRuntime {
|
|
159
|
+
setSessionContext(ctx: SessionContext): void;
|
|
160
|
+
clearSessionContext(): void;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Module-Level Changes
|
|
165
|
+
|
|
166
|
+
1. `src/runtime.ts` — Change `currentCtx` type, update `setSessionContext()` signature (drop `pi` param), add `buildSnapshot()`, `getModelInfo()`, `getSessionInfo()` methods.
|
|
167
|
+
Add imports for `SessionContext`, `ParentSnapshot`, `ModelInfo`, `buildParentSnapshot`.
|
|
168
|
+
2. `src/handlers/lifecycle.ts` — Update `LifecycleRuntime` interface (single `ctx: SessionContext` param).
|
|
169
|
+
Cast `ctx as SessionContext` in `handleSessionStart`.
|
|
170
|
+
Remove `pi` from `setSessionContext` call.
|
|
171
|
+
Import `SessionContext`.
|
|
172
|
+
3. `src/lifecycle/parent-snapshot.ts` — Change `buildParentSnapshot` param from `ExtensionContext` to `SessionContext`.
|
|
173
|
+
Update import.
|
|
174
|
+
4. `src/session/context.ts` — Change `buildParentContext` param from `ExtensionContext` to `SessionContext`.
|
|
175
|
+
Update import.
|
|
176
|
+
5. `src/service/service-adapter.ts` — Replace `getCtx`/`getModelRegistry` closures with a `ServiceRuntimeLike` interface.
|
|
177
|
+
Use `runtime.buildSnapshot()` and `runtime.currentCtx?.modelRegistry`.
|
|
178
|
+
Remove `ExtensionContext` import.
|
|
179
|
+
6. `src/index.ts` — Remove inline `buildSnapshot`, `getModelInfo`, `getSessionInfo` closures from `createAgentTool` deps.
|
|
180
|
+
Pass `runtime.buildSnapshot.bind(runtime)`, `runtime.getModelInfo.bind(runtime)`, `runtime.getSessionInfo.bind(runtime)`.
|
|
181
|
+
Update `createSubagentsService` call to pass `runtime` instead of two closures.
|
|
182
|
+
Update `lifecycle.handleSessionStart` call (drop `pi` from `setSessionContext`).
|
|
183
|
+
Remove `as any` eslint-disable for the eliminated casts.
|
|
184
|
+
7. `test/runtime.test.ts` — Update session-context method tests (single param).
|
|
185
|
+
Add tests for `buildSnapshot()`, `getModelInfo()`, `getSessionInfo()`.
|
|
186
|
+
8. `test/handlers/lifecycle.test.ts` — Update `setSessionContext` mock expectations (single param).
|
|
187
|
+
9. `test/service/service-adapter.test.ts` — Update `createSubagentsService` calls to pass a runtime-like mock instead of two closures.
|
|
188
|
+
10. `test/helpers/stub-ctx.ts` (or equivalent) — Verify the stub ctx satisfies `SessionContext`.
|
|
189
|
+
|
|
190
|
+
## Test Impact Analysis
|
|
191
|
+
|
|
192
|
+
1. **New unit tests enabled:** `SubagentRuntime.buildSnapshot()`, `.getModelInfo()`, `.getSessionInfo()` — previously untestable as anonymous closures.
|
|
193
|
+
2. **Existing tests that simplify:** `service-adapter.test.ts` no longer needs to wire `getCtx`/`getModelRegistry` closures; a simple runtime stub replaces them.
|
|
194
|
+
3. **Tests that stay as-is:** `agent-tool.test.ts` (via `make-deps.ts`) — the tool deps interface still has `buildSnapshot`, `getModelInfo`, `getSessionInfo` fields; only the wiring in `index.ts` changes.
|
|
195
|
+
The tool tests use mocks and are unaffected.
|
|
196
|
+
|
|
197
|
+
## TDD Order
|
|
198
|
+
|
|
199
|
+
1. **Red → Green:** Add `buildSnapshot()`, `getModelInfo()`, `getSessionInfo()` method tests to `runtime.test.ts`.
|
|
200
|
+
Update `setSessionContext` tests to use single param.
|
|
201
|
+
Tests fail because the methods don't exist yet.
|
|
202
|
+
Commit: `test: add SubagentRuntime context-query method tests (#193)`
|
|
203
|
+
|
|
204
|
+
2. **Green:** Change `SubagentRuntime.currentCtx` type to `SessionContext | undefined`.
|
|
205
|
+
Update `setSessionContext` to single param.
|
|
206
|
+
Add three query methods.
|
|
207
|
+
Update imports.
|
|
208
|
+
Run `pnpm run check` to verify type coherence.
|
|
209
|
+
Commit: `feat: SubagentRuntime stores typed SessionContext and owns context queries (#193)`
|
|
210
|
+
|
|
211
|
+
3. **Green:** Change `buildParentSnapshot` and `buildParentContext` to accept `SessionContext` instead of `ExtensionContext`.
|
|
212
|
+
Update imports.
|
|
213
|
+
Run `pnpm run check`.
|
|
214
|
+
Commit: `refactor: narrow buildParentSnapshot param to SessionContext (#193)`
|
|
215
|
+
|
|
216
|
+
4. **Green:** Update `handlers/lifecycle.ts` — change `LifecycleRuntime` interface, cast `ctx as SessionContext` in handler.
|
|
217
|
+
Update lifecycle test expectations.
|
|
218
|
+
Commit: `refactor: move SessionContext cast to handleSessionStart boundary (#193)`
|
|
219
|
+
|
|
220
|
+
5. **Green:** Update `service-adapter.ts` — introduce `ServiceRuntimeLike`, replace closure params with runtime.
|
|
221
|
+
Update service-adapter tests.
|
|
222
|
+
Commit: `refactor: service-adapter delegates to SubagentRuntime for context (#193)`
|
|
223
|
+
|
|
224
|
+
6. **Green:** Update `index.ts` — wire `runtime.buildSnapshot.bind(runtime)` etc. into agent tool deps.
|
|
225
|
+
Update `createSubagentsService` call.
|
|
226
|
+
Remove `as any` casts and corresponding eslint-disable comment.
|
|
227
|
+
Clean up unused imports.
|
|
228
|
+
Commit: `refactor: index.ts delegates context queries to SubagentRuntime (#193)`
|
|
229
|
+
|
|
230
|
+
7. **Verify:** Run full test suite (`pnpm vitest run`) and type check (`pnpm run check`).
|
|
231
|
+
Fix any remaining lint issues.
|
|
232
|
+
Commit: `chore: cleanup lint after SubagentRuntime context migration (#193)` (if needed)
|
|
233
|
+
|
|
234
|
+
## Risks and Mitigations
|
|
235
|
+
|
|
236
|
+
| Risk | Mitigation |
|
|
237
|
+
| ----------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
238
|
+
| `buildParentSnapshot` callers outside this package break when param changes from `ExtensionContext` to `SessionContext` | `ExtensionContext` structurally satisfies `SessionContext` — no source-level changes needed at call sites. The `/agents` command handler passes `ctx` which is still `ExtensionContext` from the SDK. |
|
|
239
|
+
| `runtime.currentCtx` is `undefined` when `buildSnapshot()` is called | Same risk exists today — the closure reads `runtime.currentCtx?.ctx` which may be undefined. The `!` assertion documents the invariant: methods are only called during an active session. |
|
|
240
|
+
| Dropping `pi` from `currentCtx` breaks something that reads it | Grep confirms `pi` is only stored and never read back from `currentCtx`. `SessionLifecycleHandler` already stores `pi` as its own constructor param. |
|
|
241
|
+
| Test fixture `make-deps.ts` still mocks `buildSnapshot`/`getModelInfo`/`getSessionInfo` on `AgentToolDeps` | Correct — the tool interface doesn't change in this issue. The closures in `index.ts` are replaced with bound methods, but the deps shape stays the same. |
|
|
242
|
+
|
|
243
|
+
## Open Questions
|
|
244
|
+
|
|
245
|
+
- None — the issue's proposed change and the architecture doc's Layer 1 spec are fully aligned.
|
|
@@ -37,3 +37,37 @@ New file `safe-fs.test.ts` was created with 13 tests for the extracted utilities
|
|
|
37
37
|
Fix: inline the literal union `"user" | "project" | "local"` directly in `memory.ts` as a local type, so it compiles cleanly until deletion in step 4.
|
|
38
38
|
- The `SKILL.md` for `package-pi-subagents` also listed `memory.ts` in the session domain table — updated alongside `architecture.md` in the docs commit.
|
|
39
39
|
- No deviations from the plan other than the two minor bugs above (both self-corrected within the same TDD step).
|
|
40
|
+
|
|
41
|
+
## Stage: Final Retrospective (2026-05-24T22:47:55Z)
|
|
42
|
+
|
|
43
|
+
### Session summary
|
|
44
|
+
|
|
45
|
+
Shipped issue #185 as `pi-subagents-v7.0.0`.
|
|
46
|
+
CI passed, issue closed, release-please PR #190 merged.
|
|
47
|
+
Three sessions total: planning, TDD (5 steps / 5 commits), shipping.
|
|
48
|
+
|
|
49
|
+
### Observations
|
|
50
|
+
|
|
51
|
+
#### What went well
|
|
52
|
+
|
|
53
|
+
- The issue's "Scope" section was precise enough that the planning session required no `ask_user` and the Explore agent's trace matched the final commit diff exactly.
|
|
54
|
+
- Consumers-first, declaration-last ordering kept each commit independently compilable (after the two self-corrected fixes).
|
|
55
|
+
- The `SKILL.md` domain table update was caught naturally during the docs step even though the plan didn't list it.
|
|
56
|
+
|
|
57
|
+
#### What caused friction (agent side)
|
|
58
|
+
|
|
59
|
+
- `missing-context` — In TDD step 1, `memory.ts` was updated to re-export `isUnsafeName` from `safe-fs`, but the function was not imported into `memory.ts`'s own scope.
|
|
60
|
+
`resolveMemoryDir` threw a `ReferenceError` at runtime.
|
|
61
|
+
Impact: one extra test-run cycle (~5 seconds) and a trivial one-line fix; no rework commit.
|
|
62
|
+
- `missing-context` — In TDD step 3, removing `MemoryScope` from `types.ts` broke `memory.ts` (scheduled for deletion in step 4).
|
|
63
|
+
The plan said "consumers-first" but didn't account for the doomed module itself being a consumer of the type.
|
|
64
|
+
Impact: one extra `pnpm run check` cycle and a local type inline; no rework commit.
|
|
65
|
+
Both share the same root cause: incremental deletion plans must account for doomed files' own imports at each intermediate step.
|
|
66
|
+
|
|
67
|
+
#### What caused friction (user side)
|
|
68
|
+
|
|
69
|
+
- None observed — all three sessions ran without user corrections or redirections.
|
|
70
|
+
|
|
71
|
+
### Changes made
|
|
72
|
+
|
|
73
|
+
1. Added a TDD planning rule to `.pi/skills/testing/SKILL.md` about accounting for doomed modules' own imports during multi-step deletion.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 192
|
|
3
|
+
issue_title: "Define SessionContext narrow interface"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #192 — Define SessionContext narrow interface
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-24T16:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned the pure-additive `SessionContext` interface for `src/types.ts`.
|
|
13
|
+
Traced all 5 consumed fields against the SDK's `ExtensionContext` type declarations to confirm shape alignment.
|
|
14
|
+
Single TDD step: add the interface and verify with `pnpm run check`.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
- The interface is trivial in scope — one new export with no consumers changing.
|
|
19
|
+
This is intentionally the smallest possible first step to unblock Layer 1 (#193).
|
|
20
|
+
- `ModelRegistry` already exists as a local narrow interface in `src/session/model-resolver.ts`; `SessionContext` imports it rather than redeclaring.
|
|
21
|
+
- `sessionManager` uses an inline structural type (3 methods) rather than importing the SDK's `ReadonlySessionManager` (13 methods) — ISP applies here.
|
|
22
|
+
- No design ambiguity required `ask_user`; the issue's proposed change section was fully specified.
|
|
23
|
+
|
|
24
|
+
## Stage: Implementation — TDD (2026-05-24T19:55:00Z)
|
|
25
|
+
|
|
26
|
+
### Session summary
|
|
27
|
+
|
|
28
|
+
Added the `SessionContext` interface to `src/types.ts` with an `import type { ModelRegistry }` from `#src/session/model-resolver`.
|
|
29
|
+
Single compile-time step — no runtime tests needed for a pure type definition.
|
|
30
|
+
Baseline: 53 test files, 848 tests; final: unchanged.
|
|
31
|
+
|
|
32
|
+
### Observations
|
|
33
|
+
|
|
34
|
+
- Pre-existing lint failure in `docs/architecture/architecture.md` (5 unused MD053 link references for issues #164, #165, #170, #171, #172) was fixed as part of the baseline verification and included in the feat commit.
|
|
35
|
+
- The interface landed exactly as planned — no deviations from the plan's Design Overview.
|
|
36
|
+
|
|
37
|
+
## Stage: Final Retrospective (2026-05-24T20:00:00Z)
|
|
38
|
+
|
|
39
|
+
### Session summary
|
|
40
|
+
|
|
41
|
+
All three stages (plan, TDD, ship) completed in a single session.
|
|
42
|
+
Released as `pi-subagents-v7.1.0`.
|
|
43
|
+
No rework, no deviations from plan.
|
|
44
|
+
|
|
45
|
+
### Observations
|
|
46
|
+
|
|
47
|
+
#### What went well
|
|
48
|
+
|
|
49
|
+
- The issue was fully specified — no ambiguity, no `ask_user` needed at any stage.
|
|
50
|
+
- Trivial scope (one interface, no consumers) made the plan-to-ship pipeline fast and mechanical.
|
|
51
|
+
- Pre-existing lint failures in `architecture.md` were caught during baseline verification and fixed without disrupting the flow.
|
|
52
|
+
|
|
53
|
+
#### What caused friction (agent side)
|
|
54
|
+
|
|
55
|
+
- None — clean execution with no rework or corrections.
|
|
56
|
+
|
|
57
|
+
#### What caused friction (user side)
|
|
58
|
+
|
|
59
|
+
- None — no intervention needed beyond invoking the three workflow commands.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 193
|
|
3
|
+
issue_title: "SubagentRuntime owns context queries"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #193 — SubagentRuntime owns context queries
|
|
7
|
+
|
|
8
|
+
## Stage: Planning (2026-05-24T21:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned the Layer 1 change that types `SubagentRuntime.currentCtx` as `SessionContext`, adds three query methods (`buildSnapshot`, `getModelInfo`, `getSessionInfo`), and eliminates 4 `as any` casts from `index.ts`.
|
|
13
|
+
The plan covers 7 TDD steps touching `runtime.ts`, `handlers/lifecycle.ts`, `parent-snapshot.ts`, `context.ts`, `service-adapter.ts`, and `index.ts`.
|
|
14
|
+
|
|
15
|
+
### Observations
|
|
16
|
+
|
|
17
|
+
- The `pi` field in `currentCtx` is never read back — only stored.
|
|
18
|
+
Dropping it is safe; `SessionLifecycleHandler` already holds `pi` as a constructor param.
|
|
19
|
+
- `ExtensionContext` structurally satisfies `SessionContext`, so changing `buildParentSnapshot`'s param type is source-compatible with the `/agents` command handler that passes raw SDK `ctx`.
|
|
20
|
+
- `service-adapter.ts` gets the biggest structural change: its two closure params (`getCtx`, `getModelRegistry`) collapse into a single `ServiceRuntimeLike` interface.
|
|
21
|
+
- No design ambiguity — the architecture doc's Layer 1 spec and the issue body are fully aligned.
|
|
22
|
+
- Test fixtures in `make-deps.ts` are unaffected because the `AgentToolDeps` interface shape doesn't change — only the wiring in `index.ts` that supplies the implementations changes.
|
|
23
|
+
|
|
24
|
+
## Stage: Implementation — TDD (2026-05-24T20:30:00Z)
|
|
25
|
+
|
|
26
|
+
### Session summary
|
|
27
|
+
|
|
28
|
+
Completed all 6 implementation TDD steps plus an architecture doc update in one session.
|
|
29
|
+
The `getSessionInfo` implementation needed `?.sessionManager.getSessionFile()` (not `?.sessionManager?.getSessionFile()`) since `sessionManager` is a required field of `SessionContext` — ESLint's `no-unnecessary-condition` caught this at the pre-commit hook.
|
|
30
|
+
Final test count: 854 (up from 848 baseline, +6 new tests for `buildSnapshot`, `getModelInfo`, `getSessionInfo`).
|
|
31
|
+
|
|
32
|
+
### Observations
|
|
33
|
+
|
|
34
|
+
- The plan's Non-Goals section incorrectly said `buildParentContext` would NOT change.
|
|
35
|
+
In practice it had to accept `SessionContext` instead of `ExtensionContext` — they are not substitutable in that direction.
|
|
36
|
+
The Module-Level Changes list was correct; only the Non-Goals prose was wrong.
|
|
37
|
+
- `context.ts` needed a local `BranchEntry` union type to handle `getBranch(): unknown[]`.
|
|
38
|
+
TypeScript's discriminated union narrowing doesn't work when the union includes a catch-all `{ type: string }` arm — explicit casts within each `if` branch were required.
|
|
39
|
+
- `service-adapter.ts` ended up using `runtime.currentCtx.modelRegistry` directly (no `getModelInfo()` call needed in the service adapter) — `ServiceRuntimeLike` only needs `currentCtx` and `buildSnapshot`.
|
|
40
|
+
This is cleaner than the plan's `getModelInfo(): { modelRegistry: unknown }` approach.
|
|
41
|
+
- Biome's `noUnusedPrivateClassMembers` warning caught the leftover `private readonly pi: unknown` in `SessionLifecycleHandler`.
|
|
42
|
+
Removed `pi` from the constructor entirely (rather than adding `_` prefix), which also cleaned up `index.ts`.
|
|
43
|
+
- The `eslint-disable` directive at the top of `index.ts` had two now-unused entries (`no-unsafe-member-access`, `no-unsafe-call`) removed by `eslint --fix`.
|
package/package.json
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { SessionContext } from "#src/types";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Session lifecycle event handlers: session_start, session_before_switch, session_shutdown.
|
|
3
5
|
*
|
|
@@ -14,7 +16,7 @@ export interface LifecycleManager {
|
|
|
14
16
|
|
|
15
17
|
/** Narrow runtime interface — only the methods lifecycle handlers call. */
|
|
16
18
|
export interface LifecycleRuntime {
|
|
17
|
-
setSessionContext(
|
|
19
|
+
setSessionContext(ctx: SessionContext): void;
|
|
18
20
|
clearSessionContext(): void;
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -22,7 +24,6 @@ export interface LifecycleRuntime {
|
|
|
22
24
|
* Handles session lifecycle events.
|
|
23
25
|
*
|
|
24
26
|
* Constructor deps:
|
|
25
|
-
* - `pi` — the ExtensionAPI instance, stored in runtime on session_start
|
|
26
27
|
* - `runtime` — owns session context state
|
|
27
28
|
* - `manager` — manages agent lifecycle (clear, abort, dispose)
|
|
28
29
|
* - `disposeNotifications` — tears down the notification system on shutdown
|
|
@@ -30,7 +31,6 @@ export interface LifecycleRuntime {
|
|
|
30
31
|
*/
|
|
31
32
|
export class SessionLifecycleHandler {
|
|
32
33
|
constructor(
|
|
33
|
-
private readonly pi: unknown,
|
|
34
34
|
private readonly runtime: LifecycleRuntime,
|
|
35
35
|
private readonly manager: LifecycleManager,
|
|
36
36
|
private readonly disposeNotifications: () => void,
|
|
@@ -38,7 +38,7 @@ export class SessionLifecycleHandler {
|
|
|
38
38
|
) {}
|
|
39
39
|
|
|
40
40
|
handleSessionStart(_event: unknown, ctx: unknown): void {
|
|
41
|
-
this.runtime.setSessionContext(
|
|
41
|
+
this.runtime.setSessionContext(ctx as SessionContext);
|
|
42
42
|
this.manager.clearCompleted();
|
|
43
43
|
}
|
|
44
44
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-unsafe-
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-argument -- Pi SDK types are not fully exported; see upstream Pi SDK for type improvements */
|
|
2
2
|
/**
|
|
3
3
|
* pi-agents — A pi extension providing Claude Code-style autonomous sub-agents.
|
|
4
4
|
*
|
|
@@ -35,7 +35,7 @@ import { publishSubagentsService, unpublishSubagentsService } from "#src/service
|
|
|
35
35
|
import { createSubagentsService } from "#src/service/service-adapter";
|
|
36
36
|
import { detectEnv } from "#src/session/env";
|
|
37
37
|
|
|
38
|
-
import {
|
|
38
|
+
import { resolveModel } from "#src/session/model-resolver";
|
|
39
39
|
import { buildAgentPrompt } from "#src/session/prompts";
|
|
40
40
|
import { deriveSubagentSessionDir } from "#src/session/session-dir";
|
|
41
41
|
import { preloadSkills } from "#src/session/skill-loader";
|
|
@@ -162,16 +162,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
162
162
|
|
|
163
163
|
// Typed service published via Symbol.for() for cross-extension access.
|
|
164
164
|
// Consumers: const { getSubagentsService } = await import("@gotgenes/pi-subagents");
|
|
165
|
-
const service = createSubagentsService(
|
|
166
|
-
manager,
|
|
167
|
-
resolveModel,
|
|
168
|
-
() => runtime.currentCtx,
|
|
169
|
-
() => (runtime.currentCtx?.ctx as { modelRegistry?: ModelRegistry } | undefined)?.modelRegistry,
|
|
170
|
-
);
|
|
165
|
+
const service = createSubagentsService(manager, resolveModel, runtime);
|
|
171
166
|
publishSubagentsService(service);
|
|
172
167
|
|
|
173
168
|
const lifecycle = new SessionLifecycleHandler(
|
|
174
|
-
pi,
|
|
175
169
|
runtime,
|
|
176
170
|
manager,
|
|
177
171
|
() => notifications.dispose(),
|
|
@@ -209,19 +203,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
209
203
|
registry,
|
|
210
204
|
agentDir: getAgentDir(),
|
|
211
205
|
settings,
|
|
212
|
-
buildSnapshot: (
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
inheritContext,
|
|
216
|
-
),
|
|
217
|
-
getModelInfo: () => ({
|
|
218
|
-
parentModel: (runtime.currentCtx?.ctx as any)?.model,
|
|
219
|
-
modelRegistry: (runtime.currentCtx?.ctx as any)?.modelRegistry,
|
|
220
|
-
}),
|
|
221
|
-
getSessionInfo: () => ({
|
|
222
|
-
parentSessionFile: (runtime.currentCtx?.ctx as any)?.sessionManager?.getSessionFile() ?? "",
|
|
223
|
-
parentSessionId: (runtime.currentCtx?.ctx as any)?.sessionManager?.getSessionId() ?? "",
|
|
224
|
-
}),
|
|
206
|
+
buildSnapshot: runtime.buildSnapshot.bind(runtime),
|
|
207
|
+
getModelInfo: runtime.getModelInfo.bind(runtime),
|
|
208
|
+
getSessionInfo: runtime.getSessionInfo.bind(runtime),
|
|
225
209
|
})));
|
|
226
210
|
|
|
227
211
|
// ---- get_subagent_result tool ----
|
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
* parent-snapshot.ts — Capture parent session state as a plain data snapshot.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
5
|
import { buildParentContext } from "#src/session/context";
|
|
6
|
+
import type { SessionContext } from "#src/types";
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Plain data snapshot of the parent session state captured at spawn time.
|
|
@@ -32,7 +32,7 @@ export interface ParentSnapshot {
|
|
|
32
32
|
* when the user requested the agent, not when a queue slot opens.
|
|
33
33
|
*/
|
|
34
34
|
export function buildParentSnapshot(
|
|
35
|
-
ctx:
|
|
35
|
+
ctx: SessionContext,
|
|
36
36
|
inheritContext?: boolean,
|
|
37
37
|
): ParentSnapshot {
|
|
38
38
|
const parentContext = inheritContext ? buildParentContext(ctx) : undefined;
|
|
@@ -40,7 +40,8 @@ export function buildParentSnapshot(
|
|
|
40
40
|
cwd: ctx.cwd,
|
|
41
41
|
systemPrompt: ctx.getSystemPrompt(),
|
|
42
42
|
model: ctx.model,
|
|
43
|
-
|
|
43
|
+
|
|
44
|
+
modelRegistry: ctx.modelRegistry!,
|
|
44
45
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- || intentional: converts empty string to undefined as well as null/undefined
|
|
45
46
|
parentContext: parentContext || undefined,
|
|
46
47
|
};
|