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.
Files changed (81) hide show
  1. package/README.md +689 -0
  2. package/dist/auth/credentialStore.js +62 -0
  3. package/dist/auth/inbox.js +193 -0
  4. package/dist/auth/profile.js +379 -0
  5. package/dist/auth/runner.js +1124 -0
  6. package/dist/backend/dashboardData.js +194 -0
  7. package/dist/backend/runArtifacts.js +48 -0
  8. package/dist/backend/runRepository.js +93 -0
  9. package/dist/bin.js +2 -0
  10. package/dist/cli/backfillSiteChecks.js +143 -0
  11. package/dist/cli/run.js +309 -0
  12. package/dist/cli/trade.js +69 -0
  13. package/dist/config.js +199 -0
  14. package/dist/core/agentProfiles.js +55 -0
  15. package/dist/core/aggregateReport.js +382 -0
  16. package/dist/core/audit.js +30 -0
  17. package/dist/core/customTaskSuite.js +148 -0
  18. package/dist/core/evaluator.js +217 -0
  19. package/dist/core/executor.js +788 -0
  20. package/dist/core/fallbackReport.js +335 -0
  21. package/dist/core/formHeuristics.js +411 -0
  22. package/dist/core/gameplaySummary.js +164 -0
  23. package/dist/core/interaction.js +202 -0
  24. package/dist/core/pageState.js +201 -0
  25. package/dist/core/planner.js +1669 -0
  26. package/dist/core/processSubmissionBatch.js +204 -0
  27. package/dist/core/runAuditJob.js +170 -0
  28. package/dist/core/runner.js +2352 -0
  29. package/dist/core/siteBrief.js +107 -0
  30. package/dist/core/siteChecks.js +1526 -0
  31. package/dist/core/taskDirectives.js +279 -0
  32. package/dist/core/taskHeuristics.js +263 -0
  33. package/dist/dashboard/client.js +1256 -0
  34. package/dist/dashboard/contracts.js +95 -0
  35. package/dist/dashboard/narrative.js +277 -0
  36. package/dist/dashboard/server.js +458 -0
  37. package/dist/dashboard/theme.js +888 -0
  38. package/dist/index.js +84 -0
  39. package/dist/llm/client.js +188 -0
  40. package/dist/paystack/account.js +123 -0
  41. package/dist/paystack/client.js +100 -0
  42. package/dist/paystack/index.js +13 -0
  43. package/dist/paystack/test-paystack.js +83 -0
  44. package/dist/paystack/transfer.js +138 -0
  45. package/dist/paystack/types.js +74 -0
  46. package/dist/paystack/webhook.js +121 -0
  47. package/dist/prompts/browserAgent.js +124 -0
  48. package/dist/prompts/reviewer.js +71 -0
  49. package/dist/reporting/clickReplay.js +290 -0
  50. package/dist/reporting/html.js +930 -0
  51. package/dist/reporting/markdown.js +238 -0
  52. package/dist/reporting/template.js +1141 -0
  53. package/dist/schemas/types.js +361 -0
  54. package/dist/submissions/customTasks.js +196 -0
  55. package/dist/submissions/html.js +770 -0
  56. package/dist/submissions/model.js +56 -0
  57. package/dist/submissions/publicUrl.js +76 -0
  58. package/dist/submissions/service.js +74 -0
  59. package/dist/submissions/store.js +37 -0
  60. package/dist/submissions/types.js +65 -0
  61. package/dist/trade/engine.js +241 -0
  62. package/dist/trade/evm/erc20.js +44 -0
  63. package/dist/trade/extractor.js +148 -0
  64. package/dist/trade/policy.js +35 -0
  65. package/dist/trade/session.js +31 -0
  66. package/dist/trade/types.js +107 -0
  67. package/dist/trade/validator.js +148 -0
  68. package/dist/utils/files.js +59 -0
  69. package/dist/utils/log.js +24 -0
  70. package/dist/utils/playwrightCompat.js +14 -0
  71. package/dist/utils/time.js +3 -0
  72. package/dist/wallet/provider.js +345 -0
  73. package/dist/wallet/relay.js +129 -0
  74. package/dist/wallet/wallet.js +178 -0
  75. package/docs/01-installation.md +134 -0
  76. package/docs/02-running-your-first-audit.md +136 -0
  77. package/docs/03-configuration.md +233 -0
  78. package/docs/04-how-the-agent-thinks.md +41 -0
  79. package/docs/05-extending-personas-and-tasks.md +42 -0
  80. package/docs/06-hardening-for-production.md +92 -0
  81. 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
+ }