jev-cdp 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/cli.js +616 -99
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,8 +27,8 @@ This is an early experimental port. See [NOTICE.md](NOTICE.md) for source attrib
|
|
|
27
27
|
Run the published CLI without adding it to a project. Bun must be installed for either command:
|
|
28
28
|
|
|
29
29
|
```bash
|
|
30
|
-
bunx jev-cdp@0.1.
|
|
31
|
-
npx -y jev-cdp@0.1.
|
|
30
|
+
bunx jev-cdp@0.1.5 help run
|
|
31
|
+
npx -y jev-cdp@0.1.5 help run
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
Chrome with a CDP endpoint and `TYPESAFE_API_KEY` are required for browser runs. FFmpeg is required for `--recording`.
|
|
@@ -98,9 +98,11 @@ bun run run -- run \
|
|
|
98
98
|
|
|
99
99
|
`--recording` captures Chrome's compositor screencast stream for the full goal and renders an H.264 MP4. Because Chrome's native pointer is not part of that stream, the adapter draws a high-contrast cursor that starts at the viewport center, glides to each target, and pulses on clicks. `--interaction-pauses` adds a deterministic delay after opening or loading a page, switching to an attached tab, or changing the URL within a page. Jev chooses during that delay, and the adapter waits only for any time left before acting. The same flag also pauses after moving to a click target and before pressing the mouse. Jev does not choose or observe these delays. `--screenshot` saves the final viewport after the goal stops; when recording is also enabled, it reuses the final screencast frame.
|
|
100
100
|
|
|
101
|
-
|
|
101
|
+
`jev-cdp run` writes JSON Lines to standard output: one `type:"action"` object per executed action, followed by one `type:"result"` object with the final status and action budget. Action objects include elapsed execution time, the operation, page and tab URLs, the viewport, and a CSS selector, role, name, frame, and coordinates for the element. Fill actions include the entered text; password fields and `--field-value-env` values are redacted and must be supplied separately for replay. Select and scroll actions include their option value or wheel delta. The CSS selector and page URL can be used as Playwright replay targets, with the coordinates as a fallback at the recorded viewport size. `--final-state` adds the final semantic page snapshot: a frame tree with URLs and loading state, actionable elements with frame identity, screen bounds, nearby text, region, clickability, and any element covering the click point, plus new-tab and redirect transitions. Automatic navigation and embedded-content waits use `--wait-budget-ms` (15 seconds by default) and do not consume `--max-steps`; a timeout returns `wait_timeout`, its pending condition and elapsed wait time, and the latest semantic state. Runtime failures emit a `type:"result"` object with `status:"error"`; diagnostic text uses standard error.
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
Each action object includes `consoleErrors` observed during that step. The result object includes `initialConsoleErrors` already present when attaching to the tab and `consoleErrors` for the full run. These fields include browser console errors, uncaught exceptions, and error-level DevTools log entries from the page and its frames. Review them alongside the semantic state; a console error does not by itself establish that the goal failed.
|
|
104
|
+
|
|
105
|
+
The snapshot includes visible controls in nested iframes, including cross-origin frames. Clicking a link there uses its frame coordinates and checks that the observed control is still current. If that click opens a new tab, Jev switches to it, returns its target ID for the next goal, and keeps the recording and 1120×780 viewport consistent across the switch.
|
|
104
106
|
|
|
105
107
|
## Supply known field values without another LLM
|
|
106
108
|
|
package/dist/cli.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// package.json
|
|
4
4
|
var package_default = {
|
|
5
5
|
name: "jev-cdp",
|
|
6
|
-
version: "0.1.
|
|
6
|
+
version: "0.1.5",
|
|
7
7
|
description: "A small Jev-powered bridge to Chrome through the Chrome DevTools Protocol.",
|
|
8
8
|
type: "module",
|
|
9
9
|
license: "MIT",
|
|
@@ -207,8 +207,15 @@ var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1
|
|
|
207
207
|
const r=e.getBoundingClientRect(), x=r.x+r.width/2, y=r.y+r.height/2, rname=role(e);
|
|
208
208
|
if (!rname || r.width<=0 || r.height<=0 || x<0 || y<0 || x>=innerWidth || y>=innerHeight) continue;
|
|
209
209
|
if (rname==='gridcell' && e.querySelector('button,[role="button"]')) continue;
|
|
210
|
+
const hit=document.elementFromPoint(x,y);
|
|
211
|
+
const coveredBy=hit && !e.contains(hit) && !hit.contains(e) ?
|
|
212
|
+
{tag:hit.tagName.toLowerCase(),text:(hit.innerText||hit.getAttribute('aria-label')||'').trim().slice(0,160),role:hit.getAttribute('role')} : null;
|
|
213
|
+
const region=e.closest('form,dialog,[role="dialog"],article,section,li,tr,[role="row"],main,nav,header,footer');
|
|
214
|
+
const nearbyText=(region?.innerText||e.parentElement?.innerText||'').trim().replace(/\\s+/g,' ').slice(0,240);
|
|
210
215
|
const base={node:identity(e),role:rname,label:name(e)||rname,...(sensitive(e)?{sensitive:true}:{}),
|
|
211
|
-
rect:{x:r.x,y:r.y,w:r.width,h:r.height}
|
|
216
|
+
rect:{x:r.x,y:r.y,w:r.width,h:r.height},frameUrl:location.href,
|
|
217
|
+
nearbyText,region:region?.getAttribute('aria-label')||region?.getAttribute('role')||region?.tagName.toLowerCase()||'',
|
|
218
|
+
clickable:!coveredBy,coveredBy};
|
|
212
219
|
for (const key of ['checked','selected','expanded','pressed']) {
|
|
213
220
|
const value=e.getAttribute('aria-'+key);
|
|
214
221
|
if (value!==null) base[key]=value;
|
|
@@ -228,14 +235,20 @@ var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1
|
|
|
228
235
|
if (editable) actions.push({...base,kind:'click',value,label:'Open '+base.label});
|
|
229
236
|
}
|
|
230
237
|
}
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
238
|
+
const roots=[document.body], shadowRoots=[];
|
|
239
|
+
for (const root of roots) for (const e of root.querySelectorAll('*')) {
|
|
240
|
+
if (e.shadowRoot) { roots.push(e.shadowRoot); shadowRoots.push(e.shadowRoot); }
|
|
241
|
+
}
|
|
242
|
+
const words=[], range=document.createRange(); let length=0;
|
|
243
|
+
for (const root of [...shadowRoots,document.body]) {
|
|
244
|
+
const walker=document.createTreeWalker(root,NodeFilter.SHOW_TEXT); let node;
|
|
245
|
+
while ((node=walker.nextNode()) && length<6000) {
|
|
246
|
+
const value=node.textContent.trim(), parent=node.parentElement;
|
|
247
|
+
if (!value || !parent || parent.closest('script,style,noscript,template') || !visible(parent)) continue;
|
|
248
|
+
range.selectNodeContents(node); const r=range.getBoundingClientRect();
|
|
249
|
+
if (r.width>0 && r.height>0 && r.bottom>0 && r.top<innerHeight && r.right>0 && r.left<innerWidth) {
|
|
250
|
+
words.push(value); length+=value.length;
|
|
251
|
+
}
|
|
239
252
|
}
|
|
240
253
|
}
|
|
241
254
|
const text=words.join('\\n').slice(0,6000), height=document.documentElement.scrollHeight;
|
|
@@ -249,7 +262,6 @@ var snapshot_default = `// Ported from browser-use/jev-ultrafast at commit 452c1
|
|
|
249
262
|
actions.forEach((a,i)=>a.id='e'+(i+1));
|
|
250
263
|
if (scrollY+innerHeight<height-2) actions.push({id:'scroll_down',kind:'scroll',label:'Scroll down',delta:560});
|
|
251
264
|
if (scrollY>0) actions.push({id:'scroll_up',kind:'scroll',label:'Scroll up',delta:-560});
|
|
252
|
-
actions.push({id:'wait',kind:'wait',label:'Wait for the page to update'});
|
|
253
265
|
return {url:location.href,title:document.title,w:innerWidth,h:innerHeight,text,
|
|
254
266
|
scroll:{y:scrollY,height},actions,marker,page_key,guards,omitted_actions};
|
|
255
267
|
})()
|
|
@@ -278,6 +290,18 @@ var RECORDING_CURSOR_INIT = `(() => {
|
|
|
278
290
|
|
|
279
291
|
class StalePageError extends Error {
|
|
280
292
|
}
|
|
293
|
+
|
|
294
|
+
class WaitTimeoutError extends Error {
|
|
295
|
+
elapsedMs;
|
|
296
|
+
pendingCondition;
|
|
297
|
+
state;
|
|
298
|
+
constructor(elapsedMs, pendingCondition, state) {
|
|
299
|
+
super(`Wait timed out after ${elapsedMs}ms: ${pendingCondition}`);
|
|
300
|
+
this.elapsedMs = elapsedMs;
|
|
301
|
+
this.pendingCondition = pendingCondition;
|
|
302
|
+
this.state = state;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
281
305
|
function stableValue(value) {
|
|
282
306
|
if (Array.isArray(value))
|
|
283
307
|
return value.map(stableValue);
|
|
@@ -323,6 +347,17 @@ class Browser {
|
|
|
323
347
|
#openedTabs = [];
|
|
324
348
|
#cdpUrl;
|
|
325
349
|
#switchOnPopup = false;
|
|
350
|
+
#loadingFrames = new Set;
|
|
351
|
+
#waitBudgetMs;
|
|
352
|
+
#waitSpentMs = 0;
|
|
353
|
+
#transitions = [];
|
|
354
|
+
#origin = null;
|
|
355
|
+
#changeListeners = new Set;
|
|
356
|
+
#targetUrls = new Map;
|
|
357
|
+
#newTargets = new Set;
|
|
358
|
+
#popupListeners = new Set;
|
|
359
|
+
#consoleErrors = [];
|
|
360
|
+
#watchedConsoleSessions = new Set;
|
|
326
361
|
constructor(cdp, sessionId, targetId, ownsTarget, browserContextId, options) {
|
|
327
362
|
this.#cdp = cdp;
|
|
328
363
|
this.#cdpUrl = options.cdpUrl;
|
|
@@ -333,6 +368,7 @@ class Browser {
|
|
|
333
368
|
this.#keepOpen = options.keepOpen ?? false;
|
|
334
369
|
this.#screenshots = options.screenshots ?? false;
|
|
335
370
|
this.#interactionPauses = options.interactionPauses ?? 0;
|
|
371
|
+
this.#waitBudgetMs = options.waitBudgetMs ?? 15000;
|
|
336
372
|
this.#recordingPath = options.recordingPath ? resolve(options.recordingPath) : undefined;
|
|
337
373
|
this.#screenshotPath = options.screenshotPath ? resolve(options.screenshotPath) : undefined;
|
|
338
374
|
}
|
|
@@ -385,10 +421,30 @@ class Browser {
|
|
|
385
421
|
const browser = new Browser(cdp, attached.sessionId, targetId, ownsTarget, browserContextId, options);
|
|
386
422
|
browser.#knownTargets = new Set((await listChromeTargets(options.cdpUrl)).map((target) => target.id));
|
|
387
423
|
try {
|
|
424
|
+
const recordTargetUrl = (params) => {
|
|
425
|
+
const info = params.targetInfo;
|
|
426
|
+
if (info?.type === "page" && info.targetId && !browser.#knownTargets.has(info.targetId)) {
|
|
427
|
+
browser.#newTargets.add(info.targetId);
|
|
428
|
+
for (const listener of browser.#popupListeners)
|
|
429
|
+
listener();
|
|
430
|
+
}
|
|
431
|
+
if (!info?.targetId || !info.url || info.url === "about:blank")
|
|
432
|
+
return;
|
|
433
|
+
const urls = browser.#targetUrls.get(info.targetId) ?? [];
|
|
434
|
+
if (urls.at(-1) !== info.url)
|
|
435
|
+
urls.push(info.url);
|
|
436
|
+
browser.#targetUrls.set(info.targetId, urls);
|
|
437
|
+
};
|
|
438
|
+
browser.#stopPageEvents.push(cdp.on("Target.targetCreated", "", recordTargetUrl), cdp.on("Target.targetInfoChanged", "", recordTargetUrl));
|
|
439
|
+
await cdp.command("Target.setDiscoverTargets", { discover: true });
|
|
388
440
|
browser.#stopPageEvents.push(cdp.on("Target.attachedToTarget", browser.#sessionId, (params) => {
|
|
389
441
|
const info = params.targetInfo;
|
|
390
|
-
if (info?.type === "iframe" && info.targetId && typeof params.sessionId === "string")
|
|
442
|
+
if (info?.type === "iframe" && info.targetId && typeof params.sessionId === "string") {
|
|
391
443
|
browser.#frameSessions.set(info.targetId, params.sessionId);
|
|
444
|
+
browser.watchConsole(params.sessionId, info.targetId).catch(() => {
|
|
445
|
+
return;
|
|
446
|
+
});
|
|
447
|
+
}
|
|
392
448
|
}));
|
|
393
449
|
browser.#stopPageEvents.push(cdp.on("Page.frameNavigated", browser.#sessionId, (params) => {
|
|
394
450
|
const frame = params.frame;
|
|
@@ -396,6 +452,7 @@ class Browser {
|
|
|
396
452
|
browser.#frameContexts.delete(frame.id);
|
|
397
453
|
}));
|
|
398
454
|
await browser.call("Target.setAutoAttach", { autoAttach: true, waitForDebuggerOnStart: false, flatten: true });
|
|
455
|
+
await browser.watchConsole();
|
|
399
456
|
await browser.call("Page.enable");
|
|
400
457
|
await browser.call("Emulation.setDeviceMetricsOverride", {
|
|
401
458
|
width: 1120,
|
|
@@ -412,7 +469,9 @@ class Browser {
|
|
|
412
469
|
await browser.startRecording();
|
|
413
470
|
return browser;
|
|
414
471
|
} catch (error) {
|
|
415
|
-
await browser.close()
|
|
472
|
+
await browser.close().catch(() => {
|
|
473
|
+
return;
|
|
474
|
+
});
|
|
416
475
|
throw error;
|
|
417
476
|
}
|
|
418
477
|
}
|
|
@@ -422,13 +481,65 @@ class Browser {
|
|
|
422
481
|
get targetId() {
|
|
423
482
|
return this.#targetId;
|
|
424
483
|
}
|
|
425
|
-
|
|
426
|
-
|
|
484
|
+
takeConsoleErrors() {
|
|
485
|
+
return this.#consoleErrors.splice(0);
|
|
486
|
+
}
|
|
487
|
+
pendingConsoleErrors() {
|
|
488
|
+
return [...this.#consoleErrors];
|
|
489
|
+
}
|
|
490
|
+
async watchConsole(sessionId = this.#sessionId, targetId = this.#targetId) {
|
|
491
|
+
if (this.#watchedConsoleSessions.has(sessionId))
|
|
427
492
|
return;
|
|
493
|
+
this.#watchedConsoleSessions.add(sessionId);
|
|
494
|
+
const record = (source, message, url, timestamp) => {
|
|
495
|
+
if (!message.trim())
|
|
496
|
+
return;
|
|
497
|
+
this.#consoleErrors.push({
|
|
498
|
+
source,
|
|
499
|
+
message: message.slice(0, 4000),
|
|
500
|
+
...url ? { url } : {},
|
|
501
|
+
...timestamp !== undefined ? { timestamp } : {},
|
|
502
|
+
targetId
|
|
503
|
+
});
|
|
504
|
+
};
|
|
505
|
+
this.#stopPageEvents.push(this.#cdp.on("Runtime.consoleAPICalled", sessionId, (params) => {
|
|
506
|
+
if (params.type !== "error")
|
|
507
|
+
return;
|
|
508
|
+
const args = params.args;
|
|
509
|
+
const message = args?.map((arg) => String(arg.value ?? arg.description ?? "")).join(" ") ?? "";
|
|
510
|
+
const frame = params.stackTrace?.callFrames?.[0];
|
|
511
|
+
record("console", message, frame?.url, typeof params.timestamp === "number" ? params.timestamp : undefined);
|
|
512
|
+
}), this.#cdp.on("Runtime.exceptionThrown", sessionId, (params) => {
|
|
513
|
+
const details = params.exceptionDetails;
|
|
514
|
+
record("exception", details?.exception?.description ?? details?.text ?? "Uncaught exception", details?.url, typeof params.timestamp === "number" ? params.timestamp : undefined);
|
|
515
|
+
}), this.#cdp.on("Log.entryAdded", sessionId, (params) => {
|
|
516
|
+
const entry = params.entry;
|
|
517
|
+
if (entry?.level === "error")
|
|
518
|
+
record("log", entry.text ?? "", entry.url, entry.timestamp);
|
|
519
|
+
}));
|
|
520
|
+
await this.#cdp.command("Runtime.enable", {}, sessionId);
|
|
521
|
+
await this.#cdp.command("Log.enable", {}, sessionId);
|
|
522
|
+
}
|
|
523
|
+
async watchPageLoads() {
|
|
428
524
|
await this.call("Page.enable");
|
|
429
525
|
const tree = await this.call("Page.getFrameTree");
|
|
430
526
|
this.#mainFrameId = tree.frameTree.frame.id;
|
|
527
|
+
await this.call("Runtime.addBinding", { name: "__jevDomChanged" });
|
|
528
|
+
const observeDom = `(() => {
|
|
529
|
+
if (window.__jevDomWatching) return;
|
|
530
|
+
window.__jevDomWatching = true;
|
|
531
|
+
const start = () => new MutationObserver(() => window.__jevDomChanged?.('change'))
|
|
532
|
+
.observe(document, {subtree:true, childList:true, attributes:true, characterData:true});
|
|
533
|
+
if (document.documentElement) start(); else document.addEventListener('DOMContentLoaded', start, {once:true});
|
|
534
|
+
})()`;
|
|
535
|
+
await this.call("Page.addScriptToEvaluateOnNewDocument", { source: observeDom });
|
|
536
|
+
await this.evaluate(observeDom).catch(() => {
|
|
537
|
+
return;
|
|
538
|
+
});
|
|
431
539
|
const loading = (params) => {
|
|
540
|
+
if (typeof params.frameId === "string")
|
|
541
|
+
this.#loadingFrames.add(params.frameId);
|
|
542
|
+
this.signalChange();
|
|
432
543
|
if (params.frameId !== this.#mainFrameId || this.#pageLoadPromise)
|
|
433
544
|
return;
|
|
434
545
|
this.#pageLoadPromise = new Promise((resolve2) => {
|
|
@@ -436,6 +547,9 @@ class Browser {
|
|
|
436
547
|
});
|
|
437
548
|
};
|
|
438
549
|
const loaded = () => {
|
|
550
|
+
if (this.#mainFrameId)
|
|
551
|
+
this.#loadingFrames.delete(this.#mainFrameId);
|
|
552
|
+
this.signalChange();
|
|
439
553
|
if (!this.#pageLoadPromise)
|
|
440
554
|
return;
|
|
441
555
|
this.#pauseUntil = performance.now() + this.#interactionPauses;
|
|
@@ -445,34 +559,69 @@ class Browser {
|
|
|
445
559
|
};
|
|
446
560
|
this.#stopPageEvents.push(this.#cdp.on("Page.frameStartedLoading", this.#sessionId, loading), this.#cdp.on("Page.frameNavigated", this.#sessionId, (params) => {
|
|
447
561
|
const frame = params.frame;
|
|
562
|
+
this.signalChange();
|
|
448
563
|
if (frame && frame.id === this.#mainFrameId && !frame.parentId)
|
|
449
564
|
loading({ frameId: frame.id });
|
|
450
565
|
}), this.#cdp.on("Page.loadEventFired", this.#sessionId, loaded), this.#cdp.on("Page.frameStoppedLoading", this.#sessionId, (params) => {
|
|
566
|
+
if (typeof params.frameId === "string")
|
|
567
|
+
this.#loadingFrames.delete(params.frameId);
|
|
568
|
+
this.signalChange();
|
|
451
569
|
if (params.frameId === this.#mainFrameId)
|
|
452
570
|
loaded();
|
|
453
571
|
}), this.#cdp.on("Page.navigatedWithinDocument", this.#sessionId, (params) => {
|
|
572
|
+
this.signalChange();
|
|
454
573
|
if (params.frameId === this.#mainFrameId) {
|
|
455
574
|
this.#pauseUntil = performance.now() + this.#interactionPauses;
|
|
456
575
|
this.resetRecordingCursor().catch(() => {
|
|
457
576
|
return;
|
|
458
577
|
});
|
|
459
578
|
}
|
|
460
|
-
}));
|
|
579
|
+
}), this.#cdp.on("Page.frameAttached", this.#sessionId, () => this.signalChange()), this.#cdp.on("Page.frameDetached", this.#sessionId, () => this.signalChange()), this.#cdp.on("Runtime.bindingCalled", this.#sessionId, () => this.signalChange()));
|
|
580
|
+
}
|
|
581
|
+
signalChange() {
|
|
582
|
+
for (const listener of this.#changeListeners)
|
|
583
|
+
listener();
|
|
584
|
+
}
|
|
585
|
+
async waitForChange(ms) {
|
|
586
|
+
await new Promise((resolve2) => {
|
|
587
|
+
const done = () => {
|
|
588
|
+
clearTimeout(timer);
|
|
589
|
+
this.#changeListeners.delete(done);
|
|
590
|
+
resolve2();
|
|
591
|
+
};
|
|
592
|
+
const timer = setTimeout(done, Math.min(ms, 150));
|
|
593
|
+
this.#changeListeners.add(done);
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
async waitForNewTarget(ms) {
|
|
597
|
+
if (this.#newTargets.size)
|
|
598
|
+
return;
|
|
599
|
+
await new Promise((resolve2) => {
|
|
600
|
+
const done = () => {
|
|
601
|
+
clearTimeout(timer);
|
|
602
|
+
this.#popupListeners.delete(done);
|
|
603
|
+
resolve2();
|
|
604
|
+
};
|
|
605
|
+
const timer = setTimeout(done, ms);
|
|
606
|
+
this.#popupListeners.add(done);
|
|
607
|
+
});
|
|
461
608
|
}
|
|
462
609
|
async waitForInteractionPause() {
|
|
463
610
|
if (!this.#interactionPauses)
|
|
464
611
|
return;
|
|
612
|
+
const waitStarted = performance.now();
|
|
465
613
|
while (true) {
|
|
466
614
|
const pageLoad = this.#pageLoadPromise;
|
|
467
615
|
if (pageLoad) {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
]);
|
|
616
|
+
const remainingBudget = this.#waitBudgetMs - this.#waitSpentMs - (performance.now() - waitStarted);
|
|
617
|
+
if (remainingBudget <= 0)
|
|
618
|
+
throw new WaitTimeoutError(Math.round(this.#waitSpentMs + performance.now() - waitStarted), "main document loading", null);
|
|
619
|
+
await Promise.race([pageLoad, Bun.sleep(remainingBudget).then(() => {
|
|
620
|
+
throw new WaitTimeoutError(Math.round(this.#waitSpentMs + performance.now() - waitStarted), "main document loading", null);
|
|
621
|
+
})]);
|
|
474
622
|
continue;
|
|
475
623
|
}
|
|
624
|
+
this.#waitSpentMs += performance.now() - waitStarted;
|
|
476
625
|
const remaining = this.#pauseUntil - performance.now();
|
|
477
626
|
if (remaining <= 0)
|
|
478
627
|
return;
|
|
@@ -612,18 +761,52 @@ class Browser {
|
|
|
612
761
|
})()`);
|
|
613
762
|
}
|
|
614
763
|
async waitForReady() {
|
|
615
|
-
const
|
|
616
|
-
|
|
764
|
+
const started = performance.now();
|
|
765
|
+
const deadline = started + this.#waitBudgetMs - this.#waitSpentMs;
|
|
766
|
+
while (performance.now() < deadline) {
|
|
617
767
|
try {
|
|
618
|
-
if (!this.#pageLoadPromise && await this.evaluate("document.readyState") === "complete")
|
|
768
|
+
if (!this.#pageLoadPromise && await this.evaluate("document.readyState") === "complete") {
|
|
769
|
+
this.#waitSpentMs += performance.now() - started;
|
|
619
770
|
return;
|
|
771
|
+
}
|
|
772
|
+
} catch (error) {
|
|
773
|
+
if (!(error instanceof StalePageError))
|
|
774
|
+
throw error;
|
|
775
|
+
}
|
|
776
|
+
await this.waitForChange(Math.max(1, deadline - performance.now()));
|
|
777
|
+
}
|
|
778
|
+
this.#waitSpentMs += performance.now() - started;
|
|
779
|
+
const state = await this.observe(false).catch(() => null);
|
|
780
|
+
throw new WaitTimeoutError(Math.round(this.#waitSpentMs), "main document loading", state);
|
|
781
|
+
}
|
|
782
|
+
async waitForSemanticReady(initial, screenshot = this.#screenshots) {
|
|
783
|
+
const started = performance.now();
|
|
784
|
+
const spentAtStart = this.#waitSpentMs;
|
|
785
|
+
const deadline = started + Math.max(0, this.#waitBudgetMs - this.#waitSpentMs);
|
|
786
|
+
const accountWait = () => {
|
|
787
|
+
this.#waitSpentMs += Math.max(0, performance.now() - started - (this.#waitSpentMs - spentAtStart));
|
|
788
|
+
};
|
|
789
|
+
let state = initial;
|
|
790
|
+
while (true) {
|
|
791
|
+
const pending = state.frames.find((frame) => frame.loading || frame.readyState !== "complete");
|
|
792
|
+
const loadingText = state.frames.length > 1 && !state.actions.some((action) => action.frameId) && /(?:^|\n)loading(?:\.{0,3}|\s)/i.test(state.text);
|
|
793
|
+
if (!pending && !loadingText) {
|
|
794
|
+
accountWait();
|
|
795
|
+
return state;
|
|
796
|
+
}
|
|
797
|
+
const condition = pending ? `frame ${pending.url || pending.id} loading` : "embedded content loading";
|
|
798
|
+
if (performance.now() >= deadline) {
|
|
799
|
+
accountWait();
|
|
800
|
+
throw new WaitTimeoutError(Math.round(this.#waitSpentMs), condition, state);
|
|
801
|
+
}
|
|
802
|
+
await this.waitForChange(Math.max(1, deadline - performance.now()));
|
|
803
|
+
try {
|
|
804
|
+
state = await this.observe(screenshot);
|
|
620
805
|
} catch (error) {
|
|
621
806
|
if (!(error instanceof StalePageError))
|
|
622
807
|
throw error;
|
|
623
808
|
}
|
|
624
|
-
await Bun.sleep(20);
|
|
625
809
|
}
|
|
626
|
-
throw new Error("Page did not finish loading within 15 seconds");
|
|
627
810
|
}
|
|
628
811
|
async evaluate(expression, awaitPromise = false) {
|
|
629
812
|
const response = await this.call("Runtime.evaluate", { expression, returnByValue: true, awaitPromise }, awaitPromise ? 15000 : undefined);
|
|
@@ -644,14 +827,48 @@ class Browser {
|
|
|
644
827
|
throw new StalePageError("Frame changed during evaluation");
|
|
645
828
|
return response.result?.value;
|
|
646
829
|
}
|
|
647
|
-
async frameOffset(frameId) {
|
|
648
|
-
const
|
|
649
|
-
const
|
|
830
|
+
async frameOffset(frameId, parentId = null) {
|
|
831
|
+
const session = parentId ? this.#frameSessions.get(parentId) ?? this.#sessionId : this.#sessionId;
|
|
832
|
+
const owner = await this.#cdp.command("DOM.getFrameOwner", { frameId }, session);
|
|
833
|
+
const box = await this.#cdp.command("DOM.getBoxModel", { backendNodeId: owner.backendNodeId }, session);
|
|
650
834
|
return { x: box.model.content[0], y: box.model.content[1] };
|
|
651
835
|
}
|
|
652
|
-
async
|
|
836
|
+
async frameScreenOffset(frameId, frames) {
|
|
837
|
+
const states = frames ?? await this.frameStates();
|
|
838
|
+
let id = frameId;
|
|
839
|
+
let x = 0, y = 0;
|
|
840
|
+
while (id) {
|
|
841
|
+
const frame = states.find((item) => item.id === id);
|
|
842
|
+
if (!frame?.parentId)
|
|
843
|
+
break;
|
|
844
|
+
const offset = await this.frameOffset(id, frame.parentId);
|
|
845
|
+
x += offset.x;
|
|
846
|
+
y += offset.y;
|
|
847
|
+
id = frame.parentId;
|
|
848
|
+
}
|
|
849
|
+
return { x, y };
|
|
850
|
+
}
|
|
851
|
+
async frameStates() {
|
|
653
852
|
const tree = await this.call("Page.getFrameTree");
|
|
654
|
-
|
|
853
|
+
const frames = [];
|
|
854
|
+
const visit = async (node, parentId) => {
|
|
855
|
+
const { id, url = "" } = node.frame;
|
|
856
|
+
let readyState = null;
|
|
857
|
+
try {
|
|
858
|
+
readyState = id === this.#mainFrameId ? await this.evaluate("document.readyState") ?? null : await this.evaluateFrame(id, "document.readyState") ?? null;
|
|
859
|
+
} catch {}
|
|
860
|
+
frames.push({
|
|
861
|
+
id,
|
|
862
|
+
parentId,
|
|
863
|
+
url,
|
|
864
|
+
readyState,
|
|
865
|
+
loading: this.#loadingFrames.has(id) || readyState !== "complete"
|
|
866
|
+
});
|
|
867
|
+
for (const child of node.childFrames ?? [])
|
|
868
|
+
await visit(child, id);
|
|
869
|
+
};
|
|
870
|
+
await visit(tree.frameTree, null);
|
|
871
|
+
return frames;
|
|
655
872
|
}
|
|
656
873
|
async discoverTabs() {
|
|
657
874
|
if (!this.#switchOnPopup)
|
|
@@ -660,6 +877,7 @@ class Browser {
|
|
|
660
877
|
if (target.type !== "page" || this.#knownTargets.has(target.id))
|
|
661
878
|
continue;
|
|
662
879
|
this.#knownTargets.add(target.id);
|
|
880
|
+
this.#newTargets.delete(target.id);
|
|
663
881
|
const attached = await this.#cdp.command("Target.attachToTarget", { targetId: target.id, flatten: true });
|
|
664
882
|
try {
|
|
665
883
|
await this.#cdp.command("Emulation.setDeviceMetricsOverride", {
|
|
@@ -680,6 +898,9 @@ class Browser {
|
|
|
680
898
|
}
|
|
681
899
|
this.#sessionId = attached.sessionId;
|
|
682
900
|
this.#targetId = target.id;
|
|
901
|
+
this.#loadingFrames.clear();
|
|
902
|
+
await this.watchConsole();
|
|
903
|
+
await this.watchPageLoads();
|
|
683
904
|
this.#switchOnPopup = false;
|
|
684
905
|
await this.startRecording();
|
|
685
906
|
} else {
|
|
@@ -687,7 +908,30 @@ class Browser {
|
|
|
687
908
|
}
|
|
688
909
|
}
|
|
689
910
|
this.#openedTabs.push({ id: target.id, url: target.url, title: target.title });
|
|
911
|
+
if (this.#origin)
|
|
912
|
+
this.#transitions.push({
|
|
913
|
+
kind: "new_tab",
|
|
914
|
+
control: this.#origin.control,
|
|
915
|
+
fromTargetId: this.#origin.targetId,
|
|
916
|
+
targetId: target.id,
|
|
917
|
+
fromUrl: this.#origin.url,
|
|
918
|
+
destinationUrl: target.url,
|
|
919
|
+
settled: false
|
|
920
|
+
});
|
|
921
|
+
const urls = this.#targetUrls.get(target.id) ?? [];
|
|
922
|
+
const requested = this.#origin?.href ?? urls[0];
|
|
923
|
+
if (this.#origin && requested && requested !== target.url)
|
|
924
|
+
this.#transitions.push({
|
|
925
|
+
kind: "redirect",
|
|
926
|
+
control: this.#origin.control,
|
|
927
|
+
fromTargetId: this.#origin.targetId,
|
|
928
|
+
targetId: target.id,
|
|
929
|
+
fromUrl: requested,
|
|
930
|
+
destinationUrl: target.url,
|
|
931
|
+
settled: false
|
|
932
|
+
});
|
|
690
933
|
}
|
|
934
|
+
this.#switchOnPopup = false;
|
|
691
935
|
}
|
|
692
936
|
async settleAfterInput() {
|
|
693
937
|
const action = this.#afterInput;
|
|
@@ -726,32 +970,42 @@ class Browser {
|
|
|
726
970
|
await this.settleAfterInput();
|
|
727
971
|
await this.discoverTabs();
|
|
728
972
|
let info;
|
|
729
|
-
|
|
973
|
+
const waitStarted = performance.now();
|
|
974
|
+
const deadline = waitStarted + Math.max(0, this.#waitBudgetMs - this.#waitSpentMs);
|
|
975
|
+
let waited = false;
|
|
976
|
+
while (true) {
|
|
730
977
|
try {
|
|
731
978
|
info = await this.evaluate(snapshot_default);
|
|
732
979
|
if (info)
|
|
733
980
|
break;
|
|
734
981
|
} catch (error) {
|
|
735
|
-
if (!(error instanceof StalePageError)
|
|
982
|
+
if (!(error instanceof StalePageError))
|
|
736
983
|
throw error;
|
|
737
984
|
}
|
|
738
|
-
|
|
985
|
+
if (performance.now() >= deadline) {
|
|
986
|
+
this.#waitSpentMs += performance.now() - waitStarted;
|
|
987
|
+
throw new WaitTimeoutError(Math.round(this.#waitSpentMs), "document navigating", null);
|
|
988
|
+
}
|
|
989
|
+
waited = true;
|
|
990
|
+
await this.waitForChange(Math.max(1, deadline - performance.now()));
|
|
739
991
|
}
|
|
740
|
-
if (
|
|
741
|
-
|
|
992
|
+
if (waited)
|
|
993
|
+
this.#waitSpentMs += performance.now() - waitStarted;
|
|
742
994
|
if (this.#openedTabs.length) {
|
|
743
|
-
const
|
|
995
|
+
const targets2 = await listChromeTargets(this.#cdpUrl);
|
|
744
996
|
this.#openedTabs = this.#openedTabs.map((tab) => {
|
|
745
|
-
const current =
|
|
997
|
+
const current = targets2.find((target) => target.id === tab.id);
|
|
746
998
|
return current ? { ...tab, url: current.url, title: current.title } : tab;
|
|
747
999
|
});
|
|
748
1000
|
info.text = `${info.text}
|
|
749
1001
|
${this.#openedTabs.map((tab) => `Opened new tab: ${tab.title} ${tab.url} (target ${tab.id})`).join(`
|
|
750
1002
|
`)}`;
|
|
751
1003
|
}
|
|
752
|
-
|
|
1004
|
+
const frames = await this.frameStates();
|
|
1005
|
+
for (const frame of frames.filter((frame2) => frame2.parentId !== null)) {
|
|
1006
|
+
const frameId = frame.id;
|
|
753
1007
|
try {
|
|
754
|
-
const offset = await this.
|
|
1008
|
+
const offset = await this.frameScreenOffset(frameId, frames);
|
|
755
1009
|
const child = await this.evaluateFrame(frameId, snapshot_default);
|
|
756
1010
|
if (!child)
|
|
757
1011
|
continue;
|
|
@@ -763,13 +1017,67 @@ ${child.text}`.slice(0, 6000);
|
|
|
763
1017
|
const rect = { ...action.rect, x: action.rect.x + offset.x, y: action.rect.y + offset.y };
|
|
764
1018
|
if (rect.x < 0 || rect.y < 0 || rect.x >= info.w || rect.y >= info.h)
|
|
765
1019
|
continue;
|
|
766
|
-
|
|
1020
|
+
const covering = await this.evaluate(`(() => {
|
|
1021
|
+
const e=document.elementFromPoint(${rect.x + rect.w / 2},${rect.y + rect.h / 2});
|
|
1022
|
+
if (!e || e.tagName==='IFRAME') return null;
|
|
1023
|
+
return {tag:e.tagName.toLowerCase(),text:(e.innerText||e.getAttribute('aria-label')||'').trim().slice(0,160),role:e.getAttribute('role')};
|
|
1024
|
+
})()`);
|
|
1025
|
+
info.actions.push({
|
|
1026
|
+
...action,
|
|
1027
|
+
frameId,
|
|
1028
|
+
rect,
|
|
1029
|
+
id: `e${info.actions.length + 1}`,
|
|
1030
|
+
clickable: action.clickable && !covering,
|
|
1031
|
+
coveredBy: covering ?? action.coveredBy
|
|
1032
|
+
});
|
|
767
1033
|
info.guards[`${frameId}:${action.node}`] = child.guards[String(action.node)];
|
|
768
1034
|
}
|
|
769
1035
|
info.marker = [info.marker, frameId, child.marker];
|
|
770
1036
|
} catch {}
|
|
771
1037
|
}
|
|
772
|
-
|
|
1038
|
+
if (this.#origin && this.#targetId === this.#origin.targetId && info.url !== this.#origin.url && !this.#transitions.some((transition) => transition.control === this.#origin.control && transition.fromUrl === this.#origin.url && transition.targetId === this.#targetId)) {
|
|
1039
|
+
this.#transitions.push({
|
|
1040
|
+
kind: "navigation",
|
|
1041
|
+
control: this.#origin.control,
|
|
1042
|
+
fromTargetId: this.#origin.targetId,
|
|
1043
|
+
targetId: this.#targetId,
|
|
1044
|
+
fromUrl: this.#origin.url,
|
|
1045
|
+
destinationUrl: info.url,
|
|
1046
|
+
settled: false
|
|
1047
|
+
});
|
|
1048
|
+
if (this.#origin.href && this.#origin.href !== info.url)
|
|
1049
|
+
this.#transitions.push({
|
|
1050
|
+
kind: "redirect",
|
|
1051
|
+
control: this.#origin.control,
|
|
1052
|
+
fromTargetId: this.#origin.targetId,
|
|
1053
|
+
targetId: this.#targetId,
|
|
1054
|
+
fromUrl: this.#origin.href,
|
|
1055
|
+
destinationUrl: info.url,
|
|
1056
|
+
settled: false
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
const targets = this.#transitions.length ? await listChromeTargets(this.#cdpUrl) : [];
|
|
1060
|
+
for (const transition of this.#transitions) {
|
|
1061
|
+
const target = targets.find((item) => item.id === transition.targetId);
|
|
1062
|
+
if (target)
|
|
1063
|
+
transition.destinationUrl = target.url;
|
|
1064
|
+
transition.settled = frames.every((frame) => !frame.loading);
|
|
1065
|
+
}
|
|
1066
|
+
if (this.#origin?.href)
|
|
1067
|
+
for (const transition of this.#transitions.filter((item) => item.kind === "new_tab" && item.control === this.#origin.control)) {
|
|
1068
|
+
if (transition.destinationUrl !== this.#origin.href && !this.#transitions.some((item) => item.kind === "redirect" && item.targetId === transition.targetId && item.fromUrl === this.#origin.href)) {
|
|
1069
|
+
this.#transitions.push({
|
|
1070
|
+
kind: "redirect",
|
|
1071
|
+
control: transition.control,
|
|
1072
|
+
fromTargetId: transition.fromTargetId,
|
|
1073
|
+
targetId: transition.targetId,
|
|
1074
|
+
fromUrl: this.#origin.href,
|
|
1075
|
+
destinationUrl: transition.destinationUrl,
|
|
1076
|
+
settled: transition.settled
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
const page = { ...info, frames, transitions: [...this.#transitions], fingerprint: fingerprint(info) };
|
|
773
1081
|
if (screenshot) {
|
|
774
1082
|
const capture = await this.call("Page.captureScreenshot", { format: "jpeg", quality: 72 });
|
|
775
1083
|
page.screenshot = capture.data;
|
|
@@ -792,15 +1100,49 @@ ${child.text}`.slice(0, 6000);
|
|
|
792
1100
|
})()`);
|
|
793
1101
|
return stableStringify(current) === stableStringify([page.page_key, page.guards[String(action.node)]]);
|
|
794
1102
|
}
|
|
795
|
-
|
|
1103
|
+
let marker = await this.evaluate(MARKER);
|
|
1104
|
+
for (const frame of (await this.frameStates()).filter((item) => item.parentId !== null)) {
|
|
1105
|
+
try {
|
|
1106
|
+
const childMarker = await this.evaluateFrame(frame.id, MARKER);
|
|
1107
|
+
if (childMarker !== undefined)
|
|
1108
|
+
marker = [marker ?? null, frame.id, childMarker];
|
|
1109
|
+
} catch {
|
|
1110
|
+
return false;
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
796
1113
|
return stableStringify(marker) === stableStringify(page.marker);
|
|
797
1114
|
}
|
|
1115
|
+
async describeElement(action) {
|
|
1116
|
+
const expression = `(node => {
|
|
1117
|
+
const e = window.__jevFast?.nodes.get(node);
|
|
1118
|
+
if (!e?.isConnected) return null;
|
|
1119
|
+
const path = [];
|
|
1120
|
+
for (let current = e; current; current = current.parentElement) {
|
|
1121
|
+
if (current.id && document.querySelectorAll('#' + CSS.escape(current.id)).length === 1) {
|
|
1122
|
+
path.unshift('#' + CSS.escape(current.id));
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
let position = 1;
|
|
1126
|
+
for (let sibling = current.previousElementSibling; sibling; sibling = sibling.previousElementSibling) {
|
|
1127
|
+
if (sibling.tagName === current.tagName) position++;
|
|
1128
|
+
}
|
|
1129
|
+
path.unshift(current.tagName.toLowerCase() + ':nth-of-type(' + position + ')');
|
|
1130
|
+
}
|
|
1131
|
+
return {css:path.join(' > '),tag:e.tagName.toLowerCase(),
|
|
1132
|
+
href:typeof e.href === 'string' ? e.href : e.getAttribute('href'),
|
|
1133
|
+
inputType:e.getAttribute('type'),frameUrl:location.href,target:e.getAttribute('target')};
|
|
1134
|
+
})(${action.node})`;
|
|
1135
|
+
const details = action.frameId ? await this.evaluateFrame(action.frameId, expression) : await this.evaluate(expression);
|
|
1136
|
+
if (!details)
|
|
1137
|
+
throw new StalePageError("Target changed before replay details were captured");
|
|
1138
|
+
return details;
|
|
1139
|
+
}
|
|
798
1140
|
async act(action, page, text) {
|
|
799
1141
|
if (!await this.fresh(page, action))
|
|
800
1142
|
throw new StalePageError("Page changed since this decision");
|
|
801
1143
|
if (action.kind === "wait") {
|
|
802
1144
|
await Bun.sleep(100);
|
|
803
|
-
return;
|
|
1145
|
+
return { element: null, performedAt: performance.now() };
|
|
804
1146
|
}
|
|
805
1147
|
if (action.kind === "scroll") {
|
|
806
1148
|
await this.animateCursor(550, 650);
|
|
@@ -811,14 +1153,24 @@ ${child.text}`.slice(0, 6000);
|
|
|
811
1153
|
deltaX: 0,
|
|
812
1154
|
deltaY: action.delta ?? 0
|
|
813
1155
|
});
|
|
814
|
-
return;
|
|
1156
|
+
return { element: null, performedAt: performance.now() };
|
|
815
1157
|
}
|
|
816
1158
|
if (typeof action.node !== "number")
|
|
817
1159
|
throw new Error("Invalid observed node");
|
|
1160
|
+
const details = await this.describeElement(action);
|
|
1161
|
+
if (action.kind === "click")
|
|
1162
|
+
this.#origin = {
|
|
1163
|
+
control: action.label,
|
|
1164
|
+
targetId: this.#targetId,
|
|
1165
|
+
url: page.url,
|
|
1166
|
+
href: details.href
|
|
1167
|
+
};
|
|
818
1168
|
const target = await (action.frameId ? this.evaluateFrame(action.frameId, `(action => {
|
|
819
1169
|
const e=window.__jevFast?.nodes.get(action.node);
|
|
820
1170
|
if (!e?.isConnected || !e.checkVisibility({checkOpacity:true,checkVisibilityCSS:true})) return null;
|
|
821
|
-
const r=e.getBoundingClientRect()
|
|
1171
|
+
const r=e.getBoundingClientRect(), x=r.x+r.width/2, y=r.y+r.height/2;
|
|
1172
|
+
if (!e.contains(document.elementFromPoint(x,y))) return null;
|
|
1173
|
+
return {x,y};
|
|
822
1174
|
})(${JSON.stringify(action)})`) : this.evaluate(`(action => {
|
|
823
1175
|
const e=window.__jevFast?.nodes.get(action.node);
|
|
824
1176
|
if (!e?.isConnected || e.matches(':disabled') || e.closest('[aria-disabled="true"],[inert]') ||
|
|
@@ -841,11 +1193,37 @@ ${child.text}`.slice(0, 6000);
|
|
|
841
1193
|
throw new Error("Dropdown execution was not confirmed");
|
|
842
1194
|
throw new StalePageError("Target changed or is covered");
|
|
843
1195
|
}
|
|
1196
|
+
let performedAt = action.kind === "select" ? performance.now() : 0;
|
|
844
1197
|
if (action.frameId) {
|
|
845
|
-
const offset = await this.
|
|
1198
|
+
const offset = await this.frameScreenOffset(action.frameId);
|
|
846
1199
|
target.x += offset.x;
|
|
847
1200
|
target.y += offset.y;
|
|
1201
|
+
const covered = await this.evaluate(`(() => {
|
|
1202
|
+
const e=document.elementFromPoint(${target.x},${target.y});
|
|
1203
|
+
return !e || e.tagName!=='IFRAME';
|
|
1204
|
+
})()`);
|
|
1205
|
+
if (covered)
|
|
1206
|
+
throw new StalePageError("Target is covered in the parent page");
|
|
848
1207
|
}
|
|
1208
|
+
let frame = null;
|
|
1209
|
+
if (action.frameId) {
|
|
1210
|
+
const frames = await this.frameStates();
|
|
1211
|
+
const parentId = frames.find((item) => item.id === action.frameId)?.parentId ?? null;
|
|
1212
|
+
const index = frames.filter((item) => item.parentId === parentId).findIndex((item) => item.id === action.frameId);
|
|
1213
|
+
if (index < 0)
|
|
1214
|
+
throw new StalePageError("Target frame changed before input");
|
|
1215
|
+
frame = { id: action.frameId, parentId, url: details.frameUrl, index };
|
|
1216
|
+
}
|
|
1217
|
+
const element = {
|
|
1218
|
+
css: details.css,
|
|
1219
|
+
role: action.role ?? null,
|
|
1220
|
+
name: action.label,
|
|
1221
|
+
tag: details.tag,
|
|
1222
|
+
href: details.href,
|
|
1223
|
+
inputType: details.inputType,
|
|
1224
|
+
point: { x: target.x, y: target.y },
|
|
1225
|
+
frame
|
|
1226
|
+
};
|
|
849
1227
|
await this.animateCursor(target.x, target.y);
|
|
850
1228
|
if (action.kind !== "select") {
|
|
851
1229
|
if (action.kind === "click" && this.#interactionPauses > 0) {
|
|
@@ -867,6 +1245,7 @@ ${child.text}`.slice(0, 6000);
|
|
|
867
1245
|
if (type === "mousePressed")
|
|
868
1246
|
await this.animateCursor(target.x, target.y, true);
|
|
869
1247
|
}
|
|
1248
|
+
performedAt = performance.now();
|
|
870
1249
|
if (action.kind === "fill") {
|
|
871
1250
|
const modifiers = process.platform === "darwin" ? 4 : 2;
|
|
872
1251
|
await this.call("Input.dispatchKeyEvent", {
|
|
@@ -883,13 +1262,15 @@ ${child.text}`.slice(0, 6000);
|
|
|
883
1262
|
modifiers
|
|
884
1263
|
});
|
|
885
1264
|
await this.call("Input.insertText", { text: text ?? "" });
|
|
1265
|
+
performedAt = performance.now();
|
|
886
1266
|
}
|
|
887
1267
|
}
|
|
888
1268
|
this.#afterInput = action;
|
|
889
|
-
if (action.
|
|
1269
|
+
if (action.kind === "click") {
|
|
890
1270
|
this.#switchOnPopup = true;
|
|
891
|
-
await
|
|
1271
|
+
await this.waitForNewTarget(details.target === "_blank" ? 650 : 100);
|
|
892
1272
|
}
|
|
1273
|
+
return { element, performedAt };
|
|
893
1274
|
}
|
|
894
1275
|
async close() {
|
|
895
1276
|
if (this.#closed)
|
|
@@ -938,9 +1319,9 @@ its matching autocomplete suggestion selected. For date pickers, CLICK the field
|
|
|
938
1319
|
Set every requested filter/control; a matching result alone does not prove a requested filter was set.
|
|
939
1320
|
Do not toggle a checkbox, switch, or radio already in the requested state.
|
|
940
1321
|
Submit populated search fields before opening a result; a populated field alone is not an applied search.
|
|
941
|
-
WAIT only when the needed control is absent/disabled, or submitted results are still loading.
|
|
942
1322
|
If Search/Submit is visible and the required fields are ready, CLICK it immediately.
|
|
943
|
-
|
|
1323
|
+
Loading and frame readiness are handled by the browser adapter before this decision.
|
|
1324
|
+
Use frame URL, nearby text, bounds, and clickability to distinguish repeated labels.
|
|
944
1325
|
DONE requires visible evidence that ALL requirements are satisfied. If asked to open a result,
|
|
945
1326
|
a matching link is not enough. BLOCKED means no supported operation can make progress.`;
|
|
946
1327
|
var TARGET = `Choose the best observed target if the next operation is the one specified in this question.
|
|
@@ -1012,13 +1393,21 @@ function actionSpace(actions) {
|
|
|
1012
1393
|
}
|
|
1013
1394
|
if (typeof action.node !== "number")
|
|
1014
1395
|
continue;
|
|
1015
|
-
|
|
1396
|
+
const identity = `${action.frameId ?? "main"}:${action.node}`;
|
|
1397
|
+
if (!indices.has(identity)) {
|
|
1016
1398
|
const index2 = String(elements.length + 1);
|
|
1017
|
-
indices.set(
|
|
1399
|
+
indices.set(identity, index2);
|
|
1018
1400
|
const element2 = {
|
|
1019
1401
|
index: index2,
|
|
1020
1402
|
label: action.label.split(" \u2192 ")[0] ?? action.label,
|
|
1021
|
-
operations: []
|
|
1403
|
+
operations: [],
|
|
1404
|
+
frameId: action.frameId ?? null,
|
|
1405
|
+
frameUrl: action.frameUrl ?? null,
|
|
1406
|
+
bounds: action.rect ?? null,
|
|
1407
|
+
nearbyText: action.nearbyText ?? "",
|
|
1408
|
+
region: action.region ?? "",
|
|
1409
|
+
clickable: action.clickable ?? true,
|
|
1410
|
+
coveredBy: action.coveredBy ?? null
|
|
1022
1411
|
};
|
|
1023
1412
|
for (const key of ["role", "value", "checked", "selected", "expanded", "pressed", "sensitive"]) {
|
|
1024
1413
|
if (action[key] !== undefined)
|
|
@@ -1030,7 +1419,9 @@ function actionSpace(actions) {
|
|
|
1030
1419
|
}
|
|
1031
1420
|
elements.push(element2);
|
|
1032
1421
|
}
|
|
1033
|
-
const index = indices.get(
|
|
1422
|
+
const index = indices.get(identity);
|
|
1423
|
+
if (action.clickable === false)
|
|
1424
|
+
continue;
|
|
1034
1425
|
const group = targets[operation] ??= {};
|
|
1035
1426
|
const element = elements[Number(index) - 1];
|
|
1036
1427
|
if (!element.operations.includes(operation))
|
|
@@ -1069,6 +1460,13 @@ async function choose(page, goal, history, providedFields = []) {
|
|
|
1069
1460
|
type: "choice",
|
|
1070
1461
|
criteria: Object.fromEntries(Object.entries(candidates).map(([index, action]) => [index, {
|
|
1071
1462
|
element: `[${index}] ${action.label}`,
|
|
1463
|
+
frame_id: action.frameId ?? null,
|
|
1464
|
+
frame: action.frameUrl ?? page.url,
|
|
1465
|
+
region: action.region ?? "",
|
|
1466
|
+
nearby_text: action.nearbyText ?? "",
|
|
1467
|
+
bounds: action.rect ?? null,
|
|
1468
|
+
clickable: action.clickable ?? true,
|
|
1469
|
+
covered_by: action.coveredBy ?? null,
|
|
1072
1470
|
current_value: action.current_value ?? action.value ?? "",
|
|
1073
1471
|
...Object.fromEntries(["role", "checked", "selected", "expanded", "pressed", "sensitive"].filter((name) => action[name] !== undefined).map((name) => [name, action[name]]))
|
|
1074
1472
|
}])),
|
|
@@ -1248,12 +1646,23 @@ function fieldText(context) {
|
|
|
1248
1646
|
}
|
|
1249
1647
|
|
|
1250
1648
|
// src/agent.ts
|
|
1649
|
+
function redactConsoleErrors(errors, secrets) {
|
|
1650
|
+
const redact = (text) => secrets.reduce((value, secret) => value.replaceAll(secret, "[redacted]"), text);
|
|
1651
|
+
return errors.map((error) => ({
|
|
1652
|
+
...error,
|
|
1653
|
+
message: redact(error.message),
|
|
1654
|
+
...error.url ? { url: redact(error.url) } : {}
|
|
1655
|
+
}));
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1251
1658
|
class Agent {
|
|
1252
1659
|
#browser;
|
|
1253
1660
|
#goal;
|
|
1254
1661
|
#maxSteps;
|
|
1255
1662
|
#screenshots;
|
|
1256
1663
|
#fieldValues;
|
|
1664
|
+
#sensitiveFieldLabels;
|
|
1665
|
+
#secretValues;
|
|
1257
1666
|
#page;
|
|
1258
1667
|
#decision = null;
|
|
1259
1668
|
#history = [];
|
|
@@ -1262,6 +1671,8 @@ class Agent {
|
|
|
1262
1671
|
#status = "ready";
|
|
1263
1672
|
#startedAt = null;
|
|
1264
1673
|
#pendingText = null;
|
|
1674
|
+
#waitTimeout;
|
|
1675
|
+
#initialConsoleErrors = [];
|
|
1265
1676
|
constructor(browser, page, options) {
|
|
1266
1677
|
this.#browser = browser;
|
|
1267
1678
|
this.#page = page;
|
|
@@ -1269,6 +1680,9 @@ class Agent {
|
|
|
1269
1680
|
this.#maxSteps = options.maxSteps;
|
|
1270
1681
|
this.#screenshots = options.screenshots ?? false;
|
|
1271
1682
|
this.#fieldValues = options.fieldValues ?? {};
|
|
1683
|
+
this.#sensitiveFieldLabels = new Set(options.sensitiveFieldLabels ?? []);
|
|
1684
|
+
this.#secretValues = Object.entries(this.#fieldValues).filter(([label, value]) => this.#sensitiveFieldLabels.has(label) && value.length > 0).map(([, value]) => value);
|
|
1685
|
+
this.#startedAt = performance.now();
|
|
1272
1686
|
}
|
|
1273
1687
|
static async create(options) {
|
|
1274
1688
|
if (!options.goal.trim())
|
|
@@ -1286,12 +1700,24 @@ class Agent {
|
|
|
1286
1700
|
recordingPath: options.recordingPath,
|
|
1287
1701
|
screenshotPath: options.screenshotPath,
|
|
1288
1702
|
freshContext: options.freshContext,
|
|
1289
|
-
interactionPauses: options.interactionPauses
|
|
1703
|
+
interactionPauses: options.interactionPauses,
|
|
1704
|
+
waitBudgetMs: options.waitBudgetMs
|
|
1290
1705
|
});
|
|
1291
1706
|
try {
|
|
1292
|
-
|
|
1707
|
+
const agent = new Agent(browser, await browser.observe(options.screenshots), options);
|
|
1708
|
+
try {
|
|
1709
|
+
await agent.waitForReadiness();
|
|
1710
|
+
} catch (error) {
|
|
1711
|
+
if (!(error instanceof WaitTimeoutError))
|
|
1712
|
+
throw error;
|
|
1713
|
+
agent.recordWaitTimeout(error);
|
|
1714
|
+
}
|
|
1715
|
+
agent.#initialConsoleErrors = redactConsoleErrors(browser.takeConsoleErrors(), agent.#secretValues);
|
|
1716
|
+
return agent;
|
|
1293
1717
|
} catch (error) {
|
|
1294
|
-
await browser.close()
|
|
1718
|
+
await browser.close().catch(() => {
|
|
1719
|
+
return;
|
|
1720
|
+
});
|
|
1295
1721
|
throw error;
|
|
1296
1722
|
}
|
|
1297
1723
|
}
|
|
@@ -1306,7 +1732,14 @@ class Agent {
|
|
|
1306
1732
|
status: this.#status,
|
|
1307
1733
|
elapsedMs: this.elapsedMs(),
|
|
1308
1734
|
maxSteps: this.#maxSteps,
|
|
1309
|
-
elements: actionSpace(this.#page.actions).elements
|
|
1735
|
+
elements: actionSpace(this.#page.actions).elements,
|
|
1736
|
+
initialConsoleErrors: [...this.#initialConsoleErrors],
|
|
1737
|
+
consoleErrors: [
|
|
1738
|
+
...this.#initialConsoleErrors,
|
|
1739
|
+
...this.#history.flatMap((entry) => entry.consoleErrors),
|
|
1740
|
+
...redactConsoleErrors(this.#browser.pendingConsoleErrors(), this.#secretValues)
|
|
1741
|
+
],
|
|
1742
|
+
...this.#waitTimeout ? { waitTimeout: this.#waitTimeout } : {}
|
|
1310
1743
|
};
|
|
1311
1744
|
}
|
|
1312
1745
|
get targetId() {
|
|
@@ -1320,15 +1753,12 @@ class Agent {
|
|
|
1320
1753
|
this.#startedAt = performance.now();
|
|
1321
1754
|
if (!await this.#browser.fresh(this.#page)) {
|
|
1322
1755
|
this.#page = await this.#browser.observe(this.#screenshots);
|
|
1756
|
+
await this.waitForReadiness();
|
|
1323
1757
|
}
|
|
1324
1758
|
this.#decision = null;
|
|
1325
|
-
if (["done", "blocked", "budget_exhausted"].includes(this.#status)) {
|
|
1759
|
+
if (["done", "blocked", "budget_exhausted", "wait_timeout"].includes(this.#status)) {
|
|
1326
1760
|
throw new Error("This run has stopped");
|
|
1327
1761
|
}
|
|
1328
|
-
if (this.#decisions.length >= this.#maxSteps * 2) {
|
|
1329
|
-
this.#status = "budget_exhausted";
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
1762
|
this.#decision = await choose(this.#page, this.#goal, this.#history, Object.keys(this.#fieldValues));
|
|
1333
1763
|
this.#decisions.push(this.#decision);
|
|
1334
1764
|
this.#status = "predicted";
|
|
@@ -1369,7 +1799,7 @@ class Agent {
|
|
|
1369
1799
|
if (provided !== undefined) {
|
|
1370
1800
|
text = provided;
|
|
1371
1801
|
helper = { model: "provided-field-value", provider: "caller", latency_ms: 0, usage: {} };
|
|
1372
|
-
this.#textCalls.push({ ...helper, field: action.label, value: action.sensitive ? "[redacted]" : text });
|
|
1802
|
+
this.#textCalls.push({ ...helper, field: action.label, value: action.sensitive || this.#sensitiveFieldLabels.has(action.label) ? "[redacted]" : text });
|
|
1373
1803
|
} else if (this.#pendingText?.contextKey === contextKey) {
|
|
1374
1804
|
({ text, helper } = this.#pendingText);
|
|
1375
1805
|
} else {
|
|
@@ -1379,7 +1809,8 @@ class Agent {
|
|
|
1379
1809
|
}
|
|
1380
1810
|
}
|
|
1381
1811
|
await this.#browser.waitForInteractionPause();
|
|
1382
|
-
|
|
1812
|
+
const fromTargetId = this.#browser.targetId;
|
|
1813
|
+
const { element, performedAt } = await this.#browser.act(action, page, text ?? undefined);
|
|
1383
1814
|
this.#pendingText = null;
|
|
1384
1815
|
const entry = {
|
|
1385
1816
|
step: this.#history.length + 1,
|
|
@@ -1389,21 +1820,33 @@ class Agent {
|
|
|
1389
1820
|
probability: decision.probabilities[selected] ?? 0,
|
|
1390
1821
|
confidence: decision.confidence,
|
|
1391
1822
|
latency_ms: decision.latency_ms,
|
|
1392
|
-
text: action.sensitive && text !== null ? "[redacted]" : text,
|
|
1823
|
+
text: (action.sensitive || this.#sensitiveFieldLabels.has(action.label)) && text !== null ? "[redacted]" : text,
|
|
1393
1824
|
text_helper: helper?.model ?? null,
|
|
1394
1825
|
text_latency_ms: helper?.latency_ms ?? 0,
|
|
1395
1826
|
operation: decision.operation,
|
|
1396
1827
|
target: decision.target,
|
|
1397
1828
|
page_changed: null,
|
|
1829
|
+
from_url: page.url,
|
|
1398
1830
|
url: page.url,
|
|
1831
|
+
viewport: { width: page.w, height: page.h },
|
|
1832
|
+
from_target_id: fromTargetId,
|
|
1833
|
+
target_id: this.#browser.targetId,
|
|
1834
|
+
element,
|
|
1835
|
+
value: action.kind === "select" ? action.value ?? null : null,
|
|
1836
|
+
delta_y: action.kind === "scroll" ? action.delta ?? 0 : null,
|
|
1837
|
+
redacted: action.kind === "fill" && (Boolean(action.sensitive) || this.#sensitiveFieldLabels.has(action.label)),
|
|
1399
1838
|
usage: decision.usage,
|
|
1400
|
-
executed_ms:
|
|
1401
|
-
elapsed_ms: this.elapsedMs()
|
|
1839
|
+
executed_ms: Math.round(performedAt - this.#startedAt),
|
|
1840
|
+
elapsed_ms: this.elapsedMs(),
|
|
1841
|
+
consoleErrors: []
|
|
1402
1842
|
};
|
|
1403
1843
|
this.#history.push(entry);
|
|
1404
1844
|
this.#page = await this.#browser.observe(this.#screenshots);
|
|
1845
|
+
await this.waitForReadiness();
|
|
1846
|
+
entry.consoleErrors = redactConsoleErrors(this.#browser.takeConsoleErrors(), this.#secretValues);
|
|
1405
1847
|
entry.page_changed = this.#page.fingerprint !== page.fingerprint;
|
|
1406
1848
|
entry.url = this.#page.url;
|
|
1849
|
+
entry.target_id = this.#browser.targetId;
|
|
1407
1850
|
entry.elapsed_ms = this.elapsedMs();
|
|
1408
1851
|
this.#status = "ready";
|
|
1409
1852
|
}
|
|
@@ -1412,21 +1855,41 @@ class Agent {
|
|
|
1412
1855
|
await this.predict();
|
|
1413
1856
|
await this.act();
|
|
1414
1857
|
} catch (error) {
|
|
1858
|
+
if (error instanceof WaitTimeoutError) {
|
|
1859
|
+
this.recordWaitTimeout(error);
|
|
1860
|
+
return this.snapshot();
|
|
1861
|
+
}
|
|
1415
1862
|
if (!(error instanceof StalePageError))
|
|
1416
1863
|
throw error;
|
|
1417
1864
|
this.#decision = null;
|
|
1418
1865
|
this.#status = "ready";
|
|
1419
1866
|
this.#page = await this.#browser.observe(this.#screenshots);
|
|
1867
|
+
try {
|
|
1868
|
+
await this.waitForReadiness();
|
|
1869
|
+
} catch (waitError) {
|
|
1870
|
+
if (!(waitError instanceof WaitTimeoutError))
|
|
1871
|
+
throw waitError;
|
|
1872
|
+
this.recordWaitTimeout(waitError);
|
|
1873
|
+
}
|
|
1420
1874
|
}
|
|
1421
1875
|
return this.snapshot();
|
|
1422
1876
|
}
|
|
1423
1877
|
async run(onStep) {
|
|
1424
|
-
while (!["done", "blocked", "budget_exhausted"].includes(this.#status)) {
|
|
1878
|
+
while (!["done", "blocked", "budget_exhausted", "wait_timeout"].includes(this.#status)) {
|
|
1425
1879
|
const state = await this.tick();
|
|
1426
1880
|
onStep?.(state);
|
|
1427
1881
|
}
|
|
1428
1882
|
return this.snapshot();
|
|
1429
1883
|
}
|
|
1884
|
+
async waitForReadiness() {
|
|
1885
|
+
this.#page = await this.#browser.waitForSemanticReady(this.#page, this.#screenshots);
|
|
1886
|
+
}
|
|
1887
|
+
recordWaitTimeout(error) {
|
|
1888
|
+
this.#status = "wait_timeout";
|
|
1889
|
+
this.#waitTimeout = { elapsedMs: error.elapsedMs, pendingCondition: error.pendingCondition };
|
|
1890
|
+
if (error.state)
|
|
1891
|
+
this.#page = error.state;
|
|
1892
|
+
}
|
|
1430
1893
|
close() {
|
|
1431
1894
|
return this.#browser.close();
|
|
1432
1895
|
}
|
|
@@ -1502,6 +1965,8 @@ Goal control:
|
|
|
1502
1965
|
--goal <text> One bounded browser goal. Required.
|
|
1503
1966
|
--max-steps <number> Maximum executed browser actions.
|
|
1504
1967
|
[env: JEV_MAX_STEPS] [default: 12]
|
|
1968
|
+
--wait-budget-ms <number> Total wall-clock budget for page and frame readiness.
|
|
1969
|
+
[default: 15000]
|
|
1505
1970
|
|
|
1506
1971
|
Browser behavior:
|
|
1507
1972
|
--visible Activate the controlled tab.
|
|
@@ -1525,12 +1990,15 @@ Evidence and output:
|
|
|
1525
1990
|
-h, --help Show this help and exit.
|
|
1526
1991
|
|
|
1527
1992
|
Output:
|
|
1528
|
-
|
|
1993
|
+
Stdout is JSON Lines: one object per executed action, then one result object.
|
|
1994
|
+
Each action includes new console errors observed during that step. The result
|
|
1995
|
+
includes initial errors and all errors observed during the run.
|
|
1996
|
+
Errors and diagnostics use stderr.
|
|
1529
1997
|
|
|
1530
1998
|
Exit codes:
|
|
1531
1999
|
0 Jev reported the goal complete.
|
|
1532
2000
|
1 Invalid configuration or runtime failure.
|
|
1533
|
-
2 Jev
|
|
2001
|
+
2 Jev was blocked or the wait budget timed out.
|
|
1534
2002
|
3 The maximum browser-step budget was exhausted.
|
|
1535
2003
|
|
|
1536
2004
|
Examples:
|
|
@@ -1613,6 +2081,7 @@ function addFieldValue(options, assignment, fromEnvironment) {
|
|
|
1613
2081
|
if (!supplied)
|
|
1614
2082
|
throw new CliError(`Environment variable is missing or empty: ${environmentName}`);
|
|
1615
2083
|
options.fieldValues[label] = supplied;
|
|
2084
|
+
options.sensitiveFieldLabels.push(label);
|
|
1616
2085
|
} else {
|
|
1617
2086
|
options.fieldValues[label] = assignment.slice(separator + 1);
|
|
1618
2087
|
}
|
|
@@ -1622,10 +2091,12 @@ function parseRunOptions(args) {
|
|
|
1622
2091
|
cdpUrl: process.env.CHROME_CDP_URL ?? DEFAULT_CDP_URL,
|
|
1623
2092
|
maxSteps: parsePositiveInteger(process.env.JEV_MAX_STEPS ?? "12", "JEV_MAX_STEPS"),
|
|
1624
2093
|
interactionPauses: 0,
|
|
2094
|
+
waitBudgetMs: 15000,
|
|
1625
2095
|
visible: enabled(process.env.JEV_BROWSER_VISIBLE),
|
|
1626
2096
|
keepOpen: enabled(process.env.JEV_BROWSER_KEEP_OPEN),
|
|
1627
2097
|
finalState: false,
|
|
1628
2098
|
fieldValues: {},
|
|
2099
|
+
sensitiveFieldLabels: [],
|
|
1629
2100
|
freshContext: enabled(process.env.JEV_BROWSER_FRESH_CONTEXT)
|
|
1630
2101
|
};
|
|
1631
2102
|
for (let index = 0;index < args.length; index++) {
|
|
@@ -1642,6 +2113,8 @@ function parseRunOptions(args) {
|
|
|
1642
2113
|
options.maxSteps = parsePositiveInteger(nextValue(args, index++, argument), argument);
|
|
1643
2114
|
else if (argument === "--interaction-pauses")
|
|
1644
2115
|
options.interactionPauses = parseNonNegativeInteger(nextValue(args, index++, argument), argument);
|
|
2116
|
+
else if (argument === "--wait-budget-ms")
|
|
2117
|
+
options.waitBudgetMs = parsePositiveInteger(nextValue(args, index++, argument), argument);
|
|
1645
2118
|
else if (argument === "--recording")
|
|
1646
2119
|
options.recordingPath = nextValue(args, index++, argument);
|
|
1647
2120
|
else if (argument === "--screenshot")
|
|
@@ -1689,17 +2162,45 @@ function parseCommonOptions(args, help) {
|
|
|
1689
2162
|
}
|
|
1690
2163
|
return options;
|
|
1691
2164
|
}
|
|
1692
|
-
function
|
|
2165
|
+
function actionEvent(entry, maxSteps) {
|
|
2166
|
+
return {
|
|
2167
|
+
type: "action",
|
|
2168
|
+
status: "executed",
|
|
2169
|
+
step: entry.step,
|
|
2170
|
+
elapsedMs: entry.executed_ms,
|
|
2171
|
+
budget: { used: entry.step, max: maxSteps, remaining: maxSteps - entry.step },
|
|
2172
|
+
page: { before: entry.from_url, after: entry.url, changed: entry.page_changed, viewport: entry.viewport },
|
|
2173
|
+
tab: { before: entry.from_target_id, after: entry.target_id },
|
|
2174
|
+
consoleErrors: entry.consoleErrors,
|
|
2175
|
+
action: {
|
|
2176
|
+
kind: entry.kind,
|
|
2177
|
+
label: entry.action,
|
|
2178
|
+
element: entry.element,
|
|
2179
|
+
...entry.kind === "fill" ? { text: entry.text, redacted: entry.redacted } : {},
|
|
2180
|
+
...entry.kind === "select" ? { optionValue: entry.value } : {},
|
|
2181
|
+
...entry.kind === "scroll" ? { deltaY: entry.delta_y, point: { x: 550, y: 650 } } : {},
|
|
2182
|
+
...entry.kind === "wait" ? { durationMs: 100 } : {}
|
|
2183
|
+
}
|
|
2184
|
+
};
|
|
2185
|
+
}
|
|
2186
|
+
function semanticState(page, elements) {
|
|
2187
|
+
const frameTree = (parentId) => page.frames.filter((frame) => frame.parentId === parentId).map((frame) => ({ ...frame, children: frameTree(frame.id) }));
|
|
1693
2188
|
return {
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
2189
|
+
url: page.url,
|
|
2190
|
+
title: page.title,
|
|
2191
|
+
text: page.text,
|
|
2192
|
+
viewport: { width: page.w, height: page.h },
|
|
2193
|
+
scroll: page.scroll,
|
|
2194
|
+
elements,
|
|
2195
|
+
frameTree: frameTree(null),
|
|
2196
|
+
transitions: page.transitions,
|
|
2197
|
+
omittedActions: page.omitted_actions
|
|
1698
2198
|
};
|
|
1699
2199
|
}
|
|
1700
2200
|
async function runGoal(args) {
|
|
1701
2201
|
const options = parseRunOptions(args);
|
|
1702
2202
|
let agent;
|
|
2203
|
+
let reportedActions = 0;
|
|
1703
2204
|
try {
|
|
1704
2205
|
agent = await Agent.create({
|
|
1705
2206
|
url: options.url,
|
|
@@ -1708,50 +2209,65 @@ async function runGoal(args) {
|
|
|
1708
2209
|
cdpUrl: options.cdpUrl,
|
|
1709
2210
|
maxSteps: options.maxSteps,
|
|
1710
2211
|
interactionPauses: options.interactionPauses,
|
|
2212
|
+
waitBudgetMs: options.waitBudgetMs,
|
|
1711
2213
|
visible: options.visible,
|
|
1712
2214
|
keepOpen: options.keepOpen,
|
|
1713
2215
|
recordingPath: options.recordingPath,
|
|
1714
2216
|
screenshotPath: options.screenshotPath,
|
|
1715
2217
|
fieldValues: options.fieldValues,
|
|
2218
|
+
sensitiveFieldLabels: options.sensitiveFieldLabels,
|
|
1716
2219
|
freshContext: options.freshContext
|
|
1717
2220
|
});
|
|
1718
|
-
|
|
1719
|
-
const result = await agent.run((state) => {
|
|
2221
|
+
await agent.run((state) => {
|
|
1720
2222
|
const action = state.history.length > reportedActions ? state.history.at(-1) : undefined;
|
|
1721
|
-
const decision = state.decisions.at(-1);
|
|
1722
2223
|
reportedActions = state.history.length;
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
const helper = action?.text_helper ? ` text=${action.text_helper}:${action.text_latency_ms}ms` : "";
|
|
1726
|
-
console.error(`elapsed=${state.elapsedMs}ms actions=${state.history.length}/${state.maxSteps} status=${state.status} operation=${operation} jev=${jevLatency ?? 0}ms${helper}`);
|
|
2224
|
+
if (action)
|
|
2225
|
+
console.log(JSON.stringify(actionEvent(action, state.maxSteps)));
|
|
1727
2226
|
});
|
|
2227
|
+
await agent.close();
|
|
2228
|
+
const result = agent.snapshot();
|
|
1728
2229
|
console.log(JSON.stringify({
|
|
2230
|
+
type: "result",
|
|
1729
2231
|
status: result.status,
|
|
1730
2232
|
targetId: agent.targetId,
|
|
1731
2233
|
url: result.page.url,
|
|
1732
2234
|
actions: result.history.length,
|
|
1733
2235
|
maxSteps: result.maxSteps,
|
|
2236
|
+
budget: { used: result.history.length, max: result.maxSteps, remaining: result.maxSteps - result.history.length },
|
|
1734
2237
|
elapsedMs: result.elapsedMs,
|
|
1735
2238
|
textCalls: result.textCalls.length,
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
},
|
|
2239
|
+
initialConsoleErrors: result.initialConsoleErrors,
|
|
2240
|
+
consoleErrors: result.consoleErrors,
|
|
2241
|
+
...result.waitTimeout ? { waitTimeout: result.waitTimeout } : {},
|
|
1740
2242
|
...options.recordingPath ? { recording: options.recordingPath } : {},
|
|
1741
2243
|
...options.screenshotPath ? { screenshot: options.screenshotPath } : {},
|
|
1742
|
-
...options.finalState ? {
|
|
1743
|
-
finalState:
|
|
1744
|
-
url: result.page.url,
|
|
1745
|
-
title: result.page.title,
|
|
1746
|
-
text: result.page.text,
|
|
1747
|
-
viewport: { width: result.page.w, height: result.page.h },
|
|
1748
|
-
scroll: result.page.scroll,
|
|
1749
|
-
elements: result.elements,
|
|
1750
|
-
omittedActions: result.page.omitted_actions
|
|
1751
|
-
}
|
|
2244
|
+
...options.finalState || result.status === "wait_timeout" ? {
|
|
2245
|
+
finalState: semanticState(result.page, result.elements)
|
|
1752
2246
|
} : {}
|
|
1753
2247
|
}));
|
|
1754
2248
|
return result.status === "done" ? 0 : result.status === "budget_exhausted" ? 3 : 2;
|
|
2249
|
+
} catch (error) {
|
|
2250
|
+
const state = agent?.snapshot();
|
|
2251
|
+
for (const action of state?.history.slice(reportedActions) ?? []) {
|
|
2252
|
+
console.log(JSON.stringify(actionEvent(action, options.maxSteps)));
|
|
2253
|
+
}
|
|
2254
|
+
console.log(JSON.stringify({
|
|
2255
|
+
type: "result",
|
|
2256
|
+
status: error instanceof WaitTimeoutError ? "wait_timeout" : "error",
|
|
2257
|
+
targetId: agent?.targetId ?? null,
|
|
2258
|
+
url: state?.page.url ?? (error instanceof WaitTimeoutError ? error.state?.url ?? null : null),
|
|
2259
|
+
actions: state?.history.length ?? 0,
|
|
2260
|
+
maxSteps: options.maxSteps,
|
|
2261
|
+
budget: { used: state?.history.length ?? 0, max: options.maxSteps, remaining: options.maxSteps - (state?.history.length ?? 0) },
|
|
2262
|
+
elapsedMs: state?.elapsedMs ?? 0,
|
|
2263
|
+
initialConsoleErrors: state?.initialConsoleErrors ?? [],
|
|
2264
|
+
consoleErrors: state?.consoleErrors ?? [],
|
|
2265
|
+
...error instanceof WaitTimeoutError ? { waitTimeout: { elapsedMs: error.elapsedMs, pendingCondition: error.pendingCondition } } : {},
|
|
2266
|
+
...state ? { finalState: semanticState(state.page, state.elements) } : error instanceof WaitTimeoutError && error.state ? { finalState: semanticState(error.state, actionSpace(error.state.actions).elements) } : {}
|
|
2267
|
+
}));
|
|
2268
|
+
if (error instanceof WaitTimeoutError)
|
|
2269
|
+
return 2;
|
|
2270
|
+
throw error;
|
|
1755
2271
|
} finally {
|
|
1756
2272
|
await agent?.close();
|
|
1757
2273
|
}
|
|
@@ -1859,5 +2375,6 @@ async function main(args = Bun.argv.slice(2)) {
|
|
|
1859
2375
|
if (import.meta.main)
|
|
1860
2376
|
process.exitCode = await main();
|
|
1861
2377
|
export {
|
|
2378
|
+
actionEvent,
|
|
1862
2379
|
main
|
|
1863
2380
|
};
|