aider-ce 0.88.20__py3-none-any.whl → 0.88.38__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.
Files changed (113) hide show
  1. aider/__init__.py +1 -1
  2. aider/_version.py +2 -2
  3. aider/args.py +63 -43
  4. aider/coders/agent_coder.py +331 -79
  5. aider/coders/agent_prompts.py +3 -15
  6. aider/coders/architect_coder.py +21 -5
  7. aider/coders/base_coder.py +661 -413
  8. aider/coders/base_prompts.py +6 -3
  9. aider/coders/chat_chunks.py +39 -17
  10. aider/commands.py +79 -15
  11. aider/diffs.py +10 -9
  12. aider/exceptions.py +1 -1
  13. aider/helpers/coroutines.py +8 -0
  14. aider/helpers/requests.py +45 -0
  15. aider/history.py +5 -0
  16. aider/io.py +179 -25
  17. aider/main.py +86 -35
  18. aider/models.py +16 -8
  19. aider/queries/tree-sitter-language-pack/c-tags.scm +3 -0
  20. aider/queries/tree-sitter-language-pack/clojure-tags.scm +5 -0
  21. aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +5 -0
  22. aider/queries/tree-sitter-language-pack/cpp-tags.scm +3 -0
  23. aider/queries/tree-sitter-language-pack/csharp-tags.scm +6 -0
  24. aider/queries/tree-sitter-language-pack/dart-tags.scm +5 -0
  25. aider/queries/tree-sitter-language-pack/elixir-tags.scm +5 -0
  26. aider/queries/tree-sitter-language-pack/elm-tags.scm +3 -0
  27. aider/queries/tree-sitter-language-pack/go-tags.scm +7 -0
  28. aider/queries/tree-sitter-language-pack/java-tags.scm +6 -0
  29. aider/queries/tree-sitter-language-pack/javascript-tags.scm +8 -0
  30. aider/queries/tree-sitter-language-pack/lua-tags.scm +5 -0
  31. aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +3 -0
  32. aider/queries/tree-sitter-language-pack/python-tags.scm +10 -0
  33. aider/queries/tree-sitter-language-pack/r-tags.scm +6 -0
  34. aider/queries/tree-sitter-language-pack/ruby-tags.scm +5 -0
  35. aider/queries/tree-sitter-language-pack/rust-tags.scm +3 -0
  36. aider/queries/tree-sitter-language-pack/solidity-tags.scm +1 -1
  37. aider/queries/tree-sitter-language-pack/swift-tags.scm +4 -1
  38. aider/queries/tree-sitter-languages/c-tags.scm +3 -0
  39. aider/queries/tree-sitter-languages/c_sharp-tags.scm +6 -0
  40. aider/queries/tree-sitter-languages/cpp-tags.scm +3 -0
  41. aider/queries/tree-sitter-languages/dart-tags.scm +2 -1
  42. aider/queries/tree-sitter-languages/elixir-tags.scm +5 -0
  43. aider/queries/tree-sitter-languages/elm-tags.scm +3 -0
  44. aider/queries/tree-sitter-languages/fortran-tags.scm +3 -0
  45. aider/queries/tree-sitter-languages/go-tags.scm +6 -0
  46. aider/queries/tree-sitter-languages/haskell-tags.scm +2 -0
  47. aider/queries/tree-sitter-languages/java-tags.scm +6 -0
  48. aider/queries/tree-sitter-languages/javascript-tags.scm +8 -0
  49. aider/queries/tree-sitter-languages/julia-tags.scm +2 -2
  50. aider/queries/tree-sitter-languages/kotlin-tags.scm +3 -0
  51. aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +6 -0
  52. aider/queries/tree-sitter-languages/php-tags.scm +6 -0
  53. aider/queries/tree-sitter-languages/python-tags.scm +10 -0
  54. aider/queries/tree-sitter-languages/ruby-tags.scm +5 -0
  55. aider/queries/tree-sitter-languages/rust-tags.scm +3 -0
  56. aider/queries/tree-sitter-languages/scala-tags.scm +2 -3
  57. aider/queries/tree-sitter-languages/typescript-tags.scm +3 -0
  58. aider/queries/tree-sitter-languages/zig-tags.scm +20 -3
  59. aider/repomap.py +71 -11
  60. aider/resources/model-metadata.json +27335 -635
  61. aider/resources/model-settings.yml +190 -0
  62. aider/scrape.py +2 -0
  63. aider/tools/__init__.py +2 -0
  64. aider/tools/command.py +84 -94
  65. aider/tools/command_interactive.py +95 -110
  66. aider/tools/delete_block.py +131 -159
  67. aider/tools/delete_line.py +97 -132
  68. aider/tools/delete_lines.py +120 -160
  69. aider/tools/extract_lines.py +288 -312
  70. aider/tools/finished.py +30 -43
  71. aider/tools/git_branch.py +107 -109
  72. aider/tools/git_diff.py +44 -56
  73. aider/tools/git_log.py +39 -53
  74. aider/tools/git_remote.py +37 -51
  75. aider/tools/git_show.py +33 -47
  76. aider/tools/git_status.py +30 -44
  77. aider/tools/grep.py +214 -242
  78. aider/tools/indent_lines.py +175 -201
  79. aider/tools/insert_block.py +220 -253
  80. aider/tools/list_changes.py +65 -80
  81. aider/tools/ls.py +64 -80
  82. aider/tools/make_editable.py +57 -73
  83. aider/tools/make_readonly.py +50 -66
  84. aider/tools/remove.py +64 -80
  85. aider/tools/replace_all.py +96 -109
  86. aider/tools/replace_line.py +118 -156
  87. aider/tools/replace_lines.py +160 -197
  88. aider/tools/replace_text.py +159 -160
  89. aider/tools/show_numbered_context.py +115 -141
  90. aider/tools/thinking.py +52 -0
  91. aider/tools/undo_change.py +78 -91
  92. aider/tools/update_todo_list.py +130 -138
  93. aider/tools/utils/base_tool.py +64 -0
  94. aider/tools/utils/output.py +118 -0
  95. aider/tools/view.py +38 -54
  96. aider/tools/view_files_matching.py +131 -134
  97. aider/tools/view_files_with_symbol.py +108 -120
  98. aider/urls.py +1 -1
  99. aider/versioncheck.py +4 -3
  100. aider/website/docs/config/adv-model-settings.md +237 -0
  101. aider/website/docs/config/agent-mode.md +36 -3
  102. aider/website/docs/config/model-aliases.md +2 -1
  103. aider/website/docs/faq.md +6 -11
  104. aider/website/docs/languages.md +2 -2
  105. aider/website/docs/more/infinite-output.md +27 -0
  106. {aider_ce-0.88.20.dist-info → aider_ce-0.88.38.dist-info}/METADATA +112 -70
  107. {aider_ce-0.88.20.dist-info → aider_ce-0.88.38.dist-info}/RECORD +112 -107
  108. aider_ce-0.88.38.dist-info/entry_points.txt +6 -0
  109. aider_ce-0.88.20.dist-info/entry_points.txt +0 -2
  110. /aider/tools/{tool_utils.py → utils/helpers.py} +0 -0
  111. {aider_ce-0.88.20.dist-info → aider_ce-0.88.38.dist-info}/WHEEL +0 -0
  112. {aider_ce-0.88.20.dist-info → aider_ce-0.88.38.dist-info}/licenses/LICENSE.txt +0 -0
  113. {aider_ce-0.88.20.dist-info → aider_ce-0.88.38.dist-info}/top_level.txt +0 -0
