hdsp-jupyter-extension 2.0.6__py3-none-any.whl → 2.0.8__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 (88) hide show
  1. agent_server/core/embedding_service.py +67 -46
  2. agent_server/core/rag_manager.py +31 -17
  3. agent_server/core/reflection_engine.py +0 -1
  4. agent_server/core/retriever.py +13 -8
  5. agent_server/core/vllm_embedding_service.py +243 -0
  6. agent_server/knowledge/watchdog_service.py +1 -1
  7. agent_server/langchain/ARCHITECTURE.md +1193 -0
  8. agent_server/langchain/agent.py +82 -588
  9. agent_server/langchain/custom_middleware.py +663 -0
  10. agent_server/langchain/executors/__init__.py +2 -7
  11. agent_server/langchain/executors/notebook_searcher.py +46 -38
  12. agent_server/langchain/hitl_config.py +71 -0
  13. agent_server/langchain/llm_factory.py +166 -0
  14. agent_server/langchain/logging_utils.py +223 -0
  15. agent_server/langchain/prompts.py +150 -0
  16. agent_server/langchain/state.py +16 -6
  17. agent_server/langchain/tools/__init__.py +19 -0
  18. agent_server/langchain/tools/file_tools.py +354 -114
  19. agent_server/langchain/tools/file_utils.py +334 -0
  20. agent_server/langchain/tools/jupyter_tools.py +18 -18
  21. agent_server/langchain/tools/lsp_tools.py +264 -0
  22. agent_server/langchain/tools/resource_tools.py +161 -0
  23. agent_server/langchain/tools/search_tools.py +198 -216
  24. agent_server/langchain/tools/shell_tools.py +54 -0
  25. agent_server/main.py +11 -1
  26. agent_server/routers/health.py +1 -1
  27. agent_server/routers/langchain_agent.py +1040 -289
  28. agent_server/routers/rag.py +8 -3
  29. hdsp_agent_core/models/rag.py +15 -1
  30. hdsp_agent_core/prompts/auto_agent_prompts.py +3 -3
  31. hdsp_agent_core/services/rag_service.py +6 -1
  32. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  33. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +3 -2
  34. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.02d346171474a0fb2dc1.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js +470 -7
  35. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
  36. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js +3196 -441
  37. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
  38. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js +9 -7
  39. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
  40. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/METADATA +2 -1
  41. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/RECORD +75 -69
  42. jupyter_ext/__init__.py +18 -0
  43. jupyter_ext/_version.py +1 -1
  44. jupyter_ext/handlers.py +1351 -58
  45. jupyter_ext/labextension/build_log.json +1 -1
  46. jupyter_ext/labextension/package.json +3 -2
  47. jupyter_ext/labextension/static/{frontend_styles_index_js.02d346171474a0fb2dc1.js → frontend_styles_index_js.8740a527757068814573.js} +470 -7
  48. jupyter_ext/labextension/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
  49. jupyter_ext/labextension/static/{lib_index_js.a223ea20056954479ae9.js → lib_index_js.e4ff4b5779b5e049f84c.js} +3196 -441
  50. jupyter_ext/labextension/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
  51. jupyter_ext/labextension/static/{remoteEntry.addf2fa038fa60304aa2.js → remoteEntry.020cdb0b864cfaa4e41e.js} +9 -7
  52. jupyter_ext/labextension/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
  53. jupyter_ext/resource_usage.py +180 -0
  54. jupyter_ext/tests/test_handlers.py +58 -0
  55. agent_server/langchain/executors/jupyter_executor.py +0 -429
  56. agent_server/langchain/middleware/__init__.py +0 -36
  57. agent_server/langchain/middleware/code_search_middleware.py +0 -278
  58. agent_server/langchain/middleware/error_handling_middleware.py +0 -338
  59. agent_server/langchain/middleware/jupyter_execution_middleware.py +0 -301
  60. agent_server/langchain/middleware/rag_middleware.py +0 -227
  61. agent_server/langchain/middleware/validation_middleware.py +0 -240
  62. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
  63. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
  64. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
  65. jupyter_ext/labextension/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
  66. jupyter_ext/labextension/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
  67. jupyter_ext/labextension/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
  68. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  69. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  70. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +0 -0
  71. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +0 -0
  72. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +0 -0
  73. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +0 -0
  74. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  75. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +0 -0
  76. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +0 -0
  77. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +0 -0
  78. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -0
  79. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +0 -0
  80. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -0
  81. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
  82. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +0 -0
  83. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
  84. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
  85. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +0 -0
  86. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -0
  87. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/WHEEL +0 -0
  88. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/licenses/LICENSE +0 -0
