@nuanu-ai/agentbrowse 0.2.7 → 0.2.8
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/README.md +36 -8
- package/dist/agentpay-stagehand-llm.d.ts.map +1 -1
- package/dist/agentpay-stagehand-llm.js +5 -1
- package/dist/commands/act.d.ts +6 -2
- package/dist/commands/act.d.ts.map +1 -1
- package/dist/commands/act.js +840 -55
- package/dist/commands/act.test-harness.d.ts +19 -0
- package/dist/commands/act.test-harness.d.ts.map +1 -0
- package/dist/commands/act.test-harness.js +245 -0
- package/dist/commands/action-acceptance.d.ts +90 -0
- package/dist/commands/action-acceptance.d.ts.map +1 -0
- package/dist/commands/action-acceptance.js +1411 -0
- package/dist/commands/action-artifacts.d.ts +33 -0
- package/dist/commands/action-artifacts.d.ts.map +1 -0
- package/dist/commands/action-artifacts.js +104 -0
- package/dist/commands/action-execution-guards.d.ts +5 -0
- package/dist/commands/action-execution-guards.d.ts.map +1 -0
- package/dist/commands/action-execution-guards.js +3 -0
- package/dist/commands/action-executor-helpers.d.ts +21 -0
- package/dist/commands/action-executor-helpers.d.ts.map +1 -0
- package/dist/commands/action-executor-helpers.js +242 -0
- package/dist/commands/action-executor.d.ts +12 -0
- package/dist/commands/action-executor.d.ts.map +1 -0
- package/dist/commands/action-executor.js +45 -0
- package/dist/commands/action-fallbacks.d.ts +6 -0
- package/dist/commands/action-fallbacks.d.ts.map +1 -0
- package/dist/commands/action-fallbacks.js +43 -0
- package/dist/commands/action-value-projection.d.ts +32 -0
- package/dist/commands/action-value-projection.d.ts.map +1 -0
- package/dist/commands/action-value-projection.js +151 -0
- package/dist/commands/browse-actions.d.ts +4 -0
- package/dist/commands/browse-actions.d.ts.map +1 -0
- package/dist/commands/browse-actions.js +4 -0
- package/dist/commands/captcha-solve.d.ts.map +1 -1
- package/dist/commands/captcha-solve.js +13 -3
- package/dist/commands/click-action-executor.d.ts +10 -0
- package/dist/commands/click-action-executor.d.ts.map +1 -0
- package/dist/commands/click-action-executor.js +68 -0
- package/dist/commands/create-intent.d.ts +6 -0
- package/dist/commands/create-intent.d.ts.map +1 -0
- package/dist/commands/create-intent.js +75 -0
- package/dist/commands/datepicker-action-executor.d.ts +12 -0
- package/dist/commands/datepicker-action-executor.d.ts.map +1 -0
- package/dist/commands/datepicker-action-executor.js +218 -0
- package/dist/commands/descriptor-validation.d.ts +27 -0
- package/dist/commands/descriptor-validation.d.ts.map +1 -0
- package/dist/commands/descriptor-validation.js +333 -0
- package/dist/commands/extract-scope-resolution.d.ts +20 -0
- package/dist/commands/extract-scope-resolution.d.ts.map +1 -0
- package/dist/commands/extract-scope-resolution.js +100 -0
- package/dist/commands/extract-stagehand-executor.d.ts +17 -0
- package/dist/commands/extract-stagehand-executor.d.ts.map +1 -0
- package/dist/commands/extract-stagehand-executor.js +18 -0
- package/dist/commands/extract.d.ts +3 -2
- package/dist/commands/extract.d.ts.map +1 -1
- package/dist/commands/extract.js +256 -39
- package/dist/commands/fill-secret.d.ts +7 -0
- package/dist/commands/fill-secret.d.ts.map +1 -0
- package/dist/commands/fill-secret.js +371 -0
- package/dist/commands/get-secrets-catalog.d.ts +6 -0
- package/dist/commands/get-secrets-catalog.d.ts.map +1 -0
- package/dist/commands/get-secrets-catalog.js +23 -0
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +41 -7
- package/dist/commands/navigate.d.ts +2 -1
- package/dist/commands/navigate.d.ts.map +1 -1
- package/dist/commands/navigate.js +49 -12
- package/dist/commands/observe-inventory.d.ts +109 -0
- package/dist/commands/observe-inventory.d.ts.map +1 -0
- package/dist/commands/observe-inventory.js +2837 -0
- package/dist/commands/observe-persistence.d.ts +14 -0
- package/dist/commands/observe-persistence.d.ts.map +1 -0
- package/dist/commands/observe-persistence.js +170 -0
- package/dist/commands/observe-projection.d.ts +84 -0
- package/dist/commands/observe-projection.d.ts.map +1 -0
- package/dist/commands/observe-projection.js +140 -0
- package/dist/commands/observe-protected.d.ts +5 -0
- package/dist/commands/observe-protected.d.ts.map +1 -0
- package/dist/commands/observe-protected.js +18 -0
- package/dist/commands/observe-semantics.d.ts +10 -0
- package/dist/commands/observe-semantics.d.ts.map +1 -0
- package/dist/commands/observe-semantics.js +338 -0
- package/dist/commands/observe-stagehand.d.ts +48 -0
- package/dist/commands/observe-stagehand.d.ts.map +1 -0
- package/dist/commands/observe-stagehand.js +105 -0
- package/dist/commands/observe-surfaces.d.ts +9 -0
- package/dist/commands/observe-surfaces.d.ts.map +1 -0
- package/dist/commands/observe-surfaces.js +195 -0
- package/dist/commands/observe.d.ts +47 -1
- package/dist/commands/observe.d.ts.map +1 -1
- package/dist/commands/observe.js +173 -20
- package/dist/commands/observe.test-harness.d.ts +67 -0
- package/dist/commands/observe.test-harness.d.ts.map +1 -0
- package/dist/commands/observe.test-harness.js +107 -0
- package/dist/commands/poll-intent.d.ts +6 -0
- package/dist/commands/poll-intent.d.ts.map +1 -0
- package/dist/commands/poll-intent.js +57 -0
- package/dist/commands/screenshot.d.ts +2 -1
- package/dist/commands/screenshot.d.ts.map +1 -1
- package/dist/commands/screenshot.js +44 -12
- package/dist/commands/select-action-executor.d.ts +10 -0
- package/dist/commands/select-action-executor.d.ts.map +1 -0
- package/dist/commands/select-action-executor.js +91 -0
- package/dist/commands/semantic-observe.d.ts +24 -0
- package/dist/commands/semantic-observe.d.ts.map +1 -0
- package/dist/commands/semantic-observe.js +344 -0
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +75 -2
- package/dist/commands/structured-grid-action-executor.d.ts +3 -0
- package/dist/commands/structured-grid-action-executor.d.ts.map +1 -0
- package/dist/commands/structured-grid-action-executor.js +4 -0
- package/dist/commands/target-resolution.d.ts +4 -0
- package/dist/commands/target-resolution.d.ts.map +1 -0
- package/dist/commands/target-resolution.js +33 -0
- package/dist/commands/text-input-action-executor.d.ts +5 -0
- package/dist/commands/text-input-action-executor.d.ts.map +1 -0
- package/dist/commands/text-input-action-executor.js +116 -0
- package/dist/commands/user-actionable.d.ts +4 -0
- package/dist/commands/user-actionable.d.ts.map +1 -0
- package/dist/commands/user-actionable.js +95 -0
- package/dist/control-semantics.d.ts +29 -0
- package/dist/control-semantics.d.ts.map +1 -0
- package/dist/control-semantics.js +299 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +95 -32
- package/dist/output.d.ts +14 -2
- package/dist/output.d.ts.map +1 -1
- package/dist/output.js +17 -29
- package/dist/playwright-runtime.d.ts +35 -0
- package/dist/playwright-runtime.d.ts.map +1 -0
- package/dist/playwright-runtime.js +224 -0
- package/dist/runtime-resolution.d.ts +9 -0
- package/dist/runtime-resolution.d.ts.map +1 -0
- package/dist/runtime-resolution.js +19 -0
- package/dist/runtime-state.d.ts +217 -0
- package/dist/runtime-state.d.ts.map +1 -0
- package/dist/runtime-state.js +629 -0
- package/dist/secrets/backend.d.ts +32 -0
- package/dist/secrets/backend.d.ts.map +1 -0
- package/dist/secrets/backend.js +169 -0
- package/dist/secrets/catalog-applicability.d.ts +5 -0
- package/dist/secrets/catalog-applicability.d.ts.map +1 -0
- package/dist/secrets/catalog-applicability.js +59 -0
- package/dist/secrets/catalog-sync.d.ts +14 -0
- package/dist/secrets/catalog-sync.d.ts.map +1 -0
- package/dist/secrets/catalog-sync.js +35 -0
- package/dist/secrets/field-policy.d.ts +3 -0
- package/dist/secrets/field-policy.d.ts.map +1 -0
- package/dist/secrets/field-policy.js +3 -0
- package/dist/secrets/fill-ordering.d.ts +11 -0
- package/dist/secrets/fill-ordering.d.ts.map +1 -0
- package/dist/secrets/fill-ordering.js +44 -0
- package/dist/secrets/form-matcher.d.ts +60 -0
- package/dist/secrets/form-matcher.d.ts.map +1 -0
- package/dist/secrets/form-matcher.js +596 -0
- package/dist/secrets/intent-output.d.ts +11 -0
- package/dist/secrets/intent-output.d.ts.map +1 -0
- package/dist/secrets/intent-output.js +64 -0
- package/dist/secrets/mock-agentpay-backend.d.ts +13 -0
- package/dist/secrets/mock-agentpay-backend.d.ts.map +1 -0
- package/dist/secrets/mock-agentpay-backend.js +87 -0
- package/dist/secrets/mock-agentpay-cabinet.d.ts +43 -0
- package/dist/secrets/mock-agentpay-cabinet.d.ts.map +1 -0
- package/dist/secrets/mock-agentpay-cabinet.js +195 -0
- package/dist/secrets/protected-artifact-guard.d.ts +25 -0
- package/dist/secrets/protected-artifact-guard.d.ts.map +1 -0
- package/dist/secrets/protected-artifact-guard.js +26 -0
- package/dist/secrets/protected-bindings.d.ts +10 -0
- package/dist/secrets/protected-bindings.d.ts.map +1 -0
- package/dist/secrets/protected-bindings.js +17 -0
- package/dist/secrets/protected-field-values.d.ts +13 -0
- package/dist/secrets/protected-field-values.d.ts.map +1 -0
- package/dist/secrets/protected-field-values.js +100 -0
- package/dist/secrets/protected-fill.d.ts +47 -0
- package/dist/secrets/protected-fill.d.ts.map +1 -0
- package/dist/secrets/protected-fill.js +512 -0
- package/dist/secrets/types.d.ts +84 -0
- package/dist/secrets/types.d.ts.map +1 -0
- package/dist/secrets/types.js +27 -0
- package/dist/session.d.ts +22 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +74 -2
- package/dist/solver/browser-launcher.d.ts.map +1 -1
- package/dist/solver/browser-launcher.js +6 -3
- package/dist/stagehand-runtime.d.ts +4 -0
- package/dist/stagehand-runtime.d.ts.map +1 -0
- package/dist/stagehand-runtime.js +10 -0
- package/dist/stagehand.d.ts +0 -5
- package/dist/stagehand.d.ts.map +1 -1
- package/dist/stagehand.js +0 -6
- package/package.json +5 -2
package/dist/commands/act.js
CHANGED
|
@@ -1,73 +1,858 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* browse act
|
|
2
|
+
* browse act <targetRef> <action> [value] — Perform a deterministic action on a stored target.
|
|
3
3
|
*/
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
4
|
+
import { saveSession } from '../session.js';
|
|
5
|
+
import { clearProtectedExposure, getProtectedExposure, getSurface, getTarget, incrementMetric, locatorCandidateKey, markTargetLifecycle, recordActionResult, registerPage, setTargetAvailability, setCurrentPage, updateTarget, } from '../runtime-state.js';
|
|
6
|
+
import { outputContractFailure, outputFailure, outputJSON } from '../output.js';
|
|
7
|
+
import { capturePageObservation, captureLocatorContextHash, captureLocatorState, createAcceptanceProbe, diagnoseNoObservableProgress, locatorStateChanged, pageObservationChanged, rankLocatorCandidates, shouldVerifyObservableProgress, waitForAcceptanceProbe, } from './action-acceptance.js';
|
|
8
|
+
import { captureActionFailureArtifacts, startActionTrace } from './action-artifacts.js';
|
|
9
|
+
import { buildLocator, resolveLocatorRoot } from './action-fallbacks.js';
|
|
10
|
+
import { projectActionValue } from './action-value-projection.js';
|
|
11
|
+
import { applyActionWithFallbacks } from './action-executor.js';
|
|
12
|
+
import { BROWSE_ACTIONS, isBrowseAction } from './browse-actions.js';
|
|
13
|
+
import { isCompatibleMutableFieldBinding, normalizePageSignature, readLocatorBindingSnapshot, readLocatorDomSignature, } from './descriptor-validation.js';
|
|
14
|
+
import { isLocatorUserActionable } from './user-actionable.js';
|
|
15
|
+
import { resolveSurfaceScopeRoot } from './target-resolution.js';
|
|
16
|
+
import { buildProtectedArtifactsSuppressed } from '../secrets/protected-artifact-guard.js';
|
|
17
|
+
import { connectPlaywright, disconnectPlaywright, listPages, resolvePageByRef, syncSessionPage, } from '../playwright-runtime.js';
|
|
18
|
+
function ensureValue(action, value) {
|
|
19
|
+
if (action === 'click')
|
|
20
|
+
return undefined;
|
|
21
|
+
if (typeof value === 'string' && value.length > 0)
|
|
22
|
+
return value;
|
|
23
|
+
throw new Error(`Act value is required for action: ${action}`);
|
|
24
|
+
}
|
|
25
|
+
function emitActPreflightFailure(params) {
|
|
26
|
+
return outputContractFailure({
|
|
27
|
+
error: params.error,
|
|
28
|
+
outcomeType: params.outcomeType,
|
|
29
|
+
message: params.message,
|
|
30
|
+
reason: params.reason,
|
|
31
|
+
targetRef: params.targetRef,
|
|
32
|
+
action: params.action,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function reasonForNoObservableProgressDiagnosis(diagnosis) {
|
|
36
|
+
switch (diagnosis.kind) {
|
|
37
|
+
case 'validation-blocked':
|
|
38
|
+
return 'The page stayed in place and surfaced validation blockers after the action.';
|
|
39
|
+
case 'target-blocked':
|
|
40
|
+
return 'The target stayed in place but remained blocked or unchanged after the action.';
|
|
41
|
+
case 'overlay-blocked':
|
|
42
|
+
return 'A blocking overlay prevented the action from producing observable progress.';
|
|
43
|
+
case 'site-noop':
|
|
44
|
+
return 'The page stayed in place and did not expose any observable progress after the action.';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function describeActFailure(params) {
|
|
48
|
+
if (params.staleReason) {
|
|
49
|
+
const reason = params.staleReason === 'page-signature-mismatch'
|
|
50
|
+
? 'The page changed since this target was observed, so the saved page signature no longer matches.'
|
|
51
|
+
: params.staleReason === 'dom-signature-mismatch'
|
|
52
|
+
? 'The target changed since it was observed, so the saved DOM signature no longer matches.'
|
|
53
|
+
: 'The saved locator no longer resolves to a live element on the page.';
|
|
54
|
+
return {
|
|
55
|
+
outcomeType: 'binding_stale',
|
|
56
|
+
message: 'The requested action cannot continue because the target is stale.',
|
|
57
|
+
reason,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (params.diagnosis) {
|
|
61
|
+
return {
|
|
62
|
+
outcomeType: 'blocked',
|
|
63
|
+
message: 'The action did not produce observable progress.',
|
|
64
|
+
reason: reasonForNoObservableProgressDiagnosis(params.diagnosis),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
const rawReason = params.failureMessage.replace(/^Act failed:\s*/, '');
|
|
68
|
+
if (rawReason === 'target_disabled') {
|
|
69
|
+
return {
|
|
70
|
+
outcomeType: 'blocked',
|
|
71
|
+
message: 'The requested action cannot continue because the target is disabled.',
|
|
72
|
+
reason: 'The runtime resolved the target, but the browser marked it as disabled.',
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
if (rawReason === 'target_readonly') {
|
|
76
|
+
return {
|
|
77
|
+
outcomeType: 'blocked',
|
|
78
|
+
message: 'The requested action cannot continue because the target is read-only.',
|
|
79
|
+
reason: 'The runtime resolved the target, but the browser marked it as read-only.',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
if (rawReason === 'target_surface_inactive') {
|
|
83
|
+
return {
|
|
84
|
+
outcomeType: 'blocked',
|
|
85
|
+
message: 'The requested action cannot continue because the target surface is inactive.',
|
|
86
|
+
reason: 'The target belongs to a surface that is no longer active or available.',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
outcomeType: 'blocked',
|
|
91
|
+
message: 'The requested action could not be completed.',
|
|
92
|
+
reason: rawReason,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export { BROWSE_ACTIONS, isBrowseAction };
|
|
96
|
+
function redactNoObservableProgressDiagnosis(diagnosis) {
|
|
97
|
+
return {
|
|
98
|
+
kind: diagnosis.kind,
|
|
99
|
+
...(diagnosis.targetState ? { targetState: diagnosis.targetState } : {}),
|
|
100
|
+
...(diagnosis.messages.length > 0
|
|
101
|
+
? {
|
|
102
|
+
messagesRedacted: true,
|
|
103
|
+
messageCount: diagnosis.messages.length,
|
|
104
|
+
}
|
|
105
|
+
: {}),
|
|
106
|
+
...(diagnosis.invalidFields.length > 0
|
|
107
|
+
? {
|
|
108
|
+
invalidFieldsRedacted: true,
|
|
109
|
+
invalidFieldCount: diagnosis.invalidFields.length,
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
112
|
+
...(diagnosis.blockingOverlays.length > 0
|
|
113
|
+
? {
|
|
114
|
+
blockingOverlaysRedacted: true,
|
|
115
|
+
blockingOverlayCount: diagnosis.blockingOverlays.length,
|
|
116
|
+
}
|
|
117
|
+
: {}),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function isEditableLikeTarget(target) {
|
|
121
|
+
if (target.controlFamily === 'text-input' ||
|
|
122
|
+
target.controlFamily === 'select' ||
|
|
123
|
+
target.controlFamily === 'datepicker') {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
const kind = (target.kind ?? '').toLowerCase();
|
|
127
|
+
const role = (target.semantics?.role ?? '').toLowerCase();
|
|
128
|
+
return (['input', 'textarea', 'select', 'combobox'].includes(kind) ||
|
|
129
|
+
['textbox', 'combobox', 'searchbox', 'spinbutton'].includes(role) ||
|
|
130
|
+
target.allowedActions.includes('fill') ||
|
|
131
|
+
target.allowedActions.includes('type') ||
|
|
132
|
+
target.allowedActions.includes('select'));
|
|
133
|
+
}
|
|
134
|
+
function shouldWatchForNewPageAfterAction(target, action) {
|
|
135
|
+
if (action === 'click') {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
if (action !== 'press') {
|
|
24
139
|
return false;
|
|
25
140
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
141
|
+
return !isEditableLikeTarget(target);
|
|
142
|
+
}
|
|
143
|
+
function targetUsesSurfaceAsPrimaryLocator(target, surface) {
|
|
144
|
+
const surfaceCandidates = new Set(surface.locatorCandidates.map(locatorCandidateKey));
|
|
145
|
+
return target.locatorCandidates.some((candidate) => surfaceCandidates.has(locatorCandidateKey(candidate)));
|
|
146
|
+
}
|
|
147
|
+
async function resolveActionRoot(page, target, attempts, surface) {
|
|
148
|
+
const baseRoot = resolveLocatorRoot(page, target.framePath ?? surface?.framePath);
|
|
149
|
+
if (!surface) {
|
|
150
|
+
return {
|
|
151
|
+
baseRoot,
|
|
152
|
+
locatorRoot: baseRoot,
|
|
153
|
+
surfaceRoot: null,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
const surfaceRoot = await resolveSurfaceScopeRoot(page, surface, attempts);
|
|
157
|
+
if (surfaceRoot) {
|
|
158
|
+
if (targetUsesSurfaceAsPrimaryLocator(target, surface)) {
|
|
159
|
+
attempts.push('surface.resolve.self-target');
|
|
160
|
+
return {
|
|
161
|
+
baseRoot,
|
|
162
|
+
locatorRoot: baseRoot,
|
|
163
|
+
surfaceRoot,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
baseRoot,
|
|
168
|
+
locatorRoot: surfaceRoot,
|
|
169
|
+
surfaceRoot,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
attempts.push('surface.resolve.fallback:page');
|
|
173
|
+
return {
|
|
174
|
+
baseRoot,
|
|
175
|
+
locatorRoot: baseRoot,
|
|
176
|
+
surfaceRoot: null,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
function resolveLocatorRootForCandidate(baseRoot, defaultRoot, surfaceRoot, candidate) {
|
|
180
|
+
if (candidate.scope === 'root') {
|
|
181
|
+
return baseRoot;
|
|
182
|
+
}
|
|
183
|
+
if (candidate.scope === 'surface') {
|
|
184
|
+
return surfaceRoot;
|
|
185
|
+
}
|
|
186
|
+
return defaultRoot;
|
|
187
|
+
}
|
|
188
|
+
async function prepareLocatorForAction(locator, action, strategy, attempts, options) {
|
|
189
|
+
const count = await locator.count().catch(() => 0);
|
|
190
|
+
if (count === 0) {
|
|
191
|
+
attempts.push(`resolve.skip:${strategy}:empty`);
|
|
192
|
+
return { locator: null };
|
|
193
|
+
}
|
|
194
|
+
if (action !== 'click' && count > 1) {
|
|
195
|
+
attempts.push(`resolve.skip:${strategy}:ambiguous:${count}`);
|
|
196
|
+
return { locator: null };
|
|
197
|
+
}
|
|
198
|
+
let resolvedLocator = locator.first();
|
|
199
|
+
if (action === 'click' && count > 1) {
|
|
200
|
+
const visibleCandidates = [];
|
|
201
|
+
for (let index = 0; index < count; index += 1) {
|
|
202
|
+
const candidate = locator.nth(index);
|
|
203
|
+
const visible = await isLocatorUserActionable(candidate);
|
|
204
|
+
if (!visible) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
visibleCandidates.push(candidate);
|
|
208
|
+
}
|
|
209
|
+
if (visibleCandidates.length === 1) {
|
|
210
|
+
attempts.push(`resolve.visible-unique:${strategy}`);
|
|
211
|
+
resolvedLocator = visibleCandidates[0] ?? locator.first();
|
|
212
|
+
}
|
|
213
|
+
else if (visibleCandidates.length > 1) {
|
|
214
|
+
attempts.push(`resolve.skip:${strategy}:ambiguous-visible:${visibleCandidates.length}`);
|
|
215
|
+
return { locator: null };
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
attempts.push(`resolve.skip:${strategy}:hidden`);
|
|
219
|
+
return { locator: null };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const visible = await isLocatorUserActionable(resolvedLocator);
|
|
223
|
+
if (!visible) {
|
|
224
|
+
attempts.push(`resolve.skip:${strategy}:hidden`);
|
|
225
|
+
return { locator: null };
|
|
226
|
+
}
|
|
227
|
+
const disabled = await resolvedLocator.isDisabled?.().catch(() => false);
|
|
228
|
+
if (disabled) {
|
|
229
|
+
attempts.push(`resolve.skip:${strategy}:disabled`);
|
|
230
|
+
return { locator: null, blockedReason: 'disabled' };
|
|
231
|
+
}
|
|
232
|
+
if (action === 'fill' || action === 'type' || options?.allowDescendantPressFallback) {
|
|
233
|
+
const editable = await resolvedLocator.isEditable().catch(() => false);
|
|
234
|
+
if (!editable) {
|
|
235
|
+
const descendantSelector = options?.allowDescendantPressFallback
|
|
236
|
+
? 'input:not([type="hidden"]), textarea, select, [contenteditable="true"], [role="textbox"], [role="combobox"]'
|
|
237
|
+
: 'input:not([type="hidden"]), textarea, select, [contenteditable="true"]';
|
|
238
|
+
const descendantCandidates = resolvedLocator.locator(descendantSelector);
|
|
239
|
+
const descendantCount = await descendantCandidates.count().catch(() => 0);
|
|
240
|
+
const candidateDescendants = [];
|
|
241
|
+
for (let index = 0; index < descendantCount; index += 1) {
|
|
242
|
+
const descendant = descendantCandidates.nth(index);
|
|
243
|
+
const descendantVisible = await isLocatorUserActionable(descendant);
|
|
244
|
+
if (!descendantVisible)
|
|
245
|
+
continue;
|
|
246
|
+
if (!options?.allowDescendantPressFallback || action === 'fill' || action === 'type') {
|
|
247
|
+
const descendantEditable = await descendant.isEditable().catch(() => false);
|
|
248
|
+
if (!descendantEditable)
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
candidateDescendants.push(descendant);
|
|
252
|
+
}
|
|
253
|
+
if (candidateDescendants.length === 1) {
|
|
254
|
+
attempts.push(options?.allowDescendantPressFallback && action === 'press'
|
|
255
|
+
? `resolve.descendant-press:${strategy}`
|
|
256
|
+
: `resolve.descendant-editable:${strategy}`);
|
|
257
|
+
return { locator: candidateDescendants[0] ?? null };
|
|
258
|
+
}
|
|
259
|
+
if (candidateDescendants.length > 1) {
|
|
260
|
+
attempts.push(`resolve.skip:${strategy}:descendant-ambiguous:${candidateDescendants.length}`);
|
|
261
|
+
return { locator: null };
|
|
262
|
+
}
|
|
263
|
+
if (options?.allowReadonlyFallback) {
|
|
264
|
+
attempts.push(`resolve.readonly-fallback:${strategy}`);
|
|
265
|
+
return { locator: resolvedLocator };
|
|
266
|
+
}
|
|
267
|
+
attempts.push(`resolve.skip:${strategy}:readonly`);
|
|
268
|
+
return { locator: null, blockedReason: 'readonly' };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { locator: resolvedLocator };
|
|
30
272
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
273
|
+
async function recoverLocatorFromSurfaceRoot(locatorRoot, target, action, attempts) {
|
|
274
|
+
if (action !== 'press' ||
|
|
275
|
+
!(target.controlFamily === 'text-input' ||
|
|
276
|
+
target.controlFamily === 'select' ||
|
|
277
|
+
target.controlFamily === 'datepicker') ||
|
|
278
|
+
typeof locatorRoot.locator !== 'function') {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const descendants = locatorRoot.locator('input:not([type="hidden"]), textarea, select, [contenteditable="true"], [role="textbox"], [role="combobox"]');
|
|
282
|
+
const count = await descendants.count().catch(() => 0);
|
|
283
|
+
const visibleDescendants = [];
|
|
284
|
+
for (let index = 0; index < count; index += 1) {
|
|
285
|
+
const descendant = descendants.nth(index);
|
|
286
|
+
const visible = await isLocatorUserActionable(descendant);
|
|
287
|
+
if (!visible) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
visibleDescendants.push(descendant);
|
|
291
|
+
}
|
|
292
|
+
if (visibleDescendants.length === 1) {
|
|
293
|
+
attempts.push('resolve.surface-descendant:press');
|
|
294
|
+
return visibleDescendants[0] ?? null;
|
|
295
|
+
}
|
|
296
|
+
if (visibleDescendants.length > 1) {
|
|
297
|
+
attempts.push(`resolve.skip:surface-descendant-ambiguous:${visibleDescendants.length}`);
|
|
298
|
+
}
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
async function capturePopupIfOpened(session, beforePages, afterPages, currentPageRef, attempts) {
|
|
302
|
+
const popup = afterPages.find((page) => !beforePages.includes(page));
|
|
303
|
+
if (!popup)
|
|
304
|
+
return null;
|
|
305
|
+
const [url, title] = await Promise.all([
|
|
306
|
+
Promise.resolve(popup.url()),
|
|
307
|
+
popup.title().catch(() => ''),
|
|
308
|
+
]);
|
|
309
|
+
const page = registerPage(session, {
|
|
310
|
+
url,
|
|
311
|
+
title,
|
|
312
|
+
openerPageRef: currentPageRef,
|
|
313
|
+
makeCurrent: true,
|
|
314
|
+
});
|
|
315
|
+
attempts.push('popup-captured');
|
|
316
|
+
return page.pageRef;
|
|
317
|
+
}
|
|
318
|
+
async function waitForPopup(context) {
|
|
319
|
+
try {
|
|
320
|
+
return await context.waitForEvent('page', { timeout: 500 });
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
async function waitForLatePage(browser, beforePages, timeoutMs = 2_000) {
|
|
327
|
+
const startedAt = Date.now();
|
|
328
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
329
|
+
const candidate = listPages(browser).find((page) => !beforePages.includes(page));
|
|
330
|
+
if (candidate) {
|
|
331
|
+
return candidate;
|
|
332
|
+
}
|
|
333
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
334
|
+
}
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
export async function act(session, targetRef, action, value) {
|
|
338
|
+
const target = getTarget(session, targetRef);
|
|
339
|
+
if (!target) {
|
|
340
|
+
return emitActPreflightFailure({
|
|
341
|
+
error: 'unknown_target_ref',
|
|
342
|
+
outcomeType: 'blocked',
|
|
343
|
+
message: 'The requested targetRef is unknown.',
|
|
344
|
+
reason: `No stored target matches targetRef ${targetRef}.`,
|
|
345
|
+
targetRef,
|
|
346
|
+
action,
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (target.lifecycle !== 'live') {
|
|
350
|
+
return emitActPreflightFailure({
|
|
351
|
+
error: 'stale_target_ref',
|
|
352
|
+
outcomeType: 'binding_stale',
|
|
353
|
+
message: 'The requested target is no longer live.',
|
|
354
|
+
reason: `Target ${targetRef} is ${target.lifecycle}${target.lifecycleReason ? ` because ${target.lifecycleReason}` : ''}.`,
|
|
355
|
+
targetRef,
|
|
356
|
+
action,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
if (target.capability !== 'actionable') {
|
|
360
|
+
return emitActPreflightFailure({
|
|
361
|
+
error: 'target_not_actionable',
|
|
362
|
+
outcomeType: 'unsupported',
|
|
363
|
+
message: 'The requested target cannot be used for actions.',
|
|
364
|
+
reason: `Target ${targetRef} has capability ${target.capability}, not actionable.`,
|
|
365
|
+
targetRef,
|
|
366
|
+
action,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (!target.allowedActions.includes(action)) {
|
|
370
|
+
return emitActPreflightFailure({
|
|
371
|
+
error: 'action_not_allowed_for_target',
|
|
372
|
+
outcomeType: 'unsupported',
|
|
373
|
+
message: 'The requested action is not allowed for this target.',
|
|
374
|
+
reason: `Target ${targetRef} allows ${target.allowedActions.join(', ')}, not ${action}.`,
|
|
375
|
+
targetRef,
|
|
376
|
+
action,
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
if (target.availability.state === 'gated' &&
|
|
380
|
+
(target.availability.reason === 'occupied' || target.availability.reason === 'not-selectable')) {
|
|
381
|
+
return emitActPreflightFailure({
|
|
382
|
+
error: 'target_gated',
|
|
383
|
+
outcomeType: 'blocked',
|
|
384
|
+
message: 'The requested target is currently gated.',
|
|
385
|
+
reason: `Target ${targetRef} is gated${target.availability.reason ? ` because ${target.availability.reason}` : ''}.`,
|
|
386
|
+
targetRef,
|
|
387
|
+
action,
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
const surface = target.surfaceRef ? getSurface(session, target.surfaceRef) : null;
|
|
391
|
+
if (surface && surface.lifecycle !== 'live') {
|
|
392
|
+
setTargetAvailability(session, targetRef, 'surface-inactive', surface.lifecycleReason ?? `surface-${surface.lifecycle}`);
|
|
393
|
+
saveSession(session);
|
|
394
|
+
return emitActPreflightFailure({
|
|
395
|
+
error: 'target_surface_not_live',
|
|
396
|
+
outcomeType: 'blocked',
|
|
397
|
+
message: 'The requested target surface is no longer live.',
|
|
398
|
+
reason: `Surface ${surface.ref} is ${surface.lifecycle}${surface.lifecycleReason ? ` because ${surface.lifecycleReason}` : ''}.`,
|
|
399
|
+
targetRef,
|
|
400
|
+
action,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
if (surface && surface.availability.state !== 'available') {
|
|
404
|
+
setTargetAvailability(session, targetRef, 'surface-inactive', surface.availability.reason ?? `surface-${surface.availability.state}`);
|
|
405
|
+
saveSession(session);
|
|
406
|
+
return emitActPreflightFailure({
|
|
407
|
+
error: 'target_surface_unavailable',
|
|
408
|
+
outcomeType: 'blocked',
|
|
409
|
+
message: 'The requested target surface is not currently available.',
|
|
410
|
+
reason: `Surface ${surface.ref} is ${surface.availability.state}${surface.availability.reason ? ` because ${surface.availability.reason}` : ''}.`,
|
|
411
|
+
targetRef,
|
|
412
|
+
action,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const actionValue = ensureValue(action, value);
|
|
416
|
+
const attempts = [];
|
|
417
|
+
const startedAt = Date.now();
|
|
418
|
+
let browser = null;
|
|
35
419
|
let failureMessage = null;
|
|
420
|
+
let failureArtifacts;
|
|
421
|
+
let currentPage = null;
|
|
422
|
+
let currentPageRef = target.pageRef;
|
|
423
|
+
const startingPageUrl = session.runtime?.pages?.[target.pageRef]?.url ?? null;
|
|
424
|
+
const protectedExposureAtStart = getProtectedExposure(session, target.pageRef);
|
|
425
|
+
let locatorStrategy = null;
|
|
426
|
+
let recoveredAfterError = false;
|
|
427
|
+
let recoveredAcceptancePolicy = null;
|
|
428
|
+
let staleReason = null;
|
|
429
|
+
let progressProbe = null;
|
|
430
|
+
let noProgressDiagnosis = null;
|
|
431
|
+
let liveTarget = target;
|
|
432
|
+
let trace = {
|
|
433
|
+
finishSuccess: async () => { },
|
|
434
|
+
finishFailure: async (_artifactDir) => undefined,
|
|
435
|
+
};
|
|
36
436
|
try {
|
|
37
|
-
|
|
437
|
+
browser = await connectPlaywright(session.cdpUrl);
|
|
38
438
|
}
|
|
39
439
|
catch (err) {
|
|
40
|
-
return
|
|
440
|
+
return emitActPreflightFailure({
|
|
441
|
+
error: 'browser_connection_failed',
|
|
442
|
+
outcomeType: 'blocked',
|
|
443
|
+
message: 'The action could not start because AgentBrowse failed to connect to the browser.',
|
|
444
|
+
reason: err instanceof Error ? err.message : String(err),
|
|
445
|
+
targetRef,
|
|
446
|
+
action,
|
|
447
|
+
});
|
|
41
448
|
}
|
|
42
449
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const message = typeof result.message === 'string' && result.message.trim().length > 0
|
|
54
|
-
? result.message.trim()
|
|
55
|
-
: `Stagehand could not complete the requested action: ${instruction}`;
|
|
56
|
-
failureMessage = `Act failed: ${message} (page: ${title} — ${url})`;
|
|
450
|
+
const page = await resolvePageByRef(browser, session, target.pageRef);
|
|
451
|
+
currentPage = page;
|
|
452
|
+
setCurrentPage(session, target.pageRef);
|
|
453
|
+
const { url } = await syncSessionPage(session, target.pageRef, page);
|
|
454
|
+
trace = await startActionTrace(page, {
|
|
455
|
+
suppressSensitiveArtifacts: Boolean(protectedExposureAtStart),
|
|
456
|
+
});
|
|
457
|
+
if (liveTarget.pageSignature && normalizePageSignature(url) !== liveTarget.pageSignature) {
|
|
458
|
+
staleReason = 'page-signature-mismatch';
|
|
459
|
+
throw new Error('stale_target_page_signature_changed');
|
|
57
460
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
461
|
+
const tryRebindMutableFieldTarget = async (resolvedLocator, strategy) => {
|
|
462
|
+
if (!liveTarget.domSignature) {
|
|
463
|
+
return false;
|
|
464
|
+
}
|
|
465
|
+
const snapshot = await readLocatorBindingSnapshot(resolvedLocator).catch(() => null);
|
|
466
|
+
if (!snapshot?.domSignature || snapshot.domSignature === liveTarget.domSignature) {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
if (!isCompatibleMutableFieldBinding(liveTarget, snapshot)) {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
attempts.push(`domSignature.rebound:${strategy}`);
|
|
473
|
+
const updatedTarget = updateTarget(session, targetRef, {
|
|
474
|
+
domSignature: snapshot.domSignature,
|
|
475
|
+
label: snapshot.label ?? liveTarget.label,
|
|
476
|
+
lifecycle: 'live',
|
|
477
|
+
lifecycleReason: undefined,
|
|
478
|
+
availability: { state: 'available' },
|
|
479
|
+
semantics: {
|
|
480
|
+
...liveTarget.semantics,
|
|
481
|
+
name: snapshot.label ?? liveTarget.semantics?.name,
|
|
482
|
+
role: snapshot.role ?? liveTarget.semantics?.role,
|
|
483
|
+
},
|
|
65
484
|
});
|
|
485
|
+
if (updatedTarget) {
|
|
486
|
+
liveTarget = updatedTarget;
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
};
|
|
490
|
+
const assertResolvedTargetStillValid = async (resolvedLocator, stage) => {
|
|
491
|
+
if (liveTarget.pageSignature && normalizePageSignature(page.url()) !== liveTarget.pageSignature) {
|
|
492
|
+
attempts.push(`stale.page-signature:${stage}`);
|
|
493
|
+
staleReason = 'page-signature-mismatch';
|
|
494
|
+
throw new Error('stale_target_page_signature_changed');
|
|
495
|
+
}
|
|
496
|
+
const liveCount = await resolvedLocator.count().catch(() => 0);
|
|
497
|
+
if (liveCount === 0) {
|
|
498
|
+
attempts.push(`stale.locator:${stage}`);
|
|
499
|
+
staleReason = 'locator-resolution-failed';
|
|
500
|
+
throw new Error('stale_target_locator_resolution_failed');
|
|
501
|
+
}
|
|
502
|
+
if (liveTarget.domSignature) {
|
|
503
|
+
const rebound = await tryRebindMutableFieldTarget(resolvedLocator, stage);
|
|
504
|
+
if (!rebound) {
|
|
505
|
+
const liveSignature = await readLocatorDomSignature(resolvedLocator).catch(() => null);
|
|
506
|
+
if (liveSignature && liveSignature !== liveTarget.domSignature) {
|
|
507
|
+
attempts.push(`stale.dom-signature:${stage}`);
|
|
508
|
+
staleReason = 'dom-signature-mismatch';
|
|
509
|
+
throw new Error('stale_target_dom_signature_changed');
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
let resolvedBy = null;
|
|
515
|
+
const beforePages = listPages(browser);
|
|
516
|
+
const shouldCheckProgress = shouldVerifyObservableProgress(target, action);
|
|
517
|
+
const beforePageObservation = shouldCheckProgress ? await capturePageObservation(page) : null;
|
|
518
|
+
const { baseRoot, locatorRoot, surfaceRoot } = await resolveActionRoot(page, target, attempts, surface);
|
|
519
|
+
let lastError = null;
|
|
520
|
+
let sawDomSignatureMismatch = false;
|
|
521
|
+
let sawDisabledTarget = false;
|
|
522
|
+
let sawReadonlyTarget = false;
|
|
523
|
+
const attemptResolvedLocator = async (resolvedLocator, strategy, options) => {
|
|
524
|
+
if (!options?.skipDomSignature && liveTarget.domSignature) {
|
|
525
|
+
const rebound = await tryRebindMutableFieldTarget(resolvedLocator, strategy);
|
|
526
|
+
if (!rebound) {
|
|
527
|
+
const liveSignature = await readLocatorDomSignature(resolvedLocator);
|
|
528
|
+
if (liveSignature && liveSignature !== liveTarget.domSignature) {
|
|
529
|
+
attempts.push(`domSignature.mismatch:${strategy}`);
|
|
530
|
+
sawDomSignatureMismatch = true;
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
let acceptanceProbe = null;
|
|
536
|
+
try {
|
|
537
|
+
const valueProjection = await projectActionValue({
|
|
538
|
+
target,
|
|
539
|
+
action,
|
|
540
|
+
actionValue,
|
|
541
|
+
locator: resolvedLocator,
|
|
542
|
+
attempts,
|
|
543
|
+
});
|
|
544
|
+
const executionValue = valueProjection?.executionValue ?? actionValue;
|
|
545
|
+
const acceptanceValue = valueProjection?.acceptanceValue ?? actionValue;
|
|
546
|
+
acceptanceProbe = await createAcceptanceProbe({
|
|
547
|
+
session,
|
|
548
|
+
page,
|
|
549
|
+
target,
|
|
550
|
+
action,
|
|
551
|
+
actionValue: acceptanceValue,
|
|
552
|
+
locator: resolvedLocator,
|
|
553
|
+
beforePageObservation,
|
|
554
|
+
});
|
|
555
|
+
attempts.push(`resolve:${strategy}`);
|
|
556
|
+
const usedFallback = await applyActionWithFallbacks(page, locatorRoot, resolvedLocator, action, executionValue, attempts, target.controlFamily, {
|
|
557
|
+
guards: {
|
|
558
|
+
assertStillValid: async (stage) => {
|
|
559
|
+
await assertResolvedTargetStillValid(resolvedLocator, stage);
|
|
560
|
+
},
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
if (usedFallback) {
|
|
564
|
+
incrementMetric(session, 'fallbackActions');
|
|
565
|
+
}
|
|
566
|
+
resolvedBy = 'playwright-locator';
|
|
567
|
+
locatorStrategy = strategy;
|
|
568
|
+
progressProbe = acceptanceProbe;
|
|
569
|
+
setTargetAvailability(session, targetRef, 'available');
|
|
570
|
+
return true;
|
|
571
|
+
}
|
|
572
|
+
catch (err) {
|
|
573
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
574
|
+
const shouldAttemptAcceptanceRecovery = acceptanceProbe !== null &&
|
|
575
|
+
(acceptanceProbe.policy === 'value-change' ||
|
|
576
|
+
acceptanceProbe.policy === 'selection' ||
|
|
577
|
+
acceptanceProbe.policy === 'date-selection' ||
|
|
578
|
+
((acceptanceProbe.policy === 'navigation' || acceptanceProbe.policy === 'submit') &&
|
|
579
|
+
staleReason !== null &&
|
|
580
|
+
staleReason !== 'page-signature-mismatch'));
|
|
581
|
+
if (acceptanceProbe !== null && shouldAttemptAcceptanceRecovery) {
|
|
582
|
+
const acceptance = await waitForAcceptanceProbe(acceptanceProbe).catch(() => null);
|
|
583
|
+
if (acceptance?.polls && acceptance.polls > 1) {
|
|
584
|
+
attempts.push(`acceptance.polled:${acceptance.polls}`);
|
|
585
|
+
}
|
|
586
|
+
if (acceptance?.accepted) {
|
|
587
|
+
attempts.push(`acceptance.recovered:${acceptanceProbe.policy}`);
|
|
588
|
+
incrementMetric(session, 'fallbackActions');
|
|
589
|
+
resolvedBy = 'playwright-locator';
|
|
590
|
+
locatorStrategy = strategy;
|
|
591
|
+
progressProbe = null;
|
|
592
|
+
lastError = null;
|
|
593
|
+
recoveredAfterError = true;
|
|
594
|
+
recoveredAcceptancePolicy = acceptanceProbe.policy;
|
|
595
|
+
setTargetAvailability(session, targetRef, 'available');
|
|
596
|
+
return true;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if (staleReason) {
|
|
600
|
+
throw lastError;
|
|
601
|
+
}
|
|
602
|
+
await assertResolvedTargetStillValid(resolvedLocator, `after-error:${strategy}`);
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
};
|
|
606
|
+
const watchForNewPage = shouldWatchForNewPageAfterAction(target, action);
|
|
607
|
+
const popupPromise = watchForNewPage
|
|
608
|
+
? waitForPopup(page.context())
|
|
609
|
+
: Promise.resolve(null);
|
|
610
|
+
for (const candidate of rankLocatorCandidates(target.locatorCandidates, action)) {
|
|
611
|
+
const candidateRoot = resolveLocatorRootForCandidate(baseRoot, locatorRoot, surfaceRoot, candidate);
|
|
612
|
+
if (!candidateRoot) {
|
|
613
|
+
attempts.push(`resolve.skip:${candidate.strategy}:surface-unavailable`);
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
const locator = buildLocator(candidateRoot, candidate);
|
|
617
|
+
if (!locator)
|
|
618
|
+
continue;
|
|
619
|
+
const preparedLocator = await prepareLocatorForAction(locator, action, candidate.strategy, attempts, {
|
|
620
|
+
allowReadonlyFallback: action === 'fill' && target.controlFamily === 'datepicker',
|
|
621
|
+
allowDescendantPressFallback: action === 'press' &&
|
|
622
|
+
(target.controlFamily === 'text-input' ||
|
|
623
|
+
target.controlFamily === 'select' ||
|
|
624
|
+
target.controlFamily === 'datepicker'),
|
|
625
|
+
});
|
|
626
|
+
if (preparedLocator.blockedReason === 'disabled') {
|
|
627
|
+
sawDisabledTarget = true;
|
|
628
|
+
}
|
|
629
|
+
if (preparedLocator.blockedReason === 'readonly') {
|
|
630
|
+
sawReadonlyTarget = true;
|
|
631
|
+
}
|
|
632
|
+
const resolvedLocator = preparedLocator.locator;
|
|
633
|
+
if (!resolvedLocator) {
|
|
634
|
+
continue;
|
|
635
|
+
}
|
|
636
|
+
const resolved = await attemptResolvedLocator(resolvedLocator, candidate.strategy);
|
|
637
|
+
if (resolved) {
|
|
638
|
+
break;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (!resolvedBy && !lastError && surfaceRoot) {
|
|
642
|
+
const recoveredLocator = await recoverLocatorFromSurfaceRoot(surfaceRoot, target, action, attempts);
|
|
643
|
+
if (recoveredLocator) {
|
|
644
|
+
await attemptResolvedLocator(recoveredLocator, 'surface-descendant', {
|
|
645
|
+
skipDomSignature: true,
|
|
646
|
+
});
|
|
647
|
+
}
|
|
66
648
|
}
|
|
649
|
+
if (!resolvedBy) {
|
|
650
|
+
if (sawDomSignatureMismatch) {
|
|
651
|
+
staleReason = 'dom-signature-mismatch';
|
|
652
|
+
throw new Error('stale_target_dom_signature_changed');
|
|
653
|
+
}
|
|
654
|
+
if (sawDisabledTarget) {
|
|
655
|
+
setTargetAvailability(session, targetRef, 'gated', 'disabled');
|
|
656
|
+
throw new Error('target_disabled');
|
|
657
|
+
}
|
|
658
|
+
if (sawReadonlyTarget && (action === 'fill' || action === 'type')) {
|
|
659
|
+
setTargetAvailability(session, targetRef, 'gated', 'readonly');
|
|
660
|
+
throw new Error('target_readonly');
|
|
661
|
+
}
|
|
662
|
+
if (!lastError &&
|
|
663
|
+
target.surfaceRef &&
|
|
664
|
+
(target.acceptancePolicy === 'selection' || target.acceptancePolicy === 'date-selection')) {
|
|
665
|
+
setTargetAvailability(session, targetRef, 'surface-inactive', 'surface-not-active');
|
|
666
|
+
throw new Error('target_surface_inactive');
|
|
667
|
+
}
|
|
668
|
+
if (!resolvedBy) {
|
|
669
|
+
if (!lastError &&
|
|
670
|
+
(target.controlFamily === 'text-input' ||
|
|
671
|
+
target.controlFamily === 'select' ||
|
|
672
|
+
target.controlFamily === 'datepicker')) {
|
|
673
|
+
staleReason = 'locator-resolution-failed';
|
|
674
|
+
throw new Error('stale_target_locator_resolution_failed');
|
|
675
|
+
}
|
|
676
|
+
if (lastError) {
|
|
677
|
+
throw lastError;
|
|
678
|
+
}
|
|
679
|
+
throw new Error('deterministic_target_resolution_failed');
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const popup = await popupPromise;
|
|
683
|
+
const latePage = !popup && watchForNewPage ? await waitForLatePage(browser, beforePages) : null;
|
|
684
|
+
if (latePage) {
|
|
685
|
+
attempts.push('late-page-captured');
|
|
686
|
+
}
|
|
687
|
+
const discoveredPage = popup ?? latePage;
|
|
688
|
+
const afterPages = discoveredPage ? [...beforePages, discoveredPage] : listPages(browser);
|
|
689
|
+
const popupPageRef = await capturePopupIfOpened(session, beforePages, afterPages, target.pageRef, attempts);
|
|
690
|
+
const finalPageRef = popupPageRef ?? target.pageRef;
|
|
691
|
+
currentPageRef = finalPageRef;
|
|
692
|
+
if (popupPageRef) {
|
|
693
|
+
const popupPage = await resolvePageByRef(browser, session, popupPageRef);
|
|
694
|
+
await syncSessionPage(session, popupPageRef, popupPage);
|
|
695
|
+
}
|
|
696
|
+
else {
|
|
697
|
+
const syncedPage = await syncSessionPage(session, target.pageRef, page);
|
|
698
|
+
if (startingPageUrl && syncedPage.url && syncedPage.url !== startingPageUrl) {
|
|
699
|
+
clearProtectedExposure(session, target.pageRef);
|
|
700
|
+
}
|
|
701
|
+
const finalProgressProbe = progressProbe;
|
|
702
|
+
if (finalProgressProbe) {
|
|
703
|
+
const acceptance = await waitForAcceptanceProbe(finalProgressProbe);
|
|
704
|
+
const afterPageObservation = acceptance.afterPageObservation;
|
|
705
|
+
const accepted = acceptance.accepted;
|
|
706
|
+
if (acceptance.polls > 1) {
|
|
707
|
+
attempts.push(`acceptance.polled:${acceptance.polls}`);
|
|
708
|
+
}
|
|
709
|
+
if (!accepted && finalProgressProbe.policy === 'value-change') {
|
|
710
|
+
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
711
|
+
throw new Error('action_postcondition_failed:value-change');
|
|
712
|
+
}
|
|
713
|
+
else if (!accepted &&
|
|
714
|
+
(finalProgressProbe.policy === 'selection' ||
|
|
715
|
+
finalProgressProbe.policy === 'date-selection') &&
|
|
716
|
+
finalProgressProbe.expectedValue !== null) {
|
|
717
|
+
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
718
|
+
throw new Error(`action_postcondition_failed:${finalProgressProbe.policy}`);
|
|
719
|
+
}
|
|
720
|
+
else if (!accepted && finalProgressProbe.policy === 'submit') {
|
|
721
|
+
attempts.push(`acceptance.failed:${finalProgressProbe.policy}`);
|
|
722
|
+
attempts.push('no-progress.detected');
|
|
723
|
+
noProgressDiagnosis = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
|
|
724
|
+
if (noProgressDiagnosis) {
|
|
725
|
+
attempts.push(`no-progress.diagnosis:${noProgressDiagnosis.kind}`);
|
|
726
|
+
}
|
|
727
|
+
throw new Error('no_observable_progress');
|
|
728
|
+
}
|
|
729
|
+
else if (!accepted) {
|
|
730
|
+
const afterLocatorObservation = finalProgressProbe.trackedStateKeys.length > 0
|
|
731
|
+
? await captureLocatorState(finalProgressProbe.locator, finalProgressProbe.trackedStateKeys)
|
|
732
|
+
: null;
|
|
733
|
+
const afterContextHash = await captureLocatorContextHash(finalProgressProbe.locator);
|
|
734
|
+
const hasComparableSignal = finalProgressProbe.trackedStateKeys.length > 0 ||
|
|
735
|
+
Boolean(finalProgressProbe.beforeContextHash || afterContextHash) ||
|
|
736
|
+
Boolean(finalProgressProbe.beforePage || afterPageObservation);
|
|
737
|
+
if (hasComparableSignal &&
|
|
738
|
+
!pageObservationChanged(finalProgressProbe.beforePage, afterPageObservation) &&
|
|
739
|
+
finalProgressProbe.beforeContextHash === afterContextHash &&
|
|
740
|
+
!locatorStateChanged(finalProgressProbe.beforeLocator, afterLocatorObservation)) {
|
|
741
|
+
attempts.push('no-progress.detected');
|
|
742
|
+
noProgressDiagnosis = await diagnoseNoObservableProgress(page, finalProgressProbe.locator);
|
|
743
|
+
if (noProgressDiagnosis) {
|
|
744
|
+
attempts.push(`no-progress.diagnosis:${noProgressDiagnosis.kind}`);
|
|
745
|
+
}
|
|
746
|
+
throw new Error('no_observable_progress');
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
if (resolvedBy === 'playwright-locator') {
|
|
752
|
+
incrementMetric(session, 'deterministicActions');
|
|
753
|
+
}
|
|
754
|
+
recordActionResult(session, true, Date.now() - startedAt);
|
|
755
|
+
saveSession(session);
|
|
756
|
+
await trace.finishSuccess();
|
|
757
|
+
outputJSON({
|
|
758
|
+
success: true,
|
|
759
|
+
targetRef,
|
|
760
|
+
action,
|
|
761
|
+
value: actionValue,
|
|
762
|
+
resolvedBy,
|
|
763
|
+
locatorStrategy,
|
|
764
|
+
pageRef: finalPageRef,
|
|
765
|
+
attempts,
|
|
766
|
+
popup: Boolean(popupPageRef),
|
|
767
|
+
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
768
|
+
iframe: Boolean(target.framePath?.length),
|
|
769
|
+
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
770
|
+
...(recoveredAfterError
|
|
771
|
+
? {
|
|
772
|
+
recoveredAfterError: true,
|
|
773
|
+
recoveredAcceptancePolicy: recoveredAcceptancePolicy ?? undefined,
|
|
774
|
+
}
|
|
775
|
+
: {}),
|
|
776
|
+
durationMs: Date.now() - startedAt,
|
|
777
|
+
metrics: session.runtime?.metrics,
|
|
778
|
+
});
|
|
67
779
|
}
|
|
68
780
|
catch (err) {
|
|
69
|
-
|
|
70
|
-
|
|
781
|
+
failureMessage = `Act failed: ${err instanceof Error ? err.message : String(err)}`;
|
|
782
|
+
recordActionResult(session, false, Date.now() - startedAt);
|
|
783
|
+
if (staleReason) {
|
|
784
|
+
markTargetLifecycle(session, targetRef, 'stale', staleReason);
|
|
785
|
+
}
|
|
786
|
+
saveSession(session);
|
|
787
|
+
if (currentPage) {
|
|
788
|
+
try {
|
|
789
|
+
const protectedExposure = getProtectedExposure(session, currentPageRef);
|
|
790
|
+
if (protectedExposure) {
|
|
791
|
+
await trace.finishSuccess();
|
|
792
|
+
failureArtifacts = buildProtectedArtifactsSuppressed(protectedExposure);
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
failureArtifacts = await captureActionFailureArtifacts({
|
|
796
|
+
page: currentPage,
|
|
797
|
+
targetRef,
|
|
798
|
+
action,
|
|
799
|
+
pageRef: currentPageRef,
|
|
800
|
+
attempts,
|
|
801
|
+
locatorStrategy,
|
|
802
|
+
popup: attempts.includes('popup-captured'),
|
|
803
|
+
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
804
|
+
iframe: Boolean(target.framePath?.length),
|
|
805
|
+
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
806
|
+
durationMs: Date.now() - startedAt,
|
|
807
|
+
error: failureMessage,
|
|
808
|
+
finishTrace: (artifactDir) => trace.finishFailure(artifactDir),
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
// Best effort only. Preserve the original action failure.
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
finally {
|
|
818
|
+
if (browser) {
|
|
819
|
+
disconnectPlaywright(browser);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
if (failureMessage) {
|
|
823
|
+
if (failureMessage === 'Act failed: no_observable_progress' && noProgressDiagnosis) {
|
|
824
|
+
failureMessage = `Act failed: no_observable_progress (${noProgressDiagnosis.kind})`;
|
|
825
|
+
}
|
|
826
|
+
const failureContract = describeActFailure({
|
|
827
|
+
failureMessage,
|
|
828
|
+
staleReason,
|
|
829
|
+
diagnosis: noProgressDiagnosis,
|
|
830
|
+
});
|
|
831
|
+
const protectedExposure = getProtectedExposure(session, currentPageRef);
|
|
832
|
+
const outputDiagnosis = protectedExposure && noProgressDiagnosis
|
|
833
|
+
? redactNoObservableProgressDiagnosis(noProgressDiagnosis)
|
|
834
|
+
: noProgressDiagnosis;
|
|
835
|
+
outputFailure({
|
|
836
|
+
error: failureMessage,
|
|
837
|
+
outcomeType: failureContract.outcomeType,
|
|
838
|
+
message: failureContract.message,
|
|
839
|
+
reason: failureContract.reason,
|
|
840
|
+
targetRef,
|
|
841
|
+
action,
|
|
842
|
+
value: actionValue,
|
|
843
|
+
pageRef: currentPageRef,
|
|
844
|
+
locatorStrategy,
|
|
845
|
+
attempts,
|
|
846
|
+
popup: attempts.includes('popup-captured'),
|
|
847
|
+
overlayHandled: attempts.includes('overlay.dismissed'),
|
|
848
|
+
iframe: Boolean(target.framePath?.length),
|
|
849
|
+
jsFallback: attempts.some((attempt) => attempt.startsWith('locator.evaluate.')),
|
|
850
|
+
durationMs: Date.now() - startedAt,
|
|
851
|
+
staleTarget: Boolean(staleReason),
|
|
852
|
+
staleReason: staleReason ?? undefined,
|
|
853
|
+
diagnosis: outputDiagnosis ?? undefined,
|
|
854
|
+
artifacts: failureArtifacts,
|
|
855
|
+
metrics: session.runtime?.metrics,
|
|
856
|
+
});
|
|
71
857
|
}
|
|
72
|
-
return outputError(failureMessage ?? `Act failed: Stagehand could not complete the requested action: ${instruction}`);
|
|
73
858
|
}
|