@narumitw/pi-subagents 0.42.0 → 0.43.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 +142 -32
- package/package.json +1 -1
- package/src/agents.ts +49 -8
- package/src/config-ui.ts +223 -28
- package/src/consult-policy.ts +15 -0
- package/src/consult-render.ts +194 -0
- package/src/consult.ts +815 -0
- package/src/cwd-policy.ts +183 -0
- package/src/execution.ts +135 -66
- package/src/in-process-transport.ts +3 -3
- package/src/inspect-render.ts +234 -0
- package/src/inspect.ts +453 -0
- package/src/limits.ts +1 -0
- package/src/params.ts +2 -0
- package/src/persistence.ts +29 -0
- package/src/registry.ts +87 -0
- package/src/render-common.ts +252 -0
- package/src/render.ts +134 -99
- package/src/runner.ts +162 -22
- package/src/safe-text.ts +67 -0
- package/src/settings.ts +199 -12
- package/src/stateful-guidance.ts +35 -0
- package/src/stateful-lifecycle.ts +31 -0
- package/src/stateful-render.ts +249 -0
- package/src/stateful-safety.ts +91 -0
- package/src/stateful.ts +254 -225
- package/src/subagents.ts +100 -19
- package/src/subprocess-transport.ts +19 -2
package/src/runner.ts
CHANGED
|
@@ -6,12 +6,14 @@ import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
|
6
6
|
import type { Message } from "@earendil-works/pi-ai";
|
|
7
7
|
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import type { AgentConfig, AgentScope, AgentSource, SubagentThinkingLevel } from "./agents.js";
|
|
9
|
+
import type { TargetPolicyAudit } from "./cwd-policy.js";
|
|
9
10
|
import {
|
|
10
11
|
appendBounded,
|
|
11
12
|
DEFAULT_MAX_CONTEXT_BYTES,
|
|
12
13
|
DEFAULT_MAX_MESSAGES,
|
|
13
14
|
DEFAULT_MAX_OUTPUT_BYTES,
|
|
14
15
|
DEFAULT_MAX_STDERR_BYTES,
|
|
16
|
+
MAX_SUBAGENT_TIMEOUT_MS,
|
|
15
17
|
truncateUtf8,
|
|
16
18
|
} from "./limits.js";
|
|
17
19
|
import { JsonLineDecoder } from "./protocol.js";
|
|
@@ -24,6 +26,11 @@ export interface UsageStats {
|
|
|
24
26
|
cacheRead: number;
|
|
25
27
|
cacheWrite: number;
|
|
26
28
|
cost: number;
|
|
29
|
+
costInput?: number;
|
|
30
|
+
costOutput?: number;
|
|
31
|
+
costCacheRead?: number;
|
|
32
|
+
costCacheWrite?: number;
|
|
33
|
+
totalTokens?: number;
|
|
27
34
|
contextTokens: number;
|
|
28
35
|
turns: number;
|
|
29
36
|
}
|
|
@@ -34,6 +41,21 @@ export type RecentActivityItem =
|
|
|
34
41
|
const MAX_RECENT_ACTIVITY_ITEMS = 10;
|
|
35
42
|
const MAX_RECENT_ACTIVITY_BYTES = 8 * 1024;
|
|
36
43
|
const MAX_RECENT_ACTIVITY_ARGUMENT_BYTES = 1024;
|
|
44
|
+
const MAX_USAGE_VALUE = Number.MAX_SAFE_INTEGER;
|
|
45
|
+
|
|
46
|
+
function protocolUsageCount(value: unknown): number {
|
|
47
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function protocolUsageCost(value: unknown): number {
|
|
51
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
52
|
+
? Math.min(value, MAX_USAGE_VALUE)
|
|
53
|
+
: 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function addUsageValue(current: number, addition: number): number {
|
|
57
|
+
return Math.min(MAX_USAGE_VALUE, current + addition);
|
|
58
|
+
}
|
|
37
59
|
|
|
38
60
|
export interface SingleResult {
|
|
39
61
|
agent: string;
|
|
@@ -58,6 +80,9 @@ export interface SingleResult {
|
|
|
58
80
|
aborted?: boolean;
|
|
59
81
|
truncated?: boolean;
|
|
60
82
|
malformedEvents?: number;
|
|
83
|
+
launchFailed?: boolean;
|
|
84
|
+
processStarted?: boolean;
|
|
85
|
+
target?: TargetPolicyAudit;
|
|
61
86
|
policy?: {
|
|
62
87
|
inherited: string[];
|
|
63
88
|
overridden: string[];
|
|
@@ -288,20 +313,41 @@ async function writePromptToTempFile(
|
|
|
288
313
|
return { dir: tmpDir, filePath };
|
|
289
314
|
}
|
|
290
315
|
|
|
291
|
-
export
|
|
316
|
+
export interface PiArgsOptions {
|
|
292
317
|
model?: string;
|
|
293
318
|
thinkingLevel?: SubagentThinkingLevel;
|
|
294
319
|
tools?: string[];
|
|
320
|
+
disableExtensions?: boolean;
|
|
321
|
+
disableSkills?: boolean;
|
|
322
|
+
disablePromptTemplates?: boolean;
|
|
323
|
+
disableContextFiles?: boolean;
|
|
324
|
+
projectTrust?: boolean;
|
|
325
|
+
baseSystemPromptPath?: string;
|
|
326
|
+
appendSystemPromptPaths?: string[];
|
|
327
|
+
/** Existing single append prompt path retained for compatibility. */
|
|
295
328
|
systemPromptPath?: string;
|
|
296
329
|
task: string;
|
|
297
|
-
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function buildPiArgs(options: PiArgsOptions): string[] {
|
|
298
333
|
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
299
334
|
if (options.model) args.push("--model", options.model);
|
|
300
335
|
if (options.thinkingLevel) args.push("--thinking", options.thinkingLevel);
|
|
336
|
+
if (options.disableExtensions) args.push("--no-extensions");
|
|
337
|
+
if (options.disableSkills) args.push("--no-skills");
|
|
338
|
+
if (options.disablePromptTemplates) args.push("--no-prompt-templates");
|
|
339
|
+
if (options.disableContextFiles) args.push("--no-context-files");
|
|
340
|
+
if (options.projectTrust !== undefined) {
|
|
341
|
+
args.push(options.projectTrust ? "--approve" : "--no-approve");
|
|
342
|
+
}
|
|
301
343
|
if (Array.isArray(options.tools)) {
|
|
302
344
|
if (options.tools.length > 0) args.push("--tools", options.tools.join(","));
|
|
303
345
|
else args.push("--no-tools");
|
|
304
346
|
}
|
|
347
|
+
if (options.baseSystemPromptPath) args.push("--system-prompt", options.baseSystemPromptPath);
|
|
348
|
+
for (const promptPath of options.appendSystemPromptPaths ?? []) {
|
|
349
|
+
args.push("--append-system-prompt", promptPath);
|
|
350
|
+
}
|
|
305
351
|
if (options.systemPromptPath) args.push("--append-system-prompt", options.systemPromptPath);
|
|
306
352
|
args.push(`Task: ${options.task}`);
|
|
307
353
|
return args;
|
|
@@ -365,6 +411,17 @@ export function terminateProcess(
|
|
|
365
411
|
|
|
366
412
|
export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
367
413
|
|
|
414
|
+
export interface ChildLaunchPolicy {
|
|
415
|
+
tools?: string[];
|
|
416
|
+
disableExtensions?: boolean;
|
|
417
|
+
disableSkills?: boolean;
|
|
418
|
+
disablePromptTemplates?: boolean;
|
|
419
|
+
disableContextFiles?: boolean;
|
|
420
|
+
projectTrust?: boolean;
|
|
421
|
+
baseSystemPrompt?: string;
|
|
422
|
+
appendSystemPromptPaths?: string[];
|
|
423
|
+
}
|
|
424
|
+
|
|
368
425
|
export async function runSingleAgent(
|
|
369
426
|
defaultCwd: string,
|
|
370
427
|
agents: AgentConfig[],
|
|
@@ -378,6 +435,7 @@ export async function runSingleAgent(
|
|
|
378
435
|
onUpdate: OnUpdateCallback | undefined,
|
|
379
436
|
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
380
437
|
invocationOverride?: { command: string; argsPrefix?: string[] },
|
|
438
|
+
launchPolicy?: ChildLaunchPolicy,
|
|
381
439
|
): Promise<SingleResult> {
|
|
382
440
|
const agent = agents.find((a) => a.name === agentName);
|
|
383
441
|
|
|
@@ -405,8 +463,9 @@ export async function runSingleAgent(
|
|
|
405
463
|
};
|
|
406
464
|
}
|
|
407
465
|
|
|
408
|
-
|
|
466
|
+
const temporaryPrompts: Array<{ dir: string; filePath: string }> = [];
|
|
409
467
|
let tmpPromptPath: string | null = null;
|
|
468
|
+
let baseSystemPromptPath: string | null = null;
|
|
410
469
|
|
|
411
470
|
let latestAssistantOutput = "";
|
|
412
471
|
let terminalAssistantOutput: string | undefined;
|
|
@@ -474,17 +533,46 @@ export async function runSingleAgent(
|
|
|
474
533
|
setErrorMessage("Subagent was aborted before start");
|
|
475
534
|
return currentResult;
|
|
476
535
|
}
|
|
536
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_SUBAGENT_TIMEOUT_MS) {
|
|
537
|
+
currentResult.exitCode = 1;
|
|
538
|
+
currentResult.stopReason = "error";
|
|
539
|
+
setErrorMessage(
|
|
540
|
+
`Invalid subagent timeout: expected 1-${MAX_SUBAGENT_TIMEOUT_MS}ms, received ${timeoutMs}`,
|
|
541
|
+
);
|
|
542
|
+
return currentResult;
|
|
543
|
+
}
|
|
477
544
|
|
|
545
|
+
if (launchPolicy?.baseSystemPrompt?.trim()) {
|
|
546
|
+
const tmp = await writePromptToTempFile(`${agent.name}-base`, launchPolicy.baseSystemPrompt);
|
|
547
|
+
temporaryPrompts.push(tmp);
|
|
548
|
+
baseSystemPromptPath = tmp.filePath;
|
|
549
|
+
}
|
|
478
550
|
if (agent.systemPrompt.trim()) {
|
|
479
551
|
const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
480
|
-
|
|
552
|
+
temporaryPrompts.push(tmp);
|
|
481
553
|
tmpPromptPath = tmp.filePath;
|
|
482
554
|
}
|
|
555
|
+
if (signal?.aborted) {
|
|
556
|
+
currentResult.exitCode = 130;
|
|
557
|
+
currentResult.aborted = true;
|
|
558
|
+
currentResult.stopReason = "aborted";
|
|
559
|
+
setErrorMessage("Subagent was aborted before launch");
|
|
560
|
+
return currentResult;
|
|
561
|
+
}
|
|
483
562
|
|
|
563
|
+
const effectiveTools =
|
|
564
|
+
launchPolicy && Object.hasOwn(launchPolicy, "tools") ? launchPolicy.tools : agent.tools;
|
|
484
565
|
const args = buildPiArgs({
|
|
485
566
|
model: agent.model,
|
|
486
567
|
thinkingLevel,
|
|
487
|
-
tools:
|
|
568
|
+
tools: effectiveTools,
|
|
569
|
+
disableExtensions: launchPolicy?.disableExtensions,
|
|
570
|
+
disableSkills: launchPolicy?.disableSkills,
|
|
571
|
+
disablePromptTemplates: launchPolicy?.disablePromptTemplates,
|
|
572
|
+
disableContextFiles: launchPolicy?.disableContextFiles,
|
|
573
|
+
projectTrust: launchPolicy?.projectTrust,
|
|
574
|
+
baseSystemPromptPath: baseSystemPromptPath ?? undefined,
|
|
575
|
+
appendSystemPromptPaths: launchPolicy?.appendSystemPromptPaths,
|
|
488
576
|
systemPromptPath: tmpPromptPath ?? undefined,
|
|
489
577
|
task,
|
|
490
578
|
});
|
|
@@ -525,6 +613,7 @@ export async function runSingleAgent(
|
|
|
525
613
|
},
|
|
526
614
|
});
|
|
527
615
|
} catch (error) {
|
|
616
|
+
currentResult.launchFailed = true;
|
|
528
617
|
currentResult.stderr = setErrorMessage(
|
|
529
618
|
error instanceof Error ? error.message : String(error),
|
|
530
619
|
);
|
|
@@ -576,19 +665,62 @@ export async function runSingleAgent(
|
|
|
576
665
|
if (msg.role === "assistant") {
|
|
577
666
|
currentResult.usage.turns++;
|
|
578
667
|
const usage = msg.usage;
|
|
579
|
-
if (usage) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
668
|
+
if (usage && typeof usage === "object") {
|
|
669
|
+
const input = protocolUsageCount(usage.input);
|
|
670
|
+
const output = protocolUsageCount(usage.output);
|
|
671
|
+
const cacheRead = protocolUsageCount(usage.cacheRead);
|
|
672
|
+
const cacheWrite = protocolUsageCount(usage.cacheWrite);
|
|
673
|
+
const reportedTotal = protocolUsageCount(usage.totalTokens);
|
|
674
|
+
const turnTotal =
|
|
675
|
+
reportedTotal ||
|
|
676
|
+
addUsageValue(addUsageValue(input, output), addUsageValue(cacheRead, cacheWrite));
|
|
677
|
+
const cost = usage.cost && typeof usage.cost === "object" ? usage.cost : undefined;
|
|
678
|
+
currentResult.usage.input = addUsageValue(currentResult.usage.input, input);
|
|
679
|
+
currentResult.usage.output = addUsageValue(currentResult.usage.output, output);
|
|
680
|
+
currentResult.usage.cacheRead = addUsageValue(
|
|
681
|
+
currentResult.usage.cacheRead,
|
|
682
|
+
cacheRead,
|
|
683
|
+
);
|
|
684
|
+
currentResult.usage.cacheWrite = addUsageValue(
|
|
685
|
+
currentResult.usage.cacheWrite,
|
|
686
|
+
cacheWrite,
|
|
687
|
+
);
|
|
688
|
+
currentResult.usage.cost = addUsageValue(
|
|
689
|
+
currentResult.usage.cost,
|
|
690
|
+
protocolUsageCost(cost?.total),
|
|
691
|
+
);
|
|
692
|
+
currentResult.usage.costInput = addUsageValue(
|
|
693
|
+
currentResult.usage.costInput ?? 0,
|
|
694
|
+
protocolUsageCost(cost?.input),
|
|
695
|
+
);
|
|
696
|
+
currentResult.usage.costOutput = addUsageValue(
|
|
697
|
+
currentResult.usage.costOutput ?? 0,
|
|
698
|
+
protocolUsageCost(cost?.output),
|
|
699
|
+
);
|
|
700
|
+
currentResult.usage.costCacheRead = addUsageValue(
|
|
701
|
+
currentResult.usage.costCacheRead ?? 0,
|
|
702
|
+
protocolUsageCost(cost?.cacheRead),
|
|
703
|
+
);
|
|
704
|
+
currentResult.usage.costCacheWrite = addUsageValue(
|
|
705
|
+
currentResult.usage.costCacheWrite ?? 0,
|
|
706
|
+
protocolUsageCost(cost?.cacheWrite),
|
|
707
|
+
);
|
|
708
|
+
currentResult.usage.totalTokens = addUsageValue(
|
|
709
|
+
currentResult.usage.totalTokens ?? 0,
|
|
710
|
+
turnTotal,
|
|
711
|
+
);
|
|
712
|
+
currentResult.usage.contextTokens = turnTotal;
|
|
586
713
|
}
|
|
587
|
-
if (msg.provider) currentResult.actualProvider = msg.provider;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
714
|
+
if (typeof msg.provider === "string") currentResult.actualProvider = msg.provider;
|
|
715
|
+
const actualModel =
|
|
716
|
+
typeof msg.responseModel === "string"
|
|
717
|
+
? msg.responseModel
|
|
718
|
+
: typeof msg.model === "string"
|
|
719
|
+
? msg.model
|
|
720
|
+
: undefined;
|
|
721
|
+
if (actualModel) currentResult.actualModel = actualModel;
|
|
722
|
+
if (typeof msg.stopReason === "string") currentResult.stopReason = msg.stopReason;
|
|
723
|
+
if (typeof msg.errorMessage === "string") setErrorMessage(msg.errorMessage);
|
|
592
724
|
}
|
|
593
725
|
emitUpdate();
|
|
594
726
|
} else if (event.type === "tool_result_end" && event.message) {
|
|
@@ -623,6 +755,9 @@ export async function runSingleAgent(
|
|
|
623
755
|
}, timeoutMs);
|
|
624
756
|
timeout.unref();
|
|
625
757
|
|
|
758
|
+
proc.once("spawn", () => {
|
|
759
|
+
currentResult.processStarted = true;
|
|
760
|
+
});
|
|
626
761
|
proc.stdout?.on("data", (data) => decoder.push(data));
|
|
627
762
|
proc.stderr?.on("data", (data) => {
|
|
628
763
|
const bounded = appendBounded(
|
|
@@ -638,6 +773,7 @@ export async function runSingleAgent(
|
|
|
638
773
|
finish(timedOut ? 124 : wasAborted ? 130 : (code ?? 0));
|
|
639
774
|
});
|
|
640
775
|
proc.on("error", (error) => {
|
|
776
|
+
currentResult.launchFailed = true;
|
|
641
777
|
const message = setErrorMessage(error.message);
|
|
642
778
|
const bounded = appendBounded(
|
|
643
779
|
currentResult.stderr,
|
|
@@ -682,23 +818,27 @@ export async function runSingleAgent(
|
|
|
682
818
|
"cwd",
|
|
683
819
|
...(agent.model ? ["model"] : []),
|
|
684
820
|
...(thinkingLevel ? ["thinkingLevel"] : []),
|
|
685
|
-
...(
|
|
821
|
+
...(effectiveTools !== undefined ? ["tools"] : []),
|
|
822
|
+
...(launchPolicy?.disableExtensions ? ["extensions"] : []),
|
|
823
|
+
...(launchPolicy?.disableSkills ? ["skills"] : []),
|
|
824
|
+
...(launchPolicy?.disablePromptTemplates ? ["promptTemplates"] : []),
|
|
825
|
+
...(launchPolicy?.disableContextFiles ? ["contextFiles"] : []),
|
|
686
826
|
],
|
|
687
827
|
unsupported: ["approvalPolicy", "sandboxProfile", "providerHeaders"],
|
|
688
828
|
};
|
|
689
829
|
return currentResult;
|
|
690
830
|
} finally {
|
|
691
|
-
|
|
831
|
+
for (const temporary of temporaryPrompts.reverse()) {
|
|
692
832
|
try {
|
|
693
|
-
fs.unlinkSync(
|
|
833
|
+
fs.unlinkSync(temporary.filePath);
|
|
694
834
|
} catch {
|
|
695
835
|
/* ignore */
|
|
696
836
|
}
|
|
697
|
-
if (tmpPromptDir)
|
|
698
837
|
try {
|
|
699
|
-
fs.rmdirSync(
|
|
838
|
+
fs.rmdirSync(temporary.dir);
|
|
700
839
|
} catch {
|
|
701
840
|
/* ignore */
|
|
702
841
|
}
|
|
842
|
+
}
|
|
703
843
|
}
|
|
704
844
|
}
|
package/src/safe-text.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as os from "node:os";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { redactPrivateText } from "./context.js";
|
|
5
|
+
import { DEFAULT_MAX_OUTPUT_BYTES, TRUNCATION_MARKER, truncateUtf8 } from "./limits.js";
|
|
6
|
+
|
|
7
|
+
export const DEFAULT_MAX_OUTPUT_LINES = 2_000;
|
|
8
|
+
|
|
9
|
+
export function safeTerminalText(value: string): string {
|
|
10
|
+
return (
|
|
11
|
+
value
|
|
12
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: Escape untrusted terminal controls while preserving newlines.
|
|
13
|
+
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/gu, "?")
|
|
14
|
+
.replace(/\r/gu, "")
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function safeTerminalLine(value: string, maxBytes = 2 * 1024): string {
|
|
19
|
+
const singleLine = safeTerminalText(redactPrivateText(value)).replace(/\s+/gu, " ").trim();
|
|
20
|
+
return truncateUtf8(singleLine, maxBytes).text.replace(/\s+/gu, " ").trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function boundText(
|
|
24
|
+
value: string,
|
|
25
|
+
maxBytes = DEFAULT_MAX_OUTPUT_BYTES,
|
|
26
|
+
maxLines = DEFAULT_MAX_OUTPUT_LINES,
|
|
27
|
+
): { text: string; truncated: boolean } {
|
|
28
|
+
const safe = safeTerminalText(value);
|
|
29
|
+
const lines = safe.split("\n");
|
|
30
|
+
const lineBounded =
|
|
31
|
+
lines.length > maxLines
|
|
32
|
+
? `${lines.slice(0, Math.max(0, maxLines - 1)).join("\n")}${TRUNCATION_MARKER}`
|
|
33
|
+
: safe;
|
|
34
|
+
const bounded = truncateUtf8(lineBounded, maxBytes);
|
|
35
|
+
return { text: bounded.text, truncated: lines.length > maxLines || bounded.truncated };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function boundedPrivateText(value: string, maxBytes: number): string {
|
|
39
|
+
return boundText(redactPrivateText(value), maxBytes).text;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function safeDisplayPath(value: string, workspace: string): string {
|
|
43
|
+
if (value.startsWith("built-in:")) return safeTerminalLine(value);
|
|
44
|
+
const resolved = path.resolve(value);
|
|
45
|
+
const agentDir = path.resolve(getAgentDir());
|
|
46
|
+
const relativeAgent = path.relative(agentDir, resolved);
|
|
47
|
+
if (
|
|
48
|
+
relativeAgent === "" ||
|
|
49
|
+
(!relativeAgent.startsWith("..") && !path.isAbsolute(relativeAgent))
|
|
50
|
+
) {
|
|
51
|
+
return relativeAgent ? `~/${safeTerminalLine(relativeAgent)}` : "~";
|
|
52
|
+
}
|
|
53
|
+
const resolvedWorkspace = path.resolve(workspace);
|
|
54
|
+
const relativeWorkspace = path.relative(resolvedWorkspace, resolved);
|
|
55
|
+
if (
|
|
56
|
+
relativeWorkspace === "" ||
|
|
57
|
+
(!relativeWorkspace.startsWith("..") && !path.isAbsolute(relativeWorkspace))
|
|
58
|
+
) {
|
|
59
|
+
return relativeWorkspace ? safeTerminalLine(relativeWorkspace) : ".";
|
|
60
|
+
}
|
|
61
|
+
const home = path.resolve(os.homedir());
|
|
62
|
+
const relativeHome = path.relative(home, resolved);
|
|
63
|
+
if (relativeHome === "" || (!relativeHome.startsWith("..") && !path.isAbsolute(relativeHome))) {
|
|
64
|
+
return relativeHome ? `~/${safeTerminalLine(relativeHome)}` : "~";
|
|
65
|
+
}
|
|
66
|
+
return safeTerminalLine(resolved);
|
|
67
|
+
}
|
package/src/settings.ts
CHANGED
|
@@ -5,12 +5,19 @@ import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
|
5
5
|
import lockfile from "proper-lockfile";
|
|
6
6
|
import {
|
|
7
7
|
type AgentConfig,
|
|
8
|
+
CONSULT_RESOURCE_POLICIES,
|
|
9
|
+
CONSULTATION_CWD_POLICIES,
|
|
8
10
|
type CompletionDelivery,
|
|
11
|
+
type ConsultationCwdPolicy,
|
|
12
|
+
type ConsultResourcePolicy,
|
|
13
|
+
DELEGATION_CWD_POLICIES,
|
|
14
|
+
type DelegationCwdPolicy,
|
|
9
15
|
isThinkingLevel,
|
|
10
16
|
type SubagentAgentConfig,
|
|
11
17
|
type SubagentSettings,
|
|
12
18
|
type SubagentThinkingLevel,
|
|
13
19
|
} from "./agents.js";
|
|
20
|
+
import { MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
|
|
14
21
|
|
|
15
22
|
export function hasOwn(obj: object, key: PropertyKey): boolean {
|
|
16
23
|
return Object.hasOwn(obj, key);
|
|
@@ -61,7 +68,12 @@ export function normalizeAgentSettings(value: unknown): SubagentAgentConfig | un
|
|
|
61
68
|
}
|
|
62
69
|
|
|
63
70
|
if (hasOwn(value, "timeoutMs")) {
|
|
64
|
-
if (
|
|
71
|
+
if (
|
|
72
|
+
value.timeoutMs !== null &&
|
|
73
|
+
(!isPositiveNumber(value.timeoutMs) || value.timeoutMs > MAX_SUBAGENT_TIMEOUT_MS)
|
|
74
|
+
) {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
65
77
|
config.timeoutMs = value.timeoutMs;
|
|
66
78
|
hasKnownField = true;
|
|
67
79
|
}
|
|
@@ -136,12 +148,52 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
|
|
|
136
148
|
}
|
|
137
149
|
settings.stateful = runtime;
|
|
138
150
|
}
|
|
151
|
+
if (hasOwn(value, "consult")) {
|
|
152
|
+
if (!isPlainObject(value.consult)) return undefined;
|
|
153
|
+
const consult: NonNullable<SubagentSettings["consult"]> = {};
|
|
154
|
+
if (hasOwn(value.consult, "resources")) {
|
|
155
|
+
if (
|
|
156
|
+
typeof value.consult.resources !== "string" ||
|
|
157
|
+
!CONSULT_RESOURCE_POLICIES.includes(value.consult.resources as ConsultResourcePolicy)
|
|
158
|
+
) {
|
|
159
|
+
return undefined;
|
|
160
|
+
}
|
|
161
|
+
consult.resources = value.consult.resources as ConsultResourcePolicy;
|
|
162
|
+
}
|
|
163
|
+
settings.consult = consult;
|
|
164
|
+
}
|
|
165
|
+
if (hasOwn(value, "cwdPolicy")) {
|
|
166
|
+
if (!isPlainObject(value.cwdPolicy)) return undefined;
|
|
167
|
+
const cwdPolicy: NonNullable<SubagentSettings["cwdPolicy"]> = {};
|
|
168
|
+
if (hasOwn(value.cwdPolicy, "consultation")) {
|
|
169
|
+
if (
|
|
170
|
+
typeof value.cwdPolicy.consultation !== "string" ||
|
|
171
|
+
!CONSULTATION_CWD_POLICIES.includes(value.cwdPolicy.consultation as ConsultationCwdPolicy)
|
|
172
|
+
) {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
cwdPolicy.consultation = value.cwdPolicy.consultation as ConsultationCwdPolicy;
|
|
176
|
+
}
|
|
177
|
+
if (hasOwn(value.cwdPolicy, "delegation")) {
|
|
178
|
+
if (
|
|
179
|
+
typeof value.cwdPolicy.delegation !== "string" ||
|
|
180
|
+
!DELEGATION_CWD_POLICIES.includes(value.cwdPolicy.delegation as DelegationCwdPolicy)
|
|
181
|
+
) {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
cwdPolicy.delegation = value.cwdPolicy.delegation as DelegationCwdPolicy;
|
|
185
|
+
}
|
|
186
|
+
settings.cwdPolicy = cwdPolicy;
|
|
187
|
+
}
|
|
139
188
|
return settings;
|
|
140
189
|
}
|
|
141
190
|
|
|
142
191
|
const SETTINGS_FILE = "pi-subagents.json";
|
|
143
192
|
const LEGACY_SETTINGS_FILE = "pi-subagents-config.json";
|
|
144
193
|
const DEFAULT_COMPLETION_DELIVERY: CompletionDelivery = "next-turn";
|
|
194
|
+
export const DEFAULT_CONSULT_RESOURCE_POLICY: ConsultResourcePolicy = "project-context";
|
|
195
|
+
export const DEFAULT_CONSULTATION_CWD_POLICY: ConsultationCwdPolicy = "anywhere";
|
|
196
|
+
export const DEFAULT_DELEGATION_CWD_POLICY: DelegationCwdPolicy = "trusted-targets";
|
|
145
197
|
const SETTINGS_LOCK_FS_ADAPTER = {
|
|
146
198
|
mkdir: fs.mkdir,
|
|
147
199
|
mkdirSync: fs.mkdirSync,
|
|
@@ -231,6 +283,32 @@ export interface CompletionDeliverySettingsSnapshot {
|
|
|
231
283
|
error?: string;
|
|
232
284
|
}
|
|
233
285
|
|
|
286
|
+
export interface ConsultResourceSettingsSnapshot {
|
|
287
|
+
path: string;
|
|
288
|
+
value: ConsultResourcePolicy;
|
|
289
|
+
source: "default" | "user settings";
|
|
290
|
+
error?: string;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export interface CwdPolicyFieldSnapshot<T> {
|
|
294
|
+
value: T;
|
|
295
|
+
source: "default" | "user settings";
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export interface CwdPolicySettingsSnapshot {
|
|
299
|
+
path: string;
|
|
300
|
+
consultation: CwdPolicyFieldSnapshot<ConsultationCwdPolicy>;
|
|
301
|
+
delegation: CwdPolicyFieldSnapshot<DelegationCwdPolicy>;
|
|
302
|
+
error?: string;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export interface SubagentSettingsSnapshot {
|
|
306
|
+
path: string;
|
|
307
|
+
settings?: SubagentSettings;
|
|
308
|
+
source: "default" | "user settings";
|
|
309
|
+
error?: string;
|
|
310
|
+
}
|
|
311
|
+
|
|
234
312
|
export function subagentSettingsFilePath(): string {
|
|
235
313
|
return path.join(getAgentDir(), SETTINGS_FILE);
|
|
236
314
|
}
|
|
@@ -265,16 +343,81 @@ function inspectSubagentSettingsPath(configPath: string): {
|
|
|
265
343
|
settings?: SubagentSettings;
|
|
266
344
|
error?: string;
|
|
267
345
|
} {
|
|
346
|
+
const fileName = path.basename(configPath);
|
|
347
|
+
let contents: string;
|
|
268
348
|
try {
|
|
269
|
-
|
|
270
|
-
const settings = normalizeSubagentSettings(raw);
|
|
271
|
-
if (!isPlainObject(raw) || !settings) {
|
|
272
|
-
throw new Error(`${path.basename(configPath)} is not a valid settings object`);
|
|
273
|
-
}
|
|
274
|
-
return { path: configPath, raw, settings };
|
|
349
|
+
contents = fs.readFileSync(configPath, "utf8");
|
|
275
350
|
} catch (error) {
|
|
276
|
-
|
|
351
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
352
|
+
return {
|
|
353
|
+
path: configPath,
|
|
354
|
+
error: `${fileName} could not be read${code ? ` (${safeErrorCode(code)})` : ""}`,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
let raw: unknown;
|
|
358
|
+
try {
|
|
359
|
+
raw = JSON.parse(contents);
|
|
360
|
+
} catch {
|
|
361
|
+
return { path: configPath, error: `${fileName} contains malformed JSON` };
|
|
362
|
+
}
|
|
363
|
+
const settings = normalizeSubagentSettings(raw);
|
|
364
|
+
if (!isPlainObject(raw) || !settings) {
|
|
365
|
+
return { path: configPath, error: `${fileName} is not a valid settings object` };
|
|
366
|
+
}
|
|
367
|
+
return { path: configPath, raw, settings };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export function inspectSubagentSettings(): SubagentSettingsSnapshot {
|
|
371
|
+
const inspected = inspectSubagentSettingsDocument();
|
|
372
|
+
return {
|
|
373
|
+
path: inspected.path,
|
|
374
|
+
settings: inspected.settings,
|
|
375
|
+
source: inspected.settings ? "user settings" : "default",
|
|
376
|
+
...(inspected.error ? { error: inspected.error } : {}),
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export function inspectConsultResourceSettings(): ConsultResourceSettingsSnapshot {
|
|
381
|
+
const inspected = inspectSubagentSettingsDocument();
|
|
382
|
+
if (!inspected.raw || !inspected.settings) {
|
|
383
|
+
return {
|
|
384
|
+
path: inspected.path,
|
|
385
|
+
value: DEFAULT_CONSULT_RESOURCE_POLICY,
|
|
386
|
+
source: "default",
|
|
387
|
+
...(inspected.error ? { error: inspected.error } : {}),
|
|
388
|
+
};
|
|
277
389
|
}
|
|
390
|
+
const explicit =
|
|
391
|
+
isPlainObject(inspected.raw.consult) && hasOwn(inspected.raw.consult, "resources");
|
|
392
|
+
return {
|
|
393
|
+
path: inspected.path,
|
|
394
|
+
value: inspected.settings.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY,
|
|
395
|
+
source: explicit ? "user settings" : "default",
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function inspectCwdPolicySettings(): CwdPolicySettingsSnapshot {
|
|
400
|
+
const inspected = inspectSubagentSettingsDocument();
|
|
401
|
+
if (!inspected.raw || !inspected.settings) {
|
|
402
|
+
return {
|
|
403
|
+
path: inspected.path,
|
|
404
|
+
consultation: { value: DEFAULT_CONSULTATION_CWD_POLICY, source: "default" },
|
|
405
|
+
delegation: { value: DEFAULT_DELEGATION_CWD_POLICY, source: "default" },
|
|
406
|
+
...(inspected.error ? { error: inspected.error } : {}),
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const rawPolicy = isPlainObject(inspected.raw.cwdPolicy) ? inspected.raw.cwdPolicy : undefined;
|
|
410
|
+
return {
|
|
411
|
+
path: inspected.path,
|
|
412
|
+
consultation: {
|
|
413
|
+
value: inspected.settings.cwdPolicy?.consultation ?? DEFAULT_CONSULTATION_CWD_POLICY,
|
|
414
|
+
source: rawPolicy && hasOwn(rawPolicy, "consultation") ? "user settings" : "default",
|
|
415
|
+
},
|
|
416
|
+
delegation: {
|
|
417
|
+
value: inspected.settings.cwdPolicy?.delegation ?? DEFAULT_DELEGATION_CWD_POLICY,
|
|
418
|
+
source: rawPolicy && hasOwn(rawPolicy, "delegation") ? "user settings" : "default",
|
|
419
|
+
},
|
|
420
|
+
};
|
|
278
421
|
}
|
|
279
422
|
|
|
280
423
|
export function inspectDelegationWorkflowSettings(): DelegationWorkflowSettingsSnapshot {
|
|
@@ -371,6 +514,50 @@ export function updateCompletionDeliverySetting(value: CompletionDelivery): void
|
|
|
371
514
|
});
|
|
372
515
|
}
|
|
373
516
|
|
|
517
|
+
export function updateConsultResourceSetting(value: ConsultResourcePolicy): void {
|
|
518
|
+
withSettingsMutationLock(() => {
|
|
519
|
+
const update = readSettingsObjectForUpdate();
|
|
520
|
+
const raw = update.document;
|
|
521
|
+
const consult = raw.consult;
|
|
522
|
+
if (consult !== undefined && !isPlainObject(consult)) {
|
|
523
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} consult settings`);
|
|
524
|
+
}
|
|
525
|
+
writeSettingsObjectUnlocked(
|
|
526
|
+
{
|
|
527
|
+
...raw,
|
|
528
|
+
consult: {
|
|
529
|
+
...(consult ?? {}),
|
|
530
|
+
resources: value,
|
|
531
|
+
},
|
|
532
|
+
},
|
|
533
|
+
update.replaceCanonical,
|
|
534
|
+
);
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
export function updateCwdPolicySetting(field: "consultation", value: ConsultationCwdPolicy): void;
|
|
539
|
+
export function updateCwdPolicySetting(field: "delegation", value: DelegationCwdPolicy): void;
|
|
540
|
+
export function updateCwdPolicySetting(
|
|
541
|
+
field: "consultation" | "delegation",
|
|
542
|
+
value: ConsultationCwdPolicy | DelegationCwdPolicy,
|
|
543
|
+
): void {
|
|
544
|
+
withSettingsMutationLock(() => {
|
|
545
|
+
const update = readSettingsObjectForUpdate();
|
|
546
|
+
const raw = update.document;
|
|
547
|
+
const cwdPolicy = raw.cwdPolicy;
|
|
548
|
+
if (cwdPolicy !== undefined && !isPlainObject(cwdPolicy)) {
|
|
549
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} cwdPolicy settings`);
|
|
550
|
+
}
|
|
551
|
+
writeSettingsObjectUnlocked(
|
|
552
|
+
{
|
|
553
|
+
...raw,
|
|
554
|
+
cwdPolicy: { ...(cwdPolicy ?? {}), [field]: value },
|
|
555
|
+
},
|
|
556
|
+
update.replaceCanonical,
|
|
557
|
+
);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
374
561
|
export function updateAgentToolsSetting(name: string, tools: string[] | undefined): void {
|
|
375
562
|
withSettingsMutationLock(() => {
|
|
376
563
|
const update = readSettingsObjectForUpdate();
|
|
@@ -417,8 +604,8 @@ function readSettingsObjectForUpdate(): SettingsObjectForUpdate {
|
|
|
417
604
|
let parsed: unknown;
|
|
418
605
|
try {
|
|
419
606
|
parsed = JSON.parse(fs.readFileSync(activePath, "utf8"));
|
|
420
|
-
} catch
|
|
421
|
-
throw new Error(`Cannot update malformed ${activeFile}
|
|
607
|
+
} catch {
|
|
608
|
+
throw new Error(`Cannot update malformed ${activeFile}`);
|
|
422
609
|
}
|
|
423
610
|
if (!isPlainObject(parsed) || !normalizeSubagentSettings(parsed)) {
|
|
424
611
|
throw new Error(`Cannot update invalid ${activeFile}`);
|
|
@@ -498,8 +685,8 @@ function readSettingsSnapshot(configPath: string): {
|
|
|
498
685
|
}
|
|
499
686
|
}
|
|
500
687
|
|
|
501
|
-
function
|
|
502
|
-
return
|
|
688
|
+
function safeErrorCode(value: string): string {
|
|
689
|
+
return value.replace(/[^A-Z0-9_-]/giu, "?").slice(0, 64);
|
|
503
690
|
}
|
|
504
691
|
|
|
505
692
|
export function uniqueToolNames(tools: string[]): string[] {
|