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 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
- Three fallback agents ship with the extension (used when no user/project agents are configured):
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 > user.
76
- Built-in agents are only used as a fallback when **all three** locations are empty.
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 | Pi default | Override model, e.g. `anthropic/claude-3-5-sonnet` |
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` | Base path for an additional agents directory (`$PI_CODING_AGENT_DIR/agents/*.md`). Agents here override user agents but are overridden by project agents. Built-in agents are only used when user, env, and project locations all yield zero agents. |
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, model, and other runtime settings. Flags the
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:` in agent frontmatter |
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
 
@@ -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 (fallback only when no user/project agents exist)
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 agents with last-write-wins deduplication by name.
148
- * Priority (lowest highest): user < project.
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 agent of userAgents) agentMap.set(agent.name, agent);
156
- for (const agent of projectAgents) agentMap.set(agent.name, agent);
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
- * When scope is "both", project agents override user agents with the same name.
168
- * If no user or project agents exist at all, bundled fallback agents are returned.
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
- const hasConfiguredAgents = userAgents.length > 0 || projectAgents.length > 0;
178
- if (!hasConfiguredAgents) {
179
- return {
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
- return {
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
- ${resumableSubagentsDisabled() ? "" : `
1668
- ### Resumable subagents
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
- pi.registerTool({
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 { agents } = discovery;
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: agentConfig?.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
- // Prefer the pre-resume model (during resume) or the current
1886
- // active model (normal runs). This prevents children from
1887
- // defaulting to whatever settings.json says at spawn time, which
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(modelToRestoreAfterResume ?? ctx.model ?? lastRestorableModel),
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
- if (!resumableSubagentsDisabled()) pi.registerTool({
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(modelToRestoreAfterResume ?? ctx.model ?? lastRestorableModel),
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.6",
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
- // Agent config takes priority; fall back to parent CLI value
565
- const model = agent.model ?? fallbackModelOverride ?? process.env[SUBAGENT_FALLBACK_MODEL_ENV] ?? _inheritedCliArgs.fallbackModel;
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;