@d3ara1n/pi-subagent 0.10.2 → 0.10.3
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 +6 -0
- package/package.json +1 -1
- package/src/index.ts +14 -22
- package/src/utils.test.ts +41 -0
- package/src/utils.ts +12 -1
package/README.md
CHANGED
|
@@ -47,6 +47,12 @@ This means:
|
|
|
47
47
|
- **Collapsed result**: `✓ explorer · Found login, registration, and token logic` + recent tool calls + usage stats
|
|
48
48
|
- **Expanded result** (Ctrl+O): Full task text, all tool calls, final output as rendered Markdown, and usage details
|
|
49
49
|
|
|
50
|
+
## Commands
|
|
51
|
+
|
|
52
|
+
| Command | Description |
|
|
53
|
+
|---------|-------------|
|
|
54
|
+
| `/subagent:doctor` | Diagnose pi invocation, model-role resolution, configuration, and role references |
|
|
55
|
+
|
|
50
56
|
## Dependencies
|
|
51
57
|
|
|
52
58
|
- [`@d3ara1n/pi-model-roles`](../pi-model-roles) — model role resolution
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@d3ara1n/pi-subagent",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Role-based subagent orchestration for pi — delegates tasks to specialized pi child processes with configurable model roles",
|
|
6
6
|
"main": "src/index.ts",
|
package/src/index.ts
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
AsyncSemaphore,
|
|
24
24
|
isProviderError,
|
|
25
25
|
effectiveTimeout,
|
|
26
|
+
hasFailedSubagentResult,
|
|
26
27
|
} from "./utils.ts";
|
|
27
28
|
import { persistSubagentHistory } from "./history.ts";
|
|
28
29
|
import { compressOutput, generateSummary } from "./output.ts";
|
|
@@ -169,6 +170,11 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
169
170
|
rebuildGuidelines(availableRoles);
|
|
170
171
|
});
|
|
171
172
|
|
|
173
|
+
pi.on("tool_result", (event) => {
|
|
174
|
+
if (event.toolName !== "delegate") return;
|
|
175
|
+
if (hasFailedSubagentResult(event.details)) return { isError: true };
|
|
176
|
+
});
|
|
177
|
+
|
|
172
178
|
pi.registerTool({
|
|
173
179
|
name: "delegate",
|
|
174
180
|
label: "Delegate to subagent",
|
|
@@ -218,16 +224,9 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
218
224
|
|
|
219
225
|
// Guard against bounded subagent nesting. A configured depth of 0 is unlimited.
|
|
220
226
|
if (config.maxDepth > 0 && CURRENT_DEPTH >= config.maxDepth) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
type: "text",
|
|
225
|
-
text: `Cannot delegate: maximum nesting depth (${config.maxDepth}) reached (current depth ${CURRENT_DEPTH}). Return a result to the caller instead of delegating further.`,
|
|
226
|
-
},
|
|
227
|
-
],
|
|
228
|
-
details: undefined as any,
|
|
229
|
-
isError: true,
|
|
230
|
-
};
|
|
227
|
+
throw new Error(
|
|
228
|
+
`Cannot delegate: maximum nesting depth (${config.maxDepth}) reached (current depth ${CURRENT_DEPTH}). Return a result to the caller instead of delegating further.`,
|
|
229
|
+
);
|
|
231
230
|
}
|
|
232
231
|
|
|
233
232
|
// Throttle state hoisted to the execute scope so the finally block can clear it.
|
|
@@ -285,16 +284,9 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
285
284
|
try {
|
|
286
285
|
await gate.acquire(signal);
|
|
287
286
|
} catch {
|
|
288
|
-
|
|
289
|
-
content: [
|
|
290
|
-
{ type: "text", text: `Subagent (${params.role}) was cancelled while queued.` },
|
|
291
|
-
],
|
|
292
|
-
details: { mode: "single", results: [] },
|
|
293
|
-
isError: true,
|
|
294
|
-
};
|
|
287
|
+
throw new Error(`Subagent (${params.role}) was cancelled while queued.`);
|
|
295
288
|
}
|
|
296
289
|
|
|
297
|
-
const rethrowToToolRuntime = Symbol("pi-subagent-rethrow");
|
|
298
290
|
try {
|
|
299
291
|
// Resolve model AFTER acquiring so the queued period stays zero-cost
|
|
300
292
|
let rolesApi: ModelRolesAPI;
|
|
@@ -522,9 +514,10 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
522
514
|
if (result.exitCode !== 0 || result.errorMessage) {
|
|
523
515
|
const failedText = `Subagent (${params.role}) failed: ${result.errorMessage || result.stderr || "unknown error"}\n\nPartial output:\n${result.output}`;
|
|
524
516
|
emitFinal([result], failedText);
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
517
|
+
return {
|
|
518
|
+
content: [{ type: "text", text: failedText }],
|
|
519
|
+
details: { mode: "single", results: [result] },
|
|
520
|
+
};
|
|
528
521
|
}
|
|
529
522
|
|
|
530
523
|
// Build concise output for the main model with usage info
|
|
@@ -544,7 +537,6 @@ export default function subagentExtension(pi: ExtensionAPI) {
|
|
|
544
537
|
details: { mode: "single", results: [result] },
|
|
545
538
|
};
|
|
546
539
|
} catch (err: any) {
|
|
547
|
-
if (err?.[rethrowToToolRuntime]) throw err;
|
|
548
540
|
const errorText = `Subagent (${params.role}) error: ${err.message || err}`;
|
|
549
541
|
emitFinal([], errorText);
|
|
550
542
|
throw new Error(errorText);
|
package/src/utils.test.ts
CHANGED
|
@@ -21,9 +21,50 @@ import {
|
|
|
21
21
|
formatTokens,
|
|
22
22
|
effectiveTimeout,
|
|
23
23
|
elapsedSeconds,
|
|
24
|
+
hasFailedSubagentResult,
|
|
24
25
|
} from "./utils.ts";
|
|
25
26
|
import type { SubagentResult, SubagentRole } from "./types.ts";
|
|
26
27
|
|
|
28
|
+
describe("subagent failure details", () => {
|
|
29
|
+
const baseResult = (overrides: Partial<SubagentResult> = {}): SubagentResult => ({
|
|
30
|
+
role: "worker",
|
|
31
|
+
task: "test task",
|
|
32
|
+
exitCode: 0,
|
|
33
|
+
messages: [],
|
|
34
|
+
output: "ok",
|
|
35
|
+
stderr: "",
|
|
36
|
+
usage: {
|
|
37
|
+
input: 0,
|
|
38
|
+
output: 0,
|
|
39
|
+
cacheRead: 0,
|
|
40
|
+
cacheWrite: 0,
|
|
41
|
+
cost: 0,
|
|
42
|
+
contextTokens: 0,
|
|
43
|
+
turns: 0,
|
|
44
|
+
},
|
|
45
|
+
activityLog: [],
|
|
46
|
+
...overrides,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("detects failed delegate results for tool_result error marking", () => {
|
|
50
|
+
assert.equal(
|
|
51
|
+
hasFailedSubagentResult({ mode: "single", results: [baseResult({ exitCode: 1 })] }),
|
|
52
|
+
true,
|
|
53
|
+
);
|
|
54
|
+
assert.equal(
|
|
55
|
+
hasFailedSubagentResult({ mode: "single", results: [baseResult({ stopReason: "timeout" })] }),
|
|
56
|
+
true,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("does not mark successful or malformed details as failed", () => {
|
|
61
|
+
assert.equal(hasFailedSubagentResult({ mode: "single", results: [baseResult()] }), false);
|
|
62
|
+
assert.equal(hasFailedSubagentResult({ mode: "single", results: [] }), false);
|
|
63
|
+
assert.equal(hasFailedSubagentResult(undefined), false);
|
|
64
|
+
assert.equal(hasFailedSubagentResult({}), false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
27
68
|
// ── sanitizeFilename: guards the path-injection fix ──
|
|
28
69
|
describe("sanitizeFilename", () => {
|
|
29
70
|
test("never yields a path separator (no directory traversal)", () => {
|
package/src/utils.ts
CHANGED
|
@@ -4,7 +4,13 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import * as os from "node:os";
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
ActivityEntry,
|
|
9
|
+
SubagentDetails,
|
|
10
|
+
SubagentRole,
|
|
11
|
+
SubagentResult,
|
|
12
|
+
ToolStatus,
|
|
13
|
+
} from "./types.ts";
|
|
8
14
|
|
|
9
15
|
/** Max output chars fed to the main model and the expanded TUI. Larger outputs are compressed (or truncated) to fit. */
|
|
10
16
|
export const MAX_OUTPUT_CHARS = 50_000;
|
|
@@ -194,6 +200,11 @@ export function isFailedResult(r: SubagentResult): boolean {
|
|
|
194
200
|
);
|
|
195
201
|
}
|
|
196
202
|
|
|
203
|
+
export function hasFailedSubagentResult(details: unknown): boolean {
|
|
204
|
+
const d = details as SubagentDetails | undefined;
|
|
205
|
+
return Array.isArray(d?.results) && d.results.some(isFailedResult);
|
|
206
|
+
}
|
|
207
|
+
|
|
197
208
|
/** Heuristic: does this result look like a provider-side failure worth retrying on the fallback role? */
|
|
198
209
|
export function isProviderError(result: SubagentResult): boolean {
|
|
199
210
|
const haystack = `${result.stderr || ""}\n${result.errorMessage || ""}`;
|