@johnnywu/pi-subagents 1.0.1 → 1.2.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/CHANGELOG.md +15 -0
- package/README.md +19 -0
- package/extensions/agent-loader.ts +3 -0
- package/extensions/env-utils.ts +26 -0
- package/extensions/index.ts +14 -0
- package/extensions/skill-resolver.ts +146 -0
- package/extensions/subagent-executor.ts +60 -1
- package/extensions/subagent-prompt.ts +13 -0
- package/extensions/subagent-tool.ts +8 -27
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
# [1.2.0](https://github.com/jwu/pi-subagents/compare/v1.1.0...v1.2.0) (2026-06-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* agent skills frontmatter support ([e479b0c](https://github.com/jwu/pi-subagents/commit/e479b0cc57c8097a89f73b491c32a099ba56bc77))
|
|
7
|
+
* expose available subagents in prompts ([047035a](https://github.com/jwu/pi-subagents/commit/047035a49e9749dc719a97f2556be9f60551cf97))
|
|
8
|
+
|
|
9
|
+
# [1.1.0](https://github.com/jwu/pi-subagents/compare/v1.0.1...v1.1.0) (2026-05-31)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* **subagent:** list available agents in system prompt Guidelines ([317ed58](https://github.com/jwu/pi-subagents/commit/317ed5845379d3edd535bc03dab8460b29a82164))
|
|
15
|
+
|
|
1
16
|
## [1.0.1](https://github.com/jwu/pi-subagents/compare/v1.0.0...v1.0.1) (2026-05-31)
|
|
2
17
|
|
|
3
18
|
|
package/README.md
CHANGED
|
@@ -48,6 +48,23 @@ Or instruct pi to delegate:
|
|
|
48
48
|
Run the code-reviewer agent on the last three commits
|
|
49
49
|
```
|
|
50
50
|
|
|
51
|
+
### Available subagents in the prompt
|
|
52
|
+
|
|
53
|
+
The extension exposes discovered sub-agents to the model in two places:
|
|
54
|
+
|
|
55
|
+
- The `subagent` tool keeps a one-line prompt guideline:
|
|
56
|
+
`Available subagents: code-reviewer, refactor, test-writer`
|
|
57
|
+
- The system prompt also gets an independent block:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
Available subagents:
|
|
61
|
+
- code-reviewer
|
|
62
|
+
- refactor
|
|
63
|
+
- test-writer
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For sub-agents launched with `systemPrompt: replace` or `systemPrompt: append`, the same block is written into the prompt passed via `--system-prompt` or `--append-system-prompt` when that agent has the `subagent` tool.
|
|
67
|
+
|
|
51
68
|
## Agent configuration
|
|
52
69
|
|
|
53
70
|
Agents are Markdown files with YAML frontmatter.
|
|
@@ -102,6 +119,8 @@ Sub-agents can spawn their own sub-agents (if the `subagent` tool is in their wh
|
|
|
102
119
|
|
|
103
120
|
**`allowedAgents`** — Whitelist enforced by the parent before spawning. A child process never sees agent names outside its parent's whitelist.
|
|
104
121
|
|
|
122
|
+
The available-subagents prompt entries respect the same filtering: parent sessions use the currently visible agents, and child sessions only list agents allowed by their parent.
|
|
123
|
+
|
|
105
124
|
These are passed via environment variables (`PI_SUBAGENT_DEPTH`, `PI_SUBAGENT_MAX_DEPTH`, `PI_SUBAGENT_ALLOWED`).
|
|
106
125
|
|
|
107
126
|
## Session storage
|
|
@@ -15,6 +15,7 @@ export interface AgentConfig {
|
|
|
15
15
|
systemPromptMode: SystemPromptMode;
|
|
16
16
|
allowedAgents?: string[];
|
|
17
17
|
maxDepth: number;
|
|
18
|
+
skills?: string[];
|
|
18
19
|
prompt: string;
|
|
19
20
|
source: AgentSource;
|
|
20
21
|
filePath: string;
|
|
@@ -117,11 +118,13 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
|
|
|
117
118
|
}
|
|
118
119
|
|
|
119
120
|
const allowedAgents = splitCsv(data.allowedAgents);
|
|
121
|
+
const skills = splitCsv(data.skills);
|
|
120
122
|
|
|
121
123
|
return {
|
|
122
124
|
name: data.name,
|
|
123
125
|
description: data.description,
|
|
124
126
|
tools: splitCsv(data.tools),
|
|
127
|
+
skills: skills.length > 0 ? skills : undefined,
|
|
125
128
|
model: data.model || undefined,
|
|
126
129
|
thinking,
|
|
127
130
|
systemPromptMode,
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type RecursionEnv = Partial<
|
|
2
|
+
Record<'PI_SUBAGENT_ALLOWED' | 'PI_SUBAGENT_DEPTH' | 'PI_SUBAGENT_MAX_DEPTH', string>
|
|
3
|
+
>;
|
|
4
|
+
|
|
5
|
+
export function parseEnvNumber(value: string | undefined): number | undefined {
|
|
6
|
+
if (value === undefined) return undefined;
|
|
7
|
+
const parsed = Number(value);
|
|
8
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function allowedAgentNames(env: RecursionEnv): Set<string> | undefined {
|
|
12
|
+
const raw = env?.PI_SUBAGENT_ALLOWED;
|
|
13
|
+
if (!raw) return undefined;
|
|
14
|
+
return new Set(
|
|
15
|
+
raw
|
|
16
|
+
.split(',')
|
|
17
|
+
.map((name) => name.trim())
|
|
18
|
+
.filter(Boolean),
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isPastMaxDepth(env: RecursionEnv): boolean {
|
|
23
|
+
const depth = parseEnvNumber(env?.PI_SUBAGENT_DEPTH);
|
|
24
|
+
const maxDepth = parseEnvNumber(env?.PI_SUBAGENT_MAX_DEPTH);
|
|
25
|
+
return depth !== undefined && maxDepth !== undefined && depth > maxDepth;
|
|
26
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
2
|
import { loadAgentDefinitions } from './agent-loader.ts';
|
|
3
|
+
import { allowedAgentNames, isPastMaxDepth } from './env-utils.ts';
|
|
4
|
+
import { appendAvailableSubagentsBlock } from './subagent-prompt.ts';
|
|
3
5
|
import { registerSubagentTool } from './subagent-tool.ts';
|
|
4
6
|
|
|
5
7
|
export default async function (pi: ExtensionAPI) {
|
|
@@ -9,5 +11,17 @@ export default async function (pi: ExtensionAPI) {
|
|
|
9
11
|
console.warn(`[pi-subagents] skipped ${warning.filePath}: ${warning.message}`);
|
|
10
12
|
}
|
|
11
13
|
|
|
14
|
+
const allowed = allowedAgentNames(process.env);
|
|
15
|
+
const agents = allowed
|
|
16
|
+
? result.agents.filter((candidate) => allowed.has(candidate.name))
|
|
17
|
+
: result.agents;
|
|
18
|
+
const agentNames = agents.map((agent) => agent.name);
|
|
19
|
+
|
|
20
|
+
if (!isPastMaxDepth(process.env) && agentNames.length > 0) {
|
|
21
|
+
pi.on('before_agent_start', (event) => ({
|
|
22
|
+
systemPrompt: appendAvailableSubagentsBlock(event.systemPrompt, agentNames),
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
registerSubagentTool(pi, { agents: result.agents });
|
|
13
27
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
|
|
5
|
+
export interface ResolvedSkill {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
location: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SkillResolverFs {
|
|
12
|
+
exists(filePath: string): boolean;
|
|
13
|
+
readFile(filePath: string): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ResolveSkillsOptions {
|
|
17
|
+
cwd: string;
|
|
18
|
+
globalDir?: string;
|
|
19
|
+
fs?: SkillResolverFs;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const defaultSkillFs: SkillResolverFs = {
|
|
23
|
+
exists(filePath) {
|
|
24
|
+
return fs.existsSync(filePath);
|
|
25
|
+
},
|
|
26
|
+
readFile(filePath) {
|
|
27
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function defaultGlobalSkillsDir(): string {
|
|
32
|
+
return path.join(os.homedir(), '.pi', 'agent', 'skills');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function parseSkillFrontmatter(content: string): { name: string; description: string } | undefined {
|
|
36
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
37
|
+
if (!normalized.startsWith('---')) return undefined;
|
|
38
|
+
|
|
39
|
+
const endIndex = normalized.indexOf('\n---', 3);
|
|
40
|
+
if (endIndex === -1) return undefined;
|
|
41
|
+
|
|
42
|
+
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
43
|
+
const lines = frontmatterBlock.split('\n');
|
|
44
|
+
const data: Record<string, string> = {};
|
|
45
|
+
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
const trimmed = lines[i].trim();
|
|
48
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
49
|
+
|
|
50
|
+
// Check if this is a YAML block scalar continuation (| or >)
|
|
51
|
+
if (
|
|
52
|
+
data._currentBlockKey !== undefined &&
|
|
53
|
+
(lines[i].startsWith(' ') || lines[i].startsWith('\t'))
|
|
54
|
+
) {
|
|
55
|
+
const current = data[data._currentBlockKey] ?? '';
|
|
56
|
+
data[data._currentBlockKey] = current ? `${current} ${trimmed}` : trimmed;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
delete data._currentBlockKey;
|
|
60
|
+
|
|
61
|
+
const separator = trimmed.indexOf(':');
|
|
62
|
+
if (separator === -1) continue;
|
|
63
|
+
const key = trimmed.slice(0, separator).trim();
|
|
64
|
+
let value = trimmed.slice(separator + 1).trim();
|
|
65
|
+
|
|
66
|
+
// Handle YAML block scalar indicators (| and >)
|
|
67
|
+
if (value === '|' || value === '>') {
|
|
68
|
+
data._currentBlockKey = key;
|
|
69
|
+
data[key] = '';
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
value = value.replace(/^['"]|['"]$/g, '');
|
|
74
|
+
if (key) data[key] = value;
|
|
75
|
+
}
|
|
76
|
+
delete data._currentBlockKey;
|
|
77
|
+
|
|
78
|
+
if (!data.description || !data.description.trim()) return undefined;
|
|
79
|
+
return { name: data.name || '', description: data.description.trim() };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findSkillFile(
|
|
83
|
+
skillName: string,
|
|
84
|
+
cwd: string,
|
|
85
|
+
globalDir: string,
|
|
86
|
+
fileSystem: SkillResolverFs,
|
|
87
|
+
): string | undefined {
|
|
88
|
+
// Priority 1: Project .agents/skills/<name>/SKILL.md
|
|
89
|
+
const projectAgentsPath = path.join(cwd, '.agents', 'skills', skillName, 'SKILL.md');
|
|
90
|
+
if (fileSystem.exists(projectAgentsPath)) return projectAgentsPath;
|
|
91
|
+
|
|
92
|
+
// Priority 1: Project .pi/skills/<name>/SKILL.md
|
|
93
|
+
const projectPiPath = path.join(cwd, '.pi', 'skills', skillName, 'SKILL.md');
|
|
94
|
+
if (fileSystem.exists(projectPiPath)) return projectPiPath;
|
|
95
|
+
|
|
96
|
+
// Priority 4: Global ~/.pi/agent/skills/<name>/SKILL.md
|
|
97
|
+
const globalPiPath = path.join(globalDir, skillName, 'SKILL.md');
|
|
98
|
+
if (fileSystem.exists(globalPiPath)) return globalPiPath;
|
|
99
|
+
|
|
100
|
+
// Priority 4: Global ~/.agents/skills/<name>/SKILL.md
|
|
101
|
+
const globalAgentsPath = path.join(os.homedir(), '.agents', 'skills', skillName, 'SKILL.md');
|
|
102
|
+
if (fileSystem.exists(globalAgentsPath)) return globalAgentsPath;
|
|
103
|
+
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function resolveSkills(
|
|
108
|
+
skillNames: string[],
|
|
109
|
+
options: ResolveSkillsOptions,
|
|
110
|
+
): { resolved: ResolvedSkill[]; missing: string[] } {
|
|
111
|
+
const cwd = options.cwd;
|
|
112
|
+
const globalDir = options.globalDir ?? defaultGlobalSkillsDir();
|
|
113
|
+
const fileSystem = options.fs ?? defaultSkillFs;
|
|
114
|
+
const resolved: ResolvedSkill[] = [];
|
|
115
|
+
const missing: string[] = [];
|
|
116
|
+
|
|
117
|
+
for (const name of skillNames) {
|
|
118
|
+
const trimmed = name.trim();
|
|
119
|
+
if (!trimmed) continue;
|
|
120
|
+
|
|
121
|
+
const filePath = findSkillFile(trimmed, cwd, globalDir, fileSystem);
|
|
122
|
+
if (!filePath) {
|
|
123
|
+
missing.push(trimmed);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const content = fileSystem.readFile(filePath);
|
|
129
|
+
const frontmatter = parseSkillFrontmatter(content);
|
|
130
|
+
if (!frontmatter) {
|
|
131
|
+
missing.push(trimmed);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
resolved.push({
|
|
136
|
+
name: frontmatter.name || trimmed,
|
|
137
|
+
description: frontmatter.description,
|
|
138
|
+
location: filePath,
|
|
139
|
+
});
|
|
140
|
+
} catch {
|
|
141
|
+
missing.push(trimmed);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { resolved, missing };
|
|
146
|
+
}
|
|
@@ -5,6 +5,8 @@ import * as os from 'node:os';
|
|
|
5
5
|
import * as path from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import type { AgentConfig } from './agent-loader.ts';
|
|
8
|
+
import { formatAvailableSubagentsBlock } from './subagent-prompt.ts';
|
|
9
|
+
import { resolveSkills } from './skill-resolver.ts';
|
|
8
10
|
|
|
9
11
|
export interface AgentUsage {
|
|
10
12
|
input: number;
|
|
@@ -77,6 +79,7 @@ export interface RunSubagentOptions {
|
|
|
77
79
|
signal?: AbortSignal;
|
|
78
80
|
onProgress?: (progress: AgentProgress) => void;
|
|
79
81
|
depth?: number;
|
|
82
|
+
availableAgents?: string[];
|
|
80
83
|
tempRoot?: string;
|
|
81
84
|
outputArchiveDir?: string;
|
|
82
85
|
agentDir?: string;
|
|
@@ -274,6 +277,40 @@ function buildTaskArgument(task: string, taskFilePath: string | undefined): stri
|
|
|
274
277
|
return taskFilePath ? `Task: @${taskFilePath}` : `Task: ${task}`;
|
|
275
278
|
}
|
|
276
279
|
|
|
280
|
+
function escapeXml(text: string): string {
|
|
281
|
+
return text
|
|
282
|
+
.replace(/&/g, '&')
|
|
283
|
+
.replace(/</g, '<')
|
|
284
|
+
.replace(/>/g, '>')
|
|
285
|
+
.replace(/"/g, '"')
|
|
286
|
+
.replace(/'/g, ''');
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function formatSkillsForPrompt(
|
|
290
|
+
skills: Array<{ name: string; description: string; location: string }>,
|
|
291
|
+
): string {
|
|
292
|
+
if (skills.length === 0) return '';
|
|
293
|
+
|
|
294
|
+
const lines = [
|
|
295
|
+
'\n\nThe following skills provide specialized instructions for specific tasks.',
|
|
296
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
297
|
+
'When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.',
|
|
298
|
+
'',
|
|
299
|
+
'<available_skills>',
|
|
300
|
+
];
|
|
301
|
+
|
|
302
|
+
for (const skill of skills) {
|
|
303
|
+
lines.push(' <skill>');
|
|
304
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
305
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
306
|
+
lines.push(` <location>${escapeXml(skill.location)}</location>`);
|
|
307
|
+
lines.push(' </skill>');
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
lines.push('</available_skills>');
|
|
311
|
+
return lines.join('\n');
|
|
312
|
+
}
|
|
313
|
+
|
|
277
314
|
function byteLength(text: string): number {
|
|
278
315
|
return Buffer.byteLength(text, 'utf8');
|
|
279
316
|
}
|
|
@@ -339,7 +376,29 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
339
376
|
|
|
340
377
|
try {
|
|
341
378
|
const promptFilePath = path.join(tempDir, 'system-prompt.md');
|
|
342
|
-
|
|
379
|
+
|
|
380
|
+
let promptContent = options.agent.prompt;
|
|
381
|
+
if (options.agent.tools.includes('subagent')) {
|
|
382
|
+
const availableSubagentsBlock = formatAvailableSubagentsBlock(
|
|
383
|
+
options.availableAgents ?? options.agent.allowedAgents ?? [],
|
|
384
|
+
);
|
|
385
|
+
if (availableSubagentsBlock) {
|
|
386
|
+
promptContent = `${promptContent.trimEnd()}\n\n${availableSubagentsBlock}`;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const skillNames = options.agent.skills;
|
|
391
|
+
if (skillNames && skillNames.length > 0) {
|
|
392
|
+
const { resolved, missing } = resolveSkills(skillNames, { cwd: options.cwd });
|
|
393
|
+
for (const name of missing) {
|
|
394
|
+
console.warn(`[pi-subagents] skill not found: ${name}`);
|
|
395
|
+
}
|
|
396
|
+
const skillInjection = formatSkillsForPrompt(resolved);
|
|
397
|
+
if (skillInjection) {
|
|
398
|
+
promptContent = `${promptContent}${skillInjection}`;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
await fileSystem.writeFile(promptFilePath, promptContent);
|
|
343
402
|
|
|
344
403
|
let taskFilePath: string | undefined;
|
|
345
404
|
if (options.task.length > TASK_FILE_THRESHOLD) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function formatAvailableSubagentsBlock(agentNames: string[]): string | undefined {
|
|
2
|
+
const names = [...new Set(agentNames.map((name) => name.trim()).filter(Boolean))].sort();
|
|
3
|
+
if (names.length === 0) return undefined;
|
|
4
|
+
|
|
5
|
+
return ['Available subagents:', ...names.map((name) => `- ${name}`)].join('\n');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function appendAvailableSubagentsBlock(systemPrompt: string, agentNames: string[]): string {
|
|
9
|
+
const block = formatAvailableSubagentsBlock(agentNames);
|
|
10
|
+
if (!block || systemPrompt.includes(block)) return systemPrompt;
|
|
11
|
+
|
|
12
|
+
return `${systemPrompt.trimEnd()}\n\n${block}`;
|
|
13
|
+
}
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
type SubagentResultLine,
|
|
18
18
|
} from './subagent-render.ts';
|
|
19
19
|
import { numberArg, preview, shortenPath, stringArg } from './tool-args.ts';
|
|
20
|
+
import { allowedAgentNames, isPastMaxDepth, type RecursionEnv } from './env-utils.ts';
|
|
20
21
|
|
|
21
22
|
const SubagentParams = {
|
|
22
23
|
type: 'object',
|
|
@@ -36,10 +37,6 @@ type SubagentParamsType = {
|
|
|
36
37
|
};
|
|
37
38
|
|
|
38
39
|
type RegisterablePi = Pick<ExtensionAPI, 'registerTool'>;
|
|
39
|
-
type RecursionEnv = Partial<
|
|
40
|
-
Record<'PI_SUBAGENT_ALLOWED' | 'PI_SUBAGENT_DEPTH' | 'PI_SUBAGENT_MAX_DEPTH', string>
|
|
41
|
-
>;
|
|
42
|
-
|
|
43
40
|
export interface RegisterSubagentToolOptions {
|
|
44
41
|
agents: AgentConfig[];
|
|
45
42
|
run?: typeof runSubagent;
|
|
@@ -70,29 +67,6 @@ function toProgressResult(progress: AgentProgress) {
|
|
|
70
67
|
};
|
|
71
68
|
}
|
|
72
69
|
|
|
73
|
-
function parseEnvNumber(value: string | undefined): number | undefined {
|
|
74
|
-
if (value === undefined) return undefined;
|
|
75
|
-
const parsed = Number(value);
|
|
76
|
-
return Number.isFinite(parsed) ? parsed : undefined;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function allowedAgentNames(env: RecursionEnv): Set<string> | undefined {
|
|
80
|
-
const raw = env?.PI_SUBAGENT_ALLOWED;
|
|
81
|
-
if (!raw) return undefined;
|
|
82
|
-
return new Set(
|
|
83
|
-
raw
|
|
84
|
-
.split(',')
|
|
85
|
-
.map((name) => name.trim())
|
|
86
|
-
.filter(Boolean),
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function isPastMaxDepth(env: RecursionEnv): boolean {
|
|
91
|
-
const depth = parseEnvNumber(env?.PI_SUBAGENT_DEPTH);
|
|
92
|
-
const maxDepth = parseEnvNumber(env?.PI_SUBAGENT_MAX_DEPTH);
|
|
93
|
-
return depth !== undefined && maxDepth !== undefined && depth > maxDepth;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
70
|
type CollapsedTheme = {
|
|
97
71
|
fg: (name: ThemeColor, text: string) => string;
|
|
98
72
|
bold: (text: string) => string;
|
|
@@ -243,11 +217,17 @@ export function registerSubagentTool(
|
|
|
243
217
|
: options.agents;
|
|
244
218
|
const runner = options.run ?? runSubagent;
|
|
245
219
|
|
|
220
|
+
const availableSubagents = agents.map((agent) => agent.name);
|
|
221
|
+
const agentNames = [...availableSubagents].sort().join(', ');
|
|
222
|
+
const promptGuidelines =
|
|
223
|
+
agentNames.length > 0 ? [`Available subagents: ${agentNames}`] : undefined;
|
|
224
|
+
|
|
246
225
|
pi.registerTool({
|
|
247
226
|
name: 'subagent',
|
|
248
227
|
label: 'Subagent',
|
|
249
228
|
description: 'Delegate a task to a named sub-agent running in an isolated pi process.',
|
|
250
229
|
promptSnippet: 'Delegate isolated tasks with subagent({ agent, task, cwd? }).',
|
|
230
|
+
promptGuidelines,
|
|
251
231
|
parameters: SubagentParams,
|
|
252
232
|
|
|
253
233
|
async execute(
|
|
@@ -270,6 +250,7 @@ export function registerSubagentTool(
|
|
|
270
250
|
cwd: params.cwd ?? ctx.cwd,
|
|
271
251
|
signal,
|
|
272
252
|
depth: Number(env.PI_SUBAGENT_DEPTH ?? '0') + 1,
|
|
253
|
+
availableAgents: availableSubagents,
|
|
273
254
|
onProgress: (progress) => onUpdate?.(toProgressResult(progress)),
|
|
274
255
|
});
|
|
275
256
|
|