@mystilleef/pi-subagent 0.11.0 → 0.12.1
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 +84 -11
- package/package.json +9 -9
- package/src/agent/agents.ts +45 -3
- package/src/child/model-resolution.ts +125 -11
- package/src/child/process-utils.ts +19 -13
- package/src/child/process.ts +126 -37
- package/src/child/prompt-contract.ts +13 -9
- 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/limits.ts +81 -0
- package/src/shared/message-utils.ts +56 -0
- package/src/shared/resource-resolution.ts +289 -0
- package/src/shared/utils.ts +35 -261
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,11 +23,9 @@ import {
|
|
|
18
23
|
} from "../shared/types.js";
|
|
19
24
|
import {
|
|
20
25
|
detectMessageError,
|
|
21
|
-
getPiInvocation,
|
|
22
|
-
getSubagentDepth,
|
|
23
26
|
getSubagentRuntimeLimits,
|
|
27
|
+
resolveAgentExtensionPaths,
|
|
24
28
|
resolveAgentSkillArgs,
|
|
25
|
-
subagentDepthEnv,
|
|
26
29
|
} from "../shared/utils.js";
|
|
27
30
|
import {
|
|
28
31
|
type ChildEventParseResult,
|
|
@@ -34,15 +37,17 @@ import { getLatestOutcomeFromMessages } from "./complete-outcome.js";
|
|
|
34
37
|
import {
|
|
35
38
|
buildModelDisplay,
|
|
36
39
|
type ChildModelSettings,
|
|
40
|
+
type ModelRegistry,
|
|
37
41
|
resolveEffectiveChildModelSettings,
|
|
38
42
|
resolveThinkingLevel,
|
|
39
43
|
} from "./model-resolution.js";
|
|
40
44
|
import {
|
|
41
45
|
appendWithByteLimit,
|
|
42
46
|
resolveCompleteExtensionPath,
|
|
47
|
+
resolvePackageExtensionPath,
|
|
43
48
|
resolveSamplingExtensionPath,
|
|
44
49
|
} from "./process-utils.js";
|
|
45
|
-
import {
|
|
50
|
+
import { SUBAGENT_RESULT_CONTRACT } from "./prompt-contract.js";
|
|
46
51
|
import {
|
|
47
52
|
beginPromptSetup,
|
|
48
53
|
cleanupPromptSetupResult,
|
|
@@ -69,8 +74,8 @@ import {
|
|
|
69
74
|
|
|
70
75
|
const COMPLETE_EXTENSION_PATH = resolveCompleteExtensionPath();
|
|
71
76
|
const SAMPLING_EXTENSION_PATH = resolveSamplingExtensionPath();
|
|
77
|
+
const PACKAGE_EXTENSION_PATH = resolvePackageExtensionPath();
|
|
72
78
|
|
|
73
|
-
export { resolveThinkingLevel } from "./model-resolution.js";
|
|
74
79
|
export { makeEmitUpdate } from "./streaming-progress.js";
|
|
75
80
|
|
|
76
81
|
type RuntimeLimits = ReturnType<typeof getSubagentRuntimeLimits>;
|
|
@@ -79,6 +84,7 @@ type SleepInhibitorAcquirer = (pid: number) => Promise<SleepInhibitorHandle>;
|
|
|
79
84
|
type RunSingleAgentOptions = {
|
|
80
85
|
acquireSleepInhibitor?: SleepInhibitorAcquirer;
|
|
81
86
|
getOrchestratorPid?: () => unknown;
|
|
87
|
+
registry?: ModelRegistry | undefined;
|
|
82
88
|
};
|
|
83
89
|
|
|
84
90
|
export type RunSingleAgentResult =
|
|
@@ -365,15 +371,40 @@ function buildSamplingEnv(agent: AgentConfig): string | undefined {
|
|
|
365
371
|
});
|
|
366
372
|
}
|
|
367
373
|
|
|
368
|
-
|
|
369
|
-
agent: AgentConfig
|
|
370
|
-
task: string
|
|
371
|
-
effectiveModel: ChildModelSettings
|
|
372
|
-
thinking: ThinkingLevel
|
|
373
|
-
resolvedSkills: { args: string[] }
|
|
374
|
-
tmpPrompt: { filePath: string } | null
|
|
375
|
-
|
|
376
|
-
|
|
374
|
+
export interface BuildPiArgsConfig {
|
|
375
|
+
agent: AgentConfig;
|
|
376
|
+
task: string;
|
|
377
|
+
effectiveModel: ChildModelSettings;
|
|
378
|
+
thinking: ThinkingLevel;
|
|
379
|
+
resolvedSkills: { args: string[] };
|
|
380
|
+
tmpPrompt: { filePath: string } | null;
|
|
381
|
+
resolvedExtensionPaths?: string[] | undefined;
|
|
382
|
+
samplingEnv?: string | undefined;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function buildPiArgs(config: BuildPiArgsConfig): string[] {
|
|
386
|
+
const {
|
|
387
|
+
agent,
|
|
388
|
+
task,
|
|
389
|
+
effectiveModel,
|
|
390
|
+
thinking,
|
|
391
|
+
resolvedSkills,
|
|
392
|
+
tmpPrompt,
|
|
393
|
+
resolvedExtensionPaths,
|
|
394
|
+
samplingEnv,
|
|
395
|
+
} = config;
|
|
396
|
+
const args: string[] = [
|
|
397
|
+
"--mode",
|
|
398
|
+
"json",
|
|
399
|
+
"-p",
|
|
400
|
+
"--no-session",
|
|
401
|
+
"--approve",
|
|
402
|
+
"--no-themes",
|
|
403
|
+
"--no-prompt-templates",
|
|
404
|
+
];
|
|
405
|
+
if (agent.extensions !== undefined) {
|
|
406
|
+
args.push("--no-extensions");
|
|
407
|
+
}
|
|
377
408
|
if (effectiveModel.provider && effectiveModel.id)
|
|
378
409
|
args.push("--provider", effectiveModel.provider);
|
|
379
410
|
if (effectiveModel.id) args.push("--model", effectiveModel.id);
|
|
@@ -383,15 +414,35 @@ function buildPiArgs(
|
|
|
383
414
|
tools.add("complete");
|
|
384
415
|
args.push("--tools", [...tools].join(","));
|
|
385
416
|
}
|
|
386
|
-
if (agent.skills
|
|
417
|
+
if (agent.skills !== undefined)
|
|
418
|
+
args.push("--no-skills", ...resolvedSkills.args);
|
|
419
|
+
if (agent.context === false) args.push("--no-context-files");
|
|
387
420
|
if (tmpPrompt) {
|
|
388
|
-
|
|
421
|
+
if (agent.replacePrompt) {
|
|
422
|
+
args.push("--system-prompt", tmpPrompt.filePath);
|
|
423
|
+
} else {
|
|
424
|
+
args.push("--append-system-prompt", tmpPrompt.filePath);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (agent.extensions !== undefined) {
|
|
428
|
+
args.push("--extension", PACKAGE_EXTENSION_PATH);
|
|
429
|
+
if (resolvedExtensionPaths && resolvedExtensionPaths.length > 0) {
|
|
430
|
+
for (const rp of resolvedExtensionPaths) {
|
|
431
|
+
if (rp !== PACKAGE_EXTENSION_PATH) {
|
|
432
|
+
args.push("--extension", rp);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
389
436
|
}
|
|
390
437
|
args.push("--extension", COMPLETE_EXTENSION_PATH);
|
|
438
|
+
if (samplingEnv) {
|
|
439
|
+
args.push("--extension", SAMPLING_EXTENSION_PATH);
|
|
440
|
+
}
|
|
441
|
+
args.push("--append-system-prompt", SUBAGENT_RESULT_CONTRACT);
|
|
391
442
|
const taskPrompt = task
|
|
392
443
|
? `Task: ${task}`
|
|
393
444
|
: "Run according to your system prompt. If no explicit task was provided, use the default context described there.";
|
|
394
|
-
args.push(
|
|
445
|
+
args.push(taskPrompt);
|
|
395
446
|
return args;
|
|
396
447
|
}
|
|
397
448
|
|
|
@@ -535,23 +586,38 @@ export async function runSingleAgent(
|
|
|
535
586
|
}
|
|
536
587
|
const requestedThinking = agent.thinking ?? parentThinking;
|
|
537
588
|
const effectiveModel = resolveEffectiveChildModelSettings(agent, parentModel);
|
|
538
|
-
const
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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);
|
|
546
598
|
const modelDisplay = buildModelDisplay(effectiveModel, thinking);
|
|
547
599
|
const resolvedSkillsPromise: Promise<{ args: string[] } | { error: string }> =
|
|
548
600
|
agent.skills
|
|
549
601
|
? resolveAgentSkillArgs(defaultCwd, agent.skills)
|
|
550
602
|
: Promise.resolve({ args: [] });
|
|
603
|
+
const extensionNames =
|
|
604
|
+
agent.extensions && agent.extensions.length > 0
|
|
605
|
+
? agent.extensions
|
|
606
|
+
: undefined;
|
|
607
|
+
const resolvedExtensionsPromise: Promise<
|
|
608
|
+
{ resolvedPaths: string[] } | { error: string }
|
|
609
|
+
> = extensionNames
|
|
610
|
+
? resolveAgentExtensionPaths(defaultCwd, extensionNames)
|
|
611
|
+
: Promise.resolve({ resolvedPaths: [] });
|
|
551
612
|
const promptSetupPromise = beginPromptSetup(agent);
|
|
552
|
-
const resolvedSkills = await
|
|
553
|
-
|
|
554
|
-
|
|
613
|
+
const [resolvedSkills, resolvedExtensions, promptSetup] = await Promise.all([
|
|
614
|
+
resolvedSkillsPromise,
|
|
615
|
+
resolvedExtensionsPromise,
|
|
616
|
+
promptSetupPromise,
|
|
617
|
+
]);
|
|
618
|
+
const abortWithError = async (
|
|
619
|
+
error: string,
|
|
620
|
+
): Promise<RunSingleAgentResult> => {
|
|
555
621
|
await cleanupPromptSetupResult(promptSetup);
|
|
556
622
|
return {
|
|
557
623
|
kind: "completed",
|
|
@@ -559,13 +625,30 @@ export async function runSingleAgent(
|
|
|
559
625
|
agentName,
|
|
560
626
|
agent.source,
|
|
561
627
|
task,
|
|
562
|
-
|
|
628
|
+
error,
|
|
629
|
+
modelDisplay,
|
|
630
|
+
),
|
|
631
|
+
};
|
|
632
|
+
};
|
|
633
|
+
if ("error" in resolvedSkills) return abortWithError(resolvedSkills.error);
|
|
634
|
+
if ("error" in resolvedExtensions)
|
|
635
|
+
return abortWithError(resolvedExtensions.error);
|
|
636
|
+
if ("error" in promptSetup) {
|
|
637
|
+
return {
|
|
638
|
+
kind: "completed",
|
|
639
|
+
result: createErrorResult(
|
|
640
|
+
agentName,
|
|
641
|
+
agent.source,
|
|
642
|
+
task,
|
|
643
|
+
`Failed to write prompt: ${
|
|
644
|
+
promptSetup.error instanceof Error
|
|
645
|
+
? promptSetup.error.message
|
|
646
|
+
: String(promptSetup.error)
|
|
647
|
+
}`,
|
|
563
648
|
modelDisplay,
|
|
564
649
|
),
|
|
565
650
|
};
|
|
566
651
|
}
|
|
567
|
-
const promptSetup = await promptSetupPromise;
|
|
568
|
-
if ("error" in promptSetup) throw promptSetup.error;
|
|
569
652
|
const startedAt = Date.now();
|
|
570
653
|
const state: SubagentState = {
|
|
571
654
|
result: initRuntimeResult(agentName, agent.source, task, modelDisplay),
|
|
@@ -576,17 +659,23 @@ export async function runSingleAgent(
|
|
|
576
659
|
const tmpPrompt = promptSetup.tmpPrompt;
|
|
577
660
|
const samplingEnv = buildSamplingEnv(agent);
|
|
578
661
|
try {
|
|
579
|
-
const args = buildPiArgs(
|
|
662
|
+
const args = buildPiArgs({
|
|
580
663
|
agent,
|
|
581
664
|
task,
|
|
582
665
|
effectiveModel,
|
|
583
|
-
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,
|
|
584
671
|
resolvedSkills,
|
|
585
672
|
tmpPrompt,
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
673
|
+
resolvedExtensionPaths:
|
|
674
|
+
agent.extensions !== undefined
|
|
675
|
+
? resolvedExtensions.resolvedPaths
|
|
676
|
+
: undefined,
|
|
677
|
+
samplingEnv,
|
|
678
|
+
});
|
|
590
679
|
const invocation = getPiInvocation(args);
|
|
591
680
|
const terminateOptions = {
|
|
592
681
|
tree: true,
|
|
@@ -5,16 +5,20 @@ export const SUBAGENT_RESULT_CONTRACT = `
|
|
|
5
5
|
|
|
6
6
|
### Directives
|
|
7
7
|
|
|
8
|
-
- Write the complete result as
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
- Write the complete task result as assistant text — mandatory, non-empty, and trimmed,
|
|
9
|
+
before calling \`complete\`.
|
|
10
|
+
- Call \`complete\` as the final action after the result text; \`outcome\` must be a short,
|
|
11
|
+
one-sentence progress summary only, not the full result.
|
|
12
|
+
- This contract applies after any amount of tool use, reading, editing, or multi-step work.
|
|
11
13
|
|
|
12
14
|
### Constraints
|
|
13
15
|
|
|
14
|
-
- **NEVER** omit the text response.
|
|
15
|
-
- **NEVER**
|
|
16
|
+
- **NEVER** omit the result text response.
|
|
17
|
+
- **NEVER** emit whitespace-only, empty-string, or blank result text before \`complete\`.
|
|
18
|
+
- **NEVER** wrap the entire result in a code block or code fence.
|
|
19
|
+
- **NEVER** end the assistant response with text alone after tool calls; a terminal
|
|
20
|
+
\`complete\` call is required.
|
|
21
|
+
- **NEVER** write assistant text after \`complete\`.
|
|
22
|
+
- Call \`complete\` exactly once; multiple \`complete\` calls are not allowed.
|
|
23
|
+
- \`outcome\` must be concise and contain only a progress summary.
|
|
16
24
|
`;
|
|
17
|
-
|
|
18
|
-
export function appendSubagentResultContract(prompt: string): string {
|
|
19
|
-
return `${prompt}\n\n${SUBAGENT_RESULT_CONTRACT}`;
|
|
20
|
-
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output and runtime limit configuration for subagent processes.
|
|
3
|
+
* Handles byte/line caps for child output and runtime safety thresholds.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export const DEFAULT_MAX_OUTPUT_BYTES = 50_000;
|
|
7
|
+
export const DEFAULT_MAX_OUTPUT_LINES = 500;
|
|
8
|
+
export const DEFAULT_AGENT_END_GRACE_MS = 250;
|
|
9
|
+
export const DEFAULT_MAX_STDERR_BYTES = 10_000;
|
|
10
|
+
export const DEFAULT_MAX_SUBAGENT_DEPTH = 3;
|
|
11
|
+
const MAX_SUBAGENT_DEPTH_CEILING = 10;
|
|
12
|
+
|
|
13
|
+
export interface SubagentOutputLimits {
|
|
14
|
+
maxBytes: number;
|
|
15
|
+
maxLines: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SubagentRuntimeLimits {
|
|
19
|
+
agentEndGraceMs: number;
|
|
20
|
+
maxStderrBytes: number;
|
|
21
|
+
maxDepth: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type EnvLimitConfig = Partial<Record<string, string | number | undefined>>;
|
|
25
|
+
|
|
26
|
+
function parsePositiveInteger(
|
|
27
|
+
value: string | number | undefined,
|
|
28
|
+
): number | undefined {
|
|
29
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
30
|
+
if (!Number.isFinite(parsed) || !Number.isInteger(parsed) || parsed < 1)
|
|
31
|
+
return undefined;
|
|
32
|
+
return parsed;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getSubagentOutputLimits(
|
|
36
|
+
config: EnvLimitConfig = process.env,
|
|
37
|
+
): SubagentOutputLimits {
|
|
38
|
+
return {
|
|
39
|
+
maxBytes:
|
|
40
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_OUTPUT_BYTES"]) ??
|
|
41
|
+
DEFAULT_MAX_OUTPUT_BYTES,
|
|
42
|
+
maxLines:
|
|
43
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_OUTPUT_LINES"]) ??
|
|
44
|
+
DEFAULT_MAX_OUTPUT_LINES,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getSubagentRuntimeLimits(
|
|
49
|
+
config: EnvLimitConfig = process.env,
|
|
50
|
+
): SubagentRuntimeLimits {
|
|
51
|
+
const maxDepth =
|
|
52
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_DEPTH"]) ??
|
|
53
|
+
DEFAULT_MAX_SUBAGENT_DEPTH;
|
|
54
|
+
return {
|
|
55
|
+
agentEndGraceMs:
|
|
56
|
+
parsePositiveInteger(config["PI_SUBAGENT_AGENT_END_GRACE_MS"]) ??
|
|
57
|
+
DEFAULT_AGENT_END_GRACE_MS,
|
|
58
|
+
maxStderrBytes:
|
|
59
|
+
parsePositiveInteger(config["PI_SUBAGENT_MAX_STDERR_BYTES"]) ??
|
|
60
|
+
DEFAULT_MAX_STDERR_BYTES,
|
|
61
|
+
maxDepth: Math.min(maxDepth, MAX_SUBAGENT_DEPTH_CEILING),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function truncateOutput(
|
|
66
|
+
text: string,
|
|
67
|
+
limits: SubagentOutputLimits = getSubagentOutputLimits(),
|
|
68
|
+
): string {
|
|
69
|
+
const lines = text.split("\n");
|
|
70
|
+
const maxBytes = Math.max(1, Math.floor(limits.maxBytes));
|
|
71
|
+
const maxLines = Math.max(1, Math.floor(limits.maxLines));
|
|
72
|
+
if (lines.length <= maxLines && Buffer.byteLength(text, "utf-8") <= maxBytes)
|
|
73
|
+
return text;
|
|
74
|
+
let result = lines.slice(0, maxLines).join("\n");
|
|
75
|
+
if (Buffer.byteLength(result, "utf-8") > maxBytes) {
|
|
76
|
+
const buf = Buffer.from(result).subarray(0, maxBytes);
|
|
77
|
+
result = buf.toString("utf-8").replace(/\uFFFD$/, "");
|
|
78
|
+
}
|
|
79
|
+
const kept = result.split("\n").length;
|
|
80
|
+
return `[TRUNCATED: first ${kept} of ${lines.length} lines]\n${result}`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message processing utilities for subagent results.
|
|
3
|
+
* Handles assistant message extraction, error detection, and failure判定.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Message } from "@earendil-works/pi-ai";
|
|
7
|
+
import type { SingleResult } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export function findLastAssistantTextMessage(messages: Message[]): number {
|
|
10
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
11
|
+
const msg = messages[i];
|
|
12
|
+
if (
|
|
13
|
+
msg?.role === "assistant" &&
|
|
14
|
+
Array.isArray(msg.content) &&
|
|
15
|
+
msg.content.some(
|
|
16
|
+
(c) =>
|
|
17
|
+
c.type === "text" &&
|
|
18
|
+
typeof c.text === "string" &&
|
|
19
|
+
c.text.trim().length > 0,
|
|
20
|
+
)
|
|
21
|
+
) {
|
|
22
|
+
return i;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return -1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function extractFinalOutputFromMessages(messages: Message[]): string {
|
|
29
|
+
const lastAsstIdx = findLastAssistantTextMessage(messages);
|
|
30
|
+
if (lastAsstIdx < 0) return "";
|
|
31
|
+
const content = messages[lastAsstIdx]?.content;
|
|
32
|
+
if (!Array.isArray(content)) return "";
|
|
33
|
+
const lastText = content.findLast((p) => p.type === "text");
|
|
34
|
+
return lastText?.type === "text" ? (lastText.text ?? "") : "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function detectMessageError(messages: Message[]): boolean {
|
|
38
|
+
const lastAssistantIdx = findLastAssistantTextMessage(messages);
|
|
39
|
+
const from = lastAssistantIdx >= 0 ? lastAssistantIdx + 1 : 0;
|
|
40
|
+
for (let i = messages.length - 1; i >= from; i--) {
|
|
41
|
+
const msg = messages[i];
|
|
42
|
+
if (msg?.role === "toolResult" && msg.isError) return true;
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function hasSubagentFailed(result: SingleResult): boolean {
|
|
48
|
+
if (result.outcome?.trim()) return false;
|
|
49
|
+
return (
|
|
50
|
+
result.exitCode !== 0 ||
|
|
51
|
+
result.stopReason === "error" ||
|
|
52
|
+
result.stopReason === "aborted" ||
|
|
53
|
+
Boolean(result.errorMessage?.trim()) ||
|
|
54
|
+
detectMessageError(result.messages ?? [])
|
|
55
|
+
);
|
|
56
|
+
}
|