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,65 @@
|
|
|
1
|
+
"""Per-turn controller intervention telemetry.
|
|
2
|
+
|
|
3
|
+
The headline target is controller_interventions_total ≈ 0 on clean successful turns.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections import Counter
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
HEADLINE_EXCLUDED_CLASSES = frozenset(
|
|
13
|
+
{
|
|
14
|
+
"context_setup",
|
|
15
|
+
"deadline_directive",
|
|
16
|
+
"safety_block",
|
|
17
|
+
}
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ControllerInterventionTracker:
|
|
22
|
+
def __init__(self, store: Any) -> None:
|
|
23
|
+
self._store = store
|
|
24
|
+
self._counts: Counter[str] = Counter()
|
|
25
|
+
self._headline_total = 0
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def headline_total(self) -> int:
|
|
29
|
+
return self._headline_total
|
|
30
|
+
|
|
31
|
+
def record(
|
|
32
|
+
self,
|
|
33
|
+
intervention_class: str,
|
|
34
|
+
detail: str,
|
|
35
|
+
*,
|
|
36
|
+
step: int | None = None,
|
|
37
|
+
metadata: Mapping[str, Any] | None = None,
|
|
38
|
+
headline_counted: bool | None = None,
|
|
39
|
+
) -> None:
|
|
40
|
+
normalized_class = str(intervention_class or "").strip() or "other"
|
|
41
|
+
normalized_detail = str(detail or "").strip() or "unspecified"
|
|
42
|
+
counted = (
|
|
43
|
+
normalized_class not in HEADLINE_EXCLUDED_CLASSES
|
|
44
|
+
if headline_counted is None
|
|
45
|
+
else bool(headline_counted)
|
|
46
|
+
)
|
|
47
|
+
self._counts[normalized_class] += 1
|
|
48
|
+
if counted:
|
|
49
|
+
self._headline_total += 1
|
|
50
|
+
payload: dict[str, Any] = {
|
|
51
|
+
"class": normalized_class,
|
|
52
|
+
"detail": normalized_detail,
|
|
53
|
+
"step": step,
|
|
54
|
+
"headline_counted": counted,
|
|
55
|
+
"controller_interventions_total": self._headline_total,
|
|
56
|
+
}
|
|
57
|
+
if metadata:
|
|
58
|
+
payload["metadata"] = dict(metadata)
|
|
59
|
+
self._store.append("controller_intervention", payload)
|
|
60
|
+
|
|
61
|
+
def payload(self) -> dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"total": self._headline_total,
|
|
64
|
+
"by_class": dict(sorted(self._counts.items())),
|
|
65
|
+
}
|
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import os
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..prompt_context import _normalize_repo_relative_hint_path
|
|
10
|
+
from ..tools_assembly import ToolDef
|
|
11
|
+
|
|
12
|
+
_SAME_BATCH_FS_READ_DEFAULT_MAX_BYTES = 20_000
|
|
13
|
+
_SAME_BATCH_FS_READ_LINES_DEFAULT_MAX_LINES = 200
|
|
14
|
+
# Mirrors tools.fs._DEFAULT_READ_LINES_MAX_BYTES: an absent max_bytes argument
|
|
15
|
+
# keys and caps identically to the tool's own default ceiling.
|
|
16
|
+
_SAME_BATCH_FS_READ_LINES_DEFAULT_MAX_BYTES = 48_000
|
|
17
|
+
_SAME_BATCH_READ_CACHE_SAFE_TOOL_NAMES = {
|
|
18
|
+
"fs_read",
|
|
19
|
+
"fs_read_lines",
|
|
20
|
+
"fs_list",
|
|
21
|
+
"web_fetch",
|
|
22
|
+
"web_search",
|
|
23
|
+
"symbol_search",
|
|
24
|
+
"test_discover",
|
|
25
|
+
"repo_map",
|
|
26
|
+
"search_rg",
|
|
27
|
+
"history_search",
|
|
28
|
+
"skill_read",
|
|
29
|
+
"git_status",
|
|
30
|
+
"git_diff",
|
|
31
|
+
"git_history",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class _SameBatchFsReadRecord:
|
|
37
|
+
path_key: str
|
|
38
|
+
raw_lines: tuple[str, ...]
|
|
39
|
+
result: dict[str, Any]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class _SameBatchFsReadLinesRecord:
|
|
44
|
+
path_key: str
|
|
45
|
+
start_line: int
|
|
46
|
+
end_line: int
|
|
47
|
+
total_lines: int | None
|
|
48
|
+
truncated: bool
|
|
49
|
+
include_line_numbers: bool
|
|
50
|
+
content_lines: tuple[str, ...]
|
|
51
|
+
result: dict[str, Any]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@dataclass
|
|
55
|
+
class _SameBatchReadReuseCache:
|
|
56
|
+
exact_fs_reads: dict[tuple[str, int, bool], dict[str, Any]] = field(default_factory=dict)
|
|
57
|
+
exact_fs_read_lines: dict[tuple[str, int, int | None, int, bool, int], dict[str, Any]] = field(
|
|
58
|
+
default_factory=dict
|
|
59
|
+
)
|
|
60
|
+
full_fs_reads: dict[str, _SameBatchFsReadRecord] = field(default_factory=dict)
|
|
61
|
+
fs_read_lines_by_path: dict[str, list[_SameBatchFsReadLinesRecord]] = field(
|
|
62
|
+
default_factory=dict
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def clear(self) -> None:
|
|
66
|
+
self.exact_fs_reads.clear()
|
|
67
|
+
self.exact_fs_read_lines.clear()
|
|
68
|
+
self.full_fs_reads.clear()
|
|
69
|
+
self.fs_read_lines_by_path.clear()
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _same_batch_read_path_key(*, root: Path, raw_path: Any) -> str | None:
|
|
73
|
+
normalized = _normalize_repo_relative_hint_path(root=root, raw=str(raw_path or ""))
|
|
74
|
+
if normalized:
|
|
75
|
+
return normalized
|
|
76
|
+
candidate = str(raw_path or "").strip().replace("\\", "/")
|
|
77
|
+
while candidate.startswith("./"):
|
|
78
|
+
candidate = candidate[2:]
|
|
79
|
+
if not candidate:
|
|
80
|
+
return None
|
|
81
|
+
normalized_candidate = os.path.normpath(candidate).replace("\\", "/")
|
|
82
|
+
if normalized_candidate in {"", ".", ".."} or normalized_candidate.startswith("../"):
|
|
83
|
+
return None
|
|
84
|
+
if normalized_candidate.startswith("/"):
|
|
85
|
+
normalized_candidate = normalized_candidate[1:]
|
|
86
|
+
return normalized_candidate or None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _split_text_preserving_lines(text: str) -> tuple[str, ...]:
|
|
90
|
+
if not text:
|
|
91
|
+
return ()
|
|
92
|
+
return tuple(text.splitlines(keepends=True))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _coerce_fs_read_request(
|
|
96
|
+
*,
|
|
97
|
+
root: Path,
|
|
98
|
+
arguments: dict[str, Any],
|
|
99
|
+
) -> tuple[str, str, int, bool] | None:
|
|
100
|
+
raw_path = str(arguments.get("path") or "")
|
|
101
|
+
path_key = _same_batch_read_path_key(root=root, raw_path=raw_path)
|
|
102
|
+
if not path_key:
|
|
103
|
+
return None
|
|
104
|
+
raw_max_bytes = arguments.get("max_bytes")
|
|
105
|
+
max_bytes = (
|
|
106
|
+
_SAME_BATCH_FS_READ_DEFAULT_MAX_BYTES if raw_max_bytes is None else int(raw_max_bytes)
|
|
107
|
+
)
|
|
108
|
+
return (
|
|
109
|
+
path_key,
|
|
110
|
+
raw_path,
|
|
111
|
+
max_bytes,
|
|
112
|
+
bool(arguments.get("allow_derived", False)),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _coerce_fs_read_lines_request(
|
|
117
|
+
*,
|
|
118
|
+
root: Path,
|
|
119
|
+
arguments: dict[str, Any],
|
|
120
|
+
) -> tuple[str, str, int, int | None, int, bool, int] | None:
|
|
121
|
+
raw_path = str(arguments.get("path") or "")
|
|
122
|
+
path_key = _same_batch_read_path_key(root=root, raw_path=raw_path)
|
|
123
|
+
if not path_key:
|
|
124
|
+
return None
|
|
125
|
+
start_line = int(arguments["start_line"]) if arguments.get("start_line") is not None else 0
|
|
126
|
+
requested_end_line = (
|
|
127
|
+
int(arguments["end_line"]) if arguments.get("end_line") is not None else None
|
|
128
|
+
)
|
|
129
|
+
max_lines = (
|
|
130
|
+
int(arguments["max_lines"])
|
|
131
|
+
if arguments.get("max_lines") is not None
|
|
132
|
+
else _SAME_BATCH_FS_READ_LINES_DEFAULT_MAX_LINES
|
|
133
|
+
)
|
|
134
|
+
include_line_numbers = bool(arguments.get("include_line_numbers", True))
|
|
135
|
+
raw_max_bytes = arguments.get("max_bytes")
|
|
136
|
+
if raw_max_bytes is None:
|
|
137
|
+
max_bytes = _SAME_BATCH_FS_READ_LINES_DEFAULT_MAX_BYTES
|
|
138
|
+
else:
|
|
139
|
+
max_bytes = int(raw_max_bytes)
|
|
140
|
+
if max_bytes < 1:
|
|
141
|
+
# Invalid ceiling: never serve it from cache - the real tool owns
|
|
142
|
+
# the FsError for out-of-range values.
|
|
143
|
+
return None
|
|
144
|
+
return (
|
|
145
|
+
path_key,
|
|
146
|
+
raw_path,
|
|
147
|
+
start_line,
|
|
148
|
+
requested_end_line,
|
|
149
|
+
max_lines,
|
|
150
|
+
include_line_numbers,
|
|
151
|
+
max_bytes,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _build_fs_read_lines_result_from_full_fs_read(
|
|
156
|
+
*,
|
|
157
|
+
raw_path: str,
|
|
158
|
+
request: tuple[str, str, int, int | None, int, bool, int],
|
|
159
|
+
record: _SameBatchFsReadRecord,
|
|
160
|
+
) -> dict[str, Any] | None:
|
|
161
|
+
_, _, start_line, requested_end_line, max_lines, include_line_numbers, max_bytes = request
|
|
162
|
+
total_lines = len(record.raw_lines)
|
|
163
|
+
if start_line < 1 or start_line > total_lines:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
effective_end_line = start_line + max_lines - 1
|
|
167
|
+
if requested_end_line is not None:
|
|
168
|
+
effective_end_line = min(effective_end_line, requested_end_line)
|
|
169
|
+
actual_end_line = min(effective_end_line, total_lines)
|
|
170
|
+
selected_lines = record.raw_lines[start_line - 1 : actual_end_line]
|
|
171
|
+
if not selected_lines:
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
if include_line_numbers:
|
|
175
|
+
content = "".join(
|
|
176
|
+
f"{lineno}: {line}"
|
|
177
|
+
for lineno, line in zip(
|
|
178
|
+
range(start_line, actual_end_line + 1), selected_lines, strict=False
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
content = "".join(selected_lines)
|
|
183
|
+
|
|
184
|
+
if len(content.encode("utf-8")) > max_bytes:
|
|
185
|
+
# The rebuilt window would exceed the request's byte ceiling; let the
|
|
186
|
+
# real tool run and apply its own clipping rather than replicate it.
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
"path": raw_path,
|
|
191
|
+
"start_line": start_line,
|
|
192
|
+
"end_line": actual_end_line,
|
|
193
|
+
"total_lines": total_lines if actual_end_line == total_lines else None,
|
|
194
|
+
"content": content,
|
|
195
|
+
"truncated": requested_end_line is None and actual_end_line < total_lines,
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _build_fs_read_lines_result_from_cached_range(
|
|
200
|
+
*,
|
|
201
|
+
raw_path: str,
|
|
202
|
+
request: tuple[str, str, int, int | None, int, bool, int],
|
|
203
|
+
record: _SameBatchFsReadLinesRecord,
|
|
204
|
+
) -> dict[str, Any] | None:
|
|
205
|
+
_, _, start_line, requested_end_line, max_lines, include_line_numbers, max_bytes = request
|
|
206
|
+
if start_line < record.start_line:
|
|
207
|
+
return None
|
|
208
|
+
if include_line_numbers != record.include_line_numbers:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
requested_max_end = start_line + max_lines - 1
|
|
212
|
+
if requested_end_line is not None:
|
|
213
|
+
requested_max_end = min(requested_max_end, requested_end_line)
|
|
214
|
+
|
|
215
|
+
if requested_end_line is not None:
|
|
216
|
+
if requested_max_end <= record.end_line:
|
|
217
|
+
actual_end_line = requested_max_end
|
|
218
|
+
elif (
|
|
219
|
+
record.total_lines is not None
|
|
220
|
+
and start_line <= record.total_lines
|
|
221
|
+
and record.end_line == record.total_lines
|
|
222
|
+
):
|
|
223
|
+
actual_end_line = record.total_lines
|
|
224
|
+
else:
|
|
225
|
+
return None
|
|
226
|
+
else:
|
|
227
|
+
if requested_max_end < record.end_line:
|
|
228
|
+
actual_end_line = requested_max_end
|
|
229
|
+
elif requested_max_end == record.end_line:
|
|
230
|
+
if record.total_lines is not None:
|
|
231
|
+
actual_end_line = requested_max_end
|
|
232
|
+
elif record.truncated:
|
|
233
|
+
actual_end_line = requested_max_end
|
|
234
|
+
else:
|
|
235
|
+
return None
|
|
236
|
+
elif (
|
|
237
|
+
record.total_lines is not None
|
|
238
|
+
and start_line <= record.total_lines
|
|
239
|
+
and record.end_line == record.total_lines
|
|
240
|
+
):
|
|
241
|
+
actual_end_line = record.total_lines
|
|
242
|
+
else:
|
|
243
|
+
return None
|
|
244
|
+
|
|
245
|
+
if actual_end_line < start_line:
|
|
246
|
+
return None
|
|
247
|
+
|
|
248
|
+
start_offset = start_line - record.start_line
|
|
249
|
+
end_offset = actual_end_line - record.start_line + 1
|
|
250
|
+
selected_lines = record.content_lines[start_offset:end_offset]
|
|
251
|
+
if len(selected_lines) != (actual_end_line - start_line + 1):
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
truncated = False
|
|
255
|
+
if requested_end_line is None:
|
|
256
|
+
if actual_end_line < record.end_line:
|
|
257
|
+
truncated = True
|
|
258
|
+
elif record.total_lines is not None:
|
|
259
|
+
truncated = actual_end_line < record.total_lines
|
|
260
|
+
else:
|
|
261
|
+
truncated = record.truncated
|
|
262
|
+
|
|
263
|
+
content = "".join(selected_lines)
|
|
264
|
+
if len(content.encode("utf-8")) > max_bytes:
|
|
265
|
+
# The rebuilt window would exceed the request's byte ceiling; let the
|
|
266
|
+
# real tool run and apply its own clipping rather than replicate it.
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
return {
|
|
270
|
+
"path": raw_path,
|
|
271
|
+
"start_line": start_line,
|
|
272
|
+
"end_line": actual_end_line,
|
|
273
|
+
"total_lines": (
|
|
274
|
+
record.total_lines
|
|
275
|
+
if record.total_lines is not None and actual_end_line == record.total_lines
|
|
276
|
+
else None
|
|
277
|
+
),
|
|
278
|
+
"content": content,
|
|
279
|
+
"truncated": truncated,
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _maybe_reuse_same_batch_read_result(
|
|
284
|
+
*,
|
|
285
|
+
root: Path,
|
|
286
|
+
cache: _SameBatchReadReuseCache,
|
|
287
|
+
tool_name: str,
|
|
288
|
+
arguments: dict[str, Any],
|
|
289
|
+
) -> dict[str, Any] | None:
|
|
290
|
+
if tool_name == "fs_read":
|
|
291
|
+
request = _coerce_fs_read_request(root=root, arguments=arguments)
|
|
292
|
+
if request is None:
|
|
293
|
+
return None
|
|
294
|
+
path_key, raw_path, max_bytes, allow_derived = request
|
|
295
|
+
cached = cache.exact_fs_reads.get((path_key, max_bytes, allow_derived))
|
|
296
|
+
if cached is None:
|
|
297
|
+
return None
|
|
298
|
+
reused = copy.deepcopy(cached)
|
|
299
|
+
reused["path"] = raw_path
|
|
300
|
+
return reused
|
|
301
|
+
|
|
302
|
+
if tool_name != "fs_read_lines":
|
|
303
|
+
return None
|
|
304
|
+
|
|
305
|
+
request = _coerce_fs_read_lines_request(root=root, arguments=arguments)
|
|
306
|
+
if request is None:
|
|
307
|
+
return None
|
|
308
|
+
(
|
|
309
|
+
path_key,
|
|
310
|
+
raw_path,
|
|
311
|
+
start_line,
|
|
312
|
+
requested_end_line,
|
|
313
|
+
max_lines,
|
|
314
|
+
include_line_numbers,
|
|
315
|
+
max_bytes,
|
|
316
|
+
) = request
|
|
317
|
+
cached_exact = cache.exact_fs_read_lines.get(
|
|
318
|
+
(path_key, start_line, requested_end_line, max_lines, include_line_numbers, max_bytes)
|
|
319
|
+
)
|
|
320
|
+
if cached_exact is not None:
|
|
321
|
+
reused = copy.deepcopy(cached_exact)
|
|
322
|
+
reused["path"] = raw_path
|
|
323
|
+
return reused
|
|
324
|
+
|
|
325
|
+
full_read = cache.full_fs_reads.get(path_key)
|
|
326
|
+
if full_read is not None:
|
|
327
|
+
reused = _build_fs_read_lines_result_from_full_fs_read(
|
|
328
|
+
raw_path=raw_path,
|
|
329
|
+
request=request,
|
|
330
|
+
record=full_read,
|
|
331
|
+
)
|
|
332
|
+
if reused is not None:
|
|
333
|
+
return reused
|
|
334
|
+
|
|
335
|
+
for record in reversed(cache.fs_read_lines_by_path.get(path_key, [])):
|
|
336
|
+
reused = _build_fs_read_lines_result_from_cached_range(
|
|
337
|
+
raw_path=raw_path,
|
|
338
|
+
request=request,
|
|
339
|
+
record=record,
|
|
340
|
+
)
|
|
341
|
+
if reused is not None:
|
|
342
|
+
return reused
|
|
343
|
+
return None
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def _remember_same_batch_read_result(
|
|
347
|
+
*,
|
|
348
|
+
root: Path,
|
|
349
|
+
cache: _SameBatchReadReuseCache,
|
|
350
|
+
tool_name: str,
|
|
351
|
+
arguments: dict[str, Any],
|
|
352
|
+
result: dict[str, Any],
|
|
353
|
+
) -> None:
|
|
354
|
+
if tool_name == "fs_read":
|
|
355
|
+
request = _coerce_fs_read_request(root=root, arguments=arguments)
|
|
356
|
+
if request is None:
|
|
357
|
+
return
|
|
358
|
+
path_key, _, max_bytes, allow_derived = request
|
|
359
|
+
stored = copy.deepcopy(result)
|
|
360
|
+
cache.exact_fs_reads[(path_key, max_bytes, allow_derived)] = stored
|
|
361
|
+
if bool(result.get("truncated")):
|
|
362
|
+
return
|
|
363
|
+
content = result.get("content")
|
|
364
|
+
if isinstance(content, str):
|
|
365
|
+
cache.full_fs_reads[path_key] = _SameBatchFsReadRecord(
|
|
366
|
+
path_key=path_key,
|
|
367
|
+
raw_lines=_split_text_preserving_lines(content),
|
|
368
|
+
result=stored,
|
|
369
|
+
)
|
|
370
|
+
return
|
|
371
|
+
|
|
372
|
+
if tool_name != "fs_read_lines":
|
|
373
|
+
return
|
|
374
|
+
request = _coerce_fs_read_lines_request(root=root, arguments=arguments)
|
|
375
|
+
if request is None:
|
|
376
|
+
return
|
|
377
|
+
(
|
|
378
|
+
path_key,
|
|
379
|
+
_,
|
|
380
|
+
start_line,
|
|
381
|
+
requested_end_line,
|
|
382
|
+
max_lines,
|
|
383
|
+
include_line_numbers,
|
|
384
|
+
max_bytes,
|
|
385
|
+
) = request
|
|
386
|
+
end_line = result.get("end_line")
|
|
387
|
+
content = result.get("content")
|
|
388
|
+
if not isinstance(end_line, int) or not isinstance(content, str):
|
|
389
|
+
return
|
|
390
|
+
if bool(result.get("byte_truncated")) or bool(result.get("line_clipped")):
|
|
391
|
+
# A byte-capped window may end mid-line; reusing it to answer other
|
|
392
|
+
# range requests would silently serve incomplete lines.
|
|
393
|
+
return
|
|
394
|
+
stored = copy.deepcopy(result)
|
|
395
|
+
cache.exact_fs_read_lines[
|
|
396
|
+
(path_key, start_line, requested_end_line, max_lines, include_line_numbers, max_bytes)
|
|
397
|
+
] = stored
|
|
398
|
+
cache.fs_read_lines_by_path.setdefault(path_key, []).append(
|
|
399
|
+
_SameBatchFsReadLinesRecord(
|
|
400
|
+
path_key=path_key,
|
|
401
|
+
start_line=start_line,
|
|
402
|
+
end_line=end_line,
|
|
403
|
+
total_lines=(
|
|
404
|
+
int(result["total_lines"]) if result.get("total_lines") is not None else None
|
|
405
|
+
),
|
|
406
|
+
truncated=bool(result.get("truncated")),
|
|
407
|
+
include_line_numbers=include_line_numbers,
|
|
408
|
+
content_lines=_split_text_preserving_lines(content),
|
|
409
|
+
result=stored,
|
|
410
|
+
)
|
|
411
|
+
)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def _same_batch_read_cache_should_invalidate(tool_name: str, tool: ToolDef | None) -> bool:
|
|
415
|
+
normalized = str(tool_name or "").strip()
|
|
416
|
+
if not normalized:
|
|
417
|
+
return True
|
|
418
|
+
if normalized not in _SAME_BATCH_READ_CACHE_SAFE_TOOL_NAMES:
|
|
419
|
+
return True
|
|
420
|
+
return tool is None
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
from collections.abc import Callable, Iterable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ...runtime_artifacts import is_runtime_artifact_path
|
|
10
|
+
from ..prompt_context import _normalize_repo_relative_hint_path
|
|
11
|
+
|
|
12
|
+
_SHELL_MUTATION_SNAPSHOT_METADATA_PREFIX = "meta"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _list_git_workspace_snapshot_paths(root: Path) -> set[str] | None:
|
|
16
|
+
try:
|
|
17
|
+
proc = subprocess.run(
|
|
18
|
+
[
|
|
19
|
+
"git",
|
|
20
|
+
"-C",
|
|
21
|
+
os.fspath(root),
|
|
22
|
+
"ls-files",
|
|
23
|
+
"-z",
|
|
24
|
+
"--cached",
|
|
25
|
+
"--others",
|
|
26
|
+
"--exclude-standard",
|
|
27
|
+
],
|
|
28
|
+
check=False,
|
|
29
|
+
capture_output=True,
|
|
30
|
+
)
|
|
31
|
+
except OSError:
|
|
32
|
+
return None
|
|
33
|
+
if proc.returncode != 0:
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
paths: set[str] = set()
|
|
37
|
+
stdout = proc.stdout or b""
|
|
38
|
+
raw_items: list[str | bytes]
|
|
39
|
+
if isinstance(stdout, str):
|
|
40
|
+
raw_items = stdout.split("\0")
|
|
41
|
+
else:
|
|
42
|
+
raw_items = stdout.split(b"\0")
|
|
43
|
+
for raw_item in raw_items:
|
|
44
|
+
if not raw_item:
|
|
45
|
+
continue
|
|
46
|
+
raw_path = (
|
|
47
|
+
raw_item
|
|
48
|
+
if isinstance(raw_item, str)
|
|
49
|
+
else raw_item.decode("utf-8", errors="surrogateescape")
|
|
50
|
+
)
|
|
51
|
+
normalized = _normalize_repo_relative_hint_path(
|
|
52
|
+
root=root,
|
|
53
|
+
raw=raw_path,
|
|
54
|
+
)
|
|
55
|
+
if not normalized or is_runtime_artifact_path(normalized, root=root):
|
|
56
|
+
continue
|
|
57
|
+
paths.add(normalized)
|
|
58
|
+
return paths
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _walk_workspace_snapshot_paths(root: Path) -> set[str]:
|
|
62
|
+
root_resolved = root.resolve()
|
|
63
|
+
paths: set[str] = set()
|
|
64
|
+
for current_root, dirnames, filenames in os.walk(root_resolved):
|
|
65
|
+
current_path = Path(current_root)
|
|
66
|
+
resolved_current = current_path.resolve()
|
|
67
|
+
rel_dir = (
|
|
68
|
+
resolved_current.relative_to(root_resolved).as_posix()
|
|
69
|
+
if resolved_current != root_resolved
|
|
70
|
+
else ""
|
|
71
|
+
)
|
|
72
|
+
kept_dirnames: list[str] = []
|
|
73
|
+
for dirname in dirnames:
|
|
74
|
+
rel_candidate = dirname if not rel_dir else f"{rel_dir}/{dirname}"
|
|
75
|
+
normalized = _normalize_repo_relative_hint_path(root=root, raw=rel_candidate)
|
|
76
|
+
if normalized and is_runtime_artifact_path(normalized, root=root):
|
|
77
|
+
continue
|
|
78
|
+
kept_dirnames.append(dirname)
|
|
79
|
+
dirnames[:] = kept_dirnames
|
|
80
|
+
for filename in filenames:
|
|
81
|
+
rel_candidate = filename if not rel_dir else f"{rel_dir}/{filename}"
|
|
82
|
+
normalized = _normalize_repo_relative_hint_path(root=root, raw=rel_candidate)
|
|
83
|
+
if not normalized or is_runtime_artifact_path(normalized, root=root):
|
|
84
|
+
continue
|
|
85
|
+
paths.add(normalized)
|
|
86
|
+
return paths
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _workspace_snapshot_signature(path: Path) -> str | None:
|
|
90
|
+
try:
|
|
91
|
+
stat = path.stat()
|
|
92
|
+
except OSError:
|
|
93
|
+
return None
|
|
94
|
+
if not path.is_file():
|
|
95
|
+
return None
|
|
96
|
+
ctime_ns = getattr(stat, "st_ctime_ns", int(stat.st_ctime * 1_000_000_000))
|
|
97
|
+
return (
|
|
98
|
+
f"{_SHELL_MUTATION_SNAPSHOT_METADATA_PREFIX}:{stat.st_size}:{stat.st_mtime_ns}:{ctime_ns}"
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _snapshot_workspace_for_command_mutation_detection(root: Path) -> dict[str, str]:
|
|
103
|
+
candidate_paths = _list_git_workspace_snapshot_paths(root)
|
|
104
|
+
if candidate_paths is None:
|
|
105
|
+
candidate_paths = _walk_workspace_snapshot_paths(root)
|
|
106
|
+
snapshot: dict[str, str] = {}
|
|
107
|
+
for rel_path in sorted(candidate_paths):
|
|
108
|
+
signature = _workspace_snapshot_signature(root / rel_path)
|
|
109
|
+
if signature is None:
|
|
110
|
+
continue
|
|
111
|
+
snapshot[rel_path] = signature
|
|
112
|
+
return snapshot
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _path_matches_snapshot_ignore(
|
|
116
|
+
rel_path: str,
|
|
117
|
+
*,
|
|
118
|
+
ignored_paths: set[str],
|
|
119
|
+
) -> bool:
|
|
120
|
+
return any(
|
|
121
|
+
rel_path == ignored or rel_path.startswith(f"{ignored}/") for ignored in ignored_paths
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _normalize_snapshot_ignore_paths(root: Path, paths: Iterable[Path]) -> set[str]:
|
|
126
|
+
ignored: set[str] = set()
|
|
127
|
+
resolved_root = root.resolve()
|
|
128
|
+
for path in paths:
|
|
129
|
+
try:
|
|
130
|
+
rel_path = path.resolve().relative_to(resolved_root)
|
|
131
|
+
except ValueError:
|
|
132
|
+
continue
|
|
133
|
+
normalized = _normalize_repo_relative_hint_path(root=root, raw=os.fspath(rel_path))
|
|
134
|
+
if normalized:
|
|
135
|
+
ignored.add(normalized)
|
|
136
|
+
return ignored
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _detect_command_mutation_paths(
|
|
140
|
+
*,
|
|
141
|
+
before: dict[str, str],
|
|
142
|
+
after: dict[str, str],
|
|
143
|
+
) -> list[str]:
|
|
144
|
+
changed = {
|
|
145
|
+
rel_path
|
|
146
|
+
for rel_path in (set(before) | set(after))
|
|
147
|
+
if before.get(rel_path) != after.get(rel_path)
|
|
148
|
+
}
|
|
149
|
+
return sorted(changed)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _run_with_command_mutation_detection(
|
|
153
|
+
*,
|
|
154
|
+
root: Path,
|
|
155
|
+
enabled: bool,
|
|
156
|
+
ignored_paths: Iterable[Path] = (),
|
|
157
|
+
operation: Callable[[], Any],
|
|
158
|
+
) -> tuple[Any, list[str]]:
|
|
159
|
+
if not enabled:
|
|
160
|
+
return operation(), []
|
|
161
|
+
ignored = _normalize_snapshot_ignore_paths(root, ignored_paths)
|
|
162
|
+
before_snapshot = {
|
|
163
|
+
rel_path: signature
|
|
164
|
+
for rel_path, signature in _snapshot_workspace_for_command_mutation_detection(root).items()
|
|
165
|
+
if not _path_matches_snapshot_ignore(rel_path, ignored_paths=ignored)
|
|
166
|
+
}
|
|
167
|
+
result = operation()
|
|
168
|
+
after_snapshot = {
|
|
169
|
+
rel_path: signature
|
|
170
|
+
for rel_path, signature in _snapshot_workspace_for_command_mutation_detection(root).items()
|
|
171
|
+
if not _path_matches_snapshot_ignore(rel_path, ignored_paths=ignored)
|
|
172
|
+
}
|
|
173
|
+
return (
|
|
174
|
+
result,
|
|
175
|
+
_detect_command_mutation_paths(
|
|
176
|
+
before=before_snapshot,
|
|
177
|
+
after=after_snapshot,
|
|
178
|
+
),
|
|
179
|
+
)
|