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.
- agents/__init__.py +32 -0
- agents/base.py +190 -0
- agents/deep/__init__.py +37 -0
- agents/deep/calibration_loop.py +144 -0
- agents/deep/critic.py +125 -0
- agents/deep/deepen.py +193 -0
- agents/deep/models.py +149 -0
- agents/deep/pipeline.py +164 -0
- agents/deep/quant_fusion.py +192 -0
- agents/deep/themes.py +95 -0
- agents/deep/tiers.py +106 -0
- agents/financial/__init__.py +10 -0
- agents/financial/catalyst.py +279 -0
- agents/financial/debate.py +145 -0
- agents/financial/earnings.py +303 -0
- agents/financial/fundamental.py +159 -0
- agents/financial/macro.py +99 -0
- agents/financial/news.py +207 -0
- agents/financial/risk.py +132 -0
- agents/financial/sector.py +279 -0
- agents/financial/synthesis.py +274 -0
- agents/financial/technical.py +258 -0
- agents/portfolio_agent.py +333 -0
- agents/realty/__init__.py +62 -0
- agents/realty/asset_diagnosis.py +150 -0
- agents/realty/business_match.py +165 -0
- agents/realty/cashflow_verify.py +208 -0
- agents/realty/contract_rules.py +209 -0
- agents/realty/energy_anomaly.py +188 -0
- agents/realty/exit_settlement.py +207 -0
- agents/realty/fulfillment_risk.py +205 -0
- agents/realty/ops_optimize.py +159 -0
- agents/realty/revenue_share.py +214 -0
- agents/registry.py +144 -0
- agents/sports/__init__.py +0 -0
- agents/sports/football_agent.py +169 -0
- agents/team.py +289 -0
- aliyun_data_client.py +660 -0
- apps/README.md +12 -0
- apps/__init__.py +2 -0
- apps/channels/README.md +15 -0
- apps/cli/README.md +13 -0
- apps/cli/__init__.py +2 -0
- apps/cli/bootstrap.py +99 -0
- apps/cli/codegen_paths.py +29 -0
- apps/cli/commands/__init__.py +16 -0
- apps/cli/commands/analysis_cmds.py +288 -0
- apps/cli/commands/backtest_cmds.py +1887 -0
- apps/cli/commands/broker_cmds.py +1154 -0
- apps/cli/commands/business_workflow_cmds.py +289 -0
- apps/cli/commands/catalog.py +84 -0
- apps/cli/commands/data_cmds.py +405 -0
- apps/cli/commands/diagnostic_cmds.py +179 -0
- apps/cli/commands/diagnostic_ops_cmds.py +696 -0
- apps/cli/commands/finance_render.py +12 -0
- apps/cli/commands/market.py +399 -0
- apps/cli/commands/market_cmds.py +1276 -0
- apps/cli/commands/market_context.py +425 -0
- apps/cli/commands/market_render.py +7 -0
- apps/cli/commands/model_cmds.py +1579 -0
- apps/cli/commands/ops_cmds.py +668 -0
- apps/cli/commands/portfolio_cmds.py +962 -0
- apps/cli/commands/report.py +377 -0
- apps/cli/commands/scaffold_templates.py +617 -0
- apps/cli/commands/session_cmds.py +179 -0
- apps/cli/commands/session_ux_cmds.py +280 -0
- apps/cli/commands/team.py +588 -0
- apps/cli/commands/team_render.py +8 -0
- apps/cli/commands/ui_cmds.py +358 -0
- apps/cli/commands/workflow_cmds.py +279 -0
- apps/cli/commands/workspace_cmds.py +1414 -0
- apps/cli/config_paths.py +70 -0
- apps/cli/config_store.py +61 -0
- apps/cli/deterministic.py +122 -0
- apps/cli/direct.py +48 -0
- apps/cli/github_app_auth.py +135 -0
- apps/cli/handlers/__init__.py +11 -0
- apps/cli/handlers/broker_handlers.py +122 -0
- apps/cli/handlers/chart_handlers.py +1309 -0
- apps/cli/handlers/market_handlers.py +2509 -0
- apps/cli/handlers/realty_handlers.py +114 -0
- apps/cli/handlers/strategy_advice.py +82 -0
- apps/cli/hooks.py +180 -0
- apps/cli/i18n.py +284 -0
- apps/cli/intent.py +136 -0
- apps/cli/intent_router.py +217 -0
- apps/cli/lifecycle_hooks.py +48 -0
- apps/cli/main.py +29 -0
- apps/cli/market_metadata.py +135 -0
- apps/cli/market_universe.py +265 -0
- apps/cli/message_processing.py +257 -0
- apps/cli/plan_mode.py +139 -0
- apps/cli/plotly_html.py +15 -0
- apps/cli/prediction_feedback.py +202 -0
- apps/cli/preflight.py +497 -0
- apps/cli/project_aria.py +60 -0
- apps/cli/prompts/__init__.py +0 -0
- apps/cli/prompts/coding.py +658 -0
- apps/cli/prompts/system_prompts.py +531 -0
- apps/cli/prompts/ui.py +434 -0
- apps/cli/providers/__init__.py +1 -0
- apps/cli/providers/base.py +271 -0
- apps/cli/providers/chat_routing.py +80 -0
- apps/cli/providers/llm/__init__.py +1 -0
- apps/cli/providers/llm/ollama_stream.py +1170 -0
- apps/cli/providers/llm/sse_stream.py +216 -0
- apps/cli/providers/runtime_bridge.py +185 -0
- apps/cli/runtime_consumer.py +489 -0
- apps/cli/session_export.py +87 -0
- apps/cli/session_jsonl.py +207 -0
- apps/cli/session_store.py +112 -0
- apps/cli/todo_tracker.py +190 -0
- apps/cli/tools/__init__.py +40 -0
- apps/cli/tools/context.py +46 -0
- apps/cli/tools/file_tools.py +112 -0
- apps/cli/tools/market_tools.py +549 -0
- apps/cli/tools/notebook_tools.py +111 -0
- apps/cli/tools/system_tools.py +669 -0
- apps/cli/tools/write_tools.py +715 -0
- apps/cli/tradingview_bridge.py +434 -0
- apps/cli/update_check.py +152 -0
- apps/cli/utils/__init__.py +0 -0
- apps/cli/utils/market_detect.py +1578 -0
- apps/daemon/README.md +14 -0
- apps/vscode/README.md +115 -0
- apps/vscode/package.json +70 -0
- aria_cli.py +11636 -0
- aria_code-4.1.3.dist-info/METADATA +952 -0
- aria_code-4.1.3.dist-info/RECORD +284 -0
- aria_code-4.1.3.dist-info/WHEEL +5 -0
- aria_code-4.1.3.dist-info/entry_points.txt +2 -0
- aria_code-4.1.3.dist-info/licenses/LICENSE +121 -0
- aria_code-4.1.3.dist-info/top_level.txt +50 -0
- aria_daemon.py +1295 -0
- aria_feishu_bot.py +1359 -0
- aria_relay_client.py +182 -0
- aria_relay_server.py +405 -0
- aria_telegram_bot.py +202 -0
- ariarc.py +328 -0
- artifacts.py +491 -0
- backtest_report.py +472 -0
- brokers/__init__.py +72 -0
- brokers/base.py +207 -0
- brokers/capabilities.py +264 -0
- brokers/cn/__init__.py +10 -0
- brokers/cn/easytrader_broker.py +193 -0
- brokers/cn/futu_broker.py +194 -0
- brokers/cn/longbridge_broker.py +190 -0
- brokers/cn/tiger_broker.py +196 -0
- brokers/cn/xtquant_broker.py +175 -0
- brokers/config.py +364 -0
- brokers/intl/__init__.py +5 -0
- brokers/intl/alpaca_broker.py +183 -0
- brokers/intl/ibkr_broker.py +215 -0
- brokers/intl/webull_broker.py +156 -0
- brokers/paper_broker.py +259 -0
- brokers/planning.py +296 -0
- brokers/registry.py +181 -0
- brokers/trading.py +237 -0
- change_store.py +127 -0
- command_safety.py +19 -0
- computer_use_tools.py +504 -0
- dashboard_generator.py +578 -0
- data_analysis_tools.py +808 -0
- data_cleaner.py +483 -0
- data_service.py +481 -0
- datasources/__init__.py +23 -0
- datasources/base.py +166 -0
- datasources/router.py +221 -0
- datasources/sources/__init__.py +15 -0
- datasources/sources/akshare_source.py +269 -0
- datasources/sources/alpha_vantage_source.py +202 -0
- datasources/sources/edgar_source.py +218 -0
- datasources/sources/finnhub_source.py +197 -0
- datasources/sources/fred_source.py +219 -0
- datasources/sources/tushare_source.py +141 -0
- datasources/sources/web_scraper_source.py +278 -0
- datasources/sources/world_bank_source.py +205 -0
- datasources/sources/yfinance_source.py +152 -0
- demo_player.py +204 -0
- doctor.py +508 -0
- file_analysis_tools.py +734 -0
- finance_formulas.py +389 -0
- football_data_client.py +1670 -0
- intent_classifier.py +358 -0
- local_finance_tools.py +3221 -0
- local_llm_provider.py +552 -0
- macro_tools.py +368 -0
- market_data_client.py +1899 -0
- mcp_client.py +506 -0
- memory_manager.py +245 -0
- model_capability.py +416 -0
- notification_tools.py +248 -0
- packages/__init__.py +23 -0
- packages/aria_agents/__init__.py +5 -0
- packages/aria_agents/manifest.py +69 -0
- packages/aria_core/__init__.py +34 -0
- packages/aria_core/architecture.py +192 -0
- packages/aria_core/export.py +124 -0
- packages/aria_core/manifest.py +65 -0
- packages/aria_infra/__init__.py +15 -0
- packages/aria_infra/arthera.py +52 -0
- packages/aria_infra/doctor.py +246 -0
- packages/aria_infra/product.py +37 -0
- packages/aria_mcp/__init__.py +25 -0
- packages/aria_mcp/bridge.py +38 -0
- packages/aria_mcp/config.py +97 -0
- packages/aria_mcp/tools.py +61 -0
- packages/aria_sdk/__init__.py +19 -0
- packages/aria_sdk/client.py +396 -0
- packages/aria_sdk/providers.py +70 -0
- packages/aria_sdk/streaming.py +73 -0
- packages/aria_sdk/types.py +86 -0
- packages/aria_services/__init__.py +55 -0
- packages/aria_services/context.py +258 -0
- packages/aria_services/data.py +11 -0
- packages/aria_services/provider_health.py +189 -0
- packages/aria_services/registry.py +213 -0
- packages/aria_services/usage.py +138 -0
- packages/aria_skills/__init__.py +5 -0
- packages/aria_skills/registry.py +59 -0
- packages/aria_tools/__init__.py +5 -0
- packages/aria_tools/registry.py +128 -0
- packages/quant_engine/__init__.py +6 -0
- packages/quant_engine/sports/__init__.py +72 -0
- packages/quant_engine/sports/calibrator.py +353 -0
- packages/quant_engine/sports/dixon_coles.py +234 -0
- packages/quant_engine/sports/elo.py +299 -0
- packages/quant_engine/sports/form.py +188 -0
- packages/quant_engine/sports/h2h.py +195 -0
- packages/quant_engine/sports/ml_model.py +354 -0
- packages/quant_engine/sports/predictor.py +311 -0
- packages/quant_engine/sports/tracker.py +664 -0
- packages/quant_engine/stochastic/__init__.py +27 -0
- packages/quant_engine/stochastic/gbm_enhanced.py +195 -0
- packages/quant_engine/stochastic/ito_calculus.py +477 -0
- packages/quant_engine/stochastic/kelly_criterion.py +181 -0
- packages/quant_engine/stochastic/monte_carlo_advanced.py +95 -0
- packages/quant_engine/stochastic/options_pricing.py +573 -0
- packages/quant_engine/stochastic/stochastic_processes.py +90 -0
- plan_utils.py +194 -0
- plugin_loader.py +328 -0
- portfolio_ledger.py +262 -0
- privacy/__init__.py +5 -0
- privacy/feedback.py +123 -0
- project_tools.py +525 -0
- providers/__init__.py +30 -0
- providers/llm/__init__.py +19 -0
- providers/llm/anthropic.py +184 -0
- providers/llm/base.py +139 -0
- providers/llm/ollama.py +128 -0
- providers/llm/openai_compat.py +282 -0
- providers/llm/registry.py +358 -0
- realty_data_tools.py +659 -0
- report_generator.py +1314 -0
- runtime/__init__.py +103 -0
- runtime/agent_loop.py +1183 -0
- runtime/approval.py +51 -0
- runtime/events.py +102 -0
- runtime/gateway.py +128 -0
- runtime/lsp.py +346 -0
- runtime/subagent.py +258 -0
- runtime/tool_executor.py +104 -0
- runtime/tool_policy.py +106 -0
- safety/__init__.py +21 -0
- safety/permissions.py +275 -0
- setup_wizard.py +653 -0
- strategy_vault.py +420 -0
- ui/__init__.py +100 -0
- ui/banner.py +310 -0
- ui/completer.py +391 -0
- ui/console.py +271 -0
- ui/image_render.py +243 -0
- ui/input_box.py +376 -0
- ui/picker.py +195 -0
- ui/render/__init__.py +11 -0
- ui/render/finance.py +1480 -0
- ui/render/market.py +225 -0
- ui/render/output.py +681 -0
- ui/render/team.py +346 -0
- ui/robot.py +235 -0
- workspace/__init__.py +6 -0
- workspace/files.py +170 -0
- 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
|
apps/cli/todo_tracker.py
ADDED
|
@@ -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"
|