jev-android

September 20, 2026 · View on GitHub

English | 简体中文

A Kotlin UI agent SDK for Android projects. TypeSafe Jev or DeepSeek selects actions from the actual controls on the current screen, and an Android accessibility service executes them. Each backend can drive a task independently: use a DeepSeek API key without a Jev account, or use a TypeSafe key for Jev. The repository includes a sample app and a local test screen that requires no app account.

The API is still unstable. The project is not published to Maven Central; the dependency coordinates below work only with the local Maven repository generated by this project. Version 0.3.7 adds task and decision fields, progress context, and new sealed AgentEvent variants. Rebuild the core, SDK, sample, and host together; update exhaustive event handlers for Evaluated and Replanning. Do not replace a single artifact in an existing binary integration. The optional Element.resourceId field introduced in 0.3.5 also changed its constructor's binary signature.

Features and limitations

  • Builds a dynamic action table from accessible controls. Jev selects an operation and its targets through multiple questions in one request; DeepSeek chooses an enumerated action through a strict select_action function schema. Both use the same execution loop and guards.
  • Adds bounded descendant text to DeepSeek's actionable container descriptions, so a clickable card can include its visible title without turning a non-actionable text child into a clickable target.
  • Exposes an optional Android view resource ID in both providers' element descriptions for control identification. Actions still target the observed element ID; a resource ID does not add action support or bypass fresh-screen checks.
  • Records Element.selected separately from checked, so selecting a non-checkable tab or row is visible to the planner and counts as a semantic page change.
  • Supports clicking, long-pressing accessible controls, replacing text field contents, scrolling forward or backward, going back, launching allowed apps, waiting, and reporting completion or a blocked task.
  • Operation.LONG_CLICK invokes a control's advertised Android ACTION_LONG_CLICK action. Operation.LONG_PRESS holds a touch at the center of a currently observed, visible, actionable control. The host controls the hold duration through Task.longPressDurationMillis (default 2,000 ms; allowed range 500–5,000 ms); the model cannot supply coordinates or a duration.
  • Neither backend generates arbitrary input text in this SDK. Task.textValues supplies named, exact candidate values for the selected model to choose from. Only the selected provider's API key is required.
  • Reads the screen again and compares fingerprints before execution. If the screen changed before any action was submitted, the agent observes again and asks the provider for a new decision, up to three consecutive refreshes. Each refresh consumes a decision step and remains within the task's step and time budgets. It never replays the old decision on the changed interface.
  • Waits for the stop control to be laid out before the first observation. Gesture geometry is checked separately, so overlay layout does not invalidate native node actions while a model request is in flight.
  • Waits up to 10 seconds for a launched app to become the foreground app before reporting an accepted launch, preventing a second launch decision while Android is still switching apps.
  • Reads back text after input to verify the complete value. An accepted action is followed by observation of its visible effect; rejected or uncertain actions stop the task and are not automatically replayed.
  • Gives both providers the current page, the last ten observed action effects, and exact action combinations already accepted without visible change on that page. The next decision is based on this evidence; the SDK does not insert a scripted fallback.
  • Includes package allowlists, step and time budgets, a minimum confidence threshold, duplicate-action guards, and a host-defined action policy.
  • Coroutine cancellation cancels in-flight HTTP requests. An accessibility overlay button stops future actions. Actions already submitted to Android cannot be undone; a submitted hold may continue until its configured duration expires and the touch is released.
  • Reports whether a service cancellation followed a Stop request, an Android accessibility interruption, or service disconnection. A cancelled task does not establish whether an in-flight gesture reached the target app.
  • DONE is only the model's claim of completion. A task returns VERIFIED only when an OutcomeVerifier is supplied and passes; otherwise, it returns UNVERIFIED.

This release does not support screenshot-based vision, guessed coordinates, arbitrary gestures beyond the bounded press-and-hold action, general WebView or Canvas recognition, password entry, text generation, lock-screen operation, or unattended long-running background tasks. Xiaomi and HyperOS compatibility has not been verified on a physical device. The host defines which settings changes, payments, messages, and other actions are allowed. The default gate permits valid actions inside allowlisted apps; it does not automatically identify every sensitive control.

