@gotgenes/pi-subagents 5.4.1 → 5.6.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 +29 -0
- package/docs/plans/0072-inject-agent-manager-collaborators.md +329 -0
- package/docs/plans/0084-extract-git-worktree-manager.md +142 -0
- package/docs/retro/0076-inject-cwd-into-agent-manager.md +35 -0
- package/docs/retro/0084-extract-git-worktree-manager.md +37 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +28 -23
- package/src/agent-runner.ts +18 -13
- package/src/index.ts +48 -41
- package/src/worktree.ts +30 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,35 @@ 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
|
+
## [5.6.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.5.0...pi-subagents-v5.6.0) (2026-05-20)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Features
|
|
12
|
+
|
|
13
|
+
* convert AgentManager to options-bag constructor with DI ([1292cec](https://github.com/gotgenes/pi-packages/commit/1292cec60c24d8e657985c53b9b3413089c2a79d))
|
|
14
|
+
* define AgentRunner interface in agent-runner.ts ([6a3c85a](https://github.com/gotgenes/pi-packages/commit/6a3c85a445daf0e4e8c01620eb6ae1a8237f1766))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Documentation
|
|
18
|
+
|
|
19
|
+
* mark [#84](https://github.com/gotgenes/pi-packages/issues/84) as done in plan ([#72](https://github.com/gotgenes/pi-packages/issues/72)) ([5cfa1ec](https://github.com/gotgenes/pi-packages/commit/5cfa1ecf080f95fbd6b4aec05b27cc9672f60267))
|
|
20
|
+
* **retro:** add retro notes for issue [#84](https://github.com/gotgenes/pi-packages/issues/84) ([99d9016](https://github.com/gotgenes/pi-packages/commit/99d90161df5fe9d302514e33c2d2d9fbfb248f25))
|
|
21
|
+
|
|
22
|
+
## [5.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.4.1...pi-subagents-v5.5.0) (2026-05-20)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
* extract WorktreeManager interface and GitWorktreeManager class ([#84](https://github.com/gotgenes/pi-packages/issues/84)) ([23efb99](https://github.com/gotgenes/pi-packages/commit/23efb99e0d5e6bf6a65b758020e00af69fe84f6e))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
### Documentation
|
|
31
|
+
|
|
32
|
+
* plan dependency-inject AgentManager's collaborators ([#72](https://github.com/gotgenes/pi-packages/issues/72)) ([a99374a](https://github.com/gotgenes/pi-packages/commit/a99374aa2b11defd301be97f64b9bdba2a618712))
|
|
33
|
+
* plan extract GitWorktreeManager class ([#84](https://github.com/gotgenes/pi-packages/issues/84)) ([47d9d93](https://github.com/gotgenes/pi-packages/commit/47d9d9368fc2f4762bf31e312ebd84e4332ca4c4))
|
|
34
|
+
* **retro:** add retro notes for issue [#76](https://github.com/gotgenes/pi-packages/issues/76) ([ceef7e0](https://github.com/gotgenes/pi-packages/commit/ceef7e05c8753bbbb6558ca507924b5562cc9c52))
|
|
35
|
+
* update plan to reference [#84](https://github.com/gotgenes/pi-packages/issues/84) as prerequisite ([#72](https://github.com/gotgenes/pi-packages/issues/72)) ([d8ad3f5](https://github.com/gotgenes/pi-packages/commit/d8ad3f544929c569789d63fcf140bd300d5ef389))
|
|
36
|
+
|
|
8
37
|
## [5.4.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.4.0...pi-subagents-v5.4.1) (2026-05-20)
|
|
9
38
|
|
|
10
39
|
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 72
|
|
3
|
+
issue_title: "refactor: dependency-inject AgentManager's collaborators"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Dependency-inject AgentManager's collaborators
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`AgentManager` directly imports and calls `runAgent`, `resumeAgent`, `createWorktree`, `cleanupWorktree`, and `pruneWorktrees`.
|
|
11
|
+
Any test of `AgentManager` must mock entire modules via `vi.mock()`, coupling the test to the internal structure of `agent-runner.ts` and `worktree.ts` rather than to `AgentManager`'s own behavior.
|
|
12
|
+
|
|
13
|
+
## Goals
|
|
14
|
+
|
|
15
|
+
- Define an `AgentRunner` interface (execution boundary) and a `WorktreeManager` interface (real object with state) for the operations `AgentManager` actually needs.
|
|
16
|
+
- Inject both into `AgentManager` via a constructor options bag, replacing the current 6-positional-parameter constructor.
|
|
17
|
+
- Remove all runtime imports of `agent-runner.ts` and `worktree.ts` from `agent-manager.ts`.
|
|
18
|
+
- Migrate `agent-manager.test.ts` from `vi.mock()` module stubs to `vi.fn()` interface stubs.
|
|
19
|
+
- No behavior change.
|
|
20
|
+
|
|
21
|
+
## Non-Goals
|
|
22
|
+
|
|
23
|
+
- Changing `AgentManager`'s public method surface (`spawn`, `spawnAndWait`, `resume`, `abort`, etc.).
|
|
24
|
+
- Refactoring `agent-runner.ts` internals (done in #71).
|
|
25
|
+
- Capturing `pi: ExtensionAPI` inside the runner — `pi` stays per-call in `SpawnArgs` for now.
|
|
26
|
+
- Extracting the notification system or widget (done in #54).
|
|
27
|
+
|
|
28
|
+
## Background
|
|
29
|
+
|
|
30
|
+
### Prerequisites
|
|
31
|
+
|
|
32
|
+
| Issue | Title | Status |
|
|
33
|
+
| ----- | --------------------------------------------------- | ------- |
|
|
34
|
+
| #69 | Create `SubagentRuntime` | ✓ Done |
|
|
35
|
+
| #71 | Extract pure agent-session assembler | ✓ Done |
|
|
36
|
+
| #76 | Inject `cwd` into `AgentManager` | ✓ Done |
|
|
37
|
+
| #80 | Consolidate `getConfig`/`getAgentConfig` | ✓ Done |
|
|
38
|
+
| #84 | Extract `GitWorktreeManager` class from worktree.ts | ✓ Done |
|
|
39
|
+
|
|
40
|
+
### Prior art
|
|
41
|
+
|
|
42
|
+
`pi-permission-system` `PermissionManager` takes a `PolicyLoader` via constructor injection.
|
|
43
|
+
The interface is defined in `policy-loader.ts` alongside the default `FilePolicyLoader` implementation.
|
|
44
|
+
`PermissionManager` imports the interface type-only — no runtime coupling to the loader module.
|
|
45
|
+
|
|
46
|
+
### Current imports in `agent-manager.ts`
|
|
47
|
+
|
|
48
|
+
```typescript
|
|
49
|
+
import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js"; // runtime + type
|
|
50
|
+
import { addUsage } from "./usage.js"; // runtime (pure utility, stays)
|
|
51
|
+
import { cleanupWorktree, createWorktree, pruneWorktrees } from "./worktree.js"; // runtime
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
After this refactor:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import type { AgentRunner } from "./agent-runner.js"; // type-only (erased at compile)
|
|
58
|
+
import type { WorktreeManager } from "./worktree.js"; // type-only (erased at compile)
|
|
59
|
+
import { addUsage } from "./usage.js"; // runtime (pure utility, stays)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Relevant constraints from AGENTS.md
|
|
63
|
+
|
|
64
|
+
- Keep modules focused and composable (one concern per file).
|
|
65
|
+
- Prefer explicit configuration over hidden behavior.
|
|
66
|
+
- When a shared interface references a collaborator, use a narrow interface type — not the concrete class.
|
|
67
|
+
|
|
68
|
+
## Design Overview
|
|
69
|
+
|
|
70
|
+
### Two collaborators, different natures
|
|
71
|
+
|
|
72
|
+
#### WorktreeManager — real object with state
|
|
73
|
+
|
|
74
|
+
The three worktree functions all operate on git worktrees relative to a repository root.
|
|
75
|
+
Today `cwd` is threaded to each call — `createWorktree(ctx.cwd, id)`, `cleanupWorktree(ctx.cwd, wt, desc)`, `pruneWorktrees(this.cwd)`.
|
|
76
|
+
In practice `ctx.cwd` and `this.cwd` are always the same value (the process working directory set at extension init).
|
|
77
|
+
|
|
78
|
+
A `WorktreeManager` class captures `cwd` at construction, eliminating the per-call threading:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// In worktree.ts
|
|
82
|
+
export interface WorktreeManager {
|
|
83
|
+
create(id: string): WorktreeInfo | undefined;
|
|
84
|
+
cleanup(wt: WorktreeInfo, description: string): WorktreeCleanupResult;
|
|
85
|
+
prune(): void;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class GitWorktreeManager implements WorktreeManager {
|
|
89
|
+
constructor(private readonly cwd: string) {}
|
|
90
|
+
create(id: string): WorktreeInfo | undefined { return createWorktree(this.cwd, id); }
|
|
91
|
+
cleanup(wt: WorktreeInfo, description: string): WorktreeCleanupResult { return cleanupWorktree(this.cwd, wt, description); }
|
|
92
|
+
prune(): void { pruneWorktrees(this.cwd); }
|
|
93
|
+
}
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
The existing free functions stay as the internal implementation and for any direct callers.
|
|
97
|
+
|
|
98
|
+
#### AgentRunner — execution boundary interface
|
|
99
|
+
|
|
100
|
+
`runAgent` and `resumeAgent` are stateless IO orchestrators.
|
|
101
|
+
They have no natural state to capture — `pi` is constant per extension but already flows through `SpawnArgs`.
|
|
102
|
+
The interface exists to decouple `AgentManager` (lifecycle management: queuing, concurrency, abort) from the execution engine (SDK sessions, prompt loops, event wiring):
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// In agent-runner.ts
|
|
106
|
+
export interface AgentRunner {
|
|
107
|
+
run(ctx: ExtensionContext, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult>;
|
|
108
|
+
resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string>;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export interface ResumeOptions {
|
|
112
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
113
|
+
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
114
|
+
onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
|
|
115
|
+
signal?: AbortSignal;
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
The existing `{ run: runAgent, resume: resumeAgent }` structurally satisfies the interface — no wrapper class needed.
|
|
120
|
+
|
|
121
|
+
### Constructor options bag
|
|
122
|
+
|
|
123
|
+
The current 6-positional-parameter constructor becomes an options bag:
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
export interface AgentManagerOptions {
|
|
127
|
+
cwd: string;
|
|
128
|
+
runner: AgentRunner;
|
|
129
|
+
worktrees: WorktreeManager;
|
|
130
|
+
maxConcurrent?: number;
|
|
131
|
+
getRunConfig?: () => RunConfig;
|
|
132
|
+
onStart?: OnAgentStart;
|
|
133
|
+
onComplete?: OnAgentComplete;
|
|
134
|
+
onCompact?: OnAgentCompact;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
All fields are used by `AgentManager` — no subset concern.
|
|
139
|
+
|
|
140
|
+
### Wiring in index.ts
|
|
141
|
+
|
|
142
|
+
```typescript
|
|
143
|
+
import { runAgent, resumeAgent } from "./agent-runner.js";
|
|
144
|
+
import { GitWorktreeManager } from "./worktree.js";
|
|
145
|
+
|
|
146
|
+
const worktrees = new GitWorktreeManager(process.cwd());
|
|
147
|
+
const manager = new AgentManager({
|
|
148
|
+
cwd: process.cwd(),
|
|
149
|
+
runner: { run: runAgent, resume: resumeAgent },
|
|
150
|
+
worktrees,
|
|
151
|
+
onComplete: (record) => { /* ... */ },
|
|
152
|
+
onStart: (record) => { /* ... */ },
|
|
153
|
+
onCompact: (record, info) => { /* ... */ },
|
|
154
|
+
getRunConfig: () => ({ defaultMaxTurns: runtime.defaultMaxTurns, graceTurns: runtime.graceTurns }),
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
### `cwd` on AgentManager after WorktreeManager captures it
|
|
159
|
+
|
|
160
|
+
`AgentManager.cwd` was previously used for two purposes:
|
|
161
|
+
|
|
162
|
+
1. Worktree operations (`createWorktree(ctx.cwd, ...)`, `pruneWorktrees(this.cwd)`) — now handled by the injected `WorktreeManager`.
|
|
163
|
+
2. No other use remains inside `AgentManager` itself.
|
|
164
|
+
|
|
165
|
+
However, `cwd` is still passed as part of `AgentManagerOptions` because `dispose()` calls `this.worktrees.prune()` — the `WorktreeManager` now owns the `cwd` for that call.
|
|
166
|
+
The `cwd` field on `AgentManagerOptions` can be dropped if no other internal use remains after the refactor.
|
|
167
|
+
A grep in step 8 will confirm whether `this.cwd` has any remaining readers; if not, it is removed.
|
|
168
|
+
|
|
169
|
+
### Test pattern after DI
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
function createManager(overrides?: Partial<AgentManagerOptions>) {
|
|
173
|
+
const runner: AgentRunner = {
|
|
174
|
+
run: vi.fn().mockResolvedValue({
|
|
175
|
+
responseText: "done", session: { dispose: vi.fn() }, aborted: false, steered: false,
|
|
176
|
+
}),
|
|
177
|
+
resume: vi.fn().mockResolvedValue("resumed"),
|
|
178
|
+
};
|
|
179
|
+
const worktrees: WorktreeManager = {
|
|
180
|
+
create: vi.fn(),
|
|
181
|
+
cleanup: vi.fn(() => ({ hasChanges: false })),
|
|
182
|
+
prune: vi.fn(),
|
|
183
|
+
};
|
|
184
|
+
return {
|
|
185
|
+
manager: new AgentManager({ cwd: "/test-cwd", runner, worktrees, ...overrides }),
|
|
186
|
+
runner,
|
|
187
|
+
worktrees,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Tests access the mock stubs directly — no `vi.mocked(runAgent)` needed:
|
|
193
|
+
|
|
194
|
+
```typescript
|
|
195
|
+
const { manager, runner } = createManager({ onComplete: (r) => { /* ... */ } });
|
|
196
|
+
manager.spawn(mockPi, mockCtx, "general-purpose", "test", { description: "test", isBackground: true });
|
|
197
|
+
expect(runner.run).toHaveBeenCalled();
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## Module-Level Changes
|
|
201
|
+
|
|
202
|
+
### `src/worktree.ts` (no changes in this issue)
|
|
203
|
+
|
|
204
|
+
`WorktreeManager` interface and `GitWorktreeManager` class were added by #84.
|
|
205
|
+
|
|
206
|
+
### `src/agent-runner.ts` (modified)
|
|
207
|
+
|
|
208
|
+
- Add `AgentRunner` interface (2 methods: `run`, `resume`).
|
|
209
|
+
- Extract `ResumeOptions` as a named type from the inline parameter type in `resumeAgent`.
|
|
210
|
+
- Export both.
|
|
211
|
+
- No changes to `runAgent()` or `resumeAgent()` implementations.
|
|
212
|
+
|
|
213
|
+
### `src/agent-manager.ts` (modified)
|
|
214
|
+
|
|
215
|
+
- Add `AgentManagerOptions` interface.
|
|
216
|
+
- Replace 6-positional-parameter constructor with single `options: AgentManagerOptions` parameter.
|
|
217
|
+
- Replace `runAgent(ctx, ...)` with `this.runner.run(ctx, ...)`.
|
|
218
|
+
- Replace `resumeAgent(session, ...)` with `this.runner.resume(session, ...)`.
|
|
219
|
+
- Replace `createWorktree(ctx.cwd, id)` with `this.worktrees.create(id)`.
|
|
220
|
+
- Replace `cleanupWorktree(ctx.cwd, wt, desc)` with `this.worktrees.cleanup(wt, desc)`.
|
|
221
|
+
- Replace `pruneWorktrees(this.cwd)` with `this.worktrees.prune()`.
|
|
222
|
+
- Remove runtime imports from `agent-runner.ts` and `worktree.ts`; keep `import type` only.
|
|
223
|
+
- Remove `this.cwd` if grep confirms no remaining readers after the worktree delegation.
|
|
224
|
+
|
|
225
|
+
### `src/index.ts` (modified)
|
|
226
|
+
|
|
227
|
+
- Import `GitWorktreeManager` from `worktree.ts`.
|
|
228
|
+
- Construct `new GitWorktreeManager(process.cwd())`.
|
|
229
|
+
- Pass options bag to `new AgentManager({ ... })`.
|
|
230
|
+
|
|
231
|
+
### `test/agent-manager.test.ts` (modified)
|
|
232
|
+
|
|
233
|
+
- Remove `vi.mock("../src/agent-runner.js", ...)` block.
|
|
234
|
+
- Remove `vi.mock("../src/worktree.js", ...)` block.
|
|
235
|
+
- Remove `import { runAgent } from "../src/agent-runner.js"` and `import { pruneWorktrees } from "../src/worktree.js"`.
|
|
236
|
+
- Add `createManager()` test helper factory.
|
|
237
|
+
- Replace all 19 `new AgentManager(...)` calls with `createManager(...)`.
|
|
238
|
+
- Update assertions from `vi.mocked(runAgent)` to `runner.run` / `runner.resume`.
|
|
239
|
+
- Update assertions from `vi.mocked(pruneWorktrees)` to `worktrees.prune`.
|
|
240
|
+
- Update assertions from `vi.mocked(createWorktree)` to `worktrees.create`.
|
|
241
|
+
|
|
242
|
+
## Test Impact Analysis
|
|
243
|
+
|
|
244
|
+
### New unit tests enabled by DI
|
|
245
|
+
|
|
246
|
+
1. Queueing behavior — verify that excess background agents are queued and started in order when running agents complete, without needing module-level mocks.
|
|
247
|
+
2. Concurrency limit enforcement — verify `maxConcurrent` is respected with controlled stub resolution.
|
|
248
|
+
3. Abort semantics — verify that aborting a queued agent removes it from the queue and sets status, using stubs that never resolve.
|
|
249
|
+
4. Lifecycle callback ordering — verify `onStart`, `onComplete`, `onCompact` fire at the right moments with correct record state.
|
|
250
|
+
5. Worktree failure modes — verify that `create` returning `undefined` throws and leaves no orphan record, via a simple `vi.fn().mockReturnValue(undefined)`.
|
|
251
|
+
|
|
252
|
+
### Existing tests that are migrated (not removed)
|
|
253
|
+
|
|
254
|
+
All 19 test sites in `agent-manager.test.ts` are migrated from `vi.mock()` + `vi.mocked()` to the `createManager()` helper with injected stubs.
|
|
255
|
+
The test logic stays identical — only the mock setup mechanism changes.
|
|
256
|
+
|
|
257
|
+
### Existing tests that stay as-is
|
|
258
|
+
|
|
259
|
+
Tests in `agent-runner.test.ts`, `agent-runner-extension-tools.test.ts`, `agent-runner-settings.test.ts`, and `session-config.test.ts` are unaffected — they test the execution engine, not the lifecycle manager.
|
|
260
|
+
|
|
261
|
+
## TDD Order
|
|
262
|
+
|
|
263
|
+
Issue #84 (extract `GitWorktreeManager`) must land first.
|
|
264
|
+
This plan assumes `WorktreeManager` interface and `GitWorktreeManager` class already exist in `worktree.ts`.
|
|
265
|
+
|
|
266
|
+
### Phase A: Define AgentRunner interface
|
|
267
|
+
|
|
268
|
+
1. Add `AgentRunner` interface and named `ResumeOptions` type in `agent-runner.ts`.
|
|
269
|
+
Export both.
|
|
270
|
+
Run existing tests.
|
|
271
|
+
Commit: `feat: define AgentRunner interface in agent-runner.ts`
|
|
272
|
+
|
|
273
|
+
### Phase B: Lift-and-shift test migration
|
|
274
|
+
|
|
275
|
+
2. Create `createManager()` test helper factory in `agent-manager.test.ts`.
|
|
276
|
+
The factory constructs `AgentManager` using the **old** positional constructor, wrapping the same `vi.mock()` stubs in a consistent helper.
|
|
277
|
+
Migrate all 19 `new AgentManager(...)` call sites to use the factory.
|
|
278
|
+
All tests pass unchanged.
|
|
279
|
+
Commit: `test: add createManager helper and migrate call sites`
|
|
280
|
+
|
|
281
|
+
### Phase C: Constructor conversion + DI
|
|
282
|
+
|
|
283
|
+
3. RED: Add a test that calls `createManager()` with `runner` and `worktrees` overrides and asserts `runner.run` is called when spawning an agent.
|
|
284
|
+
This fails because the constructor does not accept the options bag yet.
|
|
285
|
+
Commit: `test: add agent-manager test with injected AgentRunner`
|
|
286
|
+
|
|
287
|
+
4. GREEN: Convert `AgentManager` constructor to accept `AgentManagerOptions`.
|
|
288
|
+
Replace internal calls to `runAgent`/`resumeAgent`/`createWorktree`/`cleanupWorktree`/`pruneWorktrees` with `this.runner.*`/`this.worktrees.*`.
|
|
289
|
+
Remove runtime imports from `agent-runner.ts` and `worktree.ts`.
|
|
290
|
+
Update `createManager()` factory to pass injected stubs via the options bag.
|
|
291
|
+
Remove `vi.mock()` blocks for `agent-runner.js` and `worktree.js`.
|
|
292
|
+
Update all test assertions that referenced `vi.mocked(runAgent)` etc. to reference the injected stubs.
|
|
293
|
+
All tests pass.
|
|
294
|
+
Commit: `feat: convert AgentManager to options-bag constructor with DI`
|
|
295
|
+
|
|
296
|
+
### Phase D: Wiring and verification
|
|
297
|
+
|
|
298
|
+
5. Wire `index.ts`: construct `GitWorktreeManager`, pass options bag to `AgentManager`.
|
|
299
|
+
Run full test suite.
|
|
300
|
+
Commit: `refactor: wire injected deps into AgentManager (#72)`
|
|
301
|
+
|
|
302
|
+
6. Add new tests enabled by DI: queueing order, concurrency enforcement, abort-from-queue, lifecycle callback timing.
|
|
303
|
+
Commit: `test: add DI-enabled agent-manager tests`
|
|
304
|
+
|
|
305
|
+
7. Verify acceptance criteria.
|
|
306
|
+
Grep `agent-manager.ts` for runtime imports from `agent-runner.ts` and `worktree.ts` (expect none).
|
|
307
|
+
Grep for `this.cwd` — if no readers remain, remove the field and the `cwd` option.
|
|
308
|
+
Run `pnpm run check` for type safety.
|
|
309
|
+
Commit: `refactor: finalize AgentManager DI (#72)`
|
|
310
|
+
|
|
311
|
+
## Risks and Mitigations
|
|
312
|
+
|
|
313
|
+
| Risk | Mitigation |
|
|
314
|
+
| ---------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
315
|
+
| `ctx.cwd` and `this.cwd` differ for worktree operations, causing `GitWorktreeManager` to use the wrong directory | In practice both are `process.cwd()` — verified by tracing `session_start` and constructor call sites. If a difference surfaces, `WorktreeManager` can accept `cwd` per-call as a fallback. |
|
|
316
|
+
| Migrating 19 test call sites introduces test regressions | Phase B (step 3) migrates under the old constructor first, proving the helper factory works before any behavioral change. Phase C (step 5) changes the constructor and stubs atomically. |
|
|
317
|
+
| `import type` from `agent-runner.ts`/`worktree.ts` is considered a "top-level import" by reviewers | `import type` is erased by TypeScript at compile time and creates zero runtime dependency. The compiled JS will have no import from these modules. This matches the `PolicyLoader` pattern in `pi-permission-system`. |
|
|
318
|
+
| New options bag breaks the `AgentManagerLike` interface in `service-adapter.ts` | `AgentManagerLike` references `AgentManager`'s public methods (`spawn`, `getRecord`, etc.), not its constructor. The constructor change is invisible to the adapter. |
|
|
319
|
+
| `addUsage` remains as a direct runtime import from `usage.ts` | Intentional — `addUsage` is a pure accumulator function with no IO. The issue targets `agent-runner.ts` and `worktree.ts` specifically. |
|
|
320
|
+
|
|
321
|
+
## Open Questions
|
|
322
|
+
|
|
323
|
+
- Should `AgentRunner` eventually capture `pi: ExtensionAPI` to eliminate the `pi` parameter from `spawn()` and `SpawnArgs`?
|
|
324
|
+
Deferred — `pi` is already threaded through the call chain and removing it would change `AgentManager`'s public method signatures, which is a non-goal.
|
|
325
|
+
- Does the `WorktreeManager` abstraction surface further opportunities (e.g., non-git isolation strategies)?
|
|
326
|
+
Noted for future consideration — the interface makes alternative implementations possible without changing `AgentManager`.
|
|
327
|
+
- The `AgentRunner` interface is a testability seam, not a stateful object.
|
|
328
|
+
As the codebase continues untangling, a more natural execution abstraction may emerge.
|
|
329
|
+
This interface is intentionally minimal to avoid premature abstraction.
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 84
|
|
3
|
+
issue_title: "refactor: extract GitWorktreeManager class from worktree.ts free functions"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Extract GitWorktreeManager class
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
`worktree.ts` exports three free functions — `createWorktree(cwd, id)`, `cleanupWorktree(cwd, wt, desc)`, and `pruneWorktrees(cwd)` — that all operate on git worktrees relative to a repository root.
|
|
11
|
+
Every caller threads `cwd` as the first argument.
|
|
12
|
+
These functions form a cohesive unit with natural shared state (the repo root), but today there is no object to capture it.
|
|
13
|
+
|
|
14
|
+
Issue #72 (dependency-inject `AgentManager`'s collaborators) needs a `WorktreeManager` interface to inject.
|
|
15
|
+
Extracting the class first keeps #72 a clean DI + constructor refactor instead of mixing object extraction with dependency injection.
|
|
16
|
+
|
|
17
|
+
## Goals
|
|
18
|
+
|
|
19
|
+
- Define a `WorktreeManager` interface in `worktree.ts` with three methods: `create(id)`, `cleanup(wt, desc)`, `prune()` — no `cwd` parameter.
|
|
20
|
+
- Add a `GitWorktreeManager` class that captures `cwd` at construction and delegates to the existing free functions.
|
|
21
|
+
- Export both the interface and the class.
|
|
22
|
+
- Existing free functions stay exported and unchanged.
|
|
23
|
+
- No behavior change.
|
|
24
|
+
|
|
25
|
+
## Non-Goals
|
|
26
|
+
|
|
27
|
+
- Changing `AgentManager` to use the new class (that is #72).
|
|
28
|
+
- Refactoring the internal implementation of `createWorktree`, `cleanupWorktree`, or `pruneWorktrees`.
|
|
29
|
+
- Publishing `WorktreeManager` as a cross-extension API via `Symbol.for()` — it is internal to this package.
|
|
30
|
+
|
|
31
|
+
## Background
|
|
32
|
+
|
|
33
|
+
### Prerequisites
|
|
34
|
+
|
|
35
|
+
None — this is the first step in the #72 dependency chain.
|
|
36
|
+
|
|
37
|
+
### Relevant modules
|
|
38
|
+
|
|
39
|
+
| Module | Role |
|
|
40
|
+
| ----------------------- | ---------------------------------------------------------- |
|
|
41
|
+
| `src/worktree.ts` | Free functions for git worktree create/cleanup/prune |
|
|
42
|
+
| `test/worktree.test.ts` | Integration tests that create real git repos and worktrees |
|
|
43
|
+
| `src/agent-manager.ts` | Only consumer of the three free functions (6 call sites) |
|
|
44
|
+
|
|
45
|
+
### Constraints from AGENTS.md / code-style
|
|
46
|
+
|
|
47
|
+
- Keep modules focused and composable (one concern per file).
|
|
48
|
+
- When a shared interface references a collaborator, use a narrow interface type — not the concrete class.
|
|
49
|
+
- Business logic should be pure functions wherever possible — keep IO at the edges.
|
|
50
|
+
|
|
51
|
+
The free functions are IO (they shell out to `git`), so wrapping them in a class that captures `cwd` is the right level of abstraction — the class is a thin adapter, not business logic.
|
|
52
|
+
|
|
53
|
+
## Design Overview
|
|
54
|
+
|
|
55
|
+
### Interface and class
|
|
56
|
+
|
|
57
|
+
Added at the bottom of `worktree.ts`, after the existing free functions:
|
|
58
|
+
|
|
59
|
+
```typescript
|
|
60
|
+
export interface WorktreeManager {
|
|
61
|
+
create(id: string): WorktreeInfo | undefined;
|
|
62
|
+
cleanup(wt: WorktreeInfo, description: string): WorktreeCleanupResult;
|
|
63
|
+
prune(): void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class GitWorktreeManager implements WorktreeManager {
|
|
67
|
+
constructor(private readonly cwd: string) {}
|
|
68
|
+
|
|
69
|
+
create(id: string): WorktreeInfo | undefined {
|
|
70
|
+
return createWorktree(this.cwd, id);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
cleanup(wt: WorktreeInfo, description: string): WorktreeCleanupResult {
|
|
74
|
+
return cleanupWorktree(this.cwd, wt, description);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
prune(): void {
|
|
78
|
+
pruneWorktrees(this.cwd);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Why the interface lives in `worktree.ts`
|
|
84
|
+
|
|
85
|
+
The `WorktreeManager` interface references `WorktreeInfo` and `WorktreeCleanupResult`, which are already defined in `worktree.ts`.
|
|
86
|
+
Co-locating avoids a separate file for a 5-line interface.
|
|
87
|
+
This matches the #72 plan's expectation: `import type { WorktreeManager } from "./worktree.js"`.
|
|
88
|
+
|
|
89
|
+
### No changes to callers
|
|
90
|
+
|
|
91
|
+
`agent-manager.ts` continues importing and calling the free functions directly.
|
|
92
|
+
Issue #72 handles the migration to the injected `WorktreeManager`.
|
|
93
|
+
|
|
94
|
+
## Module-Level Changes
|
|
95
|
+
|
|
96
|
+
### `src/worktree.ts` (modified)
|
|
97
|
+
|
|
98
|
+
- Add `WorktreeManager` interface (3 methods).
|
|
99
|
+
- Add `GitWorktreeManager` class implementing the interface.
|
|
100
|
+
- Existing free functions, types, and imports unchanged.
|
|
101
|
+
|
|
102
|
+
### `test/worktree.test.ts` (modified)
|
|
103
|
+
|
|
104
|
+
- Add a new `describe("GitWorktreeManager")` block with tests for the class.
|
|
105
|
+
- Existing free-function tests stay as-is.
|
|
106
|
+
|
|
107
|
+
## Test Impact Analysis
|
|
108
|
+
|
|
109
|
+
### New unit tests enabled
|
|
110
|
+
|
|
111
|
+
1. `GitWorktreeManager.create` — delegates to `createWorktree` with the captured `cwd`.
|
|
112
|
+
2. `GitWorktreeManager.cleanup` — delegates to `cleanupWorktree` with the captured `cwd`.
|
|
113
|
+
3. `GitWorktreeManager.prune` — delegates to `pruneWorktrees` with the captured `cwd`.
|
|
114
|
+
|
|
115
|
+
These are thin delegation tests that verify the class wires `cwd` correctly.
|
|
116
|
+
They reuse the same real-git-repo test infrastructure already in `worktree.test.ts`.
|
|
117
|
+
|
|
118
|
+
### Existing tests
|
|
119
|
+
|
|
120
|
+
All existing tests in `worktree.test.ts` stay unchanged — they test the free functions directly.
|
|
121
|
+
|
|
122
|
+
## TDD Order
|
|
123
|
+
|
|
124
|
+
1. **RED:** Add `describe("GitWorktreeManager")` with tests for `create`, `cleanup`, and `prune`.
|
|
125
|
+
Tests import `GitWorktreeManager` which does not exist yet — compilation fails.
|
|
126
|
+
Commit: `test: add GitWorktreeManager tests`
|
|
127
|
+
|
|
128
|
+
2. **GREEN:** Add `WorktreeManager` interface and `GitWorktreeManager` class to `worktree.ts`.
|
|
129
|
+
All tests pass.
|
|
130
|
+
Run `pnpm run check` to verify types.
|
|
131
|
+
Commit: `feat: extract WorktreeManager interface and GitWorktreeManager class (#84)`
|
|
132
|
+
|
|
133
|
+
## Risks and Mitigations
|
|
134
|
+
|
|
135
|
+
| Risk | Mitigation |
|
|
136
|
+
| ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
137
|
+
| Class delegation tests are slow because they create real git repos | The existing test suite already does this; the new tests reuse the same `initGitRepo()` helper and add ~3 test cases to an already-fast suite. |
|
|
138
|
+
| Future callers bypass the interface and use `GitWorktreeManager` directly | The #72 plan types `AgentManager`'s constructor parameter as `WorktreeManager` (the interface), not the class. Code review enforces this. |
|
|
139
|
+
|
|
140
|
+
## Open Questions
|
|
141
|
+
|
|
142
|
+
None — the interface shape is specified by the issue and matches the #72 plan exactly.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 76
|
|
3
|
+
issue_title: "refactor: inject cwd into AgentManager constructor instead of reading process.cwd() in dispose()"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #76 — inject cwd into AgentManager constructor
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-19T21:00:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Planned, implemented, and shipped a single-step refactoring that injects `cwd: string` into the `AgentManager` constructor, replacing the `process.cwd()` call in `dispose()` with `this.cwd`.
|
|
13
|
+
Released as `pi-subagents-v5.4.1`.
|
|
14
|
+
The entire cycle (plan → TDD → ship → release) completed in one session with one minor friction point.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- Clean single-commit implementation: one `refactor:` commit touched 3 files, updated 18 test constructor calls plus one production call site, and added one new assertion — all green on first run.
|
|
21
|
+
- TDD Red phase worked well despite the plan calling this a "single-step refactoring."
|
|
22
|
+
Writing a new test (`"calls pruneWorktrees with the cwd passed to the constructor"`) gave a clear Red signal before the implementation change, even though the constructor signature change had to be applied atomically.
|
|
23
|
+
|
|
24
|
+
#### What caused friction (agent side)
|
|
25
|
+
|
|
26
|
+
- `wrong-abstraction` — The plan's "Test Impact Analysis" stated "No new unit tests are needed" and framed existing tests as sufficient.
|
|
27
|
+
In practice, the existing tests only called `dispose()` in `afterEach` hooks without assertions on `pruneWorktrees` arguments, so a new test was needed for a proper Red phase.
|
|
28
|
+
The user noticed the discrepancy before TDD began ("We will at least alter some tests, right?").
|
|
29
|
+
Impact: one clarifying exchange, no rework.
|
|
30
|
+
User-caught.
|
|
31
|
+
|
|
32
|
+
#### What caused friction (user side)
|
|
33
|
+
|
|
34
|
+
- None observed.
|
|
35
|
+
The user's question about test changes was a useful early catch that would have surfaced during TDD anyway.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
issue: 84
|
|
3
|
+
issue_title: "refactor: extract GitWorktreeManager class from worktree.ts free functions"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Retro: #84 — extract GitWorktreeManager class from worktree.ts free functions
|
|
7
|
+
|
|
8
|
+
## Final Retrospective (2026-05-20T13:31:00Z)
|
|
9
|
+
|
|
10
|
+
### Session summary
|
|
11
|
+
|
|
12
|
+
Extracted a `WorktreeManager` interface and `GitWorktreeManager` class from the three free functions in `worktree.ts`.
|
|
13
|
+
The two-step TDD cycle (add tests → add implementation) executed cleanly with no rework or deviations from the plan.
|
|
14
|
+
Released as `pi-subagents-v5.5.0`.
|
|
15
|
+
|
|
16
|
+
### Observations
|
|
17
|
+
|
|
18
|
+
#### What went well
|
|
19
|
+
|
|
20
|
+
- The issue body included an exact "Proposed Interface" section with TypeScript code, which made the plan nearly mechanical and eliminated all design ambiguity.
|
|
21
|
+
The `ask-user` step was correctly skipped.
|
|
22
|
+
- The two-step TDD cycle was appropriately minimal for a thin delegation extraction — no over-engineering of the test or commit structure.
|
|
23
|
+
- The full pipeline (plan → TDD → ship → release) completed in a single pass with zero corrections.
|
|
24
|
+
|
|
25
|
+
#### What caused friction (agent side)
|
|
26
|
+
|
|
27
|
+
No friction observed.
|
|
28
|
+
The issue was well-scoped and the existing pipeline instructions handled every step.
|
|
29
|
+
|
|
30
|
+
#### What caused friction (user side)
|
|
31
|
+
|
|
32
|
+
No friction observed.
|
|
33
|
+
The pipeline was driven cleanly with `/plan-issue` → `/tdd-plan` → `/ship-issue`.
|
|
34
|
+
|
|
35
|
+
### Changes made
|
|
36
|
+
|
|
37
|
+
1. Retro file created at `packages/pi-subagents/docs/retro/0084-extract-git-worktree-manager.md`.
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -9,12 +9,12 @@
|
|
|
9
9
|
import { randomUUID } from "node:crypto";
|
|
10
10
|
import type { Model } from "@earendil-works/pi-ai";
|
|
11
11
|
import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
12
|
-
import {
|
|
12
|
+
import type { AgentRunner, ToolActivity } from "./agent-runner.js";
|
|
13
13
|
import { debugLog } from "./debug.js";
|
|
14
14
|
import type { RunConfig } from "./runtime.js";
|
|
15
15
|
import type { AgentInvocation, AgentRecord, IsolationMode, SubagentType, ThinkingLevel } from "./types.js";
|
|
16
16
|
import { addUsage } from "./usage.js";
|
|
17
|
-
import {
|
|
17
|
+
import type { WorktreeManager } from "./worktree.js";
|
|
18
18
|
|
|
19
19
|
export type OnAgentComplete = (record: AgentRecord) => void;
|
|
20
20
|
export type OnAgentStart = (record: AgentRecord) => void;
|
|
@@ -24,6 +24,16 @@ export type CompactionInfo = { reason: "manual" | "threshold" | "overflow"; toke
|
|
|
24
24
|
/** Default max concurrent background agents. */
|
|
25
25
|
const DEFAULT_MAX_CONCURRENT = 4;
|
|
26
26
|
|
|
27
|
+
export interface AgentManagerOptions {
|
|
28
|
+
runner: AgentRunner;
|
|
29
|
+
worktrees: WorktreeManager;
|
|
30
|
+
maxConcurrent?: number;
|
|
31
|
+
getRunConfig?: () => RunConfig;
|
|
32
|
+
onStart?: OnAgentStart;
|
|
33
|
+
onComplete?: OnAgentComplete;
|
|
34
|
+
onCompact?: OnAgentCompact;
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
interface SpawnArgs {
|
|
28
38
|
pi: ExtensionAPI;
|
|
29
39
|
ctx: ExtensionContext;
|
|
@@ -72,7 +82,8 @@ export class AgentManager {
|
|
|
72
82
|
private onComplete?: OnAgentComplete;
|
|
73
83
|
private onStart?: OnAgentStart;
|
|
74
84
|
private onCompact?: OnAgentCompact;
|
|
75
|
-
private readonly
|
|
85
|
+
private readonly runner: AgentRunner;
|
|
86
|
+
private readonly worktrees: WorktreeManager;
|
|
76
87
|
private maxConcurrent: number;
|
|
77
88
|
private getRunConfig?: () => RunConfig;
|
|
78
89
|
|
|
@@ -81,20 +92,14 @@ export class AgentManager {
|
|
|
81
92
|
/** Number of currently running background agents. */
|
|
82
93
|
private runningBackground = 0;
|
|
83
94
|
|
|
84
|
-
constructor(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
onStart
|
|
89
|
-
onCompact
|
|
90
|
-
getRunConfig
|
|
91
|
-
|
|
92
|
-
this.cwd = cwd;
|
|
93
|
-
this.onComplete = onComplete;
|
|
94
|
-
this.onStart = onStart;
|
|
95
|
-
this.onCompact = onCompact;
|
|
96
|
-
this.getRunConfig = getRunConfig;
|
|
97
|
-
this.maxConcurrent = maxConcurrent;
|
|
95
|
+
constructor(options: AgentManagerOptions) {
|
|
96
|
+
this.runner = options.runner;
|
|
97
|
+
this.worktrees = options.worktrees;
|
|
98
|
+
this.onComplete = options.onComplete;
|
|
99
|
+
this.onStart = options.onStart;
|
|
100
|
+
this.onCompact = options.onCompact;
|
|
101
|
+
this.getRunConfig = options.getRunConfig;
|
|
102
|
+
this.maxConcurrent = options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
|
|
98
103
|
// Cleanup completed agents after 10 minutes (but keep sessions for resume)
|
|
99
104
|
this.cleanupInterval = setInterval(() => this.cleanup(), 60_000);
|
|
100
105
|
this.cleanupInterval.unref();
|
|
@@ -164,7 +169,7 @@ export class AgentManager {
|
|
|
164
169
|
// BEFORE state mutation so a throw doesn't leave the record half-running.
|
|
165
170
|
let worktreeCwd: string | undefined;
|
|
166
171
|
if (options.isolation === "worktree") {
|
|
167
|
-
const wt =
|
|
172
|
+
const wt = this.worktrees.create(id);
|
|
168
173
|
if (!wt) {
|
|
169
174
|
throw new Error(
|
|
170
175
|
'Cannot run with isolation: "worktree" — not a git repo, no commits yet, or `git worktree add` failed. ' +
|
|
@@ -190,7 +195,7 @@ export class AgentManager {
|
|
|
190
195
|
const detach = () => { detachParentSignal?.(); detachParentSignal = undefined; };
|
|
191
196
|
|
|
192
197
|
const runConfig = this.getRunConfig?.();
|
|
193
|
-
const promise =
|
|
198
|
+
const promise = this.runner.run(ctx, type, prompt, {
|
|
194
199
|
pi,
|
|
195
200
|
model: options.model,
|
|
196
201
|
maxTurns: options.maxTurns,
|
|
@@ -247,7 +252,7 @@ export class AgentManager {
|
|
|
247
252
|
|
|
248
253
|
// Clean up worktree if used
|
|
249
254
|
if (record.worktree) {
|
|
250
|
-
const wtResult =
|
|
255
|
+
const wtResult = this.worktrees.cleanup(record.worktree, options.description);
|
|
251
256
|
record.worktreeResult = wtResult;
|
|
252
257
|
if (wtResult.hasChanges && wtResult.branch) {
|
|
253
258
|
record.result = (record.result ?? "") +
|
|
@@ -281,7 +286,7 @@ export class AgentManager {
|
|
|
281
286
|
// Best-effort worktree cleanup on error
|
|
282
287
|
if (record.worktree) {
|
|
283
288
|
try {
|
|
284
|
-
const wtResult =
|
|
289
|
+
const wtResult = this.worktrees.cleanup(record.worktree, options.description);
|
|
285
290
|
record.worktreeResult = wtResult;
|
|
286
291
|
} catch (err) { debugLog("cleanupWorktree on agent error", err); }
|
|
287
292
|
}
|
|
@@ -351,7 +356,7 @@ export class AgentManager {
|
|
|
351
356
|
record.error = undefined;
|
|
352
357
|
|
|
353
358
|
try {
|
|
354
|
-
const responseText = await
|
|
359
|
+
const responseText = await this.runner.resume(record.session, prompt, {
|
|
355
360
|
onToolActivity: (activity) => {
|
|
356
361
|
if (activity.type === "end") record.toolUses++;
|
|
357
362
|
},
|
|
@@ -488,6 +493,6 @@ export class AgentManager {
|
|
|
488
493
|
}
|
|
489
494
|
this.agents.clear();
|
|
490
495
|
// Prune any orphaned git worktrees (crash recovery)
|
|
491
|
-
try {
|
|
496
|
+
try { this.worktrees.prune(); } catch (err) { debugLog("pruneWorktrees on dispose", err); }
|
|
492
497
|
}
|
|
493
498
|
}
|
package/src/agent-runner.ts
CHANGED
|
@@ -129,6 +129,23 @@ export interface RunResult {
|
|
|
129
129
|
steered: boolean;
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
+
/** Options for resuming an existing agent session. */
|
|
133
|
+
export interface ResumeOptions {
|
|
134
|
+
onToolActivity?: (activity: ToolActivity) => void;
|
|
135
|
+
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
136
|
+
onCompaction?: (info: { reason: "manual" | "threshold" | "overflow"; tokensBefore: number }) => void;
|
|
137
|
+
signal?: AbortSignal;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Execution boundary: decouples AgentManager (lifecycle management) from the
|
|
142
|
+
* SDK session orchestration in runAgent/resumeAgent.
|
|
143
|
+
*/
|
|
144
|
+
export interface AgentRunner {
|
|
145
|
+
run(ctx: ExtensionContext, type: SubagentType, prompt: string, options: RunOptions): Promise<RunResult>;
|
|
146
|
+
resume(session: AgentSession, prompt: string, options?: ResumeOptions): Promise<string>;
|
|
147
|
+
}
|
|
148
|
+
|
|
132
149
|
/**
|
|
133
150
|
* Subscribe to a session and collect the last assistant message text.
|
|
134
151
|
* Returns an object with a `getText()` getter and an `unsubscribe` function.
|
|
@@ -375,19 +392,7 @@ export async function runAgent(
|
|
|
375
392
|
export async function resumeAgent(
|
|
376
393
|
session: AgentSession,
|
|
377
394
|
prompt: string,
|
|
378
|
-
options: {
|
|
379
|
-
onToolActivity?: (activity: ToolActivity) => void;
|
|
380
|
-
onAssistantUsage?: (usage: {
|
|
381
|
-
input: number;
|
|
382
|
-
output: number;
|
|
383
|
-
cacheWrite: number;
|
|
384
|
-
}) => void;
|
|
385
|
-
onCompaction?: (info: {
|
|
386
|
-
reason: "manual" | "threshold" | "overflow";
|
|
387
|
-
tokensBefore: number;
|
|
388
|
-
}) => void;
|
|
389
|
-
signal?: AbortSignal;
|
|
390
|
-
} = {},
|
|
395
|
+
options: ResumeOptions = {},
|
|
391
396
|
): Promise<string> {
|
|
392
397
|
const collector = collectResponseText(session);
|
|
393
398
|
const cleanupAbort = forwardAbortSignal(session, options.signal);
|
package/src/index.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { defineTool, type ExtensionAPI, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { AgentManager } from "./agent-manager.js";
|
|
16
|
-
import { getAgentConversation, normalizeMaxTurns, steerAgent } from "./agent-runner.js";
|
|
16
|
+
import { getAgentConversation, normalizeMaxTurns, resumeAgent, runAgent, steerAgent } from "./agent-runner.js";
|
|
17
17
|
import { getAvailableTypes, getDefaultAgentNames, getUserAgentNames, registerAgents, resolveAgentConfig, } from "./agent-types.js";
|
|
18
18
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
19
19
|
import { type ModelRegistry, resolveModel } from "./model-resolver.js";
|
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
AgentWidget,
|
|
34
34
|
type UICtx,
|
|
35
35
|
} from "./ui/agent-widget.js";
|
|
36
|
+
import { GitWorktreeManager } from "./worktree.js";
|
|
36
37
|
|
|
37
38
|
export default function (pi: ExtensionAPI) {
|
|
38
39
|
// ---- Register custom notification renderer ----
|
|
@@ -61,49 +62,55 @@ export default function (pi: ExtensionAPI) {
|
|
|
61
62
|
});
|
|
62
63
|
|
|
63
64
|
// Background completion: emit lifecycle event and delegate to notification system
|
|
64
|
-
const manager = new AgentManager(
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
65
|
+
const manager = new AgentManager({
|
|
66
|
+
runner: { run: runAgent, resume: resumeAgent },
|
|
67
|
+
worktrees: new GitWorktreeManager(process.cwd()),
|
|
68
|
+
onComplete: (record) => {
|
|
69
|
+
// Emit lifecycle event based on terminal status
|
|
70
|
+
const isError = record.status === "error" || record.status === "stopped" || record.status === "aborted";
|
|
71
|
+
const eventData = buildEventData(record);
|
|
72
|
+
if (isError) {
|
|
73
|
+
pi.events.emit("subagents:failed", eventData);
|
|
74
|
+
} else {
|
|
75
|
+
pi.events.emit("subagents:completed", eventData);
|
|
76
|
+
}
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
78
|
+
// Persist final record for cross-extension history reconstruction
|
|
79
|
+
pi.appendEntry("subagents:record", {
|
|
80
|
+
id: record.id, type: record.type, description: record.description,
|
|
81
|
+
status: record.status, result: record.result, error: record.error,
|
|
82
|
+
startedAt: record.startedAt, completedAt: record.completedAt,
|
|
83
|
+
});
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
// Skip notification if result was already consumed via get_subagent_result
|
|
86
|
+
if (record.resultConsumed) {
|
|
87
|
+
notifications.cleanupCompleted(record.id);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
91
|
+
notifications.sendCompletion(record);
|
|
92
|
+
},
|
|
93
|
+
onStart: (record) => {
|
|
94
|
+
// Emit started event when agent transitions to running (including from queue)
|
|
95
|
+
pi.events.emit("subagents:started", {
|
|
96
|
+
id: record.id,
|
|
97
|
+
type: record.type,
|
|
98
|
+
description: record.description,
|
|
99
|
+
});
|
|
100
|
+
},
|
|
101
|
+
onCompact: (record, info) => {
|
|
102
|
+
// Emit compacted event when agent's session compacts (preserves count on record).
|
|
103
|
+
pi.events.emit("subagents:compacted", {
|
|
104
|
+
id: record.id,
|
|
105
|
+
type: record.type,
|
|
106
|
+
description: record.description,
|
|
107
|
+
reason: info.reason,
|
|
108
|
+
tokensBefore: info.tokensBefore,
|
|
109
|
+
compactionCount: record.compactionCount,
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
getRunConfig: () => ({ defaultMaxTurns: runtime.defaultMaxTurns, graceTurns: runtime.graceTurns }),
|
|
113
|
+
});
|
|
107
114
|
|
|
108
115
|
// Typed service published via Symbol.for() for cross-extension access.
|
|
109
116
|
// Consumers: const { getSubagentsService } = await import("@gotgenes/pi-subagents");
|
package/src/worktree.ts
CHANGED
|
@@ -162,3 +162,33 @@ export function pruneWorktrees(cwd: string): void {
|
|
|
162
162
|
execFileSync("git", ["worktree", "prune"], { cwd, stdio: "pipe", timeout: 5000 });
|
|
163
163
|
} catch (err) { debugLog("pruneWorktrees", err); }
|
|
164
164
|
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Interface for managing git worktrees relative to a fixed repository root.
|
|
168
|
+
* Callers do not thread `cwd` per call — it is captured at construction time.
|
|
169
|
+
*/
|
|
170
|
+
export interface WorktreeManager {
|
|
171
|
+
create(id: string): WorktreeInfo | undefined;
|
|
172
|
+
cleanup(wt: WorktreeInfo, description: string): WorktreeCleanupResult;
|
|
173
|
+
prune(): void;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Concrete implementation of WorktreeManager backed by the free functions in this module.
|
|
178
|
+
* Captures `cwd` (the repository root) at construction and delegates each method.
|
|
179
|
+
*/
|
|
180
|
+
export class GitWorktreeManager implements WorktreeManager {
|
|
181
|
+
constructor(private readonly cwd: string) {}
|
|
182
|
+
|
|
183
|
+
create(id: string): WorktreeInfo | undefined {
|
|
184
|
+
return createWorktree(this.cwd, id);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
cleanup(wt: WorktreeInfo, description: string): WorktreeCleanupResult {
|
|
188
|
+
return cleanupWorktree(this.cwd, wt, description);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
prune(): void {
|
|
192
|
+
pruneWorktrees(this.cwd);
|
|
193
|
+
}
|
|
194
|
+
}
|