@gotgenes/pi-subagents 6.9.4 → 6.11.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 +28 -0
- package/docs/architecture/architecture.md +13 -42
- package/docs/plans/0132-inject-io-into-session-config.md +219 -0
- package/docs/plans/0133-inject-sdk-boundary-into-agent-runner.md +373 -0
- package/docs/retro/0131-consolidate-shared-test-fixtures.md +46 -0
- package/docs/retro/0132-inject-io-into-session-config.md +33 -0
- package/package.json +1 -1
- package/src/agent-runner.ts +88 -23
- package/src/index.ts +32 -3
- package/src/session-config.ts +41 -10
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,34 @@ 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.11.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.10.0...pi-subagents-v6.11.0) (2026-05-22)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* inject SDK boundary into agent-runner via RunnerIO ([#133](https://github.com/gotgenes/pi-packages/issues/133)) ([a9f6a9e](https://github.com/gotgenes/pi-packages/commit/a9f6a9e8c71e307b71600409e865fb539312f539))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Documentation
|
|
17
|
+
|
|
18
|
+
* plan SDK boundary injection into agent-runner ([#133](https://github.com/gotgenes/pi-packages/issues/133)) ([1706ebc](https://github.com/gotgenes/pi-packages/commit/1706ebcc1452c6798dafb733ec8c68e6ee9e8512))
|
|
19
|
+
* **retro:** add retro notes for issue [#132](https://github.com/gotgenes/pi-packages/issues/132) ([d0af140](https://github.com/gotgenes/pi-packages/commit/d0af1409ddc18099dfdda94ab37af2b99bc46c3c))
|
|
20
|
+
* update architecture doc for Step H completion ([#133](https://github.com/gotgenes/pi-packages/issues/133)) ([f6b1258](https://github.com/gotgenes/pi-packages/commit/f6b1258f50a038df18ca1f33e3681c7bc258f4fc))
|
|
21
|
+
|
|
22
|
+
## [6.10.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.4...pi-subagents-v6.10.0) (2026-05-22)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* inject IO collaborators into assembleSessionConfig ([#132](https://github.com/gotgenes/pi-packages/issues/132)) ([74d3dbf](https://github.com/gotgenes/pi-packages/commit/74d3dbf5e67cf28f75683e55240719ad2be86490))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
* mark Step G complete in Phase 8 roadmap ([#132](https://github.com/gotgenes/pi-packages/issues/132)) ([95512bd](https://github.com/gotgenes/pi-packages/commit/95512bdec3757d5955d13e22261a90da41cea40e))
|
|
33
|
+
* plan IO collaborator injection into assembleSessionConfig ([#132](https://github.com/gotgenes/pi-packages/issues/132)) ([23c3b62](https://github.com/gotgenes/pi-packages/commit/23c3b624e8c0afb8fda72c1b5fba86cb165f78dd))
|
|
34
|
+
* **retro:** add retro notes for issue [#131](https://github.com/gotgenes/pi-packages/issues/131) ([b91cee9](https://github.com/gotgenes/pi-packages/commit/b91cee9ef69f8b1ab41be986663bad22e77a8c67))
|
|
35
|
+
|
|
8
36
|
## [6.9.4](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v6.9.3...pi-subagents-v6.9.4) (2026-05-22)
|
|
9
37
|
|
|
10
38
|
|
|
@@ -505,9 +505,8 @@ E2 (Type housekeeping) ── can start after A1, runs parallel to later steps
|
|
|
505
505
|
Phase 7 eliminated all structural smells (mutable state, closure bags, callback threading, wide dependency bags).
|
|
506
506
|
Phase 8 targets the next layer: testability friction, display module cohesion, and menu decomposition.
|
|
507
507
|
|
|
508
|
-
The test suite (
|
|
509
|
-
|
|
510
|
-
This fragility is a symptom of production code that imports IO-touching collaborators directly instead of receiving them through injection.
|
|
508
|
+
The test suite (714 tests) is comprehensive but uneven in quality.
|
|
509
|
+
Steps G and H have eliminated 11 of the original 12 `vi.mock()` calls in the runner tests, removing fragile call-sequence assertions in favour of injected stubs. (Step G resolved `session-config.test.ts`; Step H resolved both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`.)
|
|
511
510
|
|
|
512
511
|
The display and menu improvements were identified during Phase 7 but deferred because they don't gate encapsulation work.
|
|
513
512
|
They are included here because the display extraction unblocks menu decomposition.
|
|
@@ -516,8 +515,8 @@ They are included here because the display extraction unblocks menu decompositio
|
|
|
516
515
|
|
|
517
516
|
| Symptom | Location | Root cause |
|
|
518
517
|
| ----------------------------- | ------------------------------------------------------- | ----------------------------------------------------------------- |
|
|
519
|
-
| 7 `vi.mock()` calls
|
|
520
|
-
|
|
|
518
|
+
| ~~7 `vi.mock()` calls~~ | ~~`agent-runner.test.ts`~~ | ~~Resolved by Step H (#133)~~ |
|
|
519
|
+
| ~~7 `vi.mock()` calls~~ | ~~`agent-runner-extension-tools.test.ts`~~ | ~~Resolved by Step H (#133)~~ |
|
|
521
520
|
| 52 `as any` casts | Across test suite | SDK session/context interfaces too wide to construct in tests |
|
|
522
521
|
| 3× duplicated `mockSession()` | agent-manager, record-observer, ui-observer tests | No shared test fixture |
|
|
523
522
|
| 3× duplicated `makeDeps()` | agent-tool, background-spawner, foreground-runner tests | No shared tool-deps fixture |
|
|
@@ -535,48 +534,20 @@ Consolidate duplicated mock factories into `test/helpers/`.
|
|
|
535
534
|
|
|
536
535
|
Impact: reduces test boilerplate; single source of truth for mock shapes; changes to dep interfaces propagate automatically.
|
|
537
536
|
|
|
538
|
-
### Step G: Inject IO collaborators into session-config (#132)
|
|
537
|
+
### Step G: Inject IO collaborators into session-config (#132) ✓ done
|
|
539
538
|
|
|
540
|
-
`assembleSessionConfig`
|
|
541
|
-
|
|
539
|
+
`assembleSessionConfig` now accepts `io: AssemblerIO` as a required parameter.
|
|
540
|
+
`index.ts` constructs the real `AssemblerIO` from direct imports via the `RunnerIO.assemblerIO` field (wired in Step H).
|
|
541
|
+
`session-config.test.ts` injects stubs — all 4 `vi.mock()` calls eliminated, assertions shifted to `SessionConfig` output properties.
|
|
542
542
|
|
|
543
|
-
Inject
|
|
543
|
+
### Step H: Inject SDK boundary into agent-runner (#133) ✓ done
|
|
544
544
|
|
|
545
|
-
|
|
546
|
-
export interface AssemblerIO {
|
|
547
|
-
preloadSkills: (skills: string[], cwd: string) => PreloadedSkill[];
|
|
548
|
-
buildMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
|
|
549
|
-
buildReadOnlyMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
|
|
550
|
-
buildAgentPrompt: (config: AgentPromptConfig, cwd: string, env: EnvInfo, parentPrompt: string, extras: PromptExtras) => string;
|
|
551
|
-
}
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
The production call site in `agent-runner.ts` passes the real implementations.
|
|
555
|
-
Tests pass stubs or let real implementations run against controlled inputs.
|
|
556
|
-
|
|
557
|
-
Impact: eliminates all 4 `vi.mock()` calls in `session-config.test.ts`; tests verify `SessionConfig` output properties instead of mock call arguments; the assembler becomes truly pure.
|
|
558
|
-
|
|
559
|
-
### Step H: Inject SDK boundary into agent-runner (#133)
|
|
560
|
-
|
|
561
|
-
`agent-runner.ts` has 7 module mocks because it imports `createAgentSession`, `DefaultResourceLoader`, `SessionManager`, and `SettingsManager` from the Pi SDK, plus `detectEnv`, `deriveSubagentSessionDir`, and `assembleSessionConfig` from sibling modules.
|
|
562
|
-
|
|
563
|
-
After Step G, `assembleSessionConfig` no longer needs mocking (its own IO is injected).
|
|
564
|
-
The remaining SDK dependencies can be injected via a narrow `RunnerIO` interface:
|
|
565
|
-
|
|
566
|
-
```typescript
|
|
567
|
-
export interface RunnerIO {
|
|
568
|
-
createSession: (opts: SessionOptions) => AgentSession;
|
|
569
|
-
createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoader;
|
|
570
|
-
createSessionManager: (cwd: string) => SessionManager;
|
|
571
|
-
detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
|
|
572
|
-
deriveSessionDir: (parentFile: string) => string;
|
|
573
|
-
}
|
|
574
|
-
```
|
|
545
|
+
`runAgent()` now accepts `io: RunnerIO` as a required parameter bundling all IO collaborators: `detectEnv`, `getAgentDir`, `createResourceLoader`, `deriveSessionDir`, `createSessionManager`, `createSettingsManager`, `createSession`, and `assemblerIO`.
|
|
575
546
|
|
|
576
|
-
|
|
577
|
-
|
|
547
|
+
`createAgentRunner(io: RunnerIO): AgentRunner` factory captures the boundary at construction time so `AgentManager` and the `AgentRunner` interface remain unchanged.
|
|
548
|
+
`index.ts` constructs the real `RunnerIO` from Pi SDK imports and sibling modules.
|
|
578
549
|
|
|
579
|
-
Impact:
|
|
550
|
+
Impact: all 7 `vi.mock()` calls eliminated from both `agent-runner.test.ts` and `agent-runner-extension-tools.test.ts`; tests verify behavior (turn limits, tool filtering, response collection) through injected stubs; SDK imports moved to the extension entry point.
|
|
580
551
|
|
|
581
552
|
### Step I: Reduce `as any` casts in tests (#134)
|
|
582
553
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 132
|
|
3
|
+
issue_title: "Inject IO collaborators into `assembleSessionConfig`"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Inject IO collaborators into session-config
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`assembleSessionConfig` is described as a pure configuration assembler, but it directly imports three IO-touching functions (`preloadSkills`, `buildMemoryBlock`, `buildReadOnlyMemoryBlock`) and one pure function (`buildAgentPrompt`).
|
|
11
|
+
This forces `session-config.test.ts` to use 4 `vi.mock()` calls, 8 hoisted mock functions, and assertions that verify internal call sequences rather than output properties.
|
|
12
|
+
The result is fragile tests that break on any internal restructuring even when observable behavior is unchanged.
|
|
13
|
+
|
|
14
|
+
## Goals
|
|
15
|
+
|
|
16
|
+
- Define an `AssemblerIO` interface bundling the four collaborators.
|
|
17
|
+
- Add `io: AssemblerIO` as a parameter to `assembleSessionConfig()`.
|
|
18
|
+
- Replace direct imports of the four functions with calls through `io`.
|
|
19
|
+
- Update the single production call site in `agent-runner.ts` to pass real implementations.
|
|
20
|
+
- Eliminate all 4 `vi.mock()` calls in `session-config.test.ts`.
|
|
21
|
+
- Shift test assertions toward output-property verification.
|
|
22
|
+
|
|
23
|
+
## Non-Goals
|
|
24
|
+
|
|
25
|
+
- SDK boundary injection into `agent-runner` (Step H, #133) — depends on this change but is deferred to its own issue.
|
|
26
|
+
- Consolidating shared test fixtures (#131) — independent refactor that can land before or after.
|
|
27
|
+
- Changing the behavior of `assembleSessionConfig` — this is a pure structural refactor.
|
|
28
|
+
- Injecting `getMemoryToolNames` / `getReadOnlyMemoryToolNames` — these are pure utility functions with no IO; they stay as direct imports.
|
|
29
|
+
|
|
30
|
+
## Background
|
|
31
|
+
|
|
32
|
+
### Current state
|
|
33
|
+
|
|
34
|
+
`session-config.ts` imports four functions used during assembly:
|
|
35
|
+
|
|
36
|
+
| Function | Module | IO? | Purpose in assembler |
|
|
37
|
+
| -------------------------- | ----------------- | ------------------------------ | --------------------------------------- |
|
|
38
|
+
| `preloadSkills` | `skill-loader.ts` | Yes (reads `.pi/skills` files) | Loads skill content into prompt extras |
|
|
39
|
+
| `buildMemoryBlock` | `memory.ts` | Yes (reads `MEMORY.md`) | Builds read-write memory prompt section |
|
|
40
|
+
| `buildReadOnlyMemoryBlock` | `memory.ts` | Yes (reads `MEMORY.md`) | Builds read-only memory prompt section |
|
|
41
|
+
| `buildAgentPrompt` | `prompts.ts` | No (pure) | Assembles final system prompt string |
|
|
42
|
+
|
|
43
|
+
The test file mocks all four via `vi.mock()` plus mocks `getMemoryToolNames` and `getReadOnlyMemoryToolNames` from `agent-types.ts` (pure functions that are mocked only for call-argument verification).
|
|
44
|
+
|
|
45
|
+
### Established DI pattern
|
|
46
|
+
|
|
47
|
+
`AgentManager` already injects `AgentRunner` via its constructor options — the same tell-don't-ask pattern used here.
|
|
48
|
+
`assembleSessionConfig` already receives an `AgentConfigLookup` registry by parameter (migrated in #80/#108), demonstrating the incremental injection approach.
|
|
49
|
+
|
|
50
|
+
### Architecture reference
|
|
51
|
+
|
|
52
|
+
Phase 8, Step G in `docs/architecture/architecture.md`.
|
|
53
|
+
|
|
54
|
+
### Constraints from AGENTS.md
|
|
55
|
+
|
|
56
|
+
- Keep scope tight; prefer small, reversible changes.
|
|
57
|
+
- Prefer explicit configuration over hidden behavior.
|
|
58
|
+
- Business logic should be pure functions — keep IO at the edges.
|
|
59
|
+
|
|
60
|
+
## Design Overview
|
|
61
|
+
|
|
62
|
+
### `AssemblerIO` interface
|
|
63
|
+
|
|
64
|
+
Defined in `session-config.ts` alongside the existing assembler types:
|
|
65
|
+
|
|
66
|
+
```typescript
|
|
67
|
+
export interface AssemblerIO {
|
|
68
|
+
preloadSkills: (skills: string[], cwd: string) => PreloadedSkill[];
|
|
69
|
+
buildMemoryBlock: (
|
|
70
|
+
name: string,
|
|
71
|
+
scope: MemoryScope,
|
|
72
|
+
cwd: string,
|
|
73
|
+
) => string;
|
|
74
|
+
buildReadOnlyMemoryBlock: (
|
|
75
|
+
name: string,
|
|
76
|
+
scope: MemoryScope,
|
|
77
|
+
cwd: string,
|
|
78
|
+
) => string;
|
|
79
|
+
buildAgentPrompt: (
|
|
80
|
+
config: AgentPromptConfig,
|
|
81
|
+
cwd: string,
|
|
82
|
+
env: EnvInfo,
|
|
83
|
+
parentPrompt?: string,
|
|
84
|
+
extras?: PromptExtras,
|
|
85
|
+
) => string;
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The interface uses the same parameter types as the real functions.
|
|
90
|
+
The assembler calls `io.preloadSkills(...)` etc. instead of the direct imports.
|
|
91
|
+
|
|
92
|
+
### Call site in `agent-runner.ts`
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { preloadSkills } from "./skill-loader.js";
|
|
96
|
+
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
97
|
+
import { buildAgentPrompt } from "./prompts.js";
|
|
98
|
+
|
|
99
|
+
const io: AssemblerIO = {
|
|
100
|
+
preloadSkills,
|
|
101
|
+
buildMemoryBlock,
|
|
102
|
+
buildReadOnlyMemoryBlock,
|
|
103
|
+
buildAgentPrompt,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const cfg = assembleSessionConfig(type, ctx, options, env, registry, io);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
The runner constructs the real IO object once and passes it through.
|
|
110
|
+
This keeps IO at the edge (runner) and makes the assembler a genuine pure function.
|
|
111
|
+
|
|
112
|
+
### Test-side stubs
|
|
113
|
+
|
|
114
|
+
Tests create a plain object with `vi.fn()` stubs satisfying `AssemblerIO`:
|
|
115
|
+
|
|
116
|
+
```typescript
|
|
117
|
+
const io: AssemblerIO = {
|
|
118
|
+
preloadSkills: vi.fn(() => []),
|
|
119
|
+
buildMemoryBlock: vi.fn(() => "memory block"),
|
|
120
|
+
buildReadOnlyMemoryBlock: vi.fn(() => "read-only memory block"),
|
|
121
|
+
buildAgentPrompt: vi.fn(() => "assembled system prompt"),
|
|
122
|
+
};
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
This replaces all four `vi.mock()` calls and the hoisted mocks for those modules.
|
|
126
|
+
|
|
127
|
+
### Pure utility functions stay as direct imports
|
|
128
|
+
|
|
129
|
+
`getMemoryToolNames` and `getReadOnlyMemoryToolNames` from `agent-types.ts` are pure functions (no IO, no filesystem access).
|
|
130
|
+
After the IO injection, the test's `vi.mock("../src/agent-types.js", ...)` can be removed and real implementations used.
|
|
131
|
+
Tests that previously controlled these mocks to verify call arguments will instead set up input tool names to produce the desired output from the real functions, then assert on the returned `SessionConfig.toolNames`.
|
|
132
|
+
|
|
133
|
+
## Module-Level Changes
|
|
134
|
+
|
|
135
|
+
### Modified files
|
|
136
|
+
|
|
137
|
+
1. `src/session-config.ts`
|
|
138
|
+
- Add `AssemblerIO` interface export.
|
|
139
|
+
- Add `io: AssemblerIO` parameter to `assembleSessionConfig()` (after `registry`).
|
|
140
|
+
- Replace `preloadSkills(...)` with `io.preloadSkills(...)`.
|
|
141
|
+
- Replace `buildMemoryBlock(...)` with `io.buildMemoryBlock(...)`.
|
|
142
|
+
- Replace `buildReadOnlyMemoryBlock(...)` with `io.buildReadOnlyMemoryBlock(...)`.
|
|
143
|
+
- Replace `buildAgentPrompt(...)` with `io.buildAgentPrompt(...)`.
|
|
144
|
+
- Remove imports of `preloadSkills`, `buildMemoryBlock`, `buildReadOnlyMemoryBlock`, `buildAgentPrompt`.
|
|
145
|
+
- Keep imports of `getMemoryToolNames`, `getReadOnlyMemoryToolNames` (pure, no change).
|
|
146
|
+
|
|
147
|
+
2. `src/agent-runner.ts`
|
|
148
|
+
- Add imports for `preloadSkills`, `buildMemoryBlock`, `buildReadOnlyMemoryBlock`, `buildAgentPrompt`.
|
|
149
|
+
- Import `AssemblerIO` type from `session-config.ts`.
|
|
150
|
+
- Construct `AssemblerIO` object from real implementations.
|
|
151
|
+
- Pass `io` to `assembleSessionConfig()`.
|
|
152
|
+
|
|
153
|
+
3. `test/session-config.test.ts`
|
|
154
|
+
- Remove all 4 `vi.mock()` calls and the corresponding hoisted mocks.
|
|
155
|
+
- Create `io` stub object with `vi.fn()` implementations.
|
|
156
|
+
- Pass `io` to every `assembleSessionConfig()` call.
|
|
157
|
+
- Update memory-section tests to use real `getMemoryToolNames` / `getReadOnlyMemoryToolNames`.
|
|
158
|
+
- Migrate mock-call assertions to output-property assertions where the output already captures the information.
|
|
159
|
+
|
|
160
|
+
## Test Impact Analysis
|
|
161
|
+
|
|
162
|
+
1. The IO injection enables testing `assembleSessionConfig` without any module mocking.
|
|
163
|
+
Tests can choose to inject real implementations with controlled inputs (integration-style) or stubs (unit-style).
|
|
164
|
+
Previously this was impossible without `vi.mock()`.
|
|
165
|
+
|
|
166
|
+
2. Several existing tests that only verified mock-call arguments become redundant once we verify the same information through output properties (e.g., "calls buildAgentPrompt with env, cwd, parentSystemPrompt, and extras" is redundant if we verify `result.systemPrompt` reflects those inputs).
|
|
167
|
+
These can be simplified or removed.
|
|
168
|
+
|
|
169
|
+
3. Tests for model resolution, isolated mode, thinking level, and unknown-type fallback stay as-is — they already assert output properties and are unaffected by the IO injection.
|
|
170
|
+
|
|
171
|
+
## TDD Order
|
|
172
|
+
|
|
173
|
+
1. **Define `AssemblerIO` and inject into `assembleSessionConfig`.**
|
|
174
|
+
Add the `AssemblerIO` interface to `session-config.ts`.
|
|
175
|
+
Add `io: AssemblerIO` as a required parameter.
|
|
176
|
+
Replace the 4 direct function calls with `io.*` calls.
|
|
177
|
+
Remove the 4 function imports from `session-config.ts`.
|
|
178
|
+
Add the 4 imports to `agent-runner.ts` and construct the `io` object at the call site.
|
|
179
|
+
Run `pnpm run check` to verify types compile.
|
|
180
|
+
Commit: `feat: inject IO collaborators into assembleSessionConfig (#132)`
|
|
181
|
+
|
|
182
|
+
2. **Migrate test file to use injected IO stubs.**
|
|
183
|
+
Create an `io` stub object with `vi.fn()` stubs matching the existing hoisted mocks' default return values.
|
|
184
|
+
Pass `io` to all `assembleSessionConfig()` calls.
|
|
185
|
+
Remove the 3 `vi.mock()` calls for `prompts.js`, `memory.js`, and `skill-loader.js`.
|
|
186
|
+
Remove the corresponding hoisted mock variables (`mockBuildAgentPrompt`, `mockBuildMemoryBlock`, `mockBuildReadOnlyMemoryBlock`, `mockPreloadSkills`).
|
|
187
|
+
Update `beforeEach` to reset the `io` stubs instead.
|
|
188
|
+
All existing tests pass with the same assertions (io stubs replace module mocks).
|
|
189
|
+
Commit: `test: replace vi.mock with injected IO stubs in session-config tests`
|
|
190
|
+
|
|
191
|
+
3. **Drop the `agent-types.js` mock; use real pure functions.**
|
|
192
|
+
Remove the `vi.mock("../src/agent-types.js", ...)` call and the `importOriginal` pattern.
|
|
193
|
+
Remove hoisted `mockGetMemoryToolNames` and `mockGetReadOnlyMemoryToolNames`.
|
|
194
|
+
Update memory-section tests to set up `mockGetToolNamesForType` return values that produce the desired output from the real `getMemoryToolNames` / `getReadOnlyMemoryToolNames`.
|
|
195
|
+
Assertions shift from "mock was called with Set" to "result.toolNames contains expected names".
|
|
196
|
+
Commit: `test: use real getMemoryToolNames in session-config tests`
|
|
197
|
+
|
|
198
|
+
4. **Shift remaining mock-call assertions to output-property checks.**
|
|
199
|
+
Replace `expect(io.buildAgentPrompt).toHaveBeenCalledWith(...)` with assertions on `result.systemPrompt` (requires io.buildAgentPrompt stub to echo identifying values).
|
|
200
|
+
Replace `expect(io.preloadSkills).toHaveBeenCalledWith(skillList, "/tmp")` with `result.extras.skillBlocks` checks (already partially present).
|
|
201
|
+
Remove test cases that are now fully redundant with output-based tests in the same describe block.
|
|
202
|
+
Clean up any unused imports and variables.
|
|
203
|
+
Commit: `test: verify output properties in session-config tests (#132)`
|
|
204
|
+
|
|
205
|
+
## Risks and Mitigations
|
|
206
|
+
|
|
207
|
+
| Risk | Mitigation |
|
|
208
|
+
| --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
209
|
+
| Adding a parameter to `assembleSessionConfig` breaks the `agent-runner.ts` call site | Only one production call site exists; updated in the same commit (step 1). `pnpm run check` verifies. |
|
|
210
|
+
| Removing `vi.mock()` causes tests to accidentally call real IO functions | The real functions are no longer imported by `session-config.ts` after step 1. The module simply doesn't reach them. Vitest will error if any unmocked import is called. |
|
|
211
|
+
| Using real `getMemoryToolNames` / `getReadOnlyMemoryToolNames` makes tests depend on their implementation | These are pure, stable utility functions (return tool names from a set). Their behavior is well-defined and unlikely to change. Using real implementations is more robust than mocking. |
|
|
212
|
+
| Step 2 touches 40+ call sites in the test file | All changes are mechanical (add `, io` argument). A find-and-replace handles it. Each call already passes `mockAgentLookup` as the last arg; the new arg follows the same pattern. |
|
|
213
|
+
|
|
214
|
+
## Open Questions
|
|
215
|
+
|
|
216
|
+
- Should `AssemblerIO` be co-located in `session-config.ts` or extracted to a separate `session-config-types.ts`?
|
|
217
|
+
The interface is small (4 methods) and tightly coupled to the assembler.
|
|
218
|
+
Co-location in `session-config.ts` follows the existing pattern (`AssemblerContext`, `AssemblerOptions`, `SessionConfig` are all in the same file).
|
|
219
|
+
Extract only if it grows or gains consumers beyond `agent-runner.ts`.
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 133
|
|
3
|
+
issue_title: "Inject SDK boundary into `agent-runner`"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Inject SDK boundary into agent-runner
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`agent-runner.ts` directly imports five Pi SDK symbols (`createAgentSession`, `DefaultResourceLoader`, `getAgentDir`, `SessionManager`, `SettingsManager`) and two sibling modules (`detectEnv`, `deriveSubagentSessionDir`).
|
|
11
|
+
It also imports four functions (`preloadSkills`, `buildMemoryBlock`, `buildReadOnlyMemoryBlock`, `buildAgentPrompt`) solely to construct the `AssemblerIO` object introduced in #132.
|
|
12
|
+
This forces `agent-runner.test.ts` to use 7 `vi.mock()` calls, a `vi.hoisted()` block with 5+ mock factories, and a `beforeEach` that manually resets 6+ mocks.
|
|
13
|
+
Tests verify internal call patterns ("defaultResourceLoaderCtor was called with `noContextFiles: true`") rather than behavioral outcomes, making any internal restructuring break multiple tests without changing observable behavior.
|
|
14
|
+
The same 7-mock pattern is duplicated in `agent-runner-extension-tools.test.ts`.
|
|
15
|
+
|
|
16
|
+
## Goals
|
|
17
|
+
|
|
18
|
+
- Define a `RunnerIO` interface bundling all SDK and IO collaborators used by `runAgent()`.
|
|
19
|
+
- Add `io: RunnerIO` as a parameter to `runAgent()`.
|
|
20
|
+
- Provide a `createAgentRunner(io: RunnerIO): AgentRunner` factory so the `AgentRunner` interface and `AgentManager` remain unchanged.
|
|
21
|
+
- Replace direct SDK and sibling-module imports in `runAgent()` with calls through `io`.
|
|
22
|
+
- Update the wiring in `index.ts` to construct a real `RunnerIO` and use `createAgentRunner()`.
|
|
23
|
+
- Eliminate all 7 `vi.mock()` calls in `agent-runner.test.ts`.
|
|
24
|
+
- Eliminate all 7 `vi.mock()` calls in `agent-runner-extension-tools.test.ts`.
|
|
25
|
+
- Shift test assertions toward behavioral outcomes (turn limits enforced, tool filtering correct, response text collected).
|
|
26
|
+
|
|
27
|
+
## Non-Goals
|
|
28
|
+
|
|
29
|
+
- Changing `resumeAgent` — it receives an already-created `AgentSession` and has no SDK/IO deps to inject.
|
|
30
|
+
- Injecting `assembleSessionConfig` itself — the function is pure (after #132) and stays as a direct import; only its `AssemblerIO` collaborators move into `RunnerIO`.
|
|
31
|
+
- Injecting `getMemoryToolNames` / `getReadOnlyMemoryToolNames` — these are pure utility functions with no IO; they remain as direct imports in `session-config.ts`.
|
|
32
|
+
- Refactoring `filterActiveTools` or the turn-limit logic — out of scope.
|
|
33
|
+
- Consolidating shared test fixtures (#131) — independent work.
|
|
34
|
+
|
|
35
|
+
## Background
|
|
36
|
+
|
|
37
|
+
### Prerequisite
|
|
38
|
+
|
|
39
|
+
Issue #132 (inject IO into session-config) is closed.
|
|
40
|
+
`assembleSessionConfig` now receives an `AssemblerIO` parameter and no longer imports IO functions directly.
|
|
41
|
+
However, `agent-runner.ts` still imports those four functions to construct the `AssemblerIO` object, and the SDK factories remain as direct imports.
|
|
42
|
+
|
|
43
|
+
### Current vi.mock inventory in agent-runner.test.ts
|
|
44
|
+
|
|
45
|
+
| # | Module | Symbols mocked | Why mocked |
|
|
46
|
+
| --- | --------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------- |
|
|
47
|
+
| 1 | `@earendil-works/pi-coding-agent` | `createAgentSession`, `DefaultResourceLoader`, `getAgentDir`, `SessionManager`, `SettingsManager` | SDK constructors and factories |
|
|
48
|
+
| 2 | `../src/agent-types.js` | `getMemoryToolNames`, `getReadOnlyMemoryToolNames` | Pure functions used by session-config |
|
|
49
|
+
| 3 | `../src/env.js` | `detectEnv` | Async IO (shell exec) |
|
|
50
|
+
| 4 | `../src/prompts.js` | `buildAgentPrompt` | Relayed to AssemblerIO |
|
|
51
|
+
| 5 | `../src/memory.js` | `buildMemoryBlock`, `buildReadOnlyMemoryBlock` | Relayed to AssemblerIO |
|
|
52
|
+
| 6 | `../src/skill-loader.js` | `preloadSkills` | Relayed to AssemblerIO |
|
|
53
|
+
| 7 | `../src/session-dir.js` | `deriveSubagentSessionDir` | Path derivation |
|
|
54
|
+
|
|
55
|
+
`agent-runner-extension-tools.test.ts` has an identical set.
|
|
56
|
+
|
|
57
|
+
### Established DI patterns
|
|
58
|
+
|
|
59
|
+
- `AgentManager` already receives `AgentRunner` via constructor injection — the same boundary this issue pushes down one layer.
|
|
60
|
+
- `AssemblerIO` (#132) bundles four IO collaborators into a single injectable interface.
|
|
61
|
+
- `AgentManagerLike` in `service-adapter.ts` defines a narrow interface for the concrete `AgentManager` class, avoiding coupling to the concrete type.
|
|
62
|
+
|
|
63
|
+
### Architecture reference
|
|
64
|
+
|
|
65
|
+
Phase 8, Step H in `docs/architecture/architecture.md`.
|
|
66
|
+
|
|
67
|
+
### Constraints from AGENTS.md
|
|
68
|
+
|
|
69
|
+
- Keep scope tight; prefer small, reversible changes.
|
|
70
|
+
- Prefer explicit configuration over hidden behavior.
|
|
71
|
+
- Business logic should be pure functions — keep IO at the edges.
|
|
72
|
+
- Keep Pi SDK imports out of business-logic modules.
|
|
73
|
+
|
|
74
|
+
## Design Overview
|
|
75
|
+
|
|
76
|
+
### `RunnerIO` interface
|
|
77
|
+
|
|
78
|
+
Defined in `agent-runner.ts` alongside the existing runner types.
|
|
79
|
+
Bundles all IO dependencies that `runAgent()` uses:
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
/** Minimal resource-loader contract used by the runner. */
|
|
83
|
+
export interface ResourceLoaderLike {
|
|
84
|
+
reload(): Promise<void>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Minimal session-manager contract used by the runner. */
|
|
88
|
+
export interface SessionManagerLike {
|
|
89
|
+
newSession(opts: { parentSession?: string }): void;
|
|
90
|
+
getSessionFile(): string | undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Options passed to RunnerIO.createResourceLoader. */
|
|
94
|
+
export interface ResourceLoaderOptions {
|
|
95
|
+
cwd: string;
|
|
96
|
+
agentDir: string;
|
|
97
|
+
noExtensions?: boolean;
|
|
98
|
+
noSkills?: boolean;
|
|
99
|
+
noPromptTemplates?: boolean;
|
|
100
|
+
noThemes?: boolean;
|
|
101
|
+
noContextFiles?: boolean;
|
|
102
|
+
systemPromptOverride?: () => string;
|
|
103
|
+
appendSystemPromptOverride?: () => unknown[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Options passed to RunnerIO.createSession. */
|
|
107
|
+
export interface CreateSessionOptions {
|
|
108
|
+
cwd: string;
|
|
109
|
+
agentDir: string;
|
|
110
|
+
sessionManager: SessionManagerLike;
|
|
111
|
+
settingsManager: unknown;
|
|
112
|
+
modelRegistry: unknown;
|
|
113
|
+
model?: unknown;
|
|
114
|
+
tools: string[];
|
|
115
|
+
resourceLoader: ResourceLoaderLike;
|
|
116
|
+
thinkingLevel?: ThinkingLevel;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* IO boundary injected into runAgent().
|
|
121
|
+
*
|
|
122
|
+
* Decouples the runner from direct Pi SDK imports and sibling-module IO,
|
|
123
|
+
* making it testable via plain stub objects without vi.mock().
|
|
124
|
+
*/
|
|
125
|
+
export interface RunnerIO {
|
|
126
|
+
detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
|
|
127
|
+
getAgentDir: () => string;
|
|
128
|
+
createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
|
|
129
|
+
deriveSessionDir: (
|
|
130
|
+
parentSessionFile: string | undefined,
|
|
131
|
+
effectiveCwd: string,
|
|
132
|
+
) => string;
|
|
133
|
+
createSessionManager: (
|
|
134
|
+
cwd: string,
|
|
135
|
+
sessionDir: string,
|
|
136
|
+
) => SessionManagerLike;
|
|
137
|
+
createSettingsManager: (cwd: string, agentDir: string) => unknown;
|
|
138
|
+
createSession: (
|
|
139
|
+
opts: CreateSessionOptions,
|
|
140
|
+
) => Promise<{ session: AgentSession }>;
|
|
141
|
+
assemblerIO: AssemblerIO;
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The interface has 8 fields (7 functions + 1 nested `AssemblerIO`).
|
|
146
|
+
All 8 are consumed by `runAgent()` — no field is relayed without use.
|
|
147
|
+
|
|
148
|
+
### `createAgentRunner` factory
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
export function createAgentRunner(io: RunnerIO): AgentRunner {
|
|
152
|
+
return {
|
|
153
|
+
run: (snapshot, type, prompt, options) =>
|
|
154
|
+
runAgent(snapshot, type, prompt, options, io),
|
|
155
|
+
resume: resumeAgent,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
This keeps the `AgentRunner` interface unchanged.
|
|
161
|
+
`AgentManager` continues to receive an `AgentRunner` — it never sees `RunnerIO`.
|
|
162
|
+
|
|
163
|
+
### Call site in `index.ts`
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
import {
|
|
167
|
+
createAgentSession,
|
|
168
|
+
DefaultResourceLoader,
|
|
169
|
+
getAgentDir,
|
|
170
|
+
SessionManager,
|
|
171
|
+
SettingsManager,
|
|
172
|
+
} from "@earendil-works/pi-coding-agent";
|
|
173
|
+
import { detectEnv } from "./env.js";
|
|
174
|
+
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
175
|
+
import { buildAgentPrompt } from "./prompts.js";
|
|
176
|
+
import { deriveSubagentSessionDir } from "./session-dir.js";
|
|
177
|
+
import { preloadSkills } from "./skill-loader.js";
|
|
178
|
+
|
|
179
|
+
const runnerIO: RunnerIO = {
|
|
180
|
+
detectEnv,
|
|
181
|
+
getAgentDir,
|
|
182
|
+
createResourceLoader: (opts) => new DefaultResourceLoader(opts),
|
|
183
|
+
deriveSessionDir: deriveSubagentSessionDir,
|
|
184
|
+
createSessionManager: (cwd, dir) => SessionManager.create(cwd, dir),
|
|
185
|
+
createSettingsManager: (cwd, dir) => SettingsManager.create(cwd, dir),
|
|
186
|
+
createSession: createAgentSession,
|
|
187
|
+
assemblerIO: {
|
|
188
|
+
preloadSkills,
|
|
189
|
+
buildMemoryBlock,
|
|
190
|
+
buildReadOnlyMemoryBlock,
|
|
191
|
+
buildAgentPrompt,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const manager = new AgentManager({
|
|
196
|
+
runner: createAgentRunner(runnerIO),
|
|
197
|
+
// ... rest unchanged
|
|
198
|
+
});
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
SDK and IO imports move from `agent-runner.ts` to `index.ts` — the extension entry point, which is the natural IO edge.
|
|
202
|
+
|
|
203
|
+
### Test-side stubs
|
|
204
|
+
|
|
205
|
+
Tests create a plain `RunnerIO` object with `vi.fn()` stubs:
|
|
206
|
+
|
|
207
|
+
```typescript
|
|
208
|
+
function createRunnerIO(): RunnerIO {
|
|
209
|
+
return {
|
|
210
|
+
detectEnv: vi.fn(async () => ({
|
|
211
|
+
isGitRepo: false,
|
|
212
|
+
branch: "",
|
|
213
|
+
platform: "linux",
|
|
214
|
+
})),
|
|
215
|
+
getAgentDir: vi.fn(() => "/mock/agent-dir"),
|
|
216
|
+
createResourceLoader: vi.fn(() => ({ reload: vi.fn() })),
|
|
217
|
+
deriveSessionDir: vi.fn(() => "/mock/session-dir/tasks"),
|
|
218
|
+
createSessionManager: vi.fn(() => ({
|
|
219
|
+
newSession: vi.fn(),
|
|
220
|
+
getSessionFile: vi.fn(() => "/sessions/child.jsonl"),
|
|
221
|
+
})),
|
|
222
|
+
createSettingsManager: vi.fn(() => ({ kind: "settings-manager" })),
|
|
223
|
+
createSession: vi.fn(),
|
|
224
|
+
assemblerIO: {
|
|
225
|
+
preloadSkills: vi.fn(() => []),
|
|
226
|
+
buildMemoryBlock: vi.fn(() => ""),
|
|
227
|
+
buildReadOnlyMemoryBlock: vi.fn(() => ""),
|
|
228
|
+
buildAgentPrompt: vi.fn(() => "system prompt"),
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
This replaces all 7 `vi.mock()` calls, the `vi.hoisted()` block, and most of the `beforeEach` resets.
|
|
235
|
+
Each test calls `runAgent(snapshot, type, prompt, options, io)` directly with a stub `io`.
|
|
236
|
+
|
|
237
|
+
### Interaction verification — consumer call site (Tell-Don't-Ask check)
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
// In index.ts — the consumer constructs RunnerIO and hands it off:
|
|
241
|
+
const runnerIO: RunnerIO = { detectEnv, getAgentDir, ... };
|
|
242
|
+
const manager = new AgentManager({
|
|
243
|
+
runner: createAgentRunner(runnerIO),
|
|
244
|
+
});
|
|
245
|
+
// AgentManager calls runner.run(...) — never reaches through to runnerIO.
|
|
246
|
+
// Tell-Don't-Ask: ✓ Manager tells runner to run; runner uses its own IO.
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
### Pure functions stay as direct imports
|
|
250
|
+
|
|
251
|
+
`assembleSessionConfig` (pure after #132), `filterActiveTools` (module-private), `normalizeMaxTurns` (pure exported), `collectResponseText`, `getLastAssistantText`, and `forwardAbortSignal` remain as direct code — they have no IO dependencies.
|
|
252
|
+
|
|
253
|
+
`getMemoryToolNames` / `getReadOnlyMemoryToolNames` in `session-config.ts` remain as direct imports (pure, no IO).
|
|
254
|
+
The `vi.mock("../src/agent-types.js", ...)` in both test files can be removed because the mock agent config has no `memory` field, so the memory branch in `assembleSessionConfig` is never entered and those functions are never called.
|
|
255
|
+
|
|
256
|
+
## Module-Level Changes
|
|
257
|
+
|
|
258
|
+
### Modified files
|
|
259
|
+
|
|
260
|
+
1. `src/agent-runner.ts`
|
|
261
|
+
- Add `RunnerIO`, `ResourceLoaderLike`, `SessionManagerLike`, `ResourceLoaderOptions`, `CreateSessionOptions` interface exports.
|
|
262
|
+
- Add `createAgentRunner(io: RunnerIO): AgentRunner` factory export.
|
|
263
|
+
- Add `io: RunnerIO` parameter to `runAgent()`.
|
|
264
|
+
- Replace `detectEnv(...)` with `io.detectEnv(...)`.
|
|
265
|
+
- Replace `getAgentDir()` with `io.getAgentDir()`.
|
|
266
|
+
- Replace `new DefaultResourceLoader(...)` with `io.createResourceLoader(...)`.
|
|
267
|
+
- Replace `deriveSubagentSessionDir(...)` with `io.deriveSessionDir(...)`.
|
|
268
|
+
- Replace `SessionManager.create(...)` with `io.createSessionManager(...)`.
|
|
269
|
+
- Replace `SettingsManager.create(...)` with `io.createSettingsManager(...)`.
|
|
270
|
+
- Replace `createAgentSession(...)` with `io.createSession(...)`.
|
|
271
|
+
- Replace inline `AssemblerIO` construction with `io.assemblerIO`.
|
|
272
|
+
- Remove imports: `createAgentSession`, `DefaultResourceLoader`, `getAgentDir`, `SessionManager`, `SettingsManager` from `@earendil-works/pi-coding-agent`; `detectEnv` from `./env.js`; `deriveSubagentSessionDir` from `./session-dir.js`; `preloadSkills` from `./skill-loader.js`; `buildMemoryBlock`, `buildReadOnlyMemoryBlock` from `./memory.js`; `buildAgentPrompt` from `./prompts.js`.
|
|
273
|
+
- Keep imports: `type AgentSession`, `type AgentSessionEvent` from SDK (used in function signatures and event handling); `type AssemblerIO` from `./session-config.js`; `assembleSessionConfig` from `./session-config.js`; `extractText` from `./context.js`.
|
|
274
|
+
|
|
275
|
+
2. `src/index.ts`
|
|
276
|
+
- Add imports: `detectEnv` from `./env.js`; `deriveSubagentSessionDir` from `./session-dir.js`; `preloadSkills` from `./skill-loader.js`; `buildMemoryBlock`, `buildReadOnlyMemoryBlock` from `./memory.js`; `buildAgentPrompt` from `./prompts.js`.
|
|
277
|
+
- Add import: `createAgentRunner`, `type RunnerIO` from `./agent-runner.js`.
|
|
278
|
+
- Remove import: `runAgent` from `./agent-runner.js` (replaced by factory).
|
|
279
|
+
- Construct `runnerIO` object from real implementations.
|
|
280
|
+
- Replace `runner: { run: runAgent, resume: resumeAgent }` with `runner: createAgentRunner(runnerIO)`.
|
|
281
|
+
|
|
282
|
+
3. `test/agent-runner.test.ts`
|
|
283
|
+
- Remove all 7 `vi.mock()` calls and the `vi.hoisted()` block.
|
|
284
|
+
- Add `createRunnerIO()` factory function returning a stub `RunnerIO`.
|
|
285
|
+
- Pass `io` to all `runAgent()` calls.
|
|
286
|
+
- Simplify `beforeEach` to reset `io.createSession` (the only mock that needs per-test setup).
|
|
287
|
+
- Remove `mockAgentLookup.resolveAgentConfig` and `mockAgentLookup.getToolNamesForType` resets that are now unnecessary.
|
|
288
|
+
- Update assertions that verify SDK constructor arguments (e.g., `defaultResourceLoaderCtor` calls) to verify `io.createResourceLoader` calls instead.
|
|
289
|
+
- Remove the `agent-types.js` mock — pure functions run against controlled inputs.
|
|
290
|
+
|
|
291
|
+
4. `test/agent-runner-extension-tools.test.ts`
|
|
292
|
+
- Same structural changes as `agent-runner.test.ts`: remove all 7 `vi.mock()` calls, inject `RunnerIO` stubs.
|
|
293
|
+
- Keep the `createSessionWithExtensionToolRegistration` helper — it creates mock sessions for testing post-bind tool filtering, which is behavioral.
|
|
294
|
+
- Update assertions to use `io.createResourceLoader` / `io.createSession` stubs.
|
|
295
|
+
|
|
296
|
+
### Unchanged files
|
|
297
|
+
|
|
298
|
+
- `src/agent-manager.ts` — receives `AgentRunner` via injection; unaffected by `RunnerIO`.
|
|
299
|
+
- `test/agent-manager.test.ts` — already injects a mock `AgentRunner`; unaffected.
|
|
300
|
+
- `src/session-config.ts` — pure function, already receives `AssemblerIO`; unaffected.
|
|
301
|
+
- `test/session-config.test.ts` — tests the pure assembler directly; unaffected.
|
|
302
|
+
- `test/agent-runner-settings.test.ts` — tests `normalizeMaxTurns` (pure, no mocks); unaffected.
|
|
303
|
+
- `test/print-mode.test.ts` — mocks `runAgent` itself at the module level; unaffected (it tests `index.ts` notification wiring, not the runner internals).
|
|
304
|
+
|
|
305
|
+
## Test Impact Analysis
|
|
306
|
+
|
|
307
|
+
1. The `RunnerIO` injection enables testing `runAgent` without any module mocking.
|
|
308
|
+
Tests create plain stub objects satisfying `RunnerIO` — no `vi.mock()`, no `vi.hoisted()`, no module-level mock variable management.
|
|
309
|
+
This was previously impossible because `runAgent` hard-imported SDK constructors.
|
|
310
|
+
|
|
311
|
+
2. Several existing tests that verify mock constructor arguments become redundant or shift to verifying `io.*` stub calls:
|
|
312
|
+
- "passes effective cwd and agentDir to the loader and settings manager" → verifies `io.createResourceLoader` and `io.createSettingsManager` were called with expected args (simpler, no `defaultResourceLoaderCtor` indirection).
|
|
313
|
+
- "suppresses AGENTS.md/CLAUDE.md/APPEND_SYSTEM.md for subagents" → verifies `io.createResourceLoader` was called with `noContextFiles: true` and an `appendSystemPromptOverride` that returns `[]`.
|
|
314
|
+
|
|
315
|
+
3. Tests for turn-limit enforcement, abort forwarding, and response-text collection stay as-is — they already test behavioral outcomes through the mock session, not through SDK mock call patterns.
|
|
316
|
+
|
|
317
|
+
4. The extension-tools tests (Patch 2) remain behavioral — they verify `setActiveToolsByName` calls before/after `bindExtensions`.
|
|
318
|
+
The only change is how the session is created (via `io.createSession` stub instead of a module mock).
|
|
319
|
+
|
|
320
|
+
5. The `agent-types.js` mock can be removed from both test files because the mock agent configs have no `memory` field, so the code path through `getMemoryToolNames` / `getReadOnlyMemoryToolNames` is never reached.
|
|
321
|
+
|
|
322
|
+
## TDD Order
|
|
323
|
+
|
|
324
|
+
1. **Define `RunnerIO` and `createAgentRunner`; inject IO into `runAgent`.**
|
|
325
|
+
Add the `RunnerIO`, `ResourceLoaderLike`, `SessionManagerLike`, `ResourceLoaderOptions`, and `CreateSessionOptions` interfaces to `agent-runner.ts`.
|
|
326
|
+
Add `io: RunnerIO` parameter to `runAgent()`.
|
|
327
|
+
Add `createAgentRunner(io)` factory export.
|
|
328
|
+
Replace all direct SDK and IO imports with `io.*` calls inside `runAgent()`.
|
|
329
|
+
Remove the now-unused direct imports.
|
|
330
|
+
Update `index.ts` to construct `runnerIO` from real implementations and use `createAgentRunner(runnerIO)`.
|
|
331
|
+
Run `pnpm run check` to verify types compile.
|
|
332
|
+
Commit: `feat: inject SDK boundary into agent-runner via RunnerIO (#133)`
|
|
333
|
+
|
|
334
|
+
2. **Migrate `agent-runner.test.ts` to use injected `RunnerIO` stubs.**
|
|
335
|
+
Add `createRunnerIO()` helper returning a fully-stubbed `RunnerIO`.
|
|
336
|
+
Pass `io` to all `runAgent()` calls.
|
|
337
|
+
Remove all 7 `vi.mock()` calls and the `vi.hoisted()` block.
|
|
338
|
+
Simplify `beforeEach` to reset only `io.createSession`.
|
|
339
|
+
Update assertions that referenced hoisted mocks (e.g., `defaultResourceLoaderCtor`, `sessionManagerCreate`, `settingsManagerCreate`, `getAgentDir`) to reference `io.*` stubs.
|
|
340
|
+
Remove the `mockAgentLookup` mock resets that are now unnecessary.
|
|
341
|
+
All existing tests pass with equivalent assertions.
|
|
342
|
+
Commit: `test: replace vi.mock with RunnerIO stubs in agent-runner tests (#133)`
|
|
343
|
+
|
|
344
|
+
3. **Migrate `agent-runner-extension-tools.test.ts` to use injected `RunnerIO` stubs.**
|
|
345
|
+
Same structural changes as step 2: remove all 7 `vi.mock()` calls, inject `RunnerIO` stubs.
|
|
346
|
+
Keep `createSessionWithExtensionToolRegistration` helper (tests tool filtering behavior).
|
|
347
|
+
Simplify `beforeEach` and update stub references.
|
|
348
|
+
Commit: `test: replace vi.mock with RunnerIO stubs in extension-tools tests (#133)`
|
|
349
|
+
|
|
350
|
+
4. **Shift constructor-argument assertions to behavioral checks.**
|
|
351
|
+
In `agent-runner.test.ts`, update tests that verify internal SDK call arguments:
|
|
352
|
+
- Replace `expect(defaultResourceLoaderCtor).toHaveBeenCalledWith(expect.objectContaining({...}))` with `expect(io.createResourceLoader).toHaveBeenCalledWith(expect.objectContaining({...}))`.
|
|
353
|
+
- Where the assertion only verified plumbing (e.g., "settings manager gets the right cwd"), simplify to a behavioral assertion or remove if covered by other tests.
|
|
354
|
+
- Keep assertions that verify meaningful configuration decisions (e.g., `noContextFiles: true`, `appendSystemPromptOverride` returns `[]`).
|
|
355
|
+
Run full test suite.
|
|
356
|
+
Commit: `test: shift agent-runner assertions toward behavioral checks (#133)`
|
|
357
|
+
|
|
358
|
+
## Risks and Mitigations
|
|
359
|
+
|
|
360
|
+
| Risk | Mitigation |
|
|
361
|
+
| ------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
362
|
+
| `RunnerIO` at 8 fields may seem wide | All 8 are consumed by the single consumer (`runAgent`). No field is relayed without use. The interface represents a genuine IO boundary — further narrowing would require splitting `runAgent` itself (out of scope). |
|
|
363
|
+
| Removing the `agent-types.js` mock could cause failures if a test unexpectedly enters the memory branch | The mock agent config has no `memory` field (`undefined`), so the memory branch is guarded by `if (agentConfig.memory)`. Verified by reading the test's `resolveAgentConfig` mock return value. |
|
|
364
|
+
| `index.ts` accumulates many new imports | The imports move from `agent-runner.ts` to `index.ts` — the extension entry point is the natural IO edge. The total import count across the two files is unchanged. |
|
|
365
|
+
| `createAgentRunner` factory adds indirection | The factory is a one-liner that captures `io` in a closure. The `AgentRunner` interface and `AgentManager` are completely unchanged. No new abstraction layer — just a construction-time binding. |
|
|
366
|
+
| Steps 2–3 touch many call sites in two test files (add `, io` argument) | All changes are mechanical. Each `runAgent(snapshot, type, prompt, {...})` becomes `runAgent(snapshot, type, prompt, {...}, io)`. A single find-and-replace handles it. |
|
|
367
|
+
| `print-mode.test.ts` mocks `runAgent` at the module level — does the new `io` parameter break it? | `print-mode.test.ts` mocks the entire `runAgent` export with `vi.mock("../src/agent-runner.js", ...)`. The mock replaces the function entirely, so the new parameter has no effect on that test. |
|
|
368
|
+
|
|
369
|
+
## Open Questions
|
|
370
|
+
|
|
371
|
+
- Should `RunnerIO` live in `agent-runner.ts` or be extracted to a separate types file?
|
|
372
|
+
The interface is tightly coupled to `runAgent()` — co-location follows the `AssemblerIO` precedent in `session-config.ts`.
|
|
373
|
+
Extract only if a second consumer appears.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 131
|
|
3
|
+
issue_title: Consolidate shared test fixtures
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #131 — Consolidate shared test fixtures
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-22T11:30:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned and implemented the consolidation of six duplicated test factories into two shared helpers (`createMockSession` in `test/helpers/mock-session.ts`, `createToolDeps` in `test/helpers/make-deps.ts`).
|
|
13
|
+
All 715 tests pass, released as `pi-subagents-v6.9.4`.
|
|
14
|
+
The implementation was a pure test refactor with no production code changes.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- The lift-and-shift approach worked cleanly: create factory → migrate one consumer at a time → verify green after each step.
|
|
21
|
+
Each migration commit was small and isolated, making failures easy to diagnose.
|
|
22
|
+
- Structural typing as a strategy proved out — `createToolDeps()` returns `AgentToolDeps` (the superset), and `spawnBackground(deps, ...)` and `runForeground(deps, ...)` accept their narrow `BackgroundDeps`/`ForegroundDeps` interfaces without any casting.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `missing-context` — Plan used `registry.resolve("general-purpose", "/dir")` in the `make-deps.test.ts` test, but `AgentTypeRegistry` has no `resolve` method — the correct method is `resolveAgentConfig()`.
|
|
27
|
+
Impact: one test failure during step 5 red→green, fixed immediately with no rework.
|
|
28
|
+
|
|
29
|
+
- `missing-context` — Default values differed between the old narrow factories and the new shared factory: `"bg-1"` vs `"agent-1"` for spawn IDs (`background-spawner.test.ts`), `"Task done."` vs `"All done."` for result text (`foreground-runner.test.ts`).
|
|
30
|
+
Impact: two test failures in step 7, one in step 8, each requiring assertion updates before the migration step could pass.
|
|
31
|
+
|
|
32
|
+
- `missing-context` — `MockSession` interface used `ReturnType<typeof vi.fn>` which expands to `Mock<Procedure | Constructable>` in Vitest v4 — a union type TypeScript cannot call.
|
|
33
|
+
Impact: `pnpm run check` failed after all TDD steps were done, requiring a separate `style:` commit to switch to explicitly parameterized `Mock<() => void>` etc.
|
|
34
|
+
|
|
35
|
+
- `missing-context` — Removed the `AgentToolDeps` import from `agent-tool.test.ts` without checking that the `execute()` helper still referenced it.
|
|
36
|
+
Impact: caught in the same `pnpm run check` pass, fixed in the same `style:` commit.
|
|
37
|
+
|
|
38
|
+
#### What caused friction (user side)
|
|
39
|
+
|
|
40
|
+
- No user-side friction observed.
|
|
41
|
+
The plan was unambiguous, and the session ran autonomously through all 8 TDD steps plus post-checks without intervention.
|
|
42
|
+
|
|
43
|
+
### Changes made
|
|
44
|
+
|
|
45
|
+
1. `.pi/skills/testing/SKILL.md` — added TDD planning rule for diffing default values when consolidating duplicate test factories.
|
|
46
|
+
2. `.pi/skills/testing/SKILL.md` — added Vitest mock pattern rule for typing mock fields with `Mock<specific-signature>` instead of `ReturnType<typeof vi.fn>`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 132
|
|
3
|
+
issue_title: "Inject IO collaborators into `assembleSessionConfig`"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #132 — Inject IO collaborators into `assembleSessionConfig`
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-22T12:25:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Defined an `AssemblerIO` interface bundling four IO/prompt collaborators, injected it into `assembleSessionConfig`, and updated `agent-runner.ts` to pass real implementations.
|
|
13
|
+
Eliminated all 4 `vi.mock()` calls in `session-config.test.ts`, flattened the `vi.hoisted()` block into plain `vi.fn()` declarations, and shifted assertions from mock-call verification to output-property checks.
|
|
14
|
+
Released as `pi-subagents-v6.10.0`.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- Perl two-pass replacement (multi-line then single-line) handled 40+ `assembleSessionConfig` call-site updates in one command with zero manual errors.
|
|
21
|
+
- Flattening `vi.hoisted()` into regular `vi.fn()` declarations in step 3 was a clean simplification — hoisting was only needed when the mocks were referenced inside `vi.mock()` factories.
|
|
22
|
+
- Real `getMemoryToolNames` / `getReadOnlyMemoryToolNames` worked as drop-in replacements with no test rework needed — the pure functions' behavior matched what the mocks were configured to return for all existing test scenarios.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `missing-context` — `mockBuildAgentPrompt` was declared as `vi.fn(() => "assembled system prompt")` which inferred `Mock<() => string>`.
|
|
27
|
+
When step 4 used `mockImplementationOnce` with a parameterized function, TypeScript rejected it.
|
|
28
|
+
The testing skill already documents `Mock<specific-signature>` for this exact case.
|
|
29
|
+
Impact: one type-check failure, fixed by adding `Mock<AssemblerIO["buildAgentPrompt"]>` annotation; added friction but no rework.
|
|
30
|
+
|
|
31
|
+
#### What caused friction (user side)
|
|
32
|
+
|
|
33
|
+
- Nothing notable — standard prompt-template workflow with no corrections needed.
|
package/package.json
CHANGED
package/src/agent-runner.ts
CHANGED
|
@@ -6,18 +6,12 @@ import type { Model } from "@earendil-works/pi-ai";
|
|
|
6
6
|
import {
|
|
7
7
|
type AgentSession,
|
|
8
8
|
type AgentSessionEvent,
|
|
9
|
-
createAgentSession,
|
|
10
|
-
DefaultResourceLoader,
|
|
11
|
-
getAgentDir,
|
|
12
|
-
SessionManager,
|
|
13
|
-
SettingsManager,
|
|
14
9
|
} from "@earendil-works/pi-coding-agent";
|
|
15
10
|
import type { AgentConfigLookup } from "./agent-types.js";
|
|
16
11
|
import { extractText } from "./context.js";
|
|
17
|
-
import {
|
|
12
|
+
import type { EnvInfo } from "./env.js";
|
|
18
13
|
import type { ParentSnapshot } from "./parent-snapshot.js";
|
|
19
|
-
import { assembleSessionConfig } from "./session-config.js";
|
|
20
|
-
import { deriveSubagentSessionDir } from "./session-dir.js";
|
|
14
|
+
import { type AssemblerIO, assembleSessionConfig } from "./session-config.js";
|
|
21
15
|
import type { ShellExec, SubagentType, ThinkingLevel } from "./types.js";
|
|
22
16
|
|
|
23
17
|
/** Names of tools registered by this extension that subagents must NOT inherit. */
|
|
@@ -65,6 +59,63 @@ export function normalizeMaxTurns(n: number | undefined): number | undefined {
|
|
|
65
59
|
return Math.max(1, n);
|
|
66
60
|
}
|
|
67
61
|
|
|
62
|
+
// ── IO boundary ───────────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/** Minimal resource-loader contract used by the runner. */
|
|
65
|
+
export interface ResourceLoaderLike {
|
|
66
|
+
reload(): Promise<void>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Minimal session-manager contract used by the runner. */
|
|
70
|
+
export interface SessionManagerLike {
|
|
71
|
+
newSession(opts: { parentSession?: string }): void;
|
|
72
|
+
getSessionFile(): string | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Options passed to RunnerIO.createResourceLoader. */
|
|
76
|
+
export interface ResourceLoaderOptions {
|
|
77
|
+
cwd: string;
|
|
78
|
+
agentDir: string;
|
|
79
|
+
noExtensions?: boolean;
|
|
80
|
+
noSkills?: boolean;
|
|
81
|
+
noPromptTemplates?: boolean;
|
|
82
|
+
noThemes?: boolean;
|
|
83
|
+
noContextFiles?: boolean;
|
|
84
|
+
systemPromptOverride?: () => string;
|
|
85
|
+
appendSystemPromptOverride?: () => unknown[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Options passed to RunnerIO.createSession. */
|
|
89
|
+
export interface CreateSessionOptions {
|
|
90
|
+
cwd: string;
|
|
91
|
+
agentDir: string;
|
|
92
|
+
sessionManager: SessionManagerLike;
|
|
93
|
+
settingsManager: unknown;
|
|
94
|
+
modelRegistry: unknown;
|
|
95
|
+
model?: unknown;
|
|
96
|
+
tools: string[];
|
|
97
|
+
resourceLoader: ResourceLoaderLike;
|
|
98
|
+
thinkingLevel?: ThinkingLevel;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* IO boundary injected into runAgent().
|
|
103
|
+
*
|
|
104
|
+
* Decouples the runner from direct Pi SDK imports and sibling-module IO,
|
|
105
|
+
* making it testable via plain stub objects without vi.mock().
|
|
106
|
+
*/
|
|
107
|
+
export interface RunnerIO {
|
|
108
|
+
detectEnv: (exec: ShellExec, cwd: string) => Promise<EnvInfo>;
|
|
109
|
+
getAgentDir: () => string;
|
|
110
|
+
createResourceLoader: (opts: ResourceLoaderOptions) => ResourceLoaderLike;
|
|
111
|
+
deriveSessionDir: (parentSessionFile: string | undefined, effectiveCwd: string) => string;
|
|
112
|
+
createSessionManager: (cwd: string, sessionDir: string) => SessionManagerLike;
|
|
113
|
+
createSettingsManager: (cwd: string, agentDir: string) => unknown;
|
|
114
|
+
createSession: (opts: CreateSessionOptions) => Promise<{ session: AgentSession }>;
|
|
115
|
+
assemblerIO: AssemblerIO;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Public interfaces ─────────────────────────────────────────────────────────
|
|
68
119
|
|
|
69
120
|
export interface RunOptions {
|
|
70
121
|
/** Shell-exec callback for detectEnv — injected from pi.exec(). */
|
|
@@ -122,6 +173,20 @@ export interface AgentRunner {
|
|
|
122
173
|
resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string>;
|
|
123
174
|
}
|
|
124
175
|
|
|
176
|
+
/**
|
|
177
|
+
* Create an AgentRunner backed by the given IO boundary.
|
|
178
|
+
*
|
|
179
|
+
* Captures io at construction time so AgentManager remains IO-unaware.
|
|
180
|
+
*/
|
|
181
|
+
export function createAgentRunner(io: RunnerIO): AgentRunner {
|
|
182
|
+
return {
|
|
183
|
+
run: (snapshot, type, prompt, options) => runAgent(snapshot, type, prompt, options, io),
|
|
184
|
+
resume: resumeAgent,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ── Private helpers ───────────────────────────────────────────────────────────
|
|
189
|
+
|
|
125
190
|
/**
|
|
126
191
|
* Subscribe to a session and collect the last assistant message text.
|
|
127
192
|
* Returns an object with a `getText()` getter and an `unsubscribe` function.
|
|
@@ -167,15 +232,18 @@ function forwardAbortSignal(
|
|
|
167
232
|
return () => signal.removeEventListener("abort", onAbort);
|
|
168
233
|
}
|
|
169
234
|
|
|
235
|
+
// ── Public functions ──────────────────────────────────────────────────────────
|
|
236
|
+
|
|
170
237
|
export async function runAgent(
|
|
171
238
|
snapshot: ParentSnapshot,
|
|
172
239
|
type: SubagentType,
|
|
173
240
|
prompt: string,
|
|
174
241
|
options: RunOptions,
|
|
242
|
+
io: RunnerIO,
|
|
175
243
|
): Promise<RunResult> {
|
|
176
244
|
// Resolve working directory upfront — needed for detectEnv before assembly.
|
|
177
245
|
const effectiveCwd = options.cwd ?? snapshot.cwd;
|
|
178
|
-
const env = await detectEnv(options.exec, effectiveCwd);
|
|
246
|
+
const env = await io.detectEnv(options.exec, effectiveCwd);
|
|
179
247
|
|
|
180
248
|
// Assemble session configuration (synchronous, no SDK objects).
|
|
181
249
|
const cfg = assembleSessionConfig(
|
|
@@ -194,9 +262,10 @@ export async function runAgent(
|
|
|
194
262
|
},
|
|
195
263
|
env,
|
|
196
264
|
options.registry,
|
|
265
|
+
io.assemblerIO,
|
|
197
266
|
);
|
|
198
267
|
|
|
199
|
-
const agentDir = getAgentDir();
|
|
268
|
+
const agentDir = io.getAgentDir();
|
|
200
269
|
|
|
201
270
|
// Load extensions/skills: true or string[] → load; false → don't.
|
|
202
271
|
// Suppress AGENTS.md/CLAUDE.md and APPEND_SYSTEM.md — upstream's
|
|
@@ -204,7 +273,7 @@ export async function runAgent(
|
|
|
204
273
|
// would defeat prompt_mode: replace and isolated: true. Parent context, if
|
|
205
274
|
// wanted, reaches the subagent via prompt_mode: append (parentSystemPrompt
|
|
206
275
|
// is embedded in systemPromptOverride) or inherit_context (conversation).
|
|
207
|
-
const loader =
|
|
276
|
+
const loader = io.createResourceLoader({
|
|
208
277
|
cwd: cfg.effectiveCwd,
|
|
209
278
|
agentDir,
|
|
210
279
|
noExtensions: cfg.extensions === false,
|
|
@@ -220,25 +289,21 @@ export async function runAgent(
|
|
|
220
289
|
// Create a persisted SessionManager so transcripts are written in Pi's
|
|
221
290
|
// official JSONL format. Falls back to a temp directory when the parent
|
|
222
291
|
// session is not persisted (e.g. headless/API mode).
|
|
223
|
-
const sessionDir =
|
|
224
|
-
const sessionManager =
|
|
292
|
+
const sessionDir = io.deriveSessionDir(options.parentSessionFile, cfg.effectiveCwd);
|
|
293
|
+
const sessionManager = io.createSessionManager(cfg.effectiveCwd, sessionDir);
|
|
225
294
|
sessionManager.newSession({ parentSession: options.parentSessionId });
|
|
226
295
|
|
|
227
|
-
const
|
|
296
|
+
const { session } = await io.createSession({
|
|
228
297
|
cwd: cfg.effectiveCwd,
|
|
229
298
|
agentDir,
|
|
230
299
|
sessionManager,
|
|
231
|
-
settingsManager:
|
|
232
|
-
modelRegistry: snapshot.modelRegistry
|
|
233
|
-
model: cfg.model
|
|
300
|
+
settingsManager: io.createSettingsManager(cfg.effectiveCwd, agentDir),
|
|
301
|
+
modelRegistry: snapshot.modelRegistry,
|
|
302
|
+
model: cfg.model,
|
|
234
303
|
tools: cfg.toolNames,
|
|
235
304
|
resourceLoader: loader,
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
sessionOpts.thinkingLevel = cfg.thinkingLevel;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const { session } = await createAgentSession(sessionOpts);
|
|
305
|
+
thinkingLevel: cfg.thinkingLevel,
|
|
306
|
+
});
|
|
242
307
|
|
|
243
308
|
// Filter active tools: remove our own tools to prevent nesting,
|
|
244
309
|
// apply extension allowlist if specified, and apply disallowedTools denylist.
|
package/src/index.ts
CHANGED
|
@@ -11,19 +11,32 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
createAgentSession,
|
|
16
|
+
DefaultResourceLoader,
|
|
17
|
+
defineTool,
|
|
18
|
+
type ExtensionAPI,
|
|
19
|
+
getAgentDir,
|
|
20
|
+
SettingsManager as SdkSettingsManager,
|
|
21
|
+
SessionManager,
|
|
22
|
+
} from "@earendil-works/pi-coding-agent";
|
|
15
23
|
import { AgentManager, type AgentManagerObserver } from "./agent-manager.js";
|
|
16
|
-
import {
|
|
24
|
+
import { createAgentRunner, getAgentConversation, type RunnerIO, steerAgent } from "./agent-runner.js";
|
|
17
25
|
import { AgentTypeRegistry } from "./agent-types.js";
|
|
18
26
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
27
|
+
import { detectEnv } from "./env.js";
|
|
19
28
|
import { SessionLifecycleHandler, ToolStartHandler } from "./handlers/index.js";
|
|
29
|
+
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
20
30
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
21
31
|
import { buildEventData, type NotificationDetails, NotificationManager } from "./notification.js";
|
|
32
|
+
import { buildAgentPrompt } from "./prompts.js";
|
|
22
33
|
import { createNotificationRenderer } from "./renderer.js";
|
|
23
34
|
import { createSubagentRuntime } from "./runtime.js";
|
|
24
35
|
import { publishSubagentsService, unpublishSubagentsService } from "./service.js";
|
|
25
36
|
import { createSubagentsService } from "./service-adapter.js";
|
|
37
|
+
import { deriveSubagentSessionDir } from "./session-dir.js";
|
|
26
38
|
import { SettingsManager } from "./settings.js";
|
|
39
|
+
import { preloadSkills } from "./skill-loader.js";
|
|
27
40
|
import { createAgentTool } from "./tools/agent-tool.js";
|
|
28
41
|
import { createGetResultTool } from "./tools/get-result-tool.js";
|
|
29
42
|
import { getModelLabelFromConfig } from "./tools/helpers.js";
|
|
@@ -120,8 +133,24 @@ export default function (pi: ExtensionAPI) {
|
|
|
120
133
|
},
|
|
121
134
|
};
|
|
122
135
|
|
|
136
|
+
const runnerIO: RunnerIO = {
|
|
137
|
+
detectEnv,
|
|
138
|
+
getAgentDir,
|
|
139
|
+
createResourceLoader: (opts) => new DefaultResourceLoader(opts as any),
|
|
140
|
+
deriveSessionDir: deriveSubagentSessionDir,
|
|
141
|
+
createSessionManager: (cwd, dir) => SessionManager.create(cwd, dir),
|
|
142
|
+
createSettingsManager: (cwd, dir) => SdkSettingsManager.create(cwd, dir),
|
|
143
|
+
createSession: (opts) => createAgentSession(opts as any),
|
|
144
|
+
assemblerIO: {
|
|
145
|
+
preloadSkills,
|
|
146
|
+
buildMemoryBlock,
|
|
147
|
+
buildReadOnlyMemoryBlock,
|
|
148
|
+
buildAgentPrompt,
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
|
|
123
152
|
const manager = new AgentManager({
|
|
124
|
-
runner:
|
|
153
|
+
runner: createAgentRunner(runnerIO),
|
|
125
154
|
worktrees: new GitWorktreeManager(process.cwd()),
|
|
126
155
|
exec: (cmd, args, opts) => pi.exec(cmd, args, opts),
|
|
127
156
|
registry,
|
package/src/session-config.ts
CHANGED
|
@@ -16,13 +16,42 @@ import {
|
|
|
16
16
|
getReadOnlyMemoryToolNames,
|
|
17
17
|
} from "./agent-types.js";
|
|
18
18
|
import type { EnvInfo } from "./env.js";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
|
|
19
|
+
import type { PromptExtras } from "./prompts.js";
|
|
20
|
+
import type { PreloadedSkill } from "./skill-loader.js";
|
|
21
|
+
import type {
|
|
22
|
+
AgentPromptConfig,
|
|
23
|
+
MemoryScope,
|
|
24
|
+
SubagentType,
|
|
25
|
+
ThinkingLevel,
|
|
26
|
+
} from "./types.js";
|
|
23
27
|
|
|
24
28
|
// ── Public interfaces ────────────────────────────────────────────────────────
|
|
25
29
|
|
|
30
|
+
/**
|
|
31
|
+
* IO collaborators injected into `assembleSessionConfig`.
|
|
32
|
+
*
|
|
33
|
+
* Bundling the four IO-touching (or promptly testable) functions into a single
|
|
34
|
+
* interface keeps the assembler free of direct module imports and makes it
|
|
35
|
+
* trivially testable without `vi.mock()` — callers inject real implementations
|
|
36
|
+
* at the edge (`agent-runner.ts`) or stubs in tests.
|
|
37
|
+
*/
|
|
38
|
+
export interface AssemblerIO {
|
|
39
|
+
preloadSkills: (skills: string[], cwd: string) => PreloadedSkill[];
|
|
40
|
+
buildMemoryBlock: (name: string, scope: MemoryScope, cwd: string) => string;
|
|
41
|
+
buildReadOnlyMemoryBlock: (
|
|
42
|
+
name: string,
|
|
43
|
+
scope: MemoryScope,
|
|
44
|
+
cwd: string,
|
|
45
|
+
) => string;
|
|
46
|
+
buildAgentPrompt: (
|
|
47
|
+
config: AgentPromptConfig,
|
|
48
|
+
cwd: string,
|
|
49
|
+
env: EnvInfo,
|
|
50
|
+
parentPrompt?: string,
|
|
51
|
+
extras?: PromptExtras,
|
|
52
|
+
) => string;
|
|
53
|
+
}
|
|
54
|
+
|
|
26
55
|
/**
|
|
27
56
|
* Narrow context the assembler reads from the parent session.
|
|
28
57
|
* Tests construct plain objects satisfying this interface — no SDK mocking needed.
|
|
@@ -132,8 +161,8 @@ function resolveDefaultModel(
|
|
|
132
161
|
/**
|
|
133
162
|
* Assemble all configuration needed to create an agent session.
|
|
134
163
|
*
|
|
135
|
-
* Synchronous and side-effect-free
|
|
136
|
-
*
|
|
164
|
+
* Synchronous and side-effect-free — all IO is delegated through the `io`
|
|
165
|
+
* parameter. The caller is responsible for resolving `EnvInfo` beforehand
|
|
137
166
|
* via `detectEnv()`.
|
|
138
167
|
*
|
|
139
168
|
* @param type The subagent type name (case-insensitive registry lookup).
|
|
@@ -141,6 +170,7 @@ function resolveDefaultModel(
|
|
|
141
170
|
* @param options Per-call overrides (cwd, isolated, model, thinkingLevel).
|
|
142
171
|
* @param env Pre-resolved environment info from `detectEnv()`.
|
|
143
172
|
* @param registry Agent config lookup — provides resolveAgentConfig and getToolNamesForType.
|
|
173
|
+
* @param io IO collaborators (skill loader, memory builder, prompt builder).
|
|
144
174
|
*/
|
|
145
175
|
export function assembleSessionConfig(
|
|
146
176
|
type: SubagentType,
|
|
@@ -148,6 +178,7 @@ export function assembleSessionConfig(
|
|
|
148
178
|
options: AssemblerOptions,
|
|
149
179
|
env: EnvInfo,
|
|
150
180
|
registry: AgentConfigLookup,
|
|
181
|
+
io: AssemblerIO,
|
|
151
182
|
): SessionConfig {
|
|
152
183
|
const agentConfig = registry.resolveAgentConfig(type);
|
|
153
184
|
|
|
@@ -162,7 +193,7 @@ export function assembleSessionConfig(
|
|
|
162
193
|
|
|
163
194
|
// Skill preloading: when skills is string[], preload their content into the prompt
|
|
164
195
|
if (Array.isArray(skills)) {
|
|
165
|
-
const loaded = preloadSkills(skills, effectiveCwd);
|
|
196
|
+
const loaded = io.preloadSkills(skills, effectiveCwd);
|
|
166
197
|
if (loaded.length > 0) {
|
|
167
198
|
extras.skillBlocks = loaded;
|
|
168
199
|
}
|
|
@@ -185,7 +216,7 @@ export function assembleSessionConfig(
|
|
|
185
216
|
if (hasWriteTools) {
|
|
186
217
|
const extraNames = getMemoryToolNames(existingNames);
|
|
187
218
|
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
188
|
-
extras.memoryBlock = buildMemoryBlock(
|
|
219
|
+
extras.memoryBlock = io.buildMemoryBlock(
|
|
189
220
|
agentConfig.name,
|
|
190
221
|
agentConfig.memory,
|
|
191
222
|
effectiveCwd,
|
|
@@ -193,7 +224,7 @@ export function assembleSessionConfig(
|
|
|
193
224
|
} else {
|
|
194
225
|
const extraNames = getReadOnlyMemoryToolNames(existingNames);
|
|
195
226
|
if (extraNames.length > 0) toolNames = [...toolNames, ...extraNames];
|
|
196
|
-
extras.memoryBlock = buildReadOnlyMemoryBlock(
|
|
227
|
+
extras.memoryBlock = io.buildReadOnlyMemoryBlock(
|
|
197
228
|
agentConfig.name,
|
|
198
229
|
agentConfig.memory,
|
|
199
230
|
effectiveCwd,
|
|
@@ -202,7 +233,7 @@ export function assembleSessionConfig(
|
|
|
202
233
|
}
|
|
203
234
|
|
|
204
235
|
// Build system prompt from the resolved agent config
|
|
205
|
-
const systemPrompt = buildAgentPrompt(
|
|
236
|
+
const systemPrompt = io.buildAgentPrompt(
|
|
206
237
|
agentConfig,
|
|
207
238
|
effectiveCwd,
|
|
208
239
|
env,
|