@narumitw/pi-subagents 0.40.1 → 0.41.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 +21 -2
- package/package.json +1 -1
- package/src/agents.ts +308 -22
- package/src/stateful.ts +14 -2
- package/src/subagents.ts +36 -16
package/README.md
CHANGED
|
@@ -58,6 +58,12 @@ The available tools are:
|
|
|
58
58
|
- `subagent` — delegate blocking single, parallel, fan-in, or chained batch work. The main agent cannot process queued steering until the call returns.
|
|
59
59
|
- `subagent_spawn` and related lifecycle tools — when enabled, start reusable detached work, return immediately, and receive bounded completion messages automatically.
|
|
60
60
|
|
|
61
|
+
After each session starts, both delegation tools include a bounded parent-facing catalog of the agents
|
|
62
|
+
available in that session. Entries show the source (`built-in`, `user`, or `project`) and the
|
|
63
|
+
`agentScope` needed to invoke them; the `agent` parameters remain unconstrained strings for cwd and
|
|
64
|
+
scope flexibility. The catalog is rebuilt on `/reload` or the next session start, and omitted entries
|
|
65
|
+
are reported explicitly when the catalog exceeds its metadata bounds.
|
|
66
|
+
|
|
61
67
|
Choose the API by lifecycle:
|
|
62
68
|
|
|
63
69
|
| Need | Use |
|
|
@@ -89,6 +95,17 @@ When registered, the blocking `subagent` tool advertises only blocking guidance.
|
|
|
89
95
|
are registered, `subagent_spawn` adds detached guidance for the active completion-delivery policy.
|
|
90
96
|
Changing the policy through `/subagents settings` refreshes that guidance immediately.
|
|
91
97
|
|
|
98
|
+
The same descriptions also advertise the current agent catalog automatically; no preliminary list call is
|
|
99
|
+
needed. Built-ins and user agents appear under the default `agentScope: "user"`. Trusted project
|
|
100
|
+
agents appear separately and explicitly require `agentScope: "project"` or `"both"`; project-authored
|
|
101
|
+
names and descriptions are not read into metadata for untrusted projects. If a project definition
|
|
102
|
+
shares a name with a user or built-in definition, the user version is the default and the project
|
|
103
|
+
version is used only for `"project"`/`"both"`. A user override of a built-in also shows the
|
|
104
|
+
built-in fallback available with `agentScope: "project"`; `"both"` keeps the user definition. The
|
|
105
|
+
catalog is bounded and reports its omission count; metadata discovery also caps files and bytes read
|
|
106
|
+
per scope. Refreshed metadata replaces the previous session's catalog rather than accumulating stale
|
|
107
|
+
entries.
|
|
108
|
+
|
|
92
109
|
Count-selection guidance:
|
|
93
110
|
|
|
94
111
|
- Use **no subagent** for simple answers, quick targeted edits, latency-sensitive one-step work, or
|
|
@@ -429,8 +446,10 @@ test coverage, and migration risks. Report PASS/FAIL/PARTIAL with evidence.
|
|
|
429
446
|
```
|
|
430
447
|
|
|
431
448
|
`agentScope` is a top-level tool argument supplied per invocation. It is not a setting in
|
|
432
|
-
`~/.pi/agent/pi-subagents.json` and does not belong in agent frontmatter. The
|
|
433
|
-
|
|
449
|
+
`~/.pi/agent/pi-subagents.json` and does not belong in agent frontmatter. The parent-facing tool
|
|
450
|
+
metadata discovers these definitions after session start and labels their source and required scope.
|
|
451
|
+
Edit agent files and run `/reload` (or start a new session) to refresh the catalog; there is no live
|
|
452
|
+
filesystem watcher. The scope selects which custom agent directories are loaded; built-in agents remain available in every scope:
|
|
434
453
|
|
|
435
454
|
| `agentScope` | Custom agents loaded |
|
|
436
455
|
| --- | --- |
|
package/package.json
CHANGED
package/src/agents.ts
CHANGED
|
@@ -139,40 +139,109 @@ function workerSystemPrompt(): string {
|
|
|
139
139
|
export interface AgentDiscoveryResult {
|
|
140
140
|
agents: AgentConfig[];
|
|
141
141
|
projectAgentsDir: string | null;
|
|
142
|
+
omittedAgentDefinitions?: number;
|
|
143
|
+
metadataDiscoveryIncomplete?: boolean;
|
|
142
144
|
}
|
|
143
145
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
+
export interface AgentDiscoveryOptions {
|
|
147
|
+
maxFiles?: number;
|
|
148
|
+
maxFileBytes?: number;
|
|
149
|
+
maxTotalBytes?: number;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
interface LoadedAgents {
|
|
153
|
+
agents: AgentConfig[];
|
|
154
|
+
omittedAgentDefinitions: number;
|
|
155
|
+
metadataDiscoveryIncomplete: boolean;
|
|
156
|
+
}
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
|
|
158
|
+
function readFileBoundedSync(
|
|
159
|
+
filePath: string,
|
|
160
|
+
maxBytes: number | undefined,
|
|
161
|
+
): { content?: string; bytes: number; limited: boolean } {
|
|
162
|
+
if (maxBytes === undefined) {
|
|
163
|
+
try {
|
|
164
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
165
|
+
return { content, bytes: Buffer.byteLength(content), limited: false };
|
|
166
|
+
} catch {
|
|
167
|
+
return { bytes: 0, limited: false };
|
|
168
|
+
}
|
|
149
169
|
}
|
|
150
170
|
|
|
171
|
+
const readLimit = Math.max(0, maxBytes);
|
|
172
|
+
let fd: number | undefined;
|
|
173
|
+
try {
|
|
174
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY | fs.constants.O_NONBLOCK);
|
|
175
|
+
if (!fs.fstatSync(fd).isFile()) return { bytes: 0, limited: false };
|
|
176
|
+
const buffer = Buffer.allocUnsafe(readLimit + 1);
|
|
177
|
+
let offset = 0;
|
|
178
|
+
while (offset < buffer.length) {
|
|
179
|
+
const bytesRead = fs.readSync(fd, buffer, offset, buffer.length - offset, null);
|
|
180
|
+
if (bytesRead === 0) break;
|
|
181
|
+
offset += bytesRead;
|
|
182
|
+
}
|
|
183
|
+
if (offset > readLimit) return { bytes: offset, limited: true };
|
|
184
|
+
return { content: buffer.subarray(0, offset).toString("utf-8"), bytes: offset, limited: false };
|
|
185
|
+
} catch {
|
|
186
|
+
return { bytes: 0, limited: false };
|
|
187
|
+
} finally {
|
|
188
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function loadAgentsFromDir(
|
|
193
|
+
dir: string,
|
|
194
|
+
source: "user" | "project",
|
|
195
|
+
options: AgentDiscoveryOptions = {},
|
|
196
|
+
): LoadedAgents {
|
|
197
|
+
const agents: AgentConfig[] = [];
|
|
198
|
+
let omittedAgentDefinitions = 0;
|
|
199
|
+
|
|
151
200
|
let entries: fs.Dirent[];
|
|
152
201
|
try {
|
|
153
202
|
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
154
|
-
} catch {
|
|
155
|
-
return
|
|
203
|
+
} catch (error) {
|
|
204
|
+
return {
|
|
205
|
+
agents,
|
|
206
|
+
omittedAgentDefinitions,
|
|
207
|
+
metadataDiscoveryIncomplete: (error as NodeJS.ErrnoException).code !== "ENOENT",
|
|
208
|
+
};
|
|
156
209
|
}
|
|
157
210
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
211
|
+
const agentEntries = entries
|
|
212
|
+
.filter((entry) => entry.name.endsWith(".md"))
|
|
213
|
+
.filter((entry) => entry.isFile() || entry.isSymbolicLink());
|
|
214
|
+
let totalBytes = 0;
|
|
161
215
|
|
|
216
|
+
for (const [index, entry] of agentEntries.entries()) {
|
|
217
|
+
if (options.maxFiles !== undefined && index >= options.maxFiles) {
|
|
218
|
+
omittedAgentDefinitions += agentEntries.length - index;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
162
221
|
const filePath = path.join(dir, entry.name);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
222
|
+
const remainingBytes =
|
|
223
|
+
options.maxTotalBytes === undefined ? undefined : options.maxTotalBytes - totalBytes;
|
|
224
|
+
if (remainingBytes !== undefined && remainingBytes <= 0) {
|
|
225
|
+
omittedAgentDefinitions++;
|
|
167
226
|
continue;
|
|
168
227
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
228
|
+
const maxBytes =
|
|
229
|
+
options.maxFileBytes === undefined
|
|
230
|
+
? remainingBytes
|
|
231
|
+
: remainingBytes === undefined
|
|
232
|
+
? options.maxFileBytes
|
|
233
|
+
: Math.min(options.maxFileBytes, remainingBytes);
|
|
234
|
+
const loaded = readFileBoundedSync(filePath, maxBytes);
|
|
235
|
+
totalBytes += Math.min(loaded.bytes, maxBytes ?? loaded.bytes);
|
|
236
|
+
if (loaded.limited || loaded.content === undefined) {
|
|
237
|
+
if (loaded.limited) omittedAgentDefinitions++;
|
|
173
238
|
continue;
|
|
174
239
|
}
|
|
175
240
|
|
|
241
|
+
const { frontmatter, body } = parseFrontmatter<Record<string, string>>(loaded.content);
|
|
242
|
+
|
|
243
|
+
if (!frontmatter.name || !frontmatter.description) continue;
|
|
244
|
+
|
|
176
245
|
const tools = frontmatter.tools
|
|
177
246
|
?.split(",")
|
|
178
247
|
.map((t: string) => t.trim())
|
|
@@ -192,7 +261,7 @@ function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig
|
|
|
192
261
|
});
|
|
193
262
|
}
|
|
194
263
|
|
|
195
|
-
return agents;
|
|
264
|
+
return { agents, omittedAgentDefinitions, metadataDiscoveryIncomplete: false };
|
|
196
265
|
}
|
|
197
266
|
|
|
198
267
|
function isDirectory(p: string): boolean {
|
|
@@ -223,13 +292,21 @@ export function discoverAgents(
|
|
|
223
292
|
cwd: string,
|
|
224
293
|
scope: AgentScope,
|
|
225
294
|
config?: SubagentSettings,
|
|
295
|
+
options: AgentDiscoveryOptions = {},
|
|
226
296
|
): AgentDiscoveryResult {
|
|
227
297
|
const userDir = path.join(getAgentDir(), "agents");
|
|
228
298
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
229
299
|
|
|
230
|
-
const
|
|
231
|
-
|
|
232
|
-
|
|
300
|
+
const userLoaded =
|
|
301
|
+
scope === "project"
|
|
302
|
+
? { agents: [], omittedAgentDefinitions: 0, metadataDiscoveryIncomplete: false }
|
|
303
|
+
: loadAgentsFromDir(userDir, "user", options);
|
|
304
|
+
const projectLoaded =
|
|
305
|
+
scope === "user" || !projectAgentsDir
|
|
306
|
+
? { agents: [], omittedAgentDefinitions: 0, metadataDiscoveryIncomplete: false }
|
|
307
|
+
: loadAgentsFromDir(projectAgentsDir, "project", options);
|
|
308
|
+
const userAgents = userLoaded.agents;
|
|
309
|
+
const projectAgents = projectLoaded.agents;
|
|
233
310
|
|
|
234
311
|
const agentMap = new Map<string, AgentConfig>();
|
|
235
312
|
|
|
@@ -268,7 +345,16 @@ export function discoverAgents(
|
|
|
268
345
|
agentMap.set(name, nextAgent);
|
|
269
346
|
}
|
|
270
347
|
|
|
271
|
-
|
|
348
|
+
const omittedAgentDefinitions =
|
|
349
|
+
userLoaded.omittedAgentDefinitions + projectLoaded.omittedAgentDefinitions;
|
|
350
|
+
const metadataDiscoveryIncomplete =
|
|
351
|
+
userLoaded.metadataDiscoveryIncomplete || projectLoaded.metadataDiscoveryIncomplete;
|
|
352
|
+
return {
|
|
353
|
+
agents: Array.from(agentMap.values()),
|
|
354
|
+
projectAgentsDir,
|
|
355
|
+
...(omittedAgentDefinitions > 0 ? { omittedAgentDefinitions } : {}),
|
|
356
|
+
...(metadataDiscoveryIncomplete ? { metadataDiscoveryIncomplete } : {}),
|
|
357
|
+
};
|
|
272
358
|
}
|
|
273
359
|
|
|
274
360
|
export function formatAgentList(
|
|
@@ -283,3 +369,203 @@ export function formatAgentList(
|
|
|
283
369
|
remaining,
|
|
284
370
|
};
|
|
285
371
|
}
|
|
372
|
+
|
|
373
|
+
export interface AgentCatalog {
|
|
374
|
+
/** The effective catalog for the default invocation scope. */
|
|
375
|
+
user: AgentDiscoveryResult;
|
|
376
|
+
/** The project-scope catalog; custom project definitions are loaded only after project trust. */
|
|
377
|
+
project?: AgentDiscoveryResult;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export interface AgentCatalogFormatOptions {
|
|
381
|
+
maxItems?: number;
|
|
382
|
+
maxDescriptionLength?: number;
|
|
383
|
+
maxCharacters?: number;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export interface AgentCatalogFormatResult {
|
|
387
|
+
text: string;
|
|
388
|
+
omitted: number;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export const DEFAULT_AGENT_CATALOG_MAX_ITEMS = 32;
|
|
392
|
+
export const DEFAULT_AGENT_CATALOG_MAX_DESCRIPTION_LENGTH = 240;
|
|
393
|
+
export const DEFAULT_AGENT_CATALOG_MAX_CHARACTERS = 6_000;
|
|
394
|
+
export const DEFAULT_AGENT_CATALOG_MAX_FILES_PER_SCOPE = 128;
|
|
395
|
+
export const DEFAULT_AGENT_CATALOG_MAX_FILE_BYTES = 64 * 1024;
|
|
396
|
+
export const DEFAULT_AGENT_CATALOG_MAX_TOTAL_BYTES_PER_SCOPE = 2 * 1024 * 1024;
|
|
397
|
+
|
|
398
|
+
const BUILT_IN_AGENT_ORDER = new Map(BUILT_IN_AGENTS.map((agent, index) => [agent.name, index]));
|
|
399
|
+
|
|
400
|
+
function compareCatalogAgents(left: AgentConfig, right: AgentConfig): number {
|
|
401
|
+
const leftBuiltInOrder = BUILT_IN_AGENT_ORDER.get(left.name);
|
|
402
|
+
const rightBuiltInOrder = BUILT_IN_AGENT_ORDER.get(right.name);
|
|
403
|
+
if (leftBuiltInOrder !== undefined || rightBuiltInOrder !== undefined) {
|
|
404
|
+
if (leftBuiltInOrder === undefined) return 1;
|
|
405
|
+
if (rightBuiltInOrder === undefined) return -1;
|
|
406
|
+
return leftBuiltInOrder - rightBuiltInOrder;
|
|
407
|
+
}
|
|
408
|
+
return left.name.localeCompare(right.name);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function normalizeCatalogDescription(description: string, maxLength: number): string {
|
|
412
|
+
const normalized = description.replace(/\s+/gu, " ").trim();
|
|
413
|
+
if (normalized.length <= maxLength) return normalized;
|
|
414
|
+
const suffix = "…";
|
|
415
|
+
return `${normalized.slice(0, Math.max(0, maxLength - suffix.length)).trimEnd()}${suffix}`;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
type CatalogScope = "user" | "project" | "project-fallback";
|
|
419
|
+
|
|
420
|
+
function catalogAgentLine(
|
|
421
|
+
agent: AgentConfig,
|
|
422
|
+
scope: CatalogScope,
|
|
423
|
+
userNames: ReadonlySet<string>,
|
|
424
|
+
maxDescriptionLength: number,
|
|
425
|
+
): string {
|
|
426
|
+
const scopeLabel =
|
|
427
|
+
scope === "user"
|
|
428
|
+
? 'agentScope: "user"'
|
|
429
|
+
: scope === "project"
|
|
430
|
+
? 'requires agentScope: "project" or "both"'
|
|
431
|
+
: 'requires agentScope: "project" ("both" selects the user definition)';
|
|
432
|
+
const collision =
|
|
433
|
+
scope !== "user" && userNames.has(agent.name)
|
|
434
|
+
? scope === "project"
|
|
435
|
+
? "; overrides the default user definition for project/both"
|
|
436
|
+
: "; scope-specific fallback for the default user override"
|
|
437
|
+
: "";
|
|
438
|
+
return `- ${agent.name} [source: ${agent.source}; ${scopeLabel}${collision}] — ${normalizeCatalogDescription(agent.description, maxDescriptionLength)}`;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Format the effective agent variants that the parent model can invoke.
|
|
443
|
+
*
|
|
444
|
+
* User-authored descriptions are prompt text, so this formatter deliberately normalizes and bounds
|
|
445
|
+
* them. Project definitions are supplied separately by the caller so an untrusted project is never
|
|
446
|
+
* read merely to build model-facing metadata.
|
|
447
|
+
*/
|
|
448
|
+
export function formatAgentCatalog(
|
|
449
|
+
catalog: AgentCatalog,
|
|
450
|
+
options: AgentCatalogFormatOptions = {},
|
|
451
|
+
): AgentCatalogFormatResult {
|
|
452
|
+
const maxItems = Math.max(0, options.maxItems ?? DEFAULT_AGENT_CATALOG_MAX_ITEMS);
|
|
453
|
+
const maxDescriptionLength = Math.max(
|
|
454
|
+
1,
|
|
455
|
+
options.maxDescriptionLength ?? DEFAULT_AGENT_CATALOG_MAX_DESCRIPTION_LENGTH,
|
|
456
|
+
);
|
|
457
|
+
const maxCharacters = Math.max(1, options.maxCharacters ?? DEFAULT_AGENT_CATALOG_MAX_CHARACTERS);
|
|
458
|
+
const userDiscoveryIncomplete =
|
|
459
|
+
(catalog.user.omittedAgentDefinitions ?? 0) > 0 ||
|
|
460
|
+
catalog.user.metadataDiscoveryIncomplete === true;
|
|
461
|
+
const projectDiscoveryIncomplete =
|
|
462
|
+
(catalog.project?.omittedAgentDefinitions ?? 0) > 0 ||
|
|
463
|
+
catalog.project?.metadataDiscoveryIncomplete === true;
|
|
464
|
+
const discoveredUserAgents = [...catalog.user.agents].sort(compareCatalogAgents);
|
|
465
|
+
const discoveredProjectScopeAgents = [...(catalog.project?.agents ?? [])].sort(
|
|
466
|
+
compareCatalogAgents,
|
|
467
|
+
);
|
|
468
|
+
const userAgents = userDiscoveryIncomplete ? [] : discoveredUserAgents;
|
|
469
|
+
const projectScopeAgents = projectDiscoveryIncomplete ? [] : discoveredProjectScopeAgents;
|
|
470
|
+
const projectAgents = projectScopeAgents.filter((agent) => agent.source === "project");
|
|
471
|
+
const discoveredUserByName = new Map(discoveredUserAgents.map((agent) => [agent.name, agent]));
|
|
472
|
+
const userByName = new Map(userAgents.map((agent) => [agent.name, agent]));
|
|
473
|
+
const userNames = new Set(userByName.keys());
|
|
474
|
+
const potentialProjectFallbackAgents = discoveredProjectScopeAgents.filter(
|
|
475
|
+
(agent) =>
|
|
476
|
+
agent.source === "built-in" && discoveredUserByName.get(agent.name)?.source === "user",
|
|
477
|
+
);
|
|
478
|
+
const projectFallbackAgents =
|
|
479
|
+
userDiscoveryIncomplete || projectDiscoveryIncomplete ? [] : potentialProjectFallbackAgents;
|
|
480
|
+
const allEntries = [
|
|
481
|
+
...userAgents.map((agent) => ({ agent, scope: "user" as const })),
|
|
482
|
+
...projectAgents.map((agent) => ({ agent, scope: "project" as const })),
|
|
483
|
+
...projectFallbackAgents.map((agent) => ({ agent, scope: "project-fallback" as const })),
|
|
484
|
+
];
|
|
485
|
+
const boundedEntries = allEntries.slice(0, maxItems);
|
|
486
|
+
const suppressedMetadataEntries =
|
|
487
|
+
(userDiscoveryIncomplete ? discoveredUserAgents.length : 0) +
|
|
488
|
+
(projectDiscoveryIncomplete
|
|
489
|
+
? discoveredProjectScopeAgents.filter((agent) => agent.source === "project").length +
|
|
490
|
+
potentialProjectFallbackAgents.length
|
|
491
|
+
: 0);
|
|
492
|
+
const discoveryOmissions =
|
|
493
|
+
(catalog.user.omittedAgentDefinitions ?? 0) +
|
|
494
|
+
(catalog.project?.omittedAgentDefinitions ?? 0) +
|
|
495
|
+
suppressedMetadataEntries;
|
|
496
|
+
const discoveryIncomplete =
|
|
497
|
+
catalog.user.metadataDiscoveryIncomplete === true ||
|
|
498
|
+
catalog.project?.metadataDiscoveryIncomplete === true;
|
|
499
|
+
|
|
500
|
+
const render = (entries: typeof allEntries, omitted: number): string => {
|
|
501
|
+
const lines = [
|
|
502
|
+
"Available agent definitions (metadata only; runtime validation and trust remain authoritative).",
|
|
503
|
+
];
|
|
504
|
+
const userLines = entries
|
|
505
|
+
.filter((entry) => entry.scope === "user")
|
|
506
|
+
.map((entry) => catalogAgentLine(entry.agent, entry.scope, userNames, maxDescriptionLength));
|
|
507
|
+
if (userLines.length > 0) {
|
|
508
|
+
lines.push('Default scope (agentScope: "user"):');
|
|
509
|
+
lines.push(...userLines);
|
|
510
|
+
}
|
|
511
|
+
const projectLines = entries
|
|
512
|
+
.filter((entry) => entry.scope !== "user")
|
|
513
|
+
.map((entry) => catalogAgentLine(entry.agent, entry.scope, userNames, maxDescriptionLength));
|
|
514
|
+
if (projectLines.length > 0) {
|
|
515
|
+
lines.push("Trusted project/scope variants (use the required agentScope shown):");
|
|
516
|
+
lines.push(...projectLines);
|
|
517
|
+
}
|
|
518
|
+
const collisionNames = entries
|
|
519
|
+
.filter((entry) => entry.scope !== "user" && userNames.has(entry.agent.name))
|
|
520
|
+
.map((entry) => entry.agent.name);
|
|
521
|
+
if (collisionNames.length > 0 && projectLines.length > 0) {
|
|
522
|
+
const precedence = entries
|
|
523
|
+
.filter((entry) => entry.scope !== "user" && userNames.has(entry.agent.name))
|
|
524
|
+
.map((entry) =>
|
|
525
|
+
entry.scope === "project"
|
|
526
|
+
? `${entry.agent.name}: user with "user", project with "project"/"both"`
|
|
527
|
+
: `${entry.agent.name}: user with "user"/"both", built-in with "project"`,
|
|
528
|
+
);
|
|
529
|
+
lines.push(`Same-name precedence: ${precedence.join("; ")}.`);
|
|
530
|
+
}
|
|
531
|
+
if (omitted > 0) {
|
|
532
|
+
lines.push(
|
|
533
|
+
`[${omitted} additional agent definition${omitted === 1 ? "" : "s"} omitted due to metadata bounds or incomplete discovery.]`,
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if (discoveryIncomplete) {
|
|
537
|
+
lines.push("[Agent metadata discovery was incomplete; some definitions may be unavailable.]");
|
|
538
|
+
}
|
|
539
|
+
return lines.join("\n");
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
let listedCount = boundedEntries.length;
|
|
543
|
+
let text = render(
|
|
544
|
+
boundedEntries.slice(0, listedCount),
|
|
545
|
+
allEntries.length - listedCount + discoveryOmissions,
|
|
546
|
+
);
|
|
547
|
+
while (text.length > maxCharacters && listedCount > 0) {
|
|
548
|
+
listedCount -= 1;
|
|
549
|
+
text = render(
|
|
550
|
+
boundedEntries.slice(0, listedCount),
|
|
551
|
+
allEntries.length - listedCount + discoveryOmissions,
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
return { text, omitted: allEntries.length - listedCount + discoveryOmissions };
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export function discoverAgentCatalog(
|
|
558
|
+
cwd: string,
|
|
559
|
+
projectTrusted: boolean,
|
|
560
|
+
config?: SubagentSettings,
|
|
561
|
+
): AgentCatalog {
|
|
562
|
+
const options: AgentDiscoveryOptions = {
|
|
563
|
+
maxFiles: DEFAULT_AGENT_CATALOG_MAX_FILES_PER_SCOPE,
|
|
564
|
+
maxFileBytes: DEFAULT_AGENT_CATALOG_MAX_FILE_BYTES,
|
|
565
|
+
maxTotalBytes: DEFAULT_AGENT_CATALOG_MAX_TOTAL_BYTES_PER_SCOPE,
|
|
566
|
+
};
|
|
567
|
+
return {
|
|
568
|
+
user: discoverAgents(cwd, "user", config, options),
|
|
569
|
+
project: projectTrusted ? discoverAgents(cwd, "project", config, options) : undefined,
|
|
570
|
+
};
|
|
571
|
+
}
|
package/src/stateful.ts
CHANGED
|
@@ -106,6 +106,7 @@ export interface StatefulSubagentRuntimeStatus {
|
|
|
106
106
|
export interface StatefulSubagentController {
|
|
107
107
|
getCompletionDelivery(): CompletionDelivery;
|
|
108
108
|
setCompletionDelivery(value: CompletionDelivery): void;
|
|
109
|
+
setAgentCatalog(value: string): void;
|
|
109
110
|
getRuntimeStatus(): StatefulSubagentRuntimeStatus;
|
|
110
111
|
listAgents(includeClosed?: boolean): ManagedAgent[];
|
|
111
112
|
clearAgents(): Promise<number>;
|
|
@@ -127,6 +128,7 @@ export function registerStatefulSubagents(
|
|
|
127
128
|
const enabled = settings.enabled !== false;
|
|
128
129
|
const transportKind = resolveStatefulTransportKind(settings.transport);
|
|
129
130
|
let completionDelivery = resolveCompletionDelivery(settings.completionDelivery);
|
|
131
|
+
let agentCatalog = "";
|
|
130
132
|
let completionBroker: CompletionDeliveryBroker | undefined;
|
|
131
133
|
let refreshSpawnToolRegistration: (() => void) | undefined;
|
|
132
134
|
let registry: AgentRegistry | undefined;
|
|
@@ -161,6 +163,10 @@ export function registerStatefulSubagents(
|
|
|
161
163
|
completionBroker?.setDelivery(value);
|
|
162
164
|
refreshSpawnToolRegistration?.();
|
|
163
165
|
},
|
|
166
|
+
setAgentCatalog(value) {
|
|
167
|
+
agentCatalog = value;
|
|
168
|
+
refreshSpawnToolRegistration?.();
|
|
169
|
+
},
|
|
164
170
|
getRuntimeStatus() {
|
|
165
171
|
const agents = registry?.list(true) ?? [];
|
|
166
172
|
return {
|
|
@@ -315,11 +321,12 @@ export function registerStatefulSubagents(
|
|
|
315
321
|
}
|
|
316
322
|
});
|
|
317
323
|
|
|
324
|
+
const baseSpawnDescription =
|
|
325
|
+
"Start an addressable background subagent with an optional thinking level chosen for the task difficulty, return immediately with an agentId, and receive its completion asynchronously.";
|
|
318
326
|
const spawnTool = defineTool({
|
|
319
327
|
name: "subagent_spawn",
|
|
320
328
|
label: "Spawn Subagent",
|
|
321
|
-
description:
|
|
322
|
-
"Start an addressable background subagent with an optional thinking level chosen for the task difficulty, return immediately with an agentId, and receive its completion asynchronously.",
|
|
329
|
+
description: appendAgentCatalog(baseSpawnDescription, agentCatalog),
|
|
323
330
|
promptSnippet: "Start a reusable detached subagent; completion is delivered asynchronously",
|
|
324
331
|
promptGuidelines: createSpawnPromptGuidelines(completionDelivery, blockingEnabled),
|
|
325
332
|
parameters: Type.Object({
|
|
@@ -399,6 +406,7 @@ export function registerStatefulSubagents(
|
|
|
399
406
|
},
|
|
400
407
|
});
|
|
401
408
|
refreshSpawnToolRegistration = () => {
|
|
409
|
+
spawnTool.description = appendAgentCatalog(baseSpawnDescription, agentCatalog);
|
|
402
410
|
spawnTool.promptGuidelines = createSpawnPromptGuidelines(completionDelivery, blockingEnabled);
|
|
403
411
|
pi.registerTool(spawnTool);
|
|
404
412
|
};
|
|
@@ -917,6 +925,10 @@ async function cleanupClosedWorkspaces(
|
|
|
917
925
|
}
|
|
918
926
|
}
|
|
919
927
|
|
|
928
|
+
function appendAgentCatalog(baseDescription: string, catalog: string): string {
|
|
929
|
+
return catalog ? `${baseDescription}\n\n${catalog}` : baseDescription;
|
|
930
|
+
}
|
|
931
|
+
|
|
920
932
|
function result(agent: ManagedAgent, text: string) {
|
|
921
933
|
return {
|
|
922
934
|
content: [{ type: "text" as const, text }],
|
package/src/subagents.ts
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
* Uses JSON mode to capture structured output from subagents.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import type { ExtensionAPI, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { discoverAgentCatalog, formatAgentCatalog } from "./agents.js";
|
|
16
17
|
import { registerSubagentConfigCommand } from "./config-ui.js";
|
|
17
18
|
import { executeSubagent } from "./execution.js";
|
|
18
19
|
import { SubagentParams } from "./params.js";
|
|
@@ -23,43 +24,53 @@ import { registerStatefulSubagents } from "./stateful.js";
|
|
|
23
24
|
|
|
24
25
|
export default function (pi: ExtensionAPI) {
|
|
25
26
|
const settings = readSubagentSettings();
|
|
26
|
-
|
|
27
|
+
const blockingEnabled = settings?.blocking?.enabled !== false;
|
|
28
|
+
const refreshBlockingCatalog = blockingEnabled ? registerBlockingSubagent(pi) : () => undefined;
|
|
29
|
+
let refreshStatefulCatalog: (catalog: string) => void = () => undefined;
|
|
27
30
|
|
|
28
31
|
pi.on("session_start", (_event, ctx) => {
|
|
29
32
|
// Preserve a one-shot migration notice from extension load while refreshing
|
|
30
33
|
// validation against settings that may have changed before this session.
|
|
31
34
|
const loadNotice = consumeSubagentSettingsNotice();
|
|
32
|
-
readSubagentSettings();
|
|
35
|
+
const refreshedSettings = readSubagentSettings();
|
|
33
36
|
const refreshedNotice = consumeSubagentSettingsNotice();
|
|
34
37
|
const notice = [
|
|
35
38
|
...new Set([loadNotice, refreshedNotice].filter((value) => value !== undefined)),
|
|
36
39
|
].join("\n");
|
|
37
40
|
if (notice) ctx.ui.notify(notice, "warning");
|
|
41
|
+
|
|
42
|
+
const catalog = formatAgentCatalog(
|
|
43
|
+
discoverAgentCatalog(ctx.cwd, ctx.isProjectTrusted(), refreshedSettings),
|
|
44
|
+
).text;
|
|
45
|
+
refreshBlockingCatalog(catalog);
|
|
46
|
+
refreshStatefulCatalog(catalog);
|
|
38
47
|
});
|
|
39
48
|
|
|
40
|
-
const blockingEnabled = settings?.blocking?.enabled !== false;
|
|
41
49
|
const statefulRuntime = registerStatefulSubagents(pi, {
|
|
42
50
|
blockingEnabled,
|
|
43
51
|
settings: settings?.stateful,
|
|
44
52
|
});
|
|
53
|
+
refreshStatefulCatalog = statefulRuntime.setAgentCatalog;
|
|
45
54
|
registerSubagentConfigCommand(pi, {
|
|
46
55
|
...statefulRuntime,
|
|
47
56
|
getBlockingEnabled: () => blockingEnabled,
|
|
48
57
|
});
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
function registerBlockingSubagent(pi: ExtensionAPI) {
|
|
52
|
-
|
|
60
|
+
function registerBlockingSubagent(pi: ExtensionAPI): (catalog: string) => void {
|
|
61
|
+
let catalog = "";
|
|
62
|
+
const baseDescription = [
|
|
63
|
+
"Run specialized subagents as a blocking operation with isolated contexts.",
|
|
64
|
+
"The call blocks the main agent until every worker and optional aggregator finishes, so queued steering waits.",
|
|
65
|
+
"Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
|
|
66
|
+
"Parallel mode may include an aggregator fan-in step that receives all task outputs.",
|
|
67
|
+
'Default agent scope is "user" (from ~/.pi/agent/agents).',
|
|
68
|
+
'To enable project-local agents in .pi/agents, pass agentScope: "both" (or "project") as a top-level argument for that call.',
|
|
69
|
+
].join(" ");
|
|
70
|
+
const definition: ToolDefinition<typeof SubagentParams, SubagentDetails> = {
|
|
53
71
|
name: "subagent",
|
|
54
72
|
label: "Blocking Subagent",
|
|
55
|
-
description:
|
|
56
|
-
"Run specialized subagents as a blocking operation with isolated contexts.",
|
|
57
|
-
"The call blocks the main agent until every worker and optional aggregator finishes, so queued steering waits.",
|
|
58
|
-
"Modes: single (agent + task), parallel (tasks array), chain (sequential with {previous} placeholder).",
|
|
59
|
-
"Parallel mode may include an aggregator fan-in step that receives all task outputs.",
|
|
60
|
-
'Default agent scope is "user" (from ~/.pi/agent/agents).',
|
|
61
|
-
'To enable project-local agents in .pi/agents, pass agentScope: "both" (or "project") as a top-level argument for that call.',
|
|
62
|
-
].join(" "),
|
|
73
|
+
description: appendAgentCatalog(baseDescription, catalog),
|
|
63
74
|
promptSnippet:
|
|
64
75
|
"Run blocking isolated subagents only when their outputs are required before the main agent can continue.",
|
|
65
76
|
promptGuidelines: [
|
|
@@ -85,13 +96,22 @@ function registerBlockingSubagent(pi: ExtensionAPI) {
|
|
|
85
96
|
renderResult(result, options, theme) {
|
|
86
97
|
return renderSubagentResult(result, options, theme);
|
|
87
98
|
},
|
|
88
|
-
}
|
|
89
|
-
|
|
99
|
+
};
|
|
100
|
+
pi.registerTool<typeof SubagentParams, SubagentDetails>(definition);
|
|
90
101
|
pi.on("tool_result", (event) => {
|
|
91
102
|
if (event.toolName !== "subagent") return;
|
|
92
103
|
if ((event.details as (SubagentDetails & { isError?: boolean }) | undefined)?.isError)
|
|
93
104
|
return { isError: true };
|
|
94
105
|
});
|
|
106
|
+
return (nextCatalog: string) => {
|
|
107
|
+
catalog = nextCatalog;
|
|
108
|
+
definition.description = appendAgentCatalog(baseDescription, catalog);
|
|
109
|
+
pi.registerTool<typeof SubagentParams, SubagentDetails>(definition);
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function appendAgentCatalog(baseDescription: string, catalog: string): string {
|
|
114
|
+
return catalog ? `${baseDescription}\n\n${catalog}` : baseDescription;
|
|
95
115
|
}
|
|
96
116
|
|
|
97
117
|
export { parsePositiveInteger } from "./execution.js";
|