aria-code 4.1.3__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 (284) hide show
  1. agents/__init__.py +32 -0
  2. agents/base.py +190 -0
  3. agents/deep/__init__.py +37 -0
  4. agents/deep/calibration_loop.py +144 -0
  5. agents/deep/critic.py +125 -0
  6. agents/deep/deepen.py +193 -0
  7. agents/deep/models.py +149 -0
  8. agents/deep/pipeline.py +164 -0
  9. agents/deep/quant_fusion.py +192 -0
  10. agents/deep/themes.py +95 -0
  11. agents/deep/tiers.py +106 -0
  12. agents/financial/__init__.py +10 -0
  13. agents/financial/catalyst.py +279 -0
  14. agents/financial/debate.py +145 -0
  15. agents/financial/earnings.py +303 -0
  16. agents/financial/fundamental.py +159 -0
  17. agents/financial/macro.py +99 -0
  18. agents/financial/news.py +207 -0
  19. agents/financial/risk.py +132 -0
  20. agents/financial/sector.py +279 -0
  21. agents/financial/synthesis.py +274 -0
  22. agents/financial/technical.py +258 -0
  23. agents/portfolio_agent.py +333 -0
  24. agents/realty/__init__.py +62 -0
  25. agents/realty/asset_diagnosis.py +150 -0
  26. agents/realty/business_match.py +165 -0
  27. agents/realty/cashflow_verify.py +208 -0
  28. agents/realty/contract_rules.py +209 -0
  29. agents/realty/energy_anomaly.py +188 -0
  30. agents/realty/exit_settlement.py +207 -0
  31. agents/realty/fulfillment_risk.py +205 -0
  32. agents/realty/ops_optimize.py +159 -0
  33. agents/realty/revenue_share.py +214 -0
  34. agents/registry.py +144 -0
  35. agents/sports/__init__.py +0 -0
  36. agents/sports/football_agent.py +169 -0
  37. agents/team.py +289 -0
  38. aliyun_data_client.py +660 -0
  39. apps/README.md +12 -0
  40. apps/__init__.py +2 -0
  41. apps/channels/README.md +15 -0
  42. apps/cli/README.md +13 -0
  43. apps/cli/__init__.py +2 -0
  44. apps/cli/bootstrap.py +99 -0
  45. apps/cli/codegen_paths.py +29 -0
  46. apps/cli/commands/__init__.py +16 -0
  47. apps/cli/commands/analysis_cmds.py +288 -0
  48. apps/cli/commands/backtest_cmds.py +1887 -0
  49. apps/cli/commands/broker_cmds.py +1154 -0
  50. apps/cli/commands/business_workflow_cmds.py +289 -0
  51. apps/cli/commands/catalog.py +84 -0
  52. apps/cli/commands/data_cmds.py +405 -0
  53. apps/cli/commands/diagnostic_cmds.py +179 -0
  54. apps/cli/commands/diagnostic_ops_cmds.py +696 -0
  55. apps/cli/commands/finance_render.py +12 -0
  56. apps/cli/commands/market.py +399 -0
  57. apps/cli/commands/market_cmds.py +1276 -0
  58. apps/cli/commands/market_context.py +425 -0
  59. apps/cli/commands/market_render.py +7 -0
  60. apps/cli/commands/model_cmds.py +1579 -0
  61. apps/cli/commands/ops_cmds.py +668 -0
  62. apps/cli/commands/portfolio_cmds.py +962 -0
  63. apps/cli/commands/report.py +377 -0
  64. apps/cli/commands/scaffold_templates.py +617 -0
  65. apps/cli/commands/session_cmds.py +179 -0
  66. apps/cli/commands/session_ux_cmds.py +280 -0
  67. apps/cli/commands/team.py +588 -0
  68. apps/cli/commands/team_render.py +8 -0
  69. apps/cli/commands/ui_cmds.py +358 -0
  70. apps/cli/commands/workflow_cmds.py +279 -0
  71. apps/cli/commands/workspace_cmds.py +1414 -0
  72. apps/cli/config_paths.py +70 -0
  73. apps/cli/config_store.py +61 -0
  74. apps/cli/deterministic.py +122 -0
  75. apps/cli/direct.py +48 -0
  76. apps/cli/github_app_auth.py +135 -0
  77. apps/cli/handlers/__init__.py +11 -0
  78. apps/cli/handlers/broker_handlers.py +122 -0
  79. apps/cli/handlers/chart_handlers.py +1309 -0
  80. apps/cli/handlers/market_handlers.py +2509 -0
  81. apps/cli/handlers/realty_handlers.py +114 -0
  82. apps/cli/handlers/strategy_advice.py +82 -0
  83. apps/cli/hooks.py +180 -0
  84. apps/cli/i18n.py +284 -0
  85. apps/cli/intent.py +136 -0
  86. apps/cli/intent_router.py +217 -0
  87. apps/cli/lifecycle_hooks.py +48 -0
  88. apps/cli/main.py +29 -0
  89. apps/cli/market_metadata.py +135 -0
  90. apps/cli/market_universe.py +265 -0
  91. apps/cli/message_processing.py +257 -0
  92. apps/cli/plan_mode.py +139 -0
  93. apps/cli/plotly_html.py +15 -0
  94. apps/cli/prediction_feedback.py +202 -0
  95. apps/cli/preflight.py +497 -0
  96. apps/cli/project_aria.py +60 -0
  97. apps/cli/prompts/__init__.py +0 -0
  98. apps/cli/prompts/coding.py +658 -0
  99. apps/cli/prompts/system_prompts.py +531 -0
  100. apps/cli/prompts/ui.py +434 -0
  101. apps/cli/providers/__init__.py +1 -0
  102. apps/cli/providers/base.py +271 -0
  103. apps/cli/providers/chat_routing.py +80 -0
  104. apps/cli/providers/llm/__init__.py +1 -0
  105. apps/cli/providers/llm/ollama_stream.py +1170 -0
  106. apps/cli/providers/llm/sse_stream.py +216 -0
  107. apps/cli/providers/runtime_bridge.py +185 -0
  108. apps/cli/runtime_consumer.py +489 -0
  109. apps/cli/session_export.py +87 -0
  110. apps/cli/session_jsonl.py +207 -0
  111. apps/cli/session_store.py +112 -0
  112. apps/cli/todo_tracker.py +190 -0
  113. apps/cli/tools/__init__.py +40 -0
  114. apps/cli/tools/context.py +46 -0
  115. apps/cli/tools/file_tools.py +112 -0
  116. apps/cli/tools/market_tools.py +549 -0
  117. apps/cli/tools/notebook_tools.py +111 -0
  118. apps/cli/tools/system_tools.py +669 -0
  119. apps/cli/tools/write_tools.py +715 -0
  120. apps/cli/tradingview_bridge.py +434 -0
  121. apps/cli/update_check.py +152 -0
  122. apps/cli/utils/__init__.py +0 -0
  123. apps/cli/utils/market_detect.py +1578 -0
  124. apps/daemon/README.md +14 -0
  125. apps/vscode/README.md +115 -0
  126. apps/vscode/package.json +70 -0
  127. aria_cli.py +11636 -0
  128. aria_code-4.1.3.dist-info/METADATA +952 -0
  129. aria_code-4.1.3.dist-info/RECORD +284 -0
  130. aria_code-4.1.3.dist-info/WHEEL +5 -0
  131. aria_code-4.1.3.dist-info/entry_points.txt +2 -0
  132. aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
  133. aria_code-4.1.3.dist-info/top_level.txt +50 -0
  134. aria_daemon.py +1295 -0
  135. aria_feishu_bot.py +1359 -0
  136. aria_relay_client.py +182 -0
  137. aria_relay_server.py +405 -0
  138. aria_telegram_bot.py +202 -0
  139. ariarc.py +328 -0
  140. artifacts.py +491 -0
  141. backtest_report.py +472 -0
  142. brokers/__init__.py +72 -0
  143. brokers/base.py +207 -0
  144. brokers/capabilities.py +264 -0
  145. brokers/cn/__init__.py +10 -0
  146. brokers/cn/easytrader_broker.py +193 -0
  147. brokers/cn/futu_broker.py +194 -0
  148. brokers/cn/longbridge_broker.py +190 -0
  149. brokers/cn/tiger_broker.py +196 -0
  150. brokers/cn/xtquant_broker.py +175 -0
  151. brokers/config.py +364 -0
  152. brokers/intl/__init__.py +5 -0
  153. brokers/intl/alpaca_broker.py +183 -0
  154. brokers/intl/ibkr_broker.py +215 -0
  155. brokers/intl/webull_broker.py +156 -0
  156. brokers/paper_broker.py +259 -0
  157. brokers/planning.py +296 -0
  158. brokers/registry.py +181 -0
  159. brokers/trading.py +237 -0
  160. change_store.py +127 -0
  161. command_safety.py +19 -0
  162. computer_use_tools.py +504 -0
  163. dashboard_generator.py +578 -0
  164. data_analysis_tools.py +808 -0
  165. data_cleaner.py +483 -0
  166. data_service.py +481 -0
  167. datasources/__init__.py +23 -0
  168. datasources/base.py +166 -0
  169. datasources/router.py +221 -0
  170. datasources/sources/__init__.py +15 -0
  171. datasources/sources/akshare_source.py +269 -0
  172. datasources/sources/alpha_vantage_source.py +202 -0
  173. datasources/sources/edgar_source.py +218 -0
  174. datasources/sources/finnhub_source.py +197 -0
  175. datasources/sources/fred_source.py +219 -0
  176. datasources/sources/tushare_source.py +141 -0
  177. datasources/sources/web_scraper_source.py +278 -0
  178. datasources/sources/world_bank_source.py +205 -0
  179. datasources/sources/yfinance_source.py +152 -0
  180. demo_player.py +204 -0
  181. doctor.py +508 -0
  182. file_analysis_tools.py +734 -0
  183. finance_formulas.py +389 -0
  184. football_data_client.py +1670 -0
  185. intent_classifier.py +358 -0
  186. local_finance_tools.py +3221 -0
  187. local_llm_provider.py +552 -0
  188. macro_tools.py +368 -0
  189. market_data_client.py +1899 -0
  190. mcp_client.py +506 -0
  191. memory_manager.py +245 -0
  192. model_capability.py +416 -0
  193. notification_tools.py +248 -0
  194. packages/__init__.py +23 -0
  195. packages/aria_agents/__init__.py +5 -0
  196. packages/aria_agents/manifest.py +69 -0
  197. packages/aria_core/__init__.py +34 -0
  198. packages/aria_core/architecture.py +192 -0
  199. packages/aria_core/export.py +124 -0
  200. packages/aria_core/manifest.py +65 -0
  201. packages/aria_infra/__init__.py +15 -0
  202. packages/aria_infra/arthera.py +52 -0
  203. packages/aria_infra/doctor.py +246 -0
  204. packages/aria_infra/product.py +37 -0
  205. packages/aria_mcp/__init__.py +25 -0
  206. packages/aria_mcp/bridge.py +38 -0
  207. packages/aria_mcp/config.py +97 -0
  208. packages/aria_mcp/tools.py +61 -0
  209. packages/aria_sdk/__init__.py +19 -0
  210. packages/aria_sdk/client.py +396 -0
  211. packages/aria_sdk/providers.py +70 -0
  212. packages/aria_sdk/streaming.py +73 -0
  213. packages/aria_sdk/types.py +86 -0
  214. packages/aria_services/__init__.py +55 -0
  215. packages/aria_services/context.py +258 -0
  216. packages/aria_services/data.py +11 -0
  217. packages/aria_services/provider_health.py +189 -0
  218. packages/aria_services/registry.py +213 -0
  219. packages/aria_services/usage.py +138 -0
  220. packages/aria_skills/__init__.py +5 -0
  221. packages/aria_skills/registry.py +59 -0
  222. packages/aria_tools/__init__.py +5 -0
  223. packages/aria_tools/registry.py +128 -0
  224. packages/quant_engine/__init__.py +6 -0
  225. packages/quant_engine/sports/__init__.py +72 -0
  226. packages/quant_engine/sports/calibrator.py +353 -0
  227. packages/quant_engine/sports/dixon_coles.py +234 -0
  228. packages/quant_engine/sports/elo.py +299 -0
  229. packages/quant_engine/sports/form.py +188 -0
  230. packages/quant_engine/sports/h2h.py +195 -0
  231. packages/quant_engine/sports/ml_model.py +354 -0
  232. packages/quant_engine/sports/predictor.py +311 -0
  233. packages/quant_engine/sports/tracker.py +664 -0
  234. packages/quant_engine/stochastic/__init__.py +27 -0
  235. packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
  236. packages/quant_engine/stochastic/ito_calculus.py +477 -0
  237. packages/quant_engine/stochastic/kelly_criterion.py +181 -0
  238. packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
  239. packages/quant_engine/stochastic/options_pricing.py +573 -0
  240. packages/quant_engine/stochastic/stochastic_processes.py +90 -0
  241. plan_utils.py +194 -0
  242. plugin_loader.py +328 -0
  243. portfolio_ledger.py +262 -0
  244. privacy/__init__.py +5 -0
  245. privacy/feedback.py +123 -0
  246. project_tools.py +525 -0
  247. providers/__init__.py +30 -0
  248. providers/llm/__init__.py +19 -0
  249. providers/llm/anthropic.py +184 -0
  250. providers/llm/base.py +139 -0
  251. providers/llm/ollama.py +128 -0
  252. providers/llm/openai_compat.py +282 -0
  253. providers/llm/registry.py +358 -0
  254. realty_data_tools.py +659 -0
  255. report_generator.py +1314 -0
  256. runtime/__init__.py +103 -0
  257. runtime/agent_loop.py +1183 -0
  258. runtime/approval.py +51 -0
  259. runtime/events.py +102 -0
  260. runtime/gateway.py +128 -0
  261. runtime/lsp.py +346 -0
  262. runtime/subagent.py +258 -0
  263. runtime/tool_executor.py +104 -0
  264. runtime/tool_policy.py +106 -0
  265. safety/__init__.py +21 -0
  266. safety/permissions.py +275 -0
  267. setup_wizard.py +653 -0
  268. strategy_vault.py +420 -0
  269. ui/__init__.py +100 -0
  270. ui/banner.py +310 -0
  271. ui/completer.py +391 -0
  272. ui/console.py +271 -0
  273. ui/image_render.py +243 -0
  274. ui/input_box.py +376 -0
  275. ui/picker.py +195 -0
  276. ui/render/__init__.py +11 -0
  277. ui/render/finance.py +1480 -0
  278. ui/render/market.py +225 -0
  279. ui/render/output.py +681 -0
  280. ui/render/team.py +346 -0
  281. ui/robot.py +235 -0
  282. workspace/__init__.py +6 -0
  283. workspace/files.py +170 -0
  284. workspace/verify.py +113 -0
