aria-code 0.167.0__py3-none-win_amd64.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_bots/__init__.py +7 -0
- aria_bots/channels/README.md +97 -0
- aria_bots/channels/__init__.py +1 -0
- aria_bots/channels/approvals.py +263 -0
- aria_bots/channels/conversation.py +280 -0
- aria_bots/channels/digest.py +186 -0
- aria_bots/channels/registry.py +95 -0
- aria_bots/engine.py +144 -0
- aria_bots/feishu_bot.py +1385 -0
- aria_bots/logistics/__init__.py +1 -0
- aria_bots/logistics/carriers.py +253 -0
- aria_bots/logistics/freight.py +154 -0
- aria_bots/logistics/inventory.py +242 -0
- aria_bots/logistics/mcp_server.py +156 -0
- aria_bots/logistics/records.py +119 -0
- aria_bots/logistics/tenancy.py +152 -0
- aria_bots/market.py +91 -0
- aria_bots/relay_client.py +326 -0
- aria_bots/telegram_bot.py +299 -0
- aria_code/__init__.py +111 -0
- aria_code/_bin/aria-native.exe +0 -0
- aria_code/_bin/aria-runtime.exe +0 -0
- aria_code/_version.py +20 -0
- aria_code/account_memory.py +296 -0
- aria_code/account_session.py +166 -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 +49 -0
- aria_code/agents/base.py +329 -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 +315 -0
- aria_code/agents/financial/fundamental.py +178 -0
- aria_code/agents/financial/macro.py +111 -0
- aria_code/agents/financial/news.py +231 -0
- aria_code/agents/financial/risk.py +146 -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 +277 -0
- aria_code/agents/orchestrator.py +44 -0
- aria_code/agents/portfolio_agent.py +392 -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 +168 -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 +500 -0
- aria_code/agents/video/__init__.py +16 -0
- aria_code/agents/video/director.py +376 -0
- aria_code/agents/video/qa_agent.py +143 -0
- aria_code/agents/video/script_doctor.py +112 -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/navigation.schema.json +110 -0
- aria_code/agents/warehouse/task_agent.py +172 -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 +9 -0
- aria_code/apps/channels/__init__.py +17 -0
- aria_code/apps/channels/approvals.py +11 -0
- aria_code/apps/channels/conversation.py +11 -0
- aria_code/apps/channels/digest.py +11 -0
- aria_code/apps/channels/intake.py +92 -0
- aria_code/apps/channels/registry.py +11 -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/app_server.py +656 -0
- aria_code/apps/cli/auto_update.py +125 -0
- aria_code/apps/cli/bootstrap.py +206 -0
- aria_code/apps/cli/broker_render.py +206 -0
- aria_code/apps/cli/chat_turn.py +1327 -0
- aria_code/apps/cli/cloud_models.py +231 -0
- aria_code/apps/cli/codegen_paths.py +29 -0
- aria_code/apps/cli/commands/__init__.py +34 -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 +378 -0
- aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
- aria_code/apps/cli/commands/auth_cmds.py +215 -0
- aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
- aria_code/apps/cli/commands/broker_cmds.py +1236 -0
- aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
- aria_code/apps/cli/commands/canvas_cmds.py +77 -0
- aria_code/apps/cli/commands/catalog.py +189 -0
- aria_code/apps/cli/commands/core_cmds.py +2748 -0
- aria_code/apps/cli/commands/data_cmds.py +521 -0
- aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
- aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
- aria_code/apps/cli/commands/engineering_cmds.py +399 -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 +544 -0
- aria_code/apps/cli/commands/market_cmds.py +1419 -0
- aria_code/apps/cli/commands/market_context.py +432 -0
- aria_code/apps/cli/commands/market_render.py +7 -0
- aria_code/apps/cli/commands/model_cmds.py +2374 -0
- aria_code/apps/cli/commands/ops_cmds.py +721 -0
- aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
- aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
- aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
- aria_code/apps/cli/commands/report.py +476 -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 +723 -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 +297 -0
- aria_code/apps/cli/commands/workflow_cmds.py +629 -0
- aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
- aria_code/apps/cli/config_paths.py +73 -0
- aria_code/apps/cli/config_store.py +158 -0
- aria_code/apps/cli/constants.py +88 -0
- aria_code/apps/cli/context.py +34 -0
- aria_code/apps/cli/conversation_policy.py +53 -0
- aria_code/apps/cli/deterministic.py +200 -0
- aria_code/apps/cli/direct.py +48 -0
- aria_code/apps/cli/earnings_view.py +172 -0
- aria_code/apps/cli/endpoint_migration.py +64 -0
- aria_code/apps/cli/exec_events.py +255 -0
- aria_code/apps/cli/file_references.py +155 -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 +239 -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 +1314 -0
- aria_code/apps/cli/handlers/market_handlers.py +2570 -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/headless.py +352 -0
- aria_code/apps/cli/helpers.py +326 -0
- aria_code/apps/cli/hooks.py +180 -0
- aria_code/apps/cli/i18n.py +310 -0
- aria_code/apps/cli/intent.py +136 -0
- aria_code/apps/cli/intent_router.py +305 -0
- aria_code/apps/cli/intent_shadow.py +72 -0
- aria_code/apps/cli/intent_signals.py +347 -0
- aria_code/apps/cli/lifecycle_hooks.py +48 -0
- aria_code/apps/cli/local_tool_schemas.py +550 -0
- aria_code/apps/cli/main.py +39 -0
- aria_code/apps/cli/market_hours.py +100 -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/mixin_binding.py +32 -0
- aria_code/apps/cli/model_catalog.py +652 -0
- aria_code/apps/cli/model_probe.py +169 -0
- aria_code/apps/cli/model_retirement.py +65 -0
- aria_code/apps/cli/native_branding.py +12 -0
- aria_code/apps/cli/native_bridge.py +118 -0
- aria_code/apps/cli/native_entry.py +58 -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 +78 -0
- aria_code/apps/cli/prompts/__init__.py +0 -0
- aria_code/apps/cli/prompts/coding.py +71 -0
- aria_code/apps/cli/prompts/select.py +193 -0
- aria_code/apps/cli/prompts/system_prompts.py +562 -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 +775 -0
- aria_code/apps/cli/providers/chat_routing.py +139 -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 +1574 -0
- aria_code/apps/cli/providers/llm/response_cache.py +60 -0
- aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
- aria_code/apps/cli/providers/runtime_bridge.py +586 -0
- aria_code/apps/cli/providers/vertexai_stream.py +592 -0
- aria_code/apps/cli/release_download.py +159 -0
- aria_code/apps/cli/review_runner.py +111 -0
- aria_code/apps/cli/runtime_consumer.py +769 -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_refresh.py +123 -0
- aria_code/apps/cli/session_store.py +125 -0
- aria_code/apps/cli/skills_catalog.py +273 -0
- aria_code/apps/cli/task_isolation.py +211 -0
- aria_code/apps/cli/terminal_chrome.py +412 -0
- aria_code/apps/cli/todo_tracker.py +190 -0
- aria_code/apps/cli/tool_executor.py +932 -0
- aria_code/apps/cli/tool_registry.py +34 -0
- aria_code/apps/cli/tool_scope.py +113 -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 +181 -0
- aria_code/apps/cli/tools/market_tools.py +626 -0
- aria_code/apps/cli/tools/notebook_tools.py +111 -0
- aria_code/apps/cli/tools/system_tools.py +656 -0
- aria_code/apps/cli/tools/write_tools.py +961 -0
- aria_code/apps/cli/tradingview_bridge.py +529 -0
- aria_code/apps/cli/transactions.py +269 -0
- aria_code/apps/cli/turn_planning.py +137 -0
- aria_code/apps/cli/update_check.py +257 -0
- aria_code/apps/cli/updater.py +373 -0
- aria_code/apps/cli/utils/__init__.py +0 -0
- aria_code/apps/cli/utils/market_detect.py +1663 -0
- aria_code/apps/cli/web_login.py +136 -0
- aria_code/apps/cli/workflow_runner.py +127 -0
- aria_code/apps/cli/workspace_route.py +121 -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 +5244 -0
- aria_code/aria_daemon.py +1372 -0
- aria_code/aria_feishu_bot.py +16 -0
- aria_code/aria_mcp_server.py +61 -0
- aria_code/aria_relay_client.py +4 -0
- aria_code/aria_telegram_bot.py +11 -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 +16 -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 +1985 -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 +190 -0
- aria_code/command_safety.py +19 -0
- aria_code/computer_use_server.py +276 -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/sec_statements.py +217 -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/doctor.py +860 -0
- aria_code/domain/__init__.py +0 -0
- aria_code/domain/_finance_core/__init__.py +5 -0
- aria_code/domain/_finance_core/backtest_engine.py +325 -0
- aria_code/domain/_finance_core/performance_stats.py +101 -0
- aria_code/domain/backtest_engine.py +75 -0
- aria_code/domain/backtest_report.py +488 -0
- aria_code/domain/finance_formulas.py +389 -0
- aria_code/domain/performance_stats.py +6 -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/behavior.py +113 -0
- aria_code/evals/gap.py +93 -0
- aria_code/evals/harness.py +949 -0
- aria_code/evals/review_probe.py +132 -0
- aria_code/evals/runner.py +425 -0
- aria_code/evals/scorecard.py +322 -0
- aria_code/evals/summary.py +125 -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 +459 -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 +744 -0
- aria_code/memory_manager.py +273 -0
- aria_code/model_capability.py +512 -0
- aria_code/notification_tools.py +4 -0
- aria_code/openai_image_client.py +4 -0
- aria_code/packages/__init__.py +26 -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 +58 -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 +232 -0
- aria_code/packages/aria_mcp/circuit.py +82 -0
- aria_code/packages/aria_mcp/config.py +162 -0
- aria_code/packages/aria_mcp/installer.py +95 -0
- aria_code/packages/aria_mcp/server.py +1705 -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 +393 -0
- aria_code/packages/aria_sdk/providers.py +70 -0
- aria_code/packages/aria_sdk/streaming.py +75 -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 +304 -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 +137 -0
- aria_code/packages/aria_services/research_quality.py +186 -0
- aria_code/packages/aria_services/settings.py +160 -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 +5 -0
- aria_code/packages/aria_tools/financial/_paths.py +92 -0
- aria_code/packages/aria_tools/registry.py +129 -0
- aria_code/packages/quant_engine/__init__.py +33 -0
- aria_code/packages/quant_engine/_namespace.py +24 -0
- aria_code/packages/quant_engine/backtest/__init__.py +10 -0
- aria_code/packages/quant_engine/backtest/core.py +53 -0
- aria_code/packages/quant_engine/backtest/engine.py +102 -0
- aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
- aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
- aria_code/packages/quant_engine/sports/__init__.py +76 -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/predictor.py +312 -0
- aria_code/packages/quant_engine/sports/tracker.py +665 -0
- aria_code/packages/quant_engine/stochastic/__init__.py +31 -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/healthcare/__init__.py +40 -0
- aria_code/packs/logistics/__init__.py +171 -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/google_video_provider.py +256 -0
- aria_code/providers/llm/__init__.py +19 -0
- aria_code/providers/llm/anthropic.py +184 -0
- aria_code/providers/llm/arthera_gateway.py +90 -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 +137 -0
- aria_code/providers/llm/openai_compat.py +350 -0
- aria_code/providers/llm/registry.py +457 -0
- aria_code/providers/local_image_provider.py +202 -0
- aria_code/providers/local_llm_provider.py +549 -0
- aria_code/providers/ollama_models.py +79 -0
- aria_code/providers/openai_params.py +32 -0
- aria_code/providers/tool_messages.py +100 -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/review_service.py +497 -0
- aria_code/runtime/__init__.py +176 -0
- aria_code/runtime/acceptance.py +632 -0
- aria_code/runtime/agent_loop.py +1851 -0
- aria_code/runtime/agent_profiles.py +75 -0
- aria_code/runtime/approval.py +71 -0
- aria_code/runtime/baseline.py +219 -0
- aria_code/runtime/budget.py +232 -0
- aria_code/runtime/checkpoints.py +481 -0
- aria_code/runtime/consult.py +65 -0
- aria_code/runtime/contract.py +282 -0
- aria_code/runtime/delivery.py +432 -0
- aria_code/runtime/events.py +113 -0
- aria_code/runtime/gateway.py +167 -0
- aria_code/runtime/lsp.py +484 -0
- aria_code/runtime/native_index.py +153 -0
- aria_code/runtime/orchestrator.py +55 -0
- aria_code/runtime/processes.py +343 -0
- aria_code/runtime/project_graph.py +614 -0
- aria_code/runtime/repo_map.py +712 -0
- aria_code/runtime/research_bundle.py +105 -0
- aria_code/runtime/review.py +269 -0
- aria_code/runtime/run_state.py +127 -0
- aria_code/runtime/run_store.py +522 -0
- aria_code/runtime/scoped_runs.py +113 -0
- aria_code/runtime/self_healing.py +422 -0
- aria_code/runtime/semantic_diff.py +171 -0
- aria_code/runtime/subagent.py +721 -0
- aria_code/runtime/symbol_edit.py +117 -0
- aria_code/runtime/task_brief.py +46 -0
- aria_code/runtime/task_context.py +145 -0
- aria_code/runtime/task_ledger.py +108 -0
- aria_code/runtime/task_orchestrator.py +90 -0
- aria_code/runtime/task_outcome.py +33 -0
- aria_code/runtime/task_verification.py +266 -0
- aria_code/runtime/task_worktree.py +448 -0
- aria_code/runtime/tool_executor.py +329 -0
- aria_code/runtime/tool_policy.py +107 -0
- aria_code/runtime/transactions.py +216 -0
- aria_code/runtime/windows_job.py +115 -0
- aria_code/runtime/workflows.py +222 -0
- aria_code/runtime/worktrees.py +215 -0
- aria_code/runway_video_client.py +4 -0
- aria_code/safety/__init__.py +27 -0
- aria_code/safety/client_isolation.py +178 -0
- aria_code/safety/permissions.py +339 -0
- aria_code/safety/risk.py +699 -0
- aria_code/safety/sandbox.py +148 -0
- aria_code/safety/service.py +102 -0
- aria_code/service_endpoints.py +14 -0
- aria_code/setup_wizard.py +684 -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 +509 -0
- aria_code/tools/data_analysis_tools.py +831 -0
- aria_code/tools/enterprise_finance_tools.py +202 -0
- aria_code/tools/extended_tools.py +240 -0
- aria_code/tools/file_analysis_tools.py +880 -0
- aria_code/tools/image_gen_tools.py +114 -0
- aria_code/tools/local_finance_tools.py +3488 -0
- aria_code/tools/logistics_carriers.py +11 -0
- aria_code/tools/logistics_inventory.py +11 -0
- aria_code/tools/logistics_io.py +11 -0
- aria_code/tools/logistics_tenancy.py +11 -0
- aria_code/tools/logistics_tools.py +11 -0
- aria_code/tools/macro_tools.py +412 -0
- aria_code/tools/notification_tools.py +249 -0
- aria_code/tools/project_tools.py +525 -0
- aria_code/tools/realty_data_tools.py +662 -0
- aria_code/tools/spreadsheet_tools.py +399 -0
- aria_code/tools/stripe_tools.py +115 -0
- aria_code/tools/video_tools.py +500 -0
- aria_code/training/__init__.py +1 -0
- aria_code/training/compare.py +117 -0
- aria_code/training/prelude_commands.py +111 -0
- aria_code/training/prelude_eval.py +390 -0
- aria_code/training/prelude_real.py +183 -0
- aria_code/training/prelude_train.py +248 -0
- aria_code/training/sft_dataset.py +230 -0
- aria_code/training/trajectory_dataset.py +263 -0
- aria_code/ui/__init__.py +105 -0
- aria_code/ui/banner.py +404 -0
- aria_code/ui/completer.py +571 -0
- aria_code/ui/console.py +285 -0
- aria_code/ui/input_box.py +505 -0
- aria_code/ui/mentions.py +206 -0
- aria_code/ui/picker.py +248 -0
- aria_code/ui/render/__init__.py +11 -0
- aria_code/ui/render/actions.py +297 -0
- aria_code/ui/render/finance.py +1732 -0
- aria_code/ui/render/market.py +253 -0
- aria_code/ui/render/output.py +954 -0
- aria_code/ui/render/responsive.py +75 -0
- aria_code/ui/render/team.py +367 -0
- aria_code/ui/robot.py +239 -0
- aria_code/ui/startup_dashboard.py +110 -0
- aria_code/video_analysis.py +147 -0
- aria_code/video_editor.py +518 -0
- aria_code/video_workflow/__init__.py +8 -0
- aria_code/video_workflow/__main__.py +61 -0
- aria_code/video_workflow/engines/__init__.py +18 -0
- aria_code/video_workflow/engines/base.py +69 -0
- aria_code/video_workflow/engines/custom_model.py +204 -0
- aria_code/video_workflow/engines/google_veo.py +94 -0
- aria_code/video_workflow/engines/local_engine.py +71 -0
- aria_code/video_workflow/engines/registry.py +98 -0
- aria_code/video_workflow/google_models.py +278 -0
- aria_code/video_workflow/music/__init__.py +5 -0
- aria_code/video_workflow/music/foley_service.py +132 -0
- aria_code/video_workflow/music/service.py +212 -0
- aria_code/video_workflow/pipeline.py +620 -0
- aria_code/video_workflow/plan.py +202 -0
- aria_code/video_workflow/session.py +297 -0
- aria_code/workspace/__init__.py +6 -0
- aria_code/workspace/files.py +219 -0
- aria_code/workspace/verify.py +167 -0
- aria_code/workspace_changes.py +199 -0
- aria_code/workspace_service.py +196 -0
- aria_code/workspace_tools.py +150 -0
- aria_code-0.167.0.dist-info/METADATA +330 -0
- aria_code-0.167.0.dist-info/RECORD +601 -0
- aria_code-0.167.0.dist-info/WHEEL +5 -0
- aria_code-0.167.0.dist-info/entry_points.txt +5 -0
- aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
- aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
- aria_code-0.167.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,949 @@
|
|
|
1
|
+
"""Run verifiable tasks and score them by exit code.
|
|
2
|
+
|
|
3
|
+
The shape of one task
|
|
4
|
+
---------------------
|
|
5
|
+
A fixture directory, a prompt, and a command::
|
|
6
|
+
|
|
7
|
+
- id: fix-failing-test
|
|
8
|
+
prompt: "tests/test_math.py 有一个测试失败了,修好它"
|
|
9
|
+
fixture: fix_failing_test
|
|
10
|
+
verify: "{python} -m pytest -q"
|
|
11
|
+
requires: [pytest]
|
|
12
|
+
|
|
13
|
+
The fixture is copied to a scratch directory, the agent is turned loose in the
|
|
14
|
+
copy, and ``verify`` decides the outcome. Exit 0 is a pass. Nothing else
|
|
15
|
+
counts — not a confident summary, not a diff that looks right.
|
|
16
|
+
|
|
17
|
+
The property that makes a suite trustworthy
|
|
18
|
+
-------------------------------------------
|
|
19
|
+
**Every task must start red.** Before the agent runs, the harness runs
|
|
20
|
+
``verify`` on the untouched fixture. If it already passes, the task is
|
|
21
|
+
reported ``INVALID`` and scored as neither pass nor fail.
|
|
22
|
+
|
|
23
|
+
This check is worth more than any individual task. A suite that silently
|
|
24
|
+
accumulates already-green tasks reports a rising pass rate while measuring
|
|
25
|
+
less and less, and the failure is invisible precisely because the number looks
|
|
26
|
+
good — you cannot tell a task the agent solved from a task that was never
|
|
27
|
+
broken. Running the check every time costs one command per task and makes the
|
|
28
|
+
number mean something.
|
|
29
|
+
|
|
30
|
+
"Red" has to mean the check ran
|
|
31
|
+
------------------------------
|
|
32
|
+
The pre-flight alone is not enough, and the first run of this harness proved
|
|
33
|
+
it: every task reported red, and every task was red because the ``python3`` on
|
|
34
|
+
PATH had no pytest. The suite looked healthy while measuring nothing at all —
|
|
35
|
+
the same silent-inflation failure the pre-flight exists to prevent, arriving
|
|
36
|
+
through the back door.
|
|
37
|
+
|
|
38
|
+
Two things close it. ``{python}`` in a command resolves to the interpreter
|
|
39
|
+
running the harness, so a suite verifies against the environment it was
|
|
40
|
+
launched with rather than whatever ``python3`` happens to mean on this
|
|
41
|
+
machine. And a task may declare ``requires: [pytest]``; a missing import is
|
|
42
|
+
reported as ``ERROR`` — excluded from the score — instead of being counted as
|
|
43
|
+
a red test the agent is expected to fix.
|
|
44
|
+
|
|
45
|
+
Isolation
|
|
46
|
+
---------
|
|
47
|
+
Tasks run in a copy under a scratch root, never in the fixture and never in
|
|
48
|
+
the repository. An agent that deletes the workspace, writes outside it, or
|
|
49
|
+
leaves it in a broken state affects nothing but its own run's directory, which
|
|
50
|
+
is what makes it safe to keep destructive tasks in the suite — those are the
|
|
51
|
+
ones worth having. The agent also gets its own HOME / ARIA_HOME / XDG tree
|
|
52
|
+
per attempt (runner.trial_home_env), so nothing one attempt remembers reaches
|
|
53
|
+
the next.
|
|
54
|
+
|
|
55
|
+
A pass has to be the work
|
|
56
|
+
-------------------------
|
|
57
|
+
Editing a protected test is a FAIL, and so is *adding* a file that changes
|
|
58
|
+
how the check runs (conftest.py, pytest.ini, a .pth, a module named like
|
|
59
|
+
pytest or the stdlib) unless the task lists it in ``allow_new``. An exit code
|
|
60
|
+
of 125/126/127 is only "the check could not run" when the harness failed to
|
|
61
|
+
start it or a program on the command line really is missing; the agent's code
|
|
62
|
+
exiting 127 from inside the check is a FAIL.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
from __future__ import annotations
|
|
66
|
+
|
|
67
|
+
import importlib.util
|
|
68
|
+
import json
|
|
69
|
+
import re
|
|
70
|
+
import os
|
|
71
|
+
import secrets
|
|
72
|
+
import shlex
|
|
73
|
+
import shutil
|
|
74
|
+
import subprocess
|
|
75
|
+
import sys
|
|
76
|
+
import tempfile
|
|
77
|
+
import time
|
|
78
|
+
from dataclasses import dataclass, field
|
|
79
|
+
from pathlib import Path
|
|
80
|
+
from typing import Any, Callable, Iterable, List, Optional, Sequence
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"SuiteResult",
|
|
84
|
+
"TaskResult",
|
|
85
|
+
"TaskSpec",
|
|
86
|
+
"Solver",
|
|
87
|
+
"load_suite",
|
|
88
|
+
"run_suite",
|
|
89
|
+
"run_task",
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
# Outcomes. Kept as four values rather than a boolean because "the agent
|
|
93
|
+
# failed" and "the task was broken" call for opposite responses, and collapsing
|
|
94
|
+
# them is how a rotten suite goes unnoticed.
|
|
95
|
+
PASS = "pass"
|
|
96
|
+
FAIL = "fail"
|
|
97
|
+
INVALID = "invalid" # the fixture was already green: the task measures nothing
|
|
98
|
+
ERROR = "error" # the harness or the solver blew up; not the agent's score
|
|
99
|
+
|
|
100
|
+
_MAX_LOG_CHARS = 4000
|
|
101
|
+
|
|
102
|
+
# Residue that must not travel from a fixture into a task workspace.
|
|
103
|
+
# Directory names that hold build/run residue rather than source.
|
|
104
|
+
_RUN_RESIDUE = frozenset({
|
|
105
|
+
".git", "__pycache__", ".pytest_cache", ".mypy_cache", ".ruff_cache",
|
|
106
|
+
".tox", "node_modules", ".DS_Store",
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
_IGNORED_FIXTURE_ENTRIES = shutil.ignore_patterns(
|
|
110
|
+
"__pycache__", "*.pyc", "*.pyo", ".pytest_cache", ".mypy_cache",
|
|
111
|
+
".ruff_cache", ".git", "node_modules", ".DS_Store",
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _trim(text: str, limit: int = _MAX_LOG_CHARS) -> str:
|
|
116
|
+
body = (text or "").strip()
|
|
117
|
+
if len(body) <= limit:
|
|
118
|
+
return body
|
|
119
|
+
half = limit // 2
|
|
120
|
+
return f"{body[:half]}\n… [truncated] …\n{body[-half:]}"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class TaskSpec:
|
|
125
|
+
"""One verifiable task."""
|
|
126
|
+
|
|
127
|
+
id: str
|
|
128
|
+
prompt: str
|
|
129
|
+
verify: str
|
|
130
|
+
fixture: str = ""
|
|
131
|
+
timeout: int = 300
|
|
132
|
+
solve_timeout: int = 900
|
|
133
|
+
tags: tuple[str, ...] = ()
|
|
134
|
+
setup: tuple[str, ...] = ()
|
|
135
|
+
# Importable modules the check needs. Missing ones make the task ERROR
|
|
136
|
+
# rather than FAIL: an absent pytest is not a bug for the agent to fix.
|
|
137
|
+
requires: tuple[str, ...] = ()
|
|
138
|
+
# Files the agent must not modify. Defaults to the tests, because the
|
|
139
|
+
# check IS the tests: an agent that edits them can turn any task green
|
|
140
|
+
# without doing the work, and the score would be indistinguishable from a
|
|
141
|
+
# real solve. Editing one is scored FAIL no matter what the check says.
|
|
142
|
+
protect: tuple[str, ...] = ("test_*.py", "*_test.py", "tests/**")
|
|
143
|
+
# A task may legitimately start green when it is a regression guard: the
|
|
144
|
+
# point is that the agent must not *break* it. Opting out is explicit so
|
|
145
|
+
# that it is a decision someone made, not a fixture that quietly rotted.
|
|
146
|
+
allow_green_start: bool = False
|
|
147
|
+
# Files the agent never sees: present for the pre-flight, removed (with
|
|
148
|
+
# the pre-flight's caches) for the agent's turn, put back from the fixture
|
|
149
|
+
# for the score. A grader that holds the expected answers — EXPECTED =
|
|
150
|
+
# {"F-201": 5000.00, ...} — or spells out the trap in its docstrings is
|
|
151
|
+
# an answer key when it sits in the workspace. gemini-3.5-flash passed 23
|
|
152
|
+
# of the first 24 tasks with every grader in view.
|
|
153
|
+
hidden: tuple[str, ...] = ()
|
|
154
|
+
# How the agent should go about it, judged on its tool calls and reported
|
|
155
|
+
# beside the outcome without changing it. See evals/behavior.py.
|
|
156
|
+
behavior: tuple[dict, ...] = ()
|
|
157
|
+
# New files the task expects the agent to create that would otherwise be
|
|
158
|
+
# refused as planted: a conftest.py, a pyproject.toml, a module that
|
|
159
|
+
# shares a stdlib name. See _PLANTED_FILES.
|
|
160
|
+
allow_new: tuple[str, ...] = ()
|
|
161
|
+
|
|
162
|
+
@classmethod
|
|
163
|
+
def from_dict(cls, data: dict) -> "TaskSpec":
|
|
164
|
+
missing = [key for key in ("id", "prompt", "verify") if not data.get(key)]
|
|
165
|
+
if missing:
|
|
166
|
+
raise ValueError(f"task is missing required field(s): {', '.join(missing)}")
|
|
167
|
+
return cls(
|
|
168
|
+
id=str(data["id"]),
|
|
169
|
+
prompt=str(data["prompt"]),
|
|
170
|
+
verify=str(data["verify"]),
|
|
171
|
+
fixture=str(data.get("fixture") or ""),
|
|
172
|
+
timeout=int(data.get("timeout") or 300),
|
|
173
|
+
solve_timeout=int(data.get("solve_timeout") or 900),
|
|
174
|
+
tags=tuple(str(t) for t in (data.get("tags") or ())),
|
|
175
|
+
setup=tuple(str(c) for c in (data.get("setup") or ())),
|
|
176
|
+
requires=tuple(str(m) for m in (data.get("requires") or ())),
|
|
177
|
+
protect=(
|
|
178
|
+
tuple(str(g) for g in data["protect"])
|
|
179
|
+
if "protect" in data else cls.protect
|
|
180
|
+
),
|
|
181
|
+
allow_green_start=bool(data.get("allow_green_start", False)),
|
|
182
|
+
hidden=tuple(str(g) for g in (data.get("hidden") or ())),
|
|
183
|
+
behavior=tuple(_validate_behavior(c) for c in (data.get("behavior") or ())),
|
|
184
|
+
allow_new=tuple(str(g) for g in (data.get("allow_new") or ())),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _validate_behavior(raw) -> dict:
|
|
189
|
+
from .behavior import validate
|
|
190
|
+
|
|
191
|
+
return validate(raw)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
_SECRET = re.compile(
|
|
195
|
+
r"(?i)\b(api[_-]?key|token|password|secret)(\s*[=:]\s*)\S+"
|
|
196
|
+
r"|\b(?:bearer)\s+\S+"
|
|
197
|
+
r"|\b(?:sk|pk|rk)-[A-Za-z0-9_-]{8,}|\bgh[pousr]_[A-Za-z0-9]{12,}|\bAIza[0-9A-Za-z_-]{20,}"
|
|
198
|
+
r"|\bya29\.[0-9A-Za-z_-]+"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _mask(line: str) -> str:
|
|
203
|
+
def repl(match: re.Match) -> str:
|
|
204
|
+
return f"{match.group(1)}{match.group(2)}***" if match.group(1) else "***"
|
|
205
|
+
return _SECRET.sub(repl, line)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _log_tail(log: str, lines: int = 23) -> list:
|
|
209
|
+
"""The last lines of a task's log, secret-shaped values masked."""
|
|
210
|
+
kept = [line.rstrip() for line in str(log or "").splitlines() if line.strip()]
|
|
211
|
+
return [_mask(line)[:300] for line in kept[-lines:]]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass(frozen=True)
|
|
215
|
+
class TaskResult:
|
|
216
|
+
task_id: str
|
|
217
|
+
outcome: str
|
|
218
|
+
seconds: float = 0.0
|
|
219
|
+
exit_code: Optional[int] = None
|
|
220
|
+
detail: str = ""
|
|
221
|
+
log: str = ""
|
|
222
|
+
tags: tuple[str, ...] = ()
|
|
223
|
+
# What the agent actually touched. A red check with an empty list is a
|
|
224
|
+
# different failure from a red check after real edits: the first says the
|
|
225
|
+
# agent never engaged, the second says it engaged and got it wrong. Without
|
|
226
|
+
# this the two are indistinguishable in a report, and they call for
|
|
227
|
+
# opposite investigations.
|
|
228
|
+
changed: tuple[str, ...] = ()
|
|
229
|
+
# Behaviour checks on the agent's tool calls: ({check, passed, detail}, …).
|
|
230
|
+
behavior: tuple = ()
|
|
231
|
+
# Tokens the agent reported for this task ({model, provider, input_tokens, …});
|
|
232
|
+
# empty when the solver reports none, which is not the same as zero.
|
|
233
|
+
usage: dict = field(default_factory=dict)
|
|
234
|
+
# The solver ran out of its time budget. Still ERROR (not the model's
|
|
235
|
+
# score), but reported on its own: a suite where a third of the attempts
|
|
236
|
+
# time out has a pass rate over the other two thirds only, and a reader
|
|
237
|
+
# has to be able to see that.
|
|
238
|
+
timed_out: bool = False
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def counted(self) -> bool:
|
|
242
|
+
"""Whether this result belongs in the pass rate at all."""
|
|
243
|
+
return self.outcome in (PASS, FAIL)
|
|
244
|
+
|
|
245
|
+
def to_dict(self) -> dict:
|
|
246
|
+
return {
|
|
247
|
+
"task_id": self.task_id,
|
|
248
|
+
"outcome": self.outcome,
|
|
249
|
+
"seconds": round(self.seconds, 2),
|
|
250
|
+
"exit_code": self.exit_code,
|
|
251
|
+
"detail": self.detail,
|
|
252
|
+
"tags": list(self.tags),
|
|
253
|
+
"changed": list(self.changed),
|
|
254
|
+
# Why it did not pass. The first CI run's report said only "the
|
|
255
|
+
# agent did not complete (exit 1)"; the reason was in the job log.
|
|
256
|
+
"log_tail": _log_tail(self.log) if self.outcome != PASS else [],
|
|
257
|
+
**({"behavior": [dict(b) for b in self.behavior]} if self.behavior else {}),
|
|
258
|
+
**({"usage": dict(self.usage)} if self.usage else {}),
|
|
259
|
+
**({"timed_out": True} if self.timed_out else {}),
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
_USAGE_TOKEN_KEYS = ("input_tokens", "output_tokens", "thinking_tokens")
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def usage_reported(usage: Optional[dict]) -> bool:
|
|
267
|
+
"""Whether a usage record actually reports tokens.
|
|
268
|
+
|
|
269
|
+
An all-zero record is what an agent that never reached a model writes (or
|
|
270
|
+
one whose provider returns no counts); counting it as "reported" made a
|
|
271
|
+
suite's token total look complete while it summed nothing.
|
|
272
|
+
"""
|
|
273
|
+
if not usage:
|
|
274
|
+
return False
|
|
275
|
+
for key in _USAGE_TOKEN_KEYS:
|
|
276
|
+
try:
|
|
277
|
+
if int(usage.get(key) or 0) > 0:
|
|
278
|
+
return True
|
|
279
|
+
except (TypeError, ValueError):
|
|
280
|
+
continue
|
|
281
|
+
return False
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@dataclass
|
|
285
|
+
class SuiteResult:
|
|
286
|
+
name: str
|
|
287
|
+
results: List[TaskResult] = field(default_factory=list)
|
|
288
|
+
|
|
289
|
+
@property
|
|
290
|
+
def passed(self) -> int:
|
|
291
|
+
return sum(1 for r in self.results if r.outcome == PASS)
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def failed(self) -> int:
|
|
295
|
+
return sum(1 for r in self.results if r.outcome == FAIL)
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def invalid(self) -> int:
|
|
299
|
+
return sum(1 for r in self.results if r.outcome == INVALID)
|
|
300
|
+
|
|
301
|
+
@property
|
|
302
|
+
def errored(self) -> int:
|
|
303
|
+
return sum(1 for r in self.results if r.outcome == ERROR)
|
|
304
|
+
|
|
305
|
+
@property
|
|
306
|
+
def timed_out(self) -> int:
|
|
307
|
+
"""Errored attempts the solver's time budget cut off (a subset of errored)."""
|
|
308
|
+
return sum(1 for r in self.results if r.outcome == ERROR and r.timed_out)
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def scored(self) -> int:
|
|
312
|
+
return sum(1 for r in self.results if r.counted)
|
|
313
|
+
|
|
314
|
+
@property
|
|
315
|
+
def pass_rate(self) -> float:
|
|
316
|
+
"""Passes over *scored* tasks.
|
|
317
|
+
|
|
318
|
+
Invalid and errored tasks are excluded rather than counted as
|
|
319
|
+
failures. Counting them down would let one broken fixture look like an
|
|
320
|
+
agent regression; counting them up would let it hide one.
|
|
321
|
+
"""
|
|
322
|
+
return (self.passed / self.scored) if self.scored else 0.0
|
|
323
|
+
|
|
324
|
+
def usage_totals(self) -> dict:
|
|
325
|
+
"""Tokens and seconds summed over tasks whose agent reported usage.
|
|
326
|
+
|
|
327
|
+
``reported`` says how many did: a total over 12 of 30 tasks is not the
|
|
328
|
+
cost of the suite, and must not be read as one.
|
|
329
|
+
"""
|
|
330
|
+
with_usage = [r for r in self.results if usage_reported(r.usage)]
|
|
331
|
+
keys = _USAGE_TOKEN_KEYS
|
|
332
|
+
return {
|
|
333
|
+
"reported": len(with_usage),
|
|
334
|
+
"tasks": len(self.results),
|
|
335
|
+
**{key: sum(int(r.usage.get(key, 0) or 0) for r in with_usage) for key in keys},
|
|
336
|
+
"seconds": round(sum(r.seconds for r in self.results), 2),
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
def by_tag(self) -> dict:
|
|
340
|
+
"""Pass rate per tag — the per-industry scoreboard."""
|
|
341
|
+
buckets: dict[str, list[TaskResult]] = {}
|
|
342
|
+
for result in self.results:
|
|
343
|
+
for tag in result.tags:
|
|
344
|
+
buckets.setdefault(tag, []).append(result)
|
|
345
|
+
out = {}
|
|
346
|
+
for tag, results in sorted(buckets.items()):
|
|
347
|
+
scored = [r for r in results if r.counted]
|
|
348
|
+
out[tag] = {
|
|
349
|
+
"passed": sum(1 for r in scored if r.outcome == PASS),
|
|
350
|
+
"scored": len(scored),
|
|
351
|
+
"pass_rate": round(
|
|
352
|
+
sum(1 for r in scored if r.outcome == PASS) / len(scored), 4
|
|
353
|
+
) if scored else 0.0,
|
|
354
|
+
}
|
|
355
|
+
return out
|
|
356
|
+
|
|
357
|
+
def to_dict(self) -> dict:
|
|
358
|
+
return {
|
|
359
|
+
"suite": self.name,
|
|
360
|
+
"pass_rate": round(self.pass_rate, 4),
|
|
361
|
+
"passed": self.passed,
|
|
362
|
+
"failed": self.failed,
|
|
363
|
+
"invalid": self.invalid,
|
|
364
|
+
"errored": self.errored,
|
|
365
|
+
"timed_out": self.timed_out,
|
|
366
|
+
"scored": self.scored,
|
|
367
|
+
"by_tag": self.by_tag(),
|
|
368
|
+
"usage": self.usage_totals(),
|
|
369
|
+
"per_task": self.per_task(),
|
|
370
|
+
**({"behavior": self.behavior()} if any(r.behavior for r in self.results) else {}),
|
|
371
|
+
"results": [r.to_dict() for r in self.results],
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
def behavior(self) -> dict:
|
|
375
|
+
"""Passes over runs for each behaviour check, keyed ``task: check``."""
|
|
376
|
+
out: dict = {}
|
|
377
|
+
for result in self.results:
|
|
378
|
+
for item in result.behavior:
|
|
379
|
+
stats = out.setdefault(f"{result.task_id}: {item['check']}", {"passed": 0, "runs": 0})
|
|
380
|
+
stats["runs"] += 1
|
|
381
|
+
stats["passed"] += bool(item["passed"])
|
|
382
|
+
return out
|
|
383
|
+
|
|
384
|
+
def merge(self, other: "SuiteResult") -> None:
|
|
385
|
+
"""Fold another run of the same suite into this one."""
|
|
386
|
+
self.results.extend(other.results)
|
|
387
|
+
|
|
388
|
+
def per_task(self) -> dict:
|
|
389
|
+
"""Passes over attempts for each task id, across every repeat.
|
|
390
|
+
|
|
391
|
+
A single sample is not a score. Observed on the first real run: one
|
|
392
|
+
task passed alone and failed in the same suite minutes later, from the
|
|
393
|
+
same model. Reporting 2/5 without saying how many attempts produced it
|
|
394
|
+
invites reading noise as a regression.
|
|
395
|
+
"""
|
|
396
|
+
buckets: dict[str, list[TaskResult]] = {}
|
|
397
|
+
for result in self.results:
|
|
398
|
+
buckets.setdefault(result.task_id, []).append(result)
|
|
399
|
+
out = {}
|
|
400
|
+
for task_id, results in buckets.items():
|
|
401
|
+
scored = [r for r in results if r.counted]
|
|
402
|
+
out[task_id] = {
|
|
403
|
+
"passed": sum(1 for r in scored if r.outcome == PASS),
|
|
404
|
+
"attempts": len(scored),
|
|
405
|
+
"outcomes": [r.outcome for r in results],
|
|
406
|
+
}
|
|
407
|
+
return out
|
|
408
|
+
|
|
409
|
+
def summary_line(self) -> str:
|
|
410
|
+
parts = [f"{self.name}: {self.passed}/{self.scored} ({self.pass_rate:.0%})"]
|
|
411
|
+
if self.invalid:
|
|
412
|
+
parts.append(f"{self.invalid} invalid")
|
|
413
|
+
if self.errored:
|
|
414
|
+
parts.append(f"{self.errored} error")
|
|
415
|
+
if self.timed_out:
|
|
416
|
+
parts.append(f"{self.timed_out} timed out (of the errors, unscored)")
|
|
417
|
+
checks = [item for r in self.results for item in r.behavior]
|
|
418
|
+
if checks:
|
|
419
|
+
parts.append(f"behaviour {sum(bool(i['passed']) for i in checks)}/{len(checks)}")
|
|
420
|
+
return " · ".join(parts)
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
# ``(prompt, workspace) -> anything``. Injected so the harness can be tested,
|
|
424
|
+
# and so the same suite can score a different agent — the CLI, a subagent
|
|
425
|
+
# backend, or a competitor — without the suite knowing which.
|
|
426
|
+
Solver = Callable[[str, Path], Any]
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def _resolve(command: str) -> str:
|
|
430
|
+
"""Substitute ``{python}`` with the interpreter running the harness.
|
|
431
|
+
|
|
432
|
+
Without this a suite silently verifies against whatever ``python3`` means
|
|
433
|
+
on PATH, which is how five tasks once reported red because that
|
|
434
|
+
interpreter had no pytest installed.
|
|
435
|
+
|
|
436
|
+
The path is quoted, because the command runs through a shell. Unquoted, an
|
|
437
|
+
interpreter under "Application Support" — where the macOS app keeps its
|
|
438
|
+
projects — split at the space, every check exited 126, and the pre-flight
|
|
439
|
+
reported all four tasks red: measuring nothing while saying it was fine.
|
|
440
|
+
"""
|
|
441
|
+
if os.name == "nt":
|
|
442
|
+
interpreter = subprocess.list2cmdline([sys.executable])
|
|
443
|
+
else:
|
|
444
|
+
interpreter = shlex.quote(sys.executable)
|
|
445
|
+
return (command or "").replace("{python}", interpreter)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
# Exit codes with which the shell says it never ran the check at all: 125 is
|
|
449
|
+
# _run's own "could not start", 126 not executable, 127 not found, 9009 the
|
|
450
|
+
# Windows not-found. A red result for any of these is not a broken fixture and
|
|
451
|
+
# not a failed agent — counting it as either lets an environment problem pose
|
|
452
|
+
# as a measurement.
|
|
453
|
+
_COULD_NOT_RUN = {125: "could not start", 126: "not executable", 127: "command not found",
|
|
454
|
+
9009: "command not found"}
|
|
455
|
+
|
|
456
|
+
# The code alone is not evidence: the check runs the agent's code, and that
|
|
457
|
+
# code can `os._exit(127)` to turn a FAIL into an unscored ERROR. Output is
|
|
458
|
+
# not evidence either, since the same code can print "command not found". So:
|
|
459
|
+
#
|
|
460
|
+
# * 125 counts only with _run's own start-failure marker, which carries a
|
|
461
|
+
# per-process nonce the check cannot know;
|
|
462
|
+
# * 126/127/9009 count only when a program the command line names really
|
|
463
|
+
# cannot be found or executed from the task's workspace. When every program
|
|
464
|
+
# resolves, the code came from inside the check, and a red check is a FAIL.
|
|
465
|
+
_START_FAILURE = f"could not run command [{secrets.token_hex(8)}]: "
|
|
466
|
+
_SHELL_BUILTINS = frozenset({
|
|
467
|
+
"cd", "export", "set", "unset", "true", "false", ":", "test", "[", "echo",
|
|
468
|
+
"exit", "source", ".", "eval", "exec", "env", "command", "builtin", "!",
|
|
469
|
+
})
|
|
470
|
+
_SEPARATORS = frozenset({"&&", "||", ";", "|", "&"})
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def _missing_program(command: str, cwd: Optional[Path]) -> bool:
|
|
474
|
+
"""True when some program the command line runs cannot be found or executed."""
|
|
475
|
+
try:
|
|
476
|
+
tokens = shlex.split(_resolve(command), posix=os.name != "nt")
|
|
477
|
+
except ValueError:
|
|
478
|
+
return True # cannot parse it: do not claim it ran
|
|
479
|
+
starts = [0] + [i + 1 for i, tok in enumerate(tokens) if tok in _SEPARATORS]
|
|
480
|
+
for i in starts:
|
|
481
|
+
while i < len(tokens) and re.match(r"^[A-Za-z_][A-Za-z0-9_]*=", tokens[i]):
|
|
482
|
+
i += 1 # FOO=bar cmd
|
|
483
|
+
if i >= len(tokens):
|
|
484
|
+
continue
|
|
485
|
+
program = tokens[i].strip("\"'")
|
|
486
|
+
if not program or program in _SHELL_BUILTINS:
|
|
487
|
+
continue
|
|
488
|
+
if "/" in program or (os.name == "nt" and "\\" in program):
|
|
489
|
+
path = Path(program)
|
|
490
|
+
if not path.is_absolute() and cwd is not None:
|
|
491
|
+
path = Path(cwd) / path
|
|
492
|
+
if not (path.is_file() and os.access(path, os.X_OK)):
|
|
493
|
+
return True
|
|
494
|
+
elif shutil.which(program) is None:
|
|
495
|
+
return True
|
|
496
|
+
return False
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
def _could_not_run(code: int, log: str, command: str = "", cwd: Optional[Path] = None) -> str:
|
|
500
|
+
reason = _COULD_NOT_RUN.get(code)
|
|
501
|
+
if reason is None:
|
|
502
|
+
return ""
|
|
503
|
+
text = log or ""
|
|
504
|
+
if code == 125:
|
|
505
|
+
if _START_FAILURE not in text:
|
|
506
|
+
return ""
|
|
507
|
+
elif not _missing_program(command, cwd):
|
|
508
|
+
return ""
|
|
509
|
+
last = text.strip().splitlines()[-1:] or [""]
|
|
510
|
+
return f"the check itself could not run (exit {code}, {reason}): {last[0]}".rstrip(": ")
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _missing_modules(names: Iterable[str]) -> list[str]:
|
|
514
|
+
missing = []
|
|
515
|
+
for name in names:
|
|
516
|
+
try:
|
|
517
|
+
if importlib.util.find_spec(name) is None:
|
|
518
|
+
missing.append(name)
|
|
519
|
+
except (ImportError, ValueError):
|
|
520
|
+
missing.append(name)
|
|
521
|
+
return missing
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _matches(rel: str, globs: Iterable[str]) -> bool:
|
|
525
|
+
import fnmatch
|
|
526
|
+
|
|
527
|
+
name = rel.replace("\\", "/")
|
|
528
|
+
base = name.rsplit("/", 1)[-1]
|
|
529
|
+
return any(fnmatch.fnmatch(name, g) or fnmatch.fnmatch(base, g) for g in globs)
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
def _violates_protection(changed: Iterable[str], patterns: Iterable[str]) -> tuple[str, ...]:
|
|
533
|
+
"""Protected files the agent modified."""
|
|
534
|
+
globs = tuple(patterns)
|
|
535
|
+
return tuple(rel for rel in changed if _matches(rel, globs))
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
# Files that change how the check itself runs rather than what it checks. A
|
|
539
|
+
# conftest.py can rewrite every test outcome, a pytest.ini/pyproject.toml/
|
|
540
|
+
# setup.cfg/tox.ini can deselect the failing tests or add plugins, a .pth or
|
|
541
|
+
# sitecustomize.py runs at interpreter start-up, and a pytest.py can stand in
|
|
542
|
+
# for pytest. Protection covers only files that were there before the agent's
|
|
543
|
+
# turn, so an agent could pass any task by *adding* one of these. They are
|
|
544
|
+
# refused unless the task lists them in ``allow_new``.
|
|
545
|
+
_PLANTED_FILES = (
|
|
546
|
+
"conftest.py", "pytest.ini", "pyproject.toml", "setup.cfg", "tox.ini",
|
|
547
|
+
"sitecustomize.py", "usercustomize.py", "*.pth",
|
|
548
|
+
)
|
|
549
|
+
# Modules the check imports. A new file or package with one of these names
|
|
550
|
+
# shadows the real one; so does any stdlib name.
|
|
551
|
+
_CHECK_MODULES = frozenset({"pytest", "_pytest", "pluggy", "py", "iniconfig", "unittest"})
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _shadowed_module(rel: str) -> str:
|
|
555
|
+
"""The importable name *rel* would shadow, or "" when it shadows nothing."""
|
|
556
|
+
parts = rel.replace("\\", "/").split("/")
|
|
557
|
+
name = parts[-1]
|
|
558
|
+
if name in ("__init__.py", "__init__.pyc") and len(parts) > 1:
|
|
559
|
+
module = parts[-2]
|
|
560
|
+
elif name.endswith((".py", ".pyc")) and "." not in name.rsplit(".", 1)[0]:
|
|
561
|
+
module = name.rsplit(".", 1)[0]
|
|
562
|
+
else:
|
|
563
|
+
return ""
|
|
564
|
+
stdlib = getattr(sys, "stdlib_module_names", frozenset())
|
|
565
|
+
return module if module in _CHECK_MODULES or module in stdlib else ""
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def _planted(added: Iterable[str], allowed: Iterable[str]) -> tuple[str, ...]:
|
|
569
|
+
"""New files that could change how the check runs, minus what the task allows."""
|
|
570
|
+
allow = tuple(allowed)
|
|
571
|
+
out = []
|
|
572
|
+
for rel in added:
|
|
573
|
+
if allow and _matches(rel, allow):
|
|
574
|
+
continue
|
|
575
|
+
if _matches(rel, _PLANTED_FILES) or _shadowed_module(rel):
|
|
576
|
+
out.append(rel)
|
|
577
|
+
return tuple(out)
|
|
578
|
+
|
|
579
|
+
|
|
580
|
+
def _hide(workspace: Path, globs: Sequence[str]) -> tuple[str, ...]:
|
|
581
|
+
"""Remove the hidden files, and every cache the pre-flight left, from *workspace*.
|
|
582
|
+
|
|
583
|
+
The caches matter as much as the files: .pytest_cache records the failing
|
|
584
|
+
tests by name and __pycache__ holds the grader compiled, docstrings and
|
|
585
|
+
expected values included.
|
|
586
|
+
"""
|
|
587
|
+
if not globs:
|
|
588
|
+
return ()
|
|
589
|
+
hidden = tuple(
|
|
590
|
+
str(path.relative_to(workspace)) for path in sorted(workspace.rglob("*"))
|
|
591
|
+
if path.is_file() and _matches(str(path.relative_to(workspace)), globs)
|
|
592
|
+
)
|
|
593
|
+
for rel in hidden:
|
|
594
|
+
(workspace / rel).unlink()
|
|
595
|
+
for cache in sorted(workspace.rglob("*"), reverse=True):
|
|
596
|
+
if cache.is_dir() and cache.name in ("__pycache__", ".pytest_cache"):
|
|
597
|
+
shutil.rmtree(cache, ignore_errors=True)
|
|
598
|
+
return hidden
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
def _restore(hidden: Sequence[str], source: Path, workspace: Path) -> None:
|
|
602
|
+
"""Put the hidden files back from the fixture, over anything the agent wrote there."""
|
|
603
|
+
for rel in hidden:
|
|
604
|
+
target = workspace / rel
|
|
605
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
606
|
+
shutil.copy2(source / rel, target)
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _snapshot(root: Path) -> dict[str, str]:
|
|
610
|
+
"""Content digests for every file in *root*, keyed by relative path."""
|
|
611
|
+
import hashlib
|
|
612
|
+
|
|
613
|
+
out: dict[str, str] = {}
|
|
614
|
+
for path in sorted(root.rglob("*")):
|
|
615
|
+
if not path.is_file():
|
|
616
|
+
continue
|
|
617
|
+
rel = str(path.relative_to(root))
|
|
618
|
+
# Residue the *check* writes, not the agent's work. Reporting it makes
|
|
619
|
+
# every task look like it touched a dozen files and buries the one edit
|
|
620
|
+
# that matters.
|
|
621
|
+
if any(part in _RUN_RESIDUE for part in rel.replace("\\", "/").split("/")):
|
|
622
|
+
continue
|
|
623
|
+
try:
|
|
624
|
+
out[rel] = hashlib.sha256(path.read_bytes()).hexdigest()
|
|
625
|
+
except OSError:
|
|
626
|
+
continue
|
|
627
|
+
return out
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
def _changed_paths(before: dict[str, str], after: dict[str, str]) -> tuple[str, ...]:
|
|
631
|
+
"""Files the agent created, edited, or deleted."""
|
|
632
|
+
touched = [
|
|
633
|
+
rel for rel in sorted(set(before) | set(after))
|
|
634
|
+
if before.get(rel) != after.get(rel)
|
|
635
|
+
]
|
|
636
|
+
return tuple(touched)
|
|
637
|
+
|
|
638
|
+
|
|
639
|
+
# How an agent's turn can end when the agent itself was running: these are about the
|
|
640
|
+
# work, so the task's check decides. Anything else in `stop_reason` is the transport
|
|
641
|
+
# or the provider failing (an HTTP error text, a crash), which is not the model's score.
|
|
642
|
+
_AGENT_STOPS = frozenset({"completed", "checks_failed", "max_rounds", "budget_exhausted", "loop_guard"})
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _agent_finished(stdout: str) -> bool:
|
|
646
|
+
"""True when the agent's event stream ends with a turn.completed that ran to an agent-level stop."""
|
|
647
|
+
last = None
|
|
648
|
+
for line in stdout.splitlines():
|
|
649
|
+
if line.startswith("{") and '"turn.completed"' in line:
|
|
650
|
+
try:
|
|
651
|
+
last = json.loads(line)
|
|
652
|
+
except ValueError:
|
|
653
|
+
continue
|
|
654
|
+
return bool(last) and str(last.get("stop_reason") or "") in _AGENT_STOPS
|
|
655
|
+
|
|
656
|
+
|
|
657
|
+
def _solver_failure(outcome: Any) -> Optional[tuple[int, str]]:
|
|
658
|
+
"""``(exit_code, log)`` when the solver reports it did not complete.
|
|
659
|
+
|
|
660
|
+
Duck-typed on ``returncode`` so a plain callable that returns nothing —
|
|
661
|
+
every in-process solver, and the check-only one — is unaffected, while a
|
|
662
|
+
``subprocess.CompletedProcess`` from the real agent is inspected.
|
|
663
|
+
"""
|
|
664
|
+
if outcome is None:
|
|
665
|
+
return None
|
|
666
|
+
code = getattr(outcome, "returncode", None)
|
|
667
|
+
if not isinstance(code, int) or code == 0:
|
|
668
|
+
return None
|
|
669
|
+
if _agent_finished(str(getattr(outcome, "stdout", "") or "")):
|
|
670
|
+
# The agent ran its turn and said how it ended; the non-zero exit is its verdict on
|
|
671
|
+
# its own work (its acceptance checks were red), not a crash. The check scores it.
|
|
672
|
+
return None
|
|
673
|
+
log = "\n".join(
|
|
674
|
+
part for part in (
|
|
675
|
+
str(getattr(outcome, "stderr", "") or ""),
|
|
676
|
+
str(getattr(outcome, "stdout", "") or ""),
|
|
677
|
+
) if part.strip()
|
|
678
|
+
)
|
|
679
|
+
return code, log
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
def _run(command: str, cwd: Path, timeout: int) -> tuple[int, str]:
|
|
683
|
+
command = _resolve(command)
|
|
684
|
+
try:
|
|
685
|
+
proc = subprocess.run(
|
|
686
|
+
command,
|
|
687
|
+
shell=True,
|
|
688
|
+
cwd=str(cwd),
|
|
689
|
+
capture_output=True,
|
|
690
|
+
text=True,
|
|
691
|
+
timeout=timeout,
|
|
692
|
+
check=False,
|
|
693
|
+
)
|
|
694
|
+
except subprocess.TimeoutExpired:
|
|
695
|
+
# A check that never finishes is a failed check. Reporting it as
|
|
696
|
+
# anything else would let an infinite loop score as a pass.
|
|
697
|
+
return 124, f"verification timed out after {timeout}s"
|
|
698
|
+
except Exception as exc:
|
|
699
|
+
return 125, f"{_START_FAILURE}{exc}"
|
|
700
|
+
output = "\n".join(part for part in (proc.stdout, proc.stderr) if part.strip())
|
|
701
|
+
return proc.returncode, output
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
def load_suite(path: str | Path) -> tuple[str, list[TaskSpec]]:
|
|
705
|
+
"""Read a YAML suite file into task specs."""
|
|
706
|
+
import yaml
|
|
707
|
+
|
|
708
|
+
file_path = Path(path).expanduser().resolve()
|
|
709
|
+
data = yaml.safe_load(file_path.read_text(encoding="utf-8")) or {}
|
|
710
|
+
name = str(data.get("suite") or file_path.stem)
|
|
711
|
+
tasks = [TaskSpec.from_dict(entry) for entry in (data.get("tasks") or [])]
|
|
712
|
+
|
|
713
|
+
duplicates = {t.id for t in tasks if [x.id for x in tasks].count(t.id) > 1}
|
|
714
|
+
if duplicates:
|
|
715
|
+
raise ValueError(f"duplicate task id(s) in {file_path.name}: {', '.join(sorted(duplicates))}")
|
|
716
|
+
return name, tasks
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _prepare_workspace(task: TaskSpec, fixtures_root: Path, scratch: Path) -> Path:
|
|
720
|
+
workspace = scratch / task.id
|
|
721
|
+
if workspace.exists():
|
|
722
|
+
shutil.rmtree(workspace)
|
|
723
|
+
if task.fixture:
|
|
724
|
+
source = fixtures_root / task.fixture
|
|
725
|
+
if not source.is_dir():
|
|
726
|
+
raise FileNotFoundError(f"fixture not found: {source}")
|
|
727
|
+
# Never copy build residue. A __pycache__ carried over from a run of
|
|
728
|
+
# the fixture in place holds bytecode for the *broken* source, and
|
|
729
|
+
# Python will happily import it — so a task could score red after a
|
|
730
|
+
# correct fix, or green before one.
|
|
731
|
+
shutil.copytree(source, workspace, ignore=_IGNORED_FIXTURE_ENTRIES)
|
|
732
|
+
else:
|
|
733
|
+
workspace.mkdir(parents=True)
|
|
734
|
+
return workspace
|
|
735
|
+
|
|
736
|
+
|
|
737
|
+
def run_task(
|
|
738
|
+
task: TaskSpec,
|
|
739
|
+
*,
|
|
740
|
+
solver: Solver,
|
|
741
|
+
fixtures_root: str | Path,
|
|
742
|
+
scratch_root: str | Path | None = None,
|
|
743
|
+
keep_workspace: bool = False,
|
|
744
|
+
) -> TaskResult:
|
|
745
|
+
"""Prepare, pre-check, solve, and score one task."""
|
|
746
|
+
started = time.time()
|
|
747
|
+
fixtures = Path(fixtures_root).expanduser().resolve()
|
|
748
|
+
scratch = Path(scratch_root).expanduser().resolve() if scratch_root else Path(
|
|
749
|
+
tempfile.mkdtemp(prefix="aria-eval-")
|
|
750
|
+
)
|
|
751
|
+
scratch.mkdir(parents=True, exist_ok=True)
|
|
752
|
+
|
|
753
|
+
behaviour: list = [] # filled once the agent has had its turn
|
|
754
|
+
reported: dict = {} # the agent's own usage report, once it has had its turn
|
|
755
|
+
|
|
756
|
+
def _result(outcome: str, **kwargs) -> TaskResult:
|
|
757
|
+
return TaskResult(
|
|
758
|
+
task_id=task.id, outcome=outcome,
|
|
759
|
+
seconds=time.time() - started, tags=task.tags, behavior=tuple(behaviour),
|
|
760
|
+
usage=dict(reported), **kwargs,
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
# Everything below runs inside this try so the scratch directory is removed
|
|
764
|
+
# on EVERY path, not just the happy one.
|
|
765
|
+
#
|
|
766
|
+
# It used to open after the setup checks, and the two error returns above
|
|
767
|
+
# it — a missing fixture, a missing dependency — leaked one temp directory
|
|
768
|
+
# each. That is once per error path per run, and the test suite exercises
|
|
769
|
+
# both deliberately, so a full test run leaked two directories every time.
|
|
770
|
+
# 57 of them had accumulated in $TMPDIR before anyone noticed, which is the
|
|
771
|
+
# shape of this class of bug: too small to be felt, never self-correcting.
|
|
772
|
+
try:
|
|
773
|
+
try:
|
|
774
|
+
workspace = _prepare_workspace(task, fixtures, scratch)
|
|
775
|
+
except Exception as exc:
|
|
776
|
+
return _result(ERROR, detail=f"workspace setup failed: {exc}")
|
|
777
|
+
|
|
778
|
+
missing = _missing_modules(task.requires)
|
|
779
|
+
if missing:
|
|
780
|
+
return _result(
|
|
781
|
+
ERROR,
|
|
782
|
+
detail=f"environment is missing {', '.join(missing)} — the check cannot run",
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
for command in task.setup:
|
|
786
|
+
code, output = _run(command, workspace, task.timeout)
|
|
787
|
+
if code != 0:
|
|
788
|
+
return _result(ERROR, exit_code=code,
|
|
789
|
+
detail=f"setup command failed: {command}",
|
|
790
|
+
log=_trim(output))
|
|
791
|
+
|
|
792
|
+
# ── the pre-flight ────────────────────────────────────────────────
|
|
793
|
+
# Confirm the task is actually broken before asking anyone to fix it.
|
|
794
|
+
before_code, before_log = _run(task.verify, workspace, task.timeout)
|
|
795
|
+
broken = _could_not_run(before_code, before_log, task.verify, workspace)
|
|
796
|
+
if broken:
|
|
797
|
+
return _result(ERROR, exit_code=before_code, detail=broken, log=_trim(before_log))
|
|
798
|
+
if before_code == 0 and not task.allow_green_start:
|
|
799
|
+
return _result(
|
|
800
|
+
INVALID, exit_code=0,
|
|
801
|
+
detail="fixture already passes its own check — this task measures nothing",
|
|
802
|
+
log=_trim(before_log),
|
|
803
|
+
)
|
|
804
|
+
|
|
805
|
+
# ── the agent's turn ──────────────────────────────────────────────
|
|
806
|
+
hidden = _hide(workspace, task.hidden)
|
|
807
|
+
before = _snapshot(workspace)
|
|
808
|
+
try:
|
|
809
|
+
outcome = solver(task.prompt, workspace)
|
|
810
|
+
except Exception as exc:
|
|
811
|
+
# A solver crash is not the agent failing the task; scoring it as
|
|
812
|
+
# a fail would blame the model for a harness or provider outage.
|
|
813
|
+
return _result(ERROR, detail=f"solver raised: {exc}")
|
|
814
|
+
if isinstance(getattr(outcome, "usage", None), dict):
|
|
815
|
+
reported.update(outcome.usage)
|
|
816
|
+
# Only when the solver ran an agent: the pre-flight solver returns
|
|
817
|
+
# nothing, and "write_file was never called" would read as a pass.
|
|
818
|
+
if task.behavior and outcome is not None:
|
|
819
|
+
from .behavior import evaluate
|
|
820
|
+
|
|
821
|
+
behaviour.extend(evaluate(task.behavior, str(getattr(outcome, "stdout", "") or "")))
|
|
822
|
+
|
|
823
|
+
# ── the score ─────────────────────────────────────────────────────
|
|
824
|
+
# The check runs no matter what the solver reported, because the check
|
|
825
|
+
# is the source of truth and the solver's exit code is not. A run that
|
|
826
|
+
# edited the file correctly and then exited 1 on an empty final message
|
|
827
|
+
# is a PASS: the work is on disk and the tests are green.
|
|
828
|
+
changed = _changed_paths(before, _snapshot(workspace))
|
|
829
|
+
if hidden:
|
|
830
|
+
_restore(hidden, fixtures / task.fixture, workspace)
|
|
831
|
+
|
|
832
|
+
# Checked before the verdict, and it overrides a green check. Observed
|
|
833
|
+
# in a real run: an agent edited test_settlement.py rather than the
|
|
834
|
+
# module under test. The suite would have called that a PASS and the
|
|
835
|
+
# number would have been a lie.
|
|
836
|
+
#
|
|
837
|
+
# Only files that were there for the agent to tamper with: a test file
|
|
838
|
+
# it writes for itself is ordinary work, and with hidden graders the
|
|
839
|
+
# tests it is told not to edit are not there at all.
|
|
840
|
+
tampered = _violates_protection([rel for rel in changed if rel in before], task.protect)
|
|
841
|
+
if tampered:
|
|
842
|
+
return _result(
|
|
843
|
+
FAIL,
|
|
844
|
+
detail=f"modified protected file(s): {', '.join(tampered)}",
|
|
845
|
+
changed=changed,
|
|
846
|
+
)
|
|
847
|
+
# ...but a file it *adds* can still change how the check runs.
|
|
848
|
+
added = [rel for rel in changed if rel not in before and rel not in hidden
|
|
849
|
+
and (workspace / rel).is_file()]
|
|
850
|
+
planted = _planted(added, task.allow_new)
|
|
851
|
+
if planted:
|
|
852
|
+
return _result(
|
|
853
|
+
FAIL,
|
|
854
|
+
detail=(f"added file(s) that change how the check runs: {', '.join(planted)} "
|
|
855
|
+
"(declare expected ones in the task's allow_new)"),
|
|
856
|
+
changed=changed,
|
|
857
|
+
)
|
|
858
|
+
|
|
859
|
+
after_code, after_log = _run(task.verify, workspace, task.timeout)
|
|
860
|
+
if after_code == 0:
|
|
861
|
+
return _result(PASS, exit_code=0, changed=changed)
|
|
862
|
+
broken = _could_not_run(after_code, after_log, task.verify, workspace)
|
|
863
|
+
if broken:
|
|
864
|
+
return _result(ERROR, exit_code=after_code, detail=broken,
|
|
865
|
+
log=_trim(after_log), changed=changed)
|
|
866
|
+
|
|
867
|
+
# Red — now the solver's status decides who is to blame. A subprocess
|
|
868
|
+
# that died in seconds on a provider outage or a crash never gave the
|
|
869
|
+
# agent its turn, and scoring that as FAIL makes an infrastructure
|
|
870
|
+
# problem look like an incapable model. That is exactly the confusion
|
|
871
|
+
# this harness separates PASS/FAIL from ERROR to avoid.
|
|
872
|
+
failure = _solver_failure(outcome)
|
|
873
|
+
if failure is not None:
|
|
874
|
+
code, log = failure
|
|
875
|
+
timed_out = bool(getattr(outcome, "timed_out", False))
|
|
876
|
+
return _result(
|
|
877
|
+
ERROR, exit_code=code,
|
|
878
|
+
detail=(f"the agent ran out of time (exit {code}); check still red" if timed_out
|
|
879
|
+
else f"the agent did not complete (exit {code}); check still red"),
|
|
880
|
+
log=_trim(log), changed=changed, timed_out=timed_out,
|
|
881
|
+
)
|
|
882
|
+
|
|
883
|
+
detail = f"`{task.verify}` exited {after_code}"
|
|
884
|
+
if not changed:
|
|
885
|
+
detail += " — the agent changed nothing"
|
|
886
|
+
# The check's output says what is wrong; the agent's last words say
|
|
887
|
+
# what it thought it did. inventory-reorder failed twice on "reorder.json
|
|
888
|
+
# was not written" with nothing to show whether the agent computed the
|
|
889
|
+
# policy and only reported it, or never got that far.
|
|
890
|
+
agent_said = "\n".join(
|
|
891
|
+
part for part in (str(getattr(outcome, "stdout", "") or ""), str(getattr(outcome, "stderr", "") or ""))
|
|
892
|
+
if part.strip()
|
|
893
|
+
)
|
|
894
|
+
agent_tail = "\n".join([line for line in agent_said.splitlines() if line.strip()][-8:])
|
|
895
|
+
log = _trim(after_log)
|
|
896
|
+
if agent_tail:
|
|
897
|
+
log = f"{log}\n--- agent, last lines ---\n{agent_tail}"
|
|
898
|
+
return _result(
|
|
899
|
+
FAIL, exit_code=after_code, detail=detail,
|
|
900
|
+
log=log, changed=changed,
|
|
901
|
+
)
|
|
902
|
+
finally:
|
|
903
|
+
if not keep_workspace and scratch_root is None:
|
|
904
|
+
shutil.rmtree(scratch, ignore_errors=True)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def run_suite(
|
|
908
|
+
tasks: Sequence[TaskSpec],
|
|
909
|
+
*,
|
|
910
|
+
solver: Solver,
|
|
911
|
+
fixtures_root: str | Path,
|
|
912
|
+
name: str = "suite",
|
|
913
|
+
scratch_root: str | Path | None = None,
|
|
914
|
+
only: Iterable[str] = (),
|
|
915
|
+
tags: Iterable[str] = (),
|
|
916
|
+
on_result: Optional[Callable[[TaskResult], None]] = None,
|
|
917
|
+
keep_workspace: bool = False,
|
|
918
|
+
) -> SuiteResult:
|
|
919
|
+
"""Run every task and collect the scoreboard."""
|
|
920
|
+
wanted_ids = {str(i) for i in only if i}
|
|
921
|
+
wanted_tags = {str(t) for t in tags if t}
|
|
922
|
+
|
|
923
|
+
selected = [
|
|
924
|
+
task for task in tasks
|
|
925
|
+
if (not wanted_ids or task.id in wanted_ids)
|
|
926
|
+
and (not wanted_tags or wanted_tags & set(task.tags))
|
|
927
|
+
]
|
|
928
|
+
|
|
929
|
+
suite = SuiteResult(name=name)
|
|
930
|
+
for task in selected:
|
|
931
|
+
result = run_task(
|
|
932
|
+
task,
|
|
933
|
+
solver=solver,
|
|
934
|
+
fixtures_root=fixtures_root,
|
|
935
|
+
scratch_root=scratch_root,
|
|
936
|
+
keep_workspace=keep_workspace,
|
|
937
|
+
)
|
|
938
|
+
suite.results.append(result)
|
|
939
|
+
if on_result is not None:
|
|
940
|
+
on_result(result)
|
|
941
|
+
return suite
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def write_report(suite: SuiteResult, path: str | Path) -> Path:
|
|
945
|
+
"""Persist the scoreboard as JSON so runs can be compared over time."""
|
|
946
|
+
out = Path(path).expanduser().resolve()
|
|
947
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
948
|
+
out.write_text(json.dumps(suite.to_dict(), ensure_ascii=False, indent=2), encoding="utf-8")
|
|
949
|
+
return out
|