@yoqa/runner 0.3.27 → 0.3.28
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/dist/index.js +1382 -163
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -16444,6 +16444,7 @@ var apps = sqliteTable("apps", {
|
|
|
16444
16444
|
name: text("name").notNull(),
|
|
16445
16445
|
prefix: text("prefix").notNull().default(""),
|
|
16446
16446
|
context: text("context").notNull().default(""),
|
|
16447
|
+
knowledge: text("knowledge").notNull().default(""),
|
|
16447
16448
|
iosBundleId: text("ios_bundle_id").notNull().default(""),
|
|
16448
16449
|
iosAppStoreId: text("ios_app_store_id").notNull().default(""),
|
|
16449
16450
|
androidApplicationId: text("android_application_id").notNull().default(""),
|
|
@@ -16500,6 +16501,7 @@ var runs = sqliteTable("runs", {
|
|
|
16500
16501
|
buildId: text("build_id"),
|
|
16501
16502
|
status: text("status").notNull(),
|
|
16502
16503
|
executionMode: text("execution_mode").notNull().default("auto"),
|
|
16504
|
+
screenMode: text("screen_mode").notNull().default("tree"),
|
|
16503
16505
|
error: text("error"),
|
|
16504
16506
|
createdAt: integer("created_at").notNull(),
|
|
16505
16507
|
startedAt: integer("started_at"),
|
|
@@ -16524,6 +16526,7 @@ var runSteps = sqliteTable("run_steps", {
|
|
|
16524
16526
|
screenshotUri: text("screenshot_uri"),
|
|
16525
16527
|
ok: integer("ok").notNull().default(0),
|
|
16526
16528
|
latencyMs: integer("latency_ms").notNull().default(0),
|
|
16529
|
+
phasesJson: text("phases_json"),
|
|
16527
16530
|
detail: text("detail"),
|
|
16528
16531
|
command: text("command"),
|
|
16529
16532
|
createdAt: integer("created_at").notNull()
|
|
@@ -16543,6 +16546,7 @@ function ensureSchema(sqlite) {
|
|
|
16543
16546
|
name TEXT NOT NULL,
|
|
16544
16547
|
prefix TEXT NOT NULL DEFAULT '',
|
|
16545
16548
|
context TEXT NOT NULL DEFAULT '',
|
|
16549
|
+
knowledge TEXT NOT NULL DEFAULT '',
|
|
16546
16550
|
ios_bundle_id TEXT NOT NULL DEFAULT '',
|
|
16547
16551
|
ios_app_store_id TEXT NOT NULL DEFAULT '',
|
|
16548
16552
|
android_application_id TEXT NOT NULL DEFAULT '',
|
|
@@ -16626,6 +16630,7 @@ function ensureSchema(sqlite) {
|
|
|
16626
16630
|
build_id TEXT,
|
|
16627
16631
|
status TEXT NOT NULL,
|
|
16628
16632
|
execution_mode TEXT NOT NULL DEFAULT 'auto',
|
|
16633
|
+
screen_mode TEXT NOT NULL DEFAULT 'tree',
|
|
16629
16634
|
error TEXT,
|
|
16630
16635
|
created_at INTEGER NOT NULL,
|
|
16631
16636
|
started_at INTEGER,
|
|
@@ -16652,6 +16657,7 @@ function ensureSchema(sqlite) {
|
|
|
16652
16657
|
screenshot_uri TEXT,
|
|
16653
16658
|
ok INTEGER NOT NULL DEFAULT 0,
|
|
16654
16659
|
latency_ms INTEGER NOT NULL DEFAULT 0,
|
|
16660
|
+
phases_json TEXT,
|
|
16655
16661
|
detail TEXT,
|
|
16656
16662
|
command TEXT,
|
|
16657
16663
|
created_at INTEGER NOT NULL
|
|
@@ -16670,9 +16676,12 @@ function ensureSchema(sqlite) {
|
|
|
16670
16676
|
`);
|
|
16671
16677
|
migrateProvidersTable(sqlite);
|
|
16672
16678
|
migrateAppsPrefix(sqlite);
|
|
16679
|
+
migrateAppsKnowledge(sqlite);
|
|
16673
16680
|
migrateCaseScripts(sqlite);
|
|
16674
16681
|
migrateRunExecutionMode(sqlite);
|
|
16682
|
+
migrateRunScreenMode(sqlite);
|
|
16675
16683
|
migrateRunCommands(sqlite);
|
|
16684
|
+
migrateRunStepPhases(sqlite);
|
|
16676
16685
|
}
|
|
16677
16686
|
function tableColumns(sqlite, table) {
|
|
16678
16687
|
const rows = sqlite.query(`PRAGMA table_info(${table})`).all();
|
|
@@ -16701,6 +16710,12 @@ function migrateRunExecutionMode(sqlite) {
|
|
|
16701
16710
|
addColumnIfMissing(sqlite, "run_tests", "execution_mode", "execution_mode TEXT", testCols);
|
|
16702
16711
|
}
|
|
16703
16712
|
}
|
|
16713
|
+
function migrateRunScreenMode(sqlite) {
|
|
16714
|
+
const runCols = tableColumns(sqlite, "runs");
|
|
16715
|
+
if (runCols.size > 0) {
|
|
16716
|
+
addColumnIfMissing(sqlite, "runs", "screen_mode", "screen_mode TEXT NOT NULL DEFAULT 'tree'", runCols);
|
|
16717
|
+
}
|
|
16718
|
+
}
|
|
16704
16719
|
function migrateRunCommands(sqlite) {
|
|
16705
16720
|
const testCols = tableColumns(sqlite, "run_tests");
|
|
16706
16721
|
if (testCols.size > 0) {
|
|
@@ -16711,6 +16726,12 @@ function migrateRunCommands(sqlite) {
|
|
|
16711
16726
|
addColumnIfMissing(sqlite, "run_steps", "command", "command TEXT", stepCols);
|
|
16712
16727
|
}
|
|
16713
16728
|
}
|
|
16729
|
+
function migrateRunStepPhases(sqlite) {
|
|
16730
|
+
const stepCols = tableColumns(sqlite, "run_steps");
|
|
16731
|
+
if (stepCols.size === 0)
|
|
16732
|
+
return;
|
|
16733
|
+
addColumnIfMissing(sqlite, "run_steps", "phases_json", "phases_json TEXT", stepCols);
|
|
16734
|
+
}
|
|
16714
16735
|
function migrateProvidersTable(sqlite) {
|
|
16715
16736
|
const existing = tableColumns(sqlite, "providers");
|
|
16716
16737
|
if (existing.size === 0)
|
|
@@ -16793,6 +16814,12 @@ function migrateAppsPrefix(sqlite) {
|
|
|
16793
16814
|
sqlite.run("UPDATE apps SET prefix = ? WHERE id = ?", [candidate, row.id]);
|
|
16794
16815
|
}
|
|
16795
16816
|
}
|
|
16817
|
+
function migrateAppsKnowledge(sqlite) {
|
|
16818
|
+
const existing = tableColumns(sqlite, "apps");
|
|
16819
|
+
if (existing.size === 0)
|
|
16820
|
+
return;
|
|
16821
|
+
addColumnIfMissing(sqlite, "apps", "knowledge", "knowledge TEXT NOT NULL DEFAULT ''", existing);
|
|
16822
|
+
}
|
|
16796
16823
|
function getCatalogDbPath() {
|
|
16797
16824
|
return process.env.YOQA_DB_PATH ?? DEFAULT_DB_PATH;
|
|
16798
16825
|
}
|
|
@@ -16816,7 +16843,7 @@ function getCatalogDb() {
|
|
|
16816
16843
|
// package.json
|
|
16817
16844
|
var package_default = {
|
|
16818
16845
|
name: "@yoqa/runner",
|
|
16819
|
-
version: "0.3.
|
|
16846
|
+
version: "0.3.28",
|
|
16820
16847
|
description: "Yoqa local runner \u2014 Appium + catalog HTTP API (Bun). Start with yoqa serve or the yoqa-runner bin.",
|
|
16821
16848
|
license: "UNLICENSED",
|
|
16822
16849
|
type: "module",
|
|
@@ -16836,7 +16863,7 @@ var package_default = {
|
|
|
16836
16863
|
url: "git+https://github.com/Aris-ngoy/qa-agent.git",
|
|
16837
16864
|
directory: "services/runner"
|
|
16838
16865
|
},
|
|
16839
|
-
homepage: "https://yoqa.
|
|
16866
|
+
homepage: "https://yoqa.arisn.dev/docs/cli",
|
|
16840
16867
|
bugs: {
|
|
16841
16868
|
url: "https://github.com/Aris-ngoy/qa-agent/issues"
|
|
16842
16869
|
},
|
|
@@ -16912,8 +16939,8 @@ function abortAllMjpegProxies() {
|
|
|
16912
16939
|
// src/domains/devices/session.ts
|
|
16913
16940
|
import { mkdir as mkdir3 } from "fs/promises";
|
|
16914
16941
|
import { createServer as createServer2 } from "net";
|
|
16915
|
-
import { homedir as
|
|
16916
|
-
import { join as
|
|
16942
|
+
import { homedir as homedir7 } from "os";
|
|
16943
|
+
import { join as join8 } from "path";
|
|
16917
16944
|
import { remote } from "webdriverio";
|
|
16918
16945
|
|
|
16919
16946
|
// src/domains/devices/android-gestures.ts
|
|
@@ -17626,10 +17653,60 @@ async function typeText(browser, text2) {
|
|
|
17626
17653
|
return "w3c";
|
|
17627
17654
|
}
|
|
17628
17655
|
|
|
17656
|
+
// src/domains/devices/screenshot-retention.ts
|
|
17657
|
+
import { readdir as readdir2, stat as stat2, unlink } from "fs/promises";
|
|
17658
|
+
import { homedir as homedir6 } from "os";
|
|
17659
|
+
import { join as join7 } from "path";
|
|
17660
|
+
var SCREENSHOT_DIR = join7(homedir6(), ".yoqa", "runs", "screenshots");
|
|
17661
|
+
var SHOT_FILE_RE = /^shot_\d+_[0-9a-f-]+\.png$/i;
|
|
17662
|
+
var DEFAULT_RETENTION_DAYS = 7;
|
|
17663
|
+
var DAY_MS = 24 * 60 * 60 * 1000;
|
|
17664
|
+
function selectScreenshotsForPruning(entries, options) {
|
|
17665
|
+
if (options.maxAgeMs <= 0)
|
|
17666
|
+
return [];
|
|
17667
|
+
const cutoff = options.now - options.maxAgeMs;
|
|
17668
|
+
return entries.filter((entry) => SHOT_FILE_RE.test(entry.name) && entry.mtimeMs < cutoff).map((entry) => entry.name);
|
|
17669
|
+
}
|
|
17670
|
+
function resolveScreenshotRetentionMs(env = process.env) {
|
|
17671
|
+
const raw = env.YOQA_SCREENSHOT_RETENTION_DAYS;
|
|
17672
|
+
if (raw === undefined)
|
|
17673
|
+
return DEFAULT_RETENTION_DAYS * DAY_MS;
|
|
17674
|
+
const days = Number(raw);
|
|
17675
|
+
if (!Number.isFinite(days))
|
|
17676
|
+
return DEFAULT_RETENTION_DAYS * DAY_MS;
|
|
17677
|
+
return days * DAY_MS;
|
|
17678
|
+
}
|
|
17679
|
+
async function pruneOldRunScreenshots(options = {}) {
|
|
17680
|
+
const maxAgeMs = options.maxAgeMs ?? resolveScreenshotRetentionMs();
|
|
17681
|
+
if (maxAgeMs <= 0)
|
|
17682
|
+
return { removed: 0 };
|
|
17683
|
+
const dir = options.dir ?? SCREENSHOT_DIR;
|
|
17684
|
+
try {
|
|
17685
|
+
const names = (await readdir2(dir)).filter((name) => SHOT_FILE_RE.test(name));
|
|
17686
|
+
const entries = [];
|
|
17687
|
+
for (const name of names) {
|
|
17688
|
+
const info = await stat2(join7(dir, name));
|
|
17689
|
+
entries.push({ name, mtimeMs: info.mtimeMs });
|
|
17690
|
+
}
|
|
17691
|
+
let removed = 0;
|
|
17692
|
+
for (const name of selectScreenshotsForPruning(entries, {
|
|
17693
|
+
maxAgeMs,
|
|
17694
|
+
now: options.now ?? Date.now()
|
|
17695
|
+
})) {
|
|
17696
|
+
try {
|
|
17697
|
+
await unlink(join7(dir, name));
|
|
17698
|
+
removed += 1;
|
|
17699
|
+
} catch {}
|
|
17700
|
+
}
|
|
17701
|
+
return { removed };
|
|
17702
|
+
} catch {
|
|
17703
|
+
return { removed: 0 };
|
|
17704
|
+
}
|
|
17705
|
+
}
|
|
17706
|
+
|
|
17629
17707
|
// src/domains/devices/session.ts
|
|
17630
|
-
var YOQA_ROOT4 =
|
|
17708
|
+
var YOQA_ROOT4 = join8(homedir7(), ".yoqa");
|
|
17631
17709
|
var DEFAULT_MJPEG_PORT = Number(process.env.YOQA_MJPEG_PORT ?? "9100");
|
|
17632
|
-
var SCREENSHOT_DIR = join7(YOQA_ROOT4, "runs", "screenshots");
|
|
17633
17710
|
var MJPEG_SETTINGS_BASE = {
|
|
17634
17711
|
mjpegServerScreenshotQuality: 35,
|
|
17635
17712
|
mjpegScalingFactor: 50
|
|
@@ -17747,7 +17824,7 @@ function looksLikePhysicalIosUdid(udid) {
|
|
|
17747
17824
|
}
|
|
17748
17825
|
var PHYSICAL_IOS_SESSION_TIMEOUT_MS = 60000;
|
|
17749
17826
|
var SIMULATOR_WDA_SESSION_TIMEOUT_MS = 600000;
|
|
17750
|
-
var SIMULATOR_WDA_DERIVED_DATA =
|
|
17827
|
+
var SIMULATOR_WDA_DERIVED_DATA = join8(YOQA_ROOT4, "wda-sim");
|
|
17751
17828
|
function iosSessionCreateTimeoutMs(deviceId) {
|
|
17752
17829
|
return looksLikePhysicalIosUdid(deviceId) ? PHYSICAL_IOS_SESSION_TIMEOUT_MS : SIMULATOR_WDA_SESSION_TIMEOUT_MS;
|
|
17753
17830
|
}
|
|
@@ -18092,7 +18169,7 @@ async function createDeviceSession(options) {
|
|
|
18092
18169
|
const screenshot = async () => {
|
|
18093
18170
|
await mkdir3(SCREENSHOT_DIR, { recursive: true });
|
|
18094
18171
|
const frame = await captureFrame();
|
|
18095
|
-
const path =
|
|
18172
|
+
const path = join8(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
|
|
18096
18173
|
await Bun.write(path, Uint8Array.from(Buffer.from(frame.base64, "base64")));
|
|
18097
18174
|
return { path, base64: frame.base64 };
|
|
18098
18175
|
};
|
|
@@ -24287,6 +24364,7 @@ var catalogAppSchema = exports_external.object({
|
|
|
24287
24364
|
name: exports_external.string().min(1),
|
|
24288
24365
|
prefix: exports_external.string().min(1),
|
|
24289
24366
|
context: exports_external.string(),
|
|
24367
|
+
knowledge: exports_external.string(),
|
|
24290
24368
|
iosBundleId: exports_external.string(),
|
|
24291
24369
|
iosAppStoreId: exports_external.string(),
|
|
24292
24370
|
androidApplicationId: exports_external.string(),
|
|
@@ -24302,6 +24380,7 @@ var updateAppRequestSchema = exports_external.object({
|
|
|
24302
24380
|
name: exports_external.string().min(1).optional(),
|
|
24303
24381
|
prefix: exports_external.string().min(1).optional(),
|
|
24304
24382
|
context: exports_external.string().optional(),
|
|
24383
|
+
knowledge: exports_external.string().optional(),
|
|
24305
24384
|
iosBundleId: exports_external.string().optional(),
|
|
24306
24385
|
iosAppStoreId: exports_external.string().optional(),
|
|
24307
24386
|
androidApplicationId: exports_external.string().optional(),
|
|
@@ -24615,7 +24694,8 @@ var probeProviderResponseSchema = exports_external.object({
|
|
|
24615
24694
|
var providerModelSchema = exports_external.object({
|
|
24616
24695
|
id: exports_external.string().min(1),
|
|
24617
24696
|
name: exports_external.string().min(1),
|
|
24618
|
-
provider: exports_external.string().min(1).optional()
|
|
24697
|
+
provider: exports_external.string().min(1).optional(),
|
|
24698
|
+
vision: exports_external.boolean().optional()
|
|
24619
24699
|
});
|
|
24620
24700
|
var listProviderModelsResponseSchema = exports_external.object({
|
|
24621
24701
|
models: exports_external.array(providerModelSchema),
|
|
@@ -24644,6 +24724,7 @@ var runExecutionModeSchema = exports_external.union([
|
|
|
24644
24724
|
exports_external.literal("script"),
|
|
24645
24725
|
exports_external.literal("agent")
|
|
24646
24726
|
]);
|
|
24727
|
+
var runScreenModeSchema = exports_external.union([exports_external.literal("vision"), exports_external.literal("tree")]);
|
|
24647
24728
|
var createRunRequestSchema = exports_external.object({
|
|
24648
24729
|
appId: exports_external.string().min(1),
|
|
24649
24730
|
caseIds: exports_external.array(exports_external.string().min(1)).min(1),
|
|
@@ -24651,7 +24732,17 @@ var createRunRequestSchema = exports_external.object({
|
|
|
24651
24732
|
platform: devicePlatformSchema,
|
|
24652
24733
|
buildId: exports_external.string().min(1).optional(),
|
|
24653
24734
|
buildPath: exports_external.string().min(1).optional(),
|
|
24654
|
-
executionMode: runExecutionModeSchema.optional()
|
|
24735
|
+
executionMode: runExecutionModeSchema.optional(),
|
|
24736
|
+
screenMode: runScreenModeSchema.optional()
|
|
24737
|
+
});
|
|
24738
|
+
var stepPhasesSchema = exports_external.object({
|
|
24739
|
+
captureMs: exports_external.number().nonnegative(),
|
|
24740
|
+
screenMs: exports_external.number().nonnegative(),
|
|
24741
|
+
prepareMs: exports_external.number().nonnegative(),
|
|
24742
|
+
decideMs: exports_external.number().nonnegative(),
|
|
24743
|
+
actionMs: exports_external.number().nonnegative(),
|
|
24744
|
+
settleMs: exports_external.number().nonnegative(),
|
|
24745
|
+
decideRetries: exports_external.number().int().nonnegative()
|
|
24655
24746
|
});
|
|
24656
24747
|
var runStepSchema = exports_external.object({
|
|
24657
24748
|
id: exports_external.string().min(1),
|
|
@@ -24661,6 +24752,7 @@ var runStepSchema = exports_external.object({
|
|
|
24661
24752
|
screenshotUri: exports_external.string().nullable(),
|
|
24662
24753
|
ok: exports_external.boolean(),
|
|
24663
24754
|
latencyMs: exports_external.number().nonnegative(),
|
|
24755
|
+
phases: stepPhasesSchema.nullable().optional(),
|
|
24664
24756
|
detail: exports_external.string().nullable(),
|
|
24665
24757
|
command: exports_external.string().nullable().optional(),
|
|
24666
24758
|
createdAt: exports_external.number().int().nonnegative()
|
|
@@ -24685,6 +24777,7 @@ var runSchema = exports_external.object({
|
|
|
24685
24777
|
buildId: exports_external.string().nullable(),
|
|
24686
24778
|
status: runStatusSchema,
|
|
24687
24779
|
executionMode: runExecutionModeSchema,
|
|
24780
|
+
screenMode: runScreenModeSchema.optional(),
|
|
24688
24781
|
error: exports_external.string().nullable(),
|
|
24689
24782
|
createdAt: exports_external.number().int().nonnegative(),
|
|
24690
24783
|
startedAt: exports_external.number().int().nonnegative().nullable(),
|
|
@@ -24771,7 +24864,16 @@ var actionRequestSchema = exports_external.object({
|
|
|
24771
24864
|
appId: exports_external.string().optional(),
|
|
24772
24865
|
url: exports_external.string().optional(),
|
|
24773
24866
|
alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).optional(),
|
|
24774
|
-
seconds: exports_external.number().optional()
|
|
24867
|
+
seconds: exports_external.number().optional(),
|
|
24868
|
+
screenshot: exports_external.boolean().optional(),
|
|
24869
|
+
settleMs: exports_external.number().int().min(0).max(1e4).optional()
|
|
24870
|
+
});
|
|
24871
|
+
var actionResultScreenshotSchema = exports_external.object({
|
|
24872
|
+
path: exports_external.string().min(1),
|
|
24873
|
+
annotatedPath: exports_external.string().min(1).optional(),
|
|
24874
|
+
settled: exports_external.boolean(),
|
|
24875
|
+
waitedMs: exports_external.number(),
|
|
24876
|
+
changed: exports_external.boolean().nullable()
|
|
24775
24877
|
});
|
|
24776
24878
|
var actionResponseSchema = exports_external.object({
|
|
24777
24879
|
ok: exports_external.literal(true),
|
|
@@ -24779,7 +24881,8 @@ var actionResponseSchema = exports_external.object({
|
|
|
24779
24881
|
resolved: exports_external.object({
|
|
24780
24882
|
x: exports_external.number().optional(),
|
|
24781
24883
|
y: exports_external.number().optional()
|
|
24782
|
-
}).optional()
|
|
24884
|
+
}).optional(),
|
|
24885
|
+
screenshot: actionResultScreenshotSchema.optional()
|
|
24783
24886
|
});
|
|
24784
24887
|
var yoqaStatusResponseSchema = exports_external.object({
|
|
24785
24888
|
runner: exports_external.object({
|
|
@@ -25353,9 +25456,9 @@ function normalizeVisionJson(value) {
|
|
|
25353
25456
|
|
|
25354
25457
|
// src/domains/providers/vision-model.ts
|
|
25355
25458
|
import { mkdtemp as mkdtemp3, readFile as readFile2, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
25356
|
-
import { homedir as
|
|
25459
|
+
import { homedir as homedir8 } from "os";
|
|
25357
25460
|
import { tmpdir as tmpdir3 } from "os";
|
|
25358
|
-
import { join as
|
|
25461
|
+
import { join as join9 } from "path";
|
|
25359
25462
|
|
|
25360
25463
|
// ../../node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider/dist/index.js
|
|
25361
25464
|
var marker = "vercel.ai.error";
|
|
@@ -48027,9 +48130,9 @@ async function prepareVisionImage(pngBase64) {
|
|
|
48027
48130
|
if (process.platform !== "darwin") {
|
|
48028
48131
|
return { base64: pngBase64, mediaType: "image/png" };
|
|
48029
48132
|
}
|
|
48030
|
-
const dir = await mkdtemp3(
|
|
48031
|
-
const inPath =
|
|
48032
|
-
const outPath =
|
|
48133
|
+
const dir = await mkdtemp3(join9(tmpdir3(), "yoqa-vision-"));
|
|
48134
|
+
const inPath = join9(dir, "shot.png");
|
|
48135
|
+
const outPath = join9(dir, "shot.jpg");
|
|
48033
48136
|
try {
|
|
48034
48137
|
await writeFile3(inPath, new Uint8Array(bytes));
|
|
48035
48138
|
const proc = Bun.spawn([
|
|
@@ -48083,6 +48186,9 @@ function formatProviderHttpError(label, status, body) {
|
|
|
48083
48186
|
if (label === "OpenCode" && status >= 500 && (body.includes("Internal server error") || body.includes('"type":"error"'))) {
|
|
48084
48187
|
return `OpenCode Zen returned an internal error for this vision request. Many free models reject screenshots. In Settings \u2192 Provider, set the default model to ${EXAMPLE_OPENCODE_VISION_MODEL} (or another vision-capable model).`;
|
|
48085
48188
|
}
|
|
48189
|
+
if (label === "Groq" && status === 400 && (body.includes("invalid JSON schema for response_format") || body.includes("required") && body.includes("every key in properties"))) {
|
|
48190
|
+
return "Groq rejected strict JSON schema mode for this model (response_format requires every key in properties). The runner already compensates with prompt JSON \u2014 retry the run; if it persists, pick a different Groq model in Settings \u2192 Provider.";
|
|
48191
|
+
}
|
|
48086
48192
|
return `${label} request failed (${status}): ${body.slice(0, 400)}`;
|
|
48087
48193
|
}
|
|
48088
48194
|
function isJsonRepairableError(error40) {
|
|
@@ -48137,7 +48243,7 @@ async function completeWithAiSdk(input) {
|
|
|
48137
48243
|
model: input.model,
|
|
48138
48244
|
schema: input.schema,
|
|
48139
48245
|
system: input.system,
|
|
48140
|
-
maxOutputTokens: VISION_MAX_TOKENS,
|
|
48246
|
+
maxOutputTokens: input.maxOutputTokens ?? VISION_MAX_TOKENS,
|
|
48141
48247
|
experimental_repairText: async ({ text: text3 }) => salvageAgentJsonText(text3),
|
|
48142
48248
|
messages: [
|
|
48143
48249
|
{
|
|
@@ -48175,6 +48281,7 @@ async function completeWithAiSdk(input) {
|
|
|
48175
48281
|
if (!(error40 instanceof AgentProviderError) || !isJsonRepairableError(error40)) {
|
|
48176
48282
|
throw error40;
|
|
48177
48283
|
}
|
|
48284
|
+
input.onDecideRetry?.();
|
|
48178
48285
|
return run(`${input.prompt}
|
|
48179
48286
|
|
|
48180
48287
|
${JSON_REPAIR_PROMPT}`);
|
|
@@ -48184,7 +48291,7 @@ function createSdkVisionPort(opts) {
|
|
|
48184
48291
|
return {
|
|
48185
48292
|
async completeObject(input) {
|
|
48186
48293
|
const modelId = input.auth.defaultModel?.trim() || opts.defaultModel;
|
|
48187
|
-
const image = await prepareVisionImage(input.imageBase64);
|
|
48294
|
+
const image = input.image ?? await prepareVisionImage(input.imageBase64);
|
|
48188
48295
|
const model = await opts.createModel(input.auth, modelId);
|
|
48189
48296
|
return completeWithAiSdk({
|
|
48190
48297
|
label: opts.label,
|
|
@@ -48192,7 +48299,9 @@ function createSdkVisionPort(opts) {
|
|
|
48192
48299
|
schema: input.schema,
|
|
48193
48300
|
system: input.system,
|
|
48194
48301
|
prompt: input.prompt,
|
|
48195
|
-
image
|
|
48302
|
+
image,
|
|
48303
|
+
onDecideRetry: input.onDecideRetry,
|
|
48304
|
+
maxOutputTokens: opts.maxOutputTokens
|
|
48196
48305
|
});
|
|
48197
48306
|
}
|
|
48198
48307
|
};
|
|
@@ -48204,8 +48313,8 @@ function openCodeAuthPaths() {
|
|
|
48204
48313
|
const paths = [];
|
|
48205
48314
|
const xdg = process.env.XDG_DATA_HOME?.trim();
|
|
48206
48315
|
if (xdg)
|
|
48207
|
-
paths.push(
|
|
48208
|
-
paths.push(
|
|
48316
|
+
paths.push(join9(xdg, "opencode", "auth.json"));
|
|
48317
|
+
paths.push(join9(homedir8(), ".local", "share", "opencode", "auth.json"));
|
|
48209
48318
|
return paths;
|
|
48210
48319
|
}
|
|
48211
48320
|
function apiKeyFromOpenCodeAuthEntry(value) {
|
|
@@ -48298,6 +48407,64 @@ function resolveGoogleKey(auth) {
|
|
|
48298
48407
|
function resolveVertexApiKey(auth) {
|
|
48299
48408
|
return auth.apiKey?.trim() || auth.env.GOOGLE_VERTEX_API_KEY?.trim() || process.env.GOOGLE_VERTEX_API_KEY?.trim() || null;
|
|
48300
48409
|
}
|
|
48410
|
+
function isSparseResponseFormatBody(parsedBody) {
|
|
48411
|
+
const format = parsedBody.response_format;
|
|
48412
|
+
if (!format || typeof format !== "object" || Array.isArray(format))
|
|
48413
|
+
return false;
|
|
48414
|
+
const jsonSchema3 = format.json_schema;
|
|
48415
|
+
if (!jsonSchema3 || typeof jsonSchema3 !== "object" || Array.isArray(jsonSchema3)) {
|
|
48416
|
+
return false;
|
|
48417
|
+
}
|
|
48418
|
+
const schema = jsonSchema3.schema;
|
|
48419
|
+
if (!schema || typeof schema !== "object" || Array.isArray(schema))
|
|
48420
|
+
return false;
|
|
48421
|
+
const properties = schema.properties;
|
|
48422
|
+
if (!properties || typeof properties !== "object" || Array.isArray(properties)) {
|
|
48423
|
+
return false;
|
|
48424
|
+
}
|
|
48425
|
+
const keys = Object.keys(properties);
|
|
48426
|
+
if (keys.length === 0)
|
|
48427
|
+
return false;
|
|
48428
|
+
const required2 = schema.required;
|
|
48429
|
+
if (!Array.isArray(required2))
|
|
48430
|
+
return true;
|
|
48431
|
+
const requiredSet = new Set(required2.filter((entry) => typeof entry === "string"));
|
|
48432
|
+
return keys.some((key) => !requiredSet.has(key));
|
|
48433
|
+
}
|
|
48434
|
+
var GROQ_REASONING_MODEL_RE = /qwen3/i;
|
|
48435
|
+
function rewriteGroqRequestBody(rawBody) {
|
|
48436
|
+
try {
|
|
48437
|
+
const parsed = JSON.parse(rawBody);
|
|
48438
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
|
|
48439
|
+
return null;
|
|
48440
|
+
const parsedBody = { ...parsed };
|
|
48441
|
+
let mutated = false;
|
|
48442
|
+
if (typeof parsedBody.model === "string" && GROQ_REASONING_MODEL_RE.test(parsedBody.model)) {
|
|
48443
|
+
parsedBody.reasoning_effort = "none";
|
|
48444
|
+
mutated = true;
|
|
48445
|
+
}
|
|
48446
|
+
if (isSparseResponseFormatBody(parsedBody)) {
|
|
48447
|
+
const { response_format: _responseFormat, ...withoutFormat } = parsedBody;
|
|
48448
|
+
return JSON.stringify(withoutFormat);
|
|
48449
|
+
}
|
|
48450
|
+
return mutated ? JSON.stringify(parsedBody) : null;
|
|
48451
|
+
} catch {
|
|
48452
|
+
return null;
|
|
48453
|
+
}
|
|
48454
|
+
}
|
|
48455
|
+
function withGroqRequestHooks(opts) {
|
|
48456
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
48457
|
+
return async (input, init) => {
|
|
48458
|
+
const headers = new Headers(init?.headers);
|
|
48459
|
+
const requestInit = { ...init, headers };
|
|
48460
|
+
if (typeof init?.body === "string") {
|
|
48461
|
+
const rewrittenBody = rewriteGroqRequestBody(init.body);
|
|
48462
|
+
if (rewrittenBody !== null)
|
|
48463
|
+
requestInit.body = rewrittenBody;
|
|
48464
|
+
}
|
|
48465
|
+
return fetchImpl(input, requestInit);
|
|
48466
|
+
};
|
|
48467
|
+
}
|
|
48301
48468
|
function withOpenCodeRequestHooks(opts) {
|
|
48302
48469
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
48303
48470
|
return async (input, init) => {
|
|
@@ -57358,8 +57525,8 @@ var anthropic = createAnthropic();
|
|
|
57358
57525
|
// src/domains/providers/drivers/probe.ts
|
|
57359
57526
|
import { constants } from "fs";
|
|
57360
57527
|
import { access } from "fs/promises";
|
|
57361
|
-
import { homedir as
|
|
57362
|
-
import { isAbsolute, join as
|
|
57528
|
+
import { homedir as homedir9 } from "os";
|
|
57529
|
+
import { isAbsolute, join as join10 } from "path";
|
|
57363
57530
|
async function pathExists3(path) {
|
|
57364
57531
|
try {
|
|
57365
57532
|
await access(path, constants.F_OK);
|
|
@@ -57384,13 +57551,13 @@ async function which2(binary) {
|
|
|
57384
57551
|
`)[0]?.trim();
|
|
57385
57552
|
return resolved || null;
|
|
57386
57553
|
}
|
|
57387
|
-
function knownHomeBinCandidates(binaryName, home =
|
|
57554
|
+
function knownHomeBinCandidates(binaryName, home = homedir9()) {
|
|
57388
57555
|
return [
|
|
57389
|
-
|
|
57390
|
-
|
|
57391
|
-
|
|
57392
|
-
|
|
57393
|
-
|
|
57556
|
+
join10(home, ".opencode", "bin", binaryName),
|
|
57557
|
+
join10(home, ".grok", "bin", binaryName),
|
|
57558
|
+
join10(home, ".antigravity", "antigravity", "bin", binaryName),
|
|
57559
|
+
join10(home, ".local", "bin", binaryName),
|
|
57560
|
+
join10(home, ".bun", "bin", binaryName),
|
|
57394
57561
|
`/opt/homebrew/bin/${binaryName}`,
|
|
57395
57562
|
`/usr/local/bin/${binaryName}`
|
|
57396
57563
|
];
|
|
@@ -57623,7 +57790,7 @@ var anthropicDriver = {
|
|
|
57623
57790
|
// src/domains/providers/drivers/antigravity-vision.ts
|
|
57624
57791
|
import { mkdtemp as mkdtemp4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
57625
57792
|
import { tmpdir as tmpdir4 } from "os";
|
|
57626
|
-
import { join as
|
|
57793
|
+
import { join as join11 } from "path";
|
|
57627
57794
|
|
|
57628
57795
|
// ../../node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider/dist/index.js
|
|
57629
57796
|
var marker29 = "vercel.ai.error";
|
|
@@ -67096,11 +67263,11 @@ async function completeWithAgyCli(input, repairHint) {
|
|
|
67096
67263
|
if (!resolved.path) {
|
|
67097
67264
|
throw new AgentProviderError(resolved.detail || "Antigravity CLI (`agy`) not found. Install it or paste a Google AI Studio API key.");
|
|
67098
67265
|
}
|
|
67099
|
-
const image = await prepareVisionImage(input.imageBase64);
|
|
67266
|
+
const image = input.image ?? await prepareVisionImage(input.imageBase64);
|
|
67100
67267
|
const model = input.auth.defaultModel?.trim() || ANTIGRAVITY_DEFAULT_VISION_MODEL;
|
|
67101
67268
|
const ext = image.mediaType === "image/jpeg" ? "jpg" : "png";
|
|
67102
|
-
const dir = await mkdtemp4(
|
|
67103
|
-
const shotPath =
|
|
67269
|
+
const dir = await mkdtemp4(join11(tmpdir4(), "yoqa-agy-"));
|
|
67270
|
+
const shotPath = join11(dir, `shot.${ext}`);
|
|
67104
67271
|
try {
|
|
67105
67272
|
await writeFile4(shotPath, new Uint8Array(Buffer.from(image.base64, "base64")));
|
|
67106
67273
|
const userText = [
|
|
@@ -67151,7 +67318,7 @@ var antigravityVision = {
|
|
|
67151
67318
|
if (apiKey) {
|
|
67152
67319
|
const requested = input.auth.defaultModel?.trim() || ANTIGRAVITY_DEFAULT_VISION_MODEL;
|
|
67153
67320
|
const googleModelId = requested.startsWith("gemini-1.") || requested.startsWith("gemini-2.") ? requested : "gemini-2.5-flash";
|
|
67154
|
-
const image = await prepareVisionImage(input.imageBase64);
|
|
67321
|
+
const image = input.image ?? await prepareVisionImage(input.imageBase64);
|
|
67155
67322
|
const provider = createGoogle({ apiKey });
|
|
67156
67323
|
return completeWithAiSdk({
|
|
67157
67324
|
label: "Antigravity",
|
|
@@ -67159,7 +67326,8 @@ var antigravityVision = {
|
|
|
67159
67326
|
schema: input.schema,
|
|
67160
67327
|
system: input.system,
|
|
67161
67328
|
prompt: input.prompt,
|
|
67162
|
-
image
|
|
67329
|
+
image,
|
|
67330
|
+
onDecideRetry: input.onDecideRetry
|
|
67163
67331
|
});
|
|
67164
67332
|
}
|
|
67165
67333
|
try {
|
|
@@ -67168,6 +67336,7 @@ var antigravityVision = {
|
|
|
67168
67336
|
if (!(error40 instanceof AgentProviderError) || !isJsonRepairableError(error40)) {
|
|
67169
67337
|
throw error40;
|
|
67170
67338
|
}
|
|
67339
|
+
input.onDecideRetry?.();
|
|
67171
67340
|
return completeWithAgyCli(input, JSON_REPAIR_PROMPT2);
|
|
67172
67341
|
}
|
|
67173
67342
|
}
|
|
@@ -75614,7 +75783,7 @@ ${result.stderr}`.toLowerCase();
|
|
|
75614
75783
|
// src/domains/providers/drivers/cursor-vision.ts
|
|
75615
75784
|
import { mkdtemp as mkdtemp5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
|
|
75616
75785
|
import { tmpdir as tmpdir5 } from "os";
|
|
75617
|
-
import { join as
|
|
75786
|
+
import { join as join12 } from "path";
|
|
75618
75787
|
var CURSOR_DEFAULT_VISION_MODEL = "auto";
|
|
75619
75788
|
var JSON_REPAIR_PROMPT3 = "Your previous reply was not valid JSON for this task. Reply again with ONLY one strict JSON object using double quotes for every key and string (no single quotes, no markdown, no prose).";
|
|
75620
75789
|
function resolveCursorApiKey(auth) {
|
|
@@ -75627,8 +75796,8 @@ async function invokeCursorPrint(input) {
|
|
|
75627
75796
|
}
|
|
75628
75797
|
const model = input.auth.defaultModel?.trim() || CURSOR_DEFAULT_VISION_MODEL;
|
|
75629
75798
|
const ext = input.mediaType === "image/jpeg" ? "jpg" : "png";
|
|
75630
|
-
const dir = await mkdtemp5(
|
|
75631
|
-
const shotPath =
|
|
75799
|
+
const dir = await mkdtemp5(join12(tmpdir5(), "yoqa-cursor-"));
|
|
75800
|
+
const shotPath = join12(dir, `shot.${ext}`);
|
|
75632
75801
|
const apiKey = resolveCursorApiKey(input.auth);
|
|
75633
75802
|
try {
|
|
75634
75803
|
await writeFile5(shotPath, new Uint8Array(Buffer.from(input.imageBase64, "base64")));
|
|
@@ -75671,7 +75840,7 @@ ${result.stderr}`.trim();
|
|
|
75671
75840
|
}
|
|
75672
75841
|
}
|
|
75673
75842
|
async function completeOnce(input, repairHint) {
|
|
75674
|
-
const image = await prepareVisionImage(input.imageBase64);
|
|
75843
|
+
const image = input.image ?? await prepareVisionImage(input.imageBase64);
|
|
75675
75844
|
const ext = image.mediaType === "image/jpeg" ? "jpg" : "png";
|
|
75676
75845
|
const shotName = `shot.${ext}`;
|
|
75677
75846
|
const userText = [
|
|
@@ -75710,6 +75879,7 @@ var cursorVision = {
|
|
|
75710
75879
|
if (!(error40 instanceof AgentProviderError) || !isJsonRepairableError(error40)) {
|
|
75711
75880
|
throw error40;
|
|
75712
75881
|
}
|
|
75882
|
+
input.onDecideRetry?.();
|
|
75713
75883
|
return completeOnce(input, JSON_REPAIR_PROMPT3);
|
|
75714
75884
|
}
|
|
75715
75885
|
}
|
|
@@ -95601,6 +95771,18 @@ var groq = createGroq();
|
|
|
95601
95771
|
|
|
95602
95772
|
// src/domains/providers/drivers/groq.ts
|
|
95603
95773
|
var DEFAULT_BASE2 = "https://api.groq.com/openai/v1";
|
|
95774
|
+
var GROQ_VISION_MODEL_RE = /llama-4-(scout|maverick)/i;
|
|
95775
|
+
var GROQ_TEXT_ONLY_MODEL_RE = /^(qwen|deepseek|llama-3|gemma|mixtral|whisper|openai\/gpt-oss|compound-beta)/i;
|
|
95776
|
+
function groqModelVision(modelId) {
|
|
95777
|
+
const id = modelId.trim();
|
|
95778
|
+
if (!id)
|
|
95779
|
+
return;
|
|
95780
|
+
if (GROQ_VISION_MODEL_RE.test(id))
|
|
95781
|
+
return true;
|
|
95782
|
+
if (GROQ_TEXT_ONLY_MODEL_RE.test(id))
|
|
95783
|
+
return false;
|
|
95784
|
+
return;
|
|
95785
|
+
}
|
|
95604
95786
|
var groqDriver = {
|
|
95605
95787
|
kind: "groq",
|
|
95606
95788
|
label: "Groq",
|
|
@@ -95613,13 +95795,14 @@ var groqDriver = {
|
|
|
95613
95795
|
vision: createSdkVisionPort({
|
|
95614
95796
|
label: "Groq",
|
|
95615
95797
|
defaultModel: "meta-llama/llama-4-scout-17b-16e-instruct",
|
|
95798
|
+
maxOutputTokens: 2048,
|
|
95616
95799
|
createModel: (auth, modelId) => {
|
|
95617
95800
|
const apiKey = resolveGroqKey(auth);
|
|
95618
95801
|
if (!apiKey) {
|
|
95619
95802
|
throw new AgentProviderError("Groq provider has no API key");
|
|
95620
95803
|
}
|
|
95621
95804
|
const baseURL = auth.baseUrl?.trim().replace(/\/$/, "") || DEFAULT_BASE2;
|
|
95622
|
-
return createGroq({ apiKey, baseURL })(modelId);
|
|
95805
|
+
return createGroq({ apiKey, baseURL, fetch: withGroqRequestHooks({}) })(modelId);
|
|
95623
95806
|
}
|
|
95624
95807
|
}),
|
|
95625
95808
|
async probe() {
|
|
@@ -95660,7 +95843,10 @@ var groqDriver = {
|
|
|
95660
95843
|
label: "Groq"
|
|
95661
95844
|
});
|
|
95662
95845
|
return {
|
|
95663
|
-
models: (result.models ?? []).map((id) =>
|
|
95846
|
+
models: (result.models ?? []).map((id) => {
|
|
95847
|
+
const vision = groqModelVision(id);
|
|
95848
|
+
return { id, name: id, ...vision === undefined ? {} : { vision } };
|
|
95849
|
+
}),
|
|
95664
95850
|
message: result.ok ? `${result.models?.length ?? 0} models available` : result.message
|
|
95665
95851
|
};
|
|
95666
95852
|
}
|
|
@@ -95736,8 +95922,8 @@ var openaiDriver = {
|
|
|
95736
95922
|
};
|
|
95737
95923
|
|
|
95738
95924
|
// src/domains/providers/drivers/opencode-config.ts
|
|
95739
|
-
import { homedir as
|
|
95740
|
-
import { join as
|
|
95925
|
+
import { homedir as homedir10 } from "os";
|
|
95926
|
+
import { join as join13 } from "path";
|
|
95741
95927
|
var OPENAI_COMPATIBLE_PACKAGES = new Set([
|
|
95742
95928
|
"@ai-sdk/openai-compatible",
|
|
95743
95929
|
"@ai-sdk/openai",
|
|
@@ -95765,8 +95951,8 @@ function openCodeConfigPaths() {
|
|
|
95765
95951
|
const paths = [];
|
|
95766
95952
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
95767
95953
|
if (xdg)
|
|
95768
|
-
paths.push(
|
|
95769
|
-
paths.push(
|
|
95954
|
+
paths.push(join13(xdg, "opencode", "opencode.json"));
|
|
95955
|
+
paths.push(join13(homedir10(), ".config", "opencode", "opencode.json"));
|
|
95770
95956
|
return paths;
|
|
95771
95957
|
}
|
|
95772
95958
|
function parseOpenCodeCompatibleProvider(config2, providerId) {
|
|
@@ -96224,6 +96410,348 @@ async function completeVision(auth, input) {
|
|
|
96224
96410
|
return resolveVision(auth).completeObject({ ...input, auth });
|
|
96225
96411
|
}
|
|
96226
96412
|
|
|
96413
|
+
// src/domains/runs/coord-grid.ts
|
|
96414
|
+
import { crc32, deflateSync, inflateSync } from "zlib";
|
|
96415
|
+
var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
96416
|
+
function pngChunk(type, data) {
|
|
96417
|
+
const typeAndData = Buffer.concat([Buffer.from(type), data]);
|
|
96418
|
+
const length = Buffer.alloc(4);
|
|
96419
|
+
length.writeUInt32BE(data.length);
|
|
96420
|
+
const crc = Buffer.alloc(4);
|
|
96421
|
+
crc.writeUInt32BE(crc32(typeAndData) >>> 0);
|
|
96422
|
+
return Buffer.concat([length, typeAndData, crc]);
|
|
96423
|
+
}
|
|
96424
|
+
function encodeRgbaPng(image) {
|
|
96425
|
+
const { width, height, rgba } = image;
|
|
96426
|
+
const stride = width * 4;
|
|
96427
|
+
const raw2 = Buffer.alloc(height * (1 + stride));
|
|
96428
|
+
for (let y = 0;y < height; y++) {
|
|
96429
|
+
const row = y * (1 + stride);
|
|
96430
|
+
raw2[row] = 0;
|
|
96431
|
+
Buffer.from(rgba.buffer, rgba.byteOffset + y * stride, stride).copy(raw2, row + 1);
|
|
96432
|
+
}
|
|
96433
|
+
const ihdr = Buffer.alloc(13);
|
|
96434
|
+
ihdr.writeUInt32BE(width, 0);
|
|
96435
|
+
ihdr.writeUInt32BE(height, 4);
|
|
96436
|
+
ihdr[8] = 8;
|
|
96437
|
+
ihdr[9] = 6;
|
|
96438
|
+
return Buffer.concat([
|
|
96439
|
+
PNG_SIGNATURE,
|
|
96440
|
+
pngChunk("IHDR", ihdr),
|
|
96441
|
+
pngChunk("IDAT", deflateSync(raw2)),
|
|
96442
|
+
pngChunk("IEND", Buffer.alloc(0))
|
|
96443
|
+
]);
|
|
96444
|
+
}
|
|
96445
|
+
function paeth(left, up, upLeft) {
|
|
96446
|
+
const estimate = left + up - upLeft;
|
|
96447
|
+
const leftDist = Math.abs(estimate - left);
|
|
96448
|
+
const upDist = Math.abs(estimate - up);
|
|
96449
|
+
const upLeftDist = Math.abs(estimate - upLeft);
|
|
96450
|
+
if (leftDist <= upDist && leftDist <= upLeftDist)
|
|
96451
|
+
return left;
|
|
96452
|
+
if (upDist <= upLeftDist)
|
|
96453
|
+
return up;
|
|
96454
|
+
return upLeft;
|
|
96455
|
+
}
|
|
96456
|
+
function unfilter(raw2, height, stride, bpp) {
|
|
96457
|
+
const out = Buffer.alloc(height * stride);
|
|
96458
|
+
for (let y = 0;y < height; y++) {
|
|
96459
|
+
const filter8 = raw2[y * (1 + stride)];
|
|
96460
|
+
if (filter8 === undefined || filter8 > 4)
|
|
96461
|
+
return null;
|
|
96462
|
+
const src = y * (1 + stride) + 1;
|
|
96463
|
+
const dst = y * stride;
|
|
96464
|
+
for (let x = 0;x < stride; x++) {
|
|
96465
|
+
const value = raw2[src + x] ?? 0;
|
|
96466
|
+
const left = x >= bpp ? out[dst + x - bpp] ?? 0 : 0;
|
|
96467
|
+
const up = y > 0 ? out[dst - stride + x] ?? 0 : 0;
|
|
96468
|
+
const upLeft = y > 0 && x >= bpp ? out[dst - stride + x - bpp] ?? 0 : 0;
|
|
96469
|
+
let recon = value;
|
|
96470
|
+
if (filter8 === 1)
|
|
96471
|
+
recon = value + left;
|
|
96472
|
+
else if (filter8 === 2)
|
|
96473
|
+
recon = value + up;
|
|
96474
|
+
else if (filter8 === 3)
|
|
96475
|
+
recon = value + Math.floor((left + up) / 2);
|
|
96476
|
+
else if (filter8 === 4)
|
|
96477
|
+
recon = value + paeth(left, up, upLeft);
|
|
96478
|
+
out[dst + x] = recon & 255;
|
|
96479
|
+
}
|
|
96480
|
+
}
|
|
96481
|
+
return out;
|
|
96482
|
+
}
|
|
96483
|
+
function decodePng(bytes) {
|
|
96484
|
+
if (bytes.length < 8 || !bytes.subarray(0, 8).equals(PNG_SIGNATURE))
|
|
96485
|
+
return null;
|
|
96486
|
+
let width = 0;
|
|
96487
|
+
let height = 0;
|
|
96488
|
+
let colorType = -1;
|
|
96489
|
+
const idat = [];
|
|
96490
|
+
let offset = 8;
|
|
96491
|
+
while (offset + 8 <= bytes.length) {
|
|
96492
|
+
const length = bytes.readUInt32BE(offset);
|
|
96493
|
+
const type = bytes.toString("ascii", offset + 4, offset + 8);
|
|
96494
|
+
const start = offset + 8;
|
|
96495
|
+
const end = start + length;
|
|
96496
|
+
if (end + 4 > bytes.length)
|
|
96497
|
+
return null;
|
|
96498
|
+
const data = bytes.subarray(start, end);
|
|
96499
|
+
if (type === "IHDR") {
|
|
96500
|
+
if (data.length < 13)
|
|
96501
|
+
return null;
|
|
96502
|
+
width = data.readUInt32BE(0);
|
|
96503
|
+
height = data.readUInt32BE(4);
|
|
96504
|
+
if (data[8] !== 8 || data[12] !== 0)
|
|
96505
|
+
return null;
|
|
96506
|
+
colorType = data[9] ?? -1;
|
|
96507
|
+
} else if (type === "IDAT") {
|
|
96508
|
+
idat.push(Buffer.from(data));
|
|
96509
|
+
} else if (type === "IEND") {
|
|
96510
|
+
break;
|
|
96511
|
+
}
|
|
96512
|
+
offset = end + 4;
|
|
96513
|
+
}
|
|
96514
|
+
if (width < 1 || height < 1 || width > 8192 || height > 8192)
|
|
96515
|
+
return null;
|
|
96516
|
+
const bpp = colorType === 6 ? 4 : colorType === 2 ? 3 : 0;
|
|
96517
|
+
if (bpp === 0)
|
|
96518
|
+
return null;
|
|
96519
|
+
let inflated;
|
|
96520
|
+
try {
|
|
96521
|
+
inflated = inflateSync(Buffer.concat(idat));
|
|
96522
|
+
} catch {
|
|
96523
|
+
return null;
|
|
96524
|
+
}
|
|
96525
|
+
const stride = width * bpp;
|
|
96526
|
+
if (inflated.length < height * (1 + stride))
|
|
96527
|
+
return null;
|
|
96528
|
+
const raw2 = unfilter(inflated, height, stride, bpp);
|
|
96529
|
+
if (!raw2)
|
|
96530
|
+
return null;
|
|
96531
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
96532
|
+
if (bpp === 4) {
|
|
96533
|
+
rgba.set(raw2.subarray(0, rgba.length));
|
|
96534
|
+
} else {
|
|
96535
|
+
for (let i = 0;i < width * height; i++) {
|
|
96536
|
+
rgba[i * 4] = raw2[i * 3] ?? 0;
|
|
96537
|
+
rgba[i * 4 + 1] = raw2[i * 3 + 1] ?? 0;
|
|
96538
|
+
rgba[i * 4 + 2] = raw2[i * 3 + 2] ?? 0;
|
|
96539
|
+
rgba[i * 4 + 3] = 255;
|
|
96540
|
+
}
|
|
96541
|
+
}
|
|
96542
|
+
return { width, height, rgba };
|
|
96543
|
+
}
|
|
96544
|
+
function paintMagenta(rgba, index) {
|
|
96545
|
+
rgba[index] = 255;
|
|
96546
|
+
rgba[index + 1] = 0;
|
|
96547
|
+
rgba[index + 2] = 255;
|
|
96548
|
+
rgba[index + 3] = 255;
|
|
96549
|
+
}
|
|
96550
|
+
var GLYPHS = {
|
|
96551
|
+
"0": ["01110", "10001", "10001", "10001", "10001", "10001", "01110"],
|
|
96552
|
+
"1": ["00100", "01100", "00100", "00100", "00100", "00100", "01110"],
|
|
96553
|
+
"2": ["01110", "10001", "00001", "00110", "01000", "10000", "11111"],
|
|
96554
|
+
"3": ["11110", "00001", "00001", "01110", "00001", "00001", "11110"],
|
|
96555
|
+
"4": ["00010", "00110", "01010", "10010", "11111", "00010", "00010"],
|
|
96556
|
+
"5": ["11111", "10000", "11110", "00001", "00001", "10001", "01110"],
|
|
96557
|
+
"6": ["01110", "10000", "11110", "10001", "10001", "10001", "01110"],
|
|
96558
|
+
"7": ["11111", "00001", "00010", "00100", "01000", "01000", "01000"],
|
|
96559
|
+
"8": ["01110", "10001", "10001", "01110", "10001", "10001", "01110"],
|
|
96560
|
+
"9": ["01110", "10001", "10001", "01111", "00001", "00001", "01110"],
|
|
96561
|
+
"-": ["00000", "00000", "00000", "11111", "00000", "00000", "00000"]
|
|
96562
|
+
};
|
|
96563
|
+
var GRID_SLOT = [5, 15, 25, 35, 45, 55, 65, 75, 85, 95];
|
|
96564
|
+
function clampCell(value, max2) {
|
|
96565
|
+
if (!Number.isFinite(value))
|
|
96566
|
+
return 0;
|
|
96567
|
+
return Math.min(max2, Math.max(0, Math.round(value)));
|
|
96568
|
+
}
|
|
96569
|
+
function pointFromGridCell(cell) {
|
|
96570
|
+
const slot = (value) => {
|
|
96571
|
+
if (value == null || !Number.isFinite(value))
|
|
96572
|
+
return 50;
|
|
96573
|
+
const rounded = Math.round(value);
|
|
96574
|
+
if (rounded < 0 || rounded > 9)
|
|
96575
|
+
return 50;
|
|
96576
|
+
return GRID_SLOT[rounded] ?? 50;
|
|
96577
|
+
};
|
|
96578
|
+
const qx = slot(cell.qx);
|
|
96579
|
+
const qy = slot(cell.qy);
|
|
96580
|
+
return {
|
|
96581
|
+
x: clampCell(cell.col, 9) * 100 + qx,
|
|
96582
|
+
y: clampCell(cell.row, 9) * 100 + qy
|
|
96583
|
+
};
|
|
96584
|
+
}
|
|
96585
|
+
function fillRect(rgba, width, height, x, y, w, h, color) {
|
|
96586
|
+
const xEnd = Math.min(width, x + w);
|
|
96587
|
+
const yEnd = Math.min(height, y + h);
|
|
96588
|
+
for (let py = Math.max(0, y);py < yEnd; py++) {
|
|
96589
|
+
for (let px = Math.max(0, x);px < xEnd; px++) {
|
|
96590
|
+
const index = (py * width + px) * 4;
|
|
96591
|
+
rgba[index] = color[0];
|
|
96592
|
+
rgba[index + 1] = color[1];
|
|
96593
|
+
rgba[index + 2] = color[2];
|
|
96594
|
+
rgba[index + 3] = 255;
|
|
96595
|
+
}
|
|
96596
|
+
}
|
|
96597
|
+
}
|
|
96598
|
+
function paintGlyph(rgba, width, height, glyph, originX, originY, scale) {
|
|
96599
|
+
for (let row = 0;row < glyph.length; row++) {
|
|
96600
|
+
const bits = glyph[row] ?? "";
|
|
96601
|
+
for (let col = 0;col < bits.length; col++) {
|
|
96602
|
+
if (bits[col] !== "1")
|
|
96603
|
+
continue;
|
|
96604
|
+
for (let sy = 0;sy < scale; sy++) {
|
|
96605
|
+
for (let sx = 0;sx < scale; sx++) {
|
|
96606
|
+
const px = originX + col * scale + sx;
|
|
96607
|
+
const py = originY + row * scale + sy;
|
|
96608
|
+
if (px < 0 || py < 0 || px >= width || py >= height)
|
|
96609
|
+
continue;
|
|
96610
|
+
const index = (py * width + px) * 4;
|
|
96611
|
+
rgba[index] = 255;
|
|
96612
|
+
rgba[index + 1] = 255;
|
|
96613
|
+
rgba[index + 2] = 255;
|
|
96614
|
+
rgba[index + 3] = 255;
|
|
96615
|
+
}
|
|
96616
|
+
}
|
|
96617
|
+
}
|
|
96618
|
+
}
|
|
96619
|
+
}
|
|
96620
|
+
function paintCellLabels(image) {
|
|
96621
|
+
const { width, height, rgba } = image;
|
|
96622
|
+
const stroke = Math.max(2, Math.round(Math.min(width, height) / 200));
|
|
96623
|
+
for (let col = 0;col < 10; col++) {
|
|
96624
|
+
for (let row = 0;row < 10; row++) {
|
|
96625
|
+
const x0 = Math.round(col / 10 * (width - 1));
|
|
96626
|
+
const y0 = Math.round(row / 10 * (height - 1));
|
|
96627
|
+
const x1 = Math.round((col + 1) / 10 * (width - 1));
|
|
96628
|
+
const y1 = Math.round((row + 1) / 10 * (height - 1));
|
|
96629
|
+
const scale = Math.floor(Math.min(x1 - x0, y1 - y0) / 24);
|
|
96630
|
+
if (scale < 1)
|
|
96631
|
+
continue;
|
|
96632
|
+
const text3 = `${col}-${row}`;
|
|
96633
|
+
const textW = text3.length * 5 + (text3.length - 1);
|
|
96634
|
+
const boxW = (textW + 2) * scale;
|
|
96635
|
+
const boxH = 9 * scale;
|
|
96636
|
+
if (x0 + stroke + boxW >= x1 || y0 + stroke + boxH >= y1)
|
|
96637
|
+
continue;
|
|
96638
|
+
const originX = x0 + stroke;
|
|
96639
|
+
const originY = y0 + stroke;
|
|
96640
|
+
fillRect(rgba, width, height, originX, originY, boxW, boxH, [0, 0, 0]);
|
|
96641
|
+
let cursor = originX + scale;
|
|
96642
|
+
for (const char of text3) {
|
|
96643
|
+
const glyph = GLYPHS[char];
|
|
96644
|
+
if (glyph)
|
|
96645
|
+
paintGlyph(rgba, width, height, glyph, cursor, originY + scale, scale);
|
|
96646
|
+
cursor += 6 * scale;
|
|
96647
|
+
}
|
|
96648
|
+
}
|
|
96649
|
+
}
|
|
96650
|
+
}
|
|
96651
|
+
function paintNormGrid(image) {
|
|
96652
|
+
const { width, height, rgba } = image;
|
|
96653
|
+
const stroke = Math.max(2, Math.round(Math.min(width, height) / 200));
|
|
96654
|
+
const half = Math.floor(stroke / 2);
|
|
96655
|
+
const paintColumn = (x) => {
|
|
96656
|
+
for (let dx = -half;dx < stroke - half; dx++) {
|
|
96657
|
+
const px = x + dx;
|
|
96658
|
+
if (px < 0 || px >= width)
|
|
96659
|
+
continue;
|
|
96660
|
+
for (let y = 0;y < height; y++)
|
|
96661
|
+
paintMagenta(rgba, (y * width + px) * 4);
|
|
96662
|
+
}
|
|
96663
|
+
};
|
|
96664
|
+
const paintRow = (y) => {
|
|
96665
|
+
for (let dy = -half;dy < stroke - half; dy++) {
|
|
96666
|
+
const py = y + dy;
|
|
96667
|
+
if (py < 0 || py >= height)
|
|
96668
|
+
continue;
|
|
96669
|
+
for (let x = 0;x < width; x++)
|
|
96670
|
+
paintMagenta(rgba, (py * width + x) * 4);
|
|
96671
|
+
}
|
|
96672
|
+
};
|
|
96673
|
+
for (let step = 1;step <= 9; step++) {
|
|
96674
|
+
paintColumn(Math.round(step / 10 * (width - 1)));
|
|
96675
|
+
paintRow(Math.round(step / 10 * (height - 1)));
|
|
96676
|
+
}
|
|
96677
|
+
}
|
|
96678
|
+
function overlayCoordGrid(pngBase64) {
|
|
96679
|
+
const bytes = Buffer.from(pngBase64, "base64");
|
|
96680
|
+
const image = decodePng(bytes);
|
|
96681
|
+
if (!image)
|
|
96682
|
+
return null;
|
|
96683
|
+
paintNormGrid(image);
|
|
96684
|
+
paintCellLabels(image);
|
|
96685
|
+
return encodeRgbaPng(image).toString("base64");
|
|
96686
|
+
}
|
|
96687
|
+
var MARK_RED = [255, 32, 32];
|
|
96688
|
+
var MARK_WHITE = [255, 255, 255];
|
|
96689
|
+
var MARK_GREEN = [0, 200, 80];
|
|
96690
|
+
function normToPx(norm, size) {
|
|
96691
|
+
const clamped = Math.min(1000, Math.max(0, Number.isFinite(norm) ? norm : 0));
|
|
96692
|
+
return Math.round(clamped / 1000 * (size - 1));
|
|
96693
|
+
}
|
|
96694
|
+
function paintRing(image, cx, cy, inner, outer, color) {
|
|
96695
|
+
const { width, height, rgba } = image;
|
|
96696
|
+
const outerSq = outer * outer;
|
|
96697
|
+
const innerSq = inner * inner;
|
|
96698
|
+
const reach = Math.ceil(outer);
|
|
96699
|
+
for (let py = Math.max(0, cy - reach);py <= Math.min(height - 1, cy + reach); py++) {
|
|
96700
|
+
for (let px = Math.max(0, cx - reach);px <= Math.min(width - 1, cx + reach); px++) {
|
|
96701
|
+
const distSq = (px - cx) ** 2 + (py - cy) ** 2;
|
|
96702
|
+
if (distSq > outerSq || distSq < innerSq)
|
|
96703
|
+
continue;
|
|
96704
|
+
const index = (py * width + px) * 4;
|
|
96705
|
+
rgba[index] = color[0];
|
|
96706
|
+
rgba[index + 1] = color[1];
|
|
96707
|
+
rgba[index + 2] = color[2];
|
|
96708
|
+
rgba[index + 3] = 255;
|
|
96709
|
+
}
|
|
96710
|
+
}
|
|
96711
|
+
}
|
|
96712
|
+
function paintThickLine(image, x0, y0, x1, y1, thickness, color) {
|
|
96713
|
+
const steps = Math.max(Math.abs(x1 - x0), Math.abs(y1 - y0), 1);
|
|
96714
|
+
const radius = Math.max(1, Math.floor(thickness / 2));
|
|
96715
|
+
for (let step = 0;step <= steps; step++) {
|
|
96716
|
+
const cx = Math.round(x0 + (x1 - x0) * step / steps);
|
|
96717
|
+
const cy = Math.round(y0 + (y1 - y0) * step / steps);
|
|
96718
|
+
paintRing(image, cx, cy, 0, radius, color);
|
|
96719
|
+
}
|
|
96720
|
+
}
|
|
96721
|
+
function paintLandingMark(image, cx, cy, unit) {
|
|
96722
|
+
const radius = unit * 4;
|
|
96723
|
+
paintRing(image, cx, cy, radius, radius + unit * 2, MARK_WHITE);
|
|
96724
|
+
paintRing(image, cx, cy, radius + unit * 0.5, radius + unit * 1.5, MARK_RED);
|
|
96725
|
+
paintRing(image, cx, cy, 0, unit * 1.5, MARK_RED);
|
|
96726
|
+
}
|
|
96727
|
+
function paintActionMarks(image, marks) {
|
|
96728
|
+
const unit = Math.max(2, Math.round(Math.min(image.width, image.height) / 200));
|
|
96729
|
+
for (const mark of marks) {
|
|
96730
|
+
const cx = normToPx(mark.x, image.width);
|
|
96731
|
+
const cy = normToPx(mark.y, image.height);
|
|
96732
|
+
if (mark.kind === "tap") {
|
|
96733
|
+
paintLandingMark(image, cx, cy, unit);
|
|
96734
|
+
continue;
|
|
96735
|
+
}
|
|
96736
|
+
const ex = normToPx(mark.x2, image.width);
|
|
96737
|
+
const ey = normToPx(mark.y2, image.height);
|
|
96738
|
+
paintThickLine(image, cx, cy, ex, ey, unit * 3, MARK_WHITE);
|
|
96739
|
+
paintThickLine(image, cx, cy, ex, ey, unit, MARK_RED);
|
|
96740
|
+
paintRing(image, cx, cy, 0, unit * 3, MARK_WHITE);
|
|
96741
|
+
paintRing(image, cx, cy, 0, unit * 2, MARK_GREEN);
|
|
96742
|
+
paintLandingMark(image, ex, ey, unit);
|
|
96743
|
+
}
|
|
96744
|
+
}
|
|
96745
|
+
function overlayActionMarks(pngBase64, marks) {
|
|
96746
|
+
if (marks.length === 0)
|
|
96747
|
+
return null;
|
|
96748
|
+
const image = decodePng(Buffer.from(pngBase64, "base64"));
|
|
96749
|
+
if (!image)
|
|
96750
|
+
return null;
|
|
96751
|
+
paintActionMarks(image, marks);
|
|
96752
|
+
return encodeRgbaPng(image).toString("base64");
|
|
96753
|
+
}
|
|
96754
|
+
|
|
96227
96755
|
// src/domains/runs/agent.ts
|
|
96228
96756
|
var swipeDirectionSchema = exports_external.union([
|
|
96229
96757
|
exports_external.literal("up"),
|
|
@@ -96254,6 +96782,14 @@ var agentDecisionSchema = exports_external.object({
|
|
|
96254
96782
|
y: exports_external.number().optional(),
|
|
96255
96783
|
x2: exports_external.number().optional(),
|
|
96256
96784
|
y2: exports_external.number().optional(),
|
|
96785
|
+
col: exports_external.number().optional(),
|
|
96786
|
+
row: exports_external.number().optional(),
|
|
96787
|
+
qx: exports_external.number().optional(),
|
|
96788
|
+
qy: exports_external.number().optional(),
|
|
96789
|
+
col2: exports_external.number().optional(),
|
|
96790
|
+
row2: exports_external.number().optional(),
|
|
96791
|
+
qx2: exports_external.number().optional(),
|
|
96792
|
+
qy2: exports_external.number().optional(),
|
|
96257
96793
|
direction: swipeDirectionSchema.optional(),
|
|
96258
96794
|
durationMs: exports_external.number().optional(),
|
|
96259
96795
|
double: exports_external.boolean().optional(),
|
|
@@ -96316,12 +96852,174 @@ var MAX_SCREEN_SNAPSHOT_ROWS = 80;
|
|
|
96316
96852
|
function padNorm(n) {
|
|
96317
96853
|
return String(Math.round(n)).padStart(4, " ");
|
|
96318
96854
|
}
|
|
96855
|
+
function isFullBleedElement(element) {
|
|
96856
|
+
return element.width >= 900 && element.height >= 900;
|
|
96857
|
+
}
|
|
96858
|
+
var GAME_APP_RE = /\b(this is a game|is a game|game app|mobile game|video game|unity|unreal|gameplay)\b/i;
|
|
96859
|
+
function isGameApp(appContext, appKnowledge) {
|
|
96860
|
+
return GAME_APP_RE.test(`${appContext}
|
|
96861
|
+
${appKnowledge ?? ""}`);
|
|
96862
|
+
}
|
|
96863
|
+
var NO_TREE_SNAPSHOT = "(screenshot only \u2014 no accessibility tree is attached. Name the centre of the control on the screenshot.)";
|
|
96864
|
+
function isGameSurface(elements) {
|
|
96865
|
+
if (elements.length === 0 || !elements.some((element) => isFullBleedElement(element))) {
|
|
96866
|
+
return false;
|
|
96867
|
+
}
|
|
96868
|
+
return !elements.some((element) => {
|
|
96869
|
+
if (isFullBleedElement(element))
|
|
96870
|
+
return false;
|
|
96871
|
+
return element.label.trim().length > 0 || Boolean(element.id?.trim());
|
|
96872
|
+
});
|
|
96873
|
+
}
|
|
96874
|
+
function isCanvasScreen(elements) {
|
|
96875
|
+
const namedSmall = elements.filter((element) => {
|
|
96876
|
+
if (isFullBleedElement(element))
|
|
96877
|
+
return false;
|
|
96878
|
+
const named = element.label.trim().length > 0 || Boolean(element.id?.trim());
|
|
96879
|
+
return named && element.width <= 800 && element.height <= 400;
|
|
96880
|
+
});
|
|
96881
|
+
return namedSmall.length < 3;
|
|
96882
|
+
}
|
|
96883
|
+
function releaseCanvasPoint(decision, elements) {
|
|
96884
|
+
if (decision.x == null || decision.y == null || !decision.id)
|
|
96885
|
+
return decision;
|
|
96886
|
+
if (decision.type !== "tap" && decision.type !== "type" && decision.type !== "input") {
|
|
96887
|
+
return decision;
|
|
96888
|
+
}
|
|
96889
|
+
const match2 = elements.find((element) => element.id === decision.id);
|
|
96890
|
+
if (!match2 || !isFullBleedElement(match2))
|
|
96891
|
+
return decision;
|
|
96892
|
+
const { id: _fullBleedId, ...point } = decision;
|
|
96893
|
+
return point;
|
|
96894
|
+
}
|
|
96895
|
+
var SNAP_RADIUS = 100;
|
|
96896
|
+
var TAPPABLE_ROLE_RE = /button|link|cell|switch|checkbox|radio|tab|toggle|textfield|searchfield|segment|menuitem|stepper|slider|picker/i;
|
|
96897
|
+
function isTappableElement(element) {
|
|
96898
|
+
if (isFullBleedElement(element))
|
|
96899
|
+
return false;
|
|
96900
|
+
if (element.enabled === false || element.visible === false)
|
|
96901
|
+
return false;
|
|
96902
|
+
if (!element.label.trim() && !element.id?.trim())
|
|
96903
|
+
return false;
|
|
96904
|
+
return TAPPABLE_ROLE_RE.test(element.type);
|
|
96905
|
+
}
|
|
96906
|
+
function distanceToElement(element, x, y) {
|
|
96907
|
+
const dx = Math.max(element.x - x, 0, x - (element.x + element.width));
|
|
96908
|
+
const dy = Math.max(element.y - y, 0, y - (element.y + element.height));
|
|
96909
|
+
return Math.hypot(dx, dy);
|
|
96910
|
+
}
|
|
96911
|
+
function snapTapToElement(decision, elements) {
|
|
96912
|
+
const none = { decision, element: null };
|
|
96913
|
+
if (decision.type !== "tap" || decision.x == null || decision.y == null)
|
|
96914
|
+
return none;
|
|
96915
|
+
if (decision.id || decision.label || decision.description)
|
|
96916
|
+
return none;
|
|
96917
|
+
let best = null;
|
|
96918
|
+
for (const element2 of elements) {
|
|
96919
|
+
if (!isTappableElement(element2))
|
|
96920
|
+
continue;
|
|
96921
|
+
const distance = distanceToElement(element2, decision.x, decision.y);
|
|
96922
|
+
if (distance > SNAP_RADIUS)
|
|
96923
|
+
continue;
|
|
96924
|
+
const smaller = best != null && distance === best.distance && element2.width * element2.height < best.element.width * best.element.height;
|
|
96925
|
+
if (best == null || distance < best.distance || smaller)
|
|
96926
|
+
best = { element: element2, distance };
|
|
96927
|
+
}
|
|
96928
|
+
if (!best)
|
|
96929
|
+
return none;
|
|
96930
|
+
const { element } = best;
|
|
96931
|
+
const id = element.id?.trim();
|
|
96932
|
+
if (id && elements.filter((other) => other.id === element.id).length === 1) {
|
|
96933
|
+
return { decision: { ...decision, id, label: element.label.trim() || undefined }, element };
|
|
96934
|
+
}
|
|
96935
|
+
return {
|
|
96936
|
+
decision: {
|
|
96937
|
+
...decision,
|
|
96938
|
+
x: Math.round(element.x + element.width / 2),
|
|
96939
|
+
y: Math.round(element.y + element.height / 2)
|
|
96940
|
+
},
|
|
96941
|
+
element
|
|
96942
|
+
};
|
|
96943
|
+
}
|
|
96944
|
+
function forceScreenshotTap(decision) {
|
|
96945
|
+
if (decision.type !== "tap" && decision.type !== "type" && decision.type !== "input") {
|
|
96946
|
+
return decision;
|
|
96947
|
+
}
|
|
96948
|
+
if (isSystemPermissionLabel(decision.label))
|
|
96949
|
+
return decision;
|
|
96950
|
+
const { id: _treeId, label: _treeLabel, ...point } = decision;
|
|
96951
|
+
return point;
|
|
96952
|
+
}
|
|
96953
|
+
var SCREENSHOT_POINT_RETRY = "Send x and y (0\u20131000) for the centre of the control on the attached screenshot. Do not send id or label.";
|
|
96954
|
+
function screenshotPointMissing(decision) {
|
|
96955
|
+
if (decision.type !== "tap")
|
|
96956
|
+
return false;
|
|
96957
|
+
if (isSystemPermissionLabel(decision.label))
|
|
96958
|
+
return false;
|
|
96959
|
+
if (decision.description)
|
|
96960
|
+
return false;
|
|
96961
|
+
return decision.x == null || decision.y == null;
|
|
96962
|
+
}
|
|
96963
|
+
function repeatTapHint(x, y, taps, mode) {
|
|
96964
|
+
const where = `${Math.round(x)},${Math.round(y)}`;
|
|
96965
|
+
const advice = mode === "tree" ? "A screen snapshot is attached this time: if it lists the control, tap it by its id instead of guessing a point. If it does not, trust the screenshot." : mode === "grid" ? "The control probably sits near the edge of the cell: pick the neighbouring cell, or qx/qy 0\u20131 or 8\u20139, so the point moves toward it." : "Re-read the screenshot and aim at the exact centre of the control; a small control such as a close X is easy to miss by a few percent.";
|
|
96966
|
+
return `You tapped ${where} ${taps} times and the screen did not change, so that point is not on the control. Do not tap it again. ${advice}`;
|
|
96967
|
+
}
|
|
96968
|
+
function stuckWaitHint(waits) {
|
|
96969
|
+
return `The screen has not changed after ${waits} waits. Stop waiting and act on what is visible in the screenshot; a system sheet or dialog may be waiting for a tap.`;
|
|
96970
|
+
}
|
|
96971
|
+
function namedGridIndex(value, max2) {
|
|
96972
|
+
if (value == null || !Number.isFinite(value))
|
|
96973
|
+
return null;
|
|
96974
|
+
const rounded = Math.round(value);
|
|
96975
|
+
if (rounded < 0 || rounded > max2)
|
|
96976
|
+
return null;
|
|
96977
|
+
return rounded;
|
|
96978
|
+
}
|
|
96979
|
+
var GRID_CELL_RETRY = "Send col and row (0\u20139) of the labeled cell that contains the centre of the control, and qx and qy (0\u20139) for where inside that cell it sits, in tenths of the cell. Do not send x or y.";
|
|
96980
|
+
function gridPointMissing(decision) {
|
|
96981
|
+
if (decision.type !== "tap" && decision.type !== "type" && decision.type !== "input" && decision.type !== "drag") {
|
|
96982
|
+
return false;
|
|
96983
|
+
}
|
|
96984
|
+
if (isSystemPermissionLabel(decision.label))
|
|
96985
|
+
return false;
|
|
96986
|
+
if (namedGridIndex(decision.col, 9) == null || namedGridIndex(decision.row, 9) == null) {
|
|
96987
|
+
return true;
|
|
96988
|
+
}
|
|
96989
|
+
if (decision.type === "drag") {
|
|
96990
|
+
return namedGridIndex(decision.col2, 9) == null || namedGridIndex(decision.row2, 9) == null;
|
|
96991
|
+
}
|
|
96992
|
+
return false;
|
|
96993
|
+
}
|
|
96994
|
+
function applyGridPoint(decision) {
|
|
96995
|
+
if (decision.type !== "tap" && decision.type !== "type" && decision.type !== "input" && decision.type !== "drag" && decision.type !== "swipe") {
|
|
96996
|
+
return decision;
|
|
96997
|
+
}
|
|
96998
|
+
const col = namedGridIndex(decision.col, 9);
|
|
96999
|
+
const row = namedGridIndex(decision.row, 9);
|
|
97000
|
+
if (col == null || row == null)
|
|
97001
|
+
return decision;
|
|
97002
|
+
const start = pointFromGridCell({ col, row, qx: decision.qx, qy: decision.qy });
|
|
97003
|
+
const next = { ...decision, x: start.x, y: start.y };
|
|
97004
|
+
const col2 = namedGridIndex(decision.col2, 9);
|
|
97005
|
+
const row2 = namedGridIndex(decision.row2, 9);
|
|
97006
|
+
if (col2 != null && row2 != null) {
|
|
97007
|
+
const end = pointFromGridCell({ col: col2, row: row2, qx: decision.qx2, qy: decision.qy2 });
|
|
97008
|
+
next.x2 = end.x;
|
|
97009
|
+
next.y2 = end.y;
|
|
97010
|
+
}
|
|
97011
|
+
return next;
|
|
97012
|
+
}
|
|
96319
97013
|
function formatScreenSnapshot(elements) {
|
|
96320
97014
|
if (elements == null)
|
|
96321
97015
|
return "(screen tree unavailable)";
|
|
96322
97016
|
if (elements.length === 0)
|
|
96323
97017
|
return "(empty tree)";
|
|
96324
|
-
const
|
|
97018
|
+
const usable = elements.filter((element) => !isFullBleedElement(element));
|
|
97019
|
+
if (usable.length === 0) {
|
|
97020
|
+
return "(empty tree \u2014 full-screen canvas or game surface only. Ignore it. Use screenshot x,y for the centre of the control.)";
|
|
97021
|
+
}
|
|
97022
|
+
const ordered = [...usable].sort((a, b) => a.y - b.y || a.x - b.x);
|
|
96325
97023
|
const shown = ordered.slice(0, MAX_SCREEN_SNAPSHOT_ROWS);
|
|
96326
97024
|
const lines = shown.map((element) => {
|
|
96327
97025
|
const id = element.id?.trim() ? ` id=${element.id.trim()}` : "";
|
|
@@ -96501,13 +97199,44 @@ function prefersScreenshotTap(decision) {
|
|
|
96501
97199
|
return false;
|
|
96502
97200
|
return decision.x != null && decision.y != null;
|
|
96503
97201
|
}
|
|
97202
|
+
var REPLY_FORMAT = `Your entire reply MUST be a single strict JSON object (double quotes only \u2014 never single quotes) and nothing else \u2014 no markdown fences, no commentary before or after.
|
|
97203
|
+
Every action MUST include:
|
|
97204
|
+
- "reason": one short sentence summarizing the action choice
|
|
97205
|
+
- "thoughts": 2\u20134 sentences describing what you see on screen and how that led to this action`;
|
|
97206
|
+
var APP_LIFECYCLE_SHAPES = `{"type":"activate-app","appId":"com.example.app","reason":"...","thoughts":"..."}
|
|
97207
|
+
{"type":"terminate-app","appId":"com.example.app","reason":"...","thoughts":"..."}
|
|
97208
|
+
{"type":"restart-app","appId":"com.example.app","reason":"...","thoughts":"..."}
|
|
97209
|
+
{"type":"background-app","seconds":3,"reason":"...","thoughts":"..."}
|
|
97210
|
+
{"type":"open-url","url":"https://example.com","reason":"...","thoughts":"..."}
|
|
97211
|
+
{"type":"assert","assertion":"visible|not-visible","text":"...","reason":"...","thoughts":"..."}
|
|
97212
|
+
{"type":"wait","ms":500-3000,"reason":"...","thoughts":"..."}
|
|
97213
|
+
{"type":"verify","reason":"...","thoughts":"..."}
|
|
97214
|
+
{"type":"done","reason":"...","thoughts":"..."}
|
|
97215
|
+
{"type":"fail","reason":"...","thoughts":"..."}`;
|
|
97216
|
+
var KEYBOARD_GUIDANCE = `Textfield input and keyboard handling:
|
|
97217
|
+
- Typing into a textfield brings up the virtual soft keyboard on the bottom of the screen (typically covering y ~ 550\u20131000), which may obscure buttons (like Submit, Login, Next) or lower inputs.
|
|
97218
|
+
- When typing inside a textfield or when the keyboard is open:
|
|
97219
|
+
1. Dismiss keyboard by tapping away: Emit a tap on an empty, neutral background area outside textfields and above the keyboard (e.g. tap neutral header space or empty background) to dismiss the keyboard.
|
|
97220
|
+
2. Nextline / Return on keyboard: Tap the Return / Done / Next / Search key on the virtual keyboard itself (using its x,y coordinates), or append "\\n" to your input text.
|
|
97221
|
+
3. Dismiss then use app buttons: If an app button you need is obscured or covered by the soft keyboard, dismiss the keyboard first (by tapping away or pressing Return) so the button is revealed, then tap the app button on the subsequent step.`;
|
|
97222
|
+
var SWIPE_GUIDANCE = `Swipe direction is the FINGER movement (same as Inspector):
|
|
97223
|
+
- "down" (finger down, y 200\u2192800): scroll UP \u2014 reveal content above
|
|
97224
|
+
- "up" (finger up, y 800\u2192200): scroll DOWN \u2014 reveal content below / to the bottom
|
|
97225
|
+
- "left" / "right": horizontal paging
|
|
97226
|
+
When the case says "scroll up", emit direction "down". When it says "scroll down" or "to the bottom", emit direction "up". Never tap to scroll. Never emit wait as a stand-in for scrolling.`;
|
|
97227
|
+
var SHARED_RULES = `- activate-app / terminate-app / restart-app / open-url / background-app match yoqa action. Omit appId to use the catalog app id provided in the user message.
|
|
97228
|
+
- To scroll a list or page, emit swipe \u2014 never a tap in the middle of the screen as a stand-in for scrolling.
|
|
97229
|
+
- "Scroll until you cannot scroll anymore" / "to the bottom": keep emitting swipe (direction "up") until a later screenshot shows the same content. Then verify. Unchanged screens after taps do not count \u2014 taps do not scroll.
|
|
97230
|
+
- You are given ONE current instruction. Complete only that instruction, then return verify or done. Later instructions are hidden on purpose \u2014 do not invent, skip ahead, or combine them. Earlier finished instructions may be omitted \u2014 do not reconstruct them.
|
|
97231
|
+
- Do not open unrelated tabs or screens (Wallet, Profile, \u2026) unless the current instruction says to. If the current target is not visible, scroll THIS screen to find it. If it cannot be found, fail with what you see.
|
|
97232
|
+
- When THIS instruction's expected result is already visible, return verify or done. That finishes this instruction only, not the whole case. Otherwise return the next action. A separate verify check looks at the same screenshot after you decide.
|
|
97233
|
+
- Splash / loading / blank screens: return wait (do not tap imaginary tabs).
|
|
97234
|
+
- Do not repeat the same tap target from Recent actions unless the screenshot still shows you are on the wrong screen.
|
|
97235
|
+
- On fail, say what you see (e.g. "splash logo only, no bottom tabs") \u2014 never "no screenshot".`;
|
|
96504
97236
|
var SYSTEM_PROMPT = `You are a mobile QA agent controlling an app using BOTH visual screenshots and screen snapshots (accessibility tree).
|
|
96505
97237
|
A screenshot of the current device screen is ALWAYS attached to the user message as an image. You can see it. Never claim that no screenshot was provided, missing, blank, or unavailable.
|
|
96506
97238
|
|
|
96507
|
-
|
|
96508
|
-
Every action MUST include:
|
|
96509
|
-
- "reason": one short sentence summarizing the action choice
|
|
96510
|
-
- "thoughts": 2\u20134 sentences describing what you see on screen and how that led to this action
|
|
97239
|
+
${REPLY_FORMAT}
|
|
96511
97240
|
|
|
96512
97241
|
Valid shapes:
|
|
96513
97242
|
{"type":"tap","label":"Allow","reason":"...","thoughts":"..."}
|
|
@@ -96522,16 +97251,7 @@ Valid shapes:
|
|
|
96522
97251
|
{"type":"type","text":"...","x":0-1000,"y":0-1000,"reason":"...","thoughts":"..."}
|
|
96523
97252
|
{"type":"input","text":"...","id":"email_field","reason":"...","thoughts":"..."}
|
|
96524
97253
|
{"type":"alert","alertAction":"accept","reason":"...","thoughts":"..."}
|
|
96525
|
-
{
|
|
96526
|
-
{"type":"terminate-app","appId":"com.example.app","reason":"...","thoughts":"..."}
|
|
96527
|
-
{"type":"restart-app","appId":"com.example.app","reason":"...","thoughts":"..."}
|
|
96528
|
-
{"type":"background-app","seconds":3,"reason":"...","thoughts":"..."}
|
|
96529
|
-
{"type":"open-url","url":"https://example.com","reason":"...","thoughts":"..."}
|
|
96530
|
-
{"type":"assert","assertion":"visible|not-visible","text":"...","reason":"...","thoughts":"..."}
|
|
96531
|
-
{"type":"wait","ms":500-3000,"reason":"...","thoughts":"..."}
|
|
96532
|
-
{"type":"verify","reason":"...","thoughts":"..."}
|
|
96533
|
-
{"type":"done","reason":"...","thoughts":"..."}
|
|
96534
|
-
{"type":"fail","reason":"...","thoughts":"..."}
|
|
97254
|
+
${APP_LIFECYCLE_SHAPES}
|
|
96535
97255
|
|
|
96536
97256
|
Coordinates use a 0\u20131000 normalized grid (0,0 top-left of the attached screenshot). The user message also includes a screen snapshot (cleaned accessibility tree) with the same grid \u2014 x,y is each element's TOP-LEFT, not centre. Centre = x + width/2, y + height/2.
|
|
96537
97257
|
|
|
@@ -96544,32 +97264,50 @@ Use BOTH the screenshot and the screen snapshot:
|
|
|
96544
97264
|
- System permission / notification dialogs: {"type":"tap","label":"Allow"} or {"type":"alert","alertAction":"accept"}. Never guess coordinates for these.
|
|
96545
97265
|
- If a previous action failed (see Last action error), pick a different target from the snapshot + screenshot \u2014 do not repeat the same id/label/point.
|
|
96546
97266
|
|
|
96547
|
-
|
|
96548
|
-
- Typing into a textfield brings up the virtual soft keyboard on the bottom of the screen (typically covering y ~ 550\u20131000), which may obscure buttons (like Submit, Login, Next) or lower inputs.
|
|
96549
|
-
- When typing inside a textfield or when the keyboard is open:
|
|
96550
|
-
1. Dismiss keyboard by tapping away: Emit a tap on an empty, neutral background area outside textfields and above the keyboard (e.g. tap neutral header space or empty background) to dismiss the keyboard.
|
|
96551
|
-
2. Nextline / Return on keyboard: Tap the Return / Done / Next / Search key on the virtual keyboard itself (using its x,y coordinates or label/id if present), or append "\\n" to your input text.
|
|
96552
|
-
3. Dismiss then use app buttons: If an app button you need is obscured or covered by the soft keyboard, dismiss the keyboard first (by tapping away or pressing Return) so the button is revealed, then tap the app button on the subsequent step.
|
|
97267
|
+
${KEYBOARD_GUIDANCE}
|
|
96553
97268
|
|
|
96554
|
-
|
|
96555
|
-
- "down" (finger down, y 200\u2192800): scroll UP \u2014 reveal content above
|
|
96556
|
-
- "up" (finger up, y 800\u2192200): scroll DOWN \u2014 reveal content below / to the bottom
|
|
96557
|
-
- "left" / "right": horizontal paging
|
|
96558
|
-
When the case says "scroll up", emit direction "down". When it says "scroll down" or "to the bottom", emit direction "up". Never tap to scroll. Never emit wait as a stand-in for scrolling.
|
|
97269
|
+
${SWIPE_GUIDANCE}
|
|
96559
97270
|
|
|
96560
97271
|
Rules:
|
|
96561
97272
|
- Look at the attached screenshot AND the screen snapshot before deciding.
|
|
96562
97273
|
- Double-tap: {"type":"tap","double":true,...}. Long-press: {"type":"tap","durationMs":2000,...}. Drag a slider/chip with type drag, not swipe.
|
|
96563
|
-
- activate-app / terminate-app / restart-app / open-url / background-app match yoqa action. Omit appId to use the catalog app id provided in the user message.
|
|
96564
97274
|
- Assert text on the tree with {"type":"assert","assertion":"visible","text":"\u2026"}.
|
|
96565
|
-
|
|
96566
|
-
|
|
96567
|
-
|
|
96568
|
-
|
|
96569
|
-
|
|
96570
|
-
|
|
96571
|
-
|
|
96572
|
-
-
|
|
97275
|
+
${SHARED_RULES}`;
|
|
97276
|
+
var VISION_SYSTEM_PROMPT = `You are a mobile QA agent controlling an app by looking at screenshots.
|
|
97277
|
+
A screenshot of the current device screen is ALWAYS attached to the user message as an image. You can see it. Never claim that no screenshot was provided, missing, blank, or unavailable. No accessibility tree is provided \u2014 decide from the screenshot alone.
|
|
97278
|
+
|
|
97279
|
+
${REPLY_FORMAT}
|
|
97280
|
+
|
|
97281
|
+
Valid shapes:
|
|
97282
|
+
{"type":"tap","x":0-1000,"y":0-1000,"reason":"...","thoughts":"..."}
|
|
97283
|
+
{"type":"tap","x":0-1000,"y":0-1000,"double":true,"reason":"...","thoughts":"..."}
|
|
97284
|
+
{"type":"tap","x":0-1000,"y":0-1000,"durationMs":2000,"reason":"...","thoughts":"..."}
|
|
97285
|
+
{"type":"tap","label":"Allow","reason":"...","thoughts":"..."}
|
|
97286
|
+
{"type":"swipe","direction":"up|down|left|right","reason":"...","thoughts":"..."}
|
|
97287
|
+
{"type":"swipe","x":0-1000,"y":0-1000,"x2":0-1000,"y2":0-1000,"reason":"...","thoughts":"..."}
|
|
97288
|
+
{"type":"drag","x":0-1000,"y":0-1000,"x2":0-1000,"y2":0-1000,"reason":"...","thoughts":"..."}
|
|
97289
|
+
{"type":"type","text":"...","x":0-1000,"y":0-1000,"reason":"...","thoughts":"..."}
|
|
97290
|
+
{"type":"alert","alertAction":"accept","reason":"...","thoughts":"..."}
|
|
97291
|
+
${APP_LIFECYCLE_SHAPES}
|
|
97292
|
+
|
|
97293
|
+
Coordinates use a 0\u20131000 normalized grid over the attached screenshot (0,0 is the top-left corner, 1000,1000 the bottom-right). Send x,y for the CENTRE of the control you want to hit. Read it off the screenshot: a control's centre is halfway between its left and right edges, and halfway between its top and bottom edges.
|
|
97294
|
+
|
|
97295
|
+
Targeting:
|
|
97296
|
+
- Tap with "x,y" on the screenshot. Do not send id \u2014 no ids are available. Do not send description.
|
|
97297
|
+
- System permission / notification dialogs: {"type":"tap","label":"Allow"} or {"type":"alert","alertAction":"accept"}. Never guess coordinates for these.
|
|
97298
|
+
- Visual context is all you have: read layout, colors, icons, canvas controls, modal overlays, and soft keyboard visibility from the screenshot.
|
|
97299
|
+
- If a previous action failed (see Last action error), pick a different point on the screenshot \u2014 do not repeat the same point.
|
|
97300
|
+
- If a magenta grid is drawn on the screenshot, each cell is labeled column-row in its top-left (0-0 through 9-9). Then send col, row, qx, and qy for the centre of the control, and do not send x or y. qx and qy are 0\u20139, tenths of the cell from its left and top (4 or 5 is the middle). A small control near a cell edge needs qx or qy near 0 or 9, so do not default to the middle. The tap point is computed from that cell.
|
|
97301
|
+
|
|
97302
|
+
${KEYBOARD_GUIDANCE}
|
|
97303
|
+
|
|
97304
|
+
${SWIPE_GUIDANCE}
|
|
97305
|
+
|
|
97306
|
+
Rules:
|
|
97307
|
+
- Look at the attached screenshot before deciding.
|
|
97308
|
+
- Double-tap: {"type":"tap","double":true,...}. Long-press: {"type":"tap","durationMs":2000,...}. Drag a slider/chip with type drag, not swipe.
|
|
97309
|
+
- To check that text is on screen, emit {"type":"assert","assertion":"visible","text":"\u2026"} with the exact visible text.
|
|
97310
|
+
${SHARED_RULES}`;
|
|
96573
97311
|
function formatRecentActions(actions) {
|
|
96574
97312
|
if (actions.length === 0)
|
|
96575
97313
|
return "(none \u2014 first step)";
|
|
@@ -96616,6 +97354,11 @@ function formatRecentActions(actions) {
|
|
|
96616
97354
|
`);
|
|
96617
97355
|
}
|
|
96618
97356
|
var LIST_ITEM_START_RE = /^(?:\d+[.)]|\d+\s*[-\u2013]|[-*\u2022])\s+/;
|
|
97357
|
+
function continuesInstruction(rawLine, line) {
|
|
97358
|
+
if (/^\s/.test(rawLine))
|
|
97359
|
+
return true;
|
|
97360
|
+
return /^[a-z]/.test(line);
|
|
97361
|
+
}
|
|
96619
97362
|
function splitInstructionSteps(instructions) {
|
|
96620
97363
|
const trimmed = instructions.replace(/\r\n/g, `
|
|
96621
97364
|
`).trim();
|
|
@@ -96625,7 +97368,7 @@ function splitInstructionSteps(instructions) {
|
|
|
96625
97368
|
`);
|
|
96626
97369
|
const items = [];
|
|
96627
97370
|
let current = [];
|
|
96628
|
-
let
|
|
97371
|
+
let sawSteps = false;
|
|
96629
97372
|
const pushCurrent = () => {
|
|
96630
97373
|
const text3 = current.join(" ").replace(/\s+/g, " ").trim();
|
|
96631
97374
|
if (text3)
|
|
@@ -96634,12 +97377,21 @@ function splitInstructionSteps(instructions) {
|
|
|
96634
97377
|
};
|
|
96635
97378
|
for (const rawLine of lines) {
|
|
96636
97379
|
const line = rawLine.trim();
|
|
96637
|
-
if (!line)
|
|
97380
|
+
if (!line) {
|
|
97381
|
+
if (current.length > 0) {
|
|
97382
|
+
sawSteps = true;
|
|
97383
|
+
pushCurrent();
|
|
97384
|
+
}
|
|
96638
97385
|
continue;
|
|
97386
|
+
}
|
|
96639
97387
|
if (LIST_ITEM_START_RE.test(line)) {
|
|
96640
|
-
|
|
97388
|
+
sawSteps = true;
|
|
96641
97389
|
pushCurrent();
|
|
96642
97390
|
current = [line.replace(LIST_ITEM_START_RE, "").trim()];
|
|
97391
|
+
} else if (current.length > 0 && !continuesInstruction(rawLine, line)) {
|
|
97392
|
+
sawSteps = true;
|
|
97393
|
+
pushCurrent();
|
|
97394
|
+
current = [line];
|
|
96643
97395
|
} else if (current.length > 0) {
|
|
96644
97396
|
current.push(line);
|
|
96645
97397
|
} else {
|
|
@@ -96647,7 +97399,7 @@ function splitInstructionSteps(instructions) {
|
|
|
96647
97399
|
}
|
|
96648
97400
|
}
|
|
96649
97401
|
pushCurrent();
|
|
96650
|
-
if (!
|
|
97402
|
+
if (!sawSteps)
|
|
96651
97403
|
return [trimmed];
|
|
96652
97404
|
return items.length > 0 ? items : [trimmed];
|
|
96653
97405
|
}
|
|
@@ -96666,11 +97418,29 @@ function flattenCaseInstructions(flows2) {
|
|
|
96666
97418
|
}
|
|
96667
97419
|
return queue.length > 0 ? queue : [{ instructions: "", expectedResult: "" }];
|
|
96668
97420
|
}
|
|
97421
|
+
var MAX_COMPLETED_IN_PROMPT = 3;
|
|
96669
97422
|
function formatCompletedInstructions(completed) {
|
|
96670
97423
|
if (completed.length === 0)
|
|
96671
97424
|
return "(none \u2014 this is the first instruction)";
|
|
96672
|
-
|
|
97425
|
+
const omitted = Math.max(0, completed.length - MAX_COMPLETED_IN_PROMPT);
|
|
97426
|
+
const tail2 = completed.slice(-MAX_COMPLETED_IN_PROMPT);
|
|
97427
|
+
const lines = tail2.map((text3, index) => `${omitted + index + 1}. ${text3}`);
|
|
97428
|
+
if (omitted === 0)
|
|
97429
|
+
return lines.join(`
|
|
96673
97430
|
`);
|
|
97431
|
+
return `${omitted} earlier instructions already done (omitted \u2014 do not reconstruct or repeat them).
|
|
97432
|
+
${lines.join(`
|
|
97433
|
+
`)}`;
|
|
97434
|
+
}
|
|
97435
|
+
var MAX_APP_KNOWLEDGE_CHARS = 2048;
|
|
97436
|
+
function formatAppKnowledge(knowledge) {
|
|
97437
|
+
const trimmed = (knowledge ?? "").trim();
|
|
97438
|
+
if (!trimmed)
|
|
97439
|
+
return null;
|
|
97440
|
+
if (trimmed.length <= MAX_APP_KNOWLEDGE_CHARS)
|
|
97441
|
+
return trimmed;
|
|
97442
|
+
return `${trimmed.slice(0, MAX_APP_KNOWLEDGE_CHARS)}
|
|
97443
|
+
\u2026 (app knowledge truncated at ${MAX_APP_KNOWLEDGE_CHARS} characters)`;
|
|
96674
97444
|
}
|
|
96675
97445
|
function formatDecidePrompt(input) {
|
|
96676
97446
|
const ordinal = input.instructionOrdinal ?? input.stepIndex + 1;
|
|
@@ -96679,8 +97449,13 @@ function formatDecidePrompt(input) {
|
|
|
96679
97449
|
const expected = input.expectedResult.trim() ? input.expectedResult : "(none for this instruction \u2014 return verify once you have performed it and the UI has updated)";
|
|
96680
97450
|
const progress = total > 0 ? `Instruction ${ordinal} of ${total} \u2014 do ONLY this instruction${remaining > 0 ? `; ${remaining} remain after it` : " (last instruction)"}` : `Step ${input.stepIndex + 1}`;
|
|
96681
97451
|
const later = remaining > 0 ? `Later instructions exist (${remaining}) but are hidden. Do not invent or perform them. Do not open unrelated screens to skip ahead.` : "This is the last instruction of the case.";
|
|
97452
|
+
const appKnowledge = formatAppKnowledge(input.appKnowledge);
|
|
96682
97453
|
return [
|
|
96683
97454
|
`App context: ${input.appContext || "(none)"}`,
|
|
97455
|
+
...appKnowledge ? [
|
|
97456
|
+
`App knowledge (notes your team keeps about this app \u2014 trust these over guesses):
|
|
97457
|
+
${appKnowledge}`
|
|
97458
|
+
] : [],
|
|
96684
97459
|
`Test case: ${input.caseTitle}`,
|
|
96685
97460
|
progress,
|
|
96686
97461
|
"Completed instructions (already done \u2014 do not repeat):",
|
|
@@ -96692,9 +97467,19 @@ function formatDecidePrompt(input) {
|
|
|
96692
97467
|
`Recent actions:
|
|
96693
97468
|
${formatRecentActions(input.recentActions ?? [])}`,
|
|
96694
97469
|
`Last action error: ${input.lastError || "(none)"}`,
|
|
96695
|
-
|
|
96696
|
-
|
|
96697
|
-
|
|
97470
|
+
...input.screenshotOnly ? [
|
|
97471
|
+
"No accessibility tree is attached. The attached screenshot was taken first and is all you have.",
|
|
97472
|
+
"Name the centre of the control on that screenshot as x,y (0\u20131000). Do not send id. Send label only for a system permission or notification button."
|
|
97473
|
+
] : [
|
|
97474
|
+
"The screenshot is the ground truth. The tree below can miss a system sheet or dialog that covers the app, and can describe the screen underneath it. If they disagree, trust the screenshot.",
|
|
97475
|
+
"Screen snapshot (cleaned accessibility tree, 0\u20131000 grid, x,y is top-left):",
|
|
97476
|
+
input.screenSnapshot || "(unavailable)",
|
|
97477
|
+
"Look at the attached screenshot image AND the screen snapshot and decide the next action. Decide whether to use x,y coordinates or an id based on both sources. A screenshot is attached."
|
|
97478
|
+
],
|
|
97479
|
+
...input.coordGrid ? [
|
|
97480
|
+
"A magenta grid is drawn on the screenshot. Each cell is labeled in its top-left corner as column-row (0-0 is the top-left cell, 9-9 is the bottom-right). Do not send x or y. Send col and row (0\u20139) of the cell that contains the centre of the control, and qx and qy (0\u20139) for where inside that cell the centre sits, in tenths of the cell: 0 is the left or top edge, 4 or 5 is the middle, 9 is the right or bottom edge. A small control near a cell edge needs qx or qy near 0 or 9, so do not default to the middle. The tap point is computed from those values. For a drag, also send col2, row2, qx2, and qy2 for the end."
|
|
97481
|
+
] : [],
|
|
97482
|
+
`Again, do ONLY this: ${input.instructions || "(none)"}`,
|
|
96698
97483
|
'Reply with ONLY the JSON action object, including non-empty "reason" and "thoughts".'
|
|
96699
97484
|
].join(`
|
|
96700
97485
|
`);
|
|
@@ -96705,12 +97490,51 @@ function isAbsurdNoScreenshotFail(decision) {
|
|
|
96705
97490
|
const text3 = `${decision.reason} ${decision.thoughts}`.toLowerCase();
|
|
96706
97491
|
return text3.includes("no screenshot") || text3.includes("screenshot provided") || text3.includes("screenshot was not") || text3.includes("without a screenshot") || text3.includes("missing screenshot") || text3.includes("screenshot unavailable") || text3.includes("cannot see the screen") || text3.includes("no image");
|
|
96707
97492
|
}
|
|
97493
|
+
var VERIFY_PROMPT = `You check whether ONE mobile test instruction is already satisfied.
|
|
97494
|
+
A screenshot of the device AFTER the last action is attached. You can see it.
|
|
97495
|
+
Reply with ONLY one JSON object: {"type":"verify"|"continue"|"fail","reason":"...","thoughts":"..."}
|
|
97496
|
+
- verify: the current instruction's expected result is visible, or the instruction has clearly been performed.
|
|
97497
|
+
- continue: the instruction is not finished. Do not invent the next tap.
|
|
97498
|
+
- fail: the instruction cannot be completed from what is on screen.
|
|
97499
|
+
Do not propose coordinates. Do not skip ahead to later instructions.`;
|
|
97500
|
+
function formatVerifyPrompt(input) {
|
|
97501
|
+
const ordinal = input.instructionOrdinal ?? 1;
|
|
97502
|
+
const total = input.instructionCount ?? 0;
|
|
97503
|
+
const progress = total > 0 ? `Instruction ${ordinal} of ${total}.` : "";
|
|
97504
|
+
const expected = input.expectedResult.trim() ? input.expectedResult : "(none \u2014 verify once this instruction has been performed and the UI has updated)";
|
|
97505
|
+
return [
|
|
97506
|
+
`Test case: ${input.caseTitle}`,
|
|
97507
|
+
progress,
|
|
97508
|
+
`Instruction to verify: ${input.instructions || "(none)"}`,
|
|
97509
|
+
`Expected result: ${expected}`,
|
|
97510
|
+
"Look at the attached screenshot. Is this instruction satisfied now?"
|
|
97511
|
+
].filter((line) => line.length > 0).join(`
|
|
97512
|
+
`);
|
|
97513
|
+
}
|
|
97514
|
+
async function verifyInstruction(input) {
|
|
97515
|
+
const verdictSchema = exports_external.object({
|
|
97516
|
+
type: exports_external.union([exports_external.literal("verify"), exports_external.literal("continue"), exports_external.literal("fail")]),
|
|
97517
|
+
reason: exports_external.string().min(1),
|
|
97518
|
+
thoughts: exports_external.string().min(1)
|
|
97519
|
+
});
|
|
97520
|
+
const object3 = await completeVision(input.auth, {
|
|
97521
|
+
schema: verdictSchema,
|
|
97522
|
+
system: VERIFY_PROMPT,
|
|
97523
|
+
prompt: formatVerifyPrompt(input),
|
|
97524
|
+
imageBase64: input.imageBase64,
|
|
97525
|
+
image: input.image,
|
|
97526
|
+
onDecideRetry: input.onDecideRetry
|
|
97527
|
+
});
|
|
97528
|
+
return object3;
|
|
97529
|
+
}
|
|
96708
97530
|
async function decideNextAction(input) {
|
|
96709
97531
|
return completeVision(input.auth, {
|
|
96710
97532
|
schema: agentDecisionSchema,
|
|
96711
|
-
system: SYSTEM_PROMPT,
|
|
97533
|
+
system: input.screenshotOnly ? VISION_SYSTEM_PROMPT : SYSTEM_PROMPT,
|
|
96712
97534
|
prompt: formatDecidePrompt(input),
|
|
96713
|
-
imageBase64: input.imageBase64
|
|
97535
|
+
imageBase64: input.imageBase64,
|
|
97536
|
+
image: input.image,
|
|
97537
|
+
onDecideRetry: input.onDecideRetry
|
|
96714
97538
|
});
|
|
96715
97539
|
}
|
|
96716
97540
|
|
|
@@ -96877,6 +97701,7 @@ function mapApp(row) {
|
|
|
96877
97701
|
name: row.name,
|
|
96878
97702
|
prefix: row.prefix || row.id,
|
|
96879
97703
|
context: row.context,
|
|
97704
|
+
knowledge: row.knowledge,
|
|
96880
97705
|
iosBundleId: row.iosBundleId,
|
|
96881
97706
|
iosAppStoreId: row.iosAppStoreId,
|
|
96882
97707
|
androidApplicationId: row.androidApplicationId,
|
|
@@ -97062,6 +97887,7 @@ async function createApp(input) {
|
|
|
97062
97887
|
name: name30,
|
|
97063
97888
|
prefix,
|
|
97064
97889
|
context: "",
|
|
97890
|
+
knowledge: "",
|
|
97065
97891
|
iosBundleId: "",
|
|
97066
97892
|
iosAppStoreId: "",
|
|
97067
97893
|
androidApplicationId: "",
|
|
@@ -97075,6 +97901,7 @@ async function createApp(input) {
|
|
|
97075
97901
|
}
|
|
97076
97902
|
return created;
|
|
97077
97903
|
}
|
|
97904
|
+
var MAX_APP_KNOWLEDGE_CHARS2 = 2048;
|
|
97078
97905
|
async function updateApp(appId, input) {
|
|
97079
97906
|
const db = getCatalogDb();
|
|
97080
97907
|
const existing = await db.query.apps.findFirst({ where: eq(apps.id, appId) });
|
|
@@ -97085,11 +97912,15 @@ async function updateApp(appId, input) {
|
|
|
97085
97912
|
if (!name30) {
|
|
97086
97913
|
throw new CatalogValidationError("Application name is required");
|
|
97087
97914
|
}
|
|
97915
|
+
if (input.knowledge !== undefined && input.knowledge.length > MAX_APP_KNOWLEDGE_CHARS2) {
|
|
97916
|
+
throw new CatalogValidationError(`App knowledge must be at most ${MAX_APP_KNOWLEDGE_CHARS2} characters`);
|
|
97917
|
+
}
|
|
97088
97918
|
const prefix = input.prefix !== undefined ? await allocatePrefix(input.prefix.trim() || name30, appId) : existing.prefix || await allocatePrefix(name30, appId);
|
|
97089
97919
|
await db.update(apps).set({
|
|
97090
97920
|
name: name30,
|
|
97091
97921
|
prefix,
|
|
97092
97922
|
context: input.context ?? existing.context,
|
|
97923
|
+
knowledge: input.knowledge ?? existing.knowledge,
|
|
97093
97924
|
iosBundleId: input.iosBundleId ?? existing.iosBundleId,
|
|
97094
97925
|
iosAppStoreId: input.iosAppStoreId ?? existing.iosAppStoreId,
|
|
97095
97926
|
androidApplicationId: input.androidApplicationId ?? existing.androidApplicationId,
|
|
@@ -97619,8 +98450,8 @@ function createDevicesRoutes() {
|
|
|
97619
98450
|
}
|
|
97620
98451
|
|
|
97621
98452
|
// src/domains/doctor/application.ts
|
|
97622
|
-
import { homedir as
|
|
97623
|
-
import { join as
|
|
98453
|
+
import { homedir as homedir11 } from "os";
|
|
98454
|
+
import { join as join14 } from "path";
|
|
97624
98455
|
async function runCommand5(command, options) {
|
|
97625
98456
|
ensureHostToolPath();
|
|
97626
98457
|
try {
|
|
@@ -97706,7 +98537,7 @@ async function probeJava() {
|
|
|
97706
98537
|
let detail = javaBin ?? "java not on PATH";
|
|
97707
98538
|
if (javaHome)
|
|
97708
98539
|
detail = `JAVA_HOME=${javaHome}`;
|
|
97709
|
-
else if (await Bun.file(
|
|
98540
|
+
else if (await Bun.file(join14(studioJbr, "bin/java")).exists()) {
|
|
97710
98541
|
detail = `Android Studio JBR available at ${studioJbr} (JAVA_HOME unset)`;
|
|
97711
98542
|
}
|
|
97712
98543
|
const ok = Boolean(javaBin || javaHome);
|
|
@@ -97737,7 +98568,7 @@ async function probeXcodeSelect() {
|
|
|
97737
98568
|
};
|
|
97738
98569
|
}
|
|
97739
98570
|
async function probeAdbDevices() {
|
|
97740
|
-
const adb = await which3("adb") ??
|
|
98571
|
+
const adb = await which3("adb") ?? join14(homedir11(), "Library/Android/sdk/platform-tools/adb");
|
|
97741
98572
|
if (!await Bun.file(adb).exists() && !await which3("adb")) {
|
|
97742
98573
|
return {
|
|
97743
98574
|
id: "adb-devices",
|
|
@@ -97938,10 +98769,10 @@ import {
|
|
|
97938
98769
|
randomBytes
|
|
97939
98770
|
} from "crypto";
|
|
97940
98771
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
97941
|
-
import { homedir as
|
|
97942
|
-
import { dirname as dirname3, join as
|
|
97943
|
-
var YOQA_ROOT5 =
|
|
97944
|
-
var KEY_PATH =
|
|
98772
|
+
import { homedir as homedir12 } from "os";
|
|
98773
|
+
import { dirname as dirname3, join as join15 } from "path";
|
|
98774
|
+
var YOQA_ROOT5 = join15(homedir12(), ".yoqa");
|
|
98775
|
+
var KEY_PATH = join15(YOQA_ROOT5, ".provider-key");
|
|
97945
98776
|
var ALGORITHM = "aes-256-gcm";
|
|
97946
98777
|
var IV_LENGTH = 12;
|
|
97947
98778
|
var KEY_LENGTH = 32;
|
|
@@ -98733,13 +99564,12 @@ class ActionNotFoundError extends Error {
|
|
|
98733
99564
|
this.name = "ActionNotFoundError";
|
|
98734
99565
|
}
|
|
98735
99566
|
}
|
|
98736
|
-
async function performAction(session, body) {
|
|
99567
|
+
async function performAction(session, body, options) {
|
|
98737
99568
|
const locatorTap = Boolean((body.id || body.label) && (body.kind === "tap" || body.kind === "input"));
|
|
98738
99569
|
let x = body.x;
|
|
98739
99570
|
let y = body.y;
|
|
98740
99571
|
if (locatorTap) {
|
|
98741
|
-
const
|
|
98742
|
-
const elements = screen.elements ?? [];
|
|
99572
|
+
const elements = options?.screenElements ?? (await getScreen(session, { full: false })).elements ?? [];
|
|
98743
99573
|
const match2 = body.id ? findElementById(elements, body.id) : findElementByLabel(elements, body.label ?? "");
|
|
98744
99574
|
if (!match2) {
|
|
98745
99575
|
throw new ActionNotFoundError(body.id ? `No element matching id: ${body.id}` : `No element matching label: ${body.label}`);
|
|
@@ -98839,6 +99669,11 @@ async function performAction(session, body) {
|
|
|
98839
99669
|
// src/domains/runs/case-executor.ts
|
|
98840
99670
|
var MAX_STEPS_PER_CASE = 25;
|
|
98841
99671
|
var POST_ACTION_SETTLE_MS = 800;
|
|
99672
|
+
var GRID_ESCALATION_TAPS = 4;
|
|
99673
|
+
var TREE_ASSIST_ACTIONS = 2;
|
|
99674
|
+
var STUCK_WAITS = 3;
|
|
99675
|
+
var REPEAT_TAPS = 2;
|
|
99676
|
+
var REPEAT_TAP_RADIUS = 30;
|
|
98842
99677
|
var defaultClock = {
|
|
98843
99678
|
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
|
|
98844
99679
|
now: () => Date.now()
|
|
@@ -98885,6 +99720,12 @@ async function runTextAssert(input) {
|
|
|
98885
99720
|
await input.clock.sleep(400);
|
|
98886
99721
|
}
|
|
98887
99722
|
}
|
|
99723
|
+
function isPointTap(decision) {
|
|
99724
|
+
return decision.type === "tap" && decision.x != null && decision.y != null && !isSystemPermissionLabel(decision.label);
|
|
99725
|
+
}
|
|
99726
|
+
function isDeviceDecision(decision) {
|
|
99727
|
+
return decision.type !== "verify" && decision.type !== "done" && decision.type !== "fail";
|
|
99728
|
+
}
|
|
98888
99729
|
function commandForDecision(decision, defaultAppId) {
|
|
98889
99730
|
if (decision.type === "verify" || decision.type === "done" || decision.type === "fail") {
|
|
98890
99731
|
return null;
|
|
@@ -99261,25 +100102,49 @@ async function executeAgentCase(deps) {
|
|
|
99261
100102
|
let caseError = null;
|
|
99262
100103
|
let lastScreenshotUri = null;
|
|
99263
100104
|
const recordedDecisions = [];
|
|
100105
|
+
let inFlightPhases = null;
|
|
100106
|
+
let inFlightLatencyMs = 0;
|
|
99264
100107
|
const decideOnce = async (input) => {
|
|
99265
100108
|
const payload = {
|
|
99266
100109
|
auth: deps.auth,
|
|
99267
100110
|
appContext: deps.appContext,
|
|
100111
|
+
appKnowledge: deps.appKnowledge,
|
|
99268
100112
|
caseTitle: deps.catalogCase.name,
|
|
99269
100113
|
instructions: input.flow.instructions,
|
|
99270
100114
|
expectedResult: input.flow.expectedResult,
|
|
99271
100115
|
stepIndex: stepIdx,
|
|
99272
100116
|
imageBase64: input.imageBase64,
|
|
100117
|
+
image: input.image,
|
|
99273
100118
|
recentActions: input.recentActions,
|
|
99274
100119
|
screenSnapshot: input.screenSnapshot,
|
|
99275
100120
|
lastError: input.lastError,
|
|
99276
100121
|
defaultAppId: deps.defaultAppId,
|
|
99277
100122
|
completedInstructions: [...input.completedInstructions],
|
|
99278
100123
|
instructionOrdinal: input.instructionOrdinal,
|
|
99279
|
-
instructionCount: input.instructionCount
|
|
100124
|
+
instructionCount: input.instructionCount,
|
|
100125
|
+
coordGrid: input.coordGrid,
|
|
100126
|
+
screenshotOnly: input.screenshotOnly,
|
|
100127
|
+
onDecideRetry: input.onDecideRetry
|
|
100128
|
+
};
|
|
100129
|
+
const finish = (raw2) => {
|
|
100130
|
+
let next = coerceScrollIntentToSwipe(raw2);
|
|
100131
|
+
next = continueScrollingInsteadOfComplete({
|
|
100132
|
+
decision: next,
|
|
100133
|
+
instructions: input.flow.instructions,
|
|
100134
|
+
expectedResult: input.flow.expectedResult,
|
|
100135
|
+
recentActions: input.recentActions,
|
|
100136
|
+
lastSwipeMovedScreen: input.lastSwipeMovedScreen
|
|
100137
|
+
});
|
|
100138
|
+
next = releaseCanvasPoint(next, input.screenElements);
|
|
100139
|
+
if (input.screenshotOnly)
|
|
100140
|
+
next = forceScreenshotTap(next);
|
|
100141
|
+
if (input.coordGrid)
|
|
100142
|
+
next = applyGridPoint(next);
|
|
100143
|
+
return next;
|
|
99280
100144
|
};
|
|
99281
100145
|
let decision = await decide(payload);
|
|
99282
100146
|
if (isAbsurdNoScreenshotFail(decision)) {
|
|
100147
|
+
input.onDecideRetry?.();
|
|
99283
100148
|
decision = await decide(payload);
|
|
99284
100149
|
if (isAbsurdNoScreenshotFail(decision)) {
|
|
99285
100150
|
decision = {
|
|
@@ -99289,23 +100154,29 @@ async function executeAgentCase(deps) {
|
|
|
99289
100154
|
};
|
|
99290
100155
|
}
|
|
99291
100156
|
}
|
|
99292
|
-
decision =
|
|
99293
|
-
|
|
99294
|
-
|
|
99295
|
-
|
|
99296
|
-
|
|
99297
|
-
|
|
99298
|
-
|
|
99299
|
-
|
|
100157
|
+
decision = finish(decision);
|
|
100158
|
+
if (input.coordGrid && gridPointMissing(decision)) {
|
|
100159
|
+
input.onDecideRetry?.();
|
|
100160
|
+
const retried = await decide({ ...payload, lastError: GRID_CELL_RETRY });
|
|
100161
|
+
if (!isAbsurdNoScreenshotFail(retried))
|
|
100162
|
+
decision = finish(retried);
|
|
100163
|
+
} else if (input.screenshotOnly && !input.coordGrid && screenshotPointMissing(decision)) {
|
|
100164
|
+
input.onDecideRetry?.();
|
|
100165
|
+
const retried = await decide({ ...payload, lastError: SCREENSHOT_POINT_RETRY });
|
|
100166
|
+
if (!isAbsurdNoScreenshotFail(retried))
|
|
100167
|
+
decision = finish(retried);
|
|
100168
|
+
}
|
|
99300
100169
|
if ((decision.type === "activate-app" || decision.type === "terminate-app" || decision.type === "restart-app") && !decision.appId && deps.defaultAppId) {
|
|
99301
100170
|
decision = { ...decision, appId: deps.defaultAppId };
|
|
99302
100171
|
}
|
|
99303
100172
|
return decision;
|
|
99304
100173
|
};
|
|
99305
|
-
const applyDecision = async (decision) => {
|
|
100174
|
+
const applyDecision = async (decision, phases, screenElements) => {
|
|
99306
100175
|
if (decision.type === "wait") {
|
|
99307
100176
|
const waitMs = Math.min(3000, Math.max(500, decision.ms ?? 1500));
|
|
100177
|
+
const waitStarted = clock.now();
|
|
99308
100178
|
await clock.sleep(waitMs);
|
|
100179
|
+
phases.actionMs += clock.now() - waitStarted;
|
|
99309
100180
|
return "continue";
|
|
99310
100181
|
}
|
|
99311
100182
|
if (decision.type === "verify" || decision.type === "done") {
|
|
@@ -99317,6 +100188,7 @@ async function executeAgentCase(deps) {
|
|
|
99317
100188
|
if (decision.type === "assert") {
|
|
99318
100189
|
const assertion = decision.assertion === "not-visible" ? "not-visible" : "visible";
|
|
99319
100190
|
const timeoutMs = Math.min(60000, Math.max(1000, decision.timeoutMs ?? 5000));
|
|
100191
|
+
const assertStarted = clock.now();
|
|
99320
100192
|
await runTextAssert({
|
|
99321
100193
|
session: deps.session,
|
|
99322
100194
|
readScreen: readScreen2,
|
|
@@ -99326,14 +100198,18 @@ async function executeAgentCase(deps) {
|
|
|
99326
100198
|
text: decision.text ?? "",
|
|
99327
100199
|
timeoutMs
|
|
99328
100200
|
});
|
|
100201
|
+
phases.actionMs += clock.now() - assertStarted;
|
|
99329
100202
|
return "continue";
|
|
99330
100203
|
}
|
|
99331
100204
|
const body = decisionToActionRequest(decision, { defaultAppId: deps.defaultAppId });
|
|
99332
100205
|
if (!body) {
|
|
99333
100206
|
throw new Error(`${decision.type} is missing required fields`);
|
|
99334
100207
|
}
|
|
100208
|
+
const actionStarted = clock.now();
|
|
99335
100209
|
try {
|
|
99336
|
-
await perform(deps.session, body
|
|
100210
|
+
await perform(deps.session, body, {
|
|
100211
|
+
screenElements: screenElements.length > 0 ? screenElements : undefined
|
|
100212
|
+
});
|
|
99337
100213
|
} catch (error40) {
|
|
99338
100214
|
if (error40 instanceof ActionNotFoundError && (body.id || body.label) && decision.x != null && decision.y != null) {
|
|
99339
100215
|
await perform(deps.session, {
|
|
@@ -99347,8 +100223,12 @@ async function executeAgentCase(deps) {
|
|
|
99347
100223
|
} else {
|
|
99348
100224
|
throw error40;
|
|
99349
100225
|
}
|
|
100226
|
+
} finally {
|
|
100227
|
+
phases.actionMs += clock.now() - actionStarted;
|
|
99350
100228
|
}
|
|
100229
|
+
const settleStarted = clock.now();
|
|
99351
100230
|
await clock.sleep(settleMs);
|
|
100231
|
+
phases.settleMs += clock.now() - settleStarted;
|
|
99352
100232
|
return "continue";
|
|
99353
100233
|
};
|
|
99354
100234
|
try {
|
|
@@ -99356,6 +100236,65 @@ async function executeAgentCase(deps) {
|
|
|
99356
100236
|
const recentActions = [];
|
|
99357
100237
|
const completedInstructions = [];
|
|
99358
100238
|
let prevFingerprint = null;
|
|
100239
|
+
let lastVision = null;
|
|
100240
|
+
const screenMode = deps.screenMode ?? "vision";
|
|
100241
|
+
const gameApp = isGameApp(deps.appContext, deps.appKnowledge);
|
|
100242
|
+
let gridMode = gameApp;
|
|
100243
|
+
let unchangedActions = 0;
|
|
100244
|
+
let unchangedPointTaps = 0;
|
|
100245
|
+
let unchangedWaits = 0;
|
|
100246
|
+
let repeatTaps = 0;
|
|
100247
|
+
let lastTapPoint = null;
|
|
100248
|
+
const omitsTree = () => screenMode === "vision" || gridMode;
|
|
100249
|
+
const verdictForDecision = async (decision, input) => {
|
|
100250
|
+
if (decision.type === "fail") {
|
|
100251
|
+
return { type: "fail", reason: decision.reason, thoughts: decision.thoughts };
|
|
100252
|
+
}
|
|
100253
|
+
if (decision.type === "verify" || decision.type === "done") {
|
|
100254
|
+
return { type: "verify", reason: decision.reason, thoughts: decision.thoughts };
|
|
100255
|
+
}
|
|
100256
|
+
if (deps.verify) {
|
|
100257
|
+
return deps.verify({
|
|
100258
|
+
auth: deps.auth,
|
|
100259
|
+
appContext: deps.appContext,
|
|
100260
|
+
appKnowledge: deps.appKnowledge,
|
|
100261
|
+
caseTitle: deps.catalogCase.name,
|
|
100262
|
+
instructions: input.flow.instructions,
|
|
100263
|
+
expectedResult: input.flow.expectedResult,
|
|
100264
|
+
stepIndex: stepIdx,
|
|
100265
|
+
imageBase64: input.imageBase64,
|
|
100266
|
+
image: input.image,
|
|
100267
|
+
recentActions: input.recentActions,
|
|
100268
|
+
screenSnapshot: input.screenSnapshot,
|
|
100269
|
+
lastError: input.lastError,
|
|
100270
|
+
defaultAppId: deps.defaultAppId,
|
|
100271
|
+
completedInstructions: [...input.completedInstructions],
|
|
100272
|
+
instructionOrdinal: input.instructionOrdinal,
|
|
100273
|
+
instructionCount: input.instructionCount,
|
|
100274
|
+
coordGrid: input.coordGrid,
|
|
100275
|
+
screenshotOnly: input.screenshotOnly,
|
|
100276
|
+
onDecideRetry: input.onDecideRetry
|
|
100277
|
+
});
|
|
100278
|
+
}
|
|
100279
|
+
if (deps.decide) {
|
|
100280
|
+
return {
|
|
100281
|
+
type: "continue",
|
|
100282
|
+
reason: "Instruction still in progress",
|
|
100283
|
+
thoughts: decision.thoughts
|
|
100284
|
+
};
|
|
100285
|
+
}
|
|
100286
|
+
return verifyInstruction({
|
|
100287
|
+
auth: deps.auth,
|
|
100288
|
+
caseTitle: deps.catalogCase.name,
|
|
100289
|
+
instructions: input.flow.instructions,
|
|
100290
|
+
expectedResult: input.flow.expectedResult,
|
|
100291
|
+
imageBase64: input.imageBase64,
|
|
100292
|
+
image: input.image,
|
|
100293
|
+
instructionOrdinal: input.instructionOrdinal,
|
|
100294
|
+
instructionCount: input.instructionCount,
|
|
100295
|
+
onDecideRetry: input.onDecideRetry
|
|
100296
|
+
});
|
|
100297
|
+
};
|
|
99359
100298
|
for (let instructionIndex = 0;instructionIndex < instructionQueue.length; instructionIndex++) {
|
|
99360
100299
|
const instruction = instructionQueue[instructionIndex];
|
|
99361
100300
|
if (!instruction)
|
|
@@ -99365,17 +100304,158 @@ async function executeAgentCase(deps) {
|
|
|
99365
100304
|
break;
|
|
99366
100305
|
}
|
|
99367
100306
|
let instructionDone = false;
|
|
100307
|
+
let pending = null;
|
|
100308
|
+
let pendingElements = [];
|
|
99368
100309
|
for (let attempt = 0;attempt < maxSteps && !instructionDone; attempt++) {
|
|
99369
100310
|
if (deps.isAborted()) {
|
|
99370
100311
|
caseStatus = "cancelled";
|
|
99371
100312
|
instructionDone = true;
|
|
99372
100313
|
break;
|
|
99373
100314
|
}
|
|
99374
|
-
const
|
|
100315
|
+
const phases = {
|
|
100316
|
+
captureMs: 0,
|
|
100317
|
+
screenMs: 0,
|
|
100318
|
+
prepareMs: 0,
|
|
100319
|
+
decideMs: 0,
|
|
100320
|
+
actionMs: 0,
|
|
100321
|
+
settleMs: 0,
|
|
100322
|
+
decideRetries: 0
|
|
100323
|
+
};
|
|
100324
|
+
let performed = null;
|
|
100325
|
+
if (pending && isDeviceDecision(pending)) {
|
|
100326
|
+
const command2 = commandForDecision(pending, deps.defaultAppId);
|
|
100327
|
+
try {
|
|
100328
|
+
await withCurrentCommand(setCurrentCommand, command2, async () => {
|
|
100329
|
+
await applyDecision(pending, phases, pendingElements);
|
|
100330
|
+
});
|
|
100331
|
+
performed = pending;
|
|
100332
|
+
} catch (error40) {
|
|
100333
|
+
if (!isRetriableActionError(error40) || deps.isAborted())
|
|
100334
|
+
throw error40;
|
|
100335
|
+
const failed = pending;
|
|
100336
|
+
const retryStarted = clock.now();
|
|
100337
|
+
pending = await decideOnce({
|
|
100338
|
+
flow: instruction,
|
|
100339
|
+
imageBase64: lastVision?.imageBase64 ?? "",
|
|
100340
|
+
image: lastVision?.image ?? { base64: "", mediaType: "image/png" },
|
|
100341
|
+
recentActions: [...recentActions, failed],
|
|
100342
|
+
screenSnapshot: lastVision?.screenSnapshot ?? "",
|
|
100343
|
+
lastError: error40 instanceof Error ? error40.message : String(error40),
|
|
100344
|
+
lastSwipeMovedScreen: false,
|
|
100345
|
+
completedInstructions,
|
|
100346
|
+
instructionOrdinal: instructionIndex + 1,
|
|
100347
|
+
instructionCount: instructionQueue.length,
|
|
100348
|
+
screenElements: pendingElements,
|
|
100349
|
+
coordGrid: lastVision?.coordGrid ?? false,
|
|
100350
|
+
screenshotOnly: lastVision?.screenshotOnly ?? omitsTree(),
|
|
100351
|
+
onDecideRetry: () => {
|
|
100352
|
+
phases.decideRetries += 1;
|
|
100353
|
+
}
|
|
100354
|
+
});
|
|
100355
|
+
phases.decideMs += clock.now() - retryStarted;
|
|
100356
|
+
if (!isDeviceDecision(pending)) {
|
|
100357
|
+
performed = null;
|
|
100358
|
+
const terminal = pending;
|
|
100359
|
+
pending = null;
|
|
100360
|
+
const outcome2 = terminal.type === "fail" ? "fail" : "done";
|
|
100361
|
+
recentActions.push(terminal);
|
|
100362
|
+
recordedDecisions.push(terminal);
|
|
100363
|
+
await deps.appendStep({
|
|
100364
|
+
idx: stepIdx,
|
|
100365
|
+
action: {
|
|
100366
|
+
...terminal,
|
|
100367
|
+
cycle: {
|
|
100368
|
+
action: failed,
|
|
100369
|
+
decision: terminal,
|
|
100370
|
+
verify: {
|
|
100371
|
+
type: outcome2 === "fail" ? "fail" : "verify",
|
|
100372
|
+
reason: terminal.reason,
|
|
100373
|
+
thoughts: terminal.thoughts
|
|
100374
|
+
}
|
|
100375
|
+
}
|
|
100376
|
+
},
|
|
100377
|
+
screenshotUri: lastScreenshotUri,
|
|
100378
|
+
ok: outcome2 !== "fail",
|
|
100379
|
+
latencyMs: clock.now() - retryStarted,
|
|
100380
|
+
phases,
|
|
100381
|
+
detail: terminal.reason,
|
|
100382
|
+
command: commandForDecision(failed, deps.defaultAppId)
|
|
100383
|
+
});
|
|
100384
|
+
stepIdx += 1;
|
|
100385
|
+
if (outcome2 === "fail") {
|
|
100386
|
+
caseStatus = "errored";
|
|
100387
|
+
caseError = terminal.reason ?? "Agent marked the instruction as failed";
|
|
100388
|
+
} else {
|
|
100389
|
+
completedInstructions.push(instruction.instructions);
|
|
100390
|
+
}
|
|
100391
|
+
instructionDone = true;
|
|
100392
|
+
break;
|
|
100393
|
+
}
|
|
100394
|
+
const retryCommand = commandForDecision(pending, deps.defaultAppId);
|
|
100395
|
+
await withCurrentCommand(setCurrentCommand, retryCommand, async () => {
|
|
100396
|
+
await applyDecision(pending, phases, pendingElements);
|
|
100397
|
+
});
|
|
100398
|
+
recentActions.push(pending);
|
|
100399
|
+
performed = pending;
|
|
100400
|
+
}
|
|
100401
|
+
}
|
|
100402
|
+
let phaseStartedAt = clock.now();
|
|
100403
|
+
const shotStarted = phaseStartedAt;
|
|
100404
|
+
inFlightPhases = phases;
|
|
99375
100405
|
const shot = await deps.session.screenshot();
|
|
99376
100406
|
lastScreenshotUri = shot.path;
|
|
99377
|
-
const
|
|
100407
|
+
const advance = (phase) => {
|
|
100408
|
+
const now = clock.now();
|
|
100409
|
+
phases[phase] = now - phaseStartedAt;
|
|
100410
|
+
phaseStartedAt = now;
|
|
100411
|
+
};
|
|
100412
|
+
advance("captureMs");
|
|
99378
100413
|
const fingerprint = screenshotFingerprint(shot.base64);
|
|
100414
|
+
let escalatedToGrid = false;
|
|
100415
|
+
if (screenMode === "vision" && !gridMode && performed) {
|
|
100416
|
+
if (isPointTap(performed) && prevFingerprint != null && fingerprint === prevFingerprint) {
|
|
100417
|
+
unchangedPointTaps += 1;
|
|
100418
|
+
} else {
|
|
100419
|
+
unchangedPointTaps = 0;
|
|
100420
|
+
}
|
|
100421
|
+
if (unchangedPointTaps >= GRID_ESCALATION_TAPS) {
|
|
100422
|
+
gridMode = true;
|
|
100423
|
+
escalatedToGrid = true;
|
|
100424
|
+
}
|
|
100425
|
+
}
|
|
100426
|
+
if (performed && performed.type !== "wait") {
|
|
100427
|
+
unchangedActions = prevFingerprint != null && fingerprint === prevFingerprint ? unchangedActions + 1 : 0;
|
|
100428
|
+
}
|
|
100429
|
+
const treeAssist = screenMode === "vision" && !gameApp && unchangedActions >= TREE_ASSIST_ACTIONS;
|
|
100430
|
+
if (performed?.type === "wait") {
|
|
100431
|
+
unchangedWaits = prevFingerprint != null && fingerprint === prevFingerprint ? unchangedWaits + 1 : 0;
|
|
100432
|
+
} else if (performed) {
|
|
100433
|
+
unchangedWaits = 0;
|
|
100434
|
+
}
|
|
100435
|
+
if (performed && isPointTap(performed)) {
|
|
100436
|
+
const point = { x: performed.x, y: performed.y };
|
|
100437
|
+
const unchanged = prevFingerprint != null && fingerprint === prevFingerprint;
|
|
100438
|
+
const sameSpot = lastTapPoint != null && Math.hypot(point.x - lastTapPoint.x, point.y - lastTapPoint.y) <= REPEAT_TAP_RADIUS;
|
|
100439
|
+
repeatTaps = unchanged ? sameSpot ? repeatTaps + 1 : 1 : 0;
|
|
100440
|
+
lastTapPoint = point;
|
|
100441
|
+
} else if (performed) {
|
|
100442
|
+
repeatTaps = 0;
|
|
100443
|
+
lastTapPoint = null;
|
|
100444
|
+
}
|
|
100445
|
+
let tree = omitsTree() && !treeAssist ? { snapshot: NO_TREE_SNAPSHOT, elements: [] } : await readCleanedTree(readScreen2, deps.session);
|
|
100446
|
+
advance("screenMs");
|
|
100447
|
+
const canvas = !omitsTree() && isCanvasScreen(tree.elements);
|
|
100448
|
+
if ((!omitsTree() || treeAssist) && isGameSurface(tree.elements)) {
|
|
100449
|
+
gridMode = true;
|
|
100450
|
+
tree = { snapshot: NO_TREE_SNAPSHOT, elements: [] };
|
|
100451
|
+
}
|
|
100452
|
+
const assisted = treeAssist && tree.elements.length > 0;
|
|
100453
|
+
const screenshotOnly = omitsTree() && !assisted;
|
|
100454
|
+
const gridded = gridMode && !assisted || canvas ? overlayCoordGrid(shot.base64) : null;
|
|
100455
|
+
const stallHint = unchangedWaits >= STUCK_WAITS ? stuckWaitHint(unchangedWaits) : repeatTaps >= REPEAT_TAPS && lastTapPoint ? repeatTapHint(lastTapPoint.x, lastTapPoint.y, repeatTaps, assisted ? "tree" : gridMode ? "grid" : "vision") : undefined;
|
|
100456
|
+
const visionBase64 = gridded ?? shot.base64;
|
|
100457
|
+
const image = await prepareVisionImage(visionBase64);
|
|
100458
|
+
advance("prepareMs");
|
|
99379
100459
|
const lastAction = recentActions.at(-1);
|
|
99380
100460
|
const lastSwipeMovedScreen = lastAction?.type === "swipe" && prevFingerprint != null && fingerprint !== prevFingerprint;
|
|
99381
100461
|
if (deps.isAborted()) {
|
|
@@ -99385,69 +100465,106 @@ async function executeAgentCase(deps) {
|
|
|
99385
100465
|
}
|
|
99386
100466
|
const decideInput = {
|
|
99387
100467
|
flow: instruction,
|
|
99388
|
-
imageBase64:
|
|
100468
|
+
imageBase64: visionBase64,
|
|
100469
|
+
image,
|
|
99389
100470
|
recentActions,
|
|
99390
100471
|
screenSnapshot: tree.snapshot,
|
|
100472
|
+
lastError: stallHint,
|
|
99391
100473
|
lastSwipeMovedScreen,
|
|
99392
100474
|
completedInstructions,
|
|
99393
100475
|
instructionOrdinal: instructionIndex + 1,
|
|
99394
|
-
instructionCount: instructionQueue.length
|
|
100476
|
+
instructionCount: instructionQueue.length,
|
|
100477
|
+
screenElements: tree.elements,
|
|
100478
|
+
coordGrid: gridded != null,
|
|
100479
|
+
screenshotOnly,
|
|
100480
|
+
onDecideRetry: () => {
|
|
100481
|
+
phases.decideRetries += 1;
|
|
100482
|
+
}
|
|
100483
|
+
};
|
|
100484
|
+
lastVision = {
|
|
100485
|
+
imageBase64: visionBase64,
|
|
100486
|
+
image,
|
|
100487
|
+
screenSnapshot: tree.snapshot,
|
|
100488
|
+
coordGrid: gridded != null,
|
|
100489
|
+
screenshotOnly
|
|
99395
100490
|
};
|
|
99396
100491
|
let decision = await decideOnce(decideInput);
|
|
99397
100492
|
prevFingerprint = fingerprint;
|
|
99398
|
-
|
|
100493
|
+
let snappedTo = null;
|
|
100494
|
+
if (assisted) {
|
|
100495
|
+
const snapped = snapTapToElement(decision, tree.elements);
|
|
100496
|
+
decision = snapped.decision;
|
|
100497
|
+
snappedTo = snapped.element;
|
|
100498
|
+
}
|
|
100499
|
+
let verdict = await verdictForDecision(decision, decideInput);
|
|
100500
|
+
const guarded = continueScrollingInsteadOfComplete({
|
|
100501
|
+
decision: {
|
|
100502
|
+
type: "verify",
|
|
100503
|
+
reason: verdict.reason,
|
|
100504
|
+
thoughts: verdict.thoughts
|
|
100505
|
+
},
|
|
100506
|
+
instructions: instruction.instructions,
|
|
100507
|
+
expectedResult: instruction.expectedResult,
|
|
100508
|
+
recentActions,
|
|
100509
|
+
lastSwipeMovedScreen
|
|
100510
|
+
});
|
|
100511
|
+
if (verdict.type === "verify" && guarded.type !== "verify" && guarded.type !== "done") {
|
|
100512
|
+
verdict = {
|
|
100513
|
+
type: "continue",
|
|
100514
|
+
reason: guarded.reason,
|
|
100515
|
+
thoughts: guarded.thoughts
|
|
100516
|
+
};
|
|
100517
|
+
if (!isDeviceDecision(decision))
|
|
100518
|
+
decision = guarded;
|
|
100519
|
+
}
|
|
100520
|
+
advance("decideMs");
|
|
100521
|
+
const latencyMs = phaseStartedAt - shotStarted;
|
|
100522
|
+
inFlightLatencyMs = latencyMs;
|
|
99399
100523
|
if (deps.isAborted()) {
|
|
99400
100524
|
caseStatus = "cancelled";
|
|
99401
100525
|
instructionDone = true;
|
|
99402
100526
|
break;
|
|
99403
100527
|
}
|
|
99404
|
-
const
|
|
99405
|
-
|
|
99406
|
-
|
|
99407
|
-
|
|
99408
|
-
|
|
99409
|
-
|
|
99410
|
-
|
|
99411
|
-
decision
|
|
99412
|
-
|
|
99413
|
-
|
|
99414
|
-
|
|
99415
|
-
|
|
99416
|
-
|
|
99417
|
-
|
|
99418
|
-
|
|
99419
|
-
|
|
99420
|
-
|
|
99421
|
-
|
|
99422
|
-
|
|
99423
|
-
|
|
99424
|
-
|
|
99425
|
-
|
|
99426
|
-
|
|
99427
|
-
|
|
99428
|
-
|
|
99429
|
-
|
|
99430
|
-
|
|
99431
|
-
|
|
99432
|
-
});
|
|
99433
|
-
};
|
|
99434
|
-
const initialCommand = commandForDecision(decision, deps.defaultAppId);
|
|
99435
|
-
const outcome = initialCommand ? await withCurrentCommand(setCurrentCommand, initialCommand, async () => {
|
|
99436
|
-
const result = await applyWithRetry();
|
|
99437
|
-
await recordStep(commandForDecision(decision, deps.defaultAppId), result);
|
|
99438
|
-
return result;
|
|
99439
|
-
}) : await (async () => {
|
|
99440
|
-
const result = await applyWithRetry();
|
|
99441
|
-
await recordStep(null, result);
|
|
99442
|
-
return result;
|
|
99443
|
-
})();
|
|
100528
|
+
const outcome = verdict.type === "fail" ? "fail" : verdict.type === "verify" ? "done" : "continue";
|
|
100529
|
+
const command = commandForDecision(performed ?? decision, deps.defaultAppId);
|
|
100530
|
+
recentActions.push(decision);
|
|
100531
|
+
recordedDecisions.push(decision);
|
|
100532
|
+
await deps.appendStep({
|
|
100533
|
+
idx: stepIdx,
|
|
100534
|
+
action: {
|
|
100535
|
+
...decision,
|
|
100536
|
+
...escalatedToGrid ? { escalatedToGrid: true } : {},
|
|
100537
|
+
...assisted ? { treeAssist: true } : {},
|
|
100538
|
+
...snappedTo ? { snappedTo: snappedTo.id?.trim() || snappedTo.label } : {},
|
|
100539
|
+
cycle: {
|
|
100540
|
+
action: performed,
|
|
100541
|
+
decision,
|
|
100542
|
+
verify: {
|
|
100543
|
+
type: verdict.type,
|
|
100544
|
+
reason: verdict.reason,
|
|
100545
|
+
thoughts: verdict.thoughts
|
|
100546
|
+
}
|
|
100547
|
+
}
|
|
100548
|
+
},
|
|
100549
|
+
screenshotUri: shot.path,
|
|
100550
|
+
ok: outcome !== "fail",
|
|
100551
|
+
latencyMs,
|
|
100552
|
+
phases,
|
|
100553
|
+
detail: verdict.reason || (decision.type === "wait" ? decision.reason ?? `wait ${Math.min(3000, Math.max(500, decision.ms ?? 1500))}ms` : decision.reason ?? (outcome === "fail" ? "Agent failed the step" : null)),
|
|
100554
|
+
command
|
|
100555
|
+
});
|
|
99444
100556
|
if (outcome === "done") {
|
|
99445
100557
|
instructionDone = true;
|
|
99446
100558
|
completedInstructions.push(instruction.instructions);
|
|
100559
|
+
pending = null;
|
|
99447
100560
|
} else if (outcome === "fail") {
|
|
99448
100561
|
caseStatus = "errored";
|
|
99449
|
-
caseError = decision.reason
|
|
100562
|
+
caseError = verdict.reason || decision.reason || "Agent marked the instruction as failed";
|
|
99450
100563
|
instructionDone = true;
|
|
100564
|
+
pending = null;
|
|
100565
|
+
} else {
|
|
100566
|
+
pending = isDeviceDecision(decision) ? decision : null;
|
|
100567
|
+
pendingElements = tree.elements;
|
|
99451
100568
|
}
|
|
99452
100569
|
stepIdx += 1;
|
|
99453
100570
|
}
|
|
@@ -99474,7 +100591,8 @@ async function executeAgentCase(deps) {
|
|
|
99474
100591
|
},
|
|
99475
100592
|
screenshotUri: lastScreenshotUri,
|
|
99476
100593
|
ok: false,
|
|
99477
|
-
latencyMs:
|
|
100594
|
+
latencyMs: inFlightLatencyMs,
|
|
100595
|
+
phases: inFlightPhases,
|
|
99478
100596
|
detail: caseError,
|
|
99479
100597
|
command: null
|
|
99480
100598
|
});
|
|
@@ -99516,6 +100634,15 @@ function parseAction(raw2) {
|
|
|
99516
100634
|
return {};
|
|
99517
100635
|
}
|
|
99518
100636
|
}
|
|
100637
|
+
function parsePhases(raw2) {
|
|
100638
|
+
if (!raw2)
|
|
100639
|
+
return null;
|
|
100640
|
+
try {
|
|
100641
|
+
return stepPhasesSchema.parse(JSON.parse(raw2));
|
|
100642
|
+
} catch {
|
|
100643
|
+
return null;
|
|
100644
|
+
}
|
|
100645
|
+
}
|
|
99519
100646
|
function isAborted2(runId) {
|
|
99520
100647
|
return runControls.get(runId)?.aborted === true;
|
|
99521
100648
|
}
|
|
@@ -99535,6 +100662,9 @@ function parseRunExecutionMode(value) {
|
|
|
99535
100662
|
return value;
|
|
99536
100663
|
return "auto";
|
|
99537
100664
|
}
|
|
100665
|
+
function parseRunScreenMode(value) {
|
|
100666
|
+
return value === "vision" ? "vision" : "tree";
|
|
100667
|
+
}
|
|
99538
100668
|
function resolveCaseExecutionMode(runMode, hasScript) {
|
|
99539
100669
|
if (runMode === "agent")
|
|
99540
100670
|
return "agent";
|
|
@@ -99559,6 +100689,7 @@ async function loadRun(runId) {
|
|
|
99559
100689
|
screenshotUri: step.screenshotUri,
|
|
99560
100690
|
ok: step.ok === 1,
|
|
99561
100691
|
latencyMs: step.latencyMs,
|
|
100692
|
+
phases: parsePhases(step.phasesJson),
|
|
99562
100693
|
detail: step.detail,
|
|
99563
100694
|
command: step.command ?? null,
|
|
99564
100695
|
createdAt: step.createdAt
|
|
@@ -99585,6 +100716,7 @@ async function loadRun(runId) {
|
|
|
99585
100716
|
buildId: runRow.buildId,
|
|
99586
100717
|
status: runRow.status,
|
|
99587
100718
|
executionMode: parseRunExecutionMode(runRow.executionMode),
|
|
100719
|
+
screenMode: parseRunScreenMode(runRow.screenMode),
|
|
99588
100720
|
error: runRow.error,
|
|
99589
100721
|
createdAt: runRow.createdAt,
|
|
99590
100722
|
startedAt: runRow.startedAt,
|
|
@@ -99602,6 +100734,7 @@ async function appendStep(input) {
|
|
|
99602
100734
|
screenshotUri: input.screenshotUri,
|
|
99603
100735
|
ok: input.ok ? 1 : 0,
|
|
99604
100736
|
latencyMs: Math.max(0, Math.round(input.latencyMs)),
|
|
100737
|
+
phasesJson: input.phases ? JSON.stringify(input.phases) : null,
|
|
99605
100738
|
detail: input.detail,
|
|
99606
100739
|
command: input.command,
|
|
99607
100740
|
createdAt: Date.now()
|
|
@@ -99668,6 +100801,7 @@ async function executeAgentCase2(input) {
|
|
|
99668
100801
|
return executeAgentCase({
|
|
99669
100802
|
catalogCase,
|
|
99670
100803
|
appContext: input.appContext,
|
|
100804
|
+
appKnowledge: input.appKnowledge,
|
|
99671
100805
|
auth: input.auth,
|
|
99672
100806
|
session: input.session,
|
|
99673
100807
|
isAborted: () => isAborted2(input.runId),
|
|
@@ -99681,6 +100815,7 @@ async function executeAgentCase2(input) {
|
|
|
99681
100815
|
await setCurrentCommand(input.runTestId, command);
|
|
99682
100816
|
},
|
|
99683
100817
|
decide: decideNextAction,
|
|
100818
|
+
screenMode: input.screenMode,
|
|
99684
100819
|
clock: { sleep, now: () => Date.now() },
|
|
99685
100820
|
defaultAppId: input.defaultAppId
|
|
99686
100821
|
});
|
|
@@ -99722,8 +100857,10 @@ async function executeCase(input) {
|
|
|
99722
100857
|
runTestId: input.runTestId,
|
|
99723
100858
|
caseId: input.caseId,
|
|
99724
100859
|
appContext: input.appContext,
|
|
100860
|
+
appKnowledge: input.appKnowledge,
|
|
99725
100861
|
session: input.session,
|
|
99726
100862
|
auth: input.auth,
|
|
100863
|
+
screenMode: input.screenMode,
|
|
99727
100864
|
defaultAppId: input.defaultAppId
|
|
99728
100865
|
});
|
|
99729
100866
|
caseStatus = result.status;
|
|
@@ -99764,6 +100901,7 @@ async function executeRun(runId) {
|
|
|
99764
100901
|
}
|
|
99765
100902
|
const startedAt = Date.now();
|
|
99766
100903
|
await db.update(runs).set({ status: "running", startedAt, error: null }).where(eq(runs.id, runId));
|
|
100904
|
+
pruneOldRunScreenshots();
|
|
99767
100905
|
if (isAborted2(runId)) {
|
|
99768
100906
|
await persistCancelled(runId);
|
|
99769
100907
|
return;
|
|
@@ -99834,9 +100972,11 @@ async function executeRun(runId) {
|
|
|
99834
100972
|
runTestId: test.id,
|
|
99835
100973
|
caseId: test.caseId,
|
|
99836
100974
|
appContext: app.context,
|
|
100975
|
+
appKnowledge: app.knowledge,
|
|
99837
100976
|
session,
|
|
99838
100977
|
auth,
|
|
99839
100978
|
caseMode,
|
|
100979
|
+
screenMode: run.screenMode ?? "tree",
|
|
99840
100980
|
defaultAppId: run.platform === "ios" ? app.iosBundleId || undefined : app.androidApplicationId || undefined
|
|
99841
100981
|
});
|
|
99842
100982
|
if (status === "cancelled" || isAborted2(runId)) {
|
|
@@ -99901,6 +101041,7 @@ async function createRun(input) {
|
|
|
99901
101041
|
throw new RunValidationError("App not found");
|
|
99902
101042
|
}
|
|
99903
101043
|
const executionMode = input.executionMode ?? "auto";
|
|
101044
|
+
const screenMode = input.screenMode ?? "vision";
|
|
99904
101045
|
const catalogCases = [];
|
|
99905
101046
|
for (const caseId of uniqueCaseIds) {
|
|
99906
101047
|
const catalogCase = await getCase(caseId);
|
|
@@ -99942,6 +101083,7 @@ async function createRun(input) {
|
|
|
99942
101083
|
buildId,
|
|
99943
101084
|
status: "queued",
|
|
99944
101085
|
executionMode,
|
|
101086
|
+
screenMode,
|
|
99945
101087
|
error: null,
|
|
99946
101088
|
createdAt: now,
|
|
99947
101089
|
startedAt: null,
|
|
@@ -100011,6 +101153,7 @@ async function listRuns(appId) {
|
|
|
100011
101153
|
buildId: runRow.buildId,
|
|
100012
101154
|
status: runRow.status,
|
|
100013
101155
|
executionMode: parseRunExecutionMode(runRow.executionMode),
|
|
101156
|
+
screenMode: parseRunScreenMode(runRow.screenMode),
|
|
100014
101157
|
error: runRow.error,
|
|
100015
101158
|
createdAt: runRow.createdAt,
|
|
100016
101159
|
startedAt: runRow.startedAt,
|
|
@@ -100245,8 +101388,84 @@ function createServersRoutes() {
|
|
|
100245
101388
|
}
|
|
100246
101389
|
|
|
100247
101390
|
// src/interfaces/http/session.ts
|
|
100248
|
-
import { mkdir as
|
|
101391
|
+
import { mkdir as mkdir5, rename as rename2 } from "fs/promises";
|
|
100249
101392
|
import { dirname as dirname4 } from "path";
|
|
101393
|
+
|
|
101394
|
+
// src/domains/devices/action-result.ts
|
|
101395
|
+
import { mkdir as mkdir4 } from "fs/promises";
|
|
101396
|
+
import { join as join16 } from "path";
|
|
101397
|
+
var DEFAULT_SETTLE_CAP_MS = 1500;
|
|
101398
|
+
var SETTLE_POLL_MS = 250;
|
|
101399
|
+
var realClock = { now: () => Date.now(), sleep: (ms) => Bun.sleep(ms) };
|
|
101400
|
+
function frameHash(base643) {
|
|
101401
|
+
return String(Bun.hash(base643));
|
|
101402
|
+
}
|
|
101403
|
+
async function settleScreen(capture, options = {}) {
|
|
101404
|
+
const capMs = options.capMs ?? DEFAULT_SETTLE_CAP_MS;
|
|
101405
|
+
const pollMs = options.pollMs ?? SETTLE_POLL_MS;
|
|
101406
|
+
const clock = options.clock ?? realClock;
|
|
101407
|
+
const started = clock.now();
|
|
101408
|
+
let latest;
|
|
101409
|
+
let previousHash;
|
|
101410
|
+
for (;; ) {
|
|
101411
|
+
const elapsed = clock.now() - started;
|
|
101412
|
+
if (latest !== undefined && elapsed >= capMs) {
|
|
101413
|
+
return { base64: latest, settled: false, waitedMs: elapsed };
|
|
101414
|
+
}
|
|
101415
|
+
await clock.sleep(Math.max(0, Math.min(pollMs, capMs - elapsed)));
|
|
101416
|
+
latest = (await capture()).base64;
|
|
101417
|
+
const hash = frameHash(latest);
|
|
101418
|
+
if (previousHash === hash) {
|
|
101419
|
+
return { base64: latest, settled: true, waitedMs: clock.now() - started };
|
|
101420
|
+
}
|
|
101421
|
+
previousHash = hash;
|
|
101422
|
+
}
|
|
101423
|
+
}
|
|
101424
|
+
function actionMarks(body, resolved) {
|
|
101425
|
+
const x = resolved?.x ?? body.x;
|
|
101426
|
+
const y = resolved?.y ?? body.y;
|
|
101427
|
+
if (x == null || y == null)
|
|
101428
|
+
return [];
|
|
101429
|
+
if ((body.kind === "swipe" || body.kind === "drag") && body.x2 != null && body.y2 != null) {
|
|
101430
|
+
return [{ kind: "path", x, y, x2: body.x2, y2: body.y2 }];
|
|
101431
|
+
}
|
|
101432
|
+
if (body.kind === "tap" || body.kind === "input")
|
|
101433
|
+
return [{ kind: "tap", x, y }];
|
|
101434
|
+
return [];
|
|
101435
|
+
}
|
|
101436
|
+
async function writePng(dir, base643, label) {
|
|
101437
|
+
await mkdir4(dir, { recursive: true });
|
|
101438
|
+
const path = join16(dir, `${label}_${Date.now()}_${crypto.randomUUID()}.png`);
|
|
101439
|
+
await Bun.write(path, Uint8Array.from(Buffer.from(base643, "base64")));
|
|
101440
|
+
return path;
|
|
101441
|
+
}
|
|
101442
|
+
async function performActionWithScreenshot(session, body, deps = {}) {
|
|
101443
|
+
const perform = deps.perform ?? performAction;
|
|
101444
|
+
const before = await session.captureFrame().then((frame) => frame.base64, () => {
|
|
101445
|
+
return;
|
|
101446
|
+
});
|
|
101447
|
+
const response = await perform(session, body);
|
|
101448
|
+
const settle = await settleScreen(() => session.captureFrame(), {
|
|
101449
|
+
capMs: body.settleMs,
|
|
101450
|
+
pollMs: deps.pollMs,
|
|
101451
|
+
clock: deps.clock
|
|
101452
|
+
});
|
|
101453
|
+
const dir = deps.dir ?? SCREENSHOT_DIR;
|
|
101454
|
+
const path = await writePng(dir, settle.base64, "result");
|
|
101455
|
+
const marks = actionMarks(body, response.resolved);
|
|
101456
|
+
const annotated = overlayActionMarks(settle.base64, marks);
|
|
101457
|
+
const annotatedPath = annotated ? await writePng(dir, annotated, "result_marked") : undefined;
|
|
101458
|
+
const screenshot = {
|
|
101459
|
+
path,
|
|
101460
|
+
...annotatedPath ? { annotatedPath } : {},
|
|
101461
|
+
settled: settle.settled,
|
|
101462
|
+
waitedMs: settle.waitedMs,
|
|
101463
|
+
changed: settle.settled && before !== undefined ? frameHash(before) !== frameHash(settle.base64) : null
|
|
101464
|
+
};
|
|
101465
|
+
return { ...response, screenshot };
|
|
101466
|
+
}
|
|
101467
|
+
|
|
101468
|
+
// src/interfaces/http/session.ts
|
|
100250
101469
|
function sessionErrorResponse(error40) {
|
|
100251
101470
|
const message = error40 instanceof Error ? error40.message : String(error40);
|
|
100252
101471
|
if (error40 instanceof SessionBusyError) {
|
|
@@ -100345,7 +101564,7 @@ function createSessionRoutes() {
|
|
|
100345
101564
|
const shot = await session.screenshot();
|
|
100346
101565
|
let path = shot.path;
|
|
100347
101566
|
if (parsed.data.path) {
|
|
100348
|
-
await
|
|
101567
|
+
await mkdir5(dirname4(parsed.data.path), { recursive: true });
|
|
100349
101568
|
await rename2(shot.path, parsed.data.path);
|
|
100350
101569
|
path = parsed.data.path;
|
|
100351
101570
|
}
|
|
@@ -100439,7 +101658,7 @@ function createSessionRoutes() {
|
|
|
100439
101658
|
error: "A run is using this device session. Cancel the run to interact manually."
|
|
100440
101659
|
}, 409);
|
|
100441
101660
|
}
|
|
100442
|
-
const result = await performAction(session, parsed.data);
|
|
101661
|
+
const result = parsed.data.screenshot ? await performActionWithScreenshot(session, parsed.data) : await performAction(session, parsed.data);
|
|
100443
101662
|
return c.json(actionResponseSchema.parse(result));
|
|
100444
101663
|
} catch (error40) {
|
|
100445
101664
|
const gone = sessionErrorResponse(error40);
|