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,753 @@
|
|
|
1
|
+
"""Conversation compaction and summarization for NovaCode-cli.
|
|
2
|
+
|
|
3
|
+
This module provides functionality to compress conversation history
|
|
4
|
+
by generating an intelligent summary that preserves key context.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import asyncio
|
|
8
|
+
import hashlib
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
from datetime import UTC, datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from langchain_core.language_models import BaseChatModel
|
|
16
|
+
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, RemoveMessage, ToolMessage
|
|
17
|
+
from langgraph.graph.message import REMOVE_ALL_MESSAGES
|
|
18
|
+
|
|
19
|
+
from novacode_cli.context import CompactionResult
|
|
20
|
+
from novacode_cli.prompts import render_template
|
|
21
|
+
|
|
22
|
+
# Summarization prompt template (loaded from Jinja)
|
|
23
|
+
# Template file: NovaCode_cli/prompts/summarization.jinja
|
|
24
|
+
|
|
25
|
+
# Compaction rewrites the whole conversation into ONE synthetic HumanMessage
|
|
26
|
+
# carrying the summary (see compact_conversation). It has to be a HumanMessage
|
|
27
|
+
# for the provider's turn ordering, but it is not something the user said — so
|
|
28
|
+
# any surface that renders history must skip it, or the entire summarized
|
|
29
|
+
# context appears in the transcript as a message the user supposedly typed.
|
|
30
|
+
COMPACTION_SUMMARY_MARKER = "[Conversation context — previous session summarized]"
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
SUMMARY_TIMEOUT_SECONDS = 60.0
|
|
34
|
+
SUMMARY_ATTEMPTS = 3
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
async def _summary_call(model: BaseChatModel, prompt: str, purpose: str) -> BaseMessage:
|
|
38
|
+
"""Preserve each independent summary request and label its actual dispatch."""
|
|
39
|
+
from novacode_cli.tracking.request_metrics import dispatch
|
|
40
|
+
|
|
41
|
+
with dispatch(purpose):
|
|
42
|
+
response = await model.ainvoke([HumanMessage(content=prompt)])
|
|
43
|
+
if getattr(response, "tool_calls", None) or getattr(response, "invalid_tool_calls", None):
|
|
44
|
+
message = "Summary provider returned tool calls"
|
|
45
|
+
raise SummaryUnavailableError(message)
|
|
46
|
+
return response
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _state_generation(state: Any) -> str: # noqa: ANN401 - compiled graph state is framework-defined
|
|
50
|
+
"""Fingerprint checkpoint and complete messages before preparing a rewrite."""
|
|
51
|
+
data = {
|
|
52
|
+
"checkpoint": getattr(state, "config", None),
|
|
53
|
+
"messages": [m.model_dump(mode="json") for m in state.values.get("messages", [])],
|
|
54
|
+
"summarization_event": state.values.get("_summarization_event"),
|
|
55
|
+
}
|
|
56
|
+
return hashlib.sha256(json.dumps(data, sort_keys=True, default=str).encode()).hexdigest()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _validate_generation(state: Any, expected: str) -> None: # noqa: ANN401 - custom graph state
|
|
60
|
+
"""Discard a prepared summary whose source checkpoint was superseded."""
|
|
61
|
+
if _state_generation(state) != expected:
|
|
62
|
+
message = "Conversation changed during compaction; original history retained"
|
|
63
|
+
raise SummaryUnavailableError(message)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SummaryUnavailableError(RuntimeError):
|
|
67
|
+
"""The provider could not produce a usable compaction summary."""
|
|
68
|
+
|
|
69
|
+
def __init__(self, message: str = "Provider returned an empty response") -> None:
|
|
70
|
+
"""Describe a failed or empty summarization response."""
|
|
71
|
+
super().__init__(message)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def is_compaction_summary(text: str) -> bool:
|
|
75
|
+
"""True if *text* is the synthetic message compaction injects."""
|
|
76
|
+
return text.lstrip().startswith(COMPACTION_SUMMARY_MARKER)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _format_message_content(content: Any) -> str:
|
|
80
|
+
"""Format message content to a string.
|
|
81
|
+
|
|
82
|
+
Handles both string content and content blocks (list of dicts).
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
content: The message content (str or list of content blocks)
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Formatted string representation
|
|
89
|
+
"""
|
|
90
|
+
if isinstance(content, str):
|
|
91
|
+
return content
|
|
92
|
+
|
|
93
|
+
if isinstance(content, list):
|
|
94
|
+
# Handle content blocks (e.g., from Claude)
|
|
95
|
+
text_parts = []
|
|
96
|
+
for block in content:
|
|
97
|
+
if isinstance(block, dict):
|
|
98
|
+
if block.get("type") == "text":
|
|
99
|
+
text_parts.append(block.get("text", ""))
|
|
100
|
+
elif block.get("type") == "tool_use":
|
|
101
|
+
tool_name = block.get("name", "unknown_tool")
|
|
102
|
+
text_parts.append(f"[Called tool: {tool_name}]")
|
|
103
|
+
elif isinstance(block, str):
|
|
104
|
+
text_parts.append(block)
|
|
105
|
+
return " ".join(text_parts)
|
|
106
|
+
|
|
107
|
+
return str(content)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _message_parts(messages: list[BaseMessage]) -> list[str]:
|
|
111
|
+
"""Render each message to a compact ``ROLE: text`` line (per-message truncation)."""
|
|
112
|
+
parts: list[str] = []
|
|
113
|
+
for msg in messages:
|
|
114
|
+
if isinstance(msg, HumanMessage):
|
|
115
|
+
# An earlier summary being folded into a new one goes in whole: cut
|
|
116
|
+
# to 2,000 characters like a user message, everything it carried
|
|
117
|
+
# from before the previous compaction would be lost at this one.
|
|
118
|
+
earlier = msg.additional_kwargs.get("nova_summary")
|
|
119
|
+
if earlier:
|
|
120
|
+
parts.append(f"EARLIER SUMMARY (carry its facts forward):\n{earlier}")
|
|
121
|
+
continue
|
|
122
|
+
content = _format_message_content(msg.content)
|
|
123
|
+
if len(content) > 2000:
|
|
124
|
+
content = content[:2000] + "... [truncated]"
|
|
125
|
+
parts.append(f"USER: {content}")
|
|
126
|
+
elif isinstance(msg, AIMessage):
|
|
127
|
+
content = _format_message_content(msg.content)
|
|
128
|
+
if len(content) > 2000:
|
|
129
|
+
content = content[:2000] + "... [truncated]"
|
|
130
|
+
# Tool calls live in .tool_calls, not .content — on a tool-calling
|
|
131
|
+
# turn the content is usually empty, so without this the summarizer
|
|
132
|
+
# never saw WHICH file was edited or WHAT command ran, and "Files
|
|
133
|
+
# Modified" was guessed from truncated tool results.
|
|
134
|
+
calls = [_render_tool_call(tc) for tc in getattr(msg, "tool_calls", None) or []]
|
|
135
|
+
parts.append("ASSISTANT: " + "\n".join(filter(None, [content, *calls])))
|
|
136
|
+
elif isinstance(msg, ToolMessage):
|
|
137
|
+
content = _format_message_content(msg.content)
|
|
138
|
+
if len(content) > 500:
|
|
139
|
+
content = content[:500] + "... [truncated]"
|
|
140
|
+
parts.append(f"TOOL({msg.name}): {content}")
|
|
141
|
+
return parts
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
_TOOL_ARGS_CHARS = 300
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _render_tool_call(call: dict) -> str:
|
|
148
|
+
"""Render a tool call as ``-> name({args})`` with the args capped.
|
|
149
|
+
|
|
150
|
+
Enough for the summarizer to know what was touched, not a copy of the payload.
|
|
151
|
+
"""
|
|
152
|
+
args = json.dumps(call.get("args") or {}, ensure_ascii=False, default=str)
|
|
153
|
+
if len(args) > _TOOL_ARGS_CHARS:
|
|
154
|
+
args = args[:_TOOL_ARGS_CHARS] + "...}"
|
|
155
|
+
return f"-> {call.get('name', 'tool')}({args})"
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _window_tokens(model: BaseChatModel, context_window: int | None = None) -> int:
|
|
159
|
+
"""Return the model's context window in tokens.
|
|
160
|
+
|
|
161
|
+
The caller's value wins (the TUI passes the tracker's effective window,
|
|
162
|
+
which accounts for Ollama ``num_ctx``); else the static table
|
|
163
|
+
(``use_dynamic=False`` — no slow/flaky live query here).
|
|
164
|
+
"""
|
|
165
|
+
if context_window and context_window > 0:
|
|
166
|
+
return context_window
|
|
167
|
+
name = getattr(model, "model_name", None) or getattr(model, "model", None) or ""
|
|
168
|
+
try:
|
|
169
|
+
from novacode_cli.context._analysis import get_context_window_size
|
|
170
|
+
|
|
171
|
+
return get_context_window_size(str(name), use_dynamic=False)
|
|
172
|
+
except Exception: # noqa: BLE001
|
|
173
|
+
return 8192
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _budget_chars(model: BaseChatModel, context_window: int | None = None) -> int:
|
|
177
|
+
"""Character budget for the summarizer's input, sized to the model's context
|
|
178
|
+
window so a long conversation never overflows the summarization call itself.
|
|
179
|
+
|
|
180
|
+
Reserves ~45% of the window for the prompt template + generated summary;
|
|
181
|
+
rough 4-chars/token.
|
|
182
|
+
"""
|
|
183
|
+
return max(8000, int(_window_tokens(model, context_window) * 0.55) * 4)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# The recent tail compaction keeps VERBATIM, as a share of the window, capped.
|
|
187
|
+
# Codex keeps ~20k tokens of recent messages; deepagents' own summarizer keeps
|
|
188
|
+
# 10% of the window. Summarizing the turn the agent is in the middle of is the
|
|
189
|
+
# lossiest possible cut — the exact error text, the file just edited, the
|
|
190
|
+
# user's latest wording all become a paraphrase.
|
|
191
|
+
KEEP_TAIL_FRACTION = 0.10
|
|
192
|
+
KEEP_TAIL_MAX_TOKENS = 20_000
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _message_chars(msg: BaseMessage) -> int:
|
|
196
|
+
size = len(_format_message_content(msg.content))
|
|
197
|
+
for call in getattr(msg, "tool_calls", None) or []:
|
|
198
|
+
size += len(json.dumps(call.get("args") or {}, default=str))
|
|
199
|
+
return size
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _split_tail(messages: list[BaseMessage], keep_tokens: int) -> int:
|
|
203
|
+
"""Index where the verbatim tail starts (``len(messages)`` = keep nothing).
|
|
204
|
+
|
|
205
|
+
Cuts only at a real user message, so a tool call is never separated from
|
|
206
|
+
its result, and takes the EARLIEST such cut whose tail fits the budget. A
|
|
207
|
+
tail with a ToolMessage whose call is not in the tail is rejected as a
|
|
208
|
+
second guard (a steer injected mid-turn is also a HumanMessage).
|
|
209
|
+
"""
|
|
210
|
+
budget = keep_tokens * 4
|
|
211
|
+
best = len(messages)
|
|
212
|
+
size = 0
|
|
213
|
+
for i in range(len(messages) - 1, 0, -1): # index 0: nothing left to summarize
|
|
214
|
+
size += _message_chars(messages[i])
|
|
215
|
+
if size > budget:
|
|
216
|
+
break
|
|
217
|
+
msg = messages[i]
|
|
218
|
+
if isinstance(msg, HumanMessage) and not is_compaction_summary(
|
|
219
|
+
_format_message_content(msg.content)
|
|
220
|
+
):
|
|
221
|
+
best = i
|
|
222
|
+
tail = messages[best:]
|
|
223
|
+
issued = {tc.get("id") for m in tail for tc in (getattr(m, "tool_calls", None) or [])}
|
|
224
|
+
if any(isinstance(m, ToolMessage) and m.tool_call_id not in issued for m in tail):
|
|
225
|
+
return len(messages)
|
|
226
|
+
return best
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _rehydration_note() -> str:
|
|
230
|
+
"""Paths the agent touched, so it can re-read them just in time.
|
|
231
|
+
|
|
232
|
+
Claude Code and Codex re-attach recently used files after compacting. Paths
|
|
233
|
+
are the cheap form of that: the content is on disk, the summary only has
|
|
234
|
+
to say where to look.
|
|
235
|
+
"""
|
|
236
|
+
try:
|
|
237
|
+
from novacode_cli.tracking.file_tracker import (
|
|
238
|
+
get_modified_files,
|
|
239
|
+
get_recently_read_files,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
modified = get_modified_files()[-15:]
|
|
243
|
+
read = [p for p in get_recently_read_files(15) if p not in modified]
|
|
244
|
+
except Exception: # noqa: BLE001 — a hint, never worth failing compaction
|
|
245
|
+
return ""
|
|
246
|
+
lines = []
|
|
247
|
+
if modified:
|
|
248
|
+
lines.append("- Modified: " + ", ".join(modified))
|
|
249
|
+
if read:
|
|
250
|
+
lines.append("- Read recently: " + ", ".join(read))
|
|
251
|
+
if not lines:
|
|
252
|
+
return ""
|
|
253
|
+
return (
|
|
254
|
+
"## Files touched this session\n"
|
|
255
|
+
"Re-read a file before relying on its contents; the summary only "
|
|
256
|
+
"describes it.\n" + "\n".join(lines)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _continuation_framing(*, tail_kept: bool) -> str:
|
|
261
|
+
"""Mark compacted requests as history, with the current tail taking precedence."""
|
|
262
|
+
tail = (
|
|
263
|
+
"The verbatim tail after this summary is authoritative for current progress. "
|
|
264
|
+
if tail_kept
|
|
265
|
+
else "Use the latest recorded state to determine current progress. "
|
|
266
|
+
)
|
|
267
|
+
return (
|
|
268
|
+
"The earlier conversation was compacted into the summary below. "
|
|
269
|
+
+ tail
|
|
270
|
+
+ "Continue the work only if it is unfinished, from the last observed action. "
|
|
271
|
+
"Do not repeat completed steps or tool calls. Do not acknowledge or restate this summary."
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _historical_user_requests(requests: list[str]) -> str:
|
|
276
|
+
"""Preserve earlier user words without presenting completed requests as new turns."""
|
|
277
|
+
if not requests:
|
|
278
|
+
return ""
|
|
279
|
+
return (
|
|
280
|
+
"## Earlier user requests and constraints (historical context)\n\n"
|
|
281
|
+
"Keep applicable constraints, but do not assume these requests are still pending. "
|
|
282
|
+
"The summary and any verbatim tail record what was already completed.\n\n"
|
|
283
|
+
+ "\n\n".join(f"{index}. {text}" for index, text in enumerate(requests, 1))
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _archive_messages(thread_id: str, messages: list[BaseMessage]) -> Path | None:
|
|
288
|
+
"""Write the pre-compaction transcript to the session dir; return its path.
|
|
289
|
+
|
|
290
|
+
Compaction is the one irreversible step in the context pipeline: the
|
|
291
|
+
summary is a paraphrase and the originals are replaced. LangGraph keeps the
|
|
292
|
+
old checkpoint, but nothing surfaces it — so the raw messages also go to a
|
|
293
|
+
file next to the session, the way Claude Code keeps a pre-compaction
|
|
294
|
+
transcript. Reaped with the session directory. Best-effort.
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
from novacode_cli.context.archives import default_recovery_store
|
|
298
|
+
return default_recovery_store().archive(thread_id, messages)
|
|
299
|
+
except Exception:
|
|
300
|
+
logger.debug("Could not publish recovery archive", exc_info=True)
|
|
301
|
+
return None
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _chunk_parts(parts: list[str], max_chars: int) -> list[list[str]]:
|
|
305
|
+
"""Group formatted parts into chunks each within ``max_chars``."""
|
|
306
|
+
chunks: list[list[str]] = []
|
|
307
|
+
cur: list[str] = []
|
|
308
|
+
cur_len = 0
|
|
309
|
+
for p in parts:
|
|
310
|
+
if len(p) > max_chars: # a single oversized part — hard-truncate it
|
|
311
|
+
p = p[:max_chars] + "... [truncated]"
|
|
312
|
+
if cur and cur_len + len(p) + 2 > max_chars:
|
|
313
|
+
chunks.append(cur)
|
|
314
|
+
cur, cur_len = [], 0
|
|
315
|
+
cur.append(p)
|
|
316
|
+
cur_len += len(p) + 2
|
|
317
|
+
if cur:
|
|
318
|
+
chunks.append(cur)
|
|
319
|
+
return chunks
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def _summarize_text(
|
|
323
|
+
model: BaseChatModel,
|
|
324
|
+
conversation_text: str,
|
|
325
|
+
focus_instructions: str | None,
|
|
326
|
+
*,
|
|
327
|
+
tail_kept: bool = False,
|
|
328
|
+
) -> str:
|
|
329
|
+
"""One summarization LLM call over ``conversation_text``."""
|
|
330
|
+
if focus_instructions:
|
|
331
|
+
focus_text = (
|
|
332
|
+
f"\n**IMPORTANT - User requested focus on**: {focus_instructions}\n\n"
|
|
333
|
+
"Make sure to especially preserve information related to this focus area.\n"
|
|
334
|
+
)
|
|
335
|
+
else:
|
|
336
|
+
focus_text = ""
|
|
337
|
+
prompt = render_template(
|
|
338
|
+
"summarization.jinja",
|
|
339
|
+
focus_instructions=focus_text,
|
|
340
|
+
conversation=conversation_text,
|
|
341
|
+
tail_kept=tail_kept,
|
|
342
|
+
)
|
|
343
|
+
from novacode_cli.errors import is_retryable_model_error
|
|
344
|
+
|
|
345
|
+
for attempt in range(SUMMARY_ATTEMPTS):
|
|
346
|
+
try:
|
|
347
|
+
response = await asyncio.wait_for(
|
|
348
|
+
_summary_call(model, prompt, "summary.generate"), SUMMARY_TIMEOUT_SECONDS
|
|
349
|
+
)
|
|
350
|
+
summary = _format_message_content(response.content).strip()
|
|
351
|
+
if not summary:
|
|
352
|
+
raise SummaryUnavailableError() # noqa: TRY301 - retry empty output
|
|
353
|
+
return summary # noqa: TRY300
|
|
354
|
+
except Exception as error:
|
|
355
|
+
if not is_retryable_model_error(error):
|
|
356
|
+
raise
|
|
357
|
+
if attempt + 1 == SUMMARY_ATTEMPTS:
|
|
358
|
+
message = (
|
|
359
|
+
"The summary provider is unavailable after 3 attempts. "
|
|
360
|
+
"Conversation history was kept. Try /compact again shortly "
|
|
361
|
+
"or switch models with /model."
|
|
362
|
+
)
|
|
363
|
+
raise SummaryUnavailableError(message) from error
|
|
364
|
+
logger.debug("Retrying compaction summary", exc_info=True)
|
|
365
|
+
await asyncio.sleep(2**attempt)
|
|
366
|
+
message = "No summary attempts configured"
|
|
367
|
+
raise AssertionError(message)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
def _answer_budget(budget: int, questions: str) -> int:
|
|
371
|
+
"""Conversation budget for the answer pass (reserve room for questions + template)."""
|
|
372
|
+
return max(1000, budget - len(questions) - 500)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
async def _generate_questions(model: BaseChatModel, summary: str) -> str:
|
|
376
|
+
"""One LLM call: read the summary and ask what important details are missing."""
|
|
377
|
+
prompt = render_template("summarization_questions.jinja", summary=summary)
|
|
378
|
+
response = await _summary_call(model, prompt, "summary.questions")
|
|
379
|
+
return _format_message_content(response.content).strip()
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
async def _answer_questions(
|
|
383
|
+
model: BaseChatModel, questions: str, parts: list[str], budget: int
|
|
384
|
+
) -> str:
|
|
385
|
+
"""Answer the clarifying questions from the full conversation.
|
|
386
|
+
|
|
387
|
+
Fits in one call when the conversation does; otherwise answers per chunk
|
|
388
|
+
and merges the per-chunk answers into one consolidated Q&A block.
|
|
389
|
+
"""
|
|
390
|
+
text = "\n\n".join(parts)
|
|
391
|
+
q_budget = _answer_budget(budget, questions)
|
|
392
|
+
if len(text) <= q_budget:
|
|
393
|
+
prompt = render_template(
|
|
394
|
+
"summarization_answers.jinja", questions=questions, conversation=text
|
|
395
|
+
)
|
|
396
|
+
response = await _summary_call(model, prompt, "summary.answers")
|
|
397
|
+
return _format_message_content(response.content).strip()
|
|
398
|
+
|
|
399
|
+
per_chunk: list[str] = []
|
|
400
|
+
for chunk in _chunk_parts(parts, q_budget):
|
|
401
|
+
prompt = render_template(
|
|
402
|
+
"summarization_answers.jinja",
|
|
403
|
+
questions=questions,
|
|
404
|
+
conversation="\n\n".join(chunk),
|
|
405
|
+
)
|
|
406
|
+
response = await _summary_call(model, prompt, "summary.chunk_answers")
|
|
407
|
+
per_chunk.append(_format_message_content(response.content).strip())
|
|
408
|
+
|
|
409
|
+
# Merge: consolidate the per-chunk answers into one Q&A block (bounded).
|
|
410
|
+
merged = "\n\n".join(
|
|
411
|
+
f"## Excerpt {i + 1} of {len(per_chunk)}\n{a}" for i, a in enumerate(per_chunk)
|
|
412
|
+
)
|
|
413
|
+
prompt = render_template(
|
|
414
|
+
"summarization_answers.jinja",
|
|
415
|
+
questions=questions,
|
|
416
|
+
conversation=merged[:q_budget],
|
|
417
|
+
)
|
|
418
|
+
response = await _summary_call(model, prompt, "summary.merge_answers")
|
|
419
|
+
return _format_message_content(response.content).strip()
|
|
420
|
+
|
|
421
|
+
|
|
422
|
+
async def _qa_refine(model: BaseChatModel, summary: str, parts: list[str], budget: int) -> str:
|
|
423
|
+
"""Gap-fill a lossy summary: ask what it missed, restore it from the source.
|
|
424
|
+
|
|
425
|
+
Best-effort — any failure returns ``""`` so compaction falls back to the
|
|
426
|
+
plain summary. Returns the Q&A block to append, or ``""`` when nothing
|
|
427
|
+
is missing.
|
|
428
|
+
"""
|
|
429
|
+
try:
|
|
430
|
+
questions = await _generate_questions(model, summary)
|
|
431
|
+
if not questions or "NONE" in questions.upper():
|
|
432
|
+
return ""
|
|
433
|
+
answers = await _answer_questions(model, questions, parts, budget)
|
|
434
|
+
if not answers.strip():
|
|
435
|
+
return ""
|
|
436
|
+
return f"## Clarifying Q&A\n\n{answers.strip()}"
|
|
437
|
+
except Exception: # noqa: BLE001 — refinement must never break compaction
|
|
438
|
+
return ""
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
async def summarize_conversation(
|
|
442
|
+
model: BaseChatModel,
|
|
443
|
+
messages: list[BaseMessage],
|
|
444
|
+
focus_instructions: str | None = None,
|
|
445
|
+
context_window: int | None = None,
|
|
446
|
+
*,
|
|
447
|
+
tail_kept: bool = False,
|
|
448
|
+
) -> str:
|
|
449
|
+
"""Summarize a conversation, budgeted to the model's context window.
|
|
450
|
+
|
|
451
|
+
Short conversations are summarized in a single call. Long ones (that would
|
|
452
|
+
overflow the summarizer's own window — exactly when compaction matters most)
|
|
453
|
+
are summarized **hierarchically**: chunk → summarize each chunk → summarize
|
|
454
|
+
the chunk-summaries into one cohesive summary, recursing if still too large.
|
|
455
|
+
The hierarchical path then runs a **Q&A gap-filling pass** (Meta-Harness
|
|
456
|
+
port): a second call reads the summary and asks what important details are
|
|
457
|
+
missing, and a third answers those questions from the full conversation —
|
|
458
|
+
recovering details a single lossy pass drops. Best-effort: any failure falls
|
|
459
|
+
back to the plain summary.
|
|
460
|
+
|
|
461
|
+
Args:
|
|
462
|
+
model: The LLM to use for summarization
|
|
463
|
+
messages: The conversation messages to summarize
|
|
464
|
+
focus_instructions: Optional focus instructions from the user
|
|
465
|
+
context_window: Optional explicit context window (tokens); the TUI passes
|
|
466
|
+
the token tracker's effective window.
|
|
467
|
+
tail_kept: The most recent messages stay verbatim after the summary, so
|
|
468
|
+
the summary covers only what precedes them.
|
|
469
|
+
|
|
470
|
+
Returns:
|
|
471
|
+
The summarized conversation as a string
|
|
472
|
+
|
|
473
|
+
Raises:
|
|
474
|
+
Exception: If the LLM call fails
|
|
475
|
+
"""
|
|
476
|
+
parts = _message_parts(messages)
|
|
477
|
+
budget = _budget_chars(model, context_window)
|
|
478
|
+
text = "\n\n".join(parts)
|
|
479
|
+
|
|
480
|
+
if len(text) <= budget:
|
|
481
|
+
return await _summarize_text(model, text, focus_instructions, tail_kept=tail_kept)
|
|
482
|
+
|
|
483
|
+
# Too big for one call → hierarchical summarization.
|
|
484
|
+
chunks = _chunk_parts(parts, budget)
|
|
485
|
+
summaries: list[str] = []
|
|
486
|
+
for i, chunk in enumerate(chunks):
|
|
487
|
+
s = await _summarize_text(model, "\n\n".join(chunk), focus_instructions)
|
|
488
|
+
summaries.append(f"## Part {i + 1} of {len(chunks)}\n{s}")
|
|
489
|
+
combined = "\n\n".join(summaries)
|
|
490
|
+
|
|
491
|
+
# If even the combined chunk-summaries are too large, collapse them further.
|
|
492
|
+
depth = 0
|
|
493
|
+
while len(combined) > budget and depth < 3:
|
|
494
|
+
depth += 1
|
|
495
|
+
summaries = [
|
|
496
|
+
await _summarize_text(model, "\n\n".join(group), focus_instructions)
|
|
497
|
+
for group in _chunk_parts(combined.split("\n\n"), budget)
|
|
498
|
+
]
|
|
499
|
+
combined = "\n\n".join(summaries)
|
|
500
|
+
|
|
501
|
+
# Final pass: one cohesive summary from the collapsed material.
|
|
502
|
+
final = await _summarize_text(model, combined[:budget], focus_instructions, tail_kept=tail_kept)
|
|
503
|
+
|
|
504
|
+
# Q&A gap-filling (Meta-Harness port): the hierarchical path is lossy — ask
|
|
505
|
+
# what the summary missed and restore it from the full conversation. Only
|
|
506
|
+
# here: short single-pass summaries are already high-fidelity.
|
|
507
|
+
qa = await _qa_refine(model, final, parts, budget)
|
|
508
|
+
if qa:
|
|
509
|
+
final = f"{final}\n\n{qa}"
|
|
510
|
+
return final
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
async def compact_conversation(
|
|
514
|
+
agent: Any,
|
|
515
|
+
model: BaseChatModel,
|
|
516
|
+
thread_id: str,
|
|
517
|
+
focus_instructions: str | None = None,
|
|
518
|
+
context_window: int | None = None,
|
|
519
|
+
agent_dir: Path | None = None,
|
|
520
|
+
) -> CompactionResult:
|
|
521
|
+
"""Prepare a generation-validated rewrite with correlated summary requests."""
|
|
522
|
+
from novacode_cli.computation_cache import digest
|
|
523
|
+
from novacode_cli.tracking.request_metrics import correlate
|
|
524
|
+
|
|
525
|
+
with correlate(digest(thread_id)):
|
|
526
|
+
return await _compact_conversation(
|
|
527
|
+
agent, model, thread_id, focus_instructions, context_window, agent_dir
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
async def _compact_conversation(
|
|
532
|
+
agent: Any, # noqa: ANN401 - custom compiled graphs share a duck-typed state API
|
|
533
|
+
model: BaseChatModel,
|
|
534
|
+
thread_id: str,
|
|
535
|
+
focus_instructions: str | None = None,
|
|
536
|
+
context_window: int | None = None,
|
|
537
|
+
agent_dir: Path | None = None,
|
|
538
|
+
) -> CompactionResult:
|
|
539
|
+
"""Compact a conversation by summarizing and replacing history.
|
|
540
|
+
|
|
541
|
+
This function:
|
|
542
|
+
1. Retrieves the current conversation history
|
|
543
|
+
2. Generates a summary using the LLM
|
|
544
|
+
3. Replaces the conversation with a single summary message
|
|
545
|
+
|
|
546
|
+
Args:
|
|
547
|
+
agent: The LangGraph agent
|
|
548
|
+
model: The LLM model for summarization
|
|
549
|
+
thread_id: The thread/session ID
|
|
550
|
+
focus_instructions: Optional user instructions for what to preserve
|
|
551
|
+
context_window: Model context window (tokens) to budget the summary to
|
|
552
|
+
agent_dir: If given, the compaction summary is also persisted as a
|
|
553
|
+
durable memory lesson (``memories/session-summary.md``) so the
|
|
554
|
+
conversation's knowledge survives the rewrite.
|
|
555
|
+
|
|
556
|
+
Returns:
|
|
557
|
+
CompactionResult with details of the compaction operation
|
|
558
|
+
"""
|
|
559
|
+
config = {"configurable": {"thread_id": thread_id}}
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
# Get current state
|
|
563
|
+
state = await agent.aget_state(config)
|
|
564
|
+
generation = _state_generation(state)
|
|
565
|
+
messages = state.values.get("messages", [])
|
|
566
|
+
|
|
567
|
+
if not messages:
|
|
568
|
+
return CompactionResult(
|
|
569
|
+
success=False,
|
|
570
|
+
original_tokens=0,
|
|
571
|
+
new_tokens=0,
|
|
572
|
+
tokens_saved=0,
|
|
573
|
+
messages_before=0,
|
|
574
|
+
messages_after=0,
|
|
575
|
+
summary="",
|
|
576
|
+
error="No conversation history to compact.",
|
|
577
|
+
)
|
|
578
|
+
|
|
579
|
+
messages_before = len(messages)
|
|
580
|
+
from novacode_cli.agents.context_compaction import user_messages
|
|
581
|
+
|
|
582
|
+
protected_tokens = sum(max(1, len(text) // 3) for text in user_messages(messages, 0))
|
|
583
|
+
reservation = getattr(model, "max_tokens", None) or 0
|
|
584
|
+
if isinstance(reservation, int) and protected_tokens + reservation > _window_tokens(
|
|
585
|
+
model, context_window
|
|
586
|
+
):
|
|
587
|
+
raise SummaryUnavailableError(
|
|
588
|
+
"Protected instructions exceed the usable context window. Select a larger model window or explicitly revise the task. Conversation history was kept."
|
|
589
|
+
)
|
|
590
|
+
# A long transcript is tens of MB of JSON; write it off the UI's loop.
|
|
591
|
+
archived = await asyncio.to_thread(_archive_messages, thread_id, messages)
|
|
592
|
+
if not archived:
|
|
593
|
+
raise SummaryUnavailableError(
|
|
594
|
+
"Compaction archive could not be saved. Conversation history was kept."
|
|
595
|
+
)
|
|
596
|
+
from novacode_cli.session.checkpoint_coordinator import CoordinatedGraph
|
|
597
|
+
if isinstance(agent, CoordinatedGraph):
|
|
598
|
+
from novacode_cli.context.archives import default_recovery_store
|
|
599
|
+
saver = agent.coordinator.saver
|
|
600
|
+
original = await saver.aget_tuple(state.config)
|
|
601
|
+
if original:
|
|
602
|
+
packed = {
|
|
603
|
+
"checkpoint": saver.serde.dumps_typed(original.checkpoint),
|
|
604
|
+
"pending": saver.serde.dumps_typed(original.pending_writes),
|
|
605
|
+
"metadata": original.metadata,
|
|
606
|
+
}
|
|
607
|
+
archived = await asyncio.to_thread(
|
|
608
|
+
default_recovery_store().archive,
|
|
609
|
+
thread_id,
|
|
610
|
+
messages,
|
|
611
|
+
checkpoint=packed,
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
# These display statistics are estimates. Provider token counters may
|
|
615
|
+
# make synchronous network calls; use Nova's local, image-aware estimate.
|
|
616
|
+
from novacode_cli.context.estimates import estimate_content
|
|
617
|
+
|
|
618
|
+
original_tokens = (await asyncio.to_thread(estimate_content, messages)).total
|
|
619
|
+
|
|
620
|
+
# Keep the recent tail verbatim; summarize only what precedes it.
|
|
621
|
+
keep_tokens = min(
|
|
622
|
+
KEEP_TAIL_MAX_TOKENS,
|
|
623
|
+
int(_window_tokens(model, context_window) * KEEP_TAIL_FRACTION),
|
|
624
|
+
)
|
|
625
|
+
cut = _split_tail(messages, keep_tokens)
|
|
626
|
+
from novacode_cli.agents.context_compaction import pending_tool_start
|
|
627
|
+
|
|
628
|
+
cut = min(cut, pending_tool_start(messages))
|
|
629
|
+
if cut == 0:
|
|
630
|
+
raise SummaryUnavailableError(
|
|
631
|
+
"Pending tool calls prevent compaction. Resolve the pending approval or tool call first. Conversation history was kept."
|
|
632
|
+
)
|
|
633
|
+
head, tail = messages[:cut], messages[cut:]
|
|
634
|
+
|
|
635
|
+
# Generate summary (budgeted to the model's context window)
|
|
636
|
+
summary = await summarize_conversation(
|
|
637
|
+
model,
|
|
638
|
+
head,
|
|
639
|
+
focus_instructions,
|
|
640
|
+
context_window=context_window,
|
|
641
|
+
tail_kept=bool(tail),
|
|
642
|
+
)
|
|
643
|
+
|
|
644
|
+
# Replace the conversation in a single atomic update: REMOVE_ALL then
|
|
645
|
+
# summary + tail, so the summary lands BEFORE the tail (a plain
|
|
646
|
+
# RemoveMessage per id would append it after) and the state never
|
|
647
|
+
# passes through an invalid intermediate (ToolMessages with no
|
|
648
|
+
# AIMessage), which crashes langchain's _fetch_last_ai_and_tool_messages.
|
|
649
|
+
framing = _continuation_framing(tail_kept=bool(tail))
|
|
650
|
+
archive_note = (
|
|
651
|
+
f"The full pre-compaction transcript is at {archived} if an exact detail is needed."
|
|
652
|
+
if archived
|
|
653
|
+
else ""
|
|
654
|
+
)
|
|
655
|
+
from novacode_cli.agents.context_compaction import user_content_blocks, user_messages
|
|
656
|
+
from novacode_cli.context.task_progress import render_task_progress, task_progress
|
|
657
|
+
|
|
658
|
+
said = user_messages(head, 0)
|
|
659
|
+
progress = task_progress(head, state.values.get("todos"))
|
|
660
|
+
protected = _historical_user_requests(said)
|
|
661
|
+
body = "\n\n".join(
|
|
662
|
+
filter(None, [framing, protected, summary, render_task_progress(progress),
|
|
663
|
+
_rehydration_note(), archive_note])
|
|
664
|
+
)
|
|
665
|
+
blocks = user_content_blocks(head)
|
|
666
|
+
content = f"{COMPACTION_SUMMARY_MARKER}\n\n{body}"
|
|
667
|
+
summary_message = HumanMessage(
|
|
668
|
+
content=[{"type": "text", "text": content}, *blocks] if blocks else content,
|
|
669
|
+
additional_kwargs={
|
|
670
|
+
"lc_source": "summarization",
|
|
671
|
+
"user_messages": said,
|
|
672
|
+
"nova_summary": summary,
|
|
673
|
+
"nova_task_progress": progress,
|
|
674
|
+
"history_files": [str(archived)] if archived else [],
|
|
675
|
+
"user_content_blocks": blocks,
|
|
676
|
+
},
|
|
677
|
+
)
|
|
678
|
+
# Also clear any prior auto-summarization event.
|
|
679
|
+
# ContextCompactionMiddleware reconstructs the effective message list from
|
|
680
|
+
# `_summarization_event` (a cutoff index into the OLD message list); if we
|
|
681
|
+
# rewrite messages without clearing it, the next turn would slice the new
|
|
682
|
+
# list at a stale index and corrupt context. Resetting it makes the fresh
|
|
683
|
+
# summary the whole context.
|
|
684
|
+
new_messages = [RemoveMessage(id=REMOVE_ALL_MESSAGES), summary_message, *tail]
|
|
685
|
+
update_values: dict[str, Any] = {
|
|
686
|
+
"messages": new_messages,
|
|
687
|
+
"_summarization_event": None,
|
|
688
|
+
}
|
|
689
|
+
current = await agent.aget_state(config)
|
|
690
|
+
_validate_generation(current, generation)
|
|
691
|
+
from novacode_cli.session.checkpoint_coordinator import CoordinatedGraph
|
|
692
|
+
commit_options = {}
|
|
693
|
+
if isinstance(agent, CoordinatedGraph):
|
|
694
|
+
commit_options["expected_revision"] = (state.config or {}).get("configurable", {}).get("checkpoint_id")
|
|
695
|
+
try:
|
|
696
|
+
await agent.aupdate_state(config=config, values=update_values, as_node="model", **commit_options)
|
|
697
|
+
except Exception:
|
|
698
|
+
# Older graphs without the summarization state key reject the extra
|
|
699
|
+
# field; retry with just the message rewrite.
|
|
700
|
+
current = await agent.aget_state(config)
|
|
701
|
+
_validate_generation(current, generation)
|
|
702
|
+
await agent.aupdate_state(
|
|
703
|
+
config=config,
|
|
704
|
+
values={"messages": new_messages},
|
|
705
|
+
as_node="model",
|
|
706
|
+
**commit_options,
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
new_tokens = (await asyncio.to_thread(estimate_content, [summary_message, *tail])).total
|
|
710
|
+
|
|
711
|
+
# Persist the summary as a durable memory lesson so the conversation's
|
|
712
|
+
# knowledge survives the rewrite (best-effort; never fails compaction).
|
|
713
|
+
learnings = ""
|
|
714
|
+
if agent_dir is not None:
|
|
715
|
+
try:
|
|
716
|
+
from novacode_cli.hermes.memory_tiers import record_lesson
|
|
717
|
+
|
|
718
|
+
# The marker travels WITH the artifact: a derived summary is
|
|
719
|
+
# injected into future prompts, so it must carry its own
|
|
720
|
+
# provenance rather than relying on an instruction elsewhere to
|
|
721
|
+
# establish precedence. Unlabelled, it reads as verified fact.
|
|
722
|
+
record_lesson(
|
|
723
|
+
agent_dir,
|
|
724
|
+
"session-summary",
|
|
725
|
+
"[derived summary, not a verified record — may be incomplete; "
|
|
726
|
+
"observed reality wins on any conflict]\n\n" + summary,
|
|
727
|
+
)
|
|
728
|
+
learnings = summary
|
|
729
|
+
except Exception: # noqa: BLE001 — memory persistence is best-effort
|
|
730
|
+
learnings = ""
|
|
731
|
+
|
|
732
|
+
return CompactionResult(
|
|
733
|
+
success=True,
|
|
734
|
+
original_tokens=original_tokens,
|
|
735
|
+
new_tokens=new_tokens,
|
|
736
|
+
tokens_saved=max(0, original_tokens - new_tokens),
|
|
737
|
+
messages_before=messages_before,
|
|
738
|
+
messages_after=1 + len(tail),
|
|
739
|
+
summary=summary,
|
|
740
|
+
learnings=learnings,
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
except Exception as e:
|
|
744
|
+
return CompactionResult(
|
|
745
|
+
success=False,
|
|
746
|
+
original_tokens=0,
|
|
747
|
+
new_tokens=0,
|
|
748
|
+
tokens_saved=0,
|
|
749
|
+
messages_before=0,
|
|
750
|
+
messages_after=0,
|
|
751
|
+
summary="",
|
|
752
|
+
error=str(e),
|
|
753
|
+
)
|