utim-cli 2.0.0__tar.gz → 2.0.1__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 (107) hide show
  1. {utim_cli-2.0.0 → utim_cli-2.0.1}/CHANGELOG.md +16 -0
  2. {utim_cli-2.0.0/utim_cli.egg-info → utim_cli-2.0.1}/PKG-INFO +1 -1
  3. {utim_cli-2.0.0 → utim_cli-2.0.1}/pyproject.toml +1 -1
  4. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/__init__.py +1 -1
  5. utim_cli-2.0.1/utim_cli/_check_obj.py +9 -0
  6. utim_cli-2.0.1/utim_cli/_find_mcp.py +9 -0
  7. utim_cli-2.0.1/utim_cli/_find_plan.py +12 -0
  8. utim_cli-2.0.1/utim_cli/_find_tool.py +10 -0
  9. utim_cli-2.0.1/utim_cli/_patch_multimodal.py +125 -0
  10. utim_cli-2.0.1/utim_cli/_test_regex.py +22 -0
  11. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/_version.py +1 -1
  12. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/orchestrator.py +224 -45
  13. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/reflection.py +1873 -1873
  14. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tools.py +113 -5
  15. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/utim.py +2 -2
  16. {utim_cli-2.0.0 → utim_cli-2.0.1/utim_cli.egg-info}/PKG-INFO +1 -1
  17. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli.egg-info/SOURCES.txt +6 -0
  18. {utim_cli-2.0.0 → utim_cli-2.0.1}/LICENSE +0 -0
  19. {utim_cli-2.0.0 → utim_cli-2.0.1}/MANIFEST.in +0 -0
  20. {utim_cli-2.0.0 → utim_cli-2.0.1}/README.md +0 -0
  21. {utim_cli-2.0.0 → utim_cli-2.0.1}/setup.cfg +0 -0
  22. {utim_cli-2.0.0 → utim_cli-2.0.1}/setup.py +0 -0
  23. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/agent.py +0 -0
  24. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/auth.py +0 -0
  25. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/backup.py +0 -0
  26. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/billing.py +0 -0
  27. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/blender_agent.py +0 -0
  28. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/bootstrap.py +0 -0
  29. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/client_utils.py +0 -0
  30. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/config.py +0 -0
  31. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/constants.py +0 -0
  32. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/context_pruner.py +0 -0
  33. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/doctor.py +0 -0
  34. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/harbor.py +0 -0
  35. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/knowledge_graph.py +0 -0
  36. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/local_db.py +0 -0
  37. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/logger.py +0 -0
  38. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/mcp_clean_wrapper.py +0 -0
  39. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/mcp_client.py +0 -0
  40. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/mcp_registry.json +0 -0
  41. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/report.py +0 -0
  42. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/scrapy_search.py +0 -0
  43. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/__init__.py +0 -0
  44. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/auth.py +0 -0
  45. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/db.py +0 -0
  46. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/about.md +0 -0
  47. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/changelog.md +0 -0
  48. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/docs.md +0 -0
  49. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/features.md +0 -0
  50. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/license.md +0 -0
  51. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/pricing.md +0 -0
  52. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/privacy.md +0 -0
  53. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/refund.md +0 -0
  54. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/support.md +0 -0
  55. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/terms.md +0 -0
  56. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/email_utils.py +0 -0
  57. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/exchange_rate.py +0 -0
  58. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/firebase.py +0 -0
  59. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/history.py +0 -0
  60. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/logging_config.py +0 -0
  61. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/models.py +0 -0
  62. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/pricing_updater.py +0 -0
  63. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/rate_limit.py +0 -0
  64. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/router.py +0 -0
  65. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/__init__.py +0 -0
  66. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/auth_routes.py +0 -0
  67. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/completion_routes.py +0 -0
  68. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/credit_routes.py +0 -0
  69. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/feedback_routes.py +0 -0
  70. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/marketplace_routes.py +0 -0
  71. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/quota_routes.py +0 -0
  72. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/quota_share_routes.py +0 -0
  73. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/referral_routes.py +0 -0
  74. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/security_routes.py +0 -0
  75. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/session_routes.py +0 -0
  76. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/share_routes.py +0 -0
  77. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/server.py +0 -0
  78. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/storage_nodes.py +0 -0
  79. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/share.py +0 -0
  80. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/share_tui.py +0 -0
  81. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/situational_scoring.py +0 -0
  82. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/state.py +0 -0
  83. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/__init__.py +0 -0
  84. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/feedback_dialog.py +0 -0
  85. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/history_dialog.py +0 -0
  86. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/marketplace_app_state.py +0 -0
  87. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/marketplace_dialog.py +0 -0
  88. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  89. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/mcp_dialog.py +0 -0
  90. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/miniagents_dialog.py +0 -0
  91. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/model_dialog.py +0 -0
  92. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/publish_dialog.py +0 -0
  93. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/quota_dialog.py +0 -0
  94. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  95. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/quota_share_dialog.py +0 -0
  96. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/resume_dialog.py +0 -0
  97. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/skills_dialog.py +0 -0
  98. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/subagents_dialog.py +0 -0
  99. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/tools_dialog.py +0 -0
  100. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/update_dialog.py +0 -0
  101. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/utilities.py +0 -0
  102. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/vector_memory.py +0 -0
  103. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/workspace.py +0 -0
  104. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli.egg-info/dependency_links.txt +0 -0
  105. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli.egg-info/entry_points.txt +0 -0
  106. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli.egg-info/requires.txt +0 -0
  107. {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.0.1] - 2026-08-04 - Bugfix & Adaptive Iteration Budget
