Skip to content

Commit c6a7c4f

Browse files
authored
chore: page.agent timeouts sorted (#38839)
1 parent 246fef3 commit c6a7c4f

25 files changed

+232
-87
lines changed

docs/src/api/class-page.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -722,6 +722,11 @@ Initialize page agent with the llm provider and cache.
722722
- `cacheFile` ?<[string]> Cache file to use/generate code for performed actions into. Cache is not used if not specified (default).
723723
- `cacheOutFile` ?<[string]> When specified, generated entries are written into the `cacheOutFile` instead of updating the `cacheFile`.
724724

725+
### option: Page.agent.expect
726+
* since: v1.58
727+
- `expect` <[Object]>
728+
- `timeout` ?<[int]> Default timeout for expect calls in milliseconds, defaults to 5000ms.
729+
725730
### option: Page.agent.limits
726731
* since: v1.58
727732
- `limits` <[Object]>

docs/src/api/class-pageagent.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,12 @@ await agent.expect('"0 items" to be reported');
3535

3636
Expectation to assert.
3737

38+
### option: PageAgent.expect.timeout
39+
* since: v1.58
40+
- `timeout` <[float]>
41+
42+
Expect timeout in milliseconds. Defaults to `5000`. The default value can be changed via `expect.timeout` option in the config, or by specifying the `expect` property of the [`option: Page.agent.expect`] option. Pass `0` to disable timeout.
43+
3844
### option: PageAgent.expect.-inline- = %%-page-agent-call-options-v1.58-%%
3945
* since: v1.58
4046

@@ -68,6 +74,13 @@ Task to perform using agentic loop.
6874
* since: v1.58
6975
- `schema` <[z.ZodSchema]>
7076

77+
### option: PageAgent.extract.timeout
78+
* since: v1.58
79+
- `timeout` <[float]>
80+
81+
Extract timeout in milliseconds. Defaults to `5000`. The default value can be changed via `actionTimeout` option in the config, or by using the [`method: BrowserContext.setDefaultTimeout`] or
82+
[`method: Page.setDefaultTimeout`] methods. Pass `0` to disable timeout.
83+
7184
### option: PageAgent.extract.-inline- = %%-page-agent-call-options-v1.58-%%
7285
* since: v1.58
7386

@@ -94,6 +107,13 @@ await agent.perform('Click submit button');
94107

95108
Task to perform using agentic loop.
96109

110+
### option: PageAgent.perform.timeout
111+
* since: v1.58
112+
- `timeout` <[float]>
113+
114+
Perform timeout in milliseconds. Defaults to `5000`. The default value can be changed via `actionTimeout` option in the config, or by using the [`method: BrowserContext.setDefaultTimeout`] or
115+
[`method: Page.setDefaultTimeout`] methods. Pass `0` to disable timeout.
116+
97117
### option: PageAgent.perform.-inline- = %%-page-agent-call-options-v1.58-%%
98118
* since: v1.58
99119

docs/src/api/params.md

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -396,18 +396,11 @@ Maximum number of agentic actions to generate, defaults to context-wide value sp
396396

397397
Maximum number of retries when generating each action, defaults to context-wide value specified in `agent` property.
398398

399-
## page-agent-timeout
400-
* since: v1.58
401-
- `timeout` <[int]>
402-
403-
Request timeout in milliseconds. Defaults to action timeout. Pass `0` to disable timeout.
404-
405399
## page-agent-call-options-v1.58
406400
- %%-page-agent-cache-key-%%
407401
- %%-page-agent-max-tokens-%%
408402
- %%-page-agent-max-actions-%%
409403
- %%-page-agent-max-action-retries-%%
410-
- %%-page-agent-timeout-%%
411404

412405
## fetch-param-url
413406
- `url` <[string]>

packages/injected/src/injectedScript.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,7 @@ import type { Builtins } from './utilityScript';
5050
export type FrameExpectParams = Omit<channels.FrameExpectParams, 'expectedValue' | 'timeout'> & {
5151
expectedValue?: any;
5252
timeoutForLogs?: number;
53-
explicitTimeout?: number;
54-
noPreChecks?: boolean;
53+
noAutoWaiting?: boolean;
5554
};
5655

5756
export type ElementState = 'visible' | 'hidden' | 'enabled' | 'disabled' | 'editable' | 'checked' | 'unchecked' | 'indeterminate' | 'stable';

packages/playwright-client/types/types.d.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2112,6 +2112,13 @@ export interface Page {
21122112
cacheOutFile?: string;
21132113
};
21142114

2115+
expect?: {
2116+
/**
2117+
* Default timeout for expect calls in milliseconds, defaults to 5000ms.
2118+
*/
2119+
timeout?: number;
2120+
};
2121+
21152122
/**
21162123
* Limits to use for the agentic loop.
21172124
*/
@@ -5440,7 +5447,10 @@ export interface PageAgent {
54405447
maxTokens?: number;
54415448

54425449
/**
5443-
* Request timeout in milliseconds. Defaults to action timeout. Pass `0` to disable timeout.
5450+
* Expect timeout in milliseconds. Defaults to `5000`. The default value can be changed via `expect.timeout` option in
5451+
* the config, or by specifying the `expect` property of the
5452+
* [`expect`](https://playwright.dev/docs/api/class-page#page-agent-option-expect) option. Pass `0` to disable
5453+
* timeout.
54445454
*/
54455455
timeout?: number;
54465456
}): Promise<void>;
@@ -5482,7 +5492,11 @@ export interface PageAgent {
54825492
maxTokens?: number;
54835493

54845494
/**
5485-
* Request timeout in milliseconds. Defaults to action timeout. Pass `0` to disable timeout.
5495+
* Perform timeout in milliseconds. Defaults to `5000`. The default value can be changed via `actionTimeout` option in
5496+
* the config, or by using the
5497+
* [browserContext.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-browsercontext#browser-context-set-default-timeout)
5498+
* or [page.setDefaultTimeout(timeout)](https://playwright.dev/docs/api/class-page#page-set-default-timeout) methods.
5499+
* Pass `0` to disable timeout.
54865500
*/
54875501
timeout?: number;
54885502
}): Promise<{

packages/playwright-core/src/client/page.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -865,7 +865,9 @@ export class Page extends ChannelOwner<channels.PageChannel> implements api.Page
865865
systemPrompt: options.systemPrompt,
866866
};
867867
const { agent } = await this._channel.agent(params);
868-
return PageAgent.from(agent);
868+
const pageAgent = PageAgent.from(agent);
869+
pageAgent._expectTimeout = options?.expect?.timeout;
870+
return pageAgent;
869871
}
870872

