auto-coder 0.1.363__py3-none-any.whl → 0.1.365__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 (39) hide show
  1. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/METADATA +2 -2
  2. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/RECORD +39 -23
  3. autocoder/agent/base_agentic/tools/execute_command_tool_resolver.py +1 -1
  4. autocoder/auto_coder.py +46 -2
  5. autocoder/auto_coder_runner.py +2 -0
  6. autocoder/common/__init__.py +5 -0
  7. autocoder/common/file_checkpoint/__init__.py +21 -0
  8. autocoder/common/file_checkpoint/backup.py +264 -0
  9. autocoder/common/file_checkpoint/conversation_checkpoint.py +182 -0
  10. autocoder/common/file_checkpoint/examples.py +217 -0
  11. autocoder/common/file_checkpoint/manager.py +611 -0
  12. autocoder/common/file_checkpoint/models.py +156 -0
  13. autocoder/common/file_checkpoint/store.py +383 -0
  14. autocoder/common/file_checkpoint/test_backup.py +242 -0
  15. autocoder/common/file_checkpoint/test_manager.py +570 -0
  16. autocoder/common/file_checkpoint/test_models.py +360 -0
  17. autocoder/common/file_checkpoint/test_store.py +327 -0
  18. autocoder/common/file_checkpoint/test_utils.py +297 -0
  19. autocoder/common/file_checkpoint/utils.py +119 -0
  20. autocoder/common/rulefiles/autocoderrules_utils.py +114 -55
  21. autocoder/common/save_formatted_log.py +76 -5
  22. autocoder/common/utils_code_auto_generate.py +2 -1
  23. autocoder/common/v2/agent/agentic_edit.py +545 -225
  24. autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +83 -43
  25. autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +116 -29
  26. autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +179 -48
  27. autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +101 -56
  28. autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +322 -0
  29. autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +173 -132
  30. autocoder/common/v2/agent/agentic_edit_types.py +4 -0
  31. autocoder/compilers/normal_compiler.py +64 -0
  32. autocoder/events/event_manager_singleton.py +133 -4
  33. autocoder/linters/normal_linter.py +373 -0
  34. autocoder/linters/python_linter.py +4 -2
  35. autocoder/version.py +1 -1
  36. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/LICENSE +0 -0
  37. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/WHEEL +0 -0
  38. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/entry_points.txt +0 -0
  39. {auto_coder-0.1.363.dist-info → auto_coder-0.1.365.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  import os
2
- from typing import Dict, Any, Optional
2
+ from typing import Dict, Any, Optional, List, Set, Union
3
3
  from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
4
4
  from autocoder.common.v2.agent.agentic_edit_types import ListFilesTool, ToolResult # Import ToolResult from types
5
5
  from loguru import logger
@@ -18,13 +18,40 @@ class ListFilesToolResolver(BaseToolResolver):
18
18
  self.tool: ListFilesTool = tool # For type hinting
19
19
  self.shadow_manager = self.agent.shadow_manager
20
20
 
21
- def resolve(self) -> ToolResult:
22
- list_path_str = self.tool.path
23
- recursive = self.tool.recursive or False
24
- source_dir = self.args.source_dir or "."
25
- absolute_source_dir = os.path.abspath(source_dir)
26
- absolute_list_path = os.path.abspath(os.path.join(source_dir, list_path_str))
27
-
21
+ def list_files_in_dir(self, base_dir: str, recursive: bool, source_dir: str, is_outside_source: bool) -> Set[str]:
22
+ """Helper function to list files in a directory"""
23
+ result = set()
24
+ try:
25
+ if recursive:
26
+ for root, dirs, files in os.walk(base_dir):
27
+ # Modify dirs in-place to skip ignored dirs early
28
+ dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]
29
+ for name in files:
30
+ full_path = os.path.join(root, name)
31
+ if should_ignore(full_path):
32
+ continue
33
+ display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
34
+ result.add(display_path)
35
+ for d in dirs:
36
+ full_path = os.path.join(root, d)
37
+ display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
38
+ result.add(display_path + "/")
39
+ else:
40
+ for item in os.listdir(base_dir):
41
+ full_path = os.path.join(base_dir, item)
42
+ if should_ignore(full_path):
43
+ continue
44
+ display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
45
+ if os.path.isdir(full_path):
46
+ result.add(display_path + "/")
47
+ else:
48
+ result.add(display_path)
49
+ except Exception as e:
50
+ logger.warning(f"Error listing files in {base_dir}: {e}")
51
+ return result
52
+
53
+ def list_files_with_shadow(self, list_path_str: str, recursive: bool, source_dir: str, absolute_source_dir: str, absolute_list_path: str) -> Union[ToolResult, List[str]]:
54
+ """List files using shadow manager for path translation"""
28
55
  # Security check: Allow listing outside source_dir IF the original path is outside?
