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.
Files changed (139) hide show
  1. {utim_cli-2.3.2 → utim_cli-2.3.3}/CHANGELOG.md +5 -0
  2. {utim_cli-2.3.2/utim_cli.egg-info → utim_cli-2.3.3}/PKG-INFO +1 -1
  3. {utim_cli-2.3.2 → utim_cli-2.3.3}/pyproject.toml +1 -1
  4. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/__init__.py +1 -1
  5. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/_version.py +1 -1
  6. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/context_pruner.py +236 -50
  7. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/orchestrator.py +301 -255
  8. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/utim.py +6 -4
  9. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/wheel.py +3 -8
  10. {utim_cli-2.3.2 → utim_cli-2.3.3/utim_cli.egg-info}/PKG-INFO +1 -1
  11. {utim_cli-2.3.2 → utim_cli-2.3.3}/LICENSE +0 -0
  12. {utim_cli-2.3.2 → utim_cli-2.3.3}/MANIFEST.in +0 -0
  13. {utim_cli-2.3.2 → utim_cli-2.3.3}/README.md +0 -0
  14. {utim_cli-2.3.2 → utim_cli-2.3.3}/setup.cfg +0 -0
  15. {utim_cli-2.3.2 → utim_cli-2.3.3}/setup.py +0 -0
  16. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim/__init__.py +0 -0
  17. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/agent.py +0 -0
  18. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/ask_helper.py +0 -0
  19. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/auth.py +0 -0
  20. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/backup.py +0 -0
  21. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/billing.py +0 -0
  22. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/blender_agent.py +0 -0
  23. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/bootstrap.py +0 -0
  24. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/brain.py +0 -0
  25. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/client_utils.py +0 -0
  26. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/config.py +0 -0
  27. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/constants.py +0 -0
  28. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/doctor.py +0 -0
  29. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/harbor.py +0 -0
  30. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/knowledge_graph.py +0 -0
  31. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/local_db.py +0 -0
  32. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/logger.py +0 -0
  33. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/mcp_clean_wrapper.py +0 -0
  34. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/mcp_client.py +0 -0
  35. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/mcp_registry.json +0 -0
  36. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/models.txt +0 -0
  37. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/plugins/__init__.py +0 -0
  38. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/plugins/manager.py +0 -0
  39. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/plugins/registry.py +0 -0
  40. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/plugins/schema.py +0 -0
  41. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/reflection.py +0 -0
  42. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/report.py +0 -0
  43. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/scrapy_search.py +0 -0
  44. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/__init__.py +0 -0
  45. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/config.py +0 -0
  46. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/context.py +0 -0
  47. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/events.py +0 -0
  48. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/handlers.py +0 -0
  49. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/session.py +0 -0
  50. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/sdk/sidecar.py +0 -0
  51. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/__init__.py +0 -0
  52. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/admin_auth.py +0 -0
  53. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/attribution.py +0 -0
  54. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/audit_log.py +0 -0
  55. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/auth.py +0 -0
  56. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/batch_processor.py +0 -0
  57. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/captcha.py +0 -0
  58. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/cli_auth.py +0 -0
  59. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/db.py +0 -0
  60. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  61. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/about.md +0 -0
  62. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/changelog.md +0 -0
  63. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/docs.md +0 -0
  64. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/features.md +0 -0
  65. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/license.md +0 -0
  66. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/pricing.md +0 -0
  67. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/privacy.md +0 -0
  68. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/refund.md +0 -0
  69. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/support.md +0 -0
  70. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/docs_md/terms.md +0 -0
  71. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/email_utils.py +0 -0
  72. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/exchange_rate.py +0 -0
  73. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/firebase.py +0 -0
  74. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/fix_duplicate_users.py +0 -0
  75. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/history.py +0 -0
  76. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/local_llm.py +0 -0
  77. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/logging_config.py +0 -0
  78. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/micro_batcher.py +0 -0
  79. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/model_agent.py +0 -0
  80. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/models.py +0 -0
  81. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/pricing_updater.py +0 -0
  82. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/provision_build.py +0 -0
  83. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/rate_limit.py +0 -0
  84. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/rewards_engine.py +0 -0
  85. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/router.py +0 -0
  86. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/__init__.py +0 -0
  87. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/admin_db_routes.py +0 -0
  88. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/auth_routes.py +0 -0
  89. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/billing_routes.py +0 -0
  90. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/completion_routes.py +0 -0
  91. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/credit_routes.py +0 -0
  92. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/feedback_routes.py +0 -0
  93. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/marketplace_routes.py +0 -0
  94. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/quota_routes.py +0 -0
  95. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/quota_share_routes.py +0 -0
  96. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/referral_routes.py +0 -0
  97. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/rewards_routes.py +0 -0
  98. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/security_routes.py +0 -0
  99. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/session_routes.py +0 -0
  100. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/routes/share_routes.py +0 -0
  101. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/server.py +0 -0
  102. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/server/storage_nodes.py +0 -0
  103. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/share.py +0 -0
  104. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/share_tui.py +0 -0
  105. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/situational_scoring.py +0 -0
  106. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/state.py +0 -0
  107. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/subagent_manager.py +0 -0
  108. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/task_dispatcher.py +0 -0
  109. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tools.py +0 -0
  110. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/__init__.py +0 -0
  111. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/feedback_dialog.py +0 -0
  112. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/history_dialog.py +0 -0
  113. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/marketplace_app_state.py +0 -0
  114. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/marketplace_dialog.py +0 -0
  115. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  116. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/mcp_dialog.py +0 -0
  117. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/miniagents_dialog.py +0 -0
  118. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/model_dialog.py +0 -0
  119. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/plugins_dialog.py +0 -0
  120. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/publish_dialog.py +0 -0
  121. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/quota_dialog.py +0 -0
  122. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  123. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/quota_share_dialog.py +0 -0
  124. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/resume_dialog.py +0 -0
  125. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/rewards_tui.py +0 -0
  126. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/skills_dialog.py +0 -0
  127. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/subagents_dialog.py +0 -0
  128. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/thinking_display.py +0 -0
  129. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/tools_dialog.py +0 -0
  130. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/tui/update_dialog.py +0 -0
  131. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/utilities.py +0 -0
  132. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/utimmodel.txt +0 -0
  133. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/vector_memory.py +0 -0
  134. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli/workspace.py +0 -0
  135. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli.egg-info/SOURCES.txt +0 -0
  136. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli.egg-info/dependency_links.txt +0 -0
  137. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli.egg-info/entry_points.txt +0 -0
  138. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli.egg-info/requires.txt +0 -0
  139. {utim_cli-2.3.2 → utim_cli-2.3.3}/utim_cli.egg-info/top_level.txt +0 -0
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## [2.3.3] - 2026-08-20
4
+
5
+ ### 🚀 Performance & Polish
6
+ - Token usage optimization and minor enhancements
7
+
3
8
  ## [2.3.2] - 2026-08-20
