@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.
@@ -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 unfocusedObserve = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
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
- if (candidates.length === 0) {
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 candidates = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
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 unfocused = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
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 finalObserve = await (0, stagehand_guard_1.guardedObserve)(stagehand, step, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
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 unfocusedObserve = await (0, stagehand_guard_1.guardedObserve)(stagehand, undefined, { timeout: exports.STEP_WATCHDOG_MS }, captureFn, frameTarget).catch(() => []);
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,