@@ -20,6 +20,13 @@ from aider import urls, utils
20
20
 
21
21
  # Import the change tracker
22
22
  from aider.change_tracker import ChangeTracker
23
+
24
+ # Import similarity functions for tool usage analysis
25
+ from aider.helpers.similarity import (
26
+ cosine_similarity,
27
+ create_bigram_vector,
28
+ normalize_vector,
29
+ )
23
30
  from aider.mcp.server import LocalServer
24
31
  from aider.repo import ANY_GIT_ERROR
25
32
 
@@ -52,6 +59,7 @@ from aider.tools import (
52
59
  replace_lines,
53
60
  replace_text,
54
61
  show_numbered_context,
62
+ thinking,
55
63
  undo_change,
56
64
  update_todo_list,
57
65
  view,
@@ -78,8 +86,15 @@ class AgentCoder(Coder):
78
86
  self.recently_removed = {}
79
87
 
80
88
  # Tool usage history
81
- self.tool_usage_history = []
89
+ self.tool_usage_history = [] # Stores lists of tools used in each round
82
90
  self.tool_usage_retries = 10
91
+ self.last_round_tools = [] # Tools used in the current round
92
+
93
+ # Similarity tracking for tool usage
94
+ self.tool_call_vectors = [] # Store vectors for individual tool calls
95
+ self.tool_similarity_threshold = 0.99 # High threshold for exact matches
96
+ self.max_tool_vector_history = 10 # Keep history of 10 rounds
97
+
83
98
  self.read_tools = {
84
99
  "viewfilesatglob",
85
100
  "viewfilesmatching",
@@ -101,7 +116,7 @@ class AgentCoder(Coder):
101
116
  }
102
117
 
103
118
  # Configuration parameters
104
- self.max_tool_calls = 100 # Maximum number of tool calls per response
119
+ self.max_tool_calls = 10000 # Maximum number of tool calls per response
105
120
 
106
121
  # Context management parameters
107
122
  # Will be overridden by agent_config if provided
@@ -117,7 +132,7 @@ class AgentCoder(Coder):
117
132
 
118
133
  # Initialize tool registry
119
134
  self.args = kwargs.get("args")
120
- self._tool_registry = self._build_tool_registry()
135
+ self.tool_registry = self._build_tool_registry()
121
136
 
122
137
  # Track files added during current exploration
123
138
  self.files_added_in_exploration = set()
@@ -132,14 +147,20 @@ class AgentCoder(Coder):
132
147
  # Enable enhanced context blocks by default
133
148
  self.use_enhanced_context = True
134
149
 
150
+ # Caching efficiency attributes
151
+ self._last_edited_file = None
152
+ self._cur_message_divider = None
153
+
135
154
  # Initialize empty token tracking dictionary and cache structures
136
155
  # but don't populate yet to avoid startup delay
156
+ self.allowed_context_blocks = set()
137
157
  self.context_block_tokens = {}
138
158
  self.context_blocks_cache = {}
139
159
  self.tokens_calculated = False
140
160
 
141
161
  self.skip_cli_confirmations = False
142
162
 
163
+ self.agent_finished = False
143
164
  self._get_agent_config()
144
165
  super().__init__(*args, **kwargs)
145
166
 
@@ -181,6 +202,7 @@ class AgentCoder(Coder):
181
202
  replace_lines,
182
203
  replace_text,
183
204
  show_numbered_context,
205
+ thinking,
184
206
  undo_change,
185
207
  update_todo_list,
186
208
  view,
@@ -200,8 +222,9 @@ class AgentCoder(Coder):
200
222
  # Always include essential tools regardless of includelist/excludelist
201
223
  essential_tools = {"makeeditable", "replacetext", "view", "finished"}
202
224
  for module in tool_modules:
203
- if hasattr(module, "NORM_NAME") and hasattr(module, "process_response"):
204
- tool_name = module.NORM_NAME
225
+ if hasattr(module, "Tool"):
226
+ tool_class = module.Tool
227
+ tool_name = tool_class.NORM_NAME
205
228
 
206
229
  # Check if tool should be included based on configuration
207
230
  should_include = True
@@ -219,7 +242,7 @@ class AgentCoder(Coder):
219
242
  should_include = False
220
243
 
221
244
  if should_include:
222
- registry[tool_name] = module
245
+ registry[tool_name] = tool_class
223
246
 
224
247
  return registry
225
248
 
@@ -253,6 +276,25 @@ class AgentCoder(Coder):
253
276
  if "tools_excludelist" not in config:
254
277
  config["tools_excludelist"] = []
255
278
 
279
+ if "include_context_blocks" in config:
280
+ self.allowed_context_blocks = set(config["context_blocks"])
281
+ else:
282
+ self.allowed_context_blocks = {
283
+ "context_summary",
284
+ "directory_structure",
285
+ "environment_info",
286
+ "git_status",
287
+ "symbol_outline",
288
+ "todo_list",
289
+ }
290
+
291
+ if "exclude_context_blocks" in config:
292
+ for context_block in config["exclude_context_blocks"]:
293
+ try:
294
+ self.allowed_context_blocks.remove(context_block)
295
+ except KeyError:
296
+ pass
297
+
256
298
  # Apply configuration to instance
257
299
  self.large_file_token_threshold = config["large_file_token_threshold"]
258
300
  self.skip_cli_confirmations = config.get(
@@ -266,9 +308,9 @@ class AgentCoder(Coder):
266
308
  schemas = []
267
309
 
268
310
  # Get schemas from the tool registry
269
- for tool_module in self._tool_registry.values():
270
- if hasattr(tool_module, "schema"):
271
- schemas.append(tool_module.schema)
311
+ for tool_module in self.tool_registry.values():
312
+ if hasattr(tool_module, "SCHEMA"):
313
+ schemas.append(tool_module.SCHEMA)
272
314
 
273
315
  return schemas
274
316
 
@@ -323,8 +365,8 @@ class AgentCoder(Coder):
323
365
  tasks = []
324
366
 
325
367
  # Use the tool registry for execution
326
- if norm_tool_name in self._tool_registry:
327
- tool_module = self._tool_registry[norm_tool_name]
368
+ if norm_tool_name in self.tool_registry:
369
+ tool_module = self.tool_registry[norm_tool_name]
328
370
  for params in parsed_args_list:
329
371
  # Use the process_response function from the tool module
330
372
  result = tool_module.process_response(self, params)
@@ -464,11 +506,12 @@ class AgentCoder(Coder):
464
506
  ]
465
507
 
466
508
  for block_type in block_types:
467
- block_content = self._generate_context_block(block_type)
468
- if block_content:
469
- self.context_block_tokens[block_type] = self.main_model.token_count(
470
- block_content
471
- )
509
+ if block_type in self.allowed_context_blocks:
510
+ block_content = self._generate_context_block(block_type)
511
+ if block_content:
512
+ self.context_block_tokens[block_type] = self.main_model.token_count(
513
+ block_content
514
+ )
472
515
 
473
516
  # Mark as calculated
474
517
  self.tokens_calculated = True
@@ -651,7 +694,19 @@ class AgentCoder(Coder):
651
694
  if self.gpt_prompts.system_reminder:
652
695
  main_sys += "\n" + self.fmt_system_prompt(self.gpt_prompts.system_reminder)
653
696
 
654
- chunks = ChatChunks()
697
+ chunks = ChatChunks(
698
+ chunk_ordering=[
699
+ "system",
700
+ "examples",
701
+ "readonly_files",
702
+ "repo",
703
+ "chat_files",
704
+ "done",
705
+ "edit_files",
706
+ "cur",
707
+ "reminder",
708
+ ]
709
+ )
655
710
 
656
711
  if self.main_model.use_system_prompt:
657
712
  chunks.system = [
@@ -666,11 +721,40 @@ class AgentCoder(Coder):
666
721
  chunks.examples = example_messages
667
722
 
668
723
  self.summarize_end()
669
- chunks.done = list(self.done_messages)
724
+ cur_messages_list = list(self.cur_messages)
725
+ cur_messages_pre = []
726
+ cur_messages_post = cur_messages_list
727
+ chunks.readonly_files = self.get_readonly_files_messages()
728
+
729
+ # Handle the dictionary structure from get_chat_files_messages()
730
+ chat_files_result = self.get_chat_files_messages()
731
+ chunks.chat_files = chat_files_result.get("chat_files", [])
732
+ chunks.edit_files = chat_files_result.get("edit_files", [])
733
+ edit_file_names = chat_files_result.get("edit_file_names", set())
734
+
735
+ # Update edit file tracking for caching efficiency
736
+ divider = self._update_edit_file_tracking(edit_file_names)
737
+ if divider is not None:
738
+ # Split cur_messages using the divider
739
+ if divider > 0 and divider < len(cur_messages_list):
740
+ cur_messages_pre = cur_messages_list[:divider]
741
+ cur_messages_post = cur_messages_list[divider:]
670
742
 
671
743
  chunks.repo = self.get_repo_messages()
672
- chunks.readonly_files = self.get_readonly_files_messages()
673
- chunks.chat_files = self.get_chat_files_messages()
744
+ chunks.done = list(self.done_messages) + cur_messages_pre
745
+
746
+ # Add reminder if needed
747
+ if self.gpt_prompts.system_reminder:
748
+ reminder_message = [
749
+ dict(
750
+ role="system", content=self.fmt_system_prompt(self.gpt_prompts.system_reminder)
751
+ ),
752
+ ]
753
+ else:
754
+ reminder_message = []
755
+
756
+ chunks.cur = cur_messages_post
757
+ chunks.reminder = []
674
758
 
675
759
  # Make sure token counts are updated - using centralized method
676
760
  # This also populates the context block cache
@@ -689,53 +773,46 @@ class AgentCoder(Coder):
689
773
  # 1. Add relatively static blocks BEFORE done_messages
690
774
  # These blocks change less frequently and can be part of the cacheable prefix
691
775
  static_blocks = []
692
- if dir_structure:
693
- static_blocks.append(dir_structure)
694
- if env_context:
776
+ if env_context and "environment_info" in self.allowed_context_blocks:
695
777
  static_blocks.append(env_context)
778
+ if dir_structure and "directory_structure" in self.allowed_context_blocks:
779
+ static_blocks.append(dir_structure)
696
780
 
697
781
  if static_blocks:
698
782
  static_message = "\n\n".join(static_blocks)
699
783
  # Insert as a system message right before done_messages
700
- chunks.done.insert(0, dict(role="system", content=static_message))
784
+ chunks.system.append(dict(role="system", content=static_message))
701
785
 
702
786
  # 2. Add dynamic blocks AFTER chat_files
703
787
  # These blocks change with the current files in context
704
- dynamic_blocks = []
705
- if todo_list:
706
- dynamic_blocks.append(todo_list)
707
- if context_summary:
708
- dynamic_blocks.append(context_summary)
709
- if symbol_outline:
710
- dynamic_blocks.append(symbol_outline)
711
- if git_status:
712
- dynamic_blocks.append(git_status)
713
-
788
+ pre_dynamic_blocks = []
789
+ post_dynamic_blocks = []
790
+ if context_summary and "context_summary" in self.allowed_context_blocks:
791
+ pre_dynamic_blocks.append(context_summary)
792
+ if symbol_outline and "symbol_outline" in self.allowed_context_blocks:
793
+ pre_dynamic_blocks.append(symbol_outline)
794
+ if git_status and "git_status" in self.allowed_context_blocks:
795
+ pre_dynamic_blocks.append(git_status)
796
+
797
+ if todo_list and "todo_list" in self.allowed_context_blocks:
798
+ pre_dynamic_blocks.append(todo_list)
714
799
  # Add tool usage context if there are repetitive tools
715
800
  if hasattr(self, "tool_usage_history") and self.tool_usage_history:
716
801
  repetitive_tools = self._get_repetitive_tools()
717
802
  if repetitive_tools:
718
803
  tool_context = self._generate_tool_context(repetitive_tools)
719
804
  if tool_context:
720
- dynamic_blocks.append(tool_context)
805
+ pre_dynamic_blocks.append(tool_context)
721
806
 
722
- if dynamic_blocks:
723
- dynamic_message = "\n\n".join(dynamic_blocks)
724
- # Append as a system message after chat_files
725
- chunks.chat_files.append(dict(role="system", content=dynamic_message))
807
+ if pre_dynamic_blocks:
808
+ dynamic_message = "\n\n".join(pre_dynamic_blocks)
809
+ # Append as a system message on reminders
810
+ chunks.done.insert(0, dict(role="system", content=dynamic_message))
726
811
 
727
- # Add reminder if needed
728
- if self.gpt_prompts.system_reminder:
729
- reminder_message = [
730
- dict(
731
- role="system", content=self.fmt_system_prompt(self.gpt_prompts.system_reminder)
732
- ),
733
- ]
734
- else:
735
- reminder_message = []
736
-
737
- chunks.cur = list(self.cur_messages)
738
- chunks.reminder = []
812
+ if post_dynamic_blocks:
813
+ dynamic_message = "\n\n".join(post_dynamic_blocks)
814
+ # Append as a system message on reminders
815
+ reminder_message.insert(0, dict(role="system", content=dynamic_message))
739
816
 
740
817
  # Use accurate token counting method that considers enhanced context blocks
741
818
  base_messages = chunks.all_messages()
@@ -780,6 +857,37 @@ class AgentCoder(Coder):
780
857
 
781
858
  return chunks
782
859
 
860
+ def _update_edit_file_tracking(self, edit_file_names):
861
+ """
862
+ Update tracking for last edited file and message divider for caching efficiency.
863
+
864
+ When the last edited file changes, we store the current message index minus 4
865
+ as a divider to split cur_messages, moving older messages to done_messages
866
+ for better caching.
867
+ """
868
+ kept_messages = 8
869
+ if not edit_file_names:
870
+ self._cur_message_divider = 0
871
+
872
+ # Get the most recently edited file from the edit_file_names set
873
+ # We assume the first file in the sorted set is the most recent
874
+ sorted_edit_files = sorted(edit_file_names)
875
+ current_edited_file = sorted_edit_files[0] if sorted_edit_files else None
876
+
877
+ # Check if the last edited file has changed
878
+ if current_edited_file != self._last_edited_file:
879
+ # Store the new last edited file
880
+ self._last_edited_file = current_edited_file
881
+
882
+ # Calculate divider: current index minus last n messages
883
+ cur_messages_list = list(self.cur_messages)
884
+ if len(cur_messages_list) > kept_messages:
885
+ self._cur_message_divider = len(cur_messages_list) - kept_messages
886
+ else:
887
+ self._cur_message_divider = 0
888
+
889
+ return self._cur_message_divider
890
+
783
891
  def get_context_summary(self):
784
892
  """
785
893
  Generate a summary of the current context, including file content tokens and additional context blocks,
@@ -944,15 +1052,40 @@ class AgentCoder(Coder):
944
1052
  """
945
1053
  Track tool usage before calling the base implementation.
946
1054
  """
947
- self.auto_save_session()
1055
+ self.agent_finished = False
1056
+ await self.auto_save_session()
1057
+
1058
+ # Clear last round tools and start tracking new round
1059
+ self.last_round_tools = []
948
1060
 
949
1061
  if self.partial_response_tool_calls:
950
1062
  for tool_call in self.partial_response_tool_calls:
951
- self.tool_usage_history.append(tool_call.get("function", {}).get("name"))
1063
+ tool_name = tool_call.get("function", {}).get("name")
1064
+
1065
+ if tool_name:
1066
+ self.last_round_tools.append(tool_name)
1067
+
1068
+ # Create and store vector for this tool call
1069
+ # Remove id property if present before stringifying
1070
+ tool_call_copy = tool_call.copy()
1071
+ if "id" in tool_call_copy:
1072
+ del tool_call_copy["id"]
1073
+ tool_call_str = str(tool_call_copy) # Convert entire tool call to string
1074
+ tool_vector = create_bigram_vector((tool_call_str,))
1075
+ tool_vector_norm = normalize_vector(tool_vector)
1076
+ self.tool_call_vectors.append(tool_vector_norm)
1077
+
1078
+ # Add the completed round to history
1079
+ if self.last_round_tools:
1080
+ self.tool_usage_history += self.last_round_tools
1081
+ self.tool_usage_history = list(filter(None, self.tool_usage_history))
952
1082
 
953
1083
  if len(self.tool_usage_history) > self.tool_usage_retries:
954
1084
  self.tool_usage_history.pop(0)
955
1085
 
1086
+ if len(self.tool_call_vectors) > self.max_tool_vector_history:
1087
+ self.tool_call_vectors.pop(0)
1088
+
956
1089
  return await super().process_tool_calls(tool_call_response)
957
1090
 
958
1091
  async def reply_completed(self):
@@ -968,7 +1101,6 @@ class AgentCoder(Coder):
968
1101
  a final answer to the user's question.
969
1102
  """
970
1103
  # Legacy tool call processing for use_granular_editing=False
971
- self.agent_finished = False
972
1104
  content = self.partial_response_content
973
1105
  if not content or not content.strip():
974
1106
  if len(self.tool_usage_history) > self.tool_usage_retries:
@@ -1136,8 +1268,8 @@ class AgentCoder(Coder):
1136
1268
  str: Result message
1137
1269
  """
1138
1270
  # Check if tool exists in registry
1139
- if norm_tool_name in self._tool_registry:
1140
- tool_module = self._tool_registry[norm_tool_name]
1271
+ if norm_tool_name in self.tool_registry:
1272
+ tool_module = self.tool_registry[norm_tool_name]
1141
1273
  try:
1142
1274
  # Use the process_response function from the tool module
1143
1275
  result = tool_module.process_response(self, params)
@@ -1163,6 +1295,73 @@ class AgentCoder(Coder):
1163
1295
 
1164
1296
  return f"Error: Unknown tool name '{norm_tool_name}'"
1165
1297
 
1298
+ def _convert_concatenated_json_to_tool_calls(self, content):
1299
+ """
1300
+ Check if content contains concatenated JSON objects and convert them to tool call format.
1301
+
1302
+ Args:
1303
+ content (str): Content to check for concatenated JSON
1304
+
1305
+ Returns:
1306
+ str: Content with concatenated JSON converted to tool call format, or original content if no JSON found
1307
+ """
1308
+ try:
1309
+ # Use split_concatenated_json to detect and split concatenated JSON objects
1310
+ json_chunks = utils.split_concatenated_json(content)
1311
+
1312
+ # If we found multiple JSON objects, convert them to tool call format
1313
+ if len(json_chunks) >= 1:
1314
+ tool_calls = []
1315
+ for chunk in json_chunks:
1316
+ try:
1317
+ json_obj = json.loads(chunk)
1318
+ # Check if this looks like a tool call JSON object
1319
+ if (
1320
+ isinstance(json_obj, dict)
1321
+ and "name" in json_obj
1322
+ and "arguments" in json_obj
1323
+ ):
1324
+ tool_name = json_obj["name"]
1325
+ arguments = json_obj["arguments"]
1326
+
1327
+ # Convert arguments dictionary to keyword arguments string
1328
+ kw_args = []
1329
+ for key, value in arguments.items():
1330
+ if isinstance(value, str):
1331
+ # Escape quotes and wrap in quotes
1332
+ escaped_value = value.replace('"', '\\"')
1333
+ kw_args.append(f'{key}="{escaped_value}"')
1334
+ elif isinstance(value, bool):
1335
+ kw_args.append(f"{key}={str(value).lower()}")
1336
+ elif value is None:
1337
+ kw_args.append(f"{key}=None")
1338
+ else:
1339
+ # For numbers and other types, use repr for safe representation
1340
+ kw_args.append(f"{key}={repr(value)}")
1341
+
1342
+ # Join keyword arguments
1343
+ kw_args_str = ", ".join(kw_args)
1344
+
1345
+ # Convert to [tool_call(ToolName, key1="value1", key2="value2")] format
1346
+ tool_call = f"[tool_call({tool_name}, {kw_args_str})]"
1347
+ tool_calls.append(tool_call)
1348
+ else:
1349
+ # Not a tool call JSON, keep as is
1350
+ tool_calls.append(chunk)
1351
+ except json.JSONDecodeError:
1352
+ # Invalid JSON, keep as is
1353
+ tool_calls.append(chunk)
1354
+
1355
+ # If we found any tool calls, replace the content
1356
+ if any(call.startswith("[tool_") for call in tool_calls):
1357
+ return "".join(tool_calls)
1358
+
1359
+ except Exception as e:
1360
+ # If anything goes wrong, return original content
1361
+ self.io.tool_warning(f"Error converting concatenated JSON to tool calls: {str(e)}")
1362
+
1363
+ return content
1364
+
1166
1365
  async def _process_tool_commands(self, content):
1167
1366
  """
1168
1367
  Process tool commands in the `[tool_call(name, param=value)]` format within the content.
@@ -1182,20 +1381,31 @@ class AgentCoder(Coder):
1182
1381
  max_calls = self.max_tool_calls
1183
1382
  tool_names = []
1184
1383
 
1384
+ # Check if content contains concatenated JSON and convert to tool call format
1385
+ content = self._convert_concatenated_json_to_tool_calls(content)
1386
+
1185
1387
  # Check if there's a '---' separator and only process tool calls after the LAST one
1186
1388
  separator_marker = "---"
1187
1389
  content_parts = content.split(separator_marker)
1188
1390
 
1189
1391
  # If there's no separator, treat the entire content as before the separator
1392
+ # But only return immediately if no tool calls were found in the JSON conversion
1190
1393
  if len(content_parts) == 1:
1191
- # Return the original content with no tool calls processed, and the content itself as before_separator
1192
- return content, result_messages, False, content, tool_names
1394
+ # Check if we have any tool calls in the content after JSON conversion
1395
+ # If we have tool calls, we should process them even without a separator
1396
+ tool_call_pattern = r"\[tool_call\([^\]]+\)\]"
1397
+ if re.search(tool_call_pattern, content):
1398
+ # We have tool calls, so continue processing
1399
+ content_before_separator = ""
1400
+ content_after_separator = content
1401
+ else:
1402
+ # No tool calls found, return the original content
1403
+ return content, result_messages, False, content, tool_names
1193
1404
 
1194
1405
  # Take everything before the last separator (including intermediate separators)
1195
1406
  content_before_separator = separator_marker.join(content_parts[:-1])
1196
1407
  # Take only what comes after the last separator
1197
1408
  content_after_separator = content_parts[-1]
1198
-
1199
1409
  # Find tool calls using a more robust method, but only in the content after separator
1200
1410
  processed_content = content_before_separator + separator_marker
1201
1411
  last_index = 0
@@ -1492,13 +1702,14 @@ class AgentCoder(Coder):
1492
1702
 
1493
1703
  def _get_repetitive_tools(self):
1494
1704
  """
1495
- Identifies repetitive tool usage patterns from a flat list of tool calls.
1705
+ Identifies repetitive tool usage patterns from rounds of tool calls.
1496
1706
 
1497
- This method checks for the following patterns in order:
1498
- 1. If the last tool used was a write tool, it assumes progress and returns no repetitive tools.
1499
- 2. It checks for any read tool that has been used 2 or more times in the history.
1707
+ This method combines count-based and similarity-based detection:
1708
+ 1. If the last round contained a write tool, it assumes progress and returns no repetitive tools.
1709
+ 2. It checks for any read tool that has been used 2 or more times across rounds.
1500
1710
  3. If no tools are repeated, but all tools in the history are read tools,
1501
1711
  it flags all of them as potentially repetitive.
1712
+ 4. It checks for similarity-based repetition using cosine similarity on tool call strings.
1502
1713
 
1503
1714
  It avoids flagging repetition if a "write" tool was used recently,
1504
1715
  as that suggests progress is being made.
@@ -1509,31 +1720,71 @@ class AgentCoder(Coder):
1509
1720
  if history_len < 2:
1510
1721
  return set()
1511
1722
 
1512
- # If the last tool was a write tool, we're likely making progress.
1513
- if isinstance(self.tool_usage_history[-1], str):
1514
- last_tool_lower = self.tool_usage_history[-1].lower()
1723
+ # Check for similarity-based repetition
1724
+ similarity_repetitive_tools = self._get_repetitive_tools_by_similarity()
1725
+
1726
+ # Flatten the tool usage history for count-based analysis
1727
+ all_tools = []
1728
+ for round_tools in self.tool_usage_history:
1729
+ all_tools.extend(round_tools)
1515
1730
 
1516
- if last_tool_lower in self.write_tools:
1731
+ # If the last round contained a write tool, we're likely making progress.
1732
+ if self.last_round_tools:
1733
+ last_round_has_write = any(
1734
+ tool.lower() in self.write_tools for tool in self.last_round_tools
1735
+ )
1736
+ if last_round_has_write:
1517
1737
  self.tool_usage_history = []
1518
- return set()
1738
+ return similarity_repetitive_tools if len(similarity_repetitive_tools) else set()
1519
1739
 
1520
1740
  # If all tools in history are read tools, return all of them
1521
- if all(tool.lower() in self.read_tools for tool in self.tool_usage_history):
1522
- return set(tool for tool in self.tool_usage_history)
1741
+ if all(tool.lower() in self.read_tools for tool in all_tools):
1742
+ return set(all_tools)
1523
1743
 
1524
- # Check for any read tool used more than once
1525
- tool_counts = Counter(tool for tool in self.tool_usage_history)
1526
- repetitive_tools = {
1744
+ # Check for any read tool used more than once across rounds
1745
+ tool_counts = Counter(all_tools)
1746
+ count_repetitive_tools = {
1527
1747
  tool
1528
1748
  for tool, count in tool_counts.items()
1529
1749
  if count >= 2 and tool.lower() in self.read_tools
1530
1750
  }
1531
1751
 
1752
+ # Combine both detection methods
1753
+ repetitive_tools = count_repetitive_tools.union(similarity_repetitive_tools)
1754
+
1532
1755
  if repetitive_tools:
1533
1756
  return repetitive_tools
1534
1757
 
1535
1758
  return set()
1536
1759
 
1760
+ def _get_repetitive_tools_by_similarity(self):
1761
+ """
1762
+ Identifies repetitive tool usage patterns using cosine similarity on tool call strings.
1763
+
1764
+ This method checks if the latest tool calls are highly similar (>0.99 threshold)
1765
+ to historical tool calls using bigram vector similarity.
1766
+
1767
+ Returns:
1768
+ set: Set of tool names that are repetitive based on similarity
1769
+ """
1770
+ if not self.tool_usage_history or len(self.tool_call_vectors) < 2:
1771
+ return set()
1772
+
1773
+ # Get the latest tool call vector
1774
+ latest_vector = self.tool_call_vectors[-1]
1775
+
1776
+ # Check similarity against historical vectors (excluding the latest)
1777
+ for i, historical_vector in enumerate(self.tool_call_vectors[:-1]):
1778
+ similarity = cosine_similarity(latest_vector, historical_vector)
1779
+
1780
+ # If similarity is high enough, flag as repetitive
1781
+ if similarity >= self.tool_similarity_threshold:
1782
+ # Return the tool name from the corresponding position in history
1783
+ if i < len(self.tool_usage_history):
1784
+ return {self.tool_usage_history[i]}
1785
+
1786
+ return set()
1787
+
1537
1788
  def _generate_tool_context(self, repetitive_tools):
1538
1789
  """
1539
1790
  Generate a context message for the LLM about recent tool usage.
@@ -1546,8 +1797,7 @@ class AgentCoder(Coder):
1546
1797
  # Add turn and tool call statistics
1547
1798
  context_parts.append("## Turn and Tool Call Statistics")
1548
1799
  context_parts.append(f"- Current turn: {self.num_reflections + 1}")
1549
- context_parts.append(f"- Tool calls this turn: {self.tool_call_count}")
1550
- context_parts.append(f"- Total tool calls in session: {self.num_tool_calls}")
1800
+ context_parts.append(f"- Total tool calls this turn: {self.num_tool_calls}")
1551
1801
  context_parts.append("\n\n")
1552
1802
 
1553
1803
  # Add recent tool usage history
@@ -1572,7 +1822,9 @@ class AgentCoder(Coder):
1572
1822
  for tool in repetitive_tools:
1573
1823
  context_parts.append(f"- `{tool}`")
1574
1824
  context_parts.append(
1575
- "Your exploration appears to be stuck in a loop. Please try a different approach:"
1825
+ "Your exploration appears to be stuck in a loop. Please try a different approach."
1826
+ " Use the `Thinking` tool to clarify your intentions and new approach to"
1827
+ " what you are currently attempting to accomplish."
1576
1828
  )
1577
1829
  context_parts.append("\n")
1578
1830
  context_parts.append("**Suggestions for alternative approaches:**")
@@ -1914,7 +2166,7 @@ Just reply with fixed versions of the {blocks} above that failed to match.
1914
2166
  if line.startswith("??"):
1915
2167
  # Extract the filename (remove the '?? ' prefix)
1916
2168
  untracked_file = line[3:]
1917
- if not self.repo.git_ignored_file(untracked_file):
2169
+ if not self.repo.ignored_file(untracked_file):
1918
2170
  untracked_files.append(untracked_file)
1919
2171
  except Exception as e:
1920
2172
  self.io.tool_warning(f"Error getting untracked files: {str(e)}")
@@ -2000,7 +2252,7 @@ Just reply with fixed versions of the {blocks} above that failed to match.
2000
2252
  if not os.path.isfile(abs_path):
2001
2253
  return (
2002
2254
  '<context name="todo_list">\n'
2003
- "Todo list does not exist. Please update it."
2255
+ "Todo list does not exist. Please update it with the `UpdataTodoList` tool."
2004
2256
  "</context>"
2005
2257
  )
2006
2258
 
@@ -2012,7 +2264,7 @@ Just reply with fixed versions of the {blocks} above that failed to match.
2012
2264
  # Format the todo list context block
2013
2265
  result = '<context name="todo_list">\n'
2014
2266
  result += "## Current Todo List\n\n"
2015
- result += "Below is the current todo list managed via `UpdateTodoList` tool:\n\n"
2267
+ result += "Below is the current todo list managed via the `UpdateTodoList` tool:\n\n"
2016
2268
  result += f"```\n{content}\n```\n"
2017
2269
  result += "</context>"
2018
2270