alpiecode 5.1.1__tar.gz → 6.1.0__tar.gz
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.
- {alpiecode-5.1.1 → alpiecode-6.1.0}/PKG-INFO +1 -1
- {alpiecode-5.1.1 → alpiecode-6.1.0}/pyproject.toml +1 -1
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/SOURCES.txt +1 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/__init__.py +1 -1
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/agent.py +58 -8
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/compaction.py +1 -1
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/config.py +3 -3
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/discovery.py +15 -18
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/guardian.py +9 -4
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/orchestrator.py +53 -26
- alpiecode-6.1.0/src/codeagent/progress.py +175 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/prompt.py +26 -7
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/tools.py +39 -9
- {alpiecode-5.1.1 → alpiecode-6.1.0}/README.md +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/setup.cfg +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode/__init__.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/backends/base.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/backends/local_backend.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/cache.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/cli.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/client.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/context.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/doctor.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/executor.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/github.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/local_model.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/media.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/memory.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/server.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/session.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/updater.py +0 -0
- {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/vscode_installer.py +0 -0
|
@@ -37,6 +37,7 @@ _build_system_prompt = lambda workdir, is_offline=False: PromptBuilder().build_s
|
|
|
37
37
|
try:
|
|
38
38
|
from rich.console import Console
|
|
39
39
|
from rich.markdown import Markdown
|
|
40
|
+
from rich.markup import escape
|
|
40
41
|
from rich.panel import Panel
|
|
41
42
|
from rich.rule import Rule
|
|
42
43
|
from rich.text import Text
|
|
@@ -46,6 +47,9 @@ try:
|
|
|
46
47
|
except ImportError:
|
|
47
48
|
HAS_RICH = False
|
|
48
49
|
|
|
50
|
+
def escape(text: str) -> str:
|
|
51
|
+
return text
|
|
52
|
+
|
|
49
53
|
class _FallbackConsole:
|
|
50
54
|
def print(self, *args, **kwargs):
|
|
51
55
|
kwargs.pop("style", None)
|
|
@@ -78,7 +82,7 @@ def _print_tool_call(turn: int, name: str, args: dict):
|
|
|
78
82
|
if HAS_RICH:
|
|
79
83
|
args_str = json.dumps(display_args, indent=2)
|
|
80
84
|
console.print(f"\n🔧 [bold cyan]Tool:[/bold cyan] [bold]{name}[/bold]", highlight=False)
|
|
81
|
-
console.print(f" {args_str}", style="cyan"
|
|
85
|
+
console.print(Text(f" {args_str}", style="cyan"))
|
|
82
86
|
else:
|
|
83
87
|
console.print(f"\n🔧 Tool: {name}({display_args})")
|
|
84
88
|
|
|
@@ -86,7 +90,7 @@ def _print_tool_call(turn: int, name: str, args: dict):
|
|
|
86
90
|
def _print_tool_result(result: str):
|
|
87
91
|
truncated = result[:1500] + ("..." if len(result) > 1500 else "")
|
|
88
92
|
if HAS_RICH:
|
|
89
|
-
console.print(f" → {truncated}", style="green"
|
|
93
|
+
console.print(Text(f" → {truncated}", style="green"))
|
|
90
94
|
else:
|
|
91
95
|
console.print(f" → {truncated}")
|
|
92
96
|
|
|
@@ -97,7 +101,7 @@ def _print_assistant_message(content: str):
|
|
|
97
101
|
md = Markdown(content)
|
|
98
102
|
console.print(Panel(md, title="🤖 Assistant", border_style="green", padding=(0, 1)))
|
|
99
103
|
except Exception:
|
|
100
|
-
console.print(Panel(content, title="🤖 Assistant", border_style="green", padding=(0, 1)))
|
|
104
|
+
console.print(Panel(Text(content), title="🤖 Assistant", border_style="green", padding=(0, 1)))
|
|
101
105
|
else:
|
|
102
106
|
console.print(f"\n🤖 Assistant: {content}")
|
|
103
107
|
|
|
@@ -225,7 +229,7 @@ def run_agent(
|
|
|
225
229
|
data = event.data
|
|
226
230
|
if HAS_RICH:
|
|
227
231
|
console.rule("[bold blue]Agent Started[/bold blue]")
|
|
228
|
-
console.print(f"📋 Task: {task.splitlines()[0]}", style="bold")
|
|
232
|
+
console.print(Text(f"📋 Task: {task.splitlines()[0]}", style="bold"))
|
|
229
233
|
if github_repo:
|
|
230
234
|
console.print(f"🐙 GitHub Repo: {github_repo}", style="cyan")
|
|
231
235
|
if image_path:
|
|
@@ -288,8 +292,9 @@ def run_agent(
|
|
|
288
292
|
_checkpoint(workdir, "checkpoint: response")
|
|
289
293
|
|
|
290
294
|
elif event.type == "fallback" and verbose:
|
|
295
|
+
err_str = escape(str(event.data.get('error', '')))
|
|
291
296
|
if HAS_RICH:
|
|
292
|
-
console.print(f"\n⚠️ [bold yellow]Online Server Error / Timeout[/bold yellow] ({
|
|
297
|
+
console.print(f"\n⚠️ [bold yellow]Online Server Error / Timeout[/bold yellow] ({err_str})", style="yellow")
|
|
293
298
|
console.print("🔄 [bold cyan]Auto-falling back to local GGUF engine...[/bold cyan]", style="cyan")
|
|
294
299
|
else:
|
|
295
300
|
print(f"\n⚠️ Online Server Error: {event.data['error']}")
|
|
@@ -297,10 +302,41 @@ def run_agent(
|
|
|
297
302
|
|
|
298
303
|
elif event.type == "error" and verbose:
|
|
299
304
|
if HAS_RICH:
|
|
300
|
-
console.print(f"\n❌
|
|
305
|
+
console.print(Text(f"\n❌ Model Error\n Error: {event.data['error']}\n", style="bold red"))
|
|
301
306
|
else:
|
|
302
307
|
print(f"\n❌ Model Error: {event.data['error']}")
|
|
303
308
|
|
|
309
|
+
elif event.type == "stall_intervention" and verbose:
|
|
310
|
+
if HAS_RICH:
|
|
311
|
+
console.print(
|
|
312
|
+
f"🔄 [bold yellow]Progress stall detected[/bold yellow] "
|
|
313
|
+
f"(turn {event.data['turn']}, {event.data['consecutive_stalls']} stalled turns, "
|
|
314
|
+
f"intervention #{event.data['interventions']})",
|
|
315
|
+
style="yellow"
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
print(f"🔄 Progress stall detected (turn {event.data['turn']})")
|
|
319
|
+
|
|
320
|
+
elif event.type == "turn_progress" and debug:
|
|
321
|
+
snap = event.data
|
|
322
|
+
status = "✅" if snap["had_progress"] else "⚠️"
|
|
323
|
+
if HAS_RICH:
|
|
324
|
+
console.print(
|
|
325
|
+
Text(f" {status} Progress: created={snap['files_created']}, "
|
|
326
|
+
f"modified={snap['files_modified']}, stalls={snap['consecutive_stalls']}"),
|
|
327
|
+
style="dim"
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
elif event.type == "safety_ceiling" and verbose:
|
|
331
|
+
if HAS_RICH:
|
|
332
|
+
console.print(
|
|
333
|
+
f"\n🛑 [bold red]Safety ceiling ({event.data['ceiling']} turns) reached.[/bold red]\n"
|
|
334
|
+
"This is an emergency stop — the agent may be stuck in an unrecoverable loop.",
|
|
335
|
+
style="bold red"
|
|
336
|
+
)
|
|
337
|
+
else:
|
|
338
|
+
print(f"\n🛑 Safety ceiling ({event.data['ceiling']}) reached.")
|
|
339
|
+
|
|
304
340
|
elif event.type == "done":
|
|
305
341
|
summary = event.data["summary"]
|
|
306
342
|
_checkpoint(workdir, "checkpoint: done")
|
|
@@ -311,7 +347,18 @@ def run_agent(
|
|
|
311
347
|
console.rule("[bold yellow]💬 Agent Replied[/bold yellow]")
|
|
312
348
|
|
|
313
349
|
elif event.type == "max_turns_reached" and verbose:
|
|
314
|
-
|
|
350
|
+
progress = event.data.get("progress", {})
|
|
351
|
+
if HAS_RICH:
|
|
352
|
+
console.print(f"\n⚠️ [bold yellow]Safety ceiling ({event.data['max_turns']}) reached.[/bold yellow]", style="bold yellow")
|
|
353
|
+
if progress:
|
|
354
|
+
console.print(
|
|
355
|
+
f" Progress: {progress.get('progress_turns', 0)}/{progress.get('total_turns', 0)} turns made progress, "
|
|
356
|
+
f"{progress.get('files_created', 0)} files created, "
|
|
357
|
+
f"{progress.get('stall_interventions', 0)} stall interventions",
|
|
358
|
+
style="dim"
|
|
359
|
+
)
|
|
360
|
+
else:
|
|
361
|
+
print(f"\n⚠️ Safety ceiling ({event.data['max_turns']}) reached.")
|
|
315
362
|
|
|
316
363
|
return session.context.messages if "session" in locals() else []
|
|
317
364
|
|
|
@@ -385,7 +432,10 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
|
|
|
385
432
|
_checkpoint(workdir, "checkpoint: done")
|
|
386
433
|
|
|
387
434
|
elif event.type == "error":
|
|
388
|
-
|
|
435
|
+
if HAS_RICH:
|
|
436
|
+
console.print(Text(f"❌ Model error: {event.data['error']}", style="bold red"))
|
|
437
|
+
else:
|
|
438
|
+
print(f"❌ Model error: {event.data['error']}")
|
|
389
439
|
|
|
390
440
|
elif event.type == "done":
|
|
391
441
|
break
|
|
@@ -20,7 +20,7 @@ MAX_CONTEXT_TOKENS = 262_144
|
|
|
20
20
|
# Start compacting when we hit this percentage of the context window
|
|
21
21
|
COMPACT_THRESHOLD = 0.70
|
|
22
22
|
# Number of recent turns to always keep intact
|
|
23
|
-
KEEP_RECENT_TURNS =
|
|
23
|
+
KEEP_RECENT_TURNS = 12
|
|
24
24
|
# Approximate chars per token (rough heuristic)
|
|
25
25
|
CHARS_PER_TOKEN = 4
|
|
26
26
|
|
|
@@ -26,7 +26,7 @@ DEFAULTS = {
|
|
|
26
26
|
"model_repo": "169Pi/Alpie_learn_prototype_GGUF_NEW",
|
|
27
27
|
"api_key": "not-needed",
|
|
28
28
|
"hf_token": None,
|
|
29
|
-
"max_turns":
|
|
29
|
+
"max_turns": 200,
|
|
30
30
|
"temperature": 0.1,
|
|
31
31
|
"max_tokens": 8192,
|
|
32
32
|
"enable_thinking": False, # Reasoning OFF by default
|
|
@@ -88,7 +88,7 @@ class Config:
|
|
|
88
88
|
model_repo: str = "169Pi/Alpie_learn_prototype_GGUF_NEW" # HuggingFace repo for offline GGUF
|
|
89
89
|
api_key: str = "not-needed"
|
|
90
90
|
hf_token: Optional[str] = None
|
|
91
|
-
max_turns: int =
|
|
91
|
+
max_turns: int = 200 # Safety ceiling only — agent runs until DONE
|
|
92
92
|
temperature: float = 0.1
|
|
93
93
|
max_tokens: int = 8192
|
|
94
94
|
enable_thinking: bool = False
|
|
@@ -117,7 +117,7 @@ def load_config() -> Config:
|
|
|
117
117
|
data["enable_thinking"] = False
|
|
118
118
|
data["temperature"] = 0.1
|
|
119
119
|
data["max_tokens"] = 8192
|
|
120
|
-
data["max_turns"] =
|
|
120
|
+
data["max_turns"] = 200
|
|
121
121
|
data["config_version"] = CONFIG_VERSION
|
|
122
122
|
needs_save = True
|
|
123
123
|
except Exception:
|
|
@@ -49,7 +49,6 @@ class TaskContext:
|
|
|
49
49
|
dependencies: List[str] = field(default_factory=list)
|
|
50
50
|
|
|
51
51
|
# Budget (computed from complexity)
|
|
52
|
-
max_turns: int = 10
|
|
53
52
|
max_tokens: int = 8192
|
|
54
53
|
tool_set: str = "core" # none, core, full
|
|
55
54
|
enable_thinking: bool = False
|
|
@@ -178,16 +177,18 @@ def detect_environment() -> dict:
|
|
|
178
177
|
|
|
179
178
|
|
|
180
179
|
def _detect_shell(os_name: str) -> str:
|
|
181
|
-
"""Detect available shell,
|
|
180
|
+
"""Detect available shell, aligned with tools.py execution path."""
|
|
182
181
|
|
|
183
182
|
if os_name == "windows":
|
|
184
|
-
#
|
|
183
|
+
# Check if bash binary exists (Git Bash / MSYS)
|
|
184
|
+
if shutil.which("bash"):
|
|
185
|
+
return "bash"
|
|
185
186
|
if _is_wsl_available():
|
|
186
187
|
return "wsl"
|
|
187
188
|
# Fallback to PowerShell
|
|
188
189
|
if shutil.which("powershell") or shutil.which("pwsh"):
|
|
189
190
|
return "powershell"
|
|
190
|
-
return "cmd"
|
|
191
|
+
return "cmd"
|
|
191
192
|
|
|
192
193
|
if os_name in ("linux", "wsl"):
|
|
193
194
|
return "bash" # Standard on Linux/WSL
|
|
@@ -466,13 +467,14 @@ def compute_complexity(intent: str, repo_info: dict, task: str) -> str:
|
|
|
466
467
|
|
|
467
468
|
# -- MEDIUM: multi-step tasks --
|
|
468
469
|
medium_keywords = [
|
|
469
|
-
"api", "rest api", "graphql", "server",
|
|
470
|
-
"game", "snake", "tetris", "chess", "pong", "sudoku",
|
|
471
|
-
"website", "web page", "web app", "webapp",
|
|
472
|
-
"test suite", "unit tests", "integration test",
|
|
473
|
-
"dashboard", "portfolio",
|
|
474
|
-
"react", "vue", "angular", "next.js",
|
|
475
|
-
"django", "flask app", "fastapi app",
|
|
470
|
+
"api", "rest api", "graphql", "server", "backend", "frontend",
|
|
471
|
+
"game", "snake", "tetris", "chess", "pong", "sudoku", "flappy", "arcade",
|
|
472
|
+
"website", "web page", "web app", "webapp", "html", "css", "javascript",
|
|
473
|
+
"test suite", "unit tests", "integration test", "e2e test",
|
|
474
|
+
"dashboard", "portfolio", "application", "app with", "notes app", "todo app",
|
|
475
|
+
"react", "vue", "angular", "next.js", "tailwind",
|
|
476
|
+
"django", "flask app", "fastapi app", "express",
|
|
477
|
+
"refactor", "migrate", "redesign", "restructure", "investigate",
|
|
476
478
|
]
|
|
477
479
|
if any(kw in task_lower for kw in medium_keywords):
|
|
478
480
|
return "medium"
|
|
@@ -497,25 +499,21 @@ def compute_complexity(intent: str, repo_info: dict, task: str) -> str:
|
|
|
497
499
|
|
|
498
500
|
COMPLEXITY_CONFIG = {
|
|
499
501
|
"qa": {
|
|
500
|
-
"max_turns": 3,
|
|
501
502
|
"max_tokens": 4096,
|
|
502
503
|
"tool_set": "none",
|
|
503
504
|
"enable_thinking": False,
|
|
504
505
|
},
|
|
505
506
|
"low": {
|
|
506
|
-
"max_turns": 10,
|
|
507
507
|
"max_tokens": 8192,
|
|
508
508
|
"tool_set": "core",
|
|
509
509
|
"enable_thinking": False,
|
|
510
510
|
},
|
|
511
511
|
"medium": {
|
|
512
|
-
"
|
|
513
|
-
"max_tokens": 8192,
|
|
512
|
+
"max_tokens": 16384,
|
|
514
513
|
"tool_set": "full",
|
|
515
|
-
"enable_thinking":
|
|
514
|
+
"enable_thinking": True,
|
|
516
515
|
},
|
|
517
516
|
"high": {
|
|
518
|
-
"max_turns": 40,
|
|
519
517
|
"max_tokens": 16384,
|
|
520
518
|
"tool_set": "full",
|
|
521
519
|
"enable_thinking": True,
|
|
@@ -565,7 +563,6 @@ def build_task_context(task: str, workdir: Path) -> TaskContext:
|
|
|
565
563
|
entry_points=repo["entry_points"],
|
|
566
564
|
dependencies=repo["dependencies"],
|
|
567
565
|
# Budget
|
|
568
|
-
max_turns=budget["max_turns"],
|
|
569
566
|
max_tokens=budget["max_tokens"],
|
|
570
567
|
tool_set=budget["tool_set"],
|
|
571
568
|
enable_thinking=budget["enable_thinking"],
|
|
@@ -16,12 +16,17 @@ from typing import Tuple
|
|
|
16
16
|
try:
|
|
17
17
|
from rich.console import Console
|
|
18
18
|
from rich.panel import Panel
|
|
19
|
+
from rich.text import Text
|
|
20
|
+
from rich.markup import escape
|
|
19
21
|
console = Console()
|
|
20
22
|
HAS_RICH = True
|
|
21
23
|
except ImportError:
|
|
22
24
|
HAS_RICH = False
|
|
23
25
|
console = None
|
|
24
26
|
|
|
27
|
+
def escape(text: str) -> str:
|
|
28
|
+
return text
|
|
29
|
+
|
|
25
30
|
|
|
26
31
|
class RiskLevel(Enum):
|
|
27
32
|
SAFE = "safe"
|
|
@@ -134,19 +139,19 @@ def gate_command(command: str, auto_approve: bool = False) -> bool:
|
|
|
134
139
|
if risk == RiskLevel.WARNING:
|
|
135
140
|
if auto_approve:
|
|
136
141
|
if HAS_RICH:
|
|
137
|
-
console.print(f" ⚠️
|
|
142
|
+
console.print(Text(f" ⚠️ {reason}", style="yellow"))
|
|
138
143
|
return True
|
|
139
144
|
# In interactive mode, show warning but proceed
|
|
140
145
|
if HAS_RICH:
|
|
141
|
-
console.print(f" ⚠️
|
|
146
|
+
console.print(Text(f" ⚠️ {reason}", style="yellow"))
|
|
142
147
|
return True
|
|
143
148
|
|
|
144
149
|
if risk == RiskLevel.DANGEROUS:
|
|
145
150
|
if HAS_RICH:
|
|
146
151
|
console.print(Panel(
|
|
147
152
|
f"[bold red]🛑 BLOCKED — Dangerous Command[/bold red]\n\n"
|
|
148
|
-
f"Command: [cyan]{command}[/cyan]\n"
|
|
149
|
-
f"Reason: {reason}\n\n"
|
|
153
|
+
f"Command: [cyan]{escape(command)}[/cyan]\n"
|
|
154
|
+
f"Reason: {escape(reason)}\n\n"
|
|
150
155
|
f"This command has been blocked for safety.\n"
|
|
151
156
|
f"If you need to run it, do so manually in your terminal.",
|
|
152
157
|
border_style="red",
|
|
@@ -15,6 +15,7 @@ from .cache import get_cache
|
|
|
15
15
|
from .config import Config, is_server_reachable
|
|
16
16
|
from .memory import extract_and_save_memories
|
|
17
17
|
from .discovery import build_task_context, COMPLEXITY_CONFIG
|
|
18
|
+
from .progress import ProgressMonitor
|
|
18
19
|
from .prompt import PromptBuilder, classify_task
|
|
19
20
|
from .session import Session, SessionManager
|
|
20
21
|
|
|
@@ -72,13 +73,11 @@ class AgentOrchestrator:
|
|
|
72
73
|
|
|
73
74
|
comp_cfg = COMPLEXITY_CONFIG.get(complexity, COMPLEXITY_CONFIG["low"])
|
|
74
75
|
|
|
75
|
-
# ── Determine effective
|
|
76
|
-
effective_max_turns = task_context.max_turns
|
|
76
|
+
# ── Determine effective max_tokens ──
|
|
77
77
|
effective_max_tokens = task_context.max_tokens
|
|
78
78
|
|
|
79
|
-
#
|
|
80
|
-
if cfg.max_turns !=
|
|
81
|
-
effective_max_turns = cfg.max_turns
|
|
79
|
+
# Safety ceiling: hard emergency brake (should never be hit naturally)
|
|
80
|
+
safety_ceiling = cfg.max_turns if cfg.max_turns != 200 else 200
|
|
82
81
|
|
|
83
82
|
# ── Response cache check ──
|
|
84
83
|
is_cacheable = not any([image_path, video_path, url, github_repo])
|
|
@@ -137,36 +136,51 @@ class AgentOrchestrator:
|
|
|
137
136
|
"complexity": complexity,
|
|
138
137
|
})
|
|
139
138
|
|
|
139
|
+
|
|
140
|
+
|
|
140
141
|
# ── Adaptive thinking ──
|
|
141
142
|
enable_thinking = cfg.enable_thinking or task_context.enable_thinking
|
|
142
143
|
if enable_thinking and complexity in ("qa", "low"):
|
|
143
144
|
enable_thinking = False
|
|
144
145
|
yield AgentEvent("adaptive_mode", {"message": "Simple task detected, skipping deep reasoning."})
|
|
145
146
|
|
|
146
|
-
# ──
|
|
147
|
-
|
|
147
|
+
# ── Goal-driven turn loop (no fixed limit) ──
|
|
148
|
+
progress_monitor = ProgressMonitor()
|
|
149
|
+
turn = 0
|
|
150
|
+
|
|
151
|
+
while True:
|
|
152
|
+
turn += 1
|
|
148
153
|
|
|
149
|
-
for turn in range(effective_max_turns):
|
|
150
154
|
if session.cancelled:
|
|
151
|
-
yield AgentEvent("cancelled", {"turn": turn
|
|
155
|
+
yield AgentEvent("cancelled", {"turn": turn})
|
|
152
156
|
break
|
|
153
157
|
|
|
154
158
|
# Context compaction
|
|
155
159
|
if session.context.check_and_compact():
|
|
156
|
-
yield AgentEvent("compaction", {"turn": turn
|
|
157
|
-
|
|
158
|
-
# ──
|
|
159
|
-
if
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
"
|
|
165
|
-
"
|
|
166
|
-
)
|
|
167
|
-
|
|
160
|
+
yield AgentEvent("compaction", {"turn": turn})
|
|
161
|
+
|
|
162
|
+
# ── Stall Detection & Corrective Intervention ──
|
|
163
|
+
if progress_monitor.is_stalled(threshold=3):
|
|
164
|
+
advice = progress_monitor.get_stall_advice()
|
|
165
|
+
session.context.add_user_message(advice)
|
|
166
|
+
yield AgentEvent("stall_intervention", {
|
|
167
|
+
"turn": turn,
|
|
168
|
+
"consecutive_stalls": progress_monitor.consecutive_stalls,
|
|
169
|
+
"interventions": progress_monitor.stall_interventions,
|
|
170
|
+
})
|
|
171
|
+
# After 3 interventions (= 9+ stalled turns), force wrap-up
|
|
172
|
+
if progress_monitor.stall_interventions >= 3:
|
|
173
|
+
session.context.add_user_message(
|
|
174
|
+
"[SYSTEM] Multiple stall interventions have not resolved the issue. "
|
|
175
|
+
"Finish now with whatever you have. Output DONE: <summary of what was completed>."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
# ── Safety ceiling (emergency only) ──
|
|
179
|
+
if turn > safety_ceiling:
|
|
180
|
+
yield AgentEvent("safety_ceiling", {"turn": turn, "ceiling": safety_ceiling})
|
|
181
|
+
break
|
|
168
182
|
|
|
169
|
-
yield AgentEvent("turn_start", {"turn": turn
|
|
183
|
+
yield AgentEvent("turn_start", {"turn": turn})
|
|
170
184
|
|
|
171
185
|
try:
|
|
172
186
|
if enable_thinking:
|
|
@@ -218,7 +232,7 @@ class AgentOrchestrator:
|
|
|
218
232
|
results = session.executor.execute_tool_calls(tool_calls)
|
|
219
233
|
for res in results:
|
|
220
234
|
yield AgentEvent("tool_result", {
|
|
221
|
-
"turn": turn
|
|
235
|
+
"turn": turn,
|
|
222
236
|
"id": res.tool_call_id,
|
|
223
237
|
"name": res.name,
|
|
224
238
|
"content": res.content,
|
|
@@ -246,7 +260,7 @@ class AgentOrchestrator:
|
|
|
246
260
|
if tool_calls:
|
|
247
261
|
for tc in tool_calls:
|
|
248
262
|
yield AgentEvent("tool_call", {
|
|
249
|
-
"turn": turn
|
|
263
|
+
"turn": turn,
|
|
250
264
|
"id": tc.id,
|
|
251
265
|
"name": tc.name,
|
|
252
266
|
"arguments": tc.arguments,
|
|
@@ -256,7 +270,7 @@ class AgentOrchestrator:
|
|
|
256
270
|
|
|
257
271
|
for res in results:
|
|
258
272
|
yield AgentEvent("tool_result", {
|
|
259
|
-
"turn": turn
|
|
273
|
+
"turn": turn,
|
|
260
274
|
"id": res.tool_call_id,
|
|
261
275
|
"name": res.name,
|
|
262
276
|
"content": res.content,
|
|
@@ -264,6 +278,18 @@ class AgentOrchestrator:
|
|
|
264
278
|
})
|
|
265
279
|
session.context.add_tool_result(res.tool_call_id, res.content)
|
|
266
280
|
|
|
281
|
+
# ── Record progress for stall detection ──
|
|
282
|
+
tc_dicts = [{"name": tc.name, "arguments": tc.arguments} for tc in tool_calls]
|
|
283
|
+
res_dicts = [{"content": res.content, "name": res.name} for res in results]
|
|
284
|
+
snap = progress_monitor.record_turn(turn, tc_dicts, res_dicts)
|
|
285
|
+
yield AgentEvent("turn_progress", {
|
|
286
|
+
"turn": turn,
|
|
287
|
+
"had_progress": snap.had_progress,
|
|
288
|
+
"files_created": list(snap.files_created),
|
|
289
|
+
"files_modified": list(snap.files_modified),
|
|
290
|
+
"consecutive_stalls": progress_monitor.consecutive_stalls,
|
|
291
|
+
})
|
|
292
|
+
|
|
267
293
|
continue
|
|
268
294
|
|
|
269
295
|
# Text-only response = done
|
|
@@ -285,5 +311,6 @@ class AgentOrchestrator:
|
|
|
285
311
|
yield AgentEvent("done", {"summary": "Task completed."})
|
|
286
312
|
return
|
|
287
313
|
|
|
288
|
-
|
|
314
|
+
status = progress_monitor.get_status_summary()
|
|
315
|
+
yield AgentEvent("max_turns_reached", {"max_turns": safety_ceiling, "progress": status})
|
|
289
316
|
extract_and_save_memories(session.workdir, session.context.messages)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Progress Monitor for AlpieCode.
|
|
3
|
+
|
|
4
|
+
Tracks per-turn agent progress by observing tool calls and results.
|
|
5
|
+
Detects stalls (zero net progress) and thrashing (delete-after-create loops).
|
|
6
|
+
Does NOT kill the agent — only signals the orchestrator to inject corrective prompts.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Dict, List, Set, Optional
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class TurnSnapshot:
|
|
16
|
+
"""Captures what happened in a single turn."""
|
|
17
|
+
turn: int
|
|
18
|
+
tools_called: List[str] = field(default_factory=list)
|
|
19
|
+
files_created: Set[str] = field(default_factory=set)
|
|
20
|
+
files_modified: Set[str] = field(default_factory=set)
|
|
21
|
+
files_deleted: Set[str] = field(default_factory=set)
|
|
22
|
+
bash_exit_codes: List[int] = field(default_factory=list)
|
|
23
|
+
errors: List[str] = field(default_factory=list)
|
|
24
|
+
had_progress: bool = False
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ProgressMonitor:
|
|
28
|
+
"""Tracks agent progress across turns.
|
|
29
|
+
|
|
30
|
+
Progress = files created, files modified, successful bash runs,
|
|
31
|
+
or errors changing (not repeating the same error).
|
|
32
|
+
|
|
33
|
+
Stall = 3+ consecutive turns with zero net progress.
|
|
34
|
+
Thrashing = deleting files the agent itself created.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.history: List[TurnSnapshot] = []
|
|
39
|
+
self.all_files_created: Set[str] = set()
|
|
40
|
+
self.consecutive_stalls: int = 0
|
|
41
|
+
self.stall_interventions: int = 0
|
|
42
|
+
|
|
43
|
+
def record_turn(self, turn: int, tool_calls: list, tool_results: list) -> TurnSnapshot:
|
|
44
|
+
"""Record what happened in a turn and determine if progress was made."""
|
|
45
|
+
snap = TurnSnapshot(turn=turn)
|
|
46
|
+
|
|
47
|
+
for tc in tool_calls:
|
|
48
|
+
name = tc.get("name", "") if isinstance(tc, dict) else getattr(tc, "name", "")
|
|
49
|
+
args = tc.get("arguments", {}) if isinstance(tc, dict) else getattr(tc, "arguments", {})
|
|
50
|
+
if not isinstance(args, dict):
|
|
51
|
+
args = {}
|
|
52
|
+
snap.tools_called.append(name)
|
|
53
|
+
|
|
54
|
+
if name == "write_file":
|
|
55
|
+
path = args.get("path", "")
|
|
56
|
+
snap.files_created.add(path)
|
|
57
|
+
self.all_files_created.add(path)
|
|
58
|
+
|
|
59
|
+
elif name == "edit_file":
|
|
60
|
+
path = args.get("path", "")
|
|
61
|
+
snap.files_modified.add(path)
|
|
62
|
+
|
|
63
|
+
elif name == "bash":
|
|
64
|
+
cmd = args.get("command", "")
|
|
65
|
+
cmd_parts = cmd.strip().split()
|
|
66
|
+
if cmd_parts and cmd_parts[0] in ("rm", "del", "Remove-Item"):
|
|
67
|
+
for part in cmd_parts[1:]:
|
|
68
|
+
if not part.startswith("-"):
|
|
69
|
+
clean = Path(part.strip("\'\"")).name
|
|
70
|
+
snap.files_deleted.add(clean)
|
|
71
|
+
|
|
72
|
+
# Parse tool results for exit codes and errors
|
|
73
|
+
for res in tool_results:
|
|
74
|
+
content = res.get("content", "") if isinstance(res, dict) else getattr(res, "content", "")
|
|
75
|
+
if '"exit_code": 0' in content or '"exit_code":0' in content:
|
|
76
|
+
snap.bash_exit_codes.append(0)
|
|
77
|
+
elif '"exit_code":' in content:
|
|
78
|
+
snap.bash_exit_codes.append(1)
|
|
79
|
+
snap.errors.append(f"command failed: {content[:100]}")
|
|
80
|
+
if content.lower().startswith("error:") or "error:" in content[:80].lower():
|
|
81
|
+
snap.errors.append(content[:100])
|
|
82
|
+
|
|
83
|
+
# Determine progress
|
|
84
|
+
snap.had_progress = bool(
|
|
85
|
+
snap.files_created
|
|
86
|
+
or snap.files_modified
|
|
87
|
+
or (snap.bash_exit_codes and 0 in snap.bash_exit_codes)
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# No tools called at all = not progress (empty turn)
|
|
91
|
+
if not snap.tools_called:
|
|
92
|
+
snap.had_progress = True # text-only response = model is finishing
|
|
93
|
+
|
|
94
|
+
# Detect thrashing: deleting a file that was created recently
|
|
95
|
+
created_names = {Path(f).name for f in self.all_files_created}
|
|
96
|
+
thrashing = snap.files_deleted & created_names
|
|
97
|
+
if thrashing:
|
|
98
|
+
snap.had_progress = False
|
|
99
|
+
|
|
100
|
+
# Detect identical consecutive errors (stall)
|
|
101
|
+
if len(self.history) >= 2 and snap.errors:
|
|
102
|
+
prev_errors = set(e[:60] for e in self.history[-1].errors)
|
|
103
|
+
curr_errors = set(e[:60] for e in snap.errors)
|
|
104
|
+
if curr_errors and curr_errors == prev_errors:
|
|
105
|
+
snap.had_progress = False
|
|
106
|
+
|
|
107
|
+
# Detect pure read-only turns with errors as non-progress
|
|
108
|
+
if snap.errors and not snap.files_created and not snap.files_modified:
|
|
109
|
+
if not (snap.bash_exit_codes and 0 in snap.bash_exit_codes):
|
|
110
|
+
snap.had_progress = False
|
|
111
|
+
|
|
112
|
+
# Update stall counter
|
|
113
|
+
if snap.had_progress:
|
|
114
|
+
self.consecutive_stalls = 0
|
|
115
|
+
else:
|
|
116
|
+
self.consecutive_stalls += 1
|
|
117
|
+
|
|
118
|
+
self.history.append(snap)
|
|
119
|
+
return snap
|
|
120
|
+
|
|
121
|
+
def is_stalled(self, threshold: int = 3) -> bool:
|
|
122
|
+
"""Returns True if the agent has made zero net progress for N consecutive turns."""
|
|
123
|
+
return self.consecutive_stalls >= threshold
|
|
124
|
+
|
|
125
|
+
def get_stall_advice(self) -> str:
|
|
126
|
+
"""Generate a corrective prompt for the model when stalled."""
|
|
127
|
+
self.stall_interventions += 1
|
|
128
|
+
|
|
129
|
+
recent = self.history[-3:] if len(self.history) >= 3 else self.history
|
|
130
|
+
deleted = set()
|
|
131
|
+
for snap in recent:
|
|
132
|
+
deleted.update(snap.files_deleted)
|
|
133
|
+
|
|
134
|
+
created_names = {Path(f).name for f in self.all_files_created}
|
|
135
|
+
thrashing = deleted & created_names
|
|
136
|
+
|
|
137
|
+
if thrashing:
|
|
138
|
+
return (
|
|
139
|
+
f"[SYSTEM - PROGRESS MONITOR] You have been deleting files you previously "
|
|
140
|
+
f"created ({', '.join(thrashing)}). STOP deleting and rewriting from scratch. "
|
|
141
|
+
f"Use edit_file to modify specific sections, or overwrite directly with write_file. "
|
|
142
|
+
f"Take a step back: what is the simplest path to a working solution?"
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
if all(snap.errors or (snap.bash_exit_codes and 0 not in snap.bash_exit_codes) for snap in recent):
|
|
146
|
+
return (
|
|
147
|
+
"[SYSTEM - PROGRESS MONITOR] You have encountered errors for 3 consecutive turns. "
|
|
148
|
+
"STOP retrying the same approach. Instead:\n"
|
|
149
|
+
"1. Use read_file to examine the FULL current state of the file(s) you\'re editing\n"
|
|
150
|
+
"2. Identify the root cause of the error (not the symptom)\n"
|
|
151
|
+
"3. Make ONE comprehensive fix that addresses all issues\n"
|
|
152
|
+
"If the task approach is fundamentally wrong, start with a simpler design."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
"[SYSTEM - PROGRESS MONITOR] No measurable progress detected for 3 turns. "
|
|
157
|
+
"You may be stuck in a loop. Either:\n"
|
|
158
|
+
"1. Complete your current work and output DONE: <summary>\n"
|
|
159
|
+
"2. Try a completely different approach to the problem\n"
|
|
160
|
+
"3. If the code is written and working, verify with bash and finish."
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def get_status_summary(self) -> dict:
|
|
164
|
+
"""Return a summary of overall progress for logging."""
|
|
165
|
+
total_files = len(self.all_files_created)
|
|
166
|
+
total_turns = len(self.history)
|
|
167
|
+
success_turns = sum(1 for s in self.history if s.had_progress)
|
|
168
|
+
return {
|
|
169
|
+
"total_turns": total_turns,
|
|
170
|
+
"progress_turns": success_turns,
|
|
171
|
+
"stall_turns": total_turns - success_turns,
|
|
172
|
+
"files_created": total_files,
|
|
173
|
+
"stall_interventions": self.stall_interventions,
|
|
174
|
+
"consecutive_stalls": self.consecutive_stalls,
|
|
175
|
+
}
|
|
@@ -333,25 +333,21 @@ def is_simple_task(task: str) -> bool:
|
|
|
333
333
|
|
|
334
334
|
COMPLEXITY_CONFIG = {
|
|
335
335
|
"qa": {
|
|
336
|
-
"max_turns": 3,
|
|
337
336
|
"max_tokens": 4096,
|
|
338
337
|
"tools": "none", # No tools for Q&A
|
|
339
338
|
"prompt": "default",
|
|
340
339
|
},
|
|
341
340
|
"low": {
|
|
342
|
-
"max_turns": 10,
|
|
343
341
|
"max_tokens": 8192,
|
|
344
342
|
"tools": "core", # 5 core tools
|
|
345
343
|
"prompt": "default",
|
|
346
344
|
},
|
|
347
345
|
"medium": {
|
|
348
|
-
"
|
|
349
|
-
"max_tokens": 8192,
|
|
346
|
+
"max_tokens": 16384,
|
|
350
347
|
"tools": "full", # All 15 tools
|
|
351
348
|
"prompt": "default",
|
|
352
349
|
},
|
|
353
350
|
"high": {
|
|
354
|
-
"max_turns": 40,
|
|
355
351
|
"max_tokens": 16384,
|
|
356
352
|
"tools": "full", # All 15 tools
|
|
357
353
|
"prompt": "high", # Detailed system prompt
|
|
@@ -408,7 +404,7 @@ REPO_CONTEXT_TEMPLATE = """\
|
|
|
408
404
|
{extra}"""
|
|
409
405
|
|
|
410
406
|
INTENT_CREATE = """\
|
|
411
|
-
## Task Intent: Create New Code
|
|
407
|
+
## Task Intent: Create New Code (Single-file / Simple Tasks)
|
|
412
408
|
- Write the COMPLETE, WORKING code on Turn 1 using write_file
|
|
413
409
|
- Do NOT explore the filesystem first -- start coding immediately
|
|
414
410
|
- Include ALL imports, ALL functions, ALL logic -- no stubs, no TODOs
|
|
@@ -417,6 +413,23 @@ INTENT_CREATE = """\
|
|
|
417
413
|
- When everything works: DONE: <summary>
|
|
418
414
|
"""
|
|
419
415
|
|
|
416
|
+
INTENT_CREATE_COMPLEX = """\
|
|
417
|
+
## Task Intent: Create Multi-Component Code (Medium/High Complexity)
|
|
418
|
+
- Turn 1: Think through and plan the architecture. Identify ALL files needed (e.g. HTML, CSS, JS, tests).
|
|
419
|
+
- Sequential Creation: Write each file completely using write_file in logical dependency order.
|
|
420
|
+
- No Thrashing: Do NOT delete files you just created with `rm`. If adjustments are needed, use edit_file or overwrite directly.
|
|
421
|
+
- Verification: After writing the files, run or verify them using bash (e.g. run test suite, build check, or verify syntax).
|
|
422
|
+
- Early Stopping: Once verified and working, output: DONE: <summary> immediately. Do not run redundant checks.
|
|
423
|
+
"""
|
|
424
|
+
|
|
425
|
+
GOAL_DRIVEN_RULES = """\
|
|
426
|
+
## Execution & Goal Convergence Rules
|
|
427
|
+
- Efficiency First: Plan your actions to minimize wasted turns. You can execute multiple tool calls in a single turn.
|
|
428
|
+
- No Thrashing: NEVER delete a file (e.g. `rm <file>`) immediately after creating it to start over. Use `edit_file` to modify what needs fixing.
|
|
429
|
+
- Immediate Completion: As soon as your code is written and verified, output `DONE: <summary>`. Do NOT linger or rerun commands that already passed.
|
|
430
|
+
- Targeted Editing: If `edit_file` fails, use `read_file` to inspect the exact lines and whitespace before attempting another edit.
|
|
431
|
+
"""
|
|
432
|
+
|
|
420
433
|
INTENT_MODIFY = """\
|
|
421
434
|
## Task Intent: Modify Existing Code
|
|
422
435
|
- Turn 1: Use read_file to read the relevant file(s)
|
|
@@ -492,13 +505,19 @@ class PromptBuilder:
|
|
|
492
505
|
# Inject intent-specific workflow
|
|
493
506
|
intent = getattr(task_context, "intent", "create")
|
|
494
507
|
if intent == "create":
|
|
495
|
-
|
|
508
|
+
if complexity in ("medium", "high"):
|
|
509
|
+
prompt += "\n\n" + INTENT_CREATE_COMPLEX
|
|
510
|
+
else:
|
|
511
|
+
prompt += "\n\n" + INTENT_CREATE
|
|
496
512
|
elif intent == "modify":
|
|
497
513
|
prompt += "\n\n" + INTENT_MODIFY
|
|
498
514
|
elif intent == "debug":
|
|
499
515
|
prompt += "\n\n" + INTENT_DEBUG
|
|
500
516
|
elif intent == "explain":
|
|
501
517
|
prompt += "\n\n" + INTENT_EXPLAIN
|
|
518
|
+
|
|
519
|
+
if complexity in ("medium", "high"):
|
|
520
|
+
prompt += "\n\n" + GOAL_DRIVEN_RULES
|
|
502
521
|
# qa intent: no extra prompt needed (model answers directly)
|
|
503
522
|
|
|
504
523
|
memories = format_memories_for_prompt(workdir)
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
_RECENTLY_WRITTEN_FILES = set()
|
|
2
|
+
|
|
1
3
|
"""
|
|
2
4
|
Tool definitions and implementations for AlpieCode.
|
|
3
5
|
|
|
@@ -437,11 +439,11 @@ def _bash(workdir: Path, command: str) -> str:
|
|
|
437
439
|
shell_cmd,
|
|
438
440
|
cwd=workdir, capture_output=True, text=True,
|
|
439
441
|
stdin=subprocess.DEVNULL,
|
|
440
|
-
timeout=
|
|
442
|
+
timeout=120, env=env,
|
|
441
443
|
)
|
|
442
444
|
|
|
443
|
-
stdout = _smart_truncate(result.stdout,
|
|
444
|
-
stderr = _smart_truncate(result.stderr,
|
|
445
|
+
stdout = _smart_truncate(result.stdout, 3000)
|
|
446
|
+
stderr = _smart_truncate(result.stderr, 2000)
|
|
445
447
|
|
|
446
448
|
output = json.dumps({
|
|
447
449
|
"stdout": stdout,
|
|
@@ -463,6 +465,20 @@ def _bash(workdir: Path, command: str) -> str:
|
|
|
463
465
|
)
|
|
464
466
|
output = hint + output
|
|
465
467
|
|
|
468
|
+
# ── Anti-thrashing guard: warned if deleting file created in this session ──
|
|
469
|
+
cmd_parts = command.strip().split()
|
|
470
|
+
if cmd_parts and cmd_parts[0] in ("rm", "del", "Remove-Item"):
|
|
471
|
+
for part in cmd_parts[1:]:
|
|
472
|
+
base_part = Path(part.strip('"\'')).name.lower()
|
|
473
|
+
if base_part in _RECENTLY_WRITTEN_FILES:
|
|
474
|
+
_RECENTLY_WRITTEN_FILES.discard(base_part)
|
|
475
|
+
output += (
|
|
476
|
+
f"\n\n💡 ADVICE: You deleted '{base_part}' which was created in a recent turn. "
|
|
477
|
+
"Avoid deleting and rewriting entire files from scratch. "
|
|
478
|
+
"For iterative improvements, use edit_file or overwrite directly with write_file."
|
|
479
|
+
)
|
|
480
|
+
break
|
|
481
|
+
|
|
466
482
|
# ── Smart Guardrail 2: Silent success — script ran but no output ──
|
|
467
483
|
elif result.returncode == 0 and not stdout.strip() and not stderr.strip():
|
|
468
484
|
script_file = _extract_script_path(command)
|
|
@@ -505,7 +521,8 @@ def _read_file(workdir: Path, path: str, start_line: int = None, end_line: int =
|
|
|
505
521
|
def _write_file(workdir: Path, path: str, content: str) -> str:
|
|
506
522
|
p = workdir / path
|
|
507
523
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
508
|
-
p.write_text(content)
|
|
524
|
+
p.write_text(content, encoding="utf-8")
|
|
525
|
+
_RECENTLY_WRITTEN_FILES.add(Path(path).name.lower())
|
|
509
526
|
return f"wrote {len(content)} bytes to {path}"
|
|
510
527
|
|
|
511
528
|
|
|
@@ -513,13 +530,25 @@ def _edit_file(workdir: Path, path: str, old_str: str, new_str: str) -> str:
|
|
|
513
530
|
p = workdir / path
|
|
514
531
|
if not p.exists():
|
|
515
532
|
return f"error: file not found: {path}"
|
|
516
|
-
text = p.read_text()
|
|
533
|
+
text = p.read_text(encoding="utf-8", errors="replace")
|
|
517
534
|
count = text.count(old_str)
|
|
518
535
|
if count == 0:
|
|
519
|
-
|
|
536
|
+
import difflib
|
|
537
|
+
lines = text.splitlines()
|
|
538
|
+
old_lines = [ol.strip() for ol in old_str.splitlines() if ol.strip()]
|
|
539
|
+
target_line = old_lines[0] if old_lines else old_str.strip()
|
|
540
|
+
close_matches = difflib.get_close_matches(target_line, [l.strip() for l in lines], n=3, cutoff=0.3)
|
|
541
|
+
nearby = [(i + 1, l) for i, l in enumerate(lines) if l.strip() in close_matches]
|
|
542
|
+
hint = ""
|
|
543
|
+
if nearby:
|
|
544
|
+
hint = "\nClosest matching lines in file:\n" + "\n".join(f" Line {n}: {l.strip()[:80]}" for n, l in nearby[:3])
|
|
545
|
+
return (
|
|
546
|
+
f"error: old_str not found in file '{path}'. It must match existing text EXACTLY (including whitespace/indentation).{hint}\n"
|
|
547
|
+
f"→ Action: Run read_file on '{path}' around these lines to see the exact whitespace, then retry edit_file."
|
|
548
|
+
)
|
|
520
549
|
if count > 1:
|
|
521
|
-
return f"error: old_str matched {count} times
|
|
522
|
-
p.write_text(text.replace(old_str, new_str, 1))
|
|
550
|
+
return f"error: old_str matched {count} times in '{path}'. Must match exactly 1 occurrence. Include more surrounding lines in old_str to make it unique."
|
|
551
|
+
p.write_text(text.replace(old_str, new_str, 1), encoding="utf-8")
|
|
523
552
|
return "edit applied"
|
|
524
553
|
|
|
525
554
|
|
|
@@ -689,9 +718,10 @@ def _request_user_input(question: str) -> str:
|
|
|
689
718
|
try:
|
|
690
719
|
from rich.console import Console
|
|
691
720
|
from rich.panel import Panel
|
|
721
|
+
from rich.text import Text
|
|
692
722
|
console = Console()
|
|
693
723
|
console.print(Panel(
|
|
694
|
-
|
|
724
|
+
Text(question, style="bold"),
|
|
695
725
|
title="❓ Agent needs your input",
|
|
696
726
|
border_style="yellow",
|
|
697
727
|
))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|