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.
Files changed (145) hide show
  1. {utim_cli-2.3.1 → utim_cli-2.3.3}/CHANGELOG.md +10 -0
  2. {utim_cli-2.3.1/utim_cli.egg-info → utim_cli-2.3.3}/PKG-INFO +1 -1
  3. {utim_cli-2.3.1 → utim_cli-2.3.3}/pyproject.toml +1 -1
  4. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/__init__.py +1 -1
  5. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/_version.py +1 -1
  6. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/context_pruner.py +237 -64
  7. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/orchestrator.py +414 -446
  8. utim_cli-2.3.3/utim_cli/plugins/__init__.py +10 -0
  9. utim_cli-2.3.3/utim_cli/plugins/manager.py +465 -0
  10. utim_cli-2.3.3/utim_cli/plugins/registry.py +120 -0
  11. utim_cli-2.3.3/utim_cli/plugins/schema.py +230 -0
  12. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/auth.py +11 -47
  13. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/db.py +0 -47
  14. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/micro_batcher.py +12 -18
  15. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/rate_limit.py +9 -5
  16. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/router.py +3 -1
  17. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/__init__.py +3 -1
  18. utim_cli-2.3.3/utim_cli/server/routes/admin_db_routes.py +507 -0
  19. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/completion_routes.py +52 -40
  20. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/quota_routes.py +76 -12
  21. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/share.py +99 -99
  22. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/state.py +0 -1
  23. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/task_dispatcher.py +62 -20
  24. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tools.py +0 -12
  25. utim_cli-2.3.3/utim_cli/tui/plugins_dialog.py +351 -0
  26. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/utim.py +24 -40
  27. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/wheel.py +3 -8
  28. {utim_cli-2.3.1 → utim_cli-2.3.3/utim_cli.egg-info}/PKG-INFO +1 -1
  29. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli.egg-info/SOURCES.txt +6 -6
  30. utim_cli-2.3.1/utim_cli/_check_obj.py +0 -9
  31. utim_cli-2.3.1/utim_cli/_find_mcp.py +0 -9
  32. utim_cli-2.3.1/utim_cli/_find_plan.py +0 -12
  33. utim_cli-2.3.1/utim_cli/_find_tool.py +0 -10
  34. utim_cli-2.3.1/utim_cli/_patch_multimodal.py +0 -125
  35. utim_cli-2.3.1/utim_cli/_test_regex.py +0 -22
  36. {utim_cli-2.3.1 → utim_cli-2.3.3}/LICENSE +0 -0
  37. {utim_cli-2.3.1 → utim_cli-2.3.3}/MANIFEST.in +0 -0
  38. {utim_cli-2.3.1 → utim_cli-2.3.3}/README.md +0 -0
  39. {utim_cli-2.3.1 → utim_cli-2.3.3}/setup.cfg +0 -0
  40. {utim_cli-2.3.1 → utim_cli-2.3.3}/setup.py +0 -0
  41. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim/__init__.py +0 -0
  42. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/agent.py +0 -0
  43. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/ask_helper.py +0 -0
  44. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/auth.py +0 -0
  45. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/backup.py +0 -0
  46. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/billing.py +0 -0
  47. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/blender_agent.py +0 -0
  48. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/bootstrap.py +0 -0
  49. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/brain.py +0 -0
  50. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/client_utils.py +0 -0
  51. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/config.py +0 -0
  52. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/constants.py +0 -0
  53. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/doctor.py +0 -0
  54. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/harbor.py +0 -0
  55. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/knowledge_graph.py +0 -0
  56. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/local_db.py +0 -0
  57. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/logger.py +0 -0
  58. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/mcp_clean_wrapper.py +0 -0
  59. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/mcp_client.py +0 -0
  60. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/mcp_registry.json +0 -0
  61. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/models.txt +0 -0
  62. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/reflection.py +0 -0
  63. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/report.py +0 -0
  64. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/scrapy_search.py +0 -0
  65. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/__init__.py +0 -0
  66. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/config.py +0 -0
  67. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/context.py +0 -0
  68. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/events.py +0 -0
  69. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/handlers.py +0 -0
  70. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/session.py +0 -0
  71. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/sdk/sidecar.py +0 -0
  72. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/__init__.py +0 -0
  73. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/admin_auth.py +0 -0
  74. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/attribution.py +0 -0
  75. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/audit_log.py +0 -0
  76. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/batch_processor.py +0 -0
  77. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/captcha.py +0 -0
  78. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/cli_auth.py +0 -0
  79. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/SECRET_PROVISIONING.md +0 -0
  80. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/about.md +0 -0
  81. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/changelog.md +0 -0
  82. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/docs.md +0 -0
  83. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/features.md +0 -0
  84. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/license.md +0 -0
  85. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/pricing.md +0 -0
  86. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/privacy.md +0 -0
  87. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/refund.md +0 -0
  88. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/support.md +0 -0
  89. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/docs_md/terms.md +0 -0
  90. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/email_utils.py +0 -0
  91. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/exchange_rate.py +0 -0
  92. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/firebase.py +0 -0
  93. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/fix_duplicate_users.py +0 -0
  94. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/history.py +0 -0
  95. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/local_llm.py +0 -0
  96. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/logging_config.py +0 -0
  97. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/model_agent.py +0 -0
  98. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/models.py +0 -0
  99. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/pricing_updater.py +0 -0
  100. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/provision_build.py +0 -0
  101. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/rewards_engine.py +0 -0
  102. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/auth_routes.py +0 -0
  103. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/billing_routes.py +0 -0
  104. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/credit_routes.py +0 -0
  105. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/feedback_routes.py +0 -0
  106. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/marketplace_routes.py +0 -0
  107. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/quota_share_routes.py +0 -0
  108. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/referral_routes.py +0 -0
  109. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/rewards_routes.py +0 -0
  110. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/security_routes.py +0 -0
  111. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/session_routes.py +0 -0
  112. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/routes/share_routes.py +0 -0
  113. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/server.py +0 -0
  114. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/server/storage_nodes.py +0 -0
  115. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/share_tui.py +0 -0
  116. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/situational_scoring.py +0 -0
  117. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/subagent_manager.py +0 -0
  118. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/__init__.py +0 -0
  119. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/feedback_dialog.py +0 -0
  120. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/history_dialog.py +0 -0
  121. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/marketplace_app_state.py +0 -0
  122. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/marketplace_dialog.py +0 -0
  123. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/marketplace_layout_engine.py +0 -0
  124. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/mcp_dialog.py +0 -0
  125. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/miniagents_dialog.py +0 -0
  126. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/model_dialog.py +0 -0
  127. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/publish_dialog.py +0 -0
  128. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/quota_dialog.py +0 -0
  129. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/quota_redeem_dialog.py +0 -0
  130. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/quota_share_dialog.py +0 -0
  131. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/resume_dialog.py +0 -0
  132. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/rewards_tui.py +0 -0
  133. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/skills_dialog.py +0 -0
  134. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/subagents_dialog.py +0 -0
  135. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/thinking_display.py +0 -0
  136. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/tools_dialog.py +0 -0
  137. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/tui/update_dialog.py +0 -0
  138. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/utilities.py +0 -0
  139. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/utimmodel.txt +0 -0
  140. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/vector_memory.py +0 -0
  141. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli/workspace.py +0 -0
  142. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli.egg-info/dependency_links.txt +0 -0
  143. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli.egg-info/entry_points.txt +0 -0
  144. {utim_cli-2.3.1 → utim_cli-2.3.3}/utim_cli.egg-info/requires.txt +0 -0
  145. {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.1
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.1"
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.1"
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.1"
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,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
- 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], keep_last_tools: int = 5) -> List[Dict]:
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
- The last 5 tool outputs must NEVER be deduplicated, truncated, or stubbed out,
110
- even if they are duplicates of earlier reads, because the model requires the exact
111
- code lines and context to execute precise file edits and tool actions.
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
- # Identify indices of all tool messages
114
- tool_msg_indices = [i for i, m in enumerate(messages) if m.get("role") == "tool"]
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
- seen: dict = {} # (tool_name, content_hash) -> True
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 older duplicate with stub
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 = 5) -> List[Dict]:
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
- - Strips <think> blocks from non-current assistant messages to save tokens.
342
- - Deduplicates older repeated identical tool outputs (strictly preserving the last 5 tool calls verbatim).
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
- # Strip thinking blocks from older assistant messages
427
+ # Distill thinking blocks from older assistant messages
348
428
  messages = _strip_thinking_blocks(messages)
349
429
 
350
- # Compress older duplicate tool outputs while protecting last 5 tool calls
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 = 5,
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
- Periodic balanced soft pruning pass executed every 10 tool calls.
408
-
409
- Balanced policy ('not too aggressive, not too light'):
410
- - Anchors: System prompt (idx 0) and the root User prompt (turn_msg_start) are strictly preserved.
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: strip old thinking blocks from assistant turns
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. 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
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 not enough tool messages have accumulated in this turn, just sanitize and return
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
- # Older tool message indices that are eligible for soft pruning
436
- 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()
437
595
 
438
- # Track files/queries seen in read tools to detect superseding reads
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 eligible tools to track the latest reads vs earlier reads
607
+ # Reverse pass over warm/cold tools to track superseding reads
452
608
  superseded_indices = set()
453
- for idx in reversed(sorted(eligible_tool_indices)):
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
- 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:
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 EDGE CASE: Never prune error messages!
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/content
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]"