novacode-cli 1.2.0__py3-none-any.whl

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 (691) hide show
  1. novacode_cli/__init__.py +29 -0
  2. novacode_cli/__main__.py +6 -0
  3. novacode_cli/_lazy_heavy.py +99 -0
  4. novacode_cli/_version.py +3 -0
  5. novacode_cli/agent_stream.py +178 -0
  6. novacode_cli/agents/agent_file.py +250 -0
  7. novacode_cli/agents/agent_mailbox.py +120 -0
  8. novacode_cli/agents/approval_decisions.py +146 -0
  9. novacode_cli/agents/arg_repair.py +196 -0
  10. novacode_cli/agents/async_agents/__init__.py +6 -0
  11. novacode_cli/agents/async_agents/_config.py +213 -0
  12. novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
  13. novacode_cli/agents/async_agents/_specialist.py +84 -0
  14. novacode_cli/agents/async_agents/_test_commands.py +89 -0
  15. novacode_cli/agents/async_agents/_tools.py +432 -0
  16. novacode_cli/agents/async_agents/general_purpose.py +42 -0
  17. novacode_cli/agents/async_agents/langgraph.json +9 -0
  18. novacode_cli/agents/async_agents/research.py +22 -0
  19. novacode_cli/agents/async_context.py +303 -0
  20. novacode_cli/agents/async_on_demand.py +52 -0
  21. novacode_cli/agents/async_task_errors.py +75 -0
  22. novacode_cli/agents/async_workspace.py +27 -0
  23. novacode_cli/agents/attribution.py +165 -0
  24. novacode_cli/agents/browser_mode.py +119 -0
  25. novacode_cli/agents/btw_agent.py +71 -0
  26. novacode_cli/agents/caveman.py +352 -0
  27. novacode_cli/agents/caveman_support.py +370 -0
  28. novacode_cli/agents/code_mode.py +774 -0
  29. novacode_cli/agents/code_runtime.py +160 -0
  30. novacode_cli/agents/context_compaction.py +1339 -0
  31. novacode_cli/agents/core_agent.py +2710 -0
  32. novacode_cli/agents/default_subagents/__init__.py +24 -0
  33. novacode_cli/agents/default_subagents/async_subagents.py +296 -0
  34. novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
  35. novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
  36. novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
  37. novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
  38. novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
  39. novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
  40. novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
  41. novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
  42. novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
  43. novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
  44. novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
  45. novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
  46. novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
  47. novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
  48. novacode_cli/agents/default_subagents/prompt.py +88 -0
  49. novacode_cli/agents/default_subagents/subagents.py +138 -0
  50. novacode_cli/agents/dream_agent.py +93 -0
  51. novacode_cli/agents/edit_diagnostics.py +362 -0
  52. novacode_cli/agents/edit_fallback.py +62 -0
  53. novacode_cli/agents/handoff.py +166 -0
  54. novacode_cli/agents/invocation.py +152 -0
  55. novacode_cli/agents/model_budget.py +42 -0
  56. novacode_cli/agents/model_retry.py +113 -0
  57. novacode_cli/agents/model_router.py +420 -0
  58. novacode_cli/agents/openai_decisions.py +186 -0
  59. novacode_cli/agents/plan_agent/__init__.py +12 -0
  60. novacode_cli/agents/plan_agent/plan_agent.py +357 -0
  61. novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
  62. novacode_cli/agents/plan_execution.py +237 -0
  63. novacode_cli/agents/ponytail.py +66 -0
  64. novacode_cli/agents/prime.py +274 -0
  65. novacode_cli/agents/provider_caching.py +99 -0
  66. novacode_cli/agents/server_launcher.py +904 -0
  67. novacode_cli/agents/strict_tools.py +384 -0
  68. novacode_cli/agents/task_discipline.py +294 -0
  69. novacode_cli/agents/tool_call_repair.py +290 -0
  70. novacode_cli/agents/tool_offload.py +291 -0
  71. novacode_cli/agents/tool_search.py +806 -0
  72. novacode_cli/agents/tool_search_variants.py +106 -0
  73. novacode_cli/agents/tool_verdicts.py +1194 -0
  74. novacode_cli/agents/user_async_agents.py +312 -0
  75. novacode_cli/artifacts/__init__.py +10 -0
  76. novacode_cli/artifacts/registry.py +278 -0
  77. novacode_cli/artifacts/server.py +426 -0
  78. novacode_cli/assets/Nova.png +0 -0
  79. novacode_cli/assets/nova-avatar.png +0 -0
  80. novacode_cli/audio/__init__.py +301 -0
  81. novacode_cli/audio/capture.py +137 -0
  82. novacode_cli/audio/downloads.py +273 -0
  83. novacode_cli/audio/pipeline.py +391 -0
  84. novacode_cli/audio/playback.py +87 -0
  85. novacode_cli/audio/providers.py +208 -0
  86. novacode_cli/audio/speakable.py +78 -0
  87. novacode_cli/audio/stt.py +132 -0
  88. novacode_cli/audio/stt_deepgram.py +69 -0
  89. novacode_cli/audio/stt_elevenlabs.py +89 -0
  90. novacode_cli/audio/stt_parakeet.py +94 -0
  91. novacode_cli/audio/stt_whistle.py +61 -0
  92. novacode_cli/audio/summarize.py +92 -0
  93. novacode_cli/audio/tts.py +85 -0
  94. novacode_cli/audio/tts_kokoro.py +73 -0
  95. novacode_cli/audio/tts_orpheus.py +180 -0
  96. novacode_cli/audio/tts_pocket.py +131 -0
  97. novacode_cli/audio/tts_sano.py +69 -0
  98. novacode_cli/audio/vad.py +145 -0
  99. novacode_cli/backends/__init__.py +17 -0
  100. novacode_cli/backends/filesystem.py +894 -0
  101. novacode_cli/backends/native_search.py +92 -0
  102. novacode_cli/bootstrap/__init__.py +9 -0
  103. novacode_cli/bootstrap/env_snapshot.py +240 -0
  104. novacode_cli/bootstrap/steering.py +464 -0
  105. novacode_cli/bootstrap/vision_router.py +604 -0
  106. novacode_cli/brand.py +362 -0
  107. novacode_cli/cli_args.py +718 -0
  108. novacode_cli/commands/__init__.py +237 -0
  109. novacode_cli/commands/agents_commands.py +231 -0
  110. novacode_cli/commands/auth_handler.py +40 -0
  111. novacode_cli/commands/browser_use_handler.py +75 -0
  112. novacode_cli/commands/cache_handler.py +32 -0
  113. novacode_cli/commands/chat_handler.py +1357 -0
  114. novacode_cli/commands/ci_fix.py +171 -0
  115. novacode_cli/commands/claude_plan.py +331 -0
  116. novacode_cli/commands/codex_plan.py +195 -0
  117. novacode_cli/commands/commands.py +1113 -0
  118. novacode_cli/commands/create_handler.py +124 -0
  119. novacode_cli/commands/create_server.py +1887 -0
  120. novacode_cli/commands/cron_handler.py +119 -0
  121. novacode_cli/commands/dream_handler.py +203 -0
  122. novacode_cli/commands/effort_handler.py +65 -0
  123. novacode_cli/commands/evolution_handler.py +214 -0
  124. novacode_cli/commands/file_commands.py +381 -0
  125. novacode_cli/commands/handoff_handler.py +68 -0
  126. novacode_cli/commands/hooks_handler.py +269 -0
  127. novacode_cli/commands/init_handler.py +146 -0
  128. novacode_cli/commands/learning_handler.py +160 -0
  129. novacode_cli/commands/lite_mode_handler.py +76 -0
  130. novacode_cli/commands/log_commands.py +363 -0
  131. novacode_cli/commands/mcp_handler.py +29 -0
  132. novacode_cli/commands/model_handler.py +29 -0
  133. novacode_cli/commands/notifications_handler.py +158 -0
  134. novacode_cli/commands/onboard_handler.py +121 -0
  135. novacode_cli/commands/plan_handler.py +440 -0
  136. novacode_cli/commands/planner_usage.py +250 -0
  137. novacode_cli/commands/plugin_install_handler.py +38 -0
  138. novacode_cli/commands/plugins_handler.py +35 -0
  139. novacode_cli/commands/prime_handler.py +455 -0
  140. novacode_cli/commands/prompt_handler.py +149 -0
  141. novacode_cli/commands/provider_usage.py +160 -0
  142. novacode_cli/commands/ralph_events.py +106 -0
  143. novacode_cli/commands/ralph_handler.py +1293 -0
  144. novacode_cli/commands/refine_handler.py +213 -0
  145. novacode_cli/commands/research_handler.py +202 -0
  146. novacode_cli/commands/rootapp_remote.py +83 -0
  147. novacode_cli/commands/router_handler.py +36 -0
  148. novacode_cli/commands/server_commands.py +249 -0
  149. novacode_cli/commands/session_commands.py +203 -0
  150. novacode_cli/commands/share_handler.py +450 -0
  151. novacode_cli/commands/side_commands.py +366 -0
  152. novacode_cli/commands/skill_invoke.py +420 -0
  153. novacode_cli/commands/skills_commands.py +48 -0
  154. novacode_cli/commands/trace_handler.py +322 -0
  155. novacode_cli/commands/trello_handler.py +226 -0
  156. novacode_cli/commands/trello_remote.py +177 -0
  157. novacode_cli/commands/trello_server.py +1214 -0
  158. novacode_cli/commands/vision_handler.py +127 -0
  159. novacode_cli/commands/voice_handler.py +447 -0
  160. novacode_cli/commands/webhook_handler.py +134 -0
  161. novacode_cli/commands/wiki_commands.py +227 -0
  162. novacode_cli/compaction.py +753 -0
  163. novacode_cli/computation_cache.py +159 -0
  164. novacode_cli/computer/__init__.py +1 -0
  165. novacode_cli/computer/persistent.py +234 -0
  166. novacode_cli/computer/preview.py +253 -0
  167. novacode_cli/computer/runtime.py +474 -0
  168. novacode_cli/computer/windows_console.py +152 -0
  169. novacode_cli/computer/windows_qemu.py +211 -0
  170. novacode_cli/config/browser_mode.py +159 -0
  171. novacode_cli/config/catalog_models.py +95 -0
  172. novacode_cli/config/config.py +1388 -0
  173. novacode_cli/config/credentials.py +540 -0
  174. novacode_cli/config/google_oauth_auth.py +281 -0
  175. novacode_cli/config/model_capabilities.py +343 -0
  176. novacode_cli/config/model_catalog.py +275 -0
  177. novacode_cli/config/model_create.py +1325 -0
  178. novacode_cli/config/model_manager.py +695 -0
  179. novacode_cli/config/nova_config.py +1283 -0
  180. novacode_cli/config/openai_chatgpt_auth.py +384 -0
  181. novacode_cli/config/opencode_gateway.py +25 -0
  182. novacode_cli/config/plan_mode.py +188 -0
  183. novacode_cli/config/provider_auth.py +438 -0
  184. novacode_cli/config/provider_catalog.json +5127 -0
  185. novacode_cli/config/provider_registry.py +38 -0
  186. novacode_cli/config/provider_transports.py +258 -0
  187. novacode_cli/config/role_models.py +175 -0
  188. novacode_cli/context/__init__.py +50 -0
  189. novacode_cli/context/_analysis.py +799 -0
  190. novacode_cli/context/_budget.py +116 -0
  191. novacode_cli/context/_conversation.py +73 -0
  192. novacode_cli/context/_dynamic.py +264 -0
  193. novacode_cli/context/_model_config.py +380 -0
  194. novacode_cli/context/_models_dev.py +99 -0
  195. novacode_cli/context/archives.py +248 -0
  196. novacode_cli/context/estimates.py +65 -0
  197. novacode_cli/context/history.py +17 -0
  198. novacode_cli/context/knowledge_cache.py +292 -0
  199. novacode_cli/context/manager.py +101 -0
  200. novacode_cli/context/observations.py +65 -0
  201. novacode_cli/context/pressure.py +168 -0
  202. novacode_cli/context/provider_accounting.py +109 -0
  203. novacode_cli/context/snapshot.py +493 -0
  204. novacode_cli/context/strategies.py +279 -0
  205. novacode_cli/context/task_progress.py +122 -0
  206. novacode_cli/core/__init__.py +0 -0
  207. novacode_cli/core/agent_loop.py +1821 -0
  208. novacode_cli/core/autonomous_loop.py +277 -0
  209. novacode_cli/core/code_approvals.py +57 -0
  210. novacode_cli/core/input_preparation.py +276 -0
  211. novacode_cli/core/reasoning.py +67 -0
  212. novacode_cli/core/streaming.py +326 -0
  213. novacode_cli/core/subagent_tasks.py +696 -0
  214. novacode_cli/core/subagent_tracking.py +235 -0
  215. novacode_cli/core/tool_results.py +116 -0
  216. novacode_cli/core/verification_loop.py +272 -0
  217. novacode_cli/council.py +595 -0
  218. novacode_cli/council_consensus.py +115 -0
  219. novacode_cli/council_decision.py +157 -0
  220. novacode_cli/council_planner.py +1421 -0
  221. novacode_cli/council_settings.py +208 -0
  222. novacode_cli/council_web_settings.py +41 -0
  223. novacode_cli/council_workspace.py +56 -0
  224. novacode_cli/cowork/__init__.py +10 -0
  225. novacode_cli/cowork/broker_middleware.py +162 -0
  226. novacode_cli/cowork/launcher.py +102 -0
  227. novacode_cli/cowork/policy.py +284 -0
  228. novacode_cli/cowork/ui.py +826 -0
  229. novacode_cli/daemons/__init__.py +1 -0
  230. novacode_cli/daemons/registry.py +342 -0
  231. novacode_cli/desktop_notifications.py +302 -0
  232. novacode_cli/doctor.py +251 -0
  233. novacode_cli/embeddings.py +109 -0
  234. novacode_cli/entrypoint.py +56 -0
  235. novacode_cli/errors/__init__.py +19 -0
  236. novacode_cli/errors/handlers.py +648 -0
  237. novacode_cli/errors/provider_errors.py +249 -0
  238. novacode_cli/errors/taxonomy.py +80 -0
  239. novacode_cli/events.py +179 -0
  240. novacode_cli/file_ops.py +714 -0
  241. novacode_cli/git_safety.py +281 -0
  242. novacode_cli/handoff.py +383 -0
  243. novacode_cli/headless/__init__.py +18 -0
  244. novacode_cli/headless/input.py +58 -0
  245. novacode_cli/headless/output.py +224 -0
  246. novacode_cli/headless/pipe.py +532 -0
  247. novacode_cli/headless/runner.py +461 -0
  248. novacode_cli/hermes/__init__.py +39 -0
  249. novacode_cli/hermes/activation.py +297 -0
  250. novacode_cli/hermes/config.py +129 -0
  251. novacode_cli/hermes/curator.py +199 -0
  252. novacode_cli/hermes/evolution.py +374 -0
  253. novacode_cli/hermes/ledger.py +364 -0
  254. novacode_cli/hermes/memory_tiers.py +969 -0
  255. novacode_cli/hermes/middleware.py +678 -0
  256. novacode_cli/hermes/overview.py +174 -0
  257. novacode_cli/hermes/prompt_evolution.py +503 -0
  258. novacode_cli/hermes/refine_loop.py +643 -0
  259. novacode_cli/hermes/refinement_log.py +190 -0
  260. novacode_cli/hermes/review.py +674 -0
  261. novacode_cli/hermes/skill_debate.py +135 -0
  262. novacode_cli/hermes/skill_discovery.py +450 -0
  263. novacode_cli/hermes/skill_manager.py +335 -0
  264. novacode_cli/hermes/skill_proposals.py +43 -0
  265. novacode_cli/hermes/test_gaming.py +119 -0
  266. novacode_cli/hermes/tracker.py +435 -0
  267. novacode_cli/hermes/tuner.py +206 -0
  268. novacode_cli/hermes/verification_memory.py +134 -0
  269. novacode_cli/hermes/verifier.py +277 -0
  270. novacode_cli/hitl/__init__.py +22 -0
  271. novacode_cli/hitl/interrupts.py +493 -0
  272. novacode_cli/hooks.py +359 -0
  273. novacode_cli/image_utils.py +517 -0
  274. novacode_cli/init/__init__.py +7 -0
  275. novacode_cli/init/events.py +43 -0
  276. novacode_cli/input_utils.py +526 -0
  277. novacode_cli/install_commands.py +87 -0
  278. novacode_cli/integrations/__init__.py +82 -0
  279. novacode_cli/integrations/daytona.py +122 -0
  280. novacode_cli/integrations/docker.py +309 -0
  281. novacode_cli/integrations/host_path.py +111 -0
  282. novacode_cli/integrations/langsmith.py +151 -0
  283. novacode_cli/integrations/modal.py +134 -0
  284. novacode_cli/integrations/runloop.py +130 -0
  285. novacode_cli/integrations/sandbox_factory.py +1388 -0
  286. novacode_cli/integrations/sandbox_registry.py +312 -0
  287. novacode_cli/integrations/workdir_backend.py +589 -0
  288. novacode_cli/main.py +2078 -0
  289. novacode_cli/mcp/__init__.py +73 -0
  290. novacode_cli/mcp/auth.py +189 -0
  291. novacode_cli/mcp/client.py +571 -0
  292. novacode_cli/mcp/code_mode.py +5 -0
  293. novacode_cli/mcp/code_runtime.py +5 -0
  294. novacode_cli/mcp/commands.py +902 -0
  295. novacode_cli/mcp/config.py +406 -0
  296. novacode_cli/mcp/generic.py +128 -0
  297. novacode_cli/mcp/middleware.py +993 -0
  298. novacode_cli/mcp/presets.py +367 -0
  299. novacode_cli/memory/__init__.py +1 -0
  300. novacode_cli/memory/agent_memory.py +1312 -0
  301. novacode_cli/memory/builtin_lessons.py +60 -0
  302. novacode_cli/memory/dream_gates.py +192 -0
  303. novacode_cli/memory/limits.py +59 -0
  304. novacode_cli/memory/parsed_cache.py +106 -0
  305. novacode_cli/memory/recall_stats.py +97 -0
  306. novacode_cli/memory/semantic.py +116 -0
  307. novacode_cli/memory/store.py +335 -0
  308. novacode_cli/memory/wiki.py +274 -0
  309. novacode_cli/migrate.py +185 -0
  310. novacode_cli/onboarding.py +649 -0
  311. novacode_cli/onboarding_check.py +119 -0
  312. novacode_cli/path_approval.py +297 -0
  313. novacode_cli/plan_archive.py +148 -0
  314. novacode_cli/plugins/__init__.py +121 -0
  315. novacode_cli/plugins/claude_plugins.py +538 -0
  316. novacode_cli/plugins/cli.py +168 -0
  317. novacode_cli/plugins/components.py +413 -0
  318. novacode_cli/plugins/hooks.py +356 -0
  319. novacode_cli/plugins/loader.py +317 -0
  320. novacode_cli/plugins/marketplaces.py +199 -0
  321. novacode_cli/plugins/storage.py +70 -0
  322. novacode_cli/prime.py +759 -0
  323. novacode_cli/prime_factory/__init__.py +15 -0
  324. novacode_cli/prime_factory/container.py +198 -0
  325. novacode_cli/prime_factory/controller.py +565 -0
  326. novacode_cli/prime_factory/knowledge.py +261 -0
  327. novacode_cli/prime_factory/publication.py +218 -0
  328. novacode_cli/prime_factory/recovery.py +50 -0
  329. novacode_cli/prime_factory/research.py +370 -0
  330. novacode_cli/prime_factory/runner.py +608 -0
  331. novacode_cli/prime_factory/runtime.py +261 -0
  332. novacode_cli/prime_factory/state.py +201 -0
  333. novacode_cli/prime_factory/store.py +128 -0
  334. novacode_cli/prime_factory/worker.py +129 -0
  335. novacode_cli/prime_factory/workflow.py +899 -0
  336. novacode_cli/prime_factory/workspace.py +317 -0
  337. novacode_cli/prime_memory.py +290 -0
  338. novacode_cli/process_manager.py +585 -0
  339. novacode_cli/project_onboard.py +537 -0
  340. novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
  341. novacode_cli/prompts/__init__.py +168 -0
  342. novacode_cli/prompts/agent_definitions.jinja +11 -0
  343. novacode_cli/prompts/agent_generation.jinja +125 -0
  344. novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
  345. novacode_cli/prompts/claude_plan_mode.jinja +12 -0
  346. novacode_cli/prompts/coding_workflow.jinja +18 -0
  347. novacode_cli/prompts/computer_use.jinja +17 -0
  348. novacode_cli/prompts/core_agent_system.jinja +81 -0
  349. novacode_cli/prompts/delegation.jinja +15 -0
  350. novacode_cli/prompts/desktop_power.jinja +4 -0
  351. novacode_cli/prompts/file_paths.jinja +15 -0
  352. novacode_cli/prompts/file_tracker.jinja +23 -0
  353. novacode_cli/prompts/init_agents_md.jinja +56 -0
  354. novacode_cli/prompts/init_exploration.jinja +174 -0
  355. novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
  356. novacode_cli/prompts/instruction_scope.jinja +5 -0
  357. novacode_cli/prompts/learnings_extraction.jinja +45 -0
  358. novacode_cli/prompts/longterm_memory.jinja +54 -0
  359. novacode_cli/prompts/mcp.jinja +84 -0
  360. novacode_cli/prompts/memory_consolidation.jinja +40 -0
  361. novacode_cli/prompts/nova_evolution.jinja +73 -0
  362. novacode_cli/prompts/nova_review.jinja +158 -0
  363. novacode_cli/prompts/nova_skill_debate.jinja +36 -0
  364. novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
  365. novacode_cli/prompts/nova_verify.jinja +100 -0
  366. novacode_cli/prompts/onboard_dashboard.jinja +378 -0
  367. novacode_cli/prompts/plan_agent.jinja +87 -0
  368. novacode_cli/prompts/plan_scouts.jinja +5 -0
  369. novacode_cli/prompts/plan_storage.jinja +8 -0
  370. novacode_cli/prompts/planning.jinja +82 -0
  371. novacode_cli/prompts/ponytail/LICENSE +21 -0
  372. novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
  373. novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
  374. novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
  375. novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
  376. novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
  377. novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
  378. novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
  379. novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
  380. novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
  381. novacode_cli/prompts/prompt_evolution.jinja +27 -0
  382. novacode_cli/prompts/ralph_iteration.jinja +75 -0
  383. novacode_cli/prompts/refine_gate.jinja +31 -0
  384. novacode_cli/prompts/refine_plan.jinja +64 -0
  385. novacode_cli/prompts/refine_review.jinja +34 -0
  386. novacode_cli/prompts/research_delivery.jinja +9 -0
  387. novacode_cli/prompts/research_swarm.jinja +130 -0
  388. novacode_cli/prompts/response_style.jinja +5 -0
  389. novacode_cli/prompts/session_summarization.jinja +86 -0
  390. novacode_cli/prompts/skill_creation.jinja +85 -0
  391. novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
  392. novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
  393. novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
  394. novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
  395. novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
  396. novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
  397. novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
  398. novacode_cli/prompts/summarization.jinja +107 -0
  399. novacode_cli/prompts/summarization_answers.jinja +23 -0
  400. novacode_cli/prompts/summarization_questions.jinja +32 -0
  401. novacode_cli/prompts/vision_caption.jinja +21 -0
  402. novacode_cli/prompts/voice_summary.jinja +65 -0
  403. novacode_cli/prompts/wiki_file.jinja +57 -0
  404. novacode_cli/prompts/wiki_ingest.jinja +87 -0
  405. novacode_cli/recovery.py +383 -0
  406. novacode_cli/remote/__init__.py +45 -0
  407. novacode_cli/remote/approvals.py +282 -0
  408. novacode_cli/remote/async_task_clients.py +97 -0
  409. novacode_cli/remote/async_task_watcher.py +240 -0
  410. novacode_cli/remote/bridge.py +905 -0
  411. novacode_cli/remote/command_menu.py +26 -0
  412. novacode_cli/remote/command_ui.py +243 -0
  413. novacode_cli/remote/config.py +199 -0
  414. novacode_cli/remote/discord_bridge.py +530 -0
  415. novacode_cli/remote/discord_format.py +31 -0
  416. novacode_cli/remote/discord_threads.py +225 -0
  417. novacode_cli/remote/images.py +97 -0
  418. novacode_cli/remote/processor.py +601 -0
  419. novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
  420. novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
  421. novacode_cli/remote/rootapp_bot/package.json +18 -0
  422. novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
  423. novacode_cli/remote/rootapp_bridge.py +370 -0
  424. novacode_cli/remote/rootapp_setup.py +89 -0
  425. novacode_cli/remote/routing.py +103 -0
  426. novacode_cli/remote/scheduler.py +302 -0
  427. novacode_cli/remote/session.py +164 -0
  428. novacode_cli/remote/status.py +411 -0
  429. novacode_cli/remote/streaming.py +188 -0
  430. novacode_cli/remote/telegram_bridge.py +1118 -0
  431. novacode_cli/remote/telegram_format.py +215 -0
  432. novacode_cli/remote/telegram_hub.py +256 -0
  433. novacode_cli/remote/voice_notes.py +120 -0
  434. novacode_cli/remote/webhook_adapters.py +167 -0
  435. novacode_cli/remote/webhook_server.py +154 -0
  436. novacode_cli/rewind.py +150 -0
  437. novacode_cli/rooms/__init__.py +1 -0
  438. novacode_cli/rooms/bridge.py +86 -0
  439. novacode_cli/rooms/cli.py +231 -0
  440. novacode_cli/rooms/nova.py +227 -0
  441. novacode_cli/rooms/runtime.py +299 -0
  442. novacode_cli/rooms/store.py +538 -0
  443. novacode_cli/rooms/workspace.py +94 -0
  444. novacode_cli/security/__init__.py +29 -0
  445. novacode_cli/security/approval_context.py +45 -0
  446. novacode_cli/security/delegated_approval.py +68 -0
  447. novacode_cli/security/local_http.py +85 -0
  448. novacode_cli/security/middleware.py +87 -0
  449. novacode_cli/security/paths.py +35 -0
  450. novacode_cli/security/policy.py +535 -0
  451. novacode_cli/security/policy_writer.py +90 -0
  452. novacode_cli/security/prompt_injection.py +203 -0
  453. novacode_cli/security/remember.py +81 -0
  454. novacode_cli/security/rule_synthesis.py +116 -0
  455. novacode_cli/security/secret_files.py +61 -0
  456. novacode_cli/security/session_allow.py +114 -0
  457. novacode_cli/security/unicode_security.py +516 -0
  458. novacode_cli/security/validator.py +173 -0
  459. novacode_cli/server/__init__.py +9 -0
  460. novacode_cli/server/app.py +632 -0
  461. novacode_cli/server/event_adapter.py +189 -0
  462. novacode_cli/server/session_manager.py +90 -0
  463. novacode_cli/server_main.py +64 -0
  464. novacode_cli/server_runner/dev_server.py +530 -0
  465. novacode_cli/server_runner/test_runner.py +470 -0
  466. novacode_cli/session/__init__.py +1 -0
  467. novacode_cli/session/adapters/__init__.py +240 -0
  468. novacode_cli/session/adapters/claude_code.py +102 -0
  469. novacode_cli/session/adapters/codex.py +77 -0
  470. novacode_cli/session/adapters/harness_native.py +75 -0
  471. novacode_cli/session/checkpoint_coordinator.py +448 -0
  472. novacode_cli/session/context_restore.py +25 -0
  473. novacode_cli/session/history_export.py +228 -0
  474. novacode_cli/session/history_tree.py +174 -0
  475. novacode_cli/session/imported_context.py +267 -0
  476. novacode_cli/session/runtime_identity.py +32 -0
  477. novacode_cli/session/session_persistence.py +1168 -0
  478. novacode_cli/session/session_prompt_builder.py +279 -0
  479. novacode_cli/session/session_restore.py +351 -0
  480. novacode_cli/session/session_summarization.py +20 -0
  481. novacode_cli/session/transcript_journal.py +153 -0
  482. novacode_cli/sessions/__init__.py +11 -0
  483. novacode_cli/sessions/gateway.py +84 -0
  484. novacode_cli/sessions/launch.py +51 -0
  485. novacode_cli/sessions/lease.py +213 -0
  486. novacode_cli/sessions/messaging.py +255 -0
  487. novacode_cli/sessions/protocol.py +216 -0
  488. novacode_cli/sessions/resume.py +31 -0
  489. novacode_cli/sessions/supervisor.py +498 -0
  490. novacode_cli/sessions/tasks.py +73 -0
  491. novacode_cli/sessions/tui_messaging.py +413 -0
  492. novacode_cli/sessions/worker.py +652 -0
  493. novacode_cli/sessions/worktree.py +262 -0
  494. novacode_cli/shell/__init__.py +27 -0
  495. novacode_cli/shell/jobs.py +475 -0
  496. novacode_cli/shell/middleware.py +1568 -0
  497. novacode_cli/shell/os_sandbox.py +205 -0
  498. novacode_cli/shell/output.py +181 -0
  499. novacode_cli/shell/patterns.py +275 -0
  500. novacode_cli/shell/processes.py +87 -0
  501. novacode_cli/shell/utils.py +236 -0
  502. novacode_cli/skills/__init__.py +26 -0
  503. novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
  504. novacode_cli/skills/curation_middleware.py +69 -0
  505. novacode_cli/skills/executable.py +161 -0
  506. novacode_cli/skills/libraries.py +102 -0
  507. novacode_cli/skills/load.py +248 -0
  508. novacode_cli/skills/prune.py +215 -0
  509. novacode_cli/skills/refreshing_middleware.py +896 -0
  510. novacode_cli/skills/retrieval.py +248 -0
  511. novacode_cli/skills/runtime.py +118 -0
  512. novacode_cli/skills/schema.py +254 -0
  513. novacode_cli/skills/skill_creation.py +1549 -0
  514. novacode_cli/skills/skill_lock.py +75 -0
  515. novacode_cli/skills/skill_system_prompt.py +16 -0
  516. novacode_cli/skills/skills_prefs.py +126 -0
  517. novacode_cli/skills/tool_resolver.py +31 -0
  518. novacode_cli/skills/upstream.py +50 -0
  519. novacode_cli/skills/versioning.py +291 -0
  520. novacode_cli/states/Session.py +847 -0
  521. novacode_cli/states/__init__.py +31 -0
  522. novacode_cli/states/slices/__init__.py +17 -0
  523. novacode_cli/states/slices/agent_runtime.py +249 -0
  524. novacode_cli/states/slices/background_tasks.py +21 -0
  525. novacode_cli/states/slices/notifications.py +74 -0
  526. novacode_cli/states/slices/remote_bridge.py +56 -0
  527. novacode_cli/states/slices/ui_settings.py +49 -0
  528. novacode_cli/states/slices/wiki.py +25 -0
  529. novacode_cli/teammates/__init__.py +1 -0
  530. novacode_cli/teammates/controller.py +557 -0
  531. novacode_cli/teammates/graph.py +104 -0
  532. novacode_cli/teammates/registry.py +175 -0
  533. novacode_cli/teammates/service.py +254 -0
  534. novacode_cli/teammates/worker.py +19 -0
  535. novacode_cli/teams/__init__.py +1 -0
  536. novacode_cli/teams/adapters/__init__.py +1 -0
  537. novacode_cli/teams/adapters/container.py +316 -0
  538. novacode_cli/teams/adapters/inprocess.py +206 -0
  539. novacode_cli/teams/adapters/process.py +364 -0
  540. novacode_cli/teams/bootstrap.py +246 -0
  541. novacode_cli/teams/container_git.py +67 -0
  542. novacode_cli/teams/context.py +34 -0
  543. novacode_cli/teams/coordinator.py +815 -0
  544. novacode_cli/teams/gateway.py +200 -0
  545. novacode_cli/teams/host.py +64 -0
  546. novacode_cli/teams/legacy.py +52 -0
  547. novacode_cli/teams/middleware.py +176 -0
  548. novacode_cli/teams/models.py +104 -0
  549. novacode_cli/teams/output.py +47 -0
  550. novacode_cli/teams/protocol.py +50 -0
  551. novacode_cli/teams/recovery.py +80 -0
  552. novacode_cli/teams/scheduler.py +44 -0
  553. novacode_cli/teams/store.py +631 -0
  554. novacode_cli/teams/worker.py +251 -0
  555. novacode_cli/teams/workspace.py +100 -0
  556. novacode_cli/token_utils.py +256 -0
  557. novacode_cli/tools/__init__.py +104 -0
  558. novacode_cli/tools/_shared.py +97 -0
  559. novacode_cli/tools/_types.py +24 -0
  560. novacode_cli/tools/artifact_tools.py +137 -0
  561. novacode_cli/tools/code_search_tools.py +330 -0
  562. novacode_cli/tools/computer_tools.py +77 -0
  563. novacode_cli/tools/cron_tools.py +65 -0
  564. novacode_cli/tools/daemon_tool.py +290 -0
  565. novacode_cli/tools/desktop_tools.py +161 -0
  566. novacode_cli/tools/fetch_tools.py +530 -0
  567. novacode_cli/tools/http_tools.py +279 -0
  568. novacode_cli/tools/inspection_tools.py +107 -0
  569. novacode_cli/tools/job_tools.py +143 -0
  570. novacode_cli/tools/knowledge_tools.py +427 -0
  571. novacode_cli/tools/memory_tools.py +461 -0
  572. novacode_cli/tools/memory_wiki_tools.py +40 -0
  573. novacode_cli/tools/notebook_tools.py +238 -0
  574. novacode_cli/tools/oracle_tool.py +83 -0
  575. novacode_cli/tools/plan_archive_tools.py +80 -0
  576. novacode_cli/tools/plan_mode_tools.py +315 -0
  577. novacode_cli/tools/python_kernel_loop.py +165 -0
  578. novacode_cli/tools/python_kernel_tool.py +222 -0
  579. novacode_cli/tools/reflection_tools.py +34 -0
  580. novacode_cli/tools/scraper_tools.py +377 -0
  581. novacode_cli/tools/skill_tools.py +275 -0
  582. novacode_cli/tools/speak_tool.py +51 -0
  583. novacode_cli/tools/store_memory_tools.py +227 -0
  584. novacode_cli/tools/ui_tools.py +62 -0
  585. novacode_cli/tools/waiter_tools.py +408 -0
  586. novacode_cli/tools/web_tools.py +349 -0
  587. novacode_cli/tools/wiki_tools.py +152 -0
  588. novacode_cli/tools/worktree_tools.py +219 -0
  589. novacode_cli/tracking/__init__.py +10 -0
  590. novacode_cli/tracking/file_tracker.py +826 -0
  591. novacode_cli/tracking/local_metrics.py +170 -0
  592. novacode_cli/tracking/loop_guard.py +507 -0
  593. novacode_cli/tracking/request_metrics.py +60 -0
  594. novacode_cli/tracking/tracing.py +692 -0
  595. novacode_cli/tracking/usage_tree.py +677 -0
  596. novacode_cli/tracking/workspace_anchoring.py +231 -0
  597. novacode_cli/tui/__init__.py +11 -0
  598. novacode_cli/tui/animation_rate.py +9 -0
  599. novacode_cli/tui/animations.py +332 -0
  600. novacode_cli/tui/app.py +19315 -0
  601. novacode_cli/tui/auth_display.py +87 -0
  602. novacode_cli/tui/auth_screens.py +826 -0
  603. novacode_cli/tui/avatar.py +294 -0
  604. novacode_cli/tui/avatar_frames.py +21 -0
  605. novacode_cli/tui/background_tasks.py +35 -0
  606. novacode_cli/tui/browser_settings_screen.py +70 -0
  607. novacode_cli/tui/caveman_screen.py +185 -0
  608. novacode_cli/tui/council_settings_screen.py +182 -0
  609. novacode_cli/tui/council_view.py +535 -0
  610. novacode_cli/tui/decision_settings.py +268 -0
  611. novacode_cli/tui/diff_preview.py +199 -0
  612. novacode_cli/tui/gc_tuning.py +63 -0
  613. novacode_cli/tui/handoff_screen.py +206 -0
  614. novacode_cli/tui/harness.py +244 -0
  615. novacode_cli/tui/history_tree_screen.py +179 -0
  616. novacode_cli/tui/home.py +328 -0
  617. novacode_cli/tui/home_scene.py +94 -0
  618. novacode_cli/tui/icons.py +81 -0
  619. novacode_cli/tui/interface_styles.tcss +327 -0
  620. novacode_cli/tui/matrix3.py +108 -0
  621. novacode_cli/tui/mcp_tools_screen.py +131 -0
  622. novacode_cli/tui/motion.py +178 -0
  623. novacode_cli/tui/output_body.py +136 -0
  624. novacode_cli/tui/output_buffer.py +59 -0
  625. novacode_cli/tui/palette.py +334 -0
  626. novacode_cli/tui/pickers.py +237 -0
  627. novacode_cli/tui/prime_panel.py +506 -0
  628. novacode_cli/tui/provider_signin.py +213 -0
  629. novacode_cli/tui/pty_shell.py +159 -0
  630. novacode_cli/tui/reference_screens.py +444 -0
  631. novacode_cli/tui/resume_picker.py +346 -0
  632. novacode_cli/tui/rooms.py +597 -0
  633. novacode_cli/tui/screens.py +7088 -0
  634. novacode_cli/tui/session_import.py +333 -0
  635. novacode_cli/tui/session_pane.py +242 -0
  636. novacode_cli/tui/session_tab.py +42 -0
  637. novacode_cli/tui/session_transcript.py +118 -0
  638. novacode_cli/tui/shell_output.py +7 -0
  639. novacode_cli/tui/stall_watch.py +174 -0
  640. novacode_cli/tui/statusline.py +40 -0
  641. novacode_cli/tui/statusline_screen.py +91 -0
  642. novacode_cli/tui/teammate_updates.py +162 -0
  643. novacode_cli/tui/teammates.py +548 -0
  644. novacode_cli/tui/terminal.py +289 -0
  645. novacode_cli/tui/terminal_view.py +235 -0
  646. novacode_cli/tui/themes.py +118 -0
  647. novacode_cli/tui/update_notice.py +84 -0
  648. novacode_cli/tui/voice_downloads.py +113 -0
  649. novacode_cli/tui/widgets.py +2038 -0
  650. novacode_cli/tui/windows_input.py +38 -0
  651. novacode_cli/tui/workspace_approval.py +165 -0
  652. novacode_cli/ui/council_web.py +113 -0
  653. novacode_cli/ui/council_workspace.py +72 -0
  654. novacode_cli/ui/cowork_web.py +41 -0
  655. novacode_cli/ui/diff_highlight.py +160 -0
  656. novacode_cli/ui/execution.py +1396 -0
  657. novacode_cli/ui/exit_summary.py +65 -0
  658. novacode_cli/ui/hitl_approval.py +196 -0
  659. novacode_cli/ui/input_preparation.py +2 -0
  660. novacode_cli/ui/interrupt_handlers.py +215 -0
  661. novacode_cli/ui/question_prompt.py +151 -0
  662. novacode_cli/ui/status_phrases.py +157 -0
  663. novacode_cli/ui/stream_preview.py +66 -0
  664. novacode_cli/ui/streaming.py +2 -0
  665. novacode_cli/ui/subagent_tracking.py +2 -0
  666. novacode_cli/ui/ui_elements.py +1585 -0
  667. novacode_cli/ui/web_workspace.py +244 -0
  668. novacode_cli/ui_events.py +299 -0
  669. novacode_cli/update_ui.py +243 -0
  670. novacode_cli/updates.py +489 -0
  671. novacode_cli/utils/backend_patches.py +399 -0
  672. novacode_cli/utils/backend_paths.py +467 -0
  673. novacode_cli/utils/lazy_providers.py +51 -0
  674. novacode_cli/utils/model_info.py +223 -0
  675. novacode_cli/utils/pdf_extraction.py +183 -0
  676. novacode_cli/utils/startup.py +29 -0
  677. novacode_cli/utils/tasks.py +42 -0
  678. novacode_cli/utils/windows.py +9 -0
  679. novacode_cli/vixie/hook_adapter.py +127 -0
  680. novacode_cli/vixie/server.py +375 -0
  681. novacode_cli/wiki/__init__.py +15 -0
  682. novacode_cli/wiki/ask.py +56 -0
  683. novacode_cli/wiki/file.py +71 -0
  684. novacode_cli/wiki/ingest.py +184 -0
  685. novacode_cli/wiki/manager.py +261 -0
  686. novacode_cli-1.2.0.dist-info/METADATA +271 -0
  687. novacode_cli-1.2.0.dist-info/RECORD +691 -0
  688. novacode_cli-1.2.0.dist-info/WHEEL +5 -0
  689. novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
  690. novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
  691. novacode_cli-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,806 @@