Modules

ModulePurpose
corePlatform-independent task models, decision interfaces, execution loop, verification, and events
sdkJev and DeepSeek HTTP/JSON integrations, accessibility runtime, service, and stop button
sampleProvider, model, and scenario selection, editable task presets, API configuration, permission settings shortcut, result logs, and local test screen

The execution loop reads controls, builds valid choices, asks the selected provider to select an action, applies the host policy, checks that the screen is current, executes the action, and observes the result.

Observe effects before the next decision

accepted=true means Android accepted an action, not that the action achieved its goal. After acceptance, the agent observes until the page changes or the settling wait budget is exhausted. Task.settleTimeoutMillis defaults to 1,500 ms and accepts 0–10,000 ms; 0 still performs an immediate observation. WAIT uses a separate 600 ms observation window. These budgets count settling delays, not time spent inside the runtime's observation call. Progress compares the package, elements, and available apps, rather than geometry fingerprints. UI_CHANGED means that observed page data changed; NO_VISIBLE_CHANGE means no change was detected within that window. Neither result proves business success or failure.

If an accepted non-WAIT action leaves the page unchanged, its exact (operation, target, textKey) combination is excluded from repetition on that page. A changed page clears the exclusions. The current page and up to ten action-effect records go to Jev or DeepSeek so the provider can choose another supported action, wait for a delayed result, or report completion or blockage. DeepSeek also supplies a brief Decision.summary and Decision.expectedChange; these describe its intent and expected visible result, not execution instructions or evidence of success.

There is no fixed five-cycle unchanged-page failure. WAIT is not itself a failed action, and task deadlines and maxSteps still bound the run. A duplicate proposal or one below the confidence threshold permits at most two consecutive replans without dispatch, then returns BLOCKED if no acceptable proposal is produced. Replanning does not retry a rejected or uncertain mutation; those results remain terminal. The Bilibili preset's optional local one-hold policy and outcome verifier still apply independently of this generic loop.

On 2026-09-21, version 0.3.7 passed live DeepSeek Save-text verification and reached a TESTV video detail page after choosing WAIT during Bilibili loading. The search preset ended UNVERIFIED because it has no outcome verifier. Recovery from a no-effect click was validated with a deterministic provider on the phone; that recovery path has not yet been demonstrated with the live model. See validation details.

Choose a backend

BackendSDK providerRequired keyDefault model
Jev (TypeSafe)JevProviderTypeSafe API keyjev-latest
DeepSeekDeepSeekProviderDeepSeek API keydeepseek-flash

DeepSeek calls https://api.deepseek.com/beta/chat/completions with thinking disabled and a forced, strict select_action function. Each request enumerates the currently allowed action_id values; each ID maps locally to a specific operation and target. The model supplies action_id, text_key, confidence, summary, and expected_change, rather than generating operation names or raw UI target IDs. The SDK accepts exactly one call to this named function and validates its arguments locally before considering a UI action. The function call represents a decision; it does not execute a remote tool. See the official DeepSeek strict tool-call guide. This beta endpoint worked with version 0.3.4 on one DeepSeek account and a Samsung phone on 2026-09-21; that result does not establish availability for every account or model. See validation details.

The text_key argument is a string enum containing the host's nonblank input keys and an empty string for no input. The SDK checks that a selected key is compatible with the chosen action; an input action cannot use an arbitrary text value. The internal empty-string marker becomes null in the resulting Decision.

DeepSeek has a 1,024-token response budget. An empty, truncated, or invalid decision can trigger one correction request before any UI action is submitted. The correction uses the same original snapshot and task context plus a fixed rejection reason; it does not include the malformed response. The corrected decision must pass the same strict validation, host policy, and fresh-screen checks. HTTP/network failures, refusals, and content filtering are not retried. Both providers accept a custom model value; choose a model available to your account that supports the selected provider's request format.

