@johnnywu/pi-subagents 1.1.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 +24 -0
- package/README.md +42 -3
- package/extensions/agent-loader.ts +14 -1
- package/extensions/env-utils.ts +25 -1
- package/extensions/index.ts +41 -0
- package/extensions/skill-resolver.ts +228 -0
- package/extensions/subagent-executor.ts +110 -12
- package/extensions/subagent-prompt.ts +87 -0
- package/extensions/subagent-tool.ts +9 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,27 @@
|
|
|
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
|
+
|
|
17
|
+
# [1.2.0](https://github.com/jwu/pi-subagents/compare/v1.1.0...v1.2.0) (2026-06-02)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
* agent skills frontmatter support ([e479b0c](https://github.com/jwu/pi-subagents/commit/e479b0cc57c8097a89f73b491c32a099ba56bc77))
|
|
23
|
+
* expose available subagents in prompts ([047035a](https://github.com/jwu/pi-subagents/commit/047035a49e9749dc719a97f2556be9f60551cf97))
|
|
24
|
+
|
|
1
25
|
# [1.1.0](https://github.com/jwu/pi-subagents/compare/v1.0.1...v1.1.0) (2026-05-31)
|
|
2
26
|
|
|
3
27
|
|
package/README.md
CHANGED
|
@@ -48,6 +48,41 @@ 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 when the active tool set includes `subagent`:
|
|
54
|
+
|
|
55
|
+
- The `subagent` tool keeps a one-line prompt guideline:
|
|
56
|
+
`Available subagents: code-reviewer, refactor, test-writer`
|
|
57
|
+
- At agent-start time, the system prompt gets an independent block:
|
|
58
|
+
|
|
59
|
+
```text
|
|
60
|
+
Available subagents:
|
|
61
|
+
- code-reviewer
|
|
62
|
+
- refactor
|
|
63
|
+
- test-writer
|
|
64
|
+
```
|
|
65
|
+
|
|
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.
|
|
85
|
+
|
|
51
86
|
## Agent configuration
|
|
52
87
|
|
|
53
88
|
Agents are Markdown files with YAML frontmatter.
|
|
@@ -59,9 +94,10 @@ Agents are Markdown files with YAML frontmatter.
|
|
|
59
94
|
| `tools` | no | _none_ | Comma-separated tool whitelist (`read, write, bash, grep`, etc.) |
|
|
60
95
|
| `model` | no | parent's model | Provider/model-id (`anthropic/claude-sonnet-4-6`) |
|
|
61
96
|
| `thinking` | no | `off` | Reasoning level: `off`, `low`, `medium`, `high` |
|
|
62
|
-
| `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) |
|
|
63
98
|
| `allowedAgents` | no | _all_ | Comma-separated list of sub-agents this agent may spawn |
|
|
64
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` |
|
|
65
101
|
|
|
66
102
|
The Markdown body after the frontmatter is the agent's system prompt.
|
|
67
103
|
|
|
@@ -74,9 +110,10 @@ description: High-level planner that delegates to specialists
|
|
|
74
110
|
tools: subagent, read, grep, find
|
|
75
111
|
model: anthropic/claude-sonnet-4-6
|
|
76
112
|
thinking: high
|
|
77
|
-
systemPrompt:
|
|
113
|
+
systemPrompt: append
|
|
78
114
|
allowedAgents: code-reviewer, refactor, test-writer
|
|
79
115
|
maxDepth: 2
|
|
116
|
+
debug: false
|
|
80
117
|
---
|
|
81
118
|
|
|
82
119
|
You are an orchestrator. Break complex tasks into sub-tasks and delegate
|
|
@@ -102,7 +139,9 @@ Sub-agents can spawn their own sub-agents (if the `subagent` tool is in their wh
|
|
|
102
139
|
|
|
103
140
|
**`allowedAgents`** — Whitelist enforced by the parent before spawning. A child process never sees agent names outside its parent's whitelist.
|
|
104
141
|
|
|
105
|
-
|
|
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.
|
|
143
|
+
|
|
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.
|
|
106
145
|
|
|
107
146
|
## Session storage
|
|
108
147
|
|
|
@@ -15,6 +15,8 @@ export interface AgentConfig {
|
|
|
15
15
|
systemPromptMode: SystemPromptMode;
|
|
16
16
|
allowedAgents?: string[];
|
|
17
17
|
maxDepth: number;
|
|
18
|
+
debug: boolean;
|
|
19
|
+
skills?: string[];
|
|
18
20
|
prompt: string;
|
|
19
21
|
source: AgentSource;
|
|
20
22
|
filePath: string;
|
|
@@ -106,7 +108,7 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
|
|
|
106
108
|
throw new Error(`invalid thinking: ${data.thinking}`);
|
|
107
109
|
}
|
|
108
110
|
|
|
109
|
-
const systemPromptMode = (data.systemPrompt ?? '
|
|
111
|
+
const systemPromptMode = (data.systemPrompt ?? 'append') as SystemPromptMode;
|
|
110
112
|
if (!['replace', 'append'].includes(systemPromptMode)) {
|
|
111
113
|
throw new Error(`invalid systemPrompt: ${data.systemPrompt}`);
|
|
112
114
|
}
|
|
@@ -116,17 +118,28 @@ function parseAgentFile(content: string, filePath: string, source: AgentSource):
|
|
|
116
118
|
throw new Error(`invalid maxDepth: ${data.maxDepth}`);
|
|
117
119
|
}
|
|
118
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
|
+
|
|
119
129
|
const allowedAgents = splitCsv(data.allowedAgents);
|
|
130
|
+
const skills = splitCsv(data.skills);
|
|
120
131
|
|
|
121
132
|
return {
|
|
122
133
|
name: data.name,
|
|
123
134
|
description: data.description,
|
|
124
135
|
tools: splitCsv(data.tools),
|
|
136
|
+
skills: skills.length > 0 ? skills : undefined,
|
|
125
137
|
model: data.model || undefined,
|
|
126
138
|
thinking,
|
|
127
139
|
systemPromptMode,
|
|
128
140
|
allowedAgents: allowedAgents.length > 0 ? allowedAgents : undefined,
|
|
129
141
|
maxDepth,
|
|
142
|
+
debug,
|
|
130
143
|
prompt: body,
|
|
131
144
|
source,
|
|
132
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,5 +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';
|
|
5
|
+
import { allowedAgentNames, isPastMaxDepth, isSubagentReplaceSystemPrompt } from './env-utils.ts';
|
|
6
|
+
import {
|
|
7
|
+
appendAvailableSubagentsBlock,
|
|
8
|
+
appendAvailableToolsAndGuidelinesBlock,
|
|
9
|
+
} from './subagent-prompt.ts';
|
|
3
10
|
import { registerSubagentTool } from './subagent-tool.ts';
|
|
4
11
|
|
|
5
12
|
export default async function (pi: ExtensionAPI) {
|
|
@@ -9,5 +16,39 @@ export default async function (pi: ExtensionAPI) {
|
|
|
9
16
|
console.warn(`[pi-subagents] skipped ${warning.filePath}: ${warning.message}`);
|
|
10
17
|
}
|
|
11
18
|
|
|
19
|
+
const allowed = allowedAgentNames(process.env);
|
|
20
|
+
const agents = allowed
|
|
21
|
+
? result.agents.filter((candidate) => allowed.has(candidate.name))
|
|
22
|
+
: result.agents;
|
|
23
|
+
const agentNames = agents.map((agent) => agent.name);
|
|
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
|
+
|
|
36
|
+
if (!isPastMaxDepth(process.env) && agentNames.length > 0) {
|
|
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
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
12
53
|
registerSubagentTool(pi, { agents: result.agents });
|
|
13
54
|
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DefaultPackageManager,
|
|
3
|
+
SettingsManager,
|
|
4
|
+
getAgentDir,
|
|
5
|
+
type ResolvedResource,
|
|
6
|
+
} from '@earendil-works/pi-coding-agent';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as os from 'node:os';
|
|
9
|
+
import * as path from 'node:path';
|
|
10
|
+
|
|
11
|
+
export interface ResolvedSkill {
|
|
12
|
+
name: string;
|
|
13
|
+
description: string;
|
|
14
|
+
location: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface SkillResolverFs {
|
|
18
|
+
exists(filePath: string): boolean;
|
|
19
|
+
readFile(filePath: string): string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResolveSkillsOptions {
|
|
23
|
+
cwd: string;
|
|
24
|
+
agentDir?: string;
|
|
25
|
+
globalDir?: string;
|
|
26
|
+
fs?: SkillResolverFs;
|
|
27
|
+
packageSkillFiles?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ResolveSkillsResult {
|
|
31
|
+
resolved: ResolvedSkill[];
|
|
32
|
+
missing: string[];
|
|
33
|
+
skippedPackages: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultSkillFs: SkillResolverFs = {
|
|
37
|
+
exists(filePath) {
|
|
38
|
+
return fs.existsSync(filePath);
|
|
39
|
+
},
|
|
40
|
+
readFile(filePath) {
|
|
41
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function defaultGlobalSkillsDir(): string {
|
|
46
|
+
return path.join(os.homedir(), '.pi', 'agent', 'skills');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function parseSkillFrontmatter(content: string): { name: string; description: string } | undefined {
|
|
50
|
+
const normalized = content.replace(/\r\n/g, '\n');
|
|
51
|
+
if (!normalized.startsWith('---')) return undefined;
|
|
52
|
+
|
|
53
|
+
const endIndex = normalized.indexOf('\n---', 3);
|
|
54
|
+
if (endIndex === -1) return undefined;
|
|
55
|
+
|
|
56
|
+
const frontmatterBlock = normalized.slice(4, endIndex);
|
|
57
|
+
const lines = frontmatterBlock.split('\n');
|
|
58
|
+
const data: Record<string, string> = {};
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < lines.length; i++) {
|
|
61
|
+
const trimmed = lines[i].trim();
|
|
62
|
+
if (!trimmed || trimmed.startsWith('#')) continue;
|
|
63
|
+
|
|
64
|
+
// Check if this is a YAML block scalar continuation (| or >)
|
|
65
|
+
if (
|
|
66
|
+
data._currentBlockKey !== undefined &&
|
|
67
|
+
(lines[i].startsWith(' ') || lines[i].startsWith('\t'))
|
|
68
|
+
) {
|
|
69
|
+
const current = data[data._currentBlockKey] ?? '';
|
|
70
|
+
data[data._currentBlockKey] = current ? `${current} ${trimmed}` : trimmed;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
delete data._currentBlockKey;
|
|
74
|
+
|
|
75
|
+
const separator = trimmed.indexOf(':');
|
|
76
|
+
if (separator === -1) continue;
|
|
77
|
+
const key = trimmed.slice(0, separator).trim();
|
|
78
|
+
let value = trimmed.slice(separator + 1).trim();
|
|
79
|
+
|
|
80
|
+
// Handle YAML block scalar indicators (| and >)
|
|
81
|
+
if (value === '|' || value === '>') {
|
|
82
|
+
data._currentBlockKey = key;
|
|
83
|
+
data[key] = '';
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
value = value.replace(/^['"]|['"]$/g, '');
|
|
88
|
+
if (key) data[key] = value;
|
|
89
|
+
}
|
|
90
|
+
delete data._currentBlockKey;
|
|
91
|
+
|
|
92
|
+
if (!data.description || !data.description.trim()) return undefined;
|
|
93
|
+
return { name: data.name || '', description: data.description.trim() };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function findLocalSkillFile(
|
|
97
|
+
skillName: string,
|
|
98
|
+
cwd: string,
|
|
99
|
+
globalDir: string,
|
|
100
|
+
fileSystem: SkillResolverFs,
|
|
101
|
+
): string | undefined {
|
|
102
|
+
// Priority 1: Project .agents/skills/<name>/SKILL.md
|
|
103
|
+
const projectAgentsPath = path.join(cwd, '.agents', 'skills', skillName, 'SKILL.md');
|
|
104
|
+
if (fileSystem.exists(projectAgentsPath)) return projectAgentsPath;
|
|
105
|
+
|
|
106
|
+
// Priority 1: Project .pi/skills/<name>/SKILL.md
|
|
107
|
+
const projectPiPath = path.join(cwd, '.pi', 'skills', skillName, 'SKILL.md');
|
|
108
|
+
if (fileSystem.exists(projectPiPath)) return projectPiPath;
|
|
109
|
+
|
|
110
|
+
// Priority 4: Global ~/.pi/agent/skills/<name>/SKILL.md
|
|
111
|
+
const globalPiPath = path.join(globalDir, skillName, 'SKILL.md');
|
|
112
|
+
if (fileSystem.exists(globalPiPath)) return globalPiPath;
|
|
113
|
+
|
|
114
|
+
// Priority 4: Global ~/.agents/skills/<name>/SKILL.md
|
|
115
|
+
const globalAgentsPath = path.join(os.homedir(), '.agents', 'skills', skillName, 'SKILL.md');
|
|
116
|
+
if (fileSystem.exists(globalAgentsPath)) return globalAgentsPath;
|
|
117
|
+
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
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(
|
|
177
|
+
skillNames: string[],
|
|
178
|
+
options: ResolveSkillsOptions,
|
|
179
|
+
): Promise<ResolveSkillsResult> {
|
|
180
|
+
const cwd = options.cwd;
|
|
181
|
+
const globalDir = options.globalDir ?? defaultGlobalSkillsDir();
|
|
182
|
+
const fileSystem = options.fs ?? defaultSkillFs;
|
|
183
|
+
const resolved: ResolvedSkill[] = [];
|
|
184
|
+
const missing: string[] = [];
|
|
185
|
+
const pendingPackageResolution: string[] = [];
|
|
186
|
+
|
|
187
|
+
for (const name of skillNames) {
|
|
188
|
+
const trimmed = name.trim();
|
|
189
|
+
if (!trimmed) continue;
|
|
190
|
+
|
|
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
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
pendingPackageResolution.push(trimmed);
|
|
205
|
+
}
|
|
206
|
+
|
|
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
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (packageSkill) resolved.push(packageSkill);
|
|
224
|
+
else missing.push(trimmed);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { resolved, missing, skippedPackages: packageSkills.skippedPackages };
|
|
228
|
+
}
|
|
@@ -5,6 +5,7 @@ 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 { resolveSkills } from './skill-resolver.ts';
|
|
8
9
|
|
|
9
10
|
export interface AgentUsage {
|
|
10
11
|
input: number;
|
|
@@ -77,6 +78,7 @@ export interface RunSubagentOptions {
|
|
|
77
78
|
signal?: AbortSignal;
|
|
78
79
|
onProgress?: (progress: AgentProgress) => void;
|
|
79
80
|
depth?: number;
|
|
81
|
+
availableAgents?: string[];
|
|
80
82
|
tempRoot?: string;
|
|
81
83
|
outputArchiveDir?: string;
|
|
82
84
|
agentDir?: string;
|
|
@@ -86,10 +88,38 @@ export interface RunSubagentOptions {
|
|
|
86
88
|
now?: () => number;
|
|
87
89
|
}
|
|
88
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
|
+
|
|
89
103
|
const TASK_FILE_THRESHOLD = 8000;
|
|
90
104
|
const OUTPUT_MAX_BYTES = 50 * 1024;
|
|
91
105
|
const OUTPUT_MAX_LINES = 2000;
|
|
92
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
|
+
|
|
93
123
|
const defaultFs: ExecutorFs = {
|
|
94
124
|
makeTempDir(prefix) {
|
|
95
125
|
return fs.mkdtemp(prefix);
|
|
@@ -274,6 +304,40 @@ function buildTaskArgument(task: string, taskFilePath: string | undefined): stri
|
|
|
274
304
|
return taskFilePath ? `Task: @${taskFilePath}` : `Task: ${task}`;
|
|
275
305
|
}
|
|
276
306
|
|
|
307
|
+
function escapeXml(text: string): string {
|
|
308
|
+
return text
|
|
309
|
+
.replace(/&/g, '&')
|
|
310
|
+
.replace(/</g, '<')
|
|
311
|
+
.replace(/>/g, '>')
|
|
312
|
+
.replace(/"/g, '"')
|
|
313
|
+
.replace(/'/g, ''');
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function formatSkillsForPrompt(
|
|
317
|
+
skills: Array<{ name: string; description: string; location: string }>,
|
|
318
|
+
): string {
|
|
319
|
+
if (skills.length === 0) return '';
|
|
320
|
+
|
|
321
|
+
const lines = [
|
|
322
|
+
'\n\nThe following skills provide specialized instructions for specific tasks.',
|
|
323
|
+
"Use the read tool to load a skill's file when the task matches its description.",
|
|
324
|
+
'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.',
|
|
325
|
+
'',
|
|
326
|
+
'<available_skills>',
|
|
327
|
+
];
|
|
328
|
+
|
|
329
|
+
for (const skill of skills) {
|
|
330
|
+
lines.push(' <skill>');
|
|
331
|
+
lines.push(` <name>${escapeXml(skill.name)}</name>`);
|
|
332
|
+
lines.push(` <description>${escapeXml(skill.description)}</description>`);
|
|
333
|
+
lines.push(` <location>${escapeXml(skill.location)}</location>`);
|
|
334
|
+
lines.push(' </skill>');
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
lines.push('</available_skills>');
|
|
338
|
+
return lines.join('\n');
|
|
339
|
+
}
|
|
340
|
+
|
|
277
341
|
function byteLength(text: string): number {
|
|
278
342
|
return Buffer.byteLength(text, 'utf8');
|
|
279
343
|
}
|
|
@@ -296,6 +360,31 @@ function safeFilePart(value: string): string {
|
|
|
296
360
|
return value.replace(/[^a-zA-Z0-9_.-]+/g, '_');
|
|
297
361
|
}
|
|
298
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
|
+
|
|
299
388
|
function progressFromPartialResult(partialResult: unknown): AgentProgress | undefined {
|
|
300
389
|
if (!partialResult || typeof partialResult !== 'object') return undefined;
|
|
301
390
|
const details = (partialResult as { details?: unknown }).details;
|
|
@@ -339,7 +428,19 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
339
428
|
|
|
340
429
|
try {
|
|
341
430
|
const promptFilePath = path.join(tempDir, 'system-prompt.md');
|
|
342
|
-
|
|
431
|
+
|
|
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}`);
|
|
439
|
+
}
|
|
440
|
+
for (const name of promptResult.missingSkills) {
|
|
441
|
+
console.warn(`[pi-subagents] skill not found: ${name}`);
|
|
442
|
+
}
|
|
443
|
+
await fileSystem.writeFile(promptFilePath, promptResult.prompt);
|
|
343
444
|
|
|
344
445
|
let taskFilePath: string | undefined;
|
|
345
446
|
if (options.task.length > TASK_FILE_THRESHOLD) {
|
|
@@ -352,16 +453,9 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
352
453
|
AuthStorage.create(options.agentDir ? path.join(options.agentDir, 'auth.json') : undefined),
|
|
353
454
|
options.agentDir ? path.join(options.agentDir, 'models.json') : undefined,
|
|
354
455
|
);
|
|
355
|
-
const args = [
|
|
356
|
-
pi.entryPoint,
|
|
357
|
-
'--mode',
|
|
358
|
-
'json',
|
|
359
|
-
'-p',
|
|
360
|
-
'--no-skills',
|
|
361
|
-
'--no-prompt-templates',
|
|
362
|
-
'--no-context-files',
|
|
363
|
-
];
|
|
456
|
+
const args = [pi.entryPoint, '--mode', 'json', '-p', '--no-skills', '--no-prompt-templates'];
|
|
364
457
|
|
|
458
|
+
if (options.agent.systemPromptMode === 'replace') args.push('--no-context-files');
|
|
365
459
|
if (options.agent.model) args.push('--model', options.agent.model);
|
|
366
460
|
args.push('--thinking', options.agent.thinking);
|
|
367
461
|
if (options.agent.tools.length > 0) args.push('--tools', options.agent.tools.join(','));
|
|
@@ -376,10 +470,14 @@ export async function runSubagent(options: RunSubagentOptions): Promise<AgentRes
|
|
|
376
470
|
...process.env,
|
|
377
471
|
PI_SUBAGENT_DEPTH: String(options.depth ?? 1),
|
|
378
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,
|
|
379
475
|
};
|
|
380
|
-
|
|
381
|
-
|
|
476
|
+
const visibleAgents = availableSubagentsForAgent(options.agent, options.availableAgents);
|
|
477
|
+
if (visibleAgents.length > 0) {
|
|
478
|
+
env.PI_SUBAGENT_ALLOWED = visibleAgents.join(',');
|
|
382
479
|
}
|
|
480
|
+
env.PI_SUBAGENT_DEBUG = options.agent.debug ? 'true' : 'false';
|
|
383
481
|
|
|
384
482
|
const processLine = (line: string) => {
|
|
385
483
|
if (!line.trim()) return;
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
|
|
75
|
+
export function formatAvailableSubagentsBlock(agentNames: string[]): string | undefined {
|
|
76
|
+
const names = [...new Set(agentNames.map((name) => name.trim()).filter(Boolean))].sort();
|
|
77
|
+
if (names.length === 0) return undefined;
|
|
78
|
+
|
|
79
|
+
return ['Available subagents:', ...names.map((name) => `- ${name}`)].join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function appendAvailableSubagentsBlock(systemPrompt: string, agentNames: string[]): string {
|
|
83
|
+
const block = formatAvailableSubagentsBlock(agentNames);
|
|
84
|
+
if (!block || systemPrompt.includes(block)) return systemPrompt;
|
|
85
|
+
|
|
86
|
+
return `${systemPrompt.trimEnd()}\n\n${block}`;
|
|
87
|
+
}
|
|
@@ -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,
|
|
@@ -217,10 +222,8 @@ export function registerSubagentTool(
|
|
|
217
222
|
: options.agents;
|
|
218
223
|
const runner = options.run ?? runSubagent;
|
|
219
224
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
.sort()
|
|
223
|
-
.join(', ');
|
|
225
|
+
const availableSubagents = agents.map((agent) => agent.name);
|
|
226
|
+
const agentNames = [...availableSubagents].sort().join(', ');
|
|
224
227
|
const promptGuidelines =
|
|
225
228
|
agentNames.length > 0 ? [`Available subagents: ${agentNames}`] : undefined;
|
|
226
229
|
|
|
@@ -252,6 +255,7 @@ export function registerSubagentTool(
|
|
|
252
255
|
cwd: params.cwd ?? ctx.cwd,
|
|
253
256
|
signal,
|
|
254
257
|
depth: Number(env.PI_SUBAGENT_DEPTH ?? '0') + 1,
|
|
258
|
+
availableAgents: availableSubagentsForAgent(agent, availableSubagents),
|
|
255
259
|
onProgress: (progress) => onUpdate?.(toProgressResult(progress)),
|
|
256
260
|
});
|
|
257
261
|
|