@phuetz/code-buddy 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (291) hide show
  1. package/README.md +160 -165
  2. package/dist/agent/autonomous/agentic-coding-contract.d.ts +6 -6
  3. package/dist/agent/codebuddy-agent.d.ts +1 -1
  4. package/dist/agent/codebuddy-agent.js +69 -14
  5. package/dist/agent/execution/agent-executor.js +122 -12
  6. package/dist/agent/execution/tool-dependency-graph.js +7 -0
  7. package/dist/agent/execution/tool-hooks.d.ts +6 -0
  8. package/dist/agent/execution/tool-hooks.js +13 -5
  9. package/dist/agent/execution/tool-selection-strategy.d.ts +11 -2
  10. package/dist/agent/execution/tool-selection-strategy.js +67 -10
  11. package/dist/agent/extended-thinking.d.ts +8 -0
  12. package/dist/agent/extended-thinking.js +27 -0
  13. package/dist/agent/hermes-browser-backends.js +151 -11
  14. package/dist/agent/hermes-claw-migrate.d.ts +59 -0
  15. package/dist/agent/hermes-claw-migrate.js +427 -26
  16. package/dist/agent/hermes-parity-manifest.js +78 -71
  17. package/dist/agent/hermes-runtime-lifecycle.d.ts +67 -0
  18. package/dist/agent/hermes-runtime-lifecycle.js +468 -0
  19. package/dist/agent/middleware/index.d.ts +1 -0
  20. package/dist/agent/middleware/index.js +1 -0
  21. package/dist/agent/middleware/visual-validation-middleware.d.ts +17 -0
  22. package/dist/agent/middleware/visual-validation-middleware.js +67 -0
  23. package/dist/agent/model-benchmark.d.ts +77 -0
  24. package/dist/agent/model-benchmark.js +309 -0
  25. package/dist/agent/model-tier.d.ts +14 -0
  26. package/dist/agent/model-tier.js +71 -0
  27. package/dist/agent/multi-agent/agents/orchestrator-agent.js +17 -6
  28. package/dist/agent/multi-agent/multi-agent-system.js +3 -1
  29. package/dist/agent/multi-agent/session-fleet-bridge.d.ts +2 -0
  30. package/dist/agent/multi-agent/session-fleet-bridge.js +9 -1
  31. package/dist/agent/multi-agent/session-tools.js +22 -5
  32. package/dist/agent/reasoning/mcts.js +20 -6
  33. package/dist/agent/session-end-flush.d.ts +6 -1
  34. package/dist/agent/session-end-flush.js +20 -4
  35. package/dist/agent/streaming/streaming-handler.d.ts +7 -0
  36. package/dist/agent/streaming/streaming-handler.js +8 -1
  37. package/dist/agent/tool-executor.js +1 -0
  38. package/dist/agent/tool-handler.d.ts +5 -0
  39. package/dist/agent/tool-handler.js +130 -4
  40. package/dist/browser-automation/browser-operator-executor.js +15 -0
  41. package/dist/browser-automation/browser-use-runner.d.ts +96 -0
  42. package/dist/browser-automation/browser-use-runner.js +492 -0
  43. package/dist/browser-automation/camofox-runner.d.ts +107 -0
  44. package/dist/browser-automation/camofox-runner.js +287 -0
  45. package/dist/channels/dingtalk/index.js +3 -0
  46. package/dist/channels/discord/client.d.ts +9 -0
  47. package/dist/channels/discord/client.js +12 -0
  48. package/dist/channels/feishu/index.d.ts +167 -1
  49. package/dist/channels/feishu/index.js +432 -7
  50. package/dist/channels/gateway-lifecycle.d.ts +132 -0
  51. package/dist/channels/gateway-lifecycle.js +219 -0
  52. package/dist/channels/google-chat/index.d.ts +5 -1
  53. package/dist/channels/google-chat/index.js +5 -1
  54. package/dist/channels/imessage/index.d.ts +14 -0
  55. package/dist/channels/imessage/index.js +64 -17
  56. package/dist/channels/index.d.ts +4 -0
  57. package/dist/channels/index.js +4 -0
  58. package/dist/channels/irc/index.d.ts +73 -3
  59. package/dist/channels/irc/index.js +446 -11
  60. package/dist/channels/line/index.js +3 -0
  61. package/dist/channels/mattermost/index.d.ts +51 -4
  62. package/dist/channels/mattermost/index.js +303 -20
  63. package/dist/channels/nextcloud-talk/index.d.ts +89 -8
  64. package/dist/channels/nextcloud-talk/index.js +367 -16
  65. package/dist/channels/nostr/index.d.ts +121 -1
  66. package/dist/channels/nostr/index.js +396 -8
  67. package/dist/channels/ntfy/index.js +3 -0
  68. package/dist/channels/qq/index.js +3 -0
  69. package/dist/channels/slash-parity.d.ts +117 -0
  70. package/dist/channels/slash-parity.js +185 -0
  71. package/dist/channels/synology-chat/index.js +3 -0
  72. package/dist/channels/teams/index.d.ts +3 -0
  73. package/dist/channels/teams/index.js +3 -0
  74. package/dist/channels/telegram/client.d.ts +9 -0
  75. package/dist/channels/telegram/client.js +12 -0
  76. package/dist/channels/twilio-voice/index.js +3 -0
  77. package/dist/channels/webchat/index.d.ts +7 -1
  78. package/dist/channels/webchat/index.js +7 -1
  79. package/dist/channels/wecom/index.js +3 -0
  80. package/dist/channels/weixin/index.js +3 -0
  81. package/dist/channels/zalo/index.js +3 -0
  82. package/dist/cli/config-loader.js +1 -1
  83. package/dist/codebuddy/client.d.ts +30 -1
  84. package/dist/codebuddy/client.js +167 -20
  85. package/dist/codebuddy/providers/provider-chatgpt-responses.d.ts +5 -0
  86. package/dist/codebuddy/providers/provider-chatgpt-responses.js +1 -0
  87. package/dist/codebuddy/providers/provider-gemini-native.js +1 -1
  88. package/dist/codebuddy/providers/provider-openai-compat.d.ts +5 -0
  89. package/dist/codebuddy/providers/provider-openai-compat.js +82 -2
  90. package/dist/codebuddy/tool-definitions/computer-control-tools.js +32 -2
  91. package/dist/codebuddy/tool-definitions/core-tools.js +3 -3
  92. package/dist/codebuddy/tool-definitions/index.d.ts +2 -1
  93. package/dist/codebuddy/tool-definitions/index.js +2 -1
  94. package/dist/codebuddy/tool-definitions/multimodal-tools.d.ts +1 -0
  95. package/dist/codebuddy/tool-definitions/multimodal-tools.js +44 -1
  96. package/dist/codebuddy/tool-definitions/windows-tools.d.ts +2 -0
  97. package/dist/codebuddy/tool-definitions/windows-tools.js +34 -0
  98. package/dist/codebuddy/tools.d.ts +1 -1
  99. package/dist/codebuddy/tools.js +27 -24
  100. package/dist/commands/cli/hermes-commands.js +108 -0
  101. package/dist/commands/cli/native-engine-commands.js +102 -3
  102. package/dist/commands/cli/utility-commands.js +45 -0
  103. package/dist/commands/enhanced-command-handler.js +1 -1
  104. package/dist/commands/goal-cli.d.ts +30 -0
  105. package/dist/commands/goal-cli.js +210 -27
  106. package/dist/commands/handlers/goal-handler.d.ts +9 -2
  107. package/dist/commands/handlers/goal-handler.js +36 -9
  108. package/dist/commands/handlers/infra-handlers.js +36 -0
  109. package/dist/commands/handlers/memory-handlers.js +96 -1
  110. package/dist/commands/headless-slash.d.ts +2 -0
  111. package/dist/commands/headless-slash.js +12 -0
  112. package/dist/commands/llm-provider-resolution.d.ts +5 -3
  113. package/dist/commands/llm-provider-resolution.js +87 -33
  114. package/dist/commands/ollama.d.ts +25 -0
  115. package/dist/commands/ollama.js +100 -0
  116. package/dist/commands/provider.d.ts +5 -0
  117. package/dist/commands/provider.js +106 -55
  118. package/dist/commands/spec-next.js +2 -1
  119. package/dist/commands/spec-plan.js +5 -16
  120. package/dist/commands/tunnel.d.ts +2 -0
  121. package/dist/commands/tunnel.js +48 -0
  122. package/dist/config/config-resolver.d.ts +2 -1
  123. package/dist/config/config-resolver.js +54 -41
  124. package/dist/config/constants.d.ts +28 -0
  125. package/dist/config/constants.js +7 -0
  126. package/dist/config/env-schema.js +756 -0
  127. package/dist/config/model-tools.js +4 -4
  128. package/dist/config/toml-config.d.ts +16 -0
  129. package/dist/config/toml-config.js +3 -0
  130. package/dist/context/context-manager-v2.js +1 -0
  131. package/dist/daemon/autonomous-daemon.d.ts +1 -1
  132. package/dist/daemon/autonomous-daemon.js +3 -3
  133. package/dist/daemon/autonomous-loop.js +11 -2
  134. package/dist/daemon/colab-goal.js +9 -1
  135. package/dist/daemon/cron-agent-bridge.d.ts +12 -3
  136. package/dist/daemon/cron-agent-bridge.js +25 -9
  137. package/dist/desktop/codebuddy-engine-adapter.d.ts +32 -0
  138. package/dist/desktop/codebuddy-engine-adapter.js +257 -80
  139. package/dist/desktop/engine-adapter.d.ts +14 -0
  140. package/dist/desktop-automation/automation-manager.js +16 -0
  141. package/dist/desktop-automation/omniparser-runner.d.ts +63 -0
  142. package/dist/desktop-automation/omniparser-runner.js +115 -0
  143. package/dist/desktop-automation/smart-snapshot.d.ts +11 -0
  144. package/dist/desktop-automation/smart-snapshot.js +72 -5
  145. package/dist/doctor/index.js +40 -23
  146. package/dist/events/types.d.ts +11 -0
  147. package/dist/fleet/colab-store.js +14 -2
  148. package/dist/fleet/peer-session-bridge.js +50 -12
  149. package/dist/fleet/peer-tool-bridge.js +14 -0
  150. package/dist/goals/goal-decomposer.d.ts +27 -0
  151. package/dist/goals/goal-decomposer.js +293 -0
  152. package/dist/goals/goal-judge-client.d.ts +15 -0
  153. package/dist/goals/goal-judge-client.js +45 -0
  154. package/dist/goals/goal-judge.d.ts +2 -0
  155. package/dist/goals/goal-judge.js +35 -6
  156. package/dist/goals/goal-loop.d.ts +14 -0
  157. package/dist/goals/goal-loop.js +47 -2
  158. package/dist/goals/goal-manager.d.ts +6 -0
  159. package/dist/goals/goal-manager.js +67 -14
  160. package/dist/goals/goal-state.d.ts +13 -0
  161. package/dist/goals/goal-state.js +83 -13
  162. package/dist/goals/goal-store.d.ts +3 -0
  163. package/dist/goals/goal-store.js +31 -15
  164. package/dist/hooks/user-hooks.js +17 -3
  165. package/dist/index.js +464 -25
  166. package/dist/input/text-to-speech.d.ts +2 -6
  167. package/dist/input/text-to-speech.js +2 -27
  168. package/dist/integrations/screenpipe/screenpipe-client.d.ts +10 -0
  169. package/dist/integrations/screenpipe/screenpipe-client.js +12 -2
  170. package/dist/integrations/tailscale.d.ts +13 -0
  171. package/dist/integrations/tailscale.js +87 -34
  172. package/dist/knowledge/workspace-indexer.js +53 -9
  173. package/dist/mcp/client.js +1 -0
  174. package/dist/mcp/mcp-desktop-tools.d.ts +23 -0
  175. package/dist/mcp/mcp-desktop-tools.js +158 -0
  176. package/dist/mcp/mcp-server.js +2 -0
  177. package/dist/memory/enhanced-memory.d.ts +10 -0
  178. package/dist/memory/enhanced-memory.js +33 -2
  179. package/dist/memory/index.d.ts +3 -1
  180. package/dist/memory/index.js +4 -1
  181. package/dist/memory/memory-auto-proposer.d.ts +23 -0
  182. package/dist/memory/memory-auto-proposer.js +308 -0
  183. package/dist/memory/memory-candidate-queue.d.ts +92 -0
  184. package/dist/memory/memory-candidate-queue.js +261 -0
  185. package/dist/memory/persistent-memory.d.ts +45 -2
  186. package/dist/memory/persistent-memory.js +236 -40
  187. package/dist/ml/bayesian-qualifier.d.ts +1 -1
  188. package/dist/ml/bayesian-qualifier.js +35 -2
  189. package/dist/openclaw/gateway-bridge.js +5 -0
  190. package/dist/plugins/marketplace.d.ts +1 -0
  191. package/dist/plugins/marketplace.js +7 -0
  192. package/dist/prompts/execution-discipline.d.ts +14 -0
  193. package/dist/prompts/execution-discipline.js +29 -0
  194. package/dist/prompts/variation-injector.js +9 -2
  195. package/dist/protocols/acp/acp-agentic-runner.d.ts +7 -6
  196. package/dist/protocols/acp/acp-agentic-runner.js +115 -18
  197. package/dist/protocols/acp/acp-session-store.d.ts +23 -0
  198. package/dist/protocols/acp/acp-session-store.js +77 -0
  199. package/dist/protocols/acp/acp-stdio-server.d.ts +7 -0
  200. package/dist/protocols/acp/acp-stdio-server.js +71 -16
  201. package/dist/providers/active-llm-registry.d.ts +37 -0
  202. package/dist/providers/active-llm-registry.js +186 -0
  203. package/dist/providers/auxiliary-provider.d.ts +25 -0
  204. package/dist/providers/auxiliary-provider.js +192 -0
  205. package/dist/providers/codex-oauth.d.ts +1 -1
  206. package/dist/providers/codex-oauth.js +27 -4
  207. package/dist/providers/index.d.ts +3 -0
  208. package/dist/providers/index.js +4 -0
  209. package/dist/providers/provider-catalog.d.ts +70 -0
  210. package/dist/providers/provider-catalog.js +738 -0
  211. package/dist/providers/provider-fallback.d.ts +35 -0
  212. package/dist/providers/provider-fallback.js +171 -0
  213. package/dist/providers/xai-oauth.d.ts +128 -0
  214. package/dist/providers/xai-oauth.js +735 -0
  215. package/dist/scheduler/cron-scheduler.d.ts +15 -3
  216. package/dist/scheduler/cron-scheduler.js +53 -7
  217. package/dist/scheduler/script-runner.d.ts +2 -0
  218. package/dist/scheduler/script-runner.js +19 -4
  219. package/dist/scheduler/watchdog-handlers.js +7 -20
  220. package/dist/search/usearch-index.js +7 -2
  221. package/dist/security/tool-policy/tool-groups.js +2 -0
  222. package/dist/server/index.js +17 -2
  223. package/dist/server/routes/mobile.d.ts +12 -4
  224. package/dist/server/routes/mobile.js +116 -1
  225. package/dist/server/tls-config.d.ts +35 -0
  226. package/dist/server/tls-config.js +142 -0
  227. package/dist/server/tunnel-manager.d.ts +20 -0
  228. package/dist/server/tunnel-manager.js +58 -0
  229. package/dist/server/websocket/fleet-bridge.js +5 -0
  230. package/dist/services/prompt-builder.d.ts +1 -0
  231. package/dist/services/prompt-builder.js +49 -8
  232. package/dist/shared/engine-types.d.ts +15 -1
  233. package/dist/sidecar/sidecar-bridge.d.ts +1 -0
  234. package/dist/sidecar/sidecar-bridge.js +21 -10
  235. package/dist/spec/spec-store.js +8 -1
  236. package/dist/talk-mode/providers/audioreader-tts.d.ts +1 -1
  237. package/dist/talk-mode/providers/audioreader-tts.js +8 -8
  238. package/dist/templates/project-scaffolding.js +3 -2
  239. package/dist/themes/theme-schema.d.ts +10 -10
  240. package/dist/tools/application-profiles.js +38 -0
  241. package/dist/tools/bash/bash-tool.d.ts +1 -0
  242. package/dist/tools/bash/bash-tool.js +16 -2
  243. package/dist/tools/bash/command-validator.js +3 -0
  244. package/dist/tools/bash/rtk-rewrite.d.ts +16 -0
  245. package/dist/tools/bash/rtk-rewrite.js +100 -0
  246. package/dist/tools/bash/streaming-executor.js +14 -1
  247. package/dist/tools/computer-control-tool.d.ts +16 -1
  248. package/dist/tools/computer-control-tool.js +317 -5
  249. package/dist/tools/document-generator.d.ts +14 -0
  250. package/dist/tools/document-generator.js +79 -0
  251. package/dist/tools/execute-code-rpc-invoker.d.ts +26 -2
  252. package/dist/tools/execute-code-rpc-invoker.js +46 -5
  253. package/dist/tools/metadata.js +28 -0
  254. package/dist/tools/office-macro-tool.d.ts +10 -0
  255. package/dist/tools/office-macro-tool.js +93 -0
  256. package/dist/tools/registry/index.d.ts +2 -1
  257. package/dist/tools/registry/index.js +2 -1
  258. package/dist/tools/registry/memory-tools.d.ts +20 -0
  259. package/dist/tools/registry/memory-tools.js +225 -3
  260. package/dist/tools/registry/multimodal-tools.js +2 -2
  261. package/dist/tools/registry/vision-tools.d.ts +33 -0
  262. package/dist/tools/registry/vision-tools.js +308 -6
  263. package/dist/tools/registry/windows-tools.d.ts +2 -0
  264. package/dist/tools/registry/windows-tools.js +47 -0
  265. package/dist/tools/route-peer-tool.js +14 -0
  266. package/dist/tools/screenshot-tool.js +14 -2
  267. package/dist/tools/text-to-speech-tool.d.ts +1 -1
  268. package/dist/tools/text-to-speech-tool.js +2 -12
  269. package/dist/tools/tool-selector.js +3 -59
  270. package/dist/utils/config-validation/schema.d.ts +40 -7
  271. package/dist/utils/config-validation/schema.js +130 -1
  272. package/dist/utils/cost-tracker.js +21 -1
  273. package/dist/utils/disk-guard.d.ts +180 -0
  274. package/dist/utils/disk-guard.js +404 -0
  275. package/dist/utils/interactive-setup.js +35 -2
  276. package/dist/utils/model-utils.js +11 -2
  277. package/dist/utils/output-sanitizer.js +11 -0
  278. package/dist/utils/provider-detector.d.ts +8 -7
  279. package/dist/utils/provider-detector.js +19 -77
  280. package/dist/utils/settings-manager.d.ts +7 -0
  281. package/dist/utils/token-display.d.ts +3 -3
  282. package/dist/utils/token-display.js +22 -4
  283. package/dist/wizard/onboarding.d.ts +39 -0
  284. package/dist/wizard/onboarding.js +433 -21
  285. package/dist/wizard/provider-onboarding.d.ts +8 -23
  286. package/dist/wizard/provider-onboarding.js +39 -158
  287. package/package.json +4 -2
  288. package/dist/plugins/provider-onboarding.d.ts +0 -23
  289. package/dist/plugins/provider-onboarding.js +0 -116
  290. package/dist/utils/kokoro-tts.d.ts +0 -8
  291. package/dist/utils/kokoro-tts.js +0 -67
