@enricai/barnacle 1.6.9 → 1.6.10
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/scraper/deep-locator-candidates.d.ts +62 -0
- package/dist/scraper/deep-locator-candidates.d.ts.map +1 -0
- package/dist/scraper/deep-locator-candidates.js +160 -0
- package/dist/scraper/deep-locator-candidates.js.map +1 -0
- package/dist/scraper/deep-locator-fake.d.ts +88 -0
- package/dist/scraper/deep-locator-fake.d.ts.map +1 -0
- package/dist/scraper/deep-locator-fake.js +86 -0
- package/dist/scraper/deep-locator-fake.js.map +1 -0
- package/dist/scraper/flow-runner.d.ts +8 -1
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +126 -8
- package/dist/scraper/flow-runner.js.map +1 -1
- package/package.json +1 -1
|
@@ -87,6 +87,7 @@ const schemas_1 = require("../lib/llm/schemas");
|
|
|
87
87
|
const logging_1 = require("../lib/logging");
|
|
88
88
|
const call_capture_1 = require("../lib/telemetry/call-capture");
|
|
89
89
|
const call_types_1 = require("../lib/telemetry/call-types");
|
|
90
|
+
const deep_locator_candidates_1 = require("../scraper/deep-locator-candidates");
|
|
90
91
|
const errors_2 = require("../scraper/errors");
|
|
91
92
|
const frame_target_1 = require("../scraper/frame-target");
|
|
92
93
|
const phantom_click_1 = require("../scraper/phantom-click");
|
|
@@ -4408,6 +4409,28 @@ async function probeFormValidityBeforeSubmit(params) {
|
|
|
4408
4409
|
return [];
|
|
4409
4410
|
}
|
|
4410
4411
|
}
|
|
4412
|
+
/**
|
|
4413
|
+
* Adapts `resolveDeepLocatorCandidates` results into `Action`-shaped
|
|
4414
|
+
* evidence for `rephraseWithLLM`, which only knows about Stagehand's
|
|
4415
|
+
* `Action` type. Never throws: `resolveDeepLocatorCandidates` itself
|
|
4416
|
+
* degrades to `[]` on a resolver failure, so this only feeds the rephrase
|
|
4417
|
+
* prompt richer evidence when available — a frame-scoped step whose
|
|
4418
|
+
* observe AND deepLocator both come back empty just gets the same `[]`
|
|
4419
|
+
* evidence it would have gotten before this fix.
|
|
4420
|
+
*
|
|
4421
|
+
* `instruction` is forwarded to `resolveDeepLocatorCandidates` so the
|
|
4422
|
+
* evidence list is ranked by relevance to the step, same as the act path —
|
|
4423
|
+
* an unranked `[]`-then-DOM-order list would feed the rephrase LLM its
|
|
4424
|
+
* worst evidence first instead of its best.
|
|
4425
|
+
*/
|
|
4426
|
+
async function deepLocatorCandidatesAsActions(page, frameSelector, instruction) {
|
|
4427
|
+
const candidates = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameSelector, "*", instruction);
|
|
4428
|
+
return candidates.map((c) => ({
|
|
4429
|
+
selector: c.selector,
|
|
4430
|
+
description: c.accessibleText || "(no accessible text)",
|
|
4431
|
+
method: "click",
|
|
4432
|
+
}));
|
|
4433
|
+
}
|
|
4411
4434
|
/**
|
|
4412
4435
|
* Cheap pre-cascade reachability gate. Runs before the 5-attempt healing cascade
|
|
4413
4436
|
* (and any global replan) so a step aimed at the wrong page state fails fast
|
|
@@ -4419,10 +4442,16 @@ async function probeFormValidityBeforeSubmit(params) {
|
|
|
4419
4442
|
*
|
|
4420
4443
|
* `frameTarget` scopes both observe calls to a resolved cross-origin child
|
|
4421
4444
|
* frame when the flow declared `frameSelector`; omitted (main frame) is
|
|
4422
|
-
* byte-identical to today's unscoped calls.
|
|
4445
|
+
* byte-identical to today's unscoped calls. When `frameTarget.frame` is a
|
|
4446
|
+
* resolved child frame, `observe()` is blind to it (measured against a
|
|
4447
|
+
* cross-origin OOPIF — see `deep-locator-candidates.ts`'s module docblock),
|
|
4448
|
+
* so a 0-candidate focused+unfocused observe pair additionally falls back to
|
|
4449
|
+
* `resolveDeepLocatorCandidates` before declaring the step "absent" — a
|
|
4450
|
+
* frame-scoped step must not short-circuit to replan before the cascade
|
|
4451
|
+
* (which itself now routes through the same resolver) ever runs.
|
|
4423
4452
|
*/
|
|
4424
4453
|
async function probeStepBeforeAttempts(params) {
|
|
4425
|
-
const { stagehand, step, stepIndex, totalSteps, logger, captureFn, frameTarget } = params;
|
|
4454
|
+
const { stagehand, page, step, stepIndex, totalSteps, logger, captureFn, frameTarget } = params;
|
|
4426
4455
|
try {
|
|
4427
4456
|
const candidates = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget);
|
|
4428
4457
|
if (candidates.length === 0) {
|
|
@@ -4442,6 +4471,13 @@ async function probeStepBeforeAttempts(params) {
|
|
|
4442
4471
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: focused probe found 0 candidates but unfocused observe found ${unfocused.length} — treating as present (let cascade resolve)`);
|
|
4443
4472
|
return "present";
|
|
4444
4473
|
}
|
|
4474
|
+
if (frameTarget?.frame) {
|
|
4475
|
+
const deepLocatorCandidates = await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameTarget.frameSelector, "*");
|
|
4476
|
+
if (deepLocatorCandidates.length > 0) {
|
|
4477
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: observe found 0 candidates (focused and unfocused) but deepLocator found ${deepLocatorCandidates.length} — treating as present (let cascade resolve)`);
|
|
4478
|
+
return "present";
|
|
4479
|
+
}
|
|
4480
|
+
}
|
|
4445
4481
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)}: probe found 0 candidates (focused and unfocused) — treating as absent (skip cascade, route to replan if required)`);
|
|
4446
4482
|
return "absent";
|
|
4447
4483
|
}
|
|
@@ -4587,6 +4623,7 @@ async function executeStepWithHealing(params) {
|
|
|
4587
4623
|
// burning 4 attempts on a page that clearly isn't the right one.
|
|
4588
4624
|
const probeResult = await probeStepBeforeAttempts({
|
|
4589
4625
|
stagehand,
|
|
4626
|
+
page,
|
|
4590
4627
|
step,
|
|
4591
4628
|
stepIndex,
|
|
4592
4629
|
totalSteps,
|
|
@@ -4650,7 +4687,15 @@ async function executeStepWithHealing(params) {
|
|
|
4650
4687
|
.evaluate("document.body ? document.body.outerHTML : null")
|
|
4651
4688
|
.catch(() => null);
|
|
4652
4689
|
const bodyOuterHtml = typeof bodyOuterHtmlRaw === "string" ? bodyOuterHtmlRaw.slice(0, 100_000) : null;
|
|
4653
|
-
const
|
|
4690
|
+
const probeAbsentObservedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
4691
|
+
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
4692
|
+
// result degrades to the deep-locator resolver for dump evidence — this
|
|
4693
|
+
// dump feeds replanRemainingFlow's diagnostic prompt, and an empty
|
|
4694
|
+
// candidate list there returns "repeat the failed step", burning the
|
|
4695
|
+
// replan budget on every frame-scoped probe-absent failure.
|
|
4696
|
+
const unfocusedObserve = probeAbsentObservedUnfocused.length === 0 && frameTarget?.frame
|
|
4697
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector)
|
|
4698
|
+
: probeAbsentObservedUnfocused;
|
|
4654
4699
|
const dumpPath = onStepFailure?.({
|
|
4655
4700
|
stepIndex,
|
|
4656
4701
|
phase,
|
|
@@ -4973,7 +5018,58 @@ async function executeStepWithHealing(params) {
|
|
|
4973
5018
|
? { ignoreSelectors: [...triedSelectors], timeout: exports.STEP_WATCHDOG_MS }
|
|
4974
5019
|
: { timeout: exports.STEP_WATCHDOG_MS };
|
|
4975
5020
|
const candidates = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, observeOptions, captureFn, frameTarget);
|
|
4976
|
-
|
|
5021
|
+
// observe() is blind to a cross-origin OOPIF (see
|
|
5022
|
+
// deep-locator-candidates.ts's module docblock) — when the step is
|
|
5023
|
+
// frame-scoped and observe came back empty, fall back to the deep
|
|
5024
|
+
// locator resolver before declaring the attempt candidate-less.
|
|
5025
|
+
// Passing `step` ranks candidates by relevance to the instruction
|
|
5026
|
+
// (see resolveDeepLocatorCandidates) so the top pick below is the
|
|
5027
|
+
// element the step actually names, not just DOM order.
|
|
5028
|
+
// Attempt 4's ignoreSelectors has no deepLocator equivalent, so the
|
|
5029
|
+
// exclusion is applied here by filtering resolved (already-ranked)
|
|
5030
|
+
// candidates against triedSelectors instead — otherwise attempt 4
|
|
5031
|
+
// would re-pick the same failed element and burn the attempt.
|
|
5032
|
+
const deepLocatorCandidates = candidates.length === 0 && frameTarget?.frame
|
|
5033
|
+
? (await (0, deep_locator_candidates_1.resolveDeepLocatorCandidates)(page, frameTarget.frameSelector, "*", step)).filter((c) => !triedSelectors.includes(c.selector))
|
|
5034
|
+
: [];
|
|
5035
|
+
if (candidates.length === 0 && deepLocatorCandidates.length > 0) {
|
|
5036
|
+
const top = deepLocatorCandidates[0];
|
|
5037
|
+
if (top) {
|
|
5038
|
+
record.instruction = `deepLocator: ${top.accessibleText || "(no accessible text)"}`;
|
|
5039
|
+
// Deny-list guard: mirrors the observe branch's refusal below —
|
|
5040
|
+
// never act on a wizard-exit control regardless of which
|
|
5041
|
+
// candidate source (observe vs. deepLocator) surfaced it.
|
|
5042
|
+
if (isWizardExitAction(top.accessibleText, wizardExitButtonLabels)) {
|
|
5043
|
+
record.errorMessage = `refused wizard-exit control: "${top.accessibleText.slice(0, 60)}"`;
|
|
5044
|
+
triedSelectors.push(top.selector);
|
|
5045
|
+
record.triedSelectors = [top.selector];
|
|
5046
|
+
attempts.push(record);
|
|
5047
|
+
failureReasons.push(record.errorMessage);
|
|
5048
|
+
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} attempt ${attempt}: ${record.errorMessage}`);
|
|
5049
|
+
continue;
|
|
5050
|
+
}
|
|
5051
|
+
triedSelectors.push(top.selector);
|
|
5052
|
+
record.triedSelectors = [top.selector];
|
|
5053
|
+
try {
|
|
5054
|
+
await (0, deep_locator_candidates_1.clickDeepLocatorCandidate)(page, frameTarget?.frameSelector, "*", top.index);
|
|
5055
|
+
record.actResultSuccess = true;
|
|
5056
|
+
record.actResultDescription = `deepLocator clicked "${top.accessibleText || top.selector}"`;
|
|
5057
|
+
// Synthesize a click action so downstream verification (network
|
|
5058
|
+
// / url / dom) treats this exactly like any other resolved
|
|
5059
|
+
// click — same idiom as deep-submit-locator/structured-click.
|
|
5060
|
+
resolvedAction = {
|
|
5061
|
+
selector: top.selector,
|
|
5062
|
+
description: record.actResultDescription,
|
|
5063
|
+
method: "click",
|
|
5064
|
+
};
|
|
5065
|
+
}
|
|
5066
|
+
catch (err) {
|
|
5067
|
+
record.actResultSuccess = false;
|
|
5068
|
+
record.errorMessage = `deepLocator: click threw ${(0, errors_1.toErrorMessage)(err)}`;
|
|
5069
|
+
}
|
|
5070
|
+
}
|
|
5071
|
+
}
|
|
5072
|
+
else if (candidates.length === 0) {
|
|
4977
5073
|
record.errorMessage = "observe returned no candidates";
|
|
4978
5074
|
// Optional-step short-circuit: when attempt 2 confirms no candidates
|
|
4979
5075
|
// match AND the step was marked optional in the flow, skip cleanly.
|
|
@@ -5240,7 +5336,15 @@ async function executeStepWithHealing(params) {
|
|
|
5240
5336
|
"anthropic billing exhausted (FATAL_BILLING already logged); skipping rephrase";
|
|
5241
5337
|
}
|
|
5242
5338
|
else {
|
|
5243
|
-
const
|
|
5339
|
+
const observedCandidates = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5340
|
+
// observe() is blind to a cross-origin OOPIF, so a frame-scoped
|
|
5341
|
+
// empty result degrades to the deep-locator resolver for prompt
|
|
5342
|
+
// evidence rather than hard-failing — lower stakes than the
|
|
5343
|
+
// attempt-2/4 click path since this only feeds the rephrase
|
|
5344
|
+
// prompt, so a resolver error/empty result is fine to swallow.
|
|
5345
|
+
const candidates = observedCandidates.length === 0 && frameTarget?.frame
|
|
5346
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector, step)
|
|
5347
|
+
: observedCandidates;
|
|
5244
5348
|
// Fetch live-page evidence so the rephrase prompt can reason about
|
|
5245
5349
|
// form state, not just observe candidates. Mirrors the same
|
|
5246
5350
|
// extraction the cascade-exhaust dump path already does.
|
|
@@ -5255,7 +5359,10 @@ async function executeStepWithHealing(params) {
|
|
|
5255
5359
|
// frameTarget: for a frame-scoped flow the ambient UI that matters
|
|
5256
5360
|
// (the wizard's own Save/Close controls) lives inside the iframe
|
|
5257
5361
|
// alongside the failed step, not in the top document.
|
|
5258
|
-
const
|
|
5362
|
+
const observedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5363
|
+
const unfocused = observedUnfocused.length === 0 && frameTarget?.frame
|
|
5364
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector)
|
|
5365
|
+
: observedUnfocused;
|
|
5259
5366
|
const submitFailureList = extractSubmitFailureEvidence(recentCaptures, ownBackendHostnames);
|
|
5260
5367
|
const gaEventList = extractGaEventEvidence(recentCaptures);
|
|
5261
5368
|
const priorAttemptsForPrompt = attempts.map((a, i) => ({
|
|
@@ -5862,7 +5969,15 @@ async function executeStepWithHealing(params) {
|
|
|
5862
5969
|
}
|
|
5863
5970
|
}
|
|
5864
5971
|
}
|
|
5865
|
-
const
|
|
5972
|
+
const cascadeExhaustObservedFinal = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5973
|
+
// observe() is blind to a cross-origin OOPIF, so a frame-scoped empty
|
|
5974
|
+
// result degrades to the deep-locator resolver for dump evidence — this
|
|
5975
|
+
// dump feeds replanRemainingFlow's diagnostic prompt, and an empty
|
|
5976
|
+
// candidate list there returns "repeat the failed step", burning the
|
|
5977
|
+
// replan budget on every frame-scoped cascade-exhaust failure.
|
|
5978
|
+
const finalObserve = cascadeExhaustObservedFinal.length === 0 && frameTarget?.frame
|
|
5979
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector, step)
|
|
5980
|
+
: cascadeExhaustObservedFinal;
|
|
5866
5981
|
const { pageTitle, pageUrl } = await resolveDumpPageIdentity(page, frameTarget);
|
|
5867
5982
|
// Discriminator data for "Stagehand sees nothing" failures: capture the raw
|
|
5868
5983
|
// DOM and an unfocused observe so a triager can tell empty-page from
|
|
@@ -5871,7 +5986,10 @@ async function executeStepWithHealing(params) {
|
|
|
5871
5986
|
.evaluate("document.body ? document.body.outerHTML : null")
|
|
5872
5987
|
.catch(() => null);
|
|
5873
5988
|
const bodyOuterHtml = typeof bodyOuterHtmlRaw === "string" ? bodyOuterHtmlRaw.slice(0, 100_000) : null;
|
|
5874
|
-
const
|
|
5989
|
+
const cascadeExhaustObservedUnfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
|
|
5990
|
+
const unfocusedObserve = cascadeExhaustObservedUnfocused.length === 0 && frameTarget?.frame
|
|
5991
|
+
? await deepLocatorCandidatesAsActions(page, frameTarget.frameSelector)
|
|
5992
|
+
: cascadeExhaustObservedUnfocused;
|
|
5875
5993
|
const dumpPath = onStepFailure?.({
|
|
5876
5994
|
stepIndex,
|
|
5877
5995
|
phase,
|