web-agent-bridge 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,210 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WAB Runtime - Event Bus
5
+ *
6
+ * Async event system with typed events, middleware, replay buffer,
7
+ * and dead-letter queue. This is the nervous system of the Agent OS.
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+
12
+ class EventBus {
13
+ constructor(options = {}) {
14
+ this._listeners = new Map(); // event → Set<{ id, handler, filter, once }>
15
+ this._middleware = []; // global middleware
16
+ this._history = []; // event replay buffer
17
+ this._deadLetter = []; // failed events
18
+ this._maxHistory = options.maxHistory || 10000;
19
+ this._maxDeadLetter = options.maxDeadLetter || 1000;
20
+ this._stats = { emitted: 0, delivered: 0, failed: 0, dropped: 0 };
21
+ }
22
+
23
+ /**
24
+ * Subscribe to an event
25
+ * @returns {string} subscription ID for unsubscribe
26
+ */
27
+ on(event, handler, options = {}) {
28
+ if (!this._listeners.has(event)) this._listeners.set(event, new Set());
29
+ const sub = {
30
+ id: `sub_${crypto.randomBytes(8).toString('hex')}`,
31
+ handler,
32
+ filter: options.filter || null,
33
+ once: options.once || false,
34
+ priority: options.priority || 0,
35
+ };
36
+ this._listeners.get(event).add(sub);
37
+ return sub.id;
38
+ }
39
+
40
+ /**
41
+ * Subscribe once
42
+ */
43
+ once(event, handler, options = {}) {
44
+ return this.on(event, handler, { ...options, once: true });
45
+ }
46
+
47
+ /**
48
+ * Unsubscribe by subscription ID
49
+ */
50
+ off(subId) {
51
+ for (const [, subs] of this._listeners) {
52
+ for (const sub of subs) {
53
+ if (sub.id === subId) { subs.delete(sub); return true; }
54
+ }
55
+ }
56
+ return false;
57
+ }
58
+
59
+ /**
60
+ * Remove all listeners for an event
61
+ */
62
+ removeAll(event) {
63
+ if (event) this._listeners.delete(event);
64
+ else this._listeners.clear();
65
+ }
66
+
67
+ /**
68
+ * Emit an event
69
+ */
70
+ async emit(event, data, metadata = {}) {
71
+ const envelope = {
72
+ id: `evt_${crypto.randomBytes(12).toString('hex')}`,
73
+ event,
74
+ data,
75
+ metadata: {
76
+ ...metadata,
77
+ timestamp: Date.now(),
78
+ source: metadata.source || 'system',
79
+ },
80
+ };
81
+
82
+ // Run global middleware
83
+ for (const mw of this._middleware) {
84
+ try {
85
+ const result = await mw(envelope);
86
+ if (result === false) {
87
+ this._stats.dropped++;
88
+ return envelope;
89
+ }
90
+ } catch (_) { /* middleware errors don't block */ }
91
+ }
92
+
93
+ // Store in history
94
+ this._history.push(envelope);
95
+ if (this._history.length > this._maxHistory) {
96
+ this._history = this._history.slice(-Math.floor(this._maxHistory * 0.8));
97
+ }
98
+
99
+ this._stats.emitted++;
100
+
101
+ // Get listeners (event + wildcard)
102
+ const listeners = [];
103
+ const exact = this._listeners.get(event);
104
+ if (exact) for (const sub of exact) listeners.push(sub);
105
+ const wild = this._listeners.get('*');
106
+ if (wild) for (const sub of wild) listeners.push(sub);
107
+
108
+ // Also match namespace wildcards: 'task.*' matches 'task.completed'
109
+ for (const [pattern, subs] of this._listeners) {
110
+ if (pattern === event || pattern === '*') continue;
111
+ if (pattern.endsWith('.*') && event.startsWith(pattern.slice(0, -1))) {
112
+ for (const sub of subs) listeners.push(sub);
113
+ }
114
+ }
115
+
116
+ // Sort by priority (higher first)
117
+ listeners.sort((a, b) => b.priority - a.priority);
118
+
119
+ // Dispatch
120
+ const toRemove = [];
121
+ for (const sub of listeners) {
122
+ try {
123
+ if (sub.filter && !sub.filter(envelope.data, envelope.metadata)) continue;
124
+ await sub.handler(envelope.data, envelope.metadata, envelope);
125
+ this._stats.delivered++;
126
+ if (sub.once) toRemove.push(sub);
127
+ } catch (err) {
128
+ this._stats.failed++;
129
+ this._deadLetter.push({ envelope, error: err.message, subscriberId: sub.id, timestamp: Date.now() });
130
+ if (this._deadLetter.length > this._maxDeadLetter) {
131
+ this._deadLetter = this._deadLetter.slice(-Math.floor(this._maxDeadLetter * 0.8));
132
+ }
133
+ }
134
+ }
135
+
136
+ // Cleanup one-time subs
137
+ for (const sub of toRemove) {
138
+ for (const [, subs] of this._listeners) subs.delete(sub);
139
+ }
140
+
141
+ return envelope;
142
+ }
143
+
144
+ /**
145
+ * Add global middleware
146
+ */
147
+ use(middleware) {
148
+ this._middleware.push(middleware);
149
+ }
150
+
151
+ /**
152
+ * Replay events matching filter since a timestamp
153
+ */
154
+ async replay(since, filter, handler) {
155
+ const events = this._history.filter(
156
+ e => e.metadata.timestamp >= since && (!filter || filter(e))
157
+ );
158
+ for (const e of events) {
159
+ await handler(e.data, e.metadata, e);
160
+ }
161
+ return events.length;
162
+ }
163
+
164
+ /**
165
+ * Wait for a specific event (returns a promise)
166
+ */
167
+ waitFor(event, timeout = 30000, filter = null) {
168
+ return new Promise((resolve, reject) => {
169
+ const timer = setTimeout(() => {
170
+ this.off(subId);
171
+ reject(new Error(`Timeout waiting for event: ${event}`));
172
+ }, timeout);
173
+
174
+ const subId = this.once(event, (data, meta) => {
175
+ clearTimeout(timer);
176
+ resolve({ data, meta });
177
+ }, { filter });
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Get dead letter queue
183
+ */
184
+ getDeadLetters(limit = 50) {
185
+ return this._deadLetter.slice(-limit);
186
+ }
187
+
188
+ /**
189
+ * Get stats
190
+ */
191
+ getStats() {
192
+ return {
193
+ ...this._stats,
194
+ listeners: this._countListeners(),
195
+ historySize: this._history.length,
196
+ deadLetterSize: this._deadLetter.length,
197
+ };
198
+ }
199
+
200
+ _countListeners() {
201
+ let count = 0;
202
+ for (const [, subs] of this._listeners) count += subs.size;
203
+ return count;
204
+ }
205
+ }
206
+
207
+ // Singleton event bus for the runtime
208
+ const bus = new EventBus();
209
+
210
+ module.exports = { EventBus, bus };
@@ -0,0 +1,233 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * WAB Runtime - Main Entry Point
5
+ *
6
+ * The Runtime is the core of the Agent OS. It provides:
7
+ * - Task scheduling with retries and timeouts
8
+ * - Agent state management with checkpoints
9
+ * - Execution sandboxing
10
+ * - Event-driven architecture
11
+ *
12
+ * This is NOT a browser wrapper - it's a real execution runtime
13
+ * comparable to Ray or Temporal.
14
+ */
15
+
16
+ const { EventBus, bus } = require('./event-bus');
17
+ const { Scheduler, TaskState } = require('./scheduler');
18
+ const { StateManager } = require('./state-manager');
19
+ const { ExecutionSandbox } = require('./sandbox');
20
+
21
+ class WABRuntime {
22
+ constructor(options = {}) {
23
+ this.scheduler = new Scheduler({
24
+ maxConcurrent: options.maxConcurrentTasks || 20,
25
+ maxQueueSize: options.maxQueueSize || 1000,
26
+ defaultRetries: options.defaultRetries || 3,
27
+ defaultTimeout: options.defaultTimeout || 30000,
28
+ });
29
+
30
+ this.state = new StateManager({
31
+ maxCheckpoints: options.maxCheckpoints || 50,
32
+ ttl: options.stateTTL || 24 * 3600_000,
33
+ });
34
+
35
+ this.sandbox = new ExecutionSandbox({
36
+ maxConcurrent: options.maxSandboxes || 100,
37
+ defaultTimeout: options.sandboxTimeout || 30000,
38
+ });
39
+
40
+ this.events = bus;
41
+ this._started = false;
42
+ this._cleanupTimer = null;
43
+
44
+ // Register built-in task handlers
45
+ this._registerBuiltinHandlers();
46
+
47
+ // Wire events
48
+ this._wireEventHandlers();
49
+ }
50
+
51
+ /**
52
+ * Start the runtime
53
+ */
54
+ start() {
55
+ if (this._started) return;
56
+ this._started = true;
57
+
58
+ // Periodic cleanup
59
+ this._cleanupTimer = setInterval(() => {
60
+ this.state.cleanup();
61
+ this.sandbox.cleanup();
62
+ }, 600_000); // 10 min
63
+ if (this._cleanupTimer.unref) this._cleanupTimer.unref();
64
+
65
+ this.events.emit('runtime.started', {
66
+ timestamp: Date.now(),
67
+ capabilities: this.getCapabilities(),
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Stop the runtime
73
+ */
74
+ stop() {
75
+ if (!this._started) return;
76
+ this._started = false;
77
+ if (this._cleanupTimer) clearInterval(this._cleanupTimer);
78
+ this.events.emit('runtime.stopped', { timestamp: Date.now() });
79
+ }
80
+
81
+ /**
82
+ * Submit a task
83
+ */
84
+ submitTask(task) {
85
+ if (!this._started) throw new Error('Runtime not started');
86
+ return this.scheduler.submit(task);
87
+ }
88
+
89
+ /**
90
+ * Execute a task in a sandbox with full lifecycle
91
+ */
92
+ async executeInSandbox(taskId, handler, options = {}) {
93
+ // Create sandbox
94
+ const sbx = this.sandbox.create(taskId, options);
95
+
96
+ // Save initial state
97
+ this.state.save(taskId, { status: 'sandbox_created', sandboxId: sbx.id });
98
+ this.state.checkpoint(taskId, 'pre-execution');
99
+
100
+ try {
101
+ const result = await this.sandbox.execute(sbx.id, handler);
102
+
103
+ if (result.success) {
104
+ this.state.save(taskId, { status: 'completed', result: result.result });
105
+ } else {
106
+ this.state.save(taskId, { status: 'failed', error: result.error });
107
+ }
108
+
109
+ return result;
110
+ } finally {
111
+ // Cleanup sandbox after a delay
112
+ setTimeout(() => this.sandbox.destroy(sbx.id), 60000);
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Register a task type handler
118
+ */
119
+ registerTaskHandler(taskType, handler) {
120
+ this.scheduler.registerHandler(taskType, handler);
121
+ }
122
+
123
+ /**
124
+ * Get runtime capabilities for protocol exposure
125
+ */
126
+ getCapabilities() {
127
+ return {
128
+ scheduler: { maxConcurrent: 20, retries: true, deadlines: true, dependencies: true },
129
+ state: { checkpoints: true, rollback: true, ttl: true },
130
+ sandbox: { isolation: true, resourceLimits: true, auditTrail: true },
131
+ events: { async: true, replay: true, wildcards: true, deadLetter: true },
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Get runtime health and stats
137
+ */
138
+ getHealth() {
139
+ return {
140
+ status: this._started ? 'running' : 'stopped',
141
+ uptime: this._started ? Date.now() : 0,
142
+ scheduler: this.scheduler.getStats(),
143
+ state: this.state.getStats(),
144
+ sandbox: this.sandbox.getStats(),
145
+ events: this.events.getStats(),
146
+ };
147
+ }
148
+
149
+ // ─── Built-in Handlers ──────────────────────────────────────────────────
150
+
151
+ _registerBuiltinHandlers() {
152
+ // Browser task handler (delegated to data plane)
153
+ this.scheduler.registerHandler('browser', async (params, ctx) => {
154
+ ctx.reportProgress(10, 0);
155
+ // Browser tasks are handled by data-plane executor
156
+ return { delegated: 'data-plane', params };
157
+ });
158
+
159
+ // API task handler
160
+ this.scheduler.registerHandler('api', async (params, ctx) => {
161
+ ctx.reportProgress(10, 0);
162
+ return { delegated: 'data-plane', params };
163
+ });
164
+
165
+ // Extraction task handler
166
+ this.scheduler.registerHandler('extraction', async (params, ctx) => {
167
+ ctx.reportProgress(10, 0);
168
+ return { delegated: 'data-plane', params };
169
+ });
170
+
171
+ // Workflow (composite) handler
172
+ this.scheduler.registerHandler('workflow', async (params, ctx) => {
173
+ const results = [];
174
+ for (let i = 0; i < ctx.steps.length; i++) {
175
+ ctx.reportProgress(Math.floor((i / ctx.steps.length) * 100), i);
176
+ ctx.checkpoint({ step: i, results });
177
+
178
+ const step = ctx.steps[i];
179
+ const handler = this.scheduler._handlers.get(step.type || 'general');
180
+ if (handler) {
181
+ const result = await handler(step.params || {}, ctx);
182
+ results.push({ step: i, result });
183
+ }
184
+ }
185
+ ctx.reportProgress(100, ctx.steps.length);
186
+ return { steps: results };
187
+ });
188
+
189
+ // Composite task (parallel execution)
190
+ this.scheduler.registerHandler('composite', async (params, ctx) => {
191
+ if (!params.tasks || !Array.isArray(params.tasks)) {
192
+ throw new Error('Composite task requires tasks array');
193
+ }
194
+ const promises = params.tasks.map((t, i) => {
195
+ const handler = this.scheduler._handlers.get(t.type || 'general');
196
+ if (!handler) return { step: i, error: `No handler for: ${t.type}` };
197
+ return handler(t.params || {}, ctx).then(r => ({ step: i, result: r }));
198
+ });
199
+ return { results: await Promise.allSettled(promises) };
200
+ });
201
+
202
+ // General purpose handler (pass-through)
203
+ this.scheduler.registerHandler('general', async (params) => {
204
+ return { received: params };
205
+ });
206
+ }
207
+
208
+ // ─── Event Wiring ────────────────────────────────────────────────────────
209
+
210
+ _wireEventHandlers() {
211
+ // Track task states in state manager
212
+ this.events.on('task.started', (data) => {
213
+ this.state.save(data.taskId, { status: 'running', startedAt: Date.now(), attempt: data.attempt });
214
+ });
215
+
216
+ this.events.on('task.completed', (data) => {
217
+ this.state.merge(data.taskId, { status: 'completed', duration: data.duration });
218
+ });
219
+
220
+ this.events.on('task.failed', (data) => {
221
+ this.state.merge(data.taskId, { status: 'failed', error: data.error, attempts: data.attempts });
222
+ });
223
+
224
+ this.events.on('task.progress', (data) => {
225
+ this.state.merge(data.taskId, { progress: data.progress, currentStep: data.step });
226
+ });
227
+ }
228
+ }
229
+
230
+ // Singleton runtime
231
+ const runtime = new WABRuntime();
232
+
233
+ module.exports = { WABRuntime, runtime, EventBus, bus, Scheduler, TaskState, StateManager, ExecutionSandbox };
@@ -0,0 +1,264 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Deterministic Replay Engine
5
+ *
6
+ * Records all task inputs/outputs/side-effects for deterministic replay.
7
+ * Enables debugging, testing, and verification of agent workflows.
8
+ */
9
+
10
+ const crypto = require('crypto');
11
+ const { bus } = require('../runtime/event-bus');
12
+
13
+ class ReplayEngine {
14
+ constructor() {
15
+ this._recordings = new Map(); // taskId → Recording
16
+ this._maxRecordings = 5000;
17
+ this._recordingEnabled = true;
18
+ }
19
+
20
+ /**
21
+ * Start recording a task execution
22
+ */
23
+ startRecording(taskId, input) {
24
+ if (!this._recordingEnabled) return null;
25
+
26
+ const recording = {
27
+ id: `rec_${crypto.randomBytes(8).toString('hex')}`,
28
+ taskId,
29
+ input: this._deepClone(input),
30
+ steps: [],
31
+ sideEffects: [],
32
+ startedAt: Date.now(),
33
+ completedAt: null,
34
+ output: null,
35
+ error: null,
36
+ checksum: null,
37
+ replayable: true,
38
+ };
39
+
40
+ this._recordings.set(taskId, recording);
41
+ this._evict();
42
+ return recording.id;
43
+ }
44
+
45
+ /**
46
+ * Record a step in the execution
47
+ */
48
+ recordStep(taskId, step) {
49
+ const rec = this._recordings.get(taskId);
50
+ if (!rec) return;
51
+
52
+ rec.steps.push({
53
+ index: rec.steps.length,
54
+ type: step.type,
55
+ action: step.action,
56
+ input: this._deepClone(step.input),
57
+ output: this._deepClone(step.output),
58
+ duration: step.duration || 0,
59
+ timestamp: Date.now(),
60
+ });
61
+ }
62
+
63
+ /**
64
+ * Record a side effect (network call, DOM mutation, storage write, etc.)
65
+ */
66
+ recordSideEffect(taskId, effect) {
67
+ const rec = this._recordings.get(taskId);
68
+ if (!rec) return;
69
+
70
+ rec.sideEffects.push({
71
+ index: rec.sideEffects.length,
72
+ type: effect.type, // 'network', 'dom', 'storage', 'event'
73
+ target: effect.target,
74
+ data: this._deepClone(effect.data),
75
+ timestamp: Date.now(),
76
+ reversible: effect.reversible !== false,
77
+ });
78
+ }
79
+
80
+ /**
81
+ * Complete a recording
82
+ */
83
+ completeRecording(taskId, output, error = null) {
84
+ const rec = this._recordings.get(taskId);
85
+ if (!rec) return null;
86
+
87
+ rec.completedAt = Date.now();
88
+ rec.output = this._deepClone(output);
89
+ rec.error = error ? { message: error.message, code: error.code } : null;
90
+ rec.checksum = this._computeChecksum(rec);
91
+
92
+ bus.emit('replay.recording.complete', { taskId, recordingId: rec.id, steps: rec.steps.length });
93
+ return rec;
94
+ }
95
+
96
+ /**
97
+ * Replay a recorded task
98
+ * Returns the replay plan (steps to execute) with recorded outputs for verification
99
+ */
100
+ async replay(taskId, options = {}) {
101
+ const rec = this._recordings.get(taskId);
102
+ if (!rec) throw new Error(`No recording found for task ${taskId}`);
103
+ if (!rec.completedAt) throw new Error('Recording not yet complete');
104
+
105
+ const replayResult = {
106
+ recordingId: rec.id,
107
+ taskId,
108
+ originalInput: rec.input,
109
+ originalOutput: rec.output,
110
+ steps: [],
111
+ match: true,
112
+ verificationMode: options.verify !== false,
113
+ replayedAt: Date.now(),
114
+ };
115
+
116
+ // In verification mode, run each step and compare outputs
117
+ if (options.executor && options.verify !== false) {
118
+ for (const step of rec.steps) {
119
+ try {
120
+ const replayOutput = await options.executor(step);
121
+ const outputMatch = this._deepEqual(step.output, replayOutput);
122
+
123
+ replayResult.steps.push({
124
+ index: step.index,
125
+ action: step.action,
126
+ originalOutput: step.output,
127
+ replayOutput,
128
+ match: outputMatch,
129
+ });
130
+
131
+ if (!outputMatch) {
132
+ replayResult.match = false;
133
+ if (!options.continueOnMismatch) break;
134
+ }
135
+ } catch (err) {
136
+ replayResult.steps.push({
137
+ index: step.index,
138
+ action: step.action,
139
+ error: err.message,
140
+ match: false,
141
+ });
142
+ replayResult.match = false;
143
+ if (!options.continueOnMismatch) break;
144
+ }
145
+ }
146
+ } else {
147
+ // Dry-run mode: just return the recorded steps
148
+ replayResult.steps = rec.steps.map(s => ({
149
+ index: s.index,
150
+ action: s.action,
151
+ input: s.input,
152
+ output: s.output,
153
+ duration: s.duration,
154
+ }));
155
+ }
156
+
157
+ bus.emit('replay.completed', { taskId, match: replayResult.match });
158
+ return replayResult;
159
+ }
160
+
161
+ /**
162
+ * Get recording
163
+ */
164
+ getRecording(taskId) {
165
+ return this._recordings.get(taskId) || null;
166
+ }
167
+
168
+ /**
169
+ * List recordings
170
+ */
171
+ listRecordings(limit = 50) {
172
+ const all = Array.from(this._recordings.values());
173
+ return all.slice(-limit).reverse().map(r => ({
174
+ id: r.id,
175
+ taskId: r.taskId,
176
+ steps: r.steps.length,
177
+ sideEffects: r.sideEffects.length,
178
+ startedAt: r.startedAt,
179
+ completedAt: r.completedAt,
180
+ hasError: !!r.error,
181
+ checksum: r.checksum,
182
+ }));
183
+ }
184
+
185
+ /**
186
+ * Compare two recordings
187
+ */
188
+ diff(taskId1, taskId2) {
189
+ const r1 = this._recordings.get(taskId1);
190
+ const r2 = this._recordings.get(taskId2);
191
+ if (!r1 || !r2) return null;
192
+
193
+ const diffs = [];
194
+ const maxSteps = Math.max(r1.steps.length, r2.steps.length);
195
+
196
+ for (let i = 0; i < maxSteps; i++) {
197
+ const s1 = r1.steps[i];
198
+ const s2 = r2.steps[i];
199
+
200
+ if (!s1 || !s2) {
201
+ diffs.push({ index: i, type: 'missing', in: s1 ? 'recording2' : 'recording1' });
202
+ } else if (!this._deepEqual(s1.output, s2.output)) {
203
+ diffs.push({ index: i, type: 'output_mismatch', action: s1.action, output1: s1.output, output2: s2.output });
204
+ }
205
+ }
206
+
207
+ return {
208
+ match: diffs.length === 0,
209
+ inputMatch: this._deepEqual(r1.input, r2.input),
210
+ outputMatch: this._deepEqual(r1.output, r2.output),
211
+ diffs,
212
+ };
213
+ }
214
+
215
+ /**
216
+ * Enable/disable recording
217
+ */
218
+ setEnabled(enabled) {
219
+ this._recordingEnabled = enabled;
220
+ }
221
+
222
+ getStats() {
223
+ return {
224
+ totalRecordings: this._recordings.size,
225
+ enabled: this._recordingEnabled,
226
+ maxRecordings: this._maxRecordings,
227
+ };
228
+ }
229
+
230
+ // ── Internal ──
231
+
232
+ _computeChecksum(rec) {
233
+ const data = JSON.stringify({
234
+ input: rec.input,
235
+ steps: rec.steps.map(s => ({ action: s.action, output: s.output })),
236
+ output: rec.output,
237
+ });
238
+ return crypto.createHash('sha256').update(data).digest('hex').slice(0, 16);
239
+ }
240
+
241
+ _deepClone(obj) {
242
+ if (obj === undefined || obj === null) return obj;
243
+ try {
244
+ return JSON.parse(JSON.stringify(obj));
245
+ } catch {
246
+ return obj;
247
+ }
248
+ }
249
+
250
+ _deepEqual(a, b) {
251
+ return JSON.stringify(a) === JSON.stringify(b);
252
+ }
253
+
254
+ _evict() {
255
+ if (this._recordings.size <= this._maxRecordings) return;
256
+ const keys = Array.from(this._recordings.keys());
257
+ const toRemove = keys.slice(0, keys.length - this._maxRecordings);
258
+ for (const k of toRemove) this._recordings.delete(k);
259
+ }
260
+ }
261
+
262
+ const replayEngine = new ReplayEngine();
263
+
264
+ module.exports = { ReplayEngine, replayEngine };