@mystilleef/pi-subagent 0.11.0 → 0.12.1
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 +84 -11
- package/package.json +9 -9
- package/src/agent/agents.ts +45 -3
- package/src/child/model-resolution.ts +125 -11
- package/src/child/process-utils.ts +19 -13
- package/src/child/process.ts +126 -37
- package/src/child/prompt-contract.ts +13 -9
- package/src/child/prompt-setup.ts +19 -1
- package/src/orchestration/subagent-orchestrator.ts +6 -1
- package/src/output/ui.ts +40 -19
- package/src/shared/invocation.ts +33 -0
- package/src/shared/limits.ts +81 -0
- package/src/shared/message-utils.ts +56 -0
- package/src/shared/resource-resolution.ts +289 -0
- package/src/shared/utils.ts +35 -261
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic resource resolution for skills and extensions.
|
|
3
|
+
* Handles caching, discovery via DefaultResourceLoader, and name-to-path mapping.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import {
|
|
9
|
+
DefaultResourceLoader,
|
|
10
|
+
getAgentDir,
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
|
|
13
|
+
const RESOURCE_DISCOVERY_CACHE_TTL_MS = 300_000;
|
|
14
|
+
|
|
15
|
+
export const EXTENSION_DISCOVERY_CACHE_TTL_MS = RESOURCE_DISCOVERY_CACHE_TTL_MS;
|
|
16
|
+
|
|
17
|
+
type ResourceCacheEntry<T> = {
|
|
18
|
+
data: T;
|
|
19
|
+
ts: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
class ResourceCache<T> {
|
|
23
|
+
private store = new Map<string, ResourceCacheEntry<T>>();
|
|
24
|
+
|
|
25
|
+
get(key: string): T | undefined {
|
|
26
|
+
const entry = this.store.get(key);
|
|
27
|
+
if (entry && Date.now() - entry.ts <= RESOURCE_DISCOVERY_CACHE_TTL_MS) {
|
|
28
|
+
return entry.data;
|
|
29
|
+
}
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
set(key: string, data: T): void {
|
|
34
|
+
this.store.set(key, { data, ts: Date.now() });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
clear(): void {
|
|
38
|
+
this.store.clear();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const skillArgsCache = new ResourceCache<Map<string, string>>();
|
|
43
|
+
const extensionPathsCache = new ResourceCache<Map<string, string>>();
|
|
44
|
+
|
|
45
|
+
export function resetResolvedAgentSkillArgsCache(): void {
|
|
46
|
+
skillArgsCache.clear();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function resetResolvedAgentExtensionPathsCache(): void {
|
|
50
|
+
extensionPathsCache.clear();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function canonicalPath(filePath: string): Promise<string> {
|
|
54
|
+
try {
|
|
55
|
+
return await fs.promises.realpath(filePath);
|
|
56
|
+
} catch {
|
|
57
|
+
/* symlinks or missing paths fall back to absolute path resolution */
|
|
58
|
+
return path.resolve(filePath);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function buildResourceCacheKey(
|
|
63
|
+
cwd: string,
|
|
64
|
+
agentDir: string,
|
|
65
|
+
names: string[],
|
|
66
|
+
): Promise<string> {
|
|
67
|
+
const sortedNames = [...names].sort();
|
|
68
|
+
return JSON.stringify({
|
|
69
|
+
cwd: await canonicalPath(cwd),
|
|
70
|
+
agentDir: await canonicalPath(agentDir),
|
|
71
|
+
names: sortedNames,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
type ResourceResolverResult<T> = { data: T } | { error: string };
|
|
76
|
+
|
|
77
|
+
interface ResourceResolverConfig<TResult> {
|
|
78
|
+
cache: ResourceCache<Map<string, string>>;
|
|
79
|
+
loaderOptions: Record<string, boolean>;
|
|
80
|
+
getResources: (loader: DefaultResourceLoader) => {
|
|
81
|
+
items: unknown[];
|
|
82
|
+
errors: Array<{ path: string; error: string }>;
|
|
83
|
+
};
|
|
84
|
+
buildNameToResource: (items: unknown[]) => Map<string, string>;
|
|
85
|
+
buildResult: (
|
|
86
|
+
requested: string[],
|
|
87
|
+
nameToResource: Map<string, string>,
|
|
88
|
+
) => TResult;
|
|
89
|
+
resourceType: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function resolveResources<TResult>(
|
|
93
|
+
cwd: string,
|
|
94
|
+
names: string[],
|
|
95
|
+
config: ResourceResolverConfig<TResult>,
|
|
96
|
+
): Promise<ResourceResolverResult<TResult>> {
|
|
97
|
+
const requested = Array.from(new Set(names));
|
|
98
|
+
if (requested.length === 0) {
|
|
99
|
+
return { data: config.buildResult(requested, new Map()) };
|
|
100
|
+
}
|
|
101
|
+
const agentDir = getAgentDir();
|
|
102
|
+
const cacheKey = await buildResourceCacheKey(cwd, agentDir, requested);
|
|
103
|
+
const cached = config.cache.get(cacheKey);
|
|
104
|
+
if (cached) {
|
|
105
|
+
return { data: config.buildResult(requested, cached) };
|
|
106
|
+
}
|
|
107
|
+
const loader = new DefaultResourceLoader({
|
|
108
|
+
cwd,
|
|
109
|
+
agentDir,
|
|
110
|
+
...config.loaderOptions,
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
await loader.reload();
|
|
114
|
+
} catch (error) {
|
|
115
|
+
return {
|
|
116
|
+
error: `Failed to discover ${config.resourceType}s: ${error instanceof Error ? error.message : String(error)}`,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const { items, errors } = config.getResources(loader);
|
|
120
|
+
if (errors.length > 0) {
|
|
121
|
+
const details = errors.map((e) => ` ${e.path}: ${e.error}`).join("\n");
|
|
122
|
+
return {
|
|
123
|
+
error: `Failed to discover ${config.resourceType}s:\n${details}`,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const nameToResource = config.buildNameToResource(items);
|
|
127
|
+
const missing = requested.filter((name) => !nameToResource.has(name));
|
|
128
|
+
if (missing.length > 0) {
|
|
129
|
+
const available =
|
|
130
|
+
Array.from(nameToResource.keys()).sort().join(", ") || "none";
|
|
131
|
+
return {
|
|
132
|
+
error: `Unknown ${config.resourceType}${missing.length === 1 ? "" : "s"}: ${missing
|
|
133
|
+
.map((name) => `"${name}"`)
|
|
134
|
+
.join(", ")}. Available ${config.resourceType}s: ${available}.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const result = config.buildResult(requested, nameToResource);
|
|
138
|
+
config.cache.set(cacheKey, nameToResource);
|
|
139
|
+
return { data: result };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function buildSkillArgs(
|
|
143
|
+
requested: string[],
|
|
144
|
+
skillPaths: Map<string, string>,
|
|
145
|
+
): string[] {
|
|
146
|
+
return requested.flatMap((name) => ["--skill", skillPaths.get(name) ?? name]);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export async function resolveAgentSkillArgs(
|
|
150
|
+
cwd: string,
|
|
151
|
+
skillNames: string[],
|
|
152
|
+
): Promise<{ args: string[] } | { error: string }> {
|
|
153
|
+
const result = await resolveResources(cwd, skillNames, {
|
|
154
|
+
cache: skillArgsCache,
|
|
155
|
+
loaderOptions: {
|
|
156
|
+
noContextFiles: true,
|
|
157
|
+
noPromptTemplates: true,
|
|
158
|
+
noThemes: true,
|
|
159
|
+
},
|
|
160
|
+
getResources: (loader) => {
|
|
161
|
+
const { skills } = loader.getSkills();
|
|
162
|
+
return {
|
|
163
|
+
items: skills,
|
|
164
|
+
errors: [],
|
|
165
|
+
};
|
|
166
|
+
},
|
|
167
|
+
buildNameToResource: (items) => {
|
|
168
|
+
const skillMap = new Map<string, string>();
|
|
169
|
+
for (const item of items) {
|
|
170
|
+
const skill = item as { name: string; filePath: string };
|
|
171
|
+
skillMap.set(skill.name, skill.filePath ?? skill.name);
|
|
172
|
+
}
|
|
173
|
+
return skillMap;
|
|
174
|
+
},
|
|
175
|
+
buildResult: (requested, nameToResource) => {
|
|
176
|
+
return buildSkillArgs(requested, nameToResource);
|
|
177
|
+
},
|
|
178
|
+
resourceType: "skill",
|
|
179
|
+
});
|
|
180
|
+
return "error" in result ? result : { args: result.data };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function getTerminalPackageName(source: string): string {
|
|
184
|
+
const spec = source.startsWith("npm:") ? source.slice(4) : source;
|
|
185
|
+
let name: string;
|
|
186
|
+
if (spec.startsWith("@")) {
|
|
187
|
+
const slashIdx = spec.indexOf("/");
|
|
188
|
+
if (slashIdx >= 0) {
|
|
189
|
+
name = spec.slice(slashIdx + 1);
|
|
190
|
+
} else {
|
|
191
|
+
name = spec;
|
|
192
|
+
}
|
|
193
|
+
} else {
|
|
194
|
+
name = spec;
|
|
195
|
+
}
|
|
196
|
+
const versionIdx = name.indexOf("@");
|
|
197
|
+
if (versionIdx >= 0) {
|
|
198
|
+
return name.slice(0, versionIdx);
|
|
199
|
+
}
|
|
200
|
+
return name;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isLocalPathSpec(source: string): boolean {
|
|
204
|
+
return (
|
|
205
|
+
source.startsWith(".") ||
|
|
206
|
+
source.startsWith("/") ||
|
|
207
|
+
source.startsWith("~") ||
|
|
208
|
+
source.startsWith("file:")
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function buildExtensionShortName(ext: {
|
|
213
|
+
resolvedPath: string;
|
|
214
|
+
sourceInfo: { source: string; origin: string };
|
|
215
|
+
}): string {
|
|
216
|
+
if (
|
|
217
|
+
ext.sourceInfo.origin === "package" &&
|
|
218
|
+
!isLocalPathSpec(ext.sourceInfo.source)
|
|
219
|
+
) {
|
|
220
|
+
return getTerminalPackageName(ext.sourceInfo.source);
|
|
221
|
+
}
|
|
222
|
+
const fileName = path.basename(ext.resolvedPath);
|
|
223
|
+
if (/^index\.(?:ts|js)$/.test(fileName)) {
|
|
224
|
+
const dirName = path.dirname(ext.resolvedPath);
|
|
225
|
+
const base = path.basename(dirName);
|
|
226
|
+
if (base === "src" || base === "dist") {
|
|
227
|
+
return path.basename(path.dirname(dirName));
|
|
228
|
+
}
|
|
229
|
+
return base;
|
|
230
|
+
}
|
|
231
|
+
const dotIdx = fileName.lastIndexOf(".");
|
|
232
|
+
if (dotIdx > 0) {
|
|
233
|
+
return fileName.slice(0, dotIdx);
|
|
234
|
+
}
|
|
235
|
+
return fileName;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export async function resolveAgentExtensionPaths(
|
|
239
|
+
cwd: string,
|
|
240
|
+
extensionNames: string[],
|
|
241
|
+
): Promise<{ resolvedPaths: string[] } | { error: string }> {
|
|
242
|
+
const result = await resolveResources(cwd, extensionNames, {
|
|
243
|
+
cache: extensionPathsCache,
|
|
244
|
+
loaderOptions: {
|
|
245
|
+
noContextFiles: true,
|
|
246
|
+
noPromptTemplates: true,
|
|
247
|
+
noThemes: true,
|
|
248
|
+
noSkills: true,
|
|
249
|
+
},
|
|
250
|
+
getResources: (loader) => {
|
|
251
|
+
const { extensions, errors } = loader.getExtensions();
|
|
252
|
+
const shortNameToExtension = new Map<
|
|
253
|
+
string,
|
|
254
|
+
(typeof extensions)[number]
|
|
255
|
+
>();
|
|
256
|
+
for (const ext of extensions) {
|
|
257
|
+
const shortName = buildExtensionShortName(ext);
|
|
258
|
+
if (!shortNameToExtension.has(shortName)) {
|
|
259
|
+
shortNameToExtension.set(shortName, ext);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
return {
|
|
263
|
+
items: Array.from(shortNameToExtension.values()),
|
|
264
|
+
errors,
|
|
265
|
+
};
|
|
266
|
+
},
|
|
267
|
+
buildNameToResource: (items) => {
|
|
268
|
+
const shortNameToPath = new Map<string, string>();
|
|
269
|
+
for (const item of items) {
|
|
270
|
+
const ext = item as {
|
|
271
|
+
resolvedPath: string;
|
|
272
|
+
sourceInfo: { source: string; origin: string };
|
|
273
|
+
};
|
|
274
|
+
const shortName = buildExtensionShortName(ext);
|
|
275
|
+
if (!shortNameToPath.has(shortName)) {
|
|
276
|
+
shortNameToPath.set(shortName, ext.resolvedPath ?? shortName);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
return shortNameToPath;
|
|
280
|
+
},
|
|
281
|
+
buildResult: (requested, nameToResource) => {
|
|
282
|
+
return requested
|
|
283
|
+
.map((name) => nameToResource.get(name))
|
|
284
|
+
.filter((p): p is string => typeof p === "string");
|
|
285
|
+
},
|
|
286
|
+
resourceType: "extension",
|
|
287
|
+
});
|
|
288
|
+
return "error" in result ? result : { resolvedPaths: result.data };
|
|
289
|
+
}
|
package/src/shared/utils.ts
CHANGED
|
@@ -1,261 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
export
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return undefined;
|
|
37
|
-
return parsed;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function getSubagentOutputLimits(
|
|
41
|
-
config: EnvLimitConfig = process.env,
|
|
42
|
-
): SubagentOutputLimits {
|
|
43
|
-
return {
|
|
44
|
-
maxBytes:
|
|
45
|
-
parsePositiveInteger(config["PI_SUBAGENT_MAX_OUTPUT_BYTES"]) ??
|
|
46
|
-
DEFAULT_MAX_OUTPUT_BYTES,
|
|
47
|
-
maxLines:
|
|
48
|
-
parsePositiveInteger(config["PI_SUBAGENT_MAX_OUTPUT_LINES"]) ??
|
|
49
|
-
DEFAULT_MAX_OUTPUT_LINES,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function getSubagentRuntimeLimits(
|
|
54
|
-
config: EnvLimitConfig = process.env,
|
|
55
|
-
): SubagentRuntimeLimits {
|
|
56
|
-
const maxDepth =
|
|
57
|
-
parsePositiveInteger(config["PI_SUBAGENT_MAX_DEPTH"]) ??
|
|
58
|
-
DEFAULT_MAX_SUBAGENT_DEPTH;
|
|
59
|
-
return {
|
|
60
|
-
agentEndGraceMs:
|
|
61
|
-
parsePositiveInteger(config["PI_SUBAGENT_AGENT_END_GRACE_MS"]) ??
|
|
62
|
-
DEFAULT_AGENT_END_GRACE_MS,
|
|
63
|
-
maxStderrBytes:
|
|
64
|
-
parsePositiveInteger(config["PI_SUBAGENT_MAX_STDERR_BYTES"]) ??
|
|
65
|
-
DEFAULT_MAX_STDERR_BYTES,
|
|
66
|
-
maxDepth: Math.min(maxDepth, MAX_SUBAGENT_DEPTH_CEILING),
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function truncateOutput(
|
|
71
|
-
text: string,
|
|
72
|
-
limits: SubagentOutputLimits = getSubagentOutputLimits(),
|
|
73
|
-
): string {
|
|
74
|
-
const lines = text.split("\n");
|
|
75
|
-
const maxBytes = Math.max(1, Math.floor(limits.maxBytes));
|
|
76
|
-
const maxLines = Math.max(1, Math.floor(limits.maxLines));
|
|
77
|
-
if (lines.length <= maxLines && Buffer.byteLength(text, "utf-8") <= maxBytes)
|
|
78
|
-
return text;
|
|
79
|
-
let result = lines.slice(0, maxLines).join("\n");
|
|
80
|
-
if (Buffer.byteLength(result, "utf-8") > maxBytes) {
|
|
81
|
-
const buf = Buffer.from(result).subarray(0, maxBytes);
|
|
82
|
-
result = buf.toString("utf-8").replace(/\uFFFD$/, "");
|
|
83
|
-
}
|
|
84
|
-
const kept = result.split("\n").length;
|
|
85
|
-
return `[TRUNCATED: first ${kept} of ${lines.length} lines]\n${result}`;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
export async function writePromptToTempFile(
|
|
89
|
-
agentName: string,
|
|
90
|
-
prompt: string,
|
|
91
|
-
): Promise<{ dir: string; filePath: string }> {
|
|
92
|
-
const tmpDir = await fs.promises.mkdtemp(
|
|
93
|
-
path.join(os.tmpdir(), "pi-subagent-"),
|
|
94
|
-
);
|
|
95
|
-
const safeName = agentName.replace(/[^\w.-]+/g, "_");
|
|
96
|
-
const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
|
|
97
|
-
// mkdtemp guarantees a unique directory per call; no concurrent writer can hold this path.
|
|
98
|
-
await fs.promises.writeFile(filePath, prompt, {
|
|
99
|
-
encoding: "utf-8",
|
|
100
|
-
mode: 0o600,
|
|
101
|
-
});
|
|
102
|
-
return { dir: tmpDir, filePath };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export function getPiInvocation(args: string[]): {
|
|
106
|
-
command: string;
|
|
107
|
-
args: string[];
|
|
108
|
-
} {
|
|
109
|
-
const currentScript = process.argv[1];
|
|
110
|
-
if (currentScript && fs.existsSync(currentScript)) {
|
|
111
|
-
return { command: process.execPath, args: [currentScript, ...args] };
|
|
112
|
-
}
|
|
113
|
-
const execName = path.basename(process.execPath).toLowerCase();
|
|
114
|
-
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
115
|
-
if (!isGenericRuntime) {
|
|
116
|
-
return { command: process.execPath, args };
|
|
117
|
-
}
|
|
118
|
-
return { command: "pi", args };
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const SKILL_DISCOVERY_CACHE_TTL_MS = 300_000;
|
|
122
|
-
|
|
123
|
-
type ResolvedSkillArgsCacheEntry = {
|
|
124
|
-
skillPaths: Map<string, string>;
|
|
125
|
-
ts: number;
|
|
126
|
-
};
|
|
127
|
-
|
|
128
|
-
const resolvedSkillArgsCache = new Map<string, ResolvedSkillArgsCacheEntry>();
|
|
129
|
-
|
|
130
|
-
async function canonicalPath(filePath: string): Promise<string> {
|
|
131
|
-
try {
|
|
132
|
-
return await fs.promises.realpath(filePath);
|
|
133
|
-
} catch {
|
|
134
|
-
/* symlinks or missing paths fall back to absolute path resolution */
|
|
135
|
-
return path.resolve(filePath);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function buildSkillArgs(
|
|
140
|
-
requested: string[],
|
|
141
|
-
skillPaths: Map<string, string>,
|
|
142
|
-
): string[] {
|
|
143
|
-
return requested.flatMap((name) => ["--skill", skillPaths.get(name) ?? name]);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
export function resetResolvedAgentSkillArgsCache(): void {
|
|
147
|
-
resolvedSkillArgsCache.clear();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
export async function resolveAgentSkillArgs(
|
|
151
|
-
cwd: string,
|
|
152
|
-
skillNames: string[],
|
|
153
|
-
): Promise<{ args: string[] } | { error: string }> {
|
|
154
|
-
const requested = Array.from(new Set(skillNames));
|
|
155
|
-
if (requested.length === 0) return { args: [] };
|
|
156
|
-
const cacheIdentitySkills = [...requested].sort();
|
|
157
|
-
const agentDir = getAgentDir();
|
|
158
|
-
const cacheKey = JSON.stringify({
|
|
159
|
-
cwd: await canonicalPath(cwd),
|
|
160
|
-
agentDir: await canonicalPath(agentDir),
|
|
161
|
-
skills: cacheIdentitySkills,
|
|
162
|
-
});
|
|
163
|
-
const cached = resolvedSkillArgsCache.get(cacheKey);
|
|
164
|
-
if (cached && Date.now() - cached.ts <= SKILL_DISCOVERY_CACHE_TTL_MS) {
|
|
165
|
-
return { args: buildSkillArgs(requested, cached.skillPaths) };
|
|
166
|
-
}
|
|
167
|
-
const loader = new DefaultResourceLoader({
|
|
168
|
-
cwd,
|
|
169
|
-
agentDir,
|
|
170
|
-
noContextFiles: true,
|
|
171
|
-
noPromptTemplates: true,
|
|
172
|
-
noThemes: true,
|
|
173
|
-
});
|
|
174
|
-
try {
|
|
175
|
-
await loader.reload();
|
|
176
|
-
} catch (error) {
|
|
177
|
-
return {
|
|
178
|
-
error: `Failed to discover skills: ${error instanceof Error ? error.message : String(error)}`,
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
const { skills } = loader.getSkills();
|
|
182
|
-
const skillMap = new Map(skills.map((skill) => [skill.name, skill]));
|
|
183
|
-
const missing = requested.filter((name) => !skillMap.has(name));
|
|
184
|
-
if (missing.length > 0) {
|
|
185
|
-
const available =
|
|
186
|
-
skills
|
|
187
|
-
.map((skill) => skill.name)
|
|
188
|
-
.sort()
|
|
189
|
-
.join(", ") || "none";
|
|
190
|
-
return {
|
|
191
|
-
error: `Unknown skill${missing.length === 1 ? "" : "s"}: ${missing
|
|
192
|
-
.map((name) => `"${name}"`)
|
|
193
|
-
.join(", ")}. Available skills: ${available}.`,
|
|
194
|
-
};
|
|
195
|
-
}
|
|
196
|
-
const skillPaths = new Map(
|
|
197
|
-
requested.map((name) => [name, skillMap.get(name)?.filePath ?? name]),
|
|
198
|
-
);
|
|
199
|
-
const args = buildSkillArgs(requested, skillPaths);
|
|
200
|
-
resolvedSkillArgsCache.set(cacheKey, { skillPaths, ts: Date.now() });
|
|
201
|
-
return { args };
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function getSubagentDepth(): number {
|
|
205
|
-
const d = Number(process.env.PI_SUBAGENT_DEPTH ?? "0");
|
|
206
|
-
if (!Number.isFinite(d) || d < 0) return 0;
|
|
207
|
-
return Math.floor(d);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function subagentDepthEnv(): Record<string, string> {
|
|
211
|
-
return { PI_SUBAGENT_DEPTH: String(getSubagentDepth() + 1) };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function findLastAssistantTextMessage(messages: Message[]): number {
|
|
215
|
-
for (let i = messages.length - 1; i >= 0; i--) {
|
|
216
|
-
const msg = messages[i];
|
|
217
|
-
if (
|
|
218
|
-
msg?.role === "assistant" &&
|
|
219
|
-
Array.isArray(msg.content) &&
|
|
220
|
-
msg.content.some(
|
|
221
|
-
(c) =>
|
|
222
|
-
c.type === "text" &&
|
|
223
|
-
typeof c.text === "string" &&
|
|
224
|
-
c.text.trim().length > 0,
|
|
225
|
-
)
|
|
226
|
-
) {
|
|
227
|
-
return i;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
return -1;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
export function extractFinalOutputFromMessages(messages: Message[]): string {
|
|
234
|
-
const lastAsstIdx = findLastAssistantTextMessage(messages);
|
|
235
|
-
if (lastAsstIdx < 0) return "";
|
|
236
|
-
const content = messages[lastAsstIdx]?.content;
|
|
237
|
-
if (!Array.isArray(content)) return "";
|
|
238
|
-
const lastText = content.findLast((p) => p.type === "text");
|
|
239
|
-
return lastText?.type === "text" ? (lastText.text ?? "") : "";
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function detectMessageError(messages: Message[]): boolean {
|
|
243
|
-
const lastAssistantIdx = findLastAssistantTextMessage(messages);
|
|
244
|
-
const from = lastAssistantIdx >= 0 ? lastAssistantIdx + 1 : 0;
|
|
245
|
-
for (let i = messages.length - 1; i >= from; i--) {
|
|
246
|
-
const msg = messages[i];
|
|
247
|
-
if (msg?.role === "toolResult" && msg.isError) return true;
|
|
248
|
-
}
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export function hasSubagentFailed(result: SingleResult): boolean {
|
|
253
|
-
if (result.outcome?.trim()) return false;
|
|
254
|
-
return (
|
|
255
|
-
result.exitCode !== 0 ||
|
|
256
|
-
result.stopReason === "error" ||
|
|
257
|
-
result.stopReason === "aborted" ||
|
|
258
|
-
Boolean(result.errorMessage?.trim()) ||
|
|
259
|
-
detectMessageError(result.messages ?? [])
|
|
260
|
-
);
|
|
261
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Barrel export for shared utilities.
|
|
3
|
+
* Re-exports from focused modules.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export { writePromptToTempFile } from "../child/prompt-setup.js";
|
|
7
|
+
export {
|
|
8
|
+
getPiInvocation,
|
|
9
|
+
getSubagentDepth,
|
|
10
|
+
subagentDepthEnv,
|
|
11
|
+
} from "./invocation.js";
|
|
12
|
+
// Re-export all public symbols from focused modules
|
|
13
|
+
export {
|
|
14
|
+
DEFAULT_AGENT_END_GRACE_MS,
|
|
15
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
16
|
+
DEFAULT_MAX_OUTPUT_LINES,
|
|
17
|
+
DEFAULT_MAX_STDERR_BYTES,
|
|
18
|
+
DEFAULT_MAX_SUBAGENT_DEPTH,
|
|
19
|
+
getSubagentOutputLimits,
|
|
20
|
+
getSubagentRuntimeLimits,
|
|
21
|
+
truncateOutput,
|
|
22
|
+
} from "./limits.js";
|
|
23
|
+
export {
|
|
24
|
+
detectMessageError,
|
|
25
|
+
extractFinalOutputFromMessages,
|
|
26
|
+
findLastAssistantTextMessage,
|
|
27
|
+
hasSubagentFailed,
|
|
28
|
+
} from "./message-utils.js";
|
|
29
|
+
export {
|
|
30
|
+
EXTENSION_DISCOVERY_CACHE_TTL_MS,
|
|
31
|
+
resetResolvedAgentExtensionPathsCache,
|
|
32
|
+
resetResolvedAgentSkillArgsCache,
|
|
33
|
+
resolveAgentExtensionPaths,
|
|
34
|
+
resolveAgentSkillArgs,
|
|
35
|
+
} from "./resource-resolution.js";
|