site-agent-pro 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +689 -0
- package/dist/auth/credentialStore.js +62 -0
- package/dist/auth/inbox.js +193 -0
- package/dist/auth/profile.js +379 -0
- package/dist/auth/runner.js +1124 -0
- package/dist/backend/dashboardData.js +194 -0
- package/dist/backend/runArtifacts.js +48 -0
- package/dist/backend/runRepository.js +93 -0
- package/dist/bin.js +2 -0
- package/dist/cli/backfillSiteChecks.js +143 -0
- package/dist/cli/run.js +309 -0
- package/dist/cli/trade.js +69 -0
- package/dist/config.js +199 -0
- package/dist/core/agentProfiles.js +55 -0
- package/dist/core/aggregateReport.js +382 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/customTaskSuite.js +148 -0
- package/dist/core/evaluator.js +217 -0
- package/dist/core/executor.js +788 -0
- package/dist/core/fallbackReport.js +335 -0
- package/dist/core/formHeuristics.js +411 -0
- package/dist/core/gameplaySummary.js +164 -0
- package/dist/core/interaction.js +202 -0
- package/dist/core/pageState.js +201 -0
- package/dist/core/planner.js +1669 -0
- package/dist/core/processSubmissionBatch.js +204 -0
- package/dist/core/runAuditJob.js +170 -0
- package/dist/core/runner.js +2352 -0
- package/dist/core/siteBrief.js +107 -0
- package/dist/core/siteChecks.js +1526 -0
- package/dist/core/taskDirectives.js +279 -0
- package/dist/core/taskHeuristics.js +263 -0
- package/dist/dashboard/client.js +1256 -0
- package/dist/dashboard/contracts.js +95 -0
- package/dist/dashboard/narrative.js +277 -0
- package/dist/dashboard/server.js +458 -0
- package/dist/dashboard/theme.js +888 -0
- package/dist/index.js +84 -0
- package/dist/llm/client.js +188 -0
- package/dist/paystack/account.js +123 -0
- package/dist/paystack/client.js +100 -0
- package/dist/paystack/index.js +13 -0
- package/dist/paystack/test-paystack.js +83 -0
- package/dist/paystack/transfer.js +138 -0
- package/dist/paystack/types.js +74 -0
- package/dist/paystack/webhook.js +121 -0
- package/dist/prompts/browserAgent.js +124 -0
- package/dist/prompts/reviewer.js +71 -0
- package/dist/reporting/clickReplay.js +290 -0
- package/dist/reporting/html.js +930 -0
- package/dist/reporting/markdown.js +238 -0
- package/dist/reporting/template.js +1141 -0
- package/dist/schemas/types.js +361 -0
- package/dist/submissions/customTasks.js +196 -0
- package/dist/submissions/html.js +770 -0
- package/dist/submissions/model.js +56 -0
- package/dist/submissions/publicUrl.js +76 -0
- package/dist/submissions/service.js +74 -0
- package/dist/submissions/store.js +37 -0
- package/dist/submissions/types.js +65 -0
- package/dist/trade/engine.js +241 -0
- package/dist/trade/evm/erc20.js +44 -0
- package/dist/trade/extractor.js +148 -0
- package/dist/trade/policy.js +35 -0
- package/dist/trade/session.js +31 -0
- package/dist/trade/types.js +107 -0
- package/dist/trade/validator.js +148 -0
- package/dist/utils/files.js +59 -0
- package/dist/utils/log.js +24 -0
- package/dist/utils/playwrightCompat.js +14 -0
- package/dist/utils/time.js +3 -0
- package/dist/wallet/provider.js +345 -0
- package/dist/wallet/relay.js +129 -0
- package/dist/wallet/wallet.js +178 -0
- package/docs/01-installation.md +134 -0
- package/docs/02-running-your-first-audit.md +136 -0
- package/docs/03-configuration.md +233 -0
- package/docs/04-how-the-agent-thinks.md +41 -0
- package/docs/05-extending-personas-and-tasks.md +42 -0
- package/docs/06-hardening-for-production.md +92 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1669 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getPreferredAccessIdentity } from "../auth/profile.js";
|
|
3
|
+
import { generateStructured } from "../llm/client.js";
|
|
4
|
+
import { isWalletConfigured, getWalletAddress, getWalletChainId, getWalletBalances } from "../wallet/wallet.js";
|
|
5
|
+
import { BROWSER_AGENT_PROMPT } from "../prompts/browserAgent.js";
|
|
6
|
+
import { buildFormFieldKey, scoreFormFieldTargetMatch, inferFormFieldValue as inferProfileFormFieldValue, isPlaceholderFieldValue as isPlaceholderProfileFieldValue, shouldCheckField } from "./formHeuristics.js";
|
|
7
|
+
import { buildTaskDirectiveSummary, parseTaskDirectives } from "./taskDirectives.js";
|
|
8
|
+
import { classifyTaskText, extractTaskKeywords, isRegressiveTaskControlLabel, normalizeTaskText, scoreInteractiveForTask, textHasInstructionCue, textHasOutcomeCue } from "./taskHeuristics.js";
|
|
9
|
+
import { buildDefaultTradeRunOptions } from "../trade/policy.js";
|
|
10
|
+
import { pageLooksTradeReady, taskLooksLikeTrade } from "../trade/extractor.js";
|
|
11
|
+
import { listTransactions } from "../paystack/index.js";
|
|
12
|
+
import { PlannerDecisionSchema } from "../schemas/types.js";
|
|
13
|
+
const PLANNER_TIMEOUT_MS = 30000;
|
|
14
|
+
const PLANNER_MAX_RETRIES = 3;
|
|
15
|
+
const ORDERED_STEP_PATTERNS = [/^step\s+\d+\b/i, /^\d+[\.\)]\s+/, /^(first|second|third|fourth|fifth|next|then|finally)\b/i];
|
|
16
|
+
const ACTIONABLE_INSTRUCTION_PATTERNS = [
|
|
17
|
+
/^(?:step\s+\d+[:.)-]?\s*)?(?:click|tap|press|select|choose|open)\b/i,
|
|
18
|
+
/^(?:step\s+\d+[:.)-]?\s*)?(?:enter|type|fill|input|provide)\b/i,
|
|
19
|
+
/^(?:step\s+\d+[:.)-]?\s*)?(?:scroll|swipe)\b/i,
|
|
20
|
+
/^(?:step\s+\d+[:.)-]?\s*)?(?:wait|pause|hold)\b/i,
|
|
21
|
+
/^(?:step\s+\d+[:.)-]?\s*)?(?:go back|back)\b/i,
|
|
22
|
+
/^(?:first|second|third|fourth|fifth|next|then|finally)\b.*\b(?:click|tap|press|select|choose|open|enter|type|fill|scroll|wait|back)\b/i
|
|
23
|
+
];
|
|
24
|
+
const ACCESS_GATE_PATTERNS = [
|
|
25
|
+
/\baccess\b/i,
|
|
26
|
+
/\bcontinue\b/i,
|
|
27
|
+
/\bnext\b/i,
|
|
28
|
+
/\bunlock\b/i,
|
|
29
|
+
/\bview\b/i,
|
|
30
|
+
/\bproceed\b/i,
|
|
31
|
+
/\benter\b/i
|
|
32
|
+
];
|
|
33
|
+
const ACCOUNT_CREATION_TASK_PATTERN = /\b(?:sign ?up|signup|register|create(?:\s+your)?\s+(?:account|profile)|create\s+my\s+account|join)\b/i;
|
|
34
|
+
const ACCOUNT_CREATION_SUCCESS_PATTERN = /\b(?:registered users?|add another registration|account created|account ready|welcome|dashboard|profile active|view live market screen)\b/i;
|
|
35
|
+
const ACCOUNT_CREATION_STRONG_SUCCESS_PATTERN = /\b(?:registered users?|add another registration|account created|account ready|profile active|view live market screen)\b/i;
|
|
36
|
+
const ACCOUNT_CREATION_LOCAL_ONLY_PATTERN = /\b(?:browser fallback|browser storage only|using browser storage only|local server is unavailable|api is unavailable)\b/i;
|
|
37
|
+
const ACCOUNT_CREATION_VERIFICATION_PENDING_PATTERN = /\b(?:please\s+verify|verify\s+your\s+email|check\s+your\s+email|send\s+otp|enter\s+(?:the\s+)?(?:code|otp)|verification\s+code|resend\s*\(\d+\s*s\))\b/i;
|
|
38
|
+
const WALLET_CONNECTION_PENDING_PATTERN = /\b(?:connecting|check\s+(?:your\s+)?wallet|confirm\s+(?:in|with)\s+(?:wallet|metamask)|open\s+metamask|awaiting\s+wallet|waiting\s+for\s+wallet|wallet\s+connection\s+pending)\b/i;
|
|
39
|
+
const TRANSIENT_STATUS_LABEL_PATTERN = /^(?:connecting|loading|submitting|processing|working|authorizing|signing|verifying|sending|pending|syncing|please wait)$/i;
|
|
40
|
+
const PlannerInputSchema = z.object({
|
|
41
|
+
persona: z.object({
|
|
42
|
+
name: z.string(),
|
|
43
|
+
intent: z.string(),
|
|
44
|
+
constraints: z.array(z.string())
|
|
45
|
+
}),
|
|
46
|
+
task: z.object({
|
|
47
|
+
name: z.string(),
|
|
48
|
+
goal: z.string(),
|
|
49
|
+
original_instruction: z.string().default(""),
|
|
50
|
+
ordered_steps: z
|
|
51
|
+
.array(z.object({
|
|
52
|
+
action: z.string(),
|
|
53
|
+
target: z.string(),
|
|
54
|
+
raw: z.string()
|
|
55
|
+
}))
|
|
56
|
+
.default([]),
|
|
57
|
+
ordered_step_notes: z.array(z.string()).default([]),
|
|
58
|
+
ordered_step_confidence: z.enum(["high", "low", "none"]).default("high"),
|
|
59
|
+
success_condition: z.string(),
|
|
60
|
+
failure_signals: z.array(z.string()),
|
|
61
|
+
gameplay: z
|
|
62
|
+
.object({
|
|
63
|
+
rounds: z.number().optional(),
|
|
64
|
+
requireHowToPlay: z.boolean().optional()
|
|
65
|
+
})
|
|
66
|
+
.optional()
|
|
67
|
+
}),
|
|
68
|
+
siteBrief: z.object({
|
|
69
|
+
sitePurpose: z.string(),
|
|
70
|
+
intendedUserActions: z.array(z.string()),
|
|
71
|
+
summary: z.string(),
|
|
72
|
+
evidence: z.array(z.string())
|
|
73
|
+
}),
|
|
74
|
+
accessProfile: z.object({
|
|
75
|
+
email: z.string(),
|
|
76
|
+
password: z.string(),
|
|
77
|
+
firstName: z.string(),
|
|
78
|
+
lastName: z.string(),
|
|
79
|
+
fullName: z.string(),
|
|
80
|
+
phone: z.string(),
|
|
81
|
+
addressLine1: z.string(),
|
|
82
|
+
addressLine2: z.string(),
|
|
83
|
+
city: z.string(),
|
|
84
|
+
state: z.string(),
|
|
85
|
+
postalCode: z.string(),
|
|
86
|
+
country: z.string(),
|
|
87
|
+
company: z.string(),
|
|
88
|
+
walletAddress: z.string().default(""),
|
|
89
|
+
walletChainId: z.number().int().positive().nullable().default(null),
|
|
90
|
+
tradeEnabled: z.boolean().default(false)
|
|
91
|
+
}),
|
|
92
|
+
pageState: z.object({
|
|
93
|
+
title: z.string(),
|
|
94
|
+
url: z.string(),
|
|
95
|
+
visibleText: z.string(),
|
|
96
|
+
visibleLines: z.array(z.string()),
|
|
97
|
+
formFields: z.array(z.object({
|
|
98
|
+
agentId: z.string(),
|
|
99
|
+
label: z.string(),
|
|
100
|
+
placeholder: z.string(),
|
|
101
|
+
name: z.string(),
|
|
102
|
+
id: z.string(),
|
|
103
|
+
tag: z.string(),
|
|
104
|
+
inputType: z.string(),
|
|
105
|
+
autocomplete: z.string().default(""),
|
|
106
|
+
inputMode: z.string().default(""),
|
|
107
|
+
value: z.string(),
|
|
108
|
+
required: z.boolean(),
|
|
109
|
+
checked: z.boolean().optional(),
|
|
110
|
+
maxLength: z.number().int().positive().nullable().optional(),
|
|
111
|
+
options: z.array(z.string())
|
|
112
|
+
})),
|
|
113
|
+
interactive: z.array(z.object({
|
|
114
|
+
agentId: z.string(),
|
|
115
|
+
role: z.string(),
|
|
116
|
+
tag: z.string(),
|
|
117
|
+
type: z.string().optional(),
|
|
118
|
+
text: z.string(),
|
|
119
|
+
href: z.string().optional(),
|
|
120
|
+
disabled: z.boolean()
|
|
121
|
+
})),
|
|
122
|
+
numberedElements: z.array(z.string()).default([]),
|
|
123
|
+
headings: z.array(z.string()),
|
|
124
|
+
formsPresent: z.boolean(),
|
|
125
|
+
modalHints: z.array(z.string())
|
|
126
|
+
}),
|
|
127
|
+
recentPaystackTransactions: z.array(z.record(z.string(), z.unknown())).default([]),
|
|
128
|
+
walletBalances: z.record(z.string(), z.string()).default({}),
|
|
129
|
+
remainingSeconds: z.number().int().positive().optional(),
|
|
130
|
+
previous_actions: z.array(z.object({
|
|
131
|
+
step: z.number(),
|
|
132
|
+
action: z.string(),
|
|
133
|
+
target_id: z.string().default(""),
|
|
134
|
+
target: z.string().default(""),
|
|
135
|
+
success: z.boolean(),
|
|
136
|
+
state_changed: z.boolean().default(false),
|
|
137
|
+
note: z.string()
|
|
138
|
+
})).default([]),
|
|
139
|
+
history: z.array(z.object({
|
|
140
|
+
step: z.number(),
|
|
141
|
+
url: z.string(),
|
|
142
|
+
title: z.string(),
|
|
143
|
+
decision: z.object({
|
|
144
|
+
stepNumber: z.number().nullable().optional(),
|
|
145
|
+
instructionQuote: z.string().optional(),
|
|
146
|
+
action: z.string(),
|
|
147
|
+
target_id: z.string().default(""),
|
|
148
|
+
target: z.string(),
|
|
149
|
+
expectation: z.string(),
|
|
150
|
+
friction: z.string()
|
|
151
|
+
}),
|
|
152
|
+
result: z.object({
|
|
153
|
+
success: z.boolean(),
|
|
154
|
+
note: z.string(),
|
|
155
|
+
stateChanged: z.boolean().optional()
|
|
156
|
+
})
|
|
157
|
+
}))
|
|
158
|
+
});
|
|
159
|
+
function cleanErrorMessage(error) {
|
|
160
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
161
|
+
return message.replace(/\u001b\[[0-9;]*m/g, "").replace(/\s+/g, " ").trim() || "Unknown planner error";
|
|
162
|
+
}
|
|
163
|
+
function normalizeLineText(value) {
|
|
164
|
+
return value.replace(/\s+/g, " ").trim();
|
|
165
|
+
}
|
|
166
|
+
function normalizeKey(value) {
|
|
167
|
+
return normalizeLineText(value).toLowerCase();
|
|
168
|
+
}
|
|
169
|
+
function normalizeInteractiveIntentLabel(value) {
|
|
170
|
+
return normalizeLineText(value)
|
|
171
|
+
.replace(/[^a-z0-9\s]+/gi, " ")
|
|
172
|
+
.replace(/\s+/g, " ")
|
|
173
|
+
.trim();
|
|
174
|
+
}
|
|
175
|
+
function isTransientStatusLabel(value) {
|
|
176
|
+
return TRANSIENT_STATUS_LABEL_PATTERN.test(normalizeInteractiveIntentLabel(value));
|
|
177
|
+
}
|
|
178
|
+
function countSharedKeywords(left, right) {
|
|
179
|
+
const leftKeywords = extractTaskKeywords(left);
|
|
180
|
+
const rightKeywords = new Set(extractTaskKeywords(right));
|
|
181
|
+
return leftKeywords.filter((keyword) => rightKeywords.has(keyword)).length;
|
|
182
|
+
}
|
|
183
|
+
function getInteractiveLabel(item) {
|
|
184
|
+
return normalizeLineText(item.text || "");
|
|
185
|
+
}
|
|
186
|
+
function normalizeTargetId(value) {
|
|
187
|
+
return normalizeLineText(value);
|
|
188
|
+
}
|
|
189
|
+
function findInteractiveByTargetId(pageState, targetId) {
|
|
190
|
+
const normalizedTargetId = normalizeTargetId(targetId);
|
|
191
|
+
if (!normalizedTargetId) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
return pageState.interactive.find((item) => item.agentId === normalizedTargetId) ?? null;
|
|
195
|
+
}
|
|
196
|
+
function findFormFieldByTargetId(pageState, targetId) {
|
|
197
|
+
const normalizedTargetId = normalizeTargetId(targetId);
|
|
198
|
+
if (!normalizedTargetId) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
return pageState.formFields.find((field) => field.agentId === normalizedTargetId) ?? null;
|
|
202
|
+
}
|
|
203
|
+
function enrichDecisionTarget(args) {
|
|
204
|
+
const targetId = normalizeTargetId(args.decision.target_id || "");
|
|
205
|
+
const explicitTarget = normalizeLineText(args.decision.target || "");
|
|
206
|
+
if (!targetId) {
|
|
207
|
+
return {
|
|
208
|
+
...args.decision,
|
|
209
|
+
target_id: "",
|
|
210
|
+
target: explicitTarget
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const interactiveTarget = findInteractiveByTargetId(args.pageState, targetId);
|
|
214
|
+
if (interactiveTarget) {
|
|
215
|
+
return {
|
|
216
|
+
...args.decision,
|
|
217
|
+
target_id: targetId,
|
|
218
|
+
target: explicitTarget || resolveInteractiveTarget(interactiveTarget)
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
const fieldTarget = findFormFieldByTargetId(args.pageState, targetId);
|
|
222
|
+
if (fieldTarget) {
|
|
223
|
+
return {
|
|
224
|
+
...args.decision,
|
|
225
|
+
target_id: targetId,
|
|
226
|
+
target: explicitTarget || resolveFormFieldTarget(fieldTarget)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
...args.decision,
|
|
231
|
+
target_id: targetId,
|
|
232
|
+
target: explicitTarget
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
function buildInvalidTargetIdStopDecision(args) {
|
|
236
|
+
const targetId = normalizeTargetId(args.decision.target_id || "");
|
|
237
|
+
return buildStopDecision({
|
|
238
|
+
thought: targetId
|
|
239
|
+
? `The planner selected target_id '${targetId}', but that ID is not present in the current numbered page state.`
|
|
240
|
+
: `The planner returned '${args.decision.action}' without a valid target_id from the current numbered page state.`,
|
|
241
|
+
expectation: args.decision.action === "click" || args.decision.action === "type"
|
|
242
|
+
? "Stop instead of guessing another element or label that was not explicitly numbered on the page."
|
|
243
|
+
: "Stop because the requested action could not be grounded in the numbered page state."
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function enforceTargetIdDecision(args) {
|
|
247
|
+
const decision = enrichDecisionTarget(args);
|
|
248
|
+
if (decision.action === "click") {
|
|
249
|
+
return decision.target_id && findInteractiveByTargetId(args.pageState, decision.target_id)
|
|
250
|
+
? decision
|
|
251
|
+
: buildInvalidTargetIdStopDecision({ pageState: args.pageState, decision });
|
|
252
|
+
}
|
|
253
|
+
if (decision.action === "type") {
|
|
254
|
+
const fieldTarget = decision.target_id ? findFormFieldByTargetId(args.pageState, decision.target_id) : null;
|
|
255
|
+
if (!fieldTarget) {
|
|
256
|
+
return buildInvalidTargetIdStopDecision({ pageState: args.pageState, decision });
|
|
257
|
+
}
|
|
258
|
+
const inferredValue = inferFormFieldValue(fieldTarget, args.pageState.url);
|
|
259
|
+
const explicitEntry = findExplicitFieldEntryForField({
|
|
260
|
+
pageState: args.pageState,
|
|
261
|
+
taskGoal: args.taskGoal,
|
|
262
|
+
field: fieldTarget
|
|
263
|
+
});
|
|
264
|
+
if (explicitEntry) {
|
|
265
|
+
return {
|
|
266
|
+
...decision,
|
|
267
|
+
text: explicitEntry.value,
|
|
268
|
+
thought: decision.text === explicitEntry.value
|
|
269
|
+
? decision.thought
|
|
270
|
+
: `${decision.thought} (Using the exact user-supplied value '${explicitEntry.value}' for '${explicitEntry.target}'.)`
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
const plannerText = normalizeLineText(decision.text || "");
|
|
274
|
+
const fieldKey = buildFormFieldKey(fieldTarget);
|
|
275
|
+
const plannerTextLooksIntentional = plannerText.length > 0 &&
|
|
276
|
+
((/\b(?:amount|slippage|gas|token|contract)\b/i.test(fieldKey) && /^[0-9]+(?:\.[0-9]+)?$/.test(plannerText)) ||
|
|
277
|
+
(/\b(?:wallet|recipient|crypto\s+address|address)\b/i.test(fieldKey) && /^0x[a-fA-F0-9]{40}$/.test(plannerText)));
|
|
278
|
+
if (inferredValue && inferredValue !== decision.text && !shouldCheckField(inferredValue) && !plannerTextLooksIntentional) {
|
|
279
|
+
return {
|
|
280
|
+
...decision,
|
|
281
|
+
text: inferredValue,
|
|
282
|
+
thought: decision.thought + ` (Overriding LLM text '${decision.text}' with strictly generated profile value).`
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
return decision;
|
|
286
|
+
}
|
|
287
|
+
if (decision.target_id || decision.target) {
|
|
288
|
+
return {
|
|
289
|
+
...decision,
|
|
290
|
+
target_id: "",
|
|
291
|
+
target: decision.target
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return decision;
|
|
295
|
+
}
|
|
296
|
+
function enforceLoopAvoidance(args) {
|
|
297
|
+
if (!["click", "type"].includes(args.decision.action)) {
|
|
298
|
+
return args.decision;
|
|
299
|
+
}
|
|
300
|
+
const targetId = normalizeTargetId(args.decision.target_id || "");
|
|
301
|
+
if (!targetId) {
|
|
302
|
+
return args.decision;
|
|
303
|
+
}
|
|
304
|
+
const repeatedAction = [...args.history]
|
|
305
|
+
.reverse()
|
|
306
|
+
.find((entry) => entry.decision.action === args.decision.action &&
|
|
307
|
+
normalizeTargetId(entry.decision.target_id || "") === targetId);
|
|
308
|
+
if (!repeatedAction) {
|
|
309
|
+
return args.decision;
|
|
310
|
+
}
|
|
311
|
+
if (repeatedAction.result.stateChanged === false) {
|
|
312
|
+
return buildStopDecision({
|
|
313
|
+
thought: `BLOCKED: target_id '${targetId}' was already tried for '${args.decision.action}' and the page state did not change.`,
|
|
314
|
+
expectation: "Stop instead of retrying the same numbered element or clicking a random alternative to escape the loop."
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
return args.decision;
|
|
318
|
+
}
|
|
319
|
+
function historyHasAnyTradeAttempt(history) {
|
|
320
|
+
return history.some((entry) => entry.decision.action === "trade");
|
|
321
|
+
}
|
|
322
|
+
function historyHasRealTransactionClick(history) {
|
|
323
|
+
return history.some((entry) => entry.decision.action === "click" &&
|
|
324
|
+
entry.result.success &&
|
|
325
|
+
/\bexecute\s+real\s+transaction\b/i.test(entry.decision.target || entry.decision.instructionQuote || ""));
|
|
326
|
+
}
|
|
327
|
+
function pageShowsTradePending(pageState) {
|
|
328
|
+
const text = normalizeTaskText([pageState.title, pageState.visibleText, ...pageState.headings, ...pageState.modalHints].join(" "));
|
|
329
|
+
return /\b(?:sending|broadcasting|confirming|processing|pending|submitted)\b/i.test(text) && /\b(?:sell|buy|swap|send|transfer|transaction|tx)\b/i.test(text);
|
|
330
|
+
}
|
|
331
|
+
function pageShowsTradeFinalOutcome(pageState) {
|
|
332
|
+
const text = normalizeTaskText([pageState.title, pageState.visibleText, ...pageState.headings, ...pageState.modalHints].join(" "));
|
|
333
|
+
return (/\b(?:sell|buy|swap|send|transfer|transaction|tx)\b/i.test(text) &&
|
|
334
|
+
/\b(?:succeeded|success|successful|confirmed|completed|broadcast|failed|reverted|rejected|denied|cancelled|canceled|error)\b/i.test(text));
|
|
335
|
+
}
|
|
336
|
+
function directiveTargetsMatch(left, right) {
|
|
337
|
+
const normalizedLeft = normalizeKey(left);
|
|
338
|
+
const normalizedRight = normalizeKey(right);
|
|
339
|
+
if (!normalizedLeft || !normalizedRight) {
|
|
340
|
+
return false;
|
|
341
|
+
}
|
|
342
|
+
if (normalizedLeft === normalizedRight) {
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
if (normalizedLeft.includes(normalizedRight) || normalizedRight.includes(normalizedLeft)) {
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
const leftKeywords = extractTaskKeywords(left);
|
|
349
|
+
const rightKeywords = extractTaskKeywords(right);
|
|
350
|
+
if (leftKeywords.length === 0 || rightKeywords.length === 0) {
|
|
351
|
+
return false;
|
|
352
|
+
}
|
|
353
|
+
const shared = countSharedKeywords(left, right);
|
|
354
|
+
return shared >= Math.min(leftKeywords.length, rightKeywords.length) || shared >= 2;
|
|
355
|
+
}
|
|
356
|
+
function scoreInteractiveForDirectiveTarget(item, target) {
|
|
357
|
+
const label = getInteractiveLabel(item);
|
|
358
|
+
if (!label || isTransientStatusLabel(label)) {
|
|
359
|
+
return Number.NEGATIVE_INFINITY;
|
|
360
|
+
}
|
|
361
|
+
const normalizedLabel = normalizeKey(label);
|
|
362
|
+
const normalizedTarget = normalizeKey(target);
|
|
363
|
+
let score = 0;
|
|
364
|
+
if (normalizedLabel === normalizedTarget) {
|
|
365
|
+
score = Math.max(score, 220);
|
|
366
|
+
}
|
|
367
|
+
if (normalizedLabel.includes(normalizedTarget) || normalizedTarget.includes(normalizedLabel)) {
|
|
368
|
+
score = Math.max(score, 180);
|
|
369
|
+
}
|
|
370
|
+
const targetKeywords = extractTaskKeywords(target);
|
|
371
|
+
const keywordMatches = countSharedKeywords(target, label);
|
|
372
|
+
if (targetKeywords.length > 0) {
|
|
373
|
+
score = Math.max(score, keywordMatches * 40 + (keywordMatches === targetKeywords.length ? 40 : 0));
|
|
374
|
+
}
|
|
375
|
+
const role = item.role.toLowerCase();
|
|
376
|
+
const tag = item.tag.toLowerCase();
|
|
377
|
+
if (role === "button" || role === "tab" || role === "link" || tag === "button" || tag === "a") {
|
|
378
|
+
score += 10;
|
|
379
|
+
}
|
|
380
|
+
if (item.disabled) {
|
|
381
|
+
score -= 60;
|
|
382
|
+
}
|
|
383
|
+
return score;
|
|
384
|
+
}
|
|
385
|
+
function findBestDirectiveInteractiveMatch(pageState, target) {
|
|
386
|
+
const ranked = pageState.interactive
|
|
387
|
+
.map((item) => ({
|
|
388
|
+
item,
|
|
389
|
+
score: scoreInteractiveForDirectiveTarget(item, target)
|
|
390
|
+
}))
|
|
391
|
+
.filter((candidate) => Number.isFinite(candidate.score) && candidate.score >= 80)
|
|
392
|
+
.sort((left, right) => right.score - left.score);
|
|
393
|
+
return ranked[0]?.item ?? null;
|
|
394
|
+
}
|
|
395
|
+
function isSubmitLikeInteractiveLabel(label) {
|
|
396
|
+
return /^(?:submit|continue|next|finish|complete|done|send|verify|create(?:\s+\w+){0,2}\s+account|sign ?up|register|join(?: now)?|start free|save(?: and continue)?)$/i.test(normalizeInteractiveIntentLabel(label));
|
|
397
|
+
}
|
|
398
|
+
function taskLooksLikeAccountCreation(goal) {
|
|
399
|
+
return ACCOUNT_CREATION_TASK_PATTERN.test(goal);
|
|
400
|
+
}
|
|
401
|
+
function pageShowsAccountCreationSuccess(pageState) {
|
|
402
|
+
const blob = normalizeTaskText([pageState.title, pageState.visibleText, ...pageState.headings].join(" "));
|
|
403
|
+
if (ACCOUNT_CREATION_VERIFICATION_PENDING_PATTERN.test(blob)) {
|
|
404
|
+
return false;
|
|
405
|
+
}
|
|
406
|
+
if (ACCOUNT_CREATION_STRONG_SUCCESS_PATTERN.test(blob)) {
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
if (pageState.formsPresent) {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
return ACCOUNT_CREATION_SUCCESS_PATTERN.test(blob);
|
|
413
|
+
}
|
|
414
|
+
function pageShowsAccountCreationVerificationPending(pageState) {
|
|
415
|
+
const blob = normalizeTaskText([pageState.title, pageState.visibleText, ...pageState.headings].join(" "));
|
|
416
|
+
return ACCOUNT_CREATION_VERIFICATION_PENDING_PATTERN.test(blob);
|
|
417
|
+
}
|
|
418
|
+
function pageShowsAccountCreationLocalOnlyFallback(pageState) {
|
|
419
|
+
const blob = normalizeTaskText([pageState.title, pageState.visibleText, ...pageState.headings].join(" "));
|
|
420
|
+
return ACCOUNT_CREATION_LOCAL_ONLY_PATTERN.test(blob);
|
|
421
|
+
}
|
|
422
|
+
function pageShowsPendingWalletConnection(pageState) {
|
|
423
|
+
if (pageLooksTradeReady(pageState)) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
const blob = normalizeTaskText([pageState.title, pageState.visibleText, ...pageState.headings].join(" "));
|
|
427
|
+
return WALLET_CONNECTION_PENDING_PATTERN.test(blob);
|
|
428
|
+
}
|
|
429
|
+
function findBestSubmitControl(pageState) {
|
|
430
|
+
const ranked = pageState.interactive
|
|
431
|
+
.map((item) => {
|
|
432
|
+
const label = resolveInteractiveTarget(item);
|
|
433
|
+
let score = Number.NEGATIVE_INFINITY;
|
|
434
|
+
if (!item.disabled && label) {
|
|
435
|
+
if (isSubmitLikeInteractiveLabel(label)) {
|
|
436
|
+
score = 180;
|
|
437
|
+
}
|
|
438
|
+
else if (/\bsubmit\b|\bcontinue\b|\bnext\b|\bfinish\b|\bcomplete\b|\bregister\b|\bsign ?up\b|\bcreate\b.*\baccount\b/i.test(label)) {
|
|
439
|
+
score = 120;
|
|
440
|
+
}
|
|
441
|
+
const role = item.role.toLowerCase();
|
|
442
|
+
const tag = item.tag.toLowerCase();
|
|
443
|
+
if (Number.isFinite(score) && (role === "button" || role === "link" || role === "tab" || tag === "button" || tag === "a")) {
|
|
444
|
+
score += 10;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return { item, score };
|
|
448
|
+
})
|
|
449
|
+
.filter((candidate) => Number.isFinite(candidate.score) && candidate.score >= 100)
|
|
450
|
+
.sort((left, right) => right.score - left.score);
|
|
451
|
+
return ranked[0]?.item ?? null;
|
|
452
|
+
}
|
|
453
|
+
function historyHasSuccessfulClickTarget(history, target) {
|
|
454
|
+
return history.some((entry) => entry.decision.action === "click" &&
|
|
455
|
+
entry.result.success &&
|
|
456
|
+
directiveTargetsMatch(entry.decision.target || entry.decision.instructionQuote || "", target));
|
|
457
|
+
}
|
|
458
|
+
function taskHasPendingExplicitDirective(taskGoal, history) {
|
|
459
|
+
return parseTaskDirectives(taskGoal).some((directive) => {
|
|
460
|
+
if (directive.action === "fill_visible_form") {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
if (directive.action === "click") {
|
|
464
|
+
return !historyHasSuccessfulClickTarget(history, directive.target);
|
|
465
|
+
}
|
|
466
|
+
return true;
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
function buildStopDecision(args) {
|
|
470
|
+
return {
|
|
471
|
+
thought: args.thought,
|
|
472
|
+
stepNumber: args.stepNumber ?? null,
|
|
473
|
+
instructionQuote: args.instructionQuote ?? "",
|
|
474
|
+
action: "stop",
|
|
475
|
+
target_id: "",
|
|
476
|
+
target: "",
|
|
477
|
+
text: "",
|
|
478
|
+
expectation: args.expectation,
|
|
479
|
+
friction: args.friction ?? "high"
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
function hasSingleProfileConstraint(constraints) {
|
|
483
|
+
const constraintBlob = normalizeTaskText(constraints.join(" "));
|
|
484
|
+
if (!/\b(?:profile|account|identity)\b/i.test(constraintBlob)) {
|
|
485
|
+
return false;
|
|
486
|
+
}
|
|
487
|
+
return /\b(?:single|same|one|at most|no more than|only create|create only|reuse|keep using|do not create|don't create|never create)\b/i.test(constraintBlob);
|
|
488
|
+
}
|
|
489
|
+
function pageShowsActiveProfile(pageState) {
|
|
490
|
+
const pageBlob = normalizeTaskText([pageState.title, pageState.visibleText, ...pageState.headings].join(" "));
|
|
491
|
+
return /profile active|this visitor profile is saved|update profile|occupations:/i.test(pageBlob);
|
|
492
|
+
}
|
|
493
|
+
function isProfileLifecycleControlLabel(label) {
|
|
494
|
+
return /(?:create|register|sign ?up|new|add)\s+(?:profile|account)|(?:profile|account).*(?:create|register|sign ?up)|^(?:update|edit|change|reset|clear)\s+(?:profile|details?|settings?)$/i.test(label);
|
|
495
|
+
}
|
|
496
|
+
function violatesRunWideGuardrail(args) {
|
|
497
|
+
const target = normalizeTaskText(args.target);
|
|
498
|
+
if (!target) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
if (hasSingleProfileConstraint(args.suite.persona.constraints) && pageShowsActiveProfile(args.pageState)) {
|
|
502
|
+
return isRegressiveTaskControlLabel(target) || isProfileLifecycleControlLabel(target);
|
|
503
|
+
}
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
function isActionableInstruction(line) {
|
|
507
|
+
return ACTIONABLE_INSTRUCTION_PATTERNS.some((pattern) => pattern.test(line));
|
|
508
|
+
}
|
|
509
|
+
function isInteractiveLine(pageState, line) {
|
|
510
|
+
const normalizedLine = normalizeKey(line);
|
|
511
|
+
return pageState.interactive.some((item) => {
|
|
512
|
+
const label = normalizeKey(getInteractiveLabel(item));
|
|
513
|
+
return Boolean(label) && label === normalizedLine && !isTransientStatusLabel(label);
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
function isFormFieldLine(pageState, line) {
|
|
517
|
+
const normalizedLine = normalizeKey(line);
|
|
518
|
+
return pageState.formFields.some((field) => [field.label, field.placeholder, field.name, field.id].some((value) => {
|
|
519
|
+
const normalizedValue = normalizeKey(value);
|
|
520
|
+
return Boolean(normalizedValue) && normalizedValue === normalizedLine;
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
function extractOrderedInstructionLines(pageState) {
|
|
524
|
+
return pageState.visibleLines
|
|
525
|
+
.map((line, index) => ({
|
|
526
|
+
stepNumber: index + 1,
|
|
527
|
+
instructionQuote: normalizeLineText(line)
|
|
528
|
+
}))
|
|
529
|
+
.filter((entry) => entry.instructionQuote.length > 0 &&
|
|
530
|
+
(isActionableInstruction(entry.instructionQuote) ||
|
|
531
|
+
ORDERED_STEP_PATTERNS.some((pattern) => pattern.test(entry.instructionQuote)) ||
|
|
532
|
+
isInteractiveLine(pageState, entry.instructionQuote) ||
|
|
533
|
+
isFormFieldLine(pageState, entry.instructionQuote)));
|
|
534
|
+
}
|
|
535
|
+
function cleanInstructionTarget(value) {
|
|
536
|
+
return normalizeLineText(value
|
|
537
|
+
.replace(/^the\s+/i, "")
|
|
538
|
+
.replace(/\s+(?:button|link|tab|menu|menu item|option|field|input|textbox|text box|checkbox|radio button)\b.*$/i, "")
|
|
539
|
+
.replace(/[.:!?]+$/g, ""));
|
|
540
|
+
}
|
|
541
|
+
function extractClickTarget(instructionQuote) {
|
|
542
|
+
const match = instructionQuote.match(/^(?:step\s+\d+[:.)-]?\s*)?(?:click|tap|press|select|choose|open)\s+(?:the\s+)?["'“]?([^"'”]+?)["'”]?(?:\s+(?:button|link|tab|menu|menu item|option|card))?(?:[.!?].*)?$/i);
|
|
543
|
+
const target = cleanInstructionTarget(match?.[1] ?? "");
|
|
544
|
+
return target || null;
|
|
545
|
+
}
|
|
546
|
+
function extractTypeTarget(instructionQuote) {
|
|
547
|
+
const normalized = normalizeLineText(instructionQuote);
|
|
548
|
+
const explicitMatch = normalized.match(/^(?:step\s+\d+[:.)-]?\s*)?(?:enter|type|fill|input|provide)\s+(?:your\s+|the\s+)?(.+?)(?:\s+(?:field|box|input|value|details?))?(?:[.!?].*)?$/i);
|
|
549
|
+
if (explicitMatch?.[1]) {
|
|
550
|
+
return cleanInstructionTarget(explicitMatch[1]);
|
|
551
|
+
}
|
|
552
|
+
return ORDERED_STEP_PATTERNS.some((pattern) => pattern.test(normalized))
|
|
553
|
+
? cleanInstructionTarget(normalized.replace(/^step\s+\d+[:.)-]?\s*/i, "")) || null
|
|
554
|
+
: null;
|
|
555
|
+
}
|
|
556
|
+
function findInteractiveControl(pageState, instructionQuote) {
|
|
557
|
+
const normalizedLine = normalizeKey(instructionQuote);
|
|
558
|
+
return pageState.interactive.find((item) => normalizeKey(getInteractiveLabel(item)) === normalizedLine) ?? null;
|
|
559
|
+
}
|
|
560
|
+
function findInteractiveTarget(pageState, instructionQuote) {
|
|
561
|
+
const exact = findInteractiveControl(pageState, instructionQuote);
|
|
562
|
+
return exact ? getInteractiveLabel(exact) : null;
|
|
563
|
+
}
|
|
564
|
+
function findMatchingFormField(pageState, instructionQuote) {
|
|
565
|
+
const normalizedLine = normalizeKey(instructionQuote);
|
|
566
|
+
const target = extractTypeTarget(instructionQuote) ?? instructionQuote;
|
|
567
|
+
const candidates = pageState.formFields
|
|
568
|
+
.map((field) => {
|
|
569
|
+
const score = Math.max(scoreFormFieldTargetMatch(field, target), scoreFormFieldTargetMatch(field, instructionQuote), normalizedLine === normalizeKey(resolveFormFieldTarget(field)) ? 120 : 0);
|
|
570
|
+
return { field, score };
|
|
571
|
+
})
|
|
572
|
+
.filter((candidate) => candidate.score > 0)
|
|
573
|
+
.sort((left, right) => right.score - left.score);
|
|
574
|
+
return candidates[0]?.field ?? null;
|
|
575
|
+
}
|
|
576
|
+
function inferFormFieldValue(field, url) {
|
|
577
|
+
return inferProfileFormFieldValue(field, getPreferredAccessIdentity(url));
|
|
578
|
+
}
|
|
579
|
+
function extractCompactAmountTask(taskGoal) {
|
|
580
|
+
const match = normalizeLineText(taskGoal).match(/^(buy|sell|swap)\s+([0-9]+(?:\.[0-9]+)?)$/i);
|
|
581
|
+
if (!match?.[1] || !match[2]) {
|
|
582
|
+
return null;
|
|
583
|
+
}
|
|
584
|
+
const action = match[1].toLowerCase();
|
|
585
|
+
if (action !== "buy" && action !== "sell" && action !== "swap") {
|
|
586
|
+
return null;
|
|
587
|
+
}
|
|
588
|
+
return {
|
|
589
|
+
action,
|
|
590
|
+
amount: match[2]
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function extractLiteralFieldEntryTask(taskGoal) {
|
|
594
|
+
const normalized = normalizeLineText(taskGoal).replace(/^(?:task|step)\s+\d+[:.)-]?\s*/i, "");
|
|
595
|
+
const match = normalized.match(/^(?:enter|type|fill|input|provide)\s+["'“]?(.+?)["'”]?\s+(?:in|into)\s+(?:the\s+)?(.+?)(?:\s+(?:field|box|input|textbox|text box|value|details?))?$/i);
|
|
596
|
+
if (!match?.[1] || !match[2]) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
const value = normalizeLineText(match[1]);
|
|
600
|
+
const target = cleanInstructionTarget(match[2]);
|
|
601
|
+
if (!value || !target) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
return { value, target };
|
|
605
|
+
}
|
|
606
|
+
function findTradeAmountField(pageState) {
|
|
607
|
+
const ranked = pageState.formFields
|
|
608
|
+
.map((field) => {
|
|
609
|
+
const key = normalizeKey([field.label, field.placeholder, field.name, field.id, field.inputType, field.inputMode].join(" "));
|
|
610
|
+
let score = scoreFormFieldTargetMatch(field, "amount");
|
|
611
|
+
if (/\bamount\b/.test(key)) {
|
|
612
|
+
score += 120;
|
|
613
|
+
}
|
|
614
|
+
if (field.inputType === "number" || field.inputMode === "numeric" || field.inputMode === "decimal") {
|
|
615
|
+
score += 90;
|
|
616
|
+
}
|
|
617
|
+
if (/^(?:0(?:\.0+)?|[0-9]+(?:\.[0-9]+)?)$/.test(normalizeKey(field.placeholder))) {
|
|
618
|
+
score += 45;
|
|
619
|
+
}
|
|
620
|
+
if (/\b(?:qty|quantity|size|value)\b/.test(key)) {
|
|
621
|
+
score += 40;
|
|
622
|
+
}
|
|
623
|
+
if (/\b(?:slippage|gas|recipient|address|token|contract)\b/.test(key)) {
|
|
624
|
+
score -= 80;
|
|
625
|
+
}
|
|
626
|
+
return { field, score };
|
|
627
|
+
})
|
|
628
|
+
.filter((candidate) => candidate.score > 0)
|
|
629
|
+
.sort((left, right) => right.score - left.score);
|
|
630
|
+
return ranked[0]?.field ?? null;
|
|
631
|
+
}
|
|
632
|
+
function findExplicitEntryField(pageState, target) {
|
|
633
|
+
if (/\b(?:naira|ngn)\b/i.test(target)) {
|
|
634
|
+
const nairaField = pageState.formFields.find((field) => /\b(?:ngn|naira|pay|spend)\b/i.test([field.label, field.placeholder, field.name, field.id].join(" ")));
|
|
635
|
+
if (nairaField) {
|
|
636
|
+
return nairaField;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
if (/\bcrypto\b/i.test(target) || /\b(?:usdt|btc|eth|usdc|token)\b/i.test(target)) {
|
|
640
|
+
const cryptoField = pageState.formFields.find((field) => /\b(?:crypto|token|usdt|btc|eth|receive|sell|receive|amount|qty)\b/i.test([field.label, field.placeholder, field.name, field.id].join(" ")));
|
|
641
|
+
if (cryptoField) {
|
|
642
|
+
return cryptoField;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
return (findMatchingFormField(pageState, target) ??
|
|
646
|
+
(/\b(?:amount|qty|quantity|size|value)\b/i.test(target) ? findTradeAmountField(pageState) : null));
|
|
647
|
+
}
|
|
648
|
+
function findExplicitFieldEntryForField(args) {
|
|
649
|
+
const candidates = parseTaskDirectives(args.taskGoal)
|
|
650
|
+
.filter((directive) => directive.action === "type_field" && !!directive.value)
|
|
651
|
+
.map((directive) => ({
|
|
652
|
+
value: directive.value ?? "",
|
|
653
|
+
target: directive.target
|
|
654
|
+
}));
|
|
655
|
+
const literalFieldEntryTask = extractLiteralFieldEntryTask(args.taskGoal);
|
|
656
|
+
if (literalFieldEntryTask) {
|
|
657
|
+
candidates.push(literalFieldEntryTask);
|
|
658
|
+
}
|
|
659
|
+
for (const candidate of candidates) {
|
|
660
|
+
if (!candidate.value || !candidate.target) {
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
const matchedField = findExplicitEntryField(args.pageState, candidate.target);
|
|
664
|
+
if (matchedField === args.field) {
|
|
665
|
+
return candidate;
|
|
666
|
+
}
|
|
667
|
+
if (scoreFormFieldTargetMatch(args.field, candidate.target) >= 80) {
|
|
668
|
+
return candidate;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
function resolveFormFieldTarget(field) {
|
|
674
|
+
return normalizeLineText(field.label || field.placeholder || field.name || field.id || field.inputType);
|
|
675
|
+
}
|
|
676
|
+
function resolveInteractiveTarget(item) {
|
|
677
|
+
return getInteractiveLabel(item);
|
|
678
|
+
}
|
|
679
|
+
function findFormFieldStepReference(args) {
|
|
680
|
+
const candidates = [args.field.label, args.field.placeholder, args.field.name, args.field.id].map((value) => normalizeLineText(value)).filter(Boolean);
|
|
681
|
+
for (let index = 0; index < args.pageState.visibleLines.length; index += 1) {
|
|
682
|
+
const line = normalizeLineText(args.pageState.visibleLines[index] || "");
|
|
683
|
+
if (!line) {
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
if (candidates.some((candidate) => normalizeKey(candidate) === normalizeKey(line))) {
|
|
687
|
+
return {
|
|
688
|
+
stepNumber: index + 1,
|
|
689
|
+
instructionQuote: line
|
|
690
|
+
};
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
stepNumber: null,
|
|
695
|
+
instructionQuote: resolveFormFieldTarget(args.field)
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
function isPlaceholderFieldValue(field, value) {
|
|
699
|
+
return isPlaceholderProfileFieldValue(field, value);
|
|
700
|
+
}
|
|
701
|
+
function findFirstPendingFormField(pageState) {
|
|
702
|
+
for (const field of pageState.formFields) {
|
|
703
|
+
if (field.inputType === "radio" &&
|
|
704
|
+
field.name &&
|
|
705
|
+
pageState.formFields.some((candidate) => candidate !== field && candidate.inputType === "radio" && candidate.name === field.name && candidate.checked)) {
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
const inferredValue = inferFormFieldValue(field, pageState.url);
|
|
709
|
+
if (!inferredValue) {
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
if (!isPlaceholderFieldValue(field, field.value || "")) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
return field;
|
|
716
|
+
}
|
|
717
|
+
return null;
|
|
718
|
+
}
|
|
719
|
+
function findInteractiveStepReference(args) {
|
|
720
|
+
const target = resolveInteractiveTarget(args.item);
|
|
721
|
+
for (let index = 0; index < args.pageState.visibleLines.length; index += 1) {
|
|
722
|
+
const line = normalizeLineText(args.pageState.visibleLines[index] || "");
|
|
723
|
+
if (!line) {
|
|
724
|
+
continue;
|
|
725
|
+
}
|
|
726
|
+
if (normalizeKey(line) === normalizeKey(target)) {
|
|
727
|
+
return {
|
|
728
|
+
stepNumber: index + 1,
|
|
729
|
+
instructionQuote: line
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
return {
|
|
734
|
+
stepNumber: null,
|
|
735
|
+
instructionQuote: target
|
|
736
|
+
};
|
|
737
|
+
}
|
|
738
|
+
function buildTaskInteractiveDecision(args) {
|
|
739
|
+
const target = resolveInteractiveTarget(args.item);
|
|
740
|
+
const stepReference = findInteractiveStepReference({
|
|
741
|
+
pageState: args.pageState,
|
|
742
|
+
item: args.item
|
|
743
|
+
});
|
|
744
|
+
return {
|
|
745
|
+
thought: args.thought,
|
|
746
|
+
stepNumber: stepReference.stepNumber,
|
|
747
|
+
instructionQuote: stepReference.instructionQuote,
|
|
748
|
+
action: "click",
|
|
749
|
+
target_id: args.item.agentId,
|
|
750
|
+
target,
|
|
751
|
+
text: "",
|
|
752
|
+
expectation: args.expectation,
|
|
753
|
+
friction: args.friction ?? "medium"
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
function findBestTaskInteractiveCandidate(args) {
|
|
757
|
+
const task = args.suite.tasks[args.taskIndex] ?? args.suite.tasks[0];
|
|
758
|
+
if (!task) {
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
const ranked = args.pageState.interactive
|
|
762
|
+
.map((item) => ({
|
|
763
|
+
item,
|
|
764
|
+
score: scoreInteractiveForTask({
|
|
765
|
+
task,
|
|
766
|
+
item,
|
|
767
|
+
history: args.history
|
|
768
|
+
})
|
|
769
|
+
}))
|
|
770
|
+
.filter((candidate) => Number.isFinite(candidate.score) &&
|
|
771
|
+
!(args.disallowTarget?.(resolveInteractiveTarget(candidate.item)) ?? false))
|
|
772
|
+
.sort((left, right) => right.score - left.score);
|
|
773
|
+
return ranked[0] ?? null;
|
|
774
|
+
}
|
|
775
|
+
function decisionTargetsPendingField(args) {
|
|
776
|
+
if (args.decision.action !== "type") {
|
|
777
|
+
return false;
|
|
778
|
+
}
|
|
779
|
+
if (normalizeTargetId(args.decision.target_id || "") === args.pendingField.agentId) {
|
|
780
|
+
return true;
|
|
781
|
+
}
|
|
782
|
+
const targetText = normalizeLineText(args.decision.target || args.decision.instructionQuote || "");
|
|
783
|
+
if (!targetText) {
|
|
784
|
+
return false;
|
|
785
|
+
}
|
|
786
|
+
const matchedField = findMatchingFormField(args.pageState, targetText);
|
|
787
|
+
if (matchedField === args.pendingField) {
|
|
788
|
+
return true;
|
|
789
|
+
}
|
|
790
|
+
return normalizeKey(targetText) === normalizeKey(resolveFormFieldTarget(args.pendingField));
|
|
791
|
+
}
|
|
792
|
+
function buildFormFirstDecision(args) {
|
|
793
|
+
const explicitEntry = findExplicitFieldEntryForField({
|
|
794
|
+
pageState: args.pageState,
|
|
795
|
+
taskGoal: args.taskGoal,
|
|
796
|
+
field: args.pendingField
|
|
797
|
+
});
|
|
798
|
+
const text = explicitEntry?.value ?? inferFormFieldValue(args.pendingField, args.pageState.url);
|
|
799
|
+
const target = resolveFormFieldTarget(args.pendingField);
|
|
800
|
+
const stepReference = findFormFieldStepReference({
|
|
801
|
+
pageState: args.pageState,
|
|
802
|
+
field: args.pendingField
|
|
803
|
+
});
|
|
804
|
+
if (!text) {
|
|
805
|
+
return args.originalDecision;
|
|
806
|
+
}
|
|
807
|
+
return {
|
|
808
|
+
thought: explicitEntry
|
|
809
|
+
? `The user supplied '${explicitEntry.value}' for '${explicitEntry.target}', so that exact value must be entered before '${args.originalDecision.target || args.originalDecision.instructionQuote}' can proceed.`
|
|
810
|
+
: `A visible form is still incomplete, so '${args.originalDecision.target || args.originalDecision.instructionQuote}' must wait until the next unresolved field is filled first.`,
|
|
811
|
+
stepNumber: stepReference.stepNumber,
|
|
812
|
+
instructionQuote: stepReference.instructionQuote,
|
|
813
|
+
action: "type",
|
|
814
|
+
target_id: args.pendingField.agentId,
|
|
815
|
+
target,
|
|
816
|
+
text,
|
|
817
|
+
expectation: explicitEntry
|
|
818
|
+
? `Fill '${target}' with exactly '${explicitEntry.value}' before attempting any later action on this form.`
|
|
819
|
+
: `Fill '${target}' before attempting any later field or submit action on this form.`,
|
|
820
|
+
friction: "medium"
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
function isOtpTriggerClickDecision(decision) {
|
|
824
|
+
if (decision.action !== "click") {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
const targetLabel = normalizeKey(decision.target || decision.instructionQuote || "");
|
|
828
|
+
return /\b(?:send\s*(?:otp|code)|get\s*(?:otp|code)|verify\s*email|request\s*(?:otp|code))\b/.test(targetLabel);
|
|
829
|
+
}
|
|
830
|
+
function enforceFormFirstDecision(args) {
|
|
831
|
+
if (taskHasPendingExplicitDirective(args.taskGoal, args.history)) {
|
|
832
|
+
return args.decision;
|
|
833
|
+
}
|
|
834
|
+
// Allow OTP trigger clicks to pass through even when there are pending form
|
|
835
|
+
// fields — the verification step must not be deferred until all fields are
|
|
836
|
+
// filled because many sites gate remaining fields behind OTP completion.
|
|
837
|
+
if (isOtpTriggerClickDecision(args.decision)) {
|
|
838
|
+
return args.decision;
|
|
839
|
+
}
|
|
840
|
+
if (!args.pageState.formsPresent || args.pageState.formFields.length === 0) {
|
|
841
|
+
return args.decision;
|
|
842
|
+
}
|
|
843
|
+
const pendingField = findFirstPendingFormField(args.pageState);
|
|
844
|
+
if (!pendingField) {
|
|
845
|
+
return args.decision;
|
|
846
|
+
}
|
|
847
|
+
if (decisionTargetsPendingField({
|
|
848
|
+
pageState: args.pageState,
|
|
849
|
+
decision: args.decision,
|
|
850
|
+
pendingField
|
|
851
|
+
})) {
|
|
852
|
+
return args.decision;
|
|
853
|
+
}
|
|
854
|
+
return buildFormFirstDecision({
|
|
855
|
+
taskGoal: args.taskGoal,
|
|
856
|
+
pageState: args.pageState,
|
|
857
|
+
pendingField,
|
|
858
|
+
originalDecision: args.decision
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
function enforceOrderedTaskDirectives(args) {
|
|
862
|
+
const directives = parseTaskDirectives(args.task.goal);
|
|
863
|
+
const literalFieldEntryTask = extractLiteralFieldEntryTask(args.task.goal);
|
|
864
|
+
if (directives.length === 0) {
|
|
865
|
+
return args.decision;
|
|
866
|
+
}
|
|
867
|
+
for (const directive of directives) {
|
|
868
|
+
if (directive.action === "click") {
|
|
869
|
+
if (historyHasSuccessfulClickTarget(args.history, directive.target)) {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
const matchingControl = findBestDirectiveInteractiveMatch(args.pageState, directive.target);
|
|
873
|
+
if (!matchingControl) {
|
|
874
|
+
if (pageShowsPendingWalletConnection(args.pageState)) {
|
|
875
|
+
return {
|
|
876
|
+
thought: `The next literal instruction is to click '${directive.target}', but the wallet connection flow is still visibly in progress on this page.`,
|
|
877
|
+
stepNumber: null,
|
|
878
|
+
instructionQuote: "",
|
|
879
|
+
action: "wait",
|
|
880
|
+
target_id: "",
|
|
881
|
+
target: "",
|
|
882
|
+
text: "",
|
|
883
|
+
expectation: `Wait for the pending wallet connection state to finish so '${directive.target}' can become visible before continuing.`,
|
|
884
|
+
friction: "low"
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
return buildStopDecision({
|
|
888
|
+
thought: `The next literal user instruction is to click '${directive.target}', but no visible control clearly matches that target on the current page.`,
|
|
889
|
+
expectation: `Stop instead of exploring unrelated controls before '${directive.target}' is visible or unambiguous.`
|
|
890
|
+
});
|
|
891
|
+
}
|
|
892
|
+
if (args.decision.action === "click" && directiveTargetsMatch(args.decision.target || args.decision.instructionQuote || "", directive.target)) {
|
|
893
|
+
return args.decision;
|
|
894
|
+
}
|
|
895
|
+
return buildTaskInteractiveDecision({
|
|
896
|
+
pageState: args.pageState,
|
|
897
|
+
item: matchingControl,
|
|
898
|
+
thought: `The user gave an explicit ordered step to click '${directive.target}', so that named control must be used before any later action in the instruction.`,
|
|
899
|
+
expectation: `Click '${resolveInteractiveTarget(matchingControl)}' and wait for the page to update before filling fields or using any other tab.`,
|
|
900
|
+
friction: "medium"
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
if (directive.action === "type_field") {
|
|
904
|
+
const explicitValue = directive.value ??
|
|
905
|
+
(literalFieldEntryTask && normalizeKey(directive.target).includes(normalizeKey(literalFieldEntryTask.target))
|
|
906
|
+
? literalFieldEntryTask.value
|
|
907
|
+
: null);
|
|
908
|
+
const explicitTarget = explicitValue && literalFieldEntryTask && normalizeKey(directive.target).includes(normalizeKey(literalFieldEntryTask.target))
|
|
909
|
+
? literalFieldEntryTask.target
|
|
910
|
+
: directive.target;
|
|
911
|
+
const field = findExplicitEntryField(args.pageState, explicitTarget);
|
|
912
|
+
if (!field) {
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
if (!isPlaceholderFieldValue(field, field.value || "")) {
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
const value = explicitValue ?? inferFormFieldValue(field, args.pageState.url);
|
|
919
|
+
if (!value) {
|
|
920
|
+
return buildStopDecision({
|
|
921
|
+
thought: `The next literal user instruction is to fill '${directive.target}', but there is no safe inferred value for that visible field.`,
|
|
922
|
+
expectation: `Stop instead of guessing data for '${directive.target}'.`
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
if (args.decision.action === "type" &&
|
|
926
|
+
(normalizeTargetId(args.decision.target_id || "") === field.agentId ||
|
|
927
|
+
normalizeKey(args.decision.target || args.decision.instructionQuote || "") === normalizeKey(resolveFormFieldTarget(field)))) {
|
|
928
|
+
return args.decision;
|
|
929
|
+
}
|
|
930
|
+
const stepReference = findFormFieldStepReference({
|
|
931
|
+
pageState: args.pageState,
|
|
932
|
+
field
|
|
933
|
+
});
|
|
934
|
+
return {
|
|
935
|
+
thought: `The user explicitly named '${directive.target}' as the next field to fill, so that field must be completed before any later form action.`,
|
|
936
|
+
stepNumber: stepReference.stepNumber,
|
|
937
|
+
instructionQuote: stepReference.instructionQuote,
|
|
938
|
+
action: "type",
|
|
939
|
+
target_id: field.agentId,
|
|
940
|
+
target: resolveFormFieldTarget(field),
|
|
941
|
+
text: value,
|
|
942
|
+
expectation: `Fill '${resolveFormFieldTarget(field)}' and wait for the form state to update before moving on.`,
|
|
943
|
+
friction: "medium"
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
if (directive.action === "fill_visible_form") {
|
|
947
|
+
if (!args.pageState.formsPresent) {
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
// Allow OTP trigger clicks to pass through even during a fill_visible_form
|
|
951
|
+
// directive — the OTP step is part of completing the form, not a deviation.
|
|
952
|
+
if (isOtpTriggerClickDecision(args.decision)) {
|
|
953
|
+
return args.decision;
|
|
954
|
+
}
|
|
955
|
+
const pendingField = findFirstPendingFormField(args.pageState);
|
|
956
|
+
if (!pendingField) {
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
return buildFormFirstDecision({
|
|
960
|
+
taskGoal: args.task.goal,
|
|
961
|
+
pageState: args.pageState,
|
|
962
|
+
pendingField,
|
|
963
|
+
originalDecision: args.decision
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
if (directive.action === "submit") {
|
|
967
|
+
if (!args.pageState.formsPresent) {
|
|
968
|
+
continue;
|
|
969
|
+
}
|
|
970
|
+
// Allow OTP trigger clicks through even during a submit directive —
|
|
971
|
+
// the OTP must be completed before the form can be submitted.
|
|
972
|
+
if (isOtpTriggerClickDecision(args.decision)) {
|
|
973
|
+
return args.decision;
|
|
974
|
+
}
|
|
975
|
+
const pendingField = findFirstPendingFormField(args.pageState);
|
|
976
|
+
if (pendingField) {
|
|
977
|
+
return buildFormFirstDecision({
|
|
978
|
+
taskGoal: args.task.goal,
|
|
979
|
+
pageState: args.pageState,
|
|
980
|
+
pendingField,
|
|
981
|
+
originalDecision: args.decision
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
const submitControl = findBestSubmitControl(args.pageState);
|
|
985
|
+
if (!submitControl) {
|
|
986
|
+
return buildStopDecision({
|
|
987
|
+
thought: "The next literal user instruction is to submit the visible form, but there is no clear visible submit-style control on the page.",
|
|
988
|
+
expectation: "Stop instead of exploring unrelated controls before the submit action is unambiguous."
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
if (args.decision.action === "click" && isSubmitLikeInteractiveLabel(args.decision.target || args.decision.instructionQuote || "")) {
|
|
992
|
+
if (!args.decision.target_id || args.decision.target_id === submitControl.agentId) {
|
|
993
|
+
return args.decision;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
return buildTaskInteractiveDecision({
|
|
997
|
+
pageState: args.pageState,
|
|
998
|
+
item: submitControl,
|
|
999
|
+
thought: "The user explicitly instructed the agent to submit after filling the visible form, so the submit-style control must be used next.",
|
|
1000
|
+
expectation: `Click '${resolveInteractiveTarget(submitControl)}' and verify whether the form submits or the next confirmation state appears.`,
|
|
1001
|
+
friction: "medium"
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
if (directive.action === "scroll" && args.decision.action !== "scroll") {
|
|
1005
|
+
return {
|
|
1006
|
+
thought: "The user explicitly instructed the next step to scroll, so do not substitute another control first.",
|
|
1007
|
+
stepNumber: null,
|
|
1008
|
+
instructionQuote: directive.raw,
|
|
1009
|
+
action: "scroll",
|
|
1010
|
+
target_id: "",
|
|
1011
|
+
target: "",
|
|
1012
|
+
text: "",
|
|
1013
|
+
expectation: "Scroll once, then reassess the next visible step from the same task.",
|
|
1014
|
+
friction: "low"
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
if (directive.action === "wait" && args.decision.action !== "wait") {
|
|
1018
|
+
return {
|
|
1019
|
+
thought: "The user explicitly instructed the next step to wait, so do not replace that with another action.",
|
|
1020
|
+
stepNumber: null,
|
|
1021
|
+
instructionQuote: directive.raw,
|
|
1022
|
+
action: "wait",
|
|
1023
|
+
target_id: "",
|
|
1024
|
+
target: "",
|
|
1025
|
+
text: "",
|
|
1026
|
+
expectation: "Wait briefly and observe whether the requested next state appears.",
|
|
1027
|
+
friction: "low"
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
if (directive.action === "back" && args.decision.action !== "back") {
|
|
1031
|
+
return {
|
|
1032
|
+
thought: "The user explicitly instructed the next step to go back, so do not click a different control first.",
|
|
1033
|
+
stepNumber: null,
|
|
1034
|
+
instructionQuote: directive.raw,
|
|
1035
|
+
action: "back",
|
|
1036
|
+
target_id: "",
|
|
1037
|
+
target: "",
|
|
1038
|
+
text: "",
|
|
1039
|
+
expectation: "Go back once and reassess the visible page state.",
|
|
1040
|
+
friction: "medium"
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
return args.decision;
|
|
1045
|
+
}
|
|
1046
|
+
function findNextInstruction(args) {
|
|
1047
|
+
const seenInstructions = new Set(args.history
|
|
1048
|
+
.filter((entry) => normalizeLineText(entry.url) === normalizeLineText(args.pageState.url) &&
|
|
1049
|
+
normalizeLineText(entry.title) === normalizeLineText(args.pageState.title))
|
|
1050
|
+
.map((entry) => normalizeLineText(entry.decision.instructionQuote || ""))
|
|
1051
|
+
.filter(Boolean));
|
|
1052
|
+
return extractOrderedInstructionLines(args.pageState).find((entry) => !seenInstructions.has(normalizeLineText(entry.instructionQuote))) ?? null;
|
|
1053
|
+
}
|
|
1054
|
+
function pageHasStrictStepContent(pageState) {
|
|
1055
|
+
return extractOrderedInstructionLines(pageState).length > 0;
|
|
1056
|
+
}
|
|
1057
|
+
function buildFallbackDecision(args) {
|
|
1058
|
+
const task = args.suite.tasks[args.taskIndex] ?? args.suite.tasks[0];
|
|
1059
|
+
if (!task) {
|
|
1060
|
+
return buildStopDecision({
|
|
1061
|
+
thought: "No accepted task was available for planning, so there is no safe next step to execute.",
|
|
1062
|
+
expectation: "Stop rather than guessing a task that was not provided."
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
const taskProfile = classifyTaskText(task.goal);
|
|
1066
|
+
const latestSuccessfulSubmit = [...args.history]
|
|
1067
|
+
.reverse()
|
|
1068
|
+
.find((entry) => entry.decision.action === "click" && entry.result.success && isSubmitLikeInteractiveLabel(entry.decision.target || ""));
|
|
1069
|
+
if (taskLooksLikeAccountCreation(task.goal) && latestSuccessfulSubmit && pageShowsAccountCreationSuccess(args.pageState)) {
|
|
1070
|
+
const localOnlyFallback = pageShowsAccountCreationLocalOnlyFallback(args.pageState);
|
|
1071
|
+
return buildStopDecision({
|
|
1072
|
+
thought: localOnlyFallback
|
|
1073
|
+
? "Model planning was unavailable, but a submit-style action already succeeded and the page now shows a browser-only fallback post-registration state, so continuing to click around would only add misleading noise."
|
|
1074
|
+
: "Model planning was unavailable, but the signup flow already appears complete because a submit-style action succeeded and the visible page now shows a post-registration state.",
|
|
1075
|
+
expectation: localOnlyFallback
|
|
1076
|
+
? "Stop and report that the form submitted, but the page explicitly indicates browser-only fallback storage rather than a shared persisted account state."
|
|
1077
|
+
: "Stop and report the successful post-registration state instead of clicking unrelated navigation controls."
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
if (taskLooksLikeAccountCreation(task.goal) &&
|
|
1081
|
+
latestSuccessfulSubmit &&
|
|
1082
|
+
pageShowsAccountCreationVerificationPending(args.pageState) &&
|
|
1083
|
+
!taskHasPendingExplicitDirective(task.goal, args.history)) {
|
|
1084
|
+
return buildStopDecision({
|
|
1085
|
+
thought: "Model planning was unavailable, but the signup flow is still explicitly requesting email or OTP verification after the last submit-style action, so the account is not complete yet.",
|
|
1086
|
+
expectation: "Stop and report the pending verification state instead of navigating away or claiming signup success.",
|
|
1087
|
+
friction: "medium"
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
if (historyHasRealTransactionClick(args.history)) {
|
|
1091
|
+
if (pageShowsTradeFinalOutcome(args.pageState)) {
|
|
1092
|
+
return buildStopDecision({
|
|
1093
|
+
thought: "Model planning was unavailable, but the real transaction flow has reached a visible final outcome after the execute step.",
|
|
1094
|
+
expectation: "Stop and report the visible transaction result instead of editing any remaining optional form fields.",
|
|
1095
|
+
friction: "low"
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
if (pageShowsTradePending(args.pageState)) {
|
|
1099
|
+
return {
|
|
1100
|
+
thought: "Model planning was unavailable, but a real transaction was already submitted and the page still shows it as sending or pending.",
|
|
1101
|
+
stepNumber: null,
|
|
1102
|
+
instructionQuote: "",
|
|
1103
|
+
action: "wait",
|
|
1104
|
+
target_id: "",
|
|
1105
|
+
target: "",
|
|
1106
|
+
text: "",
|
|
1107
|
+
expectation: "Wait for the transaction to settle to a visible success, failure, broadcast, or confirmation state before taking any other action.",
|
|
1108
|
+
friction: "low"
|
|
1109
|
+
};
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
const compactAmountTask = extractCompactAmountTask(task.goal);
|
|
1113
|
+
if (compactAmountTask) {
|
|
1114
|
+
const amountField = findTradeAmountField(args.pageState);
|
|
1115
|
+
if (!amountField) {
|
|
1116
|
+
return buildStopDecision({
|
|
1117
|
+
thought: `The accepted task is to ${compactAmountTask.action} '${compactAmountTask.amount}', but no visible amount field is present on the current page.`,
|
|
1118
|
+
expectation: "Stop instead of revisiting earlier controls or guessing a different trade step before the amount input is visible.",
|
|
1119
|
+
friction: "high"
|
|
1120
|
+
});
|
|
1121
|
+
}
|
|
1122
|
+
if (normalizeKey(amountField.value || "") === normalizeKey(compactAmountTask.amount)) {
|
|
1123
|
+
return buildStopDecision({
|
|
1124
|
+
thought: `The visible amount field already contains '${compactAmountTask.amount}', so this amount-entry task is complete.`,
|
|
1125
|
+
expectation: "Stop this task and continue to the next accepted step instead of clicking around the trade form.",
|
|
1126
|
+
friction: "low"
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
const stepReference = findFormFieldStepReference({
|
|
1130
|
+
pageState: args.pageState,
|
|
1131
|
+
field: amountField
|
|
1132
|
+
});
|
|
1133
|
+
return {
|
|
1134
|
+
thought: `The accepted task explicitly requests a ${compactAmountTask.action} amount of '${compactAmountTask.amount}', so the visible amount field must be filled before any later trade action.`,
|
|
1135
|
+
stepNumber: stepReference.stepNumber,
|
|
1136
|
+
instructionQuote: stepReference.instructionQuote,
|
|
1137
|
+
action: "type",
|
|
1138
|
+
target_id: amountField.agentId,
|
|
1139
|
+
target: resolveFormFieldTarget(amountField),
|
|
1140
|
+
text: compactAmountTask.amount,
|
|
1141
|
+
expectation: `Fill '${resolveFormFieldTarget(amountField)}' with '${compactAmountTask.amount}' and wait for the form state to update before any later trade action.`,
|
|
1142
|
+
friction: "medium"
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const literalFieldEntryTask = extractLiteralFieldEntryTask(task.goal);
|
|
1146
|
+
if (literalFieldEntryTask) {
|
|
1147
|
+
const field = findExplicitEntryField(args.pageState, literalFieldEntryTask.target);
|
|
1148
|
+
if (!field) {
|
|
1149
|
+
return buildStopDecision({
|
|
1150
|
+
thought: `The accepted task is to enter '${literalFieldEntryTask.value}' in '${literalFieldEntryTask.target}', but that field is not visible on the current page yet.`,
|
|
1151
|
+
expectation: "Stop instead of guessing a different field before the named input is visible.",
|
|
1152
|
+
friction: "high"
|
|
1153
|
+
});
|
|
1154
|
+
}
|
|
1155
|
+
if (normalizeKey(field.value || "") === normalizeKey(literalFieldEntryTask.value)) {
|
|
1156
|
+
return buildStopDecision({
|
|
1157
|
+
thought: `The visible '${resolveFormFieldTarget(field)}' field already contains '${literalFieldEntryTask.value}', so this entry task is complete.`,
|
|
1158
|
+
expectation: "Stop this task and continue to the next accepted step instead of typing the same value again.",
|
|
1159
|
+
friction: "low"
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
const stepReference = findFormFieldStepReference({
|
|
1163
|
+
pageState: args.pageState,
|
|
1164
|
+
field
|
|
1165
|
+
});
|
|
1166
|
+
return {
|
|
1167
|
+
thought: `The accepted task explicitly says to enter '${literalFieldEntryTask.value}' in '${literalFieldEntryTask.target}', so that named field should be filled directly before any later trade action.`,
|
|
1168
|
+
stepNumber: stepReference.stepNumber,
|
|
1169
|
+
instructionQuote: stepReference.instructionQuote,
|
|
1170
|
+
action: "type",
|
|
1171
|
+
target_id: field.agentId,
|
|
1172
|
+
target: resolveFormFieldTarget(field),
|
|
1173
|
+
text: literalFieldEntryTask.value,
|
|
1174
|
+
expectation: `Fill '${resolveFormFieldTarget(field)}' with '${literalFieldEntryTask.value}' and wait for the form state to update before any later action.`,
|
|
1175
|
+
friction: "medium"
|
|
1176
|
+
};
|
|
1177
|
+
}
|
|
1178
|
+
const pendingField = findFirstPendingFormField(args.pageState);
|
|
1179
|
+
// Detect inline OTP-trigger buttons (e.g. "Send OTP", "Send Code", "Verify Email") that need
|
|
1180
|
+
// to be clicked mid-form-fill, BEFORE continuing to fill remaining fields or submitting.
|
|
1181
|
+
const emailFieldFilled = args.pageState.formFields.some((field) => {
|
|
1182
|
+
const key = normalizeKey([field.label, field.placeholder, field.name, field.id, field.autocomplete].join(" "));
|
|
1183
|
+
return /\bemail\b/.test(key) && field.value && field.value.trim().length > 0;
|
|
1184
|
+
});
|
|
1185
|
+
if (emailFieldFilled) {
|
|
1186
|
+
const otpTriggerControl = args.pageState.interactive.find((item) => {
|
|
1187
|
+
if (item.disabled) {
|
|
1188
|
+
return false;
|
|
1189
|
+
}
|
|
1190
|
+
const label = normalizeKey(getInteractiveLabel(item));
|
|
1191
|
+
return /\b(?:send\s*(?:otp|code)|get\s*(?:otp|code)|verify\s*email|request\s*(?:otp|code))\b/.test(label);
|
|
1192
|
+
});
|
|
1193
|
+
if (otpTriggerControl) {
|
|
1194
|
+
const alreadyClicked = args.history.some((entry) => entry.decision.action === "click" &&
|
|
1195
|
+
entry.result.success &&
|
|
1196
|
+
normalizeKey(entry.decision.target || "").includes(normalizeKey(getInteractiveLabel(otpTriggerControl))));
|
|
1197
|
+
if (!alreadyClicked) {
|
|
1198
|
+
const target = resolveInteractiveTarget(otpTriggerControl);
|
|
1199
|
+
const stepReference = findInteractiveStepReference({
|
|
1200
|
+
pageState: args.pageState,
|
|
1201
|
+
item: otpTriggerControl
|
|
1202
|
+
});
|
|
1203
|
+
return {
|
|
1204
|
+
thought: "Model planning was unavailable, but the email field is filled and the page has a visible OTP/verification trigger button that must be clicked before proceeding with the rest of the form.",
|
|
1205
|
+
stepNumber: stepReference.stepNumber,
|
|
1206
|
+
instructionQuote: stepReference.instructionQuote,
|
|
1207
|
+
action: "click",
|
|
1208
|
+
target_id: otpTriggerControl.agentId,
|
|
1209
|
+
target,
|
|
1210
|
+
text: "",
|
|
1211
|
+
expectation: `Click '${target}' to trigger email verification or OTP delivery, then wait for the page to respond before filling remaining fields.`,
|
|
1212
|
+
friction: "medium"
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
if (pendingField) {
|
|
1218
|
+
const target = resolveFormFieldTarget(pendingField);
|
|
1219
|
+
const explicitEntry = findExplicitFieldEntryForField({
|
|
1220
|
+
pageState: args.pageState,
|
|
1221
|
+
taskGoal: task.goal,
|
|
1222
|
+
field: pendingField
|
|
1223
|
+
});
|
|
1224
|
+
const text = explicitEntry?.value ?? inferFormFieldValue(pendingField, args.pageState.url);
|
|
1225
|
+
const stepReference = findFormFieldStepReference({
|
|
1226
|
+
pageState: args.pageState,
|
|
1227
|
+
field: pendingField
|
|
1228
|
+
});
|
|
1229
|
+
if (text) {
|
|
1230
|
+
return {
|
|
1231
|
+
thought: explicitEntry
|
|
1232
|
+
? `Model planning was unavailable, but the user supplied '${explicitEntry.value}' for '${explicitEntry.target}', so use that exact value for the matching unresolved field.`
|
|
1233
|
+
: "Model planning was unavailable, so follow strict form order and fill the first unresolved visible field before any later action.",
|
|
1234
|
+
stepNumber: stepReference.stepNumber,
|
|
1235
|
+
instructionQuote: stepReference.instructionQuote,
|
|
1236
|
+
action: "type",
|
|
1237
|
+
target_id: pendingField.agentId,
|
|
1238
|
+
target,
|
|
1239
|
+
text,
|
|
1240
|
+
expectation: explicitEntry
|
|
1241
|
+
? `Fill '${target}' with exactly '${explicitEntry.value}' and wait for the field state to update before moving on.`
|
|
1242
|
+
: `Fill '${target}' with a safe fallback value and wait for the field state to update before moving on.`,
|
|
1243
|
+
friction: "medium"
|
|
1244
|
+
};
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
if (args.tradeOptions.enabled &&
|
|
1248
|
+
taskLooksLikeTrade(task.goal) &&
|
|
1249
|
+
pageLooksTradeReady(args.pageState) &&
|
|
1250
|
+
!historyHasAnyTradeAttempt(args.history)) {
|
|
1251
|
+
return {
|
|
1252
|
+
thought: "Model planning was unavailable, but the accepted task is a crypto sell or transfer flow and the page visibly exposes a deterministic wallet handoff target.",
|
|
1253
|
+
stepNumber: null,
|
|
1254
|
+
instructionQuote: "",
|
|
1255
|
+
action: "trade",
|
|
1256
|
+
target_id: "",
|
|
1257
|
+
target: "",
|
|
1258
|
+
text: "",
|
|
1259
|
+
expectation: "Extract the visible trade instruction and hand it to the deterministic wallet executor once.",
|
|
1260
|
+
friction: "medium"
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
if (taskHasPendingExplicitDirective(task.goal, args.history)) {
|
|
1264
|
+
return buildStopDecision({
|
|
1265
|
+
thought: "Model planning was unavailable and a submitted ordered step is still pending, but it does not currently map to a safe visible field or control.",
|
|
1266
|
+
expectation: "Stop instead of treating unrelated visible page labels as new instructions.",
|
|
1267
|
+
friction: "high"
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
const pageEvidenceText = normalizeTaskText([args.pageState.title, args.pageState.visibleText, ...args.pageState.headings].join(" "));
|
|
1271
|
+
const nextInstruction = findNextInstruction({
|
|
1272
|
+
pageState: args.pageState,
|
|
1273
|
+
history: args.history
|
|
1274
|
+
});
|
|
1275
|
+
if (nextInstruction) {
|
|
1276
|
+
const matchingField = findMatchingFormField(args.pageState, nextInstruction.instructionQuote);
|
|
1277
|
+
if (matchingField) {
|
|
1278
|
+
const formValue = inferFormFieldValue(matchingField, args.pageState.url);
|
|
1279
|
+
if (formValue) {
|
|
1280
|
+
const target = resolveFormFieldTarget(matchingField);
|
|
1281
|
+
return {
|
|
1282
|
+
thought: "Model planning was unavailable, but the next unread visible step matches a form field and can be advanced safely with dummy details.",
|
|
1283
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1284
|
+
instructionQuote: nextInstruction.instructionQuote,
|
|
1285
|
+
action: "type",
|
|
1286
|
+
target_id: matchingField.agentId,
|
|
1287
|
+
target,
|
|
1288
|
+
text: formValue,
|
|
1289
|
+
expectation: `Fill '${target}' for this single step, then wait for the form state to update before moving on.`,
|
|
1290
|
+
friction: "medium"
|
|
1291
|
+
};
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const directInteractiveControl = findInteractiveControl(args.pageState, nextInstruction.instructionQuote);
|
|
1295
|
+
if (directInteractiveControl) {
|
|
1296
|
+
const directInteractiveTarget = resolveInteractiveTarget(directInteractiveControl);
|
|
1297
|
+
if (violatesRunWideGuardrail({
|
|
1298
|
+
suite: args.suite,
|
|
1299
|
+
pageState: args.pageState,
|
|
1300
|
+
target: directInteractiveTarget
|
|
1301
|
+
})) {
|
|
1302
|
+
return buildStopDecision({
|
|
1303
|
+
thought: `Model planning was unavailable, and '${directInteractiveTarget}' would violate a run-wide user constraint from the submission.`,
|
|
1304
|
+
expectation: `Stop instead of using '${directInteractiveTarget}' because it would break the accepted guardrail.`,
|
|
1305
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1306
|
+
instructionQuote: nextInstruction.instructionQuote
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
// Block regressive navigation ("Back to Home", "Home", etc.) when the page
|
|
1310
|
+
// still has a visible form — this prevents abandoning the signup mid-flow
|
|
1311
|
+
// after an OTP timeout or validation error.
|
|
1312
|
+
if (args.pageState.formsPresent && isRegressiveTaskControlLabel(directInteractiveTarget)) {
|
|
1313
|
+
return buildStopDecision({
|
|
1314
|
+
thought: `Model planning was unavailable, and '${directInteractiveTarget}' is a regressive navigation control while a form is still present on the page. Clicking it would abandon the current form flow.`,
|
|
1315
|
+
expectation: `Stop instead of navigating away from the form via '${directInteractiveTarget}'.`,
|
|
1316
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1317
|
+
instructionQuote: nextInstruction.instructionQuote
|
|
1318
|
+
});
|
|
1319
|
+
}
|
|
1320
|
+
return {
|
|
1321
|
+
thought: "Model planning was unavailable, but the next unread visible step directly matches a visible control, so follow it exactly.",
|
|
1322
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1323
|
+
instructionQuote: nextInstruction.instructionQuote,
|
|
1324
|
+
action: "click",
|
|
1325
|
+
target_id: directInteractiveControl.agentId,
|
|
1326
|
+
target: directInteractiveTarget,
|
|
1327
|
+
text: "",
|
|
1328
|
+
expectation: `Click only '${directInteractiveTarget}' and wait for the page to update before considering any later instruction.`,
|
|
1329
|
+
friction: ACCESS_GATE_PATTERNS.some((pattern) => pattern.test(nextInstruction.instructionQuote)) ? "low" : "medium"
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
const clickTarget = extractClickTarget(nextInstruction.instructionQuote);
|
|
1333
|
+
if (clickTarget) {
|
|
1334
|
+
if (violatesRunWideGuardrail({
|
|
1335
|
+
suite: args.suite,
|
|
1336
|
+
pageState: args.pageState,
|
|
1337
|
+
target: clickTarget
|
|
1338
|
+
})) {
|
|
1339
|
+
return buildStopDecision({
|
|
1340
|
+
thought: `Model planning was unavailable, and '${clickTarget}' would violate a run-wide user constraint from the submission.`,
|
|
1341
|
+
expectation: `Stop instead of clicking '${clickTarget}' because it would break the accepted guardrail.`,
|
|
1342
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1343
|
+
instructionQuote: nextInstruction.instructionQuote
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
const matchingControl = findBestDirectiveInteractiveMatch(args.pageState, clickTarget);
|
|
1347
|
+
if (!matchingControl) {
|
|
1348
|
+
return buildStopDecision({
|
|
1349
|
+
thought: `Model planning was unavailable, and the named click target '${clickTarget}' does not map to any numbered visible control on the page.`,
|
|
1350
|
+
expectation: `Stop instead of guessing which numbered element might correspond to '${clickTarget}'.`,
|
|
1351
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1352
|
+
instructionQuote: nextInstruction.instructionQuote
|
|
1353
|
+
});
|
|
1354
|
+
}
|
|
1355
|
+
return {
|
|
1356
|
+
thought: "Model planning was unavailable, but the next unread visible instruction explicitly names a control, so follow that single step without skipping ahead.",
|
|
1357
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1358
|
+
instructionQuote: nextInstruction.instructionQuote,
|
|
1359
|
+
action: "click",
|
|
1360
|
+
target_id: matchingControl.agentId,
|
|
1361
|
+
target: resolveInteractiveTarget(matchingControl),
|
|
1362
|
+
text: "",
|
|
1363
|
+
expectation: `Click only '${resolveInteractiveTarget(matchingControl)}' and wait for the page to update before considering any later instruction.`,
|
|
1364
|
+
friction: "medium"
|
|
1365
|
+
};
|
|
1366
|
+
}
|
|
1367
|
+
const typeTarget = extractTypeTarget(nextInstruction.instructionQuote);
|
|
1368
|
+
if (typeTarget) {
|
|
1369
|
+
const field = findMatchingFormField(args.pageState, typeTarget);
|
|
1370
|
+
const formValue = field ? inferFormFieldValue(field, args.pageState.url) : null;
|
|
1371
|
+
if (field && formValue) {
|
|
1372
|
+
const target = resolveFormFieldTarget(field);
|
|
1373
|
+
return {
|
|
1374
|
+
thought: "Model planning was unavailable, but the next unread visible instruction explicitly asks for form input and a matching field is present.",
|
|
1375
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1376
|
+
instructionQuote: nextInstruction.instructionQuote,
|
|
1377
|
+
action: "type",
|
|
1378
|
+
target_id: field.agentId,
|
|
1379
|
+
target,
|
|
1380
|
+
text: formValue,
|
|
1381
|
+
expectation: `Fill '${target}' for this one step and wait for the form to reflect the new value before moving on.`,
|
|
1382
|
+
friction: "medium"
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
if (/^(?:step\s+\d+[:.)-]?\s*)?(?:scroll|swipe)\b/i.test(nextInstruction.instructionQuote)) {
|
|
1387
|
+
return {
|
|
1388
|
+
thought: "Model planning was unavailable, but the next unread visible instruction is an explicit scroll step.",
|
|
1389
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1390
|
+
instructionQuote: nextInstruction.instructionQuote,
|
|
1391
|
+
action: "scroll",
|
|
1392
|
+
target_id: "",
|
|
1393
|
+
target: "",
|
|
1394
|
+
text: "",
|
|
1395
|
+
expectation: "Scroll once, then wait for the page to settle before evaluating the next visible instruction.",
|
|
1396
|
+
friction: "low"
|
|
1397
|
+
};
|
|
1398
|
+
}
|
|
1399
|
+
if (/^(?:step\s+\d+[:.)-]?\s*)?(?:wait|pause|hold)\b/i.test(nextInstruction.instructionQuote)) {
|
|
1400
|
+
return {
|
|
1401
|
+
thought: "Model planning was unavailable, but the next unread visible instruction explicitly says to wait.",
|
|
1402
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1403
|
+
instructionQuote: nextInstruction.instructionQuote,
|
|
1404
|
+
action: "wait",
|
|
1405
|
+
target_id: "",
|
|
1406
|
+
target: "",
|
|
1407
|
+
text: "",
|
|
1408
|
+
expectation: "Wait briefly, observe the result, and do not execute any later step yet.",
|
|
1409
|
+
friction: "low"
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
if (/^(?:step\s+\d+[:.)-]?\s*)?(?:go back|back)\b/i.test(nextInstruction.instructionQuote)) {
|
|
1413
|
+
return {
|
|
1414
|
+
thought: "Model planning was unavailable, but the next unread visible instruction explicitly says to go back.",
|
|
1415
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1416
|
+
instructionQuote: nextInstruction.instructionQuote,
|
|
1417
|
+
action: "back",
|
|
1418
|
+
target_id: "",
|
|
1419
|
+
target: "",
|
|
1420
|
+
text: "",
|
|
1421
|
+
expectation: "Go back once and wait for the page update before reading further instructions.",
|
|
1422
|
+
friction: "medium"
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1425
|
+
return buildStopDecision({
|
|
1426
|
+
thought: "Model planning was unavailable and the next visible instruction cannot be executed safely without guessing, so strict mode requires stopping here.",
|
|
1427
|
+
expectation: "Stop and report that the current step is ambiguous instead of inferring a missing action.",
|
|
1428
|
+
stepNumber: nextInstruction.stepNumber,
|
|
1429
|
+
instructionQuote: nextInstruction.instructionQuote
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
if (pageHasStrictStepContent(args.pageState)) {
|
|
1433
|
+
return buildStopDecision({
|
|
1434
|
+
thought: "Model planning was unavailable and there are no clearly unread actionable steps left on the page that can be followed exactly.",
|
|
1435
|
+
expectation: "Stop rather than inventing a new step, substituting another control, or reordering the page instructions."
|
|
1436
|
+
});
|
|
1437
|
+
}
|
|
1438
|
+
if (taskProfile.engagement || taskProfile.gameplay || taskProfile.buttonCoverage || taskProfile.broadNavigation) {
|
|
1439
|
+
return buildStopDecision({
|
|
1440
|
+
thought: "Model planning was unavailable, and strict execution mode forbids guessing a next click from generic live controls.",
|
|
1441
|
+
expectation: "Stop and report that no exact instruction-aligned control could be selected safely."
|
|
1442
|
+
});
|
|
1443
|
+
}
|
|
1444
|
+
if (textHasInstructionCue(pageEvidenceText) || textHasOutcomeCue(pageEvidenceText)) {
|
|
1445
|
+
return {
|
|
1446
|
+
thought: "Model planning was unavailable, so preserve the visible page state as evidence rather than guessing the next step.",
|
|
1447
|
+
stepNumber: null,
|
|
1448
|
+
instructionQuote: "",
|
|
1449
|
+
action: "extract",
|
|
1450
|
+
target_id: "",
|
|
1451
|
+
target: "",
|
|
1452
|
+
text: "",
|
|
1453
|
+
expectation: "Record the current visible state exactly as shown and wait for a clearer next instruction.",
|
|
1454
|
+
friction: "low"
|
|
1455
|
+
};
|
|
1456
|
+
}
|
|
1457
|
+
return buildStopDecision({
|
|
1458
|
+
thought: "Model planning was unavailable and the page does not expose clear sequential instructions to follow safely.",
|
|
1459
|
+
expectation: "Stop and report that the next step is ambiguous instead of guessing."
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
function usesSelfHostedPlannerRuntime(llm) {
|
|
1463
|
+
const provider = llm?.provider ?? (process.env.LLM_PROVIDER?.trim().toLowerCase() === "ollama" ? "ollama" : "openai");
|
|
1464
|
+
if (provider === "ollama") {
|
|
1465
|
+
return true;
|
|
1466
|
+
}
|
|
1467
|
+
const openaiBaseUrl = process.env.OPENAI_BASE_URL?.trim();
|
|
1468
|
+
if (!openaiBaseUrl) {
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
return !/^https:\/\/api\.openai\.com\/?v1\/?$/i.test(openaiBaseUrl);
|
|
1472
|
+
}
|
|
1473
|
+
function resolvePlannerRequestOptions(llm) {
|
|
1474
|
+
// Retries are useful for hosted models, but on local/self-hosted runtimes they can
|
|
1475
|
+
// consume most of the run budget without yielding a better plan.
|
|
1476
|
+
if (usesSelfHostedPlannerRuntime(llm)) {
|
|
1477
|
+
return {
|
|
1478
|
+
timeoutMs: PLANNER_TIMEOUT_MS,
|
|
1479
|
+
maxRetries: 0
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
return {
|
|
1483
|
+
timeoutMs: PLANNER_TIMEOUT_MS,
|
|
1484
|
+
maxRetries: PLANNER_MAX_RETRIES
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
function enforceOtpTriggerPrioritization(args) {
|
|
1488
|
+
// Check if the email field is filled
|
|
1489
|
+
const emailFieldFilled = args.pageState.formFields.some((field) => {
|
|
1490
|
+
const key = normalizeKey([field.label, field.placeholder, field.name, field.id, field.autocomplete ?? ""].join(" "));
|
|
1491
|
+
return /\bemail\b/.test(key) && field.value && field.value.trim().length > 0;
|
|
1492
|
+
});
|
|
1493
|
+
if (!emailFieldFilled) {
|
|
1494
|
+
return args.decision;
|
|
1495
|
+
}
|
|
1496
|
+
// Check if there's a visible OTP trigger button
|
|
1497
|
+
const otpTriggerControl = args.pageState.interactive.find((item) => {
|
|
1498
|
+
if (item.disabled) {
|
|
1499
|
+
return false;
|
|
1500
|
+
}
|
|
1501
|
+
const label = normalizeKey(getInteractiveLabel(item));
|
|
1502
|
+
return /\b(?:send\s*(?:otp|code)|get\s*(?:otp|code)|verify\s*email|request\s*(?:otp|code))\b/.test(label);
|
|
1503
|
+
});
|
|
1504
|
+
if (!otpTriggerControl) {
|
|
1505
|
+
return args.decision;
|
|
1506
|
+
}
|
|
1507
|
+
// Check if the OTP trigger was already clicked
|
|
1508
|
+
const alreadyClicked = args.history.some((entry) => entry.decision.action === "click" &&
|
|
1509
|
+
entry.result.success &&
|
|
1510
|
+
normalizeKey(entry.decision.target || "").includes(normalizeKey(getInteractiveLabel(otpTriggerControl))));
|
|
1511
|
+
if (alreadyClicked) {
|
|
1512
|
+
return args.decision;
|
|
1513
|
+
}
|
|
1514
|
+
// Redirect to click the OTP trigger before submitting
|
|
1515
|
+
const target = resolveInteractiveTarget(otpTriggerControl);
|
|
1516
|
+
const stepReference = findInteractiveStepReference({
|
|
1517
|
+
pageState: args.pageState,
|
|
1518
|
+
item: otpTriggerControl
|
|
1519
|
+
});
|
|
1520
|
+
return {
|
|
1521
|
+
thought: `The form has an unclicked OTP/verification trigger '${target}' that must be completed before proceeding with the rest of the form. Redirecting from '${args.decision.target || args.decision.action}' to '${target}'.`,
|
|
1522
|
+
stepNumber: stepReference.stepNumber,
|
|
1523
|
+
instructionQuote: stepReference.instructionQuote,
|
|
1524
|
+
action: "click",
|
|
1525
|
+
target_id: otpTriggerControl.agentId,
|
|
1526
|
+
target,
|
|
1527
|
+
text: "",
|
|
1528
|
+
expectation: `Click '${target}' to trigger email verification before submitting the form.`,
|
|
1529
|
+
friction: "medium"
|
|
1530
|
+
};
|
|
1531
|
+
}
|
|
1532
|
+
function finalizePlannerDecision(args) {
|
|
1533
|
+
const singleTradeEnforcedDecision = args.decision.action === "trade" && historyHasAnyTradeAttempt(args.history)
|
|
1534
|
+
? buildStopDecision({
|
|
1535
|
+
thought: "A trade handoff was already attempted for this task, so another onchain send would risk a duplicate transfer.",
|
|
1536
|
+
expectation: "Stop instead of repeating a trade handoff on the same task."
|
|
1537
|
+
})
|
|
1538
|
+
: args.decision;
|
|
1539
|
+
return enforceLoopAvoidance({
|
|
1540
|
+
history: args.history,
|
|
1541
|
+
decision: enforceTargetIdDecision({
|
|
1542
|
+
taskGoal: args.task.goal,
|
|
1543
|
+
pageState: args.pageState,
|
|
1544
|
+
decision: enforceFormFirstDecision({
|
|
1545
|
+
taskGoal: args.task.goal,
|
|
1546
|
+
history: args.history,
|
|
1547
|
+
pageState: args.pageState,
|
|
1548
|
+
decision: enforceOtpTriggerPrioritization({
|
|
1549
|
+
pageState: args.pageState,
|
|
1550
|
+
history: args.history,
|
|
1551
|
+
decision: enforceOrderedTaskDirectives({
|
|
1552
|
+
task: args.task,
|
|
1553
|
+
pageState: args.pageState,
|
|
1554
|
+
history: args.history,
|
|
1555
|
+
decision: enrichDecisionTarget({
|
|
1556
|
+
pageState: args.pageState,
|
|
1557
|
+
decision: singleTradeEnforcedDecision
|
|
1558
|
+
})
|
|
1559
|
+
})
|
|
1560
|
+
})
|
|
1561
|
+
})
|
|
1562
|
+
})
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
export async function decideNextAction(args) {
|
|
1566
|
+
const task = args.suite.tasks[args.taskIndex] ?? args.suite.tasks[0];
|
|
1567
|
+
const plannerRequestOptions = resolvePlannerRequestOptions(args.llm);
|
|
1568
|
+
const orderedSteps = parseTaskDirectives(task.goal);
|
|
1569
|
+
const orderedStepNotes = buildTaskDirectiveSummary(task.goal);
|
|
1570
|
+
const hasUnstructuredSteps = orderedSteps.some((directive) => directive.action === "unstructured");
|
|
1571
|
+
const orderedStepConfidence = orderedSteps.length === 0 ? "none" : hasUnstructuredSteps ? "low" : "high";
|
|
1572
|
+
const accessProfile = getPreferredAccessIdentity(args.pageState.url);
|
|
1573
|
+
const walletAddress = isWalletConfigured() ? await getWalletAddress().catch(() => "") : "";
|
|
1574
|
+
const tradeOptions = {
|
|
1575
|
+
...buildDefaultTradeRunOptions(),
|
|
1576
|
+
...(args.tradeOptions ?? {})
|
|
1577
|
+
};
|
|
1578
|
+
const recentTransactions = await listTransactions(5).catch(() => []);
|
|
1579
|
+
const walletBalances = await getWalletBalances().catch(() => ({}));
|
|
1580
|
+
const payload = PlannerInputSchema.parse({
|
|
1581
|
+
persona: args.suite.persona,
|
|
1582
|
+
task: {
|
|
1583
|
+
...task,
|
|
1584
|
+
original_instruction: task.goal,
|
|
1585
|
+
ordered_step_confidence: orderedStepConfidence,
|
|
1586
|
+
ordered_steps: orderedSteps.map((directive) => ({
|
|
1587
|
+
action: directive.action,
|
|
1588
|
+
target: directive.target,
|
|
1589
|
+
raw: directive.raw
|
|
1590
|
+
})),
|
|
1591
|
+
ordered_step_notes: orderedStepNotes
|
|
1592
|
+
},
|
|
1593
|
+
siteBrief: args.siteBrief,
|
|
1594
|
+
accessProfile: {
|
|
1595
|
+
...accessProfile,
|
|
1596
|
+
walletAddress,
|
|
1597
|
+
walletChainId: isWalletConfigured() ? getWalletChainId() : null,
|
|
1598
|
+
tradeEnabled: tradeOptions.enabled
|
|
1599
|
+
},
|
|
1600
|
+
pageState: args.pageState,
|
|
1601
|
+
recentPaystackTransactions: recentTransactions,
|
|
1602
|
+
walletBalances,
|
|
1603
|
+
...(args.remainingSeconds !== undefined ? { remainingSeconds: args.remainingSeconds } : {}),
|
|
1604
|
+
previous_actions: args.history.slice(-20).map((item) => ({
|
|
1605
|
+
step: item.step,
|
|
1606
|
+
action: item.decision.action,
|
|
1607
|
+
target_id: item.decision.target_id,
|
|
1608
|
+
target: item.decision.target,
|
|
1609
|
+
success: item.result.success,
|
|
1610
|
+
state_changed: item.result.stateChanged ?? false,
|
|
1611
|
+
note: item.result.note
|
|
1612
|
+
})),
|
|
1613
|
+
history: args.history.slice(-20).map((item) => ({
|
|
1614
|
+
step: item.step,
|
|
1615
|
+
url: item.url,
|
|
1616
|
+
title: item.title,
|
|
1617
|
+
decision: {
|
|
1618
|
+
stepNumber: item.decision.stepNumber,
|
|
1619
|
+
instructionQuote: item.decision.instructionQuote,
|
|
1620
|
+
action: item.decision.action,
|
|
1621
|
+
target_id: item.decision.target_id,
|
|
1622
|
+
target: item.decision.target,
|
|
1623
|
+
expectation: item.decision.expectation,
|
|
1624
|
+
friction: item.decision.friction
|
|
1625
|
+
},
|
|
1626
|
+
result: {
|
|
1627
|
+
success: item.result.success,
|
|
1628
|
+
note: item.result.note,
|
|
1629
|
+
...(item.result.stateChanged !== undefined ? { stateChanged: item.result.stateChanged } : {})
|
|
1630
|
+
}
|
|
1631
|
+
}))
|
|
1632
|
+
});
|
|
1633
|
+
try {
|
|
1634
|
+
const decision = await generateStructured({
|
|
1635
|
+
...(args.llm ?? {}),
|
|
1636
|
+
systemPrompt: BROWSER_AGENT_PROMPT,
|
|
1637
|
+
userPayload: payload,
|
|
1638
|
+
schemaName: "planner_decision",
|
|
1639
|
+
schema: PlannerDecisionSchema,
|
|
1640
|
+
timeoutMs: plannerRequestOptions.timeoutMs,
|
|
1641
|
+
maxRetries: plannerRequestOptions.maxRetries
|
|
1642
|
+
});
|
|
1643
|
+
return {
|
|
1644
|
+
decision: finalizePlannerDecision({
|
|
1645
|
+
task,
|
|
1646
|
+
pageState: args.pageState,
|
|
1647
|
+
history: args.history,
|
|
1648
|
+
decision: PlannerDecisionSchema.parse(decision)
|
|
1649
|
+
})
|
|
1650
|
+
};
|
|
1651
|
+
}
|
|
1652
|
+
catch (error) {
|
|
1653
|
+
return {
|
|
1654
|
+
decision: finalizePlannerDecision({
|
|
1655
|
+
task,
|
|
1656
|
+
pageState: args.pageState,
|
|
1657
|
+
history: args.history,
|
|
1658
|
+
decision: buildFallbackDecision({
|
|
1659
|
+
suite: args.suite,
|
|
1660
|
+
taskIndex: args.taskIndex,
|
|
1661
|
+
pageState: args.pageState,
|
|
1662
|
+
history: args.history,
|
|
1663
|
+
tradeOptions
|
|
1664
|
+
})
|
|
1665
|
+
}),
|
|
1666
|
+
fallbackReason: cleanErrorMessage(error)
|
|
1667
|
+
};
|
|
1668
|
+
}
|
|
1669
|
+
}
|