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
7 changes: 7 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-20) — Outbound CloudEvents Emitter](#whats-new-2026-06-20--outbound-cloudevents-emitter)
- [What's new (2026-06-20) — Environment-Scoped Typed Asset Store](#whats-new-2026-06-20--environment-scoped-typed-asset-store)
- [What's new (2026-06-20) — Task / Process Mining (Automation-Candidate Discovery)](#whats-new-2026-06-20--task--process-mining-automation-candidate-discovery)
- [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)
Expand Down Expand Up @@ -101,6 +102,12 @@

---

## What's new (2026-06-20) — Outbound CloudEvents Emitter

Emit run/automation events as CloudEvents. Full reference: [`docs/source/Eng/doc/new_features/v49_features_doc.rst`](docs/source/Eng/doc/new_features/v49_features_doc.rst).

- **`to_cloudevent` / `EventEmitter` / `post_cloudevent`** (`AC_emit_event`, `ac_emit_event`): the repo could receive webhooks but not **emit** events — this wraps run-lifecycle/assertion/failure data in a CloudEvents 1.0 (CNCF) envelope and optionally POSTs it over the egress-guarded HTTP client (interop with Knative, Azure Event Grid, iPaaS, generic webhooks). The `sink`/`poster` transport is injectable, so emission is unit-tested with no network.

## What's new (2026-06-20) — Environment-Scoped Typed Asset Store

Per-environment typed config + credential refs. Full reference: [`docs/source/Eng/doc/new_features/v48_features_doc.rst`](docs/source/Eng/doc/new_features/v48_features_doc.rst).
Expand Down
7 changes: 7 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-20) — 对外 CloudEvents 发送器](#本次更新-2026-06-20--对外-cloudevents-发送器)
- [本次更新 (2026-06-20) — 环境范围的带类型资产存储](#本次更新-2026-06-20--环境范围的带类型资产存储)
- [本次更新 (2026-06-20) — 任务 / 流程挖掘(自动化候选发现)](#本次更新-2026-06-20--任务--流程挖掘自动化候选发现)
- [本次更新 (2026-06-20) — 卡循环守卫(Agent Loop 进度检测)](#本次更新-2026-06-20--卡循环守卫agent-loop-进度检测)
Expand Down Expand Up @@ -100,6 +101,12 @@

---

## 本次更新 (2026-06-20) — 对外 CloudEvents 发送器

将运行/自动化事件以 CloudEvents 发送。完整参考:[`docs/source/Zh/doc/new_features/v49_features_doc.rst`](../docs/source/Zh/doc/new_features/v49_features_doc.rst)。

- **`to_cloudevent` / `EventEmitter` / `post_cloudevent`**(`AC_emit_event`、`ac_emit_event`):本项目能接收 webhook 却无法**发送**事件 —— 本功能将运行生命周期/断言/失败数据包进 CloudEvents 1.0(CNCF)信封,并可通过受出口守卫保护的 HTTP 客户端 POST 出去(与 Knative、Azure Event Grid、iPaaS、一般 webhook 互通)。`sink`/`poster` 传输可注入,因此发送在无网络下即可单元测试。

## 本次更新 (2026-06-20) — 环境范围的带类型资产存储

依环境的带类型配置 + credential 引用。完整参考:[`docs/source/Zh/doc/new_features/v48_features_doc.rst`](../docs/source/Zh/doc/new_features/v48_features_doc.rst)。
Expand Down
7 changes: 7 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-20) — 對外 CloudEvents 發送器](#本次更新-2026-06-20--對外-cloudevents-發送器)
- [本次更新 (2026-06-20) — 環境範圍的具型別資產儲存](#本次更新-2026-06-20--環境範圍的具型別資產儲存)
- [本次更新 (2026-06-20) — 任務 / 流程探勘(自動化候選發現)](#本次更新-2026-06-20--任務--流程探勘自動化候選發現)
- [本次更新 (2026-06-20) — 卡迴圈守衛(Agent Loop 進度偵測)](#本次更新-2026-06-20--卡迴圈守衛agent-loop-進度偵測)
Expand Down Expand Up @@ -100,6 +101,12 @@

---

## 本次更新 (2026-06-20) — 對外 CloudEvents 發送器

將執行/自動化事件以 CloudEvents 發送。完整參考:[`docs/source/Zh/doc/new_features/v49_features_doc.rst`](../docs/source/Zh/doc/new_features/v49_features_doc.rst)。

- **`to_cloudevent` / `EventEmitter` / `post_cloudevent`**(`AC_emit_event`、`ac_emit_event`):本專案能接收 webhook 卻無法**發送**事件 —— 本功能將執行生命週期/斷言/失敗資料包進 CloudEvents 1.0(CNCF)信封,並可透過受出口守衛保護的 HTTP 用戶端 POST 出去(與 Knative、Azure Event Grid、iPaaS、一般 webhook 互通)。`sink`/`poster` 傳輸可注入,因此發送在無網路下即可單元測試。

## 本次更新 (2026-06-20) — 環境範圍的具型別資產儲存

依環境的具型別設定 + credential 參照。完整參考:[`docs/source/Zh/doc/new_features/v48_features_doc.rst`](../docs/source/Zh/doc/new_features/v48_features_doc.rst)。
Expand Down
43 changes: 43 additions & 0 deletions docs/source/Eng/doc/new_features/v49_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
Outbound CloudEvents Emitter
============================

AutoControl can *receive* webhooks but had no way to *emit* events outbound.
CloudEvents 1.0 (CNCF) is the interop standard for event payloads — Knative,
Azure Event Grid, iPaaS, and generic webhook consumers all speak it. This wraps
run-lifecycle / assertion / failure data in a CloudEvents envelope and
(optionally) POSTs it over the HTTP binding, reusing the framework's egress
allowlist guard.

The transport is injectable (a ``sink`` / ``poster`` callable), so emission is
unit-testable with no network. Pure standard library; imports no ``PySide6``.

Headless API
------------

.. code-block:: python

from je_auto_control import to_cloudevent, post_cloudevent, EventEmitter

event = to_cloudevent("com.example.run.finished", "/runs/42",
{"status": "passed"}, subject="run-42")
# -> {specversion, id, source, type, time, datacontenttype, subject, data}

post_cloudevent("https://hooks.example.com/ce", event) # egress-guarded POST

emitter = EventEmitter(source="je_auto_control")
emitter.emit("run.started", {"flow": "checkout"})
emitter.events # captured envelopes

``to_cloudevent`` fills ``specversion`` / ``id`` (a fresh UUID) / ``time`` (now,
UTC) automatically; pass ``event_id`` / ``time`` to override. ``EventEmitter``
binds a fixed ``source`` and dispatches each envelope to a ``sink`` (an in-memory
log by default — inject your own to forward to a bus). ``post_cloudevent``
accepts a ``poster`` to inject a transport in tests.

Executor command
----------------

``AC_emit_event`` takes ``event_type`` (+ optional ``data`` / ``source`` /
``subject`` / ``url``); it returns ``{event}`` and, when ``url`` is given,
``{event, status}`` after POSTing. The same operation is exposed as the MCP tool
``ac_emit_event`` and as a Script Builder command under **Tools**.
1 change: 1 addition & 0 deletions docs/source/Eng/eng_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ Comprehensive guides for all AutoControl features.
doc/new_features/v46_features_doc
doc/new_features/v47_features_doc
doc/new_features/v48_features_doc
doc/new_features/v49_features_doc
doc/ocr_backends/ocr_backends_doc
doc/observability/observability_doc
doc/operations_layer/operations_layer_doc
Expand Down
39 changes: 39 additions & 0 deletions docs/source/Zh/doc/new_features/v49_features_doc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
對外 CloudEvents 發送器
=======================

AutoControl 能*接收* webhook,但一直無法*對外發送*事件。CloudEvents 1.0(CNCF)是事件
酬載的互通標準 —— Knative、Azure Event Grid、iPaaS 與一般 webhook 消費端皆採用。本功能
將執行生命週期 / 斷言 / 失敗資料包進 CloudEvents 信封,並(選擇性地)透過 HTTP binding
POST 出去,重用框架的出口允許清單守衛。

傳輸可注入(``sink`` / ``poster`` 可呼叫物件),因此發送在無網路下即可單元測試。純標準函
式庫;不匯入 ``PySide6``。

無頭 API
--------

.. code-block:: python

from je_auto_control import to_cloudevent, post_cloudevent, EventEmitter

event = to_cloudevent("com.example.run.finished", "/runs/42",
{"status": "passed"}, subject="run-42")
# -> {specversion, id, source, type, time, datacontenttype, subject, data}

post_cloudevent("https://hooks.example.com/ce", event) # 受出口守衛保護的 POST

emitter = EventEmitter(source="je_auto_control")
emitter.emit("run.started", {"flow": "checkout"})
emitter.events #擷取到的信封

``to_cloudevent`` 會自動填入 ``specversion`` / ``id``(新的 UUID)/ ``time``(現在,
UTC);可傳 ``event_id`` / ``time`` 覆寫。``EventEmitter`` 綁定固定的 ``source`` 並將每個
信封派送到 ``sink``(預設為記憶體內日誌 —— 可注入自己的以轉發到匯流排)。
``post_cloudevent`` 接受 ``poster`` 以在測試中注入傳輸。

執行器指令
----------

``AC_emit_event`` 接受 ``event_type``(以及選用的 ``data`` / ``source`` / ``subject`` /
``url``);回傳 ``{event}``,當提供 ``url`` 時於 POST 後回傳 ``{event, status}``。相同操作
亦提供為 MCP 工具 ``ac_emit_event``,以及 Script Builder 中 **Tools** 分類下的指令。
1 change: 1 addition & 0 deletions docs/source/Zh/zh_index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ AutoControl 所有功能的完整使用指南。
doc/new_features/v46_features_doc
doc/new_features/v47_features_doc
doc/new_features/v48_features_doc
doc/new_features/v49_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 @@ -268,6 +268,10 @@
from je_auto_control.utils.assets import (
Asset, AssetStore, AssetValue, active_environment,
)
# Outbound CloudEvents emitter
from je_auto_control.utils.events import (
EventEmitter, post_cloudevent, to_cloudevent,
)
# Background popup/interrupt watchdog (unattended automation)
from je_auto_control.utils.watchdog import (
PopupWatchdog, WatchdogRule, default_popup_watchdog,
Expand Down Expand Up @@ -728,6 +732,7 @@ def start_autocontrol_gui(*args, **kwargs):
"find_repeated_sequences", "mine_action_log",
"rank_automation_candidates",
"Asset", "AssetStore", "AssetValue", "active_environment",
"EventEmitter", "post_cloudevent", "to_cloudevent",
# MCP server
"AuditLogger", "HttpMCPServer", "MCPContent", "MCPPrompt",
"MCPPromptArgument", "MCPResource", "MCPServer", "MCPTool",
Expand Down
15 changes: 15 additions & 0 deletions je_auto_control/gui/script_builder/command_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -1099,6 +1099,21 @@ def _add_misc_specs(specs: List[CommandSpec]) -> None:
),
description="List assets (name/type/environment, no values).",
))
specs.append(CommandSpec(
"AC_emit_event", "Tools", "Emit CloudEvent",
fields=(
FieldSpec("event_type", FieldType.STRING,
placeholder="com.example.run.finished"),
FieldSpec("data", FieldType.STRING, optional=True,
placeholder='{"run_id": "42"}'),
FieldSpec("source", FieldType.STRING, optional=True,
default="je_auto_control"),
FieldSpec("subject", FieldType.STRING, optional=True),
FieldSpec("url", FieldType.STRING, optional=True,
placeholder="https://hooks.example.com/ce"),
),
description="Wrap data in a CloudEvents envelope; optionally POST it.",
))
specs.append(CommandSpec(
"AC_generate_sop", "Report", "Generate SOP Document",
fields=(
Expand Down
6 changes: 6 additions & 0 deletions je_auto_control/utils/events/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Outbound CloudEvents emitter for run-lifecycle/automation events."""
from je_auto_control.utils.events.cloud_events import (
EventEmitter, post_cloudevent, to_cloudevent,
)

__all__ = ["EventEmitter", "post_cloudevent", "to_cloudevent"]
76 changes: 76 additions & 0 deletions je_auto_control/utils/events/cloud_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""Emit automation events as CloudEvents (CNCF spec 1.0).

AutoControl can *receive* webhooks but had no way to *emit* events outbound.
CloudEvents 1.0 is the interop standard for event payloads (Knative, Azure Event
Grid, iPaaS, generic webhooks). This wraps run-lifecycle / assertion / failure
data in a CloudEvents envelope and (optionally) POSTs it over the HTTP binding,
reusing the framework's egress allowlist guard.

The transport is injectable (a ``sink`` callable), so emission is unit-testable
with no network. Pure standard library; imports no ``PySide6``.
"""
import datetime
import uuid
from typing import Any, Callable, Dict, List, Mapping, Optional

SPEC_VERSION = "1.0"


def to_cloudevent(event_type: str, source: str, data: Any, *,
subject: Optional[str] = None,
event_id: Optional[str] = None,
time: Optional[str] = None) -> Dict[str, Any]:
"""Wrap ``data`` in a CloudEvents 1.0 (structured JSON) envelope."""
envelope: Dict[str, Any] = {
"specversion": SPEC_VERSION,
"id": event_id or uuid.uuid4().hex,
"source": source,
"type": event_type,
"time": time or datetime.datetime.now(
datetime.timezone.utc).isoformat(),
"datacontenttype": "application/json",
"data": data,
}
if subject is not None:
envelope["subject"] = subject
return envelope


def post_cloudevent(url: str, event: Mapping[str, Any], *,
timeout: float = 10.0,
poster: Optional[Callable[..., Any]] = None) -> int:
"""POST a CloudEvent to ``url`` (structured mode); return the status code.

Uses the framework's egress-guarded HTTP client by default; pass ``poster``
to inject a transport in tests.
"""
if poster is not None:
return int(poster(url, event))
from je_auto_control.utils.http_client.http_client import http_request
headers = {"Content-Type": "application/cloudevents+json"}
response = http_request(url, method="POST", json_body=dict(event),
headers=headers, timeout=timeout)
return int(response.get("status", 0))


class EventEmitter:
"""Builds CloudEvents from a fixed source and dispatches them to a sink."""

def __init__(self, sink: Optional[Callable[[Dict[str, Any]], Any]] = None,
*, source: str = "je_auto_control") -> None:
"""``sink(event)`` receives each envelope; defaults to an in-memory log."""
self._source = source
self._log: List[Dict[str, Any]] = []
self._sink = sink if sink is not None else self._log.append

def emit(self, event_type: str, data: Any, *,
subject: Optional[str] = None) -> Dict[str, Any]:
"""Build a CloudEvent and hand it to the sink; return the envelope."""
event = to_cloudevent(event_type, self._source, data, subject=subject)
self._sink(event)
return event

@property
def events(self) -> List[Dict[str, Any]]:
"""Envelopes captured by the default in-memory sink."""
return list(self._log)
20 changes: 20 additions & 0 deletions je_auto_control/utils/executor/action_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -3268,6 +3268,25 @@ def _list_assets(environment: Optional[str] = None,
return store_list(environment=environment, db=db)


def _emit_event(event_type: str, data: Any = None,
source: str = "je_auto_control",
subject: Optional[str] = None,
url: Optional[str] = None) -> Dict[str, Any]:
"""Adapter: build a CloudEvent; optionally POST it (egress-guarded)."""
from je_auto_control.utils.events import post_cloudevent, to_cloudevent
if isinstance(data, str):
import json
try:
data = json.loads(data)
except ValueError:
pass
event = to_cloudevent(event_type, source, data, subject=subject)
result: Dict[str, Any] = {"event": event}
if url:
result["status"] = post_cloudevent(url, event)
return result


class Executor:
"""
Executor
Expand Down Expand Up @@ -3546,6 +3565,7 @@ def __init__(self):
"AC_set_asset": _set_asset,
"AC_get_asset": _get_asset,
"AC_list_assets": _list_assets,
"AC_emit_event": _emit_event,
"AC_a11y_record_start": _a11y_record_start,
"AC_a11y_record_stop": _a11y_record_stop,
"AC_a11y_record_events": _a11y_record_events,
Expand Down
21 changes: 20 additions & 1 deletion je_auto_control/utils/mcp_server/tools/_factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -3161,6 +3161,25 @@ def asset_tools() -> List[MCPTool]:
]


def events_tools() -> List[MCPTool]:
return [
MCPTool(
name="ac_emit_event",
description=("Wrap 'data' in a CloudEvents 1.0 envelope "
"('event_type', 'source', optional 'subject') and "
"optionally POST it to 'url' over the egress-guarded "
"HTTP client. Returns {event, status?}."),
input_schema=schema(
{"event_type": {"type": "string"}, "data": {},
"source": {"type": "string"},
"subject": {"type": "string"}, "url": {"type": "string"}},
["event_type"]),
handler=h.emit_event,
annotations=SIDE_EFFECT_ONLY,
),
]


def unattended_tools() -> List[MCPTool]:
return [
MCPTool(
Expand Down Expand Up @@ -4223,7 +4242,7 @@ def media_assert_tools() -> List[MCPTool]:
trajectory_eval_tools, compliance_tools, agent_trace_tools,
video_report_tools, fuzzy_tools, artifact_store_tools, image_dedup_tools,
locale_tools, voice_tools, coordinate_space_tools, loop_guard_tools,
process_mining_tools, asset_tools,
process_mining_tools, asset_tools, events_tools,
screen_record_tools,
process_and_shell_tools, remote_desktop_tools, gamepad_tools,
usb_passthrough_tools, assertion_tools, data_source_tools,
Expand Down
10 changes: 10 additions & 0 deletions je_auto_control/utils/mcp_server/tools/_handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1528,6 +1528,16 @@ def list_assets(environment=None, db=None):
return store_list(environment=environment, db=db)


def emit_event(event_type, data=None, source="je_auto_control",
subject=None, url=None):
from je_auto_control.utils.events import post_cloudevent, to_cloudevent
event = to_cloudevent(event_type, source, data, subject=subject)
result = {"event": event}
if url:
result["status"] = post_cloudevent(url, event)
return result


def vlm_locate(description: str,
screen_region: Optional[List[int]] = None,
model: Optional[str] = None) -> Optional[List[int]]:
Expand Down
Loading
Loading