Skip to content

Commit d2b4848

Browse files
author
Tomohiko Hiraki
committed
Merge PR #1: Response Filtering Foundation
Implements the foundation for response filtering with expectation parameters. This enables selective response content to reduce token consumption.
2 parents 457bb73 + e2f8fdb commit d2b4848

File tree

4 files changed

+658
-12
lines changed

4 files changed

+658
-12
lines changed

src/response.ts

Lines changed: 94 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
*/
1616

1717
import { renderModalStates } from './tab.js';
18+
import { mergeExpectations } from './schemas/expectation.js';
1819

1920
import type { Tab, TabSnapshot } from './tab.js';
2021
import type { ImageContent, TextContent } from '@modelcontextprotocol/sdk/types.js';
2122
import type { Context } from './context.js';
23+
import type { ExpectationOptions } from './schemas/expectation.js';
2224

2325
export class Response {
2426
private _result: string[] = [];
@@ -28,15 +30,17 @@ export class Response {
2830
private _includeSnapshot = false;
2931
private _includeTabs = false;
3032
private _tabSnapshot: TabSnapshot | undefined;
33+
private _expectation: NonNullable<ExpectationOptions>;
3134

3235
readonly toolName: string;
3336
readonly toolArgs: Record<string, any>;
3437
private _isError: boolean | undefined;
3538

36-
constructor(context: Context, toolName: string, toolArgs: Record<string, any>) {
39+
constructor(context: Context, toolName: string, toolArgs: Record<string, any>, expectation?: ExpectationOptions) {
3740
this._context = context;
3841
this.toolName = toolName;
3942
this.toolArgs = toolArgs;
43+
this._expectation = mergeExpectations(toolName, expectation);
4044
}
4145

4246
addResult(result: string) {
@@ -83,8 +87,16 @@ export class Response {
8387
async finish() {
8488
// All the async snapshotting post-action is happening here.
8589
// Everything below should race against modal states.
86-
if (this._includeSnapshot && this._context.currentTab())
87-
this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
90+
if ((this._includeSnapshot || this._expectation.includeSnapshot) && this._context.currentTab()) {
91+
const options = this._expectation.snapshotOptions;
92+
if (options?.selector) {
93+
// TODO: Implement partial snapshot capture based on selector
94+
// For now, capture full snapshot
95+
this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
96+
} else {
97+
this._tabSnapshot = await this._context.currentTabOrDie().captureSnapshot();
98+
}
99+
}
88100
for (const tab of this._context.tabs())
89101
await tab.updateTitle();
90102
}
@@ -103,25 +115,28 @@ export class Response {
103115
response.push('');
104116
}
105117

106-
// Add code if it exists.
107-
if (this._code.length) {
118+
// Add code if it exists and expectation allows it.
119+
if (this._code.length && this._expectation.includeCode) {
108120
response.push(`### Ran Playwright code
109121
\`\`\`js
110122
${this._code.join('\n')}
111123
\`\`\``);
112124
response.push('');
113125
}
114126

115-
// List browser tabs.
116-
if (this._includeSnapshot || this._includeTabs)
117-
response.push(...renderTabsMarkdown(this._context.tabs(), this._includeTabs));
127+
// List browser tabs based on expectation.
128+
const shouldIncludeTabs = this._expectation.includeTabs || this._includeTabs;
129+
const shouldIncludeSnapshot = this._expectation.includeSnapshot || this._includeSnapshot;
130+
131+
if (shouldIncludeSnapshot || shouldIncludeTabs)
132+
response.push(...renderTabsMarkdown(this._context.tabs(), shouldIncludeTabs));
118133

119-
// Add snapshot if provided.
120-
if (this._tabSnapshot?.modalStates.length) {
134+
// Add snapshot if provided and expectation allows it.
135+
if (shouldIncludeSnapshot && this._tabSnapshot?.modalStates.length) {
121136
response.push(...renderModalStates(this._context, this._tabSnapshot.modalStates));
122137
response.push('');
123-
} else if (this._tabSnapshot) {
124-
response.push(renderTabSnapshot(this._tabSnapshot));
138+
} else if (shouldIncludeSnapshot && this._tabSnapshot) {
139+
response.push(this.renderFilteredTabSnapshot(this._tabSnapshot));
125140
response.push('');
126141
}
127142

@@ -138,6 +153,73 @@ ${this._code.join('\n')}
138153

139154
return { content, isError: this._isError };
140155
}
156+
157+
private renderFilteredTabSnapshot(tabSnapshot: TabSnapshot): string {
158+
const lines: string[] = [];
159+
const consoleOptions = this._expectation.consoleOptions;
160+
161+
// Include console messages based on expectation
162+
if (this._expectation.includeConsole && tabSnapshot.consoleMessages.length) {
163+
const filteredMessages = this.filterConsoleMessages(tabSnapshot.consoleMessages, consoleOptions);
164+
if (filteredMessages.length) {
165+
lines.push(`### New console messages`);
166+
for (const message of filteredMessages)
167+
lines.push(`- ${trim(message.toString(), 100)}`);
168+
lines.push('');
169+
}
170+
}
171+
172+
// Include downloads based on expectation
173+
if (this._expectation.includeDownloads && tabSnapshot.downloads.length) {
174+
lines.push(`### Downloads`);
175+
for (const entry of tabSnapshot.downloads) {
176+
if (entry.finished)
177+
lines.push(`- Downloaded file ${entry.download.suggestedFilename()} to ${entry.outputFile}`);
178+
else
179+
lines.push(`- Downloading file ${entry.download.suggestedFilename()} ...`);
180+
}
181+
lines.push('');
182+
}
183+
184+
lines.push(`### Page state`);
185+
lines.push(`- Page URL: ${tabSnapshot.url}`);
186+
lines.push(`- Page Title: ${tabSnapshot.title}`);
187+
lines.push(`- Page Snapshot:`);
188+
lines.push('```yaml');
189+
190+
// Apply snapshot format and length restrictions
191+
let snapshot = tabSnapshot.ariaSnapshot;
192+
const snapshotOptions = this._expectation.snapshotOptions;
193+
194+
if (snapshotOptions?.maxLength && snapshot.length > snapshotOptions.maxLength) {
195+
snapshot = snapshot.slice(0, snapshotOptions.maxLength) + '...';
196+
}
197+
198+
lines.push(snapshot);
199+
lines.push('```');
200+
201+
return lines.join('\n');
202+
}
203+
204+
private filterConsoleMessages(messages: any[], options?: NonNullable<ExpectationOptions>['consoleOptions']): any[] {
205+
let filtered = messages;
206+
207+
// Filter by levels if specified
208+
if (options?.levels && options.levels.length > 0) {
209+
filtered = filtered.filter(msg => {
210+
const level = msg.type || 'log';
211+
return options.levels!.includes(level);
212+
});
213+
}
214+
215+
// Limit number of messages
216+
const maxMessages = options?.maxMessages ?? 10;
217+
if (filtered.length > maxMessages) {
218+
filtered = filtered.slice(0, maxMessages);
219+
}
220+
221+
return filtered;
222+
}
141223
}
142224

143225
function renderTabSnapshot(tabSnapshot: TabSnapshot): string {

src/schemas/expectation.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Copyright (c) Microsoft Corporation.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { z } from 'zod';
18+
19+
/**
20+
* Schema for expectation configuration that controls response content
21+
*/
22+
export const expectationSchema = z.object({
23+
includeSnapshot: z.boolean().optional().default(true),
24+
includeConsole: z.boolean().optional().default(true),
25+
includeDownloads: z.boolean().optional().default(true),
26+
includeTabs: z.boolean().optional().default(true),
27+
includeCode: z.boolean().optional().default(true),
28+
snapshotOptions: z.object({
29+
selector: z.string().optional().describe('CSS selector to limit snapshot scope'),
30+
maxLength: z.number().optional().describe('Maximum characters for snapshot'),
31+
format: z.enum(['aria', 'text', 'html']).optional().default('aria')
32+
}).optional(),
33+
consoleOptions: z.object({
34+
levels: z.array(z.enum(['log', 'warn', 'error', 'info'])).optional(),
35+
maxMessages: z.number().optional().default(10)
36+
}).optional(),
37+
imageOptions: z.object({
38+
quality: z.number().min(1).max(100).optional().describe('JPEG quality (1-100)'),
39+
maxWidth: z.number().optional().describe('Maximum width in pixels'),
40+
maxHeight: z.number().optional().describe('Maximum height in pixels'),
41+
format: z.enum(['jpeg', 'png', 'webp']).optional()
42+
}).optional()
43+
}).optional();
44+
45+
export type ExpectationOptions = z.infer<typeof expectationSchema>;
46+
47+
/**
48+
* Tool-specific default expectation configurations
49+
* These optimize token usage based on typical tool usage patterns
50+
*/
51+
const TOOL_DEFAULTS: Record<string, Required<Omit<NonNullable<ExpectationOptions>, 'snapshotOptions' | 'consoleOptions' | 'imageOptions'>>> = {
52+
// Navigation tools need full context for verification
53+
navigate: {
54+
includeSnapshot: true,
55+
includeConsole: true,
56+
includeDownloads: true,
57+
includeTabs: true,
58+
includeCode: true
59+
},
60+
61+
// Interactive tools need snapshot for feedback but less verbose logging
62+
click: {
63+
includeSnapshot: true,
64+
includeConsole: false,
65+
includeDownloads: false,
66+
includeTabs: false,
67+
includeCode: true
68+
},
69+
70+
fill: {
71+
includeSnapshot: true,
72+
includeConsole: false,
73+
includeDownloads: false,
74+
includeTabs: false,
75+
includeCode: true
76+
},
77+
78+
// Screenshot tools don't need additional context
79+
screenshot: {
80+
includeSnapshot: false,
81+
includeConsole: false,
82+
includeDownloads: false,
83+
includeTabs: false,
84+
includeCode: false
85+
},
86+
87+
// Code evaluation needs console output but minimal other info
88+
evaluate: {
89+
includeSnapshot: false,
90+
includeConsole: true,
91+
includeDownloads: false,
92+
includeTabs: false,
93+
includeCode: true
94+
},
95+
96+
// Wait operations typically don't need verbose output
97+
wait: {
98+
includeSnapshot: false,
99+
includeConsole: false,
100+
includeDownloads: false,
101+
includeTabs: false,
102+
includeCode: true
103+
}
104+
};
105+
106+
/**
107+
* General default configuration for tools without specific settings
108+
*/
109+
const GENERAL_DEFAULT: Required<Omit<NonNullable<ExpectationOptions>, 'snapshotOptions' | 'consoleOptions' | 'imageOptions'>> = {
110+
includeSnapshot: true,
111+
includeConsole: true,
112+
includeDownloads: true,
113+
includeTabs: true,
114+
includeCode: true
115+
};
116+
117+
/**
118+
* Get default expectation configuration for a specific tool
119+
* @param toolName - Name of the tool (e.g., 'click', 'navigate', 'screenshot')
120+
* @returns Default expectation configuration optimized for the tool
121+
*/
122+
export function getDefaultExpectation(toolName: string): Required<Omit<NonNullable<ExpectationOptions>, 'snapshotOptions' | 'consoleOptions' | 'imageOptions'>> {
123+
return TOOL_DEFAULTS[toolName] || GENERAL_DEFAULT;
124+
}
125+
126+
/**
127+
* Merge user-provided expectation with tool-specific defaults
128+
* @param toolName - Name of the tool
129+
* @param userExpectation - User-provided expectation options
130+
* @returns Merged expectation configuration
131+
*/
132+
export function mergeExpectations(
133+
toolName: string,
134+
userExpectation?: ExpectationOptions
135+
): NonNullable<ExpectationOptions> {
136+
const defaults = getDefaultExpectation(toolName);
137+
138+
if (!userExpectation) {
139+
return defaults;
140+
}
141+
142+
return {
143+
includeSnapshot: userExpectation.includeSnapshot ?? defaults.includeSnapshot,
144+
includeConsole: userExpectation.includeConsole ?? defaults.includeConsole,
145+
includeDownloads: userExpectation.includeDownloads ?? defaults.includeDownloads,
146+
includeTabs: userExpectation.includeTabs ?? defaults.includeTabs,
147+
includeCode: userExpectation.includeCode ?? defaults.includeCode,
148+
snapshotOptions: userExpectation.snapshotOptions,
149+
consoleOptions: userExpectation.consoleOptions,
150+
imageOptions: userExpectation.imageOptions
151+
};
152+
}

0 commit comments

Comments
 (0)