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.
Files changed (601) hide show
  1. aria_bots/__init__.py +7 -0
  2. aria_bots/channels/README.md +97 -0
  3. aria_bots/channels/__init__.py +1 -0
  4. aria_bots/channels/approvals.py +263 -0
  5. aria_bots/channels/conversation.py +280 -0
  6. aria_bots/channels/digest.py +186 -0
  7. aria_bots/channels/registry.py +95 -0
  8. aria_bots/engine.py +144 -0
  9. aria_bots/feishu_bot.py +1385 -0
  10. aria_bots/logistics/__init__.py +1 -0
  11. aria_bots/logistics/carriers.py +253 -0
  12. aria_bots/logistics/freight.py +154 -0
  13. aria_bots/logistics/inventory.py +242 -0
  14. aria_bots/logistics/mcp_server.py +156 -0
  15. aria_bots/logistics/records.py +119 -0
  16. aria_bots/logistics/tenancy.py +152 -0
  17. aria_bots/market.py +91 -0
  18. aria_bots/relay_client.py +326 -0
  19. aria_bots/telegram_bot.py +299 -0
  20. aria_code/__init__.py +111 -0
  21. aria_code/_bin/aria-native.exe +0 -0
  22. aria_code/_bin/aria-runtime.exe +0 -0
  23. aria_code/_version.py +20 -0
  24. aria_code/account_memory.py +296 -0
  25. aria_code/account_session.py +166 -0
  26. aria_code/adk_apps/__init__.py +1 -0
  27. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  28. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  29. aria_code/adk_apps/aria_research/__init__.py +1 -0
  30. aria_code/adk_apps/aria_research/agent.py +31 -0
  31. aria_code/agents/__init__.py +49 -0
  32. aria_code/agents/base.py +329 -0
  33. aria_code/agents/code_review.py +200 -0
  34. aria_code/agents/deep/__init__.py +37 -0
  35. aria_code/agents/deep/calibration_loop.py +144 -0
  36. aria_code/agents/deep/critic.py +125 -0
  37. aria_code/agents/deep/deepen.py +193 -0
  38. aria_code/agents/deep/models.py +149 -0
  39. aria_code/agents/deep/pipeline.py +164 -0
  40. aria_code/agents/deep/quant_fusion.py +192 -0
  41. aria_code/agents/deep/themes.py +95 -0
  42. aria_code/agents/deep/tiers.py +106 -0
  43. aria_code/agents/engineering/__init__.py +14 -0
  44. aria_code/agents/engineering/coder.py +222 -0
  45. aria_code/agents/engineering/tester.py +145 -0
  46. aria_code/agents/financial/__init__.py +21 -0
  47. aria_code/agents/financial/cashflow_burn.py +119 -0
  48. aria_code/agents/financial/catalyst.py +279 -0
  49. aria_code/agents/financial/corporate_finance.py +175 -0
  50. aria_code/agents/financial/debate.py +224 -0
  51. aria_code/agents/financial/earnings.py +315 -0
  52. aria_code/agents/financial/fundamental.py +178 -0
  53. aria_code/agents/financial/macro.py +111 -0
  54. aria_code/agents/financial/news.py +231 -0
  55. aria_code/agents/financial/risk.py +146 -0
  56. aria_code/agents/financial/sector.py +279 -0
  57. aria_code/agents/financial/strategist.py +121 -0
  58. aria_code/agents/financial/stripe_revenue.py +127 -0
  59. aria_code/agents/financial/synthesis.py +274 -0
  60. aria_code/agents/financial/technical.py +277 -0
  61. aria_code/agents/orchestrator.py +44 -0
  62. aria_code/agents/portfolio_agent.py +392 -0
  63. aria_code/agents/realty/__init__.py +62 -0
  64. aria_code/agents/realty/asset_diagnosis.py +152 -0
  65. aria_code/agents/realty/business_match.py +166 -0
  66. aria_code/agents/realty/cashflow_verify.py +209 -0
  67. aria_code/agents/realty/contract_rules.py +210 -0
  68. aria_code/agents/realty/energy_anomaly.py +189 -0
  69. aria_code/agents/realty/exit_settlement.py +208 -0
  70. aria_code/agents/realty/fulfillment_risk.py +206 -0
  71. aria_code/agents/realty/ops_optimize.py +160 -0
  72. aria_code/agents/realty/revenue_share.py +215 -0
  73. aria_code/agents/registry.py +168 -0
  74. aria_code/agents/signal_scheme.py +111 -0
  75. aria_code/agents/sports/__init__.py +0 -0
  76. aria_code/agents/sports/football_agent.py +197 -0
  77. aria_code/agents/supervisor.py +113 -0
  78. aria_code/agents/team.py +500 -0
  79. aria_code/agents/video/__init__.py +16 -0
  80. aria_code/agents/video/director.py +376 -0
  81. aria_code/agents/video/qa_agent.py +143 -0
  82. aria_code/agents/video/script_doctor.py +112 -0
  83. aria_code/agents/warehouse/__init__.py +25 -0
  84. aria_code/agents/warehouse/contracts.py +26 -0
  85. aria_code/agents/warehouse/dashboard.py +155 -0
  86. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  87. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  88. aria_code/agents/warehouse/inventory_health.py +24 -0
  89. aria_code/agents/warehouse/local_run.py +71 -0
  90. aria_code/agents/warehouse/local_snapshot.py +132 -0
  91. aria_code/agents/warehouse/logistics_cost.py +84 -0
  92. aria_code/agents/warehouse/logistics_sync.py +21 -0
  93. aria_code/agents/warehouse/navigation.schema.json +110 -0
  94. aria_code/agents/warehouse/task_agent.py +172 -0
  95. aria_code/agents/warehouse/workflow.py +31 -0
  96. aria_code/aliyun_data_client.py +4 -0
  97. aria_code/apps/README.md +12 -0
  98. aria_code/apps/__init__.py +2 -0
  99. aria_code/apps/channels/README.md +9 -0
  100. aria_code/apps/channels/__init__.py +17 -0
  101. aria_code/apps/channels/approvals.py +11 -0
  102. aria_code/apps/channels/conversation.py +11 -0
  103. aria_code/apps/channels/digest.py +11 -0
  104. aria_code/apps/channels/intake.py +92 -0
  105. aria_code/apps/channels/registry.py +11 -0
  106. aria_code/apps/channels/tradingview.py +94 -0
  107. aria_code/apps/cli/README.md +13 -0
  108. aria_code/apps/cli/__init__.py +2 -0
  109. aria_code/apps/cli/_optional.py +120 -0
  110. aria_code/apps/cli/app_server.py +656 -0
  111. aria_code/apps/cli/auto_update.py +125 -0
  112. aria_code/apps/cli/bootstrap.py +206 -0
  113. aria_code/apps/cli/broker_render.py +206 -0
  114. aria_code/apps/cli/chat_turn.py +1327 -0
  115. aria_code/apps/cli/cloud_models.py +231 -0
  116. aria_code/apps/cli/codegen_paths.py +29 -0
  117. aria_code/apps/cli/commands/__init__.py +34 -0
  118. aria_code/apps/cli/commands/_business_render.py +78 -0
  119. aria_code/apps/cli/commands/_ui.py +71 -0
  120. aria_code/apps/cli/commands/analysis_cmds.py +378 -0
  121. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  122. aria_code/apps/cli/commands/auth_cmds.py +215 -0
  123. aria_code/apps/cli/commands/backtest_cmds.py +2518 -0
  124. aria_code/apps/cli/commands/broker_cmds.py +1236 -0
  125. aria_code/apps/cli/commands/business_workflow_cmds.py +379 -0
  126. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  127. aria_code/apps/cli/commands/catalog.py +189 -0
  128. aria_code/apps/cli/commands/core_cmds.py +2748 -0
  129. aria_code/apps/cli/commands/data_cmds.py +521 -0
  130. aria_code/apps/cli/commands/diagnostic_cmds.py +285 -0
  131. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  132. aria_code/apps/cli/commands/engineering_cmds.py +399 -0
  133. aria_code/apps/cli/commands/file_cmds.py +312 -0
  134. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  135. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  136. aria_code/apps/cli/commands/market.py +544 -0
  137. aria_code/apps/cli/commands/market_cmds.py +1419 -0
  138. aria_code/apps/cli/commands/market_context.py +432 -0
  139. aria_code/apps/cli/commands/market_render.py +7 -0
  140. aria_code/apps/cli/commands/model_cmds.py +2374 -0
  141. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  142. aria_code/apps/cli/commands/orchestrator_cmds.py +181 -0
  143. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  144. aria_code/apps/cli/commands/portfolio_cmds.py +1316 -0
  145. aria_code/apps/cli/commands/report.py +476 -0
  146. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  147. aria_code/apps/cli/commands/session_cmds.py +236 -0
  148. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  149. aria_code/apps/cli/commands/team.py +723 -0
  150. aria_code/apps/cli/commands/team_render.py +8 -0
  151. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  152. aria_code/apps/cli/commands/warehouse_cmds.py +297 -0
  153. aria_code/apps/cli/commands/workflow_cmds.py +629 -0
  154. aria_code/apps/cli/commands/workspace_cmds.py +1543 -0
  155. aria_code/apps/cli/config_paths.py +73 -0
  156. aria_code/apps/cli/config_store.py +158 -0
  157. aria_code/apps/cli/constants.py +88 -0
  158. aria_code/apps/cli/context.py +34 -0
  159. aria_code/apps/cli/conversation_policy.py +53 -0
  160. aria_code/apps/cli/deterministic.py +200 -0
  161. aria_code/apps/cli/direct.py +48 -0
  162. aria_code/apps/cli/earnings_view.py +172 -0
  163. aria_code/apps/cli/endpoint_migration.py +64 -0
  164. aria_code/apps/cli/exec_events.py +255 -0
  165. aria_code/apps/cli/file_references.py +155 -0
  166. aria_code/apps/cli/finance_service_catalog.py +111 -0
  167. aria_code/apps/cli/football_reports.py +231 -0
  168. aria_code/apps/cli/github_app_auth.py +135 -0
  169. aria_code/apps/cli/google_login.py +239 -0
  170. aria_code/apps/cli/handlers/__init__.py +11 -0
  171. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  172. aria_code/apps/cli/handlers/chart_handlers.py +1314 -0
  173. aria_code/apps/cli/handlers/market_handlers.py +2570 -0
  174. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  175. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  176. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  177. aria_code/apps/cli/headless.py +352 -0
  178. aria_code/apps/cli/helpers.py +326 -0
  179. aria_code/apps/cli/hooks.py +180 -0
  180. aria_code/apps/cli/i18n.py +310 -0
  181. aria_code/apps/cli/intent.py +136 -0
  182. aria_code/apps/cli/intent_router.py +305 -0
  183. aria_code/apps/cli/intent_shadow.py +72 -0
  184. aria_code/apps/cli/intent_signals.py +347 -0
  185. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  186. aria_code/apps/cli/local_tool_schemas.py +550 -0
  187. aria_code/apps/cli/main.py +39 -0
  188. aria_code/apps/cli/market_hours.py +100 -0
  189. aria_code/apps/cli/market_metadata.py +135 -0
  190. aria_code/apps/cli/market_universe.py +487 -0
  191. aria_code/apps/cli/message_processing.py +310 -0
  192. aria_code/apps/cli/mixin_binding.py +32 -0
  193. aria_code/apps/cli/model_catalog.py +652 -0
  194. aria_code/apps/cli/model_probe.py +169 -0
  195. aria_code/apps/cli/model_retirement.py +65 -0
  196. aria_code/apps/cli/native_branding.py +12 -0
  197. aria_code/apps/cli/native_bridge.py +118 -0
  198. aria_code/apps/cli/native_entry.py +58 -0
  199. aria_code/apps/cli/pdf_report.py +977 -0
  200. aria_code/apps/cli/plan_mode.py +139 -0
  201. aria_code/apps/cli/plotly_html.py +15 -0
  202. aria_code/apps/cli/prediction_feedback.py +202 -0
  203. aria_code/apps/cli/preflight.py +502 -0
  204. aria_code/apps/cli/project_aria.py +60 -0
  205. aria_code/apps/cli/prompt_assembly.py +78 -0
  206. aria_code/apps/cli/prompts/__init__.py +0 -0
  207. aria_code/apps/cli/prompts/coding.py +71 -0
  208. aria_code/apps/cli/prompts/select.py +193 -0
  209. aria_code/apps/cli/prompts/system_prompts.py +562 -0
  210. aria_code/apps/cli/prompts/ui.py +436 -0
  211. aria_code/apps/cli/provider_endpoints.py +237 -0
  212. aria_code/apps/cli/provider_keys.py +73 -0
  213. aria_code/apps/cli/providers/__init__.py +1 -0
  214. aria_code/apps/cli/providers/base.py +775 -0
  215. aria_code/apps/cli/providers/chat_routing.py +139 -0
  216. aria_code/apps/cli/providers/collaboration.py +98 -0
  217. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  218. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  219. aria_code/apps/cli/providers/llm/ollama_stream.py +1574 -0
  220. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  221. aria_code/apps/cli/providers/llm/sse_stream.py +298 -0
  222. aria_code/apps/cli/providers/runtime_bridge.py +586 -0
  223. aria_code/apps/cli/providers/vertexai_stream.py +592 -0
  224. aria_code/apps/cli/release_download.py +159 -0
  225. aria_code/apps/cli/review_runner.py +111 -0
  226. aria_code/apps/cli/runtime_consumer.py +769 -0
  227. aria_code/apps/cli/sandbox.py +32 -0
  228. aria_code/apps/cli/session_export.py +120 -0
  229. aria_code/apps/cli/session_jsonl.py +208 -0
  230. aria_code/apps/cli/session_refresh.py +123 -0
  231. aria_code/apps/cli/session_store.py +125 -0
  232. aria_code/apps/cli/skills_catalog.py +273 -0
  233. aria_code/apps/cli/task_isolation.py +211 -0
  234. aria_code/apps/cli/terminal_chrome.py +412 -0
  235. aria_code/apps/cli/todo_tracker.py +190 -0
  236. aria_code/apps/cli/tool_executor.py +932 -0
  237. aria_code/apps/cli/tool_registry.py +34 -0
  238. aria_code/apps/cli/tool_scope.py +113 -0
  239. aria_code/apps/cli/tools/__init__.py +40 -0
  240. aria_code/apps/cli/tools/context.py +46 -0
  241. aria_code/apps/cli/tools/file_tools.py +181 -0
  242. aria_code/apps/cli/tools/market_tools.py +626 -0
  243. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  244. aria_code/apps/cli/tools/system_tools.py +656 -0
  245. aria_code/apps/cli/tools/write_tools.py +961 -0
  246. aria_code/apps/cli/tradingview_bridge.py +529 -0
  247. aria_code/apps/cli/transactions.py +269 -0
  248. aria_code/apps/cli/turn_planning.py +137 -0
  249. aria_code/apps/cli/update_check.py +257 -0
  250. aria_code/apps/cli/updater.py +373 -0
  251. aria_code/apps/cli/utils/__init__.py +0 -0
  252. aria_code/apps/cli/utils/market_detect.py +1663 -0
  253. aria_code/apps/cli/web_login.py +136 -0
  254. aria_code/apps/cli/workflow_runner.py +127 -0
  255. aria_code/apps/cli/workspace_route.py +121 -0
  256. aria_code/apps/daemon/README.md +14 -0
  257. aria_code/apps/vscode/README.md +115 -0
  258. aria_code/apps/vscode/package.json +70 -0
  259. aria_code/aria_cli.py +5244 -0
  260. aria_code/aria_daemon.py +1372 -0
  261. aria_code/aria_feishu_bot.py +16 -0
  262. aria_code/aria_mcp_server.py +61 -0
  263. aria_code/aria_relay_client.py +4 -0
  264. aria_code/aria_telegram_bot.py +11 -0
  265. aria_code/ariarc.py +385 -0
  266. aria_code/artifacts.py +581 -0
  267. aria_code/backtest_engine.py +4 -0
  268. aria_code/backtest_report.py +4 -0
  269. aria_code/brokers/__init__.py +78 -0
  270. aria_code/brokers/automation.py +193 -0
  271. aria_code/brokers/base.py +231 -0
  272. aria_code/brokers/capabilities.py +264 -0
  273. aria_code/brokers/cn/__init__.py +10 -0
  274. aria_code/brokers/cn/easytrader_broker.py +193 -0
  275. aria_code/brokers/cn/futu_broker.py +194 -0
  276. aria_code/brokers/cn/longbridge_broker.py +190 -0
  277. aria_code/brokers/cn/tiger_broker.py +196 -0
  278. aria_code/brokers/cn/xtquant_broker.py +225 -0
  279. aria_code/brokers/config.py +414 -0
  280. aria_code/brokers/intl/__init__.py +5 -0
  281. aria_code/brokers/intl/alpaca_broker.py +183 -0
  282. aria_code/brokers/intl/ibkr_broker.py +215 -0
  283. aria_code/brokers/intl/trading212_broker.py +183 -0
  284. aria_code/brokers/intl/webull_broker.py +156 -0
  285. aria_code/brokers/paper_broker.py +266 -0
  286. aria_code/brokers/planning.py +296 -0
  287. aria_code/brokers/registry.py +213 -0
  288. aria_code/brokers/trading.py +267 -0
  289. aria_code/canva_client.py +4 -0
  290. aria_code/change_store.py +127 -0
  291. aria_code/clients/__init__.py +0 -0
  292. aria_code/clients/aliyun_data_client.py +660 -0
  293. aria_code/clients/aria_relay_client.py +16 -0
  294. aria_code/clients/canva_client.py +284 -0
  295. aria_code/clients/figma_client.py +100 -0
  296. aria_code/clients/football_data_client.py +1671 -0
  297. aria_code/clients/kling_video_client.py +182 -0
  298. aria_code/clients/market_data_client.py +1985 -0
  299. aria_code/clients/openai_image_client.py +242 -0
  300. aria_code/clients/runway_video_client.py +157 -0
  301. aria_code/clients/warehouse_erp_client.py +105 -0
  302. aria_code/cloud_memory.py +190 -0
  303. aria_code/command_safety.py +19 -0
  304. aria_code/computer_use_server.py +276 -0
  305. aria_code/computer_use_tools.py +4 -0
  306. aria_code/dashboard_generator.py +579 -0
  307. aria_code/data_analysis_tools.py +4 -0
  308. aria_code/data_cleaner.py +483 -0
  309. aria_code/data_service.py +513 -0
  310. aria_code/datasources/__init__.py +23 -0
  311. aria_code/datasources/base.py +166 -0
  312. aria_code/datasources/router.py +253 -0
  313. aria_code/datasources/sec_statements.py +217 -0
  314. aria_code/datasources/sources/__init__.py +15 -0
  315. aria_code/datasources/sources/akshare_source.py +269 -0
  316. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  317. aria_code/datasources/sources/edgar_source.py +219 -0
  318. aria_code/datasources/sources/finnhub_source.py +198 -0
  319. aria_code/datasources/sources/fred_source.py +223 -0
  320. aria_code/datasources/sources/tushare_source.py +143 -0
  321. aria_code/datasources/sources/web_scraper_source.py +278 -0
  322. aria_code/datasources/sources/world_bank_source.py +206 -0
  323. aria_code/datasources/sources/yfinance_source.py +152 -0
  324. aria_code/doctor.py +860 -0
  325. aria_code/domain/__init__.py +0 -0
  326. aria_code/domain/_finance_core/__init__.py +5 -0
  327. aria_code/domain/_finance_core/backtest_engine.py +325 -0
  328. aria_code/domain/_finance_core/performance_stats.py +101 -0
  329. aria_code/domain/backtest_engine.py +75 -0
  330. aria_code/domain/backtest_report.py +488 -0
  331. aria_code/domain/finance_formulas.py +389 -0
  332. aria_code/domain/performance_stats.py +6 -0
  333. aria_code/domain/portfolio_ledger.py +307 -0
  334. aria_code/domain/strategy_vault.py +421 -0
  335. aria_code/evals/__init__.py +37 -0
  336. aria_code/evals/behavior.py +113 -0
  337. aria_code/evals/gap.py +93 -0
  338. aria_code/evals/harness.py +949 -0
  339. aria_code/evals/review_probe.py +132 -0
  340. aria_code/evals/runner.py +425 -0
  341. aria_code/evals/scorecard.py +322 -0
  342. aria_code/evals/summary.py +125 -0
  343. aria_code/external_agent_runner.py +115 -0
  344. aria_code/figma_client.py +4 -0
  345. aria_code/file_analysis_tools.py +4 -0
  346. aria_code/finance_formulas.py +4 -0
  347. aria_code/football_data_client.py +4 -0
  348. aria_code/image_gen_tools.py +4 -0
  349. aria_code/intent_classifier.py +459 -0
  350. aria_code/kling_video_client.py +4 -0
  351. aria_code/licensing.py +151 -0
  352. aria_code/local_finance_tools.py +4 -0
  353. aria_code/local_image_provider.py +4 -0
  354. aria_code/local_llm_provider.py +4 -0
  355. aria_code/macro_tools.py +4 -0
  356. aria_code/markdown_pdf.py +289 -0
  357. aria_code/market_data_client.py +4 -0
  358. aria_code/mcp_client.py +744 -0
  359. aria_code/memory_manager.py +273 -0
  360. aria_code/model_capability.py +512 -0
  361. aria_code/notification_tools.py +4 -0
  362. aria_code/openai_image_client.py +4 -0
  363. aria_code/packages/__init__.py +26 -0
  364. aria_code/packages/adk_bridge/__init__.py +11 -0
  365. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  366. aria_code/packages/adk_bridge/market_tools.py +125 -0
  367. aria_code/packages/aria_agents/__init__.py +5 -0
  368. aria_code/packages/aria_agents/manifest.py +78 -0
  369. aria_code/packages/aria_core/__init__.py +34 -0
  370. aria_code/packages/aria_core/architecture.py +269 -0
  371. aria_code/packages/aria_core/export.py +147 -0
  372. aria_code/packages/aria_core/manifest.py +65 -0
  373. aria_code/packages/aria_core/paths.py +57 -0
  374. aria_code/packages/aria_core/secure_file.py +68 -0
  375. aria_code/packages/aria_infra/__init__.py +15 -0
  376. aria_code/packages/aria_infra/arthera.py +58 -0
  377. aria_code/packages/aria_infra/doctor.py +246 -0
  378. aria_code/packages/aria_infra/product.py +40 -0
  379. aria_code/packages/aria_mcp/__init__.py +25 -0
  380. aria_code/packages/aria_mcp/bridge.py +232 -0
  381. aria_code/packages/aria_mcp/circuit.py +82 -0
  382. aria_code/packages/aria_mcp/config.py +162 -0
  383. aria_code/packages/aria_mcp/installer.py +95 -0
  384. aria_code/packages/aria_mcp/server.py +1705 -0
  385. aria_code/packages/aria_mcp/tools.py +81 -0
  386. aria_code/packages/aria_sdk/__init__.py +19 -0
  387. aria_code/packages/aria_sdk/client.py +393 -0
  388. aria_code/packages/aria_sdk/providers.py +70 -0
  389. aria_code/packages/aria_sdk/streaming.py +75 -0
  390. aria_code/packages/aria_sdk/types.py +86 -0
  391. aria_code/packages/aria_services/__init__.py +92 -0
  392. aria_code/packages/aria_services/cache.py +60 -0
  393. aria_code/packages/aria_services/context.py +356 -0
  394. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  395. aria_code/packages/aria_services/data.py +11 -0
  396. aria_code/packages/aria_services/provider_health.py +304 -0
  397. aria_code/packages/aria_services/references.py +247 -0
  398. aria_code/packages/aria_services/registry.py +249 -0
  399. aria_code/packages/aria_services/research_protocol.py +137 -0
  400. aria_code/packages/aria_services/research_quality.py +186 -0
  401. aria_code/packages/aria_services/settings.py +160 -0
  402. aria_code/packages/aria_services/usage.py +151 -0
  403. aria_code/packages/aria_skills/__init__.py +46 -0
  404. aria_code/packages/aria_skills/catalog.py +149 -0
  405. aria_code/packages/aria_skills/loader.py +455 -0
  406. aria_code/packages/aria_skills/registry.py +84 -0
  407. aria_code/packages/aria_tools/__init__.py +5 -0
  408. aria_code/packages/aria_tools/financial/__init__.py +5 -0
  409. aria_code/packages/aria_tools/financial/_paths.py +92 -0
  410. aria_code/packages/aria_tools/registry.py +129 -0
  411. aria_code/packages/quant_engine/__init__.py +33 -0
  412. aria_code/packages/quant_engine/_namespace.py +24 -0
  413. aria_code/packages/quant_engine/backtest/__init__.py +10 -0
  414. aria_code/packages/quant_engine/backtest/core.py +53 -0
  415. aria_code/packages/quant_engine/backtest/engine.py +102 -0
  416. aria_code/packages/quant_engine/portfolio/__init__.py +9 -0
  417. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  418. aria_code/packages/quant_engine/sports/__init__.py +76 -0
  419. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  420. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  421. aria_code/packages/quant_engine/sports/elo.py +300 -0
  422. aria_code/packages/quant_engine/sports/form.py +188 -0
  423. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  424. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  425. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  426. aria_code/packages/quant_engine/stochastic/__init__.py +31 -0
  427. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  428. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  429. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  430. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  431. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  432. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  433. aria_code/packs/__init__.py +85 -0
  434. aria_code/packs/base.py +168 -0
  435. aria_code/packs/finance/__init__.py +250 -0
  436. aria_code/packs/healthcare/__init__.py +40 -0
  437. aria_code/packs/logistics/__init__.py +171 -0
  438. aria_code/packs/payments/__init__.py +139 -0
  439. aria_code/packs/realty/__init__.py +190 -0
  440. aria_code/packs/registry.py +202 -0
  441. aria_code/packs/rules.py +90 -0
  442. aria_code/plan_utils.py +194 -0
  443. aria_code/plugin_loader.py +328 -0
  444. aria_code/portfolio_ledger.py +4 -0
  445. aria_code/preview_server.py +449 -0
  446. aria_code/privacy/__init__.py +5 -0
  447. aria_code/privacy/feedback.py +123 -0
  448. aria_code/project_review.py +278 -0
  449. aria_code/project_review_api.py +235 -0
  450. aria_code/project_review_server.py +18 -0
  451. aria_code/project_tools.py +4 -0
  452. aria_code/providers/__init__.py +30 -0
  453. aria_code/providers/google_video_provider.py +256 -0
  454. aria_code/providers/llm/__init__.py +19 -0
  455. aria_code/providers/llm/anthropic.py +184 -0
  456. aria_code/providers/llm/arthera_gateway.py +90 -0
  457. aria_code/providers/llm/autoconfig.py +213 -0
  458. aria_code/providers/llm/base.py +139 -0
  459. aria_code/providers/llm/custom.py +158 -0
  460. aria_code/providers/llm/ollama.py +137 -0
  461. aria_code/providers/llm/openai_compat.py +350 -0
  462. aria_code/providers/llm/registry.py +457 -0
  463. aria_code/providers/local_image_provider.py +202 -0
  464. aria_code/providers/local_llm_provider.py +549 -0
  465. aria_code/providers/ollama_models.py +79 -0
  466. aria_code/providers/openai_params.py +32 -0
  467. aria_code/providers/tool_messages.py +100 -0
  468. aria_code/realty_data_tools.py +4 -0
  469. aria_code/report_exporters.py +150 -0
  470. aria_code/report_generator.py +1764 -0
  471. aria_code/review_service.py +497 -0
  472. aria_code/runtime/__init__.py +176 -0
  473. aria_code/runtime/acceptance.py +632 -0
  474. aria_code/runtime/agent_loop.py +1851 -0
  475. aria_code/runtime/agent_profiles.py +75 -0
  476. aria_code/runtime/approval.py +71 -0
  477. aria_code/runtime/baseline.py +219 -0
  478. aria_code/runtime/budget.py +232 -0
  479. aria_code/runtime/checkpoints.py +481 -0
  480. aria_code/runtime/consult.py +65 -0
  481. aria_code/runtime/contract.py +282 -0
  482. aria_code/runtime/delivery.py +432 -0
  483. aria_code/runtime/events.py +113 -0
  484. aria_code/runtime/gateway.py +167 -0
  485. aria_code/runtime/lsp.py +484 -0
  486. aria_code/runtime/native_index.py +153 -0
  487. aria_code/runtime/orchestrator.py +55 -0
  488. aria_code/runtime/processes.py +343 -0
  489. aria_code/runtime/project_graph.py +614 -0
  490. aria_code/runtime/repo_map.py +712 -0
  491. aria_code/runtime/research_bundle.py +105 -0
  492. aria_code/runtime/review.py +269 -0
  493. aria_code/runtime/run_state.py +127 -0
  494. aria_code/runtime/run_store.py +522 -0
  495. aria_code/runtime/scoped_runs.py +113 -0
  496. aria_code/runtime/self_healing.py +422 -0
  497. aria_code/runtime/semantic_diff.py +171 -0
  498. aria_code/runtime/subagent.py +721 -0
  499. aria_code/runtime/symbol_edit.py +117 -0
  500. aria_code/runtime/task_brief.py +46 -0
  501. aria_code/runtime/task_context.py +145 -0
  502. aria_code/runtime/task_ledger.py +108 -0
  503. aria_code/runtime/task_orchestrator.py +90 -0
  504. aria_code/runtime/task_outcome.py +33 -0
  505. aria_code/runtime/task_verification.py +266 -0
  506. aria_code/runtime/task_worktree.py +448 -0
  507. aria_code/runtime/tool_executor.py +329 -0
  508. aria_code/runtime/tool_policy.py +107 -0
  509. aria_code/runtime/transactions.py +216 -0
  510. aria_code/runtime/windows_job.py +115 -0
  511. aria_code/runtime/workflows.py +222 -0
  512. aria_code/runtime/worktrees.py +215 -0
  513. aria_code/runway_video_client.py +4 -0
  514. aria_code/safety/__init__.py +27 -0
  515. aria_code/safety/client_isolation.py +178 -0
  516. aria_code/safety/permissions.py +339 -0
  517. aria_code/safety/risk.py +699 -0
  518. aria_code/safety/sandbox.py +148 -0
  519. aria_code/safety/service.py +102 -0
  520. aria_code/service_endpoints.py +14 -0
  521. aria_code/setup_wizard.py +684 -0
  522. aria_code/spreadsheet_tools.py +4 -0
  523. aria_code/strategy_vault.py +4 -0
  524. aria_code/tools/__init__.py +0 -0
  525. aria_code/tools/artifact_tools.py +252 -0
  526. aria_code/tools/broker_tools.py +92 -0
  527. aria_code/tools/build_quant_engine.py +97 -0
  528. aria_code/tools/code_audit_tools.py +192 -0
  529. aria_code/tools/computer_use_tools.py +509 -0
  530. aria_code/tools/data_analysis_tools.py +831 -0
  531. aria_code/tools/enterprise_finance_tools.py +202 -0
  532. aria_code/tools/extended_tools.py +240 -0
  533. aria_code/tools/file_analysis_tools.py +880 -0
  534. aria_code/tools/image_gen_tools.py +114 -0
  535. aria_code/tools/local_finance_tools.py +3488 -0
  536. aria_code/tools/logistics_carriers.py +11 -0
  537. aria_code/tools/logistics_inventory.py +11 -0
  538. aria_code/tools/logistics_io.py +11 -0
  539. aria_code/tools/logistics_tenancy.py +11 -0
  540. aria_code/tools/logistics_tools.py +11 -0
  541. aria_code/tools/macro_tools.py +412 -0
  542. aria_code/tools/notification_tools.py +249 -0
  543. aria_code/tools/project_tools.py +525 -0
  544. aria_code/tools/realty_data_tools.py +662 -0
  545. aria_code/tools/spreadsheet_tools.py +399 -0
  546. aria_code/tools/stripe_tools.py +115 -0
  547. aria_code/tools/video_tools.py +500 -0
  548. aria_code/training/__init__.py +1 -0
  549. aria_code/training/compare.py +117 -0
  550. aria_code/training/prelude_commands.py +111 -0
  551. aria_code/training/prelude_eval.py +390 -0
  552. aria_code/training/prelude_real.py +183 -0
  553. aria_code/training/prelude_train.py +248 -0
  554. aria_code/training/sft_dataset.py +230 -0
  555. aria_code/training/trajectory_dataset.py +263 -0
  556. aria_code/ui/__init__.py +105 -0
  557. aria_code/ui/banner.py +404 -0
  558. aria_code/ui/completer.py +571 -0
  559. aria_code/ui/console.py +285 -0
  560. aria_code/ui/input_box.py +505 -0
  561. aria_code/ui/mentions.py +206 -0
  562. aria_code/ui/picker.py +248 -0
  563. aria_code/ui/render/__init__.py +11 -0
  564. aria_code/ui/render/actions.py +297 -0
  565. aria_code/ui/render/finance.py +1732 -0
  566. aria_code/ui/render/market.py +253 -0
  567. aria_code/ui/render/output.py +954 -0
  568. aria_code/ui/render/responsive.py +75 -0
  569. aria_code/ui/render/team.py +367 -0
  570. aria_code/ui/robot.py +239 -0
  571. aria_code/ui/startup_dashboard.py +110 -0
  572. aria_code/video_analysis.py +147 -0
  573. aria_code/video_editor.py +518 -0
  574. aria_code/video_workflow/__init__.py +8 -0
  575. aria_code/video_workflow/__main__.py +61 -0
  576. aria_code/video_workflow/engines/__init__.py +18 -0
  577. aria_code/video_workflow/engines/base.py +69 -0
  578. aria_code/video_workflow/engines/custom_model.py +204 -0
  579. aria_code/video_workflow/engines/google_veo.py +94 -0
  580. aria_code/video_workflow/engines/local_engine.py +71 -0
  581. aria_code/video_workflow/engines/registry.py +98 -0
  582. aria_code/video_workflow/google_models.py +278 -0
  583. aria_code/video_workflow/music/__init__.py +5 -0
  584. aria_code/video_workflow/music/foley_service.py +132 -0
  585. aria_code/video_workflow/music/service.py +212 -0
  586. aria_code/video_workflow/pipeline.py +620 -0
  587. aria_code/video_workflow/plan.py +202 -0
  588. aria_code/video_workflow/session.py +297 -0
  589. aria_code/workspace/__init__.py +6 -0
  590. aria_code/workspace/files.py +219 -0
  591. aria_code/workspace/verify.py +167 -0
  592. aria_code/workspace_changes.py +199 -0
  593. aria_code/workspace_service.py +196 -0
  594. aria_code/workspace_tools.py +150 -0
  595. aria_code-0.167.0.dist-info/METADATA +330 -0
  596. aria_code-0.167.0.dist-info/RECORD +601 -0
  597. aria_code-0.167.0.dist-info/WHEEL +5 -0
  598. aria_code-0.167.0.dist-info/entry_points.txt +5 -0
  599. aria_code-0.167.0.dist-info/licenses/LICENSE +201 -0
  600. aria_code-0.167.0.dist-info/licenses/NOTICE +49 -0
  601. 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"]