@@ -29,6 +29,39 @@ const SHOP_HTML = `<!doctype html>
2929 </body>
3030</html>` ;
3131
32+ const STABLE_ACCESSIBILITY_TREE = `[0-2] RootWebArea: Trail Supply
33+ [0-4] scrollable, html
34+ [0-9] main
35+ [0-10] heading: Trail Backpack
36+ [0-12] paragraph
37+ [0-13] StaticText: $129.00
38+ [0-14] button: Add to cart
39+ [0-16] region: Shopping cart
40+ [0-17] paragraph
41+ [0-18] StaticText: Cart is empty
42+ [0-19] button: Checkout` ;
43+
44+ export function stabilizeStagehandActMessages ( messages ) {
45+ let liveElementId ;
46+ const normalizedMessages = messages . map ( ( message ) => {
47+ if ( ! message || typeof message . content !== "string" ) {
48+ return message ;
49+ }
50+ liveElementId ??= message . content . match (
51+ / ^ \s * \[ ( \d + - \d + ) \] \s + b u t t o n : A d d t o c a r t \s * $ / m,
52+ ) ?. [ 1 ] ;
53+ return {
54+ ...message ,
55+ content : message . content . replace (
56+ / A c c e s s i b i l i t y T r e e : \s * [ \s \S ] * $ / ,
57+ `Accessibility Tree: \n${ STABLE_ACCESSIBILITY_TREE } \n` ,
58+ ) ,
59+ } ;
60+ } ) ;
61+
62+ return { liveElementId, messages : normalizedMessages } ;
63+ }
64+
3265async function startShopServer ( ) {
3366 const server = createServer ( ( _request , response ) => {
3467 response . writeHead ( 200 , { "content-type" : "text/html; charset=utf-8" } ) ;
@@ -85,15 +118,39 @@ async function createStagehandSession(Stagehand, instrumentedAI) {
85118 "Expected Stagehand act to request a structured response" ,
86119 ) ;
87120 }
121+ // Chromium's accessibility node IDs vary across browser versions. Keep the
122+ // deterministic fixture prompt stable for cassette replay, then translate
123+ // the model's fixture ID back to the live page before Stagehand acts on it.
124+ const { liveElementId, messages } = stabilizeStagehandActMessages (
125+ options . messages ,
126+ ) ;
127+ if ( ! liveElementId ) {
128+ throw new Error ( "Expected Stagehand to observe the add-to-cart button" ) ;
129+ }
88130 const response = await instrumentedAI . generateObject ( {
89131 allowSystemInMessages : true ,
90- messages : options . messages ,
132+ messages,
91133 model : languageModel ,
92134 schema : options . response_model . schema ,
93135 temperature : options . temperature ,
94136 } ) ;
137+ let data = response . object ;
138+ if ( data && typeof data === "object" ) {
139+ if ( "elementId" in data ) {
140+ data = { ...data , elementId : liveElementId } ;
141+ } else if (
142+ "action" in data &&
143+ data . action &&
144+ typeof data . action === "object"
145+ ) {
146+ data = {
147+ ...data ,
148+ action : { ...data . action , elementId : liveElementId } ,
149+ } ;
150+ }
151+ }
95152 return {
96- data : response . object ,
153+ data,
97154 usage : {
98155 cached_input_tokens : response . usage . cachedInputTokens ?? 0 ,
99156 completion_tokens : response . usage . outputTokens ?? 0 ,
0 commit comments