@oai404iao/pi-subagent 0.3.0 → 0.4.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/README.md +294 -89
- package/agents/worker.md +1 -1
- package/config.example.json +3 -4
- package/config.schema.json +26 -20
- package/index.ts +2 -0
- package/package.json +13 -12
- package/src/agent-state.ts +125 -0
- package/src/agent-sync.ts +171 -88
- package/src/agents.ts +3 -22
- package/src/catalog.ts +47 -0
- package/src/completion-mailbox.ts +656 -0
- package/src/config.ts +43 -35
- package/src/coordinator.ts +2172 -326
- package/src/descriptor.ts +96 -33
- package/src/index.ts +176 -58
- package/src/mailbox.ts +451 -0
- package/src/providers.ts +221 -28
- package/src/render.ts +23 -16
- package/src/scheduler.ts +173 -0
- package/src/schemas.ts +76 -38
- package/src/task-path.ts +146 -0
- package/src/types.ts +53 -15
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oai404iao/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Durable, continuable subagents for Pi with spawn/fork providers and lifecycle controls.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -27,24 +27,24 @@
|
|
|
27
27
|
},
|
|
28
28
|
"pi": {
|
|
29
29
|
"extensions": [
|
|
30
|
-
"./
|
|
30
|
+
"./index.ts"
|
|
31
31
|
]
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
35
|
-
"@earendil-works/pi-ai": ">=0.
|
|
36
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
37
|
-
"@earendil-works/pi-tui": ">=0.
|
|
34
|
+
"@earendil-works/pi-agent-core": ">=0.85.1",
|
|
35
|
+
"@earendil-works/pi-ai": ">=0.85.1",
|
|
36
|
+
"@earendil-works/pi-coding-agent": ">=0.85.1",
|
|
37
|
+
"@earendil-works/pi-tui": ">=0.85.1",
|
|
38
38
|
"typebox": "*"
|
|
39
39
|
},
|
|
40
40
|
"optionalDependencies": {
|
|
41
|
-
"@oai404iao/pi-codex-minimal-tools": "
|
|
41
|
+
"@oai404iao/pi-codex-minimal-tools": "2.0.0"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
|
-
"@earendil-works/pi-agent-core": "
|
|
45
|
-
"@earendil-works/pi-ai": "
|
|
46
|
-
"@earendil-works/pi-coding-agent": "
|
|
47
|
-
"@earendil-works/pi-tui": "
|
|
44
|
+
"@earendil-works/pi-agent-core": "0.85.1",
|
|
45
|
+
"@earendil-works/pi-ai": "0.85.1",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
47
|
+
"@earendil-works/pi-tui": "0.85.1",
|
|
48
48
|
"@types/node": "^26.2.0",
|
|
49
49
|
"tsx": "^4.20.6",
|
|
50
50
|
"typebox": "^1.1.24",
|
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
"check": "npm run typecheck && npm test"
|
|
57
57
|
},
|
|
58
58
|
"files": [
|
|
59
|
+
"index.ts",
|
|
59
60
|
"agents/",
|
|
60
61
|
"LICENSES/",
|
|
61
62
|
"provenance/",
|
|
@@ -87,5 +88,5 @@
|
|
|
87
88
|
"optional": true
|
|
88
89
|
}
|
|
89
90
|
},
|
|
90
|
-
"gitHead": "
|
|
91
|
+
"gitHead": "88cb6f56447d88b71232cd764ca5b29251341d23"
|
|
91
92
|
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DelegationDetails,
|
|
3
|
+
SubagentStopReason,
|
|
4
|
+
} from "./types.ts";
|
|
5
|
+
|
|
6
|
+
export type AgentLifecycleState = "open" | "closed";
|
|
7
|
+
export type AgentResidencyState = "resident" | "unloaded";
|
|
8
|
+
|
|
9
|
+
export type AgentTurnState =
|
|
10
|
+
| { state: "none" }
|
|
11
|
+
| { state: "queued"; turnId: string }
|
|
12
|
+
| { state: "running"; turnId: string }
|
|
13
|
+
| { state: "completed"; turnId: string }
|
|
14
|
+
| { state: "errored"; turnId: string }
|
|
15
|
+
| { state: "interrupted"; turnId: string };
|
|
16
|
+
|
|
17
|
+
export interface AgentControlState {
|
|
18
|
+
lifecycle: AgentLifecycleState;
|
|
19
|
+
residency: AgentResidencyState;
|
|
20
|
+
turn: AgentTurnState;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createAgentControlState(): AgentControlState {
|
|
24
|
+
return {
|
|
25
|
+
lifecycle: "open",
|
|
26
|
+
residency: "resident",
|
|
27
|
+
turn: { state: "none" },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function queueAgentTurn(state: AgentControlState, turnId: string): void {
|
|
32
|
+
if (state.turn.state === "queued" || state.turn.state === "running") {
|
|
33
|
+
throw new Error(`agent turn ${state.turn.turnId} is already active`);
|
|
34
|
+
}
|
|
35
|
+
state.turn = { state: "queued", turnId };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function startAgentTurn(state: AgentControlState, turnId: string): void {
|
|
39
|
+
if (state.turn.state !== "queued" || state.turn.turnId !== turnId) {
|
|
40
|
+
throw new Error(`agent turn ${turnId} was not queued`);
|
|
41
|
+
}
|
|
42
|
+
state.turn = { state: "running", turnId };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function finishAgentTurn(
|
|
46
|
+
state: AgentControlState,
|
|
47
|
+
turnId: string,
|
|
48
|
+
stopReason: SubagentStopReason,
|
|
49
|
+
): void {
|
|
50
|
+
if (
|
|
51
|
+
(state.turn.state !== "queued" &&
|
|
52
|
+
state.turn.state !== "running" &&
|
|
53
|
+
state.turn.state !== "interrupted") ||
|
|
54
|
+
state.turn.turnId !== turnId
|
|
55
|
+
) {
|
|
56
|
+
throw new Error(`agent turn ${turnId} is not active`);
|
|
57
|
+
}
|
|
58
|
+
switch (stopReason) {
|
|
59
|
+
case "completed":
|
|
60
|
+
state.turn = { state: "completed", turnId };
|
|
61
|
+
return;
|
|
62
|
+
case "aborted":
|
|
63
|
+
state.turn = { state: "interrupted", turnId };
|
|
64
|
+
return;
|
|
65
|
+
case "error":
|
|
66
|
+
case "max-tokens":
|
|
67
|
+
state.turn = { state: "errored", turnId };
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function interruptAgentTurn(state: AgentControlState): void {
|
|
73
|
+
if (state.turn.state === "queued" || state.turn.state === "running") {
|
|
74
|
+
state.turn = { state: "interrupted", turnId: state.turn.turnId };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function setAgentResidency(
|
|
79
|
+
state: AgentControlState,
|
|
80
|
+
residency: AgentResidencyState,
|
|
81
|
+
): void {
|
|
82
|
+
state.residency = residency;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function closeAgent(state: AgentControlState): void {
|
|
86
|
+
state.lifecycle = "closed";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function currentAgentTurnId(state: AgentControlState): string | undefined {
|
|
90
|
+
return state.turn.state === "none" ? undefined : state.turn.turnId;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function delegationStatus(
|
|
94
|
+
state: AgentControlState,
|
|
95
|
+
hasOwnedChildren: boolean,
|
|
96
|
+
): DelegationDetails["status"] {
|
|
97
|
+
switch (state.turn.state) {
|
|
98
|
+
case "none":
|
|
99
|
+
case "queued":
|
|
100
|
+
return "starting";
|
|
101
|
+
case "running":
|
|
102
|
+
return "running";
|
|
103
|
+
case "completed":
|
|
104
|
+
return hasOwnedChildren ? "waiting" : "completed";
|
|
105
|
+
case "errored":
|
|
106
|
+
case "interrupted":
|
|
107
|
+
return hasOwnedChildren ? "waiting" : "failed";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function catalogStatus(
|
|
112
|
+
state: AgentControlState,
|
|
113
|
+
): "running" | "idle" | "ready" {
|
|
114
|
+
if (state.residency === "unloaded") return "ready";
|
|
115
|
+
switch (state.turn.state) {
|
|
116
|
+
case "queued":
|
|
117
|
+
case "running":
|
|
118
|
+
return "running";
|
|
119
|
+
case "none":
|
|
120
|
+
case "completed":
|
|
121
|
+
case "errored":
|
|
122
|
+
case "interrupted":
|
|
123
|
+
return "idle";
|
|
124
|
+
}
|
|
125
|
+
}
|
package/src/agent-sync.ts
CHANGED
|
@@ -30,8 +30,18 @@ interface AgentManifest {
|
|
|
30
30
|
version: 1;
|
|
31
31
|
packageVersion: string;
|
|
32
32
|
files: Record<string, string>;
|
|
33
|
+
/**
|
|
34
|
+
* Bundled preset names the user deleted. Initialization never restores
|
|
35
|
+
* them, so deleting a preset stays effective across package versions.
|
|
36
|
+
*/
|
|
37
|
+
retired: string[];
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
type ManifestReadResult =
|
|
41
|
+
| { kind: "missing" }
|
|
42
|
+
| { kind: "valid"; manifest: AgentManifest }
|
|
43
|
+
| { kind: "corrupt"; diagnostic: string };
|
|
44
|
+
|
|
35
45
|
interface BundledAgentFile {
|
|
36
46
|
name: string;
|
|
37
47
|
content: Buffer;
|
|
@@ -69,7 +79,14 @@ export interface AgentSyncResult {
|
|
|
69
79
|
manifestPath: string;
|
|
70
80
|
installed: string[];
|
|
71
81
|
updated: string[];
|
|
82
|
+
/** Bundled presets retired upstream and removed from the user directory. */
|
|
72
83
|
removed: string[];
|
|
84
|
+
/** Presets the user deleted; initialization keeps them absent. */
|
|
85
|
+
retired: string[];
|
|
86
|
+
/** Previously deleted presets that reappeared on disk as managed again. */
|
|
87
|
+
restored: string[];
|
|
88
|
+
/** True when this run recorded a new deletion or a recreation. */
|
|
89
|
+
retirementChanged: boolean;
|
|
73
90
|
preserved: string[];
|
|
74
91
|
backups: AgentBackup[];
|
|
75
92
|
diagnostics: string[];
|
|
@@ -130,10 +147,27 @@ function parseManifest(value: unknown, manifestPath: string): AgentManifest {
|
|
|
130
147
|
}
|
|
131
148
|
files[name] = hash;
|
|
132
149
|
}
|
|
150
|
+
const retired: string[] = [];
|
|
151
|
+
if (input.retired !== undefined) {
|
|
152
|
+
if (!Array.isArray(input.retired)) {
|
|
153
|
+
throw new Error("retired must be an array of bundled preset file names");
|
|
154
|
+
}
|
|
155
|
+
for (const name of input.retired) {
|
|
156
|
+
if (
|
|
157
|
+
typeof name !== "string" ||
|
|
158
|
+
basename(name) !== name ||
|
|
159
|
+
!name.endsWith(".md")
|
|
160
|
+
) {
|
|
161
|
+
throw new Error(`retired contains an invalid entry for "${String(name)}"`);
|
|
162
|
+
}
|
|
163
|
+
if (!retired.includes(name)) retired.push(name);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
133
166
|
return {
|
|
134
167
|
version: MANIFEST_VERSION,
|
|
135
168
|
packageVersion: input.packageVersion.trim(),
|
|
136
169
|
files,
|
|
170
|
+
retired,
|
|
137
171
|
};
|
|
138
172
|
}
|
|
139
173
|
|
|
@@ -182,53 +216,35 @@ function bundledAgentFiles(bundledDir: string): BundledAgentFile[] {
|
|
|
182
216
|
return files;
|
|
183
217
|
}
|
|
184
218
|
|
|
185
|
-
function readPreviousManifest(manifestPath: string):
|
|
186
|
-
if (!existsSync(manifestPath)) return
|
|
219
|
+
function readPreviousManifest(manifestPath: string): ManifestReadResult {
|
|
220
|
+
if (!existsSync(manifestPath)) return { kind: "missing" };
|
|
221
|
+
let content: Buffer | undefined;
|
|
187
222
|
try {
|
|
188
|
-
|
|
223
|
+
content = readFileSync(manifestPath);
|
|
224
|
+
const manifest = parseManifest(JSON.parse(content.toString("utf8")), manifestPath);
|
|
225
|
+
return { kind: "valid", manifest };
|
|
189
226
|
} catch (error) {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
);
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/**
|
|
201
|
-
* Identify untouched files created by the old opt-out synchronizer without
|
|
202
|
-
* changing the user filesystem. Direct bundled discovery can then use newer
|
|
203
|
-
* package definitions while real user edits continue to override them.
|
|
204
|
-
*/
|
|
205
|
-
export function unmodifiedManagedAgentNames(agentDir: string): Set<string> {
|
|
206
|
-
const manifestPath = join(agentDir, STATE_DIR_NAME, MANIFEST_FILE_NAME);
|
|
207
|
-
if (!existsSync(manifestPath)) return new Set();
|
|
208
|
-
|
|
209
|
-
let manifest: AgentManifest;
|
|
210
|
-
try {
|
|
211
|
-
manifest = parseManifest(JSON.parse(readFileSync(manifestPath, "utf8")), manifestPath);
|
|
212
|
-
} catch {
|
|
213
|
-
// A malformed historical manifest must never cause a default read-only
|
|
214
|
-
// session to hide user files or rewrite state.
|
|
215
|
-
return new Set();
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const unmodified = new Set<string>();
|
|
219
|
-
const userAgentsDir = join(agentDir, "agents");
|
|
220
|
-
for (const [name, expectedHash] of Object.entries(manifest.files)) {
|
|
227
|
+
const fingerprint =
|
|
228
|
+
typeof content === "undefined"
|
|
229
|
+
? randomUUID().slice(0, 12)
|
|
230
|
+
: hash(content).slice(0, 12);
|
|
231
|
+
const corruptPath = `${manifestPath}.corrupt-${fingerprint}`;
|
|
232
|
+
let preservation = `a copy was preserved at ${corruptPath}`;
|
|
221
233
|
try {
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
// Missing, unreadable, or replaced paths are user-controlled and
|
|
228
|
-
// therefore remain visible to discovery.
|
|
234
|
+
if (!existsSync(corruptPath)) copyFileSync(manifestPath, corruptPath);
|
|
235
|
+
} catch (backupError) {
|
|
236
|
+
preservation = `the corrupt file could not be copied: ${
|
|
237
|
+
backupError instanceof Error ? backupError.message : String(backupError)
|
|
238
|
+
}`;
|
|
229
239
|
}
|
|
240
|
+
return {
|
|
241
|
+
kind: "corrupt",
|
|
242
|
+
diagnostic:
|
|
243
|
+
`${manifestPath}: invalid manifest; bundled-template initialization was skipped and user/project agents remain available; ${preservation}: ${
|
|
244
|
+
error instanceof Error ? error.message : String(error)
|
|
245
|
+
}`,
|
|
246
|
+
};
|
|
230
247
|
}
|
|
231
|
-
return unmodified;
|
|
232
248
|
}
|
|
233
249
|
|
|
234
250
|
function sleepSync(milliseconds: number): void {
|
|
@@ -445,71 +461,135 @@ function syncBundledAgentsLocked(
|
|
|
445
461
|
manifestPath: string,
|
|
446
462
|
): AgentSyncResult {
|
|
447
463
|
const diagnostics: string[] = [];
|
|
448
|
-
const
|
|
449
|
-
|
|
464
|
+
const manifestRead = readPreviousManifest(manifestPath);
|
|
465
|
+
if (manifestRead.kind === "corrupt") {
|
|
466
|
+
return {
|
|
467
|
+
packageVersion,
|
|
468
|
+
userAgentsDir,
|
|
469
|
+
manifestPath,
|
|
470
|
+
installed: [],
|
|
471
|
+
updated: [],
|
|
472
|
+
removed: [],
|
|
473
|
+
retired: [],
|
|
474
|
+
restored: [],
|
|
475
|
+
retirementChanged: false,
|
|
476
|
+
preserved: [],
|
|
477
|
+
backups: [],
|
|
478
|
+
diagnostics: [manifestRead.diagnostic],
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
const previous = manifestRead.kind === "valid" ? manifestRead.manifest : undefined;
|
|
482
|
+
|
|
450
483
|
const files = bundledAgentFiles(options.bundledDir);
|
|
451
484
|
const currentNames = new Set(files.map((file) => file.name));
|
|
485
|
+
const previousFiles = previous?.files ?? {};
|
|
486
|
+
const retiredNames = new Set<string>();
|
|
487
|
+
const restored: string[] = [];
|
|
488
|
+
|
|
489
|
+
// A preset that was managed before and is missing now was deleted by the
|
|
490
|
+
// user. Record that deletion at every startup so a later package version
|
|
491
|
+
// cannot resurrect it; an explicit recreation restores management.
|
|
492
|
+
for (const name of previous?.retired ?? []) {
|
|
493
|
+
if (currentNames.has(name) && existsSync(join(userAgentsDir, name))) {
|
|
494
|
+
restored.push(name);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
for (const name of Object.keys(previousFiles)) {
|
|
498
|
+
if (!currentNames.has(name)) continue;
|
|
499
|
+
if (restored.includes(name)) continue;
|
|
500
|
+
if ((previous?.retired ?? []).includes(name)) {
|
|
501
|
+
retiredNames.add(name);
|
|
502
|
+
continue;
|
|
503
|
+
}
|
|
504
|
+
if (!existsSync(join(userAgentsDir, name))) retiredNames.add(name);
|
|
505
|
+
}
|
|
506
|
+
const recordedRetired = [...retiredNames].sort();
|
|
507
|
+
const retirementChanged =
|
|
508
|
+
restored.length > 0 ||
|
|
509
|
+
recordedRetired.join("\n") !== (previous?.retired ?? []).slice().sort().join("\n");
|
|
510
|
+
|
|
511
|
+
// The package copies are initialization templates, not a runtime fallback.
|
|
512
|
+
// Once this package version has been initialized, the user directory is
|
|
513
|
+
// authoritative: edits and deletions must survive every same-version start.
|
|
514
|
+
if (previous?.packageVersion === packageVersion && !retirementChanged) {
|
|
515
|
+
return {
|
|
516
|
+
packageVersion,
|
|
517
|
+
userAgentsDir,
|
|
518
|
+
manifestPath,
|
|
519
|
+
installed: [],
|
|
520
|
+
updated: [],
|
|
521
|
+
removed: [],
|
|
522
|
+
retired: recordedRetired,
|
|
523
|
+
restored: [],
|
|
524
|
+
retirementChanged: false,
|
|
525
|
+
preserved: [],
|
|
526
|
+
backups: [],
|
|
527
|
+
diagnostics,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
|
|
452
531
|
const preserved: string[] = [];
|
|
453
532
|
const actions: PlannedAction[] = [];
|
|
533
|
+
const versionChanged = previous?.packageVersion !== packageVersion;
|
|
534
|
+
|
|
535
|
+
// Agent files change only on first install and package-version changes. A
|
|
536
|
+
// same-version startup that merely recorded a deletion or a recreation only
|
|
537
|
+
// rewrites the manifest below.
|
|
538
|
+
if (versionChanged) {
|
|
539
|
+
// Plan the complete operation before changing any user agent file.
|
|
540
|
+
for (const file of files) {
|
|
541
|
+
if (retiredNames.has(file.name)) continue;
|
|
542
|
+
const destination = join(userAgentsDir, file.name);
|
|
543
|
+
const destinationState = destinationKind(destination);
|
|
544
|
+
if (destinationState.kind === "missing") {
|
|
545
|
+
actions.push({
|
|
546
|
+
name: file.name,
|
|
547
|
+
kind: "install",
|
|
548
|
+
destination,
|
|
549
|
+
destinationKind: "missing",
|
|
550
|
+
content: file.content,
|
|
551
|
+
});
|
|
552
|
+
continue;
|
|
553
|
+
}
|
|
454
554
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
555
|
+
if (
|
|
556
|
+
destinationState.kind === "file"
|
|
557
|
+
&& sameRegularFile(destination, destinationState.size, file.content)
|
|
558
|
+
) {
|
|
559
|
+
preserved.push(file.name);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
461
562
|
actions.push({
|
|
462
563
|
name: file.name,
|
|
463
|
-
kind: "
|
|
564
|
+
kind: "replace",
|
|
464
565
|
destination,
|
|
465
|
-
destinationKind:
|
|
566
|
+
destinationKind: destinationState.kind,
|
|
466
567
|
content: file.content,
|
|
467
568
|
});
|
|
468
|
-
continue;
|
|
469
569
|
}
|
|
470
570
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
571
|
+
// Retired bundled presets must not remain silently active. They are
|
|
572
|
+
// backed up like replacements, then removed; unrelated user-defined
|
|
573
|
+
// names remain.
|
|
574
|
+
for (const name of Object.keys(previousFiles).sort((left, right) => left.localeCompare(right))) {
|
|
575
|
+
if (currentNames.has(name)) continue;
|
|
576
|
+
const destination = join(userAgentsDir, name);
|
|
577
|
+
const destinationState = destinationKind(destination);
|
|
578
|
+
if (destinationState.kind === "missing") continue;
|
|
579
|
+
actions.push({
|
|
580
|
+
name,
|
|
581
|
+
kind: "remove",
|
|
582
|
+
destination,
|
|
583
|
+
destinationKind: destinationState.kind,
|
|
584
|
+
});
|
|
484
585
|
}
|
|
485
|
-
actions.push({
|
|
486
|
-
name: file.name,
|
|
487
|
-
kind: "replace",
|
|
488
|
-
destination,
|
|
489
|
-
destinationKind: destinationState.kind,
|
|
490
|
-
content: file.content,
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Retired bundled presets must not remain silently active. They are backed
|
|
495
|
-
// up like replacements, then removed; unrelated user-defined names remain.
|
|
496
|
-
for (const name of Object.keys(previous?.files ?? {}).sort((left, right) => left.localeCompare(right))) {
|
|
497
|
-
if (currentNames.has(name)) continue;
|
|
498
|
-
const destination = join(userAgentsDir, name);
|
|
499
|
-
const destinationState = destinationKind(destination);
|
|
500
|
-
if (destinationState.kind === "missing") continue;
|
|
501
|
-
actions.push({
|
|
502
|
-
name,
|
|
503
|
-
kind: "remove",
|
|
504
|
-
destination,
|
|
505
|
-
destinationKind: destinationState.kind,
|
|
506
|
-
});
|
|
507
586
|
}
|
|
508
587
|
|
|
509
588
|
const manifest: AgentManifest = {
|
|
510
589
|
version: MANIFEST_VERSION,
|
|
511
590
|
packageVersion,
|
|
512
591
|
files: Object.fromEntries(files.map((file) => [file.name, file.hash])),
|
|
592
|
+
retired: recordedRetired,
|
|
513
593
|
};
|
|
514
594
|
const stagedPaths: string[] = [];
|
|
515
595
|
const backups: AgentBackup[] = [];
|
|
@@ -576,6 +656,9 @@ function syncBundledAgentsLocked(
|
|
|
576
656
|
installed: actions.filter((action) => action.kind === "install").map((action) => action.name),
|
|
577
657
|
updated: actions.filter((action) => action.kind === "replace").map((action) => action.name),
|
|
578
658
|
removed: actions.filter((action) => action.kind === "remove").map((action) => action.name),
|
|
659
|
+
retired: recordedRetired,
|
|
660
|
+
restored: [...restored].sort(),
|
|
661
|
+
retirementChanged,
|
|
579
662
|
preserved,
|
|
580
663
|
backups,
|
|
581
664
|
diagnostics,
|
package/src/agents.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
4
4
|
import { CONFIG_DIR_NAME, getAgentDir, parseFrontmatter } from "@earendil-works/pi-coding-agent";
|
|
@@ -13,10 +13,7 @@ export interface AgentDiscoveryOptions {
|
|
|
13
13
|
cwd: string;
|
|
14
14
|
scope: AgentScope;
|
|
15
15
|
projectTrusted: boolean;
|
|
16
|
-
bundledDir: string;
|
|
17
16
|
agentDir?: string;
|
|
18
|
-
includeBundled?: boolean;
|
|
19
|
-
excludeUserAgentNames?: ReadonlySet<string>;
|
|
20
17
|
}
|
|
21
18
|
|
|
22
19
|
export interface AgentDiscoveryResult {
|
|
@@ -94,11 +91,7 @@ function loadAgentFile(filePath: string, source: AgentSource): AgentDefinition {
|
|
|
94
91
|
};
|
|
95
92
|
}
|
|
96
93
|
|
|
97
|
-
function loadDirectory(
|
|
98
|
-
dir: string,
|
|
99
|
-
source: AgentSource,
|
|
100
|
-
excludeNames?: ReadonlySet<string>,
|
|
101
|
-
): { agents: AgentDefinition[]; diagnostics: string[] } {
|
|
94
|
+
function loadDirectory(dir: string, source: AgentSource): { agents: AgentDefinition[]; diagnostics: string[] } {
|
|
102
95
|
if (!isDirectory(dir)) return { agents: [], diagnostics: [] };
|
|
103
96
|
const agents: AgentDefinition[] = [];
|
|
104
97
|
const diagnostics: string[] = [];
|
|
@@ -114,7 +107,6 @@ function loadDirectory(
|
|
|
114
107
|
|
|
115
108
|
for (const entry of entries.sort((left, right) => left.name.localeCompare(right.name))) {
|
|
116
109
|
if (!entry.name.endsWith(".md") || (!entry.isFile() && !entry.isSymbolicLink())) continue;
|
|
117
|
-
if (excludeNames?.has(entry.name)) continue;
|
|
118
110
|
const filePath = join(dir, entry.name);
|
|
119
111
|
try {
|
|
120
112
|
agents.push(loadAgentFile(filePath, source));
|
|
@@ -131,9 +123,6 @@ export function discoverAgents(options: AgentDiscoveryOptions): AgentDiscoveryRe
|
|
|
131
123
|
? findNearestProjectAgentsDir(options.cwd)
|
|
132
124
|
: undefined;
|
|
133
125
|
const sources: Array<{ dir: string; source: AgentSource }> = [];
|
|
134
|
-
if (options.includeBundled !== false) {
|
|
135
|
-
sources.push({ dir: options.bundledDir, source: "bundled" });
|
|
136
|
-
}
|
|
137
126
|
if (options.scope !== "project") {
|
|
138
127
|
sources.push({ dir: join(options.agentDir ?? getAgentDir(), "agents"), source: "user" });
|
|
139
128
|
}
|
|
@@ -148,11 +137,7 @@ export function discoverAgents(options: AgentDiscoveryOptions): AgentDiscoveryRe
|
|
|
148
137
|
|
|
149
138
|
const byName = new Map<string, AgentDefinition>();
|
|
150
139
|
for (const item of sources) {
|
|
151
|
-
const loaded = loadDirectory(
|
|
152
|
-
item.dir,
|
|
153
|
-
item.source,
|
|
154
|
-
item.source === "user" ? options.excludeUserAgentNames : undefined,
|
|
155
|
-
);
|
|
140
|
+
const loaded = loadDirectory(item.dir, item.source);
|
|
156
141
|
diagnostics.push(...loaded.diagnostics);
|
|
157
142
|
for (const agent of loaded.agents) byName.set(agent.name, agent);
|
|
158
143
|
}
|
|
@@ -168,7 +153,3 @@ export function formatAgentCatalog(agents: AgentDefinition[]): string {
|
|
|
168
153
|
if (agents.length === 0) return "(no agents)";
|
|
169
154
|
return agents.map((agent) => `${agent.name} (${agent.source}) — ${agent.description}`).join("\n");
|
|
170
155
|
}
|
|
171
|
-
|
|
172
|
-
export function hasBundledAgents(dir: string): boolean {
|
|
173
|
-
return existsSync(dir) && isDirectory(dir);
|
|
174
|
-
}
|
package/src/catalog.ts
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { foldDescriptor } from "./descriptor.ts";
|
|
3
|
+
import {
|
|
4
|
+
foldCompletionMailbox,
|
|
5
|
+
unreadCompletionCounts,
|
|
6
|
+
} from "./completion-mailbox.ts";
|
|
7
|
+
import { foldOwnedMailbox } from "./mailbox.ts";
|
|
3
8
|
import type { SessionView } from "./providers.ts";
|
|
4
9
|
import type { CatalogDiagnostic, SubagentDescriptor } from "./types.ts";
|
|
5
10
|
|
|
6
11
|
export interface PersistedDescriptor {
|
|
7
12
|
agentId: string;
|
|
13
|
+
piSessionId: string;
|
|
8
14
|
sessionFile: string;
|
|
9
15
|
descriptor: SubagentDescriptor;
|
|
16
|
+
pendingMessages: number;
|
|
17
|
+
unreadUpdatesByChild: Map<string, number>;
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
export interface PersistedCatalog {
|
|
@@ -52,10 +60,49 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
52
60
|
});
|
|
53
61
|
return;
|
|
54
62
|
}
|
|
63
|
+
let pendingMessages = 0;
|
|
64
|
+
const mailbox = foldOwnedMailbox(manager.getEntries(), {
|
|
65
|
+
parentAgentId: folded.descriptor.parentAgentId,
|
|
66
|
+
agentId: folded.descriptor.agentId,
|
|
67
|
+
});
|
|
68
|
+
if (mailbox.kind === "corrupt") {
|
|
69
|
+
diagnostics.push({
|
|
70
|
+
kind: "diagnostic",
|
|
71
|
+
piSessionId: manager.getSessionId(),
|
|
72
|
+
reason: "corrupt",
|
|
73
|
+
sessionFile: info.path,
|
|
74
|
+
...(headerParent ? { parentSessionFile: headerParent } : {}),
|
|
75
|
+
message: `corrupt subagent mailbox: ${mailbox.message}`,
|
|
76
|
+
});
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
pendingMessages = mailbox.snapshot.pending.length;
|
|
80
|
+
let unreadUpdatesByChild = new Map<string, number>();
|
|
81
|
+
const completions = foldCompletionMailbox(
|
|
82
|
+
manager.getEntries(),
|
|
83
|
+
{ parentAgentId: folded.descriptor.agentId },
|
|
84
|
+
);
|
|
85
|
+
if (completions.kind === "corrupt") {
|
|
86
|
+
diagnostics.push({
|
|
87
|
+
kind: "diagnostic",
|
|
88
|
+
piSessionId: manager.getSessionId(),
|
|
89
|
+
reason: "corrupt",
|
|
90
|
+
sessionFile: info.path,
|
|
91
|
+
...(headerParent ? { parentSessionFile: headerParent } : {}),
|
|
92
|
+
message: `corrupt completion mailbox: ${completions.message}`,
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
unreadUpdatesByChild = unreadCompletionCounts(
|
|
96
|
+
completions.snapshot,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
55
99
|
descriptors.push({
|
|
56
100
|
agentId: folded.descriptor.agentId,
|
|
101
|
+
piSessionId: manager.getSessionId(),
|
|
57
102
|
sessionFile: info.path,
|
|
58
103
|
descriptor: folded.descriptor,
|
|
104
|
+
pendingMessages,
|
|
105
|
+
unreadUpdatesByChild,
|
|
59
106
|
});
|
|
60
107
|
} else if (folded.kind === "corrupt") {
|
|
61
108
|
const headerParent = manager.getHeader()?.parentSession;
|