@loadmill/droid-cua 2.39.0 → 2.41.0
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/README.md +27 -8
- package/build/index.js +4 -2
- package/build/src/cli/headless-execution-config.js +6 -5
- package/build/src/cli/headless-target.js +15 -7
- package/build/src/cli/skill-command.js +20 -3
- package/build/src/commands/help.js +8 -4
- package/build/src/core/cua-models.js +17 -15
- package/build/src/core/droid-cua-run-usage.js +22 -19
- package/build/src/core/execution-engine.js +11 -1
- package/build/src/desktop/computer-use.js +249 -78
- package/build/src/desktop/primary-display.js +74 -0
- package/build/src/desktop/windows-visible-bounds.js +55 -0
- package/build/src/integrations/loadmill/report-upload-manager.js +37 -9
- package/build/src/prompts/base.js +9 -6
- package/package.json +1 -1
- package/skill/droid-cua/SKILL.md +5 -3
- package/skill/droid-cua/references/authoring-tests.md +77 -18
- package/skill/droid-cua/references/iterative-improvement.md +10 -6
- package/skill/droid-cua/references/onboarding-and-troubleshooting.md +6 -0
- package/skill/droid-cua/references/project-and-auth.md +1 -1
- package/skill/droid-cua/references/running-and-healing.md +12 -4
package/README.md
CHANGED
|
@@ -158,8 +158,13 @@ droid-cua skill install codex
|
|
|
158
158
|
droid-cua skill install codex --local
|
|
159
159
|
```
|
|
160
160
|
|
|
161
|
-
Use `droid-cua skill status <agent>` to
|
|
162
|
-
|
|
161
|
+
Use `droid-cua skill status <agent>` to see the installed and bundled skill
|
|
162
|
+
versions and installation path; add `--local` to inspect a project installation.
|
|
163
|
+
The skill version is stored in `SKILL.md` under `metadata.version`, independently
|
|
164
|
+
of the CLI package version. Older copies without version metadata show `unknown`.
|
|
165
|
+
The `modified` status means the files differ from the bundled copy, which can
|
|
166
|
+
indicate an older release or local edits even when version labels match.
|
|
167
|
+
Use `droid-cua skill uninstall <agent>` to remove an installation. Installed skill
|
|
163
168
|
files are never overwritten or removed after local modification unless `--force`
|
|
164
169
|
is supplied. After upgrading the npm package, update an installed skill explicitly:
|
|
165
170
|
|
|
@@ -188,6 +193,9 @@ droid-cua run tests/login.dcua --platform ios --avd "iPhone 16"
|
|
|
188
193
|
# List running desktop applications and their window IDs (macOS or Windows)
|
|
189
194
|
droid-cua --target desktop --list-desktop-apps
|
|
190
195
|
|
|
196
|
+
# Control the full primary display across applications
|
|
197
|
+
droid-cua --target desktop --instructions tests/launcher.dcua
|
|
198
|
+
|
|
191
199
|
# Control one already-running desktop application window
|
|
192
200
|
droid-cua --target desktop --desktop-app com.apple.TextEdit --desktop-window 123 --instructions tests/editor.dcua
|
|
193
201
|
|
|
@@ -239,7 +247,7 @@ backward compatibility.
|
|
|
239
247
|
Example headless config:
|
|
240
248
|
```json
|
|
241
249
|
{
|
|
242
|
-
"cuaModel": "gpt-
|
|
250
|
+
"cuaModel": "gpt-6-sol",
|
|
243
251
|
"promptCustomizations": {
|
|
244
252
|
"basePromptInstructions": "",
|
|
245
253
|
"designModeInstructions": "",
|
|
@@ -271,8 +279,8 @@ Supported CLI options include:
|
|
|
271
279
|
- `--list-devices`
|
|
272
280
|
- `--browser`
|
|
273
281
|
- `--target` (`mobile`, `web`, or `desktop`)
|
|
274
|
-
- `--desktop-app` (desktop application ID or exact name)
|
|
275
|
-
- `--desktop-window` (
|
|
282
|
+
- `--desktop-app` (limit desktop control to an application ID or exact name; omit for full desktop)
|
|
283
|
+
- `--desktop-window` (select a window within `--desktop-app`)
|
|
276
284
|
- `--desktop-input` (`physical` by default; use `accessibility` for cursor-free input)
|
|
277
285
|
- `--list-desktop-apps`
|
|
278
286
|
- `--instructions`
|
|
@@ -295,10 +303,21 @@ Supported CLI options include:
|
|
|
295
303
|
- `--debug`
|
|
296
304
|
|
|
297
305
|
`--cua-model` accepts `loadmill-smart`, `loadmill-pulse`, `loadmill-beacon`,
|
|
298
|
-
`gpt-
|
|
299
|
-
|
|
306
|
+
`gpt-6-sol`, and `gpt-6-luna`. OpenAI models use direct OpenAI mode and require
|
|
307
|
+
`OPENAI_API_KEY`. Saved direct OpenAI selections of GPT-5.6 Terra, GPT-5.6 Luna,
|
|
308
|
+
or GPT-5.4 move to GPT-6 Sol, GPT-6 Luna, or GPT-6 Sol, respectively.
|
|
309
|
+
|
|
310
|
+
Desktop control supports macOS and Windows. In the desktop app, enable **Experimental desktop control** at the bottom of Settings to show Desktop in the platform picker. Desktop runs use physical pointer and keyboard input by default. Droid shows a system notice for three seconds before taking control and a completion notice when the run finishes, fails, or is stopped. The delay is fixed; use Stop in Droid once the run starts. Avoid using the mouse or keyboard during the run. macOS requires Screen & System Audio Recording and Accessibility permissions for Droid or the terminal running the CLI. On Windows, Droid must run at the same integrity level as the target application. System notices and automatic minimization apply only to runs started in the desktop app.
|
|
311
|
+
|
|
312
|
+
CLI runs with `--target desktop` control the full primary display by default, with no application flag needed. In the desktop app, choose **Full desktop**, the first option in the Application dropdown. This mode captures and controls the primary display across applications, including apps launched during the test; no target application needs to be running when connecting. Keep all test windows on the primary display. Full desktop requires physical input. Droid minimizes itself after the three-second notice and remains minimized when the run ends; open Droid to review the result or use Stop. Existing saved application selections are preserved.
|
|
313
|
+
|
|
314
|
+
Use `--desktop-app <id-or-exact-name>` or select an application in Droid to limit control to that application's windows. Application mode brings the selected window forward and leaves it there when the run ends. Add `--desktop-window <id>` to select a particular window, or `--desktop-input accessibility` for cursor-free CLI input; both options require `--desktop-app`. Before each run and screenshot, Droid refreshes window selection within the original application so it can follow a recreated login window. If replacement is ambiguous, select a window again. If a captured window disappears or moves before input, Droid requests a fresh screenshot instead of transferring its coordinates to a replacement. Focus errors report the activation reason; they do not establish an elevation problem.
|
|
315
|
+
|
|
316
|
+
If Windows returns an all-black window capture, Droid brings the target forward and crops the visible desktop to the window's visible frame, excluding its invisible resize border. The fallback logs a warning, crops before resizing to preserve detail, and requires the target to remain unobstructed on the primary display; off-screen portions appear black. With accessibility input, bring the target forward yourself when requested. Full desktop reports capture failures directly, without this window capture fallback.
|
|
317
|
+
|
|
318
|
+
Desktop **Test design** uses the same notices once you submit your instructions and exploration starts. Full desktop minimizes Droid at that point. Droid sends the completion notice when the generated test is ready, exploration is stopped, or recovery ends in an error. It does not automatically bring itself forward; open Droid whenever you need to give guidance, use Stop, or review the generated test. Opening Test design alone does not take desktop control. When a controller rejects an action, Droid skips the remaining actions in that batch and asks the model to continue from a fresh screenshot with the failure reason. Rejected actions are not counted as completed.
|
|
300
319
|
|
|
301
|
-
|
|
320
|
+
The system notices are shown only for desktop control runs started in the desktop app. On macOS, they may not appear in the unsigned Electron dev app; verify them in a signed packaged build.
|
|
302
321
|
|
|
303
322
|
Config and precedence rules:
|
|
304
323
|
- Use `--config <file>` to supply prompt-affecting settings for headless runs.
|
package/build/index.js
CHANGED
|
@@ -534,15 +534,17 @@ async function main() {
|
|
|
534
534
|
}
|
|
535
535
|
else if (isDesktopTarget) {
|
|
536
536
|
desktopController = await createDesktopController({
|
|
537
|
+
scope: targetConfig.desktopScope,
|
|
537
538
|
app: targetConfig.desktopApp,
|
|
538
539
|
windowId: targetConfig.desktopWindow,
|
|
539
540
|
inputMode: targetConfig.desktopInput,
|
|
541
|
+
focusOnConnect: true,
|
|
540
542
|
excludePids: [process.pid],
|
|
541
543
|
});
|
|
542
544
|
targetId = desktopController.deviceId;
|
|
543
545
|
deviceInfo = desktopController.deviceInfo;
|
|
544
|
-
console.log(`Attached to desktop
|
|
545
|
-
console.log(`Model sees
|
|
546
|
+
console.log(`Attached to desktop target: ${deviceInfo.device_name}`);
|
|
547
|
+
console.log(`Model sees desktop target: ${deviceInfo.scaled_width}x${deviceInfo.scaled_height}`);
|
|
546
548
|
console.log(`Desktop input: ${deviceInfo.desktopInputMode === "physical" ? "physical OS pointer" : "cursor-free accessibility"}`);
|
|
547
549
|
process.once("SIGINT", async () => {
|
|
548
550
|
console.log("\nStopping desktop run...");
|
|
@@ -2,7 +2,7 @@ import path from "path";
|
|
|
2
2
|
import { readFile } from "fs/promises";
|
|
3
3
|
import { DEFAULT_CONTEXT_OPTIMIZATION_THRESHOLD, MAX_CONTEXT_OPTIMIZATION_THRESHOLD, MIN_CONTEXT_OPTIMIZATION_THRESHOLD, } from "../core/context-optimization.js";
|
|
4
4
|
import { AIM_ASSIST_MODES, DEFAULT_AIM_ASSIST_MODE } from "../core/aim-assist.js";
|
|
5
|
-
import { CUA_MODELS, DEFAULT_OPENAI_CUA_MODEL, isOpenAiCuaModel, isValidCuaModel, resolveCuaModel } from "../core/cua-models.js";
|
|
5
|
+
import { CUA_MODELS, DEFAULT_OPENAI_CUA_MODEL, isOpenAiCuaModel, isValidCuaModel, resolveCuaModel, upgradeLegacyCuaModel } from "../core/cua-models.js";
|
|
6
6
|
import { LLM_PROVIDER_MODES, isValidLlmProviderMode, resolveLlmProviderMode } from "../core/runtime-config.js";
|
|
7
7
|
import { parseSecretsOverrideString } from "../core/secrets.js";
|
|
8
8
|
function createEmptyPromptCustomizations() {
|
|
@@ -21,10 +21,11 @@ function assertNonEmptyString(value, label) {
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
function validateCuaModel(value, label) {
|
|
24
|
-
|
|
24
|
+
const upgraded = typeof value === "string" ? upgradeLegacyCuaModel(value) : value;
|
|
25
|
+
if (!isValidCuaModel(upgraded)) {
|
|
25
26
|
throw new Error(`${label} must be one of: ${CUA_MODELS.join(", ")}.`);
|
|
26
27
|
}
|
|
27
|
-
return
|
|
28
|
+
return upgraded;
|
|
28
29
|
}
|
|
29
30
|
function validateAimAssistMode(value, label) {
|
|
30
31
|
if (typeof value !== "string" || !AIM_ASSIST_MODES.includes(value)) {
|
|
@@ -44,8 +45,8 @@ function readEnvDefaultCuaModel() {
|
|
|
44
45
|
// OpenAI model, stay on the OpenAI default rather than crossing over to the
|
|
45
46
|
// global (Loadmill) default.
|
|
46
47
|
if (typeof envValue === "string" && envValue.trim()) {
|
|
47
|
-
const
|
|
48
|
-
return isOpenAiCuaModel(
|
|
48
|
+
const upgraded = upgradeLegacyCuaModel(envValue.trim());
|
|
49
|
+
return isOpenAiCuaModel(upgraded) ? upgraded : DEFAULT_OPENAI_CUA_MODEL;
|
|
49
50
|
}
|
|
50
51
|
return resolveCuaModel(envValue);
|
|
51
52
|
}
|
|
@@ -184,6 +184,9 @@ export function shouldWaitForLoadmillCloudCompletion(targetConfig) {
|
|
|
184
184
|
const MOBILE_ONLY_CLOUD_SOURCES = new Set(["lambdatest", "aws-device-farm"]);
|
|
185
185
|
export function resolveHeadlessTarget(args = {}) {
|
|
186
186
|
const target = normalizeTarget(args.target);
|
|
187
|
+
if (target !== "desktop" && (args["desktop-app"] != null || args["desktop-window"] != null || args["desktop-input"] != null || args["list-desktop-apps"] != null)) {
|
|
188
|
+
throw new Error("--desktop-app, --desktop-window, --desktop-input, and --list-desktop-apps can only be used with --target desktop.");
|
|
189
|
+
}
|
|
187
190
|
const targetGiven = hasNonEmptyString(args.target);
|
|
188
191
|
const avdName = hasNonEmptyString(args.avd) ? args.avd.trim() : null;
|
|
189
192
|
const platform = normalizeMobilePlatform(args.platform, "--platform");
|
|
@@ -264,18 +267,25 @@ export function resolveHeadlessTarget(args = {}) {
|
|
|
264
267
|
}
|
|
265
268
|
const listDesktopApps = args["list-desktop-apps"] != null;
|
|
266
269
|
const desktopApp = hasNonEmptyString(args["desktop-app"]) ? args["desktop-app"].trim() : null;
|
|
270
|
+
if (args["desktop-app"] != null && !desktopApp) {
|
|
271
|
+
throw new Error("--desktop-app requires an application ID or exact name.");
|
|
272
|
+
}
|
|
267
273
|
const rawWindow = readNonEmptyCliValue(args["desktop-window"]);
|
|
268
274
|
const desktopWindow = rawWindow == null ? null : Number(rawWindow);
|
|
269
275
|
const desktopInput = hasNonEmptyString(args["desktop-input"]) ? args["desktop-input"].trim().toLowerCase() : "physical";
|
|
270
|
-
if (
|
|
271
|
-
throw new Error("--desktop-
|
|
276
|
+
if (args["desktop-window"] != null && !desktopApp) {
|
|
277
|
+
throw new Error("--desktop-window requires --desktop-app.");
|
|
272
278
|
}
|
|
273
|
-
if (
|
|
279
|
+
if (args["desktop-window"] != null && (rawWindow == null || !Number.isInteger(desktopWindow) || desktopWindow < 0)) {
|
|
274
280
|
throw new Error("--desktop-window must be a non-negative integer window ID.");
|
|
275
281
|
}
|
|
276
|
-
if (
|
|
282
|
+
if ((args["desktop-input"] != null && !hasNonEmptyString(args["desktop-input"])) ||
|
|
283
|
+
(desktopInput !== "accessibility" && desktopInput !== "physical")) {
|
|
277
284
|
throw new Error('--desktop-input must be "accessibility" or "physical".');
|
|
278
285
|
}
|
|
286
|
+
if (desktopInput === "accessibility" && !desktopApp) {
|
|
287
|
+
throw new Error("--desktop-input accessibility requires --desktop-app. Full desktop uses physical input only.");
|
|
288
|
+
}
|
|
279
289
|
return {
|
|
280
290
|
target,
|
|
281
291
|
sessionMode: null,
|
|
@@ -290,14 +300,12 @@ export function resolveHeadlessTarget(args = {}) {
|
|
|
290
300
|
artifacts: null,
|
|
291
301
|
listDevices: false,
|
|
292
302
|
listDesktopApps,
|
|
303
|
+
desktopScope: desktopApp ? "window" : "desktop",
|
|
293
304
|
desktopApp,
|
|
294
305
|
desktopWindow,
|
|
295
306
|
desktopInput,
|
|
296
307
|
};
|
|
297
308
|
}
|
|
298
|
-
if (args["desktop-app"] != null || args["desktop-window"] != null || args["desktop-input"] != null || args["list-desktop-apps"] != null) {
|
|
299
|
-
throw new Error("--desktop-app, --desktop-window, --desktop-input, and --list-desktop-apps can only be used with --target desktop.");
|
|
300
|
-
}
|
|
301
309
|
if (args["session-mode"] != null) {
|
|
302
310
|
throw new Error("--session-mode can only be used with --target web.");
|
|
303
311
|
}
|
|
@@ -12,7 +12,7 @@ const SUBCOMMANDS = new Set(["install", "status", "uninstall"]);
|
|
|
12
12
|
const ALLOWED_OPTIONS = new Set(["_", "local", "force", "h", "help"]);
|
|
13
13
|
const COMMAND_DESCRIPTIONS = Object.freeze({
|
|
14
14
|
install: "Install Droid CUA skill for a coding agent",
|
|
15
|
-
status: "Show
|
|
15
|
+
status: "Show installation status and installed/bundled skill versions",
|
|
16
16
|
uninstall: "Uninstall Droid CUA skill for a coding agent",
|
|
17
17
|
});
|
|
18
18
|
function skillCommandHelp(subcommand) {
|
|
@@ -53,7 +53,7 @@ function skillRootHelp() {
|
|
|
53
53
|
"",
|
|
54
54
|
"Commands:",
|
|
55
55
|
" install Install the skill",
|
|
56
|
-
" status Show installation status",
|
|
56
|
+
" status Show installation status and skill versions",
|
|
57
57
|
" uninstall Uninstall the skill",
|
|
58
58
|
].join("\n");
|
|
59
59
|
}
|
|
@@ -103,6 +103,18 @@ async function directoryMatches(sourcePath, destinationPath) {
|
|
|
103
103
|
]);
|
|
104
104
|
return JSON.stringify(sourceSnapshot) === JSON.stringify(destinationSnapshot);
|
|
105
105
|
}
|
|
106
|
+
// Read the bundled skill's block-style metadata.version scalar without a YAML dependency.
|
|
107
|
+
async function readSkillVersion(skillPath) {
|
|
108
|
+
if (await pathType(skillPath) !== "directory")
|
|
109
|
+
return null;
|
|
110
|
+
if (await pathType(path.join(skillPath, "SKILL.md")) !== "file")
|
|
111
|
+
return null;
|
|
112
|
+
const markdown = await readFile(path.join(skillPath, "SKILL.md"), "utf8");
|
|
113
|
+
const frontmatter = markdown.replace(/\r\n/g, "\n").match(/^---\n([\s\S]*?)\n---(?:\n|$)/)?.[1];
|
|
114
|
+
const metadata = frontmatter?.match(/^metadata:[ \t]*\n((?:[ \t]+[^\n]*(?:\n|$))*)/m)?.[1];
|
|
115
|
+
const value = metadata?.match(/^ version:[ \t]*(?:"([^"\n]+)"|'([^'\n]+)'|([^\s#]+))[ \t]*(?:#.*)?$/m);
|
|
116
|
+
return value ? (value[1] ?? value[2] ?? value[3]) : null;
|
|
117
|
+
}
|
|
106
118
|
async function getInstallationState(sourcePath, destinationPath) {
|
|
107
119
|
if (await pathType(destinationPath) === "missing")
|
|
108
120
|
return "not-installed";
|
|
@@ -214,7 +226,12 @@ export async function runSkillCommand({ args, cwd = process.cwd(), home = os.hom
|
|
|
214
226
|
}
|
|
215
227
|
for (const destination of states) {
|
|
216
228
|
if (command.subcommand === "status") {
|
|
217
|
-
|
|
229
|
+
const [installedVersion, bundledVersion] = await Promise.all([
|
|
230
|
+
readSkillVersion(destination.path),
|
|
231
|
+
readSkillVersion(bundledSkillPath),
|
|
232
|
+
]);
|
|
233
|
+
const installedLabel = destination.state === "not-installed" ? "not installed" : installedVersion ?? "unknown";
|
|
234
|
+
output.log(`${destination.displayName}: ${stateLabel(destination.state)} at ${destination.path}\nInstalled skill version: ${installedLabel}\nBundled skill version: ${bundledVersion ?? "unknown"}`);
|
|
218
235
|
}
|
|
219
236
|
else if (command.subcommand === "install") {
|
|
220
237
|
if (destination.state === "installed") {
|
|
@@ -11,19 +11,21 @@ import { CUA_MODELS } from '../core/cua-models.js';
|
|
|
11
11
|
*/
|
|
12
12
|
export async function handleHelp(args, session, context) {
|
|
13
13
|
const addOutput = context?.addOutput || ((item) => console.log(item.text || item));
|
|
14
|
-
addOutput({ type: 'system', text: 'droid-cua - AI-powered mobile and
|
|
14
|
+
addOutput({ type: 'system', text: 'droid-cua - AI-powered mobile, web, and desktop testing CLI' });
|
|
15
15
|
addOutput({ type: 'info', text: '' });
|
|
16
16
|
addOutput({ type: 'info', text: 'Usage:' });
|
|
17
17
|
addOutput({ type: 'info', text: ' droid-cua --avd <device-name> [options]' });
|
|
18
18
|
addOutput({ type: 'info', text: ' droid-cua run <file-or-directory...> [options]' });
|
|
19
19
|
addOutput({ type: 'info', text: ' droid-cua --target web --instructions <file> [options]' });
|
|
20
|
+
addOutput({ type: 'info', text: ' droid-cua --target desktop --instructions <file> [options]' });
|
|
20
21
|
addOutput({ type: 'info', text: ' droid-cua --device-source lambdatest --instructions <file> [options]' });
|
|
21
22
|
addOutput({ type: 'info', text: '' });
|
|
22
23
|
addOutput({ type: 'info', text: 'Options:' });
|
|
23
24
|
addOutput({ type: 'info', text: ' --target <kind> Target kind: mobile (default), web, or desktop' });
|
|
24
|
-
addOutput({ type: 'info', text: '
|
|
25
|
-
addOutput({ type: 'info', text: ' --desktop-
|
|
26
|
-
addOutput({ type: 'info', text: ' --desktop-
|
|
25
|
+
addOutput({ type: 'info', text: ' Desktop controls the primary display by default' });
|
|
26
|
+
addOutput({ type: 'info', text: ' --desktop-app <id> Limit desktop control to a running app ID or exact name' });
|
|
27
|
+
addOutput({ type: 'info', text: ' --desktop-window <id> Select a window within --desktop-app' });
|
|
28
|
+
addOutput({ type: 'info', text: ' --desktop-input <mode> Physical pointer (default); accessibility requires --desktop-app' });
|
|
27
29
|
addOutput({ type: 'info', text: ' --list-desktop-apps List running desktop apps and windows' });
|
|
28
30
|
addOutput({ type: 'info', text: ' --avd <name> Device name (Android device ID/serial or iOS Simulator)' });
|
|
29
31
|
addOutput({ type: 'info', text: ' --platform <platform> Force platform: android or ios' });
|
|
@@ -76,6 +78,7 @@ export async function handleHelp(args, session, context) {
|
|
|
76
78
|
addOutput({ type: 'info', text: ' iOS: Uses Appium + XCUITest for iOS Simulator automation' });
|
|
77
79
|
addOutput({ type: 'info', text: ' LambdaTest: Uses real cloud devices for headless mobile runs' });
|
|
78
80
|
addOutput({ type: 'info', text: ' Web: Uses Playwright with installed Google Chrome or Microsoft Edge for browser execution' });
|
|
81
|
+
addOutput({ type: 'info', text: ' Desktop: Controls the primary display or a selected application on macOS and Windows' });
|
|
79
82
|
addOutput({ type: 'info', text: '' });
|
|
80
83
|
addOutput({ type: 'info', text: 'Platform Detection:' });
|
|
81
84
|
addOutput({ type: 'info', text: ' - Use --platform flag to force a specific platform' });
|
|
@@ -98,6 +101,7 @@ export async function handleHelp(args, session, context) {
|
|
|
98
101
|
addOutput({ type: 'info', text: ' droid-cua --instructions tests/login.dcua --context app/context.md' });
|
|
99
102
|
addOutput({ type: 'info', text: ' droid-cua --instructions tests --test-data tests/test-data.md --scenario "Run every checkout test with each buyer account"' });
|
|
100
103
|
addOutput({ type: 'info', text: ' droid-cua --target web --instructions tests/search.dcua' });
|
|
104
|
+
addOutput({ type: 'info', text: ' droid-cua --target desktop --instructions tests/launcher.dcua' });
|
|
101
105
|
addOutput({ type: 'info', text: ' droid-cua --device-source lambdatest --platform android --device-name "Galaxy S24" --os-version 14 --app app.apk --instructions tests/login.dcua' });
|
|
102
106
|
addOutput({ type: 'info', text: ' /create login-test (design a new test)' });
|
|
103
107
|
addOutput({ type: 'info', text: ' /list (see all tests)' });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const DEFAULT_OPENAI_CUA_MODEL = "gpt-
|
|
1
|
+
export const DEFAULT_OPENAI_CUA_MODEL = "gpt-6-sol";
|
|
2
2
|
export const DEFAULT_LOADMILL_CUA_MODEL = "loadmill-smart";
|
|
3
3
|
export const DEFAULT_CUA_MODEL = DEFAULT_LOADMILL_CUA_MODEL;
|
|
4
4
|
export const CUA_CONVERSATION_POLICIES = Object.freeze({
|
|
@@ -28,27 +28,28 @@ export const CUA_MODEL_OPTIONS = Object.freeze([
|
|
|
28
28
|
label: "Loadmill - Beacon",
|
|
29
29
|
}),
|
|
30
30
|
Object.freeze({
|
|
31
|
-
id: "gpt-
|
|
31
|
+
id: "gpt-6-sol",
|
|
32
32
|
providerMode: "openai",
|
|
33
33
|
providerLabel: "OpenAI",
|
|
34
|
-
name: "GPT-
|
|
35
|
-
label: "OpenAI GPT-
|
|
34
|
+
name: "GPT-6 Sol",
|
|
35
|
+
label: "OpenAI GPT-6 Sol",
|
|
36
36
|
}),
|
|
37
37
|
Object.freeze({
|
|
38
|
-
id: "gpt-
|
|
38
|
+
id: "gpt-6-luna",
|
|
39
39
|
providerMode: "openai",
|
|
40
40
|
providerLabel: "OpenAI",
|
|
41
|
-
name: "GPT-
|
|
42
|
-
label: "OpenAI GPT-
|
|
43
|
-
}),
|
|
44
|
-
Object.freeze({
|
|
45
|
-
id: "gpt-5.4",
|
|
46
|
-
providerMode: "openai",
|
|
47
|
-
providerLabel: "OpenAI",
|
|
48
|
-
name: "GPT-5.4",
|
|
49
|
-
label: "OpenAI GPT-5.4",
|
|
41
|
+
name: "GPT-6 Luna",
|
|
42
|
+
label: "OpenAI GPT-6 Luna",
|
|
50
43
|
}),
|
|
51
44
|
]);
|
|
45
|
+
const LEGACY_OPENAI_CUA_MODELS = Object.freeze({
|
|
46
|
+
"gpt-5.6-terra": "gpt-6-sol",
|
|
47
|
+
"gpt-5.6-luna": "gpt-6-luna",
|
|
48
|
+
"gpt-5.4": "gpt-6-sol",
|
|
49
|
+
});
|
|
50
|
+
export function upgradeLegacyCuaModel(value) {
|
|
51
|
+
return LEGACY_OPENAI_CUA_MODELS[value] ?? value;
|
|
52
|
+
}
|
|
52
53
|
export const CUA_MODELS = Object.freeze(CUA_MODEL_OPTIONS.map((option) => option.id));
|
|
53
54
|
export const OPENAI_CUA_MODELS = Object.freeze(CUA_MODEL_OPTIONS
|
|
54
55
|
.filter((option) => option.providerMode === "openai")
|
|
@@ -70,7 +71,8 @@ export function isLoadmillCuaModel(value) {
|
|
|
70
71
|
return LOADMILL_CUA_MODEL_SET.has(value);
|
|
71
72
|
}
|
|
72
73
|
export function resolveCuaModel(value) {
|
|
73
|
-
|
|
74
|
+
const upgraded = upgradeLegacyCuaModel(value);
|
|
75
|
+
return isValidCuaModel(upgraded) ? upgraded : DEFAULT_CUA_MODEL;
|
|
74
76
|
}
|
|
75
77
|
export function resolveCuaModelProviderMode(value) {
|
|
76
78
|
const normalized = resolveCuaModel(value);
|
|
@@ -3,29 +3,26 @@ const OPENAI_GPT_5_4_PRICING = Object.freeze({
|
|
|
3
3
|
inputPerMillionUsd: 2.5,
|
|
4
4
|
});
|
|
5
5
|
const OPENAI_GPT_5_6_TERRA_PRICING = Object.freeze({
|
|
6
|
-
inputPerMillionUsd: 2
|
|
6
|
+
inputPerMillionUsd: 2,
|
|
7
7
|
});
|
|
8
8
|
const OPENAI_GPT_5_6_LUNA_PRICING = Object.freeze({
|
|
9
|
-
inputPerMillionUsd:
|
|
9
|
+
inputPerMillionUsd: 0.2,
|
|
10
|
+
});
|
|
11
|
+
const OPENAI_GPT_6_SOL_PRICING = Object.freeze({
|
|
12
|
+
inputPerMillionUsd: 2,
|
|
13
|
+
});
|
|
14
|
+
const OPENAI_GPT_6_LUNA_PRICING = Object.freeze({
|
|
15
|
+
inputPerMillionUsd: 0.1,
|
|
10
16
|
});
|
|
11
17
|
const MODEL_PRICING = Object.freeze({
|
|
12
|
-
"gpt-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
"gpt-5.6-luna":
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
}),
|
|
21
|
-
// Loadmill Smart currently routes to GPT-5.6 Terra behind the Loadmill LLM gateway.
|
|
22
|
-
// Keep this explicit so pricing can be updated here if the backing model changes.
|
|
23
|
-
"loadmill-smart": Object.freeze({
|
|
24
|
-
...OPENAI_GPT_5_6_TERRA_PRICING,
|
|
25
|
-
}),
|
|
26
|
-
"loadmill-pulse": Object.freeze({
|
|
27
|
-
...OPENAI_GPT_5_6_LUNA_PRICING,
|
|
28
|
-
}),
|
|
18
|
+
"gpt-6-sol": OPENAI_GPT_6_SOL_PRICING,
|
|
19
|
+
"gpt-6-luna": OPENAI_GPT_6_LUNA_PRICING,
|
|
20
|
+
"gpt-5.6-terra": OPENAI_GPT_5_6_TERRA_PRICING,
|
|
21
|
+
"gpt-5.6-luna": OPENAI_GPT_5_6_LUNA_PRICING,
|
|
22
|
+
"gpt-5.4": OPENAI_GPT_5_4_PRICING,
|
|
23
|
+
// Loadmill Smart and Pulse route to GPT-6 Sol and Luna, respectively.
|
|
24
|
+
"loadmill-smart": OPENAI_GPT_6_SOL_PRICING,
|
|
25
|
+
"loadmill-pulse": OPENAI_GPT_6_LUNA_PRICING,
|
|
29
26
|
});
|
|
30
27
|
function finiteNumber(value) {
|
|
31
28
|
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
@@ -41,6 +38,12 @@ export function normalizeDroidCuaCostModel(model) {
|
|
|
41
38
|
if (typeof model !== "string") {
|
|
42
39
|
return null;
|
|
43
40
|
}
|
|
41
|
+
if (model === "gpt-6-sol" || model.startsWith("gpt-6-sol-")) {
|
|
42
|
+
return "gpt-6-sol";
|
|
43
|
+
}
|
|
44
|
+
if (model === "gpt-6-luna" || model.startsWith("gpt-6-luna-")) {
|
|
45
|
+
return "gpt-6-luna";
|
|
46
|
+
}
|
|
44
47
|
if (model === "gpt-5.6-terra" || model.startsWith("gpt-5.6-terra-")) {
|
|
45
48
|
return "gpt-5.6-terra";
|
|
46
49
|
}
|
|
@@ -408,7 +408,7 @@ export class ExecutionEngine {
|
|
|
408
408
|
// The Desktop recorder returns one from disk; for runs without a recorder (e.g. a
|
|
409
409
|
// plain CLI `--report`) render the same marker in-memory from the raw screenshot.
|
|
410
410
|
const annotatedDataUrl = await this.resolveAnnotatedDataUrl(annotatedScreenshot, currentScreenshot, action, context);
|
|
411
|
-
await this.handleModelAction(this.session.deviceId, action, this.session.deviceInfo.scale, {
|
|
411
|
+
const actionResult = await this.handleModelAction(this.session.deviceId, action, this.session.deviceInfo.scale, {
|
|
412
412
|
...context,
|
|
413
413
|
shouldStop,
|
|
414
414
|
stepId: stepContext?.stepId,
|
|
@@ -430,6 +430,13 @@ export class ExecutionEngine {
|
|
|
430
430
|
...(annotatedScreenshot?.screenshotPath ? { screenshotPath: annotatedScreenshot.screenshotPath } : {}),
|
|
431
431
|
...(context?.includeScreenshotDataUrl && annotatedDataUrl ? { screenshotDataUrl: annotatedDataUrl } : {})
|
|
432
432
|
});
|
|
433
|
+
if (actionResult?.success === false) {
|
|
434
|
+
if (shouldStop())
|
|
435
|
+
return { stopped: true, sawExplicitScreenshotAction };
|
|
436
|
+
const actionError = `${action.type} failed: ${actionResult.error || "The action could not be completed."}`;
|
|
437
|
+
this.session.addToTranscript?.(`[Action Error] ${actionError}`);
|
|
438
|
+
return { refreshModelState: true, actionError, sawExplicitScreenshotAction };
|
|
439
|
+
}
|
|
433
440
|
if (trackAction) {
|
|
434
441
|
trackAction(action);
|
|
435
442
|
}
|
|
@@ -823,6 +830,9 @@ export class ExecutionEngine {
|
|
|
823
830
|
}
|
|
824
831
|
if (actionBatchResult.refreshModelState) {
|
|
825
832
|
const followUp = await this.buildActionFollowUp(call_id, pendingSafetyChecks, true, addOutput, eventMeta, context, stepContext);
|
|
833
|
+
if (actionBatchResult.actionError) {
|
|
834
|
+
followUp.input.push({ role: "user", content: `${actionBatchResult.actionError} The remaining actions were skipped. Use the fresh screenshot to decide what to do next.` });
|
|
835
|
+
}
|
|
826
836
|
currentScreenshot = followUp.currentScreenshot ?? currentScreenshot;
|
|
827
837
|
response = await this.requestNextTurn(followUp.input, newResponseId, context, stepContext);
|
|
828
838
|
newResponseId = response.id;
|