Superfast action layer for computer-use agents

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.

For agents and LLMs

Machine-readable documentation optimized for LLM context windows:

Quickstart

Currently macOS-first. Requires Python 3.12+.

python3.12 -m venv .venv
source .venv/bin/activate
pip install -e '.[macos]'
export TYPESAFE_API_KEY=...

macOS permissions

The terminal or editor running Python needs both:

Restart the terminal after granting permissions if necessary.

Live desktop execution

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)

Deterministic demo (no API key)

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

Architecture

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 agent owns intent

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

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).

Hybrid macOS perception

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.

Freshness guards

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.

Post-action settling

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.

Terminal states

StatusMeaning
SUBTASK_COMPLETEVerification criteria appear satisfied
BLOCKEDCannot make progress with available operations
NEEDS_AGENTHigher-level reasoning required or action budget reached

API Reference

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,
)

Subtask

Contract supplied by the external agent/planner. The executor never invents verification criteria or free-form text.

  • goal str Natural-language description of what to accomplish
  • verification tuple[str, ...] Observable criteria the policy checks for completion
  • inputs Mapping[str, str|int|float|bool] Literal values the executor may use (default: empty)
  • constraints tuple[str, ...] Things the executor must not do (default: empty)
  • max_actions int Action budget (default: 30)
  • metadata Mapping[str, Any] Opaque pass-through for the caller

DesktopElement

A normalized, currently observable UI element. IDs are stable for the session lifetime and are never model-invented.

  • id str Stable identifier for this element
  • role str Semantic role (button, text_field, visible_text, ...)
  • name str Human-readable label
  • value str|int|float|bool|None Current value (text field contents, slider position, ...)
  • actions tuple[ActionKind, ...] Legal operations on this element
  • enabled bool Whether the element is interactive (default: True)
  • visible bool Whether the element is visible (default: True)
  • source str Perception source: macos_ax, macos_ocr, etc.
  • bounds Bounds|None Screen coordinates (x, y, width, height)
  • accepts_drop bool Valid DRAG_TO destination

DesktopSnapshot

Immutable capture of the current desktop state.

  • application str Frontmost application name
  • window str Active window title
  • revision str SHA-256 fingerprint of element state
  • elements tuple[DesktopElement, ...] All observable elements
  • context Mapping[str, Any] Backend metadata (pid, window_id, ...)

element(element_id) -> DesktopElement — O(1) lookup by ID. Raises KeyError.

compact() -> dict — Planner-friendly summary (visible elements only).

ActionKind

ValueTargetAdditional
CLICKyes
DOUBLE_CLICKyes
RIGHT_CLICKyes
TYPE_TEXTyesinput_key (resolved from Subtask.inputs)
SET_VALUEyesinput_key (resolved from Subtask.inputs)
PRESS_KEYnokey (ENTER, ESCAPE, TAB, ...)
HOTKEYnohotkey (MOD+C, MOD+V, ...)
SCROLLnoscroll_direction (UP, DOWN, LEFT, RIGHT)
DRAG_TOyessecondary_target_id
DRAG_BYyesdrag_dx, drag_dy

MOD means Cmd on macOS, Ctrl elsewhere.

DesktopExecutor

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.

RuntimeConfig

  • stale_retries int = 8 Max freshness re-observations before giving up
  • no_change_limit int = 3 Consecutive no-change actions before BLOCKED
  • post_action_settle_s float = 0.03 Minimum settling delay
  • timeout_s float|None = None Wall-clock timeout (None = no limit)
  • verify VerifyFn|None = None Verification callback for SUBTASK_COMPLETE

VerifyFn

VerifyFn = Callable[[DesktopSnapshot, Subtask], bool]

When set on RuntimeConfig, called before accepting SUBTASK_COMPLETE. If it returns False, the result becomes NEEDS_AGENT.

StepEvent

Emitted by run_iter() after each decision cycle.

  • step int Step counter
  • snapshot DesktopSnapshot Desktop state at decision time
  • decision Decision The policy's decision
  • action ExecutableAction|None Materialized action (None for terminal)
  • record ActionRecord|None Execution record (None for terminal)
  • result ExecutionResult|None Set only on the final event

terminal property returns True when result is set.

ExecutionResult

  • status TerminalKind How execution ended
  • subtask Subtask The original subtask
  • final_snapshot DesktopSnapshot Desktop state at termination
  • history tuple[ActionRecord, ...] Complete action history
  • observations tuple[str, ...] Satisfaction notes for verification criteria
  • reason str|None Explanation for non-complete termination

actions_taken property returns len(history).

JSON boundary

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)

Errors

All inherit from JevDesktopError(RuntimeError):

ErrorMeaningRuntime behavior
StaleDesktopStateTarget changed between observation and executionCaught, triggers re-observation
InvalidDecisionPolicy returned an illegal actionPropagates to caller
UnsupportedDesktopActionBackend does not support the action kindPropagates to caller

Backends

MacOSHybridBackend

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()

MacOSAXBackend

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.

StateMachineBackend

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
)

Policies

TypeSafeJevPolicy

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,
)

ScriptedPolicy

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),
])

Protocols

Extend arc-cua by implementing these protocols. No base class required — structural subtyping.

DesktopBackend

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.

DecisionPolicy

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.

Extending

Custom backend

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
        ...

Custom policy

Implement decide(). Return a Decision with either kind (action) or terminal (end).

Logging

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.

Dependencies

Runtime

httpx[http2] >= 0.28, < 1

macOS extras

pyobjc-framework-ApplicationServices >= 11
pyobjc-framework-Cocoa >= 11
pyobjc-framework-Quartz >= 11
pyobjc-framework-Vision >= 11