web-agent-bridge 2.9.0 → 3.0.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.
@@ -4,15 +4,19 @@
4
4
  * WAB Runtime - Task Scheduler
5
5
  *
6
6
  * Distributes and manages task execution with:
7
- * - Priority queue
7
+ * - External queue backend (SQLite/Redis/Memory)
8
+ * - Priority queue with persistent storage
8
9
  * - Retry logic with exponential backoff
9
10
  * - Timeout management
10
11
  * - Dependency resolution
11
12
  * - Concurrency control
13
+ * - Deterministic replay integration
14
+ * - Container isolation support
12
15
  */
13
16
 
14
17
  const crypto = require('crypto');
15
18
  const { bus } = require('./event-bus');
19
+ const { createQueue } = require('./queue');
16
20
 
17
21
  // Task states
18
22
  const TaskState = {
@@ -28,10 +32,13 @@ const TaskState = {
28
32
 
29
33
  class Scheduler {
30
34
  constructor(options = {}) {
31
- this._queue = []; // priority queue
35
+ this._queue = []; // fallback in-memory queue
36
+ this._externalQueue = null; // external queue backend
32
37
  this._running = new Map(); // taskId → task
33
38
  this._completed = new Map(); // taskId → result (limited buffer)
34
39
  this._handlers = new Map(); // task type → handler function
40
+ this._replayEngine = null; // optional replay integration
41
+ this._containerRunner = null; // optional container isolation
35
42
  this._maxConcurrent = options.maxConcurrent || 20;
36
43
  this._maxQueueSize = options.maxQueueSize || 1000;
37
44
  this._maxCompleted = options.maxCompleted || 500;
@@ -39,6 +46,31 @@ class Scheduler {
39
46
  this._defaultTimeout = options.defaultTimeout || 30000;
40
47
  this._processing = false;
41
48
  this._stats = { submitted: 0, completed: 0, failed: 0, retried: 0, cancelled: 0, timedOut: 0 };
49
+
50
+ // Initialize external queue
51
+ try {
52
+ this._externalQueue = createQueue('scheduler', {
53
+ maxRetries: this._defaultRetries,
54
+ processTimeout: this._defaultTimeout,
55
+ });
56
+ } catch {
57
+ // Fallback to in-memory
58
+ this._externalQueue = null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Attach the replay engine for deterministic recording
64
+ */
65
+ setReplayEngine(engine) {
66
+ this._replayEngine = engine;
67
+ }
68
+
69
+ /**
70
+ * Attach the container runner for process isolation
71
+ */
72
+ setContainerRunner(runner) {
73
+ this._containerRunner = runner;
42
74
  }
43
75
 
44
76
  /**
@@ -52,10 +84,6 @@ class Scheduler {
52
84
  * Submit a task to the scheduler
53
85
  */
54
86
  submit(task) {
55
- if (this._queue.length >= this._maxQueueSize) {
56
- throw new Error('Task queue is full');
57
- }
58
-
59
87
  const taskEntry = {
60
88
  id: task.id || `task_${crypto.randomBytes(16).toString('hex')}`,
61
89
  type: task.type || 'general',
@@ -72,6 +100,7 @@ class Scheduler {
72
100
  retriesLeft: task.retries !== undefined ? task.retries : this._defaultRetries,
73
101
  timeout: task.timeout || this._defaultTimeout,
74
102
  deadline: task.deadline || null,
103
+ isolate: task.isolate || false, // run in container if true
75
104
 
76
105
  // Dependencies
77
106
  dependsOn: task.dependsOn || [],
@@ -94,16 +123,31 @@ class Scheduler {
94
123
  checkpoints: [],
95
124
  };
96
125
 
97
- // Insert by priority (higher priority = earlier in queue)
98
- let inserted = false;
99
- for (let i = 0; i < this._queue.length; i++) {
100
- if (taskEntry.priority > this._queue[i].priority) {
101
- this._queue.splice(i, 0, taskEntry);
102
- inserted = true;
103
- break;
126
+ // Add to queue (external or in-memory)
127
+ if (this._externalQueue) {
128
+ this._externalQueue.enqueue({
129
+ type: taskEntry.type,
130
+ data: taskEntry,
131
+ priority: taskEntry.priority,
132
+ maxAttempts: taskEntry.retries + 1,
133
+ timeoutMs: taskEntry.timeout,
134
+ groupId: taskEntry.agentId || null,
135
+ });
136
+ } else {
137
+ if (this._queue.length >= this._maxQueueSize) {
138
+ throw new Error('Task queue is full');
104
139
  }
140
+ // Insert by priority (higher priority = earlier in queue)
141
+ let inserted = false;
142
+ for (let i = 0; i < this._queue.length; i++) {
143
+ if (taskEntry.priority > this._queue[i].priority) {
144
+ this._queue.splice(i, 0, taskEntry);
145
+ inserted = true;
146
+ break;
147
+ }
148
+ }
149
+ if (!inserted) this._queue.push(taskEntry);
105
150
  }
106
- if (!inserted) this._queue.push(taskEntry);
107
151
 
108
152
  this._stats.submitted++;
109
153
  bus.emit('task.queued', { taskId: taskEntry.id, type: taskEntry.type, priority: taskEntry.priority });
@@ -125,28 +169,43 @@ class Scheduler {
125
169
  if (this._processing) return;
126
170
  this._processing = true;
127
171
 
128
- while (this._queue.length > 0 && this._running.size < this._maxConcurrent) {
129
- const task = this._findNextReady();
130
- if (!task) break;
131
-
132
- // Remove from queue
133
- const idx = this._queue.indexOf(task);
134
- if (idx !== -1) this._queue.splice(idx, 1);
135
-
136
- // Check deadline
137
- if (task.deadline && Date.now() > task.deadline) {
138
- task.state = TaskState.CANCELLED;
139
- task.error = { code: 'DEADLINE_EXCEEDED', message: 'Task deadline has passed' };
140
- bus.emit('task.cancelled', { taskId: task.id, reason: 'deadline' });
141
- continue;
172
+ try {
173
+ while (this._running.size < this._maxConcurrent) {
174
+ let task = null;
175
+
176
+ if (this._externalQueue) {
177
+ // Dequeue from external queue
178
+ const item = this._externalQueue.dequeue();
179
+ if (!item) break;
180
+ task = item.data;
181
+ task._queueItemId = item.id;
182
+ } else {
183
+ // Dequeue from in-memory queue
184
+ if (this._queue.length === 0) break;
185
+ task = this._findNextReady();
186
+ if (!task) break;
187
+ const idx = this._queue.indexOf(task);
188
+ if (idx !== -1) this._queue.splice(idx, 1);
189
+ }
190
+
191
+ // Check deadline
192
+ if (task.deadline && Date.now() > task.deadline) {
193
+ task.state = TaskState.CANCELLED;
194
+ task.error = { code: 'DEADLINE_EXCEEDED', message: 'Task deadline has passed' };
195
+ if (this._externalQueue && task._queueItemId) {
196
+ this._externalQueue.fail(task._queueItemId, 'Deadline exceeded');
197
+ }
198
+ bus.emit('task.cancelled', { taskId: task.id, reason: 'deadline' });
199
+ continue;
200
+ }
201
+
202
+ // Execute
203
+ this._running.set(task.id, task);
204
+ this._executeTask(task); // fire and forget
142
205
  }
143
-
144
- // Execute
145
- this._running.set(task.id, task);
146
- this._executeTask(task); // fire and forget
206
+ } finally {
207
+ this._processing = false;
147
208
  }
148
-
149
- this._processing = false;
150
209
  }
151
210
 
152
211
  /**
@@ -174,39 +233,90 @@ class Scheduler {
174
233
 
175
234
  bus.emit('task.started', { taskId: task.id, attempt: task.attempt, type: task.type });
176
235
 
236
+ // Start deterministic recording
237
+ if (this._replayEngine) {
238
+ this._replayEngine.startRecording(task.id, {
239
+ type: task.type,
240
+ params: task.params,
241
+ objective: task.objective,
242
+ attempt: task.attempt,
243
+ });
244
+ }
245
+
177
246
  const handler = this._handlers.get(task.type);
178
247
  if (!handler) {
179
248
  task.state = TaskState.FAILED;
180
249
  task.error = { code: 'NO_HANDLER', message: `No handler for task type: ${task.type}` };
250
+ if (this._replayEngine) {
251
+ this._replayEngine.completeRecording(task.id, null, task.error);
252
+ }
181
253
  this._finishTask(task);
182
254
  return;
183
255
  }
184
256
 
185
257
  try {
186
- const result = await _withTimeout(
187
- handler(task.params, {
188
- taskId: task.id,
189
- agentId: task.agentId,
190
- siteId: task.siteId,
191
- traceId: task.traceId,
192
- steps: task.steps,
193
- attempt: task.attempt,
194
- reportProgress: (pct, step) => {
195
- task.progress = pct;
196
- if (step !== undefined) task.currentStep = step;
197
- bus.emit('task.progress', { taskId: task.id, progress: pct, step });
198
- },
199
- checkpoint: (data) => {
200
- task.checkpoints.push({ data, timestamp: Date.now(), step: task.currentStep });
201
- },
202
- }),
203
- task.timeout
204
- );
258
+ let result;
259
+
260
+ const ctx = {
261
+ taskId: task.id,
262
+ agentId: task.agentId,
263
+ siteId: task.siteId,
264
+ traceId: task.traceId,
265
+ steps: task.steps,
266
+ attempt: task.attempt,
267
+ reportProgress: (pct, step) => {
268
+ task.progress = pct;
269
+ if (step !== undefined) task.currentStep = step;
270
+ bus.emit('task.progress', { taskId: task.id, progress: pct, step });
271
+ },
272
+ checkpoint: (data) => {
273
+ task.checkpoints.push({ data, timestamp: Date.now(), step: task.currentStep });
274
+ if (this._replayEngine) {
275
+ this._replayEngine.saveCheckpoint(task.id, `step-${task.currentStep}`, data);
276
+ }
277
+ },
278
+ recordStep: (step) => {
279
+ if (this._replayEngine) {
280
+ this._replayEngine.recordStep(task.id, step);
281
+ }
282
+ },
283
+ recordSideEffect: (effect) => {
284
+ if (this._replayEngine) {
285
+ this._replayEngine.recordSideEffect(task.id, effect);
286
+ }
287
+ },
288
+ };
289
+
290
+ // Execute in container if isolation requested
291
+ if (task.isolate && this._containerRunner && typeof task.params === 'object' && task.params._code) {
292
+ const containerResult = await this._containerRunner.runInProcess(task.id, task.params._code, {
293
+ params: task.params,
294
+ timeout: task.timeout,
295
+ maxMemory: task.params._maxMemory || 256 * 1024 * 1024,
296
+ });
297
+ if (!containerResult.success) {
298
+ throw new Error(containerResult.error || 'Container execution failed');
299
+ }
300
+ result = containerResult.result;
301
+ } else {
302
+ result = await _withTimeout(handler(task.params, ctx), task.timeout);
303
+ }
205
304
 
206
305
  task.state = TaskState.COMPLETED;
207
306
  task.result = result;
208
307
  task.progress = 100;
209
308
  this._stats.completed++;
309
+
310
+ // Complete recording
311
+ if (this._replayEngine) {
312
+ this._replayEngine.completeRecording(task.id, result);
313
+ }
314
+
315
+ // Mark complete in external queue
316
+ if (this._externalQueue && task._queueItemId) {
317
+ this._externalQueue.complete(task._queueItemId, result);
318
+ }
319
+
210
320
  bus.emit('task.completed', { taskId: task.id, duration: Date.now() - task.startedAt });
211
321
  } catch (err) {
212
322
  const isTimeout = err.message.includes('timed out');
@@ -222,7 +332,18 @@ class Scheduler {
222
332
 
223
333
  setTimeout(() => {
224
334
  this._running.delete(task.id);
225
- this._queue.unshift(task); // Add back to front of queue
335
+ if (this._externalQueue) {
336
+ // Re-enqueue in external queue
337
+ this._externalQueue.enqueue({
338
+ type: task.type,
339
+ data: task,
340
+ priority: task.priority,
341
+ maxAttempts: task.retriesLeft + 1,
342
+ timeoutMs: task.timeout,
343
+ });
344
+ } else {
345
+ this._queue.unshift(task);
346
+ }
226
347
  this._processQueue();
227
348
  }, backoff);
228
349
  return;
@@ -231,6 +352,17 @@ class Scheduler {
231
352
  task.state = TaskState.FAILED;
232
353
  task.error = { code: isTimeout ? 'TIMEOUT' : 'EXECUTION_ERROR', message: err.message };
233
354
  this._stats.failed++;
355
+
356
+ // Complete recording with error
357
+ if (this._replayEngine) {
358
+ this._replayEngine.completeRecording(task.id, null, err);
359
+ }
360
+
361
+ // Mark failed in external queue
362
+ if (this._externalQueue && task._queueItemId) {
363
+ this._externalQueue.fail(task._queueItemId, err.message);
364
+ }
365
+
234
366
  bus.emit('task.failed', { taskId: task.id, error: err.message, attempts: task.attempt });
235
367
  }
236
368
 
@@ -347,11 +479,18 @@ class Scheduler {
347
479
  * Get scheduler stats
348
480
  */
349
481
  getStats() {
482
+ const queueSize = this._externalQueue
483
+ ? this._externalQueue.size()
484
+ : this._queue.length;
485
+
350
486
  return {
351
487
  ...this._stats,
352
- queueSize: this._queue.length,
488
+ queueSize,
489
+ queueBackend: this._externalQueue ? this._externalQueue.constructor.name : 'memory',
353
490
  runningCount: this._running.size,
354
491
  completedBufferSize: this._completed.size,
492
+ replayEnabled: !!this._replayEngine,
493
+ containerEnabled: !!this._containerRunner,
355
494
  };
356
495
  }
357
496