feat: release 2.2.0 with PC-only tests and execution reliability fixes

Unify task scheduling and queue state, fix Android input and observation recovery, preserve screenshot paths, and match final review criteria accurately. Add collapsible copyable results and publish the complete 2.1.0 upgrade notes. Validation: 3189 backend tests, 326 frontend tests, 24 client tests, production build and quality gates.
This commit is contained in:
chenmingxuan committed 2026-10-27 01:13:41 +08:00
1 parent fac4ccb760
commit be6e8c2abd
79 files changed
+2263 -277

No files matched your search

+44 -14
View File
@@ -28,9 +28,11 @@ import asyncio
import base64
import hashlib
import math
from io import BytesIO
from pathlib import Path
from langchain_core.messages import HumanMessage, SystemMessage
from PIL import Image
from artemis.agents.validator.categories import ValidationErrorCategory
from artemis.context import ArtemisContext
@@ -56,6 +58,21 @@ _MAX_ATTEMPTS = 3
_RETRY_DELAY_SECONDS = 0.3
def _annotate_target(image_bytes, coords, coordinate_size, **kwargs) -> bytes:
"""Map device pixels into each encoded screenshot before marking/cropping it."""
if coordinate_size is not None:
width, height = coordinate_size
with Image.open(BytesIO(image_bytes)) as image:
image_width, image_height = image.size
if abs((image_width / image_height) / (width / height) - 1) > 0.01:
raise ValueError("Screenshot orientation/geometry changed; observe the target again.")
x, y = coords
if not (0 <= x < width and 0 <= y < height):
raise ValueError("Target lies outside the observed device screen.")
coords = [round(x * image_width / width), round(y * image_height / height)]
return visualization.crop_and_annotate_target(image_bytes, coords, **kwargs)
def _strict_target(action_item: dict) -> bool:
return any(
action_item.get(key)
@@ -71,7 +88,7 @@ def _strict_target(action_item: dict) -> bool:
def _unverified(action_item: dict, message: str) -> tuple[bool, ValidationErrorCategory, str]:
if _strict_target(action_item):
return False, ValidationErrorCategory.PIXEL_TARGET_DISAPPEARED, message
return False, ValidationErrorCategory.PIXEL_VERIFICATION_UNAVAILABLE, message
return True, ValidationErrorCategory.PIXEL_BYPASSED, ""
@@ -182,6 +199,8 @@ async def _run_attempt(
action_item: dict,
state: State | None,
attempt: int,
*,
coordinate_size: tuple[int, int] | None = None,
) -> tuple[bool, ValidationErrorCategory, str] | None:
"""Runs one pixel validation attempt.
@@ -190,20 +209,18 @@ async def _run_attempt(
Raises on infrastructure errors so the caller's retry loop can decide.
"""
# A. Take fresh live screenshot (captures state changes / dynamic loading settling)
live_screenshot_b64 = None
try:
live_screenshot_b64 = await session.screenshot_b64()
success_img = True
except Exception:
success_img, live_screenshot_b64 = False, None
except Exception as exc:
raise RuntimeError(f"Failed to acquire live screenshot: {exc}") from exc
if not success_img or not live_screenshot_b64:
raise Exception("Failed to acquire live screenshot from controller/MCP tool.")
if not live_screenshot_b64:
raise RuntimeError("Live screenshot was empty; target identity could not be checked.")
# B. Decode and crop the live screen target
live_bytes = base64.b64decode(live_screenshot_b64)
live_crop_bytes = visualization.crop_and_annotate_target(
live_bytes, current_coords, crop_size=None, dot_radius=15
live_crop_bytes = _annotate_target(
live_bytes, current_coords, coordinate_size, crop_size=None, dot_radius=15
)
# C. Formulate multi-modal verification message
@@ -308,9 +325,19 @@ async def validate_action_precondition_pixel(
if not current_coords or len(current_coords) != 2:
current_coords = orig_coords
device = getattr(ctx, "device", None)
dimensions = (getattr(device, "device_width", None), getattr(device, "device_height", None))
coordinate_size = (
dimensions if all(type(value) is int and value > 0 for value in dimensions) else None
)
try:
neighborhood = visualization.crop_and_annotate_target(
base64.b64decode(pre_screenshot_b64), orig_coords, crop_size=256, dot_radius=0
neighborhood = _annotate_target(
base64.b64decode(pre_screenshot_b64),
orig_coords,
coordinate_size,
crop_size=256,
dot_radius=0,
)
except (ValueError, TypeError, OSError):
neighborhood = pre_screenshot_b64.encode()
@@ -342,8 +369,8 @@ async def validate_action_precondition_pixel(
# 2. Decode and crop the static reference (original) image ONCE outside the loop to save CPU
try:
orig_bytes = base64.b64decode(pre_screenshot_b64)
orig_crop_bytes = visualization.crop_and_annotate_target(
orig_bytes, orig_coords, crop_size=None, dot_radius=15
orig_crop_bytes = _annotate_target(
orig_bytes, orig_coords, coordinate_size, crop_size=None, dot_radius=15
)
except Exception as e:
logger.error(
@@ -378,6 +405,7 @@ async def validate_action_precondition_pixel(
action_item,
state,
attempt,
coordinate_size=coordinate_size,
)
if verdict is not None:
if not verdict[0]:
@@ -396,7 +424,9 @@ async def validate_action_precondition_pixel(
f" error: {last_error}. Target identity remains unverified."
)
return _unverified(
action_item, "Pixel verification failed; observe and locate the target again."
action_item,
f"Pixel verification unavailable after {_MAX_ATTEMPTS} attempts: {last_error}. "
"The action was not executed; restore observation before locating the target again.",
)
return _unverified(action_item, "Pixel validation did not establish target identity.")