@johnnywu/pi-subagents 1.2.0 → 1.3.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 +16 -0
- package/README.md +26 -6
- package/extensions/agent-loader.ts +11 -1
- package/extensions/env-utils.ts +25 -1
- package/extensions/index.ts +32 -5
- package/extensions/skill-resolver.ts +104 -22
- package/extensions/subagent-executor.ts +71 -32
- package/extensions/subagent-prompt.ts +74 -0
- package/extensions/subagent-tool.ts +7 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
# [1.3.0](https://github.com/jwu/pi-subagents/compare/v1.2.0...v1.3.0) (2026-06-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* filter available subagents by allowed agents ([adc6025](https://github.com/jwu/pi-subagents/commit/adc60252d96ef532719a0da1c128a8e433c74bb7))
|
|
7
|
+
* preserve append-mode subagent context ([ada0a26](https://github.com/jwu/pi-subagents/commit/ada0a264565d686917b73126301884345f824bce))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* add debug subagent prompt command ([1c49d43](https://github.com/jwu/pi-subagents/commit/1c49d43af9569856407e2a97d512d9d3b617e6d9))
|
|
13
|
+
* default subagent prompts to append ([5693c17](https://github.com/jwu/pi-subagents/commit/5693c17b2aea4244b106bff82fe7ebd5c954322c))
|
|
14
|
+
* export subagent debug prompts from frontmatter ([afe8b23](https://github.com/jwu/pi-subagents/commit/afe8b23700b9a86ef1db97463225cd828f884026))
|
|
15
|
+
* resolve package skills for subagents ([48651e1](https://github.com/jwu/pi-subagents/commit/48651e15010c501cacec9a2e71771a13c0e7c9ab))
|
|
16
|
+
|
|
1
17
|
# [1.2.0](https://github.com/jwu/pi-subagents/compare/v1.1.0...v1.2.0) (2026-06-02)
|
|
2
18
|
|
|
3
19
|
|
package/README.md
CHANGED
|
@@ -50,11 +50,11 @@ Run the code-reviewer agent on the last three commits
|
|
|
50
50
|
|
|
51
51
|
### Available subagents in the prompt
|
|
52
52
|
|
|
53
|
-
The extension exposes discovered sub-agents to the model
|
|
53
|
+
The extension exposes discovered sub-agents to the model when the active tool set includes `subagent`:
|
|
54
54
|
|
|
55
55
|
- The `subagent` tool keeps a one-line prompt guideline:
|
|
56
56
|
`Available subagents: code-reviewer, refactor, test-writer`
|
|
57
|
-
-
|
|
57
|
+
- At agent-start time, the system prompt gets an independent block:
|
|
58
58
|
|
|
59
59
|
```text
|
|
60
60
|
Available subagents:
|
|
@@ -63,7 +63,25 @@ Available subagents:
|
|
|
63
63
|
- test-writer
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
The prompt file passed to the child process contains only the agent prompt plus skills. Runtime prompt assembly then depends on `systemPrompt` mode:
|
|
67
|
+
|
|
68
|
+
- `append`: the child process uses pi's default prompt and project context files, then appends the agent prompt.
|
|
69
|
+
- `replace`: the child process replaces pi's default prompt with the agent prompt and skips project context files; pi-subagents reinjects the active `Available tools` and `Guidelines` blocks at agent-start time so custom replace prompts still expose the selected tool affordances.
|
|
70
|
+
|
|
71
|
+
The `Available subagents` block is injected by the child process at agent-start time, after `PI_SUBAGENT_ALLOWED` and recursion depth filtering are applied.
|
|
72
|
+
|
|
73
|
+
### Debug a sub-agent prompt
|
|
74
|
+
|
|
75
|
+
Set `debug: true` in an agent's frontmatter to export that sub-agent's effective runtime system prompt on each run:
|
|
76
|
+
|
|
77
|
+
```markdown
|
|
78
|
+
---
|
|
79
|
+
name: scout
|
|
80
|
+
debug: true
|
|
81
|
+
---
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
The child process writes `debug-system-prompt.md` in the project cwd. The file contains the prompt visible during `before_agent_start`, including `systemPrompt` append/replace behavior, tools/guidelines, skills, project context files in append mode, and pi-subagents' runtime `Available subagents` block when applicable.
|
|
67
85
|
|
|
68
86
|
## Agent configuration
|
|
69
87
|
|
|
@@ -76,9 +94,10 @@ Agents are Markdown files with YAML frontmatter.
|
|
|
76
94
|
| `tools` | no | _none_ | Comma-separated tool whitelist (`read, write, bash, grep`, etc.) |
|
|
77
95
|
| `model` | no | parent's model | Provider/model-id (`anthropic/claude-sonnet-4-6`) |
|
|
78
96
|
| `thinking` | no | `off` | Reasoning level: `off`, `low`, `medium`, `high` |
|
|
79
|
-
| `systemPrompt` | no | `
|
|
97
|
+
| `systemPrompt` | no | `append` | How the body is applied: `append` (append to pi default system prompt and project context) or `replace` (replace default prompt and skip project context) |
|
|
80
98
|
| `allowedAgents` | no | _all_ | Comma-separated list of sub-agents this agent may spawn |
|
|
81
99
|
| `maxDepth` | no | `10` | Maximum recursion depth (`0` = no sub-agents, `1` = one level, etc.) |
|
|
100
|
+
| `debug` | no | `false` | When `true`, export the effective runtime system prompt to `debug-system-prompt.md` |
|
|
82
101
|
|
|
83
102
|
The Markdown body after the frontmatter is the agent's system prompt.
|
|
84
103
|
|
|
@@ -91,9 +110,10 @@ description: High-level planner that delegates to specialists
|
|
|
91
110
|
tools: subagent, read, grep, find
|
|
92
111
|
model: anthropic/claude-sonnet-4-6
|
|
93
112
|
thinking: high
|
|
94
|
-
systemPrompt:
|
|
113
|
+
systemPrompt: append
|
|
95
114
|
allowedAgents: code-reviewer, refactor, test-writer
|
|
96
115
|
maxDepth: 2
|
|
116
|
+
debug: false
|
|
97
117
|
---
|
|
98
118
|
|
|
99
119
|
You are an orchestrator. Break complex tasks into sub-tasks and delegate
|
|
@@ -121,7 +141,7 @@ Sub-agents can spawn their own sub-agents (if the `subagent` tool is in their wh
|
|
|
121
141
|
|
|
122
142
|
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
143
|
|
|
124
|
-
These are passed via environment variables (`PI_SUBAGENT_DEPTH`, `PI_SUBAGENT_MAX_DEPTH`, `PI_SUBAGENT_ALLOWED`).
|
|
144
|
+
These are passed via environment variables (`PI_SUBAGENT_DEPTH`, `PI_SUBAGENT_MAX_DEPTH`, `PI_SUBAGENT_ALLOWED`). Child processes also receive `PI_SUBAGENT_NAME` and `PI_SUBAGENT_SYSTEM_PROMPT_MODE` so runtime hooks can distinguish append vs. replace behavior.
|
|
125
145
|
|
|
126
146
|
## Session storage
|
|
127
147
|
|
|
@@ -15,6 +15,7 @@ export interface AgentConfig {
|
|
|
15
15
|
systemPromptMode: SystemPromptMode;
|
|
16
16
|
allowedAgents?: string[];
|
|
17
17
|
maxDepth: number;
|
|
18
|
+
debug: boolean;
|
|
18
19
|
skills?: string[];
|
|
19
20
|
prompt: string;
|
|
20
21
|
source: AgentSource;
|
|
@@ -107,7 +108,7 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
|
|
|
107
108
|
throw new Error(`invalid thinking: ${data.thinking}`);
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
const systemPromptMode = (data.systemPrompt ?? '
|
|
111
|
+
const systemPromptMode = (data.systemPrompt ?? 'append') as SystemPromptMode;
|
|
111
112
|
if (!['replace', 'append'].includes(systemPromptMode)) {
|
|
112
113
|
throw new Error(`invalid systemPrompt: ${data.systemPrompt}`);
|
|
113
114
|
}
|
|
@@ -117,6 +118,14 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
|
|
|
117
118
|
throw new Error(`invalid maxDepth: ${data.maxDepth}`);
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
let debug = false;
|
|
122
|
+
if (data.debug !== undefined) {
|
|
123
|
+
if (data.debug !== 'true' && data.debug !== 'false') {
|
|
124
|
+
throw new Error(`invalid debug: ${data.debug}`);
|
|
125
|
+
}
|
|
126
|
+
debug = data.debug === 'true';
|
|
127
|
+
}
|
|
128
|
+
|
|
120
129
|
const allowedAgents = splitCsv(data.allowedAgents);
|
|
121
130
|
const skills = splitCsv(data.skills);
|
|
122
131
|
|
|
@@ -130,6 +139,7 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
|
|
|
130
139
|
systemPromptMode,
|
|
131
140
|
allowedAgents: allowedAgents.length > 0 ? allowedAgents : undefined,
|
|
132
141
|
maxDepth,
|
|
142
|
+
debug,
|
|
133
143
|
prompt: body,
|
|
134
144
|
source,
|
|
135
145
|
filePath,
|
package/extensions/env-utils.ts
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
|
+
export type SystemPromptModeEnv = 'replace' | 'append';
|
|
2
|
+
|
|
1
3
|
export type RecursionEnv = Partial<
|
|
2
|
-
Record<
|
|
4
|
+
Record<
|
|
5
|
+
| 'PI_SUBAGENT_ALLOWED'
|
|
6
|
+
| 'PI_SUBAGENT_DEPTH'
|
|
7
|
+
| 'PI_SUBAGENT_MAX_DEPTH'
|
|
8
|
+
| 'PI_SUBAGENT_NAME'
|
|
9
|
+
| 'PI_SUBAGENT_SYSTEM_PROMPT_MODE',
|
|
10
|
+
string
|
|
11
|
+
>
|
|
3
12
|
>;
|
|
4
13
|
|
|
5
14
|
export function parseEnvNumber(value: string | undefined): number | undefined {
|
|
@@ -24,3 +33,18 @@ export function isPastMaxDepth(env: RecursionEnv): boolean {
|
|
|
24
33
|
const maxDepth = parseEnvNumber(env?.PI_SUBAGENT_MAX_DEPTH);
|
|
25
34
|
return depth !== undefined && maxDepth !== undefined && depth > maxDepth;
|
|
26
35
|
}
|
|
36
|
+
|
|
37
|
+
export function isSubagentProcess(env: RecursionEnv): boolean {
|
|
38
|
+
const depth = parseEnvNumber(env?.PI_SUBAGENT_DEPTH);
|
|
39
|
+
return depth !== undefined && depth > 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function subagentSystemPromptMode(env: RecursionEnv): SystemPromptModeEnv | undefined {
|
|
43
|
+
const mode = env?.PI_SUBAGENT_SYSTEM_PROMPT_MODE;
|
|
44
|
+
if (mode === 'replace' || mode === 'append') return mode;
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function isSubagentReplaceSystemPrompt(env: RecursionEnv): boolean {
|
|
49
|
+
return isSubagentProcess(env) && subagentSystemPromptMode(env) === 'replace';
|
|
50
|
+
}
|
package/extensions/index.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as path from 'node:path';
|
|
2
4
|
import { loadAgentDefinitions } from './agent-loader.ts';
|
|
3
|
-
import { allowedAgentNames, isPastMaxDepth } from './env-utils.ts';
|
|
4
|
-
import {
|
|
5
|
+
import { allowedAgentNames, isPastMaxDepth, isSubagentReplaceSystemPrompt } from './env-utils.ts';
|
|
6
|
+
import {
|
|
7
|
+
appendAvailableSubagentsBlock,
|
|
8
|
+
appendAvailableToolsAndGuidelinesBlock,
|
|
9
|
+
} from './subagent-prompt.ts';
|
|
5
10
|
import { registerSubagentTool } from './subagent-tool.ts';
|
|
6
11
|
|
|
7
12
|
export default async function (pi: ExtensionAPI) {
|
|
@@ -17,10 +22,32 @@ export default async function (pi: ExtensionAPI) {
|
|
|
17
22
|
: result.agents;
|
|
18
23
|
const agentNames = agents.map((agent) => agent.name);
|
|
19
24
|
|
|
25
|
+
if (isSubagentReplaceSystemPrompt(process.env)) {
|
|
26
|
+
pi.on('before_agent_start', (event) => {
|
|
27
|
+
return {
|
|
28
|
+
systemPrompt: appendAvailableToolsAndGuidelinesBlock(
|
|
29
|
+
event.systemPrompt,
|
|
30
|
+
event.systemPromptOptions,
|
|
31
|
+
),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
20
36
|
if (!isPastMaxDepth(process.env) && agentNames.length > 0) {
|
|
21
|
-
pi.on('before_agent_start', (event) =>
|
|
22
|
-
|
|
23
|
-
|
|
37
|
+
pi.on('before_agent_start', (event) => {
|
|
38
|
+
if (!event.systemPromptOptions.selectedTools?.includes('subagent')) return;
|
|
39
|
+
return {
|
|
40
|
+
systemPrompt: appendAvailableSubagentsBlock(event.systemPrompt, agentNames),
|
|
41
|
+
};
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (process.env.PI_SUBAGENT_DEBUG === 'true') {
|
|
46
|
+
pi.on('before_agent_start', (_event, ctx) => {
|
|
47
|
+
const prompt = ctx.getSystemPrompt();
|
|
48
|
+
const outputPath = path.join(ctx.cwd, 'debug-system-prompt.md');
|
|
49
|
+
fs.writeFileSync(outputPath, prompt, 'utf-8');
|
|
50
|
+
});
|
|
24
51
|
}
|
|
25
52
|
|
|
26
53
|
registerSubagentTool(pi, { agents: result.agents });
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultPackageManager,
|
|
3
|
+
SettingsManager,
|
|
4
|
+
getAgentDir,
|
|
5
|
+
type ResolvedResource,
|
|
6
|
+
} from '@earendil-works/pi-coding-agent';
|
|
1
7
|
import * as fs from 'node:fs';
|
|
2
8
|
import * as os from 'node:os';
|
|
3
9
|
import * as path from 'node:path';
|
|
@@ -15,8 +21,16 @@ export interface SkillResolverFs {
|
|
|
15
21
|
|
|
16
22
|
export interface ResolveSkillsOptions {
|
|
17
23
|
cwd: string;
|
|
24
|
+
agentDir?: string;
|
|
18
25
|
globalDir?: string;
|
|
19
26
|
fs?: SkillResolverFs;
|
|
27
|
+
packageSkillFiles?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ResolveSkillsResult {
|
|
31
|
+
resolved: ResolvedSkill[];
|
|
32
|
+
missing: string[];
|
|
33
|
+
skippedPackages: string[];
|
|
20
34
|
}
|
|
21
35
|
|
|
22
36
|
const defaultSkillFs: SkillResolverFs = {
|
|
@@ -79,7 +93,7 @@ function parseSkillFrontmatter(content: string): { name: string; description: st
|
|
|
79
93
|
return { name: data.name || '', description: data.description.trim() };
|
|
80
94
|
}
|
|
81
95
|
|
|
82
|
-
function
|
|
96
|
+
function findLocalSkillFile(
|
|
83
97
|
skillName: string,
|
|
84
98
|
cwd: string,
|
|
85
99
|
globalDir: string,
|
|
@@ -104,43 +118,111 @@ function findSkillFile(
|
|
|
104
118
|
return undefined;
|
|
105
119
|
}
|
|
106
120
|
|
|
107
|
-
|
|
121
|
+
async function resolvePackageSkillFiles(options: ResolveSkillsOptions): Promise<{
|
|
122
|
+
files: string[];
|
|
123
|
+
skippedPackages: string[];
|
|
124
|
+
}> {
|
|
125
|
+
if (options.packageSkillFiles) {
|
|
126
|
+
return { files: options.packageSkillFiles, skippedPackages: [] };
|
|
127
|
+
}
|
|
128
|
+
if (options.fs) {
|
|
129
|
+
return { files: [], skippedPackages: [] };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const skippedPackages: string[] = [];
|
|
133
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
134
|
+
const settingsManager = SettingsManager.create(options.cwd, agentDir);
|
|
135
|
+
const packageManager = new DefaultPackageManager({
|
|
136
|
+
cwd: options.cwd,
|
|
137
|
+
agentDir,
|
|
138
|
+
settingsManager,
|
|
139
|
+
});
|
|
140
|
+
const resolvedPaths = await packageManager.resolve(async (source) => {
|
|
141
|
+
skippedPackages.push(source);
|
|
142
|
+
return 'skip';
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
files: resolvedPaths.skills
|
|
147
|
+
.filter((resource: ResolvedResource) => resource.enabled)
|
|
148
|
+
.map((resource: ResolvedResource) => resource.path),
|
|
149
|
+
skippedPackages,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function readSkill(
|
|
154
|
+
filePath: string,
|
|
155
|
+
requestedName: string,
|
|
156
|
+
fileSystem: SkillResolverFs,
|
|
157
|
+
requireNameMatch: boolean,
|
|
158
|
+
): ResolvedSkill | undefined {
|
|
159
|
+
const content = fileSystem.readFile(filePath);
|
|
160
|
+
const frontmatter = parseSkillFrontmatter(content);
|
|
161
|
+
if (!frontmatter) return undefined;
|
|
162
|
+
|
|
163
|
+
const fileSkillName = frontmatter.name || path.basename(path.dirname(filePath));
|
|
164
|
+
const markdownName = path.basename(filePath, '.md');
|
|
165
|
+
if (requireNameMatch && fileSkillName !== requestedName && markdownName !== requestedName) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return {
|
|
170
|
+
name: fileSkillName || requestedName,
|
|
171
|
+
description: frontmatter.description,
|
|
172
|
+
location: filePath,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export async function resolveSkills(
|
|
108
177
|
skillNames: string[],
|
|
109
178
|
options: ResolveSkillsOptions,
|
|
110
|
-
):
|
|
179
|
+
): Promise<ResolveSkillsResult> {
|
|
111
180
|
const cwd = options.cwd;
|
|
112
181
|
const globalDir = options.globalDir ?? defaultGlobalSkillsDir();
|
|
113
182
|
const fileSystem = options.fs ?? defaultSkillFs;
|
|
114
183
|
const resolved: ResolvedSkill[] = [];
|
|
115
184
|
const missing: string[] = [];
|
|
185
|
+
const pendingPackageResolution: string[] = [];
|
|
116
186
|
|
|
117
187
|
for (const name of skillNames) {
|
|
118
188
|
const trimmed = name.trim();
|
|
119
189
|
if (!trimmed) continue;
|
|
120
190
|
|
|
121
|
-
const
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
191
|
+
const localFilePath = findLocalSkillFile(trimmed, cwd, globalDir, fileSystem);
|
|
192
|
+
if (localFilePath) {
|
|
193
|
+
try {
|
|
194
|
+
const skill = readSkill(localFilePath, trimmed, fileSystem, false);
|
|
195
|
+
if (skill) {
|
|
196
|
+
resolved.push(skill);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
// Try package skills before marking the skill as missing.
|
|
201
|
+
}
|
|
125
202
|
}
|
|
126
203
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const frontmatter = parseSkillFrontmatter(content);
|
|
130
|
-
if (!frontmatter) {
|
|
131
|
-
missing.push(trimmed);
|
|
132
|
-
continue;
|
|
133
|
-
}
|
|
204
|
+
pendingPackageResolution.push(trimmed);
|
|
205
|
+
}
|
|
134
206
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
207
|
+
const packageSkills =
|
|
208
|
+
pendingPackageResolution.length > 0
|
|
209
|
+
? await resolvePackageSkillFiles(options)
|
|
210
|
+
: { files: [], skippedPackages: [] };
|
|
211
|
+
|
|
212
|
+
for (const trimmed of pendingPackageResolution) {
|
|
213
|
+
let packageSkill: ResolvedSkill | undefined;
|
|
214
|
+
for (const filePath of packageSkills.files) {
|
|
215
|
+
try {
|
|
216
|
+
packageSkill = readSkill(filePath, trimmed, fileSystem, true);
|
|
217
|
+
if (packageSkill) break;
|
|
218
|
+
} catch {
|
|
219
|
+
// Try the next package skill file.
|
|
220
|
+
}
|
|
142
221
|
}
|
|
222
|
+
|
|
223
|
+
if (packageSkill) resolved.push(packageSkill);
|
|
224
|
+
else missing.push(trimmed);
|
|
143
225
|
}
|
|
144
226
|
|
|
145
|
-
return { resolved, missing };
|
|
227
|
+
return { resolved, missing, skippedPackages: packageSkills.skippedPackages };
|
|
146
228
|
}
|
|
@@ -5,7 +5,6 @@ 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
8
|
import { resolveSkills } from './skill-resolver.ts';
|
|
10
9
|
|
|
11
10
|
export interface AgentUsage {
|
|
@@ -89,10 +88,38 @@ export interface RunSubagentOptions {
|
|
|
89
88
|
now?: () => number;
|
|
90
89
|
}
|
|
91
90
|
|
|
91
|
+
export interface BuildSubagentSystemPromptOptions {
|
|
92
|
+
agent: AgentConfig;
|
|
93
|
+
cwd: string;
|
|
94
|
+
agentDir?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface BuildSubagentSystemPromptResult {
|
|
98
|
+
prompt: string;
|
|
99
|
+
missingSkills: string[];
|
|
100
|
+
skippedSkillPackages: string[];
|
|
101
|
+
}
|
|
102
|
+
|
|
92
103
|
const TASK_FILE_THRESHOLD = 8000;
|
|
93
104
|
const OUTPUT_MAX_BYTES = 50 * 1024;
|
|
94
105
|
const OUTPUT_MAX_LINES = 2000;
|
|
95
106
|
|
|
107
|
+
export function availableSubagentsForAgent(
|
|
108
|
+
agent: AgentConfig,
|
|
109
|
+
candidateNames?: string[],
|
|
110
|
+
): string[] {
|
|
111
|
+
const names = candidateNames ?? agent.allowedAgents ?? [];
|
|
112
|
+
const allowed = agent.allowedAgents ? new Set(agent.allowedAgents) : undefined;
|
|
113
|
+
const seen = new Set<string>();
|
|
114
|
+
|
|
115
|
+
return names.filter((name) => {
|
|
116
|
+
if (seen.has(name)) return false;
|
|
117
|
+
if (allowed && !allowed.has(name)) return false;
|
|
118
|
+
seen.add(name);
|
|
119
|
+
return true;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
96
123
|
const defaultFs: ExecutorFs = {
|
|
97
124
|
makeTempDir(prefix) {
|
|
98
125
|
return fs.mkdtemp(prefix);
|
|
@@ -333,6 +360,31 @@ function safeFilePart(value: string): string {
|
|
|
333
360
|
return value.replace(/[^a-zA-Z0-9_.-]+/g, '_');
|
|
334
361
|
}
|
|
335
362
|
|
|
363
|
+
export async function buildSubagentSystemPrompt(
|
|
364
|
+
options: BuildSubagentSystemPromptOptions,
|
|
365
|
+
): Promise<BuildSubagentSystemPromptResult> {
|
|
366
|
+
let prompt = options.agent.prompt;
|
|
367
|
+
|
|
368
|
+
const missingSkills: string[] = [];
|
|
369
|
+
const skippedSkillPackages: string[] = [];
|
|
370
|
+
const skillNames = options.agent.skills;
|
|
371
|
+
if (skillNames && skillNames.length > 0) {
|
|
372
|
+
const resolvedSkills = await resolveSkills(skillNames, {
|
|
373
|
+
cwd: options.cwd,
|
|
374
|
+
agentDir: options.agentDir,
|
|
375
|
+
});
|
|
376
|
+
missingSkills.push(...resolvedSkills.missing);
|
|
377
|
+
skippedSkillPackages.push(...resolvedSkills.skippedPackages);
|
|
378
|
+
|
|
379
|
+
const skillInjection = formatSkillsForPrompt(resolvedSkills.resolved);
|
|
380
|
+
if (skillInjection) {
|
|
381
|
+
prompt = `${prompt}${skillInjection}`;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return { prompt, missingSkills, skippedSkillPackages };
|
|
386
|
+
}
|
|
387
|
+
|
|
336
388
|
function progressFromPartialResult(partialResult: unknown): AgentProgress | undefined {
|
|
337
389
|
if (!partialResult || typeof partialResult !== 'object') return undefined;
|
|
338
390
|
const details = (partialResult as { details?: unknown }).details;
|
|
@@ -377,28 +429,18 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
377
429
|
try {
|
|
378
430
|
const promptFilePath = path.join(tempDir, 'system-prompt.md');
|
|
379
431
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
432
|
+
const promptResult = await buildSubagentSystemPrompt({
|
|
433
|
+
agent: options.agent,
|
|
434
|
+
cwd: options.cwd,
|
|
435
|
+
agentDir: options.agentDir,
|
|
436
|
+
});
|
|
437
|
+
for (const source of promptResult.skippedSkillPackages) {
|
|
438
|
+
console.warn(`[pi-subagents] package not installed, skipping skills: ${source}`);
|
|
388
439
|
}
|
|
389
|
-
|
|
390
|
-
|
|
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
|
-
}
|
|
440
|
+
for (const name of promptResult.missingSkills) {
|
|
441
|
+
console.warn(`[pi-subagents] skill not found: ${name}`);
|
|
400
442
|
}
|
|
401
|
-
await fileSystem.writeFile(promptFilePath,
|
|
443
|
+
await fileSystem.writeFile(promptFilePath, promptResult.prompt);
|
|
402
444
|
|
|
403
445
|
let taskFilePath: string | undefined;
|
|
404
446
|
if (options.task.length > TASK_FILE_THRESHOLD) {
|
|
@@ -411,16 +453,9 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
411
453
|
AuthStorage.create(options.agentDir ? path.join(options.agentDir, 'auth.json') : undefined),
|
|
412
454
|
options.agentDir ? path.join(options.agentDir, 'models.json') : undefined,
|
|
413
455
|
);
|
|
414
|
-
const args = [
|
|
415
|
-
pi.entryPoint,
|
|
416
|
-
'--mode',
|
|
417
|
-
'json',
|
|
418
|
-
'-p',
|
|
419
|
-
'--no-skills',
|
|
420
|
-
'--no-prompt-templates',
|
|
421
|
-
'--no-context-files',
|
|
422
|
-
];
|
|
456
|
+
const args = [pi.entryPoint, '--mode', 'json', '-p', '--no-skills', '--no-prompt-templates'];
|
|
423
457
|
|
|
458
|
+
if (options.agent.systemPromptMode === 'replace') args.push('--no-context-files');
|
|
424
459
|
if (options.agent.model) args.push('--model', options.agent.model);
|
|
425
460
|
args.push('--thinking', options.agent.thinking);
|
|
426
461
|
if (options.agent.tools.length > 0) args.push('--tools', options.agent.tools.join(','));
|
|
@@ -435,10 +470,14 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
435
470
|
...process.env,
|
|
436
471
|
PI_SUBAGENT_DEPTH: String(options.depth ?? 1),
|
|
437
472
|
PI_SUBAGENT_MAX_DEPTH: String(options.agent.maxDepth),
|
|
473
|
+
PI_SUBAGENT_NAME: options.agent.name,
|
|
474
|
+
PI_SUBAGENT_SYSTEM_PROMPT_MODE: options.agent.systemPromptMode,
|
|
438
475
|
};
|
|
439
|
-
|
|
440
|
-
|
|
476
|
+
const visibleAgents = availableSubagentsForAgent(options.agent, options.availableAgents);
|
|
477
|
+
if (visibleAgents.length > 0) {
|
|
478
|
+
env.PI_SUBAGENT_ALLOWED = visibleAgents.join(',');
|
|
441
479
|
}
|
|
480
|
+
env.PI_SUBAGENT_DEBUG = options.agent.debug ? 'true' : 'false';
|
|
442
481
|
|
|
443
482
|
const processLine = (line: string) => {
|
|
444
483
|
if (!line.trim()) return;
|
|
@@ -1,3 +1,77 @@
|
|
|
1
|
+
export interface ToolGuidelinePromptOptions {
|
|
2
|
+
selectedTools?: string[];
|
|
3
|
+
toolSnippets?: Record<string, string>;
|
|
4
|
+
promptGuidelines?: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function uniqueNonEmpty(values: string[]): string[] {
|
|
8
|
+
const seen = new Set<string>();
|
|
9
|
+
const result: string[] = [];
|
|
10
|
+
for (const value of values) {
|
|
11
|
+
const normalized = value.trim();
|
|
12
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
13
|
+
seen.add(normalized);
|
|
14
|
+
result.push(normalized);
|
|
15
|
+
}
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function formatAvailableToolsAndGuidelinesBlock(
|
|
20
|
+
options: ToolGuidelinePromptOptions,
|
|
21
|
+
): string | undefined {
|
|
22
|
+
const selectedTools = options.selectedTools ?? ['read', 'bash', 'edit', 'write'];
|
|
23
|
+
const visibleTools = selectedTools.filter((name) => options.toolSnippets?.[name]);
|
|
24
|
+
const toolsList =
|
|
25
|
+
visibleTools.length > 0
|
|
26
|
+
? visibleTools.map((name) => `- ${name}: ${options.toolSnippets![name]}`).join('\n')
|
|
27
|
+
: '(none)';
|
|
28
|
+
|
|
29
|
+
const hasBashOnlyForFileExploration =
|
|
30
|
+
selectedTools.includes('bash') &&
|
|
31
|
+
!selectedTools.includes('grep') &&
|
|
32
|
+
!selectedTools.includes('find') &&
|
|
33
|
+
!selectedTools.includes('ls');
|
|
34
|
+
const guidelines = uniqueNonEmpty([
|
|
35
|
+
...(hasBashOnlyForFileExploration ? ['Use bash for file operations like ls, rg, find'] : []),
|
|
36
|
+
...(options.promptGuidelines ?? []),
|
|
37
|
+
'Be concise in your responses',
|
|
38
|
+
'Show file paths clearly when working with files',
|
|
39
|
+
]);
|
|
40
|
+
const guidelineLines = guidelines.map((guideline) => `- ${guideline}`).join('\n');
|
|
41
|
+
|
|
42
|
+
return [
|
|
43
|
+
'Available tools:',
|
|
44
|
+
toolsList,
|
|
45
|
+
'',
|
|
46
|
+
'In addition to the tools above, you may have access to other custom tools depending on the project.',
|
|
47
|
+
'',
|
|
48
|
+
'Guidelines:',
|
|
49
|
+
guidelineLines || '(none)',
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function appendBeforeTrailingRuntimeMetadata(systemPrompt: string, block: string): string {
|
|
54
|
+
const marker = '\nCurrent date:';
|
|
55
|
+
const index = systemPrompt.lastIndexOf(marker);
|
|
56
|
+
if (index === -1) return `${systemPrompt.trimEnd()}\n\n${block}`;
|
|
57
|
+
|
|
58
|
+
const before = systemPrompt.slice(0, index).trimEnd();
|
|
59
|
+
const after = systemPrompt.slice(index);
|
|
60
|
+
return `${before}\n\n${block}${after}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function appendAvailableToolsAndGuidelinesBlock(
|
|
64
|
+
systemPrompt: string,
|
|
65
|
+
options: ToolGuidelinePromptOptions,
|
|
66
|
+
): string {
|
|
67
|
+
const block = formatAvailableToolsAndGuidelinesBlock(options);
|
|
68
|
+
if (!block || systemPrompt.includes('Available tools:') || systemPrompt.includes('Guidelines:')) {
|
|
69
|
+
return systemPrompt;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return appendBeforeTrailingRuntimeMetadata(systemPrompt, block);
|
|
73
|
+
}
|
|
74
|
+
|
|
1
75
|
export function formatAvailableSubagentsBlock(agentNames: string[]): string | undefined {
|
|
2
76
|
const names = [...new Set(agentNames.map((name) => name.trim()).filter(Boolean))].sort();
|
|
3
77
|
if (names.length === 0) return undefined;
|
|
@@ -8,7 +8,12 @@ import {
|
|
|
8
8
|
} from '@earendil-works/pi-coding-agent';
|
|
9
9
|
import { Container, Markdown, Spacer, Text } from '@earendil-works/pi-tui';
|
|
10
10
|
import type { AgentConfig } from './agent-loader.ts';
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
availableSubagentsForAgent,
|
|
13
|
+
type AgentProgress,
|
|
14
|
+
type AgentResult,
|
|
15
|
+
runSubagent,
|
|
16
|
+
} from './subagent-executor.ts';
|
|
12
17
|
import {
|
|
13
18
|
contextUsageSeverity,
|
|
14
19
|
formatSubagentCall,
|
|
@@ -250,7 +255,7 @@ export function registerSubagentTool(
|
|
|
250
255
|
cwd: params.cwd ?? ctx.cwd,
|
|
251
256
|
signal,
|
|
252
257
|
depth: Number(env.PI_SUBAGENT_DEPTH ?? '0') + 1,
|
|
253
|
-
availableAgents: availableSubagents,
|
|
258
|
+
availableAgents: availableSubagentsForAgent(agent, availableSubagents),
|
|
254
259
|
onProgress: (progress) => onUpdate?.(toProgressResult(progress)),
|
|
255
260
|
});
|
|
256
261
|
|