Skip to main content

uisurf-agent

uisurf-agent is a Python package for running UI automation agents against:

  • a web browser via Playwright
  • the local desktop via desktop control utilities
  • an Android device or emulator via ADB and adbutils

The project includes:

  • a package-level Typer CLI at src/uisurf_agent/cli.py
  • A2A server entrypoints for browser, desktop, and mobile agents
  • interactive local execution for manual testing

Requirements

  • Python 3.11+
  • pip
  • Node.js only if you also use the bundled a2a-inspector
  • Playwright browser binaries installed for browser automation
  • a desktop environment if you run the desktop agent
  • Android platform tools plus an authorized ADB device or emulator if you run the mobile agent

Install

Install the package from PyPI:

pip install uisurf-agent

Install Playwright browsers if needed:

python -m playwright install

For local development from a source checkout, use uv instead:

uv sync
uv run playwright install

Environment

The package reads configuration from environment variables and .env files via python-dotenv.

Common variables:

  • UISURF_PROVIDER: registered provider name. Default is gemini.
  • UISURF_MODEL_ID / MODEL_ID: provider-specific model identifier. Gemini defaults to gemini-3-flash-preview.
  • UISURF_PROVIDER_BASE_URL / UISURF_MODEL_BASE_URL / MODEL_BASE_URL: optional provider-specific model base URL.
  • AGENT_HOST: default bind host for A2A servers
  • BROWSER_AGENT_PORT: default browser A2A port, default 8001
  • DESKTOP_AGENT_PORT: default desktop A2A port, default 8002
  • MOBILE_AGENT_PORT: default mobile A2A port, default 8003
  • BROWSER_AGENT_PUBLIC_URL: public URL advertised by the browser A2A server
  • DESKTOP_AGENT_PUBLIC_URL: public URL advertised by the desktop A2A server
  • MOBILE_AGENT_PUBLIC_URL: public URL advertised by the mobile A2A server
  • BROWSER_FAST_MODE: speeds up browser settling by waiting less aggressively
  • AUTO_MODE: automatically approve safety-gated A2A actions by default
  • BROWSER_AGENT_AUTO_MODE: browser A2A override for automatic safety approval
  • DESKTOP_AGENT_AUTO_MODE: desktop A2A override for automatic safety approval
  • MOBILE_AGENT_AUTO_MODE: mobile A2A override for automatic safety approval
  • INCLUDE_THOUGHTS: global default for model thought streaming when supported
  • BROWSER_INCLUDE_THOUGHTS: browser-only override for thought streaming
  • DESKTOP_INCLUDE_THOUGHTS: desktop-only override for thought streaming
  • MOBILE_INCLUDE_THOUGHTS: mobile-only override for thought streaming
  • BROWSER_AGENT_MAX_STEPS: browser A2A step limit, default 40
  • DESKTOP_AGENT_MAX_STEPS: desktop A2A step limit, default 40
  • MOBILE_AGENT_MAX_STEPS: mobile A2A step limit, default 40
  • DESKTOP_OBSERVATION_DELAY_MS: delay before each desktop screenshot capture
  • MOBILE_OBSERVATION_DELAY_MS: delay before each mobile screenshot capture
  • MOBILE_DEVICE_SERIAL: Android device serial used by mobile A2A when --mobile-serial is omitted
  • MAX_OBSERVATION_IMAGES: number of screenshot observations that keep image payloads in history
  • OBSERVATION_SCALE: default screenshot scale, from 0 < scale <= 1
  • BROWSER_OBSERVATION_SCALE: browser-only screenshot scale override
  • DESKTOP_OBSERVATION_SCALE: desktop-only screenshot scale override
  • MOBILE_OBSERVATION_SCALE: mobile-only screenshot scale override

Screenshot scaling only changes the image sent to the model. Action coordinates still map to the full browser viewport, desktop resolution, or Android device resolution.

When using the built-in gemini provider, you will also need the credentials required by the Google client.

CLI

The CLI is implemented with Typer and is exposed through both:

uisurf-agent --help

Current top-level command:

uisurf_agent run --help

Convenience make targets are also available for local A2A server runs:

make run-browser
make run-desktop
make run-mobile
make run-dev
make run-all

These targets start long-running local servers instead of one-off tasks. By default they bind to:

browser: http://127.0.0.1:8001/
desktop: http://127.0.0.1:8002/
mobile: http://127.0.0.1:8003/

You can override the bind host and ports from the shell:

