Hand off bounded desktop subtasks to a fast decision model. No frontier model needed for every click.
arc-cua lets a planner or CUA agent hand off bounded desktop subtasks to a fast decision model (JEV) that executes the UI loop. The planner owns intent — what to do, what text to use, what counts as success. The executor owns observation, fast decisions, freshness checks, native UI execution, and bounded termination.
from arc_cua import execute_payload
result = execute_payload(executor, {
"goal": "Play Get Lucky by Daft Punk in Spotify",
"inputs": {"search_query": "Get Lucky Daft Punk"},
"verification": ["Spotify shows Get Lucky as the current track"],
"constraints": ["Do not modify the user's library"],
"max_actions": 15,
})
# result: {"status": "SUBTASK_COMPLETE", "actions_taken": 4}
Any GPT, Claude, Gemini, local model, or deterministic planner can generate that payload. The planner deliberately lives outside the package.
Machine-readable documentation optimized for LLM context windows:
Currently macOS-first. Requires Python 3.12+.
python3.12 -m venv .venv
source .venv/bin/activate
pip install -e '.[macos]'
export TYPESAFE_API_KEY=...
The terminal or editor running Python needs both:
Restart the terminal after granting permissions if necessary.
from arc_cua import DesktopExecutor, Subtask, RuntimeConfig
from arc_cua.backends import MacOSHybridBackend
from arc_cua.policies import TypeSafeJevPolicy
executor = DesktopExecutor(
MacOSHybridBackend(),
TypeSafeJevPolicy(),
config=RuntimeConfig(timeout_s=30),
)
task = Subtask(
goal="Search for Gaussian Blur in the effects panel",
verification=("Effects panel shows Gaussian Blur",),
inputs={"effect_name": "Gaussian Blur"},
constraints=("Do not modify any clip",),
)
result = executor.run(task)
print(result.status, result.actions_taken)
from arc_cua import (
ActionKind, Decision, DesktopElement, DesktopExecutor,
DesktopSnapshot, Subtask, TerminalKind,
)
from arc_cua.backends import StateMachineBackend
from arc_cua.policies import ScriptedPolicy
def make_snapshot(state):
return DesktopSnapshot(
application="App", window="Main", revision=state["q"],
elements=(
DesktopElement(
id="search", role="text_field", name="Search",
value=state["q"], actions=(ActionKind.TYPE_TEXT,),
source="demo",
),
),
)
def transition(state, action):
if action.kind == ActionKind.TYPE_TEXT:
state["q"] = action.value
backend = StateMachineBackend({"q": ""}, make_snapshot, transition)
policy = ScriptedPolicy([
Decision(kind=ActionKind.TYPE_TEXT, target_id="search", input_key="query"),
Decision(terminal=TerminalKind.SUBTASK_COMPLETE),
])
task = Subtask(
goal="Search for cats",
verification=("Search field contains cats",),
inputs={"query": "cats"},
)
result = DesktopExecutor(backend, policy).run(task)
assert result.status == TerminalKind.SUBTASK_COMPLETE
planner / LLM
|
| Subtask(goal, inputs, verification, constraints)
v
+------------------------+
| arc-cua |
| |
| observe desktop |
| (AX + Vision OCR) |
| v |
| build legal |
| action space |
| v |
| JEV decision |<------+
| v | |
| freshness guard | |
| v | |
| execute UI action | |
| v | |
| wait for UI settle |-------+
+------------+-----------+
|
v
SUBTASK_COMPLETE / BLOCKED / NEEDS_AGENT
|
v
planner
The upstream agent decides what needs to happen, what literal text may be used, what must not happen, and what counts as success. JEV chooses which element to target and which operation to perform — but never invents arbitrary text. Literal values always originate from the agent via Subtask.inputs.
JEV is the decision backend that powers the action loop. Given structured desktop state (elements, roles, values), it selects the next UI operation from a dynamically built action space — it can only pick targets and operations the current desktop actually exposes. JEV is accessed through TypeSafe. One JEV call can resolve the operation and its parameters in parallel (speculative fan-out).
Accessibility (AX)
Semantic controls: buttons, fields, menus, roles, values, native actions. Covers well-built apps with proper accessibility trees.
Apple Vision OCR
Visible screen text with bounding boxes. Covers apps with incomplete accessibility — Electron apps, custom canvases, web views.
Both normalize into DesktopElements that JEV reasons over. JEV receives structured elements and IDs, not screenshots.
Every actionable target has a semantic or visual guard (SHA-256 fingerprint of element state). Before executing a chosen mutation, the backend checks that the target still corresponds to the UI state JEV observed. A stale action is never blindly replayed.
After a mutating action, the runtime re-observes the desktop until structurally stable or a timeout is reached. The decision model decides what to do; the runtime decides when the UI is ready to reason over again.
| Status | Meaning |
|---|---|
SUBTASK_COMPLETE | Verification criteria appear satisfied |
BLOCKED | Cannot make progress with available operations |
NEEDS_AGENT | Higher-level reasoning required or action budget reached |
All public types are available from the top-level arc_cua package:
from arc_cua import (
ActionKind, Bounds, Decision, DesktopElement, DesktopExecutor,
DesktopSnapshot, ExecutableAction, ExecutionResult, RuntimeConfig,
StepEvent, Subtask, TerminalKind, VerifyFn,
execute_payload, result_to_dict, subtask_from_dict,
)
Contract supplied by the external agent/planner. The executor never invents verification criteria or free-form text.
A normalized, currently observable UI element. IDs are stable for the session lifetime and are never model-invented.
Immutable capture of the current desktop state.
element(element_id) -> DesktopElement — O(1) lookup by ID. Raises KeyError.
compact() -> dict — Planner-friendly summary (visible elements only).
| Value | Target | Additional |
|---|---|---|
CLICK | yes | — |
DOUBLE_CLICK | yes | — |
RIGHT_CLICK | yes | — |
TYPE_TEXT | yes | input_key (resolved from Subtask.inputs) |
SET_VALUE | yes | input_key (resolved from Subtask.inputs) |
PRESS_KEY | no | key (ENTER, ESCAPE, TAB, ...) |
HOTKEY | no | hotkey (MOD+C, MOD+V, ...) |
SCROLL | no | scroll_direction (UP, DOWN, LEFT, RIGHT) |
DRAG_TO | yes | secondary_target_id |
DRAG_BY | yes | drag_dx, drag_dy |
MOD means Cmd on macOS, Ctrl elsewhere.
The core executor. Takes a backend and a policy, runs subtasks.
executor = DesktopExecutor(backend, policy, config=RuntimeConfig())
result = executor.run(task) # Returns ExecutionResult
for event in executor.run_iter(task): # Yields StepEvent per cycle
print(event.step, event.action)
executor.cancel() # Thread-safe stop signal
run() executes the subtask and returns the result. run_iter() is a generator yielding a StepEvent after each decision cycle for live observability. cancel() can be called from another thread to stop execution after the current action.
VerifyFn = Callable[[DesktopSnapshot, Subtask], bool]
When set on RuntimeConfig, called before accepting SUBTASK_COMPLETE. If it returns False, the result becomes NEEDS_AGENT.
Emitted by run_iter() after each decision cycle.
terminal property returns True when result is set.
actions_taken property returns len(history).
Three functions for the agent-facing JSON contract:
# Parse agent payload into Subtask
task = subtask_from_dict({
"goal": "...", "verification": ["..."],
"inputs": {...}, "constraints": [...], "max_actions": 15,
})
# Convert result to planner-friendly dict
d = result_to_dict(result)
# {"status", "actions_taken", "reason", "observations", "history", "final_snapshot"}
# One-shot convenience
d = execute_payload(executor, payload)
All inherit from JevDesktopError(RuntimeError):
| Error | Meaning | Runtime behavior |
|---|---|---|
StaleDesktopState | Target changed between observation and execution | Caught, triggers re-observation |
InvalidDecision | Policy returned an illegal action | Propagates to caller |
UnsupportedDesktopAction | Backend does not support the action kind | Propagates to caller |
The default production backend. Combines Accessibility and Apple Vision OCR into a single element stream. Handles modal detection, OCR deduplication, and coordinate-based execution for OCR targets.
from arc_cua.backends import MacOSHybridBackend
backend = MacOSHybridBackend()
Pure Accessibility backend. Traverses the AX tree of the frontmost application.
from arc_cua.backends import MacOSAXBackend
backend = MacOSAXBackend(max_elements=1200, max_depth=18)
Supports CLICK, DOUBLE_CLICK, RIGHT_CLICK, TYPE_TEXT, SET_VALUE, PRESS_KEY, HOTKEY, SCROLL.
Deterministic backend for tests and demos. No OS dependencies.
from arc_cua.backends import StateMachineBackend
backend = StateMachineBackend(
initial_state={"key": "value"},
snapshot_factory=my_snapshot_fn, # (state) -> DesktopSnapshot
transition=my_transition_fn, # (state, action) -> None
)
Production policy. Builds a dynamic action space from the current DesktopSnapshot and sends it to the JEV API.
from arc_cua.policies import TypeSafeJevPolicy
policy = TypeSafeJevPolicy(
api_key="...", # or TYPESAFE_API_KEY env var
model="jev-latest", # or TYPESAFE_MODEL env var
timeout_s=12.0,
)
Deterministic policy for tests. Consumes decisions in order.
from arc_cua.policies import ScriptedPolicy
policy = ScriptedPolicy([
Decision(kind=ActionKind.CLICK, target_id="btn"),
Decision(terminal=TerminalKind.SUBTASK_COMPLETE),
])
Extend arc-cua by implementing these protocols. No base class required — structural subtyping.
class DesktopBackend(Protocol):
def observe(self) -> DesktopSnapshot: ...
def is_fresh(self, snapshot: DesktopSnapshot, action: ExecutableAction) -> bool: ...
def execute(self, snapshot: DesktopSnapshot, action: ExecutableAction) -> None: ...
observe() captures current desktop state. is_fresh() checks the target guard still matches reality. execute() performs the action — raise StaleDesktopState if the target changed, InvalidDecision if the action is illegal.
class DecisionPolicy(Protocol):
def decide(
self, *,
subtask: Subtask,
snapshot: DesktopSnapshot,
history: Sequence[ActionRecord],
) -> Decision: ...
Return Decision(kind=..., target_id=...) for an action, or Decision(terminal=...) to end execution.
Implement observe(), is_fresh(), and execute(). Return DesktopElements with stable IDs. Use semantic_guard() for freshness checks.
class MyBackend:
def observe(self) -> DesktopSnapshot:
elements = self._scan_ui()
return DesktopSnapshot(
application="MyApp", window="Main",
revision=self._compute_revision(elements),
elements=tuple(elements),
)
def is_fresh(self, snapshot, action) -> bool:
if action.target_id:
current = self.observe().element(action.target_id)
return current.semantic_guard() == action.target_guard
return True
def execute(self, snapshot, action) -> None:
# Perform the UI action
...
Implement decide(). Return a Decision with either kind (action) or terminal (end).
arc-cua uses stdlib logging. Enable debug output:
import logging
logging.basicConfig(level=logging.DEBUG)
Loggers: arc_cua.runtime, arc_cua.backends.macos_ax, arc_cua.backends.macos_hybrid, arc_cua.backends.macos_ocr, arc_cua.policies.typesafe.
Runtime
httpx[http2] >= 0.28, < 1
macOS extras
pyobjc-framework-ApplicationServices >= 11
pyobjc-framework-Cocoa >= 11
pyobjc-framework-Quartz >= 11
pyobjc-framework-Vision >= 11