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.
Files changed (168) hide show
  1. package/README.md +14 -14
  2. package/bin/explorbot-cli.ts +4 -3
  3. package/boat/doc-collector/src/ai/documentarian.ts +22 -27
  4. package/boat/doc-collector/src/ai/tools.ts +67 -10
  5. package/boat/doc-collector/src/docbot.ts +28 -7
  6. package/boat/doc-collector/src/docs-renderer.ts +19 -24
  7. package/boat/doc-collector/src/screenshots.ts +33 -1
  8. package/boat/doc-collector/src/state-diagram.ts +281 -0
  9. package/dist/bin/explorbot-cli.js +3 -3
  10. package/dist/boat/doc-collector/src/ai/documentarian.js +16 -6
  11. package/dist/boat/doc-collector/src/ai/tools.js +47 -10
  12. package/dist/boat/doc-collector/src/docbot.js +25 -6
  13. package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
  14. package/dist/boat/doc-collector/src/screenshots.js +29 -0
  15. package/dist/boat/doc-collector/src/state-diagram.js +200 -0
  16. package/dist/package.json +20 -3
  17. package/dist/src/action-result.d.ts +8 -10
  18. package/dist/src/action-result.js +72 -164
  19. package/dist/src/action.d.ts +3 -10
  20. package/dist/src/action.js +18 -111
  21. package/dist/src/ai/captain/web-mode.js +0 -7
  22. package/dist/src/ai/captain.d.ts +1 -3
  23. package/dist/src/ai/captain.js +4 -19
  24. package/dist/src/ai/conversation.d.ts +2 -5
  25. package/dist/src/ai/conversation.js +9 -28
  26. package/dist/src/ai/driller.d.ts +0 -4
  27. package/dist/src/ai/driller.js +3 -12
  28. package/dist/src/ai/experience-compactor.d.ts +2 -2
  29. package/dist/src/ai/experience-compactor.js +16 -33
  30. package/dist/src/ai/fisherman-tools.js +1 -1
  31. package/dist/src/ai/fisherman.d.ts +0 -1
  32. package/dist/src/ai/fisherman.js +0 -3
  33. package/dist/src/ai/historian/codeceptjs.d.ts +2 -0
  34. package/dist/src/ai/historian/codeceptjs.js +3 -2
  35. package/dist/src/ai/historian/playwright.d.ts +2 -0
  36. package/dist/src/ai/historian/playwright.js +3 -2
  37. package/dist/src/ai/historian/utils.d.ts +0 -1
  38. package/dist/src/ai/historian/utils.js +0 -1
  39. package/dist/src/ai/historian.d.ts +5 -4
  40. package/dist/src/ai/historian.js +1 -5
  41. package/dist/src/ai/navigator.d.ts +3 -5
  42. package/dist/src/ai/navigator.js +11 -46
  43. package/dist/src/ai/pilot.d.ts +1 -3
  44. package/dist/src/ai/pilot.js +6 -17
  45. package/dist/src/ai/planner.d.ts +3 -3
  46. package/dist/src/ai/planner.js +49 -37
  47. package/dist/src/ai/provider.d.ts +4 -1
  48. package/dist/src/ai/provider.js +77 -136
  49. package/dist/src/ai/quartermaster.d.ts +21 -22
  50. package/dist/src/ai/quartermaster.js +4 -10
  51. package/dist/src/ai/rerunner.d.ts +1 -2
  52. package/dist/src/ai/rerunner.js +14 -22
  53. package/dist/src/ai/researcher/cache.js +11 -18
  54. package/dist/src/ai/researcher/deep-analysis.js +3 -6
  55. package/dist/src/ai/researcher/parser.js +1 -1
  56. package/dist/src/ai/researcher/research-result.d.ts +0 -1
  57. package/dist/src/ai/researcher/research-result.js +0 -19
  58. package/dist/src/ai/researcher/sections.js +5 -1
  59. package/dist/src/ai/researcher.js +12 -53
  60. package/dist/src/ai/rules.d.ts +1 -0
  61. package/dist/src/ai/rules.js +22 -15
  62. package/dist/src/ai/session-analyst.js +8 -5
  63. package/dist/src/ai/task-agent.d.ts +1 -1
  64. package/dist/src/ai/task-agent.js +2 -27
  65. package/dist/src/ai/tester.d.ts +0 -2
  66. package/dist/src/ai/tester.js +10 -61
  67. package/dist/src/ai/tools.d.ts +18 -7
  68. package/dist/src/ai/tools.js +119 -101
  69. package/dist/src/commands/compact-command.d.ts +1 -1
  70. package/dist/src/commands/compact-command.js +6 -6
  71. package/dist/src/commands/context-command.js +1 -1
  72. package/dist/src/commands/context-knowledge-command.js +1 -1
  73. package/dist/src/commands/experience-command.js +1 -1
  74. package/dist/src/commands/knows-command.js +1 -1
  75. package/dist/src/commands/learn-command.js +3 -1
  76. package/dist/src/components/AddKnowledge.d.ts +2 -0
  77. package/dist/src/components/AddKnowledge.js +3 -7
  78. package/dist/src/components/App.js +2 -2
  79. package/dist/src/config.d.ts +2 -0
  80. package/dist/src/config.js +12 -0
  81. package/dist/src/experience-tracker.d.ts +11 -6
  82. package/dist/src/experience-tracker.js +81 -154
  83. package/dist/src/explorbot.d.ts +4 -5
  84. package/dist/src/explorbot.js +17 -51
  85. package/dist/src/explorer.d.ts +4 -15
  86. package/dist/src/explorer.js +18 -68
  87. package/dist/src/knowledge-tracker.d.ts +2 -2
  88. package/dist/src/knowledge-tracker.js +38 -45
  89. package/dist/src/state-manager.d.ts +6 -31
  90. package/dist/src/state-manager.js +10 -129
  91. package/dist/src/utils/aria.d.ts +6 -1
  92. package/dist/src/utils/aria.js +4 -1
  93. package/dist/src/utils/cache.d.ts +15 -0
  94. package/dist/src/utils/cache.js +34 -0
  95. package/dist/src/utils/context-formatter.d.ts +6 -0
  96. package/dist/src/utils/context-formatter.js +15 -27
  97. package/dist/src/utils/html.d.ts +5 -0
  98. package/dist/src/utils/html.js +71 -0
  99. package/dist/src/utils/markdown-files.d.ts +10 -0
  100. package/dist/src/utils/markdown-files.js +21 -0
  101. package/dist/src/utils/markdown-query.d.ts +6 -0
  102. package/dist/src/utils/markdown-query.js +14 -1
  103. package/dist/src/utils/page-readiness.d.ts +1 -0
  104. package/dist/src/utils/page-readiness.js +1 -1
  105. package/dist/src/utils/secrets.js +2 -2
  106. package/dist/src/utils/strings.d.ts +2 -0
  107. package/dist/src/utils/strings.js +10 -0
  108. package/dist/src/utils/web-element.d.ts +2 -1
  109. package/dist/src/utils/web-element.js +3 -0
  110. package/dist/src/utils/web-sandbox.d.ts +4 -0
  111. package/dist/src/utils/web-sandbox.js +37 -0
  112. package/package.json +20 -3
  113. package/src/action-result.ts +70 -173
  114. package/src/action.ts +16 -130
  115. package/src/ai/captain/web-mode.ts +0 -7
  116. package/src/ai/captain.ts +5 -17
  117. package/src/ai/conversation.ts +9 -32
  118. package/src/ai/driller.ts +3 -16
  119. package/src/ai/experience-compactor.ts +16 -35
  120. package/src/ai/fisherman-tools.ts +1 -1
  121. package/src/ai/fisherman.ts +0 -4
  122. package/src/ai/historian/codeceptjs.ts +4 -2
  123. package/src/ai/historian/playwright.ts +4 -2
  124. package/src/ai/historian/utils.ts +0 -1
  125. package/src/ai/historian.ts +4 -7
  126. package/src/ai/navigator.ts +13 -49
  127. package/src/ai/pilot.ts +6 -16
  128. package/src/ai/planner.ts +56 -44
  129. package/src/ai/provider.ts +85 -145
  130. package/src/ai/quartermaster.ts +34 -41
  131. package/src/ai/rerunner.ts +17 -21
  132. package/src/ai/researcher/cache.ts +10 -16
  133. package/src/ai/researcher/deep-analysis.ts +3 -6
  134. package/src/ai/researcher/parser.ts +1 -1
  135. package/src/ai/researcher/research-result.ts +0 -18
  136. package/src/ai/researcher/sections.ts +5 -1
  137. package/src/ai/researcher.ts +11 -59
  138. package/src/ai/rules.ts +24 -13
  139. package/src/ai/session-analyst.ts +8 -5
  140. package/src/ai/task-agent.ts +3 -30
  141. package/src/ai/tester.ts +12 -73
  142. package/src/ai/tools.ts +119 -106
  143. package/src/commands/compact-command.ts +7 -8
  144. package/src/commands/context-command.ts +1 -1
  145. package/src/commands/context-knowledge-command.ts +1 -1
  146. package/src/commands/experience-command.ts +1 -1
  147. package/src/commands/knows-command.ts +1 -1
  148. package/src/commands/learn-command.ts +3 -1
  149. package/src/components/AddKnowledge.tsx +5 -7
  150. package/src/components/App.tsx +2 -2
  151. package/src/config.ts +12 -0
  152. package/src/experience-tracker.ts +88 -151
  153. package/src/explorbot.ts +17 -51
  154. package/src/explorer.ts +20 -72
  155. package/src/knowledge-tracker.ts +42 -49
  156. package/src/state-manager.ts +15 -156
  157. package/src/utils/aria.ts +11 -2
  158. package/src/utils/cache.ts +40 -0
  159. package/src/utils/context-formatter.ts +12 -15
  160. package/src/utils/html.ts +79 -0
  161. package/src/utils/markdown-files.ts +30 -0
  162. package/src/utils/markdown-query.ts +15 -1
  163. package/src/utils/page-readiness.ts +1 -1
  164. package/src/utils/secrets.ts +2 -3
  165. package/src/utils/strings.ts +12 -0
  166. package/src/utils/web-element.ts +4 -0
  167. package/src/utils/web-sandbox.ts +43 -0
  168. 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;
