@narumitw/pi-subagents 0.41.0 → 0.43.0
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 +96 -26
- package/package.json +2 -2
- package/src/agents.ts +33 -8
- package/src/config-ui.ts +85 -21
- package/src/consult-policy.ts +15 -0
- package/src/consult.ts +688 -0
- package/src/execution.ts +8 -2
- package/src/inspect.ts +405 -0
- package/src/limits.ts +1 -0
- package/src/params.ts +2 -0
- package/src/registry.ts +75 -0
- package/src/runner.ts +160 -22
- package/src/safe-text.ts +67 -0
- package/src/settings.ts +88 -1
- package/src/stateful.ts +24 -12
- package/src/subagents.ts +43 -4
package/src/runner.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
DEFAULT_MAX_MESSAGES,
|
|
13
13
|
DEFAULT_MAX_OUTPUT_BYTES,
|
|
14
14
|
DEFAULT_MAX_STDERR_BYTES,
|
|
15
|
+
MAX_SUBAGENT_TIMEOUT_MS,
|
|
15
16
|
truncateUtf8,
|
|
16
17
|
} from "./limits.js";
|
|
17
18
|
import { JsonLineDecoder } from "./protocol.js";
|
|
@@ -24,6 +25,11 @@ export interface UsageStats {
|
|
|
24
25
|
cacheRead: number;
|
|
25
26
|
cacheWrite: number;
|
|
26
27
|
cost: number;
|
|
28
|
+
costInput?: number;
|
|
29
|
+
costOutput?: number;
|
|
30
|
+
costCacheRead?: number;
|
|
31
|
+
costCacheWrite?: number;
|
|
32
|
+
totalTokens?: number;
|
|
27
33
|
contextTokens: number;
|
|
28
34
|
turns: number;
|
|
29
35
|
}
|
|
@@ -34,6 +40,21 @@ export type RecentActivityItem =
|
|
|
34
40
|
const MAX_RECENT_ACTIVITY_ITEMS = 10;
|
|
35
41
|
const MAX_RECENT_ACTIVITY_BYTES = 8 * 1024;
|
|
36
42
|
const MAX_RECENT_ACTIVITY_ARGUMENT_BYTES = 1024;
|
|
43
|
+
const MAX_USAGE_VALUE = Number.MAX_SAFE_INTEGER;
|
|
44
|
+
|
|
45
|
+
function protocolUsageCount(value: unknown): number {
|
|
46
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function protocolUsageCost(value: unknown): number {
|
|
50
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0
|
|
51
|
+
? Math.min(value, MAX_USAGE_VALUE)
|
|
52
|
+
: 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function addUsageValue(current: number, addition: number): number {
|
|
56
|
+
return Math.min(MAX_USAGE_VALUE, current + addition);
|
|
57
|
+
}
|
|
37
58
|
|
|
38
59
|
export interface SingleResult {
|
|
39
60
|
agent: string;
|
|
@@ -58,6 +79,8 @@ export interface SingleResult {
|
|
|
58
79
|
aborted?: boolean;
|
|
59
80
|
truncated?: boolean;
|
|
60
81
|
malformedEvents?: number;
|
|
82
|
+
launchFailed?: boolean;
|
|
83
|
+
processStarted?: boolean;
|
|
61
84
|
policy?: {
|
|
62
85
|
inherited: string[];
|
|
63
86
|
overridden: string[];
|
|
@@ -288,20 +311,41 @@ async function writePromptToTempFile(
|
|
|
288
311
|
return { dir: tmpDir, filePath };
|
|
289
312
|
}
|
|
290
313
|
|
|
291
|
-
export
|
|
314
|
+
export interface PiArgsOptions {
|
|
292
315
|
model?: string;
|
|
293
316
|
thinkingLevel?: SubagentThinkingLevel;
|
|
294
317
|
tools?: string[];
|
|
318
|
+
disableExtensions?: boolean;
|
|
319
|
+
disableSkills?: boolean;
|
|
320
|
+
disablePromptTemplates?: boolean;
|
|
321
|
+
disableContextFiles?: boolean;
|
|
322
|
+
projectTrust?: boolean;
|
|
323
|
+
baseSystemPromptPath?: string;
|
|
324
|
+
appendSystemPromptPaths?: string[];
|
|
325
|
+
/** Existing single append prompt path retained for compatibility. */
|
|
295
326
|
systemPromptPath?: string;
|
|
296
327
|
task: string;
|
|
297
|
-
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function buildPiArgs(options: PiArgsOptions): string[] {
|
|
298
331
|
const args: string[] = ["--mode", "json", "-p", "--no-session"];
|
|
299
332
|
if (options.model) args.push("--model", options.model);
|
|
300
333
|
if (options.thinkingLevel) args.push("--thinking", options.thinkingLevel);
|
|
334
|
+
if (options.disableExtensions) args.push("--no-extensions");
|
|
335
|
+
if (options.disableSkills) args.push("--no-skills");
|
|
336
|
+
if (options.disablePromptTemplates) args.push("--no-prompt-templates");
|
|
337
|
+
if (options.disableContextFiles) args.push("--no-context-files");
|
|
338
|
+
if (options.projectTrust !== undefined) {
|
|
339
|
+
args.push(options.projectTrust ? "--approve" : "--no-approve");
|
|
340
|
+
}
|
|
301
341
|
if (Array.isArray(options.tools)) {
|
|
302
342
|
if (options.tools.length > 0) args.push("--tools", options.tools.join(","));
|
|
303
343
|
else args.push("--no-tools");
|
|
304
344
|
}
|
|
345
|
+
if (options.baseSystemPromptPath) args.push("--system-prompt", options.baseSystemPromptPath);
|
|
346
|
+
for (const promptPath of options.appendSystemPromptPaths ?? []) {
|
|
347
|
+
args.push("--append-system-prompt", promptPath);
|
|
348
|
+
}
|
|
305
349
|
if (options.systemPromptPath) args.push("--append-system-prompt", options.systemPromptPath);
|
|
306
350
|
args.push(`Task: ${options.task}`);
|
|
307
351
|
return args;
|
|
@@ -365,6 +409,17 @@ export function terminateProcess(
|
|
|
365
409
|
|
|
366
410
|
export type OnUpdateCallback = (partial: AgentToolResult<SubagentDetails>) => void;
|
|
367
411
|
|
|
412
|
+
export interface ChildLaunchPolicy {
|
|
413
|
+
tools?: string[];
|
|
414
|
+
disableExtensions?: boolean;
|
|
415
|
+
disableSkills?: boolean;
|
|
416
|
+
disablePromptTemplates?: boolean;
|
|
417
|
+
disableContextFiles?: boolean;
|
|
418
|
+
projectTrust?: boolean;
|
|
419
|
+
baseSystemPrompt?: string;
|
|
420
|
+
appendSystemPromptPaths?: string[];
|
|
421
|
+
}
|
|
422
|
+
|
|
368
423
|
export async function runSingleAgent(
|
|
369
424
|
defaultCwd: string,
|
|
370
425
|
agents: AgentConfig[],
|
|
@@ -378,6 +433,7 @@ export async function runSingleAgent(
|
|
|
378
433
|
onUpdate: OnUpdateCallback | undefined,
|
|
379
434
|
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
380
435
|
invocationOverride?: { command: string; argsPrefix?: string[] },
|
|
436
|
+
launchPolicy?: ChildLaunchPolicy,
|
|
381
437
|
): Promise<SingleResult> {
|
|
382
438
|
const agent = agents.find((a) => a.name === agentName);
|
|
383
439
|
|
|
@@ -405,8 +461,9 @@ export async function runSingleAgent(
|
|
|
405
461
|
};
|
|
406
462
|
}
|
|
407
463
|
|
|
408
|
-
|
|
464
|
+
const temporaryPrompts: Array<{ dir: string; filePath: string }> = [];
|
|
409
465
|
let tmpPromptPath: string | null = null;
|
|
466
|
+
let baseSystemPromptPath: string | null = null;
|
|
410
467
|
|
|
411
468
|
let latestAssistantOutput = "";
|
|
412
469
|
let terminalAssistantOutput: string | undefined;
|
|
@@ -474,17 +531,46 @@ export async function runSingleAgent(
|
|
|
474
531
|
setErrorMessage("Subagent was aborted before start");
|
|
475
532
|
return currentResult;
|
|
476
533
|
}
|
|
534
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_SUBAGENT_TIMEOUT_MS) {
|
|
535
|
+
currentResult.exitCode = 1;
|
|
536
|
+
currentResult.stopReason = "error";
|
|
537
|
+
setErrorMessage(
|
|
538
|
+
`Invalid subagent timeout: expected 1-${MAX_SUBAGENT_TIMEOUT_MS}ms, received ${timeoutMs}`,
|
|
539
|
+
);
|
|
540
|
+
return currentResult;
|
|
541
|
+
}
|
|
477
542
|
|
|
543
|
+
if (launchPolicy?.baseSystemPrompt?.trim()) {
|
|
544
|
+
const tmp = await writePromptToTempFile(`${agent.name}-base`, launchPolicy.baseSystemPrompt);
|
|
545
|
+
temporaryPrompts.push(tmp);
|
|
546
|
+
baseSystemPromptPath = tmp.filePath;
|
|
547
|
+
}
|
|
478
548
|
if (agent.systemPrompt.trim()) {
|
|
479
549
|
const tmp = await writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
480
|
-
|
|
550
|
+
temporaryPrompts.push(tmp);
|
|
481
551
|
tmpPromptPath = tmp.filePath;
|
|
482
552
|
}
|
|
553
|
+
if (signal?.aborted) {
|
|
554
|
+
currentResult.exitCode = 130;
|
|
555
|
+
currentResult.aborted = true;
|
|
556
|
+
currentResult.stopReason = "aborted";
|
|
557
|
+
setErrorMessage("Subagent was aborted before launch");
|
|
558
|
+
return currentResult;
|
|
559
|
+
}
|
|
483
560
|
|
|
561
|
+
const effectiveTools =
|
|
562
|
+
launchPolicy && Object.hasOwn(launchPolicy, "tools") ? launchPolicy.tools : agent.tools;
|
|
484
563
|
const args = buildPiArgs({
|
|
485
564
|
model: agent.model,
|
|
486
565
|
thinkingLevel,
|
|
487
|
-
tools:
|
|
566
|
+
tools: effectiveTools,
|
|
567
|
+
disableExtensions: launchPolicy?.disableExtensions,
|
|
568
|
+
disableSkills: launchPolicy?.disableSkills,
|
|
569
|
+
disablePromptTemplates: launchPolicy?.disablePromptTemplates,
|
|
570
|
+
disableContextFiles: launchPolicy?.disableContextFiles,
|
|
571
|
+
projectTrust: launchPolicy?.projectTrust,
|
|
572
|
+
baseSystemPromptPath: baseSystemPromptPath ?? undefined,
|
|
573
|
+
appendSystemPromptPaths: launchPolicy?.appendSystemPromptPaths,
|
|
488
574
|
systemPromptPath: tmpPromptPath ?? undefined,
|
|
489
575
|
task,
|
|
490
576
|
});
|
|
@@ -525,6 +611,7 @@ export async function runSingleAgent(
|
|
|
525
611
|
},
|
|
526
612
|
});
|
|
527
613
|
} catch (error) {
|
|
614
|
+
currentResult.launchFailed = true;
|
|
528
615
|
currentResult.stderr = setErrorMessage(
|
|
529
616
|
error instanceof Error ? error.message : String(error),
|
|
530
617
|
);
|
|
@@ -576,19 +663,62 @@ export async function runSingleAgent(
|
|
|
576
663
|
if (msg.role === "assistant") {
|
|
577
664
|
currentResult.usage.turns++;
|
|
578
665
|
const usage = msg.usage;
|
|
579
|
-
if (usage) {
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
666
|
+
if (usage && typeof usage === "object") {
|
|
667
|
+
const input = protocolUsageCount(usage.input);
|
|
668
|
+
const output = protocolUsageCount(usage.output);
|
|
669
|
+
const cacheRead = protocolUsageCount(usage.cacheRead);
|
|
670
|
+
const cacheWrite = protocolUsageCount(usage.cacheWrite);
|
|
671
|
+
const reportedTotal = protocolUsageCount(usage.totalTokens);
|
|
672
|
+
const turnTotal =
|
|
673
|
+
reportedTotal ||
|
|
674
|
+
addUsageValue(addUsageValue(input, output), addUsageValue(cacheRead, cacheWrite));
|
|
675
|
+
const cost = usage.cost && typeof usage.cost === "object" ? usage.cost : undefined;
|
|
676
|
+
currentResult.usage.input = addUsageValue(currentResult.usage.input, input);
|
|
677
|
+
currentResult.usage.output = addUsageValue(currentResult.usage.output, output);
|
|
678
|
+
currentResult.usage.cacheRead = addUsageValue(
|
|
679
|
+
currentResult.usage.cacheRead,
|
|
680
|
+
cacheRead,
|
|
681
|
+
);
|
|
682
|
+
currentResult.usage.cacheWrite = addUsageValue(
|
|
683
|
+
currentResult.usage.cacheWrite,
|
|
684
|
+
cacheWrite,
|
|
685
|
+
);
|
|
686
|
+
currentResult.usage.cost = addUsageValue(
|
|
687
|
+
currentResult.usage.cost,
|
|
688
|
+
protocolUsageCost(cost?.total),
|
|
689
|
+
);
|
|
690
|
+
currentResult.usage.costInput = addUsageValue(
|
|
691
|
+
currentResult.usage.costInput ?? 0,
|
|
692
|
+
protocolUsageCost(cost?.input),
|
|
693
|
+
);
|
|
694
|
+
currentResult.usage.costOutput = addUsageValue(
|
|
695
|
+
currentResult.usage.costOutput ?? 0,
|
|
696
|
+
protocolUsageCost(cost?.output),
|
|
697
|
+
);
|
|
698
|
+
currentResult.usage.costCacheRead = addUsageValue(
|
|
699
|
+
currentResult.usage.costCacheRead ?? 0,
|
|
700
|
+
protocolUsageCost(cost?.cacheRead),
|
|
701
|
+
);
|
|
702
|
+
currentResult.usage.costCacheWrite = addUsageValue(
|
|
703
|
+
currentResult.usage.costCacheWrite ?? 0,
|
|
704
|
+
protocolUsageCost(cost?.cacheWrite),
|
|
705
|
+
);
|
|
706
|
+
currentResult.usage.totalTokens = addUsageValue(
|
|
707
|
+
currentResult.usage.totalTokens ?? 0,
|
|
708
|
+
turnTotal,
|
|
709
|
+
);
|
|
710
|
+
currentResult.usage.contextTokens = turnTotal;
|
|
586
711
|
}
|
|
587
|
-
if (msg.provider) currentResult.actualProvider = msg.provider;
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
712
|
+
if (typeof msg.provider === "string") currentResult.actualProvider = msg.provider;
|
|
713
|
+
const actualModel =
|
|
714
|
+
typeof msg.responseModel === "string"
|
|
715
|
+
? msg.responseModel
|
|
716
|
+
: typeof msg.model === "string"
|
|
717
|
+
? msg.model
|
|
718
|
+
: undefined;
|
|
719
|
+
if (actualModel) currentResult.actualModel = actualModel;
|
|
720
|
+
if (typeof msg.stopReason === "string") currentResult.stopReason = msg.stopReason;
|
|
721
|
+
if (typeof msg.errorMessage === "string") setErrorMessage(msg.errorMessage);
|
|
592
722
|
}
|
|
593
723
|
emitUpdate();
|
|
594
724
|
} else if (event.type === "tool_result_end" && event.message) {
|
|
@@ -623,6 +753,9 @@ export async function runSingleAgent(
|
|
|
623
753
|
}, timeoutMs);
|
|
624
754
|
timeout.unref();
|
|
625
755
|
|
|
756
|
+
proc.once("spawn", () => {
|
|
757
|
+
currentResult.processStarted = true;
|
|
758
|
+
});
|
|
626
759
|
proc.stdout?.on("data", (data) => decoder.push(data));
|
|
627
760
|
proc.stderr?.on("data", (data) => {
|
|
628
761
|
const bounded = appendBounded(
|
|
@@ -638,6 +771,7 @@ export async function runSingleAgent(
|
|
|
638
771
|
finish(timedOut ? 124 : wasAborted ? 130 : (code ?? 0));
|
|
639
772
|
});
|
|
640
773
|
proc.on("error", (error) => {
|
|
774
|
+
currentResult.launchFailed = true;
|
|
641
775
|
const message = setErrorMessage(error.message);
|
|
642
776
|
const bounded = appendBounded(
|
|
643
777
|
currentResult.stderr,
|
|
@@ -682,23 +816,27 @@ export async function runSingleAgent(
|
|
|
682
816
|
"cwd",
|
|
683
817
|
...(agent.model ? ["model"] : []),
|
|
684
818
|
...(thinkingLevel ? ["thinkingLevel"] : []),
|
|
685
|
-
...(
|
|
819
|
+
...(effectiveTools !== undefined ? ["tools"] : []),
|
|
820
|
+
...(launchPolicy?.disableExtensions ? ["extensions"] : []),
|
|
821
|
+
...(launchPolicy?.disableSkills ? ["skills"] : []),
|
|
822
|
+
...(launchPolicy?.disablePromptTemplates ? ["promptTemplates"] : []),
|
|
823
|
+
...(launchPolicy?.disableContextFiles ? ["contextFiles"] : []),
|
|
686
824
|
],
|
|
687
825
|
unsupported: ["approvalPolicy", "sandboxProfile", "providerHeaders"],
|
|
688
826
|
};
|
|
689
827
|
return currentResult;
|
|
690
828
|
} finally {
|
|
691
|
-
|
|
829
|
+
for (const temporary of temporaryPrompts.reverse()) {
|
|
692
830
|
try {
|
|
693
|
-
fs.unlinkSync(
|
|
831
|
+
fs.unlinkSync(temporary.filePath);
|
|
694
832
|
} catch {
|
|
695
833
|
/* ignore */
|
|
696
834
|
}
|
|
697
|
-
if (tmpPromptDir)
|
|
698
835
|
try {
|
|
699
|
-
fs.rmdirSync(
|
|
836
|
+
fs.rmdirSync(temporary.dir);
|
|
700
837
|
} catch {
|
|
701
838
|
/* ignore */
|
|
702
839
|
}
|
|
840
|
+
}
|
|
703
841
|
}
|
|
704
842
|
}
|
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,15 @@ 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,
|
|
8
9
|
type CompletionDelivery,
|
|
10
|
+
type ConsultResourcePolicy,
|
|
9
11
|
isThinkingLevel,
|
|
10
12
|
type SubagentAgentConfig,
|
|
11
13
|
type SubagentSettings,
|
|
12
14
|
type SubagentThinkingLevel,
|
|
13
15
|
} from "./agents.js";
|
|
16
|
+
import { MAX_SUBAGENT_TIMEOUT_MS } from "./limits.js";
|
|
14
17
|
|
|
15
18
|
export function hasOwn(obj: object, key: PropertyKey): boolean {
|
|
16
19
|
return Object.hasOwn(obj, key);
|
|
@@ -61,7 +64,12 @@ export function normalizeAgentSettings(value: unknown): SubagentAgentConfig | un
|
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
if (hasOwn(value, "timeoutMs")) {
|
|
64
|
-
if (
|
|
67
|
+
if (
|
|
68
|
+
value.timeoutMs !== null &&
|
|
69
|
+
(!isPositiveNumber(value.timeoutMs) || value.timeoutMs > MAX_SUBAGENT_TIMEOUT_MS)
|
|
70
|
+
) {
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
65
73
|
config.timeoutMs = value.timeoutMs;
|
|
66
74
|
hasKnownField = true;
|
|
67
75
|
}
|
|
@@ -136,12 +144,27 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
|
|
|
136
144
|
}
|
|
137
145
|
settings.stateful = runtime;
|
|
138
146
|
}
|
|
147
|
+
if (hasOwn(value, "consult")) {
|
|
148
|
+
if (!isPlainObject(value.consult)) return undefined;
|
|
149
|
+
const consult: NonNullable<SubagentSettings["consult"]> = {};
|
|
150
|
+
if (hasOwn(value.consult, "resources")) {
|
|
151
|
+
if (
|
|
152
|
+
typeof value.consult.resources !== "string" ||
|
|
153
|
+
!CONSULT_RESOURCE_POLICIES.includes(value.consult.resources as ConsultResourcePolicy)
|
|
154
|
+
) {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
consult.resources = value.consult.resources as ConsultResourcePolicy;
|
|
158
|
+
}
|
|
159
|
+
settings.consult = consult;
|
|
160
|
+
}
|
|
139
161
|
return settings;
|
|
140
162
|
}
|
|
141
163
|
|
|
142
164
|
const SETTINGS_FILE = "pi-subagents.json";
|
|
143
165
|
const LEGACY_SETTINGS_FILE = "pi-subagents-config.json";
|
|
144
166
|
const DEFAULT_COMPLETION_DELIVERY: CompletionDelivery = "next-turn";
|
|
167
|
+
export const DEFAULT_CONSULT_RESOURCE_POLICY: ConsultResourcePolicy = "project-context";
|
|
145
168
|
const SETTINGS_LOCK_FS_ADAPTER = {
|
|
146
169
|
mkdir: fs.mkdir,
|
|
147
170
|
mkdirSync: fs.mkdirSync,
|
|
@@ -231,6 +254,20 @@ export interface CompletionDeliverySettingsSnapshot {
|
|
|
231
254
|
error?: string;
|
|
232
255
|
}
|
|
233
256
|
|
|
257
|
+
export interface ConsultResourceSettingsSnapshot {
|
|
258
|
+
path: string;
|
|
259
|
+
value: ConsultResourcePolicy;
|
|
260
|
+
source: "default" | "user settings";
|
|
261
|
+
error?: string;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export interface SubagentSettingsSnapshot {
|
|
265
|
+
path: string;
|
|
266
|
+
settings?: SubagentSettings;
|
|
267
|
+
source: "default" | "user settings";
|
|
268
|
+
error?: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
234
271
|
export function subagentSettingsFilePath(): string {
|
|
235
272
|
return path.join(getAgentDir(), SETTINGS_FILE);
|
|
236
273
|
}
|
|
@@ -277,6 +314,35 @@ function inspectSubagentSettingsPath(configPath: string): {
|
|
|
277
314
|
}
|
|
278
315
|
}
|
|
279
316
|
|
|
317
|
+
export function inspectSubagentSettings(): SubagentSettingsSnapshot {
|
|
318
|
+
const inspected = inspectSubagentSettingsDocument();
|
|
319
|
+
return {
|
|
320
|
+
path: inspected.path,
|
|
321
|
+
settings: inspected.settings,
|
|
322
|
+
source: inspected.settings ? "user settings" : "default",
|
|
323
|
+
...(inspected.error ? { error: inspected.error } : {}),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function inspectConsultResourceSettings(): ConsultResourceSettingsSnapshot {
|
|
328
|
+
const inspected = inspectSubagentSettingsDocument();
|
|
329
|
+
if (!inspected.raw || !inspected.settings) {
|
|
330
|
+
return {
|
|
331
|
+
path: inspected.path,
|
|
332
|
+
value: DEFAULT_CONSULT_RESOURCE_POLICY,
|
|
333
|
+
source: "default",
|
|
334
|
+
...(inspected.error ? { error: inspected.error } : {}),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
const explicit =
|
|
338
|
+
isPlainObject(inspected.raw.consult) && hasOwn(inspected.raw.consult, "resources");
|
|
339
|
+
return {
|
|
340
|
+
path: inspected.path,
|
|
341
|
+
value: inspected.settings.consult?.resources ?? DEFAULT_CONSULT_RESOURCE_POLICY,
|
|
342
|
+
source: explicit ? "user settings" : "default",
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
280
346
|
export function inspectDelegationWorkflowSettings(): DelegationWorkflowSettingsSnapshot {
|
|
281
347
|
const inspected = inspectSubagentSettingsDocument();
|
|
282
348
|
if (!inspected.raw || !inspected.settings) {
|
|
@@ -371,6 +437,27 @@ export function updateCompletionDeliverySetting(value: CompletionDelivery): void
|
|
|
371
437
|
});
|
|
372
438
|
}
|
|
373
439
|
|
|
440
|
+
export function updateConsultResourceSetting(value: ConsultResourcePolicy): void {
|
|
441
|
+
withSettingsMutationLock(() => {
|
|
442
|
+
const update = readSettingsObjectForUpdate();
|
|
443
|
+
const raw = update.document;
|
|
444
|
+
const consult = raw.consult;
|
|
445
|
+
if (consult !== undefined && !isPlainObject(consult)) {
|
|
446
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} consult settings`);
|
|
447
|
+
}
|
|
448
|
+
writeSettingsObjectUnlocked(
|
|
449
|
+
{
|
|
450
|
+
...raw,
|
|
451
|
+
consult: {
|
|
452
|
+
...(consult ?? {}),
|
|
453
|
+
resources: value,
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
update.replaceCanonical,
|
|
457
|
+
);
|
|
458
|
+
});
|
|
459
|
+
}
|
|
460
|
+
|
|
374
461
|
export function updateAgentToolsSetting(name: string, tools: string[] | undefined): void {
|
|
375
462
|
withSettingsMutationLock(() => {
|
|
376
463
|
const update = readSettingsObjectForUpdate();
|
package/src/stateful.ts
CHANGED
|
@@ -24,7 +24,14 @@ import {
|
|
|
24
24
|
} from "./in-process-transport.js";
|
|
25
25
|
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
26
26
|
import { AgentPersistence } from "./persistence.js";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
AgentRegistry,
|
|
29
|
+
type AgentRunInspectionDetail,
|
|
30
|
+
type AgentRunInspectionSummary,
|
|
31
|
+
type AgentTurnCompletion,
|
|
32
|
+
type ManagedAgent,
|
|
33
|
+
} from "./registry.js";
|
|
34
|
+
import { safeTerminalLine } from "./safe-text.js";
|
|
28
35
|
import { readSubagentSettings } from "./settings.js";
|
|
29
36
|
import {
|
|
30
37
|
MailboxParamsSchema,
|
|
@@ -109,6 +116,8 @@ export interface StatefulSubagentController {
|
|
|
109
116
|
setAgentCatalog(value: string): void;
|
|
110
117
|
getRuntimeStatus(): StatefulSubagentRuntimeStatus;
|
|
111
118
|
listAgents(includeClosed?: boolean): ManagedAgent[];
|
|
119
|
+
listRunInspection(includeClosed?: boolean): AgentRunInspectionSummary[];
|
|
120
|
+
getRunInspection(agentId: string): AgentRunInspectionDetail | undefined;
|
|
112
121
|
clearAgents(): Promise<number>;
|
|
113
122
|
}
|
|
114
123
|
|
|
@@ -168,21 +177,24 @@ export function registerStatefulSubagents(
|
|
|
168
177
|
refreshSpawnToolRegistration?.();
|
|
169
178
|
},
|
|
170
179
|
getRuntimeStatus() {
|
|
171
|
-
const
|
|
180
|
+
const counts = registry?.inspectionCounts() ?? { activeAgents: 0, retainedAgents: 0 };
|
|
172
181
|
return {
|
|
173
182
|
enabled,
|
|
174
183
|
initialized: registry !== undefined,
|
|
175
184
|
transport: transportKind,
|
|
176
185
|
completionDelivery,
|
|
177
|
-
|
|
178
|
-
(agent) => agent.state === "starting" || agent.state === "running",
|
|
179
|
-
).length,
|
|
180
|
-
retainedAgents: agents.filter((agent) => agent.state !== "closed").length,
|
|
186
|
+
...counts,
|
|
181
187
|
};
|
|
182
188
|
},
|
|
183
189
|
listAgents(includeClosed = false) {
|
|
184
190
|
return registry?.list(includeClosed) ?? [];
|
|
185
191
|
},
|
|
192
|
+
listRunInspection(includeClosed = false) {
|
|
193
|
+
return registry?.listInspection(includeClosed) ?? [];
|
|
194
|
+
},
|
|
195
|
+
getRunInspection(agentId) {
|
|
196
|
+
return registry?.getInspection(agentId);
|
|
197
|
+
},
|
|
186
198
|
clearAgents,
|
|
187
199
|
};
|
|
188
200
|
if (!enabled) return controller;
|
|
@@ -450,7 +462,7 @@ export function registerStatefulSubagents(
|
|
|
450
462
|
name: "subagent_manage",
|
|
451
463
|
label: "Manage Subagents",
|
|
452
464
|
description:
|
|
453
|
-
"List retained subagents, interrupt active work while keeping an agent reusable, or close agents and release their resources.",
|
|
465
|
+
"List retained subagents through the compatibility route, interrupt active work while keeping an agent reusable, or close agents and release their resources. Prefer subagent_inspect when the whole activated capability must be read-only.",
|
|
454
466
|
promptSnippet: "List or control retained detached subagents",
|
|
455
467
|
parameters: ManageParamsSchema,
|
|
456
468
|
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
@@ -518,7 +530,7 @@ export function registerStatefulSubagents(
|
|
|
518
530
|
name: "subagent_mailbox",
|
|
519
531
|
label: "Subagent Mailbox",
|
|
520
532
|
description:
|
|
521
|
-
"Queue a bounded message without starting a turn, or read unread mailbox messages
|
|
533
|
+
"Queue a bounded message without starting a turn, or read unread mailbox messages. Read acknowledges returned messages by default; use subagent_inspect for metadata-only unread counts.",
|
|
522
534
|
promptSnippet: "Send or read queue-only detached-subagent mailbox messages",
|
|
523
535
|
parameters: MailboxParamsSchema,
|
|
524
536
|
async execute(_id, params): Promise<StatefulActionToolResult> {
|
|
@@ -608,19 +620,19 @@ async function confirmProjectAgent(
|
|
|
608
620
|
cwd: string,
|
|
609
621
|
): Promise<void> {
|
|
610
622
|
if (scope !== "project" && scope !== "both") return;
|
|
611
|
-
const discovery = discoverAgents(cwd, scope, readSubagentSettings());
|
|
612
|
-
const agent = discovery.agents.find((candidate) => candidate.name === name);
|
|
613
|
-
if (agent?.source !== "project") return;
|
|
614
623
|
if (!isSameCwd(cwd, ctx.cwd)) {
|
|
615
624
|
throw new Error("Project-local subagent definitions cannot run with an overridden cwd");
|
|
616
625
|
}
|
|
617
626
|
if (!ctx.isProjectTrusted()) {
|
|
618
627
|
throw new Error("Project-local subagent definitions require a trusted project");
|
|
619
628
|
}
|
|
629
|
+
const discovery = discoverAgents(cwd, scope, readSubagentSettings());
|
|
630
|
+
const agent = discovery.agents.find((candidate) => candidate.name === name);
|
|
631
|
+
if (agent?.source !== "project") return;
|
|
620
632
|
if (confirm && ctx.hasUI) {
|
|
621
633
|
const approved = await ctx.ui.confirm(
|
|
622
634
|
"Run project-local agent?",
|
|
623
|
-
`Agent: ${name}\nSource: ${agent.filePath}`,
|
|
635
|
+
`Agent: ${safeTerminalLine(name, 256)}\nSource: ${safeTerminalLine(agent.filePath)}`,
|
|
624
636
|
);
|
|
625
637
|
if (!approved) throw new Error("Project-local subagent was not approved");
|
|
626
638
|
}
|