utim-cli 2.3.1__tar.gz → 2.3.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {utim_cli-2.3.1 → utim_cli-2.3.3}/CHANGELOG.md +10 -0
- {utim_cli-2.3.1/utim_cli.egg-info → utim_cli-2.3.3}/PKG-INFO +1 -1
- {utim_cli-2.3.1 → utim_cli-2.3.3}/pyproject.toml +1 -1
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/_version.py +1 -1
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/context_pruner.py +237 -64
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/orchestrator.py +414 -446
- utim_cli-2.3.3/utim_cli/plugins/__init__.py +10 -0
- utim_cli-2.3.3/utim_cli/plugins/manager.py +465 -0
- utim_cli-2.3.3/utim_cli/plugins/registry.py +120 -0
- utim_cli-2.3.3/utim_cli/plugins/schema.py +230 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/auth.py +11 -47
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/db.py +0 -47
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/micro_batcher.py +12 -18
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/rate_limit.py +9 -5
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/router.py +3 -1
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/__init__.py +3 -1
- utim_cli-2.3.3/utim_cli/server/routes/admin_db_routes.py +507 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/completion_routes.py +52 -40
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/quota_routes.py +76 -12
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/share.py +99 -99
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/state.py +0 -1
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/task_dispatcher.py +62 -20
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tools.py +0 -12
- utim_cli-2.3.3/utim_cli/tui/plugins_dialog.py +351 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/utim.py +24 -40
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/wheel.py +3 -8
- {utim_cli-2.3.1 → utim_cli-2.3.3/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli.egg-info/SOURCES.txt +6 -6
- utim_cli-2.3.1/utim_cli/_check_obj.py +0 -9
- utim_cli-2.3.1/utim_cli/_find_mcp.py +0 -9
- utim_cli-2.3.1/utim_cli/_find_plan.py +0 -12
- utim_cli-2.3.1/utim_cli/_find_tool.py +0 -10
- utim_cli-2.3.1/utim_cli/_patch_multimodal.py +0 -125
- utim_cli-2.3.1/utim_cli/_test_regex.py +0 -22
- {utim_cli-2.3.1 → utim_cli-2.3.3}/LICENSE +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/MANIFEST.in +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/README.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/setup.cfg +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/setup.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim/__init__.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/agent.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/config.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/constants.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/reflection.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/report.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/models.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/model_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [2.3.3] - 2026-08-20
|
|
4
|
+
|
|
5
|
+
### 🚀 Performance & Polish
|
|
6
|
+
- Token usage optimization and minor enhancements
|
|
7
|
+
|
|
8
|
+
## [2.3.2] - 2026-08-20
|
|
9
|
+
|
|
10
|
+
### 🚀 Performance & Fixes
|
|
11
|
+
- Token usage optimization and bug fixes
|
|
12
|
+
|
|
3
13
|
## [2.3.1] - 2026-08-19
|
|
4
14
|
|
|
5
15
|
### 🛠️ Fixes & Enhancements
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: utim-cli
|
|
3
|
-
Version: 2.3.
|
|
3
|
+
Version: 2.3.3
|
|
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
|
|
@@ -67,16 +67,85 @@ CONTINUITY_PATTERNS = [
|
|
|
67
67
|
r"(?i)(tried|attempted|worked|failed|error|fix|patch)",
|
|
68
68
|
]
|
|
69
69
|
|
|
70
|
-
# Regex to
|
|
70
|
+
# Regex to match closed <think>...</think> and <thinking>...</thinking> reasoning blocks
|
|
71
71
|
_THINK_RE = re.compile(r"<think(?:ing)?>.*?</think(?:ing)?>", re.DOTALL | re.IGNORECASE)
|
|
72
|
+
# Regex to match unclosed <think> blocks (e.g. cut off mid-generation)
|
|
73
|
+
_UNCLOSED_THINK_RE = re.compile(r"<think(?:ing)?>.*$", re.DOTALL | re.IGNORECASE)
|
|
74
|
+
|
|
75
|
+
_CONCLUSION_PATTERNS = [
|
|
76
|
+
re.compile(r"(?i)\b(?:therefore|so the (?:fix|solution|plan|approach) is|the root cause is|root cause:|the issue is|to resolve this|we (?:should|need to|must)|deduced that|identified that|conclude that|will modify|will update|found that|analysis shows|strategy:)\b"),
|
|
77
|
+
re.compile(r"(?i)\b(?:call(?:ing)? tool|use tool|execute|replace_file_content|write_to_file|run_command|view_file|read_file|grep_search)\b"),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _distill_reasoning_text(raw_think: str) -> str:
|
|
82
|
+
"""Semantically distill a multi-thousand token reasoning block into a concise takeaway.
|
|
83
|
+
|
|
84
|
+
Preserves complete sentences, technical deductions, file references, and error causes
|
|
85
|
+
WITHOUT using blunt hard character/word truncation caps.
|
|
86
|
+
"""
|
|
87
|
+
if not raw_think:
|
|
88
|
+
return ""
|
|
89
|
+
|
|
90
|
+
# Strip the enclosing <think> tags if present
|
|
91
|
+
cleaned = re.sub(r"^<think(?:ing)?>", "", raw_think, flags=re.IGNORECASE).strip()
|
|
92
|
+
cleaned = re.sub(r"</think(?:ing)?>$", "", cleaned, flags=re.IGNORECASE).strip()
|
|
93
|
+
|
|
94
|
+
if not cleaned:
|
|
95
|
+
return ""
|
|
96
|
+
|
|
97
|
+
# If the reasoning block is already short (concise), keep it verbatim
|
|
98
|
+
if len(cleaned) <= 160:
|
|
99
|
+
return " ".join(cleaned.split())
|
|
100
|
+
|
|
101
|
+
# Split into logical paragraphs and sentences
|
|
102
|
+
paragraphs = [p.strip() for p in cleaned.split("\n") if p.strip()]
|
|
103
|
+
collected_deductions = []
|
|
104
|
+
|
|
105
|
+
# Pass 1: Extract sentences or bullet items matching conclusion/deduction patterns
|
|
106
|
+
for p in paragraphs:
|
|
107
|
+
if p.startswith(("-", "*", "•", "1.", "2.", "3.", "4.", "5.", "#")):
|
|
108
|
+
p_clean = p.lstrip("-*•123456789. #").strip()
|
|
109
|
+
if any(pat.search(p_clean) for pat in _CONCLUSION_PATTERNS) or len(p_clean) < 140:
|
|
110
|
+
if p_clean and p_clean not in collected_deductions:
|
|
111
|
+
collected_deductions.append(p_clean)
|
|
112
|
+
continue
|
|
113
|
+
|
|
114
|
+
sentences = re.split(r'(?<=[.?!])\s+', p)
|
|
115
|
+
for s in sentences:
|
|
116
|
+
s = s.strip()
|
|
117
|
+
if not s:
|
|
118
|
+
continue
|
|
119
|
+
if any(pat.search(s) for pat in _CONCLUSION_PATTERNS):
|
|
120
|
+
if s not in collected_deductions:
|
|
121
|
+
collected_deductions.append(s)
|
|
122
|
+
|
|
123
|
+
if collected_deductions:
|
|
124
|
+
filtered = [d for d in collected_deductions if len(d) > 10]
|
|
125
|
+
if filtered:
|
|
126
|
+
combined = "; ".join(filtered)
|
|
127
|
+
return " ".join(combined.split())
|
|
128
|
+
|
|
129
|
+
# Pass 2: Fallback if no explicit conclusion keywords found:
|
|
130
|
+
# Use the final concluding sentences/paragraph where decisions are synthesized
|
|
131
|
+
final_para = paragraphs[-1]
|
|
132
|
+
sentences = [s.strip() for s in re.split(r'(?<=[.?!])\s+', final_para) if s.strip()]
|
|
133
|
+
if len(sentences) >= 2:
|
|
134
|
+
takeaway = " ".join(sentences[-2:])
|
|
135
|
+
elif sentences:
|
|
136
|
+
takeaway = sentences[0]
|
|
137
|
+
else:
|
|
138
|
+
takeaway = final_para
|
|
139
|
+
|
|
140
|
+
return " ".join(takeaway.split())
|
|
72
141
|
|
|
73
142
|
|
|
74
143
|
def _strip_thinking_blocks(messages: List[Dict], keep_last: int = 1) -> List[Dict]:
|
|
75
|
-
"""
|
|
144
|
+
"""Condense <think>...</think> blocks from all but the last `keep_last` assistant messages.
|
|
76
145
|
|
|
77
|
-
Reasoning traces are only
|
|
78
|
-
|
|
79
|
-
|
|
146
|
+
Reasoning traces are only active in the *current* step; older ones are distilled
|
|
147
|
+
into concise semantic takeaways ([Reasoning: ...]) to save tokens while preserving
|
|
148
|
+
100% of the cognitive deduction and decision rationale without hard caps.
|
|
80
149
|
"""
|
|
81
150
|
result = []
|
|
82
151
|
# Collect indices of assistant messages with content in reverse order
|
|
@@ -85,7 +154,7 @@ def _strip_thinking_blocks(messages: List[Dict], keep_last: int = 1) -> List[Dic
|
|
|
85
154
|
if m.get("role") == "assistant" and m.get("content")
|
|
86
155
|
]
|
|
87
156
|
# The last `keep_last` indices are kept intact
|
|
88
|
-
keep_intact = set(assistant_indices[-keep_last:]) if assistant_indices else set()
|
|
157
|
+
keep_intact = set(assistant_indices[-keep_last:]) if (assistant_indices and keep_last > 0) else set()
|
|
89
158
|
|
|
90
159
|
for i, msg in enumerate(messages):
|
|
91
160
|
if (
|
|
@@ -93,49 +162,60 @@ def _strip_thinking_blocks(messages: List[Dict], keep_last: int = 1) -> List[Dic
|
|
|
93
162
|
and i not in keep_intact
|
|
94
163
|
):
|
|
95
164
|
content = msg.get("content") or ""
|
|
96
|
-
if isinstance(content, str):
|
|
97
|
-
|
|
98
|
-
if
|
|
165
|
+
if isinstance(content, str) and ("<think" in content.lower() or "</think" in content.lower()):
|
|
166
|
+
think_blocks = _THINK_RE.findall(content)
|
|
167
|
+
if not think_blocks and "<think" in content.lower():
|
|
168
|
+
unclosed_match = _UNCLOSED_THINK_RE.search(content)
|
|
169
|
+
if unclosed_match:
|
|
170
|
+
think_blocks = [unclosed_match.group(0)]
|
|
171
|
+
|
|
172
|
+
if think_blocks:
|
|
173
|
+
summaries = []
|
|
174
|
+
for tb in think_blocks:
|
|
175
|
+
distilled = _distill_reasoning_text(tb)
|
|
176
|
+
if distilled:
|
|
177
|
+
summaries.append(distilled)
|
|
178
|
+
|
|
179
|
+
clean_rest = _THINK_RE.sub("", content).strip()
|
|
180
|
+
clean_rest = _UNCLOSED_THINK_RE.sub("", clean_rest).strip()
|
|
181
|
+
|
|
182
|
+
new_parts = []
|
|
183
|
+
if summaries:
|
|
184
|
+
summary_text = " ".join(summaries)
|
|
185
|
+
new_parts.append(f"[Reasoning: {summary_text}]")
|
|
186
|
+
if clean_rest:
|
|
187
|
+
new_parts.append(clean_rest)
|
|
188
|
+
|
|
99
189
|
msg = dict(msg)
|
|
100
|
-
msg["content"] =
|
|
190
|
+
msg["content"] = "\n\n".join(new_parts) if new_parts else None
|
|
101
191
|
result.append(msg)
|
|
102
192
|
return result
|
|
103
193
|
|
|
104
194
|
|
|
105
|
-
def _deduplicate_tool_outputs(messages: List[Dict], keep_last_tools: int =
|
|
106
|
-
"""Compress repeated identical tool responses to a one-liner stub
|
|
107
|
-
WHILE STRICTLY PRESERVING the most recent `keep_last_tools` tool outputs 100% verbatim.
|
|
195
|
+
def _deduplicate_tool_outputs(messages: List[Dict], keep_last_tools: int = 4) -> List[Dict]:
|
|
196
|
+
"""Compress repeated identical tool responses to a one-liner stub.
|
|
108
197
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
198
|
+
When the agent calls the same tool with the same arguments repeatedly and gets
|
|
199
|
+
the same output (e.g. re-reading an unchanged file multiple times), only the
|
|
200
|
+
first occurrence is kept verbatim. Subsequent duplicates outside the active
|
|
201
|
+
`keep_last_tools` tail are replaced with:
|
|
202
|
+
[Duplicate tool output — identical to earlier result, omitted to save tokens]
|
|
112
203
|
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
exempt_indices = set(tool_msg_indices[-keep_last_tools:]) if len(tool_msg_indices) >= 1 else set()
|
|
204
|
+
tool_indices = [i for i, m in enumerate(messages) if m.get("role") == "tool"]
|
|
205
|
+
keep_intact_indices = set(tool_indices[-keep_last_tools:]) if (tool_indices and keep_last_tools > 0) else set()
|
|
116
206
|
|
|
117
|
-
|
|
207
|
+
# Map: (tool_name, content_hash) -> True
|
|
208
|
+
seen: dict = {}
|
|
118
209
|
result = []
|
|
119
210
|
|
|
120
211
|
for i, msg in enumerate(messages):
|
|
121
212
|
if msg.get("role") == "tool":
|
|
122
213
|
content = msg.get("content") or ""
|
|
123
214
|
tool_name = msg.get("name", "")
|
|
124
|
-
|
|
125
|
-
# CRITICAL: Never deduplicate or stub any of the last 5 tool calls!
|
|
126
|
-
if i in exempt_indices:
|
|
127
|
-
result.append(msg)
|
|
128
|
-
continue
|
|
129
|
-
|
|
130
|
-
# Never deduplicate tool error messages
|
|
131
|
-
if isinstance(content, str) and any(err in content.lower() for err in ["error", "exception", "failed", "failure", "traceback", "syntaxerror", "not found"]):
|
|
132
|
-
result.append(msg)
|
|
133
|
-
continue
|
|
134
|
-
|
|
135
215
|
# Use first 300 chars of content for the hash to avoid hashing huge outputs
|
|
136
216
|
content_key = (tool_name, hashlib.md5(content[:300].encode(errors="replace")).hexdigest())
|
|
137
|
-
if content_key in seen and len(content) > 200:
|
|
138
|
-
# Replace
|
|
217
|
+
if i not in keep_intact_indices and content_key in seen and len(content) > 200:
|
|
218
|
+
# Replace with stub
|
|
139
219
|
msg = dict(msg)
|
|
140
220
|
msg["content"] = (
|
|
141
221
|
f"[Duplicate {tool_name!r} output — identical to earlier result, "
|
|
@@ -333,21 +413,21 @@ def resolve_model_context_envelope(model_id: str, raw_context_window: int = 0) -
|
|
|
333
413
|
}
|
|
334
414
|
|
|
335
415
|
|
|
336
|
-
def sanitize_message_sequence(messages: List[Dict], keep_last_tools: int =
|
|
416
|
+
def sanitize_message_sequence(messages: List[Dict], keep_last_tools: int = 4) -> List[Dict]:
|
|
337
417
|
"""
|
|
338
418
|
Ensure the messages list is valid for LLM API calls.
|
|
339
419
|
- Each tool message must have a preceding assistant message with the matching tool_call_id.
|
|
340
420
|
- Each assistant message with tool_calls must have corresponding tool messages.
|
|
341
|
-
-
|
|
342
|
-
- Deduplicates
|
|
421
|
+
- Distills <think> blocks from non-current assistant messages to save tokens without hard caps.
|
|
422
|
+
- Deduplicates repeated identical tool outputs outside the active working tail.
|
|
343
423
|
"""
|
|
344
424
|
if not messages:
|
|
345
425
|
return messages
|
|
346
426
|
|
|
347
|
-
#
|
|
427
|
+
# Distill thinking blocks from older assistant messages
|
|
348
428
|
messages = _strip_thinking_blocks(messages)
|
|
349
429
|
|
|
350
|
-
# Compress
|
|
430
|
+
# Compress duplicate tool outputs
|
|
351
431
|
messages = _deduplicate_tool_outputs(messages, keep_last_tools=keep_last_tools)
|
|
352
432
|
|
|
353
433
|
# Step 1: Identify all tool call IDs present in tool messages
|
|
@@ -397,48 +477,124 @@ def sanitize_message_sequence(messages: List[Dict], keep_last_tools: int = 5) ->
|
|
|
397
477
|
return final_messages
|
|
398
478
|
|
|
399
479
|
|
|
480
|
+
def _distill_cold_tool_output(msg: Dict, preceding_call: Optional[Dict] = None) -> str:
|
|
481
|
+
"""Distill a cold-tier (>16 calls ago) tool result into a compact, high-density knowledge anchor.
|
|
482
|
+
|
|
483
|
+
Preserves:
|
|
484
|
+
- Target file path / query pattern / command
|
|
485
|
+
- Key definitions/symbols discovered (functions, classes, interfaces)
|
|
486
|
+
- Error root cause if failed
|
|
487
|
+
- Command exit status
|
|
488
|
+
WITHOUT keeping thousands of raw output tokens.
|
|
489
|
+
"""
|
|
490
|
+
tool_name = msg.get("name", "tool")
|
|
491
|
+
content = msg.get("content") or ""
|
|
492
|
+
if not isinstance(content, str):
|
|
493
|
+
content = str(content)
|
|
494
|
+
|
|
495
|
+
# Extract target argument if preceding tool call is available
|
|
496
|
+
target = ""
|
|
497
|
+
if preceding_call:
|
|
498
|
+
raw_args = preceding_call.get("function", {}).get("arguments", "{}")
|
|
499
|
+
try:
|
|
500
|
+
parsed_args = json.loads(raw_args) if isinstance(raw_args, str) else raw_args
|
|
501
|
+
if isinstance(parsed_args, dict):
|
|
502
|
+
for k in ["path", "file_path", "target_file", "TargetFile", "filepath", "query", "pattern", "command", "CommandLine"]:
|
|
503
|
+
if k in parsed_args and parsed_args[k]:
|
|
504
|
+
target = str(parsed_args[k])
|
|
505
|
+
break
|
|
506
|
+
except Exception:
|
|
507
|
+
pass
|
|
508
|
+
|
|
509
|
+
content_lower = content.lower()
|
|
510
|
+
is_error = (
|
|
511
|
+
any(kw in content_lower for kw in ["error:", "exception:", "failed", "failure", "traceback", "syntaxerror", "[!] error"])
|
|
512
|
+
or content.strip().startswith(("[!]", "Error:", "FileNotFoundError", "PermissionError"))
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
if is_error:
|
|
516
|
+
error_lines = [ln.strip() for ln in content.splitlines() if any(k in ln.lower() for k in ["error", "exception", "failed", "failure", "traceback"])]
|
|
517
|
+
err_msg = error_lines[0] if error_lines else content[:140].strip()
|
|
518
|
+
target_str = f" ({target})" if target else ""
|
|
519
|
+
return f"[{tool_name}{target_str} failed: {err_msg}]"
|
|
520
|
+
|
|
521
|
+
if tool_name in {"read_file", "view_file"}:
|
|
522
|
+
sigs = []
|
|
523
|
+
for line in content.splitlines():
|
|
524
|
+
line_s = line.strip()
|
|
525
|
+
if line_s.startswith(("def ", "class ", "export function ", "export class ", "type ", "interface ")):
|
|
526
|
+
sig = line_s.split(":")[0].split("{")[0].strip()
|
|
527
|
+
if sig and sig not in sigs:
|
|
528
|
+
sigs.append(sig)
|
|
529
|
+
target_str = target or "file"
|
|
530
|
+
sig_str = f"; defined: {', '.join(sigs[:5])}" if sigs else ""
|
|
531
|
+
return f"[{tool_name} {target_str}: read successfully{sig_str}]"
|
|
532
|
+
|
|
533
|
+
elif tool_name in {"grep_search", "search"}:
|
|
534
|
+
target_str = f"'{target}'" if target else "query"
|
|
535
|
+
lines = [ln.strip() for ln in content.splitlines() if ln.strip()]
|
|
536
|
+
return f"[grep_search {target_str}: completed ({len(lines)} results found)]"
|
|
537
|
+
|
|
538
|
+
elif tool_name in {"list_dir", "list_directory"}:
|
|
539
|
+
target_str = target or "directory"
|
|
540
|
+
return f"[list_dir {target_str}: inspected file tree structure]"
|
|
541
|
+
|
|
542
|
+
elif tool_name == "run_command":
|
|
543
|
+
cmd_str = f"'{target[:60]}'" if target else "command"
|
|
544
|
+
return f"[run_command {cmd_str}: completed successfully with exit code 0]"
|
|
545
|
+
|
|
546
|
+
elif tool_name in {"replace_file_content", "edit_file", "write_to_file", "write_file", "multi_replace_file_content"}:
|
|
547
|
+
target_str = target or "file"
|
|
548
|
+
return f"[{tool_name} {target_str}: modifications applied successfully]"
|
|
549
|
+
|
|
550
|
+
return f"[{tool_name}: executed successfully — knowledge preserved]"
|
|
551
|
+
|
|
552
|
+
|
|
400
553
|
def soft_prune_messages(
|
|
401
554
|
messages: List[Dict],
|
|
402
555
|
turn_msg_start: int = 1,
|
|
403
|
-
keep_last_tools: int =
|
|
556
|
+
keep_last_tools: int = 7,
|
|
557
|
+
warm_tier_tools: int = 16,
|
|
404
558
|
max_char_threshold: int = 1200,
|
|
405
559
|
) -> List[Dict]:
|
|
406
560
|
"""
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
-
|
|
411
|
-
- Active Working Tail: The most recent `keep_last_tools` (at least 5) tool results & their assistant calls are kept 100% verbatim.
|
|
412
|
-
- Errors / Failures: Any tool output containing error keywords (e.g. 'error', 'exception', 'failed', 'traceback', 'syntaxerror') is preserved verbatim to prevent debugging amnesia.
|
|
413
|
-
- Read / Search Outputs (older than tail): If longer than `max_char_threshold`, softly truncates the middle (keeps head 350 chars + tail 200 chars) with a clean diagnostic placeholder.
|
|
414
|
-
- Successful Mutation / Command Outputs: If verbose, compacted to clean status indicators without losing file targets.
|
|
415
|
-
- Duplicate File Reads: If the same file was read multiple times in older steps, earlier duplicates are condensed to lightweight references.
|
|
416
|
-
- Thinking Blocks: Stripped from older assistant messages.
|
|
417
|
-
- Message Integrity: Sanitizes assistant/tool-call sequence so no dangling tool IDs exist.
|
|
561
|
+
3-Tier Graduated Tool Compression Engine:
|
|
562
|
+
- Tier 1 (Hot Tier / Active Tail - Last 1–7 Tools): 100% Verbatim (untouched raw output in working RAM).
|
|
563
|
+
- Tier 2 (Warm Tier / Soft Prune - Tools 8–16): Soft compression preserving key code signatures, exact error lines, and imports.
|
|
564
|
+
- Tier 3 (Cold Tier / Hard Knowledge Distillation - Tools 17+): Distilled 1–2 line semantic knowledge anchors.
|
|
418
565
|
"""
|
|
419
566
|
if not messages or len(messages) <= 2:
|
|
420
567
|
return messages
|
|
421
568
|
|
|
422
|
-
# 1. First pass:
|
|
569
|
+
# 1. First pass: distill old thinking blocks from non-current assistant turns
|
|
423
570
|
cleaned_messages = _strip_thinking_blocks(messages, keep_last=1)
|
|
424
571
|
|
|
425
|
-
# 2.
|
|
572
|
+
# 2. Map tool_call_ids to preceding assistant tool_call specifications
|
|
573
|
+
assistant_call_map = {}
|
|
574
|
+
for m in cleaned_messages:
|
|
575
|
+
if m.get("role") == "assistant" and m.get("tool_calls"):
|
|
576
|
+
for tc in m.get("tool_calls", []):
|
|
577
|
+
tc_id = tc.get("id") or str(tc.get("index", "0"))
|
|
578
|
+
if tc_id:
|
|
579
|
+
assistant_call_map[tc_id] = tc
|
|
580
|
+
|
|
581
|
+
# 3. Identify all tool messages and their positions in chronological order
|
|
426
582
|
tool_indices = [
|
|
427
583
|
i for i, m in enumerate(cleaned_messages)
|
|
428
584
|
if m.get("role") == "tool" and i >= turn_msg_start
|
|
429
585
|
]
|
|
430
586
|
|
|
431
|
-
# If
|
|
587
|
+
# If within Hot Tier capacity (<=7 tools), sanitize and return
|
|
432
588
|
if len(tool_indices) <= keep_last_tools:
|
|
433
589
|
return sanitize_message_sequence(cleaned_messages, keep_last_tools=keep_last_tools)
|
|
434
590
|
|
|
435
|
-
#
|
|
436
|
-
|
|
591
|
+
# Partition tool indices into Hot, Warm, and Cold tiers
|
|
592
|
+
hot_tier_indices = set(tool_indices[-keep_last_tools:])
|
|
593
|
+
warm_tier_indices = set(tool_indices[-warm_tier_tools:-keep_last_tools]) if len(tool_indices) > keep_last_tools else set()
|
|
594
|
+
cold_tier_indices = set(tool_indices[:-warm_tier_tools]) if len(tool_indices) > warm_tier_tools else set()
|
|
437
595
|
|
|
438
|
-
# Track files
|
|
596
|
+
# Track files seen in read tools to detect superseding reads
|
|
439
597
|
seen_read_keys = set()
|
|
440
|
-
|
|
441
|
-
# Inspection and mutation tool sets
|
|
442
598
|
INSPECTION_TOOLS = {
|
|
443
599
|
"read_file", "view_file", "grep_search", "list_dir", "list_directory",
|
|
444
600
|
"search_web", "read_url_content", "search", "view_file_outline"
|
|
@@ -448,9 +604,9 @@ def soft_prune_messages(
|
|
|
448
604
|
"write_to_file", "run_command"
|
|
449
605
|
}
|
|
450
606
|
|
|
451
|
-
# Reverse pass over
|
|
607
|
+
# Reverse pass over warm/cold tools to track superseding reads
|
|
452
608
|
superseded_indices = set()
|
|
453
|
-
for idx in reversed(sorted(
|
|
609
|
+
for idx in reversed(sorted(warm_tier_indices | cold_tier_indices)):
|
|
454
610
|
m = cleaned_messages[idx]
|
|
455
611
|
tool_name = m.get("name", "")
|
|
456
612
|
content = m.get("content", "")
|
|
@@ -463,12 +619,29 @@ def soft_prune_messages(
|
|
|
463
619
|
|
|
464
620
|
result_messages = []
|
|
465
621
|
for i, msg in enumerate(cleaned_messages):
|
|
466
|
-
# Anchor preservation: message 0 (system) and user message at turn_msg_start
|
|
622
|
+
# Anchor preservation: message 0 (system) and root user message at turn_msg_start
|
|
467
623
|
if i == 0 or i == turn_msg_start:
|
|
468
624
|
result_messages.append(msg)
|
|
469
625
|
continue
|
|
470
626
|
|
|
471
|
-
|
|
627
|
+
# ── TIER 1: HOT TIER (Active Working Tail) ─────────────────────────
|
|
628
|
+
if i in hot_tier_indices:
|
|
629
|
+
# 100% Verbatim — never touch active working memory
|
|
630
|
+
result_messages.append(msg)
|
|
631
|
+
continue
|
|
632
|
+
|
|
633
|
+
# ── TIER 3: COLD TIER (Tools 17+ -> Hard Knowledge Distillation) ────
|
|
634
|
+
if i in cold_tier_indices:
|
|
635
|
+
tc_id = msg.get("tool_call_id", "")
|
|
636
|
+
preceding_call = assistant_call_map.get(tc_id)
|
|
637
|
+
distilled_content = _distill_cold_tool_output(msg, preceding_call=preceding_call)
|
|
638
|
+
new_msg = dict(msg)
|
|
639
|
+
new_msg["content"] = distilled_content
|
|
640
|
+
result_messages.append(new_msg)
|
|
641
|
+
continue
|
|
642
|
+
|
|
643
|
+
# ── TIER 2: WARM TIER (Tools 8–16 -> Soft Compression) ──────────────
|
|
644
|
+
if i in warm_tier_indices:
|
|
472
645
|
tool_name = msg.get("name", "")
|
|
473
646
|
content = msg.get("content") or ""
|
|
474
647
|
|
|
@@ -486,12 +659,12 @@ def soft_prune_messages(
|
|
|
486
659
|
or content.strip().startswith(("[!]", "Error:", "FileNotFoundError", "PermissionError"))
|
|
487
660
|
)
|
|
488
661
|
|
|
489
|
-
# CRITICAL
|
|
662
|
+
# CRITICAL: Always preserve error messages verbatim in warm tier!
|
|
490
663
|
if is_error:
|
|
491
664
|
result_messages.append(msg)
|
|
492
665
|
continue
|
|
493
666
|
|
|
494
|
-
# If this is an older superseded read of the same file
|
|
667
|
+
# If this is an older superseded read of the same file
|
|
495
668
|
if i in superseded_indices and len(content) > 300:
|
|
496
669
|
new_msg = dict(msg)
|
|
497
670
|
new_msg["content"] = f"[{tool_name} output from earlier step — superseded by subsequent file inspection]"
|