@oai404iao/pi-subagent 0.4.0-alpha.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 +102 -72
- package/config.example.json +1 -4
- package/config.schema.json +14 -28
- package/index.ts +2 -0
- package/package.json +9 -8
- package/src/agent-sync.ts +112 -40
- package/src/catalog.ts +14 -16
- package/src/config.ts +15 -52
- package/src/coordinator.ts +114 -422
- package/src/descriptor.ts +51 -81
- package/src/index.ts +57 -88
- package/src/render.ts +5 -15
- package/src/schemas.ts +8 -70
- package/src/task-path.ts +5 -47
- package/src/types.ts +14 -25
package/src/agent-sync.ts
CHANGED
|
@@ -30,6 +30,11 @@ 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
|
|
|
35
40
|
type ManifestReadResult =
|
|
@@ -74,7 +79,14 @@ export interface AgentSyncResult {
|
|
|
74
79
|
manifestPath: string;
|
|
75
80
|
installed: string[];
|
|
76
81
|
updated: string[];
|
|
82
|
+
/** Bundled presets retired upstream and removed from the user directory. */
|
|
77
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;
|
|
78
90
|
preserved: string[];
|
|
79
91
|
backups: AgentBackup[];
|
|
80
92
|
diagnostics: string[];
|
|
@@ -135,10 +147,27 @@ function parseManifest(value: unknown, manifestPath: string): AgentManifest {
|
|
|
135
147
|
}
|
|
136
148
|
files[name] = hash;
|
|
137
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
|
+
}
|
|
138
166
|
return {
|
|
139
167
|
version: MANIFEST_VERSION,
|
|
140
168
|
packageVersion: input.packageVersion.trim(),
|
|
141
169
|
files,
|
|
170
|
+
retired,
|
|
142
171
|
};
|
|
143
172
|
}
|
|
144
173
|
|
|
@@ -441,6 +470,9 @@ function syncBundledAgentsLocked(
|
|
|
441
470
|
installed: [],
|
|
442
471
|
updated: [],
|
|
443
472
|
removed: [],
|
|
473
|
+
retired: [],
|
|
474
|
+
restored: [],
|
|
475
|
+
retirementChanged: false,
|
|
444
476
|
preserved: [],
|
|
445
477
|
backups: [],
|
|
446
478
|
diagnostics: [manifestRead.diagnostic],
|
|
@@ -448,10 +480,38 @@ function syncBundledAgentsLocked(
|
|
|
448
480
|
}
|
|
449
481
|
const previous = manifestRead.kind === "valid" ? manifestRead.manifest : undefined;
|
|
450
482
|
|
|
483
|
+
const files = bundledAgentFiles(options.bundledDir);
|
|
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
|
+
|
|
451
511
|
// The package copies are initialization templates, not a runtime fallback.
|
|
452
512
|
// Once this package version has been initialized, the user directory is
|
|
453
513
|
// authoritative: edits and deletions must survive every same-version start.
|
|
454
|
-
if (previous?.packageVersion === packageVersion) {
|
|
514
|
+
if (previous?.packageVersion === packageVersion && !retirementChanged) {
|
|
455
515
|
return {
|
|
456
516
|
packageVersion,
|
|
457
517
|
userAgentsDir,
|
|
@@ -459,68 +519,77 @@ function syncBundledAgentsLocked(
|
|
|
459
519
|
installed: [],
|
|
460
520
|
updated: [],
|
|
461
521
|
removed: [],
|
|
522
|
+
retired: recordedRetired,
|
|
523
|
+
restored: [],
|
|
524
|
+
retirementChanged: false,
|
|
462
525
|
preserved: [],
|
|
463
526
|
backups: [],
|
|
464
527
|
diagnostics,
|
|
465
528
|
};
|
|
466
529
|
}
|
|
467
530
|
|
|
468
|
-
const files = bundledAgentFiles(options.bundledDir);
|
|
469
|
-
const currentNames = new Set(files.map((file) => file.name));
|
|
470
531
|
const preserved: string[] = [];
|
|
471
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
|
+
}
|
|
472
554
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
555
|
+
if (
|
|
556
|
+
destinationState.kind === "file"
|
|
557
|
+
&& sameRegularFile(destination, destinationState.size, file.content)
|
|
558
|
+
) {
|
|
559
|
+
preserved.push(file.name);
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
479
562
|
actions.push({
|
|
480
563
|
name: file.name,
|
|
481
|
-
kind: "
|
|
564
|
+
kind: "replace",
|
|
482
565
|
destination,
|
|
483
|
-
destinationKind:
|
|
566
|
+
destinationKind: destinationState.kind,
|
|
484
567
|
content: file.content,
|
|
485
568
|
});
|
|
486
|
-
continue;
|
|
487
569
|
}
|
|
488
570
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
) {
|
|
493
|
-
|
|
494
|
-
|
|
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
|
+
});
|
|
495
585
|
}
|
|
496
|
-
actions.push({
|
|
497
|
-
name: file.name,
|
|
498
|
-
kind: "replace",
|
|
499
|
-
destination,
|
|
500
|
-
destinationKind: destinationState.kind,
|
|
501
|
-
content: file.content,
|
|
502
|
-
});
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
// Retired bundled presets must not remain silently active. They are backed
|
|
506
|
-
// up like replacements, then removed; unrelated user-defined names remain.
|
|
507
|
-
for (const name of Object.keys(previous?.files ?? {}).sort((left, right) => left.localeCompare(right))) {
|
|
508
|
-
if (currentNames.has(name)) continue;
|
|
509
|
-
const destination = join(userAgentsDir, name);
|
|
510
|
-
const destinationState = destinationKind(destination);
|
|
511
|
-
if (destinationState.kind === "missing") continue;
|
|
512
|
-
actions.push({
|
|
513
|
-
name,
|
|
514
|
-
kind: "remove",
|
|
515
|
-
destination,
|
|
516
|
-
destinationKind: destinationState.kind,
|
|
517
|
-
});
|
|
518
586
|
}
|
|
519
587
|
|
|
520
588
|
const manifest: AgentManifest = {
|
|
521
589
|
version: MANIFEST_VERSION,
|
|
522
590
|
packageVersion,
|
|
523
591
|
files: Object.fromEntries(files.map((file) => [file.name, file.hash])),
|
|
592
|
+
retired: recordedRetired,
|
|
524
593
|
};
|
|
525
594
|
const stagedPaths: string[] = [];
|
|
526
595
|
const backups: AgentBackup[] = [];
|
|
@@ -587,6 +656,9 @@ function syncBundledAgentsLocked(
|
|
|
587
656
|
installed: actions.filter((action) => action.kind === "install").map((action) => action.name),
|
|
588
657
|
updated: actions.filter((action) => action.kind === "replace").map((action) => action.name),
|
|
589
658
|
removed: actions.filter((action) => action.kind === "remove").map((action) => action.name),
|
|
659
|
+
retired: recordedRetired,
|
|
660
|
+
restored: [...restored].sort(),
|
|
661
|
+
retirementChanged,
|
|
590
662
|
preserved,
|
|
591
663
|
backups,
|
|
592
664
|
diagnostics,
|
package/src/catalog.ts
CHANGED
|
@@ -61,24 +61,22 @@ export async function readPersistedCatalog(session: SessionView): Promise<Persis
|
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
let pendingMessages = 0;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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}`,
|
|
68
76
|
});
|
|
69
|
-
|
|
70
|
-
diagnostics.push({
|
|
71
|
-
kind: "diagnostic",
|
|
72
|
-
piSessionId: manager.getSessionId(),
|
|
73
|
-
reason: "corrupt",
|
|
74
|
-
sessionFile: info.path,
|
|
75
|
-
...(headerParent ? { parentSessionFile: headerParent } : {}),
|
|
76
|
-
message: `corrupt subagent mailbox: ${mailbox.message}`,
|
|
77
|
-
});
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
pendingMessages = mailbox.snapshot.pending.length;
|
|
77
|
+
return;
|
|
81
78
|
}
|
|
79
|
+
pendingMessages = mailbox.snapshot.pending.length;
|
|
82
80
|
let unreadUpdatesByChild = new Map<string, number>();
|
|
83
81
|
const completions = foldCompletionMailbox(
|
|
84
82
|
manager.getEntries(),
|
package/src/config.ts
CHANGED
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
3
|
import { CONFIG_DIR_NAME, getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import type {
|
|
5
|
-
AgentScope,
|
|
6
|
-
BackgroundProtocol,
|
|
7
|
-
ReportDelivery,
|
|
8
|
-
SubagentSettings,
|
|
9
|
-
} from "./types.ts";
|
|
4
|
+
import type { AgentScope, RuntimeMode, SubagentSettings } from "./types.ts";
|
|
10
5
|
|
|
11
6
|
export const CONFIG_FILE_NAME = "subagent.json";
|
|
12
7
|
|
|
13
8
|
export const DEFAULT_SETTINGS: Readonly<SubagentSettings> = {
|
|
14
9
|
agentScope: "user",
|
|
15
10
|
maxDepth: 3,
|
|
16
|
-
|
|
17
|
-
defaultBackground: true,
|
|
11
|
+
runtimeMode: "background",
|
|
18
12
|
maxConcurrentBackgroundRuns: 4,
|
|
19
13
|
maxIdleRuntimes: 0,
|
|
20
|
-
backgroundProtocol: "legacy",
|
|
21
|
-
reportDelivery: "wakeup",
|
|
22
14
|
inheritExtensions: false,
|
|
23
15
|
openAIIdentity: false,
|
|
24
16
|
maxOutputBytes: 50 * 1024,
|
|
@@ -27,14 +19,10 @@ export const DEFAULT_SETTINGS: Readonly<SubagentSettings> = {
|
|
|
27
19
|
const CONFIG_KEYS = new Set([
|
|
28
20
|
"$schema",
|
|
29
21
|
"agentScope",
|
|
30
|
-
"syncBundledAgents",
|
|
31
22
|
"maxDepth",
|
|
32
|
-
"
|
|
33
|
-
"defaultBackground",
|
|
23
|
+
"runtimeMode",
|
|
34
24
|
"maxConcurrentBackgroundRuns",
|
|
35
25
|
"maxIdleRuntimes",
|
|
36
|
-
"backgroundProtocol",
|
|
37
|
-
"reportDelivery",
|
|
38
26
|
"inheritExtensions",
|
|
39
27
|
"openAIIdentity",
|
|
40
28
|
"maxOutputBytes",
|
|
@@ -91,14 +79,9 @@ function parseAgentScope(value: unknown, source: string): AgentScope {
|
|
|
91
79
|
throw new Error(`${source}: agentScope must be "user", "project", or "both"`);
|
|
92
80
|
}
|
|
93
81
|
|
|
94
|
-
function
|
|
95
|
-
if (value === "
|
|
96
|
-
throw new Error(`${source}:
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function parseBackgroundProtocol(value: unknown, source: string): BackgroundProtocol {
|
|
100
|
-
if (value === "legacy" || value === "mailbox-v2") return value;
|
|
101
|
-
throw new Error(`${source}: backgroundProtocol must be "legacy" or "mailbox-v2"`);
|
|
82
|
+
function parseRuntimeMode(value: unknown, source: string): RuntimeMode {
|
|
83
|
+
if (value === "foreground" || value === "background") return value;
|
|
84
|
+
throw new Error(`${source}: runtimeMode must be "foreground" or "background"`);
|
|
102
85
|
}
|
|
103
86
|
|
|
104
87
|
function parseBoolean(value: unknown, key: string, source: string): boolean {
|
|
@@ -129,20 +112,12 @@ function applyConfig(
|
|
|
129
112
|
settings: SubagentSettings,
|
|
130
113
|
config: ConfigRecord,
|
|
131
114
|
source: string,
|
|
132
|
-
options: { allowSyncBundledAgents: boolean },
|
|
133
115
|
): SubagentSettings {
|
|
134
|
-
if (config.syncBundledAgents !== undefined && !options.allowSyncBundledAgents) {
|
|
135
|
-
throw new Error(`${source}: syncBundledAgents may be configured only in the user-level subagent.json`);
|
|
136
|
-
}
|
|
137
|
-
if (config.syncBundledAgents !== undefined) {
|
|
138
|
-
// Compatibility with 0.2/0.3 configuration files. Bundled templates are
|
|
139
|
-
// now always initialized on first install/version change, and this
|
|
140
|
-
// retired switch no longer controls runtime discovery or writes.
|
|
141
|
-
parseBoolean(config.syncBundledAgents, "syncBundledAgents", source);
|
|
142
|
-
}
|
|
143
116
|
return {
|
|
144
117
|
agentScope:
|
|
145
|
-
config.agentScope === undefined
|
|
118
|
+
config.agentScope === undefined
|
|
119
|
+
? settings.agentScope
|
|
120
|
+
: parseAgentScope(config.agentScope, source),
|
|
146
121
|
maxDepth:
|
|
147
122
|
config.maxDepth === undefined
|
|
148
123
|
? settings.maxDepth
|
|
@@ -150,14 +125,10 @@ function applyConfig(
|
|
|
150
125
|
minimum: 0,
|
|
151
126
|
maximum: Number.MAX_SAFE_INTEGER,
|
|
152
127
|
}),
|
|
153
|
-
|
|
154
|
-
config.
|
|
155
|
-
? settings.
|
|
156
|
-
:
|
|
157
|
-
defaultBackground:
|
|
158
|
-
config.defaultBackground === undefined
|
|
159
|
-
? settings.defaultBackground
|
|
160
|
-
: parseBoolean(config.defaultBackground, "defaultBackground", source),
|
|
128
|
+
runtimeMode:
|
|
129
|
+
config.runtimeMode === undefined
|
|
130
|
+
? settings.runtimeMode
|
|
131
|
+
: parseRuntimeMode(config.runtimeMode, source),
|
|
161
132
|
maxConcurrentBackgroundRuns:
|
|
162
133
|
config.maxConcurrentBackgroundRuns === undefined
|
|
163
134
|
? settings.maxConcurrentBackgroundRuns
|
|
@@ -182,14 +153,6 @@ function applyConfig(
|
|
|
182
153
|
maximum: Number.MAX_SAFE_INTEGER,
|
|
183
154
|
},
|
|
184
155
|
),
|
|
185
|
-
backgroundProtocol:
|
|
186
|
-
config.backgroundProtocol === undefined
|
|
187
|
-
? settings.backgroundProtocol
|
|
188
|
-
: parseBackgroundProtocol(config.backgroundProtocol, source),
|
|
189
|
-
reportDelivery:
|
|
190
|
-
config.reportDelivery === undefined
|
|
191
|
-
? settings.reportDelivery
|
|
192
|
-
: parseReportDelivery(config.reportDelivery, source),
|
|
193
156
|
inheritExtensions:
|
|
194
157
|
config.inheritExtensions === undefined
|
|
195
158
|
? settings.inheritExtensions
|
|
@@ -214,7 +177,7 @@ export function loadSettings(options: LoadSettingsOptions): LoadedSettings {
|
|
|
214
177
|
const userPath = join(options.agentDir ?? getAgentDir(), CONFIG_FILE_NAME);
|
|
215
178
|
const userConfig = readConfig(userPath);
|
|
216
179
|
if (userConfig) {
|
|
217
|
-
settings = applyConfig(settings, userConfig, userPath
|
|
180
|
+
settings = applyConfig(settings, userConfig, userPath);
|
|
218
181
|
sources.push(userPath);
|
|
219
182
|
}
|
|
220
183
|
|
|
@@ -223,7 +186,7 @@ export function loadSettings(options: LoadSettingsOptions): LoadedSettings {
|
|
|
223
186
|
if (projectPath) {
|
|
224
187
|
const projectConfig = readConfig(projectPath);
|
|
225
188
|
if (projectConfig) {
|
|
226
|
-
settings = applyConfig(settings, projectConfig, projectPath
|
|
189
|
+
settings = applyConfig(settings, projectConfig, projectPath);
|
|
227
190
|
sources.push(projectPath);
|
|
228
191
|
}
|
|
229
192
|
}
|