The shared confidence threshold has different meanings for the two backends. Jev provides probability distributions over the supplied choices; DeepSeek must return a self-reported confidence value in its function arguments. DeepSeek's value is not calibrated or directly comparable to Jev's probabilities, and neither is proof that an action is correct. Use host policies and outcome verification for decisions that require stronger assurance.

Build

Use JDK 17 or 21 and Android SDK Platform 36. The project includes the Gradle 8.13 wrapper. Open the root directory in Android Studio, or configure ANDROID_HOME or an untracked local.properties file:

sdk.dir=/your/path/to/Android/Sdk

macOS (Apple silicon and Intel)

The Mac is the development machine: build the SDK and install the sample on an Android phone or emulator. The agent runs on Android; it does not control macOS or iOS.

1. Install the prerequisites. Install Android Studio for your Mac's architecture and complete its setup wizard. In SDK Manager, install Android SDK Platform 36, Android SDK Platform-Tools, and Android SDK Build-Tools 35.0.0 (enable Show Package Details to select that version). For an emulator, also install Android Emulator and a system image matching your Mac: ARM64 for Apple silicon or x86_64 for Intel. Use an image with API 26 or later.

2. Clone the project. Run these commands in Terminal:

git clone https://github.com/dougsong/jev-android.git
cd jev-android
chmod +x gradlew

If macOS prompts you to install Command Line Tools when running git, complete that installation first. You do not need a separate Gradle installation; use the project's wrapper.

3. Configure Java and the Android SDK. With Android Studio installed in /Applications and the SDK in its default location:

export JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home"
export ANDROID_HOME="$HOME/Library/Android/sdk"
export PATH="$JAVA_HOME/bin:$ANDROID_HOME/platform-tools:$PATH"
java -version
./gradlew --version

Use JDK 17 or 21 for this project. Check the version printed above rather than assuming the bundled JDK version. If Android Studio bundles a different version, install JDK 17 for your Mac's architecture and set JAVA_HOME to that installation; a JDK registered with macOS can be selected with export JAVA_HOME="$(/usr/libexec/java_home -v 17)". After switching JDKs, repeat the PATH export and version checks above. When building from Android Studio, select the same JDK under Settings > Build, Execution, Deployment > Build Tools > Gradle > Gradle JDK.

Adjust ANDROID_HOME to the SDK location shown in SDK Manager if you changed it. These exports apply to the current terminal; add the appropriate lines to ~/.zshrc to reuse them. If you copied a checkout from Windows, remove or update its untracked local.properties so an old Windows sdk.dir does not override the Mac SDK location.

4. Build and run offline checks. From the project root:

./gradlew :core:test :sdk:testDebugUnitTest :sample:testDebugUnitTest :sdk:assembleRelease :sample:assembleDebug
./gradlew :sdk:lintDebug :sample:lintDebug

The first build downloads Gradle and dependencies. Alternatively, open the project in Android Studio, let Gradle sync finish, and run the sample configuration on a selected Android device. The output paths are listed below.

5. Install and try the sample. For a physical phone, enable Developer options and USB debugging, connect it with a data-capable USB cable, unlock it, and accept its debugging authorization prompt. macOS does not need an OEM USB driver for ADB. Alternatively, start an emulator from Android Studio's Device Manager. With exactly one target connected:

adb devices
adb install -r sample/build/outputs/apk/debug/sample-debug.apk
adb shell am start -n io.github.jevandroid.sample/.MainActivity

The target must appear as device, not unauthorized or offline. When several devices are connected, add -s YOUR_DEVICE_SERIAL immediately after adb in each install or launch command. In the app, choose Jev (TypeSafe) or DeepSeek, enter the matching API key, and review the selected provider's data disclosure. Keep the default model or enter a compatible custom model, enable the accessibility service manually, select Built-in: save text, then tap Run selected task. Keep the device unlocked. No key belongs in a shell command or a committed file. After installation, the sample uses its own Internet connection and does not need to stay connected to the Mac.

Optional instrumented fixture tests on a dedicated emulator or test device:

./gradlew :sample:connectedDebugAndroidTest

