explorbot 0.1.32-beta.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -19
- package/bin/explorbot-cli.ts +33 -10
- package/boat/api-tester/src/config.ts +45 -3
- package/boat/doc-collector/src/ai/documentarian.ts +26 -28
- package/boat/doc-collector/src/ai/tools.ts +74 -16
- package/boat/doc-collector/src/cli.ts +1 -6
- package/boat/doc-collector/src/docbot.ts +30 -9
- package/boat/doc-collector/src/docs-renderer.ts +19 -24
- package/boat/doc-collector/src/screenshots.ts +34 -2
- package/boat/doc-collector/src/state-diagram.ts +281 -0
- package/dist/bin/explorbot-cli.js +29 -9
- package/dist/boat/api-tester/src/config.js +43 -4
- package/dist/boat/doc-collector/src/ai/documentarian.js +19 -7
- package/dist/boat/doc-collector/src/ai/tools.js +53 -16
- package/dist/boat/doc-collector/src/cli.js +1 -5
- package/dist/boat/doc-collector/src/docbot.js +27 -8
- package/dist/boat/doc-collector/src/docs-renderer.js +18 -3
- package/dist/boat/doc-collector/src/screenshots.js +30 -1
- package/dist/boat/doc-collector/src/state-diagram.js +200 -0
- package/dist/models.json +30 -0
- package/dist/package.json +23 -3
- package/dist/src/action-result.d.ts +8 -10
- package/dist/src/action-result.js +72 -164
- package/dist/src/action.d.ts +13 -12
- package/dist/src/action.js +37 -113
- package/dist/src/ai/agent.d.ts +17 -0
- package/dist/src/ai/captain/idle-mode.js +1 -1
- package/dist/src/ai/captain/test-mode.js +1 -1
- package/dist/src/ai/captain/web-mode.js +18 -29
- package/dist/src/ai/captain.d.ts +1 -3
- package/dist/src/ai/captain.js +17 -45
- package/dist/src/ai/conversation.d.ts +2 -5
- package/dist/src/ai/conversation.js +9 -28
- package/dist/src/ai/driller.d.ts +2 -17
- package/dist/src/ai/driller.js +26 -50
- 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 +4 -3
- package/dist/src/ai/historian/playwright.d.ts +4 -1
- package/dist/src/ai/historian/playwright.js +4 -3
- package/dist/src/ai/historian/screencast.d.ts +2 -1
- package/dist/src/ai/historian/screencast.js +2 -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 +10 -8
- package/dist/src/ai/historian.js +1 -5
- package/dist/src/ai/navigator.d.ts +8 -6
- package/dist/src/ai/navigator.js +34 -65
- package/dist/src/ai/pilot.d.ts +13 -8
- package/dist/src/ai/pilot.js +53 -59
- package/dist/src/ai/planner.d.ts +5 -4
- package/dist/src/ai/planner.js +66 -52
- package/dist/src/ai/provider.d.ts +4 -1
- package/dist/src/ai/provider.js +77 -137
- package/dist/src/ai/quartermaster.d.ts +22 -22
- package/dist/src/ai/quartermaster.js +8 -14
- package/dist/src/ai/rerunner.d.ts +3 -13
- package/dist/src/ai/rerunner.js +23 -44
- package/dist/src/ai/researcher/cache.js +11 -18
- package/dist/src/ai/researcher/coordinates.js +4 -3
- package/dist/src/ai/researcher/deep-analysis.d.ts +2 -0
- package/dist/src/ai/researcher/deep-analysis.js +8 -11
- package/dist/src/ai/researcher/locators.d.ts +9 -2
- package/dist/src/ai/researcher/locators.js +65 -8
- 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.d.ts +2 -0
- package/dist/src/ai/researcher/sections.js +7 -3
- package/dist/src/ai/researcher.js +40 -106
- 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 +19 -6
- package/dist/src/ai/task-agent.js +37 -33
- package/dist/src/ai/tester.d.ts +6 -14
- package/dist/src/ai/tester.js +46 -111
- package/dist/src/ai/tools.d.ts +26 -14
- package/dist/src/ai/tools.js +138 -132
- package/dist/src/command-handler.js +1 -1
- package/dist/src/commands/compact-command.d.ts +1 -1
- package/dist/src/commands/compact-command.js +6 -6
- package/dist/src/commands/context-aria-command.js +1 -1
- package/dist/src/commands/context-command.js +8 -6
- package/dist/src/commands/context-data-command.js +2 -2
- package/dist/src/commands/context-experience-command.js +2 -2
- package/dist/src/commands/context-html-command.js +2 -2
- package/dist/src/commands/context-knowledge-command.js +2 -2
- package/dist/src/commands/drill-command.js +1 -1
- package/dist/src/commands/experience-command.js +1 -1
- package/dist/src/commands/explore-command.js +4 -3
- package/dist/src/commands/freesail-command.js +2 -2
- package/dist/src/commands/knows-command.js +1 -1
- package/dist/src/commands/learn-command.js +5 -3
- package/dist/src/commands/path-command.js +1 -1
- package/dist/src/commands/research-command.js +1 -1
- package/dist/src/commands/test-command.js +1 -1
- package/dist/src/components/AddKnowledge.d.ts +2 -0
- package/dist/src/components/AddKnowledge.js +3 -7
- package/dist/src/components/App.js +3 -3
- package/dist/src/config.d.ts +19 -0
- package/dist/src/config.js +156 -10
- package/dist/src/experience-tracker.d.ts +11 -6
- package/dist/src/experience-tracker.js +82 -155
- package/dist/src/explorbot.d.ts +21 -13
- package/dist/src/explorbot.js +84 -80
- package/dist/src/explorer.d.ts +66 -102
- package/dist/src/explorer.js +332 -623
- package/dist/src/knowledge-tracker.d.ts +2 -2
- package/dist/src/knowledge-tracker.js +38 -45
- package/dist/src/state-manager.d.ts +10 -30
- package/dist/src/state-manager.js +11 -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/hooks-runner.js +2 -4
- 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/test-files.js +1 -1
- package/dist/src/utils/web-annotate.d.ts +5 -0
- package/dist/src/utils/web-annotate.js +58 -0
- package/dist/src/utils/web-eidx.d.ts +2 -0
- package/dist/src/utils/web-eidx.js +20 -0
- package/dist/src/utils/web-element.d.ts +3 -1
- package/dist/src/utils/web-element.js +32 -0
- package/dist/src/utils/web-sandbox.d.ts +4 -0
- package/dist/src/utils/web-sandbox.js +37 -0
- package/models.json +30 -0
- package/package.json +23 -3
- package/src/action-result.ts +70 -173
- package/src/action.ts +38 -132
- package/src/ai/agent.ts +20 -0
- package/src/ai/captain/idle-mode.ts +1 -1
- package/src/ai/captain/test-mode.ts +1 -1
- package/src/ai/captain/web-mode.ts +18 -30
- package/src/ai/captain.ts +17 -42
- package/src/ai/conversation.ts +9 -32
- package/src/ai/driller.ts +53 -83
- 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 +5 -3
- package/src/ai/historian/playwright.ts +7 -4
- package/src/ai/historian/screencast.ts +4 -3
- package/src/ai/historian/utils.ts +0 -1
- package/src/ai/historian.ts +6 -8
- package/src/ai/navigator.ts +39 -70
- package/src/ai/pilot.ts +59 -61
- package/src/ai/planner.ts +76 -62
- package/src/ai/provider.ts +85 -146
- package/src/ai/quartermaster.ts +39 -45
- package/src/ai/rerunner.ts +27 -50
- package/src/ai/researcher/cache.ts +10 -16
- package/src/ai/researcher/coordinates.ts +4 -3
- package/src/ai/researcher/deep-analysis.ts +10 -11
- package/src/ai/researcher/locators.ts +66 -9
- package/src/ai/researcher/parser.ts +1 -1
- package/src/ai/researcher/research-result.ts +0 -18
- package/src/ai/researcher/sections.ts +9 -3
- package/src/ai/researcher.ts +42 -119
- package/src/ai/rules.ts +24 -13
- package/src/ai/session-analyst.ts +8 -5
- package/src/ai/task-agent.ts +45 -38
- package/src/ai/tester.ts +51 -129
- package/src/ai/tools.ts +145 -153
- package/src/command-handler.ts +1 -1
- package/src/commands/compact-command.ts +7 -8
- package/src/commands/context-aria-command.ts +1 -1
- package/src/commands/context-command.ts +8 -6
- package/src/commands/context-data-command.ts +2 -2
- package/src/commands/context-experience-command.ts +2 -2
- package/src/commands/context-html-command.ts +2 -2
- package/src/commands/context-knowledge-command.ts +2 -2
- package/src/commands/drill-command.ts +1 -1
- package/src/commands/experience-command.ts +1 -1
- package/src/commands/explore-command.ts +4 -3
- package/src/commands/freesail-command.ts +2 -2
- package/src/commands/knows-command.ts +1 -1
- package/src/commands/learn-command.ts +5 -3
- package/src/commands/path-command.ts +1 -1
- package/src/commands/research-command.ts +1 -1
- package/src/commands/test-command.ts +1 -1
- package/src/components/AddKnowledge.tsx +5 -7
- package/src/components/App.tsx +3 -3
- package/src/config.ts +185 -11
- package/src/experience-tracker.ts +89 -152
- package/src/explorbot.ts +88 -83
- package/src/explorer.ts +402 -693
- package/src/knowledge-tracker.ts +42 -49
- package/src/state-manager.ts +20 -155
- 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/hooks-runner.ts +2 -4
- 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/test-files.ts +1 -1
- package/src/utils/web-annotate.ts +64 -0
- package/src/utils/web-eidx.ts +21 -0
- package/src/utils/web-element.ts +34 -0
- package/src/utils/web-sandbox.ts +43 -0
- package/dist/boat/api-tester/example/apibot.config.js +0 -30
package/src/ai/planner.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { ActionResult } from '../action-result.ts';
|
|
4
4
|
import { setActivity } from '../activity.ts';
|
|
5
5
|
import { ConfigParser } from '../config.ts';
|
|
6
|
-
import { ExperienceTracker } from '../experience-tracker.ts';
|
|
6
|
+
import type { ExperienceTracker } from '../experience-tracker.ts';
|
|
7
7
|
import type Explorer from '../explorer.ts';
|
|
8
8
|
import { Observability } from '../observability.ts';
|
|
9
9
|
import type { StateManager } from '../state-manager.js';
|
|
@@ -14,14 +14,14 @@ import { createDebug, tag } from '../utils/logger.js';
|
|
|
14
14
|
import { jsonToTable } from '../utils/markdown-parser.ts';
|
|
15
15
|
import { mdq } from '../utils/markdown-query.js';
|
|
16
16
|
import { planToCompactAiContext } from '../utils/test-plan-markdown.ts';
|
|
17
|
-
import type { Agent } from './agent.js';
|
|
17
|
+
import type { Agent, AgentDeps } from './agent.js';
|
|
18
18
|
import { Conversation } from './conversation.ts';
|
|
19
19
|
import type { Fisherman } from './fisherman.ts';
|
|
20
20
|
import { WithSessionDedup } from './planner/session-dedup.ts';
|
|
21
|
-
import { getActiveStyle
|
|
21
|
+
import { getActiveStyle } from './planner/styles.ts';
|
|
22
22
|
import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan } from './planner/subpages.ts';
|
|
23
23
|
import type { Provider } from './provider.js';
|
|
24
|
-
import { POSSIBLE_SECTIONS, Researcher } from './researcher.ts';
|
|
24
|
+
import { POSSIBLE_SECTIONS, type Researcher } from './researcher.ts';
|
|
25
25
|
import { findSimilarStateHash } from './researcher/cache.ts';
|
|
26
26
|
import { hasFocusedSection } from './researcher/focus.ts';
|
|
27
27
|
import { capabilityGroundingRule, dataProtectionRules, fileUploadRule } from './rules.ts';
|
|
@@ -63,13 +63,13 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
63
63
|
researcher: Researcher;
|
|
64
64
|
private fisherman: Fisherman | null = null;
|
|
65
65
|
|
|
66
|
-
constructor(
|
|
66
|
+
constructor(deps: AgentDeps, researcher: Researcher) {
|
|
67
67
|
super();
|
|
68
|
-
this.explorer = explorer;
|
|
69
|
-
this.provider =
|
|
70
|
-
this.researcher =
|
|
71
|
-
this.stateManager =
|
|
72
|
-
this.experienceTracker =
|
|
68
|
+
this.explorer = deps.explorer;
|
|
69
|
+
this.provider = deps.ai;
|
|
70
|
+
this.researcher = researcher;
|
|
71
|
+
this.stateManager = deps.stateManager;
|
|
72
|
+
this.experienceTracker = deps.stateManager.getExperienceTracker();
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
setFisherman(fisherman: Fisherman): void {
|
|
@@ -98,7 +98,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
98
98
|
You are ISTQB certified senior manual QA planning exploratory testing session of a web application.
|
|
99
99
|
Each test scenario must complete a meaningful user workflow — not just open a UI element and verify it exists.
|
|
100
100
|
Bad: "Open the Help panel" — just opens something.
|
|
101
|
-
Good: "Create a new
|
|
101
|
+
Good: "Create a new post and verify it appears in the list" — completes a business action.
|
|
102
102
|
</role>
|
|
103
103
|
<task>
|
|
104
104
|
List possible testing scenarios for the web page.
|
|
@@ -171,6 +171,9 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
171
171
|
|
|
172
172
|
const tags = ['planner'];
|
|
173
173
|
if (style) tags.push(style);
|
|
174
|
+
|
|
175
|
+
const existingTestScenarios = this.getExistingTestFileScenarios(state.url);
|
|
176
|
+
|
|
174
177
|
const result = await Observability.run(`planner: ${state.url}`, { tags, sessionId: state.url }, async () => {
|
|
175
178
|
const actionResult = ActionResult.fromState(state);
|
|
176
179
|
const conversation = await this.buildConversation(actionResult, style, parentPlan, feature);
|
|
@@ -211,7 +214,6 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
211
214
|
const defaultStartUrl = this.getDefaultStartUrl(state);
|
|
212
215
|
if (parentPlan) this.currentPlan.parentPlan = parentPlan;
|
|
213
216
|
const allPreviousScenarios = this.getPreviousSessionScenarios();
|
|
214
|
-
const existingTestScenarios = this.getExistingTestFileScenarios(state.url);
|
|
215
217
|
for (const s of existingTestScenarios) allPreviousScenarios.add(s);
|
|
216
218
|
for (const t of tests) {
|
|
217
219
|
if (allPreviousScenarios.has(t.scenario.toLowerCase())) continue;
|
|
@@ -266,6 +268,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
266
268
|
}
|
|
267
269
|
|
|
268
270
|
private getExistingTestFileScenarios(currentUrl?: string): Set<string> {
|
|
271
|
+
this.lastSuite = null;
|
|
269
272
|
if (!currentUrl) return new Set<string>();
|
|
270
273
|
try {
|
|
271
274
|
this.lastSuite = new Suite(currentUrl);
|
|
@@ -277,29 +280,36 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
277
280
|
}
|
|
278
281
|
|
|
279
282
|
private cleanExperienceFlows(text: string): string | null {
|
|
280
|
-
const seenTitles = new Set<string>();
|
|
281
283
|
let result = text;
|
|
284
|
+
const seenTitles = new Set<string>();
|
|
285
|
+
for (const selector of ['section2', 'section3']) {
|
|
286
|
+
result = mdq(result)
|
|
287
|
+
.query(selector)
|
|
288
|
+
.replaceEach((section) => {
|
|
289
|
+
const heading = section.query('heading').text().trim();
|
|
290
|
+
const withoutHeadings = mdq(section.text()).query('heading').replace('');
|
|
291
|
+
const body = mdq(withoutHeadings).query('hr').replace('').trim();
|
|
292
|
+
if (body && !seenTitles.has(heading)) {
|
|
293
|
+
seenTitles.add(heading);
|
|
294
|
+
return section.text();
|
|
295
|
+
}
|
|
296
|
+
return '';
|
|
297
|
+
});
|
|
298
|
+
}
|
|
282
299
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
.query(
|
|
287
|
-
.
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
const blockquotes = section.query('blockquote').each();
|
|
298
|
-
if (blockquotes.length <= 10) continue;
|
|
299
|
-
for (const bq of blockquotes.slice(10)) {
|
|
300
|
-
result = result.replace(bq.text(), '');
|
|
301
|
-
}
|
|
302
|
-
result = result.replace(section.text().trim(), `${section.text().trim()}\n> ... and ${blockquotes.length - 10} more discoveries`);
|
|
300
|
+
const trimmedTitles = new Set<string>();
|
|
301
|
+
for (const selector of ['section2', 'section3']) {
|
|
302
|
+
result = mdq(result)
|
|
303
|
+
.query(selector)
|
|
304
|
+
.replaceEach((section) => {
|
|
305
|
+
const heading = section.query('heading').text().trim();
|
|
306
|
+
if (trimmedTitles.has(heading)) return section.text();
|
|
307
|
+
const count = section.query('blockquote').count();
|
|
308
|
+
if (count <= 10) return section.text();
|
|
309
|
+
const kept = mdq(section.text()).query('blockquote[10:]').replace('');
|
|
310
|
+
trimmedTitles.add(heading);
|
|
311
|
+
return `${kept.trimEnd()}\n> ... and ${count - 10} more discoveries\n`;
|
|
312
|
+
});
|
|
303
313
|
}
|
|
304
314
|
|
|
305
315
|
return result.trim() || null;
|
|
@@ -338,6 +348,7 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
338
348
|
If there are subpages (pages with same URL path) plan testing of those subpages as well
|
|
339
349
|
If you plan to test CRUD operations, plan them in correct order: create, read, update.
|
|
340
350
|
Do not invent specific route names, success messages, validation texts, badge counts, or welcome messages unless they are visible in research, visited pages, or prior observed flows.
|
|
351
|
+
When validation placement or wording was not observed, require feedback associated with the invalid input without inventing a specific location or message.
|
|
341
352
|
If exact wording is unknown, describe the expected result generically, for example "an authentication error is shown" or "the user stays on the login page" instead of guessing the literal text.
|
|
342
353
|
If exact redirect destination is unknown, describe the destination by visible page identity, for example "the dashboard page opens" or "the current workspace home page opens" instead of inventing a URL slug.
|
|
343
354
|
Only propose scenarios whose prerequisites are evident from page research, visited pages, or API data preparation context.
|
|
@@ -363,31 +374,26 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
363
374
|
${dataProtectionRules}
|
|
364
375
|
${fileUploadRule}
|
|
365
376
|
</rules>
|
|
366
|
-
|
|
367
|
-
${this.buildApproach(style)}
|
|
368
|
-
|
|
369
|
-
<context>
|
|
370
|
-
URL: ${state.url || 'Unknown'}
|
|
371
|
-
Title: ${state.title || 'Unknown'}
|
|
372
|
-
</context>
|
|
373
377
|
`;
|
|
374
378
|
|
|
375
379
|
conversation.addUserText(planningPrompt);
|
|
380
|
+
conversation.addUserText(this.getTasksMessage());
|
|
376
381
|
const currentState = this.stateManager.getCurrentState();
|
|
377
382
|
const research = await this.researcher.research(currentState || state, {
|
|
378
383
|
deep: true,
|
|
379
384
|
});
|
|
380
385
|
let plannerResearch = mdq(research).query('code').replace('');
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
386
|
+
plannerResearch = mdq(plannerResearch)
|
|
387
|
+
.query('table')
|
|
388
|
+
.replaceEach((table) => {
|
|
389
|
+
const rows = table.toJson();
|
|
390
|
+
if (rows.length === 0 || !rows[0].Element) return table.text();
|
|
391
|
+
const elementWithType = rows.map((r) => ({
|
|
392
|
+
Element: r.Element,
|
|
393
|
+
Type: r.Type || '',
|
|
394
|
+
}));
|
|
395
|
+
return jsonToTable(elementWithType, ['Element', 'Type']);
|
|
396
|
+
});
|
|
391
397
|
|
|
392
398
|
const hasFocusedOverlay = hasFocusedSection(plannerResearch);
|
|
393
399
|
const focusNote = hasFocusedOverlay ? "IMPORTANT: One section is marked as **Focused** — this is the user's current focus area. Concentrate testing on the Focused section FIRST — test all interactions inside it before planning tests for the rest of the page." : '';
|
|
@@ -405,6 +411,15 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
405
411
|
</page_research>
|
|
406
412
|
`);
|
|
407
413
|
|
|
414
|
+
conversation.addUserText(dedent`
|
|
415
|
+
${this.buildApproach(style)}
|
|
416
|
+
|
|
417
|
+
<context>
|
|
418
|
+
URL: ${state.url || 'Unknown'}
|
|
419
|
+
Title: ${state.title || 'Unknown'}
|
|
420
|
+
</context>
|
|
421
|
+
`);
|
|
422
|
+
|
|
408
423
|
if (this.fisherman) {
|
|
409
424
|
await this.fisherman.ensureReady(state.url);
|
|
410
425
|
if (this.fisherman.isAvailable()) {
|
|
@@ -556,8 +571,11 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
556
571
|
`);
|
|
557
572
|
}
|
|
558
573
|
|
|
559
|
-
|
|
560
|
-
|
|
574
|
+
return conversation;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
private getTasksMessage(): string {
|
|
578
|
+
return dedent`
|
|
561
579
|
<task>
|
|
562
580
|
Provide testing scenarios as structured data with the following requirements:
|
|
563
581
|
1. Create a short, descriptive plan name that summarizes what will be tested (e.g., "User Authentication Testing", "Product Catalog Navigation", "Form Validation Tests")
|
|
@@ -576,17 +594,17 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
576
594
|
|
|
577
595
|
STEPS - actionable commands:
|
|
578
596
|
- Each step should be a specific action to perform
|
|
579
|
-
- Good: "Click Create
|
|
580
|
-
- Good: "Click Star icon on '
|
|
597
|
+
- Good: "Click Create Post button", "Enter 'My New Post' as post title", "Click Save button"
|
|
598
|
+
- Good: "Click Star icon on 'Quarterly Report' item"
|
|
581
599
|
- Bad: "Open the dropdown menu" (opening is not a goal, it's a means)
|
|
582
600
|
- Bad: "Verify modal appears" (verification belongs in expected outcomes, not steps)
|
|
583
601
|
- Steps should form a complete workflow, not stop at opening a UI element
|
|
584
602
|
- NEVER split a workflow across two tests. One test = one complete action + its verification
|
|
585
603
|
|
|
586
604
|
EXPECTED OUTCOMES - verifiable results:
|
|
587
|
-
- Good: "New
|
|
588
|
-
- Good: "
|
|
589
|
-
- Good: "Success message '
|
|
605
|
+
- Good: "New post 'My New Post' appears in the posts list"
|
|
606
|
+
- Good: "Item appears under Starred filter tab"
|
|
607
|
+
- Good: "Success message 'Post created' is displayed"
|
|
590
608
|
- Good when wording is unknown: "An authentication error is displayed"
|
|
591
609
|
- Good when route is unknown: "The workspace home page is displayed"
|
|
592
610
|
- Bad: "Modal is displayed" (just verifying existence, no business value)
|
|
@@ -603,12 +621,8 @@ export class Planner extends PlannerBase implements Agent {
|
|
|
603
621
|
- Do not wrap text in ** or * quotes, ( or ) brackets.
|
|
604
622
|
- Avoid using emojis or special characters.
|
|
605
623
|
7. Only tests that can be tested from web UI should be proposed.
|
|
606
|
-
|
|
624
|
+
8. For a fresh plan propose at least ${this.MIN_TASKS} tests; when expanding an existing plan return ONLY new scenarios not in the tested list, and an empty array if none remain.
|
|
607
625
|
</task>
|
|
608
626
|
`;
|
|
609
|
-
|
|
610
|
-
conversation.addUserText(tasksMessage);
|
|
611
|
-
|
|
612
|
-
return conversation;
|
|
613
627
|
}
|
|
614
628
|
}
|
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) {
|
|
@@ -90,7 +97,6 @@ export class Provider {
|
|
|
90
97
|
await generateText({
|
|
91
98
|
model: this.config.model,
|
|
92
99
|
prompt: 'hi',
|
|
93
|
-
maxOutputTokens: 1,
|
|
94
100
|
});
|
|
95
101
|
} catch (error: any) {
|
|
96
102
|
throw new AiError(`AI connection failed: ${error.message}`);
|
|
@@ -138,12 +144,12 @@ export class Provider {
|
|
|
138
144
|
return parts.length > 0 ? parts.join('\n\n') : undefined;
|
|
139
145
|
}
|
|
140
146
|
|
|
141
|
-
getProviderOptionsForAgent(agentName: string): Record<string, any> | undefined {
|
|
147
|
+
private getProviderOptionsForAgent(agentName: string): Record<string, any> | undefined {
|
|
142
148
|
const agentConfig = this.config.agents?.[agentName as keyof typeof this.config.agents];
|
|
143
149
|
return agentConfig?.providerOptions;
|
|
144
150
|
}
|
|
145
151
|
|
|
146
|
-
getReasoningForAgent(agentName?: string): string | undefined {
|
|
152
|
+
private getReasoningForAgent(agentName?: string): string | undefined {
|
|
147
153
|
if (!agentName) return undefined;
|
|
148
154
|
const agentConfig = this.config.agents?.[agentName as keyof typeof this.config.agents];
|
|
149
155
|
return agentConfig?.reasoning;
|
|
@@ -172,6 +178,50 @@ export class Provider {
|
|
|
172
178
|
if (reasoning) config.reasoning ??= reasoning;
|
|
173
179
|
}
|
|
174
180
|
|
|
181
|
+
private buildGenerateConfig(defaults: Record<string, any>, overrides: Record<string, any>, options: any): Record<string, any> {
|
|
182
|
+
const telemetry = this.getTelemetry(options);
|
|
183
|
+
const config = this.mergeProviderOptions(
|
|
184
|
+
{
|
|
185
|
+
...defaults,
|
|
186
|
+
allowSystemInMessages: true,
|
|
187
|
+
...(this.config.config || {}),
|
|
188
|
+
...options,
|
|
189
|
+
...overrides,
|
|
190
|
+
},
|
|
191
|
+
options.agentName
|
|
192
|
+
);
|
|
193
|
+
this.finalizeConfig(config, options, telemetry);
|
|
194
|
+
return config;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private recordUsage(agentName: string, modelName: string, usage: any): void {
|
|
198
|
+
if (!usage) return;
|
|
199
|
+
Stats.recordTokens(agentName, modelName, {
|
|
200
|
+
input: usage.inputTokens ?? usage.promptTokens ?? 0,
|
|
201
|
+
output: usage.outputTokens ?? usage.completionTokens ?? 0,
|
|
202
|
+
total: usage.totalTokens ?? 0,
|
|
203
|
+
cached: extractCachedTokens(usage),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private async raceWithIdleTimeout<T>(fn: (signal: AbortSignal) => Promise<T>, timeoutMs: number): Promise<T> {
|
|
208
|
+
const cancel = { cancelled: false };
|
|
209
|
+
const controller = new AbortController();
|
|
210
|
+
const combinedSignal = combinedAbortSignal(controller);
|
|
211
|
+
try {
|
|
212
|
+
return await Promise.race([fn(combinedSignal), abortAfterIdle(timeoutMs, cancel, controller)]);
|
|
213
|
+
} finally {
|
|
214
|
+
cancel.cancelled = true;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private async recoverFromContextLength(error: any, messages: ModelMessage[], options: any, retry: (messages: ModelMessage[], options: any) => Promise<any>): Promise<any> {
|
|
219
|
+
const reduced = this.tryReduceMessages(messages, options._contextRetryLevel || 0);
|
|
220
|
+
if (!reduced) throw new ContextLengthError(error.message || error.toString());
|
|
221
|
+
tag('warning').log('Context length exceeded, retrying with reduced messages...');
|
|
222
|
+
return retry(reduced.messages, { ...options, _contextRetryLevel: reduced.nextLevel });
|
|
223
|
+
}
|
|
224
|
+
|
|
175
225
|
private initLangfuse() {
|
|
176
226
|
const langfuseConfig = this.config.langfuse;
|
|
177
227
|
const publicKey = langfuseConfig?.publicKey || process.env.LANGFUSE_PUBLIC_KEY;
|
|
@@ -207,20 +257,25 @@ export class Provider {
|
|
|
207
257
|
|
|
208
258
|
const runTelemetry = Observability.getTelemetry();
|
|
209
259
|
|
|
210
|
-
|
|
260
|
+
let optionTelemetry = options.telemetry;
|
|
261
|
+
if (options.telemetryFunctionId && !optionTelemetry?.functionId) {
|
|
262
|
+
optionTelemetry = { ...optionTelemetry, functionId: options.telemetryFunctionId };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (!optionTelemetry) {
|
|
211
266
|
return runTelemetry;
|
|
212
267
|
}
|
|
213
268
|
|
|
214
269
|
if (!runTelemetry) {
|
|
215
|
-
return
|
|
270
|
+
return optionTelemetry;
|
|
216
271
|
}
|
|
217
272
|
|
|
218
273
|
return {
|
|
219
274
|
...runTelemetry,
|
|
220
|
-
...
|
|
275
|
+
...optionTelemetry,
|
|
221
276
|
metadata: {
|
|
222
277
|
...runTelemetry.metadata,
|
|
223
|
-
...
|
|
278
|
+
...optionTelemetry.metadata,
|
|
224
279
|
},
|
|
225
280
|
};
|
|
226
281
|
}
|
|
@@ -254,12 +309,8 @@ export class Provider {
|
|
|
254
309
|
const toolCalls = response.toolCalls || [];
|
|
255
310
|
const toolResults = response.toolResults || [];
|
|
256
311
|
|
|
257
|
-
const
|
|
258
|
-
|
|
259
|
-
input: call.input,
|
|
260
|
-
output: toolResults[index]?.output,
|
|
261
|
-
wasSuccessful: toolResults[index]?.output?.success || false,
|
|
262
|
-
}));
|
|
312
|
+
const resultsById = new Map(toolResults.map((r: any) => [r.toolCallId, r]));
|
|
313
|
+
const toolExecutions = toolCalls.map((call: any) => toToolExecution(call.toolName || '', call.input, resultsById.get(call.toolCallId)?.output));
|
|
263
314
|
|
|
264
315
|
return { conversation, response, toolExecutions };
|
|
265
316
|
}
|
|
@@ -269,19 +320,7 @@ export class Provider {
|
|
|
269
320
|
setActivity(`🤖 Asking ${modelName}`, 'ai');
|
|
270
321
|
promptLog(`Using model: ${modelName}`);
|
|
271
322
|
|
|
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);
|
|
323
|
+
const config = this.buildGenerateConfig({ maxOutputTokens: 16384 }, { model, abortSignal: executionController.getAbortSignal() }, options);
|
|
285
324
|
|
|
286
325
|
promptLog(messages[messages.length - 1].content);
|
|
287
326
|
try {
|
|
@@ -303,14 +342,7 @@ export class Provider {
|
|
|
303
342
|
clearActivity();
|
|
304
343
|
responseLog(response.text);
|
|
305
344
|
|
|
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
|
-
}
|
|
345
|
+
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
314
346
|
|
|
315
347
|
return response;
|
|
316
348
|
} catch (error: any) {
|
|
@@ -318,12 +350,7 @@ export class Provider {
|
|
|
318
350
|
if (error?.name === 'AbortError') throw error;
|
|
319
351
|
if (error instanceof ContextLengthError) throw error;
|
|
320
352
|
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());
|
|
353
|
+
return this.recoverFromContextLength(error, messages, options, (m, o) => this.chat(m, model, o));
|
|
327
354
|
}
|
|
328
355
|
const message = error.message || error.toString();
|
|
329
356
|
if (message !== 'No response text from AI') {
|
|
@@ -343,50 +370,19 @@ export class Provider {
|
|
|
343
370
|
promptLog('Available tools:', toolNames);
|
|
344
371
|
promptLog(messages[messages.length - 1].content);
|
|
345
372
|
|
|
346
|
-
const telemetry = this.getTelemetry(options);
|
|
347
373
|
const maxRoundtrips = options.maxToolRoundtrips ?? 5;
|
|
348
374
|
const extraStop = options.stopWhen;
|
|
349
375
|
const stopConditions: any[] = [isStepCount(maxRoundtrips)];
|
|
350
376
|
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);
|
|
377
|
+
const config = this.buildGenerateConfig({ tools, maxOutputTokens: 16384, toolChoice: 'auto' }, { stopWhen: stopConditions, model }, options);
|
|
367
378
|
try {
|
|
368
379
|
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;
|
|
380
|
+
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
|
|
381
|
+
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
382
|
+
if (!result.text && !hasToolCall && result.finishReason === 'length') {
|
|
383
|
+
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
389
384
|
}
|
|
385
|
+
return result;
|
|
390
386
|
}, this.getRetryOptions(options));
|
|
391
387
|
|
|
392
388
|
clearActivity();
|
|
@@ -401,14 +397,7 @@ export class Provider {
|
|
|
401
397
|
|
|
402
398
|
responseLog(response.text);
|
|
403
399
|
|
|
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
|
-
}
|
|
400
|
+
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
412
401
|
|
|
413
402
|
return response;
|
|
414
403
|
} catch (error: any) {
|
|
@@ -419,12 +408,7 @@ export class Provider {
|
|
|
419
408
|
if (error?.name === 'AbortError') throw error;
|
|
420
409
|
if (error instanceof ContextLengthError) throw error;
|
|
421
410
|
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());
|
|
411
|
+
return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateWithTools(m, model, tools, o));
|
|
428
412
|
}
|
|
429
413
|
if (error.constructor?.name === 'AI_APICallError') {
|
|
430
414
|
responseLog(error.message);
|
|
@@ -440,52 +424,18 @@ export class Provider {
|
|
|
440
424
|
setActivity(`🤖 Asking ${modelName} for structured output`, 'ai');
|
|
441
425
|
promptLog(`Using model: ${modelName}`);
|
|
442
426
|
|
|
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);
|
|
427
|
+
const config = this.buildGenerateConfig({ schema }, { model: modelToUse }, options);
|
|
456
428
|
|
|
457
429
|
try {
|
|
458
430
|
promptLog(messages[messages.length - 1].content);
|
|
459
431
|
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
|
-
}
|
|
432
|
+
return (await this.raceWithIdleTimeout((signal) => generateObject({ messages, ...config, abortSignal: signal }), config.timeout || 30000)) as any;
|
|
476
433
|
}, this.getRetryOptions(options));
|
|
477
434
|
|
|
478
435
|
clearActivity();
|
|
479
436
|
responseLog(response.object);
|
|
480
437
|
|
|
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
|
-
}
|
|
438
|
+
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
489
439
|
|
|
490
440
|
return response;
|
|
491
441
|
} catch (error: any) {
|
|
@@ -493,12 +443,7 @@ export class Provider {
|
|
|
493
443
|
if (error?.name === 'AbortError') throw error;
|
|
494
444
|
if (error instanceof ContextLengthError) throw error;
|
|
495
445
|
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());
|
|
446
|
+
return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateObject(m, schema, model, o));
|
|
502
447
|
}
|
|
503
448
|
throw new AiError(error.message || error.toString());
|
|
504
449
|
}
|
|
@@ -506,7 +451,7 @@ export class Provider {
|
|
|
506
451
|
|
|
507
452
|
static isContextLengthError(error: any): boolean {
|
|
508
453
|
const msg = (error?.message || error?.toString() || '').toLowerCase();
|
|
509
|
-
return
|
|
454
|
+
return CONTEXT_LENGTH_PATTERNS.some((p) => msg.includes(p));
|
|
510
455
|
}
|
|
511
456
|
|
|
512
457
|
static trimMessagesForRetry(messages: ModelMessage[]): ModelMessage[] | null {
|
|
@@ -677,13 +622,7 @@ export class Provider {
|
|
|
677
622
|
clearActivity();
|
|
678
623
|
responseLog(response.text);
|
|
679
624
|
|
|
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
|
-
}
|
|
625
|
+
this.recordUsage('vision', this.getModelName(this.config.visionModel), response.usage);
|
|
687
626
|
|
|
688
627
|
return response;
|
|
689
628
|
} catch (error: any) {
|