@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.6

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 (207) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-x9zt79k8.md} +31 -0
  3. package/dist/cli.js +2989 -2978
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +23 -1
  7. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  8. package/dist/types/internal-urls/parse.d.ts +12 -0
  9. package/dist/types/internal-urls/router.d.ts +6 -0
  10. package/dist/types/internal-urls/types.d.ts +6 -0
  11. package/dist/types/lsp/config.d.ts +1 -0
  12. package/dist/types/lsp/types.d.ts +2 -0
  13. package/dist/types/mcp/manager.d.ts +5 -0
  14. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  15. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  16. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  17. package/dist/types/sdk.d.ts +3 -1
  18. package/dist/types/session/agent-session-types.d.ts +3 -1
  19. package/dist/types/session/agent-session.d.ts +2 -0
  20. package/dist/types/session/model-controls.d.ts +3 -0
  21. package/dist/types/session/session-advisors.d.ts +7 -1
  22. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  23. package/dist/types/session/turn-recovery.d.ts +4 -2
  24. package/dist/types/task/index.d.ts +1 -1
  25. package/dist/types/task/types.d.ts +3 -11
  26. package/dist/types/thinking.d.ts +21 -2
  27. package/dist/types/utils/title-generator.d.ts +17 -16
  28. package/package.json +12 -12
  29. package/src/capability/index.ts +43 -12
  30. package/src/capability/mcp.ts +21 -0
  31. package/src/capability/types.ts +20 -1
  32. package/src/cli/read-cli.ts +44 -2
  33. package/src/config/settings-schema.ts +27 -1
  34. package/src/eval/py/runner.py +16 -2
  35. package/src/extensibility/extensions/runner.ts +91 -5
  36. package/src/extensibility/extensions/types.ts +0 -1
  37. package/src/extensibility/hooks/types.ts +0 -1
  38. package/src/internal-urls/mcp-protocol.ts +17 -3
  39. package/src/internal-urls/parse.ts +31 -0
  40. package/src/internal-urls/router.ts +24 -4
  41. package/src/internal-urls/types.ts +6 -0
  42. package/src/live/transport.ts +2 -2
  43. package/src/lsp/client.ts +2 -2
  44. package/src/lsp/config.ts +4 -0
  45. package/src/lsp/types.ts +2 -0
  46. package/src/mcp/config.ts +26 -14
  47. package/src/mcp/manager.ts +26 -9
  48. package/src/mcp/tool-bridge.ts +52 -1
  49. package/src/memories/index.ts +25 -6
  50. package/src/modes/components/status-line/segments.ts +3 -1
  51. package/src/modes/controllers/command-controller.ts +10 -10
  52. package/src/modes/controllers/extension-ui-controller.ts +3 -3
  53. package/src/modes/rpc/rpc-mode.ts +60 -47
  54. package/src/prompts/steering/user-interjection.md +2 -5
  55. package/src/prompts/tools/task.md +4 -2
  56. package/src/sdk.ts +17 -8
  57. package/src/session/agent-session-types.ts +3 -0
  58. package/src/session/agent-session.ts +21 -4
  59. package/src/session/model-controls.ts +43 -7
  60. package/src/session/session-advisors.ts +30 -14
  61. package/src/session/session-tools.ts +4 -2
  62. package/src/session/tool-choice-queue.ts +19 -4
  63. package/src/session/turn-recovery.ts +21 -2
  64. package/src/task/executor.ts +11 -3
  65. package/src/task/index.ts +43 -32
  66. package/src/task/types.ts +12 -17
  67. package/src/thinking.ts +68 -5
  68. package/src/tools/read.ts +2 -2
  69. package/src/utils/title-generator.ts +88 -34
  70. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  71. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  72. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  73. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  74. package/dist/types/cli/update-cli.test.d.ts +0 -1
  75. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  76. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  77. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  78. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  79. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  80. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  81. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  82. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  83. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  84. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  85. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  86. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  87. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  88. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  89. package/dist/types/hindsight/client.test.d.ts +0 -1
  90. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  91. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  92. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  93. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  94. package/dist/types/launch/protocol.test.d.ts +0 -1
  95. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  96. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  97. package/dist/types/live/protocol.test.d.ts +0 -1
  98. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  99. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  100. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  101. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  102. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  103. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  104. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  105. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  106. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  107. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  108. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  109. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  110. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  111. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  112. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  113. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  114. package/dist/types/modes/print-mode.test.d.ts +0 -1
  115. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  116. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  117. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  118. package/dist/types/modes/warp-events.test.d.ts +0 -1
  119. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  120. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  121. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  122. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  123. package/dist/types/session/blob-store.test.d.ts +0 -1
  124. package/dist/types/session/messages.test.d.ts +0 -1
  125. package/dist/types/session/session-context.test.d.ts +0 -1
  126. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  127. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  128. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  129. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  130. package/dist/types/system-prompt.test.d.ts +0 -1
  131. package/dist/types/task/render.test.d.ts +0 -1
  132. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  133. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  134. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  135. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  136. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  137. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  138. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  139. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  140. package/src/advisor/__tests__/config.test.ts +0 -349
  141. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  142. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  143. package/src/cli/update-cli.test.ts +0 -28
  144. package/src/config/__tests__/model-registry.test.ts +0 -182
  145. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  146. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  147. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  148. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  149. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  150. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  151. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  152. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  153. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  154. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  155. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  156. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  157. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  158. package/src/hindsight/client.test.ts +0 -75
  159. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  160. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  161. package/src/launch/broker-list-order.test.ts +0 -89
  162. package/src/launch/broker-output-snapshot.test.ts +0 -126
  163. package/src/launch/protocol.test.ts +0 -59
  164. package/src/launch/spawn-options.test.ts +0 -31
  165. package/src/launch/terminal-output.test.ts +0 -107
  166. package/src/live/protocol.test.ts +0 -140
  167. package/src/mcp/config-writer.test.ts +0 -43
  168. package/src/mcp/smithery-auth.test.ts +0 -29
  169. package/src/mcp/smithery-registry.test.ts +0 -51
  170. package/src/mcp/transports/stdio.test.ts +0 -427
  171. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  172. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  173. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  174. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  175. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  176. package/src/modes/components/custom-editor.test.ts +0 -510
  177. package/src/modes/components/login-dialog.test.ts +0 -56
  178. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  179. package/src/modes/components/status-line/component.test.ts +0 -84
  180. package/src/modes/components/tool-execution.test.ts +0 -162
  181. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  182. package/src/modes/noninteractive-dispose.test.ts +0 -73
  183. package/src/modes/print-mode.test.ts +0 -71
  184. package/src/modes/session-teardown.test.ts +0 -219
  185. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  186. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  187. package/src/modes/warp-events.test.ts +0 -794
  188. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  189. package/src/plan-mode/model-transition.test.ts +0 -60
  190. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  191. package/src/session/agent-session-error-log.test.ts +0 -59
  192. package/src/session/blob-store.test.ts +0 -56
  193. package/src/session/messages.test.ts +0 -282
  194. package/src/session/session-context.test.ts +0 -384
  195. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  196. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  197. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  198. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  199. package/src/system-prompt.test.ts +0 -236
  200. package/src/task/render.test.ts +0 -290
  201. package/src/task/spawn-policy.test.ts +0 -62
  202. package/src/tools/__tests__/eval-description.test.ts +0 -18
  203. package/src/tools/__tests__/glob.test.ts +0 -37
  204. package/src/tools/__tests__/json-tree.test.ts +0 -35
  205. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  206. package/src/tools/hub/launch-compat.test.ts +0 -40
  207. package/src/vibe/__tests__/token-rate.test.ts +0 -96