4
+
5
+ ### 🛠️ Fixes
6
+ - **Fixed `unhashable type: 'slice'` crash in `_get_display_arg`**: `run_command` args are now defensively coerced to strings (handles malformed LLM args that arrive as list/dict instead of str).
7
+ - **Fixed thinking spinner persisting after response ends**: `STATE["thinking_topic"]` is cleared immediately after the response completes, so the spinner stops instead of animating through the entire response-end compression.
8
+ - **Response-end compression now runs in a background thread**: The elapsed-time rule prints instantly; context compression happens silently behind the scenes without blocking the UI.
9
+
10
+ ### 🧠 Adaptive Iteration Budget (Window-Aware)
11
+ - **Removed hardcoded "45 iterations max" from system directives**: The iteration budget is now computed at runtime from the model's context window.
12
+ - **Budget scales with context window** using the same buckets as compression intervals:
13
+ - `< 200k` context: budget `25`
14
+ - `200k – 500k` context: budget `35`
15
+ - `500k – 1M` context: budget `40`
16
+ - `1M+` context: budget `45`
17
+ - **One-shot milestone warnings**: Mid-task warning, "5 iterations left" urgent warning, and "+5 granted" notice each fire exactly once per model/iteration (tracked via `_milestones_fired` set) instead of repeating on every request.
18
+
3
19
  ## [2.0.0] - 2026-08-04 - UTIM V2.0 RELEASE
4
20
 
5
21
  ### 🚀 Major Release: UTIM V2.0 — Creators Ecosystem & Adaptive Intelligence
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal.
5
5
  License: MIT
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.0.0"
7
+ version = "2.0.1"
8
8
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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.0.0"
8
+ __version__ = "2.0.1"
9
9
 
10
10
  import builtins, os, pathlib, urllib.parse, re
11
11
 
