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
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
|
+
]
|