cbrowser 18.34.2 → 18.36.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/dist/analysis/accessibility-empathy.d.ts.map +1 -1
- package/dist/analysis/accessibility-empathy.js +57 -8
- package/dist/analysis/accessibility-empathy.js.map +1 -1
- package/dist/analysis/page-understanding.d.ts +118 -0
- package/dist/analysis/page-understanding.d.ts.map +1 -0
- package/dist/analysis/page-understanding.js +940 -0
- package/dist/analysis/page-understanding.js.map +1 -0
- package/dist/browser/index.d.ts +2 -0
- package/dist/browser/index.d.ts.map +1 -1
- package/dist/browser/index.js +1 -0
- package/dist/browser/index.js.map +1 -1
- package/dist/browser/site-profile-manager.d.ts +116 -0
- package/dist/browser/site-profile-manager.d.ts.map +1 -0
- package/dist/browser/site-profile-manager.js +495 -0
- package/dist/browser/site-profile-manager.js.map +1 -0
- package/dist/cognitive/goal-decomposer.d.ts +127 -0
- package/dist/cognitive/goal-decomposer.d.ts.map +1 -0
- package/dist/cognitive/goal-decomposer.js +902 -0
- package/dist/cognitive/goal-decomposer.js.map +1 -0
- package/dist/cognitive/goal-types.d.ts +140 -0
- package/dist/cognitive/goal-types.d.ts.map +1 -0
- package/dist/cognitive/goal-types.js +136 -0
- package/dist/cognitive/goal-types.js.map +1 -0
- package/dist/cognitive/index.d.ts +2 -0
- package/dist/cognitive/index.d.ts.map +1 -1
- package/dist/cognitive/index.js +5 -0
- package/dist/cognitive/index.js.map +1 -1
- package/dist/mcp-tools/base/audit-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/audit-tools.js +5 -2
- package/dist/mcp-tools/base/audit-tools.js.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/cognitive-tools.js +72 -2
- package/dist/mcp-tools/base/cognitive-tools.js.map +1 -1
- package/dist/mcp-tools/base/index.d.ts +4 -2
- package/dist/mcp-tools/base/index.d.ts.map +1 -1
- package/dist/mcp-tools/base/index.js +7 -2
- package/dist/mcp-tools/base/index.js.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/interaction-tools.js +23 -0
- package/dist/mcp-tools/base/interaction-tools.js.map +1 -1
- package/dist/mcp-tools/base/navigation-tools.d.ts.map +1 -1
- package/dist/mcp-tools/base/navigation-tools.js +13 -0
- package/dist/mcp-tools/base/navigation-tools.js.map +1 -1
- package/dist/mcp-tools/base/site-knowledge-tools.d.ts +15 -0
- package/dist/mcp-tools/base/site-knowledge-tools.d.ts.map +1 -0
- package/dist/mcp-tools/base/site-knowledge-tools.js +314 -0
- package/dist/mcp-tools/base/site-knowledge-tools.js.map +1 -0
- package/dist/mcp-tools/index.d.ts +6 -6
- package/dist/mcp-tools/index.d.ts.map +1 -1
- package/dist/mcp-tools/index.js +7 -7
- package/dist/mcp-tools/index.js.map +1 -1
- package/dist/personas.d.ts.map +1 -1
- package/dist/personas.js +369 -0
- package/dist/personas.js.map +1 -1
- package/dist/site-model/manager.d.ts +161 -0
- package/dist/site-model/manager.d.ts.map +1 -0
- package/dist/site-model/manager.js +825 -0
- package/dist/site-model/manager.js.map +1 -0
- package/dist/site-model/types.d.ts +108 -0
- package/dist/site-model/types.d.ts.map +1 -0
- package/dist/site-model/types.js +10 -0
- package/dist/site-model/types.js.map +1 -0
- package/dist/types.d.ts +18 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,902 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CBrowser - Goal Decomposer
|
|
3
|
+
* Transforms user goals into executable sub-goal trees with fallback strategies.
|
|
4
|
+
* Uses site model for informed planning, Claude API for novel goals.
|
|
5
|
+
*
|
|
6
|
+
* @copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com https://cbrowser.ai
|
|
7
|
+
* @license MIT
|
|
8
|
+
* @since v18.35.0
|
|
9
|
+
*/
|
|
10
|
+
import { GOAL_PATTERNS, INFORMATION_PATTERNS, } from "./goal-types.js";
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Constants
|
|
13
|
+
// ============================================================================
|
|
14
|
+
/** Common URL patterns mapped from semantic intent to likely paths */
|
|
15
|
+
const URL_PATTERNS = {
|
|
16
|
+
pricing: ["/pricing", "/plans", "/packages"],
|
|
17
|
+
contact: ["/contact", "/contact-us", "/about"],
|
|
18
|
+
about: ["/about", "/about-us", "/company"],
|
|
19
|
+
help: ["/help", "/support", "/faq"],
|
|
20
|
+
login: ["/login", "/signin", "/auth"],
|
|
21
|
+
signup: ["/signup", "/register", "/join"],
|
|
22
|
+
admissions: ["/admissions", "/apply", "/enrollment"],
|
|
23
|
+
careers: ["/careers", "/jobs", "/hiring"],
|
|
24
|
+
docs: ["/docs", "/documentation", "/api"],
|
|
25
|
+
blog: ["/blog", "/news", "/articles"],
|
|
26
|
+
search: ["/search", "/find", "/results"],
|
|
27
|
+
settings: ["/settings", "/account", "/profile"],
|
|
28
|
+
privacy: ["/privacy", "/privacy-policy"],
|
|
29
|
+
terms: ["/terms", "/terms-of-service", "/tos"],
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* English stop words to strip when extracting meaningful keywords from goals.
|
|
33
|
+
* Kept deliberately minimal -- we want to be aggressive about retaining
|
|
34
|
+
* domain-relevant words even if they look like common words.
|
|
35
|
+
*/
|
|
36
|
+
const STOP_WORDS = new Set([
|
|
37
|
+
"a", "an", "the", "is", "are", "was", "were", "be", "been", "being",
|
|
38
|
+
"have", "has", "had", "do", "does", "did", "will", "would", "could",
|
|
39
|
+
"should", "may", "might", "shall", "can", "need", "dare", "ought",
|
|
40
|
+
"used", "to", "of", "in", "for", "on", "with", "at", "by", "from",
|
|
41
|
+
"as", "into", "through", "during", "before", "after", "above", "below",
|
|
42
|
+
"between", "out", "off", "over", "under", "again", "further", "then",
|
|
43
|
+
"once", "here", "there", "all", "each", "every", "both", "few",
|
|
44
|
+
"more", "most", "other", "some", "such", "no", "nor", "not", "only",
|
|
45
|
+
"own", "same", "so", "than", "too", "very", "just", "because",
|
|
46
|
+
"but", "and", "or", "if", "while", "although", "that", "this",
|
|
47
|
+
"these", "those", "i", "me", "my", "we", "our", "you", "your",
|
|
48
|
+
"it", "its", "they", "them", "their", "what", "which", "who",
|
|
49
|
+
"whom", "whose", "when", "where", "why", "how", "any",
|
|
50
|
+
]);
|
|
51
|
+
/** Default action timeout in ms */
|
|
52
|
+
const DEFAULT_ACTION_TIMEOUT = 10_000;
|
|
53
|
+
/** Minimum site model success rate to trust a known path */
|
|
54
|
+
const SITE_MODEL_MIN_SUCCESS_RATE = 0.6;
|
|
55
|
+
// ============================================================================
|
|
56
|
+
// GoalDecomposer Class
|
|
57
|
+
// ============================================================================
|
|
58
|
+
/**
|
|
59
|
+
* Transforms natural-language user goals into structured, executable plans.
|
|
60
|
+
*
|
|
61
|
+
* Planning priority:
|
|
62
|
+
* 1. Site model -- if we have seen this domain before and have a path with
|
|
63
|
+
* >60% success rate, we reuse it.
|
|
64
|
+
* 2. Heuristic -- keyword-driven strategy generation using common web patterns.
|
|
65
|
+
* 3. Claude API -- future enhancement for truly novel goals (TODO).
|
|
66
|
+
*/
|
|
67
|
+
export class GoalDecomposer {
|
|
68
|
+
siteModelManager;
|
|
69
|
+
constructor(siteModelManager) {
|
|
70
|
+
this.siteModelManager = siteModelManager;
|
|
71
|
+
}
|
|
72
|
+
// --------------------------------------------------------------------------
|
|
73
|
+
// Public API
|
|
74
|
+
// --------------------------------------------------------------------------
|
|
75
|
+
/**
|
|
76
|
+
* Parse a natural language goal into a structured ParsedGoal.
|
|
77
|
+
*
|
|
78
|
+
* Classification pipeline:
|
|
79
|
+
* 1. Lowercase and normalize the goal text.
|
|
80
|
+
* 2. Match against GOAL_PATTERNS to determine GoalType.
|
|
81
|
+
* - Longest matching phrase wins.
|
|
82
|
+
* - Ties broken by declaration order in the patterns object.
|
|
83
|
+
* 3. If GoalType is "find_information", sub-classify via INFORMATION_PATTERNS.
|
|
84
|
+
* 4. Extract keywords by stripping stop words from the original text.
|
|
85
|
+
* 5. Assign confidence based on match strength.
|
|
86
|
+
*/
|
|
87
|
+
parseGoal(goal) {
|
|
88
|
+
const normalized = goal.toLowerCase().trim();
|
|
89
|
+
// Classify the goal type
|
|
90
|
+
const { type, confidence: typeConfidence } = this.classifyGoalType(normalized);
|
|
91
|
+
// Sub-classify information type if applicable
|
|
92
|
+
let informationType;
|
|
93
|
+
if (type === "find_information") {
|
|
94
|
+
informationType = this.classifyInformationType(normalized);
|
|
95
|
+
}
|
|
96
|
+
// Extract meaningful keywords
|
|
97
|
+
const keywords = this.extractKeywords(normalized);
|
|
98
|
+
return {
|
|
99
|
+
originalText: goal,
|
|
100
|
+
type,
|
|
101
|
+
informationType,
|
|
102
|
+
keywords,
|
|
103
|
+
confidence: typeConfidence,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Generate a complete execution plan for a goal on a specific domain.
|
|
108
|
+
*
|
|
109
|
+
* Strategy:
|
|
110
|
+
* 1. Parse the goal.
|
|
111
|
+
* 2. Check site model for known successful paths.
|
|
112
|
+
* 3. If no site model coverage, generate heuristic strategies.
|
|
113
|
+
* 4. Future: fall back to Claude API for truly novel goals.
|
|
114
|
+
*/
|
|
115
|
+
async decompose(goal, domain) {
|
|
116
|
+
const parsedGoal = this.parseGoal(goal);
|
|
117
|
+
// Attempt 1: Site model -- reuse known successful paths
|
|
118
|
+
const knownPath = this.siteModelManager.queryBestPath(domain, parsedGoal.type);
|
|
119
|
+
if (knownPath && knownPath.successRate >= SITE_MODEL_MIN_SUCCESS_RATE) {
|
|
120
|
+
return this.buildSiteModelPlan(parsedGoal, knownPath, domain);
|
|
121
|
+
}
|
|
122
|
+
// Attempt 2: Heuristic -- generate strategies from patterns
|
|
123
|
+
const heuristicPlan = this.buildHeuristicPlan(parsedGoal, domain);
|
|
124
|
+
// TODO: Attempt 3 -- Claude API for novel goals.
|
|
125
|
+
// When implemented, this would call the Anthropic API to generate
|
|
126
|
+
// a custom plan for goals that don't match any heuristic pattern.
|
|
127
|
+
// Gated behind isApiKeyConfigured() check.
|
|
128
|
+
// For now, the heuristic plan is always returned.
|
|
129
|
+
return heuristicPlan;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Record the outcome of a goal execution back to the site model.
|
|
133
|
+
* This closes the learning loop: successful paths get reinforced,
|
|
134
|
+
* failures get cataloged for future avoidance.
|
|
135
|
+
*/
|
|
136
|
+
async recordOutcome(domain, result) {
|
|
137
|
+
if (result.achieved) {
|
|
138
|
+
// Extract the actions from the first successfully used strategy
|
|
139
|
+
// across all completed sub-goals
|
|
140
|
+
const successfulActions = this.extractSuccessfulActions(result);
|
|
141
|
+
this.siteModelManager.recordGoalPath(domain, result.plan.goal.originalText, result.plan.goal.type, successfulActions);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
// Record failure patterns so future plans can avoid them
|
|
145
|
+
const failureConditions = [
|
|
146
|
+
`goal_type:${result.plan.goal.type}`,
|
|
147
|
+
`steps_executed:${result.stepsExecuted}`,
|
|
148
|
+
`strategies_attempted:${result.strategiesAttempted}`,
|
|
149
|
+
`sub_goals_completed:${result.subGoalsCompleted}/${result.subGoalsTotal}`,
|
|
150
|
+
];
|
|
151
|
+
if (result.failureReason) {
|
|
152
|
+
failureConditions.push(`reason:${result.failureReason}`);
|
|
153
|
+
}
|
|
154
|
+
this.siteModelManager.recordFailure(domain, domain, // page URL defaults to domain root for goal-level failures
|
|
155
|
+
"goal_execution_failed", failureConditions);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// --------------------------------------------------------------------------
|
|
159
|
+
// Goal Classification (Private)
|
|
160
|
+
// --------------------------------------------------------------------------
|
|
161
|
+
/**
|
|
162
|
+
* Classify goal text against GOAL_PATTERNS.
|
|
163
|
+
* Uses longest-match-wins to resolve ambiguity.
|
|
164
|
+
*/
|
|
165
|
+
classifyGoalType(normalizedGoal) {
|
|
166
|
+
let bestType = "find_information"; // safe default
|
|
167
|
+
let bestMatchLength = 0;
|
|
168
|
+
let matchFound = false;
|
|
169
|
+
const goalTypes = Object.keys(GOAL_PATTERNS);
|
|
170
|
+
for (const goalType of goalTypes) {
|
|
171
|
+
const patterns = GOAL_PATTERNS[goalType];
|
|
172
|
+
for (const pattern of patterns) {
|
|
173
|
+
if (normalizedGoal.includes(pattern)) {
|
|
174
|
+
if (pattern.length > bestMatchLength) {
|
|
175
|
+
bestMatchLength = pattern.length;
|
|
176
|
+
bestType = goalType;
|
|
177
|
+
matchFound = true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// Assign confidence based on match quality
|
|
183
|
+
let confidence;
|
|
184
|
+
if (matchFound && bestMatchLength >= 6) {
|
|
185
|
+
// Strong match: multi-word pattern like "sign up", "navigate to"
|
|
186
|
+
confidence = 0.9;
|
|
187
|
+
}
|
|
188
|
+
else if (matchFound) {
|
|
189
|
+
// Partial match: short keyword like "find", "buy", "open"
|
|
190
|
+
confidence = 0.7;
|
|
191
|
+
}
|
|
192
|
+
else {
|
|
193
|
+
// No keyword match at all -- guessing find_information as safest default
|
|
194
|
+
confidence = 0.5;
|
|
195
|
+
}
|
|
196
|
+
return { type: bestType, confidence };
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Sub-classify an information-seeking goal by the kind of information sought.
|
|
200
|
+
*/
|
|
201
|
+
classifyInformationType(normalizedGoal) {
|
|
202
|
+
let bestType;
|
|
203
|
+
let bestMatchLength = 0;
|
|
204
|
+
const infoTypes = Object.keys(INFORMATION_PATTERNS);
|
|
205
|
+
for (const infoType of infoTypes) {
|
|
206
|
+
const patterns = INFORMATION_PATTERNS[infoType];
|
|
207
|
+
for (const pattern of patterns) {
|
|
208
|
+
if (normalizedGoal.includes(pattern)) {
|
|
209
|
+
if (pattern.length > bestMatchLength) {
|
|
210
|
+
bestMatchLength = pattern.length;
|
|
211
|
+
bestType = infoType;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return bestType;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Extract meaningful keywords from goal text.
|
|
220
|
+
* Strips stop words, punctuation, and deduplicates.
|
|
221
|
+
*/
|
|
222
|
+
extractKeywords(normalizedGoal) {
|
|
223
|
+
const words = normalizedGoal
|
|
224
|
+
.replace(/[^\w\s-]/g, " ") // strip punctuation except hyphens
|
|
225
|
+
.split(/\s+/)
|
|
226
|
+
.filter((w) => w.length > 1) // drop single characters
|
|
227
|
+
.filter((w) => !STOP_WORDS.has(w)); // drop stop words
|
|
228
|
+
// Deduplicate while preserving order
|
|
229
|
+
return [...new Set(words)];
|
|
230
|
+
}
|
|
231
|
+
// --------------------------------------------------------------------------
|
|
232
|
+
// Plan Builders (Private)
|
|
233
|
+
// --------------------------------------------------------------------------
|
|
234
|
+
/**
|
|
235
|
+
* Build a plan from a known site model path.
|
|
236
|
+
* The site model path represents a previously successful execution,
|
|
237
|
+
* so we wrap it as a high-confidence strategy.
|
|
238
|
+
*/
|
|
239
|
+
buildSiteModelPlan(parsedGoal, knownPath, domain) {
|
|
240
|
+
const siteModelActions = knownPath.actionSequence.map((action) => ({
|
|
241
|
+
type: action.type === "wait" ? "wait" : action.type,
|
|
242
|
+
target: action.target,
|
|
243
|
+
value: action.value,
|
|
244
|
+
expectedOutcome: action.expectedOutcome,
|
|
245
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
246
|
+
}));
|
|
247
|
+
const siteModelStrategy = {
|
|
248
|
+
name: "site-model-known-path",
|
|
249
|
+
actions: siteModelActions,
|
|
250
|
+
confidence: knownPath.successRate,
|
|
251
|
+
source: "site-model",
|
|
252
|
+
};
|
|
253
|
+
// Still generate heuristic fallbacks in case the known path has degraded
|
|
254
|
+
const fallbackStrategies = this.generateAllStrategies(parsedGoal, domain);
|
|
255
|
+
const subGoal = {
|
|
256
|
+
description: `Execute known path: ${knownPath.goalDescription}`,
|
|
257
|
+
strategies: [siteModelStrategy, ...fallbackStrategies],
|
|
258
|
+
dependencies: [],
|
|
259
|
+
verificationCriteria: `Goal "${parsedGoal.originalText}" achieved with evidence`,
|
|
260
|
+
completed: false,
|
|
261
|
+
};
|
|
262
|
+
const estimatedSteps = siteModelActions.length;
|
|
263
|
+
return {
|
|
264
|
+
goal: parsedGoal,
|
|
265
|
+
subGoals: [subGoal],
|
|
266
|
+
estimatedSteps,
|
|
267
|
+
confidence: knownPath.successRate * parsedGoal.confidence,
|
|
268
|
+
source: "site-model",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Build a heuristic plan based on goal type.
|
|
273
|
+
* Each goal type gets a structured set of sub-goals with
|
|
274
|
+
* navigation, URL-pattern, and search fallback strategies.
|
|
275
|
+
*/
|
|
276
|
+
buildHeuristicPlan(parsedGoal, domain) {
|
|
277
|
+
let subGoals;
|
|
278
|
+
switch (parsedGoal.type) {
|
|
279
|
+
case "find_information":
|
|
280
|
+
subGoals = this.buildFindInformationSubGoals(parsedGoal, domain);
|
|
281
|
+
break;
|
|
282
|
+
case "complete_action":
|
|
283
|
+
subGoals = this.buildCompleteActionSubGoals(parsedGoal, domain);
|
|
284
|
+
break;
|
|
285
|
+
case "navigate_to":
|
|
286
|
+
subGoals = this.buildNavigateToSubGoals(parsedGoal, domain);
|
|
287
|
+
break;
|
|
288
|
+
case "fill_form":
|
|
289
|
+
subGoals = this.buildFillFormSubGoals(parsedGoal, domain);
|
|
290
|
+
break;
|
|
291
|
+
case "compare":
|
|
292
|
+
subGoals = this.buildCompareSubGoals(parsedGoal, domain);
|
|
293
|
+
break;
|
|
294
|
+
case "explore":
|
|
295
|
+
subGoals = this.buildExploreSubGoals(parsedGoal, domain);
|
|
296
|
+
break;
|
|
297
|
+
case "extract_data":
|
|
298
|
+
subGoals = this.buildExtractDataSubGoals(parsedGoal, domain);
|
|
299
|
+
break;
|
|
300
|
+
default: {
|
|
301
|
+
// Exhaustiveness check -- should never happen
|
|
302
|
+
const _exhaustive = parsedGoal.type;
|
|
303
|
+
subGoals = this.buildFindInformationSubGoals(parsedGoal, domain);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const estimatedSteps = subGoals.reduce((sum, sg) => sum +
|
|
307
|
+
Math.max(...sg.strategies.map((s) => s.actions.length), 1), 0);
|
|
308
|
+
// Overall confidence is the product of sub-goal max strategy confidences
|
|
309
|
+
const confidence = subGoals.reduce((product, sg) => {
|
|
310
|
+
const maxStrategyConfidence = Math.max(...sg.strategies.map((s) => s.confidence), 0.1);
|
|
311
|
+
return product * maxStrategyConfidence;
|
|
312
|
+
}, parsedGoal.confidence);
|
|
313
|
+
return {
|
|
314
|
+
goal: parsedGoal,
|
|
315
|
+
subGoals,
|
|
316
|
+
estimatedSteps,
|
|
317
|
+
confidence: Math.round(confidence * 100) / 100,
|
|
318
|
+
source: "heuristic",
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
// --------------------------------------------------------------------------
|
|
322
|
+
// Sub-Goal Builders by GoalType (Private)
|
|
323
|
+
// --------------------------------------------------------------------------
|
|
324
|
+
buildFindInformationSubGoals(parsedGoal, domain) {
|
|
325
|
+
return [
|
|
326
|
+
{
|
|
327
|
+
description: "Navigate to the page most likely containing the information",
|
|
328
|
+
strategies: this.generateAllStrategies(parsedGoal, domain),
|
|
329
|
+
dependencies: [],
|
|
330
|
+
verificationCriteria: "Landed on a page whose content relates to the goal keywords",
|
|
331
|
+
completed: false,
|
|
332
|
+
},
|
|
333
|
+
{
|
|
334
|
+
description: "Scan page content for relevant information",
|
|
335
|
+
strategies: [
|
|
336
|
+
{
|
|
337
|
+
name: "keyword-scan",
|
|
338
|
+
actions: [
|
|
339
|
+
{
|
|
340
|
+
type: "extract",
|
|
341
|
+
target: "body",
|
|
342
|
+
value: parsedGoal.keywords.join(", "),
|
|
343
|
+
expectedOutcome: "Found text matching one or more goal keywords",
|
|
344
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
345
|
+
},
|
|
346
|
+
],
|
|
347
|
+
confidence: 0.7,
|
|
348
|
+
source: "heuristic",
|
|
349
|
+
},
|
|
350
|
+
{
|
|
351
|
+
name: "scroll-and-scan",
|
|
352
|
+
actions: [
|
|
353
|
+
{
|
|
354
|
+
type: "scroll",
|
|
355
|
+
target: "down",
|
|
356
|
+
expectedOutcome: "More content visible",
|
|
357
|
+
timeout: 5_000,
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
type: "extract",
|
|
361
|
+
target: "body",
|
|
362
|
+
value: parsedGoal.keywords.join(", "),
|
|
363
|
+
expectedOutcome: "Found text matching goal keywords after scrolling",
|
|
364
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
365
|
+
},
|
|
366
|
+
],
|
|
367
|
+
confidence: 0.5,
|
|
368
|
+
source: "heuristic",
|
|
369
|
+
},
|
|
370
|
+
],
|
|
371
|
+
dependencies: [0], // depends on navigation sub-goal
|
|
372
|
+
verificationCriteria: "Extracted text that answers the user's question",
|
|
373
|
+
completed: false,
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
description: "Extract and return the found information",
|
|
377
|
+
strategies: [
|
|
378
|
+
{
|
|
379
|
+
name: "extract-answer",
|
|
380
|
+
actions: [
|
|
381
|
+
{
|
|
382
|
+
type: "extract",
|
|
383
|
+
target: "main, article, .content, #content, body",
|
|
384
|
+
value: "answer",
|
|
385
|
+
expectedOutcome: "Structured extraction of the answer to the goal",
|
|
386
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
387
|
+
},
|
|
388
|
+
],
|
|
389
|
+
confidence: 0.8,
|
|
390
|
+
source: "heuristic",
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
dependencies: [1], // depends on scan sub-goal
|
|
394
|
+
verificationCriteria: "Goal information captured in evidence array",
|
|
395
|
+
completed: false,
|
|
396
|
+
},
|
|
397
|
+
];
|
|
398
|
+
}
|
|
399
|
+
buildCompleteActionSubGoals(parsedGoal, domain) {
|
|
400
|
+
return [
|
|
401
|
+
{
|
|
402
|
+
description: "Navigate to the action page (e.g., signup, checkout, booking)",
|
|
403
|
+
strategies: this.generateAllStrategies(parsedGoal, domain),
|
|
404
|
+
dependencies: [],
|
|
405
|
+
verificationCriteria: "On a page with a form or CTA matching the intended action",
|
|
406
|
+
completed: false,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
description: "Fill in required fields on the action form",
|
|
410
|
+
strategies: [
|
|
411
|
+
{
|
|
412
|
+
name: "form-fill",
|
|
413
|
+
actions: [
|
|
414
|
+
{
|
|
415
|
+
type: "fill",
|
|
416
|
+
target: "form input:not([type=hidden]):not([type=submit])",
|
|
417
|
+
value: "auto-detect",
|
|
418
|
+
expectedOutcome: "Required form fields populated",
|
|
419
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
420
|
+
},
|
|
421
|
+
],
|
|
422
|
+
confidence: 0.6,
|
|
423
|
+
source: "heuristic",
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
dependencies: [0],
|
|
427
|
+
verificationCriteria: "Form fields are filled and valid",
|
|
428
|
+
completed: false,
|
|
429
|
+
},
|
|
430
|
+
{
|
|
431
|
+
description: "Submit the action",
|
|
432
|
+
strategies: [
|
|
433
|
+
{
|
|
434
|
+
name: "click-submit",
|
|
435
|
+
actions: [
|
|
436
|
+
{
|
|
437
|
+
type: "click",
|
|
438
|
+
target: 'button[type="submit"], input[type="submit"], .btn-primary, [data-action="submit"]',
|
|
439
|
+
expectedOutcome: "Form submitted, confirmation or next step shown",
|
|
440
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
confidence: 0.7,
|
|
444
|
+
source: "heuristic",
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
name: "click-cta",
|
|
448
|
+
actions: [
|
|
449
|
+
{
|
|
450
|
+
type: "click",
|
|
451
|
+
target: "a.cta, button.cta, .btn-action, [role=button]",
|
|
452
|
+
expectedOutcome: "Action completed or progressed",
|
|
453
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
confidence: 0.5,
|
|
457
|
+
source: "heuristic",
|
|
458
|
+
},
|
|
459
|
+
],
|
|
460
|
+
dependencies: [1],
|
|
461
|
+
verificationCriteria: "Success message, confirmation page, or next step in flow",
|
|
462
|
+
completed: false,
|
|
463
|
+
},
|
|
464
|
+
];
|
|
465
|
+
}
|
|
466
|
+
buildNavigateToSubGoals(parsedGoal, domain) {
|
|
467
|
+
return [
|
|
468
|
+
{
|
|
469
|
+
description: `Navigate to the target: ${parsedGoal.keywords.join(" ")}`,
|
|
470
|
+
strategies: this.generateAllStrategies(parsedGoal, domain),
|
|
471
|
+
dependencies: [],
|
|
472
|
+
verificationCriteria: "Current URL or page title matches the navigation target",
|
|
473
|
+
completed: false,
|
|
474
|
+
},
|
|
475
|
+
];
|
|
476
|
+
}
|
|
477
|
+
buildFillFormSubGoals(parsedGoal, domain) {
|
|
478
|
+
return [
|
|
479
|
+
{
|
|
480
|
+
description: "Navigate to the page containing the form",
|
|
481
|
+
strategies: this.generateAllStrategies(parsedGoal, domain),
|
|
482
|
+
dependencies: [],
|
|
483
|
+
verificationCriteria: "Page with a visible form loaded",
|
|
484
|
+
completed: false,
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
description: "Identify and fill form fields",
|
|
488
|
+
strategies: [
|
|
489
|
+
{
|
|
490
|
+
name: "sequential-fill",
|
|
491
|
+
actions: [
|
|
492
|
+
{
|
|
493
|
+
type: "fill",
|
|
494
|
+
target: "form",
|
|
495
|
+
value: "auto-detect-from-labels",
|
|
496
|
+
expectedOutcome: "All visible form fields populated",
|
|
497
|
+
timeout: DEFAULT_ACTION_TIMEOUT * 2,
|
|
498
|
+
},
|
|
499
|
+
],
|
|
500
|
+
confidence: 0.6,
|
|
501
|
+
source: "heuristic",
|
|
502
|
+
},
|
|
503
|
+
],
|
|
504
|
+
dependencies: [0],
|
|
505
|
+
verificationCriteria: "Form fields populated, no validation errors",
|
|
506
|
+
completed: false,
|
|
507
|
+
},
|
|
508
|
+
];
|
|
509
|
+
}
|
|
510
|
+
buildCompareSubGoals(parsedGoal, domain) {
|
|
511
|
+
return [
|
|
512
|
+
{
|
|
513
|
+
description: "Navigate to comparison or product listing page",
|
|
514
|
+
strategies: this.generateAllStrategies(parsedGoal, domain),
|
|
515
|
+
dependencies: [],
|
|
516
|
+
verificationCriteria: "On a page with multiple items to compare",
|
|
517
|
+
completed: false,
|
|
518
|
+
},
|
|
519
|
+
{
|
|
520
|
+
description: "Extract comparison data points",
|
|
521
|
+
strategies: [
|
|
522
|
+
{
|
|
523
|
+
name: "table-extract",
|
|
524
|
+
actions: [
|
|
525
|
+
{
|
|
526
|
+
type: "extract",
|
|
527
|
+
target: "table, .comparison, .pricing-table, [class*=compare]",
|
|
528
|
+
value: "comparison-data",
|
|
529
|
+
expectedOutcome: "Structured comparison data extracted",
|
|
530
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
531
|
+
},
|
|
532
|
+
],
|
|
533
|
+
confidence: 0.6,
|
|
534
|
+
source: "heuristic",
|
|
535
|
+
},
|
|
536
|
+
{
|
|
537
|
+
name: "card-extract",
|
|
538
|
+
actions: [
|
|
539
|
+
{
|
|
540
|
+
type: "extract",
|
|
541
|
+
target: ".card, .plan, .product, .feature-list",
|
|
542
|
+
value: "comparison-data",
|
|
543
|
+
expectedOutcome: "Product/plan details extracted from cards",
|
|
544
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
545
|
+
},
|
|
546
|
+
],
|
|
547
|
+
confidence: 0.5,
|
|
548
|
+
source: "heuristic",
|
|
549
|
+
},
|
|
550
|
+
],
|
|
551
|
+
dependencies: [0],
|
|
552
|
+
verificationCriteria: "At least 2 items extracted for comparison",
|
|
553
|
+
completed: false,
|
|
554
|
+
},
|
|
555
|
+
];
|
|
556
|
+
}
|
|
557
|
+
buildExploreSubGoals(parsedGoal, domain) {
|
|
558
|
+
return [
|
|
559
|
+
{
|
|
560
|
+
description: "Start from the site homepage or main listing",
|
|
561
|
+
strategies: [
|
|
562
|
+
{
|
|
563
|
+
name: "go-home",
|
|
564
|
+
actions: [
|
|
565
|
+
{
|
|
566
|
+
type: "navigate",
|
|
567
|
+
target: `https://${domain}`,
|
|
568
|
+
expectedOutcome: "Homepage loaded",
|
|
569
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
570
|
+
},
|
|
571
|
+
],
|
|
572
|
+
confidence: 0.9,
|
|
573
|
+
source: "heuristic",
|
|
574
|
+
},
|
|
575
|
+
],
|
|
576
|
+
dependencies: [],
|
|
577
|
+
verificationCriteria: "Homepage or main page loaded",
|
|
578
|
+
completed: false,
|
|
579
|
+
},
|
|
580
|
+
{
|
|
581
|
+
description: "Explore navigation and key sections",
|
|
582
|
+
strategies: [
|
|
583
|
+
{
|
|
584
|
+
name: "nav-crawl",
|
|
585
|
+
actions: [
|
|
586
|
+
{
|
|
587
|
+
type: "extract",
|
|
588
|
+
target: "nav a, header a, .menu a, [role=navigation] a",
|
|
589
|
+
value: "navigation-links",
|
|
590
|
+
expectedOutcome: "Site navigation structure mapped",
|
|
591
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
592
|
+
},
|
|
593
|
+
{
|
|
594
|
+
type: "click",
|
|
595
|
+
target: "nav a",
|
|
596
|
+
expectedOutcome: "Navigated to a section of interest",
|
|
597
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
598
|
+
},
|
|
599
|
+
],
|
|
600
|
+
confidence: 0.7,
|
|
601
|
+
source: "heuristic",
|
|
602
|
+
},
|
|
603
|
+
],
|
|
604
|
+
dependencies: [0],
|
|
605
|
+
verificationCriteria: "Visited at least 2 distinct pages",
|
|
606
|
+
completed: false,
|
|
607
|
+
},
|
|
608
|
+
];
|
|
609
|
+
}
|
|
610
|
+
buildExtractDataSubGoals(parsedGoal, domain) {
|
|
611
|
+
return [
|
|
612
|
+
{
|
|
613
|
+
description: "Navigate to the data source page",
|
|
614
|
+
strategies: this.generateAllStrategies(parsedGoal, domain),
|
|
615
|
+
dependencies: [],
|
|
616
|
+
verificationCriteria: "On a page containing the target data (table, list, grid)",
|
|
617
|
+
completed: false,
|
|
618
|
+
},
|
|
619
|
+
{
|
|
620
|
+
description: "Extract structured data from the page",
|
|
621
|
+
strategies: [
|
|
622
|
+
{
|
|
623
|
+
name: "table-extract",
|
|
624
|
+
actions: [
|
|
625
|
+
{
|
|
626
|
+
type: "extract",
|
|
627
|
+
target: "table, .data-table, [role=grid]",
|
|
628
|
+
value: "structured-data",
|
|
629
|
+
expectedOutcome: "Tabular data extracted",
|
|
630
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
631
|
+
},
|
|
632
|
+
],
|
|
633
|
+
confidence: 0.7,
|
|
634
|
+
source: "heuristic",
|
|
635
|
+
},
|
|
636
|
+
{
|
|
637
|
+
name: "list-extract",
|
|
638
|
+
actions: [
|
|
639
|
+
{
|
|
640
|
+
type: "extract",
|
|
641
|
+
target: "ul, ol, .list, .results, [role=list]",
|
|
642
|
+
value: "list-data",
|
|
643
|
+
expectedOutcome: "List data extracted",
|
|
644
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
645
|
+
},
|
|
646
|
+
],
|
|
647
|
+
confidence: 0.6,
|
|
648
|
+
source: "heuristic",
|
|
649
|
+
},
|
|
650
|
+
{
|
|
651
|
+
name: "paginated-extract",
|
|
652
|
+
actions: [
|
|
653
|
+
{
|
|
654
|
+
type: "extract",
|
|
655
|
+
target: "table, .results, [role=grid]",
|
|
656
|
+
value: "page-1-data",
|
|
657
|
+
expectedOutcome: "First page of data extracted",
|
|
658
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
type: "click",
|
|
662
|
+
target: '.next, .pagination a:last-child, [aria-label="Next"], a[rel="next"]',
|
|
663
|
+
expectedOutcome: "Navigated to next page of results",
|
|
664
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
type: "extract",
|
|
668
|
+
target: "table, .results, [role=grid]",
|
|
669
|
+
value: "page-2-data",
|
|
670
|
+
expectedOutcome: "Second page of data extracted",
|
|
671
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
confidence: 0.4,
|
|
675
|
+
source: "heuristic",
|
|
676
|
+
},
|
|
677
|
+
],
|
|
678
|
+
dependencies: [0],
|
|
679
|
+
verificationCriteria: "Data extracted with at least 1 row or item",
|
|
680
|
+
completed: false,
|
|
681
|
+
},
|
|
682
|
+
];
|
|
683
|
+
}
|
|
684
|
+
// --------------------------------------------------------------------------
|
|
685
|
+
// Strategy Generators (Private)
|
|
686
|
+
// --------------------------------------------------------------------------
|
|
687
|
+
/**
|
|
688
|
+
* Generate all three heuristic strategies for a goal:
|
|
689
|
+
* navigation-based, URL-pattern-based, and search-based.
|
|
690
|
+
* Returned in confidence-descending order.
|
|
691
|
+
*/
|
|
692
|
+
generateAllStrategies(parsedGoal, domain) {
|
|
693
|
+
return [
|
|
694
|
+
this.generateNavStrategy(parsedGoal),
|
|
695
|
+
this.generateUrlStrategy(parsedGoal, domain),
|
|
696
|
+
this.generateSearchStrategy(parsedGoal),
|
|
697
|
+
].sort((a, b) => b.confidence - a.confidence);
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Generate a navigation-based strategy.
|
|
701
|
+
* Clicks on nav links whose text matches the goal keywords.
|
|
702
|
+
*/
|
|
703
|
+
generateNavStrategy(parsedGoal) {
|
|
704
|
+
const keywords = parsedGoal.keywords;
|
|
705
|
+
// Build CSS selectors that match nav links containing goal keywords.
|
|
706
|
+
// We try exact text matches first, then broader link text searches.
|
|
707
|
+
const navSelectors = keywords.flatMap((keyword) => [
|
|
708
|
+
`nav a:has-text("${keyword}")`,
|
|
709
|
+
`header a:has-text("${keyword}")`,
|
|
710
|
+
`a[href*="${keyword}"]`,
|
|
711
|
+
]);
|
|
712
|
+
const actions = [];
|
|
713
|
+
// First: try clicking a nav link that matches the best keyword
|
|
714
|
+
if (navSelectors.length > 0) {
|
|
715
|
+
actions.push({
|
|
716
|
+
type: "click",
|
|
717
|
+
target: navSelectors[0],
|
|
718
|
+
expectedOutcome: `Navigated to a page related to "${keywords[0]}"`,
|
|
719
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
// Fallback: click any visible link containing the first keyword
|
|
723
|
+
if (keywords.length > 0) {
|
|
724
|
+
actions.push({
|
|
725
|
+
type: "click",
|
|
726
|
+
target: `a:has-text("${keywords[0]}")`,
|
|
727
|
+
expectedOutcome: `Clicked a link containing "${keywords[0]}"`,
|
|
728
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
return {
|
|
732
|
+
name: "navigation-based",
|
|
733
|
+
actions: actions.length > 0
|
|
734
|
+
? actions
|
|
735
|
+
: [
|
|
736
|
+
{
|
|
737
|
+
type: "click",
|
|
738
|
+
target: "nav a",
|
|
739
|
+
expectedOutcome: "Clicked first nav link as blind guess",
|
|
740
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
741
|
+
},
|
|
742
|
+
],
|
|
743
|
+
confidence: keywords.length > 0 ? 0.7 : 0.3,
|
|
744
|
+
source: "heuristic",
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
/**
|
|
748
|
+
* Generate a URL-pattern strategy.
|
|
749
|
+
* Tries direct navigation to common URL patterns matching the goal keywords.
|
|
750
|
+
*/
|
|
751
|
+
generateUrlStrategy(parsedGoal, domain) {
|
|
752
|
+
const keywords = parsedGoal.keywords;
|
|
753
|
+
const actions = [];
|
|
754
|
+
// Look for keywords that match known URL pattern categories
|
|
755
|
+
for (const keyword of keywords) {
|
|
756
|
+
const normalizedKeyword = keyword.toLowerCase();
|
|
757
|
+
// Check exact match against URL_PATTERNS keys
|
|
758
|
+
if (URL_PATTERNS[normalizedKeyword]) {
|
|
759
|
+
for (const path of URL_PATTERNS[normalizedKeyword]) {
|
|
760
|
+
actions.push({
|
|
761
|
+
type: "navigate",
|
|
762
|
+
target: `https://${domain}${path}`,
|
|
763
|
+
expectedOutcome: `Loaded the ${normalizedKeyword} page at ${path}`,
|
|
764
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
765
|
+
});
|
|
766
|
+
}
|
|
767
|
+
break; // Use the first matching pattern category
|
|
768
|
+
}
|
|
769
|
+
// Check partial match: "pricing info" matches "pricing"
|
|
770
|
+
for (const [category, paths] of Object.entries(URL_PATTERNS)) {
|
|
771
|
+
if (normalizedKeyword.includes(category) ||
|
|
772
|
+
category.includes(normalizedKeyword)) {
|
|
773
|
+
for (const path of paths) {
|
|
774
|
+
actions.push({
|
|
775
|
+
type: "navigate",
|
|
776
|
+
target: `https://${domain}${path}`,
|
|
777
|
+
expectedOutcome: `Loaded ${category} page at ${path}`,
|
|
778
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
break;
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
if (actions.length > 0)
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
// Fallback: construct a URL from the first keyword directly
|
|
788
|
+
if (actions.length === 0 && keywords.length > 0) {
|
|
789
|
+
const slug = keywords.slice(0, 2).join("-");
|
|
790
|
+
actions.push({
|
|
791
|
+
type: "navigate",
|
|
792
|
+
target: `https://${domain}/${slug}`,
|
|
793
|
+
expectedOutcome: `Loaded page at /${slug}`,
|
|
794
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
return {
|
|
798
|
+
name: "url-pattern-based",
|
|
799
|
+
actions: actions.length > 0
|
|
800
|
+
? actions
|
|
801
|
+
: [
|
|
802
|
+
{
|
|
803
|
+
type: "navigate",
|
|
804
|
+
target: `https://${domain}`,
|
|
805
|
+
expectedOutcome: "Loaded homepage as fallback",
|
|
806
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
807
|
+
},
|
|
808
|
+
],
|
|
809
|
+
confidence: actions.length > 0 ? 0.6 : 0.2,
|
|
810
|
+
source: "heuristic",
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Generate a search-based strategy.
|
|
815
|
+
* Finds a search input on the page, enters goal keywords, and browses results.
|
|
816
|
+
*/
|
|
817
|
+
generateSearchStrategy(parsedGoal) {
|
|
818
|
+
const searchQuery = parsedGoal.keywords.join(" ");
|
|
819
|
+
const actions = [
|
|
820
|
+
{
|
|
821
|
+
type: "click",
|
|
822
|
+
target: [
|
|
823
|
+
'input[type="search"]',
|
|
824
|
+
'input[name="q"]',
|
|
825
|
+
'input[name="query"]',
|
|
826
|
+
'input[name="search"]',
|
|
827
|
+
'input[placeholder*="earch"]',
|
|
828
|
+
'[role="search"] input',
|
|
829
|
+
"#search",
|
|
830
|
+
".search-input",
|
|
831
|
+
].join(", "),
|
|
832
|
+
expectedOutcome: "Search input focused",
|
|
833
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
type: "fill",
|
|
837
|
+
target: [
|
|
838
|
+
'input[type="search"]',
|
|
839
|
+
'input[name="q"]',
|
|
840
|
+
'input[name="query"]',
|
|
841
|
+
'input[name="search"]',
|
|
842
|
+
'input[placeholder*="earch"]',
|
|
843
|
+
'[role="search"] input',
|
|
844
|
+
"#search",
|
|
845
|
+
".search-input",
|
|
846
|
+
].join(", "),
|
|
847
|
+
value: searchQuery,
|
|
848
|
+
expectedOutcome: `Search field filled with "${searchQuery}"`,
|
|
849
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
type: "click",
|
|
853
|
+
target: [
|
|
854
|
+
'[role="search"] button',
|
|
855
|
+
'button[type="submit"]',
|
|
856
|
+
".search-button",
|
|
857
|
+
".search-submit",
|
|
858
|
+
'button[aria-label*="earch"]',
|
|
859
|
+
].join(", "),
|
|
860
|
+
expectedOutcome: "Search submitted",
|
|
861
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
862
|
+
},
|
|
863
|
+
{
|
|
864
|
+
type: "click",
|
|
865
|
+
target: ".search-results a, .results a, main a",
|
|
866
|
+
expectedOutcome: "Clicked the first search result",
|
|
867
|
+
timeout: DEFAULT_ACTION_TIMEOUT,
|
|
868
|
+
},
|
|
869
|
+
];
|
|
870
|
+
return {
|
|
871
|
+
name: "search-based",
|
|
872
|
+
actions,
|
|
873
|
+
// Search is a reliable fallback on sites that have it, but we can't
|
|
874
|
+
// know in advance whether the site has search, so moderate confidence.
|
|
875
|
+
confidence: 0.5,
|
|
876
|
+
source: "heuristic",
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
// --------------------------------------------------------------------------
|
|
880
|
+
// Helpers (Private)
|
|
881
|
+
// --------------------------------------------------------------------------
|
|
882
|
+
/**
|
|
883
|
+
* Extract the set of actions from the first successful strategy path
|
|
884
|
+
* across completed sub-goals. Used to record winning paths in the site model.
|
|
885
|
+
*/
|
|
886
|
+
extractSuccessfulActions(result) {
|
|
887
|
+
const actions = [];
|
|
888
|
+
for (const subGoal of result.plan.subGoals) {
|
|
889
|
+
if (!subGoal.completed)
|
|
890
|
+
continue;
|
|
891
|
+
// Take the first strategy's actions as the "winning" path.
|
|
892
|
+
// In a real execution engine, we would track which strategy
|
|
893
|
+
// actually succeeded; for now, we assume ordered execution.
|
|
894
|
+
const bestStrategy = subGoal.strategies[0];
|
|
895
|
+
if (bestStrategy) {
|
|
896
|
+
actions.push(...bestStrategy.actions);
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
return actions;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
//# sourceMappingURL=goal-decomposer.js.map
|