@@ -0,0 +1,9 @@
1
+ f = open('orchestrator.py', encoding='utf-8').read()
2
+ idx = f.find('Active Objective')
3
+ snippet = f[idx:idx+150]
4
+ print('Found:', 'Active Objective' in snippet)
5
+ print('Has 600:', '[:600]' in snippet)
6
+ print('Has 200:', '[:200]' in snippet)
7
+ # Write to file to avoid encoding issues
8
+ open('_check.txt', 'w', encoding='utf-8').write(snippet)
9
+ print('Written to _check.txt')
@@ -0,0 +1,9 @@
1
+ f = open('orchestrator.py', encoding='utf-8').read()
2
+ idx = f.find('class Orchestrator')
3
+ idx2 = f.find('def __init__', idx)
4
+ # Find mcp_tool_names references
5
+ import re
6
+ for m in re.finditer(r'mcp_tool_names', f):
7
+ line = f[:m.start()].count('\n') + 1
8
+ context = f[max(0,m.start()-80):m.end()+80]
9
+ print(f"Line {line}: ...{repr(context)}...")
@@ -0,0 +1,12 @@
1
+ f = open('tools.py', encoding='utf-8').read()
2
+ idx = f.find('def plan_project')
3
+ # Find the next def or the end of the function
4
+ rest = f[idx:]
5
+ # Find the save-to-file part
6
+ save_idx = rest.find('.utim_tmp/plans/')
7
+ print(f"plan_project starts at: {idx}")
8
+ print(f"save file reference at: {idx + save_idx}")
9
+ print()
10
+ # Show the save block
11
+ block = rest[save_idx-100:save_idx+500]
12
+ print(block)
@@ -0,0 +1,10 @@
1
+ f = open('orchestrator.py', encoding='utf-8').read()
2
+ # Find all "role": "tool" append blocks
3
+ import re
4
+ for m in re.finditer(r'role":\s*"tool"', f):
5
+ start = max(0, m.start() - 100)
6
+ end = min(len(f), m.end() + 300)
7
+ snippet = f[start:end]
8
+ print(f"--- Match at {m.start()} ---")
9
+ print(snippet)
10
+ print()
@@ -0,0 +1,125 @@
1
+ f = open('orchestrator.py', encoding='utf-8').read()
2
+
3
+ # Find the single tool append (second occurrence of _cap_tool_result)
4
+ idx1 = f.find('result = _cap_tool_result(func_name, result)')
5
+ idx2 = f.find('result = _cap_tool_result(func_name, result)', idx1 + 50)
6
+
7
+ # The single append starts at idx2 and ends at the "# --- Repeated Tool" line
8
+ end_marker = '\n # --- Repeated Tool '
9
+ end_idx = f.find(end_marker, idx2)
10
+
11
+ old_block = f[idx2:end_idx]
12
+ print("=== OLD BLOCK ===")
13
+ print(repr(old_block))
14
+ print()
15
+
16
+ # Build new block
17
+ new_block = '''result = _cap_tool_result(func_name, result)
18
+
19
+ # If result is an [Image: path] marker and model is vision-capable,
20
+ # inject the actual image as multimodal content
21
+ _img_match = __import__("re").match(r"^\\[Image:\\s+(.+)\\]$", result.strip())
22
+ if _img_match and func_name in ("read_file",):
23
+ try:
24
+ from utim_cli.tools import is_model_vision_capable
25
+ if is_model_vision_capable():
26
+ _img_path = _img_match.group(1)
27
+ _multimodal = _tool_result_to_multimodal(result, _img_path)
28
+ self.messages.append(
29
+ {
30
+ "role": "tool",
31
+ "tool_call_id": tc_id,
32
+ "name": func_name,
33
+ "content": _multimodal,
34
+ }
35
+ )
36
+ else:
37
+ self.messages.append(
38
+ {
39
+ "role": "tool",
40
+ "tool_call_id": tc_id,
41
+ "name": func_name,
42
+ "content": result,
43
+ }
44
+ )
45
+ except Exception:
46
+ self.messages.append(
47
+ {
48
+ "role": "tool",
49
+ "tool_call_id": tc_id,
50
+ "name": func_name,
51
+ "content": result,
52
+ }
53
+ )
54
+ else:
55
+ self.messages.append(
56
+ {
57
+ "role": "tool",
58
+ "tool_call_id": tc_id,
59
+ "name": func_name,
60
+ "content": result,
61
+ }
62
+ )'''
63
+
64
+ f = f.replace(old_block, new_block)
65
+
66
+ # Now do the parallel one (first occurrence)
67
+ idx1_end = f.find(end_marker, f.find('result = _cap_tool_result(func_name, result)'))
68
+ # Actually the parallel one ends differently - find its actual end
69
+ # It's followed by " else:" not "# --- Repeated Tool"
70
+ old_para_end_marker = '\n else:\n # Single tool'
71
+ old_para_start = f.find('result = _cap_tool_result(func_name, result)')
72
+ old_para_end = f.find(old_para_end_marker, old_para_start)
73
+ old_para_block = f[old_para_start:old_para_end]
74
+
75
+ new_para_block = '''result = _cap_tool_result(func_name, result)
76
+
77
+ # If result is an [Image: path] marker and model is vision-capable,
78
+ # inject the actual image as multimodal content
79
+ _img_match = __import__("re").match(r"^\\[Image:\\s+(.+)\\]$", result.strip())
80
+ if _img_match and func_name in ("read_file",):
81
+ try:
82
+ from utim_cli.tools import is_model_vision_capable
83
+ if is_model_vision_capable():
84
+ _img_path = _img_match.group(1)
85
+ _multimodal = _tool_result_to_multimodal(result, _img_path)
86
+ self.messages.append(
87
+ {
88
+ "role": "tool",
89
+ "tool_call_id": tc_id,
90
+ "name": func_name,
91
+ "content": _multimodal,
92
+ }
93
+ )
94
+ else:
95
+ self.messages.append(
96
+ {
97
+ "role": "tool",
98
+ "tool_call_id": tc_id,
99
+ "name": func_name,
100
+ "content": result,
101
+ }
102
+ )
103
+ except Exception:
104
+ self.messages.append(
105
+ {
106
+ "role": "tool",
107
+ "tool_call_id": tc_id,
108
+ "name": func_name,
109
+ "content": result,
110
+ }
111
+ )
112
+ else:
113
+ self.messages.append(
114
+ {
115
+ "role": "tool",
116
+ "tool_call_id": tc_id,
117
+ "name": func_name,
118
+ "content": result,
119
+ }
120
+ )'''
121
+
122
+ f = f.replace(old_para_block, new_para_block)
123
+
124
+ open('orchestrator.py', 'w', encoding='utf-8').write(f)
125
+ print("OK: both tool append blocks replaced")
@@ -0,0 +1,22 @@
1
+ import re
2
+
3
+ # Test the full noise string
4
+ text = "System.Management.Automation.PSCustomObjectSystem.Object1Preparing modules for first use.0-1-1Completed-1"
5
+
6
+ # Broader pattern: match the whole PowerShell startup noise line
7
+ _PS_NOISE_RE = re.compile(
8
+ r'System\.Management\.Automation\..*?(?:Completed-\d+|$)|'
9
+ r'Preparing modules for first use\..*?(?:Completed-\d+|$)|'
10
+ r'out-file\s*:\s*FileStream was asked to open a device.*?(?:$|\n)|'
11
+ r'For support for devices like.*?(?:$|\n)|'
12
+ r'call CreateFile, then use the FileStream.*?(?:$|\n)|'
13
+ r'CategoryInfo\s*:\s*OpenError.*?(?:$|\n)|'
14
+ r'FullyQualifiedErrorId\s*:\s*FileOpenFailure.*?(?:$|\n)|'
15
+ r'At line:\d+ char:\d+.*?(?:$|\n)',
16
+ re.IGNORECASE | re.DOTALL
17
+ )
18
+
19
+ result = _PS_NOISE_RE.sub("", text).strip()
20
+ print(f"Input: {repr(text)}")
21
+ print(f"Output: {repr(result)}")
22
+ print(f"Empty: {result == ''}")
@@ -11,4 +11,4 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.0.0"
14
+ VERSION = "2.0.1"
@@ -767,8 +767,10 @@ def _get_dynamic_directives(is_compressed: bool, messages: Optional[List[Dict]]
767
767
  "5. **Think-Create-Verify**: Execute tools directly without conversational preambles or text planning. Verify edits with `run_command` (compile/build/test).",
768
768
  "6. **Execution**: Invoke native function-calling schemas directly. Never output raw JSON, <think> tags, or conversational text before tool calls.",
769
769
  "7. **Output & Summary**: Be concise, professional, and clear. Upon finishing, provide a comprehensive summary of completion.",
770
- "8. **Iteration Budget**: Complete all task execution and verification within **45 iterations max**.",
771
770
  ]
