oira666_pi-subagent 0.3.7 → 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,8 +94,10 @@ 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
  ---
@@ -213,7 +237,8 @@ subagent *instance* by its unique name.
213
237
 
214
238
  | Env Var | Description |
215
239
  | ----------------------- | ------------------------------------------------------------ |
216
- | `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. |
217
242
 
218
243
  ## CLI Argument Proxying
219
244
 
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,6 +18,8 @@ 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;
@@ -167,19 +169,22 @@ function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {
167
169
  }
168
170
 
169
171
  /**
170
- * Merge agents with last-write-wins deduplication by name.
171
- * 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.
172
174
  */
173
- function dedupeAgents(
174
- userAgents: AgentConfig[],
175
- projectAgents: AgentConfig[],
176
- ): AgentConfig[] {
175
+ function dedupeAgents(...layers: AgentConfig[][]): AgentConfig[] {
177
176
  const agentMap = new Map<string, AgentConfig>();
178
- for (const agent of userAgents) agentMap.set(agent.name, agent);
179
- 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
+ }
180
180
  return Array.from(agentMap.values());
181
181
  }
182
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
+
183
188
  // ---------------------------------------------------------------------------
184
189
  // Public API
185
190
  // ---------------------------------------------------------------------------
@@ -198,25 +203,19 @@ export function isAgentEnabledAtLayer(
198
203
  /**
199
204
  * Discover all available agents according to the requested scope.
200
205
  *
201
- * When scope is "both", project agents override user agents with the same name.
202
- * 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.
203
209
  */
204
210
  export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
205
211
  const userDir = path.join(getAgentDir(), "agents");
206
212
  const projectAgentsDir = findNearestProjectAgentsDir(cwd);
207
213
 
214
+ const builtinAgents = hideBuiltinAgents() ? [] : loadAgentsFromDir(BUNDLED_AGENTS_DIR, "builtin");
208
215
  const userAgents = loadAgentsFromDir(userDir, "user");
209
216
  const projectAgents = projectAgentsDir ? loadAgentsFromDir(projectAgentsDir, "project") : [];
210
217
 
211
- const hasConfiguredAgents = userAgents.length > 0 || projectAgents.length > 0;
212
- if (!hasConfiguredAgents) {
213
- return {
214
- agents: loadAgentsFromDir(BUNDLED_AGENTS_DIR, "builtin"),
215
- projectAgentsDir,
216
- };
217
- }
218
-
219
- if (scope === "user") return { agents: userAgents, projectAgentsDir };
220
- if (scope === "project") return { agents: projectAgents, projectAgentsDir };
221
- 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 };
222
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
@@ -19,6 +19,7 @@ import {
19
19
  } from "@mariozechner/pi-ai";
20
20
  import { Type } from "@sinclair/typebox";
21
21
  import { type AgentConfig, discoverAgents, isAgentEnabledAtLayer } from "./agents.js";
22
+ import { loadPiSubagentsConfig } from "./config.js";
22
23
  import {
23
24
  allocateSubagentNames,
24
25
  clearResumeActive,
@@ -114,22 +115,21 @@ const BASE_SUBAGENTS_TOOL_DESCRIPTION = [
114
115
  'Parallel: { tasks: [{ agent: "writer", task: "..." }, { agent: "tester", task: "..." }] }',
115
116
  ].join("\n");
116
117
 
117
- const GPT_56_SUBAGENT_GUIDANCE =
118
+ const SUBAGENT_USAGE_GUIDANCE =
118
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).";
119
120
 
120
- export function isGpt56Model(model: unknown): boolean {
121
- if (typeof model === "string") return model.toLowerCase().includes("gpt-5.6");
122
- if (!model || typeof model !== "object") return false;
123
- const candidate = model as { id?: unknown; name?: unknown };
124
- return [candidate.id, candidate.name].some(
125
- (value) => typeof value === "string" && value.toLowerCase().includes("gpt-5.6"),
126
- );
121
+ export function getSubagentsToolDescription(): string {
122
+ return `${BASE_SUBAGENTS_TOOL_DESCRIPTION}\n\n${SUBAGENT_USAGE_GUIDANCE}`;
127
123
  }
128
124
 
129
- export function getSubagentsToolDescription(model?: unknown): string {
130
- return isGpt56Model(model)
131
- ? `${BASE_SUBAGENTS_TOOL_DESCRIPTION}\n\n${GPT_56_SUBAGENT_GUIDANCE}`
132
- : BASE_SUBAGENTS_TOOL_DESCRIPTION;
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
133
  }
134
134
 
135
135
  type ProjectAgentConfirmationSetting = "ask" | "never" | "session";
@@ -705,6 +705,8 @@ export function selectParentModelForSubagent(
705
705
  // ---------------------------------------------------------------------------
706
706
 
707
707
  export default function (pi: ExtensionAPI) {
708
+ let configuredToolPrompts = loadPiSubagentsConfig().toolPrompts;
709
+ let refreshRegisteredToolPrompts: ((cwd: string, includeProject: boolean) => void) | undefined;
708
710
  let resumeModelRegistry: any | undefined;
709
711
  let lastRestorableModel: any | undefined;
710
712
  let latestSessionCtx: any | undefined;
@@ -1395,6 +1397,9 @@ export default function (pi: ExtensionAPI) {
1395
1397
  lifecycleGeneration += 1;
1396
1398
  sessionActive = true;
1397
1399
  latestSessionCtx = ctx;
1400
+ const includeProjectConfig =
1401
+ typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted() === true;
1402
+ refreshRegisteredToolPrompts?.(ctx.cwd, includeProjectConfig);
1398
1403
  resumeModelRegistry = ctx.modelRegistry;
1399
1404
  clearSyntheticResumeState();
1400
1405
  pendingResumePlans = [];
@@ -1707,16 +1712,7 @@ export default function (pi: ExtensionAPI) {
1707
1712
  const agentList = discoveredAgents
1708
1713
  .map((a) => `- **${a.name}**: ${a.description}`)
1709
1714
  .join("\n");
1710
- return {
1711
- systemPrompt:
1712
- event.systemPrompt +
1713
- `\n\n## Available Subagents
1714
-
1715
- The following subagents are available via the \`subagents\` tool:
1716
-
1717
- ${agentList}
1718
-
1719
- ### How to call the subagents tool
1715
+ const subagentsGuidance = configuredToolPrompts[SUBAGENT_TOOL_NAME] ?? `### How to call the subagents tool
1720
1716
 
