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/src/ai/pilot.ts
CHANGED
|
@@ -7,8 +7,9 @@ import { ConfigParser } from '../config.ts';
|
|
|
7
7
|
import type Explorer from '../explorer.ts';
|
|
8
8
|
import type { PlaywrightRecorder } from '../playwright-recorder.ts';
|
|
9
9
|
import type { StateManager } from '../state-manager.ts';
|
|
10
|
+
import { Stats } from '../stats.ts';
|
|
10
11
|
import { type Test, TestResult } from '../test-plan.ts';
|
|
11
|
-
import { collectInteractiveNodes, detectFocusArea
|
|
12
|
+
import { collectInteractiveNodes, detectFocusArea } from '../utils/aria.ts';
|
|
12
13
|
import { ErrorPageError } from '../utils/error-page.ts';
|
|
13
14
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
14
15
|
|
|
@@ -22,6 +23,7 @@ import type { Provider } from './provider.ts';
|
|
|
22
23
|
import type { Researcher } from './researcher.ts';
|
|
23
24
|
import { capabilityGroundingRule, dataProtectionRules } from './rules.ts';
|
|
24
25
|
import { isInteractive } from './task-agent.ts';
|
|
26
|
+
import { withdrawVisionTools } from './tools.ts';
|
|
25
27
|
|
|
26
28
|
const CHECK_TOOLS = ['verify', 'see', 'research', 'context'];
|
|
27
29
|
const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
|
|
@@ -561,6 +563,97 @@ export class Pilot implements Agent {
|
|
|
561
563
|
return text;
|
|
562
564
|
}
|
|
563
565
|
|
|
566
|
+
async settleExpectations(task: Test, finalState?: ActionResult): Promise<SettledExpectation[]> {
|
|
567
|
+
let image: string | null = null;
|
|
568
|
+
if (finalState?.screenshot && this.provider.hasVision()) image = `data:image/png;base64,${finalState.screenshot.toString('base64')}`;
|
|
569
|
+
|
|
570
|
+
const decided = (text: string): 'passed' | 'failed' => {
|
|
571
|
+
if (task.hasAchievedAny() && !task.getRemainingExpectations().includes(text)) return 'passed';
|
|
572
|
+
return 'failed';
|
|
573
|
+
};
|
|
574
|
+
|
|
575
|
+
let undecided = task.expected.filter((text) => !task.getCheckedExpectations().includes(text));
|
|
576
|
+
if (image) undecided = task.expected;
|
|
577
|
+
if (!undecided.length) return task.expected.map((text) => ({ text, status: decided(text) }));
|
|
578
|
+
|
|
579
|
+
const schema = z.object({
|
|
580
|
+
outcomes: z.array(
|
|
581
|
+
z.object({
|
|
582
|
+
expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
|
|
583
|
+
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'),
|
|
584
|
+
evidence: z.string().nullable().describe('What settled it. For a contradiction, what each side shows. Null when there is nothing to add'),
|
|
585
|
+
})
|
|
586
|
+
),
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
let pageEvidence = '';
|
|
590
|
+
if (image) {
|
|
591
|
+
pageEvidence = dedent`
|
|
592
|
+
A screenshot of the whole page as the run left it is attached. It is the proof: an outcome is satisfied
|
|
593
|
+
when the page shows it to somebody looking at it. The log only says what the run did.
|
|
594
|
+
|
|
595
|
+
Not finding something in the picture is not by itself a disagreement. Report "contradiction" only when
|
|
596
|
+
the picture shows something incompatible with what the run claims — a list visibly empty, an error where
|
|
597
|
+
a result was expected, the old value still displayed, a control visibly disabled. When you simply cannot
|
|
598
|
+
make it out, say "unverified" and name what you could not find.
|
|
599
|
+
|
|
600
|
+
The picture covers the full page, but not the inside of a region that scrolls on its own, and not the
|
|
601
|
+
state of the page before the run ended. An outcome established earlier stays established even when the
|
|
602
|
+
page has moved past it, and that is not a contradiction.
|
|
603
|
+
`;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const userContent = dedent`
|
|
607
|
+
A test run has finished. Decide, for each expected outcome, what the run established about it.
|
|
608
|
+
|
|
609
|
+
<expected_outcomes>
|
|
610
|
+
${undecided.map((text) => `- ${text}`).join('\n')}
|
|
611
|
+
</expected_outcomes>
|
|
612
|
+
|
|
613
|
+
<run_log>
|
|
614
|
+
${task.notesToString() || 'No steps recorded.'}
|
|
615
|
+
</run_log>
|
|
616
|
+
|
|
617
|
+
${pageEvidence}
|
|
618
|
+
|
|
619
|
+
The log is written in the tester's own words, so an outcome can be satisfied by a step that describes it
|
|
620
|
+
differently. Judge by what the steps show happened, not by whether the wording matches.
|
|
621
|
+
Choose "unverified" only when the evidence neither shows the outcome happening nor shows it failing —
|
|
622
|
+
that is a statement about the run, not about the application.
|
|
623
|
+
`;
|
|
624
|
+
|
|
625
|
+
const settle = (content: any, model: any) =>
|
|
626
|
+
this.provider
|
|
627
|
+
.generateObject([{ role: 'user' as const, content }], schema, model, {
|
|
628
|
+
agentName: 'pilot',
|
|
629
|
+
telemetry: { functionId: 'pilot.settleExpectations' },
|
|
630
|
+
})
|
|
631
|
+
.catch(() => null);
|
|
632
|
+
|
|
633
|
+
let response = null;
|
|
634
|
+
if (image) {
|
|
635
|
+
const seen = [
|
|
636
|
+
{ type: 'text', text: userContent },
|
|
637
|
+
{ type: 'file', mediaType: 'image/png', data: image },
|
|
638
|
+
];
|
|
639
|
+
response = await settle(seen, this.provider.getVisionModel());
|
|
640
|
+
if (!response) {
|
|
641
|
+
Stats.visionDisabled = true;
|
|
642
|
+
tag('warning').log('⚠️ Vision model could not judge the outcomes. Settling them from the run log instead.');
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (!response) response = await settle(userContent, this.provider.getAgenticModel('pilot'));
|
|
647
|
+
|
|
648
|
+
const judged = new Map((response?.object?.outcomes || []).map((outcome: any) => [outcome.expectation, outcome]));
|
|
649
|
+
return task.expected.map((text) => {
|
|
650
|
+
if (!undecided.includes(text)) return { text, status: decided(text) };
|
|
651
|
+
const outcome = judged.get(text) as { status: SettledStatus; evidence?: string } | undefined;
|
|
652
|
+
if (!outcome) return { text, status: 'unverified' as SettledStatus };
|
|
653
|
+
return { text, status: outcome.status || 'unverified', evidence: outcome.evidence };
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
|
|
564
657
|
private formatExpectations(task: Test): string {
|
|
565
658
|
const checked = task.getCheckedExpectations();
|
|
566
659
|
const remaining = task.getRemainingExpectations();
|
|
@@ -620,6 +713,7 @@ export class Pilot implements Agent {
|
|
|
620
713
|
if (xpathCheck) planning.xpathCheck = xpathCheck;
|
|
621
714
|
if (learnExperience) planning.learnExperience = learnExperience;
|
|
622
715
|
if (askUser) planning.askUser = askUser;
|
|
716
|
+
withdrawVisionTools(planning);
|
|
623
717
|
return planning;
|
|
624
718
|
}
|
|
625
719
|
|
|
@@ -703,7 +797,7 @@ export class Pilot implements Agent {
|
|
|
703
797
|
lines.push(`url: ${state.url}`);
|
|
704
798
|
lines.push(`title: ${state.title || 'unknown'}`);
|
|
705
799
|
|
|
706
|
-
const focused =
|
|
800
|
+
const focused = state.focusedElement;
|
|
707
801
|
if (focused) {
|
|
708
802
|
const valuePart = focused.value ? ` (value: "${focused.value}")` : '';
|
|
709
803
|
lines.push(`focused: ${focused.role} "${focused.name}"${valuePart}`);
|
|
@@ -1065,3 +1159,11 @@ export class Pilot implements Agent {
|
|
|
1065
1159
|
`;
|
|
1066
1160
|
}
|
|
1067
1161
|
}
|
|
1162
|
+
|
|
1163
|
+
export type SettledStatus = 'passed' | 'failed' | 'unverified' | 'contradiction';
|
|
1164
|
+
|
|
1165
|
+
export interface SettledExpectation {
|
|
1166
|
+
text: string;
|
|
1167
|
+
status: SettledStatus;
|
|
1168
|
+
evidence?: string;
|
|
1169
|
+
}
|
package/src/ai/provider.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
|
4
4
|
import { generateObject, generateText, isStepCount, registerTelemetry } from 'ai';
|
|
5
5
|
import type { ModelMessage } from 'ai';
|
|
6
6
|
import { clearActivity, setActivity } from '../activity.ts';
|
|
7
|
-
import type
|
|
7
|
+
import { type AIConfig, configuredModels, modelName as getModelName } from '../config.js';
|
|
8
8
|
import { executionController } from '../execution-controller.ts';
|
|
9
9
|
import { Observability } from '../observability.ts';
|
|
10
10
|
import { Stats } from '../stats.ts';
|
|
@@ -88,10 +88,6 @@ export class Provider {
|
|
|
88
88
|
this.initLangfuse();
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
private getModelName(model: any): string {
|
|
92
|
-
return model?.modelId || model?.model || 'unknown';
|
|
93
|
-
}
|
|
94
|
-
|
|
95
91
|
async validateConnection(): Promise<void> {
|
|
96
92
|
try {
|
|
97
93
|
await generateText({
|
|
@@ -120,13 +116,13 @@ export class Provider {
|
|
|
120
116
|
return this.config.agenticModel || this.config.model;
|
|
121
117
|
}
|
|
122
118
|
|
|
119
|
+
getVisionModel(): any {
|
|
120
|
+
return this.config.visionModel;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
123
|
getConfiguredModels(): Record<string, string> {
|
|
124
|
-
const models: Record<string, string> = {
|
|
125
|
-
|
|
126
|
-
if (this.config.visionModel) models.visionModel = this.getModelName(this.config.visionModel);
|
|
127
|
-
for (const [agent, agentConfig] of Object.entries(this.config.agents || {})) {
|
|
128
|
-
if (agentConfig?.model) models[agent] = this.getModelName(agentConfig.model);
|
|
129
|
-
}
|
|
124
|
+
const models: Record<string, string> = {};
|
|
125
|
+
for (const [role, model] of Object.entries(configuredModels(this.config))) models[role] = model.name;
|
|
130
126
|
return models;
|
|
131
127
|
}
|
|
132
128
|
|
|
@@ -223,11 +219,7 @@ export class Provider {
|
|
|
223
219
|
}
|
|
224
220
|
|
|
225
221
|
private initLangfuse() {
|
|
226
|
-
const
|
|
227
|
-
const publicKey = langfuseConfig?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
228
|
-
const secretKey = langfuseConfig?.secretKey || process.env.LANGFUSE_SECRET_KEY;
|
|
229
|
-
const baseUrl = langfuseConfig?.baseUrl || process.env.LANGFUSE_BASE_URL || process.env.LANGFUSE_HOST;
|
|
230
|
-
const enabled = langfuseConfig?.enabled ?? Boolean(publicKey && secretKey);
|
|
222
|
+
const { enabled, publicKey, secretKey, baseUrl } = this.config.langfuse || {};
|
|
231
223
|
|
|
232
224
|
if (!enabled || !publicKey || !secretKey) {
|
|
233
225
|
return;
|
|
@@ -316,7 +308,7 @@ export class Provider {
|
|
|
316
308
|
}
|
|
317
309
|
|
|
318
310
|
async chat(messages: ModelMessage[], model: any, options: any = {}): Promise<any> {
|
|
319
|
-
const modelName =
|
|
311
|
+
const modelName = getModelName(model);
|
|
320
312
|
setActivity(`🤖 Asking ${modelName}`, 'ai');
|
|
321
313
|
promptLog(`Using model: ${modelName}`);
|
|
322
314
|
|
|
@@ -326,6 +318,7 @@ export class Provider {
|
|
|
326
318
|
try {
|
|
327
319
|
const response = await withRetry(async () => {
|
|
328
320
|
const result = await generateText({ messages, ...config });
|
|
321
|
+
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
329
322
|
if (!result.text) {
|
|
330
323
|
debugLog(result);
|
|
331
324
|
if (result.finishReason === 'length') {
|
|
@@ -342,8 +335,6 @@ export class Provider {
|
|
|
342
335
|
clearActivity();
|
|
343
336
|
responseLog(response.text);
|
|
344
337
|
|
|
345
|
-
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
346
|
-
|
|
347
338
|
return response;
|
|
348
339
|
} catch (error: any) {
|
|
349
340
|
clearActivity();
|
|
@@ -361,7 +352,7 @@ export class Provider {
|
|
|
361
352
|
}
|
|
362
353
|
|
|
363
354
|
async generateWithTools(messages: ModelMessage[], model: any, tools: any, options: any = {}): Promise<any> {
|
|
364
|
-
const modelName =
|
|
355
|
+
const modelName = getModelName(model);
|
|
365
356
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
366
357
|
promptLog(`Using model: ${modelName}`);
|
|
367
358
|
|
|
@@ -378,6 +369,7 @@ export class Provider {
|
|
|
378
369
|
try {
|
|
379
370
|
const response = await withRetry(async () => {
|
|
380
371
|
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
|
|
372
|
+
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
381
373
|
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
382
374
|
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
383
375
|
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
@@ -397,8 +389,6 @@ export class Provider {
|
|
|
397
389
|
|
|
398
390
|
responseLog(response.text);
|
|
399
391
|
|
|
400
|
-
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
401
|
-
|
|
402
392
|
return response;
|
|
403
393
|
} catch (error: any) {
|
|
404
394
|
clearActivity();
|
|
@@ -420,7 +410,7 @@ export class Provider {
|
|
|
420
410
|
|
|
421
411
|
async generateObject(messages: ModelMessage[], schema: any, model?: any, options: any = {}): Promise<any> {
|
|
422
412
|
const modelToUse = model || this.config.model;
|
|
423
|
-
const modelName =
|
|
413
|
+
const modelName = getModelName(modelToUse);
|
|
424
414
|
setActivity(`🤖 Asking ${modelName} for structured output`, 'ai');
|
|
425
415
|
promptLog(`Using model: ${modelName}`);
|
|
426
416
|
|
|
@@ -622,7 +612,7 @@ export class Provider {
|
|
|
622
612
|
clearActivity();
|
|
623
613
|
responseLog(response.text);
|
|
624
614
|
|
|
625
|
-
this.recordUsage('vision',
|
|
615
|
+
this.recordUsage('vision', getModelName(this.config.visionModel), response.usage);
|
|
626
616
|
|
|
627
617
|
return response;
|
|
628
618
|
} catch (error: any) {
|
package/src/ai/rerunner.ts
CHANGED
|
@@ -24,7 +24,7 @@ import type { Navigator } from './navigator.ts';
|
|
|
24
24
|
import { Provider } from './provider.ts';
|
|
25
25
|
import { actionRule, locatorRule, sectionContextRule } from './rules.ts';
|
|
26
26
|
import { TaskAgent } from './task-agent.ts';
|
|
27
|
-
import { createCodeceptJSTools } from './tools.ts';
|
|
27
|
+
import { createCodeceptJSTools, withdrawVisionTools } from './tools.ts';
|
|
28
28
|
|
|
29
29
|
const debugLog = createDebug('explorbot:rerunner');
|
|
30
30
|
|
|
@@ -361,6 +361,7 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
361
361
|
},
|
|
362
362
|
}),
|
|
363
363
|
};
|
|
364
|
+
withdrawVisionTools(tools);
|
|
364
365
|
|
|
365
366
|
const conversation = this.provider.startConversation(this.getHealSystemPrompt(), 'rerunner');
|
|
366
367
|
conversation.addUserText(this.getHealUserPrompt(failedCode, error));
|
|
@@ -4,6 +4,7 @@ import { Worker } from 'node:worker_threads';
|
|
|
4
4
|
import { outputPath } from '../../config.ts';
|
|
5
5
|
import { TTLCache } from '../../utils/cache.ts';
|
|
6
6
|
import { computeHtmlFingerprint } from '../../utils/html-diff.ts';
|
|
7
|
+
import { tag } from '../../utils/logger.ts';
|
|
7
8
|
import { debugLog } from './mixin.ts';
|
|
8
9
|
|
|
9
10
|
const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
@@ -15,6 +16,14 @@ const memoryCache = new TTLCache<string>(CACHE_TTL_MS);
|
|
|
15
16
|
|
|
16
17
|
let fingerprintWorker: Worker | null = null;
|
|
17
18
|
|
|
19
|
+
export function researchPath(hash: string): string {
|
|
20
|
+
return outputPath('research', `${hash}.md`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function reportResearch(hash: string, text: string): void {
|
|
24
|
+
tag('data').log('research', { path: researchPath(hash), hash, content: text });
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
function getStatesDir(): string {
|
|
19
28
|
return outputPath('states');
|
|
20
29
|
}
|
|
@@ -35,7 +44,7 @@ export function getCachedResearch(hash: string): string {
|
|
|
35
44
|
if (!hash) return '';
|
|
36
45
|
const cached = memoryCache.get(hash);
|
|
37
46
|
if (cached !== undefined) return cached;
|
|
38
|
-
const researchFile =
|
|
47
|
+
const researchFile = researchPath(hash);
|
|
39
48
|
if (!existsSync(researchFile)) return '';
|
|
40
49
|
const stats = statSync(researchFile);
|
|
41
50
|
if (Date.now() - stats.mtimeMs > CACHE_TTL_MS) return '';
|
|
@@ -46,7 +55,7 @@ export function getCachedResearch(hash: string): string {
|
|
|
46
55
|
|
|
47
56
|
export function getPreviousResearch(hash: string): string {
|
|
48
57
|
if (!hash) return '';
|
|
49
|
-
const researchFile =
|
|
58
|
+
const researchFile = researchPath(hash);
|
|
50
59
|
if (!existsSync(researchFile)) return '';
|
|
51
60
|
return readFileSync(researchFile, 'utf8');
|
|
52
61
|
}
|
|
@@ -57,6 +66,7 @@ export function saveResearch(hash: string, text: string, combinedHtml?: string):
|
|
|
57
66
|
if (!existsSync(researchDir)) mkdirSync(researchDir, { recursive: true });
|
|
58
67
|
writeFileSync(researchFile, text);
|
|
59
68
|
memoryCache.set(hash, text);
|
|
69
|
+
reportResearch(hash, text);
|
|
60
70
|
debugLog(`Research saved to ${researchFile}`);
|
|
61
71
|
|
|
62
72
|
if (combinedHtml) {
|
package/src/ai/researcher.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { annotatePageElements } from '../utils/web-annotate.ts';
|
|
|
19
19
|
import type { Agent, AgentDeps } from './agent.js';
|
|
20
20
|
import type { Navigator } from './navigator.ts';
|
|
21
21
|
import { ContextLengthError, type Provider } from './provider.js';
|
|
22
|
-
import { findSimilarResearch, getCachedResearch, saveResearch } from './researcher/cache.ts';
|
|
22
|
+
import { findSimilarResearch, getCachedResearch, reportResearch, saveResearch } from './researcher/cache.ts';
|
|
23
23
|
import { type CoordinateMethods, WithCoordinates } from './researcher/coordinates.ts';
|
|
24
24
|
import { type DeepAnalysisMethods, WithDeepAnalysis } from './researcher/deep-analysis.ts';
|
|
25
25
|
import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from './researcher/focus.ts';
|
|
@@ -102,7 +102,8 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
102
102
|
const cached = getCachedResearch(stateHash);
|
|
103
103
|
if (cached) {
|
|
104
104
|
debugLog('Previous research result found');
|
|
105
|
-
|
|
105
|
+
reportResearch(stateHash, cached);
|
|
106
|
+
return cached;
|
|
106
107
|
}
|
|
107
108
|
}
|
|
108
109
|
|
package/src/ai/rules.ts
CHANGED
|
@@ -4,13 +4,18 @@ export const recommendedCodeceptCommands = ['I.click', 'I.type', 'I.fillField',
|
|
|
4
4
|
|
|
5
5
|
const locatorPriorityRule = dedent`
|
|
6
6
|
<locator_priority>
|
|
7
|
+
When the page context shows the element a ref, such as [ref=e14], there is no locator to select: click it with clickRef
|
|
8
|
+
and that ref. A ref names one exact element, so it never matches the wrong one and never has to be narrowed. Everything
|
|
9
|
+
below is for elements the context gives no ref for.
|
|
10
|
+
|
|
7
11
|
Use the following priority when selecting locators:
|
|
8
12
|
|
|
9
13
|
1. ARIA locators (first choice) - target browser's accessibility tree, most reliable
|
|
10
14
|
Use JSON format: { "role": "button", "text": "Login" }
|
|
11
|
-
|
|
15
|
+
Copy role and text VERBATIM from the ARIA snapshot or UI map — never guess the pair.
|
|
16
|
+
If the element is absent from the snapshot, do not invent one; use text or CSS instead.
|
|
12
17
|
|
|
13
|
-
2. Text locators (second choice) - use only when
|
|
18
|
+
2. Text locators (second choice) - exact visible text, use only when unique on the page
|
|
14
19
|
Example: 'Login', 'Submit', 'Username'
|
|
15
20
|
Skip if the same text appears multiple times on the page
|
|
16
21
|
|
|
@@ -26,10 +31,10 @@ const locatorPriorityRule = dedent`
|
|
|
26
31
|
|
|
27
32
|
const contextSimplificationRule = dedent`
|
|
28
33
|
<context_simplification>
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
34
|
+
- Add a container when the target may match several elements, or the UI map gives a verified
|
|
35
|
+
section container: I.click('Save', '.modal')
|
|
36
|
+
- Skip the container when the locator is already unique (XPath, unique ID, unique text)
|
|
37
|
+
- A wrong or stale container guarantees failure: always add one fallback command WITHOUT a container
|
|
33
38
|
- No need for complex ARIA when container narrows scope sufficiently
|
|
34
39
|
</context_simplification>
|
|
35
40
|
`;
|
|
@@ -59,7 +64,7 @@ const locatorStrategyRule = dedent`
|
|
|
59
64
|
|
|
60
65
|
<good_aria_locator_example>
|
|
61
66
|
{ "role": "button", "text": "Login" },
|
|
62
|
-
{ "role": "
|
|
67
|
+
{ "role": "textbox", "text": "Name" },
|
|
63
68
|
{ "role": "link", "text": "Forgot your password?" },
|
|
64
69
|
{ "role": "link", "text": "Sign Up" },
|
|
65
70
|
{ "role": "button", "text": "Sign In" },
|
|
@@ -105,7 +110,7 @@ const locatorStrategyRule = dedent`
|
|
|
105
110
|
</xpath_rules>
|
|
106
111
|
|
|
107
112
|
<good locator example>
|
|
108
|
-
'
|
|
113
|
+
'input[placeholder="Name"]'
|
|
109
114
|
'[aria-label="Name"]'
|
|
110
115
|
'form#user_form input[name="name"]'
|
|
111
116
|
'#content-top #user_name'
|
|
@@ -245,7 +250,8 @@ export const unexpectedPopupRule = dedent`
|
|
|
245
250
|
|
|
246
251
|
export const sectionContextRule = dedent`
|
|
247
252
|
<section_context_rule>
|
|
248
|
-
|
|
253
|
+
Use a container from UI map sections when the target may match several elements. A container that is
|
|
254
|
+
wrong or stale guarantees failure, so prefer a verified one and keep a containerless fallback.
|
|
249
255
|
|
|
250
256
|
1. Identify which section contains the target element
|
|
251
257
|
2. Get the Context Locator from that section in the UI map
|
|
@@ -263,9 +269,10 @@ export const sectionContextRule = dedent`
|
|
|
263
269
|
For CSS locators - prepend section context:
|
|
264
270
|
- I.click('.main button.submit') // instead of I.click('button.submit')
|
|
265
271
|
|
|
266
|
-
|
|
272
|
+
Omit context when the locator already resolves to one element:
|
|
267
273
|
- Locator is XPath (already includes path context)
|
|
268
274
|
- Locator is a unique ID (#specific-element)
|
|
275
|
+
- Text or ARIA that appears only once on the page
|
|
269
276
|
</section_context_rule>
|
|
270
277
|
|
|
271
278
|
${unexpectedPopupRule}
|
|
@@ -4,6 +4,7 @@ import dedent from 'dedent';
|
|
|
4
4
|
import { outputPath } from '../config.ts';
|
|
5
5
|
import { Stats } from '../stats.ts';
|
|
6
6
|
import type { Test } from '../test-plan.ts';
|
|
7
|
+
import { tag } from '../utils/logger.ts';
|
|
7
8
|
import type { Agent } from './agent.ts';
|
|
8
9
|
import type { Provider } from './provider.ts';
|
|
9
10
|
|
|
@@ -117,6 +118,7 @@ export class SessionAnalyst implements Agent {
|
|
|
117
118
|
const dir = path.dirname(filePath);
|
|
118
119
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
119
120
|
writeFileSync(filePath, markdown);
|
|
121
|
+
tag('data').log('report', { path: filePath, content: markdown });
|
|
120
122
|
return filePath;
|
|
121
123
|
}
|
|
122
124
|
|
package/src/ai/task-agent.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ActionResult } from '../action-result.js';
|
|
2
2
|
import type { ExplorbotConfig } from '../config.ts';
|
|
3
|
+
import { executionController } from '../execution-controller.ts';
|
|
3
4
|
import type { ExperienceTracker } from '../experience-tracker.js';
|
|
4
5
|
import type Explorer from '../explorer.ts';
|
|
5
6
|
import type { KnowledgeTracker } from '../knowledge-tracker.js';
|
|
@@ -12,7 +13,8 @@ import type { Provider } from './provider.js';
|
|
|
12
13
|
import { Quartermaster } from './quartermaster.js';
|
|
13
14
|
|
|
14
15
|
export function isInteractive(): boolean {
|
|
15
|
-
|
|
16
|
+
if (process.env.INK_RUNNING === 'true') return true;
|
|
17
|
+
return executionController.hasInputCallback();
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
function createNullProxy<T extends object>(): T {
|