This task installs test APKs, temporarily enables the accessibility service, and restores its previous settings afterward. It uses a deterministic decision provider and does not call Jev or DeepSeek. Connect only the intended test device, since Gradle may run connected tests on multiple devices.

6. Integrate the SDK into another project. Follow the source-module instructions below, or generate the local Maven repository on your Mac:

./gradlew :core:publishCorePublicationToLocalBuildRepository :sdk:publishReleasePublicationToLocalBuildRepository

Copy build/repository to the host project's vendor/jev-maven, then use the Maven dependency and service registration shown below. The Windows staging script is not needed on macOS. These macOS instructions have been checked against the project configuration and platform documentation; a build on an actual Mac has not yet been performed.

Platform references: AGP requirements, Gradle Java compatibility, Android environment variables, device setup, and emulator architecture.

Windows

.\gradlew.bat :core:test :sdk:testDebugUnitTest :sample:testDebugUnitTest :sdk:assembleRelease :sample:assembleDebug
.\gradlew.bat :sdk:lintDebug :sample:lintDebug

Use ./gradlew on Linux. The project sets android.overridePathCheck=true to allow non-ASCII Windows paths. Move the project to an ASCII-only path if third-party tools have path-related failures.

During local validation, a Windows project path containing Chinese characters caused ClassNotFoundException in Gradle test workers. The included script builds in a new temporary ASCII-only directory and copies artifacts and reports back:

.\scripts\build-windows.ps1 -JavaHome 'C:\Program Files\Android\Android Studio\jbr'

Adjust the JDK path for your installation. The script preserves the original project files. The Android SDK location must still be available through ANDROID_HOME or local.properties.

Build outputs

  • sample/build/outputs/apk/debug/sample-debug.apk
  • sdk/build/outputs/aar/sdk-release.aar
  • core/build/libs/core-0.3.7.jar

The AAR does not bundle all dependencies. The SDK also depends on the core module, coroutines, OkHttp, and Gson. Use one of the source-module or Maven integration options below.

Integrate with an Android project

Option A: Source modules

Copy the core and sdk modules into your project, include them in the host project's settings.gradle.kts, and use compatible Kotlin and Android Gradle Plugin versions. Add these dependencies to the host app:

dependencies {
    implementation(project(":sdk"))
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")
}

Option B: Local Maven repository

.\gradlew.bat :core:publishCorePublicationToLocalBuildRepository :sdk:publishReleasePublicationToLocalBuildRepository

Copy the entire generated build/repository directory into the host project's vendor/jev-maven directory. Add this repository to dependencyResolutionManagement.repositories in the host settings:

maven { url = uri("vendor/jev-maven") }

Add these dependencies to the host app:

implementation("io.github.jevandroid:jev-android:0.3.7")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0")

Register the service

The host requires minSdk >= 26 and compileSdk >= 36. The SDK manifest contributes the Internet permission and launcher queries. The host must explicitly register the service, and the user must enable it.

Add the following inside <application> in the app manifest:

<service
    android:name="io.github.jevandroid.JevAccessibilityService"
    android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"
    android:exported="true"
    android:label="Jev UI automation">
    <intent-filter>
        <action android:name="android.accessibilityservice.AccessibilityService" />
    </intent-filter>
    <meta-data
        android:name="android.accessibilityservice"
        android:resource="@xml/jev_accessibility" />
</service>

Copy the sample's res/xml/jev_accessibility.xml and its description string from strings.xml into the host app. For timed LONG_PRESS actions, the XML must include android:canPerformGestures="true" and android:accessibilityFlags="flagReportViewIds|flagRetrieveInteractiveWindows". Native LONG_CLICK uses the control's accessibility action. Android magnification and touch exploration can affect gesture delivery. Explain that visible screen text, the task, and input candidates will be sent to the selected provider (TypeSafe or DeepSeek), then let the user enable the service through Settings.ACTION_ACCESSIBILITY_SETTINGS. The service should run in the same process as the host app.

Start a task

import io.github.jevandroid.JevAccessibilityService
import io.github.jevandroid.JevProvider
import io.github.jevandroid.core.*

