Skip to content

Commit 1a8ad93

Browse files
abrichrclaude
andauthored
fix: demo guidance plan overview, anti-loop recovery, GRPO trainer fixes (#196)
Flywheel guidance: - Add get_plan_overview() to DemoLibrary: injects full demo strategy (all steps + keyboard shortcuts) instead of one-step-at-a-time guidance - Fix _build_enriched_instruction() producing "Click on Double-click..." and omit fake coordinates from manual demos - DemoGuidedAgent sets demo_guidance on base agent for anti-loop recovery Anti-loop recovery: - New _ANTI_LOOP_WARNING_WITH_DEMO directs planner to use keyboard shortcuts from demo strategy when stuck clicking unresponsive elements - _check_action_loop() uses demo-aware warning when demo_guidance is set Chrome popup suppression: - Add registry policy key disabling SpeedComparison in task setup - Launch Chrome with --no-first-run --disable-features=SpeedComparison - Send Escape to dismiss residual popups before closing Chrome GRPO trainer (standalone): - Add vision_loss_mode config: "exclude" (default), "include", "checkpoint" with warning log when vision tensors stripped from loss computation - Add VRAM recommendations for max_new_tokens (L40S: 512, A100: 1024-2048) - Add truncation warning when output hits max_new_tokens without action - Fix float parsing crash on CLICK(x=..., y=...) literal dots Validated: flywheel 0.00 -> 0.25 (+0.25) on clear-browsing-data-chrome Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ee62eb5 commit 1a8ad93

9 files changed

Lines changed: 293 additions & 34 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"task_id": "custom-clear-chrome-data",
3+
"demo_id": "manual",
4+
"description": "Clear browsing data in Google Chrome using Ctrl+Shift+Delete shortcut",
5+
"created_at": "2026-03-27T03:01:01.183920+00:00",
6+
"metadata": {
7+
"source": "manual",
8+
"created_by": "create_manual_demo.py"
9+
},
10+
"steps": [
11+
{
12+
"step_index": 0,
13+
"screenshot_path": "",
14+
"action_type": "click",
15+
"action_description": "CLICK(0.05, 0.20)",
16+
"target_description": "",
17+
"action_value": "",
18+
"x": 0.05,
19+
"y": 0.2,
20+
"description": "Double-click Google Chrome icon on the desktop to open Chrome",
21+
"metadata": {}
22+
},
23+
{
24+
"step_index": 1,
25+
"screenshot_path": "",
26+
"action_type": "key",
27+
"action_description": "KEY(ctrl+shift+delete)",
28+
"target_description": "",
29+
"action_value": "ctrl+shift+delete",
30+
"x": null,
31+
"y": null,
32+
"description": "Press Ctrl+Shift+Delete to open Clear Browsing Data dialog directly",
33+
"metadata": {}
34+
},
35+
{
36+
"step_index": 2,
37+
"screenshot_path": "",
38+
"action_type": "click",
39+
"action_description": "CLICK(0.50, 0.70)",
40+
"target_description": "",
41+
"action_value": "",
42+
"x": 0.5,
43+
"y": 0.7,
44+
"description": "Click the Clear data button in the Clear Browsing Data dialog",
45+
"metadata": {}
46+
},
47+
{
48+
"step_index": 3,
49+
"screenshot_path": "",
50+
"action_type": "key",
51+
"action_description": "KEY(alt+f4)",
52+
"target_description": "",
53+
"action_value": "alt+f4",
54+
"x": null,
55+
"y": null,
56+
"description": "Close Chrome after clearing data",
57+
"metadata": {}
58+
}
59+
]
60+
}

example_tasks/clear-browsing-data-chrome.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,18 @@ name: "Clear browsing data in Google Chrome"
44
id: custom-clear-chrome-data
55

66
setup:
7+
# Disable "Make Chrome faster" popup via Chrome policy registry key
8+
- execute: "powershell -c \"New-Item -Path 'HKLM:\\SOFTWARE\\Policies\\Google\\Chrome' -Force; Set-ItemProperty -Path 'HKLM:\\SOFTWARE\\Policies\\Google\\Chrome' -Name 'SpeedComparisonEnabled' -Value 0 -Type DWord\""
9+
- sleep: 1
710
# Populate Chrome with browsing history so there's data to clear
8-
- execute: "powershell -c \"Start-Process chrome 'https://example.com' -WindowStyle Normal\""
11+
# Launch with flags to suppress popups (no-first-run, disable SpeedComparison, etc.)
12+
- execute: "powershell -c \"Start-Process chrome @('https://example.com', '--no-first-run', '--disable-popup-blocking', '--disable-default-apps', '--disable-features=SpeedComparison')\""
913
- sleep: 3
1014
- execute: "powershell -c \"Start-Process chrome 'https://wikipedia.org'\""
1115
- sleep: 2
16+
# Dismiss any remaining Chrome popups by pressing Escape
17+
- execute: "powershell -c \"Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('{ESCAPE}')\""
18+
- sleep: 1
1219
# Close Chrome so the agent starts fresh
1320
- execute: "powershell -c 'Stop-Process -Name chrome -Force -ErrorAction SilentlyContinue'"
1421
- sleep: 2

