explorbot 0.0.1
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/LICENSE +94 -0
- package/README.md +267 -0
- package/assets/sample-files/sample.docx +0 -0
- package/assets/sample-files/sample.mp3 +0 -0
- package/assets/sample-files/sample.mp4 +0 -0
- package/assets/sample-files/sample.pdf +21 -0
- package/assets/sample-files/sample.png +0 -0
- package/assets/sample-files/sample.xlsx +0 -0
- package/assets/sample-files/sample.zip +0 -0
- package/dist/assets/sample-files/sample.docx +0 -0
- package/dist/assets/sample-files/sample.mp3 +0 -0
- package/dist/assets/sample-files/sample.mp4 +0 -0
- package/dist/assets/sample-files/sample.pdf +21 -0
- package/dist/assets/sample-files/sample.png +0 -0
- package/dist/assets/sample-files/sample.xlsx +0 -0
- package/dist/assets/sample-files/sample.zip +0 -0
- package/dist/bin/explorbot-cli.js +683 -0
- package/dist/bin/explorbot-cli.js.map +1 -0
- package/dist/boat/api-tester/bin/apibot-cli.js +5 -0
- package/dist/boat/api-tester/bin/apibot-cli.js.map +1 -0
- package/dist/boat/api-tester/example/apibot.config.js +31 -0
- package/dist/boat/api-tester/example/apibot.config.js.map +1 -0
- package/dist/boat/api-tester/src/ai/chief/styles.js +13 -0
- package/dist/boat/api-tester/src/ai/chief/styles.js.map +1 -0
- package/dist/boat/api-tester/src/ai/chief.js +301 -0
- package/dist/boat/api-tester/src/ai/chief.js.map +1 -0
- package/dist/boat/api-tester/src/ai/curler-tools.js +263 -0
- package/dist/boat/api-tester/src/ai/curler-tools.js.map +1 -0
- package/dist/boat/api-tester/src/ai/curler.js +271 -0
- package/dist/boat/api-tester/src/ai/curler.js.map +1 -0
- package/dist/boat/api-tester/src/api-client.js +26 -0
- package/dist/boat/api-tester/src/api-client.js.map +1 -0
- package/dist/boat/api-tester/src/apibot.js +166 -0
- package/dist/boat/api-tester/src/apibot.js.map +1 -0
- package/dist/boat/api-tester/src/cli.js +262 -0
- package/dist/boat/api-tester/src/cli.js.map +1 -0
- package/dist/boat/api-tester/src/config.js +159 -0
- package/dist/boat/api-tester/src/config.js.map +1 -0
- package/dist/prompts/audit-rules.md +124 -0
- package/dist/rules/chief/general.md +11 -0
- package/dist/rules/chief/styles/curious.md +12 -0
- package/dist/rules/chief/styles/hacker.md +19 -0
- package/dist/rules/chief/styles/normal.md +11 -0
- package/dist/rules/chief/styles/psycho.md +17 -0
- package/dist/rules/navigator/multiple-locator.md +47 -0
- package/dist/rules/navigator/output.md +69 -0
- package/dist/rules/navigator/verification-actions.md +122 -0
- package/dist/rules/navigator/verification-output.md +53 -0
- package/dist/rules/planner/styles/curious.md +39 -0
- package/dist/rules/planner/styles/normal.md +21 -0
- package/dist/rules/planner/styles/psycho.md +14 -0
- package/dist/rules/researcher/list-element.md +11 -0
- package/dist/rules/researcher/screenshot-ui-map.md +30 -0
- package/dist/rules/researcher/section-ui-map.md +18 -0
- package/dist/rules/researcher/ui-map-table.md +18 -0
- package/dist/src/action-result.js +574 -0
- package/dist/src/action-result.js.map +1 -0
- package/dist/src/action.js +388 -0
- package/dist/src/action.js.map +1 -0
- package/dist/src/activity.js +86 -0
- package/dist/src/activity.js.map +1 -0
- package/dist/src/ai/agent.js +2 -0
- package/dist/src/ai/agent.js.map +1 -0
- package/dist/src/ai/bosun.js +443 -0
- package/dist/src/ai/bosun.js.map +1 -0
- package/dist/src/ai/captain/idle-mode.js +102 -0
- package/dist/src/ai/captain/idle-mode.js.map +1 -0
- package/dist/src/ai/captain/mixin.js +11 -0
- package/dist/src/ai/captain/mixin.js.map +1 -0
- package/dist/src/ai/captain/test-mode.js +251 -0
- package/dist/src/ai/captain/test-mode.js.map +1 -0
- package/dist/src/ai/captain/web-mode.js +124 -0
- package/dist/src/ai/captain/web-mode.js.map +1 -0
- package/dist/src/ai/captain.js +442 -0
- package/dist/src/ai/captain.js.map +1 -0
- package/dist/src/ai/conversation.js +176 -0
- package/dist/src/ai/conversation.js.map +1 -0
- package/dist/src/ai/experience-compactor.js +232 -0
- package/dist/src/ai/experience-compactor.js.map +1 -0
- package/dist/src/ai/fisherman-tools.js +154 -0
- package/dist/src/ai/fisherman-tools.js.map +1 -0
- package/dist/src/ai/fisherman.js +184 -0
- package/dist/src/ai/fisherman.js.map +1 -0
- package/dist/src/ai/historian.js +384 -0
- package/dist/src/ai/historian.js.map +1 -0
- package/dist/src/ai/navigator.js +493 -0
- package/dist/src/ai/navigator.js.map +1 -0
- package/dist/src/ai/pilot.js +684 -0
- package/dist/src/ai/pilot.js.map +1 -0
- package/dist/src/ai/planner/session-dedup.js +28 -0
- package/dist/src/ai/planner/session-dedup.js.map +1 -0
- package/dist/src/ai/planner/styles.js +15 -0
- package/dist/src/ai/planner/styles.js.map +1 -0
- package/dist/src/ai/planner/subpages.js +118 -0
- package/dist/src/ai/planner/subpages.js.map +1 -0
- package/dist/src/ai/planner.js +486 -0
- package/dist/src/ai/planner.js.map +1 -0
- package/dist/src/ai/provider.js +540 -0
- package/dist/src/ai/provider.js.map +1 -0
- package/dist/src/ai/quartermaster.js +210 -0
- package/dist/src/ai/quartermaster.js.map +1 -0
- package/dist/src/ai/researcher/cache.js +95 -0
- package/dist/src/ai/researcher/cache.js.map +1 -0
- package/dist/src/ai/researcher/coordinates.js +210 -0
- package/dist/src/ai/researcher/coordinates.js.map +1 -0
- package/dist/src/ai/researcher/deep-analysis.js +364 -0
- package/dist/src/ai/researcher/deep-analysis.js.map +1 -0
- package/dist/src/ai/researcher/fingerprint-worker.js +46 -0
- package/dist/src/ai/researcher/fingerprint-worker.js.map +1 -0
- package/dist/src/ai/researcher/focus.js +37 -0
- package/dist/src/ai/researcher/focus.js.map +1 -0
- package/dist/src/ai/researcher/locators.js +242 -0
- package/dist/src/ai/researcher/locators.js.map +1 -0
- package/dist/src/ai/researcher/mixin.js +3 -0
- package/dist/src/ai/researcher/mixin.js.map +1 -0
- package/dist/src/ai/researcher/parser.js +160 -0
- package/dist/src/ai/researcher/parser.js.map +1 -0
- package/dist/src/ai/researcher/research-result.js +110 -0
- package/dist/src/ai/researcher/research-result.js.map +1 -0
- package/dist/src/ai/researcher.js +776 -0
- package/dist/src/ai/researcher.js.map +1 -0
- package/dist/src/ai/rules.js +368 -0
- package/dist/src/ai/rules.js.map +1 -0
- package/dist/src/ai/task-agent.js +110 -0
- package/dist/src/ai/task-agent.js.map +1 -0
- package/dist/src/ai/tester.js +840 -0
- package/dist/src/ai/tester.js.map +1 -0
- package/dist/src/ai/tools.js +980 -0
- package/dist/src/ai/tools.js.map +1 -0
- package/dist/src/api/api-client.js +91 -0
- package/dist/src/api/api-client.js.map +1 -0
- package/dist/src/api/request-result.js +177 -0
- package/dist/src/api/request-result.js.map +1 -0
- package/dist/src/api/request-store.js +109 -0
- package/dist/src/api/request-store.js.map +1 -0
- package/dist/src/api/spec-reader.js +148 -0
- package/dist/src/api/spec-reader.js.map +1 -0
- package/dist/src/api/xhr-capture.js +91 -0
- package/dist/src/api/xhr-capture.js.map +1 -0
- package/dist/src/browser-server.js +67 -0
- package/dist/src/browser-server.js.map +1 -0
- package/dist/src/command-handler.js +363 -0
- package/dist/src/command-handler.js.map +1 -0
- package/dist/src/commands/add-rule-command.js +52 -0
- package/dist/src/commands/add-rule-command.js.map +1 -0
- package/dist/src/commands/base-command.js +14 -0
- package/dist/src/commands/base-command.js.map +1 -0
- package/dist/src/commands/clean-command.js +67 -0
- package/dist/src/commands/clean-command.js.map +1 -0
- package/dist/src/commands/context-aria-command.js +18 -0
- package/dist/src/commands/context-aria-command.js.map +1 -0
- package/dist/src/commands/context-command.js +57 -0
- package/dist/src/commands/context-command.js.map +1 -0
- package/dist/src/commands/context-data-command.js +25 -0
- package/dist/src/commands/context-data-command.js.map +1 -0
- package/dist/src/commands/context-experience-command.js +41 -0
- package/dist/src/commands/context-experience-command.js.map +1 -0
- package/dist/src/commands/context-html-command.js +26 -0
- package/dist/src/commands/context-html-command.js.map +1 -0
- package/dist/src/commands/context-knowledge-command.js +36 -0
- package/dist/src/commands/context-knowledge-command.js.map +1 -0
- package/dist/src/commands/debug-command.js +12 -0
- package/dist/src/commands/debug-command.js.map +1 -0
- package/dist/src/commands/drill-command.js +29 -0
- package/dist/src/commands/drill-command.js.map +1 -0
- package/dist/src/commands/exit-command.js +26 -0
- package/dist/src/commands/exit-command.js.map +1 -0
- package/dist/src/commands/explore-command.js +124 -0
- package/dist/src/commands/explore-command.js.map +1 -0
- package/dist/src/commands/freesail-command.js +84 -0
- package/dist/src/commands/freesail-command.js.map +1 -0
- package/dist/src/commands/help-command.js +7 -0
- package/dist/src/commands/help-command.js.map +1 -0
- package/dist/src/commands/index.js +63 -0
- package/dist/src/commands/index.js.map +1 -0
- package/dist/src/commands/knows-command.js +54 -0
- package/dist/src/commands/knows-command.js.map +1 -0
- package/dist/src/commands/learn-command.js +35 -0
- package/dist/src/commands/learn-command.js.map +1 -0
- package/dist/src/commands/navigate-command.js +16 -0
- package/dist/src/commands/navigate-command.js.map +1 -0
- package/dist/src/commands/path-command.js +70 -0
- package/dist/src/commands/path-command.js.map +1 -0
- package/dist/src/commands/plan-clear-command.js +13 -0
- package/dist/src/commands/plan-clear-command.js.map +1 -0
- package/dist/src/commands/plan-command.js +36 -0
- package/dist/src/commands/plan-command.js.map +1 -0
- package/dist/src/commands/plan-edit-command.js +8 -0
- package/dist/src/commands/plan-edit-command.js.map +1 -0
- package/dist/src/commands/plan-load-command.js +16 -0
- package/dist/src/commands/plan-load-command.js.map +1 -0
- package/dist/src/commands/plan-reload-command.js +23 -0
- package/dist/src/commands/plan-reload-command.js.map +1 -0
- package/dist/src/commands/plan-save-command.js +22 -0
- package/dist/src/commands/plan-save-command.js.map +1 -0
- package/dist/src/commands/research-command.js +38 -0
- package/dist/src/commands/research-command.js.map +1 -0
- package/dist/src/commands/start-command.js +12 -0
- package/dist/src/commands/start-command.js.map +1 -0
- package/dist/src/commands/status-command.js +19 -0
- package/dist/src/commands/status-command.js.map +1 -0
- package/dist/src/commands/test-command.js +85 -0
- package/dist/src/commands/test-command.js.map +1 -0
- package/dist/src/components/ActivityPane.js +55 -0
- package/dist/src/components/ActivityPane.js.map +1 -0
- package/dist/src/components/AddKnowledge.js +122 -0
- package/dist/src/components/AddKnowledge.js.map +1 -0
- package/dist/src/components/AddRule.js +117 -0
- package/dist/src/components/AddRule.js.map +1 -0
- package/dist/src/components/App.js +313 -0
- package/dist/src/components/App.js.map +1 -0
- package/dist/src/components/Autocomplete.js +43 -0
- package/dist/src/components/Autocomplete.js.map +1 -0
- package/dist/src/components/InputPane.js +207 -0
- package/dist/src/components/InputPane.js.map +1 -0
- package/dist/src/components/InputReadline.js +598 -0
- package/dist/src/components/InputReadline.js.map +1 -0
- package/dist/src/components/LogPane.js +123 -0
- package/dist/src/components/LogPane.js.map +1 -0
- package/dist/src/components/PlanEditor.js +126 -0
- package/dist/src/components/PlanEditor.js.map +1 -0
- package/dist/src/components/PlanPane.js +51 -0
- package/dist/src/components/PlanPane.js.map +1 -0
- package/dist/src/components/SessionTimer.js +26 -0
- package/dist/src/components/SessionTimer.js.map +1 -0
- package/dist/src/components/StateTransitionPane.js +107 -0
- package/dist/src/components/StateTransitionPane.js.map +1 -0
- package/dist/src/components/StatusPane.js +37 -0
- package/dist/src/components/StatusPane.js.map +1 -0
- package/dist/src/components/TaskPane.js +96 -0
- package/dist/src/components/TaskPane.js.map +1 -0
- package/dist/src/components/Welcome.js +52 -0
- package/dist/src/components/Welcome.js.map +1 -0
- package/dist/src/components/WelcomeChecklist.js +96 -0
- package/dist/src/components/WelcomeChecklist.js.map +1 -0
- package/dist/src/components/WelcomeCommands.js +61 -0
- package/dist/src/components/WelcomeCommands.js.map +1 -0
- package/dist/src/components/autocomplete-store.js +22 -0
- package/dist/src/components/autocomplete-store.js.map +1 -0
- package/dist/src/components/parse-keypress.js +174 -0
- package/dist/src/components/parse-keypress.js.map +1 -0
- package/dist/src/config.js +249 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/execution-controller.js +92 -0
- package/dist/src/execution-controller.js.map +1 -0
- package/dist/src/experience-tracker.js +294 -0
- package/dist/src/experience-tracker.js.map +1 -0
- package/dist/src/explorbot.js +348 -0
- package/dist/src/explorbot.js.map +1 -0
- package/dist/src/explorer.js +611 -0
- package/dist/src/explorer.js.map +1 -0
- package/dist/src/index.js +56 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/knowledge-tracker.js +184 -0
- package/dist/src/knowledge-tracker.js.map +1 -0
- package/dist/src/observability.js +126 -0
- package/dist/src/observability.js.map +1 -0
- package/dist/src/reporter.js +185 -0
- package/dist/src/reporter.js.map +1 -0
- package/dist/src/state-manager.js +427 -0
- package/dist/src/state-manager.js.map +1 -0
- package/dist/src/stats.js +44 -0
- package/dist/src/stats.js.map +1 -0
- package/dist/src/test-plan.js +343 -0
- package/dist/src/test-plan.js.map +1 -0
- package/dist/src/utils/aria.js +588 -0
- package/dist/src/utils/aria.js.map +1 -0
- package/dist/src/utils/code-extractor.js +21 -0
- package/dist/src/utils/code-extractor.js.map +1 -0
- package/dist/src/utils/context-formatter.js +205 -0
- package/dist/src/utils/context-formatter.js.map +1 -0
- package/dist/src/utils/error-page.js +19 -0
- package/dist/src/utils/error-page.js.map +1 -0
- package/dist/src/utils/expandable.js +35 -0
- package/dist/src/utils/expandable.js.map +1 -0
- package/dist/src/utils/hooks-runner.js +77 -0
- package/dist/src/utils/hooks-runner.js.map +1 -0
- package/dist/src/utils/html-diff.js +734 -0
- package/dist/src/utils/html-diff.js.map +1 -0
- package/dist/src/utils/html.js +1163 -0
- package/dist/src/utils/html.js.map +1 -0
- package/dist/src/utils/logger.js +465 -0
- package/dist/src/utils/logger.js.map +1 -0
- package/dist/src/utils/loop.js +126 -0
- package/dist/src/utils/loop.js.map +1 -0
- package/dist/src/utils/markdown-parser.js +117 -0
- package/dist/src/utils/markdown-parser.js.map +1 -0
- package/dist/src/utils/markdown-query.js +393 -0
- package/dist/src/utils/markdown-query.js.map +1 -0
- package/dist/src/utils/markdown-terminal.js +40 -0
- package/dist/src/utils/markdown-terminal.js.map +1 -0
- package/dist/src/utils/research-parser.js +2 -0
- package/dist/src/utils/research-parser.js.map +1 -0
- package/dist/src/utils/retry.js +55 -0
- package/dist/src/utils/retry.js.map +1 -0
- package/dist/src/utils/rules-loader.js +104 -0
- package/dist/src/utils/rules-loader.js.map +1 -0
- package/dist/src/utils/strings.js +14 -0
- package/dist/src/utils/strings.js.map +1 -0
- package/dist/src/utils/test-plan-markdown.js +301 -0
- package/dist/src/utils/test-plan-markdown.js.map +1 -0
- package/dist/src/utils/throttle.js +16 -0
- package/dist/src/utils/throttle.js.map +1 -0
- package/dist/src/utils/unique-names.js +13 -0
- package/dist/src/utils/unique-names.js.map +1 -0
- package/dist/src/utils/url-matcher.js +48 -0
- package/dist/src/utils/url-matcher.js.map +1 -0
- package/dist/src/utils/web-element.js +131 -0
- package/dist/src/utils/web-element.js.map +1 -0
- package/dist/src/utils/xpath.js +110 -0
- package/dist/src/utils/xpath.js.map +1 -0
- package/package.json +119 -0
- package/prompts/audit-rules.md +124 -0
- package/rules/chief/general.md +11 -0
- package/rules/chief/styles/curious.md +12 -0
- package/rules/chief/styles/hacker.md +19 -0
- package/rules/chief/styles/normal.md +11 -0
- package/rules/chief/styles/psycho.md +17 -0
- package/rules/navigator/multiple-locator.md +47 -0
- package/rules/navigator/output.md +69 -0
- package/rules/navigator/verification-actions.md +122 -0
- package/rules/navigator/verification-output.md +53 -0
- package/rules/planner/styles/curious.md +39 -0
- package/rules/planner/styles/normal.md +21 -0
- package/rules/planner/styles/psycho.md +14 -0
- package/rules/researcher/list-element.md +11 -0
- package/rules/researcher/screenshot-ui-map.md +30 -0
- package/rules/researcher/section-ui-map.md +18 -0
- package/rules/researcher/ui-map-table.md +18 -0
|
@@ -0,0 +1,980 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import dedent from 'dedent';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { ActionResult } from "../action-result.js";
|
|
5
|
+
import { TestResult } from '../test-plan.js';
|
|
6
|
+
import { extractFocusedElement } from "../utils/aria.js";
|
|
7
|
+
import { createDebug, tag } from '../utils/logger.js';
|
|
8
|
+
import { pause } from '../utils/loop.js';
|
|
9
|
+
import { WebElement } from "../utils/web-element.js";
|
|
10
|
+
import { sectionContextRule } from "./rules.js";
|
|
11
|
+
import { isInteractive } from "./task-agent.js";
|
|
12
|
+
const debugLog = createDebug('explorbot:tools');
|
|
13
|
+
export const CODECEPT_TOOLS = ['click', 'pressKey', 'form'];
|
|
14
|
+
export const ASSERTION_TOOLS = ['verify'];
|
|
15
|
+
export function createCodeceptJSTools(explorer, task) {
|
|
16
|
+
const stateManager = explorer.getStateManager();
|
|
17
|
+
return {
|
|
18
|
+
click: tool({
|
|
19
|
+
description: dedent `
|
|
20
|
+
Click an element by trying multiple CodeceptJS commands in order until one succeeds.
|
|
21
|
+
|
|
22
|
+
Follow <locator_priority> from system prompt for locator selection.
|
|
23
|
+
|
|
24
|
+
I.click(locator) - click element matching locator
|
|
25
|
+
I.click(locator, container) - click element inside a parent element (CSS selector)
|
|
26
|
+
Container narrows search area. Use when page has multiple matching elements.
|
|
27
|
+
Example: Page has 3 "Delete" buttons in different rows:
|
|
28
|
+
I.click("Delete", ".row-1") - clicks Delete inside element with class row-1
|
|
29
|
+
|
|
30
|
+
IMPORTANT: This tool ONLY accepts click commands. For typing text, use form() tool.
|
|
31
|
+
CRITICAL: All commands MUST target the SAME element using different locators.
|
|
32
|
+
This is a FALLBACK list, NOT a sequence of different clicks.
|
|
33
|
+
If you need to click multiple different elements, make SEPARATE click() calls.
|
|
34
|
+
`,
|
|
35
|
+
inputSchema: z.object({
|
|
36
|
+
commands: z.array(z.string()).describe(dedent `
|
|
37
|
+
FALLBACK LOCATORS for ONE element. All commands must click the SAME element.
|
|
38
|
+
Never mix different elements — use separate click() calls instead.
|
|
39
|
+
Order by reliability:
|
|
40
|
+
1. I.click(text, container) - PREFERRED when container is known - e.g. I.click("Save", ".modal")
|
|
41
|
+
2. I.click(ARIA, container) - e.g. I.click({"role":"button","text":"Save"}, ".modal")
|
|
42
|
+
3. I.click(CSS, container) - e.g. I.click("#btn", ".modal")
|
|
43
|
+
4. I.click(CSS) or I.click(XPath) - when locator already includes context (ID, XPath)
|
|
44
|
+
5. I.clickXY(x, y) - coordinates fallback
|
|
45
|
+
IMPORTANT: Always include at least one command WITHOUT a container as fallback,
|
|
46
|
+
in case the element moved to a different section (e.g. I.click("Save") without container).
|
|
47
|
+
`),
|
|
48
|
+
explanation: z.string().describe('Why you are clicking this element'),
|
|
49
|
+
}),
|
|
50
|
+
execute: async ({ commands: rawCommands, explanation }) => {
|
|
51
|
+
const activeNote = task.startNote(explanation);
|
|
52
|
+
if (rawCommands.length === 0) {
|
|
53
|
+
activeNote.commit(TestResult.FAILED);
|
|
54
|
+
return failedToolResult('click', 'No commands provided');
|
|
55
|
+
}
|
|
56
|
+
const invalidCommands = rawCommands.map((cmd) => cmd.trim()).filter((cmd) => cmd.startsWith('I.') && !cmd.startsWith('I.click'));
|
|
57
|
+
if (invalidCommands.length > 0) {
|
|
58
|
+
activeNote.commit(TestResult.FAILED);
|
|
59
|
+
return failedToolResult('click', `Invalid commands: ${invalidCommands.join(', ')}. Click tool only accepts I.click() or I.clickXY() commands.`, {
|
|
60
|
+
suggestion: 'Use form() tool for typing text or multiple actions, or exitIframe() to leave iframe context.',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
const commands = rawCommands.map((cmd) => {
|
|
64
|
+
const trimmed = cmd.trim();
|
|
65
|
+
if (trimmed.startsWith('I.click'))
|
|
66
|
+
return trimmed;
|
|
67
|
+
return `I.click(${JSON.stringify(trimmed)})`;
|
|
68
|
+
});
|
|
69
|
+
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
70
|
+
const action = explorer.createAction();
|
|
71
|
+
const attempts = [];
|
|
72
|
+
for (let i = 0; i < commands.length; i++) {
|
|
73
|
+
const command = transformContainsCommand(commands[i]);
|
|
74
|
+
const isLast = i === commands.length - 1;
|
|
75
|
+
const success = await action.attempt(command, explanation, isLast);
|
|
76
|
+
attempts.push({
|
|
77
|
+
command,
|
|
78
|
+
success,
|
|
79
|
+
...(action.lastError && { error: action.lastError.toString() }),
|
|
80
|
+
});
|
|
81
|
+
if (success) {
|
|
82
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, command);
|
|
83
|
+
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
84
|
+
activeNote.screenshot = await action.saveScreenshot();
|
|
85
|
+
}
|
|
86
|
+
activeNote.commit(TestResult.PASSED);
|
|
87
|
+
return successToolResult('click', { ...toolResult, attempts, code: command });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
let disambiguated = null;
|
|
91
|
+
if (attempts.some((a) => a.error?.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN))) {
|
|
92
|
+
disambiguated = await disambiguateElements(action.lastError, explanation, explorer.getAIProvider());
|
|
93
|
+
}
|
|
94
|
+
if (disambiguated) {
|
|
95
|
+
debugLog('Disambiguation picked element %d', disambiguated.position);
|
|
96
|
+
const failedCommand = attempts.find((a) => a.error?.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN))?.command;
|
|
97
|
+
const retryCommands = [];
|
|
98
|
+
if (failedCommand) {
|
|
99
|
+
retryCommands.push(failedCommand.replace(/\)$/, `, step.opts({ elementIndex: ${disambiguated.position} }))`));
|
|
100
|
+
}
|
|
101
|
+
retryCommands.push(`I.click('${disambiguated.xpath.replace(/'/g, "\\'")}')`);
|
|
102
|
+
for (const retryCmd of retryCommands) {
|
|
103
|
+
if (!(await action.attempt(retryCmd, explanation, true))) {
|
|
104
|
+
attempts.push({ command: retryCmd, success: false, error: action.lastError?.toString() });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, retryCmd);
|
|
108
|
+
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
109
|
+
activeNote.screenshot = await action.saveScreenshot();
|
|
110
|
+
}
|
|
111
|
+
activeNote.commit(TestResult.PASSED);
|
|
112
|
+
return successToolResult('click', { ...toolResult, attempts, code: retryCmd, disambiguated: true });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, commands[0]);
|
|
116
|
+
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
117
|
+
activeNote.screenshot = await action.saveScreenshot();
|
|
118
|
+
}
|
|
119
|
+
activeNote.commit(TestResult.FAILED);
|
|
120
|
+
let suggestion = "Try xpathCheck() to find the element's actual position, see() for visual analysis, or visualClick() to click by visual appearance.";
|
|
121
|
+
const lastError = attempts[attempts.length - 1]?.error || '';
|
|
122
|
+
if (lastError.includes('was not found') || lastError.includes('not found by text')) {
|
|
123
|
+
suggestion = 'Element was not found in the DOM. Use xpathCheck() to locate it, context() to refresh snapshot, or visualClick() to click by visual appearance.';
|
|
124
|
+
}
|
|
125
|
+
else if (lastError.includes('Timeout') || lastError.includes('intercept')) {
|
|
126
|
+
suggestion = 'Element exists but could not be clicked (possibly covered by overlay or not interactable). Try closing overlapping panels first, or use visualClick().';
|
|
127
|
+
}
|
|
128
|
+
return failedToolResult('click', 'All click commands failed', {
|
|
129
|
+
...toolResult,
|
|
130
|
+
attempts,
|
|
131
|
+
suggestion,
|
|
132
|
+
}, action.lastError);
|
|
133
|
+
},
|
|
134
|
+
}),
|
|
135
|
+
pressKey: tool({
|
|
136
|
+
description: dedent `
|
|
137
|
+
Press a keyboard key or key combination. Use this for special keys like Enter, Escape, Tab, Arrow keys, or key combinations with modifiers.
|
|
138
|
+
|
|
139
|
+
IMPORTANT: This tool is ONLY for single key presses or key combinations with modifiers.
|
|
140
|
+
For typing text (multiple characters), use form() tool instead.
|
|
141
|
+
|
|
142
|
+
Standard keys: Enter, Escape, Esc, Tab, Backspace, Delete, Del, ArrowUp, ArrowDown, ArrowLeft, ArrowRight, Home, End, PageUp, PageDown, Space, F1-F12, or any single character.
|
|
143
|
+
Modifiers: Control, Shift, Alt, Meta, CommandOrControl
|
|
144
|
+
|
|
145
|
+
Examples:
|
|
146
|
+
- pressKey({ key: 'Enter' }) - press Enter key
|
|
147
|
+
- pressKey({ key: 'a', modifier: 'Control' }) - press Ctrl+A
|
|
148
|
+
- pressKey({ key: 'Delete', modifier: 'Shift' }) - press Shift+Delete
|
|
149
|
+
- pressKey({ key: 'a', modifier: ['Control', 'Shift'] }) - press Ctrl+Shift+A
|
|
150
|
+
|
|
151
|
+
If you need to type multiple characters or words, use form() tool instead.
|
|
152
|
+
`,
|
|
153
|
+
inputSchema: z.object({
|
|
154
|
+
key: z.string().describe('The key to press. Can be a single character or standard key name (Enter, Escape, Tab, Delete, ArrowUp, etc.)'),
|
|
155
|
+
modifier: z
|
|
156
|
+
.union([z.string(), z.array(z.string())])
|
|
157
|
+
.optional()
|
|
158
|
+
.describe('Optional modifier key(s): Control, Shift, Alt, Meta, or CommandOrControl. Can be a single modifier or array for multiple modifiers.'),
|
|
159
|
+
explanation: z.string().describe('Reason for pressing this key.'),
|
|
160
|
+
}),
|
|
161
|
+
execute: async ({ key, modifier, explanation }) => {
|
|
162
|
+
const activeNote = task.startNote(explanation);
|
|
163
|
+
try {
|
|
164
|
+
const standardKeys = new Set(['Enter', 'Escape', 'Esc', 'Tab', 'Backspace', 'Delete', 'Del', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Home', 'End', 'PageUp', 'PageDown', 'Space', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12']);
|
|
165
|
+
const isSingleChar = key.length === 1;
|
|
166
|
+
const normalizedKey = key.toLowerCase();
|
|
167
|
+
const matchingStandardKey = Array.from(standardKeys).find((sk) => sk.toLowerCase() === normalizedKey);
|
|
168
|
+
const isStandardKey = !!matchingStandardKey;
|
|
169
|
+
const keyToUse = matchingStandardKey || key;
|
|
170
|
+
if (!isSingleChar && !isStandardKey) {
|
|
171
|
+
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
172
|
+
const action = explorer.createAction();
|
|
173
|
+
const typeCommand = `I.type(${JSON.stringify(key)})`;
|
|
174
|
+
await action.attempt(typeCommand, explanation);
|
|
175
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, key);
|
|
176
|
+
if (!action.lastError) {
|
|
177
|
+
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
178
|
+
activeNote.screenshot = await action.saveScreenshot();
|
|
179
|
+
}
|
|
180
|
+
activeNote.commit(TestResult.PASSED);
|
|
181
|
+
return successToolResult('pressKey', {
|
|
182
|
+
...toolResult,
|
|
183
|
+
message: `Automatically used type() for "${key}" (not a standard key press)`,
|
|
184
|
+
code: typeCommand,
|
|
185
|
+
fallback: true,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
const errorMsg = `pressKey fallback to type() failed: ${action.lastError?.toString()}`;
|
|
189
|
+
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
190
|
+
activeNote.screenshot = await action.saveScreenshot();
|
|
191
|
+
}
|
|
192
|
+
activeNote.commit(TestResult.FAILED);
|
|
193
|
+
return failedToolResult('pressKey', errorMsg, {
|
|
194
|
+
...toolResult,
|
|
195
|
+
code: typeCommand,
|
|
196
|
+
suggestion: 'The key was not recognized as a standard key press and type() fallback failed.',
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
const focusFreeKeys = new Set(['Escape', 'Esc', 'Tab', 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12']);
|
|
200
|
+
const needsFocus = !focusFreeKeys.has(keyToUse) && !modifier;
|
|
201
|
+
if (needsFocus) {
|
|
202
|
+
const currentAriaState = stateManager.getCurrentState()?.ariaSnapshot;
|
|
203
|
+
const focused = extractFocusedElement(currentAriaState ?? null);
|
|
204
|
+
if (!focused) {
|
|
205
|
+
activeNote.commit(TestResult.FAILED);
|
|
206
|
+
return failedToolResult('pressKey', `No element is focused. Key '${keyToUse}' requires a focused element.`, {
|
|
207
|
+
suggestion: 'Click the target element first, then press the key.',
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
212
|
+
const action = explorer.createAction();
|
|
213
|
+
let pressKeyCommand;
|
|
214
|
+
if (modifier) {
|
|
215
|
+
const modifiers = Array.isArray(modifier) ? modifier : [modifier];
|
|
216
|
+
pressKeyCommand = `I.pressKey([${modifiers.map((m) => JSON.stringify(m)).join(', ')}, ${JSON.stringify(keyToUse)}])`;
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
pressKeyCommand = `I.pressKey(${JSON.stringify(keyToUse)})`;
|
|
220
|
+
}
|
|
221
|
+
await action.attempt(pressKeyCommand, explanation);
|
|
222
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, key);
|
|
223
|
+
if (!action.lastError) {
|
|
224
|
+
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
225
|
+
activeNote.screenshot = await action.saveScreenshot();
|
|
226
|
+
}
|
|
227
|
+
activeNote.commit(TestResult.PASSED);
|
|
228
|
+
return successToolResult('pressKey', {
|
|
229
|
+
...toolResult,
|
|
230
|
+
message: `Pressed key: ${key}${modifier ? ` with modifier(s): ${Array.isArray(modifier) ? modifier.join('+') : modifier}` : ''}`,
|
|
231
|
+
code: pressKeyCommand,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
const errorMsg = `pressKey() failed: ${action.lastError?.toString()}`;
|
|
235
|
+
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
236
|
+
activeNote.screenshot = await action.saveScreenshot();
|
|
237
|
+
}
|
|
238
|
+
activeNote.commit(TestResult.FAILED);
|
|
239
|
+
return failedToolResult('pressKey', errorMsg, {
|
|
240
|
+
...toolResult,
|
|
241
|
+
code: pressKeyCommand,
|
|
242
|
+
suggestion: 'Verify the key name is correct. For typing text, use form() tool instead.',
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch (error) {
|
|
246
|
+
activeNote.commit(TestResult.FAILED);
|
|
247
|
+
const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
|
|
248
|
+
return failedToolResult('pressKey', `PressKey tool failed: ${errorMessage}`);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
}),
|
|
252
|
+
form: tool({
|
|
253
|
+
description: dedent `
|
|
254
|
+
Execute raw CodeceptJS code block with multiple commands.
|
|
255
|
+
USE THIS TOOL for all keyboard interactions: I.fillField, I.type, I.pressKey
|
|
256
|
+
|
|
257
|
+
Follow <actions> from system prompt for available commands.
|
|
258
|
+
Follow <locator_priority> from system prompt for locator selection.
|
|
259
|
+
|
|
260
|
+
Use cases:
|
|
261
|
+
- Typing into input fields (I.fillField, I.type)
|
|
262
|
+
- Pressing keyboard keys (I.pressKey)
|
|
263
|
+
- Working with iframes (switch context with I.switchTo)
|
|
264
|
+
- Performing multiple form actions in a single batch
|
|
265
|
+
- Complex interactions requiring sequential commands
|
|
266
|
+
|
|
267
|
+
Example - filling a form with context (PREFERRED):
|
|
268
|
+
I.fillField('Username', 'John', '.login-form')
|
|
269
|
+
I.selectOption('Country', 'USA', '.address-section')
|
|
270
|
+
I.attachFile('input[type="file"]', 'path/to/file', '.upload-section')
|
|
271
|
+
|
|
272
|
+
Example - filling a form with ARIA locators:
|
|
273
|
+
I.fillField({"role":"textbox","text":"Title"}, 'My Article')
|
|
274
|
+
I.selectOption({"role":"combobox","text":"Category"}, 'Technology')
|
|
275
|
+
|
|
276
|
+
Do not submit form - use verify() first to check fields were filled correctly, then click() to submit.
|
|
277
|
+
Do not use: wait functions, amOnPage, reloadPage, saveScreenshot
|
|
278
|
+
`,
|
|
279
|
+
inputSchema: z.object({
|
|
280
|
+
codeBlock: z.string().describe('Valid CodeceptJS code starting with I. Can contain multiple commands separated by newlines.'),
|
|
281
|
+
explanation: z.string().describe('Reason for executing this code sequence.'),
|
|
282
|
+
}),
|
|
283
|
+
execute: async ({ codeBlock, explanation }) => {
|
|
284
|
+
const activeNote = task.startNote(explanation);
|
|
285
|
+
try {
|
|
286
|
+
if (!codeBlock.trim()) {
|
|
287
|
+
activeNote.commit(TestResult.FAILED);
|
|
288
|
+
return failedToolResult('form', 'CodeBlock cannot be empty');
|
|
289
|
+
}
|
|
290
|
+
const lines = codeBlock
|
|
291
|
+
.split('\n')
|
|
292
|
+
.map((line) => line.trim())
|
|
293
|
+
.filter((line) => line);
|
|
294
|
+
const codeLines = lines.filter((line) => !line.startsWith('//'));
|
|
295
|
+
if (!codeLines.every((line) => line.startsWith('I.'))) {
|
|
296
|
+
activeNote.commit(TestResult.FAILED);
|
|
297
|
+
return failedToolResult('form', 'All non-comment lines must start with I.', {
|
|
298
|
+
suggestion: 'Try again but pass valid CodeceptJS code where every non-comment line starts with I.',
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
302
|
+
const formLocator = codeLines[0] || 'form';
|
|
303
|
+
const action = explorer.createAction();
|
|
304
|
+
await action.attempt(codeBlock, explanation);
|
|
305
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, formLocator);
|
|
306
|
+
if (action.lastError) {
|
|
307
|
+
const message = action.lastError ? String(action.lastError) : 'Unknown error';
|
|
308
|
+
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
309
|
+
activeNote.screenshot = await action.saveScreenshot();
|
|
310
|
+
}
|
|
311
|
+
activeNote.commit(TestResult.FAILED);
|
|
312
|
+
let formSuggestion = 'Look into error message and identify which commands passed and which failed. Continue execution using step-by-step approach using click() and form() tools.';
|
|
313
|
+
if (message.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN)) {
|
|
314
|
+
const disambiguated = await disambiguateElements(action.lastError, explanation, explorer.getAIProvider());
|
|
315
|
+
if (disambiguated) {
|
|
316
|
+
formSuggestion = `Multiple elements matched. Add step.opts({ elementIndex: ${disambiguated.position} }) to the failing command. Fallback locator: ${disambiguated.xpath}`;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
return failedToolResult('form', `Form execution FAILED! ${message}`, {
|
|
320
|
+
...toolResult,
|
|
321
|
+
code: codeBlock,
|
|
322
|
+
suggestion: formSuggestion,
|
|
323
|
+
}, action.lastError);
|
|
324
|
+
}
|
|
325
|
+
if (toolResult?.pageDiff?.ariaChanges || toolResult?.pageDiff?.urlChanged) {
|
|
326
|
+
activeNote.screenshot = await action.saveScreenshot();
|
|
327
|
+
}
|
|
328
|
+
activeNote.commit(TestResult.PASSED);
|
|
329
|
+
return successToolResult('form', {
|
|
330
|
+
...toolResult,
|
|
331
|
+
message: `Form completed successfully with ${lines.length} commands.`,
|
|
332
|
+
commandsExecuted: lines.length,
|
|
333
|
+
code: codeBlock,
|
|
334
|
+
suggestion: 'Verify the form was filled in correctly using see() tool. Submit if needed by using click() tool.',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
catch (error) {
|
|
338
|
+
activeNote.commit(TestResult.FAILED);
|
|
339
|
+
const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
|
|
340
|
+
return failedToolResult('form', `Form tool failed: ${errorMessage}`);
|
|
341
|
+
}
|
|
342
|
+
},
|
|
343
|
+
}),
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
export function createSpecialContextTools(explorer, context) {
|
|
347
|
+
const stateManager = explorer.getStateManager();
|
|
348
|
+
if (context !== 'iframe') {
|
|
349
|
+
return {};
|
|
350
|
+
}
|
|
351
|
+
return {
|
|
352
|
+
exitIframe: tool({
|
|
353
|
+
description: dedent `
|
|
354
|
+
Exit the current iframe and return to the main page context.
|
|
355
|
+
|
|
356
|
+
Use this only when you are already working inside an iframe and need to interact
|
|
357
|
+
with elements outside that iframe.
|
|
358
|
+
`,
|
|
359
|
+
inputSchema: z.object({
|
|
360
|
+
reason: z.string().optional().describe('Why you need to leave the iframe context.'),
|
|
361
|
+
}),
|
|
362
|
+
execute: async ({ reason }) => {
|
|
363
|
+
try {
|
|
364
|
+
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
365
|
+
if (!previousState.isInsideIframe) {
|
|
366
|
+
return failedToolResult('exitIframe', 'You are not inside an iframe.', {
|
|
367
|
+
suggestion: 'Continue interacting with the current page context.',
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
await explorer.switchToMainFrame();
|
|
371
|
+
const action = explorer.createAction();
|
|
372
|
+
const nextState = await action.capturePageState();
|
|
373
|
+
const toolResult = await nextState.toToolResult(previousState, 'I.switchTo()');
|
|
374
|
+
return successToolResult('exitIframe', {
|
|
375
|
+
...toolResult,
|
|
376
|
+
message: reason || 'Exited iframe and returned to the main page context.',
|
|
377
|
+
code: 'I.switchTo()',
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
|
|
382
|
+
return failedToolResult('exitIframe', `Failed to exit iframe: ${errorMessage}`);
|
|
383
|
+
}
|
|
384
|
+
},
|
|
385
|
+
}),
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
export function createAgentTools({ explorer, researcher, navigator, }) {
|
|
389
|
+
let visionDisabled = false;
|
|
390
|
+
return {
|
|
391
|
+
see: tool({
|
|
392
|
+
description: dedent `
|
|
393
|
+
Check the page contents based on current page state and screenshot.
|
|
394
|
+
This tool will trigger visual research to check the page contents on request.
|
|
395
|
+
Use it to verify the actions were performed correctly and the page is in the expected state.
|
|
396
|
+
|
|
397
|
+
<example>
|
|
398
|
+
request: "Check current state of the Login form"
|
|
399
|
+
result: "Login form is visible with username and password fields, username is filled with 'testuser' and password is empty'
|
|
400
|
+
</example>
|
|
401
|
+
`,
|
|
402
|
+
inputSchema: z.object({
|
|
403
|
+
request: z.string().describe('LLM-friendly description of the page contents to look for. 1-3 sentences. No more than 100 words.'),
|
|
404
|
+
}),
|
|
405
|
+
execute: async ({ request }) => {
|
|
406
|
+
if (visionDisabled) {
|
|
407
|
+
return failedToolResult('see', 'Vision tools are disabled for this session. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.');
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const action = explorer.createAction();
|
|
411
|
+
const actionResult = await action.caputrePageWithScreenshot();
|
|
412
|
+
if (!actionResult.screenshot) {
|
|
413
|
+
return failedToolResult('see', 'Failed to capture screenshot for analysis');
|
|
414
|
+
}
|
|
415
|
+
const analysisResult = await researcher.answerQuestionAboutScreenshot(actionResult, request);
|
|
416
|
+
if (!analysisResult) {
|
|
417
|
+
return failedToolResult('see', 'AI analysis failed to process the screenshot');
|
|
418
|
+
}
|
|
419
|
+
return successToolResult('see', {
|
|
420
|
+
analysis: analysisResult,
|
|
421
|
+
message: `Successfully analyzed screenshot for: ${request}`,
|
|
422
|
+
suggestion: 'Visual confirmation is valid evidence for test results. Use record() to note the visual findings.',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
|
|
427
|
+
visionDisabled = true;
|
|
428
|
+
tag('warning').log('⚠️ Vision model is not available. Visual checks are disabled for this session.');
|
|
429
|
+
return failedToolResult('see', `See tool failed: ${errorMessage}`, {
|
|
430
|
+
suggestion: 'Vision is now disabled. Use context() to get fresh ARIA snapshot and analyze page state from ARIA data.',
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
},
|
|
434
|
+
}),
|
|
435
|
+
context: tool({
|
|
436
|
+
description: dedent `
|
|
437
|
+
Get current page HTML and ARIA snapshot.
|
|
438
|
+
|
|
439
|
+
DO NOT call this if:
|
|
440
|
+
- You just performed an action (pageDiff already provided in response)
|
|
441
|
+
- You already have recent <page_html>/<page_aria> in context
|
|
442
|
+
- You're about to perform an action (you'll get pageDiff after)
|
|
443
|
+
|
|
444
|
+
Call ONLY when:
|
|
445
|
+
- Context is stale (many conversation turns since last state update)
|
|
446
|
+
- You suspect page changed externally (timers, auto-refresh)
|
|
447
|
+
- You need fresh state before planning next actions
|
|
448
|
+
`,
|
|
449
|
+
inputSchema: z.object({
|
|
450
|
+
reason: z.string().describe('Why do you need fresh context? Required to prevent overuse.'),
|
|
451
|
+
}),
|
|
452
|
+
execute: async ({ reason }) => {
|
|
453
|
+
try {
|
|
454
|
+
const stateManager = explorer.getStateManager();
|
|
455
|
+
const currentState = stateManager.getCurrentState();
|
|
456
|
+
if (!currentState) {
|
|
457
|
+
return failedToolResult('context', 'No current page state available.');
|
|
458
|
+
}
|
|
459
|
+
const actionResult = ActionResult.fromState(currentState);
|
|
460
|
+
const html = await actionResult.simplifiedHtml();
|
|
461
|
+
const aria = actionResult.getInteractiveARIA();
|
|
462
|
+
return successToolResult('context', {
|
|
463
|
+
url: currentState.url,
|
|
464
|
+
title: currentState.title,
|
|
465
|
+
suggestion: 'If not enough context received, call see() to visually identify elements in page contents',
|
|
466
|
+
aria,
|
|
467
|
+
html,
|
|
468
|
+
reminder: 'Context provided. Do not call context() again until you perform actions or suspect page changed.',
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
catch (error) {
|
|
472
|
+
const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
|
|
473
|
+
return failedToolResult('context', `Context tool failed: ${errorMessage}`);
|
|
474
|
+
}
|
|
475
|
+
},
|
|
476
|
+
}),
|
|
477
|
+
verify: tool({
|
|
478
|
+
description: dedent `
|
|
479
|
+
Verify an assertion about the current page state using AI-powered verification.
|
|
480
|
+
This tool uses the Navigator's verifyState method to check if the page matches the expected condition.
|
|
481
|
+
Be precise and explicit in your assertion request to avoid false positives.
|
|
482
|
+
Ask the question including the context and if possible the current user flow.
|
|
483
|
+
Identify which page area you are referring to and which must be asserted.
|
|
484
|
+
If possible provide context locator to narrow down the search area.
|
|
485
|
+
The AI will attempt multiple verification strategies using CodeceptJS assertions.
|
|
486
|
+
`,
|
|
487
|
+
inputSchema: z.object({
|
|
488
|
+
assertion: z.string().describe('The assertion or condition to verify on the current page (e.g., "User is logged in", "Form validation error is displayed in the footer")'),
|
|
489
|
+
}),
|
|
490
|
+
execute: async ({ assertion }) => {
|
|
491
|
+
try {
|
|
492
|
+
const currentState = explorer.getStateManager().getCurrentState();
|
|
493
|
+
const verifications = currentState?.verifications;
|
|
494
|
+
if (verifications?.[assertion] !== undefined) {
|
|
495
|
+
return failedToolResult('verify', `Already verified: "${assertion}" → ${verifications[assertion] ? 'PASS' : 'FAIL'}`, {
|
|
496
|
+
alreadyVerified: true,
|
|
497
|
+
verifications,
|
|
498
|
+
suggestion: verifications[assertion] ? 'This verification already passed. Call finish() to complete the test.' : 'This verification already failed. Perform actions to change the page state, then try again.',
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const action = explorer.createAction();
|
|
502
|
+
const actionResult = await action.capturePageState();
|
|
503
|
+
const result = await navigator.verifyState(assertion, actionResult);
|
|
504
|
+
if (result.verified) {
|
|
505
|
+
return successToolResult('verify', {
|
|
506
|
+
message: `Verification passed: ${assertion}`,
|
|
507
|
+
code: result.successfulCodes.join('\n'),
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return failedToolResult('verify', `Verification failed: ${assertion}`, {
|
|
511
|
+
suggestion: 'The assertion could not be verified. Check if the condition is actually present on the page or try a different assertion.',
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
catch (error) {
|
|
515
|
+
const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
|
|
516
|
+
return failedToolResult('verify', `Verify tool failed: ${errorMessage}`, {
|
|
517
|
+
error: errorMessage,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
},
|
|
521
|
+
}),
|
|
522
|
+
research: tool({
|
|
523
|
+
description: dedent `
|
|
524
|
+
Research the current page to understand its structure, UI elements, and navigation.
|
|
525
|
+
This tool provides UI map report including forms, buttons, menus, and other interactive elements.
|
|
526
|
+
|
|
527
|
+
DO NOT call this if:
|
|
528
|
+
- You already have <page_ui_map> or <initial_page_ui_map> in context
|
|
529
|
+
- You just navigated to a page (research is provided automatically)
|
|
530
|
+
- You're on the same page you already researched
|
|
531
|
+
- pageDiff was small (minor changes don't need full research)
|
|
532
|
+
|
|
533
|
+
Call ONLY when:
|
|
534
|
+
- Page structure is unclear and no UI map was provided
|
|
535
|
+
- You need to discover hidden/collapsed elements not in current context
|
|
536
|
+
- Page has dramatically changed after previous action
|
|
537
|
+
|
|
538
|
+
Avoid calling this tool twice in a row.
|
|
539
|
+
`,
|
|
540
|
+
inputSchema: z.object({
|
|
541
|
+
reason: z.string().describe('Why do you need research? What information is missing from existing context?'),
|
|
542
|
+
}),
|
|
543
|
+
execute: async ({ reason }) => {
|
|
544
|
+
try {
|
|
545
|
+
const stateManager = explorer.getStateManager();
|
|
546
|
+
const currentState = stateManager.getCurrentState();
|
|
547
|
+
if (!currentState) {
|
|
548
|
+
return failedToolResult('research', 'No current page state available. Navigate to a page first.');
|
|
549
|
+
}
|
|
550
|
+
const researchResult = await researcher.research(currentState, { screenshot: true, data: true });
|
|
551
|
+
return successToolResult('research', {
|
|
552
|
+
analysis: researchResult,
|
|
553
|
+
aria: ActionResult.fromState(currentState).getInteractiveARIA(),
|
|
554
|
+
message: `Successfully researched page: ${currentState.url}.`,
|
|
555
|
+
suggestion: dedent `
|
|
556
|
+
You received comprehensive UI map report. Use it to understand the page structure and navigate to the elements.
|
|
557
|
+
Do not ask for research() if you have <page_ui_map> for current page.
|
|
558
|
+
Follow <section_context_rule> when selecting locators for all tools.
|
|
559
|
+
|
|
560
|
+
If sections are listed in report use section container locators when picking elements from inside sections:
|
|
561
|
+
|
|
562
|
+
${sectionContextRule}
|
|
563
|
+
`,
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
catch (error) {
|
|
567
|
+
const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
|
|
568
|
+
return failedToolResult('research', `Research tool failed: ${errorMessage}`, {
|
|
569
|
+
error: errorMessage,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
}),
|
|
574
|
+
interact: tool({
|
|
575
|
+
description: dedent `
|
|
576
|
+
Execute an action on the current page using AI-powered interaction.
|
|
577
|
+
Use this to perform actions like clicking buttons, selecting options, filling forms, etc.
|
|
578
|
+
The AI will generate and try multiple CodeceptJS code strategies to accomplish the instruction.
|
|
579
|
+
`,
|
|
580
|
+
inputSchema: z.object({
|
|
581
|
+
instruction: z.string().describe('What action to perform on the page, e.g. "select new suite option", "click the Submit button"'),
|
|
582
|
+
}),
|
|
583
|
+
execute: async ({ instruction }) => {
|
|
584
|
+
try {
|
|
585
|
+
const stateManager = explorer.getStateManager();
|
|
586
|
+
const currentState = stateManager.getCurrentState();
|
|
587
|
+
if (!currentState) {
|
|
588
|
+
return failedToolResult('interact', 'No current page state available. Navigate to a page first.');
|
|
589
|
+
}
|
|
590
|
+
const previousState = ActionResult.fromState(currentState);
|
|
591
|
+
const actionResult = ActionResult.fromState(currentState);
|
|
592
|
+
const success = await navigator.resolveState(instruction, actionResult);
|
|
593
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, instruction);
|
|
594
|
+
if (success) {
|
|
595
|
+
return successToolResult('interact', {
|
|
596
|
+
...toolResult,
|
|
597
|
+
message: `Successfully executed: ${instruction}`,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
return failedToolResult('interact', `Failed to execute: ${instruction}`, {
|
|
601
|
+
...toolResult,
|
|
602
|
+
suggestion: 'The action could not be completed. Try a different instruction or use more specific element descriptions.',
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
catch (error) {
|
|
606
|
+
const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
|
|
607
|
+
return failedToolResult('interact', `Interact tool failed: ${errorMessage}`, {
|
|
608
|
+
error: errorMessage,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
},
|
|
612
|
+
}),
|
|
613
|
+
visualClick: tool({
|
|
614
|
+
description: dedent `
|
|
615
|
+
Click an element by visual identification when locator-based click() fails.
|
|
616
|
+
|
|
617
|
+
Use this as fallback when:
|
|
618
|
+
- click() failed with all provided commands
|
|
619
|
+
- Element is visually present but not accessible via locators
|
|
620
|
+
- Custom/canvas elements that don't have proper DOM structure
|
|
621
|
+
|
|
622
|
+
This tool analyzes screenshot to locate element and clicks at its coordinates.
|
|
623
|
+
`,
|
|
624
|
+
inputSchema: z.object({
|
|
625
|
+
element: z.string().describe('Visual description of element to click (e.g., "blue Submit button in the modal footer", "Settings gear icon in top right")'),
|
|
626
|
+
context: z.string().describe('What you already tried and why it failed - helps with accurate identification'),
|
|
627
|
+
}),
|
|
628
|
+
execute: async ({ element, context }) => {
|
|
629
|
+
if (visionDisabled) {
|
|
630
|
+
return failedToolResult('visualClick', 'Vision tools are disabled for this session. Use xpathCheck() to find the element, then click() with the discovered locator.');
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
const stateManager = explorer.getStateManager();
|
|
634
|
+
const currentState = stateManager.getCurrentState();
|
|
635
|
+
if (!currentState) {
|
|
636
|
+
return failedToolResult('visualClick', 'No current page state available.');
|
|
637
|
+
}
|
|
638
|
+
const previousState = ActionResult.fromState(currentState);
|
|
639
|
+
const action = explorer.createAction();
|
|
640
|
+
const actionResult = await action.caputrePageWithScreenshot();
|
|
641
|
+
if (!actionResult.screenshot) {
|
|
642
|
+
return failedToolResult('visualClick', 'Failed to capture screenshot for visual analysis');
|
|
643
|
+
}
|
|
644
|
+
const locationResult = await researcher.checkElementLocation(actionResult, element);
|
|
645
|
+
if (!locationResult) {
|
|
646
|
+
return failedToolResult('visualClick', 'Visual analysis failed to process the screenshot');
|
|
647
|
+
}
|
|
648
|
+
const coordMatch = locationResult.match(/(\d+)X,\s*(\d+)Y/i);
|
|
649
|
+
if (!coordMatch) {
|
|
650
|
+
return failedToolResult('visualClick', `Element not found: ${locationResult}`, {
|
|
651
|
+
analysis: locationResult,
|
|
652
|
+
suggestion: 'Element may not be visible on screen. Try scrolling or check if element exists.',
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
const x = Number.parseInt(coordMatch[1], 10);
|
|
656
|
+
const y = Number.parseInt(coordMatch[2], 10);
|
|
657
|
+
const clickSuccess = await action.attempt(`I.clickXY(${x}, ${y})`, `Visual click: ${element}`);
|
|
658
|
+
const toolResult = await ActionResult.fromState(stateManager.getCurrentState()).toToolResult(previousState, element);
|
|
659
|
+
if (clickSuccess) {
|
|
660
|
+
return successToolResult('visualClick', {
|
|
661
|
+
...toolResult,
|
|
662
|
+
message: `Clicked "${element}" at coordinates (${x}, ${y})`,
|
|
663
|
+
analysis: locationResult,
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
return failedToolResult('visualClick', 'Click at coordinates failed', {
|
|
667
|
+
...toolResult,
|
|
668
|
+
coordinates: { x, y },
|
|
669
|
+
analysis: locationResult,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
const errorMessage = error instanceof Error ? error.toString() : 'Unknown error occurred';
|
|
674
|
+
visionDisabled = true;
|
|
675
|
+
tag('warning').log('⚠️ Vision model is not available. Visual clicks are disabled for this session.');
|
|
676
|
+
return failedToolResult('visualClick', `visualClick tool failed: ${errorMessage}`, {
|
|
677
|
+
suggestion: 'Vision is now disabled. Use xpathCheck() to find the element, then click() with the discovered locator.',
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
},
|
|
681
|
+
}),
|
|
682
|
+
askUser: tool({
|
|
683
|
+
description: dedent `
|
|
684
|
+
Ask the user for help when you're stuck or unsure how to proceed.
|
|
685
|
+
Only available in interactive mode (TUI).
|
|
686
|
+
|
|
687
|
+
Use when:
|
|
688
|
+
- Locator-based clicks keep failing
|
|
689
|
+
- You can't find an element that should exist
|
|
690
|
+
- Form interaction isn't working as expected
|
|
691
|
+
- You need clarification on what action to take
|
|
692
|
+
`,
|
|
693
|
+
inputSchema: z.object({
|
|
694
|
+
question: z.string().describe('What you need help with - be specific about what failed'),
|
|
695
|
+
context: z.string().optional().describe('Relevant context like locators tried, errors received'),
|
|
696
|
+
}),
|
|
697
|
+
execute: async ({ question, context }) => {
|
|
698
|
+
if (!isInteractive()) {
|
|
699
|
+
return {
|
|
700
|
+
success: false,
|
|
701
|
+
message: 'User input not available in non-interactive mode',
|
|
702
|
+
suggestion: 'Continue with automated recovery',
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
const prompt = context ? `${question}\n\nContext: ${context}\n\nYour suggestion ("skip" to continue):` : `${question}\n\nYour suggestion ("skip" to continue):`;
|
|
706
|
+
const userInput = await pause(prompt);
|
|
707
|
+
if (!userInput || userInput.toLowerCase() === 'skip') {
|
|
708
|
+
return { success: false, message: 'User skipped' };
|
|
709
|
+
}
|
|
710
|
+
return {
|
|
711
|
+
success: true,
|
|
712
|
+
userSuggestion: userInput,
|
|
713
|
+
instruction: 'Follow the user suggestion. Use interact() tool to execute.',
|
|
714
|
+
};
|
|
715
|
+
},
|
|
716
|
+
}),
|
|
717
|
+
back: tool({
|
|
718
|
+
description: dedent `
|
|
719
|
+
Navigate back to the previous page (most recent URL different from current).
|
|
720
|
+
Use when you accidentally navigated to a wrong page and want to return one step.
|
|
721
|
+
For going all the way back to the starting page, use reset() instead.
|
|
722
|
+
`,
|
|
723
|
+
inputSchema: z.object({
|
|
724
|
+
reason: z.string().describe('Why you need to go back'),
|
|
725
|
+
}),
|
|
726
|
+
execute: async ({ reason }) => {
|
|
727
|
+
const stateManager = explorer.getStateManager();
|
|
728
|
+
const currentUrl = stateManager.getCurrentState()?.url;
|
|
729
|
+
const history = stateManager.getStateHistory();
|
|
730
|
+
let targetUrl = null;
|
|
731
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
732
|
+
const url = history[i].toState.url;
|
|
733
|
+
if (url !== currentUrl) {
|
|
734
|
+
targetUrl = url;
|
|
735
|
+
break;
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (!targetUrl) {
|
|
739
|
+
return failedToolResult('back', 'No previous page found in history.', {
|
|
740
|
+
suggestion: 'Use reset() to navigate to the starting page.',
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
const action = explorer.createAction();
|
|
744
|
+
const success = await action.attempt(`I.amOnPage(${JSON.stringify(targetUrl)})`, `${reason} (BACK to ${targetUrl})`);
|
|
745
|
+
if (success) {
|
|
746
|
+
const previousState = ActionResult.fromState(stateManager.getCurrentState());
|
|
747
|
+
const toolResult = await previousState.toToolResult(previousState, `I.amOnPage("${targetUrl}")`);
|
|
748
|
+
return successToolResult('back', {
|
|
749
|
+
...toolResult,
|
|
750
|
+
message: `Navigated back to ${targetUrl}`,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
return failedToolResult('back', `Failed to navigate back to ${targetUrl}`, {
|
|
754
|
+
suggestion: 'Try reset() to return to the starting page.',
|
|
755
|
+
...(action.lastError && { error: action.lastError.toString() }),
|
|
756
|
+
});
|
|
757
|
+
},
|
|
758
|
+
}),
|
|
759
|
+
getVisitedStates: tool({
|
|
760
|
+
description: 'List all previously visited page states (deduped by URL). Use to find pages to navigate back to.',
|
|
761
|
+
inputSchema: z.object({}),
|
|
762
|
+
execute: async () => {
|
|
763
|
+
const history = explorer.getStateManager().getStateHistory();
|
|
764
|
+
const seen = new Set();
|
|
765
|
+
const states = history
|
|
766
|
+
.map((t) => t.toState)
|
|
767
|
+
.filter((s) => {
|
|
768
|
+
if (seen.has(s.url))
|
|
769
|
+
return false;
|
|
770
|
+
seen.add(s.url);
|
|
771
|
+
return true;
|
|
772
|
+
})
|
|
773
|
+
.map((s, i) => ({ index: i, url: s.url, title: s.title, h1: s.h1 }));
|
|
774
|
+
return { success: true, states };
|
|
775
|
+
},
|
|
776
|
+
}),
|
|
777
|
+
xpathCheck: tool({
|
|
778
|
+
description: dedent `
|
|
779
|
+
It seems the desired element could not be reached by Tester.
|
|
780
|
+
Full HTML context is too large to provide, but you can propose XPath locators to search for the needed element.
|
|
781
|
+
|
|
782
|
+
Think carefully about the XPath — if it's too narrow you may miss the element.
|
|
783
|
+
Use broad enough locators combining: ids, classes, aria-* attributes, semantic elements, data attributes, text content.
|
|
784
|
+
|
|
785
|
+
Start broad (e.g. //button, //*[contains(text(), 'Save')]) then narrow down.
|
|
786
|
+
Multiple calls are encouraged — refine until you find a unique match.
|
|
787
|
+
|
|
788
|
+
After finding matches, visibility is automatically verified in the live browser.
|
|
789
|
+
If element exists in DOM but is not visible, consider what action could reveal it (scroll, click to expand, wait).
|
|
790
|
+
`,
|
|
791
|
+
inputSchema: z.object({
|
|
792
|
+
xpath: z.string().describe('XPath expression — use broad patterns to avoid missing the element'),
|
|
793
|
+
reason: z.string().describe('What element you are looking for and why'),
|
|
794
|
+
}),
|
|
795
|
+
execute: async ({ xpath, reason }) => {
|
|
796
|
+
const stateManager = explorer.getStateManager();
|
|
797
|
+
const currentState = stateManager.getCurrentState();
|
|
798
|
+
if (!currentState) {
|
|
799
|
+
return failedToolResult('xpathCheck', 'No current page state available.');
|
|
800
|
+
}
|
|
801
|
+
const html = ActionResult.fromState(currentState).html;
|
|
802
|
+
if (!html) {
|
|
803
|
+
return failedToolResult('xpathCheck', 'No HTML available for current page state.');
|
|
804
|
+
}
|
|
805
|
+
const result = await WebElement.findByXPath(html, xpath);
|
|
806
|
+
if (result.error) {
|
|
807
|
+
return failedToolResult('xpathCheck', `XPath error: ${result.error}`, {
|
|
808
|
+
suggestion: 'Check XPath syntax. Common issues: unescaped quotes, missing brackets, invalid axis names.',
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
if (result.totalFound === 0) {
|
|
812
|
+
return failedToolResult('xpathCheck', `No elements matched XPath: ${xpath}`, {
|
|
813
|
+
suggestion: 'Try a broader expression. Examples: //*[contains(@class, "btn")], //button, //*[contains(text(), "keyword")]',
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
const action = explorer.createAction();
|
|
817
|
+
const visible = await action.attempt(`I.seeElement(${JSON.stringify(xpath)})`, 'xpathCheck visibility', false);
|
|
818
|
+
const matchesSummary = result.elements.map((el, i) => `${i + 1}. <${el.tag} ${el.keyAttrs}> text="${el.text}" html: ${el.outerHTML}`).join('\n');
|
|
819
|
+
const visibilityNote = visible ? 'Element IS visible in browser — Tester can use this XPath as locator.' : 'Element exists in DOM but is NOT visible. May need scrolling, a click to reveal, or is hidden.';
|
|
820
|
+
return successToolResult('xpathCheck', {
|
|
821
|
+
totalFound: result.totalFound,
|
|
822
|
+
matches: matchesSummary,
|
|
823
|
+
visibilityNote,
|
|
824
|
+
xpath,
|
|
825
|
+
});
|
|
826
|
+
},
|
|
827
|
+
}),
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
const PAGE_DIFF_SUGGESTION = 'Analyze page diff. htmlParts shows what changed and WHERE — each part has a container selector. Use the container as context when clicking elements from the diff.';
|
|
831
|
+
function transformContainsCommand(command) {
|
|
832
|
+
if (!command.includes(':contains('))
|
|
833
|
+
return command;
|
|
834
|
+
const containsMatch = command.match(/:contains\(["']([^"']+)["']\)/);
|
|
835
|
+
if (!containsMatch)
|
|
836
|
+
return command;
|
|
837
|
+
const text = containsMatch[1];
|
|
838
|
+
const cleaned = command.replace(containsMatch[0], '');
|
|
839
|
+
const twoArgMatch = cleaned.match(/I\.click\(\s*(['"`])(.+?)\1\s*,\s*(['"`])(.+?)\3\s*\)/);
|
|
840
|
+
if (twoArgMatch) {
|
|
841
|
+
const baseSelector = twoArgMatch[2].trim();
|
|
842
|
+
const context = twoArgMatch[4].trim();
|
|
843
|
+
return `I.click(${JSON.stringify(text)}, ${JSON.stringify(`${context} ${baseSelector}`)})`;
|
|
844
|
+
}
|
|
845
|
+
const oneArgMatch = cleaned.match(/I\.click\(\s*(['"`])(.+?)\1\s*\)/);
|
|
846
|
+
if (oneArgMatch) {
|
|
847
|
+
const baseSelector = oneArgMatch[2].trim();
|
|
848
|
+
return `I.click(${JSON.stringify(text)}, ${JSON.stringify(baseSelector)})`;
|
|
849
|
+
}
|
|
850
|
+
return command;
|
|
851
|
+
}
|
|
852
|
+
function countAriaChanges(ariaChanges) {
|
|
853
|
+
const addedCount = (ariaChanges.match(/\n {4}- /g) || []).length;
|
|
854
|
+
const removedMatch = ariaChanges.match(/removed: (\d+) interactive/);
|
|
855
|
+
const removedCount = removedMatch ? Number.parseInt(removedMatch[1]) : 0;
|
|
856
|
+
return addedCount + removedCount;
|
|
857
|
+
}
|
|
858
|
+
function successToolResult(action, data) {
|
|
859
|
+
const result = { success: true, action, ...data };
|
|
860
|
+
if (data?.pageDiff) {
|
|
861
|
+
let suggestion = PAGE_DIFF_SUGGESTION;
|
|
862
|
+
const ariaChanges = data.pageDiff.ariaChanges || '';
|
|
863
|
+
if (countAriaChanges(ariaChanges) >= 50) {
|
|
864
|
+
suggestion = `MAJOR PAGE CHANGE. Page entered a different mode. Check htmlParts and iframes in pageDiff before next action. ${suggestion}`;
|
|
865
|
+
}
|
|
866
|
+
else if (ariaChanges.includes('heading') && ariaChanges.includes('added')) {
|
|
867
|
+
suggestion += ' WARNING: A new panel or modal may have appeared. If this was not the intended action, close it and try a different element.';
|
|
868
|
+
}
|
|
869
|
+
result.suggestion = data.suggestion ? `${data.suggestion} ${suggestion}` : suggestion;
|
|
870
|
+
}
|
|
871
|
+
return result;
|
|
872
|
+
}
|
|
873
|
+
async function failedToolResult(action, message, data, error) {
|
|
874
|
+
const result = { success: false, action, message, ...data };
|
|
875
|
+
if (data?.pageDiff) {
|
|
876
|
+
result.suggestion = data.suggestion ? `${data.suggestion} ${PAGE_DIFF_SUGGESTION}` : PAGE_DIFF_SUGGESTION;
|
|
877
|
+
}
|
|
878
|
+
const errorTexts = [message, ...(data?.attempts?.map((a) => a.error || '') || [])];
|
|
879
|
+
const hasMultipleElements = errorTexts.some((t) => t.toLowerCase().includes(MULTIPLE_ELEMENTS_PATTERN));
|
|
880
|
+
const multipleElementsSuggestion = hasMultipleElements ? getMultipleElementsSuggestion() : null;
|
|
881
|
+
if (multipleElementsSuggestion) {
|
|
882
|
+
result.suggestion = multipleElementsSuggestion;
|
|
883
|
+
result.multipleElementsDetected = true;
|
|
884
|
+
result.elements = await formatMatchedElements(error);
|
|
885
|
+
return result;
|
|
886
|
+
}
|
|
887
|
+
const notFoundSuggestion = getNotFoundSuggestion(message);
|
|
888
|
+
if (notFoundSuggestion) {
|
|
889
|
+
result.suggestion = notFoundSuggestion;
|
|
890
|
+
result.elementNotFound = true;
|
|
891
|
+
}
|
|
892
|
+
return result;
|
|
893
|
+
}
|
|
894
|
+
function getMultipleElementsSuggestion() {
|
|
895
|
+
return dedent `
|
|
896
|
+
Multiple elements matched your locator. To fix this:
|
|
897
|
+
1. Use container context: I.click({ "role": "button", "text": "Submit" }, '.form-container')
|
|
898
|
+
2. Use more specific CSS: target the actual element (input, button, a) not wrapper divs
|
|
899
|
+
3. Add distinguishing attributes: input[type="submit"], button[type="submit"], [value="..."]
|
|
900
|
+
4. If buttons have similar text like "Create" and "Create Demo", use the FULL unique text
|
|
901
|
+
5. Use xpathCheck() to inspect matched elements and pick the correct one
|
|
902
|
+
6. Use visualClick() to click the right element by visual appearance
|
|
903
|
+
`;
|
|
904
|
+
}
|
|
905
|
+
const MAX_DISAMBIGUATE_ELEMENTS = 10;
|
|
906
|
+
const MULTIPLE_ELEMENTS_PATTERN = 'multiple elements';
|
|
907
|
+
async function extractWebElements(error) {
|
|
908
|
+
if (!error || error.name !== 'MultipleElementsFound')
|
|
909
|
+
return null;
|
|
910
|
+
const elements = error.webElements;
|
|
911
|
+
if (!elements?.length)
|
|
912
|
+
return null;
|
|
913
|
+
const result = [];
|
|
914
|
+
for (let i = 0; i < Math.min(elements.length, MAX_DISAMBIGUATE_ELEMENTS); i++) {
|
|
915
|
+
try {
|
|
916
|
+
const xpath = await elements[i].toAbsoluteXPath();
|
|
917
|
+
const html = await elements[i].toSimplifiedHTML();
|
|
918
|
+
result.push({ xpath, html });
|
|
919
|
+
}
|
|
920
|
+
catch (e) {
|
|
921
|
+
debugLog('Failed to get details for element %d: %s', i, e);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return result.length > 0 ? result : null;
|
|
925
|
+
}
|
|
926
|
+
async function formatMatchedElements(error) {
|
|
927
|
+
const details = await extractWebElements(error);
|
|
928
|
+
if (!details)
|
|
929
|
+
return 'Could not fetch element details. Repeat the action to get better info.';
|
|
930
|
+
return details.map((el, i) => `Element ${i + 1}\nXPath: ${el.xpath}\nHTML: ${el.html}`).join('\n\n');
|
|
931
|
+
}
|
|
932
|
+
async function disambiguateElements(error, explanation, provider) {
|
|
933
|
+
const elementDetails = await extractWebElements(error);
|
|
934
|
+
if (!elementDetails)
|
|
935
|
+
return null;
|
|
936
|
+
const elementList = elementDetails.map((el, i) => `Element ${i + 1}:\nXPath: ${el.xpath}\nHTML: ${el.html}`).join('\n\n');
|
|
937
|
+
const schema = z.object({
|
|
938
|
+
position: z.number().nullable().describe('1-based position of the correct element, or null if none match'),
|
|
939
|
+
});
|
|
940
|
+
try {
|
|
941
|
+
const result = await provider.generateObject([
|
|
942
|
+
{
|
|
943
|
+
role: 'user',
|
|
944
|
+
content: dedent `
|
|
945
|
+
A click action failed because multiple elements matched the locator.
|
|
946
|
+
The intended action was: ${explanation}
|
|
947
|
+
|
|
948
|
+
Here are the matched elements:
|
|
949
|
+
|
|
950
|
+
${elementList}
|
|
951
|
+
|
|
952
|
+
Which element (1-${elementDetails.length}) best matches the intended action?
|
|
953
|
+
Return the position number, or null if none of them match.
|
|
954
|
+
`,
|
|
955
|
+
},
|
|
956
|
+
], schema, provider.getAgenticModel(), { agentName: 'disambiguator', timeout: 15000 });
|
|
957
|
+
const position = result?.object?.position;
|
|
958
|
+
if (position && position >= 1 && position <= elementDetails.length) {
|
|
959
|
+
return { position, xpath: elementDetails[position - 1].xpath };
|
|
960
|
+
}
|
|
961
|
+
return null;
|
|
962
|
+
}
|
|
963
|
+
catch (e) {
|
|
964
|
+
debugLog('Element disambiguation AI call failed: %s', e);
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function getNotFoundSuggestion(errorMessage) {
|
|
969
|
+
if (!errorMessage.includes('was not found') && !errorMessage.includes('not found by text|CSS|XPath')) {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
return dedent `
|
|
973
|
+
Element was not found. The locator does not exist on this page.
|
|
974
|
+
1. Use see() to visually analyze what elements are actually on the page
|
|
975
|
+
2. Use context() to get fresh HTML and ARIA snapshot
|
|
976
|
+
3. Use ONLY locators from <page_aria> or <page_html>
|
|
977
|
+
4. Prefer ARIA locators: { "role": "button", "text": "visible text" }
|
|
978
|
+
`;
|
|
979
|
+
}
|
|
980
|
+
//# sourceMappingURL=tools.js.map
|