utim-cli 2.3.4__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.
Files changed (152) hide show
  1. {utim_cli-2.3.4 → utim_cli-2.3.5}/CHANGELOG.md +8 -3
  2. {utim_cli-2.3.4/utim_cli.egg-info → utim_cli-2.3.5}/PKG-INFO +1 -1
  3. {utim_cli-2.3.4 → utim_cli-2.3.5}/pyproject.toml +1 -1
  4. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/__init__.py +1 -1
  5. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/_version.py +1 -1
  6. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/agent.py +89 -32
  7. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/ask_helper.py +5 -1
  8. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/bootstrap.py +39 -15
  9. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/brain.py +992 -909
  10. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/client_utils.py +208 -11
  11. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/config.py +5 -4
  12. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/constants.py +1 -1
  13. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/context_pruner.py +674 -49
  14. utim_cli-2.3.5/utim_cli/crawler/__init__.py +13 -0
  15. utim_cli-2.3.5/utim_cli/crawler/engine.py +344 -0
  16. utim_cli-2.3.5/utim_cli/crawler/markdown_distiller.py +251 -0
  17. utim_cli-2.3.5/utim_cli/evidence_registry.py +498 -0
  18. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/orchestrator.py +139 -90
  19. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/orchestrator_helpers.py +437 -198
  20. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/orchestrator_llm_transport.py +121 -68
  21. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/orchestrator_tool_exec.py +67 -2
  22. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/orchestrator_turn_history.py +100 -73
  23. utim_cli-2.3.5/utim_cli/plugins/hooks.py +168 -0
  24. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/plugins/manager.py +537 -465
  25. utim_cli-2.3.5/utim_cli/plugins/tool_loader.py +129 -0
  26. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/reflection.py +570 -81
  27. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/db.py +3 -0
  28. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/micro_batcher.py +8 -3
  29. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/models.py +37 -23
  30. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/pricing_updater.py +31 -6
  31. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/router.py +131 -0
  32. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/completion_routes.py +211 -94
  33. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/rewards_routes.py +8 -2
  34. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/subagent_manager.py +285 -17
  35. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/task_dispatcher.py +652 -499
  36. utim_cli-2.3.5/utim_cli/task_logger.py +204 -0
  37. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tools.py +517 -267
  38. utim_cli-2.3.5/utim_cli/tools_grep.py +442 -0
  39. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/model_dialog.py +138 -54
  40. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/utim.py +629 -128
  41. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/vector_memory.py +35 -7
  42. {utim_cli-2.3.4 → utim_cli-2.3.5/utim_cli.egg-info}/PKG-INFO +1 -1
  43. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli.egg-info/SOURCES.txt +8 -0
  44. {utim_cli-2.3.4 → utim_cli-2.3.5}/LICENSE +0 -0
  45. {utim_cli-2.3.4 → utim_cli-2.3.5}/MANIFEST.in +0 -0
  46. {utim_cli-2.3.4 → utim_cli-2.3.5}/README.md +0 -0
  47. {utim_cli-2.3.4 → utim_cli-2.3.5}/setup.cfg +0 -0
  48. {utim_cli-2.3.4 → utim_cli-2.3.5}/setup.py +0 -0
  49. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim/__init__.py +0 -0
  50. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/auth.py +0 -0
  51. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/backup.py +0 -0
  52. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/billing.py +0 -0
  53. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/blender_agent.py +0 -0
  54. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/doctor.py +0 -0
  55. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/harbor.py +0 -0
  56. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/knowledge_graph.py +0 -0
  57. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/local_db.py +0 -0
  58. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/logger.py +0 -0
  59. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/mcp_clean_wrapper.py +0 -0
  60. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/mcp_client.py +0 -0
  61. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/mcp_registry.json +0 -0
  62. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/models.txt +0 -0
  63. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/plugins/__init__.py +0 -0
  64. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/plugins/registry.py +0 -0
  65. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/plugins/schema.py +0 -0
  66. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/report.py +0 -0
  67. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/scrapy_search.py +0 -0
  68. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/sdk/__init__.py +0 -0
  69. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/sdk/config.py +0 -0
  70. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/sdk/context.py +0 -0
  71. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/sdk/events.py +0 -0
  72. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/sdk/handlers.py +0 -0
  73. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/sdk/session.py +0 -0
  74. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/sdk/sidecar.py +0 -0
  75. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/__init__.py +0 -0
  76. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/admin_auth.py +0 -0
  77. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/attribution.py +0 -0
  78. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/audit_log.py +0 -0
  79. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/auth.py +0 -0
  80. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/batch_processor.py +0 -0
  81. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/captcha.py +0 -0
  82. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/cli_auth.py +0 -0
  83. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  84. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/about.md +0 -0
  85. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/changelog.md +0 -0
  86. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/docs.md +0 -0
  87. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/features.md +0 -0
  88. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/license.md +0 -0
  89. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/pricing.md +0 -0
  90. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/privacy.md +0 -0
  91. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/refund.md +0 -0
  92. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/support.md +0 -0
  93. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/docs_md/terms.md +0 -0
  94. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/email_utils.py +0 -0
  95. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/exchange_rate.py +0 -0
  96. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/firebase.py +0 -0
  97. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/fix_duplicate_users.py +0 -0
  98. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/history.py +0 -0
  99. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/local_llm.py +0 -0
  100. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/logging_config.py +0 -0
  101. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/model_agent.py +0 -0
  102. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/provision_build.py +0 -0
  103. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/rate_limit.py +0 -0
  104. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/rewards_engine.py +0 -0
  105. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/__init__.py +0 -0
  106. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/admin_db_routes.py +0 -0
  107. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/auth_routes.py +0 -0
  108. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/billing_routes.py +0 -0
  109. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/credit_routes.py +0 -0
  110. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/feedback_routes.py +0 -0
  111. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/marketplace_routes.py +0 -0
  112. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/quota_routes.py +0 -0
  113. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/quota_share_routes.py +0 -0
  114. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/referral_routes.py +0 -0
  115. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/security_routes.py +0 -0
  116. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/session_routes.py +0 -0
  117. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/routes/share_routes.py +0 -0
  118. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/server.py +0 -0
  119. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/server/storage_nodes.py +0 -0
  120. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/share.py +0 -0
  121. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/share_tui.py +0 -0
  122. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/situational_scoring.py +0 -0
  123. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/state.py +0 -0
  124. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/__init__.py +0 -0
  125. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/feedback_dialog.py +0 -0
  126. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/history_dialog.py +0 -0
  127. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/marketplace_app_state.py +0 -0
  128. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/marketplace_dialog.py +0 -0
  129. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  130. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/mcp_dialog.py +0 -0
  131. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/miniagents_dialog.py +0 -0
  132. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/plugins_dialog.py +0 -0
  133. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/publish_dialog.py +0 -0
  134. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/quota_dialog.py +0 -0
  135. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  136. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/quota_share_dialog.py +0 -0
  137. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/resume_dialog.py +0 -0
  138. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/rewards_tui.py +0 -0
  139. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/skills_dialog.py +0 -0
  140. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/subagents_dialog.py +0 -0
  141. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/tasks_dialog.py +0 -0
  142. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/thinking_display.py +0 -0
  143. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/tools_dialog.py +0 -0
  144. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/tui/update_dialog.py +0 -0
  145. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/utilities.py +0 -0
  146. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/utimmodel.txt +0 -0
  147. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/wheel.py +0 -0
  148. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli/workspace.py +0 -0
  149. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli.egg-info/dependency_links.txt +0 -0
  150. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli.egg-info/entry_points.txt +0 -0
  151. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli.egg-info/requires.txt +0 -0
  152. {utim_cli-2.3.4 → utim_cli-2.3.5}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,11 +1,16 @@
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
+
3
10
  ## [2.3.4] - 2026-08-22
