@mystilleef/pi-subagent 0.12.0 → 0.12.2

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
@@ -190,7 +190,8 @@ replace_prompt: true
190
190
  `AGENTS.md`. Setting `false` excludes default workspace files from the
191
191
  child context window.
192
192
  - `thinking`: Controls the model thinking level (values: `off`,
193
- `minimal`, `low`, `medium`, `high`, `xhigh`). The child runner clamps
193
+ `minimal`, `low`, `medium`, `high`, `xhigh`, `max`). `xhigh` and `max`
194
+ are model-specific, opt-in levels. The child runner clamps
194
195
  unsupported levels to supported values and prints warnings.
195
196
  - `provider`: Specifies the model provider. Requires setting the `model`
196
197
  field. Omission of the `model` field when defining a `provider`
@@ -282,7 +283,7 @@ decimal, `Infinity`, and non-numeric values fall back to defaults.
282
283
 
283
284
  **Missing agent:**
284
285
 
285
- - Confirm the file lives under `~/.pi/agents/` or the nearest
286
+ - Confirm the file lives under `~/.pi/agent/agents/` or the nearest
286
287
  `.pi/agents/`.
287
288
  - Confirm `frontmatter` includes `name` and `description`.
288
289
  - Confirm `/run` uses the `name` value, not the filename.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mystilleef/pi-subagent",
3
- "version": "0.12.0",
3
+ "version": "0.12.2",
4
4
  "description": "Pi subagent for the SPAE Framework",
5
5
  "author": "Lateef Alabi-Oki <mystilleef@gmail.com>",
6
6
  "license": "MIT",
@@ -64,14 +64,14 @@
64
64
  "typebox": "*"
65
65
  },
66
66
  "devDependencies": {
67
- "@biomejs/biome": "^2.5.2",
68
- "@earendil-works/pi-agent-core": "^0.80.3",
69
- "@earendil-works/pi-ai": "^0.80.3",
70
- "@earendil-works/pi-coding-agent": "^0.80.3",
71
- "@earendil-works/pi-tui": "^0.80.3",
67
+ "@biomejs/biome": "^2.5.8",
68
+ "@earendil-works/pi-agent-core": "^0.84.2",
69
+ "@earendil-works/pi-ai": "^0.84.2",
70
+ "@earendil-works/pi-coding-agent": "^0.84.2",
71
+ "@earendil-works/pi-tui": "^0.84.2",
72
72
  "@types/bun": "^1.3.14",
73
- "@types/node": "^26.1.0",
74
- "typebox": "^1.3.4",
75
- "typescript": "^6.0.3"
73
+ "@types/node": "^26.2.0",
74
+ "typebox": "^1.3.14",
75
+ "typescript": "^7.0.2"
76
76
  }
77
77
  }
@@ -14,6 +14,7 @@ const THINKING_LEVELS = [
14
14
  "medium",
15
15
  "high",
16
16
  "xhigh",
17
+ "max",
17
18
  ] as const;
18
19
 
19
20
  export type ThinkingLevel = (typeof THINKING_LEVELS)[number];
@@ -4,8 +4,10 @@
4
4
  */
5
5
 
6
6
  import {
7
+ type Api,
7
8
  clampThinkingLevel,
8
9
  getSupportedThinkingLevels,
10
+ type Model,
9
11
  type ModelThinkingLevel,
10
12
  } from "@earendil-works/pi-ai";
11
13
  import { getModel } from "@earendil-works/pi-ai/compat";
@@ -17,22 +19,87 @@ export type ChildModelSettings = {
17
19
  };
18
20
 
19
21
  /**
20
- * Resolves the effective thinking level for a model, clamping to supported levels.
21
- * Returns a warning message if the requested level differs from the effective level.
22
+ * Minimal live-registry seam consumed by thinking-level resolution.
23
+ * Accepts the public {@link ModelRegistry} from the host extension context
24
+ * or any test double that provides the same lookup contract.
22
25
  */