@@ -4,6 +4,7 @@ File Tools for LangChain Agent
4
4
  Provides tools for file system operations:
5
5
  - read_file: Read file content
6
6
  - write_file: Write content to file (requires approval)
7
+ - edit_file: Edit file with string replacement (requires approval)
7
8
  - list_files: List directory contents
8
9
  """
9
10
 
@@ -13,34 +14,81 @@ from typing import Any, Dict, List, Optional
13
14
  from langchain_core.tools import tool
14
15
  from pydantic import BaseModel, Field
15
16
 
17
+ # Default constants for file reading (aligned with DeepAgents best practices)
18
+ DEFAULT_READ_LIMIT = 500 # Conservative default to prevent context overflow
19
+ DEFAULT_READ_OFFSET = 0
20
+
16
21
 
17
22
  class ReadFileInput(BaseModel):
18
23
  """Input schema for read_file tool"""
24
+
19
25
  path: str = Field(description="Relative path to the file to read")
20
26
  encoding: str = Field(default="utf-8", description="File encoding")
27
+ offset: int = Field(
28
+ default=DEFAULT_READ_OFFSET,
29
+ description="Line offset to start reading from (0-indexed). Use for pagination.",
30
+ )
31
+ limit: int = Field(
32
+ default=DEFAULT_READ_LIMIT,
33
+ description="Maximum number of lines to read (default: 500). Use pagination for large files.",
34
+ )
35
+ execution_result: Optional[Dict[str, Any]] = Field(
36
+ default=None,
37
+ description="Optional execution result payload from the client",
38
+ )
21
39
 
22
40
 
23
41
  class WriteFileInput(BaseModel):
24
42
  """Input schema for write_file tool"""
43
+
25
44
  path: str = Field(description="Relative path to the file to write")
26
45
  content: str = Field(description="Content to write to the file")
27
46
  encoding: str = Field(default="utf-8", description="File encoding")
47
+ overwrite: bool = Field(
48
+ default=False,
49
+ description="Whether to overwrite an existing file (default: false)",
50
+ )
51
+ execution_result: Optional[Dict[str, Any]] = Field(
52
+ default=None,
53
+ description="Optional execution result payload from the client",
54
+ )
55
+
56
+
57
+ class EditFileInput(BaseModel):
58
+ """Input schema for edit_file tool"""
59
+
60
+ path: str = Field(description="Relative path to the file to edit")
61
+ old_string: str = Field(description="The exact string to find and replace")
62
+ new_string: str = Field(description="The replacement string")
63
+ replace_all: bool = Field(
64
+ default=False,
65
+ description="Whether to replace all occurrences (default: false, requires unique match)",
66
+ )
67
+ execution_result: Optional[Dict[str, Any]] = Field(
68
+ default=None,
69
+ description="Optional execution result payload from the client",
70
+ )
28
71
 
29
72
 
30
73
  class ListFilesInput(BaseModel):
31
74
  """Input schema for list_files tool"""
75
+
32
76
  path: str = Field(default=".", description="Directory path to list")
33
77
  recursive: bool = Field(default=False, description="Whether to list recursively")
34
78
  pattern: Optional[str] = Field(
35
79
  default=None,
36
- description="Glob pattern to filter files (e.g., '*.py', '*.ipynb')"
80
+ description="Glob pattern to filter files (e.g., '*.py', '*.ipynb')",
81
+ )
82
+ execution_result: Optional[Dict[str, Any]] = Field(
83
+ default=None,
84
+ description="Optional execution result payload from the client",
37
85
  )
38
86
 
39
87
 
40
88
  def _validate_path(path: str, workspace_root: str = ".") -> str:
41
89
  """
42
90
  Validate and resolve file path.
43
-
91
+
44
92
  Security checks:
45
93
  - No absolute paths allowed
46
94
  - No parent directory traversal (..)
@@ -70,93 +118,97 @@ def _validate_path(path: str, workspace_root: str = ".") -> str:
70
118
  def read_file_tool(
71
119
  path: str,
72
120
  encoding: str = "utf-8",
73
- workspace_root: str = "."
121
+ offset: int = DEFAULT_READ_OFFSET,
122
+ limit: int = DEFAULT_READ_LIMIT,
123
+ execution_result: Optional[Dict[str, Any]] = None,
124
+ workspace_root: str = ".",
74
125
  ) -> Dict[str, Any]:
