@stablyai/playwright-base 0.1.10 → 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.cjs CHANGED
@@ -41,6 +41,9 @@ __export(index_exports, {
41
41
  });
42
42
  module.exports = __toCommonJS(index_exports);
43
43
 
44
+ // src/expect.ts
45
+ var import_internal_playwright_test = require("@stablyai/internal-playwright-test");
46
+
44
47
  // src/runtime.ts
45
48
  var configuredApiKey = process.env.STABLY_API_KEY;
46
49
  function setApiKey(apiKey) {
@@ -67,108 +70,65 @@ var isObject = (value) => {
67
70
  // src/ai/metadata.ts
68
71
  var SDK_METADATA_HEADERS = {
69
72
  "X-Client-Name": "stably-playwright-sdk-js",
70
- "X-Client-Version": "0.1.10"
73
+ "X-Client-Version": "0.2.0-next.1"
71
74
  };
72
75
 
73
- // src/ai/extract.ts
74
- var EXTRACT_ENDPOINT = "https://api.stably.ai/internal/v2/extract";
75
- var zodV4 = (() => {
76
- try {
77
- return require("zod/v4/core");
78
- } catch {
79
- return void 0;
80
- }
81
- })();
82
- var isExtractionResponse = (value) => {
76
+ // src/ai/verify-prompt.ts
77
+ var PROMPT_ASSERTION_ENDPOINT = "https://api.stably.ai/internal/v1/assert";
78
+ var parseSuccessResponse = (value) => {
83
79
  if (!isObject(value)) {
84
- return false;
80
+ throw new Error("Verify prompt returned unexpected response shape");
85
81
  }
86
- if (value.success === true) {
87
- return "value" in value;
82
+ const { reason, success } = value;
83
+ if (typeof success !== "boolean") {
84
+ throw new Error("Verify prompt returned unexpected response shape");
88
85
  }
89
- return value.success === false && typeof value.error === "string";
90
- };
91
- var isErrorResponse = (value) => {
92
- return isObject(value) && typeof value.error === "string";
93
- };
94
- var ExtractValidationError = class extends Error {
95
- constructor(message, issues) {
96
- super(message);
97
- this.issues = issues;
98
- this.name = "ExtractValidationError";
86
+ if (reason !== void 0 && typeof reason !== "string") {
87
+ throw new Error("Verify prompt returned unexpected response shape");
99
88
  }
89
+ return {
90
+ reason,
91
+ success
92
+ };
100
93
  };
101
- async function validateWithSchema(schema, value) {
102
- const result = await schema.safeParseAsync(value);
103
- if (!result.success) {
104
- throw new ExtractValidationError("Validation failed", result.error.issues);
94
+ var parseErrorResponse = (value) => {
95
+ if (!isObject(value)) {
96
+ return void 0;
105
97
  }
106
- return result.data;
107
- }
108
- async function extract({
98
+ const { error } = value;
99
+ return typeof error !== "string" ? void 0 : { error };
100
+ };
101
+ async function verifyPrompt({
109
102
  prompt,
110
- pageOrLocator,
111
- schema
103
+ screenshot
112
104
  }) {
113
- if (schema && !zodV4) {
114
- throw new Error(
115
- "Schema support requires installing zod@4. Please add it to enable schemas."
116
- );
117
- }
118
- const jsonSchema = schema && zodV4 ? zodV4?.toJSONSchema(
119
- schema
120
- ) : void 0;
121
105
  const apiKey = requireApiKey();
122
106
  const form = new FormData();
123
107
  form.append("prompt", prompt);
124
- if (jsonSchema) {
125
- form.append("jsonSchema", JSON.stringify(jsonSchema));
126
- }
127
- const pngBuffer = await pageOrLocator.screenshot({ type: "png" });
128
- const u8 = Uint8Array.from(pngBuffer);
108
+ const u8 = Uint8Array.from(screenshot);
129
109
  const blob = new Blob([u8], { type: "image/png" });
130
110
  form.append("image", blob, "screenshot.png");
131
- const response = await fetch(EXTRACT_ENDPOINT, {
132
- method: "POST",
111
+ const response = await fetch(PROMPT_ASSERTION_ENDPOINT, {
112
+ body: form,
133
113
  headers: {
134
114
  ...SDK_METADATA_HEADERS,
135
115
  Authorization: `Bearer ${apiKey}`
136
116
  },
137
- body: form
117
+ method: "POST"
138
118
  });
139
- const raw = await response.json().catch(() => void 0);
119
+ const parsed = await response.json().catch(() => void 0);
140
120
  if (response.ok) {
141
- if (!isExtractionResponse(raw)) {
142
- throw new Error("Extract returned unexpected response shape");
143
- }
144
- if (raw.success === false) {
145
- return raw.error;
146
- }
147
- const { value } = raw;
148
- return schema ? await validateWithSchema(schema, value) : typeof value === "string" ? value : JSON.stringify(value);
121
+ const { reason, success } = parseSuccessResponse(parsed);
122
+ return {
123
+ pass: success,
124
+ reason
125
+ };
149
126
  }
150
- throw new Error(isErrorResponse(raw) ? raw.error : "Extract failed");
151
- }
152
-
153
- // src/playwright-augment/methods/extract.ts
154
- function createExtract(pageOrLocator) {
155
- const impl = async (prompt, options) => {
156
- if (options?.schema) {
157
- return extract({
158
- prompt,
159
- schema: options.schema,
160
- pageOrLocator
161
- });
162
- }
163
- return extract({ prompt, pageOrLocator });
164
- };
165
- return impl;
127
+ const err = parseErrorResponse(parsed);
128
+ throw new Error(
129
+ `Verify prompt failed (${response.status})${err ? `: ${err.error}` : ""}`
130
+ );
166
131
  }
167
- var createLocatorExtract = (locator) => createExtract(locator);
168
- var createPageExtract = (page) => createExtract(page);
169
-
170
- // src/playwright-augment/methods/agent.ts
171
- var import_internal_playwright_test = require("@stablyai/internal-playwright-test");
172
132
 
173
133
  // src/playwright-type-predicates.ts
174
134
  function isPage(candidate) {
@@ -178,6 +138,9 @@ function isLocator(candidate) {
178
138
  return typeof candidate === "object" && candidate !== null && typeof candidate.screenshot === "function" && typeof candidate.nth === "function";
179
139
  }
180
140
 
141
+ // src/image-compare.ts
142
+ var jpeg = __toESM(require("jpeg-js"));
143
+
181
144
  // ../../node_modules/.pnpm/pixelmatch@7.1.0/node_modules/pixelmatch/index.js
182
145
  function pixelmatch(img1, img2, output, width, height, options = {}) {
183
146
  const {
@@ -336,7 +299,6 @@ function drawGrayPixel(img, i, alpha, output) {
336
299
 
337
300
  // src/image-compare.ts
338
301
  var import_pngjs = require("pngjs");
339
- var jpeg = __toESM(require("jpeg-js"));
340
302
  var isPng = (buffer) => {
341
303
  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;
342
304
  };
@@ -346,14 +308,14 @@ var isJpeg = (buffer) => {
346
308
  var decodeImage = (buffer) => {
347
309
  if (isPng(buffer)) {
348
310
  const png2 = import_pngjs.PNG.sync.read(buffer);
349
- return { data: png2.data, width: png2.width, height: png2.height };
311
+ return { data: png2.data, height: png2.height, width: png2.width };
350
312
  }
351
313
  if (isJpeg(buffer)) {
352
314
  const img = jpeg.decode(buffer, { maxMemoryUsageInMB: 1024 });
353
- return { data: img.data, width: img.width, height: img.height };
315
+ return { data: img.data, height: img.height, width: img.width };
354
316
  }
355
317
  const png = import_pngjs.PNG.sync.read(buffer);
356
- return { data: png.data, width: png.width, height: png.height };
318
+ return { data: png.data, height: png.height, width: png.width };
357
319
  };
358
320
  var imagesAreSimilar = ({
359
321
  image1,
@@ -405,7 +367,9 @@ async function takeStableScreenshot(target, options) {
405
367
  return await target.screenshot(options);
406
368
  };
407
369
  while (true) {
408
- if (Date.now() >= deadline) break;
370
+ if (Date.now() >= deadline) {
371
+ break;
372
+ }
409
373
  const delay = pollIntervals.length ? pollIntervals.shift() : 1e3;
410
374
  if (delay) {
411
375
  await page.waitForTimeout(delay);
@@ -424,49 +388,279 @@ async function takeStableScreenshot(target, options) {
424
388
  return actual ?? await safeScreenshot();
425
389
  }
426
390
 
391
+ // src/expect.ts
392
+ function createFailureMessage({
393
+ condition,
394
+ didPass,
395
+ isNot,
396
+ reason,
397
+ targetType
398
+ }) {
399
+ const expectation = isNot ? "not to satisfy" : "to satisfy";
400
+ const result = didPass ? "it did" : "it did not";
401
+ let message = `Expected ${targetType} ${expectation} ${JSON.stringify(condition)}, but ${result}.`;
402
+ if (reason) {
403
+ message += `
404
+
405
+ Reason: ${reason}`;
406
+ }
407
+ return message;
408
+ }
409
+ var stablyPlaywrightMatchers = {
410
+ async toMatchScreenshotPrompt(received, condition, options) {
411
+ const target = isPage(received) ? received : isLocator(received) ? received : void 0;
412
+ if (!target) {
413
+ throw new Error(
414
+ "toMatchScreenshotPrompt only supports Playwright Page and Locator instances."
415
+ );
416
+ }
417
+ const targetType = isPage(target) ? "page" : "locator";
418
+ const screenshot = await takeStableScreenshot(target, options);
419
+ const verifyResult = await verifyPrompt({ prompt: condition, screenshot });
420
+ const testInfo = import_internal_playwright_test.test.info();
421
+ testInfo.attachments.push({
422
+ body: Buffer.from(
423
+ JSON.stringify(
424
+ {
425
+ pass: verifyResult.pass,
426
+ prompt: condition,
427
+ reasoning: verifyResult.reason
428
+ },
429
+ null,
430
+ 2
431
+ ),
432
+ "utf-8"
433
+ ),
434
+ contentType: "application/json",
435
+ name: "toMatchScreenshotPrompt-reasoning"
436
+ });
437
+ return {
438
+ message: () => createFailureMessage({
439
+ condition,
440
+ didPass: verifyResult.pass,
441
+ isNot: this.isNot,
442
+ reason: verifyResult.reason,
443
+ targetType
444
+ }),
445
+ name: "toMatchScreenshotPrompt",
446
+ pass: verifyResult.pass
447
+ };
448
+ }
449
+ };
450
+
451
+ // src/playwright-augment/methods/agent.ts
452
+ var import_internal_playwright_test2 = require("@stablyai/internal-playwright-test");
453
+
454
+ // src/utils/truncate.ts
455
+ var truncate = (inp, length) => inp.length <= length || inp.length <= 3 ? inp : `${inp.slice(0, length - 3)}...`;
456
+
427
457
  // src/playwright-augment/methods/agent/construct-payload.ts
428
458
  function constructAgentPayload({
429
- sessionId,
430
- message,
459
+ activePage,
460
+ additionalContext,
431
461
  isError,
462
+ message,
463
+ model,
432
464
  screenshot,
433
- tabManager,
434
- activePage,
435
- additionalContext
465
+ sessionId,
466
+ tabManager
436
467
  }) {
437
468
  const form = new FormData();
438
- form.append("session_id", sessionId);
439
- form.append("message", message);
469
+ const viewportSize = activePage.viewportSize();
470
+ const tabs = Array.from(tabManager.entries()).map(([page, alias]) => ({
471
+ alias,
472
+ url: page.url()
473
+ }));
474
+ const activePageAlias = tabManager.get(activePage);
475
+ const metadata = {
476
+ allPages: tabs,
477
+ message,
478
+ sessionId
479
+ };
480
+ if (model) {
481
+ metadata.model = model;
482
+ }
440
483
  if (isError) {
441
- form.append("is_error", JSON.stringify(isError));
484
+ metadata.isError = isError;
442
485
  }
443
486
  if (additionalContext) {
444
- form.append("additional_context", JSON.stringify(additionalContext));
487
+ metadata.additionalContext = additionalContext;
445
488
  }
446
- const viewportSize = activePage.viewportSize();
447
489
  if (viewportSize) {
448
- form.append("page_dimensions", JSON.stringify(viewportSize));
490
+ metadata.pageDimensions = viewportSize;
449
491
  }
492
+ if (activePageAlias) {
493
+ metadata.activePageAlias = activePageAlias;
494
+ }
495
+ const metadataBlob = new Blob([JSON.stringify(metadata)], {
496
+ type: "application/json"
497
+ });
498
+ form.append("metadata", metadataBlob, "metadata.json");
450
499
  const screenshotBytes = Uint8Array.from(screenshot);
451
500
  const screenshotBlob = new Blob([screenshotBytes], { type: "image/png" });
452
501
  form.append("screenshot", screenshotBlob, "screenshot.png");
453
- const tabs = Array.from(tabManager.entries()).map(([page, alias]) => ({
454
- alias,
455
- url: page.url()
456
- }));
457
- form.append("all_pages", JSON.stringify(tabs));
458
- const activePageAlias = tabManager.get(activePage);
459
- if (activePageAlias) {
460
- form.append("active_page_alias", activePageAlias);
461
- }
462
502
  return form;
463
503
  }
464
504
 
505
+ // src/playwright-augment/methods/agent/exec-response.ts
506
+ var DEFAULT_AGENT_WAIT_MS = 3e3;
507
+ async function execResponse({
508
+ activePage: initialActivePage,
509
+ agentResponse,
510
+ browserContext,
511
+ tabManager
512
+ }) {
513
+ let activePage = initialActivePage;
514
+ try {
515
+ switch (agentResponse.action) {
516
+ case "key": {
517
+ const { text } = agentResponse;
518
+ if (text) {
519
+ await activePage.keyboard.press(text);
520
+ return { activePage, result: { message: `pressed "${text}"` } };
521
+ }
522
+ return { activePage, result: { message: "pressed key" } };
523
+ }
524
+ case "type": {
525
+ const { text } = agentResponse;
526
+ await activePage.keyboard.type(text);
527
+ return { activePage, result: { message: `typed "${text}"` } };
528
+ }
529
+ case "mouse_move": {
530
+ const [x, y] = agentResponse.coordinate;
531
+ await activePage.mouse.move(x, y);
532
+ return { activePage, result: { message: `mouse moved completed` } };
533
+ }
534
+ case "left_click": {
535
+ const [x, y] = agentResponse.coordinate;
536
+ await activePage.mouse.click(x, y);
537
+ return { activePage, result: { message: `left click completed` } };
538
+ }
539
+ case "right_click": {
540
+ const [x, y] = agentResponse.coordinate;
541
+ await activePage.mouse.click(x, y, { button: "right" });
542
+ return { activePage, result: { message: `right click completed` } };
543
+ }
544
+ case "double_click": {
545
+ const [x, y] = agentResponse.coordinate;
546
+ await activePage.mouse.dblclick(x, y);
547
+ return { activePage, result: { message: `double click completed` } };
548
+ }
549
+ case "triple_click": {
550
+ const [x, y] = agentResponse.coordinate;
551
+ await activePage.mouse.click(x, y, { clickCount: 3 });
552
+ return { activePage, result: { message: `triple click completed` } };
553
+ }
554
+ case "left_click_drag": {
555
+ const [startX, startY] = agentResponse.start_coordinate;
556
+ const [endX, endY] = agentResponse.coordinate;
557
+ await activePage.mouse.move(startX, startY);
558
+ await activePage.mouse.down();
559
+ await activePage.mouse.move(endX, endY);
560
+ await activePage.mouse.up();
561
+ return { activePage, result: { message: `drag completed` } };
562
+ }
563
+ case "screenshot": {
564
+ await takeStableScreenshot(activePage);
565
+ return { activePage, result: { message: "captured screenshot" } };
566
+ }
567
+ case "wait": {
568
+ const waitMs = agentResponse.milliseconds ?? DEFAULT_AGENT_WAIT_MS;
569
+ await activePage.waitForTimeout(waitMs);
570
+ return { activePage, result: { message: `waited ${waitMs}ms` } };
571
+ }
572
+ case "navigate_to_url": {
573
+ await activePage.goto(agentResponse.url);
574
+ return {
575
+ activePage,
576
+ result: { message: `navigated to "${agentResponse.url}"` }
577
+ };
578
+ }
579
+ case "new_tab_url": {
580
+ const newPage = await browserContext.newPage();
581
+ await newPage.goto(agentResponse.url);
582
+ await newPage.waitForLoadState("domcontentloaded");
583
+ activePage = newPage;
584
+ return { activePage, result: { message: "opened new tab" } };
585
+ }
586
+ case "switch_tab": {
587
+ const entry = Array.from(tabManager.entries()).find(
588
+ ([, alias]) => alias === agentResponse.tab_alias
589
+ );
590
+ const page = entry?.[0];
591
+ if (!page) {
592
+ throw new Error(
593
+ `Tab with alias ${agentResponse.tab_alias} not found`
594
+ );
595
+ }
596
+ await page.bringToFront();
597
+ activePage = page;
598
+ return {
599
+ activePage,
600
+ result: { message: `switched to "${agentResponse.tab_alias}"` }
601
+ };
602
+ }
603
+ case "scroll": {
604
+ const [x, y] = agentResponse.coordinate;
605
+ await activePage.mouse.move(x, y);
606
+ let deltaX = 0;
607
+ let deltaY = 0;
608
+ switch (agentResponse.scroll_direction) {
609
+ case "up":
610
+ deltaY = -agentResponse.scroll_amount;
611
+ break;
612
+ case "down":
613
+ deltaY = agentResponse.scroll_amount;
614
+ break;
615
+ case "left":
616
+ deltaX = -agentResponse.scroll_amount;
617
+ break;
618
+ case "right":
619
+ deltaX = agentResponse.scroll_amount;
620
+ break;
621
+ }
622
+ await activePage.mouse.wheel(deltaX, deltaY);
623
+ return {
624
+ activePage,
625
+ result: { message: `scrolled ${agentResponse.scroll_direction}` }
626
+ };
627
+ }
628
+ case "navigate_back": {
629
+ const res = await activePage.goBack();
630
+ if (!res) {
631
+ throw new Error("navigate_back failed: no history entry");
632
+ }
633
+ return { activePage, result: { message: "navigated back" } };
634
+ }
635
+ case "aria_snapshot": {
636
+ const ariaSnapshot = await activePage._snapshotForAI();
637
+ return {
638
+ activePage,
639
+ result: { message: `ARIA Snapshot:
640
+ ${ariaSnapshot}` }
641
+ };
642
+ }
643
+ case "terminate_test": {
644
+ const { reason, success } = agentResponse;
645
+ return {
646
+ activePage,
647
+ finalSuccess: success,
648
+ result: { message: reason, shouldTerminate: true }
649
+ };
650
+ }
651
+ }
652
+ } catch (error) {
653
+ const message = error instanceof Error ? error.message : String(error);
654
+ return { activePage, result: { isError: true, message } };
655
+ }
656
+ }
657
+
465
658
  // src/playwright-augment/methods/agent.ts
466
- var AGENT_PATH = "internal/v1/agent";
659
+ var AGENT_PATH = "internal/v3/agent";
467
660
  var STABLY_API_URL = process.env.STABLY_API_URL || "https://api.stably.ai";
468
661
  var AGENT_ENDPOINT = new URL(AGENT_PATH, STABLY_API_URL).toString();
469
662
  function createAgentStub() {
663
+ let thoughtsIndex = 0;
470
664
  return async (prompt, options) => {
471
665
  const apiKey = requireApiKey();
472
666
  const maxCycles = options.maxCycles ?? 30;
@@ -477,8 +671,8 @@ function createAgentStub() {
477
671
  });
478
672
  let activePage = options.page;
479
673
  let agentMessage = {
480
- message: prompt,
481
674
  isError: false,
675
+ message: prompt,
482
676
  shouldTerminate: false
483
677
  };
484
678
  let finalSuccess;
@@ -496,165 +690,81 @@ function createAgentStub() {
496
690
  newPageOpenedMsg = `opened new tab ${alias} (${page.url()})`;
497
691
  };
498
692
  browserContext.on("page", onNewPage);
499
- return await import_internal_playwright_test.test.step(prompt, async () => {
693
+ return await import_internal_playwright_test2.test.step(`[Agent] ${prompt}`, async () => {
500
694
  try {
501
695
  for (let i = 0; i < maxCycles; i++) {
502
696
  if (agentMessage.shouldTerminate) {
503
697
  break;
504
698
  }
505
- const screenshot = await takeStableScreenshot(activePage);
506
- const response = await fetch(AGENT_ENDPOINT, {
507
- method: "POST",
508
- headers: {
509
- ...SDK_METADATA_HEADERS,
510
- Authorization: `Bearer ${apiKey}`
511
- },
512
- body: constructAgentPayload({
513
- sessionId,
514
- message: agentMessage.message,
515
- isError: agentMessage.isError,
516
- screenshot,
517
- tabManager,
518
- activePage,
519
- additionalContext: newPageOpenedMsg ? { newPageMessage: newPageOpenedMsg } : void 0
520
- })
521
- });
522
- newPageOpenedMsg = void 0;
523
- const responseJson = await response.json();
524
- if (!response.ok) {
525
- throw new Error(
526
- `Agent call failed: ${JSON.stringify(responseJson)}`
527
- );
528
- }
529
- const agentResponse = responseJson;
530
- agentMessage = await (async () => {
531
- try {
532
- switch (agentResponse.action) {
533
- case "key": {
534
- const { text } = agentResponse;
535
- if (text) {
536
- await activePage.keyboard.press(text);
537
- return { message: `pressed "${text}"` };
538
- }
539
- return { message: "pressed key" };
540
- }
541
- case "type": {
542
- const { text } = agentResponse;
543
- await activePage.keyboard.type(text);
544
- return { message: `typed "${text}"` };
545
- }
546
- case "mouse_move": {
547
- const [x, y] = agentResponse.coordinate;
548
- await activePage.mouse.move(x, y);
549
- return { message: `mouse moved to [${x}, ${y}]` };
550
- }
551
- case "left_click": {
552
- const [x, y] = agentResponse.coordinate;
553
- await activePage.mouse.click(x, y);
554
- return { message: `left click at [${x}, ${y}]` };
555
- }
556
- case "right_click": {
557
- const [x, y] = agentResponse.coordinate;
558
- await activePage.mouse.click(x, y, { button: "right" });
559
- return { message: `right click at [${x}, ${y}]` };
560
- }
561
- case "double_click": {
562
- const [x, y] = agentResponse.coordinate;
563
- await activePage.mouse.dblclick(x, y);
564
- return { message: `double click at [${x}, ${y}]` };
565
- }
566
- case "triple_click": {
567
- const [x, y] = agentResponse.coordinate;
568
- await activePage.mouse.click(x, y, { clickCount: 3 });
569
- return { message: `triple click at [${x}, ${y}]` };
570
- }
571
- case "left_click_drag": {
572
- const [startX, startY] = agentResponse.start_coordinate;
573
- const [endX, endY] = agentResponse.coordinate;
574
- await activePage.mouse.move(startX, startY);
575
- await activePage.mouse.down();
576
- await activePage.mouse.move(endX, endY);
577
- await activePage.mouse.up();
578
- return {
579
- message: `dragged from [${startX}, ${startY}] to [${endX}, ${endY}]`
580
- };
581
- }
582
- case "screenshot": {
583
- await takeStableScreenshot(activePage);
584
- return { message: "captured screenshot" };
585
- }
586
- case "wait": {
587
- const waitMs = agentResponse.milliseconds ?? 3e3;
588
- await activePage.waitForTimeout(waitMs);
589
- return { message: `waited ${waitMs}ms` };
590
- }
591
- case "navigate_to_url": {
592
- await activePage.goto(agentResponse.url);
593
- return { message: `navigated to "${agentResponse.url}"` };
594
- }
595
- case "new_tab_url": {
596
- const newPage = await browserContext.newPage();
597
- await newPage.goto(agentResponse.url);
598
- await newPage.waitForLoadState("domcontentloaded");
599
- return { message: "opened new tab" };
600
- }
601
- case "switch_tab": {
602
- const entry = Array.from(tabManager.entries()).find(
603
- ([, alias]) => alias === agentResponse.tab_alias
604
- );
605
- const page = entry?.[0];
606
- if (!page) {
607
- throw new Error(
608
- `Tab with alias ${agentResponse.tab_alias} not found`
609
- );
610
- }
611
- await page.bringToFront();
612
- activePage = page;
613
- return {
614
- message: `switched to "${agentResponse.tab_alias}"`
615
- };
616
- }
617
- case "scroll": {
618
- const [x, y] = agentResponse.coordinate;
619
- await activePage.mouse.move(x, y);
620
- let deltaX = 0;
621
- let deltaY = 0;
622
- switch (agentResponse.scroll_direction) {
623
- case "up":
624
- deltaY = -agentResponse.scroll_amount;
625
- break;
626
- case "down":
627
- deltaY = agentResponse.scroll_amount;
628
- break;
629
- case "left":
630
- deltaX = -agentResponse.scroll_amount;
631
- break;
632
- case "right":
633
- deltaX = agentResponse.scroll_amount;
634
- break;
635
- }
636
- await activePage.mouse.wheel(deltaX, deltaY);
637
- return {
638
- message: `scrolled ${agentResponse.scroll_direction}`
639
- };
640
- }
641
- case "navigate_back": {
642
- const res = await activePage.goBack();
643
- if (!res)
644
- throw new Error("navigate_back failed: no history entry");
645
- return { message: "navigated back" };
646
- }
647
- case "terminate_test": {
648
- const { success, reason } = agentResponse;
649
- finalSuccess = success;
650
- return { message: reason, shouldTerminate: true };
651
- }
699
+ const agentResponses = await import_internal_playwright_test2.test.step(`[Thinking ${thoughtsIndex + 1}]`, async (stepInfo) => {
700
+ const screenshot = await takeStableScreenshot(activePage);
701
+ const response = await fetch(AGENT_ENDPOINT, {
702
+ body: constructAgentPayload({
703
+ activePage,
704
+ additionalContext: newPageOpenedMsg ? { newPageMessage: newPageOpenedMsg } : void 0,
705
+ isError: agentMessage.isError,
706
+ message: agentMessage.message,
707
+ model: options.model,
708
+ screenshot,
709
+ sessionId,
710
+ tabManager
711
+ }),
712
+ headers: {
713
+ ...SDK_METADATA_HEADERS,
714
+ Authorization: `Bearer ${apiKey}`
715
+ },
716
+ method: "POST"
717
+ });
718
+ newPageOpenedMsg = void 0;
719
+ const responseJson = await response.json();
720
+ if (!response.ok) {
721
+ throw new Error(
722
+ `Agent call failed: ${JSON.stringify(responseJson)}`
723
+ );
724
+ }
725
+ const reasoningTexts = responseJson.map((r) => r.content).filter((content) => content !== void 0);
726
+ const reasoningText = reasoningTexts.join("\n\n");
727
+ const truncatedReasoningText = truncate(reasoningText, 120);
728
+ await stepInfo.attach(
729
+ `[Thinking ${thoughtsIndex + 1}] ${truncatedReasoningText}`,
730
+ {
731
+ body: reasoningText,
732
+ contentType: "text/plain"
652
733
  }
653
- } catch (error) {
654
- const message = error instanceof Error ? error.message : String(error);
655
- return { message, isError: true };
734
+ );
735
+ thoughtsIndex++;
736
+ return responseJson;
737
+ });
738
+ let combinedMessages = [];
739
+ let aggregatedIsError = false;
740
+ let aggregatedShouldTerminate = false;
741
+ for (const agentResponse of agentResponses) {
742
+ const {
743
+ activePage: newActivePage,
744
+ finalSuccess: maybeFinal,
745
+ result
746
+ } = await execResponse({
747
+ activePage,
748
+ agentResponse,
749
+ browserContext,
750
+ tabManager
751
+ });
752
+ activePage = newActivePage;
753
+ combinedMessages.push(result.message);
754
+ finalSuccess = maybeFinal ?? finalSuccess;
755
+ if (result.isError) {
756
+ aggregatedIsError = true;
757
+ }
758
+ if (result.shouldTerminate) {
759
+ aggregatedShouldTerminate = true;
760
+ break;
656
761
  }
657
- })();
762
+ }
763
+ agentMessage = {
764
+ isError: aggregatedIsError,
765
+ message: combinedMessages.join("\n"),
766
+ shouldTerminate: aggregatedShouldTerminate
767
+ };
658
768
  }
659
769
  } finally {
660
770
  browserContext.off("page", onNewPage);
@@ -664,6 +774,106 @@ function createAgentStub() {
664
774
  };
665
775
  }
666
776
 
777
+ // src/ai/extract.ts
778
+ var EXTRACT_PATH = "internal/v2/extract";
779
+ var STABLY_API_URL2 = process.env.STABLY_API_URL || "https://api.stably.ai";
780
+ var EXTRACT_ENDPOINT = new URL(EXTRACT_PATH, STABLY_API_URL2).toString();
781
+ var zodV4 = (() => {
782
+ try {
783
+ return require("zod/v4/core");
784
+ } catch {
785
+ return void 0;
786
+ }
787
+ })();
788
+ var isExtractionResponse = (value) => {
789
+ if (!isObject(value)) {
790
+ return false;
791
+ }
792
+ if (value.success === true) {
793
+ return "value" in value;
794
+ }
795
+ return value.success === false && typeof value.error === "string";
796
+ };
797
+ var isErrorResponse = (value) => {
798
+ return isObject(value) && typeof value.error === "string";
799
+ };
800
+ var ExtractValidationError = class extends Error {
801
+ constructor(message, issues) {
802
+ super(message);
803
+ this.issues = issues;
804
+ this.name = "ExtractValidationError";
805
+ }
806
+ };
807
+ async function validateWithSchema(schema, value) {
808
+ const result = await schema.safeParseAsync(value);
809
+ if (!result.success) {
810
+ throw new ExtractValidationError("Validation failed", result.error.issues);
811
+ }
812
+ return result.data;
813
+ }
814
+ async function extract({
815
+ pageOrLocator,
816
+ prompt,
817
+ schema
818
+ }) {
819
+ if (schema && !zodV4) {
820
+ throw new Error(
821
+ "Schema support requires installing zod@4. Please add it to enable schemas."
822
+ );
823
+ }
824
+ const jsonSchema = schema && zodV4 ? zodV4?.toJSONSchema(
825
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
826
+ schema
827
+ ) : void 0;
828
+ const apiKey = requireApiKey();
829
+ const form = new FormData();
830
+ form.append("prompt", prompt);
831
+ if (jsonSchema) {
832
+ form.append("jsonSchema", JSON.stringify(jsonSchema));
833
+ }
834
+ const pngBuffer = await pageOrLocator.screenshot({ type: "png" });
835
+ const u8 = Uint8Array.from(pngBuffer);
836
+ const blob = new Blob([u8], { type: "image/png" });
837
+ form.append("image", blob, "screenshot.png");
838
+ const response = await fetch(EXTRACT_ENDPOINT, {
839
+ body: form,
840
+ headers: {
841
+ ...SDK_METADATA_HEADERS,
842
+ Authorization: `Bearer ${apiKey}`
843
+ },
844
+ method: "POST"
845
+ });
846
+ const raw = await response.json().catch(() => void 0);
847
+ if (response.ok) {
848
+ if (!isExtractionResponse(raw)) {
849
+ throw new Error("Extract returned unexpected response shape");
850
+ }
851
+ if (!raw.success) {
852
+ throw new Error(`Extract failed: ${raw.error}`);
853
+ }
854
+ const { value } = raw;
855
+ return schema ? await validateWithSchema(schema, value) : typeof value === "string" ? value : JSON.stringify(value);
856
+ }
857
+ throw new Error(isErrorResponse(raw) ? raw.error : "Extract failed");
858
+ }
859
+
860
+ // src/playwright-augment/methods/extract.ts
861
+ function createExtract(pageOrLocator) {
862
+ const impl = async (prompt, options) => {
863
+ if (options?.schema) {
864
+ return extract({
865
+ pageOrLocator,
866
+ prompt,
867
+ schema: options.schema
868
+ });
869
+ }
870
+ return extract({ pageOrLocator, prompt });
871
+ };
872
+ return impl;
873
+ }
874
+ var createLocatorExtract = (locator) => createExtract(locator);
875
+ var createPageExtract = (page) => createExtract(page);
876
+
667
877
  // src/playwright-augment/augment.ts
668
878
  var LOCATOR_PATCHED = Symbol.for("stably.playwright.locatorPatched");
669
879
  var LOCATOR_DESCRIBE_WRAPPED = Symbol.for(
@@ -675,9 +885,9 @@ var BROWSER_PATCHED = Symbol.for("stably.playwright.browserPatched");
675
885
  var BROWSER_TYPE_PATCHED = Symbol.for("stably.playwright.browserTypePatched");
676
886
  function defineHiddenProperty(target, key, value) {
677
887
  Object.defineProperty(target, key, {
678
- value,
679
- enumerable: false,
680
888
  configurable: true,
889
+ enumerable: false,
890
+ value,
681
891
  writable: true
682
892
  });
683
893
  }
@@ -724,6 +934,7 @@ function augmentBrowserContext(context) {
724
934
  if (originalPages) {
725
935
  context.pages = () => originalPages().map(
726
936
  (page) => augmentPage(page)
937
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
727
938
  );
728
939
  }
729
940
  if (!context.agent) {
@@ -749,6 +960,7 @@ function augmentBrowser(browser) {
749
960
  const originalContexts = browser.contexts.bind(browser);
750
961
  browser.contexts = () => originalContexts().map(
751
962
  (context) => augmentBrowserContext(context)
963
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
752
964
  );
753
965
  if (!browser.agent) {
754
966
  defineHiddenProperty(browser, "agent", createAgentStub());
@@ -772,14 +984,19 @@ function augmentBrowserType(browserType) {
772
984
  return augmentBrowser(browser);
773
985
  };
774
986
  }
775
- const originalConnectOverCDP = browserType.connectOverCDP?.bind(browserType);
987
+ const originalConnectOverCDP = browserType.connectOverCDP?.bind(
988
+ browserType
989
+ );
776
990
  if (originalConnectOverCDP) {
777
991
  browserType.connectOverCDP = async (...args) => {
778
992
  const browser = await originalConnectOverCDP(...args);
779
993
  return augmentBrowser(browser);
780
994
  };
781
995
  }
782
- const originalLaunchPersistentContext = browserType.launchPersistentContext?.bind(browserType);
996
+ const originalLaunchPersistentContext = (
997
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
998
+ browserType.launchPersistentContext?.bind(browserType)
999
+ );
783
1000
  if (originalLaunchPersistentContext) {
784
1001
  browserType.launchPersistentContext = async (...args) => {
785
1002
  const context = await originalLaunchPersistentContext(...args);
@@ -789,105 +1006,6 @@ function augmentBrowserType(browserType) {
789
1006
  defineHiddenProperty(browserType, BROWSER_TYPE_PATCHED, true);
790
1007
  return browserType;
791
1008
  }
792
-
793
- // src/ai/verify-prompt.ts
794
- var PROMPT_ASSERTION_ENDPOINT = "https://api.stably.ai/internal/v1/assert";
795
- var parseSuccessResponse = (value) => {
796
- if (!isObject(value)) {
797
- throw new Error("Verify prompt returned unexpected response shape");
798
- }
799
- const { success, reason } = value;
800
- if (typeof success !== "boolean") {
801
- throw new Error("Verify prompt returned unexpected response shape");
802
- }
803
- if (reason !== void 0 && typeof reason !== "string") {
804
- throw new Error("Verify prompt returned unexpected response shape");
805
- }
806
- return {
807
- success,
808
- reason
809
- };
810
- };
811
- var parseErrorResponse = (value) => {
812
- if (!isObject(value)) {
813
- return void 0;
814
- }
815
- const { error } = value;
816
- return typeof error !== "string" ? void 0 : { error };
817
- };
818
- async function verifyPrompt({
819
- prompt,
820
- screenshot
821
- }) {
822
- const apiKey = requireApiKey();
823
- const form = new FormData();
824
- form.append("prompt", prompt);
825
- const u8 = Uint8Array.from(screenshot);
826
- const blob = new Blob([u8], { type: "image/png" });
827
- form.append("image", blob, "screenshot.png");
828
- const response = await fetch(PROMPT_ASSERTION_ENDPOINT, {
829
- method: "POST",
830
- headers: {
831
- ...SDK_METADATA_HEADERS,
832
- Authorization: `Bearer ${apiKey}`
833
- },
834
- body: form
835
- });
836
- const parsed = await response.json().catch(() => void 0);
837
- if (response.ok) {
838
- const { success, reason } = parseSuccessResponse(parsed);
839
- return {
840
- pass: success,
841
- reason
842
- };
843
- }
844
- const err = parseErrorResponse(parsed);
845
- throw new Error(
846
- `Verify prompt failed (${response.status})${err ? `: ${err.error}` : ""}`
847
- );
848
- }
849
-
850
- // src/expect.ts
851
- function createFailureMessage({
852
- targetType,
853
- condition,
854
- didPass,
855
- isNot,
856
- reason
857
- }) {
858
- const expectation = isNot ? "not to satisfy" : "to satisfy";
859
- const result = didPass ? "it did" : "it did not";
860
- let message = `Expected ${targetType} ${expectation} ${JSON.stringify(condition)}, but ${result}.`;
861
- if (reason) {
862
- message += `
863
-
864
- Reason: ${reason}`;
865
- }
866
- return message;
867
- }
868
- var stablyPlaywrightMatchers = {
869
- async toMatchScreenshotPrompt(received, condition, options) {
870
- const target = isPage(received) ? received : isLocator(received) ? received : void 0;
871
- if (!target) {
872
- throw new Error(
873
- "toMatchScreenshotPrompt only supports Playwright Page and Locator instances."
874
- );
875
- }
876
- const targetType = isPage(target) ? "page" : "locator";
877
- const screenshot = await takeStableScreenshot(target, options);
878
- const verifyResult = await verifyPrompt({ prompt: condition, screenshot });
879
- return {
880
- pass: verifyResult.pass,
881
- message: () => createFailureMessage({
882
- targetType,
883
- condition,
884
- didPass: verifyResult.pass,
885
- reason: verifyResult.reason,
886
- isNot: this.isNot
887
- })
888
- };
889
- }
890
- };
891
1009
  // Annotate the CommonJS export names for ESM import in node:
892
1010
  0 && (module.exports = {
893
1011
  augmentBrowser,