The Problem
DroidRun automates mobile devices with natural‑language LLM commands, but the code base has several structural and SDLC weaknesses that increase the cost of change and raise operational risk. No test suite exists, dependencies are unpinned, and the Docker base image is mutable, meaning builds may silently pull different runtimes. Import cycles and duplicated logic make the agent hard to reason about and to modify safely.
What This Does
DroidRun is a Python framework (104 code files, 117 total) that sits between an LLM and a mobile device. Core execution flows through droidrun/cli/main.py (run entry point, reaches 103 functions) and droidrun/agent/executor/executor_agent.py (execute entry point, reaches 38 functions). From run the pipeline calls get_state → network request (requests.get) to obtain device state, then dispatches actions such as swipe, tap_by_coordinates, or take_screenshot via droidrun/tools/android/adb.py (14 callers, runs external commands and makes outbound network calls). The agent model is invoked by load_llm (6 callers) and the result is processed through execute → ActionResult (14 callers). File I/O occurs in droidrun/agent/trajectory/writer.py (reads/writes trajectories) and droidrun/cli/main.py (logs, portal APK download). Cryptographic or secret‑generating work is limited to droidrun/telemetry/langfuse_processor.py (26 functions, includes secret‑setting helpers). An external command is executed via droidrun/tools/android/adb.py (adb shell).
Key files and their responsibilities (by inbound call volume):
| File | Functions/Classes | Callers | Primary effects |
|---|---|---|---|
droidrun/tools/android/adb.py | 37 functions, 1 class | 14 | Runs adb commands, connects to device, takes screenshots, performs taps/swipes |
droidrun/agent/executor/executor_agent.py | 8 functions, 1 class | 1 | Prepares context, calls LLM, executes actions, writes trajectory |
droidrun/agent/codeact/codeact_agent.py | 11 functions, 1 class | 2 | Wraps CodeAct agent, builds prompts, calls model for inference |
droidrun/config_manager/config_manager.py | 12 functions, 14 classes | 13 | Central config loader; 13 modules depend on it (high‑blast-radius hub) |
droidrun/cli/main.py | 17 functions, 1 class | 2 | CLI entry points: run, connect, wrapper, setup; reads config, triggers agent execution |
droidrun/tools/helpers/element_search.py | 49 functions, 1 class | – | Provides flatten_tree, get_element_center, element matching for UI automation |
droidrun/telemetry/langfuse_processor.py | 26 functions, 1 class | 1 | Telemetry spans, records model calls, performs crypto‑related operations |
droidrun/macro/cli.py | 8 functions | 11 | Macro recording/replay entry point (replay reaches 45 functions) |
Import‑cycle members (19 modules) include droidrun/tools/__init__.py, droidrun/agent/droid/__init__.py, droidrun/agent/utils/tracing_setup.py; they mutually import each other, raising the risk of ImportError at runtime and complicating static analysis. Nesting depth of 8 in droidrun/cli/main.py, droidrun/agent/utils/prompt_resolver.py, droidrun/config_manager/path_resolver.py makes control flow hard to follow. Twenty‑two repeated 6‑line blocks appear across 16 files (e.g., droidrun/__init__.py, droidrun/agent/codeact/codeact_agent.py, droidrun/agent/scripter/scripter_agent.py), violating DRY. Three open(...) calls without context managers in droidrun/agent/utils/chat_utils.py, droidrun/agent/trajectory/writer.py, droidrun/agent/external/mai_ui.py risk file‑handle leaks. Nine files have broad except clauses that swallow errors indiscriminately (e.g., droidrun/agent/trajectory/writer.py, droidrun/cli/main.py, droidrun/tools/android/portal_client.py).
How It Is Wired
Execution starts at run in droidrun/cli/main.py:439 – the sole entry point that reaches 103 functions. From there the call graph fans out:
run→get_state(network viarequests.get) → device‑state fetch.run→wrapper→take_screenshot(network viarequests.get).run→execute(viaexecute_agent) →_execute_action→swipe(networkrequests.post).run→connect→_ensure_connected(called 18 times) →AdbTools(6 callers).
The execute path in droidrun/agent/executor/executor_agent.py:227 reaches 38 functions, calls a model for inference, and may write trajectory files. replay in droidrun/macro/cli.py:65 follows _replay_with_device → _replay_async → replay_macro → replay_action → start_app, again using requests.post for network‑bounded device commands. The setup function (cli/main.py:667) downloads a portal APK via download_versioned_portal_apk (requests.get, filesystem os.unlink).
The import‑cycle (19 modules) means any change to a shared type in droidrun/tools/__init__.py can ripple through droidrun/agent/droid/__init__.py and droidrun/agent/utils/tracing_setup.py, requiring careful refactor to break the cycle (extract shared types, invert a dependency, or defer imports). The config_manager hub (13 dependents) is a high‑blast‑radius point; modifications there affect config loading, prompt paths, and manager/executor initialization.
How To Use It
Setup The project uses pyproject.toml and setup.py; install with pip pulling optional extras:
pip install 'droidrun[google,anthropic,openai,deepseek,ollama,dev]'
No lockfile is present (pyproject.toml declares dependencies without a uv.lock/requirements.txt lock), so reproductions may vary. The Dockerfile uses python:3.12-slim (mutable tag) – pin it with a digest for reproducible builds.
Configuration Create a config file (example: droidrun/config_example.yaml) and set environment variables for API keys (OPENAI_API_KEY, ANTHROPIC_API_KEY, etc.). The config manager reads paths from droidrun/config_manager/config_manager.py and system prompts from Jinja2 templates under droidrun/config/prompts/.
Running it Invoke the CLI entry point:
python -m droidrun.cli run --device <serial> --prompt "book a hotel in SF"
or, for macro replay:
python -m droidrun.macro.cli replay --macro-file path.json
The run command (defined in droidrun/cli/main.py:439) is the primary way to start an agent session; it will load the device, fetch state, and begin the LLM‑driven action loop.
Real‑World Use
A user wants to automate a weekly habit‑tracking flow in a language‑learning app. They write a short natural‑language prompt (“open the app, tap the “streak‑save” button, take a screenshot, and log the result”). The CLI invokes run, which fetches the current UI state via adb.py, sends a model request through executor_agent.py, and the agent executes tap_by_coordinates and take_screenshot. The resulting trajectory is persisted by writer.py. If the app UI changes, the agent can retry or the user can adjust the prompt; the telemetry processor records each step for later analysis.
Code Health & Issues
- HIGH – Pin third‑party GitHub Actions to commit SHAs (
.github/workflows/black.yml,docker.yml,publish.yml,claude.yml). Tags can move; SHAs lock the exact code running with secrets. - HIGH – Add a test suite; the repo has 104 source files and no test files. Any regression reaches production undetected.
- MEDIUM – Declare least‑privilege
permissions: contents: readfor theGITHUB_TOKENin workflows that reference secrets (.github/workflows/black.yml). - MEDIUM – Enable Dependabot or Renovate; currently no update bot is configured.
- MEDIUM – Pin the container base image by digest in the
Dockerfile(python:3.12-slim→python:3.12-slim@sha256<digest>). - MEDIUM – Gate pull requests on a dependency vulnerability scan (add
dependency-review-actionorosv-scanner). - LOW – Set
timeout-minuteson workflow jobs (currently 4 jobs have no timeout).
Additional structural findings from static analysis: 19 modules in import cycles, 22 duplicated 6‑line blocks across 16 files, 3 open() calls without context managers, 9 broad except blocks, max indentation depth 8 in three files, and 5 oversized files (>600 lines) that concentrate logic and ripple widely.
The Bottom Line
DroidRun provides a capable LLM‑driven mobile‑automation layer with support for multiple providers and a rich CLI, but the code base suffers from import cycles, duplicated logic, missing tests, and unpinned dependencies that raise maintenance cost and operational risk. Teams comfortable with Python automation and willing to invest in test coverage and CI hygiene will find it useful; others may need to allocate refactor time to stabilize the import graph and add reproducible builds before production use.