Skip to content

Commit 24d996b

Browse files
abhishekspclaude
andcommitted
Add append mode support to file_write tool (#344)
- Add optional 'mode' parameter with 'write' (default) and 'append' options - Maintain backward compatibility with default overwrite behavior 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 299157a commit 24d996b

File tree

3 files changed

+210
-2
lines changed

3 files changed

+210
-2
lines changed

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ dev = [
7171
"pytest>=8.0.0,<9.0.0",
7272
"ruff>=0.13.0,<0.14.0",
7373
"responses>=0.6.1,<1.0.0",
74+
"botocore[crt]>=1.39.7,<2.0.0",
7475
"mem0ai>=0.1.104,<1.0.0",
7576
"opensearch-py>=2.8.0,<3.0.0",
7677
"nest-asyncio>=1.5.0,<2.0.0",

src/strands_tools/file_write.py

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
3939
agent = Agent(tools=[file_write])
4040
41-
# Write to a file with user confirmation
41+
# Write to a file with user confirmation (overwrites if exists)
4242
agent.tool.file_write(
4343
path="/path/to/file.txt",
4444
content="Hello World!"
@@ -49,6 +49,20 @@
4949
path="/path/to/script.py",
5050
content="def hello():\n print('Hello world!')"
5151
)
52+
53+
# Append to an existing file
54+
agent.tool.file_write(
55+
path="/path/to/log.txt",
56+
content="New log entry\n",
57+
mode="append"
58+
)
59+
60+
# Explicitly overwrite a file
61+
agent.tool.file_write(
62+
path="/path/to/config.json",
63+
content='{"setting": "value"}',
64+
mode="write"
65+
)
5266
```
5367
5468
See the file_write function docstring for more details on usage options and parameters.
@@ -82,6 +96,15 @@
8296
"type": "string",
8397
"description": "The content to write to the file",
8498
},
99+
"mode": {
100+
"type": "string",
101+
"enum": ["write", "append"],
102+
"description": (
103+
"Write mode: 'write' to overwrite file (default), "
104+
"'append' to add content to end of existing file"
105+
),
106+
"default": "write",
107+
},
85108
},
86109
"required": ["path", "content"],
87110
}
@@ -168,6 +191,8 @@ def file_write(tool: ToolUse, **kwargs: Any) -> ToolResult:
168191
- path: The path to the file to write. User paths with tilde (~)
169192
are automatically expanded.
170193
- content: The content to write to the file.
194+
- mode: (Optional) Write mode - 'write' to overwrite file (default),
195+
'append' to add content to end of existing file.
171196
**kwargs: Additional keyword arguments (not used currently)
172197
173198
Returns:
@@ -191,6 +216,24 @@ def file_write(tool: ToolUse, **kwargs: Any) -> ToolResult:
191216
tool_input = tool["input"]
192217
path = expanduser(tool_input["path"])
193218
content = tool_input["content"]
219+
mode = tool_input.get("mode", "write")
220+
221+
# Validate mode
222+
if mode not in ["write", "append"]:
223+
error_message = f"Invalid mode: '{mode}'. Must be 'write' or 'append'"
224+
error_panel = Panel(
225+
Text(error_message, style="bold red"),
226+
title="[bold red]Invalid Mode",
227+
border_style="red",
228+
box=box.HEAVY,
229+
expand=False,
230+
)
231+
console.print(error_panel)
232+
return {
233+
"toolUseId": tool_use_id,
234+
"status": "error",
235+
"content": [{"text": error_message}],
236+
}
194237

195238
strands_dev = os.environ.get("BYPASS_TOOL_CONSENT", "").lower() == "true"
196239

@@ -199,6 +242,8 @@ def file_write(tool: ToolUse, **kwargs: Any) -> ToolResult:
199242
Text.assemble(
200243
("Path: ", "cyan"),
201244
(path, "yellow"),
245+
("\nMode: ", "cyan"),
246+
(mode, "yellow"),
202247
("\nSize: ", "cyan"),
203248
(f"{len(content)} characters", "yellow"),
204249
),
@@ -256,8 +301,11 @@ def file_write(tool: ToolUse, **kwargs: Any) -> ToolResult:
256301
)
257302
)
258303

304+
# Map mode to file open mode
305+
file_mode = "w" if mode == "write" else "a"
306+
259307
# Write the file
260-
with open(path, "w") as file:
308+
with open(path, file_mode) as file:
261309
file.write(content)
262310

263311
success_message = f"File written successfully to {path}"

tests/test_file_write.py

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,3 +242,162 @@ def test_file_write_alternative_rejection(mock_user_input, temp_file):
242242

243243
# Verify file was not created
244244
assert not os.path.exists(temp_file)
245+
246+
247+
@patch("strands_tools.file_write.get_user_input")
248+
def test_file_write_append_mode(mock_user_input, temp_file):
249+
"""Test appending content to existing file."""
250+
mock_user_input.return_value = "y"
251+
252+
# Write initial content
253+
with open(temp_file, "w") as f:
254+
f.write("Initial content\n")
255+
256+
# Append using the tool
257+
tool_use = {
258+
"toolUseId": "test-append",
259+
"input": {"path": temp_file, "content": "Appended content\n", "mode": "append"},
260+
}
261+
262+
result = file_write.file_write(tool=tool_use)
263+
assert result["status"] == "success"
264+
265+
# Verify both contents are present
266+
with open(temp_file, "r") as f:
267+
content = f.read()
268+
assert "Initial content\n" in content
269+
assert "Appended content\n" in content
270+
assert content == "Initial content\nAppended content\n"
271+
272+
273+
@patch("strands_tools.file_write.get_user_input")
274+
def test_file_write_append_mode_new_file(mock_user_input, temp_file):
275+
"""Test that append mode creates file if it doesn't exist."""
276+
mock_user_input.return_value = "y"
277+
278+
# Ensure file doesn't exist
279+
assert not os.path.exists(temp_file)
280+
281+
# Append to non-existent file
282+
tool_use = {
283+
"toolUseId": "test-append-new",
284+
"input": {"path": temp_file, "content": "New file content\n", "mode": "append"},
285+
}
286+
287+
result = file_write.file_write(tool=tool_use)
288+
assert result["status"] == "success"
289+
290+
# Verify file was created with content
291+
assert os.path.exists(temp_file)
292+
with open(temp_file, "r") as f:
293+
assert f.read() == "New file content\n"
294+
295+
296+
@patch("strands_tools.file_write.get_user_input")
297+
def test_file_write_default_mode_overwrites(mock_user_input, temp_file):
298+
"""Test that default mode (write) overwrites existing content."""
299+
mock_user_input.return_value = "y"
300+
301+
# Write initial content
302+
with open(temp_file, "w") as f:
303+
f.write("Original content\n")
304+
305+
# Write without specifying mode (should default to overwrite)
306+
tool_use = {
307+
"toolUseId": "test-default-overwrite",
308+
"input": {"path": temp_file, "content": "New content\n"},
309+
}
310+
311+
result = file_write.file_write(tool=tool_use)
312+
assert result["status"] == "success"
313+
314+
# Verify original content was overwritten
315+
with open(temp_file, "r") as f:
316+
content = f.read()
317+
assert content == "New content\n"
318+
assert "Original content" not in content
319+
320+
321+
@patch("strands_tools.file_write.get_user_input")
322+
def test_file_write_write_mode_explicit(mock_user_input, temp_file):
323+
"""Test that explicit write mode overwrites existing content."""
324+
mock_user_input.return_value = "y"
325+
326+
# Write initial content
327+
with open(temp_file, "w") as f:
328+
f.write("Original content\n")
329+
330+
# Write with explicit write mode
331+
tool_use = {
332+
"toolUseId": "test-write-mode",
333+
"input": {"path": temp_file, "content": "Replacement content\n", "mode": "write"},
334+
}
335+
336+
result = file_write.file_write(tool=tool_use)
337+
assert result["status"] == "success"
338+
339+
# Verify original content was overwritten
340+
with open(temp_file, "r") as f:
341+
content = f.read()
342+
assert content == "Replacement content\n"
343+
assert "Original content" not in content
344+
345+
346+
def test_file_write_invalid_mode(temp_file):
347+
"""Test error handling for invalid mode values."""
348+
tool_use = {
349+
"toolUseId": "test-invalid-mode",
350+
"input": {"path": temp_file, "content": "Test content", "mode": "invalid"},
351+
}
352+
353+
result = file_write.file_write(tool=tool_use)
354+
355+
# Verify the error was handled correctly
356+
assert result["status"] == "error"
357+
assert "Invalid mode" in result["content"][0]["text"]
358+
assert "invalid" in result["content"][0]["text"]
359+
360+
361+
@patch.dict("os.environ", {"BYPASS_TOOL_CONSENT": "true"})
362+
def test_file_write_append_via_agent(agent, temp_file):
363+
"""Test append mode via the agent interface."""
364+
# Write initial content
365+
with open(temp_file, "w") as f:
366+
f.write("First line\n")
367+
368+
# Append via agent
369+
result = agent.tool.file_write(path=temp_file, content="Second line\n", mode="append")
370+
371+
# Verify success
372+
result_text = extract_result_text(result)
373+
assert "File write success" in result_text
374+
375+
# Verify both lines are present
376+
with open(temp_file, "r") as f:
377+
content = f.read()
378+
assert "First line\n" in content
379+
assert "Second line\n" in content
380+
381+
382+
@patch("strands_tools.file_write.get_user_input")
383+
def test_file_write_append_multiple_times(mock_user_input, temp_file):
384+
"""Test appending multiple times to the same file."""
385+
mock_user_input.return_value = "y"
386+
387+
# Write initial content
388+
with open(temp_file, "w") as f:
389+
f.write("Line 1\n")
390+
391+
# Append multiple times
392+
for i in range(2, 5):
393+
tool_use = {
394+
"toolUseId": f"test-append-{i}",
395+
"input": {"path": temp_file, "content": f"Line {i}\n", "mode": "append"},
396+
}
397+
result = file_write.file_write(tool=tool_use)
398+
assert result["status"] == "success"
399+
400+
# Verify all lines are present in order
401+
with open(temp_file, "r") as f:
402+
content = f.read()
403+
assert content == "Line 1\nLine 2\nLine 3\nLine 4\n"

0 commit comments

Comments
 (0)