@@ -14,7 +14,7 @@ import { ToolHandler } from "./tool-handler.js";
14
14
  import { BaseAgent } from "./base-agent.js";
15
15
  import { createAgentInfrastructureSync } from "./infrastructure/index.js";
16
16
  import { getLaneQueue } from "../concurrency/lane-queue.js";
17
- import { findSkill, findStarterPack } from "../skills/index.js";
17
+ import { findSkill, findStarterPack, resetSkillRegistry } from "../skills/index.js";
18
18
  import { skillMdToUnified } from "../skills/adapters/index.js";
19
19
  import { getSkillsHub } from "../skills/hub.js";
20
20
  import { MessageQueue } from "./message-queue.js";
@@ -24,6 +24,12 @@ import { initializeMemory, getMemoryManager } from "../memory/persistent-memory.
24
24
  import { getUserHooksManager } from "../hooks/user-hooks.js";
25
25
  import { isFeatureEnabled } from "../config/feature-flags.js";
26
26
  import { getActiveRunStore } from "../observability/run-store.js";
27
+ import { resetIdentityManager } from "../identity/identity-manager.js";
28
+ import { resetHotReloadManager } from "../config/hot-reload/index.js";
29
+ import { resetConfigWatcher } from "../config/hot-reload/watcher.js";
30
+ import { resetPersonaManager } from "../personas/persona-manager.js";
31
+ import { resetEnhancedMemory } from "../memory/enhanced-memory.js";
32
+ import { resetPluginMarketplace } from "../plugins/marketplace.js";
27
33
  /**
28
34
  * Main agent class that orchestrates conversation with CodeBuddy AI and tool execution
29
35
  *
@@ -76,9 +82,10 @@ export class CodeBuddyAgent extends BaseAgent {
76
82
  * @param useRAGToolSelection - Enable RAG-based tool selection (default: true)
77
83
  */
