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.
Files changed (41) hide show
  1. {alpiecode-5.1.1 → alpiecode-6.1.0}/PKG-INFO +1 -1
  2. {alpiecode-5.1.1 → alpiecode-6.1.0}/pyproject.toml +1 -1
  3. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/SOURCES.txt +1 -0
  5. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/__init__.py +1 -1
  6. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/agent.py +58 -8
  7. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/compaction.py +1 -1
  8. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/config.py +3 -3
  9. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/discovery.py +15 -18
  10. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/guardian.py +9 -4
  11. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/orchestrator.py +53 -26
  12. alpiecode-6.1.0/src/codeagent/progress.py +175 -0
  13. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/prompt.py +26 -7
  14. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/tools.py +39 -9
  15. {alpiecode-5.1.1 → alpiecode-6.1.0}/README.md +0 -0
  16. {alpiecode-5.1.1 → alpiecode-6.1.0}/setup.cfg +0 -0
  17. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode/__init__.py +0 -0
  18. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  19. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
  20. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/requires.txt +0 -0
  21. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/alpiecode.egg-info/top_level.txt +0 -0
  22. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/backends/__init__.py +0 -0
  23. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/backends/base.py +0 -0
  24. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/backends/local_backend.py +0 -0
  25. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/backends/openai_backend.py +0 -0
  26. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/cache.py +0 -0
  27. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/cli.py +0 -0
  28. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/client.py +0 -0
  29. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/context.py +0 -0
  30. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/doctor.py +0 -0
  31. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/executor.py +0 -0
  32. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/extension/alpiecode.vsix +0 -0
  33. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/github.py +0 -0
  34. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/ipython_ext.py +0 -0
  35. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/local_model.py +0 -0
  36. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/media.py +0 -0
  37. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/memory.py +0 -0
  38. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/server.py +0 -0
  39. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/session.py +0 -0
  40. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/updater.py +0 -0
  41. {alpiecode-5.1.1 → alpiecode-6.1.0}/src/codeagent/vscode_installer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 5.1.1
3
+ Version: 6.1.0
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "alpiecode"
7
- version = "5.1.1"
7
+ version = "6.1.0"
8
8
  description = "AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM"
9
9
  requires-python = ">=3.9"
10
10
  dependencies = [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alpiecode
3
- Version: 5.1.1
3
+ Version: 6.1.0
4
4
  Summary: AlpieCode — Autonomous AI Coding Agent CLI powered by local 169Pi GGUF VLM
5
5
  Requires-Python: >=3.9
6
6
  Requires-Dist: huggingface_hub>=0.20.0
@@ -25,6 +25,7 @@ src/codeagent/local_model.py
25
25
  src/codeagent/media.py
26
26
  src/codeagent/memory.py
27
27
  src/codeagent/orchestrator.py
28
+ src/codeagent/progress.py
28
29
  src/codeagent/prompt.py
29
30
  src/codeagent/server.py
30
31
  src/codeagent/session.py
@@ -16,7 +16,7 @@ Supports:
16
16
  from pathlib import Path
17
17
  from typing import Optional, Any, Dict
18
18
 
19
- __version__ = "2.0.4"
19
+ __version__ = "6.1.0"
20
20
 
21
21
 
22
22
  def run(task: str, workdir: str = ".", reasoning_level: str = "medium", **kwargs):
@@ -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", highlight=False)
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", highlight=False)
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] ({event.data['error']})", style="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❌ [bold red]Model Error[/bold red]\n Error: {event.data['error']}\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
- console.print(f"\n⚠️ Max turns ({event.data['max_turns']}) reached without completion.", style="bold yellow")
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
- console.print(f"❌ Model error: {event.data['error']}", style="bold red" if HAS_RICH else None)
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 = 20
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": 20,
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 = 20
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"] = 20
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, preferring WSL on Windows like Claude/Codex agents."""
180
+ """Detect available shell, aligned with tools.py execution path."""
182
181
 
183
182
  if os_name == "windows":
184
- # On Windows: prefer WSL (like Claude, Codex, Gemini agents do)
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
- "max_turns": 20,
513
- "max_tokens": 8192,
512
+ "max_tokens": 16384,
514
513
  "tool_set": "full",
515
- "enable_thinking": False,
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" ⚠️ [yellow]{reason}[/yellow]", highlight=False)
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" ⚠️ [yellow]{reason}[/yellow]", highlight=False)
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 max_turns and max_tokens ──
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
- # User override: if they set --max-turns explicitly, respect it
80
- if cfg.max_turns != 20: # 20 is new default, so non-default = explicit
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
- # ── Turn loop ──
147
- wrap_up_injected = False
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 + 1})
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 + 1})
157
-
158
- # ── Wrap-up injection at 80% of turns ──
159
- if not wrap_up_injected and turn >= int(effective_max_turns * 0.8):
160
- wrap_up_injected = True
161
- remaining = effective_max_turns - turn
162
- session.context.add_user_message(
163
- f"[SYSTEM] You have {remaining} turns remaining. "
164
- "Finish your current work now. If code is written and tested, "
165
- "output DONE: <summary>. If code has errors, make one final fix attempt."
166
- )
167
- yield AgentEvent("wrap_up", {"turn": turn + 1, "remaining": remaining})
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 + 1})
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 + 1,
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 + 1,
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 + 1,
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
- yield AgentEvent("max_turns_reached", {"max_turns": effective_max_turns})
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
- "max_turns": 20,
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
- prompt += "\n\n" + INTENT_CREATE
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=30, env=env,
442
+ timeout=120, env=env,
441
443
  )
442
444
 
443
- stdout = _smart_truncate(result.stdout, 6000)
444
- stderr = _smart_truncate(result.stderr, 4000)
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
- return "error: old_str not found in file. Make sure it matches exactly (including whitespace)."
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, need exactly 1 match. Use a more specific old_str."
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
- f"[bold]{question}[/bold]",
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