771
+ # NOTE: 'Iteration Budget' directive intentionally removed.
772
+ # Iteration limits are now enforced dynamically (window-aware) at runtime
773
+ # via milestone warnings, not hardcoded into the system prompt.
772
774
 
773
775
  return "\n".join(directives)
774
776
 
@@ -1239,6 +1241,10 @@ class Orchestrator:
1239
1241
  self.active_tag: Optional[str] = None # e.g. '@refactor-bot'
1240
1242
  self.active_tag_system_prompt: Optional[str] = None # custom system prompt text
1241
1243
 
1244
+ # ── MCP tool name registry ───────────────────────────────────────
1245
+ # Populated by _discover_mcp_tools() or left empty if MCP is disabled.
1246
+ self.mcp_tool_names: set = set()
1247
+
1242
1248
  # Eager session create removed for local mode - sessions are created by _persist_messages
1243
1249
 
1244
1250
  def abort(self) -> None:
@@ -2391,7 +2397,16 @@ class Orchestrator:
2391
2397
  if not cmd:
2392
2398
  cmds = arguments.get("commands", [])
2393
2399
  if cmds and isinstance(cmds, list):
2394
- cmd = "; ".join(cmds)
2400
+ cmd = "; ".join(str(c) for c in cmds)
2401
+ # Defensive coercion: cmd may be a list, dict, or other non-string type
2402
+ # if the LLM emitted malformed arguments. Coerce to string safely.
2403
+ if not isinstance(cmd, str):
2404
+ if isinstance(cmd, (list, tuple)):
2405
+ cmd = "; ".join(str(c) for c in cmd)
2406
+ elif isinstance(cmd, dict):
2407
+ cmd = " ".join(f"{k}={v}" for k, v in cmd.items())
2408
+ else:
2409
+ cmd = str(cmd) if cmd is not None else ""
2395
2410
  display = (cmd or "")[:80] + ("…" if len(cmd or "") > 80 else "")
