@tangle-network/browser-agent-driver 0.31.0 → 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/dist/artifacts/filesystem-sink.d.ts +1 -1
  2. package/dist/artifacts/filesystem-sink.js +1 -1
  3. package/dist/brain/index.d.ts +16 -9
  4. package/dist/brain/index.d.ts.map +1 -1
  5. package/dist/brain/index.js +75 -72
  6. package/dist/brain/index.js.map +1 -1
  7. package/dist/browser-launch.js +1 -3
  8. package/dist/browser-launch.js.map +1 -1
  9. package/dist/captcha.d.ts.map +1 -1
  10. package/dist/captcha.js +1 -3
  11. package/dist/captcha.js.map +1 -1
  12. package/dist/cli-attach.d.ts +2 -2
  13. package/dist/cli-attach.js +2 -2
  14. package/dist/cli-design-audit.d.ts.map +1 -1
  15. package/dist/cli-design-audit.js +14 -24
  16. package/dist/cli-design-audit.js.map +1 -1
  17. package/dist/cli-jobs.js +4 -13
  18. package/dist/cli-jobs.js.map +1 -1
  19. package/dist/cli-patterns.d.ts +1 -1
  20. package/dist/cli-patterns.d.ts.map +1 -1
  21. package/dist/cli.js +26 -28
  22. package/dist/cli.js.map +1 -1
  23. package/dist/config.d.ts +5 -3
  24. package/dist/config.d.ts.map +1 -1
  25. package/dist/config.js +1 -0
  26. package/dist/config.js.map +1 -1
  27. package/dist/design/audit/attribution/types.d.ts +3 -3
  28. package/dist/design/audit/attribution/types.d.ts.map +1 -1
  29. package/dist/design/audit/attribution/types.js +2 -2
  30. package/dist/design/audit/build-result.d.ts +71 -0
  31. package/dist/design/audit/build-result.d.ts.map +1 -0
  32. package/dist/design/audit/build-result.js +251 -0
  33. package/dist/design/audit/build-result.js.map +1 -0
  34. package/dist/design/audit/classify-ensemble.d.ts +1 -1
  35. package/dist/design/audit/classify-ensemble.d.ts.map +1 -1
  36. package/dist/design/audit/ethics/check.d.ts +2 -2
  37. package/dist/design/audit/ethics/check.d.ts.map +1 -1
  38. package/dist/design/audit/ethics/check.js +1 -1
  39. package/dist/design/audit/ethics/loader.d.ts +1 -1
  40. package/dist/design/audit/ethics/loader.d.ts.map +1 -1
  41. package/dist/design/audit/evaluate.d.ts +5 -2
  42. package/dist/design/audit/evaluate.d.ts.map +1 -1
  43. package/dist/design/audit/evaluate.js +10 -4
  44. package/dist/design/audit/evaluate.js.map +1 -1
  45. package/dist/design/audit/first-principles-mode.d.ts +1 -1
  46. package/dist/design/audit/first-principles-mode.d.ts.map +1 -1
  47. package/dist/design/audit/modality/android.d.ts +1 -1
  48. package/dist/design/audit/modality/android.d.ts.map +1 -1
  49. package/dist/design/audit/modality/html.d.ts +1 -1
  50. package/dist/design/audit/modality/html.d.ts.map +1 -1
  51. package/dist/design/audit/modality/ios.d.ts +1 -1
  52. package/dist/design/audit/modality/ios.d.ts.map +1 -1
  53. package/dist/design/audit/modality/types.d.ts +2 -2
  54. package/dist/design/audit/modality/types.d.ts.map +1 -1
  55. package/dist/design/audit/modality/types.js +1 -1
  56. package/dist/design/audit/patches/generate.d.ts +57 -0
  57. package/dist/design/audit/patches/generate.d.ts.map +1 -0
  58. package/dist/design/audit/patches/generate.js +168 -0
  59. package/dist/design/audit/patches/generate.js.map +1 -0
  60. package/dist/design/audit/patches/parse.d.ts +1 -1
  61. package/dist/design/audit/patches/parse.d.ts.map +1 -1
  62. package/dist/design/audit/patches/render.d.ts +1 -1
  63. package/dist/design/audit/patches/render.d.ts.map +1 -1
  64. package/dist/design/audit/patches/severity-enforcement.d.ts +1 -1
  65. package/dist/design/audit/patches/severity-enforcement.d.ts.map +1 -1
  66. package/dist/design/audit/patches/validate.d.ts +7 -1
  67. package/dist/design/audit/patches/validate.d.ts.map +1 -1
  68. package/dist/design/audit/patches/validate.js +8 -1
  69. package/dist/design/audit/patches/validate.js.map +1 -1
  70. package/dist/design/audit/patterns/match.d.ts +1 -1
  71. package/dist/design/audit/patterns/match.d.ts.map +1 -1
  72. package/dist/design/audit/patterns/types.d.ts +2 -2
  73. package/dist/design/audit/patterns/types.d.ts.map +1 -1
  74. package/dist/design/audit/pipeline.d.ts +4 -4
  75. package/dist/design/audit/pipeline.d.ts.map +1 -1
  76. package/dist/design/audit/pipeline.js +45 -30
  77. package/dist/design/audit/pipeline.js.map +1 -1
  78. package/dist/design/audit/rubric/loader.d.ts +1 -1
  79. package/dist/design/audit/rubric/loader.d.ts.map +1 -1
  80. package/dist/design/audit/rubric/rollup-weights.d.ts +1 -1
  81. package/dist/design/audit/rubric/rollup-weights.d.ts.map +1 -1
  82. package/dist/design/audit/{v2/types.d.ts → score-types.d.ts} +10 -10
  83. package/dist/design/audit/score-types.d.ts.map +1 -0
  84. package/dist/design/audit/{v2/types.js → score-types.js} +2 -2
  85. package/dist/design/audit/score-types.js.map +1 -0
  86. package/dist/design/audit/{v2/score.d.ts → score.d.ts} +9 -9
  87. package/dist/design/audit/score.d.ts.map +1 -0
  88. package/dist/design/audit/{v2/score.js → score.js} +17 -17
  89. package/dist/design/audit/score.js.map +1 -0
  90. package/dist/design/audit/types.d.ts +7 -7
  91. package/dist/design/audit/types.d.ts.map +1 -1
  92. package/dist/drivers/cursor-overlay.d.ts +1 -1
  93. package/dist/drivers/cursor-overlay.d.ts.map +1 -1
  94. package/dist/drivers/cursor-overlay.js +2 -2
  95. package/dist/drivers/extract-with-index.d.ts +4 -7
  96. package/dist/drivers/extract-with-index.d.ts.map +1 -1
  97. package/dist/drivers/extract-with-index.js +4 -7
  98. package/dist/drivers/extract-with-index.js.map +1 -1
  99. package/dist/drivers/overlay-label.d.ts +3 -4
  100. package/dist/drivers/overlay-label.d.ts.map +1 -1
  101. package/dist/drivers/overlay-label.js.map +1 -1
  102. package/dist/drivers/playwright.d.ts +2 -2
  103. package/dist/drivers/playwright.d.ts.map +1 -1
  104. package/dist/drivers/playwright.js +10 -24
  105. package/dist/drivers/playwright.js.map +1 -1
  106. package/dist/drivers/som-overlay.d.ts +2 -3
  107. package/dist/drivers/som-overlay.d.ts.map +1 -1
  108. package/dist/drivers/som-overlay.js +2 -3
  109. package/dist/drivers/som-overlay.js.map +1 -1
  110. package/dist/drivers/types.d.ts +3 -9
  111. package/dist/drivers/types.d.ts.map +1 -1
  112. package/dist/jobs/anti-bot.js +1 -1
  113. package/dist/jobs/anti-bot.js.map +1 -1
  114. package/dist/jobs/types.d.ts +1 -1
  115. package/dist/jobs/types.d.ts.map +1 -1
  116. package/dist/multi-actor.d.ts.map +1 -1
  117. package/dist/multi-actor.js +2 -3
  118. package/dist/multi-actor.js.map +1 -1
  119. package/dist/reports/aggregate.js +6 -6
  120. package/dist/reports/aggregate.js.map +1 -1
  121. package/dist/reports/tokens.d.ts +0 -8
  122. package/dist/reports/tokens.d.ts.map +1 -1
  123. package/dist/reports/tokens.js +1 -15
  124. package/dist/reports/tokens.js.map +1 -1
  125. package/dist/reports/types.d.ts +2 -2
  126. package/dist/reports/types.d.ts.map +1 -1
  127. package/dist/run-state.d.ts +21 -15
  128. package/dist/run-state.d.ts.map +1 -1
  129. package/dist/run-state.js +23 -22
  130. package/dist/run-state.js.map +1 -1
  131. package/dist/runner/completion-language.d.ts +2 -0
  132. package/dist/runner/completion-language.d.ts.map +1 -0
  133. package/dist/runner/completion-language.js +5 -0
  134. package/dist/runner/completion-language.js.map +1 -0
  135. package/dist/runner/direct-start.d.ts +19 -0
  136. package/dist/runner/direct-start.d.ts.map +1 -0
  137. package/dist/runner/direct-start.js +295 -0
  138. package/dist/runner/direct-start.js.map +1 -0
  139. package/dist/runner/events.d.ts +3 -6
  140. package/dist/runner/events.d.ts.map +1 -1
  141. package/dist/runner/events.js.map +1 -1
  142. package/dist/runner/fan-out.d.ts +6 -9
  143. package/dist/runner/fan-out.d.ts.map +1 -1
  144. package/dist/runner/fan-out.js +7 -15
  145. package/dist/runner/fan-out.js.map +1 -1
  146. package/dist/runner/goal-decomposer.d.ts +0 -1
  147. package/dist/runner/goal-decomposer.d.ts.map +1 -1
  148. package/dist/runner/goal-decomposer.js +0 -1
  149. package/dist/runner/goal-decomposer.js.map +1 -1
  150. package/dist/runner/google-flights-preflight.d.ts +35 -0
  151. package/dist/runner/google-flights-preflight.d.ts.map +1 -0
  152. package/dist/runner/google-flights-preflight.js +411 -0
  153. package/dist/runner/google-flights-preflight.js.map +1 -0
  154. package/dist/runner/interrupt-controller.d.ts +1 -1
  155. package/dist/runner/interrupt-controller.js +1 -1
  156. package/dist/runner/overlay-narration.d.ts +1 -1
  157. package/dist/runner/overlay-narration.js +1 -1
  158. package/dist/runner/parallel-runner.d.ts +3 -6
  159. package/dist/runner/parallel-runner.d.ts.map +1 -1
  160. package/dist/runner/parallel-runner.js +2 -3
  161. package/dist/runner/parallel-runner.js.map +1 -1
  162. package/dist/runner/pattern-extractor.d.ts +2 -2
  163. package/dist/runner/pattern-extractor.js +2 -2
  164. package/dist/runner/runner.d.ts +15 -39
  165. package/dist/runner/runner.d.ts.map +1 -1
  166. package/dist/runner/runner.js +254 -231
  167. package/dist/runner/runner.js.map +1 -1
  168. package/dist/runner/stream-webhook.d.ts +1 -1
  169. package/dist/runner/stream-webhook.js +1 -1
  170. package/dist/skills/domain-loader.d.ts +1 -2
  171. package/dist/skills/domain-loader.d.ts.map +1 -1
  172. package/dist/skills/domain-loader.js +1 -2
  173. package/dist/skills/domain-loader.js.map +1 -1
  174. package/dist/telemetry/schema.d.ts +2 -2
  175. package/dist/telemetry/schema.js +2 -2
  176. package/dist/test-runner.d.ts +4 -5
  177. package/dist/test-runner.d.ts.map +1 -1
  178. package/dist/test-runner.js +2 -3
  179. package/dist/test-runner.js.map +1 -1
  180. package/dist/types.d.ts +28 -21
  181. package/dist/types.d.ts.map +1 -1
  182. package/package.json +8 -2
  183. package/dist/design/audit/v2/build-result.d.ts +0 -47
  184. package/dist/design/audit/v2/build-result.d.ts.map +0 -1
  185. package/dist/design/audit/v2/build-result.js +0 -163
  186. package/dist/design/audit/v2/build-result.js.map +0 -1
  187. package/dist/design/audit/v2/score.d.ts.map +0 -1
  188. package/dist/design/audit/v2/score.js.map +0 -1
  189. package/dist/design/audit/v2/types.d.ts.map +0 -1
  190. package/dist/design/audit/v2/types.js.map +0 -1
