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
runtime/approval.py ADDED
@@ -0,0 +1,51 @@
1
+ """Approval decisions for tools that require user consent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class ApprovalDecision:
10
+ """Structured result from a tool approval prompt."""
11
+
12
+ approved: bool
13
+ policy: str | None = None
14
+ user_approved: bool = False
15
+ upgrade_policy: bool = False
16
+ auto_approve_session: bool = False
17
+ reason: str = ""
18
+
19
+ @classmethod
20
+ def allow(
21
+ cls,
22
+ *,
23
+ policy: str | None = None,
24
+ user_approved: bool = False,
25
+ upgrade_policy: bool = False,
26
+ auto_approve_session: bool = False,
27
+ reason: str = "",
28
+ ) -> "ApprovalDecision":
29
+ return cls(
30
+ approved=True,
31
+ policy=policy,
32
+ user_approved=user_approved,
33
+ upgrade_policy=upgrade_policy,
34
+ auto_approve_session=auto_approve_session,
35
+ reason=reason,
36
+ )
37
+
38
+ @classmethod
39
+ def deny(cls, reason: str = "") -> "ApprovalDecision":
40
+ return cls(approved=False, reason=reason)
41
+
42
+
43
+ def apply_approval_decision(params: dict, decision: ApprovalDecision) -> dict:
44
+ """Apply execution-facing approval fields to tool params."""
45
+ if decision.policy is not None:
46
+ params["policy"] = decision.policy
47
+ if decision.user_approved:
48
+ params["user_approved"] = True
49
+ if decision.upgrade_policy:
50
+ params["_upgrade_policy"] = True
51
+ return params
runtime/events.py ADDED
@@ -0,0 +1,102 @@
1
+ """Runtime event and trace records for Aria Code."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ import uuid
7
+ from dataclasses import asdict, dataclass, field
8
+ from typing import Any, Dict, List
9
+
10
+
11
+ @dataclass(frozen=True)
12
+ class RuntimeEvent:
13
+ event_id: str
14
+ type: str
15
+ timestamp: float
16
+ data: Dict[str, Any] = field(default_factory=dict)
17
+
18
+ @classmethod
19
+ def create(cls, event_type: str, data: Dict[str, Any] | None = None) -> "RuntimeEvent":
20
+ return cls(uuid.uuid4().hex[:12], event_type, time.time(), data or {})
21
+
22
+ def to_dict(self) -> Dict[str, Any]:
23
+ return asdict(self)
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class ToolCallRecord:
28
+ tool: str
29
+ params: Dict[str, Any]
30
+ result: Dict[str, Any]
31
+ elapsed_ms: float
32
+ started_at: float
33
+ ended_at: float
34
+
35
+ def to_dict(self) -> Dict[str, Any]:
36
+ return asdict(self)
37
+
38
+
39
+ @dataclass(frozen=True)
40
+ class TurnResultRecord:
41
+ status: str
42
+ success: bool
43
+ cancelled: bool
44
+ provider: str
45
+ error: str
46
+ final_text: str
47
+ summary: str
48
+ metadata: Dict[str, Any] = field(default_factory=dict)
49
+ timestamp: float = field(default_factory=time.time)
50
+
51
+ def to_dict(self) -> Dict[str, Any]:
52
+ return asdict(self)
53
+
54
+
55
+ class RuntimeTrace:
56
+ """In-memory trace of runtime events for session replay/debugging."""
57
+
58
+ def __init__(self) -> None:
59
+ self.events: List[RuntimeEvent] = []
60
+ self.tool_calls: List[ToolCallRecord] = []
61
+ self.turn_results: List[TurnResultRecord] = []
62
+
63
+ def emit(self, event_type: str, data: Dict[str, Any] | None = None) -> RuntimeEvent:
64
+ event = RuntimeEvent.create(event_type, data)
65
+ self.events.append(event)
66
+ return event
67
+
68
+ def add_tool_call(self, record: ToolCallRecord) -> None:
69
+ self.tool_calls.append(record)
70
+ self.emit("tool_result", {
71
+ "tool": record.tool,
72
+ "success": bool(record.result.get("success")),
73
+ "elapsed_ms": record.elapsed_ms,
74
+ })
75
+
76
+ def add_turn_result(self, record: Dict[str, Any]) -> TurnResultRecord:
77
+ turn = TurnResultRecord(
78
+ status=str(record.get("status", "")),
79
+ success=bool(record.get("success")),
80
+ cancelled=bool(record.get("cancelled")),
81
+ provider=str(record.get("provider", "")),
82
+ error=str(record.get("error", "")),
83
+ final_text=str(record.get("final_text", "")),
84
+ summary=str(record.get("summary", "")),
85
+ metadata=dict(record.get("metadata") or {}),
86
+ )
87
+ self.turn_results.append(turn)
88
+ self.emit("turn_complete", {
89
+ "status": turn.status,
90
+ "success": turn.success,
91
+ "cancelled": turn.cancelled,
92
+ "provider": turn.provider,
93
+ "summary": turn.summary,
94
+ })
95
+ return turn
96
+
97
+ def to_dict(self) -> Dict[str, Any]:
98
+ return {
99
+ "events": [event.to_dict() for event in self.events],
100
+ "tool_calls": [call.to_dict() for call in self.tool_calls],
101
+ "turn_results": [turn.to_dict() for turn in self.turn_results],
102
+ }
runtime/gateway.py ADDED
@@ -0,0 +1,128 @@
1
+ """Neutral agent-turn Gateway — the single entry every adapter calls.
2
+
3
+ This is the documented runtime convergence point: one tested turn driver behind
4
+ a thin set of adapters (interactive CLI, headless ``-p``, daemon, HTTP API). It
5
+ is deliberately free of UI/provider specifics — callers supply:
6
+
7
+ • ``provider_fn`` — which model/backend a round calls (built by each adapter)
8
+ • ``tool_executor`` — the local tool registry
9
+ • streaming callbacks — rendered however the adapter likes
10
+
11
+ ``run_turn`` drives :func:`runtime.run_agent`, folds its event stream into a
12
+ structured :class:`TurnResult`, and forwards callbacks live.
13
+
14
+ Because it imports only ``runtime`` primitives (no ``apps.cli``, no concrete
15
+ providers), it can be reused from another process/repo — e.g. the Arthera
16
+ FastAPI backend — so the CLI and the API share ONE agent loop instead of
17
+ re-implementing turn management three times.
18
+
19
+ Streaming note: ``run_agent`` emits tool-call / tool-result / status / complete /
20
+ cancelled / error *events*, but streams **tokens and thinking through callbacks**
21
+ (it never yields token events). So ``run_turn`` passes ``on_token``/``on_thinking``
22
+ straight to ``run_agent`` (single fire, enables live streaming) while consuming
23
+ tool/status/lifecycle via the event stream (avoids double-firing those).
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ from dataclasses import dataclass
29
+ from typing import Any, Callable, List, Optional
30
+
31
+ # Import from the submodule (not the ``runtime`` package) so this module can be
32
+ # imported from ``runtime/__init__`` without a circular dependency.
33
+ from runtime.agent_loop import (
34
+ AgentEventCancelled,
35
+ AgentEventComplete,
36
+ AgentEventError,
37
+ AgentEventStatus,
38
+ AgentEventToolCall,
39
+ AgentEventToolResult,
40
+ AgentOptions,
41
+ run_agent,
42
+ )
43
+
44
+
45
+ @dataclass(frozen=True)
46
+ class TurnResult:
47
+ """Adapter-agnostic outcome of one agent turn."""
48
+
49
+ text: str = ""
50
+ final: Any = None # AgentTurnResult when the turn completed
51
+ error: Optional[str] = None
52
+ cancelled: bool = False
53
+
54
+ @property
55
+ def ok(self) -> bool:
56
+ return self.error is None and not self.cancelled
57
+
58
+
59
+ async def run_turn(
60
+ prompt: str,
61
+ history: list,
62
+ *,
63
+ provider_fn: Callable,
64
+ tool_executor,
65
+ tool_schemas: Optional[List[dict]] = None,
66
+ on_token: Optional[Callable[[str], None]] = None,
67
+ on_thinking: Optional[Callable[[str], None]] = None,
68
+ on_tool_call: Optional[Callable[[str, dict], None]] = None,
69
+ on_tool_result: Optional[Callable[[str, dict], None]] = None,
70
+ on_status: Optional[Callable[[str, str], None]] = None,
71
+ cancel_event=None,
72
+ max_rounds: int = 30,
73
+ ) -> TurnResult:
74
+ """Drive one ``run_agent`` turn; return its text + lifecycle as a TurnResult.
75
+
76
+ Tokens are forwarded to ``on_token`` as they stream and accumulated here, so
77
+ the returned ``text`` is independent of the result object's field names; it
78
+ falls back to the turn result's authoritative ``final_text`` only if nothing
79
+ streamed (e.g. a non-streaming provider).
80
+ """
81
+ schemas = list(tool_schemas or [])
82
+ acc: List[str] = []
83
+ final = None
84
+ error: Optional[str] = None
85
+ cancelled = False
86
+
87
+ def _on_token(tok: str) -> None:
88
+ acc.append(tok)
89
+ if on_token is not None:
90
+ on_token(tok)
91
+
92
+ async for ev in run_agent(
93
+ prompt,
94
+ history,
95
+ provider_fn=provider_fn,
96
+ tool_executor=tool_executor,
97
+ options=AgentOptions(max_rounds=max_rounds, tool_schemas=schemas),
98
+ on_token=_on_token, # streamed live (run_agent emits no token events)
99
+ on_thinking=on_thinking, # streamed live (no thinking events either)
100
+ cancel_event=cancel_event,
101
+ ):
102
+ if isinstance(ev, AgentEventToolCall):
103
+ if on_tool_call is not None:
104
+ on_tool_call(ev.tool, dict(ev.params))
105
+ elif isinstance(ev, AgentEventToolResult):
106
+ if on_tool_result is not None:
107
+ on_tool_result(ev.tool, dict(ev.result))
108
+ elif isinstance(ev, AgentEventStatus):
109
+ if on_status is not None:
110
+ phase = getattr(ev, "phase", "") or getattr(ev, "state", "") or ""
111
+ on_status(phase, ev.message)
112
+ elif isinstance(ev, AgentEventComplete):
113
+ final = ev.result
114
+ elif isinstance(ev, AgentEventCancelled):
115
+ cancelled = True
116
+ if not acc:
117
+ partial = getattr(ev, "partial_text", "") or ""
118
+ if partial:
119
+ acc.append(partial)
120
+ break
121
+ elif isinstance(ev, AgentEventError):
122
+ error = ev.error
123
+ break
124
+
125
+ text = "".join(acc)
126
+ if not text and final is not None:
127
+ text = getattr(final, "final_text", "") or ""
128
+ return TurnResult(text=text, final=final, error=error, cancelled=cancelled)
runtime/lsp.py ADDED
@@ -0,0 +1,346 @@
1
+ """Minimal Language Server Protocol (LSP) client for on-demand diagnostics.
2
+
3
+ This is the "biggest gap" vs Claude Code's architecture: real diagnostics from
4
+ a language server (type errors, undefined names, unused imports) rather than
5
+ just a syntax compile check.
6
+
7
+ Design — deliberately one-shot, not a persistent server:
8
+ spawn language server → initialize → didOpen → collect publishDiagnostics →
9
+ shutdown. Each call is self-contained with strict timeouts so it never hangs
10
+ the REPL, and degrades gracefully (returns []) when no server is installed.
11
+
12
+ Supported servers (auto-detected on PATH):
13
+ Python → pylsp (pip install python-lsp-server)
14
+ TS / JS → typescript-language-server --stdio (npm i -g …)
15
+
16
+ Exposed to the LLM as the `lsp_diagnostics` tool and to users via /lsp.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import queue
24
+ import shutil
25
+ import subprocess
26
+ import threading
27
+ import time
28
+ from pathlib import Path
29
+ from typing import Optional
30
+
31
+ # ── Server registry ──────────────────────────────────────────────────────────
32
+ # suffix → (command argv, LSP languageId)
33
+ _SERVERS: dict[str, tuple[list[str], str]] = {
34
+ ".py": (["pylsp"], "python"),
35
+ ".ts": (["typescript-language-server", "--stdio"], "typescript"),
36
+ ".tsx": (["typescript-language-server", "--stdio"], "typescriptreact"),
37
+ ".js": (["typescript-language-server", "--stdio"], "javascript"),
38
+ ".jsx": (["typescript-language-server", "--stdio"], "javascriptreact"),
39
+ ".mjs": (["typescript-language-server", "--stdio"], "javascript"),
40
+ ".cjs": (["typescript-language-server", "--stdio"], "javascript"),
41
+ }
42
+
43
+ _SEVERITY = {1: "error", 2: "warning", 3: "info", 4: "hint"}
44
+
45
+ # Cache PATH lookups so repeated edits don't re-stat the filesystem each time.
46
+ _AVAILABILITY: dict[str, bool] = {}
47
+
48
+
49
+ def server_for(path) -> Optional[tuple[list[str], str]]:
50
+ """Return (argv, languageId) if a language server is installed for this
51
+ file's type, else None."""
52
+ suffix = Path(path).suffix.lower()
53
+ entry = _SERVERS.get(suffix)
54
+ if not entry:
55
+ return None
56
+ cmd, lang = entry
57
+ exe = cmd[0]
58
+ if exe not in _AVAILABILITY:
59
+ _AVAILABILITY[exe] = shutil.which(exe) is not None
60
+ if not _AVAILABILITY[exe]:
61
+ return None
62
+ return cmd, lang
63
+
64
+
65
+ def available_servers() -> dict[str, bool]:
66
+ """Report which known language servers are installed (for /lsp status)."""
67
+ out: dict[str, bool] = {}
68
+ for cmd, _ in _SERVERS.values():
69
+ exe = cmd[0]
70
+ if exe not in out:
71
+ out[exe] = shutil.which(exe) is not None
72
+ return out
73
+
74
+
75
+ # ── JSON-RPC framing ──────────────────────────────────────────────────────────
76
+
77
+ def _encode(msg: dict) -> bytes:
78
+ body = json.dumps(msg).encode("utf-8")
79
+ return f"Content-Length: {len(body)}\r\n\r\n".encode("ascii") + body
80
+
81
+
82
+ def _read_message(stdout) -> Optional[dict]:
83
+ """Read one LSP message (headers + body) from a blocking stream."""
84
+ headers: dict[bytes, bytes] = {}
85
+ while True:
86
+ line = stdout.readline()
87
+ if not line:
88
+ return None
89
+ line = line.strip()
90
+ if not line:
91
+ break # blank line terminates headers
92
+ if b":" in line:
93
+ k, v = line.split(b":", 1)
94
+ headers[k.strip().lower()] = v.strip()
95
+ length = int(headers.get(b"content-length", b"0") or 0)
96
+ if length <= 0:
97
+ return None
98
+ body = stdout.read(length)
99
+ if not body:
100
+ return None
101
+ try:
102
+ return json.loads(body.decode("utf-8"))
103
+ except Exception:
104
+ return None
105
+
106
+
107
+ def _format_diagnostics(raw: list) -> list[dict]:
108
+ out = []
109
+ for d in raw or []:
110
+ start = (d.get("range") or {}).get("start") or {}
111
+ code = d.get("code", "")
112
+ out.append({
113
+ "line": int(start.get("line", 0)) + 1, # LSP is 0-based
114
+ "col": int(start.get("character", 0)) + 1,
115
+ "severity": _SEVERITY.get(d.get("severity", 1), "info"),
116
+ "message": str(d.get("message", "")).strip(),
117
+ "source": d.get("source", "") or "",
118
+ "code": str(code) if code != "" else "",
119
+ })
120
+ out.sort(key=lambda x: (x["line"], x["col"]))
121
+ return out
122
+
123
+
124
+ # ── Core: one-shot diagnostics ────────────────────────────────────────────────
125
+
126
+ def get_diagnostics(path, timeout: float = 8.0) -> list[dict]:
127
+ """Spawn a language server, open `path`, and return its diagnostics.
128
+
129
+ Returns [] if no server is available, the file can't be read, or the
130
+ handshake times out. Never raises.
131
+ """
132
+ resolved = server_for(path)
133
+ if not resolved:
134
+ return []
135
+ cmd, lang_id = resolved
136
+
137
+ p = Path(path).expanduser().resolve()
138
+ try:
139
+ text = p.read_text(errors="replace")
140
+ except Exception:
141
+ return []
142
+
143
+ try:
144
+ proc = subprocess.Popen(
145
+ cmd,
146
+ stdin=subprocess.PIPE,
147
+ stdout=subprocess.PIPE,
148
+ stderr=subprocess.DEVNULL,
149
+ cwd=str(p.parent),
150
+ )
151
+ except Exception:
152
+ return []
153
+
154
+ msg_queue: "queue.Queue[dict]" = queue.Queue()
155
+
156
+ def _reader():
157
+ try:
158
+ while True:
159
+ msg = _read_message(proc.stdout)
160
+ if msg is None:
161
+ break
162
+ msg_queue.put(msg)
163
+ except Exception:
164
+ pass
165
+
166
+ reader = threading.Thread(target=_reader, daemon=True)
167
+ reader.start()
168
+
169
+ def _send(msg: dict) -> None:
170
+ try:
171
+ proc.stdin.write(_encode(msg))
172
+ proc.stdin.flush()
173
+ except Exception:
174
+ pass
175
+
176
+ def _cleanup() -> None:
177
+ try:
178
+ _send({"jsonrpc": "2.0", "id": 9999, "method": "shutdown", "params": None})
179
+ _send({"jsonrpc": "2.0", "method": "exit"})
180
+ except Exception:
181
+ pass
182
+ try:
183
+ proc.terminate()
184
+ proc.wait(timeout=1.0)
185
+ except Exception:
186
+ try:
187
+ proc.kill()
188
+ except Exception:
189
+ pass
190
+
191
+ file_uri = p.as_uri()
192
+ deadline = time.time() + timeout
193
+
194
+ # 1) initialize
195
+ _send({
196
+ "jsonrpc": "2.0", "id": 1, "method": "initialize",
197
+ "params": {
198
+ "processId": os.getpid(),
199
+ "rootUri": p.parent.as_uri(),
200
+ "workspaceFolders": [{"uri": p.parent.as_uri(), "name": p.parent.name}],
201
+ "capabilities": {
202
+ "textDocument": {
203
+ "publishDiagnostics": {"relatedInformation": True},
204
+ "synchronization": {"didSave": True},
205
+ },
206
+ },
207
+ },
208
+ })
209
+
210
+ initialized = False
211
+ while time.time() < deadline:
212
+ try:
213
+ msg = msg_queue.get(timeout=0.2)
214
+ except queue.Empty:
215
+ if proc.poll() is not None:
216
+ _cleanup()
217
+ return []
218
+ continue
219
+ if msg.get("id") == 1 and "result" in msg:
220
+ initialized = True
221
+ break
222
+ if not initialized:
223
+ _cleanup()
224
+ return []
225
+
226
+ # 2) initialized + didOpen
227
+ _send({"jsonrpc": "2.0", "method": "initialized", "params": {}})
228
+ _send({
229
+ "jsonrpc": "2.0", "method": "textDocument/didOpen",
230
+ "params": {
231
+ "textDocument": {
232
+ "uri": file_uri, "languageId": lang_id,
233
+ "version": 1, "text": text,
234
+ },
235
+ },
236
+ })
237
+
238
+ # 3) collect publishDiagnostics for our file. Some servers emit an empty
239
+ # set first, then a populated one after analysis — so once we see a
240
+ # matching notification we keep a short grace window for a better one.
241
+ diagnostics: list = []
242
+ got_one = False
243
+ grace_deadline = None
244
+ while time.time() < deadline:
245
+ remaining = deadline - time.time()
246
+ if grace_deadline is not None:
247
+ remaining = min(remaining, grace_deadline - time.time())
248
+ if remaining <= 0:
249
+ break
250
+ try:
251
+ msg = msg_queue.get(timeout=max(0.05, min(0.2, remaining)))
252
+ except queue.Empty:
253
+ if proc.poll() is not None:
254
+ break
255
+ continue
256
+ if msg.get("method") == "textDocument/publishDiagnostics":
257
+ params = msg.get("params", {})
258
+ if _same_uri(params.get("uri", ""), file_uri, p):
259
+ diags = params.get("diagnostics", [])
260
+ if diags:
261
+ diagnostics = diags
262
+ got_one = True
263
+ break # populated result — done
264
+ if not got_one:
265
+ diagnostics = diags # remember the empty result
266
+ got_one = True
267
+ grace_deadline = time.time() + 1.5 # wait briefly for more
268
+
269
+ _cleanup()
270
+ return _format_diagnostics(diagnostics)
271
+
272
+
273
+ def _same_uri(a: str, b: str, path: Path) -> bool:
274
+ if not a:
275
+ return False
276
+ if a == b:
277
+ return True
278
+ # Tolerate trailing-slash / encoding differences by comparing resolved paths
279
+ try:
280
+ return Path(a.replace("file://", "")).resolve() == path
281
+ except Exception:
282
+ return False
283
+
284
+
285
+ # ── Tool wrapper ──────────────────────────────────────────────────────────────
286
+
287
+ def tool_lsp_diagnostics(params: dict) -> dict:
288
+ """LLM-callable: run language-server diagnostics on a single file."""
289
+ path = params.get("path", "")
290
+ if not path:
291
+ return {"success": False, "error": "Missing 'path' parameter"}
292
+ p = Path(path).expanduser()
293
+ if not p.exists():
294
+ return {"success": False, "error": f"File not found: {p}"}
295
+
296
+ resolved = server_for(p)
297
+ if not resolved:
298
+ return {"success": True, "data": {
299
+ "path": str(p), "diagnostics": [], "available": False,
300
+ "note": f"No language server installed for '{p.suffix}' files. "
301
+ f"Python: pip install 'python-lsp-server[all]' (the [all] extra "
302
+ f"pulls in pyflakes/pycodestyle — without it pylsp reports nothing) · "
303
+ f"TS/JS: npm i -g typescript-language-server typescript",
304
+ }}
305
+
306
+ cmd, _ = resolved
307
+ diags = get_diagnostics(p)
308
+ errors = sum(1 for d in diags if d["severity"] == "error")
309
+ warnings = sum(1 for d in diags if d["severity"] == "warning")
310
+ return {"success": True, "data": {
311
+ "path": str(p),
312
+ "server": cmd[0],
313
+ "available": True,
314
+ "diagnostics": diags,
315
+ "errors": errors,
316
+ "warnings": warnings,
317
+ "total": len(diags),
318
+ }}
319
+
320
+
321
+ # ── Registry (merged into LOCAL_TOOLS in aria_cli.py) ─────────────────────────
322
+
323
+ LSP_TOOLS = {
324
+ "lsp_diagnostics": (tool_lsp_diagnostics,
325
+ "Run language-server diagnostics (errors/warnings) on a code file"),
326
+ }
327
+
328
+ LSP_SCHEMAS = [
329
+ {
330
+ "name": "lsp_diagnostics",
331
+ "description": (
332
+ "Run a language server (pylsp / typescript-language-server) on a single "
333
+ "file and return its diagnostics: type errors, undefined names, unused "
334
+ "imports, lint warnings. Use this after editing code to catch problems a "
335
+ "plain syntax check misses. Returns [] if no server is installed for the "
336
+ "file type."
337
+ ),
338
+ "parameters": {
339
+ "type": "object",
340
+ "properties": {
341
+ "path": {"type": "string", "description": "Path to the code file to analyze"},
342
+ },
343
+ "required": ["path"],
344
+ },
345
+ },
346
+ ]