Skip to content

Commit db2a052

Browse files
authored
chore: let agent refuse to perform (#38803)
1 parent 0d2dec0 commit db2a052

22 files changed

+226
-1022
lines changed

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
@@ -55,7 +55,7 @@
5555
"@eslint/compat": "^1.3.2",
5656
"@eslint/eslintrc": "^3.3.1",
5757
"@eslint/js": "^9.34.0",
58-
"@lowire/loop": "^0.0.23",
58+
"@lowire/loop": "^0.0.24",
5959
"@modelcontextprotocol/sdk": "^1.25.2",
6060
"@octokit/graphql-schema": "^15.26.0",
6161
"@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
@@ -5,7 +5,7 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION
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

77
- @hono/node-server@1.19.8 (https://github.com/honojs/node-server)
8-
- @lowire/loop@0.0.23 (https://github.com/pavelfeldman/lowire)
8+
- @lowire/loop@0.0.24 (https://github.com/pavelfeldman/lowire)
99
- @modelcontextprotocol/sdk@1.25.2 (https://github.com/modelcontextprotocol/typescript-sdk)
1010
- accepts@2.0.0 (https://github.com/jshttp/accepts)
1111
- agent-base@7.1.4 (https://github.com/TooTallNate/proxy-agents)
@@ -500,7 +500,7 @@ MIT
500500
=========================================
501501
END OF @hono/node-server@1.19.8 AND INFORMATION
502502

503-
%% @lowire/loop@0.0.23 NOTICES AND INFORMATION BEGIN HERE
503+
%% @lowire/loop@0.0.24 NOTICES AND INFORMATION BEGIN HERE
504504
=========================================
505505
Apache License
506506
Version 2.0, January 2004
@@ -704,7 +704,7 @@ Apache License
704704
See the License for the specific language governing permissions and
705705
limitations under the License.
706706
=========================================
707-
END OF @lowire/loop@0.0.23 AND INFORMATION
707+
END OF @lowire/loop@0.0.24 AND INFORMATION
708708

709709
%% @modelcontextprotocol/sdk@1.25.2 NOTICES AND INFORMATION BEGIN HERE
710710
=========================================

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
@@ -3,7 +3,7 @@
33
"version": "0.0.1",
44
"private": true,
55
"dependencies": {
6-
"@lowire/loop": "^0.0.23",
6+
"@lowire/loop": "^0.0.24",
77
"@modelcontextprotocol/sdk": "^1.25.2",
88
"zod": "^4.3.5",
99
"zod-to-json-schema": "^3.25.1"

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ async function runLoop(progress: Progress, context: Context, toolDefinitions: To
9595
throw new Error(`This action requires API key to be set on the page agent.`);
9696

9797
const { full } = await page.snapshotForAI(progress);
98-
const { tools, callTool, reportedResult } = toolsForLoop(progress, context, toolDefinitions, { resultSchema });
98+
const { tools, callTool, reportedResult, refusedToPerformReason } = toolsForLoop(progress, context, toolDefinitions, { resultSchema, refuseToPerform: 'allow' });
9999
const secrets = Object.fromEntries((context.agentParams.secrets || [])?.map(s => ([s.name, s.value])));
100100

101101
const apiCacheTextBefore = context.agentParams.apiCacheFile ?
@@ -151,10 +151,13 @@ async function runLoop(progress: Progress, context: Context, toolDefinitions: To
151151
}
152152
}
153153

154+
if (refusedToPerformReason())
155+
throw new Error(`Agent refused to perform action: ${refusedToPerformReason()}`);
156+
154157
if (error)
155158
throw new Error(`Agentic loop failed: ${error}`);
156159

157-
return { result: resultSchema ? reportedResult() : undefined };
160+
return { result: reportedResult ? reportedResult() : undefined };
158161
}
159162

160163
async function cachedPerform(progress: Progress, context: Context, cacheKey: string): Promise<actions.ActionWithCode[] | undefined> {
@@ -202,8 +205,8 @@ const allCaches = new Map<string, Cache>();
202205
async function cachedActions(cacheFile: string): Promise<Cache> {
203206
let cache = allCaches.get(cacheFile);
204207
if (!cache) {
205-
const text = await fs.promises.readFile(cacheFile, 'utf-8').catch(() => '{}');
206-
const parsed = actions.cachedActionsSchema.safeParse(JSON.parse(text));
208+
const json = await fs.promises.readFile(cacheFile, 'utf-8').then(text => JSON.parse(text)).catch(() => ({}));
209+
const parsed = actions.cachedActionsSchema.safeParse(json);
207210
if (parsed.error)
208211
throw new Error(`Failed to parse cache file ${cacheFile}:\n${zod.prettifyError(parsed.error)}`);
209212
cache = { actions: parsed.data, newActions: {} };

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,14 @@ export function defineTool<Input extends zod.Schema>(tool: ToolDefinition<Input>
3434
return tool;
3535
}
3636

37-
export function toolsForLoop(progress: Progress, context: Context, toolDefinitions: ToolDefinition[], options: { resultSchema?: loopTypes.Schema } = {}): { tools: loopTypes.Tool[], callTool: loopTypes.ToolCallback, reportedResult: () => any } {
37+
type ToolsForLoop = {
38+
tools: loopTypes.Tool[];
39+
callTool: loopTypes.ToolCallback;
40+
reportedResult?: () => any;
41+
refusedToPerformReason: () => string | undefined;
42+
};
43+
44+
export function toolsForLoop(progress: Progress, context: Context, toolDefinitions: ToolDefinition[], options: { resultSchema?: loopTypes.Schema, refuseToPerform?: 'allow' | 'deny' } = {}): ToolsForLoop {
3845
const tools = toolDefinitions.map(tool => {
3946
const result: loopTypes.Tool = {
4047
name: tool.schema.name,
@@ -43,6 +50,7 @@ export function toolsForLoop(progress: Progress, context: Context, toolDefinitio
4350
};
4451
return result;
4552
});
53+
4654
if (options.resultSchema) {
4755
tools.push({
4856
name: 'report_result',
@@ -51,7 +59,25 @@ export function toolsForLoop(progress: Progress, context: Context, toolDefinitio
5159
});
5260
}
5361

62+
if (options.refuseToPerform === 'allow') {
63+
tools.push({
64+
name: 'refuse_to_perform',
65+
description: 'Refuse to perform action.',
66+
inputSchema: {
67+
type: 'object',
68+
properties: {
69+
reason: {
70+
type: 'string',
71+
description: `Call this when you believe that you can't perform the action because something is wrong with the page. The reason will be reported to the user.`,
72+
},
73+
},
74+
required: ['reason'],
75+
},
76+
});
77+
}
78+
5479
let reportedResult: any;
80+
let refusedToPerformReason: string | undefined;
5581

5682
const callTool: loopTypes.ToolCallback = async params => {
5783
if (params.name === 'report_result') {
@@ -62,6 +88,14 @@ export function toolsForLoop(progress: Progress, context: Context, toolDefinitio
6288
};
6389
}
6490

91+
if (params.name === 'refuse_to_perform') {
92+
refusedToPerformReason = params.arguments.reason;
93+
return {
94+
content: [{ type: 'text', text: 'Done' }],
95+
isError: false,
96+
};
97+
}
98+
6599
const tool = toolDefinitions.find(t => t.schema.name === params.name);
66100
if (!tool) {
67101
return {
@@ -85,6 +119,7 @@ export function toolsForLoop(progress: Progress, context: Context, toolDefinitio
85119
return {
86120
tools,
87121
callTool,
88-
reportedResult: () => reportedResult,
122+
reportedResult: options.resultSchema ? () => reportedResult : undefined,
123+
refusedToPerformReason: () => refusedToPerformReason,
89124
};
90125
}

0 commit comments

Comments
 (0)