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.
- {utim_cli-2.0.0 → utim_cli-2.0.1}/CHANGELOG.md +16 -0
- {utim_cli-2.0.0/utim_cli.egg-info → utim_cli-2.0.1}/PKG-INFO +1 -1
- {utim_cli-2.0.0 → utim_cli-2.0.1}/pyproject.toml +1 -1
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/__init__.py +1 -1
- utim_cli-2.0.1/utim_cli/_check_obj.py +9 -0
- utim_cli-2.0.1/utim_cli/_find_mcp.py +9 -0
- utim_cli-2.0.1/utim_cli/_find_plan.py +12 -0
- utim_cli-2.0.1/utim_cli/_find_tool.py +10 -0
- utim_cli-2.0.1/utim_cli/_patch_multimodal.py +125 -0
- utim_cli-2.0.1/utim_cli/_test_regex.py +22 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/_version.py +1 -1
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/orchestrator.py +224 -45
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/reflection.py +1873 -1873
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tools.py +113 -5
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/utim.py +2 -2
- {utim_cli-2.0.0 → utim_cli-2.0.1/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli.egg-info/SOURCES.txt +6 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/LICENSE +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/MANIFEST.in +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/README.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/setup.cfg +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/setup.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/agent.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/auth.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/backup.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/billing.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/config.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/constants.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/context_pruner.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/doctor.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/harbor.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/local_db.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/logger.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/report.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/db.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/history.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/models.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/router.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/server.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/share.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/state.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/model_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/utilities.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli/workspace.py +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.0.0 → utim_cli-2.0.1}/utim_cli.egg-info/requires.txt +0 -0
- {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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "utim-cli"
|
|
7
|
-
version = "2.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"
|
|
@@ -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 == ''}")
|
|
@@ -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 "")[:
|
|
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
|
-
|
|
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
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
self.
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
"
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
"
|
|
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
|
-
|
|
5456
|
-
|
|
5457
|
-
|
|
5458
|
-
|
|
5459
|
-
|
|
5460
|
-
|
|
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
|
-
|
|
5477
|
-
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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
|
-
|
|
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
|