explorbot 0.2.5 → 0.3.1
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/prima/README.md +96 -0
- package/boat/prima/package.json +14 -10
- package/boat/prima/src/cli.ts +5 -0
- package/boat/prima/src/prima.ts +17 -4
- package/dist/boat/prima/src/cli.js +7 -0
- package/dist/boat/prima/src/prima.js +18 -4
- package/dist/models.json +4 -4
- package/dist/package.json +6 -2
- package/dist/src/action-result.d.ts +13 -0
- package/dist/src/action-result.js +46 -15
- package/dist/src/action.d.ts +5 -2
- package/dist/src/action.js +48 -17
- package/dist/src/ai/captain/web-mode.js +1 -2
- package/dist/src/ai/captain.d.ts +20 -0
- package/dist/src/ai/captain.js +10 -1
- package/dist/src/ai/conversation.d.ts +1 -0
- package/dist/src/ai/conversation.js +3 -0
- package/dist/src/ai/driller.js +6 -2
- package/dist/src/ai/fisherman-tools.d.ts +40 -1
- package/dist/src/ai/fisherman-tools.js +39 -0
- package/dist/src/ai/fisherman.js +3 -2
- package/dist/src/ai/navigator.d.ts +2 -1
- package/dist/src/ai/navigator.js +5 -9
- package/dist/src/ai/pilot.js +51 -29
- package/dist/src/ai/planner/subpages.js +2 -16
- package/dist/src/ai/planner.js +1 -1
- package/dist/src/ai/provider.d.ts +3 -0
- package/dist/src/ai/provider.js +80 -17
- package/dist/src/ai/researcher/cache.d.ts +8 -3
- package/dist/src/ai/researcher/cache.js +13 -8
- package/dist/src/ai/researcher/deep-analysis.js +1 -1
- package/dist/src/ai/researcher/fingerprint-worker.js +21 -4
- package/dist/src/ai/researcher.js +4 -3
- package/dist/src/ai/rules.js +1 -5
- package/dist/src/ai/tester.d.ts +1 -1
- package/dist/src/ai/tester.js +34 -26
- package/dist/src/ai/tools.d.ts +8 -5
- package/dist/src/ai/tools.js +79 -56
- package/dist/src/commands/explore-command.js +22 -17
- package/dist/src/commands/init-command.js +13 -20
- package/dist/src/config.d.ts +2 -1
- package/dist/src/config.js +3 -1
- package/dist/src/experience-tracker.d.ts +2 -0
- package/dist/src/experience-tracker.js +12 -0
- package/dist/src/explorbot.js +1 -1
- package/dist/src/playwright-recorder.js +6 -12
- package/dist/src/test-plan.d.ts +8 -0
- package/dist/src/test-plan.js +11 -0
- package/dist/src/utils/html-diff.d.ts +5 -0
- package/dist/src/utils/html-diff.js +65 -6
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +32 -0
- package/dist/src/utils/url-matcher.d.ts +1 -0
- package/dist/src/utils/url-matcher.js +31 -2
- package/docs/basics/getting-started.md +33 -10
- package/docs/basics/providers.md +6 -4
- package/docs/contributing/npm-package.md +73 -4
- package/models.json +4 -4
- package/package.json +6 -2
- package/src/action-result.ts +61 -16
- package/src/action.ts +51 -17
- package/src/ai/captain/web-mode.ts +1 -2
- package/src/ai/captain.ts +9 -1
- package/src/ai/conversation.ts +3 -0
- package/src/ai/driller.ts +6 -2
- package/src/ai/fisherman-tools.ts +35 -0
- package/src/ai/fisherman.ts +3 -2
- package/src/ai/navigator.ts +6 -10
- package/src/ai/pilot.ts +54 -32
- package/src/ai/planner/subpages.ts +2 -13
- package/src/ai/planner.ts +1 -1
- package/src/ai/provider.ts +111 -41
- package/src/ai/researcher/cache.ts +17 -9
- package/src/ai/researcher/deep-analysis.ts +1 -1
- package/src/ai/researcher/fingerprint-worker.ts +23 -5
- package/src/ai/researcher.ts +4 -3
- package/src/ai/rules.ts +1 -5
- package/src/ai/tester.ts +32 -27
- package/src/ai/tools.ts +84 -60
- package/src/commands/explore-command.ts +17 -14
- package/src/commands/init-command.ts +14 -20
- package/src/config.ts +4 -2
- package/src/experience-tracker.ts +13 -0
- package/src/explorbot.ts +1 -1
- package/src/playwright-recorder.ts +6 -11
- package/src/test-plan.ts +18 -0
- package/src/utils/html-diff.ts +72 -7
- package/src/utils/strings.ts +36 -0
- package/src/utils/url-matcher.ts +27 -2
package/dist/src/action.js
CHANGED
|
@@ -17,6 +17,8 @@ const debugLog = createDebug('explorbot:action');
|
|
|
17
17
|
const CAPTURE_NAVIGATION_TRANSITION_ATTEMPTS = 3;
|
|
18
18
|
const DEFAULT_ACTION_TIMEOUT = 3000;
|
|
19
19
|
const DEFAULT_PAGE_TIMEOUT = 3000;
|
|
20
|
+
const MAX_NETWORK_CALLS = 10;
|
|
21
|
+
const IMPORTANT_LOG_LEVELS = new Set(['info', 'error', 'warning', 'warn']);
|
|
20
22
|
class Action {
|
|
21
23
|
actor;
|
|
22
24
|
stateManager;
|
|
@@ -32,6 +34,8 @@ class Action {
|
|
|
32
34
|
recorder;
|
|
33
35
|
recovery;
|
|
34
36
|
mainDocumentStatus = undefined;
|
|
37
|
+
networkRequests = [];
|
|
38
|
+
baseOrigin;
|
|
35
39
|
constructor(actor, stateManager, recorder, recovery) {
|
|
36
40
|
this.actor = actor;
|
|
37
41
|
this.stateManager = stateManager;
|
|
@@ -39,6 +43,7 @@ class Action {
|
|
|
39
43
|
this.playwrightHelper = container.helpers('Playwright');
|
|
40
44
|
this.recorder = recorder;
|
|
41
45
|
this.recovery = recovery || ((fn) => fn());
|
|
46
|
+
this.baseOrigin = URL.parse(this.config.playwright?.url || '')?.origin || '';
|
|
42
47
|
}
|
|
43
48
|
async saveScreenshot() {
|
|
44
49
|
const currentState = this.stateManager.getCurrentState();
|
|
@@ -117,9 +122,7 @@ class Action {
|
|
|
117
122
|
const logPath = join(statesDir, logFile);
|
|
118
123
|
const formattedLogs = browserLogs.map((log) => {
|
|
119
124
|
const logTimestamp = new Date().toISOString();
|
|
120
|
-
|
|
121
|
-
const message = log.text || log.message || String(log);
|
|
122
|
-
return `[${logTimestamp}] ${level}: ${message}`;
|
|
125
|
+
return `[${logTimestamp}] ${log.type.toUpperCase()}: ${log.text}`;
|
|
123
126
|
});
|
|
124
127
|
fs.writeFileSync(logPath, `${formattedLogs.join('\n')}\n`, 'utf8');
|
|
125
128
|
debugLog('Page:', { url, title, size: html.length, html: html.substring(0, 100) });
|
|
@@ -142,12 +145,15 @@ class Action {
|
|
|
142
145
|
fs.writeFileSync(ariaPath, ariaSnapshot, 'utf8');
|
|
143
146
|
ariaSnapshotFile = ariaFileName;
|
|
144
147
|
}
|
|
148
|
+
const networkRequests = this.networkRequests;
|
|
149
|
+
this.networkRequests = [];
|
|
145
150
|
const result = new ActionResult({
|
|
146
151
|
html,
|
|
147
152
|
title,
|
|
148
153
|
httpStatus: await this.captureMainDocumentStatus(),
|
|
149
154
|
url,
|
|
150
155
|
browserLogs,
|
|
156
|
+
networkRequests,
|
|
151
157
|
htmlFile,
|
|
152
158
|
logFile,
|
|
153
159
|
screenshotFile,
|
|
@@ -192,27 +198,52 @@ class Action {
|
|
|
192
198
|
return undefined;
|
|
193
199
|
}
|
|
194
200
|
}
|
|
195
|
-
|
|
201
|
+
captureResponses() {
|
|
196
202
|
const page = this.playwrightHelper.page;
|
|
197
203
|
if (!page?.on || !page?.off)
|
|
198
204
|
return () => { };
|
|
199
205
|
this.mainDocumentStatus = undefined;
|
|
206
|
+
this.networkRequests = [];
|
|
200
207
|
const handler = (response) => {
|
|
201
208
|
const request = response.request();
|
|
202
|
-
if (request.resourceType() !== 'document')
|
|
203
|
-
return;
|
|
204
|
-
if (response.frame() !== page.mainFrame())
|
|
205
|
-
return;
|
|
206
209
|
const status = response.status();
|
|
207
210
|
if (typeof status !== 'number')
|
|
208
211
|
return;
|
|
209
212
|
if (status <= 0)
|
|
210
213
|
return;
|
|
211
|
-
|
|
214
|
+
if (request.resourceType() === 'document') {
|
|
215
|
+
if (response.frame() !== page.mainFrame())
|
|
216
|
+
return;
|
|
217
|
+
this.mainDocumentStatus = status;
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
this.recordNetworkCall(request, status);
|
|
212
221
|
};
|
|
213
222
|
page.on('response', handler);
|
|
214
223
|
return () => page.off('response', handler);
|
|
215
224
|
}
|
|
225
|
+
recordNetworkCall(request, status) {
|
|
226
|
+
const resourceType = request.resourceType();
|
|
227
|
+
if (resourceType !== 'xhr' && resourceType !== 'fetch')
|
|
228
|
+
return;
|
|
229
|
+
const url = URL.parse(request.url());
|
|
230
|
+
if (!url)
|
|
231
|
+
return;
|
|
232
|
+
if (url.origin !== this.baseOrigin)
|
|
233
|
+
return;
|
|
234
|
+
const call = { method: request.method(), path: url.pathname, status };
|
|
235
|
+
if (this.networkRequests.some((r) => r.method === call.method && r.path === call.path && r.status === call.status))
|
|
236
|
+
return;
|
|
237
|
+
if (this.networkRequests.length >= MAX_NETWORK_CALLS) {
|
|
238
|
+
if (status < 400)
|
|
239
|
+
return;
|
|
240
|
+
const succeeded = this.networkRequests.findIndex((r) => r.status < 400);
|
|
241
|
+
if (succeeded === -1)
|
|
242
|
+
return;
|
|
243
|
+
this.networkRequests.splice(succeeded, 1);
|
|
244
|
+
}
|
|
245
|
+
this.networkRequests.push(call);
|
|
246
|
+
}
|
|
216
247
|
/**
|
|
217
248
|
* Capture HTML snapshots of all iframes on the page
|
|
218
249
|
*/
|
|
@@ -249,12 +280,7 @@ class Action {
|
|
|
249
280
|
async captureBrowserLogs() {
|
|
250
281
|
try {
|
|
251
282
|
const logs = await this.actor.grabBrowserLogs();
|
|
252
|
-
|
|
253
|
-
const importantLogs = logs.filter((log) => {
|
|
254
|
-
const level = log.type || log.level;
|
|
255
|
-
return ['info', 'error', 'warning', 'warn'].includes(level);
|
|
256
|
-
});
|
|
257
|
-
return importantLogs;
|
|
283
|
+
return logs.map(toBrowserLog).filter((log) => IMPORTANT_LOG_LEVELS.has(log.type));
|
|
258
284
|
}
|
|
259
285
|
catch (error) {
|
|
260
286
|
debugLog('Failed to capture browser logs:', error);
|
|
@@ -270,7 +296,7 @@ class Action {
|
|
|
270
296
|
const stepListener = attachStepLogger(executedSteps, assertionSteps);
|
|
271
297
|
const groupId = this.recorder ? await this.recorder.beginAction(codeString) : null;
|
|
272
298
|
this.playwrightGroupId = groupId;
|
|
273
|
-
const
|
|
299
|
+
const detachResponses = this.captureResponses();
|
|
274
300
|
const activeSpan = Observability.getSpan();
|
|
275
301
|
const tracer = trace.getTracer('ai');
|
|
276
302
|
const stepSpan = activeSpan ? tracer.startSpan('codeceptjs.step', undefined, trace.setSpan(context.active(), activeSpan)) : null;
|
|
@@ -314,7 +340,7 @@ class Action {
|
|
|
314
340
|
}
|
|
315
341
|
finally {
|
|
316
342
|
this.restorePageTimeout();
|
|
317
|
-
|
|
343
|
+
detachResponses();
|
|
318
344
|
if (groupId)
|
|
319
345
|
await this.recorder.endAction();
|
|
320
346
|
detachStepLogger(stepListener);
|
|
@@ -393,6 +419,11 @@ async function captureHtml(page, frame, actor) {
|
|
|
393
419
|
return actor.grabSource();
|
|
394
420
|
throw new Error('Playwright page is unavailable for HTML capture');
|
|
395
421
|
}
|
|
422
|
+
function toBrowserLog(log) {
|
|
423
|
+
const type = typeof log.type === 'function' ? log.type() : log.type || log.level || 'log';
|
|
424
|
+
const text = typeof log.text === 'function' ? log.text() : log.text || log.message || String(log);
|
|
425
|
+
return { type, text: text.replace(/\s+/g, ' ').trim() };
|
|
426
|
+
}
|
|
396
427
|
async function captureTitle(page, actor) {
|
|
397
428
|
if (page?.title)
|
|
398
429
|
return page.title();
|
|
@@ -15,7 +15,7 @@ export function WithWebMode(Base) {
|
|
|
15
15
|
researcher: ctx.explorBot.agentResearcher(),
|
|
16
16
|
navigator: ctx.explorBot.agentNavigator(),
|
|
17
17
|
});
|
|
18
|
-
const { see, context, visualClick
|
|
18
|
+
const { see, context, visualClick } = agentTools;
|
|
19
19
|
const tools = {
|
|
20
20
|
navigate: tool({
|
|
21
21
|
description: 'Navigate to a URL or page description using AI-powered navigation.',
|
|
@@ -115,7 +115,6 @@ export function WithWebMode(Base) {
|
|
|
115
115
|
}),
|
|
116
116
|
...codeceptTools,
|
|
117
117
|
context,
|
|
118
|
-
learnExperience,
|
|
119
118
|
};
|
|
120
119
|
if (see)
|
|
121
120
|
tools.see = see;
|
package/dist/src/ai/captain.d.ts
CHANGED
|
@@ -59,6 +59,16 @@ export declare class Captain extends CaptainBase implements Agent {
|
|
|
59
59
|
planSummary(): string;
|
|
60
60
|
reinjectContextIfNeeded(conversation: Conversation, currentState: WebPageState): Promise<void>;
|
|
61
61
|
coreTools(task: Task, onDone: (summary: string) => void): {
|
|
62
|
+
learnExperience: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
63
|
+
fileTag: any;
|
|
64
|
+
sectionIndex: any;
|
|
65
|
+
}, {
|
|
66
|
+
title: string;
|
|
67
|
+
url: string;
|
|
68
|
+
content: string;
|
|
69
|
+
} | {
|
|
70
|
+
error: string;
|
|
71
|
+
}, import("@ai-sdk/provider-utils").Context>>;
|
|
62
72
|
done: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
63
73
|
summary: string;
|
|
64
74
|
details?: string;
|
|
@@ -91,6 +101,16 @@ export declare class Captain extends CaptainBase implements Agent {
|
|
|
91
101
|
}, import("@ai-sdk/provider-utils").Context>>;
|
|
92
102
|
};
|
|
93
103
|
tools(task: Task, onDone: (summary: string) => void): Promise<{
|
|
104
|
+
learnExperience: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
105
|
+
fileTag: any;
|
|
106
|
+
sectionIndex: any;
|
|
107
|
+
}, {
|
|
108
|
+
title: string;
|
|
109
|
+
url: string;
|
|
110
|
+
content: string;
|
|
111
|
+
} | {
|
|
112
|
+
error: string;
|
|
113
|
+
}, import("@ai-sdk/provider-utils").Context>>;
|
|
94
114
|
done: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
95
115
|
summary: string;
|
|
96
116
|
details?: string;
|
package/dist/src/ai/captain.js
CHANGED
|
@@ -15,7 +15,7 @@ import { WithWebMode } from "./captain/web-mode.js";
|
|
|
15
15
|
import { toolExecutionLabel } from './conversation.js';
|
|
16
16
|
import { Researcher } from "./researcher.js";
|
|
17
17
|
import { TaskAgent } from "./task-agent.js";
|
|
18
|
-
import { withdrawVisionTools } from "./tools.js";
|
|
18
|
+
import { createLearnExperienceTool, withdrawVisionTools } from "./tools.js";
|
|
19
19
|
const MAX_STEPS = 15;
|
|
20
20
|
const CaptainBase = WithTestMode(WithWebMode(WithIdleMode(TaskAgent)));
|
|
21
21
|
export class Captain extends CaptainBase {
|
|
@@ -216,6 +216,15 @@ export class Captain extends CaptainBase {
|
|
|
216
216
|
}
|
|
217
217
|
coreTools(task, onDone) {
|
|
218
218
|
return {
|
|
219
|
+
learnExperience: createLearnExperienceTool({
|
|
220
|
+
getExperienceTracker: () => this.getExperienceTracker(),
|
|
221
|
+
getState: () => {
|
|
222
|
+
const state = this.explorBot.stateManager().getCurrentState();
|
|
223
|
+
if (!state)
|
|
224
|
+
return null;
|
|
225
|
+
return ActionResult.fromState(state);
|
|
226
|
+
},
|
|
227
|
+
}),
|
|
219
228
|
done: tool({
|
|
220
229
|
description: 'Call when the user request is fulfilled.',
|
|
221
230
|
inputSchema: z.object({
|
|
@@ -7,6 +7,7 @@ export interface ToolExecution {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function toToolExecution(toolName: string, input: any, rawOutput: any): ToolExecution;
|
|
9
9
|
export declare function toolExecutionLabel(input: Record<string, any> | undefined): string;
|
|
10
|
+
export declare const NARRATION_TOOL = "commentary";
|
|
10
11
|
export declare class Conversation {
|
|
11
12
|
messages: ModelMessage[];
|
|
12
13
|
model: any;
|
|
@@ -7,6 +7,7 @@ export function toToolExecution(toolName, input, rawOutput) {
|
|
|
7
7
|
export function toolExecutionLabel(input) {
|
|
8
8
|
return input?.explanation || input?.assertion || input?.reason || input?.request || '';
|
|
9
9
|
}
|
|
10
|
+
export const NARRATION_TOOL = 'commentary';
|
|
10
11
|
const AUTO_COMPACT_ARIA_CHANGES_CUTOFF = 500;
|
|
11
12
|
const AUTO_COMPACT_TARGETED_HTML_CUTOFF = 500;
|
|
12
13
|
export class Conversation {
|
|
@@ -203,6 +204,8 @@ export class Conversation {
|
|
|
203
204
|
for (const part of message.content) {
|
|
204
205
|
if (part.type !== 'tool-result')
|
|
205
206
|
continue;
|
|
207
|
+
if (part.toolName === NARRATION_TOOL)
|
|
208
|
+
continue;
|
|
206
209
|
executions.push(toToolExecution(part.toolName, toolCalls.get(part.toolCallId) || {}, part.output));
|
|
207
210
|
}
|
|
208
211
|
}
|
package/dist/src/ai/driller.js
CHANGED
|
@@ -14,7 +14,7 @@ import { eidxInContainer } from "../utils/web-eidx.js";
|
|
|
14
14
|
import { WebElement } from "../utils/web-element.js";
|
|
15
15
|
import { drillLocatorRule } from "./rules.js";
|
|
16
16
|
import { TaskAgent, isInteractive } from "./task-agent.js";
|
|
17
|
-
import { createCodeceptJSTools } from "./tools.js";
|
|
17
|
+
import { createCodeceptJSTools, createLearnExperienceTool } from "./tools.js";
|
|
18
18
|
const debugLog = createDebug('explorbot:driller');
|
|
19
19
|
export class Driller extends TaskAgent {
|
|
20
20
|
ACTION_TOOLS = ['click', 'pressKey', 'form'];
|
|
@@ -234,7 +234,11 @@ export class Driller extends TaskAgent {
|
|
|
234
234
|
conversation.addUserText(await this.buildComponentPrompt(originalState, component));
|
|
235
235
|
let finished = false;
|
|
236
236
|
const actionTools = this.createVerifiedActionTools(createCodeceptJSTools(this.toolDeps, test), component);
|
|
237
|
-
const
|
|
237
|
+
const learnExperience = createLearnExperienceTool({
|
|
238
|
+
getExperienceTracker: () => this.getExperienceTracker(),
|
|
239
|
+
getState: () => ActionResult.fromState(this.stateManager.getCurrentState() || originalState),
|
|
240
|
+
});
|
|
241
|
+
const tools = { ...actionTools, learnExperience, ...this.createDrillFlowTools(originalState, test, interactive) };
|
|
238
242
|
await loop(async ({ stop, iteration }) => {
|
|
239
243
|
debugLog(`Drilling component ${component.name}, iteration ${iteration}`);
|
|
240
244
|
setActivity(`${this.emoji} Drilling ${component.name}...`, 'action');
|
|
@@ -9,18 +9,49 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
9
9
|
method: any;
|
|
10
10
|
path: any;
|
|
11
11
|
}, {
|
|
12
|
+
source: string;
|
|
13
|
+
method: any;
|
|
14
|
+
path: any;
|
|
15
|
+
definition: string;
|
|
16
|
+
rejectedCapture: {
|
|
17
|
+
status: number;
|
|
18
|
+
requestBody: any;
|
|
19
|
+
};
|
|
20
|
+
usable?: undefined;
|
|
21
|
+
rejectedRequestBody?: undefined;
|
|
22
|
+
status?: undefined;
|
|
23
|
+
requestBody?: undefined;
|
|
24
|
+
error?: undefined;
|
|
25
|
+
} | {
|
|
26
|
+
source: string;
|
|
27
|
+
method: any;
|
|
28
|
+
path: any;
|
|
29
|
+
usable: boolean;
|
|
30
|
+
rejectedRequestBody: any;
|
|
31
|
+
status: number;
|
|
32
|
+
definition?: undefined;
|
|
33
|
+
rejectedCapture?: undefined;
|
|
34
|
+
requestBody?: undefined;
|
|
35
|
+
error?: undefined;
|
|
36
|
+
} | {
|
|
12
37
|
source: string;
|
|
13
38
|
method: string;
|
|
14
39
|
path: string;
|
|
15
40
|
status: number;
|
|
16
41
|
requestBody: any;
|
|
17
42
|
definition?: undefined;
|
|
43
|
+
rejectedCapture?: undefined;
|
|
44
|
+
usable?: undefined;
|
|
45
|
+
rejectedRequestBody?: undefined;
|
|
18
46
|
error?: undefined;
|
|
19
47
|
} | {
|
|
20
48
|
source: string;
|
|
21
49
|
definition: string;
|
|
22
50
|
method?: undefined;
|
|
23
51
|
path?: undefined;
|
|
52
|
+
rejectedCapture?: undefined;
|
|
53
|
+
usable?: undefined;
|
|
54
|
+
rejectedRequestBody?: undefined;
|
|
24
55
|
status?: undefined;
|
|
25
56
|
requestBody?: undefined;
|
|
26
57
|
error?: undefined;
|
|
@@ -29,20 +60,25 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
29
60
|
error: any;
|
|
30
61
|
method?: undefined;
|
|
31
62
|
path?: undefined;
|
|
63
|
+
definition?: undefined;
|
|
64
|
+
rejectedCapture?: undefined;
|
|
65
|
+
usable?: undefined;
|
|
66
|
+
rejectedRequestBody?: undefined;
|
|
32
67
|
status?: undefined;
|
|
33
68
|
requestBody?: undefined;
|
|
34
|
-
definition?: undefined;
|
|
35
69
|
}, import("@ai-sdk/provider-utils").Context>>;
|
|
36
70
|
request: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<any, {
|
|
37
71
|
success: boolean;
|
|
38
72
|
error: string;
|
|
39
73
|
status?: undefined;
|
|
40
74
|
statusText?: undefined;
|
|
75
|
+
category?: undefined;
|
|
41
76
|
errorPreview?: undefined;
|
|
42
77
|
} | {
|
|
43
78
|
success: boolean;
|
|
44
79
|
status: number;
|
|
45
80
|
statusText: string;
|
|
81
|
+
category: ResponseCategory;
|
|
46
82
|
errorPreview: string;
|
|
47
83
|
error?: undefined;
|
|
48
84
|
} | {
|
|
@@ -50,6 +86,7 @@ export declare function createFishermanTools(apiClient: ApiClient, requestStore:
|
|
|
50
86
|
status: number;
|
|
51
87
|
error?: undefined;
|
|
52
88
|
statusText?: undefined;
|
|
89
|
+
category?: undefined;
|
|
53
90
|
errorPreview?: undefined;
|
|
54
91
|
}, import("@ai-sdk/provider-utils").Context>>;
|
|
55
92
|
finish: import("@ai-sdk/provider-utils").ExecutableTool<import("ai").Tool<{
|
|
@@ -88,3 +125,5 @@ export interface FishermanResult {
|
|
|
88
125
|
reason: string;
|
|
89
126
|
}>;
|
|
90
127
|
}
|
|
128
|
+
type ResponseCategory = 'validation' | 'authorization' | 'not_found' | 'conflict' | 'temporary' | 'server' | 'client';
|
|
129
|
+
export {};
|
|
@@ -23,6 +23,29 @@ export function createFishermanTools(apiClient, requestStore, opts) {
|
|
|
23
23
|
tag('step').log(`Fisherman: spec lookup ${method} ${path}`);
|
|
24
24
|
const captured = requestStore.findCapturedRequest(method, path);
|
|
25
25
|
if (captured) {
|
|
26
|
+
if (captured.status >= 400) {
|
|
27
|
+
const rejectedCapture = {
|
|
28
|
+
status: captured.status,
|
|
29
|
+
requestBody: captured.requestBody || 'no body',
|
|
30
|
+
};
|
|
31
|
+
if (opts.spec) {
|
|
32
|
+
try {
|
|
33
|
+
const definition = extractEndpointDefinition(opts.spec, path, opts.baseEndpoint);
|
|
34
|
+
return { source: 'spec', method, path, definition, rejectedCapture };
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { source: 'captured', method, path, usable: false, rejectedRequestBody: captured.requestBody || 'no body', status: captured.status };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
source: 'captured',
|
|
42
|
+
method: captured.method,
|
|
43
|
+
path: captured.path,
|
|
44
|
+
status: captured.status,
|
|
45
|
+
usable: false,
|
|
46
|
+
rejectedRequestBody: captured.requestBody || 'no body',
|
|
47
|
+
};
|
|
48
|
+
}
|
|
26
49
|
return {
|
|
27
50
|
source: 'captured',
|
|
28
51
|
method: captured.method,
|
|
@@ -74,6 +97,7 @@ export function createFishermanTools(apiClient, requestStore, opts) {
|
|
|
74
97
|
success: false,
|
|
75
98
|
status: reqResult.status,
|
|
76
99
|
statusText: reqResult.statusText,
|
|
100
|
+
category: responseCategory(reqResult.status),
|
|
77
101
|
errorPreview: reqResult.rawResponseBody.substring(0, 300),
|
|
78
102
|
};
|
|
79
103
|
}
|
|
@@ -127,6 +151,21 @@ export function createFishermanTools(apiClient, requestStore, opts) {
|
|
|
127
151
|
};
|
|
128
152
|
return { tools, getResult, isFinished };
|
|
129
153
|
}
|
|
154
|
+
function responseCategory(status) {
|
|
155
|
+
if (status === 400 || status === 422)
|
|
156
|
+
return 'validation';
|
|
157
|
+
if (status === 401 || status === 403)
|
|
158
|
+
return 'authorization';
|
|
159
|
+
if (status === 404)
|
|
160
|
+
return 'not_found';
|
|
161
|
+
if (status === 409)
|
|
162
|
+
return 'conflict';
|
|
163
|
+
if (status === 408 || status === 425 || status === 429)
|
|
164
|
+
return 'temporary';
|
|
165
|
+
if (status >= 500)
|
|
166
|
+
return 'server';
|
|
167
|
+
return 'client';
|
|
168
|
+
}
|
|
130
169
|
function extractKeyFields(body, result = {}, depth = 0) {
|
|
131
170
|
if (!body || typeof body !== 'object' || depth > 5)
|
|
132
171
|
return result;
|
package/dist/src/ai/fisherman.js
CHANGED
|
@@ -158,7 +158,7 @@ export class Fisherman {
|
|
|
158
158
|
|
|
159
159
|
AVAILABLE TOOLS:
|
|
160
160
|
${toolNames.join(', ')}.
|
|
161
|
-
Use tool names exactly as listed. Do not invent aliases
|
|
161
|
+
Use tool names exactly as listed. Do not invent aliases or combined names.
|
|
162
162
|
Match each tool input schema exactly. Do not invent parameter names or pass extra fields.
|
|
163
163
|
|
|
164
164
|
WORKFLOW:
|
|
@@ -170,7 +170,8 @@ export class Fisherman {
|
|
|
170
170
|
RULES:
|
|
171
171
|
- Always call getEndpointSpec before your first request to an unfamiliar endpoint
|
|
172
172
|
- Chain requests logically — create parent resources before children
|
|
173
|
-
-
|
|
173
|
+
- Use the response category and error text to decide what failed: validation requires corrected data, authorization requires valid access, not_found requires a valid path or parent, and conflict requires resolving the conflicting state
|
|
174
|
+
- Retry temporary or server failures once. Retry other failures only when the specification or error text gives a concrete correction
|
|
174
175
|
- Use realistic but unique data for each item (vary names, titles)
|
|
175
176
|
|
|
176
177
|
${dataProtectionRules}
|
|
@@ -39,12 +39,13 @@ declare class Navigator implements Agent {
|
|
|
39
39
|
resolveState(message: string, actionResult: ActionResult, opts?: {
|
|
40
40
|
action?: Action;
|
|
41
41
|
expectedUrl?: string;
|
|
42
|
+
experience?: string;
|
|
42
43
|
onAttempt?: (attempt: {
|
|
43
44
|
code: string;
|
|
44
45
|
error?: string;
|
|
45
46
|
}) => void;
|
|
46
47
|
}): Promise<boolean>;
|
|
47
|
-
buildResolutionPrompt(message: string, actionResult: ActionResult): Promise<string>;
|
|
48
|
+
buildResolutionPrompt(message: string, actionResult: ActionResult, injectedExperience?: string): Promise<string>;
|
|
48
49
|
buildRetryFeedback(failures: BatchFailure[], includeHtml: boolean, actionResult: ActionResult): Promise<string>;
|
|
49
50
|
executeAttempt(action: Action, codeBlock: string, message: string): Promise<{
|
|
50
51
|
ok: boolean;
|
package/dist/src/ai/navigator.js
CHANGED
|
@@ -200,7 +200,7 @@ class Navigator {
|
|
|
200
200
|
const expectedUrl = opts?.expectedUrl;
|
|
201
201
|
const knowledge = this.knowledgeTracker.renderRelevantContext(actionResult);
|
|
202
202
|
const conversation = this.provider.startConversation(this.systemPrompt, 'navigator');
|
|
203
|
-
conversation.addUserText(await this.buildResolutionPrompt(message, actionResult));
|
|
203
|
+
conversation.addUserText(await this.buildResolutionPrompt(message, actionResult, opts?.experience));
|
|
204
204
|
let stopReason = null;
|
|
205
205
|
const tools = {
|
|
206
206
|
stop: tool({
|
|
@@ -333,14 +333,10 @@ class Navigator {
|
|
|
333
333
|
}
|
|
334
334
|
return resolved;
|
|
335
335
|
}
|
|
336
|
-
async buildResolutionPrompt(message, actionResult) {
|
|
337
|
-
let experience = '';
|
|
338
|
-
if (!actionResult.isInsideIframe) {
|
|
339
|
-
|
|
340
|
-
if (successful.length > 0) {
|
|
341
|
-
tag('operation').log(`Found ${successful.length} experience ${pluralize(successful.length, 'file')} for: ${actionResult.url}`);
|
|
342
|
-
experience = `<experience>\nPast successful recipes recorded from prior runs for this page. Prefer these solutions first if they match the goal.\n\n${successful.join('\n\n')}\n</experience>`;
|
|
343
|
-
}
|
|
336
|
+
async buildResolutionPrompt(message, actionResult, injectedExperience) {
|
|
337
|
+
let experience = injectedExperience || '';
|
|
338
|
+
if (!experience && !actionResult.isInsideIframe) {
|
|
339
|
+
experience = this.experienceTracker.renderExperienceFor(actionResult);
|
|
344
340
|
}
|
|
345
341
|
return dedent `
|
|
346
342
|
<message>
|