@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.
- package/dist/artifacts/filesystem-sink.d.ts +1 -1
- package/dist/artifacts/filesystem-sink.js +1 -1
- package/dist/brain/index.d.ts +16 -9
- package/dist/brain/index.d.ts.map +1 -1
- package/dist/brain/index.js +75 -72
- package/dist/brain/index.js.map +1 -1
- package/dist/browser-launch.js +1 -3
- package/dist/browser-launch.js.map +1 -1
- package/dist/captcha.d.ts.map +1 -1
- package/dist/captcha.js +1 -3
- package/dist/captcha.js.map +1 -1
- package/dist/cli-attach.d.ts +2 -2
- package/dist/cli-attach.js +2 -2
- package/dist/cli-design-audit.d.ts.map +1 -1
- package/dist/cli-design-audit.js +14 -24
- package/dist/cli-design-audit.js.map +1 -1
- package/dist/cli-jobs.js +4 -13
- package/dist/cli-jobs.js.map +1 -1
- package/dist/cli-patterns.d.ts +1 -1
- package/dist/cli-patterns.d.ts.map +1 -1
- package/dist/cli.js +26 -28
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +5 -3
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/design/audit/attribution/types.d.ts +3 -3
- package/dist/design/audit/attribution/types.d.ts.map +1 -1
- package/dist/design/audit/attribution/types.js +2 -2
- package/dist/design/audit/build-result.d.ts +71 -0
- package/dist/design/audit/build-result.d.ts.map +1 -0
- package/dist/design/audit/build-result.js +251 -0
- package/dist/design/audit/build-result.js.map +1 -0
- package/dist/design/audit/classify-ensemble.d.ts +1 -1
- package/dist/design/audit/classify-ensemble.d.ts.map +1 -1
- package/dist/design/audit/ethics/check.d.ts +2 -2
- package/dist/design/audit/ethics/check.d.ts.map +1 -1
- package/dist/design/audit/ethics/check.js +1 -1
- package/dist/design/audit/ethics/loader.d.ts +1 -1
- package/dist/design/audit/ethics/loader.d.ts.map +1 -1
- package/dist/design/audit/evaluate.d.ts +5 -2
- package/dist/design/audit/evaluate.d.ts.map +1 -1
- package/dist/design/audit/evaluate.js +10 -4
- package/dist/design/audit/evaluate.js.map +1 -1
- package/dist/design/audit/first-principles-mode.d.ts +1 -1
- package/dist/design/audit/first-principles-mode.d.ts.map +1 -1
- package/dist/design/audit/modality/android.d.ts +1 -1
- package/dist/design/audit/modality/android.d.ts.map +1 -1
- package/dist/design/audit/modality/html.d.ts +1 -1
- package/dist/design/audit/modality/html.d.ts.map +1 -1
- package/dist/design/audit/modality/ios.d.ts +1 -1
- package/dist/design/audit/modality/ios.d.ts.map +1 -1
- package/dist/design/audit/modality/types.d.ts +2 -2
- package/dist/design/audit/modality/types.d.ts.map +1 -1
- package/dist/design/audit/modality/types.js +1 -1
- package/dist/design/audit/patches/generate.d.ts +57 -0
- package/dist/design/audit/patches/generate.d.ts.map +1 -0
- package/dist/design/audit/patches/generate.js +168 -0
- package/dist/design/audit/patches/generate.js.map +1 -0
- package/dist/design/audit/patches/parse.d.ts +1 -1
- package/dist/design/audit/patches/parse.d.ts.map +1 -1
- package/dist/design/audit/patches/render.d.ts +1 -1
- package/dist/design/audit/patches/render.d.ts.map +1 -1
- package/dist/design/audit/patches/severity-enforcement.d.ts +1 -1
- package/dist/design/audit/patches/severity-enforcement.d.ts.map +1 -1
- package/dist/design/audit/patches/validate.d.ts +7 -1
- package/dist/design/audit/patches/validate.d.ts.map +1 -1
- package/dist/design/audit/patches/validate.js +8 -1
- package/dist/design/audit/patches/validate.js.map +1 -1
- package/dist/design/audit/patterns/match.d.ts +1 -1
- package/dist/design/audit/patterns/match.d.ts.map +1 -1
- package/dist/design/audit/patterns/types.d.ts +2 -2
- package/dist/design/audit/patterns/types.d.ts.map +1 -1
- package/dist/design/audit/pipeline.d.ts +4 -4
- package/dist/design/audit/pipeline.d.ts.map +1 -1
- package/dist/design/audit/pipeline.js +45 -30
- package/dist/design/audit/pipeline.js.map +1 -1
- package/dist/design/audit/rubric/loader.d.ts +1 -1
- package/dist/design/audit/rubric/loader.d.ts.map +1 -1
- package/dist/design/audit/rubric/rollup-weights.d.ts +1 -1
- package/dist/design/audit/rubric/rollup-weights.d.ts.map +1 -1
- package/dist/design/audit/{v2/types.d.ts → score-types.d.ts} +10 -10
- package/dist/design/audit/score-types.d.ts.map +1 -0
- package/dist/design/audit/{v2/types.js → score-types.js} +2 -2
- package/dist/design/audit/score-types.js.map +1 -0
- package/dist/design/audit/{v2/score.d.ts → score.d.ts} +9 -9
- package/dist/design/audit/score.d.ts.map +1 -0
- package/dist/design/audit/{v2/score.js → score.js} +17 -17
- package/dist/design/audit/score.js.map +1 -0
- package/dist/design/audit/types.d.ts +7 -7
- package/dist/design/audit/types.d.ts.map +1 -1
- package/dist/drivers/cursor-overlay.d.ts +1 -1
- package/dist/drivers/cursor-overlay.d.ts.map +1 -1
- package/dist/drivers/cursor-overlay.js +2 -2
- package/dist/drivers/extract-with-index.d.ts +4 -7
- package/dist/drivers/extract-with-index.d.ts.map +1 -1
- package/dist/drivers/extract-with-index.js +4 -7
- package/dist/drivers/extract-with-index.js.map +1 -1
- package/dist/drivers/overlay-label.d.ts +3 -4
- package/dist/drivers/overlay-label.d.ts.map +1 -1
- package/dist/drivers/overlay-label.js.map +1 -1
- package/dist/drivers/playwright.d.ts +2 -2
- package/dist/drivers/playwright.d.ts.map +1 -1
- package/dist/drivers/playwright.js +10 -24
- package/dist/drivers/playwright.js.map +1 -1
- package/dist/drivers/som-overlay.d.ts +2 -3
- package/dist/drivers/som-overlay.d.ts.map +1 -1
- package/dist/drivers/som-overlay.js +2 -3
- package/dist/drivers/som-overlay.js.map +1 -1
- package/dist/drivers/types.d.ts +3 -9
- package/dist/drivers/types.d.ts.map +1 -1
- package/dist/jobs/anti-bot.js +1 -1
- package/dist/jobs/anti-bot.js.map +1 -1
- package/dist/jobs/types.d.ts +1 -1
- package/dist/jobs/types.d.ts.map +1 -1
- package/dist/multi-actor.d.ts.map +1 -1
- package/dist/multi-actor.js +2 -3
- package/dist/multi-actor.js.map +1 -1
- package/dist/reports/aggregate.js +6 -6
- package/dist/reports/aggregate.js.map +1 -1
- package/dist/reports/tokens.d.ts +0 -8
- package/dist/reports/tokens.d.ts.map +1 -1
- package/dist/reports/tokens.js +1 -15
- package/dist/reports/tokens.js.map +1 -1
- package/dist/reports/types.d.ts +2 -2
- package/dist/reports/types.d.ts.map +1 -1
- package/dist/run-state.d.ts +21 -15
- package/dist/run-state.d.ts.map +1 -1
- package/dist/run-state.js +23 -22
- package/dist/run-state.js.map +1 -1
- package/dist/runner/completion-language.d.ts +2 -0
- package/dist/runner/completion-language.d.ts.map +1 -0
- package/dist/runner/completion-language.js +5 -0
- package/dist/runner/completion-language.js.map +1 -0
- package/dist/runner/direct-start.d.ts +19 -0
- package/dist/runner/direct-start.d.ts.map +1 -0
- package/dist/runner/direct-start.js +295 -0
- package/dist/runner/direct-start.js.map +1 -0
- package/dist/runner/events.d.ts +3 -6
- package/dist/runner/events.d.ts.map +1 -1
- package/dist/runner/events.js.map +1 -1
- package/dist/runner/fan-out.d.ts +6 -9
- package/dist/runner/fan-out.d.ts.map +1 -1
- package/dist/runner/fan-out.js +7 -15
- package/dist/runner/fan-out.js.map +1 -1
- package/dist/runner/goal-decomposer.d.ts +0 -1
- package/dist/runner/goal-decomposer.d.ts.map +1 -1
- package/dist/runner/goal-decomposer.js +0 -1
- package/dist/runner/goal-decomposer.js.map +1 -1
- package/dist/runner/google-flights-preflight.d.ts +35 -0
- package/dist/runner/google-flights-preflight.d.ts.map +1 -0
- package/dist/runner/google-flights-preflight.js +411 -0
- package/dist/runner/google-flights-preflight.js.map +1 -0
- package/dist/runner/interrupt-controller.d.ts +1 -1
- package/dist/runner/interrupt-controller.js +1 -1
- package/dist/runner/overlay-narration.d.ts +1 -1
- package/dist/runner/overlay-narration.js +1 -1
- package/dist/runner/parallel-runner.d.ts +3 -6
- package/dist/runner/parallel-runner.d.ts.map +1 -1
- package/dist/runner/parallel-runner.js +2 -3
- package/dist/runner/parallel-runner.js.map +1 -1
- package/dist/runner/pattern-extractor.d.ts +2 -2
- package/dist/runner/pattern-extractor.js +2 -2
- package/dist/runner/runner.d.ts +15 -39
- package/dist/runner/runner.d.ts.map +1 -1
- package/dist/runner/runner.js +254 -231
- package/dist/runner/runner.js.map +1 -1
- package/dist/runner/stream-webhook.d.ts +1 -1
- package/dist/runner/stream-webhook.js +1 -1
- package/dist/skills/domain-loader.d.ts +1 -2
- package/dist/skills/domain-loader.d.ts.map +1 -1
- package/dist/skills/domain-loader.js +1 -2
- package/dist/skills/domain-loader.js.map +1 -1
- package/dist/telemetry/schema.d.ts +2 -2
- package/dist/telemetry/schema.js +2 -2
- package/dist/test-runner.d.ts +4 -5
- package/dist/test-runner.d.ts.map +1 -1
- package/dist/test-runner.js +2 -3
- package/dist/test-runner.js.map +1 -1
- package/dist/types.d.ts +28 -21
- package/dist/types.d.ts.map +1 -1
- package/package.json +8 -2
- package/dist/design/audit/v2/build-result.d.ts +0 -47
- package/dist/design/audit/v2/build-result.d.ts.map +0 -1
- package/dist/design/audit/v2/build-result.js +0 -163
- package/dist/design/audit/v2/build-result.js.map +0 -1
- package/dist/design/audit/v2/score.d.ts.map +0 -1
- package/dist/design/audit/v2/score.js.map +0 -1
- package/dist/design/audit/v2/types.d.ts.map +0 -1
- package/dist/design/audit/v2/types.js.map +0 -1
package/dist/runner/runner.js
CHANGED
|
@@ -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
|
-
*
|
|
42
|
-
*
|
|
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
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
334
|
-
//
|
|
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
|
-
//
|
|
339
|
-
//
|
|
340
|
-
|
|
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
|
-
//
|
|
347
|
-
//
|
|
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
|
|
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 (
|
|
437
|
+
if (initialStartUrl) {
|
|
436
438
|
const navigateStartedAt = Date.now();
|
|
437
|
-
const navPromise = withRetry(() => this.driver.execute({ action: 'navigate', url:
|
|
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
|
-
|
|
441
|
-
this.
|
|
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
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
474
|
-
//
|
|
475
|
-
//
|
|
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
|
-
//
|
|
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]
|
|
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
|
|
637
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
662
|
-
//
|
|
663
|
-
|
|
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
|
-
//
|
|
686
|
-
//
|
|
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
|
|
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
|
-
|
|
855
|
+
initialStartUrl &&
|
|
810
856
|
turns.length === 0) {
|
|
811
|
-
await this.driver.execute({ action: 'navigate', url:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
1125
|
-
//
|
|
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
|
-
//
|
|
1152
|
-
//
|
|
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.
|
|
1300
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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
|
-
// --
|
|
1513
|
-
//
|
|
1514
|
-
//
|
|
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
|
-
//
|
|
1534
|
-
//
|
|
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
|
-
//
|
|
1582
|
-
//
|
|
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
|
-
//
|
|
1592
|
-
//
|
|
1593
|
-
|
|
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]
|
|
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
|
-
//
|
|
1704
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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:
|
|
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
|
-
//
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
//
|
|
2192
|
-
//
|
|
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
|
-
//
|
|
2199
|
-
//
|
|
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
|
|
2230
|
-
//
|
|
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
|
-
//
|
|
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]
|
|
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 ?? ''} [
|
|
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
|
-
//
|
|
2314
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
-
//
|
|
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: '
|
|
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]
|
|
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
|
-
//
|
|
2581
|
-
//
|
|
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
|
-
//
|
|
2596
|
-
//
|
|
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]
|
|
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',
|