@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/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 function buildPiArgs(options: {
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
- }): string[] {
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
- let tmpPromptDir: string | null = null;
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
- tmpPromptDir = tmp.dir;
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: agent.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
- currentResult.usage.input += usage.input || 0;
581
- currentResult.usage.output += usage.output || 0;
582
- currentResult.usage.cacheRead += usage.cacheRead || 0;
583
- currentResult.usage.cacheWrite += usage.cacheWrite || 0;
584
- currentResult.usage.cost += usage.cost?.total || 0;
585
- currentResult.usage.contextTokens = usage.totalTokens || 0;
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
- if (msg.responseModel ?? msg.model)
589
- currentResult.actualModel = msg.responseModel ?? msg.model;
590
- if (msg.stopReason) currentResult.stopReason = msg.stopReason;
591
- if (msg.errorMessage) setErrorMessage(msg.errorMessage);
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
- ...(agent.tools ? ["tools"] : []),
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
- if (tmpPromptPath)
829
+ for (const temporary of temporaryPrompts.reverse()) {
692
830
  try {
693
- fs.unlinkSync(tmpPromptPath);
831
+ fs.unlinkSync(temporary.filePath);
694
832
  } catch {
695
833
  /* ignore */
696
834
  }
697
- if (tmpPromptDir)
698
835
  try {
699
- fs.rmdirSync(tmpPromptDir);
836
+ fs.rmdirSync(temporary.dir);
700
837
  } catch {
701
838
  /* ignore */
702
839
  }
840
+ }
703
841
  }
704
842
  }
@@ -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 (value.timeoutMs !== null && !isPositiveNumber(value.timeoutMs)) return undefined;
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 { AgentRegistry, type AgentTurnCompletion, type ManagedAgent } from "./registry.js";
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 agents = registry?.list(true) ?? [];
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
- activeAgents: agents.filter(
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 and optionally acknowledge them.",
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
  }