2396
2411
  dir_p = arguments.get("dir_path", "")
2397
2412
  if dir_p:
@@ -3246,6 +3261,22 @@ class Orchestrator:
3246
3261
  and param.kind not in (inspect.Parameter.VAR_KEYWORD, inspect.Parameter.VAR_POSITIONAL)
3247
3262
  and name not in filtered_args
3248
3263
  ]
3264
+ if still_missing:
3265
+ # ── Auto-recovery: if the LLM emitted empty args (raw_args == '{}'),
3266
+ # try to recover 'filepath' from recent tool-call history.
3267
+ # This handles the common case where the model forgets to pass
3268
+ # arguments on follow-up calls (e.g. "read that file again").
3269
+ if "filepath" in still_missing and not call_args:
3270
+ try:
3271
+ if not hasattr(self, "_recent_filepaths"):
3272
+ from collections import deque
3273
+ self._recent_filepaths = deque(maxlen=20)
3274
+ if self._recent_filepaths:
3275
+ _recovered = self._recent_filepaths[-1]
3276
+ filtered_args["filepath"] = _recovered
3277
+ still_missing = [n for n in still_missing if n != "filepath"]
3278
+ except Exception:
3279
+ pass
3249
3280
  if still_missing:
3250
3281
  _raw_preview = (raw_args or "")[:200]
3251
3282
  result = (
@@ -3256,6 +3287,15 @@ class Orchestrator:
3256
3287
  f"Check _ARG_ALIASES['{func_name}'] or the tool-call parser."
3257
3288
  )
3258
3289
  else:
3290
+ # Track successful filepath usage for future auto-recovery
3291
+ if "filepath" in filtered_args and isinstance(filtered_args["filepath"], str):
3292
+ try:
3293
+ if not hasattr(self, "_recent_filepaths"):
3294
+ from collections import deque
3295
+ self._recent_filepaths = deque(maxlen=20)
3296
+ self._recent_filepaths.append(filtered_args["filepath"])
3297
+ except Exception:
3298
+ pass
3259
3299
  result = func(**filtered_args)
3260
3300
  except TypeError as e:
3261
3301
  import traceback
@@ -3750,6 +3790,37 @@ class Orchestrator:
3750
3790
  pass
3751
3791
  return 10
3752
3792
 
