auto-coder 0.1.400__py3-none-any.whl → 1.0.0__py3-none-any.whl

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.

Potentially problematic release.


This version of auto-coder might be problematic. Click here for more details.

Files changed (48) hide show
  1. {auto_coder-0.1.400.dist-info → auto_coder-1.0.0.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.400.dist-info → auto_coder-1.0.0.dist-info}/RECORD +48 -31
  3. autocoder/agent/agentic_filter.py +1 -1
  4. autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +1 -1
  5. autocoder/auto_coder_runner.py +120 -26
  6. autocoder/chat_auto_coder.py +81 -22
  7. autocoder/commands/auto_command.py +1 -1
  8. autocoder/common/__init__.py +2 -2
  9. autocoder/common/file_monitor/test_file_monitor.py +307 -0
  10. autocoder/common/git_utils.py +7 -2
  11. autocoder/common/pruner/__init__.py +0 -0
  12. autocoder/common/pruner/agentic_conversation_pruner.py +197 -0
  13. autocoder/common/pruner/context_pruner.py +574 -0
  14. autocoder/common/pruner/conversation_pruner.py +132 -0
  15. autocoder/common/pruner/test_agentic_conversation_pruner.py +342 -0
  16. autocoder/common/pruner/test_context_pruner.py +546 -0
  17. autocoder/common/tokens/__init__.py +15 -0
  18. autocoder/common/tokens/counter.py +20 -0
  19. autocoder/common/v2/agent/agentic_edit.py +372 -538
  20. autocoder/common/v2/agent/agentic_edit_tools/__init__.py +8 -1
  21. autocoder/common/v2/agent/agentic_edit_tools/ac_mod_read_tool_resolver.py +40 -0
  22. autocoder/common/v2/agent/agentic_edit_tools/ac_mod_write_tool_resolver.py +43 -0
  23. autocoder/common/v2/agent/agentic_edit_tools/ask_followup_question_tool_resolver.py +8 -0
  24. autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +1 -1
  25. autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +1 -1
  26. autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +33 -88
  27. autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +8 -8
  28. autocoder/common/v2/agent/agentic_edit_tools/todo_read_tool_resolver.py +118 -0
  29. autocoder/common/v2/agent/agentic_edit_tools/todo_write_tool_resolver.py +324 -0
  30. autocoder/common/v2/agent/agentic_edit_types.py +46 -4
  31. autocoder/common/v2/agent/runner/__init__.py +31 -0
  32. autocoder/common/v2/agent/runner/base_runner.py +106 -0
  33. autocoder/common/v2/agent/runner/event_runner.py +216 -0
  34. autocoder/common/v2/agent/runner/sdk_runner.py +40 -0
  35. autocoder/common/v2/agent/runner/terminal_runner.py +283 -0
  36. autocoder/common/v2/agent/runner/tool_display.py +191 -0
  37. autocoder/index/entry.py +1 -1
  38. autocoder/plugins/token_helper_plugin.py +107 -7
  39. autocoder/run_context.py +9 -0
  40. autocoder/sdk/__init__.py +114 -81
  41. autocoder/sdk/cli/main.py +5 -0
  42. autocoder/sdk/core/auto_coder_core.py +0 -158
  43. autocoder/sdk/core/bridge.py +2 -4
  44. autocoder/version.py +1 -1
  45. {auto_coder-0.1.400.dist-info → auto_coder-1.0.0.dist-info}/WHEEL +0 -0
  46. {auto_coder-0.1.400.dist-info → auto_coder-1.0.0.dist-info}/entry_points.txt +0 -0
  47. {auto_coder-0.1.400.dist-info → auto_coder-1.0.0.dist-info}/licenses/LICENSE +0 -0
  48. {auto_coder-0.1.400.dist-info → auto_coder-1.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,307 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ FileMonitor 模块测试
5
+
6
+ 测试 FileMonitor 的基本功能:文件的增删改查监控
7
+ """
8
+
9
+ import os
10
+ import tempfile
11
+ import shutil
12
+ import time
13
+ import threading
14
+ from pathlib import Path
15
+ from typing import List, Tuple
16
+ from unittest.mock import Mock
17
+
18
+ try:
19
+ from watchfiles import Change
20
+ except ImportError:
21
+ print("警告:watchfiles 未安装,请运行: pip install watchfiles")
22
+ Change = None
23
+
24
+ from autocoder.common.file_monitor.monitor import get_file_monitor, FileMonitor
25
+
26
+
27
+ class FileMonitorTester:
28
+ """FileMonitor 测试类"""
29
+
30
+ def __init__(self):
31
+ self.temp_dir = None
32
+ self.monitor = None
33
+ self.events = []
34
+ self.event_lock = threading.Lock()
35
+
36
+ def setup(self):
37
+ """设置测试环境"""
38
+ # 创建临时目录
39
+ self.temp_dir = tempfile.mkdtemp(prefix="file_monitor_test_")
40
+ print(f"创建临时测试目录: {self.temp_dir}")
41
+
42
+ # 重置单例实例以确保测试隔离
43
+ FileMonitor.reset_instance()
44
+
45
+ # 获取监控实例
46
+ self.monitor = get_file_monitor(self.temp_dir)
47
+
48
+ # 清空事件记录
49
+ self.events.clear()
50
+
51
+ def teardown(self):
52
+ """清理测试环境"""
53
+ if self.monitor and self.monitor.is_running():
54
+ self.monitor.stop()
55
+
56
+ if self.temp_dir and os.path.exists(self.temp_dir):
57
+ shutil.rmtree(self.temp_dir)
58
+ print(f"清理临时测试目录: {self.temp_dir}")
59
+
60
+ # 重置单例实例
61
+ FileMonitor.reset_instance()
62
+
63
+ def record_event(self, change_type: 'Change', changed_path: str):
64
+ """记录文件变化事件"""
65
+ with self.event_lock:
66
+ self.events.append((change_type, changed_path))
67
+ print(f"📝 记录事件: {change_type.name} - {os.path.basename(changed_path)}")
68
+
69
+ def wait_for_events(self, expected_count: int, timeout: float = 3.0):
70
+ """等待指定数量的事件"""
71
+ start_time = time.time()
72
+ while time.time() - start_time < timeout:
73
+ with self.event_lock:
74
+ if len(self.events) >= expected_count:
75
+ return True
76
+ time.sleep(0.1)
77
+ return False
78
+
79
+ def wait_for_specific_event(self, expected_type: 'Change', expected_path: str, timeout: float = 3.0):
80
+ """等待特定类型的事件"""
81
+ start_time = time.time()
82
+ while time.time() - start_time < timeout:
83
+ with self.event_lock:
84
+ for event_type, event_path in self.events:
85
+ if event_type == expected_type and expected_path in event_path:
86
+ return True
87
+ time.sleep(0.1)
88
+ return False
89
+
90
+ def test_file_create(self):
91
+ """测试文件创建监控"""
92
+ print("\n🔍 测试用例 1: 文件创建监控")
93
+
94
+ # 注册监控所有 .txt 文件
95
+ self.monitor.register("**/*.txt", self.record_event)
96
+
97
+ # 启动监控
98
+ self.monitor.start()
99
+ time.sleep(0.5) # 等待监控启动
100
+
101
+ # 创建测试文件
102
+ test_file = os.path.join(self.temp_dir, "test_create.txt")
103
+ with open(test_file, 'w') as f:
104
+ f.write("Hello, World!")
105
+ print(f"✅ 创建文件: {os.path.basename(test_file)}")
106
+
107
+ # 等待事件
108
+ if self.wait_for_events(1):
109
+ with self.event_lock:
110
+ event_type, event_path = self.events[-1]
111
+ if event_type == Change.added and test_file in event_path:
112
+ print("✅ 文件创建事件检测成功")
113
+ return True
114
+ else:
115
+ print(f"❌ 事件类型不匹配: 期望 {Change.added.name}, 实际 {event_type.name}")
116
+ else:
117
+ print("❌ 未检测到文件创建事件")
118
+
119
+ return False
120
+
121
+ def test_file_modify(self):
122
+ """测试文件修改监控"""
123
+ print("\n🔍 测试用例 2: 文件修改监控")
124
+
125
+ # 先创建一个文件
126
+ test_file = os.path.join(self.temp_dir, "test_modify.txt")
127
+ with open(test_file, 'w') as f:
128
+ f.write("Initial content")
129
+
130
+ # 等待文件创建完成
131
+ time.sleep(1.0)
132
+
133
+ # 清空事件记录
134
+ with self.event_lock:
135
+ self.events.clear()
136
+
137
+ # 注册监控
138
+ self.monitor.register(test_file, self.record_event)
139
+
140
+ # 等待一下确保监控注册完成
141
+ time.sleep(0.5)
142
+
143
+ # 尝试多种修改方式来触发修改事件
144
+ # 方式1: 追加内容
145
+ with open(test_file, 'a') as f:
146
+ f.write("\nModified content")
147
+ time.sleep(0.5)
148
+
149
+ # 方式2: 重写文件(如果追加没有触发修改事件)
150
+ if not self.wait_for_specific_event(Change.modified, test_file, timeout=1.0):
151
+ with open(test_file, 'w') as f:
152
+ f.write("Completely new content")
153
+
154
+ print(f"✅ 修改文件: {os.path.basename(test_file)}")
155
+
156
+ # 等待修改事件或添加事件(某些系统可能报告为添加)
157
+ if (self.wait_for_specific_event(Change.modified, test_file, timeout=2.0) or
158
+ self.wait_for_specific_event(Change.added, test_file, timeout=1.0)):
159
+ print("✅ 文件修改事件检测成功")
160
+ return True
161
+ else:
162
+ # 打印所有事件用于调试
163
+ with self.event_lock:
164
+ print(f"所有事件: {[(e[0].name, os.path.basename(e[1])) for e in self.events]}")
165
+ # 如果检测到任何事件,说明监控是工作的,只是事件类型不同
166
+ if self.events:
167
+ print("⚠️ 检测到文件变化事件,但类型与预期不符(这在某些文件系统中是正常的)")
168
+ return True
169
+ else:
170
+ print("❌ 未检测到任何文件修改事件")
171
+
172
+ return False
173
+
174
+ def test_file_delete(self):
175
+ """测试文件删除监控"""
176
+ print("\n🔍 测试用例 3: 文件删除监控")
177
+
178
+ # 先创建一个文件
179
+ test_file = os.path.join(self.temp_dir, "test_delete.txt")
180
+ with open(test_file, 'w') as f:
181
+ f.write("To be deleted")
182
+
183
+ # 等待文件创建完成
184
+ time.sleep(1.0)
185
+
186
+ # 清空事件记录
187
+ with self.event_lock:
188
+ self.events.clear()
189
+
190
+ # 注册监控
191
+ self.monitor.register(test_file, self.record_event)
192
+
193
+ # 等待一下确保监控注册完成
194
+ time.sleep(0.5)
195
+
196
+ # 删除文件
197
+ os.remove(test_file)
198
+ print(f"✅ 删除文件: {os.path.basename(test_file)}")
199
+
200
+ # 等待删除事件
201
+ if self.wait_for_specific_event(Change.deleted, test_file):
202
+ print("✅ 文件删除事件检测成功")
203
+ return True
204
+ else:
205
+ # 打印所有事件用于调试
206
+ with self.event_lock:
207
+ print(f"所有事件: {[(e[0].name, os.path.basename(e[1])) for e in self.events]}")
208
+ print("❌ 未检测到文件删除事件")
209
+
210
+ return False
211
+
212
+ def test_directory_monitoring(self):
213
+ """测试目录监控"""
214
+ print("\n🔍 测试用例 4: 目录监控")
215
+
216
+ # 创建子目录
217
+ sub_dir = os.path.join(self.temp_dir, "subdir")
218
+ os.makedirs(sub_dir)
219
+
220
+ # 等待目录创建完成
221
+ time.sleep(1.0)
222
+
223
+ # 清空事件记录
224
+ with self.event_lock:
225
+ self.events.clear()
226
+
227
+ # 注册监控整个子目录
228
+ self.monitor.register(sub_dir, self.record_event)
229
+
230
+ # 等待一下确保监控注册完成
231
+ time.sleep(0.5)
232
+
233
+ # 在子目录中创建文件
234
+ test_file = os.path.join(sub_dir, "subdir_file.txt")
235
+ with open(test_file, 'w') as f:
236
+ f.write("File in subdirectory")
237
+ print(f"✅ 在子目录中创建文件: {os.path.basename(test_file)}")
238
+
239
+ # 等待文件创建事件
240
+ if self.wait_for_specific_event(Change.added, test_file):
241
+ print("✅ 目录监控事件检测成功")
242
+ return True
243
+ else:
244
+ # 打印所有事件用于调试
245
+ with self.event_lock:
246
+ print(f"所有事件: {[(e[0].name, os.path.basename(e[1])) for e in self.events]}")
247
+ print("❌ 未检测到目录监控事件")
248
+
249
+ return False
250
+
251
+ def run_all_tests(self):
252
+ """运行所有测试"""
253
+ print("🚀 开始 FileMonitor 功能测试")
254
+ print("=" * 50)
255
+
256
+ if Change is None:
257
+ print("❌ 测试失败: watchfiles 库未安装")
258
+ return False
259
+
260
+ try:
261
+ self.setup()
262
+
263
+ # 运行测试用例
264
+ test_results = []
265
+ test_results.append(self.test_file_create())
266
+ test_results.append(self.test_file_modify())
267
+ test_results.append(self.test_file_delete())
268
+ test_results.append(self.test_directory_monitoring())
269
+
270
+ # 统计结果
271
+ passed = sum(test_results)
272
+ total = len(test_results)
273
+
274
+ print("\n" + "=" * 50)
275
+ print(f"📊 测试结果: {passed}/{total} 个测试用例通过")
276
+
277
+ if passed == total:
278
+ print("🎉 所有测试用例通过!FileMonitor 功能正常")
279
+ return True
280
+ else:
281
+ print(f"⚠️ {total - passed} 个测试用例失败")
282
+ return False
283
+
284
+ except Exception as e:
285
+ print(f"❌ 测试过程中发生异常: {e}")
286
+ import traceback
287
+ traceback.print_exc()
288
+ return False
289
+ finally:
290
+ self.teardown()
291
+
292
+
293
+ def main():
294
+ """主函数"""
295
+ tester = FileMonitorTester()
296
+ success = tester.run_all_tests()
297
+
298
+ if success:
299
+ print("\n✅ FileMonitor 测试完成,所有功能正常")
300
+ exit(0)
301
+ else:
302
+ print("\n❌ FileMonitor 测试失败")
303
+ exit(1)
304
+
305
+
306
+ if __name__ == "__main__":
307
+ main()
@@ -294,7 +294,7 @@ def get_uncommitted_changes(repo_path: str) -> str:
294
294
  return f"Error: {str(e)}"
295
295
 
296
296
  @byzerllm.prompt()
297
- def generate_commit_message(changes_report: str) -> str:
297
+ def generate_commit_message(changes_report: str, query: Optional[str] = None) -> str:
298
298
  '''
299
299
  我是一个Git提交信息生成助手。我们的目标是通过一些变更报告,倒推用户的需求,将需求作为commit message。
300
300
  commit message 需要简洁,包含两部分:
@@ -635,7 +635,12 @@ def generate_commit_message(changes_report: str) -> str:
635
635
  </examples>
636
636
 
637
637
  下面是变更报告:
638
- {{ changes_report }}
638
+ {{ changes_report }}
639
+
640
+ {% if query %}
641
+ 这里你需要遵守的用户的额外要求:
642
+ {{ query }}
643
+ {% endif %}
639
644
 
640
645
  请输出commit message, 不要输出任何其他内容.
641
646
  '''
File without changes
@@ -0,0 +1,197 @@
1
+ from typing import List, Dict, Any, Union
2
+ import json
3
+ import re
4
+ from pydantic import BaseModel
5
+ import byzerllm
6
+ from autocoder.common.printer import Printer
7
+ from autocoder.rag.token_counter import count_tokens
8
+ from loguru import logger
9
+ from autocoder.common import AutoCoderArgs
10
+ from autocoder.common.save_formatted_log import save_formatted_log
11
+
12
+ class AgenticPruneStrategy(BaseModel):
13
+ name: str
14
+ description: str
15
+ config: Dict[str, Any] = {"safe_zone_tokens": 0}
16
+
17
+ class AgenticConversationPruner:
18
+ """
19
+ Specialized conversation pruner for agentic conversations that cleans up tool outputs.
20
+
21
+ This pruner specifically targets tool result messages (role='user', content contains '<tool_result>')
22
+ and replaces their content with a placeholder message to reduce token usage while maintaining
23
+ conversation flow.
24
+ """
25
+
26
+ def __init__(self, args: AutoCoderArgs, llm: Union[byzerllm.ByzerLLM, byzerllm.SimpleByzerLLM]):
27
+ self.args = args
28
+ self.llm = llm
29
+ self.printer = Printer()
30
+ self.replacement_message = "This message has been cleared. If you still want to get this information, you can call the tool again to retrieve it."
31
+
32
+ self.strategies = {
33
+ "tool_output_cleanup": AgenticPruneStrategy(
34
+ name="tool_output_cleanup",
35
+ description="Clean up tool output results by replacing content with placeholder messages",
36
+ config={"safe_zone_tokens": self.args.conversation_prune_safe_zone_tokens}
37
+ )
38
+ }
39
+
40
+ def get_available_strategies(self) -> List[Dict[str, Any]]:
41
+ """Get all available pruning strategies"""
42
+ return [strategy.model_dump() for strategy in self.strategies.values()]
43
+
44
+ def prune_conversations(self, conversations: List[Dict[str, Any]],
45
+ strategy_name: str = "tool_output_cleanup") -> List[Dict[str, Any]]:
46
+ """
47
+ Prune conversations by cleaning up tool outputs.
48
+
49
+ Args:
50
+ conversations: Original conversation list
51
+ strategy_name: Strategy name
52
+
53
+ Returns:
54
+ Pruned conversation list
55
+ """
56
+ safe_zone_tokens = self.args.conversation_prune_safe_zone_tokens
57
+ current_tokens = count_tokens(json.dumps(conversations, ensure_ascii=False))
58
+
59
+ if current_tokens <= safe_zone_tokens:
60
+ return conversations
61
+
62
+ strategy = self.strategies.get(strategy_name, self.strategies["tool_output_cleanup"])
63
+
64
+ if strategy.name == "tool_output_cleanup":
65
+ return self._tool_output_cleanup_prune(conversations, strategy.config)
66
+ else:
67
+ logger.warning(f"Unknown strategy: {strategy_name}, using tool_output_cleanup instead")
68
+ return self._tool_output_cleanup_prune(conversations, strategy.config)
69
+
70
+ def _tool_output_cleanup_prune(self, conversations: List[Dict[str, Any]],
71
+ config: Dict[str, Any]) -> List[Dict[str, Any]]:
72
+ """
73
+ Clean up tool outputs by replacing their content with placeholder messages.
74
+
75
+ This method:
76
+ 1. Identifies tool result messages (role='user' with '<tool_result' in content)
77
+ 2. Starts from the first tool output and progressively cleans them
78
+ 3. Stops when token count is within safe zone
79
+ """
80
+ safe_zone_tokens = config.get("safe_zone_tokens", 50 * 1024)
81
+ processed_conversations = conversations.copy()
82
+
83
+ # Find all tool result message indices
84
+ tool_result_indices = []
85
+ for i, conv in enumerate(processed_conversations):
86
+ if (conv.get("role") == "user" and
87
+ isinstance(conv.get("content"), str) and
88
+ self._is_tool_result_message(conv.get("content", ""))):
89
+ tool_result_indices.append(i)
90
+
91
+ logger.info(f"Found {len(tool_result_indices)} tool result messages to potentially clean")
92
+
93
+ # Clean tool outputs one by one, starting from the first one
94
+ for tool_index in tool_result_indices:
95
+ current_tokens = count_tokens(json.dumps(processed_conversations, ensure_ascii=False))
96
+
97
+ if current_tokens <= safe_zone_tokens:
98
+ logger.info(f"Token count ({current_tokens}) is within safe zone ({safe_zone_tokens}), stopping cleanup")
99
+ break
100
+
101
+ # Extract tool name for a more specific replacement message
102
+ tool_name = self._extract_tool_name(processed_conversations[tool_index]["content"])
103
+ replacement_content = self._generate_replacement_message(tool_name)
104
+
105
+ # Replace the content
106
+ original_content = processed_conversations[tool_index]["content"]
107
+ processed_conversations[tool_index]["content"] = replacement_content
108
+
109
+ logger.info(f"Cleaned tool result at index {tool_index} (tool: {tool_name}), "
110
+ f"reduced from {len(original_content)} to {len(replacement_content)} characters")
111
+
112
+ final_tokens = count_tokens(json.dumps(processed_conversations, ensure_ascii=False))
113
+ logger.info(f"Cleanup completed. Token count: {current_tokens} -> {final_tokens}")
114
+
115
+ save_formatted_log(self.args.source_dir, json.dumps(conversations, ensure_ascii=False), "agentic_pruned_conversation")
116
+
117
+ return processed_conversations
118
+
119
+ def _is_tool_result_message(self, content: str) -> bool:
120
+ """
121
+ Check if a message content contains tool result XML.
122
+
123
+ Args:
124
+ content: Message content to check
125
+
126
+ Returns:
127
+ True if content contains tool result format
128
+ """
129
+ return "<tool_result" in content and "tool_name=" in content
130
+
131
+ def _extract_tool_name(self, content: str) -> str:
132
+ """
133
+ Extract tool name from tool result XML content.
134
+
135
+ Args:
136
+ content: Tool result XML content
137
+
138
+ Returns:
139
+ Tool name or 'unknown' if not found
140
+ """
141
+ # Pattern to match: <tool_result tool_name='...' or <tool_result tool_name="..."
142
+ pattern = r"<tool_result[^>]*tool_name=['\"]([^'\"]+)['\"]"
143
+ match = re.search(pattern, content)
144
+
145
+ if match:
146
+ return match.group(1)
147
+ return "unknown"
148
+
149
+ def _generate_replacement_message(self, tool_name: str) -> str:
150
+ """
151
+ Generate a replacement message for a cleaned tool result.
152
+
153
+ Args:
154
+ tool_name: Name of the tool that was called
155
+
156
+ Returns:
157
+ Replacement message string
158
+ """
159
+ if tool_name and tool_name != "unknown":
160
+ return (f"<tool_result tool_name='{tool_name}' success='true'>"
161
+ f"<message>Content cleared to save tokens</message>"
162
+ f"<content>{self.replacement_message}</content>"
163
+ f"</tool_result>")
164
+ else:
165
+ return f"<tool_result success='true'><message>[Content cleared to save tokens, you can call the tool again to get the tool result.]</message><content>{self.replacement_message}</content></tool_result>"
166
+
167
+ def get_cleanup_statistics(self, original_conversations: List[Dict[str, Any]],
168
+ pruned_conversations: List[Dict[str, Any]]) -> Dict[str, Any]:
169
+ """
170
+ Get statistics about the cleanup process.
171
+
172
+ Args:
173
+ original_conversations: Original conversation list
174
+ pruned_conversations: Pruned conversation list
175
+
176
+ Returns:
177
+ Dictionary with cleanup statistics
178
+ """
179
+ original_tokens = count_tokens(json.dumps(original_conversations, ensure_ascii=False))
180
+ pruned_tokens = count_tokens(json.dumps(pruned_conversations, ensure_ascii=False))
181
+
182
+ # Count cleaned tool results
183
+ cleaned_count = 0
184
+ for orig, pruned in zip(original_conversations, pruned_conversations):
185
+ if (orig.get("role") == "user" and
186
+ self._is_tool_result_message(orig.get("content", "")) and
187
+ orig.get("content") != pruned.get("content")):
188
+ cleaned_count += 1
189
+
190
+ return {
191
+ "original_tokens": original_tokens,
192
+ "pruned_tokens": pruned_tokens,
193
+ "tokens_saved": original_tokens - pruned_tokens,
194
+ "compression_ratio": pruned_tokens / original_tokens if original_tokens > 0 else 1.0,
195
+ "tool_results_cleaned": cleaned_count,
196
+ "total_messages": len(original_conversations)
197
+ }