alpiecode 6.0.0__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-6.0.0 → alpiecode-6.1.0}/PKG-INFO +1 -1
  2. {alpiecode-6.0.0 → alpiecode-6.1.0}/pyproject.toml +1 -1
  3. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/SOURCES.txt +1 -0
  5. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/__init__.py +1 -1
  6. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/agent.py +43 -1
  7. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/cli.py +0 -2
  8. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/config.py +3 -3
  9. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/discovery.py +0 -6
  10. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/orchestrator.py +52 -42
  11. alpiecode-6.1.0/src/codeagent/progress.py +175 -0
  12. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/prompt.py +0 -4
  13. {alpiecode-6.0.0 → alpiecode-6.1.0}/README.md +0 -0
  14. {alpiecode-6.0.0 → alpiecode-6.1.0}/setup.cfg +0 -0
  15. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode/__init__.py +0 -0
  16. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  17. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
  18. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/requires.txt +0 -0
  19. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/top_level.txt +0 -0
  20. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/backends/__init__.py +0 -0
  21. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/backends/base.py +0 -0
  22. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/backends/local_backend.py +0 -0
  23. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/backends/openai_backend.py +0 -0
  24. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/cache.py +0 -0
  25. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/client.py +0 -0
  26. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/compaction.py +0 -0
  27. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/context.py +0 -0
  28. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/doctor.py +0 -0
  29. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/executor.py +0 -0
  30. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/extension/alpiecode.vsix +0 -0
  31. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/github.py +0 -0
  32. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/guardian.py +0 -0
  33. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/ipython_ext.py +0 -0
  34. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/local_model.py +0 -0
  35. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/media.py +0 -0
  36. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/memory.py +0 -0
  37. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/server.py +0 -0
  38. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/session.py +0 -0
  39. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/tools.py +0 -0
  40. {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/updater.py +0 -0
  41. {alpiecode-6.0.0 → 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: 6.0.0
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 = "6.0.0"
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: 6.0.0
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):
@@ -306,6 +306,37 @@ def run_agent(
306
306
  else:
307
307
  print(f"\n❌ Model Error: {event.data['error']}")
308
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
+
309
340
  elif event.type == "done":
310
341
  summary = event.data["summary"]
311
342
  _checkpoint(workdir, "checkpoint: done")
@@ -316,7 +347,18 @@ def run_agent(
316
347
  console.rule("[bold yellow]💬 Agent Replied[/bold yellow]")
317
348
 
318
349
  elif event.type == "max_turns_reached" and verbose:
319
- 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.")
320
362
 
321
363
  return session.context.messages if "session" in locals() else []
322
364
 
@@ -162,7 +162,6 @@ def main():
162
162
  if args.command == "run":
163
163
  if args.max_turns:
164
164
  cfg.max_turns = args.max_turns
165
- cfg._explicit_max_turns = True
166
165
  _show_banner()
167
166
  from .agent import run_agent
168
167
  run_agent(
@@ -178,7 +177,6 @@ def main():
178
177
  elif args.command == "chat":
179
178
  if args.max_turns:
180
179
  cfg.max_turns = args.max_turns
181
- cfg._explicit_max_turns = True
182
180
  _show_banner()
183
181
  from .agent import run_chat
184
182
  run_chat(Path(args.workdir), cfg, verbose=not args.quiet)
@@ -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
@@ -500,25 +499,21 @@ def compute_complexity(intent: str, repo_info: dict, task: str) -> str:
500
499
 
501
500
  COMPLEXITY_CONFIG = {
502
501
  "qa": {
503
- "max_turns": 3,
504
502
  "max_tokens": 4096,
505
503
  "tool_set": "none",
506
504
  "enable_thinking": False,
507
505
  },
508
506
  "low": {
509
- "max_turns": 15,
510
507
  "max_tokens": 8192,
511
508
  "tool_set": "core",
512
509
  "enable_thinking": False,
513
510
  },
514
511
  "medium": {
515
- "max_turns": 40,
516
512
  "max_tokens": 16384,
517
513
  "tool_set": "full",
518
514
  "enable_thinking": True,
519
515
  },
520
516
  "high": {
521
- "max_turns": 60,
522
517
  "max_tokens": 16384,
523
518
  "tool_set": "full",
524
519
  "enable_thinking": True,
@@ -568,7 +563,6 @@ def build_task_context(task: str, workdir: Path) -> TaskContext:
568
563
  entry_points=repo["entry_points"],
569
564
  dependencies=repo["dependencies"],
570
565
  # Budget
571
- max_turns=budget["max_turns"],
572
566
  max_tokens=budget["max_tokens"],
573
567
  tool_set=budget["tool_set"],
574
568
  enable_thinking=budget["enable_thinking"],
@@ -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 getattr(cfg, "_explicit_max_turns", False):
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,12 +136,7 @@ class AgentOrchestrator:
137
136
  "complexity": complexity,
138
137
  })
139
138
 
140
- if complexity in ("medium", "high"):
141
- session.context.add_user_message(
142
- f"[BUDGET & GOAL] Available turn budget: {effective_max_turns} turns. "
143
- "Plan the needed components, create the complete files, verify with bash, "
144
- "and finish with DONE: <summary> as soon as verification succeeds."
145
- )
139
+
146
140
 
147
141
  # ── Adaptive thinking ──
148
142
  enable_thinking = cfg.enable_thinking or task_context.enable_thinking
@@ -150,40 +144,43 @@ class AgentOrchestrator:
150
144
  enable_thinking = False
151
145
  yield AgentEvent("adaptive_mode", {"message": "Simple task detected, skipping deep reasoning."})
152
146
 
153
- # ── Turn loop ──
154
- 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
155
153
 
156
- for turn in range(effective_max_turns):
157
154
  if session.cancelled:
158
- yield AgentEvent("cancelled", {"turn": turn + 1})
155
+ yield AgentEvent("cancelled", {"turn": turn})
159
156
  break
160
157
 
161
158
  # Context compaction
162
159
  if session.context.check_and_compact():
163
- yield AgentEvent("compaction", {"turn": turn + 1})
164
-
165
- # ── Progressive wrap-up injection ──
166
- # Step 1: Gentle verification reminder at 70%
167
- if not wrap_up_injected and turn >= int(effective_max_turns * 0.70):
168
- wrap_up_injected = True
169
- remaining = effective_max_turns - turn
170
- session.context.add_user_message(
171
- f"[SYSTEM] Turn budget update: {remaining} turns remaining. "
172
- "Ensure all necessary files are created and run verification tests now. "
173
- "As soon as verification succeeds, output DONE: <summary>."
174
- )
175
- yield AgentEvent("wrap_up", {"turn": turn + 1, "remaining": remaining})
176
-
177
- # Step 2: Final wrap-up call at 90%
178
- if not getattr(self, "_final_wrap_up_injected", False) and turn >= int(effective_max_turns * 0.90):
179
- self._final_wrap_up_injected = True
180
- remaining = effective_max_turns - turn
181
- session.context.add_user_message(
182
- f"[SYSTEM] FINAL TURNS: Only {remaining} turns remaining. "
183
- "Do not start new exploration. Fix any remaining errors and output DONE: <summary>."
184
- )
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
185
182
 
186
- yield AgentEvent("turn_start", {"turn": turn + 1})
183
+ yield AgentEvent("turn_start", {"turn": turn})
187
184
 
188
185
  try:
189
186
  if enable_thinking:
@@ -235,7 +232,7 @@ class AgentOrchestrator:
235
232
  results = session.executor.execute_tool_calls(tool_calls)
236
233
  for res in results:
237
234
  yield AgentEvent("tool_result", {
238
- "turn": turn + 1,
235
+ "turn": turn,
239
236
  "id": res.tool_call_id,
240
237
  "name": res.name,
241
238
  "content": res.content,
@@ -263,7 +260,7 @@ class AgentOrchestrator:
263
260
  if tool_calls:
264
261
  for tc in tool_calls:
265
262
  yield AgentEvent("tool_call", {
266
- "turn": turn + 1,
263
+ "turn": turn,
267
264
  "id": tc.id,
268
265
  "name": tc.name,
269
266
  "arguments": tc.arguments,
@@ -273,7 +270,7 @@ class AgentOrchestrator:
273
270
 
274
271
  for res in results:
275
272
  yield AgentEvent("tool_result", {
276
- "turn": turn + 1,
273
+ "turn": turn,
277
274
  "id": res.tool_call_id,
278
275
  "name": res.name,
279
276
  "content": res.content,
@@ -281,6 +278,18 @@ class AgentOrchestrator:
281
278
  })
282
279
  session.context.add_tool_result(res.tool_call_id, res.content)
283
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
+
284
293
  continue
285
294
 
286
295
  # Text-only response = done
@@ -302,5 +311,6 @@ class AgentOrchestrator:
302
311
  yield AgentEvent("done", {"summary": "Task completed."})
303
312
  return
304
313
 
305
- 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})
306
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": 15,
343
341
  "max_tokens": 8192,
344
342
  "tools": "core", # 5 core tools
345
343
  "prompt": "default",
346
344
  },
347
345
  "medium": {
348
- "max_turns": 40,
349
346
  "max_tokens": 16384,
350
347
  "tools": "full", # All 15 tools
351
348
  "prompt": "default",
352
349
  },
353
350
  "high": {
354
- "max_turns": 60,
355
351
  "max_tokens": 16384,
356
352
  "tools": "full", # All 15 tools
357
353
  "prompt": "high", # Detailed system prompt
File without changes
File without changes