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
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import dedent from 'dedent';
|
|
2
|
+
export const helpContract = dedent `
|
|
3
|
+
Prima drives the browser opened by playwright-cli.
|
|
4
|
+
|
|
5
|
+
playwright-cli open <url> starts the session
|
|
6
|
+
prima <command> ... drives it
|
|
7
|
+
playwright-cli close ends it
|
|
8
|
+
|
|
9
|
+
One call runs a whole job:
|
|
10
|
+
|
|
11
|
+
prima check "a workflow can be created and appears in the list" --expected "the new workflow is listed"
|
|
12
|
+
prima do "open the account menu" "choose the settings entry" "switch the theme to dark" "check it took effect"
|
|
13
|
+
prima pw "({ page }) => page.click('[data-test=submit]')"
|
|
14
|
+
|
|
15
|
+
Only research maps a page. Other commands read the accessibility tree; a cached
|
|
16
|
+
research map joins when present. On large or unclear pages, run prima research first.
|
|
17
|
+
Without a usable AI model only pw works; otherwise use playwright-cli.
|
|
18
|
+
DEBUG='explorbot:*' in front of a command logs everything it does.
|
|
19
|
+
`;
|
|
20
|
+
export const checkHelp = dedent `
|
|
21
|
+
check states the outcome, not the clicks; it finds the path itself. It stays
|
|
22
|
+
on the current page and never reloads it, so an open dialog survives.
|
|
23
|
+
--expected one required outcome, repeatable for several. Without it the
|
|
24
|
+
scenario text is the outcome. Each returns under
|
|
25
|
+
### Expected outcomes as PASSED, FAILED, CONTRADICTION or not verified.
|
|
26
|
+
"not verified" means never checked, not false.
|
|
27
|
+
Proof is a full-page screenshot: what a user sees counts, the log only shows actions.
|
|
28
|
+
CONTRADICTION means screenshot and log disagree; judge the html, aria and
|
|
29
|
+
screenshot under ### Artifacts yourself.
|
|
30
|
+
ok is false when any outcome FAILED or CONTRADICTED, or the run could not finish,
|
|
31
|
+
reported as such rather than as an app failure.
|
|
32
|
+
Side issues found on the way go under ### Answer, not as step failures.
|
|
33
|
+
`;
|
|
34
|
+
export const doHelp = dedent `
|
|
35
|
+
### Steps marks each instruction ok, FAIL or ??. ?? means it ran but the run ended
|
|
36
|
+
without confirming it - read the steps above. Only FAIL fails the command.
|
|
37
|
+
Nothing runs past the last instruction. Batch the whole sequence in one call;
|
|
38
|
+
that is what keeps this tier cheap.
|
|
39
|
+
`;
|
|
40
|
+
export const askHelp = dedent `
|
|
41
|
+
Answers from a page screenshot, or from its structure with --no-vision.
|
|
42
|
+
`;
|
|
43
|
+
export const verifyHelp = dedent `
|
|
44
|
+
Reports each expressible assertion as PASSED or FAILED with its playwright form;
|
|
45
|
+
no overall verdict, read the lines. "none ran" means unexpressible, not false.
|
|
46
|
+
`;
|
|
47
|
+
export const researchHelp = dedent `
|
|
48
|
+
The map is saved per page state and joins later commands there, so one research run pays for all that follow it.
|
|
49
|
+
`;
|
|
50
|
+
export const statusHelp = dedent `
|
|
51
|
+
Reads recorded files, so it needs no browser and outlives the session.
|
|
52
|
+
The hash is matched across all recorded sites. ### Artifacts lists every kept
|
|
53
|
+
file: aria, html, screenshot and network log when captured, plus per-step captures of a do run.
|
|
54
|
+
`;
|
|
55
|
+
export const reportHelp = dedent `
|
|
56
|
+
Built from the command log, so it needs no browser and outlives the session.
|
|
57
|
+
Reports the latest session unless --pw-session names another.
|
|
58
|
+
`;
|
|
59
|
+
export const sessionHelp = dedent `
|
|
60
|
+
Parallel jobs need one --instance each. --session is ignored when attached,
|
|
61
|
+
since the attached session keeps its own. --framework is parsed but inactive;
|
|
62
|
+
reported code is CodeceptJS either way.
|
|
63
|
+
`;
|
|
@@ -18,19 +18,19 @@ import { findSiteWith, listSites } from "../../../src/global-config.js";
|
|
|
18
18
|
import { Reporter } from "../../../src/reporter.js";
|
|
19
19
|
import { Stats } from "../../../src/stats.js";
|
|
20
20
|
import { Task, Test, TestResult } from "../../../src/test-plan.js";
|
|
21
|
+
import { ariaRefSnapshot } from "../../../src/utils/aria-ref.js";
|
|
21
22
|
import { compactAriaSnapshot } from "../../../src/utils/aria.js";
|
|
22
23
|
import { browserErrorMessage } from "../../../src/utils/browser-errors.js";
|
|
23
24
|
import { pluralize } from "../../../src/utils/logger.js";
|
|
24
25
|
import { mdq } from "../../../src/utils/markdown-query.js";
|
|
25
26
|
import { safeFilename } from "../../../src/utils/strings.js";
|
|
26
|
-
import { STATUS_FILE, readArtifacts, writeArtifacts } from "./envelope.js";
|
|
27
|
+
import { STATUS_FILE, STEP_FILES, readArtifacts, writeArtifacts } from "./envelope.js";
|
|
27
28
|
import { isFunctionExpression, takePwValue, toCodeceptWrapper } from "./pw-parser.js";
|
|
28
29
|
import { readDescriptors, selectDescriptor } from "./pw-registry.js";
|
|
29
30
|
import { latestSessionFile, readSession, recordCommand, sessionFile, sessionsDir } from "./session-log.js";
|
|
30
|
-
const
|
|
31
|
+
const WITHHELD_TOOLS = ['learnExperience', 'askUser', 'research'];
|
|
31
32
|
const ITERATIONS_PER_INSTRUCTION = 2;
|
|
32
33
|
const MAX_INSTRUCTION_ITERATIONS = 24;
|
|
33
|
-
const DEFAULT_RESEARCH_AFTER_VISITS = 3;
|
|
34
34
|
const CONTEXT_HTML_CAP = 6000;
|
|
35
35
|
const MAX_TOOL_ROUNDTRIPS = 5;
|
|
36
36
|
const AI_AGENT_NAME = 'prima';
|
|
@@ -68,7 +68,6 @@ export class Prima {
|
|
|
68
68
|
server = null;
|
|
69
69
|
attached = null;
|
|
70
70
|
session = null;
|
|
71
|
-
artifacts;
|
|
72
71
|
constructor(options = {}) {
|
|
73
72
|
this.options = options;
|
|
74
73
|
this.bot = new ExplorBot({
|
|
@@ -99,6 +98,7 @@ export class Prima {
|
|
|
99
98
|
const config = await this.loadConfig();
|
|
100
99
|
await this.resolveBrowser(config, discovery);
|
|
101
100
|
await this.bot.start();
|
|
101
|
+
this.bot.agentResearcher().disable();
|
|
102
102
|
if (!this.options.url)
|
|
103
103
|
return;
|
|
104
104
|
if (this.bot.getCurrentState())
|
|
@@ -296,11 +296,17 @@ export class Prima {
|
|
|
296
296
|
settleError = error;
|
|
297
297
|
return null;
|
|
298
298
|
});
|
|
299
|
-
|
|
300
|
-
trace.push({ label: 'settling which instructions were satisfied', ok: false, proof: browserErrorMessage(settleError) });
|
|
299
|
+
const stillOpen = ledger.filter((entry) => entry.status === 'open').length;
|
|
301
300
|
for (const execution of invoked?.toolExecutions || []) {
|
|
302
301
|
this.applyLedgerReport(execution, ledger, trace);
|
|
303
302
|
}
|
|
303
|
+
let unsettled = '';
|
|
304
|
+
if (settleError)
|
|
305
|
+
unsettled = browserErrorMessage(settleError);
|
|
306
|
+
if (invoked && ledger.filter((entry) => entry.status === 'open').length === stillOpen)
|
|
307
|
+
unsettled = 'the model was asked to report every remaining instruction and reported none';
|
|
308
|
+
if (unsettled)
|
|
309
|
+
trace.push({ label: 'settling which instructions were satisfied', ok: false, proof: unsettled });
|
|
304
310
|
}
|
|
305
311
|
ledgerProgress(ledger) {
|
|
306
312
|
return ledger
|
|
@@ -382,9 +388,11 @@ export class Prima {
|
|
|
382
388
|
const guard = await this.aiGuard(command);
|
|
383
389
|
if (guard)
|
|
384
390
|
return guard;
|
|
391
|
+
const researcher = this.bot.agentResearcher();
|
|
392
|
+
researcher.enable();
|
|
385
393
|
const previousState = this.bot.stateManager().getCurrentState();
|
|
386
394
|
const result = await this.capturedResult(previousState);
|
|
387
|
-
const uiMap = await
|
|
395
|
+
const uiMap = await researcher.research(result, { screenshot: true, data: opts.data, deep: opts.deep, force: opts.fresh });
|
|
388
396
|
return this.reportEnvelope(command, result, previousState, { research: dropVolatileColumns(uiMap) });
|
|
389
397
|
}
|
|
390
398
|
async go(target) {
|
|
@@ -768,7 +776,7 @@ export class Prima {
|
|
|
768
776
|
if (!researcher || !navigator)
|
|
769
777
|
return {};
|
|
770
778
|
const tools = createAgentTools({ ...deps, researcher, navigator, withExperience: false });
|
|
771
|
-
for (const name of
|
|
779
|
+
for (const name of WITHHELD_TOOLS)
|
|
772
780
|
delete tools[name];
|
|
773
781
|
return tools;
|
|
774
782
|
}
|
|
@@ -823,14 +831,15 @@ export class Prima {
|
|
|
823
831
|
}
|
|
824
832
|
async pageContext(result) {
|
|
825
833
|
const experience = this.bot.experienceTracker?.()?.renderExperienceTocFor?.(result) || '';
|
|
826
|
-
const map =
|
|
834
|
+
const map = getPreviousResearch(result.baseHash);
|
|
835
|
+
let uiMap = '';
|
|
827
836
|
if (map) {
|
|
828
|
-
|
|
829
|
-
<page_ui_map
|
|
837
|
+
uiMap = dedent `
|
|
838
|
+
<page_ui_map>
|
|
839
|
+
A map of this page recorded by an earlier research run. It names parts the accessibility
|
|
840
|
+
tree does not, and can be out of date — the tree is what the page holds now.
|
|
830
841
|
${map}
|
|
831
842
|
</page_ui_map>
|
|
832
|
-
|
|
833
|
-
${experience}
|
|
834
843
|
`;
|
|
835
844
|
}
|
|
836
845
|
return dedent `
|
|
@@ -838,20 +847,11 @@ export class Prima {
|
|
|
838
847
|
${compactAriaSnapshot(await this.refAriaSnapshot(result), true, (value) => this.offloadValue(value))}
|
|
839
848
|
</page>
|
|
840
849
|
|
|
850
|
+
${uiMap}
|
|
851
|
+
|
|
841
852
|
${experience}
|
|
842
853
|
`;
|
|
843
854
|
}
|
|
844
|
-
researchMap(result) {
|
|
845
|
-
if (this.bot.stateManager().getVisitCount(result.url) < this.researchAfterVisits())
|
|
846
|
-
return '';
|
|
847
|
-
return getPreviousResearch(result.getStateHash());
|
|
848
|
-
}
|
|
849
|
-
researchAfterVisits() {
|
|
850
|
-
const configured = this.bot.getConfig?.()?.ai?.agents?.prima?.researchAfterVisits;
|
|
851
|
-
if (typeof configured === 'number')
|
|
852
|
-
return configured;
|
|
853
|
-
return DEFAULT_RESEARCH_AFTER_VISITS;
|
|
854
|
-
}
|
|
855
855
|
offloadValue(value) {
|
|
856
856
|
const dir = this.statusDir();
|
|
857
857
|
const name = `value-${createHash('sha1').update(value).digest('hex').slice(0, 8)}.txt`;
|
|
@@ -865,7 +865,7 @@ export class Prima {
|
|
|
865
865
|
return path.join(path.basename(dir), name);
|
|
866
866
|
}
|
|
867
867
|
async refAriaSnapshot(result) {
|
|
868
|
-
const snapshot = await Promise.resolve(this.bot.getExplorer()?.withPage?.(
|
|
868
|
+
const snapshot = await Promise.resolve(this.bot.getExplorer()?.withPage?.(ariaRefSnapshot)).catch(() => null);
|
|
869
869
|
return snapshot || result.ariaSnapshot;
|
|
870
870
|
}
|
|
871
871
|
executedCodes(code) {
|
|
@@ -923,16 +923,13 @@ export class Prima {
|
|
|
923
923
|
}
|
|
924
924
|
async successEnvelope(command, used, result, previousState) {
|
|
925
925
|
const changes = await this.pageChanges(result, previousState, used[0]);
|
|
926
|
-
const status = await this.saveStatus(result);
|
|
927
926
|
return {
|
|
928
927
|
ok: true,
|
|
929
928
|
command,
|
|
930
929
|
used,
|
|
931
930
|
page: this.pageBlock(result, previousState),
|
|
932
931
|
changes,
|
|
933
|
-
|
|
934
|
-
status,
|
|
935
|
-
artifacts: this.artifacts,
|
|
932
|
+
...(await this.envelopeTail(result)),
|
|
936
933
|
};
|
|
937
934
|
}
|
|
938
935
|
async failureEnvelope(command, error, previousState) {
|
|
@@ -940,29 +937,27 @@ export class Prima {
|
|
|
940
937
|
const failure = { error: browserErrorMessage(error) };
|
|
941
938
|
if (result.ariaSnapshot)
|
|
942
939
|
failure.compactAria = compactAriaSnapshot(result.ariaSnapshot, true);
|
|
943
|
-
const status = await this.saveStatus(result);
|
|
944
940
|
return {
|
|
945
941
|
ok: false,
|
|
946
942
|
command,
|
|
947
943
|
page: this.pageBlock(result, previousState),
|
|
948
944
|
failure,
|
|
949
|
-
|
|
950
|
-
status,
|
|
951
|
-
artifacts: this.artifacts,
|
|
945
|
+
...(await this.envelopeTail(result)),
|
|
952
946
|
};
|
|
953
947
|
}
|
|
954
948
|
async reportEnvelope(command, result, previousState, outcome) {
|
|
955
|
-
const status = await this.saveStatus(result);
|
|
956
949
|
return {
|
|
957
950
|
ok: true,
|
|
958
951
|
command,
|
|
959
952
|
page: this.pageBlock(result, previousState),
|
|
960
953
|
...outcome,
|
|
961
|
-
|
|
962
|
-
status,
|
|
963
|
-
artifacts: this.artifacts,
|
|
954
|
+
...(await this.envelopeTail(result)),
|
|
964
955
|
};
|
|
965
956
|
}
|
|
957
|
+
async envelopeTail(result) {
|
|
958
|
+
const { hash, artifacts } = await this.saveStatus(result);
|
|
959
|
+
return { instance: await this.instanceInfo(), status: hash, artifacts };
|
|
960
|
+
}
|
|
966
961
|
async capturedResult(previousState, opts = {}) {
|
|
967
962
|
const captured = await this.bot
|
|
968
963
|
.getExplorer()
|
|
@@ -1026,9 +1021,9 @@ export class Prima {
|
|
|
1026
1021
|
}
|
|
1027
1022
|
async saveStatus(result) {
|
|
1028
1023
|
const hash = this.statusHash();
|
|
1029
|
-
await this.writeSnapshot(result);
|
|
1024
|
+
const artifacts = await this.writeSnapshot(result);
|
|
1030
1025
|
writeFileSync(path.join(this.statusDir(hash), STATUS_FILE), JSON.stringify({ page: this.pageBlock(result, null) }), 'utf-8');
|
|
1031
|
-
return hash;
|
|
1026
|
+
return { hash, artifacts };
|
|
1032
1027
|
}
|
|
1033
1028
|
async writeStepFiles(index, label, diff) {
|
|
1034
1029
|
const state = this.bot.stateManager().getCurrentState();
|
|
@@ -1038,17 +1033,17 @@ export class Prima {
|
|
|
1038
1033
|
mkdirSync(dir, { recursive: true });
|
|
1039
1034
|
const stem = path.join(dir, `${index}-${safeFilename(label.slice(0, 60))}`);
|
|
1040
1035
|
const result = ActionResult.fromState(state);
|
|
1041
|
-
writeFileSync(`${stem}.aria
|
|
1042
|
-
writeFileSync(`${stem}.html`, await result.combinedHtml(), 'utf-8');
|
|
1036
|
+
writeFileSync(`${stem}.${STEP_FILES.aria}`, result.ariaSnapshot ?? '', 'utf-8');
|
|
1037
|
+
writeFileSync(`${stem}.${STEP_FILES.html}`, await result.combinedHtml(), 'utf-8');
|
|
1043
1038
|
if (diff)
|
|
1044
|
-
writeFileSync(`${stem}.diff
|
|
1039
|
+
writeFileSync(`${stem}.${STEP_FILES.diff}`, diff, 'utf-8');
|
|
1045
1040
|
}
|
|
1046
1041
|
async writeSnapshot(result) {
|
|
1047
|
-
|
|
1042
|
+
return writeArtifacts(this.statusDir(), {
|
|
1048
1043
|
aria: result.ariaSnapshot,
|
|
1049
1044
|
html: await result.combinedHtml(),
|
|
1050
1045
|
screenshot: result.screenshot,
|
|
1051
|
-
requests: this.bot.requestStore().
|
|
1046
|
+
requests: this.bot.requestStore().getCapturedRequests(),
|
|
1052
1047
|
});
|
|
1053
1048
|
}
|
|
1054
1049
|
statusHash() {
|
package/dist/package.json
CHANGED
|
@@ -26,6 +26,7 @@ interface ActionResultData extends WebPageState {
|
|
|
26
26
|
}>;
|
|
27
27
|
ariaSnapshot?: string | null;
|
|
28
28
|
ariaSnapshotFile?: string;
|
|
29
|
+
regionAria?: string | null;
|
|
29
30
|
focusedElement?: FocusedElement | null;
|
|
30
31
|
iframeURL?: string;
|
|
31
32
|
links?: Link[];
|
|
@@ -84,6 +85,7 @@ export declare class ActionResult implements ActionResultData {
|
|
|
84
85
|
links: Link[];
|
|
85
86
|
verifications?: Record<string, boolean>;
|
|
86
87
|
overlay: Region;
|
|
88
|
+
regionAria: string | null;
|
|
87
89
|
_diffCache: {
|
|
88
90
|
previousId: number | undefined;
|
|
89
91
|
diff: Diff;
|
|
@@ -109,6 +111,7 @@ export declare class ActionResult implements ActionResultData {
|
|
|
109
111
|
}): Promise<string>;
|
|
110
112
|
textHtml(htmlConfig?: HtmlConfig): Promise<string>;
|
|
111
113
|
getInteractiveARIA(): string;
|
|
114
|
+
getRegionARIA(): string;
|
|
112
115
|
getCompactARIA(): string;
|
|
113
116
|
normalizeHtmlConfig(htmlConfig?: HtmlConfig): HtmlConfig | undefined;
|
|
114
117
|
static fromState(state: WebPageState): ActionResult;
|
|
@@ -40,6 +40,7 @@ export class ActionResult {
|
|
|
40
40
|
links = [];
|
|
41
41
|
verifications;
|
|
42
42
|
overlay = new Region();
|
|
43
|
+
regionAria = null;
|
|
43
44
|
_diffCache = null;
|
|
44
45
|
constructor(data) {
|
|
45
46
|
this.id = data.id;
|
|
@@ -55,6 +56,7 @@ export class ActionResult {
|
|
|
55
56
|
this.iframeURL = data.iframeURL;
|
|
56
57
|
this.notes = data.notes ?? [];
|
|
57
58
|
this.verifications = data.verifications;
|
|
59
|
+
this.regionAria = data.regionAria ?? null;
|
|
58
60
|
// Set readonly properties
|
|
59
61
|
if (data.screenshotFile !== undefined) {
|
|
60
62
|
this.screenshotFile = data.screenshotFile;
|
|
@@ -238,6 +240,9 @@ export class ActionResult {
|
|
|
238
240
|
getInteractiveARIA() {
|
|
239
241
|
return compactAriaSnapshot(this.ariaSnapshot, false);
|
|
240
242
|
}
|
|
243
|
+
getRegionARIA() {
|
|
244
|
+
return compactAriaSnapshot(this.regionAria, false);
|
|
245
|
+
}
|
|
241
246
|
getCompactARIA() {
|
|
242
247
|
return compactAriaSnapshot(this.ariaSnapshot, true);
|
|
243
248
|
}
|
package/dist/src/action.js
CHANGED
|
@@ -165,8 +165,19 @@ class Action {
|
|
|
165
165
|
focusedElement,
|
|
166
166
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
167
167
|
});
|
|
168
|
-
if (!frame)
|
|
168
|
+
if (!frame) {
|
|
169
169
|
await this.detectRegion(result).catch((err) => debugLog('Region detection failed:', err.message));
|
|
170
|
+
const regionRoot = result.overlay.root;
|
|
171
|
+
if (result.overlay.isModal && regionRoot) {
|
|
172
|
+
result.regionAria = await this.playwrightHelper.page
|
|
173
|
+
.locator(regionRoot)
|
|
174
|
+
.ariaSnapshot()
|
|
175
|
+
.catch((err) => {
|
|
176
|
+
debugLog('Region ARIA snapshot failed:', err.message);
|
|
177
|
+
return null;
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
170
181
|
this.stateManager.updateState(result, codeBlock);
|
|
171
182
|
return result;
|
|
172
183
|
}
|
|
@@ -3,8 +3,10 @@ import dedent from 'dedent';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { extractEndpointDefinition } from "../../api/spec-reader.js";
|
|
5
5
|
import { tag } from "../../utils/logger.js";
|
|
6
|
+
import { truncate } from "../../utils/strings.js";
|
|
6
7
|
import { isDynamicSegment } from "../../utils/url-matcher.js";
|
|
7
8
|
const BODY_PREVIEW_LIMIT = 2000;
|
|
9
|
+
const READS_IN_ANSWER = 3;
|
|
8
10
|
export function createFishermanTools(apiClient, requestStore, haul, opts) {
|
|
9
11
|
const readOnly = opts.readOnly === true;
|
|
10
12
|
let finished = false;
|
|
@@ -213,7 +215,7 @@ export function createAskApiTool(fisherman, task) {
|
|
|
213
215
|
return { answered: false, reason: result.summary || 'The API could not answer this question' };
|
|
214
216
|
}
|
|
215
217
|
task.addNote(`Asked API: ${question} — ${result.summary}`);
|
|
216
|
-
tag('success').log(`Ask API: ${result.summary}`);
|
|
218
|
+
tag('success').log(`Ask API: ${truncate(result.summary, 200)}`);
|
|
217
219
|
return { answered: true, answer: result.summary };
|
|
218
220
|
},
|
|
219
221
|
}),
|
|
@@ -252,6 +254,10 @@ function synthesizeResult(haul, declaredDone, readOnly) {
|
|
|
252
254
|
succeeded = haul.successfulReads();
|
|
253
255
|
successLabel = 'successful reads';
|
|
254
256
|
}
|
|
257
|
+
if (readOnly && succeeded.length > 0) {
|
|
258
|
+
const bodies = succeeded.slice(-READS_IN_ANSWER).map((read) => `${read.toEndpoint()} → ${read.rawResponseBody.substring(0, BODY_PREVIEW_LIMIT)}`);
|
|
259
|
+
return { success: true, summary: bodies.join('\n\n'), created: [], failed: [] };
|
|
260
|
+
}
|
|
255
261
|
let summary = `Stopped before finishing: ${made.length} requests, ${succeeded.length} ${successLabel}, ${failures.length} failed`;
|
|
256
262
|
const lastFailure = failures[failures.length - 1];
|
|
257
263
|
if (lastFailure)
|
package/dist/src/ai/fisherman.js
CHANGED
|
@@ -2,6 +2,7 @@ import dedent from 'dedent';
|
|
|
2
2
|
import { isFailedRequest } from "../api/request-store.js";
|
|
3
3
|
import { listAllEndpoints } from "../api/spec-reader.js";
|
|
4
4
|
import { createDebug, tag } from "../utils/logger.js";
|
|
5
|
+
import { truncate } from "../utils/strings.js";
|
|
5
6
|
const debugLog = createDebug('explorbot:fisherman');
|
|
6
7
|
import { loop } from "../utils/loop.js";
|
|
7
8
|
import { RequestHaul } from "./fisherman/request-haul.js";
|
|
@@ -106,7 +107,7 @@ export class Fisherman {
|
|
|
106
107
|
`);
|
|
107
108
|
await this.runSession(conversation, tools, { haul, isFinished, finishFromText, label: `fisherman lookup: ${question.slice(0, 50)}` });
|
|
108
109
|
const result = getResult();
|
|
109
|
-
tag('info').log(`Fisherman answer: ${result.summary}`);
|
|
110
|
+
tag('info').log(`Fisherman answer: ${truncate(result.summary, 200)}`);
|
|
110
111
|
return result;
|
|
111
112
|
}
|
|
112
113
|
async runSession(conversation, tools, opts) {
|
package/dist/src/ai/pilot.d.ts
CHANGED
|
@@ -97,7 +97,6 @@ export declare class Pilot implements Agent {
|
|
|
97
97
|
hasSuccessfulCheckEvidence(currentState: ActionResult, testerConversation: Conversation): boolean;
|
|
98
98
|
formatSuccessfulAssertions(currentState: ActionResult, testerConversation: Conversation): string;
|
|
99
99
|
formatActions(toolCalls: any[]): string;
|
|
100
|
-
buildDeletionScope(task: Test): string;
|
|
101
100
|
getSystemPrompt(task: Test, initialState: ActionResult): string;
|
|
102
101
|
}
|
|
103
102
|
export type SettledStatus = 'passed' | 'failed' | 'unverified' | 'contradiction';
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -316,28 +316,28 @@ export class Pilot {
|
|
|
316
316
|
return dedent `
|
|
317
317
|
SCENARIO: ${task.scenario}
|
|
318
318
|
|
|
319
|
-
${this.buildDeletionScope(task)}
|
|
320
|
-
|
|
321
319
|
EXPECTED RESULTS (milestones):
|
|
322
320
|
${task.expected.map((e) => `- ${e}`).join('\n')}
|
|
323
321
|
`;
|
|
324
322
|
}
|
|
325
323
|
buildResetSystemPrompt(task) {
|
|
326
324
|
return dedent `
|
|
327
|
-
You are Pilot — decide whether a reset is legitimate. Reset
|
|
328
|
-
iteration's work
|
|
329
|
-
|
|
325
|
+
You are Pilot — decide whether a reset is legitimate. Reset only re-navigates to the start URL:
|
|
326
|
+
it writes nothing, though it abandons this iteration's work and server-side side effects persist.
|
|
327
|
+
The hazard is the tester REDOING a completed flow afterwards — duplicate data and infinite loops.
|
|
330
328
|
|
|
331
329
|
${this.buildSharedEvidenceRules()}
|
|
332
330
|
|
|
333
331
|
DECISION:
|
|
334
|
-
- "allow": current page cannot host the scenario, irrecoverable error,
|
|
335
|
-
|
|
332
|
+
- "allow": current page cannot host the scenario, irrecoverable error, no path back, or an
|
|
333
|
+
expectation requires the outcome to survive a reload or a return to the start page and no
|
|
334
|
+
reset has been taken yet this run — there the reset IS the check, not a redo.
|
|
335
|
+
- "continue": the outcome the scenario needs is already observable on the CURRENT page — verify/finish instead. Provide guidance.
|
|
336
336
|
- "fail": resetCount >= 2 and underlying situation hasn't changed; same flow tried twice with same failure mode.
|
|
337
337
|
- "skipped": feature doesn't exist on this app or prerequisites can't be met.
|
|
338
338
|
|
|
339
339
|
PRIORITY:
|
|
340
|
-
1) Successful side effects in session_log →
|
|
340
|
+
1) Successful side effects in session_log → allow reset only to re-observe them, never to repeat them.
|
|
341
341
|
2) resetCount — each prior reset raises the bar.
|
|
342
342
|
3) Tester's stated reason — weigh against evidence, don't trust blindly.
|
|
343
343
|
|
|
@@ -999,22 +999,6 @@ export class Pilot {
|
|
|
999
999
|
})
|
|
1000
1000
|
.join('\n\n');
|
|
1001
1001
|
}
|
|
1002
|
-
buildDeletionScope(task) {
|
|
1003
|
-
const deletableItems = task.plan
|
|
1004
|
-
? task.plan
|
|
1005
|
-
.listTests()
|
|
1006
|
-
.filter((t) => t.isSuccessful && t.sessionName)
|
|
1007
|
-
.map((t) => t.sessionName)
|
|
1008
|
-
: [];
|
|
1009
|
-
const scenarioLower = task.scenario.toLowerCase();
|
|
1010
|
-
if (deletableItems.length > 0) {
|
|
1011
|
-
return `For deletion scenarios, items can only be deleted if their title contains: ${deletableItems.join(', ')}`;
|
|
1012
|
-
}
|
|
1013
|
-
if (scenarioLower.includes('delete') || scenarioLower.includes('remove')) {
|
|
1014
|
-
return 'No items available for deletion — test should create an item first';
|
|
1015
|
-
}
|
|
1016
|
-
return '';
|
|
1017
|
-
}
|
|
1018
1002
|
getSystemPrompt(task, initialState) {
|
|
1019
1003
|
const interactive = isInteractive();
|
|
1020
1004
|
const stepsText = task.plannedSteps.length > 0 ? task.plannedSteps.map((s, i) => `${i + 1}. ${s}`).join('\n') : 'No planned steps';
|
package/dist/src/ai/planner.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { Conversation } from './conversation.js';
|
|
|
9
9
|
import type { Fisherman } from './fisherman.js';
|
|
10
10
|
import type { Provider } from './provider.js';
|
|
11
11
|
import { type Researcher } from './researcher.js';
|
|
12
|
+
import type { Scout } from './scout.js';
|
|
12
13
|
declare const PlannerBase: {
|
|
13
14
|
new (...args: any[]): {
|
|
14
15
|
currentPlan: Plan | null;
|
|
@@ -59,9 +60,12 @@ export declare class Planner extends PlannerBase implements Agent {
|
|
|
59
60
|
lastSuite: Suite | null;
|
|
60
61
|
researcher: Researcher;
|
|
61
62
|
fisherman: Fisherman | null;
|
|
63
|
+
scout: Scout | null;
|
|
62
64
|
constructor(deps: AgentDeps, researcher: Researcher);
|
|
63
65
|
setFisherman(fisherman: Fisherman): void;
|
|
66
|
+
setScout(scout: Scout): void;
|
|
64
67
|
get sectionOrder(): string[];
|
|
68
|
+
get docsWeight(): number;
|
|
65
69
|
getDefaultStartUrl(state: {
|
|
66
70
|
url: string;
|
|
67
71
|
fullUrl?: string;
|
package/dist/src/ai/planner.js
CHANGED
|
@@ -50,6 +50,7 @@ export class Planner extends PlannerBase {
|
|
|
50
50
|
lastSuite = null;
|
|
51
51
|
researcher;
|
|
52
52
|
fisherman = null;
|
|
53
|
+
scout = null;
|
|
53
54
|
constructor(deps, researcher) {
|
|
54
55
|
super();
|
|
55
56
|
this.explorer = deps.explorer;
|
|
@@ -62,9 +63,16 @@ export class Planner extends PlannerBase {
|
|
|
62
63
|
setFisherman(fisherman) {
|
|
63
64
|
this.fisherman = fisherman;
|
|
64
65
|
}
|
|
66
|
+
setScout(scout) {
|
|
67
|
+
this.scout = scout;
|
|
68
|
+
}
|
|
65
69
|
get sectionOrder() {
|
|
66
70
|
return ConfigParser.getInstance().getConfig().ai?.agents?.researcher?.sections || Object.keys(POSSIBLE_SECTIONS);
|
|
67
71
|
}
|
|
72
|
+
get docsWeight() {
|
|
73
|
+
const value = ConfigParser.getInstance().getConfig().ai?.agents?.planner?.docsWeight ?? 70;
|
|
74
|
+
return Math.max(0, Math.min(100, value));
|
|
75
|
+
}
|
|
68
76
|
getDefaultStartUrl(state) {
|
|
69
77
|
return state.fullUrl || state.url;
|
|
70
78
|
}
|
|
@@ -291,6 +299,7 @@ export class Planner extends PlannerBase {
|
|
|
291
299
|
const conversation = new Conversation([], model);
|
|
292
300
|
conversation.autoTrimTag('page_research', 20000);
|
|
293
301
|
conversation.autoTrimTag('tested_scenarios', 10000);
|
|
302
|
+
conversation.autoTrimTag('docs_context', 8000);
|
|
294
303
|
conversation.addUserText(this.getSystemMessage(feature));
|
|
295
304
|
const planningPrompt = dedent `
|
|
296
305
|
<task>
|
|
@@ -345,6 +354,10 @@ export class Planner extends PlannerBase {
|
|
|
345
354
|
const research = await this.researcher.research(currentState || state, {
|
|
346
355
|
deep: true,
|
|
347
356
|
});
|
|
357
|
+
let docsPromise = null;
|
|
358
|
+
if (this.scout && this.docsWeight > 0) {
|
|
359
|
+
docsPromise = this.scout.collectDocs({ url: state.url, title: state.title, feature, excludeUrls: this.knowledgeTracker.applicationSpecUrls(state) });
|
|
360
|
+
}
|
|
348
361
|
let plannerResearch = mdq(research).query('code').replace('');
|
|
349
362
|
plannerResearch = mdq(plannerResearch)
|
|
350
363
|
.query('table')
|
|
@@ -376,6 +389,21 @@ export class Planner extends PlannerBase {
|
|
|
376
389
|
if (applicationContext) {
|
|
377
390
|
conversation.addUserText(applicationContext);
|
|
378
391
|
}
|
|
392
|
+
if (docsPromise) {
|
|
393
|
+
const docs = await docsPromise;
|
|
394
|
+
if (docs) {
|
|
395
|
+
conversation.addUserText(dedent `
|
|
396
|
+
<docs_context>
|
|
397
|
+
Documentation retrieved from the collected corpus by the Scout agent.
|
|
398
|
+
Ground scenarios in these documented capabilities where they apply; treat them as supporting context, not a script.
|
|
399
|
+
|
|
400
|
+
Aim for roughly ${this.docsWeight}% of the scenarios to exercise behavior documented above; the remainder may explore beyond the documentation.
|
|
401
|
+
|
|
402
|
+
${docs}
|
|
403
|
+
</docs_context>
|
|
404
|
+
`);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
379
407
|
conversation.addUserText(dedent `
|
|
380
408
|
${this.buildApproach(style)}
|
|
381
409
|
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -366,7 +366,9 @@ export class Provider {
|
|
|
366
366
|
const modelName = getModelName(model);
|
|
367
367
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
368
368
|
promptLog(`Using model: ${modelName}`);
|
|
369
|
-
|
|
369
|
+
let toolsWithCommentary = tools;
|
|
370
|
+
if (!tools?.commentary && options.toolChoice !== 'required')
|
|
371
|
+
toolsWithCommentary = { ...tools, commentary: createHarmonyChannelFallbackTool() };
|
|
370
372
|
const toolNames = Object.keys(toolsWithCommentary || {});
|
|
371
373
|
tag('debug').log(`Tools enabled: [${toolNames.join(', ')}]`);
|
|
372
374
|
promptLog('Available tools:', toolNames);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ActionResult, type Diff } from '../../action-result.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type ExplorbotConfig } from '../../config.js';
|
|
3
3
|
import type Explorer from '../../explorer.js';
|
|
4
4
|
import type { StateManager } from '../../state-manager.js';
|
|
5
5
|
import { WebPageState } from '../../state-manager.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult } from '../../action-result.js';
|
|
3
|
+
import { agentSettings } from "../../config.js";
|
|
3
4
|
import { executionController } from "../../execution-controller.js";
|
|
4
5
|
import { diffAriaSnapshots } from "../../utils/aria.js";
|
|
5
6
|
import { extractCodeBlocks } from "../../utils/code-extractor.js";
|
|
@@ -16,7 +17,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
16
17
|
async performDeepAnalysis(state, result) {
|
|
17
18
|
tag('info').log('Starting deep analysis of expandable elements');
|
|
18
19
|
await this.navigateTo(state.fullUrl || state.url);
|
|
19
|
-
const maxClicks = this.config.
|
|
20
|
+
const maxClicks = agentSettings(this.config, 'researcher').maxExpandableClicks ?? DEFAULT_MAX_EXPANDABLE_CLICKS;
|
|
20
21
|
const expandedSections = [];
|
|
21
22
|
const navigationLinks = [];
|
|
22
23
|
let verifiedCodes = [];
|
|
@@ -62,6 +63,8 @@ export function WithDeepAnalysis(Base) {
|
|
|
62
63
|
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
63
64
|
}
|
|
64
65
|
async researchOverlay(current, previous, pageStateHash) {
|
|
66
|
+
if (!this.isEnabled())
|
|
67
|
+
return null;
|
|
65
68
|
const region = current.overlay;
|
|
66
69
|
if (!region.isOpen || !region.name)
|
|
67
70
|
return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ActionResult } from '../../action-result.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type ExplorbotConfig } from '../../config.js';
|
|
3
3
|
import type Explorer from '../../explorer.js';
|
|
4
4
|
import type { StateManager } from '../../state-manager.js';
|
|
5
5
|
import type { Provider } from '../provider.js';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
|
+
import { agentSettings } from "../../config.js";
|
|
2
3
|
import { executionController } from "../../execution-controller.js";
|
|
3
4
|
import { tag } from '../../utils/logger.js';
|
|
4
5
|
import { RulesLoader } from "../../utils/rules-loader.js";
|
|
@@ -50,7 +51,7 @@ export function WithSections(Base) {
|
|
|
50
51
|
return focused.text;
|
|
51
52
|
}
|
|
52
53
|
async _detectFocusCss() {
|
|
53
|
-
const focusSections = this.config.
|
|
54
|
+
const focusSections = agentSettings(this.config, 'researcher').focusSections;
|
|
54
55
|
if (!focusSections?.length)
|
|
55
56
|
return null;
|
|
56
57
|
for (const css of focusSections) {
|