@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.3

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 (179) hide show
  1. package/CHANGELOG.md +162 -40
  2. package/dist/cli.js +4559 -4543
  3. package/dist/types/advisor/config.d.ts +14 -6
  4. package/dist/types/advisor/runtime.d.ts +20 -11
  5. package/dist/types/autolearn/controller.d.ts +1 -0
  6. package/dist/types/config/model-discovery.d.ts +17 -0
  7. package/dist/types/config/model-resolver.d.ts +6 -2
  8. package/dist/types/config/settings-schema.d.ts +39 -10
  9. package/dist/types/config/settings.d.ts +50 -0
  10. package/dist/types/cursor.d.ts +11 -0
  11. package/dist/types/discovery/helpers.d.ts +5 -2
  12. package/dist/types/eval/agent-bridge.d.ts +7 -19
  13. package/dist/types/exec/bash-executor.d.ts +2 -0
  14. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  15. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  16. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  17. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +55 -1
  18. package/dist/types/extensibility/shared-events.d.ts +6 -0
  19. package/dist/types/extensibility/utils.d.ts +2 -2
  20. package/dist/types/lsp/client.d.ts +2 -0
  21. package/dist/types/lsp/types.d.ts +3 -0
  22. package/dist/types/mcp/transports/stdio.d.ts +42 -1
  23. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  24. package/dist/types/modes/components/agent-hub.d.ts +15 -0
  25. package/dist/types/modes/components/hook-editor.d.ts +7 -0
  26. package/dist/types/modes/components/model-hub.d.ts +5 -2
  27. package/dist/types/modes/components/session-selector.d.ts +2 -0
  28. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  29. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  30. package/dist/types/modes/print-mode.d.ts +1 -1
  31. package/dist/types/modes/warp-events.d.ts +24 -0
  32. package/dist/types/modes/warp-events.test.d.ts +1 -0
  33. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  34. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  35. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  36. package/dist/types/registry/persisted-agents.d.ts +3 -0
  37. package/dist/types/sdk.d.ts +27 -5
  38. package/dist/types/session/agent-session.d.ts +34 -10
  39. package/dist/types/session/session-entries.d.ts +6 -1
  40. package/dist/types/session/session-history-format.d.ts +10 -0
  41. package/dist/types/session/session-manager.d.ts +19 -0
  42. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  43. package/dist/types/task/executor.d.ts +26 -1
  44. package/dist/types/task/index.d.ts +1 -1
  45. package/dist/types/task/label.d.ts +1 -1
  46. package/dist/types/task/parallel.d.ts +14 -0
  47. package/dist/types/task/structured-subagent.d.ts +111 -0
  48. package/dist/types/task/types.d.ts +51 -0
  49. package/dist/types/telemetry-export.d.ts +34 -9
  50. package/dist/types/tools/approval.d.ts +8 -0
  51. package/dist/types/tools/bash.d.ts +2 -0
  52. package/dist/types/tools/essential-tools.d.ts +29 -0
  53. package/dist/types/tools/fetch.d.ts +4 -5
  54. package/dist/types/tools/hub/messaging.d.ts +5 -1
  55. package/dist/types/tools/image-gen.d.ts +2 -1
  56. package/dist/types/tools/index.d.ts +17 -1
  57. package/dist/types/tools/todo.d.ts +31 -0
  58. package/dist/types/tools/xdev.d.ts +11 -2
  59. package/dist/types/tui/tree-list.d.ts +7 -0
  60. package/dist/types/utils/markit.d.ts +11 -0
  61. package/dist/types/utils/title-generator.d.ts +2 -1
  62. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  63. package/dist/types/web/search/types.d.ts +1 -1
  64. package/package.json +21 -16
  65. package/src/advisor/__tests__/advisor.test.ts +1304 -42
  66. package/src/advisor/__tests__/config.test.ts +58 -2
  67. package/src/advisor/config.ts +76 -24
  68. package/src/advisor/runtime.ts +445 -92
  69. package/src/autolearn/controller.ts +23 -28
  70. package/src/cli/file-processor.ts +1 -2
  71. package/src/cli.ts +5 -1
  72. package/src/config/model-discovery.ts +81 -21
  73. package/src/config/model-registry.ts +25 -6
  74. package/src/config/model-resolver.ts +14 -7
  75. package/src/config/settings-schema.ts +46 -9
  76. package/src/config/settings.ts +405 -25
  77. package/src/cursor.ts +20 -3
  78. package/src/debug/report-bundle.ts +40 -4
  79. package/src/discovery/helpers.ts +28 -5
  80. package/src/eval/__tests__/agent-bridge.test.ts +133 -47
  81. package/src/eval/__tests__/prelude-agent.test.ts +29 -0
  82. package/src/eval/agent-bridge.ts +104 -477
  83. package/src/eval/jl/prelude.jl +7 -6
  84. package/src/eval/js/shared/prelude.txt +5 -4
  85. package/src/eval/py/prelude.py +11 -39
  86. package/src/eval/rb/prelude.rb +5 -6
  87. package/src/exec/bash-executor.ts +14 -5
  88. package/src/extensibility/custom-tools/loader.ts +3 -3
  89. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  90. package/src/extensibility/extensions/loader.ts +3 -3
  91. package/src/extensibility/extensions/managed-timers.ts +83 -0
  92. package/src/extensibility/extensions/runner.ts +26 -0
  93. package/src/extensibility/extensions/types.ts +18 -0
  94. package/src/extensibility/extensions/wrapper.ts +2 -1
  95. package/src/extensibility/hooks/loader.ts +3 -3
  96. package/src/extensibility/legacy-pi-coding-agent-shim.ts +149 -8
  97. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  98. package/src/extensibility/plugins/manager.ts +2 -2
  99. package/src/extensibility/shared-events.ts +6 -0
  100. package/src/extensibility/utils.ts +91 -25
  101. package/src/irc/bus.ts +22 -3
  102. package/src/launch/broker.ts +3 -2
  103. package/src/launch/client.ts +2 -2
  104. package/src/launch/presence.ts +2 -2
  105. package/src/lsp/client.ts +58 -1
  106. package/src/lsp/index.ts +62 -6
  107. package/src/lsp/types.ts +3 -0
  108. package/src/main.ts +11 -8
  109. package/src/mcp/manager.ts +9 -3
  110. package/src/mcp/oauth-flow.ts +20 -0
  111. package/src/mcp/transports/stdio.test.ts +269 -1
  112. package/src/mcp/transports/stdio.ts +255 -24
  113. package/src/modes/components/advisor-config.ts +65 -3
  114. package/src/modes/components/agent-hub.ts +1 -72
  115. package/src/modes/components/ask-dialog.ts +1 -1
  116. package/src/modes/components/bash-execution.ts +7 -3
  117. package/src/modes/components/eval-execution.ts +3 -1
  118. package/src/modes/components/hook-editor.ts +18 -3
  119. package/src/modes/components/model-hub.ts +138 -42
  120. package/src/modes/components/session-selector.ts +4 -0
  121. package/src/modes/components/status-line/component.test.ts +1 -0
  122. package/src/modes/components/status-line/segments.ts +21 -6
  123. package/src/modes/controllers/command-controller.ts +167 -47
  124. package/src/modes/controllers/event-controller.ts +5 -0
  125. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  126. package/src/modes/controllers/input-controller.ts +12 -12
  127. package/src/modes/controllers/selector-controller.ts +191 -31
  128. package/src/modes/interactive-mode.ts +169 -62
  129. package/src/modes/print-mode.ts +3 -3
  130. package/src/modes/rpc/host-tools.ts +2 -1
  131. package/src/modes/rpc/rpc-mode.ts +19 -4
  132. package/src/modes/warp-events.test.ts +794 -0
  133. package/src/modes/warp-events.ts +232 -0
  134. package/src/plan-mode/model-transition.test.ts +60 -0
  135. package/src/plan-mode/model-transition.ts +51 -0
  136. package/src/prompts/system/system-prompt.md +1 -1
  137. package/src/prompts/tools/eval.md +5 -2
  138. package/src/prompts/tools/read.md +1 -1
  139. package/src/prompts/tools/task.md +12 -8
  140. package/src/prompts/tools/write.md +1 -1
  141. package/src/registry/agent-lifecycle.ts +133 -18
  142. package/src/registry/persisted-agents.ts +74 -0
  143. package/src/sdk.ts +343 -122
  144. package/src/session/agent-session.ts +1359 -368
  145. package/src/session/session-entries.ts +6 -1
  146. package/src/session/session-history-format.ts +20 -5
  147. package/src/session/session-manager.ts +57 -0
  148. package/src/session/streaming-output.ts +41 -1
  149. package/src/slash-commands/builtin-registry.ts +7 -0
  150. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  151. package/src/system-prompt.ts +7 -2
  152. package/src/task/executor.ts +100 -22
  153. package/src/task/index.ts +258 -429
  154. package/src/task/label.ts +2 -0
  155. package/src/task/parallel.ts +43 -0
  156. package/src/task/persisted-revive.ts +19 -7
  157. package/src/task/structured-subagent.ts +642 -0
  158. package/src/task/types.ts +58 -0
  159. package/src/telemetry-export.ts +453 -97
  160. package/src/tools/__tests__/eval-description.test.ts +1 -1
  161. package/src/tools/approval.ts +11 -0
  162. package/src/tools/bash.ts +71 -38
  163. package/src/tools/essential-tools.ts +45 -0
  164. package/src/tools/fetch.ts +28 -105
  165. package/src/tools/gh.ts +169 -2
  166. package/src/tools/hub/messaging.ts +16 -3
  167. package/src/tools/image-gen.ts +69 -7
  168. package/src/tools/index.ts +50 -15
  169. package/src/tools/path-utils.ts +1 -0
  170. package/src/tools/read.ts +62 -29
  171. package/src/tools/todo.ts +126 -13
  172. package/src/tools/write.ts +22 -4
  173. package/src/tools/xdev.ts +14 -3
  174. package/src/tui/tree-list.ts +39 -0
  175. package/src/utils/markit.ts +12 -0
  176. package/src/utils/title-generator.ts +15 -4
  177. package/src/utils/zip.ts +16 -1
  178. package/src/web/search/providers/kimi.ts +18 -12
  179. package/src/web/search/types.ts +6 -1
