oira666_pi-subagent 0.3.6 → 0.3.7

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
@@ -79,8 +79,9 @@ Built-in agents are only used as a fallback when **all three** locations are emp
79
79
  ---
80
80
  name: writer
81
81
  description: Expert technical writer
82
- model: anthropic/claude-3-5-sonnet
83
82
  thinking: low
83
+ first-layer: enabled
84
+ last-layer: disabled
84
85
  tools: read,write
85
86
  ---
86
87
 
@@ -93,9 +94,11 @@ You are an expert technical writer focused on clarity and conciseness.
93
94
  | ------------- | -------- | -------------------- | -------------------------------------------------------- |
94
95
  | `name` | Yes | — | Agent identifier used in tool calls |
95
96
  | `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` |
97
+ | `model` | No | Current parent model | Legacy fallback only when live parent model context is unavailable |
97
98
  | `thinking` | No | Pi default | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` |
98
99
  | `tools` | No | `read,bash,edit,write` | Comma-separated built-in tools |
100
+ | `first-layer` | No | `enabled` | Set to `disabled` to hide/block this agent at depth 1 |
101
+ | `last-layer` | No | `enabled` | Set to `disabled` to hide/block this agent at max depth |
99
102
 
100
103
  Available tools: `read`, `bash`, `edit`, `write`.
101
104
 
@@ -103,7 +106,7 @@ The Markdown body becomes the agent's system prompt (appended to Pi's default, n
103
106
 
104
107
  ## Delegation Guards
105
108
 
106
- Depth and cycle guards prevent runaway recursive delegation.
109
+ 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
110
 
108
111
  | Config | Default | Description |
109
112
  | ------------------------------ | ------- | ------------------------------------------------ |
@@ -215,8 +218,7 @@ subagent *instance* by its unique name.
215
218
  ## CLI Argument Proxying
216
219
 
217
220
  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.
221
+ 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
222
 
221
223
  **Always forwarded verbatim:**
222
224
 
@@ -237,7 +239,7 @@ extension manages itself are blocked from being forwarded.
237
239
 
238
240
  | Flag | Overridden by |
239
241
  | --- | --- |
240
- | `--model` | `model:` in agent frontmatter |
242
+ | `--model` | Replaced at launch by the parent's currently active model (`model:` is only a no-context compatibility fallback) |
241
243
  | `--thinking` | `thinking:` in agent frontmatter |
242
244
  | `--tools` / `--no-tools` | `tools:` in agent frontmatter |
243
245
 
@@ -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
@@ -24,6 +24,10 @@ export interface AgentConfig {
24
24
  tools?: string[];
25
25
  model?: string;
26
26
  thinking?: string;
27
+ /** Whether this agent may be launched at delegation depth 1 (default: true). */
28
+ firstLayer?: boolean;
29
+ /** Whether this agent may be launched at the maximum delegation depth (default: true). */
30
+ lastLayer?: boolean;
27
31
  systemPrompt: string;
28
32
  source: AgentSource;
29
33
  filePath: string;
@@ -63,8 +67,25 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
63
67
  }
64
68
  }
65
69
 
70
+ function parseLayerSetting(
71
+ value: unknown,
72
+ field: "first-layer" | "last-layer",
73
+ filePath: string,
74
+ ): boolean {
75
+ if (value === undefined) return true;
76
+ if (typeof value === "string") {
77
+ const normalized = value.trim().toLowerCase();
78
+ if (normalized === "enabled") return true;
79
+ if (normalized === "disabled") return false;
80
+ }
81
+ console.warn(
82
+ `[pi-subagent] Ignoring invalid ${field} field in "${filePath}". Expected enabled or disabled.`,
83
+ );
84
+ return true;
85
+ }
86
+
66
87
  /** Parse a single agent markdown file into an AgentConfig. Returns null on skip. */
67
- function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
88
+ export function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | null {
68
89
  let content: string;
69
90
  try {
70
91
  content = fs.readFileSync(filePath, "utf-8");
@@ -113,6 +134,8 @@ function parseAgentFile(filePath: string, source: AgentSource): AgentConfig | nu
113
134
  tools,
114
135
  model: typeof frontmatter.model === "string" ? frontmatter.model : undefined,
115
136
  thinking: typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
137
+ firstLayer: parseLayerSetting(frontmatter["first-layer"], "first-layer", filePath),
138
+ lastLayer: parseLayerSetting(frontmatter["last-layer"], "last-layer", filePath),
116
139
  systemPrompt: body,
117
140
  source,
118
141
  filePath,
@@ -161,6 +184,17 @@ function dedupeAgents(
161
184
  // Public API
162
185
  // ---------------------------------------------------------------------------
163
186
 
187
+ /** Whether an agent is available to be launched at the requested child depth. */
188
+ export function isAgentEnabledAtLayer(
189
+ agent: AgentConfig,
190
+ targetDepth: number,
191
+ maxDepth: number,
192
+ ): boolean {
193
+ if (targetDepth === 1 && agent.firstLayer === false) return false;
194
+ if (targetDepth === maxDepth && agent.lastLayer === false) return false;
195
+ return true;
196
+ }
197
+
164
198
  /**
165
199
  * Discover all available agents according to the requested scope.
166
200
  *
package/index.ts CHANGED
@@ -18,7 +18,7 @@ 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
22
  import {
23
23
  allocateSubagentNames,
24
24
  clearResumeActive,
@@ -100,6 +100,38 @@ const SUBAGENT_STACK_ENV = "PI_SUBAGENT_STACK";
100
100
  const SUBAGENT_PREVENT_CYCLES_ENV = "PI_SUBAGENT_PREVENT_CYCLES";
101
101
  const SUBAGENT_CONFIRM_PROJECT_AGENTS_ENV = "PI_SUBAGENT_CONFIRM_PROJECT_AGENTS";
102
102
 
103
+ const BASE_SUBAGENTS_TOOL_DESCRIPTION = [
104
+ "Delegate work to specialized subagents running as isolated pi processes.",
105
+ "",
106
+ "Pass a `tasks` array. Every task in the same call runs IN PARALLEL.",
107
+ " - 1 task -> single delegation",
108
+ " - N tasks -> all N run concurrently in one call",
109
+ "",
110
+ "For sequential work (task B depends on task A's output), make separate",
111
+ "tool calls one after another. Do NOT put dependent tasks in the same array.",
112
+ "",
113
+ 'Single: { tasks: [{ agent: "writer", task: "Rewrite README.md" }] }',
114
+ 'Parallel: { tasks: [{ agent: "writer", task: "..." }, { agent: "tester", task: "..." }] }',
115
+ ].join("\n");
116
+
117
+ const GPT_56_SUBAGENT_GUIDANCE =
118
+ "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
+ 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
+ );
127
+ }
128
+
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;
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,6 +681,25 @@ 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
  // ---------------------------------------------------------------------------
@@ -664,6 +724,21 @@ export default function (pi: ExtensionAPI) {
664
724
  resumeState.trigger = "resumePrompt";
665
725
  }
666
726
 
727
+ function getParentModelForSubagent(ctx: any): any | undefined {
728
+ const currentModel = ctx?.model;
729
+ // Avoid historical lookup during normal calls: the current assistant
730
+ // response is the one that emitted the subagents tool call.
731
+ if (currentModel?.provider && currentModel.provider !== RESUME_PROVIDER) {
732
+ return currentModel;
733
+ }
734
+ return selectParentModelForSubagent(
735
+ currentModel,
736
+ modelToRestoreAfterResume,
737
+ findLastNonResumeModel(ctx) ?? getEnvFallbackModel(ctx),
738
+ lastRestorableModel,
739
+ );
740
+ }
741
+
667
742
  function scheduleSessionTask(callback: () => void, delayMs: number): void {
668
743
  const expectedGeneration = lifecycleGeneration;
669
744
  const timer = setTimeout(() => {
@@ -1342,7 +1417,7 @@ export default function (pi: ExtensionAPI) {
1342
1417
  if (!canDelegate) return;
1343
1418
 
1344
1419
  const discovery = discoverAgents(ctx.cwd, "both");
1345
- discoveredAgents = discovery.agents;
1420
+ discoveredAgents = filterAgentsForCurrentLayer(discovery.agents, currentDepth, maxDepth);
1346
1421
  currentSessionId = ctx.sessionManager.getSessionId?.() ?? "ephemeral";
1347
1422
  currentSubagentSessionRoot = getDefaultSubagentSessionRoot(ctx);
1348
1423
  if (resumableSubagentsDisabled()) {
@@ -1691,22 +1766,13 @@ keeping their full previous context:
1691
1766
 
1692
1767
  // Register the subagents tool
1693
1768
  if (canDelegate) {
1694
- pi.registerTool({
1769
+ let registeredForGpt56 = false;
1770
+ const registerSubagentsTool = (model?: unknown) => {
1771
+ registeredForGpt56 = isGpt56Model(model);
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: getSubagentsToolDescription(model),
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,6 +1986,19 @@ 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),
1989
+ });
1990
+ };
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
+ }
1924
2002
  });
1925
2003
 
1926
2004
  if (!resumableSubagentsDisabled()) pi.registerTool({
@@ -2102,7 +2180,7 @@ This guard prevents self-recursion and cyclic handoffs (for example A -> B -> A)
2102
2180
  undefined,
2103
2181
  (index) => targets[index].sessionDir,
2104
2182
  true,
2105
- formatModelFlag(modelToRestoreAfterResume ?? ctx.model ?? lastRestorableModel),
2183
+ formatModelFlag(getParentModelForSubagent(ctx)),
2106
2184
  topLevelBaseId,
2107
2185
  { names: targets.map((target) => target.name), rawPrompts: true },
2108
2186
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.3.6",
3
+ "version": "0.3.7",
4
4
  "description": "Subagent extension for Pi coding agent. Delegate tasks to specialized agents.",
5
5
  "type": "module",
6
6
  "main": "index.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;