78
84
  constructor(apiKey, baseURL, model, maxToolRounds, useRAGToolSelection = true, systemPromptId, // New: external prompt ID (default, minimal, secure, etc.)
79
- workingDirectory, systemPromptAppend) {
85
+ workingDirectory, systemPromptAppend, initialSystemPromptOverride) {
80
86
  super();
81
87
  this.systemPromptAppend = systemPromptAppend;
88
+ this.customSystemPromptOverride = initialSystemPromptOverride?.trim() || null;
82
89
  this.visionGroundingModel = process.env.CODEBUDDY_VISION_GROUNDING_MODEL || undefined;
83
90
  const initialWorkingDirectory = workingDirectory || process.cwd();
84
91
  // Determine model to use
@@ -334,6 +341,27 @@ export class CodeBuddyAgent extends BaseAgent {
334
341
  catch (err) {
335
342
  logger.debug('Failed to register AutoRepairMiddleware (non-critical)', { error: err instanceof Error ? err.message : String(err) });
336
343
  }
344
+ // Verification enforcement (priority 155) — after >= 3 file changes with
345
+ // no task_verify/run_tests, nudges the model to verify before finishing.
346
+ // Closes the "loop completes on no-more-tool-calls without a verify gate"
347
+ // gap surfaced by the Hermes agentic-loop audit (2026-06-16).
348
+ try {
349
+ const { createVerificationEnforcementMiddleware } = await import('./middleware/verification-enforcement.js');
350
+ pipeline.use(createVerificationEnforcementMiddleware());
351
+ logger.debug('VerificationEnforcementMiddleware registered in pipeline (priority 155)');
352
+ }
353
+ catch (err) {
354
+ logger.debug('Failed to register VerificationEnforcementMiddleware (non-critical)', { error: err instanceof Error ? err.message : String(err) });
355
+ }
356
+ // Visual validation middleware (priority 156) — suggests taking screenshots of generated Office documents
357
+ try {
358
+ const { VisualValidationMiddleware } = await import('./middleware/visual-validation-middleware.js');
359
+ pipeline.use(new VisualValidationMiddleware());
360
+ logger.debug('VisualValidationMiddleware registered in pipeline (priority 156)');
361
+ }
362
+ catch (err) {
363
+ logger.debug('Failed to register VisualValidationMiddleware (non-critical)', { error: err instanceof Error ? err.message : String(err) });
364
+ }
337
365
  // Quality gate middleware (priority 200) — auto-delegates to specialized agents
338
366
  try {
339
367
  const { createQualityGateMiddleware } = await import('./middleware/quality-gate-middleware.js');
@@ -492,18 +520,22 @@ Look at the screenshot and find the element matching the user's intent. Output o
492
520
  }
493
521
  async initializeAgentSystemPrompt(systemPromptId, modelName, customInstructions) {
494
522
  try {
495
- let systemPrompt = await this.promptBuilder.buildSystemPrompt(systemPromptId, modelName, customInstructions);
523
+ const hasInitialOverride = !!this.customSystemPromptOverride;
524
+ let systemPrompt = this.customSystemPromptOverride
525
+ ?? await this.promptBuilder.buildSystemPrompt(systemPromptId, modelName, customInstructions);
496
526
  // Inject repoProfile.contextPack if available
497
- try {
498
- const profiler = getRepoProfiler();
499
- const profile = await profiler.getProfile();
500
- if (profile.contextPack) {
501
- systemPrompt = `${systemPrompt}\n\n[Repo] ${profile.contextPack}`;
502
- logger.debug('RepoProfiler: injected contextPack into system prompt');
527
+ if (!hasInitialOverride) {
528
+ try {
529
+ const profiler = getRepoProfiler();
530
+ const profile = await profiler.getProfile();
531
+ if (profile.contextPack) {
532
+ systemPrompt = `${systemPrompt}\n\n[Repo] ${profile.contextPack}`;
533
+ logger.debug('RepoProfiler: injected contextPack into system prompt');
534
+ }
535
+ }
536
+ catch {
537
+ // Non-fatal — repo profiling is best-effort
503
538
  }
504
- }
505
- catch {
506
- // Non-fatal — repo profiling is best-effort
507
539
  }
508
540
  systemPrompt = this.applyRuntimeSystemPromptAppend(systemPrompt);
509
541
  // Knowledge base is already injected by PromptBuilder.buildSystemPrompt() — skip duplicate injection here.
@@ -1639,7 +1671,9 @@ Look at the screenshot and find the element matching the user's intent. Output o
1639
1671
  // Fire SessionEnd user hook (non-blocking). Shutdown errors should be
1640
1672
  // visible (warn, not debug) — silent debug logs masked prior bugs.
1641
1673
  getUserHooksManager(process.cwd()).executeHooks('SessionEnd', {}).catch((err) => logger.warn(`[user-hooks] SessionEnd error: ${err instanceof Error ? err.message : String(err)}`));
1642
- if (isFeatureEnabled('USER_MODEL_DIALECTIC_ON_SESSION_END')) {
1674
+ const skipAsyncSessionLearning = process.env.CODEBUDDY_HEADLESS === 'true' ||
1675
+ process.env.CODEBUDDY_HEADLESS === '1';
1676
+ if (!skipAsyncSessionLearning && isFeatureEnabled('USER_MODEL_DIALECTIC_ON_SESSION_END')) {
1643
1677
  const chatHistory = this.historyManager.getChatHistory();
1644
1678
  if (chatHistory && chatHistory.length > 0) {
1645
1679
  import('../memory/user-model.js')
@@ -1659,7 +1693,7 @@ Look at the screenshot and find the element matching the user's intent. Output o
1659
1693
  // WS3-T1 — session-end flush: handoff + review-gated lesson candidates.
1660
1694
  // Fire-and-forget like the dialectic above; callers that need the flush
1661
1695
  // to complete before process exit await runSessionEndFlush directly.
1662
- {
1696
+ if (!skipAsyncSessionLearning) {
1663
1697
  const chatHistory = this.historyManager.getChatHistory();
1664
1698
  if (chatHistory && chatHistory.length > 0) {
1665
1699
  import('./session-end-flush.js')
@@ -1696,6 +1730,27 @@ Look at the screenshot and find the element matching the user's intent. Output o
1696
1730
  this.contextManager.stopPeriodicSnapshot?.();
1697
1731
  this.peerRoutingConfig = null;
1698
1732
  super.dispose();
1733
+ if (skipAsyncSessionLearning) {
1734
+ cleanupHeadlessSingletonWatchers();
1735
+ }
1736
+ }
1737
+ }
1738
+ function cleanupHeadlessSingletonWatchers() {
1739
+ for (const cleanup of [
1740
+ resetSkillRegistry,
1741
+ resetIdentityManager,
1742
+ resetHotReloadManager,
1743
+ resetConfigWatcher,
1744
+ resetPersonaManager,
1745
+ resetEnhancedMemory,
1746
+ resetPluginMarketplace,
1747
+ ]) {
1748
+ try {
1749
+ cleanup();
1750
+ }
1751
+ catch (error) {
1752
+ logger.debug('Headless watcher cleanup skipped', { error: String(error) });
1753
+ }
1699
1754
  }
1700
1755
  }
1701
1756
  //# sourceMappingURL=codebuddy-agent.js.map
@@ -7,6 +7,7 @@
7
7
  *
8
8
  * @module agent/execution
9
9
  */
10
+ import { normalizeHallucinatedLocalToolCall } from "../tool-handler.js";
10
11
  import { logger } from "../../utils/logger.js";
11
12
  import { getErrorMessage } from "../../errors/index.js";
12
13
  import { sanitizeToolResult } from "../../utils/sanitize.js";
@@ -32,6 +33,22 @@ function withTimeout(promise, ms, fallback) {
32
33
  new Promise(resolve => setTimeout(() => resolve(fallback), ms)),
33
34
  ]);
34
35
  }
36
+ function isIgnorableControlToolCall(toolCall) {
37
+ const name = toolCall.function?.name?.trim();
38
+ if (!name)
39
+ return true;
40
+ // Some local models leak thinking/channel markers as tool names. Keep
41
+ // malformed-but-actionable aliases such as `thought-tool:execute_command`
42
+ // because ToolHandler normalizes those to real tools.
43
+ if (normalizeHallucinatedLocalToolCall(name, {}))
44
+ return false;
45
+ const lower = name.toLowerCase();
46
+ return (lower === 'thought' ||
47
+ lower === 'thought|' ||
48
+ lower.startsWith('thought|') ||
49
+ lower.includes('<|channel>') ||
50
+ lower.includes('<|tool_call>'));
51
+ }
35
52
  // Lazy-loaded workspace context to avoid blocking tests.
36
53
  // Includes a 3s hard timeout so git commands never stall the agent loop.
37
54
  let _getWorkspaceContext = null;
@@ -418,6 +435,21 @@ export class AgentExecutor {
418
435
  const maxToolRounds = this.config.maxToolRounds;
419
436
  let toolRounds = 0;
420
437
  let totalOutputTokens = 0;
438
+ // In-loop recovery budgets (Hermes parity): bound re-prompts WITHIN a turn
439
+ // so a length-truncated or post-tool-empty response is recovered instead of
440
+ // returned half-written. 0 disables. Per-turn counters.
441
+ const parseRecoveryBudget = (raw, dflt) => {
442
+ const n = Number.parseInt(raw ?? '', 10);
443
+ return Number.isFinite(n) && n >= 0 ? n : dflt;
444
+ };
445
+ const maxLengthContinuations = parseRecoveryBudget(process.env.CODEBUDDY_MAX_LENGTH_CONTINUATIONS, 3);
446
+ // Length-continuation ships ON (default 3, real-tested). The post-tool
447
+ // empty-response re-prompt is harder to trigger deterministically with a
448
+ // real model, so it ships OFF by default (no untested-by-default behaviour
449
+ // in the hot loop) — opt in with CODEBUDDY_MAX_EMPTY_RETRIES=N.
450
+ const maxEmptyRetries = parseRecoveryBudget(process.env.CODEBUDDY_MAX_EMPTY_RETRIES, 0);
451
+ let lengthContinuations = 0;
452
+ let emptyRetries = 0;
421
453
  // Phase (d).21 ship 4 — start a progress session for this turn.
422
454
  // The default sink (boot-wired) logs at 25/50/75/100. Lazy-import to
423
455
  // avoid circular load at module init time.
@@ -485,12 +517,14 @@ export class AgentExecutor {
485
517
  // shrink the tool set to ~5 with a minimal alwaysInclude — we
486
518
  // don't want to dangle `remember`/`lessons_*` in front of a model
487
519
  // that can't actually call tools and would inline-hallucinate them.
488
- let selectionOpts = {};
520
+ const activeModelName = this.deps.client.getCurrentModel() ?? '';
521
+ let selectionOpts = activeModelName ? { modelName: activeModelName } : {};
489
522
  try {
490
523
  const { getModelToolConfig } = await import('../../config/model-tools.js');
491
- const cfg = getModelToolConfig(this.deps.client.getCurrentModel() ?? '');
524
+ const cfg = getModelToolConfig(activeModelName);
492
525
  if (cfg.promptProfile === 'lite') {
493
526
  selectionOpts = {
527
+ ...selectionOpts,
494
528
  maxTools: 5,
495
529
  alwaysInclude: ['view_file', 'bash', 'search'],
496
530
  };
@@ -499,8 +533,9 @@ export class AgentExecutor {
499
533
  catch { /* model-tools optional, never block */ }
500
534
  const selectionResult = await this.deps.toolSelectionStrategy.selectToolsForQuery(message, selectionOpts);
501
535
  let tools = selectionResult.tools;
536
+ let forcedChatOnlyToolRunModel = null;
502
537
  if (toolRounds === 0)
503
- this.deps.toolSelectionStrategy.cacheTools(tools);
538
+ this.deps.toolSelectionStrategy.cacheTools(tools, activeModelName);
504
539
  // If the active model is flagged `supportsToolCalls: false` in
505
540
  // model-tools.ts (typical of small Ollama / LM Studio models that
506
541
  // can't reliably emit OpenAI-style tool_call frames), drop the
@@ -515,12 +550,17 @@ export class AgentExecutor {
515
550
  // `(client as { defaultModel? }).defaultModel` access always
516
551
  // resolved to undefined because that field doesn't exist on
517
552
  // the dispatcher class — left the guard latent for ages.
518
- const modelName = this.deps.client.getCurrentModel() ?? '';
553
+ const modelName = activeModelName;
519
554
  if (modelName) {
520
555
  const cfg = getModelToolConfig(modelName);
521
556
  if (cfg.supportsToolCalls === false && tools.length > 0) {
522
- logger.debug(`[agent-executor] supportsToolCalls=false for ${modelName} — dropping ${tools.length} tools from chat call`);
523
- tools = [];
557
+ if (process.env.GROK_FORCE_TOOLS === 'true') {
558
+ forcedChatOnlyToolRunModel = modelName;
559
+ }
560
+ else {
561
+ logger.debug(`[agent-executor] supportsToolCalls=false for ${modelName} — dropping ${tools.length} tools from chat call`);
562
+ tools = [];
563
+ }
524
564
  }
525
565
  }
526
566
  }
@@ -597,7 +637,7 @@ export class AgentExecutor {
597
637
  yield { type: "token_count", tokenCount: inputTokens + result.tokenCount };
598
638
  }
599
639
  }
600
- if (!this.deps.streamingHandler.hasYieldedToolCalls()) {
640
+ if (tools.length > 0 && !this.deps.streamingHandler.hasYieldedToolCalls()) {
601
641
  const extracted = this.deps.streamingHandler.extractToolCalls();
602
642
  if (extracted.toolCalls.length > 0) {
603
643
  yield { type: "tool_calls", toolCalls: extracted.toolCalls };
@@ -605,9 +645,33 @@ export class AgentExecutor {
605
645
  }
606
646
  const accumulatedMessage = this.deps.streamingHandler.getAccumulatedMessage();
607
647
  // Sanitize streamed assistant content: strip model control tokens and invisible chars
608
- const rawStreamedContent = accumulatedMessage.content || "Using tools to help you...";
648
+ let toolCalls = accumulatedMessage.tool_calls;
649
+ if (Array.isArray(toolCalls) && toolCalls.length > 0) {
650
+ const filteredToolCalls = toolCalls.filter((toolCall) => !isIgnorableControlToolCall(toolCall));
651
+ if (filteredToolCalls.length !== toolCalls.length) {
652
+ logger.debug('[agent-executor] dropped hallucinated control tool calls', {
653
+ dropped: toolCalls.length - filteredToolCalls.length,
654
+ kept: filteredToolCalls.length,
655
+ });
656
+ }
657
+ toolCalls = filteredToolCalls.length > 0 ? filteredToolCalls : undefined;
658
+ }
659
+ const hasToolCalls = Array.isArray(toolCalls) && toolCalls.length > 0;
660
+ // Pre-fallback raw content — used by in-loop recovery to tell a real
661
+ // partial answer (retry-able) from a truly empty turn (give up).
662
+ const streamedContentRaw = (accumulatedMessage.content || "").trim();
663
+ const streamFinishReason = accumulatedMessage.finishReason;
664
+ let rawStreamedContent = accumulatedMessage.content || "";
665
+ if (forcedChatOnlyToolRunModel && !hasToolCalls && !rawStreamedContent.trim()) {
666
+ rawStreamedContent =
667
+ `Blocked: ${forcedChatOnlyToolRunModel} is configured as a chat-only local model and ` +
668
+ 'returned no structured tool call even with GROK_FORCE_TOOLS=true. ' +
669
+ 'Use a tool-capable model such as qwen3.5-ctx32k or gpt-5.5 for goals that need shell/tools.';
670
+ yield { type: "content", content: `${rawStreamedContent}\n` };
671
+ }
672
+ if (!rawStreamedContent)
673
+ rawStreamedContent = "Using tools to help you...";
609
674
  const content = sanitizeAssistantOutput(rawStreamedContent);
610
- const toolCalls = accumulatedMessage.tool_calls;
611
675
  const assistantEntry = {
612
676
  type: "assistant",
613
677
  content: content,
@@ -616,6 +680,9 @@ export class AgentExecutor {
616
680
  };
617
681
  history.push(assistantEntry);
618
682
  messages.push({ role: "assistant", content: content, tool_calls: toolCalls });
683
+ const currentOutputTokens = this.deps.streamingHandler.getTokenCount() || 0;
684
+ totalOutputTokens += currentOutputTokens;
685
+ yield { type: "token_count", tokenCount: inputTokens + totalOutputTokens };
619
686
  if (toolCalls && toolCalls.length > 0) {
620
687
  toolRounds++;
621
688
  // Pre-check cost limit before executing tools (estimate only — no side effects)
@@ -703,7 +770,7 @@ export class AgentExecutor {
703
770
  // Use streaming execution for tools that support it (bash, reason, + adapter-based)
704
771
  let result;
705
772
  const _streamToolStartMs = Date.now();
706
- const STREAMING_TOOLS = ['bash', 'reason'];
773
+ const STREAMING_TOOLS = ['bash', 'reason', 'generate_document'];
707
774
  if (STREAMING_TOOLS.includes(toolCall.function.name)) {
708
775
  const gen = this.deps.toolHandler.executeToolStreaming(toolCall);
709
776
  let genResult = await gen.next();
@@ -914,8 +981,6 @@ export class AgentExecutor {
914
981
  if (terminateDetectedStreaming)
915
982
  break;
916
983
  inputTokens = this.deps.tokenCounter.countMessageTokens(messages);
917
- const currentOutputTokens = this.deps.streamingHandler.getTokenCount() || 0;
918
- totalOutputTokens += currentOutputTokens;
919
984
  yield { type: "token_count", tokenCount: inputTokens + totalOutputTokens };
920
985
  // Run after_turn middleware (handles cost recording + limit)
921
986
  if (pipeline) {
@@ -942,6 +1007,51 @@ export class AgentExecutor {
942
1007
  catch { /* masking is optional */ }
943
1008
  }
944
1009
  else {
1010
+ // ── In-loop recovery (Hermes parity) ─────────────────────────────
1011
+ // Re-prompt WITHIN the turn before accepting this as the final answer.
1012
+ // Only reachable in the no-tool-calls branch, so we never split a
1013
+ // tool_call/tool_result pair (keeps transcript-repair invariants intact).
1014
+ if (!abortController?.signal.aborted) {
1015
+ // (1) Length truncation: the model hit the output-token cap mid-prose.
1016
+ // Ask it to continue from where it stopped, bounded. A continuation
1017
+ // that produced ZERO new tokens (e.g. a too-small num_ctx) leaves
1018
+ // `streamedContentRaw` empty — we do NOT retry that (the fix is config,
1019
+ // not looping); we fall through and stop.
1020
+ if (streamFinishReason === 'length' &&
1021
+ streamedContentRaw.length > 0 &&
1022
+ lengthContinuations < maxLengthContinuations) {
1023
+ lengthContinuations++;
1024
+ logger.debug('[agent-executor] length-truncation continuation', {
1025
+ attempt: lengthContinuations,
1026
+ max: maxLengthContinuations,
1027
+ });
1028
+ messages.push({
1029
+ role: 'user',
1030
+ content: 'Your previous message was cut off because it reached the output length limit. ' +
1031
+ 'Continue it from exactly where it stopped — do not repeat earlier text and do not ' +
1032
+ 'restart. When the full response is complete, finish normally.',
1033
+ });
1034
+ continue;
1035
+ }
1036
+ // (2) Post-tool empty response: the model went silent after running
1037
+ // tools. Nudge it to use the results and continue, bounded.
1038
+ if (streamedContentRaw.length === 0 &&
1039
+ streamFinishReason !== 'length' &&
1040
+ toolRounds > 0 &&
1041
+ emptyRetries < maxEmptyRetries) {
1042
+ emptyRetries++;
1043
+ logger.debug('[agent-executor] empty-response re-prompt', {
1044
+ attempt: emptyRetries,
1045
+ max: maxEmptyRetries,
1046
+ });
1047
+ messages.push({
1048
+ role: 'user',
1049
+ content: 'Your last response was empty. Use the results of the tool calls you just made ' +
1050
+ 'to continue the task and produce your answer.',
1051
+ });
1052
+ continue;
1053
+ }
1054
+ }
945
1055
  // Fire-and-forget auto-capture on final assistant response (streaming)
946
1056
  try {
947
1057
  const { getAutoCaptureManager } = await import('../../memory/auto-capture.js');
@@ -183,6 +183,13 @@ export const TOOL_METADATA = {
183
183
  parallelSafe: false, // Single local camera device + explicit privacy boundary
184
184
  priority: 6,
185
185
  },
186
+ camera_analyze: {
187
+ reads: [],
188
+ writes: ["file"],
189
+ hasSideEffects: true,
190
+ parallelSafe: false, // Single local camera device + network vision call
191
+ priority: 6,
192
+ },
186
193
  diagram: {
187
194
  reads: [],
188
195
  writes: ["file"],
@@ -11,6 +11,12 @@
11
11
  * @module agent/execution/tool-hooks
12
12
  */
13
13
  import type { CodeBuddyMessage } from '../../codebuddy/client.js';
14
+ export type FleetEventBroadcaster = (type: any, payload: any, agentId?: string) => void;
15
+ /**
16
+ * Register the fleet event broadcaster. Called by the server at boot
17
+ * to wire up fleet events without introducing a circular dependency.
18
+ */
19
+ export declare function registerFleetBroadcaster(broadcaster: FleetEventBroadcaster): void;
14
20
  /**
15
21
  * Emit a fleet event for tool execution. Best-effort: never throws,
16
22
  * silently no-ops when fleet streaming is disabled or the WS server
@@ -11,11 +11,19 @@
11
11
  * @module agent/execution/tool-hooks
12
12
  */
13
13
  import { getUserHooksManager } from '../../hooks/user-hooks.js';
14
- // Phase (d).2 V0.4.1 — eager import so concurrent tool emits don't get
15
- // serialized through dynamic-import promise chains (same fix as (d).3
16
- // for workflow events). fleet-bridge stays lean; it lazy-imports its
17
- // own deps so this doesn't bloat CLI-only startup.
18
- import { broadcastFleetEvent as _broadcastFleetEvent } from '../../server/websocket/fleet-bridge.js';
14
+ let _fleetBroadcaster = null;
15
+ /**
16
+ * Register the fleet event broadcaster. Called by the server at boot
17
+ * to wire up fleet events without introducing a circular dependency.
18
+ */
19
+ export function registerFleetBroadcaster(broadcaster) {
20
+ _fleetBroadcaster = broadcaster;
21
+ }
22
+ function _broadcastFleetEvent(type, payload, agentId) {
23
+ if (_fleetBroadcaster) {
24
+ _fleetBroadcaster(type, payload, agentId);
25
+ }
26
+ }
19
27
  /**
20
28
  * Phase (d).2 V0.4.1 — fleet stream opt-in. When CODEBUDDY_FLEET_STREAM=1
21
29
  * (or =true), every tool execution emits a fleet:agent:tool_started /
@@ -37,6 +37,12 @@ export interface ToolSelectionConfig {
37
37
  enableCaching: boolean;
38
38
  /** Cache TTL in milliseconds (default: 5 minutes) */
39
39
  cacheTTLMs: number;
40
+ /**
41
+ * Active model name. When present, model capability rules from
42
+ * model-tools.ts are applied to the model-facing schemas before they are
43
+ * cached or sent to the provider.
44
+ */
45
+ modelName?: string;
40
46
  }
41
47
  /**
42
48
  * Result of a tool selection operation
@@ -80,6 +86,7 @@ export declare class ToolSelectionStrategy {
80
86
  private config;
81
87
  private cachedTools;
82
88
  private cachedToolNames;
89
+ private cacheModelName;
83
90
  private lastQuery;
84
91
  private lastSelection;
85
92
  private cacheTimestamp;
@@ -109,13 +116,13 @@ export declare class ToolSelectionStrategy {
109
116
  *
110
117
  * @param tools - Tools to cache
111
118
  */
112
- cacheTools(tools: CodeBuddyTool[]): void;
119
+ cacheTools(tools: CodeBuddyTool[], modelName?: string): void;
113
120
  /**
114
121
  * Get cached tools if available and valid
115
122
  *
116
123
  * @returns Cached tools or null if cache is invalid/empty
117
124
  */
118
- getCachedTools(): CodeBuddyTool[] | null;
125
+ getCachedTools(modelName?: string): CodeBuddyTool[] | null;
119
126
  /**
120
127
  * Set the active skill for tool augmentation.
121
128
  *
@@ -140,6 +147,8 @@ export declare class ToolSelectionStrategy {
140
147
  * Check if the cache is still valid
141
148
  */
142
149
  private isCacheValid;
150
+ private static normalizeModelName;
151
+ private applyModelFacingSchemaFilter;
143
152
  /**
144
153
  * Record a tool request for metrics tracking
145
154
  *
@@ -17,6 +17,7 @@
17
17
  */
18
18
  import { getAllCodeBuddyTools, getRelevantTools, classifyQuery, getSkillAugmentedTools, } from '../../codebuddy/tools.js';
19
19
  import { getToolSelector, recordToolRequest, formatToolSelectionMetrics, } from '../../tools/tool-selector.js';
20
+ import { filterToolsForModel, getModelToolConfig, } from '../../config/model-tools.js';
20
21
  import { getPromptCacheManager } from '../../optimization/prompt-cache.js';
21
22
  import { logger } from '../../utils/logger.js';
22
23
  import { getSkillsHub } from '../../skills/hub.js';
@@ -27,10 +28,10 @@ const DEFAULT_CONFIG = {
27
28
  useRAG: true,
28
29
  maxTools: 15,
29
30
  minScore: 0.5,
30
- // `remember` is force-included so the LLM can always auto-persist non-obvious
31
- // facts to .codebuddy/CODEBUDDY_MEMORY.md, even on tasks where the RAG selector
32
- // wouldn't otherwise surface the memory tools (e.g. a pure code-edit query).
33
- // Paired with the auto-memory directive in `prompt-builder.ts`.
31
+ // `remember` and `memory_propose` are force-included so the LLM can always
32
+ // either persist explicit durable facts or queue inferred/ambiguous facts for
33
+ // review, even on tasks where the RAG selector wouldn't otherwise surface the
34
+ // memory tools. Paired with the auto-memory directive in `prompt-builder.ts`.
34
35
  //
35
36
  // `lessons_add`, `lessons_propose`, and `lessons_search` are force-included for
36
37
  // the same reason (Manus AI-inspired self-improvement loop) — paired with the
@@ -38,7 +39,7 @@ const DEFAULT_CONFIG = {
38
39
  // "agent proposes, human approves" path, so the model must always see it (a
39
40
  // RAG-gated propose tool would rarely surface). `lessons_list` stays out
40
41
  // (admin-style, not needed per-turn). Wakes the dormant feature in lessons-tracker.ts.
41
- alwaysInclude: ['view_file', 'bash', 'search', 'str_replace_editor', 'web_search', 'remember', 'lessons_add', 'lessons_propose', 'lessons_search'],
42
+ alwaysInclude: ['view_file', 'bash', 'search', 'str_replace_editor', 'web_search', 'remember', 'memory_propose', 'lessons_add', 'lessons_propose', 'lessons_search'],
42
43
  useAdaptiveThreshold: true,
43
44
  enableCaching: true,
44
45
  cacheTTLMs: 5 * 60 * 1000, // 5 minutes
@@ -68,6 +69,7 @@ export class ToolSelectionStrategy {
68
69
  config;
69
70
  cachedTools = null;
70
71
  cachedToolNames = [];
72
+ cacheModelName = null;
71
73
  lastQuery = '';
72
74
  lastSelection = null;
73
75
  cacheTimestamp = 0;
@@ -92,9 +94,10 @@ export class ToolSelectionStrategy {
92
94
  */
93
95
  async selectToolsForQuery(query, options = {}) {
94
96
  const effectiveConfig = { ...this.config, ...options };
97
+ const modelName = ToolSelectionStrategy.normalizeModelName(effectiveConfig.modelName);
95
98
  this.lastQuery = query;
96
99
  // Check if we should use cached tools
97
- if (effectiveConfig.enableCaching && this.isCacheValid()) {
100
+ if (effectiveConfig.enableCaching && this.isCacheValid(modelName)) {
98
101
  logger.debug('Using cached tools for query', { query: query.slice(0, 50) });
99
102
  return {
100
103
  tools: this.cachedTools,
@@ -142,6 +145,15 @@ export class ToolSelectionStrategy {
142
145
  toolCount: tools.length,
143
146
  });
144
147
  }
148
+ tools = this.applyModelFacingSchemaFilter(tools, modelName);
149
+ this.cachedToolNames = tools.map(t => t.function.name);
150
+ if (selection) {
151
+ selection = {
152
+ ...selection,
153
+ selectedTools: tools,
154
+ };
155
+ this.lastSelection = selection;
156
+ }
145
157
  // Cache tools for prompt optimization
146
158
  const promptCacheManager = getPromptCacheManager();
147
159
  promptCacheManager.cacheTools(tools);
@@ -169,14 +181,16 @@ export class ToolSelectionStrategy {
169
181
  *
170
182
  * @param tools - Tools to cache
171
183
  */
172
- cacheTools(tools) {
184
+ cacheTools(tools, modelName) {
173
185
  if (!this.config.enableCaching)
174
186
  return;
175
187
  this.cachedTools = tools;
176
188
  this.cachedToolNames = tools.map(t => t.function.name);
189
+ this.cacheModelName = ToolSelectionStrategy.normalizeModelName(modelName);
177
190
  this.cacheTimestamp = Date.now();
178
191
  logger.debug('Tools cached for multi-round consistency', {
179
192
  toolCount: tools.length,
193
+ modelName: this.cacheModelName,
180
194
  });
181
195
  }
182
196
  /**
@@ -184,10 +198,10 @@ export class ToolSelectionStrategy {
184
198
  *
185
199
  * @returns Cached tools or null if cache is invalid/empty
186
200
  */
187
- getCachedTools() {
201
+ getCachedTools(modelName) {
188
202
  if (!this.config.enableCaching)
189
203
  return null;
190
- if (!this.isCacheValid())
204
+ if (!this.isCacheValid(modelName))
191
205
  return null;
192
206
  return this.cachedTools;
193
207
  }
@@ -243,6 +257,7 @@ export class ToolSelectionStrategy {
243
257
  clearCache() {
244
258
  this.cachedTools = null;
245
259
  this.cachedToolNames = [];
260
+ this.cacheModelName = null;
246
261
  this.cacheTimestamp = 0;
247
262
  this.activeSkill = null;
248
263
  logger.debug('Tool selection cache cleared');
@@ -250,15 +265,57 @@ export class ToolSelectionStrategy {
250
265
  /**
251
266
  * Check if the cache is still valid
252
267
  */
253
- isCacheValid() {
268
+ isCacheValid(modelName) {
254
269
  if (!this.cachedTools || this.cachedTools.length === 0)
255
270
  return false;
256
271
  if (this.cacheTimestamp === 0)
257
272
  return false;
273
+ if (modelName !== undefined) {
274
+ const normalizedModelName = ToolSelectionStrategy.normalizeModelName(modelName);
275
+ if (this.cacheModelName !== normalizedModelName)
276
+ return false;
277
+ }
258
278
  const now = Date.now();
259
279
  const age = now - this.cacheTimestamp;
260
280
  return age < this.config.cacheTTLMs;
261
281
  }
282
+ static normalizeModelName(modelName) {
283
+ const normalized = modelName?.trim();
284
+ return normalized ? normalized : null;
285
+ }
286
+ applyModelFacingSchemaFilter(tools, modelName) {
287
+ if (!modelName || tools.length === 0) {
288
+ return tools;
289
+ }
290
+ try {
291
+ const modelConfig = getModelToolConfig(modelName);
292
+ if (modelConfig.supportsToolCalls === false && process.env.GROK_FORCE_TOOLS !== 'true') {
293
+ logger.debug('Model-facing tool schemas suppressed for chat-only model', {
294
+ modelName,
295
+ removed: tools.map(t => t.function.name),
296
+ });
297
+ return [];
298
+ }
299
+ const allowedNames = new Set(filterToolsForModel(tools.map(tool => tool.function.name), modelConfig));
300
+ const filtered = tools.filter(tool => allowedNames.has(tool.function.name));
301
+ if (filtered.length !== tools.length) {
302
+ logger.debug('Model-facing tool schemas filtered by model capabilities', {
303
+ modelName,
304
+ removed: tools
305
+ .map(tool => tool.function.name)
306
+ .filter(name => !allowedNames.has(name)),
307
+ });
308
+ }
309
+ return filtered;
310
+ }
311
+ catch (error) {
312
+ logger.debug('Model-facing tool schema filter skipped', {
313
+ modelName,
314
+ error: error instanceof Error ? error.message : String(error),
315
+ });
316
+ return tools;
317
+ }
318
+ }
262
319
  /**
263
320
  * Record a tool request for metrics tracking
264
321
  *
@@ -48,6 +48,14 @@ export declare class ExtendedThinkingManager {
48
48
  * Set whether extended thinking is always enabled
49
49
  */
50
50
  setAlwaysEnabled(value: boolean): void;
51
+ /**
52
+ * Apply a UI/settings thinking level — `off | minimal | low | medium | high | xhigh`.
53
+ * `off` (or any unknown level) disables thinking; any other level enables it with a
54
+ * scaled token budget. This is the runtime entry point for hot-swapping the level from
55
+ * the Cowork ReasoningLevelPicker; the OpenAI-compat / Grok / Ollama providers read
56
+ * {@link getThinkingConfig} fresh per request, so the change takes effect next turn.
57
+ */
58
+ applyThinkingLevel(level: string): void;
51
59
  /**
52
60
  * Get the thinking configuration to merge into API request parameters.
53
61
  * Returns an object with a `thinking` key when enabled, or an empty object when disabled.