explorbot 0.5.9 → 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/README.md +1 -1
- package/bin/explorbot-cli.ts +495 -482
- package/boat/api-tester/src/ai/curler.ts +1 -1
- package/boat/api-tester/src/cli.ts +4 -4
- package/boat/api-tester/src/commands/init-command.ts +2 -0
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -1
- package/boat/doc-collector/src/screenshots.ts +1 -1
- package/boat/prima/bin/prima-cli.ts +2 -1
- package/dist/bin/explorbot-cli.js +456 -444
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -1
- package/dist/boat/api-tester/src/ai/curler.js +1 -1
- package/dist/boat/api-tester/src/cli.js +4 -4
- package/dist/boat/api-tester/src/commands/init-command.js +2 -0
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -1
- package/dist/boat/prima/bin/prima-cli.js +2 -1
- 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 +8 -4
- package/dist/src/commands/learn-command.js +0 -2
- package/dist/src/commands/options/auth-option.d.ts +6 -0
- package/dist/src/commands/options/auth-option.js +14 -0
- package/dist/src/commands/options/index.d.ts +2 -0
- package/dist/src/commands/options/index.js +2 -0
- 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 +3 -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 +18 -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/docs/workflow/agentic-usage.md +7 -0
- package/docs/workflow/knowledge.md +6 -0
- 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 +10 -3
- package/src/commands/learn-command.ts +0 -2
- package/src/commands/options/auth-option.ts +15 -0
- package/src/commands/options/index.ts +2 -0
- 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 +5 -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
package/src/action.ts
CHANGED
|
@@ -34,8 +34,6 @@ class Action {
|
|
|
34
34
|
public actionResult: ActionResult | null = null;
|
|
35
35
|
private config: ExplorbotConfig;
|
|
36
36
|
|
|
37
|
-
// action info
|
|
38
|
-
private action: string | null = null;
|
|
39
37
|
public lastError: Error | null = null;
|
|
40
38
|
public playwrightHelper: any;
|
|
41
39
|
public playwrightGroupId: string | null = null;
|
|
@@ -389,8 +387,6 @@ class Action {
|
|
|
389
387
|
}
|
|
390
388
|
|
|
391
389
|
private async executeOnce(code: string, verbatim = false): Promise<Action> {
|
|
392
|
-
let error: Error | null = null;
|
|
393
|
-
|
|
394
390
|
setActivity('🔎 Browsing...', 'action');
|
|
395
391
|
|
|
396
392
|
let codeString = code.replace(/^\(I\) => /, '').trim();
|
|
@@ -441,7 +437,6 @@ class Action {
|
|
|
441
437
|
this.assertionSteps = assertionSteps;
|
|
442
438
|
} catch (err) {
|
|
443
439
|
debugLog('Action error', errorToString(err));
|
|
444
|
-
error = err as Error;
|
|
445
440
|
if (!isPlaywright) {
|
|
446
441
|
await recorder.reset();
|
|
447
442
|
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.ts';
|
|
6
|
-
import { Test } from '../../test-plan.ts';
|
|
6
|
+
import { Plan, Test } from '../../test-plan.ts';
|
|
7
|
+
import { getStyles } from '../planner/styles.ts';
|
|
8
|
+
import { PlanningBlockedError } from '../../utils/error-page.ts';
|
|
7
9
|
import { listRecentArtifacts, readCaptainFile } from './file-tools.ts';
|
|
8
10
|
import { type Constructor, type ModeContext, resolveProjectRoot } from './mixin.ts';
|
|
9
11
|
|
|
@@ -36,11 +38,18 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
const tools: Record<string, any> = {
|
|
39
|
-
|
|
40
|
-
description:
|
|
41
|
+
plan: tool({
|
|
42
|
+
description: dedent`
|
|
43
|
+
Plan tests for the current page. Adds them to the current plan when it is for this page, otherwise starts a new plan.
|
|
44
|
+
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.
|
|
45
|
+
Returns how many tests were added, or why planning failed.
|
|
46
|
+
`,
|
|
41
47
|
inputSchema: z.object({
|
|
42
|
-
|
|
43
|
-
|
|
48
|
+
feature: z.string().optional().describe('Focus area for the Planner'),
|
|
49
|
+
style: z
|
|
50
|
+
.enum(Object.keys(getStyles()) as [string, ...string[]])
|
|
51
|
+
.optional()
|
|
52
|
+
.describe('Planning style for the Planner; omit to let it choose'),
|
|
44
53
|
tests: z
|
|
45
54
|
.array(
|
|
46
55
|
z.object({
|
|
@@ -49,33 +58,36 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
|
|
|
49
58
|
expected: z.array(z.string()).optional(),
|
|
50
59
|
})
|
|
51
60
|
)
|
|
52
|
-
.optional()
|
|
61
|
+
.optional()
|
|
62
|
+
.describe('Tests to add as written, without running the Planner'),
|
|
63
|
+
replace: z.boolean().optional().describe('Remove the tests already in this page plan first'),
|
|
64
|
+
title: z.string().optional().describe('New plan title'),
|
|
53
65
|
}),
|
|
54
|
-
execute: async ({
|
|
66
|
+
execute: async ({ feature, style, tests, replace, title }) => {
|
|
67
|
+
if (ctx.blocker instanceof PlanningBlockedError) {
|
|
68
|
+
feature = ctx.blocker.feature;
|
|
69
|
+
style = ctx.blocker.style;
|
|
70
|
+
}
|
|
71
|
+
const url = ctx.explorBot.stateManager().getCurrentState()?.url || '';
|
|
55
72
|
let plan = ctx.explorBot.getCurrentPlan();
|
|
56
|
-
if (
|
|
57
|
-
|
|
73
|
+
if (plan?.url && plan.url !== url) plan = undefined;
|
|
74
|
+
if (replace) plan?.tests.splice(0);
|
|
75
|
+
const before = new Set(plan?.tests ?? []);
|
|
76
|
+
if (!tests?.length) {
|
|
77
|
+
plan = await ctx.explorBot.plan(feature, { style });
|
|
78
|
+
const error = ctx.explorBot.lastPlanError;
|
|
79
|
+
if (error) return { success: false, message: error.message };
|
|
58
80
|
}
|
|
59
81
|
if (!plan) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
if (title) {
|
|
63
|
-
plan.title = title;
|
|
82
|
+
plan = new Plan(url);
|
|
83
|
+
ctx.explorBot.setCurrentPlan(plan);
|
|
64
84
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
plan.tests.length = 0;
|
|
68
|
-
}
|
|
69
|
-
const currentUrl = ctx.explorBot.stateManager().getCurrentState()?.url || '';
|
|
70
|
-
for (const testInput of tests) {
|
|
71
|
-
const priority = testInput.priority || 'normal';
|
|
72
|
-
const expected = testInput.expected?.length ? testInput.expected : [];
|
|
73
|
-
const test = new Test(testInput.scenario, priority, expected, currentUrl);
|
|
74
|
-
plan.addTest(test);
|
|
75
|
-
}
|
|
85
|
+
for (const t of tests ?? []) {
|
|
86
|
+
plan.addTest(new Test(t.scenario, t.priority || 'normal', t.expected ?? [], url));
|
|
76
87
|
}
|
|
88
|
+
if (title) plan.title = title;
|
|
77
89
|
plan.updateStatus();
|
|
78
|
-
return { success: true, tests: plan.tests.length };
|
|
90
|
+
return { success: true, testsAdded: plan.tests.filter((t) => !before.has(t)).length, tests: plan.tests.length };
|
|
79
91
|
},
|
|
80
92
|
}),
|
|
81
93
|
project: tool({
|
|
@@ -144,7 +156,7 @@ export function WithIdleMode<T extends Constructor>(Base: T) {
|
|
|
144
156
|
|
|
145
157
|
return dedent`
|
|
146
158
|
<idle_capabilities>
|
|
147
|
-
- Plan management:
|
|
159
|
+
- Plan management: plan() — run the Planner on the current page, or add tests you write yourself
|
|
148
160
|
- readFile() — read specific report, plan, log, generated test, knowledge, or experience file content
|
|
149
161
|
- bash() — discover files and inspect file metadata
|
|
150
162
|
- READ from: ${knowledgeDir}/, ${experienceDir}/, ${outputDir}/
|
package/src/ai/captain/mixin.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { dirname } from 'node:path';
|
|
2
1
|
import { ConfigParser } from '../../config.ts';
|
|
3
2
|
import type { ExplorBot } from '../../explorbot.ts';
|
|
4
3
|
import type { Task } from '../../test-plan.ts';
|
|
4
|
+
import type { BlockedError } from '../../utils/error-page.ts';
|
|
5
5
|
import { createDebug } from '../../utils/logger.js';
|
|
6
6
|
|
|
7
7
|
export type Constructor<T = object> = new (...args: any[]) => T;
|
|
@@ -13,6 +13,7 @@ export type CaptainMode = 'idle' | 'web' | 'test' | 'heal';
|
|
|
13
13
|
export interface ModeContext {
|
|
14
14
|
explorBot: ExplorBot;
|
|
15
15
|
task: Task;
|
|
16
|
+
blocker: BlockedError | null;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
19
|
export 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.ts';
|
|
4
5
|
import { actionRule, locatorRule, sectionContextRule } from '../rules.ts';
|
|
5
6
|
import { createAgentTools, createCodeceptJSTools } from '../tools.ts';
|
|
6
7
|
import { type Constructor, type ModeContext, debugLog } from './mixin.ts';
|
|
@@ -37,6 +38,16 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
37
38
|
},
|
|
38
39
|
}),
|
|
39
40
|
|
|
41
|
+
oracle: tool({
|
|
42
|
+
description: 'Ask a yes/no question about the current page. Much cheaper than see or context.',
|
|
43
|
+
inputSchema: z.object({ question: z.string() }),
|
|
44
|
+
execute: async ({ question }) => {
|
|
45
|
+
const captured = await ctx.explorBot.getExplorer().capture({ screenshot: true });
|
|
46
|
+
const holds = await ctx.explorBot.getProvider().oracle(question, pageEvidence(captured), { agentName: 'captain', screenshot: captured.screenshot, telemetry: { functionId: 'captain.oracle' } });
|
|
47
|
+
return { success: holds !== null, question, answer: holds };
|
|
48
|
+
},
|
|
49
|
+
}),
|
|
50
|
+
|
|
40
51
|
browser: tool({
|
|
41
52
|
description: dedent`
|
|
42
53
|
Direct browser access via Playwright. Use for diagnostics and browser management.
|
|
@@ -157,6 +168,7 @@ export function WithWebMode<T extends Constructor>(Base: T) {
|
|
|
157
168
|
- click() accepts array of commands to try in order — include ARIA, CSS, XPath variants
|
|
158
169
|
- If click() fails with all provided commands, use visualClick() tool as fallback
|
|
159
170
|
- When an action fails or result is unexpected, investigate or inform the user instead of retrying silently
|
|
171
|
+
- For a quick yes/no question about the page, use oracle instead of see or context
|
|
160
172
|
`;
|
|
161
173
|
}
|
|
162
174
|
};
|
package/src/ai/captain.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { ExplorBot } from '../explorbot.ts';
|
|
|
7
7
|
import type { WebPageState } from '../state-manager.ts';
|
|
8
8
|
import { Task, Test } from '../test-plan.ts';
|
|
9
9
|
import { formatHeadings } from '../utils/context-formatter.ts';
|
|
10
|
+
import { BlockedError } from '../utils/error-page.ts';
|
|
10
11
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
11
12
|
import { startLogCapture, stopLogCapture, tag } from '../utils/logger.js';
|
|
12
13
|
import { loop } from '../utils/loop.js';
|
|
@@ -21,6 +22,7 @@ import type { Navigator } from './navigator.ts';
|
|
|
21
22
|
import type { Provider } from './provider.ts';
|
|
22
23
|
import { Researcher } from './researcher.ts';
|
|
23
24
|
import { TaskAgent } from './task-agent.ts';
|
|
25
|
+
import type { SupervisorAction } from './tester.ts';
|
|
24
26
|
import { createLearnExperienceTool, withdrawVisionTools } from './tools.ts';
|
|
25
27
|
|
|
26
28
|
const MAX_STEPS = 15;
|
|
@@ -35,6 +37,9 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
35
37
|
private hooksRunner: HooksRunner | null = null;
|
|
36
38
|
private commandExecutor: ((cmd: string) => Promise<void>) | null = null;
|
|
37
39
|
private commandDescriptions: { name: string; description: string; options: string }[] = [];
|
|
40
|
+
private unresolvedBlockers = new Set<string>();
|
|
41
|
+
private handling = false;
|
|
42
|
+
private blocker: BlockedError | null = null;
|
|
38
43
|
|
|
39
44
|
constructor(explorBot: ExplorBot) {
|
|
40
45
|
super();
|
|
@@ -123,6 +128,7 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
123
128
|
- 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
|
|
124
129
|
- Prefer completing in fewer tool calls over thoroughness
|
|
125
130
|
- NEVER run tests unless the user explicitly asks
|
|
131
|
+
- When an agent reports it is blocked, decide which agent can resolve it and delegate to that agent
|
|
126
132
|
- 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.
|
|
127
133
|
${mode === 'web' || mode === 'heal' ? this.webModeRules() : ''}
|
|
128
134
|
${mode === 'test' || mode === 'heal' ? this.testModeRules() : ''}
|
|
@@ -304,7 +310,7 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
304
310
|
|
|
305
311
|
private async tools(task: Task, onDone: (summary: string) => void) {
|
|
306
312
|
const mode = this.getMode();
|
|
307
|
-
const ctx: ModeContext = { explorBot: this.explorBot, task };
|
|
313
|
+
const ctx: ModeContext = { explorBot: this.explorBot, task, blocker: this.blocker };
|
|
308
314
|
const core = this.coreTools(task, onDone);
|
|
309
315
|
const idle = await this.idleModeTools(ctx);
|
|
310
316
|
|
|
@@ -437,6 +443,7 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
437
443
|
|
|
438
444
|
conversation.addUserText(initialPrompt);
|
|
439
445
|
|
|
446
|
+
this.handling = true;
|
|
440
447
|
await loop(
|
|
441
448
|
async ({ stop, iteration, userInput }) => {
|
|
442
449
|
debugLog(`Captain iteration ${iteration}`);
|
|
@@ -510,7 +517,9 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
510
517
|
stop();
|
|
511
518
|
},
|
|
512
519
|
}
|
|
513
|
-
)
|
|
520
|
+
).finally(() => {
|
|
521
|
+
this.handling = false;
|
|
522
|
+
});
|
|
514
523
|
|
|
515
524
|
const finalUrl = stateManager.getCurrentState()?.url || startUrl;
|
|
516
525
|
await this.getHooksRunner().runAfterHook('captain', finalUrl);
|
|
@@ -523,15 +532,24 @@ export class Captain extends CaptainBase implements Agent {
|
|
|
523
532
|
|
|
524
533
|
return null;
|
|
525
534
|
}
|
|
535
|
+
|
|
536
|
+
async resolveBlocker(error: unknown, resolved: () => boolean | Promise<boolean>): Promise<boolean> {
|
|
537
|
+
if (!(error instanceof BlockedError) || this.handling) return false;
|
|
538
|
+
const key = `${error.name} ${error.url} ${error.blocker}`;
|
|
539
|
+
if (this.unresolvedBlockers.has(key)) return false;
|
|
540
|
+
tag('info').log(`Captain takes over: ${error.message}`);
|
|
541
|
+
this.blocker = error;
|
|
542
|
+
await this.handle(error.request, { reset: true }).finally(() => {
|
|
543
|
+
this.blocker = null;
|
|
544
|
+
});
|
|
545
|
+
if (await resolved()) return true;
|
|
546
|
+
this.unresolvedBlockers.add(key);
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
526
549
|
}
|
|
527
550
|
|
|
528
551
|
export default Captain;
|
|
529
552
|
|
|
530
|
-
interface SupervisorAction {
|
|
531
|
-
action: 'inject' | 'stop' | 'pass' | 'skip';
|
|
532
|
-
message: string;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
553
|
interface ExecutionRecoveryAction {
|
|
536
554
|
action: 'continue' | 'stop';
|
|
537
555
|
message: string;
|
package/src/ai/driller.ts
CHANGED
|
@@ -15,7 +15,6 @@ import { eidxInContainer } from '../utils/web-eidx.ts';
|
|
|
15
15
|
import { WebElement } from '../utils/web-element.ts';
|
|
16
16
|
import type { Agent, AgentDeps } from './agent.ts';
|
|
17
17
|
import type { Navigator } from './navigator.ts';
|
|
18
|
-
import type { Provider } from './provider.ts';
|
|
19
18
|
import { drillLocatorRule } from './rules.ts';
|
|
20
19
|
import { TaskAgent, isInteractive } from './task-agent.ts';
|
|
21
20
|
import { createCodeceptJSTools, createLearnExperienceTool } from './tools.ts';
|
|
@@ -177,7 +176,7 @@ export class Driller extends TaskAgent implements Agent {
|
|
|
177
176
|
setActivity(`${this.emoji} Capturing annotated page state...`, 'action');
|
|
178
177
|
const action = this.explorer.action();
|
|
179
178
|
try {
|
|
180
|
-
|
|
179
|
+
await Promise.race([
|
|
181
180
|
this.explorer.withPage(annotatePageElements),
|
|
182
181
|
new Promise<never>((_, reject) => {
|
|
183
182
|
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 { type ExperienceFile, type ExperienceTracker, RECENT_WINDOW_DAYS } from '../experience-tracker.js';
|
|
@@ -322,7 +322,7 @@ export class ExperienceCompactor implements Agent {
|
|
|
322
322
|
};
|
|
323
323
|
|
|
324
324
|
this.experienceTracker.writeExperienceFile(targetStateHash, combinedContent, newFrontmatter);
|
|
325
|
-
log(`Merged ${group.files.length} experience files into ${targetFile.filePath}`);
|
|
325
|
+
log(`Merged ${group.files.length} experience files into ${relative(process.cwd(), targetFile.filePath)}`);
|
|
326
326
|
debugLog('New URL pattern:', group.pattern);
|
|
327
327
|
|
|
328
328
|
for (const sourceFile of sourceFiles) {
|
package/src/ai/judge.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { clearActivity, setActivity } from '../activity.ts';
|
|
2
|
+
import type { ActionResult } from '../action-result.ts';
|
|
2
3
|
import type { AIConfig } from '../config.ts';
|
|
3
4
|
import { Observability } from '../observability.ts';
|
|
4
|
-
import { createDebug } from '../utils/logger.ts';
|
|
5
|
+
import { createDebug, tag } from '../utils/logger.ts';
|
|
5
6
|
import { JudgeProvider } from './judge-provider.ts';
|
|
6
7
|
|
|
7
8
|
const debugLog = createDebug('explorbot:judge');
|
|
@@ -29,6 +30,8 @@ export class Decision {
|
|
|
29
30
|
}
|
|
30
31
|
|
|
31
32
|
export class Judge {
|
|
33
|
+
private failureReported = false;
|
|
34
|
+
|
|
32
35
|
constructor(
|
|
33
36
|
private provider: JudgeProvider,
|
|
34
37
|
private enabled: { tool: boolean; direct: boolean },
|
|
@@ -93,6 +96,8 @@ export class Judge {
|
|
|
93
96
|
|
|
94
97
|
private recordFailure(error: unknown): null {
|
|
95
98
|
debugLog('judge declined: %s', error);
|
|
99
|
+
if (!this.failureReported) tag('warning').log(`Decision model ${this.provider.model} failed (${error}); continuing without it`);
|
|
100
|
+
this.failureReported = true;
|
|
96
101
|
const span = Observability.getSpan();
|
|
97
102
|
span?.setAttribute('ai.telemetry.metadata.judgeError', String(error));
|
|
98
103
|
span?.setAttribute('langfuse.observation.level', 'ERROR');
|
|
@@ -100,3 +105,7 @@ export class Judge {
|
|
|
100
105
|
return null;
|
|
101
106
|
}
|
|
102
107
|
}
|
|
108
|
+
|
|
109
|
+
export function pageEvidence(actionResult: ActionResult) {
|
|
110
|
+
return { url: actionResult.url, title: actionResult.title, page: actionResult.getCompactARIA().slice(0, JUDGE_PAGE_CAP) };
|
|
111
|
+
}
|
package/src/ai/navigator.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { type StateManager, normalizeUrl } from '../state-manager.js';
|
|
|
13
13
|
import { isFatalBrowserError } from '../utils/browser-errors.ts';
|
|
14
14
|
import { getCliName } from '../utils/cli-name.ts';
|
|
15
15
|
import { extractCodeBlocks } from '../utils/code-extractor.js';
|
|
16
|
-
import { isLoginPage } from '../utils/error-page.ts';
|
|
16
|
+
import { NavigationBlockedError, isLoginPage } from '../utils/error-page.ts';
|
|
17
17
|
import { HooksRunner } from '../utils/hooks-runner.ts';
|
|
18
18
|
import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
19
19
|
import { loop, pause } from '../utils/loop.js';
|
|
@@ -22,7 +22,6 @@ import { normalizeInlineText } from '../utils/strings.ts';
|
|
|
22
22
|
import { extractStatePath, isSameHostFamily, matchesNavigationUrl } from '../utils/url-matcher.js';
|
|
23
23
|
import type { UrlFilter } from '../utils/url-filter.ts';
|
|
24
24
|
import type { Agent, AgentDeps } from './agent.js';
|
|
25
|
-
import type { Conversation } from './conversation.js';
|
|
26
25
|
import { type Decision, JUDGE_PAGE_CAP, type Judge, UNDECIDED } from './judge.ts';
|
|
27
26
|
import type { Provider } from './provider.js';
|
|
28
27
|
import { Researcher } from './researcher.ts';
|
|
@@ -31,6 +30,7 @@ import { isInteractive } from './task-agent.js';
|
|
|
31
30
|
import { createLearnExperienceTool } from './tools.ts';
|
|
32
31
|
|
|
33
32
|
const debugLog = createDebug('explorbot:navigator');
|
|
33
|
+
const MAX_TRIED_STEPS = 5;
|
|
34
34
|
|
|
35
35
|
class Navigator implements Agent {
|
|
36
36
|
emoji = '🧭';
|
|
@@ -138,6 +138,10 @@ class Navigator implements Agent {
|
|
|
138
138
|
return matchesNavigationUrl(expectedUrl, currentUrl);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
+
isOn(url: string): boolean {
|
|
142
|
+
return this.isOnExpectedPage(url, this.stateManager);
|
|
143
|
+
}
|
|
144
|
+
|
|
141
145
|
async visit(url: string, opts: { recover?: boolean } = {}): Promise<boolean> {
|
|
142
146
|
return this.visitOnce(url, opts.recover !== false);
|
|
143
147
|
}
|
|
@@ -195,8 +199,9 @@ class Navigator implements Agent {
|
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
private navigationError(url: string, fallback: string): Error {
|
|
198
|
-
|
|
199
|
-
|
|
202
|
+
const currentUrl = this.stateManager.getCurrentState()?.url || '';
|
|
203
|
+
const tried = this.executedSteps.slice(-MAX_TRIED_STEPS).map((step) => step.command);
|
|
204
|
+
return new NavigationBlockedError(url, currentUrl, this.lastFailureReason || fallback, tried);
|
|
200
205
|
}
|
|
201
206
|
|
|
202
207
|
private failureReason(stopReason: string | null, knowledge: string, url: string): string | null {
|
|
@@ -402,7 +407,7 @@ class Navigator implements Agent {
|
|
|
402
407
|
Propose different solutions to achieve the result.
|
|
403
408
|
Solution should be valid CodeceptJS code.
|
|
404
409
|
Use only data from the <page> context to plan the solution.
|
|
405
|
-
The page is given as an ARIA snapshot only: use ARIA and text locators from it.
|
|
410
|
+
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.
|
|
406
411
|
Full HTML is provided if these solutions fail.
|
|
407
412
|
Try various ways to achieve the result
|
|
408
413
|
</task>
|
|
@@ -772,7 +777,7 @@ class Navigator implements Agent {
|
|
|
772
777
|
Do not generate assertions that would pass even if the specific claim is false.
|
|
773
778
|
</task>
|
|
774
779
|
|
|
775
|
-
${RulesLoader.loadRules('navigator', ['verification-actions'], actionResult.url || '')}
|
|
780
|
+
${RulesLoader.loadRules('navigator', ['verification-actions', 'verification-output'], actionResult.url || '').replace('{{maxAttempts}}', String(this.verifyAttempts))}
|
|
776
781
|
|
|
777
782
|
${experience}
|
|
778
783
|
|
|
@@ -858,7 +863,7 @@ class Navigator implements Agent {
|
|
|
858
863
|
|
|
859
864
|
const totalAttempted = Math.min(codeBlocks.length, this.verifyAttempts);
|
|
860
865
|
let verified = successfulCodes.length > 0 && !failures;
|
|
861
|
-
if (successfulCodes.length && failures) verified = await this.provider.oracle(message, { assertions: results.map(({ code, passed, error }) => ({ code, passed, error })) }, { agentName: 'navigator', telemetry: { functionId: 'navigator.verifyState' } });
|
|
866
|
+
if (successfulCodes.length && failures) verified = (await this.provider.oracle(message, { assertions: results.map(({ code, passed, error }) => ({ code, passed, error })) }, { agentName: 'navigator', telemetry: { functionId: 'navigator.verifyState' } })) === true;
|
|
862
867
|
if (alreadyVerified) verified = true;
|
|
863
868
|
|
|
864
869
|
const inexpressible = !alreadyVerified && totalAttempted === 0;
|
|
@@ -3,7 +3,6 @@ import { z } from 'zod';
|
|
|
3
3
|
import { normalizeUrl } from '../../state-manager.ts';
|
|
4
4
|
import type { StateManager } from '../../state-manager.ts';
|
|
5
5
|
import type { Plan } from '../../test-plan.ts';
|
|
6
|
-
import { tag } from '../../utils/logger.ts';
|
|
7
6
|
import { isSamePageFamily } from '../../utils/url-matcher.ts';
|
|
8
7
|
import type { Provider } from '../provider.ts';
|
|
9
8
|
import type { Constructor } from '../researcher/mixin.ts';
|
|
@@ -85,7 +84,7 @@ export function WithSubPages<T extends Constructor>(Base: T) {
|
|
|
85
84
|
async pickNextSubPage(candidates: SubPageCandidate[]): Promise<{ url: string; reason: string } | null> {
|
|
86
85
|
if (candidates.length === 0) return null;
|
|
87
86
|
|
|
88
|
-
const plannedEntries = [...planRegistry.entries()].map(([
|
|
87
|
+
const plannedEntries = [...planRegistry.entries()].map(([, record]) => `- ${record.url} (${record.plan.tests.length} tests${record.feature ? `, feature: ${record.feature}` : ''})`).join('\n');
|
|
89
88
|
|
|
90
89
|
const candidateList = candidates.map((c) => `- ${c.url} (visits: ${c.visitCount}${c.title ? `, title: "${c.title}"` : ''}${c.h1 ? `, h1: "${c.h1}"` : ''})`).join('\n');
|
|
91
90
|
|