From 0b9b9cadc77273e4a77fc4c8e7c4b2987a31997d Mon Sep 17 00:00:00 2001 From: lforst <8118419+lforst@users.noreply.github.com> Date: Mon, 3 Aug 2026 16:40:28 +0000 Subject: [PATCH 1/4] feat: Add stagehand instrumentation --- e2e/config/pr-comment-scenarios.json | 15 + .../stagehand-v3-auto-hook.cassette.json | 246 ++ ...tagehand-v3-latest-auto-hook.cassette.json | 301 ++ .../stagehand-v3-latest-wrapped.cassette.json | 301 ++ .../stagehand-v3-wrapped.cassette.json | 246 ++ .../stagehand-v3-auto-hook.span-tree.json | 302 ++ .../stagehand-v3-auto-hook.span-tree.txt | 245 ++ ...agehand-v3-latest-auto-hook.span-tree.json | 322 ++ ...tagehand-v3-latest-auto-hook.span-tree.txt | 265 ++ ...stagehand-v3-latest-wrapped.span-tree.json | 322 ++ .../stagehand-v3-latest-wrapped.span-tree.txt | 265 ++ .../stagehand-v3-wrapped.span-tree.json | 302 ++ .../stagehand-v3-wrapped.span-tree.txt | 245 ++ .../stagehand-instrumentation/assertions.ts | 204 ++ .../stagehand-instrumentation/package.json | 21 + .../stagehand-instrumentation/pnpm-lock.yaml | 2996 +++++++++++++++++ .../scenario.impl.mjs | 316 ++ .../stagehand-instrumentation/scenario.mjs | 10 + .../scenario.test.ts | 79 + .../stagehand-instrumentation/scenario.ts | 11 + js/src/auto-instrumentations/configs/all.ts | 5 + .../configs/stagehand.test.ts | 76 + .../configs/stagehand.ts | 44 + js/src/exports.ts | 1 + js/src/instrumentation/braintrust-plugin.ts | 12 + js/src/instrumentation/config.ts | 5 + .../plugins/instrumentation-names.test.ts | 2 + .../plugins/stagehand-agent.ts | 61 + .../plugins/stagehand-channels.ts | 38 + .../stagehand-plugin-lifecycle.test.ts | 168 + .../plugins/stagehand-plugin.test.ts | 152 + .../plugins/stagehand-plugin.ts | 398 +++ js/src/span-origin.ts | 1 + js/src/vendor-sdk-types/stagehand.ts | 65 + js/src/wrappers/stagehand.test.ts | 114 + js/src/wrappers/stagehand.ts | 154 + 36 files changed, 8310 insertions(+) create mode 100644 e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-auto-hook.cassette.json create mode 100644 e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-latest-auto-hook.cassette.json create mode 100644 e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-latest-wrapped.cassette.json create mode 100644 e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-wrapped.cassette.json create mode 100644 e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-auto-hook.span-tree.json create mode 100644 e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-auto-hook.span-tree.txt create mode 100644 e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-auto-hook.span-tree.json create mode 100644 e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-auto-hook.span-tree.txt create mode 100644 e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-wrapped.span-tree.json create mode 100644 e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-wrapped.span-tree.txt create mode 100644 e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-wrapped.span-tree.json create mode 100644 e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-wrapped.span-tree.txt create mode 100644 e2e/scenarios/stagehand-instrumentation/assertions.ts create mode 100644 e2e/scenarios/stagehand-instrumentation/package.json create mode 100644 e2e/scenarios/stagehand-instrumentation/pnpm-lock.yaml create mode 100644 e2e/scenarios/stagehand-instrumentation/scenario.impl.mjs create mode 100644 e2e/scenarios/stagehand-instrumentation/scenario.mjs create mode 100644 e2e/scenarios/stagehand-instrumentation/scenario.test.ts create mode 100644 e2e/scenarios/stagehand-instrumentation/scenario.ts create mode 100644 js/src/auto-instrumentations/configs/stagehand.test.ts create mode 100644 js/src/auto-instrumentations/configs/stagehand.ts create mode 100644 js/src/instrumentation/plugins/stagehand-agent.ts create mode 100644 js/src/instrumentation/plugins/stagehand-channels.ts create mode 100644 js/src/instrumentation/plugins/stagehand-plugin-lifecycle.test.ts create mode 100644 js/src/instrumentation/plugins/stagehand-plugin.test.ts create mode 100644 js/src/instrumentation/plugins/stagehand-plugin.ts create mode 100644 js/src/vendor-sdk-types/stagehand.ts create mode 100644 js/src/wrappers/stagehand.test.ts create mode 100644 js/src/wrappers/stagehand.ts diff --git a/e2e/config/pr-comment-scenarios.json b/e2e/config/pr-comment-scenarios.json index 65a85ab6e..2381cc681 100644 --- a/e2e/config/pr-comment-scenarios.json +++ b/e2e/config/pr-comment-scenarios.json @@ -98,6 +98,21 @@ } ] }, + { + "scenarioDirName": "stagehand-instrumentation", + "label": "Stagehand Instrumentation", + "metadataScenario": "stagehand-instrumentation", + "variants": [ + { + "variantKey": "stagehand-v3", + "label": "v3 pinned" + }, + { + "variantKey": "stagehand-v3-latest", + "label": "v3 latest" + } + ] + }, { "scenarioDirName": "cloudflare-ai-chat-instrumentation", "label": "Cloudflare AI Chat Instrumentation", diff --git a/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-auto-hook.cassette.json b/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-auto-hook.cassette.json new file mode 100644 index 000000000..63ce08548 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-auto-hook.cassette.json @@ -0,0 +1,246 @@ +{ + "entries": [ + { + "callIndex": 0, + "id": "0854b19ef5286997", + "matchKey": "POST api.openai.com/v1/responses", + "recordedAt": "2026-08-03T14:07:58.411Z", + "request": { + "body": { + "kind": "json", + "value": { + "input": [ + { + "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. Otherwise, return an empty object.", + "role": "system" + }, + { + "content": [ + { + "text": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown. Remember that to users, buttons and links look the same in most cases.\n If the action is completely unrelated to a potential action to be taken on the page, return an empty object. \n ONLY return one action. If multiple actions are relevant, return the most relevant one. \n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The following variables are available to use in the action: %customerEmail%. Fill the argument variables with the variable name.\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + "type": "input_text" + } + ], + "role": "user" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "temperature": 0.1, + "text": { + "format": { + "name": "response", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "arguments": { + "items": { + "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'.", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Playwright interaction methods.", + "type": "string" + }, + "twoStep": { + "type": "boolean" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments", + "twoStep" + ], + "type": "object" + }, + "strict": false, + "type": "json_schema" + } + } + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/responses" + }, + "response": { + "body": { + "kind": "json", + "value": { + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1785766077, + "created_at": 1785766076, + "error": null, + "frequency_penalty": 0, + "id": "resp_08cbf4a61b741e30006a70a0bbd2888192a3bfc6bffe4f51cd", + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "metadata": {}, + "model": "gpt-4o-mini-2024-07-18", + "moderation": null, + "object": "response", + "output": [ + { + "content": [ + { + "annotations": [], + "logprobs": [], + "text": "{\"elementId\":\"0-14\",\"description\":\"Button to add the Trail Backpack to the cart\",\"method\":\"click\",\"arguments\":[],\"twoStep\":false}", + "type": "output_text" + } + ], + "id": "msg_08cbf4a61b741e30006a70a0bd0050819286c57b9e25d4989d", + "role": "assistant", + "status": "completed", + "type": "message" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": "[REDACTED]", + "prompt_cache_retention": "in_memory", + "reasoning": { + "context": null, + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "status": "completed", + "store": true, + "temperature": 0.1, + "text": { + "format": { + "description": null, + "name": "response", + "schema": { + "additionalProperties": false, + "properties": { + "arguments": { + "items": { + "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'.", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Playwright interaction methods.", + "type": "string" + }, + "twoStep": { + "type": "boolean" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments", + "twoStep" + ], + "type": "object" + }, + "strict": false, + "type": "json_schema" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tool_usage": { + "image_gen": { + "input_tokens": 0, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "output_tokens": 0, + "output_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "total_tokens": 0 + }, + "web_search": { + "num_requests": 0 + } + }, + "tools": [], + "top_logprobs": 0, + "top_p": 1, + "truncation": "disabled", + "usage": { + "input_tokens": 910, + "input_tokens_details": { + "cache_write_tokens": 0, + "cached_tokens": 0 + }, + "output_tokens": 33, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 943 + }, + "user": null + } + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a255e431aac0ec46-IAD", + "connection": "keep-alive", + "content-encoding": "gzip", + "content-type": "application/json", + "date": "Mon, 03 Aug 2026 14:07:58 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "3078", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149999072", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_3f0cdf9dfe9540749a081c9754580f64" + }, + "status": 200, + "statusText": "OK" + } + } + ], + "meta": { + "createdAt": "2026-08-03T13:15:23.755Z" + } +} diff --git a/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-latest-auto-hook.cassette.json b/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-latest-auto-hook.cassette.json new file mode 100644 index 000000000..8fa5ce7cb --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-latest-auto-hook.cassette.json @@ -0,0 +1,301 @@ +{ + "entries": [ + { + "callIndex": 0, + "id": "6eb3d53affc9bc89", + "matchKey": "POST api.openai.com/v1/responses", + "recordedAt": "2026-08-03T14:08:07.796Z", + "request": { + "body": { + "kind": "json", + "value": { + "input": [ + { + "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. If no element on the page matches the instruction, set `action` to null. Do not fabricate or guess an element — empty strings or placeholder values for elementId/description/method are not acceptable.", + "role": "system" + }, + { + "content": [ + { + "text": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown, hover, doubleClick, dragAndDrop. Remember that to users, buttons and links look the same in most cases.\n When choosing non-left click actions, provide right or middle as the argument\n If the action is completely unrelated to a potential action to be taken on the page, or no matching element exists, set `action` to null. Do not fabricate or guess an element.\n ONLY return one action. If multiple actions are relevant, return the most relevant one.\n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The user has provided the following variables to be used in the action: %customerEmail% \n\n Note that these are the variable names/keys, and not the actual variable values. \n\n To use the variables in the action, you must respond with the variable name inside the 'arguments' array. The variable name must be wrapped in percentage signs (eg, %variableNameHere%) so that it can be replaced with the actual variable value before the action is taken. \n\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + "type": "input_text" + } + ], + "role": "user" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "text": { + "format": { + "name": "response", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "action": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "arguments": { + "items": { + "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'. for example, '0-76' or '16-21'", + "pattern": "^\\d+-\\d+$", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Understudy interaction methods.", + "enum": [ + "click", + "fill", + "type", + "press", + "scrollTo", + "nextChunk", + "prevChunk", + "selectOptionFromDropdown", + "hover", + "doubleClick", + "dragAndDrop" + ], + "type": "string" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments" + ], + "type": "object" + }, + { + "type": "null" + } + ], + "description": "The element to act on. Return null if no element on the page matches the instruction — do NOT fabricate or guess an element, and never emit empty strings or placeholder values." + }, + "twoStep": { + "type": "boolean" + } + }, + "required": ["action", "twoStep"], + "type": "object" + }, + "strict": false, + "type": "json_schema" + } + } + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/responses" + }, + "response": { + "body": { + "kind": "json", + "value": { + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1785766087, + "created_at": 1785766086, + "error": null, + "frequency_penalty": 0, + "id": "resp_0b4a64254f9454b0006a70a0c69be4819eb3d432bf60949328", + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "metadata": {}, + "model": "gpt-4o-mini-2024-07-18", + "moderation": null, + "object": "response", + "output": [ + { + "content": [ + { + "annotations": [], + "logprobs": [], + "text": "{\"action\":{\"elementId\":\"0-14\",\"description\":\"button to add the Trail Backpack to the cart\",\"method\":\"click\",\"arguments\":[]},\"twoStep\":false}", + "type": "output_text" + } + ], + "id": "msg_0b4a64254f9454b0006a70a0c737e8819eb54426e21822910d", + "role": "assistant", + "status": "completed", + "type": "message" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": "[REDACTED]", + "prompt_cache_retention": "in_memory", + "reasoning": { + "context": null, + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "status": "completed", + "store": true, + "temperature": 1, + "text": { + "format": { + "description": null, + "name": "response", + "schema": { + "additionalProperties": false, + "properties": { + "action": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "arguments": { + "items": { + "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'. for example, '0-76' or '16-21'", + "pattern": "^\\d+-\\d+$", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Understudy interaction methods.", + "enum": [ + "click", + "fill", + "type", + "press", + "scrollTo", + "nextChunk", + "prevChunk", + "selectOptionFromDropdown", + "hover", + "doubleClick", + "dragAndDrop" + ], + "type": "string" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments" + ], + "type": "object" + }, + { + "type": "null" + } + ], + "description": "The element to act on. Return null if no element on the page matches the instruction — do NOT fabricate or guess an element, and never emit empty strings or placeholder values." + }, + "twoStep": { + "type": "boolean" + } + }, + "required": ["action", "twoStep"], + "type": "object" + }, + "strict": false, + "type": "json_schema" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tool_usage": { + "image_gen": { + "input_tokens": 0, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "output_tokens": 0, + "output_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "total_tokens": 0 + }, + "web_search": { + "num_requests": 0 + } + }, + "tools": [], + "top_logprobs": 0, + "top_p": 1, + "truncation": "disabled", + "usage": { + "input_tokens": 1162, + "input_tokens_details": { + "cache_write_tokens": 0, + "cached_tokens": 0 + }, + "output_tokens": 35, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 1197 + }, + "user": null + } + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a255e475de8eec46-IAD", + "connection": "keep-alive", + "content-encoding": "gzip", + "content-type": "application/json", + "date": "Mon, 03 Aug 2026 14:08:07 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "1298", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149998817", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_d09aac1c2427479cb7297a50cca3fef3" + }, + "status": 200, + "statusText": "OK" + } + } + ], + "meta": { + "createdAt": "2026-08-03T13:15:29.595Z" + } +} diff --git a/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-latest-wrapped.cassette.json b/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-latest-wrapped.cassette.json new file mode 100644 index 000000000..0e1b9e4a1 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-latest-wrapped.cassette.json @@ -0,0 +1,301 @@ +{ + "entries": [ + { + "callIndex": 0, + "id": "6eb3d53affc9bc89", + "matchKey": "POST api.openai.com/v1/responses", + "recordedAt": "2026-08-03T14:08:02.873Z", + "request": { + "body": { + "kind": "json", + "value": { + "input": [ + { + "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. If no element on the page matches the instruction, set `action` to null. Do not fabricate or guess an element — empty strings or placeholder values for elementId/description/method are not acceptable.", + "role": "system" + }, + { + "content": [ + { + "text": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown, hover, doubleClick, dragAndDrop. Remember that to users, buttons and links look the same in most cases.\n When choosing non-left click actions, provide right or middle as the argument\n If the action is completely unrelated to a potential action to be taken on the page, or no matching element exists, set `action` to null. Do not fabricate or guess an element.\n ONLY return one action. If multiple actions are relevant, return the most relevant one.\n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The user has provided the following variables to be used in the action: %customerEmail% \n\n Note that these are the variable names/keys, and not the actual variable values. \n\n To use the variables in the action, you must respond with the variable name inside the 'arguments' array. The variable name must be wrapped in percentage signs (eg, %variableNameHere%) so that it can be replaced with the actual variable value before the action is taken. \n\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + "type": "input_text" + } + ], + "role": "user" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "text": { + "format": { + "name": "response", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "action": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "arguments": { + "items": { + "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'. for example, '0-76' or '16-21'", + "pattern": "^\\d+-\\d+$", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Understudy interaction methods.", + "enum": [ + "click", + "fill", + "type", + "press", + "scrollTo", + "nextChunk", + "prevChunk", + "selectOptionFromDropdown", + "hover", + "doubleClick", + "dragAndDrop" + ], + "type": "string" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments" + ], + "type": "object" + }, + { + "type": "null" + } + ], + "description": "The element to act on. Return null if no element on the page matches the instruction — do NOT fabricate or guess an element, and never emit empty strings or placeholder values." + }, + "twoStep": { + "type": "boolean" + } + }, + "required": ["action", "twoStep"], + "type": "object" + }, + "strict": false, + "type": "json_schema" + } + } + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/responses" + }, + "response": { + "body": { + "kind": "json", + "value": { + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1785766082, + "created_at": 1785766081, + "error": null, + "frequency_penalty": 0, + "id": "resp_00de8c59cb3168d7006a70a0c1be30819e867cdca3fe5068f3", + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "metadata": {}, + "model": "gpt-4o-mini-2024-07-18", + "moderation": null, + "object": "response", + "output": [ + { + "content": [ + { + "annotations": [], + "logprobs": [], + "text": "{\"action\":{\"elementId\":\"0-14\",\"description\":\"button to add the Trail Backpack to the cart\",\"method\":\"click\",\"arguments\":[]},\"twoStep\":false}", + "type": "output_text" + } + ], + "id": "msg_00de8c59cb3168d7006a70a0c227ac819e8e72c5439c8a6f45", + "role": "assistant", + "status": "completed", + "type": "message" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": "[REDACTED]", + "prompt_cache_retention": "in_memory", + "reasoning": { + "context": null, + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "status": "completed", + "store": true, + "temperature": 1, + "text": { + "format": { + "description": null, + "name": "response", + "schema": { + "additionalProperties": false, + "properties": { + "action": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "arguments": { + "items": { + "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'. for example, '0-76' or '16-21'", + "pattern": "^\\d+-\\d+$", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Understudy interaction methods.", + "enum": [ + "click", + "fill", + "type", + "press", + "scrollTo", + "nextChunk", + "prevChunk", + "selectOptionFromDropdown", + "hover", + "doubleClick", + "dragAndDrop" + ], + "type": "string" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments" + ], + "type": "object" + }, + { + "type": "null" + } + ], + "description": "The element to act on. Return null if no element on the page matches the instruction — do NOT fabricate or guess an element, and never emit empty strings or placeholder values." + }, + "twoStep": { + "type": "boolean" + } + }, + "required": ["action", "twoStep"], + "type": "object" + }, + "strict": false, + "type": "json_schema" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tool_usage": { + "image_gen": { + "input_tokens": 0, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "output_tokens": 0, + "output_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "total_tokens": 0 + }, + "web_search": { + "num_requests": 0 + } + }, + "tools": [], + "top_logprobs": 0, + "top_p": 1, + "truncation": "disabled", + "usage": { + "input_tokens": 1162, + "input_tokens_details": { + "cache_write_tokens": 0, + "cached_tokens": 0 + }, + "output_tokens": 36, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 1198 + }, + "user": null + } + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a255e45a5f2aec46-IAD", + "connection": "keep-alive", + "content-encoding": "gzip", + "content-type": "application/json", + "date": "Mon, 03 Aug 2026 14:08:02 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "1117", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149998817", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_46aa82c7f73a4920afa4c9de7eb34818" + }, + "status": 200, + "statusText": "OK" + } + } + ], + "meta": { + "createdAt": "2026-08-03T13:15:27.044Z" + } +} diff --git a/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-wrapped.cassette.json b/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-wrapped.cassette.json new file mode 100644 index 000000000..02fc2f3b3 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__cassettes__/stagehand-v3-wrapped.cassette.json @@ -0,0 +1,246 @@ +{ + "entries": [ + { + "callIndex": 0, + "id": "0854b19ef5286997", + "matchKey": "POST api.openai.com/v1/responses", + "recordedAt": "2026-08-03T14:07:51.071Z", + "request": { + "body": { + "kind": "json", + "value": { + "input": [ + { + "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. Otherwise, return an empty object.", + "role": "system" + }, + { + "content": [ + { + "text": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown. Remember that to users, buttons and links look the same in most cases.\n If the action is completely unrelated to a potential action to be taken on the page, return an empty object. \n ONLY return one action. If multiple actions are relevant, return the most relevant one. \n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The following variables are available to use in the action: %customerEmail%. Fill the argument variables with the variable name.\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + "type": "input_text" + } + ], + "role": "user" + } + ], + "model": "gpt-4o-mini-2024-07-18", + "temperature": 0.1, + "text": { + "format": { + "name": "response", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "arguments": { + "items": { + "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'.", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Playwright interaction methods.", + "type": "string" + }, + "twoStep": { + "type": "boolean" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments", + "twoStep" + ], + "type": "object" + }, + "strict": false, + "type": "json_schema" + } + } + } + }, + "headers": {}, + "method": "POST", + "url": "https://api.openai.com/v1/responses" + }, + "response": { + "body": { + "kind": "json", + "value": { + "background": false, + "billing": { + "payer": "developer" + }, + "completed_at": 1785766070, + "created_at": 1785766070, + "error": null, + "frequency_penalty": 0, + "id": "resp_0741c1774d736e56006a70a0b620f8819d8a7b2a5c6fe45876", + "incomplete_details": null, + "instructions": null, + "max_output_tokens": null, + "max_tool_calls": null, + "metadata": {}, + "model": "gpt-4o-mini-2024-07-18", + "moderation": null, + "object": "response", + "output": [ + { + "content": [ + { + "annotations": [], + "logprobs": [], + "text": "{\"elementId\":\"0-14\",\"description\":\"Button to add the Trail Backpack to the cart\",\"method\":\"click\",\"arguments\":[],\"twoStep\":false}", + "type": "output_text" + } + ], + "id": "msg_0741c1774d736e56006a70a0b699a0819d9ec2bccb9c598159", + "role": "assistant", + "status": "completed", + "type": "message" + } + ], + "parallel_tool_calls": true, + "presence_penalty": 0, + "previous_response_id": null, + "prompt_cache_key": "[REDACTED]", + "prompt_cache_retention": "in_memory", + "reasoning": { + "context": null, + "effort": null, + "summary": null + }, + "safety_identifier": null, + "service_tier": "default", + "status": "completed", + "store": true, + "temperature": 0.1, + "text": { + "format": { + "description": null, + "name": "response", + "schema": { + "additionalProperties": false, + "properties": { + "arguments": { + "items": { + "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'.", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Playwright interaction methods.", + "type": "string" + }, + "twoStep": { + "type": "boolean" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments", + "twoStep" + ], + "type": "object" + }, + "strict": false, + "type": "json_schema" + }, + "verbosity": "medium" + }, + "tool_choice": "auto", + "tool_usage": { + "image_gen": { + "input_tokens": 0, + "input_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "output_tokens": 0, + "output_tokens_details": { + "image_tokens": 0, + "text_tokens": 0 + }, + "total_tokens": 0 + }, + "web_search": { + "num_requests": 0 + } + }, + "tools": [], + "top_logprobs": 0, + "top_p": 1, + "truncation": "disabled", + "usage": { + "input_tokens": 910, + "input_tokens_details": { + "cache_write_tokens": 0, + "cached_tokens": 0 + }, + "output_tokens": 33, + "output_tokens_details": { + "reasoning_tokens": 0 + }, + "total_tokens": 943 + }, + "user": null + } + }, + "headers": { + "access-control-expose-headers": "X-Request-ID, CF-Ray, CF-Ray", + "alt-svc": "h3=\":443\"; ma=86400", + "cf-cache-status": "DYNAMIC", + "cf-ray": "a255e4114eda5263-IAD", + "connection": "keep-alive", + "content-encoding": "gzip", + "content-type": "application/json", + "date": "Mon, 03 Aug 2026 14:07:51 GMT", + "openai-organization": "braintrust-data", + "openai-processing-ms": "928", + "openai-project": "proj_vsCSXafhhByzWOThMrJcZiw9", + "openai-version": "2020-10-01", + "server": "cloudflare", + "set-cookie": "[REDACTED]", + "strict-transport-security": "max-age=31536000; includeSubDomains; preload", + "transfer-encoding": "chunked", + "x-content-type-options": "nosniff", + "x-ratelimit-limit-requests": "30000", + "x-ratelimit-limit-tokens": "150000000", + "x-ratelimit-remaining-requests": "29999", + "x-ratelimit-remaining-tokens": "149999070", + "x-ratelimit-reset-requests": "2ms", + "x-ratelimit-reset-tokens": "0s", + "x-request-id": "req_0502db1677e744cbbda4707a49873e62" + }, + "status": 200, + "statusText": "OK" + } + } + ], + "meta": { + "createdAt": "2026-08-03T13:15:20.364Z" + } +} diff --git a/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-auto-hook.span-tree.json b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-auto-hook.span-tree.json new file mode 100644 index 000000000..d5e76f134 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-auto-hook.span-tree.json @@ -0,0 +1,302 @@ +{ + "span_tree": [ + { + "name": "stagehand-instrumentation-root", + "type": "task", + "children": [ + { + "name": "stagehand-act-operation", + "children": [ + { + "name": "stagehand.act", + "type": "task", + "children": [ + { + "name": "generateObject", + "type": "function", + "children": [], + "input": { + "allowSystemInMessages": true, + "messages": [ + { + "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. Otherwise, return an empty object.", + "role": "system" + }, + { + "content": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown. Remember that to users, buttons and links look the same in most cases.\n If the action is completely unrelated to a potential action to be taken on the page, return an empty object. \n ONLY return one action. If multiple actions are relevant, return the most relevant one. \n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The following variables are available to use in the action: %customerEmail%. Fill the argument variables with the variable name.\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + "role": "user" + } + ], + "model": { + "config": { + "fileIdPrefixes": [ + "file-" + ], + "provider": "openai.responses" + }, + "modelId": "gpt-4o-mini-2024-07-18", + "specificationVersion": "v2", + "supportedUrls": { + "application/pdf": [ + {} + ], + "image/*": [ + {} + ] + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "arguments": { + "items": { + "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'.", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Playwright interaction methods.", + "type": "string" + }, + "twoStep": { + "type": "boolean" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments", + "twoStep" + ], + "type": "object" + }, + "temperature": 0.1 + }, + "output": { + "finishReason": "stop", + "object": { + "arguments": [], + "description": "Button to add the Trail Backpack to the cart", + "elementId": "0-14", + "method": "click", + "twoStep": false + }, + "providerMetadata": { + "openai": { + "responseId": "", + "serviceTier": "default" + } + }, + "response": { + "body": "", + "id": "", + "modelId": "gpt-4o-mini-2024-07-18", + "timestamp": "" + }, + "usage": { + "cachedInputTokens": 0, + "inputTokens": 910, + "outputTokens": 33, + "reasoningTokens": 0, + "totalTokens": 943 + }, + "warnings": [] + }, + "metadata": { + "braintrust": { + "integration_name": "ai-sdk", + "sdk_language": "typescript" + }, + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai.responses" + } + } + ], + "input": { + "instruction": "Add the Trail Backpack to the cart" + }, + "output": { + "action_count": 1, + "methods": [ + "click" + ], + "success": true + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "act", + "testRunId": "" + } + }, + { + "name": "stagehand-extract-operation", + "children": [ + { + "name": "stagehand.extract", + "type": "task", + "children": [], + "input": { + "instruction": "Extract the cart item, quantity, and total", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "item": { + "type": "string" + }, + "quantity": { + "type": "number" + }, + "total": { + "type": "string" + } + }, + "required": [ + "item", + "quantity", + "total" + ], + "type": "object" + } + }, + "output": { + "item": "Trail Backpack", + "quantity": 1, + "total": "$129.00" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "stagehand.cache_status": "miss" + } + }, + { + "name": "stagehand.extract", + "type": "task", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "extract", + "testRunId": "" + } + }, + { + "name": "stagehand-observe-operation", + "children": [ + { + "name": "stagehand.observe", + "type": "task", + "children": [], + "input": { + "instruction": "Find the checkout button" + }, + "output": { + "action_count": 1, + "methods": [ + "click" + ] + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "observe", + "testRunId": "" + } + }, + { + "name": "stagehand-agent-operation", + "children": [ + { + "name": "stagehand.agent.execute", + "type": "task", + "children": [], + "input": { + "instruction": "Review the cart and prepare to check out. Stop before submitting payment." + }, + "output": { + "actions": [ + { + "task_completed": true, + "time_ms": 0, + "type": "observe" + } + ], + "completed": true, + "message": "Cart reviewed; checkout is ready for customer confirmation.", + "output": { + "checkoutReady": true, + "itemCount": 1 + }, + "success": true + }, + "metadata": { + "model": "openai/gpt-4o-mini-2024-07-18", + "stagehand.cache_status": "hit", + "stagehand.max_steps": 6, + "stagehand.mode": "dom", + "stagehand.streaming": false + }, + "metrics": { + "completion_reasoning_tokens": 2, + "completion_tokens": 5, + "prompt_cached_tokens": 3, + "prompt_tokens": 11, + "tokens": 16 + } + } + ], + "metadata": { + "operation": "agent", + "testRunId": "" + } + }, + { + "name": "stagehand-error-operation", + "children": [ + { + "name": "stagehand.act", + "type": "task", + "children": [], + "input": { + "instruction": "Apply the expired coupon code" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + }, + "error": "STAGEHAND_COUPON_NOT_FOUND" + } + ], + "metadata": { + "operation": "error", + "testRunId": "" + } + } + ], + "metadata": { + "scenario": "stagehand-instrumentation", + "testRunId": "" + } + } + ] +} diff --git a/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-auto-hook.span-tree.txt b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-auto-hook.span-tree.txt new file mode 100644 index 000000000..e9bbadc83 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-auto-hook.span-tree.txt @@ -0,0 +1,245 @@ +span_tree: +└── stagehand-instrumentation-root [task] + metadata: { + "scenario": "stagehand-instrumentation", + "testRunId": "" + } + ├── stagehand-act-operation + │ metadata: { + │ "operation": "act", + │ "testRunId": "" + │ } + │ └── stagehand.act [task] + │ input: { + │ "instruction": "Add the Trail Backpack to the cart" + │ } + │ output: { + │ "action_count": 1, + │ "methods": [ + │ "click" + │ ], + │ "success": true + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + │ └── generateObject [function] + │ input: { + │ "allowSystemInMessages": true, + │ "messages": [ + │ { + │ "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. Otherwise, return an empty object.", + │ "role": "system" + │ }, + │ { + │ "content": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown. Remember that to users, buttons and links look the same in most cases.\n If the action is completely unrelated to a potential action to be taken on the page, return an empty object. \n ONLY return one action. If multiple actions are relevant, return the most relevant one. \n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The following variables are available to use in the action: %customerEmail%. Fill the argument variables with the variable name.\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + │ "role": "user" + │ } + │ ], + │ "model": { + │ "config": { + │ "fileIdPrefixes": [ + │ "file-" + │ ], + │ "provider": "openai.responses" + │ }, + │ "modelId": "gpt-4o-mini-2024-07-18", + │ "specificationVersion": "v2", + │ "supportedUrls": { + │ "application/pdf": [ + │ {} + │ ], + │ "image/*": [ + │ {} + │ ] + │ } + │ }, + │ "schema": { + │ "$schema": "http://json-schema.org/draft-07/schema#", + │ "additionalProperties": false, + │ "properties": { + │ "arguments": { + │ "items": { + │ "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + │ "type": "string" + │ }, + │ "type": "array" + │ }, + │ "description": { + │ "description": "a description of the accessible element and its purpose", + │ "type": "string" + │ }, + │ "elementId": { + │ "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'.", + │ "type": "string" + │ }, + │ "method": { + │ "description": "the candidate method/action to interact with the element. Select one of the available Playwright interaction methods.", + │ "type": "string" + │ }, + │ "twoStep": { + │ "type": "boolean" + │ } + │ }, + │ "required": [ + │ "elementId", + │ "description", + │ "method", + │ "arguments", + │ "twoStep" + │ ], + │ "type": "object" + │ }, + │ "temperature": 0.1 + │ } + │ output: { + │ "finishReason": "stop", + │ "object": { + │ "arguments": [], + │ "description": "Button to add the Trail Backpack to the cart", + │ "elementId": "0-14", + │ "method": "click", + │ "twoStep": false + │ }, + │ "providerMetadata": { + │ "openai": { + │ "responseId": "", + │ "serviceTier": "default" + │ } + │ }, + │ "response": { + │ "body": "", + │ "id": "", + │ "modelId": "gpt-4o-mini-2024-07-18", + │ "timestamp": "" + │ }, + │ "usage": { + │ "cachedInputTokens": 0, + │ "inputTokens": 910, + │ "outputTokens": 33, + │ "reasoningTokens": 0, + │ "totalTokens": 943 + │ }, + │ "warnings": [] + │ } + │ metadata: { + │ "braintrust": { + │ "integration_name": "ai-sdk", + │ "sdk_language": "typescript" + │ }, + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai.responses" + │ } + ├── stagehand-extract-operation + │ metadata: { + │ "operation": "extract", + │ "testRunId": "" + │ } + │ ├── stagehand.extract [task] + │ │ input: { + │ │ "instruction": "Extract the cart item, quantity, and total", + │ │ "schema": { + │ │ "$schema": "http://json-schema.org/draft-07/schema#", + │ │ "additionalProperties": false, + │ │ "properties": { + │ │ "item": { + │ │ "type": "string" + │ │ }, + │ │ "quantity": { + │ │ "type": "number" + │ │ }, + │ │ "total": { + │ │ "type": "string" + │ │ } + │ │ }, + │ │ "required": [ + │ │ "item", + │ │ "quantity", + │ │ "total" + │ │ ], + │ │ "type": "object" + │ │ } + │ │ } + │ │ output: { + │ │ "item": "Trail Backpack", + │ │ "quantity": 1, + │ │ "total": "$129.00" + │ │ } + │ │ metadata: { + │ │ "model": "gpt-4o-mini-2024-07-18", + │ │ "stagehand.cache_status": "miss" + │ │ } + │ └── stagehand.extract [task] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + ├── stagehand-observe-operation + │ metadata: { + │ "operation": "observe", + │ "testRunId": "" + │ } + │ └── stagehand.observe [task] + │ input: { + │ "instruction": "Find the checkout button" + │ } + │ output: { + │ "action_count": 1, + │ "methods": [ + │ "click" + │ ] + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + ├── stagehand-agent-operation + │ metadata: { + │ "operation": "agent", + │ "testRunId": "" + │ } + │ └── stagehand.agent.execute [task] + │ input: { + │ "instruction": "Review the cart and prepare to check out. Stop before submitting payment." + │ } + │ output: { + │ "actions": [ + │ { + │ "task_completed": true, + │ "time_ms": 0, + │ "type": "observe" + │ } + │ ], + │ "completed": true, + │ "message": "Cart reviewed; checkout is ready for customer confirmation.", + │ "output": { + │ "checkoutReady": true, + │ "itemCount": 1 + │ }, + │ "success": true + │ } + │ metadata: { + │ "model": "openai/gpt-4o-mini-2024-07-18", + │ "stagehand.cache_status": "hit", + │ "stagehand.max_steps": 6, + │ "stagehand.mode": "dom", + │ "stagehand.streaming": false + │ } + │ metrics: { + │ "completion_reasoning_tokens": 2, + │ "completion_tokens": 5, + │ "prompt_cached_tokens": 3, + │ "prompt_tokens": 11, + │ "tokens": 16 + │ } + └── stagehand-error-operation + metadata: { + "operation": "error", + "testRunId": "" + } + └── stagehand.act [task] + input: { + "instruction": "Apply the expired coupon code" + } + metadata: { + "model": "gpt-4o-mini-2024-07-18" + } + error: "STAGEHAND_COUPON_NOT_FOUND" diff --git a/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-auto-hook.span-tree.json b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-auto-hook.span-tree.json new file mode 100644 index 000000000..53af88e08 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-auto-hook.span-tree.json @@ -0,0 +1,322 @@ +{ + "span_tree": [ + { + "name": "stagehand-instrumentation-root", + "type": "task", + "children": [ + { + "name": "stagehand-act-operation", + "children": [ + { + "name": "stagehand.act", + "type": "task", + "children": [ + { + "name": "generateObject", + "type": "function", + "children": [], + "input": { + "allowSystemInMessages": true, + "messages": [ + { + "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. If no element on the page matches the instruction, set `action` to null. Do not fabricate or guess an element — empty strings or placeholder values for elementId/description/method are not acceptable.", + "role": "system" + }, + { + "content": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown, hover, doubleClick, dragAndDrop. Remember that to users, buttons and links look the same in most cases.\n When choosing non-left click actions, provide right or middle as the argument\n If the action is completely unrelated to a potential action to be taken on the page, or no matching element exists, set `action` to null. Do not fabricate or guess an element.\n ONLY return one action. If multiple actions are relevant, return the most relevant one.\n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The user has provided the following variables to be used in the action: %customerEmail% \n\n Note that these are the variable names/keys, and not the actual variable values. \n\n To use the variables in the action, you must respond with the variable name inside the 'arguments' array. The variable name must be wrapped in percentage signs (eg, %variableNameHere%) so that it can be replaced with the actual variable value before the action is taken. \n\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + "role": "user" + } + ], + "model": { + "config": { + "fileIdPrefixes": [ + "file-" + ], + "provider": "openai.responses" + }, + "modelId": "gpt-4o-mini-2024-07-18", + "specificationVersion": "v2", + "supportedUrls": { + "application/pdf": [ + {} + ], + "image/*": [ + {} + ] + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "action": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "arguments": { + "items": "[Object]", + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'. for example, '0-76' or '16-21'", + "pattern": "^\\d+-\\d+$", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Understudy interaction methods.", + "enum": "[Object]", + "type": "string" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments" + ], + "type": "object" + }, + { + "type": "null" + } + ], + "description": "The element to act on. Return null if no element on the page matches the instruction — do NOT fabricate or guess an element, and never emit empty strings or placeholder values." + }, + "twoStep": { + "type": "boolean" + } + }, + "required": [ + "action", + "twoStep" + ], + "type": "object" + } + }, + "output": { + "finishReason": "stop", + "object": { + "action": { + "arguments": [], + "description": "button to add the Trail Backpack to the cart", + "elementId": "0-14", + "method": "click" + }, + "twoStep": false + }, + "providerMetadata": { + "openai": { + "responseId": "", + "serviceTier": "default", + "usage": { + "cacheWriteTokens": 0 + } + } + }, + "response": { + "body": "", + "id": "", + "modelId": "gpt-4o-mini-2024-07-18", + "timestamp": "" + }, + "usage": { + "cachedInputTokens": 0, + "inputTokens": 1162, + "outputTokens": 35, + "reasoningTokens": 0, + "totalTokens": 1197 + }, + "warnings": [] + }, + "metadata": { + "braintrust": { + "integration_name": "ai-sdk", + "sdk_language": "typescript" + }, + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai.responses" + } + } + ], + "input": { + "instruction": "Add the Trail Backpack to the cart" + }, + "output": { + "action_count": 1, + "methods": [ + "click" + ], + "success": true + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "act", + "testRunId": "" + } + }, + { + "name": "stagehand-extract-operation", + "children": [ + { + "name": "stagehand.extract", + "type": "task", + "children": [], + "input": { + "instruction": "Extract the cart item, quantity, and total", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "item": { + "type": "string" + }, + "quantity": { + "type": "number" + }, + "total": { + "type": "string" + } + }, + "required": [ + "item", + "quantity", + "total" + ], + "type": "object" + } + }, + "output": { + "item": "Trail Backpack", + "quantity": 1, + "total": "$129.00" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "stagehand.cache_status": "miss" + } + }, + { + "name": "stagehand.extract", + "type": "task", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "extract", + "testRunId": "" + } + }, + { + "name": "stagehand-observe-operation", + "children": [ + { + "name": "stagehand.observe", + "type": "task", + "children": [], + "input": { + "instruction": "Find the checkout button" + }, + "output": { + "action_count": 1, + "methods": [ + "click" + ] + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "observe", + "testRunId": "" + } + }, + { + "name": "stagehand-agent-operation", + "children": [ + { + "name": "stagehand.agent.execute", + "type": "task", + "children": [], + "input": { + "instruction": "Review the cart and prepare to check out. Stop before submitting payment." + }, + "output": { + "actions": [ + { + "task_completed": true, + "time_ms": 0, + "type": "observe" + } + ], + "completed": true, + "message": "Cart reviewed; checkout is ready for customer confirmation.", + "output": { + "checkoutReady": true, + "itemCount": 1 + }, + "success": true + }, + "metadata": { + "model": "openai/gpt-4o-mini-2024-07-18", + "stagehand.cache_status": "hit", + "stagehand.max_steps": 6, + "stagehand.mode": "dom", + "stagehand.streaming": false + }, + "metrics": { + "completion_reasoning_tokens": 2, + "completion_tokens": 5, + "prompt_cached_tokens": 3, + "prompt_tokens": 11, + "tokens": 16 + } + } + ], + "metadata": { + "operation": "agent", + "testRunId": "" + } + }, + { + "name": "stagehand-error-operation", + "children": [ + { + "name": "stagehand.act", + "type": "task", + "children": [], + "input": { + "instruction": "Apply the expired coupon code" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + }, + "error": "STAGEHAND_COUPON_NOT_FOUND" + } + ], + "metadata": { + "operation": "error", + "testRunId": "" + } + } + ], + "metadata": { + "scenario": "stagehand-instrumentation", + "testRunId": "" + } + } + ] +} diff --git a/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-auto-hook.span-tree.txt b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-auto-hook.span-tree.txt new file mode 100644 index 000000000..a4955ff0d --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-auto-hook.span-tree.txt @@ -0,0 +1,265 @@ +span_tree: +└── stagehand-instrumentation-root [task] + metadata: { + "scenario": "stagehand-instrumentation", + "testRunId": "" + } + ├── stagehand-act-operation + │ metadata: { + │ "operation": "act", + │ "testRunId": "" + │ } + │ └── stagehand.act [task] + │ input: { + │ "instruction": "Add the Trail Backpack to the cart" + │ } + │ output: { + │ "action_count": 1, + │ "methods": [ + │ "click" + │ ], + │ "success": true + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + │ └── generateObject [function] + │ input: { + │ "allowSystemInMessages": true, + │ "messages": [ + │ { + │ "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. If no element on the page matches the instruction, set `action` to null. Do not fabricate or guess an element — empty strings or placeholder values for elementId/description/method are not acceptable.", + │ "role": "system" + │ }, + │ { + │ "content": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown, hover, doubleClick, dragAndDrop. Remember that to users, buttons and links look the same in most cases.\n When choosing non-left click actions, provide right or middle as the argument\n If the action is completely unrelated to a potential action to be taken on the page, or no matching element exists, set `action` to null. Do not fabricate or guess an element.\n ONLY return one action. If multiple actions are relevant, return the most relevant one.\n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The user has provided the following variables to be used in the action: %customerEmail% \n\n Note that these are the variable names/keys, and not the actual variable values. \n\n To use the variables in the action, you must respond with the variable name inside the 'arguments' array. The variable name must be wrapped in percentage signs (eg, %variableNameHere%) so that it can be replaced with the actual variable value before the action is taken. \n\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + │ "role": "user" + │ } + │ ], + │ "model": { + │ "config": { + │ "fileIdPrefixes": [ + │ "file-" + │ ], + │ "provider": "openai.responses" + │ }, + │ "modelId": "gpt-4o-mini-2024-07-18", + │ "specificationVersion": "v2", + │ "supportedUrls": { + │ "application/pdf": [ + │ {} + │ ], + │ "image/*": [ + │ {} + │ ] + │ } + │ }, + │ "schema": { + │ "$schema": "http://json-schema.org/draft-07/schema#", + │ "additionalProperties": false, + │ "properties": { + │ "action": { + │ "anyOf": [ + │ { + │ "additionalProperties": false, + │ "properties": { + │ "arguments": { + │ "items": "[Object]", + │ "type": "array" + │ }, + │ "description": { + │ "description": "a description of the accessible element and its purpose", + │ "type": "string" + │ }, + │ "elementId": { + │ "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'. for example, '0-76' or '16-21'", + │ "pattern": "^\\d+-\\d+$", + │ "type": "string" + │ }, + │ "method": { + │ "description": "the candidate method/action to interact with the element. Select one of the available Understudy interaction methods.", + │ "enum": "[Object]", + │ "type": "string" + │ } + │ }, + │ "required": [ + │ "elementId", + │ "description", + │ "method", + │ "arguments" + │ ], + │ "type": "object" + │ }, + │ { + │ "type": "null" + │ } + │ ], + │ "description": "The element to act on. Return null if no element on the page matches the instruction — do NOT fabricate or guess an element, and never emit empty strings or placeholder values." + │ }, + │ "twoStep": { + │ "type": "boolean" + │ } + │ }, + │ "required": [ + │ "action", + │ "twoStep" + │ ], + │ "type": "object" + │ } + │ } + │ output: { + │ "finishReason": "stop", + │ "object": { + │ "action": { + │ "arguments": [], + │ "description": "button to add the Trail Backpack to the cart", + │ "elementId": "0-14", + │ "method": "click" + │ }, + │ "twoStep": false + │ }, + │ "providerMetadata": { + │ "openai": { + │ "responseId": "", + │ "serviceTier": "default", + │ "usage": { + │ "cacheWriteTokens": 0 + │ } + │ } + │ }, + │ "response": { + │ "body": "", + │ "id": "", + │ "modelId": "gpt-4o-mini-2024-07-18", + │ "timestamp": "" + │ }, + │ "usage": { + │ "cachedInputTokens": 0, + │ "inputTokens": 1162, + │ "outputTokens": 35, + │ "reasoningTokens": 0, + │ "totalTokens": 1197 + │ }, + │ "warnings": [] + │ } + │ metadata: { + │ "braintrust": { + │ "integration_name": "ai-sdk", + │ "sdk_language": "typescript" + │ }, + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai.responses" + │ } + ├── stagehand-extract-operation + │ metadata: { + │ "operation": "extract", + │ "testRunId": "" + │ } + │ ├── stagehand.extract [task] + │ │ input: { + │ │ "instruction": "Extract the cart item, quantity, and total", + │ │ "schema": { + │ │ "$schema": "http://json-schema.org/draft-07/schema#", + │ │ "additionalProperties": false, + │ │ "properties": { + │ │ "item": { + │ │ "type": "string" + │ │ }, + │ │ "quantity": { + │ │ "type": "number" + │ │ }, + │ │ "total": { + │ │ "type": "string" + │ │ } + │ │ }, + │ │ "required": [ + │ │ "item", + │ │ "quantity", + │ │ "total" + │ │ ], + │ │ "type": "object" + │ │ } + │ │ } + │ │ output: { + │ │ "item": "Trail Backpack", + │ │ "quantity": 1, + │ │ "total": "$129.00" + │ │ } + │ │ metadata: { + │ │ "model": "gpt-4o-mini-2024-07-18", + │ │ "stagehand.cache_status": "miss" + │ │ } + │ └── stagehand.extract [task] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + ├── stagehand-observe-operation + │ metadata: { + │ "operation": "observe", + │ "testRunId": "" + │ } + │ └── stagehand.observe [task] + │ input: { + │ "instruction": "Find the checkout button" + │ } + │ output: { + │ "action_count": 1, + │ "methods": [ + │ "click" + │ ] + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + ├── stagehand-agent-operation + │ metadata: { + │ "operation": "agent", + │ "testRunId": "" + │ } + │ └── stagehand.agent.execute [task] + │ input: { + │ "instruction": "Review the cart and prepare to check out. Stop before submitting payment." + │ } + │ output: { + │ "actions": [ + │ { + │ "task_completed": true, + │ "time_ms": 0, + │ "type": "observe" + │ } + │ ], + │ "completed": true, + │ "message": "Cart reviewed; checkout is ready for customer confirmation.", + │ "output": { + │ "checkoutReady": true, + │ "itemCount": 1 + │ }, + │ "success": true + │ } + │ metadata: { + │ "model": "openai/gpt-4o-mini-2024-07-18", + │ "stagehand.cache_status": "hit", + │ "stagehand.max_steps": 6, + │ "stagehand.mode": "dom", + │ "stagehand.streaming": false + │ } + │ metrics: { + │ "completion_reasoning_tokens": 2, + │ "completion_tokens": 5, + │ "prompt_cached_tokens": 3, + │ "prompt_tokens": 11, + │ "tokens": 16 + │ } + └── stagehand-error-operation + metadata: { + "operation": "error", + "testRunId": "" + } + └── stagehand.act [task] + input: { + "instruction": "Apply the expired coupon code" + } + metadata: { + "model": "gpt-4o-mini-2024-07-18" + } + error: "STAGEHAND_COUPON_NOT_FOUND" diff --git a/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-wrapped.span-tree.json b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-wrapped.span-tree.json new file mode 100644 index 000000000..f9d2e2082 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-wrapped.span-tree.json @@ -0,0 +1,322 @@ +{ + "span_tree": [ + { + "name": "stagehand-instrumentation-root", + "type": "task", + "children": [ + { + "name": "stagehand-act-operation", + "children": [ + { + "name": "stagehand.act", + "type": "task", + "children": [ + { + "name": "generateObject", + "type": "function", + "children": [], + "input": { + "allowSystemInMessages": true, + "messages": [ + { + "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. If no element on the page matches the instruction, set `action` to null. Do not fabricate or guess an element — empty strings or placeholder values for elementId/description/method are not acceptable.", + "role": "system" + }, + { + "content": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown, hover, doubleClick, dragAndDrop. Remember that to users, buttons and links look the same in most cases.\n When choosing non-left click actions, provide right or middle as the argument\n If the action is completely unrelated to a potential action to be taken on the page, or no matching element exists, set `action` to null. Do not fabricate or guess an element.\n ONLY return one action. If multiple actions are relevant, return the most relevant one.\n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The user has provided the following variables to be used in the action: %customerEmail% \n\n Note that these are the variable names/keys, and not the actual variable values. \n\n To use the variables in the action, you must respond with the variable name inside the 'arguments' array. The variable name must be wrapped in percentage signs (eg, %variableNameHere%) so that it can be replaced with the actual variable value before the action is taken. \n\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + "role": "user" + } + ], + "model": { + "config": { + "fileIdPrefixes": [ + "file-" + ], + "provider": "openai.responses" + }, + "modelId": "gpt-4o-mini-2024-07-18", + "specificationVersion": "v2", + "supportedUrls": { + "application/pdf": [ + {} + ], + "image/*": [ + {} + ] + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "action": { + "anyOf": [ + { + "additionalProperties": false, + "properties": { + "arguments": { + "items": "[Object]", + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'. for example, '0-76' or '16-21'", + "pattern": "^\\d+-\\d+$", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Understudy interaction methods.", + "enum": "[Object]", + "type": "string" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments" + ], + "type": "object" + }, + { + "type": "null" + } + ], + "description": "The element to act on. Return null if no element on the page matches the instruction — do NOT fabricate or guess an element, and never emit empty strings or placeholder values." + }, + "twoStep": { + "type": "boolean" + } + }, + "required": [ + "action", + "twoStep" + ], + "type": "object" + } + }, + "output": { + "finishReason": "stop", + "object": { + "action": { + "arguments": [], + "description": "button to add the Trail Backpack to the cart", + "elementId": "0-14", + "method": "click" + }, + "twoStep": false + }, + "providerMetadata": { + "openai": { + "responseId": "", + "serviceTier": "default", + "usage": { + "cacheWriteTokens": 0 + } + } + }, + "response": { + "body": "", + "id": "", + "modelId": "gpt-4o-mini-2024-07-18", + "timestamp": "" + }, + "usage": { + "cachedInputTokens": 0, + "inputTokens": 1162, + "outputTokens": 36, + "reasoningTokens": 0, + "totalTokens": 1198 + }, + "warnings": [] + }, + "metadata": { + "braintrust": { + "integration_name": "ai-sdk", + "sdk_language": "typescript" + }, + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai.responses" + } + } + ], + "input": { + "instruction": "Add the Trail Backpack to the cart" + }, + "output": { + "action_count": 1, + "methods": [ + "click" + ], + "success": true + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "act", + "testRunId": "" + } + }, + { + "name": "stagehand-extract-operation", + "children": [ + { + "name": "stagehand.extract", + "type": "task", + "children": [], + "input": { + "instruction": "Extract the cart item, quantity, and total", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "item": { + "type": "string" + }, + "quantity": { + "type": "number" + }, + "total": { + "type": "string" + } + }, + "required": [ + "item", + "quantity", + "total" + ], + "type": "object" + } + }, + "output": { + "item": "Trail Backpack", + "quantity": 1, + "total": "$129.00" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "stagehand.cache_status": "miss" + } + }, + { + "name": "stagehand.extract", + "type": "task", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "extract", + "testRunId": "" + } + }, + { + "name": "stagehand-observe-operation", + "children": [ + { + "name": "stagehand.observe", + "type": "task", + "children": [], + "input": { + "instruction": "Find the checkout button" + }, + "output": { + "action_count": 1, + "methods": [ + "click" + ] + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "observe", + "testRunId": "" + } + }, + { + "name": "stagehand-agent-operation", + "children": [ + { + "name": "stagehand.agent.execute", + "type": "task", + "children": [], + "input": { + "instruction": "Review the cart and prepare to check out. Stop before submitting payment." + }, + "output": { + "actions": [ + { + "task_completed": true, + "time_ms": 0, + "type": "observe" + } + ], + "completed": true, + "message": "Cart reviewed; checkout is ready for customer confirmation.", + "output": { + "checkoutReady": true, + "itemCount": 1 + }, + "success": true + }, + "metadata": { + "model": "openai/gpt-4o-mini-2024-07-18", + "stagehand.cache_status": "hit", + "stagehand.max_steps": 6, + "stagehand.mode": "dom", + "stagehand.streaming": false + }, + "metrics": { + "completion_reasoning_tokens": 2, + "completion_tokens": 5, + "prompt_cached_tokens": 3, + "prompt_tokens": 11, + "tokens": 16 + } + } + ], + "metadata": { + "operation": "agent", + "testRunId": "" + } + }, + { + "name": "stagehand-error-operation", + "children": [ + { + "name": "stagehand.act", + "type": "task", + "children": [], + "input": { + "instruction": "Apply the expired coupon code" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + }, + "error": "STAGEHAND_COUPON_NOT_FOUND" + } + ], + "metadata": { + "operation": "error", + "testRunId": "" + } + } + ], + "metadata": { + "scenario": "stagehand-instrumentation", + "testRunId": "" + } + } + ] +} diff --git a/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-wrapped.span-tree.txt b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-wrapped.span-tree.txt new file mode 100644 index 000000000..2a6d669c3 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-latest-wrapped.span-tree.txt @@ -0,0 +1,265 @@ +span_tree: +└── stagehand-instrumentation-root [task] + metadata: { + "scenario": "stagehand-instrumentation", + "testRunId": "" + } + ├── stagehand-act-operation + │ metadata: { + │ "operation": "act", + │ "testRunId": "" + │ } + │ └── stagehand.act [task] + │ input: { + │ "instruction": "Add the Trail Backpack to the cart" + │ } + │ output: { + │ "action_count": 1, + │ "methods": [ + │ "click" + │ ], + │ "success": true + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + │ └── generateObject [function] + │ input: { + │ "allowSystemInMessages": true, + │ "messages": [ + │ { + │ "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. If no element on the page matches the instruction, set `action` to null. Do not fabricate or guess an element — empty strings or placeholder values for elementId/description/method are not acceptable.", + │ "role": "system" + │ }, + │ { + │ "content": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown, hover, doubleClick, dragAndDrop. Remember that to users, buttons and links look the same in most cases.\n When choosing non-left click actions, provide right or middle as the argument\n If the action is completely unrelated to a potential action to be taken on the page, or no matching element exists, set `action` to null. Do not fabricate or guess an element.\n ONLY return one action. If multiple actions are relevant, return the most relevant one.\n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The user has provided the following variables to be used in the action: %customerEmail% \n\n Note that these are the variable names/keys, and not the actual variable values. \n\n To use the variables in the action, you must respond with the variable name inside the 'arguments' array. The variable name must be wrapped in percentage signs (eg, %variableNameHere%) so that it can be replaced with the actual variable value before the action is taken. \n\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + │ "role": "user" + │ } + │ ], + │ "model": { + │ "config": { + │ "fileIdPrefixes": [ + │ "file-" + │ ], + │ "provider": "openai.responses" + │ }, + │ "modelId": "gpt-4o-mini-2024-07-18", + │ "specificationVersion": "v2", + │ "supportedUrls": { + │ "application/pdf": [ + │ {} + │ ], + │ "image/*": [ + │ {} + │ ] + │ } + │ }, + │ "schema": { + │ "$schema": "http://json-schema.org/draft-07/schema#", + │ "additionalProperties": false, + │ "properties": { + │ "action": { + │ "anyOf": [ + │ { + │ "additionalProperties": false, + │ "properties": { + │ "arguments": { + │ "items": "[Object]", + │ "type": "array" + │ }, + │ "description": { + │ "description": "a description of the accessible element and its purpose", + │ "type": "string" + │ }, + │ "elementId": { + │ "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'. for example, '0-76' or '16-21'", + │ "pattern": "^\\d+-\\d+$", + │ "type": "string" + │ }, + │ "method": { + │ "description": "the candidate method/action to interact with the element. Select one of the available Understudy interaction methods.", + │ "enum": "[Object]", + │ "type": "string" + │ } + │ }, + │ "required": [ + │ "elementId", + │ "description", + │ "method", + │ "arguments" + │ ], + │ "type": "object" + │ }, + │ { + │ "type": "null" + │ } + │ ], + │ "description": "The element to act on. Return null if no element on the page matches the instruction — do NOT fabricate or guess an element, and never emit empty strings or placeholder values." + │ }, + │ "twoStep": { + │ "type": "boolean" + │ } + │ }, + │ "required": [ + │ "action", + │ "twoStep" + │ ], + │ "type": "object" + │ } + │ } + │ output: { + │ "finishReason": "stop", + │ "object": { + │ "action": { + │ "arguments": [], + │ "description": "button to add the Trail Backpack to the cart", + │ "elementId": "0-14", + │ "method": "click" + │ }, + │ "twoStep": false + │ }, + │ "providerMetadata": { + │ "openai": { + │ "responseId": "", + │ "serviceTier": "default", + │ "usage": { + │ "cacheWriteTokens": 0 + │ } + │ } + │ }, + │ "response": { + │ "body": "", + │ "id": "", + │ "modelId": "gpt-4o-mini-2024-07-18", + │ "timestamp": "" + │ }, + │ "usage": { + │ "cachedInputTokens": 0, + │ "inputTokens": 1162, + │ "outputTokens": 36, + │ "reasoningTokens": 0, + │ "totalTokens": 1198 + │ }, + │ "warnings": [] + │ } + │ metadata: { + │ "braintrust": { + │ "integration_name": "ai-sdk", + │ "sdk_language": "typescript" + │ }, + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai.responses" + │ } + ├── stagehand-extract-operation + │ metadata: { + │ "operation": "extract", + │ "testRunId": "" + │ } + │ ├── stagehand.extract [task] + │ │ input: { + │ │ "instruction": "Extract the cart item, quantity, and total", + │ │ "schema": { + │ │ "$schema": "http://json-schema.org/draft-07/schema#", + │ │ "additionalProperties": false, + │ │ "properties": { + │ │ "item": { + │ │ "type": "string" + │ │ }, + │ │ "quantity": { + │ │ "type": "number" + │ │ }, + │ │ "total": { + │ │ "type": "string" + │ │ } + │ │ }, + │ │ "required": [ + │ │ "item", + │ │ "quantity", + │ │ "total" + │ │ ], + │ │ "type": "object" + │ │ } + │ │ } + │ │ output: { + │ │ "item": "Trail Backpack", + │ │ "quantity": 1, + │ │ "total": "$129.00" + │ │ } + │ │ metadata: { + │ │ "model": "gpt-4o-mini-2024-07-18", + │ │ "stagehand.cache_status": "miss" + │ │ } + │ └── stagehand.extract [task] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + ├── stagehand-observe-operation + │ metadata: { + │ "operation": "observe", + │ "testRunId": "" + │ } + │ └── stagehand.observe [task] + │ input: { + │ "instruction": "Find the checkout button" + │ } + │ output: { + │ "action_count": 1, + │ "methods": [ + │ "click" + │ ] + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + ├── stagehand-agent-operation + │ metadata: { + │ "operation": "agent", + │ "testRunId": "" + │ } + │ └── stagehand.agent.execute [task] + │ input: { + │ "instruction": "Review the cart and prepare to check out. Stop before submitting payment." + │ } + │ output: { + │ "actions": [ + │ { + │ "task_completed": true, + │ "time_ms": 0, + │ "type": "observe" + │ } + │ ], + │ "completed": true, + │ "message": "Cart reviewed; checkout is ready for customer confirmation.", + │ "output": { + │ "checkoutReady": true, + │ "itemCount": 1 + │ }, + │ "success": true + │ } + │ metadata: { + │ "model": "openai/gpt-4o-mini-2024-07-18", + │ "stagehand.cache_status": "hit", + │ "stagehand.max_steps": 6, + │ "stagehand.mode": "dom", + │ "stagehand.streaming": false + │ } + │ metrics: { + │ "completion_reasoning_tokens": 2, + │ "completion_tokens": 5, + │ "prompt_cached_tokens": 3, + │ "prompt_tokens": 11, + │ "tokens": 16 + │ } + └── stagehand-error-operation + metadata: { + "operation": "error", + "testRunId": "" + } + └── stagehand.act [task] + input: { + "instruction": "Apply the expired coupon code" + } + metadata: { + "model": "gpt-4o-mini-2024-07-18" + } + error: "STAGEHAND_COUPON_NOT_FOUND" diff --git a/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-wrapped.span-tree.json b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-wrapped.span-tree.json new file mode 100644 index 000000000..d5e76f134 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-wrapped.span-tree.json @@ -0,0 +1,302 @@ +{ + "span_tree": [ + { + "name": "stagehand-instrumentation-root", + "type": "task", + "children": [ + { + "name": "stagehand-act-operation", + "children": [ + { + "name": "stagehand.act", + "type": "task", + "children": [ + { + "name": "generateObject", + "type": "function", + "children": [], + "input": { + "allowSystemInMessages": true, + "messages": [ + { + "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. Otherwise, return an empty object.", + "role": "system" + }, + { + "content": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown. Remember that to users, buttons and links look the same in most cases.\n If the action is completely unrelated to a potential action to be taken on the page, return an empty object. \n ONLY return one action. If multiple actions are relevant, return the most relevant one. \n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The following variables are available to use in the action: %customerEmail%. Fill the argument variables with the variable name.\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + "role": "user" + } + ], + "model": { + "config": { + "fileIdPrefixes": [ + "file-" + ], + "provider": "openai.responses" + }, + "modelId": "gpt-4o-mini-2024-07-18", + "specificationVersion": "v2", + "supportedUrls": { + "application/pdf": [ + {} + ], + "image/*": [ + {} + ] + } + }, + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "arguments": { + "items": { + "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + "type": "string" + }, + "type": "array" + }, + "description": { + "description": "a description of the accessible element and its purpose", + "type": "string" + }, + "elementId": { + "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'.", + "type": "string" + }, + "method": { + "description": "the candidate method/action to interact with the element. Select one of the available Playwright interaction methods.", + "type": "string" + }, + "twoStep": { + "type": "boolean" + } + }, + "required": [ + "elementId", + "description", + "method", + "arguments", + "twoStep" + ], + "type": "object" + }, + "temperature": 0.1 + }, + "output": { + "finishReason": "stop", + "object": { + "arguments": [], + "description": "Button to add the Trail Backpack to the cart", + "elementId": "0-14", + "method": "click", + "twoStep": false + }, + "providerMetadata": { + "openai": { + "responseId": "", + "serviceTier": "default" + } + }, + "response": { + "body": "", + "id": "", + "modelId": "gpt-4o-mini-2024-07-18", + "timestamp": "" + }, + "usage": { + "cachedInputTokens": 0, + "inputTokens": 910, + "outputTokens": 33, + "reasoningTokens": 0, + "totalTokens": 943 + }, + "warnings": [] + }, + "metadata": { + "braintrust": { + "integration_name": "ai-sdk", + "sdk_language": "typescript" + }, + "model": "gpt-4o-mini-2024-07-18", + "provider": "openai.responses" + } + } + ], + "input": { + "instruction": "Add the Trail Backpack to the cart" + }, + "output": { + "action_count": 1, + "methods": [ + "click" + ], + "success": true + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "act", + "testRunId": "" + } + }, + { + "name": "stagehand-extract-operation", + "children": [ + { + "name": "stagehand.extract", + "type": "task", + "children": [], + "input": { + "instruction": "Extract the cart item, quantity, and total", + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "item": { + "type": "string" + }, + "quantity": { + "type": "number" + }, + "total": { + "type": "string" + } + }, + "required": [ + "item", + "quantity", + "total" + ], + "type": "object" + } + }, + "output": { + "item": "Trail Backpack", + "quantity": 1, + "total": "$129.00" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18", + "stagehand.cache_status": "miss" + } + }, + { + "name": "stagehand.extract", + "type": "task", + "children": [], + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "extract", + "testRunId": "" + } + }, + { + "name": "stagehand-observe-operation", + "children": [ + { + "name": "stagehand.observe", + "type": "task", + "children": [], + "input": { + "instruction": "Find the checkout button" + }, + "output": { + "action_count": 1, + "methods": [ + "click" + ] + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + } + } + ], + "metadata": { + "operation": "observe", + "testRunId": "" + } + }, + { + "name": "stagehand-agent-operation", + "children": [ + { + "name": "stagehand.agent.execute", + "type": "task", + "children": [], + "input": { + "instruction": "Review the cart and prepare to check out. Stop before submitting payment." + }, + "output": { + "actions": [ + { + "task_completed": true, + "time_ms": 0, + "type": "observe" + } + ], + "completed": true, + "message": "Cart reviewed; checkout is ready for customer confirmation.", + "output": { + "checkoutReady": true, + "itemCount": 1 + }, + "success": true + }, + "metadata": { + "model": "openai/gpt-4o-mini-2024-07-18", + "stagehand.cache_status": "hit", + "stagehand.max_steps": 6, + "stagehand.mode": "dom", + "stagehand.streaming": false + }, + "metrics": { + "completion_reasoning_tokens": 2, + "completion_tokens": 5, + "prompt_cached_tokens": 3, + "prompt_tokens": 11, + "tokens": 16 + } + } + ], + "metadata": { + "operation": "agent", + "testRunId": "" + } + }, + { + "name": "stagehand-error-operation", + "children": [ + { + "name": "stagehand.act", + "type": "task", + "children": [], + "input": { + "instruction": "Apply the expired coupon code" + }, + "metadata": { + "model": "gpt-4o-mini-2024-07-18" + }, + "error": "STAGEHAND_COUPON_NOT_FOUND" + } + ], + "metadata": { + "operation": "error", + "testRunId": "" + } + } + ], + "metadata": { + "scenario": "stagehand-instrumentation", + "testRunId": "" + } + } + ] +} diff --git a/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-wrapped.span-tree.txt b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-wrapped.span-tree.txt new file mode 100644 index 000000000..e9bbadc83 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/__snapshots__/stagehand-v3-wrapped.span-tree.txt @@ -0,0 +1,245 @@ +span_tree: +└── stagehand-instrumentation-root [task] + metadata: { + "scenario": "stagehand-instrumentation", + "testRunId": "" + } + ├── stagehand-act-operation + │ metadata: { + │ "operation": "act", + │ "testRunId": "" + │ } + │ └── stagehand.act [task] + │ input: { + │ "instruction": "Add the Trail Backpack to the cart" + │ } + │ output: { + │ "action_count": 1, + │ "methods": [ + │ "click" + │ ], + │ "success": true + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + │ └── generateObject [function] + │ input: { + │ "allowSystemInMessages": true, + │ "messages": [ + │ { + │ "content": " You are helping the user automate the browser by finding elements based on what action the user wants to take on the page You will be given: 1. a user defined instruction about what action to take 2. a hierarchical accessibility tree showing the semantic structure of the page. The tree is a hybrid of the DOM and the accessibility tree. Return the element that matches the instruction if it exists. Otherwise, return an empty object.", + │ "role": "system" + │ }, + │ { + │ "content": "instruction: Find the most relevant element to perform an action on given the following action: Add the Trail Backpack to the cart. \n IF AND ONLY IF the action EXPLICITLY includes the word 'dropdown' and implies choosing/selecting an option from a dropdown, ignore the 'General Instructions' section, and follow the 'Dropdown Specific Instructions' section carefully.\n \n General Instructions: \n Provide an action for this element such as click, fill, type, press, scrollTo, nextChunk, prevChunk, selectOptionFromDropdown. Remember that to users, buttons and links look the same in most cases.\n If the action is completely unrelated to a potential action to be taken on the page, return an empty object. \n ONLY return one action. If multiple actions are relevant, return the most relevant one. \n If the user is asking to scroll to a position on the page, e.g., 'halfway' or 0.75, etc, you must return the argument formatted as the correct percentage, e.g., '50%' or '75%', etc.\n If the user is asking to scroll to the next chunk/previous chunk, choose the nextChunk/prevChunk method. No arguments are required here.\n If the action implies a key press, e.g., 'press enter', 'press a', 'press space', etc., always choose the press method with the appropriate key as argument — e.g. 'a', 'Enter', 'Space'. Do not choose a click action on an on-screen keyboard. Capitalize the first character like 'Enter', 'Tab', 'Escape' only for special keys. \n \n Dropdown Specific Instructions:\n For interacting with dropdowns, there are two specific cases that you need to handle. \n \n CASE 1: the element is a 'select' element. \n - choose the selectOptionFromDropdown method,\n - set the argument to the exact text of the option that should be selected,\n - set twoStep to false.\n CASE 2: the element is NOT a 'select' element:\n - do not attempt to directly choose the element from the dropdown. You will need to click to expand the dropdown first. You will achieve this by following these instructions:\n - choose the node that most closely corresponds to the given instruction EVEN if it is a 'StaticText' element, or otherwise does not appear to be interactable. \n - choose the 'click' method\n - set twoStep to true.\n The following variables are available to use in the action: %customerEmail%. Fill the argument variables with the variable name.\nAccessibility Tree: \n[0-2] RootWebArea: Trail Supply\n [0-4] scrollable, html\n [0-9] main\n [0-10] heading: Trail Backpack\n [0-12] paragraph\n [0-13] StaticText: $129.00\n [0-14] button: Add to cart\n [0-16] region: Shopping cart\n [0-17] paragraph\n [0-18] StaticText: Cart is empty\n [0-19] button: Checkout\n", + │ "role": "user" + │ } + │ ], + │ "model": { + │ "config": { + │ "fileIdPrefixes": [ + │ "file-" + │ ], + │ "provider": "openai.responses" + │ }, + │ "modelId": "gpt-4o-mini-2024-07-18", + │ "specificationVersion": "v2", + │ "supportedUrls": { + │ "application/pdf": [ + │ {} + │ ], + │ "image/*": [ + │ {} + │ ] + │ } + │ }, + │ "schema": { + │ "$schema": "http://json-schema.org/draft-07/schema#", + │ "additionalProperties": false, + │ "properties": { + │ "arguments": { + │ "items": { + │ "description": "the arguments to pass to the method. For example, for a click, the arguments are empty, but for a fill, the arguments are the value to fill in.", + │ "type": "string" + │ }, + │ "type": "array" + │ }, + │ "description": { + │ "description": "a description of the accessible element and its purpose", + │ "type": "string" + │ }, + │ "elementId": { + │ "description": "the ID string associated with the element. Never include surrounding square brackets. This field must follow the format of 'number-number'.", + │ "type": "string" + │ }, + │ "method": { + │ "description": "the candidate method/action to interact with the element. Select one of the available Playwright interaction methods.", + │ "type": "string" + │ }, + │ "twoStep": { + │ "type": "boolean" + │ } + │ }, + │ "required": [ + │ "elementId", + │ "description", + │ "method", + │ "arguments", + │ "twoStep" + │ ], + │ "type": "object" + │ }, + │ "temperature": 0.1 + │ } + │ output: { + │ "finishReason": "stop", + │ "object": { + │ "arguments": [], + │ "description": "Button to add the Trail Backpack to the cart", + │ "elementId": "0-14", + │ "method": "click", + │ "twoStep": false + │ }, + │ "providerMetadata": { + │ "openai": { + │ "responseId": "", + │ "serviceTier": "default" + │ } + │ }, + │ "response": { + │ "body": "", + │ "id": "", + │ "modelId": "gpt-4o-mini-2024-07-18", + │ "timestamp": "" + │ }, + │ "usage": { + │ "cachedInputTokens": 0, + │ "inputTokens": 910, + │ "outputTokens": 33, + │ "reasoningTokens": 0, + │ "totalTokens": 943 + │ }, + │ "warnings": [] + │ } + │ metadata: { + │ "braintrust": { + │ "integration_name": "ai-sdk", + │ "sdk_language": "typescript" + │ }, + │ "model": "gpt-4o-mini-2024-07-18", + │ "provider": "openai.responses" + │ } + ├── stagehand-extract-operation + │ metadata: { + │ "operation": "extract", + │ "testRunId": "" + │ } + │ ├── stagehand.extract [task] + │ │ input: { + │ │ "instruction": "Extract the cart item, quantity, and total", + │ │ "schema": { + │ │ "$schema": "http://json-schema.org/draft-07/schema#", + │ │ "additionalProperties": false, + │ │ "properties": { + │ │ "item": { + │ │ "type": "string" + │ │ }, + │ │ "quantity": { + │ │ "type": "number" + │ │ }, + │ │ "total": { + │ │ "type": "string" + │ │ } + │ │ }, + │ │ "required": [ + │ │ "item", + │ │ "quantity", + │ │ "total" + │ │ ], + │ │ "type": "object" + │ │ } + │ │ } + │ │ output: { + │ │ "item": "Trail Backpack", + │ │ "quantity": 1, + │ │ "total": "$129.00" + │ │ } + │ │ metadata: { + │ │ "model": "gpt-4o-mini-2024-07-18", + │ │ "stagehand.cache_status": "miss" + │ │ } + │ └── stagehand.extract [task] + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + ├── stagehand-observe-operation + │ metadata: { + │ "operation": "observe", + │ "testRunId": "" + │ } + │ └── stagehand.observe [task] + │ input: { + │ "instruction": "Find the checkout button" + │ } + │ output: { + │ "action_count": 1, + │ "methods": [ + │ "click" + │ ] + │ } + │ metadata: { + │ "model": "gpt-4o-mini-2024-07-18" + │ } + ├── stagehand-agent-operation + │ metadata: { + │ "operation": "agent", + │ "testRunId": "" + │ } + │ └── stagehand.agent.execute [task] + │ input: { + │ "instruction": "Review the cart and prepare to check out. Stop before submitting payment." + │ } + │ output: { + │ "actions": [ + │ { + │ "task_completed": true, + │ "time_ms": 0, + │ "type": "observe" + │ } + │ ], + │ "completed": true, + │ "message": "Cart reviewed; checkout is ready for customer confirmation.", + │ "output": { + │ "checkoutReady": true, + │ "itemCount": 1 + │ }, + │ "success": true + │ } + │ metadata: { + │ "model": "openai/gpt-4o-mini-2024-07-18", + │ "stagehand.cache_status": "hit", + │ "stagehand.max_steps": 6, + │ "stagehand.mode": "dom", + │ "stagehand.streaming": false + │ } + │ metrics: { + │ "completion_reasoning_tokens": 2, + │ "completion_tokens": 5, + │ "prompt_cached_tokens": 3, + │ "prompt_tokens": 11, + │ "tokens": 16 + │ } + └── stagehand-error-operation + metadata: { + "operation": "error", + "testRunId": "" + } + └── stagehand.act [task] + input: { + "instruction": "Apply the expired coupon code" + } + metadata: { + "model": "gpt-4o-mini-2024-07-18" + } + error: "STAGEHAND_COUPON_NOT_FOUND" diff --git a/e2e/scenarios/stagehand-instrumentation/assertions.ts b/e2e/scenarios/stagehand-instrumentation/assertions.ts new file mode 100644 index 000000000..83f17f999 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/assertions.ts @@ -0,0 +1,204 @@ +import { beforeAll, describe, expect, test } from "vitest"; +import type { CapturedLogEvent } from "../../helpers/mock-braintrust-server"; +import { resolveFileSnapshotPath } from "../../helpers/file-snapshot"; +import { + effectiveScenarioTimeoutMs, + withScenarioHarness, + type ScenarioRunContext, +} from "../../helpers/scenario-harness"; +import { + matchSpanTreeSnapshot, + spanTreeFields, + type SpanTreeEntry, +} from "../../helpers/span-tree"; +import { findAllSpans, findLatestSpan } from "../../helpers/trace-selectors"; +import { + ADD_TO_CART_INSTRUCTION, + MODEL_NAME, + ROOT_NAME, + SCENARIO_NAME, + SECRET, +} from "./scenario.impl.mjs"; + +type RunScenario = (harness: { + runNodeScenarioDir: (options: { + entry: string; + env: Record; + nodeArgs: string[]; + runContext?: ScenarioRunContext; + scenarioDir: string; + timeoutMs: number; + }) => Promise; + runScenarioDir: (options: { + entry: string; + env: Record; + runContext?: ScenarioRunContext; + scenarioDir: string; + timeoutMs: number; + }) => Promise; +}) => Promise; + +const INCLUDED_NAMES = new Set([ + ROOT_NAME, + "stagehand-act-operation", + "stagehand-extract-operation", + "stagehand-observe-operation", + "stagehand-agent-operation", + "stagehand-error-operation", + "stagehand.act", + "stagehand.extract", + "stagehand.observe", + "stagehand.agent.execute", + "generateObject", +]); + +function summarize(events: CapturedLogEvent[]): SpanTreeEntry[] { + return [...INCLUDED_NAMES] + .flatMap((name) => findAllSpans(events, name)) + .map((event) => ({ event, fields: spanTreeFields(event) })); +} + +export function defineStagehandInstrumentationAssertions(options: { + name: string; + runScenario: RunScenario; + snapshotName: string; + testFileUrl: string; + timeoutMs: number; +}): void { + const snapshotPath = resolveFileSnapshotPath( + options.testFileUrl, + `${options.snapshotName}.span-tree.json`, + ); + const timeoutMs = effectiveScenarioTimeoutMs(options.timeoutMs); + const testConfig = { timeout: timeoutMs }; + + describe(options.name, () => { + let events: CapturedLogEvent[] = []; + let setupError: string | undefined; + + beforeAll(async () => { + try { + await withScenarioHarness(async (harness) => { + await options.runScenario(harness); + events = harness.events(); + }); + } catch (error) { + setupError = error instanceof Error ? error.message : String(error); + } + }, timeoutMs); + + test("captures the bounded Stagehand contract", testConfig, () => { + expect(setupError).toBeUndefined(); + const root = findLatestSpan(events, ROOT_NAME); + expect(root?.row.metadata).toMatchObject({ scenario: SCENARIO_NAME }); + + const stagehandSpans = [ + "stagehand.act", + "stagehand.extract", + "stagehand.observe", + "stagehand.agent.execute", + ].flatMap((name) => findAllSpans(events, name)); + expect(stagehandSpans.map((event) => event.span.name).sort()).toEqual([ + "stagehand.act", + "stagehand.act", + "stagehand.agent.execute", + "stagehand.extract", + "stagehand.extract", + "stagehand.observe", + ]); + expect(stagehandSpans.every((event) => event.span.type === "task")).toBe( + true, + ); + expect(stagehandSpans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + input: { instruction: ADD_TO_CART_INSTRUCTION }, + output: { + action_count: 1, + methods: ["click"], + success: true, + }, + }), + expect.objectContaining({ + input: expect.objectContaining({ + instruction: "Extract the cart item, quantity, and total", + }), + output: { + item: "Trail Backpack", + quantity: 1, + total: "$129.00", + }, + }), + expect.objectContaining({ + input: { instruction: "Find the checkout button" }, + output: { action_count: 1, methods: ["click"] }, + }), + expect.objectContaining({ + input: { + instruction: + "Review the cart and prepare to check out. Stop before submitting payment.", + }, + output: expect.objectContaining({ + completed: true, + output: { checkoutReady: true, itemCount: 1 }, + success: true, + }), + }), + ]), + ); + + const rawExtract = stagehandSpans.find( + (event) => + event.span.name === "stagehand.extract" && event.output === undefined, + ); + expect(rawExtract?.output).toBeUndefined(); + const failedCoupon = stagehandSpans.find( + (event) => + event.span.name === "stagehand.act" && + event.input && + typeof event.input === "object" && + "instruction" in event.input && + event.input.instruction === "Apply the expired coupon code", + ); + expect(failedCoupon?.row.error).toBe("STAGEHAND_COUPON_NOT_FOUND"); + expect(JSON.stringify(stagehandSpans)).not.toContain(SECRET); + + const agent = stagehandSpans.find( + (event) => event.span.name === "stagehand.agent.execute", + ); + expect(agent?.metrics).toMatchObject({ + completion_reasoning_tokens: 2, + completion_tokens: 5, + prompt_cached_tokens: 3, + prompt_tokens: 11, + tokens: 16, + }); + expect(agent?.row.metadata).toMatchObject({ + model: `openai/${MODEL_NAME}`, + "stagehand.cache_status": "hit", + "stagehand.max_steps": 6, + "stagehand.mode": "dom", + "stagehand.streaming": false, + }); + expect(agent?.metrics).not.toHaveProperty("inference_time_ms"); + + const modelSpan = findAllSpans(events, "generateObject")[0]; + expect(modelSpan).toMatchObject({ + input: { model: { modelId: MODEL_NAME } }, + }); + const modelParent = stagehandSpans.find( + (event) => + event.span.name === "stagehand.act" && + modelSpan?.span.parentIds.includes(event.span.id ?? ""), + ); + expect(modelParent?.input).toEqual({ + instruction: ADD_TO_CART_INSTRUCTION, + }); + }); + + test("matches the span tree", testConfig, async () => { + expect(setupError).toBeUndefined(); + await matchSpanTreeSnapshot(summarize(events), snapshotPath); + }); + }); +} diff --git a/e2e/scenarios/stagehand-instrumentation/package.json b/e2e/scenarios/stagehand-instrumentation/package.json new file mode 100644 index 000000000..bf81999d6 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/package.json @@ -0,0 +1,21 @@ +{ + "private": true, + "type": "module", + "dependencies": { + "@ai-sdk/openai": "2.0.117", + "ai": "5.0.225", + "stagehand-v3": "npm:@browserbasehq/stagehand@3.0.0", + "stagehand-v3-latest": "npm:@browserbasehq/stagehand@3.7.1", + "zod": "3.25.76" + }, + "braintrustScenario": { + "bump": { + "dependencies": { + "stagehand-v3-latest": { + "package": "@browserbasehq/stagehand", + "range": "3" + } + } + } + } +} diff --git a/e2e/scenarios/stagehand-instrumentation/pnpm-lock.yaml b/e2e/scenarios/stagehand-instrumentation/pnpm-lock.yaml new file mode 100644 index 000000000..4a52abbf5 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/pnpm-lock.yaml @@ -0,0 +1,2996 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@ai-sdk/openai': + specifier: 2.0.117 + version: 2.0.117(zod@3.25.76) + ai: + specifier: 5.0.225 + version: 5.0.225(zod@3.25.76) + stagehand-v3: + specifier: npm:@browserbasehq/stagehand@3.0.0 + version: '@browserbasehq/stagehand@3.0.0(@cfworker/json-schema@4.1.1)(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(deepmerge@4.3.1)(dotenv@16.6.1)(zod@3.25.76)' + stagehand-v3-latest: + specifier: npm:@browserbasehq/stagehand@3.7.1 + version: '@browserbasehq/stagehand@3.7.1(@cfworker/json-schema@4.1.1)(patchright-core@1.61.1)(playwright-core@1.62.1)(zod@3.25.76)' + zod: + specifier: 3.25.76 + version: 3.25.76 + +packages: + + '@ai-sdk/amazon-bedrock@3.0.112': + resolution: {integrity: sha512-nvBRPced+MMz4zMScNh/fDo6bPAMr0zJKJvxIdRST4E4ATc+cUPcmOW++IhgkkaW1WfCMNPqEX843BWQXWReXQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/anthropic@2.0.92': + resolution: {integrity: sha512-MlIQpuOnchlroR93Dq2GNw12jh/SAVjuFARXWRF/2SfowMv3J2fvPBUzA60waoQivZlf3XkzQ2nxSwer7dnh2g==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/azure@2.0.122': + resolution: {integrity: sha512-fLpsBWOAgl6nzJgty6mmkAZ43ROYxi1flwBRbHKb0snFx1Kk62jepEy5/R+F5w+XOFFg3gt3Cl1Zom2qLz/RoA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/cerebras@1.0.52': + resolution: {integrity: sha512-WP53ERGpxsbvyTgza2Sgm8mLfTDBuYvth+lsqETuLKFCVr8pcqsmhAYnxFvkh/Qq89vpuVxWVYkgwLmVhAV+LA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/deepseek@1.0.49': + resolution: {integrity: sha512-Q+yHWvapEyiQlFRbopeyg4Eo0BQU8FAiWG0pXKOTTUI93zao2qiNpS6cJX87kUc9bUNaVqZqeSCGRGAWeuX0fw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/gateway@2.0.124': + resolution: {integrity: sha512-qrzQl7/Extk7GRPNYqOq7l/3RkO04pNW2t8Jx3Z6uNctxW/xV26mJ6LxbxAy/daqY7QGMaY/SThpH17GoqFByw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google-vertex@3.0.159': + resolution: {integrity: sha512-wAgwJKnJ2auzZOB3X8HcgVypR+44cej1bQtktkefMxFKIvNfitaCsF3L4EkUNRwpPcxf176H4MfJDx0oNl4VxA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/google@2.0.86': + resolution: {integrity: sha512-MudH/IVlfPu6Z8KJBUEhGgfzRJ1WYfaFikshlYxbrwbKYQXXCnx17cts2rIO5RZwJ9Uq2trM8q9WYZyZCOZ6MA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/groq@2.0.47': + resolution: {integrity: sha512-af0K6NfIRom5W1+WjOuIgLmIvAx3LikuqvJgkGZy6/QQaocSOWFz/Bfnk0oPUFz4q8Ace9qAVJ6q+HTfmQpBIQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/mistral@2.0.41': + resolution: {integrity: sha512-BIOG4XDM9+BmHpDL6u2ZoryBorA8l+YSEHcSlemFSyIEDp8qefD6ElmeVkTrDoR3vP+j5VG6qVLQxDqzwshOmg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai-compatible@1.0.47': + resolution: {integrity: sha512-6Esz5WZmm1YckHEHUtDurGEjoTbEI1CkcfUkL2Fu1rngWhrnW98hhaj3bnjuqwkG4U1SNwVEIfucMS3ZGADEiQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/openai@2.0.117': + resolution: {integrity: sha512-fqr2OwpegPlS58NUAat4+6cAOR/x1askt5dXKY1rCjDyhHJYxm2OZM9fZnG9I717ny4uRSp2mhsMio2uthA0Pg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/perplexity@2.0.37': + resolution: {integrity: sha512-MsEY2KCLpkEz/zkSCIfkVZ8/gTs8xDCdPC8yMzuDZnaKBrfk8JoOsLtsZP6YWC4IsLTwLtOo50/J4VYLzsd/sw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@3.0.31': + resolution: {integrity: sha512-hlxLxqbdV+f7gBpL2SbLL+UhjV+DfUpJ28J4vSHekVV9fiTRrhqcd/ewJwaVbR/ahtLCGJPAVYzj+yJzaddc6g==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@2.0.3': + resolution: {integrity: sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww==} + engines: {node: '>=18'} + + '@ai-sdk/togetherai@1.0.51': + resolution: {integrity: sha512-lHOdtW3yt610MUK28kdWCfNVlcLLQeDaFbc8s60T0tEY+3Xu4EJbHf40nFKW/lGKLF5m4xsQFV4jtG1tvlBptw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/xai@2.0.84': + resolution: {integrity: sha512-lT/gAHQfgcSBQVZe4rr4jyL6jgnXY0GUR6kyIIkINuw3OjXRIyxV+dRC9cNFRSv98ljHp8yjOfeAj9slt//aTA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@anthropic-ai/sdk@0.39.0': + resolution: {integrity: sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==} + + '@browserbasehq/sdk@2.16.0': + resolution: {integrity: sha512-mPAuLRU9jWR7o0KJi9+gQnOBDUSIkoKbbFv4HjrA+80qWVcFacrNPlZmf4mguQnfZ0oP2t5c3ws6yuFyAX9vpA==} + + '@browserbasehq/stagehand@3.0.0': + resolution: {integrity: sha512-NRBISQonyqlJvBqUl1Iwb8f8DTawGJAQndhqoq+taji3Lm7U/GHG0yWwBTrwmM3mDh/2ueNL1GY1v0OiXBF2iA==} + peerDependencies: + deepmerge: ^4.3.1 + dotenv: ^16.4.5 + zod: 3.25.67 + + '@browserbasehq/stagehand@3.7.1': + resolution: {integrity: sha512-vAuYSZWIhh3d76BxwppNVE3dB0ztEBLBi85G6TWulZNiebdWptNoANOMuprOB/cw5dE+80b/ZZQo4G33Pc9i6w==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + patchright-core: ^1.55.2 + playwright-core: ^1.55.1 + puppeteer-core: ^24.43.0 + zod: ^3.25.76 || ^4.2.0 + peerDependenciesMeta: + patchright-core: + optional: true + playwright-core: + optional: true + puppeteer-core: + optional: true + + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + + '@fastify/busboy@2.1.1': + resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==} + engines: {node: '>=14'} + + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@hono/node-server@2.0.12': + resolution: {integrity: sha512-eWpQYr67tqJLeaSUl0Q+TquuYfUdTibpOJlUMV2FfUP7+KqCC5TufnwnlXL6mobZBJbGAYRd7ZvEBDCbLInjhg==} + engines: {node: '>=20'} + peerDependencies: + hono: ^4 + + '@langchain/core@0.3.80': + resolution: {integrity: sha512-vcJDV2vk1AlCwSh3aBm/urQ1ZrlXFFBocv11bz/NBUfLWD5/UDNMzwPdaAd2dKvNmTWa9FM2lirLU3+JCf4cRA==} + engines: {node: '>=18'} + + '@langchain/openai@0.4.9': + resolution: {integrity: sha512-NAsaionRHNdqaMjVLPkFCyjUDze+OqRHghA1Cn4fPoAafz+FXcl9c7LlEl9Xo0FH6/8yiCl7Rw2t780C/SBVxQ==} + engines: {node: '>=18'} + peerDependencies: + '@langchain/core': '>=0.3.39 <0.4.0' + + '@modelcontextprotocol/sdk@1.30.0': + resolution: {integrity: sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.5': + resolution: {integrity: sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==} + + '@protobufjs/eventemitter@1.1.1': + resolution: {integrity: sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==} + + '@protobufjs/fetch@1.1.1': + resolution: {integrity: sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.2': + resolution: {integrity: sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==} + + '@puppeteer/browsers@2.3.0': + resolution: {integrity: sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==} + engines: {node: '>=18'} + hasBin: true + + '@smithy/core@3.31.1': + resolution: {integrity: sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-codec@4.4.16': + resolution: {integrity: sha512-u6l4XW99ESjEikMOGENsZxJblxWNvkTLRbCWcC1c2VwNdmd4QgviN2RRlZxliVWEzG3kyU/m0y3iuVFtLL/6jA==} + engines: {node: '>=18.0.0'} + + '@smithy/types@4.16.1': + resolution: {integrity: sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==} + engines: {node: '>=18.0.0'} + + '@smithy/util-utf8@4.4.16': + resolution: {integrity: sha512-/gSbVMQlLRcneJ8D/JGbB1DNf0w4I7OpY2ldNUJ+myDmakPXEm9fy/X4swDlNYTOwBwsCKVJsogCfe7Txjnolg==} + engines: {node: '>=18.0.0'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + + '@types/node@26.1.2': + resolution: {integrity: sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==} + + '@types/retry@0.12.0': + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@vercel/oidc@3.1.0': + resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==} + engines: {node: '>= 20'} + + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + + ai@5.0.225: + resolution: {integrity: sha512-fZKGne2r27yjllLOlMpzrTr+E/bCFvcb8MTb4oM02hbkQnJEeTXXuPE40Ldzwqgn2Pqx9vjq1o1hQJFnwGctHw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + aws4fetch@1.0.20: + resolution: {integrity: sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g==} + + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true + + bare-events@2.9.1: + resolution: {integrity: sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + + bare-fs@4.8.0: + resolution: {integrity: sha512-fM+MhCvdQhZ7NV6S95a07gPSqjIYKn6mFaXfx266wN3ajZGl/+1AzH+ubkXQ0fFZvOe2nk9VHkzdYkQE5zMV3Q==} + engines: {bare: '>=1.28.0'} + peerDependencies: + bare-buffer: '*' + peerDependenciesMeta: + bare-buffer: + optional: true + + bare-path@3.1.1: + resolution: {integrity: sha512-JprUlveX3QjApC1cTpsUOiscADftCGVWkzitbHsRqv84hzYwYHw2mbluddsq5TvI8mH/8Ov1f4BiMAdcB0oYnQ==} + + bare-stream@2.13.3: + resolution: {integrity: sha512-Kc+brLqvEqGkjyfiwJmImAOqLZL7OsoLKuavx+hJjgVV3nLTOjloJyPMFxjUPerGGHrNH0fLU06jjykMLWrERQ==} + peerDependencies: + bare-abort-controller: '*' + bare-buffer: '*' + bare-events: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true + bare-buffer: + optional: true + bare-events: + optional: true + + bare-url@2.4.7: + resolution: {integrity: sha512-o8CRCiJtib+ycO3mE4A5UChtGX4dDP2XxsWVu9P+Zc3H8tcmKwNVEDoDTXmwN+uuMhfKeT7/i7Y26xS8W7ohoA==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + basic-ftp@5.3.1: + resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} + engines: {node: '>=10.0.0'} + + bignumber.js@9.3.1: + resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} + + body-parser@2.3.0: + resolution: {integrity: sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==} + engines: {node: '>=18'} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bufferutil@4.1.0: + resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} + engines: {node: '>=6.14.2'} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + camelcase@6.3.0: + resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==} + engines: {node: '>=10'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chrome-launcher@1.2.1: + resolution: {integrity: sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==} + engines: {node: '>=12.13.0'} + hasBin: true + + chromium-bidi@0.6.3: + resolution: {integrity: sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==} + peerDependencies: + devtools-protocol: '*' + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + console-table-printer@2.16.1: + resolution: {integrity: sha512-Sc9FRJ4O9xKGNrvulNdPfK5SyBcZ6lcaRnDE4AQ/uw6IDtjHhsqyzzqcnMikjyGaiOOF2tNOKoBhbVjRvFy9Lw==} + + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + content-type@2.0.0: + resolution: {integrity: sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==} + engines: {node: '>=18'} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + data-uri-to-buffer@4.0.1: + resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} + engines: {node: '>= 12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + dateformat@4.6.3: + resolution: {integrity: sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + + deepmerge@4.3.1: + resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} + engines: {node: '>=0.10.0'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + devtools-protocol@0.0.1312386: + resolution: {integrity: sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==} + + devtools-protocol@0.0.1464554: + resolution: {integrity: sha512-CAoP3lYfwAGQTaAXYvA6JZR0fjGUb7qec1qf4mToyoH2TZgUFeIqYcjh6f9jNuhHfuZiEdH+PONHYrLhRQX6aw==} + + devtools-protocol@0.0.1642743: + resolution: {integrity: sha512-vTCGze95hGFayPNUXv2H/3cNt/Kqv3J7XqS519j7U8HYhmtD6+eVEPHp6nRHD64dDXJ022TU4reAYhjYyuxm8Q==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} + + eventsource-parser@3.1.0: + resolution: {integrity: sha512-kJezFj9YFAMLeORyi7aCLxLbD5/qWMQnoMVlVPyHIll7lgRJCc3JVln9Vgl9nwQi0YkMnhdGTMNn7CkRRAptMg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + express-rate-limit@8.6.1: + resolution: {integrity: sha512-0D493aP61w0TJ2A0wy27riRsO7FMQ7FK+KUHOKCSfPvYo0R55aiC6emCVgFUeShH0fq0ICPVzNcgoS+BsbXQCA==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-copy@4.0.4: + resolution: {integrity: sha512-eVAiWVNPSEGIzDl5yPuLrx8fNMogScXvD9xp1Kzd41FjRIz2I3sSIcxsFeM5EzFfHAfobdvs8ZySffUopljvIA==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-fifo@1.3.2: + resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} + + fast-safe-stringify@2.1.1: + resolution: {integrity: sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==} + + fast-uri@3.1.5: + resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fetch-blob@3.2.0: + resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} + engines: {node: ^12.20 || >= 14.13} + + fetch-cookie@3.2.0: + resolution: {integrity: sha512-n61pQIxP25C6DRhcJxn7BDzgHP/+S56Urowb5WFxtcRMpU6drqXD90xjyAsVQYsNSNNVbaCcYY1DuHsdkZLuiA==} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + + formdata-polyfill@4.0.10: + resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} + engines: {node: '>=12.20.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gaxios@7.3.0: + resolution: {integrity: sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==} + engines: {node: '>=18'} + + gcp-metadata@8.1.2: + resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} + engines: {node: '>=18'} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + google-auth-library@10.9.1: + resolution: {integrity: sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==} + engines: {node: '>=18'} + + google-logging-utils@1.1.3: + resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} + engines: {node: '>=14'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + help-me@5.0.0: + resolution: {integrity: sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==} + + hono@4.12.34: + resolution: {integrity: sha512-GqXJqY/xJkJmuloTrnV1ZEXG3fqte+VjkUqoRNZXcrUidiUOP4fMSIHHY4tsqZBK++kVyWmt/AAfSUuy57/eSA==} + engines: {node: '>=16.9.0'} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + + iconv-lite@0.7.3: + resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.4.0: + resolution: {integrity: sha512-oSK96Grm3aP6OrS263xVxbNDGVL7rzBtYdpGqlDG8iQdoenDoTs/nkki+DflYbAEE8Xl6o5YxhxlrKvI3nqKXQ==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-docker@2.2.1: + resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==} + engines: {node: '>=8'} + hasBin: true + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + is-wsl@2.2.0: + resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==} + engines: {node: '>=8'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.2.8: + resolution: {integrity: sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==} + + joycon@3.1.1: + resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} + engines: {node: '>=10'} + + js-tiktoken@1.0.21: + resolution: {integrity: sha512-biOj/6M5qdgx5TKjDnFT1ymSpM5tbd3ylwDtrQvFQSu0Z7bBYko2dF+W/aUkXUPuk6IVpRxk/3Q2sHOzGlS36g==} + + json-bigint@1.0.0: + resolution: {integrity: sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + langsmith@0.3.87: + resolution: {integrity: sha512-XXR1+9INH8YX96FKWc5tie0QixWz6tOqAsAKfcJyPkE0xPep+NDz0IQLR32q4bn10QK3LqD2HN6T3n6z1YLW7Q==} + peerDependencies: + '@opentelemetry/api': '*' + '@opentelemetry/exporter-trace-otlp-proto': '*' + '@opentelemetry/sdk-trace-base': '*' + openai: '*' + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/exporter-trace-otlp-proto': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + openai: + optional: true + + lighthouse-logger@2.0.2: + resolution: {integrity: sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + marky@1.3.0: + resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@1.1.1: + resolution: {integrity: sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mustache@4.2.0: + resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==} + hasBin: true + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + netmask@2.1.1: + resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} + engines: {node: '>= 0.4.0'} + + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + + node-fetch@3.3.2: + resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + ollama-ai-provider-v2@1.5.5: + resolution: {integrity: sha512-1YwTFdPjhPNHny/DrOHO+s8oVGGIE5Jib61/KnnjPRNWQhVVimrJJdaAX3e6nNRRDXrY5zbb9cfm2+yVvgsrqw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^4.0.16 + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openai@4.104.0: + resolution: {integrity: sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==} + hasBin: true + peerDependencies: + ws: ^8.18.0 + zod: ^3.23.8 + peerDependenciesMeta: + ws: + optional: true + zod: + optional: true + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + + p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + patchright-core@1.61.1: + resolution: {integrity: sha512-d6Ju67DE6OzqlEX3WPvUX2SLEs6iMY1WERit//mpsrT98VHcztaUo0CBw2Lpxq25VdufVEiWEdcc0HnLwdiq6A==} + engines: {node: '>=18'} + hasBin: true + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + pino-abstract-transport@2.0.0: + resolution: {integrity: sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-pretty@13.1.3: + resolution: {integrity: sha512-ttXRkkOz6WWC95KeY9+xxWL6AtImwbyMHrL1mSwqwW9u+vLp/WIElvHvCSDg0xO/Dzrggz1zv3rN5ovTRVowKg==} + hasBin: true + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@9.14.0: + resolution: {integrity: sha512-8OEwKp5juEvb/MjpIc4hjqfgCNysrS94RIOMXYvpYCdm/jglrKEiAYmiumbmGhCvs+IcInsphYDFwqrjr7398w==} + hasBin: true + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + protobufjs@7.6.5: + resolution: {integrity: sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + puppeteer-core@22.15.0: + resolution: {integrity: sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==} + engines: {node: '>=18'} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + range-parser@1.3.0: + resolution: {integrity: sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + simple-wcswidth@1.1.2: + resolution: {integrity: sha512-j7piyCjAeTDSjzTSQ7DokZtMNwNlEAyxqSZeCS+CXH7fJ4jx3FuJ/mTW3mE+6JLs4VJBbcll0Kjn+KXI5t21Iw==} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + streamx@2.28.0: + resolution: {integrity: sha512-1Yowhzjf0ivGMrTIkY9hav5TxobO9qIVqUE41fiCGMGgc3CLlf4MY+9AHmZqBWgDTue0fY9zWjYFVyf6Diuobw==} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@5.0.3: + resolution: {integrity: sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==} + engines: {node: '>=14.16'} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tar-fs@3.1.3: + resolution: {integrity: sha512-/hU4AXnIdZu+Gvl1pk0oI5f5HxWsCJRtY2aFaJdk9VvyL48DWU6iU5WAIPG+wIi1YvWA6eTJvIviP/tMAZZNwQ==} + + tar-stream@3.2.0: + resolution: {integrity: sha512-ojzvCvVaNp6aOTFmG7jaRD0meowIAuPc3cMMhSgKiVWws1GyHbGd/xvnyuRKcKlMpt3qvxx6r0hreCNITP9hIg==} + + teex@1.0.1: + resolution: {integrity: sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==} + + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} + + thread-stream@3.2.0: + resolution: {integrity: sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==} + + through@2.3.8: + resolution: {integrity: sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==} + + tldts-core@7.4.10: + resolution: {integrity: sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==} + + tldts@7.4.10: + resolution: {integrity: sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==} + hasBin: true + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + tough-cookie@6.0.2: + resolution: {integrity: sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==} + engines: {node: '>=16'} + + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-is@2.1.0: + resolution: {integrity: sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==} + engines: {node: '>= 18'} + + unbzip2-stream@1.4.3: + resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} + + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + + undici-types@8.3.0: + resolution: {integrity: sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==} + + undici@5.29.0: + resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} + engines: {node: '>=14.0'} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + urlpattern-polyfill@10.0.0: + resolution: {integrity: sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==} + + uuid@10.0.0: + resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + web-streams-polyfill@3.3.3: + resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} + engines: {node: '>= 8'} + + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.1: + resolution: {integrity: sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@3.23.8: + resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@ai-sdk/amazon-bedrock@3.0.112(zod@3.25.76)': + dependencies: + '@ai-sdk/anthropic': 2.0.92(zod@3.25.76) + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + '@smithy/eventstream-codec': 4.4.16 + '@smithy/util-utf8': 4.4.16 + aws4fetch: 1.0.20 + zod: 3.25.76 + optional: true + + '@ai-sdk/anthropic@2.0.92(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@ai-sdk/azure@2.0.122(zod@3.25.76)': + dependencies: + '@ai-sdk/deepseek': 1.0.49(zod@3.25.76) + '@ai-sdk/openai': 2.0.117(zod@3.25.76) + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@ai-sdk/cerebras@1.0.52(zod@3.25.76)': + dependencies: + '@ai-sdk/openai-compatible': 1.0.47(zod@3.25.76) + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@ai-sdk/deepseek@1.0.49(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@ai-sdk/gateway@2.0.124(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + '@vercel/oidc': 3.1.0 + zod: 3.25.76 + + '@ai-sdk/google-vertex@3.0.159(zod@3.25.76)': + dependencies: + '@ai-sdk/anthropic': 2.0.92(zod@3.25.76) + '@ai-sdk/google': 2.0.86(zod@3.25.76) + '@ai-sdk/openai-compatible': 1.0.47(zod@3.25.76) + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + google-auth-library: 10.9.1 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + optional: true + + '@ai-sdk/google@2.0.86(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@ai-sdk/groq@2.0.47(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@ai-sdk/mistral@2.0.41(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@ai-sdk/openai-compatible@1.0.47(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@ai-sdk/openai@2.0.117(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/perplexity@2.0.37(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@ai-sdk/provider-utils@3.0.31(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.1.0 + undici: 5.29.0 + zod: 3.25.76 + + '@ai-sdk/provider@2.0.3': + dependencies: + json-schema: 0.4.0 + + '@ai-sdk/togetherai@1.0.51(zod@3.25.76)': + dependencies: + '@ai-sdk/openai-compatible': 1.0.47(zod@3.25.76) + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@ai-sdk/xai@2.0.84(zod@3.25.76)': + dependencies: + '@ai-sdk/openai-compatible': 1.0.47(zod@3.25.76) + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + '@anthropic-ai/sdk@0.39.0': + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@browserbasehq/sdk@2.16.0': + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + + '@browserbasehq/stagehand@3.0.0(@cfworker/json-schema@4.1.1)(@opentelemetry/api@1.9.0)(bufferutil@4.1.0)(deepmerge@4.3.1)(dotenv@16.6.1)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@anthropic-ai/sdk': 0.39.0 + '@browserbasehq/sdk': 2.16.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0) + '@langchain/openai': 0.4.9(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76)))(ws@8.21.1(bufferutil@4.1.0)) + '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + ai: 5.0.225(zod@3.25.76) + deepmerge: 4.3.1 + devtools-protocol: 0.0.1464554 + dotenv: 16.6.1 + fetch-cookie: 3.2.0 + openai: 4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76) + pino: 9.14.0 + pino-pretty: 13.1.3 + playwright: 1.62.1 + ws: 8.21.1(bufferutil@4.1.0) + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + '@ai-sdk/anthropic': 2.0.92(zod@3.25.76) + '@ai-sdk/azure': 2.0.122(zod@3.25.76) + '@ai-sdk/cerebras': 1.0.52(zod@3.25.76) + '@ai-sdk/deepseek': 1.0.49(zod@3.25.76) + '@ai-sdk/google': 2.0.86(zod@3.25.76) + '@ai-sdk/groq': 2.0.47(zod@3.25.76) + '@ai-sdk/mistral': 2.0.41(zod@3.25.76) + '@ai-sdk/openai': 2.0.117(zod@3.25.76) + '@ai-sdk/perplexity': 2.0.37(zod@3.25.76) + '@ai-sdk/togetherai': 1.0.51(zod@3.25.76) + '@ai-sdk/xai': 2.0.84(zod@3.25.76) + '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76)) + chrome-launcher: 1.2.1 + ollama-ai-provider-v2: 1.5.5(zod@3.25.76) + patchright-core: 1.61.1 + playwright-core: 1.62.1 + puppeteer-core: 22.15.0(bufferutil@4.1.0) + transitivePeerDependencies: + - '@cfworker/json-schema' + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - bare-abort-controller + - bare-buffer + - bufferutil + - encoding + - react-native-b4a + - supports-color + - utf-8-validate + + '@browserbasehq/stagehand@3.7.1(@cfworker/json-schema@4.1.1)(patchright-core@1.61.1)(playwright-core@1.62.1)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.3 + '@anthropic-ai/sdk': 0.39.0 + '@browserbasehq/sdk': 2.16.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0) + '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + ai: 5.0.225(zod@3.25.76) + devtools-protocol: 0.0.1642743 + fetch-cookie: 3.2.0 + openai: 4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76) + pino: 9.14.0 + pino-pretty: 13.1.3 + uuid: 11.1.1 + ws: 8.21.1(bufferutil@4.1.0) + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + '@ai-sdk/amazon-bedrock': 3.0.112(zod@3.25.76) + '@ai-sdk/anthropic': 2.0.92(zod@3.25.76) + '@ai-sdk/azure': 2.0.122(zod@3.25.76) + '@ai-sdk/cerebras': 1.0.52(zod@3.25.76) + '@ai-sdk/deepseek': 1.0.49(zod@3.25.76) + '@ai-sdk/google': 2.0.86(zod@3.25.76) + '@ai-sdk/google-vertex': 3.0.159(zod@3.25.76) + '@ai-sdk/groq': 2.0.47(zod@3.25.76) + '@ai-sdk/mistral': 2.0.41(zod@3.25.76) + '@ai-sdk/openai': 2.0.117(zod@3.25.76) + '@ai-sdk/perplexity': 2.0.37(zod@3.25.76) + '@ai-sdk/togetherai': 1.0.51(zod@3.25.76) + '@ai-sdk/xai': 2.0.84(zod@3.25.76) + bufferutil: 4.1.0 + chrome-launcher: 1.2.1 + ollama-ai-provider-v2: 1.5.5(zod@3.25.76) + patchright-core: 1.61.1 + playwright-core: 1.62.1 + transitivePeerDependencies: + - '@cfworker/json-schema' + - encoding + - supports-color + - utf-8-validate + + '@cfworker/json-schema@4.1.1': {} + + '@fastify/busboy@2.1.1': {} + + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76))(bufferutil@4.1.0)': + dependencies: + google-auth-library: 10.9.1 + p-retry: 4.6.2 + protobufjs: 7.6.5 + ws: 8.21.1(bufferutil@4.1.0) + optionalDependencies: + '@modelcontextprotocol/sdk': 1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@hono/node-server@2.0.12(hono@4.12.34)': + dependencies: + hono: 4.12.34 + + '@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76))': + dependencies: + '@cfworker/json-schema': 4.1.1 + ansi-styles: 5.2.0 + camelcase: 6.3.0 + decamelize: 1.2.0 + js-tiktoken: 1.0.21 + langsmith: 0.3.87(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76)) + mustache: 4.2.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + uuid: 10.0.0 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - '@opentelemetry/api' + - '@opentelemetry/exporter-trace-otlp-proto' + - '@opentelemetry/sdk-trace-base' + - openai + + '@langchain/openai@0.4.9(@langchain/core@0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76)))(ws@8.21.1(bufferutil@4.1.0))': + dependencies: + '@langchain/core': 0.3.80(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76)) + js-tiktoken: 1.0.21 + openai: 4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76) + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - encoding + - ws + + '@modelcontextprotocol/sdk@1.30.0(@cfworker/json-schema@4.1.1)(zod@3.25.76)': + dependencies: + '@hono/node-server': 2.0.12(hono@4.12.34) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.1.0 + express: 5.2.1 + express-rate-limit: 8.6.1(express@5.2.1) + hono: 4.12.34 + jose: 6.2.8 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/api@1.9.0': {} + + '@pinojs/redact@0.4.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.5': {} + + '@protobufjs/eventemitter@1.1.1': {} + + '@protobufjs/fetch@1.1.1': + dependencies: + '@protobufjs/aspromise': 1.1.2 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.2': {} + + '@puppeteer/browsers@2.3.0': + dependencies: + debug: 4.4.3 + extract-zip: 2.0.1 + progress: 2.0.3 + proxy-agent: 6.5.0 + semver: 7.8.5 + tar-fs: 3.1.3 + unbzip2-stream: 1.4.3 + yargs: 17.7.3 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + - supports-color + optional: true + + '@smithy/core@3.31.1': + dependencies: + '@smithy/types': 4.16.1 + tslib: 2.8.1 + optional: true + + '@smithy/eventstream-codec@4.4.16': + dependencies: + '@smithy/core': 3.31.1 + tslib: 2.8.1 + optional: true + + '@smithy/types@4.16.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@smithy/util-utf8@4.4.16': + dependencies: + '@smithy/core': 3.31.1 + tslib: 2.8.1 + optional: true + + '@standard-schema/spec@1.1.0': {} + + '@tootallnate/quickjs-emscripten@0.23.0': + optional: true + + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 18.19.130 + form-data: 4.0.6 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + + '@types/node@26.1.2': + dependencies: + undici-types: 8.3.0 + + '@types/retry@0.12.0': {} + + '@types/uuid@10.0.0': {} + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 26.1.2 + optional: true + + '@vercel/oidc@3.1.0': {} + + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + agent-base@7.1.4: {} + + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + + ai@5.0.225(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 2.0.124(zod@3.25.76) + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.5 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@5.0.1: + optional: true + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@5.2.0: {} + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + optional: true + + asynckit@0.4.0: {} + + atomic-sleep@1.0.0: {} + + aws4fetch@1.0.20: + optional: true + + b4a@1.8.1: + optional: true + + bare-events@2.9.1: + optional: true + + bare-fs@4.8.0: + dependencies: + bare-events: 2.9.1 + bare-path: 3.1.1 + bare-stream: 2.13.3(bare-events@2.9.1) + bare-url: 2.4.7 + fast-fifo: 1.3.2 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + bare-path@3.1.1: + optional: true + + bare-stream@2.13.3(bare-events@2.9.1): + dependencies: + b4a: 1.8.1 + streamx: 2.28.0 + teex: 1.0.1 + optionalDependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - react-native-b4a + optional: true + + bare-url@2.4.7: + dependencies: + bare-path: 3.1.1 + optional: true + + base64-js@1.5.1: {} + + basic-ftp@5.3.1: + optional: true + + bignumber.js@9.3.1: {} + + body-parser@2.3.0: + dependencies: + bytes: 3.1.2 + content-type: 2.0.0 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + on-finished: 2.4.1 + qs: 6.15.3 + raw-body: 3.0.2 + type-is: 2.1.0 + transitivePeerDependencies: + - supports-color + + buffer-crc32@0.2.13: + optional: true + + buffer-equal-constant-time@1.0.1: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + optional: true + + bufferutil@4.1.0: + dependencies: + node-gyp-build: 4.8.4 + optional: true + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + camelcase@6.3.0: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chrome-launcher@1.2.1: + dependencies: + '@types/node': 26.1.2 + escape-string-regexp: 4.0.0 + is-wsl: 2.2.0 + lighthouse-logger: 2.0.2 + transitivePeerDependencies: + - supports-color + optional: true + + chromium-bidi@0.6.3(devtools-protocol@0.0.1312386): + dependencies: + devtools-protocol: 0.0.1312386 + mitt: 3.0.1 + urlpattern-polyfill: 10.0.0 + zod: 3.23.8 + optional: true + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + optional: true + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + colorette@2.0.20: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + console-table-printer@2.16.1: + dependencies: + simple-wcswidth: 1.1.2 + + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + + content-type@2.0.0: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + data-uri-to-buffer@4.0.1: {} + + data-uri-to-buffer@6.0.2: + optional: true + + dateformat@4.6.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decamelize@1.2.0: {} + + deepmerge@4.3.1: {} + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + optional: true + + delayed-stream@1.0.0: {} + + depd@2.0.0: {} + + devtools-protocol@0.0.1312386: + optional: true + + devtools-protocol@0.0.1464554: {} + + devtools-protocol@0.0.1642743: {} + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + emoji-regex@8.0.0: + optional: true + + encodeurl@2.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + escalade@3.2.0: + optional: true + + escape-html@1.0.3: {} + + escape-string-regexp@4.0.0: + optional: true + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + optional: true + + esprima@4.0.1: + optional: true + + estraverse@5.3.0: + optional: true + + esutils@2.0.3: + optional: true + + etag@1.8.1: {} + + event-target-shim@5.0.1: {} + + eventemitter3@4.0.7: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.9.1 + transitivePeerDependencies: + - bare-abort-controller + optional: true + + eventsource-parser@3.1.0: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.1.0 + + express-rate-limit@8.6.1(express@5.2.1): + dependencies: + debug: 4.4.3 + express: 5.2.1 + ip-address: 10.4.0 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.3.0 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.3 + range-parser: 1.3.0 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.1.0 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + extend@3.0.2: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + optional: true + + fast-copy@4.0.4: {} + + fast-deep-equal@3.1.3: {} + + fast-fifo@1.3.2: + optional: true + + fast-safe-stringify@2.1.1: {} + + fast-uri@3.1.5: {} + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + optional: true + + fetch-blob@3.2.0: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 3.3.3 + + fetch-cookie@3.2.0: + dependencies: + set-cookie-parser: 2.7.2 + tough-cookie: 6.0.2 + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + form-data-encoder@1.7.2: {} + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + + formdata-polyfill@4.0.10: + dependencies: + fetch-blob: 3.2.0 + + forwarded@0.2.0: {} + + fresh@2.0.0: {} + + fsevents@2.3.2: + optional: true + + function-bind@1.1.2: {} + + gaxios@7.3.0: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + node-fetch: 3.3.2 + transitivePeerDependencies: + - supports-color + + gcp-metadata@8.1.2: + dependencies: + gaxios: 7.3.0 + google-logging-utils: 1.1.3 + json-bigint: 1.0.0 + transitivePeerDependencies: + - supports-color + + get-caller-file@2.0.5: + optional: true + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + optional: true + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.3.1 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + google-auth-library@10.9.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 7.3.0 + gcp-metadata: 8.1.2 + google-logging-utils: 1.1.3 + jws: 4.0.1 + transitivePeerDependencies: + - supports-color + + google-logging-utils@1.1.3: {} + + gopd@1.2.0: {} + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + help-me@5.0.0: {} + + hono@4.12.34: {} + + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + optional: true + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + + iconv-lite@0.7.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: + optional: true + + inherits@2.0.4: {} + + ip-address@10.4.0: {} + + ipaddr.js@1.9.1: {} + + is-docker@2.2.1: + optional: true + + is-fullwidth-code-point@3.0.0: + optional: true + + is-promise@4.0.0: {} + + is-wsl@2.2.0: + dependencies: + is-docker: 2.2.1 + optional: true + + isexe@2.0.0: {} + + jose@6.2.8: {} + + joycon@3.1.1: {} + + js-tiktoken@1.0.21: + dependencies: + base64-js: 1.5.1 + + json-bigint@1.0.0: + dependencies: + bignumber.js: 9.3.1 + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-schema@0.4.0: {} + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + langsmith@0.3.87(@opentelemetry/api@1.9.0)(openai@4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76)): + dependencies: + '@types/uuid': 10.0.0 + chalk: 4.1.2 + console-table-printer: 2.16.1 + p-queue: 6.6.2 + semver: 7.8.5 + uuid: 10.0.0 + optionalDependencies: + '@opentelemetry/api': 1.9.0 + openai: 4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76) + + lighthouse-logger@2.0.2: + dependencies: + debug: 4.4.3 + marky: 1.3.0 + transitivePeerDependencies: + - supports-color + optional: true + + long@5.3.2: {} + + lru-cache@7.18.3: + optional: true + + marky@1.3.0: + optional: true + + math-intrinsics@1.1.0: {} + + media-typer@1.1.1: {} + + merge-descriptors@2.0.0: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + minimist@1.2.8: {} + + mitt@3.0.1: + optional: true + + ms@2.1.3: {} + + mustache@4.2.0: {} + + negotiator@1.0.0: {} + + netmask@2.1.1: + optional: true + + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + + node-fetch@3.3.2: + dependencies: + data-uri-to-buffer: 4.0.1 + fetch-blob: 3.2.0 + formdata-polyfill: 4.0.10 + + node-gyp-build@4.8.4: + optional: true + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + ollama-ai-provider-v2@1.5.5(zod@3.25.76): + dependencies: + '@ai-sdk/provider': 2.0.3 + '@ai-sdk/provider-utils': 3.0.31(zod@3.25.76) + zod: 3.25.76 + optional: true + + on-exit-leak-free@2.1.2: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openai@4.104.0(ws@8.21.1(bufferutil@4.1.0))(zod@3.25.76): + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + optionalDependencies: + ws: 8.21.1(bufferutil@4.1.0) + zod: 3.25.76 + transitivePeerDependencies: + - encoding + + p-finally@1.0.0: {} + + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + + p-retry@4.6.2: + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + optional: true + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.1.1 + optional: true + + parseurl@1.3.3: {} + + patchright-core@1.61.1: + optional: true + + path-key@3.1.1: {} + + path-to-regexp@8.4.2: {} + + pend@1.2.0: + optional: true + + pino-abstract-transport@2.0.0: + dependencies: + split2: 4.2.0 + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-pretty@13.1.3: + dependencies: + colorette: 2.0.20 + dateformat: 4.6.3 + fast-copy: 4.0.4 + fast-safe-stringify: 2.1.1 + help-me: 5.0.0 + joycon: 3.1.1 + minimist: 1.2.8 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pump: 3.0.4 + secure-json-parse: 4.1.0 + sonic-boom: 4.2.1 + strip-json-comments: 5.0.3 + + pino-std-serializers@7.1.0: {} + + pino@9.14.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 2.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 3.2.0 + + pkce-challenge@5.0.1: {} + + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + + process-warning@5.1.0: {} + + progress@2.0.3: + optional: true + + protobufjs@7.6.5: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.5 + '@protobufjs/eventemitter': 1.1.1 + '@protobufjs/fetch': 1.1.1 + '@protobufjs/float': 1.0.2 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.2 + '@types/node': 26.1.2 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + optional: true + + proxy-from-env@1.1.0: + optional: true + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + puppeteer-core@22.15.0(bufferutil@4.1.0): + dependencies: + '@puppeteer/browsers': 2.3.0 + chromium-bidi: 0.6.3(devtools-protocol@0.0.1312386) + debug: 4.4.3 + devtools-protocol: 0.0.1312386 + ws: 8.21.1(bufferutil@4.1.0) + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - bufferutil + - react-native-b4a + - supports-color + - utf-8-validate + optional: true + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + quick-format-unescaped@4.0.4: {} + + range-parser@1.3.0: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.3 + unpipe: 1.0.0 + + real-require@0.2.0: {} + + require-directory@2.1.1: + optional: true + + require-from-string@2.0.2: {} + + retry@0.13.1: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.2.1: {} + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + secure-json-parse@4.1.0: {} + + semver@7.8.5: {} + + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.3.0 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + set-cookie-parser@2.7.2: {} + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + simple-wcswidth@1.1.2: {} + + smart-buffer@4.2.0: + optional: true + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.9 + transitivePeerDependencies: + - supports-color + optional: true + + socks@2.8.9: + dependencies: + ip-address: 10.4.0 + smart-buffer: 4.2.0 + optional: true + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map@0.6.1: + optional: true + + split2@4.2.0: {} + + statuses@2.0.2: {} + + streamx@2.28.0: + dependencies: + events-universal: 1.0.1 + fast-fifo: 1.3.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + optional: true + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + optional: true + + strip-json-comments@5.0.3: {} + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tar-fs@3.1.3: + dependencies: + pump: 3.0.4 + tar-stream: 3.2.0 + optionalDependencies: + bare-fs: 4.8.0 + bare-path: 3.1.1 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + optional: true + + tar-stream@3.2.0: + dependencies: + b4a: 1.8.1 + bare-fs: 4.8.0 + fast-fifo: 1.3.2 + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - bare-buffer + - react-native-b4a + optional: true + + teex@1.0.1: + dependencies: + streamx: 2.28.0 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a + optional: true + + text-decoder@1.2.7: + dependencies: + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a + optional: true + + thread-stream@3.2.0: + dependencies: + real-require: 0.2.0 + + through@2.3.8: + optional: true + + tldts-core@7.4.10: {} + + tldts@7.4.10: + dependencies: + tldts-core: 7.4.10 + + toidentifier@1.0.1: {} + + tough-cookie@6.0.2: + dependencies: + tldts: 7.4.10 + + tr46@0.0.3: {} + + tslib@2.8.1: + optional: true + + type-is@2.1.0: + dependencies: + content-type: 2.0.0 + media-typer: 1.1.1 + mime-types: 3.0.2 + + unbzip2-stream@1.4.3: + dependencies: + buffer: 5.7.1 + through: 2.3.8 + optional: true + + undici-types@5.26.5: {} + + undici-types@8.3.0: {} + + undici@5.29.0: + dependencies: + '@fastify/busboy': 2.1.1 + + unpipe@1.0.0: {} + + urlpattern-polyfill@10.0.0: + optional: true + + uuid@10.0.0: {} + + uuid@11.1.1: {} + + vary@1.1.2: {} + + web-streams-polyfill@3.3.3: {} + + web-streams-polyfill@4.0.0-beta.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + optional: true + + wrappy@1.0.2: {} + + ws@8.21.1(bufferutil@4.1.0): + optionalDependencies: + bufferutil: 4.1.0 + + y18n@5.0.8: + optional: true + + yargs-parser@21.1.1: + optional: true + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + optional: true + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + optional: true + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.23.8: + optional: true + + zod@3.25.76: {} diff --git a/e2e/scenarios/stagehand-instrumentation/scenario.impl.mjs b/e2e/scenarios/stagehand-instrumentation/scenario.impl.mjs new file mode 100644 index 000000000..dc9bdb79f --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/scenario.impl.mjs @@ -0,0 +1,316 @@ +import { createServer } from "node:http"; +import { z } from "zod/v3"; +import { + runOperation, + runTracedScenario, +} from "../../helpers/provider-runtime.mjs"; + +export const ROOT_NAME = "stagehand-instrumentation-root"; +export const SCENARIO_NAME = "stagehand-instrumentation"; +export const SECRET = "STAGEHAND_PRIVATE_SECRET"; +export const MODEL_NAME = "gpt-4o-mini-2024-07-18"; +export const ADD_TO_CART_INSTRUCTION = "Add the Trail Backpack to the cart"; + +const SHOP_HTML = ` + + Trail Supply + +
+

Trail Backpack

+

$129.00

+ +
+

Cart is empty

+ +
+
+ +`; + +async function startShopServer() { + const server = createServer((_request, response) => { + response.writeHead(200, { "content-type": "text/html; charset=utf-8" }); + response.end(SHOP_HTML); + }); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + if (!address || typeof address === "string") { + throw new Error("Expected the Stagehand shop server to use a TCP port"); + } + return { + close: () => + new Promise((resolve, reject) => { + server.close((error) => (error ? reject(error) : resolve())); + }), + url: `http://127.0.0.1:${address.port}/products/trail-backpack`, + }; +} + +async function createStagehandSession(Stagehand, instrumentedAI) { + const stagehand = new Stagehand({ + disablePino: true, + env: "LOCAL", + localBrowserLaunchOptions: { + args: ["--disable-dev-shm-usage", "--no-sandbox"], + chromiumSandbox: false, + executablePath: process.env.CHROME_PATH ?? "/usr/bin/google-chrome", + headless: true, + }, + model: { + apiKey: process.env.OPENAI_API_KEY, + baseURL: process.env.OPENAI_BASE_URL, + modelName: `openai/${MODEL_NAME}`, + }, + verbose: 0, + }); + await stagehand.init(); + const page = stagehand.context.pages()[0]; + if (!page) { + await stagehand.close({ force: true }); + throw new Error("Expected Stagehand to initialize a Chromium page"); + } + const languageModel = stagehand.llmClient?.getLanguageModel?.(); + if (!languageModel) { + await stagehand.close({ force: true }); + throw new Error("Expected Stagehand to create an AI SDK language model"); + } + stagehand.llmClient.createChatCompletion = async ({ options }) => { + if (!options.response_model) { + throw new Error( + "Expected Stagehand act to request a structured response", + ); + } + const response = await instrumentedAI.generateObject({ + allowSystemInMessages: true, + messages: options.messages, + model: languageModel, + schema: options.response_model.schema, + temperature: options.temperature, + }); + return { + data: response.object, + usage: { + cached_input_tokens: response.usage.cachedInputTokens ?? 0, + completion_tokens: response.usage.outputTokens ?? 0, + prompt_tokens: response.usage.inputTokens ?? 0, + reasoning_tokens: response.usage.reasoningTokens ?? 0, + total_tokens: response.usage.totalTokens ?? 0, + }, + }; + }; + const extractCart = async (instruction) => { + const cartText = await page.locator("#cart").innerText(); + const quantity = cartText.startsWith("1 ") ? 1 : 0; + if (!instruction) { + return `${await page.locator("body").innerText()} ${SECRET}`; + } + return { + item: "Trail Backpack", + quantity, + total: quantity === 1 ? "$129.00" : "$0.00", + cacheStatus: "MISS", + }; + }; + Object.assign(stagehand, { + _history: [], + actCache: { enabled: false }, + agentCache: { + buildConfigSignature: () => "fixture-signature", + isRecording: () => false, + sanitizeExecuteOptions: (options) => options, + shouldAttemptCache: () => false, + }, + apiClient: { + act: async ({ input, options }) => { + if (input === "Apply the expired coupon code") { + throw new Error("STAGEHAND_COUPON_NOT_FOUND"); + } + if (typeof input !== "string") { + throw new Error("Expected Stagehand act to receive an instruction"); + } + return stagehand.actHandler.act({ + instruction: input, + model: options?.model, + page, + timeout: options?.timeout, + variables: options?.variables, + }); + }, + agentExecute: async () => { + const cart = await extractCart("cart state"); + return { + success: true, + completed: true, + message: + "Cart reviewed; checkout is ready for customer confirmation.", + output: { + checkoutReady: cart.quantity === 1, + itemCount: cart.quantity, + }, + actions: [ + { + type: "observe", + taskCompleted: true, + timeMs: 15, + selector: SECRET, + reasoning: SECRET, + }, + ], + metadata: { cacheHit: true, debugUrl: SECRET }, + messages: [{ role: "assistant", content: SECRET }], + usage: { + input_tokens: 11, + output_tokens: 5, + reasoning_tokens: 2, + cached_input_tokens: 3, + inference_time_ms: 250, + }, + }; + }, + consumeLatestAgentCacheEntry: () => undefined, + end: async () => undefined, + extract: async ({ instruction }) => extractCart(instruction), + observe: async () => [ + { + method: "click", + selector: SECRET, + arguments: [SECRET], + description: SECRET, + }, + ], + }, + experimental: false, + extractHandler: { + extract: async ({ instruction }) => extractCart(instruction), + }, + flowLoggerContext: undefined, + observeHandler: { + observe: async () => [ + { + method: "click", + selector: SECRET, + arguments: [SECRET], + description: SECRET, + }, + ], + }, + stagehandMetrics: {}, + updateAgentMetricsFromUsage: () => undefined, + }); + return stagehand; +} + +export async function runStagehandInstrumentationScenario( + sdk, + ai, + decorateSDK, + decorateAI, +) { + const instrumentedSDK = decorateSDK ? decorateSDK(sdk) : sdk; + const instrumentedAI = decorateAI ? decorateAI(ai) : ai; + if (instrumentedSDK.Stagehand !== instrumentedSDK.V3) { + throw new Error("Expected Stagehand and V3 exports to remain aliases"); + } + const shopServer = await startShopServer(); + let stagehand; + + try { + stagehand = await createStagehandSession( + instrumentedSDK.Stagehand, + instrumentedAI, + ); + const page = stagehand.context.pages()[0]; + await page.goto(shopServer.url, { + timeoutMs: 10_000, + waitUntil: "domcontentloaded", + }); + if (page.url() !== shopServer.url) { + throw new Error("Stagehand did not navigate to the shop page"); + } + + await runTracedScenario({ + callback: async () => { + await runOperation("stagehand-act-operation", "act", async () => { + const result = await stagehand.act(ADD_TO_CART_INSTRUCTION, { + variables: { customerEmail: SECRET }, + }); + if (!result.success) { + throw new Error("Stagehand did not add the item to the cart"); + } + }); + + await runOperation( + "stagehand-extract-operation", + "extract", + async () => { + const cart = await stagehand.extract( + "Extract the cart item, quantity, and total", + z.object({ + item: z.string(), + quantity: z.number(), + total: z.string(), + }), + ); + if (cart.item !== "Trail Backpack" || cart.quantity !== 1) { + throw new Error("Unexpected cart extraction result"); + } + await stagehand.extract(); + }, + ); + + await runOperation( + "stagehand-observe-operation", + "observe", + async () => { + const actions = await stagehand.observe("Find the checkout button"); + if (actions.length !== 1) { + throw new Error("Expected one checkout action"); + } + }, + ); + + await runOperation("stagehand-agent-operation", "agent", async () => { + const agent = stagehand.agent({ + maxSteps: 6, + mode: "dom", + model: `openai/${MODEL_NAME}`, + stream: false, + }); + const result = await agent.execute({ + instruction: + "Review the cart and prepare to check out. Stop before submitting payment.", + variables: { customerEmail: SECRET }, + }); + if (!result.output?.checkoutReady) { + throw new Error("Unexpected Stagehand agent result"); + } + }); + + await runOperation("stagehand-error-operation", "error", async () => { + try { + await stagehand.act("Apply the expired coupon code"); + } catch (error) { + if ( + !(error instanceof Error) || + error.message !== "STAGEHAND_COUPON_NOT_FOUND" + ) { + throw error; + } + } + }); + }, + flushCount: 2, + metadata: { scenario: SCENARIO_NAME }, + projectNameBase: "e2e-stagehand-instrumentation", + rootName: ROOT_NAME, + }); + } finally { + await stagehand?.close({ force: true }); + await shopServer.close(); + } +} diff --git a/e2e/scenarios/stagehand-instrumentation/scenario.mjs b/e2e/scenarios/stagehand-instrumentation/scenario.mjs new file mode 100644 index 000000000..8c8a9f045 --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/scenario.mjs @@ -0,0 +1,10 @@ +import { runMain } from "../../helpers/provider-runtime.mjs"; +import { runStagehandInstrumentationScenario } from "./scenario.impl.mjs"; + +runMain(async () => { + const packageName = process.env.STAGEHAND_PACKAGE_NAME; + if (!packageName) throw new Error("STAGEHAND_PACKAGE_NAME is required"); + const sdk = await import(packageName); + const ai = await import("ai"); + await runStagehandInstrumentationScenario(sdk, ai); +}); diff --git a/e2e/scenarios/stagehand-instrumentation/scenario.test.ts b/e2e/scenarios/stagehand-instrumentation/scenario.test.ts new file mode 100644 index 000000000..f4c8d4efa --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/scenario.test.ts @@ -0,0 +1,79 @@ +import { describe } from "vitest"; +import { + prepareScenarioDir, + readInstalledPackageVersion, + resolveScenarioDir, +} from "../../helpers/scenario-harness"; +import { defineStagehandInstrumentationAssertions } from "./assertions"; + +const originalScenarioDir = resolveScenarioDir(import.meta.url); +const scenarioDir = await prepareScenarioDir({ + scenarioDir: originalScenarioDir, +}); +const TIMEOUT_MS = 120_000; +const scenarios = await Promise.all( + [ + { dependencyName: "stagehand-v3", snapshotPrefix: "stagehand-v3" }, + { + dependencyName: "stagehand-v3-latest", + snapshotPrefix: "stagehand-v3-latest", + }, + ].map(async (scenario) => ({ + ...scenario, + version: await readInstalledPackageVersion( + scenarioDir, + scenario.dependencyName, + ), + })), +); + +describe.sequential("variants", () => { + for (const scenario of scenarios) { + describe.sequential(`Stagehand ${scenario.version}`, () => { + defineStagehandInstrumentationAssertions({ + name: "wrapped instrumentation", + runScenario: async ({ runScenarioDir }) => { + await runScenarioDir({ + entry: "scenario.ts", + env: { STAGEHAND_PACKAGE_NAME: scenario.dependencyName }, + runContext: { + cassette: { + variantKey: `${scenario.snapshotPrefix}-wrapped`, + }, + originalScenarioDir, + variantKey: `${scenario.snapshotPrefix}-wrapped`, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotPrefix}-wrapped`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + + defineStagehandInstrumentationAssertions({ + name: "auto-hook instrumentation", + runScenario: async ({ runNodeScenarioDir }) => { + await runNodeScenarioDir({ + entry: "scenario.mjs", + env: { STAGEHAND_PACKAGE_NAME: scenario.dependencyName }, + nodeArgs: ["--import", "braintrust/hook.mjs"], + runContext: { + cassette: { + variantKey: `${scenario.snapshotPrefix}-auto-hook`, + }, + originalScenarioDir, + variantKey: `${scenario.snapshotPrefix}-auto-hook`, + }, + scenarioDir, + timeoutMs: TIMEOUT_MS, + }); + }, + snapshotName: `${scenario.snapshotPrefix}-auto-hook`, + testFileUrl: import.meta.url, + timeoutMs: TIMEOUT_MS, + }); + }); + } +}); diff --git a/e2e/scenarios/stagehand-instrumentation/scenario.ts b/e2e/scenarios/stagehand-instrumentation/scenario.ts new file mode 100644 index 000000000..de702870e --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/scenario.ts @@ -0,0 +1,11 @@ +import { wrapAISDK, wrapStagehand } from "braintrust"; +import { runMain } from "../../helpers/provider-runtime.mjs"; +import { runStagehandInstrumentationScenario } from "./scenario.impl.mjs"; + +runMain(async () => { + const packageName = process.env.STAGEHAND_PACKAGE_NAME; + if (!packageName) throw new Error("STAGEHAND_PACKAGE_NAME is required"); + const sdk = await import(packageName); + const ai = await import("ai"); + await runStagehandInstrumentationScenario(sdk, ai, wrapStagehand, wrapAISDK); +}); diff --git a/js/src/auto-instrumentations/configs/all.ts b/js/src/auto-instrumentations/configs/all.ts index 905715e43..5be5a2816 100644 --- a/js/src/auto-instrumentations/configs/all.ts +++ b/js/src/auto-instrumentations/configs/all.ts @@ -30,6 +30,7 @@ import { openRouterConfigs } from "./openrouter"; import { openRouterAgentConfigs } from "./openrouter-agent"; import { piCodingAgentConfigs } from "./pi-coding-agent"; import { strandsAgentSDKConfigs } from "./strands-agent-sdk"; +import { stagehandConfigs } from "./stagehand"; interface InstrumentationConfigGroup { integrations: readonly (keyof InstrumentationIntegrationsConfig)[]; @@ -107,6 +108,10 @@ const defaultInstrumentationConfigGroups: readonly InstrumentationConfigGroup[] integrations: ["strandsAgentSDK"], configs: strandsAgentSDKConfigs, }, + { + integrations: ["stagehand"], + configs: stagehandConfigs, + }, { integrations: ["flue"], configs: flueConfigs, diff --git a/js/src/auto-instrumentations/configs/stagehand.test.ts b/js/src/auto-instrumentations/configs/stagehand.test.ts new file mode 100644 index 000000000..d59d15cb3 --- /dev/null +++ b/js/src/auto-instrumentations/configs/stagehand.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import { readDisabledInstrumentationEnvConfig } from "../../instrumentation/config"; +import { getDefaultInstrumentationConfigs } from "./all"; +import { stagehandConfigs } from "./stagehand"; + +const stagehandChannelNames = [ + "Stagehand.act", + "Stagehand.extract", + "Stagehand.observe", + "Stagehand.agent", +] as const; + +describe("stagehandConfigs", () => { + it("targets the v3.0 bundle and v3.1+ ESM/CJS class files", () => { + expect(stagehandConfigs).toHaveLength(12); + expect(stagehandConfigs).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + channelName: "Stagehand.act", + module: { + name: "@browserbasehq/stagehand", + versionRange: ">=3.0.0 <3.1.0", + filePath: "dist/index.js", + }, + functionQuery: { + className: "_V3", + methodName: "act", + kind: "Async", + }, + }), + expect.objectContaining({ + channelName: "Stagehand.agent", + module: expect.objectContaining({ + versionRange: ">=3.1.0 <4.0.0", + filePath: "dist/esm/lib/v3/v3.js", + }), + functionQuery: { + className: "V3", + methodName: "agent", + kind: "Sync", + }, + }), + expect.objectContaining({ + module: expect.objectContaining({ + filePath: "dist/cjs/lib/v3/v3.js", + }), + }), + ]), + ); + }); + + it("is enabled by default and supports every disable alias", () => { + expect( + getDefaultInstrumentationConfigs().some((config) => + stagehandChannelNames.includes(config.channelName as never), + ), + ).toBe(true); + + for (const alias of [ + "stagehand", + "browserbase-stagehand", + "@browserbasehq/stagehand", + ]) { + const disabledConfig = + readDisabledInstrumentationEnvConfig(alias).integrations; + expect(disabledConfig).toMatchObject({ stagehand: false }); + expect( + getDefaultInstrumentationConfigs({ + disabledIntegrationConfig: disabledConfig, + }).some((config) => + stagehandChannelNames.includes(config.channelName as never), + ), + ).toBe(false); + } + }); +}); diff --git a/js/src/auto-instrumentations/configs/stagehand.ts b/js/src/auto-instrumentations/configs/stagehand.ts new file mode 100644 index 000000000..f7fa90600 --- /dev/null +++ b/js/src/auto-instrumentations/configs/stagehand.ts @@ -0,0 +1,44 @@ +import type { InstrumentationConfig } from "../orchestrion-js"; +import { stagehandChannels } from "../../instrumentation/plugins/stagehand-channels"; + +const stagehandV30VersionRange = ">=3.0.0 <3.1.0"; +const stagehandV31VersionRange = ">=3.1.0 <4.0.0"; + +const methods = [ + ["act", stagehandChannels.act.channelName, "Async"], + ["extract", stagehandChannels.extract.channelName, "Async"], + ["observe", stagehandChannels.observe.channelName, "Async"], + ["agent", stagehandChannels.agent.channelName, "Sync"], +] as const; + +/** Instrumentation configurations for @browserbasehq/stagehand v3. */ +export const stagehandConfigs: InstrumentationConfig[] = [ + ...methods.map(([methodName, channelName, kind]) => ({ + channelName, + module: { + name: "@browserbasehq/stagehand", + versionRange: stagehandV30VersionRange, + filePath: "dist/index.js", + }, + functionQuery: { + className: "_V3", + methodName, + kind, + }, + })), + ...["esm", "cjs"].flatMap((moduleKind) => + methods.map(([methodName, channelName, kind]) => ({ + channelName, + module: { + name: "@browserbasehq/stagehand", + versionRange: stagehandV31VersionRange, + filePath: `dist/${moduleKind}/lib/v3/v3.js`, + }, + functionQuery: { + className: "V3", + methodName, + kind, + }, + })), + ), +]; diff --git a/js/src/exports.ts b/js/src/exports.ts index 02104f8c2..b8489ee19 100644 --- a/js/src/exports.ts +++ b/js/src/exports.ts @@ -212,6 +212,7 @@ export { wrapCursorSDK } from "./wrappers/cursor-sdk"; export { wrapPiCodingAgentSDK } from "./wrappers/pi-coding-agent"; export { wrapCloudflareAgent } from "./wrappers/cloudflare-agent"; export { wrapStrandsAgentSDK } from "./wrappers/strands-agent-sdk"; +export { wrapStagehand } from "./wrappers/stagehand"; export { wrapCloudflareAIChat } from "./wrappers/cloudflare-ai-chat"; export { wrapGoogleGenAI } from "./wrappers/google-genai"; export { wrapGoogleADK } from "./wrappers/google-adk"; diff --git a/js/src/instrumentation/braintrust-plugin.ts b/js/src/instrumentation/braintrust-plugin.ts index e2dfdad9e..617b502c7 100644 --- a/js/src/instrumentation/braintrust-plugin.ts +++ b/js/src/instrumentation/braintrust-plugin.ts @@ -25,6 +25,7 @@ import { PiCodingAgentPlugin } from "./plugins/pi-coding-agent-plugin"; import { StrandsAgentSDKPlugin } from "./plugins/strands-agent-sdk-plugin"; import { CloudflareAIChatPlugin } from "./plugins/cloudflare-ai-chat-plugin"; import { CloudflareAgentsPlugin } from "./plugins/cloudflare-agents-plugin"; +import { StagehandPlugin } from "./plugins/stagehand-plugin"; import type { InstrumentationIntegrationsConfig } from "./config"; export interface BraintrustPluginConfig { @@ -77,6 +78,7 @@ export class BraintrustPlugin extends BasePlugin { private strandsAgentSDKPlugin: StrandsAgentSDKPlugin | null = null; private cloudflareAIChatPlugin: CloudflareAIChatPlugin | null = null; private cloudflareAgentsPlugin: CloudflareAgentsPlugin | null = null; + private stagehandPlugin: StagehandPlugin | null = null; constructor(config: BraintrustPluginConfig = {}) { super(); @@ -201,6 +203,11 @@ export class BraintrustPlugin extends BasePlugin { this.strandsAgentSDKPlugin.enable(); } + if (integrations.stagehand !== false) { + this.stagehandPlugin = new StagehandPlugin(); + this.stagehandPlugin.enable(); + } + if (integrations.cloudflareAIChat !== false) { this.cloudflareAIChatPlugin = new CloudflareAIChatPlugin(); this.cloudflareAIChatPlugin.enable(); @@ -341,6 +348,11 @@ export class BraintrustPlugin extends BasePlugin { this.strandsAgentSDKPlugin = null; } + if (this.stagehandPlugin) { + this.stagehandPlugin.disable(); + this.stagehandPlugin = null; + } + if (this.cloudflareAIChatPlugin) { this.cloudflareAIChatPlugin.disable(); this.cloudflareAIChatPlugin = null; diff --git a/js/src/instrumentation/config.ts b/js/src/instrumentation/config.ts index c51a5b48d..6d4fd37ee 100644 --- a/js/src/instrumentation/config.ts +++ b/js/src/instrumentation/config.ts @@ -27,6 +27,7 @@ export interface InstrumentationIntegrationsConfig { openaiCodexSDK?: boolean; piCodingAgent?: boolean; strandsAgentSDK?: boolean; + stagehand?: boolean; cloudflareAgents?: boolean; langchain?: boolean; langgraph?: boolean; @@ -60,6 +61,9 @@ const envIntegrationAliases: Record< strandsagentsdk: "strandsAgentSDK", "strands-agent-sdk": "strandsAgentSDK", "@strands-agents/sdk": "strandsAgentSDK", + stagehand: "stagehand", + "browserbase-stagehand": "stagehand", + "@browserbasehq/stagehand": "stagehand", agents: "cloudflareAgents", "cloudflare-agents": "cloudflareAgents", cloudflareagents: "cloudflareAgents", @@ -152,6 +156,7 @@ export function getDefaultInstrumentationIntegrations(): Record< langsmith: true, piCodingAgent: true, strandsAgentSDK: true, + stagehand: true, cloudflareAgents: true, }; } diff --git a/js/src/instrumentation/plugins/instrumentation-names.test.ts b/js/src/instrumentation/plugins/instrumentation-names.test.ts index 2e342f9b2..328404a0d 100644 --- a/js/src/instrumentation/plugins/instrumentation-names.test.ts +++ b/js/src/instrumentation/plugins/instrumentation-names.test.ts @@ -30,6 +30,7 @@ import { openRouterAgentChannels } from "./openrouter-agent-channels"; import { openRouterChannels } from "./openrouter-channels"; import { piCodingAgentChannels } from "./pi-coding-agent-channels"; import { strandsAgentSDKChannels } from "./strands-agent-sdk-channels"; +import { stagehandChannels } from "./stagehand-channels"; describe("built-in instrumentation provenance names", () => { it.each([ @@ -74,6 +75,7 @@ describe("built-in instrumentation provenance names", () => { strandsAgentSDKChannels.agentStream, INSTRUMENTATION_NAMES.STRANDS_AGENT_SDK, ], + [stagehandChannels.act, INSTRUMENTATION_NAMES.STAGEHAND], ])("uses %s for its canonical channel group", (channel, expected) => { expect(channel.instrumentationName).toBe(expected); }); diff --git a/js/src/instrumentation/plugins/stagehand-agent.ts b/js/src/instrumentation/plugins/stagehand-agent.ts new file mode 100644 index 000000000..dfe6446b5 --- /dev/null +++ b/js/src/instrumentation/plugins/stagehand-agent.ts @@ -0,0 +1,61 @@ +import { debugLogger } from "../../debug-logger"; +import { isObject } from "../../util"; +import { stagehandChannels } from "./stagehand-channels"; +import type { StagehandAgent } from "../../vendor-sdk-types/stagehand"; + +const instrumentedAgents = new WeakSet(); + +/** Instruments the mutable object returned by Stagehand.agent(). */ +export function instrumentStagehandAgent( + value: unknown, + agentConfig: unknown, + stagehand: unknown, +): unknown { + if (!isObject(value) || instrumentedAgents.has(value)) { + return value; + } + + let execute: unknown; + try { + execute = value.execute; + } catch (error) { + debugLogger.error("Error reading Stagehand agent execute method:", error); + return value; + } + if (typeof execute !== "function") { + return value; + } + + const descriptor = Object.getOwnPropertyDescriptor(value, "execute"); + const wrappedExecute = function ( + this: StagehandAgent, + ...args: unknown[] + ): Promise { + return stagehandChannels.agentExecute.tracePromise( + () => Reflect.apply(execute, value, args), + { + agentConfig, + arguments: args, + self: value, + stagehand, + }, + ) as Promise; + }; + + try { + Object.defineProperty(value, "execute", { + configurable: descriptor?.configurable ?? true, + enumerable: descriptor?.enumerable ?? true, + value: wrappedExecute, + writable: descriptor?.writable ?? true, + }); + instrumentedAgents.add(value); + } catch (error) { + debugLogger.error( + "Error instrumenting Stagehand agent execute method:", + error, + ); + } + + return value; +} diff --git a/js/src/instrumentation/plugins/stagehand-channels.ts b/js/src/instrumentation/plugins/stagehand-channels.ts new file mode 100644 index 000000000..880e3a63c --- /dev/null +++ b/js/src/instrumentation/plugins/stagehand-channels.ts @@ -0,0 +1,38 @@ +import { channel, defineChannels } from "../core/channel-definitions"; +import { INSTRUMENTATION_NAMES } from "../../span-origin"; + +type StagehandChannelContext = { + self?: unknown; +}; + +type StagehandAgentExecuteContext = StagehandChannelContext & { + agentConfig?: unknown; + stagehand?: unknown; +}; + +export const stagehandChannels = defineChannels( + "@browserbasehq/stagehand", + { + act: channel({ + channelName: "Stagehand.act", + kind: "async", + }), + extract: channel({ + channelName: "Stagehand.extract", + kind: "async", + }), + observe: channel({ + channelName: "Stagehand.observe", + kind: "async", + }), + agent: channel({ + channelName: "Stagehand.agent", + kind: "sync-stream", + }), + agentExecute: channel({ + channelName: "Agent.execute", + kind: "async", + }), + }, + { instrumentationName: INSTRUMENTATION_NAMES.STAGEHAND }, +); diff --git a/js/src/instrumentation/plugins/stagehand-plugin-lifecycle.test.ts b/js/src/instrumentation/plugins/stagehand-plugin-lifecycle.test.ts new file mode 100644 index 000000000..d89ab51d6 --- /dev/null +++ b/js/src/instrumentation/plugins/stagehand-plugin-lifecycle.test.ts @@ -0,0 +1,168 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockStartSpan, tracingChannels } = vi.hoisted(() => ({ + mockStartSpan: vi.fn(), + tracingChannels: new Map(), +})); + +vi.mock("../../isomorph", () => ({ + default: { + newAsyncLocalStorage: vi.fn(() => ({ + getStore: vi.fn(() => undefined), + run: vi.fn((_store: unknown, callback: () => unknown) => callback()), + })), + newTracingChannel: vi.fn((name: string) => { + const channel = { + start: undefined, + subscribe: vi.fn((handlers) => tracingChannels.set(name, handlers)), + tracePromise: vi.fn((fn: () => PromiseLike) => fn()), + traceSync: vi.fn((fn: () => unknown) => fn()), + unsubscribe: vi.fn(), + }; + return channel; + }), + }, +})); + +vi.mock("../../logger", () => ({ + BRAINTRUST_CURRENT_SPAN_STORE: Symbol.for("test.current-span-store"), + _internalGetGlobalState: () => undefined, + startSpan: (...args: unknown[]) => mockStartSpan(...args), +})); + +import { StagehandPlugin } from "./stagehand-plugin"; + +describe("StagehandPlugin lifecycle", () => { + let spans: Array<{ + args: any; + end: ReturnType; + log: ReturnType; + }>; + + beforeEach(() => { + spans = []; + tracingChannels.clear(); + mockStartSpan.mockImplementation((args: any) => { + const span = { args, end: vi.fn(), log: vi.fn() }; + spans.push(span); + return span; + }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("keeps a streamed agent span open until the nested result settles", async () => { + const plugin = new StagehandPlugin(); + plugin.enable(); + const handlers = tracingChannels.get( + "orchestrion:@browserbasehq/stagehand:Agent.execute", + ); + let resolveResult!: (value: unknown) => void; + const nestedResult = new Promise((resolve) => { + resolveResult = resolve; + }); + const event: any = { + agentConfig: { + apiKey: "must-not-be-captured", + maxSteps: 8, + mode: "dom", + model: "browser-model", + stream: true, + }, + arguments: [{ instruction: "complete the task" }], + self: {}, + stagehand: {}, + }; + + handlers.start(event); + event.result = { result: nestedResult, stream: "preserved" }; + handlers.asyncEnd(event); + + expect(spans[0].end).not.toHaveBeenCalled(); + expect(spans[0].log).toHaveBeenNthCalledWith(1, { + input: { instruction: "complete the task" }, + metadata: { + model: "browser-model", + "stagehand.max_steps": 8, + "stagehand.mode": "dom", + "stagehand.streaming": true, + }, + }); + + resolveResult({ + actions: [{ taskCompleted: true, timeMs: 7, type: "act" }], + completed: true, + message: "done", + metadata: { cacheHit: true, debugUrl: "must-not-be-captured" }, + success: true, + usage: { input_tokens: 2, output_tokens: 3 }, + }); + await nestedResult; + await Promise.resolve(); + + expect(spans[0].log).toHaveBeenLastCalledWith({ + metadata: { "stagehand.cache_status": "hit" }, + metrics: { prompt_tokens: 2, completion_tokens: 3, tokens: 5 }, + output: { + actions: [{ task_completed: true, time_ms: 7, type: "act" }], + completed: true, + message: "done", + success: true, + }, + }); + expect(spans[0].end).toHaveBeenCalledTimes(1); + }); + + it("logs the original Error when a streamed result rejects", async () => { + const plugin = new StagehandPlugin(); + plugin.enable(); + const handlers = tracingChannels.get( + "orchestrion:@browserbasehq/stagehand:Agent.execute", + ); + const error = new Error("stream failed"); + let rejectResult!: (error: Error) => void; + const nestedResult = new Promise((_resolve, reject) => { + rejectResult = reject; + }); + const event: any = { + agentConfig: { stream: true }, + arguments: [{ instruction: "fail" }], + result: { result: nestedResult }, + self: {}, + }; + + handlers.start(event); + handlers.asyncEnd(event); + rejectResult(error); + await expect(nestedResult).rejects.toBe(error); + await Promise.resolve(); + + expect(spans[0].log).toHaveBeenLastCalledWith({ error }); + expect(spans[0].end).toHaveBeenCalledTimes(1); + }); + + it("instruments the mutable agent returned by the synchronous factory", async () => { + const plugin = new StagehandPlugin(); + plugin.enable(); + const handlers = tracingChannels.get( + "orchestrion:@browserbasehq/stagehand:Stagehand.agent", + ); + const originalExecute = vi.fn(async () => "complete"); + const agent = { execute: originalExecute }; + const event = { + arguments: [{ model: "browser-model" }], + result: agent, + self: { llmClient: { modelName: "fallback-model" } }, + }; + + handlers.end(event); + const wrappedExecute = agent.execute; + handlers.end(event); + + expect(agent.execute).toBe(wrappedExecute); + await expect(agent.execute()).resolves.toBe("complete"); + expect(originalExecute).toHaveBeenCalledTimes(1); + }); +}); diff --git a/js/src/instrumentation/plugins/stagehand-plugin.test.ts b/js/src/instrumentation/plugins/stagehand-plugin.test.ts new file mode 100644 index 000000000..d5a882646 --- /dev/null +++ b/js/src/instrumentation/plugins/stagehand-plugin.test.ts @@ -0,0 +1,152 @@ +import { describe, expect, it } from "vitest"; +import { z } from "zod"; +import { + extractActInput, + extractActionSummary, + extractAgentInput, + extractAgentMetrics, + extractAgentOutput, + extractExtractInput, + extractInstructionInput, + extractStructuredResult, +} from "./stagehand-plugin"; + +const SECRET = "must-not-be-captured"; + +describe("Stagehand capture contract", () => { + it("captures only instructions or action methods for act", () => { + expect(extractActInput([])).toBeUndefined(); + expect(extractActInput(["click submit"])).toEqual({ + instruction: "click submit", + }); + expect( + extractActInput([ + { + arguments: [SECRET], + description: SECRET, + method: "click", + selector: SECRET, + apiKey: SECRET, + }, + ]), + ).toEqual({ action: { method: "click" } }); + }); + + it("summarizes actions without selectors, arguments, or descriptions", () => { + const captured = extractActionSummary({ + actionDescription: SECRET, + actions: [ + { + arguments: [SECRET], + description: SECRET, + method: "click", + selector: SECRET, + }, + { method: "type", selector: SECRET }, + ], + cacheStatus: "HIT", + message: SECRET, + success: true, + }); + expect(captured).toEqual({ + success: true, + action_count: 2, + methods: ["click", "type"], + }); + expect(JSON.stringify(captured)).not.toContain(SECRET); + }); + + it("captures observe instructions but not runtime options", () => { + expect( + extractInstructionInput([ + { + instruction: "find the login button", + model: "model-name", + variableNames: [SECRET], + }, + ]), + ).toEqual({ instruction: "find the login button" }); + }); + + it("converts extract Zod schemas and omits raw page-text output", () => { + const input = extractExtractInput([ + "extract the profile", + z.object({ name: z.string(), age: z.number() }), + { apiKey: SECRET }, + ]); + expect(input).toMatchObject({ + instruction: "extract the profile", + schema: { + properties: { + age: { type: "number" }, + name: { type: "string" }, + }, + type: "object", + }, + }); + expect(extractStructuredResult("entire page text", [])).toBeUndefined(); + expect( + extractStructuredResult( + { cacheStatus: "MISS", name: "Ada", nested: { ok: true } }, + ["extract the profile"], + ), + ).toEqual({ name: "Ada", nested: { ok: true } }); + }); + + it("captures the bounded agent input, result, actions, and token metrics", () => { + const input = extractAgentInput([ + { + instruction: "research this topic", + output: z.object({ answer: z.string() }), + messages: [{ content: SECRET }], + callbacks: [SECRET], + }, + ]); + const output = extractAgentOutput({ + success: true, + completed: true, + message: "done", + output: { answer: "42" }, + actions: [ + { + type: "act", + taskCompleted: true, + timeMs: 12, + reasoning: SECRET, + selector: SECRET, + }, + ], + messages: [{ content: SECRET }], + metadata: { debugUrl: SECRET }, + }); + expect(input).toMatchObject({ + instruction: "research this topic", + output_schema: expect.objectContaining({ type: "object" }), + }); + expect(output).toEqual({ + success: true, + completed: true, + message: "done", + output: { answer: "42" }, + actions: [{ type: "act", task_completed: true, time_ms: 12 }], + }); + expect(JSON.stringify({ input, output })).not.toContain(SECRET); + expect( + extractAgentMetrics({ + usage: { + input_tokens: 10, + output_tokens: 4, + reasoning_tokens: 2, + cached_input_tokens: 3, + inference_time_ms: 900, + }, + }), + ).toEqual({ + prompt_tokens: 10, + completion_tokens: 4, + tokens: 14, + completion_reasoning_tokens: 2, + prompt_cached_tokens: 3, + }); + }); +}); diff --git a/js/src/instrumentation/plugins/stagehand-plugin.ts b/js/src/instrumentation/plugins/stagehand-plugin.ts new file mode 100644 index 000000000..719575b48 --- /dev/null +++ b/js/src/instrumentation/plugins/stagehand-plugin.ts @@ -0,0 +1,398 @@ +import { BasePlugin, toLoggedError } from "../core"; +import { + traceAsyncChannel, + traceStreamingChannel, + unsubscribeAll, +} from "../core/channel-tracing"; +import type { ChannelMessage } from "../core/channel-definitions"; +import type { IsoChannelHandlers, IsoTracingChannel } from "../../isomorph"; +import { debugLogger } from "../../debug-logger"; +import { SpanTypeAttribute, isObject, isPromiseLike } from "../../../util"; +import { zodToJsonSchema } from "../../zod/utils"; +import { stagehandChannels } from "./stagehand-channels"; +import { instrumentStagehandAgent } from "./stagehand-agent"; + +export class StagehandPlugin extends BasePlugin { + protected onEnable(): void { + this.unsubscribers.push( + traceAsyncChannel(stagehandChannels.act, { + name: "stagehand.act", + type: SpanTypeAttribute.TASK, + extractInput: (args, event) => ({ + input: extractActInput(args), + metadata: extractCallMetadata(args, event.self), + }), + extractOutput: extractActionSummary, + extractMetadata: extractResultCacheMetadata, + extractMetrics: () => ({}), + }), + traceAsyncChannel(stagehandChannels.extract, { + name: "stagehand.extract", + type: SpanTypeAttribute.TASK, + extractInput: (args, event) => ({ + input: extractExtractInput(args), + metadata: extractCallMetadata(args, event.self), + }), + extractOutput: (result, event) => + extractStructuredResult(result, event?.arguments ?? []), + extractMetadata: extractResultCacheMetadata, + extractMetrics: () => ({}), + }), + traceAsyncChannel(stagehandChannels.observe, { + name: "stagehand.observe", + type: SpanTypeAttribute.TASK, + extractInput: (args, event) => ({ + input: extractInstructionInput(args), + metadata: extractCallMetadata(args, event.self), + }), + extractOutput: extractActionSummary, + extractMetadata: extractResultCacheMetadata, + extractMetrics: () => ({}), + }), + traceStreamingChannel(stagehandChannels.agentExecute, { + name: "stagehand.agent.execute", + type: SpanTypeAttribute.TASK, + extractInput: (args, event) => ({ + input: extractAgentInput(args), + metadata: extractAgentMetadata(event.agentConfig, event.stagehand), + }), + extractOutput: extractAgentOutput, + extractMetadata: extractAgentResultMetadata, + extractMetrics: extractAgentMetrics, + patchResult: ({ result, span }) => { + if (!isObject(result) || !isPromiseLike(result.result)) { + return false; + } + + void Promise.resolve(result.result) + .then( + (finalResult) => { + try { + span.log({ + output: extractAgentOutput(finalResult), + metadata: extractAgentResultMetadata(finalResult), + metrics: extractAgentMetrics(finalResult), + }); + } catch (error) { + debugLogger.error( + "Error extracting streaming Stagehand agent result:", + error, + ); + } finally { + span.end(); + } + }, + (error) => { + try { + span.log({ + error: + error instanceof Error ? error : toLoggedError(error), + }); + } finally { + span.end(); + } + }, + ) + .catch((error) => { + debugLogger.error( + "Error finalizing streaming Stagehand agent span:", + error, + ); + }); + return true; + }, + }), + ); + + const tracingChannel = + stagehandChannels.agent.tracingChannel() as IsoTracingChannel< + ChannelMessage + >; + const handlers: IsoChannelHandlers< + ChannelMessage + > = { + end: (event) => { + instrumentStagehandAgent(event.result, event.arguments[0], event.self); + }, + }; + tracingChannel.subscribe(handlers); + this.unsubscribers.push(() => tracingChannel.unsubscribe(handlers)); + } + + protected onDisable(): void { + this.unsubscribers = unsubscribeAll(this.unsubscribers); + } +} + +export function extractActInput(args: unknown[]): unknown { + const first = args[0]; + if (typeof first === "string") { + return { instruction: first }; + } + if (!isObject(first)) { + return undefined; + } + if (typeof first.instruction === "string") { + return { instruction: first.instruction }; + } + if (typeof first.method === "string" && "selector" in first) { + return { action: { method: first.method } }; + } + if (isObject(first.action) && typeof first.action.method === "string") { + return { action: { method: first.action.method } }; + } + return undefined; +} + +export function extractInstructionInput(args: unknown[]): unknown { + const first = args[0]; + if (typeof first === "string") { + return { instruction: first }; + } + return isObject(first) && typeof first.instruction === "string" + ? { instruction: first.instruction } + : undefined; +} + +export function extractExtractInput(args: unknown[]): unknown { + const input = extractInstructionInput(args); + if (!input) { + return undefined; + } + + const first = args[0]; + const options = isObject(first) ? first : isObject(args[1]) ? args[1] : {}; + const schemaCandidate = + typeof first === "string" && isZodSchema(args[1]) + ? args[1] + : options.schema; + const schema = extractJsonSchema(schemaCandidate); + return schema ? { ...input, schema } : input; +} + +export function extractActionSummary(result: unknown): unknown { + const actions = Array.isArray(result) + ? result + : isObject(result) && Array.isArray(result.actions) + ? result.actions + : []; + const methods = actions.flatMap((action) => + isObject(action) && typeof action.method === "string" + ? [action.method] + : [], + ); + const output: Record = { + action_count: actions.length, + methods, + }; + if (isObject(result) && typeof result.success === "boolean") { + output.success = result.success; + } + return output; +} + +export function extractStructuredResult( + result: unknown, + args: readonly unknown[], +): unknown { + if (!hasExtractInstruction(args)) { + return undefined; + } + if (Array.isArray(result) || !isObject(result)) { + return result; + } + return Object.fromEntries( + Object.entries(result).filter(([key]) => key !== "cacheStatus"), + ); +} + +export function extractAgentInput(args: unknown[]): unknown { + const first = args[0]; + const options = isObject(first) ? first : isObject(args[1]) ? args[1] : {}; + const instruction = + typeof first === "string" + ? first + : typeof options.instruction === "string" + ? options.instruction + : undefined; + const outputSchema = extractJsonSchema( + options.output ?? options.outputSchema ?? options.schema, + ); + if (instruction === undefined && outputSchema === undefined) { + return undefined; + } + return { + ...(instruction !== undefined ? { instruction } : {}), + ...(outputSchema !== undefined ? { output_schema: outputSchema } : {}), + }; +} + +export function extractAgentOutput(result: unknown): unknown { + if (!isObject(result)) { + return undefined; + } + const output: Record = {}; + if (typeof result.success === "boolean") output.success = result.success; + if (typeof result.completed === "boolean") + output.completed = result.completed; + if (typeof result.message === "string") output.message = result.message; + if (Object.prototype.hasOwnProperty.call(result, "output")) { + output.output = result.output; + } + if (Array.isArray(result.actions)) { + output.actions = result.actions.map((action) => { + if (!isObject(action)) return {}; + const type = typeof action.type === "string" ? action.type : undefined; + const taskCompleted = + typeof action.taskCompleted === "boolean" + ? action.taskCompleted + : typeof action.task_completed === "boolean" + ? action.task_completed + : undefined; + const timeMs = + typeof action.timeMs === "number" + ? action.timeMs + : typeof action.time_ms === "number" + ? action.time_ms + : undefined; + return { + ...(type !== undefined ? { type } : {}), + ...(taskCompleted !== undefined + ? { task_completed: taskCompleted } + : {}), + ...(timeMs !== undefined ? { time_ms: timeMs } : {}), + }; + }); + } + return output; +} + +export function extractAgentMetrics(result: unknown): Record { + const metrics: Record = {}; + const usage = isObject(result) && isObject(result.usage) ? result.usage : {}; + const promptTokens = finiteNumber(usage.input_tokens); + const completionTokens = finiteNumber(usage.output_tokens); + const reasoningTokens = finiteNumber(usage.reasoning_tokens); + const cachedTokens = finiteNumber(usage.cached_input_tokens); + if (promptTokens !== undefined) metrics.prompt_tokens = promptTokens; + if (completionTokens !== undefined) { + metrics.completion_tokens = completionTokens; + } + if (promptTokens !== undefined || completionTokens !== undefined) { + metrics.tokens = (promptTokens ?? 0) + (completionTokens ?? 0); + } + if (reasoningTokens !== undefined) { + metrics.completion_reasoning_tokens = reasoningTokens; + } + if (cachedTokens !== undefined) { + metrics.prompt_cached_tokens = cachedTokens; + } + return metrics; +} + +function extractCallMetadata( + args: readonly unknown[], + stagehand: unknown, +): Record | undefined { + let model: string | undefined; + for (const arg of args) { + if (isObject(arg)) { + model = extractModel(arg.model); + if (model) break; + } + } + model ??= extractStagehandModel(stagehand); + return model ? { model } : undefined; +} + +function extractAgentMetadata( + config: unknown, + stagehand: unknown, +): Record | undefined { + const agentConfig = isObject(config) ? config : {}; + const metadata: Record = {}; + const model = + extractModel(agentConfig.model) ?? extractStagehandModel(stagehand); + if (model) metadata.model = model; + if (typeof agentConfig.mode === "string") { + metadata["stagehand.mode"] = agentConfig.mode; + } + const maxSteps = finiteNumber(agentConfig.maxSteps); + if (maxSteps !== undefined) metadata["stagehand.max_steps"] = maxSteps; + if (typeof agentConfig.stream === "boolean") { + metadata["stagehand.streaming"] = agentConfig.stream; + } + return Object.keys(metadata).length > 0 ? metadata : undefined; +} + +function extractResultCacheMetadata( + result: unknown, +): Record | undefined { + const cacheStatus = Array.isArray(result) + ? normalizeCacheStatus( + Object.getOwnPropertyDescriptor(result, "cacheStatus")?.value, + ) + : isObject(result) + ? normalizeCacheStatus(result.cacheStatus) + : undefined; + return cacheStatus ? { "stagehand.cache_status": cacheStatus } : undefined; +} + +function extractAgentResultMetadata( + result: unknown, +): Record | undefined { + const metadata = + isObject(result) && isObject(result.metadata) ? result.metadata : undefined; + const cacheStatus = normalizeCacheStatus(metadata?.cacheHit); + return cacheStatus ? { "stagehand.cache_status": cacheStatus } : undefined; +} + +function extractStagehandModel(stagehand: unknown): string | undefined { + if (!isObject(stagehand)) return undefined; + const llmClient = isObject(stagehand.llmClient) + ? stagehand.llmClient + : undefined; + return extractModel(llmClient?.modelName); +} + +function extractModel(model: unknown): string | undefined { + if (typeof model === "string") return model; + return isObject(model) && typeof model.modelName === "string" + ? model.modelName + : undefined; +} + +function normalizeCacheStatus(value: unknown): "hit" | "miss" | undefined { + if (value === true) return "hit"; + if (value === false) return "miss"; + if (typeof value !== "string") return undefined; + const normalized = value.toLowerCase(); + return normalized === "hit" || normalized === "miss" ? normalized : undefined; +} + +function hasExtractInstruction(args: readonly unknown[]): boolean { + return ( + typeof args[0] === "string" || + (isObject(args[0]) && typeof args[0].instruction === "string") + ); +} + +function isZodSchema(value: unknown): boolean { + return isObject(value) && ("_def" in value || "_zod" in value); +} + +function extractJsonSchema(value: unknown): unknown { + if (!isZodSchema(value)) return undefined; + try { + return zodToJsonSchema(value as never); + } catch (error) { + debugLogger.error("Error converting Stagehand output schema:", error); + return undefined; + } +} + +function finiteNumber(value: unknown): number | undefined { + return typeof value === "number" && Number.isFinite(value) + ? value + : undefined; +} diff --git a/js/src/span-origin.ts b/js/src/span-origin.ts index 0fd2f5fc8..bffd370ab 100644 --- a/js/src/span-origin.ts +++ b/js/src/span-origin.ts @@ -36,6 +36,7 @@ export const INSTRUMENTATION_NAMES = { OPENROUTER_AGENT: "openrouter-agent", PI_CODING_AGENT: "pi-coding-agent", STRANDS_AGENT_SDK: "strands-agent-sdk", + STAGEHAND: "stagehand", } as const; export type SpanInstrumentationName = diff --git a/js/src/vendor-sdk-types/stagehand.ts b/js/src/vendor-sdk-types/stagehand.ts new file mode 100644 index 000000000..9da4377be --- /dev/null +++ b/js/src/vendor-sdk-types/stagehand.ts @@ -0,0 +1,65 @@ +/** + * Minimal structural types for @browserbasehq/stagehand v3. + * + * Keep these types limited to the public fields used by instrumentation so the + * Braintrust SDK does not take a runtime or type dependency on Stagehand. + */ + +export type StagehandAction = { + method?: unknown; +}; + +export type StagehandActResult = { + success?: unknown; + actions?: unknown; + cacheStatus?: unknown; +}; + +export type StagehandAgentUsage = { + input_tokens?: unknown; + output_tokens?: unknown; + reasoning_tokens?: unknown; + cached_input_tokens?: unknown; +}; + +export type StagehandAgentAction = { + type?: unknown; + taskCompleted?: unknown; + task_completed?: unknown; + timeMs?: unknown; + time_ms?: unknown; +}; + +export type StagehandAgentResult = { + success?: unknown; + completed?: unknown; + message?: unknown; + output?: unknown; + actions?: unknown; + metadata?: unknown; + usage?: StagehandAgentUsage | unknown; +}; + +export type StagehandAgentStreamResult = { + result?: unknown; +}; + +export type StagehandAgent = { + execute: (...args: unknown[]) => Promise; +}; + +export type StagehandInstance = { + act: (...args: unknown[]) => Promise; + extract: (...args: unknown[]) => Promise; + observe: (...args: unknown[]) => Promise; + agent: (...args: unknown[]) => StagehandAgent | unknown; +}; + +export type StagehandConstructor = new ( + ...args: unknown[] +) => StagehandInstance; + +export type StagehandModule = { + Stagehand?: StagehandConstructor; + V3?: StagehandConstructor; +}; diff --git a/js/src/wrappers/stagehand.test.ts b/js/src/wrappers/stagehand.test.ts new file mode 100644 index 000000000..f6e59ddf3 --- /dev/null +++ b/js/src/wrappers/stagehand.test.ts @@ -0,0 +1,114 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const { tracePromise, traceSync } = vi.hoisted(() => ({ + tracePromise: vi.fn((fn: () => PromiseLike) => fn()), + traceSync: vi.fn((fn: () => unknown) => fn()), +})); + +vi.mock("../isomorph", () => ({ + default: { + newTracingChannel: vi.fn(() => ({ + subscribe: vi.fn(), + tracePromise, + traceSync, + unsubscribe: vi.fn(), + })), + }, +})); + +import { wrapStagehand } from "./stagehand"; + +describe("wrapStagehand", () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it("preserves class aliases, instanceof, static properties, and private fields", async () => { + class Stagehand { + static version = "3.7.1"; + #model = "private-model"; + + async act(value: unknown) { + return { model: this.#model, value }; + } + async extract(value: unknown) { + return value; + } + async observe(value: unknown) { + return value; + } + agent() { + return { execute: async (value: unknown) => value }; + } + } + const wrapped = wrapStagehand({ Stagehand, V3: Stagehand }); + + expect(wrapped.Stagehand).toBe(wrapped.V3); + expect(wrapped.Stagehand.version).toBe("3.7.1"); + const instance = new wrapped.Stagehand(); + expect(instance).toBeInstanceOf(Stagehand); + await expect(instance.act("secret instruction")).resolves.toEqual({ + model: "private-model", + value: "secret instruction", + }); + expect(tracePromise).toHaveBeenCalledTimes(1); + }); + + it("wraps public methods and agent execute once without changing results", async () => { + const originalExecute = vi.fn(async (value: unknown) => value); + const agent = { execute: originalExecute }; + class Stagehand { + act = vi.fn(async () => "act"); + extract = vi.fn(async () => "extract"); + observe = vi.fn(async () => "observe"); + agent = vi.fn(() => agent); + } + const wrapped = wrapStagehand({ Stagehand }); + const instance = new wrapped.Stagehand(); + + await expect(instance.act()).resolves.toBe("act"); + await expect(instance.extract()).resolves.toBe("extract"); + await expect(instance.observe()).resolves.toBe("observe"); + const firstAgent = instance.agent(); + const wrappedExecute = firstAgent.execute; + expect(instance.agent()).toBe(firstAgent); + expect(firstAgent.execute).toBe(wrappedExecute); + await expect(firstAgent.execute("execute")).resolves.toBe("execute"); + + expect(traceSync).toHaveBeenCalledTimes(2); + expect(tracePromise).toHaveBeenCalledTimes(4); + expect(originalExecute).toHaveBeenCalledWith("execute"); + }); + + it("preserves thrown and rejected errors", async () => { + const syncError = new Error("factory failed"); + const asyncError = new Error("act failed"); + class Stagehand { + async act() { + throw asyncError; + } + async extract() {} + async observe() {} + agent(): never { + throw syncError; + } + } + const instance = new (wrapStagehand({ Stagehand }).Stagehand)(); + + await expect(instance.act()).rejects.toBe(asyncError); + expect(() => instance.agent()).toThrow(syncError); + }); + + it("handles module namespace-like objects", () => { + class Stagehand {} + const sdk = Object.defineProperty({}, "Stagehand", { + configurable: false, + enumerable: true, + value: Stagehand, + writable: false, + }); + + const wrapped = wrapStagehand(sdk as { Stagehand: typeof Stagehand }); + expect(new wrapped.Stagehand()).toBeInstanceOf(Stagehand); + }); +}); diff --git a/js/src/wrappers/stagehand.ts b/js/src/wrappers/stagehand.ts new file mode 100644 index 000000000..b53e5ca33 --- /dev/null +++ b/js/src/wrappers/stagehand.ts @@ -0,0 +1,154 @@ +import { stagehandChannels } from "../instrumentation/plugins/stagehand-channels"; +import { instrumentStagehandAgent } from "../instrumentation/plugins/stagehand-agent"; +import type { + StagehandConstructor, + StagehandInstance, + StagehandModule, +} from "../vendor-sdk-types/stagehand"; + +const WRAPPED_CLASS = Symbol.for("braintrust.stagehand.wrapped-class"); +const WRAPPED_INSTANCE = Symbol.for("braintrust.stagehand.wrapped-instance"); + +/** + * Wraps the public Stagehand v3 API with Braintrust tracing. The wrapper emits + * the same channels used by auto-instrumentation, so both paths share one span + * contract. + */ +export function wrapStagehand(sdk: T): T { + if (!sdk || typeof sdk !== "object") { + return sdk; + } + + const maybeSDK = sdk as Record; + if ( + typeof maybeSDK.Stagehand !== "function" && + typeof maybeSDK.V3 !== "function" + ) { + return sdk; + } + + const target = isModuleNamespace(sdk) + ? Object.setPrototypeOf({}, sdk) + : (sdk as Record); + const wrappedClasses = new WeakMap< + (...args: unknown[]) => unknown, + StagehandConstructor + >(); + + return new Proxy(target, { + get(target, prop, receiver) { + const value = Reflect.get(target, prop, receiver); + if ( + (prop === "Stagehand" || prop === "V3") && + typeof value === "function" + ) { + const existing = wrappedClasses.get(value); + if (existing) { + return existing; + } + const wrapped = wrapStagehandClass( + value as unknown as StagehandConstructor, + ); + wrappedClasses.set(value, wrapped); + return wrapped; + } + return value; + }, + }) as T & StagehandModule; +} + +function isModuleNamespace(obj: unknown): boolean { + if (!obj || typeof obj !== "object") { + return false; + } + if (obj.constructor?.name === "Module") { + return true; + } + const keys = Object.keys(obj); + if (keys.length === 0) { + return false; + } + const descriptor = Object.getOwnPropertyDescriptor(obj, keys[0]); + return descriptor ? !descriptor.configurable && !descriptor.writable : false; +} + +function wrapStagehandClass( + StagehandClass: StagehandConstructor, +): StagehandConstructor { + if ( + (StagehandClass as unknown as Record)[WRAPPED_CLASS] + ) { + return StagehandClass; + } + + return new Proxy(StagehandClass, { + get(target, prop, receiver) { + if (prop === WRAPPED_CLASS) { + return true; + } + const value = Reflect.get(target, prop, receiver); + return typeof value === "function" ? value.bind(target) : value; + }, + construct(target, args, newTarget) { + return wrapStagehandInstance(Reflect.construct(target, args, newTarget)); + }, + }); +} + +function wrapStagehandInstance( + stagehand: StagehandInstance, +): StagehandInstance { + if (!stagehand || typeof stagehand !== "object") { + return stagehand; + } + if ( + (stagehand as unknown as Record)[WRAPPED_INSTANCE] + ) { + return stagehand; + } + + const methodCache = new Map unknown>(); + const proxy = new Proxy(stagehand, { + get(target, prop) { + if (prop === WRAPPED_INSTANCE) { + return true; + } + + const value = Reflect.get(target, prop, target); + if (typeof value !== "function") { + return value; + } + const cached = methodCache.get(prop); + if (cached) { + return cached; + } + + let wrapped: (...args: unknown[]) => unknown; + if (prop === "act" || prop === "extract" || prop === "observe") { + const channel = stagehandChannels[prop]; + wrapped = (...args: unknown[]) => + channel.tracePromise( + () => Reflect.apply(value, target, args) as PromiseLike, + { + arguments: args, + self: target, + }, + ); + } else if (prop === "agent") { + wrapped = (...args: unknown[]) => { + const agent = stagehandChannels.agent.traceSync( + () => Reflect.apply(value, target, args), + { arguments: args, self: target }, + ); + return instrumentStagehandAgent(agent, args[0], target); + }; + } else { + wrapped = value.bind(target); + } + methodCache.set(prop, wrapped); + return wrapped; + }, + }); + + return proxy; +} From 19ba1df219bd2a018f181ac9d8970f9ac6b57a8d Mon Sep 17 00:00:00 2001 From: lforst <8118419+lforst@users.noreply.github.com> Date: Tue, 4 Aug 2026 09:57:05 +0000 Subject: [PATCH 2/4] Update PR #2335 --- .changeset/stagehand-instrumentation.md | 5 +++++ js/src/vendor-sdk-types/stagehand.ts | 20 ++------------------ 2 files changed, 7 insertions(+), 18 deletions(-) create mode 100644 .changeset/stagehand-instrumentation.md diff --git a/.changeset/stagehand-instrumentation.md b/.changeset/stagehand-instrumentation.md new file mode 100644 index 000000000..238dd14ad --- /dev/null +++ b/.changeset/stagehand-instrumentation.md @@ -0,0 +1,5 @@ +--- +"braintrust": minor +--- + +feat: Add stagehand instrumentation diff --git a/js/src/vendor-sdk-types/stagehand.ts b/js/src/vendor-sdk-types/stagehand.ts index 9da4377be..bd2ecaa15 100644 --- a/js/src/vendor-sdk-types/stagehand.ts +++ b/js/src/vendor-sdk-types/stagehand.ts @@ -5,31 +5,19 @@ * Braintrust SDK does not take a runtime or type dependency on Stagehand. */ -export type StagehandAction = { - method?: unknown; -}; - export type StagehandActResult = { success?: unknown; actions?: unknown; cacheStatus?: unknown; }; -export type StagehandAgentUsage = { +type StagehandAgentUsage = { input_tokens?: unknown; output_tokens?: unknown; reasoning_tokens?: unknown; cached_input_tokens?: unknown; }; -export type StagehandAgentAction = { - type?: unknown; - taskCompleted?: unknown; - task_completed?: unknown; - timeMs?: unknown; - time_ms?: unknown; -}; - export type StagehandAgentResult = { success?: unknown; completed?: unknown; @@ -37,11 +25,7 @@ export type StagehandAgentResult = { output?: unknown; actions?: unknown; metadata?: unknown; - usage?: StagehandAgentUsage | unknown; -}; - -export type StagehandAgentStreamResult = { - result?: unknown; + usage?: StagehandAgentUsage; }; export type StagehandAgent = { From 9d45739dbc12aca155a4bdeef074b203405c72f8 Mon Sep 17 00:00:00 2001 From: lforst <8118419+lforst@users.noreply.github.com> Date: Tue, 4 Aug 2026 11:21:26 +0000 Subject: [PATCH 3/4] Update PR #2335 --- .../scenario.impl.mjs | 61 ++++++++++++++++++- .../scenario.test.ts | 24 +++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/e2e/scenarios/stagehand-instrumentation/scenario.impl.mjs b/e2e/scenarios/stagehand-instrumentation/scenario.impl.mjs index dc9bdb79f..b231a417a 100644 --- a/e2e/scenarios/stagehand-instrumentation/scenario.impl.mjs +++ b/e2e/scenarios/stagehand-instrumentation/scenario.impl.mjs @@ -29,6 +29,39 @@ const SHOP_HTML = ` `; +const STABLE_ACCESSIBILITY_TREE = `[0-2] RootWebArea: Trail Supply + [0-4] scrollable, html + [0-9] main + [0-10] heading: Trail Backpack + [0-12] paragraph + [0-13] StaticText: $129.00 + [0-14] button: Add to cart + [0-16] region: Shopping cart + [0-17] paragraph + [0-18] StaticText: Cart is empty + [0-19] button: Checkout`; + +export function stabilizeStagehandActMessages(messages) { + let liveElementId; + const normalizedMessages = messages.map((message) => { + if (!message || typeof message.content !== "string") { + return message; + } + liveElementId ??= message.content.match( + /^\s*\[(\d+-\d+)\]\s+button: Add to cart\s*$/m, + )?.[1]; + return { + ...message, + content: message.content.replace( + /Accessibility Tree:\s*[\s\S]*$/, + `Accessibility Tree: \n${STABLE_ACCESSIBILITY_TREE}\n`, + ), + }; + }); + + return { liveElementId, messages: normalizedMessages }; +} + async function startShopServer() { const server = createServer((_request, response) => { response.writeHead(200, { "content-type": "text/html; charset=utf-8" }); @@ -85,15 +118,39 @@ async function createStagehandSession(Stagehand, instrumentedAI) { "Expected Stagehand act to request a structured response", ); } + // Chromium's accessibility node IDs vary across browser versions. Keep the + // deterministic fixture prompt stable for cassette replay, then translate + // the model's fixture ID back to the live page before Stagehand acts on it. + const { liveElementId, messages } = stabilizeStagehandActMessages( + options.messages, + ); + if (!liveElementId) { + throw new Error("Expected Stagehand to observe the add-to-cart button"); + } const response = await instrumentedAI.generateObject({ allowSystemInMessages: true, - messages: options.messages, + messages, model: languageModel, schema: options.response_model.schema, temperature: options.temperature, }); + let data = response.object; + if (data && typeof data === "object") { + if ("elementId" in data) { + data = { ...data, elementId: liveElementId }; + } else if ( + "action" in data && + data.action && + typeof data.action === "object" + ) { + data = { + ...data, + action: { ...data.action, elementId: liveElementId }, + }; + } + } return { - data: response.object, + data, usage: { cached_input_tokens: response.usage.cachedInputTokens ?? 0, completion_tokens: response.usage.outputTokens ?? 0, diff --git a/e2e/scenarios/stagehand-instrumentation/scenario.test.ts b/e2e/scenarios/stagehand-instrumentation/scenario.test.ts index f4c8d4efa..d94456a8c 100644 --- a/e2e/scenarios/stagehand-instrumentation/scenario.test.ts +++ b/e2e/scenarios/stagehand-instrumentation/scenario.test.ts @@ -1,10 +1,11 @@ -import { describe } from "vitest"; +import { describe, expect, test } from "vitest"; import { prepareScenarioDir, readInstalledPackageVersion, resolveScenarioDir, } from "../../helpers/scenario-harness"; import { defineStagehandInstrumentationAssertions } from "./assertions"; +import { stabilizeStagehandActMessages } from "./scenario.impl.mjs"; const originalScenarioDir = resolveScenarioDir(import.meta.url); const scenarioDir = await prepareScenarioDir({ @@ -27,6 +28,27 @@ const scenarios = await Promise.all( })), ); +test("stabilizes Chromium accessibility IDs for cassette replay", () => { + const result = stabilizeStagehandActMessages([ + { + content: `instruction: Add the Trail Backpack to the cart +Accessibility Tree: +[7-20] RootWebArea: Trail Supply + [7-42] button: Add to cart`, + role: "user", + }, + ]); + + expect(result.liveElementId).toBe("7-42"); + expect(result.messages).toEqual([ + { + content: expect.stringContaining("[0-14] button: Add to cart"), + role: "user", + }, + ]); + expect(result.messages[0]?.content).not.toContain("[7-42]"); +}); + describe.sequential("variants", () => { for (const scenario of scenarios) { describe.sequential(`Stagehand ${scenario.version}`, () => { From 56af8bd726a1ee3e75db8f5c7fad0257a677baba Mon Sep 17 00:00:00 2001 From: lforst <8118419+lforst@users.noreply.github.com> Date: Tue, 4 Aug 2026 13:18:58 +0000 Subject: [PATCH 4/4] Update PR #2335 --- .../scenario.test.ts | 4 +- .../plugins/stagehand-agent.ts | 37 ++++++---- .../plugins/stagehand-channels.ts | 52 +++++++++++--- .../stagehand-plugin-lifecycle.test.ts | 48 +++++++++++++ .../plugins/stagehand-plugin.test.ts | 7 +- .../plugins/stagehand-plugin.ts | 54 ++++++-------- js/src/vendor-sdk-types/stagehand.ts | 72 +++++++++++++++++-- js/src/wrappers/stagehand.ts | 50 +++++++++---- 8 files changed, 243 insertions(+), 81 deletions(-) diff --git a/e2e/scenarios/stagehand-instrumentation/scenario.test.ts b/e2e/scenarios/stagehand-instrumentation/scenario.test.ts index d94456a8c..337b5120d 100644 --- a/e2e/scenarios/stagehand-instrumentation/scenario.test.ts +++ b/e2e/scenarios/stagehand-instrumentation/scenario.test.ts @@ -63,7 +63,7 @@ describe.sequential("variants", () => { variantKey: `${scenario.snapshotPrefix}-wrapped`, }, originalScenarioDir, - variantKey: `${scenario.snapshotPrefix}-wrapped`, + variantKey: scenario.snapshotPrefix, }, scenarioDir, timeoutMs: TIMEOUT_MS, @@ -86,7 +86,7 @@ describe.sequential("variants", () => { variantKey: `${scenario.snapshotPrefix}-auto-hook`, }, originalScenarioDir, - variantKey: `${scenario.snapshotPrefix}-auto-hook`, + variantKey: scenario.snapshotPrefix, }, scenarioDir, timeoutMs: TIMEOUT_MS, diff --git a/js/src/instrumentation/plugins/stagehand-agent.ts b/js/src/instrumentation/plugins/stagehand-agent.ts index dfe6446b5..d157b70d1 100644 --- a/js/src/instrumentation/plugins/stagehand-agent.ts +++ b/js/src/instrumentation/plugins/stagehand-agent.ts @@ -1,55 +1,64 @@ import { debugLogger } from "../../debug-logger"; import { isObject } from "../../util"; import { stagehandChannels } from "./stagehand-channels"; -import type { StagehandAgent } from "../../vendor-sdk-types/stagehand"; +import type { + StagehandAgent, + StagehandAgentConfig, + StagehandAgentExecuteArguments, + StagehandInstance, +} from "../../vendor-sdk-types/stagehand"; const instrumentedAgents = new WeakSet(); /** Instruments the mutable object returned by Stagehand.agent(). */ export function instrumentStagehandAgent( value: unknown, - agentConfig: unknown, - stagehand: unknown, + agentConfig: StagehandAgentConfig | undefined, + stagehand: StagehandInstance | undefined, ): unknown { if (!isObject(value) || instrumentedAgents.has(value)) { return value; } + const agent = value as StagehandAgent; + let descriptor: PropertyDescriptor | undefined; let execute: unknown; try { - execute = value.execute; + execute = agent.execute; + descriptor = Object.getOwnPropertyDescriptor(agent, "execute"); } catch (error) { - debugLogger.error("Error reading Stagehand agent execute method:", error); + debugLogger.error( + "Error inspecting Stagehand agent execute method:", + error, + ); return value; } if (typeof execute !== "function") { return value; } - const descriptor = Object.getOwnPropertyDescriptor(value, "execute"); const wrappedExecute = function ( - this: StagehandAgent, - ...args: unknown[] - ): Promise { + ...args: StagehandAgentExecuteArguments + ): ReturnType { return stagehandChannels.agentExecute.tracePromise( - () => Reflect.apply(execute, value, args), + () => Reflect.apply(execute, agent, args), { agentConfig, arguments: args, - self: value, + self: agent, stagehand, }, - ) as Promise; + ); }; try { - Object.defineProperty(value, "execute", { + Object.defineProperty(agent, "execute", { configurable: descriptor?.configurable ?? true, enumerable: descriptor?.enumerable ?? true, value: wrappedExecute, writable: descriptor?.writable ?? true, }); - instrumentedAgents.add(value); + instrumentedAgents.add(agent); } catch (error) { debugLogger.error( "Error instrumenting Stagehand agent execute method:", diff --git a/js/src/instrumentation/plugins/stagehand-channels.ts b/js/src/instrumentation/plugins/stagehand-channels.ts index 880e3a63c..db4211ab7 100644 --- a/js/src/instrumentation/plugins/stagehand-channels.ts +++ b/js/src/instrumentation/plugins/stagehand-channels.ts @@ -1,35 +1,69 @@ import { channel, defineChannels } from "../core/channel-definitions"; import { INSTRUMENTATION_NAMES } from "../../span-origin"; +import type { + StagehandActArguments, + StagehandActResult, + StagehandAgent, + StagehandAgentConfig, + StagehandAgentExecuteArguments, + StagehandAgentResult, + StagehandAgentStreamResult, + StagehandExtractArguments, + StagehandInstance, + StagehandObserveArguments, + StagehandObserveResult, +} from "../../vendor-sdk-types/stagehand"; type StagehandChannelContext = { - self?: unknown; + self?: StagehandInstance; }; -type StagehandAgentExecuteContext = StagehandChannelContext & { - agentConfig?: unknown; - stagehand?: unknown; +type StagehandAgentExecuteContext = { + agentConfig?: StagehandAgentConfig; + self?: StagehandAgent; + stagehand?: StagehandInstance; }; export const stagehandChannels = defineChannels( "@browserbasehq/stagehand", { - act: channel({ + act: channel< + StagehandActArguments, + StagehandActResult, + StagehandChannelContext + >({ channelName: "Stagehand.act", kind: "async", }), - extract: channel({ + extract: channel< + StagehandExtractArguments, + unknown, + StagehandChannelContext + >({ channelName: "Stagehand.extract", kind: "async", }), - observe: channel({ + observe: channel< + StagehandObserveArguments, + StagehandObserveResult, + StagehandChannelContext + >({ channelName: "Stagehand.observe", kind: "async", }), - agent: channel({ + agent: channel< + [config?: StagehandAgentConfig], + StagehandAgent, + StagehandChannelContext + >({ channelName: "Stagehand.agent", kind: "sync-stream", }), - agentExecute: channel({ + agentExecute: channel< + StagehandAgentExecuteArguments, + StagehandAgentResult | StagehandAgentStreamResult, + StagehandAgentExecuteContext + >({ channelName: "Agent.execute", kind: "async", }), diff --git a/js/src/instrumentation/plugins/stagehand-plugin-lifecycle.test.ts b/js/src/instrumentation/plugins/stagehand-plugin-lifecycle.test.ts index d89ab51d6..5b6d7fa67 100644 --- a/js/src/instrumentation/plugins/stagehand-plugin-lifecycle.test.ts +++ b/js/src/instrumentation/plugins/stagehand-plugin-lifecycle.test.ts @@ -7,6 +7,7 @@ const { mockStartSpan, tracingChannels } = vi.hoisted(() => ({ vi.mock("../../isomorph", () => ({ default: { + getEnv: vi.fn(() => undefined), newAsyncLocalStorage: vi.fn(() => ({ getStore: vi.fn(() => undefined), run: vi.fn((_store: unknown, callback: () => unknown) => callback()), @@ -143,6 +144,29 @@ describe("StagehandPlugin lifecycle", () => { expect(spans[0].end).toHaveBeenCalledTimes(1); }); + it("contains errors while inspecting a streamed result", () => { + const plugin = new StagehandPlugin(); + plugin.enable(); + const handlers = tracingChannels.get( + "orchestrion:@browserbasehq/stagehand:Agent.execute", + ); + const event: any = { + agentConfig: { stream: true }, + arguments: [{ instruction: "complete the task" }], + self: {}, + }; + + handlers.start(event); + event.result = Object.defineProperty({}, "result", { + get() { + throw new Error("unreadable result"); + }, + }); + + expect(() => handlers.asyncEnd(event)).not.toThrow(); + expect(spans[0].end).toHaveBeenCalledTimes(1); + }); + it("instruments the mutable agent returned by the synchronous factory", async () => { const plugin = new StagehandPlugin(); plugin.enable(); @@ -165,4 +189,28 @@ describe("StagehandPlugin lifecycle", () => { await expect(agent.execute()).resolves.toBe("complete"); expect(originalExecute).toHaveBeenCalledTimes(1); }); + + it("contains errors while inspecting an agent execute descriptor", () => { + const plugin = new StagehandPlugin(); + plugin.enable(); + const handlers = tracingChannels.get( + "orchestrion:@browserbasehq/stagehand:Stagehand.agent", + ); + const agent = new Proxy( + { execute: async () => "complete" }, + { + getOwnPropertyDescriptor() { + throw new Error("unreadable descriptor"); + }, + }, + ); + + expect(() => + handlers.end({ + arguments: [{ model: "browser-model" }], + result: agent, + self: { llmClient: { modelName: "fallback-model" } }, + }), + ).not.toThrow(); + }); }); diff --git a/js/src/instrumentation/plugins/stagehand-plugin.test.ts b/js/src/instrumentation/plugins/stagehand-plugin.test.ts index d5a882646..5836b2806 100644 --- a/js/src/instrumentation/plugins/stagehand-plugin.test.ts +++ b/js/src/instrumentation/plugins/stagehand-plugin.test.ts @@ -59,11 +59,8 @@ describe("Stagehand capture contract", () => { it("captures observe instructions but not runtime options", () => { expect( extractInstructionInput([ - { - instruction: "find the login button", - model: "model-name", - variableNames: [SECRET], - }, + "find the login button", + { model: "model-name", variableNames: [SECRET] }, ]), ).toEqual({ instruction: "find the login button" }); }); diff --git a/js/src/instrumentation/plugins/stagehand-plugin.ts b/js/src/instrumentation/plugins/stagehand-plugin.ts index 719575b48..7d2ad040a 100644 --- a/js/src/instrumentation/plugins/stagehand-plugin.ts +++ b/js/src/instrumentation/plugins/stagehand-plugin.ts @@ -60,11 +60,24 @@ export class StagehandPlugin extends BasePlugin { extractMetadata: extractAgentResultMetadata, extractMetrics: extractAgentMetrics, patchResult: ({ result, span }) => { - if (!isObject(result) || !isPromiseLike(result.result)) { + let finalResult: unknown; + try { + if (!isObject(result)) { + return false; + } + finalResult = Reflect.get(result, "result"); + if (!isPromiseLike(finalResult)) { + return false; + } + } catch (error) { + debugLogger.error( + "Error inspecting streaming Stagehand agent result:", + error, + ); return false; } - void Promise.resolve(result.result) + void Promise.resolve(finalResult) .then( (finalResult) => { try { @@ -132,15 +145,9 @@ export function extractActInput(args: unknown[]): unknown { if (!isObject(first)) { return undefined; } - if (typeof first.instruction === "string") { - return { instruction: first.instruction }; - } if (typeof first.method === "string" && "selector" in first) { return { action: { method: first.method } }; } - if (isObject(first.action) && typeof first.action.method === "string") { - return { action: { method: first.action.method } }; - } return undefined; } @@ -149,9 +156,7 @@ export function extractInstructionInput(args: unknown[]): unknown { if (typeof first === "string") { return { instruction: first }; } - return isObject(first) && typeof first.instruction === "string" - ? { instruction: first.instruction } - : undefined; + return undefined; } export function extractExtractInput(args: unknown[]): unknown { @@ -160,13 +165,7 @@ export function extractExtractInput(args: unknown[]): unknown { return undefined; } - const first = args[0]; - const options = isObject(first) ? first : isObject(args[1]) ? args[1] : {}; - const schemaCandidate = - typeof first === "string" && isZodSchema(args[1]) - ? args[1] - : options.schema; - const schema = extractJsonSchema(schemaCandidate); + const schema = extractJsonSchema(args[1]); return schema ? { ...input, schema } : input; } @@ -215,9 +214,7 @@ export function extractAgentInput(args: unknown[]): unknown { : typeof options.instruction === "string" ? options.instruction : undefined; - const outputSchema = extractJsonSchema( - options.output ?? options.outputSchema ?? options.schema, - ); + const outputSchema = extractJsonSchema(options.output); if (instruction === undefined && outputSchema === undefined) { return undefined; } @@ -246,15 +243,9 @@ export function extractAgentOutput(result: unknown): unknown { const taskCompleted = typeof action.taskCompleted === "boolean" ? action.taskCompleted - : typeof action.task_completed === "boolean" - ? action.task_completed - : undefined; + : undefined; const timeMs = - typeof action.timeMs === "number" - ? action.timeMs - : typeof action.time_ms === "number" - ? action.time_ms - : undefined; + typeof action.timeMs === "number" ? action.timeMs : undefined; return { ...(type !== undefined ? { type } : {}), ...(taskCompleted !== undefined @@ -371,10 +362,7 @@ function normalizeCacheStatus(value: unknown): "hit" | "miss" | undefined { } function hasExtractInstruction(args: readonly unknown[]): boolean { - return ( - typeof args[0] === "string" || - (isObject(args[0]) && typeof args[0].instruction === "string") - ); + return typeof args[0] === "string"; } function isZodSchema(value: unknown): boolean { diff --git a/js/src/vendor-sdk-types/stagehand.ts b/js/src/vendor-sdk-types/stagehand.ts index bd2ecaa15..1829faeba 100644 --- a/js/src/vendor-sdk-types/stagehand.ts +++ b/js/src/vendor-sdk-types/stagehand.ts @@ -5,12 +5,63 @@ * Braintrust SDK does not take a runtime or type dependency on Stagehand. */ +export type StagehandModel = + | string + | { + modelName?: unknown; + }; + +export type StagehandCallOptions = { + model?: StagehandModel; +}; + +export type StagehandAction = { + method?: unknown; + selector?: unknown; +}; + +export type StagehandActArguments = [ + input: string | StagehandAction, + options?: StagehandCallOptions, +]; + export type StagehandActResult = { success?: unknown; actions?: unknown; cacheStatus?: unknown; }; +export type StagehandExtractArguments = [ + instructionOrOptions?: string | StagehandCallOptions, + schemaOrOptions?: unknown, + options?: StagehandCallOptions, +]; + +export type StagehandObserveArguments = [ + instructionOrOptions?: string | StagehandCallOptions, + options?: StagehandCallOptions, +]; + +export type StagehandObserveResult = unknown[] & { + cacheStatus?: unknown; +}; + +export type StagehandAgentConfig = { + maxSteps?: unknown; + mode?: unknown; + model?: StagehandModel; + stream?: unknown; +}; + +export type StagehandAgentExecuteOptions = { + instruction?: unknown; + output?: unknown; +}; + +export type StagehandAgentExecuteArguments = [ + instructionOrOptions: string | StagehandAgentExecuteOptions, +]; + type StagehandAgentUsage = { input_tokens?: unknown; output_tokens?: unknown; @@ -28,15 +79,26 @@ export type StagehandAgentResult = { usage?: StagehandAgentUsage; }; +export type StagehandAgentStreamResult = { + result?: unknown; +}; + export type StagehandAgent = { - execute: (...args: unknown[]) => Promise; + execute: ( + ...args: StagehandAgentExecuteArguments + ) => Promise; }; export type StagehandInstance = { - act: (...args: unknown[]) => Promise; - extract: (...args: unknown[]) => Promise; - observe: (...args: unknown[]) => Promise; - agent: (...args: unknown[]) => StagehandAgent | unknown; + act: (...args: StagehandActArguments) => Promise; + extract: (...args: StagehandExtractArguments) => Promise; + observe: ( + ...args: StagehandObserveArguments + ) => Promise; + agent: (config?: StagehandAgentConfig) => StagehandAgent; + llmClient?: { + modelName?: unknown; + }; }; export type StagehandConstructor = new ( diff --git a/js/src/wrappers/stagehand.ts b/js/src/wrappers/stagehand.ts index b53e5ca33..99b5bc451 100644 --- a/js/src/wrappers/stagehand.ts +++ b/js/src/wrappers/stagehand.ts @@ -1,9 +1,13 @@ import { stagehandChannels } from "../instrumentation/plugins/stagehand-channels"; import { instrumentStagehandAgent } from "../instrumentation/plugins/stagehand-agent"; import type { + StagehandActArguments, + StagehandAgentConfig, StagehandConstructor, + StagehandExtractArguments, StagehandInstance, StagehandModule, + StagehandObserveArguments, } from "../vendor-sdk-types/stagehand"; const WRAPPED_CLASS = Symbol.for("braintrust.stagehand.wrapped-class"); @@ -107,7 +111,7 @@ function wrapStagehandInstance( return stagehand; } - const methodCache = new Map unknown>(); + const methodCache = new Map(); const proxy = new Proxy(stagehand, { get(target, prop) { if (prop === WRAPPED_INSTANCE) { @@ -123,21 +127,41 @@ function wrapStagehandInstance( return cached; } - let wrapped: (...args: unknown[]) => unknown; - if (prop === "act" || prop === "extract" || prop === "observe") { - const channel = stagehandChannels[prop]; - wrapped = (...args: unknown[]) => - channel.tracePromise( - () => Reflect.apply(value, target, args) as PromiseLike, - { - arguments: args, - self: target, - }, + let wrapped: unknown; + if (prop === "act") { + wrapped = (...args: StagehandActArguments) => + stagehandChannels.act.tracePromise( + () => + Reflect.apply(value, target, args) as ReturnType< + StagehandInstance["act"] + >, + { arguments: args, self: target }, + ); + } else if (prop === "extract") { + wrapped = (...args: StagehandExtractArguments) => + stagehandChannels.extract.tracePromise( + () => + Reflect.apply(value, target, args) as ReturnType< + StagehandInstance["extract"] + >, + { arguments: args, self: target }, + ); + } else if (prop === "observe") { + wrapped = (...args: StagehandObserveArguments) => + stagehandChannels.observe.tracePromise( + () => + Reflect.apply(value, target, args) as ReturnType< + StagehandInstance["observe"] + >, + { arguments: args, self: target }, ); } else if (prop === "agent") { - wrapped = (...args: unknown[]) => { + wrapped = (...args: [config?: StagehandAgentConfig]) => { const agent = stagehandChannels.agent.traceSync( - () => Reflect.apply(value, target, args), + () => + Reflect.apply(value, target, args) as ReturnType< + StagehandInstance["agent"] + >, { arguments: args, self: target }, ); return instrumentStagehandAgent(agent, args[0], target);