tunacode-cli 0.0.9__py3-none-any.whl → 0.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.
Potentially problematic release.
This version of tunacode-cli might be problematic. Click here for more details.
- tunacode/cli/commands.py +34 -165
- tunacode/cli/main.py +15 -38
- tunacode/cli/repl.py +24 -18
- tunacode/configuration/defaults.py +1 -1
- tunacode/configuration/models.py +4 -11
- tunacode/configuration/settings.py +10 -3
- tunacode/constants.py +6 -4
- tunacode/context.py +3 -1
- tunacode/core/agents/main.py +94 -52
- tunacode/core/setup/agent_setup.py +1 -1
- tunacode/core/setup/config_setup.py +148 -78
- tunacode/core/setup/coordinator.py +4 -2
- tunacode/core/setup/environment_setup.py +1 -1
- tunacode/core/setup/git_safety_setup.py +51 -39
- tunacode/exceptions.py +2 -0
- tunacode/prompts/system.txt +1 -1
- tunacode/services/undo_service.py +16 -13
- tunacode/setup.py +6 -2
- tunacode/tools/base.py +20 -11
- tunacode/tools/update_file.py +14 -24
- tunacode/tools/write_file.py +7 -9
- tunacode/ui/completers.py +33 -98
- tunacode/ui/input.py +9 -13
- tunacode/ui/keybindings.py +3 -1
- tunacode/ui/lexers.py +17 -16
- tunacode/ui/output.py +8 -14
- tunacode/ui/panels.py +7 -5
- tunacode/ui/prompt_manager.py +4 -8
- tunacode/ui/tool_ui.py +3 -3
- tunacode/utils/system.py +0 -40
- tunacode_cli-0.0.10.dist-info/METADATA +366 -0
- tunacode_cli-0.0.10.dist-info/RECORD +65 -0
- {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.dist-info}/licenses/LICENSE +1 -1
- tunacode/cli/model_selector.py +0 -178
- tunacode/core/agents/tinyagent_main.py +0 -194
- tunacode/core/setup/optimized_coordinator.py +0 -73
- tunacode/services/enhanced_undo_service.py +0 -322
- tunacode/services/project_undo_service.py +0 -311
- tunacode/tools/tinyagent_tools.py +0 -103
- tunacode/utils/lazy_imports.py +0 -59
- tunacode/utils/regex_cache.py +0 -33
- tunacode_cli-0.0.9.dist-info/METADATA +0 -321
- tunacode_cli-0.0.9.dist-info/RECORD +0 -73
- {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.dist-info}/WHEEL +0 -0
- {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.dist-info}/entry_points.txt +0 -0
- {tunacode_cli-0.0.9.dist-info → tunacode_cli-0.0.10.dist-info}/top_level.txt +0 -0
|
@@ -13,12 +13,15 @@ from tunacode.ui.panels import panel
|
|
|
13
13
|
async def yes_no_prompt(question: str, default: bool = True) -> bool:
|
|
14
14
|
"""Simple yes/no prompt."""
|
|
15
15
|
default_text = "[Y/n]" if default else "[y/N]"
|
|
16
|
-
response = await prompt_input(
|
|
17
|
-
|
|
16
|
+
response = await prompt_input(
|
|
17
|
+
session_key="yes_no",
|
|
18
|
+
pretext=f"{question} {default_text}: "
|
|
19
|
+
)
|
|
20
|
+
|
|
18
21
|
if not response.strip():
|
|
19
22
|
return default
|
|
20
|
-
|
|
21
|
-
return response.lower().strip() in [
|
|
23
|
+
|
|
24
|
+
return response.lower().strip() in ['y', 'yes']
|
|
22
25
|
|
|
23
26
|
|
|
24
27
|
class GitSafetySetup(BaseSetup):
|
|
@@ -26,7 +29,7 @@ class GitSafetySetup(BaseSetup):
|
|
|
26
29
|
|
|
27
30
|
def __init__(self, state_manager: StateManager):
|
|
28
31
|
super().__init__(state_manager)
|
|
29
|
-
|
|
32
|
+
|
|
30
33
|
@property
|
|
31
34
|
def name(self) -> str:
|
|
32
35
|
"""Return the name of this setup step."""
|
|
@@ -42,15 +45,18 @@ class GitSafetySetup(BaseSetup):
|
|
|
42
45
|
try:
|
|
43
46
|
# Check if git is installed
|
|
44
47
|
result = subprocess.run(
|
|
45
|
-
["git", "--version"],
|
|
48
|
+
["git", "--version"],
|
|
49
|
+
capture_output=True,
|
|
50
|
+
text=True,
|
|
51
|
+
check=False
|
|
46
52
|
)
|
|
47
|
-
|
|
53
|
+
|
|
48
54
|
if result.returncode != 0:
|
|
49
55
|
await panel(
|
|
50
56
|
"⚠️ Git Not Found",
|
|
51
57
|
"Git is not installed or not in PATH. TunaCode will modify files directly.\n"
|
|
52
58
|
"It's strongly recommended to install Git for safety.",
|
|
53
|
-
border_style="yellow"
|
|
59
|
+
border_style="yellow"
|
|
54
60
|
)
|
|
55
61
|
return
|
|
56
62
|
|
|
@@ -60,31 +66,33 @@ class GitSafetySetup(BaseSetup):
|
|
|
60
66
|
capture_output=True,
|
|
61
67
|
text=True,
|
|
62
68
|
check=False,
|
|
63
|
-
cwd=Path.cwd()
|
|
69
|
+
cwd=Path.cwd()
|
|
64
70
|
)
|
|
65
|
-
|
|
71
|
+
|
|
66
72
|
if result.returncode != 0:
|
|
67
73
|
await panel(
|
|
68
74
|
"⚠️ Not a Git Repository",
|
|
69
75
|
"This directory is not a Git repository. TunaCode will modify files directly.\n"
|
|
70
76
|
"Consider initializing a Git repository for safety: git init",
|
|
71
|
-
border_style="yellow"
|
|
77
|
+
border_style="yellow"
|
|
72
78
|
)
|
|
73
79
|
return
|
|
74
80
|
|
|
75
81
|
# Get current branch name
|
|
76
82
|
result = subprocess.run(
|
|
77
|
-
["git", "branch", "--show-current"],
|
|
83
|
+
["git", "branch", "--show-current"],
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
check=True
|
|
78
87
|
)
|
|
79
88
|
current_branch = result.stdout.strip()
|
|
80
|
-
|
|
89
|
+
|
|
81
90
|
if not current_branch:
|
|
82
91
|
# Detached HEAD state
|
|
83
92
|
await panel(
|
|
84
93
|
"⚠️ Detached HEAD State",
|
|
85
|
-
"You're in a detached HEAD state. TunaCode will continue "
|
|
86
|
-
"
|
|
87
|
-
border_style="yellow",
|
|
94
|
+
"You're in a detached HEAD state. TunaCode will continue without creating a branch.",
|
|
95
|
+
border_style="yellow"
|
|
88
96
|
)
|
|
89
97
|
return
|
|
90
98
|
|
|
@@ -95,28 +103,31 @@ class GitSafetySetup(BaseSetup):
|
|
|
95
103
|
|
|
96
104
|
# Propose new branch name
|
|
97
105
|
new_branch = f"{current_branch}-tunacode"
|
|
98
|
-
|
|
106
|
+
|
|
99
107
|
# Check if there are uncommitted changes
|
|
100
108
|
result = subprocess.run(
|
|
101
|
-
["git", "status", "--porcelain"],
|
|
109
|
+
["git", "status", "--porcelain"],
|
|
110
|
+
capture_output=True,
|
|
111
|
+
text=True,
|
|
112
|
+
check=True
|
|
102
113
|
)
|
|
103
|
-
|
|
114
|
+
|
|
104
115
|
has_changes = bool(result.stdout.strip())
|
|
105
|
-
|
|
116
|
+
|
|
106
117
|
# Ask user if they want to create a safety branch
|
|
107
118
|
message = (
|
|
108
|
-
f"For safety, TunaCode can create a new branch '{new_branch}' "
|
|
109
|
-
f"based on '{current_branch}'.\n"
|
|
119
|
+
f"For safety, TunaCode can create a new branch '{new_branch}' based on '{current_branch}'.\n"
|
|
110
120
|
f"This helps protect your work from unintended changes.\n"
|
|
111
121
|
)
|
|
112
|
-
|
|
122
|
+
|
|
113
123
|
if has_changes:
|
|
114
|
-
message +=
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
message += "\n⚠️ You have uncommitted changes that will be brought to the new branch."
|
|
125
|
+
|
|
126
|
+
create_branch = await yes_no_prompt(
|
|
127
|
+
f"{message}\n\nCreate safety branch?",
|
|
128
|
+
default=True
|
|
129
|
+
)
|
|
130
|
+
|
|
120
131
|
if not create_branch:
|
|
121
132
|
# User declined - show warning
|
|
122
133
|
await panel(
|
|
@@ -124,25 +135,26 @@ class GitSafetySetup(BaseSetup):
|
|
|
124
135
|
"You've chosen to work directly on your current branch.\n"
|
|
125
136
|
"TunaCode will modify files in place. Make sure you have backups!\n"
|
|
126
137
|
"You can always use /undo to revert changes.",
|
|
127
|
-
border_style="red"
|
|
138
|
+
border_style="red"
|
|
128
139
|
)
|
|
129
140
|
# Save preference
|
|
130
141
|
self.state_manager.session.user_config["skip_git_safety"] = True
|
|
131
142
|
return
|
|
132
|
-
|
|
143
|
+
|
|
133
144
|
# Create and checkout the new branch
|
|
134
145
|
try:
|
|
135
146
|
# Check if branch already exists
|
|
136
147
|
result = subprocess.run(
|
|
137
148
|
["git", "show-ref", "--verify", f"refs/heads/{new_branch}"],
|
|
138
149
|
capture_output=True,
|
|
139
|
-
check=False
|
|
150
|
+
check=False
|
|
140
151
|
)
|
|
141
|
-
|
|
152
|
+
|
|
142
153
|
if result.returncode == 0:
|
|
143
154
|
# Branch exists, ask to use it
|
|
144
155
|
use_existing = await yes_no_prompt(
|
|
145
|
-
f"Branch '{new_branch}' already exists. Switch to it?",
|
|
156
|
+
f"Branch '{new_branch}' already exists. Switch to it?",
|
|
157
|
+
default=True
|
|
146
158
|
)
|
|
147
159
|
if use_existing:
|
|
148
160
|
subprocess.run(["git", "checkout", new_branch], check=True)
|
|
@@ -153,24 +165,24 @@ class GitSafetySetup(BaseSetup):
|
|
|
153
165
|
# Create new branch
|
|
154
166
|
subprocess.run(["git", "checkout", "-b", new_branch], check=True)
|
|
155
167
|
await ui.success(f"Created and switched to new branch: {new_branch}")
|
|
156
|
-
|
|
168
|
+
|
|
157
169
|
except subprocess.CalledProcessError as e:
|
|
158
170
|
await panel(
|
|
159
171
|
"❌ Failed to Create Branch",
|
|
160
172
|
f"Could not create branch '{new_branch}': {str(e)}\n"
|
|
161
173
|
"Continuing on current branch.",
|
|
162
|
-
border_style="red"
|
|
174
|
+
border_style="red"
|
|
163
175
|
)
|
|
164
|
-
|
|
176
|
+
|
|
165
177
|
except Exception as e:
|
|
166
178
|
# Non-fatal error - just warn the user
|
|
167
179
|
await panel(
|
|
168
180
|
"⚠️ Git Safety Setup Failed",
|
|
169
181
|
f"Could not set up Git safety: {str(e)}\n"
|
|
170
182
|
"TunaCode will continue without branch protection.",
|
|
171
|
-
border_style="yellow"
|
|
183
|
+
border_style="yellow"
|
|
172
184
|
)
|
|
173
185
|
|
|
174
186
|
async def validate(self) -> bool:
|
|
175
187
|
"""Validate git safety setup - always returns True as this is optional."""
|
|
176
|
-
return True
|
|
188
|
+
return True
|
tunacode/exceptions.py
CHANGED
tunacode/prompts/system.txt
CHANGED
|
@@ -7,18 +7,18 @@ Manages automatic commits and rollback operations.
|
|
|
7
7
|
|
|
8
8
|
import subprocess
|
|
9
9
|
import time
|
|
10
|
-
from datetime import datetime, timezone
|
|
11
10
|
from pathlib import Path
|
|
12
11
|
from typing import Optional, Tuple
|
|
13
12
|
|
|
14
|
-
from
|
|
15
|
-
|
|
13
|
+
from pydantic_ai.messages import ModelResponse, TextPart
|
|
14
|
+
|
|
15
|
+
from tunacode.constants import (ERROR_UNDO_INIT, UNDO_DISABLED_HOME, UNDO_DISABLED_NO_GIT,
|
|
16
|
+
UNDO_GIT_TIMEOUT, UNDO_INITIAL_COMMIT)
|
|
16
17
|
from tunacode.core.state import StateManager
|
|
17
18
|
from tunacode.exceptions import GitOperationError
|
|
19
|
+
from tunacode.ui import console as ui
|
|
18
20
|
from tunacode.utils.system import get_session_dir
|
|
19
21
|
|
|
20
|
-
# Removed pydantic_ai import - using dict-based messages now
|
|
21
|
-
|
|
22
22
|
|
|
23
23
|
def is_in_git_project(directory: Optional[Path] = None) -> bool:
|
|
24
24
|
"""
|
|
@@ -220,14 +220,17 @@ def perform_undo(state_manager: StateManager) -> Tuple[bool, str]:
|
|
|
220
220
|
# Add a system message to the chat history to inform the AI
|
|
221
221
|
# about the undo operation
|
|
222
222
|
state_manager.session.messages.append(
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
223
|
+
ModelResponse(
|
|
224
|
+
parts=[
|
|
225
|
+
TextPart(
|
|
226
|
+
content=(
|
|
227
|
+
"The last changes were undone. "
|
|
228
|
+
f"Commit message of undone changes: {commit_msg}"
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
],
|
|
232
|
+
kind="response",
|
|
233
|
+
)
|
|
231
234
|
)
|
|
232
235
|
|
|
233
236
|
return True, "Successfully undid last change"
|
tunacode/setup.py
CHANGED
|
@@ -12,18 +12,22 @@ from tunacode.core.setup import (AgentSetup, ConfigSetup, EnvironmentSetup, GitS
|
|
|
12
12
|
from tunacode.core.state import StateManager
|
|
13
13
|
|
|
14
14
|
|
|
15
|
-
async def setup(run_setup: bool, state_manager: StateManager) -> None:
|
|
15
|
+
async def setup(run_setup: bool, state_manager: StateManager, cli_config: dict = None) -> None:
|
|
16
16
|
"""
|
|
17
17
|
Setup TunaCode on startup using the new setup coordinator.
|
|
18
18
|
|
|
19
19
|
Args:
|
|
20
20
|
run_setup (bool): If True, force run the setup process, resetting current config.
|
|
21
21
|
state_manager (StateManager): The state manager instance.
|
|
22
|
+
cli_config (dict): Optional CLI configuration with baseurl, model, and key.
|
|
22
23
|
"""
|
|
23
24
|
coordinator = SetupCoordinator(state_manager)
|
|
24
25
|
|
|
25
26
|
# Register setup steps in order
|
|
26
|
-
|
|
27
|
+
config_setup = ConfigSetup(state_manager)
|
|
28
|
+
if cli_config:
|
|
29
|
+
config_setup.cli_config = cli_config
|
|
30
|
+
coordinator.register_step(config_setup)
|
|
27
31
|
coordinator.register_step(EnvironmentSetup(state_manager))
|
|
28
32
|
coordinator.register_step(GitSafetySetup(state_manager)) # Run after config/env but before undo
|
|
29
33
|
coordinator.register_step(UndoSetup(state_manager))
|
tunacode/tools/base.py
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"""Base tool class for all Sidekick tools.
|
|
2
2
|
|
|
3
3
|
This module provides a base class that implements common patterns
|
|
4
|
-
for all tools including error handling and
|
|
4
|
+
for all tools including error handling, UI logging, and ModelRetry support.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
from abc import ABC, abstractmethod
|
|
8
8
|
|
|
9
|
+
from pydantic_ai.exceptions import ModelRetry
|
|
10
|
+
|
|
9
11
|
from tunacode.exceptions import FileOperationError, ToolExecutionError
|
|
10
12
|
from tunacode.types import FilePath, ToolName, ToolResult, UILogger
|
|
11
13
|
|
|
@@ -26,25 +28,31 @@ class BaseTool(ABC):
|
|
|
26
28
|
|
|
27
29
|
This method wraps the tool-specific logic with:
|
|
28
30
|
- UI logging of the operation
|
|
29
|
-
- Exception handling
|
|
31
|
+
- Exception handling (except ModelRetry and ToolExecutionError)
|
|
30
32
|
- Consistent error message formatting
|
|
31
33
|
|
|
32
34
|
Returns:
|
|
33
35
|
str: Success message
|
|
34
36
|
|
|
35
37
|
Raises:
|
|
36
|
-
|
|
38
|
+
ModelRetry: Re-raised to guide the LLM
|
|
39
|
+
ToolExecutionError: Raised for all other errors with structured information
|
|
37
40
|
"""
|
|
38
41
|
try:
|
|
39
42
|
if self.ui:
|
|
40
43
|
await self.ui.info(f"{self.tool_name}({self._format_args(*args, **kwargs)})")
|
|
41
44
|
result = await self._execute(*args, **kwargs)
|
|
42
|
-
|
|
45
|
+
|
|
43
46
|
# For file operations, try to create a git commit for undo tracking
|
|
44
47
|
if isinstance(self, FileBasedTool):
|
|
45
48
|
await self._commit_for_undo()
|
|
46
|
-
|
|
49
|
+
|
|
47
50
|
return result
|
|
51
|
+
except ModelRetry as e:
|
|
52
|
+
# Log as warning and re-raise for pydantic-ai
|
|
53
|
+
if self.ui:
|
|
54
|
+
await self.ui.warning(str(e))
|
|
55
|
+
raise
|
|
48
56
|
except ToolExecutionError:
|
|
49
57
|
# Already properly formatted, just re-raise
|
|
50
58
|
raise
|
|
@@ -68,7 +76,8 @@ class BaseTool(ABC):
|
|
|
68
76
|
str: Success message describing what was done
|
|
69
77
|
|
|
70
78
|
Raises:
|
|
71
|
-
|
|
79
|
+
ModelRetry: When the LLM needs guidance
|
|
80
|
+
Exception: Any other errors will be caught and handled
|
|
72
81
|
"""
|
|
73
82
|
pass
|
|
74
83
|
|
|
@@ -139,10 +148,10 @@ class FileBasedTool(BaseTool):
|
|
|
139
148
|
- Encoding handling
|
|
140
149
|
- Git commit for undo tracking
|
|
141
150
|
"""
|
|
142
|
-
|
|
151
|
+
|
|
143
152
|
async def _commit_for_undo(self) -> None:
|
|
144
153
|
"""Create a git commit for undo tracking after file operations.
|
|
145
|
-
|
|
154
|
+
|
|
146
155
|
This method gracefully handles cases where git is not available:
|
|
147
156
|
- No git repository: Warns user about limited undo functionality
|
|
148
157
|
- Git command fails: Warns but doesn't break the main operation
|
|
@@ -151,13 +160,13 @@ class FileBasedTool(BaseTool):
|
|
|
151
160
|
try:
|
|
152
161
|
# Import here to avoid circular imports
|
|
153
162
|
from tunacode.services.undo_service import commit_for_undo, is_in_git_project
|
|
154
|
-
|
|
163
|
+
|
|
155
164
|
# Check if we're in a git project first
|
|
156
165
|
if not is_in_git_project():
|
|
157
166
|
if self.ui:
|
|
158
167
|
await self.ui.muted("⚠️ No git repository - undo functionality limited")
|
|
159
168
|
return
|
|
160
|
-
|
|
169
|
+
|
|
161
170
|
# Try to create commit with tool name as prefix
|
|
162
171
|
success = commit_for_undo(message_prefix=f"tunacode {self.tool_name.lower()}")
|
|
163
172
|
if success and self.ui:
|
|
@@ -170,7 +179,7 @@ class FileBasedTool(BaseTool):
|
|
|
170
179
|
if self.ui:
|
|
171
180
|
try:
|
|
172
181
|
await self.ui.muted("⚠️ Git commit failed - undo functionality limited")
|
|
173
|
-
except
|
|
182
|
+
except:
|
|
174
183
|
# Even the warning failed, just continue silently
|
|
175
184
|
pass
|
|
176
185
|
|
tunacode/tools/update_file.py
CHANGED
|
@@ -7,6 +7,8 @@ Enables safe text replacement in existing files with target/patch semantics.
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
|
|
10
|
+
from pydantic_ai.exceptions import ModelRetry
|
|
11
|
+
|
|
10
12
|
from tunacode.exceptions import ToolExecutionError
|
|
11
13
|
from tunacode.tools.base import FileBasedTool
|
|
12
14
|
from tunacode.types import FileContent, FilePath, ToolResult
|
|
@@ -34,17 +36,13 @@ class UpdateFileTool(FileBasedTool):
|
|
|
34
36
|
ToolResult: A message indicating success.
|
|
35
37
|
|
|
36
38
|
Raises:
|
|
37
|
-
|
|
39
|
+
ModelRetry: If file not found or target not found
|
|
38
40
|
Exception: Any file operation errors
|
|
39
41
|
"""
|
|
40
42
|
if not os.path.exists(filepath):
|
|
41
|
-
raise
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
f"File '{filepath}' not found. Cannot update. "
|
|
45
|
-
"Verify the filepath or use `write_file` if it's a new file."
|
|
46
|
-
),
|
|
47
|
-
original_error=None,
|
|
43
|
+
raise ModelRetry(
|
|
44
|
+
f"File '{filepath}' not found. Cannot update. "
|
|
45
|
+
"Verify the filepath or use `write_file` if it's a new file."
|
|
48
46
|
)
|
|
49
47
|
|
|
50
48
|
with open(filepath, "r", encoding="utf-8") as f:
|
|
@@ -55,28 +53,20 @@ class UpdateFileTool(FileBasedTool):
|
|
|
55
53
|
context_lines = 10
|
|
56
54
|
lines = original.splitlines()
|
|
57
55
|
snippet = "\n".join(lines[:context_lines])
|
|
58
|
-
#
|
|
59
|
-
raise
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
"Ensure the `target` argument exactly matches the content you want to replace. "
|
|
64
|
-
f"File starts with:\n---\n{snippet}\n---"
|
|
65
|
-
),
|
|
66
|
-
original_error=None,
|
|
56
|
+
# Use ModelRetry to guide the LLM
|
|
57
|
+
raise ModelRetry(
|
|
58
|
+
f"Target block not found in '{filepath}'. "
|
|
59
|
+
"Ensure the `target` argument exactly matches the content you want to replace. "
|
|
60
|
+
f"File starts with:\n---\n{snippet}\n---"
|
|
67
61
|
)
|
|
68
62
|
|
|
69
63
|
new_content = original.replace(target, patch, 1) # Replace only the first occurrence
|
|
70
64
|
|
|
71
65
|
if original == new_content:
|
|
72
66
|
# This could happen if target and patch are identical
|
|
73
|
-
raise
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
f"Update target found, but replacement resulted in no changes to '{filepath}'. "
|
|
77
|
-
"Was the `target` identical to the `patch`? Please check the file content."
|
|
78
|
-
),
|
|
79
|
-
original_error=None,
|
|
67
|
+
raise ModelRetry(
|
|
68
|
+
f"Update target found, but replacement resulted in no changes to '{filepath}'. "
|
|
69
|
+
"Was the `target` identical to the `patch`? Please check the file content."
|
|
80
70
|
)
|
|
81
71
|
|
|
82
72
|
with open(filepath, "w", encoding="utf-8") as f:
|
tunacode/tools/write_file.py
CHANGED
|
@@ -7,6 +7,8 @@ Creates new files with automatic directory creation and overwrite protection.
|
|
|
7
7
|
|
|
8
8
|
import os
|
|
9
9
|
|
|
10
|
+
from pydantic_ai.exceptions import ModelRetry
|
|
11
|
+
|
|
10
12
|
from tunacode.exceptions import ToolExecutionError
|
|
11
13
|
from tunacode.tools.base import FileBasedTool
|
|
12
14
|
from tunacode.types import FileContent, FilePath, ToolResult
|
|
@@ -31,19 +33,15 @@ class WriteFileTool(FileBasedTool):
|
|
|
31
33
|
ToolResult: A message indicating success.
|
|
32
34
|
|
|
33
35
|
Raises:
|
|
34
|
-
|
|
36
|
+
ModelRetry: If the file already exists
|
|
35
37
|
Exception: Any file writing errors
|
|
36
38
|
"""
|
|
37
39
|
# Prevent overwriting existing files with this tool.
|
|
38
40
|
if os.path.exists(filepath):
|
|
39
|
-
#
|
|
40
|
-
raise
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
f"File '{filepath}' already exists. Use the `update_file` tool "
|
|
44
|
-
"to modify it, or choose a different filepath."
|
|
45
|
-
),
|
|
46
|
-
original_error=None,
|
|
41
|
+
# Use ModelRetry to guide the LLM
|
|
42
|
+
raise ModelRetry(
|
|
43
|
+
f"File '{filepath}' already exists. "
|
|
44
|
+
"Use the `update_file` tool to modify it, or choose a different filepath."
|
|
47
45
|
)
|
|
48
46
|
|
|
49
47
|
# Create directories if they don't exist
|