871873
async _snapshotForAI(options: TimeoutOptions & { track?: string } = {}): Promise<{ full: string, incremental?: string }> {

packages/playwright-core/src/client/pageAgent.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,9 @@ import { Page } from './page';
2222
import type * as api from '../../types/types';
2323
import type * as channels from '@protocol/channels';
2424

25-
type PageAgentOptions = {
26-
maxTokens?: number;
27-
maxTurns?: number;
28-
cacheKey?: string;
29-
};
30-
3125
export class PageAgent extends ChannelOwner<channels.PageAgentChannel> implements api.PageAgent {
3226
private _page: Page;
27+
_expectTimeout?: number;
3328

3429
static from(channel: channels.PageAgentChannel): PageAgent {
3530
return (channel as any)._object;
@@ -41,17 +36,20 @@ export class PageAgent extends ChannelOwner<channels.PageAgentChannel> implement
4136
this._channel.on('turn', params => this.emit(Events.Page.AgentTurn, params));
4237
}
4338

44-
async expect(expectation: string, options: PageAgentOptions = {}) {
45-
await this._channel.expect({ expectation, ...options });
39+
async expect(expectation: string, options: channels.PageAgentExpectOptions = {}) {
40+
const timeout = options.timeout ?? this._expectTimeout ?? 5000;
41+
await this._channel.expect({ expectation, ...options, timeout });
4642
}
4743

48-
async perform(task: string, options: PageAgentOptions = {}) {
49-
const { usage } = await this._channel.perform({ task, ...options });
44+
async perform(task: string, options: channels.PageAgentPerformOptions = {}) {
45+
const timeout = this._page._timeoutSettings.timeout(options);
46+
const { usage } = await this._channel.perform({ task, ...options, timeout });
5047
return { usage };
5148
}
5249

53-
async extract<Schema extends any>(query: string, schema: Schema, options: PageAgentOptions = {}): Promise<{ result: any, usage: channels.AgentUsage }> {
54-
const { result, usage } = await this._channel.extract({ query, schema: this._page._platform.zodToJsonSchema(schema), ...options });
50+
async extract<Schema extends any>(query: string, schema: Schema, options: channels.PageAgentExtractOptions = {}): Promise<{ result: any, usage: channels.AgentUsage }> {
51+
const timeout = this._page._timeoutSettings.timeout(options);
52+
const { result, usage } = await this._channel.extract({ query, schema: this._page._platform.zodToJsonSchema(schema), ...options, timeout });
5553
return { result, usage };
5654
}
5755

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

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -143,14 +143,11 @@ async function innerRunAction(progress: Progress, mode: 'generate' | 'run', page
143143
}
144144

145145
async function runExpect(frame: Frame, progress: Progress, mode: 'generate' | 'run', selector: string | undefined, options: FrameExpectParams, expected: string | RegExp, matcherName: string, expectation: string) {
146-
// Pass explicit timeout to limit the single expect action inside the overall "agentic expect" multi-step progress.
147-
const timeout = expectTimeout(mode);
148146
const result = await frame.expect(progress, selector, {
149147
...options,
150-
timeoutForLogs: timeout,
151-
explicitTimeout: timeout,
152-
// Disable pre-checks to avoid them timing out, model has seen the snapshot anyway.
153-
noPreChecks: mode === 'generate',
148+
// When generating, we want the expect to pass or fail immediately and give feedback to the model.
149+
noAutoWaiting: mode === 'generate',
150+
timeoutForLogs: mode === 'generate' ? undefined : progress.timeout,
154151
});
155152
if (!result.matches === !options.isNot) {
156153
const received = matcherName === 'toMatchAriaSnapshot' ? '\n' + result.received.raw : result.received;
@@ -162,7 +159,7 @@ async function runExpect(frame: Frame, progress: Progress, mode: 'generate' | 'r
162159
expectation,
163160
locator: selector ? asLocatorDescription('javascript', selector) : undefined,
164161
timedOut: result.timedOut,
165-
timeout,
162+
timeout: mode === 'generate' ? undefined : progress.timeout,
166163
printedExpected: options.isNot ? `Expected${expectedSuffix}: not ${expectedDisplay}` : `Expected${expectedSuffix}: ${expectedDisplay}`,
167164
printedReceived: result.errorMessage ? '' : `Received: ${received}`,
168165
errorMessage: result.errorMessage,
@@ -266,7 +263,7 @@ export function traceParamsForAction(progress: Progress, action: actions.Action,
266263
expression: 'to.have.value',
267264
expectedText,
268265
isNot: !!action.isNot,
269-
timeout: expectTimeout(mode),
266+
timeout,
270267
};
271268
return { type: 'Frame', method: 'expect', title: 'Expect Value', params };
272269
} else if (action.type === 'checkbox' || action.type === 'radio') {
@@ -275,7 +272,7 @@ export function traceParamsForAction(progress: Progress, action: actions.Action,
275272
selector: action.selector,
276273
expression: 'to.be.checked',
277274
isNot: !!action.isNot,
278-
timeout: expectTimeout(mode),
275+
timeout,
279276
};
280277
return { type: 'Frame', method: 'expect', title: 'Expect Checked', params };
281278
} else {
@@ -287,7 +284,7 @@ export function traceParamsForAction(progress: Progress, action: actions.Action,
287284
selector: action.selector,
288285
expression: 'to.be.visible',
289286
isNot: !!action.isNot,
290-
timeout: expectTimeout(mode),
287+
timeout,
291288
};
292289
return { type: 'Frame', method: 'expect', title: 'Expect Visible', params };
293290
}
@@ -298,7 +295,7 @@ export function traceParamsForAction(progress: Progress, action: actions.Action,
298295
expression: 'to.match.snapshot',
299296
expectedText: [],
300297
isNot: !!action.isNot,
301-
timeout: expectTimeout(mode),
298+
timeout,
302299
};
303300
return { type: 'Frame', method: 'expect', title: 'Expect Aria Snapshot', params };
304301
}
@@ -310,7 +307,7 @@ export function traceParamsForAction(progress: Progress, action: actions.Action,
310307
expression: 'to.have.url',
311308
expectedText,
312309
isNot: !!action.isNot,
313-
timeout: expectTimeout(mode),
310+
timeout,
314311
};
315312
return { type: 'Frame', method: 'expect', title: 'Expect URL', params };
316313
}
@@ -321,7 +318,7 @@ export function traceParamsForAction(progress: Progress, action: actions.Action,
321318
expression: 'to.have.title',
322319
expectedText,
323320
isNot: !!action.isNot,
324-
timeout: expectTimeout(mode),
321+
timeout,
325322
};
326323
return { type: 'Frame', method: 'expect', title: 'Expect Title', params };
327324
}
@@ -341,7 +338,3 @@ function callMetadataForAction(progress: Progress, frame: Frame, action: actions
341338
};
342339
return callMetadata;
343340
}
344-
345-
function expectTimeout(mode: 'generate' | 'run') {
346-
return mode === 'generate' ? 0 : 5000;
347-
}

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export async function pageAgentPerform(progress: Progress, context: Context, use
5050
### Task
5151
${userTask}
5252
`;
53-
53+
progress.disableTimeout();
5454
await runLoop(progress, context, performTools, task, undefined, callParams);
5555
await updateCache(context, cacheKey);
5656
}
@@ -68,7 +68,7 @@ export async function pageAgentExpect(progress: Progress, context: Context, expe
6868
### Expectation
6969
${expectation}
7070
`;
71-
71+
progress.disableTimeout();
7272
await runLoop(progress, context, expectTools, task, undefined, callParams);
7373
await updateCache(context, cacheKey);
7474
}
@@ -101,7 +101,7 @@ async function runLoop(progress: Progress, context: Context, toolDefinitions: To
101101

102102
const apiCacheTextBefore = context.agentParams.apiCacheFile ?
103103
await fs.promises.readFile(context.agentParams.apiCacheFile, 'utf-8').catch(() => '{}') : '{}';
104-
const apiCacheBefore = JSON.parse(apiCacheTextBefore);
104+
const apiCacheBefore = JSON.parse(apiCacheTextBefore || '{}');
105105

106106
const loop = new Loop({
107107
api: context.agentParams.api as any,

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

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,34 +1389,30 @@ export class Frame extends SdkObject<FrameEventMap> {
13891389
progress.metadata.error = { error: { name: 'Expect', message: 'Expect failed' } };
13901390
};
13911391
try {
1392-
// When explicit timeout is passed, constrain expect by it, in addition to regular progress abort.
1393-
const timeoutPromise = options.explicitTimeout !== undefined ? progress.wait(options.explicitTimeout).then(() => { throw new TimeoutError(`Timed out after ${options.explicitTimeout}ms`); }) : undefined;
1394-
timeoutPromise?.catch(() => { /* Prevent unhandled promise rejection */ });
1395-
13961392
// Step 1: perform locator handlers checkpoint with a specified timeout.
13971393
if (selector)
13981394
progress.log(`waiting for ${this._asLocator(selector)}`);
1399-
if (!options.noPreChecks)
1395+
if (!options.noAutoWaiting)
14001396
await this._page.performActionPreChecks(progress);
14011397

14021398
// Step 2: perform one-shot expect check without a timeout.
14031399
// Supports the case of `expect(locator).toBeVisible({ timeout: 1 })`
14041400
// that should succeed when the locator is already visible.
14051401
try {
14061402
const resultOneShot = await this._expectInternal(progress, selector, options, lastIntermediateResult, true);
1407-
if (resultOneShot.matches !== options.isNot)
1403+
if (options.noAutoWaiting || resultOneShot.matches !== options.isNot)
14081404
return resultOneShot;
14091405
} catch (e) {
1410-
if (this.isNonRetriableError(e))
1406+
if (options.noAutoWaiting || this.isNonRetriableError(e))
14111407
throw e;
14121408
// Ignore any other errors from one-shot, we'll handle them during retries.
14131409
}
14141410

14151411
// Step 3: auto-retry expect with increasing timeouts. Bounded by the total remaining time.
14161412
const result = await this.retryWithProgressAndTimeouts(progress, [100, 250, 500, 1000], async continuePolling => {
1417-
if (!options.noPreChecks)
1413+
if (!options.noAutoWaiting)
14181414
await this._page.performActionPreChecks(progress);
1419-
const { matches, received } = await this._expectInternal(progress, selector, options, lastIntermediateResult, false, timeoutPromise);
1415+
const { matches, received } = await this._expectInternal(progress, selector, options, lastIntermediateResult, false);
14201416
if (matches === options.isNot) {
14211417
// Keep waiting in these cases:
14221418
// expect(locator).conditionThatDoesNotMatch
@@ -1446,9 +1442,9 @@ export class Frame extends SdkObject<FrameEventMap> {
14461442
}
14471443
}
14481444

1449-
private async _expectInternal(progress: Progress, selector: string | undefined, options: FrameExpectParams, lastIntermediateResult: { received?: any, isSet: boolean, errorMessage?: string }, noAbort: boolean, timeoutPromise?: Promise<never>) {
1445+
private async _expectInternal(progress: Progress, selector: string | undefined, options: FrameExpectParams, lastIntermediateResult: { received?: any, isSet: boolean, errorMessage?: string }, noAbort: boolean) {
14501446
// The first expect check, a.k.a. one-shot, always finishes - even when progress is aborted.
1451-
const race = <T>(p: Promise<T>) => noAbort ? p : (timeoutPromise ? progress.race([p, timeoutPromise]) : progress.race(p));
1447+
const race = <T>(p: Promise<T>) => noAbort ? p : progress.race(p);
14521448
const selectorInFrame = selector ? await race(this.selectors.resolveFrameForSelector(selector, { strict: true })) : undefined;
14531449

14541450
const { frame, info } = selectorInFrame || { frame: this, info: undefined };

0 commit comments

Comments
 (0)