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,279 @@
|
|
|
1
|
+
"""The two cheap context strategies that run before an automatic compaction.
|
|
2
|
+
|
|
3
|
+
Nova shrinks a long conversation in three steps, cheapest first:
|
|
4
|
+
|
|
5
|
+
1. **Snip** (:func:`snip_history`) drops zombie messages and stale markers:
|
|
6
|
+
empty assistant turns, repeated copies of the same internal-context note, file
|
|
7
|
+
reads superseded by a later read of the same range, and to-do lists replaced
|
|
8
|
+
by a later one. Nothing the model still needs is lost: the newer copy stays.
|
|
9
|
+
2. **Collapse** (:func:`collapse_turns`) restructures completed turns before a
|
|
10
|
+
boundary: each keeps the user's message and the turn's final answer verbatim,
|
|
11
|
+
and its tool loop becomes one digest line per call, with every large result
|
|
12
|
+
saved to ``/cleared/`` so ``read_file`` can bring it back.
|
|
13
|
+
3. **Auto-compact** (``agents/context_compaction.py``) summarizes with a model
|
|
14
|
+
call, only if the first two were not enough.
|
|
15
|
+
|
|
16
|
+
Both are request-local views, never edits to state: checkpoints, ``/rewind``,
|
|
17
|
+
``/tree`` and resume see the full history. Both leave the current turn alone
|
|
18
|
+
(everything from the latest user message on), so a running agent's working set
|
|
19
|
+
is never edited mid-run, and they only change at a turn boundary, which keeps
|
|
20
|
+
the provider's prompt cache stable within a run.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import json
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import TYPE_CHECKING, Any
|
|
28
|
+
|
|
29
|
+
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
|
30
|
+
|
|
31
|
+
from novacode_cli.core.streaming import is_internal_context_text
|
|
32
|
+
|
|
33
|
+
if TYPE_CHECKING:
|
|
34
|
+
from collections.abc import Callable, Sequence
|
|
35
|
+
|
|
36
|
+
from langchain_core.messages import AnyMessage
|
|
37
|
+
|
|
38
|
+
#: Completed turns kept verbatim at the end (the current one is always kept).
|
|
39
|
+
KEEP_TURNS = 3
|
|
40
|
+
#: Collapse once the request reaches this share of the window (auto-compact is ~0.82).
|
|
41
|
+
COLLAPSE_AT = 0.6
|
|
42
|
+
#: Results at least this long are saved to /cleared/ when collapsed.
|
|
43
|
+
OFFLOAD_CHARS = 400
|
|
44
|
+
#: Digest lines per collapsed turn before "… N more".
|
|
45
|
+
DIGEST_LINES = 40
|
|
46
|
+
|
|
47
|
+
SUMMARY_SOURCE = "summarization"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class StrategyReport:
|
|
52
|
+
"""What a strategy removed from the request, for /context."""
|
|
53
|
+
|
|
54
|
+
messages: int = 0
|
|
55
|
+
chars: int = 0
|
|
56
|
+
turns: int = 0
|
|
57
|
+
kinds: dict[str, int] = field(default_factory=dict)
|
|
58
|
+
|
|
59
|
+
def add(self, kind: str, chars: int, messages: int = 0) -> None:
|
|
60
|
+
"""Count one snip or collapse."""
|
|
61
|
+
self.kinds[kind] = self.kinds.get(kind, 0) + 1
|
|
62
|
+
self.chars += max(0, chars)
|
|
63
|
+
self.messages += messages
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _text(message: AnyMessage) -> str:
|
|
67
|
+
content = message.content
|
|
68
|
+
if isinstance(content, str):
|
|
69
|
+
return content
|
|
70
|
+
parts = []
|
|
71
|
+
for block in content or []:
|
|
72
|
+
if isinstance(block, str):
|
|
73
|
+
parts.append(block)
|
|
74
|
+
elif isinstance(block, dict) and block.get("type") == "text":
|
|
75
|
+
parts.append(str(block.get("text", "")))
|
|
76
|
+
return "\n".join(parts)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def is_user_turn(message: AnyMessage) -> bool:
|
|
80
|
+
"""A message the user typed, not an injected note or a summary."""
|
|
81
|
+
if not isinstance(message, HumanMessage):
|
|
82
|
+
return False
|
|
83
|
+
if (message.additional_kwargs or {}).get("lc_source"):
|
|
84
|
+
return False
|
|
85
|
+
return not is_internal_context_text(_text(message))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def current_turn_start(messages: Sequence[AnyMessage]) -> int:
|
|
89
|
+
"""Index of the latest user message; everything from here is the live turn."""
|
|
90
|
+
for index in range(len(messages) - 1, -1, -1):
|
|
91
|
+
if is_user_turn(messages[index]):
|
|
92
|
+
return index
|
|
93
|
+
return len(messages)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _read_key(call: dict[str, Any]) -> str:
|
|
97
|
+
args = call.get("args") or {}
|
|
98
|
+
path = args.get("file_path") or args.get("path") or ""
|
|
99
|
+
return json.dumps([path, args.get("offset"), args.get("limit")])
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def snip_history( # noqa: PLR0912 - one branch per kind of zombie
|
|
103
|
+
messages: Sequence[AnyMessage],
|
|
104
|
+
) -> tuple[list[AnyMessage], StrategyReport]:
|
|
105
|
+
"""Drop zombies and stale markers from the history before the current turn."""
|
|
106
|
+
report = StrategyReport()
|
|
107
|
+
end = current_turn_start(messages)
|
|
108
|
+
calls: dict[str, dict[str, Any]] = {}
|
|
109
|
+
for message in messages:
|
|
110
|
+
if isinstance(message, AIMessage):
|
|
111
|
+
for call in message.tool_calls or []:
|
|
112
|
+
calls[call.get("id") or ""] = call
|
|
113
|
+
|
|
114
|
+
# Newest occurrence of each read range / internal note / to-do list wins.
|
|
115
|
+
latest_read: dict[str, int] = {}
|
|
116
|
+
latest_note: dict[str, int] = {}
|
|
117
|
+
latest_todos = -1
|
|
118
|
+
for index, message in enumerate(messages):
|
|
119
|
+
if isinstance(message, ToolMessage) and message.status != "error":
|
|
120
|
+
call = calls.get(message.tool_call_id) or {}
|
|
121
|
+
if message.name == "read_file":
|
|
122
|
+
latest_read[_read_key(call)] = index
|
|
123
|
+
elif message.name == "write_todos":
|
|
124
|
+
latest_todos = index
|
|
125
|
+
elif isinstance(message, HumanMessage) and not is_user_turn(message):
|
|
126
|
+
if (message.additional_kwargs or {}).get("lc_source") != SUMMARY_SOURCE:
|
|
127
|
+
latest_note[_text(message)] = index
|
|
128
|
+
|
|
129
|
+
out: list[AnyMessage] = []
|
|
130
|
+
for index, message in enumerate(messages):
|
|
131
|
+
if index >= end:
|
|
132
|
+
out.append(message)
|
|
133
|
+
continue
|
|
134
|
+
if isinstance(message, AIMessage) and not message.tool_calls and not _text(message).strip():
|
|
135
|
+
report.add("empty assistant message", 0, messages=1)
|
|
136
|
+
continue
|
|
137
|
+
if isinstance(message, HumanMessage) and not is_user_turn(message):
|
|
138
|
+
text = _text(message)
|
|
139
|
+
if latest_note.get(text, index) != index:
|
|
140
|
+
report.add("repeated internal note", len(text), messages=1)
|
|
141
|
+
continue
|
|
142
|
+
if isinstance(message, ToolMessage) and message.status != "error":
|
|
143
|
+
call = calls.get(message.tool_call_id) or {}
|
|
144
|
+
if message.name == "read_file" and latest_read.get(_read_key(call), index) != index:
|
|
145
|
+
path = (call.get("args") or {}).get("file_path") or "this file"
|
|
146
|
+
stub = (
|
|
147
|
+
f"[Superseded read of {path}: a later read in this conversation "
|
|
148
|
+
"has the current contents.]"
|
|
149
|
+
)
|
|
150
|
+
report.add("superseded file read", len(_text(message)) - len(stub))
|
|
151
|
+
out.append(message.model_copy(update={"content": stub}))
|
|
152
|
+
continue
|
|
153
|
+
if message.name == "write_todos" and index != latest_todos:
|
|
154
|
+
stub = "[Superseded to-do list: a later write_todos holds the current one.]"
|
|
155
|
+
report.add("superseded to-do list", len(_text(message)) - len(stub))
|
|
156
|
+
out.append(message.model_copy(update={"content": stub}))
|
|
157
|
+
continue
|
|
158
|
+
out.append(message)
|
|
159
|
+
return out, report
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def _one_line(text: str, limit: int = 100) -> str:
|
|
163
|
+
line = " ".join(text.split())
|
|
164
|
+
return line if len(line) <= limit else line[: limit - 1] + "…"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _call_label(call: dict[str, Any]) -> str:
|
|
168
|
+
args = call.get("args") or {}
|
|
169
|
+
target = (
|
|
170
|
+
args.get("file_path")
|
|
171
|
+
or args.get("path")
|
|
172
|
+
or args.get("command")
|
|
173
|
+
or args.get("pattern")
|
|
174
|
+
or args.get("query")
|
|
175
|
+
or next((v for v in args.values() if isinstance(v, str)), "")
|
|
176
|
+
)
|
|
177
|
+
return f"{call.get('name', 'tool')} {_one_line(str(target), 80)}".strip()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def collapse_boundary(messages: Sequence[AnyMessage], keep_turns: int = KEEP_TURNS) -> str | None:
|
|
181
|
+
"""Id of the user message that starts the oldest kept turn, or None."""
|
|
182
|
+
starts = [i for i, m in enumerate(messages) if is_user_turn(m)]
|
|
183
|
+
if len(starts) <= keep_turns:
|
|
184
|
+
return None
|
|
185
|
+
return messages[starts[-keep_turns]].id
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def collapse_turns(
|
|
189
|
+
messages: Sequence[AnyMessage],
|
|
190
|
+
boundary_id: str | None,
|
|
191
|
+
save: Callable[[str, str | None, str], str | None] | None = None,
|
|
192
|
+
) -> tuple[list[AnyMessage], StrategyReport]:
|
|
193
|
+
"""Restructure the completed turns before ``boundary_id`` into digests.
|
|
194
|
+
|
|
195
|
+
``save(call_id, tool_name, text)`` stores a large result and returns the
|
|
196
|
+
placeholder naming where it went (``None`` when it could not be saved).
|
|
197
|
+
"""
|
|
198
|
+
report = StrategyReport()
|
|
199
|
+
end = next((i for i, m in enumerate(messages) if boundary_id and m.id == boundary_id), None)
|
|
200
|
+
if end is None: # no boundary, or it was summarized away
|
|
201
|
+
return list(messages), report
|
|
202
|
+
end = min(end, current_turn_start(messages))
|
|
203
|
+
|
|
204
|
+
out: list[AnyMessage] = []
|
|
205
|
+
index = 0
|
|
206
|
+
while index < end:
|
|
207
|
+
message = messages[index]
|
|
208
|
+
if not is_user_turn(message):
|
|
209
|
+
out.append(message)
|
|
210
|
+
index += 1
|
|
211
|
+
continue
|
|
212
|
+
stop = next((j for j in range(index + 1, end) if is_user_turn(messages[j])), end)
|
|
213
|
+
out.extend(_collapse_turn(list(messages[index:stop]), save, report))
|
|
214
|
+
index = stop
|
|
215
|
+
out.extend(messages[end:])
|
|
216
|
+
return out, report
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _collapse_turn(
|
|
220
|
+
turn: list[AnyMessage],
|
|
221
|
+
save: Callable[[str, str | None, str], str | None] | None,
|
|
222
|
+
report: StrategyReport,
|
|
223
|
+
) -> list[AnyMessage]:
|
|
224
|
+
"""One completed turn: its user messages, one digest, its final answer."""
|
|
225
|
+
loop = [
|
|
226
|
+
m for m in turn if isinstance(m, ToolMessage) or (isinstance(m, AIMessage) and m.tool_calls)
|
|
227
|
+
]
|
|
228
|
+
if not loop:
|
|
229
|
+
return turn
|
|
230
|
+
calls: dict[str, dict[str, Any]] = {}
|
|
231
|
+
for message in loop:
|
|
232
|
+
if isinstance(message, AIMessage):
|
|
233
|
+
for call in message.tool_calls or []:
|
|
234
|
+
calls[call.get("id") or ""] = call
|
|
235
|
+
lines: list[str] = []
|
|
236
|
+
for message in loop:
|
|
237
|
+
if isinstance(message, AIMessage):
|
|
238
|
+
note = _text(message).strip()
|
|
239
|
+
if note:
|
|
240
|
+
lines.append(f"- note: {_one_line(note, 160)}")
|
|
241
|
+
continue
|
|
242
|
+
call = calls.get(message.tool_call_id) or {"name": message.name}
|
|
243
|
+
text = _text(message)
|
|
244
|
+
outcome = "failed" if message.status == "error" else f"{len(text.splitlines())} lines"
|
|
245
|
+
where = (
|
|
246
|
+
save(message.tool_call_id, message.name, text)
|
|
247
|
+
if save and len(text) >= OFFLOAD_CHARS
|
|
248
|
+
else None
|
|
249
|
+
)
|
|
250
|
+
tail = (
|
|
251
|
+
f" (saved: {where})" if where else f": {_one_line(text, 120)}" if text.strip() else ""
|
|
252
|
+
)
|
|
253
|
+
lines.append(f"- {_call_label(call)} → {outcome}{tail}")
|
|
254
|
+
shown = lines[:DIGEST_LINES]
|
|
255
|
+
if len(lines) > DIGEST_LINES:
|
|
256
|
+
shown.append(f"- … {len(lines) - DIGEST_LINES} more steps")
|
|
257
|
+
digest = (
|
|
258
|
+
f"[Collapsed earlier work: {sum(isinstance(m, ToolMessage) for m in loop)} tool calls. "
|
|
259
|
+
"Saved results can be read back with read_file.]\n" + "\n".join(shown)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
in_loop = {id(m) for m in loop}
|
|
263
|
+
kept = [m for m in turn if id(m) not in in_loop]
|
|
264
|
+
before = sum(
|
|
265
|
+
len(_text(m)) + len(json.dumps(getattr(m, "tool_calls", None) or [])) for m in loop
|
|
266
|
+
)
|
|
267
|
+
answer = next((m for m in reversed(kept) if isinstance(m, AIMessage)), None)
|
|
268
|
+
out: list[AnyMessage] = []
|
|
269
|
+
for message in kept:
|
|
270
|
+
if message is answer:
|
|
271
|
+
# One assistant message: digest first, then the turn's answer.
|
|
272
|
+
out.append(AIMessage(content=f"{digest}\n\n{_text(answer)}", id=answer.id))
|
|
273
|
+
else:
|
|
274
|
+
out.append(message)
|
|
275
|
+
if answer is None:
|
|
276
|
+
out.insert(1 if out else 0, AIMessage(content=digest))
|
|
277
|
+
report.turns += 1
|
|
278
|
+
report.add("collapsed turn", before - len(digest), messages=len(loop))
|
|
279
|
+
return out
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Evidence-backed task progress carried by both compaction paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage, ToolMessage
|
|
9
|
+
|
|
10
|
+
from novacode_cli.core.streaming import is_internal_context_text
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from collections.abc import Sequence
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
MAX_ACTIONS = 4
|
|
17
|
+
MAX_TEXT = 120
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _short(value: Any) -> str: # noqa: ANN401 - tool arguments have arbitrary schemas
|
|
21
|
+
text = value if isinstance(value, str) else json.dumps(value, ensure_ascii=False, default=str)
|
|
22
|
+
return " ".join(text.split())[:MAX_TEXT]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _prior(messages: Sequence[BaseMessage]) -> dict[str, Any]:
|
|
26
|
+
for message in reversed(messages):
|
|
27
|
+
if isinstance(message, HumanMessage):
|
|
28
|
+
record = message.additional_kwargs.get("nova_task_progress")
|
|
29
|
+
if isinstance(record, dict) and record.get("version") == 1:
|
|
30
|
+
return record
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def task_progress(
|
|
35
|
+
messages: Sequence[BaseMessage],
|
|
36
|
+
todos: object = None,
|
|
37
|
+
*,
|
|
38
|
+
completed_head: int | None = None,
|
|
39
|
+
) -> dict[str, Any]:
|
|
40
|
+
"""Record observed actions and agent-reported todo state for the latest request.
|
|
41
|
+
|
|
42
|
+
``completed_head`` limits tool evidence to messages included in the summary.
|
|
43
|
+
A verbatim tail remains authoritative and needs no duplicate action record.
|
|
44
|
+
"""
|
|
45
|
+
prior = _prior(messages)
|
|
46
|
+
latest_request = -1
|
|
47
|
+
task = prior.get("task", "unknown")
|
|
48
|
+
for index, message in enumerate(messages):
|
|
49
|
+
if isinstance(message, HumanMessage) and not message.additional_kwargs.get("lc_source"):
|
|
50
|
+
content = message.content
|
|
51
|
+
value = content if isinstance(content, str) else str(content)
|
|
52
|
+
if value and not is_internal_context_text(value):
|
|
53
|
+
latest_request, task = index, _short(value)
|
|
54
|
+
|
|
55
|
+
same_task = latest_request < 0
|
|
56
|
+
actions = list(prior.get("completed_actions", [])) if same_task else []
|
|
57
|
+
prior_action_count = len(actions)
|
|
58
|
+
seen = {item.get("id") for item in actions if isinstance(item, dict)}
|
|
59
|
+
end = len(messages) if completed_head is None else completed_head
|
|
60
|
+
results = {
|
|
61
|
+
message.tool_call_id: message
|
|
62
|
+
for message in messages[:end]
|
|
63
|
+
if isinstance(message, ToolMessage)
|
|
64
|
+
}
|
|
65
|
+
todo_written = False
|
|
66
|
+
for message in messages[max(0, latest_request + 1) : end]:
|
|
67
|
+
if not isinstance(message, AIMessage):
|
|
68
|
+
continue
|
|
69
|
+
for call in message.tool_calls or []:
|
|
70
|
+
if call.get("name") == "write_todos":
|
|
71
|
+
todo_written = True
|
|
72
|
+
call_id = call.get("id")
|
|
73
|
+
result = results.get(call_id)
|
|
74
|
+
if result is None or call_id in seen or call.get("name") == "write_todos":
|
|
75
|
+
continue
|
|
76
|
+
actions.append(
|
|
77
|
+
{
|
|
78
|
+
"id": _short(call_id),
|
|
79
|
+
"tool": _short(call.get("name", "unknown")),
|
|
80
|
+
"args": _short(call.get("args") or {}),
|
|
81
|
+
"outcome": "error" if result.status == "error" else "returned",
|
|
82
|
+
"result": _short(result.content),
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
seen.add(call_id)
|
|
86
|
+
|
|
87
|
+
new_action = len(actions) > prior_action_count
|
|
88
|
+
valid_todos = isinstance(todos, list) and (todo_written or (same_task and not new_action))
|
|
89
|
+
if valid_todos:
|
|
90
|
+
reported = [
|
|
91
|
+
_short(item.get("content", ""))
|
|
92
|
+
for item in todos
|
|
93
|
+
if isinstance(item, dict) and item.get("status") == "completed"
|
|
94
|
+
]
|
|
95
|
+
pending = next(
|
|
96
|
+
(
|
|
97
|
+
_short(item.get("content", ""))
|
|
98
|
+
for item in todos
|
|
99
|
+
if isinstance(item, dict) and item.get("status") in {"pending", "in_progress"}
|
|
100
|
+
),
|
|
101
|
+
"unknown",
|
|
102
|
+
)
|
|
103
|
+
elif same_task:
|
|
104
|
+
reported = list(prior.get("reported_completed_todos", []))
|
|
105
|
+
pending = "unknown" if new_action else prior.get("next_action", "unknown")
|
|
106
|
+
else:
|
|
107
|
+
reported, pending = [], "unknown"
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
"version": 1,
|
|
111
|
+
"task": task,
|
|
112
|
+
"completed_actions": actions[-MAX_ACTIONS:],
|
|
113
|
+
"reported_completed_todos": reported[-MAX_ACTIONS:],
|
|
114
|
+
"next_action": pending,
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def render_task_progress(record: dict[str, Any]) -> str:
|
|
119
|
+
"""Expose the structured record to the model without upgrading claims."""
|
|
120
|
+
return "## Task progress (observed tool results; todos are agent-reported)\n" + json.dumps(
|
|
121
|
+
record, ensure_ascii=False, separators=(",", ":")
|
|
122
|
+
)
|
|
File without changes
|