23
- export function resolveThinkingLevel(
26
+ export interface ModelRegistry {
27
+ find(provider: string, modelId: string): Model<Api> | undefined;
28
+ }
29
+
30
+ export interface ResolveThinkingLevelOptions {
31
+ registry?: ModelRegistry | undefined;
32
+ }
33
+
34
+ export interface ResolvedThinkingLevel {
35
+ level: ThinkingLevel;
36
+ warning?: string | undefined;
37
+ diagnostic?: string | undefined;
38
+ }
39
+
40
+ const FALLBACK_LEVEL: ThinkingLevel = "off";
41
+
42
+ function formatProviderModelLabel(
43
+ provider: string | undefined,
44
+ modelId: string | undefined,
45
+ ): string {
46
+ const providerLabel = provider ?? "unknown";
47
+ const modelLabel = modelId ?? "unknown";
48
+ return `(provider: ${providerLabel}, model: ${modelLabel})`;
49
+ }
50
+
51
+ function formatUnsupportedWarning(
24
52
  requested: ThinkingLevel,
53
+ effective: ThinkingLevel,
54
+ provider: string | undefined,
55
+ modelId: string | undefined,
56
+ ): string {
57
+ return `Thinking level "${requested}" is not supported; using "${effective}" instead ${formatProviderModelLabel(provider, modelId)}`;
58
+ }
59
+
60
+ function formatUnconfirmedWarning(
61
+ requested: ThinkingLevel,
62
+ provider: string | undefined,
63
+ modelId: string | undefined,
64
+ ): string {
65
+ return `Thinking level "${requested}" support could not be confirmed; requesting as-is ${formatProviderModelLabel(provider, modelId)}`;
66
+ }
67
+
68
+ function resolveModel(
25
69
  provider: string,
26
70
  modelId: string,
27
- ): { level: ThinkingLevel; warning?: string } {
28
- const model = getModel(provider as never, modelId as never);
29
- if (!model) return { level: requested };
30
- const mkWarning = (effective: ThinkingLevel) =>
31
- `Thinking level "${requested}" not supported by model "${provider}/${modelId}"; using "${effective}" instead`;
71
+ registry: ModelRegistry | undefined,
72
+ ): { model: Model<Api> | undefined; diagnostic: string | undefined } {
73
+ if (registry) {
74
+ const live = registry.find(provider, modelId);
75
+ if (live) return { model: live, diagnostic: undefined };
76
+ }
77
+ const diagnostic = registry
78
+ ? undefined
79
+ : "Live model registry unavailable; falling back to static catalog.";
80
+ const staticModel = getModel(provider as never, modelId as never) as
81
+ | Model<Api>
82
+ | undefined;
83
+ return { model: staticModel, diagnostic };
84
+ }
85
+
86
+ function resolveForModel(
87
+ requested: ThinkingLevel,
88
+ provider: string | undefined,
89
+ modelId: string | undefined,
90
+ model: Model<Api>,
91
+ ): ResolvedThinkingLevel {
32
92
  if (model.reasoning === false) {
33
- return { level: "off", warning: mkWarning("off") };
93
+ return {
94
+ level: FALLBACK_LEVEL,
95
+ warning: formatUnsupportedWarning(
96
+ requested,
97
+ FALLBACK_LEVEL,
98
+ provider,
99
+ modelId,
100
+ ),
101
+ };
34
102
  }
35
- if (!model.thinkingLevelMap) return { level: requested };
36
103
  const supported = getSupportedThinkingLevels(model);
37
104
  if (supported.length === 0) return { level: requested };
38
105
  const clamped = clampThinkingLevel(
@@ -40,7 +107,54 @@ export function resolveThinkingLevel(
40
107
  requested as ModelThinkingLevel,
41
108
  ) as ThinkingLevel;
42
109
  if (clamped === requested) return { level: requested };
43
- return { level: clamped, warning: mkWarning(clamped) };
110
+ return {
111
+ level: clamped,
112
+ warning: formatUnsupportedWarning(requested, clamped, provider, modelId),
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Resolves the effective thinking level for a model, preferring the live
118
+ * registry over the static catalog. Confirmed matches (live or static) clamp
119
+ * unsupported levels with the standard unsupported-level warning. Unconfirmed
120
+ * misses—missing provider or model identifiers, missing/unavailable registries,
121
+ * and live or static catalog misses—preserve the requested level for display
122
+ * while warning that support could not be confirmed. Requested `off` always
123
+ * stays warning-free in unconfirmed paths. Registry diagnostics are returned
124
+ * separately and never included in the user-facing warning.
125
+ */
126
+ export function resolveThinkingLevel(
127
+ requested: ThinkingLevel,
128
+ provider: string | undefined,
129
+ modelId: string | undefined,
130
+ options?: ResolveThinkingLevelOptions,
131
+ ): ResolvedThinkingLevel {
132
+ if (!provider || !modelId) {
133
+ if (requested === FALLBACK_LEVEL) return { level: FALLBACK_LEVEL };
134
+ return {
135
+ level: requested,
136
+ warning: formatUnconfirmedWarning(requested, provider, modelId),
137
+ };
138
+ }
139
+ const { model, diagnostic } = resolveModel(
140
+ provider,
141
+ modelId,
142
+ options?.registry,
143
+ );
144
+ if (!model) {
145
+ if (requested === FALLBACK_LEVEL) {
146
+ return diagnostic
147
+ ? { level: FALLBACK_LEVEL, diagnostic }
148
+ : { level: FALLBACK_LEVEL };
149
+ }
150
+ return {
151
+ level: requested,
152
+ warning: formatUnconfirmedWarning(requested, provider, modelId),
153
+ diagnostic,
154
+ };
155
+ }
156
+ const result = resolveForModel(requested, provider, modelId, model);
157
+ return diagnostic ? { ...result, diagnostic } : result;
44
158
  }
45
159
 
46
160
  /**
@@ -90,7 +90,7 @@ export function truncateValidUtf8(buffer: Buffer, max: number): string {
90
90
  /**
91
91
  * Resolves the context window size in tokens for a given message.
92
92
  * Returns undefined if the message doesn't have valid provider/model info
93
- * or if the model lookup fails.
93
+ * or the model isn't in the built-in catalog.
94
94
  *
95
95
  * Rationale: Subagent usage reporting needs context window awareness to provide
96
96
  * meaningful "context full" indicators to the parent.
@@ -99,16 +99,11 @@ export function resolveContextWindowTokens(msg: Message): number | undefined {
99
99
  const m = msg as unknown as Record<string, unknown>;
100
100
  if (typeof m["provider"] !== "string" || typeof m["model"] !== "string")
101
101
  return;
102
- try {
103
- const contextWindow = getModel(
104
- m["provider"] as never,
105
- m["model"] as never,
106
- )?.contextWindow;
107
- return Number.isFinite(contextWindow) && contextWindow > 0
108
- ? contextWindow
109
- : undefined;
110
- } catch {
111
- /* model lookup failures return undefined to skip context window tracking */
112
- return;
113
- }
102
+ const contextWindow = getModel(
103
+ m["provider"] as never,
104
+ m["model"] as never,
105
+ )?.contextWindow;
106
+ return Number.isFinite(contextWindow) && contextWindow > 0
107
+ ? contextWindow
108
+ : undefined;
114
109
  }
@@ -9,6 +9,11 @@ import readline from "node:readline";
9
9
  import type { Message } from "@earendil-works/pi-ai";
10
10
  import type { AgentConfig, ThinkingLevel } from "../agent/agents.js";
11
11
  import { getFinalOutput } from "../output/ui.js";
12
+ import {
13
+ getPiInvocation,
14
+ getSubagentDepth,
15
+ subagentDepthEnv,
16
+ } from "../shared/invocation.js";
12
17
  import { serializeSamplingParams } from "../shared/sampling.js";
13
18
  import {
14
19
  type OnUpdateCallback,
@@ -18,12 +23,9 @@ import {
18
23
  } from "../shared/types.js";
19
24
  import {
20
25
  detectMessageError,
21
- getPiInvocation,
22
- getSubagentDepth,
23
26
  getSubagentRuntimeLimits,
24
27
  resolveAgentExtensionPaths,
25
28
  resolveAgentSkillArgs,
26
- subagentDepthEnv,
27
29
  } from "../shared/utils.js";
28
30
  import {
29
31
  type ChildEventParseResult,
@@ -35,6 +37,7 @@ import { getLatestOutcomeFromMessages } from "./complete-outcome.js";
35
37
  import {
36
38
  buildModelDisplay,
37
39
  type ChildModelSettings,
40
+ type ModelRegistry,
38
41
  resolveEffectiveChildModelSettings,
39
42
  resolveThinkingLevel,
40
43
  } from "./model-resolution.js";
@@ -73,7 +76,6 @@ const COMPLETE_EXTENSION_PATH = resolveCompleteExtensionPath();
73
76
  const SAMPLING_EXTENSION_PATH = resolveSamplingExtensionPath();
74
77
  const PACKAGE_EXTENSION_PATH = resolvePackageExtensionPath();
75
78
 
76
- export { resolveThinkingLevel } from "./model-resolution.js";
77
79
  export { makeEmitUpdate } from "./streaming-progress.js";
78
80
 
79
81
  type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
@@ -82,6 +84,7 @@ type SleepInhibitorAcquirer = (pid: number) => Promise<SleepInhibitorHandle>;
82
84
  type RunSingleAgentOptions = {
83
85
  acquireSleepInhibitor?: SleepInhibitorAcquirer;
84
86
  getOrchestratorPid?: () => unknown;
87
+ registry?: ModelRegistry | undefined;
85
88
  };
86
89
 
87
90
  export type RunSingleAgentResult =
@@ -583,14 +586,15 @@ export async function runSingleAgent(
583
586
  }
584
587
  const requestedThinking = agent.thinking ?? parentThinking;
585
588
  const effectiveModel = resolveEffectiveChildModelSettings(agent, parentModel);
586
- const { level: thinking, warning: thinkingWarning } =
587
- effectiveModel.provider && effectiveModel.id
588
- ? resolveThinkingLevel(
589
- requestedThinking,
590
- effectiveModel.provider,
591
- effectiveModel.id,
592
- )
593
- : { level: requestedThinking };
589
+ const resolvedThinking = resolveThinkingLevel(
590
+ requestedThinking,
591
+ effectiveModel.provider,
592
+ effectiveModel.id,
593
+ { registry: options.registry },
594
+ );
595
+ const thinking = resolvedThinking.level;
596
+ const thinkingWarning = resolvedThinking.warning;
597
+ if (resolvedThinking.diagnostic) console.warn(resolvedThinking.diagnostic);
594
598
  const modelDisplay = buildModelDisplay(effectiveModel, thinking);
595
599
  const resolvedSkillsPromise: Promise<{ args: string[] } | { error: string }> =
596
600
  agent.skills
@@ -659,7 +663,11 @@ export async function runSingleAgent(
659
663
  agent,
660
664
  task,
661
665
  effectiveModel,
662
- thinking,
666
+ // Intentionally the raw request, not `thinking`: resolveThinkingLevel's
667
+ // clamp is only an estimate when the model isn't a confirmed live-registry
668
+ // hit, and `pi` itself makes the authoritative call. `thinking`/`thinkingWarning`
669
+ // still drive the user-facing display.
670
+ thinking: requestedThinking,
663
671
  resolvedSkills,
664
672
  tmpPrompt,
665
673
  resolvedExtensionPaths:
@@ -1,6 +1,24 @@
1
1
  import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
2
4
  import type { AgentConfig } from "../agent/agents.js";
3
- import { writePromptToTempFile } from "../shared/utils.js";
5
+
6
+ export async function writePromptToTempFile(
7
+ agentName: string,
8
+ prompt: string,
9
+ ): Promise<{ dir: string; filePath: string }> {
10
+ const tmpDir = await fs.promises.mkdtemp(
11
+ path.join(os.tmpdir(), "pi-subagent-"),
12
+ );
13
+ const safeName = agentName.replace(/[^\w.-]+/g, "_");
14
+ const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
15
+ // mkdtemp guarantees a unique directory per call; no concurrent writer can hold this path.
16
+ await fs.promises.writeFile(filePath, prompt, {
17
+ encoding: "utf-8",
18
+ mode: 0o600,
19
+ });
20
+ return { dir: tmpDir, filePath };
21
+ }
4
22
 
5
23
  export type TempPrompt = { dir: string; filePath: string };
6
24
 
@@ -11,6 +11,7 @@ import type {
11
11
  AgentScope,
12
12
  ThinkingLevel,
13
13
  } from "../agent/agents.js";
14
+ import type { ModelRegistry as ChildModelRegistry } from "../child/model-resolution.js";
14
15
  import { runSingleAgent } from "../child/process.js";
15
16
  import { deliverNotification } from "../notification/delivery.js";
16
17
  import {
@@ -39,13 +40,14 @@ import {
39
40
  sanitizeResultDetails,
40
41
  } from "../progress/result-details.js";
41
42
  import { generateSubagentInstanceName } from "../shared/instance-name.js";
43
+ import { getSubagentDepth } from "../shared/invocation.js";
42
44
  import type {
43
45
  OnUpdateCallback,
44
46
  SingleResult,
45
47
  SubagentDetails,
46
48
  SubagentToolResult,
47
49
  } from "../shared/types.js";
48
- import { getSubagentDepth, hasSubagentFailed } from "../shared/utils.js";
50
+ import { hasSubagentFailed } from "../shared/utils.js";
49
51
  import {
50
52
  listRunJobs,
51
53
  type RunJob,
@@ -102,6 +104,7 @@ interface LifecycleContext {
102
104
  task: string;
103
105
  parentModel: { provider: string; id: string } | undefined;
104
106
  parentThinking: ThinkingLevel;
107
+ registry?: ChildModelRegistry;
105
108
  hostOnUpdate?: AgentToolUpdateCallback<SubagentDetails> | undefined;
106
109
  }
107
110
 
@@ -288,6 +291,7 @@ async function runSubagentLifecycle(
288
291
  lc.parentModel,
289
292
  lc.parentThinking,
290
293
  lc.debug,
294
+ { registry: lc.registry },
291
295
  );
292
296
  if (outcome.kind === "aborted") {
293
297
  const details = lc.makeDetails([outcome.result]);
@@ -474,6 +478,7 @@ async function prepareSubagentJob(
474
478
  task,
475
479
  parentModel,
476
480
  parentThinking,
481
+ registry: ctx.modelRegistry,
477
482
  hostOnUpdate,
478
483
  },
479
484
  instanceName,
package/src/output/ui.ts CHANGED
@@ -72,6 +72,40 @@ export function formatTokens(count: number): string {
72
72
  return `${(count / 1000000).toFixed(1)}M`;
73
73
  }
74
74
 
75
+ /**
76
+ * Formats the plural-aware turn count string.
77
+ */
78
+ function formatTurns(turns: number): string {
79
+ return `${turns} turn${turns > 1 ? "s" : ""}`;
80
+ }
81
+
82
+ function formatIoTokens(input: number, output: number): string {
83
+ const tokens: string[] = [];
84
+ if (input) tokens.push(`↑${formatTokens(input)}`);
85
+ if (output) tokens.push(`↓${formatTokens(output)}`);
86
+ return tokens.join(" ");
87
+ }
88
+
89
+ function formatCacheStats(cacheRead: number, cacheWrite: number): string {
90
+ const cache: string[] = [];
91
+ if (cacheRead) cache.push(`R${formatTokens(cacheRead)}`);
92
+ if (cacheWrite) cache.push(`W${formatTokens(cacheWrite)}`);
93
+ return `cache:${cache.join("/")}`;
94
+ }
95
+
96
+ /**
97
+ * Builds common usage stat parts: turns, context tokens, and cost.
98
+ * Shared between formatUsageStats and formatResultFooter.
99
+ */
100
+ function formatUsageCore(usage: UsageStats): string[] {
101
+ const parts: string[] = [];
102
+ if (usage.turns) parts.push(formatTurns(usage.turns));
103
+ if (usage.contextTokens && usage.contextTokens > 0)
104
+ parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
105
+ if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
106
+ return parts;
107
+ }
108
+
75
109
  /**
76
110
  * Formats cumulative usage statistics for compact UI display.
77
111
  */
@@ -81,20 +115,11 @@ export function formatUsageStats(
81
115
  compact?: boolean,
82
116
  ): string {
83
117
  const parts: string[] = [];
84
- if (usage.turns)
85
- parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
86
- if (usage.input || usage.output) {
87
- const tokens: string[] = [];
88
- if (usage.input) tokens.push(`↑${formatTokens(usage.input)}`);
89
- if (usage.output) tokens.push(`↓${formatTokens(usage.output)}`);
90
- parts.push(tokens.join(" "));
91
- }
92
- if (!compact && (usage.cacheRead || usage.cacheWrite)) {
93
- const cache: string[] = [];
94
- if (usage.cacheRead) cache.push(`R${formatTokens(usage.cacheRead)}`);
95
- if (usage.cacheWrite) cache.push(`W${formatTokens(usage.cacheWrite)}`);
96
- parts.push(`cache:${cache.join("/")}`);
97
- }
118
+ if (usage.turns) parts.push(formatTurns(usage.turns));
119
+ if (usage.input || usage.output)
120
+ parts.push(formatIoTokens(usage.input, usage.output));
121
+ if (!compact && (usage.cacheRead || usage.cacheWrite))
122
+ parts.push(formatCacheStats(usage.cacheRead, usage.cacheWrite));
98
123
  if (!compact && usage.contextTokens && usage.contextTokens > 0)
99
124
  parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
100
125
  if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
@@ -108,11 +133,7 @@ export function formatUsageStats(
108
133
  export function formatResultFooter(usage: UsageStats, model?: string): string {
109
134
  const parts: string[] = [];
110
135
  if (model) parts.push(model);
111
- if (usage.turns)
112
- parts.push(`${usage.turns} turn${usage.turns > 1 ? "s" : ""}`);
113
- if (usage.contextTokens && usage.contextTokens > 0)
114
- parts.push(`ctx:${formatTokens(usage.contextTokens)}`);
115
- if (usage.cost) parts.push(`$${usage.cost.toFixed(4)}`);
136
+ parts.push(...formatUsageCore(usage));
116
137
  return parts.join(" · ");
117
138
  }
118
139
 
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Process invocation and subagent depth utilities.
3
+ * Handles pi CLI discovery and recursion depth tracking via environment variables.
4
+ */
5
+
6
+ import * as fs from "node:fs";
7
+ import * as path from "node:path";
8
+
9
+ export function getPiInvocation(args: string[]): {
10
+ command: string;
11
+ args: string[];
12
+ } {
13
+ const currentScript = process.argv[1];
14
+ if (currentScript && fs.existsSync(currentScript)) {
15
+ return { command: process.execPath, args: [currentScript, ...args] };
16
+ }
17
+ const execName = path.basename(process.execPath).toLowerCase();
18
+ const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
19
+ if (!isGenericRuntime) {
20
+ return { command: process.execPath, args };
21
+ }
22
+ return { command: "pi", args };
23
+ }
24
+
25
+ export function getSubagentDepth(): number {
26
+ const d = Number(process.env.PI_SUBAGENT_DEPTH ?? "0");
27
+ if (!Number.isFinite(d) || d < 0) return 0;
28
+ return Math.floor(d);
29
+ }
30
+
31
+ export function subagentDepthEnv(): Record<string, string> {
32
+ return { PI_SUBAGENT_DEPTH: String(getSubagentDepth() + 1) };
33
+ }
@@ -1,12 +1,14 @@
1
1
  /**
2
2
  * Barrel export for shared utilities.
3
- * Re-exports from focused modules; retains standalone utilities here.
3
+ * Re-exports from focused modules.
4
4
  */
5
5
 
6
- import * as fs from "node:fs";
7
- import * as os from "node:os";
8
- import * as path from "node:path";
9
-
6
+ export { writePromptToTempFile } from "../child/prompt-setup.js";
7
+ export {
8
+ getPiInvocation,
9
+ getSubagentDepth,
10
+ subagentDepthEnv,
11
+ } from "./invocation.js";
10
12
  // Re-export all public symbols from focused modules
11
13
  export {
12
14
  DEFAULT_AGENT_END_GRACE_MS,
@@ -31,48 +33,3 @@ export {
31
33
  resolveAgentExtensionPaths,
32
34
  resolveAgentSkillArgs,
33
35
  } from "./resource-resolution.js";
34
-
35
- // Standalone utilities retained in this module
36
-
37
- export async function writePromptToTempFile(
38
- agentName: string,
39
- prompt: string,
40
- ): Promise<{ dir: string; filePath: string }> {
41
- const tmpDir = await fs.promises.mkdtemp(
42
- path.join(os.tmpdir(), "pi-subagent-"),
43
- );
44
- const safeName = agentName.replace(/[^\w.-]+/g, "_");
45
- const filePath = path.join(tmpDir, `prompt-${safeName}.md`);
46
- // mkdtemp guarantees a unique directory per call; no concurrent writer can hold this path.
47
- await fs.promises.writeFile(filePath, prompt, {
48
- encoding: "utf-8",
49
- mode: 0o600,
50
- });
51
- return { dir: tmpDir, filePath };
52
- }
53
-
54
- export function getPiInvocation(args: string[]): {
55
- command: string;
56
- args: string[];
57
- } {
58
- const currentScript = process.argv[1];
59
- if (currentScript && fs.existsSync(currentScript)) {
60
- return { command: process.execPath, args: [currentScript, ...args] };
61
- }
62
- const execName = path.basename(process.execPath).toLowerCase();
63
- const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
64
- if (!isGenericRuntime) {
65
- return { command: process.execPath, args };
66
- }
67
- return { command: "pi", args };
68
- }
69
-
70
- export function getSubagentDepth(): number {
71
- const d = Number(process.env.PI_SUBAGENT_DEPTH ?? "0");
72
- if (!Number.isFinite(d) || d < 0) return 0;
73
- return Math.floor(d);
74
- }
75
-
76
- export function subagentDepthEnv(): Record<string, string> {
77
- return { PI_SUBAGENT_DEPTH: String(getSubagentDepth() + 1) };
78
- }