@yoqa/runner 0.3.26 → 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 +1393 -162
- 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
|
|
@@ -17596,10 +17623,13 @@ function isUnsupportedCommandError(error) {
|
|
|
17596
17623
|
const message = error instanceof Error ? error.message : String(error);
|
|
17597
17624
|
return /unknown command|unknown method|not (yet )?(been )?implemented|not (yet )?supported|does not support/i.test(message);
|
|
17598
17625
|
}
|
|
17626
|
+
var W3C_RETURN_KEY = "\uE007";
|
|
17599
17627
|
function w3cKeyActions(text2) {
|
|
17600
17628
|
const actions = [];
|
|
17601
17629
|
for (const ch of text2) {
|
|
17602
|
-
|
|
17630
|
+
const value = ch === `
|
|
17631
|
+
` || ch === "\r" ? W3C_RETURN_KEY : ch;
|
|
17632
|
+
actions.push({ type: "keyDown", value }, { type: "keyUp", value });
|
|
17603
17633
|
}
|
|
17604
17634
|
return [{ type: "key", id: "keyboard", actions }];
|
|
17605
17635
|
}
|
|
@@ -17623,10 +17653,60 @@ async function typeText(browser, text2) {
|
|
|
17623
17653
|
return "w3c";
|
|
17624
17654
|
}
|
|
17625
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
|
+
|
|
17626
17707
|
// src/domains/devices/session.ts
|
|
17627
|
-
var YOQA_ROOT4 =
|
|
17708
|
+
var YOQA_ROOT4 = join8(homedir7(), ".yoqa");
|
|
17628
17709
|
var DEFAULT_MJPEG_PORT = Number(process.env.YOQA_MJPEG_PORT ?? "9100");
|
|
17629
|
-
var SCREENSHOT_DIR = join7(YOQA_ROOT4, "runs", "screenshots");
|
|
17630
17710
|
var MJPEG_SETTINGS_BASE = {
|
|
17631
17711
|
mjpegServerScreenshotQuality: 35,
|
|
17632
17712
|
mjpegScalingFactor: 50
|
|
@@ -17744,7 +17824,7 @@ function looksLikePhysicalIosUdid(udid) {
|
|
|
17744
17824
|
}
|
|
17745
17825
|
var PHYSICAL_IOS_SESSION_TIMEOUT_MS = 60000;
|
|
17746
17826
|
var SIMULATOR_WDA_SESSION_TIMEOUT_MS = 600000;
|
|
17747
|
-
var SIMULATOR_WDA_DERIVED_DATA =
|
|
17827
|
+
var SIMULATOR_WDA_DERIVED_DATA = join8(YOQA_ROOT4, "wda-sim");
|
|
17748
17828
|
function iosSessionCreateTimeoutMs(deviceId) {
|
|
17749
17829
|
return looksLikePhysicalIosUdid(deviceId) ? PHYSICAL_IOS_SESSION_TIMEOUT_MS : SIMULATOR_WDA_SESSION_TIMEOUT_MS;
|
|
17750
17830
|
}
|
|
@@ -18089,7 +18169,7 @@ async function createDeviceSession(options) {
|
|
|
18089
18169
|
const screenshot = async () => {
|
|
18090
18170
|
await mkdir3(SCREENSHOT_DIR, { recursive: true });
|
|
18091
18171
|
const frame = await captureFrame();
|
|
18092
|
-
const path =
|
|
18172
|
+
const path = join8(SCREENSHOT_DIR, `shot_${Date.now()}_${crypto.randomUUID()}.png`);
|
|
18093
18173
|
await Bun.write(path, Uint8Array.from(Buffer.from(frame.base64, "base64")));
|
|
18094
18174
|
return { path, base64: frame.base64 };
|
|
18095
18175
|
};
|
|
@@ -24284,6 +24364,7 @@ var catalogAppSchema = exports_external.object({
|
|
|
24284
24364
|
name: exports_external.string().min(1),
|
|
24285
24365
|
prefix: exports_external.string().min(1),
|
|
24286
24366
|
context: exports_external.string(),
|
|
24367
|
+
knowledge: exports_external.string(),
|
|
24287
24368
|
iosBundleId: exports_external.string(),
|
|
24288
24369
|
iosAppStoreId: exports_external.string(),
|
|
24289
24370
|
androidApplicationId: exports_external.string(),
|
|
@@ -24299,6 +24380,7 @@ var updateAppRequestSchema = exports_external.object({
|
|
|
24299
24380
|
name: exports_external.string().min(1).optional(),
|
|
24300
24381
|
prefix: exports_external.string().min(1).optional(),
|
|
24301
24382
|
context: exports_external.string().optional(),
|
|
24383
|
+
knowledge: exports_external.string().optional(),
|
|
24302
24384
|
iosBundleId: exports_external.string().optional(),
|
|
24303
24385
|
iosAppStoreId: exports_external.string().optional(),
|
|
24304
24386
|
androidApplicationId: exports_external.string().optional(),
|
|
@@ -24612,7 +24694,8 @@ var probeProviderResponseSchema = exports_external.object({
|
|
|
24612
24694
|
var providerModelSchema = exports_external.object({
|
|
24613
24695
|
id: exports_external.string().min(1),
|
|
24614
24696
|
name: exports_external.string().min(1),
|
|
24615
|
-
provider: exports_external.string().min(1).optional()
|
|
24697
|
+
provider: exports_external.string().min(1).optional(),
|
|
24698
|
+
vision: exports_external.boolean().optional()
|
|
24616
24699
|
});
|
|
24617
24700
|
var listProviderModelsResponseSchema = exports_external.object({
|
|
24618
24701
|
models: exports_external.array(providerModelSchema),
|
|
@@ -24641,6 +24724,7 @@ var runExecutionModeSchema = exports_external.union([
|
|
|
24641
24724
|
exports_external.literal("script"),
|
|
24642
24725
|
exports_external.literal("agent")
|
|
24643
24726
|
]);
|
|
24727
|
+
var runScreenModeSchema = exports_external.union([exports_external.literal("vision"), exports_external.literal("tree")]);
|
|
24644
24728
|
var createRunRequestSchema = exports_external.object({
|
|
24645
24729
|
appId: exports_external.string().min(1),
|
|
24646
24730
|
caseIds: exports_external.array(exports_external.string().min(1)).min(1),
|
|
@@ -24648,7 +24732,17 @@ var createRunRequestSchema = exports_external.object({
|
|
|
24648
24732
|
platform: devicePlatformSchema,
|
|
24649
24733
|
buildId: exports_external.string().min(1).optional(),
|
|
24650
24734
|
buildPath: exports_external.string().min(1).optional(),
|
|
24651
|
-
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()
|
|
24652
24746
|
});
|
|
24653
24747
|
var runStepSchema = exports_external.object({
|
|
24654
24748
|
id: exports_external.string().min(1),
|
|
@@ -24658,6 +24752,7 @@ var runStepSchema = exports_external.object({
|
|
|
24658
24752
|
screenshotUri: exports_external.string().nullable(),
|
|
24659
24753
|
ok: exports_external.boolean(),
|
|
24660
24754
|
latencyMs: exports_external.number().nonnegative(),
|
|
24755
|
+
phases: stepPhasesSchema.nullable().optional(),
|
|
24661
24756
|
detail: exports_external.string().nullable(),
|
|
24662
24757
|
command: exports_external.string().nullable().optional(),
|
|
24663
24758
|
createdAt: exports_external.number().int().nonnegative()
|
|
@@ -24682,6 +24777,7 @@ var runSchema = exports_external.object({
|
|
|
24682
24777
|
buildId: exports_external.string().nullable(),
|
|
24683
24778
|
status: runStatusSchema,
|
|
24684
24779
|
executionMode: runExecutionModeSchema,
|
|
24780
|
+
screenMode: runScreenModeSchema.optional(),
|
|
24685
24781
|
error: exports_external.string().nullable(),
|
|
24686
24782
|
createdAt: exports_external.number().int().nonnegative(),
|
|
24687
24783
|
startedAt: exports_external.number().int().nonnegative().nullable(),
|
|
@@ -24768,7 +24864,16 @@ var actionRequestSchema = exports_external.object({
|
|
|
24768
24864
|
appId: exports_external.string().optional(),
|
|
24769
24865
|
url: exports_external.string().optional(),
|
|
24770
24866
|
alertAction: exports_external.union([exports_external.literal("accept"), exports_external.literal("dismiss")]).optional(),
|
|
24771
|
-
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()
|
|
24772
24877
|
});
|
|
24773
24878
|
var actionResponseSchema = exports_external.object({
|
|
24774
24879
|
ok: exports_external.literal(true),
|
|
@@ -24776,7 +24881,8 @@ var actionResponseSchema = exports_external.object({
|
|
|
24776
24881
|
resolved: exports_external.object({
|
|
24777
24882
|
x: exports_external.number().optional(),
|
|
24778
24883
|
y: exports_external.number().optional()
|
|
24779
|
-
}).optional()
|
|
24884
|
+
}).optional(),
|
|
24885
|
+
screenshot: actionResultScreenshotSchema.optional()
|
|
24780
24886
|
});
|
|
24781
24887
|
var yoqaStatusResponseSchema = exports_external.object({
|
|
24782
24888
|
runner: exports_external.object({
|
|
@@ -25350,9 +25456,9 @@ function normalizeVisionJson(value) {
|
|
|
25350
25456
|
|
|
25351
25457
|
// src/domains/providers/vision-model.ts
|
|
25352
25458
|
import { mkdtemp as mkdtemp3, readFile as readFile2, rm as rm3, writeFile as writeFile3 } from "fs/promises";
|
|
25353
|
-
import { homedir as
|
|
25459
|
+
import { homedir as homedir8 } from "os";
|
|
25354
25460
|
import { tmpdir as tmpdir3 } from "os";
|
|
25355
|
-
import { join as
|
|
25461
|
+
import { join as join9 } from "path";
|
|
25356
25462
|
|
|
25357
25463
|
// ../../node_modules/@ai-sdk/gateway/node_modules/@ai-sdk/provider/dist/index.js
|
|
25358
25464
|
var marker = "vercel.ai.error";
|
|
@@ -48024,9 +48130,9 @@ async function prepareVisionImage(pngBase64) {
|
|
|
48024
48130
|
if (process.platform !== "darwin") {
|
|
48025
48131
|
return { base64: pngBase64, mediaType: "image/png" };
|
|
48026
48132
|
}
|
|
48027
|
-
const dir = await mkdtemp3(
|
|
48028
|
-
const inPath =
|
|
48029
|
-
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");
|
|
48030
48136
|
try {
|
|
48031
48137
|
await writeFile3(inPath, new Uint8Array(bytes));
|
|
48032
48138
|
const proc = Bun.spawn([
|
|
@@ -48080,6 +48186,9 @@ function formatProviderHttpError(label, status, body) {
|
|
|
48080
48186
|
if (label === "OpenCode" && status >= 500 && (body.includes("Internal server error") || body.includes('"type":"error"'))) {
|
|
48081
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).`;
|
|
48082
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
|
+
}
|
|
48083
48192
|
return `${label} request failed (${status}): ${body.slice(0, 400)}`;
|
|
48084
48193
|
}
|
|
48085
48194
|
function isJsonRepairableError(error40) {
|
|
@@ -48134,7 +48243,7 @@ async function completeWithAiSdk(input) {
|
|
|
48134
48243
|
model: input.model,
|
|
48135
48244
|
schema: input.schema,
|
|
48136
48245
|
system: input.system,
|
|
48137
|
-
maxOutputTokens: VISION_MAX_TOKENS,
|
|
48246
|
+
maxOutputTokens: input.maxOutputTokens ?? VISION_MAX_TOKENS,
|
|
48138
48247
|
experimental_repairText: async ({ text: text3 }) => salvageAgentJsonText(text3),
|
|
48139
48248
|
messages: [
|
|
48140
48249
|
{
|
|
@@ -48172,6 +48281,7 @@ async function completeWithAiSdk(input) {
|
|
|
48172
48281
|
if (!(error40 instanceof AgentProviderError) || !isJsonRepairableError(error40)) {
|
|
48173
48282
|
throw error40;
|
|
48174
48283
|
}
|
|
48284
|
+
input.onDecideRetry?.();
|
|
48175
48285
|
return run(`${input.prompt}
|
|
48176
48286
|
|
|
48177
48287
|
${JSON_REPAIR_PROMPT}`);
|
|
@@ -48181,7 +48291,7 @@ function createSdkVisionPort(opts) {
|
|
|
48181
48291
|
return {
|
|
48182
48292
|
async completeObject(input) {
|
|
48183
48293
|
const modelId = input.auth.defaultModel?.trim() || opts.defaultModel;
|
|
48184
|
-
const image = await prepareVisionImage(input.imageBase64);
|
|
48294
|
+
const image = input.image ?? await prepareVisionImage(input.imageBase64);
|
|
48185
48295
|
const model = await opts.createModel(input.auth, modelId);
|
|
48186
48296
|
return completeWithAiSdk({
|
|
48187
48297
|
label: opts.label,
|
|
@@ -48189,7 +48299,9 @@ function createSdkVisionPort(opts) {
|
|
|
48189
48299
|
schema: input.schema,
|
|
48190
48300
|
system: input.system,
|
|
48191
48301
|
prompt: input.prompt,
|
|
48192
|
-
image
|
|
48302
|
+
image,
|
|
48303
|
+
onDecideRetry: input.onDecideRetry,
|
|
48304
|
+
maxOutputTokens: opts.maxOutputTokens
|
|
48193
48305
|
});
|
|
48194
48306
|
}
|
|
48195
48307
|
};
|
|
@@ -48201,8 +48313,8 @@ function openCodeAuthPaths() {
|
|
|
48201
48313
|
const paths = [];
|
|
48202
48314
|
const xdg = process.env.XDG_DATA_HOME?.trim();
|
|
48203
48315
|
if (xdg)
|
|
48204
|
-
paths.push(
|
|
48205
|
-
paths.push(
|
|
48316
|
+
paths.push(join9(xdg, "opencode", "auth.json"));
|
|
48317
|
+
paths.push(join9(homedir8(), ".local", "share", "opencode", "auth.json"));
|
|
48206
48318
|
return paths;
|
|
48207
48319
|
}
|
|
48208
48320
|
function apiKeyFromOpenCodeAuthEntry(value) {
|
|
@@ -48295,6 +48407,64 @@ function resolveGoogleKey(auth) {
|
|
|
48295
48407
|
function resolveVertexApiKey(auth) {
|
|
48296
48408
|
return auth.apiKey?.trim() || auth.env.GOOGLE_VERTEX_API_KEY?.trim() || process.env.GOOGLE_VERTEX_API_KEY?.trim() || null;
|
|
48297
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
|
+
}
|
|
48298
48468
|
function withOpenCodeRequestHooks(opts) {
|
|
48299
48469
|
const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
|
|
48300
48470
|
return async (input, init) => {
|
|
@@ -57355,8 +57525,8 @@ var anthropic = createAnthropic();
|
|
|
57355
57525
|
// src/domains/providers/drivers/probe.ts
|
|
57356
57526
|
import { constants } from "fs";
|
|
57357
57527
|
import { access } from "fs/promises";
|
|
57358
|
-
import { homedir as
|
|
57359
|
-
import { isAbsolute, join as
|
|
57528
|
+
import { homedir as homedir9 } from "os";
|
|
57529
|
+
import { isAbsolute, join as join10 } from "path";
|
|
57360
57530
|
async function pathExists3(path) {
|
|
57361
57531
|
try {
|
|
57362
57532
|
await access(path, constants.F_OK);
|
|
@@ -57381,13 +57551,13 @@ async function which2(binary) {
|
|
|
57381
57551
|
`)[0]?.trim();
|
|
57382
57552
|
return resolved || null;
|
|
57383
57553
|
}
|
|
57384
|
-
function knownHomeBinCandidates(binaryName, home =
|
|
57554
|
+
function knownHomeBinCandidates(binaryName, home = homedir9()) {
|
|
57385
57555
|
return [
|
|
57386
|
-
|
|
57387
|
-
|
|
57388
|
-
|
|
57389
|
-
|
|
57390
|
-
|
|
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),
|
|
57391
57561
|
`/opt/homebrew/bin/${binaryName}`,
|
|
57392
57562
|
`/usr/local/bin/${binaryName}`
|
|
57393
57563
|
];
|
|
@@ -57620,7 +57790,7 @@ var anthropicDriver = {
|
|
|
57620
57790
|
// src/domains/providers/drivers/antigravity-vision.ts
|
|
57621
57791
|
import { mkdtemp as mkdtemp4, rm as rm4, writeFile as writeFile4 } from "fs/promises";
|
|
57622
57792
|
import { tmpdir as tmpdir4 } from "os";
|
|
57623
|
-
import { join as
|
|
57793
|
+
import { join as join11 } from "path";
|
|
57624
57794
|
|
|
57625
57795
|
// ../../node_modules/@ai-sdk/google/node_modules/@ai-sdk/provider/dist/index.js
|
|
57626
57796
|
var marker29 = "vercel.ai.error";
|
|
@@ -67093,11 +67263,11 @@ async function completeWithAgyCli(input, repairHint) {
|
|
|
67093
67263
|
if (!resolved.path) {
|
|
67094
67264
|
throw new AgentProviderError(resolved.detail || "Antigravity CLI (`agy`) not found. Install it or paste a Google AI Studio API key.");
|
|
67095
67265
|
}
|
|
67096
|
-
const image = await prepareVisionImage(input.imageBase64);
|
|
67266
|
+
const image = input.image ?? await prepareVisionImage(input.imageBase64);
|
|
67097
67267
|
const model = input.auth.defaultModel?.trim() || ANTIGRAVITY_DEFAULT_VISION_MODEL;
|
|
67098
67268
|
const ext = image.mediaType === "image/jpeg" ? "jpg" : "png";
|
|
67099
|
-
const dir = await mkdtemp4(
|
|
67100
|
-
const shotPath =
|
|
67269
|
+
const dir = await mkdtemp4(join11(tmpdir4(), "yoqa-agy-"));
|
|
67270
|
+
const shotPath = join11(dir, `shot.${ext}`);
|
|
67101
67271
|
try {
|
|
67102
67272
|
await writeFile4(shotPath, new Uint8Array(Buffer.from(image.base64, "base64")));
|
|
67103
67273
|
const userText = [
|
|
@@ -67148,7 +67318,7 @@ var antigravityVision = {
|
|
|
67148
67318
|
if (apiKey) {
|
|
67149
67319
|
const requested = input.auth.defaultModel?.trim() || ANTIGRAVITY_DEFAULT_VISION_MODEL;
|
|
67150
67320
|
const googleModelId = requested.startsWith("gemini-1.") || requested.startsWith("gemini-2.") ? requested : "gemini-2.5-flash";
|
|
67151
|
-
const image = await prepareVisionImage(input.imageBase64);
|
|
67321
|
+
const image = input.image ?? await prepareVisionImage(input.imageBase64);
|
|
67152
67322
|
const provider = createGoogle({ apiKey });
|
|
67153
67323
|
return completeWithAiSdk({
|
|
67154
67324
|
label: "Antigravity",
|
|
@@ -67156,7 +67326,8 @@ var antigravityVision = {
|
|
|
67156
67326
|
schema: input.schema,
|
|
67157
67327
|
system: input.system,
|
|
67158
67328
|
prompt: input.prompt,
|
|
67159
|
-
image
|
|
67329
|
+
image,
|
|
67330
|
+
onDecideRetry: input.onDecideRetry
|
|
67160
67331
|
});
|
|
67161
67332
|
}
|
|
67162
67333
|
try {
|
|
@@ -67165,6 +67336,7 @@ var antigravityVision = {
|
|
|
67165
67336
|
if (!(error40 instanceof AgentProviderError) || !isJsonRepairableError(error40)) {
|
|
67166
67337
|
throw error40;
|
|
67167
67338
|
}
|
|
67339
|
+
input.onDecideRetry?.();
|
|
67168
67340
|
return completeWithAgyCli(input, JSON_REPAIR_PROMPT2);
|
|
67169
67341
|
}
|
|
67170
67342
|
}
|
|
@@ -75611,7 +75783,7 @@ ${result.stderr}`.toLowerCase();
|
|
|
75611
75783
|
// src/domains/providers/drivers/cursor-vision.ts
|
|
75612
75784
|
import { mkdtemp as mkdtemp5, rm as rm5, writeFile as writeFile5 } from "fs/promises";
|
|
75613
75785
|
import { tmpdir as tmpdir5 } from "os";
|
|
75614
|
-
import { join as
|
|
75786
|
+
import { join as join12 } from "path";
|
|
75615
75787
|
var CURSOR_DEFAULT_VISION_MODEL = "auto";
|
|
75616
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).";
|
|
75617
75789
|
function resolveCursorApiKey(auth) {
|
|
@@ -75624,8 +75796,8 @@ async function invokeCursorPrint(input) {
|
|
|
75624
75796
|
}
|
|
75625
75797
|
const model = input.auth.defaultModel?.trim() || CURSOR_DEFAULT_VISION_MODEL;
|
|
75626
75798
|
const ext = input.mediaType === "image/jpeg" ? "jpg" : "png";
|
|
75627
|
-
const dir = await mkdtemp5(
|
|
75628
|
-
const shotPath =
|
|
75799
|
+
const dir = await mkdtemp5(join12(tmpdir5(), "yoqa-cursor-"));
|
|
75800
|
+
const shotPath = join12(dir, `shot.${ext}`);
|
|
75629
75801
|
const apiKey = resolveCursorApiKey(input.auth);
|
|
75630
75802
|
try {
|
|
75631
75803
|
await writeFile5(shotPath, new Uint8Array(Buffer.from(input.imageBase64, "base64")));
|
|
@@ -75668,7 +75840,7 @@ ${result.stderr}`.trim();
|
|
|
75668
75840
|
}
|
|
75669
75841
|
}
|
|
75670
75842
|
async function completeOnce(input, repairHint) {
|
|
75671
|
-
const image = await prepareVisionImage(input.imageBase64);
|
|
75843
|
+
const image = input.image ?? await prepareVisionImage(input.imageBase64);
|
|
75672
75844
|
const ext = image.mediaType === "image/jpeg" ? "jpg" : "png";
|
|
75673
75845
|
const shotName = `shot.${ext}`;
|
|
75674
75846
|
const userText = [
|
|
@@ -75707,6 +75879,7 @@ var cursorVision = {
|
|
|
75707
75879
|
if (!(error40 instanceof AgentProviderError) || !isJsonRepairableError(error40)) {
|
|
75708
75880
|
throw error40;
|
|
75709
75881
|
}
|
|
75882
|
+
input.onDecideRetry?.();
|
|
75710
75883
|
return completeOnce(input, JSON_REPAIR_PROMPT3);
|
|
75711
75884
|
}
|
|
75712
75885
|
}
|
|
@@ -95598,6 +95771,18 @@ var groq = createGroq();
|
|
|
95598
95771
|
|
|
95599
95772
|
// src/domains/providers/drivers/groq.ts
|
|
95600
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
|
+
}
|
|
95601
95786
|
var groqDriver = {
|
|
95602
95787
|
kind: "groq",
|
|
95603
95788
|
label: "Groq",
|
|
@@ -95610,13 +95795,14 @@ var groqDriver = {
|
|
|
95610
95795
|
vision: createSdkVisionPort({
|
|
95611
95796
|
label: "Groq",
|
|
95612
95797
|
defaultModel: "meta-llama/llama-4-scout-17b-16e-instruct",
|
|
95798
|
+
maxOutputTokens: 2048,
|
|
95613
95799
|
createModel: (auth, modelId) => {
|
|
95614
95800
|
const apiKey = resolveGroqKey(auth);
|
|
95615
95801
|
if (!apiKey) {
|
|
95616
95802
|
throw new AgentProviderError("Groq provider has no API key");
|
|
95617
95803
|
}
|
|
95618
95804
|
const baseURL = auth.baseUrl?.trim().replace(/\/$/, "") || DEFAULT_BASE2;
|
|
95619
|
-
return createGroq({ apiKey, baseURL })(modelId);
|
|
95805
|
+
return createGroq({ apiKey, baseURL, fetch: withGroqRequestHooks({}) })(modelId);
|
|
95620
95806
|
}
|
|
95621
95807
|
}),
|
|
95622
95808
|
async probe() {
|
|
@@ -95657,7 +95843,10 @@ var groqDriver = {
|
|
|
95657
95843
|
label: "Groq"
|
|
95658
95844
|
});
|
|
95659
95845
|
return {
|
|
95660
|
-
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
|
+
}),
|
|
95661
95850
|
message: result.ok ? `${result.models?.length ?? 0} models available` : result.message
|
|
95662
95851
|
};
|
|
95663
95852
|
}
|
|
@@ -95733,8 +95922,8 @@ var openaiDriver = {
|
|
|
95733
95922
|
};
|
|
95734
95923
|
|
|
95735
95924
|
// src/domains/providers/drivers/opencode-config.ts
|
|
95736
|
-
import { homedir as
|
|
95737
|
-
import { join as
|
|
95925
|
+
import { homedir as homedir10 } from "os";
|
|
95926
|
+
import { join as join13 } from "path";
|
|
95738
95927
|
var OPENAI_COMPATIBLE_PACKAGES = new Set([
|
|
95739
95928
|
"@ai-sdk/openai-compatible",
|
|
95740
95929
|
"@ai-sdk/openai",
|
|
@@ -95762,8 +95951,8 @@ function openCodeConfigPaths() {
|
|
|
95762
95951
|
const paths = [];
|
|
95763
95952
|
const xdg = process.env.XDG_CONFIG_HOME?.trim();
|
|
95764
95953
|
if (xdg)
|
|
95765
|
-
paths.push(
|
|
95766
|
-
paths.push(
|
|
95954
|
+
paths.push(join13(xdg, "opencode", "opencode.json"));
|
|
95955
|
+
paths.push(join13(homedir10(), ".config", "opencode", "opencode.json"));
|
|
95767
95956
|
return paths;
|
|
95768
95957
|
}
|
|
95769
95958
|
function parseOpenCodeCompatibleProvider(config2, providerId) {
|
|
@@ -96221,6 +96410,348 @@ async function completeVision(auth, input) {
|
|
|
96221
96410
|
return resolveVision(auth).completeObject({ ...input, auth });
|
|
96222
96411
|
}
|
|
96223
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
|
+
|
|
96224
96755
|
// src/domains/runs/agent.ts
|
|
96225
96756
|
var swipeDirectionSchema = exports_external.union([
|
|
96226
96757
|
exports_external.literal("up"),
|
|
@@ -96251,6 +96782,14 @@ var agentDecisionSchema = exports_external.object({
|
|
|
96251
96782
|
y: exports_external.number().optional(),
|
|
96252
96783
|
x2: exports_external.number().optional(),
|
|
96253
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(),
|
|
96254
96793
|
direction: swipeDirectionSchema.optional(),
|
|
96255
96794
|
durationMs: exports_external.number().optional(),
|
|
96256
96795
|
double: exports_external.boolean().optional(),
|
|
@@ -96313,12 +96852,174 @@ var MAX_SCREEN_SNAPSHOT_ROWS = 80;
|
|
|
96313
96852
|
function padNorm(n) {
|
|
96314
96853
|
return String(Math.round(n)).padStart(4, " ");
|
|
96315
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
|
+
}
|
|
96316
97013
|
function formatScreenSnapshot(elements) {
|
|
96317
97014
|
if (elements == null)
|
|
96318
97015
|
return "(screen tree unavailable)";
|
|
96319
97016
|
if (elements.length === 0)
|
|
96320
97017
|
return "(empty tree)";
|
|
96321
|
-
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);
|
|
96322
97023
|
const shown = ordered.slice(0, MAX_SCREEN_SNAPSHOT_ROWS);
|
|
96323
97024
|
const lines = shown.map((element) => {
|
|
96324
97025
|
const id = element.id?.trim() ? ` id=${element.id.trim()}` : "";
|
|
@@ -96498,13 +97199,44 @@ function prefersScreenshotTap(decision) {
|
|
|
96498
97199
|
return false;
|
|
96499
97200
|
return decision.x != null && decision.y != null;
|
|
96500
97201
|
}
|
|
96501
|
-
var
|
|
96502
|
-
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.
|
|
96503
|
-
|
|
96504
|
-
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.
|
|
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.
|
|
96505
97203
|
Every action MUST include:
|
|
96506
97204
|
- "reason": one short sentence summarizing the action choice
|
|
96507
|
-
- "thoughts": 2\u20134 sentences describing what you see on screen and how that led to this action
|
|
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".`;
|
|
97236
|
+
var SYSTEM_PROMPT = `You are a mobile QA agent controlling an app using BOTH visual screenshots and screen snapshots (accessibility tree).
|
|
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.
|
|
97238
|
+
|
|
97239
|
+
${REPLY_FORMAT}
|
|
96508
97240
|
|
|
96509
97241
|
Valid shapes:
|
|
96510
97242
|
{"type":"tap","label":"Allow","reason":"...","thoughts":"..."}
|
|
@@ -96519,45 +97251,63 @@ Valid shapes:
|
|
|
96519
97251
|
{"type":"type","text":"...","x":0-1000,"y":0-1000,"reason":"...","thoughts":"..."}
|
|
96520
97252
|
{"type":"input","text":"...","id":"email_field","reason":"...","thoughts":"..."}
|
|
96521
97253
|
{"type":"alert","alertAction":"accept","reason":"...","thoughts":"..."}
|
|
96522
|
-
{
|
|
96523
|
-
{"type":"terminate-app","appId":"com.example.app","reason":"...","thoughts":"..."}
|
|
96524
|
-
{"type":"restart-app","appId":"com.example.app","reason":"...","thoughts":"..."}
|
|
96525
|
-
{"type":"background-app","seconds":3,"reason":"...","thoughts":"..."}
|
|
96526
|
-
{"type":"open-url","url":"https://example.com","reason":"...","thoughts":"..."}
|
|
96527
|
-
{"type":"assert","assertion":"visible|not-visible","text":"...","reason":"...","thoughts":"..."}
|
|
96528
|
-
{"type":"wait","ms":500-3000,"reason":"...","thoughts":"..."}
|
|
96529
|
-
{"type":"verify","reason":"...","thoughts":"..."}
|
|
96530
|
-
{"type":"done","reason":"...","thoughts":"..."}
|
|
96531
|
-
{"type":"fail","reason":"...","thoughts":"..."}
|
|
97254
|
+
${APP_LIFECYCLE_SHAPES}
|
|
96532
97255
|
|
|
96533
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.
|
|
96534
97257
|
|
|
96535
97258
|
Use BOTH the screenshot and the screen snapshot:
|
|
96536
|
-
-
|
|
97259
|
+
- Visual context (screenshot): Observe visual layout, colors, icons, canvas controls, modal overlays, and soft keyboard visibility.
|
|
97260
|
+
- Structural context (screen snapshot): Inspect accessibility ids ("id=..."), element boundaries, and text labels.
|
|
97261
|
+
- Decide whether to target by "id" or "x,y":
|
|
97262
|
+
* Use "id" when the snapshot provides a clean, unique, and unambiguous identifier for the element.
|
|
97263
|
+
* Use "x,y" coordinates (centre of the control on the screenshot grid) when controls lack IDs, are custom-drawn icons/canvas, or when coordinates are more direct and reliable than broad container IDs.
|
|
96537
97264
|
- System permission / notification dialogs: {"type":"tap","label":"Allow"} or {"type":"alert","alertAction":"accept"}. Never guess coordinates for these.
|
|
96538
|
-
- For other in-app controls, tap screenshot x,y of the visible control if there is no id.
|
|
96539
|
-
- If the snapshot is empty or the control is only drawn (no label/id), use the screenshot.
|
|
96540
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.
|
|
96541
97266
|
|
|
96542
|
-
|
|
96543
|
-
|
|
96544
|
-
|
|
96545
|
-
- "left" / "right": horizontal paging
|
|
96546
|
-
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.
|
|
97267
|
+
${KEYBOARD_GUIDANCE}
|
|
97268
|
+
|
|
97269
|
+
${SWIPE_GUIDANCE}
|
|
96547
97270
|
|
|
96548
97271
|
Rules:
|
|
96549
97272
|
- Look at the attached screenshot AND the screen snapshot before deciding.
|
|
96550
97273
|
- Double-tap: {"type":"tap","double":true,...}. Long-press: {"type":"tap","durationMs":2000,...}. Drag a slider/chip with type drag, not swipe.
|
|
96551
|
-
- 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.
|
|
96552
97274
|
- Assert text on the tree with {"type":"assert","assertion":"visible","text":"\u2026"}.
|
|
96553
|
-
|
|
96554
|
-
|
|
96555
|
-
|
|
96556
|
-
|
|
96557
|
-
|
|
96558
|
-
|
|
96559
|
-
|
|
96560
|
-
-
|
|
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}`;
|
|
96561
97311
|
function formatRecentActions(actions) {
|
|
96562
97312
|
if (actions.length === 0)
|
|
96563
97313
|
return "(none \u2014 first step)";
|
|
@@ -96604,6 +97354,11 @@ function formatRecentActions(actions) {
|
|
|
96604
97354
|
`);
|
|
96605
97355
|
}
|
|
96606
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
|
+
}
|
|
96607
97362
|
function splitInstructionSteps(instructions) {
|
|
96608
97363
|
const trimmed = instructions.replace(/\r\n/g, `
|
|
96609
97364
|
`).trim();
|
|
@@ -96613,7 +97368,7 @@ function splitInstructionSteps(instructions) {
|
|
|
96613
97368
|
`);
|
|
96614
97369
|
const items = [];
|
|
96615
97370
|
let current = [];
|
|
96616
|
-
let
|
|
97371
|
+
let sawSteps = false;
|
|
96617
97372
|
const pushCurrent = () => {
|
|
96618
97373
|
const text3 = current.join(" ").replace(/\s+/g, " ").trim();
|
|
96619
97374
|
if (text3)
|
|
@@ -96622,12 +97377,21 @@ function splitInstructionSteps(instructions) {
|
|
|
96622
97377
|
};
|
|
96623
97378
|
for (const rawLine of lines) {
|
|
96624
97379
|
const line = rawLine.trim();
|
|
96625
|
-
if (!line)
|
|
97380
|
+
if (!line) {
|
|
97381
|
+
if (current.length > 0) {
|
|
97382
|
+
sawSteps = true;
|
|
97383
|
+
pushCurrent();
|
|
97384
|
+
}
|
|
96626
97385
|
continue;
|
|
97386
|
+
}
|
|
96627
97387
|
if (LIST_ITEM_START_RE.test(line)) {
|
|
96628
|
-
|
|
97388
|
+
sawSteps = true;
|
|
96629
97389
|
pushCurrent();
|
|
96630
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];
|
|
96631
97395
|
} else if (current.length > 0) {
|
|
96632
97396
|
current.push(line);
|
|
96633
97397
|
} else {
|
|
@@ -96635,7 +97399,7 @@ function splitInstructionSteps(instructions) {
|
|
|
96635
97399
|
}
|
|
96636
97400
|
}
|
|
96637
97401
|
pushCurrent();
|
|
96638
|
-
if (!
|
|
97402
|
+
if (!sawSteps)
|
|
96639
97403
|
return [trimmed];
|
|
96640
97404
|
return items.length > 0 ? items : [trimmed];
|
|
96641
97405
|
}
|
|
@@ -96654,11 +97418,29 @@ function flattenCaseInstructions(flows2) {
|
|
|
96654
97418
|
}
|
|
96655
97419
|
return queue.length > 0 ? queue : [{ instructions: "", expectedResult: "" }];
|
|
96656
97420
|
}
|
|
97421
|
+
var MAX_COMPLETED_IN_PROMPT = 3;
|
|
96657
97422
|
function formatCompletedInstructions(completed) {
|
|
96658
97423
|
if (completed.length === 0)
|
|
96659
97424
|
return "(none \u2014 this is the first instruction)";
|
|
96660
|
-
|
|
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(`
|
|
96661
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)`;
|
|
96662
97444
|
}
|
|
96663
97445
|
function formatDecidePrompt(input) {
|
|
96664
97446
|
const ordinal = input.instructionOrdinal ?? input.stepIndex + 1;
|
|
@@ -96667,8 +97449,13 @@ function formatDecidePrompt(input) {
|
|
|
96667
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)";
|
|
96668
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}`;
|
|
96669
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);
|
|
96670
97453
|
return [
|
|
96671
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
|
+
] : [],
|
|
96672
97459
|
`Test case: ${input.caseTitle}`,
|
|
96673
97460
|
progress,
|
|
96674
97461
|
"Completed instructions (already done \u2014 do not repeat):",
|
|
@@ -96680,9 +97467,19 @@ function formatDecidePrompt(input) {
|
|
|
96680
97467
|
`Recent actions:
|
|
96681
97468
|
${formatRecentActions(input.recentActions ?? [])}`,
|
|
96682
97469
|
`Last action error: ${input.lastError || "(none)"}`,
|
|
96683
|
-
|
|
96684
|
-
|
|
96685
|
-
|
|
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)"}`,
|
|
96686
97483
|
'Reply with ONLY the JSON action object, including non-empty "reason" and "thoughts".'
|
|
96687
97484
|
].join(`
|
|
96688
97485
|
`);
|
|
@@ -96693,12 +97490,51 @@ function isAbsurdNoScreenshotFail(decision) {
|
|
|
96693
97490
|
const text3 = `${decision.reason} ${decision.thoughts}`.toLowerCase();
|
|
96694
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");
|
|
96695
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
|
+
}
|
|
96696
97530
|
async function decideNextAction(input) {
|
|
96697
97531
|
return completeVision(input.auth, {
|
|
96698
97532
|
schema: agentDecisionSchema,
|
|
96699
|
-
system: SYSTEM_PROMPT,
|
|
97533
|
+
system: input.screenshotOnly ? VISION_SYSTEM_PROMPT : SYSTEM_PROMPT,
|
|
96700
97534
|
prompt: formatDecidePrompt(input),
|
|
96701
|
-
imageBase64: input.imageBase64
|
|
97535
|
+
imageBase64: input.imageBase64,
|
|
97536
|
+
image: input.image,
|
|
97537
|
+
onDecideRetry: input.onDecideRetry
|
|
96702
97538
|
});
|
|
96703
97539
|
}
|
|
96704
97540
|
|
|
@@ -96865,6 +97701,7 @@ function mapApp(row) {
|
|
|
96865
97701
|
name: row.name,
|
|
96866
97702
|
prefix: row.prefix || row.id,
|
|
96867
97703
|
context: row.context,
|
|
97704
|
+
knowledge: row.knowledge,
|
|
96868
97705
|
iosBundleId: row.iosBundleId,
|
|
96869
97706
|
iosAppStoreId: row.iosAppStoreId,
|
|
96870
97707
|
androidApplicationId: row.androidApplicationId,
|
|
@@ -97050,6 +97887,7 @@ async function createApp(input) {
|
|
|
97050
97887
|
name: name30,
|
|
97051
97888
|
prefix,
|
|
97052
97889
|
context: "",
|
|
97890
|
+
knowledge: "",
|
|
97053
97891
|
iosBundleId: "",
|
|
97054
97892
|
iosAppStoreId: "",
|
|
97055
97893
|
androidApplicationId: "",
|
|
@@ -97063,6 +97901,7 @@ async function createApp(input) {
|
|
|
97063
97901
|
}
|
|
97064
97902
|
return created;
|
|
97065
97903
|
}
|
|
97904
|
+
var MAX_APP_KNOWLEDGE_CHARS2 = 2048;
|
|
97066
97905
|
async function updateApp(appId, input) {
|
|
97067
97906
|
const db = getCatalogDb();
|
|
97068
97907
|
const existing = await db.query.apps.findFirst({ where: eq(apps.id, appId) });
|
|
@@ -97073,11 +97912,15 @@ async function updateApp(appId, input) {
|
|
|
97073
97912
|
if (!name30) {
|
|
97074
97913
|
throw new CatalogValidationError("Application name is required");
|
|
97075
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
|
+
}
|
|
97076
97918
|
const prefix = input.prefix !== undefined ? await allocatePrefix(input.prefix.trim() || name30, appId) : existing.prefix || await allocatePrefix(name30, appId);
|
|
97077
97919
|
await db.update(apps).set({
|
|
97078
97920
|
name: name30,
|
|
97079
97921
|
prefix,
|
|
97080
97922
|
context: input.context ?? existing.context,
|
|
97923
|
+
knowledge: input.knowledge ?? existing.knowledge,
|
|
97081
97924
|
iosBundleId: input.iosBundleId ?? existing.iosBundleId,
|
|
97082
97925
|
iosAppStoreId: input.iosAppStoreId ?? existing.iosAppStoreId,
|
|
97083
97926
|
androidApplicationId: input.androidApplicationId ?? existing.androidApplicationId,
|
|
@@ -97607,8 +98450,8 @@ function createDevicesRoutes() {
|
|
|
97607
98450
|
}
|
|
97608
98451
|
|
|
97609
98452
|
// src/domains/doctor/application.ts
|
|
97610
|
-
import { homedir as
|
|
97611
|
-
import { join as
|
|
98453
|
+
import { homedir as homedir11 } from "os";
|
|
98454
|
+
import { join as join14 } from "path";
|
|
97612
98455
|
async function runCommand5(command, options) {
|
|
97613
98456
|
ensureHostToolPath();
|
|
97614
98457
|
try {
|
|
@@ -97694,7 +98537,7 @@ async function probeJava() {
|
|
|
97694
98537
|
let detail = javaBin ?? "java not on PATH";
|
|
97695
98538
|
if (javaHome)
|
|
97696
98539
|
detail = `JAVA_HOME=${javaHome}`;
|
|
97697
|
-
else if (await Bun.file(
|
|
98540
|
+
else if (await Bun.file(join14(studioJbr, "bin/java")).exists()) {
|
|
97698
98541
|
detail = `Android Studio JBR available at ${studioJbr} (JAVA_HOME unset)`;
|
|
97699
98542
|
}
|
|
97700
98543
|
const ok = Boolean(javaBin || javaHome);
|
|
@@ -97725,7 +98568,7 @@ async function probeXcodeSelect() {
|
|
|
97725
98568
|
};
|
|
97726
98569
|
}
|
|
97727
98570
|
async function probeAdbDevices() {
|
|
97728
|
-
const adb = await which3("adb") ??
|
|
98571
|
+
const adb = await which3("adb") ?? join14(homedir11(), "Library/Android/sdk/platform-tools/adb");
|
|
97729
98572
|
if (!await Bun.file(adb).exists() && !await which3("adb")) {
|
|
97730
98573
|
return {
|
|
97731
98574
|
id: "adb-devices",
|
|
@@ -97926,10 +98769,10 @@ import {
|
|
|
97926
98769
|
randomBytes
|
|
97927
98770
|
} from "crypto";
|
|
97928
98771
|
import { chmodSync, existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
|
|
97929
|
-
import { homedir as
|
|
97930
|
-
import { dirname as dirname3, join as
|
|
97931
|
-
var YOQA_ROOT5 =
|
|
97932
|
-
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");
|
|
97933
98776
|
var ALGORITHM = "aes-256-gcm";
|
|
97934
98777
|
var IV_LENGTH = 12;
|
|
97935
98778
|
var KEY_LENGTH = 32;
|
|
@@ -98721,13 +99564,12 @@ class ActionNotFoundError extends Error {
|
|
|
98721
99564
|
this.name = "ActionNotFoundError";
|
|
98722
99565
|
}
|
|
98723
99566
|
}
|
|
98724
|
-
async function performAction(session, body) {
|
|
99567
|
+
async function performAction(session, body, options) {
|
|
98725
99568
|
const locatorTap = Boolean((body.id || body.label) && (body.kind === "tap" || body.kind === "input"));
|
|
98726
99569
|
let x = body.x;
|
|
98727
99570
|
let y = body.y;
|
|
98728
99571
|
if (locatorTap) {
|
|
98729
|
-
const
|
|
98730
|
-
const elements = screen.elements ?? [];
|
|
99572
|
+
const elements = options?.screenElements ?? (await getScreen(session, { full: false })).elements ?? [];
|
|
98731
99573
|
const match2 = body.id ? findElementById(elements, body.id) : findElementByLabel(elements, body.label ?? "");
|
|
98732
99574
|
if (!match2) {
|
|
98733
99575
|
throw new ActionNotFoundError(body.id ? `No element matching id: ${body.id}` : `No element matching label: ${body.label}`);
|
|
@@ -98827,6 +99669,11 @@ async function performAction(session, body) {
|
|
|
98827
99669
|
// src/domains/runs/case-executor.ts
|
|
98828
99670
|
var MAX_STEPS_PER_CASE = 25;
|
|
98829
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;
|
|
98830
99677
|
var defaultClock = {
|
|
98831
99678
|
sleep: (ms) => new Promise((resolve7) => setTimeout(resolve7, ms)),
|
|
98832
99679
|
now: () => Date.now()
|
|
@@ -98873,6 +99720,12 @@ async function runTextAssert(input) {
|
|
|
98873
99720
|
await input.clock.sleep(400);
|
|
98874
99721
|
}
|
|
98875
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
|
+
}
|
|
98876
99729
|
function commandForDecision(decision, defaultAppId) {
|
|
98877
99730
|
if (decision.type === "verify" || decision.type === "done" || decision.type === "fail") {
|
|
98878
99731
|
return null;
|
|
@@ -99249,25 +100102,49 @@ async function executeAgentCase(deps) {
|
|
|
99249
100102
|
let caseError = null;
|
|
99250
100103
|
let lastScreenshotUri = null;
|
|
99251
100104
|
const recordedDecisions = [];
|
|
100105
|
+
let inFlightPhases = null;
|
|
100106
|
+
let inFlightLatencyMs = 0;
|
|
99252
100107
|
const decideOnce = async (input) => {
|
|
99253
100108
|
const payload = {
|
|
99254
100109
|
auth: deps.auth,
|
|
99255
100110
|
appContext: deps.appContext,
|
|
100111
|
+
appKnowledge: deps.appKnowledge,
|
|
99256
100112
|
caseTitle: deps.catalogCase.name,
|
|
99257
100113
|
instructions: input.flow.instructions,
|
|
99258
100114
|
expectedResult: input.flow.expectedResult,
|
|
99259
100115
|
stepIndex: stepIdx,
|
|
99260
100116
|
imageBase64: input.imageBase64,
|
|
100117
|
+
image: input.image,
|
|
99261
100118
|
recentActions: input.recentActions,
|
|
99262
100119
|
screenSnapshot: input.screenSnapshot,
|
|
99263
100120
|
lastError: input.lastError,
|
|
99264
100121
|
defaultAppId: deps.defaultAppId,
|
|
99265
100122
|
completedInstructions: [...input.completedInstructions],
|
|
99266
100123
|
instructionOrdinal: input.instructionOrdinal,
|
|
99267
|
-
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;
|
|
99268
100144
|
};
|
|
99269
100145
|
let decision = await decide(payload);
|
|
99270
100146
|
if (isAbsurdNoScreenshotFail(decision)) {
|
|
100147
|
+
input.onDecideRetry?.();
|
|
99271
100148
|
decision = await decide(payload);
|
|
99272
100149
|
if (isAbsurdNoScreenshotFail(decision)) {
|
|
99273
100150
|
decision = {
|
|
@@ -99277,23 +100154,29 @@ async function executeAgentCase(deps) {
|
|
|
99277
100154
|
};
|
|
99278
100155
|
}
|
|
99279
100156
|
}
|
|
99280
|
-
decision =
|
|
99281
|
-
|
|
99282
|
-
|
|
99283
|
-
|
|
99284
|
-
|
|
99285
|
-
|
|
99286
|
-
|
|
99287
|
-
|
|
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
|
+
}
|
|
99288
100169
|
if ((decision.type === "activate-app" || decision.type === "terminate-app" || decision.type === "restart-app") && !decision.appId && deps.defaultAppId) {
|
|
99289
100170
|
decision = { ...decision, appId: deps.defaultAppId };
|
|
99290
100171
|
}
|
|
99291
100172
|
return decision;
|
|
99292
100173
|
};
|
|
99293
|
-
const applyDecision = async (decision) => {
|
|
100174
|
+
const applyDecision = async (decision, phases, screenElements) => {
|
|
99294
100175
|
if (decision.type === "wait") {
|
|
99295
100176
|
const waitMs = Math.min(3000, Math.max(500, decision.ms ?? 1500));
|
|
100177
|
+
const waitStarted = clock.now();
|
|
99296
100178
|
await clock.sleep(waitMs);
|
|
100179
|
+
phases.actionMs += clock.now() - waitStarted;
|
|
99297
100180
|
return "continue";
|
|
99298
100181
|
}
|
|
99299
100182
|
if (decision.type === "verify" || decision.type === "done") {
|
|
@@ -99305,6 +100188,7 @@ async function executeAgentCase(deps) {
|
|
|
99305
100188
|
if (decision.type === "assert") {
|
|
99306
100189
|
const assertion = decision.assertion === "not-visible" ? "not-visible" : "visible";
|
|
99307
100190
|
const timeoutMs = Math.min(60000, Math.max(1000, decision.timeoutMs ?? 5000));
|
|
100191
|
+
const assertStarted = clock.now();
|
|
99308
100192
|
await runTextAssert({
|
|
99309
100193
|
session: deps.session,
|
|
99310
100194
|
readScreen: readScreen2,
|
|
@@ -99314,14 +100198,18 @@ async function executeAgentCase(deps) {
|
|
|
99314
100198
|
text: decision.text ?? "",
|
|
99315
100199
|
timeoutMs
|
|
99316
100200
|
});
|
|
100201
|
+
phases.actionMs += clock.now() - assertStarted;
|
|
99317
100202
|
return "continue";
|
|
99318
100203
|
}
|
|
99319
100204
|
const body = decisionToActionRequest(decision, { defaultAppId: deps.defaultAppId });
|
|
99320
100205
|
if (!body) {
|
|
99321
100206
|
throw new Error(`${decision.type} is missing required fields`);
|
|
99322
100207
|
}
|
|
100208
|
+
const actionStarted = clock.now();
|
|
99323
100209
|
try {
|
|
99324
|
-
await perform(deps.session, body
|
|
100210
|
+
await perform(deps.session, body, {
|
|
100211
|
+
screenElements: screenElements.length > 0 ? screenElements : undefined
|
|
100212
|
+
});
|
|
99325
100213
|
} catch (error40) {
|
|
99326
100214
|
if (error40 instanceof ActionNotFoundError && (body.id || body.label) && decision.x != null && decision.y != null) {
|
|
99327
100215
|
await perform(deps.session, {
|
|
@@ -99335,8 +100223,12 @@ async function executeAgentCase(deps) {
|
|
|
99335
100223
|
} else {
|
|
99336
100224
|
throw error40;
|
|
99337
100225
|
}
|
|
100226
|
+
} finally {
|
|
100227
|
+
phases.actionMs += clock.now() - actionStarted;
|
|
99338
100228
|
}
|
|
100229
|
+
const settleStarted = clock.now();
|
|
99339
100230
|
await clock.sleep(settleMs);
|
|
100231
|
+
phases.settleMs += clock.now() - settleStarted;
|
|
99340
100232
|
return "continue";
|
|
99341
100233
|
};
|
|
99342
100234
|
try {
|
|
@@ -99344,6 +100236,65 @@ async function executeAgentCase(deps) {
|
|
|
99344
100236
|
const recentActions = [];
|
|
99345
100237
|
const completedInstructions = [];
|
|
99346
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
|
+
};
|
|
99347
100298
|
for (let instructionIndex = 0;instructionIndex < instructionQueue.length; instructionIndex++) {
|
|
99348
100299
|
const instruction = instructionQueue[instructionIndex];
|
|
99349
100300
|
if (!instruction)
|
|
@@ -99353,17 +100304,158 @@ async function executeAgentCase(deps) {
|
|
|
99353
100304
|
break;
|
|
99354
100305
|
}
|
|
99355
100306
|
let instructionDone = false;
|
|
100307
|
+
let pending = null;
|
|
100308
|
+
let pendingElements = [];
|
|
99356
100309
|
for (let attempt = 0;attempt < maxSteps && !instructionDone; attempt++) {
|
|
99357
100310
|
if (deps.isAborted()) {
|
|
99358
100311
|
caseStatus = "cancelled";
|
|
99359
100312
|
instructionDone = true;
|
|
99360
100313
|
break;
|
|
99361
100314
|
}
|
|
99362
|
-
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;
|
|
99363
100405
|
const shot = await deps.session.screenshot();
|
|
99364
100406
|
lastScreenshotUri = shot.path;
|
|
99365
|
-
const
|
|
100407
|
+
const advance = (phase) => {
|
|
100408
|
+
const now = clock.now();
|
|
100409
|
+
phases[phase] = now - phaseStartedAt;
|
|
100410
|
+
phaseStartedAt = now;
|
|
100411
|
+
};
|
|
100412
|
+
advance("captureMs");
|
|
99366
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");
|
|
99367
100459
|
const lastAction = recentActions.at(-1);
|
|
99368
100460
|
const lastSwipeMovedScreen = lastAction?.type === "swipe" && prevFingerprint != null && fingerprint !== prevFingerprint;
|
|
99369
100461
|
if (deps.isAborted()) {
|
|
@@ -99373,69 +100465,106 @@ async function executeAgentCase(deps) {
|
|
|
99373
100465
|
}
|
|
99374
100466
|
const decideInput = {
|
|
99375
100467
|
flow: instruction,
|
|
99376
|
-
imageBase64:
|
|
100468
|
+
imageBase64: visionBase64,
|
|
100469
|
+
image,
|
|
99377
100470
|
recentActions,
|
|
99378
100471
|
screenSnapshot: tree.snapshot,
|
|
100472
|
+
lastError: stallHint,
|
|
99379
100473
|
lastSwipeMovedScreen,
|
|
99380
100474
|
completedInstructions,
|
|
99381
100475
|
instructionOrdinal: instructionIndex + 1,
|
|
99382
|
-
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
|
|
99383
100490
|
};
|
|
99384
100491
|
let decision = await decideOnce(decideInput);
|
|
99385
100492
|
prevFingerprint = fingerprint;
|
|
99386
|
-
|
|
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;
|
|
99387
100523
|
if (deps.isAborted()) {
|
|
99388
100524
|
caseStatus = "cancelled";
|
|
99389
100525
|
instructionDone = true;
|
|
99390
100526
|
break;
|
|
99391
100527
|
}
|
|
99392
|
-
const
|
|
99393
|
-
|
|
99394
|
-
|
|
99395
|
-
|
|
99396
|
-
|
|
99397
|
-
|
|
99398
|
-
|
|
99399
|
-
decision
|
|
99400
|
-
|
|
99401
|
-
|
|
99402
|
-
|
|
99403
|
-
|
|
99404
|
-
|
|
99405
|
-
|
|
99406
|
-
|
|
99407
|
-
|
|
99408
|
-
|
|
99409
|
-
|
|
99410
|
-
|
|
99411
|
-
|
|
99412
|
-
|
|
99413
|
-
|
|
99414
|
-
|
|
99415
|
-
|
|
99416
|
-
|
|
99417
|
-
|
|
99418
|
-
|
|
99419
|
-
|
|
99420
|
-
});
|
|
99421
|
-
};
|
|
99422
|
-
const initialCommand = commandForDecision(decision, deps.defaultAppId);
|
|
99423
|
-
const outcome = initialCommand ? await withCurrentCommand(setCurrentCommand, initialCommand, async () => {
|
|
99424
|
-
const result = await applyWithRetry();
|
|
99425
|
-
await recordStep(commandForDecision(decision, deps.defaultAppId), result);
|
|
99426
|
-
return result;
|
|
99427
|
-
}) : await (async () => {
|
|
99428
|
-
const result = await applyWithRetry();
|
|
99429
|
-
await recordStep(null, result);
|
|
99430
|
-
return result;
|
|
99431
|
-
})();
|
|
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
|
+
});
|
|
99432
100556
|
if (outcome === "done") {
|
|
99433
100557
|
instructionDone = true;
|
|
99434
100558
|
completedInstructions.push(instruction.instructions);
|
|
100559
|
+
pending = null;
|
|
99435
100560
|
} else if (outcome === "fail") {
|
|
99436
100561
|
caseStatus = "errored";
|
|
99437
|
-
caseError = decision.reason
|
|
100562
|
+
caseError = verdict.reason || decision.reason || "Agent marked the instruction as failed";
|
|
99438
100563
|
instructionDone = true;
|
|
100564
|
+
pending = null;
|
|
100565
|
+
} else {
|
|
100566
|
+
pending = isDeviceDecision(decision) ? decision : null;
|
|
100567
|
+
pendingElements = tree.elements;
|
|
99439
100568
|
}
|
|
99440
100569
|
stepIdx += 1;
|
|
99441
100570
|
}
|
|
@@ -99462,7 +100591,8 @@ async function executeAgentCase(deps) {
|
|
|
99462
100591
|
},
|
|
99463
100592
|
screenshotUri: lastScreenshotUri,
|
|
99464
100593
|
ok: false,
|
|
99465
|
-
latencyMs:
|
|
100594
|
+
latencyMs: inFlightLatencyMs,
|
|
100595
|
+
phases: inFlightPhases,
|
|
99466
100596
|
detail: caseError,
|
|
99467
100597
|
command: null
|
|
99468
100598
|
});
|
|
@@ -99504,6 +100634,15 @@ function parseAction(raw2) {
|
|
|
99504
100634
|
return {};
|
|
99505
100635
|
}
|
|
99506
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
|
+
}
|
|
99507
100646
|
function isAborted2(runId) {
|
|
99508
100647
|
return runControls.get(runId)?.aborted === true;
|
|
99509
100648
|
}
|
|
@@ -99523,6 +100662,9 @@ function parseRunExecutionMode(value) {
|
|
|
99523
100662
|
return value;
|
|
99524
100663
|
return "auto";
|
|
99525
100664
|
}
|
|
100665
|
+
function parseRunScreenMode(value) {
|
|
100666
|
+
return value === "vision" ? "vision" : "tree";
|
|
100667
|
+
}
|
|
99526
100668
|
function resolveCaseExecutionMode(runMode, hasScript) {
|
|
99527
100669
|
if (runMode === "agent")
|
|
99528
100670
|
return "agent";
|
|
@@ -99547,6 +100689,7 @@ async function loadRun(runId) {
|
|
|
99547
100689
|
screenshotUri: step.screenshotUri,
|
|
99548
100690
|
ok: step.ok === 1,
|
|
99549
100691
|
latencyMs: step.latencyMs,
|
|
100692
|
+
phases: parsePhases(step.phasesJson),
|
|
99550
100693
|
detail: step.detail,
|
|
99551
100694
|
command: step.command ?? null,
|
|
99552
100695
|
createdAt: step.createdAt
|
|
@@ -99573,6 +100716,7 @@ async function loadRun(runId) {
|
|
|
99573
100716
|
buildId: runRow.buildId,
|
|
99574
100717
|
status: runRow.status,
|
|
99575
100718
|
executionMode: parseRunExecutionMode(runRow.executionMode),
|
|
100719
|
+
screenMode: parseRunScreenMode(runRow.screenMode),
|
|
99576
100720
|
error: runRow.error,
|
|
99577
100721
|
createdAt: runRow.createdAt,
|
|
99578
100722
|
startedAt: runRow.startedAt,
|
|
@@ -99590,6 +100734,7 @@ async function appendStep(input) {
|
|
|
99590
100734
|
screenshotUri: input.screenshotUri,
|
|
99591
100735
|
ok: input.ok ? 1 : 0,
|
|
99592
100736
|
latencyMs: Math.max(0, Math.round(input.latencyMs)),
|
|
100737
|
+
phasesJson: input.phases ? JSON.stringify(input.phases) : null,
|
|
99593
100738
|
detail: input.detail,
|
|
99594
100739
|
command: input.command,
|
|
99595
100740
|
createdAt: Date.now()
|
|
@@ -99656,6 +100801,7 @@ async function executeAgentCase2(input) {
|
|
|
99656
100801
|
return executeAgentCase({
|
|
99657
100802
|
catalogCase,
|
|
99658
100803
|
appContext: input.appContext,
|
|
100804
|
+
appKnowledge: input.appKnowledge,
|
|
99659
100805
|
auth: input.auth,
|
|
99660
100806
|
session: input.session,
|
|
99661
100807
|
isAborted: () => isAborted2(input.runId),
|
|
@@ -99669,6 +100815,7 @@ async function executeAgentCase2(input) {
|
|
|
99669
100815
|
await setCurrentCommand(input.runTestId, command);
|
|
99670
100816
|
},
|
|
99671
100817
|
decide: decideNextAction,
|
|
100818
|
+
screenMode: input.screenMode,
|
|
99672
100819
|
clock: { sleep, now: () => Date.now() },
|
|
99673
100820
|
defaultAppId: input.defaultAppId
|
|
99674
100821
|
});
|
|
@@ -99710,8 +100857,10 @@ async function executeCase(input) {
|
|
|
99710
100857
|
runTestId: input.runTestId,
|
|
99711
100858
|
caseId: input.caseId,
|
|
99712
100859
|
appContext: input.appContext,
|
|
100860
|
+
appKnowledge: input.appKnowledge,
|
|
99713
100861
|
session: input.session,
|
|
99714
100862
|
auth: input.auth,
|
|
100863
|
+
screenMode: input.screenMode,
|
|
99715
100864
|
defaultAppId: input.defaultAppId
|
|
99716
100865
|
});
|
|
99717
100866
|
caseStatus = result.status;
|
|
@@ -99752,6 +100901,7 @@ async function executeRun(runId) {
|
|
|
99752
100901
|
}
|
|
99753
100902
|
const startedAt = Date.now();
|
|
99754
100903
|
await db.update(runs).set({ status: "running", startedAt, error: null }).where(eq(runs.id, runId));
|
|
100904
|
+
pruneOldRunScreenshots();
|
|
99755
100905
|
if (isAborted2(runId)) {
|
|
99756
100906
|
await persistCancelled(runId);
|
|
99757
100907
|
return;
|
|
@@ -99822,9 +100972,11 @@ async function executeRun(runId) {
|
|
|
99822
100972
|
runTestId: test.id,
|
|
99823
100973
|
caseId: test.caseId,
|
|
99824
100974
|
appContext: app.context,
|
|
100975
|
+
appKnowledge: app.knowledge,
|
|
99825
100976
|
session,
|
|
99826
100977
|
auth,
|
|
99827
100978
|
caseMode,
|
|
100979
|
+
screenMode: run.screenMode ?? "tree",
|
|
99828
100980
|
defaultAppId: run.platform === "ios" ? app.iosBundleId || undefined : app.androidApplicationId || undefined
|
|
99829
100981
|
});
|
|
99830
100982
|
if (status === "cancelled" || isAborted2(runId)) {
|
|
@@ -99889,6 +101041,7 @@ async function createRun(input) {
|
|
|
99889
101041
|
throw new RunValidationError("App not found");
|
|
99890
101042
|
}
|
|
99891
101043
|
const executionMode = input.executionMode ?? "auto";
|
|
101044
|
+
const screenMode = input.screenMode ?? "vision";
|
|
99892
101045
|
const catalogCases = [];
|
|
99893
101046
|
for (const caseId of uniqueCaseIds) {
|
|
99894
101047
|
const catalogCase = await getCase(caseId);
|
|
@@ -99930,6 +101083,7 @@ async function createRun(input) {
|
|
|
99930
101083
|
buildId,
|
|
99931
101084
|
status: "queued",
|
|
99932
101085
|
executionMode,
|
|
101086
|
+
screenMode,
|
|
99933
101087
|
error: null,
|
|
99934
101088
|
createdAt: now,
|
|
99935
101089
|
startedAt: null,
|
|
@@ -99999,6 +101153,7 @@ async function listRuns(appId) {
|
|
|
99999
101153
|
buildId: runRow.buildId,
|
|
100000
101154
|
status: runRow.status,
|
|
100001
101155
|
executionMode: parseRunExecutionMode(runRow.executionMode),
|
|
101156
|
+
screenMode: parseRunScreenMode(runRow.screenMode),
|
|
100002
101157
|
error: runRow.error,
|
|
100003
101158
|
createdAt: runRow.createdAt,
|
|
100004
101159
|
startedAt: runRow.startedAt,
|
|
@@ -100233,8 +101388,84 @@ function createServersRoutes() {
|
|
|
100233
101388
|
}
|
|
100234
101389
|
|
|
100235
101390
|
// src/interfaces/http/session.ts
|
|
100236
|
-
import { mkdir as
|
|
101391
|
+
import { mkdir as mkdir5, rename as rename2 } from "fs/promises";
|
|
100237
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
|
|
100238
101469
|
function sessionErrorResponse(error40) {
|
|
100239
101470
|
const message = error40 instanceof Error ? error40.message : String(error40);
|
|
100240
101471
|
if (error40 instanceof SessionBusyError) {
|
|
@@ -100333,7 +101564,7 @@ function createSessionRoutes() {
|
|
|
100333
101564
|
const shot = await session.screenshot();
|
|
100334
101565
|
let path = shot.path;
|
|
100335
101566
|
if (parsed.data.path) {
|
|
100336
|
-
await
|
|
101567
|
+
await mkdir5(dirname4(parsed.data.path), { recursive: true });
|
|
100337
101568
|
await rename2(shot.path, parsed.data.path);
|
|
100338
101569
|
path = parsed.data.path;
|
|
100339
101570
|
}
|
|
@@ -100427,7 +101658,7 @@ function createSessionRoutes() {
|
|
|
100427
101658
|
error: "A run is using this device session. Cancel the run to interact manually."
|
|
100428
101659
|
}, 409);
|
|
100429
101660
|
}
|
|
100430
|
-
const result = await performAction(session, parsed.data);
|
|
101661
|
+
const result = parsed.data.screenshot ? await performActionWithScreenshot(session, parsed.data) : await performAction(session, parsed.data);
|
|
100431
101662
|
return c.json(actionResponseSchema.parse(result));
|
|
100432
101663
|
} catch (error40) {
|
|
100433
101664
|
const gone = sessionErrorResponse(error40);
|