1
+ """Load rarely used tools and subagents on demand, not on every model call.
2
+
3
+ Tool schemas and specialist rosters load on demand, independently of provider:
4
+
5
+ * Only **core** tools (file, shell, todos, plan mode, search), the user's
6
+ **most-used** tools (Hermes ``tool_stats``, read once per session) and tools
7
+ already **loaded** in this thread are bound.
8
+ * Every subagent except the always-listed general-purpose one drops out of the
9
+ ``task`` description the same way.
10
+ * ``search_tools`` finds the rest (hybrid search, shared with skills) and loads
11
+ them: a tool is loaded once a ``search_tools`` result names it or the model
12
+ has called it. Successful structured results are matched to their calls and
13
+ retained in checkpoints. Compaction drops visibility when that evidence leaves
14
+ context; rediscovery restores it. Search uses the owning execution's inventory.
15
+
16
+ Every tool stays registered with the graph, so a call to one that is not bound
17
+ still executes; only the schemas sent to the model shrink.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import logging
23
+ import re
24
+ import time
25
+ from difflib import get_close_matches
26
+ from fnmatch import fnmatchcase
27
+ from functools import partial, update_wrapper
28
+ from typing import TYPE_CHECKING, Any
29
+
30
+ from langchain.agents.middleware import AgentMiddleware
31
+ from langchain.tools import ToolRuntime # noqa: TC002 - Pydantic resolves runtime annotations.
32
+ from langchain_core.messages import AIMessage, SystemMessage, ToolMessage
33
+ from langchain_core.tools import StructuredTool
34
+ from pydantic import ConfigDict
35
+
36
+ from novacode_cli.agents.provider_caching import (
37
+ breakpoint_count,
38
+ native_tool_search,
39
+ public_provider,
40
+ unsupported_native_feature,
41
+ )
42
+ from novacode_cli.agents.tool_search_variants import (
43
+ BM25_TOOL,
44
+ REGEX_TOOL,
45
+ SEARCH_VARIANTS,
46
+ BM25Search,
47
+ SearchInput,
48
+ parameter_text,
49
+ regex_matches,
50
+ )
51
+ from novacode_cli.config.browser_mode import (
52
+ browser_mode_enabled,
53
+ browser_tool_allowed,
54
+ trusted_browser_servers,
55
+ )
56
+ from novacode_cli.skills.retrieval import MIN_COSINE, first_sentence, get_index
57
+
58
+ if TYPE_CHECKING:
59
+ from collections.abc import Awaitable, Callable, Sequence
60
+
61
+ from langchain.agents.middleware.types import ModelRequest, ModelResponse
62
+ from langchain_core.tools import BaseTool
63
+
64
+ from novacode_cli.skills.refreshing_middleware import RefreshingSkillsMiddleware
65
+
66
+ logger = logging.getLogger("nova.tool_search")
67
+
68
+ #: Always bound: the tools a coding turn cannot do without. Chosen from real
69
+ #: use (2026-10 audit: 5,089 calls over 40 threads, 93% of them shell,
70
+ #: read_file, edit_file and grep); a tool called a few times a month is cheaper
71
+ #: as one search_tools round-trip than as a schema resent on every call.
72
+ #: `think`, `code_search`, `web_search`, `eval`, `execute` (locally the same
73
+ #: shell as `shell`), `skills_read_resource`, the rarer async and artifact
74
+ #: tools and `list_waiters` are found by search.
75
+ CORE_TOOLS = frozenset(
76
+ {
77
+ "ls",
78
+ "read_file",
79
+ "write_file",
80
+ "edit_file",
81
+ "glob",
82
+ "grep",
83
+ "shell",
84
+ "bash",
85
+ "write_todos",
86
+ "task",
87
+ "ask_user_question",
88
+ "enter_plan_mode",
89
+ "exit_plan_mode",
90
+ "SendMessage",
91
+ "list_sessions",
92
+ "EnterWorktreeTool",
93
+ "ExitWorktreeTool",
94
+ "fetch_url",
95
+ "skills_search",
96
+ "skills_load",
97
+ "search_tools",
98
+ REGEX_TOOL,
99
+ BM25_TOOL,
100
+ "memory_search",
101
+ # The prompt delegates background work proactively and collects it with
102
+ # one short wait, so starting, checking and waiting stay bound.
103
+ "start_async_task",
104
+ "check_async_task",
105
+ "wait_for_task",
106
+ # The prompt asks for artifacts proactively.
107
+ "create_artifact",
108
+ # Surface the opt-in guest and its readiness without loading input schemas.
109
+ "computer_status",
110
+ "knowledge_lookup",
111
+ }
112
+ )
113
+ #: Never bound by use alone: `execute` duplicates `shell` locally (both run
114
+ #: through Nova's shell), so a model that used it is better steered to `shell`.
115
+ NEVER_FREQUENT = frozenset({"execute"})
116
+ #: Also bound: up to this many of the user's most-used other tools...
117
+ FREQUENT_TOOLS = 10
118
+ #: ...called at least this often in the last FREQUENT_DAYS days. All-time counts
119
+ #: kept tools from a month-old workflow (Serena, Playwright) on every call.
120
+ FREQUENT_MIN_USES = 20
121
+ FREQUENT_DAYS = 30
122
+
123
+ #: Shorter descriptions for third-party tools whose defaults are long (the
124
+ #: rules live in the system prompt). ``write_todos``: deepagents' 3.9k-char
125
+ #: text, which also contradicts Nova's one-item-in-progress rule.
126
+ DESCRIPTIONS = {
127
+ "write_todos": (
128
+ "Create or replace the task list for multi-step work: a list of todos, each "
129
+ "with `content` and a `status` of pending, in_progress or completed. Follow "
130
+ "the rules in <todo_management>. Never call it twice in parallel."
131
+ ),
132
+ }
133
+
134
+ LOADED_MARKER = "Loaded tools:"
135
+ #: Tools whose result can load others with a LOADED_MARKER line: the search,
136
+ #: and create_artifact, whose natural next step is update_artifact.
137
+ LOADING_TOOLS = frozenset({"search_tools", "tool_search", "create_artifact"}) | SEARCH_VARIANTS
138
+ MAX_SEARCH_LIMIT = 50
139
+ MAX_ANTHROPIC_BREAKPOINTS = 4
140
+ MAX_INVENTORY_CHARS = 2_000
141
+ MAX_INVENTORY_GROUPS = 10
142
+ MAX_GROUP_CHARS = 1_000
143
+ LOAD_RECORD = "nova_tool_search"
144
+
145
+
146
+ class RuntimeSearchInput(SearchInput):
147
+ """Runtime inventory is injected by the graph, never supplied by the model."""
148
+
149
+ model_config = ConfigDict(arbitrary_types_allowed=True)
150
+ runtime: ToolRuntime
151
+
152
+
153
+ def _name(tool: Any) -> str:
154
+ if isinstance(tool, dict):
155
+ return tool.get("name") or tool.get("function", {}).get("name", "")
156
+ return getattr(tool, "name", "")
157
+
158
+
159
+ def _description(tool: Any) -> str:
160
+ if isinstance(tool, dict):
161
+ return tool.get("description") or tool.get("function", {}).get("description", "")
162
+ return getattr(tool, "description", "") or ""
163
+
164
+
165
+ def _with_description(tool: BaseTool | dict, description: str) -> BaseTool | dict:
166
+ """Copy descriptions without mutating registered objects or function schemas."""
167
+ if isinstance(tool, dict):
168
+ if isinstance(tool.get("function"), dict):
169
+ return {**tool, "function": {**tool["function"], "description": description}}
170
+ return {**tool, "description": description}
171
+ return tool.model_copy(update={"description": description})
172
+
173
+
174
+ def loaded_capabilities(messages: list[Any]) -> set[tuple[str, str]]:
175
+ """Typed schema visibility from calls and their successful structured results.
176
+
177
+ Old text-only discovery reports remain readable, but require rediscovery.
178
+ A marker embedded in tool output is never a loading instruction.
179
+ """
180
+ loaded: set[tuple[str, str]] = set()
181
+ calls: dict[str, str] = {}
182
+ for msg in messages:
183
+ if isinstance(msg, AIMessage):
184
+ for call in msg.tool_calls or []:
185
+ call_id = call.get("id")
186
+ if isinstance(call_id, str):
187
+ calls[call_id] = call["name"]
188
+ loaded.add(("tool", call["name"]))
189
+ if call["name"] in {"task", "start_async_task"}:
190
+ agent = call.get("args", {}).get("subagent_type")
191
+ if isinstance(agent, str):
192
+ loaded.add(("subagent", agent))
193
+ elif isinstance(msg, ToolMessage):
194
+ expected = calls.pop(msg.tool_call_id, None)
195
+ if (
196
+ msg.status == "error"
197
+ or msg.name not in LOADING_TOOLS
198
+ or expected != msg.name
199
+ or not isinstance(msg.artifact, dict)
200
+ ):
201
+ continue
202
+ record = msg.artifact.get(LOAD_RECORD)
203
+ if (
204
+ not isinstance(record, dict)
205
+ or type(record.get("version")) is not int
206
+ or record["version"] != 1
207
+ ):
208
+ continue
209
+ capabilities = record.get("capabilities")
210
+ if not isinstance(capabilities, list):
211
+ continue
212
+ for capability in capabilities:
213
+ if (
214
+ isinstance(capability, dict)
215
+ and isinstance(capability.get("kind"), str)
216
+ and capability.get("kind") in {"tool", "subagent"}
217
+ and isinstance(capability.get("name"), str)
218
+ and capability["name"]
219
+ ):
220
+ loaded.add((capability["kind"], capability["name"]))
221
+ return loaded
222
+
223
+
224
+ def loaded_names(messages: list[Any]) -> set[str]:
225
+ """Compatibility projection; binding uses typed capabilities."""
226
+ return {name for _, name in loaded_capabilities(messages)}
227
+
228
+
229
+ def _group(names: list[str]) -> str:
230
+ """Bounded capability groups; full member names stay behind search."""
231
+ groups: dict[str, list[str]] = {}
232
+ for name in sorted(names):
233
+ prefix = name.split("_", 1)[0] if "_" in name else ""
234
+ groups.setdefault(prefix, []).append(name)
235
+ lines: list[str] = []
236
+ shown = 0
237
+ for prefix, members in groups.items():
238
+ label = f"{prefix}_*" if prefix else "other names"
239
+ line = f"- `{label[:80]}` ({len(members)} tools)"
240
+ if len(lines) >= MAX_INVENTORY_GROUPS or sum(map(len, lines)) + len(line) > MAX_GROUP_CHARS:
241
+ break
242
+ lines.append(line)
243
+ shown += len(members)
244
+ if shown < len(names):
245
+ lines.append(f"- {len(names) - shown} further tools; search to inspect.")
246
+ return "\n".join(lines)
247
+
248
+
249
+ class _AccountingRequest:
250
+ """Just enough of a ModelRequest for :func:`sent_tools`."""
251
+
252
+ def __init__(self, **fields: Any) -> None:
253
+ self.__dict__.update(fields)
254
+
255
+ def override(self, **changes: Any) -> _AccountingRequest:
256
+ return _AccountingRequest(**{**self.__dict__, **changes})
257
+
258
+
259
+ def sent_tools(tools: Sequence[Any], messages: Sequence[Any]) -> list[Any]:
260
+ """The schemas a request binds for ``messages``: selected and trimmed as sent.
261
+
262
+ For accounting before any request exists (``/context`` at startup), where the
263
+ registered schemas overstate the request several times: the ``task`` tool
264
+ alone lists every subagent (~39k chars) until this middleware trims it. The
265
+ deferred roster is read back from the ``task`` and ``start_async_task``
266
+ descriptions; the user's frequently used tools are left out.
267
+ """
268
+ from novacode_cli.agents.default_subagents.subagents import LISTED_SUBAGENTS
269
+
270
+ def roster(name: str) -> list[str]:
271
+ tool = next((t for t in tools if _name(t) == name), None)
272
+ if tool is None:
273
+ return []
274
+ lines = _description(tool).splitlines()
275
+ return [m.group(1) for line in lines if (m := re.match(r"^[ \t]*- ([^:\s]+):", line))]
276
+
277
+ middleware = ToolSearchMiddleware(
278
+ deferred_subagents={n: "" for n in roster("task") if n not in LISTED_SUBAGENTS},
279
+ async_subagents=set(roster("start_async_task")),
280
+ )
281
+ middleware._frequent = set()
282
+ request = _AccountingRequest(
283
+ tools=list(tools), messages=list(messages), system_message=SystemMessage(""), state={},
284
+ model=None, model_settings={},
285
+ )
286
+ return list(middleware._apply(request).tools) # type: ignore[arg-type]
287
+
288
+
289
+ class ToolSearchMiddleware(AgentMiddleware):
290
+ """Bind core + frequent + loaded tools; ``search_tools`` loads the rest."""
291
+
292
+ def __init__(
293
+ self,
294
+ *,
295
+ deferred_subagents: dict[str, str] | None = None,
296
+ async_subagents: set[str] | None = None,
297
+ skills: RefreshingSkillsMiddleware | None = None,
298
+ ) -> None:
299
+ """``deferred_subagents``: name -> description, hidden from ``task`` until found."""
300
+ super().__init__()
301
+ self._deferred_subagents = deferred_subagents or {}
302
+ self._async_subagents = async_subagents or set()
303
+ self._skills = skills
304
+ self._frequent: set[str] | None = None # read once, then frozen
305
+ self._bm25 = BM25Search()
306
+ self._portable_routes: set[tuple[str, str]] = set()
307
+ self.tools = [
308
+ StructuredTool.from_function(
309
+ func=update_wrapper(partial(self._search, mode=mode), self._search),
310
+ coroutine=update_wrapper(partial(self._asearch, mode=mode), self._asearch),
311
+ name=name,
312
+ args_schema=RuntimeSearchInput,
313
+ response_format="content_and_artifact",
314
+ description=description,
315
+ )
316
+ for name, mode, description in (
317
+ (
318
+ "search_tools",
319
+ "hybrid",
320
+ "Find tools/subagents by capability, exact/partial names or wildcards. "
321
+ "Searches names, descriptions and parameters. query='*' browses; limit and "
322
+ "offset page results. Found schemas load next step; approvals still apply.",
323
+ ),
324
+ (
325
+ REGEX_TOOL,
326
+ "regex",
327
+ "Find tools/subagents using case-insensitive Python regex over names, "
328
+ "descriptions and parameters. Maximum pattern length: 200 characters. "
329
+ "Use query, limit and offset to page results.",
330
+ ),
331
+ (
332
+ BM25_TOOL,
333
+ "bm25",
334
+ "Find tools/subagents using BM25 keywords over names, descriptions and "
335
+ "parameters without embeddings. Maximum query length: 500 characters. "
336
+ "Use query, limit and offset to page results.",
337
+ ),
338
+ )
339
+ ]
340
+
341
+ def _catalog_for(self, tools: Sequence[Any]) -> list[dict]:
342
+ """Discovery metadata belongs to this invocation's current inventory."""
343
+ browsing = browser_mode_enabled()
344
+ trusted = trusted_browser_servers() if browsing else frozenset()
345
+ return [
346
+ {
347
+ "name": _name(tool),
348
+ "description": _description(tool),
349
+ "kind": "tool",
350
+ "parameters": parameter_text(tool),
351
+ }
352
+ for tool in sorted(tools, key=_name)
353
+ if _name(tool)
354
+ and (
355
+ not browsing
356
+ or browser_tool_allowed(
357
+ _name(tool),
358
+ metadata=tool.get("metadata")
359
+ if isinstance(tool, dict)
360
+ else getattr(tool, "metadata", None),
361
+ trusted_servers=trusted,
362
+ )
363
+ )
364
+ ] + [
365
+ {"name": name, "description": description, "kind": "subagent"}
366
+ for name, description in sorted(self._deferred_subagents.items())
367
+ if not browsing
368
+ ]
369
+
370
+ def _search(
371
+ self,
372
+ query: str,
373
+ runtime: ToolRuntime,
374
+ limit: int = 5,
375
+ offset: int = 0,
376
+ *,
377
+ mode: str = "hybrid",
378
+ ) -> tuple[str, dict]:
379
+ """Use the owning run's inventory, including its activated skill aliases."""
380
+ tools = {_name(tool): tool for tool in runtime.tools}
381
+ if self._skills is not None:
382
+ resolved = self._skills._resolve_tools(runtime.state.get("messages", []), runtime)
383
+ for name, tool in resolved.items():
384
+ tools.setdefault(name, tool)
385
+ return self._tool_search(
386
+ query,
387
+ limit,
388
+ offset,
389
+ catalog=self._catalog_for(list(tools.values())),
390
+ _mode=mode,
391
+ )
392
+
393
+ async def _asearch(
394
+ self,
395
+ query: str,
396
+ runtime: ToolRuntime,
397
+ limit: int = 5,
398
+ offset: int = 0,
399
+ *,
400
+ mode: str = "hybrid",
401
+ ) -> tuple[str, dict]:
402
+ """Resolve async skill aliases in the same invocation context."""
403
+ tools = {_name(tool): tool for tool in runtime.tools}
404
+ if self._skills is not None:
405
+ resolved = await self._skills._aresolve_tools(
406
+ runtime.state.get("messages", []), runtime
407
+ )
408
+ for name, tool in resolved.items():
409
+ tools.setdefault(name, tool)
410
+ return self._tool_search(
411
+ query,
412
+ limit,
413
+ offset,
414
+ catalog=self._catalog_for(list(tools.values())),
415
+ _mode=mode,
416
+ )
417
+
418
+ def _search_matches(self, query: str, catalog: list[dict]) -> list[dict]:
419
+ """Rank the full catalog, retaining exact names ahead of semantic hits."""
420
+ tokens = re.findall(r"[\w.:-]+", query.casefold())
421
+ names = {item["name"].casefold() for item in catalog}
422
+ exact = [item for item in catalog if item["name"].casefold() in tokens]
423
+ patterns = [
424
+ word.strip("`\"'") for word in re.split(r"[,\s]+", query.casefold()) if "*" in word
425
+ ]
426
+ if patterns:
427
+ return [
428
+ item
429
+ for item in catalog
430
+ if item in exact
431
+ or any(fnmatchcase(item["name"].casefold(), pattern) for pattern in patterns)
432
+ ]
433
+ if tokens and set(tokens) <= names:
434
+ return exact
435
+ normalized = re.sub(r"[^\w]+|_", " ", query.casefold()).strip()
436
+ partial = [
437
+ item
438
+ for item in catalog
439
+ if normalized
440
+ and normalized in re.sub(r"[^\w]+|_", " ", item["name"].casefold()).strip()
441
+ ]
442
+ fuzzy_names = get_close_matches(query.casefold(), sorted(names), n=3, cutoff=0.8)
443
+ fuzzy = [item for item in catalog if item["name"].casefold() in fuzzy_names]
444
+ try:
445
+ ranked = [
446
+ item
447
+ for item, cosine, bm25 in get_index(catalog).search(query, k=len(catalog))
448
+ if bm25 > 0 or cosine >= MIN_COSINE
449
+ ]
450
+ except Exception: # noqa: BLE001 - discovery still works without the optional index
451
+ logger.debug("Tool retrieval index unavailable; using lexical search", exc_info=True)
452
+ ranked = [
453
+ item
454
+ for item in catalog
455
+ if set(tokens)
456
+ & set(
457
+ re.findall(
458
+ r"[\w]+",
459
+ (
460
+ item["name"]
461
+ + " "
462
+ + item["description"]
463
+ + " "
464
+ + item.get("parameters", "")
465
+ ).casefold(),
466
+ )
467
+ )
468
+ ]
469
+ hits: dict[tuple[str, str], dict] = {}
470
+ for item in [*exact, *partial, *fuzzy, *ranked]:
471
+ hits.setdefault((item["kind"], item["name"]), item)
472
+ return list(hits.values())
473
+
474
+ def _tool_search(
475
+ self,
476
+ query: str,
477
+ limit: int = 5,
478
+ offset: int = 0,
479
+ *,
480
+ catalog: list[dict],
481
+ _mode: str = "hybrid",
482
+ ) -> tuple[str, dict]:
483
+ """Find and load available tools and subagents.
484
+
485
+ Args:
486
+ query: A capability, exact or partial names, or a wildcard; '*' lists everything.
487
+ limit: Results per page (1-50).
488
+ offset: Number of matches to skip to reach another page.
489
+ catalog: Metadata from the owning invocation's current tool inventory.
490
+ """
491
+ if not 1 <= limit <= MAX_SEARCH_LIMIT or offset < 0:
492
+ message = "limit must be between 1 and 50, and offset must be nonnegative."
493
+ raise ValueError(message)
494
+ query = query if _mode == "regex" else query.strip()
495
+ if not query:
496
+ return (
497
+ "Enter a capability or tool name, or use query='*' to browse all available tools."
498
+ ), {}
499
+ if not catalog:
500
+ return "No tools are currently registered for discovery.", {}
501
+ try:
502
+ if _mode == "regex":
503
+ matches = regex_matches(catalog, query)
504
+ elif _mode == "bm25":
505
+ matches = self._bm25.search(catalog, query)
506
+ else:
507
+ matches = self._search_matches(query, catalog)
508
+ except (ValueError, re.error, TimeoutError) as exc:
509
+ return f"Tool search failed: {exc}", {}
510
+ if not matches:
511
+ return (
512
+ f"No registered tools match {query!r}. "
513
+ "Try a shorter name, a server prefix, or query='*' to browse."
514
+ ), {}
515
+ hits = matches[offset : offset + limit]
516
+ if not hits:
517
+ return (
518
+ f"There are {len(matches)} matches. Retry with offset smaller than {len(matches)}."
519
+ ), {}
520
+ tools = [h for h in hits if h["kind"] == "tool"]
521
+ agents = [h for h in hits if h["kind"] == "subagent"]
522
+ lines = [f"- `{h['name']}`: {first_sentence(h['description'], 200)}" for h in tools]
523
+ for agent in agents:
524
+ name = agent["name"]
525
+ dispatch = "start_async_task" if name in self._async_subagents else "task"
526
+ lines.append(
527
+ f'- subagent `{name}` (use `{dispatch}(subagent_type="{name}")`): '
528
+ f"{first_sentence(agent['description'], 200)}"
529
+ )
530
+ loaded = ", ".join(h["name"] for h in hits)
531
+ result = f"{LOADED_MARKER} {loaded}\n" + "\n".join(lines)
532
+ if offset or len(matches) > len(hits):
533
+ result += f"\nShowing {offset + 1}-{offset + len(hits)} of {len(matches)} matches."
534
+ if offset + len(hits) < len(matches):
535
+ result += (
536
+ f" Continue with the same query, limit={limit}, offset={offset + len(hits)}."
537
+ )
538
+ return result, {
539
+ LOAD_RECORD: {
540
+ "version": 1,
541
+ "capabilities": [{"kind": hit["kind"], "name": hit["name"]} for hit in hits],
542
+ }
543
+ }
544
+
545
+ # ── binding ────────────────────────────────────────────────────────────
546
+
547
+ async def _read_frequent(self, request: ModelRequest) -> None:
548
+ """Rank tools by recent use: Hermes's per-session ``tool_history``."""
549
+ if self._frequent is not None:
550
+ return
551
+ self._frequent = set()
552
+ store = getattr(request.runtime, "store", None)
553
+ if store is None:
554
+ return
555
+ since = time.time() - FREQUENT_DAYS * 86400
556
+ uses: dict[str, int] = {}
557
+ try:
558
+ spaces = await store.alist_namespaces(
559
+ prefix=("nova",), suffix=("tool_history",), limit=1000
560
+ )
561
+ # Filtered here too: Nova's stdlib fallback store ignores prefix/suffix.
562
+ for space in (s for s in spaces if s[:1] == ("nova",) and s[-1:] == ("tool_history",)):
563
+ item = await store.aget(space, "history")
564
+ for entry in ((item.value or {}).get("entries") or []) if item else []:
565
+ name = entry.get("tool")
566
+ if name and float(entry.get("timestamp") or 0) >= since:
567
+ uses[name] = uses.get(name, 0) + 1
568
+ except Exception: # noqa: BLE001 — ranking is a nicety, never break a turn
569
+ logger.debug("tool_history unavailable", exc_info=True)
570
+ return
571
+ ranked = sorted(
572
+ (n for n in uses if n not in CORE_TOOLS and n not in NEVER_FREQUENT),
573
+ key=lambda n: (-uses[n], n),
574
+ )
575
+ self._frequent = {n for n in ranked[:FREQUENT_TOOLS] if uses[n] >= FREQUENT_MIN_USES}
576
+
577
+ def _trim_task(self, tool: Any, hidden: list[str]) -> Any:
578
+ """Drop hidden subagents from the ``task`` description (a request-local copy).
579
+
580
+ One pass over the description's lines: with the whole roster deferred
581
+ (~90 names) a per-name regex would rescan the whole string 90 times on
582
+ every model call. Matching the ``- name:`` prefix keeps the agent name
583
+ authoritative even when a description contains regex metacharacters.
584
+ """
585
+ hidden_set = set(hidden)
586
+ kept: list[str] = []
587
+ skip_lines = 0
588
+ for line in _description(tool).splitlines():
589
+ if skip_lines:
590
+ skip_lines -= 1
591
+ continue
592
+ match = re.match(r"^[ \t]*- ([^:]+):", line)
593
+ if match and match.group(1).strip() in hidden_set:
594
+ name = match.group(1).strip()
595
+ # deepagents inserts each source description verbatim after
596
+ # the roster heading. Remove its complete block, not just line 1.
597
+ skip_lines = max(0, len(self._deferred_subagents[name].splitlines()) - 1)
598
+ continue
599
+ kept.append(line)
600
+ text = "\n".join(kept)
601
+ text += (
602
+ f"\n\n{len(hidden)} more specialist subagents are not listed here: "
603
+ "`search_tools` finds them by what they do."
604
+ )
605
+ return _with_description(tool, text)
606
+
607
+ def _compact_async_roster(self, tool: Any) -> Any:
608
+ """One line per agent in ``start_async_task``'s roster (a request-local copy).
609
+
610
+ deepagents lists each async agent with its whole description, "Use this
611
+ agent when:" bullets included (~6.5k chars for 13 agents). The names and
612
+ first sentences keep proactive delegation possible; ``search_tools``
613
+ returns any agent's full description.
614
+ """
615
+ lines, in_roster = [], False
616
+ for line in _description(tool).splitlines():
617
+ if line.startswith("Available async agent types"):
618
+ in_roster = True
619
+ lines.append(line)
620
+ continue
621
+ if in_roster and line.startswith("#"):
622
+ in_roster = False
623
+ lines.append("")
624
+ if not in_roster:
625
+ lines.append(line)
626
+ continue
627
+ match = re.match(r"^- ([^:\s]+): (.*)", line)
628
+ if match and match.group(1) in self._async_subagents:
629
+ lines.append(f"- {match.group(1)}: {first_sentence(match.group(2), 160)}")
630
+ return _with_description(tool, "\n".join(lines))
631
+
632
+ def _shorten(self, tool: Any, hidden: list[str]) -> Any:
633
+ """The request-local copy of a bound tool with its description trimmed."""
634
+ name = _name(tool)
635
+ if name == "task" and hidden:
636
+ return self._trim_task(tool, hidden)
637
+ if name == "start_async_task" and self._async_subagents:
638
+ return self._compact_async_roster(tool)
639
+ if name in DESCRIPTIONS:
640
+ return _with_description(tool, DESCRIPTIONS[name])
641
+ return tool
642
+
643
+ def _apply(self, request: ModelRequest) -> ModelRequest:
644
+ from novacode_cli.config.nova_config import NovaConfig
645
+
646
+ if NovaConfig().get_code_mode_enabled() and any(_name(t) == "eval" for t in request.tools):
647
+ from novacode_cli.agents.code_mode import code_mode_surface
648
+
649
+ # Provider search must never reintroduce the host-only inventory.
650
+ return request.override(tools=code_mode_surface(request.tools))
651
+ capabilities = loaded_capabilities(request.messages)
652
+ loaded = {name for kind, name in capabilities if kind == "tool"}
653
+ # Skill snapshots activate their optional schemas without a second search.
654
+ # Ordinary registered tools remain independently searchable.
655
+ for message in request.messages:
656
+ extra = getattr(message, "additional_kwargs", {})
657
+ snapshot = extra.get("skill", {}) if extra.get("lc_source") == "pinned_skill" else {}
658
+ if (
659
+ isinstance(message, ToolMessage)
660
+ and message.name == "skills_load"
661
+ and message.status != "error"
662
+ and isinstance(message.artifact, dict)
663
+ ):
664
+ snapshot = message.artifact.get("skill", {})
665
+ if isinstance(snapshot, dict):
666
+ include = snapshot.get("include_tools", [])
667
+ if isinstance(include, list):
668
+ loaded.update(name for name in include if isinstance(name, str))
669
+ keep = set(CORE_TOOLS) | (self._frequent or set()) | loaded
670
+ keep.update(
671
+ _name(tool)
672
+ for tool in request.tools
673
+ if (getattr(tool, "metadata", None) or {}).get("_nova_skill_active")
674
+ )
675
+ bound: list[Any] = []
676
+ deferred: list[Any] = []
677
+ for tool in sorted(request.tools, key=_name):
678
+ (bound if _name(tool) in keep else deferred).append(tool)
679
+ browsing = browser_mode_enabled()
680
+ # Search results carry specialist details; discovery never rewrites task's schema.
681
+ hidden = [] if browsing else sorted(self._deferred_subagents)
682
+ bound = [self._shorten(t, hidden) for t in bound]
683
+ optional_names = [
684
+ _name(tool) for tool in request.tools if _name(tool) and _name(tool) not in CORE_TOOLS
685
+ ]
686
+ if not optional_names and not hidden:
687
+ return self._native(request.override(tools=bound), request.tools)
688
+ note = (
689
+ "\n\n## More tools (load on demand)\n\n"
690
+ f"{len(optional_names)} optional tools and "
691
+ f"{0 if browsing else len(self._deferred_subagents)} "
692
+ "specialist subagents are searchable. Capability groups:\n"
693
+ f"{_group(optional_names)}\n\n"
694
+ 'Call search_tools("<capability>") or use exact names. query="*" browses; '
695
+ f'wildcards such as "{"*snapshot*" if browsing else "playwright_*"}", '
696
+ "limit and offset inspect further results. "
697
+ f"{REGEX_TOOL} supports regex; {BM25_TOOL} supports keyword search. "
698
+ "Search loads schemas, not permission. Normal approvals and execution policy "
699
+ "still apply. Never guess arguments: load the schema first. "
700
+ "After compaction, search again for schemas no longer available."
701
+ )[:MAX_INVENTORY_CHARS]
702
+ blocks = (
703
+ [dict(block) for block in request.system_message.content_blocks]
704
+ if request.system_message
705
+ else []
706
+ )
707
+ return self._native(
708
+ request.override(
709
+ tools=bound,
710
+ system_message=SystemMessage(content=[*blocks, {"type": "text", "text": note}]),
711
+ ),
712
+ request.tools,
713
+ )
714
+
715
+ @staticmethod
716
+ def _route_key(request: ModelRequest) -> tuple[str, str]:
717
+ metadata = getattr(request.model, "metadata", None) or {}
718
+ return str(metadata.get("nova_provider", "")), str(metadata.get("nova_model", ""))
719
+
720
+ def _native(self, request: ModelRequest, inventory: Sequence[Any]) -> ModelRequest:
721
+ """Keep native authorized inventories stable; portable routes bind discovered tools."""
722
+ if self._route_key(request) in self._portable_routes:
723
+ return request
724
+ route = native_tool_search(request.model)
725
+ settings = dict(request.model_settings or {})
726
+ if route == "anthropic":
727
+ blocks = [m.content for m in request.messages]
728
+ if request.system_message:
729
+ blocks.append(request.system_message.content)
730
+ explicit = breakpoint_count(blocks)
731
+ explicit += sum(
732
+ breakpoint_count(t if isinstance(t, dict) else getattr(t, "extras", None))
733
+ for t in inventory
734
+ )
735
+ if explicit < MAX_ANTHROPIC_BREAKPOINTS:
736
+ settings.setdefault("cache_control", {"type": "ephemeral"})
737
+ if not route:
738
+ return request.override(model_settings=settings)
739
+ from langchain_core.utils.function_calling import convert_to_openai_tool
740
+
741
+ always = CORE_TOOLS | (self._frequent or set())
742
+ tools = []
743
+ hidden = [] if browser_mode_enabled() else sorted(self._deferred_subagents)
744
+ for tool in sorted(inventory, key=_name):
745
+ if route == "anthropic" and _name(tool) == REGEX_TOOL:
746
+ continue
747
+ shortened = self._shorten(tool, hidden)
748
+ if not isinstance(shortened, dict):
749
+ extras = {**(getattr(shortened, "extras", None) or {})}
750
+ extras["defer_loading"] = _name(tool) not in always
751
+ tools.append(shortened.model_copy(update={"extras": extras}))
752
+ continue
753
+ definition = convert_to_openai_tool(shortened)
754
+ if "function" in definition:
755
+ definition = {**definition, "defer_loading": _name(tool) not in always}
756
+ tools.append(definition)
757
+ if route == "anthropic":
758
+ tools.append({"type": REGEX_TOOL, "name": "tool_search_tool_regex"})
759
+ else:
760
+ tools.append({"type": "tool_search"})
761
+ return request.override(tools=tools, model_settings=settings)
762
+
763
+ def wrap_model_call(
764
+ self,
765
+ request: ModelRequest,
766
+ handler: Callable[[ModelRequest], ModelResponse],
767
+ ) -> ModelResponse:
768
+ """Bind only core, frequent and loaded tools (sync: no usage ranking)."""
769
+ if self._frequent is None:
770
+ self._frequent = set()
771
+ try:
772
+ return handler(self._apply(request))
773
+ except Exception as exc:
774
+ if (
775
+ self._route_key(request) in self._portable_routes
776
+ or not (
777
+ native_tool_search(request.model)
778
+ or public_provider(request.model) == "anthropic"
779
+ )
780
+ or not unsupported_native_feature(exc)
781
+ ):
782
+ raise
783
+ self._portable_routes.add(self._route_key(request))
784
+ return handler(self._apply(request))
785
+
786
+ async def awrap_model_call(
787
+ self,
788
+ request: ModelRequest,
789
+ handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
790
+ ) -> ModelResponse:
791
+ """Bind only core, frequent and loaded tools."""
792
+ await self._read_frequent(request)
793
+ try:
794
+ return await handler(self._apply(request))
795
+ except Exception as exc:
796
+ if (
797
+ self._route_key(request) in self._portable_routes
798
+ or not (
799
+ native_tool_search(request.model)
800
+ or public_provider(request.model) == "anthropic"
801
+ )
802
+ or not unsupported_native_feature(exc)
803
+ ):
804
+ raise
805
+ self._portable_routes.add(self._route_key(request))
806
+ return await handler(self._apply(request))