ata-coder 2.4.2__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.
- ata_coder/__init__.py +1 -0
- ata_coder/agent.py +874 -0
- ata_coder/agent_compact.py +190 -0
- ata_coder/agent_controller.py +218 -0
- ata_coder/agent_extension.py +69 -0
- ata_coder/agent_routing.py +105 -0
- ata_coder/agent_subsystems.py +72 -0
- ata_coder/agent_tools.py +318 -0
- ata_coder/agent_undo.py +63 -0
- ata_coder/anthropic_client.py +465 -0
- ata_coder/change_tracker.py +368 -0
- ata_coder/clawd_integration.py +574 -0
- ata_coder/commands/__init__.py +128 -0
- ata_coder/commands/_core.py +184 -0
- ata_coder/commands/_safety.py +95 -0
- ata_coder/commands/_settings.py +241 -0
- ata_coder/commands/_workflow.py +451 -0
- ata_coder/commands.py +974 -0
- ata_coder/config.py +257 -0
- ata_coder/core/__init__.py +35 -0
- ata_coder/core/events.py +73 -0
- ata_coder/core/queue.py +85 -0
- ata_coder/core/state.py +17 -0
- ata_coder/event_queue.py +5 -0
- ata_coder/extension.py +654 -0
- ata_coder/extensions/__init__.py +1 -0
- ata_coder/extensions/hello_skill.py +47 -0
- ata_coder/fool_proof.py +295 -0
- ata_coder/git_workflow.py +371 -0
- ata_coder/gui.py +511 -0
- ata_coder/llm_client.py +543 -0
- ata_coder/main.py +814 -0
- ata_coder/mcp_client.py +1095 -0
- ata_coder/memory.py +539 -0
- ata_coder/model_registry.py +134 -0
- ata_coder/model_router.py +105 -0
- ata_coder/permissions.py +274 -0
- ata_coder/privilege.py +464 -0
- ata_coder/project.py +273 -0
- ata_coder/prompt_template.py +423 -0
- ata_coder/prompts/auto-mode.md +7 -0
- ata_coder/prompts/coding-rules.md +40 -0
- ata_coder/prompts/execution-guardrails.md +14 -0
- ata_coder/prompts/memory-system.md +24 -0
- ata_coder/prompts/output-style.md +23 -0
- ata_coder/prompts/safety.md +17 -0
- ata_coder/prompts/slash-commands.md +24 -0
- ata_coder/prompts/sub-agents.md +38 -0
- ata_coder/prompts/system-reminders.md +17 -0
- ata_coder/prompts/system.md +105 -0
- ata_coder/prompts/tool-policy.md +46 -0
- ata_coder/repl_theme.py +99 -0
- ata_coder/repl_tracker.py +89 -0
- ata_coder/repl_ui.py +1214 -0
- ata_coder/safety_guard.py +434 -0
- ata_coder/self_correct.py +346 -0
- ata_coder/server.py +882 -0
- ata_coder/server_session.py +159 -0
- ata_coder/server_shell.py +129 -0
- ata_coder/session.py +431 -0
- ata_coder/settings.py +439 -0
- ata_coder/setup_wizard.py +136 -0
- ata_coder/skill_extension.py +92 -0
- ata_coder/skills/architect/SKILL.md +42 -0
- ata_coder/skills/code-reviewer/SKILL.md +37 -0
- ata_coder/skills/codecraft/SKILL.md +452 -0
- ata_coder/skills/debugger/SKILL.md +45 -0
- ata_coder/skills/doc-writer/SKILL.md +36 -0
- ata_coder/skills/general-coder/SKILL.md +76 -0
- ata_coder/skills/math-calculator/README.md +40 -0
- ata_coder/skills/math-calculator/SKILL.md +59 -0
- ata_coder/skills/math-calculator/handler.py +103 -0
- ata_coder/skills/math-calculator/prompts/system.md +8 -0
- ata_coder/skills/math-calculator/requirements.txt +2 -0
- ata_coder/skills/math-calculator/resources/constants.json +8 -0
- ata_coder/skills/math-calculator/tests/test_handler.py +53 -0
- ata_coder/skills/security-auditor/SKILL.md +40 -0
- ata_coder/skills/test-writer/SKILL.md +36 -0
- ata_coder/skills/weather-skill/README.md +45 -0
- ata_coder/skills/weather-skill/handler.py +76 -0
- ata_coder/skills/weather-skill/manifest.json +48 -0
- ata_coder/skills/weather-skill/prompts/system_prompt.txt +9 -0
- ata_coder/skills/weather-skill/prompts/user_prompt_template.txt +3 -0
- ata_coder/skills/weather-skill/requirements.txt +1 -0
- ata_coder/skills/weather-skill/resources/city_list.json +17 -0
- ata_coder/skills/weather-skill/resources/error_messages.json +7 -0
- ata_coder/skills/weather-skill/tests/test_handler.py +28 -0
- ata_coder/skills/weather-skill/weather_utils.py +50 -0
- ata_coder/skills.py +1014 -0
- ata_coder/sub_agent.py +273 -0
- ata_coder/sub_agent_manager.py +203 -0
- ata_coder/system_prompt_builder.py +146 -0
- ata_coder/task_planner.py +391 -0
- ata_coder/terminal.py +318 -0
- ata_coder/test_runner.py +219 -0
- ata_coder/thread_supervisor.py +195 -0
- ata_coder/tool_defs.py +335 -0
- ata_coder/tools/__init__.py +11 -0
- ata_coder/tools/definitions.py +335 -0
- ata_coder/tools/executor.py +1036 -0
- ata_coder/tools/result.py +26 -0
- ata_coder/tools/subagent.py +332 -0
- ata_coder/tools/web.py +361 -0
- ata_coder/tools.py +1576 -0
- ata_coder/types.py +92 -0
- ata_coder/utils.py +113 -0
- ata_coder/web/css/style.css +180 -0
- ata_coder/web/index.html +84 -0
- ata_coder/web/js/app.js +489 -0
- ata_coder/web/package-lock.json +25 -0
- ata_coder/web/package.json +10 -0
- ata_coder/web/tsconfig.json +13 -0
- ata_coder-2.4.2.dist-info/METADATA +799 -0
- ata_coder-2.4.2.dist-info/RECORD +118 -0
- ata_coder-2.4.2.dist-info/WHEEL +5 -0
- ata_coder-2.4.2.dist-info/entry_points.txt +2 -0
- ata_coder-2.4.2.dist-info/licenses/LICENSE +21 -0
- ata_coder-2.4.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Self-Correction Loop — automatic error recovery.
|
|
3
|
+
|
|
4
|
+
When a tool returns an error, the agent:
|
|
5
|
+
1. Reads the error message
|
|
6
|
+
2. Diagnoses the root cause (common patterns)
|
|
7
|
+
3. Suggests a fix
|
|
8
|
+
4. Retries with corrected arguments (max 3 attempts)
|
|
9
|
+
5. Learns from failures within the session
|
|
10
|
+
|
|
11
|
+
Common error patterns detected:
|
|
12
|
+
- File not found → suggest reading directory first
|
|
13
|
+
- old_string not found → suggest reading file again
|
|
14
|
+
- Command not found → suggest alternative or install
|
|
15
|
+
- Permission denied → suggest elevated privileges
|
|
16
|
+
- Syntax error → suggest reading linter output
|
|
17
|
+
- Import error → suggest installing missing package
|
|
18
|
+
- Network error → suggest retry or check connectivity
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
import re
|
|
23
|
+
import time
|
|
24
|
+
from dataclasses import dataclass
|
|
25
|
+
from typing import Any, Callable
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
31
|
+
# Error pattern → diagnosis + fix suggestion
|
|
32
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class ErrorDiagnosis:
|
|
36
|
+
pattern: str
|
|
37
|
+
diagnosis: str
|
|
38
|
+
fix_suggestion: str
|
|
39
|
+
retry_strategy: str # "auto_fix" | "read_first" | "ask_user" | "skip"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# Common error patterns with auto-fix strategies.
|
|
43
|
+
# NOTE: "old_string not found" and "SyntaxError" removed — AST-based
|
|
44
|
+
# editing (libcst) eliminates these failures deterministically.
|
|
45
|
+
ERROR_PATTERNS: list[ErrorDiagnosis] = [
|
|
46
|
+
ErrorDiagnosis(
|
|
47
|
+
pattern=r"File not found",
|
|
48
|
+
diagnosis="The specified file does not exist at the given path.",
|
|
49
|
+
fix_suggestion="Check the file path. List the directory to find the correct path, or create the file first.",
|
|
50
|
+
retry_strategy="read_first",
|
|
51
|
+
),
|
|
52
|
+
ErrorDiagnosis(
|
|
53
|
+
pattern=r"Permission denied|EACCES|Access is denied",
|
|
54
|
+
diagnosis="The current user does not have permission to access this file or run this command.",
|
|
55
|
+
fix_suggestion="Check file permissions. Consider using elevated privileges or choosing a different path.",
|
|
56
|
+
retry_strategy="ask_user",
|
|
57
|
+
),
|
|
58
|
+
ErrorDiagnosis(
|
|
59
|
+
pattern=r"command not found|not recognized|No such file or directory",
|
|
60
|
+
diagnosis="The shell command is not available on this system.",
|
|
61
|
+
fix_suggestion="Check the command name. Consider installing the required tool or using an alternative.",
|
|
62
|
+
retry_strategy="ask_user",
|
|
63
|
+
),
|
|
64
|
+
ErrorDiagnosis(
|
|
65
|
+
pattern=r"ModuleNotFoundError|ImportError|No module named",
|
|
66
|
+
diagnosis="A required Python module is not installed.",
|
|
67
|
+
fix_suggestion="Install the missing module with pip, or check the import path.",
|
|
68
|
+
retry_strategy="auto_fix",
|
|
69
|
+
),
|
|
70
|
+
ErrorDiagnosis(
|
|
71
|
+
pattern=r"TypeError|TypeError:|wrong type|expected .* but got",
|
|
72
|
+
diagnosis="A function received an argument of the wrong type.",
|
|
73
|
+
fix_suggestion="Check the types of arguments being passed. Add type conversions if needed.",
|
|
74
|
+
retry_strategy="auto_fix",
|
|
75
|
+
),
|
|
76
|
+
ErrorDiagnosis(
|
|
77
|
+
pattern=r"timeout|timed out|Timed out",
|
|
78
|
+
diagnosis="The operation took too long and timed out.",
|
|
79
|
+
fix_suggestion="Increase the timeout, break the task into smaller pieces, or check network connectivity.",
|
|
80
|
+
retry_strategy="auto_fix",
|
|
81
|
+
),
|
|
82
|
+
ErrorDiagnosis(
|
|
83
|
+
pattern=r"rate limit|too many requests|429",
|
|
84
|
+
diagnosis="API rate limit has been exceeded.",
|
|
85
|
+
fix_suggestion="Wait and retry with exponential backoff. The client already handles this automatically.",
|
|
86
|
+
retry_strategy="auto_fix",
|
|
87
|
+
),
|
|
88
|
+
ErrorDiagnosis(
|
|
89
|
+
pattern=r"connection refused|ConnectionError|NetworkError|could not connect",
|
|
90
|
+
diagnosis="Cannot connect to the remote service.",
|
|
91
|
+
fix_suggestion="Check that the service is running. Verify the URL and port. Check network/firewall.",
|
|
92
|
+
retry_strategy="ask_user",
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
98
|
+
# Retry tracker
|
|
99
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
100
|
+
|
|
101
|
+
@dataclass
|
|
102
|
+
class RetryAttempt:
|
|
103
|
+
attempt: int
|
|
104
|
+
tool_name: str
|
|
105
|
+
original_args: dict
|
|
106
|
+
error_message: str
|
|
107
|
+
diagnosis: ErrorDiagnosis | None
|
|
108
|
+
fixed_args: dict | None
|
|
109
|
+
success: bool = False
|
|
110
|
+
timestamp: float = 0.0
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
114
|
+
# Self-Correction Engine
|
|
115
|
+
# ═══════════════════════════════════════════════════════════════════════════════
|
|
116
|
+
|
|
117
|
+
class SelfCorrectionEngine:
|
|
118
|
+
"""
|
|
119
|
+
Automatically diagnoses and recovers from tool execution errors.
|
|
120
|
+
|
|
121
|
+
Usage:
|
|
122
|
+
engine = SelfCorrectionEngine(max_retries=1) # AST editing makes retries rarely needed
|
|
123
|
+
|
|
124
|
+
result = execute_tool(name, args)
|
|
125
|
+
if not result.success:
|
|
126
|
+
diagnosis = engine.diagnose(result.error)
|
|
127
|
+
if diagnosis.retry_strategy == "auto_fix":
|
|
128
|
+
fixed_args = engine.suggest_fix(name, args, diagnosis)
|
|
129
|
+
result = execute_tool(name, fixed_args) # retry
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
def __init__(self, max_retries: int = 3):
|
|
133
|
+
self.max_retries = max_retries
|
|
134
|
+
self._attempts: list[RetryAttempt] = []
|
|
135
|
+
self._learned_patterns: dict[str, str] = {} # session learning
|
|
136
|
+
|
|
137
|
+
# ── Diagnosis ────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
def diagnose(self, error_message: str, tool_name: str = "",
|
|
140
|
+
arguments: dict | None = None) -> ErrorDiagnosis | None:
|
|
141
|
+
"""
|
|
142
|
+
Analyze an error message and return a diagnosis with fix strategy.
|
|
143
|
+
"""
|
|
144
|
+
# Check known patterns
|
|
145
|
+
for pattern in ERROR_PATTERNS:
|
|
146
|
+
if re.search(pattern.pattern, error_message, re.IGNORECASE):
|
|
147
|
+
logger.info("Diagnosed: %s → %s", pattern.pattern[:40], pattern.retry_strategy)
|
|
148
|
+
return pattern
|
|
149
|
+
|
|
150
|
+
# Check learned patterns from this session
|
|
151
|
+
for learned_pattern, fix in self._learned_patterns.items():
|
|
152
|
+
if re.search(learned_pattern, error_message, re.IGNORECASE):
|
|
153
|
+
return ErrorDiagnosis(
|
|
154
|
+
pattern=learned_pattern,
|
|
155
|
+
diagnosis="Learned from previous failure in this session",
|
|
156
|
+
fix_suggestion=fix,
|
|
157
|
+
retry_strategy="auto_fix",
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Unknown error — generic diagnosis
|
|
161
|
+
return ErrorDiagnosis(
|
|
162
|
+
pattern="unknown",
|
|
163
|
+
diagnosis=f"Unexpected error during {tool_name}",
|
|
164
|
+
fix_suggestion="Read the error carefully. Check the file, path, and arguments. Try a different approach.",
|
|
165
|
+
retry_strategy="ask_user",
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
# ── Fix suggestion ──────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
def suggest_fix(self, tool_name: str, arguments: dict,
|
|
171
|
+
diagnosis: ErrorDiagnosis,
|
|
172
|
+
error_message: str = "") -> dict | None:
|
|
173
|
+
"""
|
|
174
|
+
Suggest corrected arguments based on the diagnosis.
|
|
175
|
+
*error_message* is the original error text (used to extract module
|
|
176
|
+
names, paths, etc.). Without it the fixer has nothing to work with.
|
|
177
|
+
Returns modified arguments dict, or None if no auto-fix is possible.
|
|
178
|
+
"""
|
|
179
|
+
if diagnosis.retry_strategy == "ask_user":
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
fixed = dict(arguments)
|
|
183
|
+
|
|
184
|
+
if tool_name == "edit_file":
|
|
185
|
+
if "old_string not found" in diagnosis.pattern:
|
|
186
|
+
# Can't auto-fix without re-reading the file
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
elif tool_name == "read_file":
|
|
190
|
+
if "File not found" in diagnosis.pattern:
|
|
191
|
+
# Can't auto-fix — the file doesn't exist.
|
|
192
|
+
# Always return None to avoid a retry loop with the same args.
|
|
193
|
+
return None
|
|
194
|
+
|
|
195
|
+
elif tool_name == "run_shell":
|
|
196
|
+
cmd = arguments.get("command", "")
|
|
197
|
+
if "ModuleNotFoundError" in diagnosis.pattern or "No module named" in diagnosis.pattern:
|
|
198
|
+
# Extract module name from the actual error message.
|
|
199
|
+
# Match hyphenated names like 'python-dateutil' too.
|
|
200
|
+
match = re.search(r"No module named '([\w\-\.]+)'", error_message)
|
|
201
|
+
if not match:
|
|
202
|
+
match = re.search(r"No module named '([\w\-\.]+)'", cmd)
|
|
203
|
+
if match:
|
|
204
|
+
module = match.group(1)
|
|
205
|
+
fixed["command"] = f"pip install {module} && {cmd}"
|
|
206
|
+
return fixed
|
|
207
|
+
|
|
208
|
+
if "command not found" in diagnosis.pattern:
|
|
209
|
+
first_word = cmd.strip().split()[0] if cmd.strip() else ""
|
|
210
|
+
# Only suggest python -m for well-known tools that have Python entry points
|
|
211
|
+
_python_runnable = {"pip", "pytest", "mypy", "ruff", "black", "isort",
|
|
212
|
+
"uvicorn", "gunicorn", "jupyter", "coverage"}
|
|
213
|
+
if first_word in _python_runnable:
|
|
214
|
+
fixed["command"] = f"python -m {cmd}"
|
|
215
|
+
return fixed
|
|
216
|
+
# For other commands (git, npm, etc.), can't auto-fix
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
if "timeout" in diagnosis.pattern:
|
|
220
|
+
fixed["timeout"] = arguments.get("timeout", 120) * 2
|
|
221
|
+
return fixed
|
|
222
|
+
|
|
223
|
+
return fixed # return original args for retry
|
|
224
|
+
|
|
225
|
+
# ── Retry management ────────────────────────────────────────────────
|
|
226
|
+
|
|
227
|
+
def should_retry(self, tool_name: str, arguments: dict) -> bool:
|
|
228
|
+
"""Check if we should retry this tool call."""
|
|
229
|
+
count = sum(
|
|
230
|
+
1 for a in self._attempts
|
|
231
|
+
if a.tool_name == tool_name
|
|
232
|
+
and a.original_args == arguments
|
|
233
|
+
)
|
|
234
|
+
return count < self.max_retries
|
|
235
|
+
|
|
236
|
+
def record_attempt(self, tool_name: str, original_args: dict,
|
|
237
|
+
error: str, diagnosis: ErrorDiagnosis | None,
|
|
238
|
+
fixed_args: dict | None, success: bool = False):
|
|
239
|
+
"""Record a retry attempt for tracking."""
|
|
240
|
+
attempt = RetryAttempt(
|
|
241
|
+
attempt=len(self._attempts) + 1,
|
|
242
|
+
tool_name=tool_name,
|
|
243
|
+
original_args=original_args,
|
|
244
|
+
error_message=error,
|
|
245
|
+
diagnosis=diagnosis,
|
|
246
|
+
fixed_args=fixed_args,
|
|
247
|
+
success=success,
|
|
248
|
+
timestamp=time.time(),
|
|
249
|
+
)
|
|
250
|
+
self._attempts.append(attempt)
|
|
251
|
+
|
|
252
|
+
def learn_from_success(self, error_pattern: str, fix_description: str):
|
|
253
|
+
"""Learn a fix pattern from a successful correction."""
|
|
254
|
+
self._learned_patterns[error_pattern] = fix_description
|
|
255
|
+
logger.info("Learned fix: %s", fix_description[:80])
|
|
256
|
+
|
|
257
|
+
# ── Auto-correction loop ─────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
def auto_correct(
|
|
260
|
+
self,
|
|
261
|
+
tool_name: str,
|
|
262
|
+
arguments: dict,
|
|
263
|
+
error_message: str,
|
|
264
|
+
execute_fn: Callable[[str, dict], Any],
|
|
265
|
+
) -> tuple[Any, int]:
|
|
266
|
+
"""
|
|
267
|
+
Run the full auto-correction loop.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
tool_name: The tool that failed
|
|
271
|
+
arguments: Original arguments
|
|
272
|
+
error_message: The error from the failed execution
|
|
273
|
+
execute_fn: Function to execute the tool (name, args) → result
|
|
274
|
+
|
|
275
|
+
Returns:
|
|
276
|
+
(result, retry_count): The final result and number of retries used
|
|
277
|
+
"""
|
|
278
|
+
retries = 0
|
|
279
|
+
|
|
280
|
+
while retries < self.max_retries:
|
|
281
|
+
# Diagnose
|
|
282
|
+
diagnosis = self.diagnose(error_message, tool_name, arguments)
|
|
283
|
+
if not diagnosis:
|
|
284
|
+
break
|
|
285
|
+
|
|
286
|
+
# Check if auto-fixable
|
|
287
|
+
if diagnosis.retry_strategy == "ask_user":
|
|
288
|
+
logger.info("Cannot auto-fix: %s", diagnosis.diagnosis)
|
|
289
|
+
break
|
|
290
|
+
|
|
291
|
+
if diagnosis.retry_strategy == "read_first":
|
|
292
|
+
logger.info("Need to read context first: %s", diagnosis.diagnosis)
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
# Try auto-fix
|
|
296
|
+
fixed_args = self.suggest_fix(tool_name, arguments, diagnosis,
|
|
297
|
+
error_message=error_message)
|
|
298
|
+
retries += 1
|
|
299
|
+
|
|
300
|
+
if fixed_args is None:
|
|
301
|
+
break
|
|
302
|
+
|
|
303
|
+
logger.info(
|
|
304
|
+
"Auto-correct retry %d/%d: %s",
|
|
305
|
+
retries, self.max_retries, diagnosis.fix_suggestion[:80],
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
# Execute with fixed args
|
|
309
|
+
try:
|
|
310
|
+
result = execute_fn(tool_name, fixed_args)
|
|
311
|
+
except Exception as e:
|
|
312
|
+
result = type('Result', (), {'success': False, 'error': str(e)})()
|
|
313
|
+
|
|
314
|
+
self.record_attempt(
|
|
315
|
+
tool_name, arguments, error_message,
|
|
316
|
+
diagnosis, fixed_args, getattr(result, 'success', False),
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
if getattr(result, 'success', False):
|
|
320
|
+
# Learn from this success
|
|
321
|
+
pattern = re.escape(error_message[:80])
|
|
322
|
+
self.learn_from_success(pattern, diagnosis.fix_suggestion)
|
|
323
|
+
return result, retries
|
|
324
|
+
|
|
325
|
+
# Update error for next iteration
|
|
326
|
+
error_message = getattr(result, 'error', str(result))
|
|
327
|
+
arguments = fixed_args
|
|
328
|
+
|
|
329
|
+
return None, retries
|
|
330
|
+
|
|
331
|
+
# ── Statistics ──────────────────────────────────────────────────────
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def stats(self) -> dict:
|
|
335
|
+
total = len(self._attempts)
|
|
336
|
+
successful = sum(1 for a in self._attempts if a.success)
|
|
337
|
+
return {
|
|
338
|
+
"total_retries": total,
|
|
339
|
+
"successful_retries": successful,
|
|
340
|
+
"learned_patterns": len(self._learned_patterns),
|
|
341
|
+
"auto_fix_rate": f"{successful/max(1,total)*100:.0f}%",
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
def reset(self):
|
|
345
|
+
self._attempts.clear()
|
|
346
|
+
self._learned_patterns.clear()
|