4
11
 
5
- ### ⚡ Performance & Polish
12
+ ### ⚡ Performance
6
13
  - 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
14
 
10
15
  ## [2.3.3] - 2026-08-20
11
16
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.3.4
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.3.4"
7
+ version = "2.3.5"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -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.4"
8
+ __version__ = "2.3.5"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -11,6 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.3.4"
14
+ VERSION = "2.3.5"
15
15
 
16
16
 
@@ -286,45 +286,73 @@ class ReActAgent:
286
286
  if all_tools:
287
287
  kwargs["tools"] = all_tools
288
288
 
289
- # Call the LLM with streaming
290
- stream = self.client.chat.completions.create(**kwargs)
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
- for chunk in stream:
294
- delta = chunk.choices[0].delta
295
-
296
- if delta.content:
297
- assistant_content += delta.content
298
-
299
- if delta.tool_calls:
300
- for tc in delta.tool_calls:
301
- tc_index = tc.index if tc.index is not None else 0
302
- if current_tool_call is None or tc_index != current_tool_call.get("index"):
303
- # Flush the previous buffered tool call (check index, not id)
304
- if current_tool_call is not None:
305
- tool_calls.append(current_tool_call)
306
- current_tool_call = {
307
- "index": tc_index,
308
- "id": tc.id or "",
309
- "type": tc.type or "function",
310
- "function": {
311
- "name": tc.function.name if tc.function else "",
312
- "arguments": tc.function.arguments if tc.function else "",
313
- },
314
- }
315
- else:
316
- # Same tool call — accumulate the argument chunks
317
- if tc.id and not current_tool_call["id"]:
318
- current_tool_call["id"] = tc.id
319
- if tc.function and tc.function.arguments:
320
- current_tool_call["function"]["arguments"] += tc.function.arguments
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 = """You are the UTIM Sidecar Assistant, an embedded technical companion running alongside the active UTIM CLI session.
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
- def scan_available_skills():
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
- import os
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
- for name in os.listdir(base_dir):
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
- ydata = yaml.safe_load(parts[1])
188
- if ydata:
189
- description = ydata.get('description', '')
190
- frontmatter_name = ydata.get('name', name)
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": frontmatter_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