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 +3 -1
- package/agents.ts +21 -0
- package/index.ts +46 -8
- package/package.json +1 -1
- package/runner.ts +5 -11
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.
|
|
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 {
|
|
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 =
|
|
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
|
-
|
|
1727
|
-
|
|
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
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
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
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);
|