aria-code 0.52.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.
- aria_code/__init__.py +42 -0
- aria_code/_version.py +20 -0
- aria_code/adk_apps/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/__init__.py +1 -0
- aria_code/adk_apps/aria_code_review/agent.py +32 -0
- aria_code/adk_apps/aria_research/__init__.py +1 -0
- aria_code/adk_apps/aria_research/agent.py +31 -0
- aria_code/agents/__init__.py +34 -0
- aria_code/agents/base.py +318 -0
- aria_code/agents/code_review.py +200 -0
- aria_code/agents/deep/__init__.py +37 -0
- aria_code/agents/deep/calibration_loop.py +144 -0
- aria_code/agents/deep/critic.py +125 -0
- aria_code/agents/deep/deepen.py +193 -0
- aria_code/agents/deep/models.py +149 -0
- aria_code/agents/deep/pipeline.py +164 -0
- aria_code/agents/deep/quant_fusion.py +192 -0
- aria_code/agents/deep/themes.py +95 -0
- aria_code/agents/deep/tiers.py +106 -0
- aria_code/agents/engineering/__init__.py +14 -0
- aria_code/agents/engineering/coder.py +222 -0
- aria_code/agents/engineering/tester.py +145 -0
- aria_code/agents/financial/__init__.py +21 -0
- aria_code/agents/financial/cashflow_burn.py +119 -0
- aria_code/agents/financial/catalyst.py +279 -0
- aria_code/agents/financial/corporate_finance.py +175 -0
- aria_code/agents/financial/debate.py +224 -0
- aria_code/agents/financial/earnings.py +303 -0
- aria_code/agents/financial/fundamental.py +191 -0
- aria_code/agents/financial/macro.py +99 -0
- aria_code/agents/financial/news.py +207 -0
- aria_code/agents/financial/risk.py +165 -0
- aria_code/agents/financial/sector.py +279 -0
- aria_code/agents/financial/strategist.py +121 -0
- aria_code/agents/financial/stripe_revenue.py +127 -0
- aria_code/agents/financial/synthesis.py +274 -0
- aria_code/agents/financial/technical.py +258 -0
- aria_code/agents/orchestrator.py +44 -0
- aria_code/agents/portfolio_agent.py +363 -0
- aria_code/agents/realty/__init__.py +62 -0
- aria_code/agents/realty/asset_diagnosis.py +152 -0
- aria_code/agents/realty/business_match.py +166 -0
- aria_code/agents/realty/cashflow_verify.py +209 -0
- aria_code/agents/realty/contract_rules.py +210 -0
- aria_code/agents/realty/energy_anomaly.py +189 -0
- aria_code/agents/realty/exit_settlement.py +208 -0
- aria_code/agents/realty/fulfillment_risk.py +206 -0
- aria_code/agents/realty/ops_optimize.py +160 -0
- aria_code/agents/realty/revenue_share.py +215 -0
- aria_code/agents/registry.py +158 -0
- aria_code/agents/signal_scheme.py +111 -0
- aria_code/agents/sports/__init__.py +0 -0
- aria_code/agents/sports/football_agent.py +197 -0
- aria_code/agents/supervisor.py +113 -0
- aria_code/agents/team.py +492 -0
- aria_code/agents/warehouse/__init__.py +25 -0
- aria_code/agents/warehouse/contracts.py +26 -0
- aria_code/agents/warehouse/dashboard.py +155 -0
- aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
- aria_code/agents/warehouse/inbound_exceptions.py +25 -0
- aria_code/agents/warehouse/inventory_health.py +24 -0
- aria_code/agents/warehouse/local_run.py +71 -0
- aria_code/agents/warehouse/local_snapshot.py +132 -0
- aria_code/agents/warehouse/logistics_cost.py +84 -0
- aria_code/agents/warehouse/logistics_sync.py +21 -0
- aria_code/agents/warehouse/workflow.py +31 -0
- aria_code/aliyun_data_client.py +4 -0
- aria_code/apps/README.md +12 -0
- aria_code/apps/__init__.py +2 -0
- aria_code/apps/channels/README.md +15 -0
- aria_code/apps/channels/__init__.py +17 -0
- aria_code/apps/channels/intake.py +92 -0
- aria_code/apps/channels/registry.py +95 -0
- aria_code/apps/channels/tradingview.py +94 -0
- aria_code/apps/cli/README.md +13 -0
- aria_code/apps/cli/__init__.py +2 -0
- aria_code/apps/cli/_optional.py +120 -0
- aria_code/apps/cli/bootstrap.py +145 -0
- aria_code/apps/cli/broker_render.py +206 -0
- aria_code/apps/cli/codegen_paths.py +29 -0
- aria_code/apps/cli/commands/__init__.py +16 -0
- aria_code/apps/cli/commands/_business_render.py +78 -0
- aria_code/apps/cli/commands/_ui.py +71 -0
- aria_code/apps/cli/commands/analysis_cmds.py +369 -0
- aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
- aria_code/apps/cli/commands/auth_cmds.py +239 -0
- aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
- aria_code/apps/cli/commands/broker_cmds.py +1234 -0
- aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
- aria_code/apps/cli/commands/canvas_cmds.py +77 -0
- aria_code/apps/cli/commands/catalog.py +87 -0
- aria_code/apps/cli/commands/core_cmds.py +2520 -0
- aria_code/apps/cli/commands/data_cmds.py +457 -0
- aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
- aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
- aria_code/apps/cli/commands/file_cmds.py +312 -0
- aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
- aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
- aria_code/apps/cli/commands/market.py +434 -0
- aria_code/apps/cli/commands/market_cmds.py +1381 -0
- aria_code/apps/cli/commands/market_context.py +425 -0
- aria_code/apps/cli/commands/market_render.py +7 -0
- aria_code/apps/cli/commands/model_cmds.py +2153 -0
- aria_code/apps/cli/commands/ops_cmds.py +721 -0
- aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
- aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
- aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
- aria_code/apps/cli/commands/report.py +437 -0
- aria_code/apps/cli/commands/scaffold_templates.py +617 -0
- aria_code/apps/cli/commands/session_cmds.py +236 -0
- aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
- aria_code/apps/cli/commands/team.py +669 -0
- aria_code/apps/cli/commands/team_render.py +8 -0
- aria_code/apps/cli/commands/ui_cmds.py +401 -0
- aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
- aria_code/apps/cli/commands/workflow_cmds.py +480 -0
- aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
- aria_code/apps/cli/config_paths.py +73 -0
- aria_code/apps/cli/config_store.py +78 -0
- aria_code/apps/cli/constants.py +88 -0
- aria_code/apps/cli/context.py +34 -0
- aria_code/apps/cli/deterministic.py +191 -0
- aria_code/apps/cli/direct.py +48 -0
- aria_code/apps/cli/finance_service_catalog.py +111 -0
- aria_code/apps/cli/football_reports.py +231 -0
- aria_code/apps/cli/github_app_auth.py +135 -0
- aria_code/apps/cli/google_login.py +196 -0
- aria_code/apps/cli/handlers/__init__.py +11 -0
- aria_code/apps/cli/handlers/broker_handlers.py +122 -0
- aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
- aria_code/apps/cli/handlers/market_handlers.py +2513 -0
- aria_code/apps/cli/handlers/realty_handlers.py +114 -0
- aria_code/apps/cli/handlers/sports_handlers.py +22 -0
- aria_code/apps/cli/handlers/strategy_advice.py +82 -0
- aria_code/apps/cli/helpers.py +302 -0
- aria_code/apps/cli/hooks.py +180 -0
- aria_code/apps/cli/i18n.py +297 -0
- aria_code/apps/cli/intent.py +136 -0
- aria_code/apps/cli/intent_router.py +276 -0
- aria_code/apps/cli/intent_signals.py +344 -0
- aria_code/apps/cli/lifecycle_hooks.py +48 -0
- aria_code/apps/cli/local_tool_schemas.py +536 -0
- aria_code/apps/cli/main.py +29 -0
- aria_code/apps/cli/market_metadata.py +135 -0
- aria_code/apps/cli/market_universe.py +487 -0
- aria_code/apps/cli/message_processing.py +310 -0
- aria_code/apps/cli/model_catalog.py +620 -0
- aria_code/apps/cli/pdf_report.py +977 -0
- aria_code/apps/cli/plan_mode.py +139 -0
- aria_code/apps/cli/plotly_html.py +15 -0
- aria_code/apps/cli/prediction_feedback.py +202 -0
- aria_code/apps/cli/preflight.py +502 -0
- aria_code/apps/cli/project_aria.py +60 -0
- aria_code/apps/cli/prompt_assembly.py +59 -0
- aria_code/apps/cli/prompts/__init__.py +0 -0
- aria_code/apps/cli/prompts/coding.py +53 -0
- aria_code/apps/cli/prompts/select.py +142 -0
- aria_code/apps/cli/prompts/system_prompts.py +552 -0
- aria_code/apps/cli/prompts/ui.py +436 -0
- aria_code/apps/cli/provider_endpoints.py +237 -0
- aria_code/apps/cli/provider_keys.py +73 -0
- aria_code/apps/cli/providers/__init__.py +1 -0
- aria_code/apps/cli/providers/base.py +521 -0
- aria_code/apps/cli/providers/chat_routing.py +128 -0
- aria_code/apps/cli/providers/collaboration.py +98 -0
- aria_code/apps/cli/providers/llm/__init__.py +1 -0
- aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
- aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
- aria_code/apps/cli/providers/llm/response_cache.py +60 -0
- aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
- aria_code/apps/cli/providers/runtime_bridge.py +353 -0
- aria_code/apps/cli/providers/vertexai_stream.py +316 -0
- aria_code/apps/cli/runtime_consumer.py +657 -0
- aria_code/apps/cli/sandbox.py +32 -0
- aria_code/apps/cli/session_export.py +120 -0
- aria_code/apps/cli/session_jsonl.py +208 -0
- aria_code/apps/cli/session_store.py +112 -0
- aria_code/apps/cli/skills_catalog.py +273 -0
- aria_code/apps/cli/todo_tracker.py +190 -0
- aria_code/apps/cli/tool_executor.py +823 -0
- aria_code/apps/cli/tool_registry.py +34 -0
- aria_code/apps/cli/tool_scope.py +106 -0
- aria_code/apps/cli/tools/__init__.py +40 -0
- aria_code/apps/cli/tools/context.py +46 -0
- aria_code/apps/cli/tools/file_tools.py +180 -0
- aria_code/apps/cli/tools/market_tools.py +620 -0
- aria_code/apps/cli/tools/notebook_tools.py +111 -0
- aria_code/apps/cli/tools/system_tools.py +726 -0
- aria_code/apps/cli/tools/write_tools.py +845 -0
- aria_code/apps/cli/tradingview_bridge.py +529 -0
- aria_code/apps/cli/turn_planning.py +137 -0
- aria_code/apps/cli/update_check.py +153 -0
- aria_code/apps/cli/utils/__init__.py +0 -0
- aria_code/apps/cli/utils/market_detect.py +1597 -0
- aria_code/apps/daemon/README.md +14 -0
- aria_code/apps/vscode/README.md +115 -0
- aria_code/apps/vscode/package.json +70 -0
- aria_code/aria_cli.py +6743 -0
- aria_code/aria_daemon.py +1398 -0
- aria_code/aria_feishu_bot.py +1423 -0
- aria_code/aria_mcp_server.py +7 -0
- aria_code/aria_relay_client.py +4 -0
- aria_code/aria_relay_server.py +490 -0
- aria_code/aria_telegram_bot.py +202 -0
- aria_code/ariarc.py +385 -0
- aria_code/artifacts.py +581 -0
- aria_code/backtest_engine.py +4 -0
- aria_code/backtest_report.py +4 -0
- aria_code/brokers/__init__.py +78 -0
- aria_code/brokers/automation.py +193 -0
- aria_code/brokers/base.py +231 -0
- aria_code/brokers/capabilities.py +264 -0
- aria_code/brokers/cn/__init__.py +10 -0
- aria_code/brokers/cn/easytrader_broker.py +193 -0
- aria_code/brokers/cn/futu_broker.py +194 -0
- aria_code/brokers/cn/longbridge_broker.py +190 -0
- aria_code/brokers/cn/tiger_broker.py +196 -0
- aria_code/brokers/cn/xtquant_broker.py +225 -0
- aria_code/brokers/config.py +414 -0
- aria_code/brokers/intl/__init__.py +5 -0
- aria_code/brokers/intl/alpaca_broker.py +183 -0
- aria_code/brokers/intl/ibkr_broker.py +215 -0
- aria_code/brokers/intl/trading212_broker.py +183 -0
- aria_code/brokers/intl/webull_broker.py +156 -0
- aria_code/brokers/paper_broker.py +266 -0
- aria_code/brokers/planning.py +296 -0
- aria_code/brokers/registry.py +213 -0
- aria_code/brokers/trading.py +267 -0
- aria_code/canva_client.py +4 -0
- aria_code/change_store.py +127 -0
- aria_code/clients/__init__.py +0 -0
- aria_code/clients/aliyun_data_client.py +660 -0
- aria_code/clients/aria_relay_client.py +182 -0
- aria_code/clients/canva_client.py +284 -0
- aria_code/clients/figma_client.py +100 -0
- aria_code/clients/football_data_client.py +1671 -0
- aria_code/clients/kling_video_client.py +182 -0
- aria_code/clients/market_data_client.py +1976 -0
- aria_code/clients/openai_image_client.py +242 -0
- aria_code/clients/runway_video_client.py +157 -0
- aria_code/clients/warehouse_erp_client.py +105 -0
- aria_code/cloud_memory.py +181 -0
- aria_code/command_safety.py +19 -0
- aria_code/computer_use_tools.py +4 -0
- aria_code/dashboard_generator.py +579 -0
- aria_code/data_analysis_tools.py +4 -0
- aria_code/data_cleaner.py +483 -0
- aria_code/data_service.py +513 -0
- aria_code/datasources/__init__.py +23 -0
- aria_code/datasources/base.py +166 -0
- aria_code/datasources/router.py +253 -0
- aria_code/datasources/sources/__init__.py +15 -0
- aria_code/datasources/sources/akshare_source.py +269 -0
- aria_code/datasources/sources/alpha_vantage_source.py +203 -0
- aria_code/datasources/sources/edgar_source.py +219 -0
- aria_code/datasources/sources/finnhub_source.py +198 -0
- aria_code/datasources/sources/fred_source.py +223 -0
- aria_code/datasources/sources/tushare_source.py +143 -0
- aria_code/datasources/sources/web_scraper_source.py +278 -0
- aria_code/datasources/sources/world_bank_source.py +206 -0
- aria_code/datasources/sources/yfinance_source.py +152 -0
- aria_code/demo_player.py +207 -0
- aria_code/doctor.py +813 -0
- aria_code/domain/__init__.py +0 -0
- aria_code/domain/backtest_engine.py +354 -0
- aria_code/domain/backtest_report.py +472 -0
- aria_code/domain/finance_formulas.py +389 -0
- aria_code/domain/portfolio_ledger.py +307 -0
- aria_code/domain/strategy_vault.py +421 -0
- aria_code/evals/__init__.py +37 -0
- aria_code/evals/harness.py +598 -0
- aria_code/evals/runner.py +238 -0
- aria_code/external_agent_runner.py +115 -0
- aria_code/figma_client.py +4 -0
- aria_code/file_analysis_tools.py +4 -0
- aria_code/finance_formulas.py +4 -0
- aria_code/football_data_client.py +4 -0
- aria_code/image_gen_tools.py +4 -0
- aria_code/intent_classifier.py +386 -0
- aria_code/kling_video_client.py +4 -0
- aria_code/licensing.py +151 -0
- aria_code/local_finance_tools.py +4 -0
- aria_code/local_image_provider.py +4 -0
- aria_code/local_llm_provider.py +4 -0
- aria_code/macro_tools.py +4 -0
- aria_code/markdown_pdf.py +289 -0
- aria_code/market_data_client.py +4 -0
- aria_code/mcp_client.py +639 -0
- aria_code/memory_manager.py +246 -0
- aria_code/model_capability.py +486 -0
- aria_code/notification_tools.py +4 -0
- aria_code/openai_image_client.py +4 -0
- aria_code/packages/__init__.py +23 -0
- aria_code/packages/adk_bridge/__init__.py +11 -0
- aria_code/packages/adk_bridge/code_review_tools.py +49 -0
- aria_code/packages/adk_bridge/market_tools.py +125 -0
- aria_code/packages/aria_agents/__init__.py +5 -0
- aria_code/packages/aria_agents/manifest.py +78 -0
- aria_code/packages/aria_core/__init__.py +34 -0
- aria_code/packages/aria_core/architecture.py +269 -0
- aria_code/packages/aria_core/export.py +147 -0
- aria_code/packages/aria_core/manifest.py +65 -0
- aria_code/packages/aria_core/paths.py +57 -0
- aria_code/packages/aria_core/secure_file.py +68 -0
- aria_code/packages/aria_infra/__init__.py +15 -0
- aria_code/packages/aria_infra/arthera.py +52 -0
- aria_code/packages/aria_infra/doctor.py +246 -0
- aria_code/packages/aria_infra/product.py +40 -0
- aria_code/packages/aria_mcp/__init__.py +25 -0
- aria_code/packages/aria_mcp/bridge.py +201 -0
- aria_code/packages/aria_mcp/circuit.py +82 -0
- aria_code/packages/aria_mcp/config.py +111 -0
- aria_code/packages/aria_mcp/installer.py +95 -0
- aria_code/packages/aria_mcp/server.py +1550 -0
- aria_code/packages/aria_mcp/tools.py +81 -0
- aria_code/packages/aria_sdk/__init__.py +19 -0
- aria_code/packages/aria_sdk/client.py +396 -0
- aria_code/packages/aria_sdk/providers.py +70 -0
- aria_code/packages/aria_sdk/streaming.py +79 -0
- aria_code/packages/aria_sdk/types.py +86 -0
- aria_code/packages/aria_services/__init__.py +92 -0
- aria_code/packages/aria_services/cache.py +60 -0
- aria_code/packages/aria_services/context.py +356 -0
- aria_code/packages/aria_services/context_checkpoints.py +157 -0
- aria_code/packages/aria_services/data.py +11 -0
- aria_code/packages/aria_services/provider_health.py +268 -0
- aria_code/packages/aria_services/references.py +247 -0
- aria_code/packages/aria_services/registry.py +249 -0
- aria_code/packages/aria_services/research_protocol.py +57 -0
- aria_code/packages/aria_services/research_quality.py +186 -0
- aria_code/packages/aria_services/settings.py +157 -0
- aria_code/packages/aria_services/usage.py +151 -0
- aria_code/packages/aria_skills/__init__.py +46 -0
- aria_code/packages/aria_skills/catalog.py +149 -0
- aria_code/packages/aria_skills/loader.py +455 -0
- aria_code/packages/aria_skills/registry.py +84 -0
- aria_code/packages/aria_tools/__init__.py +5 -0
- aria_code/packages/aria_tools/financial/__init__.py +8 -0
- aria_code/packages/aria_tools/financial/_paths.py +130 -0
- aria_code/packages/aria_tools/financial/compliance.py +31 -0
- aria_code/packages/aria_tools/financial/factors.py +33 -0
- aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
- aria_code/packages/aria_tools/financial/strategy.py +31 -0
- aria_code/packages/aria_tools/registry.py +128 -0
- aria_code/packages/quant_engine/__init__.py +34 -0
- aria_code/packages/quant_engine/_namespace.py +62 -0
- aria_code/packages/quant_engine/backtest/__init__.py +14 -0
- aria_code/packages/quant_engine/backtest/core.py +53 -0
- aria_code/packages/quant_engine/backtest/engine.py +96 -0
- aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
- aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
- aria_code/packages/quant_engine/sports/__init__.py +80 -0
- aria_code/packages/quant_engine/sports/calibrator.py +354 -0
- aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
- aria_code/packages/quant_engine/sports/elo.py +300 -0
- aria_code/packages/quant_engine/sports/form.py +188 -0
- aria_code/packages/quant_engine/sports/h2h.py +195 -0
- aria_code/packages/quant_engine/sports/ml_model.py +377 -0
- aria_code/packages/quant_engine/sports/predictor.py +312 -0
- aria_code/packages/quant_engine/sports/tracker.py +665 -0
- aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
- aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
- aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
- aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- aria_code/packs/__init__.py +85 -0
- aria_code/packs/base.py +168 -0
- aria_code/packs/finance/__init__.py +250 -0
- aria_code/packs/logistics/__init__.py +169 -0
- aria_code/packs/payments/__init__.py +139 -0
- aria_code/packs/realty/__init__.py +190 -0
- aria_code/packs/registry.py +202 -0
- aria_code/packs/rules.py +90 -0
- aria_code/plan_utils.py +194 -0
- aria_code/plugin_loader.py +328 -0
- aria_code/portfolio_ledger.py +4 -0
- aria_code/preview_server.py +449 -0
- aria_code/privacy/__init__.py +5 -0
- aria_code/privacy/feedback.py +123 -0
- aria_code/project_review.py +278 -0
- aria_code/project_review_api.py +235 -0
- aria_code/project_review_server.py +18 -0
- aria_code/project_tools.py +4 -0
- aria_code/providers/__init__.py +30 -0
- aria_code/providers/llm/__init__.py +19 -0
- aria_code/providers/llm/anthropic.py +184 -0
- aria_code/providers/llm/autoconfig.py +213 -0
- aria_code/providers/llm/base.py +139 -0
- aria_code/providers/llm/custom.py +158 -0
- aria_code/providers/llm/ollama.py +129 -0
- aria_code/providers/llm/openai_compat.py +313 -0
- aria_code/providers/llm/registry.py +444 -0
- aria_code/providers/local_image_provider.py +202 -0
- aria_code/providers/local_llm_provider.py +589 -0
- aria_code/realty_data_tools.py +4 -0
- aria_code/report_exporters.py +150 -0
- aria_code/report_generator.py +1764 -0
- aria_code/runtime/__init__.py +169 -0
- aria_code/runtime/acceptance.py +481 -0
- aria_code/runtime/agent_loop.py +1520 -0
- aria_code/runtime/approval.py +57 -0
- aria_code/runtime/budget.py +232 -0
- aria_code/runtime/checkpoints.py +432 -0
- aria_code/runtime/crash_report.py +174 -0
- aria_code/runtime/events.py +113 -0
- aria_code/runtime/gateway.py +150 -0
- aria_code/runtime/lsp.py +484 -0
- aria_code/runtime/orchestrator.py +55 -0
- aria_code/runtime/repo_map.py +700 -0
- aria_code/runtime/run_state.py +127 -0
- aria_code/runtime/run_store.py +450 -0
- aria_code/runtime/self_healing.py +422 -0
- aria_code/runtime/subagent.py +547 -0
- aria_code/runtime/task_ledger.py +86 -0
- aria_code/runtime/task_orchestrator.py +90 -0
- aria_code/runtime/tool_executor.py +217 -0
- aria_code/runtime/tool_policy.py +107 -0
- aria_code/runtime/worktrees.py +215 -0
- aria_code/runway_video_client.py +4 -0
- aria_code/safety/__init__.py +23 -0
- aria_code/safety/permissions.py +306 -0
- aria_code/safety/service.py +102 -0
- aria_code/setup_wizard.py +673 -0
- aria_code/spreadsheet_tools.py +4 -0
- aria_code/strategy_vault.py +4 -0
- aria_code/tools/__init__.py +0 -0
- aria_code/tools/artifact_tools.py +252 -0
- aria_code/tools/broker_tools.py +92 -0
- aria_code/tools/build_quant_engine.py +97 -0
- aria_code/tools/code_audit_tools.py +192 -0
- aria_code/tools/computer_use_tools.py +504 -0
- aria_code/tools/data_analysis_tools.py +809 -0
- aria_code/tools/enterprise_finance_tools.py +156 -0
- aria_code/tools/extended_tools.py +240 -0
- aria_code/tools/file_analysis_tools.py +885 -0
- aria_code/tools/image_gen_tools.py +114 -0
- aria_code/tools/local_finance_tools.py +3417 -0
- aria_code/tools/logistics_tools.py +135 -0
- aria_code/tools/macro_tools.py +369 -0
- aria_code/tools/notification_tools.py +249 -0
- aria_code/tools/project_tools.py +525 -0
- aria_code/tools/realty_data_tools.py +659 -0
- aria_code/tools/spreadsheet_tools.py +388 -0
- aria_code/tools/stripe_tools.py +115 -0
- aria_code/ui/__init__.py +105 -0
- aria_code/ui/banner.py +508 -0
- aria_code/ui/completer.py +501 -0
- aria_code/ui/console.py +281 -0
- aria_code/ui/image_render.py +243 -0
- aria_code/ui/input_box.py +488 -0
- aria_code/ui/picker.py +201 -0
- aria_code/ui/render/__init__.py +11 -0
- aria_code/ui/render/finance.py +1705 -0
- aria_code/ui/render/market.py +225 -0
- aria_code/ui/render/output.py +775 -0
- aria_code/ui/render/responsive.py +75 -0
- aria_code/ui/render/team.py +354 -0
- aria_code/ui/robot.py +235 -0
- aria_code/ui/startup_dashboard.py +109 -0
- aria_code/utils/vscode_uri.py +37 -0
- aria_code/video_analysis.py +147 -0
- aria_code/video_editor.py +271 -0
- aria_code/workspace/__init__.py +6 -0
- aria_code/workspace/files.py +193 -0
- aria_code/workspace/verify.py +113 -0
- aria_code/workspace_changes.py +199 -0
- aria_code/workspace_context.py +233 -0
- aria_code/workspace_service.py +196 -0
- aria_code/workspace_tools.py +150 -0
- aria_code-0.52.0.dist-info/METADATA +237 -0
- aria_code-0.52.0.dist-info/RECORD +478 -0
- aria_code-0.52.0.dist-info/WHEEL +5 -0
- aria_code-0.52.0.dist-info/entry_points.txt +3 -0
- aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
- aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
- aria_code-0.52.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
"""Durable, conflict-aware file checkpoints for agent write operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sqlite3
|
|
9
|
+
import stat
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, Iterable, Optional
|
|
15
|
+
|
|
16
|
+
from .run_store import RunStore
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _sha256(content: str) -> str:
|
|
20
|
+
return hashlib.sha256(content.encode("utf-8", errors="replace")).hexdigest()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _json_dumps(value: Any) -> str:
|
|
24
|
+
return json.dumps(value, ensure_ascii=False, default=str, separators=(",", ":"))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _json_loads(value: str | None) -> Dict[str, Any]:
|
|
28
|
+
if not value:
|
|
29
|
+
return {}
|
|
30
|
+
try:
|
|
31
|
+
loaded = json.loads(value)
|
|
32
|
+
return loaded if isinstance(loaded, dict) else {"value": loaded}
|
|
33
|
+
except (TypeError, json.JSONDecodeError):
|
|
34
|
+
return {}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass(frozen=True)
|
|
38
|
+
class CheckpointFile:
|
|
39
|
+
checkpoint_id: str
|
|
40
|
+
path: str
|
|
41
|
+
existed_before: bool
|
|
42
|
+
before_content: str
|
|
43
|
+
after_content: str
|
|
44
|
+
before_hash: str
|
|
45
|
+
after_hash: str
|
|
46
|
+
before_mode: Optional[int]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True)
|
|
50
|
+
class CheckpointRecord:
|
|
51
|
+
checkpoint_id: str
|
|
52
|
+
sequence: int
|
|
53
|
+
run_id: Optional[str]
|
|
54
|
+
session_id: str
|
|
55
|
+
source: str
|
|
56
|
+
label: str
|
|
57
|
+
status: str
|
|
58
|
+
created_at: float
|
|
59
|
+
restored_at: Optional[float]
|
|
60
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
61
|
+
files: tuple[CheckpointFile, ...] = ()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class RestoreResult:
|
|
66
|
+
checkpoint_ids: tuple[str, ...]
|
|
67
|
+
run_id: Optional[str]
|
|
68
|
+
restored_paths: tuple[str, ...]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class CheckpointNotFoundError(KeyError):
|
|
72
|
+
"""Raised when a checkpoint or checkpoint group cannot be found."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class CheckpointConflictError(RuntimeError):
|
|
76
|
+
"""Raised when a file changed after the checkpoint was recorded."""
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class CheckpointStore:
|
|
80
|
+
"""Store file pre-images and restore them only when hashes still match."""
|
|
81
|
+
|
|
82
|
+
def __init__(self, database_path: Path | str | None = None) -> None:
|
|
83
|
+
self.run_store = RunStore(database_path)
|
|
84
|
+
self.database_path = self.run_store.database_path
|
|
85
|
+
self._initialize()
|
|
86
|
+
|
|
87
|
+
def _connect(self) -> sqlite3.Connection:
|
|
88
|
+
connection = sqlite3.connect(str(self.database_path), timeout=10.0)
|
|
89
|
+
connection.row_factory = sqlite3.Row
|
|
90
|
+
connection.execute("PRAGMA foreign_keys = ON")
|
|
91
|
+
connection.execute("PRAGMA busy_timeout = 10000")
|
|
92
|
+
return connection
|
|
93
|
+
|
|
94
|
+
def _initialize(self) -> None:
|
|
95
|
+
with self._connect() as connection:
|
|
96
|
+
connection.executescript(
|
|
97
|
+
"""
|
|
98
|
+
CREATE TABLE IF NOT EXISTS checkpoints (
|
|
99
|
+
sequence INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
100
|
+
checkpoint_id TEXT NOT NULL UNIQUE,
|
|
101
|
+
run_id TEXT,
|
|
102
|
+
session_id TEXT NOT NULL DEFAULT '',
|
|
103
|
+
source TEXT NOT NULL,
|
|
104
|
+
label TEXT NOT NULL DEFAULT '',
|
|
105
|
+
status TEXT NOT NULL DEFAULT 'active',
|
|
106
|
+
created_at REAL NOT NULL,
|
|
107
|
+
restored_at REAL,
|
|
108
|
+
metadata_json TEXT NOT NULL DEFAULT '{}',
|
|
109
|
+
FOREIGN KEY(run_id) REFERENCES runs(run_id)
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
CREATE TABLE IF NOT EXISTS checkpoint_files (
|
|
113
|
+
checkpoint_id TEXT NOT NULL,
|
|
114
|
+
path TEXT NOT NULL,
|
|
115
|
+
existed_before INTEGER NOT NULL,
|
|
116
|
+
before_content TEXT NOT NULL,
|
|
117
|
+
after_content TEXT NOT NULL,
|
|
118
|
+
before_hash TEXT NOT NULL,
|
|
119
|
+
after_hash TEXT NOT NULL,
|
|
120
|
+
before_mode INTEGER,
|
|
121
|
+
PRIMARY KEY(checkpoint_id, path),
|
|
122
|
+
FOREIGN KEY(checkpoint_id)
|
|
123
|
+
REFERENCES checkpoints(checkpoint_id) ON DELETE CASCADE
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_run_sequence
|
|
127
|
+
ON checkpoints(run_id, sequence DESC);
|
|
128
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_session_sequence
|
|
129
|
+
ON checkpoints(session_id, sequence DESC);
|
|
130
|
+
CREATE INDEX IF NOT EXISTS idx_checkpoints_status_sequence
|
|
131
|
+
ON checkpoints(status, sequence DESC);
|
|
132
|
+
"""
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
@staticmethod
|
|
136
|
+
def _row_to_file(row: sqlite3.Row) -> CheckpointFile:
|
|
137
|
+
return CheckpointFile(
|
|
138
|
+
checkpoint_id=row["checkpoint_id"],
|
|
139
|
+
path=row["path"],
|
|
140
|
+
existed_before=bool(row["existed_before"]),
|
|
141
|
+
before_content=row["before_content"],
|
|
142
|
+
after_content=row["after_content"],
|
|
143
|
+
before_hash=row["before_hash"],
|
|
144
|
+
after_hash=row["after_hash"],
|
|
145
|
+
before_mode=int(row["before_mode"]) if row["before_mode"] is not None else None,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
def _row_to_checkpoint(
|
|
149
|
+
self,
|
|
150
|
+
connection: sqlite3.Connection,
|
|
151
|
+
row: sqlite3.Row,
|
|
152
|
+
) -> CheckpointRecord:
|
|
153
|
+
file_rows = connection.execute(
|
|
154
|
+
"SELECT * FROM checkpoint_files WHERE checkpoint_id = ? ORDER BY path",
|
|
155
|
+
(row["checkpoint_id"],),
|
|
156
|
+
).fetchall()
|
|
157
|
+
return CheckpointRecord(
|
|
158
|
+
checkpoint_id=row["checkpoint_id"],
|
|
159
|
+
sequence=int(row["sequence"]),
|
|
160
|
+
run_id=row["run_id"],
|
|
161
|
+
session_id=row["session_id"],
|
|
162
|
+
source=row["source"],
|
|
163
|
+
label=row["label"],
|
|
164
|
+
status=row["status"],
|
|
165
|
+
created_at=float(row["created_at"]),
|
|
166
|
+
restored_at=float(row["restored_at"]) if row["restored_at"] is not None else None,
|
|
167
|
+
metadata=_json_loads(row["metadata_json"]),
|
|
168
|
+
files=tuple(self._row_to_file(file_row) for file_row in file_rows),
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def record_change(
|
|
172
|
+
self,
|
|
173
|
+
*,
|
|
174
|
+
path: Path | str,
|
|
175
|
+
before_content: str,
|
|
176
|
+
after_content: str,
|
|
177
|
+
existed_before: bool,
|
|
178
|
+
source: str,
|
|
179
|
+
run_id: str | None = None,
|
|
180
|
+
session_id: str = "",
|
|
181
|
+
before_mode: int | None = None,
|
|
182
|
+
label: str = "",
|
|
183
|
+
metadata: Dict[str, Any] | None = None,
|
|
184
|
+
) -> CheckpointRecord:
|
|
185
|
+
target = Path(path).expanduser().resolve()
|
|
186
|
+
if not target.is_file():
|
|
187
|
+
raise CheckpointConflictError(f"Written file is missing: {target}")
|
|
188
|
+
current = target.read_text(encoding="utf-8", errors="replace")
|
|
189
|
+
after_hash = _sha256(after_content)
|
|
190
|
+
if _sha256(current) != after_hash:
|
|
191
|
+
raise CheckpointConflictError(
|
|
192
|
+
f"File changed before checkpoint could be recorded: {target}"
|
|
193
|
+
)
|
|
194
|
+
if run_id and self.run_store.get_run(run_id) is None:
|
|
195
|
+
raise CheckpointNotFoundError(f"Unknown run id: {run_id}")
|
|
196
|
+
|
|
197
|
+
checkpoint_id = uuid.uuid4().hex[:16]
|
|
198
|
+
created_at = time.time()
|
|
199
|
+
mode = before_mode
|
|
200
|
+
if existed_before and mode is None:
|
|
201
|
+
mode = stat.S_IMODE(target.stat().st_mode)
|
|
202
|
+
with self._connect() as connection:
|
|
203
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
204
|
+
connection.execute(
|
|
205
|
+
"""INSERT INTO checkpoints
|
|
206
|
+
(checkpoint_id, run_id, session_id, source, label, status,
|
|
207
|
+
created_at, restored_at, metadata_json)
|
|
208
|
+
VALUES (?, ?, ?, ?, ?, 'active', ?, NULL, ?)""",
|
|
209
|
+
(
|
|
210
|
+
checkpoint_id,
|
|
211
|
+
run_id,
|
|
212
|
+
session_id,
|
|
213
|
+
source,
|
|
214
|
+
label,
|
|
215
|
+
created_at,
|
|
216
|
+
_json_dumps(metadata or {}),
|
|
217
|
+
),
|
|
218
|
+
)
|
|
219
|
+
connection.execute(
|
|
220
|
+
"""INSERT INTO checkpoint_files
|
|
221
|
+
(checkpoint_id, path, existed_before, before_content,
|
|
222
|
+
after_content, before_hash, after_hash, before_mode)
|
|
223
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)""",
|
|
224
|
+
(
|
|
225
|
+
checkpoint_id,
|
|
226
|
+
str(target),
|
|
227
|
+
int(existed_before),
|
|
228
|
+
before_content,
|
|
229
|
+
after_content,
|
|
230
|
+
_sha256(before_content),
|
|
231
|
+
after_hash,
|
|
232
|
+
mode,
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
row = connection.execute(
|
|
236
|
+
"SELECT * FROM checkpoints WHERE checkpoint_id = ?",
|
|
237
|
+
(checkpoint_id,),
|
|
238
|
+
).fetchone()
|
|
239
|
+
|
|
240
|
+
if run_id:
|
|
241
|
+
self.run_store.append_event(run_id, "checkpoint_created", {
|
|
242
|
+
"checkpoint_id": checkpoint_id,
|
|
243
|
+
"path": str(target),
|
|
244
|
+
"source": source,
|
|
245
|
+
})
|
|
246
|
+
assert row is not None
|
|
247
|
+
with self._connect() as connection:
|
|
248
|
+
return self._row_to_checkpoint(connection, row)
|
|
249
|
+
|
|
250
|
+
def get(self, checkpoint_id: str) -> Optional[CheckpointRecord]:
|
|
251
|
+
key = (checkpoint_id or "").strip()
|
|
252
|
+
with self._connect() as connection:
|
|
253
|
+
row = connection.execute(
|
|
254
|
+
"SELECT * FROM checkpoints WHERE checkpoint_id = ?",
|
|
255
|
+
(key,),
|
|
256
|
+
).fetchone()
|
|
257
|
+
if row is None and key:
|
|
258
|
+
rows = connection.execute(
|
|
259
|
+
"SELECT * FROM checkpoints WHERE checkpoint_id LIKE ? ORDER BY sequence DESC LIMIT 2",
|
|
260
|
+
(f"{key}%",),
|
|
261
|
+
).fetchall()
|
|
262
|
+
row = rows[0] if len(rows) == 1 else None
|
|
263
|
+
return self._row_to_checkpoint(connection, row) if row is not None else None
|
|
264
|
+
|
|
265
|
+
def list(
|
|
266
|
+
self,
|
|
267
|
+
*,
|
|
268
|
+
run_id: str | None = None,
|
|
269
|
+
session_id: str | None = None,
|
|
270
|
+
status: str | None = "active",
|
|
271
|
+
limit: int = 50,
|
|
272
|
+
) -> list[CheckpointRecord]:
|
|
273
|
+
clauses = []
|
|
274
|
+
values: list[Any] = []
|
|
275
|
+
if run_id:
|
|
276
|
+
clauses.append("run_id = ?")
|
|
277
|
+
values.append(run_id)
|
|
278
|
+
if session_id:
|
|
279
|
+
clauses.append("session_id = ?")
|
|
280
|
+
values.append(session_id)
|
|
281
|
+
if status:
|
|
282
|
+
clauses.append("status = ?")
|
|
283
|
+
values.append(status)
|
|
284
|
+
where = f" WHERE {' AND '.join(clauses)}" if clauses else ""
|
|
285
|
+
values.append(max(1, int(limit)))
|
|
286
|
+
with self._connect() as connection:
|
|
287
|
+
rows = connection.execute(
|
|
288
|
+
f"SELECT * FROM checkpoints{where} ORDER BY sequence DESC LIMIT ?",
|
|
289
|
+
values,
|
|
290
|
+
).fetchall()
|
|
291
|
+
return [self._row_to_checkpoint(connection, row) for row in rows]
|
|
292
|
+
|
|
293
|
+
def latest_run_id(self, *, session_id: str | None = None) -> Optional[str]:
|
|
294
|
+
clauses = ["status = 'active'", "run_id IS NOT NULL"]
|
|
295
|
+
values: list[Any] = []
|
|
296
|
+
if session_id:
|
|
297
|
+
clauses.append("session_id = ?")
|
|
298
|
+
values.append(session_id)
|
|
299
|
+
with self._connect() as connection:
|
|
300
|
+
row = connection.execute(
|
|
301
|
+
f"SELECT run_id FROM checkpoints WHERE {' AND '.join(clauses)} "
|
|
302
|
+
"ORDER BY sequence DESC LIMIT 1",
|
|
303
|
+
values,
|
|
304
|
+
).fetchone()
|
|
305
|
+
return row["run_id"] if row is not None else None
|
|
306
|
+
|
|
307
|
+
@staticmethod
|
|
308
|
+
def _snapshot(path: Path) -> tuple[bool, str, Optional[int]]:
|
|
309
|
+
if not path.exists():
|
|
310
|
+
return False, "", None
|
|
311
|
+
if not path.is_file():
|
|
312
|
+
raise CheckpointConflictError(f"Checkpoint path is not a file: {path}")
|
|
313
|
+
return (
|
|
314
|
+
True,
|
|
315
|
+
path.read_text(encoding="utf-8", errors="replace"),
|
|
316
|
+
stat.S_IMODE(path.stat().st_mode),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
@staticmethod
|
|
320
|
+
def _write_atomic(path: Path, content: str, mode: int | None) -> None:
|
|
321
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
322
|
+
temporary = path.with_name(f".{path.name}.aria-rewind-{uuid.uuid4().hex[:8]}")
|
|
323
|
+
try:
|
|
324
|
+
temporary.write_text(content, encoding="utf-8")
|
|
325
|
+
if mode is not None:
|
|
326
|
+
temporary.chmod(mode)
|
|
327
|
+
os.replace(temporary, path)
|
|
328
|
+
finally:
|
|
329
|
+
if temporary.exists():
|
|
330
|
+
temporary.unlink()
|
|
331
|
+
|
|
332
|
+
def _restore(self, checkpoints: Iterable[CheckpointRecord]) -> RestoreResult:
|
|
333
|
+
records = list(checkpoints)
|
|
334
|
+
if not records:
|
|
335
|
+
raise CheckpointNotFoundError("No active checkpoints to restore")
|
|
336
|
+
files = [file for record in records for file in record.files]
|
|
337
|
+
if not files:
|
|
338
|
+
raise CheckpointNotFoundError("Checkpoint contains no files")
|
|
339
|
+
|
|
340
|
+
simulated: Dict[str, tuple[bool, str]] = {}
|
|
341
|
+
backups: Dict[str, tuple[bool, str, Optional[int]]] = {}
|
|
342
|
+
for file in files:
|
|
343
|
+
path = Path(file.path)
|
|
344
|
+
if file.path not in simulated:
|
|
345
|
+
exists, content, mode = self._snapshot(path)
|
|
346
|
+
backups[file.path] = (exists, content, mode)
|
|
347
|
+
simulated[file.path] = (exists, _sha256(content))
|
|
348
|
+
exists, current_hash = simulated[file.path]
|
|
349
|
+
if not exists or current_hash != file.after_hash:
|
|
350
|
+
raise CheckpointConflictError(
|
|
351
|
+
f"Refusing to overwrite a file changed after the checkpoint: {path}"
|
|
352
|
+
)
|
|
353
|
+
simulated[file.path] = (file.existed_before, file.before_hash)
|
|
354
|
+
|
|
355
|
+
restored_paths: list[str] = []
|
|
356
|
+
try:
|
|
357
|
+
for file in files:
|
|
358
|
+
path = Path(file.path)
|
|
359
|
+
exists, current, _ = self._snapshot(path)
|
|
360
|
+
if not exists or _sha256(current) != file.after_hash:
|
|
361
|
+
raise CheckpointConflictError(
|
|
362
|
+
f"File changed while rewind was running: {path}"
|
|
363
|
+
)
|
|
364
|
+
if file.existed_before:
|
|
365
|
+
self._write_atomic(path, file.before_content, file.before_mode)
|
|
366
|
+
else:
|
|
367
|
+
path.unlink()
|
|
368
|
+
if file.path not in restored_paths:
|
|
369
|
+
restored_paths.append(file.path)
|
|
370
|
+
except Exception:
|
|
371
|
+
for path_text, (existed, content, mode) in backups.items():
|
|
372
|
+
path = Path(path_text)
|
|
373
|
+
if existed:
|
|
374
|
+
self._write_atomic(path, content, mode)
|
|
375
|
+
elif path.exists() and path.is_file():
|
|
376
|
+
path.unlink()
|
|
377
|
+
raise
|
|
378
|
+
|
|
379
|
+
restored_at = time.time()
|
|
380
|
+
checkpoint_ids = tuple(record.checkpoint_id for record in records)
|
|
381
|
+
with self._connect() as connection:
|
|
382
|
+
connection.execute("BEGIN IMMEDIATE")
|
|
383
|
+
connection.executemany(
|
|
384
|
+
"UPDATE checkpoints SET status = 'restored', restored_at = ? WHERE checkpoint_id = ?",
|
|
385
|
+
[(restored_at, checkpoint_id) for checkpoint_id in checkpoint_ids],
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
run_id = records[0].run_id if all(
|
|
389
|
+
record.run_id == records[0].run_id for record in records
|
|
390
|
+
) else None
|
|
391
|
+
if run_id:
|
|
392
|
+
self.run_store.append_event(run_id, "checkpoint_restored", {
|
|
393
|
+
"checkpoint_ids": list(checkpoint_ids),
|
|
394
|
+
"paths": restored_paths,
|
|
395
|
+
})
|
|
396
|
+
return RestoreResult(checkpoint_ids, run_id, tuple(restored_paths))
|
|
397
|
+
|
|
398
|
+
def restore_checkpoint(self, checkpoint_id: str) -> RestoreResult:
|
|
399
|
+
record = self.get(checkpoint_id)
|
|
400
|
+
if record is None or record.status != "active":
|
|
401
|
+
raise CheckpointNotFoundError(checkpoint_id)
|
|
402
|
+
return self._restore([record])
|
|
403
|
+
|
|
404
|
+
def restore_run(self, run_id: str) -> RestoreResult:
|
|
405
|
+
records = self.list(run_id=run_id, status="active", limit=10_000)
|
|
406
|
+
if not records:
|
|
407
|
+
raise CheckpointNotFoundError(run_id)
|
|
408
|
+
return self._restore(records)
|
|
409
|
+
|
|
410
|
+
def restore_latest_run(self, *, session_id: str | None = None) -> RestoreResult:
|
|
411
|
+
run_id = self.latest_run_id(session_id=session_id)
|
|
412
|
+
if not run_id:
|
|
413
|
+
raise CheckpointNotFoundError("No active run checkpoint found")
|
|
414
|
+
return self.restore_run(run_id)
|
|
415
|
+
|
|
416
|
+
def restore_latest(self, *, session_id: str | None = None) -> RestoreResult:
|
|
417
|
+
clauses = ["status = 'active'"]
|
|
418
|
+
values: list[Any] = []
|
|
419
|
+
if session_id:
|
|
420
|
+
clauses.append("session_id = ?")
|
|
421
|
+
values.append(session_id)
|
|
422
|
+
with self._connect() as connection:
|
|
423
|
+
row = connection.execute(
|
|
424
|
+
f"SELECT checkpoint_id, run_id FROM checkpoints "
|
|
425
|
+
f"WHERE {' AND '.join(clauses)} ORDER BY sequence DESC LIMIT 1",
|
|
426
|
+
values,
|
|
427
|
+
).fetchone()
|
|
428
|
+
if row is None:
|
|
429
|
+
raise CheckpointNotFoundError("No active checkpoint found")
|
|
430
|
+
if row["run_id"]:
|
|
431
|
+
return self.restore_run(row["run_id"])
|
|
432
|
+
return self.restore_checkpoint(row["checkpoint_id"])
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""崩溃记录:本地优先,不向任何第三方发送。
|
|
2
|
+
|
|
3
|
+
问题:CLI 未捕获异常时只会打一个裸 traceback 就退出,什么都不留。用户来报
|
|
4
|
+
bug 时能提供的往往只有"它崩了"——版本、provider、走到哪一步、当时的配置
|
|
5
|
+
全部丢失,只能靠来回追问重建现场。
|
|
6
|
+
|
|
7
|
+
为什么不直接接 Sentry:aria-code 是 local-first 的终端工具,用户的堆栈里可能
|
|
8
|
+
带着文件路径、项目名、甚至提示词片段。默认把这些发到第三方服务器,跟这个
|
|
9
|
+
产品的定位相悖。这里的做法是**先在本地留全**,用户愿意时再自己把文件贴出来;
|
|
10
|
+
需要 Sentry 的部署可以自行在外层包一层,本模块不阻止。
|
|
11
|
+
|
|
12
|
+
写入 ~/.aria-code/crashes/(随 aria_home() 走,老用户仍落在 ~/.arthera/)。
|
|
13
|
+
|
|
14
|
+
脱敏是**尽力而为**,不是保证:环境变量里的密钥、明显的 key 形态会被打码,
|
|
15
|
+
但堆栈的局部变量里可能藏着任何东西。所以文件默认只在本地,且提示用户贴出前
|
|
16
|
+
自己看一眼。把这条说清楚,比假装"已完全脱敏"要诚实。
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import platform
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
import traceback
|
|
27
|
+
from datetime import datetime, timezone
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, Dict, List, Optional
|
|
30
|
+
|
|
31
|
+
__all__ = ["write_crash_report", "install_excepthook", "recent_crashes", "redact"]
|
|
32
|
+
|
|
33
|
+
_MAX_KEPT = 20
|
|
34
|
+
|
|
35
|
+
# 环境变量名里出现这些片段,值一律打码
|
|
36
|
+
_SECRET_HINTS = ("KEY", "TOKEN", "SECRET", "PASSWORD", "CREDENTIAL", "AUTH")
|
|
37
|
+
|
|
38
|
+
# 值本身长得像密钥的,即使变量名无辜也打码
|
|
39
|
+
_SECRET_VALUE_RE = re.compile(
|
|
40
|
+
r"\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}"
|
|
41
|
+
r"|AIza[A-Za-z0-9_-]{30,}|[A-Za-z0-9_-]{32,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,})\b"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def redact(text: str) -> str:
|
|
46
|
+
"""给明显的密钥形态打码。尽力而为——见模块文档。"""
|
|
47
|
+
return _SECRET_VALUE_RE.sub("<redacted>", text or "")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _crash_dir() -> Path:
|
|
51
|
+
try:
|
|
52
|
+
from packages.aria_core.paths import aria_home
|
|
53
|
+
root = aria_home()
|
|
54
|
+
except Exception:
|
|
55
|
+
root = Path.home() / ".aria-code"
|
|
56
|
+
return root / "crashes"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _safe_env() -> Dict[str, str]:
|
|
60
|
+
"""只收跟 aria 相关的环境变量,且对疑似密钥打码。
|
|
61
|
+
|
|
62
|
+
不收全量环境:那里面有用户的 PATH、代理地址、其它项目的凭证,跟这次崩溃
|
|
63
|
+
多半无关,收进来只是扩大暴露面。
|
|
64
|
+
"""
|
|
65
|
+
out: Dict[str, str] = {}
|
|
66
|
+
for key, value in os.environ.items():
|
|
67
|
+
if not any(p in key.upper() for p in ("ARIA", "ARTHERA", "OLLAMA", "LMSTUDIO")):
|
|
68
|
+
continue
|
|
69
|
+
if any(h in key.upper() for h in _SECRET_HINTS):
|
|
70
|
+
out[key] = f"<set:{len(value)}chars>" if value else "<empty>"
|
|
71
|
+
else:
|
|
72
|
+
out[key] = redact(value)[:200]
|
|
73
|
+
return out
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def write_crash_report(
|
|
77
|
+
exc: BaseException,
|
|
78
|
+
*,
|
|
79
|
+
context: Optional[Dict[str, Any]] = None,
|
|
80
|
+
) -> Optional[Path]:
|
|
81
|
+
"""把一次崩溃写成本地 JSON。返回文件路径;写入失败返回 None。
|
|
82
|
+
|
|
83
|
+
写入失败绝不抛异常——崩溃记录器自己再崩一次,只会把原始错误从屏幕上
|
|
84
|
+
冲掉,让排查更难。
|
|
85
|
+
"""
|
|
86
|
+
try:
|
|
87
|
+
crash_dir = _crash_dir()
|
|
88
|
+
crash_dir.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
|
|
90
|
+
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
|
|
91
|
+
path = crash_dir / f"crash-{stamp}.json"
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
from aria_code._version import __version__ as _ver
|
|
95
|
+
except Exception:
|
|
96
|
+
_ver = "unknown"
|
|
97
|
+
|
|
98
|
+
payload = {
|
|
99
|
+
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
100
|
+
"version": _ver,
|
|
101
|
+
"python": sys.version.split()[0],
|
|
102
|
+
"platform": platform.platform(),
|
|
103
|
+
"error_type": type(exc).__name__,
|
|
104
|
+
"error": redact(str(exc))[:1000],
|
|
105
|
+
"traceback": redact("".join(traceback.format_exception(exc)))[-8000:],
|
|
106
|
+
"argv": [redact(a) for a in sys.argv[:12]],
|
|
107
|
+
"env": _safe_env(),
|
|
108
|
+
"context": {k: redact(str(v))[:500] for k, v in (context or {}).items()},
|
|
109
|
+
}
|
|
110
|
+
path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
111
|
+
_prune(crash_dir)
|
|
112
|
+
return path
|
|
113
|
+
except Exception:
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _prune(crash_dir: Path, keep: int = _MAX_KEPT) -> None:
|
|
118
|
+
"""只保留最近 N 份。崩溃记录是排查素材,不是需要永久保存的资产;
|
|
119
|
+
无上限增长的日志目录本身就会变成一个问题。"""
|
|
120
|
+
try:
|
|
121
|
+
files = sorted(crash_dir.glob("crash-*.json"), key=lambda p: p.stat().st_mtime)
|
|
122
|
+
for stale in files[:-keep]:
|
|
123
|
+
stale.unlink(missing_ok=True)
|
|
124
|
+
except Exception:
|
|
125
|
+
pass
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def recent_crashes(limit: int = 5) -> List[Dict[str, Any]]:
|
|
129
|
+
"""最近几次崩溃的摘要,供 /doctor 之类的命令展示。"""
|
|
130
|
+
out: List[Dict[str, Any]] = []
|
|
131
|
+
try:
|
|
132
|
+
files = sorted(_crash_dir().glob("crash-*.json"),
|
|
133
|
+
key=lambda p: p.stat().st_mtime, reverse=True)
|
|
134
|
+
for path in files[:limit]:
|
|
135
|
+
try:
|
|
136
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
137
|
+
out.append({
|
|
138
|
+
"file": str(path),
|
|
139
|
+
"timestamp": data.get("timestamp", ""),
|
|
140
|
+
"error_type": data.get("error_type", ""),
|
|
141
|
+
"error": (data.get("error", "") or "")[:160],
|
|
142
|
+
"version": data.get("version", ""),
|
|
143
|
+
})
|
|
144
|
+
except Exception:
|
|
145
|
+
continue
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
return out
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def install_excepthook(*, context_provider=None) -> None:
|
|
152
|
+
"""接管未捕获异常:先落盘,再按原样打印。
|
|
153
|
+
|
|
154
|
+
**保留原有输出**——用户和现有的排查习惯都依赖屏幕上那个 traceback,
|
|
155
|
+
悄悄换掉只会让人以为程序静默失败了。这里只是在它之前多留一份记录。
|
|
156
|
+
|
|
157
|
+
KeyboardInterrupt 不记:那是用户主动中断,不是故障。
|
|
158
|
+
"""
|
|
159
|
+
previous = sys.excepthook
|
|
160
|
+
|
|
161
|
+
def _hook(exc_type, exc_value, exc_tb):
|
|
162
|
+
if not issubclass(exc_type, KeyboardInterrupt):
|
|
163
|
+
ctx = {}
|
|
164
|
+
if context_provider is not None:
|
|
165
|
+
try:
|
|
166
|
+
ctx = context_provider() or {}
|
|
167
|
+
except Exception:
|
|
168
|
+
ctx = {}
|
|
169
|
+
path = write_crash_report(exc_value, context=ctx)
|
|
170
|
+
if path is not None:
|
|
171
|
+
print(f"\n[crash report] {path}", file=sys.stderr)
|
|
172
|
+
previous(exc_type, exc_value, exc_tb)
|
|
173
|
+
|
|
174
|
+
sys.excepthook = _hook
|