@revyl/verify 0.3.2 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/README.md +60 -23
- package/dist/agent/image.d.ts +7 -0
- package/dist/agent/image.js +29 -0
- package/dist/agent/loop.d.ts +97 -0
- package/dist/agent/loop.js +215 -0
- package/dist/agent/model.d.ts +91 -0
- package/dist/agent/model.js +255 -0
- package/dist/agent/providers.d.ts +23 -0
- package/dist/agent/providers.js +58 -0
- package/dist/api/client.d.ts +1 -0
- package/dist/api/client.js +2 -2
- package/dist/auth.js +2 -2
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +34 -9
- package/dist/cli/login.js +7 -7
- package/dist/cli/main.js +84 -29
- package/dist/computer-session.d.ts +9 -0
- package/dist/computer-session.js +27 -0
- package/dist/errors.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +2 -0
- package/dist/open.d.ts +6 -0
- package/dist/open.js +6 -1
- package/dist/session.d.ts +77 -2
- package/dist/session.js +294 -12
- package/docs/README.md +4 -3
- package/docs/agents.md +34 -5
- package/docs/api.md +99 -13
- package/docs/computers.md +9 -11
- package/docs/configuration.md +12 -8
- package/docs/errors.md +8 -6
- package/docs/getting-started.md +22 -17
- package/docs/llms.txt +11 -6
- package/docs/mobile.md +48 -0
- package/docs/own-model.md +216 -0
- package/docs/web.md +58 -1
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,77 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.1 (unreleased)
|
|
4
|
+
|
|
5
|
+
- **Breaking for iOS exact actions (early access):** coordinates are now screenshot pixels on every platform. On iOS, `tap`, `type`, `swipe`'s `from`, `locate`, `screenSize` and the points `tap` and `type` return used the screen's points (440 × 956 on an iPhone whose screenshots are 1320 × 2868); they now use the screenshot's pixels, and Verify converts them to points for you. A script that passed iOS points must pass screenshot pixels instead: multiply by the screenshot's width over 0.4.0's `screenSize()` width (3 on that iPhone). Android and Web coordinates are unchanged, because their screenshots already match the screen.
|
|
6
|
+
- A point outside the screenshot is refused with `INVALID_ARGUMENT` before any action is sent, and the message names the valid range. The first call in a session that takes or returns a point reads the screen size once (on iOS, plus one screenshot), and later calls reuse it. Before refusing a point, Verify reads the size again, so a rotated device or a resized Web viewport doesn't get a valid point refused. On Android and Web, if the size can't be read, the action is sent as in 0.4.0; on iOS the call fails, because the scale is needed. Calls by description send the same requests as 0.4.0 on Android and Web.
|
|
7
|
+
- `locate()` keeps an off-screen match outside `screenSize()` instead of moving it to the edge, so you can tell it is off screen, and an action at it is refused.
|
|
8
|
+
- `instruction()` and `validation()` on your own model are unchanged: the model still sees screenshots, and Verify still maps its answers to the device.
|
|
9
|
+
- Docs: one coordinate rule in the same words for TypeScript, Python and Go, in the README, `api.md`, the web and mobile guides, `agents.md` and `llms.txt`. Web coordinates are screenshot pixels, which are CSS pixels at the default device scale factor of 1.
|
|
10
|
+
|
|
11
|
+
## Go 0.3.1 (unreleased)
|
|
12
|
+
|
|
13
|
+
Published as the `go/v0.3.1` tag.
|
|
14
|
+
|
|
15
|
+
- **Breaking for iOS exact actions (early access):** coordinates are now screenshot pixels on every platform, as in TypeScript 0.4.1. On iOS, `Tap`, `Type`, `Swipe`'s `From`, `Locate`, `ScreenSize` and the points `Tap` and `Type` return used the screen's points; they now use the screenshot's pixels, and Verify converts them to points for you. Android and Web coordinates are unchanged.
|
|
16
|
+
- A point outside the screenshot returns `ErrInvalidArgument` before any action is sent, naming the valid range, after Verify reads the size again in case the screen rotated. The screen size is read once per session. On Android and Web a failed size read skips the check and sends the action as before; on iOS it returns the error.
|
|
17
|
+
- README: the same coordinate rule as TypeScript and Python.
|
|
18
|
+
|
|
19
|
+
## Python 0.2.1 (unreleased)
|
|
20
|
+
|
|
21
|
+
- **Breaking for iOS exact actions (early access):** coordinates are now screenshot pixels on every platform, as in TypeScript 0.4.1. On iOS, `tap`, `type`, `swipe`'s `from_`, `locate`, `screen_size` and the points `tap` and `type` return used the screen's points; they now use the screenshot's pixels, and Verify converts them to points for you. Android and Web coordinates are unchanged. `AsyncSession` behaves the same.
|
|
22
|
+
- A point outside the screenshot raises `INVALID_ARGUMENT` before any action is sent, naming the valid range, after Verify reads the size again in case the screen rotated. The screen size is read once per session. On Android and Web a failed size read skips the check and sends the action as before; on iOS it raises.
|
|
23
|
+
- README: the same coordinate rule as TypeScript and Go.
|
|
24
|
+
|
|
25
|
+
## 0.4.0 (2026-11-11)
|
|
26
|
+
|
|
27
|
+
- Early access: run `instruction()` and `validation()` on your own model. `open('ios', { model: 'anthropic/claude-sonnet-5-5' })` runs every step on Claude with the key from `ANTHROPIC_API_KEY`; a step's `{ model }` overrides it. Built-in providers: Anthropic, OpenAI, Google, OpenRouter, Groq, Cerebras, xAI and Ollama, each reading its usual key variable; `anthropic/` and `openai/` also honour `ANTHROPIC_BASE_URL` and `OPENAI_BASE_URL` for proxies and gateways. `{ id, baseURL, apiKey, headers }` reaches any OpenAI-compatible endpoint, with the key taken only from `apiKey`. Your model names elements and Revyl's grounding model finds them. `StepResult.model` reports turns and tokens. Revyl's agent remains the default.
|
|
28
|
+
- Custom agents: `localAgent({ model, grounding, maxTurns, modelTimeout })` with the exported `anthropic()` and `openaiCompatible()` adapters, or your own `ModelAdapter`, passed as `{ agent }`.
|
|
29
|
+
- New errors `MODEL_KEY_MISSING` (thrown before any session starts) and `MODEL_ERROR`, whose messages name the next step for a rejected key, an unknown model, a rate limit, or a model without image input or tool calls.
|
|
30
|
+
- Early access: exact device actions on Web, iOS and Android: `tap`, `type`, `swipe`, `pressKey`, `back` (Web and Android), `openUrl`, `launchApp` (iOS and Android), `locate` and `screenSize`, by device coordinates or a described target. A call a platform doesn't support throws `NOT_AVAILABLE_ON_PLATFORM` naming the alternative. `swipe` directions are finger motion on every platform, so `'up'` scrolls content down; on Web, Verify sends Revyl the matching page direction.
|
|
31
|
+
- Fix healthy sessions failing with `SESSION_ENDED` right after a step was accepted. Revyl can answer the first `step_status` polls with 404 "Live step not found" while it is still listing the step's run; Verify now keeps polling for about 10 seconds. If the step still can't be found, it raises `STEP_FAILED` naming the step and leaves the session open, because the step may still be running. Other 404s still raise `SESSION_ENDED`.
|
|
32
|
+
- `npx @revyl/verify init` writes an example that runs on a brand-new account. When you leave the app id empty for iOS or Android, the config targets the device's built-in Settings app by bundle id (`com.apple.Preferences` or `com.android.settings`), and the example opens About and checks the operating system version. It no longer writes a placeholder app id that failed with `CONFIG_INVALID`, and the generated config no longer reads `REVYL_APP_ID`.
|
|
33
|
+
- `init` asks iOS first and defaults to it instead of web.
|
|
34
|
+
- A Web target needs a Web app id: `init` with web and no `--app-id` writes no files, explains how to create the Web app in the dashboard or with `revyl app create --platform web`, and prints the `init` command to run next.
|
|
35
|
+
- `init`'s "Next" list names only commands that can pass: `node verify/example.mts`, and for Mac only once your organization has access.
|
|
36
|
+
- `init` writes the example as `verify/example.mts`, so `node verify/example.mts` runs as an ES module whether `package.json` says `"type": "commonjs"` (what `npm init -y` writes on npm 11), `"type": "module"`, or nothing. As `verify/example.ts` it failed with `Cannot use import statement outside a module` in a CommonJS project.
|
|
37
|
+
- `init --help` prints init's options instead of `Unknown option '--help'`.
|
|
38
|
+
- Add `verify login`, which signs in through the browser, and `verify logout`, which removes `~/.revyl/credentials.json`, so you don't need the Revyl CLI for either. `verify --help` explains that `init`, `login` and the first `open()` sign you in through the browser.
|
|
39
|
+
- Sign-in and missing-credential errors point to `npx @revyl/verify login` instead of the separately installed Revyl CLI.
|
|
40
|
+
- Classify the hosted refusal `computer_platform_at_capacity` (the computer already runs as many children of that platform as it can, such as a second Web child on a Mac) as `COMPUTER_LIMIT` with that `reason`. Earlier versions report it as `SESSION_BUSY`.
|
|
41
|
+
- Map Web runtime target errors on HTTP 409 (`target_not_found`, `stale_target` and similar) to `ACTION_FAILED` instead of `SESSION_BUSY`.
|
|
42
|
+
- Docs: a new "Use your own model" guide (`docs/own-model.md` in the package), exact-action sections with runnable scripts in the web and mobile guides, and the `examples/byo-model` project.
|
|
43
|
+
- Docs: a computer's lifetime is two hours from create, not 60 minutes; `expiresAt` / `expires_at` is the authoritative deadline.
|
|
44
|
+
- Docs: the Web page explains how to create the Web app, starting with the dashboard, and drops the note about Revyl turning Web off. Getting started describes the new `init` and how the Python and Go SDKs sign in. The Go README has a runnable iOS Settings example.
|
|
45
|
+
- Docs: Linux computers are no longer offered in the README, the package docs or the examples, and the README drops its Security and Telemetry sections (SECURITY.md is unchanged). `platform: 'linux'` still works; nothing is deprecated.
|
|
46
|
+
|
|
47
|
+
## Go 0.3.0 (2026-11-11)
|
|
48
|
+
|
|
49
|
+
Published as the `go/v0.3.0` tag.
|
|
50
|
+
|
|
51
|
+
- Early access: run `Instruction` and `Validation` on your own model, as in TypeScript 0.4.0. `verify.WithModel("anthropic/claude-sonnet-5-5")` on `Open` or on one step reads the provider's usual key variable. The eight providers, `ANTHROPIC_BASE_URL` and `OPENAI_BASE_URL` with or without `/v1`, and `verify.WithCustomModel(verify.CustomModel{...})` for any OpenAI-compatible endpoint. `StepResult.Model` reports turns and tokens, cached input included.
|
|
52
|
+
- Custom agents: `verify.WithAgent(verify.LocalAgent{...})` with `verify.Anthropic` and `verify.OpenAICompatible`, or your own `ModelAdapter`.
|
|
53
|
+
- New errors `ErrModelKeyMissing` (returned before any Revyl request) and `ErrModelError`, with the same next-step hints as TypeScript.
|
|
54
|
+
- Early access: exact device actions on Web, iOS and Android: `Tap`, `Type`, `Swipe`, `PressKey`, `Back` (Web and Android), `OpenURL`, `LaunchApp` (iOS and Android), `Locate` and `ScreenSize`, by `verify.Point` or `verify.Element("...")`. They take a `context.Context`, and one cut off by its deadline returns `ErrActionUncertain`. `Swipe` directions are finger motion on every platform, so `SwipeUp` scrolls content down; on Web, Verify sends Revyl the matching page direction.
|
|
55
|
+
- `Open`, `Instruction` and `Validation` now take option values (`OpenOption`, `StepOption`, `ValidationOption`). Existing `OpenOptions{...}`, `StepOptions{...}` and `ValidationOptions{...}` literals still compile; a call that spread a `[]OpenOptions`, `[]StepOptions` or `[]ValidationOptions` slice with `...` must pass the options one by one.
|
|
56
|
+
- Map Web runtime target errors on HTTP 409 (`target_not_found`, `stale_target` and similar) to `ErrActionFailed` instead of `ErrSessionBusy`.
|
|
57
|
+
- Same fix as 0.4.0: a `step_status` 404 "Live step not found" right after a step was accepted is retried for about 10 seconds and then returns `ErrStepFailed`, instead of `ErrSessionEnded`. The session stays open.
|
|
58
|
+
- Classify the hosted refusal `computer_platform_at_capacity` as `ErrComputerLimit` with that reason, like 0.4.0.
|
|
59
|
+
- Computer refusals name the Go methods and point to `Exec` instead of `exec()`, and `Navigate` on a computer is refused that way instead of suggesting `OpenURL`.
|
|
60
|
+
- README: a runnable iOS Settings quick start, how the Go SDK uses a login saved by the other SDKs, a computer's two-hour lifetime, and Linux computers are no longer offered (`verify.Linux` still works).
|
|
61
|
+
|
|
62
|
+
## Python 0.2.0 (2026-11-11)
|
|
63
|
+
|
|
64
|
+
- Early access: run `instruction()` and `validation()` on your own model, as in TypeScript 0.4.0. `open(target, model="anthropic/claude-sonnet-5-5")` runs every step on it with the key from the provider's usual variable, and a step's `model=` overrides it. The eight providers, `ANTHROPIC_BASE_URL` and `OPENAI_BASE_URL` with or without `/v1`, and `CustomModel(id, base_url, api_key, headers, provider, grounding)` for any OpenAI-compatible endpoint. Works on the sync and asyncio APIs. `StepResult.model` reports turns and tokens, cached input included.
|
|
65
|
+
- Custom agents: `local_agent(model, grounding=, max_turns=, model_timeout=, max_image_side=)` with `anthropic()` and `openai_compatible()`, or your own `ModelAdapter`, passed as `agent=`.
|
|
66
|
+
- New error codes `MODEL_KEY_MISSING` (raised before any Revyl request) and `MODEL_ERROR`, with the same next-step hints as TypeScript.
|
|
67
|
+
- Early access: exact device actions on Web, iOS and Android, on `Session` and `AsyncSession`: `tap`, `type`, `swipe`, `press_key`, `back` (Web and Android), `open_url`, `launch_app` (iOS and Android), `locate` and `screen_size`, by `Point(x, y)` or a described target. `swipe` directions are finger motion on every platform, so `"up"` scrolls content down; on Web, Verify sends Revyl the matching page direction.
|
|
68
|
+
- Map Web runtime target errors on HTTP 409 (`target_not_found`, `stale_target` and similar) to `ACTION_FAILED` instead of `SESSION_BUSY`.
|
|
69
|
+
- Same fix as 0.4.0: a `step_status` 404 "Live step not found" right after a step was accepted is retried for about 10 seconds and then raises `STEP_FAILED`, instead of `SESSION_ENDED`. The session stays open.
|
|
70
|
+
- Classify the hosted refusal `computer_platform_at_capacity` as `COMPUTER_LIMIT` with that `reason`, like 0.4.0.
|
|
71
|
+
- `navigate()` on a computer raises `NOT_AVAILABLE_ON_PLATFORM` pointing to `exec()` and file operations, like the other device calls, instead of suggesting `open_url()`.
|
|
72
|
+
- Print progress to stderr by default, like TypeScript: `[verify] <platform>: …` lines with the session link as soon as it is known, `ready`, and each instruction or validation's outcome and duration. Step text is not printed. Pass `log=False` to turn the lines off, or `log=callback` to receive them. An explicit `log=None` now means the default.
|
|
73
|
+
- README: a computer's two-hour lifetime, and Linux computers are no longer offered (`Target(platform="linux")` still works).
|
|
74
|
+
|
|
3
75
|
## 0.3.2 (2026-11-11)
|
|
4
76
|
|
|
5
77
|
- Fix `npx @revyl/verify init` in pnpm projects: it now runs `pnpm add -D`. pnpm 9 and 10 rejected `--dev`, and pnpm 12 read it as "install only devDependencies", saved Verify under `dependencies` without installing it, and removed the project's other dependencies from `node_modules`.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Verify
|
|
2
2
|
|
|
3
|
-