29
56
  is_outside_source = not absolute_list_path.startswith(absolute_source_dir)
30
57
  if is_outside_source:
@@ -49,46 +76,14 @@ class ListFilesToolResolver(BaseToolResolver):
49
76
  if shadow_exists and not os.path.isdir(shadow_dir_path):
50
77
  return ToolResult(success=False, message=f"Error: Shadow path is not a directory: {shadow_dir_path}")
51
78
 
52
- # Helper function to list files in a directory
53
- def list_files_in_dir(base_dir: str) -> set:
54
- result = set()
55
- try:
56
- if recursive:
57
- for root, dirs, files in os.walk(base_dir):
58
- # Modify dirs in-place to skip ignored dirs early
59
- dirs[:] = [d for d in dirs if not should_ignore(os.path.join(root, d))]
60
- for name in files:
61
- full_path = os.path.join(root, name)
62
- if should_ignore(full_path):
63
- continue
64
- display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
65
- result.add(display_path)
66
- for d in dirs:
67
- full_path = os.path.join(root, d)
68
- display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
69
- result.add(display_path + "/")
70
- else:
71
- for item in os.listdir(base_dir):
72
- full_path = os.path.join(base_dir, item)
73
- if should_ignore(full_path):
74
- continue
75
- display_path = os.path.relpath(full_path, source_dir) if not is_outside_source else full_path
76
- if os.path.isdir(full_path):
77
- result.add(display_path + "/")
78
- else:
79
- result.add(display_path)
80
- except Exception as e:
81
- logger.warning(f"Error listing files in {base_dir}: {e}")
82
- return result
83
-
84
79
  # Collect files from shadow and/or source directory
85
80
  shadow_files_set = set()
86
81
  if shadow_exists:
87
- shadow_files_set = list_files_in_dir(shadow_dir_path)
82
+ shadow_files_set = self.list_files_in_dir(shadow_dir_path, recursive, source_dir, is_outside_source)
88
83
 
89
84
  source_files_set = set()
90
85
  if os.path.exists(absolute_list_path) and os.path.isdir(absolute_list_path):
91
- source_files_set = list_files_in_dir(absolute_list_path)
86
+ source_files_set = self.list_files_in_dir(absolute_list_path, recursive, source_dir, is_outside_source)
92
87
 
93
88
  # Merge results, prioritizing shadow files if exist
94
89
  if shadow_exists:
@@ -101,7 +96,52 @@ class ListFilesToolResolver(BaseToolResolver):
101
96
  try:
102
97
  message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(merged_files)} items."
103
98
  logger.info(message)