1721
1717
  Each subagent runs in an **isolated process**.
1722
1718
 
@@ -1738,9 +1734,10 @@ calls one after another. Do NOT put dependent tasks in the same array.
1738
1734
  \`\`\`
1739
1735
 
1740
1736
  - Max depth: current depth ${currentDepth}, max depth ${maxDepth}
1741
- - Max subagents per tool call: ${maxParallelTasks}
1742
- ${resumableSubagentsDisabled() ? "" : `
1743
- ### Resumable subagents
1737
+ - Max subagents per tool call: ${maxParallelTasks}`;
1738
+ const resumeGuidance = resumableSubagentsDisabled()
1739
+ ? ""
1740
+ : configuredToolPrompts[RESUME_SUBAGENTS_TOOL_NAME] ?? `### Resumable subagents
1744
1741
 
1745
1742
  Every subagent run is assigned a unique, durable name (e.g. \`code-writer-01\`,
1746
1743
  \`code-reviewer-02\`) which is returned together with its results. Use the
@@ -1756,8 +1753,13 @@ keeping their full previous context:
1756
1753
  - All resumes in one call run in parallel.
1757
1754
  - You may include subagent names in the task text you give YOUR OWN subagents,
1758
1755
  so they can resume those subagents themselves.
1759
- - Names survive restarts; you can resume them in a later session of this conversation.
1760
- `}`,
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}` : ""}`,
1761
1763
  };
1762
1764
  } catch (err) {
1763
1765
  console.error("[pi-subagent] Error in before_agent_start:", err);
@@ -1766,13 +1768,11 @@ keeping their full previous context:
1766
1768
 
1767
1769
  // Register the subagents tool
1768
1770
  if (canDelegate) {
1769
- let registeredForGpt56 = false;
1770
- const registerSubagentsTool = (model?: unknown) => {
1771
- registeredForGpt56 = isGpt56Model(model);
1771
+ const registerSubagentsTool = () => {
1772
1772
  pi.registerTool({
1773
1773
  name: SUBAGENT_TOOL_NAME,
1774
1774
  label: "Subagents",
1775
- description: getSubagentsToolDescription(model),
1775
+ description: configuredToolPrompts[SUBAGENT_TOOL_NAME] ?? getSubagentsToolDescription(),
1776
1776
  parameters: SubagentParams,
1777
1777
 
1778
1778
  async execute(toolCallId, params, signal, onUpdate, ctx) {
@@ -1989,22 +1989,12 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
1989
1989
  });
1990
1990
  };
1991
1991
 
1992
- registerSubagentsTool(latestSessionCtx?.model);
1993
- pi.on("model_select", (event) => {
1994
- if (registeredForGpt56 !== isGpt56Model(event.model)) {
1995
- registerSubagentsTool(event.model);
1996
- }
1997
- });
1998
- pi.on("before_agent_start", (_event, ctx) => {
1999
- if (registeredForGpt56 !== isGpt56Model(ctx.model)) {
2000
- registerSubagentsTool(ctx.model);
2001
- }
2002
- });
2003
-
2004
- if (!resumableSubagentsDisabled()) pi.registerTool({
1992
+ const registerResumeSubagentsTool = () => {
1993
+ if (resumableSubagentsDisabled()) return;
1994
+ pi.registerTool({
2005
1995
  name: RESUME_SUBAGENTS_TOOL_NAME,
2006
1996
  label: "Resume subagents",
2007
- description: [
1997
+ description: configuredToolPrompts[RESUME_SUBAGENTS_TOOL_NAME] ?? [
2008
1998
  "Resume previously run subagents by name with a new task, keeping their full context.",
2009
1999
  "",
2010
2000
  "Every subagent run returns a unique name (e.g. code-writer-01). Pass those names",
@@ -2207,7 +2197,24 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
2207
2197
  renderCall: (args, theme, context) => renderResumeCall(args, theme, context),
2208
2198
  renderResult: (result, { expanded }, theme) =>
2209
2199
  renderResult(result, expanded, theme),
2210
- });
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);
2211
2218
  }
2212
2219
 
2213
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.7",
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",