@stablyai/playwright-base 0.1.9 → 0.2.0-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -5,6 +5,9 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
5
5
  throw Error('Dynamic require of "' + x + '" is not supported');
6
6
  });
7
7
 
8
+ // src/expect.ts
9
+ import { test } from "@stablyai/internal-playwright-test";
10
+
8
11
  // src/runtime.ts
9
12
  var configuredApiKey = process.env.STABLY_API_KEY;
10
13
  function setApiKey(apiKey) {
@@ -31,108 +34,65 @@ var isObject = (value) => {
31
34
  // src/ai/metadata.ts
32
35
  var SDK_METADATA_HEADERS = {
33
36
  "X-Client-Name": "stably-playwright-sdk-js",
34
- "X-Client-Version": "0.1.9"
37
+ "X-Client-Version": "0.2.0-next.1"
35
38
  };
36
39
 
37
- // src/ai/extract.ts
38
- var EXTRACT_ENDPOINT = "https://api.stably.ai/internal/v2/extract";
39
- var zodV4 = (() => {
40
- try {
41
- return __require("zod/v4/core");
42
- } catch {
43
- return void 0;
44
- }
45
- })();
46
- var isExtractionResponse = (value) => {
40
+ // src/ai/verify-prompt.ts
41
+ var PROMPT_ASSERTION_ENDPOINT = "https://api.stably.ai/internal/v1/assert";
42
+ var parseSuccessResponse = (value) => {
47
43
  if (!isObject(value)) {
48
- return false;
44
+ throw new Error("Verify prompt returned unexpected response shape");
49
45
  }
50
- if (value.success === true) {
51
- return "value" in value;
46
+ const { reason, success } = value;
47
+ if (typeof success !== "boolean") {
48
+ throw new Error("Verify prompt returned unexpected response shape");
52
49
  }
53
- return value.success === false && typeof value.error === "string";
54
- };
55
- var isErrorResponse = (value) => {
56
- return isObject(value) && typeof value.error === "string";
57
- };
58
- var ExtractValidationError = class extends Error {
59
- constructor(message, issues) {
60
- super(message);
61
- this.issues = issues;
62
- this.name = "ExtractValidationError";
50
+ if (reason !== void 0 && typeof reason !== "string") {
51
+ throw new Error("Verify prompt returned unexpected response shape");
63
52
  }
53
+ return {
54
+ reason,
55
+ success
56
+ };
64
57
  };
65
- async function validateWithSchema(schema, value) {
66
- const result = await schema.safeParseAsync(value);
67
- if (!result.success) {
68
- throw new ExtractValidationError("Validation failed", result.error.issues);
58
+ var parseErrorResponse = (value) => {
59
+ if (!isObject(value)) {
60
+ return void 0;
69
61
  }
70
- return result.data;
71
- }
72
- async function extract({
62
+ const { error } = value;
63
+ return typeof error !== "string" ? void 0 : { error };
64
+ };
65
+ async function verifyPrompt({
73
66
  prompt,
74
- pageOrLocator,
75
- schema
67
+ screenshot
76
68
  }) {
77
- if (schema && !zodV4) {
78
- throw new Error(
79
- "Schema support requires installing zod@4. Please add it to enable schemas."
80
- );
81
- }
82
- const jsonSchema = schema && zodV4 ? zodV4?.toJSONSchema(
83
- schema
84
- ) : void 0;
85
69
  const apiKey = requireApiKey();
86
70
  const form = new FormData();
87
71
  form.append("prompt", prompt);
88
- if (jsonSchema) {
89
- form.append("jsonSchema", JSON.stringify(jsonSchema));
90
- }
91
- const pngBuffer = await pageOrLocator.screenshot({ type: "png" });
92
- const u8 = Uint8Array.from(pngBuffer);
72
+ const u8 = Uint8Array.from(screenshot);
93
73
  const blob = new Blob([u8], { type: "image/png" });
94
74
  form.append("image", blob, "screenshot.png");
95
- const response = await fetch(EXTRACT_ENDPOINT, {
96
- method: "POST",
75
+ const response = await fetch(PROMPT_ASSERTION_ENDPOINT, {
76
+ body: form,
97
77
  headers: {
98
78
  ...SDK_METADATA_HEADERS,
99
79
  Authorization: `Bearer ${apiKey}`
100
80
  },
101
- body: form
81
+ method: "POST"
102
82
  });
103
- const raw = await response.json().catch(() => void 0);
83
+ const parsed = await response.json().catch(() => void 0);
104
84
  if (response.ok) {
105
- if (!isExtractionResponse(raw)) {
106
- throw new Error("Extract returned unexpected response shape");
107
- }
108
- if (raw.success === false) {
109
- return raw.error;
110
- }
111
- const { value } = raw;
112
- return schema ? await validateWithSchema(schema, value) : typeof value === "string" ? value : JSON.stringify(value);
85
+ const { reason, success } = parseSuccessResponse(parsed);
86
+ return {
87
+ pass: success,
88
+ reason
89
+ };
113
90
  }
114
- throw new Error(isErrorResponse(raw) ? raw.error : "Extract failed");
115
- }
116
-
117
- // src/playwright-augment/methods/extract.ts
118
- function createExtract(pageOrLocator) {
119
- const impl = async (prompt, options) => {
120
- if (options?.schema) {
121
- return extract({
122
- prompt,
123
- schema: options.schema,
124
- pageOrLocator
125
- });
126
- }
127
- return extract({ prompt, pageOrLocator });
128
- };
129
- return impl;
91
+ const err = parseErrorResponse(parsed);
92
+ throw new Error(
93
+ `Verify prompt failed (${response.status})${err ? `: ${err.error}` : ""}`
94
+ );
130
95
  }
131
- var createLocatorExtract = (locator) => createExtract(locator);
132
- var createPageExtract = (page) => createExtract(page);
133
-
134
- // src/playwright-augment/methods/agent.ts
135
- import { test } from "@stablyai/internal-playwright-test";
136
96
 
137
97
  // src/playwright-type-predicates.ts
138
98
  function isPage(candidate) {
@@ -142,6 +102,9 @@ function isLocator(candidate) {
142
102
  return typeof candidate === "object" && candidate !== null && typeof candidate.screenshot === "function" && typeof candidate.nth === "function";
143
103
  }
144
104
 
105
+ // src/image-compare.ts
106
+ import * as jpeg from "jpeg-js";
107
+
145
108
  // ../../node_modules/.pnpm/pixelmatch@7.1.0/node_modules/pixelmatch/index.js
146
109
  function pixelmatch(img1, img2, output, width, height, options = {}) {
147
110
  const {
@@ -300,7 +263,6 @@ function drawGrayPixel(img, i, alpha, output) {
300
263
 
301
264
  // src/image-compare.ts
302
265
  import { PNG } from "pngjs";
303
- import * as jpeg from "jpeg-js";
304
266
  var isPng = (buffer) => {
305
267
  return buffer.length >= 8 && buffer[0] === 137 && buffer[1] === 80 && buffer[2] === 78 && buffer[3] === 71 && buffer[4] === 13 && buffer[5] === 10 && buffer[6] === 26 && buffer[7] === 10;
306
268
  };
@@ -310,14 +272,14 @@ var isJpeg = (buffer) => {
310
272
  var decodeImage = (buffer) => {
311
273
  if (isPng(buffer)) {
312
274
  const png2 = PNG.sync.read(buffer);
313
- return { data: png2.data, width: png2.width, height: png2.height };
275
+ return { data: png2.data, height: png2.height, width: png2.width };
314
276
  }
315
277
  if (isJpeg(buffer)) {
316
278
  const img = jpeg.decode(buffer, { maxMemoryUsageInMB: 1024 });
317
- return { data: img.data, width: img.width, height: img.height };
279
+ return { data: img.data, height: img.height, width: img.width };
318
280
  }
319
281
  const png = PNG.sync.read(buffer);
320
- return { data: png.data, width: png.width, height: png.height };
282
+ return { data: png.data, height: png.height, width: png.width };
321
283
  };
322
284
  var imagesAreSimilar = ({
323
285
  image1,
@@ -369,7 +331,9 @@ async function takeStableScreenshot(target, options) {
369
331
  return await target.screenshot(options);
370
332
  };
371
333
  while (true) {
372
- if (Date.now() >= deadline) break;
334
+ if (Date.now() >= deadline) {
335
+ break;
336
+ }
373
337
  const delay = pollIntervals.length ? pollIntervals.shift() : 1e3;
374
338
  if (delay) {
375
339
  await page.waitForTimeout(delay);
@@ -388,49 +352,279 @@ async function takeStableScreenshot(target, options) {
388
352
  return actual ?? await safeScreenshot();
389
353
  }
390
354
 
355
+ // src/expect.ts
356
+ function createFailureMessage({
357
+ condition,
358
+ didPass,
359
+ isNot,
360
+ reason,
361
+ targetType
362
+ }) {
363
+ const expectation = isNot ? "not to satisfy" : "to satisfy";
364
+ const result = didPass ? "it did" : "it did not";
365
+ let message = `Expected ${targetType} ${expectation} ${JSON.stringify(condition)}, but ${result}.`;
366
+ if (reason) {
367
+ message += `
368
+
369
+ Reason: ${reason}`;
370
+ }
371
+ return message;
372
+ }
373
+ var stablyPlaywrightMatchers = {
374
+ async toMatchScreenshotPrompt(received, condition, options) {
375
+ const target = isPage(received) ? received : isLocator(received) ? received : void 0;
376
+ if (!target) {
377
+ throw new Error(
378
+ "toMatchScreenshotPrompt only supports Playwright Page and Locator instances."
379
+ );
380
+ }
381
+ const targetType = isPage(target) ? "page" : "locator";
382
+ const screenshot = await takeStableScreenshot(target, options);
383
+ const verifyResult = await verifyPrompt({ prompt: condition, screenshot });
384
+ const testInfo = test.info();
385
+ testInfo.attachments.push({
386
+ body: Buffer.from(
387
+ JSON.stringify(
388
+ {
389
+ pass: verifyResult.pass,
390
+ prompt: condition,
391
+ reasoning: verifyResult.reason
392
+ },
393
+ null,
394
+ 2
395
+ ),
396
+ "utf-8"
397
+ ),
398
+ contentType: "application/json",
399
+ name: "toMatchScreenshotPrompt-reasoning"
400
+ });
401
+ return {
402
+ message: () => createFailureMessage({
403
+ condition,
404
+ didPass: verifyResult.pass,
405
+ isNot: this.isNot,
406
+ reason: verifyResult.reason,
407
+ targetType
408
+ }),
409
+ name: "toMatchScreenshotPrompt",
410
+ pass: verifyResult.pass
411
+ };
412
+ }
413
+ };
414
+
415
+ // src/playwright-augment/methods/agent.ts
416
+ import { test as test2 } from "@stablyai/internal-playwright-test";
417
+
418
+ // src/utils/truncate.ts
419
+ var truncate = (inp, length) => inp.length <= length || inp.length <= 3 ? inp : `${inp.slice(0, length - 3)}...`;
420
+
391
421
  // src/playwright-augment/methods/agent/construct-payload.ts
392
422
  function constructAgentPayload({
393
- sessionId,
394
- message,
423
+ activePage,
424
+ additionalContext,
395
425
  isError,
426
+ message,
427
+ model,
396
428
  screenshot,
397
- tabManager,
398
- activePage,
399
- additionalContext
429
+ sessionId,
430
+ tabManager
400
431
  }) {
401
432
  const form = new FormData();
402
- form.append("session_id", sessionId);
403
- form.append("message", message);
433
+ const viewportSize = activePage.viewportSize();
434
+ const tabs = Array.from(tabManager.entries()).map(([page, alias]) => ({
435
+ alias,
436
+ url: page.url()
437
+ }));
438
+ const activePageAlias = tabManager.get(activePage);
439
+ const metadata = {
440
+ allPages: tabs,
441
+ message,
442
+ sessionId
443
+ };
444
+ if (model) {
445
+ metadata.model = model;
446
+ }
404
447
  if (isError) {
405
- form.append("is_error", JSON.stringify(isError));
448
+ metadata.isError = isError;
406
449
  }
407
450
  if (additionalContext) {
408
- form.append("additional_context", JSON.stringify(additionalContext));
451
+ metadata.additionalContext = additionalContext;
409
452
  }
410
- const viewportSize = activePage.viewportSize();
411
453
  if (viewportSize) {
412
- form.append("page_dimensions", JSON.stringify(viewportSize));
454
+ metadata.pageDimensions = viewportSize;
413
455
  }
456
+ if (activePageAlias) {
457
+ metadata.activePageAlias = activePageAlias;
458
+ }
459
+ const metadataBlob = new Blob([JSON.stringify(metadata)], {
460
+ type: "application/json"
461
+ });
462
+ form.append("metadata", metadataBlob, "metadata.json");
414
463
  const screenshotBytes = Uint8Array.from(screenshot);
415
464
  const screenshotBlob = new Blob([screenshotBytes], { type: "image/png" });
416
465
  form.append("screenshot", screenshotBlob, "screenshot.png");
417
- const tabs = Array.from(tabManager.entries()).map(([page, alias]) => ({
418
- alias,
419
- url: page.url()
420
- }));
421
- form.append("all_pages", JSON.stringify(tabs));
422
- const activePageAlias = tabManager.get(activePage);
423
- if (activePageAlias) {
424
- form.append("active_page_alias", activePageAlias);
425
- }
426
466
  return form;
427
467
  }
428
468
 
469
+ // src/playwright-augment/methods/agent/exec-response.ts
470
+ var DEFAULT_AGENT_WAIT_MS = 3e3;
471
+ async function execResponse({
472
+ activePage: initialActivePage,
473
+ agentResponse,
474
+ browserContext,
475
+ tabManager
476
+ }) {
477
+ let activePage = initialActivePage;
478
+ try {
479
+ switch (agentResponse.action) {
480
+ case "key": {
481
+ const { text } = agentResponse;
482
+ if (text) {
483
+ await activePage.keyboard.press(text);
484
+ return { activePage, result: { message: `pressed "${text}"` } };
485
+ }
486
+ return { activePage, result: { message: "pressed key" } };
487
+ }
488
+ case "type": {
489
+ const { text } = agentResponse;
490
+ await activePage.keyboard.type(text);
491
+ return { activePage, result: { message: `typed "${text}"` } };
492
+ }
493
+ case "mouse_move": {
494
+ const [x, y] = agentResponse.coordinate;
495
+ await activePage.mouse.move(x, y);
496
+ return { activePage, result: { message: `mouse moved completed` } };
497
+ }
498
+ case "left_click": {
499
+ const [x, y] = agentResponse.coordinate;
500
+ await activePage.mouse.click(x, y);
501
+ return { activePage, result: { message: `left click completed` } };
502
+ }
503
+ case "right_click": {
504
+ const [x, y] = agentResponse.coordinate;
505
+ await activePage.mouse.click(x, y, { button: "right" });
506
+ return { activePage, result: { message: `right click completed` } };
507
+ }
508
+ case "double_click": {
509
+ const [x, y] = agentResponse.coordinate;
510
+ await activePage.mouse.dblclick(x, y);
511
+ return { activePage, result: { message: `double click completed` } };
512
+ }
513
+ case "triple_click": {
514
+ const [x, y] = agentResponse.coordinate;
515
+ await activePage.mouse.click(x, y, { clickCount: 3 });
516
+ return { activePage, result: { message: `triple click completed` } };
517
+ }
518
+ case "left_click_drag": {
519
+ const [startX, startY] = agentResponse.start_coordinate;
520
+ const [endX, endY] = agentResponse.coordinate;
521
+ await activePage.mouse.move(startX, startY);
522
+ await activePage.mouse.down();
523
+ await activePage.mouse.move(endX, endY);
524
+ await activePage.mouse.up();
525
+ return { activePage, result: { message: `drag completed` } };
526
+ }
527
+ case "screenshot": {
528
+ await takeStableScreenshot(activePage);
529
+ return { activePage, result: { message: "captured screenshot" } };
530
+ }
531
+ case "wait": {
532
+ const waitMs = agentResponse.milliseconds ?? DEFAULT_AGENT_WAIT_MS;
533
+ await activePage.waitForTimeout(waitMs);
534
+ return { activePage, result: { message: `waited ${waitMs}ms` } };
535
+ }
536
+ case "navigate_to_url": {
537
+ await activePage.goto(agentResponse.url);
538
+ return {
539
+ activePage,
540
+ result: { message: `navigated to "${agentResponse.url}"` }
541
+ };
542
+ }
543
+ case "new_tab_url": {
544
+ const newPage = await browserContext.newPage();
545
+ await newPage.goto(agentResponse.url);
546
+ await newPage.waitForLoadState("domcontentloaded");
547
+ activePage = newPage;
548
+ return { activePage, result: { message: "opened new tab" } };
549
+ }
550
+ case "switch_tab": {
551
+ const entry = Array.from(tabManager.entries()).find(
552
+ ([, alias]) => alias === agentResponse.tab_alias
553
+ );
554
+ const page = entry?.[0];
555
+ if (!page) {
556
+ throw new Error(
557
+ `Tab with alias ${agentResponse.tab_alias} not found`
558
+ );
559
+ }
560
+ await page.bringToFront();
561
+ activePage = page;
562
+ return {
563
+ activePage,
564
+ result: { message: `switched to "${agentResponse.tab_alias}"` }
565
+ };
566
+ }
567
+ case "scroll": {
568
+ const [x, y] = agentResponse.coordinate;
569
+ await activePage.mouse.move(x, y);
570
+ let deltaX = 0;
571
+ let deltaY = 0;
572
+ switch (agentResponse.scroll_direction) {
573
+ case "up":
574
+ deltaY = -agentResponse.scroll_amount;
575
+ break;
576
+ case "down":
577
+ deltaY = agentResponse.scroll_amount;
578
+ break;
579
+ case "left":
580
+ deltaX = -agentResponse.scroll_amount;
581
+ break;
582
+ case "right":
583
+ deltaX = agentResponse.scroll_amount;
584
+ break;
585
+ }
586
+ await activePage.mouse.wheel(deltaX, deltaY);
587
+ return {
588
+ activePage,
589
+ result: { message: `scrolled ${agentResponse.scroll_direction}` }
590
+ };
591
+ }
592
+ case "navigate_back": {
593
+ const res = await activePage.goBack();
594
+ if (!res) {
595
+ throw new Error("navigate_back failed: no history entry");
596
+ }
597
+ return { activePage, result: { message: "navigated back" } };
598
+ }
599
+ case "aria_snapshot": {
600
+ const ariaSnapshot = await activePage._snapshotForAI();
601
+ return {
602
+ activePage,
603
+ result: { message: `ARIA Snapshot:
604
+ ${ariaSnapshot}` }
605
+ };
606
+ }
607
+ case "terminate_test": {
608
+ const { reason, success } = agentResponse;
609
+ return {
610
+ activePage,
611
+ finalSuccess: success,
612
+ result: { message: reason, shouldTerminate: true }
613
+ };
614
+ }
615
+ }
616
+ } catch (error) {
617
+ const message = error instanceof Error ? error.message : String(error);
618
+ return { activePage, result: { isError: true, message } };
619
+ }
620
+ }
621
+
429
622
  // src/playwright-augment/methods/agent.ts
430
- var AGENT_PATH = "internal/v1/agent";
623
+ var AGENT_PATH = "internal/v3/agent";
431
624
  var STABLY_API_URL = process.env.STABLY_API_URL || "https://api.stably.ai";
432
625
  var AGENT_ENDPOINT = new URL(AGENT_PATH, STABLY_API_URL).toString();
433
626
  function createAgentStub() {
627
+ let thoughtsIndex = 0;
434
628
  return async (prompt, options) => {
435
629
  const apiKey = requireApiKey();
436
630
  const maxCycles = options.maxCycles ?? 30;
@@ -441,8 +635,8 @@ function createAgentStub() {
441
635
  });
442
636
  let activePage = options.page;
443
637
  let agentMessage = {
444
- message: prompt,
445
638
  isError: false,
639
+ message: prompt,
446
640
  shouldTerminate: false
447
641
  };
448
642
  let finalSuccess;
@@ -460,165 +654,81 @@ function createAgentStub() {
460
654
  newPageOpenedMsg = `opened new tab ${alias} (${page.url()})`;
461
655
  };
462
656
  browserContext.on("page", onNewPage);
463
- return await test.step(prompt, async () => {
657
+ return await test2.step(`[Agent] ${prompt}`, async () => {
464
658
  try {
465
659
  for (let i = 0; i < maxCycles; i++) {
466
660
  if (agentMessage.shouldTerminate) {
467
661
  break;
468
662
  }
469
- const screenshot = await takeStableScreenshot(activePage);
470
- const response = await fetch(AGENT_ENDPOINT, {
471
- method: "POST",
472
- headers: {
473
- ...SDK_METADATA_HEADERS,
474
- Authorization: `Bearer ${apiKey}`
475
- },
476
- body: constructAgentPayload({
477
- sessionId,
478
- message: agentMessage.message,
479
- isError: agentMessage.isError,
480
- screenshot,
481
- tabManager,
482
- activePage,
483
- additionalContext: newPageOpenedMsg ? { newPageMessage: newPageOpenedMsg } : void 0
484
- })
485
- });
486
- newPageOpenedMsg = void 0;
487
- const responseJson = await response.json();
488
- if (!response.ok) {
489
- throw new Error(
490
- `Agent call failed: ${JSON.stringify(responseJson)}`
491
- );
492
- }
493
- const agentResponse = responseJson;
494
- agentMessage = await (async () => {
495
- try {
496
- switch (agentResponse.action) {
497
- case "key": {
498
- const { text } = agentResponse;
499
- if (text) {
500
- await activePage.keyboard.press(text);
501
- return { message: `pressed "${text}"` };
502
- }
503
- return { message: "pressed key" };
504
- }
505
- case "type": {
506
- const { text } = agentResponse;
507
- await activePage.keyboard.type(text);
508
- return { message: `typed "${text}"` };
509
- }
510
- case "mouse_move": {
511
- const [x, y] = agentResponse.coordinate;
512
- await activePage.mouse.move(x, y);
513
- return { message: `mouse moved to [${x}, ${y}]` };
514
- }
515
- case "left_click": {
516
- const [x, y] = agentResponse.coordinate;
517
- await activePage.mouse.click(x, y);
518
- return { message: `left click at [${x}, ${y}]` };
519
- }
520
- case "right_click": {
521
- const [x, y] = agentResponse.coordinate;
522
- await activePage.mouse.click(x, y, { button: "right" });
523
- return { message: `right click at [${x}, ${y}]` };
524
- }
525
- case "double_click": {
526
- const [x, y] = agentResponse.coordinate;
527
- await activePage.mouse.dblclick(x, y);
528
- return { message: `double click at [${x}, ${y}]` };
529
- }
530
- case "triple_click": {
531
- const [x, y] = agentResponse.coordinate;
532
- await activePage.mouse.click(x, y, { clickCount: 3 });
533
- return { message: `triple click at [${x}, ${y}]` };
534
- }
535
- case "left_click_drag": {
536
- const [startX, startY] = agentResponse.start_coordinate;
537
- const [endX, endY] = agentResponse.coordinate;
538
- await activePage.mouse.move(startX, startY);
539
- await activePage.mouse.down();
540
- await activePage.mouse.move(endX, endY);
541
- await activePage.mouse.up();
542
- return {
543
- message: `dragged from [${startX}, ${startY}] to [${endX}, ${endY}]`
544
- };
545
- }
546
- case "screenshot": {
547
- await takeStableScreenshot(activePage);
548
- return { message: "captured screenshot" };
549
- }
550
- case "wait": {
551
- const waitMs = agentResponse.milliseconds ?? 3e3;
552
- await activePage.waitForTimeout(waitMs);
553
- return { message: `waited ${waitMs}ms` };
554
- }
555
- case "navigate_to_url": {
556
- await activePage.goto(agentResponse.url);
557
- return { message: `navigated to "${agentResponse.url}"` };
558
- }
559
- case "new_tab_url": {
560
- const newPage = await browserContext.newPage();
561
- await newPage.goto(agentResponse.url);
562
- await newPage.waitForLoadState("domcontentloaded");
563
- return { message: "opened new tab" };
564
- }
565
- case "switch_tab": {
566
- const entry = Array.from(tabManager.entries()).find(
567
- ([, alias]) => alias === agentResponse.tab_alias
568
- );
569
- const page = entry?.[0];
570
- if (!page) {
571
- throw new Error(
572
- `Tab with alias ${agentResponse.tab_alias} not found`
573
- );
574
- }
575
- await page.bringToFront();
576
- activePage = page;
577
- return {
578
- message: `switched to "${agentResponse.tab_alias}"`
579
- };
580
- }
581
- case "scroll": {
582
- const [x, y] = agentResponse.coordinate;
583
- await activePage.mouse.move(x, y);
584
- let deltaX = 0;
585
- let deltaY = 0;
586
- switch (agentResponse.scroll_direction) {
587
- case "up":
588
- deltaY = -agentResponse.scroll_amount;
589
- break;
590
- case "down":
591
- deltaY = agentResponse.scroll_amount;
592
- break;
593
- case "left":
594
- deltaX = -agentResponse.scroll_amount;
595
- break;
596
- case "right":
597
- deltaX = agentResponse.scroll_amount;
598
- break;
599
- }
600
- await activePage.mouse.wheel(deltaX, deltaY);
601
- return {
602
- message: `scrolled ${agentResponse.scroll_direction}`
603
- };
604
- }
605
- case "navigate_back": {
606
- const res = await activePage.goBack();
607
- if (!res)
608
- throw new Error("navigate_back failed: no history entry");
609
- return { message: "navigated back" };
610
- }
611
- case "terminate_test": {
612
- const { success, reason } = agentResponse;
613
- finalSuccess = success;
614
- return { message: reason, shouldTerminate: true };
615
- }
663
+ const agentResponses = await test2.step(`[Thinking ${thoughtsIndex + 1}]`, async (stepInfo) => {
664
+ const screenshot = await takeStableScreenshot(activePage);
665
+ const response = await fetch(AGENT_ENDPOINT, {
666
+ body: constructAgentPayload({
667
+ activePage,
668
+ additionalContext: newPageOpenedMsg ? { newPageMessage: newPageOpenedMsg } : void 0,
669
+ isError: agentMessage.isError,
670
+ message: agentMessage.message,
671
+ model: options.model,
672
+ screenshot,
673
+ sessionId,
674
+ tabManager
675
+ }),
676
+ headers: {
677
+ ...SDK_METADATA_HEADERS,
678
+ Authorization: `Bearer ${apiKey}`
679
+ },
680
+ method: "POST"
681
+ });
682
+ newPageOpenedMsg = void 0;
683
+ const responseJson = await response.json();
684
+ if (!response.ok) {
685
+ throw new Error(
686
+ `Agent call failed: ${JSON.stringify(responseJson)}`
687
+ );
688
+ }
689
+ const reasoningTexts = responseJson.map((r) => r.content).filter((content) => content !== void 0);
690
+ const reasoningText = reasoningTexts.join("\n\n");
691
+ const truncatedReasoningText = truncate(reasoningText, 120);
692
+ await stepInfo.attach(
693
+ `[Thinking ${thoughtsIndex + 1}] ${truncatedReasoningText}`,
694
+ {
695
+ body: reasoningText,
696
+ contentType: "text/plain"
616
697
  }
617
- } catch (error) {
618
- const message = error instanceof Error ? error.message : String(error);
619
- return { message, isError: true };
698
+ );
699
+ thoughtsIndex++;
700
+ return responseJson;
701
+ });
702
+ let combinedMessages = [];
703
+ let aggregatedIsError = false;
704
+ let aggregatedShouldTerminate = false;
705
+ for (const agentResponse of agentResponses) {
706
+ const {
707
+ activePage: newActivePage,
708
+ finalSuccess: maybeFinal,
709
+ result
710
+ } = await execResponse({
711
+ activePage,
712
+ agentResponse,
713
+ browserContext,
714
+ tabManager
715
+ });
716
+ activePage = newActivePage;
717
+ combinedMessages.push(result.message);
718
+ finalSuccess = maybeFinal ?? finalSuccess;
719
+ if (result.isError) {
720
+ aggregatedIsError = true;
721
+ }
722
+ if (result.shouldTerminate) {
723
+ aggregatedShouldTerminate = true;
724
+ break;
620
725
  }
621
- })();
726
+ }
727
+ agentMessage = {
728
+ isError: aggregatedIsError,
729
+ message: combinedMessages.join("\n"),
730
+ shouldTerminate: aggregatedShouldTerminate
731
+ };
622
732
  }
623
733
  } finally {
624
734
  browserContext.off("page", onNewPage);
@@ -628,6 +738,106 @@ function createAgentStub() {
628
738
  };
629
739
  }
630
740
 
741
+ // src/ai/extract.ts
742
+ var EXTRACT_PATH = "internal/v2/extract";
743
+ var STABLY_API_URL2 = process.env.STABLY_API_URL || "https://api.stably.ai";
744
+ var EXTRACT_ENDPOINT = new URL(EXTRACT_PATH, STABLY_API_URL2).toString();
745
+ var zodV4 = (() => {
746
+ try {
747
+ return __require("zod/v4/core");
748
+ } catch {
749
+ return void 0;
750
+ }
751
+ })();
752
+ var isExtractionResponse = (value) => {
753
+ if (!isObject(value)) {
754
+ return false;
755
+ }
756
+ if (value.success === true) {
757
+ return "value" in value;
758
+ }
759
+ return value.success === false && typeof value.error === "string";
760
+ };
761
+ var isErrorResponse = (value) => {
762
+ return isObject(value) && typeof value.error === "string";
763
+ };
764
+ var ExtractValidationError = class extends Error {
765
+ constructor(message, issues) {
766
+ super(message);
767
+ this.issues = issues;
768
+ this.name = "ExtractValidationError";
769
+ }
770
+ };
771
+ async function validateWithSchema(schema, value) {
772
+ const result = await schema.safeParseAsync(value);
773
+ if (!result.success) {
774
+ throw new ExtractValidationError("Validation failed", result.error.issues);
775
+ }
776
+ return result.data;
777
+ }
778
+ async function extract({
779
+ pageOrLocator,
780
+ prompt,
781
+ schema
782
+ }) {
783
+ if (schema && !zodV4) {
784
+ throw new Error(
785
+ "Schema support requires installing zod@4. Please add it to enable schemas."
786
+ );
787
+ }
788
+ const jsonSchema = schema && zodV4 ? zodV4?.toJSONSchema(
789
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
790
+ schema
791
+ ) : void 0;
792
+ const apiKey = requireApiKey();
793
+ const form = new FormData();
794
+ form.append("prompt", prompt);
795
+ if (jsonSchema) {
796
+ form.append("jsonSchema", JSON.stringify(jsonSchema));
797
+ }
798
+ const pngBuffer = await pageOrLocator.screenshot({ type: "png" });
799
+ const u8 = Uint8Array.from(pngBuffer);
800
+ const blob = new Blob([u8], { type: "image/png" });
801
+ form.append("image", blob, "screenshot.png");
802
+ const response = await fetch(EXTRACT_ENDPOINT, {
803
+ body: form,
804
+ headers: {
805
+ ...SDK_METADATA_HEADERS,
806
+ Authorization: `Bearer ${apiKey}`
807
+ },
808
+ method: "POST"
809
+ });
810
+ const raw = await response.json().catch(() => void 0);
811
+ if (response.ok) {
812
+ if (!isExtractionResponse(raw)) {
813
+ throw new Error("Extract returned unexpected response shape");
814
+ }
815
+ if (!raw.success) {
816
+ throw new Error(`Extract failed: ${raw.error}`);
817
+ }
818
+ const { value } = raw;
819
+ return schema ? await validateWithSchema(schema, value) : typeof value === "string" ? value : JSON.stringify(value);
820
+ }
821
+ throw new Error(isErrorResponse(raw) ? raw.error : "Extract failed");
822
+ }
823
+
824
+ // src/playwright-augment/methods/extract.ts
825
+ function createExtract(pageOrLocator) {
826
+ const impl = async (prompt, options) => {
827
+ if (options?.schema) {
828
+ return extract({
829
+ pageOrLocator,
830
+ prompt,
831
+ schema: options.schema
832
+ });
833
+ }
834
+ return extract({ pageOrLocator, prompt });
835
+ };
836
+ return impl;
837
+ }
838
+ var createLocatorExtract = (locator) => createExtract(locator);
839
+ var createPageExtract = (page) => createExtract(page);
840
+
631
841
  // src/playwright-augment/augment.ts
632
842
  var LOCATOR_PATCHED = Symbol.for("stably.playwright.locatorPatched");
633
843
  var LOCATOR_DESCRIBE_WRAPPED = Symbol.for(
@@ -639,9 +849,9 @@ var BROWSER_PATCHED = Symbol.for("stably.playwright.browserPatched");
639
849
  var BROWSER_TYPE_PATCHED = Symbol.for("stably.playwright.browserTypePatched");
640
850
  function defineHiddenProperty(target, key, value) {
641
851
  Object.defineProperty(target, key, {
642
- value,
643
- enumerable: false,
644
852
  configurable: true,
853
+ enumerable: false,
854
+ value,
645
855
  writable: true
646
856
  });
647
857
  }
@@ -688,6 +898,7 @@ function augmentBrowserContext(context) {
688
898
  if (originalPages) {
689
899
  context.pages = () => originalPages().map(
690
900
  (page) => augmentPage(page)
901
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
691
902
  );
692
903
  }
693
904
  if (!context.agent) {
@@ -713,6 +924,7 @@ function augmentBrowser(browser) {
713
924
  const originalContexts = browser.contexts.bind(browser);
714
925
  browser.contexts = () => originalContexts().map(
715
926
  (context) => augmentBrowserContext(context)
927
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
716
928
  );
717
929
  if (!browser.agent) {
718
930
  defineHiddenProperty(browser, "agent", createAgentStub());
@@ -736,14 +948,19 @@ function augmentBrowserType(browserType) {
736
948
  return augmentBrowser(browser);
737
949
  };
738
950
  }
739
- const originalConnectOverCDP = browserType.connectOverCDP?.bind(browserType);
951
+ const originalConnectOverCDP = browserType.connectOverCDP?.bind(
952
+ browserType
953
+ );
740
954
  if (originalConnectOverCDP) {
741
955
  browserType.connectOverCDP = async (...args) => {
742
956
  const browser = await originalConnectOverCDP(...args);
743
957
  return augmentBrowser(browser);
744
958
  };
745
959
  }
746
- const originalLaunchPersistentContext = browserType.launchPersistentContext?.bind(browserType);
960
+ const originalLaunchPersistentContext = (
961
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
962
+ browserType.launchPersistentContext?.bind(browserType)
963
+ );
747
964
  if (originalLaunchPersistentContext) {
748
965
  browserType.launchPersistentContext = async (...args) => {
749
966
  const context = await originalLaunchPersistentContext(...args);
@@ -753,105 +970,6 @@ function augmentBrowserType(browserType) {
753
970
  defineHiddenProperty(browserType, BROWSER_TYPE_PATCHED, true);
754
971
  return browserType;
755
972
  }
756
-
757
- // src/ai/verify-prompt.ts
758
- var PROMPT_ASSERTION_ENDPOINT = "https://api.stably.ai/internal/v1/assert";
759
- var parseSuccessResponse = (value) => {
760
- if (!isObject(value)) {
761
- throw new Error("Verify prompt returned unexpected response shape");
762
- }
763
- const { success, reason } = value;
764
- if (typeof success !== "boolean") {
765
- throw new Error("Verify prompt returned unexpected response shape");
766
- }
767
- if (reason !== void 0 && typeof reason !== "string") {
768
- throw new Error("Verify prompt returned unexpected response shape");
769
- }
770
- return {
771
- success,
772
- reason
773
- };
774
- };
775
- var parseErrorResponse = (value) => {
776
- if (!isObject(value)) {
777
- return void 0;
778
- }
779
- const { error } = value;
780
- return typeof error !== "string" ? void 0 : { error };
781
- };
782
- async function verifyPrompt({
783
- prompt,
784
- screenshot
785
- }) {
786
- const apiKey = requireApiKey();
787
- const form = new FormData();
788
- form.append("prompt", prompt);
789
- const u8 = Uint8Array.from(screenshot);
790
- const blob = new Blob([u8], { type: "image/png" });
791
- form.append("image", blob, "screenshot.png");
792
- const response = await fetch(PROMPT_ASSERTION_ENDPOINT, {
793
- method: "POST",
794
- headers: {
795
- ...SDK_METADATA_HEADERS,
796
- Authorization: `Bearer ${apiKey}`
797
- },
798
- body: form
799
- });
800
- const parsed = await response.json().catch(() => void 0);
801
- if (response.ok) {
802
- const { success, reason } = parseSuccessResponse(parsed);
803
- return {
804
- pass: success,
805
- reason
806
- };
807
- }
808
- const err = parseErrorResponse(parsed);
809
- throw new Error(
810
- `Verify prompt failed (${response.status})${err ? `: ${err.error}` : ""}`
811
- );
812
- }
813
-
814
- // src/expect.ts
815
- function createFailureMessage({
816
- targetType,
817
- condition,
818
- didPass,
819
- isNot,
820
- reason
821
- }) {
822
- const expectation = isNot ? "not to satisfy" : "to satisfy";
823
- const result = didPass ? "it did" : "it did not";
824
- let message = `Expected ${targetType} ${expectation} ${JSON.stringify(condition)}, but ${result}.`;
825
- if (reason) {
826
- message += `
827
-
828
- Reason: ${reason}`;
829
- }
830
- return message;
831
- }
832
- var stablyPlaywrightMatchers = {
833
- async toMatchScreenshotPrompt(received, condition, options) {
834
- const target = isPage(received) ? received : isLocator(received) ? received : void 0;
835
- if (!target) {
836
- throw new Error(
837
- "toMatchScreenshotPrompt only supports Playwright Page and Locator instances."
838
- );
839
- }
840
- const targetType = isPage(target) ? "page" : "locator";
841
- const screenshot = await takeStableScreenshot(target, options);
842
- const verifyResult = await verifyPrompt({ prompt: condition, screenshot });
843
- return {
844
- pass: verifyResult.pass,
845
- message: () => createFailureMessage({
846
- targetType,
847
- condition,
848
- didPass: verifyResult.pass,
849
- reason: verifyResult.reason,
850
- isNot: this.isNot
851
- })
852
- };
853
- }
854
- };
855
973
  export {
856
974
  augmentBrowser,
857
975
  augmentBrowserContext,