package/src/lsp/client.ts CHANGED
@@ -918,7 +918,7 @@ export async function ensureFileOpen(client: LspClient, filePath: string, signal
918
918
  if (isEnoent(err)) return;
919
919
  throw err;
920
920
  }
921
- const languageId = detectLanguageId(filePath);
921
+ const languageId = client.config.languageId ?? detectLanguageId(filePath);
922
922
  throwIfAborted(signal);
923
923
 
924
924
  await sendNotification(
@@ -992,7 +992,7 @@ export async function syncContent(
992
992
 
993
993
  if (!info) {
994
994
  // Open file with provided content instead of reading from disk
995
- const languageId = detectLanguageId(filePath);
995
+ const languageId = client.config.languageId ?? detectLanguageId(filePath);
996
996
  throwIfAborted(signal);
997
997
  await sendNotification(
998
998
  client,
package/src/lsp/config.ts CHANGED
@@ -74,6 +74,8 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
74
74
  const fileTypes =
75
75
  normalizeStringArray(config.fileTypes) ?? normalizeExtensionToFileTypes(config.extensionToLanguage);
76
76
  const rootMarkers = normalizeStringArray(config.rootMarkers) ?? (config.extensionToLanguage ? ["."] : null);
77
+ const languageId =
78
+ typeof config.languageId === "string" && config.languageId.length > 0 ? config.languageId : undefined;
77
79
 
78
80
  if (!command || !fileTypes || !rootMarkers) {
79
81
  logger.warn("Ignoring invalid LSP server config (missing required fields).", { name });
@@ -95,6 +97,7 @@ function normalizeServerConfig(name: string, config: RawServerConfig): ServerCon
95
97
  args,
96
98
  fileTypes,
97
99
  rootMarkers,
100
+ languageId,
98
101
  ...(initOptions ? { initOptions } : {}),
99
102
  };
100
103
  }
@@ -423,6 +426,7 @@ function getConfigSources(cwd: string): ConfigSource[] {
423
426
  * "command": "/path/to/server",
424
427
  * "args": ["--stdio"],
425
428
  * "fileTypes": [".xyz"],
429
+ * "languageId": "xyz",
426
430
  * "rootMarkers": [".xyz-project"]
427
431
  * }
428
432
  * }
package/src/lsp/types.ts CHANGED
@@ -340,6 +340,8 @@ export interface ServerConfig {
340
340
  command: string;
341
341
  args?: string[];
342
342
  fileTypes: string[];
343
+ /** LSP language identifier sent in didOpen; inferred from the file path when omitted. */
344
+ languageId?: string;
343
345
  rootMarkers: string[];
344
346
  initOptions?: Record<string, unknown>;
345
347
  settings?: Record<string, unknown>;
package/src/mcp/config.ts CHANGED
@@ -97,14 +97,6 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
97
97
  const filterExa = options?.filterExa ?? true;
98
98
  const filterBrowser = options?.filterBrowser ?? false;
99
99
 
100
- // Load MCP servers via capability system
101
- const result = await loadCapability<MCPServer>(mcpCapability.id, { cwd });
102
-
103
- // Filter out project-level configs if disabled
104
- const servers = enableProjectConfig
105
- ? result.items
106
- : result.items.filter(server => server._source.level !== "project");
107
-
108
100
  // Load user-level disable/force-enable lists. The denylist always wins; the
109
101
  // allowlist overrides a non-writable source config's `enabled: false`.
110
102
  const userPath = getMCPConfigPath("user", cwd);
@@ -112,14 +104,34 @@ export async function loadAllMCPConfigs(cwd: string, options?: LoadMCPConfigsOpt
112
104
  readDisabledServers(userPath).then(list => new Set(list)),
113
105
  readEnabledServers(userPath).then(list => new Set(list)),
114
106
  ]);
115
- // Convert to legacy format and preserve source metadata
107
+
108
+ // Scope exclusions drop entries entirely BEFORE deduplication: with project
109
+ // config disabled, a project entry must not shadow anything.
110
+ const includeServer = (server: MCPServer & { _source: SourceMeta }): boolean =>
111
+ enableProjectConfig || server._source.level !== "project";
112
+
113
+ // Disabled servers are suppressed rather than dropped: they still own their
114
+ // name at key-level dedupe (a disabled project `foo` keeps a same-named,
115
+ // lower-priority user `foo` disabled), but never equivalence-shadow a
116
+ // differently-named enabled server — otherwise the disabled alias would be
117
+ // removed downstream and starve the surviving connection.
118
+ const suppressServer = (server: MCPServer & { _source: SourceMeta }): boolean => {
119
+ if (disabledServers.has(server.name)) return true;
120
+ if (server.enabled === false && !forcedEnabled.has(server.name)) return true;
121
+ return false;
122
+ };
123
+
124
+ const result = await loadCapability<MCPServer>(mcpCapability.id, {
125
+ cwd,
126
+ filter: includeServer,
127
+ suppress: suppressServer,
128
+ });
129
+
130
+ // Convert to legacy format and preserve source metadata.
116
131
  let configs: Record<string, MCPServerConfig> = {};
117
132
  let sources: Record<string, SourceMeta> = {};
118
- for (const server of servers) {
119
- const config = convertToLegacyConfig(server);
120
- if (disabledServers.has(server.name)) continue;
121
- if (config.enabled === false && !forcedEnabled.has(server.name)) continue;
122
- configs[server.name] = config;
133
+ for (const server of result.items) {
134
+ configs[server.name] = convertToLegacyConfig(server);
123
135
  sources[server.name] = server._source;
124
136
  }
125
137
 
@@ -1041,13 +1041,7 @@ export class MCPManager {
1041
1041
  async #loadServerResourcesAndPrompts(name: string, connection: MCPServerConnection): Promise<void> {
1042
1042
  if (serverSupportsResources(connection.capabilities)) {
1043
1043
  try {
1044
- const [resources] = await Promise.all([listResources(connection), listResourceTemplates(connection)]);
1045
-
1046
- if (this.#notificationsEnabled && connection.capabilities.resources?.subscribe) {
1047
- const uris = resources.map(r => r.uri);
1048
- const notificationEpoch = this.#notificationsEpoch;
1049
- this.#subscribeAndTrack(name, connection, uris, notificationEpoch);
1050
- }
1044
+ await this.refreshServerResources(name);
1051
1045
  } catch (error) {
1052
1046
  logger.debug("Failed to load MCP resources", { path: `mcp:${name}`, error });
1053
1047
  }
@@ -1107,8 +1101,20 @@ export class MCPManager {
1107
1101
  connection.resources = undefined;
1108
1102
  connection.resourceTemplates = undefined;
1109
1103
 
1110
- // Reload
1111
- const [resources] = await Promise.all([listResources(connection), listResourceTemplates(connection)]);
1104
+ // Reload. Template listing failures must not discard a successful
1105
+ // resources/list let both settle, then continue without templates.
1106
+ const [resourcesResult, templatesResult] = await Promise.allSettled([
1107
+ listResources(connection),
1108
+ listResourceTemplates(connection),
1109
+ ]);
1110
+ if (templatesResult.status === "rejected") {
1111
+ logger.debug("Failed to list MCP resource templates", {
1112
+ path: `mcp:${name}`,
1113
+ error: templatesResult.reason,
1114
+ });
1115
+ }
1116
+ if (resourcesResult.status === "rejected") throw resourcesResult.reason;
1117
+ const resources = resourcesResult.value;
1112
1118
  if (this.#notificationsEnabled && connection.capabilities.resources?.subscribe) {
1113
1119
  const newUris = new Set(resources.map(r => r.uri));
1114
1120
  const oldUris = this.#subscribedResources.get(name);
@@ -1161,6 +1167,17 @@ export class MCPManager {
1161
1167
  return promise;
1162
1168
  }
1163
1169
 
1170
+ /**
1171
+ * Wait until a connected server's resource catalog has been loaded.
1172
+ * Coalesces with initial loading and notification-driven refreshes.
1173
+ */
1174
+ async ensureServerResources(name: string): Promise<void> {
1175
+ const connection = this.#connections.get(name);
1176
+ if (!connection || !serverSupportsResources(connection.capabilities)) return;
1177
+ if (connection.resources !== undefined && connection.resourceTemplates !== undefined) return;
1178
+ await this.refreshServerResources(name);
1179
+ }
1180
+
1164
1181
  /**
1165
1182
  * Refresh prompts from a specific server.
1166
1183
  */
@@ -6,7 +6,7 @@
6
6
  import type { AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
7
7
  import type { TSchema } from "@oh-my-pi/pi-ai";
8
8
  import { normalizeSchemaForMCP } from "@oh-my-pi/pi-ai/utils/schema";
9
- import { untilAborted } from "@oh-my-pi/pi-utils";
9
+ import { logger, untilAborted } from "@oh-my-pi/pi-utils";
10
10
  import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
11
11
  import type { SourceMeta } from "../capability/types";
12
12
  import type {
@@ -357,6 +357,57 @@ export function createMCPToolName(serverName: string, toolName: string): string
357
357
  return `mcp__${sanitizedServerName}_${normalizedToolName}`;
358
358
  }
359
359
 
360
+ /**
361
+ * Keeps one MCP tool per minted name and logs collisions between distinct MCP
362
+ * origins. The winner is chosen by a stable origin key (server name + original
363
+ * tool name), NOT array order: MCPManager re-appends a reconnecting server's
364
+ * tools, so insertion order is mutable across reconnects and first-wins would
365
+ * silently flip ownership of the minted name. Non-MCP tools pass through
366
+ * unchanged.
367
+ */
368
+ export function deduplicateMCPToolsByName<T extends { name: string; mcpServerName?: unknown; mcpToolName?: unknown }>(
369
+ tools: readonly T[],
370
+ ): T[] {
371
+ const deduplicated: T[] = [];
372
+ const registered = new Map<string, { tool: T; originKey: string; index: number }>();
373
+
374
+ for (const tool of tools) {
375
+ if (typeof tool.mcpServerName !== "string" || typeof tool.mcpToolName !== "string") {
376
+ deduplicated.push(tool);
377
+ continue;
378
+ }
379
+
380
+ const originKey = `${tool.mcpServerName}\u0000${tool.mcpToolName}`;
381
+ const existing = registered.get(tool.name);
382
+ if (!existing) {
383
+ registered.set(tool.name, { tool, originKey, index: deduplicated.length });
384
+ deduplicated.push(tool);
385
+ continue;
386
+ }
387
+
388
+ if (existing.originKey === originKey) continue;
389
+
390
+ // Deterministic winner regardless of encounter order across reconnects.
391
+ const keepExisting = existing.originKey < originKey;
392
+ const winner = keepExisting ? existing.tool : tool;
393
+ const loser = keepExisting ? tool : existing.tool;
394
+ if (!keepExisting) {
395
+ deduplicated[existing.index] = tool;
396
+ existing.tool = tool;
397
+ existing.originKey = originKey;
398
+ }
399
+ logger.warn("MCP tool name collision; keeping stable winner", {
400
+ name: tool.name,
401
+ keptServer: winner.mcpServerName,
402
+ keptTool: winner.mcpToolName,
403
+ ignoredServer: loser.mcpServerName,
404
+ ignoredTool: loser.mcpToolName,
405
+ });
406
+ }
407
+
408
+ return deduplicated;
409
+ }
410
+
360
411
  /**
361
412
  * Parse an MCP tool name back to server and tool components.
362
413
  *
@@ -1361,12 +1361,31 @@ async function appendLearnedLine(filePath: string, line: string): Promise<void>
1361
1361
  } catch (err) {
1362
1362
  if (!isEnoent(err)) throw err;
1363
1363
  }
1364
- const prior = existing
1365
- .split("\n")
1366
- .map(l => l.trim())
1367
- .filter(l => l.startsWith("- ") && l !== line);
1368
- const lessons = [line, ...prior].slice(0, MAX_LEARNED_LESSONS);
1369
- await Bun.write(filePath, `${lessons.join("\n")}\n`);
1364
+ // Treat the file as an ordered line list so headings, prose, and blank
1365
+ // lines keep their positions relative to the bullets they scope. Managed
1366
+ // operations touch only bullet lines: dedupe removes an existing copy of
1367
+ // the incoming lesson in place, the new lesson enters at the head of the
1368
+ // first bullet run (newest-first, matching the read path and cap docs),
1369
+ // and the cap drops the oldest (bottom-most) bullets. Hand-edited content
1370
+ // outside the list region survives every write byte-for-byte.
1371
+ const lines = existing.split("\n");
1372
+ // A well-formed file ends with "\n"; drop the terminal split artifact so
1373
+ // repeated saves stay idempotent instead of growing a blank line each time.
1374
+ if (lines.at(-1) === "") lines.pop();
1375
+ const isLesson = (l: string) => l.trimStart().startsWith("- ");
1376
+ const out = lines.filter(l => !(isLesson(l) && l.trim() === line));
1377
+ const firstBullet = out.findIndex(isLesson);
1378
+ if (firstBullet === -1) out.push(line);
1379
+ else out.splice(firstBullet, 0, line);
1380
+ let lessonCount = 0;
1381
+ for (const l of out) if (isLesson(l)) lessonCount++;
1382
+ for (let i = out.length - 1; i >= 0 && lessonCount > MAX_LEARNED_LESSONS; i--) {
1383
+ if (isLesson(out[i])) {
1384
+ out.splice(i, 1);
1385
+ lessonCount--;
1386
+ }
1387
+ }
1388
+ await Bun.write(filePath, `${out.join("\n")}\n`);
1370
1389
  }
1371
1390
 
1372
1391
  /**
@@ -433,11 +433,12 @@ const costSegment: StatusLineSegment = {
433
433
  id: "cost",
434
434
  render(ctx) {
435
435
  const { cost, premiumRequests } = ctx.usageStats;
436
+ const advisorCost = ctx.session.getAdvisorCost?.() ?? 0;
436
437
  const normalizedPremiumRequests = normalizePremiumRequests(premiumRequests);
437
438
  const state = ctx.session.state;
438
439
  const usingSubscription = state.model ? ctx.session.modelRegistry.isUsingOAuth(state.model) : false;
439
440
 
440
- if (!cost && !usingSubscription && !normalizedPremiumRequests) {
441
+ if (!cost && !advisorCost && !usingSubscription && !normalizedPremiumRequests) {
441
442
  return { content: "", visible: false };
442
443
  }
443
444
 
@@ -445,6 +446,7 @@ const costSegment: StatusLineSegment = {
445
446
  if (cost) billingParts.push(`$${cost.toFixed(2)}`);
446
447
  if (normalizedPremiumRequests) billingParts.push(`★ ${formatNumber(normalizedPremiumRequests)}`);
447
448
  if (usingSubscription) billingParts.push("(sub)");
449
+ if (advisorCost) billingParts.push(`${billingParts.length ? "+ " : ""}$${advisorCost.toFixed(2)} (adv)`);
448
450
 
449
451
  return { content: theme.fg("statusLineCost", billingParts.join(" ")), visible: true };
450
452
  },
@@ -346,7 +346,7 @@ export class CommandController {
346
346
  }
347
347
  }
348
348
 
349
- this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
349
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
350
350
  }
351
351
 
352
352
  static readonly #advisorStatusGlyph: Record<string, string> = {
@@ -368,7 +368,7 @@ export class CommandController {
368
368
  async handleAdvisorStatusCommand(): Promise<void> {
369
369
  const stats = this.ctx.session.getAdvisorStats();
370
370
  if (!stats.configured) {
371
- this.ctx.present([new Spacer(1), new Text("Advisor is disabled.", 1, 0)]);
371
+ this.ctx.presentCommandOutput([new Spacer(1), new Text("Advisor is disabled.", 1, 0)]);
372
372
  return;
373
373
  }
374
374
  // Fetch live quota data (cached 5 min by the auth-gateway) so we can show
@@ -434,7 +434,7 @@ export class CommandController {
434
434
  info += `${theme.fg("dim", "Tokens:")} ${stats.tokens.total.toLocaleString()}\n`;
435
435
  if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
436
436
  }
437
- this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
437
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
438
438
  return;
439
439
  }
440
440
  // Single active advisor — detailed view.
@@ -480,7 +480,7 @@ export class CommandController {
480
480
  info += `${theme.fg("dim", "Cache Read:")} ${stats.tokens.cacheRead.toLocaleString()}\n`;
481
481
  }
482
482
  if (stats.cost > 0) info += `${theme.fg("dim", "Cost:")} $${stats.cost.toFixed(4)}\n`;
483
- this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
483
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
484
484
  }
485
485
 
486
486
  async handleJobsCommand(): Promise<void> {
@@ -497,7 +497,7 @@ export class CommandController {
497
497
 
498
498
  if (snapshot.running.length === 0 && snapshot.recent.length === 0) {
499
499
  info += `\n${theme.fg("dim", "No async jobs yet.")}\n`;
500
- this.ctx.present([new Spacer(1), new Text(info, 1, 0)]);
500
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(info, 1, 0)]);
501
501
  return;
502
502
  }
503
503
 
@@ -517,7 +517,7 @@ export class CommandController {
517
517
  }
518
518
  }
519
519
 
520
- this.ctx.present([new Spacer(1), new Text(info.trimEnd(), 1, 0)]);
520
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(info.trimEnd(), 1, 0)]);
521
521
  }
522
522
 
523
523
  async handleUsageCommand(reports?: UsageReport[] | null): Promise<void> {
@@ -558,7 +558,7 @@ export class CommandController {
558
558
  provider => (provider === currentProvider ? activeAccount : undefined),
559
559
  usageModelSelectors,
560
560
  );
561
- this.ctx.present([new Spacer(1), new Text(output, 1, 0)]);
561
+ this.ctx.presentCommandOutput([new Spacer(1), new Text(output, 1, 0)]);
562
562
  }
563
563
 
564
564
  async handleChangelogCommand(showFull = false): Promise<void> {
@@ -578,7 +578,7 @@ export class CommandController {
578
578
  block.addChild(new Spacer(1));
579
579
  block.addChild(new Markdown(changelogMarkdown + hint, 1, 1, getMarkdownTheme()));
580
580
  block.addChild(new DynamicBorder());
581
- this.ctx.present(block);
581
+ this.ctx.presentCommandOutput(block);
582
582
  }
583
583
 
584
584
  handleHotkeysCommand(): void {
@@ -607,7 +607,7 @@ export class CommandController {
607
607
  block.addChild(new Spacer(1));
608
608
  block.addChild(new Text(output, 1, 0));
609
609
  block.addChild(new DynamicBorder());
610
- this.ctx.present(block);
610
+ this.ctx.presentCommandOutput(block);
611
611
  }
612
612
 
613
613
  async handleMemoryCommand(text: string): Promise<void> {
@@ -628,7 +628,7 @@ export class CommandController {
628
628
  block.addChild(new Spacer(1));
629
629
  block.addChild(new Markdown(payload, 1, 1, getMarkdownTheme()));
630
630
  block.addChild(new DynamicBorder());
631
- this.ctx.present(block);
631
+ this.ctx.presentCommandOutput(block);
632
632
  return;
633
633
  }
634
634
 
@@ -86,7 +86,7 @@ export class ExtensionUiController {
86
86
  const uiContext: ExtensionUIContext = {
87
87
  timeoutStartsOnPresentation: true,
88
88
  select: (title, options, dialogOptions) => this.showCollabAwareSelector(title, options, dialogOptions),
89
- confirm: (title, message, _dialogOptions) => this.showHookConfirm(title, message),
89
+ confirm: (title, message, dialogOptions) => this.showHookConfirm(title, message, dialogOptions),
90
90
  input: (title, placeholder, dialogOptions) => this.showHookInput(title, placeholder, dialogOptions),
91
91
  askDialog: (questions, dialogOptions) => this.showAskDialog(questions, dialogOptions),
92
92
  notify: (message, type) => this.showHookNotify(message, type),
@@ -941,8 +941,8 @@ export class ExtensionUiController {
941
941
  /**
942
942
  * Show a confirmation dialog for hooks.
943
943
  */
944
- async showHookConfirm(title: string, message: string): Promise<boolean> {
945
- const result = await this.showHookSelector(`${title}\n${message}`, ["Yes", "No"]);
944
+ async showHookConfirm(title: string, message: string, dialogOptions?: ExtensionUIDialogOptions): Promise<boolean> {
945
+ const result = await this.showHookSelector(`${title}\n${message}`, ["Yes", "No"], dialogOptions);
946
946
  return result === "Yes";
947
947
  }
948
948
 
@@ -603,6 +603,57 @@ export function requestRpcEditor(
603
603
  } as RpcExtensionUIRequest);
604
604
  return promise;
605
605
  }
606
+
607
+ /** Sends an RPC extension dialog and cancels the remote presentation when its signal aborts. */
608
+ export function requestRpcDialog<T>(
609
+ pendingRequests: Map<string, PendingExtensionRequest>,
610
+ output: RpcOutput,
611
+ opts: ExtensionUIDialogOptions | undefined,
612
+ defaultValue: T,
613
+ request: Record<string, unknown>,
614
+ parseResponse: (response: RpcExtensionUIResponse) => T,
615
+ ): Promise<T> {
616
+ if (opts?.signal?.aborted) return Promise.resolve(defaultValue);
617
+
618
+ const id = Snowflake.next() as string;
619
+ const { promise, resolve, reject } = Promise.withResolvers<T>();
620
+ let timeoutId: NodeJS.Timeout | undefined;
621
+
622
+ const cleanup = () => {
623
+ clearTimeout(timeoutId);
624
+ opts?.signal?.removeEventListener("abort", onAbort);
625
+ pendingRequests.delete(id);
626
+ };
627
+ const onAbort = () => {
628
+ output({
629
+ type: "extension_ui_request",
630
+ id: Snowflake.next() as string,
631
+ method: "cancel",
632
+ targetId: id,
633
+ } as RpcExtensionUIRequest);
634
+ cleanup();
635
+ resolve(defaultValue);
636
+ };
637
+ opts?.signal?.addEventListener("abort", onAbort, { once: true });
638
+
639
+ if (opts?.timeout !== undefined) {
640
+ timeoutId = setTimeout(() => {
641
+ opts.onTimeout?.();
642
+ cleanup();
643
+ resolve(defaultValue);
644
+ }, opts.timeout);
645
+ }
646
+
647
+ pendingRequests.set(id, {
648
+ resolve: response => {
649
+ cleanup();
650
+ resolve(parseResponse(response));
651
+ },
652
+ reject,
653
+ });
654
+ output({ type: "extension_ui_request", id, ...request } as RpcExtensionUIRequest);
655
+ return promise;
656
+ }
606
657
  /**
607
658
  * Run in RPC mode.
608
659
  * Listens for JSON commands on stdin, outputs events and responses on stdout.
@@ -685,56 +736,14 @@ export async function runRpcMode(
685
736
  private output: (obj: RpcResponse | RpcExtensionUIRequest | object) => void,
686
737
  ) {}
687
738
 
688
- /** Helper for dialog methods with signal/timeout support */
689
- #createDialogPromise<T>(
690
- opts: ExtensionUIDialogOptions | undefined,
691
- defaultValue: T,
692
- request: Record<string, unknown>,
693
- parseResponse: (response: RpcExtensionUIResponse) => T,
694
- ): Promise<T> {
695
- if (opts?.signal?.aborted) return Promise.resolve(defaultValue);
696
-
697
- const id = Snowflake.next() as string;
698
- const { promise, resolve, reject } = Promise.withResolvers<T>();
699
- let timeoutId: NodeJS.Timeout | undefined;
700
-
701
- const cleanup = () => {
702
- if (timeoutId) clearTimeout(timeoutId);
703
- opts?.signal?.removeEventListener("abort", onAbort);
704
- this.pendingRequests.delete(id);
705
- };
706
-
707
- const onAbort = () => {
708
- cleanup();
709
- resolve(defaultValue);
710
- };
711
- opts?.signal?.addEventListener("abort", onAbort, { once: true });
712
-
713
- if (opts?.timeout !== undefined) {
714
- timeoutId = setTimeout(() => {
715
- opts.onTimeout?.();
716
- cleanup();
717
- resolve(defaultValue);
718
- }, opts.timeout);
719
- }
720
-
721
- this.pendingRequests.set(id, {
722
- resolve: (response: RpcExtensionUIResponse) => {
723
- cleanup();
724
- resolve(parseResponse(response));
725
- },
726
- reject,
727
- });
728
- this.output({ type: "extension_ui_request", id, ...request } as RpcExtensionUIRequest);
729
- return promise;
730
- }
731
-
732
739
  select(
733
740
  title: string,
734
741
  options: ExtensionUISelectItem[],
735
742
  dialogOptions?: ExtensionUIDialogOptions,
736
743
  ): Promise<string | undefined> {
737
- return this.#createDialogPromise(
744
+ return requestRpcDialog(
745
+ this.pendingRequests,
746
+ this.output,
738
747
  dialogOptions,
739
748
  undefined,
740
749
  {
@@ -748,7 +757,9 @@ export async function runRpcMode(
748
757
  }
749
758
 
750
759
  confirm(title: string, message: string, dialogOptions?: ExtensionUIDialogOptions): Promise<boolean> {
751
- return this.#createDialogPromise(
760
+ return requestRpcDialog(
761
+ this.pendingRequests,
762
+ this.output,
752
763
  dialogOptions,
753
764
  false,
754
765
  { method: "confirm", title, message, timeout: dialogOptions?.timeout },
@@ -768,7 +779,9 @@ export async function runRpcMode(
768
779
  placeholder?: string,
769
780
  dialogOptions?: ExtensionUIDialogOptions,
770
781
  ): Promise<string | undefined> {
771
- return this.#createDialogPromise(
782
+ return requestRpcDialog(
783
+ this.pendingRequests,
784
+ this.output,
772
785
  dialogOptions,
773
786
  undefined,
774
787
  { method: "input", title, placeholder, timeout: dialogOptions?.timeout },
@@ -1,9 +1,6 @@
1
- <user_interjection>
1
+ <system-notice>
2
2
  The user sent this message as an interjection while you were working. It takes
3
3
  priority and supersedes earlier instructions wherever they conflict — re-read it
4
4
  and make sure your current work reflects their intent.
5
-
6
- <message>
5
+ </system-notice>
7
6
  {{message}}
8
- </message>
9
- </user_interjection>
@@ -25,7 +25,8 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
25
25
  - `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
26
26
  - `agent`: The agent type running this item (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
27
27
  - `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
28
- - `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
28
+ {{#if effortEnabled}} - `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
29
+ {{/if}}
29
30
  - `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
30
31
  - `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
31
32
  {{#if isolationEnabled}}
@@ -39,7 +40,8 @@ Agents marked BLOCKING run inline — results return in this call; non-blocking
39
40
  - `name`: A stable CamelCase identifier (≤32 chars), used to address the agent (IRC, job ids). Generated automatically if omitted.
40
41
  - `agent`: The agent type to spawn (e.g. `scout`, `reviewer`). Omitting it gives you the general-purpose worker (`{{defaultAgent}}`) — NEVER pass that name explicitly. Only omit it after checking the agent list below and finding no specialist that fits.{{#if allowedAgentsText}} Current spawn policy allows: {{allowedAgentsText}}.{{/if}}
41
42
  - `task`: Complete, self-contained instructions. One-liners or missing acceptance criteria are PROHIBITED.
42
- - `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
43
+ {{#if effortEnabled}}- `effort`: Scale w/ complexity of this task: `"lo"`|`"med"`|`"hi"`
44
+ {{/if}}
43
45
  - `outputSchema`: Invocation-specific JSON Schema. Overrides the selected agent and parent-session schemas.
44
46
  - `schemaMode`: `"permissive"` (default) accepts a retry-exhausted invalid result with a warning; `"strict"` fails it.
45
47
  {{#if isolationEnabled}}
package/src/sdk.ts CHANGED
@@ -12,6 +12,7 @@ import {
12
12
  import type {
13
13
  Context,
14
14
  CredentialDisabledEvent,
15
+ Effort,
15
16
  Message,
16
17
  Model,
17
18
  ModelUsageHealth,
@@ -98,6 +99,7 @@ import type { HindsightSessionState } from "./hindsight/state";
98
99
  import { LocalProtocolHandler, type LocalProtocolOptions } from "./internal-urls";
99
100
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
100
101
  import {
102
+ deduplicateMCPToolsByName,
101
103
  discoverAndLoadMCPTools,
102
104
  type MCPLoadResult,
103
105
  MCPManager,
@@ -357,6 +359,8 @@ export interface CreateAgentSessionOptions {
357
359
  modelPatternDefaultFallbackChain?: string[];
358
360
  /** Thinking selector. Default: from settings, else unset */
359
361
  thinkingLevel?: ConfiguredThinkingLevel;
362
+ /** Hard ceiling on the session's thinking effort (e.g. a task spawn's `task.maxEffort`-capped hint); retry-fallback recovery re-clamps to it. */
363
+ thinkingLevelCeiling?: Effort;
360
364
  /** Models available for cycling (Ctrl+P in interactive mode) */
361
365
  scopedModels?: Array<{ model: Model; thinkingLevel?: ThinkingLevel }>;
362
366
  /** Prewalk from the starting model to a fast/cheap target at the first edit/write once the todo list exists. */
@@ -924,13 +928,14 @@ export function customToolToDefinition(tool: CustomTool): ToolDefinition {
924
928
  }
925
929
 
926
930
  function createCustomToolsExtension(tools: CustomTool[]): ExtensionFactory {
931
+ const uniqueTools = deduplicateMCPToolsByName(tools);
927
932
  return api => {
928
- for (const tool of tools) {
933
+ for (const tool of uniqueTools) {
929
934
  api.registerTool(customToolToDefinition(tool));
930
935
  }
931
936
 
932
937
  const runOnSession = async (event: CustomToolSessionEvent, ctx: ExtensionContext) => {
933
- for (const tool of tools) {
938
+ for (const tool of uniqueTools) {
934
939
  if (!tool.onSession) continue;
935
940
  try {
936
941
  await tool.onSession(event, createCustomToolContext(ctx));
@@ -1985,10 +1990,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1985
1990
  // Process provider registrations queued during extension loading.
1986
1991
  // This must happen before the runner is created so that models registered by
1987
1992
  // extensions are available for model selection on session resume / fallback.
1988
- const activeExtensionSources = extensionsResult.extensions.map(extension => extension.path);
1989
- modelRegistry.syncExtensionSources(activeExtensionSources);
1990
- for (const sourceId of new Set(activeExtensionSources)) {
1991
- modelRegistry.clearSourceRegistrations(sourceId);
1993
+ if (!restrictToolNames) {
1994
+ const activeExtensionSources = extensionsResult.extensions.map(extension => extension.path);
1995
+ modelRegistry.syncExtensionSources(activeExtensionSources);
1996
+ for (const sourceId of new Set(activeExtensionSources)) {
1997
+ modelRegistry.clearSourceRegistrations(sourceId);
1998
+ }
1992
1999
  }
1993
2000
  if (extensionsResult.runtime.pendingProviderRegistrations.length > 0) {
1994
2001
  for (const { name, config, sourceId } of extensionsResult.runtime.pendingProviderRegistrations) {
@@ -2537,8 +2544,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
2537
2544
  // Built-in tools get it in `createTools`; extension, SDK-custom, image-gen,
2538
2545
  // TTS, and startup (non-deferred) MCP tools all funnel through here, so apply
2539
2546
  // it once at this adapter boundary (idempotent — a no-op if already wrapped).
2540
- const wrappedExtensionTools: Tool[] = wrapRegisteredTools(allCustomTools, extensionRunner).map(
2541
- wrapToolWithMetaNotice,
2547
+ const wrappedExtensionTools: Tool[] = deduplicateMCPToolsByName(
2548
+ wrapRegisteredTools(allCustomTools, extensionRunner).map(wrapToolWithMetaNotice),
2542
2549
  );
2543
2550
 
2544
2551
  // All built-in tools are active (conditional tools like git/ask return null from factory if disabled)
@@ -3056,6 +3063,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3056
3063
  dialect: resolveDialect(settings.get("tools.format"), model),
3057
3064
  abortOnFabricatedToolResult: settings.get("tools.abortOnFabricatedResult"),
3058
3065
  getToolChoice: () => session?.nextToolChoiceDirective(),
3066
+ onToolChoiceUnavailable: () => session?.toolChoiceQueue.reject("unavailable"),
3059
3067
  telemetry: options.telemetry,
3060
3068
  appendOnlyContext: model
3061
3069
  ? shouldEnableAppendOnlyContext(settings.get("provider.appendOnlyContext"), model)
@@ -3131,6 +3139,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
3131
3139
  agent,
3132
3140
  pruneToolDescriptions: inlineToolDescriptors,
3133
3141
  thinkingLevel: autoThinking ? AUTO_THINKING : effectiveThinkingLevel,
3142
+ thinkingLevelCeiling: options.thinkingLevelCeiling,
3134
3143
  initialRetryFallback,
3135
3144
  prewalk: options.prewalk,
3136
3145
  planYolo: options.planYolo,