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,521 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import logging
|
|
5
|
+
import threading
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from dataclasses import dataclass, field, replace
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any, Literal
|
|
12
|
+
|
|
13
|
+
from ..config import AppConfig
|
|
14
|
+
from ..forge import RunPaths
|
|
15
|
+
from ..model_registry import ModelRegistry
|
|
16
|
+
from .comprehender import AssetComprehender
|
|
17
|
+
from .index import AssetIndex
|
|
18
|
+
from .ingestion import ingest_asset
|
|
19
|
+
from .models import (
|
|
20
|
+
AssetError,
|
|
21
|
+
AssetNotFoundError,
|
|
22
|
+
AssetRecord,
|
|
23
|
+
ComprehensionRecord,
|
|
24
|
+
)
|
|
25
|
+
from .ocr import OcrProvider, TesseractOcrProvider
|
|
26
|
+
|
|
27
|
+
LOGGER = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
SurfaceComprehensionStatus = Literal["pending", "running", "ready", "failed", "minimal"]
|
|
30
|
+
_ASSET_RUN_PATH_ATTRS: tuple[str, ...] = (
|
|
31
|
+
"asset_store_dir",
|
|
32
|
+
"assets_index_path",
|
|
33
|
+
"assets_index_lock_path",
|
|
34
|
+
"assets_raw_dir",
|
|
35
|
+
"assets_extracted_dir",
|
|
36
|
+
"assets_comprehensions_dir",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class AssetSurfaceEntry:
|
|
42
|
+
record: AssetRecord
|
|
43
|
+
comprehension_status: SurfaceComprehensionStatus
|
|
44
|
+
comprehension_source: str | None
|
|
45
|
+
comprehension_summary_preview: str
|
|
46
|
+
detected_language: str | None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class AssetSurfaceDetail:
|
|
51
|
+
record: AssetRecord
|
|
52
|
+
comprehension_status: SurfaceComprehensionStatus
|
|
53
|
+
comprehension: ComprehensionRecord | None
|
|
54
|
+
versions: list[int]
|
|
55
|
+
extracted_text_preview: str
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@dataclass(frozen=True)
|
|
59
|
+
class AssetSurfaceAddResult:
|
|
60
|
+
record: AssetRecord
|
|
61
|
+
comprehension_handle: ComprehensionRefreshHandle | None
|
|
62
|
+
comprehension_record: ComprehensionRecord | None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass
|
|
66
|
+
class ComprehensionRefreshHandle:
|
|
67
|
+
asset_id: str
|
|
68
|
+
thread: threading.Thread | None
|
|
69
|
+
started_at: datetime
|
|
70
|
+
cancellation_event: threading.Event
|
|
71
|
+
_done_event: threading.Event = field(default_factory=threading.Event, repr=False)
|
|
72
|
+
_result: ComprehensionRecord | None = field(default=None, repr=False)
|
|
73
|
+
|
|
74
|
+
def join(self, timeout: float | None = None) -> ComprehensionRecord | None:
|
|
75
|
+
if self.thread is not None:
|
|
76
|
+
self.thread.join(timeout)
|
|
77
|
+
if self.thread is not None and self.thread.is_alive():
|
|
78
|
+
return None
|
|
79
|
+
return self._result
|
|
80
|
+
|
|
81
|
+
def is_done(self) -> bool:
|
|
82
|
+
if self.thread is None:
|
|
83
|
+
return True
|
|
84
|
+
return self._done_event.is_set()
|
|
85
|
+
|
|
86
|
+
def _set_result(self, record: ComprehensionRecord) -> None:
|
|
87
|
+
self._result = record
|
|
88
|
+
self._done_event.set()
|
|
89
|
+
|
|
90
|
+
def _mark_done(self) -> None:
|
|
91
|
+
self._done_event.set()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass(frozen=True)
|
|
95
|
+
class AssetSurfaceJoinReport:
|
|
96
|
+
completed: list[str]
|
|
97
|
+
timed_out: list[str]
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class AssetSurface:
|
|
101
|
+
def __init__(
|
|
102
|
+
self,
|
|
103
|
+
*,
|
|
104
|
+
cfg: AppConfig,
|
|
105
|
+
run_paths: RunPaths,
|
|
106
|
+
model_registry: ModelRegistry | None = None,
|
|
107
|
+
ocr_provider: OcrProvider | None = None,
|
|
108
|
+
comprehender: AssetComprehender | None = None,
|
|
109
|
+
clock: Callable[[], datetime] = lambda: datetime.now(UTC),
|
|
110
|
+
) -> None:
|
|
111
|
+
self.cfg = cfg
|
|
112
|
+
self.run_paths = run_paths
|
|
113
|
+
self.index = AssetIndex(run_paths)
|
|
114
|
+
self.model_registry = model_registry or ModelRegistry(cfg=cfg)
|
|
115
|
+
self.ocr_provider = ocr_provider
|
|
116
|
+
self.comprehender = comprehender or AssetComprehender(
|
|
117
|
+
cfg=cfg,
|
|
118
|
+
model_registry=self.model_registry,
|
|
119
|
+
ocr_provider=ocr_provider,
|
|
120
|
+
run_paths=run_paths,
|
|
121
|
+
)
|
|
122
|
+
self.clock = clock
|
|
123
|
+
self._registry_lock = threading.Lock()
|
|
124
|
+
self._in_flight: dict[str, ComprehensionRefreshHandle] = {}
|
|
125
|
+
|
|
126
|
+
def list_assets(self, *, include_deleted: bool = False) -> list[AssetSurfaceEntry]:
|
|
127
|
+
return [
|
|
128
|
+
self._entry_for(record)
|
|
129
|
+
for record in self.index.records(include_deleted=include_deleted)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
def show_asset(self, asset_id: str) -> AssetSurfaceDetail:
|
|
133
|
+
record = self.index.get(asset_id, include_deleted=True)
|
|
134
|
+
return AssetSurfaceDetail(
|
|
135
|
+
record=record,
|
|
136
|
+
comprehension_status=self._surface_status(record),
|
|
137
|
+
comprehension=self.comprehension_for(asset_id),
|
|
138
|
+
versions=_comprehension_versions(self.run_paths, asset_id),
|
|
139
|
+
extracted_text_preview=_read_text_preview(
|
|
140
|
+
self.run_paths.root,
|
|
141
|
+
record.extracted_text_path,
|
|
142
|
+
limit=800,
|
|
143
|
+
),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def add_asset(
|
|
147
|
+
self,
|
|
148
|
+
path: Path,
|
|
149
|
+
*,
|
|
150
|
+
title: str,
|
|
151
|
+
description: str = "",
|
|
152
|
+
pinned: bool = False,
|
|
153
|
+
added_by: dict[str, Any] | None = None,
|
|
154
|
+
comprehend: Literal["sync", "async", "skip"] = "async",
|
|
155
|
+
comprehension_callback: Callable[[ComprehensionRecord], None] | None = None,
|
|
156
|
+
dedupe_policy: Literal["reject", "link"] = "reject",
|
|
157
|
+
) -> AssetSurfaceAddResult:
|
|
158
|
+
existing_for_link = self._existing_asset_for_path(path) if dedupe_policy == "link" else None
|
|
159
|
+
record = ingest_asset(
|
|
160
|
+
path,
|
|
161
|
+
title=title,
|
|
162
|
+
description=description,
|
|
163
|
+
run_paths=self.run_paths,
|
|
164
|
+
pinned=pinned,
|
|
165
|
+
added_by=added_by,
|
|
166
|
+
dedupe_policy=dedupe_policy,
|
|
167
|
+
)
|
|
168
|
+
LOGGER.info(
|
|
169
|
+
"asset_surface op=add_asset asset_id=%s kind=%s size_bytes=%s comprehend=%s",
|
|
170
|
+
record.id,
|
|
171
|
+
record.kind,
|
|
172
|
+
record.size_bytes,
|
|
173
|
+
comprehend,
|
|
174
|
+
)
|
|
175
|
+
if existing_for_link is not None and existing_for_link.id == record.id:
|
|
176
|
+
return AssetSurfaceAddResult(
|
|
177
|
+
record=record,
|
|
178
|
+
comprehension_handle=None,
|
|
179
|
+
comprehension_record=None,
|
|
180
|
+
)
|
|
181
|
+
if comprehend == "skip":
|
|
182
|
+
return AssetSurfaceAddResult(
|
|
183
|
+
record=record,
|
|
184
|
+
comprehension_handle=None,
|
|
185
|
+
comprehension_record=None,
|
|
186
|
+
)
|
|
187
|
+
if comprehend == "sync":
|
|
188
|
+
handle = self.refresh_comprehension(
|
|
189
|
+
record.id,
|
|
190
|
+
mode="sync",
|
|
191
|
+
callback=comprehension_callback,
|
|
192
|
+
)
|
|
193
|
+
return AssetSurfaceAddResult(
|
|
194
|
+
record=self.index.get(record.id, include_deleted=True),
|
|
195
|
+
comprehension_handle=None,
|
|
196
|
+
comprehension_record=handle.join(),
|
|
197
|
+
)
|
|
198
|
+
handle = self.refresh_comprehension(
|
|
199
|
+
record.id,
|
|
200
|
+
mode="async",
|
|
201
|
+
callback=comprehension_callback,
|
|
202
|
+
)
|
|
203
|
+
return AssetSurfaceAddResult(
|
|
204
|
+
record=record,
|
|
205
|
+
comprehension_handle=handle,
|
|
206
|
+
comprehension_record=None,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
def edit_asset(
|
|
210
|
+
self,
|
|
211
|
+
asset_id: str,
|
|
212
|
+
*,
|
|
213
|
+
title: str | None = None,
|
|
214
|
+
description: str | None = None,
|
|
215
|
+
pinned: bool | None = None,
|
|
216
|
+
retrigger_comprehension: bool = False,
|
|
217
|
+
) -> AssetSurfaceDetail:
|
|
218
|
+
current = self.index.get(asset_id, include_deleted=True)
|
|
219
|
+
if current.deleted_at is not None:
|
|
220
|
+
raise AssetError(f"Cannot edit deleted asset: {asset_id}")
|
|
221
|
+
changes: list[str] = []
|
|
222
|
+
next_record = current
|
|
223
|
+
if title is not None:
|
|
224
|
+
clean_title = title.strip()
|
|
225
|
+
if not clean_title:
|
|
226
|
+
raise AssetError("Asset title is required.")
|
|
227
|
+
if clean_title != current.title:
|
|
228
|
+
next_record = replace(next_record, title=clean_title)
|
|
229
|
+
changes.append("title")
|
|
230
|
+
if description is not None and description != current.description:
|
|
231
|
+
next_record = replace(next_record, description=description)
|
|
232
|
+
changes.append("description")
|
|
233
|
+
if pinned is not None and bool(pinned) != current.pinned:
|
|
234
|
+
next_record = replace(next_record, pinned=bool(pinned))
|
|
235
|
+
changes.append("pinned")
|
|
236
|
+
if changes:
|
|
237
|
+
self.index.update(next_record)
|
|
238
|
+
LOGGER.info(
|
|
239
|
+
"asset_surface op=edit_asset asset_id=%s changes=%s retrigger_comprehension=%s",
|
|
240
|
+
asset_id,
|
|
241
|
+
f"[{','.join(changes)}]",
|
|
242
|
+
str(bool(retrigger_comprehension)).lower(),
|
|
243
|
+
)
|
|
244
|
+
if retrigger_comprehension:
|
|
245
|
+
self.refresh_comprehension(asset_id, mode="async")
|
|
246
|
+
return self.show_asset(asset_id)
|
|
247
|
+
|
|
248
|
+
def delete_asset(self, asset_id: str) -> AssetRecord:
|
|
249
|
+
self._cancel_existing(asset_id, join=True)
|
|
250
|
+
deleted = self.index.delete(asset_id)
|
|
251
|
+
LOGGER.info("asset_surface op=delete_asset asset_id=%s", asset_id)
|
|
252
|
+
return deleted
|
|
253
|
+
|
|
254
|
+
def refresh_comprehension(
|
|
255
|
+
self,
|
|
256
|
+
asset_id: str,
|
|
257
|
+
*,
|
|
258
|
+
mode: Literal["sync", "async"] = "async",
|
|
259
|
+
callback: Callable[[ComprehensionRecord], None] | None = None,
|
|
260
|
+
angle: str | None = None,
|
|
261
|
+
) -> ComprehensionRefreshHandle:
|
|
262
|
+
LOGGER.info(
|
|
263
|
+
"asset_surface op=refresh_comprehension asset_id=%s mode=%s",
|
|
264
|
+
asset_id,
|
|
265
|
+
mode,
|
|
266
|
+
)
|
|
267
|
+
self._cancel_existing(asset_id, join=True)
|
|
268
|
+
asset = self.index.get(asset_id, include_deleted=True)
|
|
269
|
+
if asset.deleted_at is not None:
|
|
270
|
+
raise AssetError(f"Cannot refresh deleted asset: {asset_id}")
|
|
271
|
+
cancellation_event = threading.Event()
|
|
272
|
+
handle = ComprehensionRefreshHandle(
|
|
273
|
+
asset_id=asset_id,
|
|
274
|
+
thread=None,
|
|
275
|
+
started_at=self.clock(),
|
|
276
|
+
cancellation_event=cancellation_event,
|
|
277
|
+
)
|
|
278
|
+
if mode == "sync":
|
|
279
|
+
record = self._run_comprehension(
|
|
280
|
+
asset=asset,
|
|
281
|
+
handle=handle,
|
|
282
|
+
callback=callback,
|
|
283
|
+
angle=angle,
|
|
284
|
+
)
|
|
285
|
+
handle._set_result(record)
|
|
286
|
+
return handle
|
|
287
|
+
thread = threading.Thread(
|
|
288
|
+
target=self._run_comprehension_thread,
|
|
289
|
+
args=(asset, handle, callback, angle),
|
|
290
|
+
name=f"asset-comprehension-{asset_id}",
|
|
291
|
+
daemon=True,
|
|
292
|
+
)
|
|
293
|
+
handle.thread = thread
|
|
294
|
+
with self._registry_lock:
|
|
295
|
+
self._in_flight[asset_id] = handle
|
|
296
|
+
thread.start()
|
|
297
|
+
return handle
|
|
298
|
+
|
|
299
|
+
def cancel_pending_comprehensions(self) -> int:
|
|
300
|
+
with self._registry_lock:
|
|
301
|
+
handles = [
|
|
302
|
+
handle
|
|
303
|
+
for handle in self._in_flight.values()
|
|
304
|
+
if handle.thread is not None and handle.thread.is_alive()
|
|
305
|
+
]
|
|
306
|
+
for handle in handles:
|
|
307
|
+
handle.cancellation_event.set()
|
|
308
|
+
return len(handles)
|
|
309
|
+
|
|
310
|
+
def join_pending(self, *, timeout_seconds: float | None = None) -> AssetSurfaceJoinReport:
|
|
311
|
+
with self._registry_lock:
|
|
312
|
+
handles = list(self._in_flight.values())
|
|
313
|
+
completed: list[str] = []
|
|
314
|
+
timed_out: list[str] = []
|
|
315
|
+
deadline = (
|
|
316
|
+
None if timeout_seconds is None else time.monotonic() + max(float(timeout_seconds), 0.0)
|
|
317
|
+
)
|
|
318
|
+
for handle in handles:
|
|
319
|
+
remaining = None if deadline is None else max(deadline - time.monotonic(), 0.0)
|
|
320
|
+
record = handle.join(remaining)
|
|
321
|
+
if record is None and handle.thread is not None and handle.thread.is_alive():
|
|
322
|
+
timed_out.append(handle.asset_id)
|
|
323
|
+
else:
|
|
324
|
+
completed.append(handle.asset_id)
|
|
325
|
+
return AssetSurfaceJoinReport(
|
|
326
|
+
completed=list(dict.fromkeys(completed)),
|
|
327
|
+
timed_out=list(dict.fromkeys(timed_out)),
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
def comprehension_for(self, asset_id: str) -> ComprehensionRecord | None:
|
|
331
|
+
if self._is_running(asset_id):
|
|
332
|
+
return None
|
|
333
|
+
try:
|
|
334
|
+
record = self.index.get(asset_id, include_deleted=True)
|
|
335
|
+
except AssetNotFoundError:
|
|
336
|
+
return None
|
|
337
|
+
if record.comprehension_status == "pending":
|
|
338
|
+
return None
|
|
339
|
+
try:
|
|
340
|
+
return self.index.read_comprehension(asset_id)
|
|
341
|
+
except AssetError:
|
|
342
|
+
return None
|
|
343
|
+
|
|
344
|
+
def _entry_for(self, record: AssetRecord) -> AssetSurfaceEntry:
|
|
345
|
+
comprehension = self.comprehension_for(record.id)
|
|
346
|
+
status = self._surface_status(record)
|
|
347
|
+
return AssetSurfaceEntry(
|
|
348
|
+
record=record,
|
|
349
|
+
comprehension_status=status,
|
|
350
|
+
comprehension_source=comprehension.source if comprehension is not None else None,
|
|
351
|
+
comprehension_summary_preview=_preview(
|
|
352
|
+
comprehension.data.semantic_summary if comprehension is not None else "",
|
|
353
|
+
120,
|
|
354
|
+
),
|
|
355
|
+
detected_language=comprehension.detected_language
|
|
356
|
+
if comprehension is not None
|
|
357
|
+
else None,
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
def _surface_status(self, record: AssetRecord) -> SurfaceComprehensionStatus:
|
|
361
|
+
if self._is_running(record.id):
|
|
362
|
+
return "running"
|
|
363
|
+
return record.comprehension_status
|
|
364
|
+
|
|
365
|
+
def _run_comprehension_thread(
|
|
366
|
+
self,
|
|
367
|
+
asset: AssetRecord,
|
|
368
|
+
handle: ComprehensionRefreshHandle,
|
|
369
|
+
callback: Callable[[ComprehensionRecord], None] | None,
|
|
370
|
+
angle: str | None,
|
|
371
|
+
) -> None:
|
|
372
|
+
try:
|
|
373
|
+
record = self._run_comprehension(
|
|
374
|
+
asset=asset,
|
|
375
|
+
handle=handle,
|
|
376
|
+
callback=callback,
|
|
377
|
+
angle=angle,
|
|
378
|
+
)
|
|
379
|
+
handle._set_result(record)
|
|
380
|
+
finally:
|
|
381
|
+
handle._mark_done()
|
|
382
|
+
with self._registry_lock:
|
|
383
|
+
if self._in_flight.get(asset.id) is handle:
|
|
384
|
+
self._in_flight.pop(asset.id, None)
|
|
385
|
+
|
|
386
|
+
def _run_comprehension(
|
|
387
|
+
self,
|
|
388
|
+
*,
|
|
389
|
+
asset: AssetRecord,
|
|
390
|
+
handle: ComprehensionRefreshHandle,
|
|
391
|
+
callback: Callable[[ComprehensionRecord], None] | None,
|
|
392
|
+
angle: str | None = None,
|
|
393
|
+
) -> ComprehensionRecord:
|
|
394
|
+
if angle is None:
|
|
395
|
+
record = self.comprehender.comprehend(asset)
|
|
396
|
+
else:
|
|
397
|
+
record = self.comprehender.comprehend(asset, angle=angle)
|
|
398
|
+
LOGGER.info(
|
|
399
|
+
"asset_comprehension_complete asset_id=%s source=%s status=%s elapsed_ms=%s",
|
|
400
|
+
asset.id,
|
|
401
|
+
record.source,
|
|
402
|
+
record.status,
|
|
403
|
+
record.elapsed_ms,
|
|
404
|
+
)
|
|
405
|
+
if not handle.cancellation_event.is_set() and callback is not None:
|
|
406
|
+
try:
|
|
407
|
+
callback(record)
|
|
408
|
+
except Exception as exc: # noqa: BLE001
|
|
409
|
+
LOGGER.warning(
|
|
410
|
+
"asset_surface comprehension callback failed for asset_id=%s: %s",
|
|
411
|
+
asset.id,
|
|
412
|
+
exc,
|
|
413
|
+
)
|
|
414
|
+
return record
|
|
415
|
+
|
|
416
|
+
def _is_running(self, asset_id: str) -> bool:
|
|
417
|
+
with self._registry_lock:
|
|
418
|
+
handle = self._in_flight.get(asset_id)
|
|
419
|
+
return (
|
|
420
|
+
handle is not None
|
|
421
|
+
and handle.thread is not None
|
|
422
|
+
and handle.thread.is_alive()
|
|
423
|
+
and not handle.is_done()
|
|
424
|
+
)
|
|
425
|
+
|
|
426
|
+
def _cancel_existing(self, asset_id: str, *, join: bool) -> None:
|
|
427
|
+
with self._registry_lock:
|
|
428
|
+
handle = self._in_flight.pop(asset_id, None)
|
|
429
|
+
if handle is None:
|
|
430
|
+
return
|
|
431
|
+
handle.cancellation_event.set()
|
|
432
|
+
if join and handle.thread is not None and handle.thread.is_alive():
|
|
433
|
+
handle.thread.join()
|
|
434
|
+
|
|
435
|
+
def _existing_asset_for_path(self, path: Path) -> AssetRecord | None:
|
|
436
|
+
try:
|
|
437
|
+
digest = hashlib.sha256(path.expanduser().resolve().read_bytes()).hexdigest()
|
|
438
|
+
except OSError:
|
|
439
|
+
return None
|
|
440
|
+
return self.index.find_by_sha256(digest)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def build_asset_surface(
|
|
444
|
+
*,
|
|
445
|
+
cfg: AppConfig,
|
|
446
|
+
run_paths: RunPaths,
|
|
447
|
+
model_registry: ModelRegistry | None = None,
|
|
448
|
+
) -> AssetSurface:
|
|
449
|
+
if not run_paths_support_asset_surface(run_paths):
|
|
450
|
+
missing = [
|
|
451
|
+
attr
|
|
452
|
+
for attr in _ASSET_RUN_PATH_ATTRS
|
|
453
|
+
if not isinstance(getattr(run_paths, attr, None), Path)
|
|
454
|
+
]
|
|
455
|
+
raise AssetError(
|
|
456
|
+
"Run paths do not support the asset surface; missing path attributes: "
|
|
457
|
+
+ ", ".join(missing)
|
|
458
|
+
)
|
|
459
|
+
registry = model_registry or ModelRegistry(cfg=cfg)
|
|
460
|
+
ocr_provider = _ocr_provider_from_config(cfg)
|
|
461
|
+
return AssetSurface(
|
|
462
|
+
cfg=cfg,
|
|
463
|
+
run_paths=run_paths,
|
|
464
|
+
model_registry=registry,
|
|
465
|
+
ocr_provider=ocr_provider,
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
def run_paths_support_asset_surface(run_paths: Any) -> bool:
|
|
470
|
+
return all(isinstance(getattr(run_paths, attr, None), Path) for attr in _ASSET_RUN_PATH_ATTRS)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _ocr_provider_from_config(cfg: AppConfig) -> OcrProvider | None:
|
|
474
|
+
comprehension_cfg = cfg.assets.comprehension
|
|
475
|
+
if comprehension_cfg.ocr_enabled == "never":
|
|
476
|
+
return None
|
|
477
|
+
provider_name = str(comprehension_cfg.ocr_provider or "").strip().lower()
|
|
478
|
+
if provider_name != "tesseract":
|
|
479
|
+
LOGGER.warning("Unsupported OCR provider configured for assets: %s", provider_name)
|
|
480
|
+
return None
|
|
481
|
+
provider = TesseractOcrProvider(timeout_seconds=comprehension_cfg.ocr_timeout_seconds)
|
|
482
|
+
if not provider.is_available():
|
|
483
|
+
return None
|
|
484
|
+
return provider
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _comprehension_versions(run_paths: RunPaths, asset_id: str) -> list[int]:
|
|
488
|
+
directory = run_paths.assets_comprehensions_dir / asset_id
|
|
489
|
+
try:
|
|
490
|
+
paths = list(directory.glob("v*.json"))
|
|
491
|
+
except OSError:
|
|
492
|
+
return []
|
|
493
|
+
versions: list[int] = []
|
|
494
|
+
for path in paths:
|
|
495
|
+
stem = path.stem
|
|
496
|
+
if not stem.startswith("v"):
|
|
497
|
+
continue
|
|
498
|
+
try:
|
|
499
|
+
version = int(stem[1:])
|
|
500
|
+
except ValueError:
|
|
501
|
+
continue
|
|
502
|
+
if version > 0:
|
|
503
|
+
versions.append(version)
|
|
504
|
+
return sorted(set(versions))
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _read_text_preview(root: Path, repo_relative_path: str | None, *, limit: int) -> str:
|
|
508
|
+
if not repo_relative_path:
|
|
509
|
+
return ""
|
|
510
|
+
try:
|
|
511
|
+
path = (root / repo_relative_path).resolve()
|
|
512
|
+
return _preview(path.read_text(encoding="utf-8", errors="replace"), limit)
|
|
513
|
+
except OSError:
|
|
514
|
+
return ""
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
def _preview(text: str, limit: int) -> str:
|
|
518
|
+
clean = str(text or "")
|
|
519
|
+
if len(clean) <= limit:
|
|
520
|
+
return clean
|
|
521
|
+
return clean[:limit].rstrip() + "..."
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
_ASSET_UNTRUSTED_REPLACEMENTS = (
|
|
4
|
+
("[ASSET_UNTRUSTED_CONTENT]", "[ASSET_UNTRUSTED_CONTENT (asset literal)]"),
|
|
5
|
+
("[/ASSET_UNTRUSTED_CONTENT]", "[/ASSET_UNTRUSTED_CONTENT (asset literal)]"),
|
|
6
|
+
(
|
|
7
|
+
"--- BEGIN UNTRUSTED ASSET CONTENT ---",
|
|
8
|
+
"--- BEGIN UNTRUSTED ASSET CONTENT (asset literal) ---",
|
|
9
|
+
),
|
|
10
|
+
(
|
|
11
|
+
"--- END UNTRUSTED ASSET CONTENT ---",
|
|
12
|
+
"--- END UNTRUSTED ASSET CONTENT (asset literal) ---",
|
|
13
|
+
),
|
|
14
|
+
("</asset_content>", "</asset_content (asset literal)>"),
|
|
15
|
+
("</asset_summary>", "</asset_summary (asset literal)>"),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def build_untrusted_asset_text_block(
|
|
20
|
+
*,
|
|
21
|
+
asset_id: str,
|
|
22
|
+
text: str,
|
|
23
|
+
mime_type: str | None = None,
|
|
24
|
+
original_char_count: int | None = None,
|
|
25
|
+
truncated: bool = False,
|
|
26
|
+
) -> str:
|
|
27
|
+
body = _neutralize_asset_text_body(text)
|
|
28
|
+
char_count = original_char_count if original_char_count is not None else len(body)
|
|
29
|
+
return "\n".join(
|
|
30
|
+
[
|
|
31
|
+
"[ASSET_UNTRUSTED_CONTENT]",
|
|
32
|
+
f"asset_id: {asset_id}",
|
|
33
|
+
f"mime_type: {mime_type or 'unknown'}",
|
|
34
|
+
f"char_count: {char_count}",
|
|
35
|
+
f"truncated: {'true' if truncated else 'false'}",
|
|
36
|
+
"--- BEGIN UNTRUSTED ASSET CONTENT ---",
|
|
37
|
+
body,
|
|
38
|
+
"--- END UNTRUSTED ASSET CONTENT ---",
|
|
39
|
+
"[/ASSET_UNTRUSTED_CONTENT]",
|
|
40
|
+
]
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _neutralize_asset_text_body(text: str) -> str:
|
|
45
|
+
body = str(text)
|
|
46
|
+
for raw_marker, escaped_marker in _ASSET_UNTRUSTED_REPLACEMENTS:
|
|
47
|
+
body = body.replace(raw_marker, escaped_marker)
|
|
48
|
+
return body
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import threading
|
|
6
|
+
from collections import Counter
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from ..forge import RunPaths, now_iso
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AssetUsageLogger:
|
|
13
|
+
def __init__(self, *, run_paths: RunPaths, task_id: str) -> None:
|
|
14
|
+
self.run_paths = run_paths
|
|
15
|
+
self.task_id = task_id
|
|
16
|
+
self.path = run_paths.execution_asset_usage_dir / f"{_safe_task(task_id)}.jsonl"
|
|
17
|
+
self._lock = threading.Lock()
|
|
18
|
+
self._counts: Counter[str] = Counter()
|
|
19
|
+
|
|
20
|
+
def event(self, event: str, **payload: Any) -> None:
|
|
21
|
+
clean_event = str(event or "").strip()
|
|
22
|
+
if not clean_event:
|
|
23
|
+
return
|
|
24
|
+
body = {
|
|
25
|
+
"event": clean_event,
|
|
26
|
+
"ts": now_iso(),
|
|
27
|
+
**_sanitize_payload(payload),
|
|
28
|
+
}
|
|
29
|
+
self._append_json_line(body)
|
|
30
|
+
self._counts[clean_event] += 1
|
|
31
|
+
|
|
32
|
+
def mirror(self, *, asset_id: str, kind: str, status: str) -> None:
|
|
33
|
+
self.event("mirror", asset_id=asset_id, kind=kind, status=status)
|
|
34
|
+
|
|
35
|
+
def allocation_decision(self, *, asset_id: str, mode: str) -> None:
|
|
36
|
+
self.event("allocation_decision", asset_id=asset_id, mode=mode)
|
|
37
|
+
|
|
38
|
+
def inline_injection(self, *, asset_id: str, kind: str) -> None:
|
|
39
|
+
self.event("inline_injection", asset_id=asset_id, kind=kind)
|
|
40
|
+
|
|
41
|
+
def asset_read(self, *, asset_id: str, focus: bool, chars: int, cached: bool) -> None:
|
|
42
|
+
self.event(
|
|
43
|
+
"asset_read",
|
|
44
|
+
asset_id=asset_id,
|
|
45
|
+
focus=bool(focus),
|
|
46
|
+
chars=max(0, int(chars)),
|
|
47
|
+
cached=bool(cached),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
def asset_load(self, *, asset_id: str, kind: str, chars: int) -> None:
|
|
51
|
+
self.event("asset_load", asset_id=asset_id, kind=kind, chars=max(0, int(chars)))
|
|
52
|
+
|
|
53
|
+
def summary(self, *, primary_count: int, may_need_count: int, pinned_count: int) -> None:
|
|
54
|
+
self.event(
|
|
55
|
+
"summary",
|
|
56
|
+
primary_count=max(0, int(primary_count)),
|
|
57
|
+
may_need_count=max(0, int(may_need_count)),
|
|
58
|
+
pinned_count=max(0, int(pinned_count)),
|
|
59
|
+
reads=int(self._counts.get("asset_read", 0)),
|
|
60
|
+
loads=int(self._counts.get("asset_load", 0)),
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
def _append_json_line(self, payload: dict[str, Any]) -> None:
|
|
64
|
+
line = json.dumps(payload, ensure_ascii=False, sort_keys=True, default=str) + "\n"
|
|
65
|
+
encoded = line.encode("utf-8")
|
|
66
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
with self._lock:
|
|
68
|
+
fd = os.open(
|
|
69
|
+
self.path,
|
|
70
|
+
os.O_APPEND | os.O_CREAT | os.O_WRONLY,
|
|
71
|
+
0o600,
|
|
72
|
+
)
|
|
73
|
+
try:
|
|
74
|
+
os.write(fd, encoded)
|
|
75
|
+
finally:
|
|
76
|
+
os.close(fd)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _sanitize_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
80
|
+
sanitized: dict[str, Any] = {}
|
|
81
|
+
for key, value in payload.items():
|
|
82
|
+
clean_key = str(key or "").strip()
|
|
83
|
+
if not clean_key:
|
|
84
|
+
continue
|
|
85
|
+
if isinstance(value, str | int | float | bool) or value is None:
|
|
86
|
+
sanitized[clean_key] = value
|
|
87
|
+
else:
|
|
88
|
+
sanitized[clean_key] = str(value)
|
|
89
|
+
return sanitized
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _safe_task(task_id: str) -> str:
|
|
93
|
+
safe = "".join(ch if ch.isalnum() or ch in {"-", "_"} else "_" for ch in task_id)
|
|
94
|
+
return safe or "task"
|