package/src/sdk.ts CHANGED
@@ -2,13 +2,21 @@ import {
2
2
  Agent,
3
3
  type AgentEvent,
4
4
  type AgentMessage,
5
+ type AgentOptions,
5
6
  type AgentTelemetryConfig,
6
7
  type AgentTool,
7
8
  AppendOnlyContextManager,
8
9
  filterProviderReplayMessages,
9
10
  type ThinkingLevel,
10
11
  } from "@oh-my-pi/pi-agent-core";
11
- import type { Context, CredentialDisabledEvent, Message, Model, SimpleStreamOptions } from "@oh-my-pi/pi-ai";
12
+ import type {
13
+ Context,
14
+ CredentialDisabledEvent,
15
+ Message,
16
+ Model,
17
+ ProviderSessionState,
18
+ SimpleStreamOptions,
19
+ } from "@oh-my-pi/pi-ai";
12
20
  import type { Dialect } from "@oh-my-pi/pi-ai/dialect";
13
21
  import {
14
22
  getOpenAICodexTransportDetails,
@@ -26,6 +34,7 @@ import {
26
34
  } from "./advisor";
27
35
  import { type AsyncJob, AsyncJobManager } from "./async";
28
36
  import { AutoLearnController, buildAutoLearnInstructions } from "./autolearn/controller";
37
+ import { createAutoresearchExtension } from "./autoresearch";
29
38
  import { loadCapability } from "./capability";
30
39
  import { type Rule, ruleCapability, setActiveRules } from "./capability/rule";
31
40
  import { bucketRules } from "./capability/rule-buckets";
@@ -137,6 +146,7 @@ import {
137
146
  } from "./system-prompt";
138
147
  import { AgentOutputManager } from "./task/output-manager";
139
148
  import { wrapStreamFnWithProviderConcurrency } from "./task/provider-concurrency";
149
+ import type { StructuredSubagentSchemaMode } from "./task/types";
140
150
  import {
141
151
  AUTO_THINKING,
142
152
  type ConfiguredThinkingLevel,
@@ -154,6 +164,7 @@ import {
154
164
  createTools,
155
165
  createVibeTools,
156
166
  type DeferredDiagnosticsEntry,
167
+ defaultLoadModeForToolName,
157
168
  discoverStartupLspServers,
158
169
  EditTool,
159
170
  EvalTool,
@@ -473,20 +484,30 @@ export interface CreateAgentSessionOptions {
473
484
  /** File-based slash commands. Default: discovered from commands/ directories */
474
485
  slashCommands?: FileSlashCommand[];
475
486
 
476
- /** Enable MCP server discovery from .mcp.json files. Default: true */
487
+ /**
488
+ * Enable MCP capabilities. `false` skips MCP discovery and ignores
489
+ * `mcpManager`, preventing process-global or inherited MCP access. Default:
490
+ * true.
491
+ */
477
492
  enableMCP?: boolean;
478
- /** Existing MCP manager to reuse (skips discovery, propagates to toolSession). */
493
+ /** Existing MCP manager to reuse when MCP is enabled (skips discovery, propagates to toolSession). */
479
494
  mcpManager?: MCPManager;
480
495
 
481
496
  /** Enable LSP integration (tool, formatting, diagnostics, warmup). Default: true */
482
497
  enableLsp?: boolean;
498
+ /** Whether this invocation may expose IRC. `false` removes it even for subagents. */
499
+ enableIrc?: boolean;
483
500
  /** Skip subprocess-kernel availability checks and prelude warmup */
484
501
  skipPythonPreflight?: boolean;
485
502
  /** Tool names explicitly requested (enables disabled-by-default tools) */
486
503
  toolNames?: string[];
504
+ /** Limit the session to explicitly supplied tool names, without discovered extras. */
505
+ restrictToolNames?: boolean;
487
506
 
488
- /** Output schema for structured completion (subagents) */
507
+ /** Output schema for structured completion (subagents). */
489
508
  outputSchema?: unknown;
509
+ /** Enforcement policy for {@link outputSchema}; defaults to legacy permissive behavior. */
510
+ outputSchemaMode?: StructuredSubagentSchemaMode;
490
511
  /** Whether to include the yield tool by default */
491
512
  requireYieldTool?: boolean;
492
513
  /** Task recursion depth (for subagent sessions). Default: 0 */
@@ -890,7 +911,7 @@ function customToolToDefinition(tool: CustomTool): ToolDefinition {
890
911
  description: tool.description,
891
912
  parameters: tool.parameters,
892
913
  hidden: tool.hidden,
893
- loadMode: tool.loadMode ?? "discoverable",
914
+ loadMode: defaultLoadModeForToolName(tool.name, tool.loadMode),
894
915
  deferrable: tool.deferrable,
895
916
  approval: typeof tool.approval === "function" ? tool.approval.bind(tool) : tool.approval,
896
917
  mcpServerName: tool.mcpServerName,
@@ -1055,6 +1076,88 @@ function buildMCPPromptCommands(manager: MCPManager): LoadedCustomCommand[] {
1055
1076
  }
1056
1077
  return commands;
1057
1078
  }
1079
+
1080
+ /** Dependencies used to construct an isolated auto-learn capture agent. */
1081
+ export interface AutoLearnCaptureRunnerOptions {
1082
+ sourceAgent: Agent;
1083
+ captureTools: AgentTool[];
1084
+ createAgent: (options: AgentOptions) => Agent;
1085
+ onPayload?: SimpleStreamOptions["onPayload"];
1086
+ onResponse?: SimpleStreamOptions["onResponse"];
1087
+ createSessionId?: () => string;
1088
+ }
1089
+
1090
+ /** Build a private capture runner over a detached message snapshot and provider session. */
1091
+ export function createAutoLearnCaptureRunner(
1092
+ options: AutoLearnCaptureRunnerOptions,
1093
+ ): (content: string, signal?: AbortSignal) => Promise<void> {
1094
+ return async (content, signal) => {
1095
+ if (options.captureTools.length === 0 || signal?.aborted) return;
1096
+ const captureModel = options.sourceAgent.state.model;
1097
+ if (!captureModel) return;
1098
+
1099
+ const captureSessionId = options.createSessionId?.() ?? Bun.randomUUIDv7();
1100
+ const captureProviderSessionState = new Map<string, ProviderSessionState>();
1101
+ const captureMessages = options.sourceAgent.state.messages.map((message): AgentMessage => {
1102
+ if (message.role === "assistant") {
1103
+ return { ...message, responseId: undefined, providerPayload: undefined };
1104
+ }
1105
+ if (message.role === "user" || message.role === "developer") {
1106
+ return { ...message, providerPayload: undefined };
1107
+ }
1108
+ return message;
1109
+ });
1110
+ const captureAgent = options.createAgent({
1111
+ initialState: {
1112
+ systemPrompt: [...options.sourceAgent.state.systemPrompt],
1113
+ model: captureModel,
1114
+ thinkingLevel: options.sourceAgent.state.thinkingLevel,
1115
+ disableReasoning: options.sourceAgent.state.disableReasoning,
1116
+ tools: options.captureTools,
1117
+ messages: captureMessages,
1118
+ },
1119
+ sessionId: captureSessionId,
1120
+ promptCacheKey: captureSessionId,
1121
+ providerSessionState: captureProviderSessionState,
1122
+ getApiKey: requestModel => options.sourceAgent.getApiKey?.(requestModel),
1123
+ onPayload: options.onPayload,
1124
+ onResponse: options.onResponse,
1125
+ });
1126
+ captureAgent.setMetadataResolver(provider => options.sourceAgent.metadataForProvider(provider));
1127
+ const captureMessage: CustomMessage = {
1128
+ role: "custom",
1129
+ customType: "autolearn-nudge",
1130
+ content,
1131
+ display: false,
1132
+ attribution: "agent",
1133
+ timestamp: Date.now(),
1134
+ };
1135
+ const abortCapture = () => captureAgent.abort(signal?.reason);
1136
+ signal?.addEventListener("abort", abortCapture, { once: true });
1137
+ try {
1138
+ if (signal?.aborted) {
1139
+ abortCapture();
1140
+ return;
1141
+ }
1142
+ await captureAgent.prompt(captureMessage);
1143
+ } catch (error) {
1144
+ if (!signal?.aborted) throw error;
1145
+ } finally {
1146
+ signal?.removeEventListener("abort", abortCapture);
1147
+ for (const [providerKey, state] of captureProviderSessionState) {
1148
+ try {
1149
+ state.close();
1150
+ } catch (error) {
1151
+ logger.warn("Failed to close auto-learn capture provider state", {
1152
+ providerKey,
1153
+ error: String(error),
1154
+ });
1155
+ }
1156
+ }
1157
+ captureProviderSessionState.clear();
1158
+ }
1159
+ };
1160
+ }
1058
1161
  /**
1059
1162
  * Create an AgentSession with the specified options.
1060
1163
  *
@@ -1453,7 +1556,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1453
1556
  let session!: AgentSession;
1454
1557
  let hasSession = false;
1455
1558
  let hasRegistered = false;
1456
- const enableLsp = options.enableLsp ?? true;
1559
+ const restrictToolNames = options.restrictToolNames === true;
1560
+ const enableLsp = !restrictToolNames && (options.enableLsp ?? true);
1457
1561
  const asyncMaxJobs = Math.min(100, Math.max(1, settings.get("async.maxJobs") ?? 100));
1458
1562
  const ASYNC_INLINE_RESULT_MAX_CHARS = 12_000;
1459
1563
  const ASYNC_PREVIEW_MAX_CHARS = 4_000;
@@ -1550,9 +1654,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1550
1654
  setActiveToolNames,
1551
1655
  hasUI: options.hasUI ?? false,
1552
1656
  enableLsp,
1657
+ enableIrc: restrictToolNames ? false : options.enableIrc,
1658
+ restrictToolNames,
1553
1659
  get hasEditTool() {
1554
1660
  const requestedToolNames = options.toolNames ? normalizeToolNames(options.toolNames) : undefined;
1555
- return !requestedToolNames || requestedToolNames.includes("edit");
1661
+ return restrictToolNames
1662
+ ? requestedToolNames?.includes("edit") === true
1663
+ : !requestedToolNames || requestedToolNames.includes("edit");
1556
1664
  },
1557
1665
  skipPythonPreflight: options.skipPythonPreflight,
1558
1666
  contextFiles,
@@ -1564,6 +1672,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1564
1672
  rules: allRules,
1565
1673
  eventBus,
1566
1674
  outputSchema: options.outputSchema,
1675
+ outputSchemaMode: options.outputSchemaMode,
1567
1676
  requireYieldTool: options.requireYieldTool,
1568
1677
  prewalkArmed: options.prewalk !== undefined,
1569
1678
  taskDepth: options.taskDepth ?? 0,
@@ -1680,10 +1789,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1680
1789
  // Create built-in tools (already wrapped with meta notice formatting)
1681
1790
  const builtinTools = await logger.time("createAllTools", createTools, toolSession, options.toolNames);
1682
1791
 
1683
- // Discover MCP tools from .mcp.json files
1684
- let mcpManager: MCPManager | undefined = options.mcpManager;
1792
+ // Restricted sessions cannot inherit or discover MCP capabilities.
1793
+ const enableMCP = !restrictToolNames && (options.enableMCP ?? true);
1794
+ let mcpManager: MCPManager | undefined = enableMCP ? options.mcpManager : undefined;
1685
1795
  toolSession.mcpManager = mcpManager;
1686
- const enableMCP = options.enableMCP ?? true;
1796
+ toolSession.enableMCP = enableMCP;
1687
1797
  const deferMCPDiscoveryForUI = enableMCP && !mcpManager && options.hasUI === true;
1688
1798
  const customTools: CustomTool[] = [];
1689
1799
  let startDeferredMCPDiscovery: ((liveSession: AgentSession) => void) | undefined;
@@ -1769,58 +1879,62 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1769
1879
  // to mirror the AsyncJobManager ownership rule.
1770
1880
  if (mcpManager && !options.parentTaskPrefix) MCPManager.setInstance(mcpManager);
1771
1881
 
1772
- // Add image tools when generation is enabled and either no explicit tool
1773
- // whitelist was given or it names `generate_image`. Image gen is a
1774
- // discoverable custom tool: once it enters the registry the common
1775
- // partition presents it under xd:// (or routes it to BM25 discovery), so no
1776
- // source-specific force-activation is needed only this eligibility gate.
1777
- const imageGenRequested = !options.toolNames || options.toolNames.includes("generate_image");
1778
- if (settings.get("generate_image.enabled") && imageGenRequested) {
1779
- const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
1780
- if (imageGenTools.length > 0) {
1781
- customTools.push(...(imageGenTools as unknown as CustomTool[]));
1882
+ const builtInToolNames = builtinTools.map(t => t.name);
1883
+ let customToolPaths: ToolPathWithSource[] = [];
1884
+ const inlineExtensions: ExtensionFactory[] = [];
1885
+ if (!restrictToolNames) {
1886
+ // Add image tools when generation is enabled and either no explicit tool
1887
+ // whitelist was given or it names `generate_image`. Unlike built-in tools
1888
+ // (filtered in `createTools`), custom tools are force-activated via
1889
+ // `alwaysInclude` below, so an explicit `--no-tools`/whitelist must be
1890
+ // honored here or image-gen would leak past every filter (issue #5305).
1891
+ const imageGenRequested = !options.toolNames || options.toolNames.includes("generate_image");
1892
+ if (settings.get("generate_image.enabled") && imageGenRequested) {
1893
+ const imageGenTools = await logger.time("getImageGenTools", () => getImageGenTools(modelRegistry, model));
1894
+ if (imageGenTools.length > 0) {
1895
+ customTools.push(...(imageGenTools as unknown as CustomTool[]));
1896
+ }
1782
1897
  }
1783
- }
1784
1898
 
1785
- if (settings.get("speechgen.enabled")) {
1786
- customTools.push(ttsTool as unknown as CustomTool);
1787
- }
1899
+ if (settings.get("speechgen.enabled")) {
1900
+ customTools.push(ttsTool as unknown as CustomTool);
1901
+ }
1788
1902
 
1789
- // Add web search tools
1790
- if (options.toolNames?.includes("web_search")) {
1791
- customTools.push(...getSearchTools());
1792
- }
1903
+ // Add web search tools
1904
+ if (options.toolNames?.includes("web_search")) {
1905
+ customTools.push(...getSearchTools());
1906
+ }
1793
1907
 
1794
- // Discover custom tools from `.omp/tools/`, `.claude/tools/`, plugins, etc.
1795
- // Subagents reuse the parent's scan via `preloadedCustomToolPaths` to skip
1796
- // the FS walk, but ALWAYS re-call `loadCustomTools` here so factories bind
1797
- // to THIS session's `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
1798
- // Forwarding the parent's `LoadedCustomTool[]` directly would route tool
1799
- // execution back through the parent — wrong for isolated tasks and for
1800
- // pending-action queueing.
1801
- const builtInToolNames = builtinTools.map(t => t.name);
1802
- const customToolPaths: ToolPathWithSource[] =
1803
- options.preloadedCustomToolPaths ??
1804
- (await logger.time("discoverCustomToolPaths", () => discoverCustomToolPaths([], cwd)));
1805
- const customToolsLoadResult = await logger.time("loadCustomTools", () =>
1806
- loadCustomTools(customToolPaths, cwd, builtInToolNames, action => queueResolveHandler(toolSession, action)),
1807
- );
1808
- for (const { path, error } of customToolsLoadResult.errors) {
1809
- logger.error("Custom tool load failed", { path, error });
1810
- }
1811
- if (customToolsLoadResult.tools.length > 0) {
1812
- customTools.push(...customToolsLoadResult.tools.map(loaded => loaded.tool));
1908
+ // Discover custom tools from `.omp/tools/`, `.claude/tools/`, plugins, etc.
1909
+ // Subagents reuse the parent's scan via `preloadedCustomToolPaths` to skip
1910
+ // the FS walk, but ALWAYS re-call `loadCustomTools` here so factories bind
1911
+ // to THIS session's `CustomToolAPI` (cwd, exec, pushPendingAction, UI).
1912
+ // Forwarding the parent's `LoadedCustomTool[]` directly would route tool
1913
+ // execution back through the parent — wrong for isolated tasks and for
1914
+ // pending-action queueing.
1915
+ customToolPaths =
1916
+ options.preloadedCustomToolPaths ??
1917
+ (await logger.time("discoverCustomToolPaths", () => discoverCustomToolPaths([], cwd)));
1918
+ const customToolsLoadResult = await logger.time("loadCustomTools", () =>
1919
+ loadCustomTools(customToolPaths, cwd, builtInToolNames, action => queueResolveHandler(toolSession, action)),
1920
+ );
1921
+ for (const { path, error } of customToolsLoadResult.errors) {
1922
+ logger.error("Custom tool load failed", { path, error });
1923
+ }
1924
+ if (customToolsLoadResult.tools.length > 0) {
1925
+ customTools.push(...customToolsLoadResult.tools.map(loaded => loaded.tool));
1926
+ }
1927
+
1928
+ inlineExtensions.push(...(options.extensions ?? []));
1929
+ inlineExtensions.push(createAutoresearchExtension);
1930
+ if (customTools.length > 0) {
1931
+ inlineExtensions.push(createCustomToolsExtension(customTools));
1932
+ }
1813
1933
  }
1814
1934
  // Forward the path list (NOT the loaded tools) to subagents so they
1815
1935
  // re-bind under their own `CustomToolAPI` while skipping the FS scan.
1816
1936
  toolSession.customToolPaths = customToolPaths;
1817
1937
 
1818
- const inlineExtensions: ExtensionFactory[] = options.extensions ? [...options.extensions] : [];
1819
- inlineExtensions.push((await import("./autoresearch")).createAutoresearchExtension);
1820
- if (customTools.length > 0) {
1821
- inlineExtensions.push(createCustomToolsExtension(customTools));
1822
- }
1823
-
1824
1938
  // Load extensions. Three paths:
1825
1939
  // 1. `preloadedExtensions` (CLI): caller already loaded — reuse the
1826
1940
  // Extension instances. Shallow-clone `extensions` so the inline
@@ -1835,7 +1949,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1835
1949
  // the flag and pre-resolved the result already reflects that choice.
1836
1950
  let extensionPaths: string[];
1837
1951
  let extensionsResult: LoadExtensionsResult;
1838
- if (options.preloadedExtensions) {
1952
+ if (restrictToolNames) {
1953
+ // Allocate a session runtime without evaluating caller-provided extension
1954
+ // instances, paths, or factories.
1955
+ extensionPaths = [];
1956
+ extensionsResult = await loadExtensions([], cwd, eventBus);
1957
+ } else if (options.preloadedExtensions) {
1839
1958
  extensionsResult = {
1840
1959
  ...options.preloadedExtensions,
1841
1960
  extensions: [...options.preloadedExtensions.extensions],
@@ -2144,11 +2263,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2144
2263
  }
2145
2264
  }
2146
2265
 
2147
- // Discover custom commands (TypeScript slash commands)
2148
- const customCommandsResult: CustomCommandsLoadResult = options.disableExtensionDiscovery
2149
- ? { commands: [], errors: [] }
2150
- : await logger.time("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
2151
- if (!options.disableExtensionDiscovery) {
2266
+ // Restricted sessions do not discover or evaluate custom command modules.
2267
+ const customCommandsResult: CustomCommandsLoadResult =
2268
+ options.disableExtensionDiscovery || restrictToolNames
2269
+ ? { commands: [], errors: [] }
2270
+ : await logger.time("discoverCustomCommands", loadCustomCommandsInternal, { cwd, agentDir });
2271
+ if (!options.disableExtensionDiscovery && !restrictToolNames) {
2152
2272
  for (const { path, error } of customCommandsResult.errors) {
2153
2273
  logger.error("Failed to load custom command", { path, error });
2154
2274
  }
@@ -2193,8 +2313,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2193
2313
  });
2194
2314
  const toolContextStore = new ToolContextStore(getSessionContext);
2195
2315
 
2196
- const registeredTools = extensionRunner.getAllRegisteredTools();
2197
- const sdkCustomTools = options.customTools?.filter(tool => !isLegacyBuiltinToolDefinition(tool)) ?? [];
2316
+ const registeredTools = restrictToolNames ? [] : extensionRunner.getAllRegisteredTools();
2317
+ const sdkCustomTools = restrictToolNames
2318
+ ? []
2319
+ : (options.customTools?.filter(tool => !isLegacyBuiltinToolDefinition(tool)) ?? []);
2198
2320
  const allCustomTools = [
2199
2321
  ...registeredTools,
2200
2322
  ...sdkCustomTools.map(tool => {
@@ -2217,7 +2339,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2217
2339
  toolRegistry.set(tool.name, tool);
2218
2340
  builtInRegistryToolNames.add(tool.name);
2219
2341
  }
2220
- if (!toolRegistry.has("goal") && settings.get("goal.enabled")) {
2342
+ if (!restrictToolNames && !toolRegistry.has("goal") && settings.get("goal.enabled")) {
2221
2343
  const goalTool = await logger.time("createTools:goal:session", HIDDEN_TOOLS.goal, toolSession);
2222
2344
  if (goalTool) {
2223
2345
  toolRegistry.set(goalTool.name, wrapToolWithMetaNotice(goalTool));
@@ -2247,31 +2369,50 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2247
2369
  builtInRegistryToolNames.delete("edit");
2248
2370
  }
2249
2371
 
2250
- // Staged actions (tool previews, plan approval) resolve through `write`
2251
- // to the resolution devices (`xd://resolve`, `xd://reject`,
2252
- // `xd://propose`), so `write` must stay in the registry whenever any
2253
- // code path can stage one: a deferrable tool, or plan mode installing a
2254
- // plan-proposal handler (issue #1428). Dropping it on read-only sessions
2255
- // (e.g. plan-mode toolset `read`, `search`, `find`, `web_search`) leaves
2256
- // plan mode unable to exit through the intended path.
2257
- const hasDeferrableTools = Array.from(toolRegistry.values()).some(tool => tool.deferrable === true);
2258
- const hasXdevTools = (toolSession.xdevRegistry?.size ?? 0) > 0;
2259
- const planModeAvailable = settings.get("plan.enabled");
2260
- if ((hasDeferrableTools || hasXdevTools || planModeAvailable) && !toolRegistry.has("write")) {
2261
- const writeTool = await logger.time("createTools:write:session", BUILTIN_TOOLS.write, toolSession);
2262
- if (writeTool) {
2372
+ let writeRegistration: Promise<boolean> | undefined;
2373
+ const ensureWriteRegistered = (): Promise<boolean> => {
2374
+ if (toolRegistry.has("write")) return Promise.resolve(builtInRegistryToolNames.has("write"));
2375
+ writeRegistration ??= (async () => {
2376
+ const writeTool = await logger.time("createTools:write:session", BUILTIN_TOOLS.write, toolSession);
2377
+ if (!writeTool || toolRegistry.has("write")) return builtInRegistryToolNames.has("write");
2263
2378
  toolRegistry.set(
2264
2379
  writeTool.name,
2265
2380
  new ExtensionToolWrapper(wrapToolWithMetaNotice(writeTool), extensionRunner) as Tool,
2266
2381
  );
2267
2382
  builtInRegistryToolNames.add(writeTool.name);
2268
- }
2383
+ return true;
2384
+ })().finally(() => {
2385
+ writeRegistration = undefined;
2386
+ });
2387
+ return writeRegistration;
2388
+ };
2389
+
2390
+ // Existing staged/device paths need write registered before active-set assembly.
2391
+ // Deferred MCP also registers it now, but refresh activates it only after a server connects.
2392
+ const hasDeferrableTools = Array.from(toolRegistry.values()).some(tool => tool.deferrable === true);
2393
+ const hasXdevTools = (toolSession.xdevRegistry?.size ?? 0) > 0;
2394
+ const planModeAvailable = settings.get("plan.enabled");
2395
+ if (!restrictToolNames && (hasDeferrableTools || hasXdevTools || planModeAvailable || deferMCPDiscoveryForUI)) {
2396
+ await ensureWriteRegistered();
2269
2397
  }
2270
2398
 
2271
2399
  let cursorEventEmitter: ((event: AgentEvent) => void) | undefined;
2400
+ // Built-in xd:// devices (ast_edit, debug, browser, lsp, web_search) are
2401
+ // mounted in createTools BEFORE this loop wraps registry entries in
2402
+ // ExtensionToolWrapper, so the registry holds them unwrapped. The normal
2403
+ // `write xd://<tool>` path runs approval through the wrapped `write` tool's
2404
+ // tier gate, but Cursor invokes advertised devices via `tool.execute()`
2405
+ // directly — so wrap unwrapped devices here to keep the approval/deny/prompt
2406
+ // gate. Dynamic mounts (custom/MCP) already come from the wrapped registry.
2407
+ const resolveCursorDevice = (name: string): AgentTool | undefined => {
2408
+ const device = toolSession.xdevRegistry?.get(name);
2409
+ if (!device) return undefined;
2410
+ return device instanceof ExtensionToolWrapper ? device : new ExtensionToolWrapper(device, extensionRunner);
2411
+ };
2272
2412
  const cursorExecHandlers = new CursorExecHandlers({
2273
2413
  cwd,
2274
2414
  tools: toolRegistry,
2415
+ getTool: resolveCursorDevice,
2275
2416
  getToolContext: () => toolContextStore.getContext(),
2276
2417
  emitEvent: event => cursorEventEmitter?.(event),
2277
2418
  });
@@ -2291,8 +2432,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2291
2432
  ): Promise<BuildSystemPromptResult> => {
2292
2433
  toolContextStore.setToolNames(toolNames);
2293
2434
  const promptTools = buildSystemPromptToolMetadata(tools);
2294
- const memoryBackend = await resolveMemoryBackend(settings);
2295
- const memoryInstructions = await memoryBackend.buildDeveloperInstructions(agentDir, settings, session);
2435
+ const memoryBackend = restrictToolNames ? undefined : await resolveMemoryBackend(settings);
2436
+ const memoryInstructions = memoryBackend
2437
+ ? await memoryBackend.buildDeveloperInstructions(agentDir, settings, session)
2438
+ : undefined;
2296
2439
 
2297
2440
  // Build combined append prompt: memory instructions + auto-learn guidance
2298
2441
  // + MCP server instructions. For UI sessions MCP discovery is deferred, so
@@ -2307,10 +2450,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2307
2450
  // session-start build — so a subagent that filtered them out, a mid-session
2308
2451
  // enable that never built them, or a same-named custom tool while auto-learn
2309
2452
  // is off all get no guidance.
2310
- const autoLearnInstructions = buildAutoLearnInstructions({
2311
- manageSkill: builtInToolNames.includes("manage_skill"),
2312
- learn: builtInToolNames.includes("learn"),
2313
- });
2453
+ const autoLearnInstructions = restrictToolNames
2454
+ ? undefined
2455
+ : buildAutoLearnInstructions({
2456
+ manageSkill: builtInToolNames.includes("manage_skill"),
2457
+ learn: builtInToolNames.includes("learn"),
2458
+ });
2314
2459
  const appendParts: string[] = [];
2315
2460
  if (memoryInstructions) appendParts.push(memoryInstructions);
2316
2461
  if (autoLearnInstructions) appendParts.push(autoLearnInstructions);
@@ -2342,7 +2487,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2342
2487
  cwd,
2343
2488
  xdevTools: toolSession.xdevRegistry?.entries() ?? [],
2344
2489
  xdevDocs: toolSession.xdevRegistry?.docsAll() ?? "",
2345
- autoQaEnabled: isAutoQaEnabled(settings),
2490
+ autoQaEnabled: !restrictToolNames && isAutoQaEnabled(settings),
2346
2491
  resolvedCustomPrompt: options.customSystemPrompt,
2347
2492
  skills: session?.skills ?? skills,
2348
2493
  contextFiles,
@@ -2359,11 +2504,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2359
2504
  eagerTasksAlways,
2360
2505
  taskBatch: settings.get("task.batch"),
2361
2506
  taskMaxConcurrency: settings.get("task.maxConcurrency"),
2362
- taskIrcEnabled: isIrcEnabled(settings, options.taskDepth ?? 0),
2507
+ taskIrcEnabled: !restrictToolNames && isIrcEnabled(settings, options.taskDepth ?? 0),
2363
2508
  secretsEnabled,
2364
2509
  workspaceTree: workspaceTreePromise,
2365
2510
  includeWorkspaceTree,
2366
- memoryRootEnabled: memoryBackend.id === "local",
2511
+ memoryRootEnabled: memoryBackend?.id === "local",
2367
2512
  model: getActiveModelString(),
2368
2513
  includeModelInPrompt: settings.get("includeModelInPrompt"),
2369
2514
  personality: agentKind === "sub" ? "none" : settings.get("personality"),
@@ -2404,7 +2549,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2404
2549
  // exactly the builtins createTools built (`builtInToolNames` — provenance, so a
2405
2550
  // same-named custom/extension tool is never force-activated when auto-learn is
2406
2551
  // off) to keep guidance, controller, and the active set consistent.
2407
- if (explicitlyRequestedToolNames) {
2552
+ if (!restrictToolNames && explicitlyRequestedToolNames) {
2408
2553
  for (const name of ["manage_skill", "learn"]) {
2409
2554
  if (builtInToolNames.includes(name) && !explicitlyRequestedToolNames.includes(name)) {
2410
2555
  explicitlyRequestedToolNames.push(name);
@@ -2413,21 +2558,29 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2413
2558
  }
2414
2559
  const requestedToolNames = explicitlyRequestedToolNames ?? toolNamesFromRegistry;
2415
2560
  const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
2416
- const requestedToolNameSet = new Set(normalizedRequested);
2417
2561
  const defaultInactiveToolNames = new Set(
2418
2562
  registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
2419
2563
  );
2420
2564
  const requestedActiveToolNames = normalizedRequested.filter(name => name !== "goal");
2565
+ const explicitlyRequestedToolNameSet = explicitlyRequestedToolNames
2566
+ ? new Set(explicitlyRequestedToolNames)
2567
+ : undefined;
2568
+ const xdevReadAvailable =
2569
+ builtInRegistryToolNames.has("read") &&
2570
+ (explicitlyRequestedToolNameSet === undefined || explicitlyRequestedToolNameSet.has("read"));
2421
2571
  const initialRequestedActiveToolNames = options.toolNames
2422
2572
  ? requestedActiveToolNames
2423
2573
  : requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
2424
2574
  let initialToolNames = [...initialRequestedActiveToolNames];
2425
2575
 
2426
- // Custom tools and extension-registered tools are always included regardless of toolNames filter
2427
- const alwaysInclude: string[] = [
2428
- ...sdkCustomTools.map(t => (isCustomTool(t) ? t.name : t.name)),
2429
- ...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
2430
- ];
2576
+ // Custom tools and extension-registered tools are always included regardless of toolNames filter.
2577
+ // Restricted callers own the list, so never widen it with registered tools.
2578
+ const alwaysInclude: string[] = restrictToolNames
2579
+ ? []
2580
+ : [
2581
+ ...sdkCustomTools.map(t => (isCustomTool(t) ? t.name : t.name)),
2582
+ ...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
2583
+ ];
2431
2584
  for (const name of alwaysInclude) {
2432
2585
  if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
2433
2586
  initialToolNames.push(name);
@@ -2449,24 +2602,32 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2449
2602
  });
2450
2603
  hasRegistered = true;
2451
2604
 
2452
- // Partition the initial enabled set for the xd:// transport: discoverable
2453
- // tools become mounted devices; the rest stay top-level. The registry
2454
- // already holds the built-in devices (mounted in createTools); this
2455
- // reconciles the initial dynamic mounts (image-gen, TTS, startup MCP,
2456
- // active extension tools) and drops them from the top-level names so they
2457
- // never ship a schema. Presentation only — selection already happened.
2605
+ // Partition the initial enabled set for the xd:// transport: ambient
2606
+ // discoverable tools become mounted devices, while explicitly requested
2607
+ // tools keep their top-level presentation. The registry already holds the
2608
+ // default-set built-in devices from createTools; this reconciles dynamic
2609
+ // mounts (image-gen, TTS, startup MCP, active extension tools).
2458
2610
  let initialMountedXdevToolNames: string[] = [];
2459
2611
  if (toolSession.xdevRegistry) {
2460
2612
  const topLevelToolNames: string[] = [];
2461
2613
  const mountedTools: Tool[] = [];
2462
2614
  for (const name of initialToolNames) {
2463
2615
  const tool = toolRegistry.get(name);
2464
- if (tool && isMountableUnderXdev(tool)) mountedTools.push(tool);
2616
+ const explicitlyRequested = explicitlyRequestedToolNameSet?.has(name) === true;
2617
+ if (tool && xdevReadAvailable && !explicitlyRequested && isMountableUnderXdev(tool))
2618
+ mountedTools.push(tool);
2465
2619
  else topLevelToolNames.push(name);
2466
2620
  }
2467
- toolSession.xdevRegistry.reconcile(mountedTools);
2468
- initialMountedXdevToolNames = mountedTools.map(tool => tool.name);
2469
- initialToolNames = topLevelToolNames;
2621
+ const writeTransportAvailable = mountedTools.length === 0 || (await ensureWriteRegistered());
2622
+ if (writeTransportAvailable) {
2623
+ toolSession.xdevRegistry.reconcile(mountedTools);
2624
+ initialMountedXdevToolNames = mountedTools.map(tool => tool.name);
2625
+ initialToolNames = topLevelToolNames;
2626
+ if (initialMountedXdevToolNames.length > 0 && !initialToolNames.includes("write"))
2627
+ initialToolNames.push("write");
2628
+ } else {
2629
+ toolSession.xdevRegistry.reconcile([]);
2630
+ }
2470
2631
  }
2471
2632
 
2472
2633
  setActiveToolNames(initialToolNames);
@@ -2551,6 +2712,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2551
2712
  const initialTools = initialToolNames
2552
2713
  .map(name => toolRegistry.get(name))
2553
2714
  .filter((tool): tool is AgentTool => tool !== undefined);
2715
+ const autoLearnCaptureTools = initialTools.filter(tool => tool.name === "manage_skill" || tool.name === "learn");
2554
2716
 
2555
2717
  const openaiWebsocketSetting = settings.get("providers.openaiWebsockets") ?? "off";
2556
2718
  const preferOpenAICodexWebsockets =
@@ -2577,6 +2739,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2577
2739
  settings,
2578
2740
  createSettingsAwareStreamFn(settings),
2579
2741
  );
2742
+ const transformToolCallArguments = (args: Record<string, unknown>): Record<string, unknown> => {
2743
+ let result = args;
2744
+ const maxTimeout = settings.get("tools.maxTimeout");
2745
+ if (maxTimeout > 0 && typeof result.timeout === "number") {
2746
+ result = { ...result, timeout: Math.min(result.timeout, maxTimeout) };
2747
+ }
2748
+ if (obfuscator?.hasSecrets()) {
2749
+ result = deobfuscateToolArguments(obfuscator, result);
2750
+ }
2751
+ return result;
2752
+ };
2753
+ const kimiApiFormatSetting = settings.get("providers.kimiApiFormat");
2754
+ const kimiApiFormat = kimiApiFormatSetting === "auto" ? undefined : kimiApiFormatSetting;
2580
2755
  agent = new Agent({
2581
2756
  initialState: {
2582
2757
  systemPrompt,
@@ -2610,7 +2785,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2610
2785
  presencePenalty: settings.get("presencePenalty") >= 0 ? settings.get("presencePenalty") : undefined,
2611
2786
  repetitionPenalty: settings.get("repetitionPenalty") >= 0 ? settings.get("repetitionPenalty") : undefined,
2612
2787
  hideThinkingSummary: settings.get("omitThinking"),
2613
- kimiApiFormat: settings.get("providers.kimiApiFormat") ?? "anthropic",
2788
+ kimiApiFormat,
2614
2789
  preferWebsockets: preferOpenAICodexWebsockets,
2615
2790
  getToolContext: tc => toolContextStore.getContext(tc),
2616
2791
  getApiKey: requestModel => modelRegistry.resolver(requestModel, agent.sessionId),
@@ -2629,17 +2804,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2629
2804
  return settingsAwareStreamFn(streamModel, context, streamOptions);
2630
2805
  },
2631
2806
  cursorExecHandlers,
2632
- transformToolCallArguments: (args, _toolName) => {
2633
- let result = args;
2634
- const maxTimeout = settings.get("tools.maxTimeout");
2635
- if (maxTimeout > 0 && typeof result.timeout === "number") {
2636
- result = { ...result, timeout: Math.min(result.timeout, maxTimeout) };
2637
- }
2638
- if (obfuscator?.hasSecrets()) {
2639
- result = deobfuscateToolArguments(obfuscator, result);
2640
- }
2641
- return result;
2642
- },
2807
+ getCursorTools: () => [...(toolSession.xdevRegistry?.list() ?? [])],
2808
+ transformToolCallArguments,
2643
2809
  intentTracing: !!intentField,
2644
2810
  pruneToolDescriptions: inlineToolDescriptors,
2645
2811
  dialect: resolveDialect(settings.get("tools.format"), model),
@@ -2761,8 +2927,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2761
2927
  getXdevToolEntries: () => toolSession.xdevRegistry?.entries() ?? [],
2762
2928
  xdevRegistry: toolSession.xdevRegistry,
2763
2929
  initialMountedXdevToolNames,
2764
- requestedToolNames: requestedToolNameSet,
2930
+ presentationPinnedToolNames: explicitlyRequestedToolNameSet,
2765
2931
  setActiveToolNames,
2932
+ ensureWriteRegistered,
2766
2933
  getMcpServerInstructions: mcpManager
2767
2934
  ? () => {
2768
2935
  const raw = mcpManager.getServerInstructions();
@@ -2922,8 +3089,56 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2922
3089
  });
2923
3090
  };
2924
3091
 
2925
- // Auto-learn can immediately trigger a synthetic capture turn after the
2926
- // first real stop. When a memory backend is selected, install that backend's
3092
+ const runAutoLearnCapture = createAutoLearnCaptureRunner({
3093
+ sourceAgent: agent,
3094
+ captureTools: autoLearnCaptureTools,
3095
+ onPayload,
3096
+ onResponse,
3097
+ createAgent: captureOptions => {
3098
+ const captureModel = captureOptions.initialState?.model;
3099
+ const captureSessionId = captureOptions.sessionId;
3100
+ if (!captureModel || !captureSessionId) throw new Error("Auto-learn capture identity is incomplete");
3101
+ return new Agent({
3102
+ ...captureOptions,
3103
+ cwd: sessionManager.getCwd(),
3104
+ cwdResolver: () => sessionManager.getCwd(),
3105
+ convertToLlm: convertToLlmFinal,
3106
+ transformContext: async messages => wrapSteeringForModel(messages),
3107
+ transformProviderContext: async (context, transformModel) => {
3108
+ const transformed = obfuscator ? obfuscateProviderContext(obfuscator, context) : context;
3109
+ return clampProviderContextImages(transformed, transformModel);
3110
+ },
3111
+ thinkingBudgets: agent.thinkingBudgets,
3112
+ temperature: agent.temperature,
3113
+ topP: agent.topP,
3114
+ topK: agent.topK,
3115
+ minP: agent.minP,
3116
+ presencePenalty: agent.presencePenalty,
3117
+ repetitionPenalty: agent.repetitionPenalty,
3118
+ serviceTierResolver: agent.serviceTierResolver,
3119
+ hideThinkingSummary: agent.hideThinkingSummary,
3120
+ maxRetryDelayMs: agent.maxRetryDelayMs,
3121
+ kimiApiFormat,
3122
+ preferWebsockets: preferOpenAICodexWebsockets,
3123
+ getToolContext: toolCall => toolContextStore.getContext(toolCall),
3124
+ streamFn: settingsAwareStreamFn,
3125
+ transformToolCallArguments,
3126
+ intentTracing: !!intentField,
3127
+ pruneToolDescriptions: inlineToolDescriptors,
3128
+ dialect: resolveDialect(settings.get("tools.format"), captureModel),
3129
+ abortOnFabricatedToolResult: settings.get("tools.abortOnFabricatedResult"),
3130
+ appendOnlyContext: shouldEnableAppendOnlyContext(
3131
+ settings.get("provider.appendOnlyContext"),
3132
+ captureModel,
3133
+ )
3134
+ ? new AppendOnlyContextManager()
3135
+ : undefined,
3136
+ });
3137
+ },
3138
+ });
3139
+
3140
+ // Auto-learn can immediately trigger a private capture after the first real
3141
+ // stop. When a memory backend is selected, install that backend's
2927
3142
  // per-session state first so the capture turn's `learn` tool observes the
2928
3143
  // same initialized state as normal memory tools. Other sessions keep memory
2929
3144
  // startup in the background to preserve the existing startup profile.
@@ -2936,11 +3151,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2936
3151
  // and the tools; the fire-time re-check in `#onAgentEnd` still handles a
2937
3152
  // mid-session DISABLE. The subscription lives for the session's lifetime; the
2938
3153
  // reference is intentionally discarded (the listener retains it).
2939
- if (settings.get("autolearn.enabled") && taskDepth === 0) {
2940
- await logger.time("startMemoryStartupTask", startMemoryBackend);
2941
- new AutoLearnController({ session, settings });
2942
- } else {
2943
- void logger.time("startMemoryStartupTask", startMemoryBackend);
3154
+ if (!restrictToolNames) {
3155
+ if (settings.get("autolearn.enabled") && taskDepth === 0) {
3156
+ await logger.time("startMemoryStartupTask", startMemoryBackend);
3157
+ new AutoLearnController({
3158
+ session,
3159
+ settings,
3160
+ capture: content => session.runAutolearnCapture(signal => runAutoLearnCapture(content, signal)),
3161
+ });
3162
+ } else {
3163
+ void logger.time("startMemoryStartupTask", startMemoryBackend);
3164
+ }
2944
3165
  }
2945
3166
 
2946
3167
  // Wire MCP manager callbacks to session for reactive tool updates.