Skip to content

Commit 96a0e88

Browse files
authored
Merge pull request #187 from Predicate-Labs/expanded_verification
expanded verifications
2 parents e55f858 + 5f011b8 commit 96a0e88

File tree

2 files changed

+133
-7
lines changed

2 files changed

+133
-7
lines changed

src/agent-runtime.ts

Lines changed: 84 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,21 @@ export interface EventuallyOptions {
100100
timeoutMs?: number;
101101
pollMs?: number;
102102
snapshotOptions?: Record<string, any>;
103+
/**
104+
* Optional: increase snapshot `limit` across retries (additive schedule).
105+
*
106+
* Useful on long/virtualized pages where a small element limit can miss targets.
107+
*/
108+
snapshotLimitGrowth?: {
109+
/** Defaults to snapshotOptions.limit if present, else 50. */
110+
startLimit?: number;
111+
/** Defaults to startLimit. */
112+
step?: number;
113+
/** Defaults to 500. */
114+
maxLimit?: number;
115+
/** 'only_on_fail' (default) grows on attempt>1; 'all' always applies schedule. */
116+
applyOn?: 'only_on_fail' | 'all';
117+
};
103118
/** If set, `.eventually()` will treat snapshots below this confidence as failures and resnapshot. */
104119
minConfidence?: number;
105120
/** Max number of snapshot attempts to get above minConfidence before declaring exhaustion. */
@@ -133,6 +148,7 @@ export class AssertionHandle {
133148
const timeoutMs = options.timeoutMs ?? 10_000;
134149
const pollMs = options.pollMs ?? 250;
135150
const snapshotOptions = options.snapshotOptions;
151+
const snapshotLimitGrowth = options.snapshotLimitGrowth;
136152
const minConfidence = options.minConfidence;
137153
const maxSnapshotAttempts = options.maxSnapshotAttempts ?? 3;
138154
const visionProvider = options.visionProvider;
@@ -143,10 +159,45 @@ export class AssertionHandle {
143159
let attempt = 0;
144160
let snapshotAttempt = 0;
145161
let lastOutcome: ReturnType<Predicate> | null = null;
162+
let snapshotLimit: number | null = null;
163+
164+
const clampLimit = (n: number): number => {
165+
if (!Number.isFinite(n)) return 50;
166+
if (n < 1) return 1;
167+
if (n > 500) return 500;
168+
return Math.floor(n);
169+
};
170+
171+
const growthApplyOn = snapshotLimitGrowth?.applyOn ?? 'only_on_fail';
172+
const startLimit = clampLimit(
173+
snapshotLimitGrowth?.startLimit ??
174+
(typeof snapshotOptions?.limit === 'number' ? snapshotOptions.limit : 50)
175+
);
176+
const step = clampLimit(snapshotLimitGrowth?.step ?? startLimit);
177+
const maxLimit = clampLimit(snapshotLimitGrowth?.maxLimit ?? 500);
178+
179+
const limitForAttempt = (attempt1: number): number => {
180+
const base = startLimit + step * Math.max(0, attempt1 - 1);
181+
return clampLimit(Math.min(maxLimit, base));
182+
};
146183

147184
while (true) {
148185
attempt += 1;
149-
await this.runtime.snapshot(snapshotOptions);
186+
187+
const perAttemptOptions: Record<string, any> = { ...(snapshotOptions ?? {}) };
188+
snapshotLimit = null;
189+
if (snapshotLimitGrowth) {
190+
const apply =
191+
growthApplyOn === 'all' ||
192+
attempt === 1 ||
193+
(lastOutcome !== null && lastOutcome.passed === false);
194+
snapshotLimit = apply ? limitForAttempt(attempt) : startLimit;
195+
perAttemptOptions.limit = snapshotLimit;
196+
} else if (typeof perAttemptOptions.limit === 'number') {
197+
snapshotLimit = clampLimit(perAttemptOptions.limit);
198+
}
199+
200+
await this.runtime.snapshot(perAttemptOptions);
150201
snapshotAttempt += 1;
151202

152203
const diagnostics = this.runtime.lastSnapshot?.diagnostics;
@@ -173,7 +224,13 @@ export class AssertionHandle {
173224
lastOutcome,
174225
this.label,
175226
this.required,
176-
{ eventually: true, attempt, snapshot_attempt: snapshotAttempt, final: false },
227+
{
228+
eventually: true,
229+
attempt,
230+
snapshot_attempt: snapshotAttempt,
231+
snapshot_limit: snapshotLimit,
232+
final: false,
233+
},
177234
false
178235
);
179236

@@ -215,6 +272,7 @@ export class AssertionHandle {
215272
eventually: true,
216273
attempt,
217274
snapshot_attempt: snapshotAttempt,
275+
snapshot_limit: snapshotLimit,
218276
final: true,
219277
vision_fallback: true,
220278
},
@@ -251,6 +309,7 @@ export class AssertionHandle {
251309
eventually: true,
252310
attempt,
253311
snapshot_attempt: snapshotAttempt,
312+
snapshot_limit: snapshotLimit,
254313
final: true,
255314
exhausted: true,
256315
},
@@ -271,6 +330,7 @@ export class AssertionHandle {
271330
eventually: true,
272331
attempt,
273332
snapshot_attempt: snapshotAttempt,
333+
snapshot_limit: snapshotLimit,
274334
final: true,
275335
timeout: true,
276336
},
@@ -295,7 +355,13 @@ export class AssertionHandle {
295355
lastOutcome,
296356
this.label,
297357
this.required,
298-
{ eventually: true, attempt, final: false },
358+
{
359+
eventually: true,
360+
attempt,
361+
snapshot_attempt: snapshotAttempt,
362+
snapshot_limit: snapshotLimit,
363+
final: false,
364+
},
299365
false
300366
);
301367

@@ -305,7 +371,13 @@ export class AssertionHandle {
305371
lastOutcome,
306372
this.label,
307373
this.required,
308-
{ eventually: true, attempt, final: true },
374+
{
375+
eventually: true,
376+
attempt,
377+
snapshot_attempt: snapshotAttempt,
378+
snapshot_limit: snapshotLimit,
379+
final: true,
380+
},
309381
true
310382
);
311383
return true;
@@ -317,7 +389,14 @@ export class AssertionHandle {
317389
lastOutcome,
318390
this.label,
319391
this.required,
320-
{ eventually: true, attempt, final: true, timeout: true },
392+
{
393+
eventually: true,
394+
attempt,
395+
snapshot_attempt: snapshotAttempt,
396+
snapshot_limit: snapshotLimit,
397+
final: true,
398+
timeout: true,
399+
},
321400
true
322401
);
323402
if (this.required) {

src/asserts/expect.ts

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ export interface EventuallyConfig {
3535
poll?: number;
3636
/** Max number of retry attempts (default 3) */
3737
maxRetries?: number;
38+
/**
39+
* Optional: increase snapshot `limit` across retries (additive schedule).
40+
*
41+
* This mirrors AgentRuntime's AssertionHandle.eventually() behavior.
42+
*/
43+
snapshotLimitGrowth?: {
44+
startLimit?: number;
45+
step?: number;
46+
maxLimit?: number;
47+
applyOn?: 'only_on_fail' | 'all';
48+
};
3849
}
3950

4051
/**
@@ -456,14 +467,20 @@ export const expect = Object.assign(
456467
*/
457468
export class EventuallyWrapper {
458469
private _predicate: Predicate;
459-
private _config: Required<EventuallyConfig>;
470+
private _config: {
471+
timeout: number;
472+
poll: number;
473+
maxRetries: number;
474+
snapshotLimitGrowth?: EventuallyConfig['snapshotLimitGrowth'];
475+
};
460476

461477
constructor(predicate: Predicate, config: EventuallyConfig = {}) {
462478
this._predicate = predicate;
463479
this._config = {
464480
timeout: config.timeout ?? DEFAULT_TIMEOUT,
465481
poll: config.poll ?? DEFAULT_POLL,
466482
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
483+
snapshotLimitGrowth: config.snapshotLimitGrowth,
467484
};
468485
}
469486

@@ -482,6 +499,22 @@ export class EventuallyWrapper {
482499
let lastOutcome: AssertOutcome | null = null;
483500
let attempts = 0;
484501

502+
const growth = this._config.snapshotLimitGrowth;
503+
const clampLimit = (n: number): number => {
504+
if (!Number.isFinite(n)) return 50;
505+
if (n < 1) return 1;
506+
if (n > 500) return 500;
507+
return Math.floor(n);
508+
};
509+
const growthApplyOn = growth?.applyOn ?? 'only_on_fail';
510+
const startLimit = clampLimit(growth?.startLimit ?? 50);
511+
const step = clampLimit(growth?.step ?? startLimit);
512+
const maxLimit = clampLimit(growth?.maxLimit ?? 500);
513+
const limitForAttempt = (attempt1: number): number => {
514+
const base = startLimit + step * Math.max(0, attempt1 - 1);
515+
return clampLimit(Math.min(maxLimit, base));
516+
};
517+
485518
while (true) {
486519
// Check timeout (higher precedence than maxRetries)
487520
const elapsed = Date.now() - startTime;
@@ -513,7 +546,21 @@ export class EventuallyWrapper {
513546
// Take fresh snapshot if not first attempt
514547
if (attempts > 0) {
515548
try {
516-
const freshSnapshot = await snapshotFn();
549+
// If snapshotFn supports kwargs (e.g. runtime.snapshot), pass adaptive limit.
550+
let freshSnapshot: AssertContext['snapshot'];
551+
const attempt1 = attempts + 1;
552+
if (growth) {
553+
const apply =
554+
growthApplyOn === 'all' || (growthApplyOn === 'only_on_fail' && lastOutcome);
555+
const snapLimit = apply ? limitForAttempt(attempt1) : startLimit;
556+
try {
557+
freshSnapshot = await (snapshotFn as any)({ limit: snapLimit });
558+
} catch {
559+
freshSnapshot = await snapshotFn();
560+
}
561+
} else {
562+
freshSnapshot = await snapshotFn();
563+
}
517564
ctx = {
518565
snapshot: freshSnapshot,
519566
url: freshSnapshot?.url ?? ctx.url,

0 commit comments

Comments
 (0)