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/council.py
ADDED
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
"""Council of agents — independent answers + democratic voting.
|
|
2
|
+
|
|
3
|
+
A *council* takes a single question and runs it past five distinct personas,
|
|
4
|
+
each backed by the session's configured model but given its own personality.
|
|
5
|
+
Every member answers **independently** (it cannot see the others' answers), then
|
|
6
|
+
each casts a **single democratic vote** for the best answer (never its own). The
|
|
7
|
+
**majority** answer is the verdict.
|
|
8
|
+
|
|
9
|
+
:func:`run_council` is a pure async generator yielding plain ``dict`` events, so
|
|
10
|
+
it can be unit-tested without HTTP and rendered by the ``/council`` web server as
|
|
11
|
+
Server-Sent Events (see :mod:`novacode_cli.commands.chat_handler`).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import asyncio
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import TYPE_CHECKING, Any
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from collections.abc import AsyncIterator
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
# Max web searches a single persona may run before it must answer.
|
|
29
|
+
_MAX_TOOL_ROUNDS = 2
|
|
30
|
+
|
|
31
|
+
# Tool offered to each persona, as a plain OpenAI-style schema dict. Passing a
|
|
32
|
+
# dict (rather than a LangChain ``@tool``) keeps this module import-light — it
|
|
33
|
+
# avoids importing ``langchain_core`` (which pulls in transformers) at import
|
|
34
|
+
# time; the configured model already brings LangChain with it at run time.
|
|
35
|
+
_SEARCH_TOOL_NAME = "council_web_search"
|
|
36
|
+
_SEARCH_TOOL_SCHEMA: dict[str, Any] = {
|
|
37
|
+
"type": "function",
|
|
38
|
+
"function": {
|
|
39
|
+
"name": _SEARCH_TOOL_NAME,
|
|
40
|
+
"description": (
|
|
41
|
+
"Search the web (DuckDuckGo) for current, factual information. Use a "
|
|
42
|
+
"focused query. Call this only when you genuinely need up-to-date or "
|
|
43
|
+
"factual grounding for your perspective."
|
|
44
|
+
),
|
|
45
|
+
"parameters": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {
|
|
48
|
+
"query": {"type": "string", "description": "The search query."}
|
|
49
|
+
},
|
|
50
|
+
"required": ["query"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _run_council_tool(name: str, args: dict[str, Any]) -> str:
|
|
57
|
+
"""Execute a council tool call by name and return its string result."""
|
|
58
|
+
if name == _SEARCH_TOOL_NAME:
|
|
59
|
+
from novacode_cli.tools.web_tools import duckduckgo_search
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
res = duckduckgo_search(str(args.get("query", "")), max_results=4) # type: ignore
|
|
63
|
+
except Exception as exc: # noqa: BLE001
|
|
64
|
+
return f"(search error: {exc})"
|
|
65
|
+
if not res.get("success"):
|
|
66
|
+
return f"(search failed: {res.get('error', 'unknown error')})"
|
|
67
|
+
results = res.get("results", [])
|
|
68
|
+
if not results:
|
|
69
|
+
return "(no results)"
|
|
70
|
+
return "\n".join(
|
|
71
|
+
f"- {r.get('title', '')}: {r.get('body', '')} ({r.get('url', '')})"
|
|
72
|
+
for r in results
|
|
73
|
+
)
|
|
74
|
+
return f"(unknown tool: {name})"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class Persona:
|
|
79
|
+
"""One council member: an id, display identity, and a system prompt."""
|
|
80
|
+
|
|
81
|
+
id: str
|
|
82
|
+
name: str
|
|
83
|
+
avatar: str
|
|
84
|
+
color: str
|
|
85
|
+
system: str
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
PERSONAS: list[Persona] = [
|
|
89
|
+
Persona(
|
|
90
|
+
id="architect",
|
|
91
|
+
name="The Architect",
|
|
92
|
+
avatar="🏛️",
|
|
93
|
+
color="#6ea8fe",
|
|
94
|
+
system=(
|
|
95
|
+
"You are The Architect. You think in systems, boundaries, and the "
|
|
96
|
+
"long view: structure, separation of concerns, data flow, and how a "
|
|
97
|
+
"choice ages over years. You value maintainability over cleverness."
|
|
98
|
+
),
|
|
99
|
+
),
|
|
100
|
+
Persona(
|
|
101
|
+
id="pragmatist",
|
|
102
|
+
name="The Pragmatist",
|
|
103
|
+
avatar="🛠️",
|
|
104
|
+
color="#4cae8a",
|
|
105
|
+
system=(
|
|
106
|
+
"You are The Pragmatist. You care about shipping working software. "
|
|
107
|
+
"You favor the simplest thing that solves the real problem, weigh "
|
|
108
|
+
"effort against payoff, and call out over-engineering and YAGNI."
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
Persona(
|
|
112
|
+
id="skeptic",
|
|
113
|
+
name="The Skeptic",
|
|
114
|
+
avatar="🔍",
|
|
115
|
+
color="#d4a13b",
|
|
116
|
+
system=(
|
|
117
|
+
"You are The Skeptic. You hunt for what breaks: edge cases, failure "
|
|
118
|
+
"modes, race conditions, security holes, and hidden assumptions. You "
|
|
119
|
+
"are constructive but you do not let risks go unnamed."
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
Persona(
|
|
123
|
+
id="innovator",
|
|
124
|
+
name="The Innovator",
|
|
125
|
+
avatar="🚀",
|
|
126
|
+
color="#b06ae8",
|
|
127
|
+
system=(
|
|
128
|
+
"You are The Innovator. You look for the non-obvious, modern, or "
|
|
129
|
+
"creative approach others miss. You bring fresh techniques and "
|
|
130
|
+
"question whether the framing of the problem is even right."
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
Persona(
|
|
134
|
+
id="minimalist",
|
|
135
|
+
name="The Minimalist",
|
|
136
|
+
avatar="✂️",
|
|
137
|
+
color="#d46a6a",
|
|
138
|
+
system=(
|
|
139
|
+
"You are The Minimalist. You strip problems to their essence and "
|
|
140
|
+
"prefer the least code, the fewest moving parts, and the clearest "
|
|
141
|
+
"solution. You delete before you add."
|
|
142
|
+
),
|
|
143
|
+
),
|
|
144
|
+
]
|
|
145
|
+
|
|
146
|
+
_RESPONSE_GUIDE = (
|
|
147
|
+
"\n\nYou are one member of a council of advisors answering independently — "
|
|
148
|
+
"you cannot see the others' answers, so give a COMPLETE answer rather than "
|
|
149
|
+
"a single angle, and never refer to what anyone else said.\n"
|
|
150
|
+
"\n"
|
|
151
|
+
"Answer in this shape, without headings or preamble:\n"
|
|
152
|
+
"1. Your recommendation, in the first sentence. Commit to one — a reader "
|
|
153
|
+
"who stops there should know what you would do.\n"
|
|
154
|
+
"2. The reasoning that actually drives it, from your perspective.\n"
|
|
155
|
+
"3. The strongest case against your own recommendation, and why you still "
|
|
156
|
+
"hold it. If it would change your mind, name what evidence would.\n"
|
|
157
|
+
"\n"
|
|
158
|
+
"Be concrete: name the technique, the tradeoff, the failure. Prefer a "
|
|
159
|
+
"specific claim you might be wrong about over a safe generality. No "
|
|
160
|
+
"bullet-point walls, no restating the question, no 'it depends' without "
|
|
161
|
+
"saying what it depends on. Roughly 150-200 words. Speak in the first "
|
|
162
|
+
"person, in character — your perspective is why you are on this council, "
|
|
163
|
+
"but the answer must stand on its merits, not on your persona.\n"
|
|
164
|
+
"If the question turns on current or factual information you are unsure "
|
|
165
|
+
"of, search first, then answer."
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
_VOTE_GUIDE = (
|
|
169
|
+
"\n\nThe council has answered independently. Vote for the SINGLE best "
|
|
170
|
+
"answer on merit. Your own answer is not on the ballot.\n"
|
|
171
|
+
"\n"
|
|
172
|
+
"Judge by: does it actually answer the question asked; is the reasoning "
|
|
173
|
+
"sound; is it specific enough to act on; does it hold up against the risks "
|
|
174
|
+
"it acknowledges. Do NOT reward the answer that most resembles your own "
|
|
175
|
+
"outlook, length, or confidence — vote for the one you would want acted on "
|
|
176
|
+
"if the decision were yours and you had to live with it.\n"
|
|
177
|
+
"\n"
|
|
178
|
+
'Respond with ONLY this JSON and nothing else: {"choice":"<exact advisor '
|
|
179
|
+
'name>","reason":"<one specific sentence naming what made it best>"}'
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
# How much prior-round context to carry into a follow-up council.
|
|
184
|
+
_MAX_HISTORY_ROUNDS = 6
|
|
185
|
+
_HISTORY_ANSWER_CHARS = 400
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _format_history(history: list[dict[str, Any]] | None) -> str:
|
|
189
|
+
"""Render prior council rounds into a compact context block for follow-ups.
|
|
190
|
+
|
|
191
|
+
Each round is ``{"topic": str, "transcript": [[name, text], ...],
|
|
192
|
+
"winner": str}``. Older rounds and long answers are trimmed so the prompt
|
|
193
|
+
stays bounded while preserving the thread of discussion.
|
|
194
|
+
"""
|
|
195
|
+
if not history:
|
|
196
|
+
return ""
|
|
197
|
+
lines = ["Earlier in this council session (for context, so you can follow up):"]
|
|
198
|
+
for idx, rnd in enumerate(history[-_MAX_HISTORY_ROUNDS:], 1):
|
|
199
|
+
lines.append(f"\nRound {idx} — topic: {rnd.get('topic', '')}")
|
|
200
|
+
for entry in rnd.get("transcript", []):
|
|
201
|
+
try:
|
|
202
|
+
name, text = entry
|
|
203
|
+
except (ValueError, TypeError):
|
|
204
|
+
continue
|
|
205
|
+
text = (text or "").strip()
|
|
206
|
+
if len(text) > _HISTORY_ANSWER_CHARS:
|
|
207
|
+
text = text[:_HISTORY_ANSWER_CHARS] + "…"
|
|
208
|
+
lines.append(f" {name}: {text}")
|
|
209
|
+
if rnd.get("winner"):
|
|
210
|
+
lines.append(f" -> Verdict: {rnd['winner']} won that round.")
|
|
211
|
+
return "\n".join(lines)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _content_text(message: Any) -> str:
|
|
215
|
+
"""Extract plain text from a LangChain message/chunk ``.content``.
|
|
216
|
+
|
|
217
|
+
Content may be a string or a list of content blocks (e.g. Anthropic), so we
|
|
218
|
+
coalesce any text parts and ignore the rest.
|
|
219
|
+
"""
|
|
220
|
+
content = getattr(message, "content", message)
|
|
221
|
+
if isinstance(content, str):
|
|
222
|
+
return content
|
|
223
|
+
if isinstance(content, list):
|
|
224
|
+
parts: list[str] = []
|
|
225
|
+
for block in content:
|
|
226
|
+
if isinstance(block, str):
|
|
227
|
+
parts.append(block)
|
|
228
|
+
elif isinstance(block, dict):
|
|
229
|
+
text = block.get("text") or block.get("content")
|
|
230
|
+
if isinstance(text, str):
|
|
231
|
+
parts.append(text)
|
|
232
|
+
return "".join(parts)
|
|
233
|
+
return str(content) if content is not None else ""
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def _parse_scores(raw: str, valid_names: set[str]) -> list[tuple[str, int, str]]:
|
|
237
|
+
"""Parse a voter's JSON scorecard, keeping only entries for *valid_names*.
|
|
238
|
+
|
|
239
|
+
Tolerant of code fences / surrounding prose: extracts the first JSON object.
|
|
240
|
+
Scores are clamped to 1–10. Self-votes and unknown names are dropped.
|
|
241
|
+
"""
|
|
242
|
+
match = re.search(r"\{.*\}", raw, re.DOTALL)
|
|
243
|
+
if not match:
|
|
244
|
+
return []
|
|
245
|
+
try:
|
|
246
|
+
data = json.loads(match.group(0))
|
|
247
|
+
except (json.JSONDecodeError, ValueError):
|
|
248
|
+
return []
|
|
249
|
+
|
|
250
|
+
out: list[tuple[str, int, str]] = []
|
|
251
|
+
seen: set[str] = set()
|
|
252
|
+
for entry in data.get("scores", []) if isinstance(data, dict) else []:
|
|
253
|
+
if not isinstance(entry, dict):
|
|
254
|
+
continue
|
|
255
|
+
name = str(entry.get("agent", "")).strip()
|
|
256
|
+
if name not in valid_names or name in seen:
|
|
257
|
+
continue
|
|
258
|
+
try:
|
|
259
|
+
score = round(float(entry.get("score", 0)))
|
|
260
|
+
except (TypeError, ValueError):
|
|
261
|
+
continue
|
|
262
|
+
score = max(1, min(10, score))
|
|
263
|
+
reason = str(entry.get("reason", "")).strip()
|
|
264
|
+
out.append((name, score, reason))
|
|
265
|
+
seen.add(name)
|
|
266
|
+
return out
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _parse_vote(raw: str, valid_names: set[str]) -> tuple[str | None, str]:
|
|
270
|
+
"""Parse a single-choice ballot: ``{"choice": name, "reason": ...}``.
|
|
271
|
+
|
|
272
|
+
Returns ``(choice_name, reason)`` where ``choice_name`` is None if the model
|
|
273
|
+
didn't pick a valid (non-self) advisor. Tolerant of code fences / prose.
|
|
274
|
+
"""
|
|
275
|
+
match = re.search(r"\{.*\}", raw, re.DOTALL)
|
|
276
|
+
if not match:
|
|
277
|
+
return None, ""
|
|
278
|
+
try:
|
|
279
|
+
data = json.loads(match.group(0))
|
|
280
|
+
except (json.JSONDecodeError, ValueError):
|
|
281
|
+
return None, ""
|
|
282
|
+
if not isinstance(data, dict):
|
|
283
|
+
return None, ""
|
|
284
|
+
choice = str(data.get("choice", "")).strip()
|
|
285
|
+
reason = str(data.get("reason", "")).strip()
|
|
286
|
+
if choice not in valid_names:
|
|
287
|
+
return None, reason
|
|
288
|
+
return choice, reason
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def get_council_model() -> Any:
|
|
292
|
+
"""Build a chat model for the council from the session's configured provider.
|
|
293
|
+
|
|
294
|
+
Reuses :class:`~novacode_cli.config.model_manager.ModelManager` so the
|
|
295
|
+
council speaks with whatever model the user picked via ``/model`` (Ollama by
|
|
296
|
+
default). Raises if no provider can be created.
|
|
297
|
+
"""
|
|
298
|
+
from novacode_cli.config.model_manager import MODEL_PRESETS, ModelManager
|
|
299
|
+
|
|
300
|
+
mm = ModelManager()
|
|
301
|
+
current = mm.get_current_provider() # (display_name, model_name) | None
|
|
302
|
+
provider_id = "ollama"
|
|
303
|
+
model_name: str | None = None
|
|
304
|
+
if current:
|
|
305
|
+
display, model_name = current
|
|
306
|
+
for pid, preset in MODEL_PRESETS.items():
|
|
307
|
+
if preset["name"] == display:
|
|
308
|
+
provider_id = pid
|
|
309
|
+
break
|
|
310
|
+
return mm.create_model_for_provider(provider_id, model_name) # type: ignore[arg-type]
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def _stream_persona_turn(
|
|
314
|
+
persona: Persona, convo: str, model: Any
|
|
315
|
+
) -> AsyncIterator[tuple[str, str]]:
|
|
316
|
+
"""Stream one persona's turn, allowing bounded web-search tool calls.
|
|
317
|
+
|
|
318
|
+
Yields ``("delta", text)`` for streamed prose and ``("tool", query)`` when
|
|
319
|
+
the persona runs a web search. Models without tool support (or that can't
|
|
320
|
+
stream tool calls) fall back to plain streaming.
|
|
321
|
+
"""
|
|
322
|
+
system_text = persona.system + _RESPONSE_GUIDE
|
|
323
|
+
|
|
324
|
+
model_tools = None
|
|
325
|
+
if hasattr(model, "bind_tools"):
|
|
326
|
+
try:
|
|
327
|
+
model_tools = model.bind_tools([_SEARCH_TOOL_SCHEMA])
|
|
328
|
+
except Exception: # noqa: BLE001 - provider may not support tools
|
|
329
|
+
model_tools = None
|
|
330
|
+
|
|
331
|
+
if model_tools is None:
|
|
332
|
+
async for chunk in model.astream([("system", system_text), ("human", convo)]):
|
|
333
|
+
piece = _content_text(chunk)
|
|
334
|
+
if piece:
|
|
335
|
+
yield ("delta", piece)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
# Dict-form messages so we don't import langchain_core.messages here; the
|
|
339
|
+
# model converts them. The model's own tool-call message (``gathered``) is
|
|
340
|
+
# appended as returned.
|
|
341
|
+
messages: list[Any] = [
|
|
342
|
+
{"role": "system", "content": system_text},
|
|
343
|
+
{"role": "user", "content": convo},
|
|
344
|
+
]
|
|
345
|
+
rounds = 0
|
|
346
|
+
while True:
|
|
347
|
+
use_tools = rounds < _MAX_TOOL_ROUNDS
|
|
348
|
+
gathered = None
|
|
349
|
+
async for chunk in (model_tools if use_tools else model).astream(messages):
|
|
350
|
+
if use_tools:
|
|
351
|
+
try:
|
|
352
|
+
gathered = chunk if gathered is None else gathered + chunk
|
|
353
|
+
except (
|
|
354
|
+
Exception
|
|
355
|
+
):
|
|
356
|
+
gathered = None
|
|
357
|
+
piece = _content_text(chunk)
|
|
358
|
+
if piece:
|
|
359
|
+
yield ("delta", piece)
|
|
360
|
+
|
|
361
|
+
tool_calls = (
|
|
362
|
+
list(getattr(gathered, "tool_calls", None) or [])
|
|
363
|
+
if gathered is not None
|
|
364
|
+
else []
|
|
365
|
+
)
|
|
366
|
+
if not tool_calls:
|
|
367
|
+
return
|
|
368
|
+
|
|
369
|
+
messages.append(gathered)
|
|
370
|
+
for call in tool_calls:
|
|
371
|
+
args = call.get("args", {}) or {}
|
|
372
|
+
query = str(args.get("query", "")).strip()
|
|
373
|
+
yield ("tool", query)
|
|
374
|
+
result = await asyncio.to_thread(_run_council_tool, call.get("name", ""), args)
|
|
375
|
+
messages.append(
|
|
376
|
+
{"role": "tool", "content": result, "tool_call_id": call.get("id", "")}
|
|
377
|
+
)
|
|
378
|
+
rounds += 1
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
async def _cast_ballot(
|
|
382
|
+
voter: Persona,
|
|
383
|
+
answered: list[Persona],
|
|
384
|
+
answers: dict[str, str],
|
|
385
|
+
topic: str,
|
|
386
|
+
model: Any,
|
|
387
|
+
) -> tuple[Persona, str | None, str]:
|
|
388
|
+
"""One ballot. Never raises — a failed vote is simply an abstention."""
|
|
389
|
+
others = [p for p in answered if p.id != voter.id]
|
|
390
|
+
if not others:
|
|
391
|
+
return voter, None, ""
|
|
392
|
+
valid_names = {p.name for p in others}
|
|
393
|
+
|
|
394
|
+
ballot = f"The question was:\n\n{topic}\n\nThe answers:\n\n"
|
|
395
|
+
for p in others:
|
|
396
|
+
ballot += f"=== {p.name} ===\n{answers.get(p.id, '')}\n\n"
|
|
397
|
+
ballot += "Vote for the single best answer."
|
|
398
|
+
|
|
399
|
+
try:
|
|
400
|
+
resp = await model.ainvoke(
|
|
401
|
+
[("system", voter.system + _VOTE_GUIDE), ("human", ballot)]
|
|
402
|
+
)
|
|
403
|
+
return voter, *_parse_vote(_content_text(resp), valid_names)
|
|
404
|
+
except asyncio.CancelledError:
|
|
405
|
+
raise
|
|
406
|
+
except Exception: # noqa: BLE001 — a bad ballot must not kill the run
|
|
407
|
+
logger.debug("council vote by %s failed", voter.id, exc_info=True)
|
|
408
|
+
return voter, None, ""
|
|
409
|
+
|
|
410
|
+
# Ballots are independent, so run them concurrently: voting was 5 sequential
|
|
411
|
+
# model calls purely because it was written as a loop. Results are emitted in
|
|
412
|
+
# council order so the UI stays deterministic.
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
async def run_council(
|
|
416
|
+
topic: str,
|
|
417
|
+
model: Any,
|
|
418
|
+
personas: list[Persona] | None = None,
|
|
419
|
+
history: list[dict[str, Any]] | None = None,
|
|
420
|
+
*,
|
|
421
|
+
member_models: dict[str, Any] | None = None,
|
|
422
|
+
advice_only: bool = False,
|
|
423
|
+
) -> AsyncIterator[dict[str, Any]]:
|
|
424
|
+
"""Run a council debate over *topic* and yield event dicts.
|
|
425
|
+
|
|
426
|
+
*history* carries prior rounds of the same session so the advisors can build
|
|
427
|
+
on an earlier discussion (follow-up questions).
|
|
428
|
+
|
|
429
|
+
Flow: members answer concurrently and **independently** (no peer context),
|
|
430
|
+
then each casts a **single democratic vote** for the best answer (never its
|
|
431
|
+
own); the **majority** answer is the verdict.
|
|
432
|
+
|
|
433
|
+
Event ``type``s, in order:
|
|
434
|
+
``council_start`` -> for each persona ``agent_start`` / ``agent_delta``* /
|
|
435
|
+
``agent_done`` -> ``vote_start`` -> for each persona ``vote`` ->
|
|
436
|
+
``verdict`` -> ``done``.
|
|
437
|
+
"""
|
|
438
|
+
members = personas or PERSONAS
|
|
439
|
+
models = dict(member_models or {})
|
|
440
|
+
name_to_id = {p.name: p.id for p in members}
|
|
441
|
+
id_to_name = {p.id: p.name for p in members}
|
|
442
|
+
history_block = _format_history(history)
|
|
443
|
+
|
|
444
|
+
yield {
|
|
445
|
+
"type": "council_start",
|
|
446
|
+
"topic": topic,
|
|
447
|
+
"agents": [
|
|
448
|
+
{"id": p.id, "name": p.name, "avatar": p.avatar, "color": p.color}
|
|
449
|
+
for p in members
|
|
450
|
+
],
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
# Independent streams share only the output queue, never peer answers.
|
|
454
|
+
answers: dict[str, str] = {}
|
|
455
|
+
|
|
456
|
+
async def respond(persona: Persona) -> AsyncIterator[dict[str, Any]]:
|
|
457
|
+
yield {
|
|
458
|
+
"type": "agent_start",
|
|
459
|
+
"id": persona.id,
|
|
460
|
+
"name": persona.name,
|
|
461
|
+
"avatar": persona.avatar,
|
|
462
|
+
"color": persona.color,
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
convo = ""
|
|
466
|
+
if history_block:
|
|
467
|
+
convo += history_block + "\n\n"
|
|
468
|
+
convo += f"The question before the council:\n\n{topic}\n\n"
|
|
469
|
+
convo += "Give your own independent answer."
|
|
470
|
+
|
|
471
|
+
# One member failing must not end the council. A provider hiccup used to
|
|
472
|
+
# propagate out of run_council and kill the run mid-flight — the user
|
|
473
|
+
# lost the answers already on screen and got nothing. That member is
|
|
474
|
+
# dropped from the round instead; the rest still answer and vote.
|
|
475
|
+
full = ""
|
|
476
|
+
failed: str | None = None
|
|
477
|
+
try:
|
|
478
|
+
async for event in _stream_persona_turn(persona, convo, models.get(persona.id, model)):
|
|
479
|
+
if event[0] == "delta":
|
|
480
|
+
full += event[1]
|
|
481
|
+
yield {"type": "agent_delta", "id": persona.id, "text": event[1]}
|
|
482
|
+
else: # ("tool", query)
|
|
483
|
+
yield {"type": "agent_tool", "id": persona.id, "query": event[1]}
|
|
484
|
+
except asyncio.CancelledError:
|
|
485
|
+
raise # client disconnected / run cancelled — do not swallow
|
|
486
|
+
except Exception as exc: # noqa: BLE001 — one member must not end the run
|
|
487
|
+
failed = f"{type(exc).__name__}: {exc}"
|
|
488
|
+
logger.debug("council member %s failed", persona.id, exc_info=True)
|
|
489
|
+
|
|
490
|
+
text = full.strip()
|
|
491
|
+
if failed and not text:
|
|
492
|
+
yield {
|
|
493
|
+
"type": "agent_failed",
|
|
494
|
+
"id": persona.id,
|
|
495
|
+
"name": persona.name,
|
|
496
|
+
"message": failed,
|
|
497
|
+
}
|
|
498
|
+
return # Failed members are excluded from voting.
|
|
499
|
+
|
|
500
|
+
answers[persona.id] = text
|
|
501
|
+
yield {"type": "agent_done", "id": persona.id, "text": text}
|
|
502
|
+
|
|
503
|
+
events: asyncio.Queue = asyncio.Queue(maxsize=256)
|
|
504
|
+
|
|
505
|
+
async def collect(persona: Persona) -> None:
|
|
506
|
+
try:
|
|
507
|
+
async with asyncio.timeout(180):
|
|
508
|
+
async for event in respond(persona):
|
|
509
|
+
await events.put(event)
|
|
510
|
+
except Exception as exc:
|
|
511
|
+
answers.pop(persona.id, None)
|
|
512
|
+
await events.put({
|
|
513
|
+
"type": "agent_failed", "id": persona.id,
|
|
514
|
+
"name": persona.name, "message": str(exc),
|
|
515
|
+
})
|
|
516
|
+
finally:
|
|
517
|
+
if not asyncio.current_task().cancelling():
|
|
518
|
+
await events.put(None)
|
|
519
|
+
|
|
520
|
+
tasks = [asyncio.create_task(collect(persona)) for persona in members]
|
|
521
|
+
remaining = len(tasks)
|
|
522
|
+
try:
|
|
523
|
+
while remaining:
|
|
524
|
+
event = await events.get()
|
|
525
|
+
if event is None:
|
|
526
|
+
remaining -= 1
|
|
527
|
+
else:
|
|
528
|
+
yield event
|
|
529
|
+
finally:
|
|
530
|
+
for task in tasks:
|
|
531
|
+
task.cancel()
|
|
532
|
+
await asyncio.gather(*tasks, return_exceptions=True)
|
|
533
|
+
|
|
534
|
+
if advice_only:
|
|
535
|
+
yield {"type": "done"}
|
|
536
|
+
return
|
|
537
|
+
|
|
538
|
+
# --- Phase 2: democratic voting (one vote each) ----------------------
|
|
539
|
+
# Only members who actually produced an answer are on the ballot or hold a
|
|
540
|
+
# vote — a member that failed above must not be votable (nobody read an
|
|
541
|
+
# answer from it) and cannot cast one.
|
|
542
|
+
answered = [p for p in members if answers.get(p.id)]
|
|
543
|
+
if not answered:
|
|
544
|
+
yield {
|
|
545
|
+
"type": "council_error",
|
|
546
|
+
"message": "No advisor produced an answer — the model may be unavailable.",
|
|
547
|
+
}
|
|
548
|
+
yield {"type": "done"}
|
|
549
|
+
return
|
|
550
|
+
|
|
551
|
+
yield {"type": "vote_start"}
|
|
552
|
+
tally: dict[str, int] = {p.id: 0 for p in answered}
|
|
553
|
+
|
|
554
|
+
ballots = await asyncio.gather(
|
|
555
|
+
*(_cast_ballot(v, answered, answers, topic, models.get(v.id, model)) for v in answered)
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
for voter, choice_name, reason in ballots:
|
|
559
|
+
choice_id = name_to_id.get(choice_name) if choice_name else None
|
|
560
|
+
if choice_id is not None and choice_id in tally:
|
|
561
|
+
tally[choice_id] += 1
|
|
562
|
+
|
|
563
|
+
yield {
|
|
564
|
+
"type": "vote",
|
|
565
|
+
"voter": voter.id,
|
|
566
|
+
"voter_name": voter.name,
|
|
567
|
+
"choice": choice_id,
|
|
568
|
+
"choice_name": choice_name or "",
|
|
569
|
+
"reason": reason,
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
# --- Phase 3: majority verdict ---------------------------------------
|
|
573
|
+
# Winner = most votes, over the members who actually answered (a failed
|
|
574
|
+
# member is not in `tally`). Ties break by council order, so the outcome is
|
|
575
|
+
# deterministic rather than dict-order dependent. With no votes at all
|
|
576
|
+
# (every ballot abstained) the first answering member stands.
|
|
577
|
+
winner_id = (
|
|
578
|
+
max(answered, key=lambda p: (tally[p.id], -answered.index(p))).id
|
|
579
|
+
if any(tally.values())
|
|
580
|
+
else answered[0].id
|
|
581
|
+
)
|
|
582
|
+
top = tally[winner_id]
|
|
583
|
+
tied = [p.id for p in answered if tally[p.id] == top and top > 0]
|
|
584
|
+
yield {
|
|
585
|
+
"type": "verdict",
|
|
586
|
+
"winner_id": winner_id,
|
|
587
|
+
"winner_name": id_to_name.get(winner_id, "") if winner_id else "",
|
|
588
|
+
"tally": tally,
|
|
589
|
+
"votes": sum(tally.values()),
|
|
590
|
+
# Surfaced so the UI can say "tied, resolved by council order" rather
|
|
591
|
+
# than presenting a coin-flip as a clear majority.
|
|
592
|
+
"tied": len(tied) > 1,
|
|
593
|
+
"answer": answers.get(winner_id, "") if winner_id else "",
|
|
594
|
+
}
|
|
595
|
+
yield {"type": "done"}
|