@@ -0,0 +1,216 @@
1
+ """stream_chat — Aria cloud SSE provider extracted from aria_cli.py.
2
+
3
+ Streams AI responses from the Aria backend via Server-Sent Events (SSE).
4
+ Supports cancellation, thinking tokens, tool calls, retries, and usage stats.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import asyncio
9
+ import json
10
+ from typing import Callable, Optional
11
+
12
+
13
+ async def stream_chat(
14
+ base_url: str,
15
+ message: str,
16
+ history: list,
17
+ model: str = "qwen2.5:7b",
18
+ thinking_mode: str = "auto",
19
+ user_context: Optional[dict] = None,
20
+ auth_token: Optional[str] = None,
21
+ on_token: Optional[Callable[[str], None]] = None,
22
+ on_thinking: Optional[Callable[[str], None]] = None,
23
+ on_tool_call: Optional[Callable[[str, dict], None]] = None,
24
+ on_tool_result: Optional[Callable[[str, str], None]] = None,
25
+ on_status: Optional[Callable[[str, str], None]] = None,
26
+ cancel_event: Optional[asyncio.Event] = None,
27
+ project_context: str = "",
28
+ ) -> dict:
29
+ """Stream AI chat via SSE with cancel support and user context.
30
+
31
+ Parameters
32
+ ----------
33
+ project_context:
34
+ ARIA.md / CLAUDE.md content to inject into user_context.
35
+ Callers pass ``_PROJECT_CONTEXT`` from aria_cli, keeping this
36
+ module free of global state.
37
+ """
38
+ import aiohttp
39
+
40
+ url = f"{base_url}/api/v2/ai/chat/stream"
41
+
42
+ payload: dict = {
43
+ "message": message,
44
+ "conversation_history": history[-20:],
45
+ "model": model,
46
+ "thinking_mode": thinking_mode,
47
+ "stream": True,
48
+ }
49
+ if user_context:
50
+ if project_context:
51
+ user_context = {**user_context, "project_context": project_context}
52
+ payload["user_context"] = user_context
53
+
54
+ headers: dict = {}
55
+ if auth_token:
56
+ headers["Authorization"] = f"Bearer {auth_token}"
57
+
58
+ full_response = ""
59
+ thinking_content = ""
60
+ tools_used: list = []
61
+ sources: list = []
62
+ tool_calls_pending: list = []
63
+ usage: dict = {"prompt_tokens": 0, "completion_tokens": 0, "thinking_tokens": 0}
64
+
65
+ _max_connect_retries = 2
66
+ _last_connect_error: Optional[str] = None
67
+
68
+ for _attempt in range(_max_connect_retries + 1):
69
+ if cancel_event and cancel_event.is_set():
70
+ return {
71
+ "success": True, "response": "", "cancelled": True,
72
+ "tools_used": [], "sources": [], "usage": usage,
73
+ }
74
+ # Reset per-attempt accumulators
75
+ full_response = ""
76
+ thinking_content = ""
77
+ tools_used = []
78
+ sources = []
79
+ tool_calls_pending = []
80
+ usage = {"prompt_tokens": 0, "completion_tokens": 0, "thinking_tokens": 0}
81
+
82
+ try:
83
+ async with aiohttp.ClientSession() as session:
84
+ async with session.post(
85
+ url, json=payload, headers=headers,
86
+ timeout=aiohttp.ClientTimeout(total=120),
87
+ ) as resp:
88
+ if resp.status != 200:
89
+ error_text = await resp.text()
90
+ return {"success": False, "error": f"HTTP {resp.status}: {error_text[:200]}"}
91
+
92
+ buffer = ""
93
+ event_type = "delta"
94
+
95
+ async for chunk in resp.content:
96
+ if cancel_event and cancel_event.is_set():
97
+ try:
98
+ await session.post(
99
+ f"{base_url}/api/v2/ai/chat/cancel",
100
+ headers=headers,
101
+ timeout=aiohttp.ClientTimeout(total=3),
102
+ )
103
+ except Exception:
104
+ pass
105
+ return {
106
+ "success": True, "response": full_response,
107
+ "cancelled": True, "tools_used": tools_used,
108
+ "sources": sources, "usage": usage,
109
+ }
110
+
111
+ text = chunk.decode("utf-8", errors="ignore")
112
+ buffer += text
113
+
114
+ while "\n" in buffer:
115
+ line, buffer = buffer.split("\n", 1)
116
+ line = line.strip()
117
+
118
+ if not line or line.startswith(":"):
119
+ continue
120
+ if line.startswith("event:"):
121
+ event_type = line[6:].strip()
122
+ continue
123
+ if not line.startswith("data:"):
124
+ continue
125
+
126
+ data_str = line[5:].strip()
127
+ if data_str == "[DONE]":
128
+ break
129
+ try:
130
+ data = json.loads(data_str)
131
+ except json.JSONDecodeError:
132
+ continue
133
+
134
+ # Backend error: {"success": false, "error": "..."}
135
+ if data.get("success") is False:
136
+ err_msg = data.get("error", "Backend error")
137
+ return {"success": False, "error": f"Backend: {err_msg}"}
138
+
139
+ evt = data.get("type", event_type)
140
+
141
+ if evt == "delta":
142
+ token = data.get("text", data.get("content", ""))
143
+ if token:
144
+ full_response += token
145
+ usage["completion_tokens"] += 1
146
+ if on_token:
147
+ on_token(token)
148
+
149
+ elif evt == "thinking_content":
150
+ tc = data.get("content", "")
151
+ if tc:
152
+ thinking_content += tc
153
+ usage["thinking_tokens"] += 1
154
+ if on_thinking:
155
+ on_thinking(tc)
156
+
157
+ elif evt == "tool_call":
158
+ tool = data.get("tool", data.get("name", ""))
159
+ params = data.get("params", {})
160
+ tools_used.append(tool)
161
+ tool_calls_pending.append({"tool": tool, "params": params})
162
+ if on_tool_call:
163
+ on_tool_call(tool, params)
164
+
165
+ elif evt == "tool_result":
166
+ if on_tool_result:
167
+ on_tool_result(data.get("tool", ""), data.get("summary", ""))
168
+
169
+ elif evt == "status":
170
+ if on_status:
171
+ on_status(data.get("state", ""), data.get("message", ""))
172
+
173
+ elif evt == "final":
174
+ full_response = data.get("answer", full_response)
175
+ sources = data.get("sources", [])
176
+ if data.get("usage"):
177
+ u = data["usage"]
178
+ usage["prompt_tokens"] = u.get("prompt_tokens", usage["prompt_tokens"])
179
+ usage["completion_tokens"] = u.get("completion_tokens", usage["completion_tokens"])
180
+
181
+ elif evt == "error":
182
+ return {"success": False, "error": data.get("message", "Unknown error")}
183
+
184
+ return {
185
+ "success": True,
186
+ "response": full_response,
187
+ "thinking": thinking_content,
188
+ "tools_used": tools_used,
189
+ "sources": sources,
190
+ "tool_calls_pending": tool_calls_pending,
191
+ "usage": usage,
192
+ }
193
+
194
+ except asyncio.TimeoutError:
195
+ return {"success": False, "error": "Request timed out (120s)"}
196
+ except asyncio.CancelledError:
197
+ return {
198
+ "success": True, "response": full_response, "cancelled": True,
199
+ "tools_used": tools_used, "sources": sources, "usage": usage,
200
+ }
201
+ except aiohttp.ClientConnectorError as exc:
202
+ _last_connect_error = str(exc)
203
+ if _attempt < _max_connect_retries:
204
+ wait = 1.5 * (_attempt + 1)
205
+ await asyncio.sleep(wait)
206
+ if on_status:
207
+ on_status("retry", f"Connection failed, retrying ({_attempt + 2}/{_max_connect_retries + 1})...")
208
+ continue
209
+ break
210
+ except Exception as exc:
211
+ return {"success": False, "error": str(exc)}
212
+
213
+ return {
214
+ "success": False,
215
+ "error": f"Connection failed after {_max_connect_retries + 1} attempts: {_last_connect_error}",
216
+ }
@@ -0,0 +1,185 @@
1
+ """Phase-3 bridge: run one chat turn through the shared ``runtime.run_agent``.
2
+
3
+ The documented runtime next step is to route the CLI tool loop through
4
+ ``run_agent`` and keep aria_cli as orchestration glue. This module is that engine
5
+ — the two adapters run_agent needs plus a thin driver:
6
+
7
+ • build_tool_executor() — wraps the CLI's ``LOCAL_TOOLS`` ({name: (handler, schema)})
8
+ • make_provider_fn() — selects the provider (chat_routing) + streams it
9
+ • run_chat_via_runtime() — drives run_agent, renders via callbacks, returns text
10
+
11
+ It is opt-in: ``send_message`` only uses it when ``config['use_runtime_loop']`` is
12
+ on, and falls back to the proven inline loop on any error. That keeps the live
13
+ path untouched until the runtime path is verified in a real REPL.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ from typing import Any, Callable, List, Optional
19
+
20
+ from .chat_routing import first_round_route, is_placeholder_response, should_fallback
21
+
22
+
23
+ def build_tool_executor(local_tools, config: Optional[dict] = None):
24
+ """Wrap the CLI's LOCAL_TOOLS registry for run_agent."""
25
+ from runtime.tool_executor import ToolExecutor
26
+ return ToolExecutor(local_tools, config=config or {})
27
+
28
+
29
+ async def run_with_fallback(
30
+ route: str,
31
+ *,
32
+ run_cloud: Callable,
33
+ run_ollama: Callable,
34
+ on_token: Optional[Callable[[str], None]] = None,
35
+ ) -> dict:
36
+ """Primary generation per ``route``, with cloud→Ollama fallback parity.
37
+
38
+ Mirrors ``send_message``'s inline fallback, but keyed on *route* (via
39
+ ``should_fallback``) so a genuinely-good forced-backend answer is kept
40
+ instead of being discarded and re-run:
41
+
42
+ • ``skip`` / ``ollama`` → run local Ollama directly (no cloud round)
43
+ • ``cloud`` → run cloud; if it fails or returns a placeholder
44
+ (empty / canned / backend stub), fall back to
45
+ local Ollama
46
+
47
+ ``run_cloud`` / ``run_ollama`` are async ``(on_token) -> result dict``
48
+ closures; injecting them keeps this orchestration unit-testable without
49
+ real providers or network.
50
+ """
51
+ if route in ("skip", "ollama"):
52
+ return await run_ollama(on_token)
53
+
54
+ # route == "cloud": count streamed tokens so a long-but-unstreamed canned
55
+ # backend reply is recognised as a placeholder, not a real generation.
56
+ _tokens = [0]
57
+
58
+ def _counting_on_token(tok: str) -> None:
59
+ _tokens[0] += 1
60
+ if on_token is not None:
61
+ on_token(tok)
62
+
63
+ result = await run_cloud(_counting_on_token)
64
+ if result.get("cancelled"):
65
+ return result # user-cancelled — never silently re-run on a different provider
66
+
67
+ placeholder = is_placeholder_response(result.get("response", ""), _tokens[0])
68
+ if should_fallback("cloud", result, is_placeholder=placeholder):
69
+ return await run_ollama(on_token)
70
+ return result
71
+
72
+
73
+ def make_provider_fn(
74
+ *,
75
+ model: str,
76
+ config: dict,
77
+ api_url: Optional[str],
78
+ ollama_url: str,
79
+ tool_schemas: List[dict],
80
+ thinking_mode: str = "auto",
81
+ user_context: Optional[dict] = None,
82
+ auth_token: Optional[str] = None,
83
+ project_context: Any = None,
84
+ system_override: Optional[str] = None,
85
+ ) -> Callable:
86
+ """Build an async ``provider_fn`` for run_agent.
87
+
88
+ Selects the provider per chat_routing (cloud → AriaSSE backend; ollama/skip →
89
+ local Ollama) and streams it through the shared ``stream_provider_result``.
90
+ A pending system-role override is threaded the same way ``send_message`` does
91
+ it: cloud via ``user_context['system_role_override']``, Ollama via the
92
+ provider's ``system_override`` argument.
93
+ """
94
+ from apps.cli.providers.base import AriaSSEProvider, OllamaProvider
95
+ from packages.aria_sdk.streaming import stream_provider_result
96
+
97
+ _cloud_uctx = dict(user_context or {})
98
+ if system_override:
99
+ _cloud_uctx["system_role_override"] = system_override
100
+
101
+ async def _provider_fn(prompt, history, *, on_token=None, on_thinking=None,
102
+ on_tool_call=None, on_tool_result=None, on_status=None,
103
+ cancel_event=None):
104
+ route = first_round_route(model, config, api_url)
105
+
106
+ async def _stream(provider, _on_token):
107
+ return await stream_provider_result(
108
+ provider, prompt, history, tools=tool_schemas,
109
+ cancel_event=cancel_event, on_token=_on_token, on_thinking=on_thinking,
110
+ on_tool_call=on_tool_call, on_tool_result=on_tool_result, on_status=on_status,
111
+ )
112
+
113
+ async def run_cloud(_on_token):
114
+ return await _stream(
115
+ AriaSSEProvider(
116
+ api_url, model, thinking_mode=thinking_mode,
117
+ user_context=_cloud_uctx, auth_token=auth_token,
118
+ project_context=project_context,
119
+ ),
120
+ _on_token,
121
+ )
122
+
123
+ async def run_ollama(_on_token):
124
+ return await _stream(
125
+ OllamaProvider(ollama_url, model, system_override=system_override),
126
+ _on_token,
127
+ )
128
+
129
+ return await run_with_fallback(
130
+ route, run_cloud=run_cloud, run_ollama=run_ollama, on_token=on_token,
131
+ )
132
+
133
+ return _provider_fn
134
+
135
+
136
+ async def run_chat_via_runtime(
137
+ *,
138
+ prompt: str,
139
+ history: list,
140
+ local_tools,
141
+ tool_schemas: List[dict],
142
+ model: str,
143
+ config: dict,
144
+ api_url: Optional[str],
145
+ ollama_url: str,
146
+ cancel_event=None,
147
+ on_token: Optional[Callable[[str], None]] = None,
148
+ on_thinking: Optional[Callable[[str], None]] = None,
149
+ on_tool_call: Optional[Callable[[str, dict], None]] = None,
150
+ on_tool_result: Optional[Callable[[str, dict], None]] = None,
151
+ on_status: Optional[Callable[[str, str], None]] = None,
152
+ thinking_mode: str = "auto",
153
+ user_context: Optional[dict] = None,
154
+ auth_token: Optional[str] = None,
155
+ project_context: Any = None,
156
+ system_override: Optional[str] = None,
157
+ max_rounds: int = 30,
158
+ ) -> str:
159
+ """Run one chat turn through the shared runtime Gateway; return the text.
160
+
161
+ This is the CLI *adapter* for ``runtime.gateway.run_turn``: it builds the
162
+ CLI's ``provider_fn`` (AriaSSE/Ollama selection + cloud→Ollama fallback) and
163
+ tool executor (the LOCAL_TOOLS registry), then hands them to the neutral
164
+ gateway, which drives ``run_agent`` and streams via the callbacks. Returns
165
+ the assistant response text ("" if none).
166
+ """
167
+ from runtime.gateway import run_turn
168
+
169
+ provider_fn = make_provider_fn(
170
+ model=model, config=config, api_url=api_url, ollama_url=ollama_url,
171
+ tool_schemas=tool_schemas, thinking_mode=thinking_mode,
172
+ user_context=user_context, auth_token=auth_token, project_context=project_context,
173
+ system_override=system_override,
174
+ )
175
+ executor = build_tool_executor(local_tools, config)
176
+
177
+ result = await run_turn(
178
+ prompt, history,
179
+ provider_fn=provider_fn, tool_executor=executor,
180
+ tool_schemas=list(tool_schemas),
181
+ on_token=on_token, on_thinking=on_thinking,
182
+ on_tool_call=on_tool_call, on_tool_result=on_tool_result, on_status=on_status,
183
+ cancel_event=cancel_event, max_rounds=max_rounds,
184
+ )
185
+ return result.text