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,207 @@
1
+ """JSONL-backed session persistence for Aria Code.
2
+
3
+ Why JSONL instead of JSON?
4
+ • Append-per-turn — no need to rewrite the whole file on every message
5
+ • Crash-safe — partial writes leave previous turns intact
6
+ • Streamable — readers can tail -f a live session
7
+
8
+ File layout: ~/.arthera/sessions/<session_id>.jsonl
9
+ Each line is one JSON object:
10
+ {"type": "meta", "id": "...", "title": "...", "created_at": "..."}
11
+ {"type": "message", "role": "user", "content": "...", "ts": "..."}
12
+ {"type": "message", "role": "assistant", "content": "...", "ts": "..."}
13
+ {"type": "meta", "updated_at": "..."} ← appended on each save
14
+
15
+ Reading: scan all lines, reconstruct conversation from "message" entries.
16
+ Last "meta" wins for title / timestamps.
17
+ """
18
+ from __future__ import annotations
19
+
20
+ import json
21
+ from datetime import datetime, timezone
22
+ from pathlib import Path
23
+ from typing import Optional
24
+
25
+ _SESSIONS_DIR = Path.home() / ".arthera" / "sessions"
26
+
27
+
28
+ def _now() -> str:
29
+ return datetime.now(timezone.utc).isoformat()
30
+
31
+
32
+ def _session_path(session_id: str) -> Path:
33
+ return _SESSIONS_DIR / f"{session_id}.jsonl"
34
+
35
+
36
+ class JsonlSessionStore:
37
+ """Read/write JSONL session files."""
38
+
39
+ def __init__(self, sessions_dir: Optional[Path] = None) -> None:
40
+ self.root = sessions_dir or _SESSIONS_DIR
41
+ self.root.mkdir(parents=True, exist_ok=True)
42
+
43
+ def _path(self, session_id: str) -> Path:
44
+ return self.root / f"{session_id}.jsonl"
45
+
46
+ # ── Write ─────────────────────────────────────────────────────────────────
47
+
48
+ def init_session(self, session_id: str, title: str = "") -> None:
49
+ """Write the opening meta line. Call once at session start."""
50
+ p = self._path(session_id)
51
+ if p.exists():
52
+ return
53
+ line = json.dumps({
54
+ "type": "meta",
55
+ "id": session_id,
56
+ "title": title or "",
57
+ "created_at": _now(),
58
+ }, ensure_ascii=False)
59
+ p.write_text(line + "\n", encoding="utf-8")
60
+
61
+ def append_message(self, session_id: str, role: str, content: str) -> None:
62
+ """Append one message turn. Thread-safe for single-process use."""
63
+ p = self._path(session_id)
64
+ line = json.dumps({
65
+ "type": "message",
66
+ "role": role,
67
+ "content": content,
68
+ "ts": _now(),
69
+ }, ensure_ascii=False)
70
+ with p.open("a", encoding="utf-8") as f:
71
+ f.write(line + "\n")
72
+
73
+ def flush_meta(self, session_id: str, title: str = "", extra: Optional[dict] = None) -> None:
74
+ """Append an updated meta line (title, timestamps)."""
75
+ p = self._path(session_id)
76
+ meta: dict = {"type": "meta", "updated_at": _now()}
77
+ if title:
78
+ meta["title"] = title
79
+ if extra:
80
+ meta.update(extra)
81
+ with p.open("a", encoding="utf-8") as f:
82
+ f.write(json.dumps(meta, ensure_ascii=False) + "\n")
83
+
84
+ def save_conversation(
85
+ self,
86
+ session_id: str,
87
+ conversation: list[dict],
88
+ title: str = "",
89
+ ) -> None:
90
+ """Full rewrite — used when bulk-importing a JSON session into JSONL."""
91
+ p = self._path(session_id)
92
+ lines = []
93
+ lines.append(json.dumps({
94
+ "type": "meta",
95
+ "id": session_id,
96
+ "title": title or "",
97
+ "created_at": _now(),
98
+ "updated_at": _now(),
99
+ }, ensure_ascii=False))
100
+ for msg in conversation:
101
+ lines.append(json.dumps({
102
+ "type": "message",
103
+ "role": msg.get("role", "user"),
104
+ "content": str(msg.get("content", "")),
105
+ "ts": _now(),
106
+ }, ensure_ascii=False))
107
+ p.write_text("\n".join(lines) + "\n", encoding="utf-8")
108
+
109
+ # ── Read ──────────────────────────────────────────────────────────────────
110
+
111
+ def load_session(self, session_id: str) -> Optional[dict]:
112
+ """Load a session; returns None if not found."""
113
+ p = self._path(session_id)
114
+ if not p.exists():
115
+ return None
116
+
117
+ messages: list[dict] = []
118
+ meta: dict = {"id": session_id}
119
+
120
+ for raw in p.read_text(encoding="utf-8").splitlines():
121
+ raw = raw.strip()
122
+ if not raw:
123
+ continue
124
+ try:
125
+ obj = json.loads(raw)
126
+ except json.JSONDecodeError:
127
+ continue
128
+
129
+ t = obj.get("type")
130
+ if t == "meta":
131
+ meta.update({k: v for k, v in obj.items() if k != "type"})
132
+ elif t == "message":
133
+ messages.append({
134
+ "role": obj.get("role", "user"),
135
+ "content": obj.get("content", ""),
136
+ })
137
+
138
+ return {
139
+ "id": session_id,
140
+ "messages": messages,
141
+ "metadata": {
142
+ "title": meta.get("title", "Untitled"),
143
+ "created_at": meta.get("created_at", ""),
144
+ "updated_at": meta.get("updated_at", ""),
145
+ },
146
+ }
147
+
148
+ def list_sessions(self, limit: int = 20) -> list[dict]:
149
+ """Return recent sessions sorted by mtime, newest first."""
150
+ sessions = []
151
+ for p in sorted(
152
+ self.root.glob("*.jsonl"),
153
+ key=lambda x: x.stat().st_mtime,
154
+ reverse=True,
155
+ ):
156
+ session_id = p.stem
157
+ meta: dict = {}
158
+ msg_count = 0
159
+ try:
160
+ for raw in p.read_text(encoding="utf-8").splitlines():
161
+ raw = raw.strip()
162
+ if not raw:
163
+ continue
164
+ obj = json.loads(raw)
165
+ if obj.get("type") == "meta":
166
+ meta.update(obj)
167
+ elif obj.get("type") == "message":
168
+ msg_count += 1
169
+ except Exception:
170
+ continue
171
+
172
+ sessions.append({
173
+ "id": session_id,
174
+ "title": meta.get("title", "Untitled"),
175
+ "messages": msg_count,
176
+ "updated": meta.get("updated_at", ""),
177
+ "created": meta.get("created_at", ""),
178
+ })
179
+ if len(sessions) >= limit:
180
+ break
181
+
182
+ return sessions
183
+
184
+ def delete_session(self, session_id: str) -> bool:
185
+ p = self._path(session_id)
186
+ if p.exists():
187
+ p.unlink()
188
+ return True
189
+ return False
190
+
191
+ def search_sessions(self, keyword: str, limit: int = 10) -> list[dict]:
192
+ """Full-text search across all session JSONL files."""
193
+ kw = keyword.lower()
194
+ matches = []
195
+ for p in self.root.glob("*.jsonl"):
196
+ try:
197
+ text = p.read_text(encoding="utf-8")
198
+ if kw not in text.lower():
199
+ continue
200
+ result = self.load_session(p.stem)
201
+ if result:
202
+ matches.append(result)
203
+ except Exception:
204
+ continue
205
+ if len(matches) >= limit:
206
+ break
207
+ return matches
@@ -0,0 +1,112 @@
1
+ """JSON-backed session persistence for Aria Code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import json
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from typing import Optional
10
+ from apps.cli.config_paths import resolve_config_dir
11
+
12
+
13
+ class SessionManager:
14
+ """Manage chat sessions with local file persistence."""
15
+
16
+ def __init__(self, sessions_dir: Optional[Path] = None):
17
+ self.root = sessions_dir or (resolve_config_dir() / "sessions")
18
+ self.root.mkdir(parents=True, exist_ok=True)
19
+
20
+ def _path(self, session_id: str) -> Path:
21
+ return self.root / f"{session_id}.json"
22
+
23
+ def save_session(self, session_id: str, conversation: list, metadata: dict = None):
24
+ meta = dict(metadata or {})
25
+ if not meta.get("created_at"):
26
+ meta["created_at"] = datetime.now().isoformat()
27
+ for msg in conversation:
28
+ if msg.get("role") == "user":
29
+ meta.setdefault("title", str(msg.get("content", ""))[:60])
30
+ break
31
+ data = {
32
+ "id": session_id,
33
+ "messages": conversation,
34
+ "metadata": meta,
35
+ "updated_at": datetime.now().isoformat(),
36
+ }
37
+ path = self._path(session_id)
38
+ with open(path, "w", encoding="utf-8") as f:
39
+ json.dump(data, f, indent=2, ensure_ascii=False)
40
+
41
+ def load_session(self, session_id: str) -> Optional[dict]:
42
+ path = self._path(session_id)
43
+ if path.exists():
44
+ with open(path, encoding="utf-8") as f:
45
+ return json.load(f)
46
+ return None
47
+
48
+ def list_sessions(self, limit: int = 20) -> list:
49
+ sessions = []
50
+ for path in sorted(self.root.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
51
+ try:
52
+ with open(path, encoding="utf-8") as f:
53
+ data = json.load(f)
54
+ sessions.append({
55
+ "id": data.get("id", path.stem),
56
+ "title": data.get("metadata", {}).get("title", "Untitled"),
57
+ "messages": len(data.get("messages", [])),
58
+ "updated": data.get("updated_at", ""),
59
+ })
60
+ except Exception:
61
+ continue
62
+ if len(sessions) >= limit:
63
+ break
64
+ return sessions
65
+
66
+ def search_sessions(self, query: str, limit: int = 20) -> list:
67
+ """Full-text search through session message content."""
68
+ q = query.lower()
69
+ results = []
70
+ for path in sorted(self.root.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True):
71
+ try:
72
+ with open(path, encoding="utf-8") as f:
73
+ data = json.load(f)
74
+ messages = data.get("messages", [])
75
+ hits = []
76
+ for msg in messages:
77
+ content = msg.get("content", "")
78
+ if isinstance(content, str):
79
+ if q in content.lower():
80
+ idx = content.lower().index(q)
81
+ start = max(0, idx - 20)
82
+ end = min(len(content), idx + len(q) + 80)
83
+ hits.append(content[start:end])
84
+ elif isinstance(content, list):
85
+ for block in content:
86
+ if isinstance(block, dict):
87
+ text = block.get("text", "")
88
+ if text and q in text.lower():
89
+ idx = text.lower().index(q)
90
+ start = max(0, idx - 20)
91
+ end = min(len(text), idx + len(q) + 80)
92
+ hits.append(text[start:end])
93
+ if hits:
94
+ results.append({
95
+ "id": data.get("id", path.stem),
96
+ "title": data.get("metadata", {}).get("title", "Untitled"),
97
+ "updated": data.get("updated_at", ""),
98
+ "match_count": len(hits),
99
+ "preview": hits[0],
100
+ })
101
+ except Exception:
102
+ continue
103
+ if len(results) >= limit:
104
+ break
105
+ return sorted(results, key=lambda r: r["match_count"], reverse=True)
106
+
107
+ def delete_session(self, session_id: str) -> bool:
108
+ path = self._path(session_id)
109
+ if path.exists():
110
+ path.unlink()
111
+ return True
112
+ return False
@@ -0,0 +1,190 @@
1
+ """Structured task tracking for the Aria agent loop (Claude Code TodoWrite parity).
2
+
3
+ The model calls the ``update_todos`` tool with the full current task list each
4
+ time progress changes. We keep the latest list in a module-global so the
5
+ renderer (and any UI surface) can show a live checklist of multi-step work.
6
+
7
+ Design notes
8
+ ------------
9
+ * State is intentionally a process-global, mirroring how the screenshot tool
10
+ stashes a pending image. Tool handlers only receive ``params``; they have no
11
+ reference to the terminal, so a module global is the pragmatic channel.
12
+ * The list is replaced wholesale on every call (not merged) so the model owns
13
+ the source of truth and we never drift out of sync with its plan.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from typing import Any, Dict, List
18
+
19
+ _VALID_STATUS = ("pending", "in_progress", "completed")
20
+
21
+ # Latest task list the model published this turn.
22
+ _ACTIVE_TODOS: List[Dict[str, str]] = []
23
+
24
+
25
+ def get_active_todos() -> List[Dict[str, str]]:
26
+ """Return a copy of the current task list."""
27
+ return list(_ACTIVE_TODOS)
28
+
29
+
30
+ def clear_todos() -> None:
31
+ """Reset the task list (call at the start of a new user turn)."""
32
+ _ACTIVE_TODOS.clear()
33
+
34
+
35
+ def _normalize(todos: Any) -> List[Dict[str, str]]:
36
+ """Coerce model-supplied todos into a clean list of {content, status}."""
37
+ out: List[Dict[str, str]] = []
38
+ if not isinstance(todos, list):
39
+ return out
40
+ for item in todos:
41
+ if isinstance(item, str):
42
+ content, status = item, "pending"
43
+ elif isinstance(item, dict):
44
+ content = str(
45
+ item.get("content")
46
+ or item.get("task")
47
+ or item.get("title")
48
+ or item.get("step")
49
+ or ""
50
+ ).strip()
51
+ status = str(item.get("status", "pending")).strip().lower()
52
+ else:
53
+ continue
54
+ if not content:
55
+ continue
56
+ if status not in _VALID_STATUS:
57
+ # Accept a few common synonyms
58
+ status = {
59
+ "done": "completed", "complete": "completed", "finished": "completed",
60
+ "doing": "in_progress", "active": "in_progress", "wip": "in_progress",
61
+ "todo": "pending", "open": "pending", "not_started": "pending",
62
+ }.get(status, "pending")
63
+ out.append({"content": content, "status": status})
64
+ return out
65
+
66
+
67
+ def update_todos(params: dict) -> dict:
68
+ """Tool handler: replace the active task list with the model's latest plan."""
69
+ todos = _normalize(params.get("todos", params.get("tasks", [])))
70
+ if not todos:
71
+ return {
72
+ "success": False,
73
+ "error": "update_todos 需要非空的 todos 数组,每项形如 "
74
+ "{\"content\": \"步骤描述\", \"status\": \"pending|in_progress|completed\"}",
75
+ }
76
+
77
+ # At most one in_progress is the convention — demote extras to pending order
78
+ seen_in_progress = False
79
+ for t in todos:
80
+ if t["status"] == "in_progress":
81
+ if seen_in_progress:
82
+ t["status"] = "pending"
83
+ else:
84
+ seen_in_progress = True
85
+
86
+ _ACTIVE_TODOS.clear()
87
+ _ACTIVE_TODOS.extend(todos)
88
+
89
+ _render(todos)
90
+
91
+ done = sum(1 for t in todos if t["status"] == "completed")
92
+ total = len(todos)
93
+ return {
94
+ "success": True,
95
+ "data": {
96
+ "total": total,
97
+ "completed": done,
98
+ "in_progress": sum(1 for t in todos if t["status"] == "in_progress"),
99
+ "pending": sum(1 for t in todos if t["status"] == "pending"),
100
+ "todos": todos,
101
+ },
102
+ # Compact text the model reads back so it knows the tracked state
103
+ "summary": f"任务进度 {done}/{total} 已完成",
104
+ }
105
+
106
+
107
+ def _render(todos: List[Dict[str, str]]) -> None:
108
+ """Print the task list as a checklist. Uses rich when available."""
109
+ try:
110
+ import aria_cli as _ac
111
+ console = getattr(_ac, "console", None)
112
+ has_rich = getattr(_ac, "HAS_RICH", False)
113
+ except Exception:
114
+ console, has_rich = None, False
115
+
116
+ _icons = {
117
+ "completed": ("[green]✓[/green]", "✓"),
118
+ "in_progress": ("[yellow]▶[/yellow]", "▶"),
119
+ "pending": ("[dim]○[/dim]", "○"),
120
+ }
121
+ done = sum(1 for t in todos if t["status"] == "completed")
122
+ total = len(todos)
123
+
124
+ if has_rich and console is not None:
125
+ from rich.panel import Panel
126
+ from rich.text import Text
127
+ body = Text()
128
+ for i, t in enumerate(todos):
129
+ icon_rich, _ = _icons.get(t["status"], _icons["pending"])
130
+ style = (
131
+ "green" if t["status"] == "completed"
132
+ else "bold yellow" if t["status"] == "in_progress"
133
+ else "dim"
134
+ )
135
+ line = Text.from_markup(f"{icon_rich} ")
136
+ content = t["content"]
137
+ if t["status"] == "completed":
138
+ line.append(content, style="dim strike")
139
+ else:
140
+ line.append(content, style=style)
141
+ body.append_text(line)
142
+ if i < len(todos) - 1:
143
+ body.append("\n")
144
+ console.print(Panel(
145
+ body,
146
+ title=f"[bold]任务清单[/bold] [dim]{done}/{total}[/dim]",
147
+ border_style="cyan",
148
+ padding=(0, 1),
149
+ ))
150
+ else:
151
+ print(f"\n任务清单 ({done}/{total}):")
152
+ for t in todos:
153
+ _, icon_plain = _icons.get(t["status"], _icons["pending"])
154
+ print(f" {icon_plain} {t['content']}")
155
+
156
+
157
+ UPDATE_TODOS_SCHEMA = {
158
+ "type": "function",
159
+ "function": {
160
+ "name": "update_todos",
161
+ "description": (
162
+ "Track progress on a multi-step task as a live checklist. Call this when a task "
163
+ "has 3+ distinct steps: first to lay out the plan (all pending), then again each "
164
+ "time you start a step (mark it in_progress) or finish one (mark it completed). "
165
+ "Keep exactly one step in_progress at a time. Always send the FULL list every call."
166
+ ),
167
+ "parameters": {
168
+ "type": "object",
169
+ "properties": {
170
+ "todos": {
171
+ "type": "array",
172
+ "description": "The complete current task list (replaces the previous list).",
173
+ "items": {
174
+ "type": "object",
175
+ "properties": {
176
+ "content": {"type": "string", "description": "Short step description"},
177
+ "status": {
178
+ "type": "string",
179
+ "enum": list(_VALID_STATUS),
180
+ "description": "pending | in_progress | completed",
181
+ },
182
+ },
183
+ "required": ["content", "status"],
184
+ },
185
+ },
186
+ },
187
+ "required": ["todos"],
188
+ },
189
+ },
190
+ }
@@ -0,0 +1,40 @@
1
+ """apps/cli/tools — stateless tool implementations extracted from aria_cli.py."""
2
+ from .file_tools import tool_read_file, tool_list_files, tool_search_code, tool_glob
3
+ from .context import ToolContext
4
+ from .system_tools import tool_run_command, tool_web_fetch, tool_github
5
+ from .notebook_tools import (
6
+ tool_glob as tool_glob_nb,
7
+ tool_notebook_read,
8
+ tool_notebook_edit,
9
+ )
10
+ from .market_tools import (
11
+ tool_get_market_data,
12
+ tool_get_market_history,
13
+ tool_broker_query,
14
+ tool_broker_order,
15
+ )
16
+ from .write_tools import tool_write_file, tool_edit_file
17
+
18
+ __all__ = [
19
+ "ToolContext",
20
+ # file tools (stateless)
21
+ "tool_read_file",
22
+ "tool_list_files",
23
+ "tool_search_code",
24
+ "tool_glob",
25
+ # write / edit tools (use lazy imports to avoid circular dep)
26
+ "tool_write_file",
27
+ "tool_edit_file",
28
+ # system tools
29
+ "tool_run_command",
30
+ "tool_web_fetch",
31
+ "tool_github",
32
+ # notebook tools
33
+ "tool_notebook_read",
34
+ "tool_notebook_edit",
35
+ # market / broker tools
36
+ "tool_get_market_data",
37
+ "tool_get_market_history",
38
+ "tool_broker_query",
39
+ "tool_broker_order",
40
+ ]
@@ -0,0 +1,46 @@
1
+ """ToolContext — dependency bundle for tools that need write/display state."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from pathlib import Path
6
+ from typing import TYPE_CHECKING, Any
7
+
8
+ from apps.cli.config_paths import resolve_config_dir
9
+
10
+ if TYPE_CHECKING:
11
+ from rich.console import Console
12
+
13
+
14
+ @dataclass
15
+ class ToolContext:
16
+ """Passed to write-capable tools so they don't need aria_cli.py globals.
17
+
18
+ Instantiate once in ``ArtheraTerminal.__init__`` and share via reference:
19
+
20
+ self._tool_ctx = ToolContext(
21
+ console=console,
22
+ has_rich=HAS_RICH,
23
+ write_policy=_ACTIVE_WRITE_POLICY,
24
+ change_store=GLOBAL_CHANGE_STORE,
25
+ config_dir=CONFIG_DIR,
26
+ sessions_dir=SESSIONS_DIR,
27
+ )
28
+ """
29
+ console: "Console | None" = None
30
+ has_rich: bool = True
31
+ write_policy: list[str] = field(default_factory=lambda: ["desktop_only"])
32
+ change_store: Any = None # GLOBAL_CHANGE_STORE
33
+ config_dir: Path = field(default_factory=resolve_config_dir)
34
+ sessions_dir: Path = field(default_factory=lambda: resolve_config_dir() / "sessions")
35
+
36
+ # ── helpers ──────────────────────────────────────────────────────
37
+ def print(self, *args, **kwargs) -> None:
38
+ if self.has_rich and self.console is not None:
39
+ self.console.print(*args, **kwargs)
40
+ else:
41
+ import builtins
42
+ builtins.print(*args)
43
+
44
+ @property
45
+ def policy(self) -> str:
46
+ return self.write_policy[0] if self.write_policy else "desktop_only"