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,799 @@
1
+ """Context-window analysis: token breakdown, window sizing, compaction advice.
2
+
3
+ Private to the ``context`` package. Provides the deterministic, model-aware
4
+ analysis that backs ``ContextManager``: per-message token estimation, the
5
+ hardcoded model→window table (with Ollama dynamic detection as the first
6
+ source), and the compaction recommendation heuristics.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import logging
12
+ from dataclasses import dataclass, replace
13
+ from typing import TYPE_CHECKING, Any
14
+
15
+ if TYPE_CHECKING:
16
+ from langchain_core.messages import BaseMessage
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # Model context window sizes (input tokens). Approximate; may vary by version.
21
+ # Updated with actual values from Ollama (2026-04-05).
22
+ MODEL_CONTEXT_WINDOWS: dict[str, int] = {
23
+ # OpenAI models
24
+ "gpt-4": 128_000,
25
+ "gpt-4-turbo": 128_000,
26
+ "gpt-4-turbo-preview": 128_000,
27
+ "gpt-4o": 128_000,
28
+ "gpt-4o-mini": 128_000,
29
+ "gpt-5": 128_000,
30
+ "gpt-5-mini": 128_000,
31
+ "gpt-3.5-turbo": 16_000,
32
+ "gpt-3.5-turbo-16k": 16_000,
33
+ # Anthropic models — Claude 4.x
34
+ "claude-opus-4-8": 200_000,
35
+ "claude-sonnet-4-8": 200_000,
36
+ "claude-opus-4-7": 200_000,
37
+ "claude-sonnet-4-7": 200_000,
38
+ "claude-opus-4-6": 200_000,
39
+ "claude-sonnet-4-6": 200_000,
40
+ "claude-haiku-4-5-20251001": 200_000,
41
+ "claude-haiku-4-5": 200_000,
42
+ # Anthropic models — Claude 4.5
43
+ "claude-sonnet-4-5-20250929": 200_000,
44
+ "claude-opus-4-5-20251101": 200_000,
45
+ "claude-sonnet-4-5": 200_000,
46
+ "claude-opus-4-5": 200_000,
47
+ # Anthropic models — Claude 3.x
48
+ "claude-3-5-sonnet-20241022": 200_000,
49
+ "claude-3-5-haiku-20241022": 200_000,
50
+ "claude-3-opus-20240229": 200_000,
51
+ "claude-3-sonnet-20240229": 200_000,
52
+ "claude-3-haiku-20240307": 200_000,
53
+ # Google Gemini models
54
+ "gemini-3-pro-preview": 1_048_576, # 1M context
55
+ "gemini-3-flash-preview": 1_048_576, # 1M context
56
+ "gemini-2.0-flash-exp": 1_000_000,
57
+ "gemini-1.5-pro": 2_000_000,
58
+ "gemini-1.5-flash": 1_000_000,
59
+ "gemini-1.0-pro": 32_000,
60
+ # Ollama cloud models (actual values from Ollama)
61
+ "glm-5": 202_752,
62
+ "glm-4.7": 202_752,
63
+ "glm-4.6": 202_752,
64
+ "qwen3.5": 262_144, # 256K
65
+ "qwen3-coder": 262_144,
66
+ "qwen3-coder:480b-cloud": 262_144,
67
+ "qwen3-next:80b-cloud": 262_144,
68
+ "qwen3-vl:235b-instruct-cloud": 262_144,
69
+ "qwen3-vl:235b-cloud": 262_144,
70
+ "gemma4": 262_144,
71
+ "minimax-m2.7": 204_800,
72
+ "minimax-m2.5": 204_800,
73
+ "minimax-m2.1": 204_800,
74
+ "minimax-m2": 204_800,
75
+ "cogito-2.1:671b-cloud": 163_840,
76
+ "deepseek-v3.1:671b-cloud": 163_840,
77
+ "devstral-2:123b-cloud": 262_144,
78
+ "mistral-large-3:675b-cloud": 262_144,
79
+ "ministral-3:14b-cloud": 262_144,
80
+ "kimi-k2.5": 262_144,
81
+ "kimi-k2-thinking": 262_144,
82
+ "kimi-k2:1t-cloud": 262_144,
83
+ "nemotron-3-nano:30b-cloud": 1_048_576, # 1M context
84
+ # Ollama local models (actual values from Ollama)
85
+ "llama3.1": 131_072, # 128K
86
+ "llama3.2": 131_072, # 128K
87
+ "llama3": 131_072,
88
+ "mistral": 32_768, # 32K
89
+ "mixtral": 32_768,
90
+ "codellama": 16_000,
91
+ "glm-ocr": 131_072,
92
+ "qwen3": 40_960,
93
+ "qwen3:8b": 40_960,
94
+ "qwen3:1.7b": 40_960,
95
+ # SysML models
96
+ "SysML-V2-llama3.1": 131_072,
97
+ "SysML-V2-llama3.2": 131_072,
98
+ "Qwen3-4B-SysMLv2": 40_960,
99
+ "Qwen3-8B-SysMLv2": 40_960,
100
+ "Qwen3-SysMLv2": 40_960,
101
+ # GPT-OSS models
102
+ "gpt-oss:20b-cloud": 131_072,
103
+ "gpt-oss:120b-cloud": 131_072,
104
+ # Default fallback
105
+ "default": 128_000,
106
+ }
107
+
108
+ # Context usage thresholds for warnings
109
+ CONTEXT_WARNING_THRESHOLD = 0.75 # Yellow warning at 75%
110
+ CONTEXT_CRITICAL_THRESHOLD = 0.90 # Red warning at 90%
111
+
112
+ # deepagents' own default is this fraction, but Nova replaces it with the
113
+ # effective threshold below so internal compaction and `/context` agree.
114
+ LIB_SUMMARIZATION_FRACTION = 0.85
115
+
116
+ # Nova compacts between turns; the in-turn library middleware uses this same
117
+ # effective threshold, including the small-window reserve.
118
+ AUTO_COMPACT_THRESHOLD = 0.82
119
+
120
+ #: Tokens that must stay free at the moment the compaction decision is taken.
121
+ #: The check runs BETWEEN turns, so this has to cover everything that lands
122
+ #: before the next decision point: the reply the model is about to generate,
123
+ #: the next user message, the re-injected memory/skills blocks, and whatever
124
+ #: tool results that turn pulls in. Claude Code reserves ~33K of its 200K for
125
+ #: exactly this; a bare percentage does not, which is why 18% of a 40K Ollama
126
+ #: window (7.4K — less than one large file read) was not enough headroom.
127
+ MIN_RESERVE_TOKENS = 8_000
128
+
129
+
130
+ def compact_threshold_pct(context_window: int) -> float:
131
+ """Usage percentage at which to auto-compact for a given window.
132
+
133
+ The lower of :data:`AUTO_COMPACT_THRESHOLD` and "leave
134
+ :data:`MIN_RESERVE_TOKENS` free" — so large windows keep the familiar 82%
135
+ and small ones compact earlier instead of overflowing on the next turn.
136
+ """
137
+ if context_window <= 0:
138
+ return AUTO_COMPACT_THRESHOLD * 100
139
+ by_reserve = (context_window - MIN_RESERVE_TOKENS) / context_window * 100
140
+ # Floor at 50%: on a window smaller than ~16K the reserve is most of it, and
141
+ # compacting at 10% would just thrash. Such a model cannot really run an
142
+ # agent loop; the post-compaction check catches it and disables auto-compact.
143
+ return min(AUTO_COMPACT_THRESHOLD * 100, max(50.0, by_reserve))
144
+
145
+
146
+ # Where ``ContextCompactionMiddleware`` starts moving older tool results out and
147
+ # trimming old tool-call arguments (the cheap, deterministic stages that run
148
+ # BEFORE summarizing). Expressed as a fraction of the model's window and
149
+ # converted to an absolute token count at agent-build time.
150
+ #
151
+ # MUST stay below :data:`AUTO_COMPACT_THRESHOLD`: clearing tool results is far
152
+ # cheaper than replacing the conversation, so it must get its chance first.
153
+ # A fixed trigger (the previous 60_000) was window-independent and therefore
154
+ # silently never fired on the small Ollama windows in MODEL_CONTEXT_WINDOWS
155
+ # (e.g. ~40K), leaving those models with no tool-result reducer at all.
156
+ CONTEXT_EDIT_TRIGGER_FRACTION = 0.60
157
+
158
+
159
+ @dataclass
160
+ class ContextBreakdown:
161
+ """Detailed breakdown of context window usage.
162
+
163
+ Attributes:
164
+ system_prompt_tokens: Tokens used by the system prompt
165
+ user_memory_tokens: Tokens used by user agent.md
166
+ project_memory_tokens: Tokens used by project agent.md
167
+ tool_definitions_tokens: Tokens used by tool definitions
168
+ user_message_tokens: Tokens from user messages
169
+ assistant_message_tokens: Tokens from assistant messages
170
+ tool_result_tokens: Tokens from tool call results
171
+ total_tokens: Total tokens currently used
172
+ context_window_size: Maximum context window for the model
173
+ user_message_count: Number of user messages
174
+ assistant_message_count: Number of assistant messages
175
+ tool_call_count: Number of tool calls made
176
+ """
177
+
178
+ # Static/baseline content
179
+ system_prompt_tokens: int = 0
180
+ user_memory_tokens: int = 0
181
+ project_memory_tokens: int = 0
182
+ tool_definitions_tokens: int = 0
183
+
184
+ # Conversation content
185
+ user_message_tokens: int = 0
186
+ assistant_message_tokens: int = 0
187
+ tool_result_tokens: int = 0
188
+
189
+ # Totals
190
+ total_tokens: int = 0
191
+ context_window_size: int = 128_000
192
+
193
+ # Message counts
194
+ user_message_count: int = 0
195
+ assistant_message_count: int = 0
196
+ tool_call_count: int = 0
197
+
198
+ # MCP's share of the request (already inside the categories above)
199
+ mcp_tool_count: int = 0
200
+ mcp_tool_tokens: int = 0
201
+ mcp_prompt_tokens: int = 0
202
+
203
+ # What the cheap context strategies kept out of the request
204
+ snipped_tokens: int = 0
205
+ collapsed_tokens: int = 0
206
+ collapsed_turns: int = 0
207
+
208
+ @property
209
+ def baseline_tokens(self) -> int:
210
+ """Tokens used by static/baseline content (system prompt, memory, tools)."""
211
+ return (
212
+ self.system_prompt_tokens
213
+ + self.user_memory_tokens
214
+ + self.project_memory_tokens
215
+ + self.tool_definitions_tokens
216
+ )
217
+
218
+ @property
219
+ def conversation_tokens(self) -> int:
220
+ """Tokens used by conversation content (user/assistant/tool messages)."""
221
+ return self.user_message_tokens + self.assistant_message_tokens + self.tool_result_tokens
222
+
223
+ @property
224
+ def remaining_tokens(self) -> int:
225
+ """Tokens still available in context window."""
226
+ return max(0, self.context_window_size - self.total_tokens)
227
+
228
+ @property
229
+ def usage_percentage(self) -> float:
230
+ """Percentage of context window used."""
231
+ if self.context_window_size == 0:
232
+ return 0.0
233
+ return (self.total_tokens / self.context_window_size) * 100
234
+
235
+ @property
236
+ def is_warning(self) -> bool:
237
+ """Whether context usage has reached the warning threshold (75%)."""
238
+ return self.usage_percentage >= CONTEXT_WARNING_THRESHOLD * 100
239
+
240
+ @property
241
+ def is_critical(self) -> bool:
242
+ """Whether context usage has reached the critical threshold (90%)."""
243
+ return self.usage_percentage >= CONTEXT_CRITICAL_THRESHOLD * 100
244
+
245
+ @property
246
+ def should_auto_compact(self) -> bool:
247
+ """Whether Nova should compact now, BEFORE deepagents' backstop fires.
248
+
249
+ Shared with in-turn summarization, and lower on a small window where a
250
+ flat percentage leaves less free space than a large tool result needs
251
+ (:func:`compact_threshold_pct`).
252
+ """
253
+ return self.usage_percentage >= compact_threshold_pct(self.context_window_size)
254
+
255
+ def anchored_to(self, snapshot: dict[str, object]) -> ContextBreakdown:
256
+ """Use the last request's measured system prompt and tool schemas.
257
+
258
+ The startup baseline is the core prompt alone: the MCP server list, tool
259
+ discovery notes and memory injected per request were missing from it, and
260
+ scaling every category to the request's total spread them across the
261
+ conversation. With the snapshot's own figures, only the conversation
262
+ categories are scaled, into what is left.
263
+ """
264
+ def number(key: str) -> int:
265
+ value = snapshot.get(key)
266
+ return value if isinstance(value, int) else 0
267
+
268
+ total = number("estimated_input_tokens")
269
+ system = number("estimated_system_tokens")
270
+ schemas = number("estimated_schema_tokens")
271
+ if total <= 0 or not system:
272
+ return self.scaled_to(total) if total > 0 else self
273
+ rest = max(0, total - system - schemas)
274
+ conversation = replace(
275
+ self,
276
+ system_prompt_tokens=0,
277
+ user_memory_tokens=0,
278
+ project_memory_tokens=0,
279
+ tool_definitions_tokens=0,
280
+ ).scaled_to(rest) if self.conversation_tokens else replace(
281
+ self,
282
+ user_message_tokens=0,
283
+ assistant_message_tokens=0,
284
+ tool_result_tokens=0,
285
+ )
286
+ return replace(
287
+ conversation,
288
+ system_prompt_tokens=system,
289
+ user_memory_tokens=0,
290
+ project_memory_tokens=0,
291
+ tool_definitions_tokens=schemas,
292
+ total_tokens=system + schemas + conversation.conversation_tokens,
293
+ mcp_tool_count=number("mcp_tools"),
294
+ mcp_tool_tokens=number("mcp_schema_tokens"),
295
+ mcp_prompt_tokens=number("mcp_prompt_tokens"),
296
+ )
297
+
298
+ def scaled_to(self, total_tokens: int) -> ContextBreakdown:
299
+ """This breakdown with the categories rescaled to a known real total.
300
+
301
+ The categories are estimated at 4 chars/token, which runs ~20-30% light
302
+ on code and JSON tool arguments, while ``total_tokens`` can be the
303
+ provider's own count. Overriding only the total left the /context bars
304
+ summing to something other than the number printed above them. Scaling
305
+ does not make a category exact — it makes the parts agree with the
306
+ whole, which is what the bars are read for.
307
+ """
308
+ estimated = self.baseline_tokens + self.conversation_tokens
309
+ if total_tokens <= 0 or estimated <= 0:
310
+ return replace(self, total_tokens=max(0, total_tokens) or self.total_tokens)
311
+ fields = (
312
+ "system_prompt_tokens",
313
+ "user_memory_tokens",
314
+ "project_memory_tokens",
315
+ "tool_definitions_tokens",
316
+ "user_message_tokens",
317
+ "assistant_message_tokens",
318
+ "tool_result_tokens",
319
+ )
320
+ # Largest-remainder allocation keeps the displayed categories adding
321
+ # up exactly, even for small totals where independent rounding drifts.
322
+ numerators = {name: getattr(self, name) * total_tokens for name in fields}
323
+ scaled = {name: value // estimated for name, value in numerators.items()}
324
+ remainder = total_tokens - sum(scaled.values())
325
+ for name in sorted(fields, key=lambda name: numerators[name] % estimated, reverse=True)[
326
+ :remainder
327
+ ]:
328
+ scaled[name] += 1
329
+ return replace(self, **scaled, total_tokens=total_tokens)
330
+
331
+
332
+ @dataclass
333
+ class CompactionResult:
334
+ """Result of a conversation compaction operation.
335
+
336
+ Attributes:
337
+ success: Whether compaction completed successfully
338
+ original_tokens: Estimated tokens before compaction
339
+ new_tokens: Estimated tokens after compaction
340
+ tokens_saved: Tokens freed by compaction
341
+ messages_before: Number of messages before compaction
342
+ messages_after: Number of messages after compaction
343
+ summary: The generated summary text
344
+ learnings: Durable memory content persisted for this compaction ("" if none)
345
+ error: Error message if compaction failed
346
+ """
347
+
348
+ success: bool
349
+ original_tokens: int
350
+ new_tokens: int
351
+ tokens_saved: int
352
+ messages_before: int
353
+ messages_after: int
354
+ summary: str
355
+ learnings: str = ""
356
+ error: str | None = None
357
+
358
+
359
+ def _lookup_hardcoded_window(model_name: str) -> int:
360
+ """Resolve a model's architecture context window from the static table.
361
+
362
+ Tries exact, case-insensitive, substring, then model-family matches, and
363
+ falls back to the 128K default. This is the model's *trained* window; for
364
+ Ollama-served models it is bounded by ``num_ctx`` in
365
+ :func:`get_context_window_size`.
366
+ """
367
+ # Direct match in hardcoded configs
368
+ if model_name in MODEL_CONTEXT_WINDOWS:
369
+ return MODEL_CONTEXT_WINDOWS[model_name]
370
+
371
+ # Try lowercase match
372
+ model_lower = model_name.lower()
373
+ for key, size in MODEL_CONTEXT_WINDOWS.items():
374
+ if key.lower() == model_lower:
375
+ return size
376
+
377
+ # Partial match — check if any known model name is contained
378
+ for key, size in MODEL_CONTEXT_WINDOWS.items():
379
+ if key in model_name or key in model_lower:
380
+ return size
381
+ if model_name in key or model_lower in key.lower():
382
+ return size
383
+
384
+ # Check for model family prefixes
385
+ if "gpt-4" in model_lower:
386
+ return MODEL_CONTEXT_WINDOWS["gpt-4"]
387
+ if "gpt-3.5" in model_lower:
388
+ return MODEL_CONTEXT_WINDOWS["gpt-3.5-turbo"]
389
+ if "claude" in model_lower:
390
+ return 200_000 # Most Claude models have 200K
391
+ if "gemini" in model_lower:
392
+ return 1_000_000 # Conservative for Gemini
393
+ if "qwen" in model_lower or "ollama" in model_lower:
394
+ return 200_000 # Ollama default
395
+
396
+ return MODEL_CONTEXT_WINDOWS["default"]
397
+
398
+
399
+ #: What Nova works within when `context_budget_tokens` is not set. A window is
400
+ #: what a model accepts, not what a task needs: at 128K old tool results move out
401
+ #: at ~77K and the conversation is summarized at ~105K, which a long session
402
+ #: reaches rarely and a 1M window would never get near.
403
+ DEFAULT_CONTEXT_BUDGET_TOKENS = 128_000
404
+
405
+
406
+ def working_window(window: int, configured: object) -> int:
407
+ """The context Nova budgets against: the smaller of the window and the budget.
408
+
409
+ ``configured`` is the `context_budget_tokens` setting: unset means the
410
+ default budget, 0 means no budget (the whole window), anything else is the
411
+ budget. A window that is unknown (0) becomes the budget.
412
+ """
413
+ try:
414
+ budget = DEFAULT_CONTEXT_BUDGET_TOKENS if configured is None else int(configured)
415
+ except (TypeError, ValueError):
416
+ budget = DEFAULT_CONTEXT_BUDGET_TOKENS
417
+ if budget > 0 and (window <= 0 or budget < window):
418
+ return budget
419
+ return window
420
+
421
+
422
+ _budget_read: tuple[float, object] | None = None
423
+
424
+
425
+ def configured_budget() -> object:
426
+ """The ``context_budget_tokens`` setting, re-read at most every 5 seconds.
427
+
428
+ Reading the config costs ~8 ms (it parses the file), and a subagent's
429
+ compaction asks on every model call, on the loop the UI shares.
430
+ """
431
+ global _budget_read # noqa: PLW0603
432
+ import time
433
+
434
+ now = time.monotonic()
435
+ if _budget_read is None or now - _budget_read[0] > 5: # noqa: PLR2004
436
+ try:
437
+ from novacode_cli.config.nova_config import NovaConfig
438
+
439
+ value = NovaConfig().get("context_budget_tokens")
440
+ except Exception: # noqa: BLE001 - an unreadable config means the default budget
441
+ value = None
442
+ _budget_read = (now, value)
443
+ return _budget_read[1]
444
+
445
+
446
+ def get_context_window_size(model_name: str, use_dynamic: bool = True) -> int:
447
+ """The context Nova works within for a model: its window, capped by the budget.
448
+
449
+ This is the one number every part of context management measures against —
450
+ the ``/context`` bar, the auto-compact decision, the compaction middleware —
451
+ so the indicator and the behaviour cannot disagree. A 1M-token model used to
452
+ show 10% while compaction, sized from the budget, was already firing.
453
+ """
454
+ return working_window(model_window_size(model_name, use_dynamic), configured_budget())
455
+
456
+
457
+ def model_window_size(model_name: str, use_dynamic: bool = True) -> int:
458
+ """Get the *effective* context window size for a given model, before any budget.
459
+
460
+ For cloud API models (Claude/GPT/Gemini/o-series) this is the model's
461
+ published window. For Ollama-served models the effective window is
462
+ ``min(architecture_window, num_ctx)``: Nova loads Ollama models with a fixed
463
+ ``num_ctx`` (see :func:`~novacode_cli.context._dynamic.get_ollama_num_ctx`),
464
+ which is the real truncation limit. Reporting the raw architecture window
465
+ (e.g. 262K from ``ollama show``) overstated capacity when ``num_ctx`` was
466
+ smaller, and understated usage% — silently risking truncation. Conversely,
467
+ when ``num_ctx`` exceeds the model's trained window, the trained window is
468
+ the quality ceiling, so the smaller of the two is the honest number.
469
+
470
+ Args:
471
+ model_name: The name of the model (e.g., "gpt-4", "claude-3-opus", "glm-5:cloud").
472
+ use_dynamic: Whether to use dynamic detection from Ollama (default: True).
473
+
474
+ Returns:
475
+ The effective context window size in tokens (falls back to 128K default).
476
+ """
477
+ from novacode_cli.context._dynamic import is_ollama_cloud_model
478
+
479
+ cloud_api_prefixes = ("claude-", "gpt-", "gemini-", "o1", "o3", "o4")
480
+ is_cloud_api_model = any(model_name.lower().startswith(p) for p in cloud_api_prefixes)
481
+ # Ollama cloud models (`:cloud`) run on Ollama's servers — not loaded into
482
+ # local VRAM, so `ollama ps` won't list them and the local `num_ctx` cap
483
+ # doesn't apply. Their window is the model maximum from `ollama show`.
484
+ is_ollama_cloud = is_ollama_cloud_model(model_name)
485
+ # "Local model" == a model whose context is allocated in *our* VRAM.
486
+ is_local_ollama = not is_cloud_api_model and not is_ollama_cloud
487
+
488
+ if use_dynamic and is_local_ollama:
489
+ # Best source for a local model: the context Ollama ACTUALLY allocated
490
+ # for the loaded model (`ollama ps`). It may be clamped below the
491
+ # requested num_ctx to fit VRAM, so it's authoritative when present.
492
+ try:
493
+ from novacode_cli.context._dynamic import get_ollama_running_context
494
+
495
+ running = get_ollama_running_context(model_name)
496
+ if running:
497
+ logger.debug(f"Allocated context for {model_name}: {running:,} (ollama ps)")
498
+ return running
499
+ except Exception as e: # noqa: BLE001
500
+ logger.debug(f"ollama ps probe failed for {model_name}: {e}")
501
+
502
+ # Architecture / max window: dynamic `ollama show` probe (works for both
503
+ # local AND cloud Ollama models), else the static table.
504
+ arch_window: int | None = None
505
+ if use_dynamic and not is_cloud_api_model:
506
+ try:
507
+ from novacode_cli.context._dynamic import get_ollama_context_length
508
+
509
+ detected = get_ollama_context_length(model_name)
510
+ if detected:
511
+ arch_window = detected
512
+ logger.debug(f"Dynamic context for {model_name}: {detected:,} tokens (max)")
513
+ except Exception as e: # noqa: BLE001
514
+ logger.debug(
515
+ f"Dynamic context detection failed for {model_name}: {e}, falling back to hardcoded"
516
+ )
517
+ # Ollama didn't recognize it (arch_window is None) AND it isn't a cloud-API
518
+ # model → it's a gateway model (OpenCode / OpenRouter / other OpenAI-compatible
519
+ # endpoint) whose window the hardcoded table misses. Consult models.dev (the
520
+ # OpenCode-maintained catalog) for the REAL published window. Return it
521
+ # directly (uncapped): gateway models aren't VRAM-bound, so the local num_ctx
522
+ # cap must not apply — that mis-cap is what made OpenCode ctx% blow past 100%.
523
+ if arch_window is None and use_dynamic and not is_cloud_api_model:
524
+ try:
525
+ from novacode_cli.context._models_dev import get_models_dev_context
526
+
527
+ mdev = get_models_dev_context(model_name)
528
+ if mdev:
529
+ logger.debug(f"Context for {model_name}: {mdev:,} (models.dev)")
530
+ return mdev
531
+ except Exception as e: # noqa: BLE001
532
+ logger.debug(f"models.dev lookup failed for {model_name}: {e}")
533
+
534
+ if arch_window is None:
535
+ arch_window = _lookup_hardcoded_window(model_name)
536
+
537
+ # Cloud (API or Ollama-cloud): the window is the model maximum, NOT capped
538
+ # by the local num_ctx (no local VRAM allocation involved).
539
+ #
540
+ # Verified for Ollama cloud on 2026-10-11: a 9,400-token prompt sent to
541
+ # `deepseek-v4.1-flash:cloud` with num_ctx=2048 was evaluated in full
542
+ # (prompt_eval_count 9400) and a detail from its first line was recalled.
543
+ # The cloud ignores num_ctx, so the num_ctx Nova sends is NOT a limit here.
544
+ # The cost of a huge window is handled separately, by `context_budget_tokens`
545
+ # (applied in get_context_window_size) — not by misreporting what the model accepts.
546
+ if not is_local_ollama:
547
+ return arch_window
548
+
549
+ # Local Ollama model not currently loaded: predict the effective window as
550
+ # min(architecture max, the context the SERVER will allocate) — the smaller
551
+ # is what truncates. Nova cannot set that context (its endpoint has no
552
+ # num_ctx), so it reads what the server will use: the Modelfile's num_ctx,
553
+ # then OLLAMA_CONTEXT_LENGTH.
554
+ from novacode_cli.context._dynamic import predicted_local_num_ctx
555
+
556
+ effective = min(arch_window, predicted_local_num_ctx(model_name, use_dynamic=use_dynamic))
557
+ if effective != arch_window:
558
+ logger.debug(f"Capping {model_name} window {arch_window:,} → {effective:,} (num_ctx)")
559
+ return effective
560
+
561
+
562
+ def format_token_count(tokens: int) -> str:
563
+ """Format a token count for display with thousands separators."""
564
+ return f"{tokens:,}"
565
+
566
+
567
+ def _estimate_tokens(text: str) -> int:
568
+ """Estimate token count from text using a char/4 heuristic."""
569
+ return len(text) // 4
570
+
571
+
572
+ def _message_text(msg: BaseMessage) -> str:
573
+ """Extract text content from a message.
574
+
575
+ Handles both plain-string content and the provider block formats:
576
+ ``{"type": "text", "text": ...}`` plus tool-result blocks that carry their
577
+ payload under ``content`` (Anthropic ``tool_result``) so large tool outputs
578
+ aren't counted as zero.
579
+ """
580
+ content = msg.content
581
+ if isinstance(content, str):
582
+ return content
583
+ if isinstance(content, list):
584
+ parts: list[str] = []
585
+ for block in content:
586
+ if isinstance(block, dict):
587
+ # Common: {"type": "text", "text": "..."}. Fall back to a
588
+ # nested "content" payload (tool_result blocks) so the tokens
589
+ # in tool output are not silently dropped.
590
+ txt = block.get("text")
591
+ if txt:
592
+ parts.append(str(txt))
593
+ else:
594
+ inner = block.get("content")
595
+ if isinstance(inner, str):
596
+ parts.append(inner)
597
+ elif isinstance(inner, list):
598
+ for ib in inner:
599
+ if isinstance(ib, dict):
600
+ parts.append(str(ib.get("text", "")))
601
+ elif isinstance(ib, str):
602
+ parts.append(ib)
603
+ elif isinstance(block, str):
604
+ parts.append(block)
605
+ return " ".join(parts)
606
+ return str(content)
607
+
608
+
609
+ def _tool_call_text(msg: BaseMessage) -> str:
610
+ """Serialize an AIMessage's tool calls (name + arguments) for token counting.
611
+
612
+ Tool-call arguments (e.g. a ``write_file`` body or a large ``edit`` payload)
613
+ re-enter the context on every subsequent turn but live in ``msg.tool_calls``,
614
+ not in ``msg.content`` — so they must be counted explicitly or context usage
615
+ is badly under-reported.
616
+ """
617
+ import json
618
+
619
+ tool_calls = getattr(msg, "tool_calls", None) or []
620
+ parts: list[str] = []
621
+ for tc in tool_calls:
622
+ if isinstance(tc, dict):
623
+ name = tc.get("name", "")
624
+ args = tc.get("args", "")
625
+ else:
626
+ name = getattr(tc, "name", "")
627
+ args = getattr(tc, "args", "")
628
+ if name:
629
+ parts.append(str(name))
630
+ if isinstance(args, (dict, list)):
631
+ try:
632
+ parts.append(json.dumps(args, ensure_ascii=False))
633
+ except (TypeError, ValueError):
634
+ parts.append(str(args))
635
+ elif args:
636
+ parts.append(str(args))
637
+ return " ".join(parts)
638
+
639
+
640
+ def _tool_tokens(tools: list[Any]) -> int:
641
+ """Schema tokens with the request snapshot's estimator, so CTX does not jump.
642
+
643
+ Falls back to this module's own count for a definition the provider
644
+ converter cannot read.
645
+ """
646
+ from langchain_core.messages.utils import count_tokens_approximately
647
+
648
+ try:
649
+ return count_tokens_approximately([], tools=list(tools))
650
+ except Exception: # noqa: BLE001 - accounting only
651
+ return _estimate_tokens(_tool_schema_text(tools))
652
+
653
+
654
+ def _tool_schema_text(tools: list[Any]) -> str:
655
+ """Serialize tool definitions (name + description + args schema) for counting.
656
+
657
+ Unused tools' full schemas still ship on every request, so omitting them
658
+ hides a large, permanent slice of the baseline. Serialization mirrors
659
+ :func:`_tool_call_text` so both sides of a call are counted the same way.
660
+ """
661
+ import json
662
+
663
+ parts: list[str] = []
664
+ for tool in tools:
665
+ name = getattr(tool, "name", None) or (tool.get("name") if isinstance(tool, dict) else "")
666
+ if name:
667
+ parts.append(str(name))
668
+ description = getattr(tool, "description", None) or (
669
+ tool.get("description") if isinstance(tool, dict) else ""
670
+ )
671
+ if description:
672
+ parts.append(str(description))
673
+ try:
674
+ # What the model is sent: injected arguments (runtime, state) are
675
+ # left out, and those are the ones with no JSON schema.
676
+ schema = getattr(tool, "tool_call_schema", None)
677
+ except Exception: # noqa: BLE001
678
+ schema = None
679
+ if schema is None:
680
+ schema = getattr(tool, "args_schema", None)
681
+ if schema is not None:
682
+ try:
683
+ if hasattr(schema, "model_json_schema"):
684
+ parts.append(json.dumps(schema.model_json_schema(), ensure_ascii=False))
685
+ elif isinstance(schema, dict):
686
+ parts.append(json.dumps(schema, ensure_ascii=False))
687
+ except Exception: # noqa: BLE001 — one odd tool must not zero the whole breakdown
688
+ logger.debug("no JSON schema for tool %s", name, exc_info=True)
689
+ elif isinstance(tool, dict) and tool.get("parameters"):
690
+ try:
691
+ parts.append(json.dumps(tool["parameters"], ensure_ascii=False))
692
+ except (TypeError, ValueError):
693
+ parts.append(str(tool["parameters"]))
694
+ return " ".join(parts)
695
+
696
+
697
+ #: Thinking-model fields that ride along in ``additional_kwargs`` and are sent
698
+ #: back with the message on every subsequent request.
699
+ _REASONING_KEYS = ("reasoning_content", "reasoning")
700
+
701
+
702
+ def _reasoning_text(msg: BaseMessage) -> str:
703
+ """The chain of thought carried by *msg*, if the provider returned one."""
704
+ extra = getattr(msg, "additional_kwargs", None)
705
+ if not isinstance(extra, dict):
706
+ return ""
707
+ return " ".join(str(extra[k]) for k in _REASONING_KEYS if extra.get(k))
708
+
709
+
710
+ def build_context_breakdown(
711
+ messages: list[BaseMessage],
712
+ model_name: str,
713
+ use_dynamic: bool = True,
714
+ tools: list[Any] | None = None,
715
+ ) -> ContextBreakdown:
716
+ """Build a ContextBreakdown from the current conversation state.
717
+
718
+ Uses character-based token estimation (len // 4). This is approximate but
719
+ sufficient for threshold-based warnings.
720
+
721
+ Args:
722
+ messages: Current conversation messages from agent state.
723
+ model_name: Model name for looking up context window size.
724
+ use_dynamic: Whether to use dynamic detection from Ollama (default: True).
725
+ tools: Tool definitions bound to the model, if known. Their schemas ship
726
+ on every request and are a permanent part of the baseline, so they
727
+ are counted into ``tool_definitions_tokens``. Omitted (0) when the
728
+ caller cannot supply them cheaply.
729
+
730
+ Returns:
731
+ Populated ContextBreakdown with usage stats.
732
+ """
733
+ from langchain_core.messages import (
734
+ AIMessage,
735
+ HumanMessage,
736
+ SystemMessage,
737
+ ToolMessage,
738
+ )
739
+
740
+ context_window = get_context_window_size(model_name, use_dynamic=use_dynamic)
741
+
742
+ system_tokens = 0
743
+ user_tokens = 0
744
+ assistant_tokens = 0
745
+ tool_tokens = 0
746
+ user_count = 0
747
+ assistant_count = 0
748
+ tool_count = 0
749
+
750
+ for msg in messages:
751
+ text = _message_text(msg)
752
+ tokens = _estimate_tokens(text)
753
+
754
+ if isinstance(msg, SystemMessage):
755
+ system_tokens += tokens
756
+ elif isinstance(msg, HumanMessage):
757
+ user_tokens += tokens
758
+ user_count += 1
759
+ elif isinstance(msg, AIMessage):
760
+ assistant_tokens += tokens
761
+ assistant_count += 1
762
+ if getattr(msg, "tool_calls", None):
763
+ tool_count += len(msg.tool_calls)
764
+ # Tool-call arguments are part of the assistant turn and often
765
+ # dominate context (file contents, diffs) — count them.
766
+ assistant_tokens += _estimate_tokens(_tool_call_text(msg))
767
+ # A thinking model's chain of thought lives in additional_kwargs and
768
+ # has to be echoed back on every later request (see
769
+ # utils/backend_patches.apply_openai_reasoning_content_patch), so it
770
+ # is real, permanent context — invisible here until counted.
771
+ assistant_tokens += _estimate_tokens(_reasoning_text(msg))
772
+ elif isinstance(msg, ToolMessage):
773
+ tool_tokens += tokens
774
+
775
+ # Tool schemas are bound to the model, not carried in the message list, but
776
+ # they consume context on every request — count them into the baseline.
777
+ if tools and any(getattr(t, "name", None) == "search_tools" for t in tools):
778
+ # Most schemas are deferred behind search_tools, and the bound ones are
779
+ # trimmed (the task tool's subagent roster): count them as sent.
780
+ # ponytail: skips the middleware's "frequently used" extras (at most 10).
781
+ from novacode_cli.agents.tool_search import sent_tools
782
+
783
+ tools = sent_tools(tools, messages)
784
+ tool_def_tokens = _tool_tokens(tools) if tools else 0
785
+
786
+ total = system_tokens + user_tokens + assistant_tokens + tool_tokens + tool_def_tokens
787
+
788
+ return ContextBreakdown(
789
+ system_prompt_tokens=system_tokens,
790
+ tool_definitions_tokens=tool_def_tokens,
791
+ user_message_tokens=user_tokens,
792
+ assistant_message_tokens=assistant_tokens,
793
+ tool_result_tokens=tool_tokens,
794
+ total_tokens=total,
795
+ context_window_size=context_window,
796
+ user_message_count=user_count,
797
+ assistant_message_count=assistant_count,
798
+ tool_call_count=tool_count,
799
+ )