alysis-code 0.13.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.
- alysis_code/__init__.py +3 -0
- alysis_code/__main__.py +4 -0
- alysis_code/_build_info.py +14 -0
- alysis_code/account_login.py +468 -0
- alysis_code/agent/README.md +35 -0
- alysis_code/agent/__init__.py +11 -0
- alysis_code/agent/acceptance_contract.py +2217 -0
- alysis_code/agent/blast_radius.py +1403 -0
- alysis_code/agent/cache_keepalive.py +227 -0
- alysis_code/agent/completion_certificate.py +366 -0
- alysis_code/agent/completion_gate.py +306 -0
- alysis_code/agent/empty_response_stall.py +403 -0
- alysis_code/agent/errors.py +28 -0
- alysis_code/agent/llm_calls.py +475 -0
- alysis_code/agent/mutation_classification.py +227 -0
- alysis_code/agent/prompt_context.py +2508 -0
- alysis_code/agent/read_ledger.py +253 -0
- alysis_code/agent/regression_baseline.py +642 -0
- alysis_code/agent/reproduction_first.py +610 -0
- alysis_code/agent/sensitive_output.py +629 -0
- alysis_code/agent/session.py +3218 -0
- alysis_code/agent/steering.py +191 -0
- alysis_code/agent/subagent_execution.py +5177 -0
- alysis_code/agent/subagent_workspace.py +666 -0
- alysis_code/agent/tools_assembly.py +4728 -0
- alysis_code/agent/turn/__init__.py +101 -0
- alysis_code/agent/turn/core.py +8483 -0
- alysis_code/agent/turn/events.py +113 -0
- alysis_code/agent/turn/exploration.py +590 -0
- alysis_code/agent/turn/interventions.py +65 -0
- alysis_code/agent/turn/read_cache.py +420 -0
- alysis_code/agent/turn/snapshot.py +179 -0
- alysis_code/agent/turn_contract.py +661 -0
- alysis_code/agent/turn_path.py +129 -0
- alysis_code/agent/verification.py +2885 -0
- alysis_code/agent/verification_commands.py +512 -0
- alysis_code/agent/verification_evidence.py +738 -0
- alysis_code/agent_loop.py +693 -0
- alysis_code/agent_runtimes/__init__.py +51 -0
- alysis_code/agent_runtimes/base.py +114 -0
- alysis_code/agent_runtimes/builtins.py +129 -0
- alysis_code/agent_runtimes/codex_cli.py +664 -0
- alysis_code/agent_runtimes/host.py +263 -0
- alysis_code/agent_runtimes/registry.py +64 -0
- alysis_code/agent_runtimes/service.py +150 -0
- alysis_code/agentbox_client.py +416 -0
- alysis_code/agentbox_integration.py +310 -0
- alysis_code/alysis_cloud.py +152 -0
- alysis_code/approval_scope.py +276 -0
- alysis_code/assets/README.md +33 -0
- alysis_code/assets/__init__.py +126 -0
- alysis_code/assets/asset_read_core.py +281 -0
- alysis_code/assets/budget_allocator.py +456 -0
- alysis_code/assets/comprehender.py +759 -0
- alysis_code/assets/index.py +654 -0
- alysis_code/assets/ingestion.py +275 -0
- alysis_code/assets/legacy_migration.py +413 -0
- alysis_code/assets/models.py +263 -0
- alysis_code/assets/ocr.py +239 -0
- alysis_code/assets/owl/ascii/f-000.txt +13 -0
- alysis_code/assets/owl/ascii/f-001.txt +13 -0
- alysis_code/assets/owl/ascii/f-002.txt +13 -0
- alysis_code/assets/owl/ascii/f-003.txt +13 -0
- alysis_code/assets/owl/ascii/f-004.txt +13 -0
- alysis_code/assets/owl/ascii/f-005.txt +13 -0
- alysis_code/assets/owl/ascii/f-006.txt +13 -0
- alysis_code/assets/owl/ascii/f-007.txt +13 -0
- alysis_code/assets/owl/ascii/f-008.txt +13 -0
- alysis_code/assets/owl/ascii/f-009.txt +13 -0
- alysis_code/assets/owl/ascii/f-010.txt +13 -0
- alysis_code/assets/owl/ascii/f-011.txt +13 -0
- alysis_code/assets/owl/ascii/f-012.txt +13 -0
- alysis_code/assets/owl/ascii/f-013.txt +13 -0
- alysis_code/assets/owl/ascii/f-014.txt +13 -0
- alysis_code/assets/owl/ascii/f-015.txt +13 -0
- alysis_code/assets/owl/ascii/f-016.txt +13 -0
- alysis_code/assets/owl/ascii/f-017.txt +13 -0
- alysis_code/assets/owl/ascii/f-018.txt +13 -0
- alysis_code/assets/owl/ascii/f-019.txt +13 -0
- alysis_code/assets/owl/ascii/f-020.txt +13 -0
- alysis_code/assets/owl/index.html +98 -0
- alysis_code/assets/owl/show-owl.sh +761 -0
- alysis_code/assets/paths.py +49 -0
- alysis_code/assets/plan_binding.py +326 -0
- alysis_code/assets/planner_context.py +466 -0
- alysis_code/assets/planner_tools.py +184 -0
- alysis_code/assets/prompts.py +101 -0
- alysis_code/assets/replanner_context.py +239 -0
- alysis_code/assets/surface.py +521 -0
- alysis_code/assets/untrusted_content.py +48 -0
- alysis_code/assets/usage_logger.py +94 -0
- alysis_code/assets/worker_mirror.py +428 -0
- alysis_code/assets/worker_section.py +303 -0
- alysis_code/assets/worker_tools.py +468 -0
- alysis_code/atomic_io.py +83 -0
- alysis_code/auth_diagnostics.py +272 -0
- alysis_code/background_runner.py +366 -0
- alysis_code/branding.py +270 -0
- alysis_code/budget_policy.py +390 -0
- alysis_code/build_identity.py +465 -0
- alysis_code/builtin_hooks/__init__.py +7 -0
- alysis_code/builtin_hooks/notify_done_windows.py +65 -0
- alysis_code/bwrap_etc.py +76 -0
- alysis_code/cancellation.py +41 -0
- alysis_code/capabilities.py +137 -0
- alysis_code/chatgpt_codex_static_provider.py +133 -0
- alysis_code/cli.py +51 -0
- alysis_code/cli_impl/__init__.py +1 -0
- alysis_code/cli_impl/assets_cli.py +537 -0
- alysis_code/cli_impl/assets_modal.py +412 -0
- alysis_code/cli_impl/chat/__init__.py +156 -0
- alysis_code/cli_impl/chat/commands.py +2616 -0
- alysis_code/cli_impl/chat/loop.py +4508 -0
- alysis_code/cli_impl/chat/mid_turn_policy.py +125 -0
- alysis_code/cli_impl/chat/rendering.py +444 -0
- alysis_code/cli_impl/chat/state.py +124 -0
- alysis_code/cli_impl/chat_resume.py +830 -0
- alysis_code/cli_impl/chat_slash_completer.py +258 -0
- alysis_code/cli_impl/commands/__init__.py +11 -0
- alysis_code/cli_impl/commands/_shared.py +89 -0
- alysis_code/cli_impl/commands/auth.py +623 -0
- alysis_code/cli_impl/commands/chat_resume_helpers.py +1531 -0
- alysis_code/cli_impl/commands/chat_state.py +158 -0
- alysis_code/cli_impl/commands/chat_status.py +1248 -0
- alysis_code/cli_impl/commands/chat_terminal.py +942 -0
- alysis_code/cli_impl/commands/chat_tui_panels.py +1018 -0
- alysis_code/cli_impl/commands/cli_common.py +1223 -0
- alysis_code/cli_impl/commands/cli_surface.py +77 -0
- alysis_code/cli_impl/commands/config.py +131 -0
- alysis_code/cli_impl/commands/conventions.py +85 -0
- alysis_code/cli_impl/commands/execution_helpers.py +350 -0
- alysis_code/cli_impl/commands/extensions.py +401 -0
- alysis_code/cli_impl/commands/forge.py +1282 -0
- alysis_code/cli_impl/commands/forge_asset_view.py +121 -0
- alysis_code/cli_impl/commands/forge_helpers.py +1215 -0
- alysis_code/cli_impl/commands/hooks.py +737 -0
- alysis_code/cli_impl/commands/ide_bridge.py +31 -0
- alysis_code/cli_impl/commands/mcp.py +700 -0
- alysis_code/cli_impl/commands/profile.py +453 -0
- alysis_code/cli_impl/commands/prompt_helpers.py +307 -0
- alysis_code/cli_impl/commands/report.py +88 -0
- alysis_code/cli_impl/commands/root.py +1118 -0
- alysis_code/cli_impl/commands/sandbox.py +184 -0
- alysis_code/cli_impl/commands/server.py +54 -0
- alysis_code/cli_impl/commands/sessions.py +252 -0
- alysis_code/cli_impl/commands/skills.py +404 -0
- alysis_code/cli_impl/commands/startup.py +946 -0
- alysis_code/cli_impl/commands/tools.py +335 -0
- alysis_code/cli_impl/commands/update.py +364 -0
- alysis_code/cli_impl/commands/welcome.py +972 -0
- alysis_code/cli_impl/config_menu.py +3882 -0
- alysis_code/cli_impl/forge.py +4509 -0
- alysis_code/cli_impl/forge_recovery.py +485 -0
- alysis_code/cli_impl/setup_wizard.py +2409 -0
- alysis_code/cli_impl/tui/__init__.py +58 -0
- alysis_code/cli_impl/tui/app.py +4551 -0
- alysis_code/cli_impl/tui/config.py +32 -0
- alysis_code/cli_impl/tui/config_flow.py +2754 -0
- alysis_code/cli_impl/tui/config_overlay.py +566 -0
- alysis_code/cli_impl/tui/content.py +78 -0
- alysis_code/cli_impl/tui/footer.py +218 -0
- alysis_code/cli_impl/tui/forge_status.py +136 -0
- alysis_code/cli_impl/tui/markdown.py +244 -0
- alysis_code/cli_impl/tui/owl.py +109 -0
- alysis_code/cli_impl/tui/plan_meta.py +477 -0
- alysis_code/cli_impl/tui/setup_app.py +519 -0
- alysis_code/cli_impl/tui/setup_flow.py +1622 -0
- alysis_code/cli_impl/tui/state.py +101 -0
- alysis_code/cli_impl/tui/subagent_identity.py +66 -0
- alysis_code/cli_impl/tui/subagent_panel.py +186 -0
- alysis_code/cli_impl/tui/surface.py +796 -0
- alysis_code/cli_impl/tui/transcript.py +514 -0
- alysis_code/cli_impl/tui/update_prompt.py +79 -0
- alysis_code/cli_impl/tui/workspace_guard.py +384 -0
- alysis_code/clipboard.py +172 -0
- alysis_code/code_review.py +1211 -0
- alysis_code/compaction/__init__.py +28 -0
- alysis_code/compaction/conversation_compactor.py +2932 -0
- alysis_code/compaction/importance.py +177 -0
- alysis_code/compaction/settings.py +297 -0
- alysis_code/compaction/tool_output_offload.py +447 -0
- alysis_code/config.py +3509 -0
- alysis_code/conflict_auto_resolver.py +895 -0
- alysis_code/context/__init__.py +1 -0
- alysis_code/context/tool_schema_budgeter.py +220 -0
- alysis_code/crash_diagnostics.py +282 -0
- alysis_code/custom_tools/README.md +34 -0
- alysis_code/custom_tools/__init__.py +43 -0
- alysis_code/custom_tools/discovery.py +903 -0
- alysis_code/custom_tools/runtime.py +1516 -0
- alysis_code/custom_tools/session.py +227 -0
- alysis_code/custom_tools/trust.py +232 -0
- alysis_code/diff_paths.py +113 -0
- alysis_code/direction_change.py +293 -0
- alysis_code/dispatch_timing.py +306 -0
- alysis_code/durable_service_manager.py +1236 -0
- alysis_code/edit_discipline.py +659 -0
- alysis_code/error_text.py +73 -0
- alysis_code/execution_budget.py +411 -0
- alysis_code/execution_context.py +915 -0
- alysis_code/execution_deadline.py +1065 -0
- alysis_code/execution_shared.py +1904 -0
- alysis_code/extensions/README.md +30 -0
- alysis_code/extensions/__init__.py +93 -0
- alysis_code/extensions/activation.py +138 -0
- alysis_code/extensions/install.py +1436 -0
- alysis_code/extensions/manifest.py +487 -0
- alysis_code/extensions/models.py +74 -0
- alysis_code/extensions/paths.py +56 -0
- alysis_code/extensions/registry.json +4 -0
- alysis_code/extensions/registry.py +52 -0
- alysis_code/extensions/state.py +83 -0
- alysis_code/extensions/workspace_trust.py +101 -0
- alysis_code/failed_task_evidence.py +369 -0
- alysis_code/failure_category.py +315 -0
- alysis_code/feedback_report.py +1647 -0
- alysis_code/file_classification.py +485 -0
- alysis_code/forge.py +2064 -0
- alysis_code/forge_completion.py +362 -0
- alysis_code/forge_events.py +475 -0
- alysis_code/frontmatter_utils.py +95 -0
- alysis_code/git_evidence.py +1181 -0
- alysis_code/git_ops.py +560 -0
- alysis_code/git_safe.py +62 -0
- alysis_code/git_worktrees.py +190 -0
- alysis_code/hooks/README.md +33 -0
- alysis_code/hooks/__init__.py +67 -0
- alysis_code/hooks/audit.py +171 -0
- alysis_code/hooks/config.py +225 -0
- alysis_code/hooks/dispatcher.py +1110 -0
- alysis_code/hooks/models.py +447 -0
- alysis_code/hooks/trust.py +202 -0
- alysis_code/host_actions.py +543 -0
- alysis_code/host_browser.py +103 -0
- alysis_code/ide/__init__.py +5 -0
- alysis_code/ide/activity_events.py +399 -0
- alysis_code/ide/approvals.py +337 -0
- alysis_code/ide/artifacts.py +153 -0
- alysis_code/ide/browser_egress_proxy.py +1076 -0
- alysis_code/ide/cdp_websocket_transport.py +1192 -0
- alysis_code/ide/change_ledger.py +1721 -0
- alysis_code/ide/context_blocks.py +979 -0
- alysis_code/ide/event_stream.py +531 -0
- alysis_code/ide/forge_protocol.py +3112 -0
- alysis_code/ide/forge_request_ledger.py +737 -0
- alysis_code/ide/health.py +965 -0
- alysis_code/ide/managed_browser.py +2251 -0
- alysis_code/ide/management_protocol.py +3414 -0
- alysis_code/ide/mcp_oauth_coordinator.py +744 -0
- alysis_code/ide/mcp_oauth_lifecycle.py +1504 -0
- alysis_code/ide/prompt_queue.py +1070 -0
- alysis_code/ide/protocol.py +191 -0
- alysis_code/ide/resumable_swarm.py +1543 -0
- alysis_code/ide/session_search.py +295 -0
- alysis_code/ide/stdio_bridge.py +9935 -0
- alysis_code/ide/structured_state.py +1579 -0
- alysis_code/ide/swarm_protocol.py +816 -0
- alysis_code/integration_gate.py +506 -0
- alysis_code/interactive_input_guard.py +39 -0
- alysis_code/interactive_plan_mode.py +26 -0
- alysis_code/internal_artifacts.py +179 -0
- alysis_code/knowledge_base.py +1409 -0
- alysis_code/knowledge_capture.py +1190 -0
- alysis_code/knowledge_librarian.py +605 -0
- alysis_code/language_policy.py +34 -0
- alysis_code/litellm_static_provider.py +535 -0
- alysis_code/llm/__init__.py +1 -0
- alysis_code/llm/anthropic_messages.py +2288 -0
- alysis_code/llm/base.py +71 -0
- alysis_code/llm/cache_capabilities.py +985 -0
- alysis_code/llm/cache_control_blocks.py +244 -0
- alysis_code/llm/cache_policy.py +388 -0
- alysis_code/llm/factory.py +373 -0
- alysis_code/llm/gemini_generate_content.py +2652 -0
- alysis_code/llm/gemini_interactions.py +739 -0
- alysis_code/llm/metadata.py +450 -0
- alysis_code/llm/openai_compat.py +2947 -0
- alysis_code/llm/openai_responses.py +2604 -0
- alysis_code/llm/protocols.py +609 -0
- alysis_code/llm/provider_limits.py +525 -0
- alysis_code/llm/request_plan.py +389 -0
- alysis_code/llm/request_shape.py +238 -0
- alysis_code/llm/streaming.py +108 -0
- alysis_code/llm/temperature_compat.py +78 -0
- alysis_code/llm/types.py +195 -0
- alysis_code/llm/usage_normalization.py +222 -0
- alysis_code/llm_error_display.py +315 -0
- alysis_code/logging_redaction.py +326 -0
- alysis_code/managed_host_deadline.py +196 -0
- alysis_code/mcp/README.md +33 -0
- alysis_code/mcp/__init__.py +24 -0
- alysis_code/mcp/client.py +1137 -0
- alysis_code/mcp/config.py +597 -0
- alysis_code/mcp/errors.py +113 -0
- alysis_code/mcp/forge_scope.py +154 -0
- alysis_code/mcp/jsonrpc.py +214 -0
- alysis_code/mcp/manager.py +2308 -0
- alysis_code/mcp/models.py +666 -0
- alysis_code/mcp/oauth.py +972 -0
- alysis_code/mcp/oauth_runtime.py +310 -0
- alysis_code/mcp/oauth_store.py +276 -0
- alysis_code/mcp/prompts.py +329 -0
- alysis_code/mcp/resources.py +295 -0
- alysis_code/mcp/roots.py +106 -0
- alysis_code/mcp/server_requests.py +75 -0
- alysis_code/mcp/token_store.py +859 -0
- alysis_code/mcp/transport_http.py +1338 -0
- alysis_code/mcp/transport_stdio.py +1267 -0
- alysis_code/mcp/untrusted_content.py +119 -0
- alysis_code/merge_conflict_reviewer.py +729 -0
- alysis_code/model_catalog/__init__.py +1 -0
- alysis_code/model_catalog/chatgpt_codex_subscription_snapshot.json +186 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.json +44715 -0
- alysis_code/model_catalog/litellm_model_prices_snapshot.meta.json +17 -0
- alysis_code/model_metadata_policy.py +223 -0
- alysis_code/model_metadata_utils.py +103 -0
- alysis_code/model_registry.py +1420 -0
- alysis_code/model_router.py +147 -0
- alysis_code/permission_policy.py +1016 -0
- alysis_code/personas.py +451 -0
- alysis_code/pipeline_facts.py +233 -0
- alysis_code/plan_assistant.py +4763 -0
- alysis_code/plan_mode.py +393 -0
- alysis_code/plan_reconciliation.py +1228 -0
- alysis_code/plan_repair.py +652 -0
- alysis_code/plan_validation.py +1099 -0
- alysis_code/planning_constraints.py +904 -0
- alysis_code/policy.py +95 -0
- alysis_code/preview_server.py +457 -0
- alysis_code/process_reaping.py +566 -0
- alysis_code/profile_presets.py +1834 -0
- alysis_code/profiles.py +666 -0
- alysis_code/provider_auth/__init__.py +29 -0
- alysis_code/provider_auth/base.py +99 -0
- alysis_code/provider_auth/openai_codex.py +951 -0
- alysis_code/provider_auth/registry.py +76 -0
- alysis_code/provider_auth/store.py +125 -0
- alysis_code/provider_diagnostics.py +1209 -0
- alysis_code/provider_model_catalog.py +685 -0
- alysis_code/provider_telemetry.py +1699 -0
- alysis_code/provider_url.py +75 -0
- alysis_code/reasoning_contracts.py +911 -0
- alysis_code/remote_sync.py +350 -0
- alysis_code/replanning.py +1195 -0
- alysis_code/repo_scan.py +1152 -0
- alysis_code/request_estimation.py +296 -0
- alysis_code/review_gate.py +617 -0
- alysis_code/run_lock.py +1141 -0
- alysis_code/run_outcome.py +58 -0
- alysis_code/run_provenance.py +774 -0
- alysis_code/run_state.py +445 -0
- alysis_code/runtime_artifacts.py +116 -0
- alysis_code/runtime_context_features.py +78 -0
- alysis_code/runtime_kind.py +52 -0
- alysis_code/safety/__init__.py +11 -0
- alysis_code/safety/mcp_sanitize.py +29 -0
- alysis_code/safety/safe_http.py +297 -0
- alysis_code/safety/subagent_report.py +184 -0
- alysis_code/sandbox_doctor.py +682 -0
- alysis_code/sandbox_runner.py +1025 -0
- alysis_code/sandbox_settings.py +423 -0
- alysis_code/serialized_paths.py +355 -0
- alysis_code/server/__init__.py +3 -0
- alysis_code/server/app.py +367 -0
- alysis_code/server/auth.py +34 -0
- alysis_code/server/job_config.py +30 -0
- alysis_code/server/settings.py +215 -0
- alysis_code/server/store.py +193 -0
- alysis_code/server/worker_runner.py +657 -0
- alysis_code/service_persistence.py +355 -0
- alysis_code/session_artifacts.py +108 -0
- alysis_code/session_metrics.py +331 -0
- alysis_code/session_store.py +624 -0
- alysis_code/skills/README.md +34 -0
- alysis_code/skills/__init__.py +104 -0
- alysis_code/skills/conventions.py +84 -0
- alysis_code/skills/discovery.py +176 -0
- alysis_code/skills/eval_models.py +232 -0
- alysis_code/skills/eval_runner.py +372 -0
- alysis_code/skills/evals.py +1344 -0
- alysis_code/skills/install.py +293 -0
- alysis_code/skills/loader.py +118 -0
- alysis_code/skills/matching.py +103 -0
- alysis_code/skills/models.py +71 -0
- alysis_code/skills/paths.py +56 -0
- alysis_code/skills/prompting.py +500 -0
- alysis_code/skills/scaffold.py +142 -0
- alysis_code/skills/state.py +441 -0
- alysis_code/skills/transactions.py +125 -0
- alysis_code/skills/validation.py +304 -0
- alysis_code/step_budget.py +238 -0
- alysis_code/subagent_labels.py +49 -0
- alysis_code/subagents.py +1072 -0
- alysis_code/surface/__init__.py +80 -0
- alysis_code/surface/base.py +305 -0
- alysis_code/surface/console.py +387 -0
- alysis_code/surface/events.py +372 -0
- alysis_code/surface/hidden_surface.py +529 -0
- alysis_code/surface/noop_surface.py +219 -0
- alysis_code/surface/rich_surface.py +1555 -0
- alysis_code/surface/styles.py +67 -0
- alysis_code/surface/theme.py +455 -0
- alysis_code/surface/types.py +100 -0
- alysis_code/swarm_backend.py +926 -0
- alysis_code/swarm_orchestrator.py +4020 -0
- alysis_code/swarm_scheduler.py +441 -0
- alysis_code/swarm_trace.py +429 -0
- alysis_code/swarm_worker.py +2119 -0
- alysis_code/swarm_write_guard.py +348 -0
- alysis_code/task_dependencies.py +170 -0
- alysis_code/task_readiness.py +992 -0
- alysis_code/task_scope.py +2148 -0
- alysis_code/terminal_manager.py +762 -0
- alysis_code/terminal_ownership.py +460 -0
- alysis_code/text_normalization.py +30 -0
- alysis_code/token_budget.py +97 -0
- alysis_code/tools/README.md +34 -0
- alysis_code/tools/__init__.py +1 -0
- alysis_code/tools/artifacts.py +127 -0
- alysis_code/tools/availability.py +188 -0
- alysis_code/tools/fs.py +1456 -0
- alysis_code/tools/git.py +461 -0
- alysis_code/tools/history.py +229 -0
- alysis_code/tools/http_timeout.py +78 -0
- alysis_code/tools/image_generation.py +552 -0
- alysis_code/tools/registry.py +2936 -0
- alysis_code/tools/repo_map.py +476 -0
- alysis_code/tools/search.py +563 -0
- alysis_code/tools/shell.py +135 -0
- alysis_code/tools/symbols.py +1350 -0
- alysis_code/tools/test_discovery.py +643 -0
- alysis_code/tools/web.py +482 -0
- alysis_code/tools/web_search.py +2012 -0
- alysis_code/tools/web_search_dashscope.py +557 -0
- alysis_code/tools/web_search_ddgs.py +221 -0
- alysis_code/tools/web_search_provider_adapters.py +1429 -0
- alysis_code/tools/web_search_tavily.py +194 -0
- alysis_code/updates.py +933 -0
- alysis_code/usage_tracker.py +1990 -0
- alysis_code/verification_command_analysis.py +1004 -0
- alysis_code/verification_contract.py +574 -0
- alysis_code/verification_failure_summary.py +273 -0
- alysis_code/verification_repair.py +385 -0
- alysis_code/verify_gate.py +3129 -0
- alysis_code/web_research.py +1872 -0
- alysis_code/web_search_adapters.py +66 -0
- alysis_code/web_search_policy.py +27 -0
- alysis_code/workspace_binding.py +389 -0
- alysis_code/workspace_binding_ui.py +408 -0
- alysis_code/workspace_context.py +273 -0
- alysis_code/workspace_isolation.py +138 -0
- alysis_code/workspace_provisioning.py +455 -0
- alysis_code-0.13.0.dist-info/METADATA +507 -0
- alysis_code-0.13.0.dist-info/RECORD +458 -0
- alysis_code-0.13.0.dist-info/WHEEL +4 -0
- alysis_code-0.13.0.dist-info/entry_points.txt +3 -0
- alysis_code-0.13.0.dist-info/licenses/LICENSE +176 -0
- alysis_code-0.13.0.dist-info/licenses/NOTICE +4 -0
|
@@ -0,0 +1,946 @@
|
|
|
1
|
+
# ruff: noqa: F401,F403,F405,I001
|
|
2
|
+
# Legacy split module: dependencies are synced by cli_surface.py.
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from .cli_common import *
|
|
8
|
+
from .update import _cached_update_notice
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _is_narrow_terminal() -> bool:
|
|
12
|
+
return _terminal_width() < 88
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _apply_temperature_override(cfg: AppConfig, temperature: float) -> None:
|
|
16
|
+
cfg.temperature = temperature
|
|
17
|
+
cfg.coding_temperature = temperature
|
|
18
|
+
cfg.review_temperature = temperature
|
|
19
|
+
cfg.planner_temperature = temperature
|
|
20
|
+
cfg.conflict_review_temperature = temperature
|
|
21
|
+
cfg.compactor_temperature = temperature
|
|
22
|
+
cfg.chat_temperature = temperature
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _home_panel() -> Panel:
|
|
26
|
+
lines = [
|
|
27
|
+
"Quick actions:",
|
|
28
|
+
"- 1) chat interactive chat loop",
|
|
29
|
+
"- 2) run one-shot agent execution (you will be prompted for instruction)",
|
|
30
|
+
"- 3) setup first-run setup wizard",
|
|
31
|
+
"- 4) doctor environment checks",
|
|
32
|
+
"- 5) plan Forge plan command",
|
|
33
|
+
"- 6) quit exit home prompt",
|
|
34
|
+
"- forge (in chat) enter planning mode with /forge",
|
|
35
|
+
"- forge show summarize current plan artifacts",
|
|
36
|
+
]
|
|
37
|
+
update_notice = _cached_update_notice()
|
|
38
|
+
if update_notice:
|
|
39
|
+
lines.append("")
|
|
40
|
+
lines.append(update_notice)
|
|
41
|
+
content = "\n".join(lines)
|
|
42
|
+
return _Panel(content, title="Alysis Code Home", border_style="bright_black")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _home_prompt_enabled() -> bool:
|
|
46
|
+
raw = str(env_get("ALYSIS_HOME_PROMPT") or "").strip().lower()
|
|
47
|
+
return raw in {"1", "true", "yes", "on"}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _run_default_chat_action(
|
|
51
|
+
*,
|
|
52
|
+
path: Path | None = None,
|
|
53
|
+
allow_broad_workspace: bool = False,
|
|
54
|
+
mode: str | None = None,
|
|
55
|
+
model: str | None = None,
|
|
56
|
+
base_url: str | None = None,
|
|
57
|
+
temperature: float | None = None,
|
|
58
|
+
stream: bool | None = None,
|
|
59
|
+
max_steps: int | None = None,
|
|
60
|
+
subagents: bool | None = None,
|
|
61
|
+
no_log: bool = False,
|
|
62
|
+
verify_cmd: list[str] | None = None,
|
|
63
|
+
yes: bool = False,
|
|
64
|
+
) -> None:
|
|
65
|
+
# Execution-posture flags (mode/yes/no_log/verify_cmd/…) default to the plain
|
|
66
|
+
# launch values, but callers may forward the current session's flags so a
|
|
67
|
+
# relaunch (e.g. /config → switch project) preserves the user's chosen posture
|
|
68
|
+
# instead of silently resetting approval policy/mode/logging.
|
|
69
|
+
_patchable("chat", chat)(
|
|
70
|
+
path=path or Path("."),
|
|
71
|
+
create_path=False,
|
|
72
|
+
allow_broad_workspace=allow_broad_workspace,
|
|
73
|
+
image=None,
|
|
74
|
+
mode=mode,
|
|
75
|
+
model=model,
|
|
76
|
+
base_url=base_url,
|
|
77
|
+
temperature=temperature,
|
|
78
|
+
stream=stream,
|
|
79
|
+
max_steps=max_steps,
|
|
80
|
+
subagents=subagents,
|
|
81
|
+
no_log=no_log,
|
|
82
|
+
verify_cmd=verify_cmd,
|
|
83
|
+
api_key_env=None,
|
|
84
|
+
api_key_stdin=False,
|
|
85
|
+
api_key=None,
|
|
86
|
+
yes=yes,
|
|
87
|
+
diagnostic_log=None,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _configured_default_workspace() -> Path | None:
|
|
92
|
+
"""The workspace folder setup persisted (``default_workspace_path``), if any."""
|
|
93
|
+
try:
|
|
94
|
+
cfg = _patchable("load_config", load_config)()
|
|
95
|
+
raw = str((cfg.extra_fields or {}).get("default_workspace_path") or "").strip()
|
|
96
|
+
except Exception:
|
|
97
|
+
return None
|
|
98
|
+
return Path(raw).expanduser() if raw else None
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run_chat_after_setup() -> None:
|
|
102
|
+
"""Launch chat right after setup, in the workspace the user just configured.
|
|
103
|
+
|
|
104
|
+
The broad-workspace consent was already given during setup (the wizard binds
|
|
105
|
+
with ``allow_broad_workspace=True``), so we don't re-prompt the guard for that
|
|
106
|
+
same folder. Falls back to the current directory when no workspace was saved.
|
|
107
|
+
"""
|
|
108
|
+
workspace = _configured_default_workspace()
|
|
109
|
+
_run_default_chat_action(path=workspace, allow_broad_workspace=workspace is not None)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class _SubscriptionAvailability:
|
|
114
|
+
active: bool = False
|
|
115
|
+
ready: bool = True
|
|
116
|
+
provider_id: str = ""
|
|
117
|
+
message: str = ""
|
|
118
|
+
is_error: bool = False
|
|
119
|
+
selection_required: bool = False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _subscription_availability(cfg: AppConfig) -> _SubscriptionAvailability:
|
|
123
|
+
"""Return subscription readiness without deciding whether the UI may open."""
|
|
124
|
+
|
|
125
|
+
try:
|
|
126
|
+
from ...profiles import active_subscription_selection_ready, get_active_profile
|
|
127
|
+
|
|
128
|
+
active_profile = get_active_profile(cfg)
|
|
129
|
+
provider_id = str(active_profile.auth_provider or "").strip()
|
|
130
|
+
except Exception as exc:
|
|
131
|
+
return _SubscriptionAvailability(
|
|
132
|
+
active=True,
|
|
133
|
+
ready=False,
|
|
134
|
+
message=f"Model access configuration is invalid: {exc}",
|
|
135
|
+
is_error=True,
|
|
136
|
+
)
|
|
137
|
+
if not provider_id:
|
|
138
|
+
return _SubscriptionAvailability()
|
|
139
|
+
try:
|
|
140
|
+
from ...provider_auth import create_provider_auth
|
|
141
|
+
|
|
142
|
+
adapter = create_provider_auth(provider_id)
|
|
143
|
+
if active_profile.protocol != adapter.protocol or active_profile.base_url.rstrip(
|
|
144
|
+
"/"
|
|
145
|
+
) != adapter.base_url.rstrip("/"):
|
|
146
|
+
return _SubscriptionAvailability(
|
|
147
|
+
active=True,
|
|
148
|
+
ready=False,
|
|
149
|
+
provider_id=provider_id,
|
|
150
|
+
message=(
|
|
151
|
+
"The subscription connection profile is incompatible with its provider "
|
|
152
|
+
"adapter. Reconnect it through `alysis setup`."
|
|
153
|
+
),
|
|
154
|
+
is_error=True,
|
|
155
|
+
)
|
|
156
|
+
status = adapter.account_status()
|
|
157
|
+
except Exception as exc: # noqa: BLE001
|
|
158
|
+
return _SubscriptionAvailability(
|
|
159
|
+
active=True,
|
|
160
|
+
ready=False,
|
|
161
|
+
provider_id=provider_id,
|
|
162
|
+
message=f"Subscription status check failed: {exc}",
|
|
163
|
+
is_error=True,
|
|
164
|
+
)
|
|
165
|
+
if status.connected:
|
|
166
|
+
if active_subscription_selection_ready(cfg):
|
|
167
|
+
return _SubscriptionAvailability(
|
|
168
|
+
active=True,
|
|
169
|
+
ready=True,
|
|
170
|
+
provider_id=provider_id,
|
|
171
|
+
)
|
|
172
|
+
return _SubscriptionAvailability(
|
|
173
|
+
active=True,
|
|
174
|
+
ready=False,
|
|
175
|
+
provider_id=provider_id,
|
|
176
|
+
message=(
|
|
177
|
+
"Choose the subscription model and reasoning effort in "
|
|
178
|
+
"/config → Default Model before sending a message."
|
|
179
|
+
),
|
|
180
|
+
selection_required=True,
|
|
181
|
+
)
|
|
182
|
+
detail = f" ({status.detail})" if status.detail else ""
|
|
183
|
+
if not status.verified:
|
|
184
|
+
return _SubscriptionAvailability(
|
|
185
|
+
active=True,
|
|
186
|
+
ready=False,
|
|
187
|
+
provider_id=provider_id,
|
|
188
|
+
message=(
|
|
189
|
+
"The AI subscription status is temporarily unavailable"
|
|
190
|
+
f"{detail}. Try again shortly; your saved connection was not removed."
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
return _SubscriptionAvailability(
|
|
194
|
+
active=True,
|
|
195
|
+
ready=False,
|
|
196
|
+
provider_id=provider_id,
|
|
197
|
+
message=(
|
|
198
|
+
"The selected AI subscription is not connected"
|
|
199
|
+
f"{detail}. Type /login in the TUI and choose the connection, or run "
|
|
200
|
+
f"`alysis auth login {provider_id}`; use /config to change model access."
|
|
201
|
+
),
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _provider_auth_ready_for_chat() -> bool:
|
|
206
|
+
"""Report whether a model call can start; this must not gate opening the TUI."""
|
|
207
|
+
|
|
208
|
+
try:
|
|
209
|
+
availability = _subscription_availability(_patchable("load_config", load_config)())
|
|
210
|
+
except Exception as exc:
|
|
211
|
+
_console().print(f"[red]Model access configuration is invalid:[/red] {exc}")
|
|
212
|
+
return False
|
|
213
|
+
if availability.ready:
|
|
214
|
+
return True
|
|
215
|
+
style = "red" if availability.is_error else "yellow"
|
|
216
|
+
_console().print(f"[{style}]{availability.message}[/{style}]")
|
|
217
|
+
return False
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _require_active_subscription_ready(
|
|
221
|
+
*,
|
|
222
|
+
model: str | None,
|
|
223
|
+
base_url: str | None,
|
|
224
|
+
require_ready: bool = True,
|
|
225
|
+
) -> None:
|
|
226
|
+
"""Apply the subscription selection/account gate to any LLM-facing command."""
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
from ...profiles import get_active_profile
|
|
230
|
+
|
|
231
|
+
auth_profile = get_active_profile(_patchable("load_config", load_config)())
|
|
232
|
+
except Exception:
|
|
233
|
+
auth_profile = None
|
|
234
|
+
if auth_profile is None or not auth_profile.auth_provider:
|
|
235
|
+
return
|
|
236
|
+
model_is_override = model is not None and str(model).strip() != auth_profile.default_model
|
|
237
|
+
base_url_is_override = base_url is not None and (
|
|
238
|
+
str(base_url).strip().rstrip("/") != auth_profile.base_url.rstrip("/")
|
|
239
|
+
)
|
|
240
|
+
if model_is_override or base_url_is_override:
|
|
241
|
+
_console().print(
|
|
242
|
+
"[red]Subscription model and endpoint overrides are managed in "
|
|
243
|
+
"`/config` so model and reasoning stay compatible.[/red]"
|
|
244
|
+
)
|
|
245
|
+
raise typer.Exit(code=2)
|
|
246
|
+
if require_ready and not _provider_auth_ready_for_chat():
|
|
247
|
+
raise typer.Exit(code=1)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _maybe_run_startup_config_menu() -> None:
|
|
251
|
+
cfg = _patchable("load_config", load_config)()
|
|
252
|
+
execution = getattr(cfg, "execution", None)
|
|
253
|
+
if str(getattr(execution, "backend", "native") or "native") == "delegated":
|
|
254
|
+
runtime_id = str(getattr(execution, "runtime", None) or "").strip()
|
|
255
|
+
if runtime_id and runtime_id in (getattr(cfg, "agent_runtimes", {}) or {}):
|
|
256
|
+
return
|
|
257
|
+
from ..config_menu import run_config_menu
|
|
258
|
+
|
|
259
|
+
run_config_menu(cfg=cfg, auto_focus="execution")
|
|
260
|
+
return
|
|
261
|
+
api_key_present = bool(_patchable("resolve_api_key", resolve_api_key)().key)
|
|
262
|
+
provider_auth_present = False
|
|
263
|
+
try:
|
|
264
|
+
from ...profiles import get_active_profile
|
|
265
|
+
|
|
266
|
+
active_profile = get_active_profile(cfg)
|
|
267
|
+
provider_auth_present = bool(active_profile.auth_provider)
|
|
268
|
+
except Exception:
|
|
269
|
+
provider_auth_present = False
|
|
270
|
+
if provider_auth_present:
|
|
271
|
+
return
|
|
272
|
+
if cfg.model and (api_key_present or provider_auth_present):
|
|
273
|
+
return
|
|
274
|
+
from ..config_menu import run_config_menu
|
|
275
|
+
|
|
276
|
+
focus = "api_key" if not api_key_present else "model"
|
|
277
|
+
run_config_menu(cfg=cfg, auto_focus=focus)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
# Persisted by setup (see setup_wizard._ONBOARDED_KEY) once first-run onboarding
|
|
281
|
+
# completes. The first-run gate keys on this marker rather than on "a model is
|
|
282
|
+
# configured", so a genuine first launch is routed to setup even when a config
|
|
283
|
+
# that already carries a model exists on disk (e.g. a pre-staged or partially
|
|
284
|
+
# written config) — that case used to fall straight through to chat and land the
|
|
285
|
+
# user on the guarded-workspace picker instead of setup.
|
|
286
|
+
_ONBOARDED_KEY = "onboarded"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _is_onboarded() -> bool:
|
|
290
|
+
"""Whether the user has already completed first-run onboarding.
|
|
291
|
+
|
|
292
|
+
True when the explicit ``onboarded`` marker is present. For configs that
|
|
293
|
+
predate the marker we also treat a persisted ``default_workspace_path`` as an
|
|
294
|
+
equivalent signal: that key is only ever written by a *completed* setup (or by
|
|
295
|
+
explicitly choosing a default workspace in ``/config``), so existing users who
|
|
296
|
+
already finished setup are never re-sent through it. A config that merely has a
|
|
297
|
+
model set (but no completed setup) is intentionally *not* considered onboarded.
|
|
298
|
+
"""
|
|
299
|
+
try:
|
|
300
|
+
cfg = _patchable("load_config", load_config)()
|
|
301
|
+
except Exception:
|
|
302
|
+
return False
|
|
303
|
+
extra = cfg.extra_fields or {}
|
|
304
|
+
if extra.get(_ONBOARDED_KEY):
|
|
305
|
+
return True
|
|
306
|
+
return bool(str(extra.get("default_workspace_path") or "").strip())
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _should_run_first_run_setup_wizard() -> bool:
|
|
310
|
+
return not _is_onboarded()
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _try_setup_tui(*, require_flag: bool = True, announce_fallback: bool = False) -> bool | None:
|
|
314
|
+
"""Run the alt-screen setup wizard (arrow-key selection screens).
|
|
315
|
+
|
|
316
|
+
Returns ``True``/``False`` on a completed run (saved / cancelled), or
|
|
317
|
+
``None`` when the TUI could not run so the caller falls back to the classic
|
|
318
|
+
Rich wizard. ``require_flag`` honors ``ALYSIS_TUI=0`` as an explicit
|
|
319
|
+
opt-out; the explicit ``alysis setup`` command passes
|
|
320
|
+
``require_flag=False`` so it still shows the interactive screens. When
|
|
321
|
+
``announce_fallback`` is set, the reason for dropping to the classic wizard
|
|
322
|
+
is printed (dim) so a launch failure is visible rather than silent.
|
|
323
|
+
"""
|
|
324
|
+
reason: str | None = None
|
|
325
|
+
try:
|
|
326
|
+
from ..tui import is_tui_enabled
|
|
327
|
+
except Exception as exc: # noqa: BLE001 - the TUI is optional
|
|
328
|
+
reason = f"the TUI module is unavailable ({exc})"
|
|
329
|
+
else:
|
|
330
|
+
if require_flag and not is_tui_enabled():
|
|
331
|
+
reason = "ALYSIS_TUI is off"
|
|
332
|
+
# ``_is_non_interactive_terminal`` is defined in this module (via the
|
|
333
|
+
# cli_common surface); call it directly — there is no ``cli_impl.cli``.
|
|
334
|
+
elif _is_non_interactive_terminal():
|
|
335
|
+
reason = "this terminal is not interactive"
|
|
336
|
+
|
|
337
|
+
if reason is None:
|
|
338
|
+
try:
|
|
339
|
+
from ..tui.setup_app import run_setup_tui
|
|
340
|
+
|
|
341
|
+
return bool(run_setup_tui())
|
|
342
|
+
except Exception as exc: # noqa: BLE001 - any prompt_toolkit/terminal failure
|
|
343
|
+
# app.run restored the terminal on its way out, so printing is safe.
|
|
344
|
+
reason = f"the setup TUI failed to start ({exc})"
|
|
345
|
+
|
|
346
|
+
if announce_fallback and reason:
|
|
347
|
+
try:
|
|
348
|
+
_console().print(f"[dim]Using the classic setup wizard — {reason}.[/dim]")
|
|
349
|
+
except Exception:
|
|
350
|
+
pass
|
|
351
|
+
return None
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def _maybe_run_first_run_setup_wizard() -> bool:
|
|
355
|
+
if not _should_run_first_run_setup_wizard():
|
|
356
|
+
return True
|
|
357
|
+
tui_result = _try_setup_tui()
|
|
358
|
+
if tui_result is True:
|
|
359
|
+
return True
|
|
360
|
+
if tui_result is False:
|
|
361
|
+
_console().print("[yellow]Exiting without starting chat.[/yellow]")
|
|
362
|
+
return False
|
|
363
|
+
from ..setup_wizard import run_setup_wizard
|
|
364
|
+
|
|
365
|
+
if run_setup_wizard():
|
|
366
|
+
return True
|
|
367
|
+
_console().print("[yellow]Exiting without starting chat.[/yellow]")
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _run_default_run_action(instruction: str) -> None:
|
|
372
|
+
_patchable("run", run)(
|
|
373
|
+
instruction=instruction,
|
|
374
|
+
path=Path("."),
|
|
375
|
+
create_path=False,
|
|
376
|
+
allow_broad_workspace=False,
|
|
377
|
+
image=None,
|
|
378
|
+
mode=None,
|
|
379
|
+
model=None,
|
|
380
|
+
base_url=None,
|
|
381
|
+
temperature=None,
|
|
382
|
+
stream=None,
|
|
383
|
+
max_steps=None,
|
|
384
|
+
subagents=None,
|
|
385
|
+
no_log=False,
|
|
386
|
+
verify_cmd=None,
|
|
387
|
+
api_key_env=None,
|
|
388
|
+
api_key_stdin=False,
|
|
389
|
+
api_key=None,
|
|
390
|
+
yes=False,
|
|
391
|
+
benchmark=False,
|
|
392
|
+
deadline_seconds=None,
|
|
393
|
+
require_deadline=False,
|
|
394
|
+
diagnostic_log=None,
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _current_branch_label(root: Path) -> str:
|
|
399
|
+
if shutil.which("git") is None:
|
|
400
|
+
return "-"
|
|
401
|
+
proc = subprocess.run(
|
|
402
|
+
["git", "-C", os.fspath(root), "rev-parse", "--abbrev-ref", "HEAD"],
|
|
403
|
+
check=False,
|
|
404
|
+
capture_output=True,
|
|
405
|
+
text=True,
|
|
406
|
+
)
|
|
407
|
+
if proc.returncode != 0:
|
|
408
|
+
return "-"
|
|
409
|
+
branch = proc.stdout.strip()
|
|
410
|
+
return branch or "-"
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _is_git_dirty(root: Path) -> bool:
|
|
414
|
+
if shutil.which("git") is None:
|
|
415
|
+
return False
|
|
416
|
+
proc = subprocess.run(
|
|
417
|
+
["git", "-C", os.fspath(root), "status", "--porcelain"],
|
|
418
|
+
check=False,
|
|
419
|
+
capture_output=True,
|
|
420
|
+
text=True,
|
|
421
|
+
)
|
|
422
|
+
if proc.returncode != 0:
|
|
423
|
+
return False
|
|
424
|
+
return bool(proc.stdout.strip())
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _is_interactive_terminal() -> bool:
|
|
428
|
+
return bool(sys.stdin.isatty() and sys.stdout.isatty())
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def _is_non_interactive_terminal() -> bool:
|
|
432
|
+
return not _is_interactive_terminal()
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _resolve_api_key_override(
|
|
436
|
+
*,
|
|
437
|
+
api_key: str | None,
|
|
438
|
+
api_key_env: str | None,
|
|
439
|
+
api_key_stdin: bool,
|
|
440
|
+
) -> str | None:
|
|
441
|
+
# Per-command overrides stay in-memory; persistent storage is handled only
|
|
442
|
+
# by explicit config credentials commands / setup flow.
|
|
443
|
+
selected = sum(bool(x) for x in [api_key is not None, api_key_env is not None, api_key_stdin])
|
|
444
|
+
if selected > 1:
|
|
445
|
+
raise ConfigError("Use only one of: --api-key, --api-key-env, --api-key-stdin")
|
|
446
|
+
|
|
447
|
+
if api_key is not None:
|
|
448
|
+
v = api_key.strip()
|
|
449
|
+
if not v:
|
|
450
|
+
raise ConfigError("API key is empty.")
|
|
451
|
+
return v
|
|
452
|
+
|
|
453
|
+
if api_key_env is not None:
|
|
454
|
+
name = api_key_env.strip()
|
|
455
|
+
if not name:
|
|
456
|
+
raise ConfigError("--api-key-env must be non-empty.")
|
|
457
|
+
v = (env_get(name) or "").strip()
|
|
458
|
+
if not v:
|
|
459
|
+
raise ConfigError(f"Environment variable {name} is not set.")
|
|
460
|
+
return v
|
|
461
|
+
|
|
462
|
+
if api_key_stdin:
|
|
463
|
+
v = typer.prompt("API key", hide_input=True).strip()
|
|
464
|
+
if not v:
|
|
465
|
+
raise ConfigError("API key is empty.")
|
|
466
|
+
return v
|
|
467
|
+
|
|
468
|
+
return None
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _api_key_source_label(source: str) -> str:
|
|
472
|
+
normalized = str(source or "").strip()
|
|
473
|
+
if normalized == "env:ALYSIS_API_KEY":
|
|
474
|
+
return "env (ALYSIS_API_KEY)"
|
|
475
|
+
if normalized == "env:OPENAI_API_KEY":
|
|
476
|
+
return "env (OPENAI_API_KEY)"
|
|
477
|
+
if normalized.startswith("env:"):
|
|
478
|
+
return f"env ({normalized.removeprefix('env:')})"
|
|
479
|
+
if normalized.startswith("stored:profile="):
|
|
480
|
+
return f"stored ({normalized.removeprefix('stored:profile=')})"
|
|
481
|
+
if normalized in {"stored", "stored:legacy"}:
|
|
482
|
+
return "stored"
|
|
483
|
+
return "missing"
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _resolved_api_key_value() -> ApiKeyResolution:
|
|
487
|
+
return resolve_api_key()
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def _parse_bool_text(value: str) -> bool | None:
|
|
491
|
+
v = value.strip().lower()
|
|
492
|
+
if v in {"1", "true", "yes", "on"}:
|
|
493
|
+
return True
|
|
494
|
+
if v in {"0", "false", "no", "off"}:
|
|
495
|
+
return False
|
|
496
|
+
return None
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _safe_component(raw: str) -> str:
|
|
500
|
+
clean = re.sub(r"[^A-Za-z0-9_-]", "_", str(raw).strip())
|
|
501
|
+
return clean or "x"
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
def _resolve_usage_hud_default(cfg: AppConfig) -> bool:
|
|
505
|
+
raw_env = env_get("ALYSIS_SHOW_USAGE_HUD")
|
|
506
|
+
if raw_env is not None:
|
|
507
|
+
parsed_env = _parse_bool_text(raw_env)
|
|
508
|
+
if parsed_env is not None:
|
|
509
|
+
return parsed_env
|
|
510
|
+
raw_cfg = cfg.extra_fields.get("show_usage_hud")
|
|
511
|
+
if isinstance(raw_cfg, bool):
|
|
512
|
+
return raw_cfg
|
|
513
|
+
if isinstance(raw_cfg, str):
|
|
514
|
+
parsed_cfg = _parse_bool_text(raw_cfg)
|
|
515
|
+
if parsed_cfg is not None:
|
|
516
|
+
return parsed_cfg
|
|
517
|
+
return True
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _chat_usage_hud_enabled(session: Any) -> bool:
|
|
521
|
+
raw = getattr(session, "_usage_hud_enabled", True)
|
|
522
|
+
return bool(raw)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def _set_chat_usage_hud_enabled(session: Any, enabled: bool) -> None:
|
|
526
|
+
session._usage_hud_enabled = bool(enabled)
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def _refresh_chat_hud_context_cache(session: Any) -> None:
|
|
530
|
+
context_fn = getattr(session, "context_left", None)
|
|
531
|
+
if callable(context_fn):
|
|
532
|
+
try:
|
|
533
|
+
session._hud_context_cache = context_fn()
|
|
534
|
+
except Exception: # noqa: BLE001
|
|
535
|
+
session._hud_context_cache = None
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _chat_context_hud_value(session: Any) -> str:
|
|
539
|
+
value = _chat_context_percent_value(session)
|
|
540
|
+
if value is None:
|
|
541
|
+
return "n/a"
|
|
542
|
+
label = f"{value:.1f}%"
|
|
543
|
+
if value < 10.0:
|
|
544
|
+
return f"{label} !!"
|
|
545
|
+
if value < 25.0:
|
|
546
|
+
return f"{label} !"
|
|
547
|
+
return label
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _format_cost_with_unknown(
|
|
551
|
+
*,
|
|
552
|
+
known_cost: float | None,
|
|
553
|
+
unknown_calls: int,
|
|
554
|
+
style: str,
|
|
555
|
+
) -> str:
|
|
556
|
+
base = format_usd(known_cost, style=style)
|
|
557
|
+
if unknown_calls <= 0:
|
|
558
|
+
return base
|
|
559
|
+
return f"{base} (+{unknown_calls} unknown)"
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
def _known_cost_value(data: dict[str, Any]) -> float | None:
|
|
563
|
+
if int(data.get("known_cost_calls") or 0) <= 0:
|
|
564
|
+
return None
|
|
565
|
+
raw = data.get("cost_usd")
|
|
566
|
+
if raw is None:
|
|
567
|
+
return None
|
|
568
|
+
try:
|
|
569
|
+
return float(raw)
|
|
570
|
+
except (TypeError, ValueError):
|
|
571
|
+
return None
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def _format_exact_token_count(value: Any) -> str:
|
|
575
|
+
try:
|
|
576
|
+
parsed = int(value or 0)
|
|
577
|
+
except (TypeError, ValueError):
|
|
578
|
+
parsed = 0
|
|
579
|
+
return f"{max(0, parsed):,}"
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def _format_compact_token_count(value: Any) -> str:
|
|
583
|
+
try:
|
|
584
|
+
parsed = int(value or 0)
|
|
585
|
+
except (TypeError, ValueError):
|
|
586
|
+
parsed = 0
|
|
587
|
+
count = max(0, parsed)
|
|
588
|
+
if count < 10_000:
|
|
589
|
+
return f"{count:,}"
|
|
590
|
+
if count < 1_000_000:
|
|
591
|
+
return f"{count / 1_000:.1f}k"
|
|
592
|
+
return f"{count / 1_000_000:.1f}M"
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _chat_context_percent_value(session: Any) -> float | None:
|
|
596
|
+
ctx = getattr(session, "_hud_context_cache", None)
|
|
597
|
+
if ctx is None:
|
|
598
|
+
return None
|
|
599
|
+
# The primary HUD must describe provider-usable capacity. The former
|
|
600
|
+
# baseline-subtracted conversation percentage always started at 100%, even
|
|
601
|
+
# when bootstrap prompts and tool schemas already occupied a substantial
|
|
602
|
+
# part of the model window.
|
|
603
|
+
percent = getattr(ctx, "effective_percent_left", None)
|
|
604
|
+
if percent is None:
|
|
605
|
+
percent = getattr(ctx, "context_window_percent_left", None)
|
|
606
|
+
if percent is None:
|
|
607
|
+
percent = getattr(ctx, "percent_left", None)
|
|
608
|
+
if percent is None:
|
|
609
|
+
return None
|
|
610
|
+
try:
|
|
611
|
+
return float(percent)
|
|
612
|
+
except (TypeError, ValueError):
|
|
613
|
+
return None
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _chat_effective_budget_percent_value(session: Any) -> float | None:
|
|
617
|
+
ctx = getattr(session, "_hud_context_cache", None)
|
|
618
|
+
if ctx is None:
|
|
619
|
+
return None
|
|
620
|
+
percent = getattr(ctx, "effective_percent_left", None)
|
|
621
|
+
if percent is None:
|
|
622
|
+
percent = getattr(ctx, "percent_left", None)
|
|
623
|
+
if percent is None:
|
|
624
|
+
return None
|
|
625
|
+
try:
|
|
626
|
+
return float(percent)
|
|
627
|
+
except (TypeError, ValueError):
|
|
628
|
+
return None
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def _format_chat_context_percent(percent: float | None) -> str:
|
|
632
|
+
if percent is None:
|
|
633
|
+
return "n/a"
|
|
634
|
+
return f"{percent:.1f}%"
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _chat_usage_warning_line(percent: float | None) -> str | None:
|
|
638
|
+
if percent is None:
|
|
639
|
+
return None
|
|
640
|
+
if percent < 10.0:
|
|
641
|
+
return "\u26a0\ufe0e Critical input budget — run /compact to reduce context"
|
|
642
|
+
if percent < 20.0:
|
|
643
|
+
return "\u26a0\ufe0e Low input budget — run /compact to reduce context"
|
|
644
|
+
return None
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def _chat_turn_usage_style(session: Any) -> str:
|
|
648
|
+
percent = _chat_effective_budget_percent_value(session)
|
|
649
|
+
if percent is None:
|
|
650
|
+
return "dim"
|
|
651
|
+
if percent < 10.0:
|
|
652
|
+
return "bold red"
|
|
653
|
+
if percent < 20.0:
|
|
654
|
+
return "yellow"
|
|
655
|
+
if percent < 50.0:
|
|
656
|
+
return STYLE_CONTENT
|
|
657
|
+
return "dim"
|
|
658
|
+
|
|
659
|
+
|
|
660
|
+
def _format_usage_billing_for_display(row: dict[str, Any], *, compact: bool = False) -> str:
|
|
661
|
+
"""Render charge provenance without presenting estimates as exact bills."""
|
|
662
|
+
|
|
663
|
+
known_cost = _known_cost_value(row)
|
|
664
|
+
unknown_calls = int(row.get("unknown_cost_calls", row.get("unknown_cost_count", 0)) or 0)
|
|
665
|
+
estimate_calls = int(row.get("catalog_estimated_cost_calls") or 0)
|
|
666
|
+
subscription_calls = int(row.get("subscription_calls") or 0)
|
|
667
|
+
included_calls = int(row.get("included_calls") or 0)
|
|
668
|
+
local_calls = int(row.get("local_calls") or 0)
|
|
669
|
+
|
|
670
|
+
if known_cost is not None:
|
|
671
|
+
value = format_usd(known_cost, style="table")
|
|
672
|
+
if estimate_calls > 0:
|
|
673
|
+
value = f"~{value}"
|
|
674
|
+
suffixes: list[str] = []
|
|
675
|
+
if unknown_calls > 0:
|
|
676
|
+
suffixes.append(f"{unknown_calls} cost unknown")
|
|
677
|
+
if subscription_calls > 0:
|
|
678
|
+
suffixes.append("subscription")
|
|
679
|
+
if included_calls > 0:
|
|
680
|
+
suffixes.append("included")
|
|
681
|
+
if local_calls > 0:
|
|
682
|
+
suffixes.append("local")
|
|
683
|
+
if suffixes:
|
|
684
|
+
joiner = "+" if compact else ", "
|
|
685
|
+
value = f"{value} {joiner.join(suffixes)}"
|
|
686
|
+
return value
|
|
687
|
+
|
|
688
|
+
labels = [
|
|
689
|
+
label
|
|
690
|
+
for count, label in (
|
|
691
|
+
(subscription_calls, "subscription"),
|
|
692
|
+
(included_calls, "included"),
|
|
693
|
+
(local_calls, "local"),
|
|
694
|
+
)
|
|
695
|
+
if count > 0
|
|
696
|
+
]
|
|
697
|
+
if unknown_calls > 0:
|
|
698
|
+
labels.append("cost n/a" if compact else f"unknown ({unknown_calls} cost unknown)")
|
|
699
|
+
if labels:
|
|
700
|
+
return ("+" if compact else ", ").join(labels)
|
|
701
|
+
return "$0.0000"
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def _format_reported_token_total(
|
|
705
|
+
row: dict[str, Any],
|
|
706
|
+
*,
|
|
707
|
+
token_key: str,
|
|
708
|
+
reported_calls_key: str,
|
|
709
|
+
derived_calls_key: str | None = None,
|
|
710
|
+
) -> str:
|
|
711
|
+
"""Keep provider omission, a provider-reported zero, and a derived figure apart.
|
|
712
|
+
|
|
713
|
+
A derived figure is arithmetic over counts the provider did send, so it is
|
|
714
|
+
worth showing — but it is not itself reported, and labelling it as such
|
|
715
|
+
would overstate what the provider actually told us.
|
|
716
|
+
"""
|
|
717
|
+
|
|
718
|
+
total_calls = int(row.get("api_usage_calls") or 0) + int(row.get("estimate_usage_calls") or 0)
|
|
719
|
+
reported_calls = int(row.get(reported_calls_key) or 0)
|
|
720
|
+
derived_calls = int(row.get(derived_calls_key) or 0) if derived_calls_key else 0
|
|
721
|
+
known_calls = reported_calls + derived_calls
|
|
722
|
+
if total_calls <= 0 or known_calls <= 0:
|
|
723
|
+
return "not reported"
|
|
724
|
+
value = _format_exact_token_count(row.get(token_key))
|
|
725
|
+
if reported_calls <= 0:
|
|
726
|
+
suffix = " (derived)"
|
|
727
|
+
elif derived_calls > 0:
|
|
728
|
+
suffix = " (partly derived)"
|
|
729
|
+
else:
|
|
730
|
+
suffix = ""
|
|
731
|
+
if known_calls < total_calls:
|
|
732
|
+
return f"{value} (partial {known_calls}/{total_calls}){suffix}"
|
|
733
|
+
return f"{value}{suffix}"
|
|
734
|
+
|
|
735
|
+
|
|
736
|
+
def _format_usage_source_for_display(row: dict[str, Any]) -> str:
|
|
737
|
+
provider_calls = int(row.get("api_usage_calls") or 0)
|
|
738
|
+
fallback_calls = int(row.get("estimate_usage_calls") or 0)
|
|
739
|
+
corrected_calls = int(row.get("corrected_usage_calls") or 0)
|
|
740
|
+
parts = [f"provider {provider_calls}", f"fallback {fallback_calls}"]
|
|
741
|
+
if corrected_calls > 0:
|
|
742
|
+
parts.append(f"corrected {corrected_calls}")
|
|
743
|
+
return " | ".join(parts)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def _resolve_chat_mode_alias(value: str) -> str | None:
|
|
747
|
+
key = value.strip().lower()
|
|
748
|
+
return _CHAT_MODE_ALIASES.get(key)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def _resolve_trace_level(value: str) -> str | None:
|
|
752
|
+
normalized = value.strip().lower()
|
|
753
|
+
if normalized in _CHAT_TRACE_LEVELS:
|
|
754
|
+
return normalized
|
|
755
|
+
return None
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _chat_trace_level(session: Any) -> str:
|
|
759
|
+
surface = getattr(session, "surface", None)
|
|
760
|
+
level = getattr(surface, "trace_level", None)
|
|
761
|
+
if isinstance(level, str):
|
|
762
|
+
normalized = level.strip().lower()
|
|
763
|
+
if normalized in _CHAT_TRACE_LEVELS:
|
|
764
|
+
return normalized
|
|
765
|
+
return "compact"
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
def _record_chat_session_setting(*, session: Any, setting: str, value: Any) -> None:
|
|
769
|
+
store = getattr(session, "store", None)
|
|
770
|
+
append = getattr(store, "append", None)
|
|
771
|
+
if not callable(append):
|
|
772
|
+
return
|
|
773
|
+
try:
|
|
774
|
+
append(
|
|
775
|
+
"session_setting_changed",
|
|
776
|
+
{"setting": str(setting), "value": value},
|
|
777
|
+
)
|
|
778
|
+
except Exception:
|
|
779
|
+
pass
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _set_chat_stream_enabled(*, session: Any, enabled: bool) -> bool:
|
|
783
|
+
next_value = bool(enabled)
|
|
784
|
+
previous = bool(getattr(session, "stream", True))
|
|
785
|
+
session.stream = next_value
|
|
786
|
+
cfg = getattr(session, "cfg", None)
|
|
787
|
+
if cfg is not None:
|
|
788
|
+
cfg.stream = next_value
|
|
789
|
+
if next_value != previous:
|
|
790
|
+
_record_chat_session_setting(
|
|
791
|
+
session=session,
|
|
792
|
+
setting="stream",
|
|
793
|
+
value=next_value,
|
|
794
|
+
)
|
|
795
|
+
return next_value
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _session_skill_listing(session: Any) -> tuple[bool, tuple[Any, ...], tuple[Any, ...]]:
|
|
799
|
+
cfg = getattr(session, "cfg", None)
|
|
800
|
+
enabled = resolve_skills_enabled(cfg) if isinstance(cfg, AppConfig) else True
|
|
801
|
+
ordered_obj = getattr(session, "skills_ordered", None)
|
|
802
|
+
ordered = (
|
|
803
|
+
tuple(item for item in ordered_obj if item is not None)
|
|
804
|
+
if isinstance(ordered_obj, tuple)
|
|
805
|
+
else tuple(item for item in (ordered_obj or []) if item is not None)
|
|
806
|
+
)
|
|
807
|
+
registry_obj = getattr(session, "skill_registry", None)
|
|
808
|
+
if ordered or isinstance(registry_obj, dict):
|
|
809
|
+
issues = tuple(getattr(session, "skill_discovery_issues", ()) or ())
|
|
810
|
+
return enabled, ordered, issues
|
|
811
|
+
discovered = _discover_skills_for_path(path=Path(getattr(session, "root", Path("."))))
|
|
812
|
+
return enabled, tuple(discovered.ordered), tuple(discovered.issues)
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
def _set_chat_trace_level(*, session: Any, level: str) -> str:
|
|
816
|
+
normalized = _resolve_trace_level(level) or "compact"
|
|
817
|
+
previous = _chat_trace_level(session)
|
|
818
|
+
surface = getattr(session, "surface", None)
|
|
819
|
+
if surface is None:
|
|
820
|
+
return normalized
|
|
821
|
+
setter = getattr(surface, "set_trace_level", None)
|
|
822
|
+
if callable(setter):
|
|
823
|
+
applied = setter(normalized)
|
|
824
|
+
if isinstance(applied, str):
|
|
825
|
+
applied_normalized = _resolve_trace_level(applied)
|
|
826
|
+
if applied_normalized is not None:
|
|
827
|
+
if applied_normalized != previous:
|
|
828
|
+
_record_chat_session_setting(
|
|
829
|
+
session=session,
|
|
830
|
+
setting="trace_level",
|
|
831
|
+
value=applied_normalized,
|
|
832
|
+
)
|
|
833
|
+
return applied_normalized
|
|
834
|
+
return normalized
|
|
835
|
+
try:
|
|
836
|
+
surface.trace_level = normalized
|
|
837
|
+
except Exception: # noqa: BLE001
|
|
838
|
+
return normalized
|
|
839
|
+
applied = _resolve_trace_level(str(getattr(surface, "trace_level", normalized))) or normalized
|
|
840
|
+
if applied != previous:
|
|
841
|
+
_record_chat_session_setting(
|
|
842
|
+
session=session,
|
|
843
|
+
setting="trace_level",
|
|
844
|
+
value=applied,
|
|
845
|
+
)
|
|
846
|
+
return applied
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def _emit_plan_mode_trace(
|
|
850
|
+
*,
|
|
851
|
+
session: Any,
|
|
852
|
+
message: str,
|
|
853
|
+
full_only: bool = False,
|
|
854
|
+
source: str = "plan_mode",
|
|
855
|
+
) -> None:
|
|
856
|
+
clean = message.strip()
|
|
857
|
+
if not clean:
|
|
858
|
+
return
|
|
859
|
+
trace_level = _chat_trace_level(session)
|
|
860
|
+
if trace_level == "off":
|
|
861
|
+
return
|
|
862
|
+
if full_only and trace_level != "full":
|
|
863
|
+
return
|
|
864
|
+
|
|
865
|
+
store = getattr(session, "store", None)
|
|
866
|
+
append = getattr(store, "append", None)
|
|
867
|
+
if callable(append):
|
|
868
|
+
try:
|
|
869
|
+
append("progress", {"message": clean, "source": source})
|
|
870
|
+
except Exception: # noqa: BLE001
|
|
871
|
+
pass
|
|
872
|
+
|
|
873
|
+
surface = getattr(session, "surface", None)
|
|
874
|
+
handler = getattr(surface, "on_progress_update", None)
|
|
875
|
+
if callable(handler):
|
|
876
|
+
try:
|
|
877
|
+
handler(clean)
|
|
878
|
+
except Exception: # noqa: BLE001
|
|
879
|
+
return
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def _emit_forge_planner_trace(
|
|
883
|
+
*,
|
|
884
|
+
session: Any,
|
|
885
|
+
message: str,
|
|
886
|
+
full_only: bool = False,
|
|
887
|
+
) -> None:
|
|
888
|
+
_emit_plan_mode_trace(
|
|
889
|
+
session=session,
|
|
890
|
+
message=message,
|
|
891
|
+
full_only=full_only,
|
|
892
|
+
source="forge_planner",
|
|
893
|
+
)
|
|
894
|
+
|
|
895
|
+
|
|
896
|
+
def _make_forge_swarm_trace_sink(
|
|
897
|
+
*,
|
|
898
|
+
session: Any,
|
|
899
|
+
paths: Any,
|
|
900
|
+
console: Console,
|
|
901
|
+
) -> SerializedSwarmTraceSink:
|
|
902
|
+
surface = getattr(session, "surface", None)
|
|
903
|
+
store = getattr(session, "store", None)
|
|
904
|
+
return SerializedSwarmTraceSink(
|
|
905
|
+
artifact_path=Path(paths.execution_dir) / "trace" / "swarm_trace.jsonl",
|
|
906
|
+
trace_level=_chat_trace_level(session),
|
|
907
|
+
surface=surface,
|
|
908
|
+
session_store=store,
|
|
909
|
+
console=console,
|
|
910
|
+
store_source="forge_swarm",
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
def _make_plan_mode_delta_trace_callback(*, session: Any) -> Callable[[str], None] | None:
|
|
915
|
+
trace_level = _chat_trace_level(session)
|
|
916
|
+
if trace_level == "off":
|
|
917
|
+
return None
|
|
918
|
+
|
|
919
|
+
seen_chars = 0
|
|
920
|
+
started = False
|
|
921
|
+
last_full_bucket = -1
|
|
922
|
+
|
|
923
|
+
def _on_text_delta(delta: str) -> None:
|
|
924
|
+
nonlocal seen_chars, started, last_full_bucket
|
|
925
|
+
if not delta:
|
|
926
|
+
return
|
|
927
|
+
seen_chars += len(delta)
|
|
928
|
+
if not started:
|
|
929
|
+
_emit_plan_mode_trace(session=session, message="Receiving planner output...")
|
|
930
|
+
started = True
|
|
931
|
+
if trace_level != "full":
|
|
932
|
+
return
|
|
933
|
+
bucket = seen_chars // 320
|
|
934
|
+
if bucket <= last_full_bucket:
|
|
935
|
+
return
|
|
936
|
+
last_full_bucket = bucket
|
|
937
|
+
_emit_plan_mode_trace(
|
|
938
|
+
session=session,
|
|
939
|
+
message=f"Planner draft progress: ~{seen_chars} chars captured.",
|
|
940
|
+
full_only=True,
|
|
941
|
+
)
|
|
942
|
+
|
|
943
|
+
return _on_text_delta
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
__all__ = [name for name in globals() if (not name.startswith("__") or name == "__version__")]
|