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,806 @@
|
|
|
1
|
+
"""Load rarely used tools and subagents on demand, not on every model call.
|
|
2
|
+
|
|
3
|
+
Tool schemas and specialist rosters load on demand, independently of provider:
|
|
4
|
+
|
|
5
|
+
* Only **core** tools (file, shell, todos, plan mode, search), the user's
|
|
6
|
+
**most-used** tools (Hermes ``tool_stats``, read once per session) and tools
|
|
7
|
+
already **loaded** in this thread are bound.
|
|
8
|
+
* Every subagent except the always-listed general-purpose one drops out of the
|
|
9
|
+
``task`` description the same way.
|
|
10
|
+
* ``search_tools`` finds the rest (hybrid search, shared with skills) and loads
|
|
11
|
+
them: a tool is loaded once a ``search_tools`` result names it or the model
|
|
12
|
+
has called it. Successful structured results are matched to their calls and
|
|
13
|
+
retained in checkpoints. Compaction drops visibility when that evidence leaves
|
|
14
|
+
context; rediscovery restores it. Search uses the owning execution's inventory.
|
|
15
|
+
|
|
16
|
+
Every tool stays registered with the graph, so a call to one that is not bound
|
|
17
|
+
still executes; only the schemas sent to the model shrink.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import re
|
|
24
|
+
import time
|
|
25
|
+
from difflib import get_close_matches
|
|
26
|
+
from fnmatch import fnmatchcase
|
|
27
|
+
from functools import partial, update_wrapper
|
|
28
|
+
from typing import TYPE_CHECKING, Any
|
|
29
|
+
|
|
30
|
+
from langchain.agents.middleware import AgentMiddleware
|
|
31
|
+
from langchain.tools import ToolRuntime # noqa: TC002 - Pydantic resolves runtime annotations.
|
|
32
|
+
from langchain_core.messages import AIMessage, SystemMessage, ToolMessage
|
|
33
|
+
from langchain_core.tools import StructuredTool
|
|
34
|
+
from pydantic import ConfigDict
|
|
35
|
+
|
|
36
|
+
from novacode_cli.agents.provider_caching import (
|
|
37
|
+
breakpoint_count,
|
|
38
|
+
native_tool_search,
|
|
39
|
+
public_provider,
|
|
40
|
+
unsupported_native_feature,
|
|
41
|
+
)
|
|
42
|
+
from novacode_cli.agents.tool_search_variants import (
|
|
43
|
+
BM25_TOOL,
|
|
44
|
+
REGEX_TOOL,
|
|
45
|
+
SEARCH_VARIANTS,
|
|
46
|
+
BM25Search,
|
|
47
|
+
SearchInput,
|
|
48
|
+
parameter_text,
|
|
49
|
+
regex_matches,
|
|
50
|
+
)
|
|
51
|
+
from novacode_cli.config.browser_mode import (
|
|
52
|
+
browser_mode_enabled,
|
|
53
|
+
browser_tool_allowed,
|
|
54
|
+
trusted_browser_servers,
|
|
55
|
+
)
|
|
56
|
+
from novacode_cli.skills.retrieval import MIN_COSINE, first_sentence, get_index
|
|
57
|
+
|
|
58
|
+
if TYPE_CHECKING:
|
|
59
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
60
|
+
|
|
61
|
+
from langchain.agents.middleware.types import ModelRequest, ModelResponse
|
|
62
|
+
from langchain_core.tools import BaseTool
|
|
63
|
+
|
|
64
|
+
from novacode_cli.skills.refreshing_middleware import RefreshingSkillsMiddleware
|
|
65
|
+
|
|
66
|
+
logger = logging.getLogger("nova.tool_search")
|
|
67
|
+
|
|
68
|
+
#: Always bound: the tools a coding turn cannot do without. Chosen from real
|
|
69
|
+
#: use (2026-10 audit: 5,089 calls over 40 threads, 93% of them shell,
|
|
70
|
+
#: read_file, edit_file and grep); a tool called a few times a month is cheaper
|
|
71
|
+
#: as one search_tools round-trip than as a schema resent on every call.
|
|
72
|
+
#: `think`, `code_search`, `web_search`, `eval`, `execute` (locally the same
|
|
73
|
+
#: shell as `shell`), `skills_read_resource`, the rarer async and artifact
|
|
74
|
+
#: tools and `list_waiters` are found by search.
|
|
75
|
+
CORE_TOOLS = frozenset(
|
|
76
|
+
{
|
|
77
|
+
"ls",
|
|
78
|
+
"read_file",
|
|
79
|
+
"write_file",
|
|
80
|
+
"edit_file",
|
|
81
|
+
"glob",
|
|
82
|
+
"grep",
|
|
83
|
+
"shell",
|
|
84
|
+
"bash",
|
|
85
|
+
"write_todos",
|
|
86
|
+
"task",
|
|
87
|
+
"ask_user_question",
|
|
88
|
+
"enter_plan_mode",
|
|
89
|
+
"exit_plan_mode",
|
|
90
|
+
"SendMessage",
|
|
91
|
+
"list_sessions",
|
|
92
|
+
"EnterWorktreeTool",
|
|
93
|
+
"ExitWorktreeTool",
|
|
94
|
+
"fetch_url",
|
|
95
|
+
"skills_search",
|
|
96
|
+
"skills_load",
|
|
97
|
+
"search_tools",
|
|
98
|
+
REGEX_TOOL,
|
|
99
|
+
BM25_TOOL,
|
|
100
|
+
"memory_search",
|
|
101
|
+
# The prompt delegates background work proactively and collects it with
|
|
102
|
+
# one short wait, so starting, checking and waiting stay bound.
|
|
103
|
+
"start_async_task",
|
|
104
|
+
"check_async_task",
|
|
105
|
+
"wait_for_task",
|
|
106
|
+
# The prompt asks for artifacts proactively.
|
|
107
|
+
"create_artifact",
|
|
108
|
+
# Surface the opt-in guest and its readiness without loading input schemas.
|
|
109
|
+
"computer_status",
|
|
110
|
+
"knowledge_lookup",
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
#: Never bound by use alone: `execute` duplicates `shell` locally (both run
|
|
114
|
+
#: through Nova's shell), so a model that used it is better steered to `shell`.
|
|
115
|
+
NEVER_FREQUENT = frozenset({"execute"})
|
|
116
|
+
#: Also bound: up to this many of the user's most-used other tools...
|
|
117
|
+
FREQUENT_TOOLS = 10
|
|
118
|
+
#: ...called at least this often in the last FREQUENT_DAYS days. All-time counts
|
|
119
|
+
#: kept tools from a month-old workflow (Serena, Playwright) on every call.
|
|
120
|
+
FREQUENT_MIN_USES = 20
|
|
121
|
+
FREQUENT_DAYS = 30
|
|
122
|
+
|
|
123
|
+
#: Shorter descriptions for third-party tools whose defaults are long (the
|
|
124
|
+
#: rules live in the system prompt). ``write_todos``: deepagents' 3.9k-char
|
|
125
|
+
#: text, which also contradicts Nova's one-item-in-progress rule.
|
|
126
|
+
DESCRIPTIONS = {
|
|
127
|
+
"write_todos": (
|
|
128
|
+
"Create or replace the task list for multi-step work: a list of todos, each "
|
|
129
|
+
"with `content` and a `status` of pending, in_progress or completed. Follow "
|
|
130
|
+
"the rules in <todo_management>. Never call it twice in parallel."
|
|
131
|
+
),
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
LOADED_MARKER = "Loaded tools:"
|
|
135
|
+
#: Tools whose result can load others with a LOADED_MARKER line: the search,
|
|
136
|
+
#: and create_artifact, whose natural next step is update_artifact.
|
|
137
|
+
LOADING_TOOLS = frozenset({"search_tools", "tool_search", "create_artifact"}) | SEARCH_VARIANTS
|
|
138
|
+
MAX_SEARCH_LIMIT = 50
|
|
139
|
+
MAX_ANTHROPIC_BREAKPOINTS = 4
|
|
140
|
+
MAX_INVENTORY_CHARS = 2_000
|
|
141
|
+
MAX_INVENTORY_GROUPS = 10
|
|
142
|
+
MAX_GROUP_CHARS = 1_000
|
|
143
|
+
LOAD_RECORD = "nova_tool_search"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class RuntimeSearchInput(SearchInput):
|
|
147
|
+
"""Runtime inventory is injected by the graph, never supplied by the model."""
|
|
148
|
+
|
|
149
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
150
|
+
runtime: ToolRuntime
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _name(tool: Any) -> str:
|
|
154
|
+
if isinstance(tool, dict):
|
|
155
|
+
return tool.get("name") or tool.get("function", {}).get("name", "")
|
|
156
|
+
return getattr(tool, "name", "")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _description(tool: Any) -> str:
|
|
160
|
+
if isinstance(tool, dict):
|
|
161
|
+
return tool.get("description") or tool.get("function", {}).get("description", "")
|
|
162
|
+
return getattr(tool, "description", "") or ""
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _with_description(tool: BaseTool | dict, description: str) -> BaseTool | dict:
|
|
166
|
+
"""Copy descriptions without mutating registered objects or function schemas."""
|
|
167
|
+
if isinstance(tool, dict):
|
|
168
|
+
if isinstance(tool.get("function"), dict):
|
|
169
|
+
return {**tool, "function": {**tool["function"], "description": description}}
|
|
170
|
+
return {**tool, "description": description}
|
|
171
|
+
return tool.model_copy(update={"description": description})
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def loaded_capabilities(messages: list[Any]) -> set[tuple[str, str]]:
|
|
175
|
+
"""Typed schema visibility from calls and their successful structured results.
|
|
176
|
+
|
|
177
|
+
Old text-only discovery reports remain readable, but require rediscovery.
|
|
178
|
+
A marker embedded in tool output is never a loading instruction.
|
|
179
|
+
"""
|
|
180
|
+
loaded: set[tuple[str, str]] = set()
|
|
181
|
+
calls: dict[str, str] = {}
|
|
182
|
+
for msg in messages:
|
|
183
|
+
if isinstance(msg, AIMessage):
|
|
184
|
+
for call in msg.tool_calls or []:
|
|
185
|
+
call_id = call.get("id")
|
|
186
|
+
if isinstance(call_id, str):
|
|
187
|
+
calls[call_id] = call["name"]
|
|
188
|
+
loaded.add(("tool", call["name"]))
|
|
189
|
+
if call["name"] in {"task", "start_async_task"}:
|
|
190
|
+
agent = call.get("args", {}).get("subagent_type")
|
|
191
|
+
if isinstance(agent, str):
|
|
192
|
+
loaded.add(("subagent", agent))
|
|
193
|
+
elif isinstance(msg, ToolMessage):
|
|
194
|
+
expected = calls.pop(msg.tool_call_id, None)
|
|
195
|
+
if (
|
|
196
|
+
msg.status == "error"
|
|
197
|
+
or msg.name not in LOADING_TOOLS
|
|
198
|
+
or expected != msg.name
|
|
199
|
+
or not isinstance(msg.artifact, dict)
|
|
200
|
+
):
|
|
201
|
+
continue
|
|
202
|
+
record = msg.artifact.get(LOAD_RECORD)
|
|
203
|
+
if (
|
|
204
|
+
not isinstance(record, dict)
|
|
205
|
+
or type(record.get("version")) is not int
|
|
206
|
+
or record["version"] != 1
|
|
207
|
+
):
|
|
208
|
+
continue
|
|
209
|
+
capabilities = record.get("capabilities")
|
|
210
|
+
if not isinstance(capabilities, list):
|
|
211
|
+
continue
|
|
212
|
+
for capability in capabilities:
|
|
213
|
+
if (
|
|
214
|
+
isinstance(capability, dict)
|
|
215
|
+
and isinstance(capability.get("kind"), str)
|
|
216
|
+
and capability.get("kind") in {"tool", "subagent"}
|
|
217
|
+
and isinstance(capability.get("name"), str)
|
|
218
|
+
and capability["name"]
|
|
219
|
+
):
|
|
220
|
+
loaded.add((capability["kind"], capability["name"]))
|
|
221
|
+
return loaded
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def loaded_names(messages: list[Any]) -> set[str]:
|
|
225
|
+
"""Compatibility projection; binding uses typed capabilities."""
|
|
226
|
+
return {name for _, name in loaded_capabilities(messages)}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _group(names: list[str]) -> str:
|
|
230
|
+
"""Bounded capability groups; full member names stay behind search."""
|
|
231
|
+
groups: dict[str, list[str]] = {}
|
|
232
|
+
for name in sorted(names):
|
|
233
|
+
prefix = name.split("_", 1)[0] if "_" in name else ""
|
|
234
|
+
groups.setdefault(prefix, []).append(name)
|
|
235
|
+
lines: list[str] = []
|
|
236
|
+
shown = 0
|
|
237
|
+
for prefix, members in groups.items():
|
|
238
|
+
label = f"{prefix}_*" if prefix else "other names"
|
|
239
|
+
line = f"- `{label[:80]}` ({len(members)} tools)"
|
|
240
|
+
if len(lines) >= MAX_INVENTORY_GROUPS or sum(map(len, lines)) + len(line) > MAX_GROUP_CHARS:
|
|
241
|
+
break
|
|
242
|
+
lines.append(line)
|
|
243
|
+
shown += len(members)
|
|
244
|
+
if shown < len(names):
|
|
245
|
+
lines.append(f"- {len(names) - shown} further tools; search to inspect.")
|
|
246
|
+
return "\n".join(lines)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
class _AccountingRequest:
|
|
250
|
+
"""Just enough of a ModelRequest for :func:`sent_tools`."""
|
|
251
|
+
|
|
252
|
+
def __init__(self, **fields: Any) -> None:
|
|
253
|
+
self.__dict__.update(fields)
|
|
254
|
+
|
|
255
|
+
def override(self, **changes: Any) -> _AccountingRequest:
|
|
256
|
+
return _AccountingRequest(**{**self.__dict__, **changes})
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def sent_tools(tools: Sequence[Any], messages: Sequence[Any]) -> list[Any]:
|
|
260
|
+
"""The schemas a request binds for ``messages``: selected and trimmed as sent.
|
|
261
|
+
|
|
262
|
+
For accounting before any request exists (``/context`` at startup), where the
|
|
263
|
+
registered schemas overstate the request several times: the ``task`` tool
|
|
264
|
+
alone lists every subagent (~39k chars) until this middleware trims it. The
|
|
265
|
+
deferred roster is read back from the ``task`` and ``start_async_task``
|
|
266
|
+
descriptions; the user's frequently used tools are left out.
|
|
267
|
+
"""
|
|
268
|
+
from novacode_cli.agents.default_subagents.subagents import LISTED_SUBAGENTS
|
|
269
|
+
|
|
270
|
+
def roster(name: str) -> list[str]:
|
|
271
|
+
tool = next((t for t in tools if _name(t) == name), None)
|
|
272
|
+
if tool is None:
|
|
273
|
+
return []
|
|
274
|
+
lines = _description(tool).splitlines()
|
|
275
|
+
return [m.group(1) for line in lines if (m := re.match(r"^[ \t]*- ([^:\s]+):", line))]
|
|
276
|
+
|
|
277
|
+
middleware = ToolSearchMiddleware(
|
|
278
|
+
deferred_subagents={n: "" for n in roster("task") if n not in LISTED_SUBAGENTS},
|
|
279
|
+
async_subagents=set(roster("start_async_task")),
|
|
280
|
+
)
|
|
281
|
+
middleware._frequent = set()
|
|
282
|
+
request = _AccountingRequest(
|
|
283
|
+
tools=list(tools), messages=list(messages), system_message=SystemMessage(""), state={},
|
|
284
|
+
model=None, model_settings={},
|
|
285
|
+
)
|
|
286
|
+
return list(middleware._apply(request).tools) # type: ignore[arg-type]
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
class ToolSearchMiddleware(AgentMiddleware):
|
|
290
|
+
"""Bind core + frequent + loaded tools; ``search_tools`` loads the rest."""
|
|
291
|
+
|
|
292
|
+
def __init__(
|
|
293
|
+
self,
|
|
294
|
+
*,
|
|
295
|
+
deferred_subagents: dict[str, str] | None = None,
|
|
296
|
+
async_subagents: set[str] | None = None,
|
|
297
|
+
skills: RefreshingSkillsMiddleware | None = None,
|
|
298
|
+
) -> None:
|
|
299
|
+
"""``deferred_subagents``: name -> description, hidden from ``task`` until found."""
|
|
300
|
+
super().__init__()
|
|
301
|
+
self._deferred_subagents = deferred_subagents or {}
|
|
302
|
+
self._async_subagents = async_subagents or set()
|
|
303
|
+
self._skills = skills
|
|
304
|
+
self._frequent: set[str] | None = None # read once, then frozen
|
|
305
|
+
self._bm25 = BM25Search()
|
|
306
|
+
self._portable_routes: set[tuple[str, str]] = set()
|
|
307
|
+
self.tools = [
|
|
308
|
+
StructuredTool.from_function(
|
|
309
|
+
func=update_wrapper(partial(self._search, mode=mode), self._search),
|
|
310
|
+
coroutine=update_wrapper(partial(self._asearch, mode=mode), self._asearch),
|
|
311
|
+
name=name,
|
|
312
|
+
args_schema=RuntimeSearchInput,
|
|
313
|
+
response_format="content_and_artifact",
|
|
314
|
+
description=description,
|
|
315
|
+
)
|
|
316
|
+
for name, mode, description in (
|
|
317
|
+
(
|
|
318
|
+
"search_tools",
|
|
319
|
+
"hybrid",
|
|
320
|
+
"Find tools/subagents by capability, exact/partial names or wildcards. "
|
|
321
|
+
"Searches names, descriptions and parameters. query='*' browses; limit and "
|
|
322
|
+
"offset page results. Found schemas load next step; approvals still apply.",
|
|
323
|
+
),
|
|
324
|
+
(
|
|
325
|
+
REGEX_TOOL,
|
|
326
|
+
"regex",
|
|
327
|
+
"Find tools/subagents using case-insensitive Python regex over names, "
|
|
328
|
+
"descriptions and parameters. Maximum pattern length: 200 characters. "
|
|
329
|
+
"Use query, limit and offset to page results.",
|
|
330
|
+
),
|
|
331
|
+
(
|
|
332
|
+
BM25_TOOL,
|
|
333
|
+
"bm25",
|
|
334
|
+
"Find tools/subagents using BM25 keywords over names, descriptions and "
|
|
335
|
+
"parameters without embeddings. Maximum query length: 500 characters. "
|
|
336
|
+
"Use query, limit and offset to page results.",
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
]
|
|
340
|
+
|
|
341
|
+
def _catalog_for(self, tools: Sequence[Any]) -> list[dict]:
|
|
342
|
+
"""Discovery metadata belongs to this invocation's current inventory."""
|
|
343
|
+
browsing = browser_mode_enabled()
|
|
344
|
+
trusted = trusted_browser_servers() if browsing else frozenset()
|
|
345
|
+
return [
|
|
346
|
+
{
|
|
347
|
+
"name": _name(tool),
|
|
348
|
+
"description": _description(tool),
|
|
349
|
+
"kind": "tool",
|
|
350
|
+
"parameters": parameter_text(tool),
|
|
351
|
+
}
|
|
352
|
+
for tool in sorted(tools, key=_name)
|
|
353
|
+
if _name(tool)
|
|
354
|
+
and (
|
|
355
|
+
not browsing
|
|
356
|
+
or browser_tool_allowed(
|
|
357
|
+
_name(tool),
|
|
358
|
+
metadata=tool.get("metadata")
|
|
359
|
+
if isinstance(tool, dict)
|
|
360
|
+
else getattr(tool, "metadata", None),
|
|
361
|
+
trusted_servers=trusted,
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
] + [
|
|
365
|
+
{"name": name, "description": description, "kind": "subagent"}
|
|
366
|
+
for name, description in sorted(self._deferred_subagents.items())
|
|
367
|
+
if not browsing
|
|
368
|
+
]
|
|
369
|
+
|
|
370
|
+
def _search(
|
|
371
|
+
self,
|
|
372
|
+
query: str,
|
|
373
|
+
runtime: ToolRuntime,
|
|
374
|
+
limit: int = 5,
|
|
375
|
+
offset: int = 0,
|
|
376
|
+
*,
|
|
377
|
+
mode: str = "hybrid",
|
|
378
|
+
) -> tuple[str, dict]:
|
|
379
|
+
"""Use the owning run's inventory, including its activated skill aliases."""
|
|
380
|
+
tools = {_name(tool): tool for tool in runtime.tools}
|
|
381
|
+
if self._skills is not None:
|
|
382
|
+
resolved = self._skills._resolve_tools(runtime.state.get("messages", []), runtime)
|
|
383
|
+
for name, tool in resolved.items():
|
|
384
|
+
tools.setdefault(name, tool)
|
|
385
|
+
return self._tool_search(
|
|
386
|
+
query,
|
|
387
|
+
limit,
|
|
388
|
+
offset,
|
|
389
|
+
catalog=self._catalog_for(list(tools.values())),
|
|
390
|
+
_mode=mode,
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
async def _asearch(
|
|
394
|
+
self,
|
|
395
|
+
query: str,
|
|
396
|
+
runtime: ToolRuntime,
|
|
397
|
+
limit: int = 5,
|
|
398
|
+
offset: int = 0,
|
|
399
|
+
*,
|
|
400
|
+
mode: str = "hybrid",
|
|
401
|
+
) -> tuple[str, dict]:
|
|
402
|
+
"""Resolve async skill aliases in the same invocation context."""
|
|
403
|
+
tools = {_name(tool): tool for tool in runtime.tools}
|
|
404
|
+
if self._skills is not None:
|
|
405
|
+
resolved = await self._skills._aresolve_tools(
|
|
406
|
+
runtime.state.get("messages", []), runtime
|
|
407
|
+
)
|
|
408
|
+
for name, tool in resolved.items():
|
|
409
|
+
tools.setdefault(name, tool)
|
|
410
|
+
return self._tool_search(
|
|
411
|
+
query,
|
|
412
|
+
limit,
|
|
413
|
+
offset,
|
|
414
|
+
catalog=self._catalog_for(list(tools.values())),
|
|
415
|
+
_mode=mode,
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
def _search_matches(self, query: str, catalog: list[dict]) -> list[dict]:
|
|
419
|
+
"""Rank the full catalog, retaining exact names ahead of semantic hits."""
|
|
420
|
+
tokens = re.findall(r"[\w.:-]+", query.casefold())
|
|
421
|
+
names = {item["name"].casefold() for item in catalog}
|
|
422
|
+
exact = [item for item in catalog if item["name"].casefold() in tokens]
|
|
423
|
+
patterns = [
|
|
424
|
+
word.strip("`\"'") for word in re.split(r"[,\s]+", query.casefold()) if "*" in word
|
|
425
|
+
]
|
|
426
|
+
if patterns:
|
|
427
|
+
return [
|
|
428
|
+
item
|
|
429
|
+
for item in catalog
|
|
430
|
+
if item in exact
|
|
431
|
+
or any(fnmatchcase(item["name"].casefold(), pattern) for pattern in patterns)
|
|
432
|
+
]
|
|
433
|
+
if tokens and set(tokens) <= names:
|
|
434
|
+
return exact
|
|
435
|
+
normalized = re.sub(r"[^\w]+|_", " ", query.casefold()).strip()
|
|
436
|
+
partial = [
|
|
437
|
+
item
|
|
438
|
+
for item in catalog
|
|
439
|
+
if normalized
|
|
440
|
+
and normalized in re.sub(r"[^\w]+|_", " ", item["name"].casefold()).strip()
|
|
441
|
+
]
|
|
442
|
+
fuzzy_names = get_close_matches(query.casefold(), sorted(names), n=3, cutoff=0.8)
|
|
443
|
+
fuzzy = [item for item in catalog if item["name"].casefold() in fuzzy_names]
|
|
444
|
+
try:
|
|
445
|
+
ranked = [
|
|
446
|
+
item
|
|
447
|
+
for item, cosine, bm25 in get_index(catalog).search(query, k=len(catalog))
|
|
448
|
+
if bm25 > 0 or cosine >= MIN_COSINE
|
|
449
|
+
]
|
|
450
|
+
except Exception: # noqa: BLE001 - discovery still works without the optional index
|
|
451
|
+
logger.debug("Tool retrieval index unavailable; using lexical search", exc_info=True)
|
|
452
|
+
ranked = [
|
|
453
|
+
item
|
|
454
|
+
for item in catalog
|
|
455
|
+
if set(tokens)
|
|
456
|
+
& set(
|
|
457
|
+
re.findall(
|
|
458
|
+
r"[\w]+",
|
|
459
|
+
(
|
|
460
|
+
item["name"]
|
|
461
|
+
+ " "
|
|
462
|
+
+ item["description"]
|
|
463
|
+
+ " "
|
|
464
|
+
+ item.get("parameters", "")
|
|
465
|
+
).casefold(),
|
|
466
|
+
)
|
|
467
|
+
)
|
|
468
|
+
]
|
|
469
|
+
hits: dict[tuple[str, str], dict] = {}
|
|
470
|
+
for item in [*exact, *partial, *fuzzy, *ranked]:
|
|
471
|
+
hits.setdefault((item["kind"], item["name"]), item)
|
|
472
|
+
return list(hits.values())
|
|
473
|
+
|
|
474
|
+
def _tool_search(
|
|
475
|
+
self,
|
|
476
|
+
query: str,
|
|
477
|
+
limit: int = 5,
|
|
478
|
+
offset: int = 0,
|
|
479
|
+
*,
|
|
480
|
+
catalog: list[dict],
|
|
481
|
+
_mode: str = "hybrid",
|
|
482
|
+
) -> tuple[str, dict]:
|
|
483
|
+
"""Find and load available tools and subagents.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
query: A capability, exact or partial names, or a wildcard; '*' lists everything.
|
|
487
|
+
limit: Results per page (1-50).
|
|
488
|
+
offset: Number of matches to skip to reach another page.
|
|
489
|
+
catalog: Metadata from the owning invocation's current tool inventory.
|
|
490
|
+
"""
|
|
491
|
+
if not 1 <= limit <= MAX_SEARCH_LIMIT or offset < 0:
|
|
492
|
+
message = "limit must be between 1 and 50, and offset must be nonnegative."
|
|
493
|
+
raise ValueError(message)
|
|
494
|
+
query = query if _mode == "regex" else query.strip()
|
|
495
|
+
if not query:
|
|
496
|
+
return (
|
|
497
|
+
"Enter a capability or tool name, or use query='*' to browse all available tools."
|
|
498
|
+
), {}
|
|
499
|
+
if not catalog:
|
|
500
|
+
return "No tools are currently registered for discovery.", {}
|
|
501
|
+
try:
|
|
502
|
+
if _mode == "regex":
|
|
503
|
+
matches = regex_matches(catalog, query)
|
|
504
|
+
elif _mode == "bm25":
|
|
505
|
+
matches = self._bm25.search(catalog, query)
|
|
506
|
+
else:
|
|
507
|
+
matches = self._search_matches(query, catalog)
|
|
508
|
+
except (ValueError, re.error, TimeoutError) as exc:
|
|
509
|
+
return f"Tool search failed: {exc}", {}
|
|
510
|
+
if not matches:
|
|
511
|
+
return (
|
|
512
|
+
f"No registered tools match {query!r}. "
|
|
513
|
+
"Try a shorter name, a server prefix, or query='*' to browse."
|
|
514
|
+
), {}
|
|
515
|
+
hits = matches[offset : offset + limit]
|
|
516
|
+
if not hits:
|
|
517
|
+
return (
|
|
518
|
+
f"There are {len(matches)} matches. Retry with offset smaller than {len(matches)}."
|
|
519
|
+
), {}
|
|
520
|
+
tools = [h for h in hits if h["kind"] == "tool"]
|
|
521
|
+
agents = [h for h in hits if h["kind"] == "subagent"]
|
|
522
|
+
lines = [f"- `{h['name']}`: {first_sentence(h['description'], 200)}" for h in tools]
|
|
523
|
+
for agent in agents:
|
|
524
|
+
name = agent["name"]
|
|
525
|
+
dispatch = "start_async_task" if name in self._async_subagents else "task"
|
|
526
|
+
lines.append(
|
|
527
|
+
f'- subagent `{name}` (use `{dispatch}(subagent_type="{name}")`): '
|
|
528
|
+
f"{first_sentence(agent['description'], 200)}"
|
|
529
|
+
)
|
|
530
|
+
loaded = ", ".join(h["name"] for h in hits)
|
|
531
|
+
result = f"{LOADED_MARKER} {loaded}\n" + "\n".join(lines)
|
|
532
|
+
if offset or len(matches) > len(hits):
|
|
533
|
+
result += f"\nShowing {offset + 1}-{offset + len(hits)} of {len(matches)} matches."
|
|
534
|
+
if offset + len(hits) < len(matches):
|
|
535
|
+
result += (
|
|
536
|
+
f" Continue with the same query, limit={limit}, offset={offset + len(hits)}."
|
|
537
|
+
)
|
|
538
|
+
return result, {
|
|
539
|
+
LOAD_RECORD: {
|
|
540
|
+
"version": 1,
|
|
541
|
+
"capabilities": [{"kind": hit["kind"], "name": hit["name"]} for hit in hits],
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
# ── binding ────────────────────────────────────────────────────────────
|
|
546
|
+
|
|
547
|
+
async def _read_frequent(self, request: ModelRequest) -> None:
|
|
548
|
+
"""Rank tools by recent use: Hermes's per-session ``tool_history``."""
|
|
549
|
+
if self._frequent is not None:
|
|
550
|
+
return
|
|
551
|
+
self._frequent = set()
|
|
552
|
+
store = getattr(request.runtime, "store", None)
|
|
553
|
+
if store is None:
|
|
554
|
+
return
|
|
555
|
+
since = time.time() - FREQUENT_DAYS * 86400
|
|
556
|
+
uses: dict[str, int] = {}
|
|
557
|
+
try:
|
|
558
|
+
spaces = await store.alist_namespaces(
|
|
559
|
+
prefix=("nova",), suffix=("tool_history",), limit=1000
|
|
560
|
+
)
|
|
561
|
+
# Filtered here too: Nova's stdlib fallback store ignores prefix/suffix.
|
|
562
|
+
for space in (s for s in spaces if s[:1] == ("nova",) and s[-1:] == ("tool_history",)):
|
|
563
|
+
item = await store.aget(space, "history")
|
|
564
|
+
for entry in ((item.value or {}).get("entries") or []) if item else []:
|
|
565
|
+
name = entry.get("tool")
|
|
566
|
+
if name and float(entry.get("timestamp") or 0) >= since:
|
|
567
|
+
uses[name] = uses.get(name, 0) + 1
|
|
568
|
+
except Exception: # noqa: BLE001 — ranking is a nicety, never break a turn
|
|
569
|
+
logger.debug("tool_history unavailable", exc_info=True)
|
|
570
|
+
return
|
|
571
|
+
ranked = sorted(
|
|
572
|
+
(n for n in uses if n not in CORE_TOOLS and n not in NEVER_FREQUENT),
|
|
573
|
+
key=lambda n: (-uses[n], n),
|
|
574
|
+
)
|
|
575
|
+
self._frequent = {n for n in ranked[:FREQUENT_TOOLS] if uses[n] >= FREQUENT_MIN_USES}
|
|
576
|
+
|
|
577
|
+
def _trim_task(self, tool: Any, hidden: list[str]) -> Any:
|
|
578
|
+
"""Drop hidden subagents from the ``task`` description (a request-local copy).
|
|
579
|
+
|
|
580
|
+
One pass over the description's lines: with the whole roster deferred
|
|
581
|
+
(~90 names) a per-name regex would rescan the whole string 90 times on
|
|
582
|
+
every model call. Matching the ``- name:`` prefix keeps the agent name
|
|
583
|
+
authoritative even when a description contains regex metacharacters.
|
|
584
|
+
"""
|
|
585
|
+
hidden_set = set(hidden)
|
|
586
|
+
kept: list[str] = []
|
|
587
|
+
skip_lines = 0
|
|
588
|
+
for line in _description(tool).splitlines():
|
|
589
|
+
if skip_lines:
|
|
590
|
+
skip_lines -= 1
|
|
591
|
+
continue
|
|
592
|
+
match = re.match(r"^[ \t]*- ([^:]+):", line)
|
|
593
|
+
if match and match.group(1).strip() in hidden_set:
|
|
594
|
+
name = match.group(1).strip()
|
|
595
|
+
# deepagents inserts each source description verbatim after
|
|
596
|
+
# the roster heading. Remove its complete block, not just line 1.
|
|
597
|
+
skip_lines = max(0, len(self._deferred_subagents[name].splitlines()) - 1)
|
|
598
|
+
continue
|
|
599
|
+
kept.append(line)
|
|
600
|
+
text = "\n".join(kept)
|
|
601
|
+
text += (
|
|
602
|
+
f"\n\n{len(hidden)} more specialist subagents are not listed here: "
|
|
603
|
+
"`search_tools` finds them by what they do."
|
|
604
|
+
)
|
|
605
|
+
return _with_description(tool, text)
|
|
606
|
+
|
|
607
|
+
def _compact_async_roster(self, tool: Any) -> Any:
|
|
608
|
+
"""One line per agent in ``start_async_task``'s roster (a request-local copy).
|
|
609
|
+
|
|
610
|
+
deepagents lists each async agent with its whole description, "Use this
|
|
611
|
+
agent when:" bullets included (~6.5k chars for 13 agents). The names and
|
|
612
|
+
first sentences keep proactive delegation possible; ``search_tools``
|
|
613
|
+
returns any agent's full description.
|
|
614
|
+
"""
|
|
615
|
+
lines, in_roster = [], False
|
|
616
|
+
for line in _description(tool).splitlines():
|
|
617
|
+
if line.startswith("Available async agent types"):
|
|
618
|
+
in_roster = True
|
|
619
|
+
lines.append(line)
|
|
620
|
+
continue
|
|
621
|
+
if in_roster and line.startswith("#"):
|
|
622
|
+
in_roster = False
|
|
623
|
+
lines.append("")
|
|
624
|
+
if not in_roster:
|
|
625
|
+
lines.append(line)
|
|
626
|
+
continue
|
|
627
|
+
match = re.match(r"^- ([^:\s]+): (.*)", line)
|
|
628
|
+
if match and match.group(1) in self._async_subagents:
|
|
629
|
+
lines.append(f"- {match.group(1)}: {first_sentence(match.group(2), 160)}")
|
|
630
|
+
return _with_description(tool, "\n".join(lines))
|
|
631
|
+
|
|
632
|
+
def _shorten(self, tool: Any, hidden: list[str]) -> Any:
|
|
633
|
+
"""The request-local copy of a bound tool with its description trimmed."""
|
|
634
|
+
name = _name(tool)
|
|
635
|
+
if name == "task" and hidden:
|
|
636
|
+
return self._trim_task(tool, hidden)
|
|
637
|
+
if name == "start_async_task" and self._async_subagents:
|
|
638
|
+
return self._compact_async_roster(tool)
|
|
639
|
+
if name in DESCRIPTIONS:
|
|
640
|
+
return _with_description(tool, DESCRIPTIONS[name])
|
|
641
|
+
return tool
|
|
642
|
+
|
|
643
|
+
def _apply(self, request: ModelRequest) -> ModelRequest:
|
|
644
|
+
from novacode_cli.config.nova_config import NovaConfig
|
|
645
|
+
|
|
646
|
+
if NovaConfig().get_code_mode_enabled() and any(_name(t) == "eval" for t in request.tools):
|
|
647
|
+
from novacode_cli.agents.code_mode import code_mode_surface
|
|
648
|
+
|
|
649
|
+
# Provider search must never reintroduce the host-only inventory.
|
|
650
|
+
return request.override(tools=code_mode_surface(request.tools))
|
|
651
|
+
capabilities = loaded_capabilities(request.messages)
|
|
652
|
+
loaded = {name for kind, name in capabilities if kind == "tool"}
|
|
653
|
+
# Skill snapshots activate their optional schemas without a second search.
|
|
654
|
+
# Ordinary registered tools remain independently searchable.
|
|
655
|
+
for message in request.messages:
|
|
656
|
+
extra = getattr(message, "additional_kwargs", {})
|
|
657
|
+
snapshot = extra.get("skill", {}) if extra.get("lc_source") == "pinned_skill" else {}
|
|
658
|
+
if (
|
|
659
|
+
isinstance(message, ToolMessage)
|
|
660
|
+
and message.name == "skills_load"
|
|
661
|
+
and message.status != "error"
|
|
662
|
+
and isinstance(message.artifact, dict)
|
|
663
|
+
):
|
|
664
|
+
snapshot = message.artifact.get("skill", {})
|
|
665
|
+
if isinstance(snapshot, dict):
|
|
666
|
+
include = snapshot.get("include_tools", [])
|
|
667
|
+
if isinstance(include, list):
|
|
668
|
+
loaded.update(name for name in include if isinstance(name, str))
|
|
669
|
+
keep = set(CORE_TOOLS) | (self._frequent or set()) | loaded
|
|
670
|
+
keep.update(
|
|
671
|
+
_name(tool)
|
|
672
|
+
for tool in request.tools
|
|
673
|
+
if (getattr(tool, "metadata", None) or {}).get("_nova_skill_active")
|
|
674
|
+
)
|
|
675
|
+
bound: list[Any] = []
|
|
676
|
+
deferred: list[Any] = []
|
|
677
|
+
for tool in sorted(request.tools, key=_name):
|
|
678
|
+
(bound if _name(tool) in keep else deferred).append(tool)
|
|
679
|
+
browsing = browser_mode_enabled()
|
|
680
|
+
# Search results carry specialist details; discovery never rewrites task's schema.
|
|
681
|
+
hidden = [] if browsing else sorted(self._deferred_subagents)
|
|
682
|
+
bound = [self._shorten(t, hidden) for t in bound]
|
|
683
|
+
optional_names = [
|
|
684
|
+
_name(tool) for tool in request.tools if _name(tool) and _name(tool) not in CORE_TOOLS
|
|
685
|
+
]
|
|
686
|
+
if not optional_names and not hidden:
|
|
687
|
+
return self._native(request.override(tools=bound), request.tools)
|
|
688
|
+
note = (
|
|
689
|
+
"\n\n## More tools (load on demand)\n\n"
|
|
690
|
+
f"{len(optional_names)} optional tools and "
|
|
691
|
+
f"{0 if browsing else len(self._deferred_subagents)} "
|
|
692
|
+
"specialist subagents are searchable. Capability groups:\n"
|
|
693
|
+
f"{_group(optional_names)}\n\n"
|
|
694
|
+
'Call search_tools("<capability>") or use exact names. query="*" browses; '
|
|
695
|
+
f'wildcards such as "{"*snapshot*" if browsing else "playwright_*"}", '
|
|
696
|
+
"limit and offset inspect further results. "
|
|
697
|
+
f"{REGEX_TOOL} supports regex; {BM25_TOOL} supports keyword search. "
|
|
698
|
+
"Search loads schemas, not permission. Normal approvals and execution policy "
|
|
699
|
+
"still apply. Never guess arguments: load the schema first. "
|
|
700
|
+
"After compaction, search again for schemas no longer available."
|
|
701
|
+
)[:MAX_INVENTORY_CHARS]
|
|
702
|
+
blocks = (
|
|
703
|
+
[dict(block) for block in request.system_message.content_blocks]
|
|
704
|
+
if request.system_message
|
|
705
|
+
else []
|
|
706
|
+
)
|
|
707
|
+
return self._native(
|
|
708
|
+
request.override(
|
|
709
|
+
tools=bound,
|
|
710
|
+
system_message=SystemMessage(content=[*blocks, {"type": "text", "text": note}]),
|
|
711
|
+
),
|
|
712
|
+
request.tools,
|
|
713
|
+
)
|
|
714
|
+
|
|
715
|
+
@staticmethod
|
|
716
|
+
def _route_key(request: ModelRequest) -> tuple[str, str]:
|
|
717
|
+
metadata = getattr(request.model, "metadata", None) or {}
|
|
718
|
+
return str(metadata.get("nova_provider", "")), str(metadata.get("nova_model", ""))
|
|
719
|
+
|
|
720
|
+
def _native(self, request: ModelRequest, inventory: Sequence[Any]) -> ModelRequest:
|
|
721
|
+
"""Keep native authorized inventories stable; portable routes bind discovered tools."""
|
|
722
|
+
if self._route_key(request) in self._portable_routes:
|
|
723
|
+
return request
|
|
724
|
+
route = native_tool_search(request.model)
|
|
725
|
+
settings = dict(request.model_settings or {})
|
|
726
|
+
if route == "anthropic":
|
|
727
|
+
blocks = [m.content for m in request.messages]
|
|
728
|
+
if request.system_message:
|
|
729
|
+
blocks.append(request.system_message.content)
|
|
730
|
+
explicit = breakpoint_count(blocks)
|
|
731
|
+
explicit += sum(
|
|
732
|
+
breakpoint_count(t if isinstance(t, dict) else getattr(t, "extras", None))
|
|
733
|
+
for t in inventory
|
|
734
|
+
)
|
|
735
|
+
if explicit < MAX_ANTHROPIC_BREAKPOINTS:
|
|
736
|
+
settings.setdefault("cache_control", {"type": "ephemeral"})
|
|
737
|
+
if not route:
|
|
738
|
+
return request.override(model_settings=settings)
|
|
739
|
+
from langchain_core.utils.function_calling import convert_to_openai_tool
|
|
740
|
+
|
|
741
|
+
always = CORE_TOOLS | (self._frequent or set())
|
|
742
|
+
tools = []
|
|
743
|
+
hidden = [] if browser_mode_enabled() else sorted(self._deferred_subagents)
|
|
744
|
+
for tool in sorted(inventory, key=_name):
|
|
745
|
+
if route == "anthropic" and _name(tool) == REGEX_TOOL:
|
|
746
|
+
continue
|
|
747
|
+
shortened = self._shorten(tool, hidden)
|
|
748
|
+
if not isinstance(shortened, dict):
|
|
749
|
+
extras = {**(getattr(shortened, "extras", None) or {})}
|
|
750
|
+
extras["defer_loading"] = _name(tool) not in always
|
|
751
|
+
tools.append(shortened.model_copy(update={"extras": extras}))
|
|
752
|
+
continue
|
|
753
|
+
definition = convert_to_openai_tool(shortened)
|
|
754
|
+
if "function" in definition:
|
|
755
|
+
definition = {**definition, "defer_loading": _name(tool) not in always}
|
|
756
|
+
tools.append(definition)
|
|
757
|
+
if route == "anthropic":
|
|
758
|
+
tools.append({"type": REGEX_TOOL, "name": "tool_search_tool_regex"})
|
|
759
|
+
else:
|
|
760
|
+
tools.append({"type": "tool_search"})
|
|
761
|
+
return request.override(tools=tools, model_settings=settings)
|
|
762
|
+
|
|
763
|
+
def wrap_model_call(
|
|
764
|
+
self,
|
|
765
|
+
request: ModelRequest,
|
|
766
|
+
handler: Callable[[ModelRequest], ModelResponse],
|
|
767
|
+
) -> ModelResponse:
|
|
768
|
+
"""Bind only core, frequent and loaded tools (sync: no usage ranking)."""
|
|
769
|
+
if self._frequent is None:
|
|
770
|
+
self._frequent = set()
|
|
771
|
+
try:
|
|
772
|
+
return handler(self._apply(request))
|
|
773
|
+
except Exception as exc:
|
|
774
|
+
if (
|
|
775
|
+
self._route_key(request) in self._portable_routes
|
|
776
|
+
or not (
|
|
777
|
+
native_tool_search(request.model)
|
|
778
|
+
or public_provider(request.model) == "anthropic"
|
|
779
|
+
)
|
|
780
|
+
or not unsupported_native_feature(exc)
|
|
781
|
+
):
|
|
782
|
+
raise
|
|
783
|
+
self._portable_routes.add(self._route_key(request))
|
|
784
|
+
return handler(self._apply(request))
|
|
785
|
+
|
|
786
|
+
async def awrap_model_call(
|
|
787
|
+
self,
|
|
788
|
+
request: ModelRequest,
|
|
789
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
790
|
+
) -> ModelResponse:
|
|
791
|
+
"""Bind only core, frequent and loaded tools."""
|
|
792
|
+
await self._read_frequent(request)
|
|
793
|
+
try:
|
|
794
|
+
return await handler(self._apply(request))
|
|
795
|
+
except Exception as exc:
|
|
796
|
+
if (
|
|
797
|
+
self._route_key(request) in self._portable_routes
|
|
798
|
+
or not (
|
|
799
|
+
native_tool_search(request.model)
|
|
800
|
+
or public_provider(request.model) == "anthropic"
|
|
801
|
+
)
|
|
802
|
+
or not unsupported_native_feature(exc)
|
|
803
|
+
):
|
|
804
|
+
raise
|
|
805
|
+
self._portable_routes.add(self._route_key(request))
|
|
806
|
+
return await handler(self._apply(request))
|