75
126
  """
76
- Read content from a file.
77
-
127
+ Read content from a file with pagination support.
128
+
78
129
  Only relative paths within the workspace are allowed.
79
130
  Absolute paths and parent directory traversal (..) are blocked.
80
-
131
+
132
+ **IMPORTANT for large files**: Use pagination with offset and limit to avoid context overflow.
133
+ - First scan: read_file(path, limit=100) to see file structure
134
+ - Read more: read_file(path, offset=100, limit=200) for next 200 lines
135
+ - Only omit limit when necessary for immediate editing
136
+
81
137
  Args:
82
138
  path: Relative path to the file
83
139
  encoding: File encoding (default: utf-8)
84
-
140
+ offset: Line offset to start reading from (0-indexed)
141
+ limit: Maximum number of lines to read (default: 500)
142
+
85
143
  Returns:
86
144
  Dict with file content or error
87
145
  """
88
- try:
89
- resolved_path = _validate_path(path, workspace_root)
90
-
91
- if not os.path.exists(resolved_path):
92
- return {
93
- "tool": "read_file",
94
- "success": False,
95
- "error": f"File not found: {path}",
96
- "path": path,
97
- }
98
-
99
- if not os.path.isfile(resolved_path):
100
- return {
101
- "tool": "read_file",
102
- "success": False,
103
- "error": f"Not a file: {path}",
104
- "path": path,
105
- }
106
-
107
- with open(resolved_path, "r", encoding=encoding) as f:
108
- content = f.read()
109
-
110
- return {
111
- "tool": "read_file",
112
- "success": True,
113
- "path": path,
114
- "content": content,
115
- "size": len(content),
116
- }
117
-
118
- except ValueError as e:
146
+ if os.path.isabs(path):
119
147
  return {
120
- "tool": "read_file",
148
+ "tool": "read_file_tool",
121
149
  "success": False,
122
- "error": str(e),
150
+ "error": f"Absolute paths not allowed: {path}",
123
151
  "path": path,
124
152
  }
125
- except Exception as e:
153
+ if ".." in path:
126
154
  return {
127
- "tool": "read_file",
155
+ "tool": "read_file_tool",
128
156
  "success": False,
129
- "error": f"Failed to read file: {str(e)}",
157
+ "error": f"Parent directory traversal not allowed: {path}",
130
158
  "path": path,
131
159
  }
132
160
 
161
+ response: Dict[str, Any] = {
162
+ "tool": "read_file_tool",
163
+ "parameters": {
164
+ "path": path,
165
+ "encoding": encoding,
166
+ "offset": offset,
167
+ "limit": limit,
168
+ },
169
+ "status": "pending_execution",
170
+ "message": "File read queued for execution by client",
171
+ }
172
+ if execution_result is not None:
173
+ response["execution_result"] = execution_result
174
+ response["status"] = "complete"
175
+ response["message"] = "File read executed with client-reported results"
176
+ return response
177
+
133
178
 
134
179
  @tool(args_schema=WriteFileInput)
135
180
  def write_file_tool(
136
181
  path: str,
137
182
  content: str,
138
183
  encoding: str = "utf-8",
139
- workspace_root: str = "."
184
+ overwrite: bool = False,
185
+ execution_result: Optional[Dict[str, Any]] = None,
186
+ workspace_root: str = ".",
140
187
  ) -> Dict[str, Any]:
141
188
  """
142
189
  Write content to a file.
143
-
190
+
144
191
  This operation requires user approval before execution.
145
192
  Only relative paths within the workspace are allowed.
146
-
193
+
147
194
  Args:
148
195
  path: Relative path to the file
149
196
  content: Content to write
150
197
  encoding: File encoding (default: utf-8)
151
-
198
+
152
199
  Returns:
153
200
  Dict with operation status (pending approval)
154
201
  """
155
202
  try:
156
203
  resolved_path = _validate_path(path, workspace_root)
157
204
 
