utim-cli 2.3.3__tar.gz → 2.3.5__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.3 → utim_cli-2.3.5}/CHANGELOG.md +12 -0
- {utim_cli-2.3.3/utim_cli.egg-info → utim_cli-2.3.5}/PKG-INFO +1 -1
- {utim_cli-2.3.3 → utim_cli-2.3.5}/pyproject.toml +1 -1
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/_version.py +1 -1
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/agent.py +89 -32
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/ask_helper.py +14 -57
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/blender_agent.py +4 -5
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/bootstrap.py +43 -16
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/brain.py +992 -909
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/client_utils.py +352 -27
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/config.py +8 -4
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/constants.py +2 -2
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/context_pruner.py +704 -57
- utim_cli-2.3.5/utim_cli/crawler/__init__.py +13 -0
- utim_cli-2.3.5/utim_cli/crawler/engine.py +344 -0
- utim_cli-2.3.5/utim_cli/crawler/markdown_distiller.py +251 -0
- utim_cli-2.3.5/utim_cli/evidence_registry.py +498 -0
- utim_cli-2.3.5/utim_cli/orchestrator.py +1969 -0
- utim_cli-2.3.5/utim_cli/orchestrator_helpers.py +1505 -0
- utim_cli-2.3.5/utim_cli/orchestrator_llm_transport.py +1319 -0
- utim_cli-2.3.5/utim_cli/orchestrator_tool_exec.py +1599 -0
- utim_cli-2.3.5/utim_cli/orchestrator_turn_history.py +1386 -0
- utim_cli-2.3.5/utim_cli/plugins/hooks.py +168 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/plugins/manager.py +537 -465
- utim_cli-2.3.5/utim_cli/plugins/tool_loader.py +129 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/reflection.py +618 -135
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/db.py +7 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/micro_batcher.py +8 -3
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/models.py +48 -24
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/pricing_updater.py +55 -76
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/router.py +143 -18
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/admin_db_routes.py +21 -14
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/auth_routes.py +2 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/completion_routes.py +306 -134
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/rewards_routes.py +8 -2
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/subagent_manager.py +285 -17
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/task_dispatcher.py +652 -456
- utim_cli-2.3.5/utim_cli/task_logger.py +204 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tools.py +712 -432
- utim_cli-2.3.5/utim_cli/tools_grep.py +442 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/model_dialog.py +196 -71
- utim_cli-2.3.5/utim_cli/tui/tasks_dialog.py +188 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/utim.py +764 -231
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/vector_memory.py +35 -7
- {utim_cli-2.3.3 → utim_cli-2.3.5/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli.egg-info/SOURCES.txt +13 -0
- utim_cli-2.3.3/utim_cli/orchestrator.py +0 -6860
- {utim_cli-2.3.3 → utim_cli-2.3.5}/LICENSE +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/MANIFEST.in +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/README.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/setup.cfg +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/setup.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim/__init__.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/report.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/share.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/state.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/plugins_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/wheel.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.3.5] - 2026-08-25
|
|
4
|
+
|
|
5
|
+
### ⚡ Performance & Fixes
|
|
6
|
+
- **Latency Optimisation**: Micro-batching threshold tuning, zero-delay streaming, and asynchronous background reflection pipeline dispatch.
|
|
7
|
+
- **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.
|
|
8
|
+
- Minor bug fixes
|
|
9
|
+
|
|
10
|
+
## [2.3.4] - 2026-08-22
|
|
11
|
+
|
|
12
|
+
### ⚡ Performance
|
|
13
|
+
- Latency optimizations: Zero-buffer stream reading, asynchronous background experience caching, parallel multi-tool file inspection execution, and micro-batching bypass for interactive sessions.
|
|
14
|
+
|
|
3
15
|
## [2.3.3] - 2026-08-20
|
|
4
16
|
|
|
5
17
|
### 🚀 Performance & Polish
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.5
|
|
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
|
|
@@ -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
|
|
@@ -25,50 +25,17 @@ from rich import box
|
|
|
25
25
|
|
|
26
26
|
# ── Primary & Fallback Free Models ───────────────────────────────────────────
|
|
27
27
|
|
|
28
|
+
from utim_cli.client_utils import get_free_models_fallback_chain
|
|
29
|
+
|
|
28
30
|
PRIMARY_HELPER_MODEL = "openrouter/free"
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
"deepseek/deepseek-r1:free",
|
|
38
|
-
"openai/gpt-oss-20b:free",
|
|
39
|
-
"google/gemma-4-31b-it:free",
|
|
40
|
-
"google/gemma-4-26b-a4b-it:free",
|
|
41
|
-
"nvidia/nemotron-nano-12b-v2-vl:free",
|
|
42
|
-
]
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def get_free_models_fallback_chain() -> List[str]:
|
|
46
|
-
"""Returns prioritized list of free models starting with 'openrouter/free'."""
|
|
47
|
-
chain = [PRIMARY_HELPER_MODEL]
|
|
48
|
-
seen = {PRIMARY_HELPER_MODEL}
|
|
49
|
-
|
|
50
|
-
# Add dynamic live OpenRouter free models if available
|
|
51
|
-
try:
|
|
52
|
-
from utim_cli.tui.model_dialog import fetch_active_openrouter_free_models
|
|
53
|
-
live_free = fetch_active_openrouter_free_models()
|
|
54
|
-
if live_free:
|
|
55
|
-
for m in live_free:
|
|
56
|
-
if m not in seen:
|
|
57
|
-
chain.append(m)
|
|
58
|
-
seen.add(m)
|
|
59
|
-
except Exception:
|
|
60
|
-
pass
|
|
61
|
-
|
|
62
|
-
# Add default free fallbacks
|
|
63
|
-
for m in DEFAULT_FREE_FALLBACKS:
|
|
64
|
-
if m not in seen:
|
|
65
|
-
chain.append(m)
|
|
66
|
-
seen.add(m)
|
|
67
|
-
|
|
68
|
-
return chain
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
HELPER_SYSTEM_PROMPT = """You are the UTIM Sidecar Assistant, an embedded technical companion running alongside the active UTIM CLI session.
|
|
32
|
+
|
|
33
|
+
|
|
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
|
+
|
|
72
39
|
Your mission is to provide real-time architectural insights, explain active agent operations, assess codebase states, and answer developer questions clearly and factually.
|
|
73
40
|
|
|
74
41
|
### OPERATIONAL DIRECTIVES:
|
|
@@ -119,22 +86,12 @@ WEB_SEARCH_TOOL_SCHEMA = {
|
|
|
119
86
|
|
|
120
87
|
def execute_sidecar_websearch(query: str) -> str:
|
|
121
88
|
"""
|
|
122
|
-
Executes a
|
|
123
|
-
Returns direct snippets, URLs, and summaries without spawning subagents.
|
|
89
|
+
Executes a web search using DuckDuckGo search and returns structured markdown research findings from .utim_tmp.
|
|
124
90
|
"""
|
|
125
91
|
try:
|
|
126
|
-
from utim_cli.
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
config.set("subagent_model_web_search", "__non_agent__")
|
|
130
|
-
from utim_cli.tools import web_search
|
|
131
|
-
res = web_search(prompt=query, level="low")
|
|
132
|
-
return res or "No web results found."
|
|
133
|
-
finally:
|
|
134
|
-
if orig is not None:
|
|
135
|
-
config.set("subagent_model_web_search", orig)
|
|
136
|
-
else:
|
|
137
|
-
config.pop("subagent_model_web_search", None)
|
|
92
|
+
from utim_cli.tools import web_search
|
|
93
|
+
res = web_search(prompt=query, level="low")
|
|
94
|
+
return res or "No web results found."
|
|
138
95
|
except Exception as e:
|
|
139
96
|
return f"Web search error: {str(e)}"
|
|
140
97
|
|
|
@@ -427,7 +384,7 @@ def execute_ask_helper(
|
|
|
427
384
|
"max_tokens": 1000,
|
|
428
385
|
},
|
|
429
386
|
stream=False,
|
|
430
|
-
timeout=
|
|
387
|
+
timeout=(15, 300),
|
|
431
388
|
is_reflection=True,
|
|
432
389
|
)
|
|
433
390
|
if resp.status_code == 429:
|
|
@@ -62,11 +62,10 @@ _VISION_MODELS: List[str] = [
|
|
|
62
62
|
"openrouter/free"
|
|
63
63
|
]
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
]
|
|
65
|
+
from utim_cli.client_utils import get_free_models_fallback_chain
|
|
66
|
+
|
|
67
|
+
_CODE_MODELS: List[str] = get_free_models_fallback_chain()
|
|
68
|
+
|
|
70
69
|
|
|
71
70
|
|
|
72
71
|
|
|
@@ -57,12 +57,15 @@ def _sync_global_experience(api_key: str, current_folder: str):
|
|
|
57
57
|
Since .utim is now global (~/.utim), there is no need to copy experience files
|
|
58
58
|
between project folders — they are always available from the shared location.
|
|
59
59
|
"""
|
|
60
|
+
if not api_key or str(api_key).startswith(("sk-or-", "sk-ant-", "sk-proj-", "Bearer ")):
|
|
61
|
+
return
|
|
62
|
+
|
|
60
63
|
import requests
|
|
61
64
|
from utim_cli.auth import SERVER_URL
|
|
62
65
|
|
|
63
66
|
try:
|
|
64
67
|
requests.post(
|
|
65
|
-
f"{SERVER_URL}/auth/last-folder",
|
|
68
|
+
f"{SERVER_URL}/api/auth/last-folder",
|
|
66
69
|
headers={"X-API-Key": api_key},
|
|
67
70
|
json={"folder_path": current_folder},
|
|
68
71
|
timeout=5
|
|
@@ -128,16 +131,25 @@ def _write_utim_md():
|
|
|
128
131
|
from utim_cli.logger import log_error
|
|
129
132
|
log_error("bootstrap", f"Failed to create UTIM.md at {md_path}", e)
|
|
130
133
|
|
|
131
|
-
|
|
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):
|
|
132
139
|
"""
|
|
133
140
|
Scans ~/.utim/skills, .utim/skills, and .agents/skills for directories containing SKILL.md.
|
|
134
141
|
Skills listed in config['disabled_skills'] are excluded.
|
|
135
142
|
Returns:
|
|
136
143
|
dict: A dictionary mapping skill_name to dict containing path, name, description, and keywords.
|
|
137
144
|
"""
|
|
138
|
-
|
|
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
|
|
139
152
|
from pathlib import Path
|
|
140
|
-
import yaml
|
|
141
153
|
from utim_cli.config import config as _cfg
|
|
142
154
|
|
|
143
155
|
disabled: list = _cfg.get("disabled_skills") or []
|
|
@@ -160,16 +172,17 @@ def scan_available_skills():
|
|
|
160
172
|
for base_dir in paths_to_check:
|
|
161
173
|
if not base_dir.exists():
|
|
162
174
|
continue
|
|
163
|
-
|
|
175
|
+
try:
|
|
176
|
+
entries = os.listdir(base_dir)
|
|
177
|
+
except Exception:
|
|
178
|
+
continue
|
|
179
|
+
for name in entries:
|
|
164
180
|
skill_dir = base_dir / name
|
|
165
181
|
if skill_dir.is_dir():
|
|
166
182
|
skill_md = skill_dir / 'SKILL.md'
|
|
167
183
|
if skill_md.exists():
|
|
168
184
|
# Avoid duplicates
|
|
169
|
-
if name in skills:
|
|
170
|
-
continue
|
|
171
|
-
# Skip disabled skills
|
|
172
|
-
if name in disabled:
|
|
185
|
+
if name in skills or name in disabled:
|
|
173
186
|
continue
|
|
174
187
|
|
|
175
188
|
description = ""
|
|
@@ -177,14 +190,17 @@ def scan_available_skills():
|
|
|
177
190
|
content = ""
|
|
178
191
|
try:
|
|
179
192
|
with open(skill_md, 'r', encoding='utf-8') as f:
|
|
180
|
-
content = f.read()
|
|
193
|
+
content = f.read(2048) # Fast bounded read for frontmatter
|
|
181
194
|
if content.startswith('---'):
|
|
182
195
|
parts = content.split('---', 2)
|
|
183
196
|
if len(parts) >= 3:
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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('"\'')
|
|
188
204
|
except Exception:
|
|
189
205
|
pass
|
|
190
206
|
|
|
@@ -209,13 +225,24 @@ def scan_available_skills():
|
|
|
209
225
|
if len(w) > 2 and w not in stop_words:
|
|
210
226
|
words.add(w)
|
|
211
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
|
+
|
|
212
234
|
skills[name] = {
|
|
213
235
|
"path": skill_md,
|
|
214
|
-
"name":
|
|
236
|
+
"name": name,
|
|
237
|
+
"folder_name": name,
|
|
238
|
+
"frontmatter_name": frontmatter_name,
|
|
215
239
|
"description": description,
|
|
216
240
|
"keywords": list(words),
|
|
217
|
-
"content": content
|
|
241
|
+
"content": content,
|
|
242
|
+
"size_bytes": size_bytes,
|
|
218
243
|
}
|
|
244
|
+
_SKILLS_CACHE = skills
|
|
245
|
+
_SKILLS_CACHE_TS = now
|
|
219
246
|
return skills
|
|
220
247
|
|
|
221
248
|
|