omnimancer-cli 0.2.26__tar.gz → 0.3.1__tar.gz
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.
- {omnimancer_cli-0.2.26/omnimancer_cli.egg-info → omnimancer_cli-0.3.1}/PKG-INFO +1 -1
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/command_dispatch.py +4 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/commands.py +1 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/completion.py +1 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/display.py +10 -0
- omnimancer_cli-0.3.1/omnimancer/cli/h2l_command.py +508 -0
- omnimancer_cli-0.3.1/omnimancer/cli/h2l_headless.py +379 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/headless.py +295 -30
- omnimancer_cli-0.3.1/omnimancer/cli/headless_checkpoint.py +166 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/interface.py +120 -4
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/system_prompts.py +71 -34
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/turn_notify.py +7 -1
- omnimancer_cli-0.3.1/omnimancer/cli/usage.py +47 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/config_migration.py +1 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/engine.py +56 -4
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/models.py +11 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/rate_limit_fallback.py +14 -2
- omnimancer_cli-0.3.1/omnimancer/core/security/sandbox_manager.py +555 -0
- omnimancer_cli-0.3.1/omnimancer/h2l/__init__.py +6 -0
- omnimancer_cli-0.3.1/omnimancer/h2l/judge.py +245 -0
- omnimancer_cli-0.3.1/omnimancer/h2l/loop.py +263 -0
- omnimancer_cli-0.3.1/omnimancer/h2l/models.py +477 -0
- omnimancer_cli-0.3.1/omnimancer/h2l/orchestrator.py +527 -0
- omnimancer_cli-0.3.1/omnimancer/h2l/planner.py +380 -0
- omnimancer_cli-0.3.1/omnimancer/h2l/prompts.py +130 -0
- omnimancer_cli-0.3.1/omnimancer/h2l/refs.py +135 -0
- omnimancer_cli-0.3.1/omnimancer/h2l/render.py +68 -0
- omnimancer_cli-0.3.1/omnimancer/h2l/worker.py +273 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/azure.py +7 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/bedrock.py +43 -0
- omnimancer_cli-0.3.1/omnimancer/providers/cache_tokens.py +50 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/claude.py +71 -10
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/digitalocean.py +30 -1
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/gemini.py +9 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/mistral.py +7 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/openai.py +34 -2
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/openrouter.py +36 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/vertex.py +7 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/xai.py +7 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/app.py +2 -10
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1/omnimancer_cli.egg-info}/PKG-INFO +1 -1
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer_cli.egg-info/SOURCES.txt +16 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/pyproject.toml +1 -1
- omnimancer_cli-0.3.1/tests/test_claude_prompt_cache.py +158 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_openai_provider_tools.py +60 -0
- omnimancer_cli-0.3.1/tests/test_provider_cache_tokens.py +266 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_system_prompts.py +140 -23
- omnimancer_cli-0.2.26/omnimancer/cli/usage.py +0 -27
- omnimancer_cli-0.2.26/omnimancer/core/security/sandbox_manager.py +0 -381
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/LICENSE +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/README.md +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omn_fleet_hook.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/__main__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/agent_loop.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/approval_formatter.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/approval_integration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/approval_prompt.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/dynamic_commands.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/file_mentions.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/handlers/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/prompt_input.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/pt_completion.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/session.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/subagent.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/cli/tool_handler.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/approval_context.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/approval_interface.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/approval_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/config.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/file_system_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/program_executor.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/read_before_write_errors.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/status_core.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/status_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/tool_definitions.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/types.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/web_client.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent/workflow_orchestrator.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent_engine.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent_managers.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/agent_mode_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/chat_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/config_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/config_validator.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/conversation_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/env_loader.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/fallback_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/health_monitor.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/history_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/hooks.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/prompt_enhancer.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/provider_capabilities.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/provider_initializer.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/provider_registry.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/security/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/security/approval_workflow.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/security/audit_logger.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/security/permission_controller.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/security/permission_rules.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/security/security_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/core/signal_handler.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/events/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/events/emitter.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/events/jsonl_writer.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/events/schema.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/mcp/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/mcp/client.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/mcp/manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/base.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/claude_code.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/claude_credentials.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/cohere.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/factory.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/ollama.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/openai_compatible.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/providers/perplexity.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/tui/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/cli.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/models.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/sources.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/tui/fleet/widgets.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/ui/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/ui/cancellation_handler.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/ui/progress_indicator.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/ui/streaming_display.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/ui/turn_activity.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/utils/__init__.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/utils/error_handler.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/utils/errors.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer/utils/retry.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer_cli.egg-info/dependency_links.txt +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer_cli.egg-info/entry_points.txt +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer_cli.egg-info/requires.txt +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/omnimancer_cli.egg-info/top_level.txt +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/setup.cfg +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_approval_integration_compatibility.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_approval_preview_format.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_approval_remember.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_approval_system.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_azure_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_bedrock_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_claude_code_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_claude_credentials.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_claude_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_cohere_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_command_approval_security.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_config_comprehensive.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_config_migration_passthrough.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_conversation_history.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_conversation_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_core_engine.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_digitalocean_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_enhanced_model_info.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_env_loader.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_error_handling_robustness.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_execution_timeout_errors.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_fallback_manager.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_fixtures.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_gemini_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_hooks.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_httpx_compatibility.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_import_validation.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_interface_smoke_tests.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_mcp_integration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_mcp_integration_layer.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_mcp_management_commands.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_mistral_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_models.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_ollama_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_openai_compatible_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_openai_context_fit.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_openai_request_timeout.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_openai_streaming.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_openrouter_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_permission_rules.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_perplexity_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_program_executor.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_provider_aliases.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_provider_base.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_provider_capability_consistency.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_provider_discovery.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_provider_integration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_provider_registry.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_rate_limit_fallback.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_retry.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_security_validation.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_system_integration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_tool_calling_integration.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_tool_handler.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_vertex_provider.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_web_request_markers.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_workflows_comprehensive.py +0 -0
- {omnimancer_cli-0.2.26 → omnimancer_cli-0.3.1}/tests/test_xai_provider.py +0 -0
|
@@ -138,6 +138,10 @@ class CommandDispatchMixin:
|
|
|
138
138
|
await self._handle_subagents_command(command)
|
|
139
139
|
elif slash_cmd == SlashCommand.FALLBACK:
|
|
140
140
|
await self._handle_fallback_command(command)
|
|
141
|
+
elif slash_cmd == SlashCommand.H2L:
|
|
142
|
+
from .h2l_command import handle_h2l_command
|
|
143
|
+
|
|
144
|
+
await handle_h2l_command(self, command)
|
|
141
145
|
elif slash_cmd is not None:
|
|
142
146
|
self._show_info(f"Command {slash_cmd.value} is not yet implemented")
|
|
143
147
|
|
|
@@ -142,6 +142,7 @@ class CompletionManager:
|
|
|
142
142
|
"models": {0: ["refresh"]},
|
|
143
143
|
"prompts": {0: ["list"]},
|
|
144
144
|
"subagents": {0: ["list", "run"]},
|
|
145
|
+
"h2l": {0: ["plan", "run", "status", "config"], 1: ["set", "show"]},
|
|
145
146
|
"validate": {0: ["--fix", "--auto-fix"]},
|
|
146
147
|
"health": {0: ["--monitor", "--interval"]},
|
|
147
148
|
"repair": {0: ["--backup", "--force"]},
|
|
@@ -252,6 +252,16 @@ Tips:
|
|
|
252
252
|
" /subagents list configured subagents\n"
|
|
253
253
|
" /subagents run <name> <task> run a subagent on a task"
|
|
254
254
|
),
|
|
255
|
+
"h2l": (
|
|
256
|
+
"High-to-low: the high model plans and judges, low models "
|
|
257
|
+
"execute stories.\n\n"
|
|
258
|
+
"Usage:\n"
|
|
259
|
+
" /h2l <goal> plan, review the stories, then run\n"
|
|
260
|
+
" /h2l plan <goal> plan only (kept pending)\n"
|
|
261
|
+
" /h2l run run the pending plan\n"
|
|
262
|
+
" /h2l status show the pending plan\n"
|
|
263
|
+
" /h2l config [set <field> <value> ...]"
|
|
264
|
+
),
|
|
255
265
|
"validate": (
|
|
256
266
|
"Validate provider configurations.\n\n"
|
|
257
267
|
"Usage: /validate [provider] [--fix]"
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
"""The ``/h2l`` slash command and its TUI presenter (PRD §7).
|
|
2
|
+
|
|
3
|
+
Kept out of ``command_dispatch.py`` (2.4k lines) on purpose; the dispatcher
|
|
4
|
+
delegates here. The presenter talks to the user through the host CLI's
|
|
5
|
+
console and an injectable ``_h2l_input`` coroutine so tests never touch stdin.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import asyncio
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import shlex
|
|
14
|
+
import subprocess
|
|
15
|
+
import tempfile
|
|
16
|
+
import uuid
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
from rich.panel import Panel
|
|
21
|
+
from rich.table import Table
|
|
22
|
+
|
|
23
|
+
from ..h2l.models import (
|
|
24
|
+
H2LConfig,
|
|
25
|
+
H2LModelRef,
|
|
26
|
+
Plan,
|
|
27
|
+
RunSummary,
|
|
28
|
+
Story,
|
|
29
|
+
StoryOutcome,
|
|
30
|
+
StoryState,
|
|
31
|
+
Verdict,
|
|
32
|
+
WorkerReport,
|
|
33
|
+
)
|
|
34
|
+
from ..h2l.orchestrator import H2LRun, PlanDecision
|
|
35
|
+
from ..h2l.refs import H2LConfigError, validate_tiers
|
|
36
|
+
from ..h2l.render import render_summary_text
|
|
37
|
+
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
USAGE = (
|
|
41
|
+
"Usage:\n"
|
|
42
|
+
" /h2l <goal> plan, review the stories, then run them\n"
|
|
43
|
+
" /h2l plan <goal> plan only; keeps the plan pending\n"
|
|
44
|
+
" /h2l run run the pending plan\n"
|
|
45
|
+
" /h2l status show the pending plan\n"
|
|
46
|
+
" /h2l config show the effective h2l block\n"
|
|
47
|
+
" /h2l config set <field> <value> [<field> <value> ...]"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
_SETTABLE = {
|
|
51
|
+
"enabled",
|
|
52
|
+
"high",
|
|
53
|
+
"low",
|
|
54
|
+
"max_parallel",
|
|
55
|
+
"max_retries",
|
|
56
|
+
"pass_threshold",
|
|
57
|
+
"worker_tools",
|
|
58
|
+
"allow_worker_read",
|
|
59
|
+
"escalation",
|
|
60
|
+
"plan_approval",
|
|
61
|
+
"worker_max_iterations",
|
|
62
|
+
"planner_max_iterations",
|
|
63
|
+
"high_max_tokens",
|
|
64
|
+
"low_max_tokens",
|
|
65
|
+
}
|
|
66
|
+
_LIST_FIELDS = {"low", "worker_tools"}
|
|
67
|
+
_INT_FIELDS = {
|
|
68
|
+
"max_parallel",
|
|
69
|
+
"max_retries",
|
|
70
|
+
"pass_threshold",
|
|
71
|
+
"worker_max_iterations",
|
|
72
|
+
"planner_max_iterations",
|
|
73
|
+
"high_max_tokens",
|
|
74
|
+
"low_max_tokens",
|
|
75
|
+
}
|
|
76
|
+
# Optional integers: "off"/"none" clears them.
|
|
77
|
+
_OPTIONAL_INT_FIELDS = {"planner_max_iterations", "high_max_tokens", "low_max_tokens"}
|
|
78
|
+
_BOOL_FIELDS = {"enabled", "allow_worker_read", "plan_approval"}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------- presenter
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class TUIPresenter:
|
|
85
|
+
def __init__(self, cli: Any) -> None:
|
|
86
|
+
self.cli = cli
|
|
87
|
+
self.console = cli.console
|
|
88
|
+
self.summary: Optional[RunSummary] = None
|
|
89
|
+
|
|
90
|
+
async def _ask(self, prompt: str) -> str:
|
|
91
|
+
asker = getattr(self.cli, "_h2l_input", None)
|
|
92
|
+
if asker is not None:
|
|
93
|
+
answer: str = await asker(prompt)
|
|
94
|
+
return answer
|
|
95
|
+
return await asyncio.to_thread(input, prompt)
|
|
96
|
+
|
|
97
|
+
async def _edit_text(self, text: str) -> str:
|
|
98
|
+
"""Open ``$EDITOR`` on the text, or fall back to a multi-line prompt."""
|
|
99
|
+
editor = os.environ.get("EDITOR")
|
|
100
|
+
if editor:
|
|
101
|
+
with tempfile.NamedTemporaryFile(
|
|
102
|
+
"w", suffix=".md", delete=False, encoding="utf-8"
|
|
103
|
+
) as handle:
|
|
104
|
+
handle.write(text)
|
|
105
|
+
path = handle.name
|
|
106
|
+
try:
|
|
107
|
+
await asyncio.to_thread(
|
|
108
|
+
subprocess.run, [*shlex.split(editor), path], check=False
|
|
109
|
+
)
|
|
110
|
+
return Path(path).read_text(encoding="utf-8")
|
|
111
|
+
finally:
|
|
112
|
+
try:
|
|
113
|
+
os.unlink(path)
|
|
114
|
+
except OSError:
|
|
115
|
+
pass
|
|
116
|
+
self.console.print(
|
|
117
|
+
"Enter the new instructions; finish with a line containing only '.'"
|
|
118
|
+
)
|
|
119
|
+
lines: List[str] = []
|
|
120
|
+
while True:
|
|
121
|
+
line = await self._ask("")
|
|
122
|
+
if line.strip() == ".":
|
|
123
|
+
break
|
|
124
|
+
lines.append(line)
|
|
125
|
+
return "\n".join(lines) or text
|
|
126
|
+
|
|
127
|
+
def _plan_table(self, plan: Plan) -> Table:
|
|
128
|
+
table = Table(title="H2L plan")
|
|
129
|
+
table.add_column("ID", style="cyan")
|
|
130
|
+
table.add_column("Title")
|
|
131
|
+
table.add_column("Files", style="dim")
|
|
132
|
+
table.add_column("Depends", style="magenta")
|
|
133
|
+
table.add_column("Verify", style="dim")
|
|
134
|
+
for story in plan.stories:
|
|
135
|
+
table.add_row(
|
|
136
|
+
story.id,
|
|
137
|
+
story.title,
|
|
138
|
+
"\n".join(story.files),
|
|
139
|
+
", ".join(story.depends_on) or "-",
|
|
140
|
+
story.verify or "-",
|
|
141
|
+
)
|
|
142
|
+
return table
|
|
143
|
+
|
|
144
|
+
async def on_plan(self, plan: Plan, model_used: str = "") -> None:
|
|
145
|
+
if plan.design:
|
|
146
|
+
title = f"Design (planned by {model_used})" if model_used else "Design"
|
|
147
|
+
self.console.print(Panel(plan.design, title=title))
|
|
148
|
+
self.console.print(self._plan_table(plan))
|
|
149
|
+
|
|
150
|
+
async def approve_plan(self, plan: Plan) -> PlanDecision:
|
|
151
|
+
stories = list(plan.stories)
|
|
152
|
+
while True:
|
|
153
|
+
answer = (
|
|
154
|
+
await self._ask("[a]ccept [d]rop <id> [e]dit <id> [c]ancel > ")
|
|
155
|
+
).strip()
|
|
156
|
+
parts = answer.split(None, 1)
|
|
157
|
+
verb = parts[0].lower() if parts else ""
|
|
158
|
+
arg = parts[1].strip() if len(parts) > 1 else ""
|
|
159
|
+
if verb in ("a", "accept"):
|
|
160
|
+
if not stories:
|
|
161
|
+
self.cli._show_error("Nothing left to run.")
|
|
162
|
+
return PlanDecision(accepted=False)
|
|
163
|
+
return PlanDecision(
|
|
164
|
+
accepted=True, plan=plan.model_copy(update={"stories": stories})
|
|
165
|
+
)
|
|
166
|
+
if verb in ("c", "cancel"):
|
|
167
|
+
return PlanDecision(accepted=False)
|
|
168
|
+
if verb in ("d", "drop", "e", "edit"):
|
|
169
|
+
target = next((s for s in stories if s.id == arg), None)
|
|
170
|
+
if target is None:
|
|
171
|
+
self.cli._show_error(f"No story '{arg}' in the plan.")
|
|
172
|
+
continue
|
|
173
|
+
if verb in ("d", "drop"):
|
|
174
|
+
stories = [s for s in stories if s.id != arg]
|
|
175
|
+
for s in stories:
|
|
176
|
+
s.depends_on = [d for d in s.depends_on if d != arg]
|
|
177
|
+
self.cli._show_info(f"Dropped {arg}.")
|
|
178
|
+
else:
|
|
179
|
+
new_text = await self._edit_text(target.instructions)
|
|
180
|
+
stories = [
|
|
181
|
+
(
|
|
182
|
+
s.model_copy(update={"instructions": new_text})
|
|
183
|
+
if s.id == arg
|
|
184
|
+
else s
|
|
185
|
+
)
|
|
186
|
+
for s in stories
|
|
187
|
+
]
|
|
188
|
+
self.cli._show_info(f"Updated {arg}.")
|
|
189
|
+
self.console.print(
|
|
190
|
+
self._plan_table(plan.model_copy(update={"stories": stories}))
|
|
191
|
+
)
|
|
192
|
+
continue
|
|
193
|
+
self.cli._show_error("Answer a, d <id>, e <id>, or c.")
|
|
194
|
+
|
|
195
|
+
async def on_story_start(
|
|
196
|
+
self, story: Story, ref: H2LModelRef, attempt: int, agent_id: str = ""
|
|
197
|
+
) -> None:
|
|
198
|
+
self.console.print(
|
|
199
|
+
f"[cyan]{story.id}[/cyan] attempt {attempt} on [dim]{ref.label}[/dim]: "
|
|
200
|
+
f"{story.title}"
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
async def on_tool_call(
|
|
204
|
+
self, story: Story, tool_call: Any, result: Any, agent_id: str = ""
|
|
205
|
+
) -> None:
|
|
206
|
+
status = "[red]error[/red]" if getattr(result, "error", None) else "ok"
|
|
207
|
+
self.console.print(f" [dim]{story.id} › {tool_call.name} {status}[/dim]")
|
|
208
|
+
|
|
209
|
+
async def on_wait(self, label: str, seconds: float) -> None:
|
|
210
|
+
self.console.print(f" [dim]waiting on {label} ({int(seconds)}s)[/dim]")
|
|
211
|
+
|
|
212
|
+
async def on_planner_tool_call(self, tool_call: Any, result: Any) -> None:
|
|
213
|
+
arguments = tool_call.arguments if isinstance(tool_call.arguments, dict) else {}
|
|
214
|
+
if tool_call.name == "submit_plan":
|
|
215
|
+
stories = arguments.get("stories")
|
|
216
|
+
count = len(stories) if isinstance(stories, list) else 0
|
|
217
|
+
target = f"{count} stories"
|
|
218
|
+
else:
|
|
219
|
+
target = (
|
|
220
|
+
arguments.get("file_path")
|
|
221
|
+
or arguments.get("path")
|
|
222
|
+
or arguments.get("pattern")
|
|
223
|
+
or arguments.get("command")
|
|
224
|
+
or ""
|
|
225
|
+
)
|
|
226
|
+
error = getattr(result, "error", None)
|
|
227
|
+
word = "rejected" if tool_call.name == "submit_plan" else "error"
|
|
228
|
+
status = f"[red]{word}: {str(error)[:300]}[/red]" if error else ""
|
|
229
|
+
self.console.print(
|
|
230
|
+
f" [dim]planner › {tool_call.name} {target}[/dim] {status}".rstrip()
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
async def on_report(self, story: Story, report: WorkerReport) -> None:
|
|
234
|
+
verify = (
|
|
235
|
+
f"verify exit {report.verify_exit}"
|
|
236
|
+
if report.verify_exit is not None
|
|
237
|
+
else ""
|
|
238
|
+
)
|
|
239
|
+
files = ", ".join(report.files_changed) or "no files changed"
|
|
240
|
+
self.console.print(f" [dim]{story.id}: {files} {verify}[/dim]")
|
|
241
|
+
|
|
242
|
+
async def on_verdict(self, story: Story, verdict: Verdict) -> None:
|
|
243
|
+
if verdict.passed:
|
|
244
|
+
self.console.print(
|
|
245
|
+
f" [green]{story.id} passed[/green] (judge {verdict.score})"
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
failures = "; ".join(verdict.failures)
|
|
249
|
+
self.console.print(
|
|
250
|
+
f" [yellow]{story.id} failed[/yellow] (judge {verdict.score}, "
|
|
251
|
+
f"{verdict.judge}): {failures}"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
async def on_escalation(self, story: Story, outcome: StoryOutcome) -> str:
|
|
255
|
+
while True:
|
|
256
|
+
answer = (
|
|
257
|
+
(
|
|
258
|
+
await self._ask(
|
|
259
|
+
f"{story.id} failed {outcome.attempts} attempts. "
|
|
260
|
+
"[h]igh model does it [b]lock [r]etry once more > "
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
.strip()
|
|
264
|
+
.lower()
|
|
265
|
+
)
|
|
266
|
+
if answer in ("h", "high"):
|
|
267
|
+
return "high"
|
|
268
|
+
if answer in ("b", "block"):
|
|
269
|
+
return "block"
|
|
270
|
+
if answer in ("r", "retry"):
|
|
271
|
+
return "retry"
|
|
272
|
+
self.cli._show_error("Answer h, b, or r.")
|
|
273
|
+
|
|
274
|
+
async def on_done(self, summary: RunSummary) -> None:
|
|
275
|
+
self.summary = summary
|
|
276
|
+
table = Table(title="H2L summary")
|
|
277
|
+
table.add_column("ID", style="cyan")
|
|
278
|
+
table.add_column("State")
|
|
279
|
+
table.add_column("Attempts", justify="right")
|
|
280
|
+
table.add_column("Model", style="dim")
|
|
281
|
+
table.add_column("Score", justify="right")
|
|
282
|
+
table.add_column("Reason", style="dim")
|
|
283
|
+
styles = {
|
|
284
|
+
StoryState.PASSED: "green",
|
|
285
|
+
StoryState.BLOCKED: "red",
|
|
286
|
+
StoryState.SKIPPED: "yellow",
|
|
287
|
+
StoryState.CANCELLED: "yellow",
|
|
288
|
+
}
|
|
289
|
+
for outcome in summary.stories:
|
|
290
|
+
style = styles.get(outcome.state, "")
|
|
291
|
+
state = (
|
|
292
|
+
f"[{style}]{outcome.state.value}[/{style}]"
|
|
293
|
+
if style
|
|
294
|
+
else outcome.state.value
|
|
295
|
+
)
|
|
296
|
+
table.add_row(
|
|
297
|
+
outcome.id,
|
|
298
|
+
state,
|
|
299
|
+
str(outcome.attempts),
|
|
300
|
+
outcome.model or "-",
|
|
301
|
+
str(outcome.score) if outcome.score is not None else "-",
|
|
302
|
+
outcome.reason or "",
|
|
303
|
+
)
|
|
304
|
+
self.console.print(table)
|
|
305
|
+
high = summary.usage_by_tier.get("high")
|
|
306
|
+
low = summary.usage_by_tier.get("low")
|
|
307
|
+
if high is not None and low is not None:
|
|
308
|
+
self.console.print(
|
|
309
|
+
f"[dim]Tokens high {high.input_tokens} in / {high.output_tokens} out "
|
|
310
|
+
f"(${high.cost_usd:.2f}) low {low.input_tokens} in / "
|
|
311
|
+
f"{low.output_tokens} out (${low.cost_usd:.2f})[/dim]"
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ---------------------------------------------------------------- command
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _session_id(cli: Any) -> str:
|
|
319
|
+
notifier = getattr(cli, "turn_notifier", None)
|
|
320
|
+
session = getattr(notifier, "session_id", None)
|
|
321
|
+
return str(session) if session else uuid.uuid4().hex
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _h2l_config(cli: Any) -> Optional[H2LConfig]:
|
|
325
|
+
config = cli.engine.config_manager.get_config()
|
|
326
|
+
block: Optional[H2LConfig] = getattr(config, "h2l", None)
|
|
327
|
+
if block is None or not block.enabled:
|
|
328
|
+
cli._show_error(
|
|
329
|
+
"H2L is not configured. Add an 'h2l' block to config or run: "
|
|
330
|
+
"/h2l config set high <provider:model> low <provider:model[,...]>"
|
|
331
|
+
)
|
|
332
|
+
return None
|
|
333
|
+
return block
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _build_run(
|
|
337
|
+
cli: Any, config: H2LConfig, presenter: TUIPresenter
|
|
338
|
+
) -> Optional[H2LRun]:
|
|
339
|
+
try:
|
|
340
|
+
tiers = validate_tiers(config, cli.engine.config_manager, cli.engine)
|
|
341
|
+
except H2LConfigError as exc:
|
|
342
|
+
cli._show_error(str(exc))
|
|
343
|
+
return None
|
|
344
|
+
return H2LRun(
|
|
345
|
+
cli.engine,
|
|
346
|
+
config,
|
|
347
|
+
presenter,
|
|
348
|
+
cwd=Path.cwd(),
|
|
349
|
+
session_id=_session_id(cli),
|
|
350
|
+
tiers=tiers,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _post_summary(cli: Any, summary: RunSummary) -> None:
|
|
355
|
+
"""One assistant message in the parent conversation, never the transcripts."""
|
|
356
|
+
text = "H2L run summary:\n" + render_summary_text(summary)
|
|
357
|
+
try:
|
|
358
|
+
_, model = cli.engine.runtime_identity()
|
|
359
|
+
except Exception:
|
|
360
|
+
model = ""
|
|
361
|
+
chat = getattr(cli.engine, "chat_manager", None)
|
|
362
|
+
if chat is not None:
|
|
363
|
+
chat.add_assistant_message(text, model or "h2l")
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
async def handle_h2l_command(cli: Any, command: Any) -> None:
|
|
367
|
+
args: List[str] = list(command.args or [])
|
|
368
|
+
if not args:
|
|
369
|
+
cli._show_info(USAGE)
|
|
370
|
+
return
|
|
371
|
+
sub = args[0].lower()
|
|
372
|
+
|
|
373
|
+
if sub == "config":
|
|
374
|
+
await _handle_config(cli, args[1:])
|
|
375
|
+
return
|
|
376
|
+
if sub == "status":
|
|
377
|
+
pending = getattr(cli, "_h2l_pending_plan", None)
|
|
378
|
+
if pending is None:
|
|
379
|
+
cli._show_info("No pending plan. Use '/h2l plan <goal>' to create one.")
|
|
380
|
+
else:
|
|
381
|
+
await TUIPresenter(cli).on_plan(pending)
|
|
382
|
+
cli._show_info("Pending plan ready: '/h2l run' executes it.")
|
|
383
|
+
return
|
|
384
|
+
|
|
385
|
+
config = _h2l_config(cli)
|
|
386
|
+
if config is None:
|
|
387
|
+
return
|
|
388
|
+
presenter = TUIPresenter(cli)
|
|
389
|
+
|
|
390
|
+
if sub == "plan":
|
|
391
|
+
goal = " ".join(args[1:]).strip()
|
|
392
|
+
if not goal:
|
|
393
|
+
cli._show_error("Usage: /h2l plan <goal>")
|
|
394
|
+
return
|
|
395
|
+
run = _build_run(cli, config, presenter)
|
|
396
|
+
if run is None:
|
|
397
|
+
return
|
|
398
|
+
cli._show_info(f"Planning with {config.high.label}…")
|
|
399
|
+
result = await run.plan_goal(goal)
|
|
400
|
+
if result.plan is None:
|
|
401
|
+
cli._show_error(result.error or "H2L: planning failed")
|
|
402
|
+
return
|
|
403
|
+
cli._h2l_pending_plan = result.plan
|
|
404
|
+
await presenter.on_plan(result.plan)
|
|
405
|
+
cli._show_info("Plan pending: '/h2l run' executes it, '/h2l status' shows it.")
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
if sub == "run":
|
|
409
|
+
pending = getattr(cli, "_h2l_pending_plan", None)
|
|
410
|
+
if pending is None:
|
|
411
|
+
cli._show_error("No pending plan. Use '/h2l plan <goal>' first.")
|
|
412
|
+
return
|
|
413
|
+
run = _build_run(cli, config, presenter)
|
|
414
|
+
if run is None:
|
|
415
|
+
return
|
|
416
|
+
summary = await run.run_plan(pending)
|
|
417
|
+
cli._h2l_pending_plan = None
|
|
418
|
+
_post_summary(cli, summary)
|
|
419
|
+
return
|
|
420
|
+
|
|
421
|
+
goal = " ".join(args).strip()
|
|
422
|
+
run = _build_run(cli, config, presenter)
|
|
423
|
+
if run is None:
|
|
424
|
+
return
|
|
425
|
+
cli._show_info(f"Planning with {config.high.label}…")
|
|
426
|
+
summary = await run.run(goal)
|
|
427
|
+
if summary.stop_cause == "plan_failed":
|
|
428
|
+
cli._show_error(summary.error or "H2L: planning failed")
|
|
429
|
+
return
|
|
430
|
+
if summary.stop_cause == "cancelled":
|
|
431
|
+
cli._show_info("H2L run cancelled.")
|
|
432
|
+
return
|
|
433
|
+
_post_summary(cli, summary)
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
async def _handle_config(cli: Any, args: List[str]) -> None:
|
|
437
|
+
config = cli.engine.config_manager.get_config()
|
|
438
|
+
block = getattr(config, "h2l", None)
|
|
439
|
+
if not args or args[0].lower() == "show":
|
|
440
|
+
if block is None:
|
|
441
|
+
cli._show_info(
|
|
442
|
+
"No h2l block configured. Set both tiers: "
|
|
443
|
+
"/h2l config set high <ref> low <ref[,ref]>"
|
|
444
|
+
)
|
|
445
|
+
return
|
|
446
|
+
table = Table(title="h2l config")
|
|
447
|
+
table.add_column("Field", style="cyan")
|
|
448
|
+
table.add_column("Value")
|
|
449
|
+
for key, current in block.model_dump().items():
|
|
450
|
+
shown: str
|
|
451
|
+
if key == "high":
|
|
452
|
+
shown = block.high.label
|
|
453
|
+
elif key == "low":
|
|
454
|
+
shown = ", ".join(r.label for r in block.low)
|
|
455
|
+
elif isinstance(current, list):
|
|
456
|
+
shown = ", ".join(str(v) for v in current)
|
|
457
|
+
else:
|
|
458
|
+
shown = str(current)
|
|
459
|
+
table.add_row(key, shown)
|
|
460
|
+
cli.console.print(table)
|
|
461
|
+
return
|
|
462
|
+
|
|
463
|
+
if args[0].lower() != "set" or len(args) < 3 or (len(args) - 1) % 2 != 0:
|
|
464
|
+
cli._show_error("Usage: /h2l config set <field> <value> [<field> <value> ...]")
|
|
465
|
+
return
|
|
466
|
+
|
|
467
|
+
data: Dict[str, Any] = block.model_dump() if block is not None else {}
|
|
468
|
+
pairs = list(zip(args[1::2], args[2::2]))
|
|
469
|
+
for key, raw in pairs:
|
|
470
|
+
key = key.lower()
|
|
471
|
+
if key not in _SETTABLE:
|
|
472
|
+
cli._show_error(
|
|
473
|
+
f"Unknown h2l field '{key}'. Fields: {', '.join(sorted(_SETTABLE))}"
|
|
474
|
+
)
|
|
475
|
+
return
|
|
476
|
+
value: Any = raw
|
|
477
|
+
if key in _LIST_FIELDS:
|
|
478
|
+
value = [item.strip() for item in raw.split(",") if item.strip()]
|
|
479
|
+
elif key in _OPTIONAL_INT_FIELDS and raw.lower() in ("off", "none"):
|
|
480
|
+
value = None # unbounded budget / provider's own output cap
|
|
481
|
+
elif key in _INT_FIELDS:
|
|
482
|
+
try:
|
|
483
|
+
value = int(raw)
|
|
484
|
+
except ValueError:
|
|
485
|
+
cli._show_error(f"{key} must be an integer")
|
|
486
|
+
return
|
|
487
|
+
elif key in _BOOL_FIELDS:
|
|
488
|
+
value = raw.lower() in ("1", "true", "yes", "on")
|
|
489
|
+
data[key] = value
|
|
490
|
+
|
|
491
|
+
if block is None and not (data.get("high") and data.get("low")):
|
|
492
|
+
cli._show_error(
|
|
493
|
+
"No h2l block yet: set both tiers at once, e.g. "
|
|
494
|
+
"/h2l config set high claude:claude-opus-5 low gateway:qwen3-8b"
|
|
495
|
+
)
|
|
496
|
+
return
|
|
497
|
+
try:
|
|
498
|
+
new_block = H2LConfig(**data)
|
|
499
|
+
except ValueError as exc:
|
|
500
|
+
cli._show_error(f"Invalid h2l config: {exc}")
|
|
501
|
+
return
|
|
502
|
+
config.h2l = new_block
|
|
503
|
+
try:
|
|
504
|
+
cli.engine.config_manager.save_config(config)
|
|
505
|
+
except Exception as exc:
|
|
506
|
+
cli._show_error(f"Could not save config: {exc}")
|
|
507
|
+
return
|
|
508
|
+
cli._show_success("h2l config updated: " + ", ".join(f"{k}={v}" for k, v in pairs))
|