@d3ara1n/pi-subagent 0.10.2 → 0.10.4

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
@@ -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.2",
3
+ "version": "0.10.4",
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",
@@ -27,7 +27,7 @@
27
27
  }
28
28
  },
29
29
  "dependencies": {
30
- "@d3ara1n/pi-model-roles": "^0.7.0"
30
+ "@d3ara1n/pi-model-roles": "*"
31
31
  },
32
32
  "pi": {
33
33
  "extensions": [
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Configuration loading regression tests.
3
3
  *
4
- * Uses isolated global/project settings roots via PI_AGENT_DIR.
4
+ * Uses isolated global/project settings roots via PI_CODING_AGENT_DIR.
5
5
  * node --test packages/pi-subagent/src/config.test.ts
6
6
  */
7
7
 
@@ -13,7 +13,7 @@ import * as path from "node:path";
13
13
  import { loadSubagentConfig } from "./config.ts";
14
14
  import { DEFAULT_CONFIG } from "./types.ts";
15
15
 
16
- const originalAgentDir = process.env.PI_AGENT_DIR;
16
+ const originalAgentDir = process.env.PI_CODING_AGENT_DIR;
17
17
  const tempRoots: string[] = [];
18
18
 
19
19
  function makeRoot(): { agentDir: string; projectDir: string } {
@@ -23,7 +23,7 @@ function makeRoot(): { agentDir: string; projectDir: string } {
23
23
  const projectDir = path.join(root, "project");
24
24
  fs.mkdirSync(agentDir, { recursive: true });
25
25
  fs.mkdirSync(projectDir, { recursive: true });
26
- process.env.PI_AGENT_DIR = agentDir;
26
+ process.env.PI_CODING_AGENT_DIR = agentDir;
27
27
  return { agentDir, projectDir };
28
28
  }
29
29
 
@@ -37,8 +37,8 @@ function writeSettingsText(dir: string, content: string): void {
37
37
  }
38
38
 
39
39
  afterEach(() => {
40
- if (originalAgentDir === undefined) delete process.env.PI_AGENT_DIR;
41
- else process.env.PI_AGENT_DIR = originalAgentDir;
40
+ if (originalAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR;
41
+ else process.env.PI_CODING_AGENT_DIR = originalAgentDir;
42
42
  for (const root of tempRoots.splice(0)) fs.rmSync(root, { recursive: true, force: true });
43
43
  });
44
44
 
package/src/config.ts CHANGED
@@ -5,19 +5,13 @@
5
5
  * project overrides global.
6
6
  */
7
7
 
8
+ import { getAgentDir } from "@earendil-works/pi-coding-agent";
8
9
  import * as fs from "node:fs";
9
- import * as os from "node:os";
10
10
  import * as path from "node:path";
11
11
  import type { SubagentConfig } from "./types.ts";
12
12
  import { DEFAULT_CONFIG } from "./types.ts";
13
13
  import { normalizeNonNegativeInteger, normalizeNonNegativeNumber } from "./utils.ts";
14
14
 
15
- function getAgentDir(): string {
16
- const envDir = process.env.PI_AGENT_DIR;
17
- if (envDir) return envDir;
18
- return path.join(os.homedir(), ".pi", "agent");
19
- }
20
-
21
15
  function readSettingsFile(filePath: string): any {
22
16
  try {
23
17
  const content = fs.readFileSync(filePath, "utf-8");
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
- return {
222
- content: [
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
- return {
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
- const err = new Error(failedText) as Error & { [rethrowToToolRuntime]?: true };
526
- err[rethrowToToolRuntime] = true;
527
- throw err;
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 { ActivityEntry, SubagentRole, SubagentResult, ToolStatus } from "./types.ts";
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 || ""}`;