aria-code 0.52.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (478) hide show
  1. aria_code/__init__.py +42 -0
  2. aria_code/_version.py +20 -0
  3. aria_code/adk_apps/__init__.py +1 -0
  4. aria_code/adk_apps/aria_code_review/__init__.py +1 -0
  5. aria_code/adk_apps/aria_code_review/agent.py +32 -0
  6. aria_code/adk_apps/aria_research/__init__.py +1 -0
  7. aria_code/adk_apps/aria_research/agent.py +31 -0
  8. aria_code/agents/__init__.py +34 -0
  9. aria_code/agents/base.py +318 -0
  10. aria_code/agents/code_review.py +200 -0
  11. aria_code/agents/deep/__init__.py +37 -0
  12. aria_code/agents/deep/calibration_loop.py +144 -0
  13. aria_code/agents/deep/critic.py +125 -0
  14. aria_code/agents/deep/deepen.py +193 -0
  15. aria_code/agents/deep/models.py +149 -0
  16. aria_code/agents/deep/pipeline.py +164 -0
  17. aria_code/agents/deep/quant_fusion.py +192 -0
  18. aria_code/agents/deep/themes.py +95 -0
  19. aria_code/agents/deep/tiers.py +106 -0
  20. aria_code/agents/engineering/__init__.py +14 -0
  21. aria_code/agents/engineering/coder.py +222 -0
  22. aria_code/agents/engineering/tester.py +145 -0
  23. aria_code/agents/financial/__init__.py +21 -0
  24. aria_code/agents/financial/cashflow_burn.py +119 -0
  25. aria_code/agents/financial/catalyst.py +279 -0
  26. aria_code/agents/financial/corporate_finance.py +175 -0
  27. aria_code/agents/financial/debate.py +224 -0
  28. aria_code/agents/financial/earnings.py +303 -0
  29. aria_code/agents/financial/fundamental.py +191 -0
  30. aria_code/agents/financial/macro.py +99 -0
  31. aria_code/agents/financial/news.py +207 -0
  32. aria_code/agents/financial/risk.py +165 -0
  33. aria_code/agents/financial/sector.py +279 -0
  34. aria_code/agents/financial/strategist.py +121 -0
  35. aria_code/agents/financial/stripe_revenue.py +127 -0
  36. aria_code/agents/financial/synthesis.py +274 -0
  37. aria_code/agents/financial/technical.py +258 -0
  38. aria_code/agents/orchestrator.py +44 -0
  39. aria_code/agents/portfolio_agent.py +363 -0
  40. aria_code/agents/realty/__init__.py +62 -0
  41. aria_code/agents/realty/asset_diagnosis.py +152 -0
  42. aria_code/agents/realty/business_match.py +166 -0
  43. aria_code/agents/realty/cashflow_verify.py +209 -0
  44. aria_code/agents/realty/contract_rules.py +210 -0
  45. aria_code/agents/realty/energy_anomaly.py +189 -0
  46. aria_code/agents/realty/exit_settlement.py +208 -0
  47. aria_code/agents/realty/fulfillment_risk.py +206 -0
  48. aria_code/agents/realty/ops_optimize.py +160 -0
  49. aria_code/agents/realty/revenue_share.py +215 -0
  50. aria_code/agents/registry.py +158 -0
  51. aria_code/agents/signal_scheme.py +111 -0
  52. aria_code/agents/sports/__init__.py +0 -0
  53. aria_code/agents/sports/football_agent.py +197 -0
  54. aria_code/agents/supervisor.py +113 -0
  55. aria_code/agents/team.py +492 -0
  56. aria_code/agents/warehouse/__init__.py +25 -0
  57. aria_code/agents/warehouse/contracts.py +26 -0
  58. aria_code/agents/warehouse/dashboard.py +155 -0
  59. aria_code/agents/warehouse/fulfillment_leadtime.py +95 -0
  60. aria_code/agents/warehouse/inbound_exceptions.py +25 -0
  61. aria_code/agents/warehouse/inventory_health.py +24 -0
  62. aria_code/agents/warehouse/local_run.py +71 -0
  63. aria_code/agents/warehouse/local_snapshot.py +132 -0
  64. aria_code/agents/warehouse/logistics_cost.py +84 -0
  65. aria_code/agents/warehouse/logistics_sync.py +21 -0
  66. aria_code/agents/warehouse/workflow.py +31 -0
  67. aria_code/aliyun_data_client.py +4 -0
  68. aria_code/apps/README.md +12 -0
  69. aria_code/apps/__init__.py +2 -0
  70. aria_code/apps/channels/README.md +15 -0
  71. aria_code/apps/channels/__init__.py +17 -0
  72. aria_code/apps/channels/intake.py +92 -0
  73. aria_code/apps/channels/registry.py +95 -0
  74. aria_code/apps/channels/tradingview.py +94 -0
  75. aria_code/apps/cli/README.md +13 -0
  76. aria_code/apps/cli/__init__.py +2 -0
  77. aria_code/apps/cli/_optional.py +120 -0
  78. aria_code/apps/cli/bootstrap.py +145 -0
  79. aria_code/apps/cli/broker_render.py +206 -0
  80. aria_code/apps/cli/codegen_paths.py +29 -0
  81. aria_code/apps/cli/commands/__init__.py +16 -0
  82. aria_code/apps/cli/commands/_business_render.py +78 -0
  83. aria_code/apps/cli/commands/_ui.py +71 -0
  84. aria_code/apps/cli/commands/analysis_cmds.py +369 -0
  85. aria_code/apps/cli/commands/ashare_prediction_cmds.py +327 -0
  86. aria_code/apps/cli/commands/auth_cmds.py +239 -0
  87. aria_code/apps/cli/commands/backtest_cmds.py +2543 -0
  88. aria_code/apps/cli/commands/broker_cmds.py +1234 -0
  89. aria_code/apps/cli/commands/business_workflow_cmds.py +319 -0
  90. aria_code/apps/cli/commands/canvas_cmds.py +77 -0
  91. aria_code/apps/cli/commands/catalog.py +87 -0
  92. aria_code/apps/cli/commands/core_cmds.py +2520 -0
  93. aria_code/apps/cli/commands/data_cmds.py +457 -0
  94. aria_code/apps/cli/commands/diagnostic_cmds.py +230 -0
  95. aria_code/apps/cli/commands/diagnostic_ops_cmds.py +813 -0
  96. aria_code/apps/cli/commands/file_cmds.py +312 -0
  97. aria_code/apps/cli/commands/finance_service_cmds.py +60 -0
  98. aria_code/apps/cli/commands/fx_commodity_cmds.py +123 -0
  99. aria_code/apps/cli/commands/market.py +434 -0
  100. aria_code/apps/cli/commands/market_cmds.py +1381 -0
  101. aria_code/apps/cli/commands/market_context.py +425 -0
  102. aria_code/apps/cli/commands/market_render.py +7 -0
  103. aria_code/apps/cli/commands/model_cmds.py +2153 -0
  104. aria_code/apps/cli/commands/ops_cmds.py +721 -0
  105. aria_code/apps/cli/commands/orchestrator_cmds.py +179 -0
  106. aria_code/apps/cli/commands/pdf_export_cmds.py +125 -0
  107. aria_code/apps/cli/commands/portfolio_cmds.py +1278 -0
  108. aria_code/apps/cli/commands/report.py +437 -0
  109. aria_code/apps/cli/commands/scaffold_templates.py +617 -0
  110. aria_code/apps/cli/commands/session_cmds.py +236 -0
  111. aria_code/apps/cli/commands/session_ux_cmds.py +359 -0
  112. aria_code/apps/cli/commands/team.py +669 -0
  113. aria_code/apps/cli/commands/team_render.py +8 -0
  114. aria_code/apps/cli/commands/ui_cmds.py +401 -0
  115. aria_code/apps/cli/commands/warehouse_cmds.py +79 -0
  116. aria_code/apps/cli/commands/workflow_cmds.py +480 -0
  117. aria_code/apps/cli/commands/workspace_cmds.py +1494 -0
  118. aria_code/apps/cli/config_paths.py +73 -0
  119. aria_code/apps/cli/config_store.py +78 -0
  120. aria_code/apps/cli/constants.py +88 -0
  121. aria_code/apps/cli/context.py +34 -0
  122. aria_code/apps/cli/deterministic.py +191 -0
  123. aria_code/apps/cli/direct.py +48 -0
  124. aria_code/apps/cli/finance_service_catalog.py +111 -0
  125. aria_code/apps/cli/football_reports.py +231 -0
  126. aria_code/apps/cli/github_app_auth.py +135 -0
  127. aria_code/apps/cli/google_login.py +196 -0
  128. aria_code/apps/cli/handlers/__init__.py +11 -0
  129. aria_code/apps/cli/handlers/broker_handlers.py +122 -0
  130. aria_code/apps/cli/handlers/chart_handlers.py +1309 -0
  131. aria_code/apps/cli/handlers/market_handlers.py +2513 -0
  132. aria_code/apps/cli/handlers/realty_handlers.py +114 -0
  133. aria_code/apps/cli/handlers/sports_handlers.py +22 -0
  134. aria_code/apps/cli/handlers/strategy_advice.py +82 -0
  135. aria_code/apps/cli/helpers.py +302 -0
  136. aria_code/apps/cli/hooks.py +180 -0
  137. aria_code/apps/cli/i18n.py +297 -0
  138. aria_code/apps/cli/intent.py +136 -0
  139. aria_code/apps/cli/intent_router.py +276 -0
  140. aria_code/apps/cli/intent_signals.py +344 -0
  141. aria_code/apps/cli/lifecycle_hooks.py +48 -0
  142. aria_code/apps/cli/local_tool_schemas.py +536 -0
  143. aria_code/apps/cli/main.py +29 -0
  144. aria_code/apps/cli/market_metadata.py +135 -0
  145. aria_code/apps/cli/market_universe.py +487 -0
  146. aria_code/apps/cli/message_processing.py +310 -0
  147. aria_code/apps/cli/model_catalog.py +620 -0
  148. aria_code/apps/cli/pdf_report.py +977 -0
  149. aria_code/apps/cli/plan_mode.py +139 -0
  150. aria_code/apps/cli/plotly_html.py +15 -0
  151. aria_code/apps/cli/prediction_feedback.py +202 -0
  152. aria_code/apps/cli/preflight.py +502 -0
  153. aria_code/apps/cli/project_aria.py +60 -0
  154. aria_code/apps/cli/prompt_assembly.py +59 -0
  155. aria_code/apps/cli/prompts/__init__.py +0 -0
  156. aria_code/apps/cli/prompts/coding.py +53 -0
  157. aria_code/apps/cli/prompts/select.py +142 -0
  158. aria_code/apps/cli/prompts/system_prompts.py +552 -0
  159. aria_code/apps/cli/prompts/ui.py +436 -0
  160. aria_code/apps/cli/provider_endpoints.py +237 -0
  161. aria_code/apps/cli/provider_keys.py +73 -0
  162. aria_code/apps/cli/providers/__init__.py +1 -0
  163. aria_code/apps/cli/providers/base.py +521 -0
  164. aria_code/apps/cli/providers/chat_routing.py +128 -0
  165. aria_code/apps/cli/providers/collaboration.py +98 -0
  166. aria_code/apps/cli/providers/llm/__init__.py +1 -0
  167. aria_code/apps/cli/providers/llm/offline_responses.py +68 -0
  168. aria_code/apps/cli/providers/llm/ollama_stream.py +1594 -0
  169. aria_code/apps/cli/providers/llm/response_cache.py +60 -0
  170. aria_code/apps/cli/providers/llm/sse_stream.py +257 -0
  171. aria_code/apps/cli/providers/runtime_bridge.py +353 -0
  172. aria_code/apps/cli/providers/vertexai_stream.py +316 -0
  173. aria_code/apps/cli/runtime_consumer.py +657 -0
  174. aria_code/apps/cli/sandbox.py +32 -0
  175. aria_code/apps/cli/session_export.py +120 -0
  176. aria_code/apps/cli/session_jsonl.py +208 -0
  177. aria_code/apps/cli/session_store.py +112 -0
  178. aria_code/apps/cli/skills_catalog.py +273 -0
  179. aria_code/apps/cli/todo_tracker.py +190 -0
  180. aria_code/apps/cli/tool_executor.py +823 -0
  181. aria_code/apps/cli/tool_registry.py +34 -0
  182. aria_code/apps/cli/tool_scope.py +106 -0
  183. aria_code/apps/cli/tools/__init__.py +40 -0
  184. aria_code/apps/cli/tools/context.py +46 -0
  185. aria_code/apps/cli/tools/file_tools.py +180 -0
  186. aria_code/apps/cli/tools/market_tools.py +620 -0
  187. aria_code/apps/cli/tools/notebook_tools.py +111 -0
  188. aria_code/apps/cli/tools/system_tools.py +726 -0
  189. aria_code/apps/cli/tools/write_tools.py +845 -0
  190. aria_code/apps/cli/tradingview_bridge.py +529 -0
  191. aria_code/apps/cli/turn_planning.py +137 -0
  192. aria_code/apps/cli/update_check.py +153 -0
  193. aria_code/apps/cli/utils/__init__.py +0 -0
  194. aria_code/apps/cli/utils/market_detect.py +1597 -0
  195. aria_code/apps/daemon/README.md +14 -0
  196. aria_code/apps/vscode/README.md +115 -0
  197. aria_code/apps/vscode/package.json +70 -0
  198. aria_code/aria_cli.py +6743 -0
  199. aria_code/aria_daemon.py +1398 -0
  200. aria_code/aria_feishu_bot.py +1423 -0
  201. aria_code/aria_mcp_server.py +7 -0
  202. aria_code/aria_relay_client.py +4 -0
  203. aria_code/aria_relay_server.py +490 -0
  204. aria_code/aria_telegram_bot.py +202 -0
  205. aria_code/ariarc.py +385 -0
  206. aria_code/artifacts.py +581 -0
  207. aria_code/backtest_engine.py +4 -0
  208. aria_code/backtest_report.py +4 -0
  209. aria_code/brokers/__init__.py +78 -0
  210. aria_code/brokers/automation.py +193 -0
  211. aria_code/brokers/base.py +231 -0
  212. aria_code/brokers/capabilities.py +264 -0
  213. aria_code/brokers/cn/__init__.py +10 -0
  214. aria_code/brokers/cn/easytrader_broker.py +193 -0
  215. aria_code/brokers/cn/futu_broker.py +194 -0
  216. aria_code/brokers/cn/longbridge_broker.py +190 -0
  217. aria_code/brokers/cn/tiger_broker.py +196 -0
  218. aria_code/brokers/cn/xtquant_broker.py +225 -0
  219. aria_code/brokers/config.py +414 -0
  220. aria_code/brokers/intl/__init__.py +5 -0
  221. aria_code/brokers/intl/alpaca_broker.py +183 -0
  222. aria_code/brokers/intl/ibkr_broker.py +215 -0
  223. aria_code/brokers/intl/trading212_broker.py +183 -0
  224. aria_code/brokers/intl/webull_broker.py +156 -0
  225. aria_code/brokers/paper_broker.py +266 -0
  226. aria_code/brokers/planning.py +296 -0
  227. aria_code/brokers/registry.py +213 -0
  228. aria_code/brokers/trading.py +267 -0
  229. aria_code/canva_client.py +4 -0
  230. aria_code/change_store.py +127 -0
  231. aria_code/clients/__init__.py +0 -0
  232. aria_code/clients/aliyun_data_client.py +660 -0
  233. aria_code/clients/aria_relay_client.py +182 -0
  234. aria_code/clients/canva_client.py +284 -0
  235. aria_code/clients/figma_client.py +100 -0
  236. aria_code/clients/football_data_client.py +1671 -0
  237. aria_code/clients/kling_video_client.py +182 -0
  238. aria_code/clients/market_data_client.py +1976 -0
  239. aria_code/clients/openai_image_client.py +242 -0
  240. aria_code/clients/runway_video_client.py +157 -0
  241. aria_code/clients/warehouse_erp_client.py +105 -0
  242. aria_code/cloud_memory.py +181 -0
  243. aria_code/command_safety.py +19 -0
  244. aria_code/computer_use_tools.py +4 -0
  245. aria_code/dashboard_generator.py +579 -0
  246. aria_code/data_analysis_tools.py +4 -0
  247. aria_code/data_cleaner.py +483 -0
  248. aria_code/data_service.py +513 -0
  249. aria_code/datasources/__init__.py +23 -0
  250. aria_code/datasources/base.py +166 -0
  251. aria_code/datasources/router.py +253 -0
  252. aria_code/datasources/sources/__init__.py +15 -0
  253. aria_code/datasources/sources/akshare_source.py +269 -0
  254. aria_code/datasources/sources/alpha_vantage_source.py +203 -0
  255. aria_code/datasources/sources/edgar_source.py +219 -0
  256. aria_code/datasources/sources/finnhub_source.py +198 -0
  257. aria_code/datasources/sources/fred_source.py +223 -0
  258. aria_code/datasources/sources/tushare_source.py +143 -0
  259. aria_code/datasources/sources/web_scraper_source.py +278 -0
  260. aria_code/datasources/sources/world_bank_source.py +206 -0
  261. aria_code/datasources/sources/yfinance_source.py +152 -0
  262. aria_code/demo_player.py +207 -0
  263. aria_code/doctor.py +813 -0
  264. aria_code/domain/__init__.py +0 -0
  265. aria_code/domain/backtest_engine.py +354 -0
  266. aria_code/domain/backtest_report.py +472 -0
  267. aria_code/domain/finance_formulas.py +389 -0
  268. aria_code/domain/portfolio_ledger.py +307 -0
  269. aria_code/domain/strategy_vault.py +421 -0
  270. aria_code/evals/__init__.py +37 -0
  271. aria_code/evals/harness.py +598 -0
  272. aria_code/evals/runner.py +238 -0
  273. aria_code/external_agent_runner.py +115 -0
  274. aria_code/figma_client.py +4 -0
  275. aria_code/file_analysis_tools.py +4 -0
  276. aria_code/finance_formulas.py +4 -0
  277. aria_code/football_data_client.py +4 -0
  278. aria_code/image_gen_tools.py +4 -0
  279. aria_code/intent_classifier.py +386 -0
  280. aria_code/kling_video_client.py +4 -0
  281. aria_code/licensing.py +151 -0
  282. aria_code/local_finance_tools.py +4 -0
  283. aria_code/local_image_provider.py +4 -0
  284. aria_code/local_llm_provider.py +4 -0
  285. aria_code/macro_tools.py +4 -0
  286. aria_code/markdown_pdf.py +289 -0
  287. aria_code/market_data_client.py +4 -0
  288. aria_code/mcp_client.py +639 -0
  289. aria_code/memory_manager.py +246 -0
  290. aria_code/model_capability.py +486 -0
  291. aria_code/notification_tools.py +4 -0
  292. aria_code/openai_image_client.py +4 -0
  293. aria_code/packages/__init__.py +23 -0
  294. aria_code/packages/adk_bridge/__init__.py +11 -0
  295. aria_code/packages/adk_bridge/code_review_tools.py +49 -0
  296. aria_code/packages/adk_bridge/market_tools.py +125 -0
  297. aria_code/packages/aria_agents/__init__.py +5 -0
  298. aria_code/packages/aria_agents/manifest.py +78 -0
  299. aria_code/packages/aria_core/__init__.py +34 -0
  300. aria_code/packages/aria_core/architecture.py +269 -0
  301. aria_code/packages/aria_core/export.py +147 -0
  302. aria_code/packages/aria_core/manifest.py +65 -0
  303. aria_code/packages/aria_core/paths.py +57 -0
  304. aria_code/packages/aria_core/secure_file.py +68 -0
  305. aria_code/packages/aria_infra/__init__.py +15 -0
  306. aria_code/packages/aria_infra/arthera.py +52 -0
  307. aria_code/packages/aria_infra/doctor.py +246 -0
  308. aria_code/packages/aria_infra/product.py +40 -0
  309. aria_code/packages/aria_mcp/__init__.py +25 -0
  310. aria_code/packages/aria_mcp/bridge.py +201 -0
  311. aria_code/packages/aria_mcp/circuit.py +82 -0
  312. aria_code/packages/aria_mcp/config.py +111 -0
  313. aria_code/packages/aria_mcp/installer.py +95 -0
  314. aria_code/packages/aria_mcp/server.py +1550 -0
  315. aria_code/packages/aria_mcp/tools.py +81 -0
  316. aria_code/packages/aria_sdk/__init__.py +19 -0
  317. aria_code/packages/aria_sdk/client.py +396 -0
  318. aria_code/packages/aria_sdk/providers.py +70 -0
  319. aria_code/packages/aria_sdk/streaming.py +79 -0
  320. aria_code/packages/aria_sdk/types.py +86 -0
  321. aria_code/packages/aria_services/__init__.py +92 -0
  322. aria_code/packages/aria_services/cache.py +60 -0
  323. aria_code/packages/aria_services/context.py +356 -0
  324. aria_code/packages/aria_services/context_checkpoints.py +157 -0
  325. aria_code/packages/aria_services/data.py +11 -0
  326. aria_code/packages/aria_services/provider_health.py +268 -0
  327. aria_code/packages/aria_services/references.py +247 -0
  328. aria_code/packages/aria_services/registry.py +249 -0
  329. aria_code/packages/aria_services/research_protocol.py +57 -0
  330. aria_code/packages/aria_services/research_quality.py +186 -0
  331. aria_code/packages/aria_services/settings.py +157 -0
  332. aria_code/packages/aria_services/usage.py +151 -0
  333. aria_code/packages/aria_skills/__init__.py +46 -0
  334. aria_code/packages/aria_skills/catalog.py +149 -0
  335. aria_code/packages/aria_skills/loader.py +455 -0
  336. aria_code/packages/aria_skills/registry.py +84 -0
  337. aria_code/packages/aria_tools/__init__.py +5 -0
  338. aria_code/packages/aria_tools/financial/__init__.py +8 -0
  339. aria_code/packages/aria_tools/financial/_paths.py +130 -0
  340. aria_code/packages/aria_tools/financial/compliance.py +31 -0
  341. aria_code/packages/aria_tools/financial/factors.py +33 -0
  342. aria_code/packages/aria_tools/financial/risk_tools.py +33 -0
  343. aria_code/packages/aria_tools/financial/strategy.py +31 -0
  344. aria_code/packages/aria_tools/registry.py +128 -0
  345. aria_code/packages/quant_engine/__init__.py +34 -0
  346. aria_code/packages/quant_engine/_namespace.py +62 -0
  347. aria_code/packages/quant_engine/backtest/__init__.py +14 -0
  348. aria_code/packages/quant_engine/backtest/core.py +53 -0
  349. aria_code/packages/quant_engine/backtest/engine.py +96 -0
  350. aria_code/packages/quant_engine/portfolio/__init__.py +13 -0
  351. aria_code/packages/quant_engine/portfolio/optimizer.py +85 -0
  352. aria_code/packages/quant_engine/sports/__init__.py +80 -0
  353. aria_code/packages/quant_engine/sports/calibrator.py +354 -0
  354. aria_code/packages/quant_engine/sports/dixon_coles.py +234 -0
  355. aria_code/packages/quant_engine/sports/elo.py +300 -0
  356. aria_code/packages/quant_engine/sports/form.py +188 -0
  357. aria_code/packages/quant_engine/sports/h2h.py +195 -0
  358. aria_code/packages/quant_engine/sports/ml_model.py +377 -0
  359. aria_code/packages/quant_engine/sports/predictor.py +312 -0
  360. aria_code/packages/quant_engine/sports/tracker.py +665 -0
  361. aria_code/packages/quant_engine/stochastic/__init__.py +35 -0
  362. aria_code/packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  363. aria_code/packages/quant_engine/stochastic/ito_calculus.py +477 -0
  364. aria_code/packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  365. aria_code/packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  366. aria_code/packages/quant_engine/stochastic/options_pricing.py +576 -0
  367. aria_code/packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  368. aria_code/packs/__init__.py +85 -0
  369. aria_code/packs/base.py +168 -0
  370. aria_code/packs/finance/__init__.py +250 -0
  371. aria_code/packs/logistics/__init__.py +169 -0
  372. aria_code/packs/payments/__init__.py +139 -0
  373. aria_code/packs/realty/__init__.py +190 -0
  374. aria_code/packs/registry.py +202 -0
  375. aria_code/packs/rules.py +90 -0
  376. aria_code/plan_utils.py +194 -0
  377. aria_code/plugin_loader.py +328 -0
  378. aria_code/portfolio_ledger.py +4 -0
  379. aria_code/preview_server.py +449 -0
  380. aria_code/privacy/__init__.py +5 -0
  381. aria_code/privacy/feedback.py +123 -0
  382. aria_code/project_review.py +278 -0
  383. aria_code/project_review_api.py +235 -0
  384. aria_code/project_review_server.py +18 -0
  385. aria_code/project_tools.py +4 -0
  386. aria_code/providers/__init__.py +30 -0
  387. aria_code/providers/llm/__init__.py +19 -0
  388. aria_code/providers/llm/anthropic.py +184 -0
  389. aria_code/providers/llm/autoconfig.py +213 -0
  390. aria_code/providers/llm/base.py +139 -0
  391. aria_code/providers/llm/custom.py +158 -0
  392. aria_code/providers/llm/ollama.py +129 -0
  393. aria_code/providers/llm/openai_compat.py +313 -0
  394. aria_code/providers/llm/registry.py +444 -0
  395. aria_code/providers/local_image_provider.py +202 -0
  396. aria_code/providers/local_llm_provider.py +589 -0
  397. aria_code/realty_data_tools.py +4 -0
  398. aria_code/report_exporters.py +150 -0
  399. aria_code/report_generator.py +1764 -0
  400. aria_code/runtime/__init__.py +169 -0
  401. aria_code/runtime/acceptance.py +481 -0
  402. aria_code/runtime/agent_loop.py +1520 -0
  403. aria_code/runtime/approval.py +57 -0
  404. aria_code/runtime/budget.py +232 -0
  405. aria_code/runtime/checkpoints.py +432 -0
  406. aria_code/runtime/crash_report.py +174 -0
  407. aria_code/runtime/events.py +113 -0
  408. aria_code/runtime/gateway.py +150 -0
  409. aria_code/runtime/lsp.py +484 -0
  410. aria_code/runtime/orchestrator.py +55 -0
  411. aria_code/runtime/repo_map.py +700 -0
  412. aria_code/runtime/run_state.py +127 -0
  413. aria_code/runtime/run_store.py +450 -0
  414. aria_code/runtime/self_healing.py +422 -0
  415. aria_code/runtime/subagent.py +547 -0
  416. aria_code/runtime/task_ledger.py +86 -0
  417. aria_code/runtime/task_orchestrator.py +90 -0
  418. aria_code/runtime/tool_executor.py +217 -0
  419. aria_code/runtime/tool_policy.py +107 -0
  420. aria_code/runtime/worktrees.py +215 -0
  421. aria_code/runway_video_client.py +4 -0
  422. aria_code/safety/__init__.py +23 -0
  423. aria_code/safety/permissions.py +306 -0
  424. aria_code/safety/service.py +102 -0
  425. aria_code/setup_wizard.py +673 -0
  426. aria_code/spreadsheet_tools.py +4 -0
  427. aria_code/strategy_vault.py +4 -0
  428. aria_code/tools/__init__.py +0 -0
  429. aria_code/tools/artifact_tools.py +252 -0
  430. aria_code/tools/broker_tools.py +92 -0
  431. aria_code/tools/build_quant_engine.py +97 -0
  432. aria_code/tools/code_audit_tools.py +192 -0
  433. aria_code/tools/computer_use_tools.py +504 -0
  434. aria_code/tools/data_analysis_tools.py +809 -0
  435. aria_code/tools/enterprise_finance_tools.py +156 -0
  436. aria_code/tools/extended_tools.py +240 -0
  437. aria_code/tools/file_analysis_tools.py +885 -0
  438. aria_code/tools/image_gen_tools.py +114 -0
  439. aria_code/tools/local_finance_tools.py +3417 -0
  440. aria_code/tools/logistics_tools.py +135 -0
  441. aria_code/tools/macro_tools.py +369 -0
  442. aria_code/tools/notification_tools.py +249 -0
  443. aria_code/tools/project_tools.py +525 -0
  444. aria_code/tools/realty_data_tools.py +659 -0
  445. aria_code/tools/spreadsheet_tools.py +388 -0
  446. aria_code/tools/stripe_tools.py +115 -0
  447. aria_code/ui/__init__.py +105 -0
  448. aria_code/ui/banner.py +508 -0
  449. aria_code/ui/completer.py +501 -0
  450. aria_code/ui/console.py +281 -0
  451. aria_code/ui/image_render.py +243 -0
  452. aria_code/ui/input_box.py +488 -0
  453. aria_code/ui/picker.py +201 -0
  454. aria_code/ui/render/__init__.py +11 -0
  455. aria_code/ui/render/finance.py +1705 -0
  456. aria_code/ui/render/market.py +225 -0
  457. aria_code/ui/render/output.py +775 -0
  458. aria_code/ui/render/responsive.py +75 -0
  459. aria_code/ui/render/team.py +354 -0
  460. aria_code/ui/robot.py +235 -0
  461. aria_code/ui/startup_dashboard.py +109 -0
  462. aria_code/utils/vscode_uri.py +37 -0
  463. aria_code/video_analysis.py +147 -0
  464. aria_code/video_editor.py +271 -0
  465. aria_code/workspace/__init__.py +6 -0
  466. aria_code/workspace/files.py +193 -0
  467. aria_code/workspace/verify.py +113 -0
  468. aria_code/workspace_changes.py +199 -0
  469. aria_code/workspace_context.py +233 -0
  470. aria_code/workspace_service.py +196 -0
  471. aria_code/workspace_tools.py +150 -0
  472. aria_code-0.52.0.dist-info/METADATA +237 -0
  473. aria_code-0.52.0.dist-info/RECORD +478 -0
  474. aria_code-0.52.0.dist-info/WHEEL +5 -0
  475. aria_code-0.52.0.dist-info/entry_points.txt +3 -0
  476. aria_code-0.52.0.dist-info/licenses/LICENSE +201 -0
  477. aria_code-0.52.0.dist-info/licenses/NOTICE +49 -0
  478. aria_code-0.52.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,639 @@
