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
novacode_cli/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""NOVA CLI - Interactive AI coding assistant."""
|
|
2
|
+
|
|
3
|
+
import os as _os
|
|
4
|
+
|
|
5
|
+
# Must run before numpy is imported anywhere in the process. numpy's OpenBLAS
|
|
6
|
+
# pre-allocates a buffer per CPU thread at import: on a 12-thread machine that
|
|
7
|
+
# is ~380 MB of private memory for nothing — Nova's numeric work is small
|
|
8
|
+
# vector searches (skill and code retrieval over a few thousand 256-d rows),
|
|
9
|
+
# where one thread is measurably FASTER (91 us vs 180 us for a 1000-row
|
|
10
|
+
# search) because thread hand-off costs more than the arithmetic. One thread:
|
|
11
|
+
# ~9 MB. It applies to every Nova process — the TUI and each spawned session.
|
|
12
|
+
# setdefault, so an explicit setting in the environment still wins.
|
|
13
|
+
_os.environ.setdefault("OPENBLAS_NUM_THREADS", "1")
|
|
14
|
+
# Other numerical backends also default to machine-wide thread pools. Multiple
|
|
15
|
+
# Nova processes should not each occupy every core; explicit user values win.
|
|
16
|
+
_os.environ.setdefault("OMP_NUM_THREADS", "1")
|
|
17
|
+
_os.environ.setdefault("MKL_NUM_THREADS", "1")
|
|
18
|
+
_os.environ.setdefault("NUMEXPR_NUM_THREADS", "1")
|
|
19
|
+
|
|
20
|
+
# Must run before langchain_core is imported: it pulls in `transformers` (and
|
|
21
|
+
# therefore torch/numpy/PIL) at module scope for a fallback tokenizer Nova never
|
|
22
|
+
# uses, costing ~160 MB RSS and ~10 s of startup. See novacode_cli._lazy_heavy.
|
|
23
|
+
from novacode_cli import _lazy_heavy as _lazy_heavy
|
|
24
|
+
|
|
25
|
+
_lazy_heavy.install()
|
|
26
|
+
|
|
27
|
+
# from novacode_cli.main import cli_main
|
|
28
|
+
|
|
29
|
+
__all__ = []
|
novacode_cli/__main__.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Keep `transformers`/`torch` out of Nova's startup import graph.
|
|
2
|
+
|
|
3
|
+
``langchain_core.language_models.base`` does, at module scope::
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from transformers import GPT2TokenizerFast
|
|
7
|
+
_HAS_TRANSFORMERS = True
|
|
8
|
+
except ImportError:
|
|
9
|
+
_HAS_TRANSFORMERS = False
|
|
10
|
+
|
|
11
|
+
That import is only ever used for a *fallback* GPT-2 token counter
|
|
12
|
+
(``_get_token_ids_default_method``), but ``transformers`` eagerly pulls in
|
|
13
|
+
``torch``, ``numpy``, ``huggingface_hub`` and ``PIL``. Measured on this project:
|
|
14
|
+
|
|
15
|
+
full import of novacode_cli.main 305 MB RSS, ~13 s, 4056 modules
|
|
16
|
+
with transformers/torch excluded 145 MB RSS, ~2.4 s, 3053 modules
|
|
17
|
+
|
|
18
|
+
Nova talks to hosted model APIs and takes token counts from the provider
|
|
19
|
+
(``get_num_tokens_from_messages``, with char-based fallbacks at every call
|
|
20
|
+
site), so it never needs the GPT-2 tokenizer. Paying 160 MB of resident memory
|
|
21
|
+
and ~10 s of startup for it caused real problems on memory-constrained machines
|
|
22
|
+
(paging-file exhaustion, failed subprocess spawns).
|
|
23
|
+
|
|
24
|
+
This installs a meta-path finder that makes ``import transformers`` raise
|
|
25
|
+
``ImportError`` — the exact exception langchain_core already handles — so it
|
|
26
|
+
cleanly takes the ``_HAS_TRANSFORMERS = False`` branch.
|
|
27
|
+
|
|
28
|
+
Escape hatch: set ``NOVA_ALLOW_TRANSFORMERS=1`` to disable the guard. Anything
|
|
29
|
+
that genuinely needs the library (a local HF model, say) can also call
|
|
30
|
+
:func:`allow_transformers` first.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import os
|
|
36
|
+
import sys
|
|
37
|
+
from typing import Any
|
|
38
|
+
|
|
39
|
+
#: Top-level packages kept out of the startup graph.
|
|
40
|
+
_BLOCKED = frozenset({"transformers", "torch"})
|
|
41
|
+
|
|
42
|
+
_ENV_OPT_OUT = "NOVA_ALLOW_TRANSFORMERS"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class _HeavyImportGuard:
|
|
46
|
+
"""Meta-path finder that refuses the heavy optional deps.
|
|
47
|
+
|
|
48
|
+
Raising ``ImportError`` (rather than returning ``None``) is deliberate: the
|
|
49
|
+
consumers of these packages all wrap the import in ``try/except
|
|
50
|
+
ImportError`` and degrade gracefully. Returning ``None`` would just let the
|
|
51
|
+
real finder load them.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def find_spec(self, name: str, path: Any = None, target: Any = None) -> None: # noqa: ANN401, ARG002
|
|
55
|
+
if name.split(".")[0] in _BLOCKED:
|
|
56
|
+
msg = (
|
|
57
|
+
f"{name!r} is not imported at Nova startup (it costs ~160 MB of "
|
|
58
|
+
f"resident memory and is only needed for a fallback tokenizer). "
|
|
59
|
+
f"Set {_ENV_OPT_OUT}=1 if you really need it."
|
|
60
|
+
)
|
|
61
|
+
raise ImportError(msg)
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
_guard: _HeavyImportGuard | None = None
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def install() -> bool:
|
|
69
|
+
"""Install the guard. Returns True if it is now active.
|
|
70
|
+
|
|
71
|
+
No-ops when the user opted out, or when the packages are already imported
|
|
72
|
+
(blocking then would be pointless — the memory is already committed).
|
|
73
|
+
"""
|
|
74
|
+
global _guard
|
|
75
|
+
if os.environ.get(_ENV_OPT_OUT, "").strip().lower() in ("1", "true", "yes"):
|
|
76
|
+
return False
|
|
77
|
+
if _guard is not None:
|
|
78
|
+
return True
|
|
79
|
+
if any(mod in sys.modules for mod in _BLOCKED):
|
|
80
|
+
return False
|
|
81
|
+
_guard = _HeavyImportGuard()
|
|
82
|
+
sys.meta_path.insert(0, _guard)
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def allow_transformers() -> None:
|
|
87
|
+
"""Remove the guard so ``transformers``/``torch`` can be imported again."""
|
|
88
|
+
global _guard
|
|
89
|
+
if _guard is not None:
|
|
90
|
+
try:
|
|
91
|
+
sys.meta_path.remove(_guard)
|
|
92
|
+
except ValueError:
|
|
93
|
+
pass
|
|
94
|
+
_guard = None
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def is_active() -> bool:
|
|
98
|
+
"""Whether the guard is currently installed."""
|
|
99
|
+
return _guard is not None
|
novacode_cli/_version.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""UI-agnostic agent streaming.
|
|
2
|
+
|
|
3
|
+
Thin wrapper around :func:`novacode_cli.core.agent_loop.iterate_agent_events`
|
|
4
|
+
that preserves the public ``run_agent_stream`` API for backward compatibility.
|
|
5
|
+
|
|
6
|
+
Human-in-the-loop interrupts are surfaced as :class:`~novacode_cli.ui_events.InterruptRequest`
|
|
7
|
+
events carrying an ``asyncio.Future`` that the consumer must resolve with the
|
|
8
|
+
decision; the generator awaits it and resumes the graph.
|
|
9
|
+
|
|
10
|
+
When the learning loop is enabled, each completed turn is graded out-of-band and
|
|
11
|
+
the verdict is logged — that verdict is the quality signal ``/prompt`` evolution
|
|
12
|
+
(prompt-template A/B testing) and ``verification_log`` feed on. Historically only
|
|
13
|
+
the legacy CLI path (``ui/execution.py`` via ``run_with_verification``) produced
|
|
14
|
+
this signal, so the TUI — which calls ``iterate_agent_events`` through here —
|
|
15
|
+
generated nothing, leaving ``verification_log`` empty and auto prompt-evolution
|
|
16
|
+
starved. The grade runs as a fire-and-forget background task: no retries and no
|
|
17
|
+
UI events, so it never delays or alters the turn.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import asyncio
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
|
+
|
|
25
|
+
from novacode_cli import ui_events as ev
|
|
26
|
+
from novacode_cli.core.autonomous_loop import run_with_goal
|
|
27
|
+
from novacode_cli.sessions.lease import lease_holder, lease_session
|
|
28
|
+
|
|
29
|
+
if TYPE_CHECKING:
|
|
30
|
+
from collections.abc import AsyncIterator, Callable
|
|
31
|
+
|
|
32
|
+
# Hold references to in-flight grade tasks so they aren't GC'd mid-run.
|
|
33
|
+
_verification_tasks: set[asyncio.Task] = set()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class _NullAsyncCM:
|
|
37
|
+
"""A no-op async context manager (used when there is no thread to lease)."""
|
|
38
|
+
|
|
39
|
+
async def __aenter__(self) -> None:
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
async def __aexit__(self, *exc: object) -> None:
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _null_async_cm() -> _NullAsyncCM:
|
|
47
|
+
return _NullAsyncCM()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _maybe_verifier() -> Any | None:
|
|
51
|
+
"""An ``InlineVerifier`` when the learning loop is enabled, else ``None``.
|
|
52
|
+
|
|
53
|
+
Gated behind ``learning_enabled`` (opt-in) because grading adds one
|
|
54
|
+
out-of-band model call per turn. Best-effort — any setup failure returns
|
|
55
|
+
``None`` and the turn runs unverified.
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
from novacode_cli.config.nova_config import NovaConfig
|
|
59
|
+
|
|
60
|
+
if not NovaConfig().get_learning_enabled():
|
|
61
|
+
return None
|
|
62
|
+
from novacode_cli.hermes.verifier import InlineVerifier
|
|
63
|
+
from novacode_cli.memory.store import get_durable_store
|
|
64
|
+
|
|
65
|
+
return InlineVerifier(get_durable_store(), enabled=True)
|
|
66
|
+
except Exception: # noqa: BLE001 — learning is best-effort, never break a turn
|
|
67
|
+
return None
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _spawn_verification_signal(
|
|
71
|
+
verifier: Any,
|
|
72
|
+
user_input: str,
|
|
73
|
+
agent_output: str,
|
|
74
|
+
file_ops: list,
|
|
75
|
+
session_state: Any,
|
|
76
|
+
tool_results: list | None = None,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""Grade the finished turn and log the verdict (verification_log + prompt A/B).
|
|
79
|
+
|
|
80
|
+
Fire-and-forget: no retries, no UI events. Skipped for a pure no-op turn
|
|
81
|
+
(nothing produced to grade).
|
|
82
|
+
"""
|
|
83
|
+
if verifier is None or (not agent_output and not file_ops):
|
|
84
|
+
return
|
|
85
|
+
|
|
86
|
+
async def _grade() -> None:
|
|
87
|
+
try:
|
|
88
|
+
from novacode_cli.core.verification_loop import (
|
|
89
|
+
_extract_diffs,
|
|
90
|
+
_extract_test_evidence,
|
|
91
|
+
_record_prompt_ab_outcome,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
test_evidence = _extract_test_evidence(tool_results or [])
|
|
95
|
+
diffs = _extract_diffs(file_ops)
|
|
96
|
+
verdict = await verifier.grade(
|
|
97
|
+
user_input,
|
|
98
|
+
agent_output,
|
|
99
|
+
file_ops,
|
|
100
|
+
test_evidence=test_evidence,
|
|
101
|
+
diffs=diffs,
|
|
102
|
+
)
|
|
103
|
+
await verifier.log_outcome(getattr(session_state, "thread_id", ""), verdict, 0)
|
|
104
|
+
# Feed prompt-evolution (Enhancement 2) its quality signal.
|
|
105
|
+
await _record_prompt_ab_outcome(passed=verdict.passed)
|
|
106
|
+
except Exception: # noqa: BLE001 — a grading failure must never surface
|
|
107
|
+
pass
|
|
108
|
+
|
|
109
|
+
task = asyncio.create_task(_grade())
|
|
110
|
+
_verification_tasks.add(task)
|
|
111
|
+
task.add_done_callback(_verification_tasks.discard)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def run_agent_stream(
|
|
115
|
+
user_input: str,
|
|
116
|
+
agent,
|
|
117
|
+
assistant_id: str | None,
|
|
118
|
+
session_state,
|
|
119
|
+
*,
|
|
120
|
+
backend=None,
|
|
121
|
+
image_tracker=None,
|
|
122
|
+
seen_message_ids: set[str] | None = None,
|
|
123
|
+
skip_file_mentions: bool = False,
|
|
124
|
+
can_continue: Callable[[], bool] | None = None,
|
|
125
|
+
) -> AsyncIterator[Any]:
|
|
126
|
+
"""Run the agent and yield UI events.
|
|
127
|
+
|
|
128
|
+
Delegates to :func:`~novacode_cli.core.agent_loop.iterate_agent_events` (or
|
|
129
|
+
:func:`~novacode_cli.core.autonomous_loop.run_with_goal` when a goal is
|
|
130
|
+
active, so goal mode works from the TUI). Yields instances from
|
|
131
|
+
:mod:`novacode_cli.ui_events`. Terminates with a
|
|
132
|
+
:class:`~novacode_cli.ui_events.Done`, :class:`~novacode_cli.ui_events.Cancelled`,
|
|
133
|
+
or :class:`~novacode_cli.ui_events.Error` event. When learning is enabled,
|
|
134
|
+
grades the completed turn out-of-band (see module docstring).
|
|
135
|
+
|
|
136
|
+
The turn runs under a best-effort cross-process session lease (never blocks;
|
|
137
|
+
proceeds unleased on conflict).
|
|
138
|
+
"""
|
|
139
|
+
verifier = _maybe_verifier()
|
|
140
|
+
texts: list[str] = []
|
|
141
|
+
file_ops: list[Any] = []
|
|
142
|
+
tool_results: list[Any] = []
|
|
143
|
+
|
|
144
|
+
thread_id = getattr(session_state, "thread_id", None) or getattr(
|
|
145
|
+
session_state, "session_id", None
|
|
146
|
+
)
|
|
147
|
+
holder = lease_holder("tui")
|
|
148
|
+
|
|
149
|
+
# Best-effort cross-process lease. Only acquired when we have a real
|
|
150
|
+
# thread_id; never blocks (proceeds unleased on conflict).
|
|
151
|
+
_lease_cm = lease_session(thread_id, holder) if thread_id else _null_async_cm()
|
|
152
|
+
|
|
153
|
+
async with _lease_cm:
|
|
154
|
+
async for event in run_with_goal(
|
|
155
|
+
user_input,
|
|
156
|
+
agent,
|
|
157
|
+
assistant_id,
|
|
158
|
+
session_state,
|
|
159
|
+
backend=backend,
|
|
160
|
+
image_tracker=image_tracker,
|
|
161
|
+
seen_message_ids=seen_message_ids,
|
|
162
|
+
skip_file_mentions=skip_file_mentions,
|
|
163
|
+
can_continue=can_continue,
|
|
164
|
+
):
|
|
165
|
+
if verifier is not None:
|
|
166
|
+
if isinstance(event, ev.AssistantMessage):
|
|
167
|
+
texts.append(event.text)
|
|
168
|
+
elif isinstance(event, ev.FileOp) and getattr(event, "record", None) is not None:
|
|
169
|
+
file_ops.append(event.record)
|
|
170
|
+
elif isinstance(event, ev.ToolResult):
|
|
171
|
+
tool_results.append(event)
|
|
172
|
+
yield event
|
|
173
|
+
|
|
174
|
+
if verifier is not None:
|
|
175
|
+
agent_output = "\n\n".join(t for t in texts if t).strip()
|
|
176
|
+
_spawn_verification_signal(
|
|
177
|
+
verifier, user_input, agent_output, file_ops, session_state, tool_results
|
|
178
|
+
)
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""Custom subagent definitions and the tools a subagent may be given.
|
|
2
|
+
|
|
3
|
+
An ``agent.md`` is YAML frontmatter plus the system prompt::
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
color: "#0ea5e9"
|
|
7
|
+
description: "Reviews Python for security bugs"
|
|
8
|
+
tools: ["web_search", "serena_find_symbol"]
|
|
9
|
+
---
|
|
10
|
+
You are ...
|
|
11
|
+
|
|
12
|
+
``tools`` is optional. Absent means every tool (what agents always had);
|
|
13
|
+
a list means exactly those, on top of the file/shell tools every subagent gets
|
|
14
|
+
from deepagents itself (:data:`ALWAYS_INCLUDED`).
|
|
15
|
+
|
|
16
|
+
Create new definitions as flat ``.nova/agents/<name>.md`` files in a project or
|
|
17
|
+
``~/.nova/agents/<name>.md`` globally. Existing ``<name>/agent.md`` folders and
|
|
18
|
+
Claude Code agent files remain supported::
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
name: approver
|
|
22
|
+
description: Final gate; writes decision.md.
|
|
23
|
+
tools: Read, Grep, Glob, Bash, Write
|
|
24
|
+
model: opus
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
Claude tool names map to Nova's (:data:`CLAUDE_TOOL_NAMES`). A list that names
|
|
28
|
+
any file/shell tool is a Claude-style allowlist: the file/shell tools it leaves
|
|
29
|
+
out are taken away (:class:`FilesystemToolFilter`), so ``approver`` has no
|
|
30
|
+
``edit_file``.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import json
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
38
|
+
|
|
39
|
+
from langchain.agents.middleware import AgentMiddleware
|
|
40
|
+
from langchain_core.messages import ToolMessage
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from collections.abc import Awaitable, Callable
|
|
44
|
+
|
|
45
|
+
from langchain.agents.middleware.types import ModelRequest, ModelResponse
|
|
46
|
+
from langchain.tools.tool_node import ToolCallRequest
|
|
47
|
+
from langgraph.types import Command
|
|
48
|
+
|
|
49
|
+
#: Given to every subagent by deepagents' own middleware; not selectable.
|
|
50
|
+
ALWAYS_INCLUDED = (
|
|
51
|
+
"ls", "read_file", "write_file", "edit_file", "delete", "glob", "grep", "execute",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
#: Claude Code tool names in an agent file's ``tools:`` → Nova's tool names.
|
|
55
|
+
CLAUDE_TOOL_NAMES = {
|
|
56
|
+
"Read": "read_file",
|
|
57
|
+
"Write": "write_file",
|
|
58
|
+
"Edit": "edit_file",
|
|
59
|
+
"MultiEdit": "edit_file",
|
|
60
|
+
"NotebookEdit": "NotebookEditTool",
|
|
61
|
+
"Grep": "grep",
|
|
62
|
+
"Glob": "glob",
|
|
63
|
+
"GlobTool": "glob",
|
|
64
|
+
"GrepTool": "grep",
|
|
65
|
+
"LS": "ls",
|
|
66
|
+
"Bash": "execute",
|
|
67
|
+
"WebFetch": "fetch_url",
|
|
68
|
+
"WebSearch": "web_search",
|
|
69
|
+
"TodoWrite": "write_todos",
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _parse(content: str) -> tuple[dict[str, Any], str]:
|
|
74
|
+
if not content.startswith("---"):
|
|
75
|
+
return {}, content
|
|
76
|
+
parts = content.split("---", 2)
|
|
77
|
+
if len(parts) < 3:
|
|
78
|
+
return {}, content
|
|
79
|
+
front: dict[str, Any] = {}
|
|
80
|
+
try:
|
|
81
|
+
import yaml
|
|
82
|
+
|
|
83
|
+
loaded = yaml.safe_load(parts[1])
|
|
84
|
+
if isinstance(loaded, dict):
|
|
85
|
+
front = loaded
|
|
86
|
+
except Exception: # noqa: BLE001 — hand-written files: fall back to key: value lines
|
|
87
|
+
for line in parts[1].splitlines():
|
|
88
|
+
if ":" in line:
|
|
89
|
+
key, value = line.split(":", 1)
|
|
90
|
+
front[key.strip()] = value.strip().strip('"').strip("'")
|
|
91
|
+
return front, parts[2].lstrip("\n")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _split(content: str) -> tuple[dict[str, Any], str]:
|
|
95
|
+
"""Reuse parsed frontmatter by exact content, never resolved capabilities."""
|
|
96
|
+
from time import perf_counter_ns
|
|
97
|
+
|
|
98
|
+
from novacode_cli.computation_cache import digest, get, put, record_timing
|
|
99
|
+
|
|
100
|
+
started = perf_counter_ns()
|
|
101
|
+
key = digest("agent-definition-v1\0" + content)
|
|
102
|
+
record_timing("agent_definitions", "validation", perf_counter_ns() - started)
|
|
103
|
+
cached = get("agent_definitions", key)
|
|
104
|
+
if cached is not None:
|
|
105
|
+
cached = cast("list[Any]", cached)
|
|
106
|
+
return cached[0], cached[1]
|
|
107
|
+
started = perf_counter_ns()
|
|
108
|
+
front, body = _parse(content)
|
|
109
|
+
record_timing("agent_definitions", "computation", perf_counter_ns() - started)
|
|
110
|
+
if set(front) <= {"name", "description", "tools", "model", "color", "skill_names"}:
|
|
111
|
+
put("agent_definitions", key, [front, body])
|
|
112
|
+
return front, body
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def read_agent(path: Path) -> tuple[dict[str, Any], str]:
|
|
116
|
+
"""``(frontmatter, system_prompt)`` of an ``agent.md``."""
|
|
117
|
+
return _split(path.read_text(encoding="utf-8"))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def agent_tools(content: str) -> list[str] | None:
|
|
121
|
+
"""The agent's chosen tool names, or None when it did not choose (= all)."""
|
|
122
|
+
tools = _split(content)[0].get("tools")
|
|
123
|
+
if tools is None:
|
|
124
|
+
return None
|
|
125
|
+
if isinstance(tools, str):
|
|
126
|
+
tools = [t.strip() for t in tools.split(",")]
|
|
127
|
+
names = [str(t).strip() for t in tools if str(t).strip()]
|
|
128
|
+
return list(dict.fromkeys(CLAUDE_TOOL_NAMES.get(n, n) for n in names))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def filesystem_allowlist(chosen: list[str] | None) -> set[str] | None:
|
|
132
|
+
"""The file/shell tools an agent keeps, or None when it keeps all of them.
|
|
133
|
+
|
|
134
|
+
Only a list naming at least one of them restricts: Nova's own agent files
|
|
135
|
+
never list them (the /agents picker hides them), so they keep every one.
|
|
136
|
+
"""
|
|
137
|
+
allowed = set(chosen or ()) & set(ALWAYS_INCLUDED)
|
|
138
|
+
return allowed or None
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class FilesystemToolFilter(AgentMiddleware):
|
|
142
|
+
"""Takes away the file/shell tools an agent's ``tools:`` list left out.
|
|
143
|
+
|
|
144
|
+
deepagents gives every subagent all of :data:`ALWAYS_INCLUDED`; this hides
|
|
145
|
+
the others from the model and refuses a call to one anyway.
|
|
146
|
+
"""
|
|
147
|
+
|
|
148
|
+
def __init__(self, allowed: set[str]) -> None:
|
|
149
|
+
"""Keep *allowed* of :data:`ALWAYS_INCLUDED`; block the rest."""
|
|
150
|
+
super().__init__()
|
|
151
|
+
self._blocked = set(ALWAYS_INCLUDED) - allowed
|
|
152
|
+
|
|
153
|
+
def _bind(self, request: ModelRequest) -> ModelRequest:
|
|
154
|
+
keep = [t for t in request.tools if getattr(t, "name", None) not in self._blocked]
|
|
155
|
+
return request.override(tools=keep)
|
|
156
|
+
|
|
157
|
+
def _refusal(self, request: ToolCallRequest) -> ToolMessage | None:
|
|
158
|
+
call = request.tool_call
|
|
159
|
+
if call["name"] not in self._blocked:
|
|
160
|
+
return None
|
|
161
|
+
return ToolMessage(
|
|
162
|
+
f"{call['name']} is not in this agent's tools.",
|
|
163
|
+
tool_call_id=call["id"],
|
|
164
|
+
name=call["name"],
|
|
165
|
+
status="error",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def wrap_model_call(
|
|
169
|
+
self,
|
|
170
|
+
request: ModelRequest,
|
|
171
|
+
handler: Callable[[ModelRequest], ModelResponse],
|
|
172
|
+
) -> ModelResponse:
|
|
173
|
+
"""Bind only the allowed file/shell tools."""
|
|
174
|
+
return handler(self._bind(request))
|
|
175
|
+
|
|
176
|
+
async def awrap_model_call(
|
|
177
|
+
self,
|
|
178
|
+
request: ModelRequest,
|
|
179
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
180
|
+
) -> ModelResponse:
|
|
181
|
+
"""Bind only the allowed file/shell tools."""
|
|
182
|
+
return await handler(self._bind(request))
|
|
183
|
+
|
|
184
|
+
def wrap_tool_call(
|
|
185
|
+
self,
|
|
186
|
+
request: ToolCallRequest,
|
|
187
|
+
handler: Callable[[ToolCallRequest], ToolMessage | Command],
|
|
188
|
+
) -> ToolMessage | Command:
|
|
189
|
+
"""Refuse a blocked tool the model called anyway."""
|
|
190
|
+
return self._refusal(request) or handler(request)
|
|
191
|
+
|
|
192
|
+
async def awrap_tool_call(
|
|
193
|
+
self,
|
|
194
|
+
request: ToolCallRequest,
|
|
195
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
|
196
|
+
) -> ToolMessage | Command:
|
|
197
|
+
"""Refuse a blocked tool the model called anyway."""
|
|
198
|
+
return self._refusal(request) or await handler(request)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def write_agent(path: Path, front: dict[str, Any], body: str) -> None:
|
|
202
|
+
"""Write ``agent.md``. Values are JSON-quoted, which is valid YAML: a
|
|
203
|
+
description holding ``: `` or a Windows path can no longer break it."""
|
|
204
|
+
lines = ["---"]
|
|
205
|
+
for key, value in front.items():
|
|
206
|
+
if value is None:
|
|
207
|
+
continue
|
|
208
|
+
lines.append(f"{key}: {json.dumps(value, ensure_ascii=False)}")
|
|
209
|
+
lines += ["---", "", body.strip(), ""]
|
|
210
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
211
|
+
path.write_text("\n".join(lines), encoding="utf-8")
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def set_agent_tools(path: Path, tools: list[str] | None) -> None:
|
|
215
|
+
"""Replace an agent's tool selection (None = back to every tool)."""
|
|
216
|
+
front, body = read_agent(path)
|
|
217
|
+
front.pop("tools", None)
|
|
218
|
+
if tools is not None:
|
|
219
|
+
front["tools"] = sorted(tools)
|
|
220
|
+
write_agent(path, front, body)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def tool_arsenal(session_tools: list[Any] | None = None) -> list[tuple[str, str]]:
|
|
224
|
+
"""Every selectable tool as ``(name, one-line description)``, sorted.
|
|
225
|
+
|
|
226
|
+
Nova's own tools come from the live session; MCP tools from the schema
|
|
227
|
+
cache of each enabled server (named ``<server>_<tool>``, as bound).
|
|
228
|
+
"""
|
|
229
|
+
found: dict[str, str] = {}
|
|
230
|
+
for tool in session_tools or []:
|
|
231
|
+
name = getattr(tool, "name", None)
|
|
232
|
+
if name and name not in ALWAYS_INCLUDED:
|
|
233
|
+
found[name] = (getattr(tool, "description", "") or "").strip().split("\n")[0]
|
|
234
|
+
try:
|
|
235
|
+
from novacode_cli.mcp.config import MCPConfig
|
|
236
|
+
from novacode_cli.mcp.middleware import _load_schema_cache
|
|
237
|
+
|
|
238
|
+
enabled = {
|
|
239
|
+
name for name, cfg in MCPConfig().list_servers().items()
|
|
240
|
+
if not getattr(cfg, "disabled", False)
|
|
241
|
+
}
|
|
242
|
+
for server, entry in _load_schema_cache().items():
|
|
243
|
+
if server not in enabled or not isinstance(entry, dict):
|
|
244
|
+
continue
|
|
245
|
+
for raw in entry.get("tools") or []:
|
|
246
|
+
desc = (raw.get("description") or "").strip().split("\n")[0]
|
|
247
|
+
found[f"{server}_{raw.get('name')}"] = desc
|
|
248
|
+
except Exception: # noqa: BLE001 — MCP listing is best effort
|
|
249
|
+
pass
|
|
250
|
+
return sorted(found.items())
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Agent-to-agent mailbox — a lightweight in-process message bus.
|
|
2
|
+
|
|
3
|
+
Nova's subagents run synchronously (one at a time, via the `task` tool), so they
|
|
4
|
+
can't hold a live conversation. This mailbox gives peer messaging instead: any
|
|
5
|
+
agent (orchestrator or subagent) drops a message into another agent's inbox by
|
|
6
|
+
name, and reads its own inbox when it runs. The orchestrator sequences the turns
|
|
7
|
+
(dispatch A → A messages B → dispatch B → B reads + replies → A reads).
|
|
8
|
+
|
|
9
|
+
Process-global and thread-safe: subagents run in the same process as the
|
|
10
|
+
orchestrator, so a module-level store is all that's needed. It is NOT durable
|
|
11
|
+
across restarts (a conversation lives within one session), by design.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
from langchain.tools import tool
|
|
20
|
+
|
|
21
|
+
_lock = threading.RLock()
|
|
22
|
+
# recipient agent name -> list of {"from": str, "text": str, "ts": float}
|
|
23
|
+
_mailbox: dict[str, list[dict]] = {}
|
|
24
|
+
_scoped_mailbox: dict[tuple[str, str, str, str], list[dict]] = {}
|
|
25
|
+
_MAX_PER_INBOX = 100 # cap so a runaway loop can't grow memory unbounded
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@tool
|
|
29
|
+
def send_agent_message(to_agent: str, message: str, from_agent: str = "orchestrator") -> str:
|
|
30
|
+
"""Send a message to another agent's inbox (agent-to-agent communication).
|
|
31
|
+
|
|
32
|
+
Use this to hand a question, result, or instruction to a specific peer agent
|
|
33
|
+
by name. The recipient reads it with `read_agent_messages` when it next runs.
|
|
34
|
+
|
|
35
|
+
Args:
|
|
36
|
+
to_agent: The exact name of the recipient agent (e.g. "code-explorer").
|
|
37
|
+
message: The message to deliver.
|
|
38
|
+
from_agent: Your own agent name, so the recipient knows who wrote it.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Confirmation that the message was queued.
|
|
42
|
+
"""
|
|
43
|
+
to_agent = (to_agent or "").strip()
|
|
44
|
+
if not to_agent:
|
|
45
|
+
return "Error: to_agent is required."
|
|
46
|
+
from novacode_cli.teams.legacy import mailbox_identity
|
|
47
|
+
|
|
48
|
+
scope = mailbox_identity.get()
|
|
49
|
+
if scope is not None:
|
|
50
|
+
from_agent = scope[3]
|
|
51
|
+
with _lock:
|
|
52
|
+
inbox = (
|
|
53
|
+
_scoped_mailbox.setdefault((*scope[:3], to_agent), [])
|
|
54
|
+
if scope
|
|
55
|
+
else _mailbox.setdefault(to_agent, [])
|
|
56
|
+
)
|
|
57
|
+
if scope and (len(inbox) >= _MAX_PER_INBOX or len(message.encode()) > 16384):
|
|
58
|
+
return "Error: scoped inbox limit reached; read pending messages first."
|
|
59
|
+
inbox.append({"from": from_agent or "unknown", "text": message, "ts": time.time()})
|
|
60
|
+
if len(inbox) > _MAX_PER_INBOX:
|
|
61
|
+
del inbox[: len(inbox) - _MAX_PER_INBOX]
|
|
62
|
+
return f"Message delivered to '{to_agent}' inbox."
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@tool
|
|
66
|
+
def read_agent_messages(as_agent: str, clear: bool = True) -> str:
|
|
67
|
+
"""Read messages addressed to you (agent-to-agent communication).
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
as_agent: Your own agent name — the inbox to read.
|
|
71
|
+
clear: When True (default), consume the messages so they aren't re-read.
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
The messages addressed to you, newest last, or "No messages.".
|
|
75
|
+
"""
|
|
76
|
+
as_agent = (as_agent or "").strip()
|
|
77
|
+
if not as_agent:
|
|
78
|
+
return "Error: as_agent is required (pass your own agent name)."
|
|
79
|
+
from novacode_cli.teams.legacy import mailbox_identity
|
|
80
|
+
|
|
81
|
+
scope = mailbox_identity.get()
|
|
82
|
+
with _lock:
|
|
83
|
+
if scope:
|
|
84
|
+
if as_agent != scope[3]:
|
|
85
|
+
return "Error: cannot read another agent's inbox."
|
|
86
|
+
msgs = list(_scoped_mailbox.get(scope, []))
|
|
87
|
+
if clear:
|
|
88
|
+
_scoped_mailbox.pop(scope, None)
|
|
89
|
+
else:
|
|
90
|
+
msgs = list(_mailbox.get(as_agent, []))
|
|
91
|
+
if clear:
|
|
92
|
+
_mailbox[as_agent] = []
|
|
93
|
+
if not msgs:
|
|
94
|
+
return "No messages."
|
|
95
|
+
return "\n".join(f"[from {m['from']}] {m['text']}" for m in msgs)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def clear_mailbox() -> None:
|
|
99
|
+
"""Drop every inbox (used on /clear so a new conversation starts empty)."""
|
|
100
|
+
with _lock:
|
|
101
|
+
_mailbox.clear()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
clear_mailbox()
|
|
106
|
+
assert send_agent_message.invoke({"to_agent": "b", "message": "hi B", "from_agent": "a"}).startswith(
|
|
107
|
+
"Message delivered"
|
|
108
|
+
)
|
|
109
|
+
assert read_agent_messages.invoke({"as_agent": "b"}) == "[from a] hi B"
|
|
110
|
+
assert read_agent_messages.invoke({"as_agent": "b"}) == "No messages." # cleared
|
|
111
|
+
assert read_agent_messages.invoke({"as_agent": "nobody"}) == "No messages."
|
|
112
|
+
print("agent_mailbox self-check ok")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def clear_scoped_mailbox(project: str, root: str) -> None:
|
|
116
|
+
"""Release one root's legacy inboxes after its owned workers stop."""
|
|
117
|
+
with _lock:
|
|
118
|
+
for key in list(_scoped_mailbox):
|
|
119
|
+
if key[:2] == (project, root):
|
|
120
|
+
_scoped_mailbox.pop(key, None)
|