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/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..b231a417a --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/scenario.impl.mjs @@ -0,0 +1,373 @@ +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

+ +
+
+ +`; + +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" }); + 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", + ); + } + // 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, + 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, + 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..337b5120d --- /dev/null +++ b/e2e/scenarios/stagehand-instrumentation/scenario.test.ts @@ -0,0 +1,101 @@ +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({ + 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, + ), + })), +); + +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}`, () => { + 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, + }, + 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, + }, + 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..d157b70d1 --- /dev/null +++ b/js/src/instrumentation/plugins/stagehand-agent.ts @@ -0,0 +1,70 @@ +import { debugLogger } from "../../debug-logger"; +import { isObject } from "../../util"; +import { stagehandChannels } from "./stagehand-channels"; +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: 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 = agent.execute; + descriptor = Object.getOwnPropertyDescriptor(agent, "execute"); + } catch (error) { + debugLogger.error( + "Error inspecting Stagehand agent execute method:", + error, + ); + return value; + } + if (typeof execute !== "function") { + return value; + } + + const wrappedExecute = function ( + ...args: StagehandAgentExecuteArguments + ): ReturnType { + return stagehandChannels.agentExecute.tracePromise( + () => Reflect.apply(execute, agent, args), + { + agentConfig, + arguments: args, + self: agent, + stagehand, + }, + ); + }; + + try { + Object.defineProperty(agent, "execute", { + configurable: descriptor?.configurable ?? true, + enumerable: descriptor?.enumerable ?? true, + value: wrappedExecute, + writable: descriptor?.writable ?? true, + }); + instrumentedAgents.add(agent); + } 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..db4211ab7 --- /dev/null +++ b/js/src/instrumentation/plugins/stagehand-channels.ts @@ -0,0 +1,72 @@ +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?: StagehandInstance; +}; + +type StagehandAgentExecuteContext = { + agentConfig?: StagehandAgentConfig; + self?: StagehandAgent; + stagehand?: StagehandInstance; +}; + +export const stagehandChannels = defineChannels( + "@browserbasehq/stagehand", + { + act: channel< + StagehandActArguments, + StagehandActResult, + StagehandChannelContext + >({ + channelName: "Stagehand.act", + kind: "async", + }), + extract: channel< + StagehandExtractArguments, + unknown, + StagehandChannelContext + >({ + channelName: "Stagehand.extract", + kind: "async", + }), + observe: channel< + StagehandObserveArguments, + StagehandObserveResult, + StagehandChannelContext + >({ + channelName: "Stagehand.observe", + kind: "async", + }), + agent: channel< + [config?: StagehandAgentConfig], + StagehandAgent, + StagehandChannelContext + >({ + channelName: "Stagehand.agent", + kind: "sync-stream", + }), + agentExecute: channel< + StagehandAgentExecuteArguments, + StagehandAgentResult | StagehandAgentStreamResult, + StagehandAgentExecuteContext + >({ + 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..5b6d7fa67 --- /dev/null +++ b/js/src/instrumentation/plugins/stagehand-plugin-lifecycle.test.ts @@ -0,0 +1,216 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +const { mockStartSpan, tracingChannels } = vi.hoisted(() => ({ + mockStartSpan: vi.fn(), + tracingChannels: new Map(), +})); + +vi.mock("../../isomorph", () => ({ + default: { + getEnv: vi.fn(() => undefined), + 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("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(); + 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); + }); + + 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 new file mode 100644 index 000000000..5836b2806 --- /dev/null +++ b/js/src/instrumentation/plugins/stagehand-plugin.test.ts @@ -0,0 +1,149 @@ +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([ + "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..7d2ad040a --- /dev/null +++ b/js/src/instrumentation/plugins/stagehand-plugin.ts @@ -0,0 +1,386 @@ +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 }) => { + 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(finalResult) + .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.method === "string" && "selector" in first) { + return { action: { method: first.method } }; + } + return undefined; +} + +export function extractInstructionInput(args: unknown[]): unknown { + const first = args[0]; + if (typeof first === "string") { + return { instruction: first }; + } + return undefined; +} + +export function extractExtractInput(args: unknown[]): unknown { + const input = extractInstructionInput(args); + if (!input) { + return undefined; + } + + const schema = extractJsonSchema(args[1]); + 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); + 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 + : undefined; + const timeMs = + typeof action.timeMs === "number" ? action.timeMs : 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"; +} + +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..1829faeba --- /dev/null +++ b/js/src/vendor-sdk-types/stagehand.ts @@ -0,0 +1,111 @@ +/** + * 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 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; + reasoning_tokens?: unknown; + cached_input_tokens?: unknown; +}; + +export type StagehandAgentResult = { + success?: unknown; + completed?: unknown; + message?: unknown; + output?: unknown; + actions?: unknown; + metadata?: unknown; + usage?: StagehandAgentUsage; +}; + +export type StagehandAgentStreamResult = { + result?: unknown; +}; + +export type StagehandAgent = { + execute: ( + ...args: StagehandAgentExecuteArguments + ) => Promise; +}; + +export type StagehandInstance = { + act: (...args: StagehandActArguments) => Promise; + extract: (...args: StagehandExtractArguments) => Promise; + observe: ( + ...args: StagehandObserveArguments + ) => Promise; + agent: (config?: StagehandAgentConfig) => StagehandAgent; + llmClient?: { + modelName?: 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..99b5bc451 --- /dev/null +++ b/js/src/wrappers/stagehand.ts @@ -0,0 +1,178 @@ +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"); +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(); + 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: 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: [config?: StagehandAgentConfig]) => { + const agent = stagehandChannels.agent.traceSync( + () => + Reflect.apply(value, target, args) as ReturnType< + StagehandInstance["agent"] + >, + { arguments: args, self: target }, + ); + return instrumentStagehandAgent(agent, args[0], target); + }; + } else { + wrapped = value.bind(target); + } + methodCache.set(prop, wrapped); + return wrapped; + }, + }); + + return proxy; +}