@@ -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 rejectAfterIdle(ms, signal, controller) {
32
+ function abortAfterIdle(ms, cancel, controller) {
32
33
  return new Promise((_, reject) => {
33
34
  const tick = () => {
34
- if (signal.cancelled)
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
- if (!options.telemetry) {
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 options.telemetry;
248
+ return optionTelemetry;
199
249
  }
200
250
  return {
201
251
  ...runTelemetry,
202
- ...options.telemetry,
252
+ ...optionTelemetry,
203
253
  metadata: {
204
254
  ...runTelemetry.metadata,
205
- ...options.telemetry.metadata,
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 toolExecutions = toolCalls.map((call, index) => ({
232
- toolName: call.toolName || '',
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 telemetry = this.getTelemetry(options);
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
- if (response.usage) {
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
- const reduced = this.tryReduceMessages(messages, options._contextRetryLevel || 0);
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: _ignoredStopWhen, ...optionsWithoutStop } = options;
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 timeout = config.timeout || 30000;
332
- const cancel = { cancelled: false };
333
- const controller = new AbortController();
334
- const combinedSignal = AbortSignal.any([controller.signal, executionController.getAbortSignal()].filter(Boolean));
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
- if (response.usage) {
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
- const reduced = this.tryReduceMessages(messages, options._contextRetryLevel || 0);
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 telemetry = this.getTelemetry(options);
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
- const timeout = config.timeout || 30000;
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
- if (response.usage) {
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
- const reduced = this.tryReduceMessages(messages, options._contextRetryLevel || 0);
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 Provider.CONTEXT_LENGTH_PATTERNS.some((p) => msg.includes(p));
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
- if (response.usage) {
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 { existsSync, mkdirSync, writeFileSync } from 'node:fs';
1
+ import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import { join } from 'node:path';
3
3
  import { z } from 'zod';
4
- import { ConfigParser } from "../config.js";
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
- const configParser = ConfigParser.getInstance();
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.ensureDirectory();
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>;
@@ -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['explorbot-ai-healer'] = undefined;
266
+ Reflect.deleteProperty(healMod.recipes, 'explorbot-ai-healer');
267
267
  for (const name of Object.keys(this.rerunnerConfig.recipes || {})) {
268
- healMod.recipes[name] = undefined;
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({ test: '', original: failedCode, healed: healedCommand });
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
- for (const key of Object.keys(memoryCache))
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 now = Date.now();
34
- const timestamp = memoryCacheTimestamps[hash];
35
- if (timestamp && now - timestamp <= CACHE_TTL_MS) {
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 cached = readFileSync(researchFile, 'utf8');
45
- memoryCache[hash] = cached;
46
- memoryCacheTimestamps[hash] = now;
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[hash] = text;
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();