@ryan_nookpi/pi-extension-subagent 0.5.1 → 0.5.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/activity.ts CHANGED
@@ -7,6 +7,7 @@
7
7
 
8
8
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
9
9
  import type { CommandRunState } from "./types.js";
10
+ import { getUsedContextPercent } from "./utils/format-utils.js";
10
11
 
11
12
  export const SUBAGENT_ACTIVITY_CUSTOM_TYPE = "subagent-activity";
12
13
  export const SUBAGENT_ACTIVITY_SCHEMA_VERSION = 1;
@@ -24,6 +25,7 @@ export interface ActivitySnapshot {
24
25
  toolCallCount: number;
25
26
  lastLine?: string;
26
27
  contextTokens?: number;
28
+ contextWindow?: number;
27
29
  }
28
30
 
29
31
  export interface ActivityPayload {
@@ -36,12 +38,15 @@ export interface ActivityPayload {
36
38
  toolCallCount: number;
37
39
  lastLine?: string;
38
40
  contextTokens?: number;
41
+ contextWindow?: number;
42
+ contextPercent?: number;
39
43
  }
40
44
 
41
45
  export interface ActivityThrottleState {
42
46
  lastEmittedAt: number;
43
47
  lastEmittedToolName?: string;
44
48
  lastEmittedToolCallCount?: number;
49
+ lastEmittedContextPercent?: number;
45
50
  }
46
51
 
47
52
  export function createActivityThrottleState(): ActivityThrottleState {
@@ -69,8 +74,8 @@ export function normalizeActivityLine(text: string | undefined): string | undefi
69
74
  /**
70
75
  * Change-driven throttle policy. Returns a payload to emit, or null.
71
76
  *
72
- * Emits only while the run is `running`, only when `lastToolName` or
73
- * `toolCallCount` changed since the last emission, and at most once per
77
+ * Emits only while the run is `running`, when tool metadata or the displayed
78
+ * context percentage changes, and at most once per
74
79
  * `ACTIVITY_MIN_INTERVAL_MS` per run. A change suppressed by the interval
75
80
  * stays pending and emits on a later evaluation.
76
81
  */
@@ -84,14 +89,18 @@ export function evaluateActivityEmission(
84
89
  // on the run state, so a positive count is required regardless of the name.
85
90
  if (snapshot.toolCallCount <= 0) return null;
86
91
 
92
+ const contextPercent = getUsedContextPercent(snapshot.contextTokens, snapshot.contextWindow);
87
93
  const changed =
88
- snapshot.lastToolName !== state.lastEmittedToolName || snapshot.toolCallCount !== state.lastEmittedToolCallCount;
94
+ snapshot.lastToolName !== state.lastEmittedToolName ||
95
+ snapshot.toolCallCount !== state.lastEmittedToolCallCount ||
96
+ contextPercent !== state.lastEmittedContextPercent;
89
97
  if (!changed) return null;
90
98
  if (state.lastEmittedAt !== 0 && now - state.lastEmittedAt < ACTIVITY_MIN_INTERVAL_MS) return null;
91
99
 
92
100
  state.lastEmittedAt = now;
93
101
  state.lastEmittedToolName = snapshot.lastToolName;
94
102
  state.lastEmittedToolCallCount = snapshot.toolCallCount;
103
+ state.lastEmittedContextPercent = contextPercent;
95
104
 
96
105
  const payload: ActivityPayload = {
97
106
  runId: snapshot.runId,
@@ -105,10 +114,12 @@ export function evaluateActivityEmission(
105
114
  const lastLine = normalizeActivityLine(snapshot.lastLine);
106
115
  if (lastLine) payload.lastLine = lastLine;
107
116
  if (snapshot.contextTokens != null && snapshot.contextTokens > 0) payload.contextTokens = snapshot.contextTokens;
117
+ if (snapshot.contextWindow != null && snapshot.contextWindow > 0) payload.contextWindow = snapshot.contextWindow;
118
+ if (contextPercent !== undefined) payload.contextPercent = contextPercent;
108
119
  return payload;
109
120
  }
110
121
 
111
- export function snapshotFromRunState(runState: CommandRunState): ActivitySnapshot {
122
+ export function snapshotFromRunState(runState: CommandRunState, contextWindow?: number): ActivitySnapshot {
112
123
  return {
113
124
  status: runState.status,
114
125
  runId: runState.id,
@@ -120,6 +131,7 @@ export function snapshotFromRunState(runState: CommandRunState): ActivitySnapsho
120
131
  toolCallCount: runState.toolCalls,
121
132
  lastLine: runState.lastLine,
122
133
  contextTokens: runState.usage?.contextTokens,
134
+ contextWindow,
123
135
  };
124
136
  }
125
137
 
@@ -130,11 +142,16 @@ export function snapshotFromRunState(runState: CommandRunState): ActivitySnapsho
130
142
  export function createRunActivityRecorder(
131
143
  pi: Pick<ExtensionAPI, "appendEntry">,
132
144
  runState: CommandRunState,
145
+ resolveContextWindow?: (model?: string) => number | undefined,
133
146
  ): () => void {
134
147
  const throttleState = createActivityThrottleState();
135
148
  return () => {
136
149
  try {
137
- const payload = evaluateActivityEmission(throttleState, snapshotFromRunState(runState), Date.now());
150
+ const payload = evaluateActivityEmission(
151
+ throttleState,
152
+ snapshotFromRunState(runState, resolveContextWindow?.(runState.model)),
153
+ Date.now(),
154
+ );
138
155
  if (!payload) return;
139
156
  pi.appendEntry(SUBAGENT_ACTIVITY_CUSTOM_TYPE, {
140
157
  schemaVersion: SUBAGENT_ACTIVITY_SCHEMA_VERSION,
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
@@ -35,7 +35,14 @@ import {
35
35
  shouldSummarizeSubagentTask,
36
36
  summarizeSubagentDisplayTask,
37
37
  } from "./display-task.js";
38
- import { AGENT_NAME_PALETTE, agentBgIndex, formatUsageStats, truncateLines, truncatePlainToWidth } from "./format.js";
38
+ import {
39
+ AGENT_NAME_PALETTE,
40
+ agentBgIndex,
41
+ formatUsageStats,
42
+ resolveContextWindow,
43
+ truncateLines,
44
+ truncatePlainToWidth,
45
+ } from "./format.js";
39
46
  import {
40
47
  clearPendingGroupCompletion,
41
48
  consumePendingGroupCompletionsForSession,
@@ -962,9 +969,15 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
962
969
  const lines = agents.map((agent) => {
963
970
  const model = agent.model ?? "(inherit current model)";
964
971
  const thinking = agent.thinking ?? "(inherit current thinking)";
965
- 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";
966
978
  const description = agent.description ? ` · ${agent.description}` : "";
967
- 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}`;
968
981
  });
969
982
 
970
983
  return {
@@ -1223,6 +1236,15 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1223
1236
  taskForAgent = taskForDisplay;
1224
1237
  }
1225
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
+
1226
1248
  let runId: number;
1227
1249
  let runState: CommandRunState;
1228
1250
 
@@ -1402,7 +1424,9 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1402
1424
  }, RUN_TICK_INTERVAL_MS);
1403
1425
 
1404
1426
  let claudeCheckpointSent = !!runState.claudeSessionId;
1405
- const recordActivity = hiddenFromMain ? () => {} : createRunActivityRecorder(pi, runState);
1427
+ const recordActivity = hiddenFromMain
1428
+ ? () => {}
1429
+ : createRunActivityRecorder(pi, runState, (model) => resolveContextWindow(ctx, model));
1406
1430
  void (async () => {
1407
1431
  try {
1408
1432
  const { result, retryCount } = await invokeWithAutoRetry({
@@ -1662,14 +1686,16 @@ export function registerAll(pi: ExtensionAPI, store: SubagentStore): SubagentReg
1662
1686
  if (starterPackNotice) ctx.ui.notify(starterPackNotice, "info");
1663
1687
 
1664
1688
  const lines = agents.map((a) => {
1665
- const tools = a.tools?.join(",") ?? "default";
1689
+ const tools =
1690
+ (a.toolsConfigured ?? a.tools !== undefined) ? (a.tools?.length ? a.tools.join(",") : "none") : "default";
1666
1691
  const model = a.model ?? "(inherit current model)";
1667
1692
  const thinking = a.thinking ?? "(inherit current thinking)";
1668
1693
  const description = a.description ? ` · ${a.description}` : "";
1694
+ const invalid = a.configurationError ? ` · INVALID: ${a.configurationError}` : "";
1669
1695
  const colorCode = AGENT_NAME_PALETTE[agentBgIndex(a.name)];
1670
1696
  const coloredName = `\x1b[38;5;${colorCode}m${a.name}\x1b[39m`;
1671
1697
  return truncateText(
1672
- `${coloredName} [${a.source}] · model: ${model} · thinking: ${thinking} · tools: ${tools}${description}`,
1698
+ `${coloredName} [${a.source}] · model: ${model} · thinking: ${thinking} · tools: ${tools}${description}${invalid}`,
1673
1699
  220,
1674
1700
  );
1675
1701
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ryan_nookpi/pi-extension-subagent",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
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") {
@@ -1319,7 +1350,7 @@ export function createSubagentToolExecute(pi: ExtensionAPI, store: SubagentStore
1319
1350
 
1320
1351
  function launchRunInBackground(runState: CommandRunState, taskForAgent: string): Promise<FinalizedRun> {
1321
1352
  let claudeCheckpointSent = !!runState.claudeSessionId;
1322
- const recordActivity = createRunActivityRecorder(pi, runState);
1353
+ const recordActivity = createRunActivityRecorder(pi, runState, (model) => resolveContextWindow(ctx, model));
1323
1354
  return enqueueSubagentInvocation(() =>
1324
1355
  runSingleAgent(
1325
1356
  ctx.cwd,
@@ -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 = {