Skip to content

Commit 8f97da7

Browse files
authored
chore: that-should-be-it perform mode (microsoft#38503)
1 parent 360f72e commit 8f97da7

10 files changed

Lines changed: 41 additions & 32 deletions

File tree

package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"@eslint/compat": "^1.3.2",
5757
"@eslint/eslintrc": "^3.3.1",
5858
"@eslint/js": "^9.34.0",
59-
"@lowire/loop": "^0.0.6",
59+
"@lowire/loop": "^0.0.7",
6060
"@modelcontextprotocol/sdk": "^1.17.5",
6161
"@octokit/graphql-schema": "^15.26.0",
6262
"@stylistic/eslint-plugin": "^5.2.3",

packages/playwright-core/ThirdPartyNotices.txt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
44

55
This project incorporates components from the projects listed below. The original copyright notices and the licenses under which Microsoft received such components are set forth below. Microsoft reserves all rights not expressly granted herein, whether by implication, estoppel or otherwise.
66

7-
- @lowire/loop@0.0.6 (https://github.com/pavelfeldman/lowire)
7+
- @lowire/loop@0.0.7 (https://github.com/pavelfeldman/lowire)
88
- @modelcontextprotocol/sdk@1.24.2 (https://github.com/modelcontextprotocol/typescript-sdk)
99
- accepts@2.0.0 (https://github.com/jshttp/accepts)
1010
- agent-base@7.1.4 (https://github.com/TooTallNate/proxy-agents)
@@ -135,7 +135,7 @@ This project incorporates components from the projects listed below. The origina
135135
- zod-to-json-schema@3.25.0 (https://github.com/StefanTerdell/zod-to-json-schema)
136136
- zod@3.25.76 (https://github.com/colinhacks/zod)
137137

138-
%% @lowire/loop@0.0.6 NOTICES AND INFORMATION BEGIN HERE
138+
%% @lowire/loop@0.0.7 NOTICES AND INFORMATION BEGIN HERE
139139
=========================================
140140
Apache License
141141
Version 2.0, January 2004
@@ -339,7 +339,7 @@ Apache License
339339
See the License for the specific language governing permissions and
340340
limitations under the License.
341341
=========================================
342-
END OF @lowire/loop@0.0.6 AND INFORMATION
342+
END OF @lowire/loop@0.0.7 AND INFORMATION
343343

344344
%% @modelcontextprotocol/sdk@1.24.2 NOTICES AND INFORMATION BEGIN HERE
345345
=========================================

packages/playwright-core/bundles/mcp/package-lock.json

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/playwright-core/bundles/mcp/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"private": true,
55
"dependencies": {
66
"@modelcontextprotocol/sdk": "^1.24.0",
7-
"@lowire/loop": "^0.0.6",
7+
"@lowire/loop": "^0.0.7",
88
"zod": "^3.25.76",
99
"zod-to-json-schema": "^3.24.6"
1010
}

packages/playwright-core/src/server/agent/agent.ts

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import fs from 'fs';
1818

1919
import { toolsForLoop } from './backend';
2020
import { debug } from '../../utilsBundle';
21-
import { Loop, z, zodToJsonSchema } from '../../mcpBundle';
21+
import { Loop } from '../../mcpBundle';
2222
import { runAction } from './actionRunner';
2323
import { Context } from './context';
2424

@@ -34,9 +34,7 @@ export async function pagePerform(progress: Progress, page: Page, options: chann
3434
if (await cachedPerform(context, options))
3535
return;
3636

37-
await perform(context, options.task, zodToJsonSchema(z.object({
38-
error: z.string().optional().describe('An error message if the task could not be completed successfully'),
39-
})) as loopTypes.Schema, options);
37+
await perform(context, options.task, undefined, options);
4038
await updateCache(context, options);
4139
}
4240

@@ -51,7 +49,7 @@ ${options.query}`;
5149
return await perform(context, task, options.schema, options);
5250
}
5351

54-
async function perform(context: Context, userTask: string, resultSchema: loopTypes.Schema, options: { maxTurns?: number } = {}): Promise<any> {
52+
async function perform(context: Context, userTask: string, resultSchema: loopTypes.Schema | undefined, options: { maxTurns?: number } = {}): Promise<any> {
5553
const { progress, page } = context;
5654
const browserContext = page.browserContext;
5755
if (!browserContext._options.agent)
@@ -66,6 +64,13 @@ async function perform(context: Context, userTask: string, resultSchema: loopTyp
6664
debug,
6765
callTool,
6866
tools,
67+
beforeTurn: params => {
68+
const lastReply = params.conversation.messages.findLast(m => m.role === 'assistant');
69+
const toolCall = lastReply?.content.find(c => c.type === 'tool_call');
70+
if (!resultSchema && toolCall && toolCall.arguments.thatShouldBeIt)
71+
return 'break';
72+
return 'continue';
73+
},
6974
...options
7075
});
7176

@@ -75,9 +80,8 @@ async function perform(context: Context, userTask: string, resultSchema: loopTyp
7580
${full}
7681
`;
7782

78-
return await loop.run(task, {
79-
resultSchema
80-
});
83+
const { result } = await loop.run(task, { resultSchema });
84+
return result;
8185
}
8286

8387
type CachedActions = Record<string, {
@@ -121,8 +125,7 @@ async function updateCache(context: Context, options: channels.PagePerformParams
121125
async function cachedActions(cacheFile: string): Promise<CachedActions> {
122126
let cache = allCaches.get(cacheFile);
123127
if (!cache) {
124-
const text = await fs.promises.readFile(cacheFile, 'utf-8').catch(() => '{}');
125-
cache = JSON.parse(text) as CachedActions;
128+
cache = await fs.promises.readFile(cacheFile, 'utf-8').then(text => JSON.parse(text)).catch(() => ({})) as CachedActions;
126129
allCaches.set(cacheFile, cache);
127130
}
128131
return cache;

packages/playwright-core/src/server/agent/tools.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,24 @@ function defineTool<Input extends zod.Schema>(tool: ToolDefinition<Input>): Tool
3535
return tool;
3636
}
3737

38+
const baseSchema = z.object({
39+
thatShouldBeIt: z.boolean().describe('Indicates that this tool call is sufficient to complete the task. If false, the task will continue with the next tool call'),
40+
});
41+
3842
const snapshot = defineTool({
3943
schema: {
4044
name: 'browser_snapshot',
4145
title: 'Page snapshot',
4246
description: 'Capture accessibility snapshot of the current page, this is better than screenshot',
43-
inputSchema: z.object({}),
47+
inputSchema: baseSchema,
4448
},
4549

4650
handle: async (context, params) => {
4751
return await context.snapshotResult();
4852
},
4953
});
5054

51-
const elementSchema = z.object({
55+
const elementSchema = baseSchema.extend({
5256
element: z.string().describe('Human-readable element description used to obtain permission to interact with the element'),
5357
ref: z.string().describe('Exact target element reference from the page snapshot'),
5458
});
@@ -86,7 +90,7 @@ const drag = defineTool({
8690
name: 'browser_drag',
8791
title: 'Drag mouse',
8892
description: 'Perform drag and drop between two elements',
89-
inputSchema: z.object({
93+
inputSchema: baseSchema.extend({
9094
startElement: z.string().describe('Human-readable source element description used to obtain the permission to interact with the element'),
9195
startRef: z.string().describe('Exact source element reference from the page snapshot'),
9296
endElement: z.string().describe('Human-readable target element description used to obtain the permission to interact with the element'),
@@ -211,7 +215,7 @@ const fillForm = defineTool({
211215
name: 'browser_fill_form',
212216
title: 'Fill form',
213217
description: 'Fill multiple form fields',
214-
inputSchema: z.object({
218+
inputSchema: baseSchema.extend({
215219
fields: z.array(z.object({
216220
name: z.string().describe('Human-readable field name'),
217221
type: z.enum(['textbox', 'checkbox', 'radio', 'combobox', 'slider']).describe('Type of the field'),

packages/playwright/src/agents/agent.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,13 @@ export class Agent<T extends z.ZodSchema<any>> {
5050
const { clients, tools, callTool } = await this._initClients();
5151
const prompt = this.spec.description;
5252
try {
53-
return await this.loop.run<z.output<T>>(`${prompt}\n\nTask:\n${task}\n\nParams:\n${JSON.stringify(params, null, 2)}`, {
53+
const { result } = await this.loop.run<z.output<T>>(`${prompt}\n\nTask:\n${task}\n\nParams:\n${JSON.stringify(params, null, 2)}`, {
5454
...options,
5555
tools,
5656
callTool,
5757
resultSchema: this.resultSchema
5858
});
59+
return result;
5960
} finally {
6061
await this._disconnectFromServers(clients);
6162
}

tests/library/perform-task.spec.ts-cache.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"Enter x-secret-email into the email field": {
3-
"timestamp": 1765318973255,
3+
"timestamp": 1765340032886,
44
"actions": [
55
{
66
"method": "fill",
@@ -10,7 +10,7 @@
1010
]
1111
},
1212
"Fill out the form with the following details:\nName: John Smith\nAddress: 1045 La Avenida St, Mountain View, CA 94043\nEmail: john.smith@at-microsoft.com": {
13-
"timestamp": 1765318976120,
13+
"timestamp": 1765340035431,
1414
"actions": [
1515
{
1616
"method": "fill",

tests/mcp/browser.eval.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import type * as lowireLoop from '@lowire/loop';
2424
test.use({ mcpHeadless: async ({ headless }, use) => use(headless) });
2525

2626
test('fill the form', async ({ loop, client, server }) => {
27-
expect(await loop.run<{ price: number, deliveryDays: number }>(`
27+
const { result } = await loop.run<{ price: number, deliveryDays: number }>(`
2828
Navigate to ${server.PREFIX + '/evals/fill-form.html'} via Playwright MCP.
2929
Order a blue table and a green desk.
3030
Use the following details for the order form:
@@ -41,5 +41,6 @@ test('fill the form', async ({ loop, client, server }) => {
4141
price: z.number().describe('Total price in USD.'),
4242
deliveryDays: z.number().describe('Maximum estimated delivery time in days.'),
4343
})) as lowireLoop.Schema,
44-
})).toEqual({ price: 653.96, deliveryDays: 3 });
44+
});
45+
expect(result).toEqual({ price: 653.96, deliveryDays: 3 });
4546
});

0 commit comments

Comments
 (0)