3793
+ def _get_iteration_budget(self) -> int:
3794
+ """Return the max iteration budget for the current model, scaled by context window.
3795
+
3796
+ Uses the SAME 4-tier buckets as _get_compression_interval:
3797
+ - < 200k context: budget = 25 (small / local models)
3798
+ - 200k - 500k context: budget = 35
3799
+ - 500k - 1M context: budget = 40
3800
+ - 1M+ context: budget = 45 (Llama 4 Scout, Gemini 2.5 Pro)
3801
+ """
3802
+ try:
3803
+ from .server.models import get_model
3804
+ model_entry = get_model(self.model_id)
3805
+ cw = int(getattr(model_entry, "context_window", 0) or 0)
3806
+ except Exception:
3807
+ cw = 128_000
3808
+ if cw < 200_000:
3809
+ return 25
3810
+ if cw < 500_000:
3811
+ return 35
3812
+ if cw < 1_000_000:
3813
+ return 40
3814
+ return 45
3815
+
3816
+ def _get_iteration_milestones(self) -> tuple:
3817
+ """Return (budget, halfway, last5, grant_extra) for current model."""
3818
+ budget = self._get_iteration_budget()
3819
+ halfway = max(1, budget // 2)
3820
+ last_5 = max(1, budget - 5)
3821
+ grant_extra = budget
3822
+ return budget, halfway, last_5, grant_extra
3823
+
3753
3824
  def set_model(self, new_model_id: str) -> None:
3754
3825
  """Set active model ID and instantly update compression threshold and interval."""
3755
3826
  self._update_model_threshold(new_model_id)
@@ -3992,7 +4063,7 @@ class Orchestrator:
3992
4063
  # guard the IndexError is silently swallowed and the model gets no
3993
4064
  # active objective in its system prompt for the rest of the turn.
3994
4065
  if 0 < effective_turn_start < len(messages_snapshot):
3995
- obj = (messages_snapshot[effective_turn_start].get("content") or "")[:200]
4066
+ obj = (messages_snapshot[effective_turn_start].get("content") or "")[:600]
3996
4067
  active_context += f"\n- **Active Objective**: {obj}..."
3997
4068
 
3998
4069
  system_msg["content"] += active_context
@@ -4210,7 +4281,7 @@ class Orchestrator:
4210
4281
 
4211
4282
  if text_parts:
4212
4283
  if instruction:
4213
- self.console.print("\n[dim magenta]⊘ Agent requested context compression: condensing intermediate tool logs...[/dim magenta]")
4284
+ pass # Compression indicator suppressed
4214
4285
  else:
4215
4286
  est_tokens = self._estimate_tokens(self.messages)
4216
4287
  msg_count = len(self.messages)
@@ -4586,26 +4657,39 @@ class Orchestrator:
4586
4657
  turn_iteration = iteration + 1
4587
4658
 
4588
4659
  # ── Milestone Warnings (Non-hardcoded iteration limits) ────────────
4589
- if turn_iteration == 1:
4590
- self.messages.append({
4591
- "role": "user",
4592
- "content": "⚠️ [SYSTEM DIRECTIVE] Finish the task within 45 tool uses."
4593
- })
4594
- elif turn_iteration == 23:
4595
- self.messages.append({
4596
- "role": "user",
4597
- "content": "⚠️ [SYSTEM WARNING] You have already used 23 iterations. Task is still not finished, you have only 22 iterations left. Try to finish within the limit."
4598
- })
4599
- elif turn_iteration == 40:
4600
- self.messages.append({
4601
- "role": "user",
4602
- "content": "⚠️ [SYSTEM URGENT] Only 5 iterations left. Finish the task fast."
4603
- })
4604
- elif turn_iteration == 45:
4605
- self.messages.append({
4606
- "role": "user",
4607
- "content": "⚠️ [SYSTEM NOTICE] +5 iterations granted. No more will be provided. Finish the task."
4608
- })
4660
+ # Window-aware, one-shot iteration guidance. Budget scales with the
4661
+ # model's context window using the same buckets as _get_compression_interval.
4662
+ try:
4663
+ if not hasattr(self, "_milestones_fired"):
4664
+ self._milestones_fired = set()
4665
+ _budget, _halfway, _last5, _grant = self._get_iteration_milestones()
4666
+ _uid = f"{self.model_id}:{turn_iteration}"
4667
+ if turn_iteration == 1 and ("intro", _uid) not in self._milestones_fired:
4668
+ self.messages.append({
4669
+ "role": "user",
4670
+ "content": f"⚠️ [SYSTEM DIRECTIVE] Finish the task within {_budget} tool uses (scaled by model context window).",
4671
+ })
4672
+ self._milestones_fired.add(("intro", _uid))
4673
+ elif turn_iteration == _halfway and ("halfway", _uid) not in self._milestones_fired:
4674
+ self.messages.append({
4675
+ "role": "user",
4676
+ "content": f"⚠️ [SYSTEM WARNING] You have already used {turn_iteration} iterations. Task is still not finished, you have only {_budget - turn_iteration} iterations left. Try to finish within the limit (total budget: {_budget}).",
4677
+ })
4678
+ self._milestones_fired.add(("halfway", _uid))
4679
+ elif turn_iteration == _last5 and ("last5", _uid) not in self._milestones_fired:
4680
+ self.messages.append({
4681
+ "role": "user",
4682
+ "content": f"⚠️ [SYSTEM URGENT] Only 5 iterations left (of {_budget} total). Finish the task fast.",
4683
+ })
4684
+ self._milestones_fired.add(("last5", _uid))
4685
+ elif turn_iteration == _grant and ("grant", _uid) not in self._milestones_fired:
4686
+ self.messages.append({
4687
+ "role": "user",
4688
+ "content": f"⚠️ [SYSTEM NOTICE] +5 iterations granted (previous budget was {_budget}). No more will be provided. Finish the task within this limit.",
4689
+ })
4690
+ self._milestones_fired.add(("grant", _uid))
4691
+ except Exception:
4692
+ pass
4609
4693
 
4610
4694
  # Check for cancellation before each LLM call
4611
4695
  if self.cancel_event.is_set():
@@ -5452,14 +5536,50 @@ class Orchestrator:
5452
5536
  func_name = tc.get("function", {}).get("name", "")
5453
5537
  result = _cap_tool_result(func_name, result)
5454
5538
 
5455
- self.messages.append(
5456
- {
5457
- "role": "tool",
5458
- "tool_call_id": tc_id,
5459
- "name": func_name,
5460
- "content": result,
5461
- }
5462
- )
5539
+ # If result is an [Image: path] marker and model is vision-capable,
5540
+ # inject the actual image as multimodal content
5541
+ _img_match = __import__("re").match(r"^\[Image:\s+(.+)\]$", result.strip())
5542
+ if _img_match and func_name in ("read_file",):
5543
+ try:
5544
+ from utim_cli.tools import is_model_vision_capable
5545
+ if is_model_vision_capable():
5546
+ _img_path = _img_match.group(1)
5547
+ _multimodal = _tool_result_to_multimodal(result, _img_path)
5548
+ self.messages.append(
5549
+ {
5550
+ "role": "tool",
5551
+ "tool_call_id": tc_id,
5552
+ "name": func_name,
5553
+ "content": _multimodal,
5554
+ }
5555
+ )
5556
+ else:
5557
+ self.messages.append(
5558
+ {
5559
+ "role": "tool",
5560
+ "tool_call_id": tc_id,
5561
+ "name": func_name,
5562
+ "content": result,
5563
+ }
5564
+ )
5565
+ except Exception:
5566
+ self.messages.append(
5567
+ {
5568
+ "role": "tool",
5569
+ "tool_call_id": tc_id,
5570
+ "name": func_name,
5571
+ "content": result,
5572
+ }
5573
+ )
5574
+ else:
5575
+ self.messages.append(
5576
+ {
5577
+ "role": "tool",
5578
+ "tool_call_id": tc_id,
5579
+ "name": func_name,
5580
+ "content": result,
5581
+ }
5582
+ )
5463
5583
  else:
5464
5584
  # Single tool - execute directly
5465
5585
  for tc in tool_calls:
@@ -5473,15 +5593,50 @@ class Orchestrator:
5473
5593
  tc_id = tc.get("id") or str(tc.get("index", "0"))
5474
5594
  result = _cap_tool_result(func_name, result)
5475
5595
 
5476
- self.messages.append(
5477
- {
5478
- "role": "tool",
5479
- "tool_call_id": tc_id,
5480
- "name": func_name,
5481
- "content": result,
5482
- }
5483
- )
5484
-
5596
+ # If result is an [Image: path] marker and model is vision-capable,
5597
+ # inject the actual image as multimodal content
5598
+ _img_match = __import__("re").match(r"^\[Image:\s+(.+)\]$", result.strip())
5599
+ if _img_match and func_name in ("read_file",):
5600
+ try:
5601
+ from utim_cli.tools import is_model_vision_capable
5602
+ if is_model_vision_capable():
5603
+ _img_path = _img_match.group(1)
5604
+ _multimodal = _tool_result_to_multimodal(result, _img_path)
5605
+ self.messages.append(
5606
+ {
5607
+ "role": "tool",
5608
+ "tool_call_id": tc_id,
5609
+ "name": func_name,
5610
+ "content": _multimodal,
5611
+ }
5612
+ )
5613
+ else:
5614
+ self.messages.append(
5615
+ {
5616
+ "role": "tool",
5617
+ "tool_call_id": tc_id,
5618
+ "name": func_name,
5619
+ "content": result,
5620
+ }
5621
+ )
5622
+ except Exception:
5623
+ self.messages.append(
5624
+ {
5625
+ "role": "tool",
5626
+ "tool_call_id": tc_id,
5627
+ "name": func_name,
5628
+ "content": result,
5629
+ }
5630
+ )
5631
+ else:
5632
+ self.messages.append(
5633
+ {
5634
+ "role": "tool",
5635
+ "tool_call_id": tc_id,
5636
+ "name": func_name,
5637
+ "content": result,
5638
+ }
5639
+ )
5485
5640
  # --- Repeated Tool Failure Loop Detection ---
5486
5641
  if not hasattr(self, "_tool_failure_counts"):
5487
5642
  self._tool_failure_counts = {}
@@ -5578,7 +5733,7 @@ class Orchestrator:
5578
5733
  interval = getattr(self, "_compression_interval", None) or self._get_dynamic_interval()
5579
5734
  if turn_iteration > 0 and turn_iteration % interval == 0:
5580
5735
  try:
5581
- self.console.print(f"\n[dim magenta]⊘ Periodic context compression (iteration #{turn_iteration}, interval={interval}): condensing intermediate tool logs...[/dim magenta]")
5736
+ pass # Periodic compression indicator suppressed
5582
5737
  self._compress_intra_turn(turn_msg_start, f"Periodic iteration #{turn_iteration} compression")
5583
5738
  except Exception:
5584
5739
  pass
@@ -5662,12 +5817,36 @@ class Orchestrator:
5662
5817
  )
