utim-cli 2.3.2__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.2 → utim_cli-2.3.3}/CHANGELOG.md +5 -0
- {utim_cli-2.3.2/utim_cli.egg-info → utim_cli-2.3.3}/PKG-INFO +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.3}/pyproject.toml +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/__init__.py +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/_version.py +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/context_pruner.py +236 -50
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/orchestrator.py +301 -255
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/utim.py +6 -4
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/wheel.py +3 -8
- {utim_cli-2.3.2 → utim_cli-2.3.3/utim_cli.egg-info}/PKG-INFO +1 -1
- {utim_cli-2.3.2 → utim_cli-2.3.3}/LICENSE +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/MANIFEST.in +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/README.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/setup.cfg +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/setup.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/agent.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/ask_helper.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/auth.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/backup.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/billing.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/blender_agent.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/bootstrap.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/brain.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/client_utils.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/config.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/constants.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/doctor.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/harbor.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/knowledge_graph.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/local_db.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/logger.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/mcp_clean_wrapper.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/mcp_client.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/mcp_registry.json +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/models.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/plugins/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/plugins/manager.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/plugins/registry.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/plugins/schema.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/reflection.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/report.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/scrapy_search.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/config.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/context.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/events.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/handlers.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/session.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/sidecar.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/admin_auth.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/attribution.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/audit_log.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/auth.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/batch_processor.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/captcha.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/cli_auth.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/db.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/about.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/changelog.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/docs.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/features.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/license.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/pricing.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/privacy.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/refund.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/support.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/terms.md +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/email_utils.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/exchange_rate.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/firebase.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/fix_duplicate_users.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/history.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/local_llm.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/logging_config.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/micro_batcher.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/model_agent.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/models.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/pricing_updater.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/provision_build.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/rate_limit.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/rewards_engine.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/router.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/admin_db_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/auth_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/billing_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/completion_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/credit_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/feedback_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/marketplace_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/quota_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/quota_share_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/referral_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/rewards_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/security_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/session_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/share_routes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/server.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/storage_nodes.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/share.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/share_tui.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/situational_scoring.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/state.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/subagent_manager.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/task_dispatcher.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tools.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/__init__.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/feedback_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/history_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/marketplace_app_state.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/marketplace_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/marketplace_layout_engine.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/mcp_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/miniagents_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/model_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/plugins_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/publish_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/quota_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/quota_redeem_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/quota_share_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/resume_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/rewards_tui.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/skills_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/subagents_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/thinking_display.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/tools_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/update_dialog.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/utilities.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/utimmodel.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/vector_memory.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/workspace.py +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli.egg-info/SOURCES.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli.egg-info/dependency_links.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli.egg-info/entry_points.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli.egg-info/requires.txt +0 -0
- {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli.egg-info/top_level.txt +0 -0
|
@@ -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,35 +162,59 @@ 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]) -> List[Dict]:
|
|
195
|
+
def _deduplicate_tool_outputs(messages: List[Dict], keep_last_tools: int = 4) -> List[Dict]:
|
|
106
196
|
"""Compress repeated identical tool responses to a one-liner stub.
|
|
107
197
|
|
|
108
198
|
When the agent calls the same tool with the same arguments repeatedly and gets
|
|
109
199
|
the same output (e.g. re-reading an unchanged file multiple times), only the
|
|
110
|
-
first occurrence is kept verbatim. Subsequent duplicates
|
|
200
|
+
first occurrence is kept verbatim. Subsequent duplicates outside the active
|
|
201
|
+
`keep_last_tools` tail are replaced with:
|
|
111
202
|
[Duplicate tool output — identical to earlier result, omitted to save tokens]
|
|
112
203
|
"""
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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()
|
|
206
|
+
|
|
207
|
+
# Map: (tool_name, content_hash) -> True
|
|
208
|
+
seen: dict = {}
|
|
116
209
|
result = []
|
|
117
210
|
|
|
118
|
-
for msg in messages:
|
|
211
|
+
for i, msg in enumerate(messages):
|
|
119
212
|
if msg.get("role") == "tool":
|
|
120
213
|
content = msg.get("content") or ""
|
|
121
214
|
tool_name = msg.get("name", "")
|
|
122
215
|
# Use first 300 chars of content for the hash to avoid hashing huge outputs
|
|
123
216
|
content_key = (tool_name, hashlib.md5(content[:300].encode(errors="replace")).hexdigest())
|
|
124
|
-
if content_key in seen and len(content) > 200:
|
|
217
|
+
if i not in keep_intact_indices and content_key in seen and len(content) > 200:
|
|
125
218
|
# Replace with stub
|
|
126
219
|
msg = dict(msg)
|
|
127
220
|
msg["content"] = (
|
|
@@ -320,22 +413,22 @@ def resolve_model_context_envelope(model_id: str, raw_context_window: int = 0) -
|
|
|
320
413
|
}
|
|
321
414
|
|
|
322
415
|
|
|
323
|
-
def sanitize_message_sequence(messages: List[Dict]) -> List[Dict]:
|
|
416
|
+
def sanitize_message_sequence(messages: List[Dict], keep_last_tools: int = 4) -> List[Dict]:
|
|
324
417
|
"""
|
|
325
418
|
Ensure the messages list is valid for LLM API calls.
|
|
326
419
|
- Each tool message must have a preceding assistant message with the matching tool_call_id.
|
|
327
420
|
- Each assistant message with tool_calls must have corresponding tool messages.
|
|
328
|
-
-
|
|
329
|
-
- Deduplicates repeated identical tool outputs.
|
|
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.
|
|
330
423
|
"""
|
|
331
424
|
if not messages:
|
|
332
425
|
return messages
|
|
333
426
|
|
|
334
|
-
#
|
|
427
|
+
# Distill thinking blocks from older assistant messages
|
|
335
428
|
messages = _strip_thinking_blocks(messages)
|
|
336
429
|
|
|
337
430
|
# Compress duplicate tool outputs
|
|
338
|
-
messages = _deduplicate_tool_outputs(messages)
|
|
431
|
+
messages = _deduplicate_tool_outputs(messages, keep_last_tools=keep_last_tools)
|
|
339
432
|
|
|
340
433
|
# Step 1: Identify all tool call IDs present in tool messages
|
|
341
434
|
present_tool_ids = {
|
|
@@ -384,48 +477,124 @@ def sanitize_message_sequence(messages: List[Dict]) -> List[Dict]:
|
|
|
384
477
|
return final_messages
|
|
385
478
|
|
|
386
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
|
+
|
|
387
553
|
def soft_prune_messages(
|
|
388
554
|
messages: List[Dict],
|
|
389
555
|
turn_msg_start: int = 1,
|
|
390
|
-
keep_last_tools: int =
|
|
556
|
+
keep_last_tools: int = 7,
|
|
557
|
+
warm_tier_tools: int = 16,
|
|
391
558
|
max_char_threshold: int = 1200,
|
|
392
559
|
) -> List[Dict]:
|
|
393
560
|
"""
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
-
|
|
398
|
-
- Active Working Tail: The most recent `keep_last_tools` tool results & their assistant calls are kept 100% verbatim.
|
|
399
|
-
- Errors / Failures: Any tool output containing error keywords (e.g. 'error', 'exception', 'failed', 'traceback', 'syntaxerror') is preserved verbatim to prevent debugging amnesia.
|
|
400
|
-
- 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.
|
|
401
|
-
- Successful Mutation / Command Outputs: If verbose, compacted to clean status indicators without losing file targets.
|
|
402
|
-
- Duplicate File Reads: If the same file was read multiple times in older steps, earlier duplicates are condensed to lightweight references.
|
|
403
|
-
- Thinking Blocks: Stripped from older assistant messages.
|
|
404
|
-
- 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.
|
|
405
565
|
"""
|
|
406
566
|
if not messages or len(messages) <= 2:
|
|
407
567
|
return messages
|
|
408
568
|
|
|
409
|
-
# 1. First pass:
|
|
569
|
+
# 1. First pass: distill old thinking blocks from non-current assistant turns
|
|
410
570
|
cleaned_messages = _strip_thinking_blocks(messages, keep_last=1)
|
|
411
571
|
|
|
412
|
-
# 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
|
|
413
582
|
tool_indices = [
|
|
414
583
|
i for i, m in enumerate(cleaned_messages)
|
|
415
584
|
if m.get("role") == "tool" and i >= turn_msg_start
|
|
416
585
|
]
|
|
417
586
|
|
|
418
|
-
# If
|
|
587
|
+
# If within Hot Tier capacity (<=7 tools), sanitize and return
|
|
419
588
|
if len(tool_indices) <= keep_last_tools:
|
|
420
|
-
return sanitize_message_sequence(cleaned_messages)
|
|
589
|
+
return sanitize_message_sequence(cleaned_messages, keep_last_tools=keep_last_tools)
|
|
421
590
|
|
|
422
|
-
#
|
|
423
|
-
|
|
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()
|
|
424
595
|
|
|
425
|
-
# Track files
|
|
596
|
+
# Track files seen in read tools to detect superseding reads
|
|
426
597
|
seen_read_keys = set()
|
|
427
|
-
|
|
428
|
-
# Inspection and mutation tool sets
|
|
429
598
|
INSPECTION_TOOLS = {
|
|
430
599
|
"read_file", "view_file", "grep_search", "list_dir", "list_directory",
|
|
431
600
|
"search_web", "read_url_content", "search", "view_file_outline"
|
|
@@ -435,9 +604,9 @@ def soft_prune_messages(
|
|
|
435
604
|
"write_to_file", "run_command"
|
|
436
605
|
}
|
|
437
606
|
|
|
438
|
-
# Reverse pass over
|
|
607
|
+
# Reverse pass over warm/cold tools to track superseding reads
|
|
439
608
|
superseded_indices = set()
|
|
440
|
-
for idx in reversed(sorted(
|
|
609
|
+
for idx in reversed(sorted(warm_tier_indices | cold_tier_indices)):
|
|
441
610
|
m = cleaned_messages[idx]
|
|
442
611
|
tool_name = m.get("name", "")
|
|
443
612
|
content = m.get("content", "")
|
|
@@ -450,12 +619,29 @@ def soft_prune_messages(
|
|
|
450
619
|
|
|
451
620
|
result_messages = []
|
|
452
621
|
for i, msg in enumerate(cleaned_messages):
|
|
453
|
-
# 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
|
|
454
623
|
if i == 0 or i == turn_msg_start:
|
|
455
624
|
result_messages.append(msg)
|
|
456
625
|
continue
|
|
457
626
|
|
|
458
|
-
|
|
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:
|
|
459
645
|
tool_name = msg.get("name", "")
|
|
460
646
|
content = msg.get("content") or ""
|
|
461
647
|
|
|
@@ -473,12 +659,12 @@ def soft_prune_messages(
|
|
|
473
659
|
or content.strip().startswith(("[!]", "Error:", "FileNotFoundError", "PermissionError"))
|
|
474
660
|
)
|
|
475
661
|
|
|
476
|
-
# CRITICAL
|
|
662
|
+
# CRITICAL: Always preserve error messages verbatim in warm tier!
|
|
477
663
|
if is_error:
|
|
478
664
|
result_messages.append(msg)
|
|
479
665
|
continue
|
|
480
666
|
|
|
481
|
-
# If this is an older superseded read of the same file
|
|
667
|
+
# If this is an older superseded read of the same file
|
|
482
668
|
if i in superseded_indices and len(content) > 300:
|
|
483
669
|
new_msg = dict(msg)
|
|
484
670
|
new_msg["content"] = f"[{tool_name} output from earlier step — superseded by subsequent file inspection]"
|
|
@@ -523,7 +709,7 @@ def soft_prune_messages(
|
|
|
523
709
|
|
|
524
710
|
result_messages.append(msg)
|
|
525
711
|
|
|
526
|
-
return sanitize_message_sequence(result_messages)
|
|
712
|
+
return sanitize_message_sequence(result_messages, keep_last_tools=keep_last_tools)
|
|
527
713
|
|
|
528
714
|
|
|
529
715
|
def score_message_importance(message: Dict, llm_key: str = None) -> float:
|