104
- return ToolResult(success=True, message=message, content=sorted(merged_files))
99
+ return sorted(merged_files)
100
+ except Exception as e:
101
+ logger.error(f"Error listing files in '{list_path_str}': {str(e)}")
102
+ return ToolResult(success=False, message=f"An unexpected error occurred while listing files: {str(e)}")
103
+
104
+ def list_files_normal(self, list_path_str: str, recursive: bool, source_dir: str, absolute_source_dir: str, absolute_list_path: str) -> Union[ToolResult, List[str]]:
105
+ """List files directly without using shadow manager"""
106
+ # Security check: Allow listing outside source_dir IF the original path is outside?
107
+ is_outside_source = not absolute_list_path.startswith(absolute_source_dir)
108
+ if is_outside_source:
109
+ logger.warning(f"Listing path is outside the project source directory: {list_path_str}")
110
+
111
+ # Validate that the directory exists
112
+ if not os.path.exists(absolute_list_path):
113
+ return ToolResult(success=False, message=f"Error: Path not found: {list_path_str}")
114
+ if not os.path.isdir(absolute_list_path):
115
+ return ToolResult(success=False, message=f"Error: Path is not a directory: {list_path_str}")
116
+
117
+ # Collect files from the directory
118
+ files_set = self.list_files_in_dir(absolute_list_path, recursive, source_dir, is_outside_source)
119
+
120
+ try:
121
+ message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(files_set)} items."
122
+ logger.info(message)
123
+ return sorted(files_set)
105
124
  except Exception as e:
106
125
  logger.error(f"Error listing files in '{list_path_str}': {str(e)}")
107
126
  return ToolResult(success=False, message=f"An unexpected error occurred while listing files: {str(e)}")
127
+
128
+ def resolve(self) -> ToolResult:
129
+ """Resolve the list files tool by calling the appropriate implementation"""
130
+ list_path_str = self.tool.path
131
+ recursive = self.tool.recursive or False
132
+ source_dir = self.args.source_dir or "."
133
+ absolute_source_dir = os.path.abspath(source_dir)
134
+ absolute_list_path = os.path.abspath(os.path.join(source_dir, list_path_str))
135
+
136
+ # Choose the appropriate implementation based on whether shadow_manager is available
137
+ if self.shadow_manager:
138
+ result = self.list_files_with_shadow(list_path_str, recursive, source_dir, absolute_source_dir, absolute_list_path)
139
+ else:
140
+ result = self.list_files_normal(list_path_str, recursive, source_dir, absolute_source_dir, absolute_list_path)
141
+
142
+ # Handle the case where the implementation returns a sorted list instead of a ToolResult
143
+ if isinstance(result, list):
144
+ message = f"Successfully listed contents of '{list_path_str}' (Recursive: {recursive}). Found {len(result)} items."
145
+ return ToolResult(success=True, message=message, content=result)
146
+ else:
147
+ return result
@@ -1,10 +1,18 @@
1
1
  import os
2
2
  from typing import Dict, Any, Optional
3
- from autocoder.common import AutoCoderArgs
3
+ from autocoder.common import AutoCoderArgs,SourceCode
4
4
  from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
5
- from autocoder.common.v2.agent.agentic_edit_types import ReadFileTool, ToolResult # Import ToolResult from types
5
+ from autocoder.common.v2.agent.agentic_edit_types import ReadFileTool, ToolResult
6
+ from autocoder.common.context_pruner import PruneContext
7
+ from autocoder.common import SourceCode
8
+ from autocoder.rag.token_counter import count_tokens
6
9
  from loguru import logger
7
10
  import typing
11
+ from autocoder.rag.loaders import (
12
+ extract_text_from_pdf,
13
+ extract_text_from_docx,
14
+ extract_text_from_ppt
15
+ )
8
16
 
9
17
  if typing.TYPE_CHECKING:
10
18
  from autocoder.common.v2.agent.agentic_edit import AgenticEdit
@@ -15,40 +23,119 @@ class ReadFileToolResolver(BaseToolResolver):
15
23
  super().__init__(agent, tool, args)
16
24
  self.tool: ReadFileTool = tool # For type hinting
17
25
  self.shadow_manager = self.agent.shadow_manager if self.agent else None
26
+ self.context_pruner = PruneContext(
27
+ max_tokens=self.args.context_prune_safe_zone_tokens,
28
+ args=self.args,
29
+ llm=self.agent.context_prune_llm
30
+ )
18
31
 
