explorbot 0.4.9 → 0.5.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 +4 -1
- package/bin/mdq.ts +18 -0
- package/boat/api-tester/src/ai/chief.ts +72 -0
- package/boat/api-tester/src/api-client.ts +37 -0
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/mdq.js +19 -0
- package/dist/boat/api-tester/src/ai/chief.js +69 -0
- package/dist/boat/api-tester/src/api-client.js +26 -0
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +3 -2
- package/dist/src/action.js +4 -2
- package/dist/src/ai/agent.d.ts +3 -1
- package/dist/src/ai/judge-provider.d.ts +17 -0
- package/dist/src/ai/judge-provider.js +56 -0
- package/dist/src/ai/judge-tool.d.ts +2 -0
- package/dist/src/ai/judge-tool.js +33 -0
- package/dist/src/ai/judge.d.ts +28 -0
- package/dist/src/ai/judge.js +71 -0
- package/dist/src/ai/navigator.d.ts +3 -1
- package/dist/src/ai/navigator.js +24 -28
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +67 -9
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +52 -7
- package/dist/src/ai/rerunner.js +7 -0
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/researcher/locators.js +2 -2
- package/dist/src/ai/researcher/pagination.js +1 -1
- package/dist/src/ai/researcher/research-result.js +2 -2
- package/dist/src/ai/researcher.js +1 -1
- package/dist/src/ai/task-agent.d.ts +2 -0
- package/dist/src/ai/task-agent.js +3 -1
- package/dist/src/ai/tester.js +19 -15
- package/dist/src/ai/tools.d.ts +4 -3
- package/dist/src/ai/tools.js +35 -7
- package/dist/src/api/request-result.js +2 -1
- package/dist/src/api/xhr-capture.js +2 -1
- package/dist/src/command-handler.d.ts +1 -0
- package/dist/src/command-handler.js +24 -3
- package/dist/src/commands/base-command.d.ts +5 -0
- package/dist/src/commands/base-command.js +3 -0
- package/dist/src/commands/explore-command.d.ts +2 -1
- package/dist/src/commands/explore-command.js +12 -1
- package/dist/src/commands/freesail-command.js +8 -2
- package/dist/src/commands/init-command.js +1 -1
- package/dist/src/commands/navigate-command.d.ts +2 -1
- package/dist/src/commands/navigate-command.js +6 -0
- package/dist/src/commands/plan-load-command.d.ts +2 -1
- package/dist/src/commands/plan-load-command.js +4 -0
- package/dist/src/commands/plans-command.d.ts +3 -9
- package/dist/src/commands/plans-command.js +11 -21
- package/dist/src/commands/rerun-command.d.ts +2 -1
- package/dist/src/commands/rerun-command.js +5 -1
- package/dist/src/commands/research-command.d.ts +2 -1
- package/dist/src/commands/research-command.js +6 -0
- package/dist/src/commands/test-command.d.ts +2 -1
- package/dist/src/commands/test-command.js +4 -1
- package/dist/src/components/Autocomplete.js +26 -12
- package/dist/src/components/InputReadline.js +10 -1
- package/dist/src/config.d.ts +6 -0
- package/dist/src/experience-tracker.js +4 -3
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +8 -0
- package/dist/src/explorer.js +2 -3
- package/dist/src/knowledge-tracker.js +1 -1
- package/dist/src/reporter.js +8 -4
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +16 -0
- package/dist/src/test-plan.d.ts +11 -0
- package/dist/src/test-plan.js +54 -2
- package/dist/src/utils/aria-ref.js +1 -1
- package/dist/src/utils/logger.js +9 -3
- package/dist/src/utils/markdown-query.d.ts +1 -48
- package/dist/src/utils/markdown-query.js +1 -444
- package/dist/src/utils/mdq/cli.d.ts +6 -0
- package/dist/src/utils/mdq/cli.js +122 -0
- package/dist/src/utils/mdq/edit.d.ts +24 -0
- package/dist/src/utils/mdq/edit.js +147 -0
- package/dist/src/utils/mdq/query.d.ts +118 -0
- package/dist/src/utils/mdq/query.js +451 -0
- package/dist/src/utils/step-analyzer.d.ts +3 -0
- package/dist/src/utils/step-analyzer.js +7 -0
- package/dist/src/utils/strings.d.ts +1 -0
- package/dist/src/utils/strings.js +7 -0
- package/dist/src/utils/test-files.d.ts +1 -0
- package/dist/src/utils/test-files.js +5 -2
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +7 -0
- package/docs/api-testing/planning.md +1 -1
- package/docs/superpowers/plans/2026-09-15-mdq-package.md +130 -94
- package/docs/superpowers/specs/2026-09-18-judge-decision-model-design.md +79 -0
- package/package.json +3 -2
- package/src/action.ts +4 -2
- package/src/ai/agent.ts +3 -1
- package/src/ai/judge-provider.ts +62 -0
- package/src/ai/judge-tool.ts +35 -0
- package/src/ai/judge.ts +75 -0
- package/src/ai/navigator.ts +24 -27
- package/src/ai/pilot.ts +68 -9
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +51 -7
- package/src/ai/rerunner.ts +4 -0
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/researcher/locators.ts +2 -2
- package/src/ai/researcher/pagination.ts +1 -1
- package/src/ai/researcher/research-result.ts +2 -2
- package/src/ai/researcher.ts +1 -1
- package/src/ai/task-agent.ts +4 -1
- package/src/ai/tester.ts +19 -16
- package/src/ai/tools.ts +42 -7
- package/src/api/request-result.ts +2 -1
- package/src/api/xhr-capture.ts +2 -1
- package/src/command-handler.ts +28 -3
- package/src/commands/base-command.ts +9 -0
- package/src/commands/explore-command.ts +15 -2
- package/src/commands/freesail-command.ts +9 -2
- package/src/commands/init-command.ts +1 -1
- package/src/commands/navigate-command.ts +8 -1
- package/src/commands/plan-load-command.ts +6 -1
- package/src/commands/plans-command.ts +13 -29
- package/src/commands/rerun-command.ts +7 -2
- package/src/commands/research-command.ts +8 -1
- package/src/commands/test-command.ts +6 -2
- package/src/components/Autocomplete.tsx +39 -10
- package/src/components/InputReadline.tsx +10 -1
- package/src/config.ts +1 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/explorer.ts +2 -2
- package/src/knowledge-tracker.ts +1 -1
- package/src/reporter.ts +8 -4
- package/src/state-manager.ts +16 -0
- package/src/test-plan.ts +67 -2
- package/src/utils/aria-ref.ts +1 -1
- package/src/utils/logger.ts +7 -2
- package/src/utils/markdown-query.ts +1 -519
- package/src/utils/mdq/cli.ts +118 -0
- package/src/utils/mdq/edit.ts +158 -0
- package/src/utils/mdq/query.ts +556 -0
- package/src/utils/step-analyzer.ts +8 -0
- package/src/utils/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
- package/src/utils/url-matcher.ts +7 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { tool } from 'ai';
|
|
2
|
+
import dedent from 'dedent';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { failedToolResult, successToolResult } from "./tools.js";
|
|
5
|
+
export function createJudgeTool(deps, buildState) {
|
|
6
|
+
const judge = deps.judge;
|
|
7
|
+
if (!judge?.toolEnabled)
|
|
8
|
+
return {};
|
|
9
|
+
return {
|
|
10
|
+
judge: tool({
|
|
11
|
+
description: dedent `
|
|
12
|
+
Settle one judgement about the current page instead of guessing. Phrase it literally and concretely.
|
|
13
|
+
`,
|
|
14
|
+
inputSchema: z.object({
|
|
15
|
+
question: z.string().describe('The statement to confirm, or the question the options answer'),
|
|
16
|
+
options: z.array(z.string()).optional().describe('Possible answers. Omit to confirm a statement'),
|
|
17
|
+
context: z.string().optional().describe('Anything the page observation does not already carry'),
|
|
18
|
+
}),
|
|
19
|
+
execute: async ({ question, options, context }) => {
|
|
20
|
+
const state = await buildState();
|
|
21
|
+
if (context)
|
|
22
|
+
state.context = context;
|
|
23
|
+
const decision = await judge.consult(question, options ?? null, state);
|
|
24
|
+
if (decision.rejected) {
|
|
25
|
+
return failedToolResult('judge', `Not confirmed: ${question}`, {
|
|
26
|
+
suggestion: 'The page does not settle this. Gather more context or take another route; do not assume either answer.',
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
return successToolResult('judge', { question, answer: decision.value, confidence: decision.confidence });
|
|
30
|
+
},
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { AIConfig } from '../config.js';
|
|
2
|
+
import { JudgeProvider } from './judge-provider.js';
|
|
3
|
+
export declare const UNDECIDED = "undecided";
|
|
4
|
+
export declare const JUDGE_PAGE_CAP = 12000;
|
|
5
|
+
export declare class Decision {
|
|
6
|
+
readonly value: string | null;
|
|
7
|
+
readonly confidence: number;
|
|
8
|
+
constructor(value: string | null, confidence: number);
|
|
9
|
+
get approved(): boolean;
|
|
10
|
+
get rejected(): boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare class Judge {
|
|
13
|
+
provider: JudgeProvider;
|
|
14
|
+
enabled: {
|
|
15
|
+
tool: boolean;
|
|
16
|
+
direct: boolean;
|
|
17
|
+
};
|
|
18
|
+
constructor(provider: JudgeProvider, enabled: {
|
|
19
|
+
tool: boolean;
|
|
20
|
+
direct: boolean;
|
|
21
|
+
});
|
|
22
|
+
static fromConfig(config: AIConfig['decisionModel']): Judge | null;
|
|
23
|
+
get toolEnabled(): boolean;
|
|
24
|
+
decide(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>;
|
|
25
|
+
consult(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>;
|
|
26
|
+
request(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>;
|
|
27
|
+
recordFailure(error: unknown): null;
|
|
28
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { clearActivity, setActivity } from "../activity.js";
|
|
2
|
+
import { Observability } from "../observability.js";
|
|
3
|
+
import { createDebug } from "../utils/logger.js";
|
|
4
|
+
import { JudgeProvider } from "./judge-provider.js";
|
|
5
|
+
const debugLog = createDebug('explorbot:judge');
|
|
6
|
+
const APPROVAL_THRESHOLD = 0.7;
|
|
7
|
+
export const UNDECIDED = 'undecided';
|
|
8
|
+
export const JUDGE_PAGE_CAP = 12000;
|
|
9
|
+
export class Decision {
|
|
10
|
+
value;
|
|
11
|
+
confidence;
|
|
12
|
+
constructor(value, confidence) {
|
|
13
|
+
this.value = value;
|
|
14
|
+
this.confidence = confidence;
|
|
15
|
+
}
|
|
16
|
+
get approved() {
|
|
17
|
+
return this.value !== null;
|
|
18
|
+
}
|
|
19
|
+
get rejected() {
|
|
20
|
+
return this.value === null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export class Judge {
|
|
24
|
+
provider;
|
|
25
|
+
enabled;
|
|
26
|
+
constructor(provider, enabled) {
|
|
27
|
+
this.provider = provider;
|
|
28
|
+
this.enabled = enabled;
|
|
29
|
+
}
|
|
30
|
+
static fromConfig(config) {
|
|
31
|
+
if (!config)
|
|
32
|
+
return null;
|
|
33
|
+
return new Judge(new JudgeProvider(config.provider, config.model), { tool: config.tool !== false, direct: config.direct !== false });
|
|
34
|
+
}
|
|
35
|
+
get toolEnabled() {
|
|
36
|
+
return this.enabled.tool;
|
|
37
|
+
}
|
|
38
|
+
async decide(question, options, state) {
|
|
39
|
+
if (!this.enabled.direct)
|
|
40
|
+
return new Decision(null, 0);
|
|
41
|
+
return this.consult(question, options, state);
|
|
42
|
+
}
|
|
43
|
+
async consult(question, options, state) {
|
|
44
|
+
if (Array.isArray(options) && options.length < 2)
|
|
45
|
+
return new Decision(null, 0);
|
|
46
|
+
return Observability.run('judge.decide', { tags: ['judge'] }, async () => {
|
|
47
|
+
setActivity('⚖️ Asking judge...', 'ai');
|
|
48
|
+
const decision = await this.request(question, options, state).finally(() => clearActivity());
|
|
49
|
+
Observability.getSpan()?.setAttribute('ai.telemetry.metadata.judgeDecision', JSON.stringify({ question, value: decision.value, confidence: decision.confidence }));
|
|
50
|
+
return decision;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async request(question, options, state) {
|
|
54
|
+
let list;
|
|
55
|
+
if (Array.isArray(options))
|
|
56
|
+
list = options;
|
|
57
|
+
const answer = await this.provider.decide(state, question, list).catch((error) => this.recordFailure(error));
|
|
58
|
+
if (!answer)
|
|
59
|
+
return new Decision(null, 0);
|
|
60
|
+
if (answer.probability <= APPROVAL_THRESHOLD)
|
|
61
|
+
return new Decision(null, answer.probability);
|
|
62
|
+
if (answer.value === UNDECIDED)
|
|
63
|
+
return new Decision(null, answer.probability);
|
|
64
|
+
return new Decision(answer.value, answer.probability);
|
|
65
|
+
}
|
|
66
|
+
recordFailure(error) {
|
|
67
|
+
debugLog('judge declined: %s', error);
|
|
68
|
+
Observability.getSpan()?.setAttribute('ai.telemetry.metadata.judgeError', String(error));
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -8,6 +8,7 @@ import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
|
8
8
|
import { type StateManager } from '../state-manager.js';
|
|
9
9
|
import { HooksRunner } from '../utils/hooks-runner.js';
|
|
10
10
|
import type { Agent, AgentDeps } from './agent.js';
|
|
11
|
+
import { type Decision, type Judge } from './judge.js';
|
|
11
12
|
import type { Provider } from './provider.js';
|
|
12
13
|
declare class Navigator implements Agent {
|
|
13
14
|
emoji: string;
|
|
@@ -23,10 +24,10 @@ declare class Navigator implements Agent {
|
|
|
23
24
|
explorer: Explorer;
|
|
24
25
|
config: ExplorbotConfig;
|
|
25
26
|
stateManager: StateManager;
|
|
27
|
+
judge?: Judge;
|
|
26
28
|
constructor(deps: AgentDeps);
|
|
27
29
|
get verifyAttempts(): number;
|
|
28
30
|
get verifyTimeout(): number;
|
|
29
|
-
getBaseOrigin(): string | null;
|
|
30
31
|
getComparableCurrentUrl(stateManager: any, expectedUrl: string): string;
|
|
31
32
|
comparableUrl(state: {
|
|
32
33
|
url?: string;
|
|
@@ -83,6 +84,7 @@ declare class Navigator implements Agent {
|
|
|
83
84
|
args: any[];
|
|
84
85
|
}>;
|
|
85
86
|
totalAttempted: number;
|
|
87
|
+
judged?: Decision;
|
|
86
88
|
}>;
|
|
87
89
|
checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean;
|
|
88
90
|
}
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -12,7 +12,8 @@ import { createDebug, pluralize, tag } from '../utils/logger.js';
|
|
|
12
12
|
import { loop, pause } from '../utils/loop.js';
|
|
13
13
|
import { RulesLoader } from "../utils/rules-loader.js";
|
|
14
14
|
import { normalizeInlineText } from "../utils/strings.js";
|
|
15
|
-
import { extractStatePath, matchesNavigationUrl } from '../utils/url-matcher.js';
|
|
15
|
+
import { extractStatePath, isSameHostFamily, matchesNavigationUrl } from '../utils/url-matcher.js';
|
|
16
|
+
import { JUDGE_PAGE_CAP, UNDECIDED } from "./judge.js";
|
|
16
17
|
import { Researcher } from "./researcher.js";
|
|
17
18
|
import { actionRule, locatorRule, unexpectedPopupRule } from './rules.js';
|
|
18
19
|
import { isInteractive } from './task-agent.js';
|
|
@@ -67,6 +68,7 @@ class Navigator {
|
|
|
67
68
|
explorer;
|
|
68
69
|
config;
|
|
69
70
|
stateManager;
|
|
71
|
+
judge;
|
|
70
72
|
constructor(deps) {
|
|
71
73
|
this.provider = deps.ai;
|
|
72
74
|
this.explorer = deps.explorer;
|
|
@@ -75,6 +77,7 @@ class Navigator {
|
|
|
75
77
|
this.knowledgeTracker = deps.knowledgeTracker;
|
|
76
78
|
this.experienceTracker = deps.stateManager.getExperienceTracker();
|
|
77
79
|
this.hooksRunner = new HooksRunner(deps.explorer, deps.config);
|
|
80
|
+
this.judge = deps.judge;
|
|
78
81
|
}
|
|
79
82
|
get verifyAttempts() {
|
|
80
83
|
return this.config.ai?.agents?.navigator?.verifyAttempts ?? 3;
|
|
@@ -82,15 +85,6 @@ class Navigator {
|
|
|
82
85
|
get verifyTimeout() {
|
|
83
86
|
return this.config.ai?.agents?.navigator?.verifyTimeout ?? 1500;
|
|
84
87
|
}
|
|
85
|
-
getBaseOrigin() {
|
|
86
|
-
const baseUrl = this.config.playwright.url;
|
|
87
|
-
try {
|
|
88
|
-
return new URL(baseUrl).origin;
|
|
89
|
-
}
|
|
90
|
-
catch {
|
|
91
|
-
return null;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
88
|
getComparableCurrentUrl(stateManager, expectedUrl) {
|
|
95
89
|
const currentState = stateManager.getCurrentState();
|
|
96
90
|
if (!currentState)
|
|
@@ -109,19 +103,14 @@ class Navigator {
|
|
|
109
103
|
const currentFullUrl = currentState.fullUrl || currentState.url || '';
|
|
110
104
|
if (!currentFullUrl)
|
|
111
105
|
return false;
|
|
112
|
-
|
|
113
|
-
const currentOrigin = new URL(currentFullUrl).origin;
|
|
114
|
-
if (/^https?:\/\//i.test(expectedUrl)) {
|
|
115
|
-
return currentOrigin === new URL(expectedUrl).origin;
|
|
116
|
-
}
|
|
117
|
-
const baseOrigin = this.getBaseOrigin();
|
|
118
|
-
if (!baseOrigin)
|
|
119
|
-
return true;
|
|
120
|
-
return currentOrigin === baseOrigin;
|
|
121
|
-
}
|
|
122
|
-
catch {
|
|
106
|
+
if (!/^https?:\/\//i.test(currentFullUrl))
|
|
123
107
|
return !/^https?:\/\//i.test(expectedUrl);
|
|
124
|
-
|
|
108
|
+
if (/^https?:\/\//i.test(expectedUrl))
|
|
109
|
+
return isSameHostFamily(currentFullUrl, expectedUrl);
|
|
110
|
+
const baseUrl = this.config.playwright.url;
|
|
111
|
+
if (!baseUrl)
|
|
112
|
+
return true;
|
|
113
|
+
return isSameHostFamily(currentFullUrl, baseUrl);
|
|
125
114
|
}
|
|
126
115
|
isOnExpectedPage(expectedUrl, stateManager) {
|
|
127
116
|
if (!this.isSameExpectedOrigin(expectedUrl, stateManager)) {
|
|
@@ -289,8 +278,9 @@ class Navigator {
|
|
|
289
278
|
tag('warning').log(`Page state did not change at ${check.freshState.url}`);
|
|
290
279
|
}
|
|
291
280
|
else {
|
|
292
|
-
|
|
293
|
-
|
|
281
|
+
const reachedUrl = check.freshState.fullUrl || check.freshState.url;
|
|
282
|
+
lastFailure = `Reached ${reachedUrl}, expected ${expectedUrl}`;
|
|
283
|
+
tag('warning').log(`URL verification failed: expected ${expectedUrl}, got ${reachedUrl}`);
|
|
294
284
|
}
|
|
295
285
|
batchFailures.push({
|
|
296
286
|
code: codeBlock,
|
|
@@ -530,8 +520,7 @@ class Navigator {
|
|
|
530
520
|
if (!value)
|
|
531
521
|
return;
|
|
532
522
|
const normalized = normalizeUrl(value);
|
|
533
|
-
|
|
534
|
-
visitCounts.set(normalized, (visitCounts.get(normalized) || 0) + 1);
|
|
523
|
+
visitCounts.set(normalized, (visitCounts.get(normalized) || 0) + 1);
|
|
535
524
|
};
|
|
536
525
|
for (const transition of history) {
|
|
537
526
|
countVisit(transition.toState.url);
|
|
@@ -540,7 +529,7 @@ class Navigator {
|
|
|
540
529
|
if (opts?.visitedUrls) {
|
|
541
530
|
for (const url of opts.visitedUrls) {
|
|
542
531
|
const normalized = normalizeUrl(url);
|
|
543
|
-
if (
|
|
532
|
+
if (!visitCounts.has(normalized)) {
|
|
544
533
|
visitCounts.set(normalized, 1);
|
|
545
534
|
}
|
|
546
535
|
}
|
|
@@ -632,6 +621,12 @@ class Navigator {
|
|
|
632
621
|
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
633
622
|
return { verified: cachedVerification, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
634
623
|
}
|
|
624
|
+
const verifiedClaims = Object.keys(actionResult.verifications ?? {}).filter((claim) => actionResult.getVerification(claim) === true);
|
|
625
|
+
const same = await this.judge?.decide('Which already verified claim means the same as the claim under consideration?', [...verifiedClaims, UNDECIDED], { claim: message });
|
|
626
|
+
if (same?.approved) {
|
|
627
|
+
tag('operation').log(`Judge matched claim to an already verified one: "${same.value}"`);
|
|
628
|
+
return { verified: true, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
629
|
+
}
|
|
635
630
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
636
631
|
let experience = '';
|
|
637
632
|
if (!actionResult.isInsideIframe) {
|
|
@@ -754,7 +749,8 @@ class Navigator {
|
|
|
754
749
|
const inexpressible = !alreadyVerified && totalAttempted === 0;
|
|
755
750
|
if (inexpressible) {
|
|
756
751
|
tag('warning').log('No assertion could express this claim');
|
|
757
|
-
|
|
752
|
+
const judged = await this.judge?.decide('The page shows that this claim is true.', null, { claim: message, page: actionResult.getCompactARIA().slice(0, JUDGE_PAGE_CAP) });
|
|
753
|
+
return { verified: false, inexpressible, results, successfulCodes, assertionSteps, totalAttempted, judged };
|
|
758
754
|
}
|
|
759
755
|
actionResult.addVerification(message, verified);
|
|
760
756
|
this.stateManager.updateState(actionResult);
|
package/dist/src/ai/pilot.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type Test } from '../test-plan.js';
|
|
|
7
7
|
import type { Agent, AgentDeps } from './agent.js';
|
|
8
8
|
import type { Conversation } from './conversation.js';
|
|
9
9
|
import type { Fisherman } from './fisherman.js';
|
|
10
|
+
import { type Judge } from './judge.js';
|
|
10
11
|
import type { Navigator } from './navigator.js';
|
|
11
12
|
import type { Provider } from './provider.js';
|
|
12
13
|
import type { Researcher } from './researcher.js';
|
|
@@ -21,6 +22,7 @@ export declare class Pilot implements Agent {
|
|
|
21
22
|
requestStore: RequestStore;
|
|
22
23
|
playwrightRecorder: PlaywrightRecorder;
|
|
23
24
|
fisherman: Fisherman | null;
|
|
25
|
+
judge?: Judge;
|
|
24
26
|
constructor(deps: AgentDeps, agentTools: any, researcher: Researcher);
|
|
25
27
|
setFisherman(fisherman: Fisherman): void;
|
|
26
28
|
get stepsToReview(): number;
|
|
@@ -41,12 +43,14 @@ export declare class Pilot implements Agent {
|
|
|
41
43
|
reviewNewPage(task: Test, currentState: ActionResult, testerConversation: Conversation): Promise<string>;
|
|
42
44
|
analyzeProgress(task: Test, currentState: ActionResult, testerConversation: Conversation): Promise<string>;
|
|
43
45
|
settleExpectations(task: Test, finalState?: ActionResult): Promise<SettledExpectation[]>;
|
|
46
|
+
settleByJudge(task: Test, expectations: string[]): Promise<Map<string, SettledStatus>>;
|
|
44
47
|
formatExpectations(task: Test): string;
|
|
45
48
|
sendToPilot(userText: string, functionId: string, opts: {
|
|
46
49
|
tools?: boolean;
|
|
47
50
|
maxToolRoundtrips?: number;
|
|
48
51
|
task: Test;
|
|
49
52
|
}): Promise<string>;
|
|
53
|
+
announcePreparedData(text: string, task: Test, preparedCount: number): string;
|
|
50
54
|
getExperienceToc(): string;
|
|
51
55
|
pickPlanningTools(): Record<string, unknown>;
|
|
52
56
|
fishermanStatus(): string;
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -4,13 +4,14 @@ import { z } from 'zod';
|
|
|
4
4
|
import { ActionResult } from "../action-result.js";
|
|
5
5
|
import { ConfigParser } from "../config.js";
|
|
6
6
|
import { Stats } from "../stats.js";
|
|
7
|
-
import { TestResult } from "../test-plan.js";
|
|
7
|
+
import { TestResult, TestStatus } from "../test-plan.js";
|
|
8
8
|
import { collectInteractiveNodes } from "../utils/aria.js";
|
|
9
9
|
import { ErrorPageError } from "../utils/error-page.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
const debugLog = createDebug('explorbot:pilot');
|
|
12
12
|
import { truncateJson } from "../utils/strings.js";
|
|
13
13
|
import { createAskApiTool } from "./fisherman/tools.js";
|
|
14
|
+
import { UNDECIDED } from "./judge.js";
|
|
14
15
|
import { capabilityGroundingRule, dataProtectionRules } from "./rules.js";
|
|
15
16
|
import { isInteractive } from "./task-agent.js";
|
|
16
17
|
import { withdrawVisionTools } from "./tools.js";
|
|
@@ -21,6 +22,10 @@ const PILOT_REASONING_LIMIT = 500;
|
|
|
21
22
|
const PILOT_MESSAGE_LIMIT = 2;
|
|
22
23
|
const PILOT_MESSAGE_MAX_LENGTH = 160;
|
|
23
24
|
const PILOT_REQUEST_LIMIT = 5;
|
|
25
|
+
const OUTCOME_STATUS = {
|
|
26
|
+
'The run shows this outcome happened.': 'passed',
|
|
27
|
+
'The run shows this outcome did not happen.': 'failed',
|
|
28
|
+
};
|
|
24
29
|
export class Pilot {
|
|
25
30
|
emoji = '🧭';
|
|
26
31
|
provider;
|
|
@@ -32,6 +37,7 @@ export class Pilot {
|
|
|
32
37
|
requestStore;
|
|
33
38
|
playwrightRecorder;
|
|
34
39
|
fisherman = null;
|
|
40
|
+
judge;
|
|
35
41
|
constructor(deps, agentTools, researcher) {
|
|
36
42
|
this.provider = deps.ai;
|
|
37
43
|
this.agentTools = agentTools;
|
|
@@ -40,6 +46,7 @@ export class Pilot {
|
|
|
40
46
|
this.stateManager = deps.stateManager;
|
|
41
47
|
this.requestStore = deps.requestStore;
|
|
42
48
|
this.playwrightRecorder = deps.playwrightRecorder;
|
|
49
|
+
this.judge = deps.judge;
|
|
43
50
|
}
|
|
44
51
|
setFisherman(fisherman) {
|
|
45
52
|
this.fisherman = fisherman;
|
|
@@ -95,7 +102,9 @@ export class Pilot {
|
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
104
|
const schema = z.object({
|
|
98
|
-
decision: z
|
|
105
|
+
decision: z
|
|
106
|
+
.enum(['pass', 'fail', 'continue', 'skipped'])
|
|
107
|
+
.describe('pass = scenario goal accomplished, fail = the app misbehaved, continue = tester should keep going, skipped = the scenario cannot be judged against this app (its premise does not hold, it is irrelevant, or systematic execution failures prevented testing)'),
|
|
99
108
|
reason: z.string().describe('Concise user-facing reason, maximum 1 short sentence and 120 characters. Do NOT repeat the decision status; explain only the evidence. For continue: explain why rejected and suggest alternatives.'),
|
|
100
109
|
guidance: z.string().nullable().describe('Required for "continue": specific actionable instruction for the tester — what exactly to verify, retry differently, or complete next. Be concrete.'),
|
|
101
110
|
requestVerification: z
|
|
@@ -358,9 +367,13 @@ export class Pilot {
|
|
|
358
367
|
DOM assertion can't be made.
|
|
359
368
|
Do not pass when Tester achieved only a related navigation/filter/tab/status outcome instead of the
|
|
360
369
|
requested action, workflow, or entity detail goal.
|
|
361
|
-
- "fail":
|
|
362
|
-
|
|
363
|
-
|
|
370
|
+
- "fail": the app misbehaved — the scenario's action ran against the right target and the app
|
|
371
|
+
produced a wrong, broken, or missing outcome. Not reaching the goal is not by itself a fail.
|
|
372
|
+
- "skipped": the scenario cannot be judged against this app — the page shows its premise does not
|
|
373
|
+
hold (the assumed constraint, field, or behaviour is designed differently), the target entity or
|
|
374
|
+
feature is not the one here, the scenario is irrelevant, OR systematic infrastructure failures
|
|
375
|
+
(LLM errors, crashes) prevented testing. NOT for "test failed to interact" — that's "fail" or
|
|
376
|
+
"continue".
|
|
364
377
|
- "continue": goal incomplete but the control for the NEXT step is present on the current page, or a
|
|
365
378
|
concrete missing check would change your verdict. Guidance must name that step.
|
|
366
379
|
If a verify() asserted a state that was ALREADY TRUE before the test, it proves nothing — reject.
|
|
@@ -474,6 +487,9 @@ export class Pilot {
|
|
|
474
487
|
const toolCalls = testerConversation.getToolExecutions().slice(-this.stepsToReview);
|
|
475
488
|
const actionsContext = this.formatActions(toolCalls);
|
|
476
489
|
const stateContext = this.buildStateContext(currentState);
|
|
490
|
+
const healthy = await this.judge?.decide('The run is moving toward the goal and can continue without a supervisor reviewing it now.', null, { scenario: task.scenario, state: stateContext, recentActions: actionsContext });
|
|
491
|
+
if (healthy?.approved)
|
|
492
|
+
return '';
|
|
477
493
|
const hasFailures = toolCalls.length === 0 || toolCalls.some((t) => !t.wasSuccessful);
|
|
478
494
|
const text = await this.sendToPilot(dedent `
|
|
479
495
|
START URL: ${task.startUrl}
|
|
@@ -510,8 +526,12 @@ export class Pilot {
|
|
|
510
526
|
let undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
|
|
511
527
|
if (image)
|
|
512
528
|
undecided = task.expected;
|
|
529
|
+
let settledByJudge = new Map();
|
|
530
|
+
if (!image)
|
|
531
|
+
settledByJudge = await this.settleByJudge(task, undecided);
|
|
532
|
+
undecided = undecided.filter((text) => !settledByJudge.has(text));
|
|
513
533
|
if (!undecided.length)
|
|
514
|
-
return task.expected.map((text) => ({ text, status: decided(text) }));
|
|
534
|
+
return task.expected.map((text) => ({ text, status: settledByJudge.get(text) || decided(text) }));
|
|
515
535
|
const schema = z.object({
|
|
516
536
|
outcomes: z.array(z.object({
|
|
517
537
|
expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
|
|
@@ -575,6 +595,9 @@ export class Pilot {
|
|
|
575
595
|
response = await settle(userContent, this.provider.getAgenticModel('pilot'));
|
|
576
596
|
const judged = new Map((response?.object?.outcomes || []).map((outcome) => [outcome.expectation, outcome]));
|
|
577
597
|
return task.expected.map((text) => {
|
|
598
|
+
const byJudge = settledByJudge.get(text);
|
|
599
|
+
if (byJudge)
|
|
600
|
+
return { text, status: byJudge };
|
|
578
601
|
if (!undecided.includes(text))
|
|
579
602
|
return { text, status: decided(text) };
|
|
580
603
|
const outcome = judged.get(text);
|
|
@@ -583,6 +606,20 @@ export class Pilot {
|
|
|
583
606
|
return { text, status: outcome.status || 'unverified', evidence: outcome.evidence };
|
|
584
607
|
});
|
|
585
608
|
}
|
|
609
|
+
async settleByJudge(task, expectations) {
|
|
610
|
+
const settled = new Map();
|
|
611
|
+
const judge = this.judge;
|
|
612
|
+
if (!judge)
|
|
613
|
+
return settled;
|
|
614
|
+
const state = { scenario: task.scenario, runLog: task.notesToString() || 'No steps recorded.' };
|
|
615
|
+
await Promise.all(expectations.map(async (text) => {
|
|
616
|
+
const decision = await judge.decide(`What did this run establish about the expected outcome: ${text}`, [...Object.keys(OUTCOME_STATUS), UNDECIDED], state);
|
|
617
|
+
const status = OUTCOME_STATUS[decision.value ?? ''];
|
|
618
|
+
if (status)
|
|
619
|
+
settled.set(text, status);
|
|
620
|
+
}));
|
|
621
|
+
return settled;
|
|
622
|
+
}
|
|
586
623
|
formatExpectations(task) {
|
|
587
624
|
const checked = task.getCheckedExpectations();
|
|
588
625
|
const remaining = task.getRemainingExpectations();
|
|
@@ -599,6 +636,7 @@ export class Pilot {
|
|
|
599
636
|
}
|
|
600
637
|
this.conversation.addUserText(finalUserText);
|
|
601
638
|
const tools = { ...this.pickPlanningTools(), ...this.buildFishermanTools(opts.task) };
|
|
639
|
+
const preparedCount = opts.task.preparedData.length;
|
|
602
640
|
const result = await this.provider.invokeConversation(this.conversation, tools, {
|
|
603
641
|
maxToolRoundtrips: opts.maxToolRoundtrips ?? 0,
|
|
604
642
|
toolChoice: opts.tools ? 'auto' : 'none',
|
|
@@ -606,7 +644,7 @@ export class Pilot {
|
|
|
606
644
|
stopWhen: () => opts.task.hasFinished,
|
|
607
645
|
telemetry: { functionId },
|
|
608
646
|
});
|
|
609
|
-
const text = result?.response?.text || '';
|
|
647
|
+
const text = this.announcePreparedData(result?.response?.text || '', opts.task, preparedCount);
|
|
610
648
|
const learned = (result?.toolExecutions || []).filter((e) => e.toolName === 'learnExperience' && e.output?.content).map((e) => ({ url: e.output.url, content: e.output.content }));
|
|
611
649
|
if (learned.length === 0)
|
|
612
650
|
return text;
|
|
@@ -622,6 +660,23 @@ export class Pilot {
|
|
|
622
660
|
</applied_experience>
|
|
623
661
|
`;
|
|
624
662
|
}
|
|
663
|
+
announcePreparedData(text, task, preparedCount) {
|
|
664
|
+
const prepared = task.preparedData.slice(preparedCount);
|
|
665
|
+
if (prepared.length === 0)
|
|
666
|
+
return text;
|
|
667
|
+
let refresh = '';
|
|
668
|
+
if (task.status === TestStatus.IN_PROGRESS)
|
|
669
|
+
refresh = 'It was created after the page loaded, so the page does not show it yet. Run I.refreshPage() through form() before looking for it.';
|
|
670
|
+
return dedent `
|
|
671
|
+
${text}
|
|
672
|
+
|
|
673
|
+
<prepared_data>
|
|
674
|
+
Pilot created this data through the API for this test. Use it instead of creating the same data through the UI:
|
|
675
|
+
${prepared.map((item) => `- ${item}`).join('\n')}
|
|
676
|
+
${refresh}
|
|
677
|
+
</prepared_data>
|
|
678
|
+
`;
|
|
679
|
+
}
|
|
625
680
|
getExperienceToc() {
|
|
626
681
|
const state = this.stateManager.getCurrentState();
|
|
627
682
|
if (!state)
|
|
@@ -629,7 +684,7 @@ export class Pilot {
|
|
|
629
684
|
return this.stateManager.getExperienceTracker().renderExperienceTocFor(ActionResult.fromState(state));
|
|
630
685
|
}
|
|
631
686
|
pickPlanningTools() {
|
|
632
|
-
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser } = this.agentTools ?? {};
|
|
687
|
+
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser, judge } = this.agentTools ?? {};
|
|
633
688
|
const planning = {};
|
|
634
689
|
if (see)
|
|
635
690
|
planning.see = see;
|
|
@@ -647,6 +702,8 @@ export class Pilot {
|
|
|
647
702
|
planning.learnExperience = learnExperience;
|
|
648
703
|
if (askUser)
|
|
649
704
|
planning.askUser = askUser;
|
|
705
|
+
if (judge)
|
|
706
|
+
planning.judge = judge;
|
|
650
707
|
withdrawVisionTools(planning);
|
|
651
708
|
return planning;
|
|
652
709
|
}
|
|
@@ -694,6 +751,7 @@ export class Pilot {
|
|
|
694
751
|
});
|
|
695
752
|
const stepText = `Precondition: created ${items.join(', ')}`;
|
|
696
753
|
task.addStep(stepText);
|
|
754
|
+
task.preparedData.push(...items);
|
|
697
755
|
tag('success').log(stepText);
|
|
698
756
|
return { noted: true, prepared: true, created: result.created };
|
|
699
757
|
},
|
|
@@ -1047,7 +1105,7 @@ export class Pilot {
|
|
|
1047
1105
|
Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
|
|
1048
1106
|
visualClick, back, getVisitedStates, reset, stop, finish, record.
|
|
1049
1107
|
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1050
|
-
Reloading is not a tool: to re-read a page from the server, instruct Tester to run I.
|
|
1108
|
+
Reloading is not a tool: to re-read a page from the server, instruct Tester to run I.refreshPage() through form.
|
|
1051
1109
|
|
|
1052
1110
|
${capabilityGroundingRule}
|
|
1053
1111
|
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -263,13 +263,14 @@ export class Planner extends PlannerBase {
|
|
|
263
263
|
.replaceEach((section) => {
|
|
264
264
|
const heading = section.query('heading').text().trim();
|
|
265
265
|
const withoutHeadings = mdq(section.text()).query('heading').replace('');
|
|
266
|
-
const body = mdq(withoutHeadings).query('hr').replace('').trim();
|
|
266
|
+
const body = mdq(withoutHeadings).query('hr').replace('').toString().trim();
|
|
267
267
|
if (body && !seenTitles.has(heading)) {
|
|
268
268
|
seenTitles.add(heading);
|
|
269
269
|
return section.text();
|
|
270
270
|
}
|
|
271
271
|
return '';
|
|
272
|
-
})
|
|
272
|
+
})
|
|
273
|
+
.toString();
|
|
273
274
|
}
|
|
274
275
|
const trimmedTitles = new Set();
|
|
275
276
|
for (const selector of ['section2', 'section3']) {
|
|
@@ -282,10 +283,11 @@ export class Planner extends PlannerBase {
|
|
|
282
283
|
const count = section.query('blockquote').count();
|
|
283
284
|
if (count <= 10)
|
|
284
285
|
return section.text();
|
|
285
|
-
const kept = mdq(section.text()).query('blockquote[10:]').replace('');
|
|
286
|
+
const kept = mdq(section.text()).query('blockquote[10:]').replace('').toString();
|
|
286
287
|
trimmedTitles.add(heading);
|
|
287
288
|
return `${kept.trimEnd()}\n> ... and ${count - 10} more discoveries\n`;
|
|
288
|
-
})
|
|
289
|
+
})
|
|
290
|
+
.toString();
|
|
289
291
|
}
|
|
290
292
|
return result.trim() || null;
|
|
291
293
|
}
|
|
@@ -360,7 +362,7 @@ export class Planner extends PlannerBase {
|
|
|
360
362
|
if (this.scout && this.docsWeight > 0) {
|
|
361
363
|
docsPromise = this.scout.collectDocs({ url: state.url, title: state.title, feature, excludeUrls: this.knowledgeTracker.applicationSpecUrls(state) });
|
|
362
364
|
}
|
|
363
|
-
let plannerResearch = mdq(research).query('code').replace('');
|
|
365
|
+
let plannerResearch = mdq(research).query('code').replace('').toString();
|
|
364
366
|
plannerResearch = mdq(plannerResearch)
|
|
365
367
|
.query('table')
|
|
366
368
|
.replaceEach((table) => {
|
|
@@ -372,7 +374,8 @@ export class Planner extends PlannerBase {
|
|
|
372
374
|
Type: r.Type || '',
|
|
373
375
|
}));
|
|
374
376
|
return jsonToTable(elementWithType, ['Element', 'Type']);
|
|
375
|
-
})
|
|
377
|
+
})
|
|
378
|
+
.toString();
|
|
376
379
|
const hasFocusedOverlay = hasFocusedSection(plannerResearch);
|
|
377
380
|
const focusNote = hasFocusedOverlay ? "IMPORTANT: One section is marked as **Focused** — this is the user's current focus area. Concentrate testing on the Focused section FIRST — test all interactions inside it before planning tests for the rest of the page." : '';
|
|
378
381
|
const featureFilter = feature ? `FOCUS FILTER: Only propose scenarios using elements relevant to "${feature}". Ignore all other elements.` : '';
|
|
@@ -32,8 +32,11 @@ export declare class Provider {
|
|
|
32
32
|
finalizeConfig(config: Record<string, any>, options: any, telemetry: any): void;
|
|
33
33
|
buildGenerateConfig(defaults: Record<string, any>, overrides: Record<string, any>, options: any): Record<string, any>;
|
|
34
34
|
recordUsage(agentName: string, modelName: string, usage: any): void;
|
|
35
|
-
raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number
|
|
35
|
+
raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number, busy?: {
|
|
36
|
+
tools: number;
|
|
37
|
+
}): Promise<T>;
|
|
36
38
|
recoverFromContextLength(error: any, messages: ModelMessage[], options: any, retry: (messages: ModelMessage[], options: any) => Promise<any>): Promise<any>;
|
|
39
|
+
recoverWithPlainJson(messages: ModelMessage[], schema: any, model: any, options: any): Promise<any>;
|
|
37
40
|
initLangfuse(): void;
|
|
38
41
|
getTelemetry(options: any): any;
|
|
39
42
|
startConversation(systemMessage: string, agentName?: string, model?: any): Conversation;
|