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,489 @@
1
+ """CLI consumers for runtime/model events.
2
+
3
+ The runtime core emits typed events and asks for approval decisions. This
4
+ module keeps terminal-specific rendering and prompts at the CLI adapter layer
5
+ instead of letting them spread through the agent loop.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import re
11
+ import sys
12
+ import time
13
+ from typing import Any, Callable
14
+
15
+ from runtime import (
16
+ AgentEventStatus,
17
+ AgentEventThinking,
18
+ AgentEventToken,
19
+ AgentEventToolCall,
20
+ AgentEventToolResult,
21
+ ApprovalDecision,
22
+ )
23
+
24
+
25
+ _REPETITION_MARKER = "*[model stopped — repetition detected]*"
26
+ _REPETITION_NOTICE = (
27
+ "\n\n> 已检测到模型开始重复输出,已自动停止展开。"
28
+ "上方结果仍然有效;如需继续,请指定要补充的部分。"
29
+ )
30
+
31
+
32
+ class TerminalRuntimeEventConsumer:
33
+ """Consume runtime/provider events and render them to a terminal."""
34
+
35
+ def __init__(
36
+ self,
37
+ *,
38
+ terminal: Any,
39
+ console: Any,
40
+ has_rich: bool,
41
+ markdown_cls: type | None,
42
+ live_cls: type | None,
43
+ strip_latex: Callable[[str], str],
44
+ set_robot_state: Callable[[Any], None] | None = None,
45
+ streaming_state: Any = None,
46
+ print_tool_call: Callable[[str, dict], None] | None = None,
47
+ print_tool_done: Callable[[str, int, bool], None] | None = None,
48
+ fallback_from: str = "local",
49
+ live_update_interval: float = 0.08,
50
+ ) -> None:
51
+ self.terminal = terminal
52
+ self.console = console
53
+ self.has_rich = has_rich
54
+ self.markdown_cls = markdown_cls
55
+ self.live_cls = live_cls
56
+ self.strip_latex = strip_latex
57
+ self.set_robot_state = set_robot_state
58
+ self.streaming_state = streaming_state
59
+ self.print_tool_call = print_tool_call
60
+ self.print_tool_done = print_tool_done
61
+ self.fallback_from = fallback_from
62
+ self.live_update_interval = live_update_interval
63
+
64
+ self.response_text = ""
65
+ self.streamed_any = False
66
+ self.token_count = 0
67
+ self.thinking_tokens = 0
68
+ self.thinking_shown = False
69
+ self.thinking_start: float | None = None
70
+ self.thinking_finished = False
71
+ self.thinking_preview_buf: list[str] = []
72
+ self.thinking_full_buf: list[str] = []
73
+ self.tool_start_times: dict[str, float] = {}
74
+ self.repetition_stopped = False
75
+ self.repetition_notice_printed = False
76
+
77
+ self.live_display = None
78
+ self.spinner = None
79
+ self.first_token_received_ref = [False]
80
+ self.token_start_time: float | None = None
81
+ self.last_live_update = 0.0
82
+ self.use_plain_print_ref = [False]
83
+ self.use_batch_render_ref = [False]
84
+ self.latex_buf_ref = [""]
85
+ self.in_latex_ref = [False]
86
+
87
+ @property
88
+ def first_token_received(self) -> bool:
89
+ return self.first_token_received_ref[0]
90
+
91
+ @first_token_received.setter
92
+ def first_token_received(self, value: bool) -> None:
93
+ self.first_token_received_ref[0] = value
94
+
95
+ @property
96
+ def use_plain_print(self) -> bool:
97
+ return self.use_plain_print_ref[0]
98
+
99
+ @use_plain_print.setter
100
+ def use_plain_print(self, value: bool) -> None:
101
+ self.use_plain_print_ref[0] = value
102
+
103
+ @property
104
+ def use_batch_render(self) -> bool:
105
+ return self.use_batch_render_ref[0]
106
+
107
+ @use_batch_render.setter
108
+ def use_batch_render(self, value: bool) -> None:
109
+ self.use_batch_render_ref[0] = value
110
+
111
+ @property
112
+ def latex_buf(self) -> str:
113
+ return self.latex_buf_ref[0]
114
+
115
+ @latex_buf.setter
116
+ def latex_buf(self, value: str) -> None:
117
+ self.latex_buf_ref[0] = value
118
+
119
+ @property
120
+ def in_latex(self) -> bool:
121
+ return self.in_latex_ref[0]
122
+
123
+ @in_latex.setter
124
+ def in_latex(self, value: bool) -> None:
125
+ self.in_latex_ref[0] = value
126
+
127
+ def start_spinner(self) -> None:
128
+ if self.has_rich and self.spinner is None and not self.first_token_received:
129
+ self.spinner = self.console.status(
130
+ "[dim]思考中… [/dim][dim italic]esc 取消[/dim italic]",
131
+ spinner="dots",
132
+ spinner_style="dim",
133
+ )
134
+ self.spinner.__enter__()
135
+
136
+ def stop_spinner(self) -> None:
137
+ if self.spinner is not None:
138
+ try:
139
+ self.spinner.__exit__(None, None, None)
140
+ except Exception:
141
+ pass
142
+ self.spinner = None
143
+
144
+ def stop_live(self, discard: bool = False) -> None:
145
+ self.stop_spinner()
146
+ if self.live_display:
147
+ try:
148
+ if discard:
149
+ try:
150
+ from rich.text import Text as _RichText
151
+
152
+ self.live_display.update(_RichText(""))
153
+ self.live_display.refresh()
154
+ except Exception:
155
+ pass
156
+ self.live_display.stop()
157
+ except Exception:
158
+ pass
159
+ self.live_display = None
160
+ elif self.first_token_received and not discard and not self.use_batch_render:
161
+ print(flush=True)
162
+
163
+ def set_batch_render_mode(self, enabled: bool = True) -> None:
164
+ self.use_plain_print = enabled
165
+ self.use_batch_render = enabled
166
+
167
+ def reset_stream_state(self) -> None:
168
+ self.response_text = ""
169
+ self.streamed_any = False
170
+ self.token_count = 0
171
+ self.first_token_received = False
172
+ self.token_start_time = None
173
+ self.latex_buf = ""
174
+ self.in_latex = False
175
+
176
+ def flush_latex_buf(self) -> str:
177
+ raw = self.latex_buf
178
+ self.latex_buf = ""
179
+ self.in_latex = False
180
+ return self.strip_latex(raw) if raw.strip() else raw
181
+
182
+ def _show_repetition_notice(self) -> None:
183
+ if self.repetition_notice_printed or self.use_batch_render:
184
+ return
185
+ self.repetition_notice_printed = True
186
+ if self.live_display and self.has_rich and self.markdown_cls is not None:
187
+ clean_text = self.response_text.split(_REPETITION_MARKER, 1)[0].rstrip()
188
+ self.live_display.update(self.markdown_cls(self.strip_latex(clean_text + _REPETITION_NOTICE)))
189
+ self.live_display.refresh()
190
+ return
191
+ print(_REPETITION_NOTICE, end="", flush=True)
192
+
193
+ def finalize_text(self, final_text: str) -> str:
194
+ if self.in_latex and self.latex_buf:
195
+ leftover = self.flush_latex_buf()
196
+ final_text = (final_text or "") + leftover
197
+ if self.use_plain_print and not self.use_batch_render:
198
+ print(leftover, end="", flush=True)
199
+ return final_text
200
+
201
+ def _finish_thinking(self) -> None:
202
+ if not self.thinking_shown or self.thinking_finished:
203
+ return
204
+ self.thinking_finished = True
205
+ self.stop_spinner()
206
+ elapsed_t = time.time() - self.thinking_start if self.thinking_start else 0
207
+ self.terminal._last_thinking = "".join(self.thinking_full_buf).strip()
208
+ t_info = f"Thought for {elapsed_t:.1f}s"
209
+ if self.thinking_tokens > 0:
210
+ t_info += f" · {self.thinking_tokens:,} tokens"
211
+ ctrlo = " [dim]· Ctrl+O 展开[/dim]" if self.terminal._last_thinking else ""
212
+ if self.has_rich:
213
+ sys.stdout.write("\r\033[K")
214
+ sys.stdout.flush()
215
+ self.console.print(f" [dim]✻[/dim] [dim]{t_info}[/dim]{ctrlo}")
216
+ if self.terminal.config.get("thinking_preview") and self.thinking_preview_buf:
217
+ preview_text = "".join(self.thinking_preview_buf)[:280].strip()
218
+ if len("".join(self.thinking_preview_buf)) > 280:
219
+ preview_text += "…"
220
+ self.console.print(f" [dim italic]{preview_text}[/dim italic]")
221
+ else:
222
+ print(f"\r ✻ {t_info}")
223
+
224
+ def on_token(self, token: str) -> None:
225
+ if not self.first_token_received:
226
+ self.first_token_received = True
227
+ self.token_start_time = time.time()
228
+ if self.set_robot_state is not None:
229
+ self.set_robot_state(self.streaming_state)
230
+ if not self.use_batch_render:
231
+ self.stop_spinner()
232
+
233
+ if "<|im_start|>" in token or "<|im_end|>" in token:
234
+ token = token.replace("<|im_start|>", "").replace("<|im_end|>", "")
235
+ if not token.strip():
236
+ return
237
+
238
+ meta_artifacts = (
239
+ "(注释:",
240
+ "(注释:",
241
+ "(提示:",
242
+ "(提示:",
243
+ "请使用实际注入的数据",
244
+ "请使用实际数据",
245
+ "实际注入的数据",
246
+ "[system]",
247
+ "[/system]",
248
+ "[INST]",
249
+ "[/INST]",
250
+ )
251
+ if any(a in token for a in meta_artifacts):
252
+ token = re.sub(
253
+ r"\(注[释释]:[^))]*[))]|(注[释释]:[^))]*[))]"
254
+ r"|\(提示:[^))]*[))]|(提示:[^))]*[))]"
255
+ r"|请使用实际(?:注入的)?数据[^。\n]*"
256
+ r"|\[/?(?:system|INST)\]",
257
+ "",
258
+ token,
259
+ )
260
+ if not token.strip():
261
+ return
262
+
263
+ if _REPETITION_MARKER in token:
264
+ if self.use_batch_render:
265
+ self.response_text += token
266
+ self.streamed_any = True
267
+ self.token_count += 1
268
+ self.repetition_stopped = True
269
+ return
270
+ before, _, _after = token.partition(_REPETITION_MARKER)
271
+ if before:
272
+ self.on_token(before)
273
+ self.response_text += _REPETITION_MARKER
274
+ self.streamed_any = True
275
+ self.token_count += 1
276
+ self.repetition_stopped = True
277
+ self._show_repetition_notice()
278
+ return
279
+
280
+ self._finish_thinking()
281
+
282
+ if self.use_batch_render:
283
+ self.response_text += token
284
+ self.streamed_any = True
285
+ self.token_count += 1
286
+ return
287
+
288
+ open_delims = (r"\(", r"\[", "$$")
289
+ close_delims = (r"\)", r"\]", "$$")
290
+ if not self.in_latex:
291
+ if any(d in token for d in open_delims):
292
+ self.in_latex = True
293
+ self.latex_buf = token
294
+ tail = token
295
+ for od, cd in zip(open_delims, close_delims):
296
+ if od in tail:
297
+ after = tail[tail.index(od) + len(od):]
298
+ if cd in after:
299
+ token = self.flush_latex_buf()
300
+ break
301
+ else:
302
+ self.response_text += self.latex_buf
303
+ self.streamed_any = True
304
+ self.token_count += 1
305
+ return
306
+ else:
307
+ token = self.strip_latex(token)
308
+ else:
309
+ self.latex_buf += token
310
+ if any(d in token for d in close_delims):
311
+ token = self.flush_latex_buf()
312
+ else:
313
+ self.response_text += token
314
+ self.streamed_any = True
315
+ self.token_count += 1
316
+ return
317
+
318
+ self.response_text += token
319
+ self.streamed_any = True
320
+ self.token_count += 1
321
+ can_live = (
322
+ self.has_rich
323
+ and not self.use_plain_print
324
+ and getattr(self.console, "is_terminal", False)
325
+ and not getattr(self.console, "is_dumb_terminal", True)
326
+ and self.markdown_cls is not None
327
+ and self.live_cls is not None
328
+ )
329
+ if can_live:
330
+ now = time.time()
331
+ md = self.markdown_cls(self.strip_latex(self.response_text))
332
+ if self.live_display is None:
333
+ self.live_display = self.live_cls(
334
+ md,
335
+ console=self.console,
336
+ refresh_per_second=12,
337
+ vertical_overflow="visible",
338
+ )
339
+ self.live_display.start()
340
+ self.last_live_update = now
341
+ elif now - self.last_live_update >= self.live_update_interval:
342
+ self.live_display.update(md)
343
+ self.last_live_update = now
344
+ else:
345
+ print(token, end="", flush=True)
346
+
347
+ def on_thinking(self, content: str) -> None:
348
+ if not self.thinking_shown:
349
+ self.stop_spinner()
350
+ self.thinking_start = time.time()
351
+ self.thinking_shown = True
352
+ self.thinking_tokens += 1
353
+ if self.thinking_tokens % 30 == 1:
354
+ elapsed = time.time() - self.thinking_start
355
+ sys.stdout.write(
356
+ f"\r \033[2m✻\033[0m \033[2m思考中 {elapsed:.1f}s "
357
+ f"({self.thinking_tokens} tokens)\033[0m "
358
+ )
359
+ sys.stdout.flush()
360
+ if len("".join(self.thinking_preview_buf)) < 300:
361
+ self.thinking_preview_buf.append(content)
362
+ if len("".join(self.thinking_full_buf)) < 8000:
363
+ self.thinking_full_buf.append(content)
364
+
365
+ def on_tool_call(self, tool: str, params: dict) -> None:
366
+ self._finish_thinking()
367
+ if self.print_tool_call is not None:
368
+ self.print_tool_call(tool, params if isinstance(params, dict) else {})
369
+ self.tool_start_times[tool] = time.time()
370
+
371
+ def on_tool_result(self, tool: str, summary: Any) -> None:
372
+ elapsed_ms = int((time.time() - self.tool_start_times.pop(tool, time.time())) * 1000)
373
+ ok = not (isinstance(summary, dict) and not summary.get("success", True))
374
+ if self.print_tool_done is not None:
375
+ self.print_tool_done(tool, elapsed_ms, success=ok)
376
+
377
+ ts = time.strftime("%H:%M:%S")
378
+ entry = f"[{ts}] {tool}: {str(summary)[:100]}"
379
+ self.terminal._transcript_log.append(entry)
380
+ if len(self.terminal._transcript_log) > 100:
381
+ self.terminal._transcript_log = self.terminal._transcript_log[-100:]
382
+
383
+ if tool in ("TaskCreate", "TaskUpdate") and isinstance(summary, dict):
384
+ tid = summary.get("id") or summary.get("task_id")
385
+ title = summary.get("title", "")
386
+ status = summary.get("status", "pending")
387
+ if tid:
388
+ existing = next((t for t in self.terminal._task_list if t.get("id") == tid), None)
389
+ if existing:
390
+ existing["status"] = status
391
+ if title:
392
+ existing["title"] = title
393
+ else:
394
+ self.terminal._task_list.append({"id": tid, "title": title, "status": status})
395
+
396
+ def on_status(self, state: str, message: str) -> None:
397
+ if state != "fallback":
398
+ return
399
+ match = re.search(r"(?:from\s+)?(\w+)\s*(?:→|->|to)\s*(\w+)", message or "", re.I)
400
+ if match:
401
+ from_provider, to_provider = match.group(1), match.group(2)
402
+ else:
403
+ from_provider, to_provider = self.fallback_from, "cloud"
404
+ from ui.render.output import print_fallback_toast
405
+
406
+ print_fallback_toast(
407
+ from_provider,
408
+ to_provider,
409
+ message or "",
410
+ console=self.console,
411
+ has_rich=self.has_rich,
412
+ )
413
+
414
+ def handle_runtime_event(self, event: Any) -> None:
415
+ if isinstance(event, AgentEventToken):
416
+ self.on_token(event.text)
417
+ elif isinstance(event, AgentEventThinking):
418
+ self.on_thinking(event.content)
419
+ elif isinstance(event, AgentEventToolCall):
420
+ self.on_tool_call(event.tool, event.params)
421
+ elif isinstance(event, AgentEventToolResult):
422
+ self.on_tool_result(event.tool, event.result)
423
+ elif isinstance(event, AgentEventStatus):
424
+ self.on_status(event.state, event.message)
425
+
426
+
427
+ class TerminalApprovalEventConsumer:
428
+ """Terminal-side approval prompt consumer for runtime tool execution."""
429
+
430
+ def __init__(
431
+ self,
432
+ *,
433
+ terminal: Any,
434
+ console: Any,
435
+ has_rich: bool,
436
+ confirm_decision: Callable[..., ApprovalDecision],
437
+ apply_decision: Callable[[dict, ApprovalDecision], dict],
438
+ save_config: Callable[[dict], None],
439
+ ) -> None:
440
+ self.terminal = terminal
441
+ self.console = console
442
+ self.has_rich = has_rich
443
+ self.confirm_decision = confirm_decision
444
+ self.apply_decision = apply_decision
445
+ self.save_config = save_config
446
+
447
+ async def approve(
448
+ self,
449
+ tool_name: str,
450
+ tool_params: dict,
451
+ *,
452
+ stop_before_prompt: Callable[[], None] | None = None,
453
+ ) -> ApprovalDecision:
454
+ if stop_before_prompt is not None:
455
+ stop_before_prompt()
456
+ try:
457
+ approval = self.confirm_decision(
458
+ tool_name,
459
+ tool_params,
460
+ config_policy=self.terminal.config.get("command_policy", "safe"),
461
+ )
462
+ except KeyboardInterrupt:
463
+ approval = ApprovalDecision.deny("KeyboardInterrupt")
464
+
465
+ self.terminal._record_feedback(
466
+ "tool_accept" if approval.approved else "tool_reject",
467
+ tool_name,
468
+ )
469
+ if not approval.approved:
470
+ from ui.render.output import print_tool_blocked
471
+
472
+ print_tool_blocked(tool_name, "用户取消", console=self.console, has_rich=self.has_rich)
473
+ return approval
474
+
475
+ def apply(self, tool_params: dict, approval: ApprovalDecision) -> dict:
476
+ self.apply_decision(tool_params, approval)
477
+ if approval.upgrade_policy:
478
+ tool_params.pop("_upgrade_policy", None)
479
+ self.terminal.config["command_policy"] = "balanced"
480
+ try:
481
+ self.save_config(self.terminal.config)
482
+ if self.has_rich:
483
+ self.console.print(" [dim]策略已升级为 balanced 并保存[/dim]")
484
+ except Exception:
485
+ pass
486
+ return tool_params
487
+
488
+
489
+ __all__ = ["TerminalApprovalEventConsumer", "TerminalRuntimeEventConsumer"]
@@ -0,0 +1,87 @@
1
+ """Session export helpers for Aria Code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from typing import Any, Optional, Sequence
8
+
9
+ from artifacts import artifact_summary as build_artifact_summary
10
+ from apps.cli.config_paths import config_snapshot
11
+ from packages.aria_core import build_session_diagnostic_bundle
12
+
13
+
14
+ def _safe_title_from_conversation(conversation: Sequence[dict]) -> str:
15
+ for msg in conversation:
16
+ if msg.get("role") == "user":
17
+ return str(msg.get("content", ""))[:60]
18
+ return "Aria Code Session"
19
+
20
+
21
+ def build_session_export_payload(
22
+ fmt: str,
23
+ conversation: Sequence[dict],
24
+ *,
25
+ session_id: str = "",
26
+ config: Optional[dict] = None,
27
+ paths: Optional[dict] = None,
28
+ trace: Any = None,
29
+ provider_health: Optional[list] = None,
30
+ ) -> tuple[str, str, str]:
31
+ """Return (content, extension, suggested_filename_prefix)."""
32
+ fmt = (fmt or "json").lower().strip()
33
+
34
+ if fmt == "json":
35
+ content = json.dumps(list(conversation), indent=2, ensure_ascii=False)
36
+ return content, "json", "aria_code_chat"
37
+
38
+ if fmt == "csv":
39
+ lines = ["role,content"]
40
+ for msg in conversation:
41
+ escaped = str(msg.get("content", "")).replace('"', '""').replace("\n", " ")
42
+ lines.append(f'{msg.get("role", "user")},"{escaped}"')
43
+ return "\n".join(lines), "csv", "aria_code_chat"
44
+
45
+ if fmt == "md":
46
+ lines = [f"# Aria Code Chat Export — {datetime.now().strftime('%Y-%m-%d %H:%M')}\n"]
47
+ for msg in conversation:
48
+ prefix = "**You:**" if msg.get("role") == "user" else "**Aria:**"
49
+ lines.append(f"{prefix}\n{msg.get('content', '')}\n")
50
+ return "\n".join(lines), "md", "aria_code_chat"
51
+
52
+ if fmt == "sft":
53
+ pairs = []
54
+ conv = list(conversation)
55
+ i = 0
56
+ while i < len(conv) - 1:
57
+ if conv[i].get("role") == "user" and conv[i + 1].get("role") == "assistant":
58
+ user_text = str(conv[i].get("content", "")).strip()
59
+ assistant_text = str(conv[i + 1].get("content", "")).strip()
60
+ if len(user_text) > 10 and len(assistant_text) > 20 and not user_text.startswith("Tool results:"):
61
+ pairs.append({
62
+ "instruction": user_text,
63
+ "input": "",
64
+ "output": assistant_text,
65
+ "source": "aria_cli_export",
66
+ "timestamp": datetime.now().strftime("%Y-%m-%d"),
67
+ })
68
+ i += 2
69
+ else:
70
+ i += 1
71
+ if not pairs:
72
+ raise ValueError("No user→assistant pairs to export")
73
+ return json.dumps(pairs, indent=2, ensure_ascii=False), "json", "aria_sft"
74
+
75
+ if fmt == "bundle":
76
+ bundle = build_session_diagnostic_bundle(
77
+ session_id=session_id,
78
+ conversation=conversation,
79
+ config=config,
80
+ paths=paths or config_snapshot(),
81
+ trace=trace,
82
+ provider_health=provider_health,
83
+ artifact_summary=build_artifact_summary(),
84
+ )
85
+ return json.dumps(bundle, indent=2, ensure_ascii=False), "json", "aria_bundle"
86
+
87
+ raise ValueError("Unsupported export format")