Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

## Table of Contents

- [What's new (2026-06-19) — Resilience Primitives](#whats-new-2026-06-19--resilience-primitives)
- [What's new (2026-06-19) — Timed Input Macros](#whats-new-2026-06-19--timed-input-macros)
- [What's new (2026-06-19) — Semantic Screen State](#whats-new-2026-06-19--semantic-screen-state)
- [What's new (2026-06-19) — Set-of-Marks Overlay](#whats-new-2026-06-19--set-of-marks-overlay)
Expand Down Expand Up @@ -77,6 +78,13 @@

---

## What's new (2026-06-19) — Resilience Primitives

Reusable retry + circuit-breaker primitives. Full reference: [`docs/source/Eng/doc/new_features/v25_features_doc.rst`](docs/source/Eng/doc/new_features/v25_features_doc.rst).

- **RetryPolicy** — `RetryPolicy(...).run(fn)` / `retry_call(fn)`: retry on configured exceptions with exponential backoff (injectable sleep). (The existing `AC_retry` flow command already retries an action body; this is the reusable callable wrapper.)
- **CircuitBreaker** — `CircuitBreaker` / `CircuitOpenError` (`AC_circuit_call`, `ac_circuit_call`): open after N consecutive failures, short-circuit until a reset timeout, then half-open — stops a retry storm hammering a downed dependency. Injectable clock; `AC_circuit_call` runs an action list through a named breaker.

## What's new (2026-06-19) — Timed Input Macros

Replay input with timing fidelity + a press-hold-release DSL, full stack. Full reference: [`docs/source/Eng/doc/new_features/v24_features_doc.rst`](docs/source/Eng/doc/new_features/v24_features_doc.rst).
Expand Down
8 changes: 8 additions & 0 deletions README/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目录

- [本次更新 (2026-06-19) — 韧性原语](#本次更新-2026-06-19--韧性原语)
- [本次更新 (2026-06-19) — 计时输入宏](#本次更新-2026-06-19--计时输入宏)
- [本次更新 (2026-06-19) — 语义屏幕状态](#本次更新-2026-06-19--语义屏幕状态)
- [本次更新 (2026-06-19) — Set-of-Marks 叠图](#本次更新-2026-06-19--set-of-marks-叠图)
Expand Down Expand Up @@ -76,6 +77,13 @@

---

## 本次更新 (2026-06-19) — 韧性原语

可重用的 retry 与断路器原语。完整参考:[`docs/source/Zh/doc/new_features/v25_features_doc.rst`](../docs/source/Zh/doc/new_features/v25_features_doc.rst)。

- **RetryPolicy** — `RetryPolicy(...).run(fn)` / `retry_call(fn)`:在配置的异常上以指数退避重试(可注入 sleep)。(既有 `AC_retry` 流程指令已能对动作 body 重试;这是可重用的可调用包装器。)
- **CircuitBreaker** — `CircuitBreaker` / `CircuitOpenError`(`AC_circuit_call`、`ac_circuit_call`):连续失败 N 次后打开、短路至重置超时、再半开——避免重试风暴打垮已故障依赖。可注入 clock;`AC_circuit_call` 让动作列表通过具名断路器执行。

## 本次更新 (2026-06-19) — 计时输入宏

以时间保真度重播输入 + 按住-放开 DSL,走完整五层。完整参考:[`docs/source/Zh/doc/new_features/v24_features_doc.rst`](../docs/source/Zh/doc/new_features/v24_features_doc.rst)。
Expand Down
8 changes: 8 additions & 0 deletions README/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

## 目錄

- [本次更新 (2026-06-19) — 韌性原語](#本次更新-2026-06-19--韌性原語)
- [本次更新 (2026-06-19) — 計時輸入巨集](#本次更新-2026-06-19--計時輸入巨集)
- [本次更新 (2026-06-19) — 語意螢幕狀態](#本次更新-2026-06-19--語意螢幕狀態)
- [本次更新 (2026-06-19) — Set-of-Marks 疊圖](#本次更新-2026-06-19--set-of-marks-疊圖)
Expand Down Expand Up @@ -76,6 +77,13 @@

---

## 本次更新 (2026-06-19) — 韌性原語

可重用的 retry 與斷路器原語。完整參考:[`docs/source/Zh/doc/new_features/v25_features_doc.rst`](../docs/source/Zh/doc/new_features/v25_features_doc.rst)。

- **RetryPolicy** — `RetryPolicy(...).run(fn)` / `retry_call(fn)`:在設定的例外上以指數退避重試(可注入 sleep)。(既有 `AC_retry` 流程指令已能對動作 body 重試;這是可重用的可呼叫包裝器。)
- **CircuitBreaker** — `CircuitBreaker` / `CircuitOpenError`(`AC_circuit_call`、`ac_circuit_call`):連續失敗 N 次後開啟、短路至重置逾時、再半開——避免重試風暴打掛已故障依賴。可注入 clock;`AC_circuit_call` 讓動作清單透過具名斷路器執行。

## 本次更新 (2026-06-19) — 計時輸入巨集

以時間保真度重播輸入 + 按住-放開 DSL,走完整五層。完整參考:[`docs/source/Zh/doc/new_features/v24_features_doc.rst`](../docs/source/Zh/doc/new_features/v24_features_doc.rst)。
Expand Down
53 changes: 53 additions & 0 deletions docs/source/Eng/doc/new_features/v25_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
==================================================
New Features (2026-06-19) — Resilience Primitives
==================================================

Reusable resilience primitives — a retry-with-backoff policy and a circuit
breaker — plus an executor command that runs an action list through a named
breaker. Pure standard library; both primitives take injectable
``sleep`` / ``clock`` so they are unit-tested deterministically.

(The existing ``AC_retry`` flow command already retries an action *body*;
this adds the reusable :class:`RetryPolicy` callable wrapper and the new
:class:`CircuitBreaker`.)

.. contents::
:local:
:depth: 2


RetryPolicy
==========

::

from je_auto_control import RetryPolicy, retry_call

RetryPolicy(max_attempts=5, backoff=0.1, multiplier=2.0).run(flaky_fn)
retry_call(flaky_fn, max_attempts=3) # convenience

Retries ``func`` on the configured ``exceptions`` with exponential backoff
(``backoff * multiplier**n``, optionally capped by ``max_backoff``),
re-raising the last error when attempts are exhausted.


CircuitBreaker
=============

::

from je_auto_control import CircuitBreaker, CircuitOpenError

breaker = CircuitBreaker(failure_threshold=5, reset_timeout=30.0)
try:
breaker.call(call_remote_service)
except CircuitOpenError:
... # short-circuited — the dependency is down

Opens after ``failure_threshold`` consecutive failures and short-circuits
(raising :class:`CircuitOpenError`) until ``reset_timeout`` elapses, then
half-opens for one trial; a success closes it. Stops a retry storm from
hammering a downed dependency.

``AC_circuit_call`` / ``ac_circuit_call`` run an action list through a
**named** breaker (state shared across calls), returning ``{state, record}``.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v22_features_doc
doc/new_features/v23_features_doc
doc/new_features/v24_features_doc
doc/new_features/v25_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
50 changes: 50 additions & 0 deletions docs/source/Zh/doc/new_features/v25_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
==========================================
新功能 (2026-06-19) — 韌性原語
==========================================

可重用的韌性原語——retry-with-backoff 策略與斷路器(circuit breaker)
——並提供一個執行器指令,讓動作清單透過具名斷路器執行。純標準庫;兩個
原語都接受可注入的 ``sleep`` / ``clock``,因此能做決定性單元測試。

(既有的 ``AC_retry`` 流程指令已能對動作 *body* 重試;本功能新增可重用的
:class:`RetryPolicy` 可呼叫包裝器與全新的 :class:`CircuitBreaker`。)

.. contents::
:local:
:depth: 2


RetryPolicy
===========

::

from je_auto_control import RetryPolicy, retry_call

RetryPolicy(max_attempts=5, backoff=0.1, multiplier=2.0).run(flaky_fn)
retry_call(flaky_fn, max_attempts=3) # 便利函式

在設定的 ``exceptions`` 上以指數退避重試 ``func``(``backoff *
multiplier**n``,可用 ``max_backoff`` 上限夾限),嘗試耗盡後重新拋出最後
一個錯誤。


CircuitBreaker
==============

::

from je_auto_control import CircuitBreaker, CircuitOpenError

breaker = CircuitBreaker(failure_threshold=5, reset_timeout=30.0)
try:
breaker.call(call_remote_service)
except CircuitOpenError:
... # 已短路——依賴掛了

連續失敗達 ``failure_threshold`` 次後開啟並短路(拋出
:class:`CircuitOpenError`),直到 ``reset_timeout`` 過去後半開試一次;成功
即關閉。可避免重試風暴持續打掛已故障的依賴。

``AC_circuit_call`` / ``ac_circuit_call`` 讓動作清單透過**具名**斷路器
執行(狀態跨呼叫共享),回傳 ``{state, record}``。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v22_features_doc
doc/new_features/v23_features_doc
doc/new_features/v24_features_doc
doc/new_features/v25_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
5 changes: 5 additions & 0 deletions je_auto_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@
)
# Timed input replay + declarative input-sequence DSL
from je_auto_control.utils.input_macro import replay_timeline, run_sequence
# Resilience primitives (retry-with-backoff + circuit breaker)
from je_auto_control.utils.resilience import (
CircuitBreaker, CircuitOpenError, RetryPolicy, retry_call,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -604,6 +608,7 @@ def start_autocontrol_gui(*args, **kwargs):
"describe_screen", "diff_snapshots", "screen_changed", "snapshot",
"snapshot_screen",
"replay_timeline", "run_sequence",
"CircuitBreaker", "CircuitOpenError", "RetryPolicy", "retry_call",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
13 changes: 13 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,19 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
_add_set_of_marks_specs(specs)
_add_screen_state_specs(specs)
_add_input_macro_specs(specs)
_add_resilience_specs(specs)


def _add_resilience_specs(specs: List[CommandSpec]) -> None:
specs.append(CommandSpec(
"AC_circuit_call", "Flow", "Circuit Breaker Call",
fields=(
FieldSpec("name", FieldType.STRING),
FieldSpec("threshold", FieldType.INT, optional=True, default=5),
FieldSpec("reset_s", FieldType.FLOAT, optional=True, default=30.0),
),
description="Run 'actions' (JSON view) via a named circuit breaker.",
))


def _add_input_macro_specs(specs: List[CommandSpec]) -> None:
Expand Down
15 changes: 15 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2794,6 +2794,20 @@ def _input_sequence(steps: List[Dict[str, Any]]) -> Dict[str, Any]:
return {"log": run_sequence(steps)}


_CIRCUIT_BREAKERS: Dict[str, Any] = {}


def _circuit_call(name: str, actions: List[Any], threshold: int = 5,
reset_s: float = 30.0) -> Dict[str, Any]:
"""Adapter: run an action list through a named circuit breaker."""
from je_auto_control.utils.resilience import CircuitBreaker
breaker = _CIRCUIT_BREAKERS.setdefault(
name, CircuitBreaker(int(threshold), float(reset_s)))
record = breaker.call(
lambda: executor.execute_action(list(actions), raise_on_error=True))
return {"state": breaker.state, "record": record}


class Executor:
"""
Executor
Expand Down Expand Up @@ -3012,6 +3026,7 @@ def __init__(self):
"AC_describe_screen": _describe_screen,
"AC_replay_timeline": _replay_timeline,
"AC_input_sequence": _input_sequence,
"AC_circuit_call": _circuit_call,
"AC_a11y_record_start": _a11y_record_start,
"AC_a11y_record_stop": _a11y_record_stop,
"AC_a11y_record_events": _a11y_record_events,
Expand Down
22 changes: 21 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -2400,6 +2400,26 @@ def input_macro_tools() -> List[MCPTool]:
]


def resilience_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_circuit_call",
description=("Run an action list through a named circuit breaker: "
"after 'threshold' failures it opens and short-"
"circuits for 'reset_s' seconds. Returns {state, "
"record}."),
input_schema=schema({
"name": {"type": "string"},
"actions": {"type": "array"},
"threshold": {"type": "integer"},
"reset_s": {"type": "number"}},
required=["name", "actions"]),
handler=h.circuit_call,
annotations=SIDE_EFFECT_ONLY,
),
]


def unattended_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -3455,7 +3475,7 @@ def media_assert_tools() -> List[MCPTool]:
agent_memory_tools, determinism_tools, observer_tools,
sbom_tools, sharding_tools, data_quality_tools, i18n_tools,
checkpoint_tools, set_of_marks_tools, screen_state_tools,
input_macro_tools,
input_macro_tools, resilience_tools,
screen_record_tools,
process_and_shell_tools, remote_desktop_tools, gamepad_tools,
usb_passthrough_tools, assertion_tools, data_source_tools,
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1173,6 +1173,12 @@ def input_sequence(steps):
return {"log": _rs(steps)}


def circuit_call(name, actions, threshold=5, reset_s=30.0):
from je_auto_control.utils.executor.action_executor import _circuit_call
return _circuit_call(name, actions, threshold=int(threshold),
reset_s=float(reset_s))


def vlm_locate(description: str,
screen_region: Optional[List[int]] = None,
model: Optional[str] = None) -> Optional[List[int]]:
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/resilience/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Resilience primitives: retry-with-backoff and a circuit breaker."""
from je_auto_control.utils.resilience.resilience import (
CircuitBreaker, CircuitOpenError, RetryPolicy, retry_call,
)

__all__ = ["CircuitBreaker", "CircuitOpenError", "RetryPolicy", "retry_call"]
Loading
Loading