1
+ """
2
+ mcp_client.py — MCP (Model Context Protocol) client for Aria Code.
3
+
4
+ Connects to any MCP server via stdio transport and exposes its tools as
5
+ first-class LOCAL_TOOLS in the Aria Code tool loop.
6
+
7
+ Config file: ~/.arthera/mcp_servers.json
8
+ Example::
9
+
10
+ {
11
+ "servers": [
12
+ {
13
+ "name": "quant_engine",
14
+ "command": "python3",
15
+ "args": ["/path/to/mcp_server.py"],
16
+ "env": {"PYTHONPATH": "/path/to/project"},
17
+ "description": "Arthera quant engine MCP tools"
18
+ },
19
+ {
20
+ "name": "filesystem",
21
+ "command": "npx",
22
+ "args": ["-y", "@modelcontextprotocol/server-filesystem", "/path/to/projects"],
23
+ "description": "Filesystem access"
24
+ },
25
+ {
26
+ "name": "apify",
27
+ "command": "npx",
28
+ "args": ["-y", "@apify/actors-mcp-server@latest", "--tools", "actors,docs,apify/rag-web-browser"],
29
+ "env": {"APIFY_TOKEN": "${APIFY_TOKEN}"},
30
+ "enabled": false,
31
+ "description": "Approved Apify web research Actors (enable only after token and data-use review)"
32
+ }
33
+ ]
34
+ }
35
+
36
+ Usage::
37
+
38
+ registry = MCPToolRegistry()
39
+ await registry.start_all()
40
+ registry.register_into(LOCAL_TOOLS, LOCAL_TOOL_SCHEMAS)
41
+
42
+ # Tools are now callable as regular LOCAL_TOOLS entries
43
+ result = await registry.call_tool("quant_engine/backtest_strategy", {"symbol": "AAPL"})
44
+ """
45
+
46
+ from __future__ import annotations
47
+
48
+ import asyncio
49
+ import json
50
+ import logging
51
+ import os
52
+ import pathlib
53
+ import re
54
+ import traceback
55
+ from typing import Any, Callable, Dict, List, Optional, Tuple
56
+
57
+ from aria_code.packages.aria_mcp.circuit import ServerCircuit
58
+ from aria_code.packages.aria_core.paths import aria_home
59
+
60
+ logger = logging.getLogger(__name__)
61
+
62
+ MCP_CONFIG_PATH = aria_home() / "mcp_servers.json"
63
+
64
+ # MCP JSON-RPC protocol version
65
+ MCP_PROTOCOL_VERSION = "2024-11-05"
66
+
67
+
68
+ def model_safe_tool_name(server_name: str, tool_name: str) -> str:
69
+ """Return an OpenAI/Ollama-compatible qualified MCP function name."""
70
+ server = re.sub(r"[^A-Za-z0-9_-]+", "_", str(server_name)).strip("_") or "server"
71
+ tool = re.sub(r"[^A-Za-z0-9_-]+", "_", str(tool_name)).strip("_") or "tool"
72
+ return f"mcp__{server}__{tool}"
73
+
74
+
75
+ # ---------------------------------------------------------------------------
76
+ # JSON-RPC helpers
77
+ # ---------------------------------------------------------------------------
78
+
79
+ def _make_request(method: str, params: Any = None, req_id: int = 1) -> bytes:
80
+ msg = {"jsonrpc": "2.0", "id": req_id, "method": method}
81
+ if params is not None:
82
+ msg["params"] = params
83
+ return (json.dumps(msg) + "\n").encode()
84
+
85
+
86
+ def _make_notification(method: str, params: Any = None) -> bytes:
87
+ msg = {"jsonrpc": "2.0", "method": method}
88
+ if params is not None:
89
+ msg["params"] = params
90
+ return (json.dumps(msg) + "\n").encode()
91
+
92
+
93
+ # ---------------------------------------------------------------------------
94
+ # MCPServer — one subprocess connection
95
+ # ---------------------------------------------------------------------------
96
+
97
+ class MCPServer:
98
+ """
99
+ Manages a single MCP server subprocess (stdio transport).
100
+
101
+ Lifecycle: start() → list_tools() → call_tool(…) → stop()
102
+ """
103
+
104
+ def __init__(
105
+ self,
106
+ name: str,
107
+ command: str,
108
+ args: List[str],
109
+ env: Optional[Dict[str, str]] = None,
110
+ description: str = "",
111
+ timeout: float = 30.0,
112
+ ):
113
+ self.name = name
114
+ self.command = command
115
+ self.args = args
116
+ self.env = env or {}
117
+ self.description = description
118
+ self.timeout = timeout
119
+
120
+ self._proc: Optional[asyncio.subprocess.Process] = None
121
+ self._req_id: int = 0
122
+ self._pending: Dict[int, asyncio.Future] = {}
123
+ self._reader_task: Optional[asyncio.Task] = None
124
+ self._tools: List[Dict[str, Any]] = []
125
+ self._running = False
126
+
127
+ # ── internal ───────────────────────────────────────────────────────────
128
+
129
+ def _next_id(self) -> int:
130
+ self._req_id += 1
131
+ return self._req_id
132
+
133
+ async def _send(self, data: bytes):
134
+ if self._proc and self._proc.stdin:
135
+ self._proc.stdin.write(data)
136
+ await self._proc.stdin.drain()
137
+
138
+ async def _reader_loop(self):
139
+ """Background task: read lines from server stdout and dispatch to futures."""
140
+ if not self._proc or not self._proc.stdout:
141
+ return
142
+ try:
143
+ async for raw in self._proc.stdout:
144
+ line = raw.decode("utf-8", errors="ignore").strip()
145
+ if not line:
146
+ continue
147
+ try:
148
+ msg = json.loads(line)
149
+ except json.JSONDecodeError:
150
+ logger.debug("[MCP:%s] Non-JSON: %s", self.name, line[:100])
151
+ continue
152
+
153
+ req_id = msg.get("id")
154
+ if req_id is not None and req_id in self._pending:
155
+ fut = self._pending.pop(req_id)
156
+ if not fut.done():
157
+ if "error" in msg:
158
+ fut.set_exception(RuntimeError(str(msg["error"])))
159
+ else:
160
+ fut.set_result(msg.get("result"))
161
+ except asyncio.CancelledError:
162
+ pass
163
+ except Exception as exc:
164
+ logger.debug("[MCP:%s] Reader loop error: %s", self.name, exc)
165
+
166
+ async def _request(self, method: str, params: Any = None) -> Any:
167
+ rid = self._next_id()
168
+ fut = asyncio.get_event_loop().create_future()
169
+ self._pending[rid] = fut
170
+ await self._send(_make_request(method, params, rid))
171
+ try:
172
+ return await asyncio.wait_for(fut, timeout=self.timeout)
173
+ except asyncio.TimeoutError:
174
+ self._pending.pop(rid, None)
175
+ raise TimeoutError(f"MCP {self.name}/{method} timed out after {self.timeout}s")
176
+
177
+ # ── public API ─────────────────────────────────────────────────────────
178
+
179
+ async def start(self) -> bool:
180
+ """Start the server subprocess and perform MCP handshake."""
181
+ if self._running:
182
+ return True
183
+ try:
184
+ env = {**os.environ, **self.env}
185
+ self._proc = await asyncio.create_subprocess_exec(
186
+ self.command, *self.args,
187
+ stdin=asyncio.subprocess.PIPE,
188
+ stdout=asyncio.subprocess.PIPE,
189
+ stderr=asyncio.subprocess.PIPE,
190
+ env=env,
191
+ )
192
+ except FileNotFoundError as exc:
193
+ logger.warning("[MCP:%s] Command not found: %s", self.name, exc)
194
+ return False
195
+ except Exception as exc:
196
+ logger.warning("[MCP:%s] Failed to start: %s", self.name, exc)
197
+ return False
198
+
199
+ self._reader_task = asyncio.create_task(self._reader_loop())
200
+
201
+ # MCP initialize handshake
202
+ try:
203
+ await self._request("initialize", {
204
+ "protocolVersion": MCP_PROTOCOL_VERSION,
205
+ "capabilities": {"tools": {}},
206
+ "clientInfo": {"name": "aria-code", "version": "3.0"},
207
+ })
208
+ await self._send(_make_notification("notifications/initialized"))
209
+ except Exception as exc:
210
+ logger.warning("[MCP:%s] Handshake failed: %s", self.name, exc)
211
+ await self.stop()
212
+ return False
213
+
214
+ # Discover tools
215
+ await self._refresh_tools()
216
+ self._running = True
217
+ logger.info("[MCP:%s] Started with %d tools", self.name, len(self._tools))
218
+ return True
219
+
220
+ async def _refresh_tools(self):
221
+ try:
222
+ result = await self._request("tools/list")
223
+ self._tools = result.get("tools", []) if result else []
224
+ except Exception as exc:
225
+ logger.debug("[MCP:%s] tools/list failed: %s", self.name, exc)
226
+ self._tools = []
227
+
228
+ @property
229
+ def tools(self) -> List[Dict[str, Any]]:
230
+ return self._tools
231
+
232
+ async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
233
+ """Call a tool on this server. Returns {"success": bool, "result": …}."""
234
+ if not self._running:
235
+ return {"success": False, "error": f"MCP server {self.name!r} not running"}
236
+ # Strip server prefix if present (e.g. "quant_engine/backtest_strategy")
237
+ short_name = tool_name.split("/")[-1]
238
+ try:
239
+ result = await self._request("tools/call", {
240
+ "name": short_name,
241
+ "arguments": arguments,
242
+ })
243
+ if result is None:
244
+ return {"success": True, "result": None}
245
+ # MCP returns {content: [{type, text}]} or plain value
246
+ content = result.get("content", result)
247
+ if isinstance(content, list):
248
+ parts = [c.get("text", "") if isinstance(c, dict) else str(c) for c in content]
249
+ text = "\n".join(parts)
250
+ try:
251
+ parsed = json.loads(text)
252
+ return {"success": True, **parsed} if isinstance(parsed, dict) else {"success": True, "result": parsed}
253
+ except Exception:
254
+ return {"success": True, "result": text}
255
+ return {"success": True, "result": content}
256
+ except Exception as exc:
257
+ return {"success": False, "error": str(exc)}
258
+
259
+ @property
260
+ def is_alive(self) -> bool:
261
+ """Process exists and has not exited (handshake state tracked separately)."""
262
+ return self._proc is not None and self._proc.returncode is None
263
+
264
+ async def restart(self) -> bool:
265
+ """Tear down and re-launch the subprocess (reconnect policy)."""
266
+ await self.stop()
267
+ return await self.start()
268
+
269
+ async def stop(self):
270
+ self._running = False
271
+ if self._reader_task:
272
+ self._reader_task.cancel()
273
+ try:
274
+ await asyncio.wait_for(self._reader_task, timeout=1.0)
275
+ except Exception:
276
+ pass
277
+ self._reader_task = None
278
+ if self._proc:
279
+ try:
280
+ self._proc.stdin.close()
281
+ await asyncio.wait_for(self._proc.wait(), timeout=3.0)
282
+ except Exception:
283
+ try:
284
+ self._proc.kill()
285
+ except Exception:
286
+ pass
287
+ self._proc = None
288
+
289
+
290
+ # ---------------------------------------------------------------------------
291
+ # MCPToolRegistry — manages multiple servers + tool registration
292
+ # ---------------------------------------------------------------------------
293
+
294
+ class MCPToolRegistry:
295
+ """
296
+ Loads MCP server config from ``~/.arthera/mcp_servers.json``, starts
297
+ each server, discovers tools, and registers them in the CLI's tool loop.
298
+
299
+ Internal naming convention: ``{server_name}/{tool_name}``.
300
+ Model-facing schemas use ``mcp__{server_name}__{tool_name}`` because tool
301
+ APIs reject slashes in function names.
302
+ This avoids collisions when two servers expose a tool with the same name.
303
+ """
304
+
305
+ def __init__(self, config_path: pathlib.Path = MCP_CONFIG_PATH):
306
+ self.config_path = config_path
307
+ self._servers: Dict[str, MCPServer] = {}
308
+ self._tool_map: Dict[str, Tuple[str, str]] = {} # qualified_name → (server_name, tool_name)
309
+ self._circuits: Dict[str, "ServerCircuit"] = {} # per-server failure isolation
310
+ self._loaded = False
311
+ self._event_loop: Optional[asyncio.AbstractEventLoop] = None
312
+
313
+ # ── config ─────────────────────────────────────────────────────────────
314
+
315
+ def load_config(self) -> List[Dict[str, Any]]:
316
+ if not self.config_path.exists():
317
+ return []
318
+ try:
319
+ with open(self.config_path) as f:
320
+ data = json.load(f)
321
+ return data.get("servers", [])
322
+ except Exception as exc:
323
+ logger.warning("MCP config load failed: %s", exc)
324
+ return []
325
+
326
+ def save_example_config(self):
327
+ """Write an example mcp_servers.json if none exists."""
328
+ if self.config_path.exists():
329
+ return
330
+ self.config_path.parent.mkdir(parents=True, exist_ok=True)
331
+ example = {
332
+ "servers": [
333
+ {
334
+ "name": "quant_engine",
335
+ "command": "python3",
336
+ "args": ["path/to/mcp_server.py"],
337
+ "env": {"PYTHONPATH": "path/to/project"},
338
+ "description": "Your Arthera quant engine (backtest, signals, factors)"
339
+ },
340
+ {
341
+ "name": "apify",
342
+ "command": "npx",
343
+ "args": ["-y", "@apify/actors-mcp-server@latest", "--tools", "actors,docs,apify/rag-web-browser"],
344
+ "env": {"APIFY_TOKEN": "${APIFY_TOKEN}"},
345
+ "enabled": False,
346
+ "description": "Apify web research (review Actor terms and enable explicitly)"
347
+ }
348
+ ]
349
+ }
350
+ with open(self.config_path, "w") as f:
351
+ json.dump(example, f, indent=2, ensure_ascii=False)
352
+
353
+ # ── lifecycle ──────────────────────────────────────────────────────────
354
+
355
+ async def start_all(self) -> Dict[str, bool]:
356
+ """Start all configured MCP servers. Returns {name: started_ok}."""
357
+ self._event_loop = asyncio.get_running_loop()
358
+ server_configs = self.load_config()
359
+ results = {}
360
+ for cfg in server_configs:
361
+ name = cfg.get("name", "unnamed")
362
+ command = cfg.get("command", "")
363
+ args = cfg.get("args", [])
364
+ env = cfg.get("env", {})
365
+ desc = cfg.get("description", "")
366
+ if not command:
367
+ continue
368
+ if not cfg.get("enabled", True):
369
+ logger.debug("MCP server %r disabled in config, skipping", name)
370
+ continue
371
+ # expand ${VAR} placeholders in args and env values
372
+ def _expand(s: str) -> str:
373
+ import re as _re
374
+ return _re.sub(r'\$\{(\w+)\}', lambda m: os.environ.get(m.group(1), m.group(0)), s)
375
+ args = [_expand(a) for a in args]
376
+ env = {k: _expand(v) for k, v in env.items() if v}
377
+ srv = MCPServer(name=name, command=command, args=args, env=env, description=desc)
378
+ ok = await srv.start()
379
+ if ok:
380
+ self._servers[name] = srv
381
+ for tool in srv.tools:
382
+ qualified = f"{name}/{tool['name']}"
383
+ self._tool_map[qualified] = (name, tool["name"])
384
+ results[name] = ok
385
+ self._loaded = True
386
+ return results
387
+
388
+ async def stop_all(self):
389
+ for srv in self._servers.values():
390
+ await srv.stop()
391
+ self._servers.clear()
392
+ self._tool_map.clear()
393
+ self._loaded = False
394
+ self._event_loop = None
395
+
396
+ # ── tool info ──────────────────────────────────────────────────────────
397
+
398
+ def all_tools(self) -> List[Dict[str, Any]]:
399
+ """Return all discovered tools with qualified names."""
400
+ result = []
401
+ for srv_name, srv in self._servers.items():
402
+ for tool in srv.tools:
403
+ t = dict(tool)
404
+ t["qualified_name"] = f"{srv_name}/{tool['name']}"
405
+ t["server"] = srv_name
406
+ result.append(t)
407
+ return result
408
+
409
+ # ── call ───────────────────────────────────────────────────────────────
410
+
411
+ def _circuit_for(self, srv_name: str) -> ServerCircuit:
412
+ circuit = self._circuits.get(srv_name)
413
+ if circuit is None:
414
+ circuit = self._circuits[srv_name] = ServerCircuit()
415
+ return circuit
416
+
417
+ async def call_tool(self, qualified_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
418
+ if "/" in qualified_name:
419
+ srv_name, tool_name = qualified_name.split("/", 1)
420
+ else:
421
+ # Try to find by short name
422
+ srv_name, tool_name = self._resolve_short_name(qualified_name)
423
+
424
+ srv = self._servers.get(srv_name)
425
+ if srv is None:
426
+ return {"success": False, "error": f"MCP server {srv_name!r} not found"}
427
+
428
+ # Per-server failure isolation: a wedged/dead server fails fast instead
429
+ # of costing every call its full request timeout for the whole session.
430
+ circuit = self._circuit_for(srv_name)
431
+ if not circuit.allow_call():
432
+ return {
433
+ "success": False,
434
+ "error": (
435
+ f"MCP server {srv_name!r} temporarily disabled after repeated "
436
+ f"failures ({circuit.describe()}). It will be probed again "
437
+ f"automatically; /mcp reload {srv_name} forces a restart now."
438
+ ),
439
+ "circuit": circuit.state,
440
+ }
441
+
442
+ # Reconnect policy: if the subprocess died, use this (probe) call to
443
+ # relaunch it before dispatching.
444
+ if not srv.is_alive:
445
+ logger.info("[MCP:%s] Process dead — attempting restart", srv_name)
446
+ if not await srv.restart():
447
+ circuit.record_failure()
448
+ return {
449
+ "success": False,
450
+ "error": f"MCP server {srv_name!r} is down and restart failed",
451
+ "circuit": circuit.state,
452
+ }
453
+
454
+ result = await srv.call_tool(tool_name, arguments)
455
+
456
+ # 上游限流(429/too many requests):短退避后重试一次。
457
+ # 与 market_data_client 的报价路径同策略,MCP 工具在此统一收口。
458
+ if not result.get("success") and self._looks_rate_limited(result):
459
+ logger.warning(
460
+ "[MCP:%s] %s rate-limited upstream; retrying once in 3s",
461
+ srv_name, tool_name,
462
+ )
463
+ await asyncio.sleep(3.0)
464
+ result = await srv.call_tool(tool_name, arguments)
465
+
466
+ if result.get("success"):
467
+ circuit.record_success()
468
+ elif self._looks_rate_limited(result):
469
+ # 限流是上游数据源配额问题,不是本服务器故障——不计入熔断,
470
+ # 否则一波限流会把整个服务器禁用,殃及无关工具。错误仍如实上抛。
471
+ pass
472
+ else:
473
+ circuit.record_failure()
474
+ return result
475
+
476
+ @staticmethod
477
+ def _looks_rate_limited(result: Dict[str, Any]) -> bool:
478
+ text = str(result.get("error") or result.get("result") or "").lower()
479
+ return any(t in text for t in ("rate limit", "rate-limit", "rate limited", "too many requests", "429"))
480
+
481
+ async def reload_server(self, srv_name: str) -> bool:
482
+ """Force stop/start of one server and reset its circuit (reload policy)."""
483
+ srv = self._servers.get(srv_name)
484
+ if srv is None:
485
+ return False
486
+ ok = await srv.restart()
487
+ if ok:
488
+ self._circuit_for(srv_name).record_success()
489
+ self._tool_map = {q: st for q, st in self._tool_map.items() if st[0] != srv_name}
490
+ for tool in srv.tools:
491
+ self._tool_map[f"{srv_name}/{tool['name']}"] = (srv_name, tool["name"])
492
+ return ok
493
+
494
+ def _resolve_short_name(self, name: str) -> Tuple[str, str]:
495
+ for qname, (srv, tool) in self._tool_map.items():
496
+ if tool == name:
497
+ return srv, tool
498
+ return "unknown", name
499
+
500
+ # ── registration ───────────────────────────────────────────────────────
501
+
502
+ def register_into(
503
+ self,
504
+ tool_registry: Dict,
505
+ schema_registry: List,
506
+ overwrite: bool = False,
507
+ ) -> int:
508
+ """
509
+ Add all MCP tools into the CLI's LOCAL_TOOLS and LOCAL_TOOL_SCHEMAS.
510
+
511
+ Model-facing tool names use ``mcp__server__tool`` while handlers retain
512
+ the MCP server's internal ``server/tool`` qualified name.
513
+
514
+ Returns number of tools registered.
515
+ """
516
+ import asyncio as _asyncio
517
+
518
+ added = 0
519
+ existing_names = set(tool_registry.keys())
520
+
521
+ for srv_name, srv in self._servers.items():
522
+ for tool in srv.tools:
523
+ qname = f"{srv_name}/{tool['name']}"
524
+ model_name = model_safe_tool_name(srv_name, tool["name"])
525
+ if model_name in existing_names and not overwrite:
526
+ continue
527
+
528
+ # ToolExecutor invokes synchronous handlers in a worker thread.
529
+ # Route the coroutine back to the event loop that owns the MCP
530
+ # subprocess streams; asyncio.run() in the worker creates a
531
+ # different loop and fails with cross-loop Future errors.
532
+ def _make_sync_handler(qn: str) -> Callable:
533
+ def _handler(params: dict) -> dict:
534
+ try:
535
+ current_loop = _asyncio.get_running_loop()
536
+ except RuntimeError:
537
+ current_loop = None
538
+ owner_loop = self._event_loop
539
+ if owner_loop and owner_loop.is_running():
540
+ if current_loop is owner_loop:
541
+ return {
542
+ "success": False,
543
+ "error": "MCP sync handler called on its owner event loop; use ToolExecutor.execute",
544
+ }
545
+ future = _asyncio.run_coroutine_threadsafe(
546
+ self.call_tool(qn, params), owner_loop
547
+ )
548
+ return future.result(timeout=60)
549
+ return _asyncio.run(self.call_tool(qn, params))
550
+ return _handler
551
+
552
+ tool_registry[model_name] = (
553
+ _make_sync_handler(qname),
554
+ tool.get("description", f"MCP tool from {srv_name}"),
555
+ )
556
+ existing_names.add(model_name)
557
+ added += 1
558
+
559
+ # Build schema
560
+ input_schema = tool.get("inputSchema") or tool.get("parameters") or {
561
+ "type": "object", "properties": {}, "required": []
562
+ }
563
+ schema = {
564
+ "type": "function",
565
+ "function": {
566
+ "name": model_name,
567
+ "description": f"[{srv_name}] {tool.get('description', '')}",
568
+ "parameters": input_schema,
569
+ },
570
+ }
571
+ # Replace in place rather than append. The handler dict above
572
+ # is keyed by name so re-registering is idempotent there, but
573
+ # the schema list is not — reconnecting MCP (which core_cmds
574
+ # does with overwrite=True) appended a second copy of every
575
+ # tool. Most providers tolerate that and silently spend the
576
+ # context; Vertex rejects the whole request with "Duplicate
577
+ # function declaration found".
578
+ for index, existing in enumerate(schema_registry):
579
+ if (existing.get("function") or existing).get("name") == model_name:
580
+ schema_registry[index] = schema
581
+ break
582
+ else:
583
+ schema_registry.append(schema)
584
+
585
+ return added
586
+
587
+ # ── async call tool (for use inside async context) ─────────────────────
588
+
589
+ async def call_tool_async(
590
+ self,
591
+ qualified_name: str,
592
+ arguments: Dict[str, Any],
593
+ ) -> Dict[str, Any]:
594
+ return await self.call_tool(qualified_name, arguments)
595
+
596
+ # ── status ─────────────────────────────────────────────────────────────
597
+
598
+ def status(self) -> List[Dict[str, Any]]:
599
+ return [
600
+ {
601
+ "name": name,
602
+ "running": srv._running,
603
+ "alive": srv.is_alive,
604
+ "circuit": self._circuit_for(name).describe(),
605
+ "tool_count": len(srv.tools),
606
+ "description": srv.description,
607
+ "tools": [t["name"] for t in srv.tools],
608
+ }
609
+ for name, srv in self._servers.items()
610
+ ]
611
+
612
+
613
+ # ---------------------------------------------------------------------------
614
+ # Module-level singleton (lazy-initialized)
615
+ # ---------------------------------------------------------------------------
616
+
617
+ _registry: Optional[MCPToolRegistry] = None
618
+
619
+
620
+ def get_registry() -> MCPToolRegistry:
621
+ global _registry
622
+ if _registry is None:
623
+ _registry = MCPToolRegistry()
624
+ return _registry
625
+
626
+
627
+ async def init_mcp(tool_registry: Dict, schema_registry: List) -> Dict[str, bool]:
628
+ """
629
+ Convenience function: load config, start all servers, register tools.
630
+ Call once at startup.
631
+
632
+ Returns {server_name: started_ok} dict.
633
+ """
634
+ reg = get_registry()
635
+ results = await reg.start_all()
636
+ n = reg.register_into(tool_registry, schema_registry)
637
+ if n > 0:
638
+ logger.info("MCP: registered %d tools from %d servers", n, len(results))
639
+ return results