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.
- {alpiecode-6.0.0 → alpiecode-6.1.0}/PKG-INFO +1 -1
- {alpiecode-6.0.0 → alpiecode-6.1.0}/pyproject.toml +1 -1
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/PKG-INFO +1 -1
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/SOURCES.txt +1 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/__init__.py +1 -1
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/agent.py +43 -1
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/cli.py +0 -2
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/config.py +3 -3
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/discovery.py +0 -6
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/orchestrator.py +52 -42
- alpiecode-6.1.0/src/codeagent/progress.py +175 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/prompt.py +0 -4
- {alpiecode-6.0.0 → alpiecode-6.1.0}/README.md +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/setup.cfg +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode/__init__.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/dependency_links.txt +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/entry_points.txt +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/requires.txt +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/alpiecode.egg-info/top_level.txt +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/backends/__init__.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/backends/base.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/backends/local_backend.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/backends/openai_backend.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/cache.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/client.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/compaction.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/context.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/doctor.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/executor.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/extension/alpiecode.vsix +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/github.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/guardian.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/ipython_ext.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/local_model.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/media.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/memory.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/server.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/session.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/tools.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/updater.py +0 -0
- {alpiecode-6.0.0 → alpiecode-6.1.0}/src/codeagent/vscode_installer.py +0 -0
|
@@ -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
|
-
|
|
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":
|
|
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"],
|
|
@@ -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
|
|
File without changes
|