@johnnywu/pi-subagents 1.2.0 → 1.4.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 +23 -0
- package/README.md +30 -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 +202 -48
- package/extensions/subagent-executor.ts +77 -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,26 @@
|
|
|
1
|
+
# [1.4.0](https://github.com/jwu/pi-subagents/compare/v1.3.0...v1.4.0) (2026-06-02)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* support wildcard skills ([962eaa4](https://github.com/jwu/pi-subagents/commit/962eaa486cd73342629cfecf4e7bcc8a1ceb4710))
|
|
7
|
+
|
|
8
|
+
# [1.3.0](https://github.com/jwu/pi-subagents/compare/v1.2.0...v1.3.0) (2026-06-02)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* filter available subagents by allowed agents ([adc6025](https://github.com/jwu/pi-subagents/commit/adc60252d96ef532719a0da1c128a8e433c74bb7))
|
|
14
|
+
* preserve append-mode subagent context ([ada0a26](https://github.com/jwu/pi-subagents/commit/ada0a264565d686917b73126301884345f824bce))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
### Features
|
|
18
|
+
|
|
19
|
+
* add debug subagent prompt command ([1c49d43](https://github.com/jwu/pi-subagents/commit/1c49d43af9569856407e2a97d512d9d3b617e6d9))
|
|
20
|
+
* default subagent prompts to append ([5693c17](https://github.com/jwu/pi-subagents/commit/5693c17b2aea4244b106bff82fe7ebd5c954322c))
|
|
21
|
+
* export subagent debug prompts from frontmatter ([afe8b23](https://github.com/jwu/pi-subagents/commit/afe8b23700b9a86ef1db97463225cd828f884026))
|
|
22
|
+
* resolve package skills for subagents ([48651e1](https://github.com/jwu/pi-subagents/commit/48651e15010c501cacec9a2e71771a13c0e7c9ab))
|
|
23
|
+
|
|
1
24
|
# [1.2.0](https://github.com/jwu/pi-subagents/compare/v1.1.0...v1.2.0) (2026-06-02)
|
|
2
25
|
|
|
3
26
|
|
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,12 +94,16 @@ 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) |
|
|
98
|
+
| `skills` | no | _none_ | Comma-separated skill names or simple wildcard patterns (`*`, `obsidian-*`) to load (resolved from project `.agents/skills/`, `.pi/skills/`, global `~/.pi/agent/skills/`, or npm packages) |
|
|
80
99
|
| `allowedAgents` | no | _all_ | Comma-separated list of sub-agents this agent may spawn |
|
|
81
100
|
| `maxDepth` | no | `10` | Maximum recursion depth (`0` = no sub-agents, `1` = one level, etc.) |
|
|
101
|
+
| `debug` | no | `false` | When `true`, export the effective runtime system prompt to `debug-system-prompt.md` |
|
|
82
102
|
|
|
83
103
|
The Markdown body after the frontmatter is the agent's system prompt.
|
|
84
104
|
|
|
105
|
+
`skills` entries match the skill frontmatter `name`. Use `skills: *` to load all available skills, or prefix-style patterns such as `skills: obsidian-*` to load matching skills. Only `*` is supported as a wildcard; glob features like `?` or `{a,b}` are not supported.
|
|
106
|
+
|
|
85
107
|
### Example with all fields
|
|
86
108
|
|
|
87
109
|
```markdown
|
|
@@ -91,9 +113,11 @@ description: High-level planner that delegates to specialists
|
|
|
91
113
|
tools: subagent, read, grep, find
|
|
92
114
|
model: anthropic/claude-sonnet-4-6
|
|
93
115
|
thinking: high
|
|
94
|
-
systemPrompt:
|
|
116
|
+
systemPrompt: append
|
|
117
|
+
skills: tdd, obsidian-*
|
|
95
118
|
allowedAgents: code-reviewer, refactor, test-writer
|
|
96
119
|
maxDepth: 2
|
|
120
|
+
debug: false
|
|
97
121
|
---
|
|
98
122
|
|
|
99
123
|
You are an orchestrator. Break complex tasks into sub-tasks and delegate
|
|
@@ -121,7 +145,7 @@ Sub-agents can spawn their own sub-agents (if the `subagent` tool is in their wh
|
|
|
121
145
|
|
|
122
146
|
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
147
|
|
|
124
|
-
These are passed via environment variables (`PI_SUBAGENT_DEPTH`, `PI_SUBAGENT_MAX_DEPTH`, `PI_SUBAGENT_ALLOWED`).
|
|
148
|
+
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
149
|
|
|
126
150
|
## Session storage
|
|
127
151
|
|
|
@@ -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';
|
|
@@ -11,12 +17,22 @@ export interface ResolvedSkill {
|
|
|
11
17
|
export interface SkillResolverFs {
|
|
12
18
|
exists(filePath: string): boolean;
|
|
13
19
|
readFile(filePath: string): string;
|
|
20
|
+
listFiles?(dir: string): string[];
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export interface ResolveSkillsOptions {
|
|
17
24
|
cwd: string;
|
|
25
|
+
agentDir?: string;
|
|
18
26
|
globalDir?: string;
|
|
19
27
|
fs?: SkillResolverFs;
|
|
28
|
+
packageSkillFiles?: string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResolveSkillsResult {
|
|
32
|
+
resolved: ResolvedSkill[];
|
|
33
|
+
missing: string[];
|
|
34
|
+
skippedPackages: string[];
|
|
35
|
+
warnings: string[];
|
|
20
36
|
}
|
|
21
37
|
|
|
22
38
|
const defaultSkillFs: SkillResolverFs = {
|
|
@@ -26,6 +42,16 @@ const defaultSkillFs: SkillResolverFs = {
|
|
|
26
42
|
readFile(filePath) {
|
|
27
43
|
return fs.readFileSync(filePath, 'utf-8');
|
|
28
44
|
},
|
|
45
|
+
listFiles(dir) {
|
|
46
|
+
try {
|
|
47
|
+
return fs
|
|
48
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
49
|
+
.map((entry) => path.join(dir, entry.name))
|
|
50
|
+
.sort();
|
|
51
|
+
} catch {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
},
|
|
29
55
|
};
|
|
30
56
|
|
|
31
57
|
function defaultGlobalSkillsDir(): string {
|
|
@@ -75,72 +101,200 @@ function parseSkillFrontmatter(content: string): { name: string; description: st
|
|
|
75
101
|
}
|
|
76
102
|
delete data._currentBlockKey;
|
|
77
103
|
|
|
78
|
-
|
|
79
|
-
return { name: data.name || '', description: data.description.trim() };
|
|
104
|
+
return { name: data.name || '', description: (data.description ?? '').trim() };
|
|
80
105
|
}
|
|
81
106
|
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
107
|
+
async function resolvePackageSkillFiles(options: ResolveSkillsOptions): Promise<{
|
|
108
|
+
files: string[];
|
|
109
|
+
skippedPackages: string[];
|
|
110
|
+
}> {
|
|
111
|
+
if (options.packageSkillFiles) {
|
|
112
|
+
return { files: options.packageSkillFiles, skippedPackages: [] };
|
|
113
|
+
}
|
|
114
|
+
if (options.fs) {
|
|
115
|
+
return { files: [], skippedPackages: [] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const skippedPackages: string[] = [];
|
|
119
|
+
const agentDir = options.agentDir ?? getAgentDir();
|
|
120
|
+
const settingsManager = SettingsManager.create(options.cwd, agentDir);
|
|
121
|
+
const packageManager = new DefaultPackageManager({
|
|
122
|
+
cwd: options.cwd,
|
|
123
|
+
agentDir,
|
|
124
|
+
settingsManager,
|
|
125
|
+
});
|
|
126
|
+
const resolvedPaths = await packageManager.resolve(async (source) => {
|
|
127
|
+
skippedPackages.push(source);
|
|
128
|
+
return 'skip';
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
files: resolvedPaths.skills
|
|
133
|
+
.filter((resource: ResolvedResource) => resource.enabled)
|
|
134
|
+
.map((resource: ResolvedResource) => resource.path),
|
|
135
|
+
skippedPackages,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
interface ReadSkillResult {
|
|
140
|
+
skill?: ResolvedSkill;
|
|
141
|
+
warning?: string;
|
|
142
|
+
}
|
|
91
143
|
|
|
92
|
-
|
|
93
|
-
const
|
|
94
|
-
|
|
144
|
+
function readSkill(filePath: string, fileSystem: SkillResolverFs): ReadSkillResult | undefined {
|
|
145
|
+
const content = fileSystem.readFile(filePath);
|
|
146
|
+
const frontmatter = parseSkillFrontmatter(content);
|
|
147
|
+
if (!frontmatter) return undefined;
|
|
95
148
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
149
|
+
const fileSkillName = frontmatter.name.trim();
|
|
150
|
+
if (!fileSkillName) {
|
|
151
|
+
return { warning: `skill missing required field: name: ${filePath}` };
|
|
152
|
+
}
|
|
153
|
+
if (!frontmatter.description) return undefined;
|
|
99
154
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
155
|
+
return {
|
|
156
|
+
skill: {
|
|
157
|
+
name: fileSkillName,
|
|
158
|
+
description: frontmatter.description,
|
|
159
|
+
location: filePath,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
}
|
|
103
163
|
|
|
104
|
-
|
|
164
|
+
interface CollectedSkills {
|
|
165
|
+
byName: Map<string, ResolvedSkill>;
|
|
166
|
+
warnings: string[];
|
|
167
|
+
skippedPackages: string[];
|
|
105
168
|
}
|
|
106
169
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
)
|
|
170
|
+
const collectedSkillsCache = new Map<string, Promise<CollectedSkills>>();
|
|
171
|
+
|
|
172
|
+
function listSkillFilesInDir(dir: string, fileSystem: SkillResolverFs): string[] {
|
|
173
|
+
if (!fileSystem.listFiles) return [];
|
|
174
|
+
|
|
175
|
+
return fileSystem
|
|
176
|
+
.listFiles(dir)
|
|
177
|
+
.map((entry) => path.join(entry, 'SKILL.md'))
|
|
178
|
+
.filter((filePath) => fileSystem.exists(filePath));
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function addSkillFile(
|
|
182
|
+
filePath: string,
|
|
183
|
+
fileSystem: SkillResolverFs,
|
|
184
|
+
byName: Map<string, ResolvedSkill>,
|
|
185
|
+
warnings: string[],
|
|
186
|
+
): void {
|
|
187
|
+
try {
|
|
188
|
+
const result = readSkill(filePath, fileSystem);
|
|
189
|
+
if (!result) return;
|
|
190
|
+
if (result.warning) warnings.push(result.warning);
|
|
191
|
+
if (result.skill && !byName.has(result.skill.name)) byName.set(result.skill.name, result.skill);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
warnings.push(
|
|
194
|
+
`skill could not be read: ${filePath}: ${error instanceof Error ? error.message : String(error)}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function collectSkills(options: ResolveSkillsOptions): Promise<CollectedSkills> {
|
|
111
200
|
const cwd = options.cwd;
|
|
112
201
|
const globalDir = options.globalDir ?? defaultGlobalSkillsDir();
|
|
113
202
|
const fileSystem = options.fs ?? defaultSkillFs;
|
|
203
|
+
const byName = new Map<string, ResolvedSkill>();
|
|
204
|
+
const warnings: string[] = [];
|
|
205
|
+
|
|
206
|
+
for (const dir of [
|
|
207
|
+
path.join(cwd, '.agents', 'skills'),
|
|
208
|
+
path.join(cwd, '.pi', 'skills'),
|
|
209
|
+
globalDir,
|
|
210
|
+
path.join(os.homedir(), '.agents', 'skills'),
|
|
211
|
+
]) {
|
|
212
|
+
for (const filePath of listSkillFilesInDir(dir, fileSystem)) {
|
|
213
|
+
addSkillFile(filePath, fileSystem, byName, warnings);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const packageSkills = await resolvePackageSkillFiles(options);
|
|
218
|
+
for (const filePath of packageSkills.files) {
|
|
219
|
+
addSkillFile(filePath, fileSystem, byName, warnings);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return { byName, warnings, skippedPackages: packageSkills.skippedPackages };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function skillsCacheKey(options: ResolveSkillsOptions): string | undefined {
|
|
226
|
+
if (options.fs) return undefined;
|
|
227
|
+
return JSON.stringify({
|
|
228
|
+
cwd: options.cwd,
|
|
229
|
+
agentDir: options.agentDir,
|
|
230
|
+
globalDir: options.globalDir ?? defaultGlobalSkillsDir(),
|
|
231
|
+
packageSkillFiles: options.packageSkillFiles,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function getCollectedSkills(options: ResolveSkillsOptions): Promise<CollectedSkills> {
|
|
236
|
+
const cacheKey = skillsCacheKey(options);
|
|
237
|
+
if (!cacheKey) return collectSkills(options);
|
|
238
|
+
|
|
239
|
+
let cached = collectedSkillsCache.get(cacheKey);
|
|
240
|
+
if (!cached) {
|
|
241
|
+
cached = collectSkills(options);
|
|
242
|
+
collectedSkillsCache.set(cacheKey, cached);
|
|
243
|
+
}
|
|
244
|
+
return cached;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function wildcardToRegex(pattern: string): RegExp {
|
|
248
|
+
const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
249
|
+
return new RegExp(`^${escaped}$`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function hasWildcard(pattern: string): boolean {
|
|
253
|
+
return pattern.includes('*');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export async function resolveSkills(
|
|
257
|
+
skillNames: string[],
|
|
258
|
+
options: ResolveSkillsOptions,
|
|
259
|
+
): Promise<ResolveSkillsResult> {
|
|
260
|
+
const requestedNames = skillNames.map((name) => name.trim()).filter(Boolean);
|
|
261
|
+
if (requestedNames.length === 0) {
|
|
262
|
+
return { resolved: [], missing: [], skippedPackages: [], warnings: [] };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const collected = await getCollectedSkills(options);
|
|
114
266
|
const resolved: ResolvedSkill[] = [];
|
|
115
267
|
const missing: string[] = [];
|
|
268
|
+
const seen = new Set<string>();
|
|
116
269
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
270
|
+
function addSkill(skill: ResolvedSkill): void {
|
|
271
|
+
if (seen.has(skill.name)) return;
|
|
272
|
+
seen.add(skill.name);
|
|
273
|
+
resolved.push(skill);
|
|
274
|
+
}
|
|
120
275
|
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
|
|
276
|
+
for (const requestedName of requestedNames) {
|
|
277
|
+
if (hasWildcard(requestedName)) {
|
|
278
|
+
const regex = wildcardToRegex(requestedName);
|
|
279
|
+
let matched = false;
|
|
280
|
+
for (const skill of collected.byName.values()) {
|
|
281
|
+
if (!regex.test(skill.name)) continue;
|
|
282
|
+
matched = true;
|
|
283
|
+
addSkill(skill);
|
|
284
|
+
}
|
|
285
|
+
if (!matched) missing.push(requestedName);
|
|
124
286
|
continue;
|
|
125
287
|
}
|
|
126
288
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
}
|
|
289
|
+
const skill = collected.byName.get(requestedName);
|
|
290
|
+
if (skill) addSkill(skill);
|
|
291
|
+
else missing.push(requestedName);
|
|
143
292
|
}
|
|
144
293
|
|
|
145
|
-
return {
|
|
294
|
+
return {
|
|
295
|
+
resolved,
|
|
296
|
+
missing,
|
|
297
|
+
skippedPackages: collected.skippedPackages,
|
|
298
|
+
warnings: collected.warnings,
|
|
299
|
+
};
|
|
146
300
|
}
|
|
@@ -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,39 @@ 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
|
+
skillWarnings: string[];
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
const TASK_FILE_THRESHOLD = 8000;
|
|
93
105
|
const OUTPUT_MAX_BYTES = 50 * 1024;
|
|
94
106
|
const OUTPUT_MAX_LINES = 2000;
|
|
95
107
|
|
|
108
|
+
export function availableSubagentsForAgent(
|
|
109
|
+
agent: AgentConfig,
|
|
110
|
+
candidateNames?: string[],
|
|
111
|
+
): string[] {
|
|
112
|
+
const names = candidateNames ?? agent.allowedAgents ?? [];
|
|
113
|
+
const allowed = agent.allowedAgents ? new Set(agent.allowedAgents) : undefined;
|
|
114
|
+
const seen = new Set<string>();
|
|
115
|
+
|
|
116
|
+
return names.filter((name) => {
|
|
117
|
+
if (seen.has(name)) return false;
|
|
118
|
+
if (allowed && !allowed.has(name)) return false;
|
|
119
|
+
seen.add(name);
|
|
120
|
+
return true;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
96
124
|
const defaultFs: ExecutorFs = {
|
|
97
125
|
makeTempDir(prefix) {
|
|
98
126
|
return fs.mkdtemp(prefix);
|
|
@@ -333,6 +361,33 @@ function safeFilePart(value: string): string {
|
|
|
333
361
|
return value.replace(/[^a-zA-Z0-9_.-]+/g, '_');
|
|
334
362
|
}
|
|
335
363
|
|
|
364
|
+
export async function buildSubagentSystemPrompt(
|
|
365
|
+
options: BuildSubagentSystemPromptOptions,
|
|
366
|
+
): Promise<BuildSubagentSystemPromptResult> {
|
|
367
|
+
let prompt = options.agent.prompt;
|
|
368
|
+
|
|
369
|
+
const missingSkills: string[] = [];
|
|
370
|
+
const skippedSkillPackages: string[] = [];
|
|
371
|
+
const skillWarnings: string[] = [];
|
|
372
|
+
const skillNames = options.agent.skills;
|
|
373
|
+
if (skillNames && skillNames.length > 0) {
|
|
374
|
+
const resolvedSkills = await resolveSkills(skillNames, {
|
|
375
|
+
cwd: options.cwd,
|
|
376
|
+
agentDir: options.agentDir,
|
|
377
|
+
});
|
|
378
|
+
missingSkills.push(...resolvedSkills.missing);
|
|
379
|
+
skippedSkillPackages.push(...resolvedSkills.skippedPackages);
|
|
380
|
+
skillWarnings.push(...resolvedSkills.warnings);
|
|
381
|
+
|
|
382
|
+
const skillInjection = formatSkillsForPrompt(resolvedSkills.resolved);
|
|
383
|
+
if (skillInjection) {
|
|
384
|
+
prompt = `${prompt}${skillInjection}`;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { prompt, missingSkills, skippedSkillPackages, skillWarnings };
|
|
389
|
+
}
|
|
390
|
+
|
|
336
391
|
function progressFromPartialResult(partialResult: unknown): AgentProgress | undefined {
|
|
337
392
|
if (!partialResult || typeof partialResult !== 'object') return undefined;
|
|
338
393
|
const details = (partialResult as { details?: unknown }).details;
|
|
@@ -377,28 +432,21 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
377
432
|
try {
|
|
378
433
|
const promptFilePath = path.join(tempDir, 'system-prompt.md');
|
|
379
434
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
}
|
|
435
|
+
const promptResult = await buildSubagentSystemPrompt({
|
|
436
|
+
agent: options.agent,
|
|
437
|
+
cwd: options.cwd,
|
|
438
|
+
agentDir: options.agentDir,
|
|
439
|
+
});
|
|
440
|
+
for (const source of promptResult.skippedSkillPackages) {
|
|
441
|
+
console.warn(`[pi-subagents] package not installed, skipping skills: ${source}`);
|
|
388
442
|
}
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
console.warn(`[pi-subagents] skill not found: ${name}`);
|
|
395
|
-
}
|
|
396
|
-
const skillInjection = formatSkillsForPrompt(resolved);
|
|
397
|
-
if (skillInjection) {
|
|
398
|
-
promptContent = `${promptContent}${skillInjection}`;
|
|
399
|
-
}
|
|
443
|
+
for (const warning of promptResult.skillWarnings) {
|
|
444
|
+
console.warn(`[pi-subagents] ${warning}`);
|
|
445
|
+
}
|
|
446
|
+
for (const name of promptResult.missingSkills) {
|
|
447
|
+
console.warn(`[pi-subagents] skill not found: ${name}`);
|
|
400
448
|
}
|
|
401
|
-
await fileSystem.writeFile(promptFilePath,
|
|
449
|
+
await fileSystem.writeFile(promptFilePath, promptResult.prompt);
|
|
402
450
|
|
|
403
451
|
let taskFilePath: string | undefined;
|
|
404
452
|
if (options.task.length > TASK_FILE_THRESHOLD) {
|
|
@@ -411,16 +459,9 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
411
459
|
AuthStorage.create(options.agentDir ? path.join(options.agentDir, 'auth.json') : undefined),
|
|
412
460
|
options.agentDir ? path.join(options.agentDir, 'models.json') : undefined,
|
|
413
461
|
);
|
|
414
|
-
const args = [
|
|
415
|
-
pi.entryPoint,
|
|
416
|
-
'--mode',
|
|
417
|
-
'json',
|
|
418
|
-
'-p',
|
|
419
|
-
'--no-skills',
|
|
420
|
-
'--no-prompt-templates',
|
|
421
|
-
'--no-context-files',
|
|
422
|
-
];
|
|
462
|
+
const args = [pi.entryPoint, '--mode', 'json', '-p', '--no-skills', '--no-prompt-templates'];
|
|
423
463
|
|
|
464
|
+
if (options.agent.systemPromptMode === 'replace') args.push('--no-context-files');
|
|
424
465
|
if (options.agent.model) args.push('--model', options.agent.model);
|
|
425
466
|
args.push('--thinking', options.agent.thinking);
|
|
426
467
|
if (options.agent.tools.length > 0) args.push('--tools', options.agent.tools.join(','));
|
|
@@ -435,10 +476,14 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
435
476
|
...process.env,
|
|
436
477
|
PI_SUBAGENT_DEPTH: String(options.depth ?? 1),
|
|
437
478
|
PI_SUBAGENT_MAX_DEPTH: String(options.agent.maxDepth),
|
|
479
|
+
PI_SUBAGENT_NAME: options.agent.name,
|
|
480
|
+
PI_SUBAGENT_SYSTEM_PROMPT_MODE: options.agent.systemPromptMode,
|
|
438
481
|
};
|
|
439
|
-
|
|
440
|
-
|
|
482
|
+
const visibleAgents = availableSubagentsForAgent(options.agent, options.availableAgents);
|
|
483
|
+
if (visibleAgents.length > 0) {
|
|
484
|
+
env.PI_SUBAGENT_ALLOWED = visibleAgents.join(',');
|
|
441
485
|
}
|
|
486
|
+
env.PI_SUBAGENT_DEBUG = options.agent.debug ? 'true' : 'false';
|
|
442
487
|
|
|
443
488
|
const processLine = (line: string) => {
|
|
444
489
|
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
|
|