explorbot 0.5.10 → 0.6.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/bin/explorbot-cli.ts +494 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/dist/bin/explorbot-cli.js +455 -444
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/models.json +1 -1
- package/dist/package.json +2 -1
- package/dist/rules/navigator/output.md +2 -8
- package/dist/rules/navigator/verification-actions.md +6 -2
- package/dist/rules/planner/ignored-pages.md +1 -0
- package/dist/src/action-result.js +1 -1
- package/dist/src/action.d.ts +0 -1
- package/dist/src/action.js +0 -4
- package/dist/src/ai/captain/idle-mode.js +42 -26
- package/dist/src/ai/captain/mixin.d.ts +2 -0
- package/dist/src/ai/captain/web-mode.js +11 -0
- package/dist/src/ai/captain.d.ts +6 -4
- package/dist/src/ai/captain.js +25 -1
- package/dist/src/ai/driller.js +1 -1
- package/dist/src/ai/experience-compactor.js +2 -2
- package/dist/src/ai/judge.d.ts +7 -0
- package/dist/src/ai/judge.js +8 -1
- package/dist/src/ai/navigator.d.ts +1 -0
- package/dist/src/ai/navigator.js +11 -7
- package/dist/src/ai/planner/subpages.js +1 -1
- package/dist/src/ai/planner.d.ts +12 -5
- package/dist/src/ai/planner.js +138 -86
- package/dist/src/ai/provider.d.ts +1 -1
- package/dist/src/ai/provider.js +16 -5
- package/dist/src/ai/rerunner.js +1 -1
- package/dist/src/ai/researcher/cache.js +14 -6
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -1
- package/dist/src/ai/researcher/deep-analysis.js +7 -2
- package/dist/src/ai/researcher/fingerprint-worker.js +4 -4
- package/dist/src/ai/researcher.js +44 -24
- package/dist/src/ai/rules.js +6 -7
- package/dist/src/ai/scout.d.ts +12 -2
- package/dist/src/ai/scout.js +103 -8
- package/dist/src/ai/tester.d.ts +9 -5
- package/dist/src/ai/tester.js +22 -42
- package/dist/src/ai/tools.js +10 -8
- package/dist/src/application-spec.d.ts +2 -0
- package/dist/src/application-spec.js +17 -12
- package/dist/src/commands/context-experience-command.js +0 -1
- package/dist/src/commands/context-knowledge-command.js +0 -1
- package/dist/src/commands/explore-command.d.ts +3 -2
- package/dist/src/commands/explore-command.js +35 -23
- package/dist/src/commands/help-json-command.js +3 -1
- package/dist/src/commands/init-command.js +4 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/plan-command.d.ts +3 -0
- package/dist/src/commands/plan-command.js +43 -17
- package/dist/src/commands/plan-save-command.js +6 -17
- package/dist/src/commands/plans-command.js +1 -1
- package/dist/src/commands/test-command.js +12 -2
- package/dist/src/components/App.d.ts +1 -2
- package/dist/src/components/App.js +1 -3
- package/dist/src/components/InputPane.js +0 -5
- package/dist/src/config.d.ts +3 -2
- package/dist/src/config.js +2 -12
- package/dist/src/explorbot.d.ts +3 -2
- package/dist/src/explorbot.js +32 -10
- package/dist/src/explorer.js +6 -7
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +18 -6
- package/dist/src/playwright-recorder.d.ts +0 -1
- package/dist/src/playwright-recorder.js +0 -2
- package/dist/src/reporter.d.ts +1 -1
- package/dist/src/reporter.js +3 -3
- package/dist/src/test-plan.js +1 -1
- package/dist/src/utils/error-page.d.ts +17 -0
- package/dist/src/utils/error-page.js +53 -2
- package/dist/src/utils/html-diff.d.ts +1 -2
- package/dist/src/utils/html-diff.js +4 -44
- package/dist/src/utils/logger.js +27 -10
- package/dist/src/utils/markdown-terminal.js +1 -2
- package/dist/src/utils/next-steps.js +20 -16
- package/dist/src/utils/page-readiness.d.ts +2 -1
- package/dist/src/utils/page-readiness.js +24 -5
- package/dist/src/utils/test-plan-markdown.d.ts +3 -0
- package/dist/src/utils/test-plan-markdown.js +35 -4
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +5 -0
- package/docs/basics/providers.md +1 -1
- package/docs/contributing/regression-tests.md +19 -4
- package/docs/reference/commands.md +6 -2
- package/docs/reference/configuration.md +1 -1
- package/docs/web-testing/agents.md +8 -0
- package/docs/web-testing/customization.md +2 -0
- package/docs/web-testing/researcher.md +1 -1
- package/models.json +1 -1
- package/package.json +2 -1
- package/rules/navigator/output.md +2 -8
- package/rules/navigator/verification-actions.md +6 -2
- package/rules/planner/ignored-pages.md +1 -0
- package/src/action-result.ts +1 -2
- package/src/action.ts +0 -5
- package/src/ai/captain/idle-mode.ts +38 -26
- package/src/ai/captain/mixin.ts +2 -1
- package/src/ai/captain/web-mode.ts +12 -0
- package/src/ai/captain.ts +25 -7
- package/src/ai/driller.ts +1 -2
- package/src/ai/experience-compactor.ts +2 -2
- package/src/ai/judge.ts +10 -1
- package/src/ai/navigator.ts +12 -7
- package/src/ai/planner/subpages.ts +1 -2
- package/src/ai/planner.ts +140 -92
- package/src/ai/provider.ts +15 -6
- package/src/ai/rerunner.ts +1 -2
- package/src/ai/researcher/cache.ts +16 -7
- package/src/ai/researcher/deep-analysis.ts +7 -2
- package/src/ai/researcher/fingerprint-worker.ts +5 -5
- package/src/ai/researcher.ts +44 -28
- package/src/ai/rules.ts +6 -7
- package/src/ai/scout.ts +98 -10
- package/src/ai/tester.ts +29 -47
- package/src/ai/tools.ts +11 -9
- package/src/application-spec.ts +19 -13
- package/src/commands/clean-command.ts +1 -1
- package/src/commands/context-command.ts +0 -1
- package/src/commands/context-experience-command.ts +0 -1
- package/src/commands/context-knowledge-command.ts +0 -1
- package/src/commands/explore-command.ts +36 -23
- package/src/commands/help-json-command.ts +8 -6
- package/src/commands/init-command.ts +5 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/plan-command.ts +44 -17
- package/src/commands/plan-save-command.ts +6 -17
- package/src/commands/plans-command.ts +1 -1
- package/src/commands/test-command.ts +11 -2
- package/src/components/App.tsx +2 -6
- package/src/components/InputPane.tsx +0 -5
- package/src/config.ts +4 -16
- package/src/experience-tracker.ts +1 -1
- package/src/explorbot.ts +31 -11
- package/src/explorer.ts +6 -7
- package/src/knowledge-tracker.ts +18 -7
- package/src/playwright-recorder.ts +0 -2
- package/src/reporter.ts +3 -4
- package/src/test-plan.ts +1 -2
- package/src/utils/error-page.ts +64 -2
- package/src/utils/html-diff.ts +4 -56
- package/src/utils/html.ts +0 -1
- package/src/utils/logger.ts +30 -12
- package/src/utils/markdown-terminal.ts +1 -2
- package/src/utils/next-steps.ts +19 -17
- package/src/utils/page-readiness.ts +33 -7
- package/src/utils/test-plan-markdown.ts +36 -4
- package/src/utils/url-matcher.ts +6 -0
|
@@ -25,7 +25,7 @@ export class Curler {
|
|
|
25
25
|
debugLog('Starting test:', test.scenario);
|
|
26
26
|
this.requestState.clear();
|
|
27
27
|
test.start();
|
|
28
|
-
await this.reporter.reportTestStart(
|
|
28
|
+
await this.reporter.reportTestStart();
|
|
29
29
|
const conversation = this.provider.startConversation(this.buildSystemPrompt(), 'curler', this.provider.getAgenticModel('curler'));
|
|
30
30
|
const tools = createCurlerTools(this.apiClient, this.requestState, test, opts?.searchSpec);
|
|
31
31
|
const knowledge = test.startUrl && this.knowledgeTracker?.renderEndpointKnowledge(test.startUrl);
|
|
@@ -18,7 +18,7 @@ export function createApiCommands(name = 'api') {
|
|
|
18
18
|
.option('--style <style>', 'Planning style: basename of a file in rules/chief/styles/')
|
|
19
19
|
.option('--fresh', 'Start planning from scratch')
|
|
20
20
|
.action(async (endpoint, options) => {
|
|
21
|
-
await run(
|
|
21
|
+
await run(options, endpoint, async (bot) => {
|
|
22
22
|
const command = new PlanCommand(bot);
|
|
23
23
|
command.prefix = name;
|
|
24
24
|
command.style = options.style;
|
|
@@ -47,7 +47,7 @@ export function createApiCommands(name = 'api') {
|
|
|
47
47
|
});
|
|
48
48
|
RecommendedModelsCommand.register(cmd);
|
|
49
49
|
addCommonOptions(cmd.command('test <planfile> [index]').description('Execute tests from a plan file. Index: 1, 1-3, *')).action(async (planfile, index, options) => {
|
|
50
|
-
await run(
|
|
50
|
+
await run(options, undefined, async (bot) => {
|
|
51
51
|
const command = new TestCommand(bot);
|
|
52
52
|
command.index = index;
|
|
53
53
|
await command.execute(planfile);
|
|
@@ -57,7 +57,7 @@ export function createApiCommands(name = 'api') {
|
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
addCommonOptions(cmd.command('explore <endpoint>').description('Full cycle: plan, execute tests, re-plan. Use * to cover many endpoints, or the base endpoint for all of them')).action(async (endpoint, options) => {
|
|
60
|
-
await run(
|
|
60
|
+
await run(options, endpoint, async (bot) => {
|
|
61
61
|
const command = new ExploreCommand(bot);
|
|
62
62
|
await command.execute(endpoint);
|
|
63
63
|
if (command.result.failed)
|
|
@@ -96,7 +96,7 @@ export function createApiCommands(name = 'api') {
|
|
|
96
96
|
});
|
|
97
97
|
return cmd;
|
|
98
98
|
}
|
|
99
|
-
async function run(
|
|
99
|
+
async function run(options, endpoint, body) {
|
|
100
100
|
setPreserveConsoleLogs(true);
|
|
101
101
|
try {
|
|
102
102
|
if (endpoint && URL.canParse(endpoint))
|
package/dist/models.json
CHANGED
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "explorbot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "CLI app built with React Ink, CodeceptJS, and Playwright",
|
|
5
5
|
"license": "Elastic-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -131,6 +131,7 @@
|
|
|
131
131
|
"sambanova-ai-provider": "^1.2.2",
|
|
132
132
|
"strip-ansi": "^7.1.2",
|
|
133
133
|
"turndown": "^7.2.1",
|
|
134
|
+
"wrap-ansi": "^9.0.2",
|
|
134
135
|
"unique-names-generator": "^4.7.1",
|
|
135
136
|
"yaml": "^2.8.3",
|
|
136
137
|
"yargs": "^17.7.2",
|
|
@@ -65,14 +65,8 @@ Trying to fill the form on the page
|
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
```js
|
|
68
|
-
I.fillField('
|
|
68
|
+
I.fillField('//html/body//form//input[@name="name"]', 'Value');
|
|
69
69
|
```
|
|
70
70
|
</example_output>
|
|
71
71
|
|
|
72
|
-
If
|
|
73
|
-
|
|
74
|
-
<example_output>
|
|
75
|
-
```js
|
|
76
|
-
throw new Error('No resolution');
|
|
77
|
-
```
|
|
78
|
-
</example_output>
|
|
72
|
+
If no solution is possible, call stop() with the reason instead of proposing code.
|
|
@@ -26,6 +26,10 @@ Prefer ARIA locators for reliable element detection.
|
|
|
26
26
|
I.seeElement('.success-message');
|
|
27
27
|
</example>
|
|
28
28
|
|
|
29
|
+
To assert text inside element via `has-text` CSS, prefer using I.see() instead:
|
|
30
|
+
|
|
31
|
+
Prefer `I.see('Collection', '.users')` over `I.seeElement(".users div:has-text("Collection")")`
|
|
32
|
+
|
|
29
33
|
### I.seeInField
|
|
30
34
|
|
|
31
35
|
I.seeInField(<locator>, <value>, <context>)
|
|
@@ -141,8 +145,8 @@ NEVER use `:has-text(...)` inside a seeElement/dontSeeElement locator. Checking
|
|
|
141
145
|
NEVER emit two assertions that check the same fact with different shapes. `I.see(text, locator)` and `I.seeElement("<locator>:has-text('text')")` verify the same thing — pick one (prefer I.see). One claim, one assertion.
|
|
142
146
|
</verification_rules>
|
|
143
147
|
|
|
144
|
-
[
|
|
148
|
+
[Prefer the commands listed here; use other CodeceptJS I.* commands when none of these fits]
|
|
145
149
|
[INTERACT ONLY WITH ELEMENTS THAT ARE ON THE PAGE HTML OR ARIA SNAPSHOT]
|
|
146
150
|
[DO NOT USE WAIT FUNCTIONS]
|
|
147
151
|
|
|
148
|
-
</actions>
|
|
152
|
+
</actions>
|
|
@@ -615,7 +615,7 @@ export class Diff {
|
|
|
615
615
|
async calculate() {
|
|
616
616
|
if (!this.previous)
|
|
617
617
|
return;
|
|
618
|
-
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html
|
|
618
|
+
this._htmlDiffResult = await htmlDiff(this.previous.html, this.current.html);
|
|
619
619
|
if (!this._isSameUrl) {
|
|
620
620
|
this._messages = liveRegionMessages(this.previous.html, this.current.html);
|
|
621
621
|
return;
|
package/dist/src/action.d.ts
CHANGED
package/dist/src/action.js
CHANGED
|
@@ -27,8 +27,6 @@ class Action {
|
|
|
27
27
|
stateManager;
|
|
28
28
|
actionResult = null;
|
|
29
29
|
config;
|
|
30
|
-
// action info
|
|
31
|
-
action = null;
|
|
32
30
|
lastError = null;
|
|
33
31
|
playwrightHelper;
|
|
34
32
|
playwrightGroupId = null;
|
|
@@ -366,7 +364,6 @@ class Action {
|
|
|
366
364
|
}
|
|
367
365
|
}
|
|
368
366
|
async executeOnce(code, verbatim = false) {
|
|
369
|
-
let error = null;
|
|
370
367
|
setActivity('🔎 Browsing...', 'action');
|
|
371
368
|
let codeString = code.replace(/^\(I\) => /, '').trim();
|
|
372
369
|
const executedSteps = [];
|
|
@@ -410,7 +407,6 @@ class Action {
|
|
|
410
407
|
}
|
|
411
408
|
catch (err) {
|
|
412
409
|
debugLog('Action error', errorToString(err));
|
|
413
|
-
error = err;
|
|
414
410
|
if (!isPlaywright) {
|
|
415
411
|
await recorder.reset();
|
|
416
412
|
await recorder.start();
|
|
@@ -3,7 +3,9 @@ import { createBashTool } from 'bash-tool';
|
|
|
3
3
|
import dedent from 'dedent';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { ConfigParser } from "../../config.js";
|
|
6
|
-
import { Test } from "../../test-plan.js";
|
|
6
|
+
import { Plan, Test } from "../../test-plan.js";
|
|
7
|
+
import { getStyles } from "../planner/styles.js";
|
|
8
|
+
import { PlanningBlockedError } from "../../utils/error-page.js";
|
|
7
9
|
import { listRecentArtifacts, readCaptainFile } from "./file-tools.js";
|
|
8
10
|
import { resolveProjectRoot } from "./mixin.js";
|
|
9
11
|
let cachedBashTool = null;
|
|
@@ -32,44 +34,58 @@ export function WithIdleMode(Base) {
|
|
|
32
34
|
});
|
|
33
35
|
}
|
|
34
36
|
const tools = {
|
|
35
|
-
|
|
36
|
-
description:
|
|
37
|
+
plan: tool({
|
|
38
|
+
description: dedent `
|
|
39
|
+
Plan tests for the current page. Adds them to the current plan when it is for this page, otherwise starts a new plan.
|
|
40
|
+
Without tests, the Planner writes the tests. With tests, exactly those tests are added and the Planner does not run: use it for tests the user dictates.
|
|
41
|
+
Returns how many tests were added, or why planning failed.
|
|
42
|
+
`,
|
|
37
43
|
inputSchema: z.object({
|
|
38
|
-
|
|
39
|
-
|
|
44
|
+
feature: z.string().optional().describe('Focus area for the Planner'),
|
|
45
|
+
style: z
|
|
46
|
+
.enum(Object.keys(getStyles()))
|
|
47
|
+
.optional()
|
|
48
|
+
.describe('Planning style for the Planner; omit to let it choose'),
|
|
40
49
|
tests: z
|
|
41
50
|
.array(z.object({
|
|
42
51
|
scenario: z.string(),
|
|
43
52
|
priority: z.enum(['critical', 'important', 'high', 'normal', 'low']).optional(),
|
|
44
53
|
expected: z.array(z.string()).optional(),
|
|
45
54
|
}))
|
|
46
|
-
.optional()
|
|
55
|
+
.optional()
|
|
56
|
+
.describe('Tests to add as written, without running the Planner'),
|
|
57
|
+
replace: z.boolean().optional().describe('Remove the tests already in this page plan first'),
|
|
58
|
+
title: z.string().optional().describe('New plan title'),
|
|
47
59
|
}),
|
|
48
|
-
execute: async ({
|
|
60
|
+
execute: async ({ feature, style, tests, replace, title }) => {
|
|
61
|
+
if (ctx.blocker instanceof PlanningBlockedError) {
|
|
62
|
+
feature = ctx.blocker.feature;
|
|
63
|
+
style = ctx.blocker.style;
|
|
64
|
+
}
|
|
65
|
+
const url = ctx.explorBot.stateManager().getCurrentState()?.url || '';
|
|
49
66
|
let plan = ctx.explorBot.getCurrentPlan();
|
|
50
|
-
if (
|
|
51
|
-
plan =
|
|
67
|
+
if (plan?.url && plan.url !== url)
|
|
68
|
+
plan = undefined;
|
|
69
|
+
if (replace)
|
|
70
|
+
plan?.tests.splice(0);
|
|
71
|
+
const before = new Set(plan?.tests ?? []);
|
|
72
|
+
if (!tests?.length) {
|
|
73
|
+
plan = await ctx.explorBot.plan(feature, { style });
|
|
74
|
+
const error = ctx.explorBot.lastPlanError;
|
|
75
|
+
if (error)
|
|
76
|
+
return { success: false, message: error.message };
|
|
52
77
|
}
|
|
53
78
|
if (!plan) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
if (title) {
|
|
57
|
-
plan.title = title;
|
|
79
|
+
plan = new Plan(url);
|
|
80
|
+
ctx.explorBot.setCurrentPlan(plan);
|
|
58
81
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
plan.tests.length = 0;
|
|
62
|
-
}
|
|
63
|
-
const currentUrl = ctx.explorBot.stateManager().getCurrentState()?.url || '';
|
|
64
|
-
for (const testInput of tests) {
|
|
65
|
-
const priority = testInput.priority || 'normal';
|
|
66
|
-
const expected = testInput.expected?.length ? testInput.expected : [];
|
|
67
|
-
const test = new Test(testInput.scenario, priority, expected, currentUrl);
|
|
68
|
-
plan.addTest(test);
|
|
69
|
-
}
|
|
82
|
+
for (const t of tests ?? []) {
|
|
83
|
+
plan.addTest(new Test(t.scenario, t.priority || 'normal', t.expected ?? [], url));
|
|
70
84
|
}
|
|
85
|
+
if (title)
|
|
86
|
+
plan.title = title;
|
|
71
87
|
plan.updateStatus();
|
|
72
|
-
return { success: true, tests: plan.tests.length };
|
|
88
|
+
return { success: true, testsAdded: plan.tests.filter((t) => !before.has(t)).length, tests: plan.tests.length };
|
|
73
89
|
},
|
|
74
90
|
}),
|
|
75
91
|
project: tool({
|
|
@@ -132,7 +148,7 @@ export function WithIdleMode(Base) {
|
|
|
132
148
|
const outputDir = config.dirs?.output || 'output';
|
|
133
149
|
return dedent `
|
|
134
150
|
<idle_capabilities>
|
|
135
|
-
- Plan management:
|
|
151
|
+
- Plan management: plan() — run the Planner on the current page, or add tests you write yourself
|
|
136
152
|
- readFile() — read specific report, plan, log, generated test, knowledge, or experience file content
|
|
137
153
|
- bash() — discover files and inspect file metadata
|
|
138
154
|
- READ from: ${knowledgeDir}/, ${experienceDir}/, ${outputDir}/
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { ExplorBot } from '../../explorbot.js';
|
|
2
2
|
import type { Task } from '../../test-plan.js';
|
|
3
|
+
import type { BlockedError } from '../../utils/error-page.js';
|
|
3
4
|
export type Constructor<T = object> = new (...args: any[]) => T;
|
|
4
5
|
export declare const debugLog: (...args: any[]) => void;
|
|
5
6
|
export type CaptainMode = 'idle' | 'web' | 'test' | 'heal';
|
|
6
7
|
export interface ModeContext {
|
|
7
8
|
explorBot: ExplorBot;
|
|
8
9
|
task: Task;
|
|
10
|
+
blocker: BlockedError | null;
|
|
9
11
|
}
|
|
10
12
|
export declare function resolveProjectRoot(): string | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { tool } from 'ai';
|
|
2
2
|
import dedent from 'dedent';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { pageEvidence } from "../judge.js";
|
|
4
5
|
import { actionRule, locatorRule, sectionContextRule } from "../rules.js";
|
|
5
6
|
import { createAgentTools, createCodeceptJSTools } from "../tools.js";
|
|
6
7
|
import { debugLog } from "./mixin.js";
|
|
@@ -35,6 +36,15 @@ export function WithWebMode(Base) {
|
|
|
35
36
|
}
|
|
36
37
|
},
|
|
37
38
|
}),
|
|
39
|
+
oracle: tool({
|
|
40
|
+
description: 'Ask a yes/no question about the current page. Much cheaper than see or context.',
|
|
41
|
+
inputSchema: z.object({ question: z.string() }),
|
|
42
|
+
execute: async ({ question }) => {
|
|
43
|
+
const captured = await ctx.explorBot.getExplorer().capture({ screenshot: true });
|
|
44
|
+
const holds = await ctx.explorBot.getProvider().oracle(question, pageEvidence(captured), { agentName: 'captain', screenshot: captured.screenshot, telemetry: { functionId: 'captain.oracle' } });
|
|
45
|
+
return { success: holds !== null, question, answer: holds };
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
38
48
|
browser: tool({
|
|
39
49
|
description: dedent `
|
|
40
50
|
Direct browser access via Playwright. Use for diagnostics and browser management.
|
|
@@ -146,6 +156,7 @@ export function WithWebMode(Base) {
|
|
|
146
156
|
- click() accepts array of commands to try in order — include ARIA, CSS, XPath variants
|
|
147
157
|
- If click() fails with all provided commands, use visualClick() tool as fallback
|
|
148
158
|
- When an action fails or result is unexpected, investigate or inform the user instead of retrying silently
|
|
159
|
+
- For a quick yes/no question about the page, use oracle instead of see or context
|
|
149
160
|
`;
|
|
150
161
|
}
|
|
151
162
|
};
|
package/dist/src/ai/captain.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ExperienceTracker } from '../experience-tracker.js';
|
|
|
2
2
|
import type { ExplorBot } from '../explorbot.js';
|
|
3
3
|
import type { WebPageState } from '../state-manager.js';
|
|
4
4
|
import { Task, Test } from '../test-plan.js';
|
|
5
|
+
import { BlockedError } from '../utils/error-page.js';
|
|
5
6
|
import { HooksRunner } from '../utils/hooks-runner.js';
|
|
6
7
|
import type { Agent } from './agent.js';
|
|
7
8
|
import { type CaptainMode, type ModeContext } from './captain/mixin.js';
|
|
@@ -9,6 +10,7 @@ import { type Conversation } from './conversation.js';
|
|
|
9
10
|
import type { Navigator } from './navigator.js';
|
|
10
11
|
import type { Provider } from './provider.js';
|
|
11
12
|
import { TaskAgent } from './task-agent.js';
|
|
13
|
+
import type { SupervisorAction } from './tester.js';
|
|
12
14
|
declare const CaptainBase: {
|
|
13
15
|
new (...args: any[]): {
|
|
14
16
|
testModeTools(ctx: ModeContext): Record<string, any>;
|
|
@@ -39,6 +41,9 @@ export declare class Captain extends CaptainBase implements Agent {
|
|
|
39
41
|
description: string;
|
|
40
42
|
options: string;
|
|
41
43
|
}[];
|
|
44
|
+
unresolvedBlockers: Set<string>;
|
|
45
|
+
handling: boolean;
|
|
46
|
+
blocker: BlockedError | null;
|
|
42
47
|
constructor(explorBot: ExplorBot);
|
|
43
48
|
setCommandExecutor(fn: (cmd: string) => Promise<void>, descriptions: {
|
|
44
49
|
name: string;
|
|
@@ -148,12 +153,9 @@ export declare class Captain extends CaptainBase implements Agent {
|
|
|
148
153
|
handle(input: string, options?: {
|
|
149
154
|
reset?: boolean;
|
|
150
155
|
}): Promise<string | null>;
|
|
156
|
+
resolveBlocker(error: unknown, resolved: () => boolean | Promise<boolean>): Promise<boolean>;
|
|
151
157
|
}
|
|
152
158
|
export default Captain;
|
|
153
|
-
interface SupervisorAction {
|
|
154
|
-
action: 'inject' | 'stop' | 'pass' | 'skip';
|
|
155
|
-
message: string;
|
|
156
|
-
}
|
|
157
159
|
interface ExecutionRecoveryAction {
|
|
158
160
|
action: 'continue' | 'stop';
|
|
159
161
|
message: string;
|
package/dist/src/ai/captain.js
CHANGED
|
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
|
|
4
4
|
import { ActionResult } from '../action-result.js';
|
|
5
5
|
import { Task } from "../test-plan.js";
|
|
6
6
|
import { formatHeadings } from "../utils/context-formatter.js";
|
|
7
|
+
import { BlockedError } from "../utils/error-page.js";
|
|
7
8
|
import { HooksRunner } from "../utils/hooks-runner.js";
|
|
8
9
|
import { startLogCapture, stopLogCapture, tag } from '../utils/logger.js';
|
|
9
10
|
import { loop } from '../utils/loop.js';
|
|
@@ -26,6 +27,9 @@ export class Captain extends CaptainBase {
|
|
|
26
27
|
hooksRunner = null;
|
|
27
28
|
commandExecutor = null;
|
|
28
29
|
commandDescriptions = [];
|
|
30
|
+
unresolvedBlockers = new Set();
|
|
31
|
+
handling = false;
|
|
32
|
+
blocker = null;
|
|
29
33
|
constructor(explorBot) {
|
|
30
34
|
super();
|
|
31
35
|
this.explorBot = explorBot;
|
|
@@ -106,6 +110,7 @@ export class Captain extends CaptainBase {
|
|
|
106
110
|
- After a successful action, if the pageDiff confirms the goal, call done() immediately — do not verify with see() or context() unless the user explicitly asked for verification
|
|
107
111
|
- Prefer completing in fewer tool calls over thoroughness
|
|
108
112
|
- NEVER run tests unless the user explicitly asks
|
|
113
|
+
- When an agent reports it is blocked, decide which agent can resolve it and delegate to that agent
|
|
109
114
|
- If you are answering with information rather than completing a browser action, include the actual user-facing answer in done({ details }). Do not only say that it was shown or explained.
|
|
110
115
|
${mode === 'web' || mode === 'heal' ? this.webModeRules() : ''}
|
|
111
116
|
${mode === 'test' || mode === 'heal' ? this.testModeRules() : ''}
|
|
@@ -281,7 +286,7 @@ export class Captain extends CaptainBase {
|
|
|
281
286
|
}
|
|
282
287
|
async tools(task, onDone) {
|
|
283
288
|
const mode = this.getMode();
|
|
284
|
-
const ctx = { explorBot: this.explorBot, task };
|
|
289
|
+
const ctx = { explorBot: this.explorBot, task, blocker: this.blocker };
|
|
285
290
|
const core = this.coreTools(task, onDone);
|
|
286
291
|
const idle = await this.idleModeTools(ctx);
|
|
287
292
|
if (mode === 'heal')
|
|
@@ -393,6 +398,7 @@ export class Captain extends CaptainBase {
|
|
|
393
398
|
Execute the request using available tools. After a successful action, check the pageDiff — if it confirms the goal, call done() immediately.
|
|
394
399
|
`;
|
|
395
400
|
conversation.addUserText(initialPrompt);
|
|
401
|
+
this.handling = true;
|
|
396
402
|
await loop(async ({ stop, iteration, userInput }) => {
|
|
397
403
|
debugLog(`Captain iteration ${iteration}`);
|
|
398
404
|
if (isDone) {
|
|
@@ -452,6 +458,8 @@ export class Captain extends CaptainBase {
|
|
|
452
458
|
tag('error').log(`Captain error: ${error}`);
|
|
453
459
|
stop();
|
|
454
460
|
},
|
|
461
|
+
}).finally(() => {
|
|
462
|
+
this.handling = false;
|
|
455
463
|
});
|
|
456
464
|
const finalUrl = stateManager.getCurrentState()?.url || startUrl;
|
|
457
465
|
await this.getHooksRunner().runAfterHook('captain', finalUrl);
|
|
@@ -463,6 +471,22 @@ export class Captain extends CaptainBase {
|
|
|
463
471
|
}
|
|
464
472
|
return null;
|
|
465
473
|
}
|
|
474
|
+
async resolveBlocker(error, resolved) {
|
|
475
|
+
if (!(error instanceof BlockedError) || this.handling)
|
|
476
|
+
return false;
|
|
477
|
+
const key = `${error.name} ${error.url} ${error.blocker}`;
|
|
478
|
+
if (this.unresolvedBlockers.has(key))
|
|
479
|
+
return false;
|
|
480
|
+
tag('info').log(`Captain takes over: ${error.message}`);
|
|
481
|
+
this.blocker = error;
|
|
482
|
+
await this.handle(error.request, { reset: true }).finally(() => {
|
|
483
|
+
this.blocker = null;
|
|
484
|
+
});
|
|
485
|
+
if (await resolved())
|
|
486
|
+
return true;
|
|
487
|
+
this.unresolvedBlockers.add(key);
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
466
490
|
}
|
|
467
491
|
export default Captain;
|
|
468
492
|
function isExplicitSlashRequest(input, command) {
|
package/dist/src/ai/driller.js
CHANGED
|
@@ -119,7 +119,7 @@ export class Driller extends TaskAgent {
|
|
|
119
119
|
setActivity(`${this.emoji} Capturing annotated page state...`, 'action');
|
|
120
120
|
const action = this.explorer.action();
|
|
121
121
|
try {
|
|
122
|
-
|
|
122
|
+
await Promise.race([
|
|
123
123
|
this.explorer.withPage(annotatePageElements),
|
|
124
124
|
new Promise((_, reject) => {
|
|
125
125
|
setTimeout(() => reject(new Error('annotateElements timeout')), 15000);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { unlinkSync } from 'node:fs';
|
|
2
|
-
import { basename } from 'node:path';
|
|
2
|
+
import { basename, relative } from 'node:path';
|
|
3
3
|
import dedent from 'dedent';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { RECENT_WINDOW_DAYS } from '../experience-tracker.js';
|
|
@@ -258,7 +258,7 @@ export class ExperienceCompactor {
|
|
|
258
258
|
mergedFrom: group.files.map((f) => f.data.url),
|
|
259
259
|
};
|
|
260
260
|
this.experienceTracker.writeExperienceFile(targetStateHash, combinedContent, newFrontmatter);
|
|
261
|
-
log(`Merged ${group.files.length} experience files into ${targetFile.filePath}`);
|
|
261
|
+
log(`Merged ${group.files.length} experience files into ${relative(process.cwd(), targetFile.filePath)}`);
|
|
262
262
|
debugLog('New URL pattern:', group.pattern);
|
|
263
263
|
for (const sourceFile of sourceFiles) {
|
|
264
264
|
try {
|
package/dist/src/ai/judge.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ActionResult } from '../action-result.js';
|
|
1
2
|
import type { AIConfig } from '../config.js';
|
|
2
3
|
import { JudgeProvider } from './judge-provider.js';
|
|
3
4
|
export declare const UNDECIDED = "undecided";
|
|
@@ -17,6 +18,7 @@ export declare class Judge {
|
|
|
17
18
|
direct: boolean;
|
|
18
19
|
};
|
|
19
20
|
threshold: number;
|
|
21
|
+
failureReported: boolean;
|
|
20
22
|
constructor(provider: JudgeProvider, enabled: {
|
|
21
23
|
tool: boolean;
|
|
22
24
|
direct: boolean;
|
|
@@ -30,3 +32,8 @@ export declare class Judge {
|
|
|
30
32
|
request(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>;
|
|
31
33
|
recordFailure(error: unknown): null;
|
|
32
34
|
}
|
|
35
|
+
export declare function pageEvidence(actionResult: ActionResult): {
|
|
36
|
+
url: string;
|
|
37
|
+
title: string;
|
|
38
|
+
page: string;
|
|
39
|
+
};
|
package/dist/src/ai/judge.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { clearActivity, setActivity } from "../activity.js";
|
|
2
2
|
import { Observability } from "../observability.js";
|
|
3
|
-
import { createDebug } from "../utils/logger.js";
|
|
3
|
+
import { createDebug, tag } from "../utils/logger.js";
|
|
4
4
|
import { JudgeProvider } from "./judge-provider.js";
|
|
5
5
|
const debugLog = createDebug('explorbot:judge');
|
|
6
6
|
const APPROVAL_THRESHOLD = 0.7;
|
|
@@ -27,6 +27,7 @@ export class Judge {
|
|
|
27
27
|
provider;
|
|
28
28
|
enabled;
|
|
29
29
|
threshold;
|
|
30
|
+
failureReported = false;
|
|
30
31
|
constructor(provider, enabled, threshold = APPROVAL_THRESHOLD) {
|
|
31
32
|
this.provider = provider;
|
|
32
33
|
this.enabled = enabled;
|
|
@@ -95,6 +96,9 @@ export class Judge {
|
|
|
95
96
|
}
|
|
96
97
|
recordFailure(error) {
|
|
97
98
|
debugLog('judge declined: %s', error);
|
|
99
|
+
if (!this.failureReported)
|
|
100
|
+
tag('warning').log(`Decision model ${this.provider.model} failed (${error}); continuing without it`);
|
|
101
|
+
this.failureReported = true;
|
|
98
102
|
const span = Observability.getSpan();
|
|
99
103
|
span?.setAttribute('ai.telemetry.metadata.judgeError', String(error));
|
|
100
104
|
span?.setAttribute('langfuse.observation.level', 'ERROR');
|
|
@@ -102,3 +106,6 @@ export class Judge {
|
|
|
102
106
|
return null;
|
|
103
107
|
}
|
|
104
108
|
}
|
|
109
|
+
export function pageEvidence(actionResult) {
|
|
110
|
+
return { url: actionResult.url, title: actionResult.title, page: actionResult.getCompactARIA().slice(0, JUDGE_PAGE_CAP) };
|
|
111
|
+
}
|
|
@@ -36,6 +36,7 @@ declare class Navigator implements Agent {
|
|
|
36
36
|
}, expectedUrl: string): string;
|
|
37
37
|
isSameExpectedOrigin(expectedUrl: string, stateManager: any): boolean;
|
|
38
38
|
isOnExpectedPage(expectedUrl: string, stateManager: any): boolean;
|
|
39
|
+
isOn(url: string): boolean;
|
|
39
40
|
visit(url: string, opts?: {
|
|
40
41
|
recover?: boolean;
|
|
41
42
|
}): Promise<boolean>;
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -7,7 +7,7 @@ import { normalizeUrl } from '../state-manager.js';
|
|
|
7
7
|
import { isFatalBrowserError } from "../utils/browser-errors.js";
|
|
8
8
|
import { getCliName } from "../utils/cli-name.js";
|
|
9
9
|
import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
10
|
-
import { isLoginPage } from "../utils/error-page.js";
|
|
10
|
+
import { NavigationBlockedError, isLoginPage } from "../utils/error-page.js";
|
|
11
11
|
import { HooksRunner } from "../utils/hooks-runner.js";
|
|
12
12
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
13
13
|
import { loop, pause } from '../utils/loop.js';
|
|
@@ -20,6 +20,7 @@ import { actionRule, credentialsRule, locatorRule, unexpectedPopupRule } from '.
|
|
|
20
20
|
import { isInteractive } from './task-agent.js';
|
|
21
21
|
import { createLearnExperienceTool } from "./tools.js";
|
|
22
22
|
const debugLog = createDebug('explorbot:navigator');
|
|
23
|
+
const MAX_TRIED_STEPS = 5;
|
|
23
24
|
class Navigator {
|
|
24
25
|
emoji = '🧭';
|
|
25
26
|
provider;
|
|
@@ -120,6 +121,9 @@ class Navigator {
|
|
|
120
121
|
const currentUrl = this.getComparableCurrentUrl(stateManager, expectedUrl);
|
|
121
122
|
return matchesNavigationUrl(expectedUrl, currentUrl);
|
|
122
123
|
}
|
|
124
|
+
isOn(url) {
|
|
125
|
+
return this.isOnExpectedPage(url, this.stateManager);
|
|
126
|
+
}
|
|
123
127
|
async visit(url, opts = {}) {
|
|
124
128
|
return this.visitOnce(url, opts.recover !== false);
|
|
125
129
|
}
|
|
@@ -171,9 +175,9 @@ class Navigator {
|
|
|
171
175
|
}
|
|
172
176
|
}
|
|
173
177
|
navigationError(url, fallback) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
return new
|
|
178
|
+
const currentUrl = this.stateManager.getCurrentState()?.url || '';
|
|
179
|
+
const tried = this.executedSteps.slice(-MAX_TRIED_STEPS).map((step) => step.command);
|
|
180
|
+
return new NavigationBlockedError(url, currentUrl, this.lastFailureReason || fallback, tried);
|
|
177
181
|
}
|
|
178
182
|
failureReason(stopReason, knowledge, url) {
|
|
179
183
|
const reasons = [];
|
|
@@ -364,7 +368,7 @@ class Navigator {
|
|
|
364
368
|
Propose different solutions to achieve the result.
|
|
365
369
|
Solution should be valid CodeceptJS code.
|
|
366
370
|
Use only data from the <page> context to plan the solution.
|
|
367
|
-
The page is given as an ARIA snapshot only: use ARIA and text locators from it.
|
|
371
|
+
The page is given as an ARIA snapshot only: use ARIA and text locators from it; propose CSS/XPath only once <page_html> is provided.
|
|
368
372
|
Full HTML is provided if these solutions fail.
|
|
369
373
|
Try various ways to achieve the result
|
|
370
374
|
</task>
|
|
@@ -692,7 +696,7 @@ class Navigator {
|
|
|
692
696
|
Do not generate assertions that would pass even if the specific claim is false.
|
|
693
697
|
</task>
|
|
694
698
|
|
|
695
|
-
${RulesLoader.loadRules('navigator', ['verification-actions'], actionResult.url || '')}
|
|
699
|
+
${RulesLoader.loadRules('navigator', ['verification-actions', 'verification-output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.verifyAttempts))}
|
|
696
700
|
|
|
697
701
|
${experience}
|
|
698
702
|
|
|
@@ -765,7 +769,7 @@ class Navigator {
|
|
|
765
769
|
const totalAttempted = Math.min(codeBlocks.length, this.verifyAttempts);
|
|
766
770
|
let verified = successfulCodes.length > 0 && !failures;
|
|
767
771
|
if (successfulCodes.length && failures)
|
|
768
|
-
verified = await this.provider.oracle(message, { assertions: results.map(({ code, passed, error }) => ({ code, passed, error })) }, { agentName: 'navigator', telemetry: { functionId: 'navigator.verifyState' } });
|
|
772
|
+
verified = (await this.provider.oracle(message, { assertions: results.map(({ code, passed, error }) => ({ code, passed, error })) }, { agentName: 'navigator', telemetry: { functionId: 'navigator.verifyState' } })) === true;
|
|
769
773
|
if (alreadyVerified)
|
|
770
774
|
verified = true;
|
|
771
775
|
const inexpressible = !alreadyVerified && totalAttempted === 0;
|
|
@@ -68,7 +68,7 @@ export function WithSubPages(Base) {
|
|
|
68
68
|
async pickNextSubPage(candidates) {
|
|
69
69
|
if (candidates.length === 0)
|
|
70
70
|
return null;
|
|
71
|
-
const plannedEntries = [...planRegistry.entries()].map(([
|
|
71
|
+
const plannedEntries = [...planRegistry.entries()].map(([, record]) => `- ${record.url} (${record.plan.tests.length} tests${record.feature ? `, feature: ${record.feature}` : ''})`).join('\n');
|
|
72
72
|
const candidateList = candidates.map((c) => `- ${c.url} (visits: ${c.visitCount}${c.title ? `, title: "${c.title}"` : ''}${c.h1 ? `, h1: "${c.h1}"` : ''})`).join('\n');
|
|
73
73
|
const conversation = this.provider.startConversation(dedent `
|
|
74
74
|
You pick the next most business-relevant sub-page to test from a list of candidates.
|