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.
Files changed (153) hide show
  1. {utim_cli-2.3.3 → utim_cli-2.3.5}/CHANGELOG.md +12 -0
  2. {utim_cli-2.3.3/utim_cli.egg-info → utim_cli-2.3.5}/PKG-INFO +1 -1
  3. {utim_cli-2.3.3 → utim_cli-2.3.5}/pyproject.toml +1 -1
  4. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/__init__.py +1 -1
  5. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/_version.py +1 -1
  6. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/agent.py +89 -32
  7. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/ask_helper.py +14 -57
  8. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/blender_agent.py +4 -5
  9. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/bootstrap.py +43 -16
  10. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/brain.py +992 -909
  11. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/client_utils.py +352 -27
  12. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/config.py +8 -4
  13. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/constants.py +2 -2
  14. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/context_pruner.py +704 -57
  15. utim_cli-2.3.5/utim_cli/crawler/__init__.py +13 -0
  16. utim_cli-2.3.5/utim_cli/crawler/engine.py +344 -0
  17. utim_cli-2.3.5/utim_cli/crawler/markdown_distiller.py +251 -0
  18. utim_cli-2.3.5/utim_cli/evidence_registry.py +498 -0
  19. utim_cli-2.3.5/utim_cli/orchestrator.py +1969 -0
  20. utim_cli-2.3.5/utim_cli/orchestrator_helpers.py +1505 -0
  21. utim_cli-2.3.5/utim_cli/orchestrator_llm_transport.py +1319 -0
  22. utim_cli-2.3.5/utim_cli/orchestrator_tool_exec.py +1599 -0
  23. utim_cli-2.3.5/utim_cli/orchestrator_turn_history.py +1386 -0
  24. utim_cli-2.3.5/utim_cli/plugins/hooks.py +168 -0
  25. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/plugins/manager.py +537 -465
  26. utim_cli-2.3.5/utim_cli/plugins/tool_loader.py +129 -0
  27. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/reflection.py +618 -135
  28. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/db.py +7 -0
  29. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/micro_batcher.py +8 -3
  30. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/models.py +48 -24
  31. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/pricing_updater.py +55 -76
  32. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/router.py +143 -18
  33. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/admin_db_routes.py +21 -14
  34. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/auth_routes.py +2 -0
  35. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/completion_routes.py +306 -134
  36. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/rewards_routes.py +8 -2
  37. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/subagent_manager.py +285 -17
  38. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/task_dispatcher.py +652 -456
  39. utim_cli-2.3.5/utim_cli/task_logger.py +204 -0
  40. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tools.py +712 -432
  41. utim_cli-2.3.5/utim_cli/tools_grep.py +442 -0
  42. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/model_dialog.py +196 -71
  43. utim_cli-2.3.5/utim_cli/tui/tasks_dialog.py +188 -0
  44. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/utim.py +764 -231
  45. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/vector_memory.py +35 -7
  46. {utim_cli-2.3.3 → utim_cli-2.3.5/utim_cli.egg-info}/PKG-INFO +1 -1
  47. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli.egg-info/SOURCES.txt +13 -0
  48. utim_cli-2.3.3/utim_cli/orchestrator.py +0 -6860
  49. {utim_cli-2.3.3 → utim_cli-2.3.5}/LICENSE +0 -0
  50. {utim_cli-2.3.3 → utim_cli-2.3.5}/MANIFEST.in +0 -0
  51. {utim_cli-2.3.3 → utim_cli-2.3.5}/README.md +0 -0
  52. {utim_cli-2.3.3 → utim_cli-2.3.5}/setup.cfg +0 -0
  53. {utim_cli-2.3.3 → utim_cli-2.3.5}/setup.py +0 -0
  54. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim/__init__.py +0 -0
  55. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/auth.py +0 -0
  56. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/backup.py +0 -0
  57. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/billing.py +0 -0
  58. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/doctor.py +0 -0
  59. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/harbor.py +0 -0
  60. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/knowledge_graph.py +0 -0
  61. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/local_db.py +0 -0
  62. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/logger.py +0 -0
  63. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/mcp_clean_wrapper.py +0 -0
  64. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/mcp_client.py +0 -0
  65. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/mcp_registry.json +0 -0
  66. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/models.txt +0 -0
  67. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/plugins/__init__.py +0 -0
  68. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/plugins/registry.py +0 -0
  69. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/plugins/schema.py +0 -0
  70. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/report.py +0 -0
  71. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/scrapy_search.py +0 -0
  72. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/__init__.py +0 -0
  73. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/config.py +0 -0
  74. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/context.py +0 -0
  75. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/events.py +0 -0
  76. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/handlers.py +0 -0
  77. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/session.py +0 -0
  78. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/sdk/sidecar.py +0 -0
  79. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/__init__.py +0 -0
  80. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/admin_auth.py +0 -0
  81. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/attribution.py +0 -0
  82. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/audit_log.py +0 -0
  83. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/auth.py +0 -0
  84. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/batch_processor.py +0 -0
  85. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/captcha.py +0 -0
  86. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/cli_auth.py +0 -0
  87. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  88. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/about.md +0 -0
  89. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/changelog.md +0 -0
  90. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/docs.md +0 -0
  91. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/features.md +0 -0
  92. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/license.md +0 -0
  93. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/pricing.md +0 -0
  94. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/privacy.md +0 -0
  95. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/refund.md +0 -0
  96. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/support.md +0 -0
  97. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/docs_md/terms.md +0 -0
  98. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/email_utils.py +0 -0
  99. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/exchange_rate.py +0 -0
  100. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/firebase.py +0 -0
  101. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/fix_duplicate_users.py +0 -0
  102. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/history.py +0 -0
  103. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/local_llm.py +0 -0
  104. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/logging_config.py +0 -0
  105. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/model_agent.py +0 -0
  106. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/provision_build.py +0 -0
  107. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/rate_limit.py +0 -0
  108. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/rewards_engine.py +0 -0
  109. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/__init__.py +0 -0
  110. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/billing_routes.py +0 -0
  111. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/credit_routes.py +0 -0
  112. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/feedback_routes.py +0 -0
  113. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/marketplace_routes.py +0 -0
  114. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/quota_routes.py +0 -0
  115. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/quota_share_routes.py +0 -0
  116. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/referral_routes.py +0 -0
  117. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/security_routes.py +0 -0
  118. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/session_routes.py +0 -0
  119. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/routes/share_routes.py +0 -0
  120. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/server.py +0 -0
  121. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/server/storage_nodes.py +0 -0
  122. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/share.py +0 -0
  123. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/share_tui.py +0 -0
  124. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/situational_scoring.py +0 -0
  125. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/state.py +0 -0
  126. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/__init__.py +0 -0
  127. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/feedback_dialog.py +0 -0
  128. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/history_dialog.py +0 -0
  129. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/marketplace_app_state.py +0 -0
  130. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/marketplace_dialog.py +0 -0
  131. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  132. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/mcp_dialog.py +0 -0
  133. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/miniagents_dialog.py +0 -0
  134. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/plugins_dialog.py +0 -0
  135. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/publish_dialog.py +0 -0
  136. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/quota_dialog.py +0 -0
  137. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  138. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/quota_share_dialog.py +0 -0
  139. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/resume_dialog.py +0 -0
  140. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/rewards_tui.py +0 -0
  141. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/skills_dialog.py +0 -0
  142. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/subagents_dialog.py +0 -0
  143. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/thinking_display.py +0 -0
  144. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/tools_dialog.py +0 -0
  145. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/tui/update_dialog.py +0 -0
  146. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/utilities.py +0 -0
  147. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/utimmodel.txt +0 -0
  148. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/wheel.py +0 -0
  149. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli/workspace.py +0 -0
  150. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli.egg-info/dependency_links.txt +0 -0
  151. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli.egg-info/entry_points.txt +0 -0
  152. {utim_cli-2.3.3 → utim_cli-2.3.5}/utim_cli.egg-info/requires.txt +0 -0
  153. {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
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.3"
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.3"
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.3"
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
@@ -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
- DEFAULT_FREE_FALLBACKS = [
31
- "google/gemini-2.5-flash:free",
32
- "meta-llama/llama-3.3-70b-instruct:free",
33
- "cohere/north-mini-code:free",
34
- "poolside/laguna-xs-2.1:free",
35
- "nvidia/nemotron-3-ultra-550b-a55b:free",
36
- "inclusionai/ling-3.0-flash:free",
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 fast non-agent web search using search API proxies and fallback search engines.
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.config import config
127
- orig = config.get("subagent_model_web_search")
128
- try:
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=30,
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
- _CODE_MODELS: List[str] = [
66
- DEFAULT_MODEL,
67
- "cohere/north-mini-code:free",
68
- "openrouter/free"
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
- 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):
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
- 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
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
- 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:
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
- ydata = yaml.safe_load(parts[1])
185
- if ydata:
186
- description = ydata.get('description', '')
187
- 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('"\'')
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": frontmatter_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