@@ -37,9 +37,12 @@ import { DecisionCache } from './decision-cache.js';
37
37
  import { VerdictTracker, extractCurrentMarker, buildProgressLabel, } from './overlay-narration.js';
38
38
  import { applyDemoOverride } from './demo-overrides.js';
39
39
  import { matchDeterministicPattern } from './deterministic-patterns.js';
40
+ import { deriveDirectStartUrl, shouldAcceptRolledBookingCompletion } from './direct-start.js';
41
+ import { prepareGoogleFlightsSearch, shouldAcceptRolledGoogleFlightsCompletion } from './google-flights-preflight.js';
42
+ import { containsSelfContradictingCompletion } from './completion-language.js';
40
43
  /**
41
- * Gen 6.1: detect that the agent is filling a multi-field form one input at
42
- * a time and inject a hint that demands a `fill` batch on the next turn.
44
+ * Detect when the agent is filling a multi-field form one input at a time and
45
+ * inject a hint that demands a `fill` batch on the next turn.
43
46
  *
44
47
  * Trigger conditions (all must hold):
45
48
  * 1. The agent's most recent action was a single-step `type` on the
@@ -49,12 +52,9 @@ import { matchDeterministicPattern } from './deterministic-patterns.js';
49
52
  * 3. We haven't already injected this hint in the last turn (to avoid
50
53
  * hint loops if the agent ignores it)
51
54
  *
52
- * Why threshold of 1 type + 2 unused (not 3 consecutive types):
53
- * Multi-step forms often have 2 fields per step before the user clicks
54
- * "Next". Waiting for 3 consecutive types means the detector never fires
55
- * on a typical 2-field-per-step form. Firing on the FIRST type action
56
- * when the form clearly has more fields catches every multi-field form
57
- * the moment the agent starts on it.
55
+ * The detector fires after one type action when two or more unused fields
56
+ * remain, which catches common two-field-per-step forms before the agent
57
+ * burns extra turns.
58
58
  *
59
59
  * The hint is high-priority (100) so it survives ctxBudget truncation, and
60
60
  * it explicitly lists the unused @refs from the current snapshot so the LLM
@@ -133,8 +133,7 @@ const DEFAULT_MAX_TURNS = 20;
133
133
  const DEFAULT_RETRIES = 3;
134
134
  const DEFAULT_RETRY_DELAY_MS = 1000;
135
135
  const DEFAULT_MICRO_PLAN_ACTIONS = 2;
136
- // Gen 18: clickAt/typeAt added so vision-mode can emit multi-action turns
137
- // Gen 23: clickLabel/typeLabel for SoM-based actions
136
+ // Safe action verbs for micro-plans emitted by the model.
138
137
  const SAFE_MICRO_ACTIONS = new Set(['click', 'type', 'press', 'hover', 'select', 'scroll', 'wait', 'clickAt', 'typeAt', 'clickLabel', 'typeLabel']);
139
138
  const DEFAULT_SUPERVISOR = {
140
139
  enabled: true,
@@ -154,7 +153,7 @@ const DEFI_BRAIN_CONTEXT = '\nWALLET/DeFi MODE ACTIVE — crypto app patterns:\n
154
153
  '- NETWORK SELECTOR: Do NOT change the network/chain. If a network dropdown opens accidentally, close it immediately.\n' +
155
154
  '- COOKIE BANNERS: Dismiss immediately via Escape or Reject button — don\'t spend multiple turns on consent dialogs.\n';
156
155
  /**
157
- * Gen 7.2: detect placeholder patterns in a planner-generated complete.result.
156
+ * Detect placeholder patterns in a planner-generated complete.result.
158
157
  *
159
158
  * The planner has to commit to its `complete.result` text BEFORE any prior
160
159
  * runScript step actually runs, so on extraction tasks it fabricates
@@ -188,26 +187,7 @@ export function hasPlaceholderPattern(text) {
188
187
  }
189
188
  return false;
190
189
  }
191
- /**
192
- * Gen 9 — runtime two-pass extraction. When the planner emits a single
193
- * runScript step (per Gen 7.2 rule #7) and that script returns null /
194
- * empty / whitespace / `{x: null}` / a placeholder pattern, the auto-
195
- * complete-from-runScript path should NOT fire. Instead the runner should
196
- * mark the plan as deviated and fall through to the per-action loop where
197
- * Brain.decide can re-observe the loaded page and emit a smarter action
198
- * (different selector, click+wait, scroll, etc.).
199
- *
200
- * This addresses the failure mode the Gen 8 head-to-head gauntlet
201
- * surfaced: bad's planner-only path lost to browser-use's per-action loop
202
- * on tasks where the first runScript pick was wrong (npm, mdn signature,
203
- * w3c, github, wikipedia variance). Two-pass gives bad's per-action loop
204
- * the same recovery surface browser-use uses, with the planner's speed
205
- * advantage on the cases where runScript succeeds first try.
206
- *
207
- * "Meaningful" means: not empty/whitespace, not the literal string `null`
208
- * or `undefined`, and not matching `hasPlaceholderPattern` (which already
209
- * detects JSON null fields, "<from prior step>" markers, etc.).
210
- */
190
+ /** Returns true only when runScript output contains usable extracted data. */
211
191
  export function isMeaningfulRunScriptOutput(output) {
212
192
  if (typeof output !== 'string')
213
193
  return false;
@@ -242,6 +222,25 @@ export function isMeaningfulRunScriptOutput(output) {
242
222
  }
243
223
  return true;
244
224
  }
