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
ui/render/output.py
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
"""Generic tool-result and error rendering for Aria Code.
|
|
2
|
+
|
|
3
|
+
All functions accept console / has_rich as parameters so they stay
|
|
4
|
+
import-free from aria_cli.py and testable in isolation.
|
|
5
|
+
|
|
6
|
+
Public surface
|
|
7
|
+
--------------
|
|
8
|
+
FINANCE_TOOL_NAMES frozenset of tool names with dedicated renderers
|
|
9
|
+
clean_tool_error_message(e) short user-facing string from any exception
|
|
10
|
+
error_hint(msg, context) actionable recovery suggestion
|
|
11
|
+
print_error(msg, context, *, console, has_rich, rich_box)
|
|
12
|
+
print_tool_result(...)
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import difflib
|
|
18
|
+
import pathlib
|
|
19
|
+
import re
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
# ── Finance tool name registry ─────────────────────────────────────────────────
|
|
24
|
+
|
|
25
|
+
FINANCE_TOOL_NAMES: frozenset = frozenset({
|
|
26
|
+
"get_market_data", "get_crypto_data", "get_forex_data",
|
|
27
|
+
"get_commodities_data", "get_futures_data", "calculate_factors",
|
|
28
|
+
"backtest_strategy", "cloud_backtest", "get_risk_metrics",
|
|
29
|
+
"optimize_positions", "get_sector_performance", "get_northbound_flow",
|
|
30
|
+
"screen_ashare", "get_limit_up_pool", "get_market_indices",
|
|
31
|
+
"analyze_news", "get_bonds_data", "get_ai_signal",
|
|
32
|
+
"get_market_insights", "get_predictions",
|
|
33
|
+
"broker_query", "broker_order",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── Tool display helpers ──────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
def tool_display_kind(tool_name: str) -> str:
|
|
40
|
+
"""Return a user-facing service/tool kind without exposing local targets."""
|
|
41
|
+
if tool_name.startswith("mcp__"):
|
|
42
|
+
return "MCP"
|
|
43
|
+
if tool_name in FINANCE_TOOL_NAMES:
|
|
44
|
+
return "finance tool"
|
|
45
|
+
if tool_name in {"web_search", "search_web"}:
|
|
46
|
+
return "web search"
|
|
47
|
+
if tool_name == "web_fetch":
|
|
48
|
+
return "web fetch"
|
|
49
|
+
if tool_name in {"read_file", "write_file", "edit_file", "list_files", "search_code"}:
|
|
50
|
+
return "file tool"
|
|
51
|
+
if tool_name == "run_command":
|
|
52
|
+
return "shell tool"
|
|
53
|
+
if tool_name.startswith("skill") or tool_name in {"TaskCreate", "TaskUpdate"}:
|
|
54
|
+
return "skill"
|
|
55
|
+
if tool_name.startswith("broker_") or tool_name in {"broker_query", "broker_order"}:
|
|
56
|
+
return "broker tool"
|
|
57
|
+
return "tool"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def tool_display_label(tool_name: str) -> str:
|
|
61
|
+
"""Short label for activity UI: tool name plus its service kind."""
|
|
62
|
+
if tool_name.startswith("mcp__"):
|
|
63
|
+
parts = tool_name.split("__")
|
|
64
|
+
if len(parts) >= 3:
|
|
65
|
+
return f"{parts[1]} · {parts[2].replace('_', ' ')} · MCP"
|
|
66
|
+
return "MCP"
|
|
67
|
+
return f"{tool_name} · {tool_display_kind(tool_name)}"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def format_turn_footer(metadata, *, mode: str = "compact", copy_available: bool = False) -> str:
|
|
71
|
+
"""Return the post-response status line.
|
|
72
|
+
|
|
73
|
+
``full`` keeps the historical token-heavy line for debugging. ``compact``
|
|
74
|
+
is the default interactive UI: elapsed time, provider, tools, and /copy.
|
|
75
|
+
"""
|
|
76
|
+
mode = (mode or "compact").strip().lower()
|
|
77
|
+
if mode in {"off", "none", "false", "0"}:
|
|
78
|
+
return ""
|
|
79
|
+
parts = list(getattr(metadata, "parts", []) or [])
|
|
80
|
+
if mode in {"full", "debug", "verbose"}:
|
|
81
|
+
footer = " · ".join(parts)
|
|
82
|
+
if copy_available:
|
|
83
|
+
footer = f"{footer} /copy" if footer else "/copy"
|
|
84
|
+
return footer
|
|
85
|
+
|
|
86
|
+
elapsed = parts[0] if parts else ""
|
|
87
|
+
provider = str(getattr(metadata, "provider", "") or "").strip()
|
|
88
|
+
tools = list(getattr(metadata, "tools", []) or [])
|
|
89
|
+
out: list[str] = []
|
|
90
|
+
if elapsed:
|
|
91
|
+
out.append(elapsed)
|
|
92
|
+
if provider and provider not in {"aws", "local"}:
|
|
93
|
+
out.append(provider)
|
|
94
|
+
if tools:
|
|
95
|
+
shown = " ".join(str(t) for t in tools[:3])
|
|
96
|
+
if len(tools) > 3:
|
|
97
|
+
shown += f" +{len(tools) - 3}"
|
|
98
|
+
out.append(shown)
|
|
99
|
+
if copy_available:
|
|
100
|
+
out.append("/copy")
|
|
101
|
+
return " · ".join(out)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def display_path(path: object, *, fallback: str = "file") -> str:
|
|
105
|
+
"""Return a path-safe display value for user-facing UI."""
|
|
106
|
+
if not path:
|
|
107
|
+
return fallback
|
|
108
|
+
try:
|
|
109
|
+
name = pathlib.Path(str(path)).name
|
|
110
|
+
except Exception:
|
|
111
|
+
name = ""
|
|
112
|
+
return name or fallback
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
# ── Error helpers ──────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
def clean_tool_error_message(error: object) -> str:
|
|
118
|
+
raw = str(error or "failed").strip()
|
|
119
|
+
low = raw.lower()
|
|
120
|
+
if not raw:
|
|
121
|
+
return "操作失败"
|
|
122
|
+
if "curl: (28)" in low or "timed out" in low or "timeout" in low:
|
|
123
|
+
return "请求超时,数据源暂时不可用。请稍后重试或运行 /health 检查服务。"
|
|
124
|
+
if "connection refused" in low:
|
|
125
|
+
return "连接被拒绝,服务暂时不可用。请检查本地服务或网络。"
|
|
126
|
+
if "connection aborted" in low or "remotedisconnected" in low:
|
|
127
|
+
return "网络连接中断,数据源未完成响应。请稍后重试。"
|
|
128
|
+
# Generic connection / proxy / DNS failures — collapse the verbose
|
|
129
|
+
# urllib3 HTTPSConnectionPool(...) dump into a single readable line.
|
|
130
|
+
if any(s in low for s in (
|
|
131
|
+
"httpsconnectionpool", "httpconnectionpool", "max retries exceeded",
|
|
132
|
+
"proxyerror", "failed to establish a new connection",
|
|
133
|
+
"nameresolutionerror", "getaddrinfo failed", "newconnectionerror",
|
|
134
|
+
)):
|
|
135
|
+
import re as _re3
|
|
136
|
+
_host = _re3.search(r"host=['\"]([^'\"]+)['\"]", raw)
|
|
137
|
+
_hint = f"(数据源 {_host.group(1)})" if _host else ""
|
|
138
|
+
return f"数据源连接失败{_hint},可能是网络或代理问题。请检查网络后重试。"
|
|
139
|
+
if "rate" in low or "429" in low or "too many requests" in low:
|
|
140
|
+
return "数据源请求频率受限,请稍后重试。"
|
|
141
|
+
# Collapse verbose HTTP error strings: "web_fetch failed: 401 Client Error: Unauthorized for url: https://..."
|
|
142
|
+
import re as _re
|
|
143
|
+
_http = _re.match(r"web_fetch failed:\s*(\d{3})\s+\w[\w\s]+?:\s*([\w\s]+?)(?:\s+for url:.*)?$", raw, _re.I)
|
|
144
|
+
if _http:
|
|
145
|
+
code, phrase = _http.group(1), _http.group(2).strip()
|
|
146
|
+
return f"HTTP {code} {phrase}"
|
|
147
|
+
if "traceback" in low:
|
|
148
|
+
return raw.splitlines()[-1][:160] if raw.splitlines() else "运行失败"
|
|
149
|
+
return raw[:200]
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def error_hint(error: str, context: str = "") -> str:
|
|
153
|
+
err_lower = error.lower() if error else ""
|
|
154
|
+
if "connection" in err_lower or "refused" in err_lower or "unreachable" in err_lower:
|
|
155
|
+
return "Hint: Backend unreachable. Try /health or check your network."
|
|
156
|
+
if "timeout" in err_lower or "timed out" in err_lower:
|
|
157
|
+
return "Hint: Request timed out. Try again or check /health."
|
|
158
|
+
# External web pages that block scraping (paywall / anti-bot) — NOT an Aria
|
|
159
|
+
# login problem, so /login must not be suggested.
|
|
160
|
+
_is_web = any(m in err_lower for m in (
|
|
161
|
+
"http://", "https://", "www.", ".com", ".org", ".net",
|
|
162
|
+
"web_fetch", "web fetch", "forbidden",
|
|
163
|
+
))
|
|
164
|
+
if "401" in err_lower or "unauthorized" in err_lower:
|
|
165
|
+
if any(h in err_lower for h in ("finnhub", "alphavantage", "polygon", "api/v1", "api/v2/finance")):
|
|
166
|
+
return "Hint: API key required — /apikey set finnhub <KEY> (free at finnhub.io)"
|
|
167
|
+
if _is_web:
|
|
168
|
+
return "Hint: This site blocks automated access (paywall/anti-bot). Try another source."
|
|
169
|
+
return "Hint: Authentication required. Run /login to sign in."
|
|
170
|
+
if "403" in err_lower or "forbidden" in err_lower:
|
|
171
|
+
if _is_web:
|
|
172
|
+
return "Hint: This site blocks automated access (paywall/anti-bot). Try another source."
|
|
173
|
+
return "Hint: Access denied. Check your API key or subscription."
|
|
174
|
+
if "429" in err_lower or "rate" in err_lower:
|
|
175
|
+
return "Hint: Rate limited. Wait a moment and try again."
|
|
176
|
+
if ("ollama" in err_lower or "ollama http" in err_lower) and (
|
|
177
|
+
"not found" in err_lower or "404" in err_lower
|
|
178
|
+
):
|
|
179
|
+
m = re.search(r"model ['\"]?([^'\"]+)['\"]? not found", err_lower)
|
|
180
|
+
model_hint = m.group(1) if m else "the requested model"
|
|
181
|
+
try:
|
|
182
|
+
from local_llm_provider import list_ollama_models
|
|
183
|
+
available = list_ollama_models("http://localhost:11434")
|
|
184
|
+
if available:
|
|
185
|
+
suggestion = available[0]
|
|
186
|
+
return (
|
|
187
|
+
f"Hint: Ollama model '{model_hint}' not found.\n"
|
|
188
|
+
f" Available: {', '.join(available[:4])}\n"
|
|
189
|
+
f" Run: /config model {suggestion}"
|
|
190
|
+
)
|
|
191
|
+
except Exception:
|
|
192
|
+
pass
|
|
193
|
+
return (
|
|
194
|
+
f"Hint: Ollama model not found. Run `ollama list` to see available models.\n"
|
|
195
|
+
f" Or pull one: ollama pull qwen2.5-coder:7b"
|
|
196
|
+
)
|
|
197
|
+
# "File not found" is a path error. Tell the model firmly NOT to keep
|
|
198
|
+
# guessing filenames (it otherwise loops app.py→script.py→main.py…).
|
|
199
|
+
if "file not found" in err_lower or "no such file" in err_lower:
|
|
200
|
+
return ("Hint: This file does not exist. Do NOT guess other filenames — "
|
|
201
|
+
"list the directory first, or this question may not need a file at all.")
|
|
202
|
+
if "404" in err_lower and context == "tool":
|
|
203
|
+
return "Hint: Tool not available. Check /tools for available tools."
|
|
204
|
+
if "not found" in err_lower and context == "session":
|
|
205
|
+
return "Hint: Session not found. Run /sessions to list available."
|
|
206
|
+
if "404" in err_lower or ("not found" in err_lower and context not in ("tool", "")):
|
|
207
|
+
return "Hint: Resource not found. Check the symbol or path."
|
|
208
|
+
if "no data" in err_lower or "no result" in err_lower:
|
|
209
|
+
return "Hint: No data returned. Verify the symbol spelling."
|
|
210
|
+
if "500" in err_lower or "internal" in err_lower:
|
|
211
|
+
return "Hint: Server error. Try again in a moment or /health to check."
|
|
212
|
+
if context == "login":
|
|
213
|
+
return "Hint: Check email/password. Usage: /login email password"
|
|
214
|
+
return ""
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
# ── Error panel ────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
def print_error(
|
|
220
|
+
msg: str,
|
|
221
|
+
context: str = "",
|
|
222
|
+
*,
|
|
223
|
+
console,
|
|
224
|
+
has_rich: bool,
|
|
225
|
+
rich_box,
|
|
226
|
+
) -> None:
|
|
227
|
+
if has_rich:
|
|
228
|
+
from rich.panel import Panel
|
|
229
|
+
hint = error_hint(msg, context)
|
|
230
|
+
body = f"[red]{msg}[/red]"
|
|
231
|
+
if hint:
|
|
232
|
+
body += f"\n[dim]{hint}[/dim]"
|
|
233
|
+
console.print(Panel(body, border_style="red", box=rich_box.ROUNDED, padding=(0, 1)))
|
|
234
|
+
else:
|
|
235
|
+
print(msg)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ── Tool result ────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
def print_tool_result(
|
|
241
|
+
tool_name: str,
|
|
242
|
+
result: dict,
|
|
243
|
+
elapsed: float = 0,
|
|
244
|
+
params: dict = None,
|
|
245
|
+
*,
|
|
246
|
+
console,
|
|
247
|
+
has_rich: bool,
|
|
248
|
+
rich_box,
|
|
249
|
+
print_finance_fn, # callable(tool_name, result) for finance tools
|
|
250
|
+
bot_mode: bool = False,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Render a tool result summary — Codex-style ⎿ tree connector."""
|
|
253
|
+
if bot_mode:
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
ts = f" [dim]{elapsed:.1f}s[/dim]" if elapsed >= 0.1 else ""
|
|
257
|
+
ts_plain = f" {elapsed:.1f}s" if elapsed >= 0.1 else ""
|
|
258
|
+
params = params or {}
|
|
259
|
+
|
|
260
|
+
if tool_name in FINANCE_TOOL_NAMES:
|
|
261
|
+
print_finance_fn(tool_name, result)
|
|
262
|
+
if ts and has_rich:
|
|
263
|
+
console.print(f" [dim]⎿[/dim]{ts}")
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
if result.get("success"):
|
|
267
|
+
data = result.get("data", {})
|
|
268
|
+
|
|
269
|
+
if tool_name == "write_file":
|
|
270
|
+
lines = data.get("lines") or (params.get("content", "").count("\n") + 1 if params.get("content") else 0)
|
|
271
|
+
size = data.get("size_bytes") or len((params.get("content", "") or "").encode())
|
|
272
|
+
size_str = f"{size}B" if size < 1024 else f"{size // 1024}KB"
|
|
273
|
+
if has_rich:
|
|
274
|
+
console.print(f" [dim]⎿[/dim] [green]✓[/green] [dim]file tool {lines} lines {size_str}[/dim]{ts}")
|
|
275
|
+
else:
|
|
276
|
+
print(f" ⎿ ✓ file tool {lines} lines {size_str}{ts_plain}")
|
|
277
|
+
|
|
278
|
+
elif tool_name == "edit_file":
|
|
279
|
+
old = params.get("old_string", "")
|
|
280
|
+
new = params.get("new_string", "")
|
|
281
|
+
if old and new and has_rich:
|
|
282
|
+
import re as _re_diff
|
|
283
|
+
diff = list(difflib.unified_diff(
|
|
284
|
+
old.splitlines(),
|
|
285
|
+
new.splitlines(),
|
|
286
|
+
lineterm="",
|
|
287
|
+
))
|
|
288
|
+
if diff:
|
|
289
|
+
_hdr = " [dim]⎿[/dim] [#C08050]file tool[/#C08050]"
|
|
290
|
+
console.print(f"{_hdr}{ts}")
|
|
291
|
+
o_ln = n_ln = 0
|
|
292
|
+
for line in diff[2:]:
|
|
293
|
+
# Hunk header: @@ -old_start,n +new_start,n @@
|
|
294
|
+
m = _re_diff.match(r"@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@", line)
|
|
295
|
+
if m:
|
|
296
|
+
o_ln, n_ln = int(m.group(1)), int(m.group(2))
|
|
297
|
+
console.print(f" [dim]…[/dim]")
|
|
298
|
+
continue
|
|
299
|
+
body = line[1:].rstrip()
|
|
300
|
+
if line.startswith("+"):
|
|
301
|
+
console.print(f" [dim]{n_ln:>4}[/dim] [green]+ {body}[/green]")
|
|
302
|
+
n_ln += 1
|
|
303
|
+
elif line.startswith("-"):
|
|
304
|
+
console.print(f" [dim]{o_ln:>4}[/dim] [red]- {body}[/red]")
|
|
305
|
+
o_ln += 1
|
|
306
|
+
else:
|
|
307
|
+
console.print(f" [dim]{n_ln:>4}[/dim] [dim] {body}[/dim]")
|
|
308
|
+
o_ln += 1
|
|
309
|
+
n_ln += 1
|
|
310
|
+
else:
|
|
311
|
+
console.print(f" [dim]⎿ no change[/dim]{ts}")
|
|
312
|
+
elif has_rich:
|
|
313
|
+
console.print(f" [dim]⎿ edited[/dim]{ts}")
|
|
314
|
+
else:
|
|
315
|
+
print(f" ⎿ edited{ts_plain}")
|
|
316
|
+
|
|
317
|
+
elif tool_name == "run_command":
|
|
318
|
+
stdout = data.get("stdout", "").strip()
|
|
319
|
+
returncode = data.get("returncode", data.get("exit_code", 0))
|
|
320
|
+
if has_rich:
|
|
321
|
+
from rich.panel import Panel
|
|
322
|
+
rc_color = "green" if returncode == 0 else "red"
|
|
323
|
+
rc_icon = "✓" if returncode == 0 else "✗"
|
|
324
|
+
console.print(f" [dim]⎿[/dim] [{rc_color}]{rc_icon} exit {returncode}[/{rc_color}]{ts}")
|
|
325
|
+
if stdout:
|
|
326
|
+
out_lines = stdout.splitlines()
|
|
327
|
+
if len(out_lines) > 3:
|
|
328
|
+
truncated = "\n".join(out_lines[:12])
|
|
329
|
+
if len(out_lines) > 12:
|
|
330
|
+
truncated += f"\n[dim]… +{len(out_lines) - 12} lines[/dim]"
|
|
331
|
+
if data.get("full_output_path"):
|
|
332
|
+
truncated += f"\n[dim]full output saved: {data.get('full_output_path')}[/dim]"
|
|
333
|
+
console.print(Panel(
|
|
334
|
+
f"[dim]{truncated}[/dim]",
|
|
335
|
+
border_style="dim",
|
|
336
|
+
box=rich_box.SIMPLE,
|
|
337
|
+
padding=(0, 1),
|
|
338
|
+
))
|
|
339
|
+
else:
|
|
340
|
+
for ol in out_lines:
|
|
341
|
+
console.print(f" [dim]{ol[:120]}[/dim]")
|
|
342
|
+
if data.get("full_output_path"):
|
|
343
|
+
console.print(" [dim]full output saved[/dim]")
|
|
344
|
+
else:
|
|
345
|
+
print(f" ⎿ exit {returncode}{ts_plain}")
|
|
346
|
+
for ol in stdout.splitlines()[:4]:
|
|
347
|
+
print(f" {ol[:100]}")
|
|
348
|
+
|
|
349
|
+
elif tool_name == "read_file":
|
|
350
|
+
lines = data.get("lines", 0)
|
|
351
|
+
if has_rich:
|
|
352
|
+
console.print(f" [dim]⎿ file tool {lines} lines[/dim]{ts}")
|
|
353
|
+
else:
|
|
354
|
+
print(f" ⎿ file tool {lines} lines{ts_plain}")
|
|
355
|
+
|
|
356
|
+
elif tool_name == "list_files":
|
|
357
|
+
count = data.get("count", 0)
|
|
358
|
+
if has_rich:
|
|
359
|
+
color = "yellow" if count == 0 else "dim"
|
|
360
|
+
msg = "0 items — no matches" if count == 0 else f"{count} items"
|
|
361
|
+
console.print(f" [{color}]⎿ {msg}[/{color}]{ts}")
|
|
362
|
+
else:
|
|
363
|
+
print(f" ⎿ {count} items{ts_plain}")
|
|
364
|
+
|
|
365
|
+
elif tool_name == "search_code":
|
|
366
|
+
matches = len(data.get("matches", []))
|
|
367
|
+
if has_rich:
|
|
368
|
+
console.print(f" [dim]⎿ {matches} matches[/dim]{ts}")
|
|
369
|
+
else:
|
|
370
|
+
print(f" ⎿ {matches} matches{ts_plain}")
|
|
371
|
+
|
|
372
|
+
elif tool_name == "web_fetch":
|
|
373
|
+
length = data.get("length", 0)
|
|
374
|
+
trunc = data.get("truncated", False)
|
|
375
|
+
len_str = f" {length:,} chars" if length else ""
|
|
376
|
+
trunc_str = " [dim]truncated[/dim]" if trunc else ""
|
|
377
|
+
if has_rich:
|
|
378
|
+
console.print(f" [dim]⎿ web fetch{len_str}[/dim]{trunc_str}{ts}")
|
|
379
|
+
else:
|
|
380
|
+
print(f" ⎿ web fetch{ts_plain}")
|
|
381
|
+
|
|
382
|
+
elif tool_name in ("web_search", "search_web"):
|
|
383
|
+
results = data.get("results", [])
|
|
384
|
+
count = len(results)
|
|
385
|
+
if has_rich:
|
|
386
|
+
console.print(f" [dim]⎿ {count} results[/dim]{ts}")
|
|
387
|
+
else:
|
|
388
|
+
print(f" ⎿ {count} results{ts_plain}")
|
|
389
|
+
|
|
390
|
+
else:
|
|
391
|
+
short = tool_display_kind(tool_name)
|
|
392
|
+
if has_rich:
|
|
393
|
+
console.print(f" [dim]⎿ {short} done[/dim]{ts}")
|
|
394
|
+
else:
|
|
395
|
+
print(f" ⎿ done{ts_plain}")
|
|
396
|
+
|
|
397
|
+
else:
|
|
398
|
+
error = clean_tool_error_message(result.get("error", "failed"))
|
|
399
|
+
hint = error_hint(str(error), context="tool")
|
|
400
|
+
if has_rich:
|
|
401
|
+
console.print(f" [dim]⎿[/dim] [red]✗ {error[:120]}[/red]")
|
|
402
|
+
if hint:
|
|
403
|
+
console.print(f" [dim]{hint}[/dim]")
|
|
404
|
+
else:
|
|
405
|
+
print(f" ⎿ ✗ {error[:80]}")
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ── Activity group (OpenClaw-style batch summary) ──────────────────────────────
|
|
409
|
+
|
|
410
|
+
def _one_line_tool_summary(
|
|
411
|
+
tool_name: str,
|
|
412
|
+
result: dict,
|
|
413
|
+
elapsed: float,
|
|
414
|
+
params: dict,
|
|
415
|
+
) -> tuple[str, str]:
|
|
416
|
+
"""Return (status_markup, detail_markup) for one tool in an activity table."""
|
|
417
|
+
params = params or {}
|
|
418
|
+
ts = f"[dim] {elapsed:.1f}s[/dim]" if elapsed >= 0.1 else ""
|
|
419
|
+
|
|
420
|
+
if not result.get("success"):
|
|
421
|
+
error = clean_tool_error_message(result.get("error", "failed"))
|
|
422
|
+
return "[red]✗[/red]", f"[red]{error[:80]}[/red]{ts}"
|
|
423
|
+
|
|
424
|
+
data = result.get("data", {})
|
|
425
|
+
kind = tool_display_kind(tool_name)
|
|
426
|
+
|
|
427
|
+
if tool_name == "write_file":
|
|
428
|
+
lines = data.get("lines") or (params.get("content", "").count("\n") + 1 if params.get("content") else 0)
|
|
429
|
+
size = data.get("size_bytes") or len((params.get("content", "") or "").encode())
|
|
430
|
+
size_str = f"{size}B" if size < 1024 else f"{size // 1024}KB"
|
|
431
|
+
return "[green]✓[/green]", f"[dim]{kind} {lines} lines {size_str}[/dim]{ts}"
|
|
432
|
+
|
|
433
|
+
elif tool_name == "edit_file":
|
|
434
|
+
return "[green]✓[/green]", f"[dim]edited {kind}[/dim]{ts}"
|
|
435
|
+
|
|
436
|
+
elif tool_name == "run_command":
|
|
437
|
+
rc = data.get("returncode", data.get("exit_code", 0))
|
|
438
|
+
icon = "[green]✓[/green]" if rc == 0 else "[red]✗[/red]"
|
|
439
|
+
color = "green" if rc == 0 else "red"
|
|
440
|
+
suffix = " [dim]· full output saved[/dim]" if data.get("full_output_path") else ""
|
|
441
|
+
return icon, f"[{color}]exit {rc}[/{color}]{suffix}{ts}"
|
|
442
|
+
|
|
443
|
+
elif tool_name == "read_file":
|
|
444
|
+
lines = data.get("lines", 0)
|
|
445
|
+
return "[green]✓[/green]", f"[dim]{kind} {lines} lines[/dim]{ts}"
|
|
446
|
+
|
|
447
|
+
elif tool_name == "list_files":
|
|
448
|
+
count = data.get("count", 0)
|
|
449
|
+
color = "yellow" if count == 0 else "dim"
|
|
450
|
+
msg = "no matches" if count == 0 else f"{count} items"
|
|
451
|
+
return "[green]✓[/green]", f"[{color}]{msg}[/{color}]{ts}"
|
|
452
|
+
|
|
453
|
+
elif tool_name == "search_code":
|
|
454
|
+
matches = len(data.get("matches", []))
|
|
455
|
+
return "[green]✓[/green]", f"[dim]{matches} matches[/dim]{ts}"
|
|
456
|
+
|
|
457
|
+
elif tool_name == "web_fetch":
|
|
458
|
+
length = data.get("length", 0)
|
|
459
|
+
len_s = f" {length:,}c" if length else ""
|
|
460
|
+
return "[green]✓[/green]", f"[dim]{kind}{len_s}[/dim]{ts}"
|
|
461
|
+
|
|
462
|
+
elif tool_name in ("web_search", "search_web"):
|
|
463
|
+
count = len(data.get("results", []))
|
|
464
|
+
return "[green]✓[/green]", f"[dim]{count} results[/dim]{ts}"
|
|
465
|
+
|
|
466
|
+
else:
|
|
467
|
+
return "[green]✓[/green]", f"[dim]{kind} done[/dim]{ts}"
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def print_tool_activity_group(
|
|
471
|
+
results: list, # list of (tool_name, result, elapsed, params)
|
|
472
|
+
*,
|
|
473
|
+
console,
|
|
474
|
+
has_rich: bool,
|
|
475
|
+
rich_box,
|
|
476
|
+
print_finance_fn,
|
|
477
|
+
bot_mode: bool = False,
|
|
478
|
+
) -> None:
|
|
479
|
+
"""Render multiple tool results as a compact Activity block (OpenClaw style).
|
|
480
|
+
|
|
481
|
+
For N >= 2 tools: prints a titled table.
|
|
482
|
+
For N == 1: delegates to print_tool_result (single-line).
|
|
483
|
+
"""
|
|
484
|
+
if bot_mode or not results:
|
|
485
|
+
return
|
|
486
|
+
|
|
487
|
+
if len(results) == 1:
|
|
488
|
+
tool_name, result, elapsed, params = results[0]
|
|
489
|
+
print_tool_result(tool_name, result, elapsed, params,
|
|
490
|
+
console=console, has_rich=has_rich, rich_box=rich_box,
|
|
491
|
+
print_finance_fn=print_finance_fn, bot_mode=bot_mode)
|
|
492
|
+
return
|
|
493
|
+
|
|
494
|
+
total_elapsed = sum(e for _, _, e, _ in results)
|
|
495
|
+
n = len(results)
|
|
496
|
+
|
|
497
|
+
# Finance tools: print with dedicated renderer, then add to activity table
|
|
498
|
+
finance_rows = []
|
|
499
|
+
for tool_name, result, elapsed, params in results:
|
|
500
|
+
if tool_name in FINANCE_TOOL_NAMES:
|
|
501
|
+
print_finance_fn(tool_name, result)
|
|
502
|
+
finance_rows.append(tool_name)
|
|
503
|
+
|
|
504
|
+
if has_rich:
|
|
505
|
+
from rich.table import Table
|
|
506
|
+
ts_total = f" [dim]{total_elapsed:.1f}s[/dim]" if total_elapsed >= 0.1 else ""
|
|
507
|
+
header = f"[dim]Activity · {n} tools[/dim]{ts_total}"
|
|
508
|
+
console.print(f"\n {header}")
|
|
509
|
+
tbl = Table.grid(padding=(0, 2))
|
|
510
|
+
tbl.add_column(no_wrap=True, min_width=14, style="dim") # tool name
|
|
511
|
+
tbl.add_column(no_wrap=True, min_width=2) # status icon
|
|
512
|
+
tbl.add_column() # detail
|
|
513
|
+
|
|
514
|
+
from collections import OrderedDict
|
|
515
|
+
_mcp_groups: "OrderedDict[str, list]" = OrderedDict()
|
|
516
|
+
for tool_name, result, elapsed, params in results:
|
|
517
|
+
if tool_name in finance_rows:
|
|
518
|
+
icon = "[green]✓[/green]" if result.get("success") else "[red]✗[/red]"
|
|
519
|
+
tbl.add_row(tool_name, icon, "")
|
|
520
|
+
elif tool_name.startswith("mcp__"):
|
|
521
|
+
# Defer MCP calls — collapse per server below
|
|
522
|
+
_server = tool_name.split("__")[1] if len(tool_name.split("__")) >= 2 else "mcp"
|
|
523
|
+
_mcp_groups.setdefault(_server, []).append((tool_name, result))
|
|
524
|
+
else:
|
|
525
|
+
icon, detail = _one_line_tool_summary(tool_name, result, elapsed, params)
|
|
526
|
+
tbl.add_row(f"[dim]{tool_name}[/dim]", icon, detail)
|
|
527
|
+
|
|
528
|
+
# Collapsed MCP rows: "server · tool" for one, "called N times" for many
|
|
529
|
+
for _server, _calls in _mcp_groups.items():
|
|
530
|
+
_all_ok = all(r.get("success") for _, r in _calls)
|
|
531
|
+
_icon = "[green]✓[/green]" if _all_ok else "[red]✗[/red]"
|
|
532
|
+
if len(_calls) == 1:
|
|
533
|
+
_tn = _calls[0][0].split("__")
|
|
534
|
+
_label = _tn[2].replace("_", " ") if len(_tn) >= 3 else _server
|
|
535
|
+
tbl.add_row(f"[dim]{_server}[/dim]", _icon, f"[dim]{_label} · MCP[/dim]")
|
|
536
|
+
else:
|
|
537
|
+
tbl.add_row(f"[dim]{_server}[/dim]", _icon,
|
|
538
|
+
f"[dim]called {len(_calls)} times · MCP[/dim]")
|
|
539
|
+
|
|
540
|
+
from rich.padding import Padding
|
|
541
|
+
console.print(Padding(tbl, (0, 0, 0, 4)))
|
|
542
|
+
|
|
543
|
+
# For run_command with stdout, still print the output panel
|
|
544
|
+
for tool_name, result, elapsed, params in results:
|
|
545
|
+
if tool_name == "run_command" and result.get("success"):
|
|
546
|
+
stdout = result.get("data", {}).get("stdout", "").strip()
|
|
547
|
+
if stdout:
|
|
548
|
+
from rich.panel import Panel
|
|
549
|
+
out_lines = stdout.splitlines()
|
|
550
|
+
if len(out_lines) > 3:
|
|
551
|
+
truncated = "\n".join(out_lines[:12])
|
|
552
|
+
if len(out_lines) > 12:
|
|
553
|
+
truncated += f"\n[dim]… +{len(out_lines) - 12} lines[/dim]"
|
|
554
|
+
if result.get("data", {}).get("full_output_path"):
|
|
555
|
+
truncated += f"\n[dim]full output saved: {result.get('data', {}).get('full_output_path')}[/dim]"
|
|
556
|
+
console.print(Panel(f"[dim]{truncated}[/dim]",
|
|
557
|
+
border_style="dim", box=rich_box.SIMPLE,
|
|
558
|
+
padding=(0, 1)))
|
|
559
|
+
else:
|
|
560
|
+
for ol in out_lines:
|
|
561
|
+
console.print(f" [dim]{ol[:120]}[/dim]")
|
|
562
|
+
|
|
563
|
+
# For edit_file, still print diff
|
|
564
|
+
elif tool_name == "edit_file" and result.get("success"):
|
|
565
|
+
old = (params or {}).get("old_string", "")
|
|
566
|
+
new = (params or {}).get("new_string", "")
|
|
567
|
+
if old and new:
|
|
568
|
+
diff = list(difflib.unified_diff(
|
|
569
|
+
old.splitlines(keepends=True),
|
|
570
|
+
new.splitlines(keepends=True),
|
|
571
|
+
lineterm="",
|
|
572
|
+
))
|
|
573
|
+
for line in diff[2:]:
|
|
574
|
+
if line.startswith("+"):
|
|
575
|
+
console.print(f" [green]{line.rstrip()}[/green]")
|
|
576
|
+
elif line.startswith("-"):
|
|
577
|
+
console.print(f" [red]{line.rstrip()}[/red]")
|
|
578
|
+
else:
|
|
579
|
+
ts_total = f" {total_elapsed:.1f}s" if total_elapsed >= 0.1 else ""
|
|
580
|
+
print(f"\n Activity · {n} tools{ts_total}")
|
|
581
|
+
for tool_name, result, elapsed, params in results:
|
|
582
|
+
icon, detail = _one_line_tool_summary(tool_name, result, elapsed, params)
|
|
583
|
+
detail_plain = re.sub(r"\[/?[^\]]+\]", "", detail)
|
|
584
|
+
icon_plain = "✓" if result.get("success") else "✗"
|
|
585
|
+
print(f" {tool_name:<18}{icon_plain} {detail_plain}")
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
# ── Fallback / model-switch toast ──────────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
def print_fallback_toast(
|
|
591
|
+
from_provider: str,
|
|
592
|
+
to_provider: str,
|
|
593
|
+
reason: str = "",
|
|
594
|
+
*,
|
|
595
|
+
console,
|
|
596
|
+
has_rich: bool,
|
|
597
|
+
) -> None:
|
|
598
|
+
"""Show a transient yellow notification when the active model/provider switches."""
|
|
599
|
+
if not has_rich:
|
|
600
|
+
print(f"\n ⚡ 模型切换 {from_provider} → {to_provider}{(' ' + reason) if reason else ''}")
|
|
601
|
+
return
|
|
602
|
+
body = f"[bold #C08050]⚡[/bold #C08050] [#C08050]{from_provider}[/#C08050] [dim]→[/dim] [#C08050]{to_provider}[/#C08050]"
|
|
603
|
+
if reason:
|
|
604
|
+
body += f"\n [dim]{reason}[/dim]"
|
|
605
|
+
console.print(f"\n {body}")
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
# ── Context pressure warning ───────────────────────────────────────────────────
|
|
609
|
+
|
|
610
|
+
_CTX_WARNED: dict[str, float] = {} # session_id → last warn time
|
|
611
|
+
|
|
612
|
+
def print_context_warning(
|
|
613
|
+
est_tokens: int,
|
|
614
|
+
max_tokens: int,
|
|
615
|
+
*,
|
|
616
|
+
console,
|
|
617
|
+
has_rich: bool,
|
|
618
|
+
session_id: str = "",
|
|
619
|
+
cooldown: float = 120.0, # only warn once every 2 min per session
|
|
620
|
+
) -> None:
|
|
621
|
+
"""Warn when context is >85% full; rate-limited to avoid spam."""
|
|
622
|
+
if max_tokens <= 0:
|
|
623
|
+
return
|
|
624
|
+
ratio = est_tokens / max_tokens
|
|
625
|
+
if ratio < 0.85:
|
|
626
|
+
return
|
|
627
|
+
now = time.monotonic()
|
|
628
|
+
if now - _CTX_WARNED.get(session_id, 0) < cooldown:
|
|
629
|
+
return
|
|
630
|
+
_CTX_WARNED[session_id] = now
|
|
631
|
+
|
|
632
|
+
def _k(n: int) -> str:
|
|
633
|
+
return f"{n // 1000}K" if n >= 1000 else str(n)
|
|
634
|
+
|
|
635
|
+
pct = int(ratio * 100)
|
|
636
|
+
if has_rich:
|
|
637
|
+
color = "red" if ratio >= 0.95 else "#C08050"
|
|
638
|
+
icon = "●" if ratio >= 0.95 else "⚠"
|
|
639
|
+
msg = f" [{color}]{icon} 上下文 {pct}% 已满 ({_k(est_tokens)}/{_k(max_tokens)} tokens)[/{color}]"
|
|
640
|
+
msg += " [dim]→ /compact 压缩历史 /clear 重置[/dim]"
|
|
641
|
+
console.print(msg)
|
|
642
|
+
else:
|
|
643
|
+
print(f" ⚠ 上下文 {pct}% ({_k(est_tokens)}/{_k(max_tokens)} tokens) — /compact 或 /clear")
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
# ── Blocked / cancelled tool visual ───────────────────────────────────────────
|
|
647
|
+
|
|
648
|
+
def print_tool_blocked(
|
|
649
|
+
tool_name: str,
|
|
650
|
+
reason: str = "用户取消",
|
|
651
|
+
*,
|
|
652
|
+
console,
|
|
653
|
+
has_rich: bool,
|
|
654
|
+
) -> None:
|
|
655
|
+
"""Show a styled 'Blocked' line when tool execution is denied or cancelled."""
|
|
656
|
+
if has_rich:
|
|
657
|
+
console.print(
|
|
658
|
+
f" [dim]⎿[/dim] [#C08050]⊘ {tool_name}[/#C08050] [dim]{reason}[/dim]"
|
|
659
|
+
)
|
|
660
|
+
else:
|
|
661
|
+
print(f" ⎿ ⊘ {tool_name} {reason}")
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
# ── Robot thinking / response header ──────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
def print_thinking_header(*, console, has_rich: bool) -> None:
|
|
667
|
+
"""Print a subtle copper 'Aria ▸' header before each AI response stream.
|
|
668
|
+
|
|
669
|
+
Gives the response a clear starting-point rather than appearing inline.
|
|
670
|
+
Called once per turn, right before the first streaming token is printed.
|
|
671
|
+
"""
|
|
672
|
+
if not has_rich:
|
|
673
|
+
return
|
|
674
|
+
console.print("[bold #C08050]▣[/bold #C08050] [dim #C08050]Aria[/dim #C08050]", end=" ")
|
|
675
|
+
|
|
676
|
+
|
|
677
|
+
def print_done_footer(elapsed: float, *, console, has_rich: bool) -> None:
|
|
678
|
+
"""Print a dim elapsed-time line after the response stream ends."""
|
|
679
|
+
if not has_rich:
|
|
680
|
+
return
|
|
681
|
+
console.print(f"\n[dim] ✓ {elapsed:.1f}s[/dim]")
|