explorbot 0.3.0 → 0.3.2
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/bin/explorbot-cli.ts +3 -3
- package/dist/bin/explorbot-cli.js +3 -2
- package/dist/package.json +1 -1
- package/dist/src/action-result.d.ts +3 -0
- package/dist/src/action-result.js +3 -0
- package/dist/src/action.d.ts +1 -0
- package/dist/src/action.js +12 -1
- package/dist/src/ai/conversation.d.ts +1 -0
- package/dist/src/ai/conversation.js +3 -0
- package/dist/src/ai/driller.js +3 -1
- package/dist/src/ai/fisherman.js +1 -1
- package/dist/src/ai/pilot.js +14 -9
- package/dist/src/ai/provider.d.ts +5 -0
- package/dist/src/ai/provider.js +84 -19
- package/dist/src/ai/researcher/deep-analysis.js +2 -2
- package/dist/src/ai/tester.js +22 -6
- package/dist/src/commands/exit-command.js +1 -1
- package/dist/src/commands/explore-command.js +22 -17
- package/dist/src/config.d.ts +2 -1
- package/dist/src/explorbot.js +1 -0
- package/dist/src/state-manager.d.ts +2 -0
- package/dist/src/state-manager.js +3 -3
- package/dist/src/utils/html.d.ts +2 -1
- package/dist/src/utils/html.js +10 -4
- package/dist/src/utils/overlay.d.ts +24 -0
- package/dist/src/utils/overlay.js +43 -0
- package/package.json +1 -1
- package/src/action-result.ts +5 -0
- package/src/action.ts +15 -1
- package/src/ai/conversation.ts +3 -0
- package/src/ai/driller.ts +3 -1
- package/src/ai/fisherman.ts +1 -1
- package/src/ai/pilot.ts +15 -10
- package/src/ai/provider.ts +115 -43
- package/src/ai/researcher/deep-analysis.ts +2 -2
- package/src/ai/tester.ts +20 -7
- package/src/commands/exit-command.ts +1 -1
- package/src/commands/explore-command.ts +17 -14
- package/src/config.ts +2 -1
- package/src/explorbot.ts +1 -0
- package/src/state-manager.ts +4 -3
- package/src/utils/html.ts +13 -4
- package/src/utils/overlay.ts +51 -0
package/bin/explorbot-cli.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { render } from 'ink';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { App } from '../src/components/App.js';
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
|
+
import { flushTelemetry } from '../src/ai/provider.js';
|
|
12
13
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
14
|
import { ExplorBot, type ExplorBotOptions } from '../src/explorbot.js';
|
|
14
15
|
import { remote } from '../src/remote.js';
|
|
@@ -103,9 +104,7 @@ async function startTUI(explorBot: ExplorBot): Promise<void> {
|
|
|
103
104
|
async function showStatsAndExit(code: number): Promise<never> {
|
|
104
105
|
if (remote.isAttached()) {
|
|
105
106
|
await remote.close(code);
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
if (Stats.hasActivity()) {
|
|
107
|
+
} else if (Stats.hasActivity()) {
|
|
109
108
|
await new Promise<void>((resolve) => {
|
|
110
109
|
const { unmount } = render(
|
|
111
110
|
React.createElement(StatusPane, {
|
|
@@ -121,6 +120,7 @@ async function showStatsAndExit(code: number): Promise<never> {
|
|
|
121
120
|
);
|
|
122
121
|
});
|
|
123
122
|
}
|
|
123
|
+
await flushTelemetry();
|
|
124
124
|
process.exit(code);
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -9,6 +9,7 @@ import { render } from 'ink';
|
|
|
9
9
|
import React from 'react';
|
|
10
10
|
import { App } from '../src/components/App.js';
|
|
11
11
|
import { StatusPane } from '../src/components/StatusPane.js';
|
|
12
|
+
import { flushTelemetry } from '../src/ai/provider.js';
|
|
12
13
|
import { ConfigParser, EXPLORBOT_ENV_VARS, PROVIDERS } from '../src/config.js';
|
|
13
14
|
import { ExplorBot } from '../src/explorbot.js';
|
|
14
15
|
import { remote } from '../src/remote.js';
|
|
@@ -78,9 +79,8 @@ async function startTUI(explorBot) {
|
|
|
78
79
|
async function showStatsAndExit(code) {
|
|
79
80
|
if (remote.isAttached()) {
|
|
80
81
|
await remote.close(code);
|
|
81
|
-
process.exit(code);
|
|
82
82
|
}
|
|
83
|
-
if (Stats.hasActivity()) {
|
|
83
|
+
else if (Stats.hasActivity()) {
|
|
84
84
|
await new Promise((resolve) => {
|
|
85
85
|
const { unmount } = render(React.createElement(StatusPane, {
|
|
86
86
|
onComplete: () => {
|
|
@@ -93,6 +93,7 @@ async function showStatsAndExit(code) {
|
|
|
93
93
|
});
|
|
94
94
|
});
|
|
95
95
|
}
|
|
96
|
+
await flushTelemetry();
|
|
96
97
|
process.exit(code);
|
|
97
98
|
}
|
|
98
99
|
addCommonOptions(program.command('start [path]').description('Start web exploration')).action(async (startPath, options) => {
|
package/dist/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { type HtmlConfig } from './config.js';
|
|
|
2
2
|
import type { Link, WebPageState } from './state-manager.js';
|
|
3
3
|
import { TTLCache } from './utils/cache.js';
|
|
4
4
|
import { type HtmlDiffPart, type HtmlDiffResult } from './utils/html-diff.js';
|
|
5
|
+
import { Overlay } from './utils/overlay.js';
|
|
5
6
|
interface ActionResultData extends WebPageState {
|
|
6
7
|
html?: string;
|
|
7
8
|
fullUrl?: string | undefined;
|
|
@@ -28,6 +29,7 @@ interface ActionResultData extends WebPageState {
|
|
|
28
29
|
focusedElement?: FocusedElement | null;
|
|
29
30
|
iframeURL?: string;
|
|
30
31
|
links?: Link[];
|
|
32
|
+
overlayHtml?: string;
|
|
31
33
|
}
|
|
32
34
|
export interface PageDiff {
|
|
33
35
|
urlChanged: boolean;
|
|
@@ -81,6 +83,7 @@ export declare class ActionResult implements ActionResultData {
|
|
|
81
83
|
notes: string[];
|
|
82
84
|
links: Link[];
|
|
83
85
|
verifications?: Record<string, boolean>;
|
|
86
|
+
overlay: Overlay;
|
|
84
87
|
constructor(data: ActionResultData);
|
|
85
88
|
get hash(): string;
|
|
86
89
|
get html(): string;
|
|
@@ -5,6 +5,7 @@ import { TTLCache } from "./utils/cache.js";
|
|
|
5
5
|
import { htmlDiff, liveRegionMessages } from "./utils/html-diff.js";
|
|
6
6
|
import { extractHeadings, extractLinks, extractTargetedHtml, htmlCombinedSnapshot, htmlMinimalUISnapshot, htmlTextSnapshot, minifyHtml } from "./utils/html.js";
|
|
7
7
|
import { createDebug } from "./utils/logger.js";
|
|
8
|
+
import { Overlay } from "./utils/overlay.js";
|
|
8
9
|
import { slugify } from "./utils/strings.js";
|
|
9
10
|
import { extractStatePath, matchesUrl } from "./utils/url-matcher.js";
|
|
10
11
|
const debugLog = createDebug('explorbot:state');
|
|
@@ -37,6 +38,7 @@ export class ActionResult {
|
|
|
37
38
|
notes = [];
|
|
38
39
|
links = [];
|
|
39
40
|
verifications;
|
|
41
|
+
overlay = new Overlay();
|
|
40
42
|
constructor(data) {
|
|
41
43
|
this.id = data.id;
|
|
42
44
|
this.timestamp = data.timestamp ?? new Date();
|
|
@@ -77,6 +79,7 @@ export class ActionResult {
|
|
|
77
79
|
if (data.ariaSnapshot !== undefined) {
|
|
78
80
|
this._ariaSnapshot = data.ariaSnapshot;
|
|
79
81
|
}
|
|
82
|
+
this.overlay = Overlay.resolve(data);
|
|
80
83
|
if (!this.fullUrl && this.url) {
|
|
81
84
|
this.fullUrl = this.url;
|
|
82
85
|
}
|
package/dist/src/action.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ declare class Action {
|
|
|
32
32
|
includeScreenshot?: boolean;
|
|
33
33
|
codeBlock?: string;
|
|
34
34
|
}): Promise<ActionResult>;
|
|
35
|
+
captureOverlayHtml(): Promise<string>;
|
|
35
36
|
captureMainDocumentStatus(): Promise<number | undefined>;
|
|
36
37
|
captureResponses(): () => void;
|
|
37
38
|
recordNetworkCall(request: any, status: number): void;
|
package/dist/src/action.js
CHANGED
|
@@ -8,8 +8,9 @@ import { clearActivity, setActivity } from "./activity.js";
|
|
|
8
8
|
import { ConfigParser, outputPath } from './config.js';
|
|
9
9
|
import { Observability } from "./observability.js";
|
|
10
10
|
import { browserErrorMessage, isFatalBrowserError, isNavigationTransitionError } from "./utils/browser-errors.js";
|
|
11
|
-
import { captureHtmlForSnapshot, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
11
|
+
import { captureHtmlForSnapshot, getVisibleOverlayHtmlExtractorSource, htmlCombinedSnapshot, minifyHtml } from './utils/html.js';
|
|
12
12
|
import { createDebug, setStepSpanParent, tag } from './utils/logger.js';
|
|
13
|
+
import { Overlay } from './utils/overlay.js';
|
|
13
14
|
import { sleep, waitForPageReadiness } from "./utils/page-readiness.js";
|
|
14
15
|
import { safeFilename } from "./utils/strings.js";
|
|
15
16
|
import { codeceptJSSandbox, hasPlaywrightCommands, playwrightSandbox, sanitizeCodeBlock } from "./utils/web-sandbox.js";
|
|
@@ -131,10 +132,13 @@ class Action {
|
|
|
131
132
|
let ariaSnapshot = null;
|
|
132
133
|
let ariaSnapshotFile = undefined;
|
|
133
134
|
let focusedElement = null;
|
|
135
|
+
let overlayHtml = '';
|
|
134
136
|
try {
|
|
135
137
|
const page = this.playwrightHelper.page;
|
|
136
138
|
ariaSnapshot = await page.locator('body').ariaSnapshot();
|
|
137
139
|
focusedElement = await page.evaluate(readFocusedElement);
|
|
140
|
+
if (!frame)
|
|
141
|
+
overlayHtml = await this.captureOverlayHtml();
|
|
138
142
|
}
|
|
139
143
|
catch (err) {
|
|
140
144
|
debugLog('ARIA snapshot failed:', err instanceof Error ? `${err.message}\n${err.stack}` : err);
|
|
@@ -161,6 +165,7 @@ class Action {
|
|
|
161
165
|
ariaSnapshot,
|
|
162
166
|
ariaSnapshotFile,
|
|
163
167
|
focusedElement,
|
|
168
|
+
overlayHtml: overlayHtml || undefined,
|
|
164
169
|
iframeURL: frame ? frame.url?.() || 'iframe' : undefined,
|
|
165
170
|
});
|
|
166
171
|
this.stateManager.updateState(result, codeBlock);
|
|
@@ -175,6 +180,12 @@ class Action {
|
|
|
175
180
|
return new ActionResult({ url, error: msg });
|
|
176
181
|
}
|
|
177
182
|
}
|
|
183
|
+
async captureOverlayHtml() {
|
|
184
|
+
return this.playwrightHelper.page.evaluate(({ extractorSource, config }) => {
|
|
185
|
+
const extract = new Function(`return ${extractorSource}`)();
|
|
186
|
+
return extract(config);
|
|
187
|
+
}, { extractorSource: getVisibleOverlayHtmlExtractorSource(), config: Overlay.captureConfig() });
|
|
188
|
+
}
|
|
178
189
|
async captureMainDocumentStatus() {
|
|
179
190
|
if (this.mainDocumentStatus)
|
|
180
191
|
return this.mainDocumentStatus;
|
|
@@ -7,6 +7,7 @@ export interface ToolExecution {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function toToolExecution(toolName: string, input: any, rawOutput: any): ToolExecution;
|
|
9
9
|
export declare function toolExecutionLabel(input: Record<string, any> | undefined): string;
|
|
10
|
+
export declare const NARRATION_TOOL = "commentary";
|
|
10
11
|
export declare class Conversation {
|
|
11
12
|
messages: ModelMessage[];
|
|
12
13
|
model: any;
|
|
@@ -7,6 +7,7 @@ export function toToolExecution(toolName, input, rawOutput) {
|
|
|
7
7
|
export function toolExecutionLabel(input) {
|
|
8
8
|
return input?.explanation || input?.assertion || input?.reason || input?.request || '';
|
|
9
9
|
}
|
|
10
|
+
export const NARRATION_TOOL = 'commentary';
|
|
10
11
|
const AUTO_COMPACT_ARIA_CHANGES_CUTOFF = 500;
|
|
11
12
|
const AUTO_COMPACT_TARGETED_HTML_CUTOFF = 500;
|
|
12
13
|
export class Conversation {
|
|
@@ -203,6 +204,8 @@ export class Conversation {
|
|
|
203
204
|
for (const part of message.content) {
|
|
204
205
|
if (part.type !== 'tool-result')
|
|
205
206
|
continue;
|
|
207
|
+
if (part.toolName === NARRATION_TOOL)
|
|
208
|
+
continue;
|
|
206
209
|
executions.push(toToolExecution(part.toolName, toolCalls.get(part.toolCallId) || {}, part.output));
|
|
207
210
|
}
|
|
208
211
|
}
|
package/dist/src/ai/driller.js
CHANGED
|
@@ -9,6 +9,7 @@ import { collectInteractiveNodes } from "../utils/aria.js";
|
|
|
9
9
|
import { EXPLORBOT_ATTRS, HTML_COMPOSITE_AREA_HINTS, HTML_COMPOSITE_TARGET_ROLES, HTML_EXTRACTION_LIMITS, HTML_FORM_CONTROL_ROLES, HTML_FORM_CONTROL_TAGS, HTML_INTERACTIVE_ROLES, HTML_SELECTORS, HTML_VISIBILITY_LIMITS, getComponentScopeHtmlExtractorSource, getVisibleOverlayHtmlExtractorSource, inferHtmlRole, } from "../utils/html.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
import { loop, pause } from "../utils/loop.js";
|
|
12
|
+
import { OVERLAY_SELECTORS } from "../utils/overlay.js";
|
|
12
13
|
import { annotatePageElements } from "../utils/web-annotate.js";
|
|
13
14
|
import { eidxInContainer } from "../utils/web-eidx.js";
|
|
14
15
|
import { WebElement } from "../utils/web-element.js";
|
|
@@ -578,7 +579,8 @@ export class Driller extends TaskAgent {
|
|
|
578
579
|
config: {
|
|
579
580
|
interactiveContentSelector: HTML_SELECTORS.interactiveContent,
|
|
580
581
|
limits: HTML_EXTRACTION_LIMITS,
|
|
581
|
-
overlaySelectors:
|
|
582
|
+
overlaySelectors: OVERLAY_SELECTORS.semanticOverlays,
|
|
583
|
+
overlaySemanticSelector: OVERLAY_SELECTORS.overlaySemanticSelector,
|
|
582
584
|
visibilityLimits: HTML_VISIBILITY_LIMITS,
|
|
583
585
|
},
|
|
584
586
|
}));
|
package/dist/src/ai/fisherman.js
CHANGED
|
@@ -158,7 +158,7 @@ export class Fisherman {
|
|
|
158
158
|
|
|
159
159
|
AVAILABLE TOOLS:
|
|
160
160
|
${toolNames.join(', ')}.
|
|
161
|
-
Use tool names exactly as listed. Do not invent aliases
|
|
161
|
+
Use tool names exactly as listed. Do not invent aliases or combined names.
|
|
162
162
|
Match each tool input schema exactly. Do not invent parameter names or pass extra fields.
|
|
163
163
|
|
|
164
164
|
WORKFLOW:
|
package/dist/src/ai/pilot.js
CHANGED
|
@@ -5,7 +5,7 @@ import { ActionResult } from "../action-result.js";
|
|
|
5
5
|
import { ConfigParser } from "../config.js";
|
|
6
6
|
import { Stats } from "../stats.js";
|
|
7
7
|
import { TestResult } from "../test-plan.js";
|
|
8
|
-
import { collectInteractiveNodes
|
|
8
|
+
import { collectInteractiveNodes } from "../utils/aria.js";
|
|
9
9
|
import { ErrorPageError } from "../utils/error-page.js";
|
|
10
10
|
import { createDebug, tag } from "../utils/logger.js";
|
|
11
11
|
const debugLog = createDebug('explorbot:pilot');
|
|
@@ -13,7 +13,8 @@ import { truncateJson } from "../utils/strings.js";
|
|
|
13
13
|
import { capabilityGroundingRule, dataProtectionRules } from "./rules.js";
|
|
14
14
|
import { isInteractive } from "./task-agent.js";
|
|
15
15
|
import { withdrawVisionTools } from "./tools.js";
|
|
16
|
-
const CHECK_TOOLS = ['verify', 'see', 'research'
|
|
16
|
+
const CHECK_TOOLS = ['verify', 'see', 'research'];
|
|
17
|
+
const EVIDENCE_TOOLS = ['verify', 'see'];
|
|
17
18
|
const META_TOOLS = ['record', 'reset', 'stop', 'finish'];
|
|
18
19
|
const PILOT_MESSAGE_LIMIT = 2;
|
|
19
20
|
const PILOT_MESSAGE_MAX_LENGTH = 160;
|
|
@@ -280,6 +281,10 @@ export class Pilot {
|
|
|
280
281
|
overrides the others — weigh them together. Tester's record() notes are the LEAST reliable; always
|
|
281
282
|
cross-check against actual actions and state. Visual screenshot analysis is strong for UI state
|
|
282
283
|
(active tabs, visible counts, colors).
|
|
284
|
+
Judge every check by WHAT IT ESTABLISHES, never by the fact that it ran. A check that executed
|
|
285
|
+
successfully is failure evidence when its content negates the scenario goal — the goal's object
|
|
286
|
+
absent, the action not performed, the interaction impossible. "The check passed" and "the goal was
|
|
287
|
+
met" are different claims.
|
|
283
288
|
If the final page clearly shows an equivalent success state in a different UI form, do not fail only
|
|
284
289
|
because one narrow assertion targeted a specific badge, count, toast, or wording that the product
|
|
285
290
|
represents differently.
|
|
@@ -499,7 +504,7 @@ export class Pilot {
|
|
|
499
504
|
async settleExpectations(task, finalState) {
|
|
500
505
|
let image = null;
|
|
501
506
|
if (finalState?.screenshot && this.provider.hasVision())
|
|
502
|
-
image =
|
|
507
|
+
image = finalState.screenshot;
|
|
503
508
|
const decided = (text) => {
|
|
504
509
|
if (task.hasAchievedAny() && !task.getRemainingExpectations().includes(text))
|
|
505
510
|
return 'passed';
|
|
@@ -733,7 +738,7 @@ export class Pilot {
|
|
|
733
738
|
lines.push(`h2: ${state.h2 || ''}`);
|
|
734
739
|
lines.push(`h3: ${state.h3 || ''}`);
|
|
735
740
|
lines.push(`h4: ${state.h4 || ''}`);
|
|
736
|
-
const focusArea =
|
|
741
|
+
const focusArea = state.overlay;
|
|
737
742
|
if (focusArea.detected) {
|
|
738
743
|
lines.push(`modal: ${focusArea.name || focusArea.type}`);
|
|
739
744
|
}
|
|
@@ -897,20 +902,20 @@ export class Pilot {
|
|
|
897
902
|
hasSuccessfulCheckEvidence(currentState, testerConversation) {
|
|
898
903
|
if (Object.values(currentState.verifications ?? {}).some(Boolean))
|
|
899
904
|
return true;
|
|
900
|
-
return testerConversation.getToolExecutions().some((t) =>
|
|
905
|
+
return testerConversation.getToolExecutions().some((t) => EVIDENCE_TOOLS.includes(t.toolName) && t.wasSuccessful);
|
|
901
906
|
}
|
|
902
907
|
formatSuccessfulAssertions(currentState, testerConversation) {
|
|
903
908
|
const lines = [];
|
|
904
909
|
for (const [assertion, passed] of Object.entries(currentState.verifications ?? {})) {
|
|
905
910
|
if (passed)
|
|
906
|
-
lines.push(`
|
|
911
|
+
lines.push(`state verification (passed): ${assertion}`);
|
|
907
912
|
}
|
|
908
913
|
for (const exec of testerConversation.getToolExecutions()) {
|
|
909
|
-
if (!
|
|
914
|
+
if (!EVIDENCE_TOOLS.includes(exec.toolName) || !exec.wasSuccessful)
|
|
910
915
|
continue;
|
|
911
916
|
const description = exec.input?.assertion || exec.input?.request || truncateJson(exec.input);
|
|
912
917
|
const result = exec.output?.message || exec.output?.analysis || exec.output?.result;
|
|
913
|
-
lines.push(`
|
|
918
|
+
lines.push(`CHECK ${exec.toolName} (executed successfully): ${description}${result ? ` -> ${result}` : ''}`);
|
|
914
919
|
}
|
|
915
920
|
return [...new Set(lines)].join('\n');
|
|
916
921
|
}
|
|
@@ -1028,7 +1033,7 @@ export class Pilot {
|
|
|
1028
1033
|
|
|
1029
1034
|
Tester tools: click, pressKey, form, see, verify, interact, context, research, xpathCheck,
|
|
1030
1035
|
visualClick, back, getVisitedStates, reset, stop, finish, record.
|
|
1031
|
-
Use tool names exactly as listed. Do not invent combined names
|
|
1036
|
+
Use tool names exactly as listed. Do not invent combined names or aliases.
|
|
1032
1037
|
|
|
1033
1038
|
${capabilityGroundingRule}
|
|
1034
1039
|
|
|
@@ -7,14 +7,18 @@ declare class AiError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
export declare class ContextLengthError extends Error {
|
|
9
9
|
}
|
|
10
|
+
export declare function flushTelemetry(): Promise<void>;
|
|
10
11
|
export declare class Provider {
|
|
11
12
|
config: AIConfig;
|
|
12
13
|
telemetryEnabled: boolean;
|
|
13
14
|
otelSdk: NodeSDK | null;
|
|
14
15
|
defaultRetryOptions: RetryOptions;
|
|
15
16
|
lastConversation: Conversation | null;
|
|
17
|
+
activeModelCalls: number;
|
|
18
|
+
modelCallWaiters: (() => void)[];
|
|
16
19
|
constructor(config: AIConfig);
|
|
17
20
|
validateConnection(): Promise<void>;
|
|
21
|
+
stop(): Promise<void>;
|
|
18
22
|
getModelForAgent(agentName?: string): any;
|
|
19
23
|
getAgenticModel(agentName?: string): any;
|
|
20
24
|
getVisionModel(): any;
|
|
@@ -23,6 +27,7 @@ export declare class Provider {
|
|
|
23
27
|
getProviderOptionsForAgent(agentName: string): Record<string, any> | undefined;
|
|
24
28
|
getReasoningForAgent(agentName?: string): string | undefined;
|
|
25
29
|
getRetryOptions(options?: any): RetryOptions;
|
|
30
|
+
withModelRequestSlot<T>(fn: () => Promise<T>): Promise<T>;
|
|
26
31
|
mergeProviderOptions(config: Record<string, any>, agentName?: string): Record<string, any>;
|
|
27
32
|
finalizeConfig(config: Record<string, any>, options: any, telemetry: any): void;
|
|
28
33
|
buildGenerateConfig(defaults: Record<string, any>, overrides: Record<string, any>, options: any): Record<string, any>;
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { OpenTelemetry } from '@ai-sdk/otel';
|
|
2
2
|
import { LangfuseSpanProcessor } from '@langfuse/otel';
|
|
3
3
|
import { NodeSDK } from '@opentelemetry/sdk-node';
|
|
4
|
-
import {
|
|
4
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
5
|
+
import { generateObject, generateText, isStepCount, registerTelemetry, tool } from 'ai';
|
|
6
|
+
import { z } from 'zod';
|
|
5
7
|
import { clearActivity, setActivity } from "../activity.js";
|
|
6
8
|
import { configuredModels, modelName as getModelName } from '../config.js';
|
|
7
9
|
import { executionController } from "../execution-controller.js";
|
|
@@ -10,7 +12,7 @@ import { Stats } from "../stats.js";
|
|
|
10
12
|
import { createDebug, tag } from '../utils/logger.js';
|
|
11
13
|
import { withRetry } from '../utils/retry.js';
|
|
12
14
|
import { RulesLoader } from "../utils/rules-loader.js";
|
|
13
|
-
import { Conversation, toToolExecution } from './conversation.js';
|
|
15
|
+
import { Conversation, NARRATION_TOOL, toToolExecution } from './conversation.js';
|
|
14
16
|
const debugLog = createDebug('explorbot:provider');
|
|
15
17
|
const promptLog = createDebug('explorbot:provider:out');
|
|
16
18
|
const responseLog = createDebug('explorbot:provider:in');
|
|
@@ -18,7 +20,26 @@ class AiError extends Error {
|
|
|
18
20
|
}
|
|
19
21
|
export class ContextLengthError extends Error {
|
|
20
22
|
}
|
|
23
|
+
const DEFAULT_PARALLEL_REQUESTS = 4;
|
|
24
|
+
const modelSlotContext = new AsyncLocalStorage();
|
|
25
|
+
const HARMONY_CHANNELS = ['commentary', 'analysis', 'final'];
|
|
26
|
+
function createHarmonyChannelFallbackTool() {
|
|
27
|
+
return tool({
|
|
28
|
+
description: 'Internal compatibility fallback for model channel output. Do not call directly.',
|
|
29
|
+
inputSchema: z.record(z.string(), z.any()),
|
|
30
|
+
execute: async () => ({ message: 'Noted. Continue with your next action.' }),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
21
33
|
let telemetryRegistered = false;
|
|
34
|
+
let beforeExitFlushHooked = false;
|
|
35
|
+
let activeOtelSdk = null;
|
|
36
|
+
export async function flushTelemetry() {
|
|
37
|
+
const sdk = activeOtelSdk;
|
|
38
|
+
activeOtelSdk = null;
|
|
39
|
+
if (!sdk)
|
|
40
|
+
return;
|
|
41
|
+
await sdk.shutdown().catch((error) => debugLog(`Telemetry flush failed: ${error instanceof Error ? error.message : error}`));
|
|
42
|
+
}
|
|
22
43
|
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'];
|
|
23
44
|
function extractCachedTokens(usage) {
|
|
24
45
|
if (!usage)
|
|
@@ -73,6 +94,8 @@ export class Provider {
|
|
|
73
94
|
},
|
|
74
95
|
};
|
|
75
96
|
lastConversation = null;
|
|
97
|
+
activeModelCalls = 0;
|
|
98
|
+
modelCallWaiters = [];
|
|
76
99
|
constructor(config) {
|
|
77
100
|
if (!config?.model) {
|
|
78
101
|
throw new AiError('AI model is not configured. Set ai.model in your config file.');
|
|
@@ -91,6 +114,9 @@ export class Provider {
|
|
|
91
114
|
throw new AiError(`AI connection failed: ${error.message}`);
|
|
92
115
|
}
|
|
93
116
|
}
|
|
117
|
+
async stop() {
|
|
118
|
+
await flushTelemetry();
|
|
119
|
+
}
|
|
94
120
|
getModelForAgent(agentName) {
|
|
95
121
|
if (!agentName) {
|
|
96
122
|
return this.config.model;
|
|
@@ -140,9 +166,31 @@ export class Provider {
|
|
|
140
166
|
getRetryOptions(options = {}) {
|
|
141
167
|
return {
|
|
142
168
|
...this.defaultRetryOptions,
|
|
143
|
-
maxAttempts: options.maxRetries || this.defaultRetryOptions.maxAttempts,
|
|
169
|
+
maxAttempts: options.maxRetries || this.config.retryAttempts || this.defaultRetryOptions.maxAttempts,
|
|
170
|
+
baseDelay: this.config.retryDelay || this.defaultRetryOptions.baseDelay,
|
|
144
171
|
};
|
|
145
172
|
}
|
|
173
|
+
async withModelRequestSlot(fn) {
|
|
174
|
+
if (modelSlotContext.getStore())
|
|
175
|
+
return fn();
|
|
176
|
+
const limit = Math.max(1, this.config.maxParallelRequests ?? DEFAULT_PARALLEL_REQUESTS);
|
|
177
|
+
if (this.activeModelCalls >= limit || this.modelCallWaiters.length > 0) {
|
|
178
|
+
await new Promise((resolve) => this.modelCallWaiters.push(resolve));
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
this.activeModelCalls++;
|
|
182
|
+
}
|
|
183
|
+
try {
|
|
184
|
+
return await modelSlotContext.run(true, fn);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
const next = this.modelCallWaiters.shift();
|
|
188
|
+
if (next)
|
|
189
|
+
next();
|
|
190
|
+
else
|
|
191
|
+
this.activeModelCalls--;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
146
194
|
mergeProviderOptions(config, agentName) {
|
|
147
195
|
if (!agentName)
|
|
148
196
|
return config;
|
|
@@ -215,7 +263,12 @@ export class Provider {
|
|
|
215
263
|
spanProcessors: [processor],
|
|
216
264
|
instrumentations: [],
|
|
217
265
|
});
|
|
266
|
+
activeOtelSdk = this.otelSdk;
|
|
218
267
|
void this.otelSdk.start();
|
|
268
|
+
if (!beforeExitFlushHooked) {
|
|
269
|
+
process.on('beforeExit', () => void flushTelemetry());
|
|
270
|
+
beforeExitFlushHooked = true;
|
|
271
|
+
}
|
|
219
272
|
if (!telemetryRegistered) {
|
|
220
273
|
registerTelemetry(new OpenTelemetry());
|
|
221
274
|
telemetryRegistered = true;
|
|
@@ -257,7 +310,7 @@ export class Provider {
|
|
|
257
310
|
}
|
|
258
311
|
async invokeConversation(conversation, tools, options = {}) {
|
|
259
312
|
const response = tools ? await this.generateWithTools(conversation.messages, conversation.model, tools, options) : await this.chat(conversation.messages, conversation.model, options);
|
|
260
|
-
const responseMessages = response.
|
|
313
|
+
const responseMessages = response.responseMessages || [];
|
|
261
314
|
if (responseMessages.length > 0) {
|
|
262
315
|
conversation.messages.push(...responseMessages);
|
|
263
316
|
tag('debug').log('Added', responseMessages.length, 'messages from response');
|
|
@@ -269,7 +322,7 @@ export class Provider {
|
|
|
269
322
|
const toolCalls = response.toolCalls || [];
|
|
270
323
|
const toolResults = response.toolResults || [];
|
|
271
324
|
const resultsById = new Map(toolResults.map((r) => [r.toolCallId, r]));
|
|
272
|
-
const toolExecutions = toolCalls.map((call) => toToolExecution(call.toolName || '', call.input, resultsById.get(call.toolCallId)?.output));
|
|
325
|
+
const toolExecutions = toolCalls.filter((call) => call.toolName !== NARRATION_TOOL).map((call) => toToolExecution(call.toolName || '', call.input, resultsById.get(call.toolCallId)?.output));
|
|
273
326
|
return { conversation, response, toolExecutions };
|
|
274
327
|
}
|
|
275
328
|
async chat(messages, model, options = {}) {
|
|
@@ -279,7 +332,7 @@ export class Provider {
|
|
|
279
332
|
const config = this.buildGenerateConfig({ maxOutputTokens: 16384 }, { model, abortSignal: executionController.getAbortSignal() }, options);
|
|
280
333
|
promptLog(messages[messages.length - 1].content);
|
|
281
334
|
try {
|
|
282
|
-
const response = await withRetry(async () => {
|
|
335
|
+
const response = await this.withModelRequestSlot(() => withRetry(async () => {
|
|
283
336
|
const result = await generateText({ messages, ...config });
|
|
284
337
|
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
285
338
|
if (!result.text) {
|
|
@@ -293,7 +346,7 @@ export class Provider {
|
|
|
293
346
|
debugLog('finishReason=length, response may be truncated');
|
|
294
347
|
}
|
|
295
348
|
return result;
|
|
296
|
-
}, this.getRetryOptions(options));
|
|
349
|
+
}, this.getRetryOptions(options)));
|
|
297
350
|
clearActivity();
|
|
298
351
|
responseLog(response.text);
|
|
299
352
|
return response;
|
|
@@ -318,7 +371,8 @@ export class Provider {
|
|
|
318
371
|
const modelName = getModelName(model);
|
|
319
372
|
setActivity(`🤖 Asking ${modelName} with dynamic tools`, 'ai');
|
|
320
373
|
promptLog(`Using model: ${modelName}`);
|
|
321
|
-
const
|
|
374
|
+
const toolsWithCommentary = tools?.commentary ? tools : { ...tools, commentary: createHarmonyChannelFallbackTool() };
|
|
375
|
+
const toolNames = Object.keys(toolsWithCommentary || {});
|
|
322
376
|
tag('debug').log(`Tools enabled: [${toolNames.join(', ')}]`);
|
|
323
377
|
promptLog('Available tools:', toolNames);
|
|
324
378
|
promptLog(messages[messages.length - 1].content);
|
|
@@ -327,9 +381,9 @@ export class Provider {
|
|
|
327
381
|
const stopConditions = [isStepCount(maxRoundtrips)];
|
|
328
382
|
if (extraStop)
|
|
329
383
|
stopConditions.push(extraStop);
|
|
330
|
-
const config = this.buildGenerateConfig({ tools, maxOutputTokens: 16384, toolChoice: 'auto', experimental_repairToolCall: repairToolCall }, { stopWhen: stopConditions, model }, options);
|
|
384
|
+
const config = this.buildGenerateConfig({ tools: toolsWithCommentary, maxOutputTokens: 16384, toolChoice: 'auto', experimental_repairToolCall: repairToolCall }, { stopWhen: stopConditions, model }, options);
|
|
331
385
|
try {
|
|
332
|
-
const response = await withRetry(async () => {
|
|
386
|
+
const response = await this.withModelRequestSlot(() => withRetry(async () => {
|
|
333
387
|
const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), config.timeout || 30000));
|
|
334
388
|
this.recordUsage(options.agentName || 'unknown', modelName, result.usage);
|
|
335
389
|
const hasToolCall = (result.toolCalls?.length || 0) > 0;
|
|
@@ -337,7 +391,7 @@ export class Provider {
|
|
|
337
391
|
throw new ContextLengthError('AI response empty: output truncated at maxTokens. Increase maxOutputTokens in config or use a model with higher output capacity.');
|
|
338
392
|
}
|
|
339
393
|
return result;
|
|
340
|
-
}, this.getRetryOptions(options));
|
|
394
|
+
}, this.getRetryOptions(options)));
|
|
341
395
|
clearActivity();
|
|
342
396
|
// Log tool usage summary
|
|
343
397
|
if (response.toolCalls && response.toolCalls.length > 0) {
|
|
@@ -376,9 +430,9 @@ export class Provider {
|
|
|
376
430
|
const config = this.buildGenerateConfig({ schema }, { model: modelToUse }, options);
|
|
377
431
|
try {
|
|
378
432
|
promptLog(messages[messages.length - 1].content);
|
|
379
|
-
const response = await withRetry(async () => {
|
|
433
|
+
const response = await this.withModelRequestSlot(() => withRetry(async () => {
|
|
380
434
|
return (await this.raceWithIdleTimeout((signal) => generateObject({ messages, ...config, abortSignal: signal }), config.timeout || 30000));
|
|
381
|
-
}, this.getRetryOptions(options));
|
|
435
|
+
}, this.getRetryOptions(options)));
|
|
382
436
|
clearActivity();
|
|
383
437
|
responseLog(response.object);
|
|
384
438
|
this.recordUsage(options.agentName || 'unknown', modelName, response.usage);
|
|
@@ -512,7 +566,6 @@ export class Provider {
|
|
|
512
566
|
throw new Error('Vision model not configured. Please set ai.visionModel in your config.');
|
|
513
567
|
}
|
|
514
568
|
setActivity(`🤖 Processing image with ${this.config.visionModel}`, 'ai');
|
|
515
|
-
const imageData = `data:image/png;base64,${image.toString()}`;
|
|
516
569
|
const messages = [
|
|
517
570
|
{
|
|
518
571
|
role: 'user',
|
|
@@ -524,7 +577,7 @@ export class Provider {
|
|
|
524
577
|
{
|
|
525
578
|
type: 'file',
|
|
526
579
|
mediaType: 'image/png',
|
|
527
|
-
data:
|
|
580
|
+
data: image,
|
|
528
581
|
},
|
|
529
582
|
],
|
|
530
583
|
},
|
|
@@ -540,12 +593,12 @@ export class Provider {
|
|
|
540
593
|
config.telemetry = telemetry;
|
|
541
594
|
try {
|
|
542
595
|
promptLog(`Processing image with prompt: ${prompt}`);
|
|
543
|
-
const response = await withRetry(async () => {
|
|
596
|
+
const response = await this.withModelRequestSlot(() => withRetry(async () => {
|
|
544
597
|
return await generateText({
|
|
545
598
|
messages,
|
|
546
599
|
...config,
|
|
547
600
|
});
|
|
548
|
-
}, this.getRetryOptions());
|
|
601
|
+
}, this.getRetryOptions()));
|
|
549
602
|
clearActivity();
|
|
550
603
|
responseLog(response.text);
|
|
551
604
|
this.recordUsage('vision', getModelName(this.config.visionModel), response.usage);
|
|
@@ -559,13 +612,13 @@ export class Provider {
|
|
|
559
612
|
}
|
|
560
613
|
}
|
|
561
614
|
hasVision() {
|
|
562
|
-
return this.config.visionModel !== undefined;
|
|
615
|
+
return this.config.visionModel !== undefined && !Stats.visionDisabled;
|
|
563
616
|
}
|
|
564
617
|
}
|
|
565
618
|
function repairToolCall(options) {
|
|
566
619
|
if (options.toolCall.toolName.includes('<|channel|>'))
|
|
567
620
|
return repairChannelMarker(options);
|
|
568
|
-
return
|
|
621
|
+
return repairHarmonyChannel(options);
|
|
569
622
|
}
|
|
570
623
|
function repairChannelMarker({ toolCall, tools }) {
|
|
571
624
|
const markerIndex = toolCall.toolName.indexOf('<|channel|>');
|
|
@@ -577,4 +630,16 @@ function repairChannelMarker({ toolCall, tools }) {
|
|
|
577
630
|
tag('warning').log(`Repaired tool name '${toolCall.toolName}' → '${toolName}'`);
|
|
578
631
|
return { ...toolCall, toolName };
|
|
579
632
|
}
|
|
633
|
+
function repairHarmonyChannel({ toolCall, tools }) {
|
|
634
|
+
if (!HARMONY_CHANNELS.includes(toolCall.toolName))
|
|
635
|
+
return null;
|
|
636
|
+
if (!tools.commentary)
|
|
637
|
+
return null;
|
|
638
|
+
let input = toolCall.input;
|
|
639
|
+
if (typeof input !== 'string' || !input.trim().startsWith('{')) {
|
|
640
|
+
input = JSON.stringify({ content: typeof input === 'string' ? input : JSON.stringify(input ?? null) });
|
|
641
|
+
}
|
|
642
|
+
tag('warning').log(`Repaired tool name '${toolCall.toolName}' → 'commentary'`);
|
|
643
|
+
return { ...toolCall, toolName: NARRATION_TOOL, input };
|
|
644
|
+
}
|
|
580
645
|
export { AiError, Provider as AIProvider };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dedent from 'dedent';
|
|
2
2
|
import { ActionResult } from '../../action-result.js';
|
|
3
3
|
import { executionController } from "../../execution-controller.js";
|
|
4
|
-
import {
|
|
4
|
+
import { diffAriaSnapshots } from "../../utils/aria.js";
|
|
5
5
|
import { extractCodeBlocks } from "../../utils/code-extractor.js";
|
|
6
6
|
import { tag } from '../../utils/logger.js';
|
|
7
7
|
import { mdq } from "../../utils/markdown-query.js";
|
|
@@ -60,7 +60,7 @@ export function WithDeepAnalysis(Base) {
|
|
|
60
60
|
this._appendExtendedResearch(result, expandedSections, navigationLinks);
|
|
61
61
|
}
|
|
62
62
|
async researchOverlay(current, previous, pageStateHash) {
|
|
63
|
-
const focusArea =
|
|
63
|
+
const focusArea = current.overlay;
|
|
64
64
|
if (!focusArea.detected || !focusArea.name)
|
|
65
65
|
return null;
|
|
66
66
|
if (focusArea.type !== 'dialog' && focusArea.type !== 'modal')
|