@ryan_nookpi/pi-extension-subagent 0.5.2 → 0.5.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/agents.ts CHANGED
@@ -21,6 +21,10 @@ export interface AgentConfig {
21
21
  name: string;
22
22
  description: string;
23
23
  tools?: string[];
24
+ /** Whether the tools field was explicitly present. Explicit [] disables all tools. */
25
+ toolsConfigured?: boolean;
26
+ /** Discovery-time validation failure. Invalid higher-priority definitions still mask lower-priority agents. */
27
+ configurationError?: string;
24
28
  model?: string;
25
29
  thinking?: AgentThinkingLevel;
26
30
  systemPrompt: string;
@@ -145,7 +149,14 @@ function loadAgentsFromDir(dir: string, source: "user" | "project", options: Loa
145
149
  if (typeof frontmatter.name !== "string" || typeof frontmatter.description !== "string") continue;
146
150
  if (!frontmatter.name || !frontmatter.description) continue;
147
151
 
148
- const tools = normalizeTools(frontmatter.tools, format);
152
+ const toolsConfigured = Object.hasOwn(frontmatter, "tools");
153
+ let tools: string[] | undefined;
154
+ let configurationError: string | undefined;
155
+ try {
156
+ tools = toolsConfigured ? normalizeTools(frontmatter.tools, format) : undefined;
157
+ } catch (error) {
158
+ configurationError = `${filePath}: ${error instanceof Error ? error.message : String(error)}`;
159
+ }
149
160
  const model = normalizeModel(typeof frontmatter.model === "string" ? frontmatter.model : undefined, format);
150
161
  const thinking = normalizeThinkingLevel(
151
162
  typeof frontmatter.thinking === "string" ? frontmatter.thinking : undefined,
@@ -156,6 +167,8 @@ function loadAgentsFromDir(dir: string, source: "user" | "project", options: Loa
156
167
  name: frontmatter.name,
157
168
  description: frontmatter.description,
158
169
  tools,
170
+ toolsConfigured,
171
+ configurationError,
159
172
  model,
160
173
  thinking,
161
174
  systemPrompt: attachCommonSubagentRule(body, runtime),
package/claude-args.ts CHANGED
@@ -5,6 +5,7 @@ import { mapPiToolsToClaude } from "./utils/agent-utils.js";
5
5
  export interface ClaudeArgsConfig {
6
6
  prompt: string;
7
7
  tools: string[];
8
+ toolsConfigured?: boolean;
8
9
  model?: string;
9
10
  thinking?: string;
10
11
  resumeSessionId?: string;
@@ -63,7 +64,7 @@ export function buildClaudeArgs(config: ClaudeArgsConfig): string[] {
63
64
 
64
65
  const claudeTools = mapPiToolsToClaude(config.tools);
65
66
 
66
- if (claudeTools.length > 0) {
67
+ if (config.toolsConfigured || claudeTools.length > 0) {
67
68
  args.push("--tools", claudeTools.join(","));
68
69
  args.push("--allowedTools", claudeTools.join(","));
69
70
  }
@@ -31,8 +31,12 @@ function normalizeTaskForSubagentPrompt(task: string): string {
31
31
  return task;
32
32
  }
33
33
 
34
- function resolveClaudeSdkBuiltInTools(requestedTools?: string[]): string[] | undefined {
35
- if (!requestedTools || requestedTools.length === 0) return undefined;
34
+ function resolveClaudeSdkBuiltInTools(
35
+ requestedTools: string[] | undefined,
36
+ toolsConfigured: boolean,
37
+ ): string[] | undefined {
38
+ if (!toolsConfigured) return undefined;
39
+ if (!requestedTools || requestedTools.length === 0) return [];
36
40
  return mapPiToolsToClaude(requestedTools);
37
41
  }
38
42
 
@@ -74,7 +78,7 @@ export async function runClaudeAgentViaSdk(
74
78
 
75
79
  let enabledTools: string[] | undefined;
76
80
  try {
77
- enabledTools = resolveClaudeSdkBuiltInTools(agent.tools);
81
+ enabledTools = resolveClaudeSdkBuiltInTools(agent.tools, agent.toolsConfigured ?? agent.tools !== undefined);
78
82
  } catch (error: any) {
79
83
  return {
80
84
  agent: agent.name,
@@ -140,7 +144,7 @@ export async function runClaudeAgentViaSdk(
140
144
  settingSources: [],
141
145
  includePartialMessages: true,
142
146
  persistSession: true,
143
- ...(enabledTools ? { tools: enabledTools, allowedTools: enabledTools } : {}),
147
+ ...(enabledTools !== undefined ? { tools: enabledTools, allowedTools: enabledTools } : {}),
144
148
  resume: resumeSessionId,
145
149
  stderr: (data) => {
146
150
  stderrBuf += data;
package/commands.ts CHANGED
@@ -969,9 +969,15 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
969
969
  const lines = agents.map((agent) => {
970
970
  const model = agent.model ?? "(inherit current model)";
971
971
  const thinking = agent.thinking ?? "(inherit current thinking)";
972
- const tools = agent.tools && agent.tools.length > 0 ? agent.tools.join(",") : "default";
972
+ const tools =
973
+ (agent.toolsConfigured ?? agent.tools !== undefined)
974
+ ? agent.tools?.length
975
+ ? agent.tools.join(",")
976
+ : "none"
977
+ : "default";
973
978
  const description = agent.description ? ` · ${agent.description}` : "";
974
- return `${agent.name} [${agent.source}] · model: ${model} · thinking: ${thinking} · tools: ${tools}${description}`;
979
+ const invalid = agent.configurationError ? ` · INVALID: ${agent.configurationError}` : "";
980
+ return `${agent.name} [${agent.source}] · model: ${model} · thinking: ${thinking} · tools: ${tools}${description}${invalid}`;
975
981
  });
976
982
 
977
983
  return {
@@ -1230,6 +1236,15 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1230
1236
  taskForAgent = taskForDisplay;
1231
1237
  }
1232
1238
 
1239
+ const selectedConfig = agents.find((agent) => agent.name === selectedAgent);
1240
+ if (selectedConfig?.configurationError) {
1241
+ ctx.ui.notify(
1242
+ `Invalid agent configuration for "${selectedAgent}": ${selectedConfig.configurationError}`,
1243
+ "error",
1244
+ );
1245
+ return;
1246
+ }
1247
+
1233
1248
  let runId: number;
1234
1249
  let runState: CommandRunState;
1235
1250
 
@@ -1671,14 +1686,16 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1671
1686
  if (starterPackNotice) ctx.ui.notify(starterPackNotice, "info");
1672
1687
 
1673
1688
  const lines = agents.map((a) => {
1674
- const tools = a.tools?.join(",") ?? "default";
1689
+ const tools =
1690
+ (a.toolsConfigured ?? a.tools !== undefined) ? (a.tools?.length ? a.tools.join(",") : "none") : "default";
1675
1691
  const model = a.model ?? "(inherit current model)";
1676
1692
  const thinking = a.thinking ?? "(inherit current thinking)";
1677
1693
  const description = a.description ? ` · ${a.description}` : "";
1694
+ const invalid = a.configurationError ? ` · INVALID: ${a.configurationError}` : "";
1678
1695
  const colorCode = AGENT_NAME_PALETTE[agentBgIndex(a.name)];
1679
1696
  const coloredName = `\x1b[38;5;${colorCode}m${a.name}\x1b[39m`;
1680
1697
  return truncateText(
1681
- `${coloredName} [${a.source}] · model: ${model} · thinking: ${thinking} · tools: ${tools}${description}`,
1698
+ `${coloredName} [${a.source}] · model: ${model} · thinking: ${thinking} · tools: ${tools}${description}${invalid}`,
1682
1699
  220,
1683
1700
  );
1684
1701
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.5.2",
3
+ "version": "0.5.4",
4
4
  "description": "Asynchronous subagent delegation for pi with run, batch, chain, and continuation workflows.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/runner.ts CHANGED
@@ -82,6 +82,12 @@ function appendStderrDiagnostic(result: SingleResult, message: string): void {
82
82
  result.stderr = result.stderr ? `${result.stderr.trimEnd()}\n${line}\n` : `${line}\n`;
83
83
  }
84
84
 
85
+ function normalizeChildExitCode(code: number | null, signal: NodeJS.Signals | null, fallbackCode = 0): number {
86
+ if (code !== null) return code;
87
+ if (signal !== null) return 1;
88
+ return fallbackCode;
89
+ }
90
+
85
91
  /**
86
92
  * Prevent tasks starting with `/` from being treated as slash commands
87
93
  * inside the spawned pi process.
@@ -225,6 +231,19 @@ export async function runSingleAgent(
225
231
  };
226
232
  }
227
233
 
234
+ if (agent.configurationError) {
235
+ return {
236
+ agent: agent.name,
237
+ agentSource: agent.source,
238
+ task,
239
+ exitCode: 1,
240
+ messages: [],
241
+ stderr: `Invalid agent configuration: ${agent.configurationError}`,
242
+ usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
243
+ step,
244
+ };
245
+ }
246
+
228
247
  if (agent.runtime === "claude") {
229
248
  if (resolveClaudeRuntimeMode(defaultCwd) === "sdk") {
230
249
  const { runClaudeAgentViaSdk } = await import("./claude-sdk-runner.js");
@@ -364,6 +383,7 @@ async function runClaudeAgent(
364
383
  const args = buildClaudeArgs({
365
384
  prompt: normalizeTaskForSubagentPrompt(task),
366
385
  tools: agent.tools ?? [],
386
+ toolsConfigured: agent.toolsConfigured ?? agent.tools !== undefined,
367
387
  model: agent.model,
368
388
  thinking: agent.thinking,
369
389
  resumeSessionId: resumeId,
@@ -545,7 +565,7 @@ async function runClaudeAgent(
545
565
 
546
566
  proc.on("exit", (code, terminationSignal) => {
547
567
  procExited = true;
548
- lastExitCode = code ?? 0;
568
+ lastExitCode = normalizeChildExitCode(code, terminationSignal, lastExitCode);
549
569
  lastProcessSignal = terminationSignal;
550
570
  diagnose({ event: "exit", childPid: proc.pid, code, signal: terminationSignal });
551
571
  if (streamState.resultReceived) {
@@ -553,6 +573,9 @@ async function runClaudeAgent(
553
573
  resolveOnce(0);
554
574
  return;
555
575
  }
576
+ if (terminationSignal && !settled && !wasAborted) {
577
+ stderrBuf += `[runner] process terminated by signal ${terminationSignal}\n`;
578
+ }
556
579
  exitFallbackTimer = setTimeout(() => {
557
580
  settleReason = "exit_fallback_timeout";
558
581
  resolveOnce(lastExitCode);
@@ -565,7 +588,7 @@ async function runClaudeAgent(
565
588
  diagnose({ event: "close", childPid: proc.pid, code, signal: terminationSignal });
566
589
  if (!settled) {
567
590
  settleReason = "close";
568
- resolveOnce(streamState.resultReceived ? 0 : (code ?? lastExitCode ?? 0));
591
+ resolveOnce(streamState.resultReceived ? 0 : normalizeChildExitCode(code, lastProcessSignal, lastExitCode));
569
592
  }
570
593
  });
571
594
 
@@ -640,7 +663,8 @@ async function runPiAgent(
640
663
  else args.push("--no-session");
641
664
  if (agent.model) args.push("--model", agent.model);
642
665
  if (agent.thinking) args.push("--thinking", agent.thinking);
643
- if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
666
+ if ((agent.toolsConfigured ?? agent.tools !== undefined) && agent.tools?.length === 0) args.push("--no-tools");
667
+ else if (agent.tools && agent.tools.length > 0) args.push("--tools", agent.tools.join(","));
644
668
 
645
669
  let tmpPromptDir: string | null = null;
646
670
  let tmpPromptPath: string | null = null;
@@ -1066,9 +1090,12 @@ async function runPiAgent(
1066
1090
 
1067
1091
  proc.on("exit", (code, terminationSignal) => {
1068
1092
  procExited = true;
1069
- lastExitCode = code ?? 0;
1093
+ lastExitCode = normalizeChildExitCode(code, terminationSignal, lastExitCode);
1070
1094
  lastProcessSignal = terminationSignal;
1071
1095
  diagnose({ event: "exit", childPid: proc.pid, code, signal: terminationSignal });
1096
+ if (terminationSignal && !settled && !wasAborted) {
1097
+ appendStderrDiagnostic(currentResult, `process terminated by signal ${terminationSignal}`);
1098
+ }
1072
1099
  // In rare cases stdout/stderr pipes may stay open after process exit.
1073
1100
  // Use a short fallback so runs cannot stay "running" forever.
1074
1101
  exitFallbackTimer = setTimeout(() => {
@@ -1082,7 +1109,7 @@ async function runPiAgent(
1082
1109
  lastProcessSignal = terminationSignal ?? lastProcessSignal;
1083
1110
  diagnose({ event: "close", childPid: proc.pid, code, signal: terminationSignal });
1084
1111
  settleReason = "close";
1085
- resolveOnce(code ?? lastExitCode ?? 0);
1112
+ resolveOnce(normalizeChildExitCode(code, lastProcessSignal, lastExitCode));
1086
1113
  });
1087
1114
 
1088
1115
  proc.on("error", (error) => {
package/tool-execute.ts CHANGED
@@ -9,7 +9,7 @@
9
9
  import * as fs from "node:fs";
10
10
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
11
11
  import { createRunActivityRecorder } from "./activity.js";
12
- import { discoverAgents } from "./agents.js";
12
+ import { type AgentConfig, discoverAgents } from "./agents.js";
13
13
  import { parseSubagentToolCommand, SUBAGENT_CLI_HELP_TEXT } from "./cli.js";
14
14
  import {
15
15
  DEFAULT_TURN_COUNT,
@@ -313,8 +313,15 @@ export function diagnoseResultFailure(result: SingleResult): ResultFailureDiagno
313
313
  };
314
314
  }
315
315
  }
316
- if (result.exitCode !== 0)
317
- return { failed: true, errorClass, reason: `Subagent process exited with code ${result.exitCode}.` };
316
+ if (result.exitCode !== 0) {
317
+ const detail = (result.errorMessage || result.stderr || "").trim();
318
+ const detailPreview = detail ? truncateLines(detail, 4).slice(0, STATUS_OUTPUT_PREVIEW_MAX_CHARS).trimEnd() : "";
319
+ return {
320
+ failed: true,
321
+ errorClass,
322
+ reason: `Subagent process exited with code ${result.exitCode}.${detailPreview ? ` Cause: ${detailPreview}` : ""}`,
323
+ };
324
+ }
318
325
  if (result.stopReason === "error")
319
326
  return { failed: true, errorClass, reason: result.errorMessage || "Subagent reported stopReason=error." };
320
327
  if (result.stopReason === "aborted")
@@ -810,9 +817,15 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
810
817
  const lines = agents.map((agent) => {
811
818
  const model = agent.model ?? "(inherit current model)";
812
819
  const thinking = agent.thinking ?? "(inherit current thinking)";
813
- const tools = agent.tools && agent.tools.length > 0 ? agent.tools.join(",") : "default";
820
+ const tools =
821
+ (agent.toolsConfigured ?? agent.tools !== undefined)
822
+ ? agent.tools?.length
823
+ ? agent.tools.join(",")
824
+ : "none"
825
+ : "default";
814
826
  const description = agent.description ? ` · ${agent.description}` : "";
815
- return `${agent.name} [${agent.source}] · model: ${model} · thinking: ${thinking} · tools: ${tools}${description}`;
827
+ const invalid = agent.configurationError ? ` · INVALID: ${agent.configurationError}` : "";
828
+ return `${agent.name} [${agent.source}] · model: ${model} · thinking: ${thinking} · tools: ${tools}${description}${invalid}`;
816
829
  });
817
830
 
818
831
  return {
@@ -902,6 +915,24 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
902
915
  isError: true,
903
916
  };
904
917
  }
918
+
919
+ const invalidAgents = [...new Set(requestedNames)]
920
+ .map((name) => agents.find((agent) => agent.name === name))
921
+ .filter((agent): agent is AgentConfig => Boolean(agent?.configurationError));
922
+ if (invalidAgents.length > 0) {
923
+ return {
924
+ content: [
925
+ {
926
+ type: "text",
927
+ text: `❌ Invalid agent configuration:\n${invalidAgents
928
+ .map((agent) => `- ${agent.name}: ${agent.configurationError}`)
929
+ .join("\n")}`,
930
+ },
931
+ ],
932
+ details: makeDetails(),
933
+ isError: true,
934
+ };
935
+ }
905
936
  }
906
937
 
907
938
  if (asyncAction === "list") {
@@ -1442,7 +1473,6 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1442
1473
  existingRunState: continueFromRun,
1443
1474
  });
1444
1475
  const launchSummary = toLaunchSummary(runState, continueFromRun ? "continue" : "run");
1445
- const startedState = continueFromRun ? "resumed" : "started";
1446
1476
 
1447
1477
  if (!shouldRunAsync) {
1448
1478
  let finalized: FinalizedRun;
@@ -1475,7 +1505,6 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1475
1505
  };
1476
1506
  }
1477
1507
 
1478
- pi.sendMessage(buildRunStartMessage(runState, startedState), { deliverAs: "followUp", triggerTurn: false });
1479
1508
  ctx.ui?.notify?.(
1480
1509
  `${continueFromRun ? `Resumed subagent #${runState.id}` : `Started subagent #${runState.id}`}: ${resolvedAgent}`,
1481
1510
  "info",
@@ -89,20 +89,38 @@ export const CLAUDE_MODEL_ALIAS_MAP: Record<string, string> = {
89
89
  * For "claude" format, maps Claude tool names to pi equivalents.
90
90
  */
91
91
  export function normalizeTools(rawTools: unknown, format: "pi" | "claude"): string[] | undefined {
92
- const parsed = (Array.isArray(rawTools) ? rawTools : typeof rawTools === "string" ? rawTools.split(",") : [])
93
- .filter((tool): tool is string => typeof tool === "string")
94
- .map((tool) => tool.trim())
95
- .filter(Boolean);
92
+ if (rawTools === undefined) return undefined;
96
93
 
97
- if (parsed.length === 0) return undefined;
98
- if (format === "pi") return parsed;
94
+ let parsed: string[];
95
+ if (Array.isArray(rawTools)) {
96
+ if (rawTools.some((tool) => typeof tool !== "string" || tool.trim().length === 0)) {
97
+ throw new Error("Explicit tools must contain only non-empty strings.");
98
+ }
99
+ parsed = rawTools.map((tool) => (tool as string).trim());
100
+ } else if (typeof rawTools === "string") {
101
+ parsed = rawTools
102
+ .split(",")
103
+ .map((tool) => tool.trim())
104
+ .filter(Boolean);
105
+ if (parsed.length === 0) throw new Error("Explicit tools must not be an empty string.");
106
+ } else {
107
+ throw new Error("Explicit tools must be a comma-separated string or an array of strings.");
108
+ }
99
109
 
100
- const mapped = parsed
101
- .map((tool) => CLAUDE_TOOL_MAP[tool.toLowerCase()] ?? undefined)
102
- .filter((tool): tool is string => Boolean(tool));
110
+ if (format === "pi") return Array.from(new Set(parsed));
103
111
 
104
- if (mapped.length === 0) return undefined;
105
- return Array.from(new Set(mapped));
112
+ const unsupported = parsed.filter((tool) => !CLAUDE_TOOL_MAP[tool.toLowerCase()]);
113
+ if (unsupported.length > 0) {
114
+ throw new Error(
115
+ `Unsupported Claude tool${unsupported.length > 1 ? "s" : ""}: ${unsupported.join(", ")}. Supported tools: ${Object.keys(
116
+ CLAUDE_TOOL_MAP,
117
+ )
118
+ .filter((tool) => CLAUDE_TOOL_MAP[tool])
119
+ .join(", ")}`,
120
+ );
121
+ }
122
+
123
+ return Array.from(new Set(parsed.map((tool) => CLAUDE_TOOL_MAP[tool.toLowerCase()] as string)));
106
124
  }
107
125
 
108
126
  /**
@@ -8,7 +8,7 @@ type SummaryResult = Awaited<ReturnType<typeof completeSimple>>;
8
8
  type AuthResult = {
9
9
  ok: boolean;
10
10
  apiKey?: string;
11
- headers?: Record<string, string>;
11
+ headers?: Record<string, string | null>;
12
12
  };
13
13
 
14
14
  export type ShortLabelContext = {