auto-coder 0.1.399__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 (71) hide show
  1. {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/METADATA +1 -1
  2. {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/RECORD +71 -35
  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 +121 -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/ac_style_command_parser/parser.py +27 -12
  10. autocoder/common/auto_coder_lang.py +78 -0
  11. autocoder/common/command_completer_v2.py +1 -1
  12. autocoder/common/file_monitor/test_file_monitor.py +307 -0
  13. autocoder/common/git_utils.py +7 -2
  14. autocoder/common/pruner/__init__.py +0 -0
  15. autocoder/common/pruner/agentic_conversation_pruner.py +197 -0
  16. autocoder/common/pruner/context_pruner.py +574 -0
  17. autocoder/common/pruner/conversation_pruner.py +132 -0
  18. autocoder/common/pruner/test_agentic_conversation_pruner.py +342 -0
  19. autocoder/common/pruner/test_context_pruner.py +546 -0
  20. autocoder/common/pull_requests/__init__.py +256 -0
  21. autocoder/common/pull_requests/base_provider.py +191 -0
  22. autocoder/common/pull_requests/config.py +66 -0
  23. autocoder/common/pull_requests/example.py +1 -0
  24. autocoder/common/pull_requests/exceptions.py +46 -0
  25. autocoder/common/pull_requests/manager.py +201 -0
  26. autocoder/common/pull_requests/models.py +164 -0
  27. autocoder/common/pull_requests/providers/__init__.py +23 -0
  28. autocoder/common/pull_requests/providers/gitcode_provider.py +19 -0
  29. autocoder/common/pull_requests/providers/gitee_provider.py +20 -0
  30. autocoder/common/pull_requests/providers/github_provider.py +214 -0
  31. autocoder/common/pull_requests/providers/gitlab_provider.py +29 -0
  32. autocoder/common/pull_requests/test_module.py +1 -0
  33. autocoder/common/pull_requests/utils.py +344 -0
  34. autocoder/common/tokens/__init__.py +77 -0
  35. autocoder/common/tokens/counter.py +231 -0
  36. autocoder/common/tokens/file_detector.py +105 -0
  37. autocoder/common/tokens/filters.py +111 -0
  38. autocoder/common/tokens/models.py +28 -0
  39. autocoder/common/v2/agent/agentic_edit.py +538 -590
  40. autocoder/common/v2/agent/agentic_edit_tools/__init__.py +8 -1
  41. autocoder/common/v2/agent/agentic_edit_tools/ac_mod_read_tool_resolver.py +40 -0
  42. autocoder/common/v2/agent/agentic_edit_tools/ac_mod_write_tool_resolver.py +43 -0
  43. autocoder/common/v2/agent/agentic_edit_tools/ask_followup_question_tool_resolver.py +8 -0
  44. autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +1 -1
  45. autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +1 -1
  46. autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +33 -88
  47. autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +8 -8
  48. autocoder/common/v2/agent/agentic_edit_tools/todo_read_tool_resolver.py +118 -0
  49. autocoder/common/v2/agent/agentic_edit_tools/todo_write_tool_resolver.py +324 -0
  50. autocoder/common/v2/agent/agentic_edit_types.py +47 -4
  51. autocoder/common/v2/agent/runner/__init__.py +31 -0
  52. autocoder/common/v2/agent/runner/base_runner.py +106 -0
  53. autocoder/common/v2/agent/runner/event_runner.py +216 -0
  54. autocoder/common/v2/agent/runner/sdk_runner.py +40 -0
  55. autocoder/common/v2/agent/runner/terminal_runner.py +283 -0
  56. autocoder/common/v2/agent/runner/tool_display.py +191 -0
  57. autocoder/index/entry.py +1 -1
  58. autocoder/plugins/token_helper_plugin.py +107 -7
  59. autocoder/run_context.py +9 -0
  60. autocoder/sdk/__init__.py +114 -81
  61. autocoder/sdk/cli/handlers.py +2 -1
  62. autocoder/sdk/cli/main.py +9 -2
  63. autocoder/sdk/cli/options.py +4 -3
  64. autocoder/sdk/core/auto_coder_core.py +7 -152
  65. autocoder/sdk/core/bridge.py +5 -4
  66. autocoder/sdk/models/options.py +8 -6
  67. autocoder/version.py +1 -1
  68. {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/WHEEL +0 -0
  69. {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/entry_points.txt +0 -0
  70. {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/licenses/LICENSE +0 -0
  71. {auto_coder-0.1.399.dist-info → auto_coder-1.0.0.dist-info}/top_level.txt +0 -0
@@ -12,7 +12,10 @@ from .attempt_completion_tool_resolver import AttemptCompletionToolResolver
12
12
  from .plan_mode_respond_tool_resolver import PlanModeRespondToolResolver
13
13
  from .use_mcp_tool_resolver import UseMcpToolResolver
14
14
  from .use_rag_tool_resolver import UseRAGToolResolver
15
- from .list_package_info_tool_resolver import ListPackageInfoToolResolver
15
+ from .todo_read_tool_resolver import TodoReadToolResolver
16
+ from .todo_write_tool_resolver import TodoWriteToolResolver
17
+ from .ac_mod_read_tool_resolver import ACModReadToolResolver
18
+ from .ac_mod_write_tool_resolver import ACModWriteToolResolver
16
19
 
17
20
  __all__ = [
18
21
  "BaseToolResolver",
@@ -29,4 +32,8 @@ __all__ = [
29
32
  "UseMcpToolResolver",
30
33
  "UseRAGToolResolver",
31
34
  "ListPackageInfoToolResolver",
35
+ "TodoReadToolResolver",
36
+ "TodoWriteToolResolver",
37
+ "ACModReadToolResolver",
38
+ "ACModWriteToolResolver",
32
39
  ]
@@ -0,0 +1,40 @@
1
+
2
+ import os
3
+ from typing import Optional
4
+ from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
5
+ from autocoder.common.v2.agent.agentic_edit_types import ACModReadTool, ToolResult
6
+ from loguru import logger
7
+ import typing
8
+
9
+ if typing.TYPE_CHECKING:
10
+ from autocoder.common.v2.agent.agentic_edit import AgenticEdit
11
+
12
+ class ACModReadToolResolver(BaseToolResolver):
13
+ def __init__(self, agent: Optional['AgenticEdit'], tool: ACModReadTool, args):
14
+ super().__init__(agent, tool, args)
15
+ self.tool: ACModReadTool = tool
16
+
17
+ def resolve(self) -> ToolResult:
18
+ source_dir = self.args.source_dir or "."
19
+ input_path = self.tool.path.strip()
20
+ abs_input_path = os.path.abspath(os.path.join(source_dir, input_path)) if not os.path.isabs(input_path) else input_path
21
+
22
+ # # 校验输入目录是否在项目目录内
23
+ # if not abs_input_path.startswith(abs_source_dir):
24
+ # return ToolResult(success=False, message=f"Error: Access denied. Path outside project: {self.tool.path}")
25
+
26
+ # 直接在输入文件夹中查找 .ac.mod.md 文件
27
+ mod_file_path = os.path.join(abs_input_path, ".ac.mod.md")
28
+
29
+ logger.info(f"Looking for package info at: {mod_file_path}")
30
+
31
+ if not os.path.exists(mod_file_path):
32
+ return ToolResult(success=True, message=f"The path {self.tool.path} is NOT AC module.", content="")
33
+
34
+ try:
35
+ with open(mod_file_path, 'r', encoding='utf-8', errors='replace') as f:
36
+ content = f.read()
37
+ return ToolResult(success=True, message=f"The path {self.tool.path} is AC module.", content=content)
38
+ except Exception as e:
39
+ logger.error(f"Error reading package info file: {e}")
40
+ return ToolResult(success=False, message=f"Error reading {self.tool.path}/.ac.mod.md file: {e}")
@@ -0,0 +1,43 @@
1
+ import os
2
+ import re
3
+ from typing import Optional, List, Tuple
4
+ from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
5
+ from autocoder.common.v2.agent.agentic_edit_tools.replace_in_file_tool_resolver import ReplaceInFileToolResolver
6
+ from autocoder.common.v2.agent.agentic_edit_types import ACModWriteTool, ToolResult,ReplaceInFileTool
7
+ from loguru import logger
8
+ import typing
9
+
10
+ if typing.TYPE_CHECKING:
11
+ from autocoder.common.v2.agent.agentic_edit import AgenticEdit
12
+
13
+ class ACModWriteToolResolver(BaseToolResolver):
14
+ def __init__(self, agent: Optional['AgenticEdit'], tool: ACModWriteTool, args):
15
+ super().__init__(agent, tool, args)
16
+ self.tool: ACModWriteTool = tool
17
+
18
+ def resolve(self) -> ToolResult:
19
+ source_dir = self.args.source_dir or "."
20
+ input_path = self.tool.path.strip()
21
+
22
+ # Check if the path already contains .ac.mod.md file name
23
+ if input_path.endswith('.ac.mod.md'):
24
+ # Path already includes the filename
25
+ if not os.path.isabs(input_path):
26
+ mod_file_path = os.path.abspath(os.path.join(source_dir, input_path))
27
+ else:
28
+ mod_file_path = input_path
29
+
30
+ # Create the parent directory if it doesn't exist
31
+ parent_dir = os.path.dirname(mod_file_path)
32
+ os.makedirs(parent_dir, exist_ok=True)
33
+ else:
34
+ # Path is a directory, need to append .ac.mod.md
35
+ abs_input_path = os.path.abspath(os.path.join(source_dir, input_path)) if not os.path.isabs(input_path) else input_path
36
+
37
+ # Create the directory if it doesn't exist
38
+ os.makedirs(abs_input_path, exist_ok=True)
39
+
40
+ # Path to the .ac.mod.md file
41
+ mod_file_path = os.path.join(abs_input_path, ".ac.mod.md")
42
+
43
+ return ReplaceInFileToolResolver(self.agent, ReplaceInFileTool(path=mod_file_path, diff=self.tool.diff), self.args).resolve()
@@ -29,6 +29,14 @@ class AskFollowupQuestionToolResolver(BaseToolResolver):
29
29
  Packages the question and options to be handled by the main loop/UI.
30
30
  This resolver doesn't directly ask the user but prepares the data for it.
31
31
  """
32
+ # Check if running in CLI mode, if so return immediately with a message
33
+ # instructing the model to solve the problem on its own
34
+ if get_run_context().is_cli() or self.agent.args.enable_agentic_auto_approve:
35
+ return ToolResult(
36
+ success=False,
37
+ message="Remember, you cannot ask follow-up questions. Please try to solve the problem on your own using the available information. Do not give up and do your best to find a solution."
38
+ )
39
+
32
40
  question = self.tool.question
33
41
  options = self.tool.options or []
34
42
  options_text = "\n".join([f"{i+1}. {option}" for i, option in enumerate(options)])
@@ -9,7 +9,7 @@ from autocoder.common import shells
9
9
  from autocoder.common.printer import Printer
10
10
  from loguru import logger
11
11
  import typing
12
- from autocoder.common.context_pruner import PruneContext
12
+ from autocoder.common.pruner.context_pruner import PruneContext
13
13
  from autocoder.rag.token_counter import count_tokens
14
14
  from autocoder.common import SourceCode
15
15
  from autocoder.common import AutoCoderArgs
@@ -3,7 +3,7 @@ from typing import Dict, Any, Optional
3
3
  from autocoder.common import AutoCoderArgs,SourceCode
4
4
  from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
5
5
  from autocoder.common.v2.agent.agentic_edit_types import ReadFileTool, ToolResult
6
- from autocoder.common.context_pruner import PruneContext
6
+ from autocoder.common.pruner.context_pruner import PruneContext
7
7
  from autocoder.common import SourceCode
8
8
  from autocoder.rag.token_counter import count_tokens
9
9
  from loguru import logger
@@ -3,7 +3,8 @@ import re
3
3
  import glob
4
4
  from typing import Dict, Any, Optional, List, Union
5
5
  from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
6
- from autocoder.common.v2.agent.agentic_edit_types import SearchFilesTool, ToolResult # Import ToolResult from types
6
+ # Import ToolResult from types
7
+ from autocoder.common.v2.agent.agentic_edit_types import SearchFilesTool, ToolResult
7
8
  from loguru import logger
8
9
  from autocoder.common import AutoCoderArgs
9
10
  import typing
@@ -11,7 +12,7 @@ import typing
11
12
  from autocoder.common.ignorefiles.ignore_file_utils import should_ignore
12
13
 
13
14
  if typing.TYPE_CHECKING:
14
- from autocoder.common.v2.agent.agentic_edit import AgenticEdit
15
+ from autocoder.common.v2.agent.agentic_edit import AgenticEdit
15
16
 
16
17
 
17
18
  class SearchFilesToolResolver(BaseToolResolver):
@@ -24,12 +25,13 @@ class SearchFilesToolResolver(BaseToolResolver):
24
25
  """Helper function to search in a directory"""
25
26
  search_results = []
26
27
  search_glob_pattern = os.path.join(base_dir, "**", file_pattern)
27
-
28
- logger.info(f"Searching for regex '{regex_pattern}' in files matching '{file_pattern}' under '{base_dir}' (shadow: {is_shadow}) with ignore rules applied.")
29
-
28
+
29
+ logger.info(
30
+ f"Searching for regex '{regex_pattern}' in files matching '{file_pattern}' under '{base_dir}' (shadow: {is_shadow}) with ignore rules applied.")
31
+
30
32
  if compiled_regex is None:
31
33
  compiled_regex = re.compile(regex_pattern)
32
-
34
+
33
35
  for filepath in glob.glob(search_glob_pattern, recursive=True):
34
36
  abs_path = os.path.abspath(filepath)
35
37
  if should_ignore(abs_path):
@@ -43,17 +45,22 @@ class SearchFilesToolResolver(BaseToolResolver):
43
45
  if compiled_regex.search(line):
44
46
  context_start = max(0, i - 2)
45
47
  context_end = min(len(lines), i + 3)
46
- context = "".join([f"{j+1}: {lines[j]}" for j in range(context_start, context_end)])
47
-
48
+ context = "".join(
49
+ [f"{j+1}: {lines[j]}" for j in range(context_start, context_end)])
50
+
48
51
  if is_shadow and self.shadow_manager:
49
52
  try:
50
- abs_project_path = self.shadow_manager.from_shadow_path(filepath)
51
- relative_path = os.path.relpath(abs_project_path, source_dir)
53
+ abs_project_path = self.shadow_manager.from_shadow_path(
54
+ filepath)
55
+ relative_path = os.path.relpath(
56
+ abs_project_path, source_dir)
52
57
  except Exception:
53
- relative_path = os.path.relpath(filepath, source_dir)
58
+ relative_path = os.path.relpath(
59
+ filepath, source_dir)
54
60
  else:
55
- relative_path = os.path.relpath(filepath, source_dir)
56
-
61
+ relative_path = os.path.relpath(
62
+ filepath, source_dir)
63
+
57
64
  search_results.append({
58
65
  "path": relative_path,
59
66
  "line_number": i + 1,
@@ -61,73 +68,12 @@ class SearchFilesToolResolver(BaseToolResolver):
61
68
  "context": context.strip()
62
69
  })
63
70
  except Exception as e:
64
- logger.warning(f"Could not read or process file {filepath}: {e}")
71
+ logger.warning(
72
+ f"Could not read or process file {filepath}: {e}")
65
73
  continue
66
-
67
- return search_results
68
-
69
- def search_files_with_shadow(self, search_path_str: str, regex_pattern: str, file_pattern: str, source_dir: str, absolute_source_dir: str, absolute_search_path: str) -> Union[ToolResult, List[Dict[str, Any]]]:
70
- """Search files using shadow manager for path translation"""
71
- # Security check
72
- if not absolute_search_path.startswith(absolute_source_dir):
73
- return ToolResult(success=False, message=f"Error: Access denied. Attempted to search outside the project directory: {search_path_str}")
74
-
75
- # Check if shadow directory exists
76
- shadow_exists = False
77
- shadow_dir_path = None
78
- if self.shadow_manager:
79
- try:
80
- shadow_dir_path = self.shadow_manager.to_shadow_path(absolute_search_path)
81
- if os.path.exists(shadow_dir_path) and os.path.isdir(shadow_dir_path):
82
- shadow_exists = True
83
- except Exception as e:
84
- logger.warning(f"Error checking shadow path for {absolute_search_path}: {e}")
85
-
86
- # Validate that at least one of the directories exists
87
- if not os.path.exists(absolute_search_path) and not shadow_exists:
88
- return ToolResult(success=False, message=f"Error: Search path not found: {search_path_str}")
89
- if os.path.exists(absolute_search_path) and not os.path.isdir(absolute_search_path):
90
- return ToolResult(success=False, message=f"Error: Search path is not a directory: {search_path_str}")
91
- if shadow_exists and not os.path.isdir(shadow_dir_path):
92
- return ToolResult(success=False, message=f"Error: Shadow search path is not a directory: {shadow_dir_path}")
93
74
 
94
- try:
95
- compiled_regex = re.compile(regex_pattern)
96
-
97
- # Search in both directories and merge results
98
- shadow_results = []
99
- source_results = []
100
-
101
- if shadow_exists:
102
- shadow_results = self.search_in_dir(shadow_dir_path, regex_pattern, file_pattern, source_dir, is_shadow=True, compiled_regex=compiled_regex)
103
-
104
- if os.path.exists(absolute_search_path) and os.path.isdir(absolute_search_path):
105
- source_results = self.search_in_dir(absolute_search_path, regex_pattern, file_pattern, source_dir, is_shadow=False, compiled_regex=compiled_regex)
106
-
107
- # Merge results, prioritizing shadow results
108
- # Create a dictionary for quick lookup
109
- results_dict = {}
110
- for result in source_results:
111
- key = (result["path"], result["line_number"])
112
- results_dict[key] = result
113
-
114
- # Override with shadow results
115
- for result in shadow_results:
116
- key = (result["path"], result["line_number"])
117
- results_dict[key] = result
118
-
119
- # Convert back to list
120
- merged_results = list(results_dict.values())
121
-
122
- return merged_results
75
+ return search_results
123
76
 
124
- except re.error as e:
125
- logger.error(f"Invalid regex pattern '{regex_pattern}': {e}")
126
- return ToolResult(success=False, message=f"Invalid regex pattern: {e}")
127
- except Exception as e:
128
- logger.error(f"Error during file search: {str(e)}")
129
- return ToolResult(success=False, message=f"An unexpected error occurred during search: {str(e)}")
130
-
131
77
  def search_files_normal(self, search_path_str: str, regex_pattern: str, file_pattern: str, source_dir: str, absolute_source_dir: str, absolute_search_path: str) -> Union[ToolResult, List[Dict[str, Any]]]:
132
78
  """Search files directly without using shadow manager"""
133
79
  # Security check
@@ -142,10 +88,11 @@ class SearchFilesToolResolver(BaseToolResolver):
142
88
 
143
89
  try:
144
90
  compiled_regex = re.compile(regex_pattern)
145
-
91
+
146
92
  # Search in the directory
147
- search_results = self.search_in_dir(absolute_search_path, regex_pattern, file_pattern, source_dir, is_shadow=False, compiled_regex=compiled_regex)
148
-
93
+ search_results = self.search_in_dir(
94
+ absolute_search_path, regex_pattern, file_pattern, source_dir, is_shadow=False, compiled_regex=compiled_regex)
95
+
149
96
  return search_results
150
97
 
151
98
  except re.error as e:
@@ -162,21 +109,19 @@ class SearchFilesToolResolver(BaseToolResolver):
162
109
  file_pattern = self.tool.file_pattern or "*"
163
110
  source_dir = self.args.source_dir or "."
164
111
  absolute_source_dir = os.path.abspath(source_dir)
165
- absolute_search_path = os.path.abspath(os.path.join(source_dir, search_path_str))
112
+ absolute_search_path = os.path.abspath(
113
+ os.path.join(source_dir, search_path_str))
166
114
 
167
- # Choose the appropriate implementation based on whether shadow_manager is available
168
- if self.shadow_manager:
169
- result = self.search_files_with_shadow(search_path_str, regex_pattern, file_pattern, source_dir, absolute_source_dir, absolute_search_path)
170
- else:
171
- result = self.search_files_normal(search_path_str, regex_pattern, file_pattern, source_dir, absolute_source_dir, absolute_search_path)
115
+ result = self.search_files_normal(
116
+ search_path_str, regex_pattern, file_pattern, source_dir, absolute_source_dir, absolute_search_path)
172
117
 
173
118
  # Handle the case where the implementation returns a list instead of a ToolResult
174
119
  if isinstance(result, list):
175
- total_results = len(result)
120
+ total_results = len(result)
176
121
  # Limit results to 200 if needed
177
122
  if total_results > 200:
178
123
  truncated_results = result[:200]
179
- message = f"Search completed. Found {total_results} matches, showing only the first 200."
124
+ message = f"Search completed. Found {total_results} matches, showing only the first 200."
180
125
  logger.info(message)
181
126
  return ToolResult(success=True, message=message, content=truncated_results)
182
127
  else:
@@ -110,7 +110,7 @@ def test_create_new_file(test_args, temp_test_dir, mock_agent_no_shadow):
110
110
  result = resolver.resolve()
111
111
 
112
112
  assert result.success is True
113
- assert "成功写入文件" in result.message or "Successfully wrote file" in result.message
113
+ assert "Successfully wrote file" in result.message
114
114
 
115
115
  expected_file_abs_path = os.path.join(temp_test_dir, file_path)
116
116
  assert os.path.exists(expected_file_abs_path)
@@ -135,7 +135,7 @@ def test_overwrite_existing_file(test_args, temp_test_dir, mock_agent_no_shadow)
135
135
 
136
136
  assert result.success is True
137
137
  assert os.path.exists(abs_file_path)
138
- with open(abs_file_path, "r", encoding="utf-f8") as f:
138
+ with open(abs_file_path, "r", encoding="utf-8") as f:
139
139
  assert f.read() == new_content
140
140
  mock_agent_no_shadow.record_file_change.assert_called_once_with(file_path, "modified", content=new_content, diffs=None)
141
141
 
@@ -202,7 +202,7 @@ def test_path_outside_project_root_fails(test_args, temp_test_dir, mock_agent_no
202
202
  result = resolver.resolve()
203
203
 
204
204
  assert result.success is False
205
- assert "访问被拒绝" in result.message or "Access denied" in result.message
205
+ assert "Access denied" in result.message
206
206
  assert not os.path.exists(outside_abs_path)
207
207
 
208
208
  # shutil.rmtree(another_temp_dir) # Clean up the other temp dir if created by this test
@@ -226,8 +226,8 @@ def test_linting_not_called_if_disabled(test_args, temp_test_dir, mock_agent_no_
226
226
  if mock_agent_no_shadow and hasattr(mock_agent_no_shadow, 'shadow_linter') and mock_agent_no_shadow.shadow_linter:
227
227
  mock_agent_no_shadow.shadow_linter.lint_shadow_file.assert_not_called()
228
228
 
229
- # Check if "代码质量检查已禁用" or "Linting is disabled" is in message
230
- assert "代码质量检查已禁用" in result.message or "Linting is disabled" in result.message or "成功写入文件" in result.message
229
+ # Check if "Linting is disabled" or "Successfully wrote file" is in message
230
+ assert "Linting is disabled" in result.message or "Successfully wrote file" in result.message
231
231
 
232
232
 
233
233
  def test_linting_called_if_enabled(test_args, temp_test_dir, mock_agent_with_shadow):
@@ -251,7 +251,7 @@ def test_linting_called_if_enabled(test_args, temp_test_dir, mock_agent_with_sha
251
251
  # The actual path passed to lint_shadow_file will be the shadow path
252
252
  shadow_path = mock_agent_with_shadow.shadow_manager.to_shadow_path(os.path.join(temp_test_dir, file_path))
253
253
  mock_agent_with_shadow.shadow_linter.lint_shadow_file.assert_called_with(shadow_path)
254
- assert "代码质量检查通过" in result.message or "Linting passed" in result.message
254
+ assert "Linting passed" in result.message
255
255
 
256
256
 
257
257
  def test_create_file_with_shadow_manager(test_args, temp_test_dir, mock_agent_with_shadow):
@@ -311,12 +311,12 @@ def test_linting_error_message_propagation(test_args, temp_test_dir, mock_agent_
311
311
 
312
312
  # Temporarily patch _format_lint_issues within the resolver instance for this test
313
313
  # to ensure consistent output for assertion.
314
- formatted_issue_text = f"文件: {mock_agent_with_shadow.shadow_manager.to_shadow_path(os.path.join(temp_test_dir, file_path))}\n - [错误] 1行, 0列: SyntaxError: Missing parentheses in call to 'print' (规则: E999)\n"
314
+ formatted_issue_text = f"File: {mock_agent_with_shadow.shadow_manager.to_shadow_path(os.path.join(temp_test_dir, file_path))}\n - [ERROR] Line 1, Column 0: SyntaxError: Missing parentheses in call to 'print' (Rule: E999)\n"
315
315
  with patch.object(resolver, '_format_lint_issues', return_value=formatted_issue_text) as mock_format:
316
316
  result = resolver.resolve()
317
317
 
318
318
  assert result.success is True # Write itself is successful
319
319
  mock_format.assert_called_once_with(mock_lint_result)
320
- assert "代码质量检查发现 1 个问题" in result.message or "Linting found 1 issue(s)" in result.message
320
+ assert "Linting found 1 issue(s)" in result.message
321
321
  assert "SyntaxError: Missing parentheses in call to 'print'" in result.message
322
322
 
@@ -0,0 +1,118 @@
1
+ from typing import Dict, Any, Optional, List
2
+ from autocoder.common.v2.agent.agentic_edit_tools.base_tool_resolver import BaseToolResolver
3
+ from autocoder.common.v2.agent.agentic_edit_types import TodoReadTool, ToolResult
4
+ from loguru import logger
5
+ import typing
6
+ from autocoder.common import AutoCoderArgs
7
+ import os
8
+ import json
9
+ from datetime import datetime
10
+
11
+ if typing.TYPE_CHECKING:
12
+ from autocoder.common.v2.agent.agentic_edit import AgenticEdit
13
+
14
+
15
+ class TodoReadToolResolver(BaseToolResolver):
16
+ def __init__(self, agent: Optional['AgenticEdit'], tool: TodoReadTool, args: AutoCoderArgs):
17
+ super().__init__(agent, tool, args)
18
+ self.tool: TodoReadTool = tool # For type hinting
19
+
20
+ def _get_todo_file_path(self) -> str:
21
+ """Get the path to the todo file for this session."""
22
+ source_dir = self.args.source_dir or "."
23
+ todo_dir = os.path.join(source_dir, ".auto-coder", "todos")
24
+ os.makedirs(todo_dir, exist_ok=True)
25
+ return os.path.join(todo_dir, "current_session.json")
26
+
27
+ def _load_todos(self) -> List[Dict[str, Any]]:
28
+ """Load todos from the session file."""
29
+ todo_file = self._get_todo_file_path()
30
+ if not os.path.exists(todo_file):
31
+ return []
32
+
33
+ try:
34
+ with open(todo_file, 'r', encoding='utf-8') as f:
35
+ data = json.load(f)
36
+ return data.get('todos', [])
37
+ except Exception as e:
38
+ logger.warning(f"Failed to load todos: {e}")
39
+ return []
40
+
41
+ def _format_todo_display(self, todos: List[Dict[str, Any]]) -> str:
42
+ """Format todos for display."""
43
+ if not todos:
44
+ return "No todos found for this session."
45
+
46
+ output = []
47
+ output.append("=== Current Session Todo List ===\n")
48
+
49
+ # Group by status
50
+ pending = [t for t in todos if t.get('status') == 'pending']
51
+ in_progress = [t for t in todos if t.get('status') == 'in_progress']
52
+ completed = [t for t in todos if t.get('status') == 'completed']
53
+
54
+ if in_progress:
55
+ output.append("🔄 In Progress:")
56
+ for todo in in_progress:
57
+ priority_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(todo.get('priority', 'medium'), "⚪")
58
+ output.append(f" {priority_icon} [{todo['id']}] {todo['content']}")
59
+ if todo.get('notes'):
60
+ output.append(f" 📝 {todo['notes']}")
61
+ output.append("")
62
+
63
+ if pending:
64
+ output.append("⏳ Pending:")
65
+ for todo in pending:
66
+ priority_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(todo.get('priority', 'medium'), "⚪")
67
+ output.append(f" {priority_icon} [{todo['id']}] {todo['content']}")
68
+ if todo.get('notes'):
69
+ output.append(f" 📝 {todo['notes']}")
70
+ output.append("")
71
+
72
+ if completed:
73
+ output.append("✅ Completed:")
74
+ for todo in completed:
75
+ priority_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(todo.get('priority', 'medium'), "⚪")
76
+ output.append(f" {priority_icon} [{todo['id']}] {todo['content']}")
77
+ if todo.get('notes'):
78
+ output.append(f" 📝 {todo['notes']}")
79
+ output.append("")
80
+
81
+ # Add summary
82
+ total = len(todos)
83
+ pending_count = len(pending)
84
+ in_progress_count = len(in_progress)
85
+ completed_count = len(completed)
86
+
87
+ output.append(f"📊 Summary: Total {total} items | Pending {pending_count} | In Progress {in_progress_count} | Completed {completed_count}")
88
+
89
+ return "\n".join(output)
90
+
91
+ def resolve(self) -> ToolResult:
92
+ """
93
+ Read the current todo list and return it in a formatted display.
94
+ """
95
+ try:
96
+ logger.info("Reading current todo list")
97
+
98
+ # Load todos from file
99
+ todos = self._load_todos()
100
+
101
+ # Format for display
102
+ formatted_display = self._format_todo_display(todos)
103
+
104
+ logger.info(f"Found {len(todos)} todos in current session")
105
+
106
+ return ToolResult(
107
+ success=True,
108
+ message="Todo list retrieved successfully.",
109
+ content=formatted_display
110
+ )
111
+
112
+ except Exception as e:
113
+ logger.error(f"Error reading todo list: {e}")
114
+ return ToolResult(
115
+ success=False,
116
+ message=f"Failed to read todo list: {str(e)}",
117
+ content=None
118
+ )