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,303 @@
|
|
|
1
|
+
"""Which provider and model an async-agent graph runs on, chosen per dispatch.
|
|
2
|
+
|
|
3
|
+
The async graphs are built once, when the LangGraph server boots, so their model used
|
|
4
|
+
to be fixed for the life of the process: changing it meant editing ``.env`` and
|
|
5
|
+
recreating the container. This module makes it a per-run choice instead.
|
|
6
|
+
|
|
7
|
+
The CLI attaches the stored ``async`` role to every dispatch as the run's ``context``
|
|
8
|
+
(:func:`current_async_context`), and :class:`AsyncModelOverrideMiddleware` swaps that
|
|
9
|
+
model in for the run. A deployment driven purely by ``docker compose`` still behaves
|
|
10
|
+
exactly as before: no context on the wire means the environment wins
|
|
11
|
+
(:func:`novacode_cli.config.model_create.build_async_agent_model`).
|
|
12
|
+
|
|
13
|
+
Precedence, in one place: a stored ``async`` role beats the environment, because it is
|
|
14
|
+
the later and more specific statement of intent -- the user picked it in ``/model``.
|
|
15
|
+
The one exception is a per-graph variable such as ``PLAN_SCOUT_MODEL``, which is set
|
|
16
|
+
with a single agent in mind and therefore still wins over the shared role.
|
|
17
|
+
|
|
18
|
+
Models are built with :func:`novacode_cli.config.model_create.build_chat_model` rather
|
|
19
|
+
than langchain's ``init_chat_model``, because the latter does not know Nova's own
|
|
20
|
+
providers (``opencode``, ``nvidia``).
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import asyncio
|
|
26
|
+
import logging
|
|
27
|
+
import os
|
|
28
|
+
from dataclasses import dataclass
|
|
29
|
+
from typing import TYPE_CHECKING, Any
|
|
30
|
+
|
|
31
|
+
from langchain.agents.middleware.types import AgentMiddleware
|
|
32
|
+
|
|
33
|
+
from novacode_cli.agents.model_budget import (
|
|
34
|
+
_MODEL_CALL_BUDGET,
|
|
35
|
+
)
|
|
36
|
+
from novacode_cli.agents.model_budget import (
|
|
37
|
+
_MODEL_REQUEST_TIMEOUT as _MODEL_REQUEST_TIMEOUT,
|
|
38
|
+
)
|
|
39
|
+
from novacode_cli.agents.model_budget import (
|
|
40
|
+
_bounded_model as _bounded_model,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
if TYPE_CHECKING:
|
|
44
|
+
from langchain_core.language_models import BaseChatModel
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
#: Models already built, keyed by ``(provider, model)``. A swap happens on every model
|
|
49
|
+
#: call, so building per call would rebuild a client dozens of times per run. A
|
|
50
|
+
#: concurrent miss builds twice and the last write wins: one extra object, no harm.
|
|
51
|
+
_MODEL_CACHE: dict[tuple[str, str], BaseChatModel] = {}
|
|
52
|
+
|
|
53
|
+
#: Whether :func:`install_async_model_context` has already wrapped the factories.
|
|
54
|
+
_PATCHED = False
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class NovaAsyncContext:
|
|
59
|
+
"""The run context the CLI attaches to every async dispatch.
|
|
60
|
+
|
|
61
|
+
Two plain fields, so the CLI and the graphs can share this without either importing
|
|
62
|
+
the other's machinery.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
provider: str | None = None
|
|
66
|
+
model: str | None = None
|
|
67
|
+
#: The dispatching session's approval setting; delegates auto-approve independently.
|
|
68
|
+
auto_approve: bool = False
|
|
69
|
+
#: Host-provided owner intent; a subagent task description cannot supply consent.
|
|
70
|
+
user_request: str = ""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _fields(context: Any) -> tuple[str, str]:
|
|
74
|
+
"""Read ``(provider, model)`` off *context*, however it arrived.
|
|
75
|
+
|
|
76
|
+
A run context is rehydrated from JSON on the server, so it may be this dataclass or
|
|
77
|
+
a plain mapping depending on how the graph was invoked. Both are accepted.
|
|
78
|
+
"""
|
|
79
|
+
if context is None:
|
|
80
|
+
return "", ""
|
|
81
|
+
if isinstance(context, dict):
|
|
82
|
+
provider, model_name = context.get("provider"), context.get("model")
|
|
83
|
+
else:
|
|
84
|
+
provider = getattr(context, "provider", None)
|
|
85
|
+
model_name = getattr(context, "model", None)
|
|
86
|
+
return str(provider or "").strip(), str(model_name or "").strip()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def build_context_model(context: Any) -> BaseChatModel | None:
|
|
90
|
+
"""Return the model *context* asks for, or ``None`` to leave the graph alone.
|
|
91
|
+
|
|
92
|
+
A context that cannot be built (unknown provider, missing credentials) is logged
|
|
93
|
+
and ignored rather than raised: one bad choice must not fail a background run that
|
|
94
|
+
would otherwise have worked on the graph's own model.
|
|
95
|
+
"""
|
|
96
|
+
provider, model_name = _fields(context)
|
|
97
|
+
if not provider or not model_name:
|
|
98
|
+
return None
|
|
99
|
+
|
|
100
|
+
key = (provider, model_name)
|
|
101
|
+
cached = _MODEL_CACHE.get(key)
|
|
102
|
+
if cached is not None:
|
|
103
|
+
return cached
|
|
104
|
+
|
|
105
|
+
from novacode_cli.config.model_create import build_chat_model
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
built = build_chat_model(provider, model_name)
|
|
109
|
+
except Exception: # noqa: BLE001 - any failure means "use the graph default"
|
|
110
|
+
logger.warning(
|
|
111
|
+
"Async run asked for %s:%s but it could not be built; using the graph default",
|
|
112
|
+
provider,
|
|
113
|
+
model_name,
|
|
114
|
+
exc_info=True,
|
|
115
|
+
)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
_MODEL_CACHE[key] = built
|
|
119
|
+
return built
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class AsyncModelOverrideMiddleware(AgentMiddleware):
|
|
123
|
+
"""Run the graph on the run's model, for that run only.
|
|
124
|
+
|
|
125
|
+
Outermost on purpose: it has to see every model call before any provider-specific
|
|
126
|
+
middleware handles it.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
def __init__(self, *, per_agent_model_var: str | None = None) -> None:
|
|
130
|
+
"""Bind the middleware to one graph.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
per_agent_model_var: An environment variable that pins this graph's model,
|
|
134
|
+
such as plan-scout's ``PLAN_SCOUT_MODEL``. When it is set the run
|
|
135
|
+
context is ignored, so a setting made for this one agent still wins over
|
|
136
|
+
the shared ``async`` role.
|
|
137
|
+
"""
|
|
138
|
+
super().__init__()
|
|
139
|
+
self._per_agent_model_var = per_agent_model_var
|
|
140
|
+
|
|
141
|
+
def _model_for(self, request: Any) -> BaseChatModel | None:
|
|
142
|
+
"""The model this request should use, or ``None`` to leave it as it is."""
|
|
143
|
+
var = self._per_agent_model_var
|
|
144
|
+
if var and os.environ.get(var, "").strip():
|
|
145
|
+
return None
|
|
146
|
+
return build_context_model(getattr(request.runtime, "context", None))
|
|
147
|
+
|
|
148
|
+
def wrap_model_call(self, request: Any, handler: Any) -> Any: # noqa: ANN401
|
|
149
|
+
"""Route the call to the run's model when it named one."""
|
|
150
|
+
model = self._model_for(request)
|
|
151
|
+
return handler(request if model is None else request.override(model=model))
|
|
152
|
+
|
|
153
|
+
async def awrap_model_call(self, request: Any, handler: Any) -> Any: # noqa: ANN401
|
|
154
|
+
"""Async twin of :meth:`wrap_model_call`.
|
|
155
|
+
|
|
156
|
+
The first build of a model reads Nova's settings: the working directory,
|
|
157
|
+
and each provider key through a lock file (``time.sleep``, ``os.unlink``).
|
|
158
|
+
On the server's event loop those are blocking calls, which ``langgraph
|
|
159
|
+
dev`` refuses outright, so the build failed and every run silently fell
|
|
160
|
+
back to the graph's default model. A cached model is returned inline;
|
|
161
|
+
only a miss goes to a thread.
|
|
162
|
+
"""
|
|
163
|
+
key = _fields(getattr(request.runtime, "context", None))
|
|
164
|
+
if not all(key) or key in _MODEL_CACHE: # nothing to build, or already built
|
|
165
|
+
model = self._model_for(request)
|
|
166
|
+
else:
|
|
167
|
+
model = await asyncio.to_thread(self._model_for, request)
|
|
168
|
+
from novacode_cli.agents.model_retry import NovaSubagentRetryMiddleware
|
|
169
|
+
|
|
170
|
+
routed = request if model is None else request.override(model=model)
|
|
171
|
+
return await NovaSubagentRetryMiddleware(call_budget=_MODEL_CALL_BUDGET).awrap_model_call(
|
|
172
|
+
routed, handler
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def current_async_context() -> NovaAsyncContext | None:
|
|
177
|
+
"""The model async agents should run on, as a run context.
|
|
178
|
+
|
|
179
|
+
In order: the stored ``async`` role; else, when ``ASYNC_AGENT_PROVIDER`` or
|
|
180
|
+
``ASYNC_AGENT_MODEL`` is set, ``None`` (the environment is an explicit
|
|
181
|
+
instruction, and the graphs read it themselves); else the main agent's own
|
|
182
|
+
model. That last step is what makes the agents follow whatever provider the
|
|
183
|
+
session runs on: they used to fall back to a fixed Ollama model, a leftover
|
|
184
|
+
of the container they ran in, which failed for anyone without Ollama.
|
|
185
|
+
"""
|
|
186
|
+
from novacode_cli.config.nova_config import NovaConfig
|
|
187
|
+
|
|
188
|
+
role = NovaConfig().get_role_model("async") or {}
|
|
189
|
+
provider = str(role.get("provider") or "").strip()
|
|
190
|
+
model_name = str(role.get("model") or "").strip()
|
|
191
|
+
if provider and model_name:
|
|
192
|
+
return NovaAsyncContext(provider=provider, model=model_name)
|
|
193
|
+
|
|
194
|
+
if (
|
|
195
|
+
os.environ.get("ASYNC_AGENT_PROVIDER", "").strip()
|
|
196
|
+
or os.environ.get("ASYNC_AGENT_MODEL", "").strip()
|
|
197
|
+
):
|
|
198
|
+
return None
|
|
199
|
+
try:
|
|
200
|
+
from novacode_cli.utils.model_info import get_model_info
|
|
201
|
+
|
|
202
|
+
provider, model_name, _display = get_model_info()
|
|
203
|
+
except Exception: # noqa: BLE001 — no main model to inherit: leave it to the graphs
|
|
204
|
+
return None
|
|
205
|
+
if not provider or not model_name:
|
|
206
|
+
return None
|
|
207
|
+
return NovaAsyncContext(provider=str(provider), model=str(model_name))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def install_async_model_context() -> None:
|
|
211
|
+
"""Make every async dispatch carry the stored ``async`` role as run context.
|
|
212
|
+
|
|
213
|
+
deepagents builds the dispatch itself -- ``runs.create(thread_id, assistant_id,
|
|
214
|
+
input)`` -- and passes no ``context``, so the remote graph has no way to know which
|
|
215
|
+
model the user picked. Wrapping the client factory is the narrowest seam for that:
|
|
216
|
+
the request is otherwise left exactly as deepagents made it.
|
|
217
|
+
|
|
218
|
+
The context is read lazily, at dispatch time, so a ``/model`` change applies to the
|
|
219
|
+
very next launch rather than the next server restart. ``runs.create`` already
|
|
220
|
+
accepts ``context`` in langgraph-sdk and the graphs declare ``context_schema``, so
|
|
221
|
+
this is only the wiring in between.
|
|
222
|
+
|
|
223
|
+
Idempotent: calling it twice leaves the factories wrapped once.
|
|
224
|
+
"""
|
|
225
|
+
global _PATCHED # noqa: PLW0603 - the guard is the point
|
|
226
|
+
if _PATCHED:
|
|
227
|
+
return
|
|
228
|
+
_PATCHED = True
|
|
229
|
+
|
|
230
|
+
from deepagents.middleware import async_subagents
|
|
231
|
+
|
|
232
|
+
for name in ("get_client", "get_sync_client"):
|
|
233
|
+
factory = getattr(async_subagents, name, None)
|
|
234
|
+
if factory is None or getattr(factory, "_nova_context_bound", False):
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
def wrapped(*args: Any, _factory: Any = factory, **kwargs: Any) -> Any:
|
|
238
|
+
"""Bind the run context onto the client the factory just built."""
|
|
239
|
+
from novacode_cli.agents.async_task_errors import bind_run_errors
|
|
240
|
+
|
|
241
|
+
return bind_run_errors(_bind_context(_factory(*args, **kwargs)))
|
|
242
|
+
|
|
243
|
+
wrapped._nova_context_bound = True # type: ignore[attr-defined]
|
|
244
|
+
setattr(async_subagents, name, wrapped)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _bind_context(client: Any) -> Any:
|
|
248
|
+
"""Return *client* with ``runs.create`` defaulting ``context`` to the async role."""
|
|
249
|
+
# `getattr` with a default widens to `Any | None`, so a client that exposes no
|
|
250
|
+
# `runs` is left alone and the type is narrowed before the monkey-patch below.
|
|
251
|
+
runs: Any = getattr(client, "runs", None)
|
|
252
|
+
if runs is None:
|
|
253
|
+
return client
|
|
254
|
+
|
|
255
|
+
original = getattr(runs, "create", None)
|
|
256
|
+
if original is None or getattr(original, "_nova_context_bound", False):
|
|
257
|
+
return client
|
|
258
|
+
|
|
259
|
+
def create(*args: Any, **kwargs: Any) -> Any:
|
|
260
|
+
"""Create the run, naming the async role's model unless one was given."""
|
|
261
|
+
if kwargs.get("context") is None:
|
|
262
|
+
from dataclasses import replace
|
|
263
|
+
|
|
264
|
+
from novacode_cli.tools.plan_mode_tools import _auto_approve_var
|
|
265
|
+
|
|
266
|
+
context = current_async_context()
|
|
267
|
+
from novacode_cli.security.approval_context import current_user_request
|
|
268
|
+
|
|
269
|
+
owner_request = current_user_request.get()
|
|
270
|
+
if owner_request is not None:
|
|
271
|
+
context = replace(context or NovaAsyncContext(), user_request=owner_request)
|
|
272
|
+
# Fixed at launch: the run cannot see a later toggle in this session.
|
|
273
|
+
if _auto_approve_var.get():
|
|
274
|
+
context = replace(context or NovaAsyncContext(), auto_approve=True)
|
|
275
|
+
if context is not None:
|
|
276
|
+
config = kwargs.get("config")
|
|
277
|
+
configurable = config.get("configurable") if isinstance(config, dict) else None
|
|
278
|
+
# deepagents uses this configurable only as a fallback for finding
|
|
279
|
+
# the parent. Its async launcher also puts parent_thread_id in the
|
|
280
|
+
# child thread metadata, which is the supported lookup path when a
|
|
281
|
+
# run context is present. LangGraph rejects a run containing both.
|
|
282
|
+
if isinstance(configurable, dict) and "deepagents_parent" in configurable:
|
|
283
|
+
configurable = dict(configurable)
|
|
284
|
+
configurable.pop("deepagents_parent", None)
|
|
285
|
+
config = dict(config)
|
|
286
|
+
if configurable:
|
|
287
|
+
config["configurable"] = configurable
|
|
288
|
+
else:
|
|
289
|
+
config.pop("configurable", None)
|
|
290
|
+
if config:
|
|
291
|
+
kwargs["config"] = config
|
|
292
|
+
else:
|
|
293
|
+
kwargs.pop("config", None)
|
|
294
|
+
# Preserve unrelated configurable values. This run can still launch
|
|
295
|
+
# with its graph default, while avoiding the server's 400 response.
|
|
296
|
+
remaining = config.get("configurable") if isinstance(config, dict) else None
|
|
297
|
+
if not remaining:
|
|
298
|
+
kwargs["context"] = context
|
|
299
|
+
return original(*args, **kwargs)
|
|
300
|
+
|
|
301
|
+
create._nova_context_bound = True # type: ignore[attr-defined]
|
|
302
|
+
runs.create = create
|
|
303
|
+
return client
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Start Nova's own agent server when an async agent is first dispatched.
|
|
2
|
+
|
|
3
|
+
The server used to be started while Nova booted, whether or not the session ever
|
|
4
|
+
delegated anything: a second Python process holding six compiled graphs, on a
|
|
5
|
+
machine that may have no memory to spare. It is now only *planned* at boot (a
|
|
6
|
+
port is reserved, so the specs can name it) and started here, by the first
|
|
7
|
+
``start_async_task``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import asyncio
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from langchain.agents.middleware.types import AgentMiddleware
|
|
16
|
+
from langchain_core.messages import ToolMessage
|
|
17
|
+
|
|
18
|
+
#: The dispatch tool. The others (check/update/cancel/list) only make sense once
|
|
19
|
+
#: something has been started, so they never trigger a launch.
|
|
20
|
+
START_TOOL = "start_async_task"
|
|
21
|
+
|
|
22
|
+
_FAILED = (
|
|
23
|
+
"The async agent server could not be started, so no background task was "
|
|
24
|
+
"launched. Delegate this with the `task` tool (an in-process subagent) instead."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AsyncServerOnDemandMiddleware(AgentMiddleware):
|
|
29
|
+
"""Bring the planned agent server up before the first async dispatch."""
|
|
30
|
+
|
|
31
|
+
async def awrap_tool_call(self, request: Any, handler: Any) -> Any: # noqa: ANN401
|
|
32
|
+
call = getattr(request, "tool_call", None) or {}
|
|
33
|
+
if call.get("name") == START_TOOL:
|
|
34
|
+
from novacode_cli.agents import server_launcher
|
|
35
|
+
|
|
36
|
+
if not await server_launcher.ensure_started():
|
|
37
|
+
return ToolMessage(content=_FAILED, tool_call_id=call.get("id", ""), status="error")
|
|
38
|
+
return await handler(request)
|
|
39
|
+
|
|
40
|
+
def wrap_tool_call(self, request: Any, handler: Any) -> Any: # noqa: ANN401
|
|
41
|
+
"""Sync twin, for a subagent invoked with ``invoke``.
|
|
42
|
+
|
|
43
|
+
That runs on a worker thread with no event loop of its own, so the
|
|
44
|
+
launch gets a short-lived one.
|
|
45
|
+
"""
|
|
46
|
+
call = getattr(request, "tool_call", None) or {}
|
|
47
|
+
if call.get("name") == START_TOOL:
|
|
48
|
+
from novacode_cli.agents import server_launcher
|
|
49
|
+
|
|
50
|
+
if not asyncio.run(server_launcher.ensure_started()):
|
|
51
|
+
return ToolMessage(content=_FAILED, tool_call_id=call.get("id", ""), status="error")
|
|
52
|
+
return handler(request)
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ruff: noqa: INP001 - agents uses namespace packaging
|
|
2
|
+
"""Recover failed async-run diagnostics from LangGraph's checkpoint tasks."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import inspect
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _with_error(run: dict[str, Any], state: dict[str, Any], run_id: str) -> dict[str, Any]:
|
|
14
|
+
# A resumed thread may already have a newer checkpoint from a different run.
|
|
15
|
+
if (state.get("metadata") or {}).get("run_id") != run_id:
|
|
16
|
+
return run
|
|
17
|
+
from novacode_cli.security.prompt_injection import _credential_values, _redact
|
|
18
|
+
from novacode_cli.session.history_export import redact
|
|
19
|
+
|
|
20
|
+
secrets = _credential_values()
|
|
21
|
+
errors = []
|
|
22
|
+
for task in state.get("tasks") or []:
|
|
23
|
+
error = task.get("error")
|
|
24
|
+
if error:
|
|
25
|
+
errors.append(redact(_redact(str(error), secrets)))
|
|
26
|
+
if not errors:
|
|
27
|
+
return run
|
|
28
|
+
return {**run, "error": "\n".join(errors)[:4000]}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def bind_run_errors(client: Any) -> Any: # noqa: ANN401
|
|
32
|
+
"""Enrich runs.get only when the server omitted a failed run's error.
|
|
33
|
+
|
|
34
|
+
LangGraph's Run schema has no error field; errors live in ThreadState.tasks.
|
|
35
|
+
Keep deepagents' normal check tool and state updates by filling that gap at
|
|
36
|
+
its existing client-factory seam. Never fetch or return child transcripts.
|
|
37
|
+
"""
|
|
38
|
+
runs: Any = getattr(client, "runs", None)
|
|
39
|
+
original = getattr(runs, "get", None)
|
|
40
|
+
if original is None or getattr(original, "_nova_errors_bound", False) is True:
|
|
41
|
+
return client
|
|
42
|
+
|
|
43
|
+
def needs_error(run: dict[str, Any]) -> bool:
|
|
44
|
+
return run.get("status") == "error" and not run.get("error")
|
|
45
|
+
|
|
46
|
+
if inspect.iscoroutinefunction(original):
|
|
47
|
+
|
|
48
|
+
async def aget(thread_id: str, run_id: str, **kwargs: Any) -> Any: # noqa: ANN401
|
|
49
|
+
run = await original(thread_id, run_id, **kwargs)
|
|
50
|
+
if needs_error(run):
|
|
51
|
+
try:
|
|
52
|
+
state = await client.threads.get_state(thread_id=thread_id)
|
|
53
|
+
return _with_error(run, state, run_id)
|
|
54
|
+
except Exception: # noqa: BLE001 - status remains usable if state is unavailable
|
|
55
|
+
logger.debug("Failed to fetch async error checkpoint for run %s", run_id)
|
|
56
|
+
return run
|
|
57
|
+
|
|
58
|
+
replacement: Any = aget
|
|
59
|
+
else:
|
|
60
|
+
|
|
61
|
+
def get(thread_id: str, run_id: str, **kwargs: Any) -> Any: # noqa: ANN401
|
|
62
|
+
run = original(thread_id, run_id, **kwargs)
|
|
63
|
+
if needs_error(run):
|
|
64
|
+
try:
|
|
65
|
+
state = client.threads.get_state(thread_id=thread_id)
|
|
66
|
+
return _with_error(run, state, run_id)
|
|
67
|
+
except Exception: # noqa: BLE001 - status remains usable if state is unavailable
|
|
68
|
+
logger.debug("Failed to fetch async error checkpoint for run %s", run_id)
|
|
69
|
+
return run
|
|
70
|
+
|
|
71
|
+
replacement = get
|
|
72
|
+
|
|
73
|
+
replacement._nova_errors_bound = True
|
|
74
|
+
runs.get = replacement
|
|
75
|
+
return client
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""The directory the async-agent graphs work in.
|
|
2
|
+
|
|
3
|
+
The graphs used ``Path.cwd()`` of the server process, which is wherever
|
|
4
|
+
``langgraph.json`` lives: Nova's own repo. Launched from another project they
|
|
5
|
+
scouted Nova's source and reported it as findings about that project.
|
|
6
|
+
|
|
7
|
+
The launcher now names the session's workspace in ``NOVA_WORKSPACE_ROOT`` and the
|
|
8
|
+
graphs read it here. Without the variable (the Docker container, or a server
|
|
9
|
+
someone started by hand) the server's own directory is used, as before.
|
|
10
|
+
|
|
11
|
+
Resolved once, at import: ``os.getcwd()`` is a blocking call that ``langgraph
|
|
12
|
+
dev`` rejects on its event loop.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
WORKSPACE_ROOT_VAR = "NOVA_WORKSPACE_ROOT"
|
|
21
|
+
|
|
22
|
+
_ROOT = Path(os.environ.get(WORKSPACE_ROOT_VAR, "").strip() or os.getcwd()).resolve()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def workspace_root() -> Path:
|
|
26
|
+
"""Where the graphs read, search and run commands."""
|
|
27
|
+
return _ROOT
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Configurable commit and PR attribution without repository hooks or history edits."""
|
|
2
|
+
# ruff: noqa: INP001 -- agents is an existing namespace package.
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import asyncio
|
|
7
|
+
import re
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from langchain.agents.middleware.types import AgentMiddleware
|
|
11
|
+
from langchain_core.messages import SystemMessage
|
|
12
|
+
|
|
13
|
+
from novacode_cli.config.nova_config import (
|
|
14
|
+
DEFAULT_ATTRIBUTION_EMAIL,
|
|
15
|
+
DEFAULT_ATTRIBUTION_NAME,
|
|
16
|
+
NovaConfig,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Awaitable, Callable
|
|
21
|
+
|
|
22
|
+
from langchain.agents.middleware.types import ModelRequest, ModelResponse, ToolCallRequest
|
|
23
|
+
from langchain_core.messages import AIMessage, ToolMessage
|
|
24
|
+
from langgraph.types import Command
|
|
25
|
+
|
|
26
|
+
COAUTHOR = f"Co-Authored-By: {DEFAULT_ATTRIBUTION_NAME} <{DEFAULT_ATTRIBUTION_EMAIL}>"
|
|
27
|
+
LOGO_URL = "https://raw.githubusercontent.com/Babitdor/NovaCode/main/assets/nova-attribution.png"
|
|
28
|
+
PR_FOOTER = (
|
|
29
|
+
f'<img src="{LOGO_URL}" alt="NovaCode logo" width="24" height="24" /> '
|
|
30
|
+
"Generated with [NovaCode](https://github.com/Babitdor/NovaCode)"
|
|
31
|
+
)
|
|
32
|
+
_TRAILER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9-]*\s*:")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def commit_message(message: str, *, coauthor: str = COAUTHOR) -> str:
|
|
36
|
+
"""Append a co-author once, preserving the final existing trailer block."""
|
|
37
|
+
stripped = message.rstrip()
|
|
38
|
+
lines = stripped.splitlines()
|
|
39
|
+
start = max((i + 1 for i, line in enumerate(lines) if not line.strip()), default=0)
|
|
40
|
+
tail = lines[start:]
|
|
41
|
+
trailers = bool(
|
|
42
|
+
start
|
|
43
|
+
and tail
|
|
44
|
+
and _TRAILER.match(tail[0])
|
|
45
|
+
and all(_TRAILER.match(line) or line.startswith((" ", "\t")) for line in tail)
|
|
46
|
+
)
|
|
47
|
+
if trailers and any(line.strip().casefold() == coauthor.casefold() for line in tail):
|
|
48
|
+
return message
|
|
49
|
+
separator = "\n" if trailers else "\n\n"
|
|
50
|
+
return stripped + separator + coauthor + "\n"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def pull_request_body(body: str) -> str:
|
|
54
|
+
"""Append a compact repository-hosted logo footer once."""
|
|
55
|
+
if PR_FOOTER in body:
|
|
56
|
+
return body
|
|
57
|
+
return body.rstrip() + "\n\n---\n\n" + PR_FOOTER
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def attribution_instructions(preferences: dict[str, bool], *, coauthor: str = COAUTHOR) -> str:
|
|
61
|
+
"""Describe the effective preference for shell commands and other transports."""
|
|
62
|
+
commits = (
|
|
63
|
+
"For Git commits you create, append this trailer once, in the final trailer block:\n"
|
|
64
|
+
f"{coauthor}\n"
|
|
65
|
+
"Use git commit --trailer or a commit-message file so quoting is correct "
|
|
66
|
+
"for the current shell."
|
|
67
|
+
if preferences["commits"]
|
|
68
|
+
else "Do not add automatic NovaCode co-author trailers to commits."
|
|
69
|
+
)
|
|
70
|
+
prs = (
|
|
71
|
+
"For pull requests you create, append this exact logo footer once after the description:\n"
|
|
72
|
+
f"{PR_FOOTER}\n"
|
|
73
|
+
"For gh pr create, prefer --body-file with UTF-8 Markdown."
|
|
74
|
+
if preferences["pullRequests"]
|
|
75
|
+
else "Do not add automatic NovaCode text, logos or generated-by footers to pull requests."
|
|
76
|
+
)
|
|
77
|
+
return (
|
|
78
|
+
"<nova_attribution>\n" + commits + "\n" + prs + "\n"
|
|
79
|
+
"These preferences do not authorize commits, pushes or PR creation. Preserve the human "
|
|
80
|
+
"author, existing co-authors and user-supplied content. Do not install Git hooks, change "
|
|
81
|
+
"global Git identity, amend history or create extra commits for attribution. "
|
|
82
|
+
"A co-author trailer credits a commit; it does not establish line-level provenance.\n"
|
|
83
|
+
"</nova_attribution>"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class AttributionMiddleware(AgentMiddleware):
|
|
88
|
+
"""Apply live preferences to requests and structured creation tool payloads."""
|
|
89
|
+
|
|
90
|
+
@staticmethod
|
|
91
|
+
def prepare(request: ModelRequest) -> ModelRequest:
|
|
92
|
+
"""Append preferences without editing stored history or the base prompt."""
|
|
93
|
+
config = NovaConfig()
|
|
94
|
+
identity = config.get_attribution_identity()
|
|
95
|
+
coauthor = f"Co-Authored-By: {identity['name']} <{identity['email']}>"
|
|
96
|
+
note = attribution_instructions(config.get_attribution(), coauthor=coauthor)
|
|
97
|
+
original = request.system_message
|
|
98
|
+
content = original.content if original else ""
|
|
99
|
+
content = (
|
|
100
|
+
content + "\n\n" + note
|
|
101
|
+
if isinstance(content, str)
|
|
102
|
+
else [*content, {"type": "text", "text": note}]
|
|
103
|
+
)
|
|
104
|
+
system = (
|
|
105
|
+
original.model_copy(update={"content": content})
|
|
106
|
+
if original
|
|
107
|
+
else SystemMessage(content=content)
|
|
108
|
+
)
|
|
109
|
+
return request.override(system_message=system)
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def prepare_tool(request: ToolCallRequest) -> ToolCallRequest:
|
|
113
|
+
"""Normalize supported structured payloads; never rewrite shell command strings."""
|
|
114
|
+
name = request.tool_call["name"].lower()
|
|
115
|
+
actions = ("create_pull_request", "git_commit", "create_or_update_file", "push_files")
|
|
116
|
+
action = next((a for a in actions if name == a or name.endswith("_" + a)), None)
|
|
117
|
+
if action is None:
|
|
118
|
+
return request
|
|
119
|
+
config = NovaConfig()
|
|
120
|
+
preferences = config.get_attribution()
|
|
121
|
+
args = dict(request.tool_call.get("args") or {})
|
|
122
|
+
if action == "create_pull_request" and preferences["pullRequests"]:
|
|
123
|
+
if "body" not in args:
|
|
124
|
+
schema = request.tool.args if request.tool else {}
|
|
125
|
+
if "body" not in schema:
|
|
126
|
+
return request
|
|
127
|
+
body = args.get("body", "")
|
|
128
|
+
if not isinstance(body, str):
|
|
129
|
+
return request # Leave malformed arguments for normal schema validation.
|
|
130
|
+
args["body"] = pull_request_body(body)
|
|
131
|
+
elif action != "create_pull_request" and preferences["commits"]:
|
|
132
|
+
if not isinstance(args.get("message"), str):
|
|
133
|
+
return request
|
|
134
|
+
identity = config.get_attribution_identity()
|
|
135
|
+
coauthor = f"Co-Authored-By: {identity['name']} <{identity['email']}>"
|
|
136
|
+
args["message"] = commit_message(args["message"], coauthor=coauthor)
|
|
137
|
+
else:
|
|
138
|
+
return request
|
|
139
|
+
return request.override(tool_call={**request.tool_call, "args": args})
|
|
140
|
+
|
|
141
|
+
def wrap_model_call(
|
|
142
|
+
self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
|
|
143
|
+
) -> ModelResponse | AIMessage:
|
|
144
|
+
"""Supply attribution preferences to synchronous model calls."""
|
|
145
|
+
return handler(self.prepare(request))
|
|
146
|
+
|
|
147
|
+
async def awrap_model_call(
|
|
148
|
+
self, request: ModelRequest, handler: Callable[[ModelRequest], Awaitable[ModelResponse]]
|
|
149
|
+
) -> ModelResponse | AIMessage:
|
|
150
|
+
"""Supply preferences without blocking the event loop on config reads."""
|
|
151
|
+
return await handler(await asyncio.to_thread(self.prepare, request))
|
|
152
|
+
|
|
153
|
+
def wrap_tool_call(
|
|
154
|
+
self, request: ToolCallRequest, handler: Callable[[ToolCallRequest], ToolMessage | Command]
|
|
155
|
+
) -> ToolMessage | Command:
|
|
156
|
+
"""Normalize supported synchronous tool calls before execution."""
|
|
157
|
+
return handler(self.prepare_tool(request))
|
|
158
|
+
|
|
159
|
+
async def awrap_tool_call(
|
|
160
|
+
self,
|
|
161
|
+
request: ToolCallRequest,
|
|
162
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
|
163
|
+
) -> ToolMessage | Command:
|
|
164
|
+
"""Normalize supported async tool calls without blocking config reads."""
|
|
165
|
+
return await handler(await asyncio.to_thread(self.prepare_tool, request))
|