@sparkleideas/browser 3.0.0-alpha.3

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.
@@ -0,0 +1,282 @@
1
+ /**
2
+ * @sparkleideas/browser - ReasoningBank Integration
3
+ * Connects browser trajectories to @sparkleideas/agentic-flow's learning system
4
+ */
5
+
6
+ import type { BrowserTrajectory, BrowserTrajectoryStep, Snapshot } from '../domain/types.js';
7
+
8
+ // ============================================================================
9
+ // ReasoningBank Pattern Types
10
+ // ============================================================================
11
+
12
+ export interface BrowserPattern {
13
+ id: string;
14
+ type: 'navigation' | 'interaction' | 'extraction' | 'form' | 'auth' | 'test';
15
+ goal: string;
16
+ steps: PatternStep[];
17
+ successRate: number;
18
+ avgDuration: number;
19
+ lastUsed: string;
20
+ usageCount: number;
21
+ embedding?: number[];
22
+ }
23
+
24
+ export interface PatternStep {
25
+ action: string;
26
+ selector?: string;
27
+ value?: string;
28
+ condition?: string;
29
+ }
30
+
31
+ // ============================================================================
32
+ // ReasoningBank Adapter
33
+ // ============================================================================
34
+
35
+ export class ReasoningBankAdapter {
36
+ private patterns: Map<string, BrowserPattern> = new Map();
37
+ private trajectoryBuffer: BrowserTrajectory[] = [];
38
+ private readonly maxBufferSize = 100;
39
+
40
+ /**
41
+ * Store a completed trajectory for learning
42
+ */
43
+ async storeTrajectory(trajectory: BrowserTrajectory): Promise<void> {
44
+ this.trajectoryBuffer.push(trajectory);
45
+
46
+ // Process buffer when full
47
+ if (this.trajectoryBuffer.length >= this.maxBufferSize) {
48
+ await this.processBuffer();
49
+ }
50
+
51
+ // Extract pattern from successful trajectories
52
+ if (trajectory.success) {
53
+ const pattern = this.extractPattern(trajectory);
54
+ if (pattern) {
55
+ this.patterns.set(pattern.id, pattern);
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Extract a reusable pattern from a trajectory
62
+ */
63
+ private extractPattern(trajectory: BrowserTrajectory): BrowserPattern | null {
64
+ if (trajectory.steps.length < 2) return null;
65
+
66
+ const patternId = this.generatePatternId(trajectory.goal);
67
+ const existing = this.patterns.get(patternId);
68
+
69
+ const steps: PatternStep[] = trajectory.steps.map(step => ({
70
+ action: step.action,
71
+ selector: this.normalizeSelector(step.input.target as string),
72
+ value: step.input.value as string,
73
+ condition: step.input.waitUntil as string || step.input.text as string,
74
+ }));
75
+
76
+ const avgDuration = trajectory.steps.reduce((sum, s) => sum + (s.result.duration || 0), 0) / trajectory.steps.length;
77
+
78
+ if (existing) {
79
+ // Update existing pattern
80
+ return {
81
+ ...existing,
82
+ successRate: (existing.successRate * existing.usageCount + 1) / (existing.usageCount + 1),
83
+ avgDuration: (existing.avgDuration * existing.usageCount + avgDuration) / (existing.usageCount + 1),
84
+ lastUsed: new Date().toISOString(),
85
+ usageCount: existing.usageCount + 1,
86
+ };
87
+ }
88
+
89
+ return {
90
+ id: patternId,
91
+ type: this.inferPatternType(trajectory),
92
+ goal: trajectory.goal,
93
+ steps,
94
+ successRate: 1,
95
+ avgDuration,
96
+ lastUsed: new Date().toISOString(),
97
+ usageCount: 1,
98
+ };
99
+ }
100
+
101
+ /**
102
+ * Infer pattern type from trajectory
103
+ */
104
+ private inferPatternType(trajectory: BrowserTrajectory): BrowserPattern['type'] {
105
+ const actions = trajectory.steps.map(s => s.action);
106
+ const goal = trajectory.goal.toLowerCase();
107
+
108
+ if (goal.includes('login') || goal.includes('auth') || actions.includes('state-save')) {
109
+ return 'auth';
110
+ }
111
+ if (goal.includes('test') || goal.includes('verify') || goal.includes('assert')) {
112
+ return 'test';
113
+ }
114
+ if (goal.includes('extract') || goal.includes('scrape') || actions.filter(a => a === 'getText').length > 3) {
115
+ return 'extraction';
116
+ }
117
+ if (goal.includes('form') || goal.includes('submit') || actions.filter(a => a === 'fill').length > 2) {
118
+ return 'form';
119
+ }
120
+ if (actions.filter(a => a === 'click').length > 3) {
121
+ return 'interaction';
122
+ }
123
+ return 'navigation';
124
+ }
125
+
126
+ /**
127
+ * Normalize selector for pattern matching
128
+ */
129
+ private normalizeSelector(selector?: string): string | undefined {
130
+ if (!selector) return undefined;
131
+
132
+ // Keep refs as-is (they're from snapshots)
133
+ if (selector.startsWith('@e')) {
134
+ return '{ref}'; // Placeholder for any ref
135
+ }
136
+
137
+ // Keep semantic locators
138
+ if (selector.startsWith('text=') || selector.startsWith('role=')) {
139
+ return selector;
140
+ }
141
+
142
+ // Generalize CSS selectors
143
+ return selector
144
+ .replace(/\[data-testid="[^"]+"\]/g, '[data-testid="{testid}"]')
145
+ .replace(/#\w+/g, '#{id}')
146
+ .replace(/\.\w+/g, '.{class}');
147
+ }
148
+
149
+ /**
150
+ * Generate pattern ID from goal
151
+ */
152
+ private generatePatternId(goal: string): string {
153
+ return `pattern-${goal
154
+ .toLowerCase()
155
+ .replace(/[^a-z0-9]+/g, '-')
156
+ .slice(0, 50)}-${Date.now().toString(36)}`;
157
+ }
158
+
159
+ /**
160
+ * Process buffered trajectories (batch learning)
161
+ */
162
+ private async processBuffer(): Promise<void> {
163
+ const successful = this.trajectoryBuffer.filter(t => t.success);
164
+ const failed = this.trajectoryBuffer.filter(t => !t.success);
165
+
166
+ // Learn from failures
167
+ for (const failure of failed) {
168
+ await this.analyzeFailure(failure);
169
+ }
170
+
171
+ // Clear buffer
172
+ this.trajectoryBuffer = [];
173
+ }
174
+
175
+ /**
176
+ * Analyze a failed trajectory to learn what went wrong
177
+ */
178
+ private async analyzeFailure(trajectory: BrowserTrajectory): Promise<void> {
179
+ const failedStep = trajectory.steps.find(s => !s.result.success);
180
+ if (!failedStep) return;
181
+
182
+ // Store failure pattern for avoidance
183
+ const failureId = `failure-${failedStep.action}-${Date.now().toString(36)}`;
184
+ console.log(`[ReasoningBank] Learned from failure: ${failureId} - ${failedStep.result.error}`);
185
+ }
186
+
187
+ /**
188
+ * Find similar patterns for a goal
189
+ */
190
+ async findSimilarPatterns(goal: string, limit = 5): Promise<BrowserPattern[]> {
191
+ const patterns = Array.from(this.patterns.values());
192
+
193
+ // Simple text similarity for now
194
+ // In production, use HNSW with embeddings
195
+ const scored = patterns.map(p => ({
196
+ pattern: p,
197
+ score: this.textSimilarity(goal.toLowerCase(), p.goal.toLowerCase()),
198
+ }));
199
+
200
+ return scored
201
+ .filter(s => s.score > 0.3)
202
+ .sort((a, b) => b.score - a.score)
203
+ .slice(0, limit)
204
+ .map(s => s.pattern);
205
+ }
206
+
207
+ /**
208
+ * Simple text similarity
209
+ */
210
+ private textSimilarity(a: string, b: string): number {
211
+ const wordsA = new Set(a.split(/\s+/));
212
+ const wordsB = new Set(b.split(/\s+/));
213
+ const intersection = new Set([...wordsA].filter(w => wordsB.has(w)));
214
+ const union = new Set([...wordsA, ...wordsB]);
215
+ return intersection.size / union.size;
216
+ }
217
+
218
+ /**
219
+ * Get recommended steps for a goal
220
+ */
221
+ async getRecommendedSteps(goal: string): Promise<PatternStep[]> {
222
+ const similar = await this.findSimilarPatterns(goal, 1);
223
+ if (similar.length === 0) return [];
224
+
225
+ return similar[0].steps;
226
+ }
227
+
228
+ /**
229
+ * Record verdict for SONA learning
230
+ */
231
+ async recordVerdict(trajectoryId: string, success: boolean, feedback?: string): Promise<void> {
232
+ // In production, this would update SONA weights
233
+ console.log(`[ReasoningBank] Verdict for ${trajectoryId}: ${success ? 'SUCCESS' : 'FAILURE'}${feedback ? ` - ${feedback}` : ''}`);
234
+ }
235
+
236
+ /**
237
+ * Get pattern stats
238
+ */
239
+ getStats(): { totalPatterns: number; avgSuccessRate: number; bufferedTrajectories: number } {
240
+ const patterns = Array.from(this.patterns.values());
241
+ const avgSuccessRate = patterns.length > 0
242
+ ? patterns.reduce((sum, p) => sum + p.successRate, 0) / patterns.length
243
+ : 0;
244
+
245
+ return {
246
+ totalPatterns: patterns.length,
247
+ avgSuccessRate,
248
+ bufferedTrajectories: this.trajectoryBuffer.length,
249
+ };
250
+ }
251
+
252
+ /**
253
+ * Export patterns for persistence
254
+ */
255
+ exportPatterns(): BrowserPattern[] {
256
+ return Array.from(this.patterns.values());
257
+ }
258
+
259
+ /**
260
+ * Import patterns from storage
261
+ */
262
+ importPatterns(patterns: BrowserPattern[]): void {
263
+ for (const pattern of patterns) {
264
+ this.patterns.set(pattern.id, pattern);
265
+ }
266
+ }
267
+ }
268
+
269
+ // ============================================================================
270
+ // Singleton Instance
271
+ // ============================================================================
272
+
273
+ let instance: ReasoningBankAdapter | null = null;
274
+
275
+ export function getReasoningBank(): ReasoningBankAdapter {
276
+ if (!instance) {
277
+ instance = new ReasoningBankAdapter();
278
+ }
279
+ return instance;
280
+ }
281
+
282
+ export default ReasoningBankAdapter;