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,514 @@
|
|
|
1
|
+
"""Thread-safe conversation model for the full-screen TUI.
|
|
2
|
+
|
|
3
|
+
The agent turn runs on a worker thread and pushes content here (streamed
|
|
4
|
+
assistant tokens, tool-trace lines, errors); the prompt_toolkit UI thread reads
|
|
5
|
+
snapshots of it on every redraw. All mutation goes through a lock and triggers a
|
|
6
|
+
(thread-safe) ``invalidate`` so the next redraw picks up the change.
|
|
7
|
+
|
|
8
|
+
Kept free of any agent imports so it can be unit-tested in isolation.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import threading
|
|
14
|
+
import time
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
|
|
17
|
+
from .forge_status import forge_status_bucket, forge_status_is_terminal
|
|
18
|
+
|
|
19
|
+
# (role, text) — role drives styling in the app: "user" | "assistant" | "reasoning"
|
|
20
|
+
# | "trace" | "error" | "warn" | "info" | "system".
|
|
21
|
+
Entry = tuple[str, str]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _normalize_visible(text: str) -> str:
|
|
25
|
+
"""Whitespace-collapsed comparison key for assistant text, so a re-emitted
|
|
26
|
+
reply that differs only in incidental spacing still reads as a duplicate."""
|
|
27
|
+
return " ".join(str(text or "").split())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class TuiTranscript:
|
|
31
|
+
def __init__(self, *, invalidate: Callable[[], None] | None = None) -> None:
|
|
32
|
+
self.entries: list[Entry] = []
|
|
33
|
+
self._lock = threading.RLock()
|
|
34
|
+
self._status: str | None = None
|
|
35
|
+
self._assistant_index: int | None = None
|
|
36
|
+
# Live provider-generated reasoning-summary block: index of the open
|
|
37
|
+
# entry, when it started, and elapsed seconds for each closed block.
|
|
38
|
+
self._reasoning_index: int | None = None
|
|
39
|
+
self._reasoning_start: float | None = None
|
|
40
|
+
self._reasoning_block_id: str | None = None
|
|
41
|
+
self._reasoning_block_ids: dict[int, str] = {}
|
|
42
|
+
self._reasoning_secs: dict[int, int] = {}
|
|
43
|
+
self._trace_level = "compact"
|
|
44
|
+
# Live Forge execution view: a task table + phase/spinner rendered at the
|
|
45
|
+
# bottom of the transcript while ``/execute plan`` runs the swarm on a
|
|
46
|
+
# worker thread. ``None`` when no run is active. Shape:
|
|
47
|
+
# {"run_id", "tasks": [{"id","title","status"}], "active": str|None,
|
|
48
|
+
# "active_map": {task_id: {"started": float, "phase": str,
|
|
49
|
+
# "message": str}},
|
|
50
|
+
# "phase": str, "message": str, "started": float, "done": bool}
|
|
51
|
+
# ``active`` is the MOST-RECENT task the swarm touched (drives the single
|
|
52
|
+
# bottom phase line, kept for back-compat); ``active_map`` tracks EVERY
|
|
53
|
+
# currently-running task so the renderer can spin them all and show a
|
|
54
|
+
# per-task elapsed timer. A task is pruned from ``active_map`` the moment
|
|
55
|
+
# its status turns terminal (done/failed/obsolete).
|
|
56
|
+
self._forge: dict[str, object] | None = None
|
|
57
|
+
self._invalidate: Callable[[], None] = invalidate or (lambda: None)
|
|
58
|
+
|
|
59
|
+
def set_invalidate(self, fn: Callable[[], None]) -> None:
|
|
60
|
+
self._invalidate = fn
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def trace_level(self) -> str:
|
|
64
|
+
with self._lock:
|
|
65
|
+
return self._trace_level
|
|
66
|
+
|
|
67
|
+
def set_trace_level(self, level: str) -> str:
|
|
68
|
+
normalized = str(level or "").strip().lower()
|
|
69
|
+
with self._lock:
|
|
70
|
+
if normalized not in {"off", "compact", "full"}:
|
|
71
|
+
normalized = self._trace_level
|
|
72
|
+
self._trace_level = normalized
|
|
73
|
+
self._touch()
|
|
74
|
+
return normalized
|
|
75
|
+
|
|
76
|
+
def _touch(self) -> None:
|
|
77
|
+
try:
|
|
78
|
+
self._invalidate()
|
|
79
|
+
except Exception:
|
|
80
|
+
pass
|
|
81
|
+
|
|
82
|
+
# ---- safe provider reasoning-summary block ----
|
|
83
|
+
def _close_reasoning_locked(self) -> None:
|
|
84
|
+
"""Close the open reasoning block, recording its elapsed seconds. Caller
|
|
85
|
+
holds the lock. Any content/tool/turn boundary collapses the summary."""
|
|
86
|
+
if self._reasoning_index is not None:
|
|
87
|
+
if self._reasoning_start is not None:
|
|
88
|
+
self._reasoning_secs[self._reasoning_index] = max(
|
|
89
|
+
0, int(time.monotonic() - self._reasoning_start)
|
|
90
|
+
)
|
|
91
|
+
self._reasoning_index = None
|
|
92
|
+
self._reasoning_start = None
|
|
93
|
+
self._reasoning_block_id = None
|
|
94
|
+
|
|
95
|
+
def begin_reasoning(self, block_id: str) -> None:
|
|
96
|
+
"""Open a safe-summary block with a stable provider-call identifier.
|
|
97
|
+
|
|
98
|
+
Reusing the active identifier is idempotent. A different identifier
|
|
99
|
+
closes the previous block first so retries and later agent steps can
|
|
100
|
+
never merge their summaries merely because no visible tool line landed
|
|
101
|
+
between the two provider calls.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
normalized = str(block_id or "").strip()
|
|
105
|
+
if not normalized:
|
|
106
|
+
return
|
|
107
|
+
with self._lock:
|
|
108
|
+
if self._reasoning_index is not None and self._reasoning_block_id == normalized:
|
|
109
|
+
return
|
|
110
|
+
self._close_reasoning_locked()
|
|
111
|
+
self.entries.append(("reasoning", ""))
|
|
112
|
+
self._reasoning_index = len(self.entries) - 1
|
|
113
|
+
self._reasoning_start = time.monotonic()
|
|
114
|
+
self._reasoning_block_id = normalized
|
|
115
|
+
self._reasoning_block_ids[self._reasoning_index] = normalized
|
|
116
|
+
self._touch()
|
|
117
|
+
|
|
118
|
+
def end_reasoning(self, block_id: str | None = None) -> None:
|
|
119
|
+
"""Collapse the open summary block (e.g. when a tool starts) without
|
|
120
|
+
appending anything — records its elapsed seconds for the summary."""
|
|
121
|
+
with self._lock:
|
|
122
|
+
normalized = str(block_id or "").strip()
|
|
123
|
+
if normalized and normalized != self._reasoning_block_id:
|
|
124
|
+
return
|
|
125
|
+
self._close_reasoning_locked()
|
|
126
|
+
self._touch()
|
|
127
|
+
|
|
128
|
+
def stream_reasoning(self, delta: str) -> None:
|
|
129
|
+
if not delta:
|
|
130
|
+
return
|
|
131
|
+
with self._lock:
|
|
132
|
+
if self._reasoning_index is None:
|
|
133
|
+
self.entries.append(("reasoning", ""))
|
|
134
|
+
self._reasoning_index = len(self.entries) - 1
|
|
135
|
+
self._reasoning_start = time.monotonic()
|
|
136
|
+
self._reasoning_block_id = f"legacy-{self._reasoning_index}"
|
|
137
|
+
self._reasoning_block_ids[self._reasoning_index] = self._reasoning_block_id
|
|
138
|
+
role, current = self.entries[self._reasoning_index]
|
|
139
|
+
self.entries[self._reasoning_index] = (role, current + delta)
|
|
140
|
+
self._touch()
|
|
141
|
+
|
|
142
|
+
# ---- generic appends ----
|
|
143
|
+
def append(self, role: str, text: str) -> None:
|
|
144
|
+
with self._lock:
|
|
145
|
+
self._close_reasoning_locked()
|
|
146
|
+
self.entries.append((role, text))
|
|
147
|
+
# Any non-streamed line closes the open assistant block.
|
|
148
|
+
if role != "assistant":
|
|
149
|
+
self._assistant_index = None
|
|
150
|
+
self._touch()
|
|
151
|
+
|
|
152
|
+
def append_trace(self, line: str) -> None:
|
|
153
|
+
"""Append a trace line, coalescing into the previous trace block.
|
|
154
|
+
|
|
155
|
+
Consecutive trace lines (e.g. a stream of swarm-progress events) join into
|
|
156
|
+
one multi-line entry instead of becoming separate blocks — the renderer
|
|
157
|
+
puts a blank spacer *between* entries, so without coalescing a busy run
|
|
158
|
+
would be sprayed across the pane."""
|
|
159
|
+
with self._lock:
|
|
160
|
+
self._close_reasoning_locked()
|
|
161
|
+
if self.entries and self.entries[-1][0] == "trace":
|
|
162
|
+
role, current = self.entries[-1]
|
|
163
|
+
self.entries[-1] = (role, f"{current}\n{line}")
|
|
164
|
+
else:
|
|
165
|
+
self.entries.append(("trace", line))
|
|
166
|
+
self._assistant_index = None
|
|
167
|
+
self._touch()
|
|
168
|
+
|
|
169
|
+
def append_user(self, text: str) -> None:
|
|
170
|
+
with self._lock:
|
|
171
|
+
self._close_reasoning_locked()
|
|
172
|
+
self.entries.append(("user", text))
|
|
173
|
+
self._assistant_index = None
|
|
174
|
+
# A fresh submission ends the prior live Forge view (its final table
|
|
175
|
+
# stays in scrollback only while it is the latest thing shown).
|
|
176
|
+
self._forge = None
|
|
177
|
+
self._touch()
|
|
178
|
+
|
|
179
|
+
# ---- streaming assistant block ----
|
|
180
|
+
def begin_turn(self) -> None:
|
|
181
|
+
with self._lock:
|
|
182
|
+
self._close_reasoning_locked()
|
|
183
|
+
self._assistant_index = None
|
|
184
|
+
self._status = None
|
|
185
|
+
self._touch()
|
|
186
|
+
|
|
187
|
+
def stream_assistant(self, delta: str) -> None:
|
|
188
|
+
if not delta:
|
|
189
|
+
return
|
|
190
|
+
with self._lock:
|
|
191
|
+
# The first answer token collapses the reasoning block.
|
|
192
|
+
self._close_reasoning_locked()
|
|
193
|
+
if self._assistant_index is None:
|
|
194
|
+
self.entries.append(("assistant", ""))
|
|
195
|
+
self._assistant_index = len(self.entries) - 1
|
|
196
|
+
role, current = self.entries[self._assistant_index]
|
|
197
|
+
self.entries[self._assistant_index] = (role, current + delta)
|
|
198
|
+
self._status = None
|
|
199
|
+
self._touch()
|
|
200
|
+
|
|
201
|
+
def restart_assistant(self) -> None:
|
|
202
|
+
"""Clear the open streamed block: a transport retry is about to
|
|
203
|
+
restream the reply from scratch, so the abandoned generation's tokens
|
|
204
|
+
must not remain in front of it."""
|
|
205
|
+
with self._lock:
|
|
206
|
+
if self._assistant_index is not None:
|
|
207
|
+
role, _current = self.entries[self._assistant_index]
|
|
208
|
+
self.entries[self._assistant_index] = (role, "")
|
|
209
|
+
self._touch()
|
|
210
|
+
|
|
211
|
+
def finish_assistant(self, text: str = "") -> None:
|
|
212
|
+
with self._lock:
|
|
213
|
+
self._close_reasoning_locked()
|
|
214
|
+
if self._assistant_index is not None:
|
|
215
|
+
role, current = self.entries[self._assistant_index]
|
|
216
|
+
if not current and text:
|
|
217
|
+
self.entries[self._assistant_index] = (role, text)
|
|
218
|
+
elif current and text:
|
|
219
|
+
cur_norm = _normalize_visible(current)
|
|
220
|
+
txt_norm = _normalize_visible(text)
|
|
221
|
+
if (
|
|
222
|
+
cur_norm != txt_norm
|
|
223
|
+
and cur_norm.endswith(txt_norm)
|
|
224
|
+
and len(txt_norm) * 4 >= len(cur_norm)
|
|
225
|
+
):
|
|
226
|
+
# A provider-level retry restreamed the reply into the
|
|
227
|
+
# same live block: a mid-stream failure abandons
|
|
228
|
+
# generation A after its tokens are already visible,
|
|
229
|
+
# the transport retry re-runs the request, and
|
|
230
|
+
# generation B streams into the same open block — the
|
|
231
|
+
# block ends up "A + B" (wordings can differ, so the
|
|
232
|
+
# verbatim dedupe below never catches it). The final
|
|
233
|
+
# text is authoritative — it is exactly what the
|
|
234
|
+
# session history keeps — so when the block ends with
|
|
235
|
+
# it and carries an abandoned prefix, snap to the
|
|
236
|
+
# final. The >=25% length guard keeps a pathological
|
|
237
|
+
# tail-fragment final from truncating a legit block.
|
|
238
|
+
self.entries[self._assistant_index] = (role, text)
|
|
239
|
+
elif text.strip():
|
|
240
|
+
# A multi-step turn can re-emit the same answer after its live
|
|
241
|
+
# block was already closed by an intervening tool/trace line (which
|
|
242
|
+
# reset _assistant_index). Only append a fresh block when it is NOT
|
|
243
|
+
# a verbatim repeat of the turn's most recent assistant block —
|
|
244
|
+
# otherwise the same reply stacks up 2-3 times. Content-based and
|
|
245
|
+
# provider-agnostic (see also the render-layer collapse in app.py).
|
|
246
|
+
last = self._last_turn_assistant_text_locked()
|
|
247
|
+
if last is None or _normalize_visible(last) != _normalize_visible(text):
|
|
248
|
+
self.entries.append(("assistant", text))
|
|
249
|
+
self._assistant_index = None
|
|
250
|
+
self._status = None
|
|
251
|
+
self._touch()
|
|
252
|
+
|
|
253
|
+
def _last_turn_assistant_text_locked(self) -> str | None:
|
|
254
|
+
"""Text of the most recent assistant entry within the current turn, or
|
|
255
|
+
``None`` if none precedes a ``user`` boundary. Caller holds the lock."""
|
|
256
|
+
for role, text in reversed(self.entries):
|
|
257
|
+
if role == "user":
|
|
258
|
+
return None
|
|
259
|
+
if role == "assistant":
|
|
260
|
+
return text
|
|
261
|
+
return None
|
|
262
|
+
|
|
263
|
+
# ---- transient status (working line) ----
|
|
264
|
+
def set_status(self, status: str | None) -> None:
|
|
265
|
+
with self._lock:
|
|
266
|
+
self._status = (status or None) if status is None else (status.strip() or None)
|
|
267
|
+
self._touch()
|
|
268
|
+
|
|
269
|
+
@property
|
|
270
|
+
def status(self) -> str | None:
|
|
271
|
+
with self._lock:
|
|
272
|
+
return self._status
|
|
273
|
+
|
|
274
|
+
# ---- live Forge execution view ----
|
|
275
|
+
def forge_begin(self, run_id: str, tasks: list[tuple[str, str, str]]) -> None:
|
|
276
|
+
"""Open the live Forge execution view with the initial task list.
|
|
277
|
+
|
|
278
|
+
``tasks`` is a list of ``(id, title, status)``; statuses then refresh as
|
|
279
|
+
the swarm runs (``forge_update_statuses``) and on completion
|
|
280
|
+
(``forge_finish``)."""
|
|
281
|
+
with self._lock:
|
|
282
|
+
self._forge = {
|
|
283
|
+
"run_id": str(run_id or ""),
|
|
284
|
+
"tasks": [
|
|
285
|
+
{"id": str(tid), "title": str(title), "status": str(status or "planned")}
|
|
286
|
+
for tid, title, status in tasks
|
|
287
|
+
],
|
|
288
|
+
"active": None,
|
|
289
|
+
"active_map": {},
|
|
290
|
+
"phase": "execute",
|
|
291
|
+
"message": "Starting…",
|
|
292
|
+
"started": time.monotonic(),
|
|
293
|
+
"done": False,
|
|
294
|
+
"ok": True,
|
|
295
|
+
}
|
|
296
|
+
self._touch()
|
|
297
|
+
|
|
298
|
+
def _forge_reconcile_active_map_locked(self) -> None:
|
|
299
|
+
"""Keep ``active_map`` in step with the current task statuses.
|
|
300
|
+
|
|
301
|
+
Adds a per-task start stamp when a task enters a running state (so a
|
|
302
|
+
status-driven run — not just an event-driven one — gets a live timer),
|
|
303
|
+
and prunes any task whose status has turned terminal so a finished row
|
|
304
|
+
stops spinning. Caller holds the lock."""
|
|
305
|
+
if self._forge is None:
|
|
306
|
+
return
|
|
307
|
+
active_map: dict = self._forge["active_map"] # type: ignore[assignment]
|
|
308
|
+
for task in self._forge["tasks"]: # type: ignore[index]
|
|
309
|
+
tid = task["id"]
|
|
310
|
+
status = str(task.get("status") or "")
|
|
311
|
+
if forge_status_is_terminal(status):
|
|
312
|
+
active_map.pop(tid, None)
|
|
313
|
+
elif forge_status_bucket(status) == "running" and tid not in active_map:
|
|
314
|
+
active_map[tid] = {"started": time.monotonic(), "phase": "", "message": ""}
|
|
315
|
+
|
|
316
|
+
def forge_update_statuses(self, statuses: dict[str, str]) -> None:
|
|
317
|
+
"""Update task statuses (keyed by task id) from the latest plan snapshot."""
|
|
318
|
+
with self._lock:
|
|
319
|
+
if self._forge is None:
|
|
320
|
+
return
|
|
321
|
+
for task in self._forge["tasks"]: # type: ignore[index]
|
|
322
|
+
new_status = statuses.get(task["id"])
|
|
323
|
+
if new_status:
|
|
324
|
+
task["status"] = str(new_status)
|
|
325
|
+
self._forge_reconcile_active_map_locked()
|
|
326
|
+
self._touch()
|
|
327
|
+
|
|
328
|
+
def forge_sync_tasks(self, tasks: list[tuple[str, str, str]]) -> None:
|
|
329
|
+
"""Reconcile the live view with the plan on disk: update existing task
|
|
330
|
+
statuses AND append any new tasks (e.g. ones plan enrichment added mid-run),
|
|
331
|
+
preserving order so the table never silently drops a real task."""
|
|
332
|
+
with self._lock:
|
|
333
|
+
if self._forge is None:
|
|
334
|
+
return
|
|
335
|
+
existing = {task["id"]: task for task in self._forge["tasks"]} # type: ignore[index]
|
|
336
|
+
for tid, title, status in tasks:
|
|
337
|
+
row = existing.get(str(tid))
|
|
338
|
+
if row is not None:
|
|
339
|
+
if status:
|
|
340
|
+
row["status"] = str(status)
|
|
341
|
+
else:
|
|
342
|
+
new_row = {
|
|
343
|
+
"id": str(tid),
|
|
344
|
+
"title": str(title),
|
|
345
|
+
"status": str(status or "planned"),
|
|
346
|
+
}
|
|
347
|
+
self._forge["tasks"].append(new_row) # type: ignore[union-attr]
|
|
348
|
+
existing[str(tid)] = new_row
|
|
349
|
+
self._forge_reconcile_active_map_locked()
|
|
350
|
+
self._touch()
|
|
351
|
+
|
|
352
|
+
def forge_set_active(self, task_id: str | None, phase: str = "", message: str = "") -> None:
|
|
353
|
+
"""Mark the task the swarm is currently working on, plus the phase/message
|
|
354
|
+
shown on the live status line."""
|
|
355
|
+
with self._lock:
|
|
356
|
+
if self._forge is None:
|
|
357
|
+
return
|
|
358
|
+
if task_id is not None:
|
|
359
|
+
tid = str(task_id)
|
|
360
|
+
self._forge["active"] = tid
|
|
361
|
+
# Upsert into the live worker set: a first sighting stamps its
|
|
362
|
+
# start (for the per-task timer); later events only refresh the
|
|
363
|
+
# phase/message. A terminal task is never (re)added — a late event
|
|
364
|
+
# after completion must not resurrect a finished row.
|
|
365
|
+
active_map: dict = self._forge["active_map"] # type: ignore[assignment]
|
|
366
|
+
if not forge_status_is_terminal(self._forge_task_status_locked(tid)):
|
|
367
|
+
entry = active_map.get(tid)
|
|
368
|
+
if entry is None:
|
|
369
|
+
entry = {"started": time.monotonic(), "phase": "", "message": ""}
|
|
370
|
+
active_map[tid] = entry
|
|
371
|
+
if phase:
|
|
372
|
+
entry["phase"] = str(phase)
|
|
373
|
+
if message:
|
|
374
|
+
entry["message"] = str(message)
|
|
375
|
+
if phase:
|
|
376
|
+
self._forge["phase"] = str(phase)
|
|
377
|
+
if message:
|
|
378
|
+
self._forge["message"] = str(message)
|
|
379
|
+
self._touch()
|
|
380
|
+
|
|
381
|
+
def _forge_task_status_locked(self, task_id: str) -> str:
|
|
382
|
+
"""Current status of ``task_id`` in the live view ('' if unknown).
|
|
383
|
+
Caller holds the lock."""
|
|
384
|
+
if self._forge is None:
|
|
385
|
+
return ""
|
|
386
|
+
for task in self._forge["tasks"]: # type: ignore[index]
|
|
387
|
+
if task["id"] == task_id:
|
|
388
|
+
return str(task.get("status") or "")
|
|
389
|
+
return ""
|
|
390
|
+
|
|
391
|
+
def forge_finish(self, statuses: dict[str, str], summary: str = "", ok: bool = True) -> None:
|
|
392
|
+
"""Apply final task statuses and mark the run done (table stays visible).
|
|
393
|
+
|
|
394
|
+
``ok`` is the overall outcome (no failed/remaining tasks) — it colours the
|
|
395
|
+
summary line green vs red so it never contradicts the per-task glyphs."""
|
|
396
|
+
with self._lock:
|
|
397
|
+
if self._forge is None:
|
|
398
|
+
return
|
|
399
|
+
for task in self._forge["tasks"]: # type: ignore[index]
|
|
400
|
+
new_status = statuses.get(task["id"])
|
|
401
|
+
if new_status:
|
|
402
|
+
task["status"] = str(new_status)
|
|
403
|
+
self._forge["active"] = None
|
|
404
|
+
self._forge["active_map"] = {} # run over: no live workers
|
|
405
|
+
self._forge["done"] = True
|
|
406
|
+
self._forge["ok"] = bool(ok)
|
|
407
|
+
self._forge["phase"] = "done"
|
|
408
|
+
if summary:
|
|
409
|
+
self._forge["message"] = str(summary)
|
|
410
|
+
self._touch()
|
|
411
|
+
|
|
412
|
+
def forge_clear(self) -> None:
|
|
413
|
+
with self._lock:
|
|
414
|
+
self._forge = None
|
|
415
|
+
self._touch()
|
|
416
|
+
|
|
417
|
+
def forge_snapshot(self) -> dict[str, object] | None:
|
|
418
|
+
"""Return a copy of the live Forge view, or ``None`` when inactive."""
|
|
419
|
+
with self._lock:
|
|
420
|
+
if self._forge is None:
|
|
421
|
+
return None
|
|
422
|
+
return {
|
|
423
|
+
"run_id": self._forge["run_id"],
|
|
424
|
+
"tasks": [dict(task) for task in self._forge["tasks"]], # type: ignore[union-attr]
|
|
425
|
+
"active": self._forge["active"],
|
|
426
|
+
"active_map": {
|
|
427
|
+
tid: dict(info)
|
|
428
|
+
for tid, info in self._forge["active_map"].items() # type: ignore[union-attr]
|
|
429
|
+
},
|
|
430
|
+
"phase": self._forge["phase"],
|
|
431
|
+
"message": self._forge["message"],
|
|
432
|
+
"started": self._forge["started"],
|
|
433
|
+
"done": self._forge["done"],
|
|
434
|
+
"ok": self._forge.get("ok", True),
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
def clear(self) -> None:
|
|
438
|
+
with self._lock:
|
|
439
|
+
self.entries.clear()
|
|
440
|
+
self._assistant_index = None
|
|
441
|
+
self._reasoning_index = None
|
|
442
|
+
self._reasoning_start = None
|
|
443
|
+
self._reasoning_block_id = None
|
|
444
|
+
self._reasoning_block_ids.clear()
|
|
445
|
+
self._reasoning_secs.clear()
|
|
446
|
+
self._status = None
|
|
447
|
+
self._forge = None
|
|
448
|
+
self._touch()
|
|
449
|
+
|
|
450
|
+
def load_history(self, messages: list[dict[str, object]]) -> None:
|
|
451
|
+
"""Replace the transcript with a resumed conversation's history.
|
|
452
|
+
|
|
453
|
+
Clears every transient bit (like :meth:`clear`) and repopulates the pane
|
|
454
|
+
from ``messages`` (the loaded history of a ``/resume`` target) so the user
|
|
455
|
+
sees the prior conversation after switching sessions. Only ``user`` and
|
|
456
|
+
``assistant`` turns with text content are shown — tool calls/results are
|
|
457
|
+
omitted so the reloaded view reads as the clean conversation. Because no
|
|
458
|
+
assistant block stays "open" (``_assistant_index`` is reset), every
|
|
459
|
+
assistant entry renders as a completed (markdown) reply.
|
|
460
|
+
"""
|
|
461
|
+
with self._lock:
|
|
462
|
+
self.entries.clear()
|
|
463
|
+
self._assistant_index = None
|
|
464
|
+
self._reasoning_index = None
|
|
465
|
+
self._reasoning_start = None
|
|
466
|
+
self._reasoning_block_id = None
|
|
467
|
+
self._reasoning_block_ids.clear()
|
|
468
|
+
self._reasoning_secs.clear()
|
|
469
|
+
self._status = None
|
|
470
|
+
self._forge = None
|
|
471
|
+
for message in messages or []:
|
|
472
|
+
if not isinstance(message, dict):
|
|
473
|
+
continue
|
|
474
|
+
role = str(message.get("role") or "").strip().lower()
|
|
475
|
+
if role not in ("user", "assistant"):
|
|
476
|
+
continue
|
|
477
|
+
content = message.get("content")
|
|
478
|
+
if not isinstance(content, str):
|
|
479
|
+
continue
|
|
480
|
+
text = content.rstrip()
|
|
481
|
+
if not text:
|
|
482
|
+
continue
|
|
483
|
+
self.entries.append((role, text))
|
|
484
|
+
self._touch()
|
|
485
|
+
|
|
486
|
+
def reasoning_snapshot(self) -> tuple[int | None, dict[int, int]]:
|
|
487
|
+
"""Return ``(live_reasoning_index, {entry_index: elapsed_seconds})``.
|
|
488
|
+
|
|
489
|
+
``live_reasoning_index`` is the still-streaming reasoning entry (rendered
|
|
490
|
+
in full, dim); the map gives the duration of each closed block so the
|
|
491
|
+
renderer can collapse it to a one-line "thought for Ns" summary.
|
|
492
|
+
"""
|
|
493
|
+
with self._lock:
|
|
494
|
+
return self._reasoning_index, dict(self._reasoning_secs)
|
|
495
|
+
|
|
496
|
+
def reasoning_block_ids(self) -> dict[int, str]:
|
|
497
|
+
"""Return stable identifiers for safe-summary transcript entries."""
|
|
498
|
+
|
|
499
|
+
with self._lock:
|
|
500
|
+
return dict(self._reasoning_block_ids)
|
|
501
|
+
|
|
502
|
+
def snapshot(self) -> tuple[list[Entry], str | None, int | None]:
|
|
503
|
+
"""Return ``(entries, status, streaming_index)`` under the lock.
|
|
504
|
+
|
|
505
|
+
``streaming_index`` is the index of the still-streaming assistant entry
|
|
506
|
+
(``None`` when no block is open), so the renderer keeps that one plain and
|
|
507
|
+
only markdown-renders completed replies — read atomically here to avoid a
|
|
508
|
+
race with a worker finishing the block mid-redraw.
|
|
509
|
+
"""
|
|
510
|
+
with self._lock:
|
|
511
|
+
return list(self.entries), self._status, self._assistant_index
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
__all__ = ["TuiTranscript", "Entry"]
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""TUI-native startup prompt for a newer Alysis Code release.
|
|
2
|
+
|
|
3
|
+
Shown at launch — before the first-run setup wizard and the guarded-workspace
|
|
4
|
+
picker — whenever the cached update check knows about a newer PyPI release the
|
|
5
|
+
user has not skipped or snoozed. Rendered as its own short-lived full-screen
|
|
6
|
+
``Application`` via the shared :func:`_run_option_picker`, so it matches the
|
|
7
|
+
visual grammar of the setup + workspace-guard screens.
|
|
8
|
+
|
|
9
|
+
Degrades gracefully: any terminal / prompt_toolkit failure returns
|
|
10
|
+
``(None, False)`` so the caller can fall back to the classic inline prompt,
|
|
11
|
+
exactly like the workspace-guard pickers do.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from .workspace_guard import _run_option_picker
|
|
19
|
+
|
|
20
|
+
UPDATE_ACTION_UPDATE = "update"
|
|
21
|
+
UPDATE_ACTION_LATER = "later"
|
|
22
|
+
UPDATE_ACTION_SKIP = "skip"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def select_update_action(
|
|
26
|
+
*,
|
|
27
|
+
current_version: str,
|
|
28
|
+
latest_version: str,
|
|
29
|
+
command: str | None = None,
|
|
30
|
+
unsupported_reason: str | None = None,
|
|
31
|
+
input: Any | None = None,
|
|
32
|
+
output: Any | None = None,
|
|
33
|
+
) -> tuple[str | None, bool]:
|
|
34
|
+
"""Ask whether to apply a newer release now.
|
|
35
|
+
|
|
36
|
+
Returns ``(action, interactive_available)`` where ``action`` is one of
|
|
37
|
+
``"update"`` / ``"later"`` / ``"skip"``, or ``None`` when the user cancels
|
|
38
|
+
(Esc / Ctrl-C — treated as "later" by the caller). Focus starts on
|
|
39
|
+
"Remind me later" so a reflexive Enter never runs an installer — matching
|
|
40
|
+
the classic prompt's ``default="n"`` and the `alysis update` flow's
|
|
41
|
+
``typer.confirm(default=False)``.
|
|
42
|
+
"""
|
|
43
|
+
if command:
|
|
44
|
+
update_label = "Update now"
|
|
45
|
+
update_desc = f"Runs: {command}"
|
|
46
|
+
else:
|
|
47
|
+
# No runnable command for this install: the row shows how to update
|
|
48
|
+
# manually instead of promising an automatic one.
|
|
49
|
+
update_label = "Show update instructions"
|
|
50
|
+
update_desc = unsupported_reason or "Shows how to update this install."
|
|
51
|
+
rows = [
|
|
52
|
+
(UPDATE_ACTION_UPDATE, update_label, update_desc),
|
|
53
|
+
(
|
|
54
|
+
UPDATE_ACTION_LATER,
|
|
55
|
+
"Remind me later",
|
|
56
|
+
"Keep this version for now; ask again on a later launch.",
|
|
57
|
+
),
|
|
58
|
+
(
|
|
59
|
+
UPDATE_ACTION_SKIP,
|
|
60
|
+
f"Skip version {latest_version}",
|
|
61
|
+
"Don't ask again for this release.",
|
|
62
|
+
),
|
|
63
|
+
]
|
|
64
|
+
return _run_option_picker(
|
|
65
|
+
subtitle="update",
|
|
66
|
+
title=f"Alysis Code {latest_version} is available — you have {current_version}",
|
|
67
|
+
rows=rows,
|
|
68
|
+
default_index=1,
|
|
69
|
+
input=input,
|
|
70
|
+
output=output,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"UPDATE_ACTION_LATER",
|
|
76
|
+
"UPDATE_ACTION_SKIP",
|
|
77
|
+
"UPDATE_ACTION_UPDATE",
|
|
78
|
+
"select_update_action",
|
|
79
|
+
]
|