19
- def resolve(self) -> ToolResult:
20
- file_path = self.tool.path
21
- source_dir = self.args.source_dir or "."
22
- abs_project_dir = os.path.abspath(source_dir)
23
- abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
32
+ def _prune_file_content(self, content: str, file_path: str) -> str:
33
+ """对文件内容进行剪枝处理"""
34
+ if not self.context_pruner:
35
+ return content
36
+
37
+ # 计算 token 数量
38
+ tokens = count_tokens(content)
39
+ if tokens <= self.args.context_prune_safe_zone_tokens:
40
+ return content
41
+
42
+ # 创建 SourceCode 对象
43
+ source_code = SourceCode(
44
+ module_name=file_path,
45
+ source_code=content,
46
+ tokens=tokens
47
+ )
48
+
49
+ # 使用 context_pruner 进行剪枝
50
+ pruned_sources = self.context_pruner.handle_overflow(
51
+ file_sources=[source_code],
52
+ conversations=self.agent.current_conversations if self.agent else [],
53
+ strategy=self.args.context_prune_strategy
54
+ )
24
55
 
25
- # # Security check: ensure the path is within the source directory
26
- # if not abs_file_path.startswith(abs_project_dir):
27
- # return ToolResult(success=False, message=f"Error: Access denied. Attempted to read file outside the project directory: {file_path}")
56
+ if not pruned_sources:
57
+ return content
28
58
 