// Call on the main thread, for example from a button callback.
// Obtain the key from the host configuration; do not embed it in source code.
val service = JevAccessibilityService.connected.value
    ?: error("Enable the accessibility service first")

val job = service.start(
    task = Task(
        goal = "Open Bilibili, search for Xiaomi phone reviews, and stop on the search results screen",
        allowedPackages = setOf("tv.danmaku.bili"),
        textValues = mapOf("search_query" to "Xiaomi phone reviews"),
        maxSteps = 20,
    ),
    provider = JevProvider(apiKey = yourTypeSafeKey),
    onEvent = { event -> /* Update progress without logging input contents. */ },
    onError = { error -> /* Show the error and allow manual takeover. */ },
)

// Invoke from a separate stop button. Alternatively, call service.stop().
job.cancel()

This example demonstrates the API; it is not a verified Bilibili workflow. Put cancellation in a separate stop-button handler in a real app.

To run the same task with DeepSeek, import DeepSeekProvider and pass it as the provider. No TypeSafe key or Jev call is needed:

import io.github.jevandroid.DeepSeekProvider
import io.github.jevandroid.JevAccessibilityService
import io.github.jevandroid.core.Task

val service = JevAccessibilityService.connected.value
    ?: error("Enable the accessibility service first")

val job = service.start(
    task = Task(
        goal = "Open Bilibili, search for Xiaomi phone reviews, and stop on the search results screen",
        allowedPackages = setOf("tv.danmaku.bili"),
        textValues = mapOf("search_query" to "Xiaomi phone reviews"),
        maxSteps = 20,
    ),
    provider = DeepSeekProvider(
        apiKey = yourDeepSeekKey,
        model = "deepseek-flash", // Optional; this is the default.
    ),
    onEvent = { event -> /* Update progress without logging input contents. */ },
    onError = { error -> /* Show the error and allow manual takeover. */ },
)

The main-thread, cancellation, and verification requirements are the same for both providers. This code demonstrates the API. On 2026-09-21, a live DeepSeek run of Built-in: save text returned VERIFIED and displayed Saved: Hello Jev. A separate run of the Bilibili testv search preset reached a TESTV video detail page; its completion status was UNVERIFIED because the preset has no independent outcome verifier. See validation details.

An OutcomeVerifier receives a fresh UiSnapshot and can check the specific business result. The sample checks the saved value or long-press result displayed on its test screen. Its Bilibili triple-action preset also checks all three selected controls on the same observed video detail page. If the verifier returns false, the result is UNVERIFIED.

An ActionGate can connect to the host's confirmation UI or business rules. Returning false stops the task. If the screen changes while confirmation is pending, the runtime rejects the old decision before dispatch; a permitted refresh obtains a new decision and applies the gate again. A provider's confidence alone should not authorize sensitive actions.

Implement DecisionProvider to use another model, or implement DeviceRuntime to test with a simulated device. When using JevAgent directly, the host owns coroutine lifetimes, error handling, stop controls, and exclusive access to the device. JevAccessibilityService.start is usually simpler and permits only one active task per service.

Try the sample on an Android phone

  1. Install the sample debug APK, choose Jev (TypeSafe) or DeepSeek, and enter that provider's API key. The model field defaults to jev-latest or deepseek-flash; compatible custom models are allowed. Review the disclosure for the selected provider. DeepSeek mode needs no TypeSafe account.
  2. Enable the accessibility service in system settings. Menus and restrictions vary between Android versions and vendors, including Xiaomi HyperOS and Samsung One UI; follow the controls available on your device.
  3. Select Built-in: save text, then tap Run selected task. Keep the device unlocked. The task should enter Hello Jev, click Save, and verify that Saved: Hello Jev appears. Use Built-in: long press to try a timed press-and-hold on the test control.
  4. Use Stop Jev in the upper-right corner to stop execution. Return to the sample's main screen to inspect the result. Only VERIFIED indicates that the local outcome check passed.
  5. Choose another scenario or Custom task, review its goal, allowed packages, input candidates, and hold duration, then tap Run selected task. The overlay button can cover controls near the top of the screen; avoid controls under it in this release.

