explorbot 0.4.3 → 0.4.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/boat/api-tester/src/apibot.ts +8 -13
- package/boat/api-tester/src/cli.ts +7 -3
- package/boat/api-tester/src/config.ts +45 -9
- package/boat/prima/src/cli.ts +33 -99
- package/boat/prima/src/envelope.ts +3 -1
- package/boat/prima/src/help.ts +72 -0
- package/boat/prima/src/prima.ts +41 -46
- package/dist/boat/api-tester/src/apibot.js +7 -6
- package/dist/boat/api-tester/src/cli.js +9 -3
- package/dist/boat/api-tester/src/config.js +32 -6
- package/dist/boat/prima/src/cli.js +30 -86
- package/dist/boat/prima/src/envelope.js +2 -1
- package/dist/boat/prima/src/help.js +63 -0
- package/dist/boat/prima/src/prima.js +39 -44
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +3 -0
- package/dist/src/action-result.js +5 -0
- package/dist/src/action.js +12 -1
- package/dist/src/ai/fisherman/tools.js +7 -1
- package/dist/src/ai/fisherman.js +2 -1
- package/dist/src/ai/pilot.d.ts +0 -1
- package/dist/src/ai/pilot.js +8 -24
- package/dist/src/ai/planner.d.ts +4 -0
- package/dist/src/ai/planner.js +28 -0
- package/dist/src/ai/provider.js +3 -1
- package/dist/src/ai/researcher/deep-analysis.d.ts +1 -1
- package/dist/src/ai/researcher/deep-analysis.js +4 -1
- package/dist/src/ai/researcher/sections.d.ts +1 -1
- package/dist/src/ai/researcher/sections.js +2 -1
- package/dist/src/ai/researcher.js +25 -11
- package/dist/src/ai/rules.js +8 -7
- package/dist/src/ai/scout/tools.d.ts +17 -0
- package/dist/src/ai/scout/tools.js +130 -0
- package/dist/src/ai/scout.d.ts +21 -0
- package/dist/src/ai/scout.js +150 -0
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +27 -33
- package/dist/src/ai/tools.js +61 -30
- package/dist/src/application-spec.d.ts +3 -0
- package/dist/src/application-spec.js +21 -5
- package/dist/src/commands/config-command.js +6 -2
- package/dist/src/config.d.ts +9 -1
- package/dist/src/config.js +14 -0
- package/dist/src/explorbot.d.ts +3 -0
- package/dist/src/explorbot.js +33 -0
- package/dist/src/knowledge-tracker.d.ts +1 -0
- package/dist/src/knowledge-tracker.js +3 -0
- package/dist/src/state-manager.js +5 -1
- package/dist/src/utils/aria-ref.d.ts +16 -0
- package/dist/src/utils/aria-ref.js +47 -0
- package/dist/src/utils/aria.js +3 -3
- package/dist/src/utils/web-annotate.js +3 -15
- package/dist/src/utils/web-element.d.ts +0 -2
- package/dist/src/utils/web-element.js +0 -8
- package/docs/api-testing/basics.md +12 -4
- package/docs/reference/commands.md +1 -0
- package/docs/reference/configuration.md +28 -1
- package/docs/web-testing/agents.md +9 -1
- package/docs/web-testing/planner.md +5 -0
- package/docs/workflow/agentic-usage.md +3 -1
- package/docs/workflow/application-spec.md +4 -0
- package/package.json +1 -1
- package/src/action-result.ts +7 -0
- package/src/action.ts +14 -2
- package/src/ai/fisherman/tools.ts +8 -1
- package/src/ai/fisherman.ts +2 -1
- package/src/ai/pilot.ts +8 -25
- package/src/ai/planner.ts +33 -0
- package/src/ai/provider.ts +2 -1
- package/src/ai/researcher/deep-analysis.ts +4 -2
- package/src/ai/researcher/sections.ts +2 -2
- package/src/ai/researcher.ts +28 -11
- package/src/ai/rules.ts +8 -7
- package/src/ai/scout/tools.ts +150 -0
- package/src/ai/scout.ts +173 -0
- package/src/ai/tester.ts +25 -30
- package/src/ai/tools.ts +75 -36
- package/src/application-spec.ts +22 -4
- package/src/commands/config-command.ts +4 -1
- package/src/config.ts +23 -0
- package/src/explorbot.ts +36 -0
- package/src/knowledge-tracker.ts +4 -0
- package/src/state-manager.ts +6 -1
- package/src/utils/aria-ref.ts +61 -0
- package/src/utils/aria.ts +3 -3
- package/src/utils/web-annotate.ts +3 -15
- package/src/utils/web-element.ts +0 -9
|
@@ -118,6 +118,7 @@ EXPLORBOT_AI_PROVIDER=openrouter \
|
|
|
118
118
|
| `EXPLORBOT_KNOWLEDGE_FILE` | Path to a knowledge markdown file |
|
|
119
119
|
| `EXPLORBOT_SPEC` | Docbot application spec directory or index.md, used as page knowledge |
|
|
120
120
|
| `EXPLORBOT_API_SPEC` | OpenAPI spec path for the API boat |
|
|
121
|
+
| `EXPLORBOT_API_HEADERS` | Headers sent with every API request, one "Name: value" per line |
|
|
121
122
|
| `EXPLORBOT_NO_BANNER` | Suppress the startup banner, for machine-readable output |
|
|
122
123
|
| `EXPLORBOT_MAX_DURATION` | Wall-clock budget in minutes for an explore run; same as --max-duration |
|
|
123
124
|
<!-- END env -->
|
|
@@ -179,6 +179,7 @@ Each agent takes its own model and system prompt.
|
|
|
179
179
|
| `rerunner` | Heals failing steps when re-running generated tests |
|
|
180
180
|
| `analyst` | Writes the end-of-session markdown report |
|
|
181
181
|
| `fisherman` | Prepares test data through API requests |
|
|
182
|
+
| `scout` | Retrieves relevant documentation for the Planner |
|
|
182
183
|
| `chief` | API test planning |
|
|
183
184
|
| `curler` | API test execution |
|
|
184
185
|
|
|
@@ -206,7 +207,7 @@ agents: {
|
|
|
206
207
|
| `beforeHook` | `Hook \| HookPatternMap` | Code to run before agent execution |
|
|
207
208
|
| `afterHook` | `Hook \| HookPatternMap` | Code to run after agent execution |
|
|
208
209
|
|
|
209
|
-
Some agents take extra options: `pilot` accepts `stepsToReview` (recent steps reviewed per check, default 5); `planner` accepts `styles` (see [Planning styles](#planning-styles)); `rerunner` accepts `healLimit` (max heal attempts, default 3) and `recipes` (custom heal recipes, see [Rerunning Tests](../web-testing/rerun.md)). Researcher and Historian options are documented below.
|
|
210
|
+
Some agents take extra options: `pilot` accepts `stepsToReview` (recent steps reviewed per check, default 5); `planner` accepts `styles` (see [Planning styles](#planning-styles)) and `docsWeight` (share of scenarios grounded in documentation when Scout is enabled, default 70); `scout` accepts `dirs` (see [Scout agent](#scout-agent)); `rerunner` accepts `healLimit` (max heal attempts, default 3) and `recipes` (custom heal recipes, see [Rerunning Tests](../web-testing/rerun.md)). Researcher and Historian options are documented below.
|
|
210
211
|
|
|
211
212
|
See [Agent hooks](../web-testing/hooks.md) for hook configuration.
|
|
212
213
|
|
|
@@ -257,6 +258,31 @@ See [AI providers](../basics/providers.md) for recommended models and provider s
|
|
|
257
258
|
|
|
258
259
|
Fisherman prepares test data over the API before a scenario runs, and can also answer questions about data that already exists without creating or changing anything. Pilot reaches this read-only capability through its `askApi(question)` tool, calling it to check whether suitable data is already there — or to get the exact name or id of an existing record — before deciding whether to create anything through `precondition()`. In replicate mode, where Fisherman learns the API by watching browser traffic instead of reading a spec, the read endpoints it can query come from successful GET requests observed in the browser, alongside the write endpoints already captured from XHR traffic. The endpoint list shown to the model names only the path and its query-parameter names, never their values; the underlying capture on disk holds the full request URL and headers — what write captures already hold — but no response body.
|
|
259
260
|
|
|
261
|
+
### Scout agent
|
|
262
|
+
|
|
263
|
+
Scout retrieves documentation relevant to the page being planned and hands it to the Planner as a `<docs_context>` block, so scenarios can be grounded in what the application documents say. It is opt-in and needs documentation collected beforehand:
|
|
264
|
+
|
|
265
|
+
```javascript
|
|
266
|
+
ai: {
|
|
267
|
+
agents: {
|
|
268
|
+
scout: {
|
|
269
|
+
enabled: true, // Opt in — Scout never runs without this
|
|
270
|
+
dirs: ['docs'], // Extra markdown directories to search, beyond the spec bundle
|
|
271
|
+
},
|
|
272
|
+
planner: {
|
|
273
|
+
docsWeight: 70, // Roughly 70% of scenarios exercise documented behavior, the rest explore beyond it
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
| Option | Type | Description |
|
|
280
|
+
|--------|------|-------------|
|
|
281
|
+
| `enabled` | `boolean` | Turn Scout on. Default: off. |
|
|
282
|
+
| `dirs` | `string[]` | Markdown directories added to the corpus, resolved relative to the project |
|
|
283
|
+
|
|
284
|
+
The corpus combines the [application spec](../workflow/application-spec.md) bundle (`--spec` / `EXPLORBOT_SPEC` / `dirs.spec`, set by `explorbot docs collect`) with the `dirs` above. Scout scans it with the same `bash` + `readFile` tools Captain uses: the corpus is loaded into an in-memory sandbox and the model itself runs `rg` (or `grep` — whichever is installed) to explore it. One of the two must be on PATH — Scout fails loudly when neither is found. Pages already injected for the current URL as `<application_spec>` are excluded from the Scout corpus, so the two blocks never duplicate each other. Files under `dirs` that carry no page URL are listed as hand-written notes for Scout to inspect when they are relevant.
|
|
285
|
+
|
|
260
286
|
## Playwright settings
|
|
261
287
|
|
|
262
288
|
### Browser selection
|
|
@@ -512,6 +538,7 @@ export default {
|
|
|
512
538
|
quartermaster: { /* ... */ },
|
|
513
539
|
historian: { /* ... */ },
|
|
514
540
|
fisherman: { /* ... */ },
|
|
541
|
+
scout: { enabled: true, dirs: ['docs'] }, // Documentation retrieval for the Planner
|
|
515
542
|
rerunner: { /* ... */ },
|
|
516
543
|
analyst: { /* ... */ },
|
|
517
544
|
},
|
|
@@ -44,7 +44,7 @@ See [Researcher Agent](./researcher.md) for configuration and usage.
|
|
|
44
44
|
|
|
45
45
|
Generates test scenarios from research findings.
|
|
46
46
|
|
|
47
|
-
The Planner writes business-focused scenarios with priority levels (critical/important/high/normal/low) and expected outcomes for verification. It balances positive and negative cases, skips scenarios you already have, and cycles through planning styles (normal, psycho, curious) to broaden coverage across iterations. You can add your own styles and page-specific rules.
|
|
47
|
+
The Planner writes business-focused scenarios with priority levels (critical/important/high/normal/low) and expected outcomes for verification. It balances positive and negative cases, skips scenarios you already have, and cycles through planning styles (normal, psycho, curious) to broaden coverage across iterations. You can add your own styles and page-specific rules. With the Scout agent enabled, it also plans from collected documentation, weighted by `docsWeight`.
|
|
48
48
|
|
|
49
49
|
Commands that use Planner:
|
|
50
50
|
- `/plan [--focus <feature>]`
|
|
@@ -52,6 +52,14 @@ Commands that use Planner:
|
|
|
52
52
|
|
|
53
53
|
See [Planner Agent](./planner.md) for planning styles, customization, and configuration.
|
|
54
54
|
|
|
55
|
+
## Scout Agent
|
|
56
|
+
|
|
57
|
+
Retrieves documentation relevant to the page being planned.
|
|
58
|
+
|
|
59
|
+
Scout searches the collected documentation corpus — the [application spec](../workflow/application-spec.md) from `explorbot docs collect` plus any extra markdown directories you configure — and reports the documented capabilities, states and transitions that matter for the current page and focus. The Planner receives them as a `<docs_context>` block and grounds part of its scenarios in them. Pages already injected as `<application_spec>` for the current URL are not repeated. Scout is opt-in (`ai.agents.scout.enabled`) and searches with ripgrep or grep, falling back to an in-process scan when neither is installed.
|
|
60
|
+
|
|
61
|
+
See [Configuration: Scout agent](../reference/configuration.md#scout-agent).
|
|
62
|
+
|
|
55
63
|
## Tester Agent
|
|
56
64
|
|
|
57
65
|
Runs the planned scenarios.
|
|
@@ -53,6 +53,11 @@ ai: {
|
|
|
53
53
|
| `styles` | `string[]` | `['normal', 'curious', 'psycho']` | Style names and cycling order |
|
|
54
54
|
| `rules` | `RuleEntry[]` | `[]` | URL-aware rule files from `rules/planner/` |
|
|
55
55
|
| `systemPrompt` | `string` | - | Inline instructions appended to the prompt |
|
|
56
|
+
| `docsWeight` | `number` | `70` | With Scout enabled, the rough share of scenarios exercising documented behavior; the rest explore beyond the documentation |
|
|
57
|
+
|
|
58
|
+
## Planning from documentation
|
|
59
|
+
|
|
60
|
+
With the [Scout agent](../reference/configuration.md#scout-agent) enabled, the Planner also receives a `<docs_context>` block — capabilities, states and transitions retrieved from collected documentation that are relevant to the current page and focus. `docsWeight` steers the mix: at `70` roughly seven of ten scenarios exercise documented behavior and three explore what the documentation does not cover. Set it to `100` for documentation-only planning, or lower it to lean on the Planner's own reading of the page. Pages whose documentation is already injected as `<application_spec>` are not repeated in `<docs_context>`.
|
|
56
61
|
|
|
57
62
|
## Planning Styles
|
|
58
63
|
|
|
@@ -58,6 +58,7 @@ No `init`, no config file, no project directory, no model IDs to look up. These
|
|
|
58
58
|
| `EXPLORBOT_KNOWLEDGE_FILE` | no | Path to a knowledge markdown file |
|
|
59
59
|
| `EXPLORBOT_SPEC` | no | Docbot application spec directory or index.md, used as page knowledge |
|
|
60
60
|
| `EXPLORBOT_API_SPEC` | no | OpenAPI spec path for the API boat |
|
|
61
|
+
| `EXPLORBOT_API_HEADERS` | no | Headers sent with every API request, one "Name: value" per line |
|
|
61
62
|
| `EXPLORBOT_NO_BANNER` | no | Suppress the startup banner, for machine-readable output |
|
|
62
63
|
| `EXPLORBOT_MAX_DURATION` | no | Wall-clock budget in minutes for an explore run; same as --max-duration |
|
|
63
64
|
<!-- END env -->
|
|
@@ -231,11 +232,12 @@ The same variables drive API testing and doc collection.
|
|
|
231
232
|
```bash
|
|
232
233
|
EXPLORBOT_URL=https://api.example.com \
|
|
233
234
|
EXPLORBOT_API_SPEC=./openapi.yaml \
|
|
235
|
+
EXPLORBOT_API_HEADERS="Authorization: Bearer $TOKEN" \
|
|
234
236
|
EXPLORBOT_AI_PROVIDER=openrouter \
|
|
235
237
|
npx explorbot api explore /users
|
|
236
238
|
```
|
|
237
239
|
|
|
238
|
-
The API boat also takes those
|
|
240
|
+
The API boat also takes those three as flags, so one line carries the whole run: `npx explorbot api explore https://api.example.com --spec ./openapi.yaml -H "Authorization: Bearer $TOKEN"`. Given a full URL, `api explore` reads it as the base endpoint; a path like `/users` needs the base in `--endpoint` or `EXPLORBOT_URL`.
|
|
239
241
|
|
|
240
242
|
```bash
|
|
241
243
|
EXPLORBOT_AI_PROVIDER=openrouter \
|
|
@@ -75,3 +75,7 @@ Screenshots and other relative links may be included for readers, but Explorbot
|
|
|
75
75
|
## Validation
|
|
76
76
|
|
|
77
77
|
Explorbot rejects a bundle when `index.md` or `pages/` is missing, when it contains no page files, or when a page has an unsupported format, version, or missing URL.
|
|
78
|
+
|
|
79
|
+
## Scout
|
|
80
|
+
|
|
81
|
+
Beyond the per-URL injection, the same bundle feeds the [Scout agent](../reference/configuration.md#scout-agent): when Scout is enabled, it searches `pages/` (and any extra `ai.agents.scout.dirs`) for documentation relevant to the page being planned and reports it to the Planner. Pages already injected for the current URL are excluded from scouting, so the two channels never duplicate each other.
|
package/package.json
CHANGED
package/src/action-result.ts
CHANGED
|
@@ -33,6 +33,7 @@ interface ActionResultData extends WebPageState {
|
|
|
33
33
|
iframeSnapshots?: Array<{ src: string; html: string; id?: string }>;
|
|
34
34
|
ariaSnapshot?: string | null;
|
|
35
35
|
ariaSnapshotFile?: string;
|
|
36
|
+
regionAria?: string | null;
|
|
36
37
|
focusedElement?: FocusedElement | null;
|
|
37
38
|
iframeURL?: string;
|
|
38
39
|
links?: Link[];
|
|
@@ -90,6 +91,7 @@ export class ActionResult implements ActionResultData {
|
|
|
90
91
|
public links: Link[] = [];
|
|
91
92
|
public verifications?: Record<string, boolean>;
|
|
92
93
|
public overlay: Region = new Region();
|
|
94
|
+
public regionAria: string | null = null;
|
|
93
95
|
private _diffCache: { previousId: number | undefined; diff: Diff } | null = null;
|
|
94
96
|
|
|
95
97
|
constructor(data: ActionResultData) {
|
|
@@ -106,6 +108,7 @@ export class ActionResult implements ActionResultData {
|
|
|
106
108
|
this.iframeURL = data.iframeURL;
|
|
107
109
|
this.notes = data.notes ?? [];
|
|
108
110
|
this.verifications = data.verifications;
|
|
111
|
+
this.regionAria = data.regionAria ?? null;
|
|
109
112
|
|
|
110
113
|
// Set readonly properties
|
|
111
114
|
if (data.screenshotFile !== undefined) {
|
|
@@ -304,6 +307,10 @@ export class ActionResult implements ActionResultData {
|
|
|
304
307
|
return compactAriaSnapshot(this.ariaSnapshot, false);
|
|
305
308
|
}
|
|
306
309
|
|
|
310
|
+
getRegionARIA(): string {
|
|
311
|
+
return compactAriaSnapshot(this.regionAria, false);
|
|
312
|
+
}
|
|
313
|
+
|
|
307
314
|
getCompactARIA(): string {
|
|
308
315
|
return compactAriaSnapshot(this.ariaSnapshot, true);
|
|
309
316
|
}
|
package/src/action.ts
CHANGED
|
@@ -14,8 +14,8 @@ import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError }
|
|
|
14
14
|
import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
15
15
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
16
16
|
import { Overlay, OverlayPage } from './utils/overlay.js';
|
|
17
|
-
import type { Region } from './utils/region.js';
|
|
18
17
|
import { sleep, waitForPageReadiness } from './utils/page-readiness.ts';
|
|
18
|
+
import type { Region } from './utils/region.js';
|
|
19
19
|
import { safeFilename } from './utils/strings.ts';
|
|
20
20
|
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from './utils/web-sandbox.ts';
|
|
21
21
|
|
|
@@ -182,7 +182,19 @@ class Action {
|
|
|
182
182
|
focusedElement,
|
|
183
183
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
184
184
|
});
|
|
185
|
-
if (!frame)
|
|
185
|
+
if (!frame) {
|
|
186
|
+
await this.detectRegion(result).catch((err: Error) => debugLog('Region detection failed:', err.message));
|
|
187
|
+
const regionRoot = result.overlay.root;
|
|
188
|
+
if (result.overlay.isModal && regionRoot) {
|
|
189
|
+
result.regionAria = await this.playwrightHelper.page
|
|
190
|
+
.locator(regionRoot)
|
|
191
|
+
.ariaSnapshot()
|
|
192
|
+
.catch((err: Error) => {
|
|
193
|
+
debugLog('Region ARIA snapshot failed:', err.message);
|
|
194
|
+
return null;
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
186
198
|
this.stateManager.updateState(result, codeBlock);
|
|
187
199
|
return result;
|
|
188
200
|
} catch (err) {
|
|
@@ -7,11 +7,13 @@ import type { RequestStore } from '../../api/request-store.ts';
|
|
|
7
7
|
import { extractEndpointDefinition } from '../../api/spec-reader.ts';
|
|
8
8
|
import type { Test } from '../../test-plan.ts';
|
|
9
9
|
import { tag } from '../../utils/logger.ts';
|
|
10
|
+
import { truncate } from '../../utils/strings.ts';
|
|
10
11
|
import { isDynamicSegment } from '../../utils/url-matcher.ts';
|
|
11
12
|
import type { Fisherman } from '../fisherman.ts';
|
|
12
13
|
import type { RequestHaul } from './request-haul.ts';
|
|
13
14
|
|
|
14
15
|
const BODY_PREVIEW_LIMIT = 2000;
|
|
16
|
+
const READS_IN_ANSWER = 3;
|
|
15
17
|
|
|
16
18
|
export function createFishermanTools(apiClient: ApiClient, requestStore: RequestStore, haul: RequestHaul, opts: { spec?: any; baseEndpoint?: string; readOnly?: boolean }) {
|
|
17
19
|
const readOnly = opts.readOnly === true;
|
|
@@ -242,7 +244,7 @@ export function createAskApiTool(fisherman: Fisherman | null, task: Test) {
|
|
|
242
244
|
}
|
|
243
245
|
|
|
244
246
|
task.addNote(`Asked API: ${question} — ${result.summary}`);
|
|
245
|
-
tag('success').log(`Ask API: ${result.summary}`);
|
|
247
|
+
tag('success').log(`Ask API: ${truncate(result.summary, 200)}`);
|
|
246
248
|
return { answered: true, answer: result.summary };
|
|
247
249
|
},
|
|
248
250
|
}),
|
|
@@ -285,6 +287,11 @@ function synthesizeResult(haul: RequestHaul, declaredDone: boolean, readOnly: bo
|
|
|
285
287
|
succeeded = haul.successfulReads();
|
|
286
288
|
successLabel = 'successful reads';
|
|
287
289
|
}
|
|
290
|
+
if (readOnly && succeeded.length > 0) {
|
|
291
|
+
const bodies = succeeded.slice(-READS_IN_ANSWER).map((read) => `${read.toEndpoint()} → ${read.rawResponseBody.substring(0, BODY_PREVIEW_LIMIT)}`);
|
|
292
|
+
return { success: true, summary: bodies.join('\n\n'), created: [], failed: [] };
|
|
293
|
+
}
|
|
294
|
+
|
|
288
295
|
let summary = `Stopped before finishing: ${made.length} requests, ${succeeded.length} ${successLabel}, ${failures.length} failed`;
|
|
289
296
|
const lastFailure = failures[failures.length - 1];
|
|
290
297
|
if (lastFailure) summary += `; last failure: ${lastFailure.toSummary()}`;
|
package/src/ai/fisherman.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { ApiClient } from '../api/api-client.ts';
|
|
|
3
3
|
import { type EndpointFamily, type RequestStore, isFailedRequest } from '../api/request-store.ts';
|
|
4
4
|
import { listAllEndpoints } from '../api/spec-reader.ts';
|
|
5
5
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
6
|
+
import { truncate } from '../utils/strings.ts';
|
|
6
7
|
|
|
7
8
|
const debugLog = createDebug('explorbot:fisherman');
|
|
8
9
|
import { loop } from '../utils/loop.ts';
|
|
@@ -137,7 +138,7 @@ export class Fisherman implements Agent {
|
|
|
137
138
|
await this.runSession(conversation, tools, { haul, isFinished, finishFromText, label: `fisherman lookup: ${question.slice(0, 50)}` });
|
|
138
139
|
|
|
139
140
|
const result = getResult();
|
|
140
|
-
tag('info').log(`Fisherman answer: ${result.summary}`);
|
|
141
|
+
tag('info').log(`Fisherman answer: ${truncate(result.summary, 200)}`);
|
|
141
142
|
return result;
|
|
142
143
|
}
|
|
143
144
|
|
package/src/ai/pilot.ts
CHANGED
|
@@ -363,8 +363,6 @@ export class Pilot implements Agent {
|
|
|
363
363
|
return dedent`
|
|
364
364
|
SCENARIO: ${task.scenario}
|
|
365
365
|
|
|
366
|
-
${this.buildDeletionScope(task)}
|
|
367
|
-
|
|
368
366
|
EXPECTED RESULTS (milestones):
|
|
369
367
|
${task.expected.map((e) => `- ${e}`).join('\n')}
|
|
370
368
|
`;
|
|
@@ -372,20 +370,22 @@ export class Pilot implements Agent {
|
|
|
372
370
|
|
|
373
371
|
private buildResetSystemPrompt(task: Test): string {
|
|
374
372
|
return dedent`
|
|
375
|
-
You are Pilot — decide whether a reset is legitimate. Reset
|
|
376
|
-
iteration's work
|
|
377
|
-
|
|
373
|
+
You are Pilot — decide whether a reset is legitimate. Reset only re-navigates to the start URL:
|
|
374
|
+
it writes nothing, though it abandons this iteration's work and server-side side effects persist.
|
|
375
|
+
The hazard is the tester REDOING a completed flow afterwards — duplicate data and infinite loops.
|
|
378
376
|
|
|
379
377
|
${this.buildSharedEvidenceRules()}
|
|
380
378
|
|
|
381
379
|
DECISION:
|
|
382
|
-
- "allow": current page cannot host the scenario, irrecoverable error,
|
|
383
|
-
|
|
380
|
+
- "allow": current page cannot host the scenario, irrecoverable error, no path back, or an
|
|
381
|
+
expectation requires the outcome to survive a reload or a return to the start page and no
|
|
382
|
+
reset has been taken yet this run — there the reset IS the check, not a redo.
|
|
383
|
+
- "continue": the outcome the scenario needs is already observable on the CURRENT page — verify/finish instead. Provide guidance.
|
|
384
384
|
- "fail": resetCount >= 2 and underlying situation hasn't changed; same flow tried twice with same failure mode.
|
|
385
385
|
- "skipped": feature doesn't exist on this app or prerequisites can't be met.
|
|
386
386
|
|
|
387
387
|
PRIORITY:
|
|
388
|
-
1) Successful side effects in session_log →
|
|
388
|
+
1) Successful side effects in session_log → allow reset only to re-observe them, never to repeat them.
|
|
389
389
|
2) resetCount — each prior reset raises the bar.
|
|
390
390
|
3) Tester's stated reason — weigh against evidence, don't trust blindly.
|
|
391
391
|
|
|
@@ -1102,23 +1102,6 @@ export class Pilot implements Agent {
|
|
|
1102
1102
|
.join('\n\n');
|
|
1103
1103
|
}
|
|
1104
1104
|
|
|
1105
|
-
private buildDeletionScope(task: Test): string {
|
|
1106
|
-
const deletableItems = task.plan
|
|
1107
|
-
? task.plan
|
|
1108
|
-
.listTests()
|
|
1109
|
-
.filter((t) => t.isSuccessful && t.sessionName)
|
|
1110
|
-
.map((t) => t.sessionName!)
|
|
1111
|
-
: [];
|
|
1112
|
-
const scenarioLower = task.scenario.toLowerCase();
|
|
1113
|
-
if (deletableItems.length > 0) {
|
|
1114
|
-
return `For deletion scenarios, items can only be deleted if their title contains: ${deletableItems.join(', ')}`;
|
|
1115
|
-
}
|
|
1116
|
-
if (scenarioLower.includes('delete') || scenarioLower.includes('remove')) {
|
|
1117
|
-
return 'No items available for deletion — test should create an item first';
|
|
1118
|
-
}
|
|
1119
|
-
return '';
|
|
1120
|
-
}
|
|
1121
|
-
|
|
1122
1105
|
private getSystemPrompt(task: Test, initialState: ActionResult): string {
|
|
1123
1106
|
const interactive = isInteractive();
|
|
1124
1107
|
const stepsText = task.plannedSteps.length > 0 ? task.plannedSteps.map((s, i) => `${i + 1}. ${s}`).join('\n') : 'No planned steps';
|
package/src/ai/planner.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { POSSIBLE_SECTIONS, type Researcher } from './researcher.ts';
|
|
|
25
25
|
import { findSimilarStateHash } from './researcher/cache.ts';
|
|
26
26
|
import { hasFocusedSection } from './researcher/focus.ts';
|
|
27
27
|
import { capabilityGroundingRule, dataProtectionRules, fileUploadRule } from './rules.ts';
|
|
28
|
+
import type { Scout } from './scout.ts';
|
|
28
29
|
|
|
29
30
|
const debugLog = createDebug('explorbot:planner');
|
|
30
31
|
|
|
@@ -63,6 +64,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
63
64
|
private lastSuite: Suite | null = null;
|
|
64
65
|
researcher: Researcher;
|
|
65
66
|
private fisherman: Fisherman | null = null;
|
|
67
|
+
private scout: Scout | null = null;
|
|
66
68
|
|
|
67
69
|
constructor(deps: AgentDeps, researcher: Researcher) {
|
|
68
70
|
super();
|
|
@@ -78,10 +80,19 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
78
80
|
this.fisherman = fisherman;
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
setScout(scout: Scout): void {
|
|
84
|
+
this.scout = scout;
|
|
85
|
+
}
|
|
86
|
+
|
|
81
87
|
private get sectionOrder(): string[] {
|
|
82
88
|
return ConfigParser.getInstance().getConfig().ai?.agents?.researcher?.sections || Object.keys(POSSIBLE_SECTIONS);
|
|
83
89
|
}
|
|
84
90
|
|
|
91
|
+
private get docsWeight(): number {
|
|
92
|
+
const value = ConfigParser.getInstance().getConfig().ai?.agents?.planner?.docsWeight ?? 70;
|
|
93
|
+
return Math.max(0, Math.min(100, value));
|
|
94
|
+
}
|
|
95
|
+
|
|
85
96
|
private getDefaultStartUrl(state: { url: string; fullUrl?: string }): string {
|
|
86
97
|
return state.fullUrl || state.url;
|
|
87
98
|
}
|
|
@@ -328,6 +339,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
328
339
|
const conversation = new Conversation([], model);
|
|
329
340
|
conversation.autoTrimTag('page_research', 20000);
|
|
330
341
|
conversation.autoTrimTag('tested_scenarios', 10000);
|
|
342
|
+
conversation.autoTrimTag('docs_context', 8000);
|
|
331
343
|
|
|
332
344
|
conversation.addUserText(this.getSystemMessage(feature));
|
|
333
345
|
|
|
@@ -385,6 +397,11 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
385
397
|
const research = await this.researcher.research(currentState || state, {
|
|
386
398
|
deep: true,
|
|
387
399
|
});
|
|
400
|
+
|
|
401
|
+
let docsPromise: Promise<string> | null = null;
|
|
402
|
+
if (this.scout && this.docsWeight > 0) {
|
|
403
|
+
docsPromise = this.scout.collectDocs({ url: state.url, title: state.title, feature, excludeUrls: this.knowledgeTracker.applicationSpecUrls(state) });
|
|
404
|
+
}
|
|
388
405
|
let plannerResearch = mdq(research).query('code').replace('');
|
|
389
406
|
plannerResearch = mdq(plannerResearch)
|
|
390
407
|
.query('table')
|
|
@@ -419,6 +436,22 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
419
436
|
conversation.addUserText(applicationContext);
|
|
420
437
|
}
|
|
421
438
|
|
|
439
|
+
if (docsPromise) {
|
|
440
|
+
const docs = await docsPromise;
|
|
441
|
+
if (docs) {
|
|
442
|
+
conversation.addUserText(dedent`
|
|
443
|
+
<docs_context>
|
|
444
|
+
Documentation retrieved from the collected corpus by the Scout agent.
|
|
445
|
+
Ground scenarios in these documented capabilities where they apply; treat them as supporting context, not a script.
|
|
446
|
+
|
|
447
|
+
Aim for roughly ${this.docsWeight}% of the scenarios to exercise behavior documented above; the remainder may explore beyond the documentation.
|
|
448
|
+
|
|
449
|
+
${docs}
|
|
450
|
+
</docs_context>
|
|
451
|
+
`);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
422
455
|
conversation.addUserText(dedent`
|
|
423
456
|
${this.buildApproach(style)}
|
|
424
457
|
|
package/src/ai/provider.ts
CHANGED
|
@@ -409,7 +409,8 @@ export class Provider {
|
|
|
409
409
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
410
410
|
promptLog(`Using model: ${modelName}`);
|
|
411
411
|
|
|
412
|
-
|
|
412
|
+
let toolsWithCommentary = tools;
|
|
413
|
+
if (!tools?.commentary && options.toolChoice !== 'required') toolsWithCommentary = { ...tools, commentary: createHarmonyChannelFallbackTool() };
|
|
413
414
|
const toolNames = Object.keys(toolsWithCommentary || {});
|
|
414
415
|
tag('debug').log(`Tools enabled: [${toolNames.join(', ')}]`);
|
|
415
416
|
promptLog('Available tools:', toolNames);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult, type Diff } from '../../action-result.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type ExplorbotConfig, agentSettings } from '../../config.ts';
|
|
4
4
|
import { executionController } from '../../execution-controller.ts';
|
|
5
5
|
import type Explorer from '../../explorer.ts';
|
|
6
6
|
import type { StateManager } from '../../state-manager.js';
|
|
@@ -31,7 +31,7 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
31
31
|
tag('info').log('Starting deep analysis of expandable elements');
|
|
32
32
|
await (this as any).navigateTo(state.fullUrl || state.url);
|
|
33
33
|
|
|
34
|
-
const maxClicks = (this.config
|
|
34
|
+
const maxClicks = agentSettings(this.config, 'researcher').maxExpandableClicks ?? DEFAULT_MAX_EXPANDABLE_CLICKS;
|
|
35
35
|
|
|
36
36
|
const expandedSections: string[] = [];
|
|
37
37
|
const navigationLinks: Array<{ code: string; url: string }> = [];
|
|
@@ -88,6 +88,8 @@ export function WithDeepAnalysis<T extends Constructor>(Base: T) {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
async researchOverlay(current: ActionResult, previous: ActionResult, pageStateHash: string): Promise<string | null> {
|
|
91
|
+
if (!(this as any).isEnabled()) return null;
|
|
92
|
+
|
|
91
93
|
const region = current.overlay;
|
|
92
94
|
if (!region.isOpen || !region.name) return null;
|
|
93
95
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import type { ActionResult } from '../../action-result.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type ExplorbotConfig, agentSettings } from '../../config.ts';
|
|
4
4
|
import { executionController } from '../../execution-controller.ts';
|
|
5
5
|
import type Explorer from '../../explorer.ts';
|
|
6
6
|
import type { StateManager } from '../../state-manager.js';
|
|
@@ -66,7 +66,7 @@ export function WithSections<T extends Constructor>(Base: T) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
private async _detectFocusCss(): Promise<string | null> {
|
|
69
|
-
const focusSections = (this.config
|
|
69
|
+
const focusSections = agentSettings(this.config, 'researcher').focusSections;
|
|
70
70
|
if (!focusSections?.length) return null;
|
|
71
71
|
|
|
72
72
|
for (const css of focusSections) {
|
package/src/ai/researcher.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult } from '../action-result.js';
|
|
3
3
|
import { setActivity } from '../activity.ts';
|
|
4
|
-
import { ConfigParser, type ExplorbotConfig, outputPath } from '../config.ts';
|
|
4
|
+
import { ConfigParser, type ExplorbotConfig, type ResearcherAgentConfig, agentSettings, outputPath } from '../config.ts';
|
|
5
5
|
import { executionController } from '../execution-controller.ts';
|
|
6
6
|
import type { ExperienceTracker } from '../experience-tracker.ts';
|
|
7
7
|
import type Explorer from '../explorer.ts';
|
|
@@ -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, reportResearch, saveResearch } from './researcher/cache.ts';
|
|
22
|
+
import { findSimilarResearch, getCachedResearch, getPreviousResearch, 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';
|
|
@@ -62,13 +62,23 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
62
62
|
constructor(deps: AgentDeps) {
|
|
63
63
|
super(deps);
|
|
64
64
|
this.experienceTracker = deps.stateManager.getExperienceTracker();
|
|
65
|
+
this.settings.reasoning ??= 'low';
|
|
66
|
+
}
|
|
65
67
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
get settings(): ResearcherAgentConfig {
|
|
69
|
+
return agentSettings(this.config, 'researcher');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
isEnabled(): boolean {
|
|
73
|
+
return this.settings.enabled !== false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
enable(): void {
|
|
77
|
+
this.settings.enabled = true;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
disable(): void {
|
|
81
|
+
this.settings.enabled = false;
|
|
72
82
|
}
|
|
73
83
|
|
|
74
84
|
protected getNavigator(): Navigator {
|
|
@@ -94,7 +104,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
94
104
|
|
|
95
105
|
async research(state: WebPageState, opts: { screenshot?: boolean; force?: boolean; deep?: boolean; data?: boolean; fix?: boolean; _retriesLeft?: number } = {}): Promise<string> {
|
|
96
106
|
const { screenshot = false, force = false, deep = false, data = false, fix = true } = opts;
|
|
97
|
-
const maxRetries =
|
|
107
|
+
const maxRetries = this.settings.retries ?? 2;
|
|
98
108
|
let retriesLeft = opts._retriesLeft ?? maxRetries;
|
|
99
109
|
this.actionResult = ActionResult.fromState(state);
|
|
100
110
|
const stateHash = this.actionResult.baseHash;
|
|
@@ -109,6 +119,13 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
109
119
|
}
|
|
110
120
|
}
|
|
111
121
|
|
|
122
|
+
if (!this.isEnabled()) {
|
|
123
|
+
debugLog('Researcher is disabled, answering with the recorded map');
|
|
124
|
+
const recorded = getPreviousResearch(stateHash);
|
|
125
|
+
if (recorded) reportResearch(stateHash, recorded);
|
|
126
|
+
return recorded;
|
|
127
|
+
}
|
|
128
|
+
|
|
112
129
|
Stats.researches++;
|
|
113
130
|
|
|
114
131
|
const sessionName = `researcher: ${state.url}`;
|
|
@@ -341,7 +358,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
341
358
|
}
|
|
342
359
|
|
|
343
360
|
private async waitUntilSettled(screenshot: boolean): Promise<boolean> {
|
|
344
|
-
const errorPageTimeout =
|
|
361
|
+
const errorPageTimeout = this.settings.errorPageTimeout ?? 10;
|
|
345
362
|
if (errorPageTimeout <= 0) return false;
|
|
346
363
|
|
|
347
364
|
const includeScreenshot = screenshot && this.provider.hasVision();
|
|
@@ -374,7 +391,7 @@ export class Researcher extends ResearcherBase implements Agent {
|
|
|
374
391
|
}
|
|
375
392
|
|
|
376
393
|
private getConfiguredSections(): Record<string, string> {
|
|
377
|
-
const configSections =
|
|
394
|
+
const configSections = this.settings.sections;
|
|
378
395
|
if (!configSections?.length) return POSSIBLE_SECTIONS;
|
|
379
396
|
const filtered: Record<string, string> = {};
|
|
380
397
|
for (const key of configSections) {
|
package/src/ai/rules.ts
CHANGED
|
@@ -8,8 +8,9 @@ const locatorPriorityRule = dedent`
|
|
|
8
8
|
|
|
9
9
|
1. ARIA locators (first choice) - target browser's accessibility tree, most reliable
|
|
10
10
|
Use JSON format: { "role": "button", "text": "Login" }
|
|
11
|
-
Copy role and text VERBATIM from the ARIA snapshot
|
|
12
|
-
|
|
11
|
+
Copy role and text VERBATIM from the ARIA snapshot, UI map, or the page diff that
|
|
12
|
+
reported the element — never guess the pair; a guessed role can silently match a
|
|
13
|
+
different element with the same text. If named nowhere, use text or CSS instead.
|
|
13
14
|
|
|
14
15
|
2. Text locators (second choice) - exact visible text, use only when unique on the page
|
|
15
16
|
Example: 'Login', 'Submit', 'Username'
|
|
@@ -238,10 +239,10 @@ export const unexpectedPopupRule = dedent`
|
|
|
238
239
|
If buttons are disabled unexpectedly, check if a popup is blocking interaction or if required form fields are empty.
|
|
239
240
|
|
|
240
241
|
Dismiss strategy (try in order):
|
|
241
|
-
1. I.
|
|
242
|
-
2. I.
|
|
243
|
-
3. I.click('
|
|
244
|
-
4. I.
|
|
242
|
+
1. I.pressKey('Escape') — press Escape to dismiss
|
|
243
|
+
2. I.click('Cancel') — click Cancel button if present
|
|
244
|
+
3. I.click({ role: 'button', text: 'Close' }) — click X/close button if present
|
|
245
|
+
4. I.clickXY(0, 0) via form() tool and check if page diff changed
|
|
245
246
|
</unexpected_popup_rule>
|
|
246
247
|
`;
|
|
247
248
|
|
|
@@ -335,7 +336,7 @@ export const actionRule = dedent`
|
|
|
335
336
|
Prefer text/ARIA locators with context over complex CSS/XPath selectors.
|
|
336
337
|
For inline create/edit flows, after filling a field verify it contains the value, then confirm using the nearest explicit button/link, an adjacent icon-only confirm control in the same row/form, or Enter if the field remains focused.
|
|
337
338
|
If locator doesn't work, try CSS or XPath locators.
|
|
338
|
-
If nothing works, use
|
|
339
|
+
If nothing works, use visualClick() — it locates the target in a screenshot before clicking it.
|
|
339
340
|
|
|
340
341
|
When a click result reports several matches, pick one from its numbered list by position rather than guessing a new locator.
|
|
341
342
|
Reuse the same locator with step.opts({ elementIndex: N }) as the LAST argument. N is the "Element N" number.
|