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.
- {alpiecode-6.0.0 → alpiecode-6.2.0}/PKG-INFO +1 -1
- {alpiecode-6.0.0 → alpiecode-6.2.0}/pyproject.toml +1 -1
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/SOURCES.txt +1 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/__init__.py +1 -1
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/agent.py +136 -55
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/cli.py +0 -5
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/config.py +3 -3
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/discovery.py +0 -6
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/guardian.py +5 -2
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/orchestrator.py +52 -42
- alpiecode-6.2.0/src/codeagent/progress.py +175 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/prompt.py +0 -4
- {alpiecode-6.0.0 → alpiecode-6.2.0}/README.md +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/setup.cfg +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode/__init__.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/backends/base.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/backends/local_backend.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/cache.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/client.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/compaction.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/context.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/doctor.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/executor.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/github.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/local_model.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/media.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/memory.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/server.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/session.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/tools.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/updater.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.2.0}/src/codeagent/vscode_installer.py +0 -0
|
@@ -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
|
-
|
|
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"
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
104
|
+
console.print(f"\n⏺ {name} {summary}")
|
|
88
105
|
|
|
89
106
|
|
|
90
107
|
def _print_tool_result(result: str):
|
|
91
|
-
|
|
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
|
-
|
|
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"
|
|
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
|
-
|
|
102
|
-
console.print(
|
|
143
|
+
console.print()
|
|
144
|
+
console.print(Markdown(text))
|
|
103
145
|
except Exception:
|
|
104
|
-
console.print(
|
|
146
|
+
console.print()
|
|
147
|
+
console.print(text)
|
|
105
148
|
else:
|
|
106
|
-
|
|
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
|
|
231
|
-
|
|
232
|
-
|
|
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
|
|
308
|
+
if debug:
|
|
267
309
|
if HAS_RICH:
|
|
268
|
-
console.
|
|
310
|
+
console.print(f"[dim]── Step {current_turn} ──[/dim]")
|
|
269
311
|
else:
|
|
270
|
-
|
|
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
|
|
383
|
+
summary = event.data.get("summary", "")
|
|
311
384
|
_checkpoint(workdir, "checkpoint: done")
|
|
312
|
-
if
|
|
313
|
-
|
|
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
|
-
|
|
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
|
|
452
|
+
if event.type == "turn_start" and debug:
|
|
372
453
|
if HAS_RICH:
|
|
373
|
-
console.
|
|
454
|
+
console.print(f"[dim]── Step {event.data['turn']} ──[/dim]")
|
|
374
455
|
else:
|
|
375
|
-
|
|
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":
|
|
29
|
+
"max_turns": 200,
|
|
30
30
|
"temperature": 0.1,
|
|
31
31
|
"max_tokens": 8192,
|
|
32
32
|
"enable_thinking": False, # Reasoning OFF by default
|
|
@@ -88,7 +88,7 @@ class Config:
|
|
|
88
88
|
model_repo: str = "169Pi/Alpie_learn_prototype_GGUF_NEW" # HuggingFace repo for offline GGUF
|
|
89
89
|
api_key: str = "not-needed"
|
|
90
90
|
hf_token: Optional[str] = None
|
|
91
|
-
max_turns: int =
|
|
91
|
+
max_turns: int = 200 # Safety ceiling only — agent runs until DONE
|
|
92
92
|
temperature: float = 0.1
|
|
93
93
|
max_tokens: int = 8192
|
|
94
94
|
enable_thinking: bool = False
|
|
@@ -117,7 +117,7 @@ def load_config() -> Config:
|
|
|
117
117
|
data["enable_thinking"] = False
|
|
118
118
|
data["temperature"] = 0.1
|
|
119
119
|
data["max_tokens"] = 8192
|
|
120
|
-
data["max_turns"] =
|
|
120
|
+
data["max_turns"] = 200
|
|
121
121
|
data["config_version"] = CONFIG_VERSION
|
|
122
122
|
needs_save = True
|
|
123
123
|
except Exception:
|
|
@@ -49,7 +49,6 @@ class TaskContext:
|
|
|
49
49
|
dependencies: List[str] = field(default_factory=list)
|
|
50
50
|
|
|
51
51
|
# Budget (computed from complexity)
|
|
52
|
-
max_turns: int = 10
|
|
53
52
|
max_tokens: int = 8192
|
|
54
53
|
tool_set: str = "core" # none, core, full
|
|
55
54
|
enable_thinking: bool = False
|
|
@@ -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
|
|
142
|
-
|
|
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
|
|
76
|
-
effective_max_turns = task_context.max_turns
|
|
76
|
+
# ── Determine effective max_tokens ──
|
|
77
77
|
effective_max_tokens = task_context.max_tokens
|
|
78
78
|
|
|
79
|
-
#
|
|
80
|
-
if
|
|
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
|
-
|
|
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
|
-
# ──
|
|
154
|
-
|
|
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
|
|
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
|
|
164
|
-
|
|
165
|
-
# ──
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
"
|
|
173
|
-
|
|
174
|
-
)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|