Skip to content

Commit 5272824

Browse files
committed
Add mechanical stuck-loop guard for agent loops
1 parent 952bb4a commit 5272824

15 files changed

Lines changed: 436 additions & 1 deletion

File tree

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
## Table of Contents
1515

16+
- [What's new (2026-06-20) — Stuck-Loop Guard (Agent Loop Progress Detection)](#whats-new-2026-06-20--stuck-loop-guard-agent-loop-progress-detection)
1617
- [What's new (2026-06-20) — Coordinate-Space Mapping (Model Grid ⇄ Physical Pixels)](#whats-new-2026-06-20--coordinate-space-mapping-model-grid--physical-pixels)
1718
- [What's new (2026-06-20) — Voice-Command Router](#whats-new-2026-06-20--voice-command-router)
1819
- [What's new (2026-06-20) — Locale-Aware Number, Currency & Date Parsing](#whats-new-2026-06-20--locale-aware-number-currency--date-parsing)
@@ -98,6 +99,12 @@
9899

99100
---
100101

102+
## What's new (2026-06-20) — Stuck-Loop Guard (Agent Loop Progress Detection)
103+
104+
Catch agents stuck in no-progress loops. Full reference: [`docs/source/Eng/doc/new_features/v46_features_doc.rst`](docs/source/Eng/doc/new_features/v46_features_doc.rst).
105+
106+
- **`LoopGuard` / `digest_result`** (`AC_loop_guard_observe` / `AC_loop_guard_reset`, `ac_*`): the top computer-use failure mode is an agent repeating an action with no effect — and the model can't see its own loop. `LoopGuard` watches the `(tool, args, result)` stream and flags `repeat` (same call N times), `ping_pong` (A-B-A-B), and `no_op` (observation digest unchanged), escalating `ok``warn``critical` by run length. Complements the step/time budget and offline trajectory eval; pure-stdlib, deterministic.
107+
101108
## What's new (2026-06-20) — Coordinate-Space Mapping (Model Grid ⇄ Physical Pixels)
102109

103110
Translate computer-use model clicks to real pixels. Full reference: [`docs/source/Eng/doc/new_features/v45_features_doc.rst`](docs/source/Eng/doc/new_features/v45_features_doc.rst).

README/README_zh-CN.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目录
1414

15+
- [本次更新 (2026-06-20) — 卡循环守卫(Agent Loop 进度检测)](#本次更新-2026-06-20--卡循环守卫agent-loop-进度检测)
1516
- [本次更新 (2026-06-20) — 坐标空间映射(模型网格 ⇄ 物理像素)](#本次更新-2026-06-20--坐标空间映射模型网格--物理像素)
1617
- [本次更新 (2026-06-20) — 语音指令路由器](#本次更新-2026-06-20--语音指令路由器)
1718
- [本次更新 (2026-06-20) — 区域设置感知的数字、货币与日期解析](#本次更新-2026-06-20--区域设置感知的数字货币与日期解析)
@@ -97,6 +98,12 @@
9798

9899
---
99100

101+
## 本次更新 (2026-06-20) — 卡循环守卫(Agent Loop 进度检测)
102+
103+
捕捉卡在无进展循环的 agent。完整参考:[`docs/source/Zh/doc/new_features/v46_features_doc.rst`](../docs/source/Zh/doc/new_features/v46_features_doc.rst)
104+
105+
- **`LoopGuard` / `digest_result`**(`AC_loop_guard_observe` / `AC_loop_guard_reset``ac_*`):电脑操作最主要的失败模式是 agent 重复一个无效果的动作 —— 而模型看不到自己的循环。`LoopGuard` 观察 `(tool, args, result)` 流并标记 `repeat`(相同调用 N 次)、`ping_pong`(A-B-A-B)与 `no_op`(观察摘要不变),依执行长度由 `ok``warn``critical` 升级。与步数/时间预算及离线轨迹评估互补;纯标准库、具确定性。
106+
100107
## 本次更新 (2026-06-20) — 坐标空间映射(模型网格 ⇄ 物理像素)
101108

102109
将电脑操作模型的点击转成物理像素。完整参考:[`docs/source/Zh/doc/new_features/v45_features_doc.rst`](../docs/source/Zh/doc/new_features/v45_features_doc.rst)

README/README_zh-TW.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
## 目錄
1414

15+
- [本次更新 (2026-06-20) — 卡迴圈守衛(Agent Loop 進度偵測)](#本次更新-2026-06-20--卡迴圈守衛agent-loop-進度偵測)
1516
- [本次更新 (2026-06-20) — 座標空間對映(模型網格 ⇄ 實體像素)](#本次更新-2026-06-20--座標空間對映模型網格--實體像素)
1617
- [本次更新 (2026-06-20) — 語音指令路由器](#本次更新-2026-06-20--語音指令路由器)
1718
- [本次更新 (2026-06-20) — 區域設定感知的數字、貨幣與日期解析](#本次更新-2026-06-20--區域設定感知的數字貨幣與日期解析)
@@ -97,6 +98,12 @@
9798

9899
---
99100

101+
## 本次更新 (2026-06-20) — 卡迴圈守衛(Agent Loop 進度偵測)
102+
103+
捕捉卡在無進展迴圈的 agent。完整參考:[`docs/source/Zh/doc/new_features/v46_features_doc.rst`](../docs/source/Zh/doc/new_features/v46_features_doc.rst)
104+
105+
- **`LoopGuard` / `digest_result`**(`AC_loop_guard_observe` / `AC_loop_guard_reset``ac_*`):電腦操作最主要的失敗模式是 agent 重複一個無效果的動作 —— 而模型看不到自己的迴圈。`LoopGuard` 觀察 `(tool, args, result)` 串流並標記 `repeat`(相同呼叫 N 次)、`ping_pong`(A-B-A-B)與 `no_op`(觀察摘要不變),依執行長度由 `ok``warn``critical` 升級。與步數/時間預算及離線軌跡評估互補;純標準函式庫、具確定性。
106+
100107
## 本次更新 (2026-06-20) — 座標空間對映(模型網格 ⇄ 實體像素)
101108

102109
將電腦操作模型的點擊轉成真實像素。完整參考:[`docs/source/Zh/doc/new_features/v45_features_doc.rst`](../docs/source/Zh/doc/new_features/v45_features_doc.rst)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
Stuck-Loop Guard (Agent Loop Progress Detection)
2+
================================================
3+
4+
The dominant computer-use failure mode is an agent burning its budget repeating
5+
an action that has no effect — and the model usually can't see its own loop, so
6+
it must be caught **mechanically** from outside, by watching the stream of
7+
``(tool, args, result)`` triples. ``LoopGuard`` flags three patterns:
8+
9+
- ``repeat`` — the same ``(tool, args)`` fired many times in a row;
10+
- ``ping_pong`` — two actions alternating A-B-A-B with no progress;
11+
- ``no_op`` — the observation (a screenshot/state digest) never changes.
12+
13+
It complements a step/time budget (which can't tell a productive loop from a
14+
stuck one) and the offline trajectory evaluator. Pure standard library
15+
(``collections`` + ``hashlib``), deterministic; imports no ``PySide6``.
16+
17+
Headless API
18+
------------
19+
20+
.. code-block:: python
21+
22+
from je_auto_control import LoopGuard, digest_result
23+
24+
guard = LoopGuard(warn=8, critical=15)
25+
for step in agent_steps:
26+
verdict = guard.observe(step.tool, step.args,
27+
digest_result(step.screenshot))
28+
if verdict.level == "critical":
29+
break # abort: stuck loop
30+
if verdict.level == "warn":
31+
nudge_the_model(verdict.pattern)
32+
33+
``observe`` returns ``{pattern, level, count}`` where ``level`` is ``ok`` /
34+
``warn`` / ``critical`` once the run length crosses the thresholds. ``count`` is
35+
the length of the detected run. ``digest_result`` makes a stable short hash of a
36+
screenshot/observation (bytes or any JSON-able value). ``reset`` clears history.
37+
38+
Executor commands
39+
-----------------
40+
41+
A module-level default guard backs the executor/MCP surfaces so a flow can track
42+
progress across steps:
43+
44+
================================ ===================================================
45+
Command Effect
46+
================================ ===================================================
47+
``AC_loop_guard_observe`` Feed a step; returns ``{pattern, level, count}``.
48+
``AC_loop_guard_reset`` Clear the default guard's history.
49+
================================ ===================================================
50+
51+
The same operations are exposed as MCP tools (``ac_loop_guard_observe`` /
52+
``ac_loop_guard_reset``) and as Script Builder commands under **Agent**.

docs/source/Eng/eng_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ Comprehensive guides for all AutoControl features.
6868
doc/new_features/v43_features_doc
6969
doc/new_features/v44_features_doc
7070
doc/new_features/v45_features_doc
71+
doc/new_features/v46_features_doc
7172
doc/ocr_backends/ocr_backends_doc
7273
doc/observability/observability_doc
7374
doc/operations_layer/operations_layer_doc
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
卡迴圈守衛(Agent Loop 進度偵測)
2+
================================
3+
4+
電腦操作最主要的失敗模式,是 agent 不斷重複一個沒有效果的動作而耗盡預算 —— 而模型通常
5+
看不到自己的迴圈,因此必須從外部以**機械方式**偵測,藉由觀察 ``(tool, args, result)``
6+
三元組串流。``LoopGuard`` 會標記三種模式:
7+
8+
- ``repeat`` —— 相同的 ``(tool, args)`` 連續觸發多次;
9+
- ``ping_pong`` —— 兩個動作以 A-B-A-B 交替而毫無進展;
10+
- ``no_op`` —— 觀察結果(截圖/狀態摘要)從未改變。
11+
12+
它與步數/時間預算(無法分辨有進展的迴圈與卡住的迴圈)以及離線軌跡評估互補。純標準函式
13+
庫(``collections`` + ``hashlib``)、具確定性;不匯入 ``PySide6``。
14+
15+
無頭 API
16+
--------
17+
18+
.. code-block:: python
19+
20+
from je_auto_control import LoopGuard, digest_result
21+
22+
guard = LoopGuard(warn=8, critical=15)
23+
for step in agent_steps:
24+
verdict = guard.observe(step.tool, step.args,
25+
digest_result(step.screenshot))
26+
if verdict.level == "critical":
27+
break # 中止:卡住的迴圈
28+
if verdict.level == "warn":
29+
nudge_the_model(verdict.pattern)
30+
31+
``observe`` 回傳 ``{pattern, level, count}``,其中 ``level`` 在執行長度跨過門檻後為
32+
``ok`` / ``warn`` / ``critical``。``count`` 為偵測到的執行長度。``digest_result`` 為截
33+
圖/觀察結果(位元組或任何可 JSON 化的值)產生穩定的短雜湊。``reset`` 清除歷史。
34+
35+
執行器指令
36+
----------
37+
38+
模組層級的預設守衛支撐 executor/MCP 介面,讓流程可跨步驟追蹤進度:
39+
40+
================================ ===================================================
41+
指令 效果
42+
================================ ===================================================
43+
``AC_loop_guard_observe`` 餵入一步;回傳 ``{pattern, level, count}``。
44+
``AC_loop_guard_reset`` 清除預設守衛的歷史。
45+
================================ ===================================================
46+
47+
相同操作亦提供為 MCP 工具(``ac_loop_guard_observe`` / ``ac_loop_guard_reset``),以及
48+
Script Builder 中 **Agent** 分類下的指令。

docs/source/Zh/zh_index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ AutoControl 所有功能的完整使用指南。
6868
doc/new_features/v43_features_doc
6969
doc/new_features/v44_features_doc
7070
doc/new_features/v45_features_doc
71+
doc/new_features/v46_features_doc
7172
doc/ocr_backends/ocr_backends_doc
7273
doc/observability/observability_doc
7374
doc/operations_layer/operations_layer_doc

je_auto_control/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,10 @@
255255
from je_auto_control.utils.coordinate_space import (
256256
CoordinateSpace, downscale_png, normalized_space, xga_space,
257257
)
258+
# Mechanical stuck-loop detection for agent loops
259+
from je_auto_control.utils.loop_guard import (
260+
LoopGuard, LoopVerdict, default_loop_guard, digest_result,
261+
)
258262
# Background popup/interrupt watchdog (unattended automation)
259263
from je_auto_control.utils.watchdog import (
260264
PopupWatchdog, WatchdogRule, default_popup_watchdog,
@@ -710,6 +714,7 @@ def start_autocontrol_gui(*args, **kwargs):
710714
"parse_number",
711715
"VoiceCommand", "VoiceRouter", "default_voice_router",
712716
"CoordinateSpace", "downscale_png", "normalized_space", "xga_space",
717+
"LoopGuard", "LoopVerdict", "default_loop_guard", "digest_result",
713718
# MCP server
714719
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
715720
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",

je_auto_control/gui/script_builder/command_schema.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,21 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
10411041
),
10421042
description="Map a physical-pixel coordinate to a model grid.",
10431043
))
1044+
specs.append(CommandSpec(
1045+
"AC_loop_guard_observe", "Agent", "Loop Guard: Observe Step",
1046+
fields=(
1047+
FieldSpec("tool", FieldType.STRING, placeholder="AC_click_mouse"),
1048+
FieldSpec("args", FieldType.STRING, optional=True,
1049+
placeholder='{"x": 10, "y": 20}'),
1050+
FieldSpec("result_digest", FieldType.STRING, optional=True),
1051+
),
1052+
description="Detect repeat/ping-pong/no-op stuck-loop patterns.",
1053+
))
1054+
specs.append(CommandSpec(
1055+
"AC_loop_guard_reset", "Agent", "Loop Guard: Reset",
1056+
fields=(),
1057+
description="Clear the default loop guard's history.",
1058+
))
10441059
specs.append(CommandSpec(
10451060
"AC_generate_sop", "Report", "Generate SOP Document",
10461061
fields=(

je_auto_control/utils/executor/action_executor.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3213,6 +3213,22 @@ def _to_model(x: int, y: int, physical_w: int, physical_h: int,
32133213
return {"x": mx, "y": my}
32143214

32153215

3216+
def _loop_guard_observe(tool: str, args: Any = None,
3217+
result_digest: str = "") -> Dict[str, Any]:
3218+
"""Adapter: feed a step to the default loop guard; report the verdict."""
3219+
from je_auto_control.utils.loop_guard import default_loop_guard
3220+
verdict = default_loop_guard.observe(tool, args, result_digest)
3221+
return {"pattern": verdict.pattern, "level": verdict.level,
3222+
"count": verdict.count}
3223+
3224+
3225+
def _loop_guard_reset() -> Dict[str, Any]:
3226+
"""Adapter: clear the default loop guard's history."""
3227+
from je_auto_control.utils.loop_guard import default_loop_guard
3228+
default_loop_guard.reset()
3229+
return {"reset": True}
3230+
3231+
32163232
class Executor:
32173233
"""
32183234
Executor
@@ -3485,6 +3501,8 @@ def __init__(self):
34853501
"AC_voice_clear": _voice_clear,
34863502
"AC_to_physical": _to_physical,
34873503
"AC_to_model": _to_model,
3504+
"AC_loop_guard_observe": _loop_guard_observe,
3505+
"AC_loop_guard_reset": _loop_guard_reset,
34883506
"AC_a11y_record_start": _a11y_record_start,
34893507
"AC_a11y_record_stop": _a11y_record_stop,
34903508
"AC_a11y_record_events": _a11y_record_events,

0 commit comments

Comments
 (0)