225
+ export function shouldUsePlannerForScenario(scenario, mode = 'always') {
226
+ if (mode !== 'auto')
227
+ return true;
228
+ const tags = new Set((scenario.tags ?? []).map((tag) => tag.toLowerCase()));
229
+ if (tags.has('extraction'))
230
+ return false;
231
+ const goal = scenario.goal.toLowerCase();
232
+ if (/\breturn\s+only\s+(?:a\s+)?json\b/.test(goal) ||
233
+ /\bvalid\s+json\s+object\b/.test(goal) ||
234
+ /\bexactly\s+these?\s+keys?\b/.test(goal)) {
235
+ return false;
236
+ }
237
+ if (/\b(?:find|extract|look up|lookup|read|identify)\b/.test(goal) &&
238
+ /\b(?:return|answer|provide)\b/.test(goal) &&
239
+ /\b(?:json|number|year|date|price|downloads?|count|signature|metric|value)\b/.test(goal)) {
240
+ return false;
241
+ }
242
+ return true;
243
+ }
245
244
  export class BrowserAgent {
246
245
  driver;
247
246
  brain;
@@ -263,7 +262,7 @@ export class BrowserAgent {
263
262
  // persists, never crosses runs.
264
263
  decisionCache;
265
264
  extensions;
266
- /** Gen 29: cached so compound-goal sub-tabs inherit the macro catalog. */
265
+ /** Cached so compound-goal sub-tabs inherit the macro catalog. */
267
266
  macroPromptBlock;
268
267
  constructor(options) {
269
268
  this.driver = options.driver;
@@ -292,8 +291,7 @@ export class BrowserAgent {
292
291
  this.runRegistry = options.runRegistry;
293
292
  }
294
293
  async run(scenario) {
295
- // Gen 21: parallel tab execution for compound goals.
296
- // Pre-flight: check if the goal should be decomposed into parallel sub-goals.
294
+ // Pre-flight compound goals into parallel sub-goals when enabled.
297
295
  if (this.config.parallelTabs?.enabled && scenario.goal && scenario.startUrl) {
298
296
  const context = this.driver.getPage?.()?.context();
299
297
  if (context) {
@@ -307,8 +305,7 @@ export class BrowserAgent {
307
305
  const { runParallel } = await import('./parallel-runner.js');
308
306
  // Inherit the top-level macro catalog + driver's macro registry so
309
307
  // sub-tab agents see the same capability surface. Without this the
310
- // sub-agents emit macro actions (their brain still has the block)
311
- // into a driver that rejects them — a silent failure mode.
308
+ // sub-agents emit macro actions into a driver that rejects them.
312
309
  const topDriverOptions = this.driver.getDriverOptions?.();
313
310
  const result = await runParallel({
314
311
  context,
@@ -330,24 +327,29 @@ export class BrowserAgent {
330
327
  }
331
328
  }
332
329
  }
333
- // Gen 14: vision mode gets more turns each turn takes ~15s (screenshot
334
- // encode + image tokens) vs ~5s for DOM-first. Without the boost, vision
335
- // runs out of turns before completing multi-step tasks.
330
+ // Vision and hybrid modes get more turns because screenshot turns are
331
+ // slower and multi-step tasks need more wall-clock budget.
336
332
  const isVisionMode = this.config.observationMode === 'vision' || this.config.observationMode === 'hybrid';
337
333
  const baseMaxTurns = scenario.maxTurns || DEFAULT_MAX_TURNS;
338
- // Gen 26: 30 turn minimum for vision. 15/51 failures were turn budget
339
- // exhaustion at 20. The cost cap (200k tokens) is the real bound.
340
- const maxTurns = isVisionMode ? Math.max(baseMaxTurns, 30) : baseMaxTurns;
334
+ // maxTurns is mutable because active runs can receive a one-time extension
335
+ // when they reach the configured cap while still making page progress.
336
+ let maxTurns = isVisionMode ? Math.max(baseMaxTurns, 30) : baseMaxTurns;
337
+ let extensionGranted = false;
338
+ const EXTENSION_TURNS_GRANTED = 5;
339
+ const EXTENSION_HARD_CAP = 25;
340
+ const EXTENSION_PROGRESS_LOOKBACK = 3;
341
341
  const retries = this.config.retries ?? DEFAULT_RETRIES;
342
342
  const retryDelayMs = this.config.retryDelayMs ?? DEFAULT_RETRY_DELAY_MS;
343
343
  const turns = [];
344
344
  const startTime = Date.now();
345
345
  const phaseTimings = {};
346
- // Gen 27: vision+planner mode gets token budget (300k). Gen 26 showed
347
- // 4 cost_cap failures and 18 turn-exhausted tasks (now getting 30 turns
348
- // but hitting 200k cap). The timeout (600s) is the real safety net.
346
+ // Vision modes get a larger token budget; the wall-clock timeout remains
347
+ // the outer safety bound.
349
348
  const visionBudgetMultiplier = isVisionMode ? 3 : 1;
350
349
  const runState = new RunState(maxTurns, Math.round(DEFAULT_TOKEN_BUDGET * visionBudgetMultiplier));
350
+ const directStart = deriveDirectStartUrl(scenario);
351
+ let googleFlightsPreflight;
352
+ const initialStartUrl = directStart?.url || scenario.startUrl;
351
353
  const runId = scenario.sessionId
352
354
  ? `${scenario.sessionId}_${Date.now()}`
353
355
  : RunRegistry.generateRunId();
@@ -395,7 +397,7 @@ export class BrowserAgent {
395
397
  return agentResult;
396
398
  };
397
399
  // Wrap onTurn to include mid-run manifest updates (every 3 turns) and to
398
- // accumulate per-turn token usage for the Gen 10 cost cap.
400
+ // accumulate per-turn token usage for the cost cap.
399
401
  const originalOnTurn = this.onTurn;
400
402
  this.onTurn = (turn) => {
401
403
  originalOnTurn?.(turn);
@@ -432,20 +434,47 @@ export class BrowserAgent {
432
434
  // Start navigation and load memory in parallel. Navigation is async (network
433
435
  // I/O) while memory init is sync (readFileSync), so memory completes while
434
436
  // the network request is in flight — saving the serial cost of disk reads.
435
- if (scenario.startUrl) {
437
+ if (initialStartUrl) {
436
438
  const navigateStartedAt = Date.now();
437
- const navPromise = withRetry(() => this.driver.execute({ action: 'navigate', url: scenario.startUrl }), retries, retryDelayMs, undefined, scenario.signal);
439
+ const navPromise = withRetry(() => this.driver.execute({ action: 'navigate', url: initialStartUrl }), retries, retryDelayMs, undefined, scenario.signal);
438
440
  // Load domain-scoped memory while navigation is in progress
439
441
  if (this.projectStore) {
440
- this.knowledge = new AppKnowledge(this.projectStore.getKnowledgePath(scenario.startUrl), scenario.startUrl);
441
- this.selectorCache = new SelectorCache(this.projectStore.getSelectorCachePath(scenario.startUrl));
442
+ const memoryStartUrl = scenario.startUrl || initialStartUrl;
443
+ this.knowledge = new AppKnowledge(this.projectStore.getKnowledgePath(memoryStartUrl), memoryStartUrl);
444
+ this.selectorCache = new SelectorCache(this.projectStore.getSelectorCachePath(memoryStartUrl));
442
445
  }
443
446
  await navPromise;
444
447
  phaseTimings.initialNavigateMs = Date.now() - navigateStartedAt;
445
448
  this.onPhaseTiming?.('navigate', phaseTimings.initialNavigateMs);
446
- // REVERTED: page warm-up delay caused pre-first-turn timeouts on Google
447
- // Flights (0-turn failures at 600s) and click timeouts on Allrecipes.
448
- // DataDome bypass needs a different approach — not blocking the main loop.
449
+ googleFlightsPreflight = await prepareGoogleFlightsSearch(this.driver.getPage?.(), scenario, { timeoutMs: 45_000 });
450
+ if (googleFlightsPreflight?.blockingReason) {
451
+ const state = await this.driver.observe().catch(() => ({
452
+ url: this.driver.getUrl?.() || googleFlightsPreflight?.finalUrl || initialStartUrl,
453
+ title: 'Google Flights',
454
+ snapshot: googleFlightsPreflight?.blockingReason || 'Google Flights preflight blocked the requested search.',
455
+ }));
456
+ const reason = googleFlightsPreflight.blockingReason;
457
+ turns.push({
458
+ turn: 1,
459
+ state,
460
+ action: { action: 'abort', reason },
461
+ reasoning: 'Google Flights preflight detected a first-party blocking state before LLM control.',
462
+ expectedEffect: 'Run exits without substituting adjacent dates for an unavailable requested date.',
463
+ durationMs: Date.now() - navigateStartedAt,
464
+ });
465
+ return buildResult({
466
+ success: false,
467
+ reason,
468
+ turns,
469
+ totalMs: Date.now() - startTime,
470
+ goalVerification: {
471
+ achieved: false,
472
+ confidence: 0.95,
473
+ evidence: [reason, `URL: ${state.url}`],
474
+ missing: ['The exact requested Google Flights date could not be searched in the live site state.'],
475
+ },
476
+ });
477
+ }
449
478
  }
450
479
  // Don't wait on warmup before entering the loop — it races against the
451
480
  // first observe and decode. Make sure any unhandled rejection is silenced.
@@ -461,7 +490,7 @@ export class BrowserAgent {
461
490
  });
462
491
  const supervisorConfig = {
463
492
  enabled: this.config.supervisor?.enabled ?? DEFAULT_SUPERVISOR.enabled,
464
- // Gen 28: models.supervisor overrides supervisor.model, falls back to main
493
+ // Per-role supervisor model overrides the supervisor default, then falls back to main.
465
494
  model: this.config.models?.supervisor?.model || this.config.supervisor?.model || this.config.model || 'gpt-5.4',
466
495
  provider: (this.config.models?.supervisor?.provider || this.config.supervisor?.provider || this.config.provider || 'openai'),
467
496
  useVision: this.config.supervisor?.useVision ?? DEFAULT_SUPERVISOR.useVision,
@@ -470,24 +499,17 @@ export class BrowserAgent {
470
499
  maxInterventions: this.config.supervisor?.maxInterventions ?? DEFAULT_SUPERVISOR.maxInterventions,
471
500
  hardStallWindow: this.config.supervisor?.hardStallWindow ?? DEFAULT_SUPERVISOR.hardStallWindow,
472
501
  };
473
- // Gen 7 / 7.1: planner-first path. When `plannerEnabled: true` (and not
474
- // disabled via BAD_PLANNER=0), make a single LLM call to generate a
475
- // plan, then execute it deterministically.
476
- //
477
- // Gen 7.1 (replan-on-deviation): when a plan deviates, instead of
478
- // immediately falling through to the per-action loop, call Brain.plan()
479
- // AGAIN with the current page state and a deviation context. Cap at
480
- // `maxReplans` total replan attempts (= initial plan + maxReplans
481
- // additional plan calls). The system prompt is byte-stable so prompt
482
- // cache still hits — only the user message carries the deviation
483
- // history. On exhaustion, fall through to the per-action loop with a
484
- // [REPLAN] hint, exactly like Gen 7 did.
502
+ // Planner-first path: make one LLM call to generate a plan, then execute
503
+ // it deterministically. On deviation, replan from the current page state
504
+ // with deviation context; after the retry budget is exhausted, fall back
505
+ // to the per-action loop with a [REPLAN] hint.
485
506
  //
486
507
  // Plan execution writes to the same `turns` array, so post-run analysis
487
508
  // sees a unified timeline regardless of which path completed the run.
488
509
  let planFallbackContext = '';
489
510
  let plannerStartTurn = 0;
490
- const plannerEnabled = this.config.plannerEnabled === true && process.env.BAD_PLANNER !== '0';
511
+ const plannerEnabled = this.config.plannerEnabled === true && process.env.BAD_PLANNER !== '0'
512
+ && shouldUsePlannerForScenario(scenario, this.config.plannerMode ?? 'always');
491
513
  const maxReplans = 3;
492
514
  if (plannerEnabled && scenario.startUrl) {
493
515
  // Need an initial observe so the planner has something to look at.
@@ -495,11 +517,7 @@ export class BrowserAgent {
495
517
  // primes the planner. The result is also stashed as cachedPostState
496
518
  // so the per-action fallback's first observe is short-circuited.
497
519
  //
498
- // Gen 8: on real-web tasks (planner-on-realweb config), wait for
499
- // the page to settle BEFORE the planner observes. SPA pages like
500
- // npmjs.com load their data via JS after DOMContentLoaded — without
501
- // a settle wait the planner snapshots a half-loaded page and emits
502
- // runScript queries against selectors that don't exist yet.
520
+ // Give dynamic pages time to populate before the planner observes.
503
521
  const settleMs = this.config.initialObserveSettleMs ?? 0;
504
522
  if (settleMs > 0) {
505
523
  const page = this.driver.getPage?.();
@@ -513,7 +531,7 @@ export class BrowserAgent {
513
531
  await new Promise((resolve) => setTimeout(resolve, settleMs));
514
532
  }
515
533
  if (this.config.debug) {
516
- console.log(`[Runner] Gen 8 initial settle: waited ${settleMs}ms (or networkidle) before planner observe`);
534
+ console.log(`[Runner] Initial settle: waited ${settleMs}ms (or networkidle) before planner observe`);
517
535
  }
518
536
  }
519
537
  const initialState = await this.driver.observe().catch(() => undefined);
@@ -633,10 +651,8 @@ export class BrowserAgent {
633
651
  totalMs: Date.now() - startTime,
634
652
  });
635
653
  }
636
- // All replan attempts (or initial plan) deviated. Fall through to
637
- // the per-action loop with a [REPLAN] hint that names the final
638
- // deviation. The per-action loop with Gen 6.1 batch detection will
639
- // finish the work.
654
+ // All replan attempts deviated. Fall through to the per-action loop
655
+ // with a [REPLAN] hint that names the final deviation.
640
656
  if (lastDeviationReason) {
641
657
  plannerStartTurn = cumulativeTurnsConsumed;
642
658
  planFallbackContext = `\n[REPLAN] After ${attempt} planner attempt${attempt === 1 ? '' : 's'} (1 initial + ${attempt - 1} replan${attempt === 2 ? '' : 's'}), the planner could not produce a working plan. Final deviation: ${lastDeviationReason}\nThe runner has fallen back to per-action mode. Continue toward the original goal from the current page state.\n`;
@@ -654,13 +670,31 @@ export class BrowserAgent {
654
670
  }
655
671
  }
656
672
  }
657
- // Gen 32 — overlay narration tracker. Per-session; accumulates verdict
658
- // markers so a ledger the agent keeps re-emitting doesn't spam badges.
673
+ // Per-session overlay narration tracker; suppresses duplicate verdict badges.
659
674
  const verdictTracker = new VerdictTracker();
660
675
  for (let i = 1 + plannerStartTurn; i <= maxTurns; i++) {
661
- // Gen 32 honor user-driven pause from the interrupt controller.
662
- // Blocks until `r` is pressed (resume) or `q` is pressed (abort).
663
- // A rejected beforeTurn is treated as an abort.
676
+ // Grant a one-time max-turns extension when the run reaches its cap
677
+ // while still making recent progress.
678
+ if (i === maxTurns
679
+ && !extensionGranted
680
+ && !isVisionMode
681
+ && maxTurns < EXTENSION_HARD_CAP
682
+ && runState.lastProgressTurn >= maxTurns - EXTENSION_PROGRESS_LOOKBACK) {
683
+ const extendedMax = Math.min(maxTurns + EXTENSION_TURNS_GRANTED, EXTENSION_HARD_CAP);
684
+ const extra = extendedMax - maxTurns;
685
+ if (extra > 0) {
686
+ this.bus.emitNow({
687
+ type: 'recovery-fired',
688
+ runId,
689
+ turn: i,
690
+ strategy: 'max-turns-extension',
691
+ feedback: `Granted +${extra} extra turns (cap ${maxTurns} → ${extendedMax}); progress at turn ${runState.lastProgressTurn}.`,
692
+ });
693
+ maxTurns = extendedMax;
694
+ extensionGranted = true;
695
+ }
696
+ }
697
+ // Honor user-driven pause or abort from the interrupt controller.
664
698
  if (this.beforeTurn) {
665
699
  try {
666
700
  await this.beforeTurn(i);
@@ -682,10 +716,8 @@ export class BrowserAgent {
682
716
  totalMs: Date.now() - startTime,
683
717
  });
684
718
  }
685
- // Gen 10: hard cost cap. Stops the per-action loop from burning unbounded
686
- // tokens on cases where recovery isn't converging (the Gen 9 death-spiral
687
- // failure mode where reddit hit $0.32 / 173K tokens). The cap is enforced
688
- // BEFORE the next LLM call so the case aborts cleanly with a reason.
719
+ // Enforce the token budget before the next LLM call so stalled recovery
720
+ // loops abort cleanly with a reason.
689
721
  if (runState.isTokenBudgetExhausted) {
690
722
  return buildResult({
691
723
  success: false,
@@ -698,10 +730,7 @@ export class BrowserAgent {
698
730
  this.bus.emitNow({ type: 'turn-started', runId, turn: i });
699
731
  try {
700
732
  // -- 1. Check for recovery before observing --
701
- // Only run analyzeRecovery when there's a non-zero error trail. Used
702
- // to run unconditionally; lazy-skipping it when there are no recent
703
- // errors avoids the per-turn cost on the happy path. (Gen 5 lazy
704
- // decision graph computation, change #20 in the pursuit spec.)
733
+ // Only run analyzeRecovery when recent turns show errors.
705
734
  const hasErrorTrail = turns.length >= 2
706
735
  && (runState.consecutiveErrors > 0
707
736
  || turns.slice(-5).some((t) => t.error || t.verified === false));
@@ -803,12 +832,29 @@ export class BrowserAgent {
803
832
  ...(screenshotDataUrl ? { screenshot: screenshotDataUrl } : {}),
804
833
  durationMs: observeDurationMs,
805
834
  });
835
+ // Track page progress for the one-time max-turns extension. URL
836
+ // changes or >5% snapshot-size movement count as progress.
837
+ const priorTurn = turns[turns.length - 1];
838
+ if (priorTurn) {
839
+ const urlChanged = priorTurn.state?.url !== state.url;
840
+ const priorSize = priorTurn.state?.snapshot?.length ?? 0;
841
+ const sizeDelta = priorSize > 0
842
+ ? Math.abs(state.snapshot.length - priorSize) / priorSize
843
+ : 1; // first comparison counts as change
844
+ if (urlChanged || sizeDelta > 0.05) {
845
+ runState.lastProgressTurn = i;
846
+ }
847
+ }
848
+ else {
849
+ // No prior turn — this is the first observe. Count as progress.
850
+ runState.lastProgressTurn = i;
851
+ }
806
852
  // Auto-navigate: if we're on about:blank with a startUrl, navigate without
807
853
  // consuming an LLM turn. The agent always does wait->navigate on blank pages.
808
854
  if (state.url === 'about:blank' &&
809
- scenario.startUrl &&
855
+ initialStartUrl &&
810
856
  turns.length === 0) {
811
- await this.driver.execute({ action: 'navigate', url: scenario.startUrl }).catch(() => { });
857
+ await this.driver.execute({ action: 'navigate', url: initialStartUrl }).catch(() => { });
812
858
  // Re-observe after navigation
813
859
  const reState = await withRetry(() => this.driver.observe(), retries, retryDelayMs, undefined, scenario.signal);
814
860
  Object.assign(state, reState);
@@ -893,16 +939,8 @@ export class BrowserAgent {
893
939
  ctxBudget.add('blocker-recovery', '\nDialog dismissed but URL unchanged — prior action may have been voided. Re-submit if needed.\n', 90);
894
940
  }
895
941
  }
896
- // Gen 6.1: Mandatory batch fill detection.
897
- //
898
- // If the agent has done 3+ consecutive single-step `type` actions on
899
- // the same URL (i.e., it's filling a multi-field form one input at a
900
- // time), inject a high-priority hint into extraContext that DEMANDS
901
- // the next action be a `fill` covering the remaining fields.
902
- //
903
- // This is the runner-side enforcement layer for Gen 6 batch verbs.
904
- // Prompt rules alone (Gen 6) didn't reliably steer the agent toward
905
- // batch fill — runtime feedback does.
942
+ // Enforce batch fill when the agent starts filling a multi-field form
943
+ // one input at a time.
906
944
  const batchFillHint = detectBatchFillOpportunity(turns, state);
907
945
  if (batchFillHint && process.env.BAD_BATCH_HINT !== '0') {
908
946
  ctxBudget.add('mandatory-batch-fill', batchFillHint, 100);
@@ -962,9 +1000,7 @@ export class BrowserAgent {
962
1000
  if (searchScoutFeedback) {
963
1001
  ctxBudget.add('search-scout', `\n${searchScoutFeedback}\n`, 50);
964
1002
  }
965
- // Lazy supervisor signal: only compute when supervisor is enabled
966
- // AND we're past the minimum-turns gate. Used to run unconditionally
967
- // every turn even when supervisor was disabled. Gen 5 evolve round 1.
1003
+ // Compute supervisor signals only after the supervisor is eligible.
968
1004
  const supervisorEligible = supervisorConfig.enabled &&
969
1005
  i >= supervisorConfig.minTurnsBeforeInvoke &&
970
1006
  runState.supervisorInterventions < supervisorConfig.maxInterventions &&
@@ -1095,6 +1131,19 @@ export class BrowserAgent {
1095
1131
  if (this.config.walletMode && i === 1) {
1096
1132
  ctxBudget.add('wallet-defi-context', DEFI_BRAIN_CONTEXT, 35);
1097
1133
  }
1134
+ if (directStart && i === 1) {
1135
+ ctxBudget.add('direct-start', `\nDIRECT START: The initial URL was intentionally prepared by the ${directStart.profile} site profile (${directStart.reason}). If the URL uses future-equivalent Booking dates, keep those live bookable dates unless the page itself rejects them; do not navigate back to stale past dates.\n`, 82);
1136
+ }
1137
+ if (googleFlightsPreflight && i === 1) {
1138
+ const { spec } = googleFlightsPreflight;
1139
+ const rollText = spec.dateRoll
1140
+ ? ` The stale benchmark date(s) were intentionally rolled from ${spec.dateRoll.originalDepartureDate}${spec.dateRoll.originalReturnDate ? `..${spec.dateRoll.originalReturnDate}` : ''} to live Google Flights dates ${spec.dateRoll.departureDate}${spec.dateRoll.returnDate ? `..${spec.dateRoll.returnDate}` : ''}; keep those live dates unless Google rejects them.`
1141
+ : '';
1142
+ const comparisonText = /\b(non-?stop|fewest stops?|least number of stops?|shortest|duration|total travel time)\b/i.test(scenario.goal)
1143
+ ? ' For non-stop, fewest-stop, shortest-duration, or travel-time comparison tasks, first use the visible result cards already on the prepared results page; they include airline, price, stops, and duration. Do not open Price graph, Date grid, or broad filter panels unless the visible cards do not contain the requested comparison evidence.'
1144
+ : '';
1145
+ ctxBudget.add('google-flights-preflight', `\nGOOGLE FLIGHTS PREFLIGHT: ${googleFlightsPreflight.prepared ? 'The runner already prepared the search page' : 'The runner attempted to prepare the search page'} for ${spec.origin} to ${spec.destination}, ${spec.tripType}, ${spec.departureDate}${spec.returnDate ? ` to ${spec.returnDate}` : ''}. ${googleFlightsPreflight.reason}.${rollText} Your job now is to extract concrete visible Google Flights evidence from the current page; do not restart the form unless the prepared state is visibly wrong.${comparisonText} If Google says the exact requested live date is unavailable, too far in the future, or returns no results, abort with that evidence instead of substituting adjacent dates.\n`, 84);
1146
+ }
1098
1147
  if (this.referenceTrajectory) {
1099
1148
  ctxBudget.add('reference-trajectory', `\nREFERENCE TRAJECTORY — A similar task was completed before:\n${this.referenceTrajectory}\nUse this as a guide, but adapt to the current page state.\n`, 40);
1100
1149
  }
@@ -1121,12 +1170,8 @@ export class BrowserAgent {
1121
1170
  if (lastTurn?.action.action === 'runScript' && !lastTurn.error) {
1122
1171
  ctxBudget.add('extraction-guard', '\nData extracted. If it answers the goal, complete now.\n', 80);
1123
1172
  }
1124
- // REVERTED: form stall DDG/external search fallback. Caused the agent
1125
- // to navigate to Priceline, Expedia, DuckDuckGo which all block with
1126
- // anti-bot. Worse than staying on the original site and grinding.
1127
- // The stall detection idea is sound but the fallback destination is wrong.
1128
- // TODO: revisit with a same-site strategy (runScript extraction, URL
1129
- // construction from current state) instead of cross-site navigation.
1173
+ // Cross-site fallbacks are intentionally disabled here; stay on the
1174
+ // original site and recover with same-site extraction/navigation.
1130
1175
  {
1131
1176
  }
1132
1177
  const extraContext = ctxBudget.build();
@@ -1148,10 +1193,8 @@ export class BrowserAgent {
1148
1193
  const aiTangleOutputContext = aiTangleOutputCompletion
1149
1194
  ? `\nVERIFIED OUTPUT STATE DETECTED:\n${aiTangleOutputCompletion.feedback}\nReturn a terminal \`complete\` action now with concrete evidence.\n`
1150
1195
  : '';
1151
- // Gen 7: include the plan fallback hint on the FIRST per-action turn
1152
- // after a plan deviation. The hint tells the LLM what failed and from
1153
- // what point to recover. We only inject it once (consume it after
1154
- // first use) so it doesn't pollute every subsequent turn.
1196
+ // Include the plan fallback hint on the first per-action turn after
1197
+ // a plan deviation, then consume it.
1155
1198
  const planFallbackHint = planFallbackContext;
1156
1199
  if (planFallbackContext)
1157
1200
  planFallbackContext = '';
@@ -1240,10 +1283,6 @@ export class BrowserAgent {
1240
1283
  }
1241
1284
  }
1242
1285
  else {
1243
- // REVERTED: micro-movements during LLM thinking caused interference
1244
- // with page state on interactive sites. The mouse.move calls during
1245
- // decide() could trigger hover states, tooltips, or dismiss elements
1246
- // the agent was about to click.
1247
1286
  decision = await withRetry(() => this.brain.decide(scenario.goal, decisionState, finalExtraContext || undefined, { current: i, max: maxTurns }, { forceVision }), retries, retryDelayMs, (attempt, err) => {
1248
1287
  if (this.config.debug) {
1249
1288
  console.log(`[Runner] LLM retry ${attempt}: ${err.message}`);
@@ -1296,9 +1335,8 @@ export class BrowserAgent {
1296
1335
  durationMs: decideDurationMs,
1297
1336
  });
1298
1337
  }
1299
- // -- 4a. Gen 32 — narrate to the cursor overlay. Fire-and-forget.
1300
- // Four signals pushed to the page-context overlay so a viewer of
1301
- // the recorded video can READ the agent's work, not just watch it:
1338
+ // -- 4a. Narrate to the cursor overlay. Fire-and-forget.
1339
+ // Signals pushed to the page-context overlay:
1302
1340
  // 1. Reasoning panel (top-right) — the agent's own text
1303
1341
  // 2. Progress bar + chip (top) — turn N with optional ledger marker
1304
1342
  // 3. Verdict badges (bottom-left) — POSITIVE/CLEARED/REVIEW events
@@ -1476,13 +1514,11 @@ export class BrowserAgent {
1476
1514
  this.onTurn?.(turn);
1477
1515
  continue;
1478
1516
  }
1479
- // -- 5d. Handle extractWithIndex action (Gen 10) --
1517
+ // -- 5d. Handle extractWithIndex action --
1480
1518
  // Returns a numbered list of every visible element matching `query`,
1481
1519
  // each with its tag, textContent, key attributes, and a stable
1482
1520
  // selector. The agent picks elements by index in the next turn.
1483
- // This is the Gen 10 capability change: pick-by-content instead of
1484
- // pick-by-selector. Works on data the planner couldn't see at plan
1485
- // time (XHR-loaded content, dl/dt/dd, deeply-nested wrappers).
1521
+ // Pick by visible content instead of brittle selectors.
1486
1522
  if (action.action === 'extractWithIndex') {
1487
1523
  const page = this.driver.getPage?.();
1488
1524
  if (page) {
@@ -1509,11 +1545,9 @@ export class BrowserAgent {
1509
1545
  this.onTurn?.(turn);
1510
1546
  continue;
1511
1547
  }
1512
- // -- Gen 33: mid-run parallel fan-out. Agent picked N candidates
1513
- // from the current page and wants to explore each in its own tab.
1514
- // We spawn sub-agents in the same BrowserContext, collect their
1515
- // verdicts, and inject the merged result as feedback for the next
1516
- // turn. Parent page state is untouched.
1548
+ // -- Mid-run parallel fan-out --
1549
+ // Explore independent candidates in sibling tabs, then inject the
1550
+ // merged result as feedback for the next turn.
1517
1551
  if (action.action === 'fanOut') {
1518
1552
  const page = this.driver.getPage?.();
1519
1553
  const context = page?.context();
@@ -1530,10 +1564,8 @@ export class BrowserAgent {
1530
1564
  context,
1531
1565
  config: this.config,
1532
1566
  currentUrl: state.url,
1533
- // Gen 34 give the executor a handle on the parent driver so
1534
- // it can drive the Hydra overlay (grid init, live thumbnail
1535
- // streaming, verdict chips, collapse animation) on the
1536
- // parent page for the whole fan-out duration.
1567
+ // Parent driver lets the executor drive the fan-out overlay for
1568
+ // the whole fan-out duration.
1537
1569
  parentDriver: this.driver,
1538
1570
  ...(topDriverOptions
1539
1571
  ? { driverOptions: (() => { const { showCursor: _sc, ...rest } = topDriverOptions; return rest; })() }
@@ -1578,20 +1610,14 @@ export class BrowserAgent {
1578
1610
  // (>50 chars) combined with script-extracted evidence means the
1579
1611
  // verifier almost always agrees — save the round-trip.
1580
1612
  //
1581
- // Gen 12: content-aware gate. gpt-5.4 writes verbose narratives
1582
- // that admit failure ("could not complete", "not visible", "did
1583
- // not take effect") yet marks success. The old heuristic (length
1584
- // + evidence + no errors) rubber-stamped these. Now we scan the
1585
- // result text for self-contradicting phrases and force LLM
1586
- // verification when found. This fixes the 6/8 judge disagreement
1587
- // cases from Gen 11 evolve R2.
1613
+ // Content-aware gate: self-contradicting completion text forces
1614
+ // LLM verification instead of using the fast path.
1588
1615
  const agentResult = action.result || '';
1589
1616
  const recentErrors = turns.slice(-2).filter(t => t.error).length;
1590
1617
  const hasScriptEvidence = verificationEvidence.some(e => e.startsWith('SCRIPT RESULT:'));
1591
- // Content-aware gate: detect when the agent's own text admits
1592
- // failure despite claiming success. These phrases were found in
1593
- // 6 of 8 false-pass cases on WebVoyager with gpt-5.4.
1594
- const selfContradicting = /\b(?:could not (?:complete|find|fulfill|verify|confirm|locate|access|extract|retrieve)|not (?:visible|available|found|present|accessible|displayed|shown|confirmed|verified)|did not (?:take effect|work|succeed|load|return)|unable to (?:find|complete|verify|access|extract|retrieve)|no (?:visible (?:answer|result|data|content)|results? (?:found|returned|available))|(?:failed|failure) to (?:find|complete|set|select|navigate)|unfortunately|I (?:was|am) unable|(?:task|request|goal) (?:is|was) (?:not |in)complete)\b/i.test(agentResult);
1618
+ // Detect when the agent's own text admits failure despite
1619
+ // claiming success.
1620
+ const selfContradicting = containsSelfContradictingCompletion(agentResult);
1595
1621
  const fastPathEligible = agentResult.length > 50 &&
1596
1622
  recentErrors === 0 &&
1597
1623
  hasScriptEvidence &&
@@ -1612,7 +1638,7 @@ export class BrowserAgent {
1612
1638
  // own text suggests failure. The LLM verifier reads the actual
1613
1639
  // content and makes the right call.
1614
1640
  if (this.config.debug) {
1615
- console.log('[Runner] Gen 12: fast-path BLOCKED agent result contains self-contradicting language, forcing LLM verification');
1641
+ console.log('[Runner] Fast-path blocked: agent result contains self-contradicting language, forcing LLM verification');
1616
1642
  }
1617
1643
  goalResult = await this.brain.verifyGoalCompletion(state, scenario.goal, buildGoalVerificationClaim(agentResult, verificationEvidence));
1618
1644
  }
@@ -1622,7 +1648,19 @@ export class BrowserAgent {
1622
1648
  if (this.config.debug) {
1623
1649
  console.log(`[Runner] Goal verification: achieved=${goalResult.achieved}, confidence=${goalResult.confidence}`);
1624
1650
  }
1651
+ if (selfContradicting && goalResult.achieved) {
1652
+ goalResult = {
1653
+ achieved: false,
1654
+ confidence: Math.min(goalResult.confidence, 0.4),
1655
+ evidence: goalResult.evidence,
1656
+ missing: [
1657
+ ...goalResult.missing,
1658
+ 'Rejected because the completion text itself admits that the exact requested task was not completed.',
1659
+ ],
1660
+ };
1661
+ }
1625
1662
  if (!goalResult.achieved
1663
+ && !selfContradicting
1626
1664
  && shouldAcceptFirstPartyBoundaryCompletion(scenario.goal, state.url, goalResult, action.result || '')) {
1627
1665
  goalResult = {
1628
1666
  ...goalResult,
@@ -1636,6 +1674,7 @@ export class BrowserAgent {
1636
1674
  };
1637
1675
  }
1638
1676
  if (!goalResult.achieved
1677
+ && !selfContradicting
1639
1678
  && shouldAcceptSearchWorkflowCompletion(scenario.goal, goalResult, action.result || '', verificationEvidence)) {
1640
1679
  goalResult = {
1641
1680
  ...goalResult,
@@ -1649,6 +1688,7 @@ export class BrowserAgent {
1649
1688
  };
1650
1689
  }
1651
1690
  if (!goalResult.achieved
1691
+ && !selfContradicting
1652
1692
  && shouldAcceptScriptBackedCompletion(scenario.goal, state, goalResult, action.result || '', verificationEvidence)) {
1653
1693
  goalResult = {
1654
1694
  ...goalResult,
@@ -1661,6 +1701,34 @@ export class BrowserAgent {
1661
1701
  missing: [],
1662
1702
  };
1663
1703
  }
1704
+ if (!goalResult.achieved
1705
+ && !selfContradicting
1706
+ && shouldAcceptRolledBookingCompletion(directStart, goalResult, action.result || '', state)) {
1707
+ goalResult = {
1708
+ ...goalResult,
1709
+ achieved: true,
1710
+ confidence: Math.max(goalResult.confidence, 0.82),
1711
+ evidence: [
1712
+ ...goalResult.evidence,
1713
+ 'Accepted under Booking direct-start date-roll policy: the stale benchmark dates were mapped to equivalent live bookable dates and the completion included concrete Booking evidence.',
1714
+ ],
1715
+ missing: [],
1716
+ };
1717
+ }
1718
+ if (!goalResult.achieved
1719
+ && !selfContradicting
1720
+ && shouldAcceptRolledGoogleFlightsCompletion(googleFlightsPreflight, goalResult, action.result || '', state)) {
1721
+ goalResult = {
1722
+ ...goalResult,
1723
+ achieved: true,
1724
+ confidence: Math.max(goalResult.confidence, 0.82),
1725
+ evidence: [
1726
+ ...goalResult.evidence,
1727
+ 'Accepted under Google Flights preflight date-roll policy: stale benchmark flight dates were mapped to equivalent live Google Flights dates and the completion included concrete flight-result evidence.',
1728
+ ],
1729
+ missing: [],
1730
+ };
1731
+ }
1664
1732
  const contentTypeMismatch = detectCompletionContentTypeMismatch(scenario.goal, state, action.result || '', verificationEvidence);
1665
1733
  if (contentTypeMismatch) {
1666
1734
  runState.verificationRejectionCount++;
@@ -1680,9 +1748,9 @@ export class BrowserAgent {
1680
1748
  // Tier C: 3+ rejections + confidence ≥0.40 → accept (prevent total turn exhaustion)
1681
1749
  const hasSupplementalEvidence = verificationEvidence.length > 0;
1682
1750
  const priorRejections = runState.verificationRejectionCount;
1683
- const shouldAccept = (priorRejections >= 1 && goalResult.confidence >= 0.55 && hasSupplementalEvidence) ||
1751
+ const shouldAccept = !selfContradicting && ((priorRejections >= 1 && goalResult.confidence >= 0.55 && hasSupplementalEvidence) ||
1684
1752
  (priorRejections >= 2 && goalResult.confidence >= 0.50) ||
1685
- (priorRejections >= 3 && goalResult.confidence >= 0.40);
1753
+ (priorRejections >= 3 && goalResult.confidence >= 0.40));
1686
1754
  if (shouldAccept) {
1687
1755
  goalResult = {
1688
1756
  ...goalResult,
@@ -1700,9 +1768,8 @@ export class BrowserAgent {
1700
1768
  runState.verificationRejectionCount++;
1701
1769
  turn.verificationFailure = goalResult.missing.join('; ') || 'Goal verification failed';
1702
1770
  runState.firstSufficientEvidenceTurn ??= i;
1703
- // Gen 24b: checkpoint replay on 2nd rejection. Navigate back
1704
- // to a previous page where the agent had correct data, instead
1705
- // of continuing from the wrong-path state.
1771
+ // On the second rejection, roll back to a previous checkpoint
1772
+ // before trying a different path.
1706
1773
  let replayNote = '';
1707
1774
  if (runState.verificationRejectionCount === 2 && runState.checkpoints.length >= 2) {
1708
1775
  // Go back to the second-to-last checkpoint (before the wrong path)
@@ -1715,7 +1782,7 @@ export class BrowserAgent {
1715
1782
  catch { /* rollback failed, continue from current state */ }
1716
1783
  }
1717
1784
  }
1718
- // Gen 19: progressive strategy-shift escalation on rejection.
1785
+ // Escalate recovery guidance after repeated verification rejects.
1719
1786
  let escalation;
1720
1787
  if (runState.verificationRejectionCount >= 3) {
1721
1788
  escalation = ' STRATEGY SHIFT REQUIRED: Your previous approaches have failed 3 times. Try a COMPLETELY different method: use navigate to go to a different search engine or URL, try extractWithIndex instead of runScript, or scroll to look for the data in a different part of the page. Do NOT repeat what you just tried.';
@@ -1899,7 +1966,7 @@ export class BrowserAgent {
1899
1966
  else {
1900
1967
  runState.clearConsecutiveErrors();
1901
1968
  executeTimeoutRecoveries = 0; // Reset on successful action
1902
- // Gen 27: surface form reset warnings from batch fill verification
1969
+ // Surface form reset warnings from batch fill verification.
1903
1970
  if ('warning' in execResult && typeof execResult.warning === 'string') {
1904
1971
  const warning = execResult.warning;
1905
1972
  this.brain.injectFeedback(warning);
@@ -1907,8 +1974,7 @@ export class BrowserAgent {
1907
1974
  console.log(`[Runner] Fill warning: ${warning}`);
1908
1975
  }
1909
1976
  }
1910
- // Gen 24b: save checkpoint when URL changes after successful action.
1911
- // These are rollback points for wrong-path recovery.
1977
+ // Save URL checkpoints after successful navigation for rollback.
1912
1978
  const postUrl = this.driver.getPage?.()?.url() || '';
1913
1979
  const lastCheckpointUrl = runState.checkpoints[runState.checkpoints.length - 1]?.url;
1914
1980
  if (postUrl && postUrl !== 'about:blank' && postUrl !== lastCheckpointUrl) {
@@ -2076,10 +2142,15 @@ export class BrowserAgent {
2076
2142
  }
2077
2143
  }
2078
2144
  }
2079
- // Max turns reached
2145
+ // Max turns reached. The adaptive extension fired (or didn't) inside
2146
+ // the loop body — see the `extensionGranted` check at the start of
2147
+ // each iteration. By the time we reach this point, the (possibly
2148
+ // extended) cap was exhausted.
2080
2149
  return buildResult({
2081
2150
  success: false,
2082
- reason: `Max turns (${maxTurns}) reached`,
2151
+ reason: extensionGranted
2152
+ ? `Max turns (${maxTurns}) reached after +${EXTENSION_TURNS_GRANTED} extension; recent progress at turn ${runState.lastProgressTurn}.`
2153
+ : `Max turns (${maxTurns}) reached`,
2083
2154
  turns,
2084
2155
  totalMs: Date.now() - startTime,
2085
2156
  });
@@ -2112,9 +2183,7 @@ export class BrowserAgent {
2112
2183
  try {
2113
2184
  if (this.knowledge && scenario && result) {
2114
2185
  this.knowledge.recordSession(buildSession(scenario, result));
2115
- // Gen 26b: extract reusable patterns from successful runs.
2116
- // Patterns gain confidence with repeated observation and auto-decay
2117
- // when contradicted. Low-confidence facts are pruned automatically.
2186
+ // Extract reusable patterns from successful runs.
2118
2187
  if (result.success && turns && turns.length > 0) {
2119
2188
  const domain = safeHostname(scenario.startUrl || '') || '';
2120
2189
  if (domain) {
@@ -2155,7 +2224,7 @@ export class BrowserAgent {
2155
2224
  * - Generic text match -> check if text appears in snapshot
2156
2225
  */
2157
2226
  /**
2158
- * Gen 7: execute a Plan deterministically without re-entering the LLM
2227
+ * Execute a Plan deterministically without re-entering the LLM
2159
2228
  * between steps. Each step:
2160
2229
  * 1. Drives the action via driver.execute (existing path, gets bus events)
2161
2230
  * 2. Verifies the post-condition via verifyExpectedEffect
@@ -2181,25 +2250,17 @@ export class BrowserAgent {
2181
2250
  * one plan call per N steps. To make the run-level cost tally honest,
2182
2251
  * we attribute the plan call to the FIRST step's Turn artifact so the
2183
2252
  * downstream sum (in baseline-summary.json / report.json) reflects the
2184
- * real LLM spend. This was the metric bug that caused Gen 7.1 runs to
2185
- * report $0 cost while Gen 7 baseline runs reported $0.50.
2253
+ * real LLM spend.
2186
2254
  */
2187
2255
  planCallTokens) {
2188
2256
  let currentTurnIndex = startingTurnIndex;
2189
2257
  let lastState = turns[turns.length - 1]?.state
2190
2258
  ?? { url: '', title: '', snapshot: '' };
2191
- // Gen 7.2: track the last successful `runScript` output across plan steps
2192
- // so a downstream `complete` step with placeholder values (null,
2193
- // "<from prior step>", etc.) can be substituted with the real script
2194
- // output. The planner has to commit to its `complete.result` text BEFORE
2195
- // runScript runs, so on extraction tasks it fabricates placeholders.
2196
- // This deterministic substitution fixes that without an extra LLM call.
2259
+ // Track the last successful runScript output so placeholder complete
2260
+ // results can be substituted with the real script output.
2197
2261
  let lastRunScriptOutput = null;
2198
- // Gen 10: track the last extractWithIndex match list. Unlike runScript,
2199
- // we do NOT auto-substitute this into a placeholder complete — the LLM
2200
- // must read the formatted match list and pick by index. When the plan
2201
- // ends with extractWithIndex (or runs out of valid steps), we fall
2202
- // through to the per-action loop with the match list as feedback.
2262
+ // Track extractWithIndex matches for per-action fallback; the LLM must
2263
+ // read the list and pick by index.
2203
2264
  let lastExtractOutput = null;
2204
2265
  for (let stepIdx = 0; stepIdx < plan.steps.length; stepIdx++) {
2205
2266
  if (scenario.signal?.aborted) {
@@ -2226,10 +2287,8 @@ export class BrowserAgent {
2226
2287
  // timeline regardless of whether the runner used the planner or the
2227
2288
  // per-action loop.
2228
2289
  //
2229
- // Token attribution: the FIRST step of each plan carries the
2230
- // Brain.plan() LLM call's token usage. Without this, runs that stay
2231
- // in plan-mode (Gen 7.1) report $0 cost while their Brain.plan()
2232
- // calls actually spent real tokens.
2290
+ // Token attribution: the first step carries the Brain.plan() LLM call's
2291
+ // token usage so run-level cost includes planning.
2233
2292
  const isFirstStep = stepIdx === 0;
2234
2293
  const turn = {
2235
2294
  turn: turnNumber,
@@ -2249,21 +2308,16 @@ export class BrowserAgent {
2249
2308
  // Terminal actions: complete and abort don't go through driver.execute
2250
2309
  // — the runner handles them as the end of the plan.
2251
2310
  if (step.action.action === 'complete') {
2252
- // Gen 7.2 placeholder substitution: if the planner emitted a complete
2253
- // with placeholder values AND we have a real runScript output from
2254
- // earlier in the plan, use the runScript output as the final result.
2255
- // Detection is conservative: only substitute when the planner clearly
2256
- // didn't know real values at planning time (null literals, "<from
2257
- // prior step>", "{{...}}" templates, "<placeholder>", etc.).
2311
+ // Substitute placeholder complete results with prior runScript output.
2258
2312
  let resolvedResult = step.action.result;
2259
2313
  if (lastRunScriptOutput
2260
2314
  && typeof resolvedResult === 'string'
2261
2315
  && hasPlaceholderPattern(resolvedResult)) {
2262
2316
  if (this.config.debug) {
2263
- console.log(`[Runner] Gen 7.2: substituting placeholder complete.result with runScript output (${lastRunScriptOutput.length} chars)`);
2317
+ console.log(`[Runner] Substituting placeholder complete.result with runScript output (${lastRunScriptOutput.length} chars)`);
2264
2318
  }
2265
2319
  resolvedResult = lastRunScriptOutput;
2266
- turn.reasoning = `${turn.reasoning ?? ''} [Gen 7.2 substituted runScript output]`.trim();
2320
+ turn.reasoning = `${turn.reasoning ?? ''} [substituted runScript output]`.trim();
2267
2321
  }
2268
2322
  turn.durationMs = Date.now() - stepStartedAt;
2269
2323
  turns.push(turn);
@@ -2310,12 +2364,8 @@ export class BrowserAgent {
2310
2364
  // execute-started / execute-completed events on the bus exactly
2311
2365
  // like the per-action loop does.
2312
2366
  //
2313
- // CRITICAL: each plan step gets a 10s wall-clock cap (vs the driver's
2314
- // default 30s). Plan steps assume every selector was just observed in
2315
- // the snapshot at planning time — a missing element should fail
2316
- // FAST and trigger fallback to per-action mode, NOT block the run for
2317
- // 30s. Batch verbs already enforce a 5s per-field cap internally,
2318
- // but single-step type/click/press/select use the full 30s default.
2367
+ // Cap each plan step at 10s so missing selectors fail quickly and hand
2368
+ // control back to per-action mode.
2319
2369
  this.bus.emitNow({ type: 'execute-started', runId, turn: turnNumber, action: step.action });
2320
2370
  const execStartedAt = Date.now();
2321
2371
  const planStepTimeoutMs = 10_000;
@@ -2380,18 +2430,14 @@ export class BrowserAgent {
2380
2430
  runState.clearConsecutiveErrors();
2381
2431
  if (execResult.bounds)
2382
2432
  turn.actionBounds = execResult.bounds;
2383
- // Gen 7.2: capture runScript output so a downstream complete step
2384
- // with placeholder values can be substituted with the real output.
2385
- // This is the supply side of the placeholder-substitution fix above.
2433
+ // Capture runScript output for placeholder substitution and evidence.
2386
2434
  if (step.action.action === 'runScript' && typeof execResult.data === 'string' && execResult.data.length > 0) {
2387
2435
  lastRunScriptOutput = execResult.data;
2388
2436
  if (execResult.data.length > 10) {
2389
2437
  runState.recordEvidence(`EXTRACTED (turn ${currentTurnIndex}): ${execResult.data.slice(0, 500)}`);
2390
2438
  }
2391
2439
  }
2392
- // Gen 10: capture extractWithIndex match list for fall-through to the
2393
- // per-action loop. The LLM must read the list and pick by index — we
2394
- // do not auto-complete with the raw match list.
2440
+ // Capture extractWithIndex output for per-action fallback.
2395
2441
  if (step.action.action === 'extractWithIndex' && typeof execResult.data === 'string' && execResult.data.length > 0) {
2396
2442
  lastExtractOutput = execResult.data;
2397
2443
  // Also push as goal verification evidence so the verifier sees what
@@ -2405,8 +2451,7 @@ export class BrowserAgent {
2405
2451
  // Verify the post-condition. We re-observe to get the post-action
2406
2452
  // state, then run the same verifyExpectedEffect helper the per-action
2407
2453
  // loop uses. The fresh observe is also stashed in cachedPostState so
2408
- // the next step's pre-step observe is short-circuited (Gen 4 lazy
2409
- // observe optimization).
2454
+ // the next step's pre-step observe can reuse it.
2410
2455
  this.bus.emitNow({
2411
2456
  type: 'verify-started',
2412
2457
  runId,
@@ -2530,18 +2575,7 @@ export class BrowserAgent {
2530
2575
  };
2531
2576
  }
2532
2577
  }
2533
- // Gen 7.2 auto-complete-from-runScript: if the plan ended without an
2534
- // explicit complete BUT the last successful step was a runScript with
2535
- // non-empty output, treat the runScript output as the final result and
2536
- // synthesize a complete turn. This handles the planner-prompt path where
2537
- // the planner correctly emits ONLY runScript on extraction tasks (per
2538
- // rule #7) — without this, we'd fall through to a 4-5 turn per-action
2539
- // loop that's much slower than necessary.
2540
- //
2541
- // Detection: the LAST step in the plan was a `runScript` AND we captured
2542
- // a non-empty output for it. We don't check intermediate steps because
2543
- // a plan like [navigate, click, runScript] where runScript is last is
2544
- // exactly the extraction-task shape we want to short-circuit.
2578
+ // Auto-complete when the plan ends with meaningful runScript output.
2545
2579
  const lastStep = plan.steps[plan.steps.length - 1];
2546
2580
  if (lastStep
2547
2581
  && lastStep.action.action === 'runScript'
@@ -2551,7 +2585,7 @@ export class BrowserAgent {
2551
2585
  turn: synthTurnNumber,
2552
2586
  state: lastState,
2553
2587
  action: { action: 'complete', result: lastRunScriptOutput },
2554
- reasoning: 'Gen 7.2 auto-complete: plan ended after runScript, runner emitted complete with the runScript output',
2588
+ reasoning: 'Auto-complete: plan ended after runScript, runner emitted complete with the runScript output',
2555
2589
  durationMs: 0,
2556
2590
  };
2557
2591
  turns.push(synthTurn);
@@ -2568,7 +2602,7 @@ export class BrowserAgent {
2568
2602
  durationMs: 0,
2569
2603
  });
2570
2604
  if (this.config.debug) {
2571
- console.log(`[Runner] Gen 7.2: auto-emitted complete with runScript output (${lastRunScriptOutput.length} chars) after plan exhausted`);
2605
+ console.log(`[Runner] Auto-emitted complete with runScript output (${lastRunScriptOutput.length} chars) after plan exhausted`);
2572
2606
  }
2573
2607
  return {
2574
2608
  kind: 'completed',
@@ -2577,12 +2611,8 @@ export class BrowserAgent {
2577
2611
  turnsConsumed: plan.steps.length + 1,
2578
2612
  };
2579
2613
  }
2580
- // Gen 10: if the plan ended with extractWithIndex, fall through to the
2581
- // per-action loop with the match list as feedback. The LLM must read
2582
- // the matches and pick by index — we do NOT auto-complete with the raw
2583
- // match list. This is the planner-emits-extract path for extraction
2584
- // tasks like npm/mdn/python-docs where the planner used the new
2585
- // extractWithIndex action.
2614
+ // If the plan produced extractWithIndex matches, fall through with the
2615
+ // match list so the LLM can choose the correct index.
2586
2616
  if (lastExtractOutput) {
2587
2617
  return {
2588
2618
  kind: 'deviated',
@@ -2592,20 +2622,13 @@ export class BrowserAgent {
2592
2622
  turnsConsumed: plan.steps.length,
2593
2623
  };
2594
2624
  }
2595
- // Gen 9 (cherry-picked into Gen 10): if the last step WAS a runScript
2596
- // but the output was NOT meaningful (null, empty, placeholder), DO NOT
2597
- // auto-complete with garbage. Fall through to the per-action loop with
2598
- // a deviation reason that names the empty output. In Gen 10 the per-
2599
- // action loop has TWO new tools that make this recovery actually work:
2600
- // 1. extractWithIndex (the wide-query content-match action) — see
2601
- // data-extraction rule #25
2602
- // 2. cost cap (100k tokens) — bounds any death-spiral if the LLM
2603
- // can't recover, preventing the Gen 9.1 reddit failure mode
2625
+ // If the plan ends with runScript but output is empty or placeholder-like,
2626
+ // fall through to per-action mode instead of completing with bad data.
2604
2627
  if (lastStep
2605
2628
  && lastStep.action.action === 'runScript'
2606
2629
  && !isMeaningfulRunScriptOutput(lastRunScriptOutput)) {
2607
2630
  if (this.config.debug) {
2608
- console.log(`[Runner] Gen 9: runScript returned no meaningful output (${JSON.stringify(lastRunScriptOutput).slice(0, 100)}); falling through to per-action loop for two-pass extraction`);
2631
+ console.log(`[Runner] runScript returned no meaningful output (${JSON.stringify(lastRunScriptOutput).slice(0, 100)}); falling through to per-action loop for two-pass extraction`);
2609
2632
  }
2610
2633
  return {
2611
2634
  kind: 'deviated',