make run-browser HOST=127.0.0.1 BROWSER_PORT=8001
make run-desktop HOST=127.0.0.1 DESKTOP_PORT=8002
make run-mobile HOST=127.0.0.1 MOBILE_PORT=8003 MOBILE_SERIAL=5C060DLCR002MM MOBILE_OBSERVATION_DELAY_MS=1500
make run-dev HOST=127.0.0.1 BROWSER_PORT=8001 DESKTOP_PORT=8002 MOBILE_PORT=8003
make run-all HOST=127.0.0.1 BROWSER_PORT=8001 DESKTOP_PORT=8002 MOBILE_PORT=8003

If one of those ports is already in use, the make target will exit early with a clear message. Rerun with free ports if needed.

If you want the previous one-off local task mode, use the interactive targets:

make run-browser-interactive TASK="Open example.com and summarize the page"
make run-desktop-interactive TASK="Open Terminal and run pwd"

Run the browser agent interactively

uisurf_agent run browser_agent \
  --task "Open example.com and summarize the page" \
  --fast-mode \
  --no-include-thoughts \
  --max-observation-images 2 \
  --observation-scale 0.75 \
  --max-steps 20

Run headless:

uisurf_agent run browser_agent \
  --headless \
  --task "Go to Hacker News and summarize the top 5 stories"

Select a registered provider:

uisurf_agent run browser_agent \
  --provider gemini \
  --model-id gemini-3-flash-preview \
  --task "Open example.com and summarize the page"

Extending providers

Provider implementations and custom model registration are documented in mkdocs/models/external-models.md.

Extending browser, desktop, and mobile tools

Browser, desktop, and mobile custom tools use the same registry pattern. A declaration function describes the tool schema sent to the model. An optional handler executes the tool when the selected agent receives that function call:

from typing import Any

from uisurf_agent import BrowserAgent, register_browser_tool


async def save_page_title(agent: BrowserAgent, args: dict[str, Any]) -> None:
    title = await agent._browser_controller.page.title()
    print(f"{args['label']}: {title}")


@register_browser_tool(handler=save_page_title)
def save_page_title(label: str) -> dict[str, str]:
    """Save the current page title with a label."""
    return {"label": label}

Use register_desktop_tool(...) for desktop-only tools, register_mobile_tool(...) for Android-only tools, or register_tool("browser" | "desktop" | "mobile", ...) when the environment is selected dynamically. If a registered tool has no handler, the agent falls back to a controller method with the same name. Installed packages can expose tool setup through the uisurf_agent.tools entry-point group. Entry points may expose a registration function, a ToolRegistration, or a declaration callable named like browser.my_tool, desktop.my_tool, or mobile.my_tool.

Run the desktop agent interactively

uisurf_agent run desktop_agent \
  --task "Open Terminal and run pwd" \
  --desktop-observation-delay-ms 750 \
  --no-include-thoughts \
  --max-observation-images 2 \
  --observation-scale 0.75 \
  --max-steps 10

Automatically approve safety-gated actions:

uisurf_agent run desktop_agent \
  --task "Open a text editor and type Hello World" \
  --auto-mode

Run the mobile agent interactively

First verify that ADB can see your Android device or emulator:

adb devices -l

Then run MobileAgent:

uisurf_agent run mobile_agent \
  --task "Open Android Settings and tell me what is visible" \
  --mobile-serial 5C060DLCR002MM \
  --auto-mode \
  --max-steps 10

If --mobile-serial is omitted, the controller uses the first authorized ADB device.

For Python API usage, see mkdocs/agents/mobile.md.

A2A Server Mode

Browser, desktop, and mobile agents can be exposed as A2A servers through the CLI.

Browser A2A server

uisurf_agent run browser_agent \
  --mode a2a \
  --host 0.0.0.0 \
  --port 8080

If --port is omitted, the browser agent defaults to 8001.

Desktop A2A server

uisurf_agent run desktop_agent \
  --mode a2a \
  --host 0.0.0.0 \
  --port 8081

If --port is omitted, the desktop agent defaults to 8002.

Mobile A2A server

uisurf_agent run mobile_agent \
  --mode a2a \
  --host 0.0.0.0 \
  --port 8003 \
  --mobile-serial 5C060DLCR002MM

If --port is omitted, the mobile agent defaults to 8003. If --mobile-serial is omitted, the server uses MOBILE_DEVICE_SERIAL or the first authorized ADB device.

MCP Mode