Automation uses the current foreground interface. You cannot simultaneously use other apps manually. System permissions, sign-in, and inaccessible controls require manual handling.

On a Samsung SM-F9460 running Android 16, the system component com.samsung.android.onetouch intercepted two-second holds during local fixture testing. The tests passed with its long-press option temporarily disabled, and that setting was restored afterward; see VALIDATION.md. If a hold opens a system feature instead of the target control, review that feature's settings and disable its long-press trigger or exclude the target app if an exclusion is available. The SDK does not change these system settings. It stops when a hold moves the foreground outside the original app.

Available scenarios

Selecting a scenario fills the task, package allowlist, input candidates, and hold duration; these fields remain editable. Selection does not start automation. Run selected task opens the local test screen for built-in scenarios or starts the selected external-app task. Provider selection and API keys are independent of the scenario.

Hold duration (milliseconds) accepts whole milliseconds from 500 to 5,000. The Bilibili triple-action preset defaults to 4,000 ms; the built-in hold fixture and SDK default remain 2,000 ms. The sample passes the selected value into Task.longPressDurationMillis before requesting a model decision. Change this field to adjust the actual touch duration; changing only the goal's wording does not configure a longer gesture. The triple-action log records the requested duration, for example requested=4000 ms; this is the configured duration, not a measurement of uninterrupted touch delivery.

ScenarioTargetBehavior
Built-in: save textSample appEnter Hello Jev, save it, and check the displayed result.
Built-in: long pressSample appHold the test control for two seconds by default and check its visible result.
Bilibili: search testvtv.danmaku.biliSearch for testv, skip ads and live streams, and open the first ordinary video.
Bilibili: search + triple actiontv.danmaku.biliSearch for testv, open the first ordinary video, then hold Like once for four seconds by default to attempt the combined like, coin, and favorite action.
Custom taskUser configurationStart with blank fields and supply a goal, allowed packages, and exact input candidates.

The Bilibili presets target the mainland Android app. The search preset has no independent outcome verifier, so its completion claim returns UNVERIFIED. The triple-action preset can affect the signed-in account and spend Bilibili coins. On 2026-09-21, version 0.3.6 completed a live DeepSeek run on a Samsung SM-F9460: one hold configured for 4,000 ms was followed by all three controls reporting selected, and the local verifier returned VERIFIED. Independent UI inspection confirmed the three selected controls. The Samsung long-press setting remained enabled during this live run. This verifies the visible result for that run, not general compatibility or backend coin accounting; see validation details.

The triple-action preset includes a local policy beyond its model instructions. It recognizes the exact tv.danmaku.bili:id/frame_like, frame_coin, and frame_fav controls on a video detail page, permits one timed hold on Like only when all three are unchecked, and rejects separate like, coin, or favorite actions. A hold consumes the allowance when an execution result is received; a stale decision rejected before dispatch does not consume it. After the hold, the policy checks up to four observations, with waits between them and no further interaction. Missing controls, existing selections, or an unsupported layout stop the attempt instead of guessing a replacement target.

Its outcome verifier requires all three controls to report checked=true and the same observed video title as before the hold. This verifies the visible final state, not the first search result's ordering, a new coin transaction, or the backend account record. The timed hold uses the observed control's center, and Android accepting the gesture is not proof that Bilibili completed the combined action. If the controls remain unselected, the preset cannot report VERIFIED.

