explorbot 0.4.10 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/bin/explorbot-cli.ts +2 -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/README.md +31 -4
- package/boat/prima/bin/prima-cli.ts +2 -1
- package/boat/prima/src/prima.ts +41 -2
- package/dist/bin/explorbot-cli.js +2 -1
- 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/bin/prima-cli.js +2 -1
- package/dist/boat/prima/src/prima.js +42 -2
- package/dist/package.json +4 -3
- 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 -0
- package/dist/src/ai/navigator.js +13 -4
- package/dist/src/ai/pilot.d.ts +4 -0
- package/dist/src/ai/pilot.js +57 -5
- package/dist/src/ai/planner.js +9 -6
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +53 -8
- 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/rules.js +13 -0
- 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/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/options/decision-model-option.d.ts +6 -0
- package/dist/src/commands/options/decision-model-option.js +10 -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-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 +7 -0
- package/dist/src/config.js +11 -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/knowledge-tracker.js +1 -1
- 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 +8 -2
- 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/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/docs/api-testing/planning.md +1 -1
- package/docs/basics/providers.md +37 -0
- package/docs/index.json +1 -0
- package/docs/reference/commands.md +1 -0
- package/docs/reference/configuration.md +2 -0
- 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/docs/web-testing/decisions.md +108 -0
- package/docs/workflow/agentic-usage.md +1 -0
- package/package.json +4 -3
- 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 +15 -4
- package/src/ai/pilot.ts +58 -5
- package/src/ai/planner.ts +9 -6
- package/src/ai/provider.ts +52 -8
- 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/rules.ts +13 -0
- 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/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/options/decision-model-option.ts +11 -0
- package/src/commands/options/index.ts +2 -0
- 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 +11 -0
- package/src/experience-tracker.ts +4 -3
- package/src/explorbot.ts +8 -0
- package/src/knowledge-tracker.ts +1 -1
- 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 +6 -1
- 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/strings.ts +7 -0
- package/src/utils/test-files.ts +5 -2
package/dist/src/ai/agent.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ import type Explorer from '../explorer.js';
|
|
|
4
4
|
import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
5
5
|
import type { PlaywrightRecorder } from '../playwright-recorder.js';
|
|
6
6
|
import type { StateManager } from '../state-manager.js';
|
|
7
|
+
import type { Judge } from './judge.js';
|
|
7
8
|
import type { AIProvider } from './provider.js';
|
|
8
9
|
export interface Agent {
|
|
9
10
|
emoji?: string;
|
|
@@ -16,5 +17,6 @@ export interface AgentDeps {
|
|
|
16
17
|
knowledgeTracker: KnowledgeTracker;
|
|
17
18
|
requestStore: RequestStore;
|
|
18
19
|
playwrightRecorder: PlaywrightRecorder;
|
|
20
|
+
judge?: Judge;
|
|
19
21
|
}
|
|
20
|
-
export type ToolDeps = Pick<AgentDeps, 'explorer' | 'stateManager' | 'ai'>;
|
|
22
|
+
export type ToolDeps = Pick<AgentDeps, 'explorer' | 'stateManager' | 'ai' | 'judge'>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export declare class JudgeProvider {
|
|
2
|
+
readonly model: string;
|
|
3
|
+
fetchImpl: typeof fetch;
|
|
4
|
+
requestTimeoutMs: number;
|
|
5
|
+
url: string;
|
|
6
|
+
apiKey: string;
|
|
7
|
+
constructor(provider: string, model: string);
|
|
8
|
+
decide(state: unknown, question: string, options?: string[]): Promise<ProviderDecision>;
|
|
9
|
+
static endpointFor(provider: string): {
|
|
10
|
+
url: string;
|
|
11
|
+
keyName: string;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export interface ProviderDecision {
|
|
15
|
+
value: string;
|
|
16
|
+
probability: number;
|
|
17
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
const REQUEST_TIMEOUT_MS = 15000;
|
|
2
|
+
export class JudgeProvider {
|
|
3
|
+
model;
|
|
4
|
+
fetchImpl = fetch;
|
|
5
|
+
requestTimeoutMs = REQUEST_TIMEOUT_MS;
|
|
6
|
+
url;
|
|
7
|
+
apiKey;
|
|
8
|
+
constructor(provider, model) {
|
|
9
|
+
this.model = model;
|
|
10
|
+
const { url, keyName } = JudgeProvider.endpointFor(provider);
|
|
11
|
+
const apiKey = process.env[keyName];
|
|
12
|
+
if (!apiKey)
|
|
13
|
+
throw new Error(`Set ${keyName} to use the decision model`);
|
|
14
|
+
this.url = url;
|
|
15
|
+
this.apiKey = apiKey;
|
|
16
|
+
}
|
|
17
|
+
async decide(state, question, options) {
|
|
18
|
+
let q = { type: 'noul', instructions: question };
|
|
19
|
+
if (options)
|
|
20
|
+
q = { type: 'choice', instructions: question, criteria: Object.fromEntries(options.map((option, index) => [String(index), option])) };
|
|
21
|
+
const body = JSON.stringify({ model: this.model, state, questions: { q } });
|
|
22
|
+
const controller = new AbortController();
|
|
23
|
+
const timer = setTimeout(() => controller.abort(), this.requestTimeoutMs);
|
|
24
|
+
try {
|
|
25
|
+
const response = await this.fetchImpl(this.url, {
|
|
26
|
+
method: 'POST',
|
|
27
|
+
headers: { Authorization: `Bearer ${this.apiKey}`, 'Content-Type': 'application/json' },
|
|
28
|
+
body,
|
|
29
|
+
signal: controller.signal,
|
|
30
|
+
});
|
|
31
|
+
if (!response.ok)
|
|
32
|
+
throw new Error(`http_${response.status}`);
|
|
33
|
+
const answer = (await response.json())?.answers?.q;
|
|
34
|
+
if (!options && typeof answer?.noul === 'number')
|
|
35
|
+
return { value: 'yes', probability: answer.noul };
|
|
36
|
+
const value = options?.[Number(answer?.choice)];
|
|
37
|
+
const probability = answer?.probabilities?.[answer?.choice];
|
|
38
|
+
if (value === undefined || typeof probability !== 'number')
|
|
39
|
+
throw new Error('malformed_body');
|
|
40
|
+
return { value, probability };
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
static endpointFor(provider) {
|
|
47
|
+
switch (provider) {
|
|
48
|
+
case 'openrouter':
|
|
49
|
+
return { url: 'https://openrouter.ai/api/alpha/decisions', keyName: 'OPENROUTER_API_KEY' };
|
|
50
|
+
case 'typesafe':
|
|
51
|
+
return { url: 'https://api.typesafe.ai/v1/systemone', keyName: 'TYPESAFE_API_KEY' };
|
|
52
|
+
default:
|
|
53
|
+
throw new Error(`Unknown decision model provider "${provider}" — use "openrouter" or "typesafe"`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -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,6 +24,7 @@ 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;
|
|
@@ -82,6 +84,7 @@ declare class Navigator implements Agent {
|
|
|
82
84
|
args: any[];
|
|
83
85
|
}>;
|
|
84
86
|
totalAttempted: number;
|
|
87
|
+
judged?: Decision;
|
|
85
88
|
}>;
|
|
86
89
|
checkAlreadyVerified(aiResponse: string, actionResult: ActionResult): boolean;
|
|
87
90
|
}
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -13,6 +13,7 @@ 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
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;
|
|
@@ -517,8 +520,7 @@ class Navigator {
|
|
|
517
520
|
if (!value)
|
|
518
521
|
return;
|
|
519
522
|
const normalized = normalizeUrl(value);
|
|
520
|
-
|
|
521
|
-
visitCounts.set(normalized, (visitCounts.get(normalized) || 0) + 1);
|
|
523
|
+
visitCounts.set(normalized, (visitCounts.get(normalized) || 0) + 1);
|
|
522
524
|
};
|
|
523
525
|
for (const transition of history) {
|
|
524
526
|
countVisit(transition.toState.url);
|
|
@@ -527,7 +529,7 @@ class Navigator {
|
|
|
527
529
|
if (opts?.visitedUrls) {
|
|
528
530
|
for (const url of opts.visitedUrls) {
|
|
529
531
|
const normalized = normalizeUrl(url);
|
|
530
|
-
if (
|
|
532
|
+
if (!visitCounts.has(normalized)) {
|
|
531
533
|
visitCounts.set(normalized, 1);
|
|
532
534
|
}
|
|
533
535
|
}
|
|
@@ -619,6 +621,12 @@ class Navigator {
|
|
|
619
621
|
tag('operation').log(`Reusing cached verification: ${cachedVerification ? 'PASS' : 'FAIL'}`);
|
|
620
622
|
return { verified: cachedVerification, inexpressible: false, results: [], successfulCodes: [], assertionSteps: [], totalAttempted: 0 };
|
|
621
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
|
+
}
|
|
622
630
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
623
631
|
let experience = '';
|
|
624
632
|
if (!actionResult.isInsideIframe) {
|
|
@@ -741,7 +749,8 @@ class Navigator {
|
|
|
741
749
|
const inexpressible = !alreadyVerified && totalAttempted === 0;
|
|
742
750
|
if (inexpressible) {
|
|
743
751
|
tag('warning').log('No assertion could express this claim');
|
|
744
|
-
|
|
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 };
|
|
745
754
|
}
|
|
746
755
|
actionResult.addVerification(message, verified);
|
|
747
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;
|
|
@@ -480,6 +487,9 @@ export class Pilot {
|
|
|
480
487
|
const toolCalls = testerConversation.getToolExecutions().slice(-this.stepsToReview);
|
|
481
488
|
const actionsContext = this.formatActions(toolCalls);
|
|
482
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 '';
|
|
483
493
|
const hasFailures = toolCalls.length === 0 || toolCalls.some((t) => !t.wasSuccessful);
|
|
484
494
|
const text = await this.sendToPilot(dedent `
|
|
485
495
|
START URL: ${task.startUrl}
|
|
@@ -516,8 +526,12 @@ export class Pilot {
|
|
|
516
526
|
let undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
|
|
517
527
|
if (image)
|
|
518
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));
|
|
519
533
|
if (!undecided.length)
|
|
520
|
-
return task.expected.map((text) => ({ text, status: decided(text) }));
|
|
534
|
+
return task.expected.map((text) => ({ text, status: settledByJudge.get(text) || decided(text) }));
|
|
521
535
|
const schema = z.object({
|
|
522
536
|
outcomes: z.array(z.object({
|
|
523
537
|
expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
|
|
@@ -581,6 +595,9 @@ export class Pilot {
|
|
|
581
595
|
response = await settle(userContent, this.provider.getAgenticModel('pilot'));
|
|
582
596
|
const judged = new Map((response?.object?.outcomes || []).map((outcome) => [outcome.expectation, outcome]));
|
|
583
597
|
return task.expected.map((text) => {
|
|
598
|
+
const byJudge = settledByJudge.get(text);
|
|
599
|
+
if (byJudge)
|
|
600
|
+
return { text, status: byJudge };
|
|
584
601
|
if (!undecided.includes(text))
|
|
585
602
|
return { text, status: decided(text) };
|
|
586
603
|
const outcome = judged.get(text);
|
|
@@ -589,6 +606,20 @@ export class Pilot {
|
|
|
589
606
|
return { text, status: outcome.status || 'unverified', evidence: outcome.evidence };
|
|
590
607
|
});
|
|
591
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
|
+
}
|
|
592
623
|
formatExpectations(task) {
|
|
593
624
|
const checked = task.getCheckedExpectations();
|
|
594
625
|
const remaining = task.getRemainingExpectations();
|
|
@@ -605,6 +636,7 @@ export class Pilot {
|
|
|
605
636
|
}
|
|
606
637
|
this.conversation.addUserText(finalUserText);
|
|
607
638
|
const tools = { ...this.pickPlanningTools(), ...this.buildFishermanTools(opts.task) };
|
|
639
|
+
const preparedCount = opts.task.preparedData.length;
|
|
608
640
|
const result = await this.provider.invokeConversation(this.conversation, tools, {
|
|
609
641
|
maxToolRoundtrips: opts.maxToolRoundtrips ?? 0,
|
|
610
642
|
toolChoice: opts.tools ? 'auto' : 'none',
|
|
@@ -612,7 +644,7 @@ export class Pilot {
|
|
|
612
644
|
stopWhen: () => opts.task.hasFinished,
|
|
613
645
|
telemetry: { functionId },
|
|
614
646
|
});
|
|
615
|
-
const text = result?.response?.text || '';
|
|
647
|
+
const text = this.announcePreparedData(result?.response?.text || '', opts.task, preparedCount);
|
|
616
648
|
const learned = (result?.toolExecutions || []).filter((e) => e.toolName === 'learnExperience' && e.output?.content).map((e) => ({ url: e.output.url, content: e.output.content }));
|
|
617
649
|
if (learned.length === 0)
|
|
618
650
|
return text;
|
|
@@ -628,6 +660,23 @@ export class Pilot {
|
|
|
628
660
|
</applied_experience>
|
|
629
661
|
`;
|
|
630
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
|
+
}
|
|
631
680
|
getExperienceToc() {
|
|
632
681
|
const state = this.stateManager.getCurrentState();
|
|
633
682
|
if (!state)
|
|
@@ -635,7 +684,7 @@ export class Pilot {
|
|
|
635
684
|
return this.stateManager.getExperienceTracker().renderExperienceTocFor(ActionResult.fromState(state));
|
|
636
685
|
}
|
|
637
686
|
pickPlanningTools() {
|
|
638
|
-
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser } = this.agentTools ?? {};
|
|
687
|
+
const { see, context, verify, research, getVisitedStates, xpathCheck, learnExperience, askUser, judge } = this.agentTools ?? {};
|
|
639
688
|
const planning = {};
|
|
640
689
|
if (see)
|
|
641
690
|
planning.see = see;
|
|
@@ -653,6 +702,8 @@ export class Pilot {
|
|
|
653
702
|
planning.learnExperience = learnExperience;
|
|
654
703
|
if (askUser)
|
|
655
704
|
planning.askUser = askUser;
|
|
705
|
+
if (judge)
|
|
706
|
+
planning.judge = judge;
|
|
656
707
|
withdrawVisionTools(planning);
|
|
657
708
|
return planning;
|
|
658
709
|
}
|
|
@@ -700,6 +751,7 @@ export class Pilot {
|
|
|
700
751
|
});
|
|
701
752
|
const stepText = `Precondition: created ${items.join(', ')}`;
|
|
702
753
|
task.addStep(stepText);
|
|
754
|
+
task.preparedData.push(...items);
|
|
703
755
|
tag('success').log(stepText);
|
|
704
756
|
return { noted: true, prepared: true, created: result.created };
|
|
705
757
|
},
|
|
@@ -1053,7 +1105,7 @@ export class Pilot {
|
|
|
1053
1105
|
Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
|
|
1054
1106
|
visualClick, back, getVisitedStates, reset, stop, finish, record.
|
|
1055
1107
|
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1056
|
-
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.
|
|
1057
1109
|
|
|
1058
1110
|
${capabilityGroundingRule}
|
|
1059
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;
|