158
- return {
159
- "tool": "write_file",
205
+ response: Dict[str, Any] = {
206
+ "tool": "write_file_tool",
207
+ "parameters": {
208
+ "path": path,
209
+ "encoding": encoding,
210
+ "overwrite": overwrite,
211
+ },
160
212
  "status": "pending_approval",
161
213
  "path": path,
162
214
  "resolved_path": resolved_path,
@@ -164,10 +216,99 @@ def write_file_tool(
164
216
  "content_length": len(content),
165
217
  "message": "File write operation requires user approval",
166
218
  }
219
+ if execution_result is not None:
220
+ response["execution_result"] = execution_result
221
+ response["status"] = "complete"
222
+ response["message"] = "File write executed with client-reported results"
223
+ return response
167
224
 
168
225
  except ValueError as e:
169
226
  return {
170
- "tool": "write_file",
227
+ "tool": "write_file_tool",
228
+ "success": False,
229
+ "error": str(e),
230
+ "path": path,
231
+ }
232
+
233
+
234
+ @tool(args_schema=EditFileInput)
235
+ def edit_file_tool(
236
+ path: str,
237
+ old_string: str,
238
+ new_string: str,
239
+ replace_all: bool = False,
240
+ execution_result: Optional[Dict[str, Any]] = None,
241
+ workspace_root: str = ".",
242
+ ) -> Dict[str, Any]:
243
+ """
244
+ Edit a file by replacing a specific string with another.
245
+
246
+ This operation requires user approval before execution.
247
+ The old_string must be unique in the file unless replace_all=True.
248
+
249
+ Args:
250
+ path: Relative path to the file
251
+ old_string: The exact string to find and replace
252
+ new_string: The replacement string
253
+ replace_all: Whether to replace all occurrences
254
+
255
+ Returns:
256
+ Dict with operation status and diff preview (pending approval)
257
+ """
258
+ # Security validation
259
+ if os.path.isabs(path):
260
+ return {
261
+ "tool": "edit_file_tool",
262
+ "success": False,
263
+ "error": f"Absolute paths not allowed: {path}",
264
+ "path": path,
265
+ }
266
+ if ".." in path:
267
+ return {
268
+ "tool": "edit_file_tool",
269
+ "success": False,
270
+ "error": f"Parent directory traversal not allowed: {path}",
271
+ "path": path,
272
+ }
273
+
274
+ try:
275
+ resolved_path = _validate_path(path, workspace_root)
276
+
277
+ # Build response with diff preview
278
+ # Note: actual file content will be read by client for diff generation
279
+ old_preview = old_string[:200] + "..." if len(old_string) > 200 else old_string
280
+ new_preview = new_string[:200] + "..." if len(new_string) > 200 else new_string
281
+
282
+ response: Dict[str, Any] = {
283
+ "tool": "edit_file_tool",
284
+ "parameters": {
285
+ "path": path,
286
+ "old_string": old_string,
287
+ "new_string": new_string,
288
+ "replace_all": replace_all,
289
+ },
290
+ "status": "pending_approval",
291
+ "path": path,
292
+ "resolved_path": resolved_path,
293
+ "old_string_preview": old_preview,
294
+ "new_string_preview": new_preview,
295
+ "replace_all": replace_all,
296
+ "message": "File edit operation requires user approval",
297
+ }
298
+
299
+ if execution_result is not None:
300
+ response["execution_result"] = execution_result
301
+ response["status"] = "complete"
302
+ response["message"] = "File edit executed with client-reported results"
303
+ # Include diff if provided by client
304
+ if "diff" in execution_result:
305
+ response["diff"] = execution_result["diff"]
306
+
307
+ return response
308
+
309
+ except ValueError as e:
310
+ return {
311
+ "tool": "edit_file_tool",
171
312
  "success": False,
172
313
  "error": str(e),
173
314
  "path": path,
@@ -179,94 +320,191 @@ def list_files_tool(
179
320
  path: str = ".",
180
321
  recursive: bool = False,
181
322
  pattern: Optional[str] = None,
182
- workspace_root: str = "."
323
+ execution_result: Optional[Dict[str, Any]] = None,
324
+ workspace_root: str = ".",
183
325
  ) -> Dict[str, Any]:
184
326
  """
185
327
  List files and directories.
186
-
328
+
187
329
  Args:
188
330
  path: Directory path to list (default: current directory)
189
331
  recursive: Whether to list recursively
190
332
  pattern: Optional glob pattern to filter (e.g., '*.py')
191
-
333
+
192
334
  Returns:
193
335
  Dict with list of files and directories
194
336
  """
195
- import fnmatch
337
+ response: Dict[str, Any] = {
338
+ "tool": "list_files_tool",
339
+ "parameters": {
340
+ "path": path,
341
+ "recursive": recursive,
342
+ "pattern": pattern,
343
+ },
344
+ "status": "pending_execution",
345
+ "message": "File listing queued for execution by client",
346
+ }
347
+ if execution_result is not None:
348
+ response["execution_result"] = execution_result
349
+ response["status"] = "complete"
350
+ response["message"] = "File listing executed with client-reported results"
351
+ return response
352
+
353
+
354
+ class EditOperation(BaseModel):
355
+ """Single edit operation for multiedit_file tool"""
356
+
357
+ old_string: str = Field(description="The exact string to find and replace")
358
+ new_string: str = Field(description="The replacement string")
359
+ replace_all: bool = Field(
360
+ default=False,
361
+ description="Whether to replace all occurrences (default: false)"
362
+ )
196
363
 
197
- try:
198
- resolved_path = _validate_path(path, workspace_root)
199
364
 
200
- if not os.path.exists(resolved_path):
201
- return {
202
- "tool": "list_files",
203
- "success": False,
204
- "error": f"Directory not found: {path}",
205
- "path": path,
206
- }
365
+ class MultiEditInput(BaseModel):
366
+ """Input schema for multiedit_file tool"""
207
367
 
208
- if not os.path.isdir(resolved_path):
209
- return {
210
- "tool": "list_files",
211
- "success": False,
212
- "error": f"Not a directory: {path}",
213
- "path": path,
214
- }
215
-
216
- files: List[Dict[str, Any]] = []
217
- dirs: List[str] = []
218
-
219
- if recursive:
220
- for root, dirnames, filenames in os.walk(resolved_path):
221
- rel_root = os.path.relpath(root, resolved_path)
222
- for dirname in dirnames:
223
- dir_path = os.path.join(rel_root, dirname) if rel_root != "." else dirname
224
- dirs.append(dir_path)
225
- for filename in filenames:
226
- if pattern and not fnmatch.fnmatch(filename, pattern):
227
- continue
228
- file_path = os.path.join(rel_root, filename) if rel_root != "." else filename
229
- full_path = os.path.join(root, filename)
230
- files.append({
231
- "name": filename,
232
- "path": file_path,
233
- "size": os.path.getsize(full_path),
234
- })
235
- else:
236
- for entry in os.scandir(resolved_path):
237
- if entry.is_dir():
238
- dirs.append(entry.name)
239
- elif entry.is_file():
240
- if pattern and not fnmatch.fnmatch(entry.name, pattern):
241
- continue
242
- files.append({
243
- "name": entry.name,
244
- "path": entry.name,
245
- "size": entry.stat().st_size,
246
- })
368
+ path: str = Field(description="Relative path to the file to edit")
369
+ edits: List[EditOperation] = Field(
370
+ description="List of edit operations to apply sequentially"
371
+ )
372
+ execution_result: Optional[Dict[str, Any]] = Field(
373
+ default=None,
374
+ description="Optional execution result payload from the client",
375
+ )
247
376
 
377
+
378
+ @tool(args_schema=MultiEditInput)
379
+ def multiedit_file_tool(
380
+ path: str,
381
+ edits: List[EditOperation],
382
+ execution_result: Optional[Dict[str, Any]] = None,
383
+ workspace_root: str = ".",
384
+ ) -> Dict[str, Any]:
385
+ """
386
+ Apply multiple sequential edits to a single file atomically.
387
+
388
+ This is more efficient than multiple edit_file_tool calls when you need
389
+ to make several changes to the same file. All edits are validated before
390
+ any are applied - if one fails, none are applied.
391
+
392
+ Use this tool when:
393
+ - Making multiple related changes to a file
394
+ - Updating several config values at once
395
+ - Refactoring multiple sections of code
396
+
397
+ Args:
398
+ path: Relative path to the file
399
+ edits: List of edit operations, each containing:
400
+ - old_string: The exact string to find and replace
401
+ - new_string: The replacement string
402
+ - replace_all: (optional) Whether to replace all occurrences
403
+
404
+ Returns:
405
+ Dict with operation status, edits_applied count, and diff preview
406
+
407
+ Example:
408
+ multiedit_file_tool(
409
+ path="config.py",
410
+ edits=[
411
+ {"old_string": "DEBUG = True", "new_string": "DEBUG = False"},
412
+ {"old_string": "LOG_LEVEL = 'INFO'", "new_string": "LOG_LEVEL = 'WARNING'"}
413
+ ]
414
+ )
415
+ """
416
+ # Security validation
417
+ if os.path.isabs(path):
418
+ return {
419
+ "tool": "multiedit_file_tool",
420
+ "success": False,
421
+ "error": f"Absolute paths not allowed: {path}",
422
+ "path": path,
423
+ }
424
+ if ".." in path:
248
425
  return {
249
- "tool": "list_files",
250
- "success": True,
426
+ "tool": "multiedit_file_tool",
427
+ "success": False,
428
+ "error": f"Parent directory traversal not allowed: {path}",
251
429
  "path": path,
252
- "directories": sorted(dirs),
253
- "files": sorted(files, key=lambda x: x["name"]),
254
- "total_dirs": len(dirs),
255
- "total_files": len(files),
256
430
  }
257
431
 
258
- except ValueError as e:
432
+ if not edits or len(edits) == 0:
259
433
  return {
260
- "tool": "list_files",
434
+ "tool": "multiedit_file_tool",
261
435
  "success": False,
262
- "error": str(e),
436
+ "error": "At least one edit is required",
437
+ "path": path,
438
+ }
439
+
440
+ try:
441
+ resolved_path = _validate_path(path, workspace_root)
442
+
443
+ # Build edits preview (handle both EditOperation objects and dicts)
444
+ edits_preview = []
445
+ edits_as_dicts = []
446
+ for i, edit in enumerate(edits[:5]): # Preview first 5
447
+ # Support both Pydantic model and dict access
448
+ if hasattr(edit, "old_string"):
449
+ old_str = edit.old_string
450
+ new_str = edit.new_string
451
+ replace_all_val = edit.replace_all
452
+ else:
453
+ old_str = edit.get("old_string", "")
454
+ new_str = edit.get("new_string", "")
455
+ replace_all_val = edit.get("replace_all", False)
456
+
457
+ old_preview = (old_str[:50] + "...") if len(old_str) > 50 else old_str
458
+ new_preview = (new_str[:50] + "...") if len(new_str) > 50 else new_str
459
+ edits_preview.append({
460
+ "index": i,
461
+ "old_preview": old_preview,
462
+ "new_preview": new_preview,
463
+ "replace_all": replace_all_val
464
+ })
465
+
466
+ # Convert all edits to dicts for serialization
467
+ for edit in edits:
468
+ if hasattr(edit, "model_dump"):
469
+ edits_as_dicts.append(edit.model_dump())
470
+ elif hasattr(edit, "dict"):
471
+ edits_as_dicts.append(edit.dict())
472
+ else:
473
+ edits_as_dicts.append(edit)
474
+
475
+ response: Dict[str, Any] = {
476
+ "tool": "multiedit_file_tool",
477
+ "parameters": {
478
+ "path": path,
479
+ "edits_count": len(edits),
480
+ "edits": edits_as_dicts,
481
+ },
482
+ "status": "pending_approval",
263
483
  "path": path,
484
+ "resolved_path": resolved_path,
485
+ "edits_preview": edits_preview,
486
+ "total_edits": len(edits),
487
+ "message": f"Multi-edit operation ({len(edits)} edits) requires user approval",
264
488
  }
265
- except Exception as e:
489
+
490
+ if execution_result is not None:
491
+ response["execution_result"] = execution_result
492
+ response["status"] = "complete"
493
+ response["message"] = "Multi-edit executed with client-reported results"
494
+ if "diff" in execution_result:
495
+ response["diff"] = execution_result["diff"]
496
+ if "edits_applied" in execution_result:
497
+ response["edits_applied"] = execution_result["edits_applied"]
498
+ if "edits_failed" in execution_result:
499
+ response["edits_failed"] = execution_result["edits_failed"]
500
+
501
+ return response
502
+
503
+ except ValueError as e:
266
504
  return {
267
- "tool": "list_files",
505
+ "tool": "multiedit_file_tool",
268
506
  "success": False,
269
- "error": f"Failed to list directory: {str(e)}",
507
+ "error": str(e),
270
508
  "path": path,
271
509
  }
272
510
 
@@ -275,5 +513,7 @@ def list_files_tool(
275
513
  FILE_TOOLS = [
276
514
  read_file_tool,
277
515
  write_file_tool,
516
+ edit_file_tool,
517
+ multiedit_file_tool,
278
518
  list_files_tool,
279
519
  ]