Data handling and error behavior

  • Only visible controls from allowlisted apps are read. For other screens, the snapshot contains only the foreground package name and the list of apps the task may launch.
  • Password nodes and their subtrees are skipped. This is not comprehensive personal-data redaction: other visible text in allowed apps may contain private information. Screen information, including exposed view resource IDs, the task goal, input candidates, task history, and up to ten observed action-effect records with target details and decision summaries are sent to the selected provider.
  • No screenshots are uploaded. Each provider sends its API key only to its own fixed endpoint: Jev uses https://api.typesafe.ai/v1/systemone; DeepSeek uses https://api.deepseek.com/beta/chat/completions. Redirects and automatic connection retries are disabled. Selecting one provider does not call the other provider.
  • A snapshot contains at most 220 elements, and node traversal is bounded. Long screens require scrolling. Controls omitted by truncation are not offered to either model.
  • The sample keeps separate keys for Jev and DeepSeek only in memory; switching providers does not reuse the other provider's key. Keys are not persisted or backed up. Screenshots are disabled on the configuration screen. The SDK does not manage host credentials. Apps distributed to other users should use user-supplied keys or a controlled backend.
  • HTTP, protocol, and runtime exceptions reach onError. DeepSeek response rejections include a sanitized reason code (such as EMPTY_CONTENT, TRUNCATED, or INVALID_TARGET) and attempt count, without raw response content or a nested parser error. One permitted decision correction is separate from HTTP retries and does not replay a UI action. Cancellation follows coroutine cancellation semantics and is not returned as success. The service's lastStopReason provides a fixed diagnostic for Stop requests, Android interruptions, and disconnection; it contains no task or screen content.
  • Action rejection, uncertain execution, host-gate rejection, an explicit provider blockage, or exhausting the three permitted consecutive stale-screen refreshes returns BLOCKED. A duplicate or low-confidence proposal is discarded before dispatch and permits at most two consecutive page-based replans. All refreshes and replans consume the decision budget and remain inside the task deadline. A stale-screen refresh requires proof that no action was submitted; an accepted action resets its consecutive-refresh counter. A rejected or uncertain mutation is never automatically resubmitted.
  • A model's completion claim and Android's performAction=true are not evidence that the intended business result was achieved.

Tests and future work

Offline tests cover cancellation, timeouts, step limits, allowlists, invalid targets, input-value restrictions, outcome verification, low confidence, stopping without resubmitting a failed action, and Jev response distribution and branch validation. DeepSeek tests cover the strict function schema, action-ID mapping, invalid function calls or arguments, malformed or truncated responses, bounded decision correction, sanitized rejection reasons, HTTP status handling, response-size limits, and transport cancellation. Long-press tests check action eligibility and provider decisions; sample tests cover scenario configuration and provider-key isolation when switching backends. Provider responses and HTTP behavior are tested with offline fixtures and mocks, not live API calls. Android builds and lint do not establish real-device task success. See VALIDATION.md for the current validation status.

The :sample:connectedDebugAndroidTest task uses a deterministic decision provider on a connected emulator or test device. It checks real accessibility reads, text entry, clicking, long-pressing, result verification, app launch completion, stale-screen refreshes, and allowlist enforcement. Tests temporarily enable the service and restore the previous accessibility settings afterward. Run them only on a dedicated test device. These automated results do not call Jev or DeepSeek or establish live API availability or real-model success rates.

Custom providers can implement ContextualDecisionProvider to receive DecisionContext with recent observed effects, excluded exact action combinations, and a replan reason. Existing DecisionProvider implementations still work through the original three-argument method and remain subject to the agent's duplicate, confidence, policy, and freshness guards, but do not receive the additional context. Custom runtimes can implement DetailedDeviceRuntime to distinguish StaleBeforeDispatch from a rejected or uncertain operation. Existing DeviceRuntime implementations retain their Boolean API; false remains terminal because the agent cannot prove that no action was submitted.

Initial validation priorities include live model calls, Chinese-language target interfaces, and physical Xiaomi devices. Future work may add screenshot assistance, optional text generation, more input controls, a draggable stop button, and provider performance benchmarks. Set a model explicitly with JevProvider(apiKey = key, model = "...") or DeepSeekProvider(apiKey = key, model = "..."); availability and behavior depend on the provider. The default jev-latest follows server-side updates.

References and license

The dynamic action table and multiple questions per request were inspired by browser-use/jev-ultrafast. The Android implementation was written independently. This is not an official Browser Use, TypeSafe, or DeepSeek SDK.

This project uses the MIT license. The model service, Android platform, and dependencies have their own terms. Personal installation and app-store distribution have different requirements; review the relevant accessibility automation policies before submitting an app to a store.