@mystilleef/pi-subagent 0.7.0 → 0.8.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 +1 -1
- package/package.json +8 -9
- package/src/agent/agent-cache.ts +48 -20
- package/src/agent/agents.ts +11 -6
- package/src/child/process.ts +1 -1
- package/src/shared/utils.ts +46 -7
package/README.md
CHANGED
|
@@ -151,7 +151,7 @@ executable automation.
|
|
|
151
151
|
**Environment variables:**
|
|
152
152
|
|
|
153
153
|
- `PI_SUBAGENT_DEPTH`: nested subagent depth guard. Nested calls stop at
|
|
154
|
-
depth `
|
|
154
|
+
depth `3`.
|
|
155
155
|
- `PI_SUBAGENT_MAX_OUTPUT_BYTES`: max returned output bytes. Default:
|
|
156
156
|
`50000`.
|
|
157
157
|
- `PI_SUBAGENT_MAX_OUTPUT_LINES`: max returned output lines. Default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mystilleef/pi-subagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Pi subagent for the SPAE Framework",
|
|
5
5
|
"author": "Lateef Alabi-Oki <mystilleef@gmail.com>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -48,12 +48,11 @@
|
|
|
48
48
|
},
|
|
49
49
|
"scripts": {
|
|
50
50
|
"typecheck": "tsc --noEmit",
|
|
51
|
-
"lint": "biome check --error-on-warnings .",
|
|
52
|
-
"fix": "biome check --write --unsafe .",
|
|
51
|
+
"lint": "biome check --write --unsafe --error-on-warnings .",
|
|
53
52
|
"migrate": "biome migrate --write",
|
|
54
53
|
"coverage": "bun test --coverage",
|
|
55
54
|
"check": "bun lint && bun typecheck",
|
|
56
|
-
"verify": "bun migrate && bun
|
|
55
|
+
"verify": "bun migrate && bun check && bun coverage",
|
|
57
56
|
"pack:smoke": "bun scripts/pack-smoke.ts",
|
|
58
57
|
"release": "sh -c 'npm version \"$1\" -m \"chore(release): %s\" && git push --follow-tags' --"
|
|
59
58
|
},
|
|
@@ -66,13 +65,13 @@
|
|
|
66
65
|
},
|
|
67
66
|
"devDependencies": {
|
|
68
67
|
"@biomejs/biome": "^2.4.16",
|
|
69
|
-
"@earendil-works/pi-agent-core": "^0.78.
|
|
70
|
-
"@earendil-works/pi-ai": "^0.78.
|
|
71
|
-
"@earendil-works/pi-coding-agent": "^0.78.
|
|
72
|
-
"@earendil-works/pi-tui": "^0.78.
|
|
68
|
+
"@earendil-works/pi-agent-core": "^0.78.1",
|
|
69
|
+
"@earendil-works/pi-ai": "^0.78.1",
|
|
70
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
71
|
+
"@earendil-works/pi-tui": "^0.78.1",
|
|
73
72
|
"@types/bun": "^1.3.14",
|
|
74
73
|
"@types/node": "^25.9.1",
|
|
75
|
-
"typebox": "^1.1
|
|
74
|
+
"typebox": "^1.2.1",
|
|
76
75
|
"typescript": "^6.0.3"
|
|
77
76
|
}
|
|
78
77
|
}
|
package/src/agent/agent-cache.ts
CHANGED
|
@@ -9,12 +9,14 @@ import {
|
|
|
9
9
|
discoverAgentsAsync,
|
|
10
10
|
emptyScopeResult,
|
|
11
11
|
getUserAgentsDir,
|
|
12
|
-
|
|
12
|
+
readMarkdownDirWithStatusAsync,
|
|
13
13
|
} from "./agents.js";
|
|
14
14
|
|
|
15
15
|
interface AgentDiscoveryScopeSnapshot {
|
|
16
16
|
markdownFiles: string[];
|
|
17
17
|
fileHashes: Record<string, string | null>;
|
|
18
|
+
listingTrusted: boolean;
|
|
19
|
+
directory: string | null;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
type AgentDiscoverySnapshots = Record<AgentSource, AgentDiscoveryScopeSnapshot>;
|
|
@@ -52,7 +54,12 @@ function getFreshCacheEntry(
|
|
|
52
54
|
}
|
|
53
55
|
|
|
54
56
|
function emptySnapshot(): AgentDiscoveryScopeSnapshot {
|
|
55
|
-
return {
|
|
57
|
+
return {
|
|
58
|
+
markdownFiles: [],
|
|
59
|
+
fileHashes: {},
|
|
60
|
+
listingTrusted: false,
|
|
61
|
+
directory: null,
|
|
62
|
+
};
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
function cloneScopeSnapshot(
|
|
@@ -62,6 +69,8 @@ function cloneScopeSnapshot(
|
|
|
62
69
|
return {
|
|
63
70
|
markdownFiles: [...snapshot.markdownFiles],
|
|
64
71
|
fileHashes: { ...snapshot.fileHashes },
|
|
72
|
+
listingTrusted: snapshot.listingTrusted,
|
|
73
|
+
directory: snapshot.directory,
|
|
65
74
|
};
|
|
66
75
|
}
|
|
67
76
|
|
|
@@ -71,14 +80,17 @@ function equalStringSets(a: string[], b: string[]): boolean {
|
|
|
71
80
|
return b.every((value) => set.has(value));
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
function
|
|
75
|
-
|
|
76
|
-
right: AgentDiscoveryScopeSnapshot,
|
|
83
|
+
function snapshotHasRequiredMetadata(
|
|
84
|
+
snapshot: AgentDiscoveryScopeSnapshot,
|
|
77
85
|
): boolean {
|
|
78
|
-
if (
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
86
|
+
if (typeof snapshot.listingTrusted !== "boolean") return false;
|
|
87
|
+
if (snapshot.directory !== null && typeof snapshot.directory !== "string")
|
|
88
|
+
return false;
|
|
89
|
+
if (
|
|
90
|
+
!equalStringSets(snapshot.markdownFiles, Object.keys(snapshot.fileHashes))
|
|
91
|
+
)
|
|
92
|
+
return false;
|
|
93
|
+
return true;
|
|
82
94
|
}
|
|
83
95
|
|
|
84
96
|
function scopeAgentsMatchListing(
|
|
@@ -118,16 +130,20 @@ async function hashMarkdownFileAsync(
|
|
|
118
130
|
async function buildScopeSnapshotAsync(
|
|
119
131
|
dir: string | null,
|
|
120
132
|
): Promise<AgentDiscoveryScopeSnapshot> {
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
const markdownFiles = entries.map((entry) => entry.name);
|
|
133
|
+
const listing = await readMarkdownDirWithStatusAsync(dir);
|
|
134
|
+
const markdownFiles = listing.entries.map((entry) => entry.name);
|
|
124
135
|
const hashPairs = await Promise.all(
|
|
125
136
|
markdownFiles.map(
|
|
126
137
|
async (fileName) =>
|
|
127
|
-
[fileName, await hashMarkdownFileAsync(dir, fileName)] as const,
|
|
138
|
+
[fileName, await hashMarkdownFileAsync(dir ?? "", fileName)] as const,
|
|
128
139
|
),
|
|
129
140
|
);
|
|
130
|
-
return {
|
|
141
|
+
return {
|
|
142
|
+
markdownFiles,
|
|
143
|
+
fileHashes: Object.fromEntries(hashPairs),
|
|
144
|
+
listingTrusted: listing.ok,
|
|
145
|
+
directory: dir ? path.resolve(dir) : null,
|
|
146
|
+
};
|
|
131
147
|
}
|
|
132
148
|
|
|
133
149
|
async function buildCacheSnapshotsAsync(
|
|
@@ -146,13 +162,22 @@ async function canTrustDerivedScopeAsync(
|
|
|
146
162
|
): Promise<boolean> {
|
|
147
163
|
const dir =
|
|
148
164
|
source === "user" ? getUserAgentsDir() : bothEntry.projectAgentsDir;
|
|
165
|
+
const resolvedDir = dir ? path.resolve(dir) : null;
|
|
149
166
|
const scopeResult = bothEntry.scopes[source];
|
|
150
167
|
const cachedSnapshot = bothEntry.snapshots?.[source];
|
|
151
168
|
if (!cachedSnapshot) return false;
|
|
169
|
+
if (!snapshotHasRequiredMetadata(cachedSnapshot)) return false;
|
|
170
|
+
if (!cachedSnapshot.listingTrusted) return false;
|
|
171
|
+
if (cachedSnapshot.directory !== resolvedDir) return false;
|
|
152
172
|
if (!scopeAgentsMatchListing(scopeResult, source, dir)) return false;
|
|
153
173
|
if (!equalStringSets(scopeResult.markdownFiles, cachedSnapshot.markdownFiles))
|
|
154
174
|
return false;
|
|
155
|
-
|
|
175
|
+
const listing = await readMarkdownDirWithStatusAsync(dir);
|
|
176
|
+
if (!listing.ok) return false;
|
|
177
|
+
return equalStringSets(
|
|
178
|
+
cachedSnapshot.markdownFiles,
|
|
179
|
+
listing.entries.map((entry) => entry.name),
|
|
180
|
+
);
|
|
156
181
|
}
|
|
157
182
|
|
|
158
183
|
function buildSourceRecord<T>(
|
|
@@ -220,11 +245,14 @@ async function primeScopedCacheEntriesAsync(
|
|
|
220
245
|
ctx: CacheOperationContext,
|
|
221
246
|
bothEntry: AgentDiscoveryCacheEntry,
|
|
222
247
|
): Promise<void> {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
248
|
+
const missingSources = AGENT_SOURCES.filter(
|
|
249
|
+
(source) => !getFreshCacheEntry(ctx, cacheKey(ctx.cwd, source)),
|
|
250
|
+
);
|
|
251
|
+
await Promise.all(
|
|
252
|
+
missingSources.map((source) =>
|
|
253
|
+
deriveOrDiscoverScopedEntryAsync(ctx, source, bothEntry),
|
|
254
|
+
),
|
|
255
|
+
);
|
|
228
256
|
}
|
|
229
257
|
|
|
230
258
|
export async function getCachedAgentDiscovery(
|
package/src/agent/agents.ts
CHANGED
|
@@ -143,7 +143,7 @@ async function loadAgentsFromDirAsync(
|
|
|
143
143
|
dir: string,
|
|
144
144
|
source: AgentSource,
|
|
145
145
|
): Promise<AgentDiscoveryScopeResult> {
|
|
146
|
-
const markdownEntries = await
|
|
146
|
+
const markdownEntries = (await readMarkdownDirWithStatusAsync(dir)).entries;
|
|
147
147
|
const markdownFiles = markdownEntries.map((entry) => entry.name);
|
|
148
148
|
const parsedAgents = await Promise.all(
|
|
149
149
|
markdownEntries.map((entry) =>
|
|
@@ -162,15 +162,20 @@ export function isMarkdownDirent(entry: Dirent): boolean {
|
|
|
162
162
|
);
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
export
|
|
165
|
+
export interface MarkdownDirListing {
|
|
166
|
+
entries: Dirent[];
|
|
167
|
+
ok: boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export async function readMarkdownDirWithStatusAsync(
|
|
166
171
|
dir: string | null,
|
|
167
|
-
): Promise<
|
|
168
|
-
if (!dir) return [];
|
|
172
|
+
): Promise<MarkdownDirListing> {
|
|
173
|
+
if (!dir) return { entries: [], ok: true };
|
|
169
174
|
try {
|
|
170
175
|
const entries = await fsPromises.readdir(dir, { withFileTypes: true });
|
|
171
|
-
return entries.filter(isMarkdownDirent);
|
|
176
|
+
return { entries: entries.filter(isMarkdownDirent), ok: true };
|
|
172
177
|
} catch {
|
|
173
|
-
return [];
|
|
178
|
+
return { entries: [], ok: false };
|
|
174
179
|
}
|
|
175
180
|
}
|
|
176
181
|
|
package/src/child/process.ts
CHANGED
|
@@ -73,7 +73,7 @@ export function resolveThinkingLevel(
|
|
|
73
73
|
return { level: clamped, warning: mkWarning(clamped) };
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
const MAX_SUBAGENT_DEPTH =
|
|
76
|
+
const MAX_SUBAGENT_DEPTH = 3;
|
|
77
77
|
export const TOOL_RESULT_FAILED_MESSAGE = "Subagent tool result failed.";
|
|
78
78
|
|
|
79
79
|
type RuntimeResult = SingleResult & { messages: Message[] };
|
package/src/shared/utils.ts
CHANGED
|
@@ -89,15 +89,54 @@ export function getPiInvocation(args: string[]): {
|
|
|
89
89
|
return { command: "pi", args };
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
const SKILL_DISCOVERY_CACHE_TTL_MS = 300_000;
|
|
93
|
+
|
|
94
|
+
type ResolvedSkillArgsCacheEntry = {
|
|
95
|
+
skillPaths: Map<string, string>;
|
|
96
|
+
ts: number;
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const resolvedSkillArgsCache = new Map<string, ResolvedSkillArgsCacheEntry>();
|
|
100
|
+
|
|
101
|
+
async function canonicalPath(filePath: string): Promise<string> {
|
|
102
|
+
try {
|
|
103
|
+
return await fs.promises.realpath(filePath);
|
|
104
|
+
} catch {
|
|
105
|
+
return path.resolve(filePath);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function buildSkillArgs(
|
|
110
|
+
requested: string[],
|
|
111
|
+
skillPaths: Map<string, string>,
|
|
112
|
+
): string[] {
|
|
113
|
+
return requested.flatMap((name) => ["--skill", skillPaths.get(name) ?? name]);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function resetResolvedAgentSkillArgsCache(): void {
|
|
117
|
+
resolvedSkillArgsCache.clear();
|
|
118
|
+
}
|
|
119
|
+
|
|
92
120
|
export async function resolveAgentSkillArgs(
|
|
93
121
|
cwd: string,
|
|
94
122
|
skillNames: string[],
|
|
95
123
|
): Promise<{ args: string[] } | { error: string }> {
|
|
96
124
|
const requested = Array.from(new Set(skillNames));
|
|
97
125
|
if (requested.length === 0) return { args: [] };
|
|
126
|
+
const cacheIdentitySkills = [...requested].sort();
|
|
127
|
+
const agentDir = getAgentDir();
|
|
128
|
+
const cacheKey = JSON.stringify({
|
|
129
|
+
cwd: await canonicalPath(cwd),
|
|
130
|
+
agentDir: await canonicalPath(agentDir),
|
|
131
|
+
skills: cacheIdentitySkills,
|
|
132
|
+
});
|
|
133
|
+
const cached = resolvedSkillArgsCache.get(cacheKey);
|
|
134
|
+
if (cached && Date.now() - cached.ts <= SKILL_DISCOVERY_CACHE_TTL_MS) {
|
|
135
|
+
return { args: buildSkillArgs(requested, cached.skillPaths) };
|
|
136
|
+
}
|
|
98
137
|
const loader = new DefaultResourceLoader({
|
|
99
138
|
cwd,
|
|
100
|
-
agentDir
|
|
139
|
+
agentDir,
|
|
101
140
|
noContextFiles: true,
|
|
102
141
|
noPromptTemplates: true,
|
|
103
142
|
noThemes: true,
|
|
@@ -124,12 +163,12 @@ export async function resolveAgentSkillArgs(
|
|
|
124
163
|
.join(", ")}. Available skills: ${available}.`,
|
|
125
164
|
};
|
|
126
165
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
};
|
|
166
|
+
const skillPaths = new Map(
|
|
167
|
+
requested.map((name) => [name, skillMap.get(name)?.filePath ?? name]),
|
|
168
|
+
);
|
|
169
|
+
const args = buildSkillArgs(requested, skillPaths);
|
|
170
|
+
resolvedSkillArgsCache.set(cacheKey, { skillPaths, ts: Date.now() });
|
|
171
|
+
return { args };
|
|
133
172
|
}
|
|
134
173
|
|
|
135
174
|
export function getSubagentDepth(): number {
|