alpiecode 6.0.0__tar.gz → 6.2.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.2.0}/PKG-INFO +1 -1
  2. {alpiecode-6.0.0 → alpiecode-6.2.0}/pyproject.toml +1 -1
  3. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/PKG-INFO +1 -1
  4. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/SOURCES.txt +1 -0
  5. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/__init__.py +1 -1
  6. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/agent.py +136 -55
  7. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/cli.py +0 -5
  8. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/config.py +3 -3
  9. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/discovery.py +0 -6
  10. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/guardian.py +5 -2
  11. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/orchestrator.py +52 -42
  12. alpiecode-6.2.0/src/codeagent/progress.py +175 -0
  13. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/prompt.py +0 -4
  14. {alpiecode-6.0.0 → alpiecode-6.2.0}/README.md +0 -0
  15. {alpiecode-6.0.0 → alpiecode-6.2.0}/setup.cfg +0 -0
  16. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode/__init__.py +0 -0
  17. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
  18. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
  19. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/requires.txt +0 -0
  20. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/top_level.txt +0 -0
  21. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/backends/__init__.py +0 -0
  22. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/backends/base.py +0 -0
  23. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/backends/local_backend.py +0 -0
  24. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/backends/openai_backend.py +0 -0
  25. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/cache.py +0 -0
  26. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/client.py +0 -0
  27. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/compaction.py +0 -0
  28. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/context.py +0 -0
  29. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/doctor.py +0 -0
  30. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/executor.py +0 -0
  31. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/extension/alpiecode.vsix +0 -0
  32. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/github.py +0 -0
  33. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/ipython_ext.py +0 -0
  34. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/local_model.py +0 -0
  35. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/media.py +0 -0
  36. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/memory.py +0 -0
  37. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/server.py +0 -0
  38. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/session.py +0 -0
  39. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/tools.py +0 -0
  40. {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/updater.py +0 -0
  41. {alpiecode-6.0.0 → alpiecode-6.2.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.2.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.2.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.2.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):
@@ -65,45 +65,88 @@ except ImportError:
65
65
  def _print_reasoning(reasoning: str):
66
66
  if not reasoning or not reasoning.strip():
67
67
  return
68
+ clean = reasoning.strip()
68
69
  if HAS_RICH:
69
- text = Text(reasoning.strip(), style="dim italic")
70
- console.print(Panel(text, title="💭 Thinking", border_style="dim blue", padding=(0, 1)))
70
+ console.print(Text(f"💭 {clean}", style="dim italic"))
71
71
  else:
72
- console.print(f"\n💭 Thinking: {reasoning.strip()}")
72
+ console.print(f"💭 {clean}")
73
+
74
+
75
+ def _format_tool_summary(name: str, args: dict) -> str:
76
+ if not isinstance(args, dict):
77
+ return ""
78
+ if name == "bash":
79
+ cmd = args.get("command", "").strip()
80
+ return f"$ {cmd[:80]}..." if len(cmd) > 80 else f"$ {cmd}"
81
+ elif name in ("write_file", "edit_file", "read_file"):
82
+ path = args.get("path", "")
83
+ extra = ""
84
+ if name == "write_file" and "content" in args:
85
+ lines = len(str(args["content"]).splitlines())
86
+ extra = f" ({lines} lines)"
87
+ return f"{path}{extra}"
88
+ elif name == "list_files":
89
+ return args.get("path", ".")
90
+ elif name in ("search", "web_search"):
91
+ return f"'{args.get('query', '')}'"
92
+ elif name == "fetch_web_page":
93
+ return args.get("url", "")
94
+ else:
95
+ parts = [f"{k}={repr(v)[:30]}" for k, v in args.items() if k != "content"]
96
+ return " ".join(parts)
73
97
 
74
98
 
75
99
  def _print_tool_call(turn: int, name: str, args: dict):
76
- display_args = {}
77
- for k, v in args.items():
78
- if isinstance(v, str) and len(v) > 200:
79
- display_args[k] = v[:200] + "..."
80
- else:
81
- display_args[k] = v
100
+ summary = _format_tool_summary(name, args)
82
101
  if HAS_RICH:
83
- args_str = json.dumps(display_args, indent=2)
84
- console.print(f"\n🔧 [bold cyan]Tool:[/bold cyan] [bold]{name}[/bold]", highlight=False)
85
- console.print(Text(f" {args_str}", style="cyan"))
102
+ console.print(f"\n⏺ [bold cyan]{name}[/bold cyan] [white]{summary}[/white]")
86
103
  else:
87
- console.print(f"\n🔧 Tool: {name}({display_args})")
104
+ console.print(f"\n {name} {summary}")
88
105
 
89
106
 
90
107
  def _print_tool_result(result: str):
91
- truncated = result[:1500] + ("..." if len(result) > 1500 else "")
108
+ clean = result.strip()
109
+ if '"exit_code"' in clean:
110
+ try:
111
+ data = json.loads(clean.split("\n", 1)[-1] if clean.startswith("⚠️") else clean)
112
+ out = data.get("stdout", "").strip()
113
+ err = data.get("stderr", "").strip()
114
+ code = data.get("exit_code", 0)
115
+ if code == 0:
116
+ clean = out if out else "success"
117
+ else:
118
+ clean = f"exit {code}: {err}" if err else f"exit {code}"
119
+ except Exception:
120
+ pass
121
+
122
+ lines = clean.splitlines()
123
+ if len(lines) > 8:
124
+ display = "\n".join(lines[:6]) + f"\n ... ({len(lines)-6} lines omitted)"
125
+ else:
126
+ display = clean[:500] + ("..." if len(clean) > 500 else "")
127
+
92
128
  if HAS_RICH:
93
- console.print(Text(f" {truncated}", style="green"))
129
+ style = "dim green" if not ("exit 1" in clean or "error:" in clean.lower()[:30]) else "dim red"
130
+ console.print(Text(f" └ {display}", style=style))
94
131
  else:
95
- console.print(f" {truncated}")
132
+ console.print(f" {display}")
96
133
 
97
134
 
98
135
  def _print_assistant_message(content: str):
136
+ if not content or not content.strip():
137
+ return
138
+ text = content.strip()
139
+ if text.upper().startswith("DONE:"):
140
+ text = text[5:].strip()
99
141
  if HAS_RICH:
100
142
  try:
101
- md = Markdown(content)
102
- console.print(Panel(md, title="🤖 Assistant", border_style="green", padding=(0, 1)))
143
+ console.print()
144
+ console.print(Markdown(text))
103
145
  except Exception:
104
- console.print(Panel(Text(content), title="🤖 Assistant", border_style="green", padding=(0, 1)))
146
+ console.print()
147
+ console.print(text)
105
148
  else:
106
- console.print(f"\n🤖 Assistant: {content}")
149
+ print(f"\n{text}")
107
150
 
108
151
 
109
152
  # ── Git helpers ───────────────────────────────────────────────────────
@@ -227,9 +270,27 @@ def run_agent(
227
270
 
228
271
  elif event.type == "start" and verbose:
229
272
  data = event.data
230
- if HAS_RICH:
231
- console.rule("[bold blue]Agent Started[/bold blue]")
232
- console.print(Text(f"📋 Task: {task.splitlines()[0]}", style="bold"))
273
+ if debug:
274
+ if HAS_RICH:
275
+ console.rule("[bold blue]Agent Started (Debug)[/bold blue]")
276
+ console.print(Text(f"📋 Task: {task.splitlines()[0]}", style="bold"))
277
+ console.print(f"📂 Workdir: {workdir}", style="dim")
278
+ mode_str = "[bold green]ONLINE[/bold green]" if not data["is_offline"] else "[bold yellow]OFFLINE[/bold yellow]"
279
+ console.print(f"🌐 Mode: {mode_str}", style="dim")
280
+ comp = data.get("complexity", "low")
281
+ comp_label = {"qa": "Q&A (instant)", "low": "Low (fast)", "medium": "Medium (balanced)", "high": "High (thorough)"}.get(comp, comp)
282
+ console.print(f"⚡ Complexity: {comp_label}", style="dim")
283
+ if last_discovery:
284
+ console.print(f"🔍 Discovery: {last_discovery.get('intent', 'create')} on {last_discovery.get('project_type', 'empty')} project", style="dim")
285
+ else:
286
+ print(f"[Start] task={task.splitlines()[0]}, mode={'offline' if data['is_offline'] else 'online'}")
287
+ else:
288
+ # Clean, professional presentation like Claude Code / Codex
289
+ if data.get("is_offline"):
290
+ if HAS_RICH:
291
+ console.print("[dim yellow]⚡ Offline mode[/dim yellow]")
292
+ else:
293
+ print("⚡ Offline mode")
233
294
  if github_repo:
234
295
  console.print(f"🐙 GitHub Repo: {github_repo}", style="cyan")
235
296
  if image_path:
@@ -238,36 +299,17 @@ def run_agent(
238
299
  console.print(f"🎬 Video: {video_path}", style="cyan")
239
300
  if url:
240
301
  console.print(f"📺 URL: {url}", style="cyan")
241
- console.print(f"📂 Workdir: {workdir}", style="dim")
242
- if not data["is_offline"]:
243
- console.print(f"🌐 Mode: [bold green]ONLINE[/bold green]", style="dim")
244
- else:
245
- console.print(f"🧠 Mode: [bold yellow]OFFLINE[/bold yellow]", style="dim")
246
- comp = data.get("complexity", "low")
247
- comp_label = {"qa": "Q&A (instant)", "low": "Low (fast)", "medium": "Medium (balanced)", "high": "High (thorough)"}.get(comp, comp)
248
- comp_color = {"qa": "cyan", "low": "green", "medium": "yellow", "high": "red"}.get(comp, "white")
249
- console.print(f"⚡ Complexity: [bold {comp_color}]{comp_label}[/bold {comp_color}]", style="dim")
250
- if last_discovery:
251
- intent_str = last_discovery.get('intent', 'create').title()
252
- proj_str = last_discovery.get('project_type', 'empty')
253
- console.print(f"🔍 Discovery: {intent_str} on {proj_str} project ({last_discovery.get('shell', 'bash')})", style="dim")
254
- if cfg.enable_thinking:
255
- console.print(f"🧠 Reasoning: [bold green]ON[/bold green]", style="dim")
256
- else:
257
- console.rule("Agent Started")
258
- console.print(f"📋 Task: {task.splitlines()[0]}")
259
- console.print(f"📂 Workdir: {workdir}")
260
302
 
261
303
  elif event.type == "adaptive_mode" and verbose and HAS_RICH:
262
304
  console.print("⚡ [dim]Adaptive mode: simple task detected, skipping deep reasoning for speed[/dim]")
263
305
 
264
306
  elif event.type == "turn_start":
265
307
  current_turn = event.data["turn"]
266
- if verbose:
308
+ if debug:
267
309
  if HAS_RICH:
268
- console.rule(f"[bold]Turn {current_turn}[/bold]", style="blue")
310
+ console.print(f"[dim]── Step {current_turn} ──[/dim]")
269
311
  else:
270
- console.rule(f"Turn {current_turn}")
312
+ print(f"── Step {current_turn} ──")
271
313
 
272
314
  elif event.type == "compaction" and verbose:
273
315
  console.print("🗜️ Compacting context (approaching token limit)...", style="yellow")
@@ -306,17 +348,56 @@ def run_agent(
306
348
  else:
307
349
  print(f"\n❌ Model Error: {event.data['error']}")
308
350
 
351
+ elif event.type == "stall_intervention" and verbose:
352
+ if HAS_RICH:
353
+ console.print(
354
+ f"🔄 [bold yellow]Progress stall detected[/bold yellow] "
355
+ f"(turn {event.data['turn']}, {event.data['consecutive_stalls']} stalled turns, "
356
+ f"intervention #{event.data['interventions']})",
357
+ style="yellow"
358
+ )
359
+ else:
360
+ print(f"🔄 Progress stall detected (turn {event.data['turn']})")
361
+
362
+ elif event.type == "turn_progress" and debug:
363
+ snap = event.data
364
+ status = "✅" if snap["had_progress"] else "⚠️"
365
+ if HAS_RICH:
366
+ console.print(
367
+ Text(f" {status} Progress: created={snap['files_created']}, "
368
+ f"modified={snap['files_modified']}, stalls={snap['consecutive_stalls']}"),
369
+ style="dim"
370
+ )
371
+
372
+ elif event.type == "safety_ceiling" and verbose:
373
+ if HAS_RICH:
374
+ console.print(
375
+ f"\n🛑 [bold red]Safety ceiling ({event.data['ceiling']} turns) reached.[/bold red]\n"
376
+ "This is an emergency stop — the agent may be stuck in an unrecoverable loop.",
377
+ style="bold red"
378
+ )
379
+ else:
380
+ print(f"\n🛑 Safety ceiling ({event.data['ceiling']}) reached.")
381
+
309
382
  elif event.type == "done":
310
- summary = event.data["summary"]
383
+ summary = event.data.get("summary", "")
311
384
  _checkpoint(workdir, "checkpoint: done")
312
- if verbose and HAS_RICH:
313
- if "DONE" in summary.upper():
314
- console.rule("[bold green]✅ Task Complete[/bold green]")
315
- else:
316
- console.rule("[bold yellow]💬 Agent Replied[/bold yellow]")
385
+ if debug and HAS_RICH:
386
+ console.rule("[bold green]✅ Complete[/bold green]")
317
387
 
318
388
  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")
389
+ progress = event.data.get("progress", {})
390
+ if HAS_RICH:
391
+ console.print(f"\n⚠️ [bold yellow]Safety ceiling ({event.data['max_turns']}) reached.[/bold yellow]", style="bold yellow")
392
+ if progress:
393
+ console.print(
394
+ f" Progress: {progress.get('progress_turns', 0)}/{progress.get('total_turns', 0)} turns made progress, "
395
+ f"{progress.get('files_created', 0)} files created, "
396
+ f"{progress.get('stall_interventions', 0)} stall interventions",
397
+ style="dim"
398
+ )
399
+ else:
400
+ print(f"\n⚠️ Safety ceiling ({event.data['max_turns']}) reached.")
320
401
 
321
402
  return session.context.messages if "session" in locals() else []
322
403
 
@@ -368,11 +449,11 @@ def run_chat(workdir: Path, cfg: Config, verbose: bool = True) -> None:
368
449
  break
369
450
 
370
451
  for event in orchestrator.run_task(session, user_input, cfg):
371
- if event.type == "turn_start" and verbose:
452
+ if event.type == "turn_start" and debug:
372
453
  if HAS_RICH:
373
- console.rule(f"[bold]Turn {event.data['turn']}[/bold]", style="blue")
454
+ console.print(f"[dim]── Step {event.data['turn']} ──[/dim]")
374
455
  else:
375
- console.rule(f"Turn {event.data['turn']}")
456
+ print(f"── Step {event.data['turn']} ──")
376
457
 
377
458
  elif event.type == "thinking" and verbose:
378
459
  _print_reasoning(event.data["content"])
@@ -162,8 +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
- _show_banner()
167
165
  from .agent import run_agent
168
166
  run_agent(
169
167
  args.task, Path(args.workdir), cfg,
@@ -178,13 +176,11 @@ def main():
178
176
  elif args.command == "chat":
179
177
  if args.max_turns:
180
178
  cfg.max_turns = args.max_turns
181
- cfg._explicit_max_turns = True
182
179
  _show_banner()
183
180
  from .agent import run_chat
184
181
  run_chat(Path(args.workdir), cfg, verbose=not args.quiet)
185
182
 
186
183
  elif args.command == "plan":
187
- _show_banner()
188
184
  plan_task = (
189
185
  f"PLANNING ONLY — Do NOT make any file edits. "
190
186
  f"Analyze the codebase and create a detailed implementation plan for the following task. "
@@ -208,7 +204,6 @@ def main():
208
204
  sys.exit(run_doctor())
209
205
 
210
206
  elif args.command == "explain":
211
- _show_banner()
212
207
  target = args.target
213
208
  target_path = Path(args.workdir) / target if not Path(target).is_absolute() else Path(target)
214
209
  if target_path.exists() and target_path.is_file():
@@ -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"],
@@ -48,6 +48,8 @@ SAFE_PREFIXES = [
48
48
  "python -m pytest", "python3 -m pytest", "pytest", "npm test",
49
49
  "npm run test", "make test", "cargo test", "go test",
50
50
  "python -c", "python3 -c", "node -e",
51
+ "python3 ", "python ", "node ", "bun ", "deno ", "cargo ", "go ",
52
+ "make", "cmake", "gcc", "g++", "clang", "clang++", "./",
51
53
  "tree", "sort", "uniq", "cut", "awk", "sed -n", "diff",
52
54
  "env", "printenv", "set",
53
55
  "type", "command -v",
@@ -138,8 +140,9 @@ def gate_command(command: str, auto_approve: bool = False) -> bool:
138
140
 
139
141
  if risk == RiskLevel.WARNING:
140
142
  if auto_approve:
141
- if HAS_RICH:
142
- console.print(Text(f" ⚠️ {reason}", style="yellow"))
143
+ if "Unknown command" not in reason:
144
+ if HAS_RICH:
145
+ console.print(Text(f" ⚠️ {reason}", style="yellow"))
143
146
  return True
144
147
  # In interactive mode, show warning but proceed
145
148
  if HAS_RICH:
@@ -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