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.
- package/README.ar.md +18 -0
- package/README.md +32 -0
- package/package.json +1 -1
- package/public/.well-known/agent-tools.json +180 -0
- package/sdk/index.d.ts +170 -0
- package/sdk/index.js +246 -1
- package/sdk/package.json +1 -1
- package/server/adapters/index.js +520 -0
- package/server/control-plane/index.js +301 -0
- package/server/data-plane/index.js +354 -0
- package/server/index.js +6 -0
- package/server/llm/index.js +404 -0
- package/server/migrations/004_agent_os.sql +158 -0
- package/server/observability/failure-analysis.js +337 -0
- package/server/observability/index.js +394 -0
- package/server/protocol/capabilities.js +223 -0
- package/server/protocol/index.js +243 -0
- package/server/protocol/schema.js +584 -0
- package/server/registry/certification.js +271 -0
- package/server/registry/index.js +326 -0
- package/server/routes/runtime.js +1136 -0
- package/server/runtime/event-bus.js +210 -0
- package/server/runtime/index.js +233 -0
- package/server/runtime/replay.js +264 -0
- package/server/runtime/sandbox.js +266 -0
- package/server/runtime/scheduler.js +395 -0
- package/server/runtime/session-engine.js +293 -0
- package/server/runtime/state-manager.js +188 -0
- package/server/security/index.js +368 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WAB Runtime - Execution Sandbox
|
|
5
|
+
*
|
|
6
|
+
* Isolates task execution. Each task runs in its own sandbox with:
|
|
7
|
+
* - Resource limits (memory, CPU time, network)
|
|
8
|
+
* - Permission boundaries
|
|
9
|
+
* - Isolated state
|
|
10
|
+
* - Audit trail
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const crypto = require('crypto');
|
|
14
|
+
|
|
15
|
+
class ExecutionSandbox {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this._sandboxes = new Map();
|
|
18
|
+
this._maxConcurrent = options.maxConcurrent || 100;
|
|
19
|
+
this._defaultTimeout = options.defaultTimeout || 30000;
|
|
20
|
+
this._stats = { created: 0, completed: 0, failed: 0, timedOut: 0 };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a new sandbox for a task
|
|
25
|
+
*/
|
|
26
|
+
create(taskId, options = {}) {
|
|
27
|
+
if (this._sandboxes.size >= this._maxConcurrent) {
|
|
28
|
+
throw new Error('Maximum concurrent sandbox limit reached');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const sandbox = {
|
|
32
|
+
id: `sbx_${crypto.randomBytes(12).toString('hex')}`,
|
|
33
|
+
taskId,
|
|
34
|
+
agentId: options.agentId || null,
|
|
35
|
+
siteId: options.siteId || null,
|
|
36
|
+
|
|
37
|
+
// Resource limits
|
|
38
|
+
limits: {
|
|
39
|
+
timeout: options.timeout || this._defaultTimeout,
|
|
40
|
+
maxMemory: options.maxMemory || 128 * 1024 * 1024, // 128MB
|
|
41
|
+
maxNetworkCalls: options.maxNetworkCalls || 100,
|
|
42
|
+
maxDomOperations: options.maxDomOperations || 1000,
|
|
43
|
+
allowedDomains: options.allowedDomains || ['*'],
|
|
44
|
+
blockedSelectors: options.blockedSelectors || [],
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Runtime state
|
|
48
|
+
state: 'created',
|
|
49
|
+
usage: {
|
|
50
|
+
networkCalls: 0,
|
|
51
|
+
domOperations: 0,
|
|
52
|
+
startedAt: null,
|
|
53
|
+
completedAt: null,
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Permission boundaries
|
|
57
|
+
capabilities: new Set(options.capabilities || []),
|
|
58
|
+
|
|
59
|
+
// Audit trail
|
|
60
|
+
audit: [],
|
|
61
|
+
|
|
62
|
+
// Isolated key-value store
|
|
63
|
+
store: new Map(),
|
|
64
|
+
|
|
65
|
+
createdAt: Date.now(),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
this._sandboxes.set(sandbox.id, sandbox);
|
|
69
|
+
this._stats.created++;
|
|
70
|
+
return sandbox;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Execute a function within a sandbox
|
|
75
|
+
*/
|
|
76
|
+
async execute(sandboxId, fn) {
|
|
77
|
+
const sandbox = this._sandboxes.get(sandboxId);
|
|
78
|
+
if (!sandbox) throw new Error(`Sandbox not found: ${sandboxId}`);
|
|
79
|
+
if (sandbox.state !== 'created' && sandbox.state !== 'running') {
|
|
80
|
+
throw new Error(`Sandbox ${sandboxId} is in state ${sandbox.state}, cannot execute`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
sandbox.state = 'running';
|
|
84
|
+
sandbox.usage.startedAt = Date.now();
|
|
85
|
+
|
|
86
|
+
// Create scoped context for the function
|
|
87
|
+
const context = this._createContext(sandbox);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const result = await _withTimeout(fn(context), sandbox.limits.timeout);
|
|
91
|
+
sandbox.state = 'completed';
|
|
92
|
+
sandbox.usage.completedAt = Date.now();
|
|
93
|
+
this._stats.completed++;
|
|
94
|
+
|
|
95
|
+
sandbox.audit.push({
|
|
96
|
+
action: 'complete',
|
|
97
|
+
timestamp: Date.now(),
|
|
98
|
+
duration: sandbox.usage.completedAt - sandbox.usage.startedAt,
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
return { success: true, result, sandbox: this._getSandboxSummary(sandbox) };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
sandbox.state = err.message.includes('timed out') ? 'timeout' : 'failed';
|
|
104
|
+
sandbox.usage.completedAt = Date.now();
|
|
105
|
+
|
|
106
|
+
if (sandbox.state === 'timeout') this._stats.timedOut++;
|
|
107
|
+
else this._stats.failed++;
|
|
108
|
+
|
|
109
|
+
sandbox.audit.push({
|
|
110
|
+
action: sandbox.state,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
error: err.message,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
return { success: false, error: err.message, sandbox: this._getSandboxSummary(sandbox) };
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Create a scoped execution context
|
|
121
|
+
*/
|
|
122
|
+
_createContext(sandbox) {
|
|
123
|
+
const self = this;
|
|
124
|
+
return {
|
|
125
|
+
taskId: sandbox.taskId,
|
|
126
|
+
agentId: sandbox.agentId,
|
|
127
|
+
siteId: sandbox.siteId,
|
|
128
|
+
|
|
129
|
+
// Capability check
|
|
130
|
+
hasCapability(cap) {
|
|
131
|
+
return sandbox.capabilities.has(cap);
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
requireCapability(cap) {
|
|
135
|
+
if (!sandbox.capabilities.has(cap)) {
|
|
136
|
+
throw new Error(`Sandbox lacks capability: ${cap}`);
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
// Domain check
|
|
141
|
+
checkDomain(domain) {
|
|
142
|
+
if (sandbox.limits.allowedDomains[0] === '*') return true;
|
|
143
|
+
return sandbox.limits.allowedDomains.some(d => domain.endsWith(d));
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
// Resource tracking
|
|
147
|
+
trackNetworkCall() {
|
|
148
|
+
sandbox.usage.networkCalls++;
|
|
149
|
+
if (sandbox.usage.networkCalls > sandbox.limits.maxNetworkCalls) {
|
|
150
|
+
throw new Error('Network call limit exceeded');
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
trackDomOperation() {
|
|
155
|
+
sandbox.usage.domOperations++;
|
|
156
|
+
if (sandbox.usage.domOperations > sandbox.limits.maxDomOperations) {
|
|
157
|
+
throw new Error('DOM operation limit exceeded');
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
// Isolated store
|
|
162
|
+
set(key, value) { sandbox.store.set(key, value); },
|
|
163
|
+
get(key) { return sandbox.store.get(key); },
|
|
164
|
+
|
|
165
|
+
// Audit
|
|
166
|
+
log(action, details) {
|
|
167
|
+
sandbox.audit.push({ action, details, timestamp: Date.now() });
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
// Selector validation
|
|
171
|
+
checkSelector(selector) {
|
|
172
|
+
for (const blocked of sandbox.limits.blockedSelectors) {
|
|
173
|
+
if (selector.includes(blocked)) {
|
|
174
|
+
throw new Error(`Selector blocked by sandbox policy: ${blocked}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return true;
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
// Read sandbox time remaining
|
|
181
|
+
get timeRemaining() {
|
|
182
|
+
if (!sandbox.usage.startedAt) return sandbox.limits.timeout;
|
|
183
|
+
return Math.max(0, sandbox.limits.timeout - (Date.now() - sandbox.usage.startedAt));
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get sandbox summary (safe to expose)
|
|
190
|
+
*/
|
|
191
|
+
_getSandboxSummary(sandbox) {
|
|
192
|
+
return {
|
|
193
|
+
id: sandbox.id,
|
|
194
|
+
taskId: sandbox.taskId,
|
|
195
|
+
state: sandbox.state,
|
|
196
|
+
usage: { ...sandbox.usage },
|
|
197
|
+
auditCount: sandbox.audit.length,
|
|
198
|
+
duration: sandbox.usage.completedAt
|
|
199
|
+
? sandbox.usage.completedAt - sandbox.usage.startedAt
|
|
200
|
+
: null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Destroy a sandbox and free resources
|
|
206
|
+
*/
|
|
207
|
+
destroy(sandboxId) {
|
|
208
|
+
const sandbox = this._sandboxes.get(sandboxId);
|
|
209
|
+
if (sandbox) {
|
|
210
|
+
sandbox.store.clear();
|
|
211
|
+
this._sandboxes.delete(sandboxId);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Get audit trail for a sandbox
|
|
217
|
+
*/
|
|
218
|
+
getAudit(sandboxId) {
|
|
219
|
+
const sandbox = this._sandboxes.get(sandboxId);
|
|
220
|
+
return sandbox ? [...sandbox.audit] : [];
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* List active sandboxes
|
|
225
|
+
*/
|
|
226
|
+
listActive() {
|
|
227
|
+
const active = [];
|
|
228
|
+
for (const [, sb] of this._sandboxes) {
|
|
229
|
+
if (sb.state === 'created' || sb.state === 'running') {
|
|
230
|
+
active.push(this._getSandboxSummary(sb));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return active;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
getStats() {
|
|
237
|
+
return { ...this._stats, active: this._sandboxes.size };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Cleanup completed/failed sandboxes older than maxAge
|
|
242
|
+
*/
|
|
243
|
+
cleanup(maxAge = 3600_000) {
|
|
244
|
+
const cutoff = Date.now() - maxAge;
|
|
245
|
+
let cleaned = 0;
|
|
246
|
+
for (const [id, sb] of this._sandboxes) {
|
|
247
|
+
if (sb.state !== 'created' && sb.state !== 'running' && sb.createdAt < cutoff) {
|
|
248
|
+
sb.store.clear();
|
|
249
|
+
this._sandboxes.delete(id);
|
|
250
|
+
cleaned++;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return cleaned;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function _withTimeout(promise, ms) {
|
|
258
|
+
if (!ms || ms <= 0) return promise;
|
|
259
|
+
return new Promise((resolve, reject) => {
|
|
260
|
+
const timer = setTimeout(() => reject(new Error(`Sandbox execution timed out after ${ms}ms`)), ms);
|
|
261
|
+
promise.then(r => { clearTimeout(timer); resolve(r); })
|
|
262
|
+
.catch(e => { clearTimeout(timer); reject(e); });
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
module.exports = { ExecutionSandbox };
|
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* WAB Runtime - Task Scheduler
|
|
5
|
+
*
|
|
6
|
+
* Distributes and manages task execution with:
|
|
7
|
+
* - Priority queue
|
|
8
|
+
* - Retry logic with exponential backoff
|
|
9
|
+
* - Timeout management
|
|
10
|
+
* - Dependency resolution
|
|
11
|
+
* - Concurrency control
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
const { bus } = require('./event-bus');
|
|
16
|
+
|
|
17
|
+
// Task states
|
|
18
|
+
const TaskState = {
|
|
19
|
+
QUEUED: 'queued',
|
|
20
|
+
SCHEDULED: 'scheduled',
|
|
21
|
+
RUNNING: 'running',
|
|
22
|
+
PAUSED: 'paused',
|
|
23
|
+
COMPLETED: 'completed',
|
|
24
|
+
FAILED: 'failed',
|
|
25
|
+
CANCELLED: 'cancelled',
|
|
26
|
+
RETRYING: 'retrying',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
class Scheduler {
|
|
30
|
+
constructor(options = {}) {
|
|
31
|
+
this._queue = []; // priority queue
|
|
32
|
+
this._running = new Map(); // taskId → task
|
|
33
|
+
this._completed = new Map(); // taskId → result (limited buffer)
|
|
34
|
+
this._handlers = new Map(); // task type → handler function
|
|
35
|
+
this._maxConcurrent = options.maxConcurrent || 20;
|
|
36
|
+
this._maxQueueSize = options.maxQueueSize || 1000;
|
|
37
|
+
this._maxCompleted = options.maxCompleted || 500;
|
|
38
|
+
this._defaultRetries = options.defaultRetries || 3;
|
|
39
|
+
this._defaultTimeout = options.defaultTimeout || 30000;
|
|
40
|
+
this._processing = false;
|
|
41
|
+
this._stats = { submitted: 0, completed: 0, failed: 0, retried: 0, cancelled: 0, timedOut: 0 };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Register a handler for a task type
|
|
46
|
+
*/
|
|
47
|
+
registerHandler(taskType, handler) {
|
|
48
|
+
this._handlers.set(taskType, handler);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Submit a task to the scheduler
|
|
53
|
+
*/
|
|
54
|
+
submit(task) {
|
|
55
|
+
if (this._queue.length >= this._maxQueueSize) {
|
|
56
|
+
throw new Error('Task queue is full');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const taskEntry = {
|
|
60
|
+
id: task.id || `task_${crypto.randomBytes(16).toString('hex')}`,
|
|
61
|
+
type: task.type || 'general',
|
|
62
|
+
objective: task.objective || '',
|
|
63
|
+
params: task.params || {},
|
|
64
|
+
steps: task.steps || [],
|
|
65
|
+
priority: task.priority || 50,
|
|
66
|
+
agentId: task.agentId || null,
|
|
67
|
+
siteId: task.siteId || null,
|
|
68
|
+
traceId: task.traceId || `trace_${crypto.randomBytes(16).toString('hex')}`,
|
|
69
|
+
|
|
70
|
+
// Execution config
|
|
71
|
+
retries: task.retries !== undefined ? task.retries : this._defaultRetries,
|
|
72
|
+
retriesLeft: task.retries !== undefined ? task.retries : this._defaultRetries,
|
|
73
|
+
timeout: task.timeout || this._defaultTimeout,
|
|
74
|
+
deadline: task.deadline || null,
|
|
75
|
+
|
|
76
|
+
// Dependencies
|
|
77
|
+
dependsOn: task.dependsOn || [],
|
|
78
|
+
|
|
79
|
+
// State
|
|
80
|
+
state: TaskState.QUEUED,
|
|
81
|
+
attempt: 0,
|
|
82
|
+
currentStep: 0,
|
|
83
|
+
result: null,
|
|
84
|
+
error: null,
|
|
85
|
+
progress: 0,
|
|
86
|
+
|
|
87
|
+
// Timestamps
|
|
88
|
+
submittedAt: Date.now(),
|
|
89
|
+
scheduledAt: null,
|
|
90
|
+
startedAt: null,
|
|
91
|
+
completedAt: null,
|
|
92
|
+
|
|
93
|
+
// Checkpoints (for resume/rollback)
|
|
94
|
+
checkpoints: [],
|
|
95
|
+
};
|
|
96
|
+
|
|
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;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!inserted) this._queue.push(taskEntry);
|
|
107
|
+
|
|
108
|
+
this._stats.submitted++;
|
|
109
|
+
bus.emit('task.queued', { taskId: taskEntry.id, type: taskEntry.type, priority: taskEntry.priority });
|
|
110
|
+
|
|
111
|
+
// Try to process immediately
|
|
112
|
+
this._processQueue();
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
taskId: taskEntry.id,
|
|
116
|
+
status: taskEntry.state,
|
|
117
|
+
position: this._queue.indexOf(taskEntry),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Process the task queue
|
|
123
|
+
*/
|
|
124
|
+
async _processQueue() {
|
|
125
|
+
if (this._processing) return;
|
|
126
|
+
this._processing = true;
|
|
127
|
+
|
|
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;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Execute
|
|
145
|
+
this._running.set(task.id, task);
|
|
146
|
+
this._executeTask(task); // fire and forget
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this._processing = false;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Find next task whose dependencies are satisfied
|
|
154
|
+
*/
|
|
155
|
+
_findNextReady() {
|
|
156
|
+
for (const task of this._queue) {
|
|
157
|
+
if (task.dependsOn.length === 0) return task;
|
|
158
|
+
const allDone = task.dependsOn.every(depId => {
|
|
159
|
+
const dep = this._completed.get(depId);
|
|
160
|
+
return dep && dep.state === TaskState.COMPLETED;
|
|
161
|
+
});
|
|
162
|
+
if (allDone) return task;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Execute a single task
|
|
169
|
+
*/
|
|
170
|
+
async _executeTask(task) {
|
|
171
|
+
task.state = TaskState.RUNNING;
|
|
172
|
+
task.startedAt = Date.now();
|
|
173
|
+
task.attempt++;
|
|
174
|
+
|
|
175
|
+
bus.emit('task.started', { taskId: task.id, attempt: task.attempt, type: task.type });
|
|
176
|
+
|
|
177
|
+
const handler = this._handlers.get(task.type);
|
|
178
|
+
if (!handler) {
|
|
179
|
+
task.state = TaskState.FAILED;
|
|
180
|
+
task.error = { code: 'NO_HANDLER', message: `No handler for task type: ${task.type}` };
|
|
181
|
+
this._finishTask(task);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
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
|
+
);
|
|
205
|
+
|
|
206
|
+
task.state = TaskState.COMPLETED;
|
|
207
|
+
task.result = result;
|
|
208
|
+
task.progress = 100;
|
|
209
|
+
this._stats.completed++;
|
|
210
|
+
bus.emit('task.completed', { taskId: task.id, duration: Date.now() - task.startedAt });
|
|
211
|
+
} catch (err) {
|
|
212
|
+
const isTimeout = err.message.includes('timed out');
|
|
213
|
+
if (isTimeout) this._stats.timedOut++;
|
|
214
|
+
|
|
215
|
+
if (task.retriesLeft > 0 && !isTimeout) {
|
|
216
|
+
// Retry with exponential backoff
|
|
217
|
+
task.retriesLeft--;
|
|
218
|
+
task.state = TaskState.RETRYING;
|
|
219
|
+
this._stats.retried++;
|
|
220
|
+
const backoff = Math.min(1000 * Math.pow(2, task.attempt - 1), 30000);
|
|
221
|
+
bus.emit('task.retrying', { taskId: task.id, attempt: task.attempt, backoff });
|
|
222
|
+
|
|
223
|
+
setTimeout(() => {
|
|
224
|
+
this._running.delete(task.id);
|
|
225
|
+
this._queue.unshift(task); // Add back to front of queue
|
|
226
|
+
this._processQueue();
|
|
227
|
+
}, backoff);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
task.state = TaskState.FAILED;
|
|
232
|
+
task.error = { code: isTimeout ? 'TIMEOUT' : 'EXECUTION_ERROR', message: err.message };
|
|
233
|
+
this._stats.failed++;
|
|
234
|
+
bus.emit('task.failed', { taskId: task.id, error: err.message, attempts: task.attempt });
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
this._finishTask(task);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Finish a task (move to completed buffer)
|
|
242
|
+
*/
|
|
243
|
+
_finishTask(task) {
|
|
244
|
+
task.completedAt = Date.now();
|
|
245
|
+
this._running.delete(task.id);
|
|
246
|
+
|
|
247
|
+
// Store in completed buffer
|
|
248
|
+
this._completed.set(task.id, task);
|
|
249
|
+
if (this._completed.size > this._maxCompleted) {
|
|
250
|
+
const oldest = this._completed.keys().next().value;
|
|
251
|
+
this._completed.delete(oldest);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Process more tasks
|
|
255
|
+
this._processQueue();
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Get task status
|
|
260
|
+
*/
|
|
261
|
+
getTask(taskId) {
|
|
262
|
+
// Check running
|
|
263
|
+
let task = this._running.get(taskId);
|
|
264
|
+
if (!task) {
|
|
265
|
+
// Check queue
|
|
266
|
+
task = this._queue.find(t => t.id === taskId);
|
|
267
|
+
}
|
|
268
|
+
if (!task) {
|
|
269
|
+
// Check completed
|
|
270
|
+
task = this._completed.get(taskId);
|
|
271
|
+
}
|
|
272
|
+
if (!task) return null;
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
id: task.id,
|
|
276
|
+
type: task.type,
|
|
277
|
+
objective: task.objective,
|
|
278
|
+
state: task.state,
|
|
279
|
+
progress: task.progress,
|
|
280
|
+
currentStep: task.currentStep,
|
|
281
|
+
totalSteps: task.steps.length,
|
|
282
|
+
attempt: task.attempt,
|
|
283
|
+
result: task.result,
|
|
284
|
+
error: task.error,
|
|
285
|
+
checkpoints: task.checkpoints.length,
|
|
286
|
+
submittedAt: task.submittedAt,
|
|
287
|
+
startedAt: task.startedAt,
|
|
288
|
+
completedAt: task.completedAt,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* Cancel a task
|
|
294
|
+
*/
|
|
295
|
+
cancel(taskId) {
|
|
296
|
+
// Remove from queue
|
|
297
|
+
const idx = this._queue.findIndex(t => t.id === taskId);
|
|
298
|
+
if (idx !== -1) {
|
|
299
|
+
const task = this._queue.splice(idx, 1)[0];
|
|
300
|
+
task.state = TaskState.CANCELLED;
|
|
301
|
+
task.completedAt = Date.now();
|
|
302
|
+
this._completed.set(task.id, task);
|
|
303
|
+
this._stats.cancelled++;
|
|
304
|
+
bus.emit('task.cancelled', { taskId, reason: 'user' });
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Mark running task as cancelled (handler must check)
|
|
309
|
+
const running = this._running.get(taskId);
|
|
310
|
+
if (running) {
|
|
311
|
+
running.state = TaskState.CANCELLED;
|
|
312
|
+
this._stats.cancelled++;
|
|
313
|
+
bus.emit('task.cancelled', { taskId, reason: 'user' });
|
|
314
|
+
return true;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Pause a running task (handler must support this)
|
|
322
|
+
*/
|
|
323
|
+
pause(taskId) {
|
|
324
|
+
const task = this._running.get(taskId);
|
|
325
|
+
if (task && task.state === TaskState.RUNNING) {
|
|
326
|
+
task.state = TaskState.PAUSED;
|
|
327
|
+
bus.emit('task.paused', { taskId });
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Resume a paused task
|
|
335
|
+
*/
|
|
336
|
+
resume(taskId) {
|
|
337
|
+
const task = this._running.get(taskId);
|
|
338
|
+
if (task && task.state === TaskState.PAUSED) {
|
|
339
|
+
task.state = TaskState.RUNNING;
|
|
340
|
+
bus.emit('task.resumed', { taskId });
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
return false;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get scheduler stats
|
|
348
|
+
*/
|
|
349
|
+
getStats() {
|
|
350
|
+
return {
|
|
351
|
+
...this._stats,
|
|
352
|
+
queueSize: this._queue.length,
|
|
353
|
+
runningCount: this._running.size,
|
|
354
|
+
completedBufferSize: this._completed.size,
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* List tasks by state
|
|
360
|
+
*/
|
|
361
|
+
listTasks(state, limit = 50) {
|
|
362
|
+
const tasks = [];
|
|
363
|
+
|
|
364
|
+
if (!state || state === TaskState.QUEUED) {
|
|
365
|
+
for (const t of this._queue.slice(0, limit)) {
|
|
366
|
+
tasks.push(this.getTask(t.id));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!state || state === TaskState.RUNNING) {
|
|
370
|
+
for (const [, t] of this._running) {
|
|
371
|
+
if (tasks.length >= limit) break;
|
|
372
|
+
tasks.push(this.getTask(t.id));
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
if (!state || [TaskState.COMPLETED, TaskState.FAILED, TaskState.CANCELLED].includes(state)) {
|
|
376
|
+
for (const [, t] of this._completed) {
|
|
377
|
+
if (tasks.length >= limit) break;
|
|
378
|
+
if (!state || t.state === state) tasks.push(this.getTask(t.id));
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return tasks.slice(0, limit);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function _withTimeout(promise, ms) {
|
|
387
|
+
if (!ms || ms <= 0) return promise;
|
|
388
|
+
return new Promise((resolve, reject) => {
|
|
389
|
+
const timer = setTimeout(() => reject(new Error(`Task timed out after ${ms}ms`)), ms);
|
|
390
|
+
promise.then(r => { clearTimeout(timer); resolve(r); })
|
|
391
|
+
.catch(e => { clearTimeout(timer); reject(e); });
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
module.exports = { Scheduler, TaskState };
|