5663
5818
  self.console.print(Rule(f"[dim]⚙ {elapsed_str}[/dim]"))
5664
5819
 
5665
- # Response-end context compression (triggered after agent response ends)
5820
+ # CRITICAL: Clear the thinking-topic spinner IMMEDIATELY after the response
5821
+ # ends so the spinner stops animating. Otherwise the spinner keeps spinning
5822
+ # while background compression runs, which looks like the agent is still working.
5823
+ try:
5824
+ from utim_cli.state import STATE
5825
+ STATE["thinking_topic"] = None
5826
+ except Exception:
5827
+ pass
5828
+
5829
+ # Response-end context compression (triggered after agent response ends).
5830
+ # MUST run in background so it doesn't block the main UI loop. The thinking
5831
+ # indicator is already cleared above, so the user sees the response-end Rule
5832
+ # immediately and compression happens silently behind the scenes.
5666
5833
  if not self.cancel_event.is_set():
5667
5834
  try:
5668
- self._compress_intra_turn(turn_msg_start, instruction="Response end compression")
5835
+ import threading
5836
+ _compress_thread = threading.Thread(
5837
+ target=self._compress_intra_turn,
5838
+ args=(turn_msg_start,),
5839
+ kwargs={"instruction": "Response end compression"},
5840
+ daemon=True,
5841
+ name="utim-response-compress",
5842
+ )
5843
+ _compress_thread.start()
5669
5844
  except Exception:
5670
- pass
5845
+ # Fallback: if threading fails for any reason, run synchronously.
5846
+ try:
5847
+ self._compress_intra_turn(turn_msg_start, instruction="Response end compression")
5848
+ except Exception:
5849
+ pass
5671
5850
 
5672
5851
  # Save turn snapshot for /rewind (even if cancelled — partial work matters)
5673
5852
  # ALWAYS save the turn, even if there are no code changes, so the user can rewind the conversation