@sparkleideas/browser 3.0.0-alpha.18
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 +730 -0
- package/agents/architect.yaml +11 -0
- package/agents/coder.yaml +11 -0
- package/agents/reviewer.yaml +10 -0
- package/agents/security-architect.yaml +10 -0
- package/agents/tester.yaml +10 -0
- package/docker/Dockerfile +22 -0
- package/docker/docker-compose.yml +52 -0
- package/docker/test-fixtures/index.html +61 -0
- package/package.json +56 -0
- package/skills/browser/SKILL.md +204 -0
- package/src/agent/index.ts +35 -0
- package/src/application/browser-service.ts +570 -0
- package/src/domain/types.ts +324 -0
- package/src/index.ts +156 -0
- package/src/infrastructure/agent-browser-adapter.ts +654 -0
- package/src/infrastructure/hooks-integration.ts +170 -0
- package/src/infrastructure/memory-integration.ts +449 -0
- package/src/infrastructure/reasoningbank-adapter.ts +282 -0
- package/src/infrastructure/security-integration.ts +528 -0
- package/src/infrastructure/workflow-templates.ts +479 -0
- package/src/mcp-tools/browser-tools.ts +1210 -0
- package/src/mcp-tools/index.ts +6 -0
- package/src/skill/index.ts +24 -0
- package/tests/agent-browser-adapter.test.ts +328 -0
- package/tests/browser-service.test.ts +137 -0
- package/tests/e2e/browser-e2e.test.ts +175 -0
- package/tests/memory-integration.test.ts +277 -0
- package/tests/reasoningbank-adapter.test.ts +219 -0
- package/tests/security-integration.test.ts +194 -0
- package/tests/workflow-templates.test.ts +231 -0
- package/tmp.json +0 -0
- package/tsconfig.json +20 -0
- package/vitest.config.ts +15 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @sparkleideas/browser - ReasoningBank Integration
|
|
3
|
+
* Connects browser trajectories to 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;
|