@oh-my-pi/pi-coding-agent 17.2.12 → 17.2.14

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.
Files changed (143) hide show
  1. package/CHANGELOG.md +63 -0
  2. package/dist/{CHANGELOG-k9ghy5sn.md → CHANGELOG-za420td8.md} +63 -0
  3. package/dist/cli.js +3658 -3627
  4. package/dist/types/advisor/delta-split.d.ts +24 -0
  5. package/dist/types/advisor/runtime.d.ts +2 -2
  6. package/dist/types/async/job-manager.d.ts +8 -1
  7. package/dist/types/cli/update-cli.d.ts +53 -1
  8. package/dist/types/config/keybindings.d.ts +10 -0
  9. package/dist/types/config/model-resolver.d.ts +15 -2
  10. package/dist/types/config/settings-schema.d.ts +14 -0
  11. package/dist/types/discovery/agents-md.d.ts +10 -1
  12. package/dist/types/eval/runner-cache.d.ts +12 -0
  13. package/dist/types/extensibility/extensions/runner.d.ts +12 -3
  14. package/dist/types/extensibility/extensions/types.d.ts +15 -4
  15. package/dist/types/extensibility/plugins/marketplace/manager.d.ts +4 -1
  16. package/dist/types/lib/xai-http.d.ts +0 -1
  17. package/dist/types/mcp/tool-bridge.d.ts +8 -5
  18. package/dist/types/modes/components/agent-hub-renderer.d.ts +6 -1
  19. package/dist/types/modes/components/status-line/types.d.ts +4 -0
  20. package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -4
  21. package/dist/types/modes/interactive-mode.d.ts +17 -8
  22. package/dist/types/modes/types.d.ts +7 -10
  23. package/dist/types/modes/utils/hotkeys-markdown.d.ts +1 -1
  24. package/dist/types/session/agent-session-types.d.ts +4 -0
  25. package/dist/types/session/agent-session.d.ts +24 -3
  26. package/dist/types/session/messages.d.ts +20 -0
  27. package/dist/types/session/retry-fallback-chains.d.ts +13 -0
  28. package/dist/types/session/session-advisors.d.ts +1 -1
  29. package/dist/types/session/session-history-format.d.ts +10 -0
  30. package/dist/types/session/session-maintenance.d.ts +1 -1
  31. package/dist/types/session/session-tools.d.ts +35 -5
  32. package/dist/types/session/turn-recovery.d.ts +34 -5
  33. package/dist/types/slash-commands/types.d.ts +5 -1
  34. package/dist/types/task/executor.d.ts +1 -1
  35. package/dist/types/tools/approval.d.ts +7 -0
  36. package/dist/types/tools/builtin-names.d.ts +1 -2
  37. package/dist/types/tools/index.d.ts +1 -0
  38. package/dist/types/tools/think.d.ts +38 -0
  39. package/dist/types/tools/todo.d.ts +14 -15
  40. package/dist/types/tools/write.d.ts +2 -2
  41. package/dist/types/utils/local-date.d.ts +2 -0
  42. package/dist/types/vibe/runtime.d.ts +1 -1
  43. package/dist/types/web/parallel.d.ts +1 -0
  44. package/dist/types/web/search/providers/brave.d.ts +8 -3
  45. package/dist/types/web/search/providers/codex.d.ts +6 -0
  46. package/dist/types/web/search/providers/firecrawl.d.ts +3 -2
  47. package/dist/types/web/search/providers/jina.d.ts +3 -3
  48. package/dist/types/web/search/providers/parallel.d.ts +1 -0
  49. package/dist/types/web/search/providers/perplexity.d.ts +4 -0
  50. package/dist/types/web/search/providers/tinyfish.d.ts +2 -0
  51. package/package.json +13 -13
  52. package/src/advisor/delta-split.ts +98 -0
  53. package/src/advisor/runtime.ts +321 -69
  54. package/src/async/job-manager.ts +14 -3
  55. package/src/cli/gallery-fixtures/agentic.ts +16 -0
  56. package/src/cli/plugin-cli.ts +30 -2
  57. package/src/cli/update-cli.ts +259 -24
  58. package/src/config/keybindings.ts +52 -9
  59. package/src/config/model-resolver.ts +19 -3
  60. package/src/config/settings-schema.ts +16 -0
  61. package/src/cursor.ts +10 -5
  62. package/src/discovery/agents-md.ts +61 -23
  63. package/src/eval/jl/kernel.ts +2 -20
  64. package/src/eval/py/kernel.ts +2 -20
  65. package/src/eval/rb/kernel.ts +2 -20
  66. package/src/eval/runner-cache.ts +41 -0
  67. package/src/exec/non-interactive-env.ts +14 -3
  68. package/src/extensibility/extensions/loader.ts +5 -2
  69. package/src/extensibility/extensions/runner.ts +184 -66
  70. package/src/extensibility/extensions/types.ts +26 -2
  71. package/src/extensibility/extensions/wrapper.ts +13 -7
  72. package/src/extensibility/plugins/marketplace/manager.ts +6 -2
  73. package/src/hindsight/client.ts +1 -1
  74. package/src/lib/xai-http.ts +0 -4
  75. package/src/lsp/client.ts +2 -0
  76. package/src/lsp/servers.ts +1 -1
  77. package/src/mcp/tool-bridge.ts +15 -6
  78. package/src/modes/components/agent-hub-renderer.ts +9 -3
  79. package/src/modes/components/agent-hub.ts +2 -1
  80. package/src/modes/components/status-line/component.ts +58 -6
  81. package/src/modes/components/status-line/segments.ts +12 -1
  82. package/src/modes/components/status-line/types.ts +1 -0
  83. package/src/modes/components/user-message.ts +20 -5
  84. package/src/modes/controllers/event-controller.ts +48 -0
  85. package/src/modes/controllers/extension-ui-controller.ts +14 -7
  86. package/src/modes/controllers/input-controller.ts +25 -6
  87. package/src/modes/controllers/selector-controller.ts +5 -0
  88. package/src/modes/interactive-mode.ts +315 -129
  89. package/src/modes/rpc/rpc-frame.ts +13 -5
  90. package/src/modes/theme/tui-adapters.ts +4 -5
  91. package/src/modes/types.ts +13 -7
  92. package/src/modes/utils/hotkeys-markdown.ts +10 -6
  93. package/src/prompts/system/system-prompt.md +10 -1
  94. package/src/registry/persisted-agents.ts +43 -8
  95. package/src/sdk.ts +146 -9
  96. package/src/session/agent-session-types.ts +4 -0
  97. package/src/session/agent-session.ts +91 -10
  98. package/src/session/messages.ts +98 -28
  99. package/src/session/retry-fallback-chains.ts +14 -0
  100. package/src/session/session-advisors.ts +32 -15
  101. package/src/session/session-history-format.ts +15 -1
  102. package/src/session/session-maintenance.ts +8 -8
  103. package/src/session/session-manager.ts +6 -2
  104. package/src/session/session-tools.ts +355 -183
  105. package/src/session/turn-recovery.ts +225 -47
  106. package/src/slash-commands/builtin-modes.ts +41 -12
  107. package/src/slash-commands/types.ts +5 -1
  108. package/src/task/executor.ts +92 -45
  109. package/src/task/structured-subagent.ts +5 -5
  110. package/src/tools/approval.ts +44 -10
  111. package/src/tools/builtin-names.ts +1 -2
  112. package/src/tools/fetch.ts +21 -2
  113. package/src/tools/image-gen.ts +6 -8
  114. package/src/tools/index.ts +8 -0
  115. package/src/tools/renderers.ts +2 -0
  116. package/src/tools/think.ts +62 -0
  117. package/src/tools/todo.ts +70 -26
  118. package/src/tools/tts.ts +3 -2
  119. package/src/tools/write.ts +7 -3
  120. package/src/utils/local-date.ts +13 -0
  121. package/src/utils/tools-manager.ts +2 -2
  122. package/src/vibe/runtime.ts +22 -14
  123. package/src/web/kagi.ts +91 -34
  124. package/src/web/parallel.ts +11 -2
  125. package/src/web/scrapers/crates-io.ts +2 -2
  126. package/src/web/scrapers/discogs.ts +2 -2
  127. package/src/web/scrapers/docs-rs.ts +2 -2
  128. package/src/web/scrapers/github.ts +2 -2
  129. package/src/web/scrapers/musicbrainz.ts +1 -2
  130. package/src/web/scrapers/pubmed.ts +2 -2
  131. package/src/web/scrapers/sec-edgar.ts +2 -2
  132. package/src/web/search/providers/brave.ts +121 -46
  133. package/src/web/search/providers/codex.ts +88 -12
  134. package/src/web/search/providers/exa.ts +45 -10
  135. package/src/web/search/providers/firecrawl.ts +53 -11
  136. package/src/web/search/providers/gemini.ts +139 -27
  137. package/src/web/search/providers/jina.ts +48 -25
  138. package/src/web/search/providers/parallel.ts +23 -9
  139. package/src/web/search/providers/perplexity.ts +24 -7
  140. package/src/web/search/providers/searxng.ts +77 -1
  141. package/src/web/search/providers/tavily.ts +23 -22
  142. package/src/web/search/providers/tinyfish.ts +44 -10
  143. package/src/web/search/providers/xai.ts +85 -14
