pdd-cli 0.0.45__py3-none-any.whl → 0.0.90__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.
- pdd/__init__.py +4 -4
- pdd/agentic_common.py +863 -0
- pdd/agentic_crash.py +534 -0
- pdd/agentic_fix.py +1179 -0
- pdd/agentic_langtest.py +162 -0
- pdd/agentic_update.py +370 -0
- pdd/agentic_verify.py +183 -0
- pdd/auto_deps_main.py +15 -5
- pdd/auto_include.py +63 -5
- pdd/bug_main.py +3 -2
- pdd/bug_to_unit_test.py +2 -0
- pdd/change_main.py +11 -4
- pdd/cli.py +22 -1181
- pdd/cmd_test_main.py +73 -21
- pdd/code_generator.py +58 -18
- pdd/code_generator_main.py +672 -25
- pdd/commands/__init__.py +42 -0
- pdd/commands/analysis.py +248 -0
- pdd/commands/fix.py +140 -0
- pdd/commands/generate.py +257 -0
- pdd/commands/maintenance.py +174 -0
- pdd/commands/misc.py +79 -0
- pdd/commands/modify.py +230 -0
- pdd/commands/report.py +144 -0
- pdd/commands/templates.py +215 -0
- pdd/commands/utility.py +110 -0
- pdd/config_resolution.py +58 -0
- pdd/conflicts_main.py +8 -3
- pdd/construct_paths.py +258 -82
- pdd/context_generator.py +10 -2
- pdd/context_generator_main.py +113 -11
- pdd/continue_generation.py +47 -7
- pdd/core/__init__.py +0 -0
- pdd/core/cli.py +503 -0
- pdd/core/dump.py +554 -0
- pdd/core/errors.py +63 -0
- pdd/core/utils.py +90 -0
- pdd/crash_main.py +44 -11
- pdd/data/language_format.csv +71 -63
- pdd/data/llm_model.csv +20 -18
- pdd/detect_change_main.py +5 -4
- pdd/fix_code_loop.py +330 -76
- pdd/fix_error_loop.py +207 -61
- pdd/fix_errors_from_unit_tests.py +4 -3
- pdd/fix_main.py +75 -18
- pdd/fix_verification_errors.py +12 -100
- pdd/fix_verification_errors_loop.py +306 -272
- pdd/fix_verification_main.py +28 -9
- pdd/generate_output_paths.py +93 -10
- pdd/generate_test.py +16 -5
- pdd/get_jwt_token.py +9 -2
- pdd/get_run_command.py +73 -0
- pdd/get_test_command.py +68 -0
- pdd/git_update.py +70 -19
- pdd/incremental_code_generator.py +2 -2
- pdd/insert_includes.py +11 -3
- pdd/llm_invoke.py +1269 -103
- pdd/load_prompt_template.py +36 -10
- pdd/pdd_completion.fish +25 -2
- pdd/pdd_completion.sh +30 -4
- pdd/pdd_completion.zsh +79 -4
- pdd/postprocess.py +10 -3
- pdd/preprocess.py +228 -15
- pdd/preprocess_main.py +8 -5
- pdd/prompts/agentic_crash_explore_LLM.prompt +49 -0
- pdd/prompts/agentic_fix_explore_LLM.prompt +45 -0
- pdd/prompts/agentic_fix_harvest_only_LLM.prompt +48 -0
- pdd/prompts/agentic_fix_primary_LLM.prompt +85 -0
- pdd/prompts/agentic_update_LLM.prompt +1071 -0
- pdd/prompts/agentic_verify_explore_LLM.prompt +45 -0
- pdd/prompts/auto_include_LLM.prompt +100 -905
- pdd/prompts/detect_change_LLM.prompt +122 -20
- pdd/prompts/example_generator_LLM.prompt +22 -1
- pdd/prompts/extract_code_LLM.prompt +5 -1
- pdd/prompts/extract_program_code_fix_LLM.prompt +7 -1
- pdd/prompts/extract_prompt_update_LLM.prompt +7 -8
- pdd/prompts/extract_promptline_LLM.prompt +17 -11
- pdd/prompts/find_verification_errors_LLM.prompt +6 -0
- pdd/prompts/fix_code_module_errors_LLM.prompt +4 -2
- pdd/prompts/fix_errors_from_unit_tests_LLM.prompt +8 -0
- pdd/prompts/fix_verification_errors_LLM.prompt +22 -0
- pdd/prompts/generate_test_LLM.prompt +21 -6
- pdd/prompts/increase_tests_LLM.prompt +1 -5
- pdd/prompts/insert_includes_LLM.prompt +228 -108
- pdd/prompts/trace_LLM.prompt +25 -22
- pdd/prompts/unfinished_prompt_LLM.prompt +85 -1
- pdd/prompts/update_prompt_LLM.prompt +22 -1
- pdd/pytest_output.py +127 -12
- pdd/render_mermaid.py +236 -0
- pdd/setup_tool.py +648 -0
- pdd/simple_math.py +2 -0
- pdd/split_main.py +3 -2
- pdd/summarize_directory.py +49 -6
- pdd/sync_determine_operation.py +543 -98
- pdd/sync_main.py +81 -31
- pdd/sync_orchestration.py +1334 -751
- pdd/sync_tui.py +848 -0
- pdd/template_registry.py +264 -0
- pdd/templates/architecture/architecture_json.prompt +242 -0
- pdd/templates/generic/generate_prompt.prompt +174 -0
- pdd/trace.py +168 -12
- pdd/trace_main.py +4 -3
- pdd/track_cost.py +151 -61
- pdd/unfinished_prompt.py +49 -3
- pdd/update_main.py +549 -67
- pdd/update_model_costs.py +2 -2
- pdd/update_prompt.py +19 -4
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/METADATA +19 -6
- pdd_cli-0.0.90.dist-info/RECORD +153 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/licenses/LICENSE +1 -1
- pdd_cli-0.0.45.dist-info/RECORD +0 -116
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/WHEEL +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/entry_points.txt +0 -0
- {pdd_cli-0.0.45.dist-info → pdd_cli-0.0.90.dist-info}/top_level.txt +0 -0
pdd/fix_code_loop.py
CHANGED
|
@@ -1,30 +1,201 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
1
2
|
import os
|
|
2
3
|
import shutil
|
|
3
4
|
import subprocess
|
|
4
5
|
import sys
|
|
6
|
+
import threading
|
|
5
7
|
from pathlib import Path
|
|
6
|
-
from typing import Tuple, Optional, Union
|
|
7
|
-
|
|
8
|
+
from typing import Tuple, Optional, Union, List
|
|
9
|
+
|
|
10
|
+
# Try to import DEFAULT_TIME, with fallback
|
|
11
|
+
try:
|
|
12
|
+
from . import DEFAULT_TIME
|
|
13
|
+
except ImportError:
|
|
14
|
+
DEFAULT_TIME = 0.5
|
|
15
|
+
|
|
16
|
+
# Try to import agentic modules, with fallbacks
|
|
17
|
+
try:
|
|
18
|
+
from .agentic_crash import run_agentic_crash
|
|
19
|
+
except ImportError:
|
|
20
|
+
def run_agentic_crash(**kwargs):
|
|
21
|
+
return (False, "Agentic crash handler not available", 0.0, "N/A", [])
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
from .get_language import get_language
|
|
25
|
+
except ImportError:
|
|
26
|
+
def get_language(ext):
|
|
27
|
+
return "unknown"
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
from .agentic_langtest import default_verify_cmd_for
|
|
31
|
+
except ImportError:
|
|
32
|
+
def default_verify_cmd_for(lang, verification_program):
|
|
33
|
+
return None
|
|
34
|
+
|
|
35
|
+
def _normalize_agentic_result(result):
|
|
36
|
+
"""
|
|
37
|
+
Normalize run_agentic_crash result into: (success: bool, msg: str, cost: float, model: str, changed_files: List[str])
|
|
38
|
+
Handles older 2/3/4-tuple shapes used by tests/monkeypatches.
|
|
39
|
+
"""
|
|
40
|
+
if isinstance(result, tuple):
|
|
41
|
+
if len(result) == 5:
|
|
42
|
+
ok, msg, cost, model, changed_files = result
|
|
43
|
+
return bool(ok), str(msg), float(cost), str(model or "agentic-cli"), list(changed_files or [])
|
|
44
|
+
if len(result) == 4:
|
|
45
|
+
ok, msg, cost, model = result
|
|
46
|
+
return bool(ok), str(msg), float(cost), str(model or "agentic-cli"), []
|
|
47
|
+
if len(result) == 3:
|
|
48
|
+
ok, msg, cost = result
|
|
49
|
+
return bool(ok), str(msg), float(cost), "agentic-cli", []
|
|
50
|
+
if len(result) == 2:
|
|
51
|
+
ok, msg = result
|
|
52
|
+
return bool(ok), str(msg), 0.0, "agentic-cli", []
|
|
53
|
+
# Fallback (shouldn't happen)
|
|
54
|
+
return False, "Invalid agentic result shape", 0.0, "agentic-cli", []
|
|
55
|
+
|
|
56
|
+
def _safe_run_agentic_crash(*, prompt_file, code_file, program_file, crash_log_file, cwd=None):
|
|
57
|
+
"""
|
|
58
|
+
Call (possibly monkeypatched) run_agentic_crash and normalize its return.
|
|
59
|
+
Maps arguments to the expected signature of run_agentic_crash.
|
|
60
|
+
|
|
61
|
+
Note: cwd parameter is accepted for compatibility but not passed to run_agentic_crash
|
|
62
|
+
as it determines the working directory from prompt_file.parent internally.
|
|
63
|
+
"""
|
|
64
|
+
if not prompt_file:
|
|
65
|
+
return False, "Agentic fix requires a valid prompt file.", 0.0, "agentic-cli", []
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
# Ensure inputs are Path objects as expected by run_agentic_crash
|
|
69
|
+
call_args = {
|
|
70
|
+
"prompt_file": Path(prompt_file),
|
|
71
|
+
"code_file": Path(code_file),
|
|
72
|
+
"program_file": Path(program_file),
|
|
73
|
+
"crash_log_file": Path(crash_log_file),
|
|
74
|
+
"verbose": True,
|
|
75
|
+
"quiet": False,
|
|
76
|
+
}
|
|
77
|
+
# Note: cwd is not passed - run_agentic_crash uses prompt_file.parent as project root
|
|
78
|
+
|
|
79
|
+
res = run_agentic_crash(**call_args)
|
|
80
|
+
return _normalize_agentic_result(res)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
return False, f"Agentic crash handler failed: {e}", 0.0, "agentic-cli", []
|
|
8
83
|
|
|
9
84
|
# Use Rich for pretty printing to the console
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
console = Console(record=True)
|
|
13
|
-
rprint = console.print
|
|
85
|
+
try:
|
|
86
|
+
from rich.console import Console
|
|
87
|
+
console = Console(record=True)
|
|
88
|
+
rprint = console.print
|
|
89
|
+
except ImportError:
|
|
90
|
+
# Fallback if Rich is not available
|
|
91
|
+
def rprint(*args, **kwargs):
|
|
92
|
+
print(*args)
|
|
14
93
|
|
|
15
94
|
# Use relative import for internal modules
|
|
16
95
|
try:
|
|
17
|
-
# Attempt relative import for package context
|
|
18
96
|
from .fix_code_module_errors import fix_code_module_errors
|
|
19
97
|
except ImportError:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
98
|
+
try:
|
|
99
|
+
from fix_code_module_errors import fix_code_module_errors
|
|
100
|
+
except ImportError:
|
|
101
|
+
# Provide a stub that will fail gracefully
|
|
102
|
+
def fix_code_module_errors(**kwargs):
|
|
103
|
+
return (False, False, "", "", "Module not available", 0.0, None)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ProcessResult:
|
|
107
|
+
def __init__(self, returncode, stdout, stderr):
|
|
108
|
+
self.returncode = returncode
|
|
109
|
+
self.stdout = stdout
|
|
110
|
+
self.stderr = stderr
|
|
111
|
+
|
|
112
|
+
def run_process_with_output(cmd_args, timeout=300):
|
|
113
|
+
"""
|
|
114
|
+
Runs a process, streaming stdout/stderr to the console while capturing them.
|
|
115
|
+
Allows interaction via stdin.
|
|
116
|
+
|
|
117
|
+
Uses start_new_session=True to create a new process group, allowing us to
|
|
118
|
+
kill all child processes if the main process times out.
|
|
119
|
+
"""
|
|
120
|
+
import os
|
|
121
|
+
import signal
|
|
122
|
+
|
|
123
|
+
try:
|
|
124
|
+
proc = subprocess.Popen(
|
|
125
|
+
cmd_args,
|
|
126
|
+
stdin=subprocess.DEVNULL,
|
|
127
|
+
stdout=subprocess.PIPE,
|
|
128
|
+
stderr=subprocess.PIPE,
|
|
129
|
+
bufsize=0,
|
|
130
|
+
start_new_session=True # Create new process group for clean termination
|
|
131
|
+
)
|
|
132
|
+
except Exception as e:
|
|
133
|
+
return -1, "", str(e)
|
|
134
|
+
|
|
135
|
+
captured_stdout = []
|
|
136
|
+
captured_stderr = []
|
|
137
|
+
|
|
138
|
+
def stream_pipe(pipe, sink, capture_list):
|
|
139
|
+
while True:
|
|
140
|
+
try:
|
|
141
|
+
chunk = pipe.read(1)
|
|
142
|
+
if not chunk:
|
|
143
|
+
break
|
|
144
|
+
capture_list.append(chunk)
|
|
145
|
+
except (ValueError, IOError, OSError):
|
|
146
|
+
# OSError can occur when pipe is closed during read
|
|
147
|
+
break
|
|
148
|
+
|
|
149
|
+
t_out = threading.Thread(target=stream_pipe, args=(proc.stdout, sys.stdout, captured_stdout), daemon=True)
|
|
150
|
+
t_err = threading.Thread(target=stream_pipe, args=(proc.stderr, sys.stderr, captured_stderr), daemon=True)
|
|
151
|
+
|
|
152
|
+
t_out.start()
|
|
153
|
+
t_err.start()
|
|
154
|
+
|
|
155
|
+
timed_out = False
|
|
156
|
+
try:
|
|
157
|
+
proc.wait(timeout=timeout)
|
|
158
|
+
except subprocess.TimeoutExpired:
|
|
159
|
+
timed_out = True
|
|
160
|
+
captured_stderr.append(b"\n[Timeout]\n")
|
|
161
|
+
|
|
162
|
+
# Kill process and entire process group if needed
|
|
163
|
+
if timed_out or proc.returncode is None:
|
|
164
|
+
try:
|
|
165
|
+
# Kill entire process group to handle forked children
|
|
166
|
+
os.killpg(os.getpgid(proc.pid), signal.SIGKILL)
|
|
167
|
+
except (ProcessLookupError, OSError):
|
|
168
|
+
# Process group may already be dead
|
|
169
|
+
pass
|
|
170
|
+
try:
|
|
171
|
+
proc.kill()
|
|
172
|
+
proc.wait(timeout=5)
|
|
173
|
+
except Exception:
|
|
174
|
+
pass
|
|
175
|
+
|
|
176
|
+
# Close pipes to unblock reader threads
|
|
177
|
+
try:
|
|
178
|
+
proc.stdout.close()
|
|
179
|
+
except Exception:
|
|
180
|
+
pass
|
|
181
|
+
try:
|
|
182
|
+
proc.stderr.close()
|
|
183
|
+
except Exception:
|
|
184
|
+
pass
|
|
185
|
+
|
|
186
|
+
# Wait for threads with timeout to prevent indefinite hangs
|
|
187
|
+
THREAD_JOIN_TIMEOUT = 5 # seconds
|
|
188
|
+
t_out.join(timeout=THREAD_JOIN_TIMEOUT)
|
|
189
|
+
t_err.join(timeout=THREAD_JOIN_TIMEOUT)
|
|
190
|
+
|
|
191
|
+
# If threads are still alive after timeout, log it (they're daemon threads so won't block exit)
|
|
192
|
+
if t_out.is_alive() or t_err.is_alive():
|
|
193
|
+
captured_stderr.append(b"\n[Thread join timeout - some output may be lost]\n")
|
|
194
|
+
|
|
195
|
+
stdout_str = b"".join(captured_stdout).decode('utf-8', errors='replace')
|
|
196
|
+
stderr_str = b"".join(captured_stderr).decode('utf-8', errors='replace')
|
|
197
|
+
|
|
198
|
+
return proc.returncode if proc.returncode is not None else -1, stdout_str, stderr_str
|
|
28
199
|
|
|
29
200
|
|
|
30
201
|
def fix_code_loop(
|
|
@@ -38,6 +209,8 @@ def fix_code_loop(
|
|
|
38
209
|
error_log_file: str,
|
|
39
210
|
verbose: bool = False,
|
|
40
211
|
time: float = DEFAULT_TIME,
|
|
212
|
+
prompt_file: str = "",
|
|
213
|
+
agentic_fallback: bool = True,
|
|
41
214
|
) -> Tuple[bool, str, str, int, float, Optional[str]]:
|
|
42
215
|
"""
|
|
43
216
|
Attempts to fix errors in a code module through multiple iterations.
|
|
@@ -53,6 +226,8 @@ def fix_code_loop(
|
|
|
53
226
|
error_log_file: Path to the error log file.
|
|
54
227
|
verbose: Enable detailed logging (default: False).
|
|
55
228
|
time: Time limit for the LLM calls (default: DEFAULT_TIME).
|
|
229
|
+
prompt_file: Path to the prompt file.
|
|
230
|
+
agentic_fallback: Enable agentic fallback if the primary fix mechanism fails.
|
|
56
231
|
|
|
57
232
|
Returns:
|
|
58
233
|
Tuple containing the following in order:
|
|
@@ -63,15 +238,68 @@ def fix_code_loop(
|
|
|
63
238
|
- total_cost (float): Total cost of all fix attempts.
|
|
64
239
|
- model_name (str | None): Name of the LLM model used (or None if no LLM calls were made).
|
|
65
240
|
"""
|
|
66
|
-
#
|
|
241
|
+
# Handle default time if passed as None (though signature defaults to DEFAULT_TIME)
|
|
242
|
+
if time is None:
|
|
243
|
+
time = DEFAULT_TIME
|
|
244
|
+
|
|
245
|
+
# --- Start: File Checks ---
|
|
67
246
|
if not Path(code_file).is_file():
|
|
68
|
-
# Raising error for code file is acceptable as it's fundamental
|
|
69
247
|
raise FileNotFoundError(f"Code file not found: {code_file}")
|
|
70
248
|
if not Path(verification_program).is_file():
|
|
71
|
-
# Handle missing verification program gracefully as per test expectation
|
|
72
249
|
rprint(f"[bold red]Error: Verification program not found: {verification_program}[/bold red]")
|
|
73
250
|
return False, "", "", 0, 0.0, None
|
|
74
|
-
# --- End:
|
|
251
|
+
# --- End: File Checks ---
|
|
252
|
+
|
|
253
|
+
is_python = str(code_file).lower().endswith(".py")
|
|
254
|
+
if not is_python:
|
|
255
|
+
# For non-Python files, run the verification program to get an initial error state
|
|
256
|
+
rprint(f"[cyan]Non-Python target detected. Running verification program to get initial state...[/cyan]")
|
|
257
|
+
lang = get_language(os.path.splitext(code_file)[1])
|
|
258
|
+
verify_cmd = default_verify_cmd_for(lang, verification_program)
|
|
259
|
+
if not verify_cmd:
|
|
260
|
+
raise ValueError(f"No default verification command for language: {lang}")
|
|
261
|
+
|
|
262
|
+
verify_result = subprocess.run(verify_cmd, capture_output=True, text=True, shell=True)
|
|
263
|
+
pytest_output = (verify_result.stdout or "") + "\n" + (verify_result.stderr or "")
|
|
264
|
+
if verify_result.returncode != 0:
|
|
265
|
+
rprint("[cyan]Non-Python target failed initial verification. Triggering agentic fallback...[/cyan]")
|
|
266
|
+
error_log_path = Path(error_log_file)
|
|
267
|
+
error_log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
268
|
+
with open(error_log_path, "w") as f:
|
|
269
|
+
f.write(pytest_output)
|
|
270
|
+
|
|
271
|
+
success, _msg, agent_cost, agent_model, agent_changed_files = _safe_run_agentic_crash(
|
|
272
|
+
prompt_file=prompt_file,
|
|
273
|
+
code_file=code_file,
|
|
274
|
+
program_file=verification_program,
|
|
275
|
+
crash_log_file=error_log_file,
|
|
276
|
+
cwd=Path(prompt_file).parent if prompt_file else None
|
|
277
|
+
)
|
|
278
|
+
final_program = ""
|
|
279
|
+
final_code = ""
|
|
280
|
+
try:
|
|
281
|
+
with open(verification_program, "r") as f:
|
|
282
|
+
final_program = f.read()
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
try:
|
|
286
|
+
with open(code_file, "r") as f:
|
|
287
|
+
final_code = f.read()
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
return success, final_program, final_code, 1, agent_cost, agent_model
|
|
291
|
+
else:
|
|
292
|
+
rprint("[green]Non-Python tests passed. No fix needed.[/green]")
|
|
293
|
+
try:
|
|
294
|
+
final_program = ""
|
|
295
|
+
final_code = ""
|
|
296
|
+
with open(verification_program, "r") as f:
|
|
297
|
+
final_program = f.read()
|
|
298
|
+
with open(code_file, "r") as f:
|
|
299
|
+
final_code = f.read()
|
|
300
|
+
except Exception as e:
|
|
301
|
+
rprint(f"[yellow]Warning: Could not read final files: {e}[/yellow]")
|
|
302
|
+
return True, final_program, final_code, 0, 0.0, "N/A"
|
|
75
303
|
|
|
76
304
|
# Step 1: Remove existing error log file
|
|
77
305
|
try:
|
|
@@ -83,14 +311,13 @@ def fix_code_loop(
|
|
|
83
311
|
rprint(f"Error log file not found, no need to remove: {error_log_file}")
|
|
84
312
|
except OSError as e:
|
|
85
313
|
rprint(f"[bold red]Error removing log file {error_log_file}: {e}[/bold red]")
|
|
86
|
-
# Decide if this is fatal or not; for now, we continue
|
|
87
314
|
|
|
88
315
|
# Step 2: Initialize variables
|
|
89
316
|
attempts = 0
|
|
90
317
|
total_cost = 0.0
|
|
91
318
|
success = False
|
|
92
319
|
model_name = None
|
|
93
|
-
history_log = "<history>\n"
|
|
320
|
+
history_log = "<history>\n"
|
|
94
321
|
|
|
95
322
|
# Create initial backups before any modifications
|
|
96
323
|
code_file_path = Path(code_file)
|
|
@@ -105,33 +332,41 @@ def fix_code_loop(
|
|
|
105
332
|
rprint(f"Created initial backups: {original_code_backup}, {original_program_backup}")
|
|
106
333
|
except Exception as e:
|
|
107
334
|
rprint(f"[bold red]Error creating initial backups: {e}[/bold red]")
|
|
108
|
-
# If backups fail, we cannot guarantee restoration. Return failure.
|
|
109
335
|
return False, "", "", 0, 0.0, None
|
|
110
336
|
|
|
337
|
+
# Initialize process for scope
|
|
338
|
+
process = None
|
|
111
339
|
|
|
112
340
|
# Step 3: Enter the fixing loop
|
|
113
341
|
while attempts < max_attempts and total_cost <= budget:
|
|
114
|
-
current_attempt
|
|
115
|
-
|
|
116
|
-
|
|
342
|
+
# current_attempt is used for logging the current iteration number
|
|
343
|
+
current_iteration_number = attempts + 1
|
|
344
|
+
rprint(f"\n[bold cyan]Attempt {current_iteration_number}/{max_attempts}...[/bold cyan]")
|
|
345
|
+
attempt_log_entry = f' <attempt number="{current_iteration_number}">\n'
|
|
117
346
|
|
|
118
347
|
# b. Run the verification program
|
|
119
348
|
if verbose:
|
|
120
349
|
rprint(f"Running verification: {sys.executable} {verification_program}")
|
|
121
350
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
351
|
+
try:
|
|
352
|
+
returncode, stdout, stderr = run_process_with_output(
|
|
353
|
+
[sys.executable, verification_program],
|
|
354
|
+
timeout=300
|
|
355
|
+
)
|
|
356
|
+
process = ProcessResult(returncode, stdout, stderr)
|
|
357
|
+
|
|
358
|
+
verification_status = f"Success (Return Code: {process.returncode})" if process.returncode == 0 else f"Failure (Return Code: {process.returncode})"
|
|
359
|
+
verification_output = process.stdout or "[No standard output]"
|
|
360
|
+
verification_error = process.stderr or "[No standard error]"
|
|
361
|
+
except Exception as e:
|
|
362
|
+
verification_status = f"Failure (Exception: {e})"
|
|
363
|
+
verification_output = "[Exception occurred]"
|
|
364
|
+
verification_error = str(e)
|
|
365
|
+
process = ProcessResult(-1, "", str(e))
|
|
128
366
|
|
|
129
|
-
verification_status = f"Success (Return Code: {process.returncode})" if process.returncode == 0 else f"Failure (Return Code: {process.returncode})"
|
|
130
|
-
verification_output = process.stdout or "[No standard output]"
|
|
131
|
-
verification_error = process.stderr or "[No standard error]"
|
|
132
367
|
|
|
133
368
|
# Add verification results to the attempt log entry
|
|
134
|
-
attempt_log_entry += f"""
|
|
369
|
+
attempt_log_entry += f"""
|
|
135
370
|
<verification>
|
|
136
371
|
<status>{verification_status}</status>
|
|
137
372
|
<output><![CDATA[
|
|
@@ -155,7 +390,7 @@ def fix_code_loop(
|
|
|
155
390
|
current_error_message = verification_error # Use stderr as the primary error source
|
|
156
391
|
|
|
157
392
|
# Add current error to the attempt log entry
|
|
158
|
-
attempt_log_entry += f"""
|
|
393
|
+
attempt_log_entry += f"""
|
|
159
394
|
<current_error><![CDATA[
|
|
160
395
|
{current_error_message}
|
|
161
396
|
]]></current_error>
|
|
@@ -164,13 +399,13 @@ def fix_code_loop(
|
|
|
164
399
|
# Check budget *before* making the potentially expensive LLM call for the next attempt
|
|
165
400
|
# (Only check if cost > 0 to avoid breaking before first attempt if budget is 0)
|
|
166
401
|
if total_cost > budget and attempts > 0: # Check after first attempt cost is added
|
|
167
|
-
rprint(f"[bold yellow]Budget exceeded (${total_cost:.4f} > ${budget:.4f}) before attempt {
|
|
402
|
+
rprint(f"[bold yellow]Budget exceeded (${total_cost:.4f} > ${budget:.4f}) before attempt {current_iteration_number}. Stopping.[/bold yellow]")
|
|
168
403
|
history_log += attempt_log_entry + " <error>Budget exceeded before LLM call</error>\n </attempt>\n"
|
|
169
404
|
break
|
|
170
405
|
|
|
171
406
|
# Check max attempts *before* the LLM call for this attempt
|
|
172
407
|
if attempts >= max_attempts:
|
|
173
|
-
rprint(f"[bold red]Maximum attempts ({max_attempts}) reached before attempt {
|
|
408
|
+
rprint(f"[bold red]Maximum attempts ({max_attempts}) reached before attempt {current_iteration_number}. Stopping.[/bold red]")
|
|
174
409
|
# No need to add to history here, loop condition handles it
|
|
175
410
|
break
|
|
176
411
|
|
|
@@ -178,16 +413,16 @@ def fix_code_loop(
|
|
|
178
413
|
# Create backup copies for this iteration BEFORE calling LLM
|
|
179
414
|
code_base, code_ext = os.path.splitext(code_file)
|
|
180
415
|
program_base, program_ext = os.path.splitext(verification_program)
|
|
181
|
-
code_backup_path = f"{code_base}_{
|
|
182
|
-
program_backup_path = f"{program_base}_{
|
|
416
|
+
code_backup_path = f"{code_base}_{current_iteration_number}{code_ext}"
|
|
417
|
+
program_backup_path = f"{program_base}_{current_iteration_number}{program_ext}"
|
|
183
418
|
|
|
184
419
|
try:
|
|
185
420
|
shutil.copy2(code_file, code_backup_path)
|
|
186
421
|
shutil.copy2(verification_program, program_backup_path)
|
|
187
422
|
if verbose:
|
|
188
|
-
rprint(f"Created backups for attempt {
|
|
423
|
+
rprint(f"Created backups for attempt {current_iteration_number}: {code_backup_path}, {program_backup_path}")
|
|
189
424
|
except Exception as e:
|
|
190
|
-
rprint(f"[bold red]Error creating backups for attempt {
|
|
425
|
+
rprint(f"[bold red]Error creating backups for attempt {current_iteration_number}: {e}[/bold red]")
|
|
191
426
|
history_log += attempt_log_entry + f" <error>Failed to create backups: {e}</error>\n </attempt>\n"
|
|
192
427
|
break # Cannot proceed reliably without backups
|
|
193
428
|
|
|
@@ -233,17 +468,17 @@ def fix_code_loop(
|
|
|
233
468
|
rprint(f"[bold red]Error calling fix_code_module_errors: {e}[/bold red]")
|
|
234
469
|
cost = 0.0 # Assume no cost if the call failed
|
|
235
470
|
# Add error to the attempt log entry
|
|
236
|
-
attempt_log_entry += f"""
|
|
471
|
+
attempt_log_entry += f"""
|
|
237
472
|
<fixing>
|
|
238
473
|
<error>LLM call failed: {e}</error>
|
|
239
474
|
</fixing>
|
|
240
475
|
"""
|
|
241
|
-
# Continue to the next attempt or break if limits reached? Let's break.
|
|
242
476
|
history_log += attempt_log_entry + " </attempt>\n" # Log the attempt with the LLM error
|
|
477
|
+
attempts += 1 # Increment attempts even if LLM call failed
|
|
243
478
|
break # Stop if the fixing mechanism itself fails
|
|
244
479
|
|
|
245
480
|
# Add fixing results to the attempt log entry
|
|
246
|
-
attempt_log_entry += f"""
|
|
481
|
+
attempt_log_entry += f"""
|
|
247
482
|
<fixing>
|
|
248
483
|
<llm_analysis><![CDATA[
|
|
249
484
|
{program_code_fix or "[No analysis provided]"}
|
|
@@ -269,11 +504,13 @@ def fix_code_loop(
|
|
|
269
504
|
rprint(f"[bold red]Error writing to log file {error_log_file}: {e}[/bold red]")
|
|
270
505
|
|
|
271
506
|
|
|
272
|
-
# Add cost and
|
|
507
|
+
# Add cost and increment attempt counter (as per fix report) *before* checking budget
|
|
273
508
|
total_cost += cost
|
|
509
|
+
attempts += 1 # Moved this line here as per fix report
|
|
274
510
|
rprint(f"Attempt Cost: ${cost:.4f}, Total Cost: ${total_cost:.4f}, Budget: ${budget:.4f}")
|
|
511
|
+
|
|
275
512
|
if total_cost > budget:
|
|
276
|
-
rprint(f"[bold yellow]Budget exceeded (${total_cost:.4f} > ${budget:.4f}) after attempt {
|
|
513
|
+
rprint(f"[bold yellow]Budget exceeded (${total_cost:.4f} > ${budget:.4f}) after attempt {attempts}. Stopping.[/bold yellow]")
|
|
277
514
|
break # Stop loop
|
|
278
515
|
|
|
279
516
|
# If LLM suggested no changes but verification failed, stop to prevent loops
|
|
@@ -295,8 +532,7 @@ def fix_code_loop(
|
|
|
295
532
|
success = False # Mark as failed if we can't write updates
|
|
296
533
|
break # Stop if we cannot apply fixes
|
|
297
534
|
|
|
298
|
-
#
|
|
299
|
-
attempts += 1
|
|
535
|
+
# The original 'attempts += 1' was here. It has been moved earlier.
|
|
300
536
|
|
|
301
537
|
# Check if max attempts reached after incrementing (for the next loop iteration check)
|
|
302
538
|
if attempts >= max_attempts:
|
|
@@ -358,39 +594,57 @@ def fix_code_loop(
|
|
|
358
594
|
rprint(f"[bold red]Final write to log file {error_log_file} failed: {e}[/bold red]")
|
|
359
595
|
|
|
360
596
|
# Determine final number of attempts for reporting
|
|
361
|
-
#
|
|
362
|
-
# If loop finished by failure (budget, max_attempts, no_change_needed, error),
|
|
363
|
-
# the number of attempts *initiated* is 'attempts + 1' unless max_attempts was exactly hit.
|
|
364
|
-
# The tests seem to expect the number of attempts *initiated*.
|
|
365
|
-
# Let's refine the calculation slightly for clarity.
|
|
366
|
-
# 'attempts' holds the count of *completed* loops (0-indexed).
|
|
367
|
-
# 'current_attempt' holds the user-facing number (1-indexed) of the loop *currently running or just finished*.
|
|
597
|
+
# The 'attempts' variable correctly counts the number of LLM fix cycles that were initiated.
|
|
368
598
|
final_attempts_reported = attempts
|
|
369
|
-
if not success:
|
|
370
|
-
# If failure occurred, it happened *during* or *after* the 'current_attempt' was initiated.
|
|
371
|
-
# If loop broke due to budget/no_change/error, current_attempt reflects the attempt number where failure occurred.
|
|
372
|
-
# If loop broke because attempts >= max_attempts, the last valid value for current_attempt was max_attempts.
|
|
373
|
-
# The number of attempts *tried* is current_attempt.
|
|
374
|
-
# However, the tests seem aligned with the previous logic. Let's stick to it unless further tests fail.
|
|
375
|
-
final_attempts_reported = attempts if success else (attempts + 1 if attempts < max_attempts and process.returncode != 0 else attempts)
|
|
376
|
-
# Re-evaluating the test logic:
|
|
377
|
-
# - Budget test: attempts=1 when loop breaks, expects 2. (attempts+1) -> 2. Correct.
|
|
378
|
-
# - Max attempts test: attempts=0 when loop breaks (no change), max_attempts=2, expects <=2. (attempts+1) -> 1. Correct.
|
|
379
|
-
# - If max_attempts=2 was reached *normally* (failed attempt 1, failed attempt 2), attempts would be 2.
|
|
380
|
-
# The logic `attempts + 1 if attempts < max_attempts else attempts` would return 2. Correct.
|
|
381
|
-
# Let's simplify the return calculation based on 'attempts' which counts completed loops.
|
|
382
|
-
final_attempts_reported = attempts # Number of fully completed fix cycles
|
|
383
|
-
if not success and process and process.returncode != 0: # If we failed after at least one verification run
|
|
384
|
-
# Count the final failed attempt unless success was achieved on the very last possible attempt
|
|
385
|
-
if attempts < max_attempts:
|
|
386
|
-
final_attempts_reported += 1
|
|
387
599
|
|
|
600
|
+
if not success and agentic_fallback:
|
|
601
|
+
# Ensure error_log_file exists before calling agentic fix
|
|
602
|
+
try:
|
|
603
|
+
if not os.path.exists(error_log_file) or os.path.getsize(error_log_file) == 0:
|
|
604
|
+
# Write minimal error log for agentic fix
|
|
605
|
+
error_log_path = Path(error_log_file)
|
|
606
|
+
error_log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
607
|
+
with open(error_log_path, "w") as elog:
|
|
608
|
+
if process:
|
|
609
|
+
elog.write(f"Verification failed with return code: {process.returncode}\n")
|
|
610
|
+
if process.stdout:
|
|
611
|
+
elog.write(f"\nStdout:\n{process.stdout}\n")
|
|
612
|
+
if process.stderr:
|
|
613
|
+
elog.write(f"\nStderr:\n{process.stderr}\n")
|
|
614
|
+
else:
|
|
615
|
+
elog.write("No error information available\n")
|
|
616
|
+
except Exception as e:
|
|
617
|
+
rprint(f"[yellow]Warning: Could not write error log before agentic fallback: {e}[/yellow]")
|
|
618
|
+
|
|
619
|
+
rprint(f"[cyan]Attempting agentic fallback (prompt_file={prompt_file!r})...[/cyan]")
|
|
620
|
+
agent_success, agent_msg, agent_cost, agent_model, agent_changed_files = _safe_run_agentic_crash(
|
|
621
|
+
prompt_file=prompt_file,
|
|
622
|
+
code_file=code_file,
|
|
623
|
+
program_file=verification_program,
|
|
624
|
+
crash_log_file=error_log_file,
|
|
625
|
+
cwd=Path(prompt_file).parent if prompt_file else None
|
|
626
|
+
)
|
|
627
|
+
total_cost += agent_cost
|
|
628
|
+
if not agent_success:
|
|
629
|
+
rprint(f"[bold red]Agentic fallback failed: {agent_msg}[/bold red]")
|
|
630
|
+
if agent_changed_files:
|
|
631
|
+
rprint(f"[cyan]Agent modified {len(agent_changed_files)} file(s):[/cyan]")
|
|
632
|
+
for f in agent_changed_files:
|
|
633
|
+
rprint(f" • {f}")
|
|
634
|
+
if agent_success:
|
|
635
|
+
model_name = agent_model or model_name
|
|
636
|
+
try:
|
|
637
|
+
final_code_content = Path(code_file).read_text(encoding='utf-8')
|
|
638
|
+
final_program_content = Path(verification_program).read_text(encoding='utf-8')
|
|
639
|
+
except Exception as e:
|
|
640
|
+
rprint(f"[yellow]Warning: Could not read files after successful agentic fix: {e}[/yellow]")
|
|
641
|
+
success = True
|
|
388
642
|
|
|
389
643
|
return (
|
|
390
644
|
success,
|
|
391
645
|
final_program_content,
|
|
392
646
|
final_code_content,
|
|
393
|
-
final_attempts_reported,
|
|
647
|
+
final_attempts_reported,
|
|
394
648
|
total_cost,
|
|
395
649
|
model_name,
|
|
396
650
|
)
|
|
@@ -519,4 +773,4 @@ sys.exit(0) # Exit with zero code for success
|
|
|
519
773
|
# for f in Path(".").glob("dummy_verify_*.py"): # Remove attempt backups like dummy_verify_1.py
|
|
520
774
|
# if "_original_backup" not in f.name: os.remove(f)
|
|
521
775
|
# except OSError as e:
|
|
522
|
-
# print(f"Error cleaning up dummy files: {e}")
|
|
776
|
+
# print(f"Error cleaning up dummy files: {e}")
|