59
+ return pruned_sources[0].source_code
60
+
61
+ def _read_file_content(self, file_path_to_read: str) -> str:
62
+ content = ""
63
+ ext = os.path.splitext(file_path_to_read)[1].lower()
64
+
65
+ if ext == '.pdf':
66
+ logger.info(f"Extracting text from PDF: {file_path_to_read}")
67
+ content = extract_text_from_pdf(file_path_to_read)
68
+ elif ext == '.docx':
69
+ logger.info(f"Extracting text from DOCX: {file_path_to_read}")
70
+ content = extract_text_from_docx(file_path_to_read)
71
+ elif ext in ('.pptx', '.ppt'):
72
+ logger.info(f"Extracting text from PPT/PPTX: {file_path_to_read}")
73
+ slide_texts = []
74
+ for slide_identifier, slide_text_content in extract_text_from_ppt(file_path_to_read):
75
+ slide_texts.append(f"--- Slide {slide_identifier} ---\n{slide_text_content}")
76
+ content = "\n\n".join(slide_texts) if slide_texts else ""
77
+ else:
78
+ logger.info(f"Reading plain text file: {file_path_to_read}")
79
+ with open(file_path_to_read, 'r', encoding='utf-8', errors='replace') as f:
80
+ content = f.read()
81
+
82
+ # 对内容进行剪枝处理
83
+ return self._prune_file_content(content, file_path_to_read)
84
+
85
+ def read_file_with_shadow(self, file_path: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
86
+ """Read file using shadow manager for path translation"""
29
87
  try:
30
- try:
31
- if self.shadow_manager:
32
- shadow_path = self.shadow_manager.to_shadow_path(abs_file_path)
33
- # If shadow file exists, read from it
34
- if os.path.exists(shadow_path) and os.path.isfile(shadow_path):
35
- with open(shadow_path, 'r', encoding='utf-8', errors='replace') as f:
36
- content = f.read()
37
- logger.info(f"[Shadow] Successfully read shadow file: {shadow_path}")
38
- return ToolResult(success=True, message=f"Successfully read file (shadow): {file_path}", content=content)
39
- except Exception as e:
40
- pass
41
- # else fallback to original file
42
- # Fallback to original file
88
+ # Check if shadow file exists
89
+ shadow_path = self.shadow_manager.to_shadow_path(abs_file_path)
90
+ if os.path.exists(shadow_path) and os.path.isfile(shadow_path):
91
+ try:
92
+ content = self._read_file_content(shadow_path)
93
+ logger.info(f"[Shadow] Successfully processed shadow file: {shadow_path}")
94
+ return ToolResult(success=True, message=f"{file_path}", content=content)
95
+ except Exception as e_shadow:
96
+ logger.warning(f"Failed to read shadow file {shadow_path} with _read_file_content: {str(e_shadow)}. Falling back to original file.")
97
+
98
+ # If shadow file doesn't exist or reading failed, try original file
43
99
  if not os.path.exists(abs_file_path):
44
100
  return ToolResult(success=False, message=f"Error: File not found at path: {file_path}")
45
101
  if not os.path.isfile(abs_file_path):
46
102
  return ToolResult(success=False, message=f"Error: Path is not a file: {file_path}")
47
103
 
48
- with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
49
- content = f.read()
50
- logger.info(f"Successfully read file: {file_path}")
51
- return ToolResult(success=True, message=f"Successfully read file: {file_path}", content=content)
104
+ content = self._read_file_content(abs_file_path)
105
+ logger.info(f"Successfully processed file: {file_path}")
106
+ return ToolResult(success=True, message=f"{file_path}", content=content)
107
+
52
108
  except Exception as e:
53
- logger.error(f"Error reading file '{file_path}': {str(e)}")
54
- return ToolResult(success=False, message=f"An error occurred while reading the file: {str(e)}")
109
+ logger.warning(f"Error processing file '{file_path}': {str(e)}")
110
+ logger.exception(e)
111
+ return ToolResult(success=False, message=f"An error occurred while processing the file '{file_path}': {str(e)}")
112
+
113
+ def read_file_normal(self, file_path: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
114
+ """Read file directly without using shadow manager"""
115
+ try:
116
+ if not os.path.exists(abs_file_path):
117
+ return ToolResult(success=False, message=f"Error: File not found at path: {file_path}")
118
+ if not os.path.isfile(abs_file_path):
119
+ return ToolResult(success=False, message=f"Error: Path is not a file: {file_path}")
120
+
121
+ content = self._read_file_content(abs_file_path)
122
+ logger.info(f"Successfully processed file: {file_path}")
123
+ return ToolResult(success=True, message=f"{file_path}", content=content)
124
+
125
+ except Exception as e:
126
+ logger.warning(f"Error processing file '{file_path}': {str(e)}")
127
+ logger.exception(e)
128
+ return ToolResult(success=False, message=f"An error occurred while processing the file '{file_path}': {str(e)}")
129
+
130
+ def resolve(self) -> ToolResult:
131
+ """Resolve the read file tool by calling the appropriate implementation"""
132
+ file_path = self.tool.path
133
+ source_dir = self.args.source_dir or "."
134
+ abs_project_dir = os.path.abspath(source_dir)
135
+ abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
136
+
137
+ # Choose the appropriate implementation based on whether shadow_manager is available
138
+ if self.shadow_manager:
139
+ return self.read_file_with_shadow(file_path, source_dir, abs_project_dir, abs_file_path)
140
+ else:
141
+ return self.read_file_normal(file_path, source_dir, abs_project_dir, abs_file_path)
@@ -4,7 +4,9 @@ from typing import Dict, Any, Optional, List, Tuple
4
4
  import typing
5
5
  from autocoder.common import AutoCoderArgs
6
6
  from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
7
- from autocoder.common.v2.agent.agentic_edit_types import ReplaceInFileTool, ToolResult # Import ToolResult from types
7
+ from autocoder.common.v2.agent.agentic_edit_types import ReplaceInFileTool, ToolResult
8
+ from autocoder.common.file_checkpoint.models import FileChange as CheckpointFileChange
9
+ from autocoder.common.file_checkpoint.manager import FileChangeManager as CheckpointFileChangeManager
8
10
  from loguru import logger
9
11
  from autocoder.common.auto_coder_lang import get_message_with_format
10
12
  if typing.TYPE_CHECKING:
@@ -84,30 +86,18 @@ class ReplaceInFileToolResolver(BaseToolResolver):
84
86
 
85
87
  return "\n".join(formatted_issues)
86
88
 
87
- def resolve(self) -> ToolResult:
88
- file_path = self.tool.path
89
- diff_content = self.tool.diff
90
- source_dir = self.args.source_dir or "."
91
- abs_project_dir = os.path.abspath(source_dir)
92
- abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
93
-
94
- # Security check
95
- if not abs_file_path.startswith(abs_project_dir):
96
- return ToolResult(success=False, message=get_message_with_format("replace_in_file.access_denied", file_path=file_path))
97
-
98
- # Determine target path: shadow file if shadow_manager exists
99
- target_path = abs_file_path
100
- if self.shadow_manager:
89
+ def replace_in_file_with_shadow(self, file_path: str, diff_content: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
90
+ """Replace content in file using shadow manager for path translation"""
91
+ try:
92
+ # Determine target path: shadow file
101
93
  target_path = self.shadow_manager.to_shadow_path(abs_file_path)
102
94
 
103
- # If shadow file does not exist yet, but original file exists, copy original content into shadow first? No, just treat as normal file.
104
- # For now, read from shadow if exists, else fallback to original file
105
- try:
95
+ # Read original content
106
96
  if os.path.exists(target_path) and os.path.isfile(target_path):
107
97
  with open(target_path, 'r', encoding='utf-8', errors='replace') as f:
108
98
  original_content = f.read()
109
- elif self.shadow_manager and os.path.exists(abs_file_path) and os.path.isfile(abs_file_path):
110
- # If shadow doesn't exist, but original exists, read original content (create shadow implicitly later)
99
+ elif os.path.exists(abs_file_path) and os.path.isfile(abs_file_path):
100
+ # If shadow doesn't exist, but original exists, read original content
111
101
  with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
112
102
  original_content = f.read()
113
103
  # create parent dirs of shadow if needed
@@ -118,43 +108,40 @@ class ReplaceInFileToolResolver(BaseToolResolver):
118
108
  logger.info(f"[Shadow] Initialized shadow file from original: {target_path}")
119
109
  else:
120
110
  return ToolResult(success=False, message=get_message_with_format("replace_in_file.file_not_found", file_path=file_path))
121
- except Exception as e:
122
- logger.error(f"Error reading file for replace '{file_path}': {str(e)}")
123
- return ToolResult(success=False, message=get_message_with_format("replace_in_file.read_error", error=str(e)))
124
111
 
125
- parsed_blocks = self.parse_diff(diff_content)
126
- if not parsed_blocks:
127
- return ToolResult(success=False, message=get_message_with_format("replace_in_file.no_valid_blocks"))
112
+ parsed_blocks = self.parse_diff(diff_content)
113
+ if not parsed_blocks:
114
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.no_valid_blocks"))
128
115
 
129
- current_content = original_content
130
- applied_count = 0
131
- errors = []
116
+ current_content = original_content
117
+ applied_count = 0
118
+ errors = []
132
119
 
133
- # Apply blocks sequentially
134
- for i, (search_block, replace_block) in enumerate(parsed_blocks):
135
- start_index = current_content.find(search_block)
120
+ # Apply blocks sequentially
121
+ for i, (search_block, replace_block) in enumerate(parsed_blocks):
122
+ start_index = current_content.find(search_block)
136
123
 
137
- if start_index != -1:
138
- current_content = current_content[:start_index] + replace_block + current_content[start_index + len(search_block):]
139
- applied_count += 1
140
- logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
141
- else:
142
- error_message = f"SEARCH block {i+1} not found in the current file content. Content to search:\n---\n{search_block}\n---"
143
- logger.warning(error_message)
144
- context_start = max(0, original_content.find(search_block[:20]) - 100)
145
- context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
146
- logger.warning(f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---")
147
- errors.append(error_message)
148
- # continue applying remaining blocks
124
+ if start_index != -1:
125
+ current_content = current_content[:start_index] + replace_block + current_content[start_index + len(search_block):]
126
+ applied_count += 1
127
+ logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
128
+ else:
129
+ error_message = f"SEARCH block {i+1} not found in the current file content. Content to search:\n---\n{search_block}\n---"
130
+ logger.warning(error_message)
131
+ context_start = max(0, original_content.find(search_block[:20]) - 100)
132
+ context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
133
+ logger.warning(f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---")
134
+ errors.append(error_message)
149
135
 
150
- if applied_count == 0 and errors:
151
- return ToolResult(success=False, message=get_message_with_format("replace_in_file.apply_failed", errors="\n".join(errors)))
136
+ if applied_count == 0 and errors:
137
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.apply_failed", errors="\n".join(errors)))
152
138
 
153
- try:
139
+ # Write the modified content back to shadow file
154
140
  os.makedirs(os.path.dirname(target_path), exist_ok=True)
155
141
  with open(target_path, 'w', encoding='utf-8') as f:
156
142
  f.write(current_content)
157
- logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
143
+
144
+ logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to shadow file: {file_path}")
158
145
 
159
146
  # 新增:执行代码质量检查
160
147
  lint_results = None
@@ -212,6 +199,132 @@ class ReplaceInFileToolResolver(BaseToolResolver):
212
199
  "content": current_content,
213
200
  }
214
201
 
202
+ # 只有在启用Lint时才添加Lint结果
203
+ if enable_lint:
204
+ result_content["lint_results"] = {
205
+ "has_issues": has_lint_issues,
206
+ "issues": formatted_issues if has_lint_issues else None
207
+ }
208
+
209
+ return ToolResult(success=True, message=message, content=result_content)
210
+ except Exception as e:
211
+ logger.error(f"Error writing replaced content to shadow file '{file_path}': {str(e)}")
212
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.write_error", error=str(e)))
213
+
214
+ def replace_in_file_normal(self, file_path: str, diff_content: str, source_dir: str, abs_project_dir: str, abs_file_path: str) -> ToolResult:
215
+ """Replace content in file directly without using shadow manager"""
216
+ try:
217
+ # Read original content
218
+ if not os.path.exists(abs_file_path):
219
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.file_not_found", file_path=file_path))
220
+ if not os.path.isfile(abs_file_path):
221
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.not_a_file", file_path=file_path))
222
+
223
+ with open(abs_file_path, 'r', encoding='utf-8', errors='replace') as f:
224
+ original_content = f.read()
225
+
226
+ parsed_blocks = self.parse_diff(diff_content)
227
+ if not parsed_blocks:
228
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.no_valid_blocks"))
229
+
230
+ current_content = original_content
231
+ applied_count = 0
232
+ errors = []
233
+
234
+ # Apply blocks sequentially
235
+ for i, (search_block, replace_block) in enumerate(parsed_blocks):
236
+ start_index = current_content.find(search_block)
237
+
238
+ if start_index != -1:
239
+ current_content = current_content[:start_index] + replace_block + current_content[start_index + len(search_block):]
240
+ applied_count += 1
241
+ logger.info(f"Applied SEARCH/REPLACE block {i+1} in file {file_path}")
242
+ else:
243
+ error_message = f"SEARCH block {i+1} not found in the current file content. Content to search:\n---\n{search_block}\n---"
244
+ logger.warning(error_message)
245
+ context_start = max(0, original_content.find(search_block[:20]) - 100)
246
+ context_end = min(len(original_content), context_start + 200 + len(search_block[:20]))
247
+ logger.warning(f"Approximate context in file:\n---\n{original_content[context_start:context_end]}\n---")
248
+ errors.append(error_message)
249
+
250
+ if applied_count == 0 and errors:
251
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.apply_failed", errors="\n".join(errors)))
252
+
253
+ # Write the modified content back to file
254
+ if self.agent and self.agent.checkpoint_manager:
255
+ changes = {
256
+ file_path: CheckpointFileChange(
257
+ file_path=file_path,
258
+ content=current_content,
259
+ is_deletion=False,
260
+ is_new=True
261
+ )
262
+ }
263
+ change_group_id = self.args.event_file
264
+
265
+ self.agent.checkpoint_manager.apply_changes_with_conversation(
266
+ changes=changes,
267
+ conversations=self.agent.current_conversations,
268
+ change_group_id=change_group_id,
269
+ metadata={"event_file": self.args.event_file}
270
+ )
271
+ else:
272
+ with open(abs_file_path, 'w', encoding='utf-8') as f:
273
+ f.write(current_content)
274
+
275
+ logger.info(f"Successfully applied {applied_count}/{len(parsed_blocks)} changes to file: {file_path}")
276
+
277
+ # 新增:执行代码质量检查
278
+ lint_results = None
279
+ lint_message = ""
280
+ formatted_issues = ""
281
+ has_lint_issues = False
282
+
283
+ # 检查是否启用了Lint功能
284
+ enable_lint = self.args.enable_auto_fix_lint
285
+
286
+ if enable_lint:
287
+ try:
288
+ if self.agent.linter:
289
+ lint_results = self.agent.linter.lint_file(file_path)
290
+ if lint_results and lint_results.issues:
291
+ has_lint_issues = True
292
+ # 格式化 lint 问题
293
+ formatted_issues = self._format_lint_issues(lint_results)
294
+ lint_message = f"\n\n代码质量检查发现 {len(lint_results.issues)} 个问题:\n{formatted_issues}"
295
+ except Exception as e:
296
+ logger.error(f"Lint 检查失败: {str(e)}")
297
+ lint_message = "\n\n尝试进行代码质量检查时出错。"
298
+ else:
299
+ logger.info("代码质量检查已禁用")
300
+
301
+ # 构建包含 lint 结果的返回消息
302
+ if errors:
303
+ message = get_message_with_format("replace_in_file.apply_success_with_warnings",
304
+ applied=applied_count,
305
+ total=len(parsed_blocks),
306
+ file_path=file_path,
307
+ errors="\n".join(errors))
308
+ else:
309
+ message = get_message_with_format("replace_in_file.apply_success",
310
+ applied=applied_count,
311
+ total=len(parsed_blocks),
312
+ file_path=file_path)
313
+
314
+ # 将 lint 消息添加到结果中,如果启用了Lint
315
+ if enable_lint:
316
+ message += lint_message
317
+
318
+ # 变更跟踪,回调AgenticEdit
319
+ if self.agent:
320
+ rel_path = os.path.relpath(abs_file_path, abs_project_dir)
321
+ self.agent.record_file_change(rel_path, "modified", diff=diff_content, content=current_content)
322
+
323
+ # 附加 lint 结果到返回内容
324
+ result_content = {
325
+ "content": current_content,
326
+ }
327
+
215
328
  # 只有在启用Lint时才添加Lint结果
216
329
  if enable_lint:
217
330
  result_content["lint_results"] = {
@@ -223,3 +336,21 @@ class ReplaceInFileToolResolver(BaseToolResolver):
223
336
  except Exception as e:
224
337
  logger.error(f"Error writing replaced content to file '{file_path}': {str(e)}")
225
338
  return ToolResult(success=False, message=get_message_with_format("replace_in_file.write_error", error=str(e)))
339
+
340
+ def resolve(self) -> ToolResult:
341
+ """Resolve the replace in file tool by calling the appropriate implementation"""
342
+ file_path = self.tool.path
343
+ diff_content = self.tool.diff
344
+ source_dir = self.args.source_dir or "."
345
+ abs_project_dir = os.path.abspath(source_dir)
346
+ abs_file_path = os.path.abspath(os.path.join(source_dir, file_path))
347
+
348
+ # Security check
349
+ if not abs_file_path.startswith(abs_project_dir):
350
+ return ToolResult(success=False, message=get_message_with_format("replace_in_file.access_denied", file_path=file_path))
351
+
352
+ # Choose the appropriate implementation based on whether shadow_manager is available
353
+ if self.shadow_manager:
354
+ return self.replace_in_file_with_shadow(file_path, diff_content, source_dir, abs_project_dir, abs_file_path)
355
+ else:
356
+ return self.replace_in_file_normal(file_path, diff_content, source_dir, abs_project_dir, abs_file_path)