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
|
@@ -12,7 +12,6 @@ export declare class Provider {
|
|
|
12
12
|
telemetryEnabled: boolean;
|
|
13
13
|
otelSdk: NodeSDK | null;
|
|
14
14
|
defaultRetryOptions: RetryOptions;
|
|
15
|
-
static readonly CONTEXT_LENGTH_PATTERNS: string[];
|
|
16
15
|
lastConversation: Conversation | null;
|
|
17
16
|
constructor(config: AIConfig);
|
|
18
17
|
getModelName(model: any): string;
|
|
@@ -26,6 +25,10 @@ export declare class Provider {
|
|
|
26
25
|
getRetryOptions(options?: any): RetryOptions;
|
|
27
26
|
mergeProviderOptions(config: Record<string, any>, agentName?: string): Record<string, any>;
|
|
28
27
|
finalizeConfig(config: Record<string, any>, options: any, telemetry: any): void;
|
|
28
|
+
buildGenerateConfig(defaults: Record<string, any>, overrides: Record<string, any>, options: any): Record<string, any>;
|
|
29
|
+
recordUsage(agentName: string, modelName: string, usage: any): void;
|
|
30
|
+
raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number): Promise<T>;
|
|
31
|
+
recoverFromContextLength(error: any, messages: ModelMessage[], options: any, retry: (messages: ModelMessage[], options: any) => Promise<any>): Promise<any>;
|
|
29
32
|
initLangfuse(): void;
|
|
30
33
|
getTelemetry(options: any): any;
|
|
31
34
|
startConversation(systemMessage: string, agentName?: string, model?: any): Conversation;
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -9,7 +9,7 @@ import { Stats } from "../stats.js";
|
|
|
9
9
|
import { createDebug, tag } from '../utils/logger.js';
|
|
10
10
|
import { withRetry } from '../utils/retry.js';
|
|
11
11
|
import { RulesLoader } from "../utils/rules-loader.js";
|
|
12
|
-
import { Conversation } from './conversation.js';
|
|
12
|
+
import { Conversation, toToolExecution } from './conversation.js';
|
|
13
13
|
const debugLog = createDebug('explorbot:provider');
|
|
14
14
|
const promptLog = createDebug('explorbot:provider:out');
|
|
15
15
|
const responseLog = createDebug('explorbot:provider:in');
|
|
@@ -18,6 +18,7 @@ class AiError extends Error {
|
|
|
18
18
|
export class ContextLengthError extends Error {
|
|
19
19
|
}
|
|
20
20
|
let telemetryRegistered = false;
|
|
21
|
+
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'];
|
|
21
22
|
function extractCachedTokens(usage) {
|
|
22
23
|
if (!usage)
|
|
23
24
|
return 0;
|
|
@@ -28,10 +29,10 @@ function extractCachedTokens(usage) {
|
|
|
28
29
|
const fromRaw = raw?.prompt_tokens_details?.cached_tokens ?? raw?.promptTokensDetails?.cachedTokens;
|
|
29
30
|
return typeof fromRaw === 'number' ? fromRaw : 0;
|
|
30
31
|
}
|
|
31
|
-
function
|
|
32
|
+
function abortAfterIdle(ms, cancel, controller) {
|
|
32
33
|
return new Promise((_, reject) => {
|
|
33
34
|
const tick = () => {
|
|
34
|
-
if (
|
|
35
|
+
if (cancel.cancelled)
|
|
35
36
|
return;
|
|
36
37
|
if (executionController.isAwaitingInput()) {
|
|
37
38
|
setTimeout(tick, ms);
|
|
@@ -43,6 +44,12 @@ function rejectAfterIdle(ms, signal, controller) {
|
|
|
43
44
|
setTimeout(tick, ms);
|
|
44
45
|
});
|
|
45
46
|
}
|
|
47
|
+
function combinedAbortSignal(controller) {
|
|
48
|
+
const executionSignal = executionController.getAbortSignal();
|
|
49
|
+
if (!executionSignal)
|
|
50
|
+
return controller.signal;
|
|
51
|
+
return AbortSignal.any([controller.signal, executionSignal]);
|
|
52
|
+
}
|
|
46
53
|
export class Provider {
|
|
47
54
|
config;
|
|
48
55
|
telemetryEnabled = false;
|
|
@@ -64,7 +71,6 @@ export class Provider {
|
|
|
64
71
|
!error.message.includes('output truncated at maxTokens'));
|
|
65
72
|
},
|
|
66
73
|
};
|
|
67
|
-
static CONTEXT_LENGTH_PATTERNS = ['reduce the length', 'context length', 'maximum context', 'token limit', 'too many tokens', 'max_tokens', 'context_length_exceeded', 'output truncated at maxtokens'];
|
|
68
74
|
lastConversation = null;
|
|
69
75
|
constructor(config) {
|
|
70
76
|
if (!config?.model) {
|
|
@@ -161,6 +167,46 @@ export class Provider {
|
|
|
161
167
|
if (reasoning)
|
|
162
168
|
config.reasoning ??= reasoning;
|
|
163
169
|
}
|
|
170
|
+
buildGenerateConfig(defaults, overrides, options) {
|
|
171
|
+
const telemetry = this.getTelemetry(options);
|
|
172
|
+
const config = this.mergeProviderOptions({
|
|
173
|
+
...defaults,
|
|
174
|
+
allowSystemInMessages: true,
|
|
175
|
+
...(this.config.config || {}),
|
|
176
|
+
...options,
|
|
177
|
+
...overrides,
|
|
178
|
+
}, options.agentName);
|
|
179
|
+
this.finalizeConfig(config, options, telemetry);
|
|
180
|
+
return config;
|
|
181
|
+
}
|
|
182
|
+
recordUsage(agentName, modelName, usage) {
|
|
183
|
+
if (!usage)
|
|
184
|
+
return;
|
|
185
|
+
Stats.recordTokens(agentName, modelName, {
|
|
186
|
+
input: usage.inputTokens ?? usage.promptTokens ?? 0,
|
|
187
|
+
output: usage.outputTokens ?? usage.completionTokens ?? 0,
|
|
188
|
+
total: usage.totalTokens ?? 0,
|
|
189
|
+
cached: extractCachedTokens(usage),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
async raceWithIdleTimeout(fn, timeoutMs) {
|
|
193
|
+
const cancel = { cancelled: false };
|
|
194
|
+
const controller = new AbortController();
|
|
195
|
+
const combinedSignal = combinedAbortSignal(controller);
|
|
196
|
+
try {
|
|
197
|
+
return await Promise.race([fn(combinedSignal), abortAfterIdle(timeoutMs, cancel, controller)]);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
cancel.cancelled = true;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
async recoverFromContextLength(error, messages, options, retry) {
|
|
204
|
+
const reduced = this.tryReduceMessages(messages, options._contextRetryLevel || 0);
|
|
205
|
+
if (!reduced)
|
|
206
|
+
throw new ContextLengthError(error.message || error.toString());
|
|
207
|
+
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
208
|
+
return retry(reduced.messages, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
209
|
+
}
|
|
164
210
|
initLangfuse() {
|
|
165
211
|
const langfuseConfig = this.config.langfuse;
|
|
166
212
|
const publicKey = langfuseConfig?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
@@ -191,18 +237,22 @@ export class Provider {
|
|
|
191
237
|
return undefined;
|
|
192
238
|
}
|
|
193
239
|
const runTelemetry = Observability.getTelemetry();
|
|
194
|
-
|
|
240
|
+
let optionTelemetry = options.telemetry;
|
|
241
|
+
if (options.telemetryFunctionId && !optionTelemetry?.functionId) {
|
|
242
|
+
optionTelemetry = { ...optionTelemetry, functionId: options.telemetryFunctionId };
|
|
243
|
+
}
|
|
244
|
+
if (!optionTelemetry) {
|
|
195
245
|
return runTelemetry;
|
|
196
246
|
}
|
|
197
247
|
if (!runTelemetry) {
|
|
198
|
-
return
|
|
248
|
+
return optionTelemetry;
|
|
199
249
|
}
|
|
200
250
|
return {
|
|
201
251
|
...runTelemetry,
|
|
202
|
-
...
|
|
252
|
+
...optionTelemetry,
|
|
203
253
|
metadata: {
|
|
204
254
|
...runTelemetry.metadata,
|
|
205
|
-
...
|
|
255
|
+
...optionTelemetry.metadata,
|
|
206
256
|
},
|
|
207
257
|
};
|
|
208
258
|
}
|
|
@@ -228,28 +278,15 @@ export class Provider {
|
|
|
228
278
|
this.lastConversation = conversation;
|
|
229
279
|
const toolCalls = response.toolCalls || [];
|
|
230
280
|
const toolResults = response.toolResults || [];
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
input: call.input,
|
|
234
|
-
output: toolResults[index]?.output,
|
|
235
|
-
wasSuccessful: toolResults[index]?.output?.success || false,
|
|
236
|
-
}));
|
|
281
|
+
const resultsById = new Map(toolResults.map((r) => [r.toolCallId, r]));
|
|
282
|
+
const toolExecutions = toolCalls.map((call) => toToolExecution(call.toolName || '', call.input, resultsById.get(call.toolCallId)?.output));
|
|
237
283
|
return { conversation, response, toolExecutions };
|
|
238
284
|
}
|
|
239
285
|
async chat(messages, model, options = {}) {
|
|
240
286
|
const modelName = this.getModelName(model);
|
|
241
287
|
setActivity(`🤖 Asking ${modelName}`, 'ai');
|
|
242
288
|
promptLog(`Using model: ${modelName}`);
|
|
243
|
-
const
|
|
244
|
-
const config = this.mergeProviderOptions({
|
|
245
|
-
maxOutputTokens: 16384,
|
|
246
|
-
allowSystemInMessages: true,
|
|
247
|
-
...(this.config.config || {}),
|
|
248
|
-
...options,
|
|
249
|
-
model,
|
|
250
|
-
abortSignal: executionController.getAbortSignal(),
|
|
251
|
-
}, options.agentName);
|
|
252
|
-
this.finalizeConfig(config, options, telemetry);
|
|
289
|
+
const config = this.buildGenerateConfig({ maxOutputTokens: 16384 }, { model, abortSignal: executionController.getAbortSignal() }, options);
|
|
253
290
|
promptLog(messages[messages.length - 1].content);
|
|
254
291
|
try {
|
|
255
292
|
const response = await withRetry(async () => {
|
|
@@ -268,14 +305,7 @@ export class Provider {
|
|
|
268
305
|
}, this.getRetryOptions(options));
|
|
269
306
|
clearActivity();
|
|
270
307
|
responseLog(response.text);
|
|
271
|
-
|
|
272
|
-
Stats.recordTokens(options.agentName || 'unknown', modelName, {
|
|
273
|
-
input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
|
|
274
|
-
output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
|
|
275
|
-
total: response.usage.totalTokens ?? 0,
|
|
276
|
-
cached: extractCachedTokens(response.usage),
|
|
277
|
-
});
|
|
278
|
-
}
|
|
308
|
+
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
279
309
|
return response;
|
|
280
310
|
}
|
|
281
311
|
catch (error) {
|
|
@@ -285,12 +315,7 @@ export class Provider {
|
|
|
285
315
|
if (error instanceof ContextLengthError)
|
|
286
316
|
throw error;
|
|
287
317
|
if (Provider.isContextLengthError(error)) {
|
|
288
|
-
|
|
289
|
-
if (reduced) {
|
|
290
|
-
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
291
|
-
return this.chat(reduced.messages, model, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
292
|
-
}
|
|
293
|
-
throw new ContextLengthError(error.message || error.toString());
|
|
318
|
+
return this.recoverFromContextLength(error, messages, options, (m, o) => this.chat(m, model, o));
|
|
294
319
|
}
|
|
295
320
|
const message = error.message || error.toString();
|
|
296
321
|
if (message !== 'No response text from AI') {
|
|
@@ -307,49 +332,20 @@ export class Provider {
|
|
|
307
332
|
tag('debug').log(`Tools enabled: [${toolNames.join(', ')}]`);
|
|
308
333
|
promptLog('Available tools:', toolNames);
|
|
309
334
|
promptLog(messages[messages.length - 1].content);
|
|
310
|
-
const telemetry = this.getTelemetry(options);
|
|
311
335
|
const maxRoundtrips = options.maxToolRoundtrips ?? 5;
|
|
312
336
|
const extraStop = options.stopWhen;
|
|
313
337
|
const stopConditions = [isStepCount(maxRoundtrips)];
|
|
314
338
|
if (extraStop)
|
|
315
339
|
stopConditions.push(extraStop);
|
|
316
|
-
const { stopWhen:
|
|
317
|
-
const config = this.mergeProviderOptions({
|
|
318
|
-
tools,
|
|
319
|
-
maxOutputTokens: 16384,
|
|
320
|
-
toolChoice: 'auto',
|
|
321
|
-
allowSystemInMessages: true,
|
|
322
|
-
...(this.config.config || {}),
|
|
323
|
-
...optionsWithoutStop,
|
|
324
|
-
stopWhen: stopConditions,
|
|
325
|
-
model,
|
|
326
|
-
abortSignal: executionController.getAbortSignal(),
|
|
327
|
-
}, options.agentName);
|
|
328
|
-
this.finalizeConfig(config, options, telemetry);
|
|
340
|
+
const config = this.buildGenerateConfig({ tools, maxOutputTokens: 16384, toolChoice: 'auto' }, { stopWhen: stopConditions, model }, options);
|
|
329
341
|
try {
|
|
330
342
|
const response = await withRetry(async () => {
|
|
331
|
-
const
|
|
332
|
-
const
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
try {
|
|
336
|
-
const result = (await Promise.race([
|
|
337
|
-
generateText({
|
|
338
|
-
messages,
|
|
339
|
-
...config,
|
|
340
|
-
abortSignal: combinedSignal,
|
|
341
|
-
}),
|
|
342
|
-
rejectAfterIdle(timeout, cancel, controller),
|
|
343
|
-
]));
|
|
344
|
-
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
345
|
-
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
346
|
-
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
347
|
-
}
|
|
348
|
-
return result;
|
|
349
|
-
}
|
|
350
|
-
finally {
|
|
351
|
-
cancel.cancelled = true;
|
|
343
|
+
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000));
|
|
344
|
+
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
345
|
+
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
346
|
+
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
352
347
|
}
|
|
348
|
+
return result;
|
|
353
349
|
}, this.getRetryOptions(options));
|
|
354
350
|
clearActivity();
|
|
355
351
|
// Log tool usage summary
|
|
@@ -360,14 +356,7 @@ export class Provider {
|
|
|
360
356
|
});
|
|
361
357
|
}
|
|
362
358
|
responseLog(response.text);
|
|
363
|
-
|
|
364
|
-
Stats.recordTokens(options.agentName || 'unknown', modelName, {
|
|
365
|
-
input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
|
|
366
|
-
output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
|
|
367
|
-
total: response.usage.totalTokens ?? 0,
|
|
368
|
-
cached: extractCachedTokens(response.usage),
|
|
369
|
-
});
|
|
370
|
-
}
|
|
359
|
+
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
371
360
|
return response;
|
|
372
361
|
}
|
|
373
362
|
catch (error) {
|
|
@@ -380,12 +369,7 @@ export class Provider {
|
|
|
380
369
|
if (error instanceof ContextLengthError)
|
|
381
370
|
throw error;
|
|
382
371
|
if (Provider.isContextLengthError(error)) {
|
|
383
|
-
|
|
384
|
-
if (reduced) {
|
|
385
|
-
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
386
|
-
return this.generateWithTools(reduced.messages, model, tools, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
387
|
-
}
|
|
388
|
-
throw new ContextLengthError(error.message || error.toString());
|
|
372
|
+
return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateWithTools(m, model, tools, o));
|
|
389
373
|
}
|
|
390
374
|
if (error.constructor?.name === 'AI_APICallError') {
|
|
391
375
|
responseLog(error.message);
|
|
@@ -399,47 +383,15 @@ export class Provider {
|
|
|
399
383
|
const modelName = this.getModelName(modelToUse);
|
|
400
384
|
setActivity(`🤖 Asking ${modelName} for structured output`, 'ai');
|
|
401
385
|
promptLog(`Using model: ${modelName}`);
|
|
402
|
-
const
|
|
403
|
-
const config = this.mergeProviderOptions({
|
|
404
|
-
schema,
|
|
405
|
-
allowSystemInMessages: true,
|
|
406
|
-
...(this.config.config || {}),
|
|
407
|
-
...options,
|
|
408
|
-
model: modelToUse,
|
|
409
|
-
abortSignal: executionController.getAbortSignal(),
|
|
410
|
-
}, options.agentName);
|
|
411
|
-
this.finalizeConfig(config, options, telemetry);
|
|
386
|
+
const config = this.buildGenerateConfig({ schema }, { model: modelToUse }, options);
|
|
412
387
|
try {
|
|
413
388
|
promptLog(messages[messages.length - 1].content);
|
|
414
389
|
const response = await withRetry(async () => {
|
|
415
|
-
|
|
416
|
-
const cancel = { cancelled: false };
|
|
417
|
-
const controller = new AbortController();
|
|
418
|
-
const combinedSignal = AbortSignal.any([controller.signal, executionController.getAbortSignal()].filter(Boolean));
|
|
419
|
-
try {
|
|
420
|
-
return (await Promise.race([
|
|
421
|
-
generateObject({
|
|
422
|
-
messages,
|
|
423
|
-
...config,
|
|
424
|
-
abortSignal: combinedSignal,
|
|
425
|
-
}),
|
|
426
|
-
rejectAfterIdle(timeout, cancel, controller),
|
|
427
|
-
]));
|
|
428
|
-
}
|
|
429
|
-
finally {
|
|
430
|
-
cancel.cancelled = true;
|
|
431
|
-
}
|
|
390
|
+
return (await this.raceWithIdleTimeout((signal) => generateObject({ messages, ...config, abortSignal: signal }), config.timeout || 30000));
|
|
432
391
|
}, this.getRetryOptions(options));
|
|
433
392
|
clearActivity();
|
|
434
393
|
responseLog(response.object);
|
|
435
|
-
|
|
436
|
-
Stats.recordTokens(options.agentName || 'unknown', modelName, {
|
|
437
|
-
input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
|
|
438
|
-
output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
|
|
439
|
-
total: response.usage.totalTokens ?? 0,
|
|
440
|
-
cached: extractCachedTokens(response.usage),
|
|
441
|
-
});
|
|
442
|
-
}
|
|
394
|
+
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
443
395
|
return response;
|
|
444
396
|
}
|
|
445
397
|
catch (error) {
|
|
@@ -449,19 +401,14 @@ export class Provider {
|
|
|
449
401
|
if (error instanceof ContextLengthError)
|
|
450
402
|
throw error;
|
|
451
403
|
if (Provider.isContextLengthError(error)) {
|
|
452
|
-
|
|
453
|
-
if (reduced) {
|
|
454
|
-
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
455
|
-
return this.generateObject(reduced.messages, schema, model, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
456
|
-
}
|
|
457
|
-
throw new ContextLengthError(error.message || error.toString());
|
|
404
|
+
return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateObject(m, schema, model, o));
|
|
458
405
|
}
|
|
459
406
|
throw new AiError(error.message || error.toString());
|
|
460
407
|
}
|
|
461
408
|
}
|
|
462
409
|
static isContextLengthError(error) {
|
|
463
410
|
const msg = (error?.message || error?.toString() || '').toLowerCase();
|
|
464
|
-
return
|
|
411
|
+
return CONTEXT_LENGTH_PATTERNS.some((p) => msg.includes(p));
|
|
465
412
|
}
|
|
466
413
|
static trimMessagesForRetry(messages) {
|
|
467
414
|
const tagRegex = /<(\w[\w-]*)>([\s\S]*?)<\/\1>/g;
|
|
@@ -611,13 +558,7 @@ export class Provider {
|
|
|
611
558
|
}, this.getRetryOptions());
|
|
612
559
|
clearActivity();
|
|
613
560
|
responseLog(response.text);
|
|
614
|
-
|
|
615
|
-
Stats.recordTokens('vision', this.getModelName(this.config.visionModel), {
|
|
616
|
-
input: response.usage.inputTokens ?? response.usage.promptTokens ?? 0,
|
|
617
|
-
output: response.usage.outputTokens ?? response.usage.completionTokens ?? 0,
|
|
618
|
-
total: response.usage.totalTokens ?? 0,
|
|
619
|
-
});
|
|
620
|
-
}
|
|
561
|
+
this.recordUsage('vision', this.getModelName(this.config.visionModel), response.usage);
|
|
621
562
|
return response;
|
|
622
563
|
}
|
|
623
564
|
catch (error) {
|
|
@@ -3,6 +3,27 @@ import type { StateManager, StateTransition, WebPageState } from '../state-manag
|
|
|
3
3
|
import type { Task } from '../test-plan.js';
|
|
4
4
|
import type { Conversation, ToolExecution } from './conversation.js';
|
|
5
5
|
import type { Provider } from './provider.js';
|
|
6
|
+
export declare class Quartermaster {
|
|
7
|
+
provider: Provider;
|
|
8
|
+
model?: any;
|
|
9
|
+
outputDir: string;
|
|
10
|
+
pageAnalyses: Map<string, PageAnalysis>;
|
|
11
|
+
unsubscribe: (() => void) | null;
|
|
12
|
+
playwrightHelper: any;
|
|
13
|
+
pendingAnalyses: Promise<void>[];
|
|
14
|
+
constructor(provider: Provider, options?: {
|
|
15
|
+
model?: any;
|
|
16
|
+
});
|
|
17
|
+
start(playwrightHelper: any, stateManager: StateManager): void;
|
|
18
|
+
stop(): void;
|
|
19
|
+
onPageChange(event: StateTransition): void;
|
|
20
|
+
runAxeAnalysis(state: WebPageState): Promise<void>;
|
|
21
|
+
analyzeSession(task: Task, initialState: ActionResult, conversation: Conversation): Promise<AnalysisReport | null>;
|
|
22
|
+
generateSemanticAnalysis(axeViolations: AxeViolation[], executions: ToolExecution[], initialState: ActionResult): Promise<SemanticIssue[]>;
|
|
23
|
+
addNotesToTask(task: Task, report: AnalysisReport): void;
|
|
24
|
+
saveReport(stateHash: string, report: AnalysisReport): void;
|
|
25
|
+
formatReportMarkdown(report: AnalysisReport): string;
|
|
26
|
+
}
|
|
6
27
|
interface AxeViolation {
|
|
7
28
|
id: string;
|
|
8
29
|
impact: 'critical' | 'serious' | 'moderate' | 'minor';
|
|
@@ -32,26 +53,4 @@ interface AnalysisReport {
|
|
|
32
53
|
axeViolations: AxeViolation[];
|
|
33
54
|
semanticIssues: SemanticIssue[];
|
|
34
55
|
}
|
|
35
|
-
export declare class Quartermaster {
|
|
36
|
-
provider: Provider;
|
|
37
|
-
model?: any;
|
|
38
|
-
outputDir: string;
|
|
39
|
-
pageAnalyses: Map<string, PageAnalysis>;
|
|
40
|
-
unsubscribe: (() => void) | null;
|
|
41
|
-
playwrightHelper: any;
|
|
42
|
-
pendingAnalyses: Promise<void>[];
|
|
43
|
-
constructor(provider: Provider, options?: {
|
|
44
|
-
model?: any;
|
|
45
|
-
});
|
|
46
|
-
start(playwrightHelper: any, stateManager: StateManager): void;
|
|
47
|
-
stop(): void;
|
|
48
|
-
ensureDirectory(): void;
|
|
49
|
-
onPageChange(event: StateTransition): void;
|
|
50
|
-
runAxeAnalysis(state: WebPageState): Promise<void>;
|
|
51
|
-
analyzeSession(task: Task, initialState: ActionResult, conversation: Conversation): Promise<AnalysisReport | null>;
|
|
52
|
-
generateSemanticAnalysis(axeViolations: AxeViolation[], executions: ToolExecution[], initialState: ActionResult): Promise<SemanticIssue[]>;
|
|
53
|
-
addNotesToTask(task: Task, report: AnalysisReport): void;
|
|
54
|
-
saveReport(stateHash: string, report: AnalysisReport): void;
|
|
55
|
-
formatReportMarkdown(report: AnalysisReport): string;
|
|
56
|
-
}
|
|
57
56
|
export {};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import {
|
|
4
|
+
import { outputPath } from "../config.js";
|
|
5
5
|
import { createDebug, tag } from "../utils/logger.js";
|
|
6
6
|
import { isCodeceptToolName } from "../utils/step-analyzer.js";
|
|
7
7
|
const debugLog = createDebug('explorbot:quartermaster');
|
|
@@ -16,12 +16,11 @@ export class Quartermaster {
|
|
|
16
16
|
constructor(provider, options) {
|
|
17
17
|
this.provider = provider;
|
|
18
18
|
this.model = options?.model;
|
|
19
|
-
|
|
20
|
-
this.outputDir = join(configParser.getOutputDir(), 'a11y');
|
|
19
|
+
this.outputDir = outputPath('a11y');
|
|
21
20
|
}
|
|
22
21
|
start(playwrightHelper, stateManager) {
|
|
23
22
|
this.playwrightHelper = playwrightHelper;
|
|
24
|
-
this.
|
|
23
|
+
mkdirSync(this.outputDir, { recursive: true });
|
|
25
24
|
this.unsubscribe = stateManager.onStateChange((event) => {
|
|
26
25
|
this.onPageChange(event);
|
|
27
26
|
});
|
|
@@ -34,11 +33,6 @@ export class Quartermaster {
|
|
|
34
33
|
}
|
|
35
34
|
debugLog('Quartermaster stopped');
|
|
36
35
|
}
|
|
37
|
-
ensureDirectory() {
|
|
38
|
-
if (!existsSync(this.outputDir)) {
|
|
39
|
-
mkdirSync(this.outputDir, { recursive: true });
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
36
|
onPageChange(event) {
|
|
43
37
|
const state = event.toState;
|
|
44
38
|
if (!state?.hash)
|
|
@@ -13,11 +13,11 @@ export declare class Rerunner extends TaskAgent implements Agent {
|
|
|
13
13
|
provider: Provider;
|
|
14
14
|
agentTools: any;
|
|
15
15
|
healedSteps: Array<{
|
|
16
|
-
test: string;
|
|
17
16
|
original: string;
|
|
18
17
|
healed: string;
|
|
19
18
|
}>;
|
|
20
19
|
traceDir: string;
|
|
20
|
+
static pluginsWired: boolean;
|
|
21
21
|
constructor(explorer: Explorer, provider: Provider, agentTools?: any);
|
|
22
22
|
getNavigator(): Navigator;
|
|
23
23
|
getExperienceTracker(): ExperienceTracker;
|
|
@@ -26,7 +26,6 @@ export declare class Rerunner extends TaskAgent implements Agent {
|
|
|
26
26
|
get rerunnerConfig(): Record<string, any>;
|
|
27
27
|
get healLimit(): number;
|
|
28
28
|
get healMaxIterations(): number;
|
|
29
|
-
listTests(testsDir: string): void;
|
|
30
29
|
rerun(filePath: string, options?: {
|
|
31
30
|
testIndices?: number[];
|
|
32
31
|
}): Promise<RerunResult>;
|
package/dist/src/ai/rerunner.js
CHANGED
|
@@ -14,10 +14,10 @@ import { ActionResult } from "../action-result.js";
|
|
|
14
14
|
import { setActivity } from "../activity.js";
|
|
15
15
|
import { Stats } from "../stats.js";
|
|
16
16
|
import { Task, Test, TestResult } from "../test-plan.js";
|
|
17
|
+
import { formatHeadings } from "../utils/context-formatter.js";
|
|
17
18
|
import { createDebug, tag } from "../utils/logger.js";
|
|
18
19
|
import { loop } from "../utils/loop.js";
|
|
19
20
|
import { RulesLoader } from "../utils/rules-loader.js";
|
|
20
|
-
import { loadTestSuites, printTestList } from "../utils/test-files.js";
|
|
21
21
|
import { toolExecutionLabel } from "./conversation.js";
|
|
22
22
|
import { actionRule, locatorRule, sectionContextRule } from "./rules.js";
|
|
23
23
|
import { TaskAgent } from "./task-agent.js";
|
|
@@ -31,6 +31,7 @@ export class Rerunner extends TaskAgent {
|
|
|
31
31
|
agentTools;
|
|
32
32
|
healedSteps = [];
|
|
33
33
|
traceDir = '';
|
|
34
|
+
static pluginsWired = false;
|
|
34
35
|
constructor(explorer, provider, agentTools) {
|
|
35
36
|
super();
|
|
36
37
|
this.explorer = explorer;
|
|
@@ -58,9 +59,6 @@ export class Rerunner extends TaskAgent {
|
|
|
58
59
|
get healMaxIterations() {
|
|
59
60
|
return this.rerunnerConfig.healMaxIterations ?? 3;
|
|
60
61
|
}
|
|
61
|
-
listTests(testsDir) {
|
|
62
|
-
printTestList(loadTestSuites(testsDir));
|
|
63
|
-
}
|
|
64
62
|
async rerun(filePath, options) {
|
|
65
63
|
const absPath = resolve(filePath);
|
|
66
64
|
if (!existsSync(absPath)) {
|
|
@@ -191,7 +189,6 @@ export class Rerunner extends TaskAgent {
|
|
|
191
189
|
}
|
|
192
190
|
setupPlugins() {
|
|
193
191
|
const healMod = heal.default || heal;
|
|
194
|
-
healMod.connectToEvents();
|
|
195
192
|
healMod.addRecipe('explorbot-ai-healer', {
|
|
196
193
|
priority: 10,
|
|
197
194
|
fn: async (context) => {
|
|
@@ -202,20 +199,26 @@ export class Rerunner extends TaskAgent {
|
|
|
202
199
|
for (const [name, recipe] of Object.entries(userRecipes)) {
|
|
203
200
|
healMod.addRecipe(name, recipe);
|
|
204
201
|
}
|
|
202
|
+
const outputDir = global.output_dir || 'output';
|
|
203
|
+
this.traceDir = `${outputDir}/rerun-traces`;
|
|
204
|
+
if (Rerunner.pluginsWired)
|
|
205
|
+
return;
|
|
206
|
+
Rerunner.pluginsWired = true;
|
|
207
|
+
healMod.connectToEvents();
|
|
205
208
|
let currentTest = null;
|
|
206
209
|
let healTries = 0;
|
|
207
210
|
let isHealing = false;
|
|
208
211
|
let caughtError = null;
|
|
209
|
-
const healLimit = this.healLimit;
|
|
210
212
|
codeceptjs.event.dispatcher.on('test.before', (test) => {
|
|
211
213
|
currentTest = test;
|
|
212
214
|
healTries = 0;
|
|
213
215
|
caughtError = null;
|
|
216
|
+
isHealing = false;
|
|
214
217
|
});
|
|
215
218
|
codeceptjs.event.dispatcher.on('step.after', (step) => {
|
|
216
219
|
if (isHealing)
|
|
217
220
|
return;
|
|
218
|
-
if (healTries >= healLimit)
|
|
221
|
+
if (healTries >= this.healLimit)
|
|
219
222
|
return;
|
|
220
223
|
if (!healMod.hasCorrespondingRecipes(step))
|
|
221
224
|
return;
|
|
@@ -249,9 +252,6 @@ export class Rerunner extends TaskAgent {
|
|
|
249
252
|
maxTimeout: 5000,
|
|
250
253
|
factor: 1.5,
|
|
251
254
|
});
|
|
252
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
253
|
-
const outputDir = global.output_dir || 'output';
|
|
254
|
-
this.traceDir = `${outputDir}/rerun_${timestamp}`;
|
|
255
255
|
const aiTrace = aiTracePlugin.default || aiTracePlugin;
|
|
256
256
|
aiTrace(this.rerunnerConfig.aiTrace || { output: this.traceDir });
|
|
257
257
|
import('@testomatio/reporter/codecept')
|
|
@@ -263,9 +263,9 @@ export class Rerunner extends TaskAgent {
|
|
|
263
263
|
}
|
|
264
264
|
teardownHealing() {
|
|
265
265
|
const healMod = heal.default || heal;
|
|
266
|
-
healMod.recipes
|
|
266
|
+
Reflect.deleteProperty(healMod.recipes, 'explorbot-ai-healer');
|
|
267
267
|
for (const name of Object.keys(this.rerunnerConfig.recipes || {})) {
|
|
268
|
-
healMod.recipes
|
|
268
|
+
Reflect.deleteProperty(healMod.recipes, name);
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
271
|
async healStep(step, error) {
|
|
@@ -378,7 +378,7 @@ export class Rerunner extends TaskAgent {
|
|
|
378
378
|
if (!healed) {
|
|
379
379
|
throw new Error(`Could not heal: ${failedCode}`);
|
|
380
380
|
}
|
|
381
|
-
this.healedSteps.push({
|
|
381
|
+
this.healedSteps.push({ original: failedCode, healed: healedCommand });
|
|
382
382
|
console.log(chalk.green(` ${figureSet.tick} Healed: ${healedCommand}`));
|
|
383
383
|
};
|
|
384
384
|
}
|
|
@@ -419,15 +419,7 @@ export class Rerunner extends TaskAgent {
|
|
|
419
419
|
getHealUserPrompt(failedCode, error) {
|
|
420
420
|
const state = this.explorer.getStateManager().getCurrentState();
|
|
421
421
|
const actionResult = state ? ActionResult.fromState(state) : null;
|
|
422
|
-
const headings =
|
|
423
|
-
if (state?.h1)
|
|
424
|
-
headings.push(`H1: ${state.h1}`);
|
|
425
|
-
if (state?.h2)
|
|
426
|
-
headings.push(`H2: ${state.h2}`);
|
|
427
|
-
if (state?.h3)
|
|
428
|
-
headings.push(`H3: ${state.h3}`);
|
|
429
|
-
if (state?.h4)
|
|
430
|
-
headings.push(`H4: ${state.h4}`);
|
|
422
|
+
const headings = formatHeadings(state || {});
|
|
431
423
|
return dedent `
|
|
432
424
|
A test step failed and needs healing.
|
|
433
425
|
|
|
@@ -2,14 +2,14 @@ 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.js";
|
|
5
|
+
import { TTLCache } from "../../utils/cache.js";
|
|
5
6
|
import { computeHtmlFingerprint } from "../../utils/html-diff.js";
|
|
6
7
|
import { debugLog } from "./mixin.js";
|
|
7
8
|
const CACHE_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours
|
|
8
9
|
const FINGERPRINT_MAX_AGE_MS = 60 * 60 * 1000; // 1 hour
|
|
9
10
|
const FINGERPRINT_WORKER_TIMEOUT_MS = 10_000;
|
|
10
11
|
const SIMILARITY_THRESHOLD = 90;
|
|
11
|
-
const memoryCache =
|
|
12
|
-
const memoryCacheTimestamps = {};
|
|
12
|
+
const memoryCache = new TTLCache(CACHE_TTL_MS);
|
|
13
13
|
let fingerprintWorker = null;
|
|
14
14
|
function getStatesDir() {
|
|
15
15
|
return outputPath('states');
|
|
@@ -22,29 +22,23 @@ function getFingerprintWorker() {
|
|
|
22
22
|
return fingerprintWorker;
|
|
23
23
|
}
|
|
24
24
|
export function clearResearchCache() {
|
|
25
|
-
|
|
26
|
-
delete memoryCache[key];
|
|
27
|
-
for (const key of Object.keys(memoryCacheTimestamps))
|
|
28
|
-
delete memoryCacheTimestamps[key];
|
|
25
|
+
memoryCache.clear();
|
|
29
26
|
}
|
|
30
27
|
export function getCachedResearch(hash) {
|
|
31
28
|
if (!hash)
|
|
32
29
|
return '';
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return memoryCache[hash] || '';
|
|
37
|
-
}
|
|
30
|
+
const cached = memoryCache.get(hash);
|
|
31
|
+
if (cached !== undefined)
|
|
32
|
+
return cached;
|
|
38
33
|
const researchFile = outputPath('research', `${hash}.md`);
|
|
39
34
|
if (!existsSync(researchFile))
|
|
40
35
|
return '';
|
|
41
36
|
const stats = statSync(researchFile);
|
|
42
|
-
if (now - stats.mtimeMs > CACHE_TTL_MS)
|
|
37
|
+
if (Date.now() - stats.mtimeMs > CACHE_TTL_MS)
|
|
43
38
|
return '';
|
|
44
|
-
const
|
|
45
|
-
memoryCache
|
|
46
|
-
|
|
47
|
-
return cached;
|
|
39
|
+
const fromDisk = readFileSync(researchFile, 'utf8');
|
|
40
|
+
memoryCache.set(hash, fromDisk);
|
|
41
|
+
return fromDisk;
|
|
48
42
|
}
|
|
49
43
|
export function getPreviousResearch(hash) {
|
|
50
44
|
if (!hash)
|
|
@@ -60,8 +54,7 @@ export function saveResearch(hash, text, combinedHtml) {
|
|
|
60
54
|
if (!existsSync(researchDir))
|
|
61
55
|
mkdirSync(researchDir, { recursive: true });
|
|
62
56
|
writeFileSync(researchFile, text);
|
|
63
|
-
memoryCache
|
|
64
|
-
memoryCacheTimestamps[hash] = Date.now();
|
|
57
|
+
memoryCache.set(hash, text);
|
|
65
58
|
debugLog(`Research saved to ${researchFile}`);
|
|
66
59
|
if (combinedHtml) {
|
|
67
60
|
const statesDir = getStatesDir();
|