oira666_pi-subagent 0.3.12 → 0.3.13

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
@@ -130,7 +130,9 @@ The Markdown body becomes the agent's system prompt (appended to Pi's default, n
130
130
 
131
131
  ## Delegation Guards
132
132
 
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. Cycle checks are applied per task: in a mixed parallel call, cyclic tasks fail while legal siblings still run.
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. When cycle prevention is enabled, agents already in the current delegation stack are omitted from the child model's available-agent list. The runner still checks every task as a safety boundary: in a mixed parallel call, cyclic tasks fail while legal siblings still run.
134
+
135
+ A nested delegation failure is returned to its calling agent as a recoverable tool error. If that agent subsequently retries, completes the work itself, and produces a successful final answer, the earlier tool error does not incorrectly turn the completed agent—and all of its ancestors—into failures.
134
136
 
135
137
  | Config | Default | Description |
136
138
  | ------------------------------ | ------- | ------------------------------------------------ |
package/agents.ts CHANGED
@@ -200,6 +200,27 @@ export function isAgentEnabledAtLayer(
200
200
  return true;
201
201
  }
202
202
 
203
+ /**
204
+ * Return only agents that may be advertised for the next delegation.
205
+ *
206
+ * The runner still enforces cycle prevention as a final safety boundary, but
207
+ * forbidden agents must not be presented to the model as available choices.
208
+ */
209
+ export function filterAdvertisedAgents(
210
+ agents: AgentConfig[],
211
+ targetDepth: number,
212
+ maxDepth: number,
213
+ delegationStack: string[],
214
+ preventCycles: boolean,
215
+ ): AgentConfig[] {
216
+ const blocked = preventCycles ? new Set(delegationStack) : null;
217
+ return agents.filter(
218
+ (agent) =>
219
+ isAgentEnabledAtLayer(agent, targetDepth, maxDepth) &&
220
+ !blocked?.has(agent.name),
221
+ );
222
+ }
223
+
203
224
  /**
204
225
  * Discover all available agents according to the requested scope.
205
226
  *
package/index.ts CHANGED
@@ -18,7 +18,12 @@ import {
18
18
  lazyStream,
19
19
  } from "@mariozechner/pi-ai";
20
20
  import { Type } from "@sinclair/typebox";
21
- import { type AgentConfig, discoverAgents, isAgentEnabledAtLayer } from "./agents.js";
21
+ import {
22
+ type AgentConfig,
23
+ discoverAgents,
24
+ filterAdvertisedAgents,
25
+ isAgentEnabledAtLayer,
26
+ } from "./agents.js";
22
27
  import { loadPiSubagentsConfig } from "./config.js";
23
28
  import {
24
29
  allocateSubagentNames,
@@ -430,6 +435,22 @@ function filterAgentsForCurrentLayer(
430
435
  return agents.filter((agent) => isAgentEnabledAtLayer(agent, targetDepth, maxDepth));
431
436
  }
432
437
 
438
+ function filterAgentsForPrompt(
439
+ agents: AgentConfig[],
440
+ currentDepth: number,
441
+ maxDepth: number,
442
+ delegationStack: string[],
443
+ preventCycles: boolean,
444
+ ): AgentConfig[] {
445
+ return filterAdvertisedAgents(
446
+ agents,
447
+ currentDepth + 1,
448
+ maxDepth,
449
+ delegationStack,
450
+ preventCycles,
451
+ );
452
+ }
453
+
433
454
  function formatAgentNames(agents: AgentConfig[]): string {
434
455
  return agents.map((a) => `${a.name} (${a.source})`).join(", ") || "none";
435
456
  }
@@ -1424,7 +1445,13 @@ export default function (pi: ExtensionAPI) {
1424
1445
  if (!canDelegate) return;
1425
1446
 
1426
1447
  const discovery = discoverAgents(ctx.cwd, "both");
1427
- discoveredAgents = filterAgentsForCurrentLayer(discovery.agents, currentDepth, maxDepth);
1448
+ discoveredAgents = filterAgentsForPrompt(
1449
+ discovery.agents,
1450
+ currentDepth,
1451
+ maxDepth,
1452
+ ancestorAgentStack,
1453
+ preventCycles,
1454
+ );
1428
1455
  currentSessionId = ctx.sessionManager.getSessionId?.() ?? "ephemeral";
1429
1456
  currentSubagentSessionRoot = getDefaultSubagentSessionRoot(ctx);
1430
1457
  if (resumableSubagentsDisabled()) {
@@ -1720,11 +1747,12 @@ export default function (pi: ExtensionAPI) {
1720
1747
  pi.on("before_agent_start", async (event) => {
1721
1748
  try {
1722
1749
  if (!canDelegate) return;
1723
- if (discoveredAgents.length === 0) return;
1724
1750
 
1725
- const agentList = discoveredAgents
1726
- .map((a) => `- **${a.name}**: ${a.description}`)
1727
- .join("\n");
1751
+ const agentList = discoveredAgents.length > 0
1752
+ ? discoveredAgents
1753
+ .map((a) => `- **${a.name}**: ${a.description}`)
1754
+ .join("\n")
1755
+ : "_No agents are available for the next delegation layer. Do not call the subagents tool._";
1728
1756
  const subagentsGuidance = configuredToolPrompts[SUBAGENT_TOOL_NAME] ?? `### How to call the subagents tool
1729
1757
 
1730
1758
  Each subagent runs in an **isolated process**.
@@ -1746,8 +1774,18 @@ calls one after another. Do NOT put dependent tasks in the same array.
1746
1774
  { "tasks": [{ "agent": "agent-a", "task": "..." }, { "agent": "agent-b", "task": "..." }] }
1747
1775
  \`\`\`
1748
1776
 
1749
- - Max depth: current depth ${currentDepth}, max depth ${maxDepth}
1750
1777
  - Max subagents per tool call: ${maxParallelTasks}`;
1778
+ const delegationStackText = ancestorAgentStack.length > 0
1779
+ ? ancestorAgentStack.join(" -> ")
1780
+ : "(root)";
1781
+ const delegationGuardGuidance = `### Delegation guards
1782
+
1783
+ - Current depth: ${currentDepth}; max depth: ${maxDepth}
1784
+ - Cycle prevention: ${preventCycles ? "enabled" : "disabled"}
1785
+ - Current delegation stack: ${delegationStackText}
1786
+ ${preventCycles
1787
+ ? "- Agents already in this stack are intentionally omitted from the available list. Do not request omitted agent names."
1788
+ : "- Cyclic delegation is allowed by configuration."}`;
1751
1789
  const resumeGuidance = resumableSubagentsDisabled()
1752
1790
  ? ""
1753
1791
  : configuredToolPrompts[RESUME_SUBAGENTS_TOOL_NAME] ?? `### Resumable subagents
@@ -1772,7 +1810,7 @@ keeping their full previous context:
1772
1810
 
1773
1811
  The following subagents are available via the \`subagents\` tool:
1774
1812
 
1775
- ${agentList}\n\n${subagentsGuidance}${resumeGuidance ? `\n\n${resumeGuidance}` : ""}`,
1813
+ ${agentList}\n\n${subagentsGuidance}\n\n${delegationGuardGuidance}${resumeGuidance ? `\n\n${resumeGuidance}` : ""}`,
1776
1814
  };
1777
1815
  } catch (err) {
1778
1816
  console.error("[pi-subagent] Error in before_agent_start:", err);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oira666_pi-subagent",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
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
@@ -21,7 +21,6 @@ import {
21
21
  emptyUsage,
22
22
  extractToolCalls,
23
23
  getFinalOutput,
24
- getNestedSubagentErrorSummary,
25
24
  isResultError,
26
25
  isResultSuccess,
27
26
  isSubagentDetails,
@@ -1239,16 +1238,11 @@ export async function runAgentSubprocess(opts: RunAgentOptions): Promise<SingleR
1239
1238
  if (!result.stderr.trim()) result.stderr = result.errorMessage;
1240
1239
  }
1241
1240
 
1242
- if (result.exitCode === 0) {
1243
- const nestedErrorSummary = getNestedSubagentErrorSummary(result.messages);
1244
- if (nestedErrorSummary) {
1245
- result.exitCode = 1;
1246
- result.stopReason = "error";
1247
- result.errorMessage = nestedErrorSummary;
1248
- if (!result.stderr.trim()) result.stderr = nestedErrorSummary;
1249
- }
1250
- }
1251
-
1241
+ // A failed nested delegation is a recoverable tool error, just like a
1242
+ // failed bash/read call. Pi returns that error to the calling model, which
1243
+ // may retry, choose another approach, or finish the task itself. Do not
1244
+ // overwrite a later successful terminal answer with an earlier nested
1245
+ // tool failure.
1252
1246
  return result;
1253
1247
  } catch (err) {
1254
1248
  const msg = err instanceof Error ? err.message : String(err);