oira666_pi-subagent 0.3.6 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -10
- package/agents/team-lead.md +1 -0
- package/agents.ts +56 -23
- package/config.ts +81 -0
- package/index.ts +129 -44
- package/package.json +2 -1
- package/runner.ts +9 -2
package/README.md
CHANGED
|
@@ -56,13 +56,35 @@ Multiple tasks run in parallel:
|
|
|
56
56
|
|
|
57
57
|
Each task supports `agent` (the agent *type* to spawn) and `task`.
|
|
58
58
|
|
|
59
|
+
## Tool Prompt Overrides
|
|
60
|
+
|
|
61
|
+
The complete LLM-facing description of each extension tool can be replaced in
|
|
62
|
+
`pi-subagents.json`. Supported locations, from lowest to highest priority:
|
|
63
|
+
|
|
64
|
+
1. `~/.pi/pi-subagents.json`
|
|
65
|
+
2. `$PI_CODING_AGENT_DIR/pi-subagents.json` (normally `~/.pi/agent/pi-subagents.json`)
|
|
66
|
+
3. The nearest trusted project `.pi/pi-subagents.json`, walking up from the current directory
|
|
67
|
+
|
|
68
|
+
Project values override global values per tool. Missing prompts keep their
|
|
69
|
+
built-in defaults. Use a JSON object for `tool-prompts`:
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"tool-prompts": {
|
|
74
|
+
"subagents": "Your complete replacement prompt for the subagents tool.",
|
|
75
|
+
"resume_subagents": "Your complete replacement prompt for the resume tool."
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
59
80
|
## Bundled Agents
|
|
60
81
|
|
|
61
|
-
|
|
82
|
+
Four built-in agents ship with the extension and remain available alongside custom agents by default:
|
|
62
83
|
|
|
63
84
|
- `code-writer` — implementation and refactoring
|
|
64
85
|
- `code-reviwer` — code review and risk finding
|
|
65
86
|
- `code-architect` — technical design and approach selection
|
|
87
|
+
- `team-lead` — decomposition and delegated multi-agent implementation
|
|
66
88
|
|
|
67
89
|
## Defining Agents
|
|
68
90
|
|
|
@@ -72,15 +94,18 @@ Create Markdown files with YAML frontmatter:
|
|
|
72
94
|
- **Env agents:** `$PI_CODING_AGENT_DIR/agents/*.md` *(when `PI_CODING_AGENT_DIR` is set)*
|
|
73
95
|
- **Project agents:** `.pi/agents/*.md` *(may prompt for confirmation — see `PI_SUBAGENT_CONFIRM_PROJECT_AGENTS`)*
|
|
74
96
|
|
|
75
|
-
Agent discovery priority (highest wins on name collision): project > env >
|
|
76
|
-
Built-in agents
|
|
97
|
+
Agent discovery priority (highest wins on name collision): project > env/user > built-in.
|
|
98
|
+
Built-in agents remain available alongside custom agents unless
|
|
99
|
+
`PI_SUBAGENT_HIDE_BUILTIN_AGENTS=true`. A custom definition with the same name
|
|
100
|
+
as a built-in agent overrides that built-in definition.
|
|
77
101
|
|
|
78
102
|
```markdown
|
|
79
103
|
---
|
|
80
104
|
name: writer
|
|
81
105
|
description: Expert technical writer
|
|
82
|
-
model: anthropic/claude-3-5-sonnet
|
|
83
106
|
thinking: low
|
|
107
|
+
first-layer: enabled
|
|
108
|
+
last-layer: disabled
|
|
84
109
|
tools: read,write
|
|
85
110
|
---
|
|
86
111
|
|
|
@@ -93,9 +118,11 @@ You are an expert technical writer focused on clarity and conciseness.
|
|
|
93
118
|
| ------------- | -------- | -------------------- | -------------------------------------------------------- |
|
|
94
119
|
| `name` | Yes | — | Agent identifier used in tool calls |
|
|
95
120
|
| `description` | Yes | — | What the agent does (shown to the main agent) |
|
|
96
|
-
| `model` | No |
|
|
121
|
+
| `model` | No | Current parent model | Legacy fallback only when live parent model context is unavailable |
|
|
97
122
|
| `thinking` | No | Pi default | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
|
|
98
123
|
| `tools` | No | `read,bash,edit,write` | Comma-separated built-in tools |
|
|
124
|
+
| `first-layer` | No | `enabled` | Set to `disabled` to hide/block this agent at depth 1 |
|
|
125
|
+
| `last-layer` | No | `enabled` | Set to `disabled` to hide/block this agent at max depth |
|
|
99
126
|
|
|
100
127
|
Available tools: `read`, `bash`, `edit`, `write`.
|
|
101
128
|
|
|
@@ -103,7 +130,7 @@ The Markdown body becomes the agent's system prompt (appended to Pi's default, n
|
|
|
103
130
|
|
|
104
131
|
## Delegation Guards
|
|
105
132
|
|
|
106
|
-
Depth and cycle guards prevent runaway recursive delegation.
|
|
133
|
+
Depth and cycle guards prevent runaway recursive delegation. Layer availability is evaluated for the child being launched: depth 1 is the first layer, and `PI_SUBAGENT_MAX_DEPTH` is the last layer. The bundled `team-lead` agent sets `last-layer: disabled` so it cannot consume the final delegation layer.
|
|
107
134
|
|
|
108
135
|
| Config | Default | Description |
|
|
109
136
|
| ------------------------------ | ------- | ------------------------------------------------ |
|
|
@@ -210,13 +237,13 @@ subagent *instance* by its unique name.
|
|
|
210
237
|
|
|
211
238
|
| Env Var | Description |
|
|
212
239
|
| ----------------------- | ------------------------------------------------------------ |
|
|
213
|
-
| `PI_CODING_AGENT_DIR` |
|
|
240
|
+
| `PI_CODING_AGENT_DIR` | Override Pi's agent config directory. Agents are read from `$PI_CODING_AGENT_DIR/agents/*.md`, and tool prompts from `$PI_CODING_AGENT_DIR/pi-subagents.json`. |
|
|
241
|
+
| `PI_SUBAGENT_HIDE_BUILTIN_AGENTS` | Set to `true`/`on`/`yes`/`1` to hide all bundled agents. By default they are available alongside custom agents. |
|
|
214
242
|
|
|
215
243
|
## CLI Argument Proxying
|
|
216
244
|
|
|
217
245
|
Flags passed to the parent `pi` process are forwarded to subagent child
|
|
218
|
-
processes, so they inherit the same provider, API key,
|
|
219
|
-
extension manages itself are blocked from being forwarded.
|
|
246
|
+
processes, so they inherit the same provider, API key, and other runtime settings. At every new launch, the extension explicitly passes the parent's currently active model; changing `/model` mid-conversation therefore affects all subsequently started subagents. Flags the extension manages itself are blocked from being forwarded.
|
|
220
247
|
|
|
221
248
|
**Always forwarded verbatim:**
|
|
222
249
|
|
|
@@ -237,7 +264,7 @@ extension manages itself are blocked from being forwarded.
|
|
|
237
264
|
|
|
238
265
|
| Flag | Overridden by |
|
|
239
266
|
| --- | --- |
|
|
240
|
-
| `--model` | `model:`
|
|
267
|
+
| `--model` | Replaced at launch by the parent's currently active model (`model:` is only a no-context compatibility fallback) |
|
|
241
268
|
| `--thinking` | `thinking:` in agent frontmatter |
|
|
242
269
|
| `--tools` / `--no-tools` | `tools:` in agent frontmatter |
|
|
243
270
|
|
package/agents/team-lead.md
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: team-lead
|
|
3
3
|
description: "A team of agents with different specializations that can take any complex task, split it into parts, and implement architecture, generation, review, or any other kind of task."
|
|
4
|
+
last-layer: disabled
|
|
4
5
|
---
|
|
5
6
|
|
|
6
7
|
You are an experienced team lead, focused on tasks management. You don't do any work yourself. You delegate.
|
package/agents.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Lookup locations:
|
|
8
8
|
* - User agents: ~/.pi/agent/agents/*.md (or $PI_CODING_AGENT_DIR/agents/ when env var is set)
|
|
9
9
|
* - Project agents: .pi/agents/*.md (walks up from cwd)
|
|
10
|
-
* - Bundled agents: ./agents/*.md (
|
|
10
|
+
* - Bundled agents: ./agents/*.md (included unless PI_SUBAGENT_HIDE_BUILTIN_AGENTS is true)
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { getAgentDir, parseFrontmatter } from "@mariozechner/pi-coding-agent";
|
|
@@ -18,12 +18,18 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
export type AgentScope = "user" | "project" | "both";
|
|
19
19
|
export type AgentSource = "user" | "project" | "builtin";
|
|
20
20
|
|
|
21
|
+
export const SUBAGENT_HIDE_BUILTIN_AGENTS_ENV = "PI_SUBAGENT_HIDE_BUILTIN_AGENTS";
|
|
22
|
+
|
|
21
23
|
export interface AgentConfig {
|
|
22
24
|
name: string;
|
|
23
25
|
description: string;
|
|
24
26
|
tools?: string[];
|
|
25
27
|
model?: string;
|
|
26
28
|
thinking?: string;
|
|
29
|
+
/** Whether this agent may be launched at delegation depth 1 (default: true). */
|
|
30
|
+
firstLayer?: boolean;
|
|
31
|
+
/** Whether this agent may be launched at the maximum delegation depth (default: true). */
|
|
32
|
+
lastLayer?: boolean;
|
|
27
33
|
systemPrompt: string;
|
|
28
34
|
source: AgentSource;
|
|
29
35
|
filePath: string;
|
|
@@ -63,8 +69,25 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
|
63
69
|
}
|
|
64
70
|
}
|
|
65
71
|
|
|
72
|
+
function parseLayerSetting(
|
|
73
|
+
value: unknown,
|
|
74
|
+
field: "first-layer" | "last-layer",
|
|
75
|
+
filePath: string,
|
|
76
|
+
): boolean {
|
|
77
|
+
if (value === undefined) return true;
|
|
78
|
+
if (typeof value === "string") {
|
|
79
|
+
const normalized = value.trim().toLowerCase();
|
|
80
|
+
if (normalized === "enabled") return true;
|
|
81
|
+
if (normalized === "disabled") return false;
|
|
82
|
+
}
|
|
83
|
+
console.warn(
|
|
84
|
+
`[pi-subagent] Ignoring invalid ${field} field in "${filePath}". Expected enabled or disabled.`,
|
|
85
|
+
);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
66
89
|
/** Parse a single agent markdown file into an AgentConfig. Returns null on skip. */
|
|
67
|
-
function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
|
|
90
|
+
export function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
|
|
68
91
|
let content: string;
|
|
69
92
|
try {
|
|
70
93
|
content = fs.readFileSync(filePath, "utf-8");
|
|
@@ -113,6 +136,8 @@ function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | nu
|
|
|
113
136
|
tools,
|
|
114
137
|
model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
|
|
115
138
|
thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
|
|
139
|
+
firstLayer: parseLayerSetting(frontmatter["first-layer"], "first-layer", filePath),
|
|
140
|
+
lastLayer: parseLayerSetting(frontmatter["last-layer"], "last-layer", filePath),
|
|
116
141
|
systemPrompt: body,
|
|
117
142
|
source,
|
|
118
143
|
filePath,
|
|
@@ -144,45 +169,53 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
|
|
|
144
169
|
}
|
|
145
170
|
|
|
146
171
|
/**
|
|
147
|
-
* Merge
|
|
148
|
-
*
|
|
172
|
+
* Merge agent layers with last-write-wins deduplication by name.
|
|
173
|
+
* Layers must be passed from lowest to highest priority.
|
|
149
174
|
*/
|
|
150
|
-
function dedupeAgents(
|
|
151
|
-
userAgents: AgentConfig[],
|
|
152
|
-
projectAgents: AgentConfig[],
|
|
153
|
-
): AgentConfig[] {
|
|
175
|
+
function dedupeAgents(...layers: AgentConfig[][]): AgentConfig[] {
|
|
154
176
|
const agentMap = new Map<string, AgentConfig>();
|
|
155
|
-
for (const
|
|
156
|
-
|
|
177
|
+
for (const agents of layers) {
|
|
178
|
+
for (const agent of agents) agentMap.set(agent.name, agent);
|
|
179
|
+
}
|
|
157
180
|
return Array.from(agentMap.values());
|
|
158
181
|
}
|
|
159
182
|
|
|
183
|
+
function hideBuiltinAgents(): boolean {
|
|
184
|
+
const value = process.env[SUBAGENT_HIDE_BUILTIN_AGENTS_ENV]?.trim().toLowerCase();
|
|
185
|
+
return value === "1" || value === "true" || value === "yes" || value === "on";
|
|
186
|
+
}
|
|
187
|
+
|
|
160
188
|
// ---------------------------------------------------------------------------
|
|
161
189
|
// Public API
|
|
162
190
|
// ---------------------------------------------------------------------------
|
|
163
191
|
|
|
192
|
+
/** Whether an agent is available to be launched at the requested child depth. */
|
|
193
|
+
export function isAgentEnabledAtLayer(
|
|
194
|
+
agent: AgentConfig,
|
|
195
|
+
targetDepth: number,
|
|
196
|
+
maxDepth: number,
|
|
197
|
+
): boolean {
|
|
198
|
+
if (targetDepth === 1 && agent.firstLayer === false) return false;
|
|
199
|
+
if (targetDepth === maxDepth && agent.lastLayer === false) return false;
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
164
203
|
/**
|
|
165
204
|
* Discover all available agents according to the requested scope.
|
|
166
205
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
206
|
+
* Built-in agents are included at the lowest priority unless
|
|
207
|
+
* PI_SUBAGENT_HIDE_BUILTIN_AGENTS is true. Custom agents with the same name
|
|
208
|
+
* override their built-in counterpart.
|
|
169
209
|
*/
|
|
170
210
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
171
211
|
const userDir = path.join(getAgentDir(), "agents");
|
|
172
212
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
173
213
|
|
|
214
|
+
const builtinAgents = hideBuiltinAgents() ? [] : loadAgentsFromDir(BUNDLED_AGENTS_DIR, "builtin");
|
|
174
215
|
const userAgents = loadAgentsFromDir(userDir, "user");
|
|
175
216
|
const projectAgents = projectAgentsDir ? loadAgentsFromDir(projectAgentsDir, "project") : [];
|
|
176
217
|
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
agents: loadAgentsFromDir(BUNDLED_AGENTS_DIR, "builtin"),
|
|
181
|
-
projectAgentsDir,
|
|
182
|
-
};
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (scope === "user") return { agents: userAgents, projectAgentsDir };
|
|
186
|
-
if (scope === "project") return { agents: projectAgents, projectAgentsDir };
|
|
187
|
-
return { agents: dedupeAgents(userAgents, projectAgents), projectAgentsDir };
|
|
218
|
+
if (scope === "user") return { agents: dedupeAgents(builtinAgents, userAgents), projectAgentsDir };
|
|
219
|
+
if (scope === "project") return { agents: dedupeAgents(builtinAgents, projectAgents), projectAgentsDir };
|
|
220
|
+
return { agents: dedupeAgents(builtinAgents, userAgents, projectAgents), projectAgentsDir };
|
|
188
221
|
}
|
package/config.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { getAgentDir } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as os from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
export const PI_SUBAGENTS_CONFIG_FILE = "pi-subagents.json";
|
|
7
|
+
|
|
8
|
+
export interface PiSubagentsConfig {
|
|
9
|
+
toolPrompts: Record<string, string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function readToolPrompts(filePath: string): Record<string, string> {
|
|
13
|
+
if (!fs.existsSync(filePath)) return {};
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const parsed = JSON.parse(fs.readFileSync(filePath, "utf8")) as unknown;
|
|
17
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
18
|
+
console.warn(`[pi-subagent] Ignoring invalid config "${filePath}". Expected a JSON object.`);
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const toolPrompts = (parsed as Record<string, unknown>)["tool-prompts"];
|
|
23
|
+
if (toolPrompts === undefined) return {};
|
|
24
|
+
if (!toolPrompts || typeof toolPrompts !== "object" || Array.isArray(toolPrompts)) {
|
|
25
|
+
console.warn(`[pi-subagent] Ignoring invalid tool-prompts in "${filePath}". Expected an object of tool-name to prompt strings.`);
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result: Record<string, string> = {};
|
|
30
|
+
for (const [toolName, prompt] of Object.entries(toolPrompts)) {
|
|
31
|
+
if (typeof prompt === "string" && prompt.trim().length > 0) {
|
|
32
|
+
result[toolName] = prompt;
|
|
33
|
+
} else {
|
|
34
|
+
console.warn(`[pi-subagent] Ignoring invalid prompt for tool "${toolName}" in "${filePath}". Expected a non-empty string.`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
} catch (err) {
|
|
39
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
+
console.warn(`[pi-subagent] Failed to read config "${filePath}": ${message}`);
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Find the nearest project-local .pi/pi-subagents.json while walking up from cwd. */
|
|
46
|
+
export function findProjectConfig(cwd: string): string | null {
|
|
47
|
+
let dir = path.resolve(cwd);
|
|
48
|
+
while (true) {
|
|
49
|
+
const candidate = path.join(dir, ".pi", PI_SUBAGENTS_CONFIG_FILE);
|
|
50
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
51
|
+
const parent = path.dirname(dir);
|
|
52
|
+
if (parent === dir) return null;
|
|
53
|
+
dir = parent;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load tool prompt overrides from lowest to highest priority:
|
|
59
|
+
* ~/.pi/pi-subagents.json
|
|
60
|
+
* $PI_CODING_AGENT_DIR/pi-subagents.json (normally ~/.pi/agent/pi-subagents.json)
|
|
61
|
+
* nearest project .pi/pi-subagents.json (trusted projects only)
|
|
62
|
+
*/
|
|
63
|
+
export function loadPiSubagentsConfig(
|
|
64
|
+
cwd?: string,
|
|
65
|
+
includeProject = false,
|
|
66
|
+
): PiSubagentsConfig {
|
|
67
|
+
const paths = [
|
|
68
|
+
path.join(os.homedir(), ".pi", PI_SUBAGENTS_CONFIG_FILE),
|
|
69
|
+
path.join(getAgentDir(), PI_SUBAGENTS_CONFIG_FILE),
|
|
70
|
+
];
|
|
71
|
+
if (cwd && includeProject) {
|
|
72
|
+
const projectConfig = findProjectConfig(cwd);
|
|
73
|
+
if (projectConfig) paths.push(projectConfig);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const toolPrompts: Record<string, string> = {};
|
|
77
|
+
for (const filePath of new Set(paths)) {
|
|
78
|
+
Object.assign(toolPrompts, readToolPrompts(filePath));
|
|
79
|
+
}
|
|
80
|
+
return { toolPrompts };
|
|
81
|
+
}
|
package/index.ts
CHANGED
|
@@ -18,7 +18,8 @@ import {
|
|
|
18
18
|
lazyStream,
|
|
19
19
|
} from "@mariozechner/pi-ai";
|
|
20
20
|
import { Type } from "@sinclair/typebox";
|
|
21
|
-
import { type AgentConfig, discoverAgents } from "./agents.js";
|
|
21
|
+
import { type AgentConfig, discoverAgents, isAgentEnabledAtLayer } from "./agents.js";
|
|
22
|
+
import { loadPiSubagentsConfig } from "./config.js";
|
|
22
23
|
import {
|
|
23
24
|
allocateSubagentNames,
|
|
24
25
|
clearResumeActive,
|
|
@@ -100,6 +101,37 @@ const SUBAGENT_STACK_ENV = "PI_SUBAGENT_STACK";
|
|
|
100
101
|
const SUBAGENT_PREVENT_CYCLES_ENV = "PI_SUBAGENT_PREVENT_CYCLES";
|
|
101
102
|
const SUBAGENT_CONFIRM_PROJECT_AGENTS_ENV = "PI_SUBAGENT_CONFIRM_PROJECT_AGENTS";
|
|
102
103
|
|
|
104
|
+
const BASE_SUBAGENTS_TOOL_DESCRIPTION = [
|
|
105
|
+
"Delegate work to specialized subagents running as isolated pi processes.",
|
|
106
|
+
"",
|
|
107
|
+
"Pass a `tasks` array. Every task in the same call runs IN PARALLEL.",
|
|
108
|
+
" - 1 task -> single delegation",
|
|
109
|
+
" - N tasks -> all N run concurrently in one call",
|
|
110
|
+
"",
|
|
111
|
+
"For sequential work (task B depends on task A's output), make separate",
|
|
112
|
+
"tool calls one after another. Do NOT put dependent tasks in the same array.",
|
|
113
|
+
"",
|
|
114
|
+
'Single: { tasks: [{ agent: "writer", task: "Rewrite README.md" }] }',
|
|
115
|
+
'Parallel: { tasks: [{ agent: "writer", task: "..." }, { agent: "tester", task: "..." }] }',
|
|
116
|
+
].join("\n");
|
|
117
|
+
|
|
118
|
+
const SUBAGENT_USAGE_GUIDANCE =
|
|
119
|
+
"Be careful with subagents: use them when the user explicitly asks or when they are truly necessary, because they are expensive. Good cases: running several exploration tasks in parallel, solving several tasks in parallel, or delegating several large tasks to separate subagents. Bad cases (don't do this): creating many nested subagents with similar tasks, using sequential subagents for simple short tasks, running a subagent just to read a file or execute a bash command, or delegating work that does not need a team or parallel execution (unless the user asked you to).";
|
|
120
|
+
|
|
121
|
+
export function getSubagentsToolDescription(): string {
|
|
122
|
+
return `${BASE_SUBAGENTS_TOOL_DESCRIPTION}\n\n${SUBAGENT_USAGE_GUIDANCE}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function sameToolPrompts(
|
|
126
|
+
left: Record<string, string>,
|
|
127
|
+
right: Record<string, string>,
|
|
128
|
+
): boolean {
|
|
129
|
+
const leftKeys = Object.keys(left);
|
|
130
|
+
const rightKeys = Object.keys(right);
|
|
131
|
+
return leftKeys.length === rightKeys.length &&
|
|
132
|
+
leftKeys.every((key) => left[key] === right[key]);
|
|
133
|
+
}
|
|
134
|
+
|
|
103
135
|
type ProjectAgentConfirmationSetting = "ask" | "never" | "session";
|
|
104
136
|
type ProjectAgentApproval = "once" | "session" | "no";
|
|
105
137
|
|
|
@@ -387,6 +419,15 @@ function makeDetailsFactory(
|
|
|
387
419
|
};
|
|
388
420
|
}
|
|
389
421
|
|
|
422
|
+
function filterAgentsForCurrentLayer(
|
|
423
|
+
agents: AgentConfig[],
|
|
424
|
+
currentDepth: number,
|
|
425
|
+
maxDepth: number,
|
|
426
|
+
): AgentConfig[] {
|
|
427
|
+
const targetDepth = currentDepth + 1;
|
|
428
|
+
return agents.filter((agent) => isAgentEnabledAtLayer(agent, targetDepth, maxDepth));
|
|
429
|
+
}
|
|
430
|
+
|
|
390
431
|
function formatAgentNames(agents: AgentConfig[]): string {
|
|
391
432
|
return agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
|
|
392
433
|
}
|
|
@@ -640,11 +681,32 @@ function getRestorableModel(ctx: any): any | undefined {
|
|
|
640
681
|
return findLastNonResumeModel(ctx) ?? getEnvFallbackModel(ctx);
|
|
641
682
|
}
|
|
642
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Pick the parent model inherited by a subagent launch.
|
|
686
|
+
*
|
|
687
|
+
* A normal tool call was emitted by the current model, so that model is the
|
|
688
|
+
* authoritative choice. Looking backward in the session is only appropriate
|
|
689
|
+
* while our own synthetic resume model is active (or no current model exists).
|
|
690
|
+
*/
|
|
691
|
+
export function selectParentModelForSubagent(
|
|
692
|
+
currentModel: any | undefined,
|
|
693
|
+
modelBeforeSynthetic: any | undefined,
|
|
694
|
+
historicalRealModel: any | undefined,
|
|
695
|
+
lastRestorableModel: any | undefined,
|
|
696
|
+
): any | undefined {
|
|
697
|
+
if (currentModel?.provider && currentModel.provider !== RESUME_PROVIDER) {
|
|
698
|
+
return currentModel;
|
|
699
|
+
}
|
|
700
|
+
return modelBeforeSynthetic ?? historicalRealModel ?? lastRestorableModel;
|
|
701
|
+
}
|
|
702
|
+
|
|
643
703
|
// ---------------------------------------------------------------------------
|
|
644
704
|
// Extension entry point
|
|
645
705
|
// ---------------------------------------------------------------------------
|
|
646
706
|
|
|
647
707
|
export default function (pi: ExtensionAPI) {
|
|
708
|
+
let configuredToolPrompts = loadPiSubagentsConfig().toolPrompts;
|
|
709
|
+
let refreshRegisteredToolPrompts: ((cwd: string, includeProject: boolean) => void) | undefined;
|
|
648
710
|
let resumeModelRegistry: any | undefined;
|
|
649
711
|
let lastRestorableModel: any | undefined;
|
|
650
712
|
let latestSessionCtx: any | undefined;
|
|
@@ -664,6 +726,21 @@ export default function (pi: ExtensionAPI) {
|
|
|
664
726
|
resumeState.trigger = "resumePrompt";
|
|
665
727
|
}
|
|
666
728
|
|
|
729
|
+
function getParentModelForSubagent(ctx: any): any | undefined {
|
|
730
|
+
const currentModel = ctx?.model;
|
|
731
|
+
// Avoid historical lookup during normal calls: the current assistant
|
|
732
|
+
// response is the one that emitted the subagents tool call.
|
|
733
|
+
if (currentModel?.provider && currentModel.provider !== RESUME_PROVIDER) {
|
|
734
|
+
return currentModel;
|
|
735
|
+
}
|
|
736
|
+
return selectParentModelForSubagent(
|
|
737
|
+
currentModel,
|
|
738
|
+
modelToRestoreAfterResume,
|
|
739
|
+
findLastNonResumeModel(ctx) ?? getEnvFallbackModel(ctx),
|
|
740
|
+
lastRestorableModel,
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
|
|
667
744
|
function scheduleSessionTask(callback: () => void, delayMs: number): void {
|
|
668
745
|
const expectedGeneration = lifecycleGeneration;
|
|
669
746
|
const timer = setTimeout(() => {
|
|
@@ -1320,6 +1397,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
1320
1397
|
lifecycleGeneration += 1;
|
|
1321
1398
|
sessionActive = true;
|
|
1322
1399
|
latestSessionCtx = ctx;
|
|
1400
|
+
const includeProjectConfig =
|
|
1401
|
+
typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted() === true;
|
|
1402
|
+
refreshRegisteredToolPrompts?.(ctx.cwd, includeProjectConfig);
|
|
1323
1403
|
resumeModelRegistry = ctx.modelRegistry;
|
|
1324
1404
|
clearSyntheticResumeState();
|
|
1325
1405
|
pendingResumePlans = [];
|
|
@@ -1342,7 +1422,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1342
1422
|
if (!canDelegate) return;
|
|
1343
1423
|
|
|
1344
1424
|
const discovery = discoverAgents(ctx.cwd, "both");
|
|
1345
|
-
discoveredAgents = discovery.agents;
|
|
1425
|
+
discoveredAgents = filterAgentsForCurrentLayer(discovery.agents, currentDepth, maxDepth);
|
|
1346
1426
|
currentSessionId = ctx.sessionManager.getSessionId?.() ?? "ephemeral";
|
|
1347
1427
|
currentSubagentSessionRoot = getDefaultSubagentSessionRoot(ctx);
|
|
1348
1428
|
if (resumableSubagentsDisabled()) {
|
|
@@ -1632,16 +1712,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1632
1712
|
const agentList = discoveredAgents
|
|
1633
1713
|
.map((a) => `- **${a.name}**: ${a.description}`)
|
|
1634
1714
|
.join("\n");
|
|
1635
|
-
|
|
1636
|
-
systemPrompt:
|
|
1637
|
-
event.systemPrompt +
|
|
1638
|
-
`\n\n## Available Subagents
|
|
1639
|
-
|
|
1640
|
-
The following subagents are available via the \`subagents\` tool:
|
|
1641
|
-
|
|
1642
|
-
${agentList}
|
|
1643
|
-
|
|
1644
|
-
### How to call the subagents tool
|
|
1715
|
+
const subagentsGuidance = configuredToolPrompts[SUBAGENT_TOOL_NAME] ?? `### How to call the subagents tool
|
|
1645
1716
|
|
|
1646
1717
|
Each subagent runs in an **isolated process**.
|
|
1647
1718
|
|
|
@@ -1663,9 +1734,10 @@ calls one after another. Do NOT put dependent tasks in the same array.
|
|
|
1663
1734
|
\`\`\`
|
|
1664
1735
|
|
|
1665
1736
|
- Max depth: current depth ${currentDepth}, max depth ${maxDepth}
|
|
1666
|
-
- Max subagents per tool call: ${maxParallelTasks}
|
|
1667
|
-
|
|
1668
|
-
|
|
1737
|
+
- Max subagents per tool call: ${maxParallelTasks}`;
|
|
1738
|
+
const resumeGuidance = resumableSubagentsDisabled()
|
|
1739
|
+
? ""
|
|
1740
|
+
: configuredToolPrompts[RESUME_SUBAGENTS_TOOL_NAME] ?? `### Resumable subagents
|
|
1669
1741
|
|
|
1670
1742
|
Every subagent run is assigned a unique, durable name (e.g. \`code-writer-01\`,
|
|
1671
1743
|
\`code-reviewer-02\`) which is returned together with its results. Use the
|
|
@@ -1681,8 +1753,13 @@ keeping their full previous context:
|
|
|
1681
1753
|
- All resumes in one call run in parallel.
|
|
1682
1754
|
- You may include subagent names in the task text you give YOUR OWN subagents,
|
|
1683
1755
|
so they can resume those subagents themselves.
|
|
1684
|
-
- Names survive restarts; you can resume them in a later session of this conversation
|
|
1685
|
-
|
|
1756
|
+
- Names survive restarts; you can resume them in a later session of this conversation.`;
|
|
1757
|
+
return {
|
|
1758
|
+
systemPrompt: `${event.systemPrompt}\n\n## Available Subagents
|
|
1759
|
+
|
|
1760
|
+
The following subagents are available via the \`subagents\` tool:
|
|
1761
|
+
|
|
1762
|
+
${agentList}\n\n${subagentsGuidance}${resumeGuidance ? `\n\n${resumeGuidance}` : ""}`,
|
|
1686
1763
|
};
|
|
1687
1764
|
} catch (err) {
|
|
1688
1765
|
console.error("[pi-subagent] Error in before_agent_start:", err);
|
|
@@ -1691,22 +1768,11 @@ keeping their full previous context:
|
|
|
1691
1768
|
|
|
1692
1769
|
// Register the subagents tool
|
|
1693
1770
|
if (canDelegate) {
|
|
1694
|
-
|
|
1771
|
+
const registerSubagentsTool = () => {
|
|
1772
|
+
pi.registerTool({
|
|
1695
1773
|
name: SUBAGENT_TOOL_NAME,
|
|
1696
1774
|
label: "Subagents",
|
|
1697
|
-
description: [
|
|
1698
|
-
"Delegate work to specialized subagents running as isolated pi processes.",
|
|
1699
|
-
"",
|
|
1700
|
-
"Pass a `tasks` array. Every task in the same call runs IN PARALLEL.",
|
|
1701
|
-
" - 1 task -> single delegation",
|
|
1702
|
-
" - N tasks -> all N run concurrently in one call",
|
|
1703
|
-
"",
|
|
1704
|
-
"For sequential work (task B depends on task A's output), make separate",
|
|
1705
|
-
"tool calls one after another. Do NOT put dependent tasks in the same array.",
|
|
1706
|
-
"",
|
|
1707
|
-
'Single: { tasks: [{ agent: "writer", task: "Rewrite README.md" }] }',
|
|
1708
|
-
'Parallel: { tasks: [{ agent: "writer", task: "..." }, { agent: "tester", task: "..." }] }',
|
|
1709
|
-
].join("\n"),
|
|
1775
|
+
description: configuredToolPrompts[SUBAGENT_TOOL_NAME] ?? getSubagentsToolDescription(),
|
|
1710
1776
|
parameters: SubagentParams,
|
|
1711
1777
|
|
|
1712
1778
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
@@ -1714,7 +1780,7 @@ keeping their full previous context:
|
|
|
1714
1780
|
recordToolCallStart(toolCallId);
|
|
1715
1781
|
updateLatestBroadcastTargets(undefined);
|
|
1716
1782
|
const discovery = discoverAgents(ctx.cwd, "both");
|
|
1717
|
-
const
|
|
1783
|
+
const agents = filterAgentsForCurrentLayer(discovery.agents, currentDepth, maxDepth);
|
|
1718
1784
|
|
|
1719
1785
|
const makeDetails = makeDetailsFactory(
|
|
1720
1786
|
discovery.projectAgentsDir,
|
|
@@ -1852,7 +1918,8 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1852
1918
|
return {
|
|
1853
1919
|
agent: task.agent,
|
|
1854
1920
|
task: task.task,
|
|
1855
|
-
model:
|
|
1921
|
+
model:
|
|
1922
|
+
formatModelFlag(getParentModelForSubagent(ctx)) ?? agentConfig?.model,
|
|
1856
1923
|
tools: agentConfig?.tools,
|
|
1857
1924
|
sessionDir:
|
|
1858
1925
|
resumePlan?.details?.results[index]?.sessionDir ??
|
|
@@ -1882,11 +1949,9 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1882
1949
|
resumePlan?.details?.results[0],
|
|
1883
1950
|
getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, 0),
|
|
1884
1951
|
!!resumePlan,
|
|
1885
|
-
//
|
|
1886
|
-
//
|
|
1887
|
-
|
|
1888
|
-
// can change while the parent session is long-running.
|
|
1889
|
-
formatModelFlag(modelToRestoreAfterResume ?? ctx.model ?? lastRestorableModel),
|
|
1952
|
+
// Normal calls inherit the model that emitted this tool call;
|
|
1953
|
+
// synthetic resume calls recover the preceding real model.
|
|
1954
|
+
formatModelFlag(getParentModelForSubagent(ctx)),
|
|
1890
1955
|
topLevelBaseId,
|
|
1891
1956
|
names[0],
|
|
1892
1957
|
);
|
|
@@ -1902,7 +1967,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1902
1967
|
resumePlan?.details?.results,
|
|
1903
1968
|
(index) => getSessionDirForTask(resumePlan?.previousToolCallId ?? toolCallId, index),
|
|
1904
1969
|
!!resumePlan,
|
|
1905
|
-
formatModelFlag(
|
|
1970
|
+
formatModelFlag(getParentModelForSubagent(ctx)),
|
|
1906
1971
|
topLevelBaseId,
|
|
1907
1972
|
{ names },
|
|
1908
1973
|
);
|
|
@@ -1921,12 +1986,15 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
1921
1986
|
renderCall: (args, theme, context) => renderCall(args, theme, context),
|
|
1922
1987
|
renderResult: (result, { expanded }, theme) =>
|
|
1923
1988
|
renderResult(result, expanded, theme),
|
|
1924
|
-
|
|
1989
|
+
});
|
|
1990
|
+
};
|
|
1925
1991
|
|
|
1926
|
-
|
|
1992
|
+
const registerResumeSubagentsTool = () => {
|
|
1993
|
+
if (resumableSubagentsDisabled()) return;
|
|
1994
|
+
pi.registerTool({
|
|
1927
1995
|
name: RESUME_SUBAGENTS_TOOL_NAME,
|
|
1928
1996
|
label: "Resume subagents",
|
|
1929
|
-
description: [
|
|
1997
|
+
description: configuredToolPrompts[RESUME_SUBAGENTS_TOOL_NAME] ?? [
|
|
1930
1998
|
"Resume previously run subagents by name with a new task, keeping their full context.",
|
|
1931
1999
|
"",
|
|
1932
2000
|
"Every subagent run returns a unique name (e.g. code-writer-01). Pass those names",
|
|
@@ -2102,7 +2170,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
2102
2170
|
undefined,
|
|
2103
2171
|
(index) => targets[index].sessionDir,
|
|
2104
2172
|
true,
|
|
2105
|
-
formatModelFlag(
|
|
2173
|
+
formatModelFlag(getParentModelForSubagent(ctx)),
|
|
2106
2174
|
topLevelBaseId,
|
|
2107
2175
|
{ names: targets.map((target) => target.name), rawPrompts: true },
|
|
2108
2176
|
);
|
|
@@ -2129,7 +2197,24 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
|
|
|
2129
2197
|
renderCall: (args, theme, context) => renderResumeCall(args, theme, context),
|
|
2130
2198
|
renderResult: (result, { expanded }, theme) =>
|
|
2131
2199
|
renderResult(result, expanded, theme),
|
|
2132
|
-
|
|
2200
|
+
});
|
|
2201
|
+
};
|
|
2202
|
+
|
|
2203
|
+
const registerToolsWithConfig = (
|
|
2204
|
+
cwd?: string,
|
|
2205
|
+
includeProject = false,
|
|
2206
|
+
force = false,
|
|
2207
|
+
) => {
|
|
2208
|
+
const nextToolPrompts = loadPiSubagentsConfig(cwd, includeProject).toolPrompts;
|
|
2209
|
+
if (!force && sameToolPrompts(configuredToolPrompts, nextToolPrompts)) return;
|
|
2210
|
+
configuredToolPrompts = nextToolPrompts;
|
|
2211
|
+
registerSubagentsTool();
|
|
2212
|
+
registerResumeSubagentsTool();
|
|
2213
|
+
};
|
|
2214
|
+
refreshRegisteredToolPrompts = (cwd, includeProject) => {
|
|
2215
|
+
registerToolsWithConfig(cwd, includeProject);
|
|
2216
|
+
};
|
|
2217
|
+
registerToolsWithConfig(undefined, false, true);
|
|
2133
2218
|
}
|
|
2134
2219
|
|
|
2135
2220
|
function getSessionDirForTask(toolCallId: string, index: number): string {
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oira666_pi-subagent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"index.ts",
|
|
9
9
|
"agents.ts",
|
|
10
|
+
"config.ts",
|
|
10
11
|
"runner.ts",
|
|
11
12
|
"resume.ts",
|
|
12
13
|
"names.ts",
|
package/runner.ts
CHANGED
|
@@ -542,6 +542,12 @@ export function processJsonLine(line: string, result: SingleResult): boolean {
|
|
|
542
542
|
// Build pi CLI arguments
|
|
543
543
|
// ---------------------------------------------------------------------------
|
|
544
544
|
|
|
545
|
+
export function resolveSubagentModel(agentModel?: string, currentParentModel?: string): string | undefined {
|
|
546
|
+
// The active parent model is authoritative. Agent frontmatter is retained as
|
|
547
|
+
// a compatibility fallback only for callers that cannot supply live context.
|
|
548
|
+
return currentParentModel ?? agentModel ?? process.env[SUBAGENT_FALLBACK_MODEL_ENV] ?? _inheritedCliArgs.fallbackModel;
|
|
549
|
+
}
|
|
550
|
+
|
|
545
551
|
function buildPiArgs(
|
|
546
552
|
agent: AgentConfig,
|
|
547
553
|
systemPromptPath: string | null,
|
|
@@ -561,8 +567,9 @@ function buildPiArgs(
|
|
|
561
567
|
if (sessionDir) args.push("--session-dir", sessionDir);
|
|
562
568
|
if (resumeSession) args.push("--continue");
|
|
563
569
|
|
|
564
|
-
//
|
|
565
|
-
|
|
570
|
+
// Always use the model active in the parent at launch time. This matters
|
|
571
|
+
// when /model changed after the parent process originally started.
|
|
572
|
+
const model = resolveSubagentModel(agent.model, fallbackModelOverride);
|
|
566
573
|
if (model) args.push("--model", model);
|
|
567
574
|
|
|
568
575
|
const thinking = agent.thinking ?? _inheritedCliArgs.fallbackThinking;
|