@enricai/barnacle 1.9.1 → 1.9.3
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/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/lib/llm/schemas.d.ts +10 -0
- package/dist/lib/llm/schemas.d.ts.map +1 -1
- package/dist/lib/llm/schemas.js +19 -0
- package/dist/lib/llm/schemas.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +5 -0
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/scraper/flow-runner.d.ts +17 -5
- package/dist/scraper/flow-runner.d.ts.map +1 -1
- package/dist/scraper/flow-runner.js +99 -51
- package/dist/scraper/flow-runner.js.map +1 -1
- package/dist/scripts/recon-browser.d.ts +79 -19
- package/dist/scripts/recon-browser.d.ts.map +1 -1
- package/dist/scripts/recon-browser.js +207 -47
- package/dist/scripts/recon-browser.js.map +1 -1
- package/dist/scripts/recon-generate.d.ts +32 -2
- package/dist/scripts/recon-generate.d.ts.map +1 -1
- package/dist/scripts/recon-generate.js +116 -13
- package/dist/scripts/recon-generate.js.map +1 -1
- package/package.json +1 -1
|
@@ -2393,11 +2393,21 @@ async function fillTextDatepickerInput(target, selector, value) {
|
|
|
2393
2393
|
* Returns null for non-fillable elements (clicks, selects, etc.) — caller
|
|
2394
2394
|
* knows to skip the check.
|
|
2395
2395
|
*
|
|
2396
|
+
* Accepts any selector a call site holds: Stagehand's `xpath=…`, an
|
|
2397
|
+
* already-stripped bare XPath, or a non-xpath form (`deeplocator=…`,
|
|
2398
|
+
* `deep-index:N`). Non-xpath forms can't be read via `document.evaluate`, so
|
|
2399
|
+
* the selector is normalized through {@link xpathBodyForEvaluate} and a form
|
|
2400
|
+
* this reader can't resolve returns null (same "couldn't verify — skip" contract
|
|
2401
|
+
* as a non-fillable element) rather than evaluating an invalid XPath expression.
|
|
2402
|
+
*
|
|
2396
2403
|
* Site-agnostic: works on any <input>, <textarea>, or [contenteditable]
|
|
2397
2404
|
* element regardless of framework wrapping. Industry-standard pattern
|
|
2398
2405
|
* (react-testing-library's `getByDisplayValue` does the same readback).
|
|
2399
2406
|
*/
|
|
2400
|
-
async function verifyFillReadback(target,
|
|
2407
|
+
async function verifyFillReadback(target, selector, expectedValue) {
|
|
2408
|
+
const xpath = xpathBodyForEvaluate(selector);
|
|
2409
|
+
if (xpath === null)
|
|
2410
|
+
return null;
|
|
2401
2411
|
const expr = `(() => {
|
|
2402
2412
|
const xpath = ${JSON.stringify(xpath)};
|
|
2403
2413
|
const expected = ${JSON.stringify(expectedValue)};
|
|
@@ -2518,6 +2528,22 @@ const STATE_CLASS_METHODS = new Set([
|
|
|
2518
2528
|
function xpathBody(selector) {
|
|
2519
2529
|
return selector.startsWith("xpath=") ? selector.slice("xpath=".length) : null;
|
|
2520
2530
|
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Resolve any selector a caller might hold into a bare XPath body for
|
|
2533
|
+
* `document.evaluate`. `verifyFillReadback` is shared across call sites that
|
|
2534
|
+
* carry different selector forms — Stagehand's `xpath=…` (act path), an
|
|
2535
|
+
* already-stripped body (the datepicker primitive's internal readback), and
|
|
2536
|
+
* non-xpath forms like `deeplocator=… >> nth=N` / `deep-index:N` (deep-locator
|
|
2537
|
+
* cascades) — so a single `xpath=`-strip is insufficient and feeding a
|
|
2538
|
+
* prefixed string straight to `document.evaluate` yields an invalid expression.
|
|
2539
|
+
* Accepts an `xpath=`-prefixed selector or a bare XPath (starts with `/` or
|
|
2540
|
+
* `(`); returns null for every other form so the reader resolves nothing rather
|
|
2541
|
+
* than evaluating garbage.
|
|
2542
|
+
*/
|
|
2543
|
+
function xpathBodyForEvaluate(selector) {
|
|
2544
|
+
const stripped = selector.startsWith("xpath=") ? selector.slice("xpath=".length) : selector;
|
|
2545
|
+
return stripped.startsWith("/") || stripped.startsWith("(") ? stripped : null;
|
|
2546
|
+
}
|
|
2521
2547
|
/** How long the upload primitive waits for a post-setInputFiles network POST. */
|
|
2522
2548
|
const UPLOAD_NETWORK_TIMEOUT_MS = 5_000;
|
|
2523
2549
|
/** Polling interval while waiting for the upload's network signal. */
|
|
@@ -3046,23 +3072,31 @@ function parseFillStep(instruction) {
|
|
|
3046
3072
|
* canonical `<label> field with '<value>'` shape, so real-world prose that
|
|
3047
3073
|
* inserts words between the field noun and `with` — e.g. "Fill in the Start
|
|
3048
3074
|
* Date field FOR WORK EXPERIENCE with '01/2020'" — parses to `null`, missing
|
|
3049
|
-
* the fill intent entirely. This looser parser recognizes a
|
|
3050
|
-
*
|
|
3051
|
-
*
|
|
3075
|
+
* the fill intent entirely. This looser parser recognizes a value-carrying
|
|
3076
|
+
* entry verb and returns just the value, so the act-success datepicker guard
|
|
3077
|
+
* can key off intent that survives that phrasing drift. Two shapes are
|
|
3078
|
+
* accepted: `fill … with '<value>'` and the replanner's own escalation
|
|
3079
|
+
* `type '<value>' into …` — the replanner rewords a stuck date fill as
|
|
3080
|
+
* "Type '01/2020' into the Start Date input", which carries no `fill` verb, so
|
|
3081
|
+
* without the `type` shape the guard could never fire on any replanned step.
|
|
3052
3082
|
*
|
|
3053
3083
|
* Deliberately excludes select steps (checked first, mirroring
|
|
3054
3084
|
* `resolveDeepLocatorActuation`'s precedence) so a `select '…'` step whose
|
|
3055
3085
|
* prose happens to trail a quoted value is never mistaken for a fill. Returns
|
|
3056
|
-
* `null` for clicks, selects, and any step without a quoted
|
|
3086
|
+
* `null` for clicks, selects, and any step without a quoted value.
|
|
3057
3087
|
*/
|
|
3058
3088
|
function parseFillValueIntent(instruction) {
|
|
3059
3089
|
if (parseSelectStep(instruction))
|
|
3060
3090
|
return null;
|
|
3061
|
-
if (
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3091
|
+
if (/\bfill(?:\s+in)?\b/i.test(instruction)) {
|
|
3092
|
+
const value = instruction.match(/\bwith\s+'([^']+)'/i)?.[1]?.trim();
|
|
3093
|
+
return value ? { value } : null;
|
|
3094
|
+
}
|
|
3095
|
+
if (/\btype\b/i.test(instruction)) {
|
|
3096
|
+
const value = instruction.match(/\btype\s+'([^']+)'/i)?.[1]?.trim();
|
|
3097
|
+
return value ? { value } : null;
|
|
3098
|
+
}
|
|
3099
|
+
return null;
|
|
3066
3100
|
}
|
|
3067
3101
|
/**
|
|
3068
3102
|
* Parse a single-choice RADIO flow step into the option to click and (when
|
|
@@ -3316,11 +3350,17 @@ async function applySelectValue(page, target, selIdx, value) {
|
|
|
3316
3350
|
const stillInvalid = (await target.evaluate(invalidExpr).catch(() => false));
|
|
3317
3351
|
return { ok: true, stillInvalid };
|
|
3318
3352
|
}
|
|
3353
|
+
/**
|
|
3354
|
+
* Returns the resolved `<select>`'s DOM id on success (empty string when the
|
|
3355
|
+
* element has no id), or `null` when the step wasn't handled here (no matching
|
|
3356
|
+
* select, no commit) and should fall through to the cascade. The id becomes the
|
|
3357
|
+
* step's stable `targetId` for cross-run convergence.
|
|
3358
|
+
*/
|
|
3319
3359
|
async function trySelectPrimitive(params) {
|
|
3320
3360
|
const { page, target, instruction, logger, anthropic, captureFn } = params;
|
|
3321
3361
|
const parsed = parseSelectStep(instruction);
|
|
3322
3362
|
if (!parsed)
|
|
3323
|
-
return
|
|
3363
|
+
return null;
|
|
3324
3364
|
const { option, questionLabel } = parsed;
|
|
3325
3365
|
const optLabel = `option "${option.slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
3326
3366
|
// Phase 1 (browser, no mutation): find the target select — the one whose
|
|
@@ -3349,7 +3389,7 @@ async function trySelectPrimitive(params) {
|
|
|
3349
3389
|
for (let i = 0; i < selects.length; i++) {
|
|
3350
3390
|
const opts = Array.from(selects[i].options || []);
|
|
3351
3391
|
const m = opts.find((o) => norm(o.textContent) === wantOpt || norm(o.value) === wantOpt);
|
|
3352
|
-
if (m) detMatches.push({ selIdx: i, value: m.value, text: m.textContent });
|
|
3392
|
+
if (m) detMatches.push({ selIdx: i, value: m.value, text: m.textContent, id: selects[i].id || "" });
|
|
3353
3393
|
}
|
|
3354
3394
|
if (detMatches.length === 1) {
|
|
3355
3395
|
return { selectPresent: true, detMatch: detMatches[0] };
|
|
@@ -3394,7 +3434,7 @@ async function trySelectPrimitive(params) {
|
|
|
3394
3434
|
const options = Array.from(sel.options || [])
|
|
3395
3435
|
.filter((o) => !o.disabled && (o.value || (o.textContent || "").trim()))
|
|
3396
3436
|
.map((o) => ({ text: (o.textContent || "").replace(/\\s+/g, " ").trim(), value: o.value }));
|
|
3397
|
-
if (options.length > 0) candidates.push({ selIdx: i, label: selLabelText(sel), options });
|
|
3437
|
+
if (options.length > 0) candidates.push({ selIdx: i, id: sel.id || "", label: selLabelText(sel), options });
|
|
3398
3438
|
}
|
|
3399
3439
|
if (candidates.length === 0) return { selectPresent: true, candidates: [] };
|
|
3400
3440
|
return { selectPresent: true, candidates };
|
|
@@ -3407,7 +3447,7 @@ async function trySelectPrimitive(params) {
|
|
|
3407
3447
|
// fall through to the cascade unchanged; the LLM picker can't help here.
|
|
3408
3448
|
if (!enumResult?.selectPresent) {
|
|
3409
3449
|
logger.info(`select primitive: no <select> on page for ${optLabel}; falling through to cascade`);
|
|
3410
|
-
return
|
|
3450
|
+
return null;
|
|
3411
3451
|
}
|
|
3412
3452
|
// Deterministic unique-option match: set it, settle, and confirm it committed
|
|
3413
3453
|
// (cleared the required/invalid marker). A set that doesn't clear the marker
|
|
@@ -3417,15 +3457,15 @@ async function trySelectPrimitive(params) {
|
|
|
3417
3457
|
const { ok, stillInvalid } = await applySelectValue(page, target, enumResult.detMatch.selIdx, enumResult.detMatch.value);
|
|
3418
3458
|
if (ok && !stillInvalid) {
|
|
3419
3459
|
logger.info(`select primitive: set dropdown to "${enumResult.detMatch.text.trim().slice(0, 40)}" (${optLabel})`);
|
|
3420
|
-
return
|
|
3460
|
+
return enumResult.detMatch.id;
|
|
3421
3461
|
}
|
|
3422
3462
|
logger.info(`select primitive: dropdown value for ${optLabel} did not commit (ok=${ok} stillInvalid=${stillInvalid}); falling through to cascade`);
|
|
3423
|
-
return
|
|
3463
|
+
return null;
|
|
3424
3464
|
}
|
|
3425
3465
|
const candidates = enumResult.candidates ?? [];
|
|
3426
3466
|
if (anthropic === null || candidates.length === 0) {
|
|
3427
3467
|
logger.info(`select primitive: no unique option match for ${optLabel}${anthropic === null ? " (no LLM client)" : ""}; falling through to cascade`);
|
|
3428
|
-
return
|
|
3468
|
+
return null;
|
|
3429
3469
|
}
|
|
3430
3470
|
// LLM picks WHICH dropdown answers the question and which option in it.
|
|
3431
3471
|
const verdict = await (0, select_option_1.judgeSelectOptionWithLLM)({
|
|
@@ -3442,7 +3482,7 @@ async function trySelectPrimitive(params) {
|
|
|
3442
3482
|
});
|
|
3443
3483
|
if (!verdict || verdict.selectIndex === null || verdict.optionIndex === null) {
|
|
3444
3484
|
logger.info(`select primitive: LLM found no matching dropdown for ${optLabel}${verdict ? ` (${verdict.reason})` : ""}; falling through to cascade`);
|
|
3445
|
-
return
|
|
3485
|
+
return null;
|
|
3446
3486
|
}
|
|
3447
3487
|
// biome-ignore lint/style/noNonNullAssertion: guarded above by the verdict.selectIndex === null early-return
|
|
3448
3488
|
const chosenCandidate = candidates[verdict.selectIndex];
|
|
@@ -3454,14 +3494,14 @@ async function trySelectPrimitive(params) {
|
|
|
3454
3494
|
const { ok, stillInvalid } = await applySelectValue(page, target, chosenCandidate.selIdx, chosenOption.value);
|
|
3455
3495
|
if (ok && !stillInvalid) {
|
|
3456
3496
|
logger.info(`select primitive: LLM chose "${chosenOption.text.slice(0, 40)}" for ${optLabel} (${verdict.reason.slice(0, 60)})`);
|
|
3457
|
-
return
|
|
3497
|
+
return chosenCandidate.id;
|
|
3458
3498
|
}
|
|
3459
3499
|
logger.info(`select primitive: LLM-chosen value for ${optLabel} did not commit (ok=${ok} stillInvalid=${stillInvalid}); falling through`);
|
|
3460
|
-
return
|
|
3500
|
+
return null;
|
|
3461
3501
|
}
|
|
3462
3502
|
catch (err) {
|
|
3463
3503
|
logger.warn(`select primitive: evaluate threw: ${(0, errors_1.toErrorMessage)(err)}; falling through`);
|
|
3464
|
-
return
|
|
3504
|
+
return null;
|
|
3465
3505
|
}
|
|
3466
3506
|
}
|
|
3467
3507
|
/** Option texts that answer a required select without volunteering info — used
|
|
@@ -3645,11 +3685,16 @@ async function tryFillRequiredSelectsPrimitive(params) {
|
|
|
3645
3685
|
* no checkbox groups, or no option fits (fall through to the cascade — which is
|
|
3646
3686
|
* also where `<select>`-only pages go, since trySelectPrimitive runs first).
|
|
3647
3687
|
*/
|
|
3688
|
+
/**
|
|
3689
|
+
* Returns the resolved checkbox's DOM id on success (empty string when it has
|
|
3690
|
+
* no id), or `null` when the step wasn't handled here. See trySelectPrimitive
|
|
3691
|
+
* for the id-as-targetId rationale.
|
|
3692
|
+
*/
|
|
3648
3693
|
async function tryCheckboxPrimitive(params) {
|
|
3649
3694
|
const { page, target, instruction, logger, anthropic, captureFn } = params;
|
|
3650
3695
|
const parsed = parseSelectStep(instruction);
|
|
3651
3696
|
if (!parsed)
|
|
3652
|
-
return
|
|
3697
|
+
return null;
|
|
3653
3698
|
const { option, questionLabel } = parsed;
|
|
3654
3699
|
const optLabel = `option "${option.slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
3655
3700
|
// Phase 1 (browser, no mutation): find checkbox GROUPS and their options.
|
|
@@ -3723,7 +3768,7 @@ async function tryCheckboxPrimitive(params) {
|
|
|
3723
3768
|
const grpEl = groupEls[h.gi];
|
|
3724
3769
|
const cb = grpEl.querySelectorAll("input[type=checkbox]")[h.bi];
|
|
3725
3770
|
const ok = cb ? setChecked(cb) : false;
|
|
3726
|
-
return { groupPresent: true, applied: true, ok, chosen: h.text };
|
|
3771
|
+
return { groupPresent: true, applied: true, ok, chosen: h.text, id: cb ? (cb.id || "") : "" };
|
|
3727
3772
|
}
|
|
3728
3773
|
// LLM path: return groups (label + option texts) for the picker.
|
|
3729
3774
|
return { groupPresent: true, applied: false, groups: groups.map((g) => ({ gi: g.gi, label: g.label, options: g.options })) };
|
|
@@ -3733,15 +3778,15 @@ async function tryCheckboxPrimitive(params) {
|
|
|
3733
3778
|
// (SPA render-lag); poll until it appears or the cap is hit.
|
|
3734
3779
|
const enumResult = await pollEnumerate(page, target, enumerateExpr, (r) => r?.groupPresent === true);
|
|
3735
3780
|
if (!enumResult?.groupPresent)
|
|
3736
|
-
return
|
|
3781
|
+
return null; // no checkbox groups → cascade
|
|
3737
3782
|
if (enumResult.applied && enumResult.ok) {
|
|
3738
3783
|
logger.info(`checkbox primitive: checked "${(enumResult.chosen || "").trim().slice(0, 40)}" (${optLabel})`);
|
|
3739
|
-
return
|
|
3784
|
+
return enumResult.id ?? "";
|
|
3740
3785
|
}
|
|
3741
3786
|
const groups = enumResult.groups ?? [];
|
|
3742
3787
|
if (anthropic === null || groups.length === 0) {
|
|
3743
3788
|
logger.info(`checkbox primitive: no unique option match for ${optLabel}${anthropic === null ? " (no LLM client)" : ""}; falling through to cascade`);
|
|
3744
|
-
return
|
|
3789
|
+
return null;
|
|
3745
3790
|
}
|
|
3746
3791
|
// LLM picks which group answers the question + which option. Reuse the
|
|
3747
3792
|
// select-option judge (candidate "dropdowns" == checkbox groups here).
|
|
@@ -3759,7 +3804,7 @@ async function tryCheckboxPrimitive(params) {
|
|
|
3759
3804
|
});
|
|
3760
3805
|
if (!verdict || verdict.selectIndex === null || verdict.optionIndex === null) {
|
|
3761
3806
|
logger.info(`checkbox primitive: LLM found no matching group for ${optLabel}${verdict ? ` (${verdict.reason})` : ""}; falling through to cascade`);
|
|
3762
|
-
return
|
|
3807
|
+
return null;
|
|
3763
3808
|
}
|
|
3764
3809
|
// biome-ignore lint/style/noNonNullAssertion: guarded above by the verdict.selectIndex === null early-return
|
|
3765
3810
|
const chosenGroup = groups[verdict.selectIndex];
|
|
@@ -3778,19 +3823,19 @@ async function tryCheckboxPrimitive(params) {
|
|
|
3778
3823
|
cb.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3779
3824
|
cb.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3780
3825
|
}
|
|
3781
|
-
return { ok: cb.checked === true };
|
|
3826
|
+
return { ok: cb.checked === true, id: cb.id || "" };
|
|
3782
3827
|
})(${JSON.stringify(chosenGroup.gi)}, ${JSON.stringify(chosenOption.bi)})`;
|
|
3783
3828
|
const applyResult = (await target.evaluate(applyExpr));
|
|
3784
3829
|
if (applyResult?.ok) {
|
|
3785
3830
|
logger.info(`checkbox primitive: LLM checked "${chosenOption.text.slice(0, 40)}" for ${optLabel} (${verdict.reason.slice(0, 60)})`);
|
|
3786
|
-
return
|
|
3831
|
+
return applyResult.id;
|
|
3787
3832
|
}
|
|
3788
3833
|
logger.info(`checkbox primitive: LLM-chosen checkbox did not stick for ${optLabel}; falling through`);
|
|
3789
|
-
return
|
|
3834
|
+
return null;
|
|
3790
3835
|
}
|
|
3791
3836
|
catch (err) {
|
|
3792
3837
|
logger.warn(`checkbox primitive: evaluate threw: ${(0, errors_1.toErrorMessage)(err)}; falling through`);
|
|
3793
|
-
return
|
|
3838
|
+
return null;
|
|
3794
3839
|
}
|
|
3795
3840
|
}
|
|
3796
3841
|
/**
|
|
@@ -3976,7 +4021,7 @@ async function tryRadioPrimitive(params) {
|
|
|
3976
4021
|
const { page, target, instruction, logger, anthropic, captureFn } = params;
|
|
3977
4022
|
const parsed = parseRadioStep(instruction);
|
|
3978
4023
|
if (!parsed)
|
|
3979
|
-
return
|
|
4024
|
+
return null;
|
|
3980
4025
|
const { option, questionLabel } = parsed;
|
|
3981
4026
|
const optLabel = `option "${option.slice(0, 40)}"${questionLabel ? `, question "${questionLabel.slice(0, 40)}"` : ""}`;
|
|
3982
4027
|
// Phase 1 (browser): find radio GROUPS and their options. A group is a
|
|
@@ -4048,10 +4093,10 @@ async function tryRadioPrimitive(params) {
|
|
|
4048
4093
|
try {
|
|
4049
4094
|
const enumResult = await pollEnumerate(page, target, enumerateExpr, (r) => r?.groupPresent === true);
|
|
4050
4095
|
if (!enumResult?.groupPresent)
|
|
4051
|
-
return
|
|
4096
|
+
return null; // no radio group → cascade
|
|
4052
4097
|
const groups = enumResult.groups ?? [];
|
|
4053
4098
|
if (groups.length === 0)
|
|
4054
|
-
return
|
|
4099
|
+
return null;
|
|
4055
4100
|
// Deterministic + positional selection (excludes already-answered groups,
|
|
4056
4101
|
// fixes the empty-label universal-match bug). Only genuine labeled ambiguity
|
|
4057
4102
|
// defers to the LLM.
|
|
@@ -4064,19 +4109,19 @@ async function tryRadioPrimitive(params) {
|
|
|
4064
4109
|
});
|
|
4065
4110
|
if (applied) {
|
|
4066
4111
|
logger.info(`radio primitive: selected "${(chosenOpt?.text ?? "").trim().slice(0, 40)}" (${optLabel})`);
|
|
4067
|
-
return
|
|
4112
|
+
return chosenOpt?.id ?? "";
|
|
4068
4113
|
}
|
|
4069
4114
|
logger.info(`radio primitive: chosen radio did not stick for ${optLabel}; falling through`);
|
|
4070
|
-
return
|
|
4115
|
+
return null;
|
|
4071
4116
|
}
|
|
4072
4117
|
if (selection === null) {
|
|
4073
4118
|
logger.info(`radio primitive: no group offers option for ${optLabel}; falling through to cascade`);
|
|
4074
|
-
return
|
|
4119
|
+
return null;
|
|
4075
4120
|
}
|
|
4076
4121
|
// selection === "ambiguous": multiple labeled groups match → let the LLM pick.
|
|
4077
4122
|
if (anthropic === null) {
|
|
4078
4123
|
logger.info(`radio primitive: ambiguous labeled match for ${optLabel} (no LLM client); falling through to cascade`);
|
|
4079
|
-
return
|
|
4124
|
+
return null;
|
|
4080
4125
|
}
|
|
4081
4126
|
// LLM picks which group answers the question + which option. Reuse the
|
|
4082
4127
|
// select-option judge (candidate "dropdowns" == radio groups here). Only
|
|
@@ -4096,7 +4141,7 @@ async function tryRadioPrimitive(params) {
|
|
|
4096
4141
|
});
|
|
4097
4142
|
if (!verdict || verdict.selectIndex === null || verdict.optionIndex === null) {
|
|
4098
4143
|
logger.info(`radio primitive: LLM found no matching group for ${optLabel}${verdict ? ` (${verdict.reason})` : ""}; falling through to cascade`);
|
|
4099
|
-
return
|
|
4144
|
+
return null;
|
|
4100
4145
|
}
|
|
4101
4146
|
// biome-ignore lint/style/noNonNullAssertion: guarded above by the verdict.selectIndex === null early-return
|
|
4102
4147
|
const chosenGroup = llmGroups[verdict.selectIndex];
|
|
@@ -4110,14 +4155,14 @@ async function tryRadioPrimitive(params) {
|
|
|
4110
4155
|
};
|
|
4111
4156
|
if (applyResult?.ok) {
|
|
4112
4157
|
logger.info(`radio primitive: LLM selected "${chosenOption.text.slice(0, 40)}" for ${optLabel} (${verdict.reason.slice(0, 60)})`);
|
|
4113
|
-
return
|
|
4158
|
+
return chosenOption.id ?? "";
|
|
4114
4159
|
}
|
|
4115
4160
|
logger.info(`radio primitive: LLM-chosen radio did not stick for ${optLabel}; falling through`);
|
|
4116
|
-
return
|
|
4161
|
+
return null;
|
|
4117
4162
|
}
|
|
4118
4163
|
catch (err) {
|
|
4119
4164
|
logger.warn(`radio primitive: evaluate threw: ${(0, errors_1.toErrorMessage)(err)}; falling through`);
|
|
4120
|
-
return
|
|
4165
|
+
return null;
|
|
4121
4166
|
}
|
|
4122
4167
|
}
|
|
4123
4168
|
/**
|
|
@@ -5152,16 +5197,17 @@ async function executeStepWithHealing(params) {
|
|
|
5152
5197
|
// falls back to the main-frame target when no frameSelector is set, so this
|
|
5153
5198
|
// bridge stays behavior-identical for every existing site.
|
|
5154
5199
|
const selectFrameTarget = frameTarget ?? (0, frame_target_1.mainFrameTarget)(page);
|
|
5155
|
-
|
|
5200
|
+
const selectTargetId = await trySelectPrimitive({
|
|
5156
5201
|
page,
|
|
5157
5202
|
target: selectFrameTarget,
|
|
5158
5203
|
instruction: step,
|
|
5159
5204
|
logger,
|
|
5160
5205
|
anthropic,
|
|
5161
5206
|
captureFn,
|
|
5162
|
-
})
|
|
5207
|
+
});
|
|
5208
|
+
if (selectTargetId !== null) {
|
|
5163
5209
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} resolved by select primitive`);
|
|
5164
|
-
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
5210
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom", targetId: selectTargetId });
|
|
5165
5211
|
return "completed";
|
|
5166
5212
|
}
|
|
5167
5213
|
// When the step is a "select 'X'" against a multi-select CHECKBOX group
|
|
@@ -5169,16 +5215,17 @@ async function executeStepWithHealing(params) {
|
|
|
5169
5215
|
// screening questions this way — answer it directly in the DOM. Runs AFTER
|
|
5170
5216
|
// trySelectPrimitive (which handles <select> and no-ops on checkbox-only
|
|
5171
5217
|
// pages). No-op (falls through) when there's no checkbox group or no match.
|
|
5172
|
-
|
|
5218
|
+
const checkboxTargetId = await tryCheckboxPrimitive({
|
|
5173
5219
|
page,
|
|
5174
5220
|
target: selectFrameTarget,
|
|
5175
5221
|
instruction: step,
|
|
5176
5222
|
logger,
|
|
5177
5223
|
anthropic,
|
|
5178
5224
|
captureFn,
|
|
5179
|
-
})
|
|
5225
|
+
});
|
|
5226
|
+
if (checkboxTargetId !== null) {
|
|
5180
5227
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} resolved by checkbox primitive`);
|
|
5181
|
-
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
5228
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom", targetId: checkboxTargetId });
|
|
5182
5229
|
return "completed";
|
|
5183
5230
|
}
|
|
5184
5231
|
// When the step is a single-choice RADIO answer ("Click the 'Yes' answer for
|
|
@@ -5187,16 +5234,17 @@ async function executeStepWithHealing(params) {
|
|
|
5187
5234
|
// reach the observe cascade's el.click() fallback that fails to commit MUI/
|
|
5188
5235
|
// React controlled state (the wizard ATS's Basic-Info Step-2 wall). No-op (falls
|
|
5189
5236
|
// through) when there's no radio group or no confident option match.
|
|
5190
|
-
|
|
5237
|
+
const radioTargetId = await tryRadioPrimitive({
|
|
5191
5238
|
page,
|
|
5192
5239
|
target: frameTarget ?? (0, frame_target_1.mainFrameTarget)(page),
|
|
5193
5240
|
instruction: step,
|
|
5194
5241
|
logger,
|
|
5195
5242
|
anthropic,
|
|
5196
5243
|
captureFn,
|
|
5197
|
-
})
|
|
5244
|
+
});
|
|
5245
|
+
if (radioTargetId !== null) {
|
|
5198
5246
|
logger.info(`${formatStepPrefix(stepIndex, totalSteps)} resolved by radio primitive`);
|
|
5199
|
-
trajectory?.push({ stepIndex, verifiedBy: "dom" });
|
|
5247
|
+
trajectory?.push({ stepIndex, verifiedBy: "dom", targetId: radioTargetId });
|
|
5200
5248
|
return "completed";
|
|
5201
5249
|
}
|
|
5202
5250
|
// On a CATCH-ALL step ("for any remaining … question"), fill every
|