explorbot 0.4.2 → 0.4.4
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/bin/explorbot-cli.ts +6 -1
- 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 +33 -43
- package/dist/bin/explorbot-cli.js +5 -1
- 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 +29 -41
- 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/request-haul.d.ts +1 -0
- package/dist/src/ai/fisherman/request-haul.js +3 -0
- package/dist/src/ai/fisherman/tools.d.ts +50 -0
- package/dist/src/ai/{fisherman-tools.js → fisherman/tools.js} +78 -13
- package/dist/src/ai/fisherman.d.ts +12 -3
- package/dist/src/ai/fisherman.js +89 -13
- package/dist/src/ai/pilot.d.ts +13 -1
- package/dist/src/ai/pilot.js +20 -7
- 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 +2 -0
- package/dist/src/ai/tester.d.ts +1 -0
- package/dist/src/ai/tester.js +27 -33
- package/dist/src/ai/tools.js +5 -0
- package/dist/src/api/request-result.js +3 -1
- package/dist/src/api/request-store.d.ts +6 -1
- package/dist/src/api/request-store.js +55 -17
- package/dist/src/api/xhr-capture.d.ts +2 -0
- package/dist/src/api/xhr-capture.js +35 -10
- package/dist/src/commands/config-command.js +6 -2
- package/dist/src/commands/help-json-command.d.ts +31 -0
- package/dist/src/commands/help-json-command.js +58 -0
- package/dist/src/config.d.ts +3 -0
- package/dist/src/config.js +14 -0
- package/dist/src/state-manager.js +5 -1
- package/docs/api-testing/basics.md +12 -4
- package/docs/reference/commands.md +2 -0
- package/docs/reference/configuration.md +4 -0
- package/docs/superpowers/plans/2026-09-03-fisherman-query-api.md +1361 -0
- package/docs/workflow/agentic-usage.md +15 -1
- package/package.json +1 -1
- package/src/action-result.ts +7 -0
- package/src/action.ts +14 -2
- package/src/ai/fisherman/request-haul.ts +4 -0
- package/src/ai/{fisherman-tools.ts → fisherman/tools.ts} +93 -20
- package/src/ai/fisherman.ts +104 -15
- package/src/ai/pilot.ts +20 -7
- 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 +2 -0
- package/src/ai/tester.ts +25 -30
- package/src/ai/tools.ts +6 -0
- package/src/api/request-result.ts +2 -1
- package/src/api/request-store.ts +58 -18
- package/src/api/xhr-capture.ts +39 -11
- package/src/commands/config-command.ts +4 -1
- package/src/commands/help-json-command.ts +74 -0
- package/src/config.ts +16 -0
- package/src/state-manager.ts +6 -1
- package/dist/src/ai/fisherman-tools.d.ts +0 -147
package/dist/src/ai/fisherman.js
CHANGED
|
@@ -4,8 +4,8 @@ import { listAllEndpoints } from "../api/spec-reader.js";
|
|
|
4
4
|
import { createDebug, tag } from "../utils/logger.js";
|
|
5
5
|
const debugLog = createDebug('explorbot:fisherman');
|
|
6
6
|
import { loop } from "../utils/loop.js";
|
|
7
|
-
import { createFishermanTools } from "./fisherman-tools.js";
|
|
8
7
|
import { RequestHaul } from "./fisherman/request-haul.js";
|
|
8
|
+
import { createFishermanTools } from "./fisherman/tools.js";
|
|
9
9
|
import { dataProtectionRules } from "./rules.js";
|
|
10
10
|
const MAX_ITERATIONS = 15;
|
|
11
11
|
const MAX_TOOL_ROUNDTRIPS = 5;
|
|
@@ -58,7 +58,6 @@ export class Fisherman {
|
|
|
58
58
|
debugLog(`endpoints:\n${endpointList || '(none)'}`);
|
|
59
59
|
if (!endpointList) {
|
|
60
60
|
tag('warning').log('Fisherman: no endpoints available');
|
|
61
|
-
this.mode = 'disabled';
|
|
62
61
|
return { success: false, summary: 'No API endpoints available', created: [], failed: [] };
|
|
63
62
|
}
|
|
64
63
|
await this.refreshAuth();
|
|
@@ -70,6 +69,47 @@ export class Fisherman {
|
|
|
70
69
|
});
|
|
71
70
|
const conversation = this.provider.startConversation(this.buildSystemPrompt(endpointList, Object.keys(tools), scopeUrl), 'fisherman');
|
|
72
71
|
conversation.addUserText(this.buildTaskPrompt(instructions));
|
|
72
|
+
await this.runSession(conversation, tools, { haul, isFinished, finishFromText, label: `fisherman: ${instructions.slice(0, 50)}` });
|
|
73
|
+
const result = getResult();
|
|
74
|
+
tag('info').log(`Fisherman result: ${result.summary}`);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
async lookupData(question, scopeUrl, sessionName) {
|
|
78
|
+
this.sessionName = sessionName;
|
|
79
|
+
tag('info').log(`Fisherman [read]: ${question}`);
|
|
80
|
+
await this.ensureReady(scopeUrl);
|
|
81
|
+
if (this.mode === 'disabled') {
|
|
82
|
+
debugLog('disabled — no data for scope');
|
|
83
|
+
return { success: false, summary: 'No API data available for this scope', created: [], failed: [] };
|
|
84
|
+
}
|
|
85
|
+
const endpointList = this.buildEndpointList(scopeUrl, 'read');
|
|
86
|
+
debugLog(`read endpoints:\n${endpointList || '(none)'}`);
|
|
87
|
+
if (!endpointList) {
|
|
88
|
+
tag('warning').log('Fisherman: no read endpoints available');
|
|
89
|
+
return { success: false, summary: 'No read endpoints are known for this scope', created: [], failed: [] };
|
|
90
|
+
}
|
|
91
|
+
await this.refreshAuth();
|
|
92
|
+
const haul = new RequestHaul(this.requestStore);
|
|
93
|
+
const { tools, getResult, isFinished, finishFromText } = createFishermanTools(this.apiClient, this.requestStore, haul, {
|
|
94
|
+
spec: this.spec,
|
|
95
|
+
baseEndpoint: this.baseEndpoint,
|
|
96
|
+
readOnly: true,
|
|
97
|
+
});
|
|
98
|
+
const conversation = this.provider.startConversation(this.buildLookupSystemPrompt(endpointList, Object.keys(tools), scopeUrl), 'fisherman');
|
|
99
|
+
conversation.addUserText(dedent `
|
|
100
|
+
Answer this question about data that already exists:
|
|
101
|
+
|
|
102
|
+
${question}
|
|
103
|
+
|
|
104
|
+
Make the requests needed to answer it, then call finish with the answer.
|
|
105
|
+
If the available endpoints cannot answer it, call stop with the reason.
|
|
106
|
+
`);
|
|
107
|
+
await this.runSession(conversation, tools, { haul, isFinished, finishFromText, label: `fisherman lookup: ${question.slice(0, 50)}` });
|
|
108
|
+
const result = getResult();
|
|
109
|
+
tag('info').log(`Fisherman answer: ${result.summary}`);
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
async runSession(conversation, tools, opts) {
|
|
73
113
|
await loop(async ({ stop, iteration }) => {
|
|
74
114
|
debugLog(`iteration ${iteration}`);
|
|
75
115
|
const invokeResult = await this.provider.invokeConversation(conversation, tools, {
|
|
@@ -77,17 +117,17 @@ export class Fisherman {
|
|
|
77
117
|
agentName: 'fisherman',
|
|
78
118
|
});
|
|
79
119
|
debugLog(`iteration ${iteration} done, text: ${invokeResult?.response?.text?.slice(0, 200) || '(none)'}`);
|
|
80
|
-
if (isFinished()) {
|
|
120
|
+
if (opts.isFinished()) {
|
|
81
121
|
stop();
|
|
82
122
|
return;
|
|
83
123
|
}
|
|
84
124
|
if (!invokeResult?.toolExecutions?.length) {
|
|
85
125
|
debugLog('no tool call in this turn — treating as finish');
|
|
86
|
-
finishFromText(invokeResult?.response?.text);
|
|
126
|
+
opts.finishFromText(invokeResult?.response?.text);
|
|
87
127
|
stop();
|
|
88
128
|
return;
|
|
89
129
|
}
|
|
90
|
-
if (this.isStuckOnEndpoint(haul)) {
|
|
130
|
+
if (this.isStuckOnEndpoint(opts.haul)) {
|
|
91
131
|
tag('warning').log('Fisherman: repeated failures on the same endpoint — stopping');
|
|
92
132
|
stop();
|
|
93
133
|
return;
|
|
@@ -99,7 +139,7 @@ export class Fisherman {
|
|
|
99
139
|
}, {
|
|
100
140
|
maxAttempts: MAX_ITERATIONS,
|
|
101
141
|
observability: {
|
|
102
|
-
name:
|
|
142
|
+
name: opts.label,
|
|
103
143
|
agent: 'fisherman',
|
|
104
144
|
sessionId: this.sessionName,
|
|
105
145
|
},
|
|
@@ -109,9 +149,6 @@ export class Fisherman {
|
|
|
109
149
|
stop();
|
|
110
150
|
},
|
|
111
151
|
});
|
|
112
|
-
const result = getResult();
|
|
113
|
-
tag('info').log(`Fisherman result: ${result.summary}`);
|
|
114
|
-
return result;
|
|
115
152
|
}
|
|
116
153
|
async detectMode(scopeUrl) {
|
|
117
154
|
if (this.hasApiConfig) {
|
|
@@ -143,18 +180,20 @@ export class Fisherman {
|
|
|
143
180
|
this.apiClient.setHeaders(this.configHeaders);
|
|
144
181
|
}
|
|
145
182
|
}
|
|
146
|
-
buildEndpointList(scopeUrl) {
|
|
183
|
+
buildEndpointList(scopeUrl, family = 'write') {
|
|
147
184
|
this.scopeDegraded = false;
|
|
148
185
|
if (this.mode === 'achieve' && this.spec) {
|
|
149
|
-
|
|
186
|
+
let specEndpoints = listAllEndpoints(this.spec, this.baseEndpoint);
|
|
187
|
+
if (family === 'read')
|
|
188
|
+
specEndpoints = keepReadLines(specEndpoints);
|
|
150
189
|
if (specEndpoints)
|
|
151
190
|
return specEndpoints;
|
|
152
191
|
}
|
|
153
|
-
const scoped = this.requestStore.toEndpointList(scopeUrl || '/');
|
|
192
|
+
const scoped = this.requestStore.toEndpointList(scopeUrl || '/', family);
|
|
154
193
|
if (scoped)
|
|
155
194
|
return scoped;
|
|
156
195
|
this.scopeDegraded = true;
|
|
157
|
-
return this.requestStore.toEndpointList();
|
|
196
|
+
return this.requestStore.toEndpointList(undefined, family);
|
|
158
197
|
}
|
|
159
198
|
buildSystemPrompt(endpointList, toolNames, scopeUrl) {
|
|
160
199
|
let scopeBlock = '';
|
|
@@ -192,6 +231,37 @@ export class Fisherman {
|
|
|
192
231
|
${dataProtectionRules}
|
|
193
232
|
`;
|
|
194
233
|
}
|
|
234
|
+
buildLookupSystemPrompt(endpointList, toolNames, scopeUrl) {
|
|
235
|
+
let scopeBlock = '';
|
|
236
|
+
if (scopeUrl) {
|
|
237
|
+
scopeBlock = `\n\nSCOPE: You are answering about ${scopeUrl}.`;
|
|
238
|
+
if (this.scopeDegraded)
|
|
239
|
+
scopeBlock += '\nThe endpoint list could not be narrowed to this scope and may include endpoints belonging to other scopes. Prefer the endpoint whose path belongs to this scope.';
|
|
240
|
+
}
|
|
241
|
+
return dedent `
|
|
242
|
+
You are Fisherman — reading the API to report what data already exists. You change nothing.
|
|
243
|
+
|
|
244
|
+
AVAILABLE ENDPOINTS:
|
|
245
|
+
${endpointList}
|
|
246
|
+
${scopeBlock}
|
|
247
|
+
|
|
248
|
+
AVAILABLE TOOLS:
|
|
249
|
+
${toolNames.join(', ')}.
|
|
250
|
+
Use tool names exactly as listed. Do not invent aliases or combined names.
|
|
251
|
+
Match each tool input schema exactly. Do not invent parameter names or pass extra fields.
|
|
252
|
+
|
|
253
|
+
WORKFLOW:
|
|
254
|
+
1. Pick the endpoint that lists the kind of item the question is about
|
|
255
|
+
2. Request it, and when the answer needs a parent resource, request the parent first and use its id
|
|
256
|
+
3. Call finish with the answer, quoting the concrete names, titles and ids the responses returned
|
|
257
|
+
|
|
258
|
+
RULES:
|
|
259
|
+
- Report only what a response actually returned. Never describe data you did not read
|
|
260
|
+
- Report an empty collection as empty. An absent item must not be reported as present
|
|
261
|
+
- Answer the question that was asked and stop. Do not survey unrelated endpoints
|
|
262
|
+
- Use the response category and error text to correct a failed request. Retry a temporary or server failure once
|
|
263
|
+
`;
|
|
264
|
+
}
|
|
195
265
|
isStuckOnEndpoint(haul) {
|
|
196
266
|
const made = haul.requests();
|
|
197
267
|
if (made.length < REPEATED_FAILURE_LIMIT)
|
|
@@ -213,3 +283,9 @@ export class Fisherman {
|
|
|
213
283
|
`;
|
|
214
284
|
}
|
|
215
285
|
}
|
|
286
|
+
function keepReadLines(endpointList) {
|
|
287
|
+
return endpointList
|
|
288
|
+
.split('\n')
|
|
289
|
+
.filter((line) => line.startsWith('GET '))
|
|
290
|
+
.join('\n');
|
|
291
|
+
}
|
package/dist/src/ai/pilot.d.ts
CHANGED
|
@@ -49,7 +49,19 @@ export declare class Pilot implements Agent {
|
|
|
49
49
|
}): Promise<string>;
|
|
50
50
|
getExperienceToc(): string;
|
|
51
51
|
pickPlanningTools(): Record<string, unknown>;
|
|
52
|
-
|
|
52
|
+
fishermanStatus(): string;
|
|
53
|
+
buildFishermanTools(task: Test): {
|
|
54
|
+
askApi: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
55
|
+
question: any;
|
|
56
|
+
}, {
|
|
57
|
+
answered: boolean;
|
|
58
|
+
reason: string;
|
|
59
|
+
answer?: undefined;
|
|
60
|
+
} | {
|
|
61
|
+
answered: boolean;
|
|
62
|
+
answer: string;
|
|
63
|
+
reason?: undefined;
|
|
64
|
+
}, import("@ai-sdk/provider-utils").Context>>;
|
|
53
65
|
precondition: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
54
66
|
description: any;
|
|
55
67
|
}, {
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -10,6 +10,7 @@ import { ErrorPageError } from "../utils/error-page.js";
|
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
const debugLog = createDebug('explorbot:pilot');
|
|
12
12
|
import { truncateJson } from "../utils/strings.js";
|
|
13
|
+
import { createAskApiTool } from "./fisherman/tools.js";
|
|
13
14
|
import { capabilityGroundingRule, dataProtectionRules } from "./rules.js";
|
|
14
15
|
import { isInteractive } from "./task-agent.js";
|
|
15
16
|
import { withdrawVisionTools } from "./tools.js";
|
|
@@ -398,7 +399,8 @@ export class Pilot {
|
|
|
398
399
|
|
|
399
400
|
Plan the test execution for this scenario.
|
|
400
401
|
|
|
401
|
-
FIRST: Decide if precondition() is needed.
|
|
402
|
+
FIRST: Decide if precondition() is needed. When the page does not settle whether suitable data
|
|
403
|
+
already exists, call askApi() to find out before creating any.
|
|
402
404
|
|
|
403
405
|
Call precondition() WHEN:
|
|
404
406
|
- The scenario edits/deletes/modifies an item, and you want a DISPOSABLE item to act on safely
|
|
@@ -603,7 +605,7 @@ export class Pilot {
|
|
|
603
605
|
finalUserText = `${tocBlock}\n\n${userText}`;
|
|
604
606
|
}
|
|
605
607
|
this.conversation.addUserText(finalUserText);
|
|
606
|
-
const tools = { ...this.pickPlanningTools(), ...this.
|
|
608
|
+
const tools = { ...this.pickPlanningTools(), ...this.buildFishermanTools(opts.task) };
|
|
607
609
|
const result = await this.provider.invokeConversation(this.conversation, tools, {
|
|
608
610
|
maxToolRoundtrips: opts.maxToolRoundtrips ?? 0,
|
|
609
611
|
toolChoice: opts.tools ? 'auto' : 'none',
|
|
@@ -655,7 +657,12 @@ export class Pilot {
|
|
|
655
657
|
withdrawVisionTools(planning);
|
|
656
658
|
return planning;
|
|
657
659
|
}
|
|
658
|
-
|
|
660
|
+
fishermanStatus() {
|
|
661
|
+
if (this.fisherman?.isAvailable())
|
|
662
|
+
return 'available';
|
|
663
|
+
return 'none';
|
|
664
|
+
}
|
|
665
|
+
buildFishermanTools(task) {
|
|
659
666
|
const unavailable = 'Data was not created and cannot be created automatically. Do not call precondition again for this test — continue with what the page already shows.';
|
|
660
667
|
return {
|
|
661
668
|
precondition: tool({
|
|
@@ -666,7 +673,7 @@ export class Pilot {
|
|
|
666
673
|
execute: async ({ description }) => {
|
|
667
674
|
task.addNote(`Precondition: ${description}`);
|
|
668
675
|
tag('info').log(`Precondition: ${description}`);
|
|
669
|
-
debugLog(`precondition: ${description}, fisherman: ${this.
|
|
676
|
+
debugLog(`precondition: ${description}, fisherman: ${this.fishermanStatus()}`);
|
|
670
677
|
if (!this.fisherman || !this.fisherman.isAvailable()) {
|
|
671
678
|
const skipReason = await this.checkDataAvailability(task, description, 'Fisherman not available');
|
|
672
679
|
if (skipReason)
|
|
@@ -698,6 +705,7 @@ export class Pilot {
|
|
|
698
705
|
return { noted: true, prepared: true, created: result.created };
|
|
699
706
|
},
|
|
700
707
|
}),
|
|
708
|
+
...createAskApiTool(this.fisherman, task),
|
|
701
709
|
};
|
|
702
710
|
}
|
|
703
711
|
async checkDataAvailability(task, requestedData, fishermanReason) {
|
|
@@ -1047,7 +1055,7 @@ export class Pilot {
|
|
|
1047
1055
|
- Click SUCCESS but executed locator ≠ explanation intent, or "skipped" attempts present → wrong element clicked.
|
|
1048
1056
|
- form(I.type()) SUCCESS but "element" shows a button/link → keys went to wrong element; click the input first.
|
|
1049
1057
|
- ariaDiff shows 5+ added/removed → page entered new mode (editor/modal); call context() before guessing selectors.
|
|
1050
|
-
- Empty dropdown/list when items expected → wait explicitly, then check the state changed: ariaDiff and any GET that loaded data. If still nothing loaded, confirm the empty state with verify().
|
|
1058
|
+
- Empty dropdown/list when items expected → wait explicitly, then check the state changed: ariaDiff and any GET that loaded data. If still nothing loaded, confirm the empty state with verify(), or askApi() for whether the data exists at all.
|
|
1051
1059
|
- Search-and-select needs SEQUENCE: focus trigger → type to filter → click option. Tell Tester to split into separate tool calls.
|
|
1052
1060
|
- Multi-action explanation in one tool call → instruct Tester to split.
|
|
1053
1061
|
|
|
@@ -1063,8 +1071,13 @@ export class Pilot {
|
|
|
1063
1071
|
|
|
1064
1072
|
${capabilityGroundingRule}
|
|
1065
1073
|
|
|
1066
|
-
YOUR Pilot-only
|
|
1067
|
-
|
|
1074
|
+
YOUR Pilot-only tools, both over the API:
|
|
1075
|
+
|
|
1076
|
+
askApi(question) — ask what data already exists. It changes nothing. Use it to check whether
|
|
1077
|
+
suitable data is already there before creating any, and to get the exact name or id of an existing
|
|
1078
|
+
record a step must act on.
|
|
1079
|
+
|
|
1080
|
+
precondition(description) — create FRESH disposable test data. Never request users. Use when:
|
|
1068
1081
|
|
|
1069
1082
|
- Scenario edits/deletes/modifies an item → create a disposable target ("1 post").
|
|
1070
1083
|
- Scenario needs auxiliary data (labels, categories, statuses for filtering).
|
|
@@ -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) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult } from '../action-result.js';
|
|
3
3
|
import { setActivity } from "../activity.js";
|
|
4
|
-
import { outputPath } from "../config.js";
|
|
4
|
+
import { agentSettings, outputPath } from "../config.js";
|
|
5
5
|
import { executionController } from "../execution-controller.js";
|
|
6
6
|
import { Observability } from "../observability.js";
|
|
7
7
|
import { Stats } from "../stats.js";
|
|
@@ -13,7 +13,7 @@ import { mdq } from "../utils/markdown-query.js";
|
|
|
13
13
|
import { RulesLoader } from "../utils/rules-loader.js";
|
|
14
14
|
import { annotatePageElements } from "../utils/web-annotate.js";
|
|
15
15
|
import { ContextLengthError } from './provider.js';
|
|
16
|
-
import { findSimilarResearch, getCachedResearch, reportResearch, saveResearch } from "./researcher/cache.js";
|
|
16
|
+
import { findSimilarResearch, getCachedResearch, getPreviousResearch, reportResearch, saveResearch } from "./researcher/cache.js";
|
|
17
17
|
import { WithCoordinates } from "./researcher/coordinates.js";
|
|
18
18
|
import { WithDeepAnalysis } from "./researcher/deep-analysis.js";
|
|
19
19
|
import { detectFocusedSection, hasFocusedSection, markSectionAsFocused, pickDefaultFocusedSection } from "./researcher/focus.js";
|
|
@@ -42,12 +42,19 @@ export class Researcher extends ResearcherBase {
|
|
|
42
42
|
constructor(deps) {
|
|
43
43
|
super(deps);
|
|
44
44
|
this.experienceTracker = deps.stateManager.getExperienceTracker();
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
45
|
+
this.settings.reasoning ??= 'low';
|
|
46
|
+
}
|
|
47
|
+
get settings() {
|
|
48
|
+
return agentSettings(this.config, 'researcher');
|
|
49
|
+
}
|
|
50
|
+
isEnabled() {
|
|
51
|
+
return this.settings.enabled !== false;
|
|
52
|
+
}
|
|
53
|
+
enable() {
|
|
54
|
+
this.settings.enabled = true;
|
|
55
|
+
}
|
|
56
|
+
disable() {
|
|
57
|
+
this.settings.enabled = false;
|
|
51
58
|
}
|
|
52
59
|
getNavigator() {
|
|
53
60
|
throw new Error('not implemented');
|
|
@@ -70,7 +77,7 @@ export class Researcher extends ResearcherBase {
|
|
|
70
77
|
}
|
|
71
78
|
async research(state, opts = {}) {
|
|
72
79
|
const { screenshot = false, force = false, deep = false, data = false, fix = true } = opts;
|
|
73
|
-
const maxRetries = this.
|
|
80
|
+
const maxRetries = this.settings.retries ?? 2;
|
|
74
81
|
let retriesLeft = opts._retriesLeft ?? maxRetries;
|
|
75
82
|
this.actionResult = ActionResult.fromState(state);
|
|
76
83
|
const stateHash = this.actionResult.baseHash;
|
|
@@ -83,6 +90,13 @@ export class Researcher extends ResearcherBase {
|
|
|
83
90
|
return cached;
|
|
84
91
|
}
|
|
85
92
|
}
|
|
93
|
+
if (!this.isEnabled()) {
|
|
94
|
+
debugLog('Researcher is disabled, answering with the recorded map');
|
|
95
|
+
const recorded = getPreviousResearch(stateHash);
|
|
96
|
+
if (recorded)
|
|
97
|
+
reportResearch(stateHash, recorded);
|
|
98
|
+
return recorded;
|
|
99
|
+
}
|
|
86
100
|
Stats.researches++;
|
|
87
101
|
const sessionName = `researcher: ${state.url}`;
|
|
88
102
|
return Observability.run(sessionName, { tags: ['researcher'], sessionId: stateHash }, async () => {
|
|
@@ -286,7 +300,7 @@ export class Researcher extends ResearcherBase {
|
|
|
286
300
|
this.actionResult = await this.explorer.visit(url, { screenshot: screenshot ?? false });
|
|
287
301
|
}
|
|
288
302
|
async waitUntilSettled(screenshot) {
|
|
289
|
-
const errorPageTimeout = this.
|
|
303
|
+
const errorPageTimeout = this.settings.errorPageTimeout ?? 10;
|
|
290
304
|
if (errorPageTimeout <= 0)
|
|
291
305
|
return false;
|
|
292
306
|
const includeScreenshot = screenshot && this.provider.hasVision();
|
|
@@ -316,7 +330,7 @@ export class Researcher extends ResearcherBase {
|
|
|
316
330
|
return false;
|
|
317
331
|
}
|
|
318
332
|
getConfiguredSections() {
|
|
319
|
-
const configSections = this.
|
|
333
|
+
const configSections = this.settings.sections;
|
|
320
334
|
if (!configSections?.length)
|
|
321
335
|
return POSSIBLE_SECTIONS;
|
|
322
336
|
const filtered = {};
|
package/dist/src/ai/rules.js
CHANGED
|
@@ -163,6 +163,8 @@ export const dataProtectionRules = dedent `
|
|
|
163
163
|
Do not use Fisherman or API data preparation to bypass a no-mutation, read-only, search,
|
|
164
164
|
filter, tab, or list-inspection constraint. Use visible existing data when it is available.
|
|
165
165
|
If no suitable data exists, report the missing precondition instead of creating data.
|
|
166
|
+
Reading through the API to establish what already exists is not a mutation and stays allowed
|
|
167
|
+
under a read-only constraint.
|
|
166
168
|
|
|
167
169
|
Destructive actions are allowed only against data created by the current scenario
|
|
168
170
|
or prepared for that scenario by Fisherman/API preconditions. Existing application data must
|
package/dist/src/ai/tester.d.ts
CHANGED
package/dist/src/ai/tester.js
CHANGED
|
@@ -40,6 +40,7 @@ export class Tester extends TaskAgent {
|
|
|
40
40
|
MAX_ITERATIONS = 30;
|
|
41
41
|
MAX_EXTENSIONS = 2;
|
|
42
42
|
ASSERTION_TOOLS = ['verify'];
|
|
43
|
+
pendingReview = '';
|
|
43
44
|
researcher;
|
|
44
45
|
navigator;
|
|
45
46
|
agentTools;
|
|
@@ -97,6 +98,7 @@ export class Tester extends TaskAgent {
|
|
|
97
98
|
this.seenUiMapUrls.clear();
|
|
98
99
|
this.lastAnalyzedStateHash = null;
|
|
99
100
|
this.stalledIterations = 0;
|
|
101
|
+
this.pendingReview = '';
|
|
100
102
|
this.previousRegionPresent = null;
|
|
101
103
|
this.regionTransitioned = false;
|
|
102
104
|
this.stateManager.clearHistory();
|
|
@@ -293,7 +295,7 @@ export class Tester extends TaskAgent {
|
|
|
293
295
|
const result = await this.provider.invokeConversation(conversation, tools, {
|
|
294
296
|
maxToolRoundtrips: 3,
|
|
295
297
|
toolChoice: 'required',
|
|
296
|
-
stopWhen: () => task.hasFinished,
|
|
298
|
+
stopWhen: () => task.hasFinished || !!this.pendingReview,
|
|
297
299
|
});
|
|
298
300
|
if (!result)
|
|
299
301
|
throw new Error('Failed to get response from provider');
|
|
@@ -339,6 +341,15 @@ export class Tester extends TaskAgent {
|
|
|
339
341
|
`);
|
|
340
342
|
}
|
|
341
343
|
}
|
|
344
|
+
if (this.pendingReview && this.pilot) {
|
|
345
|
+
const reviewed = this.pendingReview;
|
|
346
|
+
this.pendingReview = '';
|
|
347
|
+
const reviewState = this.getCurrentState();
|
|
348
|
+
if (reviewed === 'finish')
|
|
349
|
+
await this.pilot.reviewFinish(task, reviewState, conversation, this.navigator);
|
|
350
|
+
if (reviewed === 'stop')
|
|
351
|
+
await this.pilot.reviewStop(task, reviewState, conversation);
|
|
352
|
+
}
|
|
342
353
|
if (task.hasFinished) {
|
|
343
354
|
stop();
|
|
344
355
|
return;
|
|
@@ -513,17 +524,20 @@ export class Tester extends TaskAgent {
|
|
|
513
524
|
}
|
|
514
525
|
if (region.isModal) {
|
|
515
526
|
const areaName = region.name ? ` "${region.name}"` : '';
|
|
516
|
-
let
|
|
517
|
-
if (region.root)
|
|
518
|
-
|
|
527
|
+
let scoping = 'Use <page_aria> to confirm the element you target is actually inside the overlay.';
|
|
528
|
+
if (region.root) {
|
|
529
|
+
scoping = `Its root is \`${region.root}\` — build every locator as ARIA scoped to that root, e.g. I.click({ role: 'button', text: 'Continue' }, '${region.root}')`;
|
|
530
|
+
}
|
|
519
531
|
context += dedent `
|
|
520
532
|
<overlay>
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
Use <page_aria> to confirm the element you target is actually inside the overlay.
|
|
525
|
-
</overlay>
|
|
533
|
+
You are inside an overlay${areaName} opened above the page.
|
|
534
|
+
${scoping}
|
|
535
|
+
Elements outside the overlay are behind it and not actionable while it is open — they may share names or roles with the ones inside, so never target them by bare text.
|
|
526
536
|
`;
|
|
537
|
+
const regionAria = currentState.getRegionARIA();
|
|
538
|
+
if (regionAria)
|
|
539
|
+
context += `\nIt holds exactly these elements:\n<overlay_aria>\n${regionAria}\n</overlay_aria>`;
|
|
540
|
+
context += '\n</overlay>\n';
|
|
527
541
|
}
|
|
528
542
|
if (!region.isModal && region.isOpen && isNewState) {
|
|
529
543
|
let rootHint = '';
|
|
@@ -920,18 +934,8 @@ export class Tester extends TaskAgent {
|
|
|
920
934
|
}),
|
|
921
935
|
execute: async ({ reason }) => {
|
|
922
936
|
task.addNote(`Stop requested: ${reason}`);
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
await this.pilot.reviewStop(task, currentState, conversation);
|
|
926
|
-
if (!task.hasFinished) {
|
|
927
|
-
return {
|
|
928
|
-
success: false,
|
|
929
|
-
action: 'stop',
|
|
930
|
-
message: 'Stop rejected; Continue execution',
|
|
931
|
-
};
|
|
932
|
-
}
|
|
933
|
-
}
|
|
934
|
-
else {
|
|
937
|
+
this.pendingReview = 'stop';
|
|
938
|
+
if (!this.pilot) {
|
|
935
939
|
task.addNote(reason, TestResult.FAILED);
|
|
936
940
|
task.finish(TestResult.FAILED);
|
|
937
941
|
}
|
|
@@ -966,18 +970,8 @@ export class Tester extends TaskAgent {
|
|
|
966
970
|
return { success: true, action: 'finish', message: 'already finished' };
|
|
967
971
|
}
|
|
968
972
|
task.addNote(`Finish requested: ${verify}`);
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
await this.pilot.reviewFinish(task, currentState, conversation, this.navigator);
|
|
972
|
-
if (!task.hasFinished) {
|
|
973
|
-
return {
|
|
974
|
-
success: false,
|
|
975
|
-
action: 'finish',
|
|
976
|
-
message: 'Finishing rejected; Continue execution',
|
|
977
|
-
};
|
|
978
|
-
}
|
|
979
|
-
}
|
|
980
|
-
else {
|
|
973
|
+
this.pendingReview = 'finish';
|
|
974
|
+
if (!this.pilot) {
|
|
981
975
|
task.addNote('Test finished successfully', TestResult.PASSED);
|
|
982
976
|
task.finish(TestResult.PASSED);
|
|
983
977
|
}
|
package/dist/src/ai/tools.js
CHANGED
|
@@ -634,6 +634,11 @@ export function createAgentTools({ explorer, stateManager, ai, researcher, navig
|
|
|
634
634
|
return failedToolResult('research', 'No current page state available. Navigate to a page first.');
|
|
635
635
|
}
|
|
636
636
|
const researchResult = await researcher.research(currentState, { screenshot: true, data: true });
|
|
637
|
+
if (!researchResult) {
|
|
638
|
+
return failedToolResult('research', 'No UI map is available for this page.', {
|
|
639
|
+
suggestion: 'Use context() to read the page structure and act on the elements it lists.',
|
|
640
|
+
});
|
|
641
|
+
}
|
|
637
642
|
return successToolResult('research', {
|
|
638
643
|
analysis: researchResult,
|
|
639
644
|
aria: cap(ActionResult.fromState(currentState).getInteractiveARIA(), ARIA_OUTPUT_CAP),
|
|
@@ -96,7 +96,9 @@ export class RequestResult {
|
|
|
96
96
|
yaml += body;
|
|
97
97
|
}
|
|
98
98
|
writeFileSync(this.requestFile, yaml, 'utf8');
|
|
99
|
-
|
|
99
|
+
if (!this._rawResponseBody)
|
|
100
|
+
return;
|
|
101
|
+
writeFileSync(this.responseFile, this._rawResponseBody, 'utf8');
|
|
100
102
|
}
|
|
101
103
|
static load(requestFile) {
|
|
102
104
|
const content = readFileSync(requestFile, 'utf8');
|
|
@@ -6,8 +6,10 @@ export declare class RequestStore {
|
|
|
6
6
|
onFailedListeners: Array<(r: RequestResult) => void>;
|
|
7
7
|
outputDir: string;
|
|
8
8
|
sessionStartedAt: Date;
|
|
9
|
+
readEndpointKeys: Set<string>;
|
|
9
10
|
constructor(outputDir: string);
|
|
10
11
|
addCapturedRequest(result: RequestResult): void;
|
|
12
|
+
addReadRequest(result: RequestResult): void;
|
|
11
13
|
addFailedRequest(result: RequestResult): void;
|
|
12
14
|
getFailedRequests(): RequestResult[];
|
|
13
15
|
onFailedRequest(cb: (r: RequestResult) => void): () => void;
|
|
@@ -15,12 +17,15 @@ export declare class RequestStore {
|
|
|
15
17
|
getCapturedRequests(): RequestResult[];
|
|
16
18
|
getMadeRequests(): RequestResult[];
|
|
17
19
|
getLastRequest(): RequestResult | undefined;
|
|
18
|
-
toEndpointList(scopePath?: string): string;
|
|
20
|
+
toEndpointList(scopePath?: string, methods?: EndpointFamily): string;
|
|
19
21
|
extractAuthHeaders(): Record<string, string>;
|
|
20
22
|
findCapturedRequest(method: string, searchPath: string): RequestResult | undefined;
|
|
21
23
|
toLog(): string;
|
|
22
24
|
loadFromDisk(): void;
|
|
23
25
|
getWriteRequestsForScope(scopePath: string): RequestResult[];
|
|
26
|
+
getReadRequestsForScope(scopePath: string): RequestResult[];
|
|
24
27
|
clear(): void;
|
|
28
|
+
getRequestsForScope(scopePath: string, methods: EndpointFamily): RequestResult[];
|
|
25
29
|
}
|
|
26
30
|
export declare function isFailedRequest(request: RequestResult): boolean;
|
|
31
|
+
export type EndpointFamily = 'read' | 'write';
|