utim-cli 2.3.4__tar.gz → 2.3.6__tar.gz
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.
- {utim_cli-2.3.4 → utim_cli-2.3.6}/CHANGELOG.md +16 -3
- {utim_cli-2.3.4/utim_cli.egg-info → utim_cli-2.3.6}/PKG-INFO +1 -1
- {utim_cli-2.3.4 → utim_cli-2.3.6}/pyproject.toml +1 -1
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/__init__.py +27 -1
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/_version.py +1 -1
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/agent.py +89 -32
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/ask_helper.py +5 -1
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/bootstrap.py +39 -15
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/brain.py +992 -909
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/client_utils.py +208 -11
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/config.py +5 -4
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/constants.py +1 -1
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/context_pruner.py +674 -49
- utim_cli-2.3.6/utim_cli/crawler/__init__.py +13 -0
- utim_cli-2.3.6/utim_cli/crawler/engine.py +344 -0
- utim_cli-2.3.6/utim_cli/crawler/markdown_distiller.py +251 -0
- utim_cli-2.3.6/utim_cli/dependency_manager.py +419 -0
- utim_cli-2.3.6/utim_cli/doctor.py +275 -0
- utim_cli-2.3.6/utim_cli/evidence_registry.py +498 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/orchestrator.py +139 -90
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/orchestrator_helpers.py +437 -198
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/orchestrator_llm_transport.py +121 -68
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/orchestrator_tool_exec.py +67 -2
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/orchestrator_turn_history.py +100 -73
- utim_cli-2.3.6/utim_cli/platform_installer.py +462 -0
- utim_cli-2.3.6/utim_cli/plugins/hooks.py +168 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/plugins/manager.py +537 -465
- utim_cli-2.3.6/utim_cli/plugins/tool_loader.py +129 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/reflection.py +570 -81
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/db.py +3 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/micro_batcher.py +8 -3
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/models.py +37 -23
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/pricing_updater.py +31 -6
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/router.py +131 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/completion_routes.py +211 -94
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/rewards_routes.py +8 -2
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/subagent_manager.py +285 -17
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/task_dispatcher.py +652 -499
- utim_cli-2.3.6/utim_cli/task_logger.py +204 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tools.py +542 -272
- utim_cli-2.3.6/utim_cli/tools_grep.py +442 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/model_dialog.py +138 -54
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/utim.py +878 -130
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/vector_memory.py +35 -7
- {utim_cli-2.3.4 → utim_cli-2.3.6/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli.egg-info/SOURCES.txt +10 -0
- utim_cli-2.3.4/utim_cli/doctor.py +0 -128
- {utim_cli-2.3.4 → utim_cli-2.3.6}/LICENSE +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/MANIFEST.in +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/README.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/setup.cfg +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/setup.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim/__init__.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/report.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/admin_db_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/share.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/state.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/plugins_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/tasks_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/wheel.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.4 → utim_cli-2.3.6}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.3.6] - 2026-08-25
|
|
4
|
+
|
|
5
|
+
### 🐞 Small Bug Fix
|
|
6
|
+
- **Vision image fallback on friend laptops**: Fixed silent exception swallowing in the vision-tool flow that prevented image attachments from reaching the model on certain environments (notably a friend's laptop where the read-file path differed). The fallback now properly surfaces the analyze_image path so vision-enabled models always receive the image payload.
|
|
7
|
+
- **Universal core-dependency auto-installation**: UTIM no longer requires users to manually install core dependencies before first run. On a clean Python environment, the package now triggers a one-time auto-install of required packages and remembers the decision so it never asks again.
|
|
8
|
+
- **OS-aware dependency resolution (incl. Termux)**: Optional dependencies are now selected per-platform. Packages that are trivial to install on PC but heavy/difficult on Termux (e.g. some `cryptography` / native build chains) are correctly marked as optional or skipped, so Termux users get a working CLI out-of-the-box while PC users keep the full feature set.
|
|
9
|
+
- **CLI startup safety net**: The `ensure_core_deps()` import-guard in `utim_cli/__init__.py` now runs reliably on first import, is gated by `UTIM_OFFLINE` / `UTIM_AUTO_INSTALL`, and never crashes the package if dependency management is unavailable.
|
|
10
|
+
|
|
11
|
+
## [2.3.5] - 2026-08-25
|
|
12
|
+
|
|
13
|
+
### ⚡ Performance & Fixes
|
|
14
|
+
- **Latency Optimisation**: Micro-batching threshold tuning, zero-delay streaming, and asynchronous background reflection pipeline dispatch.
|
|
15
|
+
- **Model Hallucination & Token Bloat Fix**: Multi-level periodic pattern and stream repetition interceptor that truncates duplicate reasoning and loop hallucinations in real-time, preventing context bloat.
|
|
16
|
+
- Minor bug fixes
|
|
17
|
+
|
|
3
18
|
## [2.3.4] - 2026-08-22
|
|
4
19
|
|
|
5
|
-
### ⚡ Performance
|
|
20
|
+
### ⚡ Performance
|
|
6
21
|
- Latency optimizations: Zero-buffer stream reading, asynchronous background experience caching, parallel multi-tool file inspection execution, and micro-batching bypass for interactive sessions.
|
|
7
|
-
- Dynamic fallback model resolution from server model registry with `openrouter/free` priority.
|
|
8
|
-
- Configured reasoning capability toggling across server admin interface and CLI model selector.
|
|
9
22
|
|
|
10
23
|
## [2.3.3] - 2026-08-20
|
|
11
24
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.6
|
|
4
4
|
Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
|
|
5
5
|
License: Emend AI Proprietary EULA
|
|
6
6
|
Project-URL: Homepage, https://utim.dev
|
|
@@ -5,7 +5,7 @@ try:
|
|
|
5
5
|
from utim_cli._version import VERSION as __version__
|
|
6
6
|
except ImportError:
|
|
7
7
|
# Fallback during first-install / editable-install edge cases
|
|
8
|
-
__version__ = "2.3.
|
|
8
|
+
__version__ = "2.3.6"
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
import builtins, os, pathlib, urllib.parse, re
|
|
@@ -113,3 +113,29 @@ try:
|
|
|
113
113
|
|
|
114
114
|
except Exception:
|
|
115
115
|
pass
|
|
116
|
+
|
|
117
|
+
# ── Startup safety net: ensure core dependencies are present ──
|
|
118
|
+
# This runs once per Python process when the utim_cli package is first imported.
|
|
119
|
+
# Behavior:
|
|
120
|
+
# * UTIM_OFFLINE=1 -> skip (assumed pre-installed)
|
|
121
|
+
# * UTIM_AUTO_INSTALL=0 -> skip (CI / locked-down envs)
|
|
122
|
+
# * non-interactive stdout -> skip (can't prompt)
|
|
123
|
+
# * anything else -> call ensure_core_deps() which may prompt the
|
|
124
|
+
# user the first time and install silently thereafter.
|
|
125
|
+
# * any exception in dep mgmt is swallowed (we never want __init__ to crash)
|
|
126
|
+
try:
|
|
127
|
+
import os as _os_init
|
|
128
|
+
import sys as _sys_init
|
|
129
|
+
if _os_init.environ.get("UTIM_OFFLINE") != "1" and _os_init.environ.get("UTIM_AUTO_INSTALL") != "0":
|
|
130
|
+
# Only run the check once per process
|
|
131
|
+
_already_checked = getattr(_sys_init.modules.get("utim_cli"), "_core_deps_checked", False)
|
|
132
|
+
if not _already_checked:
|
|
133
|
+
_sys_init.modules["utim_cli"]._core_deps_checked = True
|
|
134
|
+
try:
|
|
135
|
+
from utim_cli.dependency_manager import ensure_core_deps
|
|
136
|
+
ensure_core_deps(verbose=False) # quiet by default
|
|
137
|
+
except Exception:
|
|
138
|
+
# Never let dep-checking block CLI startup
|
|
139
|
+
pass
|
|
140
|
+
except Exception:
|
|
141
|
+
pass
|
|
@@ -286,45 +286,73 @@ class ReActAgent:
|
|
|
286
286
|
if all_tools:
|
|
287
287
|
kwargs["tools"] = all_tools
|
|
288
288
|
|
|
289
|
-
#
|
|
290
|
-
|
|
289
|
+
# Provider-safety pass: drop null/empty `tool_calls` keys from
|
|
290
|
+
# history (strict providers 400 on `messages.N.tool_calls: null`).
|
|
291
|
+
kwargs["messages"] = sanitize_messages_for_api(kwargs.get("messages") or [])
|
|
292
|
+
|
|
293
|
+
# Call the LLM with streaming (retry once on transport errors so a
|
|
294
|
+
# mid-stream failure doesn't silently kill the whole run)
|
|
295
|
+
try:
|
|
296
|
+
stream = self.client.chat.completions.create(**kwargs)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
if getattr(self, "_stream_retries", 0) >= 2:
|
|
299
|
+
self.console.print(f"[red]Stream failed repeatedly: {e}[/red]")
|
|
300
|
+
break
|
|
301
|
+
self._stream_retries = getattr(self, "_stream_retries", 0) + 1
|
|
302
|
+
self.console.print(f"[yellow]Stream error ({e}) — retrying...[/yellow]")
|
|
303
|
+
time.sleep(1.5)
|
|
304
|
+
continue
|
|
291
305
|
|
|
292
306
|
# Collect streaming chunks
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
307
|
+
finish_reason = ""
|
|
308
|
+
empty_stream = True
|
|
309
|
+
try:
|
|
310
|
+
for chunk in stream:
|
|
311
|
+
if not chunk.choices:
|
|
312
|
+
continue
|
|
313
|
+
empty_stream = False
|
|
314
|
+
delta = chunk.choices[0].delta
|
|
315
|
+
|
|
316
|
+
if delta.content:
|
|
317
|
+
assistant_content += delta.content
|
|
318
|
+
|
|
319
|
+
if delta.tool_calls:
|
|
320
|
+
for tc in delta.tool_calls:
|
|
321
|
+
tc_index = tc.index if tc.index is not None else 0
|
|
322
|
+
if current_tool_call is None or tc_index != current_tool_call.get("index"):
|
|
323
|
+
# Flush the previous buffered tool call (check index, not id)
|
|
324
|
+
if current_tool_call is not None:
|
|
325
|
+
tool_calls.append(current_tool_call)
|
|
326
|
+
current_tool_call = {
|
|
327
|
+
"index": tc_index,
|
|
328
|
+
"id": tc.id or "",
|
|
329
|
+
"type": tc.type or "function",
|
|
330
|
+
"function": {
|
|
331
|
+
"name": tc.function.name if tc.function else "",
|
|
332
|
+
"arguments": tc.function.arguments if tc.function else "",
|
|
333
|
+
},
|
|
334
|
+
}
|
|
335
|
+
else:
|
|
336
|
+
# Same tool call — accumulate the argument chunks
|
|
337
|
+
if tc.id and not current_tool_call["id"]:
|
|
338
|
+
current_tool_call["id"] = tc.id
|
|
339
|
+
if tc.function and tc.function.arguments:
|
|
340
|
+
current_tool_call["function"]["arguments"] += tc.function.arguments
|
|
341
|
+
|
|
342
|
+
fr = chunk.choices[0].finish_reason
|
|
343
|
+
if fr:
|
|
344
|
+
finish_reason = fr
|
|
345
|
+
except Exception as e:
|
|
346
|
+
self.console.print(f"[yellow]Stream interrupted mid-response ({e}).[/yellow]")
|
|
347
|
+
finish_reason = finish_reason or "aborted"
|
|
348
|
+
self._stream_retries = 0
|
|
321
349
|
|
|
322
350
|
# Flush the last buffered tool call
|
|
323
351
|
if current_tool_call is not None:
|
|
324
352
|
tool_calls.append(current_tool_call)
|
|
325
353
|
|
|
326
354
|
# Parse XML-style tool calls if present in the text content
|
|
327
|
-
from .client_utils import parse_xml_tool_calls
|
|
355
|
+
from .client_utils import parse_xml_tool_calls, sanitize_messages_for_api
|
|
328
356
|
parsed_content, parsed_tool_calls = parse_xml_tool_calls(assistant_content)
|
|
329
357
|
if parsed_tool_calls:
|
|
330
358
|
assistant_content = parsed_content or ""
|
|
@@ -338,8 +366,37 @@ class ReActAgent:
|
|
|
338
366
|
if not tool_calls:
|
|
339
367
|
self.console.print()
|
|
340
368
|
|
|
341
|
-
# If no tool calls, we are done
|
|
369
|
+
# If no tool calls, normally we are done — but check WHY the stream
|
|
370
|
+
# ended, because some models stop abruptly on token budget
|
|
371
|
+
# (finish_reason="length") or return a transiently empty response.
|
|
342
372
|
if not tool_calls:
|
|
373
|
+
if finish_reason == "stop":
|
|
374
|
+
# Clean, intentional completion from the model — this is
|
|
375
|
+
# the normal exit path.
|
|
376
|
+
self._empty_retries = 0
|
|
377
|
+
break
|
|
378
|
+
|
|
379
|
+
if finish_reason in ("length", "max_tokens"):
|
|
380
|
+
# Model hit its output cap mid-task. Keep it going instead
|
|
381
|
+
# of silently ending the turn.
|
|
382
|
+
self.console.print("[dim](output limit hit — continuing automatically)[/dim]")
|
|
383
|
+
self.messages.append({
|
|
384
|
+
"role": "assistant",
|
|
385
|
+
"content": assistant_content or "",
|
|
386
|
+
})
|
|
387
|
+
self.messages.append({
|
|
388
|
+
"role": "user",
|
|
389
|
+
"content": "[system] Your previous response was cut off by the output token limit. Continue exactly where you left off without repeating what you already said.",
|
|
390
|
+
})
|
|
391
|
+
continue
|
|
392
|
+
|
|
393
|
+
if (not assistant_content.strip()) and empty_stream:
|
|
394
|
+
# Provider glitch: completely empty completion. Retry once.
|
|
395
|
+
if getattr(self, "_empty_retries", 0) < 1:
|
|
396
|
+
self._empty_retries = 1
|
|
397
|
+
self.console.print("[dim](empty response from model — retrying)[/dim]")
|
|
398
|
+
continue
|
|
399
|
+
self._empty_retries = 0
|
|
343
400
|
break
|
|
344
401
|
|
|
345
402
|
# Save the assistant message with tool_calls
|
|
@@ -31,7 +31,11 @@ PRIMARY_HELPER_MODEL = "openrouter/free"
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
|
|
34
|
-
HELPER_SYSTEM_PROMPT = """
|
|
34
|
+
HELPER_SYSTEM_PROMPT = """<identity>
|
|
35
|
+
You are the UTIM Sidecar Assistant, an embedded technical companion running alongside the active UTIM CLI session.
|
|
36
|
+
- Your identity is strictly the UTIM Sidecar Assistant. Never identify as ox-alpha, ChatGPT, Claude, Qwen, DeepSeek, or other base models.
|
|
37
|
+
</identity>
|
|
38
|
+
|
|
35
39
|
Your mission is to provide real-time architectural insights, explain active agent operations, assess codebase states, and answer developer questions clearly and factually.
|
|
36
40
|
|
|
37
41
|
### OPERATIONAL DIRECTIVES:
|
|
@@ -131,16 +131,25 @@ def _write_utim_md():
|
|
|
131
131
|
from utim_cli.logger import log_error
|
|
132
132
|
log_error("bootstrap", f"Failed to create UTIM.md at {md_path}", e)
|
|
133
133
|
|
|
134
|
-
|
|
134
|
+
_SKILLS_CACHE = {}
|
|
135
|
+
_SKILLS_CACHE_TS = 0.0
|
|
136
|
+
_SKILLS_CACHE_TTL = 300.0 # seconds
|
|
137
|
+
|
|
138
|
+
def scan_available_skills(force_reload: bool = False):
|
|
135
139
|
"""
|
|
136
140
|
Scans ~/.utim/skills, .utim/skills, and .agents/skills for directories containing SKILL.md.
|
|
137
141
|
Skills listed in config['disabled_skills'] are excluded.
|
|
138
142
|
Returns:
|
|
139
143
|
dict: A dictionary mapping skill_name to dict containing path, name, description, and keywords.
|
|
140
144
|
"""
|
|
141
|
-
|
|
145
|
+
global _SKILLS_CACHE, _SKILLS_CACHE_TS
|
|
146
|
+
import time
|
|
147
|
+
now = time.time()
|
|
148
|
+
if not force_reload and _SKILLS_CACHE and (now - _SKILLS_CACHE_TS) < _SKILLS_CACHE_TTL:
|
|
149
|
+
return _SKILLS_CACHE
|
|
150
|
+
|
|
151
|
+
import os, re
|
|
142
152
|
from pathlib import Path
|
|
143
|
-
import yaml
|
|
144
153
|
from utim_cli.config import config as _cfg
|
|
145
154
|
|
|
146
155
|
disabled: list = _cfg.get("disabled_skills") or []
|
|
@@ -163,16 +172,17 @@ def scan_available_skills():
|
|
|
163
172
|
for base_dir in paths_to_check:
|
|
164
173
|
if not base_dir.exists():
|
|
165
174
|
continue
|
|
166
|
-
|
|
175
|
+
try:
|
|
176
|
+
entries = os.listdir(base_dir)
|
|
177
|
+
except Exception:
|
|
178
|
+
continue
|
|
179
|
+
for name in entries:
|
|
167
180
|
skill_dir = base_dir / name
|
|
168
181
|
if skill_dir.is_dir():
|
|
169
182
|
skill_md = skill_dir / 'SKILL.md'
|
|
170
183
|
if skill_md.exists():
|
|
171
184
|
# Avoid duplicates
|
|
172
|
-
if name in skills:
|
|
173
|
-
continue
|
|
174
|
-
# Skip disabled skills
|
|
175
|
-
if name in disabled:
|
|
185
|
+
if name in skills or name in disabled:
|
|
176
186
|
continue
|
|
177
187
|
|
|
178
188
|
description = ""
|
|
@@ -180,14 +190,17 @@ def scan_available_skills():
|
|
|
180
190
|
content = ""
|
|
181
191
|
try:
|
|
182
192
|
with open(skill_md, 'r', encoding='utf-8') as f:
|
|
183
|
-
content = f.read()
|
|
193
|
+
content = f.read(2048) # Fast bounded read for frontmatter
|
|
184
194
|
if content.startswith('---'):
|
|
185
195
|
parts = content.split('---', 2)
|
|
186
196
|
if len(parts) >= 3:
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
197
|
+
fm = parts[1]
|
|
198
|
+
desc_m = re.search(r'^description:\s*(.+)$', fm, re.MULTILINE)
|
|
199
|
+
if desc_m:
|
|
200
|
+
description = desc_m.group(1).strip().strip('"\'')
|
|
201
|
+
name_m = re.search(r'^name:\s*(.+)$', fm, re.MULTILINE)
|
|
202
|
+
if name_m:
|
|
203
|
+
frontmatter_name = name_m.group(1).strip().strip('"\'')
|
|
191
204
|
except Exception:
|
|
192
205
|
pass
|
|
193
206
|
|
|
@@ -212,13 +225,24 @@ def scan_available_skills():
|
|
|
212
225
|
if len(w) > 2 and w not in stop_words:
|
|
213
226
|
words.add(w)
|
|
214
227
|
|
|
228
|
+
size_bytes = 0
|
|
229
|
+
try:
|
|
230
|
+
size_bytes = skill_md.stat().st_size
|
|
231
|
+
except Exception:
|
|
232
|
+
size_bytes = len(content.encode('utf-8')) if content else 0
|
|
233
|
+
|
|
215
234
|
skills[name] = {
|
|
216
235
|
"path": skill_md,
|
|
217
|
-
"name":
|
|
236
|
+
"name": name,
|
|
237
|
+
"folder_name": name,
|
|
238
|
+
"frontmatter_name": frontmatter_name,
|
|
218
239
|
"description": description,
|
|
219
240
|
"keywords": list(words),
|
|
220
|
-
"content": content
|
|
241
|
+
"content": content,
|
|
242
|
+
"size_bytes": size_bytes,
|
|
221
243
|
}
|
|
244
|
+
_SKILLS_CACHE = skills
|
|
245
|
+
_SKILLS_CACHE_TS = now
|
|
222
246
|
return skills
|
|
223
247
|
|
|
224
248
|
|