|
|
4
4
|
|
|
5
|
-
Verify runs end-to-end checks in a cloud browser, iOS simulator, or Android emulator on Revyl. You write what the agent should do and what must be true afterward, in plain sentences, and call it from a script, a coding agent, CI, or the test runner you already use. Verify also opens Mac
|
|
5
|
+
Verify runs end-to-end checks in a cloud browser, iOS simulator, or Android emulator on Revyl. You write what the agent should do and what must be true afterward, in plain sentences, and call it from a script, a coding agent, CI, or the test runner you already use. When you know exactly what to press, you can tap, type and swipe yourself instead. Verify also opens Mac computers for commands, files, and screenshots.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
8
8
|
import { open } from '@revyl/verify';
|
|
@@ -41,47 +41,84 @@ Or set it up yourself:
|
|
|
41
41
|
npx @revyl/verify init
|
|
42
42
|
```
|
|
43
43
|
|
|
44
|
-
`init` adds Verify to your project, asks what to open, writes `verify.config.ts` and an example script, signs you in through the browser on the first interactive run, and prints the command that runs the example. To skip the config, run `npm install @revyl/verify` and save the example above as `verify/check.mts`, then run `node verify/check.mts`. [Getting started](https://github.com/RevylAI/verify/blob/v0.
|
|
44
|
+
`init` adds Verify to your project, asks what to open, writes `verify.config.ts` and an example script, signs you in through the browser on the first interactive run, and prints the command that runs the example. With no app id, the iOS and Android examples open the built-in Settings app, so they run on a new account before you upload anything. To skip the config, run `npm install @revyl/verify` and save the example above as `verify/check.mts`, then run `node verify/check.mts`. [Getting started](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/getting-started.md) covers sign-in, your own app, and [CI](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/ci.md).
|
|
45
45
|
|
|
46
|
-
For finished projects, see [`examples/`](https://github.com/RevylAI/verify/tree/v0.
|
|
46
|
+
For finished projects, see [`examples/`](https://github.com/RevylAI/verify/tree/v0.4.1/examples/): a [web store](https://github.com/RevylAI/verify/tree/v0.4.1/examples/web-store/), [mobile Settings](https://github.com/RevylAI/verify/tree/v0.4.1/examples/mobile-settings/), a [Mac workspace](https://github.com/RevylAI/verify/tree/v0.4.1/examples/mac-workspace/), a small [agent platform](https://github.com/RevylAI/verify/tree/v0.4.1/examples/agent-platform/) that runs web and iOS side by side, and [your own model](https://github.com/RevylAI/verify/tree/v0.4.1/examples/byo-model/) (with [Python](https://github.com/RevylAI/verify/blob/v0.4.1/python/examples/byo_model.py) and [Go](https://github.com/RevylAI/verify/blob/v0.4.1/go/examples/byomodel/main.go) versions).
|
|
47
|
+
|
|
48
|
+
## Exact actions
|
|
49
|
+
|
|
50
|
+
Tap, type, swipe and press keys yourself, by a description Revyl locates on screen or by coordinates, in the same script as your steps:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
await session.type({ target: 'Email field' }, 'demo@example.com'); // Revyl finds the element you describe
|
|
54
|
+
await session.pressKey('enter');
|
|
55
|
+
await session.swipe('up'); // scrolls the content down
|
|
56
|
+
await session.tap({ x: 196, y: 640 }); // or a point in screenshot pixels
|
|
57
|
+
await session.instruction('Accept the terms if they are shown'); // the agent where it's fuzzy
|
|
58
|
+
await session.validation('The welcome screen is shown');
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`back`, `openUrl`, `launchApp`, `locate` and `screenSize` round them out; [which call works where](#which-call-works-where) shows the platform limits. Early access. In TypeScript (`@revyl/verify` 0.4.0), Python (`revyl-verify` 0.2.0) and Go (v0.3.0); the [Python](https://github.com/RevylAI/verify/blob/v0.4.1/python/README.md#exact-actions) and [Go](https://github.com/RevylAI/verify/blob/v0.4.1/go/README.md#exact-actions) READMEs show them with each language's names. The [web](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/web.md#exact-actions) and [mobile](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/mobile.md#exact-actions) guides have runnable scripts, and [api.md](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/api.md#exact-actions) lists every call.
|
|
62
|
+
|
|
63
|
+
## Your own model
|
|
64
|
+
|
|
65
|
+
Steps run on Revyl's agent unless you pass a model. Then the agent loop runs on your machine with your key, and Revyl provides the device and finds the elements your model names:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
const session = await open('ios', { model: 'anthropic/claude-sonnet-5-5' }); // key from ANTHROPIC_API_KEY
|
|
69
|
+
await session.instruction('Open General, then About');
|
|
70
|
+
await session.instruction('Open Privacy', { model: 'openai/gpt-5.5' }); // or per step
|
|
71
|
+
|
|
72
|
+
await open('ios', { model: { id: 'qwen-3.8-27b', baseURL: 'https://api.cerebras.ai/v1', apiKey: process.env.CEREBRAS_API_KEY } });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
In TypeScript (`@revyl/verify` 0.4.0), Python (`revyl-verify` 0.2.0) and Go (v0.3.0); the [Python](https://github.com/RevylAI/verify/blob/v0.4.1/python/README.md#your-own-model) and [Go](https://github.com/RevylAI/verify/blob/v0.4.1/go/README.md#your-own-model) READMEs show them with each language's names. Built in: `anthropic`, `openai`, `google`, `openrouter`, `groq`, `cerebras`, `xai` and `ollama`; anything OpenAI-compatible works through `baseURL`. Early access; [Use your own model](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/own-model.md) covers the providers, picking a model, what Revyl still sees, limits and troubleshooting.
|
|
47
76
|
|
|
48
77
|
## Packages
|
|
49
78
|
|
|
50
79
|
| Package | Install | Status |
|
|
51
80
|
| --- | --- | --- |
|
|
52
|
-
| [`@revyl/verify`](https://www.npmjs.com/package/@revyl/verify) | `npm install @revyl/verify` | TypeScript SDK and the `verify` CLI (`init`, `whoami`). Published on npm, Node.js 22.18 or later. |
|
|
81
|
+
| [`@revyl/verify`](https://www.npmjs.com/package/@revyl/verify) | `npm install @revyl/verify` | TypeScript SDK and the `verify` CLI (`init`, `login`, `logout`, `whoami`). Published on npm, Node.js 22.18 or later. |
|
|
53
82
|
| [`revyl-verify`](https://www.npmjs.com/package/revyl-verify) (npm) | `npm install revyl-verify` | Alias of `@revyl/verify` at the same version. |
|
|
54
|
-
| [Go module](https://github.com/RevylAI/verify/tree/go/v0.
|
|
55
|
-
| [`revyl-verify`](https://pypi.org/project/revyl-verify/) (PyPI) | `pip install revyl-verify` or `uv add revyl-verify` | Python SDK, published on PyPI. Python 3.10 or later, sync and asyncio APIs, no CLI; see the [Python guide](https://github.com/RevylAI/verify/blob/v0.
|
|
83
|
+
| [Go module](https://github.com/RevylAI/verify/tree/go/v0.3.1/go) | `go get github.com/RevylAI/verify/go@v0.3.1` | Published as the `go/v0.3.1` tag, Go 1.26.9 or later. Calls return errors instead of throwing. |
|
|
84
|
+
| [`revyl-verify`](https://pypi.org/project/revyl-verify/) (PyPI) | `pip install revyl-verify` or `uv add revyl-verify` | Python SDK, published on PyPI. Python 3.10 or later, sync and asyncio APIs, no CLI; see the [Python guide](https://github.com/RevylAI/verify/blob/v0.4.1/python/README.md). |
|
|
56
85
|
|
|
57
86
|
## Platforms
|
|
58
87
|
|
|
59
|
-
| Platform | Instructions / validations | Commands / files / processes | PNG screenshot |
|
|
60
|
-
| --- | :---: | :---: | :---: |
|
|
61
|
-
| Web | Yes | — | Yes |
|
|
62
|
-
| iOS simulator | Yes | — | Yes |
|
|
63
|
-
| Android emulator | Yes | — | Yes |
|
|
64
|
-
| Mac computer | — | Yes | Yes |
|
|
65
|
-
| Linux computer | — | Yes | Yes |
|
|
88
|
+
| Platform | Instructions / validations | Exact actions | Commands / files / processes | PNG screenshot |
|
|
89
|
+
| --- | :---: | :---: | :---: | :---: |
|
|
90
|
+
| Web | Yes | Yes | — | Yes |
|
|
91
|
+
| iOS simulator | Yes | Yes | — | Yes |
|
|
92
|
+
| Android emulator | Yes | Yes | — | Yes |
|
|
93
|
+
| Mac computer | — | — | Yes | Yes |
|
|
66
94
|
|
|
67
|
-
|
|
95
|
+
### Which call works where
|
|
68
96
|
|
|
69
|
-
|
|
97
|
+
| Call | Web | iOS | Android | Mac computer |
|
|
98
|
+
| --- | --- | --- | --- | --- |
|
|
99
|
+
| `tap`, `type`, `swipe`, `pressKey`, `locate`, `screenSize` | Yes | Yes | Yes | No, use `exec()` and files |
|
|
100
|
+
| `back()` | Yes, browser history | No, tap the on-screen back control | Yes, the system back button | No |
|
|
101
|
+
| `openUrl(url)` | Yes, loads an http(s) URL in the tab | Yes, a URL or deep link | Yes, a URL or deep link | No |
|
|
102
|
+
| `launchApp(bundleId)` | No, use `openUrl` or `navigate` | Yes | Yes | No |
|
|
103
|
+
| `navigate(url)` | Yes | No, use `openUrl` | No, use `openUrl` | No |
|
|
104
|
+
| `instruction`, `validation`, your own model | Yes | Yes | Yes | No, use `exec()` |
|
|
105
|
+
| `screenshot()` | Yes | Yes | Yes | Yes |
|
|
106
|
+
| `exec`, files, processes | No, open a Mac computer | No, open a Mac computer | No, open a Mac computer | Yes |
|
|
70
107
|
|
|
71
|
-
|
|
108
|
+
**Coordinates.** Prefer a description, such as `tap({ target: 'Sign in button' })`: Revyl finds the element on the current screen, and you never handle coordinates. Otherwise a point is `{ x, y }` in screenshot pixels on every platform: whole numbers from the top-left corner of the image `screenshot()` returns. So a position you read off a screenshot goes to `tap()`, `type()` and `swipe()` unchanged. `screenSize()` returns the screenshot's width and height, and `locate()` and the points `tap()` and `type()` return use the same pixels. A point outside the screenshot is refused with `INVALID_ARGUMENT` before any action is sent, and the message names the valid range. On iOS, Verify converts the pixels to the screen's points for you. On Web they equal CSS pixels at the default device scale factor of 1, and you still pass screenshot pixels at any other. This holds from TypeScript 0.4.1, Python 0.2.1 and Go v0.3.1; earlier versions took iOS points.
|
|
72
109
|
|
|
73
|
-
|
|
110
|
+
A call a platform doesn't support throws `NOT_AVAILABLE_ON_PLATFORM`, and its message names the alternative.
|
|
74
111
|
|
|
75
|
-
|
|
112
|
+
Web is available to every organization. Mac computers are early access and have a server-bounded lifetime; see [computers](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/computers.md).
|
|
76
113
|
|
|
77
|
-
##
|
|
114
|
+
## Documentation
|
|
78
115
|
|
|
79
|
-
|
|
116
|
+
The [documentation](https://github.com/RevylAI/verify/tree/v0.4.1/packages/verify/docs/) covers the [API](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/api.md), [configuration](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/configuration.md), [web](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/web.md), [mobile](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/mobile.md), [computers](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/computers.md), [CI](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/ci.md), [your own model](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/own-model.md), [errors and troubleshooting](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/errors.md), [coding agents](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/agents.md), and [live end-to-end results](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/end-to-end.md). The npm package ships these pages in `node_modules/@revyl/verify/docs/`, with an [llms.txt](https://github.com/RevylAI/verify/blob/v0.4.1/packages/verify/docs/llms.txt) index, so a coding agent can read the docs for the version you installed. The Revyl docs site has an overview at [docs.revyl.com/verify](https://docs.revyl.com/verify).
|
|
80
117
|
|
|
81
118
|
## Status
|
|
82
119
|
|
|
83
|
-
Verify is at 0.x, so APIs can still change between minor releases; the [changelog](https://github.com/RevylAI/verify/blob/v0.
|
|
120
|
+
Verify is at 0.x, so APIs can still change between minor releases; the [changelog](https://github.com/RevylAI/verify/blob/v0.4.1/CHANGELOG.md) records each release. Mac computers are early access. See [Contributing](https://github.com/RevylAI/verify/blob/v0.4.1/CONTRIBUTING.md) and [Releasing](https://github.com/RevylAI/verify/blob/v0.4.1/RELEASING.md) to work on Verify.
|
|
84
121
|
|
|
85
122
|
## License
|
|
86
123
|
|
|
87
|
-
[Apache 2.0](https://github.com/RevylAI/verify/blob/v0.
|
|
124
|
+
[Apache 2.0](https://github.com/RevylAI/verify/blob/v0.4.1/LICENSE).
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface ModelImage {
|
|
2
|
+
png: Uint8Array;
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
}
|
|
6
|
+
/** Shrinks a PNG by the smallest whole factor that keeps its longer side within maxSide, averaging each block of pixels. */
|
|
7
|
+
export declare function fitPng(bytes: Uint8Array, maxSide: number): ModelImage;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PNG } from 'pngjs';
|
|
2
|
+
/** Shrinks a PNG by the smallest whole factor that keeps its longer side within maxSide, averaging each block of pixels. */
|
|
3
|
+
export function fitPng(bytes, maxSide) {
|
|
4
|
+
const source = PNG.sync.read(Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength));
|
|
5
|
+
const factor = Math.ceil(Math.max(source.width, source.height) / maxSide);
|
|
6
|
+
if (factor <= 1)
|
|
7
|
+
return { png: bytes, width: source.width, height: source.height };
|
|
8
|
+
const width = Math.floor(source.width / factor);
|
|
9
|
+
const height = Math.floor(source.height / factor);
|
|
10
|
+
const target = new PNG({ width, height });
|
|
11
|
+
const area = factor * factor;
|
|
12
|
+
for (let y = 0; y < height; y += 1) {
|
|
13
|
+
for (let x = 0; x < width; x += 1) {
|
|
14
|
+
const sums = [0, 0, 0, 0];
|
|
15
|
+
for (let dy = 0; dy < factor; dy += 1) {
|
|
16
|
+
const row = (y * factor + dy) * source.width;
|
|
17
|
+
for (let dx = 0; dx < factor; dx += 1) {
|
|
18
|
+
const offset = (row + x * factor + dx) * 4;
|
|
19
|
+
for (let channel = 0; channel < 4; channel += 1)
|
|
20
|
+
sums[channel] += source.data[offset + channel];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
const out = (y * width + x) * 4;
|
|
24
|
+
for (let channel = 0; channel < 4; channel += 1)
|
|
25
|
+
target.data[out + channel] = Math.round(sums[channel] / area);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return { png: PNG.sync.write(target), width, height };
|
|
29
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { ModelAdapter } from './model.js';
|
|
2
|
+
/** A point in screenshot pixels, from the top-left corner. */
|
|
3
|
+
export interface Point {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
}
|
|
7
|
+
/** An element described in words, which Revyl's grounding model locates on the current screen. */
|
|
8
|
+
export interface Target {
|
|
9
|
+
target: string;
|
|
10
|
+
}
|
|
11
|
+
export type Location = Point | Target;
|
|
12
|
+
export type SwipeDirection = 'up' | 'down' | 'left' | 'right';
|
|
13
|
+
export type DeviceAction = {
|
|
14
|
+
kind: 'tap';
|
|
15
|
+
at: Location;
|
|
16
|
+
} | {
|
|
17
|
+
kind: 'type';
|
|
18
|
+
at: Location;
|
|
19
|
+
text: string;
|
|
20
|
+
clear: boolean;
|
|
21
|
+
} | {
|
|
22
|
+
kind: 'swipe';
|
|
23
|
+
direction: SwipeDirection;
|
|
24
|
+
from?: Location;
|
|
25
|
+
} | {
|
|
26
|
+
kind: 'key';
|
|
27
|
+
key: 'enter' | 'backspace';
|
|
28
|
+
} | {
|
|
29
|
+
kind: 'back';
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'open_url';
|
|
32
|
+
url: string;
|
|
33
|
+
};
|
|
34
|
+
/** What the agent loop needs from a session. DeviceSession provides it. */
|
|
35
|
+
export interface AgentControls {
|
|
36
|
+
readonly platform: 'ios' | 'android' | 'web';
|
|
37
|
+
capture(signal: AbortSignal): Promise<{
|
|
38
|
+
png: Uint8Array;
|
|
39
|
+
screenToken?: string;
|
|
40
|
+
}>;
|
|
41
|
+
screenSize(signal: AbortSignal): Promise<{
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
}>;
|
|
45
|
+
perform(action: DeviceAction, screenToken: string | undefined, signal: AbortSignal): Promise<Point | undefined>;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* model: the model gives screenshot coordinates, and only device actions reach Revyl.
|
|
49
|
+
* revyl: the model names the element and Revyl's grounding model finds it, so element descriptions reach Revyl too.
|
|
50
|
+
*/
|
|
51
|
+
export type Grounding = 'model' | 'revyl';
|
|
52
|
+
export interface LocalAgentOptions {
|
|
53
|
+
model: ModelAdapter;
|
|
54
|
+
/** Defaults to 'revyl'. */
|
|
55
|
+
grounding?: Grounding;
|
|
56
|
+
/** Model turns an instruction may take, each ending in one action. Defaults to 20, at most 100. */
|
|
57
|
+
maxTurns?: number;
|
|
58
|
+
/** Milliseconds one model call may take, retries included. Defaults to 60000. */
|
|
59
|
+
modelTimeout?: number;
|
|
60
|
+
/** The longest screenshot side sent to the model, in pixels. Defaults to 1280. */
|
|
61
|
+
maxImageSide?: number;
|
|
62
|
+
}
|
|
63
|
+
export interface LocalAgent {
|
|
64
|
+
readonly model: ModelAdapter;
|
|
65
|
+
readonly grounding: Grounding;
|
|
66
|
+
readonly maxTurns: number;
|
|
67
|
+
readonly modelTimeout: number;
|
|
68
|
+
readonly maxImageSide: number;
|
|
69
|
+
}
|
|
70
|
+
/** One model turn of a local step. */
|
|
71
|
+
export interface ModelTurn {
|
|
72
|
+
/** The action the model chose, as text, such as 'tap "General"' or 'verdict true'. */
|
|
73
|
+
action: string;
|
|
74
|
+
/** Whether the action ran; a failed action is reported back to the model on its next turn. */
|
|
75
|
+
ok: boolean;
|
|
76
|
+
modelMs: number;
|
|
77
|
+
actionMs: number;
|
|
78
|
+
inputTokens: number;
|
|
79
|
+
outputTokens: number;
|
|
80
|
+
}
|
|
81
|
+
/** How a step ran on your model instead of Revyl's agent. */
|
|
82
|
+
export interface ModelRun {
|
|
83
|
+
/** The model's name, such as 'anthropic/claude-sonnet-5-5'. */
|
|
84
|
+
id: string;
|
|
85
|
+
grounding: Grounding;
|
|
86
|
+
turns: ModelTurn[];
|
|
87
|
+
inputTokens: number;
|
|
88
|
+
outputTokens: number;
|
|
89
|
+
}
|
|
90
|
+
/** Runs instructions and validations on a model you choose, on your machine. Revyl only receives device actions. */
|
|
91
|
+
export declare function localAgent(options: LocalAgentOptions): LocalAgent;
|
|
92
|
+
/** Runs one instruction or validation through the agent's model. The caller enforces the step deadline through signal. */
|
|
93
|
+
export declare function runLocalStep(type: 'instruction' | 'validation', task: string, agent: LocalAgent, controls: AgentControls, signal: AbortSignal, log: (line: string) => void): Promise<{
|
|
94
|
+
passed: boolean;
|
|
95
|
+
reason?: string;
|
|
96
|
+
run: ModelRun;
|
|
97
|
+
}>;
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
2
|
+
import { VerifyError, errorMessage, isVerifyError } from '../errors.js';
|
|
3
|
+
import { fitPng } from './image.js';
|
|
4
|
+
/** Runs instructions and validations on a model you choose, on your machine. Revyl only receives device actions. */
|
|
5
|
+
export function localAgent(options) {
|
|
6
|
+
if (!options || typeof options.model?.complete !== 'function' || typeof options.model.name !== 'string') {
|
|
7
|
+
throw new VerifyError('INVALID_ARGUMENT', 'localAgent() needs a model adapter, such as anthropic() or openaiCompatible().');
|
|
8
|
+
}
|
|
9
|
+
const grounding = options.grounding ?? 'revyl';
|
|
10
|
+
if (grounding !== 'model' && grounding !== 'revyl')
|
|
11
|
+
throw new VerifyError('INVALID_ARGUMENT', "localAgent() grounding must be 'model' or 'revyl'.");
|
|
12
|
+
const maxTurns = bounded(options.maxTurns ?? 20, 1, 100, 'maxTurns');
|
|
13
|
+
const modelTimeout = bounded(options.modelTimeout ?? 60_000, 1_000, 600_000, 'modelTimeout');
|
|
14
|
+
const maxImageSide = bounded(options.maxImageSide ?? 1280, 256, 4096, 'maxImageSide');
|
|
15
|
+
return Object.freeze({ model: options.model, grounding, maxTurns, modelTimeout, maxImageSide });
|
|
16
|
+
}
|
|
17
|
+
const VALIDATION_ATTEMPTS = 3;
|
|
18
|
+
const NO_TOOL_LIMIT = 3;
|
|
19
|
+
const ABORTING_CODES = new Set(['SESSION_ENDED', 'SESSION_BUSY', 'AUTH_REJECTED', 'EDGE_BLOCKED', 'MODEL_ERROR', 'TIMEOUT', 'API_ERROR', 'ACTION_UNCERTAIN']);
|
|
20
|
+
/** Runs one instruction or validation through the agent's model. The caller enforces the step deadline through signal. */
|
|
21
|
+
export async function runLocalStep(type, task, agent, controls, signal, log) {
|
|
22
|
+
const run = { id: agent.model.name, grounding: agent.grounding, turns: [], inputTokens: 0, outputTokens: 0 };
|
|
23
|
+
const history = [];
|
|
24
|
+
const size = await controls.screenSize(signal);
|
|
25
|
+
const tools = type === 'validation' ? [VERDICT_TOOL] : instructionTools(controls.platform, agent.grounding);
|
|
26
|
+
const turns = type === 'validation' ? VALIDATION_ATTEMPTS : agent.maxTurns;
|
|
27
|
+
let turnsWithoutTool = 0;
|
|
28
|
+
for (let turn = 1; turn <= turns; turn += 1) {
|
|
29
|
+
signal.throwIfAborted();
|
|
30
|
+
const shot = await controls.capture(signal);
|
|
31
|
+
const image = fitPng(shot.png, agent.maxImageSide);
|
|
32
|
+
const modelStarted = Date.now();
|
|
33
|
+
const reply = await agent.model.complete({
|
|
34
|
+
system: type === 'validation' ? validationPrompt(controls.platform) : instructionPrompt(controls.platform, agent.grounding),
|
|
35
|
+
text: turnText(type, task, image, history),
|
|
36
|
+
image,
|
|
37
|
+
tools,
|
|
38
|
+
}, AbortSignal.any([signal, AbortSignal.timeout(agent.modelTimeout)]));
|
|
39
|
+
const record = { action: 'no tool call', ok: false, modelMs: Date.now() - modelStarted, actionMs: 0, inputTokens: reply.usage.inputTokens, outputTokens: reply.usage.outputTokens };
|
|
40
|
+
run.turns.push(record);
|
|
41
|
+
run.inputTokens += record.inputTokens;
|
|
42
|
+
run.outputTokens += record.outputTokens;
|
|
43
|
+
const call = reply.toolCall;
|
|
44
|
+
if (reply.truncated) {
|
|
45
|
+
throw new VerifyError('MODEL_ERROR', `${agent.model.name}'s reply was cut off at its output token cap ${call ? 'in the middle of a tool call, so Verify did not use it' : 'before it called a tool'}. Pass the model to localAgent() through an adapter with a higher maxTokens, such as anthropic({ model, maxTokens: 8192 }) or openaiCompatible({ model, baseURL, maxTokens: 8192 }).`);
|
|
46
|
+
}
|
|
47
|
+
if (!call) {
|
|
48
|
+
turnsWithoutTool += 1;
|
|
49
|
+
if (turnsWithoutTool === NO_TOOL_LIMIT) {
|
|
50
|
+
throw new VerifyError('MODEL_ERROR', `${agent.model.name} answered ${NO_TOOL_LIMIT} times in a row without calling a tool. Pick a model that supports tool calls.`);
|
|
51
|
+
}
|
|
52
|
+
history.push(`${turn}. You answered without calling a tool. Call exactly one tool.`);
|
|
53
|
+
log(` · turn ${turn}: no tool call (${record.modelMs} ms)`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
turnsWithoutTool = 0;
|
|
57
|
+
if (!tools.some((tool) => tool.name === call.name)) {
|
|
58
|
+
record.action = call.name.slice(0, 40);
|
|
59
|
+
history.push(`${turn}. ${record.action}: failed: not one of the offered tools (${tools.map((tool) => tool.name).join(', ')}).`);
|
|
60
|
+
log(` · turn ${turn}: ${record.action} is not an offered tool (${record.modelMs} ms)`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (call.name === 'done' || call.name === 'verdict') {
|
|
64
|
+
const passed = (call.name === 'done' ? call.arguments.success : call.arguments.passed) === true;
|
|
65
|
+
const reason = typeof call.arguments.reason === 'string' ? call.arguments.reason.slice(0, 1_000) : undefined;
|
|
66
|
+
record.action = `${call.name} ${passed}`;
|
|
67
|
+
record.ok = true;
|
|
68
|
+
log(` · turn ${turn}: ${record.action} (${record.modelMs} ms)`);
|
|
69
|
+
return { passed, ...(reason ? { reason } : {}), run };
|
|
70
|
+
}
|
|
71
|
+
const actionStarted = Date.now();
|
|
72
|
+
try {
|
|
73
|
+
const action = toAction(call, controls.platform, agent.grounding, image, size);
|
|
74
|
+
record.action = describe(action);
|
|
75
|
+
const at = await controls.perform(action, shot.screenToken, signal);
|
|
76
|
+
record.ok = true;
|
|
77
|
+
history.push(`${turn}. ${record.action}${at ? ` at (${at.x}, ${at.y})` : ''}: done.`);
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
if (isVerifyError(error) && ABORTING_CODES.has(error.code))
|
|
81
|
+
throw error;
|
|
82
|
+
if (record.action === 'no tool call')
|
|
83
|
+
record.action = call.name;
|
|
84
|
+
history.push(`${turn}. ${record.action}: failed: ${errorMessage(error).slice(0, 300)}`);
|
|
85
|
+
}
|
|
86
|
+
record.actionMs = Date.now() - actionStarted;
|
|
87
|
+
log(` · turn ${turn}: ${record.action}${record.ok ? '' : ' (failed)'} (model ${record.modelMs} ms, action ${record.actionMs} ms)`);
|
|
88
|
+
await delay(controls.platform === 'web' ? 500 : 1_000, undefined, { signal });
|
|
89
|
+
}
|
|
90
|
+
if (type === 'validation')
|
|
91
|
+
throw new VerifyError('STEP_FAILED', `${agent.model.name} gave no verdict in ${turns} attempts.`);
|
|
92
|
+
return { passed: false, reason: `${agent.model.name} did not finish within ${turns} turns.`, run };
|
|
93
|
+
}
|
|
94
|
+
function toAction(call, platform, grounding, image, size) {
|
|
95
|
+
const args = call.arguments;
|
|
96
|
+
const location = (required) => {
|
|
97
|
+
if (grounding === 'revyl') {
|
|
98
|
+
if (typeof args.target === 'string' && args.target.trim())
|
|
99
|
+
return { target: args.target.trim().slice(0, 500) };
|
|
100
|
+
if (required)
|
|
101
|
+
throw new VerifyError('INVALID_ARGUMENT', 'target is required: describe the element by its visible text or appearance.');
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
if (typeof args.x !== 'number' || typeof args.y !== 'number') {
|
|
105
|
+
if (required)
|
|
106
|
+
throw new VerifyError('INVALID_ARGUMENT', 'x and y are required.');
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
if (args.x < 0 || args.y < 0 || args.x >= image.width || args.y >= image.height) {
|
|
110
|
+
throw new VerifyError('INVALID_ARGUMENT', `(${args.x}, ${args.y}) is outside the ${image.width}x${image.height} screenshot.`);
|
|
111
|
+
}
|
|
112
|
+
return { x: Math.round((args.x * size.width) / image.width), y: Math.round((args.y * size.height) / image.height) };
|
|
113
|
+
};
|
|
114
|
+
switch (call.name) {
|
|
115
|
+
case 'tap':
|
|
116
|
+
return { kind: 'tap', at: location(true) };
|
|
117
|
+
case 'type':
|
|
118
|
+
if (typeof args.text !== 'string')
|
|
119
|
+
throw new VerifyError('INVALID_ARGUMENT', 'text is required.');
|
|
120
|
+
return { kind: 'type', at: location(true), text: args.text, clear: args.clear !== false };
|
|
121
|
+
case 'swipe': {
|
|
122
|
+
if (!['up', 'down', 'left', 'right'].includes(String(args.direction)))
|
|
123
|
+
throw new VerifyError('INVALID_ARGUMENT', 'direction must be up, down, left or right.');
|
|
124
|
+
const from = location(false);
|
|
125
|
+
return { kind: 'swipe', direction: args.direction, ...(from ? { from } : {}) };
|
|
126
|
+
}
|
|
127
|
+
case 'press_key':
|
|
128
|
+
if (args.key !== 'enter' && args.key !== 'backspace')
|
|
129
|
+
throw new VerifyError('INVALID_ARGUMENT', 'key must be enter or backspace.');
|
|
130
|
+
return { kind: 'key', key: args.key };
|
|
131
|
+
case 'back':
|
|
132
|
+
if (platform === 'ios')
|
|
133
|
+
throw new VerifyError('NOT_AVAILABLE_ON_PLATFORM', 'iOS has no back button; tap the on-screen back control.');
|
|
134
|
+
return { kind: 'back' };
|
|
135
|
+
case 'open_url':
|
|
136
|
+
if (typeof args.url !== 'string' || !args.url)
|
|
137
|
+
throw new VerifyError('INVALID_ARGUMENT', 'url is required.');
|
|
138
|
+
return { kind: 'open_url', url: args.url };
|
|
139
|
+
default:
|
|
140
|
+
throw new VerifyError('INVALID_ARGUMENT', `${call.name} is not a tool.`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function describe(action) {
|
|
144
|
+
const where = (at) => (at === undefined ? '' : 'target' in at ? ` "${at.target}"` : ` (${at.x}, ${at.y})`);
|
|
145
|
+
switch (action.kind) {
|
|
146
|
+
case 'tap':
|
|
147
|
+
return `tap${where(action.at)}`;
|
|
148
|
+
case 'type':
|
|
149
|
+
return `type ${JSON.stringify(action.text.length > 40 ? `${action.text.slice(0, 40)}…` : action.text)} into${where(action.at)}`;
|
|
150
|
+
case 'swipe':
|
|
151
|
+
return `swipe ${action.direction}${action.from ? ` from${where(action.from)}` : ''}`;
|
|
152
|
+
case 'key':
|
|
153
|
+
return `press ${action.key}`;
|
|
154
|
+
case 'back':
|
|
155
|
+
return 'back';
|
|
156
|
+
case 'open_url':
|
|
157
|
+
return `open ${action.url}`;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const PLATFORM_LABEL = { ios: 'iPhone simulator', android: 'Android emulator', web: 'web browser' };
|
|
161
|
+
function instructionPrompt(platform, grounding) {
|
|
162
|
+
return [
|
|
163
|
+
`You operate a ${PLATFORM_LABEL[platform]} to carry out a task. Every turn you see the current screenshot and the actions taken so far, and you call exactly one tool.`,
|
|
164
|
+
grounding === 'revyl'
|
|
165
|
+
? 'To point at an element, describe it in target by its visible text or look, for example "General row" or "Search field". Do not give coordinates.'
|
|
166
|
+
: 'To point at an element, give the pixel coordinates of its center in the screenshot, x to the right and y down from the top-left corner.',
|
|
167
|
+
'Check the screenshot before each action: the previous action may not have done what you expected. To see content further down, swipe up.',
|
|
168
|
+
'Call done with success true as soon as the screenshot shows the task is complete. Call done with success false and a reason if it cannot be done.',
|
|
169
|
+
].join('\n');
|
|
170
|
+
}
|
|
171
|
+
function validationPrompt(platform) {
|
|
172
|
+
return `You check whether a statement holds on the current screen of a ${PLATFORM_LABEL[platform]}. Judge only what the screenshot shows, then call verdict with passed and a one-sentence reason.`;
|
|
173
|
+
}
|
|
174
|
+
function turnText(type, task, image, history) {
|
|
175
|
+
const lines = [type === 'validation' ? `Statement: ${task}` : `Task: ${task}`, `The screenshot is ${image.width}x${image.height} pixels.`];
|
|
176
|
+
if (type === 'instruction')
|
|
177
|
+
lines.push(history.length ? `Actions so far:\n${history.slice(-30).join('\n')}` : 'No actions yet.');
|
|
178
|
+
else if (history.length)
|
|
179
|
+
lines.push(`Earlier attempts:\n${history.join('\n')}`);
|
|
180
|
+
return lines.join('\n');
|
|
181
|
+
}
|
|
182
|
+
const VERDICT_TOOL = {
|
|
183
|
+
name: 'verdict',
|
|
184
|
+
description: 'Report whether the statement holds on the screen.',
|
|
185
|
+
parameters: { type: 'object', properties: { passed: { type: 'boolean' }, reason: { type: 'string' } }, required: ['passed', 'reason'] },
|
|
186
|
+
};
|
|
187
|
+
function instructionTools(platform, grounding) {
|
|
188
|
+
const at = grounding === 'revyl'
|
|
189
|
+
? { properties: { target: { type: 'string', description: 'The element, by its visible text or look.' } }, required: ['target'] }
|
|
190
|
+
: { properties: { x: { type: 'integer', description: 'Screenshot pixel x.' }, y: { type: 'integer', description: 'Screenshot pixel y.' } }, required: ['x', 'y'] };
|
|
191
|
+
const tools = [
|
|
192
|
+
{ name: 'tap', description: 'Tap an element.', parameters: { type: 'object', ...at } },
|
|
193
|
+
{
|
|
194
|
+
name: 'type',
|
|
195
|
+
description: 'Tap a text field and type into it. Clears the field first unless clear is false.',
|
|
196
|
+
parameters: { type: 'object', properties: { ...at.properties, text: { type: 'string' }, clear: { type: 'boolean' } }, required: [...at.required, 'text'] },
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'swipe',
|
|
200
|
+
description: "Swipe from a point, or from the screen's center. up moves the finger up, which scrolls content down.",
|
|
201
|
+
parameters: { type: 'object', properties: { direction: { type: 'string', enum: ['up', 'down', 'left', 'right'] }, ...at.properties }, required: ['direction'] },
|
|
202
|
+
},
|
|
203
|
+
{ name: 'press_key', description: 'Press a key on the focused field.', parameters: { type: 'object', properties: { key: { type: 'string', enum: ['enter', 'backspace'] } }, required: ['key'] } },
|
|
204
|
+
{ name: 'open_url', description: platform === 'web' ? 'Load a URL in the current tab.' : 'Open a URL or deep link on the device.', parameters: { type: 'object', properties: { url: { type: 'string' } }, required: ['url'] } },
|
|
205
|
+
{ name: 'done', description: 'Finish the task.', parameters: { type: 'object', properties: { success: { type: 'boolean' }, reason: { type: 'string' } }, required: ['success', 'reason'] } },
|
|
206
|
+
];
|
|
207
|
+
if (platform !== 'ios')
|
|
208
|
+
tools.splice(4, 0, { name: 'back', description: platform === 'web' ? 'Go back in browser history.' : 'Press the Android back button.', parameters: { type: 'object', properties: {} } });
|
|
209
|
+
return tools;
|
|
210
|
+
}
|
|
211
|
+
function bounded(value, min, max, name) {
|
|
212
|
+
if (!Number.isInteger(value) || value < min || value > max)
|
|
213
|
+
throw new VerifyError('INVALID_ARGUMENT', `localAgent() ${name} must be an integer from ${min} to ${max}.`);
|
|
214
|
+
return value;
|
|
215
|
+
}
|