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,1327 @@
|
|
|
1
|
+
"""One interactive chat turn: ArtheraTerminal.send_message.
|
|
2
|
+
|
|
3
|
+
Moved out of aria_cli.py unchanged, at 1,200 lines the largest method there.
|
|
4
|
+
It still runs on aria_cli's module-level names (console, LOCAL_TOOLS,
|
|
5
|
+
_PROJECT_CONTEXT, _run_deterministic_chain, …): aria_cli binds a copy of
|
|
6
|
+
ChatTurnMixin to its own namespace with mixin_binding.bind_mixin and
|
|
7
|
+
ArtheraTerminal inherits it.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ChatTurnMixin:
|
|
14
|
+
"""send_message for ArtheraTerminal."""
|
|
15
|
+
|
|
16
|
+
async def send_message(self, message: str, system_override: Optional[str] = None,
|
|
17
|
+
evidence_grounded: bool = False, route_text: bool = False):
|
|
18
|
+
"""Send message to Aria AI with agentic tool loop, smart fallback, markdown."""
|
|
19
|
+
if getattr(self, "_streaming", False):
|
|
20
|
+
is_zh = str(self.config.get("ui_lang", "en")).lower().startswith("zh")
|
|
21
|
+
notice = (
|
|
22
|
+
"上一条请求仍在处理中;按 Esc 取消后再提交。"
|
|
23
|
+
if is_zh else
|
|
24
|
+
"A request is already running. Press Esc to cancel it before submitting another."
|
|
25
|
+
)
|
|
26
|
+
if HAS_RICH:
|
|
27
|
+
console.print(f" [yellow]{notice}[/yellow]")
|
|
28
|
+
else:
|
|
29
|
+
print(f" {notice}")
|
|
30
|
+
return
|
|
31
|
+
reference_context = ""
|
|
32
|
+
reference_service = getattr(self, "_reference_service", None)
|
|
33
|
+
if reference_service is not None and "@" in message:
|
|
34
|
+
prepared = reference_service.prepare(message)
|
|
35
|
+
if prepared.errors:
|
|
36
|
+
self._print_reference_errors(prepared)
|
|
37
|
+
return
|
|
38
|
+
if prepared.references:
|
|
39
|
+
self._print_reference_summary(prepared)
|
|
40
|
+
message = prepared.expanded_text
|
|
41
|
+
reference_context = prepared.context_block
|
|
42
|
+
|
|
43
|
+
# Resolve same-name securities before deterministic routing or the LLM
|
|
44
|
+
# can silently pick the first ticker. The clarification lives outside
|
|
45
|
+
# conversation history so the eventual request is recorded only once.
|
|
46
|
+
if not system_override and not message.lstrip().startswith("/"):
|
|
47
|
+
from apps.cli.market_universe import (
|
|
48
|
+
ambiguous_market_candidates,
|
|
49
|
+
select_market_candidate,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
def _show_market_choices(name: str, candidates: list[Any]) -> str:
|
|
53
|
+
lines = [f"“{name}”对应多个交易标的,请先确认:"]
|
|
54
|
+
for index, candidate in enumerate(candidates, start=1):
|
|
55
|
+
lines.append(
|
|
56
|
+
f"{index}. {candidate.name} — {candidate.symbol} · {candidate.market}"
|
|
57
|
+
)
|
|
58
|
+
lines.append("回复编号、证券代码或市场(如“2”“1234.HK”“港股”);输入“取消”退出。")
|
|
59
|
+
prompt = "\n".join(lines)
|
|
60
|
+
if HAS_RICH:
|
|
61
|
+
console.print("\n[bold]Aria[/bold] [dim]标的确认[/dim]")
|
|
62
|
+
console.print(make_markdown(prompt))
|
|
63
|
+
console.print()
|
|
64
|
+
else:
|
|
65
|
+
print(f"\nAria 标的确认\n{prompt}\n")
|
|
66
|
+
return prompt
|
|
67
|
+
|
|
68
|
+
pending_resolution = dict(self._pending_market_resolution or {})
|
|
69
|
+
if pending_resolution:
|
|
70
|
+
candidates = list(pending_resolution.get("candidates") or [])
|
|
71
|
+
selected = select_market_candidate(message, candidates)
|
|
72
|
+
if message.strip().lower() in {"取消", "cancel", "算了", "不用了"}:
|
|
73
|
+
self._pending_market_resolution = None
|
|
74
|
+
notice = "已取消标的选择。"
|
|
75
|
+
console.print(f"[dim]{notice}[/dim]") if HAS_RICH else print(notice)
|
|
76
|
+
return
|
|
77
|
+
if selected is not None:
|
|
78
|
+
original = str(pending_resolution.get("original") or "")
|
|
79
|
+
start = int(pending_resolution.get("start") or 0)
|
|
80
|
+
mention = str(pending_resolution.get("mention") or "")
|
|
81
|
+
message = f"{original[:start]}{selected.symbol}{original[start + len(mention):]}"
|
|
82
|
+
self._pending_market_resolution = None
|
|
83
|
+
notice = f"已确认:{selected.name} · {selected.symbol} · {selected.market}"
|
|
84
|
+
if HAS_RICH:
|
|
85
|
+
console.print(f"[green]✓[/green] {notice}")
|
|
86
|
+
else:
|
|
87
|
+
print(notice)
|
|
88
|
+
elif re.fullmatch(r"[\w.^=\-]+|港股|美股|A股|沪深|香港|美国", message.strip(), re.I):
|
|
89
|
+
_show_market_choices(str(pending_resolution.get("mention") or "该名称"), candidates)
|
|
90
|
+
return
|
|
91
|
+
else:
|
|
92
|
+
# A new natural-language request replaces the pending one.
|
|
93
|
+
self._pending_market_resolution = None
|
|
94
|
+
|
|
95
|
+
ambiguous = ambiguous_market_candidates(message)
|
|
96
|
+
if ambiguous:
|
|
97
|
+
start, mention, candidates = ambiguous[0]
|
|
98
|
+
self._pending_market_resolution = {
|
|
99
|
+
"original": message,
|
|
100
|
+
"start": start,
|
|
101
|
+
"mention": mention,
|
|
102
|
+
"candidates": candidates,
|
|
103
|
+
}
|
|
104
|
+
_show_market_choices(mention, candidates)
|
|
105
|
+
return
|
|
106
|
+
# Only what the person typed is routed to a command. A prompt a command
|
|
107
|
+
# built and sent here was routed too: /report's "为 MSFT 生成一份专业
|
|
108
|
+
# Markdown 投研报告" matched the report route and ran /report again.
|
|
109
|
+
if (
|
|
110
|
+
route_text
|
|
111
|
+
and not system_override
|
|
112
|
+
and self._pending_image is None
|
|
113
|
+
and not (self._file_session is not None and self._file_session.get_active() is not None)
|
|
114
|
+
and self._project_session is None
|
|
115
|
+
and not getattr(self, "_send_message_route_active", False)
|
|
116
|
+
):
|
|
117
|
+
routed = route_top_level_text(message, set(self.commands.commands))
|
|
118
|
+
if routed is not None:
|
|
119
|
+
self._send_message_route_active = True
|
|
120
|
+
try:
|
|
121
|
+
_announce_route(routed.text)
|
|
122
|
+
self._maybe_show_intent_preflight(routed.text)
|
|
123
|
+
await self.commands.execute(routed.text)
|
|
124
|
+
finally:
|
|
125
|
+
self._send_message_route_active = False
|
|
126
|
+
return
|
|
127
|
+
routed = _natural_language_visual_artifact_route(message, set(self.commands.commands))
|
|
128
|
+
if routed is not None:
|
|
129
|
+
self._send_message_route_active = True
|
|
130
|
+
try:
|
|
131
|
+
_announce_route(routed.text)
|
|
132
|
+
self._maybe_show_intent_preflight(routed.text)
|
|
133
|
+
await self.commands.execute(routed.text)
|
|
134
|
+
finally:
|
|
135
|
+
self._send_message_route_active = False
|
|
136
|
+
return
|
|
137
|
+
self._maybe_show_intent_preflight(message)
|
|
138
|
+
# Store optional system prompt override (used by /file analyze)
|
|
139
|
+
self._system_override = system_override
|
|
140
|
+
# Fire prompt_submit hook (Claude Code: UserPromptSubmit)
|
|
141
|
+
_run_event_hook("prompt_submit", {
|
|
142
|
+
"ARIA_MESSAGE": message[:500],
|
|
143
|
+
"ARIA_SESSION": self.session_id,
|
|
144
|
+
"ARIA_PROVIDER": self._last_provider,
|
|
145
|
+
})
|
|
146
|
+
# Attach pending image block if /vision was used before this message
|
|
147
|
+
if self._pending_image is not None:
|
|
148
|
+
user_content = [
|
|
149
|
+
{"type": "text", "text": message},
|
|
150
|
+
self._pending_image,
|
|
151
|
+
]
|
|
152
|
+
self._pending_image = None
|
|
153
|
+
elif (self._file_session is not None and
|
|
154
|
+
self._file_session.get_active() is not None):
|
|
155
|
+
# Inject loaded-file context as a text block before the user question.
|
|
156
|
+
# Only inject for the FIRST message after /file load (tracked via flag),
|
|
157
|
+
# then keep the file in system prompt for follow-up turns.
|
|
158
|
+
_fc = self._file_session.get_active()
|
|
159
|
+
_fc_ctx = self._file_session.build_context_block(max_chars=14_000)
|
|
160
|
+
user_content = f"[文件上下文已加载: {_fc.filename}]\n\n{message}"
|
|
161
|
+
# Persist file context in system prompt so follow-up questions work
|
|
162
|
+
if not hasattr(self, "_file_ctx_injected") or not self._file_ctx_injected:
|
|
163
|
+
self._file_ctx_injected = True
|
|
164
|
+
# Pre-pend file content to the very first user message
|
|
165
|
+
user_content = (f"以下是用户上传的文件内容,请在回答时参考:\n{_fc_ctx}\n\n"
|
|
166
|
+
f"---用户问题---\n{message}")
|
|
167
|
+
elif self._project_session is not None:
|
|
168
|
+
# Inject project context for the first message, then rely on history
|
|
169
|
+
_ps = self._project_session
|
|
170
|
+
user_content = f"[项目已加载: {_ps.name}]\n\n{message}"
|
|
171
|
+
if not self._project_ctx_injected:
|
|
172
|
+
self._project_ctx_injected = True
|
|
173
|
+
_pc_ctx = _ps.build_llm_context(max_chars=14_000)
|
|
174
|
+
user_content = (
|
|
175
|
+
f"以下是已加载的项目信息,请在完成任务时参考:\n{_pc_ctx}\n\n"
|
|
176
|
+
f"---用户请求---\n{message}"
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
user_content = message
|
|
180
|
+
self._file_ctx_injected = False # reset when no file loaded
|
|
181
|
+
self._project_ctx_injected = False # reset when no project loaded
|
|
182
|
+
|
|
183
|
+
if reference_context:
|
|
184
|
+
if isinstance(user_content, list) and user_content and isinstance(user_content[0], dict):
|
|
185
|
+
first_text = str(user_content[0].get("text") or "")
|
|
186
|
+
user_content[0]["text"] = f"{first_text}\n\n{reference_context}"
|
|
187
|
+
elif isinstance(user_content, str):
|
|
188
|
+
user_content = f"{user_content}\n\n{reference_context}"
|
|
189
|
+
|
|
190
|
+
try:
|
|
191
|
+
_incoming_for_compact = (
|
|
192
|
+
json.dumps(user_content, ensure_ascii=False)
|
|
193
|
+
if isinstance(user_content, (list, dict))
|
|
194
|
+
else str(user_content)
|
|
195
|
+
)
|
|
196
|
+
await self._maybe_auto_compact_before_turn(_incoming_for_compact)
|
|
197
|
+
except Exception:
|
|
198
|
+
pass
|
|
199
|
+
# The whole session as it stands before this turn, for /rewind turn.
|
|
200
|
+
from apps.cli import transactions as _transactions
|
|
201
|
+
_transactions.capture(self, message)
|
|
202
|
+
self.conversation.append({"role": "user", "content": user_content})
|
|
203
|
+
|
|
204
|
+
# ── 路由决策:支持工具调用的模型走 LLM+tool call,否则走确定性路由 ──
|
|
205
|
+
# 支持 function calling 的模型(Claude / GPT-4 class / qwen-72b+)能自己
|
|
206
|
+
# 识别公司名 → ticker 并调 get_market_data,不需要硬编码字典。
|
|
207
|
+
# 本地小模型(<14B)工具调用不稳定,保留确定性路由作降级。
|
|
208
|
+
_curr_model_id = self.config.get("model", "")
|
|
209
|
+
from aria_code.apps.cli.workspace_route import workspace_config
|
|
210
|
+
_runtime_config = workspace_config(message, _curr_model_id, self.config, self.api_url)
|
|
211
|
+
_model_has_tools = False
|
|
212
|
+
if _HAS_MODEL_CAP:
|
|
213
|
+
try:
|
|
214
|
+
_mc = get_model_capability(_curr_model_id)
|
|
215
|
+
_model_has_tools = bool(_mc.tool_calls and _mc.context_window >= 8192)
|
|
216
|
+
except Exception:
|
|
217
|
+
pass
|
|
218
|
+
if _model_has_tools:
|
|
219
|
+
try:
|
|
220
|
+
from apps.cli.providers.chat_routing import model_receives_local_tools
|
|
221
|
+
_model_has_tools = model_receives_local_tools(_curr_model_id, _runtime_config, self.api_url)
|
|
222
|
+
except Exception:
|
|
223
|
+
pass
|
|
224
|
+
|
|
225
|
+
# ── Broker guide intent: broad discovery should not start an add wizard ──
|
|
226
|
+
if _is_broker_guide_intent(message):
|
|
227
|
+
if HAS_RICH:
|
|
228
|
+
console.print("\n[bold]Aria[/bold] [dim] 正在打开券商与服务指南…[/dim]\n")
|
|
229
|
+
await self.commands.cmd_broker("guide")
|
|
230
|
+
await self.commands.cmd_broker("services")
|
|
231
|
+
await self.commands.cmd_packages("services")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
# ── Broker setup intent: intercept before LLM / deterministic routing ──
|
|
235
|
+
if _is_broker_setup_intent(message):
|
|
236
|
+
_btype = _detect_broker_type(message)
|
|
237
|
+
if HAS_RICH:
|
|
238
|
+
from apps.cli.utils.market_detect import _BROKER_SETUP_NAMES
|
|
239
|
+
_display = _BROKER_SETUP_NAMES.get(_btype, ("",))[0] if _btype else ""
|
|
240
|
+
_label = f" 正在启动{_display}配置向导…" if _display else " 正在启动券商配置向导…"
|
|
241
|
+
console.print(f"\n[bold]Aria[/bold] [dim]{_label}[/dim]\n")
|
|
242
|
+
await self.commands._cmd_broker_add(_btype)
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
if _is_artifact_action_followup(message) and getattr(self, "_pending_market_artifact", None):
|
|
246
|
+
pending_artifact = dict(self._pending_market_artifact or {})
|
|
247
|
+
action_summary = _handle_pending_artifact_action(pending_artifact, message)
|
|
248
|
+
if HAS_RICH:
|
|
249
|
+
console.print("\n[bold]Aria[/bold]\n")
|
|
250
|
+
console.print(make_markdown(_strip_latex(action_summary)))
|
|
251
|
+
console.print()
|
|
252
|
+
else:
|
|
253
|
+
print("\nAria\n")
|
|
254
|
+
print(action_summary)
|
|
255
|
+
self.conversation.append({"role": "assistant", "content": action_summary})
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
if _is_artifact_location_followup(message) and getattr(self, "_pending_market_artifact", None):
|
|
259
|
+
pending_artifact = dict(self._pending_market_artifact or {})
|
|
260
|
+
_print_pending_artifact_location(pending_artifact)
|
|
261
|
+
_paths = [
|
|
262
|
+
str(pending_artifact.get(key) or "").strip()
|
|
263
|
+
for key in ("pine_path", "html_path", "png_path", "chart_path", "path", "raw_path", "url")
|
|
264
|
+
if str(pending_artifact.get(key) or "").strip()
|
|
265
|
+
]
|
|
266
|
+
_children = pending_artifact.get("children") or []
|
|
267
|
+
if isinstance(_children, list):
|
|
268
|
+
for _child in _children:
|
|
269
|
+
if isinstance(_child, dict):
|
|
270
|
+
_child_path = str(_child.get("html_path") or _child.get("chart_path") or _child.get("path") or "").strip()
|
|
271
|
+
if _child_path:
|
|
272
|
+
_paths.append(_child_path)
|
|
273
|
+
_summary = "最近生成文件:\n" + "\n".join(f"- {p}" for p in _paths) if _paths else "最近任务尚未记录具体文件路径。"
|
|
274
|
+
self.conversation.append({"role": "assistant", "content": _summary})
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
if _is_market_artifact_followup(message) and getattr(self, "_pending_market_artifact", None):
|
|
278
|
+
pending_artifact = dict(self._pending_market_artifact or {})
|
|
279
|
+
symbol = str(pending_artifact.get("symbol") or "").strip()
|
|
280
|
+
period = str(pending_artifact.get("period") or "1y").strip() or "1y"
|
|
281
|
+
if symbol:
|
|
282
|
+
if HAS_RICH:
|
|
283
|
+
console.print(f"\n[bold]Aria[/bold]\n")
|
|
284
|
+
console.print(f" [dim]继续上一项市场图表任务:{symbol} {period}[/dim]")
|
|
285
|
+
await self.commands.cmd_chart(f"{symbol} {period}")
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
# ── Football prediction intercept → built-in Poisson handler ──────────
|
|
289
|
+
if await self._try_football_nl_intercept(message):
|
|
290
|
+
return
|
|
291
|
+
|
|
292
|
+
_det_wants_analysis = False # set True for snapshot + analysis query → LLM follows
|
|
293
|
+
deterministic = _run_deterministic_chain(
|
|
294
|
+
message, model_has_tools=_model_has_tools, history=self.conversation[:-1])
|
|
295
|
+
if deterministic.get("success") or _is_stock_chart_analysis_request(message):
|
|
296
|
+
# The handlers answer in the language of the question; so does the
|
|
297
|
+
# frame around their answer.
|
|
298
|
+
_det_en = _detect_lang(message) == "en"
|
|
299
|
+
final_text = deterministic.get("response", "")
|
|
300
|
+
if not final_text:
|
|
301
|
+
final_text = (
|
|
302
|
+
f"Market analysis did not finish: {deterministic.get('error', 'unknown error')}"
|
|
303
|
+
if _det_en else
|
|
304
|
+
f"市场分析未完成:{deterministic.get('error', '未知错误')}"
|
|
305
|
+
)
|
|
306
|
+
_tools = deterministic.get("tools_used", [])
|
|
307
|
+
_tool_label = ({
|
|
308
|
+
"market_snapshot": "Market snapshot",
|
|
309
|
+
"stock_chart": "Chart analysis",
|
|
310
|
+
"broker_query": "Account data",
|
|
311
|
+
"realty_query": "Real-estate data",
|
|
312
|
+
"strategy_advice": "Strategy framework",
|
|
313
|
+
} if _det_en else {
|
|
314
|
+
"market_snapshot": "市场快照",
|
|
315
|
+
"stock_chart": "图表分析",
|
|
316
|
+
"broker_query": "账户数据",
|
|
317
|
+
"realty_query": "房地产数据",
|
|
318
|
+
"strategy_advice": "策略框架",
|
|
319
|
+
}).get(_tools[0], _tools[0]) if _tools else ("Local analysis" if _det_en else "本地分析")
|
|
320
|
+
_rate_limited = deterministic.get("rate_limited", False)
|
|
321
|
+
_rl_note = (
|
|
322
|
+
f" [yellow]⚠ {'Data source rate-limited' if _det_en else '数据源限流'}[/yellow]"
|
|
323
|
+
if _rate_limited else ""
|
|
324
|
+
)
|
|
325
|
+
if _tools and _tools[0] == "stock_chart":
|
|
326
|
+
_chart_symbol = deterministic.get("symbol") or _extract_market_symbol(message)
|
|
327
|
+
self._pending_market_artifact = {
|
|
328
|
+
"kind": "stock_chart",
|
|
329
|
+
"symbol": _chart_symbol,
|
|
330
|
+
"period": "1y",
|
|
331
|
+
"html_path": deterministic.get("chart_path", ""),
|
|
332
|
+
"command": f"/chart {_chart_symbol or 'AAPL'} 1y",
|
|
333
|
+
}
|
|
334
|
+
if _tools and _tools[0] == "market_snapshot":
|
|
335
|
+
_snapshot_now = time.time()
|
|
336
|
+
if not _is_market_snapshot_refresh_request(message):
|
|
337
|
+
_repeat_notice = _build_market_snapshot_repeat_notice(
|
|
338
|
+
deterministic,
|
|
339
|
+
self._last_market_snapshot_cache,
|
|
340
|
+
now=_snapshot_now,
|
|
341
|
+
lang="en" if _det_en else "zh",
|
|
342
|
+
)
|
|
343
|
+
if _repeat_notice:
|
|
344
|
+
final_text = _repeat_notice
|
|
345
|
+
deterministic = dict(
|
|
346
|
+
deterministic,
|
|
347
|
+
response=final_text,
|
|
348
|
+
analysis_complete=True,
|
|
349
|
+
compressed_repeat=True,
|
|
350
|
+
)
|
|
351
|
+
_cache_entry = _market_snapshot_cache_entry(deterministic, now=_snapshot_now)
|
|
352
|
+
if _cache_entry:
|
|
353
|
+
self._last_market_snapshot_cache = _cache_entry
|
|
354
|
+
# For snapshot + "分析" queries: show data then continue to LLM for deep analysis
|
|
355
|
+
_det_wants_analysis = (
|
|
356
|
+
not bool(deterministic.get("analysis_complete"))
|
|
357
|
+
and any(k in message for k in ("分析", "analyze", "analysis", "对比", "比较", "compare"))
|
|
358
|
+
and bool(_tools) and _tools[0] == "market_snapshot"
|
|
359
|
+
)
|
|
360
|
+
_disclaimer = "" if _tools and _tools[0] == "strategy_advice" else (
|
|
361
|
+
" · Not investment advice" if _det_en else " · 本内容不构成投资建议")
|
|
362
|
+
if HAS_RICH:
|
|
363
|
+
# ⏺/✓ workflow indicator — mirrors LLM tool call display style
|
|
364
|
+
_t_icon = _tools[0] if _tools else "local"
|
|
365
|
+
console.print(f"\n [#C08050]⏺[/#C08050] [bold]{_t_icon}[/bold]")
|
|
366
|
+
if deterministic.get("compressed_repeat"):
|
|
367
|
+
_done_label = "unchanged" if _det_en else "未变化"
|
|
368
|
+
elif _tools and _tools[0] == "strategy_advice":
|
|
369
|
+
_done_label = "ready" if _det_en else "已生成"
|
|
370
|
+
else:
|
|
371
|
+
_done_label = "fetched" if _det_en else "数据已获取"
|
|
372
|
+
console.print(f" [green]✓[/green] [dim]{_tool_label} {_done_label}[/dim]")
|
|
373
|
+
console.print()
|
|
374
|
+
console.print(make_markdown(_strip_latex(final_text)))
|
|
375
|
+
console.print(f"\n[dim]{_tool_label}{_disclaimer}[/dim]{_rl_note}\n")
|
|
376
|
+
else:
|
|
377
|
+
print("\nAria\n")
|
|
378
|
+
print(final_text)
|
|
379
|
+
print(f"\n{_tool_label}{_disclaimer}\n")
|
|
380
|
+
self.conversation.append({"role": "assistant", "content": final_text})
|
|
381
|
+
self._last_response = final_text
|
|
382
|
+
if not _det_wants_analysis:
|
|
383
|
+
return
|
|
384
|
+
# Analysis query: fall through to LLM for deep commentary on the snapshot data
|
|
385
|
+
|
|
386
|
+
model = self.config.get("model", "qwen2.5:7b")
|
|
387
|
+
thinking_mode = self.config.get("thinking_mode", "auto")
|
|
388
|
+
from aria_code.account_session import prepare_account, account_id
|
|
389
|
+
_memory_owner = account_id(self.config)
|
|
390
|
+
from aria_code.apps.cli.config_store import save_cli_config
|
|
391
|
+
from aria_code.apps.cli.config_paths import resolve_paths
|
|
392
|
+
save_account = lambda cfg: save_cli_config(resolve_paths(), cfg)
|
|
393
|
+
auth_token = await prepare_account(self.config, save_account)
|
|
394
|
+
if account_id(self.config) != _memory_owner:
|
|
395
|
+
print("账户已切换,请重新发送这条消息。")
|
|
396
|
+
return
|
|
397
|
+
auth_token = auth_token or self.config.get("auth_token")
|
|
398
|
+
user_context = _build_user_context(self.config)
|
|
399
|
+
self.cancel_event = asyncio.Event()
|
|
400
|
+
self._streaming = True
|
|
401
|
+
set_robot_state(RobotState.THINKING)
|
|
402
|
+
_esc_watcher.start(self.cancel_event)
|
|
403
|
+
|
|
404
|
+
# Context pressure warning — only once per session when > 85% full
|
|
405
|
+
_est_tokens = sum(len(m.get("content", "")) for m in self.conversation) // 3
|
|
406
|
+
_max_ctx = get_model_cfg(self.config.get("model", "qwen2.5:7b")).get("num_ctx", 16384)
|
|
407
|
+
from ui.render.output import print_context_warning as _pcw
|
|
408
|
+
_pcw(_est_tokens, _max_ctx, console=console, has_rich=HAS_RICH,
|
|
409
|
+
session_id=getattr(self, "session_id", ""))
|
|
410
|
+
|
|
411
|
+
if HAS_RICH:
|
|
412
|
+
console.print()
|
|
413
|
+
start_time = time.time()
|
|
414
|
+
self._begin_runtime_run(message)
|
|
415
|
+
|
|
416
|
+
# --- Dynamic max_rounds: scale with task complexity ---
|
|
417
|
+
# Treat this as a soft budget. If the model is still making concrete
|
|
418
|
+
# tool progress at the soft limit, keep going so one user instruction
|
|
419
|
+
# can finish end-to-end instead of stopping after a tool result.
|
|
420
|
+
# (decision logic: apps/cli/turn_planning.py, unit-tested in isolation)
|
|
421
|
+
_task_complexity_signals = is_complex_task(message)
|
|
422
|
+
max_rounds, hard_max_rounds = round_budget_for(_task_complexity_signals)
|
|
423
|
+
|
|
424
|
+
# --- Task decomposition for complex multi-step requests ---
|
|
425
|
+
# For long or multi-step messages, ask the AI to produce a plan first,
|
|
426
|
+
# then inject it as context so the agentic loop follows a clear path.
|
|
427
|
+
_decomp_plan: str = ""
|
|
428
|
+
if should_decompose(message, _task_complexity_signals):
|
|
429
|
+
self._transition_runtime_run(
|
|
430
|
+
RunStatus.PLANNING,
|
|
431
|
+
reason="complex_request_decomposition",
|
|
432
|
+
)
|
|
433
|
+
_decomp_prompt = (
|
|
434
|
+
"Break the following user request into a numbered step-by-step execution plan "
|
|
435
|
+
"(max 8 steps, one line each). Be concrete and tool-aware. "
|
|
436
|
+
"Output ONLY the numbered list, nothing else.\n\n"
|
|
437
|
+
f"Request: {message[:600]}"
|
|
438
|
+
)
|
|
439
|
+
try:
|
|
440
|
+
_plan_result = await stream_provider_result(
|
|
441
|
+
OllamaProvider(
|
|
442
|
+
self.config.get("ollama_url", "http://localhost:11434"),
|
|
443
|
+
self.config.get("model", "qwen2.5:7b"),
|
|
444
|
+
show_market_prefetch_status=False,
|
|
445
|
+
),
|
|
446
|
+
_decomp_prompt,
|
|
447
|
+
[],
|
|
448
|
+
tools=[],
|
|
449
|
+
)
|
|
450
|
+
if _plan_result.get("success") and _plan_result.get("response"):
|
|
451
|
+
_decomp_plan = _plan_result["response"].strip()
|
|
452
|
+
except Exception:
|
|
453
|
+
pass # decomposition is best-effort
|
|
454
|
+
|
|
455
|
+
self._transition_runtime_run(
|
|
456
|
+
RunStatus.RUNNING,
|
|
457
|
+
reason="agent_loop_started",
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
# Inject plan as a prefix to the first turn's message so the AI
|
|
461
|
+
# follows the decomposed steps rather than free-forming the approach.
|
|
462
|
+
# (assembly decisions: apps/cli/prompt_assembly.py, unit-tested)
|
|
463
|
+
current_message = build_base_message(
|
|
464
|
+
message,
|
|
465
|
+
wants_analysis_commentary=_det_wants_analysis,
|
|
466
|
+
decomposition_plan=_decomp_plan,
|
|
467
|
+
snapshot=deterministic.get("response", "") if _det_wants_analysis else "",
|
|
468
|
+
lang=self.config.get("ui_lang", "en") or "en",
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
# Referenced paths stay as pointers. The model must use audited file
|
|
472
|
+
# tools, preserving permissions, tool traces, and context efficiency.
|
|
473
|
+
if should_prepend_file_tool_hint(_det_wants_analysis, reference_context):
|
|
474
|
+
_file_tool_hint = _build_file_tool_hint(message)
|
|
475
|
+
if _file_tool_hint:
|
|
476
|
+
current_message = _file_tool_hint + current_message
|
|
477
|
+
|
|
478
|
+
# ── ML 预测信号注入:聊天中自动检测标的并注入 5 日预测参考 ──────────
|
|
479
|
+
# 仅在 LLM 路径触发(已过确定性路由),且消息含分析意图时启用。
|
|
480
|
+
# 信号注入 current_message(不污染 conversation history),3s 超时。
|
|
481
|
+
_ml_signal_syms: list = []
|
|
482
|
+
try:
|
|
483
|
+
if _is_stock_analysis_intent(message) and not _model_has_tools:
|
|
484
|
+
_ml_signal_syms = _extract_market_symbols(message, limit=3)
|
|
485
|
+
if _ml_signal_syms:
|
|
486
|
+
_ml_sig = _fetch_quick_ml_signal(_ml_signal_syms)
|
|
487
|
+
current_message = with_ml_signal_prefix(current_message, _ml_sig)
|
|
488
|
+
except Exception:
|
|
489
|
+
_ml_signal_syms = []
|
|
490
|
+
|
|
491
|
+
turn_state = AgentTurnState(provider="aws")
|
|
492
|
+
provider = turn_state.provider
|
|
493
|
+
token_count = 0
|
|
494
|
+
thinking_tokens = 0
|
|
495
|
+
elapsed = 0.0
|
|
496
|
+
|
|
497
|
+
try:
|
|
498
|
+
# The update_todos tool writes the aria_code.* copy; clearing the
|
|
499
|
+
# bare-named one cleared a list nothing wrote to.
|
|
500
|
+
from aria_code.apps.cli.todo_tracker import clear_todos as _clear_todos
|
|
501
|
+
_clear_todos() # reset task checklist for this new turn
|
|
502
|
+
except Exception:
|
|
503
|
+
pass
|
|
504
|
+
|
|
505
|
+
_response_header_printed = False
|
|
506
|
+
|
|
507
|
+
def _print_response_header() -> None:
|
|
508
|
+
nonlocal _response_header_printed
|
|
509
|
+
if _response_header_printed:
|
|
510
|
+
return
|
|
511
|
+
_response_header_printed = True
|
|
512
|
+
_answer_model = self._actual_model or self.config.get("model", "")
|
|
513
|
+
_answer_meta = f" [dim]· {_answer_model}[/dim]" if _answer_model else ""
|
|
514
|
+
_agent_name = getattr(self, "_active_agent_name", "Aria")
|
|
515
|
+
_agent_color = getattr(self, "_active_agent_color", "blue")
|
|
516
|
+
if _agent_name == "Aria":
|
|
517
|
+
_agent_color = "bold"
|
|
518
|
+
if HAS_RICH:
|
|
519
|
+
console.print(f"[{_agent_color}]{_agent_name}[/{_agent_color}]{_answer_meta}")
|
|
520
|
+
else:
|
|
521
|
+
print(f"{_agent_name}{' · ' + _answer_model if _answer_model else ''}")
|
|
522
|
+
# A task about this folder on a model that cannot open it gets a guess
|
|
523
|
+
# that reads like a result (backend_chat answered "5" for a function
|
|
524
|
+
# that returns -1). Stop this task before generating a guessed result.
|
|
525
|
+
if not _model_has_tools:
|
|
526
|
+
from apps.cli.workspace_route import needs_workspace, no_tools_message
|
|
527
|
+
if needs_workspace(message):
|
|
528
|
+
self._no_tools_notice_shown = True
|
|
529
|
+
_zh_nt = str(self.config.get("ui_lang", "en")).lower().startswith("zh")
|
|
530
|
+
_why = no_tools_message(self.config, lang="zh" if _zh_nt else "en")
|
|
531
|
+
if HAS_RICH:
|
|
532
|
+
from aria_code.ui.render.output import print_hanging
|
|
533
|
+
print_hanging(console, " ! ", _why, style="yellow")
|
|
534
|
+
else:
|
|
535
|
+
print(f" ! {_why}")
|
|
536
|
+
return
|
|
537
|
+
# ── Single-shot turn through the shared runtime Gateway ─────────────
|
|
538
|
+
# The per-round inline agent loop that used to live here was removed
|
|
539
|
+
# (2026-07) after the runtime path was validated with real turns:
|
|
540
|
+
# plain chat, inline-parity, and native tool-calling all served by
|
|
541
|
+
# gateway.run_turn → run_agent, which owns rounds, tool execution,
|
|
542
|
+
# loop-guard and (now) per-tool approval. This block only adapts the
|
|
543
|
+
# terminal — stream consumer, approval UI, run-store transitions —
|
|
544
|
+
# to that loop and renders its outcome.
|
|
545
|
+
from apps.cli.providers.runtime_bridge import run_chat_via_runtime
|
|
546
|
+
|
|
547
|
+
response_text = ""
|
|
548
|
+
# Explored / Ran / Edit cells instead of a ⏺ and a ✓ per call;
|
|
549
|
+
# tool_display=classic keeps the per-call lines.
|
|
550
|
+
from ui.render.actions import ActionView
|
|
551
|
+
_action_view = (
|
|
552
|
+
None if _ARIA_BOT_MODE or self.config.get("tool_display", "actions") == "classic"
|
|
553
|
+
else ActionView()
|
|
554
|
+
)
|
|
555
|
+
stream_consumer = TerminalRuntimeEventConsumer(
|
|
556
|
+
terminal=self,
|
|
557
|
+
console=console,
|
|
558
|
+
has_rich=HAS_RICH,
|
|
559
|
+
markdown_cls=make_markdown,
|
|
560
|
+
live_cls=Live,
|
|
561
|
+
strip_latex=_strip_latex,
|
|
562
|
+
set_robot_state=set_robot_state,
|
|
563
|
+
streaming_state=RobotState.STREAMING,
|
|
564
|
+
print_tool_call=_print_tool_call,
|
|
565
|
+
print_tool_done=_print_tool_done,
|
|
566
|
+
fallback_from=self._last_provider or "local",
|
|
567
|
+
ui_lang=self.config.get("ui_lang", "en") or "en",
|
|
568
|
+
on_response_start=_print_response_header,
|
|
569
|
+
action_view=_action_view,
|
|
570
|
+
)
|
|
571
|
+
_start_spinner = stream_consumer.start_spinner
|
|
572
|
+
_stop_spinner = stream_consumer.stop_spinner
|
|
573
|
+
_stop_live = stream_consumer.stop_live
|
|
574
|
+
_flush_latex_buf = stream_consumer.flush_latex_buf
|
|
575
|
+
_first_token_received = stream_consumer.first_token_received_ref
|
|
576
|
+
_use_plain_print = stream_consumer.use_plain_print_ref
|
|
577
|
+
_use_batch_render = stream_consumer.use_batch_render_ref
|
|
578
|
+
_latex_buf = stream_consumer.latex_buf_ref
|
|
579
|
+
_in_latex = stream_consumer.in_latex_ref
|
|
580
|
+
|
|
581
|
+
_start_spinner()
|
|
582
|
+
|
|
583
|
+
on_token = stream_consumer.on_token
|
|
584
|
+
on_thinking = stream_consumer.on_thinking
|
|
585
|
+
on_tool_call = stream_consumer.on_tool_call
|
|
586
|
+
on_tool_result = stream_consumer.on_tool_result
|
|
587
|
+
on_status = stream_consumer.on_status
|
|
588
|
+
|
|
589
|
+
# Interactive tool approval, threaded through the gateway into
|
|
590
|
+
# run_agent's tool loop (previously an inline-loop exclusive — the
|
|
591
|
+
# runtime path used to execute _CONFIRM_TOOLS without prompting).
|
|
592
|
+
approval_consumer = TerminalApprovalEventConsumer(
|
|
593
|
+
terminal=self,
|
|
594
|
+
console=console,
|
|
595
|
+
has_rich=HAS_RICH,
|
|
596
|
+
confirm_decision=_confirm_tool_execution_decision,
|
|
597
|
+
apply_decision=_apply_tool_approval,
|
|
598
|
+
save_config=save_config,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
async def _approval_callback(tool_name: str, tool_params: dict) -> ApprovalDecision:
|
|
602
|
+
self._transition_runtime_run(
|
|
603
|
+
RunStatus.WAITING_APPROVAL,
|
|
604
|
+
reason="tool_requires_approval",
|
|
605
|
+
data={"tool": tool_name},
|
|
606
|
+
)
|
|
607
|
+
try:
|
|
608
|
+
return await approval_consumer.approve(
|
|
609
|
+
tool_name,
|
|
610
|
+
tool_params,
|
|
611
|
+
stop_before_prompt=_stop_live,
|
|
612
|
+
)
|
|
613
|
+
finally:
|
|
614
|
+
self._transition_runtime_run(
|
|
615
|
+
RunStatus.RUNNING,
|
|
616
|
+
reason="tool_approval_resolved",
|
|
617
|
+
data={"tool": tool_name},
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
def _approval_applier(tool_params: dict, approval: ApprovalDecision) -> dict:
|
|
621
|
+
return approval_consumer.apply(tool_params, approval)
|
|
622
|
+
|
|
623
|
+
# Parity with the old inline local_mode rendering: Ollama generates
|
|
624
|
+
# with no live display — accumulate silently, Rich-render at the end.
|
|
625
|
+
if self.config.get("local_mode", False):
|
|
626
|
+
_use_plain_print[0] = True
|
|
627
|
+
_use_batch_render[0] = True
|
|
628
|
+
|
|
629
|
+
# Capture the pending system-role override WITHOUT consuming it yet:
|
|
630
|
+
# only a confirmed-successful turn clears it.
|
|
631
|
+
_rt_sys_ov = getattr(self, "_system_override", None)
|
|
632
|
+
_rt_turn = None
|
|
633
|
+
from packages.aria_services.research_protocol import (
|
|
634
|
+
grounding_tool_names,
|
|
635
|
+
requires_financial_evidence,
|
|
636
|
+
)
|
|
637
|
+
_requires_financial_evidence = requires_financial_evidence(message)
|
|
638
|
+
# 空响应自动重试:云端模型偶发空补全时,同 provider 重放本轮一次,
|
|
639
|
+
# 而不是把"请重试"推给用户(60s 的工具结果/思考不该因一次抽风作废)。
|
|
640
|
+
# 仅对 empty_response 重试;其他错误(配额/鉴权等)走原有 rescue 链。
|
|
641
|
+
_ACTION_VIEW_ACTIVE[0] = _action_view is not None
|
|
642
|
+
# The task worktree this turn edits instead of the user's files; None
|
|
643
|
+
# when isolation is off or this is not a git repository.
|
|
644
|
+
from apps.cli import task_isolation as _task_isolation
|
|
645
|
+
|
|
646
|
+
def _task_note(text: str) -> None:
|
|
647
|
+
if HAS_RICH:
|
|
648
|
+
from rich.markup import escape as _escape_markup
|
|
649
|
+
console.print(f" [dim]{_escape_markup(text)}[/dim]")
|
|
650
|
+
else:
|
|
651
|
+
print(f" {text}")
|
|
652
|
+
|
|
653
|
+
from aria_code.apps.cli.conversation_policy import is_conversation_only_message
|
|
654
|
+
_task = None if (not (_rt_sys_ov or "").strip() and is_conversation_only_message(message)) else _task_isolation.prepare(_runtime_config, notify=_task_note)
|
|
655
|
+
_task_context = _task_isolation.execution_context(_task, _runtime_config)
|
|
656
|
+
try:
|
|
657
|
+
for _rt_attempt in range(2):
|
|
658
|
+
try:
|
|
659
|
+
_rt_turn = await run_chat_via_runtime(
|
|
660
|
+
prompt=current_message, history=self.conversation[:-1],
|
|
661
|
+
local_tools=LOCAL_TOOLS, tool_schemas=LOCAL_TOOL_SCHEMAS,
|
|
662
|
+
model=model, config=_runtime_config, api_url=self.api_url,
|
|
663
|
+
ollama_url=self.config.get("ollama_url", "http://localhost:11434"),
|
|
664
|
+
cancel_event=self.cancel_event,
|
|
665
|
+
on_token=on_token, on_thinking=on_thinking,
|
|
666
|
+
on_tool_call=on_tool_call,
|
|
667
|
+
on_tool_result=on_tool_result, on_status=on_status,
|
|
668
|
+
thinking_mode=thinking_mode, user_context=user_context,
|
|
669
|
+
auth_token=auth_token, project_context=_PROJECT_CONTEXT,
|
|
670
|
+
system_override=_rt_sys_ov,
|
|
671
|
+
max_rounds=hard_max_rounds,
|
|
672
|
+
confirm_tools=_CONFIRM_TOOLS,
|
|
673
|
+
approval_callback=_approval_callback,
|
|
674
|
+
approval_applier=_approval_applier,
|
|
675
|
+
requires_evidence=_requires_financial_evidence,
|
|
676
|
+
grounding_tools=grounding_tool_names(LOCAL_TOOL_SCHEMAS),
|
|
677
|
+
evidence_already_grounded=bool(
|
|
678
|
+
evidence_grounded
|
|
679
|
+
or (_det_wants_analysis and deterministic.get("success"))
|
|
680
|
+
),
|
|
681
|
+
execution_context=lambda: {
|
|
682
|
+
"_run_id": self._active_run_id,
|
|
683
|
+
"_session_id": self.session_id,
|
|
684
|
+
**_task_context,
|
|
685
|
+
},
|
|
686
|
+
return_result=True,
|
|
687
|
+
)
|
|
688
|
+
except Exception as _rt_err:
|
|
689
|
+
logger.error("Runtime turn failed: %s", _rt_err)
|
|
690
|
+
_rt_turn = None
|
|
691
|
+
|
|
692
|
+
_rt_probe_text = getattr(_rt_turn, "text", "") if _rt_turn is not None else ""
|
|
693
|
+
_rt_probe_cancelled = bool(getattr(_rt_turn, "cancelled", False)) or bool(
|
|
694
|
+
self.cancel_event is not None and self.cancel_event.is_set()
|
|
695
|
+
)
|
|
696
|
+
if _rt_probe_cancelled or (_rt_probe_text or "").strip():
|
|
697
|
+
break
|
|
698
|
+
_rt_probe_err = (
|
|
699
|
+
getattr(_rt_turn, "error", None) if _rt_turn is not None else None
|
|
700
|
+
) or "empty_response"
|
|
701
|
+
if _rt_attempt == 0 and "empty_response" in str(_rt_probe_err) and "ARIA-4223" not in str(_rt_probe_err):
|
|
702
|
+
if HAS_RICH:
|
|
703
|
+
console.print(" [dim yellow]⚠ Empty response from model; auto-retrying once...[/dim yellow]")
|
|
704
|
+
else:
|
|
705
|
+
print(" ⚠ Empty response from model; auto-retrying once...")
|
|
706
|
+
continue
|
|
707
|
+
break
|
|
708
|
+
|
|
709
|
+
finally:
|
|
710
|
+
# Reset whatever ends the turn: /run after it must print its output.
|
|
711
|
+
_ACTION_VIEW_ACTIVE[0] = False
|
|
712
|
+
if _action_view is not None:
|
|
713
|
+
self._action_details = list(_action_view.details)
|
|
714
|
+
response_text = stream_consumer.response_text
|
|
715
|
+
token_count = stream_consumer.token_count
|
|
716
|
+
thinking_tokens = stream_consumer.thinking_tokens
|
|
717
|
+
_stop_live()
|
|
718
|
+
|
|
719
|
+
_rt_text = getattr(_rt_turn, "text", "") if _rt_turn is not None else ""
|
|
720
|
+
_rt_cancelled = bool(getattr(_rt_turn, "cancelled", False)) or bool(
|
|
721
|
+
self.cancel_event is not None and self.cancel_event.is_set()
|
|
722
|
+
)
|
|
723
|
+
if _rt_cancelled:
|
|
724
|
+
result = {"success": False, "response": response_text, "cancelled": True}
|
|
725
|
+
turn_state.append_response(response_text)
|
|
726
|
+
elif (_rt_text or "").strip():
|
|
727
|
+
self._system_override = None # consumed by the successful turn
|
|
728
|
+
_rt_final = getattr(_rt_turn, "final", None)
|
|
729
|
+
_rt_metadata = getattr(_rt_final, "metadata", None)
|
|
730
|
+
_rt_provider = (
|
|
731
|
+
getattr(_rt_final, "provider", "")
|
|
732
|
+
or str(self.config.get("local_provider") or "ollama")
|
|
733
|
+
)
|
|
734
|
+
result = {
|
|
735
|
+
"success": bool(getattr(_rt_turn, "ok", False)),
|
|
736
|
+
"response": _rt_text,
|
|
737
|
+
"error": getattr(_rt_turn, "error", None) or getattr(_rt_final, "error", ""),
|
|
738
|
+
"stop_reason": getattr(_rt_final, "stop_reason", "completed"),
|
|
739
|
+
"acceptance": getattr(_rt_final, "acceptance", None),
|
|
740
|
+
"delivery": getattr(_rt_final, "delivery", None),
|
|
741
|
+
# Captured now: the run id is cleared when the run is marked done,
|
|
742
|
+
# before the report that names it for /rewind is printed.
|
|
743
|
+
"run_id": self._active_run_id,
|
|
744
|
+
"provider": _rt_provider,
|
|
745
|
+
"cancelled": False,
|
|
746
|
+
"usage": {
|
|
747
|
+
"prompt_tokens": getattr(_rt_metadata, "prompt_tokens", 0),
|
|
748
|
+
"completion_tokens": getattr(_rt_metadata, "completion_tokens", 0),
|
|
749
|
+
"thinking_tokens": getattr(_rt_metadata, "thinking_tokens", 0),
|
|
750
|
+
},
|
|
751
|
+
"tools_used": list(getattr(_rt_final, "tools", []) or []),
|
|
752
|
+
"sources": list(getattr(_rt_final, "sources", []) or []),
|
|
753
|
+
}
|
|
754
|
+
# 状态条 ctx% 的真实数据源:provider 上报的本轮上下文占用
|
|
755
|
+
# (prompt 含系统提示/skills,字符估算看不见这部分)。
|
|
756
|
+
try:
|
|
757
|
+
self._last_prompt_tokens = int(
|
|
758
|
+
(getattr(_rt_metadata, "prompt_tokens", 0) or 0)
|
|
759
|
+
+ (getattr(_rt_metadata, "completion_tokens", 0) or 0)
|
|
760
|
+
)
|
|
761
|
+
self._last_prompt_tokens_msgs = len(self.conversation)
|
|
762
|
+
except Exception:
|
|
763
|
+
pass
|
|
764
|
+
response_text = stream_consumer.response_text or _rt_text
|
|
765
|
+
turn_state.provider = _rt_provider
|
|
766
|
+
turn_state.apply_model_result(result, response_text)
|
|
767
|
+
provider = turn_state.provider
|
|
768
|
+
self._last_provider = _rt_provider
|
|
769
|
+
else:
|
|
770
|
+
# Runtime turn failed or produced nothing. Keep the old inline
|
|
771
|
+
# chain's most valuable recovery in compact form: one direct
|
|
772
|
+
# cloud-API rescue (providers/llm/registry fallback chain),
|
|
773
|
+
# honoring the provider_fallback config.
|
|
774
|
+
_err = (getattr(_rt_turn, "error", None) if _rt_turn is not None else None) or "empty_response"
|
|
775
|
+
result = {"success": False, "error": str(_err), "response": "", "cancelled": False}
|
|
776
|
+
_fallback_mode = str(self.config.get("provider_fallback", "configured")).lower()
|
|
777
|
+
_rescue = None
|
|
778
|
+
if _fallback_mode != "off" and "ARIA-4223" not in str(_err) and "ollama_model_unavailable:" not in str(_err):
|
|
779
|
+
try:
|
|
780
|
+
from providers.llm.registry import stream_cloud_fallback
|
|
781
|
+
_rescue = await stream_cloud_fallback(
|
|
782
|
+
current_message, self.conversation,
|
|
783
|
+
on_token=on_token,
|
|
784
|
+
cancel_event=self.cancel_event,
|
|
785
|
+
include_defaults=_fallback_mode == "auto",
|
|
786
|
+
)
|
|
787
|
+
except Exception as _rescue_err:
|
|
788
|
+
logger.debug(
|
|
789
|
+
"Cloud rescue after runtime failure did not complete: %s",
|
|
790
|
+
_rescue_err,
|
|
791
|
+
)
|
|
792
|
+
_rescue = None
|
|
793
|
+
if _rescue is not None and _rescue.get("cancelled"):
|
|
794
|
+
response_text = stream_consumer.response_text
|
|
795
|
+
result = {"success": False, "response": response_text, "cancelled": True}
|
|
796
|
+
turn_state.append_response(response_text)
|
|
797
|
+
elif (
|
|
798
|
+
_rescue is not None
|
|
799
|
+
and _rescue.get("success")
|
|
800
|
+
and (_rescue.get("response") or "").strip()
|
|
801
|
+
):
|
|
802
|
+
self._system_override = None
|
|
803
|
+
result = _rescue
|
|
804
|
+
response_text = stream_consumer.response_text or result.get("response", "")
|
|
805
|
+
token_count = stream_consumer.token_count
|
|
806
|
+
thinking_tokens = stream_consumer.thinking_tokens
|
|
807
|
+
turn_state.provider = result.get("provider", "cloud")
|
|
808
|
+
turn_state.apply_model_result(result, response_text)
|
|
809
|
+
provider = turn_state.provider
|
|
810
|
+
self._last_provider = provider
|
|
811
|
+
else:
|
|
812
|
+
stream_consumer.finish(TurnPhase.ERROR)
|
|
813
|
+
set_robot_state(RobotState.ERROR)
|
|
814
|
+
turn_result = turn_state.build_error_result(
|
|
815
|
+
result.get("error"),
|
|
816
|
+
elapsed=elapsed,
|
|
817
|
+
fallback_response=response_text,
|
|
818
|
+
token_count=token_count,
|
|
819
|
+
thinking_tokens=thinking_tokens,
|
|
820
|
+
)
|
|
821
|
+
self._last_turn_envelope = turn_result.to_envelope()
|
|
822
|
+
_turn_envelope = self._last_turn_envelope.to_dict()
|
|
823
|
+
if self.runtime_trace is not None:
|
|
824
|
+
try:
|
|
825
|
+
self.runtime_trace.add_turn_result(_turn_envelope)
|
|
826
|
+
except Exception:
|
|
827
|
+
pass
|
|
828
|
+
error_presentation = AgentErrorPresentation.from_error(
|
|
829
|
+
result.get("error", "Unknown error"),
|
|
830
|
+
lang=self.config.get("ui_lang", "en") or "en",
|
|
831
|
+
)
|
|
832
|
+
_offline_answer = (
|
|
833
|
+
_data_without_model(message, self.conversation[:-1])
|
|
834
|
+
if _model_unavailable(result.get("error")) else {}
|
|
835
|
+
)
|
|
836
|
+
if _offline_answer:
|
|
837
|
+
# The data is the answer; the model problem is a note under it.
|
|
838
|
+
_en_note = _detect_lang(message) == "en"
|
|
839
|
+
_answer = _offline_answer["response"]
|
|
840
|
+
if HAS_RICH:
|
|
841
|
+
console.print()
|
|
842
|
+
console.print(make_markdown(_strip_latex(_answer))) # its header carries the disclaimer
|
|
843
|
+
else:
|
|
844
|
+
print("\n" + _answer)
|
|
845
|
+
self.conversation.append({"role": "assistant", "content": _answer})
|
|
846
|
+
self._last_response = _answer
|
|
847
|
+
error_presentation = AgentErrorPresentation(
|
|
848
|
+
error=error_presentation.error,
|
|
849
|
+
level="warning",
|
|
850
|
+
lines=[
|
|
851
|
+
("模型不可用,以上为数据快照(未经模型分析)。" if not _en_note else
|
|
852
|
+
"The model is unavailable, so this is the data snapshot without model analysis."),
|
|
853
|
+
*error_presentation.lines[-1:],
|
|
854
|
+
],
|
|
855
|
+
)
|
|
856
|
+
console.print() if HAS_RICH else print()
|
|
857
|
+
# The user has now been told, in their language, what went
|
|
858
|
+
# wrong. A host that also receives the raw error text (the
|
|
859
|
+
# native front end gets it in turn.completed) must not show it
|
|
860
|
+
# a second time under a heading of its own.
|
|
861
|
+
self._error_presented = True
|
|
862
|
+
if error_presentation.use_generic_error_prefix:
|
|
863
|
+
_print_error(error_presentation.lines[0])
|
|
864
|
+
else:
|
|
865
|
+
_tone = "red" if error_presentation.level == "error" else "yellow"
|
|
866
|
+
for idx, ln in enumerate(error_presentation.lines):
|
|
867
|
+
if HAS_RICH:
|
|
868
|
+
from aria_code.ui.render.output import print_hanging
|
|
869
|
+
style = f"bold {_tone}" if idx == 0 and len(error_presentation.lines) > 1 else _tone
|
|
870
|
+
print_hanging(console, " ", ln, style)
|
|
871
|
+
else:
|
|
872
|
+
print(f" {ln}")
|
|
873
|
+
console.print() if HAS_RICH else print()
|
|
874
|
+
|
|
875
|
+
# --- Turn finished (runtime loop done) — render + record the outcome ---
|
|
876
|
+
_esc_watcher.stop()
|
|
877
|
+
self._streaming = False
|
|
878
|
+
if result.get("cancelled"):
|
|
879
|
+
set_robot_state(RobotState.IDLE)
|
|
880
|
+
elif result.get("success"):
|
|
881
|
+
set_robot_state(RobotState.DONE)
|
|
882
|
+
else:
|
|
883
|
+
set_robot_state(RobotState.ERROR)
|
|
884
|
+
elapsed = time.time() - start_time
|
|
885
|
+
|
|
886
|
+
# ── Unified cancellation path ──────────────────────────────────────────
|
|
887
|
+
# All cancel sources (model cancel, ESC between tools, KeyboardInterrupt)
|
|
888
|
+
# converge here via result["cancelled"]=True. A single AgentTurnResult
|
|
889
|
+
# carries partial text and timing so callers see a consistent shape.
|
|
890
|
+
if result.get("cancelled"):
|
|
891
|
+
stream_consumer.finish(TurnPhase.CANCELLED)
|
|
892
|
+
_stop_live()
|
|
893
|
+
turn_result = turn_state.build_cancelled_result(
|
|
894
|
+
elapsed=elapsed,
|
|
895
|
+
token_count=token_count,
|
|
896
|
+
thinking_tokens=thinking_tokens,
|
|
897
|
+
)
|
|
898
|
+
# Repetition protection is surfaced by providers as a cancelled
|
|
899
|
+
# turn. Sanitize the partial answer before rendering, persisting or
|
|
900
|
+
# feeding it back into the next context; otherwise the internal
|
|
901
|
+
# marker and an unfinished Markdown table leak into the conversation.
|
|
902
|
+
if stream_consumer.repetition_stopped or "*[model stopped — repetition detected]*" in (turn_result.final_text or ""):
|
|
903
|
+
from dataclasses import replace as _replace_turn_result
|
|
904
|
+
|
|
905
|
+
turn_result = _replace_turn_result(
|
|
906
|
+
turn_result,
|
|
907
|
+
final_text=_recover_repetition_stopped_text(turn_result.final_text),
|
|
908
|
+
)
|
|
909
|
+
self._last_turn_envelope = turn_result.to_envelope()
|
|
910
|
+
_turn_envelope = self._last_turn_envelope.to_dict()
|
|
911
|
+
if self.runtime_trace is not None:
|
|
912
|
+
try:
|
|
913
|
+
self.runtime_trace.add_turn_result(_turn_envelope)
|
|
914
|
+
except Exception:
|
|
915
|
+
pass
|
|
916
|
+
self._transition_runtime_run(
|
|
917
|
+
RunStatus.CANCELLED,
|
|
918
|
+
reason="user_or_provider_cancelled",
|
|
919
|
+
provider=turn_result.provider,
|
|
920
|
+
data={"elapsed_seconds": elapsed},
|
|
921
|
+
)
|
|
922
|
+
if HAS_RICH:
|
|
923
|
+
# Batch-render mode (Ollama): tokens were silently accumulated.
|
|
924
|
+
# Render whatever was generated before the cancel so the user
|
|
925
|
+
# can see partial output rather than a blank screen.
|
|
926
|
+
if turn_result.final_text and _use_batch_render[0]:
|
|
927
|
+
_stop_spinner()
|
|
928
|
+
console.print(make_markdown(_strip_latex(turn_result.final_text)))
|
|
929
|
+
_cancel_text = "已停止重复输出" if stream_consumer.repetition_stopped else "Cancelled"
|
|
930
|
+
console.print(f"\n[dim]{_cancel_text}[/dim]")
|
|
931
|
+
console.print(Rule(style="dim"))
|
|
932
|
+
else:
|
|
933
|
+
if turn_result.final_text:
|
|
934
|
+
print(turn_result.final_text)
|
|
935
|
+
print("\n (cancelled)")
|
|
936
|
+
if turn_result.final_text:
|
|
937
|
+
self.conversation.append(
|
|
938
|
+
{"role": "assistant", "content": turn_result.final_text}
|
|
939
|
+
)
|
|
940
|
+
_run_event_hook("response_done", {
|
|
941
|
+
"ARIA_RESPONSE": (turn_result.final_text or "")[:500],
|
|
942
|
+
"ARIA_PROVIDER": turn_result.provider,
|
|
943
|
+
"ARIA_TOKENS": str((token_count or 0)),
|
|
944
|
+
"ARIA_SESSION": self.session_id,
|
|
945
|
+
"ARIA_TURN_STATUS": _turn_envelope.get("status", ""),
|
|
946
|
+
"ARIA_TURN_SUMMARY": _turn_envelope.get("summary", "")[:500],
|
|
947
|
+
})
|
|
948
|
+
if _HAS_JSON_HOOKS:
|
|
949
|
+
try:
|
|
950
|
+
_fire_json_hook(
|
|
951
|
+
"ResponseDone",
|
|
952
|
+
response=(turn_result.final_text or "")[:500],
|
|
953
|
+
session_id=self.session_id,
|
|
954
|
+
turn=_turn_envelope,
|
|
955
|
+
hooks=_JSON_HOOKS,
|
|
956
|
+
)
|
|
957
|
+
except Exception:
|
|
958
|
+
pass
|
|
959
|
+
if self.config.get("auto_save_sessions") and self._jsonl_store is not None:
|
|
960
|
+
try:
|
|
961
|
+
if turn_result.final_text:
|
|
962
|
+
self._jsonl_store.append_message(self.session_id, "assistant", turn_result.final_text)
|
|
963
|
+
self._jsonl_store.flush_meta(
|
|
964
|
+
self.session_id,
|
|
965
|
+
extra={
|
|
966
|
+
"last_turn_status": self._last_turn_envelope.status,
|
|
967
|
+
"last_turn_provider": self._last_turn_envelope.provider,
|
|
968
|
+
"last_turn_summary": self._last_turn_envelope.summary,
|
|
969
|
+
},
|
|
970
|
+
)
|
|
971
|
+
except Exception:
|
|
972
|
+
pass
|
|
973
|
+
return
|
|
974
|
+
|
|
975
|
+
if result.get("success") and not result.get("cancelled"):
|
|
976
|
+
turn_result = turn_state.build_result(
|
|
977
|
+
elapsed=elapsed,
|
|
978
|
+
fallback_response=result.get("response", ""),
|
|
979
|
+
token_count=token_count,
|
|
980
|
+
thinking_tokens=thinking_tokens,
|
|
981
|
+
)
|
|
982
|
+
self._last_turn_envelope = turn_result.to_envelope()
|
|
983
|
+
_turn_envelope = self._last_turn_envelope.to_dict()
|
|
984
|
+
final_text = turn_result.final_text
|
|
985
|
+
|
|
986
|
+
if not (final_text or "").strip():
|
|
987
|
+
stream_consumer.finish(TurnPhase.ERROR)
|
|
988
|
+
set_robot_state(RobotState.ERROR)
|
|
989
|
+
empty_result = turn_state.build_error_result(
|
|
990
|
+
"empty_response",
|
|
991
|
+
elapsed=elapsed,
|
|
992
|
+
token_count=token_count,
|
|
993
|
+
thinking_tokens=thinking_tokens,
|
|
994
|
+
)
|
|
995
|
+
self._last_turn_envelope = empty_result.to_envelope()
|
|
996
|
+
if self.runtime_trace is not None:
|
|
997
|
+
try:
|
|
998
|
+
self.runtime_trace.add_turn_result(self._last_turn_envelope.to_dict())
|
|
999
|
+
except Exception:
|
|
1000
|
+
pass
|
|
1001
|
+
self._transition_runtime_run(
|
|
1002
|
+
RunStatus.FAILED,
|
|
1003
|
+
reason="response_validation_failed",
|
|
1004
|
+
error="empty_response",
|
|
1005
|
+
provider=empty_result.provider,
|
|
1006
|
+
data={"check": "non_empty_response"},
|
|
1007
|
+
)
|
|
1008
|
+
presentation = AgentErrorPresentation.from_error(
|
|
1009
|
+
"empty_response",
|
|
1010
|
+
lang=self.config.get("ui_lang", "en") or "en",
|
|
1011
|
+
)
|
|
1012
|
+
_tone = "red" if presentation.level == "error" else "yellow"
|
|
1013
|
+
for idx, line in enumerate(presentation.lines):
|
|
1014
|
+
if HAS_RICH:
|
|
1015
|
+
from aria_code.ui.render.output import print_hanging
|
|
1016
|
+
style = f"bold {_tone}" if idx == 0 else _tone
|
|
1017
|
+
print_hanging(console, " ", line, style)
|
|
1018
|
+
else:
|
|
1019
|
+
print(f" {line}")
|
|
1020
|
+
return
|
|
1021
|
+
|
|
1022
|
+
if self.runtime_trace is not None:
|
|
1023
|
+
try:
|
|
1024
|
+
self.runtime_trace.add_turn_result(_turn_envelope)
|
|
1025
|
+
except Exception:
|
|
1026
|
+
pass
|
|
1027
|
+
|
|
1028
|
+
self._transition_runtime_run(
|
|
1029
|
+
RunStatus.VERIFYING,
|
|
1030
|
+
reason="model_turn_completed",
|
|
1031
|
+
provider=turn_result.provider,
|
|
1032
|
+
data={
|
|
1033
|
+
"checks": ["provider_success", "non_empty_response"],
|
|
1034
|
+
"tools_used": turn_state.unique_tools(),
|
|
1035
|
+
},
|
|
1036
|
+
)
|
|
1037
|
+
self.runtime_trace.emit("verification_completed", {
|
|
1038
|
+
"passed": True,
|
|
1039
|
+
"checks": ["provider_success", "non_empty_response"],
|
|
1040
|
+
})
|
|
1041
|
+
|
|
1042
|
+
# Flush any unclosed LaTeX buffer (e.g. stream cut off mid-formula).
|
|
1043
|
+
# This only matters for the non-batch plain-print path; in batch-render
|
|
1044
|
+
# mode the full raw response is rendered below anyway.
|
|
1045
|
+
if _in_latex[0] and _latex_buf[0]:
|
|
1046
|
+
_leftover = _flush_latex_buf()
|
|
1047
|
+
final_text = (final_text or "") + _leftover
|
|
1048
|
+
if _use_plain_print[0] and not _use_batch_render[0]:
|
|
1049
|
+
print(_leftover, end="", flush=True)
|
|
1050
|
+
final_text = _recover_repetition_stopped_text(final_text)
|
|
1051
|
+
|
|
1052
|
+
# Stop progressive Live display (final state stays in terminal)
|
|
1053
|
+
_stop_live()
|
|
1054
|
+
|
|
1055
|
+
# ── Render final response ──────────────────────────────────────
|
|
1056
|
+
if _use_batch_render[0] and final_text:
|
|
1057
|
+
# Ollama batch-render: spinner was kept running during generation.
|
|
1058
|
+
# Stop it and render the COMPLETE response through Rich Markdown +
|
|
1059
|
+
# _strip_latex in one pass. This correctly handles:
|
|
1060
|
+
# • "$$" split across two single-"$" tokens (tokeniser-dependent)
|
|
1061
|
+
# • All LaTeX spacing commands (\; \, \quad etc.)
|
|
1062
|
+
# • Markdown headings, bold, tables
|
|
1063
|
+
_stop_spinner()
|
|
1064
|
+
stream_consumer.ensure_response_started()
|
|
1065
|
+
_render_answer_block(final_text)
|
|
1066
|
+
elif token_count == 0 and final_text:
|
|
1067
|
+
# Non-streamed response (e.g. complete() API path): render markdown.
|
|
1068
|
+
stream_consumer.ensure_response_started()
|
|
1069
|
+
_render_answer_block(final_text)
|
|
1070
|
+
|
|
1071
|
+
stream_consumer.finish(TurnPhase.DONE)
|
|
1072
|
+
|
|
1073
|
+
self.conversation.append({"role": "assistant", "content": final_text})
|
|
1074
|
+
import time as _time_ts
|
|
1075
|
+
self._last_turn_ts = _time_ts.time()
|
|
1076
|
+
|
|
1077
|
+
# ── 预测反馈记录:为本轮检测到的标的写入 DPO 训练素材 ──────────────
|
|
1078
|
+
if _ml_signal_syms and final_text:
|
|
1079
|
+
for _sym in _ml_signal_syms:
|
|
1080
|
+
self._record_prediction(_sym, final_text)
|
|
1081
|
+
|
|
1082
|
+
# The runtime's delivery report: what changed, what was verified,
|
|
1083
|
+
# risk, checkpoints — from evidence, not from the model's summary.
|
|
1084
|
+
_delivery = result.get("delivery") if isinstance(result, dict) else None
|
|
1085
|
+
if _delivery and self.config.get("delivery_report", True):
|
|
1086
|
+
try:
|
|
1087
|
+
from ui.render.output import print_delivery_report
|
|
1088
|
+
print_delivery_report(
|
|
1089
|
+
console if HAS_RICH else None,
|
|
1090
|
+
_delivery,
|
|
1091
|
+
run_id=str(result.get("run_id") or ""),
|
|
1092
|
+
root=_task_context.get("_workspace") or os.getcwd(),
|
|
1093
|
+
)
|
|
1094
|
+
except Exception as _delivery_err:
|
|
1095
|
+
logger.debug("delivery report not shown: %s", _delivery_err)
|
|
1096
|
+
|
|
1097
|
+
# A turn that changed files in its task worktree: apply, keep, discard.
|
|
1098
|
+
await self._offer_task_apply(_task, _task_note)
|
|
1099
|
+
|
|
1100
|
+
# Whether the code now passes its checks — what the next
|
|
1101
|
+
# transaction point records, and what /rewind green looks for.
|
|
1102
|
+
from aria_code.runtime.transactions import verdict_from_acceptance
|
|
1103
|
+
self._code_verdict = verdict_from_acceptance(
|
|
1104
|
+
result.get("acceptance") if isinstance(result, dict) else None,
|
|
1105
|
+
getattr(self, "_code_verdict", ""),
|
|
1106
|
+
)
|
|
1107
|
+
|
|
1108
|
+
# Metadata line — detailed stats
|
|
1109
|
+
metadata = turn_result.metadata
|
|
1110
|
+
prompt_t = metadata.prompt_tokens
|
|
1111
|
+
completion_t = metadata.completion_tokens
|
|
1112
|
+
think_t = metadata.thinking_tokens
|
|
1113
|
+
self._last_response = final_text # for /copy
|
|
1114
|
+
_context_compacted_from_usage = False
|
|
1115
|
+
|
|
1116
|
+
_ctx_max = get_model_cfg(self.config.get("model", "qwen2.5:7b")).get("num_ctx", 16384)
|
|
1117
|
+
if HAS_RICH:
|
|
1118
|
+
from ui.render.output import format_turn_footer as _format_turn_footer
|
|
1119
|
+
_footer = _format_turn_footer(
|
|
1120
|
+
metadata,
|
|
1121
|
+
mode="off" if getattr(self, "_native_frontend", False) else self.config.get("response_footer", "compact"),
|
|
1122
|
+
copy_available=bool(final_text),
|
|
1123
|
+
)
|
|
1124
|
+
if _footer:
|
|
1125
|
+
console.print(f"\n[dim]{_footer}[/dim]")
|
|
1126
|
+
else:
|
|
1127
|
+
from ui.render.output import format_turn_footer as _format_turn_footer
|
|
1128
|
+
_footer = _format_turn_footer(
|
|
1129
|
+
metadata,
|
|
1130
|
+
mode="off" if getattr(self, "_native_frontend", False) else self.config.get("response_footer", "compact"),
|
|
1131
|
+
copy_available=bool(final_text),
|
|
1132
|
+
)
|
|
1133
|
+
if _footer:
|
|
1134
|
+
print(f"\n{_footer}\n")
|
|
1135
|
+
|
|
1136
|
+
# Context pressure: if the real provider prompt is already hot,
|
|
1137
|
+
# compact immediately after this turn. This catches cases where the
|
|
1138
|
+
# provider count includes large system/tool context that the local
|
|
1139
|
+
# char estimate misses.
|
|
1140
|
+
if prompt_t > 0 and _ctx_max > 0:
|
|
1141
|
+
_ctx_fill_pct = prompt_t / _ctx_max
|
|
1142
|
+
try:
|
|
1143
|
+
_compact_threshold = float(self.config.get("auto_compact_threshold", 0.78))
|
|
1144
|
+
except Exception:
|
|
1145
|
+
_compact_threshold = 0.78
|
|
1146
|
+
_compact_threshold = max(0.50, min(0.95, _compact_threshold))
|
|
1147
|
+
if bool(self.config.get("auto_compact_context", True)) and _ctx_fill_pct >= _compact_threshold:
|
|
1148
|
+
_old_pct = int(_ctx_fill_pct * 100)
|
|
1149
|
+
try:
|
|
1150
|
+
await self.commands._smart_compact_async(silent=True)
|
|
1151
|
+
except Exception:
|
|
1152
|
+
try:
|
|
1153
|
+
self.conversation = _compact_messages(
|
|
1154
|
+
self.conversation,
|
|
1155
|
+
model_key=self.config.get("model", "qwen2.5:7b"),
|
|
1156
|
+
)
|
|
1157
|
+
except Exception:
|
|
1158
|
+
if len(self.conversation) > 10:
|
|
1159
|
+
self.conversation = self.conversation[-10:]
|
|
1160
|
+
self._auto_compact_count += 1
|
|
1161
|
+
_context_compacted_from_usage = True
|
|
1162
|
+
if HAS_RICH:
|
|
1163
|
+
console.print(f" [dim]↩ Auto-compacted context after response ({_old_pct}% full)[/dim]")
|
|
1164
|
+
elif _ctx_fill_pct >= 0.85:
|
|
1165
|
+
from ui.render.output import print_context_warning as _print_context_warning
|
|
1166
|
+
_print_context_warning(
|
|
1167
|
+
prompt_t,
|
|
1168
|
+
_ctx_max,
|
|
1169
|
+
console=console,
|
|
1170
|
+
has_rich=HAS_RICH,
|
|
1171
|
+
session_id=self.session_id,
|
|
1172
|
+
)
|
|
1173
|
+
elif _ctx_fill_pct >= 0.70 and HAS_RICH:
|
|
1174
|
+
_ctx_color = "#aa8800"
|
|
1175
|
+
console.print(
|
|
1176
|
+
f"[{_ctx_color}] ⚠ 上下文 {int(_ctx_fill_pct * 100)}% 已用,"
|
|
1177
|
+
f"将按阈值 {int(_compact_threshold * 100)}% 自动压缩。[/{_ctx_color}]"
|
|
1178
|
+
)
|
|
1179
|
+
|
|
1180
|
+
# ── Accumulate session-level usage stats (for /cost) ──────────
|
|
1181
|
+
self._session_input_tokens += prompt_t or 0
|
|
1182
|
+
self._session_output_tokens += completion_t or 0
|
|
1183
|
+
self._session_thinking_tokens += think_t or 0
|
|
1184
|
+
self._session_turns += 1
|
|
1185
|
+
|
|
1186
|
+
# Fire response_done lifecycle hooks (shell + JSON)
|
|
1187
|
+
_turn_envelope = self._last_turn_envelope.to_dict() if self._last_turn_envelope else {}
|
|
1188
|
+
_run_event_hook("response_done", {
|
|
1189
|
+
"ARIA_RESPONSE": (final_text or "")[:500],
|
|
1190
|
+
"ARIA_PROVIDER": turn_result.provider,
|
|
1191
|
+
"ARIA_TOKENS": str((prompt_t or 0) + (completion_t or 0)),
|
|
1192
|
+
"ARIA_SESSION": self.session_id,
|
|
1193
|
+
"ARIA_TURN_STATUS": _turn_envelope.get("status", ""),
|
|
1194
|
+
"ARIA_TURN_SUMMARY": _turn_envelope.get("summary", "")[:500],
|
|
1195
|
+
})
|
|
1196
|
+
if _HAS_JSON_HOOKS:
|
|
1197
|
+
try:
|
|
1198
|
+
_fire_json_hook(
|
|
1199
|
+
"ResponseDone",
|
|
1200
|
+
response=(final_text or "")[:500],
|
|
1201
|
+
session_id=self.session_id,
|
|
1202
|
+
turn=_turn_envelope,
|
|
1203
|
+
hooks=_JSON_HOOKS,
|
|
1204
|
+
)
|
|
1205
|
+
except Exception:
|
|
1206
|
+
pass
|
|
1207
|
+
|
|
1208
|
+
# Auto-capture user preferences / facts expressed in this turn
|
|
1209
|
+
try:
|
|
1210
|
+
from memory_manager import auto_capture_from_turn as _acft, MemoryManager as _MM
|
|
1211
|
+
if account_id(self.config) == _memory_owner:
|
|
1212
|
+
_acft(message, final_text or "", _MM(config=self.config))
|
|
1213
|
+
except Exception:
|
|
1214
|
+
pass
|
|
1215
|
+
|
|
1216
|
+
# Trim conversation history to prevent unbounded growth
|
|
1217
|
+
if len(self.conversation) > 40:
|
|
1218
|
+
self.conversation = self.conversation[-40:]
|
|
1219
|
+
|
|
1220
|
+
# Auto-warn when context approaches the limit; auto-compact before
|
|
1221
|
+
# the prompt is already at the edge and tool traces become noisy.
|
|
1222
|
+
# The decision is in apps/cli/turn_planning.py, which is also where
|
|
1223
|
+
# the other two compaction paths' rules are documented: this one
|
|
1224
|
+
# used to ignore auto_compact_context and auto_compact_threshold
|
|
1225
|
+
# entirely, so turning auto-compaction off did not turn it off.
|
|
1226
|
+
_est = estimate_context_tokens(self.conversation)
|
|
1227
|
+
_max = get_model_cfg(self.config.get("model", "qwen2.5:7b")).get("num_ctx", 16384)
|
|
1228
|
+
_ctx = post_turn_context_decision(
|
|
1229
|
+
_est, _max,
|
|
1230
|
+
auto_compact_enabled=bool(self.config.get("auto_compact_context", True)),
|
|
1231
|
+
threshold=self.config.get("auto_compact_threshold", 0.78),
|
|
1232
|
+
already_compacted=_context_compacted_from_usage,
|
|
1233
|
+
)
|
|
1234
|
+
_pct = _ctx["fill_pct"]
|
|
1235
|
+
if _ctx["should_compact"]:
|
|
1236
|
+
# Auto-compact: silently summarise and truncate
|
|
1237
|
+
try:
|
|
1238
|
+
await self.commands._smart_compact_async(silent=True)
|
|
1239
|
+
except Exception:
|
|
1240
|
+
# Fallback: hard trim
|
|
1241
|
+
self.conversation = self.conversation[-10:]
|
|
1242
|
+
if HAS_RICH:
|
|
1243
|
+
console.print(f" [dim]↩ Auto-compacted context (was {_pct}% full)[/dim]")
|
|
1244
|
+
elif _ctx["should_warn"] and HAS_RICH:
|
|
1245
|
+
_color = "yellow" if _pct < 85 else "red"
|
|
1246
|
+
console.print(
|
|
1247
|
+
f" [{_color}]⚠ Context {_pct}% full "
|
|
1248
|
+
f"({_est:,}/{_max:,} tokens) — /compact to free space[/{_color}]"
|
|
1249
|
+
)
|
|
1250
|
+
|
|
1251
|
+
# Auto-save session (JSON + JSONL dual write)
|
|
1252
|
+
if self.config.get("auto_save_sessions"):
|
|
1253
|
+
try:
|
|
1254
|
+
self.session_mgr.save_session(self.session_id, self.conversation)
|
|
1255
|
+
except Exception:
|
|
1256
|
+
pass
|
|
1257
|
+
# JSONL: append only the two new messages (user + assistant) for crash safety
|
|
1258
|
+
if self._jsonl_store is not None:
|
|
1259
|
+
try:
|
|
1260
|
+
self._jsonl_store.append_message(self.session_id, "user", message)
|
|
1261
|
+
if final_text:
|
|
1262
|
+
self._jsonl_store.append_message(self.session_id, "assistant", final_text)
|
|
1263
|
+
if self._last_turn_envelope is not None:
|
|
1264
|
+
self._jsonl_store.flush_meta(
|
|
1265
|
+
self.session_id,
|
|
1266
|
+
extra={
|
|
1267
|
+
"last_turn_status": self._last_turn_envelope.status,
|
|
1268
|
+
"last_turn_provider": self._last_turn_envelope.provider,
|
|
1269
|
+
"last_turn_summary": self._last_turn_envelope.summary,
|
|
1270
|
+
},
|
|
1271
|
+
)
|
|
1272
|
+
except Exception:
|
|
1273
|
+
pass
|
|
1274
|
+
|
|
1275
|
+
# Auto-extract preference signals into global memory
|
|
1276
|
+
if self.memory_mgr and final_text and account_id(self.config) == _memory_owner:
|
|
1277
|
+
try:
|
|
1278
|
+
from memory_manager import extract_preference_signal
|
|
1279
|
+
_sig = extract_preference_signal(message, final_text)
|
|
1280
|
+
if _sig:
|
|
1281
|
+
self.memory_mgr.append("user_profile", _sig, title="User Profile")
|
|
1282
|
+
except Exception:
|
|
1283
|
+
pass
|
|
1284
|
+
|
|
1285
|
+
self._transition_runtime_run(
|
|
1286
|
+
RunStatus.SUCCEEDED,
|
|
1287
|
+
reason="turn_completed",
|
|
1288
|
+
provider=turn_result.provider,
|
|
1289
|
+
data={
|
|
1290
|
+
"elapsed_seconds": elapsed,
|
|
1291
|
+
"prompt_tokens": prompt_t,
|
|
1292
|
+
"completion_tokens": completion_t,
|
|
1293
|
+
"thinking_tokens": think_t,
|
|
1294
|
+
"tools_used": turn_state.unique_tools(),
|
|
1295
|
+
},
|
|
1296
|
+
)
|
|
1297
|
+
|
|
1298
|
+
if not result.get("success") and not result.get("cancelled"):
|
|
1299
|
+
self._transition_runtime_run(
|
|
1300
|
+
RunStatus.FAILED,
|
|
1301
|
+
reason="provider_or_agent_error",
|
|
1302
|
+
error=str(result.get("error") or "unknown_error"),
|
|
1303
|
+
provider=str(result.get("provider") or provider),
|
|
1304
|
+
data={"elapsed_seconds": elapsed},
|
|
1305
|
+
)
|
|
1306
|
+
|
|
1307
|
+
async def _offer_task_apply(self, task, say) -> None:
|
|
1308
|
+
"""Ask whether a task's changes should reach the user's files."""
|
|
1309
|
+
if task is None:
|
|
1310
|
+
return
|
|
1311
|
+
from apps.cli import task_isolation
|
|
1312
|
+
|
|
1313
|
+
def _choose(options, title):
|
|
1314
|
+
return _arrow_select(options, selected=0, title=title,
|
|
1315
|
+
shortcuts={"a": 0, "k": 1, "d": 2},
|
|
1316
|
+
collapse_to="task")
|
|
1317
|
+
|
|
1318
|
+
loop = asyncio.get_running_loop()
|
|
1319
|
+
try:
|
|
1320
|
+
await loop.run_in_executor(None, lambda: task_isolation.offer(
|
|
1321
|
+
task, choose=_choose, say=say, session_id=self.session_id))
|
|
1322
|
+
except Exception as exc:
|
|
1323
|
+
logger.debug("task apply offer failed: %s", exc)
|
|
1324
|
+
say(f"Task {task.task_id} kept; /task apply or /task discard.")
|
|
1325
|
+
|
|
1326
|
+
|
|
1327
|
+
__all__ = ["ChatTurnMixin"]
|