hdsp-jupyter-extension 2.0.7__py3-none-any.whl → 2.0.10__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 (84) hide show
  1. agent_server/core/embedding_service.py +67 -46
  2. agent_server/core/rag_manager.py +40 -17
  3. agent_server/core/retriever.py +12 -6
  4. agent_server/core/vllm_embedding_service.py +246 -0
  5. agent_server/langchain/ARCHITECTURE.md +7 -51
  6. agent_server/langchain/agent.py +39 -20
  7. agent_server/langchain/custom_middleware.py +206 -62
  8. agent_server/langchain/hitl_config.py +6 -9
  9. agent_server/langchain/llm_factory.py +85 -1
  10. agent_server/langchain/logging_utils.py +52 -13
  11. agent_server/langchain/prompts.py +85 -45
  12. agent_server/langchain/tools/__init__.py +14 -10
  13. agent_server/langchain/tools/file_tools.py +266 -40
  14. agent_server/langchain/tools/file_utils.py +334 -0
  15. agent_server/langchain/tools/jupyter_tools.py +0 -1
  16. agent_server/langchain/tools/lsp_tools.py +264 -0
  17. agent_server/langchain/tools/resource_tools.py +12 -12
  18. agent_server/langchain/tools/search_tools.py +3 -158
  19. agent_server/main.py +7 -0
  20. agent_server/routers/langchain_agent.py +207 -102
  21. agent_server/routers/rag.py +8 -3
  22. hdsp_agent_core/models/rag.py +15 -1
  23. hdsp_agent_core/services/rag_service.py +6 -1
  24. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  25. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +3 -2
  26. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js +251 -5
  27. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
  28. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js +1831 -274
  29. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js.map +1 -0
  30. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js +11 -9
  31. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js.map +1 -0
  32. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
  33. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
  34. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
  35. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
  36. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
  37. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
  38. {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/METADATA +1 -3
  39. hdsp_jupyter_extension-2.0.10.dist-info/RECORD +144 -0
  40. jupyter_ext/__init__.py +18 -0
  41. jupyter_ext/_version.py +1 -1
  42. jupyter_ext/handlers.py +176 -1
  43. jupyter_ext/labextension/build_log.json +1 -1
  44. jupyter_ext/labextension/package.json +3 -2
  45. jupyter_ext/labextension/static/{frontend_styles_index_js.4770ec0fb2d173b6deb4.js → frontend_styles_index_js.2d9fb488c82498c45c2d.js} +251 -5
  46. jupyter_ext/labextension/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
  47. jupyter_ext/labextension/static/{lib_index_js.29cf4312af19e86f82af.js → lib_index_js.dc6434bee96ab03a0539.js} +1831 -274
  48. jupyter_ext/labextension/static/lib_index_js.dc6434bee96ab03a0539.js.map +1 -0
  49. jupyter_ext/labextension/static/{remoteEntry.61343eb4cf0577e74b50.js → remoteEntry.4a252df3ade74efee8d6.js} +11 -9
  50. jupyter_ext/labextension/static/remoteEntry.4a252df3ade74efee8d6.js.map +1 -0
  51. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
  52. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
  53. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
  54. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
  55. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
  56. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
  57. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
  58. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
  59. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
  60. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
  61. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
  62. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
  63. hdsp_jupyter_extension-2.0.7.dist-info/RECORD +0 -141
  64. jupyter_ext/labextension/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
  65. jupyter_ext/labextension/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
  66. jupyter_ext/labextension/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
  67. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
  68. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
  69. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
  70. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  71. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  72. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.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
  73. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.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
  74. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.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
  75. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.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
  76. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  77. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.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
  78. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.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
  79. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
  80. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.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
  81. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
  82. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
  83. {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/WHEEL +0 -0
  84. {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/licenses/LICENSE +0 -0
@@ -4,24 +4,32 @@ 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
- - list_files: List directory contents
7
+ - edit_file: Edit file with string replacement (requires approval)
8
8
  """
9
9
 
10
10
  import os
11
- from typing import Any, Dict, Optional
11
+ from typing import Any, Dict, List, Optional
12
12
 
13
13
  from langchain_core.tools import tool
14
14
  from pydantic import BaseModel, Field
15
15
 
16
+ # Default constants for file reading (aligned with DeepAgents best practices)
17
+ DEFAULT_READ_LIMIT = 500 # Conservative default to prevent context overflow
18
+ DEFAULT_READ_OFFSET = 0
19
+
16
20
 
17
21
  class ReadFileInput(BaseModel):
18
22
  """Input schema for read_file tool"""
19
23
 
20
24
  path: str = Field(description="Relative path to the file to read")
21
25
  encoding: str = Field(default="utf-8", description="File encoding")
22
- max_lines: Optional[int] = Field(
23
- default=None,
24
- description="Maximum number of lines to read",
26
+ offset: int = Field(
27
+ default=DEFAULT_READ_OFFSET,
28
+ description="Line offset to start reading from (0-indexed). Use for pagination.",
29
+ )
30
+ limit: int = Field(
31
+ default=DEFAULT_READ_LIMIT,
32
+ description="Maximum number of lines to read (default: 500). Use pagination for large files.",
25
33
  )
26
34
  execution_result: Optional[Dict[str, Any]] = Field(
27
35
  default=None,
@@ -45,14 +53,15 @@ class WriteFileInput(BaseModel):
45
53
  )
46
54
 
47
55
 
48
- class ListFilesInput(BaseModel):
49
- """Input schema for list_files tool"""
56
+ class EditFileInput(BaseModel):
57
+ """Input schema for edit_file tool"""
50
58
 
51
- path: str = Field(default=".", description="Directory path to list")
52
- recursive: bool = Field(default=False, description="Whether to list recursively")
53
- pattern: Optional[str] = Field(
54
- default=None,
55
- description="Glob pattern to filter files (e.g., '*.py', '*.ipynb')",
59
+ path: str = Field(description="Relative path to the file to edit")
60
+ old_string: str = Field(description="The exact string to find and replace")
61
+ new_string: str = Field(description="The replacement string")
62
+ replace_all: bool = Field(
63
+ default=False,
64
+ description="Whether to replace all occurrences (default: false, requires unique match)",
56
65
  )
57
66
  execution_result: Optional[Dict[str, Any]] = Field(
58
67
  default=None,
@@ -93,19 +102,27 @@ def _validate_path(path: str, workspace_root: str = ".") -> str:
93
102
  def read_file_tool(
94
103
  path: str,
95
104
  encoding: str = "utf-8",
96
- max_lines: Optional[int] = None,
105
+ offset: int = DEFAULT_READ_OFFSET,
106
+ limit: int = DEFAULT_READ_LIMIT,
97
107
  execution_result: Optional[Dict[str, Any]] = None,
98
108
  workspace_root: str = ".",
99
109
  ) -> Dict[str, Any]:
100
110
  """
101
- Read content from a file.
111
+ Read content from a file with pagination support.
102
112
 
103
113
  Only relative paths within the workspace are allowed.
104
114
  Absolute paths and parent directory traversal (..) are blocked.
105
115
 
116
+ **IMPORTANT for large files**: Use pagination with offset and limit to avoid context overflow.
117
+ - First scan: read_file(path, limit=100) to see file structure
118
+ - Read more: read_file(path, offset=100, limit=200) for next 200 lines
119
+ - Only omit limit when necessary for immediate editing
120
+
106
121
  Args:
107
122
  path: Relative path to the file
108
123
  encoding: File encoding (default: utf-8)
124
+ offset: Line offset to start reading from (0-indexed)
125
+ limit: Maximum number of lines to read (default: 500)
109
126
 
110
127
  Returns:
111
128
  Dict with file content or error
@@ -130,7 +147,8 @@ def read_file_tool(
130
147
  "parameters": {
131
148
  "path": path,
132
149
  "encoding": encoding,
133
- "max_lines": max_lines,
150
+ "offset": offset,
151
+ "limit": limit,
134
152
  },
135
153
  "status": "pending_execution",
136
154
  "message": "File read queued for execution by client",
@@ -197,45 +215,253 @@ def write_file_tool(
197
215
  }
198
216
 
199
217
 
200
- @tool(args_schema=ListFilesInput)
201
- def list_files_tool(
202
- path: str = ".",
203
- recursive: bool = False,
204
- pattern: Optional[str] = None,
218
+ @tool(args_schema=EditFileInput)
219
+ def edit_file_tool(
220
+ path: str,
221
+ old_string: str,
222
+ new_string: str,
223
+ replace_all: bool = False,
205
224
  execution_result: Optional[Dict[str, Any]] = None,
206
225
  workspace_root: str = ".",
207
226
  ) -> Dict[str, Any]:
208
227
  """
209
- List files and directories.
228
+ Edit a file by replacing a specific string with another.
229
+
230
+ This operation requires user approval before execution.
231
+ The old_string must be unique in the file unless replace_all=True.
210
232
 
211
233
  Args:
212
- path: Directory path to list (default: current directory)
213
- recursive: Whether to list recursively
214
- pattern: Optional glob pattern to filter (e.g., '*.py')
234
+ path: Relative path to the file
235
+ old_string: The exact string to find and replace
236
+ new_string: The replacement string
237
+ replace_all: Whether to replace all occurrences
215
238
 
216
239
  Returns:
217
- Dict with list of files and directories
240
+ Dict with operation status and diff preview (pending approval)
218
241
  """
219
- response: Dict[str, Any] = {
220
- "tool": "list_files_tool",
221
- "parameters": {
242
+ # Security validation
243
+ if os.path.isabs(path):
244
+ return {
245
+ "tool": "edit_file_tool",
246
+ "success": False,
247
+ "error": f"Absolute paths not allowed: {path}",
222
248
  "path": path,
223
- "recursive": recursive,
224
- "pattern": pattern,
225
- },
226
- "status": "pending_execution",
227
- "message": "File listing queued for execution by client",
228
- }
229
- if execution_result is not None:
230
- response["execution_result"] = execution_result
231
- response["status"] = "complete"
232
- response["message"] = "File listing executed with client-reported results"
233
- return response
249
+ }
250
+ if ".." in path:
251
+ return {
252
+ "tool": "edit_file_tool",
253
+ "success": False,
254
+ "error": f"Parent directory traversal not allowed: {path}",
255
+ "path": path,
256
+ }
257
+
258
+ try:
259
+ resolved_path = _validate_path(path, workspace_root)
260
+
261
+ # Build response with diff preview
262
+ # Note: actual file content will be read by client for diff generation
263
+ old_preview = old_string[:200] + "..." if len(old_string) > 200 else old_string
264
+ new_preview = new_string[:200] + "..." if len(new_string) > 200 else new_string
265
+
266
+ response: Dict[str, Any] = {
267
+ "tool": "edit_file_tool",
268
+ "parameters": {
269
+ "path": path,
270
+ "old_string": old_string,
271
+ "new_string": new_string,
272
+ "replace_all": replace_all,
273
+ },
274
+ "status": "pending_approval",
275
+ "path": path,
276
+ "resolved_path": resolved_path,
277
+ "old_string_preview": old_preview,
278
+ "new_string_preview": new_preview,
279
+ "replace_all": replace_all,
280
+ "message": "File edit operation requires user approval",
281
+ }
282
+
283
+ if execution_result is not None:
284
+ response["execution_result"] = execution_result
285
+ response["status"] = "complete"
286
+ response["message"] = "File edit executed with client-reported results"
287
+ # Include diff if provided by client
288
+ if "diff" in execution_result:
289
+ response["diff"] = execution_result["diff"]
290
+
291
+ return response
292
+
293
+ except ValueError as e:
294
+ return {
295
+ "tool": "edit_file_tool",
296
+ "success": False,
297
+ "error": str(e),
298
+ "path": path,
299
+ }
300
+
301
+
302
+ class EditOperation(BaseModel):
303
+ """Single edit operation for multiedit_file tool"""
304
+
305
+ old_string: str = Field(description="The exact string to find and replace")
306
+ new_string: str = Field(description="The replacement string")
307
+ replace_all: bool = Field(
308
+ default=False, description="Whether to replace all occurrences (default: false)"
309
+ )
310
+
311
+
312
+ class MultiEditInput(BaseModel):
313
+ """Input schema for multiedit_file tool"""
314
+
315
+ path: str = Field(description="Relative path to the file to edit")
316
+ edits: List[EditOperation] = Field(
317
+ description="List of edit operations to apply sequentially"
318
+ )
319
+ execution_result: Optional[Dict[str, Any]] = Field(
320
+ default=None,
321
+ description="Optional execution result payload from the client",
322
+ )
323
+
324
+
325
+ @tool(args_schema=MultiEditInput)
326
+ def multiedit_file_tool(
327
+ path: str,
328
+ edits: List[EditOperation],
329
+ execution_result: Optional[Dict[str, Any]] = None,
330
+ workspace_root: str = ".",
331
+ ) -> Dict[str, Any]:
332
+ """
333
+ Apply multiple sequential edits to a single file atomically.
334
+
335
+ This is more efficient than multiple edit_file_tool calls when you need
336
+ to make several changes to the same file. All edits are validated before
337
+ any are applied - if one fails, none are applied.
338
+
339
+ Use this tool when:
340
+ - Making multiple related changes to a file
341
+ - Updating several config values at once
342
+ - Refactoring multiple sections of code
343
+
344
+ Args:
345
+ path: Relative path to the file
346
+ edits: List of edit operations, each containing:
347
+ - old_string: The exact string to find and replace
348
+ - new_string: The replacement string
349
+ - replace_all: (optional) Whether to replace all occurrences
350
+
351
+ Returns:
352
+ Dict with operation status, edits_applied count, and diff preview
353
+
354
+ Example:
355
+ multiedit_file_tool(
356
+ path="config.py",
357
+ edits=[
358
+ {"old_string": "DEBUG = True", "new_string": "DEBUG = False"},
359
+ {"old_string": "LOG_LEVEL = 'INFO'", "new_string": "LOG_LEVEL = 'WARNING'"}
360
+ ]
361
+ )
362
+ """
363
+ # Security validation
364
+ if os.path.isabs(path):
365
+ return {
366
+ "tool": "multiedit_file_tool",
367
+ "success": False,
368
+ "error": f"Absolute paths not allowed: {path}",
369
+ "path": path,
370
+ }
371
+ if ".." in path:
372
+ return {
373
+ "tool": "multiedit_file_tool",
374
+ "success": False,
375
+ "error": f"Parent directory traversal not allowed: {path}",
376
+ "path": path,
377
+ }
378
+
379
+ if not edits or len(edits) == 0:
380
+ return {
381
+ "tool": "multiedit_file_tool",
382
+ "success": False,
383
+ "error": "At least one edit is required",
384
+ "path": path,
385
+ }
386
+
387
+ try:
388
+ resolved_path = _validate_path(path, workspace_root)
389
+
390
+ # Build edits preview (handle both EditOperation objects and dicts)
391
+ edits_preview = []
392
+ edits_as_dicts = []
393
+ for i, edit in enumerate(edits[:5]): # Preview first 5
394
+ # Support both Pydantic model and dict access
395
+ if hasattr(edit, "old_string"):
396
+ old_str = edit.old_string
397
+ new_str = edit.new_string
398
+ replace_all_val = edit.replace_all
399
+ else:
400
+ old_str = edit.get("old_string", "")
401
+ new_str = edit.get("new_string", "")
402
+ replace_all_val = edit.get("replace_all", False)
403
+
404
+ old_preview = (old_str[:50] + "...") if len(old_str) > 50 else old_str
405
+ new_preview = (new_str[:50] + "...") if len(new_str) > 50 else new_str
406
+ edits_preview.append(
407
+ {
408
+ "index": i,
409
+ "old_preview": old_preview,
410
+ "new_preview": new_preview,
411
+ "replace_all": replace_all_val,
412
+ }
413
+ )
414
+
415
+ # Convert all edits to dicts for serialization
416
+ for edit in edits:
417
+ if hasattr(edit, "model_dump"):
418
+ edits_as_dicts.append(edit.model_dump())
419
+ elif hasattr(edit, "dict"):
420
+ edits_as_dicts.append(edit.dict())
421
+ else:
422
+ edits_as_dicts.append(edit)
423
+
424
+ response: Dict[str, Any] = {
425
+ "tool": "multiedit_file_tool",
426
+ "parameters": {
427
+ "path": path,
428
+ "edits_count": len(edits),
429
+ "edits": edits_as_dicts,
430
+ },
431
+ "status": "pending_approval",
432
+ "path": path,
433
+ "resolved_path": resolved_path,
434
+ "edits_preview": edits_preview,
435
+ "total_edits": len(edits),
436
+ "message": f"Multi-edit operation ({len(edits)} edits) requires user approval",
437
+ }
438
+
439
+ if execution_result is not None:
440
+ response["execution_result"] = execution_result
441
+ response["status"] = "complete"
442
+ response["message"] = "Multi-edit executed with client-reported results"
443
+ if "diff" in execution_result:
444
+ response["diff"] = execution_result["diff"]
445
+ if "edits_applied" in execution_result:
446
+ response["edits_applied"] = execution_result["edits_applied"]
447
+ if "edits_failed" in execution_result:
448
+ response["edits_failed"] = execution_result["edits_failed"]
449
+
450
+ return response
451
+
452
+ except ValueError as e:
453
+ return {
454
+ "tool": "multiedit_file_tool",
455
+ "success": False,
456
+ "error": str(e),
457
+ "path": path,
458
+ }
234
459
 
235
460
 
236
461
  # Export all tools
237
462
  FILE_TOOLS = [
238
463
  read_file_tool,
239
464
  write_file_tool,
240
- list_files_tool,
465
+ edit_file_tool,
466
+ multiedit_file_tool,
241
467
  ]