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 isgemini.UISURF_MODEL_ID/MODEL_ID: provider-specific model identifier. Gemini defaults togemini-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 serversBROWSER_AGENT_PORT: default browser A2A port, default8001DESKTOP_AGENT_PORT: default desktop A2A port, default8002MOBILE_AGENT_PORT: default mobile A2A port, default8003BROWSER_AGENT_PUBLIC_URL: public URL advertised by the browser A2A serverDESKTOP_AGENT_PUBLIC_URL: public URL advertised by the desktop A2A serverMOBILE_AGENT_PUBLIC_URL: public URL advertised by the mobile A2A serverBROWSER_FAST_MODE: speeds up browser settling by waiting less aggressivelyAUTO_MODE: automatically approve safety-gated A2A actions by defaultBROWSER_AGENT_AUTO_MODE: browser A2A override for automatic safety approvalDESKTOP_AGENT_AUTO_MODE: desktop A2A override for automatic safety approvalMOBILE_AGENT_AUTO_MODE: mobile A2A override for automatic safety approvalINCLUDE_THOUGHTS: global default for model thought streaming when supportedBROWSER_INCLUDE_THOUGHTS: browser-only override for thought streamingDESKTOP_INCLUDE_THOUGHTS: desktop-only override for thought streamingMOBILE_INCLUDE_THOUGHTS: mobile-only override for thought streamingBROWSER_AGENT_MAX_STEPS: browser A2A step limit, default40DESKTOP_AGENT_MAX_STEPS: desktop A2A step limit, default40MOBILE_AGENT_MAX_STEPS: mobile A2A step limit, default40DESKTOP_OBSERVATION_DELAY_MS: delay before each desktop screenshot captureMOBILE_OBSERVATION_DELAY_MS: delay before each mobile screenshot captureMOBILE_DEVICE_SERIAL: Android device serial used by mobile A2A when--mobile-serialis omittedMAX_OBSERVATION_IMAGES: number of screenshot observations that keep image payloads in historyOBSERVATION_SCALE: default screenshot scale, from0 < scale <= 1BROWSER_OBSERVATION_SCALE: browser-only screenshot scale overrideDESKTOP_OBSERVATION_SCALE: desktop-only screenshot scale overrideMOBILE_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_KEYnorGOOGLE_API_KEYis provided. - VNC password auth is disabled by default. Set
PASSWORD_REQUIRED=trueif 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.
MobileAgentis 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
- src/uisurf_agent/cli.py: Typer CLI
- src/uisurf_agent/a2a/browser_a2a.py: browser A2A server
- src/uisurf_agent/a2a/desktop_a2a.py: desktop A2A server
- src/uisurf_agent/a2a/mobile_a2a.py: mobile A2A server
- src/uisurf_agent/agents/browser_agent.py: browser automation agent
- src/uisurf_agent/agents/desktop_agent.py: desktop automation agent
- src/uisurf_agent/agents/mobile_agent.py: Android automation agent
- src/uisurf_agent/utils/mobile_controller.py: ADB-backed mobile controller
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3d11bf035091d8ca8219a6d4a6427284d1ad371cd27ed50b7650798230410300
|
|
| MD5 |
d2ce8c4f3dff9550b11297c8ee7739f3
|
|
| BLAKE2b-256 |
b42a5a21d3c37c9efbbafa51b3b4a3b4e6b8d4bc284143a7e3215cd5aef06796
|
Provenance
The following attestation bundles were made for uisurf_agent-0.2.0.tar.gz:
Publisher:
publish-pypi.yml on OpenSciML/uisurf-agent
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
uisurf_agent-0.2.0.tar.gz -
Subject digest:
3d11bf035091d8ca8219a6d4a6427284d1ad371cd27ed50b7650798230410300 - Sigstore transparency entry: 2326658897
- Sigstore integration time:
-
Permalink:
OpenSciML/uisurf-agent@f14045d5d8ccfad8ec0a1947a4a605b3582dffe7 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/OpenSciML
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@f14045d5d8ccfad8ec0a1947a4a605b3582dffe7 -
Trigger Event:
workflow_dispatch
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2d4b32772b189660658bb3b52eb59131d928a2b7b08eb4b20b4c72cfa83a46a
|
|
| MD5 |
bb743160fc21b4d4d4ce6548bba0d7bc
|
|
| BLAKE2b-256 |
6ca35077caf4d3bf67163c9e2cb2546e48ee9c180e49abeb0d77b4d0f93789fe
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
uisurf_agent-0.2.0-py3-none-any.whl -
Subject digest:
a2d4b32772b189660658bb3b52eb59131d928a2b7b08eb4b20b4c72cfa83a46a - Sigstore transparency entry: 2326659394
- Sigstore integration time:
-
Permalink:
OpenSciML/uisurf-agent@f14045d5d8ccfad8ec0a1947a4a605b3582dffe7 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/OpenSciML
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@f14045d5d8ccfad8ec0a1947a4a605b3582dffe7 -
Trigger Event:
workflow_dispatch
-
Statement type: