@gotgenes/pi-subagents 5.5.0 → 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
CHANGED
|
@@ -5,6 +5,20 @@ 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
|
+
|
|
8
22
|
## [5.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v5.4.1...pi-subagents-v5.5.0) (2026-05-20)
|
|
9
23
|
|
|
10
24
|
|
|
@@ -35,7 +35,7 @@ Any test of `AgentManager` must mock entire modules via `vi.mock()`, coupling th
|
|
|
35
35
|
| #71 | Extract pure agent-session assembler | ✓ Done |
|
|
36
36
|
| #76 | Inject `cwd` into `AgentManager` | ✓ Done |
|
|
37
37
|
| #80 | Consolidate `getConfig`/`getAgentConfig` | ✓ Done |
|
|
38
|
-
| #84 | Extract `GitWorktreeManager` class from worktree.ts |
|
|
38
|
+
| #84 | Extract `GitWorktreeManager` class from worktree.ts | ✓ Done |
|
|
39
39
|
|
|
40
40
|
### Prior art
|
|
41
41
|
|
|
@@ -201,7 +201,7 @@ expect(runner.run).toHaveBeenCalled();
|
|
|
201
201
|
|
|
202
202
|
### `src/worktree.ts` (no changes in this issue)
|
|
203
203
|
|
|
204
|
-
`WorktreeManager` interface and `GitWorktreeManager` class
|
|
204
|
+
`WorktreeManager` interface and `GitWorktreeManager` class were added by #84.
|
|
205
205
|
|
|
206
206
|
### `src/agent-runner.ts` (modified)
|
|
207
207
|
|
|
@@ -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");
|