@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 +3 -2
- package/package.json +9 -9
- package/src/agent/agents.ts +1 -0
- package/src/child/model-resolution.ts +125 -11
- package/src/child/process-utils.ts +8 -13
- package/src/child/process.ts +21 -13
- package/src/child/prompt-setup.ts +19 -1
- package/src/orchestration/subagent-orchestrator.ts +6 -1
- package/src/output/ui.ts +40 -19
- package/src/shared/invocation.ts +33 -0
- package/src/shared/utils.ts +7 -50
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`).
|
|
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.
|
|
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.
|
|
68
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
69
|
-
"@earendil-works/pi-ai": "^0.
|
|
70
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
71
|
-
"@earendil-works/pi-tui": "^0.
|
|
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.
|
|
74
|
-
"typebox": "^1.3.
|
|
75
|
-
"typescript": "^
|
|
73
|
+
"@types/node": "^26.2.0",
|
|
74
|
+
"typebox": "^1.3.14",
|
|
75
|
+
"typescript": "^7.0.2"
|
|
76
76
|
}
|
|
77
77
|
}
|
package/src/agent/agents.ts
CHANGED
|
@@ -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
|
-
*
|
|
21
|
-
*
|
|
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
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
if (
|
|
30
|
-
|
|
31
|
-
|
|
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 {
|
|
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 {
|
|
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
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
}
|
package/src/child/process.ts
CHANGED
|
@@ -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
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/shared/utils.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Barrel export for shared utilities.
|
|
3
|
-
* Re-exports from focused modules
|
|
3
|
+
* Re-exports from focused modules.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
}
|