example_tasks/notepad-hello.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
# Simple 2-step task: open Notepad and type "Hello World"
2-
name: "Open Notepad and type Hello World"
2+
name: "Open Windows Notepad (the built-in text editor, NOT VS Code or any browser) using the Start menu or Win+R run dialog, then type Hello World"
33
id: custom-notepad-hello
44

55
setup:
66
- execute: "powershell -c 'Stop-Process -Name notepad -Force -ErrorAction SilentlyContinue'"
77
- execute: "powershell -c 'Stop-Process -Name msedge -Force -ErrorAction SilentlyContinue'"
8+
- execute: "powershell -c 'Stop-Process -Name Code -Force -ErrorAction SilentlyContinue'"
89
- execute: "powershell -c 'Remove-Item $env:USERPROFILE\\Desktop\\*.txt -Force -ErrorAction SilentlyContinue'"
910
- sleep: 1
1011

openadapt_evals/agents/demo_guided_agent.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,9 +225,22 @@ def act(
225225
)
226226

227227
# -- Step 3: Augment task instruction ----------------------------------
228+
# On step 0, inject the FULL plan overview so the planner sees the
229+
# complete strategy. On subsequent steps, inject a lighter per-step
230+
# hint. The plan overview is always included so the planner can
231+
# track progress — it's cheap (a few lines of text).
228232
augmented_task = task
229233
if guidance.available:
230-
guidance_text = guidance.to_prompt_text()
234+
# Always include the plan overview so the planner knows the
235+
# overall strategy.
236+
plan_overview = self._demo_library.get_plan_overview(task.task_id)
237+
step_hint = guidance.to_prompt_text()
238+
239+
if plan_overview:
240+
guidance_text = f"{plan_overview}\n\n{step_hint}"
241+
else:
242+
guidance_text = step_hint
243+
231244
augmented_task = BenchmarkTask(
232245
task_id=task.task_id,
233246
instruction=f"{task.instruction}\n\n{guidance_text}",
@@ -237,6 +250,12 @@ def act(
237250
raw_config=task.raw_config,
238251
evaluation_spec=task.evaluation_spec,
239252
)
253+
254+
# Also set demo_guidance on the base agent so that anti-loop
255+
# recovery can reference the strategy (keyboard shortcuts etc.)
256+
if plan_overview and hasattr(self._base_agent, "demo_guidance"):
257+
self._base_agent.demo_guidance = plan_overview
258+
240259
logger.debug(
241260
"Injected demo guidance for step %d (confidence=%.2f)",
242261
self._step_index, guidance.confidence,

openadapt_evals/agents/planner_grounder_agent.py

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,25 @@
134134

135135
# Warning injected when repeated identical actions are detected.
136136
_ANTI_LOOP_WARNING = (
137-
"\nWARNING: Your last {n} actions were identical and failed. "
138-
"You MUST try a completely different approach: dismiss any dialogs, "
139-
"try keyboard shortcuts, or interact with different UI elements. "
137+
"\n⚠ WARNING: You are stuck in a loop. Your last {n} actions were "
138+
"identical and failed. You MUST try a completely different approach. "
140139
"Do NOT repeat the same action again.\n"
141140
)
142141

142+
# Stronger warning that references demo strategy when available.
143+
_ANTI_LOOP_WARNING_WITH_DEMO = (
144+
"\n⚠ WARNING: You are stuck in a loop. Your last {n} actions were "
145+
"identical and failed. You MUST try a completely different approach.\n\n"
146+
"A demonstration strategy is available (see above). Use it now:\n"
147+
"- If the demo mentions keyboard shortcuts (e.g., Ctrl+Shift+Delete, "
148+
"Win+R, Alt+F4), USE THEM instead of clicking.\n"
149+
"- If the demo suggests a different sequence of steps, follow that "
150+
"sequence instead of your current approach.\n"
151+
"- Prefer 'key' actions over 'click' actions when stuck — keyboard "
152+
"shortcuts bypass UI elements that may be unresponsive or mislocated.\n\n"
153+
"Do NOT repeat the same action. Pick a DIFFERENT action type.\n"
154+
)
155+
143156
# Number of consecutive identical actions that triggers the anti-loop warning.
144157
_ANTI_LOOP_THRESHOLD = 3
145158

@@ -563,6 +576,12 @@ def _check_action_loop(self) -> str:
563576
returns an anti-loop warning string to inject into the planner
564577
prompt. Otherwise returns an empty string.
565578
579+
When demo guidance is available (``self.demo_guidance`` is non-empty),
580+
the warning explicitly directs the planner to use keyboard shortcuts
581+
and alternative actions from the demonstration strategy, since the
582+
demo often contains fallback approaches (e.g., Ctrl+Shift+Delete)
583+
that work when clicking fails.
584+
566585
The comparison uses exact string matching on the instruction
567586
portion of the history entry (the text after ``(instruction: ``
568587
and before the closing ``)``).
@@ -595,6 +614,15 @@ def _check_action_loop(self) -> str:
595614
threshold,
596615
instructions[0],
597616
)
617+
# Use the demo-aware warning when demo guidance is available,
618+
# so the planner is directed to use keyboard shortcuts and
619+
# alternative approaches from the demonstration.
620+
if self.demo_guidance:
621+
logger.info(
622+
"Anti-loop: demo guidance available, injecting "
623+
"demo-strategy recovery prompt"
624+
)
625+
return _ANTI_LOOP_WARNING_WITH_DEMO.format(n=threshold)
598626
return _ANTI_LOOP_WARNING.format(n=threshold)
599627

600628
return ""

openadapt_evals/demo_library.py

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -202,14 +202,19 @@ def to_prompt_text(self) -> str:
202202
return ""
203203

204204
lines = [
205-
"DEMONSTRATION GUIDANCE:",
205+
"DEMONSTRATION GUIDANCE (current step hint):",
206206
f" Expected action: {self.action_type}",
207207
]
208208
if self.target_description:
209209
lines.append(f" Target: {self.target_description}")
210210
if self.action_value:
211211
lines.append(f" Value: {self.action_value}")
212-
lines.append(f" Instruction: {self.instruction}")
212+
# Use description (natural language) rather than
213+
# coordinate-based instruction when available.
214+
instruction = self.instruction
215+
if self.metadata.get("description"):
216+
instruction = self.metadata["description"]
217+
lines.append(f" Instruction: {instruction}")
213218
lines.append(
214219
" NOTE: Adapt if the current UI state differs from the demo. "
215220
"This is guidance, not a rigid script. "
@@ -1533,6 +1538,11 @@ def align_step(
15331538
if alignment_trace is not None:
15341539
guidance_metadata["alignment_trace"] = asdict(alignment_trace)
15351540

1541+
# Pass the step's natural-language description so to_prompt_text()
1542+
# can prefer it over coordinate-based instruction text.
1543+
if step.description:
1544+
guidance_metadata["description"] = step.description
1545+
15361546
return DemoGuidance(
15371547
available=True,
15381548
step_index=step.step_index,
@@ -1549,6 +1559,46 @@ def align_step(
15491559
visual_distance=visual_distance,
15501560
)
15511561

1562+
def get_plan_overview(self, task_id: str) -> str:
1563+
"""Return a high-level strategy overview of the full demo.
1564+
1565+
Instead of one step at a time, this gives the planner ALL demo
1566+
steps as a numbered plan. The planner can then use this as a
1567+
strategy reference while making its own decisions based on the
1568+
actual screenshot.
1569+
1570+
Coordinates are intentionally omitted -- the planner should
1571+
decide WHERE to click based on what it sees, not on hardcoded
1572+
positions from a demo recorded on a different screen.
1573+
1574+
Returns:
1575+
Multi-line string suitable for injection into the planner
1576+
prompt, or empty string if no demo exists.
1577+
"""
1578+
demo = self.get_demo(task_id)
1579+
if demo is None or not demo.steps:
1580+
return ""
1581+
1582+
lines = [
1583+
"DEMONSTRATION STRATEGY (from a successful prior execution):",
1584+
f"This task was previously completed in {len(demo.steps)} steps:",
1585+
]
1586+
for step in demo.steps:
1587+
# Prefer description (natural language) over action_description
1588+
label = step.description or step.action_description
1589+
action_hint = ""
1590+
if step.action_type == "key" and step.action_value:
1591+
action_hint = f" [{step.action_value}]"
1592+
lines.append(f" {step.step_index + 1}. {label}{action_hint}")
1593+
1594+
lines.append("")
1595+
lines.append(
1596+
"Use this as a STRATEGY GUIDE. Adapt to the current screen state -- "
1597+
"skip steps that are already done, and decide click targets based on "
1598+
"what you actually see, not memorized coordinates."
1599+
)
1600+
return "\n".join(lines)
1601+
15521602
def enrich_demo(
15531603
self,
15541604
task_id: str,
@@ -1707,13 +1757,17 @@ def _build_enriched_instruction(
17071757
) -> str:
17081758
"""Build a human-readable instruction from a DemoStep.
17091759
1710-
When the step has a VLM-generated ``description``, the instruction
1711-
reads like *"Click on three-dot menu button in Chrome toolbar at
1712-
approximately (0.960, 0.066)"* instead of the raw
1713-
``CLICK(0.960, 0.066)`` notation.
1760+
When the step has a natural-language ``description``, returns it
1761+
directly — the description already contains the action verb and
1762+
target (e.g., *"Double-click Google Chrome icon"*).
1763+
1764+
Coordinates from manual demos (hardcoded placeholders) are omitted
1765+
because they don't correspond to real screen positions. For VLM-
1766+
enriched demos with real screenshots, coordinates are included as
1767+
approximate hints.
17141768
1715-
For non-click actions or steps without descriptions, falls back to
1716-
the original ``action_description``.
1769+
For steps without descriptions, falls back to the original
1770+
``action_description``.
17171771
17181772
Args:
17191773
step: The demo step to build an instruction for.
@@ -1723,14 +1777,18 @@ def _build_enriched_instruction(
17231777
Returns:
17241778
Human-readable instruction string.
17251779
"""
1726-
if step.description and step.action_type == "click":
1727-
# Rich instruction with element description
1728-
if normalized_x is not None and normalized_y is not None:
1729-
return (
1730-
f"Click on {step.description} at approximately "
1731-
f"({normalized_x:.3f}, {normalized_y:.3f})"
1732-
)
1733-
return f"Click on {step.description}"
1780+
if step.description:
1781+
# If the description already reads as a complete sentence/
1782+
# instruction (starts with a verb), use it directly.
1783+
# Only append coordinates when we have real screenshots
1784+
# (manual demos have empty screenshot_path and fake coords).
1785+
if step.action_type == "click" and step.screenshot_path:
1786+
if normalized_x is not None and normalized_y is not None:
1787+
return (
1788+
f"{step.description} (approximately at "
1789+
f"({normalized_x:.3f}, {normalized_y:.3f}))"
1790+
)
1791+
return step.description
17341792

17351793
# Fallback: original action description
17361794
return step.action_description

openadapt_evals/training/standalone/config.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,24 @@ class TrainingConfig:
1717
num_rollouts_per_step: int = 8
1818
max_steps_per_episode: int = 15
1919
temperature: float = 0.7
20-
max_new_tokens: int = 512 # 2048 OOMs on L40S; 512 sufficient for Thought+Action
20+
# Maximum tokens the model may generate per action step.
21+
# VRAM recommendations (Qwen2.5-VL-7B, 4-bit LoRA, single image):
22+
# - L40S 48 GB → 512 (default, sufficient for Thought + Action)
23+
# - A100 80 GB → 1024–2048
24+
# - H100 80 GB → 2048+
25+
# Values above 512 on ≤48 GB GPUs will likely OOM during the loss
26+
# backward pass. If you see truncation warnings at runtime, increase
27+
# this value AND move to a larger GPU.
28+
max_new_tokens: int = 512
29+
30+
# Vision tensor handling during the loss forward pass.
31+
# "exclude" – (default) strip vision tensors; log-probs are text-only.
32+
# Safe on ≤48 GB GPUs.
33+
# "include" – keep vision tensors; full multimodal backward. May OOM
34+
# on < 80 GB VRAM.
35+
# "checkpoint" – gradient-checkpoint the vision encoder to cut peak VRAM.
36+
vision_loss_mode: str = "exclude"
37+
2138
server_url: str = "http://localhost:5001"
2239
task_ids: list[str] = field(default_factory=list)
2340
task_dir: str | None = None

openadapt_evals/training/standalone/prompt.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -113,8 +113,12 @@ def parse_vlm_output_to_action(
113113
xf = max(0.0, min(1.0, float(m.group(1))))
114114
yf = max(0.0, min(1.0, float(m.group(2))))
115115
return SimpleAction(type="click", x=int(xf * width), y=int(yf * height))
116-
except (ValueError, OverflowError):
117-
logger.warning("Malformed CLICK coords: x=%s y=%s", m.group(1), m.group(2))
116+
except (ValueError, TypeError, OverflowError):
117+
# The regex [\d.]+ can match literal "..." from model output
118+
# (e.g. CLICK(x=..., y=...)), causing float("...") to raise
119+
# ValueError. Fall through to DONE rather than crashing.
120+
logger.warning("Malformed CLICK coords: x=%s y=%s — returning DONE", m.group(1), m.group(2))
121+
return SimpleAction(type="done")
118122

119123
# TYPE(text="...")
120124
m = re.search(r"""TYPE\(text=["']([^"'\\]*(?:\\.[^"'\\]*)*)["']\)""", text, re.IGNORECASE)

0 commit comments

Comments
 (0)