The CLI accepts --mode mcp, but MCP server mode is not implemented yet. The command currently exits with a clear error instead of starting a server.

Docker

The repository includes a containerized runtime that starts:

  • noVNC on port 6080
  • a Chromium instance inside the container with remote debugging on port 9222
  • the browser agent A2A server on port 8001
  • the desktop agent A2A server on port 8002

Provide environment variables

The recommended approach is to place secrets and runtime settings in a local .env file in the repository root. The wrapper script run.sh will automatically pass that file to Docker with --env-file if it exists.

Example .env:

GEMINI_API_KEY=your_key_here
UISURF_PROVIDER=gemini
UISURF_MODEL_ID=gemini-3-flash-preview
AGENT_HOST=0.0.0.0
BROWSER_AGENT_PORT=8001
DESKTOP_AGENT_PORT=8002
BROWSER_FAST_MODE=true
AUTO_MODE=false
INCLUDE_THOUGHTS=false
DESKTOP_OBSERVATION_DELAY_MS=750
MAX_OBSERVATION_IMAGES=2
OBSERVATION_SCALE=0.75
PASSWORD_REQUIRED=false

You can also point the wrapper at a different file:

ENV_FILE=.env.local sh ./run.sh

Build and run the container

From the repository root:

sh ./run.sh

The script builds the image from docker/Dockerfile, starts the container, and publishes the default ports to the host.

Default host endpoints:

  • noVNC: http://localhost:6080
  • browser A2A server: http://localhost:6080/browser/
  • desktop A2A server: http://localhost:6080/desktop/

Override published ports

The wrapper script supports environment variable overrides:

BROWSER_AGENT_PUBLIC_URL=http://localhost:6081/browser/ \
DESKTOP_AGENT_PUBLIC_URL=http://localhost:6081/desktop/ \
PORT=6081 \
sh ./run.sh

View logs

docker logs -f uisurf-agent-test

Stop the container

docker rm -f uisurf-agent-test

Notes

  • The container startup will fail early if neither GEMINI_API_KEY nor GOOGLE_API_KEY is provided.
  • VNC password auth is disabled by default. Set PASSWORD_REQUIRED=true if you want the frontend to require a VNC password again.
  • Inside the container, Chromium is started separately and the browser controller connects to it over CDP at http://127.0.0.1:9222.
  • The desktop and browser agents are both started through the package CLI from src/uisurf_agent/cli.py.
  • MobileAgent is available from the package CLI/Python API and A2A mode, but the default Docker runtime does not start a mobile A2A server or provision an Android emulator/device.

Logging

The package configures a Rich-backed library logger in src/uisurf_agent/init.py. The logger name is uisurf_agent.

Package Layout

Development

Validate the package modules compile:

uv run python -m compileall src/uisurf_agent

Show CLI help locally through the package module:

uv run python -m uisurf_agent --help

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

uisurf_agent-0.2.0.tar.gz (974.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

uisurf_agent-0.2.0-py3-none-any.whl (88.2 kB view details)

Uploaded Python 3

File details

Details for the file uisurf_agent-0.2.0.tar.gz.

File metadata

  • Download URL: uisurf_agent-0.2.0.tar.gz
  • Upload date:
  • Size: 974.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for uisurf_agent-0.2.0.tar.gz
Algorithm Hash digest
SHA256 3d11bf035091d8ca8219a6d4a6427284d1ad371cd27ed50b7650798230410300
MD5 d2ce8c4f3dff9550b11297c8ee7739f3
BLAKE2b-256 b42a5a21d3c37c9efbbafa51b3b4a3b4e6b8d4bc284143a7e3215cd5aef06796

See more details on using hashes here.

Provenance

The following attestation bundles were made for uisurf_agent-0.2.0.tar.gz:

Publisher: publish-pypi.yml on OpenSciML/uisurf-agent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file uisurf_agent-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: uisurf_agent-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 88.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for uisurf_agent-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a2d4b32772b189660658bb3b52eb59131d928a2b7b08eb4b20b4c72cfa83a46a
MD5 bb743160fc21b4d4d4ce6548bba0d7bc
BLAKE2b-256 6ca35077caf4d3bf67163c9e2cb2546e48ee9c180e49abeb0d77b4d0f93789fe

See more details on using hashes here.

Provenance

The following attestation bundles were made for uisurf_agent-0.2.0-py3-none-any.whl:

Publisher: publish-pypi.yml on OpenSciML/uisurf-agent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page