@@ -1,6 +1,7 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
1
2
  import type { Agent, AgentTool } from "@oh-my-pi/pi-agent-core";
2
3
  import type { Model } from "@oh-my-pi/pi-ai";
3
- import { isRecord, logger, prompt, stringProperty } from "@oh-my-pi/pi-utils";
4
+ import { isRecord, logger, prompt, stringProperty, untilAborted } from "@oh-my-pi/pi-utils";
4
5
  import { reset as resetCapabilities } from "../capability";
5
6
  import type { ModelRegistry } from "../config/model-registry";
6
7
  import { formatModelString } from "../config/model-resolver";
@@ -67,10 +68,14 @@ interface SessionToolsOptions {
67
68
  toolRegistry?: Map<string, AgentTool>;
68
69
  createVibeTools?: () => AgentTool[];
69
70
  createComputerTool?: () => Promise<AgentTool | null>;
71
+ /** Creates the private `think` scratchpad tool for runtime setting changes. */
72
+ createThinkTool?: () => Promise<AgentTool | null>;
70
73
  /** Creates the built-in `inspect_image` tool for session-scoped runtime enablement (see {@link SessionTools.setInspectImageMode}). */
71
74
  createInspectImageTool?: () => Promise<AgentTool | null>;
72
75
  builtInToolNames?: Iterable<string>;
73
76
  presentationPinnedToolNames?: ReadonlySet<string>;
77
+ /** MCP tool names whose current registry entries came from the manager snapshot. */
78
+ mcpManagerToolNames?: Iterable<string>;
74
79
  ensureWriteRegistered?: () => Promise<boolean>;
75
80
  rebuildSystemPrompt?: (
76
81
  toolNames: string[],
@@ -181,10 +186,13 @@ export class SessionTools {
181
186
  #toolRegistry: Map<string, AgentTool>;
182
187
  #createVibeTools: (() => AgentTool[]) | undefined;
183
188
  #createComputerTool: SessionToolsOptions["createComputerTool"];
189
+ #createThinkTool: SessionToolsOptions["createThinkTool"];
184
190
  #createInspectImageTool: SessionToolsOptions["createInspectImageTool"];
185
191
  #installedVibeToolNames = new Set<string>();
186
192
  #builtInToolNames: Set<string>;
187
193
  #rpcHostToolNames = new Set<string>();
194
+ #mcpManagerToolNames = new Set<string>();
195
+ #extensionMcpTools = new Map<string, AgentTool>();
188
196
  #xdev: XdevState | undefined;
189
197
  #pendingXdevMountDelta: { added: Set<string>; removed: Set<string> } | undefined;
190
198
  /**
@@ -216,7 +224,8 @@ export class SessionTools {
216
224
  * prompt carries no catalog (no mounts, or a custom prompt that omits the section).
217
225
  */
218
226
  #basePromptXdevNames: ReadonlySet<string> = new Set();
219
- #mcpRefreshTail: Promise<void> = Promise.resolve();
227
+ #toolRegistryMutationScope = new AsyncLocalStorage<boolean>();
228
+ #toolRegistryMutationTail: Promise<void> = Promise.resolve();
220
229
  #promptModelKey: string | undefined;
221
230
  #rebuildSystemPrompt: SessionToolsOptions["rebuildSystemPrompt"];
222
231
  #getLocalCalendarDate: () => string;
@@ -235,8 +244,20 @@ export class SessionTools {
235
244
  this.#toolRegistry = options.toolRegistry ?? new Map();
236
245
  this.#createVibeTools = options.createVibeTools;
237
246
  this.#createComputerTool = options.createComputerTool;
247
+ this.#createThinkTool = options.createThinkTool;
238
248
  this.#createInspectImageTool = options.createInspectImageTool;
239
249
  this.#builtInToolNames = new Set(options.builtInToolNames ?? []);
250
+ this.#mcpManagerToolNames = new Set(options.mcpManagerToolNames ?? []);
251
+ if (options.mcpManagerToolNames === undefined) {
252
+ for (const name of this.#toolRegistry.keys()) {
253
+ if (isMCPToolName(name)) this.#mcpManagerToolNames.add(name);
254
+ }
255
+ }
256
+ for (const [name, tool] of this.#toolRegistry) {
257
+ if (isMCPToolName(name) && !this.#mcpManagerToolNames.has(name)) {
258
+ this.#extensionMcpTools.set(name, tool);
259
+ }
260
+ }
240
261
  this.#presentationPinnedToolNames = options.presentationPinnedToolNames;
241
262
  this.#ensureWriteRegistered = options.ensureWriteRegistered;
242
263
  this.#rebuildSystemPrompt = options.rebuildSystemPrompt;
@@ -362,6 +383,65 @@ export class SessionTools {
362
383
  return this.#builtInToolNames.has(name);
363
384
  }
364
385
 
386
+ /** Updates source provenance when a live registry entry is replaced or restored. */
387
+ setToolBuiltIn(name: string, builtIn: boolean): void {
388
+ if (builtIn) {
389
+ this.#builtInToolNames.add(name);
390
+ } else {
391
+ this.#builtInToolNames.delete(name);
392
+ }
393
+ }
394
+
395
+ /** Whether the live registry entry is owned by the RPC host. */
396
+ hasRpcHostTool(name: string): boolean {
397
+ return this.#rpcHostToolNames.has(name);
398
+ }
399
+
400
+ /** Whether the current MCP entry came from the manager snapshot. */
401
+ hasMCPManagerTool(name: string): boolean {
402
+ return this.#mcpManagerToolNames.has(name);
403
+ }
404
+
405
+ /** Restores manager ownership after a lifecycle registration rollback. */
406
+ setMCPManagerTool(name: string, managerOwned: boolean): void {
407
+ if (managerOwned) {
408
+ this.#mcpManagerToolNames.add(name);
409
+ } else {
410
+ this.#mcpManagerToolNames.delete(name);
411
+ }
412
+ }
413
+
414
+ /** Current extension-owned MCP entry retained across manager refreshes. */
415
+ getExtensionMCPTool(name: string): AgentTool | undefined {
416
+ return this.#extensionMcpTools.get(name);
417
+ }
418
+
419
+ /** Updates extension ownership when a lifecycle registration commits or rolls back. */
420
+ setExtensionMCPTool(name: string, tool: AgentTool | undefined): void {
421
+ if (!isMCPToolName(name)) return;
422
+ if (tool) {
423
+ this.#extensionMcpTools.set(name, tool);
424
+ this.#mcpManagerToolNames.delete(name);
425
+ } else {
426
+ this.#extensionMcpTools.delete(name);
427
+ }
428
+ }
429
+
430
+ /** Serializes every registry and presentation mutation for this session. */
431
+ runToolRegistryMutation<T>(mutation: () => Promise<T>, signal?: AbortSignal): Promise<T> {
432
+ if (this.#toolRegistryMutationScope.getStore()) return untilAborted(signal, mutation);
433
+ const serialized = this.#toolRegistryMutationTail.then(() => {
434
+ signal?.throwIfAborted();
435
+ return this.#toolRegistryMutationScope.run(true, mutation);
436
+ });
437
+ const operation = untilAborted(signal, serialized);
438
+ this.#toolRegistryMutationTail = serialized.then(
439
+ () => undefined,
440
+ () => undefined,
441
+ );
442
+ return operation;
443
+ }
444
+
365
445
  /** Names of every registered tool. */
366
446
  getAllToolNames(): string[] {
367
447
  return Array.from(this.#toolRegistry.keys());
@@ -401,40 +481,46 @@ export class SessionTools {
401
481
  }
402
482
 
403
483
  /** Installs and activates the ephemeral vibe tool set. */
404
- async activateVibeTools(baseToolNames: string[]): Promise<void> {
405
- const createVibeTools = this.#createVibeTools;
406
- if (!createVibeTools) {
407
- throw new Error("Vibe tools are unavailable in this session.");
408
- }
484
+ activateVibeTools(baseToolNames: string[]): Promise<void> {
485
+ return this.runToolRegistryMutation(async () => {
486
+ const createVibeTools = this.#createVibeTools;
487
+ if (!createVibeTools) {
488
+ throw new Error("Vibe tools are unavailable in this session.");
489
+ }
409
490
 
410
- const tools = createVibeTools();
411
- const vibeToolNames = tools.map(tool => tool.name);
412
- if (new Set(vibeToolNames).size !== vibeToolNames.length) {
413
- throw new Error("Vibe tool names must be unique.");
414
- }
491
+ const tools = createVibeTools();
492
+ const vibeToolNames = tools.map(tool => tool.name);
493
+ if (new Set(vibeToolNames).size !== vibeToolNames.length) {
494
+ throw new Error("Vibe tool names must be unique.");
495
+ }
415
496
 
416
- for (const tool of tools) {
417
- if (this.#toolRegistry.has(tool.name)) continue;
418
- this.#toolRegistry.set(tool.name, this.#wrapRuntimeTool(tool));
419
- this.#builtInToolNames.add(tool.name);
420
- this.#installedVibeToolNames.add(tool.name);
421
- }
497
+ for (const tool of tools) {
498
+ if (this.#toolRegistry.has(tool.name)) continue;
499
+ this.#toolRegistry.set(tool.name, this.#wrapRuntimeTool(tool));
500
+ this.#builtInToolNames.add(tool.name);
501
+ this.#installedVibeToolNames.add(tool.name);
502
+ }
422
503
 
423
- await this.applyActiveToolsByName([...new Set([...baseToolNames, ...vibeToolNames])]);
504
+ await this.#applyActiveToolsByName([...new Set([...baseToolNames, ...vibeToolNames])]);
505
+ });
424
506
  }
425
507
 
426
508
  /** Uninstalls vibe tools and activates the replacement set. */
427
- async deactivateVibeTools(nextToolNames: string[]): Promise<void> {
428
- this.#uninstallVibeTools();
429
- await this.applyActiveToolsByName(nextToolNames);
509
+ deactivateVibeTools(nextToolNames: string[]): Promise<void> {
510
+ return this.runToolRegistryMutation(async () => {
511
+ this.#uninstallVibeTools();
512
+ await this.#applyActiveToolsByName(nextToolNames);
513
+ });
430
514
  }
431
515
 
432
516
  /** Removes vibe tools without restoring a source-session snapshot. */
433
- async removeVibeToolsPreservingActive(): Promise<void> {
434
- const removed = new Set(this.#installedVibeToolNames);
435
- this.#uninstallVibeTools();
436
- const nextActive = this.getActiveToolNames().filter(name => !removed.has(name));
437
- await this.applyActiveToolsByName(nextActive);
517
+ removeVibeToolsPreservingActive(): Promise<void> {
518
+ return this.runToolRegistryMutation(async () => {
519
+ const removed = new Set(this.#installedVibeToolNames);
520
+ this.#uninstallVibeTools();
521
+ const nextActive = this.getActiveToolNames().filter(name => !removed.has(name));
522
+ await this.#applyActiveToolsByName(nextActive);
523
+ });
438
524
  }
439
525
 
440
526
  #uninstallVibeTools(): void {
@@ -633,11 +719,20 @@ export class SessionTools {
633
719
  }
634
720
 
635
721
  /** Applies an enabled tool set and reconciles its `xd://` partition. */
636
- async applyActiveToolsByName(toolNames: string[]): Promise<void> {
722
+ applyActiveToolsByName(toolNames: string[], forcePromptRefresh = false, signal?: AbortSignal): Promise<void> {
723
+ return this.runToolRegistryMutation(
724
+ () => this.#applyActiveToolsByName(toolNames, forcePromptRefresh, signal),
725
+ signal,
726
+ );
727
+ }
728
+
729
+ async #applyActiveToolsByName(toolNames: string[], forcePromptRefresh = false, signal?: AbortSignal): Promise<void> {
730
+ signal?.throwIfAborted();
637
731
  toolNames = normalizeToolNames(toolNames);
638
732
  let builtInWriteAvailable = this.#builtInToolNames.has("write");
639
733
  if (toolNames.includes("write") && !builtInWriteAvailable) {
640
- builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
734
+ const writeRegistration = this.#ensureWriteRegistered?.();
735
+ builtInWriteAvailable = writeRegistration ? (await untilAborted(signal, writeRegistration)) === true : false;
641
736
  if (builtInWriteAvailable) this.#builtInToolNames.add("write");
642
737
  }
643
738
  const selectedTools = toolNames.flatMap(name => {
@@ -669,7 +764,8 @@ export class SessionTools {
669
764
  const activeDeferrableTool = tools.some(tool => tool.deferrable === true);
670
765
  const transportNeeded = mountNames.size > 0 || activeDeferrableTool || this.#host.planModeEnabled();
671
766
  if (transportNeeded && !builtInWriteAvailable) {
672
- builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
767
+ const writeRegistration = this.#ensureWriteRegistered?.();
768
+ builtInWriteAvailable = writeRegistration ? (await untilAborted(signal, writeRegistration)) === true : false;
673
769
  if (builtInWriteAvailable) this.#builtInToolNames.add("write");
674
770
  }
675
771
  if (transportNeeded && builtInWriteAvailable) {
@@ -699,13 +795,14 @@ export class SessionTools {
699
795
  try {
700
796
  if (this.#rebuildSystemPrompt) {
701
797
  const signature = this.#computeAppliedToolSignature(validToolNames, tools);
702
- if (signature !== this.#lastAppliedToolSignature) {
703
- const built = await this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry);
798
+ if (forcePromptRefresh || signature !== this.#lastAppliedToolSignature) {
799
+ const built = await untilAborted(signal, this.#rebuildSystemPrompt(validToolNames, this.#toolRegistry));
704
800
  rebuiltSystemPrompt = built.systemPrompt;
705
801
  rebuiltSignature = signature;
706
802
  rebuiltXdevCatalogNames = built.xdevCatalogNames;
707
803
  }
708
804
  }
805
+ signal?.throwIfAborted();
709
806
  } catch (error) {
710
807
  this.#setMountedNames(previousMounted);
711
808
  this.#setActiveToolNames?.(previousActiveToolNames);
@@ -917,15 +1014,17 @@ export class SessionTools {
917
1014
  }
918
1015
 
919
1016
  /** Selects enabled tools, ignoring names absent from the registry. */
920
- async setActiveToolsByName(toolNames: string[]): Promise<void> {
921
- const normalized = normalizeToolNames(toolNames);
922
- // Transport-write eligibility keys off the *current* active set: an ordinary
923
- // selection change should not demote `write` unless it is already active.
924
- await this.#applyToolPresentation(
925
- normalized,
926
- this.#xdev?.mountedNames ?? new Set(),
927
- this.getActiveToolNames().includes("write"),
928
- );
1017
+ setActiveToolsByName(toolNames: string[]): Promise<void> {
1018
+ return this.runToolRegistryMutation(async () => {
1019
+ const normalized = normalizeToolNames(toolNames);
1020
+ // Transport-write eligibility keys off the *current* active set: an ordinary
1021
+ // selection change should not demote `write` unless it is already active.
1022
+ await this.#applyToolPresentation(
1023
+ normalized,
1024
+ this.#xdev?.mountedNames ?? new Set(),
1025
+ this.getActiveToolNames().includes("write"),
1026
+ );
1027
+ });
929
1028
  }
930
1029
 
931
1030
  /**
@@ -938,19 +1037,31 @@ export class SessionTools {
938
1037
  * `xd://` remain mount-eligible, even when the live mount set has drifted.
939
1038
  *
940
1039
  * Names outside `mountedToolNames` are pinned top-level for this application;
941
- * names in the mounted subset remain eligible for xdev mounting. Delegates the
942
- * actual apply through {@link applyActiveToolsByName} and restores the prior runtime
943
- * selection if that apply throws.
1040
+ * names in the mounted subset remain eligible for xdev mounting. Set
1041
+ * `forcePromptRefresh` when an enabled tool's schema or prompt-visible metadata
1042
+ * changed without changing its name or presentation.
1043
+ *
1044
+ * Delegates the actual apply through {@link applyActiveToolsByName} and restores
1045
+ * the prior runtime selection if that apply throws.
944
1046
  */
945
- async setActiveToolPresentation(toolNames: string[], mountedToolNames: string[]): Promise<void> {
946
- const normalized = normalizeToolNames(toolNames);
947
- // Restoration targets a snapshot, so write eligibility comes from the
948
- // *target* set rather than whatever happens to be active mid-rollback.
949
- await this.#applyToolPresentation(
950
- normalized,
951
- new Set(normalizeToolNames(mountedToolNames)),
952
- normalized.includes("write"),
953
- );
1047
+ setActiveToolPresentation(
1048
+ toolNames: string[],
1049
+ mountedToolNames: string[],
1050
+ forcePromptRefresh = false,
1051
+ signal?: AbortSignal,
1052
+ ): Promise<void> {
1053
+ return this.runToolRegistryMutation(async () => {
1054
+ const normalized = normalizeToolNames(toolNames);
1055
+ // Restoration targets a snapshot, so write eligibility comes from the
1056
+ // *target* set rather than whatever happens to be active mid-rollback.
1057
+ await this.#applyToolPresentation(
1058
+ normalized,
1059
+ new Set(normalizeToolNames(mountedToolNames)),
1060
+ normalized.includes("write"),
1061
+ forcePromptRefresh,
1062
+ signal,
1063
+ );
1064
+ }, signal);
954
1065
  }
955
1066
 
956
1067
  /**
@@ -962,6 +1073,8 @@ export class SessionTools {
962
1073
  normalized: string[],
963
1074
  mounted: ReadonlySet<string>,
964
1075
  writeSelected: boolean,
1076
+ forcePromptRefresh = false,
1077
+ signal?: AbortSignal,
965
1078
  ): Promise<void> {
966
1079
  const transportWriteActive =
967
1080
  writeSelected &&
@@ -974,7 +1087,7 @@ export class SessionTools {
974
1087
  normalized.filter(name => !mounted.has(name) && !(name === "write" && transportWriteActive)),
975
1088
  );
976
1089
  try {
977
- await this.applyActiveToolsByName(normalized);
1090
+ await this.#applyActiveToolsByName(normalized, forcePromptRefresh, signal);
978
1091
  } catch (error) {
979
1092
  this.#runtimeSelectedToolNames = previousRuntimeSelectedToolNames;
980
1093
  throw error;
@@ -982,24 +1095,26 @@ export class SessionTools {
982
1095
  }
983
1096
 
984
1097
  /** Replaces memory-backend tools while preserving unrelated selections. */
985
- async replaceMemoryTools(tools: AgentTool[]): Promise<void> {
986
- const removed = new Set<string>(MEMORY_BACKEND_TOOL_NAMES.filter(name => this.#builtInToolNames.has(name)));
987
- const nextActive = this.getEnabledToolNames().filter(name => !removed.has(name));
988
- for (const name of removed) {
989
- this.#toolRegistry.delete(name);
990
- this.#builtInToolNames.delete(name);
991
- }
1098
+ replaceMemoryTools(tools: AgentTool[]): Promise<void> {
1099
+ return this.runToolRegistryMutation(async () => {
1100
+ const removed = new Set<string>(MEMORY_BACKEND_TOOL_NAMES.filter(name => this.#builtInToolNames.has(name)));
1101
+ const nextActive = this.getEnabledToolNames().filter(name => !removed.has(name));
1102
+ for (const name of removed) {
1103
+ this.#toolRegistry.delete(name);
1104
+ this.#builtInToolNames.delete(name);
1105
+ }
992
1106
 
993
- for (const tool of tools) {
994
- if (!MEMORY_BACKEND_TOOL_NAMES.some(name => name === tool.name) || this.#toolRegistry.has(tool.name)) {
995
- continue;
1107
+ for (const tool of tools) {
1108
+ if (!MEMORY_BACKEND_TOOL_NAMES.some(name => name === tool.name) || this.#toolRegistry.has(tool.name)) {
1109
+ continue;
1110
+ }
1111
+ const wrapped = this.#wrapRuntimeTool(tool);
1112
+ this.#toolRegistry.set(wrapped.name, wrapped);
1113
+ this.#builtInToolNames.add(wrapped.name);
1114
+ nextActive.push(wrapped.name);
996
1115
  }
997
- const wrapped = this.#wrapRuntimeTool(tool);
998
- this.#toolRegistry.set(wrapped.name, wrapped);
999
- this.#builtInToolNames.add(wrapped.name);
1000
- nextActive.push(wrapped.name);
1001
- }
1002
- await this.applyActiveToolsByName([...new Set(nextActive)]);
1116
+ await this.#applyActiveToolsByName([...new Set(nextActive)]);
1117
+ });
1003
1118
  }
1004
1119
 
1005
1120
  /**
@@ -1015,34 +1130,67 @@ export class SessionTools {
1015
1130
  * @returns false when enabling was requested but this session cannot build the
1016
1131
  * tool (e.g. restricted child sessions have no factory).
1017
1132
  */
1018
- async setComputerToolEnabled(enabled: boolean): Promise<boolean> {
1019
- const logState = (): void => this.#logComputerState("Computer tool state changed", enabled);
1020
- const active = this.getEnabledToolNames();
1021
- if (!enabled) {
1022
- if (active.includes("computer")) {
1023
- await this.applyActiveToolsByName(active.filter(name => name !== "computer"));
1133
+ setComputerToolEnabled(enabled: boolean): Promise<boolean> {
1134
+ return this.runToolRegistryMutation(async () => {
1135
+ const logState = (): void => this.#logComputerState("Computer tool state changed", enabled);
1136
+ const active = this.getEnabledToolNames();
1137
+ if (!enabled) {
1138
+ if (active.includes("computer")) {
1139
+ await this.#applyActiveToolsByName(active.filter(name => name !== "computer"));
1140
+ }
1141
+ logState();
1142
+ return true;
1143
+ }
1144
+ if (!this.#toolRegistry.has("computer")) {
1145
+ const tool = await this.#createComputerTool?.();
1146
+ if (tool?.name !== "computer") {
1147
+ const model = this.#host.model();
1148
+ logger.warn("Computer tool could not be created", {
1149
+ model: model ? formatModelString(model) : undefined,
1150
+ });
1151
+ return false;
1152
+ }
1153
+ const wrapped = this.#wrapRuntimeTool(tool);
1154
+ this.#toolRegistry.set(wrapped.name, wrapped);
1155
+ this.#builtInToolNames.add(wrapped.name);
1156
+ }
1157
+ if (!active.includes("computer")) {
1158
+ await this.#applyActiveToolsByName([...active, "computer"]);
1024
1159
  }
1025
1160
  logState();
1026
1161
  return true;
1027
- }
1028
- if (!this.#toolRegistry.has("computer")) {
1029
- const tool = await this.#createComputerTool?.();
1030
- if (tool?.name !== "computer") {
1031
- const model = this.#host.model();
1032
- logger.warn("Computer tool could not be created", {
1033
- model: model ? formatModelString(model) : undefined,
1034
- });
1035
- return false;
1162
+ });
1163
+ }
1164
+
1165
+ /**
1166
+ * Session-scoped enable/disable for the private `think` scratchpad tool.
1167
+ *
1168
+ * Enabling constructs the tool once and refreshes the model's tool contract;
1169
+ * disabling removes it from the active set while preserving its registry entry.
1170
+ *
1171
+ * @returns false when enabling was requested but this session cannot build the tool.
1172
+ */
1173
+ setThinkToolEnabled(enabled: boolean): Promise<boolean> {
1174
+ return this.runToolRegistryMutation(async () => {
1175
+ const active = this.getEnabledToolNames();
1176
+ if (!enabled) {
1177
+ if (active.includes("think")) {
1178
+ await this.#applyActiveToolsByName(active.filter(name => name !== "think"));
1179
+ }
1180
+ return true;
1036
1181
  }
1037
- const wrapped = this.#wrapRuntimeTool(tool);
1038
- this.#toolRegistry.set(wrapped.name, wrapped);
1039
- this.#builtInToolNames.add(wrapped.name);
1040
- }
1041
- if (!active.includes("computer")) {
1042
- await this.applyActiveToolsByName([...active, "computer"]);
1043
- }
1044
- logState();
1045
- return true;
1182
+ if (!this.#toolRegistry.has("think")) {
1183
+ const tool = await this.#createThinkTool?.();
1184
+ if (tool?.name !== "think") return false;
1185
+ const wrapped = this.#wrapRuntimeTool(tool);
1186
+ this.#toolRegistry.set(wrapped.name, wrapped);
1187
+ this.#builtInToolNames.add(wrapped.name);
1188
+ }
1189
+ if (!active.includes("think")) {
1190
+ await this.#applyActiveToolsByName([...active, "think"]);
1191
+ }
1192
+ return true;
1193
+ });
1046
1194
  }
1047
1195
 
1048
1196
  /** Current effective inspect_image state for `/vision status`. */
@@ -1065,48 +1213,50 @@ export class SessionTools {
1065
1213
  * @returns false when the tool should be active but this session cannot
1066
1214
  * build it (e.g. restricted child sessions have no factory).
1067
1215
  */
1068
- async reconcileInspectImageTool(): Promise<boolean> {
1069
- const expected = isInspectImageToolActive({
1070
- settings: this.#host.settings,
1071
- getActiveModel: () => this.#host.model(),
1072
- getInspectImageModeOverride: () => this.#host.getInspectImageModeOverride(),
1073
- });
1074
- // Keep the read tool's advertised description in sync BEFORE any prompt
1075
- // rebuild below, passing the post-change availability so the prompt never
1076
- // lags a flip in either direction. Per-read lazy sync is the backstop.
1077
- const syncReadDescription = (available: boolean): void => {
1078
- const readTool = this.#toolRegistry.get("read") as
1079
- | { syncInspectImageState?: (available?: boolean) => boolean }
1080
- | undefined;
1081
- readTool?.syncInspectImageState?.(available);
1082
- };
1083
- const active = this.getEnabledToolNames();
1084
- const isActive = active.includes("inspect_image");
1085
- if (expected === isActive) {
1086
- syncReadDescription(isActive);
1087
- return true;
1088
- }
1089
- if (!expected) {
1090
- syncReadDescription(false);
1091
- await this.applyActiveToolsByName(active.filter(name => name !== "inspect_image"));
1092
- return true;
1093
- }
1094
- if (!this.#toolRegistry.has("inspect_image")) {
1095
- const tool = await this.#createInspectImageTool?.();
1096
- if (tool?.name !== "inspect_image") {
1097
- logger.warn("inspect_image tool could not be created", {
1098
- model: this.#host.model()?.id,
1099
- });
1216
+ reconcileInspectImageTool(): Promise<boolean> {
1217
+ return this.runToolRegistryMutation(async () => {
1218
+ const expected = isInspectImageToolActive({
1219
+ settings: this.#host.settings,
1220
+ getActiveModel: () => this.#host.model(),
1221
+ getInspectImageModeOverride: () => this.#host.getInspectImageModeOverride(),
1222
+ });
1223
+ // Keep the read tool's advertised description in sync BEFORE any prompt
1224
+ // rebuild below, passing the post-change availability so the prompt never
1225
+ // lags a flip in either direction. Per-read lazy sync is the backstop.
1226
+ const syncReadDescription = (available: boolean): void => {
1227
+ const readTool = this.#toolRegistry.get("read") as
1228
+ | { syncInspectImageState?: (available?: boolean) => boolean }
1229
+ | undefined;
1230
+ readTool?.syncInspectImageState?.(available);
1231
+ };
1232
+ const active = this.getEnabledToolNames();
1233
+ const isActive = active.includes("inspect_image");
1234
+ if (expected === isActive) {
1235
+ syncReadDescription(isActive);
1236
+ return true;
1237
+ }
1238
+ if (!expected) {
1100
1239
  syncReadDescription(false);
1101
- return false;
1240
+ await this.#applyActiveToolsByName(active.filter(name => name !== "inspect_image"));
1241
+ return true;
1102
1242
  }
1103
- const wrapped = this.#wrapRuntimeTool(tool);
1104
- this.#toolRegistry.set(wrapped.name, wrapped);
1105
- this.#builtInToolNames.add(wrapped.name);
1106
- }
1107
- syncReadDescription(true);
1108
- await this.applyActiveToolsByName([...active, "inspect_image"]);
1109
- return true;
1243
+ if (!this.#toolRegistry.has("inspect_image")) {
1244
+ const tool = await this.#createInspectImageTool?.();
1245
+ if (tool?.name !== "inspect_image") {
1246
+ logger.warn("inspect_image tool could not be created", {
1247
+ model: this.#host.model()?.id,
1248
+ });
1249
+ syncReadDescription(false);
1250
+ return false;
1251
+ }
1252
+ const wrapped = this.#wrapRuntimeTool(tool);
1253
+ this.#toolRegistry.set(wrapped.name, wrapped);
1254
+ this.#builtInToolNames.add(wrapped.name);
1255
+ }
1256
+ syncReadDescription(true);
1257
+ await this.#applyActiveToolsByName([...active, "inspect_image"]);
1258
+ return true;
1259
+ });
1110
1260
  }
1111
1261
 
1112
1262
  /**
@@ -1115,20 +1265,22 @@ export class SessionTools {
1115
1265
  * path — including retry-fallback switches that bypass
1116
1266
  * {@link syncAfterModelChange}.
1117
1267
  */
1118
- async reconcileInspectImageAfterModelChange(): Promise<void> {
1119
- const before = this.getEnabledToolNames().includes("inspect_image");
1120
- const reconciled = await this.reconcileInspectImageTool();
1121
- const after = this.getEnabledToolNames().includes("inspect_image");
1122
- if (!reconciled || before === after) return;
1123
- const model = this.#host.model();
1124
- const modelName = model ? formatModelString(model) : "the current model";
1125
- this.#host.emitNotice(
1126
- "info",
1127
- after
1128
- ? `inspect_image is now available: ${modelName} has no native image input.`
1129
- : `inspect_image is now hidden: ${modelName} supports image input natively. Override with /vision on.`,
1130
- "vision",
1131
- );
1268
+ reconcileInspectImageAfterModelChange(): Promise<void> {
1269
+ return this.runToolRegistryMutation(async () => {
1270
+ const before = this.getEnabledToolNames().includes("inspect_image");
1271
+ const reconciled = await this.reconcileInspectImageTool();
1272
+ const after = this.getEnabledToolNames().includes("inspect_image");
1273
+ if (!reconciled || before === after) return;
1274
+ const model = this.#host.model();
1275
+ const modelName = model ? formatModelString(model) : "the current model";
1276
+ this.#host.emitNotice(
1277
+ "info",
1278
+ after
1279
+ ? `inspect_image is now available: ${modelName} has no native image input.`
1280
+ : `inspect_image is now hidden: ${modelName} supports image input natively. Override with /vision on.`,
1281
+ "vision",
1282
+ );
1283
+ });
1132
1284
  }
1133
1285
 
1134
1286
  /**
@@ -1139,16 +1291,22 @@ export class SessionTools {
1139
1291
  *
1140
1292
  * @returns false when `on` was requested but the tool cannot be built here.
1141
1293
  */
1142
- async setInspectImageMode(mode: InspectImageMode): Promise<boolean> {
1143
- this.#host.setInspectImageModeOverride(mode === "auto" ? undefined : mode);
1144
- const applied = await this.reconcileInspectImageTool();
1145
- const { active, model } = this.inspectImageState();
1146
- logger.debug("inspect_image mode changed", { mode, active, model });
1147
- return applied;
1294
+ setInspectImageMode(mode: InspectImageMode): Promise<boolean> {
1295
+ return this.runToolRegistryMutation(async () => {
1296
+ this.#host.setInspectImageModeOverride(mode === "auto" ? undefined : mode);
1297
+ const applied = await this.reconcileInspectImageTool();
1298
+ const { active, model } = this.inspectImageState();
1299
+ logger.debug("inspect_image mode changed", { mode, active, model });
1300
+ return applied;
1301
+ });
1148
1302
  }
1149
1303
 
1150
1304
  /** Rebuilds the stable base prompt for the current tools and model. */
1151
- async refreshBaseSystemPrompt(): Promise<void> {
1305
+ refreshBaseSystemPrompt(): Promise<void> {
1306
+ return this.runToolRegistryMutation(() => this.#refreshBaseSystemPrompt());
1307
+ }
1308
+
1309
+ async #refreshBaseSystemPrompt(): Promise<void> {
1152
1310
  if (this.#host.isDisposed() || !this.#rebuildSystemPrompt) return;
1153
1311
  const activeToolNames = this.getActiveToolNames();
1154
1312
  this.#setActiveToolNames?.(activeToolNames);
@@ -1299,32 +1457,25 @@ export class SessionTools {
1299
1457
  */
1300
1458
  refreshMCPTools(mcpTools: CustomTool[]): Promise<void> {
1301
1459
  const snapshot = [...mcpTools];
1302
- const refresh = this.#mcpRefreshTail.then(() =>
1303
- this.#host.isDisposed() ? undefined : this.#applyMCPToolRefresh(snapshot),
1460
+ return this.runToolRegistryMutation(() =>
1461
+ this.#host.isDisposed() ? Promise.resolve() : this.#applyMCPToolRefresh(snapshot),
1304
1462
  );
1305
- this.#mcpRefreshTail = refresh.catch(() => {});
1306
- return refresh;
1307
1463
  }
1308
1464
 
1309
1465
  async #applyMCPToolRefresh(mcpTools: CustomTool[]): Promise<void> {
1310
- const existingNames = Array.from(this.#toolRegistry.keys());
1311
- const previousMcpTools = new Map(
1312
- existingNames.flatMap(name => {
1313
- const tool = this.#toolRegistry.get(name);
1314
- return isMCPToolName(name) && tool ? [[name, tool] as const] : [];
1315
- }),
1316
- );
1466
+ const previousMcpTools = new Map<string, AgentTool>();
1467
+ for (const [name, tool] of this.#toolRegistry) {
1468
+ if (isMCPToolName(name)) previousMcpTools.set(name, tool);
1469
+ }
1470
+ const previousMcpManagerToolNames = new Set(this.#mcpManagerToolNames);
1471
+ const previousActiveMcpToolNames = this.getEnabledToolNames().filter(isMCPToolName);
1317
1472
  const restorePreviousMcpTools = () => {
1318
1473
  for (const name of this.#toolRegistry.keys()) {
1319
1474
  if (isMCPToolName(name)) this.#toolRegistry.delete(name);
1320
1475
  }
1321
1476
  for (const [name, tool] of previousMcpTools) this.#toolRegistry.set(name, tool);
1477
+ this.#mcpManagerToolNames = previousMcpManagerToolNames;
1322
1478
  };
1323
- for (const name of existingNames) {
1324
- if (isMCPToolName(name)) {
1325
- this.#toolRegistry.delete(name);
1326
- }
1327
- }
1328
1479
 
1329
1480
  const getCustomToolContext = (): CustomToolContext => ({
1330
1481
  sessionManager: this.#host.sessionManager,
@@ -1340,20 +1491,36 @@ export class SessionTools {
1340
1491
  });
1341
1492
 
1342
1493
  const extensionRunner = this.#host.extensionRunner();
1343
- const uniqueMcpTools = deduplicateMCPToolsByName(mcpTools);
1344
- for (const customTool of uniqueMcpTools) {
1494
+ const managerTools = deduplicateMCPToolsByName(mcpTools).map(customTool => {
1345
1495
  const wrapped = wrapToolWithMetaNotice(CustomToolAdapter.wrap(customTool, getCustomToolContext) as AgentTool);
1346
- const finalTool = (
1347
- extensionRunner ? new ExtensionToolWrapper(wrapped, extensionRunner) : wrapped
1348
- ) as AgentTool;
1349
- this.#toolRegistry.set(finalTool.name, finalTool);
1496
+ return (extensionRunner ? new ExtensionToolWrapper(wrapped, extensionRunner) : wrapped) as AgentTool;
1497
+ });
1498
+ const managerToolSet = new Set(managerTools);
1499
+ const reconciledTools = deduplicateMCPToolsByName([...this.#extensionMcpTools.values(), ...managerTools]);
1500
+
1501
+ for (const name of this.#toolRegistry.keys()) {
1502
+ if (isMCPToolName(name)) this.#toolRegistry.delete(name);
1503
+ }
1504
+ this.#mcpManagerToolNames.clear();
1505
+ for (const tool of reconciledTools) {
1506
+ this.#toolRegistry.set(tool.name, tool);
1507
+ if (managerToolSet.has(tool)) this.#mcpManagerToolNames.add(tool.name);
1350
1508
  }
1351
1509
 
1352
- // Every connected MCP tool is selected; centralized repartitioning owns
1353
- // presentation pins and write-transport activation/removal.
1354
- const nextActive = [...new Set([...this.#getActiveNonMCPToolNames(), ...uniqueMcpTools.map(tool => tool.name)])];
1510
+ // Connected manager tools become active immediately. Extension-owned MCP
1511
+ // tools retain their prior selection while both sets share one registry.
1512
+ const retainedActiveExtensionToolNames = previousActiveMcpToolNames.filter(
1513
+ name => this.#extensionMcpTools.has(name) && this.#toolRegistry.has(name),
1514
+ );
1515
+ const nextActive = [
1516
+ ...new Set([
1517
+ ...this.#getActiveNonMCPToolNames(),
1518
+ ...this.#mcpManagerToolNames,
1519
+ ...retainedActiveExtensionToolNames,
1520
+ ]),
1521
+ ];
1355
1522
  try {
1356
- await this.applyActiveToolsByName(nextActive);
1523
+ await this.#applyActiveToolsByName(nextActive);
1357
1524
  if (this.#host.isDisposed()) restorePreviousMcpTools();
1358
1525
  } catch (error) {
1359
1526
  restorePreviousMcpTools();
@@ -1362,7 +1529,12 @@ export class SessionTools {
1362
1529
  }
1363
1530
 
1364
1531
  /** Replaces RPC host-owned tools and refreshes the active set before the next model call. */
1365
- async refreshRpcHostTools(rpcTools: AgentTool[]): Promise<void> {
1532
+ refreshRpcHostTools(rpcTools: AgentTool[]): Promise<void> {
1533
+ const snapshot = [...rpcTools];
1534
+ return this.runToolRegistryMutation(() => this.#applyRpcHostToolRefresh(snapshot));
1535
+ }
1536
+
1537
+ async #applyRpcHostToolRefresh(rpcTools: AgentTool[]): Promise<void> {
1366
1538
  const nextToolNames = rpcTools.map(tool => tool.name);
1367
1539
  const uniqueToolNames = new Set(nextToolNames);
1368
1540
  if (uniqueToolNames.size !== nextToolNames.length) {
@@ -1406,7 +1578,7 @@ export class SessionTools {
1406
1578
  .filter(tool => !tool.hidden && !previousRpcHostToolNames.has(tool.name))
1407
1579
  .map(tool => tool.name);
1408
1580
  try {
1409
- await this.applyActiveToolsByName(
1581
+ await this.#applyActiveToolsByName(
1410
1582
  Array.from(new Set([...activeNonRpcToolNames, ...preservedRpcToolNames, ...autoActivatedRpcToolNames])),
1411
1583
  );
1412
1584
  } catch (error) {