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,355 @@
|
|
|
1
|
+
"""Persistent-service support: non-interactive defaults, probes, finalize check.
|
|
2
|
+
|
|
3
|
+
Two independent reliability problems live here because both are about a command
|
|
4
|
+
that outlives -- or stalls -- the tool call that started it.
|
|
5
|
+
|
|
6
|
+
*Persistence.* A process the agent starts normally shares the agent's process
|
|
7
|
+
group, and that group is exactly what gets signalled at turn finalization and
|
|
8
|
+
session close. A service started that way is guaranteed to be dead by the time
|
|
9
|
+
an external verifier looks for it. Surviving means leaving the group: its own
|
|
10
|
+
session, no controlling terminal, stdio on files rather than pipes nobody will
|
|
11
|
+
drain.
|
|
12
|
+
|
|
13
|
+
*Non-interaction.* Nobody is at the keyboard. A command that stops to ask a
|
|
14
|
+
question holds its pipe open until the hard kill and spends the rest of the run
|
|
15
|
+
answering nothing.
|
|
16
|
+
|
|
17
|
+
Stdlib only, no package imports: the agent pulls this in from the tool layer and
|
|
18
|
+
the turn controller, and the tests load it straight from this file path in a
|
|
19
|
+
bare interpreter.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import errno
|
|
25
|
+
import os
|
|
26
|
+
import re
|
|
27
|
+
import socket
|
|
28
|
+
import subprocess
|
|
29
|
+
from collections.abc import Callable, Iterable
|
|
30
|
+
from dataclasses import dataclass
|
|
31
|
+
from typing import Any
|
|
32
|
+
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
# Non-interactive shell defaults
|
|
35
|
+
# ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
# The standard opt-outs for the three ecosystems that prompt most often. apt
|
|
38
|
+
# asks about config files and service restarts; git asks for credentials on a
|
|
39
|
+
# private remote; pip asks before overwriting. Each one is a silent stall.
|
|
40
|
+
NON_INTERACTIVE_ENV_DEFAULTS: dict[str, str] = {
|
|
41
|
+
"DEBIAN_FRONTEND": "noninteractive",
|
|
42
|
+
"GIT_TERMINAL_PROMPT": "0",
|
|
43
|
+
"PIP_NO_INPUT": "1",
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def apply_non_interactive_defaults(env: dict[str, str]) -> dict[str, str]:
|
|
48
|
+
"""Fill in the non-interactive defaults ``env`` does not already set.
|
|
49
|
+
|
|
50
|
+
Never overrides. A task that deliberately exports ``DEBIAN_FRONTEND=dialog``
|
|
51
|
+
keeps it -- these are defaults for the unconfigured case, not policy.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
merged = dict(env)
|
|
55
|
+
for key, value in NON_INTERACTIVE_ENV_DEFAULTS.items():
|
|
56
|
+
if key not in merged:
|
|
57
|
+
merged[key] = value
|
|
58
|
+
return merged
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Detached spawn
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def persist_spawn_kwargs() -> dict[str, Any]:
|
|
67
|
+
"""Popen kwargs that move the child out of the agent's process group.
|
|
68
|
+
|
|
69
|
+
This is the whole of persistence. ``start_new_session`` makes the child a
|
|
70
|
+
session and group leader, so a ``killpg`` aimed at the agent's group cannot
|
|
71
|
+
reach it, and it has no controlling terminal to be hung up on. Windows has
|
|
72
|
+
no sessions; a new process group is the closest equivalent and at least
|
|
73
|
+
detaches the child from console Ctrl-C.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
77
|
+
return {"creationflags": subprocess.CREATE_NEW_PROCESS_GROUP}
|
|
78
|
+
return {"start_new_session": True}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
# Liveness probes
|
|
83
|
+
# ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
LOOPBACK_HOST = "127.0.0.1"
|
|
86
|
+
DEFAULT_PROBE_TIMEOUT_S = 0.5
|
|
87
|
+
_MIN_PORT = 1
|
|
88
|
+
_MAX_PORT = 65535
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def valid_port(port: object) -> bool:
|
|
92
|
+
return isinstance(port, int) and not isinstance(port, bool) and _MIN_PORT <= port <= _MAX_PORT
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def probe_tcp_port(
|
|
96
|
+
port: object,
|
|
97
|
+
*,
|
|
98
|
+
host: str = LOOPBACK_HOST,
|
|
99
|
+
timeout_s: float = DEFAULT_PROBE_TIMEOUT_S,
|
|
100
|
+
) -> bool:
|
|
101
|
+
"""True when something accepts a TCP connection on ``port``.
|
|
102
|
+
|
|
103
|
+
Deliberately the cheapest possible check: connect, close. It answers the
|
|
104
|
+
only question that matters to a verifier about to curl the port.
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
if not valid_port(port):
|
|
108
|
+
return False
|
|
109
|
+
try:
|
|
110
|
+
with socket.create_connection(
|
|
111
|
+
(host, int(port)), # type: ignore[arg-type]
|
|
112
|
+
timeout=max(0.01, float(timeout_s)),
|
|
113
|
+
):
|
|
114
|
+
return True
|
|
115
|
+
except OSError:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def describe_port_probe(port: object, listening: bool) -> str:
|
|
120
|
+
if not valid_port(port):
|
|
121
|
+
return ""
|
|
122
|
+
number = int(port) # type: ignore[call-overload]
|
|
123
|
+
return f"listening on :{number}" if listening else f"nothing listening on :{number}"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def pid_alive(pid: object) -> bool:
|
|
127
|
+
"""True when ``pid`` names a live process.
|
|
128
|
+
|
|
129
|
+
``os.kill(pid, 0)`` is the POSIX existence probe. It is *not* one on
|
|
130
|
+
Windows, where signal 0 still terminates the target, so that platform takes
|
|
131
|
+
the OpenProcess route instead.
|
|
132
|
+
"""
|
|
133
|
+
|
|
134
|
+
if not isinstance(pid, int) or isinstance(pid, bool) or pid <= 0:
|
|
135
|
+
return False
|
|
136
|
+
if os.name == "nt": # pragma: no cover - exercised on Windows
|
|
137
|
+
return _windows_pid_alive(pid)
|
|
138
|
+
try:
|
|
139
|
+
os.kill(pid, 0)
|
|
140
|
+
except OSError as exc:
|
|
141
|
+
if exc.errno == errno.EPERM:
|
|
142
|
+
# Alive, just not ours to signal.
|
|
143
|
+
return True
|
|
144
|
+
return False
|
|
145
|
+
return True
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _windows_pid_alive(pid: int) -> bool: # pragma: no cover - exercised on Windows
|
|
149
|
+
try:
|
|
150
|
+
import ctypes
|
|
151
|
+
from ctypes import wintypes
|
|
152
|
+
except Exception:
|
|
153
|
+
return False
|
|
154
|
+
process_query_limited_information = 0x1000
|
|
155
|
+
still_active = 259
|
|
156
|
+
kernel32 = ctypes.windll.kernel32 # type: ignore[attr-defined]
|
|
157
|
+
handle = kernel32.OpenProcess(process_query_limited_information, False, int(pid))
|
|
158
|
+
if not handle:
|
|
159
|
+
return False
|
|
160
|
+
try:
|
|
161
|
+
exit_code = wintypes.DWORD()
|
|
162
|
+
if not kernel32.GetExitCodeProcess(handle, ctypes.byref(exit_code)):
|
|
163
|
+
return False
|
|
164
|
+
return int(exit_code.value) == still_active
|
|
165
|
+
finally:
|
|
166
|
+
kernel32.CloseHandle(handle)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# ---------------------------------------------------------------------------
|
|
170
|
+
# Port inference
|
|
171
|
+
# ---------------------------------------------------------------------------
|
|
172
|
+
|
|
173
|
+
# First match wins, so the explicit forms come before the positional ones. This
|
|
174
|
+
# is a convenience heuristic only: an explicit probe_port argument always wins,
|
|
175
|
+
# and a miss costs nothing beyond a pid-only liveness line.
|
|
176
|
+
_PORT_PATTERNS: tuple[re.Pattern[str], ...] = (
|
|
177
|
+
re.compile(r"--port[=\s]+(\d{1,5})"),
|
|
178
|
+
re.compile(r"\bPORT=(\d{1,5})"),
|
|
179
|
+
# docker-style publish: the host port is the one a verifier can reach.
|
|
180
|
+
re.compile(r"\s-p[=\s]+(\d{1,5}):\d{1,5}"),
|
|
181
|
+
re.compile(r"\s-p[=\s]+(\d{1,5})\b"),
|
|
182
|
+
# 0.0.0.0:8000, localhost:3000, :5000
|
|
183
|
+
re.compile(r"(?:\d{1,3}(?:\.\d{1,3}){3}|localhost)?:(\d{1,5})\b"),
|
|
184
|
+
re.compile(r"\bport\s*=\s*(\d{1,5})", re.IGNORECASE),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def parse_probe_port(cmd: str) -> int | None:
|
|
189
|
+
"""Best-effort port extraction from a service command line."""
|
|
190
|
+
|
|
191
|
+
text = str(cmd or "")
|
|
192
|
+
if not text.strip():
|
|
193
|
+
return None
|
|
194
|
+
for pattern in _PORT_PATTERNS:
|
|
195
|
+
match = pattern.search(text)
|
|
196
|
+
if match is None:
|
|
197
|
+
continue
|
|
198
|
+
try:
|
|
199
|
+
candidate = int(match.group(1))
|
|
200
|
+
except (TypeError, ValueError): # pragma: no cover - regex guarantees digits
|
|
201
|
+
continue
|
|
202
|
+
if valid_port(candidate):
|
|
203
|
+
return candidate
|
|
204
|
+
return None
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def resolve_probe_port(*, requested: object, cmd: str) -> int | None:
|
|
208
|
+
"""An explicit ``probe_port`` wins; otherwise infer one from the command."""
|
|
209
|
+
|
|
210
|
+
if requested is None or (isinstance(requested, str) and not requested.strip()):
|
|
211
|
+
return parse_probe_port(cmd)
|
|
212
|
+
try:
|
|
213
|
+
candidate = int(requested) # type: ignore[call-overload]
|
|
214
|
+
except (TypeError, ValueError):
|
|
215
|
+
raise ValueError("probe_port must be an integer between 1 and 65535") from None
|
|
216
|
+
if isinstance(requested, bool) or not valid_port(candidate):
|
|
217
|
+
raise ValueError("probe_port must be an integer between 1 and 65535")
|
|
218
|
+
return candidate
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def readiness_spec_for_port(
|
|
222
|
+
port: int,
|
|
223
|
+
*,
|
|
224
|
+
host: str = LOOPBACK_HOST,
|
|
225
|
+
timeout_s: float = 5.0,
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""A readiness spec the durable-service manager already understands."""
|
|
228
|
+
|
|
229
|
+
return {"type": "tcp", "host": host, "port": int(port), "timeout_s": float(timeout_s)}
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
# Registry
|
|
234
|
+
# ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
@dataclass(frozen=True)
|
|
238
|
+
class PersistentServiceRecord:
|
|
239
|
+
"""One persist-mode start, as remembered for the finalization re-check."""
|
|
240
|
+
|
|
241
|
+
service_id: str
|
|
242
|
+
command: str
|
|
243
|
+
pid: int
|
|
244
|
+
probe_port: int | None = None
|
|
245
|
+
|
|
246
|
+
def summary(self) -> str:
|
|
247
|
+
label = " ".join(str(self.command or "").split()) or self.service_id
|
|
248
|
+
if len(label) > 120:
|
|
249
|
+
label = f"{label[:117]}..."
|
|
250
|
+
if self.probe_port is not None:
|
|
251
|
+
return f"{label} (pid {self.pid}, port {self.probe_port})"
|
|
252
|
+
return f"{label} (pid {self.pid})"
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
@dataclass(frozen=True)
|
|
256
|
+
class LivenessReport:
|
|
257
|
+
pid_alive: bool
|
|
258
|
+
probe_port: int | None = None
|
|
259
|
+
port_listening: bool | None = None
|
|
260
|
+
|
|
261
|
+
@property
|
|
262
|
+
def healthy(self) -> bool:
|
|
263
|
+
if not self.pid_alive:
|
|
264
|
+
return False
|
|
265
|
+
if self.probe_port is None:
|
|
266
|
+
return True
|
|
267
|
+
return bool(self.port_listening)
|
|
268
|
+
|
|
269
|
+
def describe(self) -> str:
|
|
270
|
+
parts = ["pid alive" if self.pid_alive else "pid not running"]
|
|
271
|
+
if self.probe_port is not None:
|
|
272
|
+
parts.append(describe_port_probe(self.probe_port, bool(self.port_listening)))
|
|
273
|
+
return "; ".join(part for part in parts if part)
|
|
274
|
+
|
|
275
|
+
def as_payload(self) -> dict[str, Any]:
|
|
276
|
+
payload: dict[str, Any] = {"pid_alive": self.pid_alive, "liveness": self.describe()}
|
|
277
|
+
if self.probe_port is not None:
|
|
278
|
+
payload["probe_port"] = self.probe_port
|
|
279
|
+
payload["port_listening"] = bool(self.port_listening)
|
|
280
|
+
return payload
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
PidProbe = Callable[[object], bool]
|
|
284
|
+
PortProbe = Callable[[object], bool]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def check_service(
|
|
288
|
+
record: PersistentServiceRecord,
|
|
289
|
+
*,
|
|
290
|
+
pid_probe: PidProbe = pid_alive,
|
|
291
|
+
port_probe: PortProbe = probe_tcp_port,
|
|
292
|
+
) -> LivenessReport:
|
|
293
|
+
alive = bool(pid_probe(record.pid))
|
|
294
|
+
if record.probe_port is None:
|
|
295
|
+
return LivenessReport(pid_alive=alive)
|
|
296
|
+
# A dead pid cannot be serving; skip the connect and its timeout.
|
|
297
|
+
listening = bool(port_probe(record.probe_port)) if alive else False
|
|
298
|
+
return LivenessReport(pid_alive=alive, probe_port=record.probe_port, port_listening=listening)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class PersistentServiceRegistry:
|
|
302
|
+
"""Session-scoped record of persist-mode starts, in start order."""
|
|
303
|
+
|
|
304
|
+
def __init__(self) -> None:
|
|
305
|
+
self._records: dict[str, PersistentServiceRecord] = {}
|
|
306
|
+
|
|
307
|
+
def register(self, record: PersistentServiceRecord) -> None:
|
|
308
|
+
self._records[record.service_id] = record
|
|
309
|
+
|
|
310
|
+
def forget(self, service_id: object) -> None:
|
|
311
|
+
self._records.pop(str(service_id), None)
|
|
312
|
+
|
|
313
|
+
def records(self) -> tuple[PersistentServiceRecord, ...]:
|
|
314
|
+
return tuple(self._records.values())
|
|
315
|
+
|
|
316
|
+
def __len__(self) -> int:
|
|
317
|
+
return len(self._records)
|
|
318
|
+
|
|
319
|
+
def __bool__(self) -> bool:
|
|
320
|
+
return bool(self._records)
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
# ---------------------------------------------------------------------------
|
|
324
|
+
# Finalization re-check
|
|
325
|
+
# ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
# The single model-visible string this module introduces. Kept as one constant
|
|
328
|
+
# so it is greppable and cannot drift.
|
|
329
|
+
SERVICE_CHECK_NOTICE_TEMPLATE = (
|
|
330
|
+
"Service check: process started as a persistent service is no longer running: "
|
|
331
|
+
"{summary}. Restart it or note why it is not needed."
|
|
332
|
+
)
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def finalize_service_notice(
|
|
336
|
+
records: Iterable[PersistentServiceRecord],
|
|
337
|
+
*,
|
|
338
|
+
pid_probe: PidProbe = pid_alive,
|
|
339
|
+
port_probe: PortProbe = probe_tcp_port,
|
|
340
|
+
) -> str | None:
|
|
341
|
+
"""One notice covering every unhealthy service, or None when all are fine.
|
|
342
|
+
|
|
343
|
+
Deliberately a single message no matter how many services died: the point is
|
|
344
|
+
to tell the model something needs attention, not to spend the remaining
|
|
345
|
+
budget enumerating.
|
|
346
|
+
"""
|
|
347
|
+
|
|
348
|
+
unhealthy: list[str] = []
|
|
349
|
+
for record in records:
|
|
350
|
+
report = check_service(record, pid_probe=pid_probe, port_probe=port_probe)
|
|
351
|
+
if not report.healthy:
|
|
352
|
+
unhealthy.append(f"{record.summary()} - {report.describe()}")
|
|
353
|
+
if not unhealthy:
|
|
354
|
+
return None
|
|
355
|
+
return SERVICE_CHECK_NOTICE_TEMPLATE.format(summary="; ".join(unhealthy))
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path, PurePosixPath
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def normalize_artifact_parts(parts: tuple[str, ...]) -> list[str]:
|
|
8
|
+
normalized: list[str] = []
|
|
9
|
+
for raw_part in parts:
|
|
10
|
+
text = str(raw_part).strip().replace("\\", "/")
|
|
11
|
+
if not text:
|
|
12
|
+
raise ValueError("artifact path part cannot be empty")
|
|
13
|
+
candidate = PurePosixPath(text)
|
|
14
|
+
if candidate.is_absolute():
|
|
15
|
+
raise ValueError("artifact path part must be relative")
|
|
16
|
+
for segment in candidate.parts:
|
|
17
|
+
if segment in {"", "."}:
|
|
18
|
+
continue
|
|
19
|
+
if segment == "..":
|
|
20
|
+
raise ValueError("artifact path cannot traverse upward")
|
|
21
|
+
normalized.append(segment)
|
|
22
|
+
if not normalized:
|
|
23
|
+
raise ValueError("artifact path cannot be empty")
|
|
24
|
+
return normalized
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _path_is_under_root(*, path: Path, root: Path | None) -> bool:
|
|
28
|
+
if root is None:
|
|
29
|
+
return False
|
|
30
|
+
try:
|
|
31
|
+
path.resolve().relative_to(root.resolve())
|
|
32
|
+
except ValueError:
|
|
33
|
+
return False
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class ModelArtifactReference:
|
|
39
|
+
locator: str
|
|
40
|
+
artifact_readable_via_fs: bool
|
|
41
|
+
artifact_location: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class SessionArtifactLayout:
|
|
46
|
+
filesystem_root: Path
|
|
47
|
+
locator_prefix: str = "session_artifacts"
|
|
48
|
+
|
|
49
|
+
def artifact_fs_path(self, *parts: str) -> Path:
|
|
50
|
+
return self.filesystem_root.joinpath(*normalize_artifact_parts(parts))
|
|
51
|
+
|
|
52
|
+
def artifact_locator(self, *parts: str) -> str:
|
|
53
|
+
locator_parts = normalize_artifact_parts((self.locator_prefix, *parts))
|
|
54
|
+
return "/".join(locator_parts)
|
|
55
|
+
|
|
56
|
+
def locator_for_path(self, artifact_path: Path) -> str:
|
|
57
|
+
rel = artifact_path.resolve().relative_to(self.filesystem_root.resolve()).as_posix()
|
|
58
|
+
return self.artifact_locator(rel)
|
|
59
|
+
|
|
60
|
+
def resolve_locator(self, locator: str) -> Path:
|
|
61
|
+
"""Resolve one logical locator inside this session's artifact root."""
|
|
62
|
+
|
|
63
|
+
text = str(locator or "").strip()
|
|
64
|
+
if not text:
|
|
65
|
+
raise ValueError("locator cannot be empty")
|
|
66
|
+
if "\\" in text or text.startswith("/"):
|
|
67
|
+
raise ValueError("locator must be a relative POSIX locator")
|
|
68
|
+
segments = text.split("/")
|
|
69
|
+
if any(segment in {"", ".", ".."} for segment in segments):
|
|
70
|
+
raise ValueError("locator contains an invalid path segment")
|
|
71
|
+
prefix = normalize_artifact_parts((self.locator_prefix,))
|
|
72
|
+
if segments[: len(prefix)] != prefix or len(segments) == len(prefix):
|
|
73
|
+
raise ValueError(f"locator must start with {self.artifact_locator('x')[:-1]}")
|
|
74
|
+
|
|
75
|
+
root = self.filesystem_root.resolve()
|
|
76
|
+
candidate = root.joinpath(*segments[len(prefix) :]).resolve()
|
|
77
|
+
try:
|
|
78
|
+
candidate.relative_to(root)
|
|
79
|
+
except ValueError as exc:
|
|
80
|
+
raise ValueError("locator resolves outside the session artifact root") from exc
|
|
81
|
+
return candidate
|
|
82
|
+
|
|
83
|
+
def model_reference_for_path(
|
|
84
|
+
self,
|
|
85
|
+
*,
|
|
86
|
+
artifact_path: Path,
|
|
87
|
+
workspace_root: Path | None,
|
|
88
|
+
) -> ModelArtifactReference:
|
|
89
|
+
readable_via_fs = _path_is_under_root(path=artifact_path, root=workspace_root)
|
|
90
|
+
return ModelArtifactReference(
|
|
91
|
+
locator=self.locator_for_path(artifact_path),
|
|
92
|
+
artifact_readable_via_fs=readable_via_fs,
|
|
93
|
+
artifact_location="workspace_root" if readable_via_fs else "external_session_store",
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def display_reference_for_path(
|
|
97
|
+
self,
|
|
98
|
+
*,
|
|
99
|
+
artifact_path: Path,
|
|
100
|
+
workspace_root: Path | None,
|
|
101
|
+
) -> str:
|
|
102
|
+
model_reference = self.model_reference_for_path(
|
|
103
|
+
artifact_path=artifact_path,
|
|
104
|
+
workspace_root=workspace_root,
|
|
105
|
+
)
|
|
106
|
+
if model_reference.artifact_readable_via_fs and workspace_root is not None:
|
|
107
|
+
return artifact_path.resolve().relative_to(workspace_root.resolve()).as_posix()
|
|
108
|
+
return model_reference.locator
|