4
9
 
5
10
  ### 🚀 Performance & Fixes
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: utim-cli
3
- Version: 2.3.2
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
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "utim-cli"
7
- version = "2.3.2"
7
+ version = "2.3.3"
8
8
 
9
9
  description = "UTIM – Universal Terminal Intelligence Manager. A powerful agentic AI coding assistant for your terminal."
10
10
  readme = "README.md"
@@ -5,7 +5,7 @@ try:
5
5
  from utim_cli._version import VERSION as __version__
6
6
  except ImportError:
7
7
  # Fallback during first-install / editable-install edge cases
8
- __version__ = "2.3.2"
8
+ __version__ = "2.3.3"
9
9
 
10
10
 
11
11
  import builtins, os, pathlib, urllib.parse, re
@@ -11,6 +11,6 @@
11
11
  # from here so every surface stays in sync automatically.
12
12
  # ─────────────────────────────────────────────────────────────────────────────
13
13
 
14
- VERSION = "2.3.2"
14
+ VERSION = "2.3.3"
15
15
 
16
16
 
@@ -67,16 +67,85 @@ CONTINUITY_PATTERNS = [
67
67
  r"(?i)(tried|attempted|worked|failed|error|fix|patch)",
68
68
  ]
69
69
 
70
- # Regex to strip <think>...</think> and <thinking>...</thinking> reasoning blocks
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
- """Remove <think>...</think> blocks from all but the last `keep_last` assistant messages.
144
+ """Condense <think>...</think> blocks from all but the last `keep_last` assistant messages.
76
145
 
77
- Reasoning traces are only useful in the *current* step; older ones waste tokens
78
- without adding signal. We keep the most recent one intact so the model can see
79
- its own chain-of-thought for the ongoing action.
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
- stripped = _THINK_RE.sub("", content).strip()
98
- if stripped != content:
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"] = stripped if stripped else None
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 are replaced with:
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
- # Map: (tool_call_id_of_preceding_assistant_call -> output_hash)
114
- # We key on (tool_name, args_hash) -> first_output_hash seen
115
- seen: dict = {} # (tool_name_or_id, content_hash) -> True
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
- - Strips <think> blocks from non-current assistant messages to save tokens.
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
- # Strip thinking blocks from older assistant messages
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 = 4,
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
- Periodic balanced soft pruning pass executed every 10 tool calls.
395
-
396
- Balanced policy ('not too aggressive, not too light'):
397
- - Anchors: System prompt (idx 0) and the root User prompt (turn_msg_start) are strictly preserved.
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: strip old thinking blocks from assistant turns
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. Identify all tool messages and their positions
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 not enough tool messages have accumulated in this turn, just sanitize and return
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
- # Older tool message indices that are eligible for soft pruning
423
- eligible_tool_indices = set(tool_indices[:-keep_last_tools])
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/queries seen in read tools to detect superseding reads
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 eligible tools to track the latest reads vs earlier reads
607
+ # Reverse pass over warm/cold tools to track superseding reads
439
608
  superseded_indices = set()
440
- for idx in reversed(sorted(eligible_tool_indices)):
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
- if i in eligible_tool_indices:
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 EDGE CASE: Never prune error messages!
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/content
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: