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.
- novacode_cli/__init__.py +29 -0
- novacode_cli/__main__.py +6 -0
- novacode_cli/_lazy_heavy.py +99 -0
- novacode_cli/_version.py +3 -0
- novacode_cli/agent_stream.py +178 -0
- novacode_cli/agents/agent_file.py +250 -0
- novacode_cli/agents/agent_mailbox.py +120 -0
- novacode_cli/agents/approval_decisions.py +146 -0
- novacode_cli/agents/arg_repair.py +196 -0
- novacode_cli/agents/async_agents/__init__.py +6 -0
- novacode_cli/agents/async_agents/_config.py +213 -0
- novacode_cli/agents/async_agents/_mcp_tools.py +192 -0
- novacode_cli/agents/async_agents/_specialist.py +84 -0
- novacode_cli/agents/async_agents/_test_commands.py +89 -0
- novacode_cli/agents/async_agents/_tools.py +432 -0
- novacode_cli/agents/async_agents/general_purpose.py +42 -0
- novacode_cli/agents/async_agents/langgraph.json +9 -0
- novacode_cli/agents/async_agents/research.py +22 -0
- novacode_cli/agents/async_context.py +303 -0
- novacode_cli/agents/async_on_demand.py +52 -0
- novacode_cli/agents/async_task_errors.py +75 -0
- novacode_cli/agents/async_workspace.py +27 -0
- novacode_cli/agents/attribution.py +165 -0
- novacode_cli/agents/browser_mode.py +119 -0
- novacode_cli/agents/btw_agent.py +71 -0
- novacode_cli/agents/caveman.py +352 -0
- novacode_cli/agents/caveman_support.py +370 -0
- novacode_cli/agents/code_mode.py +774 -0
- novacode_cli/agents/code_runtime.py +160 -0
- novacode_cli/agents/context_compaction.py +1339 -0
- novacode_cli/agents/core_agent.py +2710 -0
- novacode_cli/agents/default_subagents/__init__.py +24 -0
- novacode_cli/agents/default_subagents/async_subagents.py +296 -0
- novacode_cli/agents/default_subagents/builtin/browser-automation-agent.md +110 -0
- novacode_cli/agents/default_subagents/builtin/bug-fix-agent.md +22 -0
- novacode_cli/agents/default_subagents/builtin/bug-investigation-agent.md +42 -0
- novacode_cli/agents/default_subagents/builtin/code-explorer.md +37 -0
- novacode_cli/agents/default_subagents/builtin/code-review-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/dependency-audit-agent.md +34 -0
- novacode_cli/agents/default_subagents/builtin/documentation-update-agent.md +46 -0
- novacode_cli/agents/default_subagents/builtin/performance-audit-agent.md +41 -0
- novacode_cli/agents/default_subagents/builtin/plan-scout-agent.md +64 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-agent.md +35 -0
- novacode_cli/agents/default_subagents/builtin/refactoring-specialist-agent.md +175 -0
- novacode_cli/agents/default_subagents/builtin/security-audit-agent.md +96 -0
- novacode_cli/agents/default_subagents/builtin/test-generation-agent.md +33 -0
- novacode_cli/agents/default_subagents/builtin/test-runner-agent.md +55 -0
- novacode_cli/agents/default_subagents/prompt.py +88 -0
- novacode_cli/agents/default_subagents/subagents.py +138 -0
- novacode_cli/agents/dream_agent.py +93 -0
- novacode_cli/agents/edit_diagnostics.py +362 -0
- novacode_cli/agents/edit_fallback.py +62 -0
- novacode_cli/agents/handoff.py +166 -0
- novacode_cli/agents/invocation.py +152 -0
- novacode_cli/agents/model_budget.py +42 -0
- novacode_cli/agents/model_retry.py +113 -0
- novacode_cli/agents/model_router.py +420 -0
- novacode_cli/agents/openai_decisions.py +186 -0
- novacode_cli/agents/plan_agent/__init__.py +12 -0
- novacode_cli/agents/plan_agent/plan_agent.py +357 -0
- novacode_cli/agents/plan_agent/plan_mode_middleware.py +64 -0
- novacode_cli/agents/plan_execution.py +237 -0
- novacode_cli/agents/ponytail.py +66 -0
- novacode_cli/agents/prime.py +274 -0
- novacode_cli/agents/provider_caching.py +99 -0
- novacode_cli/agents/server_launcher.py +904 -0
- novacode_cli/agents/strict_tools.py +384 -0
- novacode_cli/agents/task_discipline.py +294 -0
- novacode_cli/agents/tool_call_repair.py +290 -0
- novacode_cli/agents/tool_offload.py +291 -0
- novacode_cli/agents/tool_search.py +806 -0
- novacode_cli/agents/tool_search_variants.py +106 -0
- novacode_cli/agents/tool_verdicts.py +1194 -0
- novacode_cli/agents/user_async_agents.py +312 -0
- novacode_cli/artifacts/__init__.py +10 -0
- novacode_cli/artifacts/registry.py +278 -0
- novacode_cli/artifacts/server.py +426 -0
- novacode_cli/assets/Nova.png +0 -0
- novacode_cli/assets/nova-avatar.png +0 -0
- novacode_cli/audio/__init__.py +301 -0
- novacode_cli/audio/capture.py +137 -0
- novacode_cli/audio/downloads.py +273 -0
- novacode_cli/audio/pipeline.py +391 -0
- novacode_cli/audio/playback.py +87 -0
- novacode_cli/audio/providers.py +208 -0
- novacode_cli/audio/speakable.py +78 -0
- novacode_cli/audio/stt.py +132 -0
- novacode_cli/audio/stt_deepgram.py +69 -0
- novacode_cli/audio/stt_elevenlabs.py +89 -0
- novacode_cli/audio/stt_parakeet.py +94 -0
- novacode_cli/audio/stt_whistle.py +61 -0
- novacode_cli/audio/summarize.py +92 -0
- novacode_cli/audio/tts.py +85 -0
- novacode_cli/audio/tts_kokoro.py +73 -0
- novacode_cli/audio/tts_orpheus.py +180 -0
- novacode_cli/audio/tts_pocket.py +131 -0
- novacode_cli/audio/tts_sano.py +69 -0
- novacode_cli/audio/vad.py +145 -0
- novacode_cli/backends/__init__.py +17 -0
- novacode_cli/backends/filesystem.py +894 -0
- novacode_cli/backends/native_search.py +92 -0
- novacode_cli/bootstrap/__init__.py +9 -0
- novacode_cli/bootstrap/env_snapshot.py +240 -0
- novacode_cli/bootstrap/steering.py +464 -0
- novacode_cli/bootstrap/vision_router.py +604 -0
- novacode_cli/brand.py +362 -0
- novacode_cli/cli_args.py +718 -0
- novacode_cli/commands/__init__.py +237 -0
- novacode_cli/commands/agents_commands.py +231 -0
- novacode_cli/commands/auth_handler.py +40 -0
- novacode_cli/commands/browser_use_handler.py +75 -0
- novacode_cli/commands/cache_handler.py +32 -0
- novacode_cli/commands/chat_handler.py +1357 -0
- novacode_cli/commands/ci_fix.py +171 -0
- novacode_cli/commands/claude_plan.py +331 -0
- novacode_cli/commands/codex_plan.py +195 -0
- novacode_cli/commands/commands.py +1113 -0
- novacode_cli/commands/create_handler.py +124 -0
- novacode_cli/commands/create_server.py +1887 -0
- novacode_cli/commands/cron_handler.py +119 -0
- novacode_cli/commands/dream_handler.py +203 -0
- novacode_cli/commands/effort_handler.py +65 -0
- novacode_cli/commands/evolution_handler.py +214 -0
- novacode_cli/commands/file_commands.py +381 -0
- novacode_cli/commands/handoff_handler.py +68 -0
- novacode_cli/commands/hooks_handler.py +269 -0
- novacode_cli/commands/init_handler.py +146 -0
- novacode_cli/commands/learning_handler.py +160 -0
- novacode_cli/commands/lite_mode_handler.py +76 -0
- novacode_cli/commands/log_commands.py +363 -0
- novacode_cli/commands/mcp_handler.py +29 -0
- novacode_cli/commands/model_handler.py +29 -0
- novacode_cli/commands/notifications_handler.py +158 -0
- novacode_cli/commands/onboard_handler.py +121 -0
- novacode_cli/commands/plan_handler.py +440 -0
- novacode_cli/commands/planner_usage.py +250 -0
- novacode_cli/commands/plugin_install_handler.py +38 -0
- novacode_cli/commands/plugins_handler.py +35 -0
- novacode_cli/commands/prime_handler.py +455 -0
- novacode_cli/commands/prompt_handler.py +149 -0
- novacode_cli/commands/provider_usage.py +160 -0
- novacode_cli/commands/ralph_events.py +106 -0
- novacode_cli/commands/ralph_handler.py +1293 -0
- novacode_cli/commands/refine_handler.py +213 -0
- novacode_cli/commands/research_handler.py +202 -0
- novacode_cli/commands/rootapp_remote.py +83 -0
- novacode_cli/commands/router_handler.py +36 -0
- novacode_cli/commands/server_commands.py +249 -0
- novacode_cli/commands/session_commands.py +203 -0
- novacode_cli/commands/share_handler.py +450 -0
- novacode_cli/commands/side_commands.py +366 -0
- novacode_cli/commands/skill_invoke.py +420 -0
- novacode_cli/commands/skills_commands.py +48 -0
- novacode_cli/commands/trace_handler.py +322 -0
- novacode_cli/commands/trello_handler.py +226 -0
- novacode_cli/commands/trello_remote.py +177 -0
- novacode_cli/commands/trello_server.py +1214 -0
- novacode_cli/commands/vision_handler.py +127 -0
- novacode_cli/commands/voice_handler.py +447 -0
- novacode_cli/commands/webhook_handler.py +134 -0
- novacode_cli/commands/wiki_commands.py +227 -0
- novacode_cli/compaction.py +753 -0
- novacode_cli/computation_cache.py +159 -0
- novacode_cli/computer/__init__.py +1 -0
- novacode_cli/computer/persistent.py +234 -0
- novacode_cli/computer/preview.py +253 -0
- novacode_cli/computer/runtime.py +474 -0
- novacode_cli/computer/windows_console.py +152 -0
- novacode_cli/computer/windows_qemu.py +211 -0
- novacode_cli/config/browser_mode.py +159 -0
- novacode_cli/config/catalog_models.py +95 -0
- novacode_cli/config/config.py +1388 -0
- novacode_cli/config/credentials.py +540 -0
- novacode_cli/config/google_oauth_auth.py +281 -0
- novacode_cli/config/model_capabilities.py +343 -0
- novacode_cli/config/model_catalog.py +275 -0
- novacode_cli/config/model_create.py +1325 -0
- novacode_cli/config/model_manager.py +695 -0
- novacode_cli/config/nova_config.py +1283 -0
- novacode_cli/config/openai_chatgpt_auth.py +384 -0
- novacode_cli/config/opencode_gateway.py +25 -0
- novacode_cli/config/plan_mode.py +188 -0
- novacode_cli/config/provider_auth.py +438 -0
- novacode_cli/config/provider_catalog.json +5127 -0
- novacode_cli/config/provider_registry.py +38 -0
- novacode_cli/config/provider_transports.py +258 -0
- novacode_cli/config/role_models.py +175 -0
- novacode_cli/context/__init__.py +50 -0
- novacode_cli/context/_analysis.py +799 -0
- novacode_cli/context/_budget.py +116 -0
- novacode_cli/context/_conversation.py +73 -0
- novacode_cli/context/_dynamic.py +264 -0
- novacode_cli/context/_model_config.py +380 -0
- novacode_cli/context/_models_dev.py +99 -0
- novacode_cli/context/archives.py +248 -0
- novacode_cli/context/estimates.py +65 -0
- novacode_cli/context/history.py +17 -0
- novacode_cli/context/knowledge_cache.py +292 -0
- novacode_cli/context/manager.py +101 -0
- novacode_cli/context/observations.py +65 -0
- novacode_cli/context/pressure.py +168 -0
- novacode_cli/context/provider_accounting.py +109 -0
- novacode_cli/context/snapshot.py +493 -0
- novacode_cli/context/strategies.py +279 -0
- novacode_cli/context/task_progress.py +122 -0
- novacode_cli/core/__init__.py +0 -0
- novacode_cli/core/agent_loop.py +1821 -0
- novacode_cli/core/autonomous_loop.py +277 -0
- novacode_cli/core/code_approvals.py +57 -0
- novacode_cli/core/input_preparation.py +276 -0
- novacode_cli/core/reasoning.py +67 -0
- novacode_cli/core/streaming.py +326 -0
- novacode_cli/core/subagent_tasks.py +696 -0
- novacode_cli/core/subagent_tracking.py +235 -0
- novacode_cli/core/tool_results.py +116 -0
- novacode_cli/core/verification_loop.py +272 -0
- novacode_cli/council.py +595 -0
- novacode_cli/council_consensus.py +115 -0
- novacode_cli/council_decision.py +157 -0
- novacode_cli/council_planner.py +1421 -0
- novacode_cli/council_settings.py +208 -0
- novacode_cli/council_web_settings.py +41 -0
- novacode_cli/council_workspace.py +56 -0
- novacode_cli/cowork/__init__.py +10 -0
- novacode_cli/cowork/broker_middleware.py +162 -0
- novacode_cli/cowork/launcher.py +102 -0
- novacode_cli/cowork/policy.py +284 -0
- novacode_cli/cowork/ui.py +826 -0
- novacode_cli/daemons/__init__.py +1 -0
- novacode_cli/daemons/registry.py +342 -0
- novacode_cli/desktop_notifications.py +302 -0
- novacode_cli/doctor.py +251 -0
- novacode_cli/embeddings.py +109 -0
- novacode_cli/entrypoint.py +56 -0
- novacode_cli/errors/__init__.py +19 -0
- novacode_cli/errors/handlers.py +648 -0
- novacode_cli/errors/provider_errors.py +249 -0
- novacode_cli/errors/taxonomy.py +80 -0
- novacode_cli/events.py +179 -0
- novacode_cli/file_ops.py +714 -0
- novacode_cli/git_safety.py +281 -0
- novacode_cli/handoff.py +383 -0
- novacode_cli/headless/__init__.py +18 -0
- novacode_cli/headless/input.py +58 -0
- novacode_cli/headless/output.py +224 -0
- novacode_cli/headless/pipe.py +532 -0
- novacode_cli/headless/runner.py +461 -0
- novacode_cli/hermes/__init__.py +39 -0
- novacode_cli/hermes/activation.py +297 -0
- novacode_cli/hermes/config.py +129 -0
- novacode_cli/hermes/curator.py +199 -0
- novacode_cli/hermes/evolution.py +374 -0
- novacode_cli/hermes/ledger.py +364 -0
- novacode_cli/hermes/memory_tiers.py +969 -0
- novacode_cli/hermes/middleware.py +678 -0
- novacode_cli/hermes/overview.py +174 -0
- novacode_cli/hermes/prompt_evolution.py +503 -0
- novacode_cli/hermes/refine_loop.py +643 -0
- novacode_cli/hermes/refinement_log.py +190 -0
- novacode_cli/hermes/review.py +674 -0
- novacode_cli/hermes/skill_debate.py +135 -0
- novacode_cli/hermes/skill_discovery.py +450 -0
- novacode_cli/hermes/skill_manager.py +335 -0
- novacode_cli/hermes/skill_proposals.py +43 -0
- novacode_cli/hermes/test_gaming.py +119 -0
- novacode_cli/hermes/tracker.py +435 -0
- novacode_cli/hermes/tuner.py +206 -0
- novacode_cli/hermes/verification_memory.py +134 -0
- novacode_cli/hermes/verifier.py +277 -0
- novacode_cli/hitl/__init__.py +22 -0
- novacode_cli/hitl/interrupts.py +493 -0
- novacode_cli/hooks.py +359 -0
- novacode_cli/image_utils.py +517 -0
- novacode_cli/init/__init__.py +7 -0
- novacode_cli/init/events.py +43 -0
- novacode_cli/input_utils.py +526 -0
- novacode_cli/install_commands.py +87 -0
- novacode_cli/integrations/__init__.py +82 -0
- novacode_cli/integrations/daytona.py +122 -0
- novacode_cli/integrations/docker.py +309 -0
- novacode_cli/integrations/host_path.py +111 -0
- novacode_cli/integrations/langsmith.py +151 -0
- novacode_cli/integrations/modal.py +134 -0
- novacode_cli/integrations/runloop.py +130 -0
- novacode_cli/integrations/sandbox_factory.py +1388 -0
- novacode_cli/integrations/sandbox_registry.py +312 -0
- novacode_cli/integrations/workdir_backend.py +589 -0
- novacode_cli/main.py +2078 -0
- novacode_cli/mcp/__init__.py +73 -0
- novacode_cli/mcp/auth.py +189 -0
- novacode_cli/mcp/client.py +571 -0
- novacode_cli/mcp/code_mode.py +5 -0
- novacode_cli/mcp/code_runtime.py +5 -0
- novacode_cli/mcp/commands.py +902 -0
- novacode_cli/mcp/config.py +406 -0
- novacode_cli/mcp/generic.py +128 -0
- novacode_cli/mcp/middleware.py +993 -0
- novacode_cli/mcp/presets.py +367 -0
- novacode_cli/memory/__init__.py +1 -0
- novacode_cli/memory/agent_memory.py +1312 -0
- novacode_cli/memory/builtin_lessons.py +60 -0
- novacode_cli/memory/dream_gates.py +192 -0
- novacode_cli/memory/limits.py +59 -0
- novacode_cli/memory/parsed_cache.py +106 -0
- novacode_cli/memory/recall_stats.py +97 -0
- novacode_cli/memory/semantic.py +116 -0
- novacode_cli/memory/store.py +335 -0
- novacode_cli/memory/wiki.py +274 -0
- novacode_cli/migrate.py +185 -0
- novacode_cli/onboarding.py +649 -0
- novacode_cli/onboarding_check.py +119 -0
- novacode_cli/path_approval.py +297 -0
- novacode_cli/plan_archive.py +148 -0
- novacode_cli/plugins/__init__.py +121 -0
- novacode_cli/plugins/claude_plugins.py +538 -0
- novacode_cli/plugins/cli.py +168 -0
- novacode_cli/plugins/components.py +413 -0
- novacode_cli/plugins/hooks.py +356 -0
- novacode_cli/plugins/loader.py +317 -0
- novacode_cli/plugins/marketplaces.py +199 -0
- novacode_cli/plugins/storage.py +70 -0
- novacode_cli/prime.py +759 -0
- novacode_cli/prime_factory/__init__.py +15 -0
- novacode_cli/prime_factory/container.py +198 -0
- novacode_cli/prime_factory/controller.py +565 -0
- novacode_cli/prime_factory/knowledge.py +261 -0
- novacode_cli/prime_factory/publication.py +218 -0
- novacode_cli/prime_factory/recovery.py +50 -0
- novacode_cli/prime_factory/research.py +370 -0
- novacode_cli/prime_factory/runner.py +608 -0
- novacode_cli/prime_factory/runtime.py +261 -0
- novacode_cli/prime_factory/state.py +201 -0
- novacode_cli/prime_factory/store.py +128 -0
- novacode_cli/prime_factory/worker.py +129 -0
- novacode_cli/prime_factory/workflow.py +899 -0
- novacode_cli/prime_factory/workspace.py +317 -0
- novacode_cli/prime_memory.py +290 -0
- novacode_cli/process_manager.py +585 -0
- novacode_cli/project_onboard.py +537 -0
- novacode_cli/prompts/System_Prompt_Nova.jinja +23 -0
- novacode_cli/prompts/__init__.py +168 -0
- novacode_cli/prompts/agent_definitions.jinja +11 -0
- novacode_cli/prompts/agent_generation.jinja +125 -0
- novacode_cli/prompts/async_agents/general_purpose.jinja +22 -0
- novacode_cli/prompts/claude_plan_mode.jinja +12 -0
- novacode_cli/prompts/coding_workflow.jinja +18 -0
- novacode_cli/prompts/computer_use.jinja +17 -0
- novacode_cli/prompts/core_agent_system.jinja +81 -0
- novacode_cli/prompts/delegation.jinja +15 -0
- novacode_cli/prompts/desktop_power.jinja +4 -0
- novacode_cli/prompts/file_paths.jinja +15 -0
- novacode_cli/prompts/file_tracker.jinja +23 -0
- novacode_cli/prompts/init_agents_md.jinja +56 -0
- novacode_cli/prompts/init_exploration.jinja +174 -0
- novacode_cli/prompts/init_semantic_orchestrate.jinja +37 -0
- novacode_cli/prompts/instruction_scope.jinja +5 -0
- novacode_cli/prompts/learnings_extraction.jinja +45 -0
- novacode_cli/prompts/longterm_memory.jinja +54 -0
- novacode_cli/prompts/mcp.jinja +84 -0
- novacode_cli/prompts/memory_consolidation.jinja +40 -0
- novacode_cli/prompts/nova_evolution.jinja +73 -0
- novacode_cli/prompts/nova_review.jinja +158 -0
- novacode_cli/prompts/nova_skill_debate.jinja +36 -0
- novacode_cli/prompts/nova_standing_instructions.jinja +26 -0
- novacode_cli/prompts/nova_verify.jinja +100 -0
- novacode_cli/prompts/onboard_dashboard.jinja +378 -0
- novacode_cli/prompts/plan_agent.jinja +87 -0
- novacode_cli/prompts/plan_scouts.jinja +5 -0
- novacode_cli/prompts/plan_storage.jinja +8 -0
- novacode_cli/prompts/planning.jinja +82 -0
- novacode_cli/prompts/ponytail/LICENSE +21 -0
- novacode_cli/prompts/ponytail/UPSTREAM.json +15 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-config.js +175 -0
- novacode_cli/prompts/ponytail/hooks/ponytail-instructions.js +98 -0
- novacode_cli/prompts/ponytail/skills/ponytail/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-audit/SKILL.md +103 -0
- novacode_cli/prompts/ponytail/skills/ponytail-debt/SKILL.md +44 -0
- novacode_cli/prompts/ponytail/skills/ponytail-gain/SKILL.md +52 -0
- novacode_cli/prompts/ponytail/skills/ponytail-help/SKILL.md +76 -0
- novacode_cli/prompts/ponytail/skills/ponytail-review/SKILL.md +101 -0
- novacode_cli/prompts/prompt_evolution.jinja +27 -0
- novacode_cli/prompts/ralph_iteration.jinja +75 -0
- novacode_cli/prompts/refine_gate.jinja +31 -0
- novacode_cli/prompts/refine_plan.jinja +64 -0
- novacode_cli/prompts/refine_review.jinja +34 -0
- novacode_cli/prompts/research_delivery.jinja +9 -0
- novacode_cli/prompts/research_swarm.jinja +130 -0
- novacode_cli/prompts/response_style.jinja +5 -0
- novacode_cli/prompts/session_summarization.jinja +86 -0
- novacode_cli/prompts/skill_creation.jinja +85 -0
- novacode_cli/prompts/subagents/fact_checker.jinja +48 -0
- novacode_cli/prompts/subagents/financial_analyst.jinja +50 -0
- novacode_cli/prompts/subagents/literature_reviewer.jinja +52 -0
- novacode_cli/prompts/subagents/market_analyst.jinja +48 -0
- novacode_cli/prompts/subagents/research_synthesizer.jinja +57 -0
- novacode_cli/prompts/subagents/technical_researcher.jinja +56 -0
- novacode_cli/prompts/subagents/web_researcher.jinja +46 -0
- novacode_cli/prompts/summarization.jinja +107 -0
- novacode_cli/prompts/summarization_answers.jinja +23 -0
- novacode_cli/prompts/summarization_questions.jinja +32 -0
- novacode_cli/prompts/vision_caption.jinja +21 -0
- novacode_cli/prompts/voice_summary.jinja +65 -0
- novacode_cli/prompts/wiki_file.jinja +57 -0
- novacode_cli/prompts/wiki_ingest.jinja +87 -0
- novacode_cli/recovery.py +383 -0
- novacode_cli/remote/__init__.py +45 -0
- novacode_cli/remote/approvals.py +282 -0
- novacode_cli/remote/async_task_clients.py +97 -0
- novacode_cli/remote/async_task_watcher.py +240 -0
- novacode_cli/remote/bridge.py +905 -0
- novacode_cli/remote/command_menu.py +26 -0
- novacode_cli/remote/command_ui.py +243 -0
- novacode_cli/remote/config.py +199 -0
- novacode_cli/remote/discord_bridge.py +530 -0
- novacode_cli/remote/discord_format.py +31 -0
- novacode_cli/remote/discord_threads.py +225 -0
- novacode_cli/remote/images.py +97 -0
- novacode_cli/remote/processor.py +601 -0
- novacode_cli/remote/rootapp_bot/bot.cjs +16 -0
- novacode_cli/remote/rootapp_bot/package-lock.json +2683 -0
- novacode_cli/remote/rootapp_bot/package.json +18 -0
- novacode_cli/remote/rootapp_bot/transport.cjs +80 -0
- novacode_cli/remote/rootapp_bridge.py +370 -0
- novacode_cli/remote/rootapp_setup.py +89 -0
- novacode_cli/remote/routing.py +103 -0
- novacode_cli/remote/scheduler.py +302 -0
- novacode_cli/remote/session.py +164 -0
- novacode_cli/remote/status.py +411 -0
- novacode_cli/remote/streaming.py +188 -0
- novacode_cli/remote/telegram_bridge.py +1118 -0
- novacode_cli/remote/telegram_format.py +215 -0
- novacode_cli/remote/telegram_hub.py +256 -0
- novacode_cli/remote/voice_notes.py +120 -0
- novacode_cli/remote/webhook_adapters.py +167 -0
- novacode_cli/remote/webhook_server.py +154 -0
- novacode_cli/rewind.py +150 -0
- novacode_cli/rooms/__init__.py +1 -0
- novacode_cli/rooms/bridge.py +86 -0
- novacode_cli/rooms/cli.py +231 -0
- novacode_cli/rooms/nova.py +227 -0
- novacode_cli/rooms/runtime.py +299 -0
- novacode_cli/rooms/store.py +538 -0
- novacode_cli/rooms/workspace.py +94 -0
- novacode_cli/security/__init__.py +29 -0
- novacode_cli/security/approval_context.py +45 -0
- novacode_cli/security/delegated_approval.py +68 -0
- novacode_cli/security/local_http.py +85 -0
- novacode_cli/security/middleware.py +87 -0
- novacode_cli/security/paths.py +35 -0
- novacode_cli/security/policy.py +535 -0
- novacode_cli/security/policy_writer.py +90 -0
- novacode_cli/security/prompt_injection.py +203 -0
- novacode_cli/security/remember.py +81 -0
- novacode_cli/security/rule_synthesis.py +116 -0
- novacode_cli/security/secret_files.py +61 -0
- novacode_cli/security/session_allow.py +114 -0
- novacode_cli/security/unicode_security.py +516 -0
- novacode_cli/security/validator.py +173 -0
- novacode_cli/server/__init__.py +9 -0
- novacode_cli/server/app.py +632 -0
- novacode_cli/server/event_adapter.py +189 -0
- novacode_cli/server/session_manager.py +90 -0
- novacode_cli/server_main.py +64 -0
- novacode_cli/server_runner/dev_server.py +530 -0
- novacode_cli/server_runner/test_runner.py +470 -0
- novacode_cli/session/__init__.py +1 -0
- novacode_cli/session/adapters/__init__.py +240 -0
- novacode_cli/session/adapters/claude_code.py +102 -0
- novacode_cli/session/adapters/codex.py +77 -0
- novacode_cli/session/adapters/harness_native.py +75 -0
- novacode_cli/session/checkpoint_coordinator.py +448 -0
- novacode_cli/session/context_restore.py +25 -0
- novacode_cli/session/history_export.py +228 -0
- novacode_cli/session/history_tree.py +174 -0
- novacode_cli/session/imported_context.py +267 -0
- novacode_cli/session/runtime_identity.py +32 -0
- novacode_cli/session/session_persistence.py +1168 -0
- novacode_cli/session/session_prompt_builder.py +279 -0
- novacode_cli/session/session_restore.py +351 -0
- novacode_cli/session/session_summarization.py +20 -0
- novacode_cli/session/transcript_journal.py +153 -0
- novacode_cli/sessions/__init__.py +11 -0
- novacode_cli/sessions/gateway.py +84 -0
- novacode_cli/sessions/launch.py +51 -0
- novacode_cli/sessions/lease.py +213 -0
- novacode_cli/sessions/messaging.py +255 -0
- novacode_cli/sessions/protocol.py +216 -0
- novacode_cli/sessions/resume.py +31 -0
- novacode_cli/sessions/supervisor.py +498 -0
- novacode_cli/sessions/tasks.py +73 -0
- novacode_cli/sessions/tui_messaging.py +413 -0
- novacode_cli/sessions/worker.py +652 -0
- novacode_cli/sessions/worktree.py +262 -0
- novacode_cli/shell/__init__.py +27 -0
- novacode_cli/shell/jobs.py +475 -0
- novacode_cli/shell/middleware.py +1568 -0
- novacode_cli/shell/os_sandbox.py +205 -0
- novacode_cli/shell/output.py +181 -0
- novacode_cli/shell/patterns.py +275 -0
- novacode_cli/shell/processes.py +87 -0
- novacode_cli/shell/utils.py +236 -0
- novacode_cli/skills/__init__.py +26 -0
- novacode_cli/skills/builtin/handoff/SKILL.md +15 -0
- novacode_cli/skills/curation_middleware.py +69 -0
- novacode_cli/skills/executable.py +161 -0
- novacode_cli/skills/libraries.py +102 -0
- novacode_cli/skills/load.py +248 -0
- novacode_cli/skills/prune.py +215 -0
- novacode_cli/skills/refreshing_middleware.py +896 -0
- novacode_cli/skills/retrieval.py +248 -0
- novacode_cli/skills/runtime.py +118 -0
- novacode_cli/skills/schema.py +254 -0
- novacode_cli/skills/skill_creation.py +1549 -0
- novacode_cli/skills/skill_lock.py +75 -0
- novacode_cli/skills/skill_system_prompt.py +16 -0
- novacode_cli/skills/skills_prefs.py +126 -0
- novacode_cli/skills/tool_resolver.py +31 -0
- novacode_cli/skills/upstream.py +50 -0
- novacode_cli/skills/versioning.py +291 -0
- novacode_cli/states/Session.py +847 -0
- novacode_cli/states/__init__.py +31 -0
- novacode_cli/states/slices/__init__.py +17 -0
- novacode_cli/states/slices/agent_runtime.py +249 -0
- novacode_cli/states/slices/background_tasks.py +21 -0
- novacode_cli/states/slices/notifications.py +74 -0
- novacode_cli/states/slices/remote_bridge.py +56 -0
- novacode_cli/states/slices/ui_settings.py +49 -0
- novacode_cli/states/slices/wiki.py +25 -0
- novacode_cli/teammates/__init__.py +1 -0
- novacode_cli/teammates/controller.py +557 -0
- novacode_cli/teammates/graph.py +104 -0
- novacode_cli/teammates/registry.py +175 -0
- novacode_cli/teammates/service.py +254 -0
- novacode_cli/teammates/worker.py +19 -0
- novacode_cli/teams/__init__.py +1 -0
- novacode_cli/teams/adapters/__init__.py +1 -0
- novacode_cli/teams/adapters/container.py +316 -0
- novacode_cli/teams/adapters/inprocess.py +206 -0
- novacode_cli/teams/adapters/process.py +364 -0
- novacode_cli/teams/bootstrap.py +246 -0
- novacode_cli/teams/container_git.py +67 -0
- novacode_cli/teams/context.py +34 -0
- novacode_cli/teams/coordinator.py +815 -0
- novacode_cli/teams/gateway.py +200 -0
- novacode_cli/teams/host.py +64 -0
- novacode_cli/teams/legacy.py +52 -0
- novacode_cli/teams/middleware.py +176 -0
- novacode_cli/teams/models.py +104 -0
- novacode_cli/teams/output.py +47 -0
- novacode_cli/teams/protocol.py +50 -0
- novacode_cli/teams/recovery.py +80 -0
- novacode_cli/teams/scheduler.py +44 -0
- novacode_cli/teams/store.py +631 -0
- novacode_cli/teams/worker.py +251 -0
- novacode_cli/teams/workspace.py +100 -0
- novacode_cli/token_utils.py +256 -0
- novacode_cli/tools/__init__.py +104 -0
- novacode_cli/tools/_shared.py +97 -0
- novacode_cli/tools/_types.py +24 -0
- novacode_cli/tools/artifact_tools.py +137 -0
- novacode_cli/tools/code_search_tools.py +330 -0
- novacode_cli/tools/computer_tools.py +77 -0
- novacode_cli/tools/cron_tools.py +65 -0
- novacode_cli/tools/daemon_tool.py +290 -0
- novacode_cli/tools/desktop_tools.py +161 -0
- novacode_cli/tools/fetch_tools.py +530 -0
- novacode_cli/tools/http_tools.py +279 -0
- novacode_cli/tools/inspection_tools.py +107 -0
- novacode_cli/tools/job_tools.py +143 -0
- novacode_cli/tools/knowledge_tools.py +427 -0
- novacode_cli/tools/memory_tools.py +461 -0
- novacode_cli/tools/memory_wiki_tools.py +40 -0
- novacode_cli/tools/notebook_tools.py +238 -0
- novacode_cli/tools/oracle_tool.py +83 -0
- novacode_cli/tools/plan_archive_tools.py +80 -0
- novacode_cli/tools/plan_mode_tools.py +315 -0
- novacode_cli/tools/python_kernel_loop.py +165 -0
- novacode_cli/tools/python_kernel_tool.py +222 -0
- novacode_cli/tools/reflection_tools.py +34 -0
- novacode_cli/tools/scraper_tools.py +377 -0
- novacode_cli/tools/skill_tools.py +275 -0
- novacode_cli/tools/speak_tool.py +51 -0
- novacode_cli/tools/store_memory_tools.py +227 -0
- novacode_cli/tools/ui_tools.py +62 -0
- novacode_cli/tools/waiter_tools.py +408 -0
- novacode_cli/tools/web_tools.py +349 -0
- novacode_cli/tools/wiki_tools.py +152 -0
- novacode_cli/tools/worktree_tools.py +219 -0
- novacode_cli/tracking/__init__.py +10 -0
- novacode_cli/tracking/file_tracker.py +826 -0
- novacode_cli/tracking/local_metrics.py +170 -0
- novacode_cli/tracking/loop_guard.py +507 -0
- novacode_cli/tracking/request_metrics.py +60 -0
- novacode_cli/tracking/tracing.py +692 -0
- novacode_cli/tracking/usage_tree.py +677 -0
- novacode_cli/tracking/workspace_anchoring.py +231 -0
- novacode_cli/tui/__init__.py +11 -0
- novacode_cli/tui/animation_rate.py +9 -0
- novacode_cli/tui/animations.py +332 -0
- novacode_cli/tui/app.py +19315 -0
- novacode_cli/tui/auth_display.py +87 -0
- novacode_cli/tui/auth_screens.py +826 -0
- novacode_cli/tui/avatar.py +294 -0
- novacode_cli/tui/avatar_frames.py +21 -0
- novacode_cli/tui/background_tasks.py +35 -0
- novacode_cli/tui/browser_settings_screen.py +70 -0
- novacode_cli/tui/caveman_screen.py +185 -0
- novacode_cli/tui/council_settings_screen.py +182 -0
- novacode_cli/tui/council_view.py +535 -0
- novacode_cli/tui/decision_settings.py +268 -0
- novacode_cli/tui/diff_preview.py +199 -0
- novacode_cli/tui/gc_tuning.py +63 -0
- novacode_cli/tui/handoff_screen.py +206 -0
- novacode_cli/tui/harness.py +244 -0
- novacode_cli/tui/history_tree_screen.py +179 -0
- novacode_cli/tui/home.py +328 -0
- novacode_cli/tui/home_scene.py +94 -0
- novacode_cli/tui/icons.py +81 -0
- novacode_cli/tui/interface_styles.tcss +327 -0
- novacode_cli/tui/matrix3.py +108 -0
- novacode_cli/tui/mcp_tools_screen.py +131 -0
- novacode_cli/tui/motion.py +178 -0
- novacode_cli/tui/output_body.py +136 -0
- novacode_cli/tui/output_buffer.py +59 -0
- novacode_cli/tui/palette.py +334 -0
- novacode_cli/tui/pickers.py +237 -0
- novacode_cli/tui/prime_panel.py +506 -0
- novacode_cli/tui/provider_signin.py +213 -0
- novacode_cli/tui/pty_shell.py +159 -0
- novacode_cli/tui/reference_screens.py +444 -0
- novacode_cli/tui/resume_picker.py +346 -0
- novacode_cli/tui/rooms.py +597 -0
- novacode_cli/tui/screens.py +7088 -0
- novacode_cli/tui/session_import.py +333 -0
- novacode_cli/tui/session_pane.py +242 -0
- novacode_cli/tui/session_tab.py +42 -0
- novacode_cli/tui/session_transcript.py +118 -0
- novacode_cli/tui/shell_output.py +7 -0
- novacode_cli/tui/stall_watch.py +174 -0
- novacode_cli/tui/statusline.py +40 -0
- novacode_cli/tui/statusline_screen.py +91 -0
- novacode_cli/tui/teammate_updates.py +162 -0
- novacode_cli/tui/teammates.py +548 -0
- novacode_cli/tui/terminal.py +289 -0
- novacode_cli/tui/terminal_view.py +235 -0
- novacode_cli/tui/themes.py +118 -0
- novacode_cli/tui/update_notice.py +84 -0
- novacode_cli/tui/voice_downloads.py +113 -0
- novacode_cli/tui/widgets.py +2038 -0
- novacode_cli/tui/windows_input.py +38 -0
- novacode_cli/tui/workspace_approval.py +165 -0
- novacode_cli/ui/council_web.py +113 -0
- novacode_cli/ui/council_workspace.py +72 -0
- novacode_cli/ui/cowork_web.py +41 -0
- novacode_cli/ui/diff_highlight.py +160 -0
- novacode_cli/ui/execution.py +1396 -0
- novacode_cli/ui/exit_summary.py +65 -0
- novacode_cli/ui/hitl_approval.py +196 -0
- novacode_cli/ui/input_preparation.py +2 -0
- novacode_cli/ui/interrupt_handlers.py +215 -0
- novacode_cli/ui/question_prompt.py +151 -0
- novacode_cli/ui/status_phrases.py +157 -0
- novacode_cli/ui/stream_preview.py +66 -0
- novacode_cli/ui/streaming.py +2 -0
- novacode_cli/ui/subagent_tracking.py +2 -0
- novacode_cli/ui/ui_elements.py +1585 -0
- novacode_cli/ui/web_workspace.py +244 -0
- novacode_cli/ui_events.py +299 -0
- novacode_cli/update_ui.py +243 -0
- novacode_cli/updates.py +489 -0
- novacode_cli/utils/backend_patches.py +399 -0
- novacode_cli/utils/backend_paths.py +467 -0
- novacode_cli/utils/lazy_providers.py +51 -0
- novacode_cli/utils/model_info.py +223 -0
- novacode_cli/utils/pdf_extraction.py +183 -0
- novacode_cli/utils/startup.py +29 -0
- novacode_cli/utils/tasks.py +42 -0
- novacode_cli/utils/windows.py +9 -0
- novacode_cli/vixie/hook_adapter.py +127 -0
- novacode_cli/vixie/server.py +375 -0
- novacode_cli/wiki/__init__.py +15 -0
- novacode_cli/wiki/ask.py +56 -0
- novacode_cli/wiki/file.py +71 -0
- novacode_cli/wiki/ingest.py +184 -0
- novacode_cli/wiki/manager.py +261 -0
- novacode_cli-1.2.0.dist-info/METADATA +271 -0
- novacode_cli-1.2.0.dist-info/RECORD +691 -0
- novacode_cli-1.2.0.dist-info/WHEEL +5 -0
- novacode_cli-1.2.0.dist-info/entry_points.txt +2 -0
- novacode_cli-1.2.0.dist-info/licenses/LICENSE +21 -0
- 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
|
+
)
|