test(computer): measure independently verified tasks (#36216)

* test(computer): measure independently verified tasks

* ci(computer): fit benchmark to repository guards

* fix(computer): stabilize benchmark fixtures and CI
This commit is contained in:
Kelvin Amoaba
2026-10-30 04:20:18 +00:00
committed by GitHub
parent 54a4f26ec8
commit 596e5f52d6
14 changed files with 2187 additions and 0 deletions
@@ -28,6 +28,7 @@ jobs:
['ssh-hostile-hosts', 'ssh-hostile-hosts.yml'],
['mobile', 'mobile.yml'],
['computer-e2e', 'computer-e2e.yml'],
['computer-task-benchmark', 'computer-task-benchmark.yml'],
['computer-macos-native', 'computer-macos-native.yml'],
['wsl-orcad-e2e', 'wsl-orcad-e2e.yml'],
['performance-contracts', 'performance-contracts.yml'],
@@ -0,0 +1,158 @@
name: Computer-use task benchmark
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, converted_to_draft]
paths:
- 'config/benchmarks/computer-use/**'
- 'native/computer-use-linux/**'
- 'native/computer-use-windows/**'
- '.github/workflows/computer-task-benchmark.yml'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: computer-task-benchmark-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
ORCA_BACKGROUND_LAUNCH: '1'
CUA_TAG: cua-driver-rs-v0.38.0
CUA_DRIVER_RS_TELEMETRY_ENABLED: '0'
DO_NOT_TRACK: '1'
CUA_DRIVER_RS_UPDATE_CHECK: '0'
ELECTRON_VERSION: '43.8.1'
BASELINE_SHA: 030c88deecf3adc212d22f57b085e4bd5c6e48e6
jobs:
contract:
if: github.event_name != 'pull_request' || github.event.pull_request.draft != true
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- run: python -m unittest discover -s config/benchmarks/computer-use -p 'test_*.py'
linux:
if: github.event_name != 'pull_request' || github.event.pull_request.draft != true
runs-on: ubuntu-latest
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
target: [fixture, electron]
env:
BENCH_TARGET: ${{ matrix.target }}
ORCA_COMPUTER_BENCH_ISOLATED: '1'
ACCESSIBILITY_ENABLED: '1'
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Fetch immutable baseline
run: git fetch --no-tags --depth=1 origin "$BASELINE_SHA"
- name: Install isolated desktop and accessibility libraries
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends xvfb openbox dbus-x11 at-spi2-core python3-gi \
gir1.2-atspi-2.0 gir1.2-gtk-3.0 python3-tk xdotool xclip ripgrep libnss3 libgbm1 libasound2t64
- name: Download and verify pinned cua-driver
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p "$RUNNER_TEMP/cua-dl" "$RUNNER_TEMP/cua"
cd "$RUNNER_TEMP/cua-dl"
asset='cua-driver-rs-0.38.0-linux-x86_64-binary.tar.gz'
gh release download "$CUA_TAG" --repo trycua/cua --pattern checksums.txt --pattern "$asset"
rg " $asset\$" checksums.txt | sha256sum -c -
tar -xzf "$asset" -C "$RUNNER_TEMP/cua"
- name: Install Electron
if: matrix.target == 'electron'
run: |
npm install --no-save --no-package-lock --prefix "$RUNNER_TEMP/electron" "electron@$ELECTRON_VERSION"
node "$RUNNER_TEMP/electron/node_modules/electron/install.js"
- name: Run task comparison on Xvfb
run: |
export CANDIDATE_SHA="$(git rev-parse HEAD)"
xvfb-run -a --server-args='-screen 0 1280x1024x24' dbus-run-session -- bash -euc '
openbox >"$RUNNER_TEMP/openbox.log" 2>&1 &
/usr/bin/python3 config/benchmarks/computer-use/bench.py --target "$BENCH_TARGET" \
--baseline "$BASELINE_SHA" --candidate "$CANDIDATE_SHA" \
--cua "$RUNNER_TEMP/cua/cua-driver" \
--electron "$RUNNER_TEMP/electron/node_modules/electron/dist/electron" \
--out "out/task-linux-$BENCH_TARGET" --n 3 --budget 1800
'
- if: always()
uses: actions/upload-artifact@v4
with:
name: computer-task-linux-${{ matrix.target }}
path: out/task-linux-${{ matrix.target }}
if-no-files-found: warn
windows:
if: github.event_name != 'pull_request' || github.event.pull_request.draft != true
runs-on: windows-latest
timeout-minutes: 40
strategy:
fail-fast: false
matrix:
target: [fixture, electron]
env:
BENCH_TARGET: ${{ matrix.target }}
ORCA_COMPUTER_BENCH_ISOLATED: '1'
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- uses: actions/setup-python@v6
with:
python-version: '3.12'
- name: Fetch immutable baseline
run: git fetch --no-tags --depth=1 origin $env:BASELINE_SHA
- name: Download and verify pinned cua-driver
env:
GH_TOKEN: ${{ github.token }}
shell: pwsh
run: |
$download = Join-Path $env:RUNNER_TEMP 'cua-dl'
$destination = Join-Path $env:RUNNER_TEMP 'cua'
New-Item -ItemType Directory -Force $download, $destination | Out-Null
$asset = 'cua-driver-rs-0.38.0-windows-x86_64-binary.zip'
gh release download $env:CUA_TAG --repo trycua/cua --pattern checksums.txt --pattern $asset --dir $download
$matches = @(Select-String -Path (Join-Path $download 'checksums.txt') -Pattern " $([regex]::Escape($asset))$")
if ($matches.Count -ne 1) { throw 'expected exactly one checksum' }
$expected = $matches[0].Line.Split(' ')[0]
$actual = (Get-FileHash (Join-Path $download $asset) -Algorithm SHA256).Hash.ToLower()
if ($expected -ne $actual) { throw 'cua-driver checksum mismatch' }
Expand-Archive (Join-Path $download $asset) -DestinationPath $destination
- name: Install Electron
if: matrix.target == 'electron'
run: |
npm install --no-save --no-package-lock --prefix "$env:RUNNER_TEMP\electron" "electron@$env:ELECTRON_VERSION"
node "$env:RUNNER_TEMP\electron\node_modules\electron\install.js"
- name: Run task comparison on disposable Windows desktop
run: |
$candidate = git rev-parse HEAD
python config/benchmarks/computer-use/bench.py --target $env:BENCH_TARGET `
--baseline $env:BASELINE_SHA --candidate $candidate `
--cua "$env:RUNNER_TEMP\cua\cua-driver.exe" `
--electron "$env:RUNNER_TEMP\electron\node_modules\electron\dist\electron.exe" `
--out "out/task-windows-$env:BENCH_TARGET" --n 3 --budget 1800
- if: always()
uses: actions/upload-artifact@v4
with:
name: computer-task-windows-${{ matrix.target }}
path: out/task-windows-${{ matrix.target }}
if-no-files-found: warn
+78
View File
@@ -0,0 +1,78 @@
# Computer-use task benchmark
Compares Orca's direct Linux AT-SPI and Windows UI Automation providers with
cua-driver 0.38.0. This does not measure the CLI, sidecar, model, or complete agent
workflows. It does not install cua-driver in Orca.
Run the `Computer-use task benchmark` GitHub Actions workflow on a branch. Native
input runs only on its disposable Windows runner or Linux Xvfb desktop. The runner
refuses to run on the developer's desktop. Keep `ORCA_BACKGROUND_LAUNCH=1`.
The first matrix covers GTK/WinForms and Electron fixtures. Editor applications,
Wayland, macOS and additional Windows control frameworks need separate coverage.
Do not generalize these results to every application on a platform.
## Measurement contract
- Baseline and candidate runtimes come from full commit SHAs, copied before the
run. Artifacts include source hashes, cua executable hash/version, image,
seed, complete trial plan, responses, snapshots and independent fixture states.
- Each individual trial launches a fresh target, sentinel and provider. Process
groups on Linux and jobs on Windows contain children, including timed-out calls.
The provider is terminated and reaped before the terminal oracle read. No retry
follows an uncertain mutation.
- Reset carries a unique nonce bound to the fixture instance and PID. The fixture
acknowledges after applying text, selection, logical focus, counters, combo,
scroll and menu state. Sentinel foreground and stacking are independently checked.
- Invalid preparation executes no provider operation, is excluded from the scored
denominator, and makes the run incomplete with nonzero exit status. A missing or
ambiguous control after valid setup is a scored coverage failure.
- Strict selectors require a unique matching role and label. References are fresh
before the task and after every step. There is no label-only or role-only fallback.
- Completion requires exact independent final state, including unchanged decoy text
and exactly one click/submit. Raw text is never cleaned. API errors, missing final
observations and observed interference remain separate from task completion.
- The observer starts before provider initialization and initial selector snapshot,
and stays active through all steps, reads, shutdown and settling. No sentinel
refocusing happens during measurement. Idle and deliberate-steal controls must pass.
- Complete tasks run without simulated user typing. Separate click and scroll probes
type `q` into the sentinel. Event counters preserve evidence even if agent input
selects or deletes text. Observations sample roughly every 4 ms; they cannot prove
the absence of shorter focus/pointer/clipboard changes. X11 clipboard observations
detect ownership changes, not all changes made by the same owner.
- Engine order rotates across rounds from a recorded seed. Use a multiple of three
rounds for balanced positions. Default/background and explicitly requested
foreground profiles are separate trials. Foreground can intentionally interfere.
- Comparable per-step time includes the action and an additional full no-screenshot
observation for every provider. Raw action time is diagnostic because Orca already
returns a snapshot with mutations. Task time includes provider startup, discovery
and all action/read pairs. Medians include completed tasks only; failed-attempt
durations and sample counts are separate. Diagnostic serialization and disk I/O
happen after timing. Final independent readback after shutdown is an upper
bound on effect time, recorded separately.
- Fatal errors, invalid setup, failed observers, unobserved completion, budget
exhaustion or missing planned cells leave JSON evidence and exit nonzero. A valid
observed task failure remains a benchmark result, not an infrastructure failure.
Tasks cover explicit text targeting with another field logically focused, Unicode
entry plus submit, select-all replacement plus submit, and color choice plus one
click. Interference probes remain separate from task latency comparisons.
Explicit-target typing is a native capability probe. Orca's public CLI/RPC currently
accepts typing at the focused field and does not expose an element target for text.
Failure of this probe identifies a capability to consider adding; it does not prove
a violation of the current public typing contract. Click-then-type tasks exercise
the existing sequence, although this benchmark still bypasses public transports.
## Headless contract checks
```sh
ORCA_BACKGROUND_LAUNCH=1 python3 -m unittest discover -s config/benchmarks/computer-use -p 'test_*.py'
```
These checks protect the historical false-success cases and process-tree cleanup.
They do not import desktop observers or generate native input.
Transports, fixtures and observers were adapted from the historical
`bench/computer-cua` prototype at `68a68ccf0f16c0a7f952d55adfe21850247b8ee9`.
Historical checkouts and result artifacts remain unchanged.
+378
View File
@@ -0,0 +1,378 @@
#!/usr/bin/env python3
"""Compare direct native providers on disposable, independently observed task fixtures."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import platform
import re
import subprocess
import sys
import time
import uuid
from process_scope import ProcessScope
from trial_contract import (DiscoveryError, MARKER, SELECTORS, TASKS, acknowledged, prepared,
schedule, select_unique, summarize, verdict)
HERE = Path(__file__).resolve().parent
REPO = HERE.parents[2]
WIN = sys.platform == "win32"
def read_json(path):
try:
return json.loads(Path(path).read_text(encoding="utf-8-sig"))
except (OSError, ValueError):
return None
def write_json(path, value):
temporary = Path(str(path) + ".tmp")
temporary.write_text(json.dumps(value, ensure_ascii=False, indent=2), encoding="utf-8")
temporary.replace(path)
def wait_for(predicate, timeout=10):
end = time.monotonic() + timeout
while time.monotonic() < end:
value = predicate()
if value:
return value
time.sleep(0.02)
raise TimeoutError("independent setup/observation did not become ready")
def pin_source(ref, output):
if not re.fullmatch(r"[0-9a-f]{40}", ref):
raise ValueError("source must be a full commit SHA")
hashes = {}
for relative in ("native/computer-use-linux/runtime.py", "native/computer-use-windows/runtime.ps1"):
data = subprocess.check_output(["git", "show", f"{ref}:{relative}"], cwd=REPO)
destination = output / relative
destination.parent.mkdir(parents=True, exist_ok=True)
destination.write_bytes(data)
hashes[relative] = hashlib.sha256(data).hexdigest()
return {"sha": ref, "files": hashes}
class Trial:
def __init__(self, args, directory, log):
self.args, self.directory, self.log = args, directory, log
self.instance = uuid.uuid4().hex
directory.mkdir()
self.targets = ProcessScope(str(directory))
self.engines = ProcessScope(str(directory))
self.sampler = self.typer = None
self.state_path = directory / "state.json"
self.sentinel_path = directory / "sentinel.json"
self.nonce = None
def spawn_target(self, command, **extra):
env = {**os.environ, "BENCH_INSTANCE": self.instance, "BENCH_STATE_DIR": str(self.directory)}
return self.targets.spawn(command, stdout=self.log, stderr=self.log, env=env, **extra)
def window(self, pid, title):
return wait_for(lambda: obs.find_window(pid=pid, title=title) if WIN
else obs.find_client(pid, title), 30)
def state(self):
state = read_json(self.state_path)
if state and state.get("instance") == self.instance and state.get("pid") == self.pid:
return state
return None
def launch(self):
if self.args.target == "electron":
flags = ["--force-renderer-accessibility"] + ([] if WIN else ["--no-sandbox"])
proc = self.spawn_target([self.args.electron, *flags, str(HERE / "electron/main.js")])
title = "BenchElectron"
elif WIN:
proc = self.spawn_target(["powershell.exe", "-NoLogo", "-NoProfile", "-STA", "-File",
str(HERE / "fixture_win.ps1"), "-StateDir", str(self.directory)], hidden=True)
title = "BenchTarget"
else:
proc = self.spawn_target([sys.executable, str(HERE / "fixture_gtk.py"), str(self.directory)])
title = "BenchTarget"
self.pid = proc.pid
self.target = self.window(proc.pid, title)
obs.place(self.target, 60, 60, 760, 560)
wait_for(self.state)
proc = self.spawn_target([sys.executable, str(HERE / "sentinel.py"),
str(self.sentinel_path), "880x660+20+20"], hidden=True)
self.sentinel_pid = proc.pid
self.sentinel = self.window(proc.pid, "BenchSentinel")
wait_for(lambda: read_json(self.sentinel_path))
# Tk's initial focus callback must finish before calibration or measurement.
time.sleep(0.2)
def front(self):
if WIN:
return obs.foreground() == self.sentinel and not obs.owned_above(self.sentinel, self.pid)
display = obs.main_display()
stacking = display.stacking()
windows = display.windows_of(self.pid)
return (display.active() == self.sentinel and display.focus() in display.descendants(self.sentinel)
and self.sentinel in stacking and self.target in stacking and windows
and all(stacking.index(window) < stacking.index(self.sentinel) for window in windows if window in stacking))
def prepare(self, task):
self.nonce = uuid.uuid4().hex
write_json(self.directory / "cmd", {"instance": self.instance, "nonce": self.nonce, "task": task})
wait_for(lambda: prepared(self.state() or {}, task, self.instance, self.pid, self.nonce), 5)
obs.force_foreground(self.sentinel)
wait_for(self.front, 5)
if WIN:
obs.u.SetCursorPos(2, 2)
else:
obs.xdotool("mousemove", 2, 2)
wait_for(lambda: prepared(self.state() or {}, task, self.instance, self.pid, self.nonce), 5)
def sample(self):
self.sampler = obs.Sampler(self.sentinel, self.target, self.pid)
self.sampler.start()
def stop_sampling(self):
if self.sampler:
sampler, self.sampler = self.sampler, None
result = sampler.stop()
if result["n_samples"] < 3:
raise RuntimeError("observer did not collect enough samples")
return result
return {}
def calibrate(self):
self.prepare("click_interference")
self.sample()
self.typer = obs.Typer()
self.typer.start()
time.sleep(0.65)
self.typer.stop()
sent = self.typer.sent
self.typer = None
if sent < 3:
raise RuntimeError("simulated user input control failed")
wait_for(lambda: (read_json(self.sentinel_path) or {}).get("q", 0) == sent, 2)
idle = self.stop_sampling()
if any(idle[key] for key in ("sentinel_lost_focus", "target_raised", "pointer_moved", "clipboard_changed")):
raise RuntimeError("idle observer control failed")
self.sample()
obs.force_foreground(self.target)
time.sleep(0.15)
steal = self.stop_sampling()
if not all(steal[key] for key in ("sentinel_lost_focus", "target_activated", "target_raised")):
raise RuntimeError("intentional focus-steal observer control failed")
return {"idle": idle, "steal": steal, "keys_sent": sent}
def engine(self, name, sources):
from providers import Cua, CuaEngine, OrcaEngine, OrcaLinux, OrcaWin, cua_sc
if name == "cua":
client = Cua(self.args.cua, str(self.directory / "cua-home"), self.log, self.engines)
response = client.call("list_windows", {"pid": self.pid})
if response.get("isError") or "_rpc_error" in response:
raise RuntimeError("cua list_windows failed: " + str(response))
windows = cua_sc(response).get("windows") or []
matches = [window for window in windows if window.get("window_id") is not None
and window.get("title") in ("BenchTarget", "BenchElectron")]
if len(matches) != 1:
raise DiscoveryError(f"cua list_windows matched {len(matches)} fixture windows")
return CuaEngine(client, self.pid, matches[0]["window_id"])
return OrcaEngine((OrcaWin if WIN else OrcaLinux)(str(sources / name), self.log, self.engines), self.pid)
def run(self, cell, sources):
row = {**cell, "prepared": False, "api_errors": [], "action_read_ms": [], "steps": []}
state = None
task_started = None
snapshots, responses = [], []
try:
self.launch()
self.prepare(cell["task"])
row["initial_state"] = self.state()
row["prepared"] = True
self.sample()
row["sentinel_before"] = read_json(self.sentinel_path)
if cell["task"].endswith("_interference"):
self.typer = obs.Typer()
self.typer.start()
task_started = time.perf_counter()
engine = self.engine(cell["engine"], sources)
acquire_started = time.perf_counter()
elements = engine.snapshot()
row["acquisition_ms"] = (time.perf_counter() - acquire_started) * 1000
snapshots.append(engine.last_snapshot)
for action, selector in TASKS[cell["task"]]:
element = select_unique(elements, SELECTORS[selector])
started = time.perf_counter()
result = engine.act(action, element, cell["profile"] == "foreground", MARKER)
row["steps"].append({"action": action, "result": result,
"action_ms": (time.perf_counter() - started) * 1000})
if result.get("error"):
row["api_errors"].append(result)
responses.append(engine.last_response)
# Identical extra full no-screenshot read for all providers; reuse fresh refs next step.
elements = engine.snapshot()
row["action_read_ms"].append((time.perf_counter() - started) * 1000)
snapshots.append(engine.last_snapshot)
if result.get("error"):
break
except DiscoveryError as error:
row["discovery_error"] = str(error)
except Exception as error:
row["api_errors"].append(repr(error))
finally:
if task_started is not None:
row["attempt_ms"] = (time.perf_counter() - task_started) * 1000
try:
# No late operation can mutate the fixture after the terminal oracle starts.
self.engines.close()
if self.typer:
self.typer.stop()
row["keys_sent"] = self.typer.sent
self.typer = None
if row["prepared"]:
if task_started is not None:
row["elapsed_to_quiescent_ms"] = (time.perf_counter() - task_started) * 1000
before = (self.state() or {}).get("seq", -1)
time.sleep(0.4)
def fresh_state():
value = self.state()
return value if value and value.get("seq", -1) > before and acknowledged(
value, self.instance, self.pid, self.nonce) else None
state = wait_for(fresh_state, 2)
if task_started is not None:
row["terminal_readback_ms"] = (time.perf_counter() - task_started) * 1000
row["sentinel_after"] = read_json(self.sentinel_path)
sentinel_before = row["sentinel_before"]
sentinel_after = row["sentinel_after"]
if not all(value and value.get("pid") == self.sentinel_pid
for value in (sentinel_before, sentinel_after)):
raise RuntimeError("missing independent sentinel counters")
if cell["task"].endswith("_interference") and row.get("keys_sent", 0) == 0:
row["input_observer_error"] = "no simulated user input was delivered"
row["keys_received"] = sentinel_after.get("q", 0) - sentinel_before.get("q", 0)
row["keys_lost"] = max(0, row.get("keys_sent", 0) - row["keys_received"])
row["agent_text_in_sentinel"] = sentinel_after.get("other", 0) - sentinel_before.get("other", 0)
row["target_q_characters"] = state["text"].count("q") + state["decoyText"].count("q")
except Exception as error:
row["terminal_observation_error"] = repr(error)
try:
row["observation"] = self.stop_sampling()
except Exception as error:
row["observer_error"] = repr(error)
row["final_state"] = state
row["verdict"] = verdict(row, state)
for index, snapshot in enumerate(snapshots):
write_json(self.directory / f"snapshot-{index}.json", snapshot)
for index, response in enumerate(responses):
write_json(self.directory / f"response-{index}.json", response)
return row
def close(self):
if self.typer:
self.typer.stop()
try:
self.engines.close()
finally:
try:
self.stop_sampling()
finally:
self.targets.close()
def markdown(meta, summary):
lines = [f"## {meta['os']} / {meta['target']}", "",
f"Run complete: {meta['complete']}. Cells: {meta['executed']}/{meta['planned']}. Seed: {meta['seed']}.", "",
"| task | profile | engine | completed/scored | invalid | unobserved | API errors | task ms | action + read ms | focus lost | raised |",
"|---|---|---|---|---|---|---|---|---|---|---|"]
for row in summary:
lines.append(f"| {row['task']} | {row['profile']} | {row['engine']} | {row['completed']}/{row['scored']} | "
f"{row['invalid']} | {row['unobserved']} | {row['api_errors']} | {row['task_ms']} | "
f"{row['action_read_ms']} | {row['sentinel_lost_focus']} | {row['target_raised']} |")
if meta.get("fatal"):
lines.extend(["", f"Incomplete: `{meta['fatal']}`"])
return "\n".join(lines) + "\n"
def main():
if (os.environ.get("ORCA_COMPUTER_BENCH_ISOLATED") != "1" or os.environ.get("GITHUB_ACTIONS") != "true"
or os.environ.get("ORCA_BACKGROUND_LAUNCH") != "1" or sys.platform not in ("linux", "win32")):
raise SystemExit("Native input benchmark requires the isolated Linux/Windows GitHub Actions desktop")
global obs
if WIN:
import obs_win as obs
else:
import obs_x11 as obs
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--target", choices=["fixture", "electron"], required=True)
parser.add_argument("--cua", required=True)
parser.add_argument("--electron")
parser.add_argument("--out", type=Path, required=True)
parser.add_argument("--baseline", default="030c88deecf3adc212d22f57b085e4bd5c6e48e6")
parser.add_argument("--candidate", required=True)
parser.add_argument("--n", type=int, default=3)
parser.add_argument("--seed", type=int, default=417)
parser.add_argument("--budget", type=int, default=1800)
args = parser.parse_args()
if args.n < 1 or (args.target == "electron" and not args.electron):
parser.error("positive --n and an Electron executable for Electron targets are required")
args.out = args.out.resolve()
args.out.mkdir(parents=True, exist_ok=False)
sources = args.out / "sources"
cells = []
for cell in schedule(["baseline", "candidate", "cua"], TASKS, args.n, args.seed):
profiles = ["background", "foreground"] if not cell["task"].endswith("_interference") else ["background"]
cells.extend({**cell, "profile": profile} for profile in profiles)
meta = {"os": platform.platform(), "target": args.target, "seed": args.seed, "n": args.n,
"python": sys.version, "image": os.environ.get("ImageVersion"), "planned": len(cells),
"complete": False, "executed": 0, "sources": {}}
rows = []
started = time.monotonic()
try:
meta["sources"] = {name: pin_source(sha, sources / name)
for name, sha in (("baseline", args.baseline), ("candidate", args.candidate))}
meta["cua_sha256"] = hashlib.sha256(Path(args.cua).read_bytes()).hexdigest()
meta["cua_version"] = subprocess.check_output([args.cua, "--version"], text=True, timeout=15).strip()
if "0.38.0" not in meta["cua_version"]:
raise RuntimeError("expected pinned cua-driver 0.38.0")
with (args.out / "engines.log").open("ab") as log:
control = Trial(args, args.out / "controls", log)
try:
control.launch()
meta["calibration"] = control.calibrate()
finally:
control.close()
for index, cell in enumerate(cells):
if time.monotonic() - started >= args.budget:
raise TimeoutError("budget exhausted; planned cells remain")
trial = Trial(args, args.out / f"trial-{index:03d}", log)
try:
row = trial.run(cell, sources)
rows.append(row)
finally:
trial.close()
meta["executed"] = len(rows)
write_json(args.out / "results.json", {"meta": meta, "rows": rows})
print(f"{index + 1}/{len(cells)} {cell}: {row['verdict']}", flush=True)
meta["complete"] = (len(rows) == len(cells) and all(row["prepared"] and not row.get("observer_error")
and not row.get("terminal_observation_error") and not row.get("input_observer_error") and row["verdict"] != "unobserved" for row in rows))
except Exception as error:
import traceback
traceback.print_exc()
meta["fatal"] = repr(error)
finally:
meta["executed"] = len(rows)
meta["elapsed_seconds"] = time.monotonic() - started
summary = summarize(rows)
write_json(args.out / "results.json", {"meta": meta, "summary": summary, "rows": rows, "plan": cells})
report = markdown(meta, summary)
(args.out / "summary.md").write_text(report, encoding="utf-8")
print(report)
if os.environ.get("GITHUB_STEP_SUMMARY"):
with open(os.environ["GITHUB_STEP_SUMMARY"], "a", encoding="utf-8") as stream:
stream.write(report)
return 0 if meta["complete"] else 1
if __name__ == "__main__":
sys.exit(main())
@@ -0,0 +1,81 @@
// Disposable native-provider fixture, launched only on an isolated desktop.
const { app, BrowserWindow, ipcMain } = require('electron')
const fs = require('node:fs')
const path = require('node:path')
const dir = process.env.BENCH_STATE_DIR
const instance = process.env.BENCH_INSTANCE
process.on('uncaughtException', (error) => {
console.error(error)
app.exit(1)
})
if (process.env.ORCA_COMPUTER_BENCH_ISOLATED !== '1') {
process.exit(1)
}
app.whenReady().then(() => {
const w = new BrowserWindow({
x: 60,
y: 60,
width: 760,
height: 560,
title: 'BenchElectron',
webPreferences: { nodeIntegration: true, contextIsolation: false, backgroundThrottling: false }
})
ipcMain.on('st', (_event, state) => {
const data = JSON.stringify({ ...state, instance, activeForm: w.isFocused(), pid: process.pid })
try {
fs.writeFileSync(path.join(dir, 'state.json.tmp'), data)
fs.renameSync(path.join(dir, 'state.json.tmp'), path.join(dir, 'state.json'))
} catch (error) {
// Windows readers can briefly deny replacement; the next state tick retries.
if (!['EACCES', 'EPERM', 'EBUSY'].includes(error.code)) {
throw error
}
}
})
w.loadURL(
`data:text/html,${encodeURIComponent(`<html><head><title>BenchElectron</title></head>
<body style="font:14px sans-serif;height:4000px">
<input id=field aria-label="field"><input id=decoy aria-label="decoy">
<button id=button>Increment</button>
<select id=color aria-label="Color"><option>Red</option><option>Green</option><option>Blue</option></select>
<script>
const { ipcRenderer } = require('electron');
const field = document.getElementById('field'), decoy = document.getElementById('decoy');
const color = document.getElementById('color');
let count = 0, returns = 0, ack = null, seq = 0;
document.getElementById('button').onclick = () => count++;
field.addEventListener('keydown', event => { if (event.key === 'Enter') returns++ });
function report() {
ipcRenderer.send('st', { count, returns, ack, seq: ++seq, text: field.value, decoyText: decoy.value,
top: window.scrollY, combo: color.value, selStart: field.selectionStart,
selLen: field.selectionEnd - field.selectionStart, check: false, menuHits: 0, dropOpen: false,
logicalFocus: document.activeElement === field ? 'field' : document.activeElement === decoy ? 'decoy' : 'other' });
}
ipcRenderer.on('cmd', (_event, cmd) => {
count = returns = 0;
field.value = cmd.task === 'replace_submit' ? 'hello' : '';
field.setSelectionRange(field.value.length, field.value.length);
decoy.value = 'untouched'; decoy.setSelectionRange(9, 9);
color.selectedIndex = 0; color.blur(); window.scrollTo(0, 0);
(cmd.task === 'targeted_type' ? decoy : field).focus();
ack = cmd.nonce;
report();
});
setInterval(report, 30);
</script></body></html>`)}`
)
setInterval(() => {
const cmdPath = path.join(dir, 'cmd')
if (!fs.existsSync(cmdPath)) {
return
}
const raw = fs.readFileSync(cmdPath, 'utf8').trim()
if (raw) {
const cmd = JSON.parse(raw)
fs.unlinkSync(cmdPath)
if (cmd.instance === instance) {
w.webContents.send('cmd', cmd)
}
}
}, 30)
})
@@ -0,0 +1,125 @@
#!/usr/bin/env python3
"""GTK3 target for the cua-vs-Orca background bench. Writes state.json every 30 ms; obeys cmd."""
import json
import os
import sys
if os.environ.get("ORCA_COMPUTER_BENCH_ISOLATED") != "1" or os.environ.get("GITHUB_ACTIONS") != "true":
raise SystemExit("fixture requires isolated CI desktop")
import gi
gi.require_version("Gtk", "3.0")
from gi.repository import Gdk, GLib, Gtk # noqa: E402
state_dir = sys.argv[1]
STATE = os.path.join(state_dir, "state.json")
CMD = os.path.join(state_dir, "cmd")
instance = os.environ["BENCH_INSTANCE"]
ack = None
sequence = 0
s = {"count": 0, "returns": 0, "menuHits": 0, "last": None}
def named(widget, name):
widget.get_accessible().set_name(name)
return widget
Gtk.Settings.get_default().set_property("gtk-entry-select-on-focus", False)
win = Gtk.Window(title="BenchTarget")
win.set_default_size(760, 560)
win.move(60, 60)
win.connect("destroy", Gtk.main_quit)
accel = Gtk.AccelGroup()
win.add_accel_group(accel)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=6)
win.add(box)
menubar = Gtk.MenuBar()
edit = Gtk.MenuItem(label="Edit")
sub = Gtk.Menu()
sub.set_accel_group(accel)
bump = Gtk.MenuItem(label="Bump")
bump.add_accelerator("activate", accel, ord("b"), Gdk.ModifierType.CONTROL_MASK, Gtk.AccelFlags.VISIBLE)
bump.connect("activate", lambda _w: s.__setitem__("menuHits", s["menuHits"] + 1))
sub.append(bump)
edit.set_submenu(sub)
menubar.append(edit)
box.pack_start(menubar, False, False, 0)
button = Gtk.Button(label="Increment")
button.connect("clicked", lambda _b: s.__setitem__("count", s["count"] + 1))
check = Gtk.CheckButton(label="Check me")
field = named(Gtk.Entry(), "field")
field.connect("activate", lambda _e: s.__setitem__("returns", s["returns"] + 1))
decoy = named(Gtk.Entry(), "decoy")
combo = named(Gtk.ComboBoxText(), "Color")
for item in ("Red", "Green", "Blue"):
combo.append_text(item)
combo.set_active(0)
scroll = named(Gtk.ScrolledWindow(), "list")
scroll.set_size_request(300, 250)
rows = Gtk.ListBox()
for i in range(1, 301):
rows.add(Gtk.Label(label=f"Row {i}"))
scroll.add(rows)
for widget in (button, check, field, decoy, combo):
box.pack_start(widget, False, False, 0)
box.pack_start(scroll, True, True, 0)
def command(c):
global ack
if c["instance"] != instance:
return
s["count"] = s["returns"] = s["menuHits"] = 0
check.set_active(False)
sub.popdown()
combo.set_active(0)
scroll.get_vadjustment().set_value(0)
field.set_text("hello" if c["task"] == "replace_submit" else "")
field.set_position(len(field.get_text()))
field.select_region(len(field.get_text()), len(field.get_text()))
decoy.set_text("untouched")
decoy.set_position(9)
decoy.select_region(9, 9)
win.set_focus(decoy if c["task"] == "targeted_type" else field)
ack = c["nonce"]
def tick():
global sequence
sequence += 1
try:
with open(CMD) as f:
c = f.read().strip()
if c:
open(CMD, "w").close()
command(json.loads(c))
except (OSError, ValueError):
pass
bounds = field.get_selection_bounds()
sel = list(bounds) if bounds else [field.get_position(), field.get_position()]
state = {
"instance": instance, "ack": ack, "seq": sequence, "decoyText": decoy.get_text(),
"logicalFocus": "field" if win.get_focus() is field else "decoy" if win.get_focus() is decoy else "other",
"count": s["count"], "check": check.get_active(), "text": field.get_text(), "returns": s["returns"],
"selStart": sel[0], "selLen": sel[1] - sel[0], "combo": combo.get_active_text(),
"top": scroll.get_vadjustment().get_value(), "menuHits": s["menuHits"], "dropOpen": sub.get_visible(),
"activeForm": win.is_active(), "fieldFocused": win.get_focus() is field, "pid": os.getpid(),
}
data = json.dumps(state)
if data != s["last"]:
tmp = STATE + ".tmp"
with open(tmp, "w") as f:
f.write(data)
os.replace(tmp, STATE)
s["last"] = data
return True
win.show_all()
win.set_focus(field)
GLib.timeout_add(30, tick)
Gtk.main()
@@ -0,0 +1,124 @@
# WinForms target for the cua-vs-Orca background bench. Writes its state to state.json every 30 ms
# and acknowledges each JSON command after applying it.
param([string]$StateDir)
$ErrorActionPreference = "Stop"
if ($env:ORCA_COMPUTER_BENCH_ISOLATED -ne "1" -or $env:GITHUB_ACTIONS -ne "true") { throw "fixture requires isolated CI desktop" }
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
[System.Windows.Forms.Application]::EnableVisualStyles()
$statePath = Join-Path $StateDir "state.json"
$cmdPath = Join-Path $StateDir "cmd"
$instance = $env:BENCH_INSTANCE
$script:ack = $null
$script:sequence = 0
$script:count = 0
$script:returns = 0
$script:menuHits = 0
$script:last = ""
$form = New-Object System.Windows.Forms.Form
$form.Text = "BenchTarget"
$form.StartPosition = "Manual"
$form.Location = New-Object System.Drawing.Point(60, 60)
$form.Size = New-Object System.Drawing.Size(760, 560)
$menu = New-Object System.Windows.Forms.MenuStrip
$edit = New-Object System.Windows.Forms.ToolStripMenuItem("Edit")
$bump = New-Object System.Windows.Forms.ToolStripMenuItem("Bump")
$bump.ShortcutKeys = [System.Windows.Forms.Keys]::Control -bor [System.Windows.Forms.Keys]::B
$bump.Add_Click({ $script:menuHits++ })
[void]$edit.DropDownItems.Add($bump)
[void]$menu.Items.Add($edit)
$form.MainMenuStrip = $menu
$form.Controls.Add($menu)
$button = New-Object System.Windows.Forms.Button
$button.Text = "Increment"
$button.Location = New-Object System.Drawing.Point(20, 40)
$button.Add_Click({ $script:count++ })
$form.Controls.Add($button)
$check = New-Object System.Windows.Forms.CheckBox
$check.Text = "Check me"
$check.Location = New-Object System.Drawing.Point(20, 80)
$form.Controls.Add($check)
$field = New-Object System.Windows.Forms.TextBox
$field.Name = "field"
$field.AccessibleName = "field"
$field.Location = New-Object System.Drawing.Point(20, 120)
$field.Width = 300
$field.Add_KeyDown({
param($sender, $e)
if ($e.KeyCode -eq [System.Windows.Forms.Keys]::Enter) { $script:returns++; $e.SuppressKeyPress = $true }
})
$form.Controls.Add($field)
$decoy = New-Object System.Windows.Forms.TextBox
$decoy.Name = "decoy"
$decoy.AccessibleName = "decoy"
$decoy.Location = New-Object System.Drawing.Point(350, 120)
$decoy.Width = 300
$form.Controls.Add($decoy)
$combo = New-Object System.Windows.Forms.ComboBox
$combo.DropDownStyle = "DropDownList"
$combo.AccessibleName = "Color"
[void]$combo.Items.AddRange(@("Red", "Green", "Blue"))
$combo.SelectedIndex = 0
$combo.Location = New-Object System.Drawing.Point(20, 160)
$form.Controls.Add($combo)
$list = New-Object System.Windows.Forms.ListBox
$list.AccessibleName = "list"
$list.Location = New-Object System.Drawing.Point(20, 200)
$list.Size = New-Object System.Drawing.Size(300, 250)
foreach ($i in 1..300) { [void]$list.Items.Add("Row $i") }
$form.Controls.Add($list)
function Invoke-BenchCommand($c) {
if ($c.instance -ne $instance) { return }
$script:count = 0; $script:returns = 0; $script:menuHits = 0
$check.Checked = $false
$edit.HideDropDown()
$combo.SelectedIndex = 0
$combo.DroppedDown = $false
$list.TopIndex = 0
$field.Text = if ($c.task -eq 'replace_submit') { 'hello' } else { '' }
$field.SelectionStart = $field.Text.Length
$field.SelectionLength = 0
$decoy.Text = 'untouched'
$decoy.SelectionStart = $decoy.Text.Length
$decoy.SelectionLength = 0
$form.ActiveControl = if ($c.task -eq 'targeted_type') { $decoy } else { $field }
$script:ack = $c.nonce
}
$timer = New-Object System.Windows.Forms.Timer
$timer.Interval = 30
$timer.Add_Tick({
$script:sequence++
try {
if (Test-Path $cmdPath) {
$c = ([IO.File]::ReadAllText($cmdPath)).Trim()
if ($c) { [IO.File]::WriteAllText($cmdPath, ""); Invoke-BenchCommand ($c | ConvertFrom-Json) }
}
} catch {}
$state = [ordered]@{
instance = $instance; ack = $script:ack; seq = $script:sequence
decoyText = $decoy.Text
logicalFocus = $(if ($form.ActiveControl -eq $field) { 'field' } elseif ($form.ActiveControl -eq $decoy) { 'decoy' } else { 'other' })
count = $script:count; check = $check.Checked; text = $field.Text; returns = $script:returns
selStart = $field.SelectionStart; selLen = $field.SelectionLength; combo = [string]$combo.SelectedItem
top = $list.TopIndex; menuHits = $script:menuHits; dropOpen = ($edit.DropDown.Visible -or $combo.DroppedDown)
activeForm = ($null -ne [System.Windows.Forms.Form]::ActiveForm); fieldFocused = $field.Focused
pid = $PID
}
$json = $state | ConvertTo-Json -Compress
if ($json -ne $script:last) {
try { [IO.File]::WriteAllText($statePath, $json); $script:last = $json } catch {}
}
})
$form.Add_Shown({ $form.ActiveControl = $field; $timer.Start() })
[System.Windows.Forms.Application]::Run($form)
+309
View File
@@ -0,0 +1,309 @@
"""Windows host observation (foreground, z-order, cursor, clipboard) and the user's typer, via user32."""
import ctypes
import ctypes.wintypes as W
import threading
import time
try:
ctypes.windll.shcore.SetProcessDpiAwareness(2)
except Exception:
ctypes.windll.user32.SetProcessDPIAware()
u = ctypes.WinDLL("user32", use_last_error=True)
k32 = ctypes.WinDLL("kernel32", use_last_error=True)
GW_HWNDPREV = 3
WM_GETTEXT, WM_GETTEXTLENGTH, WM_SETTEXT = 0x000D, 0x000E, 0x000C
EM_GETSEL, EM_SETSEL, EM_GETFIRSTVISIBLELINE, EM_LINESCROLL = 0x00B0, 0x00B1, 0x00CE, 0x00B6
WM_VSCROLL, SB_TOP = 0x0115, 6
SMTO_ABORTIFHUNG = 0x2
SWP_NOACTIVATE, SWP_NOZORDER = 0x0010, 0x0004
SPI_GETFOREGROUNDLOCKTIMEOUT = 0x2000
ULONG_PTR = ctypes.c_size_t
class KEYBDINPUT(ctypes.Structure):
_fields_ = [("wVk", W.WORD), ("wScan", W.WORD), ("dwFlags", W.DWORD), ("time", W.DWORD), ("dwExtraInfo", ULONG_PTR)]
class MOUSEINPUT(ctypes.Structure):
_fields_ = [("dx", W.LONG), ("dy", W.LONG), ("mouseData", W.DWORD), ("dwFlags", W.DWORD), ("time", W.DWORD), ("dwExtraInfo", ULONG_PTR)]
class _U(ctypes.Union):
_fields_ = [("ki", KEYBDINPUT), ("mi", MOUSEINPUT)]
class INPUT(ctypes.Structure):
_fields_ = [("type", W.DWORD), ("u", _U)]
WNDENUMPROC = ctypes.WINFUNCTYPE(W.BOOL, W.HWND, W.LPARAM)
u.EnumWindows.argtypes = [WNDENUMPROC, W.LPARAM]
u.GetWindowThreadProcessId.argtypes = [W.HWND, ctypes.POINTER(W.DWORD)]
u.GetWindowThreadProcessId.restype = W.DWORD
u.GetForegroundWindow.restype = W.HWND
u.GetWindow.restype = W.HWND
u.GetWindow.argtypes = [W.HWND, W.UINT]
u.FindWindowExW.restype = W.HWND
u.FindWindowExW.argtypes = [W.HWND, W.HWND, W.LPCWSTR, W.LPCWSTR]
u.SendMessageTimeoutW.argtypes = [W.HWND, W.UINT, W.WPARAM, W.LPARAM, W.UINT, W.UINT, ctypes.POINTER(ULONG_PTR)]
u.SendMessageTimeoutW.restype = W.LPARAM
u.SetWindowPos.argtypes = [W.HWND, W.HWND, ctypes.c_int, ctypes.c_int, ctypes.c_int, ctypes.c_int, W.UINT]
u.SendInput.argtypes = [W.UINT, ctypes.POINTER(INPUT), ctypes.c_int]
u.AttachThreadInput.argtypes = [W.DWORD, W.DWORD, W.BOOL]
u.SetForegroundWindow.argtypes = [W.HWND]
u.BringWindowToTop.argtypes = [W.HWND]
u.IsWindowVisible.argtypes = [W.HWND]
u.GetClassNameW.argtypes = [W.HWND, W.LPWSTR, ctypes.c_int]
u.GetWindowTextW.argtypes = [W.HWND, W.LPWSTR, ctypes.c_int]
u.GetWindowRect.argtypes = [W.HWND, ctypes.POINTER(W.RECT)]
def hval(h):
return int(h or 0)
def pid_of(hwnd):
p = W.DWORD(0)
u.GetWindowThreadProcessId(hwnd, ctypes.byref(p))
return p.value
def title_of(hwnd):
b = ctypes.create_unicode_buffer(512)
u.GetWindowTextW(hwnd, b, 512)
return b.value
def class_of(hwnd):
b = ctypes.create_unicode_buffer(256)
u.GetClassNameW(hwnd, b, 256)
return b.value
def top_windows():
out = []
def cb(h, _l):
out.append(hval(h))
return True
u.EnumWindows(WNDENUMPROC(cb), 0)
return out
def find_window(pid=None, title=None, cls=None, visible=True):
for h in top_windows():
if visible and not u.IsWindowVisible(h):
continue
if pid is not None and pid_of(h) != pid:
continue
if title is not None and title not in title_of(h):
continue
if cls is not None and class_of(h) != cls:
continue
return h
return 0
def describe(h):
r = W.RECT()
u.GetWindowRect(h, ctypes.byref(r))
return {"hwnd": h, "pid": pid_of(h), "title": title_of(h), "class": class_of(h), "rect": [r.left, r.top, r.right, r.bottom]}
def foreground():
return hval(u.GetForegroundWindow())
def cursor():
p = W.POINT()
u.GetCursorPos(ctypes.byref(p))
return (p.x, p.y)
def clip_seq():
return u.GetClipboardSequenceNumber()
def lock_timeout():
v = W.DWORD(0)
u.SystemParametersInfoW(SPI_GETFOREGROUNDLOCKTIMEOUT, 0, ctypes.byref(v), 0)
return v.value
def session_id():
s = W.DWORD(0)
k32.ProcessIdToSessionId(k32.GetCurrentProcessId(), ctypes.byref(s))
return s.value
def owned_above(sentinel, target_pid, limit=2000):
"""True when a visible top-level window of target_pid sits above the sentinel in z-order."""
h = u.GetWindow(sentinel, GW_HWNDPREV)
n = 0
while h and n < limit:
if u.IsWindowVisible(h) and pid_of(h) == target_pid:
return True
h = u.GetWindow(h, GW_HWNDPREV)
n += 1
return False
def place(hwnd, x, y, w, h):
u.SetWindowPos(hwnd, 0, x, y, w, h, SWP_NOACTIVATE | SWP_NOZORDER)
def force_foreground(hwnd):
"""Bench setup only: re-front the sentinel between trials (AttachThreadInput, no keystrokes)."""
if foreground() == hwnd:
return True
cur = k32.GetCurrentThreadId()
fg = foreground()
threads = {u.GetWindowThreadProcessId(fg, None) if fg else 0, u.GetWindowThreadProcessId(hwnd, None)} - {0, cur}
attached = [t for t in threads if u.AttachThreadInput(cur, t, True)]
u.BringWindowToTop(hwnd)
u.SetForegroundWindow(hwnd)
for t in attached:
u.AttachThreadInput(cur, t, False)
for _ in range(25):
if foreground() == hwnd:
return True
time.sleep(0.02)
return foreground() == hwnd
def send(hwnd, msg, wp=0, lp=0, timeout=1000):
res = ULONG_PTR(0)
ok = u.SendMessageTimeoutW(hwnd, msg, wp, lp, SMTO_ABORTIFHUNG, timeout, ctypes.byref(res))
return res.value if ok else None
def edit_child(top):
"""Notepad's editor: classic Edit, or RichEditD2DPT in the WinUI Notepad."""
for cls in ("Edit", "RichEditD2DPT"):
h = hval(u.FindWindowExW(top, None, cls, None))
if h:
return h
stack = [top]
while stack:
p = stack.pop()
c = hval(u.FindWindowExW(p, None, None, None))
while c:
if class_of(c) in ("Edit", "RichEditD2DPT"):
return c
stack.append(c)
c = hval(u.FindWindowExW(p, c, None, None))
return 0
def edit_text(h):
n = send(h, WM_GETTEXTLENGTH) or 0
buf = ctypes.create_unicode_buffer(n + 2)
send(h, WM_GETTEXT, n + 1, ctypes.addressof(buf))
return buf.value
def edit_set(h, text, caret=None):
buf = ctypes.create_unicode_buffer(text)
send(h, WM_SETTEXT, 0, ctypes.addressof(buf))
c = len(text) if caret is None else caret
send(h, EM_SETSEL, c, c)
send(h, WM_VSCROLL, SB_TOP, 0)
def edit_sel(h):
v = send(h, EM_GETSEL) or 0
return (v & 0xFFFF, (v >> 16) & 0xFFFF)
def edit_first_line(h):
return send(h, EM_GETFIRSTVISIBLELINE) or 0
class Typer:
"""Simulated user: one 'q' every 200 ms through SendInput (goes to whatever has keyboard focus)."""
def __init__(self, period=0.2):
self.period = period
self.sent = 0
self.run = False
self.vk = 0x51
self.scan = u.MapVirtualKeyW(self.vk, 0)
def _key(self, up):
i = INPUT(type=1)
i.u.ki = KEYBDINPUT(self.vk, self.scan, 0x0002 if up else 0, 0, 0)
return i
def _loop(self):
while self.run:
arr = (INPUT * 2)(self._key(False), self._key(True))
if u.SendInput(2, arr, ctypes.sizeof(INPUT)) == 2:
self.sent += 1
time.sleep(self.period)
def start(self):
self.run = True
self.thread = threading.Thread(target=self._loop, daemon=True)
self.thread.start()
def stop(self):
self.run = False
self.thread.join()
time.sleep(0.1)
class Sampler:
"""Samples every ~4 ms: foreground window, target-above-sentinel, cursor, clipboard sequence."""
def __init__(self, sentinel, target, target_pid, own_pids=()):
self.s, self.t, self.tpid = sentinel, target, target_pid
self.own = set(own_pids)
def _sample(self):
fg = foreground()
return (time.perf_counter(), fg, pid_of(fg) if fg else 0, owned_above(self.s, self.tpid), cursor(), clip_seq())
def start(self):
self.base = self._sample()
self.samples = [self.base]
self.error = None
self.run = True
self.th = threading.Thread(target=self._loop, daemon=True)
self.th.start()
def _loop(self):
try:
while self.run:
self.samples.append(self._sample())
time.sleep(0.004)
except Exception as error:
self.error = error
def stop(self):
self.run = False
self.th.join()
if self.error:
raise RuntimeError("observer sampling failed") from self.error
self.samples.append(self._sample())
b = self.base
ss = self.samples
lost = [x for x in ss if x[1] != self.s]
return {
"sentinel_lost_focus": bool(lost),
"lost_ms": round(_dur(ss, lambda x: x[1] != self.s) * 1000, 1),
"target_activated": any(x[2] == self.tpid for x in ss),
"target_raised": any(x[3] for x in ss),
"pointer_moved": any(abs(x[4][0] - b[4][0]) > 1 or abs(x[4][1] - b[4][1]) > 1 for x in ss),
"pointer_end": list(ss[-1][4]), "pointer_start": list(b[4]),
"clipboard_changed": any(x[5] != b[5] for x in ss),
"fg_pids": sorted({x[2] for x in ss}),
"n_samples": len(ss),
}
def _dur(ss, pred):
return sum(b[0] - a[0] for a, b in zip(ss, ss[1:]) if pred(a))
+263
View File
@@ -0,0 +1,263 @@
"""X11 host observation (active window, input focus, stacking, pointer, selections) and the user's typer."""
import ctypes
import ctypes.util
import subprocess
import threading
import time
X = ctypes.cdll.LoadLibrary(ctypes.util.find_library("X11") or "libX11.so.6")
XT = ctypes.cdll.LoadLibrary(ctypes.util.find_library("Xtst") or "libXtst.so.6")
Window = ctypes.c_ulong
Atom = ctypes.c_ulong
P = ctypes.POINTER
vp = ctypes.c_void_p
X.XOpenDisplay.restype = vp
X.XOpenDisplay.argtypes = [ctypes.c_char_p]
X.XInitThreads.restype = ctypes.c_int
X.XDefaultRootWindow.restype = Window
X.XDefaultRootWindow.argtypes = [vp]
X.XInternAtom.restype = Atom
X.XInternAtom.argtypes = [vp, ctypes.c_char_p, ctypes.c_int]
X.XGetWindowProperty.argtypes = [vp, Window, Atom, ctypes.c_long, ctypes.c_long, ctypes.c_int, Atom,
P(Atom), P(ctypes.c_int), P(ctypes.c_ulong), P(ctypes.c_ulong), P(vp)]
X.XFree.argtypes = [vp]
X.XGetInputFocus.argtypes = [vp, P(Window), P(ctypes.c_int)]
X.XQueryPointer.argtypes = [vp, Window, P(Window), P(Window), P(ctypes.c_int), P(ctypes.c_int),
P(ctypes.c_int), P(ctypes.c_int), P(ctypes.c_uint)]
X.XGetSelectionOwner.restype = Window
X.XGetSelectionOwner.argtypes = [vp, Atom]
X.XQueryTree.argtypes = [vp, Window, P(Window), P(Window), P(P(Window)), P(ctypes.c_uint)]
X.XStringToKeysym.restype = ctypes.c_ulong
X.XStringToKeysym.argtypes = [ctypes.c_char_p]
X.XKeysymToKeycode.restype = ctypes.c_ubyte
X.XKeysymToKeycode.argtypes = [vp, ctypes.c_ulong]
X.XFlush.argtypes = [vp]
X.XCloseDisplay.argtypes = [vp]
XT.XTestFakeKeyEvent.argtypes = [vp, ctypes.c_uint, ctypes.c_int, ctypes.c_ulong]
X.XInitThreads()
ERRH = ctypes.CFUNCTYPE(ctypes.c_int, vp, vp)(lambda _d, _e: 0)
X.XSetErrorHandler(ERRH)
class Display:
def __init__(self):
self.d = X.XOpenDisplay(None)
if not self.d:
raise RuntimeError("cannot open X display")
self.root = X.XDefaultRootWindow(self.d)
self.atoms = {}
def atom(self, name):
if name not in self.atoms:
self.atoms[name] = X.XInternAtom(self.d, name.encode(), 0)
return self.atoms[name]
def prop_longs(self, win, name):
at, fmt, n, after, data = Atom(), ctypes.c_int(), ctypes.c_ulong(), ctypes.c_ulong(), vp()
rc = X.XGetWindowProperty(self.d, win, self.atom(name), 0, 4096, 0, 0, ctypes.byref(at), ctypes.byref(fmt),
ctypes.byref(n), ctypes.byref(after), ctypes.byref(data))
if rc != 0 or not data.value:
return []
try:
if fmt.value != 32:
return []
arr = ctypes.cast(data, P(ctypes.c_ulong))
return [arr[i] for i in range(n.value)]
finally:
X.XFree(data)
def active(self):
v = self.prop_longs(self.root, "_NET_ACTIVE_WINDOW")
return v[0] if v else 0
def stacking(self):
return self.prop_longs(self.root, "_NET_CLIENT_LIST_STACKING")
def clients(self):
return self.prop_longs(self.root, "_NET_CLIENT_LIST")
def pid_of(self, w):
v = self.prop_longs(w, "_NET_WM_PID")
return v[0] if v else 0
def focus(self):
w, r = Window(), ctypes.c_int()
X.XGetInputFocus(self.d, ctypes.byref(w), ctypes.byref(r))
return w.value
def pointer(self):
r, c = Window(), Window()
rx, ry, wx, wy = ctypes.c_int(), ctypes.c_int(), ctypes.c_int(), ctypes.c_int()
m = ctypes.c_uint()
X.XQueryPointer(self.d, self.root, ctypes.byref(r), ctypes.byref(c), ctypes.byref(rx), ctypes.byref(ry),
ctypes.byref(wx), ctypes.byref(wy), ctypes.byref(m))
return (rx.value, ry.value)
def owner(self, sel):
return X.XGetSelectionOwner(self.d, self.atom(sel))
def descendants(self, w):
out, stack = {w}, [w]
while stack:
p = stack.pop()
r, par, kids, n = Window(), Window(), P(Window)(), ctypes.c_uint()
if not X.XQueryTree(self.d, p, ctypes.byref(r), ctypes.byref(par), ctypes.byref(kids), ctypes.byref(n)):
continue
for i in range(n.value):
out.add(kids[i])
stack.append(kids[i])
if kids:
X.XFree(kids)
return out
def windows_of(self, pid):
return [w for w in self.clients() if self.pid_of(w) == pid]
def close(self):
if self.d:
X.XCloseDisplay(self.d)
self.d = None
_MAIN = []
def main_display():
"""One connection for the bench's main thread; Xvfb allows only ~256 clients."""
if not _MAIN:
_MAIN.append(Display())
return _MAIN[0]
def find_client(pid, title=None):
"""Managed client window of pid (by _NET_WM_PID), else by exact title (Tk may omit _NET_WM_PID)."""
d = main_display()
ws = d.windows_of(pid)
if ws:
return ws[0]
if title:
clients = set(d.clients())
out = xdotool("search", "--name", f"^{title}$").stdout.split()
for w in out:
if int(w) in clients:
return int(w)
return 0
def xdotool(*args):
return subprocess.run(["xdotool", *map(str, args)], capture_output=True, text=True, timeout=10)
def window_name(w):
return xdotool("getwindowname", w).stdout.strip()
def place(w, x, y, width, height):
xdotool("windowsize", w, width, height)
xdotool("windowmove", w, x, y)
def force_foreground(w):
"""Bench setup only: re-front the sentinel between trials."""
d = main_display()
if d.active() == w:
return True
xdotool("windowactivate", "--sync", w)
xdotool("windowraise", w)
for _ in range(25):
if d.active() == w:
return True
time.sleep(0.02)
return d.active() == w
class Typer:
"""Simulated user: one 'q' every 200 ms through XTest (goes to the X input focus)."""
def __init__(self, period=0.2):
self.period = period
self.sent = 0
self.run = False
def _loop(self):
d = Display()
kc = X.XKeysymToKeycode(d.d, X.XStringToKeysym(b"q"))
while self.run:
XT.XTestFakeKeyEvent(d.d, kc, 1, 0)
XT.XTestFakeKeyEvent(d.d, kc, 0, 0)
X.XFlush(d.d)
self.sent += 1
time.sleep(self.period)
d.close()
def start(self):
self.run = True
self.thread = threading.Thread(target=self._loop, daemon=True)
self.thread.start()
def stop(self):
self.run = False
self.thread.join()
time.sleep(0.1)
class Sampler:
"""Samples every ~4 ms: active window, input focus, stacking, pointer, CLIPBOARD/PRIMARY owners."""
def __init__(self, sentinel, target, target_pid, own_pids=()):
self.s, self.t, self.tpid = sentinel, target, target_pid
self.s_set = main_display().descendants(sentinel)
def _sample(self, d):
st = d.stacking()
act = d.active()
foc = d.focus()
tw = set(d.windows_of(self.tpid)) or {self.t}
raised = False
if self.s in st:
si = st.index(self.s)
raised = any(st.index(w) > si for w in tw if w in st)
return (time.perf_counter(), act, foc, raised, d.pointer(), d.owner("CLIPBOARD"), d.owner("PRIMARY"), act in tw)
def start(self):
self.d = main_display()
self.base = self._sample(self.d)
self.samples = [self.base]
self.error = None
self.run = True
self.th = threading.Thread(target=self._loop, daemon=True)
self.th.start()
def _loop(self):
d = Display()
try:
while self.run:
self.samples.append(self._sample(d))
time.sleep(0.004)
except Exception as error:
self.error = error
finally:
d.close()
def stop(self):
self.run = False
self.th.join()
if self.error:
raise RuntimeError("observer sampling failed") from self.error
self.samples.append(self._sample(self.d))
b, ss = self.base, self.samples
lost = lambda x: x[1] != self.s or (x[2] not in self.s_set)
return {
"sentinel_lost_focus": any(lost(x) for x in ss),
"lost_ms": round(sum(q[0] - p[0] for p, q in zip(ss, ss[1:]) if lost(p)) * 1000, 1),
"target_activated": any(x[7] for x in ss),
"target_raised": any(x[3] for x in ss),
"pointer_moved": any(abs(x[4][0] - b[4][0]) > 1 or abs(x[4][1] - b[4][1]) > 1 for x in ss),
"pointer_end": list(ss[-1][4]), "pointer_start": list(b[4]),
"clipboard_changed": any(x[5] != b[5] for x in ss),
"primary_changed": any(x[6] != b[6] for x in ss),
"focus_windows": sorted({hex(x[2]) for x in ss})[:6],
"n_samples": len(ss),
}
@@ -0,0 +1,103 @@
"""Contain each trial's process tree, including operations that time out."""
import os
import signal
import subprocess
import sys
import tempfile
WIN = sys.platform == "win32"
if WIN:
import ctypes
from ctypes import wintypes as W
class BasicLimit(ctypes.Structure):
_fields_ = [("processTime", ctypes.c_longlong), ("jobTime", ctypes.c_longlong),
("flags", W.DWORD), ("minWorkingSet", ctypes.c_size_t),
("maxWorkingSet", ctypes.c_size_t), ("activeProcesses", W.DWORD),
("affinity", ctypes.c_size_t), ("priority", W.DWORD), ("scheduling", W.DWORD)]
class IoCounters(ctypes.Structure):
_fields_ = [(name, ctypes.c_ulonglong) for name in
("readOps", "writeOps", "otherOps", "readBytes", "writeBytes", "otherBytes")]
class ExtendedLimit(ctypes.Structure):
_fields_ = [("basic", BasicLimit), ("io", IoCounters), ("processMemory", ctypes.c_size_t),
("jobMemory", ctypes.c_size_t), ("peakProcess", ctypes.c_size_t), ("peakJob", ctypes.c_size_t)]
kernel = ctypes.WinDLL("kernel32", use_last_error=True)
kernel.CreateJobObjectW.argtypes = [ctypes.c_void_p, W.LPCWSTR]
kernel.CreateJobObjectW.restype = W.HANDLE
kernel.SetInformationJobObject.argtypes = [W.HANDLE, ctypes.c_int, ctypes.c_void_p, W.DWORD]
kernel.AssignProcessToJobObject.argtypes = [W.HANDLE, W.HANDLE]
kernel.TerminateJobObject.argtypes = [W.HANDLE, W.UINT]
kernel.CloseHandle.argtypes = [W.HANDLE]
kernel.QueryInformationJobObject.argtypes = [W.HANDLE, ctypes.c_int, ctypes.c_void_p, W.DWORD, ctypes.c_void_p]
nt = ctypes.WinDLL("ntdll")
nt.NtResumeProcess.argtypes = [W.HANDLE]
nt.NtResumeProcess.restype = ctypes.c_long
class Accounting(ctypes.Structure):
_fields_ = [("user", ctypes.c_longlong), ("kernel", ctypes.c_longlong),
("periodUser", ctypes.c_longlong), ("periodKernel", ctypes.c_longlong),
("faults", W.DWORD), ("total", W.DWORD), ("active", W.DWORD), ("terminated", W.DWORD)]
class ProcessScope:
def __init__(self, directory=None):
self.directory = directory or tempfile.mkdtemp(prefix="orca-computer-trial-")
self.procs = []
self.job = None
if WIN:
self.job = kernel.CreateJobObjectW(None, None)
limits = ExtendedLimit()
limits.basic.flags = 0x2000 # Closing the job kills descendants too.
if not self.job or not kernel.SetInformationJobObject(self.job, 9, ctypes.byref(limits), ctypes.sizeof(limits)):
raise ctypes.WinError(ctypes.get_last_error())
def spawn(self, args, hidden=False, **kwargs):
if WIN:
kwargs["creationflags"] = 0x4 | (0x08000000 if hidden else 0)
else:
kwargs["start_new_session"] = True
proc = subprocess.Popen(args, **kwargs)
self.procs.append(proc)
if WIN:
# Suspend until assigned so children cannot escape the trial job.
if not kernel.AssignProcessToJobObject(self.job, int(proc._handle)):
proc.kill()
proc.wait()
raise ctypes.WinError(ctypes.get_last_error())
if nt.NtResumeProcess(int(proc._handle)) != 0:
raise RuntimeError("failed to resume contained process")
return proc
def close(self):
import time
if WIN and self.job:
if not kernel.TerminateJobObject(self.job, 1):
raise ctypes.WinError(ctypes.get_last_error())
end = time.monotonic() + 10
while True:
info = Accounting()
if not kernel.QueryInformationJobObject(self.job, 1, ctypes.byref(info), ctypes.sizeof(info), None):
raise ctypes.WinError(ctypes.get_last_error())
if info.active == 0:
break
if time.monotonic() >= end:
raise TimeoutError("trial job did not become quiescent")
time.sleep(0.02)
kernel.CloseHandle(self.job)
self.job = None
elif not WIN:
for proc in self.procs:
try:
os.killpg(proc.pid, signal.SIGKILL)
except ProcessLookupError:
pass
for proc in self.procs:
proc.wait(timeout=10)
for stream in (proc.stdin, proc.stdout):
if stream:
stream.close()
self.procs.clear()
+275
View File
@@ -0,0 +1,275 @@
"""Direct native-provider transports, adapted from bench/computer-cua at 68a68ccf0f16."""
import json
import os
import queue
import re
import sys
import subprocess
import threading
import time
WIN = sys.platform == "win32"
def now():
return time.perf_counter() * 1000
def md_rows(text, snapshot_id):
out = []
for line in (text or "").splitlines():
match = re.match(r'\s*-\s*\[(\d+)\]\s*(.*)', line)
if match:
body = match.group(2)
role = re.split(r'["\[(]', body, maxsplit=1)[0].strip()
out.append({"role": role, "label": body[len(role):],
"element_token": f"{snapshot_id}:{match.group(1)}"})
return out
class Cua:
"""cua-driver over MCP stdio, `mcp --direct` (the process owns the runtime on Windows/Linux)."""
def __init__(self, exe, home, logf, processes):
env = dict(os.environ)
env.update({
"CUA_DRIVER_RS_TELEMETRY_ENABLED": "0", "CUA_TELEMETRY_ENABLED": "0", "DO_NOT_TRACK": "1",
"CUA_DRIVER_RS_UPDATE_CHECK": "0", "CUA_DRIVER_RS_HOME": home, "CUA_HOME": home,
"CUA_DRIVER_TELEMETRY_HOME": home,
# Disposable CI desktop, as in cua's own live-smoke workflow: no interactive approvals.
"CUA_DRIVER_PERMISSION_MODE": "unrestricted", "CUA_DRIVER_DANGEROUSLY_BYPASS_APPROVALS": "1",
})
os.makedirs(home, exist_ok=True)
t0 = now()
self.p = processes.spawn([exe, "mcp", "--direct", "--no-overlay"], stdin=subprocess.PIPE,
stdout=subprocess.PIPE, stderr=logf, env=env)
self.pid = self.p.pid
self.q = {}
self.cv = threading.Condition()
self.nid = 0
threading.Thread(target=self._reader, daemon=True).start()
init = self.rpc("initialize", {"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": {"name": "orca-bench", "version": "0"}}, timeout=60)
self._send({"jsonrpc": "2.0", "method": "notifications/initialized"})
self.startup_ms = round(now() - t0, 1)
self.server_info = (init or {}).get("serverInfo")
def _reader(self):
for line in self.p.stdout:
line = line.strip()
if not line:
continue
try:
m = json.loads(line.decode("utf-8", "replace"))
except ValueError:
continue
if "id" in m:
with self.cv:
self.q[m["id"]] = (m, len(line))
self.cv.notify_all()
def _send(self, obj):
self.p.stdin.write((json.dumps(obj) + "\n").encode())
self.p.stdin.flush()
def rpc(self, method, params, timeout=30):
with self.cv:
self.nid += 1
i = self.nid
self._send({"jsonrpc": "2.0", "id": i, "method": method, "params": params})
end = time.time() + timeout
with self.cv:
while i not in self.q:
left = end - time.time()
if left <= 0:
raise TimeoutError(method)
self.cv.wait(left)
m, n = self.q.pop(i)
self.last_raw = n
if "error" in m:
return {"_rpc_error": m["error"]}
return m["result"]
def call(self, tool, args, timeout=30):
t0 = now()
r = self.rpc("tools/call", {"name": tool, "arguments": args}, timeout)
r["_ms"] = now() - t0
r["_raw"] = self.last_raw
return r
def cua_text(r):
return "\n".join(c.get("text", "") for c in r.get("content") or [] if c.get("type") == "text")
def cua_sc(r):
return r.get("structuredContent") or {}
class OrcaWin:
"""runtime.ps1 -Serve over NDJSON, as DesktopScriptRuntimeHost runs it."""
def __init__(self, root, logf, processes):
script = os.path.join(root, "native", "computer-use-windows", "runtime.ps1")
t0 = now()
self.p = processes.spawn(["powershell.exe", "-NoLogo", "-NoProfile", "-NonInteractive",
"-File", script, "-Serve"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=logf,
hidden=True)
self.pid = self.p.pid
self.lines = queue.Queue()
threading.Thread(target=self._reader, daemon=True).start()
ready = self.lines.get(timeout=120)
if '"ready"' not in ready:
raise RuntimeError("runtime.ps1 did not announce readiness: " + ready[:200])
self.startup_ms = round(now() - t0, 1)
self.rid = 0
def _reader(self):
for line in self.p.stdout:
self.lines.put(line.decode("utf-8", "replace").strip())
def op(self, req, timeout=30):
self.rid += 1
req = {**req, "requestId": self.rid}
t0 = now()
self.p.stdin.write((json.dumps(req) + "\n").encode("utf-8"))
self.p.stdin.flush()
end = time.time() + timeout
while True:
line = self.lines.get(timeout=max(0.01, end - time.time()))
try:
m = json.loads(line)
except ValueError:
continue
if m.get("requestId") == self.rid:
return m, now() - t0, len(line)
class OrcaLinux:
"""One subprocess per operation, matching the Linux production transport."""
def __init__(self, root, logf, processes):
self.script = os.path.join(root, "native", "computer-use-linux", "runtime.py")
self.logf, self.processes = logf, processes
self.path = os.path.join(processes.directory, "operation.json")
def op(self, req, timeout=30):
with open(self.path, "w", encoding="utf-8") as stream:
json.dump(req, stream)
started = now()
proc = self.processes.spawn([sys.executable, self.script, self.path],
stdout=-1, stderr=self.logf)
raw, _ = proc.communicate(timeout=timeout)
text = raw.decode("utf-8", "replace")
try:
result = json.loads(text)
except ValueError:
raise RuntimeError("invalid native response: " + text[:300])
return result, now() - started, len(raw)
class CuaEngine:
name = "cua"
def __init__(self, c, pid, wid):
self.c, self.pid, self.wid = c, pid, wid
self.elements = []
def snapshot(self):
r = self.c.call("get_window_state", {"pid": self.pid, "window_id": self.wid, "include_screenshot": False, "full_output": True})
s = cua_sc(r)
if r.get("isError") or "_rpc_error" in r:
raise RuntimeError(cua_text(r) or str(r))
self.elements = s.get("elements") or md_rows(s.get("tree_markdown") or cua_text(r), s.get("snapshot_id"))
self.last_snapshot = r
return self.elements
def act(self, action, element, escalate, text=""):
base = {"pid": self.pid, "window_id": self.wid}
tok = element.get("element_token")
if not tok:
raise RuntimeError("selected cua element has no token")
if action == "click":
tool, args = "click", {**base, "element_token": tok}
elif action == "type":
tool, args = "type_text", {**base, "element_token": tok, "text": text}
elif action == "return":
tool, args = "press_key", {**base, "element_token": tok, "key": "return"}
elif action == "ctrl_a":
tool, args = "hotkey", {**base, "element_token": tok, "keys": ["ctrl", "a"]}
elif action == "scroll":
tool, args = "scroll", {**base, "element_token": tok, "direction": "down", "amount": 3}
elif action == "choose":
tool, args = "set_value", {**base, "element_token": tok, "value": "Blue"}
if escalate and tool in ("click", "type_text", "press_key", "hotkey", "scroll"):
args["delivery_mode"] = "foreground"
if "element_token" in args and not args["element_token"]:
return {"ms": 0, "reported": "no_element", "detail": "selector found no element", "error": True}
r = self.c.call(tool, args)
self.last_response = r
if "_rpc_error" in r:
return {"ms": r["_ms"], "reported": "rpc_error", "detail": json.dumps(r["_rpc_error"])[:300], "error": True}
s = cua_sc(r)
err = bool(r.get("isError"))
code = s.get("code") or (s["error"].get("code") if isinstance(s.get("error"), dict) else None)
effect = s.get("effect")
reported = effect or (("error:" + str(code)) if err else "ok(no effect field)")
if err and effect:
reported = f"{effect}(isError)"
return {"ms": r["_ms"], "reported": reported, "error": err, "route": s.get("route"),
"delivery": (s.get("delivery") or {}).get("mode") if isinstance(s.get("delivery"), dict) else s.get("delivery"),
"code": code, "detail": (s.get("summary") or cua_text(r))[:300]}
class OrcaEngine:
name = "orca"
def __init__(self, o, pid):
self.o, self.app = o, f"pid:{pid}"
self.target = {}
self.elements = []
def snapshot(self):
r, ms, n = self.o.op({"tool": "get_app_state", "app": self.app, "noScreenshot": True})
if not r.get("ok"):
raise RuntimeError("orca get_app_state: " + str(r.get("error"))[:300])
snap = r["snapshot"]
self.elements = snap.get("elements") or []
# Mirror desktopActionWindowTarget: Windows targets by id, Linux by index.
self.target = {"windowId": snap.get("windowId")} if snap.get("windowId") is not None else {"windowIndex": snap.get("windowIndex")}
self.last_snapshot = snap
return self.elements
def _req(self, tool, element=None, escalate=False, **extra):
req = {"tool": tool, "app": self.app, "element": element, "windowBounds": None, **self.target,
"noScreenshot": True, "restoreWindow": bool(escalate)}
req.update(extra)
return req
def act(self, action, el, escalate, text=""):
if action == "click":
req = self._req("click", el, escalate)
elif action == "type":
req = self._req("type_text", el, escalate, text=text)
elif action == "return":
req = self._req("press_key", el, escalate, key="return")
elif action == "ctrl_a":
req = self._req("hotkey", el, escalate, key="ctrl+a")
elif action == "scroll":
req = self._req("scroll", el, escalate, direction="down", pages=1)
elif action == "choose":
req = self._req("set_value", el, escalate, value="Blue")
r, ms, _ = self.o.op(req)
self.last_response = r
return self._result(r, ms)
def _result(self, r, ms):
if not r.get("ok"):
return {"ms": ms, "reported": "error", "error": True, "detail": str(r.get("error"))[:300]}
a = r.get("action") or {}
v = (a.get("verification") or {}).get("state")
return {"ms": ms, "reported": v or "ok(no verification)", "error": False, "route": a.get("path"), "detail": json.dumps(a)[:300]}
@@ -0,0 +1,57 @@
"""The "user": a frontmost Tk window that receives one 'q' every 200 ms from the bench typer.
Writes cumulative counts to <out>: q = 'q' keystrokes that reached it, other = any other
characters (agent input that leaked into the user's window).
"""
import json
import os
import sys
if os.environ.get("ORCA_COMPUTER_BENCH_ISOLATED") != "1" or os.environ.get("GITHUB_ACTIONS") != "true":
raise SystemExit("fixture requires isolated CI desktop")
import tkinter as tk
out, geometry = sys.argv[1], sys.argv[2]
root = tk.Tk()
root.title("BenchSentinel")
root.geometry(geometry)
text = tk.Text(root, font=("Courier", 10))
text.pack(fill="both", expand=True)
base = {"q": 0, "other": 0}
ptr = {"clicks": 0, "wheels": 0}
root.bind_all("<ButtonPress>", lambda e: ptr.__setitem__("wheels" if e.num in (4, 5) else "clicks", ptr["wheels" if e.num in (4, 5) else "clicks"] + 1))
root.bind_all("<MouseWheel>", lambda e: ptr.__setitem__("wheels", ptr["wheels"] + 1))
last = [None]
def key(event):
if event.char == 'q':
base['q'] += 1
elif event.char:
base['other'] += 1
root.bind_all('<KeyPress>', key)
def tick():
try:
focus = str(root.focus_get())
except Exception:
focus = "?"
data = json.dumps({"q": base["q"], "other": base["other"], "focus": focus, "pid": os.getpid(), **ptr})
if data != last[0]:
tmp = out + ".tmp"
try:
with open(tmp, "w") as f:
f.write(data)
os.replace(tmp, out)
last[0] = data
except OSError:
pass
root.after(20, tick)
root.after(100, lambda: (root.lift(), root.focus_force(), text.focus_set()))
root.after(20, tick)
root.mainloop()
@@ -0,0 +1,111 @@
"""Regressions for false success claims and unsafe trial reuse in the original benchmark."""
from pathlib import Path
import sys
import tempfile
import time
import unittest
from process_scope import ProcessScope
from trial_contract import (DECOY, DiscoveryError, MARKER, TASKS, acknowledged, completed,
initial_state, prepared, schedule, select_unique, summarize, verdict)
class TaskContractTests(unittest.TestCase):
def test_reset_requires_current_command_on_current_fixture(self):
state = {**initial_state("targeted_type"), "instance": "new", "pid": 19, "ack": "current"}
self.assertTrue(prepared(state, "targeted_type", "new", 19, "current"))
for key, value in (("ack", "previous"), ("instance", "old"), ("pid", 20),
("logicalFocus", "field"), ("selLen", 1), ("top", 1), ("dropOpen", True)):
with self.subTest(key=key):
self.assertFalse(prepared({**state, key: value}, "targeted_type", "new", 19, "current"))
self.assertFalse(acknowledged(state, "new", 19, "previous"))
def test_selectors_never_substitute_another_control(self):
selector = (r"button", r"\bincrement\b")
desired = {"role": "button", "label": "Increment"}
self.assertEqual(select_unique([{"role": "button", "label": "Delete"}, desired], selector), desired)
for elements in ([{"role": "button", "label": "Delete"}],
[{"role": "pane", "label": "Increment"}], [desired, dict(desired)], []):
with self.subTest(elements=elements), self.assertRaises(DiscoveryError):
select_unique(elements, selector)
def test_completion_requires_exact_raw_text_and_unchanged_decoy(self):
final = {**initial_state("targeted_type"), "text": MARKER}
self.assertTrue(completed(final, "targeted_type"))
for text in ("q" + MARKER, MARKER + "q", "Orca qcafé Ω", ""):
with self.subTest(text=text):
self.assertFalse(completed({**final, "text": text}, "targeted_type"))
self.assertFalse(completed({**final, "decoyText": DECOY + MARKER}, "targeted_type"))
def test_exactly_once_actions_reject_intermediate_and_duplicate_effects(self):
for task, key in (("choose_and_click", "count"), ("type_submit", "returns"), ("replace_submit", "returns")):
final = {**initial_state(task), key: 1}
if task == "choose_and_click":
final["combo"] = "Blue"
else:
final["text"] = MARKER
self.assertTrue(completed(final, task))
for value in (0, 2):
with self.subTest(task=task, value=value):
self.assertFalse(completed({**final, key: value}, task))
def test_observed_completion_is_independent_of_api_errors(self):
row = {"prepared": True, "task": "targeted_type", "api_errors": ["timeout"]}
self.assertEqual(verdict(row, {**initial_state(row["task"]), "text": MARKER}), "completed")
self.assertEqual(verdict(row, initial_state(row["task"])), "failed")
self.assertEqual(verdict(row, None), "unobserved")
self.assertEqual(verdict({**row, "prepared": False}, None), "invalid_setup")
def test_invalid_setup_does_not_inflate_scored_completion(self):
base = {"engine": "candidate", "task": "targeted_type", "profile": "background"}
result = summarize([{**base, "verdict": "invalid_setup"},
{**base, "verdict": "failed", "discovery_error": "no field"},
{**base, "verdict": "unobserved"},
{**base, "verdict": "completed", "api_errors": ["late error"]}])[0]
self.assertEqual((result["planned"], result["scored"], result["invalid"], result["completed"]), (4, 3, 1, 1))
self.assertEqual((result["discovery_failures"], result["unobserved"], result["api_errors"]), (1, 1, 1))
def test_partial_failures_cannot_make_task_latency_look_fast(self):
base = {"engine": "candidate", "task": "type_submit", "profile": "background"}
result = summarize([{**base, "verdict": "completed", "attempt_ms": 400, "action_read_ms": [100, 150]},
{**base, "verdict": "failed", "attempt_ms": 2, "action_read_ms": [1]},
{**base, "verdict": "unobserved", "attempt_ms": 30000}])[0]
self.assertEqual(result["task_ms"], 400)
self.assertEqual(result["action_read_ms"], 125)
self.assertEqual((result["timed_completions"], result["timed_failures"]), (1, 2))
self.assertEqual(result["failed_attempt_ms"], 15001)
def test_engine_order_is_reproducible_and_rotates_each_round(self):
cells = schedule(["baseline", "candidate", "cua"], TASKS, 3, 417)
self.assertEqual(cells, schedule(["baseline", "candidate", "cua"], TASKS, 3, 417))
self.assertEqual(len(cells), len(TASKS) * 9)
for task in TASKS:
positions = [[cell["engine"] for cell in cells if cell["task"] == task and cell["round"] == index]
for index in range(3)]
for index in range(3):
self.assertEqual({order[index] for order in positions}, {"baseline", "candidate", "cua"})
def test_process_scope_stops_descendant_writes_before_returning(self):
with tempfile.TemporaryDirectory() as directory:
heartbeat = Path(directory) / "heartbeat"
child = "import pathlib,time; p=pathlib.Path(" + repr(str(heartbeat)) + "); "
child += "\nwhile True:\n p.write_text(str(time.monotonic())); time.sleep(0.02)"
parent = "import subprocess,sys,time; subprocess.Popen([sys.executable,'-c'," + repr(child) + "]); time.sleep(60)"
scope = ProcessScope(directory)
try:
proc = scope.spawn([sys.executable, "-c", parent], hidden=True)
end = time.monotonic() + 10
while not heartbeat.exists() and time.monotonic() < end:
time.sleep(0.01)
self.assertTrue(heartbeat.exists(), "descendant never started")
scope.close()
self.assertIsNotNone(proc.poll())
final = heartbeat.read_bytes()
time.sleep(0.12)
self.assertEqual(heartbeat.read_bytes(), final)
finally:
scope.close()
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,124 @@
"""Task definitions and independently observed benchmark verdicts. No desktop imports."""
import random
import re
import statistics
MARKER = "Orca café Ω"
DECOY = "untouched"
SELECTORS = {
"field": (r"\b(edit|text|entry|textbox)\b", r"\bfield\b"),
"button": (r"\b(button|push button)\b", r"\bincrement\b"),
"color": (r"combo|popup|list ?box|menu ?button", r"\bcolor\b"),
"scroll": (r"list|scroll|document|web", r"\blist\b|\bbenchelectron\b"),
}
TASKS = {
"targeted_type": [("type", "field")],
"type_submit": [("click", "field"), ("type", "field"), ("return", "field")],
"replace_submit": [("click", "field"), ("ctrl_a", "field"), ("type", "field"), ("return", "field")],
"choose_and_click": [("choose", "color"), ("click", "button")],
"click_interference": [("click", "button")],
"scroll_interference": [("scroll", "scroll")],
}
ROLE_KEYS = ("role", "control_type", "role_description", "localized_role", "subrole", "controlType", "localizedControlType")
LABEL_KEYS = ("label", "name", "title", "description", "automation_id", "automationId", "help", "placeholder")
class DiscoveryError(Exception):
pass
def select_unique(elements, selector):
role, label = selector
matches = [element for element in elements
if re.search(role, " ".join(str(element.get(key) or "") for key in ROLE_KEYS), re.I)
and any(re.search(label, str(element.get(key) or ""), re.I) for key in LABEL_KEYS)]
if len(matches) != 1:
raise DiscoveryError(f"selector {selector!r} matched {len(matches)} elements")
return matches[0]
def initial_state(task):
text = "hello" if task == "replace_submit" else ""
return {"text": text, "decoyText": DECOY, "count": 0, "returns": 0, "combo": "Red", "top": 0,
"check": False, "menuHits": 0, "dropOpen": False, "selStart": len(text), "selLen": 0,
"logicalFocus": "decoy" if task == "targeted_type" else "field"}
def acknowledged(state, instance, pid, nonce):
return (state.get("instance") == instance and state.get("pid") == pid
and state.get("ack") == nonce)
def prepared(state, task, instance, pid, nonce):
return acknowledged(state, instance, pid, nonce) and all(
state.get(key) == value for key, value in initial_state(task).items())
def completed(state, task):
expected = initial_state(task)
for key in ("selStart", "selLen", "logicalFocus"):
del expected[key]
if task in ("targeted_type", "type_submit", "replace_submit"):
expected["text"] = MARKER
if task in ("type_submit", "replace_submit"):
expected["returns"] = 1
if task in ("choose_and_click", "click_interference"):
expected["count"] = 1
if task == "choose_and_click":
expected["combo"] = "Blue"
if task == "scroll_interference":
del expected["top"]
if not isinstance(state.get("top"), (int, float)) or state["top"] <= 0:
return False
return all(state.get(key) == value for key, value in expected.items())
def verdict(row, state):
if not row.get("prepared"):
return "invalid_setup"
if state is None:
return "unobserved"
return "completed" if completed(state, row["task"]) else "failed"
def schedule(engines, tasks, rounds, seed):
rng = random.Random(seed)
order = list(engines)
rng.shuffle(order)
cells = []
for round_index in range(rounds):
task_order = list(tasks)
rng.shuffle(task_order)
for task in task_order:
for index in range(len(order)):
cells.append({"engine": order[(index + round_index) % len(order)],
"task": task, "round": round_index})
return cells
def summarize(rows):
groups = {}
for row in rows:
groups.setdefault((row["engine"], row["task"], row["profile"]), []).append(row)
summary = []
for (engine, task, profile), group in sorted(groups.items()):
valid = [row for row in group if row["verdict"] != "invalid_setup"]
successful = [row for row in valid if row["verdict"] == "completed"]
times = [row["attempt_ms"] for row in successful if row.get("attempt_ms") is not None]
failures = [row["attempt_ms"] for row in valid if row["verdict"] != "completed" and row.get("attempt_ms") is not None]
paired = [sample for row in successful for sample in row.get("action_read_ms", [])]
summary.append({"engine": engine, "task": task, "profile": profile, "planned": len(group),
"scored": len(valid), "invalid": len(group) - len(valid),
"completed": sum(row["verdict"] == "completed" for row in valid),
"unobserved": sum(row["verdict"] == "unobserved" for row in valid),
"api_errors": sum(bool(row.get("api_errors")) for row in valid),
**{key: sum(row.get(key, 0) for row in valid) for key in
("keys_sent", "keys_received", "keys_lost", "agent_text_in_sentinel", "target_q_characters")},
"discovery_failures": sum(bool(row.get("discovery_error")) for row in valid),
"task_ms": round(statistics.median(times), 1) if times else None,
"failed_attempt_ms": round(statistics.median(failures), 1) if failures else None,
"timed_completions": len(times), "timed_failures": len(failures),
"action_read_ms": round(statistics.median(paired), 1) if paired else None,
**{key: sum(bool(row.get("observation", {}).get(key)) for row in valid)
for key in ("sentinel_lost_focus", "target_raised", "pointer_moved", "clipboard_changed")}})
return summary