explorbot 0.1.32-beta.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -14
- package/bin/explorbot-cli.ts +4 -3
- package/boat/doc-collector/src/ai/documentarian.ts +22 -27
- package/boat/doc-collector/src/ai/tools.ts +67 -10
- package/boat/doc-collector/src/docbot.ts +28 -7
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +33 -1
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +3 -3
- package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
- package/dist/boat/doc-collector/src/ai/tools.js +47 -10
- package/dist/boat/doc-collector/src/docbot.js +25 -6
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +29 -0
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/package.json +20 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +3 -10
- package/dist/src/action.js +18 -111
- package/dist/src/ai/captain/web-mode.js +0 -7
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +4 -19
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +0 -4
- package/dist/src/ai/driller.js +3 -12
- package/dist/src/ai/experience-compactor.d.ts +2 -2
- package/dist/src/ai/experience-compactor.js +16 -33
- package/dist/src/ai/fisherman-tools.js +1 -1
- package/dist/src/ai/fisherman.d.ts +0 -1
- package/dist/src/ai/fisherman.js +0 -3
- package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
- package/dist/src/ai/historian/codeceptjs.js +3 -2
- package/dist/src/ai/historian/playwright.d.ts +2 -0
- package/dist/src/ai/historian/playwright.js +3 -2
- package/dist/src/ai/historian/utils.d.ts +0 -1
- package/dist/src/ai/historian/utils.js +0 -1
- package/dist/src/ai/historian.d.ts +5 -4
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +3 -5
- package/dist/src/ai/navigator.js +11 -46
- package/dist/src/ai/pilot.d.ts +1 -3
- package/dist/src/ai/pilot.js +6 -17
- package/dist/src/ai/planner.d.ts +3 -3
- package/dist/src/ai/planner.js +49 -37
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -136
- package/dist/src/ai/quartermaster.d.ts +21 -22
- package/dist/src/ai/quartermaster.js +4 -10
- package/dist/src/ai/rerunner.d.ts +1 -2
- package/dist/src/ai/rerunner.js +14 -22
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/deep-analysis.js +3 -6
- package/dist/src/ai/researcher/parser.js +1 -1
- package/dist/src/ai/researcher/research-result.d.ts +0 -1
- package/dist/src/ai/researcher/research-result.js +0 -19
- package/dist/src/ai/researcher/sections.js +5 -1
- package/dist/src/ai/researcher.js +12 -53
- package/dist/src/ai/rules.d.ts +1 -0
- package/dist/src/ai/rules.js +22 -15
- package/dist/src/ai/session-analyst.js +8 -5
- package/dist/src/ai/task-agent.d.ts +1 -1
- package/dist/src/ai/task-agent.js +2 -27
- package/dist/src/ai/tester.d.ts +0 -2
- package/dist/src/ai/tester.js +10 -61
- package/dist/src/ai/tools.d.ts +18 -7
- package/dist/src/ai/tools.js +119 -101
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-command.js +1 -1
- package/dist/src/commands/context-knowledge-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +3 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +2 -2
- package/dist/src/config.d.ts +2 -0
- package/dist/src/config.js +12 -0
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +81 -154
- package/dist/src/explorbot.d.ts +4 -5
- package/dist/src/explorbot.js +17 -51
- package/dist/src/explorer.d.ts +4 -15
- package/dist/src/explorer.js +18 -68
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +6 -31
- package/dist/src/state-manager.js +10 -129
- package/dist/src/utils/aria.d.ts +6 -1
- package/dist/src/utils/aria.js +4 -1
- package/dist/src/utils/cache.d.ts +15 -0
- package/dist/src/utils/cache.js +34 -0
- package/dist/src/utils/context-formatter.d.ts +6 -0
- package/dist/src/utils/context-formatter.js +15 -27
- package/dist/src/utils/html.d.ts +5 -0
- package/dist/src/utils/html.js +71 -0
- package/dist/src/utils/markdown-files.d.ts +10 -0
- package/dist/src/utils/markdown-files.js +21 -0
- package/dist/src/utils/markdown-query.d.ts +6 -0
- package/dist/src/utils/markdown-query.js +14 -1
- package/dist/src/utils/page-readiness.d.ts +1 -0
- package/dist/src/utils/page-readiness.js +1 -1
- package/dist/src/utils/secrets.js +2 -2
- package/dist/src/utils/strings.d.ts +2 -0
- package/dist/src/utils/strings.js +10 -0
- package/dist/src/utils/web-element.d.ts +2 -1
- package/dist/src/utils/web-element.js +3 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/package.json +20 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +16 -130
- package/src/ai/captain/web-mode.ts +0 -7
- package/src/ai/captain.ts +5 -17
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +3 -16
- package/src/ai/experience-compactor.ts +16 -35
- package/src/ai/fisherman-tools.ts +1 -1
- package/src/ai/fisherman.ts +0 -4
- package/src/ai/historian/codeceptjs.ts +4 -2
- package/src/ai/historian/playwright.ts +4 -2
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +4 -7
- package/src/ai/navigator.ts +13 -49
- package/src/ai/pilot.ts +6 -16
- package/src/ai/planner.ts +56 -44
- package/src/ai/provider.ts +85 -145
- package/src/ai/quartermaster.ts +34 -41
- package/src/ai/rerunner.ts +17 -21
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/deep-analysis.ts +3 -6
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +5 -1
- package/src/ai/researcher.ts +11 -59
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +3 -30
- package/src/ai/tester.ts +12 -73
- package/src/ai/tools.ts +119 -106
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-command.ts +1 -1
- package/src/commands/context-knowledge-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +3 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +2 -2
- package/src/config.ts +12 -0
- package/src/experience-tracker.ts +88 -151
- package/src/explorbot.ts +17 -51
- package/src/explorer.ts +20 -72
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +15 -156
- package/src/utils/aria.ts +11 -2
- package/src/utils/cache.ts +40 -0
- package/src/utils/context-formatter.ts +12 -15
- package/src/utils/html.ts +79 -0
- package/src/utils/markdown-files.ts +30 -0
- package/src/utils/markdown-query.ts +15 -1
- package/src/utils/page-readiness.ts +1 -1
- package/src/utils/secrets.ts +2 -3
- package/src/utils/strings.ts +12 -0
- package/src/utils/web-element.ts +4 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
package/src/ai/provider.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { Stats } from '../stats.ts';
|
|
|
11
11
|
import { createDebug, tag } from '../utils/logger.js';
|
|
12
12
|
import { type RetryOptions, withRetry } from '../utils/retry.js';
|
|
13
13
|
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
14
|
-
import { Conversation } from './conversation.js';
|
|
14
|
+
import { Conversation, toToolExecution } from './conversation.js';
|
|
15
15
|
|
|
16
16
|
const debugLog = createDebug('explorbot:provider');
|
|
17
17
|
const promptLog = createDebug('explorbot:provider:out');
|
|
@@ -22,6 +22,8 @@ export class ContextLengthError extends Error {}
|
|
|
22
22
|
|
|
23
23
|
let telemetryRegistered = false;
|
|
24
24
|
|
|
25
|
+
const CONTEXT_LENGTH_PATTERNS = ['reduce the length', 'context length', 'maximum context', 'token limit', 'too many tokens', 'max_tokens', 'context_length_exceeded', 'output truncated at maxtokens'];
|
|
26
|
+
|
|
25
27
|
function extractCachedTokens(usage: any): number {
|
|
26
28
|
if (!usage) return 0;
|
|
27
29
|
const direct = usage.inputTokenDetails?.cacheReadTokens ?? usage.cachedInputTokens;
|
|
@@ -31,10 +33,10 @@ function extractCachedTokens(usage: any): number {
|
|
|
31
33
|
return typeof fromRaw === 'number' ? fromRaw : 0;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
|
-
function
|
|
36
|
+
function abortAfterIdle(ms: number, cancel: { cancelled: boolean }, controller: AbortController): Promise<never> {
|
|
35
37
|
return new Promise((_, reject) => {
|
|
36
38
|
const tick = () => {
|
|
37
|
-
if (
|
|
39
|
+
if (cancel.cancelled) return;
|
|
38
40
|
if (executionController.isAwaitingInput()) {
|
|
39
41
|
setTimeout(tick, ms);
|
|
40
42
|
return;
|
|
@@ -46,6 +48,12 @@ function rejectAfterIdle(ms: number, signal: { cancelled: boolean }, controller:
|
|
|
46
48
|
});
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
function combinedAbortSignal(controller: AbortController): AbortSignal {
|
|
52
|
+
const executionSignal = executionController.getAbortSignal();
|
|
53
|
+
if (!executionSignal) return controller.signal;
|
|
54
|
+
return AbortSignal.any([controller.signal, executionSignal]);
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
export class Provider {
|
|
50
58
|
private config: AIConfig;
|
|
51
59
|
private telemetryEnabled = false;
|
|
@@ -70,7 +78,6 @@ export class Provider {
|
|
|
70
78
|
},
|
|
71
79
|
};
|
|
72
80
|
|
|
73
|
-
static readonly CONTEXT_LENGTH_PATTERNS = ['reduce the length', 'context length', 'maximum context', 'token limit', 'too many tokens', 'max_tokens', 'context_length_exceeded', 'output truncated at maxtokens'];
|
|
74
81
|
lastConversation: Conversation | null = null;
|
|
75
82
|
|
|
76
83
|
constructor(config: AIConfig) {
|
|
@@ -138,12 +145,12 @@ export class Provider {
|
|
|
138
145
|
return parts.length > 0 ? parts.join('\n\n') : undefined;
|
|
139
146
|
}
|
|
140
147
|
|
|
141
|
-
getProviderOptionsForAgent(agentName: string): Record<string, any> | undefined {
|
|
148
|
+
private getProviderOptionsForAgent(agentName: string): Record<string, any> | undefined {
|
|
142
149
|
const agentConfig = this.config.agents?.[agentName as keyof typeof this.config.agents];
|
|
143
150
|
return agentConfig?.providerOptions;
|
|
144
151
|
}
|
|
145
152
|
|
|
146
|
-
getReasoningForAgent(agentName?: string): string | undefined {
|
|
153
|
+
private getReasoningForAgent(agentName?: string): string | undefined {
|
|
147
154
|
if (!agentName) return undefined;
|
|
148
155
|
const agentConfig = this.config.agents?.[agentName as keyof typeof this.config.agents];
|
|
149
156
|
return agentConfig?.reasoning;
|
|
@@ -172,6 +179,50 @@ export class Provider {
|
|
|
172
179
|
if (reasoning) config.reasoning ??= reasoning;
|
|
173
180
|
}
|
|
174
181
|
|
|
182
|
+
private buildGenerateConfig(defaults: Record<string, any>, overrides: Record<string, any>, options: any): Record<string, any> {
|
|
183
|
+
const telemetry = this.getTelemetry(options);
|
|
184
|
+
const config = this.mergeProviderOptions(
|
|
185
|
+
{
|
|
186
|
+
...defaults,
|
|
187
|
+
allowSystemInMessages: true,
|
|
188
|
+
...(this.config.config || {}),
|
|
189
|
+
...options,
|
|
190
|
+
...overrides,
|
|
191
|
+
},
|
|
192
|
+
options.agentName
|
|
193
|
+
);
|
|
194
|
+
this.finalizeConfig(config, options, telemetry);
|
|
195
|
+
return config;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private recordUsage(agentName: string, modelName: string, usage: any): void {
|
|
199
|
+
if (!usage) return;
|
|
200
|
+
Stats.recordTokens(agentName, modelName, {
|
|
201
|
+
input: usage.inputTokens ?? usage.promptTokens ?? 0,
|
|
202
|
+
output: usage.outputTokens ?? usage.completionTokens ?? 0,
|
|
203
|
+
total: usage.totalTokens ?? 0,
|
|
204
|
+
cached: extractCachedTokens(usage),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number): Promise<T> {
|
|
209
|
+
const cancel = { cancelled: false };
|
|
210
|
+
const controller = new AbortController();
|
|
211
|
+
const combinedSignal = combinedAbortSignal(controller);
|
|
212
|
+
try {
|
|
213
|
+
return await Promise.race([fn(combinedSignal), abortAfterIdle(timeoutMs, cancel, controller)]);
|
|
214
|
+
} finally {
|
|
215
|
+
cancel.cancelled = true;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private async recoverFromContextLength(error: any, messages: ModelMessage[], options: any, retry: (messages: ModelMessage[], options: any) => Promise<any>): Promise<any> {
|
|
220
|
+
const reduced = this.tryReduceMessages(messages, options._contextRetryLevel || 0);
|
|
221
|
+
if (!reduced) throw new ContextLengthError(error.message || error.toString());
|
|
222
|
+
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
223
|
+
return retry(reduced.messages, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
224
|
+
}
|
|
225
|
+
|
|
175
226
|
private initLangfuse() {
|
|
176
227
|
const langfuseConfig = this.config.langfuse;
|
|
177
228
|
const publicKey = langfuseConfig?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
@@ -207,20 +258,25 @@ export class Provider {
|
|
|
207
258
|
|
|
208
259
|
const runTelemetry = Observability.getTelemetry();
|
|
209
260
|
|
|
210
|
-
|
|
261
|
+
let optionTelemetry = options.telemetry;
|
|
262
|
+
if (options.telemetryFunctionId && !optionTelemetry?.functionId) {
|
|
263
|
+
optionTelemetry = { ...optionTelemetry, functionId: options.telemetryFunctionId };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (!optionTelemetry) {
|
|
211
267
|
return runTelemetry;
|
|
212
268
|
}
|
|
213
269
|
|
|
214
270
|
if (!runTelemetry) {
|
|
215
|
-
return
|
|
271
|
+
return optionTelemetry;
|
|
216
272
|
}
|
|
217
273
|
|
|
218
274
|
return {
|
|
219
275
|
...runTelemetry,
|
|
220
|
-
...
|
|
276
|
+
...optionTelemetry,
|
|
221
277
|
metadata: {
|
|
222
278
|
...runTelemetry.metadata,
|
|
223
|
-
...
|
|
279
|
+
...optionTelemetry.metadata,
|
|
224
280
|
},
|
|
225
281
|
};
|
|
226
282
|
}
|
|
@@ -254,12 +310,8 @@ export class Provider {
|
|
|
254
310
|
const toolCalls = response.toolCalls || [];
|
|
255
311
|
const toolResults = response.toolResults || [];
|
|
256
312
|
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
input: call.input,
|
|
260
|
-
output: toolResults[index]?.output,
|
|
261
|
-
wasSuccessful: toolResults[index]?.output?.success || false,
|
|
262
|
-
}));
|
|
313
|
+
const resultsById = new Map(toolResults.map((r: any) => [r.toolCallId, r]));
|
|
314
|
+
const toolExecutions = toolCalls.map((call: any) => toToolExecution(call.toolName || '', call.input, resultsById.get(call.toolCallId)?.output));
|
|
263
315
|
|
|
264
316
|
return { conversation, response, toolExecutions };
|
|
265
317
|
}
|
|
@@ -269,19 +321,7 @@ export class Provider {
|
|
|
269
321
|
setActivity(`🤖 Asking ${modelName}`, 'ai');
|
|
270
322
|
promptLog(`Using model: ${modelName}`);
|
|
271
323
|
|
|
272
|
-
const
|
|
273
|
-
const config = this.mergeProviderOptions(
|
|
274
|
-
{
|
|
275
|
-
maxOutputTokens: 16384,
|
|
276
|
-
allowSystemInMessages: true,
|
|
277
|
-
...(this.config.config || {}),
|
|
278
|
-
...options,
|
|
279
|
-
model,
|
|
280
|
-
abortSignal: executionController.getAbortSignal(),
|
|
281
|
-
},
|
|
282
|
-
options.agentName
|
|
283
|
-
);
|
|
284
|
-
this.finalizeConfig(config, options, telemetry);
|
|
324
|
+
const config = this.buildGenerateConfig({ maxOutputTokens: 16384 }, { model, abortSignal: executionController.getAbortSignal() }, options);
|
|
285
325
|
|
|
286
326
|
promptLog(messages[messages.length - 1].content);
|
|
287
327
|
try {
|
|
@@ -303,14 +343,7 @@ export class Provider {
|
|
|
303
343
|
clearActivity();
|
|
304
344
|
responseLog(response.text);
|
|
305
345
|
|
|
306
|
-
|
|
307
|
-
Stats.recordTokens(options.agentName || 'unknown', modelName, {
|
|
308
|
-
input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
|
|
309
|
-
output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
|
|
310
|
-
total: response.usage.totalTokens ?? 0,
|
|
311
|
-
cached: extractCachedTokens(response.usage),
|
|
312
|
-
});
|
|
313
|
-
}
|
|
346
|
+
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
314
347
|
|
|
315
348
|
return response;
|
|
316
349
|
} catch (error: any) {
|
|
@@ -318,12 +351,7 @@ export class Provider {
|
|
|
318
351
|
if (error?.name === 'AbortError') throw error;
|
|
319
352
|
if (error instanceof ContextLengthError) throw error;
|
|
320
353
|
if (Provider.isContextLengthError(error)) {
|
|
321
|
-
|
|
322
|
-
if (reduced) {
|
|
323
|
-
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
324
|
-
return this.chat(reduced.messages, model, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
325
|
-
}
|
|
326
|
-
throw new ContextLengthError(error.message || error.toString());
|
|
354
|
+
return this.recoverFromContextLength(error, messages, options, (m, o) => this.chat(m, model, o));
|
|
327
355
|
}
|
|
328
356
|
const message = error.message || error.toString();
|
|
329
357
|
if (message !== 'No response text from AI') {
|
|
@@ -343,50 +371,19 @@ export class Provider {
|
|
|
343
371
|
promptLog('Available tools:', toolNames);
|
|
344
372
|
promptLog(messages[messages.length - 1].content);
|
|
345
373
|
|
|
346
|
-
const telemetry = this.getTelemetry(options);
|
|
347
374
|
const maxRoundtrips = options.maxToolRoundtrips ?? 5;
|
|
348
375
|
const extraStop = options.stopWhen;
|
|
349
376
|
const stopConditions: any[] = [isStepCount(maxRoundtrips)];
|
|
350
377
|
if (extraStop) stopConditions.push(extraStop);
|
|
351
|
-
const { stopWhen:
|
|
352
|
-
const config = this.mergeProviderOptions(
|
|
353
|
-
{
|
|
354
|
-
tools,
|
|
355
|
-
maxOutputTokens: 16384,
|
|
356
|
-
toolChoice: 'auto',
|
|
357
|
-
allowSystemInMessages: true,
|
|
358
|
-
...(this.config.config || {}),
|
|
359
|
-
...optionsWithoutStop,
|
|
360
|
-
stopWhen: stopConditions,
|
|
361
|
-
model,
|
|
362
|
-
abortSignal: executionController.getAbortSignal(),
|
|
363
|
-
},
|
|
364
|
-
options.agentName
|
|
365
|
-
);
|
|
366
|
-
this.finalizeConfig(config, options, telemetry);
|
|
378
|
+
const config = this.buildGenerateConfig({ tools, maxOutputTokens: 16384, toolChoice: 'auto' }, { stopWhen: stopConditions, model }, options);
|
|
367
379
|
try {
|
|
368
380
|
const response = await withRetry(async () => {
|
|
369
|
-
const
|
|
370
|
-
const
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
try {
|
|
374
|
-
const result = (await Promise.race([
|
|
375
|
-
generateText({
|
|
376
|
-
messages,
|
|
377
|
-
...config,
|
|
378
|
-
abortSignal: combinedSignal,
|
|
379
|
-
}),
|
|
380
|
-
rejectAfterIdle(timeout, cancel, controller),
|
|
381
|
-
])) as any;
|
|
382
|
-
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
383
|
-
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
384
|
-
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
385
|
-
}
|
|
386
|
-
return result;
|
|
387
|
-
} finally {
|
|
388
|
-
cancel.cancelled = true;
|
|
381
|
+
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
|
|
382
|
+
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
383
|
+
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
384
|
+
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
389
385
|
}
|
|
386
|
+
return result;
|
|
390
387
|
}, this.getRetryOptions(options));
|
|
391
388
|
|
|
392
389
|
clearActivity();
|
|
@@ -401,14 +398,7 @@ export class Provider {
|
|
|
401
398
|
|
|
402
399
|
responseLog(response.text);
|
|
403
400
|
|
|
404
|
-
|
|
405
|
-
Stats.recordTokens(options.agentName || 'unknown', modelName, {
|
|
406
|
-
input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
|
|
407
|
-
output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
|
|
408
|
-
total: response.usage.totalTokens ?? 0,
|
|
409
|
-
cached: extractCachedTokens(response.usage),
|
|
410
|
-
});
|
|
411
|
-
}
|
|
401
|
+
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
412
402
|
|
|
413
403
|
return response;
|
|
414
404
|
} catch (error: any) {
|
|
@@ -419,12 +409,7 @@ export class Provider {
|
|
|
419
409
|
if (error?.name === 'AbortError') throw error;
|
|
420
410
|
if (error instanceof ContextLengthError) throw error;
|
|
421
411
|
if (Provider.isContextLengthError(error)) {
|
|
422
|
-
|
|
423
|
-
if (reduced) {
|
|
424
|
-
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
425
|
-
return this.generateWithTools(reduced.messages, model, tools, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
426
|
-
}
|
|
427
|
-
throw new ContextLengthError(error.message || error.toString());
|
|
412
|
+
return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateWithTools(m, model, tools, o));
|
|
428
413
|
}
|
|
429
414
|
if (error.constructor?.name === 'AI_APICallError') {
|
|
430
415
|
responseLog(error.message);
|
|
@@ -440,52 +425,18 @@ export class Provider {
|
|
|
440
425
|
setActivity(`🤖 Asking ${modelName} for structured output`, 'ai');
|
|
441
426
|
promptLog(`Using model: ${modelName}`);
|
|
442
427
|
|
|
443
|
-
const
|
|
444
|
-
const config = this.mergeProviderOptions(
|
|
445
|
-
{
|
|
446
|
-
schema,
|
|
447
|
-
allowSystemInMessages: true,
|
|
448
|
-
...(this.config.config || {}),
|
|
449
|
-
...options,
|
|
450
|
-
model: modelToUse,
|
|
451
|
-
abortSignal: executionController.getAbortSignal(),
|
|
452
|
-
},
|
|
453
|
-
options.agentName
|
|
454
|
-
);
|
|
455
|
-
this.finalizeConfig(config, options, telemetry);
|
|
428
|
+
const config = this.buildGenerateConfig({ schema }, { model: modelToUse }, options);
|
|
456
429
|
|
|
457
430
|
try {
|
|
458
431
|
promptLog(messages[messages.length - 1].content);
|
|
459
432
|
const response = await withRetry(async () => {
|
|
460
|
-
|
|
461
|
-
const cancel = { cancelled: false };
|
|
462
|
-
const controller = new AbortController();
|
|
463
|
-
const combinedSignal = AbortSignal.any([controller.signal, executionController.getAbortSignal()].filter(Boolean) as AbortSignal[]);
|
|
464
|
-
try {
|
|
465
|
-
return (await Promise.race([
|
|
466
|
-
generateObject({
|
|
467
|
-
messages,
|
|
468
|
-
...config,
|
|
469
|
-
abortSignal: combinedSignal,
|
|
470
|
-
}),
|
|
471
|
-
rejectAfterIdle(timeout, cancel, controller),
|
|
472
|
-
])) as any;
|
|
473
|
-
} finally {
|
|
474
|
-
cancel.cancelled = true;
|
|
475
|
-
}
|
|
433
|
+
return (await this.raceWithIdleTimeout((signal) => generateObject({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
|
|
476
434
|
}, this.getRetryOptions(options));
|
|
477
435
|
|
|
478
436
|
clearActivity();
|
|
479
437
|
responseLog(response.object);
|
|
480
438
|
|
|
481
|
-
|
|
482
|
-
Stats.recordTokens(options.agentName || 'unknown', modelName, {
|
|
483
|
-
input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
|
|
484
|
-
output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
|
|
485
|
-
total: response.usage.totalTokens ?? 0,
|
|
486
|
-
cached: extractCachedTokens(response.usage),
|
|
487
|
-
});
|
|
488
|
-
}
|
|
439
|
+
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
489
440
|
|
|
490
441
|
return response;
|
|
491
442
|
} catch (error: any) {
|
|
@@ -493,12 +444,7 @@ export class Provider {
|
|
|
493
444
|
if (error?.name === 'AbortError') throw error;
|
|
494
445
|
if (error instanceof ContextLengthError) throw error;
|
|
495
446
|
if (Provider.isContextLengthError(error)) {
|
|
496
|
-
|
|
497
|
-
if (reduced) {
|
|
498
|
-
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
499
|
-
return this.generateObject(reduced.messages, schema, model, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
500
|
-
}
|
|
501
|
-
throw new ContextLengthError(error.message || error.toString());
|
|
447
|
+
return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateObject(m, schema, model, o));
|
|
502
448
|
}
|
|
503
449
|
throw new AiError(error.message || error.toString());
|
|
504
450
|
}
|
|
@@ -506,7 +452,7 @@ export class Provider {
|
|
|
506
452
|
|
|
507
453
|
static isContextLengthError(error: any): boolean {
|
|
508
454
|
const msg = (error?.message || error?.toString() || '').toLowerCase();
|
|
509
|
-
return
|
|
455
|
+
return CONTEXT_LENGTH_PATTERNS.some((p) => msg.includes(p));
|
|
510
456
|
}
|
|
511
457
|
|
|
512
458
|
static trimMessagesForRetry(messages: ModelMessage[]): ModelMessage[] | null {
|
|
@@ -677,13 +623,7 @@ export class Provider {
|
|
|
677
623
|
clearActivity();
|
|
678
624
|
responseLog(response.text);
|
|
679
625
|
|
|
680
|
-
|
|
681
|
-
Stats.recordTokens('vision', this.getModelName(this.config.visionModel), {
|
|
682
|
-
input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
|
|
683
|
-
output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
|
|
684
|
-
total: response.usage.totalTokens ?? 0,
|
|
685
|
-
});
|
|
686
|
-
}
|
|
626
|
+
this.recordUsage('vision', this.getModelName(this.config.visionModel), response.usage);
|
|
687
627
|
|
|
688
628
|
return response;
|
|
689
629
|
} catch (error: any) {
|
package/src/ai/quartermaster.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import type { ActionResult } from '../action-result.ts';
|
|
5
|
-
import {
|
|
5
|
+
import { outputPath } from '../config.ts';
|
|
6
6
|
import type { StateManager, StateTransition, WebPageState } from '../state-manager.ts';
|
|
7
7
|
import type { Task } from '../test-plan.ts';
|
|
8
8
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
@@ -12,36 +12,6 @@ import type { Provider } from './provider.ts';
|
|
|
12
12
|
|
|
13
13
|
const debugLog = createDebug('explorbot:quartermaster');
|
|
14
14
|
|
|
15
|
-
interface AxeViolation {
|
|
16
|
-
id: string;
|
|
17
|
-
impact: 'critical' | 'serious' | 'moderate' | 'minor';
|
|
18
|
-
description: string;
|
|
19
|
-
helpUrl: string;
|
|
20
|
-
nodes: Array<{ html: string; target: string[] }>;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface PageAnalysis {
|
|
24
|
-
url: string;
|
|
25
|
-
stateHash: string;
|
|
26
|
-
timestamp: string;
|
|
27
|
-
axeViolations: AxeViolation[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
interface SemanticIssue {
|
|
31
|
-
type: 'unclear_intention' | 'confusing_naming' | 'hard_to_interact';
|
|
32
|
-
element: string;
|
|
33
|
-
issue: string;
|
|
34
|
-
suggestion: string;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
interface AnalysisReport {
|
|
38
|
-
scenario: string;
|
|
39
|
-
timestamp: string;
|
|
40
|
-
url: string;
|
|
41
|
-
axeViolations: AxeViolation[];
|
|
42
|
-
semanticIssues: SemanticIssue[];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
15
|
export class Quartermaster {
|
|
46
16
|
private provider: Provider;
|
|
47
17
|
private model?: any;
|
|
@@ -55,13 +25,12 @@ export class Quartermaster {
|
|
|
55
25
|
this.provider = provider;
|
|
56
26
|
this.model = options?.model;
|
|
57
27
|
|
|
58
|
-
|
|
59
|
-
this.outputDir = join(configParser.getOutputDir(), 'a11y');
|
|
28
|
+
this.outputDir = outputPath('a11y');
|
|
60
29
|
}
|
|
61
30
|
|
|
62
31
|
start(playwrightHelper: any, stateManager: StateManager): void {
|
|
63
32
|
this.playwrightHelper = playwrightHelper;
|
|
64
|
-
this.
|
|
33
|
+
mkdirSync(this.outputDir, { recursive: true });
|
|
65
34
|
|
|
66
35
|
this.unsubscribe = stateManager.onStateChange((event) => {
|
|
67
36
|
this.onPageChange(event);
|
|
@@ -78,12 +47,6 @@ export class Quartermaster {
|
|
|
78
47
|
debugLog('Quartermaster stopped');
|
|
79
48
|
}
|
|
80
49
|
|
|
81
|
-
private ensureDirectory(): void {
|
|
82
|
-
if (!existsSync(this.outputDir)) {
|
|
83
|
-
mkdirSync(this.outputDir, { recursive: true });
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
|
|
87
50
|
private onPageChange(event: StateTransition): void {
|
|
88
51
|
const state = event.toState;
|
|
89
52
|
if (!state?.hash) return;
|
|
@@ -284,3 +247,33 @@ Focus on what would confuse a real user or caused the agent to make mistakes.`;
|
|
|
284
247
|
return content;
|
|
285
248
|
}
|
|
286
249
|
}
|
|
250
|
+
|
|
251
|
+
interface AxeViolation {
|
|
252
|
+
id: string;
|
|
253
|
+
impact: 'critical' | 'serious' | 'moderate' | 'minor';
|
|
254
|
+
description: string;
|
|
255
|
+
helpUrl: string;
|
|
256
|
+
nodes: Array<{ html: string; target: string[] }>;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
interface PageAnalysis {
|
|
260
|
+
url: string;
|
|
261
|
+
stateHash: string;
|
|
262
|
+
timestamp: string;
|
|
263
|
+
axeViolations: AxeViolation[];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface SemanticIssue {
|
|
267
|
+
type: 'unclear_intention' | 'confusing_naming' | 'hard_to_interact';
|
|
268
|
+
element: string;
|
|
269
|
+
issue: string;
|
|
270
|
+
suggestion: string;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
interface AnalysisReport {
|
|
274
|
+
scenario: string;
|
|
275
|
+
timestamp: string;
|
|
276
|
+
url: string;
|
|
277
|
+
axeViolations: AxeViolation[];
|
|
278
|
+
semanticIssues: SemanticIssue[];
|
|
279
|
+
}
|
package/src/ai/rerunner.ts
CHANGED
|
@@ -17,10 +17,10 @@ import type Explorer from '../explorer.ts';
|
|
|
17
17
|
import type { KnowledgeTracker } from '../knowledge-tracker.ts';
|
|
18
18
|
import { Stats } from '../stats.ts';
|
|
19
19
|
import { Task, Test, TestResult } from '../test-plan.ts';
|
|
20
|
+
import { formatHeadings } from '../utils/context-formatter.ts';
|
|
20
21
|
import { createDebug, tag } from '../utils/logger.ts';
|
|
21
22
|
import { loop } from '../utils/loop.ts';
|
|
22
23
|
import { RulesLoader } from '../utils/rules-loader.ts';
|
|
23
|
-
import { loadTestSuites, printTestList } from '../utils/test-files.ts';
|
|
24
24
|
import type { Agent } from './agent.ts';
|
|
25
25
|
import { toolExecutionLabel } from './conversation.ts';
|
|
26
26
|
import type { Navigator } from './navigator.ts';
|
|
@@ -38,8 +38,9 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
38
38
|
private explorer: Explorer;
|
|
39
39
|
private provider: Provider;
|
|
40
40
|
private agentTools: any;
|
|
41
|
-
private healedSteps: Array<{
|
|
41
|
+
private healedSteps: Array<{ original: string; healed: string }> = [];
|
|
42
42
|
private traceDir = '';
|
|
43
|
+
private static pluginsWired = false;
|
|
43
44
|
|
|
44
45
|
constructor(explorer: Explorer, provider: Provider, agentTools?: any) {
|
|
45
46
|
super();
|
|
@@ -76,10 +77,6 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
76
77
|
return this.rerunnerConfig.healMaxIterations ?? 3;
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
listTests(testsDir: string): void {
|
|
80
|
-
printTestList(loadTestSuites(testsDir));
|
|
81
|
-
}
|
|
82
|
-
|
|
83
80
|
async rerun(filePath: string, options?: { testIndices?: number[] }): Promise<RerunResult> {
|
|
84
81
|
const absPath = resolve(filePath);
|
|
85
82
|
if (!existsSync(absPath)) {
|
|
@@ -219,7 +216,6 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
219
216
|
|
|
220
217
|
private setupPlugins(): void {
|
|
221
218
|
const healMod = heal.default || heal;
|
|
222
|
-
healMod.connectToEvents();
|
|
223
219
|
|
|
224
220
|
healMod.addRecipe('explorbot-ai-healer', {
|
|
225
221
|
priority: 10,
|
|
@@ -233,21 +229,29 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
233
229
|
healMod.addRecipe(name, recipe);
|
|
234
230
|
}
|
|
235
231
|
|
|
232
|
+
const outputDir = (global as any).output_dir || 'output';
|
|
233
|
+
this.traceDir = `${outputDir}/rerun-traces`;
|
|
234
|
+
|
|
235
|
+
if (Rerunner.pluginsWired) return;
|
|
236
|
+
Rerunner.pluginsWired = true;
|
|
237
|
+
|
|
238
|
+
healMod.connectToEvents();
|
|
239
|
+
|
|
236
240
|
let currentTest: any = null;
|
|
237
241
|
let healTries = 0;
|
|
238
242
|
let isHealing = false;
|
|
239
243
|
let caughtError: any = null;
|
|
240
|
-
const healLimit = this.healLimit;
|
|
241
244
|
|
|
242
245
|
codeceptjs.event.dispatcher.on('test.before', (test: any) => {
|
|
243
246
|
currentTest = test;
|
|
244
247
|
healTries = 0;
|
|
245
248
|
caughtError = null;
|
|
249
|
+
isHealing = false;
|
|
246
250
|
});
|
|
247
251
|
|
|
248
252
|
codeceptjs.event.dispatcher.on('step.after', (step: any) => {
|
|
249
253
|
if (isHealing) return;
|
|
250
|
-
if (healTries >= healLimit) return;
|
|
254
|
+
if (healTries >= this.healLimit) return;
|
|
251
255
|
if (!healMod.hasCorrespondingRecipes(step)) return;
|
|
252
256
|
|
|
253
257
|
codeceptjs.recorder.catchWithoutStop(async (err: any) => {
|
|
@@ -286,10 +290,6 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
286
290
|
factor: 1.5,
|
|
287
291
|
});
|
|
288
292
|
|
|
289
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
290
|
-
const outputDir = (global as any).output_dir || 'output';
|
|
291
|
-
this.traceDir = `${outputDir}/rerun_${timestamp}`;
|
|
292
|
-
|
|
293
293
|
const aiTrace = aiTracePlugin.default || aiTracePlugin;
|
|
294
294
|
aiTrace(this.rerunnerConfig.aiTrace || { output: this.traceDir });
|
|
295
295
|
|
|
@@ -303,9 +303,9 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
303
303
|
|
|
304
304
|
private teardownHealing(): void {
|
|
305
305
|
const healMod = heal.default || heal;
|
|
306
|
-
healMod.recipes
|
|
306
|
+
Reflect.deleteProperty(healMod.recipes, 'explorbot-ai-healer');
|
|
307
307
|
for (const name of Object.keys(this.rerunnerConfig.recipes || {})) {
|
|
308
|
-
healMod.recipes
|
|
308
|
+
Reflect.deleteProperty(healMod.recipes, name);
|
|
309
309
|
}
|
|
310
310
|
}
|
|
311
311
|
|
|
@@ -432,7 +432,7 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
432
432
|
throw new Error(`Could not heal: ${failedCode}`);
|
|
433
433
|
}
|
|
434
434
|
|
|
435
|
-
this.healedSteps.push({
|
|
435
|
+
this.healedSteps.push({ original: failedCode, healed: healedCommand });
|
|
436
436
|
console.log(chalk.green(` ${figureSet.tick} Healed: ${healedCommand}`));
|
|
437
437
|
};
|
|
438
438
|
}
|
|
@@ -477,11 +477,7 @@ export class Rerunner extends TaskAgent implements Agent {
|
|
|
477
477
|
const state = this.explorer.getStateManager().getCurrentState();
|
|
478
478
|
const actionResult = state ? ActionResult.fromState(state) : null;
|
|
479
479
|
|
|
480
|
-
const headings
|
|
481
|
-
if (state?.h1) headings.push(`H1: ${state.h1}`);
|
|
482
|
-
if (state?.h2) headings.push(`H2: ${state.h2}`);
|
|
483
|
-
if (state?.h3) headings.push(`H3: ${state.h3}`);
|
|
484
|
-
if (state?.h4) headings.push(`H4: ${state.h4}`);
|
|
480
|
+
const headings = formatHeadings(state || {});
|
|
485
481
|
|
|
486
482
|
return dedent`
|
|
487
483
|
A test step failed and needs healing.
|
|
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'no
|
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { Worker } from 'node:worker_threads';
|
|
4
4
|
import { outputPath } from '../../config.ts';
|
|
5
|
+
import { TTLCache } from '../../utils/cache.ts';
|
|
5
6
|
import { computeHtmlFingerprint } from '../../utils/html-diff.ts';
|
|
6
7
|
import { debugLog } from './mixin.ts';
|
|
7
8
|
|
|
@@ -10,8 +11,7 @@ const FINGERPRINT_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
|
10
11
|
const FINGERPRINT_WORKER_TIMEOUT_MS = 10_000;
|
|
11
12
|
const SIMILARITY_THRESHOLD = 90;
|
|
12
13
|
|
|
13
|
-
const memoryCache
|
|
14
|
-
const memoryCacheTimestamps: Record<string, number> = {};
|
|
14
|
+
const memoryCache = new TTLCache<string>(CACHE_TTL_MS);
|
|
15
15
|
|
|
16
16
|
let fingerprintWorker: Worker | null = null;
|
|
17
17
|
|
|
@@ -28,25 +28,20 @@ function getFingerprintWorker(): Worker {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function clearResearchCache(): void {
|
|
31
|
-
|
|
32
|
-
for (const key of Object.keys(memoryCacheTimestamps)) delete memoryCacheTimestamps[key];
|
|
31
|
+
memoryCache.clear();
|
|
33
32
|
}
|
|
34
33
|
|
|
35
34
|
export function getCachedResearch(hash: string): string {
|
|
36
35
|
if (!hash) return '';
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
if (timestamp && now - timestamp <= CACHE_TTL_MS) {
|
|
40
|
-
return memoryCache[hash] || '';
|
|
41
|
-
}
|
|
36
|
+
const cached = memoryCache.get(hash);
|
|
37
|
+
if (cached !== undefined) return cached;
|
|
42
38
|
const researchFile = outputPath('research', `${hash}.md`);
|
|
43
39
|
if (!existsSync(researchFile)) return '';
|
|
44
40
|
const stats = statSync(researchFile);
|
|
45
|
-
if (now - stats.mtimeMs > CACHE_TTL_MS) return '';
|
|
46
|
-
const
|
|
47
|
-
memoryCache
|
|
48
|
-
|
|
49
|
-
return cached;
|
|
41
|
+
if (Date.now() - stats.mtimeMs > CACHE_TTL_MS) return '';
|
|
42
|
+
const fromDisk = readFileSync(researchFile, 'utf8');
|
|
43
|
+
memoryCache.set(hash, fromDisk);
|
|
44
|
+
return fromDisk;
|
|
50
45
|
}
|
|
51
46
|
|
|
52
47
|
export function getPreviousResearch(hash: string): string {
|
|
@@ -61,8 +56,7 @@ export function saveResearch(hash: string, text: string, combinedHtml?: string):
|
|
|
61
56
|
const researchFile = join(researchDir, `${hash}.md`);
|
|
62
57
|
if (!existsSync(researchDir)) mkdirSync(researchDir, { recursive: true });
|
|
63
58
|
writeFileSync(researchFile, text);
|
|
64
|
-
memoryCache
|
|
65
|
-
memoryCacheTimestamps[hash] = Date.now();
|
|
59
|
+
memoryCache.set(hash, text);
|
|
66
60
|
debugLog(`Research saved to ${researchFile}`);
|
|
67
61
|
|
|
68
62
|
if (combinedHtml) {
|