explorbot 0.2.3 → 0.2.5
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 +26 -8
- package/boat/api-tester/src/cli.ts +17 -0
- package/boat/api-tester/src/config.ts +4 -2
- package/boat/doc-collector/bin/doc-collector-cli.ts +2 -0
- package/boat/doc-collector/src/ai/documentarian.ts +61 -31
- package/boat/doc-collector/src/cli.ts +14 -1
- package/boat/doc-collector/src/config.ts +4 -2
- package/boat/prima/bin/prima-cli.ts +0 -0
- package/boat/prima/src/activity-line.ts +33 -0
- package/boat/prima/src/cli.ts +127 -86
- package/boat/prima/src/envelope.ts +102 -52
- package/boat/prima/src/prima.ts +567 -128
- package/boat/prima/src/pw-parser.ts +11 -1
- package/boat/prima/src/pw-registry.ts +4 -5
- package/boat/prima/src/session-log.ts +126 -0
- package/dist/bin/explorbot-cli.js +26 -8
- package/dist/boat/api-tester/bin/apibot-cli.js +2 -0
- package/dist/boat/api-tester/src/cli.js +17 -0
- package/dist/boat/api-tester/src/config.js +4 -2
- package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -0
- package/dist/boat/doc-collector/src/ai/documentarian.js +44 -19
- package/dist/boat/doc-collector/src/cli.js +14 -1
- package/dist/boat/doc-collector/src/config.js +4 -2
- package/dist/boat/prima/src/activity-line.js +30 -0
- package/dist/boat/prima/src/cli.js +109 -77
- package/dist/boat/prima/src/envelope.js +94 -44
- package/dist/boat/prima/src/prima.js +533 -119
- package/dist/boat/prima/src/pw-parser.js +13 -1
- package/dist/boat/prima/src/pw-registry.js +4 -5
- package/dist/boat/prima/src/session-log.js +108 -0
- package/dist/package.json +3 -2
- package/dist/rules/navigator/verification-actions.md +20 -0
- package/dist/src/action-result.d.ts +7 -0
- package/dist/src/action-result.js +4 -0
- package/dist/src/action.d.ts +2 -0
- package/dist/src/action.js +41 -2
- package/dist/src/ai/captain/web-mode.js +6 -3
- package/dist/src/ai/captain.js +2 -0
- package/dist/src/ai/navigator.d.ts +34 -0
- package/dist/src/ai/navigator.js +237 -181
- package/dist/src/ai/pilot.d.ts +7 -0
- package/dist/src/ai/pilot.js +90 -2
- package/dist/src/ai/provider.d.ts +2 -2
- package/dist/src/ai/provider.js +14 -23
- package/dist/src/ai/rerunner.js +2 -1
- package/dist/src/ai/researcher/cache.d.ts +2 -0
- package/dist/src/ai/researcher/cache.js +10 -2
- package/dist/src/ai/researcher.js +3 -2
- package/dist/src/ai/rules.js +17 -10
- package/dist/src/ai/session-analyst.js +2 -0
- package/dist/src/ai/task-agent.js +4 -1
- package/dist/src/ai/tester.d.ts +6 -3
- package/dist/src/ai/tester.js +50 -46
- package/dist/src/ai/tools.d.ts +14 -0
- package/dist/src/ai/tools.js +117 -37
- package/dist/src/commands/config-command.d.ts +51 -0
- package/dist/src/commands/config-command.js +117 -0
- package/dist/src/commands/index.js +2 -0
- package/dist/src/config.d.ts +9 -1
- package/dist/src/config.js +53 -4
- package/dist/src/execution-controller.d.ts +2 -0
- package/dist/src/execution-controller.js +6 -0
- package/dist/src/explorbot.d.ts +2 -1
- package/dist/src/explorbot.js +7 -2
- package/dist/src/explorer.js +2 -3
- package/dist/src/playwright-recorder.js +30 -0
- package/dist/src/remote.d.ts +55 -0
- package/dist/src/remote.js +235 -0
- package/dist/src/reporter.d.ts +1 -0
- package/dist/src/reporter.js +7 -1
- package/dist/src/state-manager.d.ts +2 -1
- package/dist/src/state-manager.js +3 -1
- package/dist/src/stats.d.ts +1 -0
- package/dist/src/stats.js +1 -0
- package/dist/src/test-plan.d.ts +3 -0
- package/dist/src/test-plan.js +26 -0
- package/dist/src/utils/aria.d.ts +2 -8
- package/dist/src/utils/aria.js +69 -40
- package/dist/src/utils/html.js +1 -0
- package/dist/src/utils/logger.d.ts +7 -1
- package/dist/src/utils/logger.js +32 -0
- package/dist/src/utils/page-readiness.js +18 -1
- package/dist/src/utils/url-matcher.js +3 -0
- package/dist/src/utils/web-element.d.ts +2 -0
- package/dist/src/utils/web-element.js +8 -0
- package/dist/src/utils/web-sandbox.d.ts +1 -1
- package/dist/src/utils/web-sandbox.js +2 -3
- package/docs/api-testing/basics.md +90 -0
- package/docs/api-testing/planning.md +57 -0
- package/docs/api-testing/running-tests.md +55 -0
- package/docs/assets/cloud-report.png +0 -0
- package/docs/assets/html-report.png +0 -0
- package/docs/assets/langfuse-trace.png +0 -0
- package/docs/assets/successful-explore-run.png +0 -0
- package/docs/basics/getting-started.md +140 -0
- package/docs/basics/prerequisites.md +63 -0
- package/docs/basics/providers.md +362 -0
- package/docs/basics/running.md +78 -0
- package/docs/contributing/ai-integration-tests.md +57 -0
- package/docs/contributing/contributing.md +90 -0
- package/docs/contributing/demo-videos.md +36 -0
- package/docs/contributing/npm-package.md +138 -0
- package/docs/contributing/observability.md +227 -0
- package/docs/contributing/regression-tests.md +103 -0
- package/docs/contributing/testing.md +95 -0
- package/docs/doc-collection/basics.md +128 -0
- package/docs/doc-collection/crawling.md +67 -0
- package/docs/doc-collection/interactive-mode.md +99 -0
- package/docs/index.json +87 -0
- package/docs/reference/commands.md +997 -0
- package/docs/reference/configuration.md +569 -0
- package/docs/reference/scripting.md +303 -0
- package/docs/reference/websocket.md +50 -0
- package/docs/superpowers/plans/2026-08-01-actor-boat.md +925 -0
- package/docs/superpowers/plans/2026-08-01-prima-boat.md +1120 -0
- package/docs/superpowers/reviews/2026-08-06-prima-vs-playwright-cli.md +268 -0
- package/docs/superpowers/specs/2026-08-01-actor-boat-design.md +204 -0
- package/docs/superpowers/specs/2026-08-01-prima-boat-design.md +242 -0
- package/docs/superpowers/specs/2026-08-03-global-config-design.md +138 -0
- package/docs/superpowers/specs/2026-08-07-prima-fixes-design.md +394 -0
- package/docs/superpowers/specs/2026-08-18-prima-false-verdicts.md +159 -0
- package/docs/web-testing/agents.md +158 -0
- package/docs/web-testing/automated-tests.md +134 -0
- package/docs/web-testing/basics.md +91 -0
- package/docs/web-testing/customization.md +131 -0
- package/docs/web-testing/hooks.md +238 -0
- package/docs/web-testing/page-interaction.md +84 -0
- package/docs/web-testing/planner.md +122 -0
- package/docs/web-testing/rerun.md +164 -0
- package/docs/web-testing/researcher.md +380 -0
- package/docs/workflow/agentic-usage.md +233 -0
- package/docs/workflow/application-spec.md +73 -0
- package/docs/workflow/ci.md +202 -0
- package/docs/workflow/knowledge.md +310 -0
- package/docs/workflow/planning-styles.md +67 -0
- package/docs/workflow/reporting.md +133 -0
- package/docs/workflow/test-plans.md +90 -0
- package/package.json +3 -2
- package/rules/navigator/verification-actions.md +20 -0
- package/src/action-result.ts +11 -0
- package/src/action.ts +43 -3
- package/src/ai/captain/web-mode.ts +6 -3
- package/src/ai/captain.ts +3 -0
- package/src/ai/navigator.ts +255 -186
- package/src/ai/pilot.ts +104 -2
- package/src/ai/provider.ts +14 -24
- package/src/ai/rerunner.ts +2 -1
- package/src/ai/researcher/cache.ts +12 -2
- package/src/ai/researcher.ts +3 -2
- package/src/ai/rules.ts +17 -10
- package/src/ai/session-analyst.ts +2 -0
- package/src/ai/task-agent.ts +3 -1
- package/src/ai/tester.ts +52 -45
- package/src/ai/tools.ts +136 -37
- package/src/commands/config-command.ts +146 -0
- package/src/commands/index.ts +2 -0
- package/src/config.ts +60 -5
- package/src/execution-controller.ts +8 -0
- package/src/explorbot.ts +7 -3
- package/src/explorer.ts +2 -2
- package/src/playwright-recorder.ts +23 -0
- package/src/remote.ts +244 -0
- package/src/reporter.ts +7 -1
- package/src/state-manager.ts +6 -2
- package/src/stats.ts +1 -0
- package/src/test-plan.ts +29 -0
- package/src/utils/aria.ts +65 -45
- package/src/utils/html.ts +1 -0
- package/src/utils/logger.ts +33 -2
- package/src/utils/page-readiness.ts +24 -1
- package/src/utils/url-matcher.ts +3 -0
- package/src/utils/web-element.ts +9 -0
- package/src/utils/web-sandbox.ts +3 -4
package/dist/src/ai/pilot.js
CHANGED
|
@@ -3,14 +3,16 @@ import dedent from 'dedent';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { ActionResult } from "../action-result.js";
|
|
5
5
|
import { ConfigParser } from "../config.js";
|
|
6
|
+
import { Stats } from "../stats.js";
|
|
6
7
|
import { TestResult } from "../test-plan.js";
|
|
7
|
-
import { collectInteractiveNodes, detectFocusArea
|
|
8
|
+
import { collectInteractiveNodes, detectFocusArea } from "../utils/aria.js";
|
|
8
9
|
import { ErrorPageError } from "../utils/error-page.js";
|
|
9
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
10
11
|
const debugLog = createDebug('explorbot:pilot');
|
|
11
12
|
import { truncateJson } from "../utils/strings.js";
|
|
12
13
|
import { capabilityGroundingRule, dataProtectionRules } from "./rules.js";
|
|
13
14
|
import { isInteractive } from "./task-agent.js";
|
|
15
|
+
import { withdrawVisionTools } from "./tools.js";
|
|
14
16
|
const CHECK_TOOLS = ['verify', 'see', 'research', 'context'];
|
|
15
17
|
const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
|
|
16
18
|
export class Pilot {
|
|
@@ -484,6 +486,91 @@ export class Pilot {
|
|
|
484
486
|
}
|
|
485
487
|
return text;
|
|
486
488
|
}
|
|
489
|
+
async settleExpectations(task, finalState) {
|
|
490
|
+
let image = null;
|
|
491
|
+
if (finalState?.screenshot && this.provider.hasVision())
|
|
492
|
+
image = `data:image/png;base64,${finalState.screenshot.toString('base64')}`;
|
|
493
|
+
const decided = (text) => {
|
|
494
|
+
if (task.hasAchievedAny() && !task.getRemainingExpectations().includes(text))
|
|
495
|
+
return 'passed';
|
|
496
|
+
return 'failed';
|
|
497
|
+
};
|
|
498
|
+
let undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
|
|
499
|
+
if (image)
|
|
500
|
+
undecided = task.expected;
|
|
501
|
+
if (!undecided.length)
|
|
502
|
+
return task.expected.map((text) => ({ text, status: decided(text) }));
|
|
503
|
+
const schema = z.object({
|
|
504
|
+
outcomes: z.array(z.object({
|
|
505
|
+
expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
|
|
506
|
+
status: z.enum(['passed', 'failed', 'unverified', 'contradiction']).describe('passed = the evidence shows it happened, failed = the evidence shows it did not, unverified = the run never established either way, contradiction = the picture and the run disagree'),
|
|
507
|
+
evidence: z.string().nullable().describe('What settled it. For a contradiction, what each side shows. Null when there is nothing to add'),
|
|
508
|
+
})),
|
|
509
|
+
});
|
|
510
|
+
let pageEvidence = '';
|
|
511
|
+
if (image) {
|
|
512
|
+
pageEvidence = dedent `
|
|
513
|
+
A screenshot of the whole page as the run left it is attached. It is the proof: an outcome is satisfied
|
|
514
|
+
when the page shows it to somebody looking at it. The log only says what the run did.
|
|
515
|
+
|
|
516
|
+
Not finding something in the picture is not by itself a disagreement. Report "contradiction" only when
|
|
517
|
+
the picture shows something incompatible with what the run claims — a list visibly empty, an error where
|
|
518
|
+
a result was expected, the old value still displayed, a control visibly disabled. When you simply cannot
|
|
519
|
+
make it out, say "unverified" and name what you could not find.
|
|
520
|
+
|
|
521
|
+
The picture covers the full page, but not the inside of a region that scrolls on its own, and not the
|
|
522
|
+
state of the page before the run ended. An outcome established earlier stays established even when the
|
|
523
|
+
page has moved past it, and that is not a contradiction.
|
|
524
|
+
`;
|
|
525
|
+
}
|
|
526
|
+
const userContent = dedent `
|
|
527
|
+
A test run has finished. Decide, for each expected outcome, what the run established about it.
|
|
528
|
+
|
|
529
|
+
<expected_outcomes>
|
|
530
|
+
${undecided.map((text) => `- ${text}`).join('\n')}
|
|
531
|
+
</expected_outcomes>
|
|
532
|
+
|
|
533
|
+
<run_log>
|
|
534
|
+
${task.notesToString() || 'No steps recorded.'}
|
|
535
|
+
</run_log>
|
|
536
|
+
|
|
537
|
+
${pageEvidence}
|
|
538
|
+
|
|
539
|
+
The log is written in the tester's own words, so an outcome can be satisfied by a step that describes it
|
|
540
|
+
differently. Judge by what the steps show happened, not by whether the wording matches.
|
|
541
|
+
Choose "unverified" only when the evidence neither shows the outcome happening nor shows it failing —
|
|
542
|
+
that is a statement about the run, not about the application.
|
|
543
|
+
`;
|
|
544
|
+
const settle = (content, model) => this.provider
|
|
545
|
+
.generateObject([{ role: 'user', content }], schema, model, {
|
|
546
|
+
agentName: 'pilot',
|
|
547
|
+
telemetry: { functionId: 'pilot.settleExpectations' },
|
|
548
|
+
})
|
|
549
|
+
.catch(() => null);
|
|
550
|
+
let response = null;
|
|
551
|
+
if (image) {
|
|
552
|
+
const seen = [
|
|
553
|
+
{ type: 'text', text: userContent },
|
|
554
|
+
{ type: 'file', mediaType: 'image/png', data: image },
|
|
555
|
+
];
|
|
556
|
+
response = await settle(seen, this.provider.getVisionModel());
|
|
557
|
+
if (!response) {
|
|
558
|
+
Stats.visionDisabled = true;
|
|
559
|
+
tag('warning').log('⚠️ Vision model could not judge the outcomes. Settling them from the run log instead.');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (!response)
|
|
563
|
+
response = await settle(userContent, this.provider.getAgenticModel('pilot'));
|
|
564
|
+
const judged = new Map((response?.object?.outcomes || []).map((outcome) => [outcome.expectation, outcome]));
|
|
565
|
+
return task.expected.map((text) => {
|
|
566
|
+
if (!undecided.includes(text))
|
|
567
|
+
return { text, status: decided(text) };
|
|
568
|
+
const outcome = judged.get(text);
|
|
569
|
+
if (!outcome)
|
|
570
|
+
return { text, status: 'unverified' };
|
|
571
|
+
return { text, status: outcome.status || 'unverified', evidence: outcome.evidence };
|
|
572
|
+
});
|
|
573
|
+
}
|
|
487
574
|
formatExpectations(task) {
|
|
488
575
|
const checked = task.getCheckedExpectations();
|
|
489
576
|
const remaining = task.getRemainingExpectations();
|
|
@@ -547,6 +634,7 @@ export class Pilot {
|
|
|
547
634
|
planning.learnExperience = learnExperience;
|
|
548
635
|
if (askUser)
|
|
549
636
|
planning.askUser = askUser;
|
|
637
|
+
withdrawVisionTools(planning);
|
|
550
638
|
return planning;
|
|
551
639
|
}
|
|
552
640
|
buildPreconditionTool(task) {
|
|
@@ -624,7 +712,7 @@ export class Pilot {
|
|
|
624
712
|
const lines = [];
|
|
625
713
|
lines.push(`url: ${state.url}`);
|
|
626
714
|
lines.push(`title: ${state.title || 'unknown'}`);
|
|
627
|
-
const focused =
|
|
715
|
+
const focused = state.focusedElement;
|
|
628
716
|
if (focused) {
|
|
629
717
|
const valuePart = focused.value ? ` (value: "${focused.value}")` : '';
|
|
630
718
|
lines.push(`focused: ${focused.role} "${focused.name}"${valuePart}`);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
2
2
|
import type { ModelMessage } from 'ai';
|
|
3
|
-
import type
|
|
3
|
+
import { type AIConfig } from '../config.js';
|
|
4
4
|
import { type RetryOptions } from '../utils/retry.js';
|
|
5
5
|
import { Conversation } from './conversation.js';
|
|
6
6
|
declare class AiError extends Error {
|
|
@@ -14,10 +14,10 @@ export declare class Provider {
|
|
|
14
14
|
defaultRetryOptions: RetryOptions;
|
|
15
15
|
lastConversation: Conversation | null;
|
|
16
16
|
constructor(config: AIConfig);
|
|
17
|
-
getModelName(model: any): string;
|
|
18
17
|
validateConnection(): Promise<void>;
|
|
19
18
|
getModelForAgent(agentName?: string): any;
|
|
20
19
|
getAgenticModel(agentName?: string): any;
|
|
20
|
+
getVisionModel(): any;
|
|
21
21
|
getConfiguredModels(): Record<string, string>;
|
|
22
22
|
getSystemPromptForAgent(agentName: string, currentUrl?: string): string | undefined;
|
|
23
23
|
getProviderOptionsForAgent(agentName: string): Record<string, any> | undefined;
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -3,6 +3,7 @@ import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
|
3
3
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
4
4
|
import { generateObject, generateText, isStepCount, registerTelemetry } from 'ai';
|
|
5
5
|
import { clearActivity, setActivity } from "../activity.js";
|
|
6
|
+
import { configuredModels, modelName as getModelName } from '../config.js';
|
|
6
7
|
import { executionController } from "../execution-controller.js";
|
|
7
8
|
import { Observability } from "../observability.js";
|
|
8
9
|
import { Stats } from "../stats.js";
|
|
@@ -79,9 +80,6 @@ export class Provider {
|
|
|
79
80
|
this.config = config;
|
|
80
81
|
this.initLangfuse();
|
|
81
82
|
}
|
|
82
|
-
getModelName(model) {
|
|
83
|
-
return model?.modelId || model?.model || 'unknown';
|
|
84
|
-
}
|
|
85
83
|
async validateConnection() {
|
|
86
84
|
try {
|
|
87
85
|
await generateText({
|
|
@@ -108,16 +106,13 @@ export class Provider {
|
|
|
108
106
|
}
|
|
109
107
|
return this.config.agenticModel || this.config.model;
|
|
110
108
|
}
|
|
109
|
+
getVisionModel() {
|
|
110
|
+
return this.config.visionModel;
|
|
111
|
+
}
|
|
111
112
|
getConfiguredModels() {
|
|
112
|
-
const models = {
|
|
113
|
-
|
|
114
|
-
models
|
|
115
|
-
if (this.config.visionModel)
|
|
116
|
-
models.visionModel = this.getModelName(this.config.visionModel);
|
|
117
|
-
for (const [agent, agentConfig] of Object.entries(this.config.agents || {})) {
|
|
118
|
-
if (agentConfig?.model)
|
|
119
|
-
models[agent] = this.getModelName(agentConfig.model);
|
|
120
|
-
}
|
|
113
|
+
const models = {};
|
|
114
|
+
for (const [role, model] of Object.entries(configuredModels(this.config)))
|
|
115
|
+
models[role] = model.name;
|
|
121
116
|
return models;
|
|
122
117
|
}
|
|
123
118
|
getSystemPromptForAgent(agentName, currentUrl) {
|
|
@@ -207,11 +202,7 @@ export class Provider {
|
|
|
207
202
|
return retry(reduced.messages, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
208
203
|
}
|
|
209
204
|
initLangfuse() {
|
|
210
|
-
const
|
|
211
|
-
const publicKey = langfuseConfig?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
212
|
-
const secretKey = langfuseConfig?.secretKey || process.env.LANGFUSE_SECRET_KEY;
|
|
213
|
-
const baseUrl = langfuseConfig?.baseUrl || process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST;
|
|
214
|
-
const enabled = langfuseConfig?.enabled ?? Boolean(publicKey && secretKey);
|
|
205
|
+
const { enabled, publicKey, secretKey, baseUrl } = this.config.langfuse || {};
|
|
215
206
|
if (!enabled || !publicKey || !secretKey) {
|
|
216
207
|
return;
|
|
217
208
|
}
|
|
@@ -282,7 +273,7 @@ export class Provider {
|
|
|
282
273
|
return { conversation, response, toolExecutions };
|
|
283
274
|
}
|
|
284
275
|
async chat(messages, model, options = {}) {
|
|
285
|
-
const modelName =
|
|
276
|
+
const modelName = getModelName(model);
|
|
286
277
|
setActivity(`🤖 Asking ${modelName}`, 'ai');
|
|
287
278
|
promptLog(`Using model: ${modelName}`);
|
|
288
279
|
const config = this.buildGenerateConfig({ maxOutputTokens: 16384 }, { model, abortSignal: executionController.getAbortSignal() }, options);
|
|
@@ -290,6 +281,7 @@ export class Provider {
|
|
|
290
281
|
try {
|
|
291
282
|
const response = await withRetry(async () => {
|
|
292
283
|
const result = await generateText({ messages, ...config });
|
|
284
|
+
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
293
285
|
if (!result.text) {
|
|
294
286
|
debugLog(result);
|
|
295
287
|
if (result.finishReason === 'length') {
|
|
@@ -304,7 +296,6 @@ export class Provider {
|
|
|
304
296
|
}, this.getRetryOptions(options));
|
|
305
297
|
clearActivity();
|
|
306
298
|
responseLog(response.text);
|
|
307
|
-
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
308
299
|
return response;
|
|
309
300
|
}
|
|
310
301
|
catch (error) {
|
|
@@ -324,7 +315,7 @@ export class Provider {
|
|
|
324
315
|
}
|
|
325
316
|
}
|
|
326
317
|
async generateWithTools(messages, model, tools, options = {}) {
|
|
327
|
-
const modelName =
|
|
318
|
+
const modelName = getModelName(model);
|
|
328
319
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
329
320
|
promptLog(`Using model: ${modelName}`);
|
|
330
321
|
const toolNames = Object.keys(tools || {});
|
|
@@ -340,6 +331,7 @@ export class Provider {
|
|
|
340
331
|
try {
|
|
341
332
|
const response = await withRetry(async () => {
|
|
342
333
|
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000));
|
|
334
|
+
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
343
335
|
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
344
336
|
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
345
337
|
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
@@ -355,7 +347,6 @@ export class Provider {
|
|
|
355
347
|
});
|
|
356
348
|
}
|
|
357
349
|
responseLog(response.text);
|
|
358
|
-
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
359
350
|
return response;
|
|
360
351
|
}
|
|
361
352
|
catch (error) {
|
|
@@ -379,7 +370,7 @@ export class Provider {
|
|
|
379
370
|
}
|
|
380
371
|
async generateObject(messages, schema, model, options = {}) {
|
|
381
372
|
const modelToUse = model || this.config.model;
|
|
382
|
-
const modelName =
|
|
373
|
+
const modelName = getModelName(modelToUse);
|
|
383
374
|
setActivity(`🤖 Asking ${modelName} for structured output`, 'ai');
|
|
384
375
|
promptLog(`Using model: ${modelName}`);
|
|
385
376
|
const config = this.buildGenerateConfig({ schema }, { model: modelToUse }, options);
|
|
@@ -557,7 +548,7 @@ export class Provider {
|
|
|
557
548
|
}, this.getRetryOptions());
|
|
558
549
|
clearActivity();
|
|
559
550
|
responseLog(response.text);
|
|
560
|
-
this.recordUsage('vision',
|
|
551
|
+
this.recordUsage('vision', getModelName(this.config.visionModel), response.usage);
|
|
561
552
|
return response;
|
|
562
553
|
}
|
|
563
554
|
catch (error) {
|
package/dist/src/ai/rerunner.js
CHANGED
|
@@ -21,7 +21,7 @@ import { RulesLoader } from "../utils/rules-loader.js";
|
|
|
21
21
|
import { toolExecutionLabel } from "./conversation.js";
|
|
22
22
|
import { actionRule, locatorRule, sectionContextRule } from "./rules.js";
|
|
23
23
|
import { TaskAgent } from "./task-agent.js";
|
|
24
|
-
import { createCodeceptJSTools } from "./tools.js";
|
|
24
|
+
import { createCodeceptJSTools, withdrawVisionTools } from "./tools.js";
|
|
25
25
|
const debugLog = createDebug('explorbot:rerunner');
|
|
26
26
|
export class Rerunner extends TaskAgent {
|
|
27
27
|
ACTION_TOOLS = ['click', 'pressKey', 'form'];
|
|
@@ -322,6 +322,7 @@ export class Rerunner extends TaskAgent {
|
|
|
322
322
|
},
|
|
323
323
|
}),
|
|
324
324
|
};
|
|
325
|
+
withdrawVisionTools(tools);
|
|
325
326
|
const conversation = this.provider.startConversation(this.getHealSystemPrompt(), 'rerunner');
|
|
326
327
|
conversation.addUserText(this.getHealUserPrompt(failedCode, error));
|
|
327
328
|
await loop(async ({ stop }) => {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export declare function researchPath(hash: string): string;
|
|
2
|
+
export declare function reportResearch(hash: string, text: string): void;
|
|
1
3
|
export declare function clearResearchCache(): void;
|
|
2
4
|
export declare function getCachedResearch(hash: string): string;
|
|
3
5
|
export declare function getPreviousResearch(hash: string): string;
|
|
@@ -4,6 +4,7 @@ import { Worker } from 'node:worker_threads';
|
|
|
4
4
|
import { outputPath } from "../../config.js";
|
|
5
5
|
import { TTLCache } from "../../utils/cache.js";
|
|
6
6
|
import { computeHtmlFingerprint } from "../../utils/html-diff.js";
|
|
7
|
+
import { tag } from "../../utils/logger.js";
|
|
7
8
|
import { debugLog } from "./mixin.js";
|
|
8
9
|
const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
9
10
|
const FINGERPRINT_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
@@ -11,6 +12,12 @@ const FINGERPRINT_WORKER_TIMEOUT_MS = 10_000;
|
|
|
11
12
|
const SIMILARITY_THRESHOLD = 90;
|
|
12
13
|
const memoryCache = new TTLCache(CACHE_TTL_MS);
|
|
13
14
|
let fingerprintWorker = null;
|
|
15
|
+
export function researchPath(hash) {
|
|
16
|
+
return outputPath('research', `${hash}.md`);
|
|
17
|
+
}
|
|
18
|
+
export function reportResearch(hash, text) {
|
|
19
|
+
tag('data').log('research', { path: researchPath(hash), hash, content: text });
|
|
20
|
+
}
|
|
14
21
|
function getStatesDir() {
|
|
15
22
|
return outputPath('states');
|
|
16
23
|
}
|
|
@@ -30,7 +37,7 @@ export function getCachedResearch(hash) {
|
|
|
30
37
|
const cached = memoryCache.get(hash);
|
|
31
38
|
if (cached !== undefined)
|
|
32
39
|
return cached;
|
|
33
|
-
const researchFile =
|
|
40
|
+
const researchFile = researchPath(hash);
|
|
34
41
|
if (!existsSync(researchFile))
|
|
35
42
|
return '';
|
|
36
43
|
const stats = statSync(researchFile);
|
|
@@ -43,7 +50,7 @@ export function getCachedResearch(hash) {
|
|
|
43
50
|
export function getPreviousResearch(hash) {
|
|
44
51
|
if (!hash)
|
|
45
52
|
return '';
|
|
46
|
-
const researchFile =
|
|
53
|
+
const researchFile = researchPath(hash);
|
|
47
54
|
if (!existsSync(researchFile))
|
|
48
55
|
return '';
|
|
49
56
|
return readFileSync(researchFile, 'utf8');
|
|
@@ -55,6 +62,7 @@ export function saveResearch(hash, text, combinedHtml) {
|
|
|
55
62
|
mkdirSync(researchDir, { recursive: true });
|
|
56
63
|
writeFileSync(researchFile, text);
|
|
57
64
|
memoryCache.set(hash, text);
|
|
65
|
+
reportResearch(hash, text);
|
|
58
66
|
debugLog(`Research saved to ${researchFile}`);
|
|
59
67
|
if (combinedHtml) {
|
|
60
68
|
const statesDir = getStatesDir();
|
|
@@ -13,7 +13,7 @@ import { mdq } from "../utils/markdown-query.js";
|
|
|
13
13
|
import { RulesLoader } from "../utils/rules-loader.js";
|
|
14
14
|
import { annotatePageElements } from "../utils/web-annotate.js";
|
|
15
15
|
import { ContextLengthError } from './provider.js';
|
|
16
|
-
import { findSimilarResearch, getCachedResearch, saveResearch } from "./researcher/cache.js";
|
|
16
|
+
import { findSimilarResearch, getCachedResearch, reportResearch, saveResearch } from "./researcher/cache.js";
|
|
17
17
|
import { WithCoordinates } from "./researcher/coordinates.js";
|
|
18
18
|
import { WithDeepAnalysis } from "./researcher/deep-analysis.js";
|
|
19
19
|
import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from "./researcher/focus.js";
|
|
@@ -76,7 +76,8 @@ export class Researcher extends ResearcherBase {
|
|
|
76
76
|
const cached = getCachedResearch(stateHash);
|
|
77
77
|
if (cached) {
|
|
78
78
|
debugLog('Previous research result found');
|
|
79
|
-
|
|
79
|
+
reportResearch(stateHash, cached);
|
|
80
|
+
return cached;
|
|
80
81
|
}
|
|
81
82
|
}
|
|
82
83
|
Stats.researches++;
|
package/dist/src/ai/rules.js
CHANGED
|
@@ -2,13 +2,18 @@ import dedent from 'dedent';
|
|
|
2
2
|
export const recommendedCodeceptCommands = ['I.click', 'I.type', 'I.fillField', 'I.see', 'I.seeElement'];
|
|
3
3
|
const locatorPriorityRule = dedent `
|
|
4
4
|
<locator_priority>
|
|
5
|
+
When the page context shows the element a ref, such as [ref=e14], there is no locator to select: click it with clickRef
|
|
6
|
+
and that ref. A ref names one exact element, so it never matches the wrong one and never has to be narrowed. Everything
|
|
7
|
+
below is for elements the context gives no ref for.
|
|
8
|
+
|
|
5
9
|
Use the following priority when selecting locators:
|
|
6
10
|
|
|
7
11
|
1. ARIA locators (first choice) - target browser's accessibility tree, most reliable
|
|
8
12
|
Use JSON format: { "role": "button", "text": "Login" }
|
|
9
|
-
|
|
13
|
+
Copy role and text VERBATIM from the ARIA snapshot or UI map — never guess the pair.
|
|
14
|
+
If the element is absent from the snapshot, do not invent one; use text or CSS instead.
|
|
10
15
|
|
|
11
|
-
2. Text locators (second choice) - use only when
|
|
16
|
+
2. Text locators (second choice) - exact visible text, use only when unique on the page
|
|
12
17
|
Example: 'Login', 'Submit', 'Username'
|
|
13
18
|
Skip if the same text appears multiple times on the page
|
|
14
19
|
|
|
@@ -23,10 +28,10 @@ const locatorPriorityRule = dedent `
|
|
|
23
28
|
`;
|
|
24
29
|
const contextSimplificationRule = dedent `
|
|
25
30
|
<context_simplification>
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
-
|
|
31
|
+
- Add a container when the target may match several elements, or the UI map gives a verified
|
|
32
|
+
section container: I.click('Save', '.modal')
|
|
33
|
+
- Skip the container when the locator is already unique (XPath, unique ID, unique text)
|
|
34
|
+
- A wrong or stale container guarantees failure: always add one fallback command WITHOUT a container
|
|
30
35
|
- No need for complex ARIA when container narrows scope sufficiently
|
|
31
36
|
</context_simplification>
|
|
32
37
|
`;
|
|
@@ -55,7 +60,7 @@ const locatorStrategyRule = dedent `
|
|
|
55
60
|
|
|
56
61
|
<good_aria_locator_example>
|
|
57
62
|
{ "role": "button", "text": "Login" },
|
|
58
|
-
{ "role": "
|
|
63
|
+
{ "role": "textbox", "text": "Name" },
|
|
59
64
|
{ "role": "link", "text": "Forgot your password?" },
|
|
60
65
|
{ "role": "link", "text": "Sign Up" },
|
|
61
66
|
{ "role": "button", "text": "Sign In" },
|
|
@@ -101,7 +106,7 @@ const locatorStrategyRule = dedent `
|
|
|
101
106
|
</xpath_rules>
|
|
102
107
|
|
|
103
108
|
<good locator example>
|
|
104
|
-
'
|
|
109
|
+
'input[placeholder="Name"]'
|
|
105
110
|
'[aria-label="Name"]'
|
|
106
111
|
'form#user_form input[name="name"]'
|
|
107
112
|
'#content-top #user_name'
|
|
@@ -231,7 +236,8 @@ export const unexpectedPopupRule = dedent `
|
|
|
231
236
|
`;
|
|
232
237
|
export const sectionContextRule = dedent `
|
|
233
238
|
<section_context_rule>
|
|
234
|
-
|
|
239
|
+
Use a container from UI map sections when the target may match several elements. A container that is
|
|
240
|
+
wrong or stale guarantees failure, so prefer a verified one and keep a containerless fallback.
|
|
235
241
|
|
|
236
242
|
1. Identify which section contains the target element
|
|
237
243
|
2. Get the Context Locator from that section in the UI map
|
|
@@ -249,9 +255,10 @@ export const sectionContextRule = dedent `
|
|
|
249
255
|
For CSS locators - prepend section context:
|
|
250
256
|
- I.click('.main button.submit') // instead of I.click('button.submit')
|
|
251
257
|
|
|
252
|
-
|
|
258
|
+
Omit context when the locator already resolves to one element:
|
|
253
259
|
- Locator is XPath (already includes path context)
|
|
254
260
|
- Locator is a unique ID (#specific-element)
|
|
261
|
+
- Text or ARIA that appears only once on the page
|
|
255
262
|
</section_context_rule>
|
|
256
263
|
|
|
257
264
|
${unexpectedPopupRule}
|
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import dedent from 'dedent';
|
|
4
4
|
import { outputPath } from "../config.js";
|
|
5
5
|
import { Stats } from "../stats.js";
|
|
6
|
+
import { tag } from "../utils/logger.js";
|
|
6
7
|
export class SessionAnalyst {
|
|
7
8
|
emoji = '🧐';
|
|
8
9
|
provider;
|
|
@@ -103,6 +104,7 @@ export class SessionAnalyst {
|
|
|
103
104
|
if (!existsSync(dir))
|
|
104
105
|
mkdirSync(dir, { recursive: true });
|
|
105
106
|
writeFileSync(filePath, markdown);
|
|
107
|
+
tag('data').log('report', { path: filePath, content: markdown });
|
|
106
108
|
return filePath;
|
|
107
109
|
}
|
|
108
110
|
serializeTest(test, ref) {
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { executionController } from "../execution-controller.js";
|
|
1
2
|
import { HooksRunner } from "../utils/hooks-runner.js";
|
|
2
3
|
export function isInteractive() {
|
|
3
|
-
|
|
4
|
+
if (process.env.INK_RUNNING === 'true')
|
|
5
|
+
return true;
|
|
6
|
+
return executionController.hasInputCallback();
|
|
4
7
|
}
|
|
5
8
|
function createNullProxy() {
|
|
6
9
|
return new Proxy({}, {
|
package/dist/src/ai/tester.d.ts
CHANGED
|
@@ -31,7 +31,6 @@ export declare class Tester extends TaskAgent implements Agent {
|
|
|
31
31
|
seenUiMapUrls: Set<string>;
|
|
32
32
|
lastAnalyzedStateHash: string | null;
|
|
33
33
|
stalledIterations: number;
|
|
34
|
-
hasSuccessfulAssertion: boolean;
|
|
35
34
|
readonly MAX_STALLED_ITERATIONS = 3;
|
|
36
35
|
constructor(deps: AgentDeps, researcher: Researcher, navigator: Navigator, agentTools?: any);
|
|
37
36
|
getNavigator(): Navigator;
|
|
@@ -40,16 +39,17 @@ export declare class Tester extends TaskAgent implements Agent {
|
|
|
40
39
|
getCurrentState(): ActionResult;
|
|
41
40
|
get progressCheckInterval(): number;
|
|
42
41
|
getConversation(): Conversation | null;
|
|
43
|
-
test(task: Test): Promise<{
|
|
42
|
+
test(task: Test, opts?: TestOptions): Promise<{
|
|
44
43
|
success: boolean;
|
|
45
44
|
}>;
|
|
46
|
-
runTestSession(task: Test, initialState: ActionResult, conversation: Conversation, handlers: TestSessionHandlers): Promise<{
|
|
45
|
+
runTestSession(task: Test, initialState: ActionResult, conversation: Conversation, handlers: TestSessionHandlers, opts: TestOptions): Promise<{
|
|
47
46
|
success: boolean;
|
|
48
47
|
}>;
|
|
49
48
|
shouldAnalyzeProgress(iteration: number, currentState: ActionResult): boolean;
|
|
50
49
|
shouldStopForStalledExecution(task: Test, previousState: ActionResult, toolExecutions: any[]): boolean;
|
|
51
50
|
prepareInstructionsForNextStep(task: Test): Promise<string>;
|
|
52
51
|
reinjectContextIfNeeded(iteration: number, currentState: ActionResult): Promise<string>;
|
|
52
|
+
interactiveAriaWithRefs(state: ActionResult): Promise<string>;
|
|
53
53
|
finishTest(task: Test): void;
|
|
54
54
|
abortStartedTestOnErrorPage(task: Test, actionResult: ActionResult): Promise<{
|
|
55
55
|
success: boolean;
|
|
@@ -93,4 +93,7 @@ export declare class Tester extends TaskAgent implements Agent {
|
|
|
93
93
|
interface TestSessionHandlers {
|
|
94
94
|
offFailedRequest?: () => void;
|
|
95
95
|
}
|
|
96
|
+
export interface TestOptions {
|
|
97
|
+
startOnCurrentPage?: boolean;
|
|
98
|
+
}
|
|
96
99
|
export {};
|